PageSourceSearch

https://dissei.ai/finance-bg.js

js dissei.ai collected 2026-10-05 15:32:17 UTC 19,009 bytes, 393 lines download raw bytes

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.