1// Binary tile walk transitions â adapted from new_bnb_website PromoBinaryTransition. 2// Used when --site-bg-style is binary-aura for promo swaps, scroll entrance, and page-reveal. 3// Canvas grid of glyphs (0/1, hex, symbolsâ¦) walks on to cover, then walks off to reveal. 4 5import { isBinaryAuraMode, isConstrainedClient } from "./site-bg-style.js?v=4"; 6 7const CELL_PX = 14; // fixed glyph cell â bigger areas get more tiles, not bigger chars 8const WALK_IN_MS = 420; 9const WALK_OUT_MS = 500; 10const HOLD_MS = 120; 11 12const BIN = "01"; 13const HEX = "0123456789ABCDEF"; 14const SYMS = "{}[]<>/\\|;:$#@!&%^~=-+*.?_"; 15const TOKENS = [ 16 "0xDEAD", "0xBEEF", "0xC0DE", "CVE-2026", "TCP", "UDP", 17 "SYN", "ACK", "SSH", "DNS", "EXFIL", "INJECT", "PAYLOAD", 18 "NMAP", "ROOT", "HASH", "/bin/sh", "192.168", "10.0.0", ":443" 19]; 20const BRAND_WORDS = [ 21 "BACKDOORS", "BREACHES", "RECKAH", "ANTISYPHON", "WWHF", 22 "BHIS", "BLACKHILLS", "INFOSEC", "INCIDENT", "RESPONSE", 23 "HACKINFEST", "WILDWEST", "EXPLOIT", "DEFEND", "DETECT" 24]; 25const HACK_PHRASES = [ 26 "DECRYPTING...", "SCANNING...", "BREACH DETECTED", 27 "ACCESS GRANTED", "FIREWALL BYPASS", "ROOT ACQUIRED", 28 "PAYLOAD ARMED", "TUNNEL OPEN", "RECON COMPLETE", 29 "INJECTING...", "EXFILTRATING", "HASH CRACKED" 30]; 31 32const GLYPH_MODES = [ 33 "mixed", "mixed", "pureBinary", "hexDump", "brandScroll", 34 "hackTerminal", "symbolStorm", "countdown", "chase" 35]; 36const LIGHT_GLYPH_MODES = ["mixed", "pureBinary", "hexDump", "symbolStorm"]; 37 38const PATTERNS = [ 39 orderRandom, orderDiagonalTL, orderDiagonalBR, orderDiagonalTR, orderDiagonalBL, 40 orderSpiralOut, orderSpiralIn, orderCathode, 41 orderColumnSweepLR, orderColumnSweepRL, orderRowSweepDown, orderRowSweepUp 42]; 43 44const running = new WeakMap(); 45 46export { isBinaryAuraMode, isConstrainedClient }; 47 48function getPerfProfile(width, height) { 49 let prefersReducedMotion = false; 50 try { 51 prefersReducedMotion = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches); 52 } catch { /* ignore */ } 53 54 const cores = Math.max(1, navigator.hardwareConcurrency || 4); 55 const memory = Math.max(1, navigator.deviceMemory || 4); 56 const area = Math.max(1, width * height); 57 const constrained = prefersReducedMotion || isConstrainedClient() || cores <= 4 || memory <= 4 || area >= 900000; 58 const medium = !constrained && (cores <= 8 || memory <= 8 || area >= 650000); 59 const tier = constrained ? "low" : (medium ? "medium" : "high"); 60 61 // Character/cell size stays CELL_PX always â never enlarge glyphs on big surfaces. 62 // Tier only throttles glyph churn / DPR for perf. 63 if (tier === "low") { 64 return { 65 tier, 66 cellPx: CELL_PX, 67 glyphUpdateModulo: 10, 68 fontScale: 0.72, 69 maxDpr: 1, 70 forceGlyphMode: "pureBinary" 71 }; 72 } 73 if (tier === "medium") { 74 return { 75 tier, 76 cellPx: CELL_PX, 77 glyphUpdateModulo: 7, 78 fontScale: 0.72, 79 maxDpr: 1.5, 80 useLightGlyphModes: true 81 }; 82 } 83 return { 84 tier, 85 cellPx: CELL_PX, 86 glyphUpdateModulo: 4, 87 fontScale: 0.72, 88 maxDpr: 2 89 }; 90} 91 92/** Fixed-size cells: cols/rows grow with the region so glyphs stay the same size. */ 93function gridForSize(width, height, perf) { 94 const cell = Math.max(8, perf.cellPx || CELL_PX); 95 const cols = Math.max(1, Math.ceil(width / cell)); 96 const rows = Math.max(1, Math.ceil(height / cell)); 97 return { cols, rows }; 98} 99 100/** Shared cell pitch across one or more panels (promo pair). */ 101function gridsForPanels(sizes, perf) { 102 const cell = Math.max(8, perf.cellPx || CELL_PX); 103 const rows = Math.max(1, Math.ceil(Math.max(...sizes.map((s) => s.height)) / cell)); 104 const colCounts = sizes.map((s) => Math.max(1, Math.ceil(s.width / cell))); 105 return { rows, colCounts }; 106} 107 108/** 109 * Collect ink boxes for text lines + media inside host, in canvas-local coords 110 * (origin at canvas top-left = host left / offsetTop). 111 * Glyphs only paint where these boxes land â not a solid block square. 112 */ 113function collectContentInkRects(host, offsetTop, canvasHeight) { 114 if (!host || canvasHeight < 4) return []; 115 const hostRect = host.getBoundingClientRect(); 116 const canvasTop = hostRect.top + (offsetTop || 0); 117 const canvasBottom = canvasTop + canvasHeight; 118 const canvasLeft = hostRect.left; 119 const out = []; 120 const pad = 1; 121 122 function addViewportRect(r) { 123 if (!r || r.width < 2 || r.height < 2) return; 124 // Skip boxes outside the canvas band. 125 if (r.bottom <= canvasTop + 1 || r.top >= canvasBottom - 1) return; 126 if (r.right <= canvasLeft + 1 || r.left >= hostRect.right - 1) return; 127 out.push({ 128 left: r.left - canvasLeft - pad, 129 top: r.top - canvasTop - pad, 130 right: r.right - canvasLeft + pad, 131 bottom: r.bottom - canvasTop + pad 132 }); 133 } 134 135 function isSkippable(el) { 136 if (!el || el.nodeType !== 1) return true; 137 if (el.closest(".section-header")) return true; 138 if (el.matches && el.matches("script, style, noscript, .binary-tile-canvas, .scroll-hex-canvas, .promo-binary-canvas, .page-reveal-canvas, .page-reveal-binary-canvas")) { 139 return true; 140 } 141 return false;
142 } 143 144 // Media / replaced content. 145 const media = host.querySelectorAll( 146 "img, picture, video, svg, iframe, canvas, .card-grid-media, .image-block-frame, .feature-split-media, .site-hero-media" 147 ); 148 for (const el of media) { 149 if (isSkippable(el)) continue; 150 if (el.matches && el.matches(".binary-tile-canvas, .scroll-hex-canvas, .promo-binary-canvas")) continue; 151 addViewportRect(el.getBoundingClientRect()); 152 } 153 154 // Text line boxes (follows wrapped words, not the full paragraph square). 155 const walker = document.createTreeWalker(host, NodeFilter.SHOW_TEXT, { 156 acceptNode(node) { 157 if (!node || !node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT; 158 const parent = node.parentElement; 159 if (!parent || isSkippable(parent)) return NodeFilter.FILTER_REJECT; 160 if (parent.closest("script, style, noscript, .section-header")) return NodeFilter.FILTER_REJECT; 161 return NodeFilter.FILTER_ACCEPT; 162 } 163 }); 164 const range = document.createRange(); 165 let textNode = walker.nextNode(); 166 let textBudget = 400; 167 while (textNode && textBudget-- > 0) { 168 try { 169 range.selectNodeContents(textNode); 170 const rects = range.getClientRects(); 171 for (let i = 0; i < rects.length; i++) addViewportRect(rects[i]); 172 } catch { /* ignore */ } 173 textNode = walker.nextNode(); 174 } 175 176 return out; 177} 178 179/** Mark grid cells whose center sits inside any ink rect. */ 180function buildInkCellMask(cols, rows, width, height, inkRects) { 181 const total = cols * rows; 182 const mask = new Uint8Array(total); 183 if (!inkRects || inkRects.length === 0) { 184 mask.fill(1); 185 return mask; 186 } 187 const cellW = width / cols; 188 const cellH = height / rows; 189 for (let i = 0; i < total; i++) { 190 const c = i % cols; 191 const r = (i / cols) | 0; 192 const cx = (c + 0.5) * cellW; 193 const cy = (r + 0.5) * cellH; 194 for (let k = 0; k < inkRects.length; k++) { 195 const box = inkRects[k]; 196 if (cx >= box.left && cx <= box.right && cy >= box.top && cy <= box.bottom) { 197 mask[i] = 1; 198 break; 199 } 200 } 201 } 202 // If measurement failed somehow, fall back to full cover. 203 let any = false; 204 for (let i = 0; i < total; i++) { 205 if (mask[i]) { any = true; break; } 206 } 207 if (!any) mask.fill(1); 208 return mask; 209} 210
211function resolveThemeBackgroundColor(el) { 212 let node = el; 213 while (node && node !== document.documentElement) { 214 const bg = (getComputedStyle(node).backgroundColor || "").trim(); 215 if (isVisibleColor(bg)) return toOpaqueRgb(bg); 216 node = node.parentElement; 217 } 218 const rootBg = (getComputedStyle(document.documentElement).backgroundColor || "").trim(); 219 if (isVisibleColor(rootBg)) return toOpaqueRgb(rootBg); 220 try { 221 const page = getComputedStyle(document.documentElement).getPropertyValue("--page-bg").trim(); 222 if (page) return page; 223 } catch { /* ignore */ } 224 return "rgb(16, 18, 18)"; 225} 226 227function resolveGlyphRgb() { 228 try { 229 const probe = document.createElement("span"); 230 probe.style.color = "var(--aura-mid-color, var(--accent-gold, #f5b335))"; 231 probe.hidden = true; 232 document.body.appendChild(probe); 233 const color = getComputedStyle(probe).color; 234 document.body.removeChild(probe); 235 const m = color && color.match(/[\d.]+/g); 236 if (m && m.length >= 3) { 237 return { r: +m[0], g: +m[1], b: +m[2] }; 238 } 239 } catch { /* ignore */ } 240 return { r: 180, g: 230, b: 245 }; 241} 242 243function isVisibleColor(value) { 244 return value && value !== "transparent" && value !== "rgba(0, 0, 0, 0)"; 245} 246 247function toOpaqueRgb(color) { 248 const m = color.match(/^rgba?\(([^)]+)\)$/i); 249 if (!m) return color; 250 const parts = m[1].split(",").map((s) => s.trim()); 251 if (parts.length < 3) return color; 252 const r = clampInt(parts[0], 0, 255); 253 const g = clampInt(parts[1], 0, 255); 254 const b = clampInt(parts[2], 0, 255); 255 return "rgb(" + r + ", " + g + ", " + b + ")"; 256} 257 258function clampInt(v, min, max) { 259 let n = parseFloat(v); 260 if (!isFinite(n)) return min; 261 n = Math.round(n); 262 return n < min ? min : (n > max ? max : n); 263} 264 265function clamp(v, min, max) { return v < min ? min : (v > max ? max : v); } 266function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } 267 268function createRng(seed) { 269 let x = (seed >>> 0) || 1; 270 return function () { 271 x ^= (x << 13) >>> 0; 272 x ^= x >>> 17; 273 x ^= (x << 5) >>> 0; 274 return (x >>> 0) / 4294967296; 275 }; 276} 277 278function hashPulse(i, tick) { 279 return 0.5 + 0.5 * Math.sin((i * 0.37) + (tick * 0.41)); 280} 281 282function shuffledIndices(total, rng) { 283 const arr = new Array(total); 284 for (let i = 0; i < total; i++) arr[i] = i; 285 for (let j = total - 1; j > 0; j--) { 286 const k = Math.floor(rng() * (j + 1)); 287 const tmp = arr[j]; 288 arr[j] = arr[k]; 289 arr[k] = tmp; 290 } 291 return arr; 292} 293 294function buildScoredOrder(cols, rows, rng, scoreFn) { 295 const total = cols * rows; 296 const items = new Array(total); 297 for (let i = 0; i < total; i++) { 298 const c = i % cols; 299 const r = (i / cols) | 0; 300 items[i] = { idx: i, score: scoreFn(c, r) + rng() * 0.12 }; 301 } 302 items.sort((a, b) => a.score - b.score); 303 return items.map((it) => it.idx); 304} 305 306function orderRandom(cols, rows, rng) { return shuffledIndices(cols * rows, rng); } 307function orderDiagonalTL(cols, rows, rng) { 308 return buildScoredOrder(cols, rows, rng, (c, r) => (c / Math.max(1, cols - 1)) + (r / Math.max(1, rows - 1))); 309} 310function orderDiagonalBR(cols, rows, rng) { 311 return buildScoredOrder(cols, rows, rng, (c, r) => (1 - c / Math.max(1, cols - 1)) + (1 - r / Math.max(1, rows - 1))); 312} 313function orderDiagonalTR(cols, rows, rng) { 314 return buildScoredOrder(cols, rows, rng, (c, r) => (1 - c / Math.max(1, cols - 1)) + (r / Math.max(1, rows - 1))); 315} 316function orderDiagonalBL(cols, rows, rng) { 317 return buildScoredOrder(cols, rows, rng, (c, r) => (c / Math.max(1, cols - 1)) + (1 - r / Math.max(1, rows - 1))); 318} 319function orderSpiralOut(cols, rows, rng) { 320 const cx = Math.max(1, (cols - 1) / 2); 321 const cy = Math.max(1, (rows - 1) / 2); 322 return buildScoredOrder(cols, rows, rng, (c, r) => { 323 const dx = (c - cx) / cx; 324 const dy = (r - cy) / cy; 325 return Math.sqrt(dx * dx + dy * dy); 326 }); 327} 328function orderSpiralIn(cols, rows, rng) { 329 const cx = Math.max(1, (cols - 1) / 2); 330 const cy = Math.max(1, (rows - 1) / 2); 331 return buildScoredOrder(cols, rows, rng, (c, r) => { 332 const dx = (c - cx) / cx; 333 const dy = (r - cy) / cy; 334 return 2 - Math.sqrt(dx * dx + dy * dy); 335 }); 336} 337function orderCathode(cols, rows, rng) { 338 const cx = Math.max(1, (cols - 1) / 2); 339 const cy = Math.max(1, (rows - 1) / 2); 340 return buildScoredOrder(cols, rows, rng, (c, r) => { 341 const dx = Math.abs(c - cx) / cx; 342 const dy = Math.abs(r - cy) / cy; 343 return Math.max(dx, dy); 344 }); 345} 346function orderColumnSweepLR(cols, rows, rng) { 347 return buildScoredOrder(cols, rows, rng, (c) => c / Math.max(1, cols - 1)); 348} 349function orderColumnSweepRL(cols, rows, rng) { 350 return buildScoredOrder(cols, rows, rng, (c) => 1 - c / Math.max(1, cols - 1)); 351} 352function orderRowSweepDown(cols, rows, rng) { 353 return buildScoredOrder(cols, rows, rng, (c, r) => r / Math.max(1, rows - 1)); 354} 355function orderRowSweepUp(cols, rows, rng) { 356 return buildScoredOrder(cols, rows, rng, (c, r) => 1 - r / Math.max(1, rows - 1)); 357} 358 359function pickGlyphMode(rng, perf) { 360 if (perf && perf.forceGlyphMode) return perf.forceGlyphMode; 361 const pool = (perf && perf.useLightGlyphModes) ? LIGHT_GLYPH_MODES : GLYPH_MODES; 362 return pool[Math.floor(rng() * pool.length)]; 363} 364 365function pickGlyph(idx, tick, mode, cols) {
366 const key = ((idx * 31) ^ (tick * 17) ^ 0x9e3779b9) >>> 0; 367 switch (mode) { 368 case "pureBinary": 369 return BIN.charAt((key + tick) & 1); 370 case "hexDump": 371 if (key % 12 === 0) return SYMS.charAt(key % SYMS.length); 372 return HEX.charAt((key + tick * 3) % HEX.length); 373 case "brandScroll": { 374 const word = BRAND_WORDS[(key + ((tick / 4) | 0)) % BRAND_WORDS.length]; 375 const col = idx % (cols || 22); 376 if (key % 5 === 0) return BIN.charAt(key & 1); 377 return word.charAt((col + tick) % word.length); 378 } 379 case "hackTerminal": { 380 const phrase = HACK_PHRASES[(key + ((tick / 6) | 0)) % HACK_PHRASES.length]; 381 if (key % 7 === 0) return HEX.charAt(key % HEX.length); 382 return phrase.charAt((idx + tick) % phrase.length); 383 } 384 case "symbolStorm": 385 if (key % 4 === 0) return BIN.charAt(key & 1); 386 if (key % 3 === 0) return HEX.charAt((key + tick) % HEX.length); 387 return SYMS.charAt((key + tick * 2) % SYMS.length); 388 case "countdown": { 389 const countVal = Math.abs(255 - ((idx + tick * 7) % 512)); 390 const hexStr = countVal.toString(16).toUpperCase(); 391 return hexStr.charAt((tick + idx) % Math.max(1, hexStr.length)); 392 } 393 case "chase": { 394 const runner = ((tick * 3) % (cols || 22)); 395 const col2 = idx % (cols || 22); 396 const dist = Math.abs(col2 - runner); 397 if (dist <= 1) return dist === 0 ? ">" : (col2 < runner ? "<" : ">"); 398 if (key % 6 === 0) return SYMS.charAt(key % SYMS.length); 399 return BIN.charAt((key + tick) & 1); 400 } 401 default: { 402 const lane = key % 100; 403 if (lane < 40) return BIN.charAt(key & 1); 404 if (lane < 72) return HEX.charAt(key % HEX.length); 405 if (lane < 86) return SYMS.charAt(key % SYMS.length); 406 const token = TOKENS[key % TOKENS.length]; 407 return token.charAt((tick + key) % token.length); 408 } 409 } 410} 411 412function ensureCanvas(host, width, height, perf, className, topPx) { 413 const cls = className || "binary-tile-canvas"; 414 let canvas = host.querySelector(":scope > ." + cls.split(" ").join(".")); 415 if (!canvas) { 416 canvas = document.createElement("canvas"); 417 canvas.className = cls; 418 canvas.setAttribute("aria-hidden", "true"); 419 host.appendChild(canvas); 420 } 421 const maxDpr = perf && perf.maxDpr ? perf.maxDpr : 2; 422 const dpr = Math.max(1, Math.min(maxDpr, window.devicePixelRatio || 1)); 423 canvas.width = Math.floor(width * dpr); 424 canvas.height = Math.floor(height * dpr); 425 canvas.style.cssText = 426 "position:absolute;left:0;width:100%;pointer-events:none;z-index:29;border-radius:inherit;"
427 + "top:" + (topPx || 0) + "px;height:" + height + "px;"; 428 429 const ctx = canvas.getContext("2d"); 430 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 431 ctx.clearRect(0, 0, width, height); 432 ctx.textAlign = "center"; 433 ctx.textBaseline = "middle"; 434 return { canvas, ctx, width, height, host, perf, glyphRgb: resolveGlyphRgb() }; 435} 436 437function clearCanvas(state) { 438 if (!state || !state.ctx) return; 439 state.ctx.clearRect(0, 0, state.width, state.height); 440} 441 442function removeCanvas(state) { 443 if (!state) return; 444 clearCanvas(state); 445 if (state.canvas && state.canvas.parentNode) { 446 state.canvas.parentNode.removeChild(state.canvas); 447 } 448} 449 450function drawTiles(state, cols, rows, pixelOn, glyphs, elapsed, themeBg, glyphMode, perf) { 451 const ctx = state.ctx; 452 const width = state.width; 453 const height = state.height; 454 const cellW = width / cols; 455 const cellH = height / rows; 456 const tick = (elapsed * 0.038) | 0; 457 const updateModulo = perf && perf.glyphUpdateModulo ? perf.glyphUpdateModulo : 4; 458 const fontScale = perf && perf.fontScale ? perf.fontScale : 0.74; 459 // Font tracks CELL_PX, not stretched cell boxes â tall/wide blocks keep the same glyph size. 460 const fontPx = Math.max(8, Math.floor((perf && perf.cellPx ? perf.cellPx : CELL_PX) * fontScale)); 461 const base = state.glyphRgb || { r: 180, g: 230, b: 245 }; 462 463 ctx.font = fontPx + 'px "Courier New", ui-monospace, monospace'; 464 ctx.clearRect(0, 0, width, height); 465 466 for (let i = 0; i < pixelOn.length; i++) { 467 if (!pixelOn[i]) continue; 468 469 if (((tick + i * 3) % updateModulo) === 0) { 470 glyphs[i] = pickGlyph(i, tick, glyphMode, cols); 471 } 472 473 const x = i % cols; 474 const y = (i / cols) | 0; 475 const px0 = Math.floor(x * cellW); 476 const py0 = Math.floor(y * cellH); 477 const pw = Math.ceil((x + 1) * cellW) - px0; 478 const ph = Math.ceil((y + 1) * cellH) - py0; 479 480 ctx.globalAlpha = 1; 481 ctx.fillStyle = themeBg; 482 ctx.fillRect(px0, py0, pw, ph); 483 484 const flicker = 0.55 + 0.45 * hashPulse(i, tick); 485 const huePulse = hashPulse(i * 3 + 7, tick); 486 const r = clampInt(base.r + huePulse * 40, 0, 255); 487 const g = clampInt(base.g + huePulse * 25, 0, 255); 488 const b = clampInt(base.b + huePulse * 15, 0, 255); 489 490 ctx.fillStyle = "rgba(" + r + "," + g + "," + b + "," + (0.62 * flicker).toFixed(3) + ")"; 491 ctx.fillText(glyphs[i], px0 + pw * 0.5, py0 + ph * 0.5); 492 } 493 ctx.globalAlpha = 1; 494} 495 496function stopHost(host) { 497 const state = running.get(host); 498 if (!state) return; 499 if (state.rafId) cancelAnimationFrame(state.rafId); 500 if (state.canvasState) removeCanvas(state.canvasState); 501 if (state.canvasStates) { 502 for (const cs of state.canvasStates) removeCanvas(cs); 503 } 504 running.delete(host); 505} 506 507/** 508 * Scroll / block entrance: hex-equivalent â start fully covered, show content 509 * underneath, then walk tiles out once. (Promo swaps still use playCoverReveal.) 510 * Glyphs stick to text-line / media ink boxes instead of a solid block square. 511 */ 512export function revealRegion(host, rect, offsetTop, onDone, beforeAnimate) { 513 if (!host || !rect || rect.width < 4 || rect.height < 4) { 514 if (beforeAnimate) beforeAnimate(); 515 if (onDone) onDone(); 516 return; 517 } 518 519 // iOS Safari OOMs / watchdog-kills on full-block glyph canvases â skip cover. 520 if (isConstrainedClient()) { 521 if (beforeAnimate) beforeAnimate(); 522 if (onDone) onDone(); 523 return; 524 } 525 526 const prevPosition = host.style.position; 527 if (getComputedStyle(host).position === "static") { 528 host.style.position = "relative"; 529 } 530 531 const top = offsetTop || 0; 532 const width = Math.max(4, rect.width); 533 const height = Math.max(4, rect.height); 534 let inkRects = []; 535 try { 536 inkRects = collectContentInkRects(host, top, height); 537 } catch { /* fall back to full mask */ } 538 539 playEntryReveal([{ 540 el: host, 541 width, 542 height, 543 top, 544 inkRects 545 }], { 546 onReady: () => { 547 if (beforeAnimate) beforeAnimate(); 548 }, 549 onDone: () => { 550 if (prevPosition !== undefined) host.style.position = prevPosition || ""; 551 if (onDone) onDone(); 552 } 553 }); 554} 555 556/** 557 * Promo slide swap: walk-in cover â onCovered (swap) â hold â walk-out. 558 * panels: array of { el, width?, height?, top? } (1 or 2 for paired rails). 559 */ 560export function playCoverReveal(panels, { onCovered, onDone, onRaf } = {}) { 561 const list = (panels || []).filter((p) => p && p.el); 562 if (list.length === 0) { 563 if (onDone) onDone(); 564 return; 565 } 566 567 for (const p of list) stopHost(p.el); 568 569 const sizes = list.map((p) => ({ 570 el: p.el, 571 width: p.width != null ? Math.max(4, p.width) : Math.max(140, p.el.clientWidth || 0), 572 height: p.height != null ? Math.max(4, p.height) : Math.max(180, p.el.clientHeight || 0
572), 573 top: p.top || 0 574 })); 575 const totalW = sizes.reduce((s, x) => s + x.width, 0); 576 const maxH = Math.max(...sizes.map((x) => x.height)); 577 const perf = getPerfProfile(totalW, maxH); 578 const { rows, colCounts } = gridsForPanels(sizes, perf); 579 const panelCount = sizes.length; 580 const panelTotals = colCounts.map((c) => c * rows); 581 const wideCols = colCounts.reduce((a, c) => a + c, 0); 582 const wideTotal = wideCols * rows; 583 const colOffsets = []; 584 { 585 let acc = 0; 586 for (let i = 0; i < panelCount; i++) { 587 colOffsets.push(acc); 588 acc += colCounts[i]; 589 } 590 } 591 const themeBg = resolveThemeBackgroundColor(sizes[0].el); 592 const rng = createRng(((Date.now() ^ (totalW << 8) ^ (maxH << 4)) >>> 0) + 1); 593 const patternFn = PATTERNS[Math.floor(rng() * PATTERNS.length)]; 594 const glyphMode = pickGlyphMode(rng, perf); 595 const wideOrder = patternFn(wideCols, rows, rng); 596 597 const canvasStates = sizes.map((s) => 598 ensureCanvas(s.el, s.width, s.height, perf, "binary-tile-canvas promo-binary-canvas", s.top)); 599 const pixelOns = panelTotals.map((n) => new Uint8Array(n)); 600 const glyphsArr = panelTotals.map((n, pi) => { 601 const g = new Array(n); 602 for (let i = 0; i < n; i++) g[i] = pickGlyph(i + pi * 997, 0, glyphMode, colCounts[pi]); 603 return g; 604 }); 605 606 function wideToPanel(wideIdx) { 607 const wx = wideIdx % wideCols; 608 const wy = (wideIdx / wideCols) | 0; 609 let side = panelCount - 1; 610 for (let i = 0; i < panelCount; i++) { 611 if (wx < colOffsets[i] + colCounts[i]) { 612 side = i; 613 break; 614 } 615 } 616 const localX = wx - colOffsets[side]; 617 return { side, local: wy * colCounts[side] + localX, cols: colCounts[side] }; 618 } 619 620 let walkInCount = 0; 621 let walkOutCount = 0; 622 let phase = 0; 623 let holdStart = 0; 624 let startTs = 0; 625 let rafId = 0; 626 let coveredFired = false; 627 const safety = setTimeout(() => finish(), WALK_IN_MS + HOLD_MS + WALK_OUT_MS + 2000); 628 629 function drawAll(elapsed) { 630 for (let i = 0; i < panelCount; i++) { 631 drawTiles(canvasStates[i], colCounts[i], rows, pixelOns[i], glyphsArr[i], elapsed, themeBg, glyphMode, perf); 632 } 633 } 634 635 function finish() { 636 clearTimeout(safety); 637 if (rafId) cancelAnimationFrame(rafId); 638 for (const cs of canvasStates) removeCanvas(cs); 639 for (const s of sizes) running.delete(s.el); 640 if (onDone) onDone(); 641 } 642 643 function frame(ts) { 644 if (!startTs) startTs = ts; 645 const elapsed = ts - startTs; 646 647 if (phase === 0) { 648 const progress = clamp(elapsed / WALK_IN_MS, 0, 1); 649 const targetOn = Math.floor(easeInOutCubic(progress) * wideTotal); 650 while (walkInCount < targetOn && walkInCount < wideTotal) { 651 const cell = wideToPanel(wideOrder[walkInCount]); 652 pixelOns[cell.side][cell.local] = 1; 653 walkInCount++; 654 } 655 drawAll(elapsed); 656 if (walkInCount >= wideTotal) { 657 phase = 1; 658 holdStart = ts; 659 if (!coveredFired) { 660 coveredFired = true; 661 if (onCovered) onCovered(); 662 } 663 } 664 } else if (phase === 1) { 665 drawAll(elapsed); 666 if (ts - holdStart >= HOLD_MS) { 667 phase = 2; 668 walkOutCount = 0; 669 } 670 } else { 671 const walkOutElapsed = ts - holdStart - HOLD_MS; 672 const progress2 = clamp(walkOutElapsed / WALK_OUT_MS, 0, 1); 673 const targetOff = Math.floor(easeInOutCubic(progress2) * wideTotal); 674 while (walkOutCount < targetOff && walkOutCount < wideTotal) { 675 const cell2 = wideToPanel(wideOrder[wideTotal - 1 - walkOutCount]); 676 pixelOns[cell2.side][cell2.local] = 0; 677 walkOutCount++; 678 } 679 drawAll(elapsed); 680 if (walkOutCount >= wideTotal) { 681 finish(); 682 return; 683 } 684 } 685 686 rafId = requestAnimationFrame(frame); 687 if (onRaf) onRaf(rafId); 688 for (const s of sizes) { 689 running.set(s.el, { rafId, canvasStates }); 690 } 691 } 692 693 rafId = requestAnimationFrame(frame); 694 if (onRaf) onRaf(rafId); 695 for (const s of sizes) running.set(s.el, { rafId, canvasStates }); 696} 697 698/** Entry reveal: start fully covered, walk out (page-reveal / first promo paint). */ 699export function playEntryReveal(panels, { onReady, onDone, onRaf } = {}) { 700 const list = (panels || []).filter((p) => p && p.el); 701 if (list.length === 0) { 702 if (onReady) onReady(); 703 if (onDone) onDone(); 704 return; 705 } 706 707 for (const p of list) stopHost(p.el); 708 709 const sizes = list.map((p) => ({ 710 el: p.el, 711 width: p.width != null ? Math.max(4, p.width) : Math.max(140, p.el.clientWidth || 0), 712 height: p.height != null ? Math.max(4, p.height) : Math.max(120, p.el.clientHeight || 0
712), 713 top: p.top || 0, 714 inkRects: p.inkRects || null 715 })); 716 const totalW = sizes.reduce((s, x) => s + x.width, 0); 717 const maxH = Math.max(...sizes.map((x) => x.height)); 718 const perf = getPerfProfile(totalW, maxH); 719 const { rows, colCounts } = gridsForPanels(sizes, perf); 720 const panelCount = sizes.length; 721 const panelTotals = colCounts.map((c) => c * rows); 722 const wideCols = colCounts.reduce((a, c) => a + c, 0); 723 const colOffsets = []; 724 { 725 let acc = 0; 726 for (let i = 0; i < panelCount; i++) { 727 colOffsets.push(acc); 728 acc += colCounts[i]; 729 } 730 } 731 const themeBg = resolveThemeBackgroundColor(sizes[0].el); 732 const rng = createRng(((Date.now() ^ (totalW << 8) ^ (maxH << 4)) >>> 0) + 1); 733 const patternFn = PATTERNS[Math.floor(rng() * PATTERNS.length)]; 734 const glyphMode = pickGlyphMode(rng, perf); 735 const wideOrder = patternFn(wideCols, rows, rng); 736 737 const cellMasks = sizes.map((s, pi) => 738 buildInkCellMask(colCounts[pi], rows, s.width, s.height, s.inkRects)); 739 740 function wideToPanel(wideIdx) { 741 const wx = wideIdx % wideCols; 742 const wy = (wideIdx / wideCols) | 0; 743 let side = panelCount - 1; 744 for (let i = 0; i < panelCount; i++) { 745 if (wx < colOffsets[i] + colCounts[i]) { 746 side = i; 747 break; 748 } 749 } 750 const localX = wx - colOffsets[side]; 751 return { side, local: wy * colCounts[side] + localX }; 752 } 753 754 // Only cells that sit on text/media ink participate in the walk. 755 const activeOrder = []; 756 for (let i = 0; i < wideOrder.length; i++) { 757 const cell = wideToPanel(wideOrder[i]); 758 if (cellMasks[cell.side][cell.local]) activeOrder.push(wideOrder[i]); 759 } 760 const activeTotal = activeOrder.length; 761 762 const canvasStates = sizes.map((s) => 763 ensureCanvas(s.el, s.width, s.height, perf, "binary-tile-canvas promo-binary-canvas", s.top)); 764 const pixelOns = panelTotals.map((n, pi) => { 765 const a = new Uint8Array(n); 766 const mask = cellMasks[pi]; 767 for (let i = 0; i < n; i++) a[i] = mask[i] ? 1 : 0; 768 return a; 769 }); 770 const glyphsArr = panelTotals.map((n, pi) => { 771 const g = new Array(n); 772 for (let i = 0; i < n; i++) g[i] = pickGlyph(i + pi * 997, 0, glyphMode, colCounts[pi]); 773 return g; 774 }); 775 776 for (let i = 0; i < panelCount; i++) { 777 drawTiles(canvasStates[i], colCounts[i], rows, pixelOns[i], glyphsArr[i], 0, themeBg, glyphMode, perf); 778 } 779 // Content becomes visible under the ink-shaped cover, then tiles walk off once. 780 if (onReady) onReady(); 781 782 let walkOutCount = 0; 783 let startTs = 0; 784 let rafId = 0; 785 const safety = setTimeout(() => finish(), WALK_OUT_MS + 1500); 786 787 function finish() { 788 clearTimeout(safety); 789 if (rafId) cancelAnimationFrame(rafId); 790 for (const cs of canvasStates) removeCanvas(cs); 791 for (const s of sizes) running.delete(s.el); 792 if (onDone) onDone(); 793 } 794 795 function frame(ts) { 796 if (!startTs) startTs = ts; 797 const elapsed = ts - startTs; 798 const progress = clamp(elapsed / WALK_OUT_MS, 0, 1); 799 const targetOff = Math.floor(easeInOutCubic(progress) * activeTotal); 800 while (walkOutCount < targetOff && walkOutCount < activeTotal) { 801 const cell = wideToPanel(activeOrder[activeTotal - 1 - walkOutCount]); 802 pixelOns[cell.side][cell.local] = 0; 803 walkOutCount++; 804 } 805 for (let i = 0; i < panelCount; i++) { 806 drawTiles(canvasStates[i], colCounts[i], rows, pixelOns[i], glyphsArr[i], elapsed, themeBg, glyphMode, perf); 807 } 808 if (walkOutCount >= activeTotal) { 809 finish(); 810 return; 811 } 812 rafId = requestAnimationFrame(frame); 813 if (onRaf) onRaf(rafId); 814 for (const s of sizes) running.set(s.el, { rafId, canvasStates }); 815 } 816 817 rafId = requestAnimationFrame(frame); 818 if (onRaf) onRaf(rafId); 819 for (const s of sizes) running.set(s.el, { rafId, canvasStates }); 820} 821 822/** 823 * Full-viewport page leave cover (walk-in), returns a handle with promote/abort. 824 */ 825export function beginViewportCover({ onDone, startFullyCovered } = {}) { 826 // iPhone Safari: a full-viewport fillText grid at load is a hard crash. 827 if (isConstrainedClient()) { 828 let disposed = false;
829 const finish = () => { 830 if (disposed) return; 831 disposed = true; 832 if (typeof onDone === "function") { 833 try { onDone(); } catch { /* ignore */ } 834 } 835 }; 836 if (startFullyCovered) { 837 // Match "already covered â reveal" without allocating a canvas. 838 queueMicrotask(finish); 839 } 840 return { 841 promoteToReveal() { finish(); }, 842 abort: finish, 843 get disposed() { return disposed; }, 844 get phase() { return disposed ? "done" : (startFullyCovered ? "reveal" : "leave"); } 845 }; 846 } 847 848 const host = document.body; 849 stopHost(host); 850 851 const width = window.innerWidth; 852 const height = window.innerHeight; 853 const perf = getPerfProfile(width, height); 854 const { cols, rows } = gridForSize(width, height, perf); 855 const total = cols * rows; 856 const themeBg = resolveThemeBackgroundColor(document.documentElement); 857 const canvas = document.createElement("canvas"); 858 canvas.className = "binary-tile-canvas page-reveal-binary-canvas"; 859 canvas.setAttribute("aria-hidden", "true"); 860 canvas.style.cssText = "position:fixed;inset:0;width:100vw;height:100vh;pointer-events:none;z-index:30;"; 861 const dpr = Math.max(1, Math.min(perf.maxDpr || 2, window.devicePixelRatio || 1)); 862 canvas.width = Math.floor(width * dpr); 863 canvas.height = Math.floor(height * dpr); 864 document.body.appendChild(canvas); 865 const ctx = canvas.getContext("2d"); 866 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 867 ctx.textAlign = "center"; 868 ctx.textBaseline = "middle"; 869 const canvasState = { canvas, ctx, width, height, host, perf, glyphRgb: resolveGlyphRgb() }; 870 871 const rng = createRng(((Date.now() ^ (width << 8)) >>> 0) + 1); 872 const patternFn = PATTERNS[Math.floor(rng() * PATTERNS.length)]; 873 const glyphMode = pickGlyphMode(rng, perf); 874 const walkOrder = patternFn(cols, rows, rng); 875 const pixelOn = new Uint8Array(total); 876 const glyphs = new Array(total); 877 for (let i = 0; i < total; i++) glyphs[i] = pickGlyph(i, 0, glyphMode, cols); 878 879 let walkInCount = 0; 880 let walkOutCount = 0; 881 let phase = "leave"; // leave | hold | reveal 882 let startTs = 0; 883 let revealStart = 0; 884 let rafId = 0; 885 let disposed = false; 886 887 if (startFullyCovered) { 888 pixelOn.fill(1); 889 walkInCount = total; 890 phase = "reveal"; 891 drawTiles(canvasState, cols, rows, pixelOn, glyphs, 0, themeBg, glyphMode, perf); 892 } 893 894 function dispose() { 895 if (disposed) return; 896 disposed = true; 897 if (rafId) cancelAnimationFrame(rafId); 898 if (canvas.parentNode) canvas.parentNode.removeChild(canvas); 899 running.delete(host); 900 if (typeof onDone === "function") { 901 try { onDone(); } catch { /* ignore */ } 902 } 903 } 904 905 function frame(ts) { 906 if (disposed) return; 907 if (!startTs) startTs = ts; 908 const elapsed = ts - startTs; 909 910 if (phase === "leave") { 911 const progress = clamp(elapsed / WALK_IN_MS, 0, 1); 912 const targetOn = Math.floor(easeInOutCubic(progress) * total); 913 while (walkInCount < targetOn && walkInCount < total) { 914 pixelOn[walkOrder[walkInCount]] = 1; 915 walkInCount++; 916 } 917 drawTiles(canvasState, cols, rows, pixelOn, glyphs, elapsed, themeBg, glyphMode, perf); 918 if (walkInCount >= total) { 919 phase = "hold"; 920 } 921 } else if (phase === "hold") { 922 drawTiles(canvasState, cols, rows, pixelOn, glyphs, elapsed, themeBg, glyphMode, perf); 923 } else if (phase === "reveal") { 924 if (!revealStart) revealStart = ts; 925 const revElapsed = ts - revealStart; 926 const progress = clamp(revElapsed / WALK_OUT_MS, 0, 1); 927 const targetOff = Math.floor(easeInOutCubic(progress) * total); 928 while (walkOutCount < targetOff && walkOutCount < total) { 929 pixelOn[walkOrder[total - 1 - walkOutCount]] = 0; 930 walkOutCount++; 931 } 932 drawTiles(canvasState, cols, rows, pixelOn, glyphs, elapsed, themeBg, glyphMode, perf); 933 if (walkOutCount >= total) { 934 dispose(); 935 return; 936 } 937 } 938 939 rafId = requestAnimationFrame(frame); 940 running.set(host, { rafId, canvasState }); 941 } 942 943 rafId = requestAnimationFrame(frame); 944 running.set(host, { rafId, canvasState }); 945 946 return { 947 promoteToReveal() { 948 if (disposed) return; 949 pixelOn.fill(1); 950 walkOutCount = 0; 951 phase = "reveal"; 952 revealStart = 0; 953 }, 954 abort: dispose,
955 get disposed() { return disposed; }, 956 get phase() { return phase; } 957 }; 958}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.