1/* Ledger paper + dot-image field mounted behind every page. 2 * 3 * Runtime contract: 4 * - fixed viewport-sized canvas #bg-net (never larger than the layout 5 * viewport, so it cannot create scrollable overflow), first child of 6 * <body>, z-index 0; the PAD overscan lives only in offscreen buffers 7 * and the drawing coordinate space, cropped to the viewport on draw; 8 * - three depth planes at different scroll-parallax rates (.15/.30/.50): 9 * far + mid are baked offscreen with gaussian blur (resize-only cost), 10 * - the dot field resolves into a rising yield-curve silhouette generated 11 * procedurally (no image assets) that dissolves toward the sheet edges; 12 * - static single composite under prefers-reduced-motion (no rAF loop); 13 * - phones animate on a lighter profile (wider lattice, fewer live dots); 14 * - paused on hidden tab; rebuilt on resize; 15 * - opt-in upgrades via <body data-finbg="interactive"> (index only): 16 * cursor repulsion with spring return (hover-capable devices only), 17 * redline settlement sweeps; debug hooks window.__finbg / __finbgPulse. 18 */ 19(function () { 20 if (typeof window === "undefined" || typeof document === "undefined") return; 21 22 var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 23 var small = window.matchMedia && window.matchMedia("(max-width: 767px)").matches; 24 var hoverFine = window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches; 25 26 var INK = "23,24,28"; 27 var STEEL = "15,76,129"; // --steel-blue #0f4c81 28 var SKY = "115,153,198"; // --sky #7399c6 29 var REDLINE = "193,90,73"; // --redline-soft 30 var CFG = { 31 cell: 56, // minor rule spacing (px); every 4th rule is a major 32 minorAlpha: .08, majorAlpha: .15, 33 dotGap: 11, // silhouette lattice spacing (px) 34 dotMax: 380, // live near-plane cap â this bounds physics cost 35 accentShare: .22, // share of dots in --sky 36 floor: .05 // ambient dot probability away from the bands 37 }; 38 if (small) { CFG.cell = 64; CFG.dotGap = 16; CFG.dotMax = 150; } 39 var PAD = 160; 40 // Plane translations stay <= MAXP * .5 (~77px) < PAD, so shifted layers can 41 // never expose an unpainted strip inside the viewport. 42 var PLANES = [ 43 { f: .15, blur: 22, alpha: .30 }, // far haze 44 { f: .30, blur: 14, alpha: .45 }, // mid body 45 { f: .50, blur: 0, alpha: .9 } // near: live dots + rules ride this rate 46 ]; 47 var NEAR = PLANES[2].f; 48 49 // interactive opt-in (index only). Read at kick time, not eval: the 50 // attribute is set by a pre-DOMContentLoaded listener in the layout, and 51 // this script now runs deferred (before that listener fires). 52 var INTERACT = false; 53 // repulsion tuning: dots flee the cursor inside R, spring home outside it 54 var REPEL_R = 120, REPEL_R2 = REPEL_R * REPEL_R, REPEL_F = 1.5, 55 SPRING = .02, DAMP = .86; 56 // settlement-sweep cadence (ms) 57 var SETTLE_MIN = 8000, SETTLE_MAX = 14000; 58 59 /* The canvas is server-rendered by the root layout on the data build 60 * (React owns the node â script-inserted body children break hydration, 61 * #418). Adopt it at kick time; the create+insert path is a fallback 62 * for stray mounts (older pages, tests). Geometry for the adopted node 63 * lives in data-live.css; the fallback sets it inline. */ 64 var canvas = null, ctx = null; 65 function mount() { 66 canvas = document.getElementById("bg-net"); 67 if (canvas) { ctx = canvas.getContext("2d"); return; } 68 canvas = document.createElement("canvas"); 69 canvas.id = "bg-net"; 70 canvas.setAttribute("aria-hidden", "true"); 71 var s = canvas.style; 72 s.position = "fixed"; 73 s.top = "0"; 74 s.left = "0"; 75 s.width = "100%"; 76 s.height = "100%"; 77 s.zIndex = "0"; 78 s.pointerEvents = "none"; 79 document.body.insertBefore(canvas, document.body.firstChild); 80 ctx = canvas.getContext("2d"); 81 } 82 83 // W/H: overscanned drawing space (viewport + 2*PAD) used by buffers, dots 84 // and the silhouette. VW/VH: CSS viewport the DOM canvas actually covers. 85 var W = 0, H = 0, VW = 0, VH = 0, DPR = 1, dots = [], planes = [], sweeps = [], flashes = [], raf = 0, nextSettle = 0; 86 var gridCv = null; // ruled paper, rendered once per build, blitted per frame 87 var curX = 0, curY = 0, ptrX = 0, ptrY = 0; 88 var mouseX = 0, mouseY = 0, hasPtr = false;
89 var POINTER = 42, MAXP = PAD - 6; 90 91 function clampp(v, m) { return v < -m ? -m : v > m ? m : v; } 92 function rand(a, b) { return a + Math.random() * (b - a); } 93 94 /* ---- procedural silhouette: rising yield-curve --------------------- */ 95 // Primary density band climbs left->right (falling y); a fainter 96 // covenant-headroom band rides above it. Both pass through a smoothstep 97 // edge window so the figure dissolves toward the sheet borders: abstract 98 // speckle up close, a recognisable chart line from a distance. 99 function curveY(x) { 100 var u = x / W; 101 return H * (.72 - .42 * u) + Math.sin(u * 6.4 + 1.1) * H * .05 + Math.sin(u * 15 + 4) * H * .016; 102 } 103 function edgeW(x, y) { 104 var m = Math.min(x / (W * .24), (W - x) / (W * .24), y / (H * .3), (H - y) / (H * .3), 1); 105 return m <= 0 ? 0 : m * m * (3 - 2 * m); 106 } 107 function fieldAt(x, y) { 108 var cy = curveY(x), dy = y - cy, sig = H * .07; 109 var main = Math.exp(-(dy * dy) / (2 * sig * sig)); 110 var hy = y - (cy - H * .13), hsig = H * .032; 111 var head = .45 * Math.exp(-(hy * hy) / (2 * hsig * hsig)); 112 return (main + head + CFG.floor) * edgeW(x, y); 113 } 114 115 function paintDot(g, x, y, v) { 116 var accent = Math.random() < CFG.accentShare; 117 var inkish = !accent && Math.random() < .18; // a few ink marks weave it into the paper 118 g.fillStyle = "rgba(" + (accent ? SKY : inkish ? INK : STEEL) + "," + Math.min(.7, .12 + v * .46).toFixed(3) + ")"; 119 g.beginPath(); g.arc(x, y, .8 + v * 2.0 + Math.random() * .5, 0, 6.2832); g.fill(); 120 } 121 122 // Bake a blurred depth plane once per resize; per-frame work is a single 123 // translated drawImage, which is why blur lives here and not in draw(). 124 // The dots are painted sharp onto a scratch canvas and blurred in one 125 // drawImage pass: canvas 2D applies ctx.filter per draw call, so a blur set 126 // before the dot loop would run a full-surface gaussian per dot (about a 127 // second of main-thread stall on every page boot, for the same pixels). 128 function bakePlane(spec) { 129 var pw = Math.floor(W * DPR), ph = Math.floor(H * DPR); 130 var sharp = document.createElement("canvas"); 131 sharp.width = pw; sharp.height = ph; 132 var g = sharp.getContext("2d"); 133 g.setTransform(DPR, 0, 0, DPR, 0, 0); 134 for (var y = CFG.dotGap / 2; y < H; y += CFG.dotGap) { 135 for (var x = CFG.dotGap / 2; x < W; x += CFG.dotGap) { 136 var jx = x + rand(-CFG.dotGap, CFG.dotGap) * .35, jy = y + rand(-CFG.dotGap, CFG.dotGap) * .35; 137 var v = fieldAt(jx, jy); 138 if (Math.random() < v * .85) paintDot(g, jx, jy, v); 139 } 140 } 141 var cv = document.createElement("canvas"); 142 cv.width = pw; cv.height = ph; 143 var o = cv.getContext("2d"); 144 if (spec.blur) o.filter = "blur(" + (spec.blur * DPR) + "px)"; 145 o.globalAlpha = spec.alpha; 146 o.drawImage(sharp, 0, 0); 147 spec.cv = cv; 148 } 149 150 function buildGrid() { 151 gridCv = document.createElement("canvas"); 152 gridCv.width = Math.floor(W * DPR); gridCv.height = Math.floor(H * DPR); 153 var g = gridCv.getContext("2d"); 154 g.setTransform(DPR, 0, 0, DPR, 0, 0); 155 g.lineWidth = 1; 156 var k = 0, p; 157 for (p = 0; p <= W; p += CFG.cell, k++) { 158 g.strokeStyle = "rgba(" + INK + "," + (k % 4 ? CFG.minorAlpha : CFG.majorAlpha) + ")"; 159 g.beginPath(); g.moveTo(p + .5, 0); g.lineTo(p + .5, H); g.stroke(); 160 } 161 for (p = 0, k = 0; p <= H; p += CFG.cell, k++) { 162 g.strokeStyle = "rgba(" + INK + "," + (k % 4 ? CFG.minorAlpha : CFG.majorAlpha) + ")"; 163 g.beginPath(); g.moveTo(0, p + .5); g.lineTo(W, p + .5); g.stroke(); 164 } 165 } 166 167 // Near plane: sparser than the baked sheets â these carry the crisp detail 168 // and the only per-frame physics, hence the hard dotMax budget. 169 function buildDots() { 170 dots = []; 171 var gap = CFG.dotGap * 1.4; 172 for (var y = gap / 2; y < H && dots.length < CFG.dotMax; y += gap) { 173 for (var x = gap / 2; x < W && dots.length < CFG.dotMax; x += gap) { 174 var px = x + rand(-4, 4), py = y + rand(-4, 4); 175 var v = fieldAt(px, py); 176 if (v > .14 && Math.random() < v) dots.push({ 177 x: px, y: py, r: .8 + v * 1.9, 178 a: Math.min(.62, .11 + v * .40), 179 accent: Math.random() < CFG.accentShare, 180 ph: rand(0, 6.28), // autonomous-drift phase 181 ox: 0, oy: 0, vx: 0, vy: 0 // repulsion displacement/velocity 182 }); 183 } 184 } 185 } 186 187 function build() { 188 /* Texture ceiling: the sheet's CSS filter (blur + saturate, the owner readability ruling) re-rasterizes this canvas every animated frame, and that cost scales with backing-store pixels. The field is soft dots and hairlines under a 3px blur at 70% opacity - past ~0.75x we buy texture bytes, not visible detail. This one number is what kept weak GPUs compositor-bound. */ 189 DPR = Math.min(window.devicePixelRatio || 1, LOW_POWER ? 0.6 : 0.75); 190 // Layout viewport, not window.innerWidth: on mobile innerWidth reports 191 // any scrollable overflow width, which would feed back into the size. 192 var de = document.documentElement; 193 VW = de.clientWidth || window.innerWidth; 194 VH = de.clientHeight || window.innerHeight; 195 W = VW + PAD * 2; 196 H = VH + PAD * 2; 197 canvas.width = Math.floor(VW * DPR); 198 canvas.height = Math.floor(VH * DPR); 199 // Drawing space origin sits at (-PAD,-PAD) of the viewport: translate by 200 // -PAD so every overscan-space coordinate lands where the old oversized 201 // canvas placed it; the backing store crops whatever falls outside. 202 ctx.setTransform(DPR, 0, 0, DPR, -PAD * DPR, -PAD * DPR); 203 buildGrid(); 204 planes = []; 205 for (var i = 0; i < 2; i++) { 206 var p = { f: PLANES[i].f, blur: PLANES[i].blur, alpha: PLANES[i].alpha }; 207 bakePlane(p); planes.push(p); 208 } 209 buildDots(); 210 sweeps = []; flashes = []; 211 } 212 213 function easeOffsets() { 214 var doc = document.documentElement; 215 var range = Math.max(1, doc.scrollHeight - VH); 216 var progress = Math.min(1, Math.max(0, window.pageYOffset / range)); 217 var scroll = (1 - progress * 2) * MAXP * .85; 218 var tgtX = clampp(ptrX, MAXP); 219 var tgtY = clampp(ptrY * .4 + scroll, MAXP); 220 // exponential smoothing rebased on elapsed frames: .08 per 60fps frame 221 var k = 1 - Math.pow(1 - .08, dt); 222 curX += (tgtX - curX) * k; 223 curY += (tgtY - curY) * k; 224 } 225 226 function step() { 227 var repel = INTERACT && hoverFine && hasPtr; 228 var cx = mouseX + PAD, cy = mouseY + PAD; // drawing-space origin sits at (-PAD,-PAD) 229 for (var i = 0; i < dots.length; i++) { 230 var d = dots[i]; 231 if (repel) { 232 var dx = d.x + d.ox - cx, dy = d.y + d.oy - cy, q = dx * dx + dy * dy; 233 if (q < REPEL_R2 && q > .01) { 234 var dist = Math.sqrt(q), push = (1 - dist / REPEL_R) * REPEL_F * dt; 235 d.vx += (dx / dist) * push; d.vy += (dy / dist) * push; 236 } 237 } 238 d.vx += -SPRING * dt * d.ox; d.vy += -SPRING * dt * d.oy; // spring home 239 var damp = Math.pow(DAMP, dt); 240 d.vx *= damp; d.vy *= damp; 241 d.ox += d.vx * dt; d.oy += d.vy * dt; 242 } 243 } 244 245 function drawDots(now) { 246 var t = now * .00023; 247 for (var i = 0; i < dots.length; i++) { 248 var d = dots[i]; 249 // gentle autonomous wander keeps non-interactive pages alive 250 var wx = Math.sin(t + d.ph) * 1.7, wy = Math.cos(t * .83 + d.ph * 1.7) * 1.7; 251 ctx.fillStyle = "rgba(" + (d.accent ? SKY : STEEL) + "," + d.a.toFixed(3) + ")"; 252 ctx.beginPath(); 253 ctx.arc(d.x + d.ox + wx, d.y + d.oy + wy, d.r, 0, 6.2832); 254 ctx.fill(); 255 } 256 } 257 258 // interactive: a redline mark runs down a rule column, leaving a fading 259 // trail, and lands as a small [ ] bracket flash â a settlement being marked.
260 function drawSweeps() { 261 for (var p = sweeps.length - 1; p >= 0; p--) { 262 var sw = sweeps[p]; sw.t += sw.spd * dt; 263 if (sw.t >= 1) { flashes.push({ x: sw.x, y: sw.endY, t: 0 }); sweeps.splice(p, 1); continue; } 264 var y = -20 + (sw.endY + 20) * sw.t; 265 var TAIL = 90, fade = Math.sin(Math.min(1, sw.t * 1.15) * Math.PI); 266 var grad = ctx.createLinearGradient(sw.x, y - TAIL, sw.x, y); 267 grad.addColorStop(0, "rgba(" + REDLINE + ",0)"); 268 grad.addColorStop(1, "rgba(" + REDLINE + "," + (0.55 * fade).toFixed(3) + ")"); 269 ctx.strokeStyle = grad; ctx.lineWidth = 1.6; 270 ctx.beginPath(); ctx.moveTo(sw.x, y - TAIL); ctx.lineTo(sw.x, y); ctx.stroke(); 271 } 272 for (var r = flashes.length - 1; r >= 0; r--) { 273 var fl = flashes[r]; fl.t += 0.03 * dt; 274 if (fl.t >= 1) { flashes.splice(r, 1); continue; } 275 var a = (1 - fl.t) * 0.6, o = 7 + fl.t * 4; 276 ctx.strokeStyle = "rgba(" + REDLINE + "," + a.toFixed(3) + ")"; ctx.lineWidth = 1.6; 277 // [ ] â two bracket strokes flanking the landing point 278 ctx.beginPath(); 279 ctx.moveTo(fl.x - o + 3, fl.y - 8); ctx.lineTo(fl.x - o, fl.y - 8); ctx.lineTo(fl.x - o, fl.y + 8); ctx.lineTo(fl.x - o + 3, fl.y + 8); 280 ctx.moveTo(fl.x + o - 3, fl.y - 8); ctx.lineTo(fl.x + o, fl.y - 8); ctx.lineTo(fl.x + o, fl.y + 8); ctx.lineTo(fl.x + o - 3, fl.y + 8); 281 ctx.stroke(); 282 } 283 } 284 285 function spawnSettle() { 286 if (!W) return false; 287 var col = (Math.random() * Math.floor(W / CFG.cell) | 0) * CFG.cell; 288 sweeps.push({ x: col, endY: rand(H * .25, H * .75), t: 0, spd: rand(.008, .013) }); 289 return true; 290 } 291 292 function draw(now) { 293 if (!reduce) easeOffsets(); // reduced motion keeps curX/curY pinned at 0 294 ctx.clearRect(PAD, PAD, VW, VH); // exactly the visible backing store 295 if (gridCv) ctx.drawImage(gridCv, curX * NEAR, curY * NEAR, W, H); 296 for (var i = 0; i < planes.length; i++) { 297 var p = planes[i]; 298 ctx.drawImage(p.cv, curX * p.f, curY * p.f, W, H); 299 } 300 drawDots(reduce ? 0 : now); 301 if (INTERACT) drawSweeps(); 302 } 303 304 // Ambient frame budget history: weak laptops first froze on the full-viewport canvas under the backdrop-filter nav; the fix that actually held was the 0.75x texture ceiling in build(). The LOW_POWER flag now only tightens that ceiling further. 305 var dm = navigator.deviceMemory; 306 var LOW_POWER = 307 (navigator.hardwareConcurrency || 8) <= 4 || 308 (typeof dm === "number" && dm > 0 ? dm : 8) <= 4; 309 var FRAME_MIN = 32; // ms between draws â 30fps ambient everywhere: the drift, parallax and repulsion are slow signals; 60fps redraws doubled compositor load for no visible gain (and froze weak GPUs) 310 var lastDraw = -1e9; 311 var lastStep = -1; 312 // elapsed 60fps-equivalent frames since the last painted frame; every 313 // per-frame state update (physics, parallax easing, sweeps/flashes) scales 314 // by it so the LOW_POWER cap changes sampling rate, never animation speed 315 var dt = 1; 316 317 function frame(now) { 318 now = now || 0; 319 raf = window.requestAnimationFrame(frame); 320 if (now - lastDraw < FRAME_MIN) return; 321 lastDraw = now; 322 dt = lastStep < 0 ? 1 : Math.min((now - lastStep) / (1000 / 60), 3); 323 lastStep = now; 324 step(); 325 if (INTERACT) { 326 if (!nextSettle) nextSettle = now + rand(SETTLE_MIN, SETTLE_MAX); 327 else if (now >= nextSettle) { spawnSettle(); nextSettle = now + rand(SETTLE_MIN, SETTLE_MAX); } 328 } 329 draw(now); 330 } 331 function start() { if (!raf) raf = window.requestAnimationFrame(frame); } 332 function stop() { if (raf) { window.cancelAnimationFrame(raf); raf = 0; } } 333 334 var rt; 335 function onResize() { if (!ctx) return; clearTimeout(rt); rt = setTimeout(function () { build(); if (reduce) draw(0); }, 200); } 336 337 var __kick = function () { 338 /* Data-arm pages only â the /lender interstitial on the data build 339 * loads this script but must stay canvas-free (gate R3). */ 340 if (!document.querySelector(".arm-data")) return; 341 // The editorial surfaces use one resolved texture; motion belongs to 342 // the content entrances, so an idle page does not repaint this sheet.
343 if (document.documentElement.dataset.finbg === "static") reduce = true; 344 INTERACT = !!(document.body && document.body.getAttribute("data-finbg") === "interactive"); 345 mount(); 346 build(); 347 if (reduce) { draw(0); return; } // one resolved frame; no loop, no listeners 348 start(); 349 if (hoverFine || INTERACT) { 350 window.addEventListener("pointermove", function (e) { 351 if (hoverFine) { ptrX = -(e.clientX / VW - .5) * 2 * POINTER; ptrY = -(e.clientY / VH - .5) * 2 * POINTER; } 352 if (INTERACT) { mouseX = e.clientX; mouseY = e.clientY; hasPtr = true; } 353 }, { passive: true }); 354 } 355 if (INTERACT) { 356 document.documentElement.addEventListener("mouseleave", function () { hasPtr = false; }, { passive: true }); 357 window.addEventListener("blur", function () { hasPtr = false; }); 358 // debug hooks (interactive only â absent on baseline sub-pages) 359 window.__finbgPulse = function () { var ok = spawnSettle(); nextSettle = 0; return ok; }; 360 window.__finbg = { 361 nodes: function () { return dots.map(function (d) { return { x: d.x, y: d.y, r: d.r, ox: d.ox, oy: d.oy }; }); }, 362 // stop/start: freeze on the current frame (visual tests, screenshots) 363 stop: function () { stop(); }, 364 start: function () { start(); }, 365 setPointer: function (cx, cy) { mouseX = cx; mouseY = cy; hasPtr = true; }, 366 clearPointer: function () { hasPtr = false; }, 367 pulse: function () { return window.__finbgPulse(); }, 368 state: function () { return { interactive: INTERACT, dots: dots.length, sweeps: sweeps.length, flashes: flashes.length, hasPtr: hasPtr }; } 369 }; 370 } 371 document.addEventListener("visibilitychange", function () { if (document.hidden) stop(); else start(); }); 372 }; 373 /* First paint of the sheet waits only for React hydration (the layout 374 * signals it from a post-hydration effect), not load + idle callback: 375 * the canvas is React-owned, and sizing it before hydration commits 376 * triggers a #418 recovery that resets it. Hydration typically lands 377 * well before first paint of heavy pages, so the background is resolved 378 * inside the cross-document view transition's cross-fade. A React-owned 379 * canvas must remain untouched if hydration is delayed or fails. */ 380 var __kicked = false; 381 function __go() { 382 if (__kicked) return; 383 __kicked = true; 384 requestAnimationFrame(__kick); 385 } 386 function __fbi() { 387 if (window.__FINBG_HYDRATED || !document.getElementById("bg-net")) { __go(); return; } 388 window.addEventListener("finbg-hydrated", __go, { once: true }); 389 } 390 if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", __fbi, { once: true }); 391 else __fbi(); 392 window.addEventListener("resize", onResize); 393})();
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.