Fix: mobile clamp + packing; restore prod aesthetics (hint/icon layering, vivid tile colors, order override)

This commit is contained in:
makearmy 2025-08-24 18:13:24 -04:00
parent 378233eb8a
commit 814e7a89ce
3 changed files with 96 additions and 69 deletions

View file

@ -1,8 +1,13 @@
{
"brand": "LASER EVERYTHING",
"grid": { "gap": 12, "fit": "stretch" },
"reveal": { "durationMs": 2600, "revertDelayMs": 700 },
"grid": {
"gap": 12,
"fit": "stretch"
},
"reveal": {
"durationMs": 2600,
"revertDelayMs": 700
},
"hint": {
"enabled": true,
"rows": 24,
@ -11,12 +16,13 @@
"opacity": 0.02,
"angleDeg": -12,
"rowGapPx": 8,
"spacing": "\u00A0",
"spacing": "\u00a0",
"planeScale": 3,
"offsetAmpPct": 35,
"featherPct": 0
},
"layout": { "respectConfigOrder": false
},
"tiles": [
{
"title": "Training",

View file

@ -17,10 +17,8 @@
<script>
/* ======================================================
Deterministic tiles + “wrapping paper” hints (SVG-free)
Hint rows are long text lines on a huge plane; tile clips.
Config default: /configs/index.json (override with ?config=name)
====================================================== */
(async () => {
/* ---------- tiny seeded RNG (deterministic) ---------- */
function xmur3(str){
@ -56,26 +54,25 @@
return u.pathname + u.search;
}
// Default config (simple & clear). Your JSON can override any/all.
// Default config (your JSON can override any/all).
let cfg = {
brand: "LASER EVERYTHING",
grid: { gap: 12, fit: "stretch" }, // fit: 'stretch' | 'fitCells'
reveal: { durationMs: 2600, revertDelayMs: 700 },
hint: {
enabled: true,
rows: 5, // number of rows
textCase: "upper", // 'upper' | 'lower' | 'none'
fontPx: 16, // base size in px
rows: 5,
textCase: "upper",
fontPx: 16,
opacity: 0.12,
angleDeg: -12, // rotation of the hint plane
rowGapPx: 8, // vertical gap between rows
spacing: "\u00A0\u00A0\u00A0",// between repeats (use NBSP or your string)
planeScale: 3, // plane size vs tile (3 = 300% both axes)
offsetAmpPct: 35, // max per-row horizontal offset (% of tile width)
featherPct: 0 // 0..10 soft mask on extreme left/right (optional)
angleDeg: -12,
rowGapPx: 8,
spacing: "\u00A0\u00A0\u00A0",
planeScale: 3,
offsetAmpPct: 35,
featherPct: 0
},
layout: { respectConfigOrder: false }, // <— override knob
tiles: []
};
@ -88,7 +85,7 @@
/* ---------- apply brand + CSS vars ---------- */
if (cfg.brand) document.querySelector('.brand').textContent = cfg.brand;
if (cfg.grid?.gap != null) document.documentElement.style.setProperty('--gap', (cfg.grid.gap|0)+'px');
if (cfg.grid?.gap != null) document.documentElement.style.setProperty('--gap', (cfg.grid.gap|0)+'px');
if (cfg.reveal?.durationMs != null) document.documentElement.style.setProperty('--reveal-ms', (cfg.reveal.durationMs|0)+'ms');
// Hint CSS vars
@ -98,7 +95,8 @@
root.style.setProperty('--hint-angle', (H.angleDeg!=null ? H.angleDeg : -12) + 'deg');
root.style.setProperty('--hint-size', (H.fontPx!=null ? (H.fontPx|0)+'px' : '16px'));
root.style.setProperty('--hint-row-gap', (H.rowGapPx!=null ? (H.rowGapPx|0)+'px' : '8px'));
root.style.setProperty('--hint-plane', (H.planeScale!=null ? +H.planeScale : 3));
root.style.setProperty('--hint-plane', String(H.planeScale!=null ? +H.planeScale : 3));
root.style.setProperty('--hint-feather', (H.featherPct!=null ? +H.featherPct : 0) + '%');
const grid = document.getElementById('grid');
@ -118,14 +116,16 @@
}
/* ---------- build tiles ---------- */
const frag = document.createDocumentFragment();
const tiles = [];
const frag = document.createDocumentFragment();
(cfg.tiles||[]).forEach((t,i)=>{
const a = document.createElement('a');
a.className='tile';
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){
@ -136,10 +136,10 @@
a.style.backgroundPosition=t.bgPos||'center';
}
// Wipe cover
// Cover (wipe)
const cover = document.createElement('div'); cover.className='cover'; a.appendChild(cover);
// ===== Hint layer (novel approach) =====
// Hint layer (above cover; visible when hidden)
if ((cfg.hint?.enabled!==false) && (t.hintTitle || t.title)){
const hintLayer = document.createElement('div');
hintLayer.className = 'hint-layer';
@ -149,35 +149,28 @@
const stack = document.createElement('div');
stack.className = 'hint-stack';
stack.style.setProperty('--plane-scale', String(cfg.hint?.planeScale ?? 3));
hintLayer.appendChild(stack);
// Prepare deterministic RNG per tile
const seed = 'row|' + (t.title || ('tile'+i));
const rnd = rngFrom(seed);
// Compose very long content (no measuring).
const baseTitle = titleForHint(t.hintTitle || t.title || '');
const spacing = (cfg.hint?.spacing ?? '\u00A0\u00A0\u00A0');
const chunk = (baseTitle + spacing);
const repeats = 240; // big enough that it ALWAYS bleeds past edges
const repeats = 240;
const lineText = chunk.repeat(repeats);
// Build rows, centered in the big plane, with deterministic offsets.
const rows = Math.max(1, cfg.hint?.rows|0 || 3);
for (let r=0; r<rows; r++){
const row = document.createElement('div');
row.className = 'hint-row';
row.textContent = lineText;
// offset (px) = (random[-1..1] * amp% * tileWidth)
// we read tile width later (in layout), so set a data attribute now:
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
row.dataset.offsetScale = String(offs); // fraction of tile width
row.dataset.offsetScale = String(offs);
stack.appendChild(row);
}
a.appendChild(hintLayer);
}
@ -200,24 +193,36 @@
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
// Sizing (raw spans from config)
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)));
frag.appendChild(a);
tiles.push(a);
});
grid.textContent = '';
grid.appendChild(frag);
/* ----- ORDER: largest-first by default; allow override to keep JSON order ----- */
const respectOrder = !!(cfg.layout && cfg.layout.respectConfigOrder);
const ordered = respectOrder ? tiles.slice()
: tiles.slice().sort((a,b)=>{
const area = (+b.dataset.w * +b.dataset.h) - (+a.dataset.w * +a.dataset.h);
if (area) return area;
const ta = a.querySelector('.title')?.textContent || '';
const tb = b.querySelector('.title')?.textContent || '';
return ta.localeCompare(tb);
});
/* ---------- layout (header-aware, deterministic wipe) ---------- */
const frag2 = document.createDocumentFragment();
ordered.forEach(n => frag2.appendChild(n));
grid.textContent = '';
grid.appendChild(frag2);
/* ---------- layout (header-aware) ---------- */
function layout(){
const W = innerWidth;
const H = innerHeight;
const V = visualViewport;
const vh = (V && V.height) ? V.height : H;
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));
@ -225,7 +230,7 @@
const gap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--gap')) || 0;
const fit = (cfg.grid?.fit || 'stretch').toLowerCase();
const S = tiles.reduce((s, el)=> s + (+el.dataset.w * +el.dataset.h), 0) || 1;
const S = ordered.reduce((s, el)=> s + (+el.dataset.w * +el.dataset.h), 0) || 1;
const A = W / (HH || 1);
let cols = Math.max(1, Math.round(Math.sqrt(S * A)));
let rows = Math.max(1, Math.ceil(S / cols));
@ -235,11 +240,13 @@
Math.floor((HH - gap*(r+1)) / r)
));
let best = { cols, rows, unit: unitFor(cols, rows) };
let best = { cols, rows, unit: unitFor(cols, rows), holes: Math.max(0, cols*rows - S) };
for (let c=Math.max(1, cols-3); c<=cols+3; c++){
const r = Math.max(1, Math.ceil(S / c));
const u = unitFor(c, r);
if (u > best.unit || (u === best.unit && r < best.rows)) best = { cols:c, rows:r, unit:u };
const h = Math.max(0, c*r - S);
const better = (u > best.unit) || (u === best.unit && h < best.holes) || (u === best.unit && h === best.holes && r < best.rows);
if (better) best = { cols:c, rows:r, unit:u, holes:h };
}
grid.style.gap = gap + 'px';
@ -255,11 +262,15 @@
grid.style.gridAutoRows = `${best.unit}px`;
}
tiles.forEach(n=>{
n.style.gridColumn = `span ${n.dataset.w}`;
n.style.gridRow = `span ${n.dataset.h}`;
ordered.forEach(n=>{
/* Phone-only span clamp to keep everything on one page */
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}`;
// Resolve deterministic wipe dir (seeded by title)
/* Deterministic wipe dir (prod = 140%) */
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]];
@ -267,12 +278,11 @@
n.style.setProperty('--dx', (dx*140)+'%');
n.style.setProperty('--dy', (dy*140)+'%');
// Update row pixel offsets based on current tile width
/* Recompute hint row offsets using current tile width */
const tw = n.getBoundingClientRect().width;
n.querySelectorAll('.hint-row').forEach(row=>{
const scale = parseFloat(row.dataset.offsetScale || '0'); // [-1..1] * amp%
const px = scale * tw; // px offset
row.style.setProperty('--row-offset-px', px + 'px');
const scale = parseFloat(row.dataset.offsetScale || '0');
row.style.setProperty('--row-offset-px', (scale * tw) + 'px');
});
});
}
@ -282,7 +292,7 @@
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
// JS-assisted reveal class only
tiles.forEach(tile=>{
ordered.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'));

View file

@ -24,6 +24,7 @@ body{
font:16px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial;
overflow:hidden; /* tiles view */
}
/* make sure <a> tiles never pick UA link blue */
a{ color:inherit; text-decoration:none }
/* ===== Header ===== */
@ -54,23 +55,35 @@ a{ color:inherit; text-decoration:none }
/* ===== Tiles ===== */
.tile{
position:relative; display:block; overflow:hidden;
/* Prod aesthetic: bright config color is the actual tile bg */
background:var(--color,#1f2937);
/* subtle frame */
border:1px solid rgba(255,255,255,.08);
transition:opacity .55s ease, transform .55s ease;
}
/* Cover sits ABOVE text (to hide it until reveal) and BELOW hint+icon */
.cover{
position:absolute; inset:0; z-index:2; background:#242424;
transform:translate(0,0);
transition:transform var(--reveal-ms) cubic-bezier(.22,.8,.18,1);
pointer-events:none; will-change:transform;
}
/* Layer stack matches prod look:
* 1 = text, 2 = cover, 3 = hint (visible when hidden), 4 = icon (visible when hidden) */
.layer,.cover{
position:absolute; inset:0; display:grid; place-items:center; justify-items:center;
text-align:center; padding:clamp(12px,3vh,28px);
transition:opacity .55s ease, transform .55s ease; pointer-events:none;
}
.text-layer{ z-index:1; opacity:0; transform:translateY(8px) }
.hint-layer{ z-index:3; opacity:var(--hint-opacity); }
.icon-layer{ z-index:4; opacity:1 }
.icon{ width:clamp(36px,9vh,110px); height:auto; object-fit:contain }
.icon.icon-text{ width:auto; font-size:clamp(24px,10vh,120px); line-height:1 }
.kicker{ font-weight:900; letter-spacing:.22em; text-transform:uppercase; font-size:12px; opacity:.95; margin:0 0 .25em }
@ -78,27 +91,32 @@ a{ color:inherit; text-decoration:none }
.desc{ margin:.45em 0 0; opacity:.92; font-size:14px; max-width:min(72ch,90%) }
.text-layer>div{ display:flex; flex-direction:column; align-items:center }
/* Reveal states */
/* Reveal: slide the cover off; fade icon out; fade text in; hide hint */
.tile:hover .cover,
.tile:focus-visible .cover,
.tile.js-reveal .cover{ transform:translate(var(--dx),var(--dy)) }
.tile:hover .icon-layer,
.tile:focus-visible .icon-layer,
.tile.js-reveal .icon-layer{ opacity:0; transform:scale(.965) }
.tile:hover .text-layer,
.tile:focus-visible .text-layer,
.tile.js-reveal .text-layer{ opacity:1; transform:none }
/* Hide hint during reveal for the flip */
.tile:hover .hint-layer,
.tile:focus-visible .hint-layer,
.tile.js-reveal .hint-layer{ opacity:0 }
@media (prefers-reduced-motion:reduce){
.tile,.cover,.layer{ transition:none }
}
/* ===== Hint layer (novel approach) =====
* A huge plane rotated & centered. Rows are long lines of text.
* The tile clips the plane, so text can be sliced mid-word at edges. */
/* ===== Hint layer geometry (unchanged from prod) ===== */
.hint-layer{
position:absolute; inset:0; z-index:3; pointer-events:none;
display:block; opacity:var(--hint-opacity);
position:absolute; inset:0; pointer-events:none;
display:block;
/* optional soft fade at extreme sides */
-webkit-mask-image:linear-gradient(to right,
rgba(0,0,0,0) var(--hint-feather),
@ -122,19 +140,13 @@ a{ color:inherit; text-decoration:none }
}
.hint-row{
position:relative; display:block;
/* make the line VERY long; no measuring required */
white-space:nowrap; letter-spacing:.22em; line-height:1;
font-weight:900; text-transform:uppercase; font-size:var(--hint-size);
transform:translateX(var(--row-offset-px, 0px));
/* the content string itself is repeated in JS so it actually fills */
}
.hint-line{ opacity:1 } /* keep lines solid */
/* Hide hint during reveal for the “flip” effect */
.tile:hover .hint-layer,
.tile:focus-visible .hint-layer,
.tile.js-reveal .hint-layer{ opacity:0 }
/* ===== Static pages (unchanged from prior work) ===== */
/* ===== Static pages (from prod) ===== */
body.page{
min-height:100vh; margin:0; color:var(--fg); background:var(--bg);
overflow-y:auto; overflow-x:hidden;
@ -189,5 +201,4 @@ body.page{
.title{ font-size:clamp(16px,5.2vw,22px) }
.desc{ font-size:13px }
.icon{ width:clamp(28px,11vh,90px) }
.back-btn,.btn{ padding:.55rem .85rem }
}