Fix mobile edge rounding: precise content-size grid + epsilon; keep center alignment; captions under icons
This commit is contained in:
parent
c4a5bff4a3
commit
f2c01a4b9a
8 changed files with 686 additions and 153 deletions
75
support.html
75
support.html
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
<header class="site-header" id="siteHeader" role="banner">
|
||||
<a class="brand" href="/">LASER EVERYTHING</a>
|
||||
<!-- Optional: <form class="search-bar" role="search"><input type="search" placeholder="Search…"/></form> -->
|
||||
</header>
|
||||
|
||||
<main id="grid" class="grid" aria-label="Masonry menu"></main>
|
||||
|
|
@ -20,7 +21,7 @@
|
|||
Config default: /configs/index.json (override with ?config=name)
|
||||
====================================================== */
|
||||
(async () => {
|
||||
/* ---------- tiny seeded RNG (deterministic) ---------- */
|
||||
/* ---------- tiny seeded RNG ---------- */
|
||||
function xmur3(str){
|
||||
let h = 1779033703 ^ str.length;
|
||||
for (let i=0;i<str.length;i++){
|
||||
|
|
@ -54,10 +55,10 @@
|
|||
return u.pathname + u.search;
|
||||
}
|
||||
|
||||
// Default config (your JSON can override any/all).
|
||||
// Default config (your JSON overrides)
|
||||
let cfg = {
|
||||
brand: "LASER EVERYTHING",
|
||||
grid: { gap: 12, fit: "stretch" }, // fit: 'stretch' | 'fitCells'
|
||||
grid: { gap: 12, fit: "stretch" },
|
||||
reveal: { durationMs: 2600, revertDelayMs: 700 },
|
||||
hint: {
|
||||
enabled: true,
|
||||
|
|
@ -72,7 +73,7 @@
|
|||
offsetAmpPct: 35,
|
||||
featherPct: 0
|
||||
},
|
||||
layout: { respectConfigOrder: false }, // <— override knob
|
||||
layout: { respectConfigOrder: false },
|
||||
tiles: []
|
||||
};
|
||||
|
||||
|
|
@ -125,7 +126,6 @@
|
|||
a.href = t.href || '#';
|
||||
a.setAttribute('aria-label', t.title || ('Tile ' + (i+1)));
|
||||
|
||||
/* Use config color everywhere (prod aesthetic) */
|
||||
if (t.color) a.style.setProperty('--color', t.color);
|
||||
|
||||
if (t.image){
|
||||
|
|
@ -139,7 +139,7 @@
|
|||
// Cover (wipe)
|
||||
const cover = document.createElement('div'); cover.className='cover'; a.appendChild(cover);
|
||||
|
||||
// Hint layer (above cover; visible when hidden)
|
||||
// Hint layer
|
||||
if ((cfg.hint?.enabled!==false) && (t.hintTitle || t.title)){
|
||||
const hintLayer = document.createElement('div');
|
||||
hintLayer.className = 'hint-layer';
|
||||
|
|
@ -147,9 +147,7 @@
|
|||
hintLayer.style.setProperty('--hint-feather', (cfg.hint.featherPct|0) + '%');
|
||||
}
|
||||
|
||||
const stack = document.createElement('div');
|
||||
stack.className = 'hint-stack';
|
||||
hintLayer.appendChild(stack);
|
||||
const stack = document.createElement('div'); stack.className = 'hint-stack'; hintLayer.appendChild(stack);
|
||||
|
||||
const seed = 'row|' + (t.title || ('tile'+i));
|
||||
const rnd = rngFrom(seed);
|
||||
|
|
@ -165,16 +163,14 @@
|
|||
const row = document.createElement('div');
|
||||
row.className = 'hint-row';
|
||||
row.textContent = lineText;
|
||||
|
||||
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
|
||||
row.dataset.offsetScale = String(offs);
|
||||
|
||||
stack.appendChild(row);
|
||||
}
|
||||
a.appendChild(hintLayer);
|
||||
}
|
||||
|
||||
// Icon layer
|
||||
// Icon layer + caption (hidden state)
|
||||
const iconL=document.createElement('div'); iconL.className='layer icon-layer';
|
||||
if (t.icon){
|
||||
if (/\.(png|svg|jpg|jpeg|webp|gif)$/i.test(t.icon)) {
|
||||
|
|
@ -183,9 +179,13 @@
|
|||
const span=document.createElement('span'); span.className='icon icon-text'; span.textContent=String(t.icon); iconL.appendChild(span);
|
||||
}
|
||||
}
|
||||
const caption=document.createElement('div');
|
||||
caption.className='icon-caption';
|
||||
caption.textContent = t.title || ('Tile ' + (i+1));
|
||||
iconL.appendChild(caption);
|
||||
a.appendChild(iconL);
|
||||
|
||||
// Text layer
|
||||
// Text layer (revealed)
|
||||
const textL=document.createElement('div'); textL.className='layer text-layer';
|
||||
const wrap=document.createElement('div');
|
||||
const k=document.createElement('div'); k.className='kicker'; k.textContent=t.kicker||''; if(!k.textContent) k.style.display='none'; wrap.appendChild(k);
|
||||
|
|
@ -193,7 +193,7 @@
|
|||
const d=document.createElement('p'); d.className='desc'; d.textContent=((t.description??t.desc)??''); if(!d.textContent.trim()) d.style.display='none'; wrap.appendChild(d);
|
||||
textL.appendChild(wrap); a.appendChild(textL);
|
||||
|
||||
// Sizing (raw spans from config)
|
||||
// Sizing
|
||||
const span = parseSpan(t.size || 1);
|
||||
a.dataset.w = String(Math.max(1, Math.min(6, span.w)));
|
||||
a.dataset.h = String(Math.max(1, Math.min(6, span.h)));
|
||||
|
|
@ -201,7 +201,7 @@
|
|||
tiles.push(a);
|
||||
});
|
||||
|
||||
/* ----- ORDER: largest-first by default; allow override to keep JSON order ----- */
|
||||
/* ----- ORDER: largest-first (default) vs config order ----- */
|
||||
const respectOrder = !!(cfg.layout && cfg.layout.respectConfigOrder);
|
||||
const ordered = respectOrder ? tiles.slice()
|
||||
: tiles.slice().sort((a,b)=>{
|
||||
|
|
@ -217,27 +217,34 @@
|
|||
grid.textContent = '';
|
||||
grid.appendChild(frag2);
|
||||
|
||||
/* ---------- layout (header-aware) ---------- */
|
||||
/* ---------- layout (header-aware, edge-rounding safe) ---------- */
|
||||
function layout(){
|
||||
// viewport and header height
|
||||
const W = innerWidth;
|
||||
const H = innerHeight;
|
||||
const VV = visualViewport;
|
||||
const vh = (VV && VV.height) ? VV.height : H;
|
||||
|
||||
const header = document.getElementById('siteHeader');
|
||||
const HH = Math.max(0, vh - (header ? header.getBoundingClientRect().height : 0));
|
||||
const HH = Math.max(1, H - (header ? header.getBoundingClientRect().height : 0));
|
||||
|
||||
// gap
|
||||
const gap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--gap')) || 0;
|
||||
|
||||
// USE CONTENT BOX SIZES (padding removed) to avoid last-column overflow
|
||||
const contentW = W - gap * 2;
|
||||
const contentH = HH - gap * 2;
|
||||
|
||||
const fit = (cfg.grid?.fit || 'stretch').toLowerCase();
|
||||
|
||||
// total tile area
|
||||
const S = ordered.reduce((s, el)=> s + (+el.dataset.w * +el.dataset.h), 0) || 1;
|
||||
const A = W / (HH || 1);
|
||||
const A = contentW / (contentH || 1);
|
||||
let cols = Math.max(1, Math.round(Math.sqrt(S * A)));
|
||||
let rows = Math.max(1, Math.ceil(S / cols));
|
||||
|
||||
// square unit that fits both axes, using content sizes
|
||||
const unitFor = (c, r) => Math.max(1, Math.min(
|
||||
Math.floor((W - gap*(c+1)) / c),
|
||||
Math.floor((HH - gap*(r+1)) / r)
|
||||
Math.floor((contentW - gap * (c - 1)) / c),
|
||||
Math.floor((contentH - gap * (r - 1)) / r)
|
||||
));
|
||||
|
||||
let best = { cols, rows, unit: unitFor(cols, rows), holes: Math.max(0, cols*rows - S) };
|
||||
|
|
@ -252,25 +259,29 @@
|
|||
grid.style.gap = gap + 'px';
|
||||
grid.style.padding = gap + 'px';
|
||||
|
||||
// sub-pixel epsilon to guarantee no overflow in final column/row
|
||||
const EPS = 0.75;
|
||||
|
||||
if (fit === 'stretch'){
|
||||
const unitW = (W - gap * (best.cols + 1)) / best.cols;
|
||||
const unitH = (HH - gap * (best.rows + 1)) / best.rows;
|
||||
const unitW = (contentW - gap * (best.cols - 1)) / best.cols - EPS;
|
||||
const unitH = (contentH - gap * (best.rows - 1)) / best.rows - EPS;
|
||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
|
||||
grid.style.gridAutoRows = `${unitH}px`;
|
||||
grid.style.gridAutoRows = `${unitH}px`;
|
||||
} else {
|
||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${best.unit}px)`;
|
||||
grid.style.gridAutoRows = `${best.unit}px`;
|
||||
const unit = best.unit - EPS;
|
||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unit}px)`;
|
||||
grid.style.gridAutoRows = `${unit}px`;
|
||||
}
|
||||
|
||||
ordered.forEach(n=>{
|
||||
/* Phone-only span clamp to keep everything on one page */
|
||||
// phone span clamp
|
||||
const isPhone = innerWidth <= 520;
|
||||
const w = Math.min(+n.dataset.w, isPhone ? 2 : +n.dataset.w);
|
||||
const h = Math.min(+n.dataset.h, isPhone ? 2 : +n.dataset.h);
|
||||
n.style.gridColumn = `span ${w}`;
|
||||
n.style.gridRow = `span ${h}`;
|
||||
|
||||
/* Deterministic wipe dir (prod = 140%) */
|
||||
// deterministic wipe
|
||||
const title = n.querySelector('.title')?.textContent || '';
|
||||
const R = rngFrom('wipe|'+title);
|
||||
const dirs = [[1,0],[-1,0],[0,-1],[0,1],[1,-1],[-1,-1],[-1,1],[1,1]];
|
||||
|
|
@ -278,7 +289,7 @@
|
|||
n.style.setProperty('--dx', (dx*140)+'%');
|
||||
n.style.setProperty('--dy', (dy*140)+'%');
|
||||
|
||||
/* Recompute hint row offsets using current tile width */
|
||||
// hint offsets
|
||||
const tw = n.getBoundingClientRect().width;
|
||||
n.querySelectorAll('.hint-row').forEach(row=>{
|
||||
const scale = parseFloat(row.dataset.offsetScale || '0');
|
||||
|
|
@ -291,8 +302,8 @@
|
|||
addEventListener('resize', resched);
|
||||
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
|
||||
|
||||
// JS-assisted reveal class only
|
||||
ordered.forEach(tile=>{
|
||||
// js-assisted reveal class
|
||||
document.querySelectorAll('.tile').forEach(tile=>{
|
||||
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
|
||||
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
|
||||
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue