Initial import
This commit is contained in:
commit
a7d09c6850
51 changed files with 1580 additions and 0 deletions
295
support.html
Normal file
295
support.html
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
<!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)
|
||||
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){
|
||||
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/support.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 (simple & clear). 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
|
||||
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)
|
||||
},
|
||||
|
||||
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', (H.planeScale!=null ? +H.planeScale : 3));
|
||||
|
||||
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 frag = document.createDocumentFragment();
|
||||
const tiles = [];
|
||||
|
||||
(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)));
|
||||
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';
|
||||
}
|
||||
|
||||
// Wipe cover
|
||||
const cover = document.createElement('div'); cover.className='cover'; a.appendChild(cover);
|
||||
|
||||
// ===== Hint layer (novel approach) =====
|
||||
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';
|
||||
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 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
|
||||
|
||||
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
|
||||
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);
|
||||
|
||||
/* ---------- layout (header-aware, deterministic wipe) ---------- */
|
||||
function layout(){
|
||||
const W = innerWidth;
|
||||
const H = innerHeight;
|
||||
const V = visualViewport;
|
||||
const vh = (V && V.height) ? V.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 = tiles.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) };
|
||||
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 };
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
tiles.forEach(n=>{
|
||||
n.style.gridColumn = `span ${n.dataset.w}`;
|
||||
n.style.gridRow = `span ${n.dataset.h}`;
|
||||
|
||||
// Resolve deterministic wipe dir (seeded by title)
|
||||
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)+'%');
|
||||
|
||||
// Update row pixel offsets based on 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');
|
||||
});
|
||||
});
|
||||
}
|
||||
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
|
||||
tiles.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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue