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
|
|
@ -83,17 +83,6 @@
|
||||||
"bgFit": "cover",
|
"bgFit": "cover",
|
||||||
"bgPos": "center",
|
"bgPos": "center",
|
||||||
"description": "Discord is where you want to be. Active, friendly and intelligent community members await you."
|
"description": "Discord is where you want to be. Active, friendly and intelligent community members await you."
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Fediverse",
|
|
||||||
"href": "https://makearmy.io",
|
|
||||||
"icon": "/icons/fediverse.png",
|
|
||||||
"image": "",
|
|
||||||
"color": "#1289a1",
|
|
||||||
"size": "2x1",
|
|
||||||
"bgFit": "cover",
|
|
||||||
"bgPos": "center",
|
|
||||||
"description": "We run a wide range of fediverse instances. Check out the list at MakeArmy!"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
"href": "https://www.patreon.com/LaserEverything",
|
"href": "https://www.patreon.com/LaserEverything",
|
||||||
"icon": "/icons/patreon.png",
|
"icon": "/icons/patreon.png",
|
||||||
"color": "#f39a31",
|
"color": "#f39a31",
|
||||||
"size": 1,
|
"size": "2x1",
|
||||||
"bgFit": "cover",
|
"bgFit": "cover",
|
||||||
"bgPos": "center",
|
"bgPos": "center",
|
||||||
"description": "Mainstream supporter platform for creators."
|
"description": "Mainstream supporter platform for creators."
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
"icon": "/icons/liberapay.png",
|
"icon": "/icons/liberapay.png",
|
||||||
"image": "",
|
"image": "",
|
||||||
"color": "#134823",
|
"color": "#134823",
|
||||||
"size": "2x1",
|
"size": "1",
|
||||||
"bgFit": "cover",
|
"bgFit": "cover",
|
||||||
"bgPos": "center",
|
"bgPos": "center",
|
||||||
"description": "FOSS and Privacy First supporter platform for creators."
|
"description": "FOSS and Privacy First supporter platform for creators."
|
||||||
|
|
|
||||||
75
index.html
75
index.html
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
<header class="site-header" id="siteHeader" role="banner">
|
<header class="site-header" id="siteHeader" role="banner">
|
||||||
<a class="brand" href="/">LASER EVERYTHING</a>
|
<a class="brand" href="/">LASER EVERYTHING</a>
|
||||||
|
<!-- Optional: <form class="search-bar" role="search"><input type="search" placeholder="Search…"/></form> -->
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main id="grid" class="grid" aria-label="Masonry menu"></main>
|
<main id="grid" class="grid" aria-label="Masonry menu"></main>
|
||||||
|
|
@ -20,7 +21,7 @@
|
||||||
Config default: /configs/index.json (override with ?config=name)
|
Config default: /configs/index.json (override with ?config=name)
|
||||||
====================================================== */
|
====================================================== */
|
||||||
(async () => {
|
(async () => {
|
||||||
/* ---------- tiny seeded RNG (deterministic) ---------- */
|
/* ---------- tiny seeded RNG ---------- */
|
||||||
function xmur3(str){
|
function xmur3(str){
|
||||||
let h = 1779033703 ^ str.length;
|
let h = 1779033703 ^ str.length;
|
||||||
for (let i=0;i<str.length;i++){
|
for (let i=0;i<str.length;i++){
|
||||||
|
|
@ -54,10 +55,10 @@
|
||||||
return u.pathname + u.search;
|
return u.pathname + u.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default config (your JSON can override any/all).
|
// Default config (your JSON overrides)
|
||||||
let cfg = {
|
let cfg = {
|
||||||
brand: "LASER EVERYTHING",
|
brand: "LASER EVERYTHING",
|
||||||
grid: { gap: 12, fit: "stretch" }, // fit: 'stretch' | 'fitCells'
|
grid: { gap: 12, fit: "stretch" },
|
||||||
reveal: { durationMs: 2600, revertDelayMs: 700 },
|
reveal: { durationMs: 2600, revertDelayMs: 700 },
|
||||||
hint: {
|
hint: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -72,7 +73,7 @@
|
||||||
offsetAmpPct: 35,
|
offsetAmpPct: 35,
|
||||||
featherPct: 0
|
featherPct: 0
|
||||||
},
|
},
|
||||||
layout: { respectConfigOrder: false }, // <— override knob
|
layout: { respectConfigOrder: false },
|
||||||
tiles: []
|
tiles: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -125,7 +126,6 @@
|
||||||
a.href = t.href || '#';
|
a.href = t.href || '#';
|
||||||
a.setAttribute('aria-label', t.title || ('Tile ' + (i+1)));
|
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.color) a.style.setProperty('--color', t.color);
|
||||||
|
|
||||||
if (t.image){
|
if (t.image){
|
||||||
|
|
@ -139,7 +139,7 @@
|
||||||
// Cover (wipe)
|
// Cover (wipe)
|
||||||
const cover = document.createElement('div'); cover.className='cover'; a.appendChild(cover);
|
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)){
|
if ((cfg.hint?.enabled!==false) && (t.hintTitle || t.title)){
|
||||||
const hintLayer = document.createElement('div');
|
const hintLayer = document.createElement('div');
|
||||||
hintLayer.className = 'hint-layer';
|
hintLayer.className = 'hint-layer';
|
||||||
|
|
@ -147,9 +147,7 @@
|
||||||
hintLayer.style.setProperty('--hint-feather', (cfg.hint.featherPct|0) + '%');
|
hintLayer.style.setProperty('--hint-feather', (cfg.hint.featherPct|0) + '%');
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = document.createElement('div');
|
const stack = document.createElement('div'); stack.className = 'hint-stack'; hintLayer.appendChild(stack);
|
||||||
stack.className = 'hint-stack';
|
|
||||||
hintLayer.appendChild(stack);
|
|
||||||
|
|
||||||
const seed = 'row|' + (t.title || ('tile'+i));
|
const seed = 'row|' + (t.title || ('tile'+i));
|
||||||
const rnd = rngFrom(seed);
|
const rnd = rngFrom(seed);
|
||||||
|
|
@ -165,16 +163,14 @@
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'hint-row';
|
row.className = 'hint-row';
|
||||||
row.textContent = lineText;
|
row.textContent = lineText;
|
||||||
|
|
||||||
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
|
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
|
||||||
row.dataset.offsetScale = String(offs);
|
row.dataset.offsetScale = String(offs);
|
||||||
|
|
||||||
stack.appendChild(row);
|
stack.appendChild(row);
|
||||||
}
|
}
|
||||||
a.appendChild(hintLayer);
|
a.appendChild(hintLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icon layer
|
// Icon layer + caption (hidden state)
|
||||||
const iconL=document.createElement('div'); iconL.className='layer icon-layer';
|
const iconL=document.createElement('div'); iconL.className='layer icon-layer';
|
||||||
if (t.icon){
|
if (t.icon){
|
||||||
if (/\.(png|svg|jpg|jpeg|webp|gif)$/i.test(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 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);
|
a.appendChild(iconL);
|
||||||
|
|
||||||
// Text layer
|
// Text layer (revealed)
|
||||||
const textL=document.createElement('div'); textL.className='layer text-layer';
|
const textL=document.createElement('div'); textL.className='layer text-layer';
|
||||||
const wrap=document.createElement('div');
|
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);
|
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);
|
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);
|
textL.appendChild(wrap); a.appendChild(textL);
|
||||||
|
|
||||||
// Sizing (raw spans from config)
|
// Sizing
|
||||||
const span = parseSpan(t.size || 1);
|
const span = parseSpan(t.size || 1);
|
||||||
a.dataset.w = String(Math.max(1, Math.min(6, span.w)));
|
a.dataset.w = String(Math.max(1, Math.min(6, span.w)));
|
||||||
a.dataset.h = String(Math.max(1, Math.min(6, span.h)));
|
a.dataset.h = String(Math.max(1, Math.min(6, span.h)));
|
||||||
|
|
@ -201,7 +201,7 @@
|
||||||
tiles.push(a);
|
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 respectOrder = !!(cfg.layout && cfg.layout.respectConfigOrder);
|
||||||
const ordered = respectOrder ? tiles.slice()
|
const ordered = respectOrder ? tiles.slice()
|
||||||
: tiles.slice().sort((a,b)=>{
|
: tiles.slice().sort((a,b)=>{
|
||||||
|
|
@ -217,27 +217,34 @@
|
||||||
grid.textContent = '';
|
grid.textContent = '';
|
||||||
grid.appendChild(frag2);
|
grid.appendChild(frag2);
|
||||||
|
|
||||||
/* ---------- layout (header-aware) ---------- */
|
/* ---------- layout (header-aware, edge-rounding safe) ---------- */
|
||||||
function layout(){
|
function layout(){
|
||||||
|
// viewport and header height
|
||||||
const W = innerWidth;
|
const W = innerWidth;
|
||||||
const H = innerHeight;
|
const H = innerHeight;
|
||||||
const VV = visualViewport;
|
|
||||||
const vh = (VV && VV.height) ? VV.height : H;
|
|
||||||
|
|
||||||
const header = document.getElementById('siteHeader');
|
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;
|
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();
|
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 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 cols = Math.max(1, Math.round(Math.sqrt(S * A)));
|
||||||
let rows = Math.max(1, Math.ceil(S / cols));
|
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(
|
const unitFor = (c, r) => Math.max(1, Math.min(
|
||||||
Math.floor((W - gap*(c+1)) / c),
|
Math.floor((contentW - gap * (c - 1)) / c),
|
||||||
Math.floor((HH - gap*(r+1)) / r)
|
Math.floor((contentH - gap * (r - 1)) / r)
|
||||||
));
|
));
|
||||||
|
|
||||||
let best = { cols, rows, unit: unitFor(cols, rows), holes: Math.max(0, cols*rows - S) };
|
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.gap = gap + 'px';
|
||||||
grid.style.padding = 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'){
|
if (fit === 'stretch'){
|
||||||
const unitW = (W - gap * (best.cols + 1)) / best.cols;
|
const unitW = (contentW - gap * (best.cols - 1)) / best.cols - EPS;
|
||||||
const unitH = (HH - gap * (best.rows + 1)) / best.rows;
|
const unitH = (contentH - gap * (best.rows - 1)) / best.rows - EPS;
|
||||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
|
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
|
||||||
grid.style.gridAutoRows = `${unitH}px`;
|
grid.style.gridAutoRows = `${unitH}px`;
|
||||||
} else {
|
} else {
|
||||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${best.unit}px)`;
|
const unit = best.unit - EPS;
|
||||||
grid.style.gridAutoRows = `${best.unit}px`;
|
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unit}px)`;
|
||||||
|
grid.style.gridAutoRows = `${unit}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
ordered.forEach(n=>{
|
ordered.forEach(n=>{
|
||||||
/* Phone-only span clamp to keep everything on one page */
|
// phone span clamp
|
||||||
const isPhone = innerWidth <= 520;
|
const isPhone = innerWidth <= 520;
|
||||||
const w = Math.min(+n.dataset.w, isPhone ? 2 : +n.dataset.w);
|
const w = Math.min(+n.dataset.w, isPhone ? 2 : +n.dataset.w);
|
||||||
const h = Math.min(+n.dataset.h, isPhone ? 2 : +n.dataset.h);
|
const h = Math.min(+n.dataset.h, isPhone ? 2 : +n.dataset.h);
|
||||||
n.style.gridColumn = `span ${w}`;
|
n.style.gridColumn = `span ${w}`;
|
||||||
n.style.gridRow = `span ${h}`;
|
n.style.gridRow = `span ${h}`;
|
||||||
|
|
||||||
/* Deterministic wipe dir (prod = 140%) */
|
// deterministic wipe
|
||||||
const title = n.querySelector('.title')?.textContent || '';
|
const title = n.querySelector('.title')?.textContent || '';
|
||||||
const R = rngFrom('wipe|'+title);
|
const R = rngFrom('wipe|'+title);
|
||||||
const dirs = [[1,0],[-1,0],[0,-1],[0,1],[1,-1],[-1,-1],[-1,1],[1,1]];
|
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('--dx', (dx*140)+'%');
|
||||||
n.style.setProperty('--dy', (dy*140)+'%');
|
n.style.setProperty('--dy', (dy*140)+'%');
|
||||||
|
|
||||||
/* Recompute hint row offsets using current tile width */
|
// hint offsets
|
||||||
const tw = n.getBoundingClientRect().width;
|
const tw = n.getBoundingClientRect().width;
|
||||||
n.querySelectorAll('.hint-row').forEach(row=>{
|
n.querySelectorAll('.hint-row').forEach(row=>{
|
||||||
const scale = parseFloat(row.dataset.offsetScale || '0');
|
const scale = parseFloat(row.dataset.offsetScale || '0');
|
||||||
|
|
@ -291,8 +302,8 @@
|
||||||
addEventListener('resize', resched);
|
addEventListener('resize', resched);
|
||||||
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
|
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
|
||||||
|
|
||||||
// JS-assisted reveal class only
|
// js-assisted reveal class
|
||||||
ordered.forEach(tile=>{
|
document.querySelectorAll('.tile').forEach(tile=>{
|
||||||
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
|
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
|
||||||
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
|
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
|
||||||
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
|
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
|
||||||
|
|
|
||||||
305
index.html.bak
Normal file
305
index.html.bak
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Laser Everything</title>
|
||||||
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="site-header" id="siteHeader" role="banner">
|
||||||
|
<a class="brand" href="/">LASER EVERYTHING</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="grid" class="grid" aria-label="Masonry menu"></main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ======================================================
|
||||||
|
Deterministic tiles + “wrapping paper” hints (SVG-free)
|
||||||
|
Config default: /configs/index.json (override with ?config=name)
|
||||||
|
====================================================== */
|
||||||
|
(async () => {
|
||||||
|
/* ---------- tiny seeded RNG (deterministic) ---------- */
|
||||||
|
function xmur3(str){
|
||||||
|
let h = 1779033703 ^ str.length;
|
||||||
|
for (let i=0;i<str.length;i++){
|
||||||
|
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
|
||||||
|
h = (h<<13) | (h>>>19);
|
||||||
|
}
|
||||||
|
return function(){
|
||||||
|
h = Math.imul(h ^ (h>>>16), 2246822507);
|
||||||
|
h = Math.imul(h ^ (h>>>13), 3266489909);
|
||||||
|
return (h ^= h>>>16) >>> 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function mulberry32(a){
|
||||||
|
return function(){
|
||||||
|
let t = a += 0x6D2B79F5;
|
||||||
|
t = Math.imul(t ^ (t>>>15), t | 1);
|
||||||
|
t ^= t + Math.imul(t ^ (t>>>7), t | 61);
|
||||||
|
return ((t ^ (t>>>14)) >>> 0) / 4294967296;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const rngFrom = (seedStr) => mulberry32(xmur3(seedStr)());
|
||||||
|
|
||||||
|
/* ---------- load config ---------- */
|
||||||
|
function resolveConfigPath(){
|
||||||
|
const q = new URLSearchParams(location.search).get('config');
|
||||||
|
let p = q || '/configs/index.json';
|
||||||
|
if (!/^https?:\/\//i.test(p) && !p.startsWith('/')) p = '/configs/'+p;
|
||||||
|
if (!/\.json(\?|$)/i.test(p)) p += '.json';
|
||||||
|
const u = new URL(p, location.origin);
|
||||||
|
if (u.origin !== location.origin) throw new Error('Cross-origin config not allowed');
|
||||||
|
return u.pathname + u.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
textCase: "upper",
|
||||||
|
fontPx: 16,
|
||||||
|
opacity: 0.12,
|
||||||
|
angleDeg: -12,
|
||||||
|
rowGapPx: 8,
|
||||||
|
spacing: "\u00A0\u00A0\u00A0",
|
||||||
|
planeScale: 3,
|
||||||
|
offsetAmpPct: 35,
|
||||||
|
featherPct: 0
|
||||||
|
},
|
||||||
|
layout: { respectConfigOrder: false }, // <— override knob
|
||||||
|
tiles: []
|
||||||
|
};
|
||||||
|
|
||||||
|
let CONFIG_URL = '/configs/index.json';
|
||||||
|
try { CONFIG_URL = resolveConfigPath(); } catch {}
|
||||||
|
try {
|
||||||
|
const r = await fetch(CONFIG_URL, { cache:'no-store' });
|
||||||
|
if (r.ok) Object.assign(cfg, await r.json());
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
/* ---------- 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.reveal?.durationMs != null) document.documentElement.style.setProperty('--reveal-ms', (cfg.reveal.durationMs|0)+'ms');
|
||||||
|
|
||||||
|
// Hint CSS vars
|
||||||
|
const H = cfg.hint || {};
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.style.setProperty('--hint-opacity', String(H.opacity ?? 0.12));
|
||||||
|
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', String(H.planeScale!=null ? +H.planeScale : 3));
|
||||||
|
root.style.setProperty('--hint-feather', (H.featherPct!=null ? +H.featherPct : 0) + '%');
|
||||||
|
|
||||||
|
const grid = document.getElementById('grid');
|
||||||
|
|
||||||
|
/* ---------- helpers ---------- */
|
||||||
|
function parseSpan(v){
|
||||||
|
if (typeof v==='number') return {w:v,h:v};
|
||||||
|
if (typeof v==='string'){ const m=v.toLowerCase().match(/^(\d+)x(\d+)$/); if (m) return {w:+m[1], h:+m[2]}; }
|
||||||
|
if (v && typeof v==='object') return {w:Math.max(1,+v.w||1), h:Math.max(1,+v.h||1)};
|
||||||
|
return {w:1,h:1};
|
||||||
|
}
|
||||||
|
function titleForHint(title){
|
||||||
|
if (!title) return '';
|
||||||
|
const mode = (cfg.hint?.textCase ?? 'upper').toLowerCase();
|
||||||
|
if (mode === 'upper') return title.toUpperCase();
|
||||||
|
if (mode === 'lower') return title.toLowerCase();
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- build tiles ---------- */
|
||||||
|
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){
|
||||||
|
const src=(t.image.startsWith('/')||t.image.startsWith('http')) ? t.image : ('/images/'+t.image);
|
||||||
|
a.style.backgroundImage='url("'+src+'")';
|
||||||
|
a.style.backgroundSize=t.bgFit||'cover';
|
||||||
|
a.style.backgroundRepeat='no-repeat';
|
||||||
|
a.style.backgroundPosition=t.bgPos||'center';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cover (wipe)
|
||||||
|
const cover = document.createElement('div'); cover.className='cover'; a.appendChild(cover);
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
if ((cfg.hint?.featherPct|0) > 0){
|
||||||
|
hintLayer.style.setProperty('--hint-feather', (cfg.hint.featherPct|0) + '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack = document.createElement('div');
|
||||||
|
stack.className = 'hint-stack';
|
||||||
|
hintLayer.appendChild(stack);
|
||||||
|
|
||||||
|
const seed = 'row|' + (t.title || ('tile'+i));
|
||||||
|
const rnd = rngFrom(seed);
|
||||||
|
|
||||||
|
const baseTitle = titleForHint(t.hintTitle || t.title || '');
|
||||||
|
const spacing = (cfg.hint?.spacing ?? '\u00A0\u00A0\u00A0');
|
||||||
|
const chunk = (baseTitle + spacing);
|
||||||
|
const repeats = 240;
|
||||||
|
const lineText = chunk.repeat(repeats);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
|
||||||
|
row.dataset.offsetScale = String(offs);
|
||||||
|
|
||||||
|
stack.appendChild(row);
|
||||||
|
}
|
||||||
|
a.appendChild(hintLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon layer
|
||||||
|
const iconL=document.createElement('div'); iconL.className='layer icon-layer';
|
||||||
|
if (t.icon){
|
||||||
|
if (/\.(png|svg|jpg|jpeg|webp|gif)$/i.test(t.icon)) {
|
||||||
|
const img=document.createElement('img'); img.className='icon'; img.alt=''; img.src=t.icon; iconL.appendChild(img);
|
||||||
|
} else {
|
||||||
|
const span=document.createElement('span'); span.className='icon icon-text'; span.textContent=String(t.icon); iconL.appendChild(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.appendChild(iconL);
|
||||||
|
|
||||||
|
// Text layer
|
||||||
|
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);
|
||||||
|
const h2=document.createElement('h2'); h2.className='title'; h2.textContent=t.title||('Tile '+(i+1)); wrap.appendChild(h2);
|
||||||
|
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)
|
||||||
|
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)));
|
||||||
|
|
||||||
|
tiles.push(a);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ----- 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 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 gap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--gap')) || 0;
|
||||||
|
const fit = (cfg.grid?.fit || 'stretch').toLowerCase();
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
const unitFor = (c, r) => Math.max(1, Math.min(
|
||||||
|
Math.floor((W - gap*(c+1)) / c),
|
||||||
|
Math.floor((HH - gap*(r+1)) / r)
|
||||||
|
));
|
||||||
|
|
||||||
|
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);
|
||||||
|
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';
|
||||||
|
grid.style.padding = gap + 'px';
|
||||||
|
|
||||||
|
if (fit === 'stretch'){
|
||||||
|
const unitW = (W - gap * (best.cols + 1)) / best.cols;
|
||||||
|
const unitH = (HH - gap * (best.rows + 1)) / best.rows;
|
||||||
|
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
|
||||||
|
grid.style.gridAutoRows = `${unitH}px`;
|
||||||
|
} else {
|
||||||
|
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${best.unit}px)`;
|
||||||
|
grid.style.gridAutoRows = `${best.unit}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
|
||||||
|
/* 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]];
|
||||||
|
const [dx,dy] = dirs[Math.floor(R()*dirs.length)];
|
||||||
|
n.style.setProperty('--dx', (dx*140)+'%');
|
||||||
|
n.style.setProperty('--dy', (dy*140)+'%');
|
||||||
|
|
||||||
|
/* 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');
|
||||||
|
row.style.setProperty('--row-offset-px', (scale * tw) + 'px');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
layout();
|
||||||
|
const resched = () => { clearTimeout(layout._t); layout._t=setTimeout(layout, 50); };
|
||||||
|
addEventListener('resize', resched);
|
||||||
|
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
|
||||||
|
|
||||||
|
// JS-assisted reveal class only
|
||||||
|
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'));
|
||||||
|
tile.addEventListener('focusout', ()=>tile.classList.remove('js-reveal'));
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
75
social.html
75
social.html
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
<header class="site-header" id="siteHeader" role="banner">
|
<header class="site-header" id="siteHeader" role="banner">
|
||||||
<a class="brand" href="/">LASER EVERYTHING</a>
|
<a class="brand" href="/">LASER EVERYTHING</a>
|
||||||
|
<!-- Optional: <form class="search-bar" role="search"><input type="search" placeholder="Search…"/></form> -->
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main id="grid" class="grid" aria-label="Masonry menu"></main>
|
<main id="grid" class="grid" aria-label="Masonry menu"></main>
|
||||||
|
|
@ -20,7 +21,7 @@
|
||||||
Config default: /configs/index.json (override with ?config=name)
|
Config default: /configs/index.json (override with ?config=name)
|
||||||
====================================================== */
|
====================================================== */
|
||||||
(async () => {
|
(async () => {
|
||||||
/* ---------- tiny seeded RNG (deterministic) ---------- */
|
/* ---------- tiny seeded RNG ---------- */
|
||||||
function xmur3(str){
|
function xmur3(str){
|
||||||
let h = 1779033703 ^ str.length;
|
let h = 1779033703 ^ str.length;
|
||||||
for (let i=0;i<str.length;i++){
|
for (let i=0;i<str.length;i++){
|
||||||
|
|
@ -54,10 +55,10 @@
|
||||||
return u.pathname + u.search;
|
return u.pathname + u.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default config (your JSON can override any/all).
|
// Default config (your JSON overrides)
|
||||||
let cfg = {
|
let cfg = {
|
||||||
brand: "LASER EVERYTHING",
|
brand: "LASER EVERYTHING",
|
||||||
grid: { gap: 12, fit: "stretch" }, // fit: 'stretch' | 'fitCells'
|
grid: { gap: 12, fit: "stretch" },
|
||||||
reveal: { durationMs: 2600, revertDelayMs: 700 },
|
reveal: { durationMs: 2600, revertDelayMs: 700 },
|
||||||
hint: {
|
hint: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -72,7 +73,7 @@
|
||||||
offsetAmpPct: 35,
|
offsetAmpPct: 35,
|
||||||
featherPct: 0
|
featherPct: 0
|
||||||
},
|
},
|
||||||
layout: { respectConfigOrder: false }, // <— override knob
|
layout: { respectConfigOrder: false },
|
||||||
tiles: []
|
tiles: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -125,7 +126,6 @@
|
||||||
a.href = t.href || '#';
|
a.href = t.href || '#';
|
||||||
a.setAttribute('aria-label', t.title || ('Tile ' + (i+1)));
|
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.color) a.style.setProperty('--color', t.color);
|
||||||
|
|
||||||
if (t.image){
|
if (t.image){
|
||||||
|
|
@ -139,7 +139,7 @@
|
||||||
// Cover (wipe)
|
// Cover (wipe)
|
||||||
const cover = document.createElement('div'); cover.className='cover'; a.appendChild(cover);
|
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)){
|
if ((cfg.hint?.enabled!==false) && (t.hintTitle || t.title)){
|
||||||
const hintLayer = document.createElement('div');
|
const hintLayer = document.createElement('div');
|
||||||
hintLayer.className = 'hint-layer';
|
hintLayer.className = 'hint-layer';
|
||||||
|
|
@ -147,9 +147,7 @@
|
||||||
hintLayer.style.setProperty('--hint-feather', (cfg.hint.featherPct|0) + '%');
|
hintLayer.style.setProperty('--hint-feather', (cfg.hint.featherPct|0) + '%');
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = document.createElement('div');
|
const stack = document.createElement('div'); stack.className = 'hint-stack'; hintLayer.appendChild(stack);
|
||||||
stack.className = 'hint-stack';
|
|
||||||
hintLayer.appendChild(stack);
|
|
||||||
|
|
||||||
const seed = 'row|' + (t.title || ('tile'+i));
|
const seed = 'row|' + (t.title || ('tile'+i));
|
||||||
const rnd = rngFrom(seed);
|
const rnd = rngFrom(seed);
|
||||||
|
|
@ -165,16 +163,14 @@
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'hint-row';
|
row.className = 'hint-row';
|
||||||
row.textContent = lineText;
|
row.textContent = lineText;
|
||||||
|
|
||||||
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
|
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
|
||||||
row.dataset.offsetScale = String(offs);
|
row.dataset.offsetScale = String(offs);
|
||||||
|
|
||||||
stack.appendChild(row);
|
stack.appendChild(row);
|
||||||
}
|
}
|
||||||
a.appendChild(hintLayer);
|
a.appendChild(hintLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icon layer
|
// Icon layer + caption (hidden state)
|
||||||
const iconL=document.createElement('div'); iconL.className='layer icon-layer';
|
const iconL=document.createElement('div'); iconL.className='layer icon-layer';
|
||||||
if (t.icon){
|
if (t.icon){
|
||||||
if (/\.(png|svg|jpg|jpeg|webp|gif)$/i.test(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 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);
|
a.appendChild(iconL);
|
||||||
|
|
||||||
// Text layer
|
// Text layer (revealed)
|
||||||
const textL=document.createElement('div'); textL.className='layer text-layer';
|
const textL=document.createElement('div'); textL.className='layer text-layer';
|
||||||
const wrap=document.createElement('div');
|
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);
|
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);
|
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);
|
textL.appendChild(wrap); a.appendChild(textL);
|
||||||
|
|
||||||
// Sizing (raw spans from config)
|
// Sizing
|
||||||
const span = parseSpan(t.size || 1);
|
const span = parseSpan(t.size || 1);
|
||||||
a.dataset.w = String(Math.max(1, Math.min(6, span.w)));
|
a.dataset.w = String(Math.max(1, Math.min(6, span.w)));
|
||||||
a.dataset.h = String(Math.max(1, Math.min(6, span.h)));
|
a.dataset.h = String(Math.max(1, Math.min(6, span.h)));
|
||||||
|
|
@ -201,7 +201,7 @@
|
||||||
tiles.push(a);
|
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 respectOrder = !!(cfg.layout && cfg.layout.respectConfigOrder);
|
||||||
const ordered = respectOrder ? tiles.slice()
|
const ordered = respectOrder ? tiles.slice()
|
||||||
: tiles.slice().sort((a,b)=>{
|
: tiles.slice().sort((a,b)=>{
|
||||||
|
|
@ -217,27 +217,34 @@
|
||||||
grid.textContent = '';
|
grid.textContent = '';
|
||||||
grid.appendChild(frag2);
|
grid.appendChild(frag2);
|
||||||
|
|
||||||
/* ---------- layout (header-aware) ---------- */
|
/* ---------- layout (header-aware, edge-rounding safe) ---------- */
|
||||||
function layout(){
|
function layout(){
|
||||||
|
// viewport and header height
|
||||||
const W = innerWidth;
|
const W = innerWidth;
|
||||||
const H = innerHeight;
|
const H = innerHeight;
|
||||||
const VV = visualViewport;
|
|
||||||
const vh = (VV && VV.height) ? VV.height : H;
|
|
||||||
|
|
||||||
const header = document.getElementById('siteHeader');
|
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;
|
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();
|
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 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 cols = Math.max(1, Math.round(Math.sqrt(S * A)));
|
||||||
let rows = Math.max(1, Math.ceil(S / cols));
|
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(
|
const unitFor = (c, r) => Math.max(1, Math.min(
|
||||||
Math.floor((W - gap*(c+1)) / c),
|
Math.floor((contentW - gap * (c - 1)) / c),
|
||||||
Math.floor((HH - gap*(r+1)) / r)
|
Math.floor((contentH - gap * (r - 1)) / r)
|
||||||
));
|
));
|
||||||
|
|
||||||
let best = { cols, rows, unit: unitFor(cols, rows), holes: Math.max(0, cols*rows - S) };
|
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.gap = gap + 'px';
|
||||||
grid.style.padding = 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'){
|
if (fit === 'stretch'){
|
||||||
const unitW = (W - gap * (best.cols + 1)) / best.cols;
|
const unitW = (contentW - gap * (best.cols - 1)) / best.cols - EPS;
|
||||||
const unitH = (HH - gap * (best.rows + 1)) / best.rows;
|
const unitH = (contentH - gap * (best.rows - 1)) / best.rows - EPS;
|
||||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
|
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
|
||||||
grid.style.gridAutoRows = `${unitH}px`;
|
grid.style.gridAutoRows = `${unitH}px`;
|
||||||
} else {
|
} else {
|
||||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${best.unit}px)`;
|
const unit = best.unit - EPS;
|
||||||
grid.style.gridAutoRows = `${best.unit}px`;
|
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unit}px)`;
|
||||||
|
grid.style.gridAutoRows = `${unit}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
ordered.forEach(n=>{
|
ordered.forEach(n=>{
|
||||||
/* Phone-only span clamp to keep everything on one page */
|
// phone span clamp
|
||||||
const isPhone = innerWidth <= 520;
|
const isPhone = innerWidth <= 520;
|
||||||
const w = Math.min(+n.dataset.w, isPhone ? 2 : +n.dataset.w);
|
const w = Math.min(+n.dataset.w, isPhone ? 2 : +n.dataset.w);
|
||||||
const h = Math.min(+n.dataset.h, isPhone ? 2 : +n.dataset.h);
|
const h = Math.min(+n.dataset.h, isPhone ? 2 : +n.dataset.h);
|
||||||
n.style.gridColumn = `span ${w}`;
|
n.style.gridColumn = `span ${w}`;
|
||||||
n.style.gridRow = `span ${h}`;
|
n.style.gridRow = `span ${h}`;
|
||||||
|
|
||||||
/* Deterministic wipe dir (prod = 140%) */
|
// deterministic wipe
|
||||||
const title = n.querySelector('.title')?.textContent || '';
|
const title = n.querySelector('.title')?.textContent || '';
|
||||||
const R = rngFrom('wipe|'+title);
|
const R = rngFrom('wipe|'+title);
|
||||||
const dirs = [[1,0],[-1,0],[0,-1],[0,1],[1,-1],[-1,-1],[-1,1],[1,1]];
|
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('--dx', (dx*140)+'%');
|
||||||
n.style.setProperty('--dy', (dy*140)+'%');
|
n.style.setProperty('--dy', (dy*140)+'%');
|
||||||
|
|
||||||
/* Recompute hint row offsets using current tile width */
|
// hint offsets
|
||||||
const tw = n.getBoundingClientRect().width;
|
const tw = n.getBoundingClientRect().width;
|
||||||
n.querySelectorAll('.hint-row').forEach(row=>{
|
n.querySelectorAll('.hint-row').forEach(row=>{
|
||||||
const scale = parseFloat(row.dataset.offsetScale || '0');
|
const scale = parseFloat(row.dataset.offsetScale || '0');
|
||||||
|
|
@ -291,8 +302,8 @@
|
||||||
addEventListener('resize', resched);
|
addEventListener('resize', resched);
|
||||||
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
|
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
|
||||||
|
|
||||||
// JS-assisted reveal class only
|
// js-assisted reveal class
|
||||||
ordered.forEach(tile=>{
|
document.querySelectorAll('.tile').forEach(tile=>{
|
||||||
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
|
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
|
||||||
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
|
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
|
||||||
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
|
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
|
||||||
|
|
|
||||||
90
styles.css
90
styles.css
|
|
@ -24,14 +24,14 @@ body{
|
||||||
font:16px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial;
|
font:16px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial;
|
||||||
overflow:hidden; /* tiles view */
|
overflow:hidden; /* tiles view */
|
||||||
}
|
}
|
||||||
/* make sure <a> tiles never pick UA link blue */
|
/* prevent UA link blue on tiles */
|
||||||
a{ color:inherit; text-decoration:none }
|
a{ color:inherit; text-decoration:none }
|
||||||
|
|
||||||
/* ===== Header ===== */
|
/* ===== Header ===== */
|
||||||
.site-header{
|
.site-header{
|
||||||
position:fixed; left:0; right:0; top:0;
|
position:fixed; left:0; right:0; top:0;
|
||||||
height:calc(var(--header-h) + var(--safe-top));
|
height:calc(var(--header-h) + var(--safe-top));
|
||||||
display:flex; align-items:center; justify-content:center;
|
display:flex; align-items:center; justify-content:center; gap:12px;
|
||||||
padding:0 clamp(12px,3vw,24px);
|
padding:0 clamp(12px,3vw,24px);
|
||||||
background:rgba(0,0,0,.55); backdrop-filter:blur(6px);
|
background:rgba(0,0,0,.55); backdrop-filter:blur(6px);
|
||||||
border-bottom:1px solid rgba(255,255,255,.10);
|
border-bottom:1px solid rgba(255,255,255,.10);
|
||||||
|
|
@ -42,6 +42,10 @@ a{ color:inherit; text-decoration:none }
|
||||||
font-size:clamp(12px,1.4vw,14px); opacity:.95; text-decoration:none;
|
font-size:clamp(12px,1.4vw,14px); opacity:.95; text-decoration:none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Optional search bar in header */
|
||||||
|
.search-bar{ flex:0 1 480px; max-width:480px; }
|
||||||
|
.search-bar input{ width:100%; }
|
||||||
|
|
||||||
/* ===== Grid ===== */
|
/* ===== Grid ===== */
|
||||||
.grid{
|
.grid{
|
||||||
position:fixed; inset:0;
|
position:fixed; inset:0;
|
||||||
|
|
@ -79,13 +83,29 @@ a{ color:inherit; text-decoration:none }
|
||||||
position:absolute; inset:0; display:grid; place-items:center; justify-items:center;
|
position:absolute; inset:0; display:grid; place-items:center; justify-items:center;
|
||||||
text-align:center; padding:clamp(12px,3vh,28px);
|
text-align:center; padding:clamp(12px,3vh,28px);
|
||||||
transition:opacity .55s ease, transform .55s ease; pointer-events:none;
|
transition:opacity .55s ease, transform .55s ease; pointer-events:none;
|
||||||
|
|
||||||
|
/* NEW: keep centering based on visible interior to avoid edge rounding issues */
|
||||||
|
max-width:100%;
|
||||||
|
max-height:100%;
|
||||||
}
|
}
|
||||||
.text-layer{ z-index:1; opacity:0; transform:translateY(8px) }
|
.text-layer{ z-index:1; opacity:0; transform:translateY(8px) }
|
||||||
.hint-layer{ z-index:3; opacity:var(--hint-opacity); }
|
.hint-layer{ z-index:3; opacity:var(--hint-opacity) }
|
||||||
.icon-layer{ z-index:4; opacity:1 }
|
.icon-layer{ z-index:4; opacity:1 }
|
||||||
|
|
||||||
|
/* Icon + small bold caption under it */
|
||||||
.icon{ width:clamp(36px,9vh,110px); height:auto; object-fit:contain }
|
.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 }
|
.icon.icon-text{ width:auto; font-size:clamp(24px,10vh,120px); line-height:1 }
|
||||||
|
|
||||||
|
.icon-caption{
|
||||||
|
margin-top:.4rem;
|
||||||
|
font-weight:800;
|
||||||
|
font-size:12px;
|
||||||
|
line-height:1.1;
|
||||||
|
max-width:92%;
|
||||||
|
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
|
||||||
.kicker{ font-weight:900; letter-spacing:.22em; text-transform:uppercase; font-size:12px; opacity:.95; margin:0 0 .25em }
|
.kicker{ font-weight:900; letter-spacing:.22em; text-transform:uppercase; font-size:12px; opacity:.95; margin:0 0 .25em }
|
||||||
.title{ margin:0; font-weight:1000; letter-spacing:.2px; font-size:clamp(16px,2.2vw,28px); line-height:1.1 }
|
.title{ margin:0; font-weight:1000; letter-spacing:.2px; font-size:clamp(16px,2.2vw,28px); line-height:1.1 }
|
||||||
.desc{ margin:.45em 0 0; opacity:.92; font-size:14px; max-width:min(72ch,90%) }
|
.desc{ margin:.45em 0 0; opacity:.92; font-size:14px; max-width:min(72ch,90%) }
|
||||||
|
|
@ -104,7 +124,6 @@ a{ color:inherit; text-decoration:none }
|
||||||
.tile:focus-visible .text-layer,
|
.tile:focus-visible .text-layer,
|
||||||
.tile.js-reveal .text-layer{ opacity:1; transform:none }
|
.tile.js-reveal .text-layer{ opacity:1; transform:none }
|
||||||
|
|
||||||
/* Hide hint during reveal for the flip */
|
|
||||||
.tile:hover .hint-layer,
|
.tile:hover .hint-layer,
|
||||||
.tile:focus-visible .hint-layer,
|
.tile:focus-visible .hint-layer,
|
||||||
.tile.js-reveal .hint-layer{ opacity:0 }
|
.tile.js-reveal .hint-layer{ opacity:0 }
|
||||||
|
|
@ -113,11 +132,9 @@ a{ color:inherit; text-decoration:none }
|
||||||
.tile,.cover,.layer{ transition:none }
|
.tile,.cover,.layer{ transition:none }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Hint layer geometry (unchanged from prod) ===== */
|
/* ===== Hint layer geometry ===== */
|
||||||
.hint-layer{
|
.hint-layer{
|
||||||
position:absolute; inset:0; pointer-events:none;
|
position:absolute; inset:0; pointer-events:none; display:block;
|
||||||
display:block;
|
|
||||||
/* optional soft fade at extreme sides */
|
|
||||||
-webkit-mask-image:linear-gradient(to right,
|
-webkit-mask-image:linear-gradient(to right,
|
||||||
rgba(0,0,0,0) var(--hint-feather),
|
rgba(0,0,0,0) var(--hint-feather),
|
||||||
#000 calc(var(--hint-feather) + 1px),
|
#000 calc(var(--hint-feather) + 1px),
|
||||||
|
|
@ -135,8 +152,7 @@ a{ color:inherit; text-decoration:none }
|
||||||
width:calc(var(--hint-plane) * 100%);
|
width:calc(var(--hint-plane) * 100%);
|
||||||
height:calc(var(--hint-plane) * 100%);
|
height:calc(var(--hint-plane) * 100%);
|
||||||
display:flex; flex-direction:column; justify-content:center; align-items:center;
|
display:flex; flex-direction:column; justify-content:center; align-items:center;
|
||||||
gap:var(--hint-row-gap);
|
gap:var(--hint-row-gap); overflow:hidden;
|
||||||
overflow:hidden; /* clip rows to plane; tile clips plane */
|
|
||||||
}
|
}
|
||||||
.hint-row{
|
.hint-row{
|
||||||
position:relative; display:block;
|
position:relative; display:block;
|
||||||
|
|
@ -144,41 +160,19 @@ a{ color:inherit; text-decoration:none }
|
||||||
font-weight:900; text-transform:uppercase; font-size:var(--hint-size);
|
font-weight:900; text-transform:uppercase; font-size:var(--hint-size);
|
||||||
transform:translateX(var(--row-offset-px, 0px));
|
transform:translateX(var(--row-offset-px, 0px));
|
||||||
}
|
}
|
||||||
.hint-line{ opacity:1 } /* keep lines solid */
|
.hint-line{ opacity:1 }
|
||||||
|
|
||||||
/* ===== Static pages (from prod) ===== */
|
/* ===== Static pages preserved ===== */
|
||||||
body.page{
|
body.page{ min-height:100vh; margin:0; color:var(--fg); background:var(--bg); overflow-y:auto; overflow-x:hidden }
|
||||||
min-height:100vh; margin:0; color:var(--fg); background:var(--bg);
|
.page-wrap{ box-sizing:border-box; min-height:100svh; padding:var(--gap); padding-top:calc(var(--gap) + var(--header-h) + var(--safe-top)); padding-bottom:calc(var(--gap) + var(--safe-bottom)); display:grid; grid-template-rows:auto 1fr; gap:var(--gap) }
|
||||||
overflow-y:auto; overflow-x:hidden;
|
|
||||||
}
|
|
||||||
.page-wrap{
|
|
||||||
box-sizing:border-box; min-height:100svh;
|
|
||||||
padding:var(--gap);
|
|
||||||
padding-top:calc(var(--gap) + var(--header-h) + var(--safe-top));
|
|
||||||
padding-bottom:calc(var(--gap) + var(--safe-bottom));
|
|
||||||
display:grid; grid-template-rows:auto 1fr; gap:var(--gap);
|
|
||||||
}
|
|
||||||
.page-header{ display:flex; align-items:center; justify-content:space-between; gap:var(--gap) }
|
.page-header{ display:flex; align-items:center; justify-content:space-between; gap:var(--gap) }
|
||||||
.back-btn,.btn{
|
.back-btn,.btn{ display:inline-flex; align-items:center; gap:.6rem; border-radius:999px; padding:.6rem .9rem; border:1px solid rgba(255,255,255,0.10); background:rgba(255,255,255,0.04); color:inherit; text-decoration:none; outline:none; transition:background 300ms, transform 160ms }
|
||||||
display:inline-flex; align-items:center; gap:.6rem;
|
|
||||||
border-radius:999px; padding:.6rem .9rem;
|
|
||||||
border:1px solid rgba(255,255,255,0.10);
|
|
||||||
background:rgba(255,255,255,0.04);
|
|
||||||
color:inherit; text-decoration:none; outline:none;
|
|
||||||
transition:background 300ms, transform 160ms;
|
|
||||||
}
|
|
||||||
.btn.primary{ background:rgba(255,255,255,0.08) }
|
.btn.primary{ background:rgba(255,255,255,0.08) }
|
||||||
.back-btn:hover,.btn:hover{ background:rgba(255,255,255,0.07); transform:translateY(-1px) }
|
.back-btn:hover,.btn:hover{ background:rgba(255,255,255,0.07); transform:translateY(-1px) }
|
||||||
.back-btn:focus-visible,.btn:focus-visible{ box-shadow:0 0 0 3px rgba(154,168,255,.45) }
|
.back-btn:focus-visible,.btn:focus-visible{ box-shadow:0 0 0 3px rgba(154,168,255,.45) }
|
||||||
.back-btn svg{ width:18px; height:18px; flex:0 0 auto; opacity:.95 }
|
.back-btn svg{ width:18px; height:18px; flex:0 0 auto; opacity:.95 }
|
||||||
.page-main{ display:grid; place-items:start center }
|
.page-main{ display:grid; place-items:start center }
|
||||||
.article{
|
.article{ width:min(90ch,100%); line-height:1.6; background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.10); border-radius:16px; padding:clamp(16px,3vw,28px) }
|
||||||
width:min(90ch,100%); line-height:1.6;
|
|
||||||
background:rgba(255,255,255,0.04);
|
|
||||||
border:1px solid rgba(255,255,255,0.10);
|
|
||||||
border-radius:16px;
|
|
||||||
padding:clamp(16px,3vw,28px);
|
|
||||||
}
|
|
||||||
.article h1{ margin:0 0 .35em; font-size:clamp(22px,3.2vw,36px); letter-spacing:.2px; text-align:center }
|
.article h1{ margin:0 0 .35em; font-size:clamp(22px,3.2vw,36px); letter-spacing:.2px; text-align:center }
|
||||||
.subtitle{ margin:0 0 1.1em; opacity:.85; text-align:center; font-size:clamp(14px,1.6vw,16px) }
|
.subtitle{ margin:0 0 1.1em; opacity:.85; text-align:center; font-size:clamp(14px,1.6vw,16px) }
|
||||||
.cta-row{ display:flex; flex-wrap:wrap; gap:.6rem; justify-content:center; margin:0 0 1.25em }
|
.cta-row{ display:flex; flex-wrap:wrap; gap:.6rem; justify-content:center; margin:0 0 1.25em }
|
||||||
|
|
@ -195,10 +189,18 @@ body.page{
|
||||||
.party-list{ display:grid; gap:.35rem; margin:.6rem 0 1rem; padding-left:1.2rem }
|
.party-list{ display:grid; gap:.35rem; margin:.6rem 0 1rem; padding-left:1.2rem }
|
||||||
|
|
||||||
/* ===== Mobile ===== */
|
/* ===== Mobile ===== */
|
||||||
@media (max-width:480px){
|
@media (max-width:520px){
|
||||||
:root{ --gap:8px; --header-h:48px }
|
:root{ --gap:10px; --header-h:48px }
|
||||||
.kicker{ font-size:11px }
|
|
||||||
.title{ font-size:clamp(16px,5.2vw,22px) }
|
/* let header wrap and push search under brand to avoid horizontal scroll */
|
||||||
.desc{ font-size:13px }
|
.site-header{ flex-wrap:wrap; justify-content:center }
|
||||||
.icon{ width:clamp(28px,11vh,90px) }
|
.search-bar{ flex:1 1 100%; max-width:100%; width:100%; margin-top:8px }
|
||||||
|
.search-bar input{ width:100% }
|
||||||
|
|
||||||
|
/* smaller icons so they center inside 1x1 / 2x2 tiles on phones */
|
||||||
|
.icon{ width:clamp(28px,14vw,56px); height:auto }
|
||||||
|
.icon.icon-text{ font-size:clamp(22px,12vw,56px) }
|
||||||
|
|
||||||
|
/* keep caption readable but compact */
|
||||||
|
.icon-caption{ font-size:11px; max-width:95% }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
204
styles.css.bak
Normal file
204
styles.css.bak
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
:root{
|
||||||
|
--bg:#121212;
|
||||||
|
--fg:#eceff3;
|
||||||
|
--gap:12px;
|
||||||
|
--reveal-ms:2600ms;
|
||||||
|
--header-h:56px;
|
||||||
|
--safe-top: env(safe-area-inset-top, 0px);
|
||||||
|
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
|
||||||
|
/* Hint knobs (wired from config.hint) */
|
||||||
|
--hint-opacity:.12;
|
||||||
|
--hint-angle:-12deg;
|
||||||
|
--hint-size:16px;
|
||||||
|
--hint-row-gap:8px;
|
||||||
|
--hint-plane:3;
|
||||||
|
--hint-feather:0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Base ===== */
|
||||||
|
*{ box-sizing:border-box }
|
||||||
|
html,body{ height:100% }
|
||||||
|
body{
|
||||||
|
margin:0; color:var(--fg); background:var(--bg);
|
||||||
|
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 ===== */
|
||||||
|
.site-header{
|
||||||
|
position:fixed; left:0; right:0; top:0;
|
||||||
|
height:calc(var(--header-h) + var(--safe-top));
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
padding:0 clamp(12px,3vw,24px);
|
||||||
|
background:rgba(0,0,0,.55); backdrop-filter:blur(6px);
|
||||||
|
border-bottom:1px solid rgba(255,255,255,.10);
|
||||||
|
z-index:2000;
|
||||||
|
}
|
||||||
|
.site-header .brand{
|
||||||
|
font-weight:900; letter-spacing:.22em; text-transform:uppercase;
|
||||||
|
font-size:clamp(12px,1.4vw,14px); opacity:.95; text-decoration:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Grid ===== */
|
||||||
|
.grid{
|
||||||
|
position:fixed; inset:0;
|
||||||
|
top:calc(var(--header-h) + var(--safe-top));
|
||||||
|
display:grid; grid-auto-flow:dense;
|
||||||
|
grid-template-columns:repeat(12, 80px);
|
||||||
|
grid-auto-rows:80px;
|
||||||
|
gap:var(--gap); padding:var(--gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 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 }
|
||||||
|
.title{ margin:0; font-weight:1000; letter-spacing:.2px; font-size:clamp(16px,2.2vw,28px); line-height:1.1 }
|
||||||
|
.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: 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 geometry (unchanged from prod) ===== */
|
||||||
|
.hint-layer{
|
||||||
|
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),
|
||||||
|
#000 calc(var(--hint-feather) + 1px),
|
||||||
|
#000 calc(100% - var(--hint-feather) - 1px),
|
||||||
|
rgba(0,0,0,0) calc(100% - var(--hint-feather)));
|
||||||
|
mask-image:linear-gradient(to right,
|
||||||
|
rgba(0,0,0,0) var(--hint-feather),
|
||||||
|
#000 calc(var(--hint-feather) + 1px),
|
||||||
|
#000 calc(100% - var(--hint-feather) - 1px),
|
||||||
|
rgba(0,0,0,0) calc(100% - var(--hint-feather)));
|
||||||
|
}
|
||||||
|
.hint-stack{
|
||||||
|
position:absolute; left:50%; top:50%;
|
||||||
|
transform:translate(-50%,-50%) rotate(var(--hint-angle));
|
||||||
|
width:calc(var(--hint-plane) * 100%);
|
||||||
|
height:calc(var(--hint-plane) * 100%);
|
||||||
|
display:flex; flex-direction:column; justify-content:center; align-items:center;
|
||||||
|
gap:var(--hint-row-gap);
|
||||||
|
overflow:hidden; /* clip rows to plane; tile clips plane */
|
||||||
|
}
|
||||||
|
.hint-row{
|
||||||
|
position:relative; display:block;
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
.hint-line{ opacity:1 } /* keep lines solid */
|
||||||
|
|
||||||
|
/* ===== Static pages (from prod) ===== */
|
||||||
|
body.page{
|
||||||
|
min-height:100vh; margin:0; color:var(--fg); background:var(--bg);
|
||||||
|
overflow-y:auto; overflow-x:hidden;
|
||||||
|
}
|
||||||
|
.page-wrap{
|
||||||
|
box-sizing:border-box; min-height:100svh;
|
||||||
|
padding:var(--gap);
|
||||||
|
padding-top:calc(var(--gap) + var(--header-h) + var(--safe-top));
|
||||||
|
padding-bottom:calc(var(--gap) + var(--safe-bottom));
|
||||||
|
display:grid; grid-template-rows:auto 1fr; gap:var(--gap);
|
||||||
|
}
|
||||||
|
.page-header{ display:flex; align-items:center; justify-content:space-between; gap:var(--gap) }
|
||||||
|
.back-btn,.btn{
|
||||||
|
display:inline-flex; align-items:center; gap:.6rem;
|
||||||
|
border-radius:999px; padding:.6rem .9rem;
|
||||||
|
border:1px solid rgba(255,255,255,0.10);
|
||||||
|
background:rgba(255,255,255,0.04);
|
||||||
|
color:inherit; text-decoration:none; outline:none;
|
||||||
|
transition:background 300ms, transform 160ms;
|
||||||
|
}
|
||||||
|
.btn.primary{ background:rgba(255,255,255,0.08) }
|
||||||
|
.back-btn:hover,.btn:hover{ background:rgba(255,255,255,0.07); transform:translateY(-1px) }
|
||||||
|
.back-btn:focus-visible,.btn:focus-visible{ box-shadow:0 0 0 3px rgba(154,168,255,.45) }
|
||||||
|
.back-btn svg{ width:18px; height:18px; flex:0 0 auto; opacity:.95 }
|
||||||
|
.page-main{ display:grid; place-items:start center }
|
||||||
|
.article{
|
||||||
|
width:min(90ch,100%); line-height:1.6;
|
||||||
|
background:rgba(255,255,255,0.04);
|
||||||
|
border:1px solid rgba(255,255,255,0.10);
|
||||||
|
border-radius:16px;
|
||||||
|
padding:clamp(16px,3vw,28px);
|
||||||
|
}
|
||||||
|
.article h1{ margin:0 0 .35em; font-size:clamp(22px,3.2vw,36px); letter-spacing:.2px; text-align:center }
|
||||||
|
.subtitle{ margin:0 0 1.1em; opacity:.85; text-align:center; font-size:clamp(14px,1.6vw,16px) }
|
||||||
|
.cta-row{ display:flex; flex-wrap:wrap; gap:.6rem; justify-content:center; margin:0 0 1.25em }
|
||||||
|
.callout{ margin:1.2em 0; padding:1rem; border:1px solid rgba(255,255,255,0.10); border-radius:12px; background:rgba(255,255,255,.03) }
|
||||||
|
.callout.warn{ border-color:rgba(255,99,99,.5); background:rgba(255,0,0,.06) }
|
||||||
|
.callout h3{ margin:.2em 0 .6em; font-size:1rem; letter-spacing:.08em; text-transform:uppercase; opacity:.9 }
|
||||||
|
.article h2{ margin:1.6em 0 .6em; font-size:clamp(18px,2.4vw,24px) }
|
||||||
|
.article h3{ margin:1.2em 0 .4em; font-size:clamp(16px,2vw,20px) }
|
||||||
|
.article p{ margin:.8em 0 }
|
||||||
|
.article ul,.article ol{ margin:.8em 0 .8em 1.2em }
|
||||||
|
.article a{ color:inherit; text-decoration:underline; text-underline-offset:2px }
|
||||||
|
.legal{ border-color:rgba(255,255,255,.18) }
|
||||||
|
.sig{ margin-top:1.5em; padding-top:1em; border-top:1px solid rgba(255,255,255,0.10); display:grid; gap:.25rem }
|
||||||
|
.party-list{ display:grid; gap:.35rem; margin:.6rem 0 1rem; padding-left:1.2rem }
|
||||||
|
|
||||||
|
/* ===== Mobile ===== */
|
||||||
|
@media (max-width:480px){
|
||||||
|
:root{ --gap:8px; --header-h:48px }
|
||||||
|
.kicker{ font-size:11px }
|
||||||
|
.title{ font-size:clamp(16px,5.2vw,22px) }
|
||||||
|
.desc{ font-size:13px }
|
||||||
|
.icon{ width:clamp(28px,11vh,90px) }
|
||||||
|
}
|
||||||
75
support.html
75
support.html
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
<header class="site-header" id="siteHeader" role="banner">
|
<header class="site-header" id="siteHeader" role="banner">
|
||||||
<a class="brand" href="/">LASER EVERYTHING</a>
|
<a class="brand" href="/">LASER EVERYTHING</a>
|
||||||
|
<!-- Optional: <form class="search-bar" role="search"><input type="search" placeholder="Search…"/></form> -->
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main id="grid" class="grid" aria-label="Masonry menu"></main>
|
<main id="grid" class="grid" aria-label="Masonry menu"></main>
|
||||||
|
|
@ -20,7 +21,7 @@
|
||||||
Config default: /configs/index.json (override with ?config=name)
|
Config default: /configs/index.json (override with ?config=name)
|
||||||
====================================================== */
|
====================================================== */
|
||||||
(async () => {
|
(async () => {
|
||||||
/* ---------- tiny seeded RNG (deterministic) ---------- */
|
/* ---------- tiny seeded RNG ---------- */
|
||||||
function xmur3(str){
|
function xmur3(str){
|
||||||
let h = 1779033703 ^ str.length;
|
let h = 1779033703 ^ str.length;
|
||||||
for (let i=0;i<str.length;i++){
|
for (let i=0;i<str.length;i++){
|
||||||
|
|
@ -54,10 +55,10 @@
|
||||||
return u.pathname + u.search;
|
return u.pathname + u.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default config (your JSON can override any/all).
|
// Default config (your JSON overrides)
|
||||||
let cfg = {
|
let cfg = {
|
||||||
brand: "LASER EVERYTHING",
|
brand: "LASER EVERYTHING",
|
||||||
grid: { gap: 12, fit: "stretch" }, // fit: 'stretch' | 'fitCells'
|
grid: { gap: 12, fit: "stretch" },
|
||||||
reveal: { durationMs: 2600, revertDelayMs: 700 },
|
reveal: { durationMs: 2600, revertDelayMs: 700 },
|
||||||
hint: {
|
hint: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -72,7 +73,7 @@
|
||||||
offsetAmpPct: 35,
|
offsetAmpPct: 35,
|
||||||
featherPct: 0
|
featherPct: 0
|
||||||
},
|
},
|
||||||
layout: { respectConfigOrder: false }, // <— override knob
|
layout: { respectConfigOrder: false },
|
||||||
tiles: []
|
tiles: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -125,7 +126,6 @@
|
||||||
a.href = t.href || '#';
|
a.href = t.href || '#';
|
||||||
a.setAttribute('aria-label', t.title || ('Tile ' + (i+1)));
|
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.color) a.style.setProperty('--color', t.color);
|
||||||
|
|
||||||
if (t.image){
|
if (t.image){
|
||||||
|
|
@ -139,7 +139,7 @@
|
||||||
// Cover (wipe)
|
// Cover (wipe)
|
||||||
const cover = document.createElement('div'); cover.className='cover'; a.appendChild(cover);
|
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)){
|
if ((cfg.hint?.enabled!==false) && (t.hintTitle || t.title)){
|
||||||
const hintLayer = document.createElement('div');
|
const hintLayer = document.createElement('div');
|
||||||
hintLayer.className = 'hint-layer';
|
hintLayer.className = 'hint-layer';
|
||||||
|
|
@ -147,9 +147,7 @@
|
||||||
hintLayer.style.setProperty('--hint-feather', (cfg.hint.featherPct|0) + '%');
|
hintLayer.style.setProperty('--hint-feather', (cfg.hint.featherPct|0) + '%');
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = document.createElement('div');
|
const stack = document.createElement('div'); stack.className = 'hint-stack'; hintLayer.appendChild(stack);
|
||||||
stack.className = 'hint-stack';
|
|
||||||
hintLayer.appendChild(stack);
|
|
||||||
|
|
||||||
const seed = 'row|' + (t.title || ('tile'+i));
|
const seed = 'row|' + (t.title || ('tile'+i));
|
||||||
const rnd = rngFrom(seed);
|
const rnd = rngFrom(seed);
|
||||||
|
|
@ -165,16 +163,14 @@
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'hint-row';
|
row.className = 'hint-row';
|
||||||
row.textContent = lineText;
|
row.textContent = lineText;
|
||||||
|
|
||||||
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
|
const offs = (rnd()*2 - 1) * ((cfg.hint?.offsetAmpPct ?? 35)/100);
|
||||||
row.dataset.offsetScale = String(offs);
|
row.dataset.offsetScale = String(offs);
|
||||||
|
|
||||||
stack.appendChild(row);
|
stack.appendChild(row);
|
||||||
}
|
}
|
||||||
a.appendChild(hintLayer);
|
a.appendChild(hintLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icon layer
|
// Icon layer + caption (hidden state)
|
||||||
const iconL=document.createElement('div'); iconL.className='layer icon-layer';
|
const iconL=document.createElement('div'); iconL.className='layer icon-layer';
|
||||||
if (t.icon){
|
if (t.icon){
|
||||||
if (/\.(png|svg|jpg|jpeg|webp|gif)$/i.test(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 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);
|
a.appendChild(iconL);
|
||||||
|
|
||||||
// Text layer
|
// Text layer (revealed)
|
||||||
const textL=document.createElement('div'); textL.className='layer text-layer';
|
const textL=document.createElement('div'); textL.className='layer text-layer';
|
||||||
const wrap=document.createElement('div');
|
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);
|
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);
|
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);
|
textL.appendChild(wrap); a.appendChild(textL);
|
||||||
|
|
||||||
// Sizing (raw spans from config)
|
// Sizing
|
||||||
const span = parseSpan(t.size || 1);
|
const span = parseSpan(t.size || 1);
|
||||||
a.dataset.w = String(Math.max(1, Math.min(6, span.w)));
|
a.dataset.w = String(Math.max(1, Math.min(6, span.w)));
|
||||||
a.dataset.h = String(Math.max(1, Math.min(6, span.h)));
|
a.dataset.h = String(Math.max(1, Math.min(6, span.h)));
|
||||||
|
|
@ -201,7 +201,7 @@
|
||||||
tiles.push(a);
|
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 respectOrder = !!(cfg.layout && cfg.layout.respectConfigOrder);
|
||||||
const ordered = respectOrder ? tiles.slice()
|
const ordered = respectOrder ? tiles.slice()
|
||||||
: tiles.slice().sort((a,b)=>{
|
: tiles.slice().sort((a,b)=>{
|
||||||
|
|
@ -217,27 +217,34 @@
|
||||||
grid.textContent = '';
|
grid.textContent = '';
|
||||||
grid.appendChild(frag2);
|
grid.appendChild(frag2);
|
||||||
|
|
||||||
/* ---------- layout (header-aware) ---------- */
|
/* ---------- layout (header-aware, edge-rounding safe) ---------- */
|
||||||
function layout(){
|
function layout(){
|
||||||
|
// viewport and header height
|
||||||
const W = innerWidth;
|
const W = innerWidth;
|
||||||
const H = innerHeight;
|
const H = innerHeight;
|
||||||
const VV = visualViewport;
|
|
||||||
const vh = (VV && VV.height) ? VV.height : H;
|
|
||||||
|
|
||||||
const header = document.getElementById('siteHeader');
|
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;
|
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();
|
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 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 cols = Math.max(1, Math.round(Math.sqrt(S * A)));
|
||||||
let rows = Math.max(1, Math.ceil(S / cols));
|
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(
|
const unitFor = (c, r) => Math.max(1, Math.min(
|
||||||
Math.floor((W - gap*(c+1)) / c),
|
Math.floor((contentW - gap * (c - 1)) / c),
|
||||||
Math.floor((HH - gap*(r+1)) / r)
|
Math.floor((contentH - gap * (r - 1)) / r)
|
||||||
));
|
));
|
||||||
|
|
||||||
let best = { cols, rows, unit: unitFor(cols, rows), holes: Math.max(0, cols*rows - S) };
|
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.gap = gap + 'px';
|
||||||
grid.style.padding = 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'){
|
if (fit === 'stretch'){
|
||||||
const unitW = (W - gap * (best.cols + 1)) / best.cols;
|
const unitW = (contentW - gap * (best.cols - 1)) / best.cols - EPS;
|
||||||
const unitH = (HH - gap * (best.rows + 1)) / best.rows;
|
const unitH = (contentH - gap * (best.rows - 1)) / best.rows - EPS;
|
||||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
|
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
|
||||||
grid.style.gridAutoRows = `${unitH}px`;
|
grid.style.gridAutoRows = `${unitH}px`;
|
||||||
} else {
|
} else {
|
||||||
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${best.unit}px)`;
|
const unit = best.unit - EPS;
|
||||||
grid.style.gridAutoRows = `${best.unit}px`;
|
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unit}px)`;
|
||||||
|
grid.style.gridAutoRows = `${unit}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
ordered.forEach(n=>{
|
ordered.forEach(n=>{
|
||||||
/* Phone-only span clamp to keep everything on one page */
|
// phone span clamp
|
||||||
const isPhone = innerWidth <= 520;
|
const isPhone = innerWidth <= 520;
|
||||||
const w = Math.min(+n.dataset.w, isPhone ? 2 : +n.dataset.w);
|
const w = Math.min(+n.dataset.w, isPhone ? 2 : +n.dataset.w);
|
||||||
const h = Math.min(+n.dataset.h, isPhone ? 2 : +n.dataset.h);
|
const h = Math.min(+n.dataset.h, isPhone ? 2 : +n.dataset.h);
|
||||||
n.style.gridColumn = `span ${w}`;
|
n.style.gridColumn = `span ${w}`;
|
||||||
n.style.gridRow = `span ${h}`;
|
n.style.gridRow = `span ${h}`;
|
||||||
|
|
||||||
/* Deterministic wipe dir (prod = 140%) */
|
// deterministic wipe
|
||||||
const title = n.querySelector('.title')?.textContent || '';
|
const title = n.querySelector('.title')?.textContent || '';
|
||||||
const R = rngFrom('wipe|'+title);
|
const R = rngFrom('wipe|'+title);
|
||||||
const dirs = [[1,0],[-1,0],[0,-1],[0,1],[1,-1],[-1,-1],[-1,1],[1,1]];
|
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('--dx', (dx*140)+'%');
|
||||||
n.style.setProperty('--dy', (dy*140)+'%');
|
n.style.setProperty('--dy', (dy*140)+'%');
|
||||||
|
|
||||||
/* Recompute hint row offsets using current tile width */
|
// hint offsets
|
||||||
const tw = n.getBoundingClientRect().width;
|
const tw = n.getBoundingClientRect().width;
|
||||||
n.querySelectorAll('.hint-row').forEach(row=>{
|
n.querySelectorAll('.hint-row').forEach(row=>{
|
||||||
const scale = parseFloat(row.dataset.offsetScale || '0');
|
const scale = parseFloat(row.dataset.offsetScale || '0');
|
||||||
|
|
@ -291,8 +302,8 @@
|
||||||
addEventListener('resize', resched);
|
addEventListener('resize', resched);
|
||||||
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
|
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
|
||||||
|
|
||||||
// JS-assisted reveal class only
|
// js-assisted reveal class
|
||||||
ordered.forEach(tile=>{
|
document.querySelectorAll('.tile').forEach(tile=>{
|
||||||
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
|
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
|
||||||
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
|
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
|
||||||
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
|
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue