1(function () { 2 'use strict'; 3 4 /* theme-aware ink for canvases drawn on the page background */ 5 function rzInk(alpha) { 6 var dark = document.documentElement.getAttribute('data-theme') === 'dark'; 7 return 'rgba(' + (dark ? '236,238,244' : '20,21,26') + ',' + alpha + ')'; 8 } 9 10 11 /* ââ Hi-DPI canvas helper: back the canvas with (rendered px à devicePixelRatio) 12 and scale the 2D transform so drawing code keeps its logical WÃH coords. ââ */ 13 function rzFitCanvas(cv, g, W, H) { 14 function fit() { 15 var rect = cv.getBoundingClientRect(); 16 var cssW = rect.width || W; 17 var scale = Math.min(4, Math.max(1, (cssW / W) * (window.devicePixelRatio || 1))); 18 var pw = Math.round(W * scale), ph = Math.round(H * scale); 19 if (cv.width !== pw || cv.height !== ph) { cv.width = pw; cv.height = ph; } 20 g.setTransform(scale, 0, 0, scale, 0, 0); 21 } 22 fit(); 23 window.addEventListener('resize', fit); 24 window.addEventListener('load', fit); 25 document.addEventListener('rz-why-variant', fit); /* re-fit when a hidden variant is shown */ 26 document.addEventListener('rz-theme-change', fit); 27 return fit; 28 } 29 window.rzFitCanvas = rzFitCanvas; 30 31 /* ââ 1. SCROLL REVEAL ââââââââââââââââââââââââââââââââââââââââ */ 32 var revealObs = new IntersectionObserver(function (entries) { 33 entries.forEach(function (entry) { 34 if (entry.isIntersecting) { 35 entry.target.classList.add('is-visible'); 36 revealObs.unobserve(entry.target); 37 } 38 }); 39 }, { threshold: 0.10 }); 40 41 document.querySelectorAll('.rz-reveal').forEach(function (el) { 42 revealObs.observe(el); 43 }); 44 45 /* ââ 2. TYPEWRITER TAGLINE âââââââââââââââââââââââââââââââââââ */ 46 var tw = document.getElementById('rz-tw'); 47 if (tw) { 48 var lines = [ 49 '> Penetration Testing & Red Teaming', 50 '> Incident Response â 24/7 On-Call', 51 '> Zero-Day Threat Intelligence', 52 '> Cloud & OT Security Assessments' 53 ]; 54 var li = 0, ci = 0, deleting = false, paused = false; 55 56 /* look the line up in the i18n string table for the active language */ 57 function twLine(i) { 58 var lang = document.documentElement.lang || 'en'; 59 if (lang === 'en') return lines[i]; 60 var S = window.RZ_I18N_STRINGS || {}; 61 return (S[lang] && S[lang][lines[i]]) || lines[i]; 62 } 63 64 function tick() { 65 if (paused) return; 66 var line = twLine(li); 67 if (!deleting) { 68 ci++; 69 tw.textContent = line.slice(0, ci); 70 if (ci >= line.length) { 71 deleting = true; 72 paused = true; 73 setTimeout(function () { paused = false; tick(); }, 2200); 74 return; 75 } 76 } else { 77 ci--; 78 tw.textContent = line.slice(0, ci); 79 if (ci <= 0) { 80 deleting = false; 81 li = (li + 1) % lines.length; 82 } 83 } 84 setTimeout(tick, deleting ? 32 : 55); 85 } 86 87 setTimeout(tick, 900); 88 } 89 90 /* ââ 3. PARTICLE NETWORK (hero background) âââââââââââââââââââ */ 91 var canvas = document.getElementById('rz-particle-canvas'); 92 if (canvas) { 93 var ctx = canvas.getContext('2d'); 94 var pts = []; 95 var COUNT = 65; 96 var MAX_DIST = 130; 97 var RAF; 98 /* five equal accent colours for the node dots */ 99 var ACC = ['#7d14d7', '#5801f5', '#f50034', '#85f403', '#e6e414']; 100 101 function resize() { 102 canvas.width = canvas.offsetWidth || window.innerWidth; 103 canvas.height = canvas.offsetHeight || window.innerHeight; 104 } 105 106 function spawn() { 107 pts = []; 108 for (var i = 0; i < COUNT; i++) { 109 var big = Math.random() < 0.15; 110 pts.push({ 111 x: Math.random() * canvas.width, 112 y: Math.random() * canvas.height, 113 vx: (Math.random() - 0.5) * 0.42, 114 vy: (Math.random() - 0.5) * 0.30, 115 r: big ? (Math.random() * 2 + 2.5) : (Math.random() * 1.5 + 1.2), 116 bright: big, 117 col: ACC[i % ACC.length] 118 }); 119 } 120 } 121 122 function draw() { 123 ctx.clearRect(0, 0, canvas.width, canvas.height); 124 125 for (var i = 0; i < pts.length; i++) { 126 var p = pts[i]; 127 p.x += p.vx; 128 p.y += p.vy; 129 if (p.x < 0 || p.x > canvas.width) p.vx *= -1; 130 if (p.y < 0 || p.y > canvas.height) p.vy *= -1; 131 } 132 133 for (var i = 0; i < pts.length; i++) { 134 for (var j = i + 1; j < pts.length; j++) { 135 var dx = pts[i].x - pts[j].x; 136 var dy = pts[i].y - pts[j].y; 137 var d = Math.sqrt(dx * dx + dy * dy); 138 if (d < MAX_DIST) { 139 var a = (1 - d / MAX_DIST) * 0.55; 140 ctx.strokeStyle = rzInk(a * 0.5); 141 ctx.lineWidth = 0.8; 142 ctx.beginPath(); 143 ctx.moveTo(pts[i].x, pts[i].y); 144 ctx.lineTo(pts[j].x, pts[j].y); 145 ctx.stroke(); 146 } 147 } 148 } 149 150 for (var i = 0; i < pts.length; i++) { 151 ctx.beginPath(); 152 ctx.arc(pts[i].x, pts[i].y, pts[i].r, 0, Math.PI * 2); 153 ctx.fillStyle = pts[i].col; 154 ctx.globalAlpha = pts[i].bright ? 0.95 : 0.6; 155 ctx.fill(); 156 } 157 ctx.globalAlpha = 1; 158 159 RAF = requestAnimationFrame(draw); 160 } 161 162 document.addEventListener('visibilitychange', function () { 163 if (document.hidden) { cancelAnimationFrame(RAF); } 164 else { RAF = requestAnimationFrame(draw); } 165 }); 166 167 resize(); 168 spawn(); 169 draw(); 170 171 window.addEventListener('resize', function () { resize(); spawn(); }); 172 } 173 174 /* ââ 4. THREAT RADAR (hero right side) âââââââââââââââââââââââ */ 175 var radarCanvas = document.getElementById('rz-radar-canvas'); 176 if (radarCanvas) { 177 var RC = radarCanvas.getContext('2d'); 178 var RW = 400, RH = 400, CX = 200, CY = 200, RAD = 182; 179 rzFitCanvas(radarCanvas, RC, RW, RH); 180 181 var threats = [
182 { a: 38, d: 0.52, lbl: 'CVE-2024-4577', r: 4.5, lit: 0 }, 183 { a: 105, d: 0.74, lbl: 'PORT SCAN', r: 3.5, lit: 0 }, 184 { a: 172, d: 0.40, lbl: 'BRUTEFORCE', r: 3.5, lit: 0 }, 185 { a: 238, d: 0.65, lbl: 'C2 BEACON', r: 4, lit: 0 }, 186 { a: 295, d: 0.82, lbl: 'DATA EXFIL', r: 4, lit: 0 }, 187 { a: 62, d: 0.88, lbl: 'RECON ACTIVE', r: 3, lit: 0 }, 188 { a: 148, d: 0.30, lbl: 'PIVOT ATTEMPT', r: 3, lit: 0 }, 189 ]; 190 191 var sweep = 270; /* start pointing up */ 192 var SPEED = 1.0; 193 /* five equal accent colours for the blips */ 194 var ACCr = ['#7d14d7', '#5801f5', '#f50034', '#85f403', '#e6e414']; 195 196 function bx(t) { return CX + Math.cos(t.a * Math.PI / 180) * RAD * t.d; } 197 function by(t) { return CY + Math.sin(t.a * Math.PI / 180) * RAD * t.d; } 198 199 function drawRadar() { 200 RC.clearRect(0, 0, RW, RH); 201 202 /* ââ background disc (barely-there on white) ââ */ 203 RC.beginPath(); 204 RC.arc(CX, CY, RAD + 4, 0, Math.PI * 2); 205 RC.fillStyle = rzInk(0.025); 206 RC.fill(); 207 208 /* ââ outer border ââ */ 209 RC.beginPath(); 210 RC.arc(CX, CY, RAD, 0, Math.PI * 2); 211 RC.strokeStyle = rzInk(0.35); 212 RC.lineWidth = 1.6; 213 RC.stroke(); 214 215 /* ââ range rings ââ */ 216 [0.25, 0.5, 0.75, 1].forEach(function (f) { 217 RC.beginPath(); 218 RC.arc(CX, CY, RAD * f, 0, Math.PI * 2); 219 RC.strokeStyle = f === 1 ? rzInk(0.22) : rzInk(0.09); 220 RC.lineWidth = f === 1 ? 1.4 : 0.8; 221 RC.stroke(); 222 }); 223 224 /* ââ spoke lines ââ */ 225 for (var deg = 0; deg < 360; deg += 30) { 226 var r = deg * Math.PI / 180; 227 RC.beginPath(); 228 RC.moveTo(CX, CY); 229 RC.lineTo(CX + Math.cos(r) * RAD, CY + Math.sin(r) * RAD); 230 RC.strokeStyle = rzInk(0.05); 231 RC.lineWidth = 0.7; 232 RC.stroke(); 233 } 234 235 /* ââ sweep trail (90° fade) ââ */ 236 for (var i = 88; i >= 0; i--) { 237 var a1 = (sweep - i) * Math.PI / 180; 238 var a2 = (sweep - i - 1) * Math.PI / 180; 239 var op = (1 - i / 88) * 0.14; 240 RC.beginPath(); 241 RC.moveTo(CX, CY); 242 RC.arc(CX, CY, RAD, a1, a2, true); 243 RC.closePath(); 244 RC.fillStyle = rzInk(op); 245 RC.fill(); 246 } 247 248 /* ââ sweep line ââ */ 249 var sr = sweep * Math.PI / 180; 250 RC.beginPath(); 251 RC.moveTo(CX, CY); 252 RC.lineTo(CX + Math.cos(sr) * RAD, CY + Math.sin(sr) * RAD); 253 RC.strokeStyle = rzInk(0.65); 254 RC.lineWidth = 2; 255 RC.stroke(); 256 257 /* ââ blips (each a different accent, equal peers) ââ */ 258 threats.forEach(function (t, idx) { 259 if (!t.col) t.col = ACCr[idx % ACCr.length]; 260 var diff = ((sweep - t.a) % 360 + 360) % 360; 261 if (diff < 2) t.lit = 1.0; 262 t.lit = Math.max(0, t.lit - 0.004); 263 264 if (t.lit < 0.05) return; 265 266 var tx = bx(t), ty = by(t); 267 268 /* expanding ping ring */ 269 if (diff < 45 && t.lit > 0.3) { 270 var ringR = diff * 1.8; 271 RC.globalAlpha = t.lit * 0.45; 272 RC.beginPath(); 273 RC.arc(tx, ty, t.r + ringR, 0, Math.PI * 2); 274 RC.strokeStyle = t.col; 275 RC.lineWidth = 0.9; 276 RC.stroke(); 277 } 278 279 /* dot */ 280 RC.globalAlpha = Math.min(t.lit * 1.4, 0.95); 281 RC.beginPath(); 282 RC.arc(tx, ty, t.r, 0, Math.PI * 2); 283 RC.fillStyle = t.col; 284 RC.fill(); 285 286 /* glow */ 287 RC.globalAlpha = t.lit * 0.3; 288 RC.beginPath(); 289 RC.arc(tx, ty, t.r + 2, 0, Math.PI * 2); 290 RC.strokeStyle = t.col; 291 RC.lineWidth = 1; 292 RC.stroke(); 293 294 /* label */ 295 if (t.lit > 0.25) { 296 var lx = tx + (tx > CX ? t.r + 5 : -(t.lbl.length * 5.2 + t.r + 5)); 297 var ly = ty - t.r - 3; 298 if (lx < 5) lx = 5; 299 if (lx + t.lbl.length * 5.2 > RW - 5) lx = RW - t.lbl.length * 5.2 - 5; 300 RC.globalAlpha = t.lit * 0.95; 301 RC.fillStyle = t.col; 302 RC.font = 'bold 8.5px "Courier New", monospace'; 303 RC.fillText(t.lbl, lx, ly); 304 } 305 RC.globalAlpha = 1; 306 }); 307 308 /* ââ centre dot ââ */ 309 RC.beginPath(); 310 RC.arc(CX, CY, 3.5, 0, Math.PI * 2); 311 RC.fillStyle = rzInk(1); 312 RC.fill(); 313 RC.beginPath(); 314 RC.arc(CX, CY, 7, 0, Math.PI * 2); 315 RC.strokeStyle = rzInk(0.3); 316 RC.lineWidth = 1; 317 RC.stroke(); 318 319 /* ââ status label ââ */ 320 RC.fillStyle = rzInk(0.5); 321 RC.font = 'bold 9px "Courier New", monospace'; 322 RC.textAlign = 'center'; 323 RC.fillText('â SENTINEL SCAN ACTIVE', CX, RH - 8); 324 RC.textAlign = 'left'; 325 326 /* ââ degree markers ââ */ 327 RC.fillStyle = rzInk(0.28); 328 RC.font = '8px "Courier New", monospace'; 329 RC.textAlign = 'center'; 330 ['0°','90°','180°','270°'].forEach(function (lbl, i) { 331 var ar = i * 90 * Math.PI / 180;
332 var mx = CX + Math.cos(ar) * (RAD + 14); 333 var my = CY + Math.sin(ar) * (RAD + 14) + 3; 334 RC.fillText(lbl, mx, my); 335 }); 336 RC.textAlign = 'left'; 337 338 sweep = (sweep + SPEED) % 360; 339 requestAnimationFrame(drawRadar); 340 } 341 342 drawRadar(); 343 } 344 345 /* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 346 SERVICE & INDUSTRY PAGE ANIMATIONS 347 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 348 349 /* ââ 6. PENTESTING â Network Port Scanner ââââââââââââââââââââ */ 350 (function () { 351 /* ââ 6. PENTESTING â Attack-surface constellation âââââââââââââ 352 A target hub, two orbit rings, eight services. Ink probes leave 353 the hub in sequence; open services settle into an accent ring, 354 closed ones stay hollow. The outer bezel turns slowly. Rhymes 355 with Phase 02 of the engagement track (Reconnaissance â the 356 attack-surface map). Blueprint-on-white, tokens read at runtime, 357 time-based easing, ~13 s loop with a hold. ââ */ 358 var cv = document.getElementById('rz-visual-pentest'); 359 if (!cv) return; 360 var W = 480, H = 480; 361 var g = cv.getContext('2d'); 362 if (!g) return; 363 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store + zoom-aware transform */ 364 365 var PI = Math.PI, TAU = PI * 2; 366 var CX = 240, CY = 232; 367 var R_HUB = 15, R1 = 88, R2 = 168, R_NODE = 11; 368 var LOOP = 13.0; /* seconds per cycle */ 369 var T_IDLE = 0.9, T_STEP = 0.9, T_TRAVEL = 0.75; 370 var T_PING = 0.5, T_BLOOM = 0.8, T_SETTLE = 0.6, T_FADE = 0.9; 371 var T_TEXT = 0.35; /* chip text crossfade */ 372 var ROT_PERIOD = 100; /* seconds per turn of the outer bezel */ 373 var N_OPEN = 3; 374 375 /* probe order runs clockwise, alternating inner / outer ring */ 376 var NODES = [ 377 { port: '443', svc: 'HTTPS', r: R1, a: 337.5, open: true }, 378 { port: '445', svc: 'SMB', r: R2, a: 22.5, open: false }, 379 { port: '22', svc: 'SSH', r: R1, a: 67.5, open: true }, 380 { port: '3306', svc: 'MYSQL', r: R2, a: 112.5, open: false }, 381 { port: '21', svc: 'FTP', r: R1, a: 157.5, open: false }, 382 { port: '3389', svc: 'RDP', r: R2, a: 202.5, open: true }, 383 { port: '25', svc: 'SMTP', r: R1, a: 247.5, open: false }, 384 { port: '8080', svc: 'HTTP-ALT', r: R2, a: 292.5, open: false } 385 ]; 386 var N = NODES.length, i, k = 0; 387 for (i = 0; i < N; i++) { 388 var nd = NODES[i], rad = nd.a * PI / 180; 389 nd.cos = Math.cos(rad); nd.sin = Math.sin(rad); 390 nd.x = CX + nd.cos * nd.r; nd.y = CY + nd.sin * nd.r; 391 nd.label = nd.port + ' · ' + nd.svc; 392 if (Math.abs(nd.cos) > 0.5) { /* beside the node, never across the ring */ 393 nd.align = nd.cos > 0 ? 'left' : 'right'; 394 nd.lx = nd.x + (nd.cos > 0 ? 1 : -1) * (R_NODE + 5); 395 nd.ly = nd.y + nd.sin * (R_NODE + 9) + 3; 396 } else { /* above / below the node */ 397 nd.align = 'center'; 398 nd.lx = nd.x + nd.cos * (R_NODE + 4); 399 nd.ly = nd.sin > 0 ? nd.y + R_NODE + 12 : nd.y - R_NODE - 5; 400 } 401 nd.openIdx = nd.open ? k++ : -1; /* which of the open services (for accent rotation) */ 402 nd.tStart = T_IDLE + i * T_STEP; 403 nd.tArrive = nd.tStart + T_TRAVEL; 404 } 405 406 /* five equal accents, house rotation order; fall back if a token is missing */ 407 var ACC_KEYS = ['purple', 'lime', 'crimson', 'indigo', 'yellow']; 408 var ACC_FALL = { purple: '#7d14d7', lime: '#85f403', crimson: '#f50034', indigo: '#5801f5', yellow: '#e6e414' }; 409 function tokens() { 410 var cs = null, dark = document.documentElement.getAttribute('data-theme') === 'dark'; 411 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 412 function rd(name, fb) { 413 var v = ''; 414 try { v = cs ? (cs.getPropertyValue(name) || '') : ''; } catch (e) {} 415 v = v.replace(/^\s+|\s+$/g, ''); 416 return v || fb; 417 } 418 var acc = [], j; 419 for (j = 0; j < ACC_KEYS.length; j++) acc.push(rd('--rz-' + ACC_KEYS[j], ACC_FALL[ACC_KEYS[j]])); 420 return { acc: acc, paper: rd('--rz-paper', dark ? '#0e0f14' : '#ffffff') }; 421 } 422 423 /* ââ easing + small helpers ââ */ 424 function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); } 425 function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; } 426 function easeOut(p) { return 1 - Math.pow(1 - p, 3); } 427 function easeOutQuint(p) { return 1 - Math.pow(1 - p, 5); } 428 function circle(x, y, r) { g.beginPath(); g.arc(x, y, r, 0, TAU); } 429 function line(x1, y1, x2, y2) { g.beginPath(); g.moveTo(x1, y1); g.lineTo(x2, y2); g.stroke(); } 430 function roundRect(x, y, w, h, r) { 431 g.beginPath(); 432 g.moveTo(x + r, y); 433 g.lineTo(x + w - r, y); g.arcTo(x + w, y, x + w, y + r, r); 434 g.lineTo(x + w, y + h - r); g.arcTo(x + w, y + h, x + w - r, y + h, r); 435 g.lineTo(x + r, y + h); g.arcTo(x, y + h, x, y + h - r, r); 436 g.lineTo(x, y + r); g.arcTo(x, y, x + r, y, r); 437 g.closePath(); 438 } 439 /* letter-spaced text (canvas letterSpacing is not universal) */ 440 function spaced(txt, x, y, sp, align) { 441 var ws = [], w = 0, j; 442 for (j = 0; j < txt.length; j++) { ws.push(g.measureText(txt.charAt(j)).width); w += ws[j]; } 443 w += sp * (txt.length - 1); 444 var px = align === 'right' ? x - w : (align === 'center' ? x - w / 2 : x); 445 g.textAlign = 'left'; 446 for (j = 0; j < txt.length; j++) { g.fillText(txt.charAt(j), px, y);
446 px += ws[j] + sp; } 447 return w; 448 } 449 var FONT_LABEL = "600 8.5px 'Oxanium', sans-serif"; 450 var FONT_TAG = "700 8.5px 'Oxanium', sans-serif"; 451 var FONT_MONO = "bold 9px 'Courier New', monospace"; 452 var FONT_MONO_S = "bold 8.5px 'Courier New', monospace"; 453 var CHIP_FINAL = 'SURFACE MAPPED · ' + N + '/' + N + ' · ' + N_OPEN + ' OPEN'; 454 455 /* chip text crossfade memory (the only non-derived state) */ 456 var chipText = '', chipPrev = '', chipChangedAt = -10; 457 458 /* ââ one composed frame at loop-time t (0..LOOP), absolute time T ââ */ 459 function scene(t, T, tk, instant) { 460 g.clearRect(0, 0, W, H); 461 g.lineCap = 'round'; g.lineJoin = 'round'; 462 g.textBaseline = 'alphabetic'; 463 464 var keep = 1 - easeInOut(clamp01((t - (LOOP - T_FADE)) / T_FADE)); /* end-of-cycle fade */ 465 var cycle = Math.floor(T / LOOP); 466 var acc = tk.acc, paper = tk.paper; 467 function accentFor(nd) { return acc[(cycle * N_OPEN + nd.openIdx) % acc.length]; } 468 469 /* â corner crop-brackets (the sheet) â */ 470 g.strokeStyle = rzInk(0.45); g.lineWidth = 1; 471 var m = 8.5, L = 12; 472 line(m, m, m + L, m); line(m, m, m, m + L); 473 line(W - m, m, W - m - L, m); line(W - m, m, W - m, m + L); 474 line(m, H - m, m + L, H - m); line(m, H - m, m, H - m - L); 475 line(W - m, H - m, W - m - L, H - m); line(W - m, H - m, W - m, H - m - L); 476 477 /* â eyebrow + phase readout â */ 478 g.fillStyle = rzInk(0.62); g.font = FONT_LABEL; 479 var ew = spaced('ATTACK-SURFACE MAP', 27, 19, 1.2, 'left'); 480 g.fillStyle = rzInk(0.22); g.fillRect(27, 23, ew, 1); 481 g.fillStyle = rzInk(0.55); g.font = FONT_MONO_S; g.textAlign = 'right'; 482 g.fillText('PHASE 02 · D2âD4', W - 27, 19); 483 484 /* â inner orbit (dashed) â */ 485 g.strokeStyle = rzInk(0.16); g.lineWidth = 1; 486 if (g.setLineDash) g.setLineDash([2, 6]); 487 circle(CX, CY, R1); g.stroke(); 488 if (g.setLineDash) g.setLineDash([]); 489 490 /* â outer orbit + slowly turning ruler bezel â */ 491 g.strokeStyle = rzInk(0.13); circle(CX, CY, R2); g.stroke(); 492 var rot = (T / ROT_PERIOD) * TAU, j, a, c, s, len, major; 493 for (j = 0; j < 48; j++) { 494 major = (j % 4) === 0; 495 a = rot + j * TAU / 48; c = Math.cos(a); s = Math.sin(a); 496 len = major ? 7 : 3.5; 497 g.strokeStyle = rzInk(major ? 0.36 : 0.2); 498 line(CX + c * R2, CY + s * R2, CX + c * (R2 - len), CY + s * (R2 - len)); 499 } 500 501 /* â per-node timing â */ 502 var st = [], p = [], nd; 503 for (j = 0; j < N; j++) { 504 nd = NODES[j]; 505 p[j] = clamp01((t - nd.tStart) / T_TRAVEL); 506 st[j] = easeOut(clamp01((t - nd.tArrive) / T_SETTLE)) * keep; 507 } 508 509 /* â spokes â */ 510 for (j = 0; j < N; j++) { 511 nd = NODES[j]; 512 var glow = (p[j] > 0 && p[j] < 1) ? Math.sin(p[j] * PI) * 0.16 : 0; 513 var alpha = 0.13 + glow + (nd.open ? 0.13 : 0.03) * st[j]; 514 g.strokeStyle = rzInk(alpha); g.lineWidth = 1; 515 line(CX + nd.cos * (R_HUB + 2), CY + nd.sin * (R_HUB + 2), CX + nd.cos * (nd.r - R_NODE - 2), CY + nd.sin * (nd.r - R_NODE - 2)); 516 } 517 518 /* â probes in flight (comet tail + dot) â */ 519 for (j = 0; j < N; j++) { 520 if (!(p[j] > 0 && p[j] < 1)) continue; 521 nd = NODES[j]; 522 var rA = R_HUB + 3, rB = nd.r - R_NODE - 3; 523 var pe = easeInOut(p[j]), pt = Math.max(0, pe - 0.16); 524 var r1 = rA + (rB - rA) * pe, r0 = rA + (rB - rA) * pt; 525 var dotA = Math.min(1, p[j] / 0.08); 526 var gr = g.createLinearGradient(CX + nd.cos * r0, CY + nd.sin * r0, CX + nd.cos * r1, CY + nd.sin * r1); 527 gr.addColorStop(0, rzInk(0)); gr.addColorStop(1, rzInk(0.45 * dotA)); 528 g.strokeStyle = gr; g.lineWidth = 1.6; 529 line(CX + nd.cos * r0, CY + nd.sin * r0, CX + nd.cos * r1, CY + nd.sin * r1); 530 g.fillStyle = rzInk(0.9 * dotA); circle(CX + nd.cos * r1, CY + nd.sin * r1, 2.6); g.fill(); 531 } 532 533 /* â arrival blooms â */ 534 for (j = 0; j < N; j++) { 535 nd = NODES[j]; 536 var bl = clamp01((t - nd.tArrive) / T_BLOOM); 537 if (bl <= 0 || bl >= 1) continue; 538 var br = R_NODE + 12 * easeOutQuint(bl); 539 g.lineWidth = 1.2; 540 if (nd.open) { g.strokeStyle = accentFor(nd); g.globalAlpha = (1 - bl) * 0.6; } 541 else { g.strokeStyle = rzInk((1 - bl) * 0.45); } 542 circle(nd.x, nd.y, br); g.stroke(); 543 g.globalAlpha = 1; 544 } 545 546 /* â service nodes â */ 547 for (j = 0; j < N; j++) { 548 nd = NODES[j]; 549 var sv = st[j]; 550 g.fillStyle = paper; circle(nd.x, nd.y, R_NODE); g.fill(); 551 if (nd.open) { 552 /* ink ring hands over to a double accent ring + accent dot */ 553 if (sv < 1) { g.strokeStyle = rzInk(0.32 * (1 - sv)); g.lineWidth = 1; circle(nd.x, nd.y, R_NODE); g.stroke(); } 554 if (sv > 0) { 555 var col = accentFor(nd); 556 g.strokeStyle = col; g.fillStyle = col; 557 g.globalAlpha = sv; g.lineWidth = 1.6; circle(nd.x, nd.y, R_NODE); g.stroke(); 558 g.globalAlpha = sv * 0.55; g.lineWidth = 1; circle(nd.x, nd.y, R_NODE + 5); g.stroke(); 559 g.globalAlpha = sv; circle(nd.x, nd.y, 2.4); g.fill(); 560 g.globalAlpha = 1; 561 } 562 } else { 563 /* closed: stays hollow, firms up, takes a small ink dash */ 564 g.strokeStyle = rzInk(0.32 + 0.3 * sv); g.lineWidth = 1; circle(nd.x, nd.y, R_NODE); g.stroke(); 565 if (sv > 0) { g.strokeStyle = rzInk(0.6 * sv); g.lineWidth = 1.2; line(nd.x - 3.5, nd.y, nd.x + 3.5, nd.y); } 566 } 567 /* port · service micro-label */ 568 g.font = FONT_MONO_S; g.textAlign = nd.align; 569 g.fillStyle = rzInk(0.38 + (nd.open ? 0.47 : 0.2) * sv); 570 g.fillText(nd.label, nd.lx, nd.ly); 571 } 572 573 /* â hub: double ink ring, probe pings, TARGET tag â */ 574 for (j = 0; j < N; j++) { 575 var pg = clamp01((t - NODES[j].tStart) / T_PING); 576 if (pg <= 0 || pg >= 1) continue; 577 g.strokeStyle = rzInk((1 - pg) * 0.35); g.lineWidth = 1; 578 circle(CX, CY, R_HUB + 9 * easeOut(pg)); g.stroke(); 579 } 580 g.fillStyle = paper; circle(CX, CY, R_HUB); g.fill(); 581 g.strokeStyle = rzInk(0.9); g.lineWidth = 1.3; circle(CX, CY, R_HUB); g.stroke(); 582 g.strokeStyle = rzInk(0.5); g.lineWidth = 1; circle(CX, CY, 9.5); g.stroke(); 583 g.fillStyle = rzInk(0.9); circle(CX, CY, 2.2); g.fill(); 584 g.font = FONT_TAG; 585 var tw = 0; 586 for (j = 0; j < 6; j++) tw += g.measureText('TARGET'.charAt(j)).width; 587 tw += 1.3 * 5; 588 g.fillStyle = paper; roundRect(CX - tw / 2 - 4, CY + 24, tw + 8, 13, 3); g.fill(); /* label halo */ 589 g.fillStyle = rzInk(0.8);
590 spaced('TARGET', CX, CY + 34, 1.3, 'center'); 591 592 /* â status chip â */ 593 var n = 0, o = 0; 594 for (j = 0; j < N; j++) if (t >= NODES[j].tArrive) { n++; if (NODES[j].open) o++; } 595 var txt = n === N ? CHIP_FINAL : (n === 0 ? 'PROBING SURFACE · 0/' + N : 'PROBING SURFACE · ' + n + '/' + N + ' · ' + o + ' OPEN'); 596 if (txt !== chipText) { chipPrev = chipText; chipText = txt; chipChangedAt = t; } 597 var e = instant ? 1 : easeOut(clamp01((t - chipChangedAt) / T_TEXT)); 598 if (chipChangedAt > t) e = 1; /* clock wrapped */ 599 g.font = FONT_MONO; 600 var cw = g.measureText(CHIP_FINAL).width + 30, ch = 24, cx = CX - cw / 2, cy = 432; 601 roundRect(cx, cy, cw, ch, 4); 602 g.fillStyle = paper; g.fill(); 603 g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke(); 604 g.textAlign = 'center'; g.textBaseline = 'middle'; 605 if (e < 1 && chipPrev) { g.fillStyle = rzInk(0.78 * (1 - e)); g.fillText(chipPrev, CX, cy + ch / 2 + 0.5); } 606 g.fillStyle = rzInk(0.78 * e); g.fillText(chipText, CX, cy + ch / 2 + 0.5); 607 g.textBaseline = 'alphabetic'; 608 } 609 610 /* ââ reduced motion: one composed, fully mapped frame ââ */ 611 var reduce = false;
612 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 613 if (reduce) { 614 var drawStatic = function () { scene(LOOP - T_FADE - 0.05, 0, tokens(), true); }; 615 drawStatic(); 616 window.addEventListener('resize', drawStatic); 617 window.addEventListener('load', drawStatic); 618 document.addEventListener('rz-theme-change', drawStatic); 619 if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic); 620 return; 621 } 622 623 /* ââ time-based loop; pauses when the tab is hidden or the hero is off-screen ââ */ 624 var raf = null, t0 = null, pausedAt = null, inView = true; 625 function now() { return (window.performance && performance.now) ? performance.now() : +new Date(); } 626 function frame(ts) { 627 raf = null; 628 if (t0 === null) t0 = ts; 629 var T = Math.max(0, (ts - t0) / 1000); 630 scene(T % LOOP, T, tokens(), false); 631 raf = requestAnimationFrame(frame); 632 } 633 function start() { 634 if (raf !== null || document.hidden || !inView) return; 635 if (pausedAt !== null && t0 !== null) t0 += now() - pausedAt; /* resume without a time jump */ 636 pausedAt = null; 637 raf = requestAnimationFrame(frame); 638 } 639 function stop() { 640 if (raf === null) return; 641 cancelAnimationFrame(raf); raf = null; pausedAt = now(); 642 } 643 document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); }); 644 if ('IntersectionObserver' in window) { 645 try { 646 new IntersectionObserver(function (entries) { 647 for (var q = 0; q < entries.length; q++) inView = entries[q].isIntersecting; 648 if (inView) start(); else stop(); 649 }, { threshold: 0 }).observe(cv); 650 } catch (e) {} 651 } 652 start(); 653 })(); 654 655 /* ââ 7. RED TEAM â ATT&CK Kill Chain âââââââââââââââââââââââââ */ 656 (function () { 657 /* ââ 7. RED TEAM â Kill-Chain Matrix âââââââââââââââââââââââââââ 658 Rhymes with the page's "Operation dossier" section: a 5Ã4 tactic 659 matrix (RECON · ACCESS · FOOTHOLD · MOVE · OBJECTIVE) in which one 660 cell per column is lit in sequence by a travelling signal. Flow 661 connectors are drawn in the column accent, then settle into a faint 662 ink trail; a ruler-tick timeline scrubber with an eased caret tracks 663 the phase; once the objective is reached, a lime "detection 664 opportunity" tick is pinned to the MOVE cell. Time-based and eased, 665 pauses when hidden / off-screen, one static frame under reduced motion. */ 666 var cv = document.getElementById('rz-visual-redteam'); 667 if (!cv) return; 668 var W = 480, H = 480; 669 var g = cv.getContext('2d'); 670 if (!g) return; 671 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store */ 672 673 /* columns mirror the section's five tactics; readouts are illustrative */ 674 var COLS = [ 675 { lbl: 'RECON', read: 'TA0043 · T1595 ACTIVE SCANNING', row: 2, tok: '--rz-purple', fb: '#7d14d7' }, 676 { lbl: 'ACCESS', read: 'TA0001 · T1566 PHISHING', row: 1, tok: '--rz-lime', fb: '#85f403' }, 677 { lbl: 'FOOTHOLD', read: 'TA0011 · T1071 APP-LAYER C2', row: 0, tok: '--rz-crimson', fb: '#f50034' }, 678 { lbl: 'MOVE', read: 'TA0008 · T1550 PASS-THE-HASH', row: 1, tok: '--rz-indigo', fb: '#5801f5' }, 679 { lbl: 'OBJECTIVE', read: 'TA0010 · T1041 EXFIL OVER C2', row: 2, tok: '--rz-yellow', fb: '#e6e414' } 680 ]; 681 var N = COLS.length, ROWS = 4, DET = 3; /* detection tick lands on MOVE */ 682 683 /* geometry (logical px) */ 684 var CW = 58, CH = 44, PX = 88, PY = 58, X0 = 64, Y0 = 136; /* cell size, pitch, first centre */ 685 var CR = [3, 8, 8, 8]; /* folder-card corner radii */ 686 var HEAD_Y = 50, LABEL_Y = 100, RAIL_Y = 378, RAIL_X0 = 48, RAIL_X1 = 432, FOOT_Y = 425; 687 688 /* cadence: 500ms lead-in · 5 à (800ms travel + 700ms dwell) · detection · hold · 600ms reset */ 689 var T_IN = 500, TRAVEL = 800, PHASE = 1500, FADE = 600; 690 var T_DONE = T_IN + (N - 1) * PHASE + TRAVEL; /* objective reached 7300 */ 691 var T_DET = T_IN + N * PHASE + 200; /* detection tick appears 8200 */ 692 var LOOP = 11400, T_FADE = LOOP - FADE; /* ~11.4 s per cycle */ 693 694 var reduce = false;
695 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 696 697 function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); } 698 function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; } 699 function easeOut(p) { return 1 - Math.pow(1 - p, 3); } 700 function easeOut5(p) { return 1 - Math.pow(1 - p, 5); } 701 function pad(n) { return (n < 10 ? '0' : '') + n; } 702 function cx(c) { return X0 + c * PX; } 703 function cy(r) { return Y0 + r * PY; } 704 705 /* ââ colour tokens: read from CSS each frame so theme switches are picked up ââ */ 706 function parseColor(v, fb) { 707 var m, r = -1, gg = -1, b = -1; 708 v = (v || '').replace(/^\s+|\s+$/g, ''); 709 if (v.charAt(0) === '#') { 710 if (v.length === 4) { 711 r = parseInt(v.charAt(1) + v.charAt(1), 16); gg = parseInt(v.charAt(2) + v.charAt(2), 16); b = parseInt(v.charAt(3) + v.charAt(3), 16); 712 } else if (v.length >= 7) { 713 r = parseInt(v.substr(1, 2), 16); gg = parseInt(v.substr(3, 2), 16); b = parseInt(v.substr(5, 2), 16); 714 } 715 } else if ((m = /rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/.exec(v))) { 716 r = Math.round(+m[1]); gg = Math.round(+m[2]); b = Math.round(+m[3]); 717 } 718 if (isNaN(r) || isNaN(gg) || isNaN(b) || r < 0 || gg < 0 || b < 0) { 719 return v === fb ? [125, 20, 215] : parseColor(fb, fb); 720 } 721 return [r, gg, b]; 722 } 723 function rgba(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + a + ')'; } 724 function tokens() { 725 var cs = null, out = { paper: '#ffffff', acc: [], lime: null }, i; 726 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 727 function t(name, fb) { 728 var v = ''; 729 if (cs) { try { v = cs.getPropertyValue(name); } catch (e) {} } 730 v = (v || '').replace(/^\s+|\s+$/g, ''); 731 return v || fb; 732 } 733 out.paper = t('--rz-paper', '#ffffff'); 734 for (i = 0; i < N; i++) out.acc[i] = parseColor(t(COLS[i].tok, COLS[i].fb), COLS[i].fb); 735 out.lime = parseColor(t('--rz-lime', '#85f403'), '#85f403'); 736 return out; 737 } 738 739 /* ââ drawing helpers ââ */ 740 function rrect(x, y, w, h, r) { /* per-corner radii, no roundRect dependency */ 741 var tl = r[0], tr = r[1], br = r[2], bl = r[3]; 742 g.beginPath(); 743 g.moveTo(x + tl, y); 744 g.lineTo(x + w - tr, y); g.arcTo(x + w, y, x + w, y + tr, tr); 745 g.lineTo(x + w, y + h - br); g.arcTo(x + w, y + h, x + w - br, y + h, br); 746 g.lineTo(x + bl, y + h); g.arcTo(x, y + h, x, y + h - bl, bl); 747 g.lineTo(x, y + tl); g.arcTo(x, y, x + tl, y, tl); 748 g.closePath(); 749 } 750 function tracked(str, x, y, track, align) { /* letter-spaced text; returns its start x and width */ 751 var i, w = [], total = 0, sx; 752 for (i = 0; i < str.length; i++) { w[i] = g.measureText(str.charAt(i)).width; total += w[i]; } 753 total += track * (str.length - 1); 754 sx = align === 'right' ? x - total : (align === 'center' ? x - total / 2 : x); 755 g.textAlign = 'left'; 756 for (i = 0, x = sx; i < str.length; i++) { g.fillText(str.charAt(i), x, y); x += w[i] + track; } 757 return { x: sx, w: total }; 758 } 759 /* paper chip whose text swaps between a and b with progress f (slide + fade) */ 760 function chip(a, b, f, x, yMid, align, paper) { 761 var wa, wb, w, h = 22, bx, by; 762 g.font = "700 9px 'Courier New', monospace"; 763 wa = g.measureText(a).width + 20; wb = g.measureText(b).width + 20; 764 w = wa + (wb - wa) * f; 765 bx = align === 'right' ? x - w : x; by = yMid - h / 2; 766 rrect(bx, by, w, h, [3, 6, 6, 6]); 767 g.fillStyle = paper; g.fill(); 768 g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke(); 769 g.textAlign = 'left'; g.textBaseline = 'middle'; 770 if (f < 1) { g.fillStyle = rzInk(0.75 * (1 - f)); g.fillText(a, bx + 10, yMid + 0.5 - 4 * f); } 771 if (f > 0) { g.fillStyle = rzInk(0.75 * f); g.fillText(b, bx + 10, yMid + 0.5 + 4 * (1 - f)); } 772 g.textBaseline = 'alphabetic'; 773 } 774 775 /* ââ signal path: one flow connector per column, flattened once ââ */ 776 function cubic(x0, y0, x1, y1, steps) { /* horizontal tangents at both ends */ 777 var h = (x1 - x0) * 0.5, pts = [], i, t, u; 778 for (i = 0; i <= steps; i++) { 779 t = i / steps; u = 1 - t; 780 pts.push({ 781 x: u * u * u * x0 + 3 * u * u * t * (x0 + h) + 3 * u * t * t * (x1 - h) + t * t * t * x1, 782 y: y0 + (y1 - y0) * (3 * t * t - 2 * t * t * t) 783 }); 784 } 785 return pts; 786 } 787 function buildSeg(pts) { 788 var cum = [0], L = 0, i, dx, dy; 789 for (i = 1; i < pts.length; i++) { dx = pts[i].x - pts[i - 1].x; dy = pts[i].y - pts[i - 1].y; L += Math.sqrt(dx * dx + dy * dy); cum[i] = L; } 790 return { pts: pts, cum: cum, len: L }; 791 } 792 var SEGS = [], k; 793 SEGS[0] = buildSeg([{ x: 6, y: cy(COLS[0].row) }, { x: cx(0) - CW / 2, y: cy(COLS[0].row) }]); /* lead-in from outside */ 794 for (k = 1; k < N; k++) {
795 SEGS[k] = buildSeg(cubic(cx(k - 1) + CW / 2, cy(COLS[k - 1].row), cx(k) - CW / 2, cy(COLS[k].row), 32)); 796 } 797 function pointAt(s, p) { 798 var d = s.len * p, i, a, b, f; 799 if (p <= 0) return s.pts[0]; 800 if (p >= 1) return s.pts[s.pts.length - 1]; 801 for (i = 1; i < s.pts.length; i++) { 802 if (s.cum[i] >= d) { 803 a = s.pts[i - 1]; b = s.pts[i]; f = (d - s.cum[i - 1]) / ((s.cum[i] - s.cum[i - 1]) || 1); 804 return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f }; 805 } 806 } 807 return s.pts[s.pts.length - 1]; 808 } 809 function tracePartial(s, p) { 810 var d = s.len * p, i, e; 811 g.beginPath(); g.moveTo(s.pts[0].x, s.pts[0].y); 812 for (i = 1; i < s.pts.length; i++) { if (s.cum[i] <= d) g.lineTo(s.pts[i].x, s.pts[i].y); else break; } 813 e = pointAt(s, p); g.lineTo(e.x, e.y); 814 } 815 816 /* ââ everything derives from the loop time â no per-frame state, no jitter ââ */ 817 function phaseStart(i) { return T_IN + i * PHASE; } 818 function segState(t, i) { 819 var s = phaseStart(i), landed = t >= s + TRAVEL; 820 return { 821 started: t >= s, 822 p: t < s ? 0 : easeInOut(clamp01((t - s) / TRAVEL)), 823 landed: landed, 824 age: landed ? t - s - TRAVEL : 0 825 }; 826 } 827 function caretX(t) { 828 var i, local, p, from, q; 829 if (t < T_IN) return RAIL_X0; 830 if (t >= T_FADE) { q = easeInOut(clamp01((t - T_FADE) / FADE)); return cx(N - 1) + (RAIL_X0 - cx(N - 1)) * q; } 831 i = Math.min(N - 1, Math.floor((t - T_IN) / PHASE)); 832 local = t - phaseStart(i); 833 p = easeInOut(clamp01(local / TRAVEL)); 834 from = i === 0 ? RAIL_X0 : cx(i - 1); 835 return from + (cx(i) - from) * p; 836 } 837 838 function draw(t, loop) { 839 var tk = tokens(), st = [], i, r, c, x, y, a, f, q, e, pt, mix, cur, prev, swap, G = 1, hA, det, w, lit, o; 840 var done, dA, txtA, txtB, dc, dr, bx, by, bs, lp, ca, rise, chipX, chipY, lime; 841 842 if (t >= T_FADE) G = 1 - easeInOut(clamp01((t - T_FADE) / FADE)); /* clean reset: dynamic marks fade */ 843 for (i = 0; i < N; i++) st[i] = segState(t, i); 844 cur = t < T_IN ? -1 : Math.min(N - 1, Math.floor((t - T_IN) / PHASE)); 845 /* readout swap: previous â current column, eased over 300ms from the phase start */ 846 if (cur < 0) { prev = N - 1; swap = loop > 0 ? easeOut(clamp01(t / 300)) : 1; cur = 0; } 847 else { prev = cur > 0 ? cur - 1 : cur; swap = cur > 0 ? easeOut(clamp01((t - phaseStart(cur)) / 300)) : 1; } 848 lime = tk.lime; 849 850 g.clearRect(0, 0, W, H); 851 g.textBaseline = 'alphabetic'; 852 g.lineJoin = 'round'; 853 854 /* ââ corner crop-brackets ââ */ 855 g.lineCap = 'butt'; 856 g.strokeStyle = rzInk(0.3); g.lineWidth = 1.5; 857 g.beginPath(); 858 g.moveTo(16, 30); g.lineTo(16, 16); g.lineTo(30, 16); 859 g.moveTo(450, 16); g.lineTo(464, 16); g.lineTo(464, 30); 860 g.moveTo(464, 450); g.lineTo(464, 464); g.lineTo(450, 464); 861 g.moveTo(30, 464); g.lineTo(16, 464); g.lineTo(16, 450); 862 g.stroke(); 863 g.lineCap = 'round'; 864 865 /* ââ header: eyebrow + phase chip ââ */ 866 g.font = "600 10px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.55); 867 tracked('ADVERSARY EMULATION', 36, HEAD_Y + 4, 1.5, 'left'); 868 txtA = 'PHASE ' + pad(prev + 1) + ' / ' + pad(N); 869 txtB = 'PHASE ' + pad(cur + 1) + ' / ' + pad(N); 870 chip(txtA, txtB, swap, 444, HEAD_Y, 'right', tk.paper); 871 872 /* ââ column labels (brighten on landing, accent underline) ââ */ 873 g.font = "600 9.5px 'Oxanium', sans-serif"; 874 for (i = 0; i < N; i++) { 875 lit = st[i].landed ? easeOut(clamp01(st[i].age / 350)) * G : 0; 876 g.fillStyle = rzInk(0.42 + 0.46 * lit); 877 tracked(COLS[i].lbl, cx(i), LABEL_Y, 1.2, 'center'); 878 if (lit > 0) { g.fillStyle = rgba(tk.acc[i], lit); g.fillRect(cx(i) - 9, LABEL_Y + 5, 18, 2); } 879 } 880 881 /* detection annotation progress (used by the slot outline below) */ 882 det = t >= T_DET ? 1 : 0; 883 ca = det ? easeOut(clamp01((t - T_DET - 300) / 450)) * G : 0; 884 885 /* ââ matrix: 20 quiet cell outlines ââ */ 886 g.lineWidth = 1; 887 for (r = 0; r < ROWS; r++) { 888 for (c = 0; c < N;
888 c++) { 889 a = 0.16; 890 if (c === DET && r === COLS[DET].row + 1) a *= (1 - ca); /* this slot hosts the detection note */ 891 if (a <= 0.002) continue; 892 rrect(cx(c) - CW / 2, cy(r) - CH / 2, CW, CH, CR); 893 g.strokeStyle = rzInk(a); g.stroke(); 894 } 895 } 896 897 /* ââ connectors: accent while in flight, settling to a faint ink trail ââ */ 898 for (i = 0; i < N; i++) { 899 if (!st[i].started || st[i].p <= 0) continue; 900 mix = st[i].landed ? easeOut(clamp01((st[i].age - 100) / 500)) : 0; 901 tracePartial(SEGS[i], st[i].p); 902 if (mix < 1) { g.strokeStyle = rgba(tk.acc[i], 0.95 * (1 - mix) * G); g.lineWidth = 1.5; g.stroke(); } 903 if (mix > 0) { g.strokeStyle = rzInk(0.3 * mix * G); g.lineWidth = 1.25; g.stroke(); } 904 } 905 906 /* ââ lit cells: 18% accent fill + accent ring, settle pulse on landing ââ */ 907 for (i = 0; i < N; i++) { 908 if (!st[i].landed) continue; 909 f = easeOut(clamp01(st[i].age / 350)) * G; 910 x = cx(i) - CW / 2; y = cy(COLS[i].row) - CH / 2; 911 rrect(x, y, CW, CH, CR); 912 g.fillStyle = rgba(tk.acc[i], 0.18 * f); g.fill(); 913 g.strokeStyle = rgba(tk.acc[i], 0.9 * f); g.lineWidth = 1.5; g.stroke(); 914 q = clamp01(st[i].age / 700); 915 if (q < 1) { 916 e = 8 * easeOut(q); 917 rrect(x - e, y - e, CW + 2 * e, CH + 2 * e, [3 + e, 8 + e, 8 + e, 8 + e]); 918 g.strokeStyle = rgba(tk.acc[i], 0.35 * (1 - q) * G); g.lineWidth = 1; g.stroke(); 919 } 920 } 921 922 /* ââ signal head (only while a connector is being drawn) ââ */ 923 for (i = 0; i < N; i++) { 924 if (!st[i].started || st[i].p <= 0) continue; 925 hA = st[i].landed ? 1 - clamp01(st[i].age / 150) : 1; 926 if (hA <= 0) continue; 927 pt = pointAt(SEGS[i], st[i].p); 928 g.fillStyle = rgba(tk.acc[i], 0.18 * hA * G); g.beginPath(); g.arc(pt.x, pt.y, 8, 0, Math.PI * 2); g.fill(); 929 g.fillStyle = rgba(tk.acc[i], hA * G); g.beginPath(); g.arc(pt.x, pt.y, 3.5, 0, Math.PI * 2); g.fill(); 930 } 931 932 /* ââ detection opportunity: lime tick on the MOVE cell + a paper note in the slot below ââ */ 933 if (det && G > 0) { 934 dc = DET; dr = COLS[DET].row; 935 bx = cx(dc) + CW / 2 - 1; by = cy(dr) - CH / 2 + 1; /* badge on the cell's top-right corner */ 936 bs = easeOut5(clamp01((t - T_DET) / 450)); 937 lp = easeOut(clamp01((t - T_DET - 150) / 400)); 938 if (bs > 0) { 939 g.save(); g.translate(bx, by); g.scale(bs, bs); g.globalAlpha = G; 940 g.fillStyle = tk.paper; g.beginPath(); g.arc(0, 0, 7, 0, Math.PI * 2); g.fill(); 941 g.strokeStyle = rgba(lime, 1); g.lineWidth = 1.5; g.stroke(); 942 g.beginPath(); g.moveTo(-3, 0.5); g.lineTo(-1, 2.5); g.lineTo(3, -2.5); g.stroke(); 943 g.restore(); 944 } 945 if (lp > 0) { /* leader from the cell's bottom edge to the note */ 946 y = cy(dr) + CH / 2; 947 g.strokeStyle = rgba(lime, 0.9 * G); g.lineWidth = 1; 948 g.beginPath(); g.moveTo(cx(dc), y); g.lineTo(cx(dc), y + (PY - CH) * lp); g.stroke(); 949 } 950 if (ca > 0) { 951 rise = 4 * (1 - ca / (G || 1)); 952 chipX = cx(dc) - 34; chipY = cy(dr + 1) - CH / 2 + rise; 953 g.globalAlpha = ca; 954 rrect(chipX, chipY, 68, CH, CR); 955 g.fillStyle = tk.paper; g.fill(); 956 g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke(); 957 g.fillStyle = rgba(lime, 1); g.fillRect(chipX + 4, chipY + 9, 2.5, CH - 18); 958 g.font = "700 7.5px 'Courier New', monospace"; g.fillStyle = rzInk(0.8); 959 tracked('DETECTION', chipX + 12, chipY + 19, 0.5, 'left'); 960 tracked('OPPORTUNITY', chipX + 12, chipY + 30, 0.5, 'left'); 961 g.globalAlpha = 1; 962 } 963 } 964 965 /* ââ timeline scrubber: ruler ticks, rail, notches, caret ââ */ 966 g.lineCap = 'butt'; g.lineWidth = 1; 967 for (x = RAIL_X0; x <= RAIL_X1; x += 12) { 968 a = Math.min(1, (x - RAIL_X0) / 48, (RAIL_X1 - x) / 48); 969 if (a <= 0) continue; 970 g.strokeStyle = rzInk(0.22 * a); 971 g.beginPath(); g.moveTo(x + 0.5, RAIL_Y - 4.5); g.lineTo(x + 0.5, RAIL_Y + 4.5); g.stroke(); 972 } 973 g.strokeStyle = rzInk(0.16); g.lineWidth = 1.5; 974 g.beginPath(); g.moveTo(RAIL_X0, RAIL_Y); g.lineTo(RAIL_X1, RAIL_Y); g.stroke(); 975 g.lineCap = 'round'; 976 for (i = 0; i < N; i++) { 977 lit = st[i].landed ? easeOut(clamp01(st[i].age / 300)) * G : 0; 978 g.beginPath(); g.arc(cx(i), RAIL_Y, 4.5, 0, Math.PI * 2); 979 g.fillStyle = tk.paper; g.fill(); 980 g.strokeStyle = rzInk(0.3); g.lineWidth = 1.25; g.stroke(); 981 if (lit > 0) { 982 g.fillStyle = rgba(tk.acc[i], lit); g.fill(); 983 g.strokeStyle = rgba(tk.acc[i], lit); g.stroke(); 984 } 985 q = st[i].landed ? clamp01(st[i].age / 700) : 1; 986 if (q < 1) { 987 g.beginPath(); g.arc(cx(i), RAIL_Y, 4.5 + 7 * easeOut(q), 0, Math.PI * 2); 988 g.strokeStyle = rgba(tk.acc[i], 0.35 * (1 - q) * G); g.lineWidth = 1; g.stroke(); 989 } 990 } 991 x = caretX(t); 992 o = (t < T_IN) ? 0 : G; /* halo: column accent while running, ink at rest */ 993 g.beginPath(); g.arc(x, RAIL_Y, 9, 0, Math.PI * 2); 994 g.fillStyle = rzInk(0.08 * (1 - o)); g.fill(); 995 if (o > 0) { g.fillStyle = rgba(tk.acc[cur], 0.18 * o); g.fill(); } 996 g.fillStyle = rzInk(0.9); 997 rrect(x - 1.25, RAIL_Y - 10, 2.5, 20, [1.25, 1.25, 1.25, 1.25]); g.fill(); 998 g.beginPath(); g.moveTo(x - 4, RAIL_Y - 15); g.lineTo(x + 4, RAIL_Y - 15); g.lineTo(x, RAIL_Y - 10); g.closePath(); g.fill(); 999 1000 /* ââ footer: tactic / technique readout, status ââ */ 1001 chip(COLS[prev].read, COLS[cur].read, swap, 36, FOOT_Y, 'left', tk.paper); 1002 done = t >= T_DONE; 1003 dA = done ? easeOut(clamp01((t - T_DONE) / 350)) * G : 0; 1004 g.font = "600 9px 'Oxanium', sans-serif"; 1005 if (dA < 1) { /* slide-swap, same motion as the chips */ 1006 y = FOOT_Y - 4 * dA; 1007 g.fillStyle = rzInk(0.5 * (1 - dA)); 1008 w = tracked('REPLAYING KILL CHAIN', 444, y + 3, 1.2, 'right'); 1009 g.fillStyle = rzInk(0.7 * (1 - dA)); g.beginPath(); g.arc(w.x - 11, y, 2.5, 0, Math.PI * 2); g.fill(); 1010 } 1011 if (dA > 0) { 1012 y = FOOT_Y + 4 * (1 - dA); 1013 g.fillStyle = rzInk(0.7 * dA); 1014 w = tracked('OBJECTIVES REACHED', 444, y + 3, 1.2, 'right'); 1015 g.fillStyle = rgba(lime, dA); g.beginPath(); g.arc(w.x - 11, y, 2.5, 0, Math.PI * 2); g.fill(); 1016 } 1017 } 1018 1019 /* ââ reduced motion: one composed frame, redrawn on resize / theme / font load ââ */ 1020 var T_STATIC = T_DET + 1200; 1021 function drawStatic() { draw(T_STATIC, 1); } 1022 if (reduce) { 1023 drawStatic(); 1024 window.addEventListener('resize', drawStatic); 1025 window.addEventListener('load', drawStatic); 1026 document.addEventListener('rz-theme-change', drawStatic); 1027 if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic); 1028 return; 1029 } 1030 1031 /* ââ time-based loop; pauses when the tab is hidden or the canvas is off-screen ââ */ 1032 var t0 = 0, started = false, raf = 0, running = false, pausedAt = 0, inView = true; 1033 function nowMs() { return (window.performance && performance.now) ? performance.now() : Date.now(); } 1034 function frame(now) { 1035 var el; 1036 raf = 0; 1037 if (typeof now !== 'number') now = nowMs(); 1038 if (!started) { started = true; t0 = now; } 1039 el = now - t0; 1040 draw(el % LOOP, Math.floor(el / LOOP)); 1041 if (running) raf = requestAnimationFrame(frame); 1042 } 1043 function start() { 1044 if (running) return; 1045 running = true; 1046 if (pausedAt && started) t0 += nowMs() - pausedAt; 1047 pausedAt = 0; 1048 if (!raf) raf = requestAnimationFrame(frame); 1049 } 1050 function stop() { 1051 if (!running) return; 1052 running = false; pausedAt = nowMs();
1053 if (raf) { cancelAnimationFrame(raf); raf = 0; } 1054 } 1055 function sync() { if (document.hidden || !inView) stop(); else start(); } 1056 document.addEventListener('visibilitychange', sync); 1057 if ('IntersectionObserver' in window) { 1058 try { 1059 new IntersectionObserver(function (entries) { 1060 for (var i = 0; i < entries.length; i++) inView = entries[i].isIntersecting; 1061 sync(); 1062 }, { threshold: 0 }).observe(cv); 1063 } catch (e) {} 1064 } 1065 sync(); 1066 })(); 1067 1068 /* ââ 8. INCIDENT RESPONSE â IR Playbook Timeline âââââââââââââ */ 1069 (function () { 1070 /* Response Dial â hero visual for incident-response.html. 1071 A circular instrument that rhymes with the page's Response Clock 1072 section: fine ink tick ring, six faint phase arcs, an eased hand 1073 that settles at every phase boundary, the active arc filling with 1074 its accent as the hand passes, a small pulse at each boundary, and 1075 a paper chip in the centre with the phase and a T+ counter. 1076 Everything derives from loop time (no per-frame state, no jitter); 1077 pauses when the tab is hidden or the canvas is off-screen; draws 1078 one composed frame under prefers-reduced-motion. âââââââââââââââ */ 1079 var cv = document.getElementById('rz-visual-ir'); 1080 if (!cv) return; 1081 var W = 480, H = 480, CX = 240, CY = 240; 1082 var g = cv.getContext('2d'); 1083 if (!g) return; 1084 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store */ 1085 1086 /* phases mirror the Response Clock section â accents cycle like its 1087 nth-child(5n+k) rule; timings are illustrative, in minutes */ 1088 var PHASES = [ 1089 { name: 'PREPARE', read: 'STANDBY', t0: null, t1: null, step: 0 }, 1090 { name: 'IDENTIFY', read: 'T+00:00', t0: 0, t1: 60, step: 1 }, 1091 { name: 'CONTAIN', read: 'T+01:00', t0: 60, t1: 480, step: 5 }, 1092 { name: 'ERADICATE', read: 'T+08:00', t0: 480, t1: 1440, step: 15 }, 1093 { name: 'RECOVER', read: 'T+24:00', t0: 1440, t1: 4320, step: 30 }, 1094 { name: 'LEARN', read: 'T+72:00', t0: 4320, t1: 5760, step: 30 } 1095 ]; 1096 var TOK = ['--rz-purple', '--rz-lime', '--rz-crimson', '--rz-indigo', '--rz-yellow']; 1097 var FB = ['#7d14d7', '#85f403', '#f50034', '#5801f5', '#e6e414']; 1098 var N = 6, SECTOR = 60, GAP = 3; 1099 1100 /* cadence: eased sweep, settle at the boundary, hold once the cycle 1101 completes, then a soft reset â 12.8 s per loop */ 1102 var SWEEP = 1500, DWELL = 380, PH = SWEEP + DWELL; 1103 var END_HOLD = 1500, RESET = 520, PULSE = 900; 1104 var LOOP = N * PH + END_HOLD; 1105 1106 /* geometry â same proportions as the section dial */ 1107 var R_NUM = 222, R_RING = 200, R_ARC = 166, ARC_W = 12, R_READ = 134; 1108 var R_HAND = 184, R_DISC = 104, R_DISC_IN = 96, R_TRAIL_OUT = 150, R_TRAIL_IN = 118; 1109 1110 var reduce = false;
1111 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 1112 1113 function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); } 1114 function easeInOut(p) { p = clamp01(p); return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; } 1115 function easeOut(p) { p = clamp01(p); return 1 - Math.pow(1 - p, 3); } 1116 function smooth(p) { p = clamp01(p); return p * p * (3 - 2 * p); } 1117 function rad(deg) { return (deg - 90) * Math.PI / 180; } /* 0° at 12 o'clock, clockwise */ 1118 function px(r, deg) { return CX + r * Math.cos(rad(deg)); } 1119 function py(r, deg) { return CY + r * Math.sin(rad(deg)); } 1120 function pad(n) { return (n < 10 ? '0' : '') + n; } 1121 function fmt(min) { var m = Math.max(0, Math.floor(min)); return 'T+' + pad(Math.floor(m / 60)) + ':' + pad(m % 60); } 1122 1123 /* design tokens, read once per frame so a theme switch is picked up */ 1124 function tokens() { 1125 var cs = null, out = { paper: '#ffffff', cols: FB.slice() }, i, v; 1126 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 1127 if (!cs) return out; 1128 v = (cs.getPropertyValue('--rz-paper') || '').replace(/^\s+|\s+$/g, ''); 1129 if (v) out.paper = v; 1130 for (i = 0; i < TOK.length; i++) { 1131 v = (cs.getPropertyValue(TOK[i]) || '').replace(/^\s+|\s+$/g, ''); 1132 if (v) out.cols[i] = v; 1133 } 1134 return out; 1135 } 1136 1137 /* letter-spaced text (canvas letterSpacing is not universal) */ 1138 function tracked(str, x, y, track, align) { 1139 var i, w = [], total = 0, cx; 1140 for (i = 0; i < str.length; i++) { w[i] = g.measureText(str.charAt(i)).width; total += w[i]; } 1141 total += track * (str.length - 1); 1142 cx = align === 'right' ? x - total : (align === 'center' ? x - total / 2 : x); 1143 g.textAlign = 'left'; 1144 for (i = 0; i < str.length; i++) { g.fillText(str.charAt(i), cx, y); cx += w[i] + track; } 1145 } 1146 1147 function arc(r, a0, a1, width, style, alpha) { 1148 if (a1 - a0 < 0.05 || alpha <= 0.002) return; 1149 g.beginPath(); g.arc(CX, CY, r, rad(a0), rad(a1)); 1150 g.lineWidth = width; g.lineCap = 'butt'; g.strokeStyle = style; 1151 g.globalAlpha = alpha; g.stroke(); g.globalAlpha = 1; 1152 } 1153 function wedge(a0, a1, alpha) { /* swept trail between the chip and the arcs */ 1154 if (a1 - a0 < 0.5 || alpha <= 0.002) return; 1155 g.beginPath(); 1156 g.arc(CX, CY, R_TRAIL_OUT, rad(a0), rad(a1)); 1157 g.arc(CX, CY, R_TRAIL_IN, rad(a1), rad(a0), true); 1158 g.closePath(); 1159 g.fillStyle = rzInk(alpha); g.fill(); 1160 } 1161 function tickLine(r0, r1, deg) { g.moveTo(px(r0, deg), py(r0, deg)); g.lineTo(px(r1, deg), py(r1, deg)); } 1162 1163 /* the whole scene is a pure function of loop time t â [0, LOOP) */ 1164 function stateAt(t) { 1165 var s = { phase: 0, u: 0, angle: 0, dwell: 0, hold: false, reset: 0, w: [0, 0, 0, 0, 0, 0] }; 1166 var i, lt, k, et; 1167 if (t < N * PH) { 1168 i = Math.floor(t / PH); lt = t - i * PH; 1169 s.phase = i; 1170 if (lt < SWEEP) { 1171 s.u = easeInOut(lt / SWEEP); 1172 s.angle = i * SECTOR + SECTOR * s.u; 1173 s.w[i] = 1; 1174 } else { /* settled on the boundary */ 1175 s.u = 1; s.dwell = (lt - SWEEP) / DWELL; s.angle = (i + 1) * SECTOR; 1176 k = smooth(s.dwell); 1177 if (i + 1 < N) { s.w[i] = 1 - k; s.w[i + 1] = k; } else { s.w[i] = 1; } 1178 } 1179 } else { /* cycle complete: hold, then soft reset */ 1180 s.phase = N - 1; s.u = 1; s.dwell = 1; s.angle = 360; s.hold = true; 1181 et = t - N * PH; 1182 s.reset = et > END_HOLD - RESET ? smooth((et - (END_HOLD - RESET)) / RESET) : 0; 1183 s.w[N - 1] = 1 - s.reset; s.w[0] = s.reset; 1184 } 1185 return s; 1186 } 1187 1188 function counterText(j, s) { 1189 var ph = PHASES[j], u, m; 1190 if (ph.t0 === null) return ph.read; 1191 u = s.hold ? 1 : (j < s.phase ? 1 : (j > s.phase ? 0 : s.u)); 1192 m = ph.t0 + (ph.t1 - ph.t0) * u; 1193 m = Math.floor(m / ph.step) * ph.step; 1194 return fmt(m); 1195 } 1196 1197 function draw(t) { 1198 var s = stateAt(t), tk = tokens(), dk = smooth(s.dwell); 1199 var j, k, a0, a1, base, wgt, mid, e, p, age, ang, x, y, active, txt; 1200 g.clearRect(0, 0, W, H); 1201 g.textBaseline = 'middle'; 1202 if (g.setLineDash) g.setLineDash([]); 1203 1204 /* ââ frame furniture: crop brackets, eyebrow, foot note ââ */ 1205 g.fillStyle = rzInk(0.3); 1206 g.fillRect(6, 6, 14, 2); g.fillRect(6, 6, 2, 14); 1207 g.fillRect(W - 20, 6, 14, 2);
1207 g.fillRect(W - 8, 6, 2, 14); 1208 g.fillRect(6, H - 8, 14, 2); g.fillRect(6, H - 20, 2, 14); 1209 g.fillRect(W - 20, H - 8, 14, 2); g.fillRect(W - 8, H - 20, 2, 14); 1210 g.fillStyle = rzInk(0.85); g.fillRect(16, 19, 18, 2); 1211 g.font = "600 9px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.55); 1212 tracked('RESPONSE CLOCK', 42, 20, 1.3, 'left'); 1213 g.font = "8px 'Courier New', monospace"; g.fillStyle = rzInk(0.4); 1214 tracked('TIMINGS ILLUSTRATIVE', CX, H - 9, 0.6, 'center'); 1215 1216 /* ââ tick ring ââ */ 1217 g.strokeStyle = rzInk(0.13); g.lineWidth = 1; 1218 g.beginPath(); g.arc(CX, CY, R_RING, 0, Math.PI * 2); g.stroke(); 1219 g.strokeStyle = rzInk(0.22); g.lineWidth = 1; g.beginPath(); 1220 for (j = 0; j < 60; j++) { if (j % 5) tickLine(194, R_RING, j * 6); } 1221 g.stroke(); 1222 g.strokeStyle = rzInk(0.35); g.lineWidth = 1.4; g.beginPath(); 1223 for (j = 1; j < 12; j += 2) tickLine(189, R_RING, j * 30); 1224 g.stroke(); 1225 g.strokeStyle = rzInk(0.6); g.lineWidth = 1.6; g.beginPath(); 1226 for (j = 0; j < N; j++) tickLine(186, 208, j * SECTOR); 1227 g.stroke(); 1228 1229 /* ââ swept trail ââ */ 1230 if (!s.hold) { 1231 if (s.dwell > 0) wedge(s.phase * SECTOR, (s.phase + 1) * SECTOR, 0.055 * (1 - dk)); 1232 else wedge(s.phase * SECTOR, s.angle, 0.055); 1233 } 1234 1235 /* ââ phase arcs: pending â accent while active â neutral once done ââ */ 1236 for (j = 0; j < N; j++) { 1237 a0 = j * SECTOR + GAP; a1 = (j + 1) * SECTOR - GAP; 1238 if (s.hold) base = 0.24 + (0.10 - 0.24) * s.reset; 1239 else if (j < s.phase) base = 0.24; 1240 else if (j === s.phase) base = s.dwell > 0 ? 0.24 : 0.10; 1241 else base = 0.10; 1242 arc(R_ARC, a0, a1, ARC_W, rzInk(base), 1); 1243 if (!s.hold && j === s.phase) { 1244 if (s.dwell > 0) arc(R_ARC, a0, a1, ARC_W, tk.cols[j % 5], 1 - dk); 1245 else { 1246 e = j * SECTOR + Math.min(SECTOR - GAP, Math.max(GAP, SECTOR * s.u)); 1247 arc(R_ARC, a0, e, ARC_W, tk.cols[j % 5], 1); 1248 } 1249 } 1250 } 1251 1252 /* ââ rim numerals + micro readouts ââ */ 1253 for (j = 0; j < N; j++) { 1254 mid = j * SECTOR + SECTOR / 2; wgt = s.w[j]; 1255 g.font = "700 12px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.45 + 0.5 * wgt); g.textAlign = 'center'; 1256 g.fillText(pad(j + 1), px(R_NUM, mid), py(R_NUM, mid)); 1257 g.font = "9px 'Courier New', monospace"; g.fillStyle = rzInk(0.42 + 0.45 * wgt); 1258 g.fillText(PHASES[j].read, px(R_READ, mid), py(R_READ, mid)); 1259 } 1260 1261 /* ââ boundary pulses: fired the moment the hand settles ââ */ 1262 for (j = 0; j < N; j++) { 1263 age = t - (j * PH + SWEEP); 1264 if (age < 0 || age >= PULSE) continue; 1265 p = age / PULSE; ang = (j + 1) * SECTOR; x = px(R_ARC, ang); y = py(R_ARC, ang); 1266 g.strokeStyle = tk.cols[j % 5]; g.fillStyle = tk.cols[j % 5]; 1267 g.globalAlpha = Math.pow(1 - p, 1.6) * 0.9; g.lineWidth = 1.4; 1268 g.beginPath(); g.arc(x, y, 4 + 26 * easeOut(p), 0, Math.PI * 2); g.stroke(); 1269 g.beginPath(); g.arc(x, y, 2.6, 0, Math.PI * 2); g.fill(); 1270 g.globalAlpha = (1 - p) * 0.9; g.lineWidth = 1.8; 1271 g.beginPath(); tickLine(186, 208, ang); g.stroke(); 1272 g.globalAlpha = 1; 1273 } 1274 1275 /* ââ hand ââ */ 1276 g.strokeStyle = rzInk(0.92); g.fillStyle = rzInk(0.92); g.lineWidth = 2; g.lineCap = 'round'; 1277 g.beginPath(); g.moveTo(CX, CY); g.lineTo(px(R_HAND, s.angle), py(R_HAND, s.angle)); g.stroke(); 1278 g.beginPath(); g.arc(px(R_HAND, s.angle), py(R_HAND, s.angle), 3.2, 0, Math.PI * 2); g.fill(); 1279 g.lineCap = 'butt'; 1280 1281 /* ââ centre paper chip ââ */ 1282 g.fillStyle = tk.paper; g.strokeStyle = rzInk(0.15); g.lineWidth = 1; 1283 g.beginPath(); g.arc(CX, CY, R_DISC, 0, Math.PI * 2); g.fill(); g.stroke(); 1284 if (g.setLineDash) { 1285 g.setLineDash([2, 4]); g.strokeStyle = rzInk(0.08); 1286 g.beginPath(); g.arc(CX, CY, R_DISC_IN, 0, Math.PI * 2); g.stroke(); 1287 g.setLineDash([]); 1288 } 1289 1290 /* kicker / numeral / name / accent mark / counter â crossfaded at boundaries */ 1291 g.font = "600 9px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.5); 1292 tracked('PHASE', CX, CY - 47, 1.6, 'center'); 1293 active = []; 1294 for (j = 0; j < N; j++) { if (s.w[j] > 0.003) active.push(j); } 1295 for (k = 0; k < active.length; k++) { 1296 j = active[k]; wgt = s.w[j]; 1297 g.globalAlpha = wgt; 1298 g.font = "800 30px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.95); g.textAlign = 'center'; 1299 g.fillText(pad(j + 1), CX, CY - 14); 1300 g.font = "700 11px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.8); 1301 tracked(PHASES[j].name, CX, CY + 14, 1.1, 'center'); 1302 g.fillStyle = tk.cols[j % 5]; g.fillRect(CX - 11, CY + 27, 22, 2); 1303 g.globalAlpha = 1; 1304 } 1305 /* the counter is continuous across a boundary, so never dim it when both texts agree */ 1306 g.font = "700 14px 'Courier New', monospace"; g.fillStyle = rzInk(0.9); g.textAlign = 'center'; 1307 if (active.length === 2 && counterText(active[0], s) === counterText(active[1], s)) { 1308 g.fillText(counterText(active[0], s), CX, CY + 46); 1309 } else { 1310 for (k = 0; k < active.length; k++) { 1311 j = active[k]; txt = counterText(j, s); 1312 g.globalAlpha = s.w[j]; g.fillText(txt, CX, CY + 46); g.globalAlpha = 1; 1313 } 1314 } 1315 } 1316 1317 /* ââ reduced motion: one composed frame, redrawn on resize / theme / font load ââ */ 1318 var T_STATIC = 2 * PH + 0.62 * SWEEP; 1319 function drawStatic() { draw(T_STATIC); } 1320 if (reduce) { 1321 drawStatic(); 1322 window.addEventListener('resize', drawStatic); 1323 window.addEventListener('load', drawStatic); 1324 document.addEventListener('rz-theme-change', drawStatic);
1325 try { if (document.fonts && document.fonts.ready) document.fonts.ready.then(drawStatic); } catch (e) {} 1326 return; 1327 } 1328 1329 /* ââ time-based loop; pauses when the tab is hidden or the canvas is off-screen ââ */ 1330 var t0 = 0, started = false, raf = 0, running = false, pausedAt = 0, inView = true; 1331 function now() { return (window.performance && performance.now) ? performance.now() : Date.now(); } 1332 function frame(ts) { 1333 raf = 0; 1334 if (!started) { started = true; t0 = ts; } 1335 draw((ts - t0) % LOOP); 1336 if (running) raf = requestAnimationFrame(frame); 1337 } 1338 function start() { 1339 if (running) return; 1340 running = true; 1341 if (pausedAt && started) t0 += now() - pausedAt; 1342 pausedAt = 0; 1343 if (!raf) raf = requestAnimationFrame(frame); 1344 } 1345 function stop() { 1346 if (!running) return; 1347 running = false; pausedAt = now(); 1348 if (raf) { cancelAnimationFrame(raf); raf = 0; } 1349 } 1350 function sync() { if (document.hidden || !inView) stop(); else start(); } 1351 document.addEventListener('visibilitychange', sync); 1352 if ('IntersectionObserver' in window) { 1353 new IntersectionObserver(function (entries) { inView = entries[0].isIntersecting; sync(); }, { threshold: 0 }).observe(cv); 1354 } 1355 sync(); 1356 })(); 1357 1358 /* ââ 9. CLOUD SECURITY â Architecture Scan âââââââââââââââââââ */ 1359 (function () { 1360 var cv = document.getElementById('rz-visual-cloud'); 1361 if (!cv) return; 1362 var W = 480, H = 480; 1363 var g = cv.getContext('2d'); 1364 if (!g) return; 1365 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store + zoom-aware transform */ 1366 1367 /* ------------------------------------------------------------ 1368 LAYERED PLANES â "Sheet 01" of the page's Architecture Blueprint. 1369 Four isometric planes (Identity â Data) stacked on a z-axis, each 1370 with ink check-nodes. A scan line sweeps the stack; as it crosses a 1371 plane its nodes resolve to a lime check or a crimson cross and the 1372 plane's front edge takes its accent. One finding gets a leader line 1373 (accent anchor on the plane edge, ink tick at the chip) to a paper 1374 chip. Tokens read at runtime, neutrals via rzInk(); time-based and 1375 eased; 12.6 s loop with a long hold; the stack floats as one object. 1376 ------------------------------------------------------------ */ 1377 1378 /* ---- geometry (logical px) ---- */ 1379 var N = 4; 1380 var PX = 30, PW = 210, PH = 58, SH = 46, DX = 10, PY0 = 82, GAP = 84; 1381 var ROT = ['purple', 'lime', 'crimson', 'indigo', 'yellow']; /* site accent rotation */ 1382 var PLANES = [ 1383 { idx: 'L1', name: 'IDENTITY', nodes: [[0.17, 0.30, 0], [0.42, 0.30, 0], [0.67, 0.30, 0], [0.50, 0.68, 0]] }, 1384 { idx: 'L2', name: 'NETWORK', nodes: [[0.15, 0.30, 0], [0.40, 0.30, 0], [0.65, 0.30, 1], [0.35, 0.68, 0]] }, 1385 { idx: 'L3', name: 'COMPUTE', nodes: [[0.20, 0.32, 0], [0.46, 0.32, 0], [0.72, 0.32, 0]] }, 1386 { idx: 'L4', name: 'DATA', nodes: [[0.16, 0.30, 0], [0.42, 0.30, 0], [0.68, 0.30, 0], [0.32, 0.68, 0]] } 1387 ]; 1388 /* a point on plane i: u along the front edge, v backâfront (0 = far edge) */ 1389 function pt(i, u, v) { 1390 return { x: PX + (N - 1 - i) * DX + SH * (1 - v) + u * PW, y: PY0 + i * GAP + v * PH }; 1391 } 1392 1393 /* ---- timeline (seconds) ---- */ 1394 var T_IN = 0.55, MOVE = 1.15, HOLD = 0.45, SEG = MOVE + HOLD; 1395 var T_SCAN_END = T_IN + N * SEG; /* 6.95 â line leaves the stack */ 1396 var T_RESET = 11.2, RESET_D = 0.7, T_LOOP = 12.6; 1397 1398 function clamp01(x) { return x < 0 ? 0 : x > 1 ? 1 : x; } 1399 function inOut3(x) { x = clamp01(x); return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; } 1400 function invInOut3(q) { q = clamp01(q); return q < 0.5 ? Math.pow(q / 4, 1 / 3) : 1 - Math.pow(2 * (1 - q), 1 / 3) / 2; } 1401 function out3(x) { x = clamp01(x); return 1 - Math.pow(1 - x, 3); } 1402 function out5(x) { x = clamp01(x); return 1 - Math.pow(1 - x, 5); } 1403 1404 /* scan waypoints: rest above each plane, exit below the last */ 1405 var WAY = [], i, k, p, n; 1406 for (i = 0; i < N; i++) { 1407 PLANES[i].top = PY0 + i * GAP; 1408 PLANES[i].bot = PLANES[i].top + PH; 1409 PLANES[i].acc = ROT[i % ROT.length]; 1410 PLANES[i].list = []; 1411 PLANES[i].tDone = T_IN + i * SEG + MOVE; /* plane fully crossed */ 1412 WAY.push(PLANES[i].top - 16); 1413 } 1414 WAY.push(PLANES[N - 1].bot + 16); 1415 1416 function scanY(t) { 1417 var s = t - T_IN; 1418 if (s <= 0) return WAY[0]; 1419 var seg = Math.floor(s / SEG); 1420 if (seg >= N) return WAY[N];
1421 var u = s - seg * SEG, q = u < MOVE ? inOut3(u / MOVE) : 1; 1422 return WAY[seg] + (WAY[seg + 1] - WAY[seg]) * q; 1423 } 1424 function scanAlpha(t) { 1425 if (t < T_IN) return out3((t - 0.1) / 0.45); 1426 if (t < T_SCAN_END) return 1; 1427 return 1 - out3((t - T_SCAN_END) / 0.5); 1428 } 1429 1430 /* nodes, with the exact moment the line crosses each one (inverse of the ease) */ 1431 var nodes = [], finding = null; 1432 for (i = 0; i < N; i++) { 1433 for (k = 0; k < PLANES[i].nodes.length; k++) { 1434 p = PLANES[i].nodes[k]; 1435 var q = pt(i, p[0], p[1]); 1436 n = { x: q.x, y: q.y, u: p[0], v: p[1], plane: i, bad: p[2] === 1 }; 1437 n.tHit = T_IN + i * SEG + MOVE * invInOut3((n.y - WAY[i]) / (WAY[i + 1] - WAY[i])); 1438 nodes.push(n); PLANES[i].list.push(n); 1439 if (n.bad && !finding) finding = n; 1440 } 1441 } 1442 var ANCHOR_X = pt(finding.plane, 1, finding.v).x; /* plane right edge at the finding's depth */ 1443 var T_CHIP = finding.tHit + 0.5; 1444 1445 /* ---- colour tokens: read once per frame, parsed to triplets ---- */ 1446 var FALL = { purple: '#7d14d7', indigo: '#5801f5', crimson: '#f50034', lime: '#85f403', yellow: '#e6e414' }; 1447 function parseCol(s) { 1448 s = (s || '').replace(/^\s+|\s+$/g, ''); 1449 var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s), h, v; 1450 if (m) { 1451 h = m[1]; 1452 if (h.length === 3) h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2); 1453 v = parseInt(h, 16); 1454 return [(v >> 16) & 255, (v >> 8) & 255, v & 255]; 1455 } 1456 m = /rgba?\(([^)]+)\)/.exec(s); 1457 if (m) { v = m[1].split(','); return [parseFloat(v[0]), parseFloat(v[1]), parseFloat(v[2])]; } 1458 return null; 1459 } 1460 function rgba(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + a + ')'; } 1461 function readTokens() { 1462 var cs = null, dark = document.documentElement.getAttribute('data-theme') === 'dark'; 1463 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 1464 function tok(name, fb) { 1465 var v = null; 1466 if (cs) { try { v = parseCol(cs.getPropertyValue(name)); } catch (e2) {} } 1467 return v || parseCol(fb); 1468 } 1469 return { 1470 dark: dark, 1471 purple: tok('--rz-purple', FALL.purple), 1472 indigo: tok('--rz-indigo', FALL.indigo), 1473 crimson: tok('--rz-crimson', FALL.crimson), 1474 lime: tok('--rz-lime', FALL.lime), 1475 yellow: tok('--rz-yellow', FALL.yellow), 1476 paper: tok('--rz-paper', dark ? '#0e0f14' : '#ffffff') 1477 }; 1478 } 1479 1480 /* ---- drawing helpers ---- */ 1481 var F_EYE = '600 9.5px "Oxanium", sans-serif'; 1482 var F_NAME = '700 10px "Oxanium", sans-serif'; 1483 var F_MONO = '8.5px "Courier New", Courier, monospace'; 1484 var F_MONO_B = '700 8.5px "Courier New", Courier, monospace'; 1485 1486 /* letter-spaced text (canvas letterSpacing is not universal); returns the width */ 1487 function tracked(str, x, y, track, align) { 1488 var w = 0, j, ch, cx; 1489 for (j = 0; j < str.length; j++) w += g.measureText(str.charAt(j)).width; 1490 w += track * (str.length - 1); 1491 cx = align === 'right' ? x - w : align === 'center' ? x - w / 2 : x; 1492 g.textAlign = 'left'; 1493 for (j = 0; j < str.length; j++) { ch = str.charAt(j); g.fillText(ch, cx, y); cx += g.measureText(ch).width + track; } 1494 return w; 1495 } 1496 function pad2(v) { return (v < 10 ? '0' : '') + v; } 1497 function roundRect(x, y, w, h, r) { 1498 g.beginPath(); 1499 g.moveTo(x + r, y); g.lineTo(x + w - r, y); g.quadraticCurveTo(x + w, y, x + w, y + r); 1500 g.lineTo(x + w, y + h - r); g.quadraticCurveTo(x + w, y + h, x + w - r, y + h); 1501 g.lineTo(x + r, y + h); g.quadraticCurveTo(x, y + h, x, y + h - r); 1502 g.lineTo(x, y + r); g.quadraticCurveTo(x, y, x + r, y); g.closePath(); 1503 } 1504 /* stroke the first `prog` (0..1) of a polyline â marks draw in stroke by stroke */ 1505 function partialPoly(pts, prog) { 1506 var total = 0, segs = [], j, dx, dy, L, left, f; 1507 for (j = 1; j < pts.length; j++) { dx = pts[j][0] - pts[j - 1][0];
1507 dy = pts[j][1] - pts[j - 1][1]; L = Math.sqrt(dx * dx + dy * dy); segs.push(L); total += L; } 1508 left = total * clamp01(prog); 1509 if (left <= 0) return; 1510 g.beginPath(); g.moveTo(pts[0][0], pts[0][1]); 1511 for (j = 1; j < pts.length; j++) { 1512 if (left >= segs[j - 1]) { g.lineTo(pts[j][0], pts[j][1]); left -= segs[j - 1]; } 1513 else { f = segs[j - 1] ? left / segs[j - 1] : 0; g.lineTo(pts[j - 1][0] + (pts[j][0] - pts[j - 1][0]) * f, pts[j - 1][1] + (pts[j][1] - pts[j - 1][1]) * f); break; } 1514 } 1515 g.stroke(); 1516 } 1517 function checkMark(x, y, prog) { partialPoly([[x - 2.8, y + 0.2], [x - 0.9, y + 2.1], [x + 3.0, y - 2.4]], prog); } 1518 function crossMark(x, y, prog) { 1519 partialPoly([[x - 2.5, y - 2.5], [x + 2.5, y + 2.5]], prog * 2); 1520 if (prog > 0.5) partialPoly([[x + 2.5, y - 2.5], [x - 2.5, y + 2.5]], (prog - 0.5) * 2); 1521 } 1522 1523 /* ---------------- sheet chrome (static; does not float) ---------------- */ 1524 function drawSheet(C, t, F, done, pass, fail) { 1525 var inset = 8, arm = 14, j, x, y, sx, sy; 1526 /* corner crop-brackets */ 1527 g.strokeStyle = rzInk(0.3); g.lineWidth = 1.25; g.lineCap = 'butt'; 1528 for (j = 0; j < 4; j++) { 1529 x = j & 1 ? W - inset : inset; y = j >> 1 ? H - inset : inset; sx = j & 1 ? -1 : 1; sy = j >> 1 ? -1 : 1; 1530 g.beginPath(); g.moveTo(x, y + sy * arm); g.lineTo(x, y); g.lineTo(x + sx * arm, y); g.stroke(); 1531 } 1532 /* head: eyebrow · five equal swatches · sheet readout */ 1533 g.textBaseline = 'alphabetic'; 1534 g.fillStyle = rzInk(0.55); g.font = F_EYE; 1535 var ew = tracked('ARCHITECTURE BLUEPRINT', 30, 33, 1.5, 'left'); 1536 for (j = 0; j < 5; j++) { g.fillStyle = rgba(C[ROT[j]], 0.95); g.fillRect(30 + ew + 12 + j * 9, 26, 6, 6); } 1537 /* live plane count crossfades through the reset */ 1538 var mix = Math.abs(2 * F - 1), shown = F >= 0.5 ? done : 0; 1539 g.fillStyle = rzInk(0.58 * (0.3 + 0.7 * mix)); g.font = F_MONO; 1540 tracked('SHEET 01 · SCAN ' + pad2(shown) + '/' + pad2(N), W - 30, 33, 0.8, 'right'); 1541 /* ruler */ 1542 var rx0 = 30, rx1 = W - 30, ry = 46; 1543 g.strokeStyle = rzInk(0.16); g.lineWidth = 1; 1544 g.beginPath(); g.moveTo(rx0, ry + 0.5); g.lineTo(rx1, ry + 0.5); g.stroke(); 1545 g.strokeStyle = rzInk(0.26); 1546 g.beginPath(); 1547 for (j = 0; rx0 + j <= rx1; j += 8) { var h = j % 40 === 0 ? 7 : 3; g.moveTo(rx0 + j + 0.5, ry); g.lineTo(rx0 + j + 0.5, ry - h); } 1548 g.stroke(); 1549 /* foot: hairline, mark legend, live tally */ 1550 var fy = H - 30; 1551 g.strokeStyle = rzInk(0.1); g.beginPath(); g.moveTo(30, fy - 13.5); g.lineTo(W - 30, fy - 13.5); g.stroke(); 1552 g.lineWidth = 1.5; g.lineCap = 'round'; g.lineJoin = 'round'; 1553 g.strokeStyle = rgba(C.lime, 0.95); checkMark(35, fy - 0.5, 1); 1554 g.fillStyle = rzInk(0.58); g.font = F_MONO; g.textBaseline = 'middle'; 1555 var lw = tracked('PASS', 43, fy, 0.8, 'left'); 1556 g.strokeStyle = rgba(C.crimson, 0.95); crossMark(43 + lw + 14, fy - 0.5, 1); 1557 g.fillStyle = rzInk(0.58); 1558 tracked('FINDING', 43 + lw + 22, fy, 0.8, 'left'); 1559 var sp = F >= 0.5 ? pass : 0, sf = F >= 0.5 ? fail : 0; 1560 g.fillStyle = rzInk(0.58 * (0.3 + 0.7 * mix)); 1561 tracked('PASS ' + pad2(sp) + ' · FINDINGS ' + pad2(sf), W - 30, fy, 0.8, 'right'); 1562 g.textBaseline = 'alphabetic'; g.lineCap = 'butt'; 1563 } 1564 1565 /* ---------------- one plane ---------------- */ 1566 function drawPlane(C, i, t, F, sy, sA) { 1567 var P = PLANES[i], a = pt(i, 0, 0), b = pt(i, 1, 0), c = pt(i, 1, 1), d = pt(i, 0, 1), j, r; 1568 /* translucent paper face */ 1569 g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(b.x, b.y); g.lineTo(c.x, c.y); g.lineTo(d.x, d.y); g.closePath(); 1570 g.fillStyle = rgba(C.paper, 0.86); g.fill(); 1571 /* dotted isometric grid */ 1572 g.fillStyle = rzInk(0.17); 1573 var rows = [0.17, 0.335, 0.5, 0.665, 0.83], vv, uu; 1574 for (j = 0; j < rows.length; j++) { 1575 vv = rows[j]; 1576 for (uu = 8; uu <= PW - 8; uu += 12) { 1577 r = pt(i, uu / PW, vv); 1578 g.beginPath(); g.arc(r.x, r.y, 0.9, 0, 6.2832); g.fill(); 1579 } 1580 } 1581 /* outline */ 1582 g.strokeStyle = rzInk(0.34); g.lineWidth = 1; g.lineJoin = 'miter'; 1583 g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(b.x, b.y); g.lineTo(c.x, c.y); g.lineTo(d.x, d.y); g.closePath(); g.stroke(); 1584 /* front edge: ink depth strip always; accent strip grows as the line crosses the plane */ 1585 g.fillStyle = rzInk(0.12); g.fillRect(d.x, d.y + 4, PW, 3); 1586 var prog = clamp01((sy - P.top) / PH) * F; 1587 if (prog > 0) { g.fillStyle = rgba(C[P.acc], 1); g.fillRect(d.x, d.y + 1, PW * prog, 3); } 1588 /* dashed topology edge through the nodes */ 1589 g.strokeStyle = rzInk(0.22); g.lineWidth = 1; g.setLineDash([3, 3]); 1590 g.beginPath(); 1591 for (j = 0; j < P.list.length; j++) { if (j === 0) g.moveTo(P.list[j].x, P.list[j].y); else g.lineTo(P.list[j].x, P.list[j].y); } 1592 g.stroke(); g.setLineDash([]); 1593 /* caption, bottom-left, on a paper halo */
1594 var cap = pt(i, 0.03, 0.87); 1595 g.font = F_MONO; var w1 = 0, cw; 1596 for (j = 0; j < P.idx.length; j++) w1 += g.measureText(P.idx.charAt(j)).width + 1; 1597 g.font = F_NAME; cw = w1 + 6; 1598 for (j = 0; j < P.name.length; j++) cw += g.measureText(P.name.charAt(j)).width + 1.4; 1599 g.fillStyle = rgba(C.paper, 1); g.fillRect(cap.x - 3, cap.y - 7, cw + 6, 12); 1600 g.textBaseline = 'alphabetic'; 1601 g.fillStyle = rzInk(0.55); g.font = F_MONO; 1602 var lw = tracked(P.idx, cap.x, cap.y + 3, 1, 'left'); 1603 g.fillStyle = rzInk(0.88); g.font = F_NAME; 1604 tracked(P.name, cap.x + lw + 7, cap.y + 3, 1.4, 'left'); 1605 /* severity marks, top-left, once the plane has been crossed (finding planes only) */ 1606 var nf = 0; 1607 for (j = 0; j < P.list.length; j++) if (P.list[j].bad) nf++; 1608 if (nf > 0 && t >= P.tDone && F > 0) { 1609 var sa = out3((t - P.tDone) / 0.45) * F, s0 = pt(i, 0.06, 0.2); 1610 g.globalAlpha = sa; 1611 for (j = 0; j < nf; j++) { g.fillStyle = rzInk(0.9); g.fillRect(s0.x + j * 10, s0.y - 3.5, 7, 7); } 1612 g.globalAlpha = 1; 1613 } 1614 /* nodes */ 1615 for (j = 0; j < P.list.length; j++) drawNode(C, P.list[j], t, F); 1616 } 1617 1618 function drawNode(C, n, t, F) { 1619 var hit = t >= n.tHit && F > 0, col = n.bad ? C.crimson : C.lime, r = 5.5, since = hit ? t - n.tHit : 0, q; 1620 /* resolve: one soft ring expanding out of the node */ 1621 if (hit && since < 0.9) { 1622 q = since / 0.9; 1623 g.strokeStyle = rgba(col, 0.6 * Math.pow(1 - q, 1.6) * F); g.lineWidth = 1; 1624 g.beginPath(); g.arc(n.x, n.y, r + 15 * out3(q), 0, 6.2832); g.stroke(); 1625 } 1626 /* the finding keeps a slow breathing ring */ 1627 if (hit && n.bad) { 1628 g.strokeStyle = rgba(col, (0.18 + 0.1 * Math.sin(t * 6.2832 / 3.4)) * F); g.lineWidth = 1; 1629 g.beginPath(); g.arc(n.x, n.y, 11, 0, 6.2832); g.stroke(); 1630 } 1631 /* node */ 1632 g.beginPath(); g.arc(n.x, n.y, r, 0, 6.2832); 1633 g.fillStyle = rgba(C.paper, 1); g.fill(); 1634 g.strokeStyle = rzInk(0.42 + (hit ? 0.46 * F : 0)); g.lineWidth = hit ? 1.2 : 1.1; g.stroke(); 1635 /* mark */ 1636 if (hit) { 1637 var mp = out5(since / 0.5); 1638 g.strokeStyle = rgba(col, F); g.lineWidth = 1.6; g.lineCap = 'round'; g.lineJoin = 'round'; 1639 if (n.bad) crossMark(n.x, n.y, mp); else checkMark(n.x, n.y, mp); 1640 g.lineCap = 'butt'; 1641 } 1642 } 1643 1644 /* ---------------- leader + paper chip ---------------- */ 1645 function drawCallout(C, t, F) { 1646 if (t < T_CHIP || F <= 0) return; 1647 var lp = out3((t - T_CHIP) / 0.55); 1648 var ca = out3((t - T_CHIP - 0.3) / 0.35) * F; 1649 var n = finding, ny = n.y, label = 'PLANE ' + pad2(n.plane + 1) + ' · 1 FINDING', tw = 0, j; 1650 g.font = F_MONO_B; 1651 for (j = 0; j < label.length; j++) tw += g.measureText(label.charAt(j)).width + 0.6; 1652 var chipH = 22, chipW = Math.round(tw + 26), chipX = W - 22 - chipW, chipY = Math.round(ny - chipH / 2); 1653 if (chipX < ANCHOR_X + 24) chipX = ANCHOR_X + 24; 1654 var L1 = ANCHOR_X - (n.x + 7), L2 = chipX - 3 - ANCHOR_X, run = (L1 + L2) * lp; 1655 /* stub across the plane, accent anchor on its edge, hairline on to the chip */ 1656 g.lineWidth = 1; 1657 g.strokeStyle = rzInk(0.42 * F); 1658 g.beginPath(); g.moveTo(n.x + 7, ny + 0.5); g.lineTo(n.x + 7 + Math.min(run, L1), ny + 0.5); g.stroke(); 1659 if (run > L1) { 1660 g.strokeStyle = rzInk(0.3 * F); 1661 g.beginPath(); g.moveTo(ANCHOR_X, ny + 0.5); g.lineTo(ANCHOR_X + Math.min(run - L1, L2), ny + 0.5); g.stroke(); 1662 var as = out3((run - L1) / 10); 1663 g.beginPath(); g.arc(ANCHOR_X, ny, 3.5 * as, 0, 6.2832); 1664 g.fillStyle = rgba(C.crimson, F); g.fill(); 1665 g.strokeStyle = rgba(C.paper, F); g.lineWidth = 1.5; g.stroke(); 1666 } 1667 if (lp >= 1) { 1668 g.strokeStyle = rzInk(0.9 * F); g.lineWidth = 1; 1669 g.beginPath(); g.moveTo(chipX - 2.5, ny - 5.5); g.lineTo(chipX - 2.5, ny + 5.5); g.stroke(); 1670 } 1671 if (ca <= 0) return; 1672 /* chip: paper, 1px ink hairline, crimson bar, soft lift */ 1673 g.save(); 1674 g.globalAlpha = ca; 1675 g.shadowColor = C.dark ? 'rgba(0,0,0,0.55)' : 'rgba(20,21,26,0.10)'; 1676 g.shadowBlur = 12; g.shadowOffsetY = 4; 1677 roundRect(chipX + 0.5, chipY + 0.5, chipW, chipH, 4); 1678 g.fillStyle = rgba(C.paper, 1); g.fill(); 1679 g.shadowColor = 'transparent'; g.shadowBlur = 0; g.shadowOffsetY = 0; 1680 g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke(); 1681 g.save(); roundRect(chipX + 0.5, chipY + 0.5, chipW, chipH, 4); g.clip(); 1682 g.fillStyle = rgba(C.crimson, 1); g.fillRect(chipX, chipY, 3, chipH + 1);
1683 g.restore(); 1684 g.fillStyle = rzInk(0.85); g.font = F_MONO_B; g.textBaseline = 'middle'; 1685 tracked(label, chipX + 14, ny + 1, 0.6, 'left'); 1686 g.textBaseline = 'alphabetic'; 1687 g.restore(); 1688 } 1689 1690 /* ---------------- scan line: hugs the plane it is crossing, re-aims during the hold ---------------- */ 1691 function scanExtent(t) { 1692 var s = t - T_IN, seg = s <= 0 ? 0 : Math.floor(s / SEG), u = s - seg * SEG, k0, k1, m; 1693 if (seg >= N) { seg = N - 1; u = MOVE; } 1694 k0 = seg; k1 = Math.min(N - 1, seg + 1); 1695 m = (s > 0 && u > MOVE) ? inOut3((u - MOVE) / HOLD) : 0; 1696 var l0 = pt(k0, 0, 1).x, l1 = pt(k1, 0, 1).x, r0 = pt(k0, 1, 0).x, r1 = pt(k1, 1, 0).x; 1697 return [l0 + (l1 - l0) * m - 10, r0 + (r1 - r0) * m + 10]; 1698 } 1699 function drawScan(C, t, sy, sA) { 1700 if (sA <= 0) return; 1701 var ex = scanExtent(t), x0 = ex[0], x1 = ex[1]; 1702 var grad = g.createLinearGradient(0, sy - 18, 0, sy); 1703 grad.addColorStop(0, rzInk(0)); grad.addColorStop(1, rzInk(0.06 * sA)); 1704 g.fillStyle = grad; g.fillRect(x0, sy - 18, x1 - x0, 18); 1705 g.strokeStyle = rzInk(0.72 * sA); g.lineWidth = 1; 1706 g.beginPath(); g.moveTo(x0, sy + 0.5); g.lineTo(x1, sy + 0.5); g.stroke(); 1707 g.beginPath(); 1708 g.moveTo(x0 + 0.5, sy - 4); g.lineTo(x0 + 0.5, sy + 5); 1709 g.moveTo(x1 - 0.5, sy - 4); g.lineTo(x1 - 0.5, sy + 5); 1710 g.stroke(); 1711 } 1712 1713 /* ---------------- compose one frame: t = loop time, ta = absolute time (float) ---------------- */ 1714 function draw(t, ta) { 1715 var C = readTokens(), j; 1716 var F = t < T_RESET ? 1 : 1 - inOut3((t - T_RESET) / RESET_D); 1717 var sy = scanY(t), sA = clamp01(scanAlpha(t)); 1718 var done = 0, pass = 0, fail = 0; 1719 for (j = 0; j < N; j++) if (t >= PLANES[j].tDone) done++; 1720 for (j = 0; j < nodes.length; j++) if (t >= nodes[j].tHit) { if (nodes[j].bad) fail++; else pass++; } 1721 1722 g.clearRect(0, 0, W, H); 1723 drawSheet(C, t, F, done, pass, fail); 1724 1725 /* the stack floats as one object (absolute time, so the loop seam never jumps) */ 1726 var fx = ta ? 1.5 * Math.sin(ta * 6.2832 / 9.3) : 0; 1727 var fy = ta ? 3.0 * Math.sin(ta * 6.2832 / 6.7) : 0; 1728 g.save(); g.translate(fx, fy); 1729 /* dashed z-axis risers joining the planes into one stack */ 1730 var d0 = pt(0, 0, 1), d3 = pt(N - 1, 0, 1), b0 = pt(0, 1, 0), b3 = pt(N - 1, 1, 0); 1731 g.strokeStyle = rzInk(0.2); g.lineWidth = 1; g.setLineDash([2, 4]); 1732 g.beginPath(); g.moveTo(d3.x, d3.y); g.lineTo(d0.x, d0.y); g.moveTo(b3.x, b3.y); g.lineTo(b0.x, b0.y); g.stroke(); 1733 g.setLineDash([]); 1734 for (j = 0; j < N; j++) drawPlane(C, j, t, F, sy, sA); 1735 drawCallout(C, t, F); 1736 drawScan(C, t, sy, sA); 1737 g.restore(); 1738 } 1739 1740 /* ---------------- loop / reduced motion / visibility ---------------- */ 1741 var reduce = false;
1742 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 1743 function now() { return (window.performance && performance.now) ? performance.now() : Date.now(); } 1744 1745 if (reduce) { 1746 /* one composed frame: scan complete, marks and chip resolved, no float */ 1747 var still = function () { draw(9.0, 0); }; 1748 still(); 1749 if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(still); 1750 window.addEventListener('resize', still); 1751 window.addEventListener('load', still); 1752 document.addEventListener('rz-theme-change', still); 1753 return; 1754 } 1755 1756 var raf = 0, t0 = now(), pausedAt = 0; 1757 function frame() { 1758 raf = 0; 1759 var el = (now() - t0) / 1000; 1760 draw(el % T_LOOP, el); 1761 raf = requestAnimationFrame(frame); 1762 } 1763 function start() { if (!raf) raf = requestAnimationFrame(frame); } 1764 function stop() { if (raf) { cancelAnimationFrame(raf); raf = 0; } } 1765 document.addEventListener('visibilitychange', function () { 1766 if (document.hidden) { pausedAt = now(); stop(); } 1767 else { if (pausedAt) { t0 += now() - pausedAt; pausedAt = 0; } start(); } 1768 }); 1769 if (document.hidden) pausedAt = now(); else start(); 1770 })(); 1771 1772 /* ââ 10. CODE REVIEW â SAST Vulnerability Scanner ââââââââââââ */ 1773 (function () { 1774 /* ââ 10. CODE REVIEW â Code Scan (diff pane on paper) âââââââââ 1775 A paper diff pane in the language of the RZ-METHOD:codereview 1776 section: Courier line numbers + sign column, ink token bars for 1777 code, a review cursor that reads line by line, and three findings 1778 that get a â/+ accent gutter pair, a numbered accent-ring pin and 1779 a paper cue chip (INJECTION · AUTHZ · SECRET). Everything neutral 1780 goes through rzInk(); paper + accents are read from the CSS 1781 tokens each frame, so dark mode follows automatically. 1782 Timeline is time-based (~13.5 s loop with an end hold), eased, 1783 pauses when the tab is hidden, and collapses to one composed 1784 static frame under prefers-reduced-motion. */ 1785 var cv = document.getElementById('rz-visual-code'); 1786 if (!cv) return; 1787 var W = 480, H = 480; 1788 var g = cv.getContext('2d'); 1789 if (!g) return; 1790 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store */ 1791 1792 /* ---- tokens: read each frame, light values as fallback ---- */ 1793 var FB = { purple: '#7d14d7', indigo: '#5801f5', crimson: '#f50034', lime: '#85f403', yellow: '#e6e414', paper: '#ffffff' }; 1794 function readTokens() { 1795 var cs = null, t = {}; 1796 try { cs = window.getComputedStyle(document.documentElement); } catch (e) { cs = null; } 1797 function tok(name, fb) { 1798 var v = ''; 1799 if (cs) { try { v = (cs.getPropertyValue(name) || '').replace(/^\s+|\s+$/g, ''); } catch (e) { v = ''; } } 1800 return v || fb; 1801 } 1802 t.purple = tok('--rz-purple', FB.purple); 1803 t.indigo = tok('--rz-indigo', FB.indigo); 1804 t.crimson = tok('--rz-crimson', FB.crimson); 1805 t.lime = tok('--rz-lime', FB.lime); 1806 t.yellow = tok('--rz-yellow', FB.yellow); 1807 t.paper = tok('--rz-paper', FB.paper); 1808 t.dark = document.documentElement.getAttribute('data-theme') === 'dark'; 1809 return t; 1810 } 1811 function withAlpha(col, a) { 1812 var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(col); 1813 if (!m) return col; 1814 var h = m[1]; 1815 if (h.length === 3) h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2); 1816 var n = parseInt(h, 16); 1817 return 'rgba(' + (n >> 16 & 255) + ',' + (n >> 8 & 255) + ',' + (n & 255) + ',' + a + ')'; 1818 } 1819 1820 /* ---- easing / helpers ---- */ 1821 function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; } 1822 function easeInOutCubic(u) { u = clamp01(u); return u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2; } 1823 function easeOutCubic(u) { u = clamp01(u); return 1 - Math.pow(1 - u, 3); } 1824 function lerp(a, b, u) { return a + (b - a) * u; } 1825 function rrect(x, y, w, h, tl, tr, br, bl) { 1826 g.beginPath(); 1827 g.moveTo(x + tl, y); 1828 g.lineTo(x + w - tr, y); g.arcTo(x + w, y, x + w, y + tr, tr); 1829 g.lineTo(x + w, y + h - br); g.arcTo(x + w, y + h, x + w - br, y + h, br); 1830 g.lineTo(x + bl, y + h); g.arcTo(x, y + h, x, y + h - bl, bl); 1831 g.lineTo(x, y + tl); g.arcTo(x, y, x + tl, y, tl); 1832 g.closePath(); 1833 } 1834 /* tracked text (canvas letterSpacing is not universal): returns width */ 1835 function spaced(str, x, y, sp, align) { 1836 var i, w = 0, ws = []; 1837 for (i = 0; i < str.length; i++) { ws[i] = g.measureText(str.charAt(i)).width;
1837 w += ws[i]; } 1838 w += sp * (str.length - 1); 1839 var cx = align === 'right' ? x - w : align === 'center' ? x - w / 2 : x; 1840 var keep = g.textAlign; g.textAlign = 'left'; 1841 for (i = 0; i < str.length; i++) { g.fillText(str.charAt(i), cx, y); cx += ws[i] + sp; } 1842 g.textAlign = keep; 1843 return w; 1844 } 1845 var OX = "'Oxanium', sans-serif", MONO = "'Courier New', monospace"; 1846 1847 /* ---- geometry (logical 480Ã480) ---- */ 1848 var CX0 = 22, CY0 = 26, CW = 436, CH = 428; /* the pane */ 1849 var CX1 = CX0 + CW, CY1 = CY0 + CH; /* 458 / 454 */ 1850 var HEAD_H = 42; /* pane header band */ 1851 var ROW_H = 22, N = 14, ROWS_Y = CY0 + HEAD_H + 12; 1852 var BODY_Y1 = ROWS_Y + N * ROW_H; /* 388 */ 1853 var FOOT_Y = BODY_Y1 + 16, HAZ_H = 7; /* status bar + hazard strip */ 1854 var FOOT_H = CY1 - HAZ_H - FOOT_Y; 1855 var LN_X = CX0 + 30, SIGN_X = CX0 + 40, CODE_X = CX0 + 50, INDENT = 16; 1856 var BAR_H = 6, GAP = 6, CHIP_R = CX1 - 16; 1857 1858 /* ---- mock code: [width, ink alpha] token bars per row ---- */ 1859 var ROWS = [ 1860 { ln: 82, ind: 0, seg: [[22, .38], [70, .24], [34, .16]] }, 1861 { ln: 83, ind: 1, seg: [[46, .24], [10, .16], [58, .18]] }, 1862 { ln: 84, ind: 1, seg: [[28, .38], [36, .24], [62, .14]] }, 1863 { ln: 85, ind: 1, seg: [[40, .24], [14, .16], [96, .14]], find: 0 }, 1864 { ln: 85, ind: 1, seg: [[40, .24], [14, .16], [72, .18], [30, .14]], fix: 0 }, 1865 { ln: 86, ind: 1, seg: [[22, .38], [52, .24]] }, 1866 { ln: 87, ind: 0, seg: [[22, .38], [78, .24], [40, .16]] }, 1867 { ln: 88, ind: 1, seg: [[30, .38], [54, .24], [40, .16]], find: 1 }, 1868 { ln: 88, ind: 1, seg: [[22, .38], [34, .24], [12, .16], [64, .18]], fix: 1 }, 1869 { ln: 89, ind: 2, seg: [[44, .24], [30, .16]] }, 1870 { ln: 90, ind: 1, seg: [[30, .38], [60, .24]] }, 1871 { ln: 91, ind: 1, seg: [[46, .24], [10, .16], [88, .14]], find: 2 }, 1872 { ln: 91, ind: 1, seg: [[46, .24], [10, .16], [40, .24], [44, .16]], fix: 2 }, 1873 { ln: 92, ind: 1, seg: [[30, .38], [48, .24], [22, .16]] } 1874 ]; 1875 var FINDINGS = [ 1876 { row: 3, fixRow: 4, label: 'INJECTION', acc: 'purple' }, 1877 { row: 7, fixRow: 8, label: 'AUTHZ', acc: 'crimson' }, 1878 { row: 11, fixRow: 12, label: 'SECRET', acc: 'indigo' } 1879 ]; 1880 function rowBarsEnd(row) { 1881 var x = CODE_X + row.ind * INDENT, i; 1882 for (i = 0; i < row.seg.length; i++) x += row.seg[i][0] + (i ? GAP : 0); 1883 return x; 1884 } 1885 1886 /* ---- timeline (seconds) ---- */ 1887 var T_HOLD0 = 0.8, MOVE = 0.24, DW_CTX = 0.22, DW_FIND = 1.15, DW_FIX = 0.6; 1888 var arrive = [], dwell = [], i, tt = T_HOLD0; 1889 for (i = 0; i < N; i++) { 1890 arrive[i] = tt; 1891 dwell[i] = ROWS[i].find != null ? DW_FIND : ROWS[i].fix != null ? DW_FIX : DW_CTX; 1892 tt += dwell[i] + MOVE; 1893 } 1894 var T_END = arrive[N - 1] + dwell[N - 1]; /* cursor finished reading */ 1895 var CUR_OUT = 0.4, HOLD_END = 1.6, RESET = 0.6; 1896 var DUR = T_END + CUR_OUT + HOLD_END + RESET; /* â 13.5 s */ 1897 var T_STATIC = arrive[N - 1] + 0.1; /* composed frame for reduced motion */ 1898 1899 function cursorRow(t) { 1900 if (t <= arrive[0]) return 0; 1901 for (var k = 0; k < N; k++) { 1902 var leave = arrive[k] + dwell[k]; 1903 if (t <= leave || k === N - 1) return k; 1904 if (t < arrive[k + 1]) return k + easeInOutCubic((t - leave) / (arrive[k + 1] - leave)); 1905 } 1906 return N - 1; 1907 } 1908 1909 /* ---- one frame ---- */ 1910 function draw(t) { 1911 g.clearRect(0, 0, W, H); 1912 var tk = readTokens(); 1913 var ACC = { purple: tk.purple, indigo: tk.indigo, crimson: tk.crimson, lime: tk.lime, yellow: tk.yellow }; 1914 var SPECTRUM = [tk.purple, tk.crimson, tk.lime, tk.indigo, tk.yellow]; 1915 1916 var gfade = t > DUR - RESET ? 1 - easeInOutCubic((t - (DUR - RESET)) / RESET) : 1; 1917 var cRow = cursorRow(t); 1918 var cAlpha = t < 0.4 ? easeOutCubic(t / 0.4) : t < T_END ? 1 : 1 - easeInOutCubic((t - T_END) / CUR_OUT); 1919 var readAll = easeOutCubic((t - T_END) / 0.4); 1920 var numA = (t < 0.3 ? easeOutCubic(t / 0.3) : 1) * gfade; /* live counters fade through the loop seam */ 1921 var k, r, x, y, yc, j; 1922 1923 /* finding / fix reveal envelopes */ 1924 var F = []; 1925 var nMinus = 0, nPlus = 0; 1926 for (k = 0; k < FINDINGS.length; k++) {
1927 var a = arrive[FINDINGS[k].row], a2 = arrive[FINDINGS[k].fixRow]; 1928 F[k] = { 1929 e1: easeOutCubic((t - a) / 0.45) * gfade, /* gutter, sign, strike */ 1930 e2: easeOutCubic((t - a - 0.10) / 0.40) * gfade, /* pin */ 1931 e3: easeInOutCubic((t - a - 0.20) / 0.40) * gfade, /* leader */ 1932 e4: easeOutCubic((t - a - 0.40) / 0.45) * gfade, /* chip */ 1933 f1: easeOutCubic((t - a2) / 0.45) * gfade, /* fix row */ 1934 a2: a2 1935 }; 1936 if (t >= a) nMinus++; 1937 if (t >= a2) nPlus++; 1938 } 1939 1940 /* ââ pane: paper card with soft lift, 4/12/12/12 radius ââ */ 1941 g.save(); 1942 g.shadowColor = tk.dark ? 'rgba(0,0,0,0.35)' : 'rgba(20,21,26,0.06)'; 1943 g.shadowBlur = 14; g.shadowOffsetY = 2; 1944 rrect(CX0, CY0, CW, CH, 4, 12, 12, 12); 1945 g.fillStyle = tk.paper; g.fill(); 1946 g.restore(); 1947 1948 g.save(); 1949 rrect(CX0, CY0, CW, CH, 4, 12, 12, 12); 1950 g.clip(); 1951 1952 /* header band */ 1953 g.fillStyle = rzInk(0.05); 1954 g.fillRect(CX0, CY0, CW, HEAD_H); 1955 g.fillStyle = rzInk(0.13); 1956 g.fillRect(CX0, CY0 + HEAD_H, CW, 1); 1957 1958 /* ruler ticks along the top threshold (masked at both ends) */ 1959 for (x = CX0 + 12; x < CX1 - 4; x += 24) { 1960 var u = (x - CX0) / CW, mask = u < 0.12 ? u / 0.12 : u > 0.88 ? (1 - u) / 0.12 : 1; 1961 g.fillStyle = rzInk(0.24 * mask); 1962 g.fillRect(x, CY0, 1, 9); 1963 } 1964 1965 /* header readouts */ 1966 g.textBaseline = 'middle'; 1967 yc = CY0 + HEAD_H / 2 + 1; 1968 g.textAlign = 'left'; 1969 g.font = '9px ' + MONO; g.fillStyle = rzInk(0.5); 1970 g.fillText('c4d2a90', CX0 + 14, yc); 1971 var hw = g.measureText('c4d2a90').width; 1972 g.font = 'bold 9.5px ' + MONO; g.fillStyle = rzInk(0.92); 1973 g.fillText('api/orders.py', CX0 + 14 + hw + 10, yc); 1974 g.textAlign = 'right'; 1975 g.font = '9px ' + MONO; g.fillStyle = rzInk(0.5); 1976 g.fillText('03/05', CX1 - 14, yc); 1977 var iw = g.measureText('03/05').width; 1978 g.fillStyle = rzInk(0.5 * numA); 1979 g.fillText('+' + nPlus + ' â' + nMinus, CX1 - 14 - iw - 12, yc); 1980 1981 /* ââ review cursor band (under the rows) ââ */ 1982 if (cAlpha > 0.001) { 1983 y = ROWS_Y + cRow * ROW_H; 1984 g.fillStyle = rzInk(0.045 * cAlpha); 1985 g.fillRect(CX0, y, CW, ROW_H); 1986 g.fillStyle = rzInk(0.30 * cAlpha); 1987 g.fillRect(CX0, y, CW, 1); 1988 g.fillStyle = rzInk(0.12 * cAlpha); 1989 g.fillRect(CX0, y + ROW_H - 1, CW, 1); 1990 /* caret */ 1991 g.fillStyle = rzInk(0.75 * cAlpha); 1992 g.beginPath(); 1993 g.moveTo(CX0 + 7, y + ROW_H / 2 - 4); 1994 g.lineTo(CX0 + 12, y + ROW_H / 2); 1995 g.lineTo(CX0 + 7, y + ROW_H / 2 + 4); 1996 g.closePath(); g.fill(); 1997 } 1998 1999 /* ââ rows ââ */ 2000 for (r = 0; r < N; r++) { 2001 var row = ROWS[r]; 2002 y = ROWS_Y + r * ROW_H; yc = y + ROW_H / 2; 2003 var read = Math.max(clamp01(cRow - r), readAll) * gfade; 2004 var rm = 0.62 + 0.38 * read; /* unread rows sit lighter */ 2005 var fx = CODE_X + row.ind * INDENT; 2006 var fk = null; 2007 if (row.find != null) fk = F[row.find]; 2008 if (row.fix != null) { 2009 fk = F[row.fix]; 2010 if (fk.f1 <= 0.001) continue; /* fix line not written yet */ 2011 var acc = ACC[FINDINGS[row.fix].acc]; 2012 g.fillStyle = rzInk(0.05 * fk.f1); 2013 g.fillRect(CX0, y, CW, ROW_H); 2014 g.fillStyle = withAlpha(acc, fk.f1); 2015 g.fillRect(CX0, yc - ROW_H / 2 * fk.f1, 3, ROW_H * fk.f1); 2016 g.textAlign = 'right'; g.font = '9px ' + MONO; g.fillStyle = rzInk(0.42 * fk.f1); 2017 g.fillText(String(row.ln), LN_X, yc + 0.5); 2018 g.textAlign = 'center'; g.font = 'bold 10px ' + MONO; g.fillStyle = rzInk(0.9 * fk.f1); 2019 g.fillText('+', SIGN_X, yc + 0.5); 2020 for (j = 0; j < row.seg.length; j++) { 2021 var gw = easeOutCubic((t - fk.a2 - 0.06 * j) / 0.4) * gfade; 2022 if (gw > 0) { 2023 g.fillStyle = rzInk((row.seg[j][1] + 0.1) * gw); 2024 rrect(fx, yc - BAR_H / 2, row.seg[j][0] * gw, BAR_H, 3, 3, 3, 3); g.fill(); 2025 } 2026 fx += row.seg[j][0] + GAP; 2027 } 2028 continue; 2029 } 2030 /* context / finding row */ 2031 g.textAlign = 'right'; g.font = '9px ' + MONO; 2032 g.fillStyle = rzInk(0.42 * (0.72 + 0.28 * read)); 2033 g.fillText(String(row.ln), LN_X, yc + 0.5); 2034 var e1 = fk ? fk.e1 : 0; 2035 if (fk && e1 > 0) { 2036 var accF = ACC[FINDINGS[row.find].acc]; 2037 g.fillStyle = withAlpha(accF, e1); 2038 g.fillRect(CX0, yc - ROW_H / 2 * e1, 3, ROW_H * e1); 2039 g.textAlign = 'center'; g.font = 'bold 10px ' + MONO; g.fillStyle = rzInk(0.9 * e1); 2040 g.fillText('â', SIGN_X, yc + 0.5); 2041 } 2042 for (j = 0; j < row.seg.length; j++) { 2043 g.fillStyle = rzInk(lerp(row.seg[j][1] * rm, 0.13, e1)); 2044 rrect(fx, yc - BAR_H / 2, row.seg[j][0], BAR_H, 3, 3, 3, 3); g.fill(); 2045 fx += row.seg[j][0] + GAP; 2046 } 2047 if (e1 > 0) { /* strike-through on the flagged line */ 2048 var sx0 = CODE_X + row.ind * INDENT, sx1 = fx - GAP; 2049 g.fillStyle = rzInk(0.55 * e1); 2050 g.fillRect(sx0, yc - 0.5, (sx1 - sx0) * e1, 1); 2051 } 2052 } 2053 2054 /* ââ pins, leaders, chips ââ */ 2055 for (k = 0; k < FINDINGS.length; k++) { 2056 var fd = FINDINGS[k], fe = F[k]; 2057 if (fe.e2 <= 0.001) continue; 2058 var accP = ACC[fd.acc]; 2059 var py = ROWS_Y + fd.row * ROW_H + ROW_H / 2; 2060 var px = rowBarsEnd(ROWS[fd.row]) + 15; 2061 /* chip metrics */ 2062 g.font = '600 8.5px ' + OX; 2063 var lw = 0; 2064 for (j = 0; j < fd.label.length; j++) lw += g.measureText(fd.label.charAt(j)).width; 2065 lw += 1.2 * (fd.label.length - 1); 2066 var chipW = Math.round(lw + 20), chipH = 18; 2067 var chipX = CHIP_R - chipW + 6 * (1 - fe.e4), chipY = py - chipH / 2; 2068 /* leader */ 2069 if (fe.e3 > 0) { 2070 g.strokeStyle = withAlpha(accP, 0.6 * fe.e3); g.lineWidth = 1; 2071 g.beginPath(); 2072 g.moveTo(px + 9, py + 0.5); 2073 g.lineTo(px + 9 + (CHIP_R - chipW - 4 - (px + 9)) * fe.e3, py + 0.5); 2074 g.stroke(); 2075 } 2076 /* numbered pin with accent ring */ 2077 var pr = 7 * (0.4 + 0.6 * fe.e2); 2078 g.beginPath(); g.arc(px, py, pr, 0, Math.PI * 2); 2079 g.fillStyle = tk.paper; g.fill(); 2080 g.strokeStyle = withAlpha(accP, fe.e2); g.lineWidth = 2; g.stroke(); 2081 var na = clamp01((fe.e2 - 0.5) * 2); 2082 if (na > 0) { 2083 g.textAlign = 'center'; g.font = '700 8px ' + OX; g.fillStyle = rzInk(0.95 * na); 2084 g.fillText(String(k + 1), px, py + 0.5); 2085 } 2086 /* paper cue chip */ 2087 if (fe.e4 > 0) { 2088 rrect(chipX, chipY, chipW, chipH, 4, 4, 4, 4); 2089 g.fillStyle = tk.paper; g.fill(); 2090 g.strokeStyle = rzInk(0.15 * fe.e4); g.lineWidth = 1; g.stroke(); 2091 g.save(); 2092 rrect(chipX, chipY, chipW, chipH, 4, 4, 4, 4); g.clip(); 2093 g.fillStyle = withAlpha(accP, fe.e4); 2094 g.fillRect(chipX, chipY, 2, chipH);
2095 g.restore(); 2096 g.font = '600 8.5px ' + OX; g.fillStyle = rzInk(0.9 * fe.e4); 2097 spaced(fd.label, chipX + 11, py + 0.5, 1.2, 'left'); 2098 } 2099 } 2100 2101 /* ââ status bar ââ */ 2102 g.fillStyle = rzInk(0.13); 2103 g.fillRect(CX0, FOOT_Y, CW, 1); 2104 var yf = FOOT_Y + FOOT_H / 2 + 0.5; 2105 for (k = 0; k < 5; k++) { /* five equal accents, eyebrow mark */ 2106 g.fillStyle = SPECTRUM[k]; 2107 g.fillRect(CX0 + 14 + k * 5, yf - 1.5, 5, 3); 2108 } 2109 var nFound = 0; 2110 for (k = 0; k < FINDINGS.length; k++) if (F[k].e1 > 0) nFound++; 2111 g.textAlign = 'left'; g.font = '600 9.5px ' + OX; g.fillStyle = rzInk(0.85); 2112 var tx = CX0 + 14 + 25 + 10; 2113 tx += spaced('5 FILES · ', tx, yf, 1.3, 'left') + 1.3; 2114 g.fillStyle = rzInk(0.85 * numA); 2115 tx += spaced(String(nFound), tx, yf, 1.3, 'left') + 1.3; 2116 g.fillStyle = rzInk(0.85); 2117 spaced(' FINDINGS', tx, yf, 1.3, 'left'); 2118 /* file progress: 5 segments, current file fills with the cursor */ 2119 var segW = 16, segGap = 4, sx = CX1 - 14 - (5 * segW + 4 * segGap); 2120 var prog = clamp01(cRow / (N - 1)) * gfade; 2121 for (k = 0; k < 5; k++) { 2122 var sxx = sx + k * (segW + segGap); 2123 g.fillStyle = rzInk(k < 2 ? 0.7 : 0.15); 2124 g.fillRect(sxx, yf - 1.5, segW, 3); 2125 if (k === 2 && prog > 0) { g.fillStyle = rzInk(0.7); g.fillRect(sxx, yf - 1.5, segW * prog, 3); } 2126 } 2127 var lnNow = ROWS[Math.round(cRow)].ln; 2128 g.textAlign = 'right'; g.font = '8.5px ' + MONO; g.fillStyle = rzInk(0.5 * numA); 2129 g.fillText('LN ' + (lnNow < 100 ? '0' : '') + lnNow, sx - 12, yf); 2130 2131 /* ââ hazard threshold along the bottom edge ââ */ 2132 g.fillStyle = rzInk(0.07); 2133 g.fillRect(CX0, CY1 - HAZ_H - 1, CW, 1); 2134 g.save(); 2135 g.beginPath(); g.rect(CX0, CY1 - HAZ_H, CW, HAZ_H); g.clip(); 2136 g.strokeStyle = rzInk(0.2); g.lineWidth = 4.5; 2137 g.beginPath(); 2138 for (x = CX0 - 10; x < CX1 + 10; x += 19.8) { 2139 g.moveTo(x, CY1 + 2); g.lineTo(x + 12, CY1 - HAZ_H - 2); 2140 } 2141 g.stroke(); 2142 g.restore(); 2143 2144 /* ââ corner crop-brackets ââ */ 2145 g.fillStyle = rzInk(0.28); 2146 var bx0 = CX0 + 7, by0 = CY0 + 7, bx1 = CX1 - 7, by1 = CY1 - 7; 2147 g.fillRect(bx0, by0, 13, 2); g.fillRect(bx0, by0, 2, 13); 2148 g.fillRect(bx1 - 13, by0, 13, 2); g.fillRect(bx1 - 2, by0, 2, 13);
2149 g.fillRect(bx0, by1 - 2, 13, 2); g.fillRect(bx0, by1 - 13, 2, 13); 2150 g.fillRect(bx1 - 13, by1 - 2, 13, 2); g.fillRect(bx1 - 2, by1 - 13, 2, 13); 2151 2152 g.restore(); /* pane clip */ 2153 2154 /* pane border */ 2155 rrect(CX0 + 0.5, CY0 + 0.5, CW - 1, CH - 1, 4, 12, 12, 12); 2156 g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke(); 2157 } 2158 2159 /* ---- reduced motion: one composed frame, redrawn only if the 2160 backing store is reset (resize / theme) ---- */ 2161 var reduce = false; 2162 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) { reduce = false; } 2163 if (reduce) { 2164 var drawStatic = function () { draw(T_STATIC); }; 2165 drawStatic(); 2166 try { if (document.fonts && document.fonts.ready) document.fonts.ready.then(drawStatic); } catch (e) {} 2167 window.addEventListener('resize', drawStatic); 2168 window.addEventListener('load', drawStatic); 2169 document.addEventListener('rz-theme-change', drawStatic); 2170 document.addEventListener('rz-why-variant', drawStatic); 2171 return; 2172 } 2173 2174 /* ---- RAF loop, time-based, paused while hidden ---- */ 2175 var raf = 0, running = false, t0 = performance.now(), hiddenAt = 0; 2176 function loop(now) { 2177 raf = requestAnimationFrame(loop); 2178 if (cv.offsetParent === null) return; /* wrapper is display:none (â¤900px): skip the draw */ 2179 draw(((now - t0) / 1000) % DUR); 2180 } 2181 function start() { if (running) return; running = true; raf = requestAnimationFrame(loop); } 2182 function stop() { running = false; if (raf) cancelAnimationFrame(raf); raf = 0; } 2183 document.addEventListener('visibilitychange', function () { 2184 if (document.hidden) { hiddenAt = performance.now(); stop(); } 2185 else { if (hiddenAt) { t0 += performance.now() - hiddenAt; hiddenAt = 0; } start(); } 2186 }); 2187 if (document.hidden) { hiddenAt = performance.now(); draw(0); } else { start(); } 2188 })(); 2189 2190 /* ââ 11. DIGITAL FORENSICS â Hex Dump Evidence Extractor ââââââ */ 2191 (function () { 2192 'use strict'; 2193 /* ââ 11. DIGITAL FORENSICS â Evidence Ledger (chain of custody) ââ 2194 A vertical ruler rail on the left; five paper evidence cards 2195 (EV-001â¦EV-005) file in one at a time, a SHA-256 fragment types 2196 in, an accent double-ring seal stamps the card (with a subtle 2197 squash), and chain links draw between consecutive checkpoints. 2198 Ends on an "INTEGRITY VERIFIED" tick, holds, fades, loops. 2199 Tokens are read every frame (paper + five accents); neutral 2200 ink goes through rzInk(). Transparent background. */ 2201 var cv = document.getElementById('rz-visual-forensics'); 2202 if (!cv) return; 2203 var W = 480, H = 480; 2204 var g = cv.getContext('2d'); 2205 if (!g) return; 2206 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store */ 2207 2208 /* ---- layout (logical px) ---- */ 2209 var N = 5; 2210 var RAIL_X = 52, RAIL_TOP = 48; 2211 var CARD_X = 82, CARD_W = 362, CARD_H = 52, CARD_GAP = 18, BODY_Y = 58; 2212 var HEAD_RULE_Y = 40, FOOT_RULE_Y = 404, RAIL_BOT = FOOT_RULE_Y; 2213 2214 /* ---- illustrative ledger entries (ids + readouts only) ---- */ 2215 var ENTRIES = [ 2216 { id: 'EV-001', label: 'TRIAGE', t: 'T+0h', hash: '9c2e17a4d0f3b8e6â¦b04d' }, 2217 { id: 'EV-002', label: 'ACQUISITION', t: 'T+4h', hash: '4fa1d3c07e92b5a8â¦77e2' }, 2218 { id: 'EV-003', label: 'TIMELINE', t: 'T+2d', hash: 'e08b5c1f6a3d9427â¦1a9f' }, 2219 { id: 'EV-004', label: 'ROOT CAUSE', t: 'T+5d', hash: '71d4aa2e8c5f03b9â¦c3e6' }, 2220 { id: 'EV-005', label: 'REPORT', t: 'T+10d', hash: 'b6f2093d5e7a1c48â¦58d1' } 2221 ]; 2222 /* same five-accent rotation as the methodology ledger rows */ 2223 var ACCENT_VARS = ['--rz-purple', '--rz-lime', '--rz-crimson', '--rz-indigo', '--rz-yellow']; 2224 var ACCENT_FALLBACK = ['#7d14d7', '#85f403', '#f50034', '#5801f5', '#e6e414']; 2225 2226 /* ---- timeline (seconds) ---- */ 2227 var T_INTRO = 0.5, T_PITCH = 1.55; 2228 var C_SLIDE = [0, 0.45], C_TYPE = [0.35, 1.0], C_STAMP = [1.02, 1.44], C_LINK = [1.25, 1.75]; 2229 var T_LAST = T_INTRO + (N - 1) * T_PITCH + C_LINK[1]; /* 8.45 â last card filed */ 2230 var T_TAIL = [T_LAST, T_LAST + 0.4]; /* rail runs down to the footer */ 2231 var T_VERIFY = [T_LAST + 0.4, T_LAST + 1.0]; /* ring + tick + label */ 2232 var T_HOLD_END = T_LAST + 4.2; /* 12.65 â hold on the sealed ledger */ 2233 var T_OUT = [T_HOLD_END, T_HOLD_END + 0.6]; /* soft fade before the loop */ 2234 var LOOP = T_OUT[1] + 0.2; /* ~13.45 s */ 2235 2236 var reduce = false;
2237 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 2238 2239 /* ---- helpers ---- */ 2240 function seg(t, a, b) { var p = (t - a) / (b - a); return p <= 0 ? 0 : (p >= 1 ? 1 : p); } 2241 function easeOutQuint(p) { return 1 - Math.pow(1 - p, 5); } 2242 function easeOutCubic(p) { return 1 - Math.pow(1 - p, 3); } 2243 function easeInOutCubic(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; } 2244 function pad2(n) { return n < 10 ? '0' + n : '' + n; } 2245 function fOx(w, px) { return w + ' ' + px + 'px Oxanium, sans-serif'; } 2246 function fMono(w, px) { return w + ' ' + px + 'px "Courier New", Courier, monospace'; } 2247 2248 /* design tokens, read once per frame */ 2249 function theme() { 2250 var cs = null, acc = [], i; 2251 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 2252 function v(name, fb) { 2253 var s = cs ? cs.getPropertyValue(name) : ''; 2254 s = s ? s.replace(/^\s+|\s+$/g, '') : ''; 2255 return s || fb; 2256 } 2257 for (i = 0; i < N; i++) acc.push(v(ACCENT_VARS[i], ACCENT_FALLBACK[i])); 2258 return { 2259 dark: document.documentElement.getAttribute('data-theme') === 'dark', 2260 paper: v('--rz-paper', '#ffffff'), 2261 acc: acc 2262 }; 2263 } 2264 2265 /* rounded-rect path; r = number or [tl, tr, br, bl] */ 2266 function rr(x, y, w, h, r) { 2267 var tl, tr, br, bl; 2268 if (typeof r === 'number') { tl = tr = br = bl = r; } else { tl = r[0]; tr = r[1]; br = r[2]; bl = r[3]; } 2269 g.beginPath(); 2270 g.moveTo(x + tl, y); 2271 g.lineTo(x + w - tr, y); g.arcTo(x + w, y, x + w, y + tr, tr); 2272 g.lineTo(x + w, y + h - br); g.arcTo(x + w, y + h, x + w - br, y + h, br); 2273 g.lineTo(x + bl, y + h); g.arcTo(x, y + h, x, y + h - bl, bl); 2274 g.lineTo(x, y + tl); g.arcTo(x, y, x + tl, y, tl); 2275 g.closePath(); 2276 } 2277 2278 /* letter-spaced text (uppercase eyebrows); align 'left' | 'right' */ 2279 function tracked(txt, x, y, px, em, align) { 2280 var sp = px * em, i, n = txt.length, widths = [], total = 0, cx, prev = g.textAlign; 2281 for (i = 0; i < n; i++) { widths[i] = g.measureText(txt.charAt(i)).width; total += widths[i]; } 2282 total += sp * (n - 1); 2283 cx = align === 'right' ? x - total : x; 2284 g.textAlign = 'left'; 2285 for (i = 0; i < n; i++) { g.fillText(txt.charAt(i), cx, y); cx += widths[i] + sp; } 2286 g.textAlign = prev; 2287 } 2288 2289 function dotY(i) { return BODY_Y + i * (CARD_H + CARD_GAP) + CARD_H / 2; } 2290 2291 /* per-card state from its local time */ 2292 function cardState(lt) { 2293 var s = { on: lt >= 0, slide: 0, typed: 0, caret: false, stampP: 0, dotFill: 0, 2294 sealScale: 1, sealAlpha: 0, squash: 1, link: 0 }; 2295 if (lt < 0) return s; 2296 s.slide = easeOutQuint(seg(lt, C_SLIDE[0], C_SLIDE[1])); 2297 s.typed = seg(lt, C_TYPE[0], C_TYPE[1]); 2298 var pSt = seg(lt, C_STAMP[0], C_STAMP[1]); 2299 s.stampP = pSt; 2300 s.caret = lt >= 0.18 && pSt <= 0; 2301 if (pSt > 0) { 2302 /* stamp: fly in large, land slightly under, settle (rhymes with the CSS keyframe) */ 2303 if (pSt < 0.6) { s.sealScale = 1.9 - 0.96 * easeOutCubic(pSt / 0.6); s.sealAlpha = Math.min(1, pSt / 0.14); } 2304 else { s.sealScale = 0.94 + 0.06 * easeInOutCubic((pSt - 0.6) / 0.4); s.sealAlpha = 1; } 2305 s.squash = 1 - 0.032 * Math.sin(Math.PI * seg(pSt, 0.42, 0.92)); /* card takes the impact */ 2306 s.dotFill = seg(pSt, 0.55, 0.8); /* checkpoint lights */ 2307 } 2308 s.link = easeInOutCubic(seg(lt, C_LINK[0], C_LINK[1])); 2309 return s; 2310 } 2311 2312 /* ---- drawing ---- */ 2313 function drawCrops() { 2314 var m = 8, L = 14; 2315 g.strokeStyle = rzInk(0.3); g.lineWidth = 1.5; g.lineCap = 'butt'; 2316 g.beginPath(); 2317 g.moveTo(m, m + L); g.lineTo(m, m); g.lineTo(m + L, m); 2318 g.moveTo(W - m - L, m); g.lineTo(W - m, m); g.lineTo(W - m, m + L); 2319 g.moveTo(W - m, H - m - L); g.lineTo(W - m, H - m); g.lineTo(W - m - L, H - m); 2320 g.moveTo(m + L, H - m); g.lineTo(m, H - m); g.lineTo(m, H - m - L); 2321 g.stroke(); 2322 } 2323 2324 function drawTape(x, y, w, h, th) { 2325 var k; 2326 g.save(); 2327 g.beginPath(); g.rect(x, y, w, h); g.clip(); 2328 g.strokeStyle = rzInk(th.dark ? 0.16 : 0.13); g.lineWidth = 3.5; g.lineCap = 'butt'; 2329 g.beginPath(); 2330 for (k = -h; k < w + h; k += 8) { g.moveTo(x + k, y + h + 1); g.lineTo(x + k + h + 2, y - 1); } 2331 g.stroke();
2332 g.restore(); 2333 } 2334 2335 function drawHeader(th, cards, fade) { 2336 var i, x, la, hgt; 2337 g.textAlign = 'left'; 2338 g.font = fOx('600', 9); g.fillStyle = rzInk(0.88); 2339 tracked('EVIDENCE CUSTODY LOG', 20, 21, 9, 0.16, 'left'); 2340 drawTape(256, 13, 44, 8, th); 2341 g.font = fOx('600', 7.5); g.fillStyle = rzInk(0.45); 2342 tracked('CASE REF', 314, 20, 7.5, 0.12, 'left'); 2343 g.fillStyle = rzInk(0.3); g.fillRect(364, 13, 26, 7); g.fillRect(394, 13, 12, 7); /* redacted */ 2344 /* five swatches light up as entries are sealed */ 2345 for (i = 0; i < N; i++) { 2346 x = 416 + i * 9; 2347 g.fillStyle = rzInk(0.12); g.fillRect(x, 13, 6, 6); 2348 la = cards[i].dotFill * fade; 2349 if (la > 0) { g.save(); g.globalAlpha = la; g.fillStyle = th.acc[i]; g.fillRect(x, 13, 6, 6); g.restore(); } 2350 } 2351 /* ruler */ 2352 g.strokeStyle = rzInk(0.13); g.lineWidth = 1; 2353 g.beginPath(); g.moveTo(20, HEAD_RULE_Y + 0.5); g.lineTo(460, HEAD_RULE_Y + 0.5); g.stroke(); 2354 g.strokeStyle = rzInk(0.22); 2355 g.beginPath(); 2356 for (x = 20; x <= 460; x += 8) { hgt = ((x - 20) % 40 === 0) ? 9 : 4; g.moveTo(x + 0.5, HEAD_RULE_Y); g.lineTo(x + 0.5, HEAD_RULE_Y - hgt); } 2357 g.stroke(); 2358 } 2359 2360 function drawRail() { 2361 var y, len; 2362 g.strokeStyle = rzInk(0.13); g.lineWidth = 1; 2363 g.beginPath(); g.moveTo(RAIL_X, RAIL_TOP); g.lineTo(RAIL_X, RAIL_BOT); g.stroke(); 2364 g.strokeStyle = rzInk(0.22); 2365 g.beginPath(); 2366 for (y = RAIL_TOP; y <= RAIL_BOT; y += 8) { len = ((y - RAIL_TOP) % 32 === 0) ? 9 : 4; g.moveTo(RAIL_X + 5, y + 0.5); g.lineTo(RAIL_X + 5 + len, y + 0.5); } 2367 g.stroke(); 2368 g.save(); 2369 g.translate(30, RAIL_BOT - 4); g.rotate(-Math.PI / 2); 2370 g.font = fOx('600', 7.5); g.fillStyle = rzInk(0.45); g.textAlign = 'left'; 2371 tracked('CHAIN OF CUSTODY', 0, 0, 7.5, 0.16, 'left'); 2372 g.restore(); 2373 } 2374 2375 function drawLink(x, ym, q, th, fade) { 2376 g.save(); 2377 g.globalAlpha = fade * q; 2378 g.translate(x, ym); g.scale(0.6 + 0.4 * q, 0.6 + 0.4 * q); 2379 g.lineWidth = 1.4; g.strokeStyle = rzInk(0.85); g.fillStyle = th.paper; 2380 rr(-3.5, -11, 7, 13, 3.5); g.fill(); g.stroke(); 2381 rr(-3.5, -2, 7, 13, 3.5); g.fill(); g.stroke(); 2382 g.restore(); 2383 } 2384 2385 function drawIntegrity(th, cards, pTail, fade) { 2386 var yEnd = RAIL_TOP, i, ya, yb, p, a, y; 2387 for (i = 0; i < N; i++) { 2388 ya = i === 0 ? RAIL_TOP : dotY(i - 1); yb = dotY(i); p = cards[i].link; 2389 if (p <= 0) break; 2390 yEnd = ya + (yb - ya) * p; 2391 if (p < 1) break; 2392 } 2393 if (i === N && pTail > 0) yEnd = dotY(N - 1) + (RAIL_BOT - dotY(N - 1)) * pTail; 2394 if (yEnd > RAIL_TOP && fade > 0) { 2395 g.save(); g.globalAlpha = fade; 2396 g.strokeStyle = rzInk(0.9); g.lineWidth = 2; g.lineCap = 'butt'; 2397 g.beginPath(); g.moveTo(RAIL_X, RAIL_TOP); g.lineTo(RAIL_X, yEnd); g.stroke(); 2398 for (i = 1; i < N; i++) { 2399 p = cards[i].link; if (p <= 0.5) continue; 2400 drawLink(RAIL_X, (dotY(i - 1) + dotY(i)) / 2, easeOutCubic(seg(p, 0.5, 0.9)), th, fade); 2401 } 2402 g.restore(); 2403 } 2404 /* checkpoint dots: hollow ink ring, filled with the card's accent on sealing */ 2405 for (i = 0; i < N; i++) { 2406 if (!cards[i].on) continue; 2407 a = cards[i].slide * fade; if (a <= 0) continue; 2408 y = dotY(i); 2409 g.save(); g.globalAlpha = a; 2410 g.fillStyle = th.paper; g.beginPath(); g.arc(RAIL_X, y, 6.5, 0, Math.PI * 2); g.fill(); 2411 g.strokeStyle = rzInk(0.85); g.lineWidth = 1.5; g.beginPath(); g.arc(RAIL_X, y, 3.8, 0, Math.PI * 2); g.stroke(); 2412 if (cards[i].dotFill > 0) { 2413 g.globalAlpha = a * cards[i].dotFill; 2414 g.fillStyle = th.acc[i]; g.beginPath(); g.arc(RAIL_X, y, 4.7, 0, Math.PI * 2); g.fill(); 2415 } 2416 g.restore(); 2417 } 2418 } 2419 2420 function drawSeal(cx, cy, s, acc, a) { 2421 g.save(); 2422 g.globalAlpha = a * s.sealAlpha; 2423 g.translate(cx, cy); g.scale(s.sealScale, s.sealScale); g.rotate(-0.12); 2424 g.strokeStyle = acc; g.lineWidth = 2; 2425 g.beginPath(); g.arc(0, 0, 14, 0, Math.PI * 2); g.stroke(); 2426 g.globalAlpha = a * s.sealAlpha * 0.6; g.lineWidth = 1; 2427 g.beginPath(); g.arc(0, 0, 10.5, 0, Math.PI * 2); g.stroke(); 2428 g.globalAlpha = a * s.sealAlpha; 2429 g.strokeStyle = rzInk(0.9); g.lineWidth = 2; g.lineCap = 'round'; g.lineJoin = 'round'; 2430 g.beginPath(); g.moveTo(-5, 0.5); g.lineTo(-1.5, 4); g.lineTo(5.5, -4); g.stroke();
2431 g.restore(); 2432 } 2433 2434 function drawCard(i, s, th, fade) { 2435 if (!s.on) return; 2436 var a = s.slide * fade; 2437 if (a <= 0) return; 2438 var e = ENTRIES[i]; 2439 var x = CARD_X + (1 - s.slide) * 26, y = BODY_Y + i * (CARD_H + CARD_GAP); 2440 var w = CARD_W, h = CARD_H, n, typedTxt, tw; 2441 g.save(); 2442 g.globalAlpha = a; 2443 g.translate(x + w / 2, y + h / 2); g.scale(1, s.squash); g.translate(-(x + w / 2), -(y + h / 2)); 2444 /* soft lift */ 2445 g.fillStyle = th.dark ? 'rgba(0,0,0,0.28)' : 'rgba(20,21,26,0.045)'; 2446 rr(x + 1, y + 2, w - 2, h, [3, 8, 8, 8]); g.fill(); 2447 g.fillStyle = th.dark ? 'rgba(0,0,0,0.16)' : 'rgba(20,21,26,0.022)'; 2448 rr(x + 2, y + 4, w - 4, h, [3, 8, 8, 8]); g.fill(); 2449 /* paper chip */ 2450 g.fillStyle = th.paper; rr(x, y, w, h, [3, 8, 8, 8]); g.fill(); 2451 g.strokeStyle = rzInk(0.16); g.lineWidth = 1; g.stroke(); 2452 /* accent tick + id + label + time */ 2453 g.fillStyle = th.acc[i]; g.fillRect(x + 12, y + 13, 3, 12); 2454 g.textAlign = 'left'; g.font = fOx('700', 11); g.fillStyle = rzInk(0.92); g.fillText(e.id, x + 21, y + 23); 2455 g.font = fOx('600', 8); g.fillStyle = rzInk(0.5); tracked(e.label, x + 72, y + 22, 8, 0.12, 'left'); 2456 g.font = fMono('700', 9); g.fillStyle = rzInk(0.55); g.textAlign = 'right'; g.fillText(e.t, x + w - 64, y + 23); 2457 /* hash line */ 2458 g.textAlign = 'left'; g.font = fOx('600', 7.5); g.fillStyle = rzInk(0.45); tracked('SHA-256', x + 12, y + 42, 7.5, 0.12, 'left'); 2459 n = Math.floor(s.typed * e.hash.length + 1e-6); 2460 typedTxt = e.hash.substring(0, n); 2461 g.font = fMono('400', 9.5); g.fillStyle = rzInk(0.82); 2462 if (n > 0) g.fillText(typedTxt, x + 58, y + 42); 2463 if (s.caret) { tw = n > 0 ? g.measureText(typedTxt).width : 0; g.fillStyle = rzInk(0.5); g.fillRect(x + 58 + tw + 1, y + 34, 5, 10); } 2464 if (s.stampP > 0) drawSeal(x + w - 30, y + h / 2, s, th.acc[i], a); 2465 g.restore(); 2466 } 2467 2468 function drawTick(cx, cy, p) { 2469 if (p <= 0) return; 2470 var A = [-3.4, 0.4], B = [-1, 2.8], C = [3.8, -2.6]; 2471 var l1 = Math.sqrt(Math.pow(B[0] - A[0], 2) + Math.pow(B[1] - A[1], 2)); 2472 var l2 = Math.sqrt(Math.pow(C[0] - B[0], 2) + Math.pow(C[1] - B[1], 2)); 2473 var d = p * (l1 + l2), u, v; 2474 g.beginPath(); g.moveTo(cx + A[0], cy + A[1]); 2475 if (d <= l1) { u = d / l1; g.lineTo(cx + A[0] + (B[0] - A[0]) * u, cy + A[1] + (B[1] - A[1]) * u); } 2476 else { g.lineTo(cx + B[0], cy + B[1]); v = (d - l1) / l2; g.lineTo(cx + B[0] + (C[0] - B[0]) * v, cy + B[1] + (C[1] - B[1]) * v); } 2477 g.stroke(); 2478 } 2479 2480 function drawFooter(t, sealed, pVer, fade) { 2481 var x, SY = 432, aLive, pulse, pr, pt, pl; 2482 g.strokeStyle = rzInk(0.13); g.lineWidth = 1; 2483 g.beginPath(); g.moveTo(20, FOOT_RULE_Y + 0.5); g.lineTo(460, FOOT_RULE_Y + 0.5); g.stroke(); 2484 g.strokeStyle = rzInk(0.22); g.beginPath(); 2485 for (x = 20; x <= 460; x += 24) { g.moveTo(x + 0.5, FOOT_RULE_Y + 1); g.lineTo(x + 0.5, FOOT_RULE_Y + 7); } 2486 g.stroke(); 2487 g.font = fMono('400', 8); g.fillStyle = rzInk(0.45); 2488 tracked('SHA-256 · UTC · WRITE-BLOCKED', 458, SY + 3.5, 8, 0.06, 'right'); 2489 /* live status â verified */ 2490 aLive = (1 - seg(pVer, 0, 0.35)) * fade * seg(t, 0, 0.4); 2491 if (aLive > 0) { 2492 g.save(); g.globalAlpha = aLive; 2493 pulse = reduce ? 0.8 : 0.3 + 0.55 * (0.5 + 0.5 * Math.sin(t * Math.PI * 2 / 1.6)); 2494 g.fillStyle = rzInk(pulse); g.beginPath(); g.arc(24, SY, 3, 0, Math.PI * 2); g.fill(); 2495 g.font = fOx('600', 9); g.fillStyle = rzInk(0.7); tracked('SEALING', 34, SY + 3.5, 9, 0.14, 'left'); 2496 g.font = fMono('700', 9); g.fillStyle = rzInk(0.5); g.textAlign = 'left'; g.fillText(pad2(sealed) + '/' + pad2(N), 96, SY + 3.5); 2497 g.restore(); 2498 } 2499 if (pVer > 0 && fade > 0) { 2500 g.save(); g.globalAlpha = fade; 2501 pr = easeOutCubic(seg(pVer, 0, 0.55)); 2502 g.strokeStyle = rzInk(0.85); g.lineWidth = 1.5; g.lineCap = 'round'; g.lineJoin = 'round'; 2503 g.beginPath(); g.arc(24, SY, 7, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * pr); g.stroke(); 2504 pt = easeOutCubic(seg(pVer, 0.35, 0.8)); 2505 g.lineWidth = 1.8; drawTick(24, SY, pt); 2506 pl = easeOutCubic(seg(pVer, 0.45, 1)); 2507 g.globalAlpha = fade * pl; 2508 g.font = fOx('700', 10.5); g.fillStyle = rzInk(0.92); 2509 tracked('INTEGRITY VERIFIED', 38 - 6 * (1 - pl), SY + 4, 10.5, 0.12, 'left');
2510 g.restore(); 2511 } 2512 } 2513 2514 function draw(t, th) { 2515 var i, cards = [], sealed = 0, st; 2516 var fade = 1 - easeInOutCubic(seg(t, T_OUT[0], T_OUT[1])); 2517 g.clearRect(0, 0, W, H); 2518 g.textBaseline = 'alphabetic'; g.lineJoin = 'miter'; 2519 for (i = 0; i < N; i++) { 2520 st = cardState(t - (T_INTRO + i * T_PITCH)); 2521 if (st.dotFill >= 1) sealed++; 2522 cards.push(st); 2523 } 2524 drawCrops(); 2525 drawHeader(th, cards, fade); 2526 drawRail(); 2527 drawIntegrity(th, cards, easeInOutCubic(seg(t, T_TAIL[0], T_TAIL[1])), fade); 2528 for (i = 0; i < N; i++) drawCard(i, cards[i], th, fade); 2529 drawFooter(t, sealed, seg(t, T_VERIFY[0], T_VERIFY[1]), fade); 2530 } 2531 2532 /* ---- reduced motion: one composed frame of the sealed ledger ---- */ 2533 if (reduce) { 2534 var pending = 0; 2535 var redraw = function () { 2536 if (pending) return; 2537 pending = requestAnimationFrame(function () { pending = 0; draw(T_HOLD_END - 0.5, theme()); }); 2538 }; 2539 redraw(); 2540 window.addEventListener('resize', redraw); 2541 window.addEventListener('load', redraw); 2542 document.addEventListener('rz-theme-change', redraw); 2543 document.addEventListener('rz-why-variant', redraw); 2544 if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(redraw); 2545 return; 2546 } 2547 2548 /* ---- RAF loop, paused while the tab is hidden ---- */ 2549 var raf = 0, t0 = 0, pausedAt = 0; 2550 function loop(now) { 2551 raf = 0; 2552 if (!t0) t0 = now; 2553 if (cv.clientWidth) draw(((now - t0) / 1000) % LOOP, theme()); /* skip when hidden by layout */ 2554 raf = requestAnimationFrame(loop); 2555 } 2556 function start() { if (!raf) raf = requestAnimationFrame(loop); } 2557 function stop() { if (raf) { cancelAnimationFrame(raf); raf = 0; } } 2558 document.addEventListener('visibilitychange', function () { 2559 if (document.hidden) { pausedAt = performance.now(); stop(); } 2560 else { if (t0 && pausedAt) t0 += performance.now() - pausedAt; pausedAt = 0; start(); } 2561 }); 2562 start(); 2563 })(); 2564 2565 /* ââ 12. MALWARE ANALYSIS â Sandbox Process Tree âââââââââââââ */ 2566 (function () { 2567 var cv = document.getElementById('rz-visual-malware'); 2568 if (!cv) return; 2569 var W = 480, H = 480; 2570 var g = cv.getContext('2d'); 2571 if (!g) return; 2572 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store + zoom-aware transform */ 2573 2574 /* ââ timeline (seconds, one loop) ââââââââââââââââââââââââââââââ 2575 STATIC lane: sample in, hashed 0.0 â 2.4 2576 DYNAMIC lane: process tree spawns 2.4 â 8.0 2577 RE lane: flagged nodes resolved 8.0 â 9.9 2578 verdict chip, tree settles, hold 9.9 â 12.7 2579 fade out, clean loop 12.7 â 13.6 */ 2580 var LOOP = 13.6; 2581 var T_ROOT = 0.3, T_HASH = 1.0, T_DYN = 2.4, T_RE = 8.0, T_VERD = 9.9, T_SETTLE = 10.6, T_OUT = 12.7; 2582 var FADE = 0.8, SW = 0.6, LINK_D = 0.55, SPAWN_LAG = 0.42, SPAWN_D = 0.45, T_STATIC = 11.8; 2583 2584 /* ââ frame geometry (mirrors the methodology "isolated lab" frame) ââ */ 2585 var FX = 16, FY = 16, FW = 448, FH = 448, PAD = 18, BUS = 46; 2586 2587 /* ââ the tree: t = link start, flag = suspicious at, tagAt = resolved in RE ââ */ 2588 var nodes = [ 2589 { id: 'sample.exe', x: 240, y: 104, r: 9, p: -1, t: T_ROOT, root: true }, 2590 { id: 'cmd.exe', x: 126, y: 176, r: 7, p: 0, t: 2.70, stub: true }, 2591 { id: 'svchost.exe', x: 240, y: 176, r: 7, p: 0, t: 3.25 }, 2592 { id: 'powershell.exe', x: 354, y: 176, r: 7, p: 0, t: 3.80, flag: 4.70, tag: 'T1059.001', tagAt: 8.25 }, 2593 { id: 'whoami.exe', x: 52, y: 248, r: 7, p: 1, t: 4.55, stub: true }, 2594 { id: 'net.exe', x: 126, y: 248, r: 7, p: 1, t: 4.95 }, 2595 { id: 'schtasks.exe', x: 200, y: 248, r: 7, p: 1, t: 5.35, flag: 6.10, tag: 'T1053.005', tagAt: 8.65 }, 2596 { id: 'rundll32.exe', x: 310, y: 248, r: 7, p: 3, t: 5.95, stub: true, flag: 6.90, tag: 'T1218.011', tagAt: 9.05 }, 2597 { id: 'conhost.exe', x: 398, y: 248, r: 7, p: 3, t: 6.35 }, 2598 { id: 'cdn-â¢â¢â¢â¢â¢[.]top:443', x: 310, y: 320, r: 8, p: 7, t: 7.10, stub: true, net: true, flag: 7.85, tag: 'T1071.001', tagAt: 9.45 } 2599 ]; 2600 /* junction dots where a parent fans out to several children */ 2601 (function () { 2602 var i, n, kids = {}; 2603 for (i = 1; i < nodes.length; i++) { n = nodes[i]; kids[n.p] = (kids[n.p] || 0) + 1; if (n.stub) nodes[n.p].fan = n.t + 0.22; } 2604 for (i = 0; i < nodes.length; i++) if (kids[i] < 2) nodes[i].fan = 0; 2605 })(); 2606 2607 var LANES = [{ lbl: 'STATIC', key: 'indigo' }, { lbl: 'DYNAMIC', key: 'crimson' }, { lbl: 'RE', key: 'lime' }];
2608 var HASH = 'sha256 3f9aâ¦e21c'; 2609 var VERDICT = 'FAMILY MATCHED · IOCs 12'; 2610 var VERDICT_SUB = 'AsyncRAT · T1071.001'; 2611 2612 /* ââ helpers ââ */ 2613 function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; } 2614 function seg(t, s, d) { return clamp01((t - s) / d); } 2615 function eio(t) { t = clamp01(t); return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } 2616 function eo(t) { t = clamp01(t); return 1 - Math.pow(1 - t, 3); } 2617 function lerp(a, b, k) { return a + (b - a) * k; } 2618 function lerpSpan(a, b, k) { return [lerp(a[0], b[0], k), lerp(a[1], b[1], k)]; } 2619 function pad2(n) { return (n < 10 ? '0' : '') + n; } 2620 function now() { return (window.performance && window.performance.now) ? window.performance.now() : Date.now(); } 2621 2622 var LIGHT = { purple: '#7d14d7', indigo: '#5801f5', crimson: '#f50034', lime: '#85f403', yellow: '#e6e414', paper: '#ffffff' }; 2623 var DARK = { purple: '#9a45ee', indigo: '#7a56ff', crimson: '#ff3355', lime: '#8ef513', yellow: '#f0ee2a', paper: '#0e0f14' }; 2624 function tokens() { 2625 var root = document.documentElement, fb = root.getAttribute('data-theme') === 'dark' ? DARK : LIGHT, cs = null, out = {}, k, v; 2626 try { cs = window.getComputedStyle(root); } catch (e) {} 2627 for (k in fb) if (fb.hasOwnProperty(k)) { 2628 v = ''; 2629 if (cs) { try { v = (cs.getPropertyValue('--rz-' + k) || '').replace(/^\s+|\s+$/g, ''); } catch (e2) {} } 2630 out[k] = v || fb[k]; 2631 } 2632 return out; 2633 } 2634 2635 function spacedW(txt, sp) { var i, w = 0; for (i = 0; i < txt.length; i++) w += g.measureText(txt.charAt(i)).width + (i < txt.length - 1 ? sp : 0); return w; } 2636 function spaced(txt, x, y, sp, align) { 2637 var w = spacedW(txt, sp), sx = align === 'center' ? x - w / 2 : align === 'right' ? x - w : x, i, ch, prev = g.textAlign; 2638 g.textAlign = 'left'; 2639 for (i = 0; i < txt.length; i++) { ch = txt.charAt(i); g.fillText(ch, sx, y); sx += g.measureText(ch).width + sp; } 2640 g.textAlign = prev; 2641 return w; 2642 } 2643 function rrect(x, y, w, h, r) { 2644 g.beginPath(); g.moveTo(x + r, y); g.lineTo(x + w - r, y); g.quadraticCurveTo(x + w, y, x + w, y + r); 2645 g.lineTo(x + w, y + h - r); g.quadraticCurveTo(x + w, y + h, x + w - r, y + h); g.lineTo(x + r, y + h); 2646 g.quadraticCurveTo(x, y + h, x, y + h - r); g.lineTo(x, y + r); g.quadraticCurveTo(x, y, x + r, y); g.closePath(); 2647 } 2648 function chip(x, y, w, h, tk) { rrect(x + 0.5, y + 0.5, w - 1, h - 1, 5); g.fillStyle = tk.paper; g.fill(); g.lineWidth = 1; g.strokeStyle = rzInk(0.15); g.stroke(); } 2649 function shape(n, r) { 2650 g.beginPath(); 2651 if (n.net) { g.moveTo(n.x, n.y - r); g.lineTo(n.x + r, n.y); g.lineTo(n.x, n.y + r); g.lineTo(n.x - r, n.y); g.closePath(); } 2652 else g.arc(n.x, n.y, r, 0, Math.PI * 2); 2653 } 2654 /* orthogonal polyline drawn up to a fraction of its length */ 2655 function partial(pts, frac) { 2656 var i, l, lens = [], total = 0, left, k; 2657 for (i = 1; i < pts.length; i++) { l = Math.abs(pts[i][0] - pts[i - 1][0]) + Math.abs(pts[i][1] - pts[i - 1][1]); lens.push(l); total += l; } 2658 left = total * frac; 2659 g.beginPath(); g.moveTo(pts[0][0], pts[0][1]); 2660 for (i = 1; i < pts.length; i++) { 2661 l = lens[i - 1]; 2662 if (left >= l) { g.lineTo(pts[i][0], pts[i][1]); left -= l; } 2663 else { k = l > 0 ? left / l : 0; g.lineTo(lerp(pts[i - 1][0], pts[i][0], k), lerp(pts[i - 1][1], pts[i][1], k)); break; } 2664 } 2665 g.stroke(); 2666 } 2667 function hazard(x, y, w, h) { 2668 var s; 2669 g.save(); g.beginPath(); g.rect(x, y, w, h); g.clip(); 2670 g.strokeStyle = rzInk(0.28); g.lineWidth = 5; 2671 for (s = -h; s < w + h; s += 14.14) { g.beginPath(); g.moveTo(x + s, y + h + 1); g.lineTo(x + s + h + 2, y - 1); g.stroke(); } 2672 g.restore(); 2673 } 2674 2675 /* ââ phase state for the lane header ââ */ 2676 function laneWeights(t) { 2677 var k; 2678 if (t < T_DYN) return [1, 0, 0]; 2679 if (t < T_DYN + SW) { k = eio((t - T_DYN) / SW); return [1 - k, k, 0]; } 2680 if (t < T_RE) return [0, 1, 0]; 2681 if (t < T_RE + SW) { k = eio((t - T_RE) / SW); return [0, 1 - k, k]; } 2682 if (t < T_VERD) return [0, 0, 1]; 2683 if (t < T_VERD + 0.7) { k = eio((t - T_VERD) / 0.7); return [k, k, 1]; } 2684 if (t < T_OUT) return [1, 1, 1]; 2685 k = eio((t - T_OUT) / FADE); return [1, 1 - k, 1 - k]; 2686 } 2687 function underline(t, sp) { 2688 var full = [sp[0][0], sp[2][1]]; 2689 if (t < T_DYN) return sp[0];
2690 if (t < T_DYN + SW) return lerpSpan(sp[0], sp[1], eio((t - T_DYN) / SW)); 2691 if (t < T_RE) return sp[1]; 2692 if (t < T_RE + SW) return lerpSpan(sp[1], sp[2], eio((t - T_RE) / SW)); 2693 if (t < T_VERD) return sp[2]; 2694 if (t < T_VERD + 0.7) return lerpSpan(sp[2], full, eio((t - T_VERD) / 0.7)); 2695 if (t < T_OUT) return full; 2696 return lerpSpan(full, sp[0], eio((t - T_OUT) / FADE)); 2697 } 2698 function readout(t) { 2699 var B = [T_HASH, T_DYN, T_RE, T_VERD, T_OUT + FADE * 0.5], i, d = 1; 2700 var txt = t < B[0] ? 't+0' : t < B[1] ? 't+4h' : t < B[2] ? 't+12h' : t < B[3] ? 't+48h' : t < B[4] ? 't+5d' : 't+0'; 2701 for (i = 0; i < B.length; i++) d = Math.min(d, Math.abs(t - B[i]) / 0.25); 2702 return { txt: txt, a: eo(d) }; 2703 } 2704 2705 /* ââ static chrome: frame, crop brackets, label, hazard, ruler ââ */ 2706 function drawFrame(tk) { 2707 var o = 6, L = 14, x0 = FX - o, y0 = FY - o, x1 = FX + FW + o, y1 = FY + FH + o, x, f, a, lw; 2708 g.lineWidth = 1; g.strokeStyle = rzInk(0.13); g.strokeRect(FX + 0.5, FY + 0.5, FW - 1, FH - 1); 2709 g.lineWidth = 2; g.strokeStyle = rzInk(0.85); g.beginPath(); 2710 g.moveTo(x0, y0 + L); g.lineTo(x0, y0); g.lineTo(x0 + L, y0); 2711 g.moveTo(x1 - L, y0); g.lineTo(x1, y0); g.lineTo(x1, y0 + L); 2712 g.moveTo(x0, y1 - L); g.lineTo(x0, y1); g.lineTo(x0 + L, y1); 2713 g.moveTo(x1 - L, y1); g.lineTo(x1, y1); g.lineTo(x1, y1 - L); 2714 g.stroke(); 2715 /* frame label knocks out the border on a paper chip */ 2716 g.font = "8.5px 'Courier New', monospace"; g.textBaseline = 'middle'; 2717 lw = spacedW('ISOLATED LAB', 0.7); 2718 g.fillStyle = tk.paper; g.fillRect(FX + PAD - 6, FY - 7, lw + 12, 14); 2719 g.fillStyle = rzInk(0.55); spaced('ISOLATED LAB', FX + PAD, FY + 0.5, 0.7, 'left'); 2720 g.textBaseline = 'alphabetic'; 2721 hazard(FX + FW - PAD - 64, FY - 1, 64, 7); 2722 for (x = FX + 12; x < FX + FW; x += 24) { 2723 f = (x - FX) / FW; a = Math.min(1, Math.min(f, 1 - f) / 0.12); 2724 g.fillStyle = rzInk(0.22 * a); g.fillRect(x, FY + FH - 8, 1, 7); 2725 } 2726 g.fillStyle = rzInk(0.45); g.font = "8.5px 'Courier New', monospace"; 2727 spaced('PROCESS TREE', FX + PAD, FY + FH - 17, 0.7, 'left'); 2728 } 2729 2730 /* ââ lane header: STATIC · DYNAMIC · RE with a sliding ink cursor ââ */ 2731 function drawHeader(t, tk) { 2732 var y = 52, x = FX + PAD, i, sp = [], w = laneWeights(t), lw, sx, u, r; 2733 g.font = "600 10px 'Oxanium', sans-serif"; g.textBaseline = 'alphabetic'; g.textAlign = 'left'; 2734 for (i = 0; i < LANES.length; i++) { 2735 sx = x; 2736 g.fillStyle = tk[LANES[i].key]; rrect(x, y - 7, 6, 6, 1.5); g.fill(); 2737 x += 12; 2738 lw = spacedW(LANES[i].lbl, 1.4); 2739 g.fillStyle = rzInk(0.42 + 0.48 * w[i]); spaced(LANES[i].lbl, x, y, 1.4, 'left'); 2740 x += lw; sp.push([sx, x]); 2741 if (i < LANES.length - 1) { g.fillStyle = rzInk(0.35); g.textAlign = 'center'; g.fillText('·', x + 11, y); g.textAlign = 'left'; x += 22; } 2742 } 2743 u = underline(t, sp); 2744 g.fillStyle = rzInk(0.85); g.fillRect(u[0], y + 7, u[1] - u[0], 1); 2745 r = readout(t); 2746 g.font = "9px 'Courier New', monospace"; g.textAlign = 'right'; g.fillStyle = rzInk(0.5 * r.a); 2747 g.fillText(r.txt, FX + FW - PAD, y); g.textAlign = 'left'; 2748 } 2749 2750 /* ââ the animated part: links, nodes, flags, tags, chips ââ */ 2751 function drawTree(t, tk, out) { 2752 var i, n, par, lp, ym, pts, jd, s, r, settle, k, q, f, pg, pq, la, ta, ha, root, cx, cy, hw, va, l1w, cw, ch, vx, vy, pc = 0, fc = 0, t0; 2753 2754 /* links: ink elbows from under a parent's label to the top of each child */ 2755 g.globalAlpha = out; g.lineWidth = 1; g.strokeStyle = rzInk(0.55); g.lineCap = 'butt'; g.lineJoin = 'miter'; 2756 for (i = 1; i < nodes.length; i++) { 2757 n = nodes[i]; par = nodes[n.p]; 2758 lp = seg(t, n.t, LINK_D); if (lp <= 0) continue; 2759 ym = par.y + BUS; 2760 if (n.stub) pts = [[par.x + 0.5, par.y + par.r + 18], [par.x + 0.5, ym + 0.5], [n.x + 0.5, ym + 0.5], [n.x + 0.5, n.y - n.r - 2]]; 2761 else pts = [[par.x + 0.5, ym + 0.5], [n.x + 0.5, ym + 0.5], [n.x + 0.5, n.y - n.r - 2]]; 2762 partial(pts, eio(lp)); 2763 } 2764 for (i = 0; i < nodes.length; i++) { 2765 n = nodes[i]; if (!n.fan) continue; 2766 jd = seg(t, n.fan, 0.3); if (jd <= 0) continue; 2767 g.globalAlpha = out * eo(jd); g.fillStyle = rzInk(0.85); 2768 g.beginPath(); g.arc(n.x + 0.5, n.y + BUS + 0.5, 2.5, 0, Math.PI * 2); g.fill(); 2769 } 2770 2771 /* nodes */ 2772 for (i = 0; i < nodes.length; i++) { 2773 n = nodes[i]; t0 = n.t + (n.root ? 0 : SPAWN_LAG); 2774 s = eo(seg(t, t0, SPAWN_D)); if (s <= 0) continue; 2775 if (seg(t, t0, SPAWN_D) > 0.5) pc++; 2776 r = n.r * s; 2777 g.globalAlpha = out; 2778 shape(n, r); g.fillStyle = tk.paper; g.fill(); 2779 g.lineWidth = n.root ? 1.6 : 1.3; g.strokeStyle = rzInk(n.root ? 0.8 : 0.6); shape(n, r); g.stroke(); 2780 2781 if (n.root) { 2782 g.fillStyle = tk.purple; rrect(n.x - 2.5, n.y - 2.5, 5, 5, 1); g.fill(); 2783 settle = 1 - eio(seg(t, T_SETTLE, 0.8)); 2784 k = ((t - T_ROOT) % 2.6) / 2.6; 2785 if (t >= T_ROOT + 0.4 && k < 0.7 && settle > 0) { 2786 q = eo(k / 0.7); 2787 g.globalAlpha = out * settle * 0.45 * (1 - q); g.lineWidth = 1.2; g.strokeStyle = tk.purple; 2788 g.beginPath(); g.arc(n.x, n.y, n.r + 10 * q, 0, Math.PI * 2); g.stroke(); 2789 g.globalAlpha = out; 2790 } 2791 } 2792 2793 if (n.flag) { 2794 f = eio(seg(t, n.flag, 0.5)); 2795 if (seg(t, n.flag, 0.5) > 0.5) fc++; 2796 if (f > 0) { 2797 g.globalAlpha = out * f; g.lineWidth = 1.5; g.strokeStyle = tk.crimson; shape(n, r); g.stroke(); 2798 g.fillStyle = tk.crimson; g.beginPath(); g.arc(n.x, n.y, 2.2 * f, 0, Math.PI * 2); g.fill(); 2799 pg = seg(t, n.flag, 0.9); 2800 if (pg > 0 && pg < 1) { 2801 pq = eo(pg); g.globalAlpha = out * 0.5 * (1 - pq); g.lineWidth = 1; 2802 g.beginPath(); g.arc(n.x, n.y, n.r + 9 * pq, 0, Math.PI * 2); g.stroke(); 2803 } 2804 g.globalAlpha = out; 2805 } 2806 } 2807 2808 la = seg(t, t0 + 0.12, 0.35); 2809 if (la > 0) { 2810 g.globalAlpha = out * eo(la); g.textAlign = 'center'; g.textBaseline = 'alphabetic'; 2811 if (n.net) { g.font = "8.5px 'Courier New', monospace"; g.fillStyle = rzInk(0.7); } 2812 else { g.font = "600 10px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.85); } 2813 g.fillText(n.id, n.x, n.y + n.r + 12); g.textAlign = 'left'; 2814 } 2815 if (n.tag) { 2816 ta = seg(t, n.tagAt, 0.4); 2817 if (ta > 0) { 2818 g.globalAlpha = out * eo(ta); g.font = "8px 'Courier New', monospace"; g.fillStyle = rzInk(0.6); 2819 g.fillText(n.tag, n.x + n.r + 6 + (1 - eo(ta)) * 4, n.y + 3); 2820 } 2821 } 2822 g.globalAlpha = out; 2823 } 2824 2825 /* hash readout beside the sample (STATIC lane) */ 2826 ha = eo(seg(t, T_HASH, 0.45)); 2827 if (ha > 0) { 2828 root = nodes[0]; cx = root.x + root.r + 4 + (1 - ha) * 6; cy = root.y; 2829 g.globalAlpha = out * ha; 2830 g.setLineDash([2, 3]); g.lineWidth = 1; g.strokeStyle = rzInk(0.4); 2831 g.beginPath(); g.moveTo(cx, cy + 0.5); g.lineTo(cx + 10, cy + 0.5); g.stroke(); g.setLineDash([]); 2832 g.font = "8px 'Courier New', monospace"; hw = g.measureText(HASH).width; 2833 chip(cx + 12, cy - 9, hw + 14, 18, tk); 2834 g.textBaseline = 'middle'; g.fillStyle = rzInk(0.6); g.fillText(HASH, cx + 19, cy + 0.5); g.textBaseline = 'alphabetic'; 2835 } 2836 2837 /* verdict chip (yellow tab, like the methodology's verdict node) */ 2838 va = eo(seg(t, T_VERD, 0.6)); 2839 if (va > 0) { 2840 g.globalAlpha = out * va; 2841 g.font = "700 11px 'Oxanium', sans-serif"; l1w = g.measureText(VERDICT).width; 2842 cw = Math.ceil(l1w) + 44; ch = 40; vx = Math.round(240 - cw / 2); vy = 382 + (1 - va) * 8; 2843 chip(vx, vy, cw, ch, tk); 2844 g.fillStyle = tk.yellow; g.fillRect(vx + 14, vy, cw - 28, 3); 2845 g.textAlign = 'center'; g.fillStyle = rzInk(0.92); g.fillText(VERDICT, 240, vy + 18); 2846 g.font = "8px 'Courier New', monospace"; g.fillStyle = rzInk(0.5); g.fillText(VERDICT_SUB, 240, vy + 31); 2847 g.textAlign = 'left'; 2848 } 2849 2850 /* live counters, bottom right */ 2851 g.globalAlpha = out; g.font = "8.5px 'Courier New', monospace"; g.textAlign = 'right'; g.fillStyle = rzInk(0.5); 2852 g.fillText('PROC ' + pad2(pc) + ' · FLAG ' + pad2(fc), FX + FW - PAD, FY + FH - 17); g.textAlign = 'left'; 2853 } 2854 2855 function draw(t) { 2856 var tk = tokens(), out = t < T_OUT ? 1 : 1 - eio((t - T_OUT) / FADE); 2857 g.clearRect(0, 0, W, H); 2858 g.globalAlpha = 1; g.setLineDash([]); g.lineCap = 'butt'; g.lineJoin = 'miter'; g.textAlign = 'left'; g.textBaseline = 'alphabetic'; 2859 drawFrame(tk); 2860 drawHeader(t, tk); 2861 if (out > 0) drawTree(t, tk, out); 2862 g.globalAlpha = 1; 2863 } 2864 2865 /* ââ reduced motion: one composed frame, redrawn only when the canvas refits ââ */ 2866 var reduce = false;
2867 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 2868 if (reduce) { 2869 var still = function () { window.requestAnimationFrame(function () { draw(T_STATIC); }); }; 2870 still(); 2871 window.addEventListener('resize', still); window.addEventListener('load', still); 2872 document.addEventListener('rz-theme-change', still); document.addEventListener('rz-why-variant', still); 2873 return; 2874 } 2875 2876 /* ââ time-based loop, paused while the tab is hidden ââ */ 2877 var t0 = now(), pausedAt = null, raf = 0; 2878 function frame(ts) { 2879 raf = 0; if (document.hidden) return; 2880 draw(((ts - t0) / 1000) % LOOP); 2881 raf = window.requestAnimationFrame(frame); 2882 } 2883 function start() { if (!raf) raf = window.requestAnimationFrame(frame); } 2884 document.addEventListener('visibilitychange', function () { 2885 if (document.hidden) { if (raf) { window.cancelAnimationFrame(raf); raf = 0; } pausedAt = now(); } 2886 else { if (pausedAt !== null) { t0 += now() - pausedAt; pausedAt = null; } start(); } 2887 }); 2888 if (document.hidden) pausedAt = now(); else start(); 2889 })(); 2890 2891 /* ââ 13. MOBILE APP TESTING â Security Scanner ââââââââââââââââ */ 2892 (function () { 2893 'use strict'; 2894 /* ââ 13. MOBILE APP TESTING â Device layers (exploded view) ââ 2895 A blueprint phone whose four inner planes (UI · RUNTIME · STORAGE · 2896 NETWORK) lift apart into an exploded stack. A scan visits each 2897 plane top-down, a finding pin lands on STORAGE, the stack holds, 2898 then settles back. Same rotateX(54°)·rotateZ(-40°) view as the 2899 page's "Testing, layer by layer" methodology scene. Tokens are 2900 read every frame; neutral ink goes through rzInk(). Transparent 2901 background · time-based, eased · pauses when hidden · one static 2902 frame under prefers-reduced-motion. ES5, no globals. */ 2903 var cv = document.getElementById('rz-visual-mobile'); 2904 if (!cv) return; 2905 var W = 480, H = 480; 2906 var g = cv.getContext('2d'); 2907 if (!g) return; 2908 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store */ 2909 2910 /* ---- projection (orthographic): u = phone width, v = phone height, z = stack normal ---- */ 2911 var UX = 0.766, UY = -0.378, VX = 0.643, VY = 0.450, ZY = -0.809; 2912 var PW = 132, PH = 264, PR = 22; /* shell, local px */ 2913 var INS = 8, SW = PW - 2 * INS, SH = PH - 2 * INS, SR = 16; /* inner planes */ 2914 var OX = 104, OY = 262; /* shell origin on screen */ 2915 var GAP = 52, HL_LIFT = 10; /* exploded gap / highlight lift (local z) */ 2916 var GS = 1.16, GY = 14; /* glyph space 100Ã190 â plane */ 2917 var CHIP_X = 356, CHIP_W = 112; /* finding chip column / width */ 2918 2919 var LAYERS = [ /* index 0 sits in the shell, 3 is the glass */ 2920 { name: 'NETWORK', sub: 'TLS · API', num: '04', v: '--rz-indigo', fb: '#5801f5' }, 2921 { name: 'STORAGE', sub: 'KEYCHAIN · DB', num: '03', v: '--rz-crimson', fb: '#f50034' }, 2922 { name: 'RUNTIME', sub: 'IPC · HOOKS', num: '02', v: '--rz-lime', fb: '#85f403' }, 2923 { name: 'UI', sub: 'APP LOGIC', num: '01', v: '--rz-purple', fb: '#7d14d7' } 2924 ]; 2925 var SCAN_ORDER = [3, 2, 1, 0]; /* attacker works down: UI â NETWORK */ 2926 var PIN_K = 1, PIN_U = 72, PIN_V = 229; /* finding pin on STORAGE (front rim of the DB; clears the plane above) */ 2927 var SPECTRUM = ['--rz-purple', '--rz-crimson', '--rz-lime', '--rz-indigo', '--rz-yellow']; 2928 var SPECTRUM_FB = ['#7d14d7', '#f50034', '#85f403', '#5801f5', '#e6e414']; 2929 2930 /* ---- timeline (seconds) ---- */ 2931 var T_EXPLODE = 0.8, EXPLODE_DUR = 1.25, STAGGER = 0.08; 2932 var T_SCAN = 2.7, SCAN_DUR = 1.5; 2933 var T_PIN = T_SCAN + 2 * SCAN_DUR + 1.2; /* 6.9 â as the STORAGE sweep lands */ 2934 var T_HOLD = T_SCAN + 4 * SCAN_DUR; /* 8.7 */ 2935 var T_COLLAPSE = T_HOLD + 1.4; /* 10.1 */ 2936 var LOOP = T_COLLAPSE + EXPLODE_DUR + 3 * STAGGER + 1.5; /* â 13.1 */ 2937 2938 var reduce = false;
2939 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 2940 var nowFn = (window.performance && performance.now) ? function () { return performance.now(); } : function () { return Date.now(); }; 2941 2942 /* ---- easing / helpers ---- */ 2943 function seg(t, a, b) { var p = (t - a) / (b - a); return p <= 0 ? 0 : (p >= 1 ? 1 : p); } 2944 function ioQuint(p) { return p < 0.5 ? 16 * p * p * p * p * p : 1 - Math.pow(-2 * p + 2, 5) / 2; } 2945 function ioCubic(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; } 2946 function outCubic(p) { return 1 - Math.pow(1 - p, 3); } 2947 function outBack(p) { var c1 = 1.2, c3 = c1 + 1; return 1 + c3 * Math.pow(p - 1, 3) + c1 * Math.pow(p - 1, 2); } 2948 function fOx(w, px) { return w + ' ' + px + 'px Oxanium, sans-serif'; } 2949 function fMono(w, px) { return w + ' ' + px + 'px "Courier New", Courier, monospace'; } 2950 2951 /* colour parsing (#rgb / #rrggbb / rgb[a]()) with a memo so per-frame reads stay cheap */ 2952 var memo = {}; 2953 function rgb(str, fb) { 2954 var s = (str || '').replace(/^\s+|\s+$/g, ''), m, out; 2955 if (memo[s]) return memo[s]; 2956 if ((m = /^#([0-9a-f]{3})$/i.exec(s))) out = [parseInt(m[1].charAt(0) + m[1].charAt(0), 16), parseInt(m[1].charAt(1) + m[1].charAt(1), 16), parseInt(m[1].charAt(2) + m[1].charAt(2), 16)]; 2957 else if ((m = /^#([0-9a-f]{6})$/i.exec(s))) out = [parseInt(m[1].substr(0, 2), 16), parseInt(m[1].substr(2, 2), 16), parseInt(m[1].substr(4, 2), 16)]; 2958 else if ((m = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/i.exec(s))) out = [+m[1], +m[2], +m[3]]; 2959 else out = rgb(fb, '#000000'); 2960 memo[s] = out; 2961 return out; 2962 } 2963 function rgba(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + (a < 0 ? 0 : a > 1 ? 1 : a) + ')'; } 2964 function mix(a, b, t) { return [Math.round(a[0] + (b[0] - a[0]) * t), Math.round(a[1] + (b[1] - a[1]) * t), Math.round(a[2] + (b[2] - a[2]) * t)]; } 2965 2966 /* design tokens, read once per frame */ 2967 function theme() { 2968 var cs = null, i, T; 2969 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 2970 function v(name, fb) { var s = cs ? cs.getPropertyValue(name) : ''; s = s ? s.replace(/^\s+|\s+$/g, '') : ''; return s || fb; } 2971 T = { dark: document.documentElement.getAttribute('data-theme') === 'dark', acc: [], spec: [] }; 2972 T.paper = rgb(v('--rz-paper', T.dark ? '#0e0f14' : '#ffffff'), T.dark ? '#0e0f14' : '#ffffff'); 2973 T.ink = rgb(v('--rz-ink', T.dark ? '#eceef4' : '#14151a'), T.dark ? '#eceef4' : '#14151a'); 2974 for (i = 0; i < 4; i++) T.acc.push(rgb(v(LAYERS[i].v, LAYERS[i].fb), LAYERS[i].fb)); 2975 for (i = 0; i < 5; i++) T.spec.push(rgb(v(SPECTRUM[i], SPECTRUM_FB[i]), SPECTRUM_FB[i])); 2976 return T; 2977 } 2978 2979 /* rounded-rect path (arcTo â projects to true ellipses under the plane transform) */ 2980 function rr(x, y, w, h, r) { 2981 g.moveTo(x + r, y); 2982 g.lineTo(x + w - r, y); g.arcTo(x + w, y, x + w, y + r, r); 2983 g.lineTo(x + w, y + h - r); g.arcTo(x + w, y + h, x + w - r, y + h, r); 2984 g.lineTo(x + r, y + h); g.arcTo(x, y + h, x, y + h - r, r); 2985 g.lineTo(x, y + r); g.arcTo(x, y, x + r, y, r); 2986 g.closePath(); 2987 } 2988 /* letter-spaced text; align 'left' | 'right' | 'center' */ 2989 function tracked(txt, x, y, px, em, align) { 2990 var sp = px * em, i, n = txt.length, widths = [], total = 0, cx, prev = g.textAlign; 2991 for (i = 0; i < n; i++) { widths[i] = g.measureText(txt.charAt(i)).width; total += widths[i]; } 2992 total += sp * (n - 1); 2993 cx = align === 'right' ? x - total : (align === 'center' ? x - total / 2 : x); 2994 g.textAlign = 'left'; 2995 for (i = 0; i < n; i++) { g.fillText(txt.charAt(i), cx, y); cx += widths[i] + sp; } 2996 g.textAlign = prev; 2997 return total; 2998 } 2999 3000 /* ---- plane space: paths are built under the projection, strokes happen after restore (crisp 1px) ---- */ 3001 function shellXf() { g.transform(UX, UY, VX, VY, OX, OY); } 3002 function planeXf(lift) { g.transform(UX, UY, VX, VY, OX + INS * (UX + VX), OY + INS * (UY + VY) + lift * ZY); } 3003 function proj(lift, u, v) { return { x: OX + (INS + u) * UX + (INS + v) * VX, y: OY + (INS + u) * UY + (INS + v) * VY + lift * ZY }; } 3004 function planePath(lift) { g.save(); planeXf(lift); g.beginPath(); rr(0, 0, SW, SH, SR);
3004 g.restore(); } 3005 function glyphXf(lift) { planeXf(lift); g.translate(0, GY); g.scale(GS, GS); } 3006 3007 /* ---- layer glyphs (100Ã190 space, same art as the methodology planes); part = 'ink' | 'acc' | 'accFill' ---- */ 3008 function gRR(x, y, w, h, r) { rr(x, y, w, h, r); } 3009 function gLine(x1, y1, x2, y2) { g.moveTo(x1, y1); g.lineTo(x2, y2); } 3010 function gPoly(pts) { var i; g.moveTo(pts[0], pts[1]); for (i = 2; i < pts.length; i += 2) g.lineTo(pts[i], pts[i + 1]); } 3011 function gCircle(cx, cy, r) { g.moveTo(cx + r, cy); g.arc(cx, cy, r, 0, Math.PI * 2); } 3012 function gEllipse(cx, cy, rx, ry) { g.save(); g.translate(cx, cy); g.scale(1, ry / rx); g.moveTo(rx, 0); g.arc(0, 0, rx, 0, Math.PI * 2); g.restore(); } 3013 function cylinderCurve(x, y) { /* front rim of a 48-wide cylinder, apex 8 below y */ 3014 g.moveTo(x, y); g.bezierCurveTo(x, y + 4, x + 11, y + 8, x + 24, y + 8); g.bezierCurveTo(x + 37, y + 8, x + 48, y + 4, x + 48, y); 3015 } 3016 /* SVG-style circular arc from the current point (x1,y1) to (x2,y2); small arc, sweep = 1 â clockwise on screen */ 3017 function svgArc(x1, y1, r, x2, y2) { 3018 var dx = x2 - x1, dy = y2 - y1, d = Math.sqrt(dx * dx + dy * dy) / 2, h, mx, my, cx, cy; 3019 if (r < d) r = d; 3020 h = Math.sqrt(Math.max(0, r * r - d * d)); 3021 mx = (x1 + x2) / 2; my = (y1 + y2) / 2; 3022 cx = mx - dy / (2 * d) * h; cy = my + dx / (2 * d) * h; 3023 g.arc(cx, cy, r, Math.atan2(y1 - cy, x1 - cx), Math.atan2(y2 - cy, x2 - cx), false); 3024 } 3025 function cloud(x, y) { /* methodology cloud path, translated */ 3026 g.moveTo(x + 30, y + 44); 3027 svgArc(x + 30, y + 44, 12, x + 53, y + 40); 3028 svgArc(x + 53, y + 40, 10, x + 62, y + 56); 3029 g.lineTo(x + 32, y + 56); 3030 svgArc(x + 32, y + 56, 9, x + 30, y + 44); 3031 g.closePath(); 3032 } 3033 function brace(x, y, dir) { /* { (dir 1) or } (dir -1), 40 tall, opening at x */ 3034 g.moveTo(x, y); 3035 g.bezierCurveTo(x - 8 * dir, y, x - 8 * dir, y + 8, x - 8 * dir, y + 14); 3036 g.bezierCurveTo(x - 8 * dir, y + 20, x - 14 * dir, y + 20, x - 14 * dir, y + 20); 3037 g.bezierCurveTo(x - 8 * dir, y + 20, x - 8 * dir, y + 20, x - 8 * dir, y + 26); 3038 g.bezierCurveTo(x - 8 * dir, y + 32, x - 8 * dir, y + 40, x, y + 40); 3039 } 3040 /* Lower planes are partly hidden by the plane above while exploded, so each glyph keeps its 3041 content in the always-visible L (left strip u<40 · bottom strip v>154 in glyph space). */ 3042 var GLYPH = [ 3043 function (part) { /* NETWORK: traffic arrows (left) · cloud (front, accent) · brackets */ 3044 if (part === 'ink') { 3045 gLine(33, 62, 33, 40); gLine(33, 62, 29, 57); gLine(33, 62, 37, 57); 3046 gPoly([76, 164, 71, 170, 76, 176]); gPoly([88, 164, 93, 170, 88, 176]); 3047 } else if (part === 'acc') { 3048 gLine(21, 40, 21, 62); gLine(21, 40, 17, 45); gLine(21, 40, 25, 45); 3049 cloud(0, 126); 3050 gLine(80, 166, 84, 174); 3051 } 3052 }, 3053 function (part) { /* STORAGE: key (left) · database cylinder (front-right, carries the pin) */ 3054 if (part === 'ink') { 3055 gCircle(23, 42, 7); 3056 gLine(28, 47, 41, 60); gLine(35, 54, 39, 50); gLine(39, 58, 43, 54); 3057 gLine(34, 160, 34, 178); gLine(82, 160, 82, 178); 3058 cylinderCurve(34, 178); cylinderCurve(34, 169); 3059 } else if (part === 'acc') { 3060 gEllipse(58, 160, 24, 8); 3061 } 3062 }, 3063 function (part) { /* RUNTIME: IPC boxes (left) · hook braces (front) */ 3064 if (part === 'ink') { 3065 gRR(13, 38, 22, 16, 3); gRR(13, 72, 22, 16, 3); 3066 brace(30, 152, 1); brace(70, 152, -1); 3067 } else if (part === 'acc') { 3068 gLine(20, 58, 20, 68); gLine(28, 68, 28, 58); 3069 gPoly([17, 65, 20, 68, 23, 65]); gPoly([25, 61, 28, 58, 31, 61]); 3070 } else if (part === 'accFill') { 3071 gCircle(50, 172, 3); 3072 } 3073 }, 3074 function (part) { /* UI: wireframe screen */ 3075 if (part === 'ink') { 3076 gRR(18, 26, 64, 20, 3); gRR(18, 54, 64, 14, 3); gRR(18, 76, 64, 14, 3); 3077 } else if (part === 'acc') { 3078 gRR(18, 10, 30, 6, 3); gRR(18, 120, 42, 12, 6); gLine(70, 126, 82, 126); 3079 } 3080 } 3081 ]; 3082 3083 /* ---- animation state from loop time ---- */ 3084 function state(t) { 3085 var s = { lift: [0, 0, 0, 0], hl: [0, 0, 0, 0], sweep: [-1, -1, -1, -1], label: [0, 0, 0, 0], 3086 pin: 0, lead: 0, chip: 0, explode: 0, scanK: -1, read: '' }; 3087 var k, i, e, up, dn, lt, off, fadeOut; 3088 fadeOut = 1 - seg(t, T_COLLAPSE, T_COLLAPSE + 0.4); 3089 for (k = 0; k < 4; k++) { 3090 off = (3 - k) * STAGGER; 3091 up = ioQuint(seg(t, T_EXPLODE + off, T_EXPLODE + off + EXPLODE_DUR)); 3092 dn = ioQuint(seg(t, T_COLLAPSE + k * STAGGER, T_COLLAPSE + k * STAGGER + EXPLODE_DUR)); 3093 e = up * (1 - dn); 3094 s.lift[k] = GAP * k * e; 3095 if (k === 3) s.explode = e; 3096 s.label[k] = outCubic(seg(t, T_EXPLODE + off + 0.55, T_EXPLODE + off + 1.15)) * fadeOut; 3097 } 3098 for (i = 0; i < 4; i++) { 3099 k = SCAN_ORDER[i]; 3100 lt = t - (T_SCAN + i * SCAN_DUR); 3101 if (lt >= 0 && lt < SCAN_DUR) { 3102 s.hl[k] = ioCubic(seg(lt, 0, 0.3)) * (1 - ioCubic(seg(lt, 1.2, 1.5))); 3103 s.sweep[k] = ioCubic(seg(lt, 0.2, 1.2)); 3104 s.lift[k] += HL_LIFT * s.hl[k]; 3105 s.scanK = k; 3106 } 3107 } 3108 s.pin = outBack(seg(t, T_PIN, T_PIN + 0.45)) * (1 - seg(t, T_COLLAPSE, T_COLLAPSE + 0.3)); 3109 s.lead = ioCubic(seg(t, T_PIN + 0.3, T_PIN + 0.7)) * (1 - seg(t, T_COLLAPSE, T_COLLAPSE + 0.3)); 3110 s.chip = outCubic(seg(t, T_PIN + 0.6, T_PIN + 0.95)) * (1 - seg(t, T_COLLAPSE, T_COLLAPSE + 0.3)); 3111 if (t < T_EXPLODE + 0.5 || t >= T_COLLAPSE + 0.6) s.read = 'ASSEMBLED · 04 LAYERS'; 3112 else if (t < T_SCAN) s.read = 'EXPLODED · 04 LAYERS'; 3113 else if (s.scanK >= 0) s.read = 'SCAN · ' + LAYERS[s.scanK].num + ' ' + LAYERS[s.scanK].name; 3114 else if (t < T_COLLAPSE + 0.6) s.read = 'FINDINGS · 01 OPEN'; 3115 return s; 3116 } 3117 3118 /* readout crossfade (discrete text, soft change) */ 3119 var readTxt = '', readSince = 0; 3120 function readAlpha(txt) { 3121 var n = nowFn(); 3122 if (txt !== readTxt) { readTxt = txt; readSince = n; } 3123 return Math.min(1, (n - readSince) / 280); 3124 } 3125 3126 /* ---- drawing ---- */ 3127 function drawFurniture(T, s) { 3128 var i, x, y, a = 14, ins = 4, readA = reduce ? 1 : readAlpha(s.read); 3129 /* corner crop-brackets (blueprint cue, as on the methodology figure) */ 3130 g.lineWidth = 1.5; g.strokeStyle = rzInk(0.30); g.beginPath(); 3131 g.moveTo(ins, ins + a); g.lineTo(ins, ins); g.lineTo(ins + a, ins); 3132 g.moveTo(W - ins - a, ins); g.lineTo(W - ins, ins); g.lineTo(W - ins, ins + a); 3133 g.moveTo(W - ins, H - ins - a); g.lineTo(W - ins, H - ins); g.lineTo(W - ins - a, H - ins); 3134 g.moveTo(ins + a, H - ins); g.lineTo(ins, H - ins); g.lineTo(ins, H - ins - a); 3135 g.stroke(); 3136 /* status chip (top-left): eyebrow + spectrum bar + micro-readout */ 3137 x = 18; y = 18; 3138 g.beginPath(); rr(x, y, 158, 40, 5); 3139 g.fillStyle = rgba(T.paper, 0.92); g.fill(); 3140 g.lineWidth = 1; g.strokeStyle = rzInk(0.15); g.stroke(); 3141 for (i = 0; i < 5;
3141 i++) { g.fillStyle = rgba(T.spec[i], 1); g.fillRect(x + 12 + i * 5, y + 11, 5, 4); } 3142 g.font = fOx(600, 9); g.fillStyle = rzInk(0.85); g.textBaseline = 'alphabetic'; 3143 tracked('DEVICE TEARDOWN', x + 44, y + 18, 9, 0.14, 'left'); 3144 g.font = fMono(400, 8.5); g.fillStyle = rzInk(0.55 * readA); 3145 tracked(s.read, x + 12, y + 32, 8.5, 0.06, 'left'); 3146 /* figure caption (bottom-left) */ 3147 g.font = fMono(400, 8); g.fillStyle = rzInk(0.5); 3148 tracked('FIG · EXPLODED VIEW', 18, H - 16, 8, 0.14, 'left'); 3149 /* z-axis ruler: long ticks at the four exploded layer heights (the label rows), minor ticks every 13 z */ 3150 x = 64; y = OY + INS * (UY + VY) + 22 * VY; 3151 g.lineWidth = 1; g.strokeStyle = rzInk(0.22); g.beginPath(); 3152 g.moveTo(x, y + 8); g.lineTo(x, y + 3 * GAP * ZY - 8); 3153 for (i = 0; i <= 3; i++) { g.moveTo(x - 4, y + i * GAP * ZY); g.lineTo(x + 4, y + i * GAP * ZY); } 3154 for (i = 1; i < 12; i++) { g.moveTo(x - 2, y + i * 13 * ZY); g.lineTo(x + 2, y + i * 13 * ZY); } 3155 g.stroke(); 3156 g.font = fMono(400, 7.5); g.fillStyle = rzInk(0.45); g.textAlign = 'center'; 3157 g.fillText('Z', x, y + 3 * GAP * ZY - 14); 3158 g.textAlign = 'left'; 3159 } 3160 3161 function drawShell(T) { 3162 g.save(); shellXf(); g.beginPath(); rr(0, 0, PW, PH, PR); g.restore(); 3163 g.save(); 3164 g.shadowColor = T.dark ? 'rgba(0,0,0,0.55)' : 'rgba(20,21,26,0.14)'; 3165 g.shadowBlur = 22; g.shadowOffsetY = 10; 3166 g.fillStyle = rgba(mix(T.paper, T.ink, 0.03), 0.96); g.fill(); 3167 g.restore(); 3168 g.lineWidth = 1.4; g.strokeStyle = rzInk(0.72); g.stroke(); 3169 /* side keys along the back-left edge */ 3170 g.save(); shellXf(); g.beginPath(); 3171 g.moveTo(-3, 58); g.lineTo(-3, 76); g.moveTo(-3, 84); g.lineTo(-3, 102); g.moveTo(PW + 3, 66); g.lineTo(PW + 3, 92); 3172 g.restore(); 3173 g.lineWidth = 1.6; g.strokeStyle = rzInk(0.45); g.stroke(); 3174 } 3175 3176 function drawPlane(k, s, T) { 3177 var acc = T.acc[k], hl = s.hl[k], lift = s.lift[k], u, v, sw, grad, c, lifted; 3178 lifted = k > 0 ? Math.min(1, lift / (GAP * k)) : 0; 3179 /* body + shadow */ 3180 planePath(lift); 3181 g.save(); 3182 if (lifted > 0.02) { 3183 g.shadowColor = T.dark ? 'rgba(0,0,0,' + (0.5 * lifted).toFixed(3) + ')' : 'rgba(20,21,26,' + (0.13 * lifted).toFixed(3) + ')'; 3184 g.shadowBlur = 18; g.shadowOffsetY = 9; 3185 } 3186 g.fillStyle = rgba(mix(mix(T.paper, T.ink, 0.05), acc, 0.13 * hl), 0.97); g.fill(); 3187 g.restore(); 3188 /* interior (clipped) */ 3189 g.save(); planePath(lift); g.clip(); 3190 g.save(); planeXf(lift); g.beginPath(); 3191 for (u = 7.5; u < SW; u += 15) for (v = 7.5; v < SH; v += 15) g.rect(u - 0.6, v - 0.6, 1.2, 1.2); 3192 g.restore(); g.fillStyle = rzInk(0.16); g.fill(); 3193 g.save(); glyphXf(lift); g.beginPath(); GLYPH[k]('ink'); g.restore(); 3194 g.lineWidth = 1.15; g.lineJoin = 'round'; g.lineCap = 'round'; g.strokeStyle = rzInk(0.5 + 0.25 * hl); g.stroke(); 3195 g.save(); glyphXf(lift); g.beginPath(); GLYPH[k]('acc'); g.restore(); 3196 g.lineWidth = 1.35; g.strokeStyle = rgba(acc, 0.92); g.stroke(); 3197 g.save(); glyphXf(lift); g.beginPath(); GLYPH[k]('accFill'); g.restore(); 3198 g.fillStyle = rgba(acc, 0.95); g.fill(); 3199 if (k === 3) { /* glass: notch + home indicator */ 3200 g.save(); planeXf(lift); g.beginPath(); rr(SW / 2 - 18, 6, 36, 7, 3.5); rr(SW / 2 - 16, SH - 10, 32, 3, 1.5); g.restore(); 3201 g.lineWidth = 1; g.strokeStyle = rzInk(0.55); g.stroke(); 3202 } 3203 if (s.sweep[k] >= 0 && hl > 0) { /* scan: soft band trailing a thin accent line */ 3204 sw = s.sweep[k] * SH; 3205 g.save(); planeXf(lift); 3206 grad = g.createLinearGradient(0, sw - 46, 0, sw); 3207 grad.addColorStop(0, rgba(acc, 0)); grad.addColorStop(1, rgba(acc, 0.2 * hl)); 3208 g.fillStyle = grad; g.fillRect(-2, sw - 46, SW + 4, 46); 3209 g.beginPath(); g.moveTo(-2, sw); g.lineTo(SW + 2, sw); 3210 g.restore(); 3211 g.lineWidth = 1.4; g.strokeStyle = rgba(acc, 0.95 * hl); g.stroke(); 3212 } 3213 g.restore(); 3214 /* border (ink â accent while scanned) + accent front edge */ 3215 planePath(lift); 3216 c = mix(T.ink, acc, hl); 3217 g.lineWidth = 1; g.strokeStyle = rgba(c, 0.30 + 0.65 * hl); g.stroke(); 3218 g.save(); planeXf(lift); g.beginPath(); g.moveTo(SR, SH); g.lineTo(SW - SR, SH);
3218 g.restore(); 3219 g.lineWidth = 2.2; g.lineCap = 'butt'; g.strokeStyle = rgba(acc, 0.9); g.stroke(); 3220 /* number tab (lives in the plane, like the CSS tab) */ 3221 g.save(); planeXf(lift); g.beginPath(); rr(7, 7, 20, 13, 3); g.restore(); 3222 g.fillStyle = rgba(T.paper, 0.85); g.fill(); 3223 g.lineWidth = 1; g.strokeStyle = rgba(acc, 0.95); g.stroke(); 3224 g.save(); planeXf(lift); 3225 g.font = fMono(700, 8.5); g.fillStyle = rgba(acc, 1); g.textAlign = 'center'; g.textBaseline = 'middle'; 3226 g.fillText(LAYERS[k].num, 17, 13.5); 3227 g.restore(); 3228 g.textAlign = 'left'; g.textBaseline = 'alphabetic'; 3229 } 3230 3231 function drawLabels(s, T) { 3232 var k, a, p, xr, nw, acc, L; 3233 for (k = 0; k < 4; k++) { 3234 a = s.label[k]; if (a <= 0.01) continue; 3235 L = LAYERS[k]; acc = T.acc[k]; 3236 p = proj(s.lift[k], 0, 22); 3237 xr = p.x - 26; 3238 /* leader + anchor dot (accent while scanned) */ 3239 g.lineWidth = 1; g.strokeStyle = s.hl[k] > 0 ? rgba(acc, a * (0.4 + 0.6 * s.hl[k])) : rzInk(0.4 * a); 3240 g.beginPath(); g.moveTo(p.x - 3, p.y); g.lineTo(xr + 6, p.y); g.stroke(); 3241 g.beginPath(); g.arc(p.x, p.y, 1.8, 0, Math.PI * 2); 3242 g.fillStyle = s.hl[k] > 0 ? rgba(acc, a) : rzInk(0.65 * a); g.fill(); 3243 /* 0n NAME / sub-line */ 3244 g.font = fOx(700, 11); g.fillStyle = rzInk((0.72 + 0.28 * s.hl[k]) * a); 3245 nw = tracked(L.name, xr, p.y + 4, 11, 0.08, 'right'); 3246 g.font = fMono(700, 9); g.fillStyle = rgba(acc, a); g.textAlign = 'right'; 3247 g.fillText(L.num, xr - nw - 7, p.y + 4); 3248 g.font = fMono(400, 8); g.fillStyle = rzInk(0.5 * a); 3249 tracked(L.sub, xr, p.y + 16, 8, 0.06, 'right'); 3250 g.textAlign = 'left'; 3251 } 3252 } 3253 3254 function drawPin(s, T) { 3255 var acc = T.acc[PIN_K], p, hx, hy, pop, a, lx, cx, cy, cw = CHIP_W, ch = 32; 3256 if (s.pin <= 0.01) return; 3257 pop = s.pin; a = Math.min(1, pop * 2.5); 3258 p = proj(s.lift[PIN_K], PIN_U, PIN_V); 3259 hx = p.x; hy = p.y - 14 * pop; 3260 g.lineWidth = 1.2; g.strokeStyle = rgba(acc, a); 3261 g.beginPath(); g.moveTo(p.x, p.y); g.lineTo(hx, hy); g.stroke(); 3262 g.beginPath(); g.arc(p.x, p.y, 1.8, 0, Math.PI * 2); g.fillStyle = rgba(acc, a); g.fill(); 3263 g.beginPath(); g.arc(hx, hy, 4.6, 0, Math.PI * 2); 3264 g.fillStyle = rgba(T.paper, a); g.fill(); 3265 g.lineWidth = 1.6; g.strokeStyle = rgba(acc, a); g.stroke(); 3266 g.beginPath(); g.arc(hx, hy, 1.6, 0, Math.PI * 2); g.fillStyle = rgba(acc, a); g.fill(); 3267 /* leader to the chip column */ 3268 if (s.lead > 0.01) { 3269 lx = hx + 7 + (CHIP_X - hx - 7) * s.lead; 3270 g.lineWidth = 1; g.strokeStyle = rzInk(0.45 * a); 3271 g.beginPath(); g.moveTo(hx + 7, hy); g.lineTo(lx, hy); g.stroke(); 3272 } 3273 /* finding chip (paper, 1px ink border) */ 3274 if (s.chip > 0.01) { 3275 cx = CHIP_X + 4 + 6 * (1 - s.chip); cy = hy - ch / 2; 3276 g.save(); g.globalAlpha = s.chip; 3277 g.beginPath(); rr(cx, cy, cw, ch, 4); 3278 g.fillStyle = rgba(T.paper, 0.96); g.fill(); 3279 g.lineWidth = 1; g.strokeStyle = rzInk(0.15); g.stroke(); 3280 g.fillStyle = rgba(acc, 1); g.fillRect(cx + 9, cy + 8, 5, 5); 3281 g.font = fOx(700, 9); g.fillStyle = rzInk(0.88); 3282 tracked('F-01 · HIGH', cx + 20, cy + 13.5, 9, 0.1, 'left'); 3283 g.font = fMono(400, 8.5); g.fillStyle = rzInk(0.58); 3284 tracked('SQLITE · CLEARTEXT', cx + 9, cy + 25.5, 8.5, 0.03, 'left'); 3285 g.restore(); 3286 } 3287 } 3288 3289 function draw(s) { 3290 var T = theme(), k; 3291 g.clearRect(0, 0, W, H); 3292 g.lineJoin = 'round'; g.lineCap = 'butt'; g.textAlign = 'left'; g.textBaseline = 'alphabetic'; 3293 drawFurniture(T, s); 3294 drawShell(T); 3295 for (k = 0; k < 4; k++) drawPlane(k, s, T); 3296 drawLabels(s, T); 3297 drawPin(s, T); 3298 } 3299 3300 /* ---- run ---- */ 3301 if (reduce) { 3302 /* one composed frame: exploded, STORAGE highlighted, finding pinned */ 3303 var st = state(T_HOLD + 0.5); 3304 st.hl[PIN_K] = 1; st.read = 'FINDINGS · 01 OPEN'; 3305 var drawStatic = function () { draw(st); }; 3306 drawStatic(); 3307 if (document.fonts && document.fonts.ready) document.fonts.ready.then(drawStatic); 3308 window.addEventListener('resize', drawStatic); 3309 document.addEventListener('rz-theme-change', drawStatic); 3310 return; 3311 } 3312 var raf = 0, running = false, visible = true, t0 = nowFn(); 3313 function loop() { 3314 raf = requestAnimationFrame(loop); 3315 draw(state(((nowFn() - t0) / 1000) % LOOP)); 3316 } 3317 function start() { if (!running && visible && !document.hidden) { running = true; raf = requestAnimationFrame(loop); } } 3318 function stop() { if (running) { running = false; cancelAnimationFrame(raf); } } 3319 document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); }); 3320 if ('IntersectionObserver' in window) { 3321 new IntersectionObserver(function (entries) {
3322 visible = !!(entries[0] && entries[0].isIntersecting); 3323 if (visible) start(); else stop(); 3324 }, { threshold: 0 }).observe(cv); 3325 } 3326 start(); 3327 })(); 3328 3329 /* ââ 14. PHISHING â Social Engineering Funnel âââââââââââââââââ */ 3330 (function () { 3331 /* ââ 14. PHISHING â Campaign mail flow âââââââââââââââââââââââââ 3332 Envelopes leave the target list, cross the mail gateway, open 3333 en route and are held one beat at the lure detector: most pass, 3334 three are flagged crimson (clicked), one turns lime (reported), 3335 one bounces at the gateway, one never opens. Below, the same 3336 five-stage campaign funnel as the methodology section fills in 3337 eased steps as each event happens. Blueprint-on-white, tokens 3338 read at runtime, time-based easing, ~12.4 s loop with a hold. ââ */ 3339 var cv = document.getElementById('rz-visual-phishing'); 3340 if (!cv) return; 3341 var W = 480, H = 480; 3342 var g = cv.getContext('2d'); 3343 if (!g) return; 3344 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store + zoom-aware transform */ 3345 3346 var PI = Math.PI, TAU = PI * 2; 3347 3348 /* ââ lane geometry ââ */ 3349 var LY = 136; /* lane centre line */ 3350 var X_IN = 66, X_GATE = 160, X_BNC = 148, X_DET = 320, X_END = 440; 3351 var GAP = 12; /* half-height of the gateway slot */ 3352 var R_DET = 24, R_BRK = 34; /* detector ring / bracket square */ 3353 var TAG_DY = 38; /* verdict tag hangs this far under the lane */ 3354 3355 /* ââ funnel geometry ââ */ 3356 var F_HEAD = 264, F_RULE = 278, F_BASE = 396, F_H = 92, F_BW = 60; 3357 var F_LBL = 412, F_PCT = 426; 3358 3359 /* ââ timeline (seconds) ââ */ 3360 var LOOP = 12.4, T_FADE = 0.9; 3361 var T0 = 0.7, CAD = 0.95; /* first send, cadence */ 3362 var D1 = 0.8, D2 = 1.0, D3 = 0.4, D4 = 1.2; /* listâgate, gateâdetector, hold, detectorâexit */ 3363 var T_A = D1, T_B = D1 + D2, T_C = T_B + D3, T_X = T_C + D4; 3364 var T_EVT = 0.55; /* one funnel increment */ 3365 var T_TEXT = 0.35; /* chip text crossfade */ 3366 3367 /* ââ roster: pass | click | bounce | closed | report ââ */ 3368 var KIND = ['pass', 'click', 'bounce', 'click', 'report', 'closed', 'pass', 'click']; 3369 var N = KIND.length; 3370 var ENV = [], EVT = [[], [], [], [], []], i; 3371 for (i = 0; i < N; i++) { 3372 var k = KIND[i], s = T0 + i * CAD; 3373 var e = { 3374 kind: k, s: s, 3375 opens: k !== 'bounce' && k !== 'closed', 3376 stage: k === 'click' ? 3 : (k === 'report' ? 4 : -1), 3377 acc: k === 'click' ? 2 : 3, /* crimson / lime */ 3378 tag: k === 'click' ? 'CLICKED' : 'REPORTED' 3379 }; 3380 ENV.push(e); 3381 EVT[0].push(s); /* targeted */ 3382 if (k !== 'bounce') EVT[1].push(s + T_A); /* delivered */ 3383 if (e.opens) EVT[2].push(s + T_A + D2 * 0.5); /* opened */ 3384 if (e.stage > 0) EVT[e.stage].push(s + T_B + 0.25); 3385 } 3386 var STAGES = [ 3387 { n: '01', name: 'TARGETED', lvl: 100, acc: 0 }, 3388 { n: '02', name: 'DELIVERED', lvl: 76, acc: 1 }, 3389 { n: '03', name: 'OPENED', lvl: 54, acc: 4 }, 3390 { n: '04', name: 'CLICKED', lvl: 36, acc: 2 }, 3391 { n: '05', name: 'REPORTED', lvl: 22, acc: 3 } 3392 ]; 3393 var N_CLICK = EVT[3].length, N_REP = EVT[4].length; 3394 3395 /* five equal accents; fall back if a token is missing */ 3396 var ACC_KEYS = ['purple', 'indigo', 'crimson', 'lime', 'yellow']; 3397 var ACC_FALL = { purple: '#7d14d7', indigo: '#5801f5', crimson: '#f50034', lime: '#85f403', yellow: '#e6e414' }; 3398 function tokens() { 3399 var cs = null, dark = document.documentElement.getAttribute('data-theme') === 'dark'; 3400 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 3401 function rd(name, fb) { 3402 var v = ''; 3403 try { v = cs ? (cs.getPropertyValue(name) || '') : ''; } catch (e) {} 3404 v = v.replace(/^\s+|\s+$/g, ''); 3405 return v || fb; 3406 } 3407 var acc = [], j; 3408 for (j = 0; j < ACC_KEYS.length; j++) acc.push(rd('--rz-' + ACC_KEYS[j], ACC_FALL[ACC_KEYS[j]])); 3409 return { acc: acc, paper: rd('--rz-paper', dark ? '#0e0f14' : '#ffffff') }; 3410 } 3411 3412 /* ââ easing + small helpers ââ */ 3413 function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); } 3414 function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; } 3415 function easeOut(p) { return 1 - Math.pow(1 - p, 3); } 3416 function easeOutQuint(p) { return 1 - Math.pow(1 - p, 5); } 3417 function circle(x, y, r) { g.beginPath(); g.arc(x, y, r, 0, TAU); } 3418 function line(x1, y1, x2, y2) { g.beginPath(); g.moveTo(x1, y1); g.lineTo(x2, y2); g.stroke(); } 3419 function dash(a) { if (g.setLineDash) g.setLineDash(a); } 3420 function roundRect(x, y, w, h, r) { 3421 g.beginPath(); 3422 g.moveTo(x + r, y); 3423 g.lineTo(x + w - r, y); g.arcTo(x + w, y, x + w, y + r, r); 3424 g.lineTo(x + w, y + h - r); g.arcTo(x + w, y + h, x + w - r, y + h, r); 3425 g.lineTo(x + r, y + h); g.arcTo(x, y + h, x, y + h - r, r); 3426 g.lineTo(x, y + r); g.arcTo(x, y, x + r, y, r); 3427 g.closePath(); 3428 } 3429 /* letter-spaced text (canvas letterSpacing is not universal) */ 3430 function spacedW(txt, sp) { 3431 var w = 0, j; 3432 for (j = 0; j < txt.length; j++) w += g.measureText(txt.charAt(j)).width; 3433 return w + sp * (txt.length - 1); 3434 } 3435 function spaced(txt, x, y, sp, align) { 3436 var w = spacedW(txt, sp), j; 3437 var px = align === 'right' ? x - w : (align === 'center' ? x - w / 2 : x); 3438 g.textAlign = 'left'; 3439 for (j = 0; j < txt.length; j++) { g.fillText(txt.charAt(j), px, y); px += g.measureText(txt.charAt(j)).width + sp; } 3440 return w; 3441 } 3442 /* ruler: hairline with ticks standing on it */ 3443 function ruler(x0, x1, y) { 3444 var x, major; 3445 g.lineWidth = 1; 3446 g.strokeStyle = rzInk(0.2); line(x0, y, x1, y); 3447 for (x = x0; x <= x1; x += 8) { 3448 major = ((x - x0) % 40) === 0; 3449 g.strokeStyle = rzInk(major ? 0.36 : 0.2); 3450 line(x, y, x, y - (major ? 6 : 3)); 3451 } 3452 } 3453 /* hazard stripes clipped to a small band */ 3454 function hazard(x, y, w, h, alpha) { 3455 var d; 3456 g.save(); 3457 g.beginPath(); g.rect(x, y, w, h); g.clip(); 3458 g.strokeStyle = rzInk(alpha); g.lineWidth = 1; 3459 for (d = -h; d < w + h; d += 5) line(x + d, y + h, x + d + h, y);
3460 g.restore(); 3461 } 3462 /* corner crop-brackets around a square of half-size r */ 3463 function brackets(cx, cy, r, arm, alpha) { 3464 g.strokeStyle = rzInk(alpha); g.lineWidth = 1; 3465 line(cx - r, cy - r, cx - r + arm, cy - r); line(cx - r, cy - r, cx - r, cy - r + arm); 3466 line(cx + r, cy - r, cx + r - arm, cy - r); line(cx + r, cy - r, cx + r, cy - r + arm); 3467 line(cx - r, cy + r, cx - r + arm, cy + r); line(cx - r, cy + r, cx - r, cy + r - arm); 3468 line(cx + r, cy + r, cx + r - arm, cy + r); line(cx + r, cy + r, cx + r, cy + r - arm); 3469 } 3470 3471 var FONT_LABEL = "600 8.5px 'Oxanium', sans-serif"; 3472 var FONT_STAGE = "600 7.5px 'Oxanium', sans-serif"; 3473 var FONT_STAGE_N = "800 10px 'Oxanium', sans-serif"; 3474 var FONT_MONO = "bold 9px 'Courier New', monospace"; 3475 var FONT_MONO_S = "bold 8px 'Courier New', monospace"; 3476 3477 /* chip text crossfade memory (the only non-derived state) */ 3478 var chipText = '', chipPrev = '', chipChangedAt = -10; 3479 3480 /* ââ envelope glyph: paper body, ink outline, flap lifts as it opens ââ */ 3481 function envelope(x, y, a, open, paper) { 3482 if (a <= 0.005) return; 3483 g.globalAlpha = a; 3484 roundRect(x - 12, y - 8, 24, 16, 2); 3485 g.fillStyle = paper; g.fill(); 3486 g.strokeStyle = rzInk(0.78); g.lineWidth = 1.2; g.stroke(); 3487 var tipY = y + 1 - 16 * open; /* flap tip: inside the body â lifted above it */ 3488 g.beginPath(); g.moveTo(x - 12, y - 8); g.lineTo(x, tipY); g.lineTo(x + 12, y - 8); g.stroke(); 3489 if (open > 0.5) { /* the letter shows once the flap is up */ 3490 var la = (open - 0.5) * 2; 3491 g.strokeStyle = rzInk(0.42 * la); g.lineWidth = 1; 3492 line(x - 7, y - 1, x + 7, y - 1); line(x - 7, y + 3, x + 3, y + 3); 3493 } 3494 g.globalAlpha = 1; 3495 } 3496 3497 /* ââ per-envelope state at own time o ââ */ 3498 function envState(e, t) { 3499 var o = t - e.s, p, q; 3500 if (o <= 0 || o >= T_X) return null; 3501 var st = { x: 0, a: 1, open: 0, fa: 0 }; 3502 if (e.kind === 'bounce') { 3503 if (o >= T_A + 0.5) return null; 3504 if (o < T_A) { p = o / T_A; st.x = X_IN + (X_BNC - X_IN) * easeInOut(p); st.a = easeOut(clamp01(p / 0.2)); } 3505 else { q = clamp01((o - T_A - 0.1) / 0.4); st.x = X_BNC - 8 * easeOut(q); st.a = 1 - easeInOut(q); } 3506 return st; 3507 } 3508 if (o < T_A) { p = o / T_A; st.x = X_IN + (X_GATE - X_IN) * easeInOut(p); st.a = easeOut(clamp01(p / 0.2)); } 3509 else if (o < T_B) { p = (o - T_A) / D2; st.x = X_GATE + (X_DET - X_GATE) * easeInOut(p); if (e.opens) st.open = easeInOut(clamp01(p / 0.5)); } 3510 else if (o < T_C) { st.x = X_DET; if (e.opens) st.open = 1; } 3511 else { p = (o - T_C) / D4; st.x = X_DET + (X_END - X_DET) * easeInOut(p); if (e.opens) st.open = 1; st.a = 1 - easeInOut(clamp01((p - 0.78) / 0.22)); } 3512 if (e.kind === 'closed') st.a *= 0.42; 3513 if (e.stage > 0) st.fa = easeOut(clamp01((o - (T_B + 0.2)) / 0.3)); 3514 return st; 3515 } 3516 3517 function stageFill(si, t) { 3518 var ev = EVT[si], sum = 0, j; 3519 for (j = 0; j < ev.length; j++) sum += easeOut(clamp01((t - ev[j]) / T_EVT)); 3520 return ev.length ? sum / ev.length : 0; 3521 } 3522 3523 /* ââ one composed frame at loop-time t (0..LOOP), absolute time T ââ */ 3524 function scene(t, T, tk, instant) { 3525 g.clearRect(0, 0, W, H); 3526 g.lineCap = 'round'; g.lineJoin = 'round'; 3527 g.textBaseline = 'alphabetic'; 3528 3529 var keep = 1 - easeInOut(clamp01((t - (LOOP - T_FADE)) / T_FADE)); /* end-of-cycle reset */ 3530 var cycle = Math.floor(T / LOOP); 3531 var acc = tk.acc, paper = tk.paper; 3532 var wave = 'WAVE 0' + (1 + cycle % 4); 3533 var j, e, st, o, p, x, w, a; 3534 3535 /* â corner crop-brackets (the sheet) â */ 3536 g.strokeStyle = rzInk(0.45); g.lineWidth = 1; 3537 var m = 8.5, L = 12; 3538 line(m, m, m + L, m); line(m, m, m, m + L); 3539 line(W - m, m, W - m - L, m); line(W - m, m, W - m, m + L); 3540 line(m, H - m, m + L, H - m); line(m, H - m, m, H - m - L); 3541 line(W - m, H - m, W - m - L, H - m); line(W - m, H - m, W - m, H - m - L); 3542 3543 /* â eyebrow + phase readout â */ 3544 g.fillStyle = rzInk(0.62); g.font = FONT_LABEL; 3545 var ew = spaced('CAMPAIGN MAIL FLOW', 27, 19, 1.2, 'left'); 3546 g.fillStyle = rzInk(0.22); g.fillRect(27, 23, ew, 1); 3547 g.fillStyle = rzInk(0.55); g.font = FONT_MONO_S; g.textAlign = 'right'; 3548 g.fillText('PHASE 03 · WAVE SEND', W - 27, 19); 3549 3550 /* â station labels with leaders â */ 3551 g.fillStyle = rzInk(0.6); g.font = FONT_LABEL; 3552 spaced('GATEWAY', X_GATE, 74, 1.2, 'center'); 3553 spaced('LURE DETECTOR', X_DET, 74, 1.2, 'center'); 3554 g.strokeStyle = rzInk(0.28); g.lineWidth = 1; 3555 line(X_GATE, 80, X_GATE, LY - 46); line(X_DET, 80, X_DET, LY - 44); 3556 3557 /* â lane: target list glyph, dashed line, end arrow â */ 3558 g.strokeStyle = rzInk(0.5); g.lineWidth = 1; 3559 line(43, LY - 6, 53, LY - 6); line(43, LY, 53, LY); line(43, LY + 6, 53, LY + 6); 3560 g.strokeStyle = rzInk(0.22); dash([2, 5]); 3561 line(60, LY, 436, LY); 3562 dash([]); 3563 g.strokeStyle = rzInk(0.5); 3564 line(437, LY - 4, 442, LY); line(437, LY + 4, 442, LY); 3565 3566 /* â gateway: slotted threshold with a hazard band â */ 3567 hazard(X_GATE - 9, LY - 40, 6, 40 - GAP, 0.16); 3568 hazard(X_GATE - 9, LY + GAP, 6, 40 - GAP, 0.16); 3569 g.strokeStyle = rzInk(0.55); g.lineWidth = 1.2; 3570 line(X_GATE, LY - 40, X_GATE, LY - GAP); line(X_GATE, LY + GAP, X_GATE, LY + 40); 3571 g.lineWidth = 1; 3572 line(X_GATE - 3, LY - GAP, X_GATE + 3, LY - GAP); line(X_GATE - 3, LY + GAP, X_GATE + 3, LY + GAP); 3573 3574 /* â lure detector: ring, cross ticks, crop-bracket corners â */ 3575 g.strokeStyle = rzInk(0.22); g.lineWidth = 1; circle(X_DET, LY, R_DET); g.stroke(); 3576 g.strokeStyle = rzInk(0.35); 3577 line(X_DET, LY - R_DET, X_DET, LY - R_DET - 4); line(X_DET, LY + R_DET, X_DET, LY + R_DET + 4); 3578 line(X_DET - R_DET, LY, X_DET - R_DET - 4, LY); line(X_DET + R_DET, LY, X_DET + R_DET + 4, LY); 3579 brackets(X_DET, LY, R_BRK, 8, 0.42); 3580 3581 /* â events at the stations: gate blooms, slot closing, scan arcs â */ 3582 for (j = 0; j < N; j++) { 3583 e = ENV[j]; o = t - e.s; 3584 if (e.kind === 'bounce') { 3585 p = clamp01((o - T_A) / 0.5); 3586 if (p > 0 && p < 1) { /* the slot closes for a beat */ 3587 g.strokeStyle = rzInk(0.7 * Math.sin(p * PI)); g.lineWidth = 1.2; 3588 line(X_GATE, LY - GAP, X_GATE, LY + GAP); 3589 } 3590 continue; 3591 } 3592 p = clamp01((o - T_A) / 0.55); 3593 if (p > 0 && p < 1) { /* delivered: ink ring leaves the gate */ 3594 g.strokeStyle = rzInk((1 - p) * 0.4); g.lineWidth = 1; 3595 circle(X_GATE, LY, 6 + 16 * easeOutQuint(p)); g.stroke(); 3596 } 3597 var sp = clamp01((o - T_B) / 0.3); /* scan arc sweeps once round the reticle */ 3598 var sf = 1 - easeOut(clamp01((o - T_B - 0.3) / 0.3)); 3599 if (sp > 0 && sf > 0) { 3600 g.lineWidth = 1.5; 3601 if (e.stage > 0) { g.strokeStyle = acc[e.acc]; g.globalAlpha = 0.9 * sf; } 3602 else { g.strokeStyle = rzInk(0.55 * sf); } 3603 g.beginPath(); g.arc(X_DET, LY, R_DET, -PI / 2, -PI / 2 + TAU * easeInOut(sp)); g.stroke(); 3604 g.globalAlpha = 1; 3605 } 3606 } 3607 3608 /* â envelopes in flight, verdict outlines and tags â */ 3609 g.font = FONT_MONO_S; 3610 for (j = 0; j < N; j++) { 3611 e = ENV[j]; st = envState(e, t); 3612 if (!st) continue; 3613 x = st.x; 3614 envelope(x, LY, st.a, st.open, paper); 3615 if (st.fa > 0 && st.a > 0.005) { 3616 a = st.fa * st.a; 3617 g.globalAlpha = a * 0.9; 3618 g.strokeStyle = acc[e.acc]; g.lineWidth = 1.2; dash([3, 3]); 3619 roundRect(x - 15, LY - 11, 30, 22, 3); g.stroke(); 3620 dash([]); 3621 /* verdict tag: paper chip, accent swatch, ink text */ 3622 w = g.measureText(e.tag).width + 24; 3623 g.globalAlpha = a; 3624 roundRect(x - w / 2, LY + TAG_DY, w, 14, 3); 3625 g.fillStyle = paper; g.fill(); 3626 g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke(); 3627 g.fillStyle = acc[e.acc]; g.fillRect(x - w / 2 + 7, LY + TAG_DY + 4.5, 5, 5); 3628 g.fillStyle = rzInk(0.78); g.textAlign = 'left'; g.textBaseline = 'middle'; 3629 g.fillText(e.tag, x - w / 2 + 17, LY + TAG_DY + 7.5); 3630 g.textBaseline = 'alphabetic'; 3631 g.globalAlpha = 1; 3632 } 3633 } 3634 3635 /* â status chip â */ 3636 var sent = 0, clicked = 0, reported = 0; 3637 for (j = 0; j < N; j++) { 3638 e = ENV[j]; 3639 if (t >= e.s) sent++; 3640 if (e.stage > 0 && t >= e.s + T_B + 0.25) { if (e.stage === 3) clicked++; else reported++; } 3641 } 3642 var txt = wave + ' · SENT ' + sent + '/' + N; 3643 if (clicked) txt += ' · CLICKED ' + clicked; 3644 if (reported) txt += ' · REPORTED ' + reported; 3645 if (txt !== chipText) { chipPrev = chipText; chipText = txt; chipChangedAt = t; } 3646 var te = instant ? 1 : easeOut(clamp01((t - chipChangedAt) / T_TEXT)); 3647 if (chipChangedAt > t) te = 1; /* clock wrapped */ 3648 g.font = FONT_MONO; 3649 var cw = g.measureText('WAVE 00 · SENT ' + N + '/' + N + ' · CLICKED ' + N_CLICK + ' · REPORTED ' + N_REP).width + 30; 3650 var ch = 24, cx = W / 2 - cw / 2, cy = 200; 3651 roundRect(cx, cy, cw, ch, 4); 3652 g.fillStyle = paper; g.fill(); 3653 g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke(); 3654 g.textAlign = 'center'; g.textBaseline = 'middle'; 3655 if (te < 1 && chipPrev) { g.fillStyle = rzInk(0.78 * (1 - te));
3655 g.fillText(chipPrev, W / 2, cy + ch / 2 + 0.5); } 3656 g.fillStyle = rzInk(0.78 * te); g.fillText(chipText, W / 2, cy + ch / 2 + 0.5); 3657 g.textBaseline = 'alphabetic'; 3658 3659 /* â funnel panel head: readout + five swatches â */ 3660 g.fillStyle = rzInk(0.55); g.font = FONT_MONO_S; g.textAlign = 'left'; 3661 g.fillText('CAMPAIGN FUNNEL', 40, F_HEAD); 3662 for (j = 0; j < 5; j++) { g.fillStyle = acc[j]; g.fillRect(440 - 6 - (4 - j) * 9, F_HEAD - 7, 6, 6); } 3663 ruler(40, 440, F_RULE); 3664 3665 /* â dashed spine through the funnel's centre line â */ 3666 g.strokeStyle = rzInk(0.22); g.lineWidth = 1; dash([3, 4]); 3667 line(40, F_BASE - F_H / 2, 440, F_BASE - F_H / 2); 3668 dash([]); 3669 3670 /* â five stepped ink slabs with accent caps â */ 3671 for (j = 0; j < 5; j++) { 3672 var sg = STAGES[j], fill = stageFill(j, t) * keep; 3673 var bx = 80 + j * 80 - F_BW / 2, bh = F_H * (sg.lvl / 100) * fill, yy; 3674 if (bh > 0.5) { 3675 g.fillStyle = rzInk(0.86); g.fillRect(bx, F_BASE - bh, F_BW, bh); 3676 g.fillStyle = paper; 3677 for (yy = F_BASE - 10; yy > F_BASE - bh + 1; yy -= 10) g.fillRect(bx, yy, F_BW, 1); 3678 g.fillStyle = acc[sg.acc]; g.fillRect(bx, F_BASE - bh, F_BW, Math.min(3, bh)); 3679 } 3680 /* stage number + name, centred as one line */ 3681 g.font = FONT_STAGE_N; var nw = g.measureText(sg.n).width; 3682 g.font = FONT_STAGE; var sw = spacedW(sg.name, 0.9); 3683 var lx = 80 + j * 80 - (nw + 4 + sw) / 2; 3684 g.font = FONT_STAGE_N; g.fillStyle = rzInk(0.85); g.textAlign = 'left'; g.fillText(sg.n, lx, F_LBL); 3685 g.font = FONT_STAGE; g.fillStyle = rzInk(0.55); spaced(sg.name, lx + nw + 4, F_LBL, 0.9, 'left'); 3686 g.font = FONT_MONO_S; g.fillStyle = rzInk(0.5); g.textAlign = 'center'; 3687 g.fillText(Math.round(sg.lvl * fill) + '%', 80 + j * 80, F_PCT); 3688 } 3689 g.strokeStyle = rzInk(0.3); g.lineWidth = 1; line(40, F_BASE + 0.5, 440, F_BASE + 0.5); 3690 } 3691 3692 /* ââ reduced motion: one composed mid-wave frame ââ */ 3693 var reduce = false;
3694 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 3695 if (reduce) { 3696 var tStatic = ENV[4].s + T_C - 0.08; /* reported envelope held at the detector, a clicked one leaving */ 3697 var drawStatic = function () { scene(tStatic, tStatic, tokens(), true); }; 3698 drawStatic(); 3699 window.addEventListener('resize', drawStatic); 3700 window.addEventListener('load', drawStatic); 3701 document.addEventListener('rz-theme-change', drawStatic); 3702 if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic); 3703 return; 3704 } 3705 3706 /* ââ time-based loop; pauses when the tab is hidden or the hero is off-screen ââ */ 3707 var raf = null, t0 = null, pausedAt = null, inView = true; 3708 function now() { return (window.performance && performance.now) ? performance.now() : +new Date(); } 3709 function frame(ts) { 3710 raf = null; 3711 if (t0 === null) t0 = ts; 3712 var T = Math.max(0, (ts - t0) / 1000); 3713 scene(T % LOOP, T, tokens(), false); 3714 raf = requestAnimationFrame(frame); 3715 } 3716 function start() { 3717 if (raf !== null || document.hidden || !inView) return; 3718 if (pausedAt !== null && t0 !== null) t0 += now() - pausedAt; /* resume without a time jump */ 3719 pausedAt = null; 3720 raf = requestAnimationFrame(frame); 3721 } 3722 function stop() { 3723 if (raf === null) return; 3724 cancelAnimationFrame(raf); raf = null; pausedAt = now(); 3725 } 3726 document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); }); 3727 if ('IntersectionObserver' in window) { 3728 try { 3729 new IntersectionObserver(function (entries) { 3730 for (var q = 0; q < entries.length; q++) inView = entries[q].isIntersecting; 3731 if (inView) start(); else stop(); 3732 }, { threshold: 0 }).observe(cv); 3733 } catch (e) {} 3734 } 3735 start(); 3736 })(); 3737 3738 /* ââ 15. CUSTOM PROJECTS â Security Architecture Builder ââââââââ */ 3739 (function () { 3740 'use strict'; 3741 /* ââ 15. CUSTOM PROJECTS â Modular Assembly ââââââââââââââââââââââ 3742 Nine paper modules (ink squares with accent folder tabs) fly in 3743 from the edges and snap into a 3Ã3 blueprint grid; tongue-and- 3744 groove pins draw between seated neighbours; on completion the 3745 crop-brackets pulse once and the status chip reads ASSEMBLED. 3746 The build then lifts apart gently and the loop repeats. 3747 Time-based, eased, transparent, token-driven (light + dark). ââ */ 3748 var cv = document.getElementById('rz-visual-custom'); 3749 if (!cv) return; 3750 var W = 480, H = 480; 3751 var g = cv.getContext('2d'); 3752 if (!g) return; 3753 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store + zoom-aware transform */ 3754 3755 /* ---- layout (logical px) ---- */ 3756 var TILE = 88, GAP = 20; 3757 var GX = 88, GY = 90; /* grid origin: 3Ã88 + 2Ã20 = 304, centred */ 3758 var FX0 = 56, FY0 = 62, FX1 = 424, FY1 = 422; /* crop-bracket frame */ 3759 var HEAD_Y = 33, RULER_Y = 50, CHIP_Y = 436, CHIP_H = 22; /* header baseline, ruler, status chip */ 3760 3761 /* ---- timeline (seconds) ---- */ 3762 var T_START = 0.35, T_STAG = 0.42, T_FLY = 0.78; /* fly-in: start, stagger, duration */ 3763 var T_CONN = 0.35, T_RIVET = 0.18; /* pin draw, rivet pop */ 3764 var T_PULSE = 5.0, T_PULSE_D = 1.0; /* bracket pulse */ 3765 var T_DONE = 5.1; /* chip flips to ASSEMBLED */ 3766 var T_OUT = 8.6, T_OUT_STAG = 0.16, T_OUT_D = 0.6; /* lift-off (reverse order) */ 3767 var PERIOD = 12.2; /* full loop incl. holds */ 3768 var FLY_D = 84, OUT_D = 22, SET_D = 18; /* side-column travel (stays inside the canvas), lift-off, middle-column set-down */ 3769 3770 /* ---- easing ---- */ 3771 function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); } 3772 function ioQuint(t) { return t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2; } 3773 function ioCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } 3774 function outCubic(t) { return 1 - Math.pow(1 - t, 3); } 3775 function inCubic(t) { return t * t * t; } 3776 3777 /* ---- modules: reading order, five-accent rotation, nearest-edge approach ---- */ 3778 var ACC = ['purple', 'lime', 'crimson', 'indigo', 'yellow']; 3779 var tiles = [], conns = [], i; 3780 for (i = 0; i < 9; i++) { 3781 var c = i % 3, r = Math.floor(i / 3); 3782 var side = (c !== 1); /* side columns fly in from the edges ⦠*/ 3783 var dx = side ? c - 1 : 0, dy = side ? 0 : r - 1; /* ⦠the middle column is set down from above the board */ 3784 tiles.push({ 3785 x: GX + c * (TILE + GAP), y: GY + r * (TILE + GAP), 3786 dx: dx, dy: dy, 3787 dIn: side ? FLY_D : SET_D, dOut: side ? OUT_D : SET_D, 3788 rot0: ((i % 2) ? 1 : -1) * (side ? 2.4 : 0.8) * Math.PI / 180, 3789 pop: side ? 0.08 : 0.14, 3790 acc: ACC[i % 5], 3791 num: '0' + (i + 1), 3792 tIn: T_START + i * T_STAG, 3793 tOut: T_OUT + (8 - i) * T_OUT_STAG 3794 }); 3795 if (c < 2) conns.push({ a: i, b: i + 1, h: true }); 3796 if (r < 2) conns.push({ a: i, b: i + 3, h: false }); 3797 } 3798 3799 function tileState(tl, t) { 3800 var s = { on: false, a: 0, ox: 0, oy: 0, rot: 0, sc: 1, lift: 0, seated: false }; 3801 if (t < tl.tIn) return s; 3802 if (t < tl.tIn + T_FLY) { 3803 var p = (t - tl.tIn) / T_FLY, e = ioQuint(p); 3804 s.on = true; 3805 s.a = outCubic(clamp01(p / 0.5)); 3806 s.ox = tl.dx * tl.dIn * (1 - e); 3807 s.oy = tl.dy * tl.dIn * (1 - e); 3808 s.rot = tl.rot0 * (1 - e); 3809 s.sc = 1 + tl.pop * (1 - e); 3810 s.lift = 1 - e; 3811 return s; 3812 } 3813 if (t < tl.tOut) { s.on = true; s.a = 1; s.seated = true; return s; } 3814 if (t < tl.tOut + T_OUT_D) { 3815 var q = (t - tl.tOut) / T_OUT_D, f = ioCubic(q); 3816 s.on = true; 3817 s.a = 1 - outCubic(clamp01((q - 0.2) / 0.8)); 3818 s.ox = tl.dx * tl.dOut * f; 3819 s.oy = tl.dy * tl.dOut * f; 3820 s.sc = 1 + 0.05 * f; 3821 s.lift = 0.6 * f; 3822 return s; 3823 } 3824 return s; 3825 } 3826 3827 function meterAlpha(tl, t) { 3828 var seatAt = tl.tIn + T_FLY; 3829 if (t < seatAt) return 0; 3830 if (t < tl.tOut) return outCubic(clamp01((t - seatAt) / 0.25)); 3831 return 1 - clamp01((t - tl.tOut) / 0.3); 3832 } 3833 3834 function connState(cn, t) { 3835 var ta = tiles[cn.a], tb = tiles[cn.b]; 3836 var tOn = Math.max(ta.tIn, tb.tIn) + T_FLY + 0.05; 3837 var tOff = Math.min(ta.tOut, tb.tOut) - 0.30; /* unplug just before either module lifts */ 3838 if (t < tOn || t >= tOff + 0.28) return null; 3839 if (t < tOn + T_CONN) return { e: outCubic((t - tOn) / T_CONN), rivet: 0 }; 3840 if (t < tOff) return { e: 1, rivet: outCubic(clamp01((t - tOn - T_CONN) / T_RIVET)) }; 3841 var q = (t - tOff) / 0.28; 3842 return { e: 1 - inCubic(q), rivet: 1 - clamp01(q / 0.3) }; 3843 } 3844 3845 /* ---- tokens: read once per frame; fall back to the site palette ---- */ 3846 function palette() { 3847 var dark = document.documentElement.getAttribute('data-theme') === 'dark'; 3848 var cs = null; 3849 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 3850 function tok(n, fb) { 3851 var v = cs ? cs.getPropertyValue(n) : ''; 3852 v = v ? v.replace(/^\s+|\s+$/g, '') : ''; 3853 return v || fb; 3854 } 3855 return { 3856 paper: tok('--rz-paper', dark ? '#0e0f14' : '#ffffff'), 3857 purple: tok('--rz-purple', dark ? '#9a45ee' : '#7d14d7'), 3858 indigo: tok('--rz-indigo', dark ? '#7a56ff' : '#5801f5'), 3859 crimson: tok('--rz-crimson', dark ? '#ff3355' : '#f50034'), 3860 lime: tok('--rz-lime', dark ? '#8ef513' : '#85f403'), 3861 yellow: tok('--rz-yellow', dark ? '#f0ee2a' : '#e6e414'), 3862 shadow: function (a) { return (dark ? 'rgba(0,0,0,' : 'rgba(20,21,26,') + a + ')'; } 3863 }; 3864 } 3865 3866 /* ---- primitives ---- */ 3867 function rrect(x, y, w, h, tl, tr, br, bl) { 3868 g.beginPath(); 3869 g.moveTo(x + tl, y); 3870 g.lineTo(x + w - tr, y); 3871 if (tr) g.arcTo(x + w, y, x + w, y + tr, tr); else g.lineTo(x + w, y); 3872 g.lineTo(x + w, y + h - br); 3873 if (br) g.arcTo(x + w, y + h, x + w - br, y + h, br); else g.lineTo(x + w, y + h); 3874 g.lineTo(x + bl, y + h); 3875 if (bl) g.arcTo(x, y + h, x, y + h - bl, bl); else g.lineTo(x, y + h); 3876 g.lineTo(x, y + tl); 3877 if (tl) g.arcTo(x, y, x + tl, y, tl); else g.lineTo(x, y); 3878 g.closePath(); 3879 } 3880 function spacedWidth(str, sp) { 3881 var w = 0, k; 3882 for (k = 0; k < str.length; k++) w += g.measureText(str.charAt(k)).width; 3883 return w + sp * (str.length - 1); 3884 } 3885 function spaced(str, x, y, sp, align) { 3886 var w = spacedWidth(str, sp), k; 3887 var cx = align === 'right' ? x - w : (align === 'center' ? x - w / 2 : x); 3888 g.textAlign = 'left'; 3889 for (k = 0; k < str.length; k++) { 3890 var ch = str.charAt(k);
3891 g.fillText(ch, cx, y); 3892 cx += g.measureText(ch).width + sp; 3893 } 3894 return w; 3895 } 3896 var MONO = '9px "Courier New", Courier, monospace'; 3897 var MONO_S = '8px "Courier New", Courier, monospace'; 3898 var LABEL = '600 10px "Oxanium", sans-serif'; 3899 3900 /* ---- parts ---- */ 3901 function drawHeader(pal, t, seated) { 3902 var k; 3903 g.font = LABEL; g.fillStyle = rzInk(0.55); 3904 var ew = spaced('MODULAR BUILD', FX0, HEAD_Y, 1.4, 'left'); 3905 var sx = FX0 + ew + 12; 3906 for (k = 0; k < 5; k++) { g.fillStyle = pal[ACC[k]]; g.fillRect(sx + k * 8, HEAD_Y - 8, 5, 5); } 3907 3908 /* readout chip: "MOD n/09" + one meter square per module */ 3909 g.font = MONO; 3910 var label = 'MOD 0' + seated + '/09', lw = g.measureText(label).width; 3911 var mw = 9 * 7 + 8 * 3, padX = 10, cw = Math.ceil(padX + lw + 10 + mw + padX); 3912 var cx0 = FX1 - cw, cy0 = HEAD_Y - 15; 3913 g.fillStyle = pal.paper; rrect(cx0, cy0, cw, CHIP_H, 4, 4, 4, 4); g.fill(); 3914 g.lineWidth = 1; g.strokeStyle = rzInk(0.15); 3915 rrect(cx0 + 0.5, cy0 + 0.5, cw - 1, CHIP_H - 1, 4, 4, 4, 4); g.stroke(); 3916 g.textAlign = 'left'; g.fillStyle = rzInk(0.55); 3917 g.fillText(label, cx0 + padX, HEAD_Y); 3918 var mx = FX1 - padX - mw, my = cy0 + (CHIP_H - 7) / 2; 3919 for (k = 0; k < 9; k++) { 3920 var bx = mx + k * 10, a = meterAlpha(tiles[k], t); 3921 g.strokeStyle = rzInk(0.24); 3922 g.strokeRect(bx + 0.5, my + 0.5, 6, 6); 3923 if (a > 0) { 3924 g.globalAlpha = a; g.fillStyle = pal[tiles[k].acc]; 3925 g.fillRect(bx, my, 7, 7); 3926 g.globalAlpha = 1; 3927 } 3928 } 3929 } 3930 3931 function drawRuler() { 3932 var x; 3933 g.lineWidth = 1; 3934 g.strokeStyle = rzInk(0.13); 3935 g.beginPath(); g.moveTo(FX0, RULER_Y + 0.5); g.lineTo(FX1, RULER_Y + 0.5); g.stroke(); 3936 g.strokeStyle = rzInk(0.22); 3937 g.beginPath(); 3938 for (x = FX0; x <= FX1; x += 6) { 3939 var hh = ((x - FX0) % 30 === 0) ? 7 : 3; 3940 g.moveTo(x + 0.5, RULER_Y); g.lineTo(x + 0.5, RULER_Y - hh); 3941 } 3942 g.stroke(); 3943 } 3944 3945 function drawBrackets(bloom) { 3946 var len = 16 + 4 * bloom, off = 5 * bloom; 3947 var x0 = FX0 - off, y0 = FY0 - off, x1 = FX1 + off, y1 = FY1 + off; 3948 g.strokeStyle = rzInk(0.28 + 0.55 * bloom); g.lineWidth = 2; 3949 g.beginPath(); 3950 g.moveTo(x0, y0 + 1); g.lineTo(x0 + len, y0 + 1); g.moveTo(x0 + 1, y0); g.lineTo(x0 + 1, y0 + len); 3951 g.moveTo(x1, y0 + 1); g.lineTo(x1 - len, y0 + 1); g.moveTo(x1 - 1, y0); g.lineTo(x1 - 1, y0 + len); 3952 g.moveTo(x0, y1 - 1); g.lineTo(x0 + len, y1 - 1); g.moveTo(x0 + 1, y1); g.lineTo(x0 + 1, y1 - len); 3953 g.moveTo(x1, y1 - 1); g.lineTo(x1 - len, y1 - 1); g.moveTo(x1 - 1, y1); g.lineTo(x1 - 1, y1 - len); 3954 g.stroke(); 3955 g.lineWidth = 1; 3956 } 3957 3958 function drawSlot(tl, a, d) { 3959 var x = tl.x, y = tl.y, cx = x + TILE / 2, cy = y + TILE / 2; 3960 g.lineWidth = 1; 3961 if (a > 0.01) { 3962 g.strokeStyle = rzInk(0.24 * a); g.setLineDash([4, 4]); 3963 rrect(x + 0.5, y + 0.5, TILE - 1, TILE - 1, 0, 10, 10, 10); g.stroke(); 3964 g.setLineDash([]); 3965 } 3966 if (d <= 0.01) return; 3967 a = d; 3968 g.strokeStyle = rzInk(0.2 * a); 3969 g.beginPath(); 3970 g.moveTo(cx - 5, cy + 0.5); g.lineTo(cx + 5, cy + 0.5); 3971 g.moveTo(cx + 0.5, cy - 5); g.lineTo(cx + 0.5, cy + 5); 3972 g.stroke(); 3973 g.font = MONO_S; g.textAlign = 'right'; g.fillStyle = rzInk(0.32 * a); 3974 g.fillText(tl.num, x + TILE - 10, y + TILE - 9); 3975 } 3976 3977 function drawConn(cn, st, pal) { 3978 var ta = tiles[cn.a], len = GAP * st.e, T = 8; 3979 if (len < 0.5) return; 3980 g.lineWidth = 1; 3981 if (cn.h) { 3982 var x0 = ta.x + TILE, yc = ta.y + TILE / 2; 3983 g.fillStyle = pal.paper; rrect(x0, yc - T / 2, len, T, 0, 3, 3, 0); g.fill(); 3984 g.strokeStyle = rzInk(0.3); rrect(x0, yc - T / 2 + 0.5, len, T - 1, 0, 3, 3, 0); g.stroke(); 3985 if (st.rivet > 0) { 3986 g.fillStyle = rzInk(0.5); g.beginPath(); 3987 g.arc(x0 + GAP / 2, yc, 1.7 * st.rivet, 0, Math.PI * 2); g.fill(); 3988 } 3989 } else { 3990 var xc = ta.x + TILE / 2, y0 = ta.y + TILE; 3991 g.fillStyle = pal.paper; rrect(xc - T / 2, y0, T, len, 0, 0, 3, 3); g.fill(); 3992 g.strokeStyle = rzInk(0.3); rrect(xc - T / 2 + 0.5, y0, T - 1, len, 0, 0, 3, 3); g.stroke(); 3993 if (st.rivet > 0) { 3994 g.fillStyle = rzInk(0.5); g.beginPath(); 3995 g.arc(xc, y0 + GAP / 2, 1.7 * st.rivet, 0, Math.PI * 2); g.fill(); 3996 } 3997 } 3998 } 3999 4000 /* module drawn centred on the origin; caller applies translate / rotate / scale */ 4001 function drawTile(tl, st, pal) {
4002 var w = TILE, h = TILE, x = -w / 2, y = -h / 2; 4003 if (st.lift > 0.001) { 4004 g.shadowColor = pal.shadow(0.3 * st.lift); 4005 g.shadowBlur = 26 * st.lift; 4006 g.shadowOffsetY = 12 * st.lift; 4007 } 4008 g.fillStyle = pal.paper; 4009 rrect(x, y, w, h, 0, 10, 10, 10); g.fill(); 4010 g.shadowColor = 'rgba(0,0,0,0)'; g.shadowBlur = 0; g.shadowOffsetY = 0; 4011 4012 g.lineWidth = 1; g.strokeStyle = rzInk(0.26); 4013 rrect(x + 0.5, y + 0.5, w - 1, h - 1, 0, 10, 10, 10); g.stroke(); 4014 4015 /* folder tab: paper, ink hairline, accent top bar, slanted trailing edge */ 4016 g.beginPath(); 4017 g.moveTo(x + 0.5, y); g.lineTo(x + 0.5, y - 9.5); g.lineTo(x + 25, y - 9.5); g.lineTo(x + 32, y); 4018 g.closePath(); 4019 g.fillStyle = pal.paper; g.fill(); 4020 g.beginPath(); 4021 g.moveTo(x + 0.5, y); g.lineTo(x + 0.5, y - 9.5); g.lineTo(x + 25, y - 9.5); g.lineTo(x + 32, y); 4022 g.strokeStyle = rzInk(0.26); g.stroke(); 4023 g.beginPath(); 4024 g.moveTo(x, y - 10); g.lineTo(x + 25.5, y - 10); g.lineTo(x + 27.8, y - 7); g.lineTo(x, y - 7); 4025 g.closePath(); 4026 g.fillStyle = pal[tl.acc]; g.fill(); 4027 4028 /* module content: title bar, two text hairlines, dashed divider, output line, index */ 4029 g.fillStyle = rzInk(0.62); g.fillRect(x + 12, y + 18, 34, 2); 4030 g.fillStyle = rzInk(0.16); g.fillRect(x + 12, y + 29, 54, 1); g.fillRect(x + 12, y + 37, 40, 1); 4031 g.strokeStyle = rzInk(0.16); g.setLineDash([3, 3]); 4032 g.beginPath(); g.moveTo(x + 12, y + 56.5); g.lineTo(x + w - 12, y + 56.5); g.stroke(); 4033 g.setLineDash([]); 4034 g.fillStyle = rzInk(0.4); g.fillRect(x + 12, y + 66, 22, 1.5); 4035 g.font = MONO_S; g.textAlign = 'right'; g.fillStyle = rzInk(0.5); 4036 g.fillText(tl.num, x + w - 10, y + h - 9); 4037 } 4038 4039 function drawChip(pal, done) { 4040 var padX = 12, dot = 6, gapD = 8; 4041 g.font = LABEL; 4042 var wA = spacedWidth('BUILD 01 · ASSEMBLING', 1); 4043 var wB = spacedWidth('BUILD 01 · ASSEMBLED', 1); 4044 var w = Math.ceil(padX * 2 + dot + gapD + Math.max(wA, wB)); 4045 var x = FX0, y = CHIP_Y; 4046 g.fillStyle = pal.paper; rrect(x, y, w, CHIP_H, 4, 4, 4, 4); g.fill(); 4047 g.lineWidth = 1; g.strokeStyle = rzInk(0.15); 4048 rrect(x + 0.5, y + 0.5, w - 1, CHIP_H - 1, 4, 4, 4, 4); g.stroke(); 4049 4050 var dx = x + padX + dot / 2, dy = y + CHIP_H / 2; 4051 g.strokeStyle = rzInk(0.45); 4052 g.beginPath(); g.arc(dx, dy, dot / 2 - 0.5, 0, Math.PI * 2); g.stroke(); 4053 if (done > 0) { 4054 g.globalAlpha = done; g.fillStyle = rzInk(0.85); 4055 g.beginPath(); g.arc(dx, dy, dot / 2, 0, Math.PI * 2); g.fill(); 4056 g.globalAlpha = 1; 4057 } 4058 var tx = x + padX + dot + gapD, ty = y + CHIP_H / 2 + 3.5; 4059 if (done < 1) { g.fillStyle = rzInk(0.72 * (1 - done)); spaced('BUILD 01 · ASSEMBLING', tx, ty, 1, 'left'); } 4060 if (done > 0) { g.fillStyle = rzInk(0.85 * done); spaced('BUILD 01 · ASSEMBLED', tx, ty, 1, 'left'); } 4061 } 4062 4063 /* ---- compose one frame at loop time t (seconds) ---- */ 4064 function render(t) { 4065 var pal = palette(), states = [], seated = 0, k, st; 4066 g.clearRect(0, 0, W, H); 4067 g.textBaseline = 'alphabetic'; g.lineCap = 'butt'; g.lineJoin = 'miter'; 4068 g.globalAlpha = 1; g.setLineDash([]); 4069 4070 for (k = 0; k < 9; k++) { states[k] = tileState(tiles[k], t); if (states[k].seated) seated++; } 4071 4072 drawHeader(pal, t, seated); 4073 drawRuler(); 4074 drawBrackets(Math.sin(Math.PI * clamp01((t - T_PULSE) / T_PULSE_D))); 4075 4076 for (k = 0; k < 9; k++) { st = states[k]; drawSlot(tiles[k], 1 - 0.9 * st.a * (1 - st.lift), 1 - st.a); } 4077 for (k = 0; k < conns.length; k++) { var cs = connState(conns[k], t); if (cs) drawConn(conns[k], cs, pal); } 4078 4079 /* seated modules first, then anything in the air on top */ 4080 var pass; 4081 for (pass = 0; pass < 2; pass++) { 4082 for (k = 0; k < 9; k++) { 4083 st = states[k]; 4084 if (!st.on || (pass === 0) !== (st.lift < 0.001)) continue; 4085 g.save(); 4086 g.globalAlpha = st.a; 4087 g.translate(tiles[k].x + TILE / 2 + st.ox, tiles[k].y + TILE / 2 + st.oy); 4088 g.rotate(st.rot); g.scale(st.sc, st.sc); 4089 drawTile(tiles[k], st, pal);
4090 g.restore(); 4091 } 4092 } 4093 4094 var done = outCubic(clamp01((t - T_DONE) / 0.35)) * (1 - outCubic(clamp01((t - T_OUT) / 0.4))); 4095 drawChip(pal, done); 4096 } 4097 4098 /* ---- loop: time-based, pauses when hidden, static frame under reduced motion ---- */ 4099 var reduce = false, mq = null; 4100 try { mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; reduce = !!(mq && mq.matches); } catch (e) {} 4101 function now() { return (window.performance && window.performance.now) ? window.performance.now() : Date.now(); } 4102 var t0 = now(), raf = 0, running = false; 4103 4104 function frame() { 4105 raf = 0; 4106 if (!running) return; 4107 render(((now() - t0) / 1000) % PERIOD); 4108 raf = requestAnimationFrame(frame); 4109 } 4110 function start() { if (running || reduce || document.hidden) return; running = true; if (!raf) raf = requestAnimationFrame(frame); } 4111 function stop() { running = false; if (raf) { cancelAnimationFrame(raf); raf = 0; } } 4112 function drawStatic() { render(7.0); } /* fully assembled, brackets at rest */ 4113 function onEnv() { if (!running) drawStatic(); } /* re-fit / theme change while static */ 4114 4115 document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); }); 4116 window.addEventListener('resize', onEnv); 4117 window.addEventListener('load', onEnv); 4118 document.addEventListener('rz-theme-change', onEnv); 4119 try { if (document.fonts && document.fonts.ready) document.fonts.ready.then(onEnv); } catch (e) {} 4120 try { 4121 var onMq = function (e) { reduce = !!e.matches; if (reduce) { stop(); drawStatic(); } else { start(); } }; 4122 if (mq && mq.addEventListener) mq.addEventListener('change', onMq); 4123 else if (mq && mq.addListener) mq.addListener(onMq); 4124 } catch (e) {} 4125 4126 if (reduce) drawStatic(); else start(); 4127 })(); 4128 4129 /* ââ 16. CRITICAL INFRASTRUCTURE â ICS/SCADA Monitor âââââââââââ */ 4130 (function () { 4131 /* ââ 16. CRITICAL INFRASTRUCTURE â ICS schematic (P&ID sheet) ââ 4132 Control net (L2) â segmentation conduit C-01 â fieldbus (L1) â 4133 process piping (L0). Flow dashes run through the pipe and FT-101 4134 traces onto a paper chip (lime). An unauthorized write travels 4135 from the off-sheet IT connector down to PLC-01 (crimson pulse), 4136 the conduit closes (indigo), the trace steadies and the next 4137 attempt is stopped at the gate. Rhymes with the page's zones & 4138 conduits methodology. Blueprint-on-white, tokens read at runtime, 4139 time-based easing, ~12.6 s loop with holds. ââ */ 4140 var cv = document.getElementById('rz-visual-ics'); 4141 if (!cv) return; 4142 var W = 480, H = 480; 4143 var g = cv.getContext('2d'); 4144 if (!g) return; 4145 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store + zoom-aware transform */ 4146 4147 var PI = Math.PI, TAU = PI * 2; 4148 var LOOP = 12.6; /* seconds per cycle */ 4149 var T_P1 = 2.3, D_P1 = 1.2, T_HIT = T_P1 + D_P1; /* write reaches PLC-01 at 3.5 s */ 4150 var T_GATE = 5.4, D_GATE = 0.8, T_CLOSED = T_GATE + D_GATE; /* conduit closed at 6.2 s */ 4151 var T_CALM = 6.3, D_CALM = 2.0; /* trace steadies 6.3 â 8.3 s */ 4152 var T_P2 = 8.6, D_P2 = 0.8, T_BLOCK = T_P2 + D_P2; /* second attempt stopped at 9.4 s */ 4153 var T_OPEN = 11.6, D_OPEN = 0.8; /* conduit re-opens for the next cycle */ 4154 var T_TEXT = 0.35; /* chip text crossfade */ 4155 var SPARK_WIN = 6.0, SPARK_N = 96; /* strip-chart window (s) / samples */ 4156 var FLOW_PX = 22; /* pipe dash speed, px/s */ 4157 4158 /* ââ sheet geometry (logical 480Ã480) ââ */ 4159 var Y_L2 = 70, Y_L1 = 176, Y_L0 = 300, SX = 240; 4160 var HMI = { x: 64, y: 57, w: 72, h: 26 }; 4161 var PLC = { x: 118, y: 163, w: 64, h: 26 }; 4162 var RTU = { x: 298, y: 163, w: 64, h: 26 }; 4163 var GATE = { x: 216, y: 108, w: 48, h: 24 }; 4164 var OFF = { tip: 372, x: 382, r: 424, hh: 10 }; 4165 var TANK = { x: 352, y: 268, w: 52, h: 70 }; 4166 var FT = { x: 270, y: Y_L0, r: 13 }; 4167 var PT = { x: 378, y: 241, r: 13 }; 4168 var VALVE = { x: 120, y: Y_L0 }; 4169 var PUMP = { x: 190, y: Y_L0, r: 11 }; 4170 var PIPE0 = 56, PIPE1 = TANK.x; 4171 var SPARK = { x: 56, y: 342, w: 176, h: 62 }; 4172 var CHIP_Y = 428, CHIP_H = 24; 4173 var PATH1 = [[OFF.tip, Y_L2], [SX, Y_L2], [SX, Y_L1], [PLC.x + PLC.w, Y_L1]]; 4174 var PATH2 = [[OFF.tip, Y_L2], [SX, Y_L2], [SX, GATE.y - 4]]; /* stops against the closed conduit */ 4175 4176 /* ââ tokens: five equal accents + paper, read at runtime with fallbacks ââ */ 4177 var FALL = { purple: '#7d14d7', lime: '#85f403', crimson: '#f50034', indigo: '#5801f5', yellow: '#e6e414' }; 4178 function tokens() { 4179 var cs = null, dark = document.documentElement.getAttribute('data-theme') === 'dark'; 4180 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 4181 function rd(name, fb) { 4182 var v = ''; 4183 try { v = cs ? (cs.getPropertyValue(name) || '') : ''; } catch (e) {} 4184 v = v.replace(/^\s+|\s+$/g, ''); 4185 return v || fb; 4186 } 4187 return { 4188 purple: rd('--rz-purple', FALL.purple), lime: rd('--rz-lime', FALL.lime), 4189 crimson: rd('--rz-crimson', FALL.crimson), indigo: rd('--rz-indigo', FALL.indigo), 4190 yellow: rd('--rz-yellow', FALL.yellow), 4191 paper: rd('--rz-paper', dark ? '#0e0f14' : '#ffffff') 4192 }; 4193 } 4194 4195 /* ââ easing + drawing helpers ââ */ 4196 function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); } 4197 function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; } 4198 function easeOut(p) { return 1 - Math.pow(1 - p, 3); } 4199 function easeOutQuint(p) { return 1 - Math.pow(1 - p, 5); } 4200 function circle(x, y, r) { g.beginPath(); g.arc(x, y, r, 0, TAU); } 4201 function line(x1, y1, x2, y2) { g.beginPath(); g.moveTo(x1, y1); g.lineTo(x2, y2); g.stroke(); } 4202 function roundRect(x, y, w, h, r) { 4203 g.beginPath(); 4204 g.moveTo(x + r, y); 4205 g.lineTo(x + w - r, y); g.arcTo(x + w, y, x + w, y + r, r); 4206 g.lineTo(x + w, y + h - r); g.arcTo(x + w, y + h, x + w - r, y + h, r); 4207 g.lineTo(x + r, y + h); g.arcTo(x, y + h, x, y + h - r, r); 4208 g.lineTo(x, y + r); g.arcTo(x, y, x + r, y, r); 4209 g.closePath(); 4210 } 4211 function dash(arr) { if (g.setLineDash) g.setLineDash(arr); } 4212 /* letter-spaced text (canvas letterSpacing is not universal) */ 4213 function spaced(txt, x, y, sp, align) { 4214 var ws = [], w = 0, j; 4215 for (j = 0; j < txt.length; j++) { ws.push(g.measureText(txt.charAt(j)).width); w += ws[j]; } 4216 w += sp * (txt.length - 1); 4217 var px = align === 'right' ? x - w : (align === 'center' ? x - w / 2 : x); 4218 g.textAlign = 'left'; 4219 for (j = 0; j < txt.length; j++) { g.fillText(txt.charAt(j), px, y);
4219 px += ws[j] + sp; } 4220 return w; 4221 } 4222 /* orthogonal polyline helpers (probe routes) */ 4223 function pathLen(p) { 4224 var L = 0, i; 4225 for (i = 1; i < p.length; i++) L += Math.abs(p[i][0] - p[i - 1][0]) + Math.abs(p[i][1] - p[i - 1][1]); 4226 return L; 4227 } 4228 function pathPoint(p, d) { 4229 var i, sl, f; 4230 for (i = 1; i < p.length; i++) { 4231 sl = Math.abs(p[i][0] - p[i - 1][0]) + Math.abs(p[i][1] - p[i - 1][1]); 4232 if (d <= sl) { f = sl ? d / sl : 0; return [p[i - 1][0] + (p[i][0] - p[i - 1][0]) * f, p[i - 1][1] + (p[i][1] - p[i - 1][1]) * f]; } 4233 d -= sl; 4234 } 4235 return [p[p.length - 1][0], p[p.length - 1][1]]; 4236 } 4237 /* ink probe: short fading tail + dot, riding a route */ 4238 function probe(path, prog, alpha) { 4239 var d = pathLen(path) * prog, i, a, b, k = 7, step = 4; 4240 g.lineWidth = 1.6; 4241 for (i = k; i >= 1; i--) { 4242 a = pathPoint(path, Math.max(0, d - i * step)); b = pathPoint(path, Math.max(0, d - (i - 1) * step)); 4243 g.strokeStyle = rzInk(alpha * 0.5 * (1 - i / (k + 1))); line(a[0], a[1], b[0], b[1]); 4244 } 4245 var h = pathPoint(path, d); 4246 g.fillStyle = rzInk(0.9 * alpha); circle(h[0], h[1], 2.6); g.fill(); 4247 } 4248 /* ISA instrument bubble: circle, centre line, tag over number */ 4249 function bubble(b, tag, num, paper) { 4250 g.fillStyle = paper; circle(b.x, b.y, b.r); g.fill(); 4251 g.strokeStyle = rzInk(0.7); g.lineWidth = 1; circle(b.x, b.y, b.r); g.stroke(); 4252 g.strokeStyle = rzInk(0.4); line(b.x - b.r, b.y, b.x + b.r, b.y); 4253 g.font = FONT_MONO_S; g.textAlign = 'center'; g.fillStyle = rzInk(0.8); 4254 g.fillText(tag, b.x, b.y - 3); g.fillText(num, b.x, b.y + 7.5); 4255 } 4256 function nodeBox(b, label, paper) { 4257 roundRect(b.x, b.y, b.w, b.h, 3); g.fillStyle = paper; g.fill(); 4258 g.strokeStyle = rzInk(0.62); g.lineWidth = 1; g.stroke(); 4259 g.font = FONT_NODE; g.textBaseline = 'middle'; g.fillStyle = rzInk(0.88); 4260 spaced(label, b.x + b.w / 2, b.y + b.h / 2 + 0.5, 0.6, 'center'); 4261 g.textBaseline = 'alphabetic'; 4262 } 4263 4264 var FONT_LABEL = "600 8.5px 'Oxanium', sans-serif"; 4265 var FONT_NODE = "700 10px 'Oxanium', sans-serif"; 4266 var FONT_MONO = "bold 9px 'Courier New', monospace"; 4267 var FONT_MONO_S = "bold 8px 'Courier New', monospace"; 4268 var CHIP_TXT = [ 4269 'PROCESS NOMINAL · FT-101 42.0 m³/h', 4270 'UNAUTHORIZED WRITE · PLC-01', 4271 'CONDUIT C-01 CLOSED · ZONE ISOLATED', 4272 'WRITE BLOCKED · PROCESS STEADY' 4273 ]; 4274 4275 /* FT-101 flow trace: a smooth function of loop time (no per-frame randomness) */ 4276 function flowAt(tl) { 4277 var base = 0.55 * Math.sin(TAU * tl / (LOOP / 2)) + 0.35 * Math.sin(TAU * tl / (LOOP / 5) + 1.1); 4278 var env = easeInOut(clamp01((tl - T_HIT) / 1.0)) * (1 - easeInOut(clamp01((tl - T_CALM) / D_CALM))); 4279 var dist = env * (3.4 * Math.sin(TAU * tl * 1.3) + 1.7 * Math.sin(TAU * tl * 2.9 + 0.7)); 4280 return 42.0 + base + dist; 4281 } 4282 function loopT(tau) { return ((tau % LOOP) + LOOP) % LOOP; } 4283 4284 /* chip text crossfade memory (the only non-derived state) */ 4285 var chipIdx = -1, chipPrev = -1, chipChangedAt = -10; 4286 4287 /* ââ one composed frame at loop-time t (0..LOOP), absolute time T ââ */ 4288 function scene(t, T, tk, instant) { 4289 g.clearRect(0, 0, W, H); 4290 g.lineCap = 'round'; g.lineJoin = 'round'; 4291 g.textBaseline = 'alphabetic'; 4292 var paper = tk.paper, j, p, d, a; 4293 4294 /* â corner crop-brackets (the sheet) â */ 4295 g.strokeStyle = rzInk(0.45); g.lineWidth = 1; 4296 var m = 8.5, L = 12; 4297 line(m, m, m + L, m); line(m, m, m, m + L); 4298 line(W - m, m, W - m - L, m); line(W - m, m, W - m, m + L); 4299 line(m, H - m, m + L, H - m); line(m, H - m, m, H - m - L); 4300 line(W - m, H - m, W - m - L, H - m); line(W - m, H - m, W - m, H - m - L); 4301 4302 /* â eyebrow · swatches · sheet readout · ruler strip â */ 4303 g.fillStyle = rzInk(0.62); g.font = FONT_LABEL; 4304 var ew = spaced('ICS SCHEMATIC', 27, 19, 1.2, 'left'); 4305 g.fillStyle = rzInk(0.22); g.fillRect(27, 23, ew, 1); 4306 g.font = FONT_MONO_S; g.textAlign = 'right'; g.fillStyle = rzInk(0.55); 4307 var rt = 'P&ID · IEC 62443', rw = g.measureText(rt).width; 4308 g.fillText(rt, W - 27, 19); 4309 var sw = [tk.purple, tk.lime, tk.crimson, tk.indigo, tk.yellow], sx0 = W - 27 - rw - 12 - 37; 4310 for (j = 0; j < 5; j++) { g.fillStyle = sw[j]; g.fillRect(sx0 + j * 8, 12, 5, 5); } 4311 g.fillStyle = rzInk(0.14); g.fillRect(27, 32, W - 54, 1); 4312 for (j = 0; j * 6 <= W - 54; j++) { 4313 var major = (j % 5) === 0; 4314 g.fillStyle = rzInk(major ? 0.3 : 0.18); 4315 g.fillRect(27 + j * 6, 32 - (major ? 6 : 3), 1, major ? 6 : 3); 4316 } 4317 4318 /* â level tags â */ 4319 g.font = FONT_MONO_S; g.textAlign = 'left'; g.fillStyle = rzInk(0.42); 4320 g.fillText('L2', 27, Y_L2 + 3); g.fillText('L1', 27, Y_L1 + 3); g.fillText('L0', 27, Y_L0 + 3); 4321 4322 /* â network lines: control net, spine, fieldbus â */ 4323 g.strokeStyle = rzInk(0.5); g.lineWidth = 1.2; 4324 line(HMI.x + HMI.w, Y_L2, OFF.tip, Y_L2); 4325 line(SX, Y_L2, SX, Y_L1); 4326 line(PLC.x + PLC.w, Y_L1, RTU.x, Y_L1); 4327 4328 /* â instrument signal lines (dashed, orthogonal) â */ 4329 g.strokeStyle = rzInk(0.36); g.lineWidth = 1; dash([3, 3]); 4330 g.beginPath(); g.moveTo(PLC.x + PLC.w / 2, PLC.y + PLC.h); g.lineTo(PLC.x + PLC.w / 2, 212); g.lineTo(FT.x, 212); g.lineTo(FT.x, FT.y - FT.r); g.stroke(); 4331 g.beginPath(); g.moveTo(RTU.x + RTU.w / 2, RTU.y + RTU.h); g.lineTo(RTU.x + RTU.w / 2, 212); g.lineTo(PT.x, 212); g.lineTo(PT.x, PT.y - PT.r); g.stroke(); 4332 dash([]); 4333 4334 /* â process pipe with moving flow dashes â */ 4335 g.strokeStyle = rzInk(0.16); g.lineWidth = 3; line(PIPE0, Y_L0, PIPE1, Y_L0); 4336 dash([7, 9]); g.lineDashOffset = -((T * FLOW_PX) % 16); 4337 g.strokeStyle = rzInk(0.55); g.lineWidth = 1.4; line(PIPE0, Y_L0, PIPE1, Y_L0); 4338 dash([]); g.lineDashOffset = 0; 4339 g.strokeStyle = rzInk(0.5); g.lineWidth = 1; line(PIPE0, Y_L0 - 5, PIPE0, Y_L0 + 5); /* inlet break */ 4340 g.fillStyle = rzInk(0.6); 4341 var ax = [84, 229]; 4342 for (j = 0; j < 2; j++) { g.beginPath(); g.moveTo(ax[j] - 4, Y_L0 - 4); g.lineTo(ax[j] + 3, Y_L0); g.lineTo(ax[j] - 4, Y_L0 + 4); g.closePath(); g.fill(); } 4343 4344 /* â leader from FT-101 to its trace chip â */ 4345 g.strokeStyle = rzInk(0.28); g.lineWidth = 1; 4346 g.beginPath(); g.moveTo(FT.x, FT.y + FT.r); g.lineTo(FT.x, 352); g.lineTo(SPARK.x + SPARK.w, 352); g.stroke(); 4347 g.fillStyle = rzInk(0.45); circle(SPARK.x + SPARK.w, 352, 1.8); g.fill(); 4348 4349 /* â probes: the write that lands, and the one the conduit stops â */ 4350 if (t >= T_P1 && t < T_HIT + 0.2) {
4351 p = easeInOut(clamp01((t - T_P1) / D_P1)); 4352 probe(PATH1, p, 1 - clamp01((t - T_HIT) / 0.2)); 4353 } 4354 if (t >= T_P2 && t < T_BLOCK + 0.9) { /* stopped at the conduit, then dissolves */ 4355 p = easeInOut(clamp01((t - T_P2) / D_P2)); 4356 probe(PATH2, p, 1 - easeInOut(clamp01((t - T_BLOCK) / 0.9))); 4357 } 4358 4359 /* â segmentation conduit C-01 (gate) â */ 4360 var closed = easeInOut(clamp01((t - T_GATE) / D_GATE)) * (1 - easeInOut(clamp01((t - T_OPEN) / D_OPEN))); 4361 roundRect(GATE.x, GATE.y, GATE.w, GATE.h, 3); g.fillStyle = paper; g.fill(); 4362 g.strokeStyle = rzInk(0.55); g.lineWidth = 1; g.stroke(); 4363 var gy = GATE.y + GATE.h / 2, gl = 5 + 15 * closed; 4364 if (closed < 1) { /* open: the spine shows through, dashed */ 4365 g.strokeStyle = rzInk(0.3 * (1 - closed)); dash([2, 3]); line(SX, GATE.y + 3, SX, GATE.y + GATE.h - 3); dash([]); 4366 } 4367 g.lineWidth = 2.5; 4368 g.strokeStyle = rzInk(0.72 * (1 - closed)); 4369 line(GATE.x + 4, gy, GATE.x + 4 + gl, gy); line(GATE.x + GATE.w - 4, gy, GATE.x + GATE.w - 4 - gl, gy); 4370 if (closed > 0) { 4371 g.strokeStyle = tk.indigo; g.globalAlpha = closed; 4372 line(GATE.x + 4, gy, GATE.x + 4 + gl, gy); line(GATE.x + GATE.w - 4, gy, GATE.x + GATE.w - 4 - gl, gy); 4373 g.globalAlpha = 1; 4374 } 4375 g.font = FONT_MONO_S; g.textAlign = 'left'; 4376 g.fillStyle = rzInk(0.55 * (1 - closed)); g.fillText('C-01', GATE.x + GATE.w + 8, gy + 3); 4377 if (closed > 0) { g.fillStyle = tk.indigo; g.globalAlpha = 0.95 * closed; g.fillText('C-01', GATE.x + GATE.w + 8, gy + 3); g.globalAlpha = 1; } 4378 /* indigo blooms: on close, and when the second write is stopped */ 4379 var bt = [T_CLOSED, T_BLOCK]; 4380 for (j = 0; j < 2; j++) { 4381 p = clamp01((t - bt[j]) / 0.7); 4382 if (p <= 0 || p >= 1) continue; 4383 d = 2 + 14 * easeOutQuint(p); 4384 roundRect(GATE.x - d, GATE.y - d, GATE.w + 2 * d, GATE.h + 2 * d, 3 + d * 0.5); 4385 g.strokeStyle = tk.indigo; g.lineWidth = 1.2; g.globalAlpha = (1 - p) * 0.55; g.stroke(); g.globalAlpha = 1; 4386 } 4387 4388 /* â off-sheet connector (L4 · IT), HMI, RTU, junctions â */ 4389 g.beginPath(); g.moveTo(OFF.tip, Y_L2); g.lineTo(OFF.x, Y_L2 - OFF.hh); g.lineTo(OFF.r, Y_L2 - OFF.hh); 4390 g.lineTo(OFF.r, Y_L2 + OFF.hh); g.lineTo(OFF.x, Y_L2 + OFF.hh); g.closePath(); 4391 g.fillStyle = paper; g.fill(); g.strokeStyle = rzInk(0.55); g.lineWidth = 1; g.stroke(); 4392 g.font = FONT_MONO_S; g.textAlign = 'center'; g.fillStyle = rzInk(0.62); 4393 g.fillText('L4 · IT', (OFF.x + OFF.r) / 2 - 2, Y_L2 + 3); 4394 nodeBox(HMI, 'HMI', paper); 4395 nodeBox(RTU, 'RTU-02', paper); 4396 g.fillStyle = rzInk(0.7); circle(SX, Y_L2, 2.2); g.fill(); circle(SX, Y_L1, 2.2); g.fill(); 4397 4398 /* â PLC-01 with the crimson alert â */ 4399 var al = easeOut(clamp01((t - T_HIT) / 0.3)) * (1 - easeInOut(clamp01((t - 6.4) / 1.2))); 4400 for (j = 0; j < 3; j++) { 4401 p = clamp01((t - (T_HIT + j * 0.5)) / 1.0); 4402 if (p <= 0 || p >= 1) continue; 4403 d = 3 + 20 * easeOutQuint(p); 4404 roundRect(PLC.x - d, PLC.y - d, PLC.w + 2 * d, PLC.h + 2 * d, 3 + d * 0.5); 4405 g.strokeStyle = tk.crimson; g.lineWidth = 1.2; g.globalAlpha = (1 - p) * 0.55; g.stroke(); g.globalAlpha = 1; 4406 } 4407 nodeBox(PLC, 'PLC-01', paper); 4408 if (al > 0) { 4409 roundRect(PLC.x, PLC.y, PLC.w, PLC.h, 3); 4410 g.strokeStyle = tk.crimson; g.lineWidth = 1.5; g.globalAlpha = al; g.stroke(); 4411 g.fillStyle = tk.crimson; circle(PLC.x + PLC.w - 7, PLC.y + 7, 2.2); g.fill(); 4412 g.globalAlpha = 1; 4413 } 4414 4415 /* â process equipment (L0): control valve, pump, FT-101, tank, PT-102 â */ 4416 g.fillStyle = paper; g.strokeStyle = rzInk(0.7); g.lineWidth = 1; 4417 g.beginPath(); g.moveTo(VALVE.x - 12, VALVE.y - 7); g.lineTo(VALVE.x - 12, VALVE.y + 7); g.lineTo(VALVE.x, VALVE.y); g.closePath(); g.fill(); g.stroke(); 4418 g.beginPath(); g.moveTo(VALVE.x + 12, VALVE.y - 7); g.lineTo(VALVE.x + 12, VALVE.y + 7); g.lineTo(VALVE.x, VALVE.y); g.closePath(); g.fill(); g.stroke(); 4419 line(VALVE.x, VALVE.y - 7, VALVE.x, VALVE.y - 17); 4420 g.beginPath(); g.arc(VALVE.x, VALVE.y - 17, 8, PI, TAU); g.closePath(); g.fillStyle = paper; g.fill(); g.stroke(); 4421 g.fillStyle = paper;
4421 circle(PUMP.x, PUMP.y, PUMP.r); g.fill(); g.stroke(); 4422 g.fillStyle = rzInk(0.62); 4423 g.beginPath(); g.moveTo(PUMP.x - 5, PUMP.y - 6); g.lineTo(PUMP.x - 5, PUMP.y + 6); g.lineTo(PUMP.x + 7, PUMP.y); g.closePath(); g.fill(); 4424 g.font = FONT_MONO_S; g.textAlign = 'center'; g.fillStyle = rzInk(0.5); 4425 g.fillText('FV-101', VALVE.x, Y_L0 + 22); g.fillText('P-101', PUMP.x, Y_L0 + 22); 4426 bubble(FT, 'FT', '101', paper); 4427 roundRect(TANK.x, TANK.y, TANK.w, TANK.h, 6); g.fillStyle = paper; g.fill(); 4428 g.save(); g.clip(); 4429 g.fillStyle = rzInk(0.06); g.fillRect(TANK.x, TANK.y + 38, TANK.w, TANK.h - 38); 4430 g.strokeStyle = rzInk(0.35); g.lineWidth = 1; line(TANK.x, TANK.y + 38, TANK.x + TANK.w, TANK.y + 38); 4431 g.restore(); 4432 roundRect(TANK.x, TANK.y, TANK.w, TANK.h, 6); g.strokeStyle = rzInk(0.7); g.lineWidth = 1; g.stroke(); 4433 g.font = FONT_MONO_S; g.textAlign = 'center'; g.fillStyle = rzInk(0.5); 4434 g.fillText('T-101', TANK.x + TANK.w / 2, TANK.y + TANK.h + 14); 4435 g.strokeStyle = rzInk(0.5); line(PT.x, PT.y + PT.r, PT.x, TANK.y); 4436 bubble(PT, 'PT', '102', paper); 4437 4438 /* â FT-101 trace chip (paper, lime strip-chart) â */ 4439 roundRect(SPARK.x, SPARK.y, SPARK.w, SPARK.h, 4); g.fillStyle = paper; g.fill(); 4440 g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke(); 4441 g.font = FONT_MONO_S; g.textAlign = 'left'; g.fillStyle = rzInk(0.55); 4442 g.fillText('FT-101 · FLOW', SPARK.x + 9, SPARK.y + 14); 4443 var vq = flowAt(loopT(Math.floor(t * 2) / 2)); 4444 g.font = FONT_MONO; g.textAlign = 'right'; g.fillStyle = rzInk(0.82); 4445 g.fillText(vq.toFixed(1) + ' m³/h', SPARK.x + SPARK.w - 9, SPARK.y + 14); 4446 var px = SPARK.x + 9, pw = SPARK.w - 18, py = SPARK.y + 24, ph = SPARK.h - 32; 4447 g.strokeStyle = rzInk(0.12); dash([2, 4]); line(px, py + ph / 2, px + pw, py + ph / 2); dash([]); 4448 g.strokeStyle = tk.lime; g.lineWidth = 1.5; g.beginPath(); 4449 var lx = 0, ly = 0; 4450 for (j = 0; j < SPARK_N; j++) { 4451 var tau = T - SPARK_WIN + SPARK_WIN * j / (SPARK_N - 1); 4452 lx = px + pw * j / (SPARK_N - 1); 4453 ly = py + ph * (1 - clamp01((flowAt(loopT(tau)) - 35) / 14)); 4454 if (j === 0) g.moveTo(lx, ly); else g.lineTo(lx, ly); 4455 } 4456 g.stroke(); 4457 g.fillStyle = tk.lime; circle(lx, ly, 2.2); g.fill(); 4458 4459 /* â status chip â */ 4460 var idx = (t >= T_HIT && t < T_CLOSED) ? 1 : (t >= T_CLOSED && t < T_BLOCK) ? 2 : (t >= T_BLOCK && t < T_OPEN + 0.1) ? 3 : 0; 4461 if (idx !== chipIdx) { chipPrev = chipIdx; chipIdx = idx; chipChangedAt = t; } 4462 var e = instant ? 1 : easeOut(clamp01((t - chipChangedAt) / T_TEXT)); 4463 if (chipChangedAt > t) e = 1; /* clock wrapped */ 4464 var mk = [tk.lime, tk.crimson, tk.indigo, tk.purple]; 4465 g.font = FONT_MONO; 4466 var cw = 0; 4467 for (j = 0; j < CHIP_TXT.length; j++) cw = Math.max(cw, g.measureText(CHIP_TXT[j]).width); 4468 cw += 40; 4469 var cx = W / 2 - cw / 2, cy = CHIP_Y; 4470 roundRect(cx, cy, cw, CHIP_H, 4); g.fillStyle = paper; g.fill(); 4471 g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke(); 4472 g.textAlign = 'left'; g.textBaseline = 'middle'; 4473 if (e < 1 && chipPrev >= 0) { 4474 g.fillStyle = mk[chipPrev]; g.globalAlpha = 1 - e; g.fillRect(cx + 12, cy + CHIP_H / 2 - 2.5, 5, 5); g.globalAlpha = 1; 4475 g.fillStyle = rzInk(0.78 * (1 - e)); g.fillText(CHIP_TXT[chipPrev], cx + 25, cy + CHIP_H / 2 + 0.5); 4476 } 4477 g.fillStyle = mk[chipIdx]; g.globalAlpha = e; g.fillRect(cx + 12, cy + CHIP_H / 2 - 2.5, 5, 5); g.globalAlpha = 1; 4478 g.fillStyle = rzInk(0.78 * e); g.fillText(CHIP_TXT[chipIdx], cx + 25, cy + CHIP_H / 2 + 0.5); 4479 g.textBaseline = 'alphabetic'; 4480 } 4481 4482 /* ââ reduced motion: one composed frame (write landed, conduit closed, trace disturbed) ââ */ 4483 var reduce = false;
4484 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 4485 if (reduce) { 4486 var drawStatic = function () { scene(6.35, 6.35, tokens(), true); }; 4487 drawStatic(); 4488 window.addEventListener('resize', drawStatic); 4489 window.addEventListener('load', drawStatic); 4490 document.addEventListener('rz-theme-change', drawStatic); 4491 if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic); 4492 return; 4493 } 4494 4495 /* ââ time-based loop; pauses when the tab is hidden or the hero is off-screen ââ */ 4496 var raf = null, t0 = null, pausedAt = null, inView = true; 4497 function now() { return (window.performance && performance.now) ? performance.now() : +new Date(); } 4498 function frame(ts) { 4499 raf = null; 4500 if (t0 === null) t0 = ts; 4501 var T = Math.max(0, (ts - t0) / 1000); 4502 scene(T % LOOP, T, tokens(), false); 4503 raf = requestAnimationFrame(frame); 4504 } 4505 function start() { 4506 if (raf !== null || document.hidden || !inView) return; 4507 if (pausedAt !== null && t0 !== null) t0 += now() - pausedAt; /* resume without a time jump */ 4508 pausedAt = null; 4509 raf = requestAnimationFrame(frame); 4510 } 4511 function stop() { 4512 if (raf === null) return; 4513 cancelAnimationFrame(raf); raf = null; pausedAt = now(); 4514 } 4515 document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); }); 4516 if ('IntersectionObserver' in window) { 4517 try { 4518 new IntersectionObserver(function (entries) { 4519 for (var q = 0; q < entries.length; q++) inView = entries[q].isIntersecting; 4520 if (inView) start(); else stop(); 4521 }, { threshold: 0 }).observe(cv); 4522 } catch (e) {} 4523 } 4524 start(); 4525 })(); 4526 4527 /* ââ 17. FINANCE â Transaction Fraud Detection âââââââââââââââ */ 4528 (function () { 4529 /* ââ 17. FINANCE â Transaction Graph âââââââââââââââââââââââââââââ 4530 Rhymes with the page's transaction-monitoring story (DORA / PCI DSS): 4531 eight account nodes (ink rings, ACC-#### micro-labels) joined by a 4532 faint blueprint graph. Transfers travel the edges as ink dots; one 4533 three-hop path lights crimson edge by edge, the destination account 4534 gets a crimson ring and a paper risk chip ("RISK 0.92 · HOLD"), a 4535 ruler-tick risk scale below tracks the score, and the case then 4536 resolves in lime ("CLEARED"). Time-based and eased, pauses when 4537 hidden / off-screen, one static frame under reduced motion. 4538 All readouts are illustrative. */ 4539 var cv = document.getElementById('rz-visual-finance'); 4540 if (!cv) return; 4541 var W = 480, H = 480; 4542 var g = cv.getContext('2d'); 4543 if (!g) return; 4544 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store */ 4545 4546 /* ââ graph (logical px): node centres + micro-label offsets ââ */ 4547 var R = 13; 4548 var NODES = [ 4549 { id: 'ACC-1041', x: 118, y: 152, lx: 0, ly: 25, al: 'center' }, 4550 { id: 'ACC-2270', x: 242, y: 116, lx: 0, ly: -20, al: 'center' }, 4551 { id: 'ACC-3385', x: 372, y: 152, lx: 0, ly: -20, al: 'center' }, 4552 { id: 'ACC-4416', x: 96, y: 240, lx: -19, ly: 3, al: 'right' }, 4553 { id: 'ACC-5528', x: 214, y: 212, lx: 4, ly: 31, al: 'left' }, 4554 { id: 'ACC-6639', x: 326, y: 236, lx: -6, ly: 25, al: 'center' }, 4555 { id: 'ACC-7823', x: 146, y: 322, lx: 0, ly: 25, al: 'center' }, 4556 { id: 'ACC-8907', x: 398, y: 312, lx: 0, ly: 25, al: 'center' } 4557 ]; 4558 var EDGES = [[0, 1], [1, 2], [0, 3], [0, 4], [1, 4], [2, 5], [4, 5], [3, 6], [4, 6], [5, 7], [2, 7], [3, 4]]; 4559 var DEST = 7, THRESH = 0.80; 4560 4561 /* ââ cadence (ms): transfers in start order; s:1 = the suspicious three-hop path ââ */ 4562 var TRAVEL = 750, TRAVEL_S = 800; 4563 var FLOWS = [ 4564 { t: 700, a: 1, b: 0 }, 4565 { t: 1450, a: 3, b: 6 }, 4566 { t: 2200, a: 2, b: 5 }, 4567 { t: 2900, a: 4, b: 1 }, 4568 { t: 3500, a: 7, b: 2 }, 4569 { t: 3900, a: 0, b: 4, s: 1 }, 4570 { t: 4900, a: 4, b: 5, s: 1 }, 4571 { t: 5300, a: 2, b: 1 }, 4572 { t: 5900, a: 5, b: 7, s: 1 }, 4573 { t: 8300, a: 1, b: 2 }, 4574 { t: 10500, a: 6, b: 4 } 4575 ]; 4576 var NF = FLOWS.length, TXN_BASE = 128; 4577 var T_HOLD = 6700, T_CLEAR = 9400, FADE = 600, LOOP = 12600, T_FADE = LOOP - FADE; /* ~12.6 s per cycle */ 4578 /* crimson ring per account on the path: origin as the hop leaves, the rest as each hop lands */ 4579 var SUS_ON = { 0: 3900, 4: 4700, 5: 5700, 7: T_HOLD }; 4580 /* risk score keyframes: [time, target, ease ms] â eased, never stepped */ 4581 var SCORE = [[0, 0.05], [1450, 0.08], [2200, 0.06], [2950, 0.11], [3650, 0.07], [4250, 0.09], 4582 [4700, 0.34], [5700, 0.66], [6700, 0.92], [T_CLEAR, 0.05, 700], [11250, 0.08], [11900, 0.05]]; 4583 /* captions (paper chip, bottom-left) and status (bottom-right) */ 4584 var CAP = [ 4585 { t: 0, s: 'SCORING · FAN-OUT · VELOCITY' }, 4586 { t: 4900, s: 'PATH ANOMALY · 3 HOPS' }, 4587 { t: T_HOLD + 300, s: 'HOLD · â¬38,500 · ANALYST REVIEW' }, 4588 { t: T_CLEAR, s: 'REVIEW CLOSED · FUNDS RELEASED' } 4589 ]; 4590 var STAT = [ 4591 { t: 0, s: 'MONITORING', c: 'ink' }, 4592 { t: T_HOLD, s: 'ON HOLD', c: 'crimson' }, 4593 { t: T_CLEAR, s: 'RESOLVED', c: 'lime' } 4594 ]; 4595 4596 var HEAD_Y = 50, RAIL_Y = 390, RAIL_X0 = 112, RAIL_X1 = 432, FOOT_Y = 428; 4597 4598 var reduce = false;
4599 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 4600 4601 function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); } 4602 function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; } 4603 function easeOut(p) { return 1 - Math.pow(1 - p, 3); } 4604 function easeOut5(p) { return 1 - Math.pow(1 - p, 5); } 4605 function pad4(n) { return ('0000' + n).slice(-4); } 4606 4607 /* ââ colour tokens: read from CSS each frame so theme switches are picked up ââ */ 4608 function parseColor(v, fb) { 4609 var m, r = -1, gg = -1, b = -1; 4610 v = (v || '').replace(/^\s+|\s+$/g, ''); 4611 if (v.charAt(0) === '#') { 4612 if (v.length === 4) { 4613 r = parseInt(v.charAt(1) + v.charAt(1), 16); gg = parseInt(v.charAt(2) + v.charAt(2), 16); b = parseInt(v.charAt(3) + v.charAt(3), 16); 4614 } else if (v.length >= 7) { 4615 r = parseInt(v.substr(1, 2), 16); gg = parseInt(v.substr(3, 2), 16); b = parseInt(v.substr(5, 2), 16); 4616 } 4617 } else if ((m = /rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/.exec(v))) { 4618 r = Math.round(+m[1]); gg = Math.round(+m[2]); b = Math.round(+m[3]); 4619 } 4620 if (isNaN(r) || isNaN(gg) || isNaN(b) || r < 0 || gg < 0 || b < 0) { 4621 return v === fb ? [125, 20, 215] : parseColor(fb, fb); 4622 } 4623 return [r, gg, b]; 4624 } 4625 function rgba(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + a + ')'; } 4626 function mixC(a, b, f) { 4627 return [Math.round(a[0] + (b[0] - a[0]) * f), Math.round(a[1] + (b[1] - a[1]) * f), Math.round(a[2] + (b[2] - a[2]) * f)]; 4628 } 4629 function inkRGB() { return document.documentElement.getAttribute('data-theme') === 'dark' ? [236, 238, 244] : [20, 21, 26]; } 4630 function tokens() { 4631 var cs = null, out; 4632 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 4633 function t(name, fb) { 4634 var v = ''; 4635 if (cs) { try { v = cs.getPropertyValue(name); } catch (e) {} } 4636 v = (v || '').replace(/^\s+|\s+$/g, ''); 4637 return v || fb; 4638 } 4639 out = { paper: t('--rz-paper', '#ffffff') }; 4640 out.crimson = parseColor(t('--rz-crimson', '#f50034'), '#f50034'); 4641 out.lime = parseColor(t('--rz-lime', '#85f403'), '#85f403'); 4642 out.purple = parseColor(t('--rz-purple', '#7d14d7'), '#7d14d7'); 4643 out.indigo = parseColor(t('--rz-indigo', '#5801f5'), '#5801f5'); 4644 out.yellow = parseColor(t('--rz-yellow', '#e6e414'), '#e6e414'); 4645 out.ink = inkRGB(); 4646 return out; 4647 } 4648 4649 /* ââ drawing helpers ââ */ 4650 function rrect(x, y, w, h, r) { /* per-corner radii, no roundRect dependency */ 4651 var tl = r[0], tr = r[1], br = r[2], bl = r[3]; 4652 g.beginPath(); 4653 g.moveTo(x + tl, y); 4654 g.lineTo(x + w - tr, y); g.arcTo(x + w, y, x + w, y + tr, tr); 4655 g.lineTo(x + w, y + h - br); g.arcTo(x + w, y + h, x + w - br, y + h, br); 4656 g.lineTo(x + bl, y + h); g.arcTo(x, y + h, x, y + h - bl, bl); 4657 g.lineTo(x, y + tl); g.arcTo(x, y, x + tl, y, tl); 4658 g.closePath(); 4659 } 4660 function tracked(str, x, y, track, align) { /* letter-spaced text; returns its start x and width */ 4661 var i, w = [], total = 0, sx; 4662 for (i = 0; i < str.length; i++) { w[i] = g.measureText(str.charAt(i)).width; total += w[i]; } 4663 total += track * (str.length - 1); 4664 sx = align === 'right' ? x - total : (align === 'center' ? x - total / 2 : x); 4665 g.textAlign = 'left'; 4666 for (i = 0, x = sx; i < str.length; i++) { g.fillText(str.charAt(i), x, y); x += w[i] + track; } 4667 return { x: sx, w: total }; 4668 } 4669 /* paper chip whose text swaps between a and b with progress f (slide + fade); 4670 bar = optional accent rgb for a folder-tab side bar */ 4671 function chip(a, b, f, x, yMid, align, paper, bar) { 4672 var pad = bar ? 18 : 10, wa, wb, w, h = 22, bx, by; 4673 g.font = "700 9px 'Courier New', monospace"; 4674 wa = g.measureText(a).width + pad + 10; wb = g.measureText(b).width + pad + 10; 4675 w = wa + (wb - wa) * f; 4676 bx = align === 'right' ? x - w : x; by = yMid - h / 2; 4677 rrect(bx, by, w, h, [3, 6, 6, 6]); 4678 g.fillStyle = paper; g.fill(); 4679 g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke(); 4680 if (bar) { g.fillStyle = rgba(bar, 1); g.fillRect(bx + 5, by + 6, 2.5, h - 12); } 4681 g.textAlign = 'left'; g.textBaseline = 'middle'; 4682 if (f < 1) { g.fillStyle = rzInk(0.78 * (1 - f)); g.fillText(a, bx + pad, yMid + 0.5 - 4 * f); } 4683 if (f > 0) { g.fillStyle = rzInk(0.78 * f); g.fillText(b, bx + pad, yMid + 0.5 + 4 * (1 - f)); } 4684 g.textBaseline = 'alphabetic';
4685 return { x: bx, w: w }; 4686 } 4687 /* which entry of a timed list is current, plus the eased swap from the previous one */ 4688 function stage(list, t, loop) { 4689 var i = 0, k; 4690 for (k = 1; k < list.length; k++) if (t >= list[k].t) i = k; 4691 if (i === 0) return { prev: list[list.length - 1], cur: list[0], f: loop > 0 ? easeOut(clamp01(t / 350)) : 1 }; 4692 return { prev: list[i - 1], cur: list[i], f: easeOut(clamp01((t - list[i].t) / 350)) }; 4693 } 4694 4695 /* ââ everything derives from the loop time â no per-frame state, no jitter ââ */ 4696 function flowState(f, t) { 4697 var d = f.s ? TRAVEL_S : TRAVEL, started = t >= f.t, landed = t >= f.t + d; 4698 return { started: started, p: started ? easeInOut(clamp01((t - f.t) / d)) : 0, landed: landed, age: landed ? t - f.t - d : 0 }; 4699 } 4700 function score(t) { 4701 var v = SCORE[0][1], i, k; 4702 for (i = 1; i < SCORE.length; i++) { 4703 k = SCORE[i]; 4704 if (t < k[0]) break; 4705 v += (k[1] - v) * easeOut5(clamp01((t - k[0]) / (k[2] || 500))); 4706 } 4707 return v; 4708 } 4709 function txnCount(t) { var n = 0, i; for (i = 0; i < NF; i++) if (t >= FLOWS[i].t) n++; return n; } 4710 4711 function draw(t, loop) { 4712 var tk = tokens(), st = [], i, j, a, b, f, q, x, y, w, e, m, pos, G = 1, n, sw, s, hot, sx, tx; 4713 var brt = [], susA = [], susMix, cl, clr, held, lp, ca, rise, bs, cap, stt, col, r, o, hA, ink; 4714 4715 if (t >= T_FADE) G = 1 - easeInOut(clamp01((t - T_FADE) / FADE)); /* clean reset: dynamic marks fade */ 4716 for (i = 0; i < NF; i++) st[i] = flowState(FLOWS[i], t); 4717 held = t >= T_HOLD; 4718 cl = t >= T_CLEAR ? easeOut(clamp01((t - T_CLEAR) / 600)) : 0; /* crimson settles to ink after clearance */ 4719 clr = t >= T_CLEAR ? easeOut(clamp01((t - T_CLEAR) / 400)) : 0; /* destination ring: crimson â lime */ 4720 ink = tk.ink; 4721 4722 /* per-node brightness (recent landings) and crimson ring alpha */ 4723 for (i = 0; i < NODES.length; i++) { 4724 brt[i] = 0; susA[i] = 0; 4725 if (SUS_ON[i] !== undefined && t >= SUS_ON[i]) { 4726 susA[i] = easeOut(clamp01((t - SUS_ON[i]) / 300)) * (i === DEST ? 1 : 0.6) * G; 4727 if (i !== DEST) susA[i] *= (1 - cl); 4728 } 4729 } 4730 for (i = 0; i < NF; i++) { 4731 if (st[i].landed) brt[FLOWS[i].b] = Math.max(brt[FLOWS[i].b], 1 - clamp01(st[i].age / 900)); 4732 } 4733 4734 g.clearRect(0, 0, W, H); 4735 g.textBaseline = 'alphabetic'; 4736 g.lineJoin = 'round'; 4737 4738 /* ââ corner crop-brackets ââ */ 4739 g.lineCap = 'butt'; 4740 g.strokeStyle = rzInk(0.3); g.lineWidth = 1.5; 4741 g.beginPath(); 4742 g.moveTo(16, 30); g.lineTo(16, 16); g.lineTo(30, 16); 4743 g.moveTo(450, 16); g.lineTo(464, 16); g.lineTo(464, 30); 4744 g.moveTo(464, 450); g.lineTo(464, 464); g.lineTo(450, 464); 4745 g.moveTo(30, 464); g.lineTo(16, 464); g.lineTo(16, 450); 4746 g.stroke(); 4747 g.lineCap = 'round'; 4748 4749 /* ââ header: eyebrow · five swatches · transaction counter chip ââ */ 4750 g.font = "600 10px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.55); 4751 w = tracked('TRANSACTION MONITORING', 36, HEAD_Y + 4, 1.5, 'left'); 4752 x = w.x + w.w + 12; 4753 col = [tk.purple, tk.lime, tk.crimson, tk.indigo, tk.yellow]; 4754 for (i = 0; i < 5; i++) { g.fillStyle = rgba(col[i], 1); g.fillRect(x + i * 9, HEAD_Y - 3, 6, 6); } 4755 n = txnCount(t); 4756 sw = n > 0 ? easeOut(clamp01((t - FLOWS[n - 1].t) / 300)) : 1; 4757 n += TXN_BASE + loop * NF; 4758 chip('TXN ' + pad4(n - 1), 'TXN ' + pad4(n), sw, 444, HEAD_Y, 'right', tk.paper, null); 4759 4760 /* ââ the graph: quiet blueprint edges ââ */ 4761 g.lineCap = 'butt'; g.lineWidth = 1; g.strokeStyle = rzInk(0.14); 4762 g.beginPath(); 4763 for (i = 0; i < EDGES.length; i++) { 4764 a = NODES[EDGES[i][0]]; b = NODES[EDGES[i][1]]; 4765 g.moveTo(a.x, a.y); g.lineTo(b.x, b.y); 4766 } 4767 g.stroke(); 4768 g.lineCap = 'round'; 4769 4770 /* ââ traces: ink while a transfer is in flight (then fading); crimson for the path, persisting ââ */ 4771 for (i = 0; i < NF; i++) { 4772 f = FLOWS[i]; s = st[i]; 4773 if (!s.started || s.p <= 0) continue; 4774 a = NODES[f.a]; b = NODES[f.b]; 4775 pos = { x: a.x + (b.x - a.x) * s.p, y: a.y + (b.y - a.y) * s.p }; 4776 if (f.s) { 4777 o = 0.9 * (1 - cl) * G; m = 0.32 * cl * G; 4778 g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(pos.x, pos.y); 4779 if (o > 0.002) { g.strokeStyle = rgba(tk.crimson, o); g.lineWidth = 1.5; g.stroke(); } 4780 if (m > 0.002) { g.strokeStyle = rzInk(m); g.lineWidth = 1.25; g.stroke(); } 4781 } else {
4782 o = 0.45 * (s.landed ? 1 - clamp01(s.age / 600) : 1) * G; 4783 if (o <= 0.002) continue; 4784 g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(pos.x, pos.y); 4785 g.strokeStyle = rzInk(o); g.lineWidth = 1.25; g.stroke(); 4786 } 4787 } 4788 4789 /* ââ transfers: a dot per edge in flight (ink; crimson along the path) ââ */ 4790 for (i = 0; i < NF; i++) { 4791 f = FLOWS[i]; s = st[i]; 4792 if (!s.started) continue; 4793 hA = s.landed ? 1 - clamp01(s.age / 150) : 1; 4794 if (hA <= 0) continue; 4795 a = NODES[f.a]; b = NODES[f.b]; 4796 pos = { x: a.x + (b.x - a.x) * s.p, y: a.y + (b.y - a.y) * s.p }; 4797 col = f.s ? tk.crimson : ink; 4798 g.fillStyle = rgba(col, (f.s ? 0.18 : 0.10) * hA * G); g.beginPath(); g.arc(pos.x, pos.y, f.s ? 8 : 7, 0, Math.PI * 2); g.fill(); 4799 g.fillStyle = rgba(col, (f.s ? 1 : 0.85) * hA * G); g.beginPath(); g.arc(pos.x, pos.y, f.s ? 3.5 : 3, 0, Math.PI * 2); g.fill(); 4800 } 4801 4802 /* ââ accounts: paper disc + ink ring; landing pulse; crimson / lime rings on the path ââ */ 4803 for (i = 0; i < NODES.length; i++) { 4804 a = NODES[i]; 4805 g.beginPath(); g.arc(a.x, a.y, R, 0, Math.PI * 2); 4806 g.fillStyle = tk.paper; g.fill(); 4807 g.strokeStyle = rzInk(0.5 + 0.4 * brt[i]); g.lineWidth = 1.25; g.stroke(); 4808 g.fillStyle = rzInk(0.45 + 0.35 * brt[i]); g.beginPath(); g.arc(a.x, a.y, 2, 0, Math.PI * 2); g.fill(); 4809 if (susA[i] > 0.002) { 4810 col = i === DEST ? mixC(tk.crimson, tk.lime, clr) : tk.crimson; 4811 g.beginPath(); g.arc(a.x, a.y, R, 0, Math.PI * 2); 4812 g.strokeStyle = rgba(col, susA[i]); g.lineWidth = i === DEST ? 1.75 : 1.5; g.stroke(); 4813 if (i === DEST) { /* outer hold ring */ 4814 g.beginPath(); g.arc(a.x, a.y, R + 5, 0, Math.PI * 2); 4815 g.strokeStyle = rgba(col, 0.45 * susA[i]); g.lineWidth = 1; g.stroke(); 4816 } 4817 } 4818 } 4819 for (i = 0; i < NF; i++) { /* landing pulses */ 4820 s = st[i]; 4821 if (!s.landed || s.age >= 700) continue; 4822 q = clamp01(s.age / 700); b = NODES[FLOWS[i].b]; 4823 g.beginPath(); g.arc(b.x, b.y, R + 11 * easeOut(q), 0, Math.PI * 2); 4824 g.strokeStyle = rgba(FLOWS[i].s ? tk.crimson : ink, 0.35 * (1 - q) * G); g.lineWidth = 1; g.stroke(); 4825 } 4826 4827 /* ââ micro-labels on paper knock-outs ââ */ 4828 g.font = "8px 'Courier New', monospace"; g.textAlign = 'left'; 4829 for (i = 0; i < NODES.length; i++) { 4830 a = NODES[i]; w = g.measureText(a.id).width; 4831 x = a.al === 'center' ? a.x + a.lx - w / 2 : (a.al === 'right' ? a.x + a.lx - w : a.x + a.lx); 4832 y = a.y + a.ly; 4833 g.fillStyle = tk.paper; g.fillRect(x - 3, y - 8, w + 6, 11); 4834 g.fillStyle = rzInk(0.55 + 0.3 * Math.max(brt[i], susA[i] / (G || 1))); 4835 g.fillText(a.id, x, y); 4836 } 4837 4838 /* ââ destination annotation: leader, risk chip (HOLD â CLEARED), clearance badge ââ */ 4839 if (held && G > 0) { 4840 b = NODES[DEST]; 4841 col = mixC(tk.crimson, tk.lime, clr); 4842 lp = easeOut(clamp01((t - T_HOLD - 150) / 350)); 4843 if (lp > 0) { 4844 g.strokeStyle = rgba(col, 0.9 * G); g.lineWidth = 1; 4845 g.beginPath(); g.moveTo(b.x - R - 1, b.y); g.lineTo(b.x - R - 1 - 23 * lp, b.y); g.stroke(); 4846 } 4847 ca = easeOut(clamp01((t - T_HOLD - 300) / 450)); 4848 if (ca > 0) { 4849 rise = 4 * (1 - ca); 4850 g.globalAlpha = ca * G; 4851 chip('RISK 0.92 · HOLD', 'CLEARED', easeOut(clamp01((t - T_CLEAR) / 350)), b.x - R - 24, b.y + rise, 'right', tk.paper, col); 4852 g.globalAlpha = 1; 4853 } 4854 bs = easeOut5(clamp01((t - T_CLEAR) / 450)); 4855 if (bs > 0) { /* check badge on the ring's top-right */ 4856 g.save(); g.translate(b.x + 9.2, b.y - 9.2); g.scale(bs, bs); g.globalAlpha = G; 4857 g.fillStyle = tk.paper; g.beginPath(); g.arc(0, 0, 7, 0, Math.PI * 2); g.fill(); 4858 g.strokeStyle = rgba(tk.lime, 1); g.lineWidth = 1.5; g.stroke(); 4859 g.beginPath(); g.moveTo(-3, 0.5); g.lineTo(-1, 2.5); g.lineTo(3, -2.5); g.stroke();
4860 g.restore(); 4861 } 4862 } 4863 4864 /* ââ risk scale: ruler ticks, hairline rail, threshold mark, eased caret + readout ââ */ 4865 s = score(t); 4866 hot = clamp01((s - (THRESH - 0.03)) / 0.05); 4867 sx = RAIL_X0 + s * (RAIL_X1 - RAIL_X0); 4868 tx = RAIL_X0 + THRESH * (RAIL_X1 - RAIL_X0); 4869 g.font = "600 9px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.55); 4870 tracked('RISK SCORE', 36, RAIL_Y + 3, 1.2, 'left'); 4871 g.lineCap = 'butt'; g.lineWidth = 1; 4872 for (i = 0; i <= 20; i++) { 4873 x = RAIL_X0 + i * 16; r = i % 5 === 0 ? 5 : 2.5; 4874 g.strokeStyle = rzInk(0.24); 4875 g.beginPath(); g.moveTo(x + 0.5, RAIL_Y - r); g.lineTo(x + 0.5, RAIL_Y + r); g.stroke(); 4876 } 4877 g.strokeStyle = rzInk(0.16); g.lineWidth = 1.5; 4878 g.beginPath(); g.moveTo(RAIL_X0, RAIL_Y); g.lineTo(RAIL_X1, RAIL_Y); g.stroke(); 4879 /* progress line: ink up to the threshold, crimson beyond it */ 4880 g.lineWidth = 1.5; 4881 g.strokeStyle = rzInk(0.4); 4882 g.beginPath(); g.moveTo(RAIL_X0, RAIL_Y); g.lineTo(Math.min(sx, tx), RAIL_Y); g.stroke(); 4883 if (sx > tx) { 4884 g.strokeStyle = rgba(tk.crimson, 0.9); 4885 g.beginPath(); g.moveTo(tx, RAIL_Y); g.lineTo(sx, RAIL_Y); g.stroke(); 4886 } 4887 g.strokeStyle = rgba(tk.crimson, 0.9); g.lineWidth = 2; 4888 g.beginPath(); g.moveTo(tx, RAIL_Y - 6); g.lineTo(tx, RAIL_Y + 6); g.stroke(); 4889 g.font = "8px 'Courier New', monospace"; g.textAlign = 'center'; 4890 g.fillStyle = rzInk(0.45); 4891 g.fillText('0', RAIL_X0, RAIL_Y + 17); g.fillText('1.0', RAIL_X1, RAIL_Y + 17); 4892 g.fillStyle = rgba(tk.crimson, 0.9); g.fillText('0.80', tx, RAIL_Y + 17); 4893 g.lineCap = 'round'; 4894 col = mixC(ink, tk.crimson, hot); 4895 g.beginPath(); g.arc(sx, RAIL_Y, 8, 0, Math.PI * 2); 4896 g.fillStyle = rgba(col, 0.08 + 0.1 * hot); g.fill(); 4897 g.fillStyle = rzInk(0.9); 4898 rrect(sx - 1.25, RAIL_Y - 9, 2.5, 18, [1.25, 1.25, 1.25, 1.25]); g.fill(); 4899 g.font = "700 9px 'Courier New', monospace"; g.textAlign = 'center'; 4900 g.fillStyle = rgba(col, 0.8 + 0.15 * hot); 4901 g.fillText(s.toFixed(2), sx, RAIL_Y - 14); 4902 4903 /* ââ footer: caption chip, status ââ */ 4904 cap = stage(CAP, t, loop); 4905 chip(cap.prev.s, cap.cur.s, cap.f, 36, FOOT_Y, 'left', tk.paper, null); 4906 stt = stage(STAT, t, loop); 4907 g.font = "600 9px 'Oxanium', sans-serif"; 4908 for (j = 0; j < 2; j++) { 4909 e = j === 0 ? stt.prev : stt.cur; 4910 f = j === 0 ? 1 - stt.f : stt.f; 4911 if (f <= 0) continue; 4912 y = j === 0 ? FOOT_Y - 4 * (1 - f) : FOOT_Y + 4 * (1 - f); 4913 g.fillStyle = rzInk(0.6 * f); 4914 w = tracked(e.s, 444, y + 3, 1.2, 'right'); 4915 g.fillStyle = e.c === 'ink' ? rzInk(0.7 * f) : rgba(e.c === 'lime' ? tk.lime : tk.crimson, f); 4916 g.beginPath(); g.arc(w.x - 11, y, 2.5, 0, Math.PI * 2); g.fill(); 4917 } 4918 } 4919 4920 /* ââ reduced motion: one composed frame (the hold), redrawn on resize / theme / font load ââ */ 4921 var T_STATIC = T_HOLD + 1500; 4922 function drawStatic() { draw(T_STATIC, 1); } 4923 if (reduce) { 4924 drawStatic(); 4925 window.addEventListener('resize', drawStatic); 4926 window.addEventListener('load', drawStatic); 4927 document.addEventListener('rz-theme-change', drawStatic); 4928 if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic); 4929 return; 4930 } 4931 4932 /* ââ time-based loop; pauses when the tab is hidden or the canvas is off-screen ââ */ 4933 var t0 = 0, started = false, raf = 0, running = false, pausedAt = 0, inView = true; 4934 function nowMs() { return (window.performance && performance.now) ? performance.now() : Date.now(); } 4935 function frame(now) { 4936 var el; 4937 raf = 0; 4938 if (typeof now !== 'number') now = nowMs(); 4939 if (!started) { started = true; t0 = now; } 4940 el = now - t0; 4941 draw(el % LOOP, Math.floor(el / LOOP)); 4942 if (running) raf = requestAnimationFrame(frame); 4943 } 4944 function start() { 4945 if (running) return; 4946 running = true; 4947 if (pausedAt && started) t0 += nowMs() - pausedAt; 4948 pausedAt = 0; 4949 if (!raf) raf = requestAnimationFrame(frame); 4950 } 4951 function stop() { 4952 if (!running) return; 4953 running = false; pausedAt = nowMs();
4954 if (raf) { cancelAnimationFrame(raf); raf = 0; } 4955 } 4956 function sync() { if (document.hidden || !inView) stop(); else start(); } 4957 document.addEventListener('visibilitychange', sync); 4958 if ('IntersectionObserver' in window) { 4959 try { 4960 new IntersectionObserver(function (entries) { 4961 for (var i = 0; i < entries.length; i++) inView = entries[i].isIntersecting; 4962 sync(); 4963 }, { threshold: 0 }).observe(cv); 4964 } catch (e) {} 4965 } 4966 sync(); 4967 })(); 4968 4969 /* ââ 18. GOVERNMENT â APT Threat Intelligence ââââââââââââââââ */ 4970 (function () { 4971 /* ââ 18. GOVERNMENT â Threat-intel board ââââââââââââââââââââââ 4972 A honeycomb of asset cells on the sheet. Intel arrives as ink 4973 dots from four feed sources in the sheet's corners; every hit 4974 looks alike until correlation lines draw between the related 4975 ones. Six links (six shared TTPs) lock a five-cell cluster â 4976 the only place the five accents appear â and the dossier chip 4977 resolves. Blueprint on white, tokens read at runtime, time-based 4978 easing, ~12.4 s loop with a hold. ââ */ 4979 var cv = document.getElementById('rz-visual-gov'); 4980 if (!cv) return; 4981 var W = 480, H = 480; 4982 var g = cv.getContext('2d'); 4983 if (!g) return; 4984 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store + zoom-aware transform */ 4985 4986 var PI = Math.PI, TAU = PI * 2; 4987 var CX = 240, CY = 228; 4988 var R = 22, RINGS = 4; /* cell circumradius, honeycomb rings */ 4989 var HW = Math.sqrt(3) * R, VS = 1.5 * R; /* column pitch, row pitch */ 4990 var LOOP = 12.4; /* seconds per cycle */ 4991 var T_FIRST = 0.7, T_STEP = 0.5, T_TRAVEL = 0.75, T_PING = 0.5; 4992 var T_BLOOM = 0.7, T_SETTLE = 0.5, T_LINK = 0.6; 4993 var T_LOCK = 6.5, T_LOCKIN = 0.6, T_LOCKSTEP = 0.1; 4994 var T_FADE = 0.9, T_TEXT = 0.35; 4995 var N_ACC = 5; 4996 4997 /* pointy-top hexagon, first vertex at the top */ 4998 var HC = [], HS = [], k; 4999 for (k = 0; k < 6; k++) { HC.push(Math.cos((60 * k - 90) * PI / 180)); HS.push(Math.sin((60 * k - 90) * PI / 180)); } 5000 function cellX(q, r) { return CX + HW * (q + r / 2); } 5001 function cellY(q, r) { return CY + VS * r; } 5002 5003 /* the board: every cell within RINGS of the centre (axial coords) */ 5004 var CELLS = [], q, r; 5005 for (r = -RINGS; r <= RINGS; r++) { 5006 for (q = Math.max(-RINGS, -r - RINGS); q <= Math.min(RINGS, -r + RINGS); q++) CELLS.push({ x: cellX(q, r), y: cellY(q, r) }); 5007 } 5008 5009 /* feed sources in the sheet's corners; side = label to the right (1) / left (-1) */ 5010 var SRC = [ 5011 { name: 'CERT', x: 40, y: 64, side: 1 }, 5012 { name: 'ISAC', x: 440, y: 64, side: -1 }, 5013 { name: 'OSINT', x: 40, y: 392, side: 1 }, 5014 { name: 'PARTNER', x: 440, y: 392, side: -1 } 5015 ]; 5016 5017 /* intel hits in arrival order; c = cluster index (A..E) or -1 for noise */ 5018 var HITS = [ 5019 { q: -2, r: -1, src: 0, c: 0 }, 5020 { q: 3, r: -3, src: 1, c: -1 }, 5021 { q: 0, r: -2, src: 1, c: 1 }, 5022 { q: -4, r: 2, src: 2, c: -1 }, 5023 { q: 2, r: -1, src: 1, c: 2 }, 5024 { q: 3, r: 1, src: 3, c: -1 }, 5025 { q: 1, r: 1, src: 3, c: 3 }, 5026 { q: 0, r: -4, src: 0, c: -1 }, 5027 { q: -1, r: 2, src: 2, c: 4 } 5028 ]; 5029 var NH = HITS.length, CL = [], i, h; 5030 for (i = 0; i < NH; i++) { 5031 h = HITS[i]; 5032 h.x = cellX(h.q, h.r); h.y = cellY(h.q, h.r); 5033 h.t0 = T_FIRST + i * T_STEP; /* leaves the source */ 5034 h.t1 = h.t0 + T_TRAVEL; /* lands on the cell */ 5035 if (h.c >= 0) CL[h.c] = h; 5036 } 5037 5038 /* correlation links between cluster cells â six of them, one per shared TTP */ 5039 var LINKS = [ 5040 { a: 0, b: 1, d: 0.2 }, { a: 1, b: 2, d: 0.2 }, { a: 2, b: 3, d: 0.2 }, 5041 { a: 1, b: 3, d: 0.35 }, { a: 3, b: 4, d: 0.2 }, { a: 4, b: 0, d: 0.35 } 5042 ]; 5043 var NL = LINKS.length, ln; 5044 for (i = 0; i < NL; i++) { 5045 ln = LINKS[i]; 5046 ln.t0 = Math.max(CL[ln.a].t1, CL[ln.b].t1) + ln.d; 5047 var ax = CL[ln.a].x, ay = CL[ln.a].y, bx = CL[ln.b].x, by = CL[ln.b].y; 5048 var dx = bx - ax, dy = by - ay, len = Math.sqrt(dx * dx + dy * dy), trim = R - 3; 5049 ln.x1 = ax + dx / len * trim; ln.y1 = ay + dy / len * trim; /* edge to edge, not centre to centre */ 5050 ln.x2 = bx - dx / len * trim;
5050 ln.y2 = by - dy / len * trim; 5051 } 5052 5053 /* five equal accents, house rotation order; fall back if a token is missing */ 5054 var ACC_KEYS = ['purple', 'lime', 'crimson', 'indigo', 'yellow']; 5055 var ACC_FALL = { purple: '#7d14d7', lime: '#85f403', crimson: '#f50034', indigo: '#5801f5', yellow: '#e6e414' }; 5056 function tokens() { 5057 var cs = null, dark = document.documentElement.getAttribute('data-theme') === 'dark'; 5058 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 5059 function rd(name, fb) { 5060 var v = ''; 5061 try { v = cs ? (cs.getPropertyValue(name) || '') : ''; } catch (e) {} 5062 v = v.replace(/^\s+|\s+$/g, ''); 5063 return v || fb; 5064 } 5065 var acc = [], j; 5066 for (j = 0; j < ACC_KEYS.length; j++) acc.push(rd('--rz-' + ACC_KEYS[j], ACC_FALL[ACC_KEYS[j]])); 5067 return { acc: acc, paper: rd('--rz-paper', dark ? '#0e0f14' : '#ffffff') }; 5068 } 5069 5070 /* ââ easing + small helpers ââ */ 5071 function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); } 5072 function easeInOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; } 5073 function easeOut(p) { return 1 - Math.pow(1 - p, 3); } 5074 function easeOutQuint(p) { return 1 - Math.pow(1 - p, 5); } 5075 function hex(x, y, rad) { 5076 g.beginPath(); 5077 g.moveTo(x + HC[0] * rad, y + HS[0] * rad); 5078 for (var j = 1; j < 6; j++) g.lineTo(x + HC[j] * rad, y + HS[j] * rad); 5079 g.closePath(); 5080 } 5081 function circle(x, y, rad) { g.beginPath(); g.arc(x, y, rad, 0, TAU); } 5082 function line(x1, y1, x2, y2) { g.beginPath(); g.moveTo(x1, y1); g.lineTo(x2, y2); g.stroke(); } 5083 /* letter-spaced text (canvas letterSpacing is not universal) */ 5084 function spaced(txt, x, y, sp, align) { 5085 var ws = [], w = 0, j; 5086 for (j = 0; j < txt.length; j++) { ws.push(g.measureText(txt.charAt(j)).width); w += ws[j]; } 5087 w += sp * (txt.length - 1); 5088 var px = align === 'right' ? x - w : (align === 'center' ? x - w / 2 : x); 5089 g.textAlign = 'left'; 5090 for (j = 0; j < txt.length; j++) { g.fillText(txt.charAt(j), px, y); px += ws[j] + sp; } 5091 return w; 5092 } 5093 /* dossier chip: rounded body with a small folder tab at the top-left */ 5094 function chipPath(x, y, w, hh) { 5095 var rr = 4, tw = 22, th = 5; 5096 g.beginPath(); 5097 g.moveTo(x, y + hh - rr); 5098 g.lineTo(x, y - th + 2); g.arcTo(x, y - th, x + 2, y - th, 2); 5099 g.lineTo(x + tw - 4, y - th); g.lineTo(x + tw, y); 5100 g.lineTo(x + w - rr, y); g.arcTo(x + w, y, x + w, y + rr, rr); 5101 g.lineTo(x + w, y + hh - rr); g.arcTo(x + w, y + hh, x + w - rr, y + hh, rr); 5102 g.lineTo(x + rr, y + hh); g.arcTo(x, y + hh, x, y + hh - rr, rr); 5103 g.closePath(); 5104 } 5105 var FONT_LABEL = "600 10px 'Oxanium', sans-serif"; 5106 var FONT_MONO = "bold 9px 'Courier New', monospace"; 5107 var FONT_MONO_S = "bold 8.5px 'Courier New', monospace"; 5108 var CHIP_IDLE = 'INTEL FEED · 04 SOURCES'; 5109 var CHIP_FINAL = 'APT · TTPs 6 · CONFIDENCE HIGH'; 5110 5111 /* chip text crossfade memory (the only non-derived state) */ 5112 var chipText = '', chipPrev = '', chipChangedAt = -10; 5113 5114 /* ââ one composed frame at loop-time t (0..LOOP), absolute time T ââ */ 5115 function scene(t, T, tk, instant) { 5116 g.clearRect(0, 0, W, H); 5117 g.lineCap = 'round'; g.lineJoin = 'round'; 5118 g.textBaseline = 'alphabetic'; 5119 5120 var keep = 1 - easeInOut(clamp01((t - (LOOP - T_FADE)) / T_FADE)); /* end-of-cycle fade */ 5121 var cycle = Math.floor(T / LOOP); 5122 var acc = tk.acc, paper = tk.paper; 5123 var lockAll = easeOut(clamp01((t - T_LOCK) / T_LOCKIN)) * keep; 5124 var j, x, y, a; 5125 5126 /* â corner crop-brackets (the sheet) â */ 5127 g.strokeStyle = rzInk(0.45); g.lineWidth = 1; 5128 var m = 8.5, L = 12; 5129 line(m, m, m + L, m); line(m, m, m, m + L); 5130 line(W - m, m, W - m - L, m); line(W - m, m, W - m, m + L); 5131 line(m, H - m, m + L, H - m); line(m, H - m, m, H - m - L); 5132 line(W - m, H - m, W - m - L, H - m); line(W - m, H - m, W - m, H - m - L); 5133 5134 /* â eyebrow + window readout â */ 5135 g.fillStyle = rzInk(0.62); g.font = FONT_LABEL; 5136 var ew = spaced('THREAT-INTEL BOARD', 27, 20, 1.4, 'left'); 5137 g.fillStyle = rzInk(0.22);
5137 g.fillRect(27, 24, ew, 1); 5138 g.fillStyle = rzInk(0.55); g.font = FONT_MONO_S; g.textAlign = 'right'; 5139 g.fillText('WINDOW · 24H', W - 27, 20); 5140 5141 /* â the honeycomb of asset cells â */ 5142 g.strokeStyle = rzInk(0.14); g.lineWidth = 1; 5143 for (j = 0; j < CELLS.length; j++) { hex(CELLS[j].x, CELLS[j].y, R - 1.5); g.stroke(); } 5144 5145 /* â per-hit timing â */ 5146 var fl = [], st = [], n = 0; 5147 for (j = 0; j < NH; j++) { 5148 h = HITS[j]; 5149 fl[j] = clamp01((t - h.t0) / T_TRAVEL); 5150 st[j] = easeOut(clamp01((t - h.t1) / T_SETTLE)) * keep; 5151 if (t >= h.t1) n++; 5152 } 5153 5154 /* â feed sources: port glyph, label, departure pings â */ 5155 for (j = 0; j < SRC.length; j++) { 5156 var s = SRC[j], live = 0, q2; 5157 for (q2 = 0; q2 < NH; q2++) { 5158 if (HITS[q2].src !== j) continue; 5159 var pg = clamp01((t - HITS[q2].t0) / T_PING); 5160 if (pg > 0 && pg < 1) { 5161 g.strokeStyle = rzInk((1 - pg) * 0.4); g.lineWidth = 1; 5162 circle(s.x, s.y, 5 + 11 * easeOut(pg)); g.stroke(); 5163 } 5164 if (fl[q2] > 0 && fl[q2] < 1) live = 1; 5165 } 5166 g.fillStyle = paper; g.fillRect(s.x - 4, s.y - 4, 8, 8); 5167 g.strokeStyle = rzInk(0.55); g.lineWidth = 1; g.strokeRect(s.x - 3.5, s.y - 3.5, 7, 7); 5168 g.fillStyle = rzInk(0.35 + 0.55 * live); circle(s.x, s.y, 1.6); g.fill(); 5169 g.font = FONT_MONO_S; g.textAlign = s.side > 0 ? 'left' : 'right'; 5170 g.fillStyle = rzInk(0.55); 5171 g.fillText(s.name, s.x + s.side * 10, s.y + 3); 5172 } 5173 5174 /* â correlation links, drawn edge to edge as both ends land â */ 5175 var linked = 0; 5176 for (j = 0; j < NL; j++) { 5177 ln = LINKS[j]; 5178 var lp = easeInOut(clamp01((t - ln.t0) / T_LINK)); 5179 if (lp <= 0) continue; 5180 if (lp >= 1) linked++; 5181 g.strokeStyle = rzInk((0.32 + 0.14 * lockAll) * keep); g.lineWidth = 1 + 0.15 * lockAll; 5182 line(ln.x1, ln.y1, ln.x1 + (ln.x2 - ln.x1) * lp, ln.y1 + (ln.y2 - ln.y1) * lp); 5183 } 5184 5185 /* â landed cells: identical ink marks until the cluster locks â */ 5186 for (j = 0; j < NH; j++) { 5187 var sv = st[j]; 5188 if (sv <= 0) continue; 5189 h = HITS[j]; 5190 if (h.c >= 0) { 5191 var lk = easeOut(clamp01((t - (T_LOCK + h.c * T_LOCKSTEP)) / T_LOCKIN)) * keep; 5192 var col = acc[(cycle + h.c) % N_ACC]; 5193 /* ink mark hands over to an accent tint, ring and dot */ 5194 g.fillStyle = rzInk(0.07 * sv * (1 - lk)); hex(h.x, h.y, R - 1.5); g.fill(); 5195 g.strokeStyle = rzInk(0.28 * sv * (1 - lk)); g.lineWidth = 1; hex(h.x, h.y, R - 1.5); g.stroke(); 5196 if (lk > 0) { 5197 g.fillStyle = col; g.strokeStyle = col; 5198 g.globalAlpha = 0.1 * lk; hex(h.x, h.y, R - 1.5); g.fill(); 5199 g.globalAlpha = lk; g.lineWidth = 1.5; hex(h.x, h.y, R - 2.5); g.stroke(); 5200 g.globalAlpha = 1; 5201 } 5202 if (lk < 1) { g.fillStyle = rzInk(0.75 * sv * (1 - lk)); circle(h.x, h.y, 2.4); g.fill(); } 5203 if (lk > 0) { g.fillStyle = col; g.globalAlpha = lk; circle(h.x, h.y, 2.4); g.fill(); g.globalAlpha = 1; } 5204 } else { 5205 /* noise: same mark, dismissed with a small dash once the cluster locks */ 5206 var dim = 1 - 0.55 * lockAll; 5207 g.fillStyle = rzInk(0.07 * sv * dim); hex(h.x, h.y, R - 1.5); g.fill(); 5208 g.strokeStyle = rzInk(0.28 * sv * dim); g.lineWidth = 1; hex(h.x, h.y, R - 1.5); g.stroke(); 5209 g.fillStyle = rzInk(0.75 * sv * dim); circle(h.x, h.y, 2.4); g.fill(); 5210 if (lockAll > 0) { g.strokeStyle = rzInk(0.5 * lockAll * sv); g.lineWidth = 1.2; line(h.x - 4, h.y + 7, h.x + 4, h.y + 7); } 5211 } 5212 } 5213 5214 /* â arrival blooms (ink) and lock blooms (accent) â */ 5215 for (j = 0; j < NH; j++) { 5216 h = HITS[j]; 5217 var bl = clamp01((t - h.t1) / T_BLOOM); 5218 if (bl > 0 && bl < 1) { 5219 g.strokeStyle = rzInk((1 - bl) * 0.45); g.lineWidth = 1.2; 5220 hex(h.x, h.y, R + 12 * easeOutQuint(bl)); g.stroke(); 5221 } 5222 if (h.c < 0) continue; 5223 var lb = clamp01((t - (T_LOCK + h.c * T_LOCKSTEP)) / T_BLOOM); 5224 if (lb > 0 && lb < 1) { 5225 g.strokeStyle = acc[(cycle + h.c) % N_ACC]; g.globalAlpha = (1 - lb) * 0.55 * keep; g.lineWidth = 1.2; 5226 hex(h.x, h.y, R + 14 * easeOutQuint(lb)); g.stroke(); 5227 g.globalAlpha = 1; 5228 } 5229 } 5230 5231 /* â intel dots in flight (comet tail + dot) â */ 5232 for (j = 0; j < NH; j++) { 5233 if (!(fl[j] > 0 && fl[j] < 1)) continue; 5234 h = HITS[j]; 5235 var so = SRC[h.src]; 5236 var pe = easeInOut(fl[j]), pt = Math.max(0, pe - 0.16); 5237 var hx = so.x + (h.x - so.x) * pe, hy = so.y + (h.y - so.y) * pe; 5238 var tx = so.x + (h.x - so.x) * pt, ty = so.y + (h.y - so.y) * pt; 5239 var dotA = Math.min(1, fl[j] / 0.08); 5240 var gr = g.createLinearGradient(tx, ty, hx, hy); 5241 gr.addColorStop(0, rzInk(0)); gr.addColorStop(1, rzInk(0.45 * dotA)); 5242 g.strokeStyle = gr; g.lineWidth = 1.6; line(tx, ty, hx, hy); 5243 g.fillStyle = rzInk(0.9 * dotA); circle(hx, hy, 2.6); g.fill(); 5244 } 5245 5246 /* â dossier chip â */ 5247 var txt = n === 0 ? CHIP_IDLE : (t >= T_LOCK ? CHIP_FINAL : 'CORRELATING · ' + n + ' HITS · ' + linked + ' LINKED'); 5248 if (t >= LOOP - T_FADE) txt = CHIP_IDLE; 5249 if (txt !== chipText) { chipPrev = chipText; chipText = txt; chipChangedAt = t; } 5250 var e = instant ? 1 : easeOut(clamp01((t - chipChangedAt) / T_TEXT)); 5251 if (chipChangedAt > t) e = 1; /* clock wrapped */ 5252 g.font = FONT_MONO; 5253 var cw = g.measureText(CHIP_FINAL).width + 30, ch = 24, cx = CX - cw / 2, cy = 432; 5254 chipPath(cx, cy, cw, ch); 5255 g.fillStyle = paper; g.fill(); 5256 g.strokeStyle = rzInk(0.15); g.lineWidth = 1; g.stroke(); 5257 g.textAlign = 'center'; g.textBaseline = 'middle'; 5258 if (e < 1 && chipPrev) { g.fillStyle = rzInk(0.78 * (1 - e));
5258 g.fillText(chipPrev, CX, cy + ch / 2 + 0.5); } 5259 g.fillStyle = rzInk(0.78 * e); g.fillText(chipText, CX, cy + ch / 2 + 0.5); 5260 g.textBaseline = 'alphabetic'; 5261 } 5262 5263 /* ââ reduced motion: one composed, fully correlated frame ââ */ 5264 var reduce = false; 5265 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 5266 if (reduce) { 5267 var drawStatic = function () { scene(LOOP - T_FADE - 0.05, 0, tokens(), true); }; 5268 drawStatic(); 5269 window.addEventListener('resize', drawStatic); 5270 window.addEventListener('load', drawStatic); 5271 document.addEventListener('rz-theme-change', drawStatic); 5272 if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(drawStatic); 5273 return; 5274 } 5275 5276 /* ââ time-based loop; pauses when the tab is hidden or the hero is off-screen ââ */ 5277 var raf = null, t0 = null, pausedAt = null, inView = true; 5278 function now() { return (window.performance && performance.now) ? performance.now() : +new Date(); } 5279 function frame(ts) { 5280 raf = null; 5281 if (t0 === null) t0 = ts; 5282 var T = Math.max(0, (ts - t0) / 1000); 5283 scene(T % LOOP, T, tokens(), false); 5284 raf = requestAnimationFrame(frame); 5285 } 5286 function start() { 5287 if (raf !== null || document.hidden || !inView) return; 5288 if (pausedAt !== null && t0 !== null) t0 += now() - pausedAt; /* resume without a time jump */ 5289 pausedAt = null; 5290 raf = requestAnimationFrame(frame); 5291 } 5292 function stop() { 5293 if (raf === null) return; 5294 cancelAnimationFrame(raf); raf = null; pausedAt = now(); 5295 } 5296 document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); }); 5297 if ('IntersectionObserver' in window) { 5298 try { 5299 new IntersectionObserver(function (entries) { 5300 for (var q3 = 0; q3 < entries.length; q3++) inView = entries[q3].isIntersecting; 5301 if (inView) start(); else stop(); 5302 }, { threshold: 0 }).observe(cv); 5303 } catch (e) {} 5304 } 5305 start(); 5306 })(); 5307 5308 /* ââ 19. HEALTHCARE â Medical IoT Security ââââââââââââââââââââ */ 5309 (function () { 5310 var cv = document.getElementById('rz-visual-health'); 5311 if (!cv) return; 5312 var W = 480, H = 480; 5313 var g = cv.getContext('2d'); 5314 if (!g) return; 5315 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store */ 5316 5317 var TAU = Math.PI * 2; 5318 5319 /* ââ timeline (seconds) â one calm loop with a long hold ââ */ 5320 var LOOP = 12.6; 5321 var T_ANOM = 2.2; /* anomaly surfaces on the pump */ 5322 var T_RING = 4.6; /* segmentation ring draws in (1.1s) */ 5323 var T_SEVER = 5.3; /* links to the pump are cut (0.8s) */ 5324 var T_ROUTE = 6.0; /* mesh re-routes wearable â gateway (1.0s) */ 5325 var T_RESET = 11.0; /* everything eases back to nominal (1.2s) */ 5326 5327 /* ââ palette: read tokens once per frame, fall back per theme ââ */ 5328 var FB = { 5329 light: { purple: '#7d14d7', indigo: '#5801f5', crimson: '#f50034', lime: '#85f403', yellow: '#e6e414', paper: '#ffffff' }, 5330 dark: { purple: '#9a45ee', indigo: '#7a56ff', crimson: '#ff3355', lime: '#8ef513', yellow: '#f0ee2a', paper: '#0e0f14' } 5331 }; 5332 var pal = {}; 5333 function readPalette() { 5334 var dark = document.documentElement.getAttribute('data-theme') === 'dark'; 5335 var fb = dark ? FB.dark : FB.light, cs = null; 5336 try { cs = window.getComputedStyle(document.documentElement); } catch (e) {} 5337 function tok(n, f) { var v = cs ? cs.getPropertyValue(n) : ''; v = (v || '').replace(/\s/g, ''); return v || f; } 5338 pal.purple = tok('--rz-purple', fb.purple); 5339 pal.indigo = tok('--rz-indigo', fb.indigo); 5340 pal.crimson = tok('--rz-crimson', fb.crimson); 5341 pal.lime = tok('--rz-lime', fb.lime); 5342 pal.yellow = tok('--rz-yellow', fb.yellow); 5343 pal.paper = tok('--rz-paper', fb.paper); 5344 } 5345 /* colour token + alpha â rgba() (hex or rgb() input; anything else passes through) */ 5346 function tint(c, a) { 5347 var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(c); 5348 if (m) { 5349 var h = m[1]; 5350 if (h.length === 3) h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2); 5351 return 'rgba(' + parseInt(h.substr(0, 2), 16) + ',' + parseInt(h.substr(2, 2), 16) + ',' + parseInt(h.substr(4, 2), 16) + ',' + a + ')'; 5352 } 5353 var r = /^rgba?\(([^)]+)\)$/i.exec(c); 5354 if (r) { var p = r[1].split(','); return 'rgba(' + p[0] + ',' + p[1] + ',' + p[2] + ',' + a + ')'; } 5355 return c; 5356 } 5357 5358 /* ââ easing ââ */ 5359 function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; } 5360 function cubicIO(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; } 5361 function quintIO(p) { return p < 0.5 ? 16 * p * p * p * p * p : 1 - Math.pow(-2 * p + 2, 5) / 2; } 5362 function cubicOut(p) { return 1 - Math.pow(1 - p, 3); } 5363 function seg(t, t0, dur, ease) { return ease(clamp01((t - t0) / dur)); } 5364 function lerp(a, b, p) { return a + (b - a) * p; } 5365 5366 /* ââ path helpers ââ */ 5367 function rr(x, y, w, h, r) { /* rounded rect, uniform radius */ 5368 g.moveTo(x + r, y); 5369 g.lineTo(x + w - r, y); g.quadraticCurveTo(x + w, y, x + w, y + r); 5370 g.lineTo(x + w, y + h - r); g.quadraticCurveTo(x + w, y + h, x + w - r, y + h); 5371 g.lineTo(x + r, y + h); g.quadraticCurveTo(x, y + h, x, y + h - r); 5372 g.lineTo(x, y + r); g.quadraticCurveTo(x, y, x + r, y); 5373 g.closePath(); 5374 } 5375 function tabRect(x, y, w, h) { /* folder-tab radius: 4 / 10 / 10 / 10 (rhymes with the page cards) */ 5376 var a = 4, b = 10; 5377 g.moveTo(x + a, y); 5378 g.lineTo(x + w - b, y); g.quadraticCurveTo(x + w, y, x + w, y + b); 5379 g.lineTo(x + w, y + h - b); g.quadraticCurveTo(x + w, y + h, x + w - b, y + h); 5380 g.lineTo(x + b, y + h); g.quadraticCurveTo(x, y + h, x, y + h - b); 5381 g.lineTo(x, y + a); g.quadraticCurveTo(x, y, x + a, y); 5382 g.closePath(); 5383 } 5384 /* tracked text (canvas letterSpacing is not universal) â returns drawn width */ 5385 function spaced(txt, x, y, sp, align) { 5386 var i, w = 0, ws = []; 5387 for (i = 0; i < txt.length; i++) { ws[i] = g.measureText(txt.charAt(i)).width;
5387 w += ws[i]; } 5388 w += sp * (txt.length - 1); 5389 var sx = align === 'right' ? x - w : align === 'center' ? x - w / 2 : x; 5390 g.textAlign = 'left'; 5391 for (i = 0; i < txt.length; i++) { g.fillText(txt.charAt(i), sx, y); sx += ws[i] + sp; } 5392 return w; 5393 } 5394 function spacedWidth(txt, sp) { 5395 var i, w = 0; 5396 for (i = 0; i < txt.length; i++) w += g.measureText(txt.charAt(i)).width; 5397 return w + sp * (txt.length - 1); 5398 } 5399 5400 /* ââ device glyphs (ink strokes, local coords centred on the node) ââ */ 5401 function gMonitor() { 5402 g.beginPath(); rr(-9, -8, 18, 13, 2); g.stroke(); 5403 g.beginPath(); g.moveTo(-6, -1.5); g.lineTo(-3, -1.5); g.lineTo(-1.5, -5); g.lineTo(0.5, 1.5); g.lineTo(2, -1.5); g.lineTo(6, -1.5); g.stroke(); 5404 g.beginPath(); g.moveTo(0, 5); g.lineTo(0, 8.5); g.moveTo(-4.5, 8.5); g.lineTo(4.5, 8.5); g.stroke(); 5405 } 5406 function gPump() { 5407 g.beginPath(); rr(-6.5, -9.5, 13, 19, 2.5); g.stroke(); 5408 g.beginPath(); rr(-4, -6.5, 8, 4.5, 1); g.stroke(); 5409 g.beginPath(); g.arc(-2.5, 3, 1.1, 0, TAU); g.moveTo(3.6, 3); g.arc(2.5, 3, 1.1, 0, TAU); g.fill(); 5410 g.beginPath(); g.moveTo(-6.5, -3); g.lineTo(-10.5, -3); g.lineTo(-10.5, -9); g.stroke(); 5411 g.beginPath(); g.arc(-10.5, -11.5, 1.6, 0, TAU); g.stroke(); 5412 } 5413 function gImaging() { 5414 g.beginPath(); g.arc(0, -1, 9.5, 0, TAU); g.stroke(); 5415 g.beginPath(); g.arc(0, -1, 4, 0, TAU); g.stroke(); 5416 g.beginPath(); g.moveTo(-11, 10.5); g.lineTo(11, 10.5); g.stroke(); 5417 } 5418 function gBadge() { 5419 g.beginPath(); rr(-7, -8, 14, 17, 2); g.stroke(); 5420 g.beginPath(); g.moveTo(-2, -8); g.lineTo(-2, -11.5); g.lineTo(2, -11.5); g.lineTo(2, -8); g.stroke(); 5421 g.beginPath(); g.arc(0, -2.5, 2.4, 0, TAU); g.stroke(); 5422 g.beginPath(); g.moveTo(-4, 2.5); g.lineTo(4, 2.5); g.moveTo(-3, 5.5); g.lineTo(3, 5.5); g.stroke(); 5423 } 5424 function gGateway() { 5425 g.beginPath(); rr(-9.5, -1.5, 19, 10, 2); g.stroke(); 5426 g.beginPath(); g.arc(-5, 3.5, 1.1, 0, TAU); g.moveTo(1.1, 3.5); g.arc(0, 3.5, 1.1, 0, TAU); g.moveTo(6.1, 3.5); g.arc(5, 3.5, 1.1, 0, TAU); g.fill(); 5427 g.beginPath(); g.moveTo(-4.5, -1.5); g.lineTo(-4.5, -9); g.moveTo(4.5, -1.5); g.lineTo(4.5, -9); g.stroke(); 5428 g.beginPath(); g.moveTo(-6.5, -9); g.lineTo(-2.5, -9); g.moveTo(2.5, -9); g.lineTo(6.5, -9); g.stroke(); 5429 } 5430 function gWearable() { 5431 g.beginPath(); rr(-6.5, -6.5, 13, 13, 3); g.stroke(); 5432 g.beginPath(); rr(-3.5, -11, 7, 4, 1); g.stroke(); 5433 g.beginPath(); rr(-3.5, 7, 7, 4, 1); g.stroke(); 5434 g.beginPath(); g.moveTo(0, 0); g.lineTo(0, -3.5); g.moveTo(0, 0); g.lineTo(2.5, 1); g.stroke(); 5435 } 5436 5437 /* ââ mesh ââ */ 5438 var nodes = [ 5439 { lbl: 'WEARABLE', x: 256, y: 104, glyph: gWearable }, 5440 { lbl: 'MONITOR', x: 112, y: 158, glyph: gMonitor }, 5441 { lbl: 'PUMP', x: 386, y: 158, glyph: gPump }, 5442 { lbl: 'GATEWAY', x: 250, y: 238, glyph: gGateway, hub: true }, 5443 { lbl: 'BADGE', x: 110, y: 316, glyph: gBadge }, 5444 { lbl: 'IMAGING', x: 392, y: 316, glyph: gImaging } 5445 ]; 5446 var ANOM = 2; /* the infusion pump misbehaves */ 5447 var edges = [[0, 1], [0, 2], [1, 3], [2, 3], [3, 5], [3, 4], [4, 1], [5, 2], [4, 5]]; 5448 var reroute = [0, 3]; /* wearable â gateway, established after isolation */ 5449 var NODE_R = 22; 5450 var RING_R = 46; 5451 5452 /* ââ vitals trace (P · QRS · T), phase 0..1 ââ */ 5453 function bump(p, c, w, a) { var d = (p - c) / w; return a * Math.exp(-d * d); } 5454 function ecg(p) { 5455 return bump(p, 0.18, 0.030, 0.13) - bump(p, 0.360, 0.007, 0.08) + bump(p, 0.385, 0.011, 1.0) 5456 - bump(p, 0.412, 0.009, 0.24) + bump(p, 0.58, 0.045, 0.26); 5457 } 5458 5459 /* ââ frame ââ */ 5460 function draw(now, t) { 5461 g.clearRect(0, 0, W, H); 5462 readPalette(); 5463 g.lineCap = 'round'; g.lineJoin = 'round'; g.textBaseline = 'middle'; 5464 5465 /* phases */ 5466 var resetF = seg(t, T_RESET, 1.2, cubicIO); 5467 var anom = seg(t, T_ANOM, 0.5, cubicOut) * (1 - resetF); 5468 var ringP = seg(t, T_RING, 1.1, quintIO); 5469 var ringA = 1 - resetF; 5470 var tickA = seg(t, T_RING + 1.15, 0.4, cubicOut) * ringA; 5471 var hazA = seg(t, T_RING + 0.9, 0.7, cubicIO) * ringA; 5472 var sever = seg(t, T_SEVER, 0.8, cubicIO); 5473 var restore = seg(t, T_RESET, 1.2, cubicIO); 5474 var cutF = 1 - sever + sever * restore; /* 1 = link intact, 0 = link cut */ 5475 var routeP = seg(t, T_ROUTE, 1.0, quintIO); 5476 var routeA = 1 - resetF; 5477 var routeOn = seg(t, T_ROUTE + 1.0, 0.4, cubicOut) * routeA; 5478 var chipA = seg(t, T_ANOM + 0.25, 0.5, cubicOut) * (1 - resetF); 5479 var isoF = seg(t, T_SEVER + 0.2, 0.5, cubicIO); 5480 var breathe = 0.75 + 0.25 * Math.sin(now / 1000 * TAU / 1.8); 5481 5482 var i, a, b, x, y, m; 5483 5484 function edgeFactor(k) { /* 1 = link intact, 0 = link cut */ 5485 var e = edges[k]; 5486 return (e[0] === ANOM || e[1] === ANOM) ? cutF : 1; 5487 } 5488 function packet(ax, ay, bx, by, phase, alpha) { /* telemetry packet (purple) */ 5489 var px = lerp(ax, bx, phase), py = lerp(ay, by, phase); 5490 g.fillStyle = tint(pal.purple, 0.15 * alpha); 5491 g.beginPath(); g.arc(px, py, 4.5, 0, TAU); g.fill(); 5492 g.fillStyle = tint(pal.purple, 0.9 * alpha); 5493 g.beginPath(); g.arc(px, py, 2.1, 0, TAU); g.fill(); 5494 } 5495 5496 /* ââ frame furniture: ruler ticks (top edge) + corner crop-brackets ââ */ 5497 g.lineWidth = 1; 5498 for (x = 24; x < W; x += 24) { 5499 var fx = x / W; 5500 m = fx < 0.12 ? fx / 0.12 : fx > 0.88 ? (1 - fx) / 0.12 : 1; 5501 g.strokeStyle = rzInk(0.22 * m); 5502 g.beginPath(); g.moveTo(x + 0.5, 0); g.lineTo(x + 0.5, 7); g.stroke(); 5503 } 5504 g.strokeStyle = rzInk(0.3); g.lineWidth = 1.5; 5505 var ins = 5, arm = 14; 5506 g.beginPath(); 5507 g.moveTo(ins, ins + arm); g.lineTo(ins, ins); g.lineTo(ins + arm, ins); 5508 g.moveTo(W - ins - arm, ins); g.lineTo(W - ins, ins); g.lineTo(W - ins, ins + arm); 5509 g.moveTo(ins, H - ins - arm); g.lineTo(ins, H - ins); g.lineTo(ins + arm, H - ins); 5510 g.moveTo(W - ins - arm, H - ins); g.lineTo(W - ins, H - ins); g.lineTo(W - ins, H - ins - arm); 5511 g.stroke(); 5512 5513 /* ââ header: eyebrow + micro readout ââ */ 5514 g.strokeStyle = rzInk(0.85); g.lineWidth = 2; 5515 g.beginPath(); g.moveTo(26, 40); g.lineTo(44, 40); g.stroke(); 5516 g.font = '600 10px "Oxanium", sans-serif'; g.fillStyle = rzInk(0.62); 5517 spaced('MEDICAL IoT MESH', 52, 40, 1.4, 'left'); 5518 var links = 0; 5519 for (i = 0; i < edges.length; i++) links += (edgeFactor(i) > 0.5) ? 1 : 0; 5520 if (routeP > 0.5 && routeA > 0.5) links += 1; 5521 g.font = '9px "Courier New", monospace'; g.fillStyle = rzInk(0.5); 5522 spaced('WARD 3 · 6 NODES · ' + links + ' LINKS', 26, 57, 0.4, 'left'); 5523 5524 /* ââ mesh links ââ */ 5525 g.lineWidth = 1; 5526 for (i = 0; i < edges.length; i++) { 5527 var f = edgeFactor(i); 5528 a = nodes[edges[i][0]]; b = nodes[edges[i][1]]; 5529 if (f < 0.999) { /* cut link: faint dashed ghost keeps the topology legible */ 5530 g.strokeStyle = rzInk(0.09 * (1 - f)); g.setLineDash([3, 5]); 5531 g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(b.x, b.y); g.stroke(); 5532 g.setLineDash([]); 5533 } 5534 if (f <= 0.001) continue; 5535 g.strokeStyle = rzInk(0.24 * f); 5536 g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(b.x, b.y); g.stroke(); 5537 } 5538 /* re-route: grows from the wearable to the gateway, yellow leading mark while drawing */ 5539 if (routeP > 0 && routeA > 0.001) { 5540 a = nodes[reroute[0]]; b = nodes[reroute[1]]; 5541 x = lerp(a.x, b.x, routeP); y = lerp(a.y, b.y, routeP); 5542 g.strokeStyle = rzInk(0.24 * routeA); g.lineWidth = 1; 5543 g.beginPath(); g.moveTo(a.x, a.y); g.lineTo(x, y); g.stroke(); 5544 if (routeP < 1) { 5545 var lead = Math.sin(Math.PI * routeP); 5546 g.fillStyle = tint(pal.yellow, 0.95 * lead); 5547 g.beginPath(); g.arc(x, y, 2.6, 0, TAU); g.fill(); 5548 g.strokeStyle = tint(pal.yellow, 0.35 * lead); g.lineWidth = 1; 5549 g.beginPath(); g.arc(x, y, 6, 0, TAU); g.stroke(); 5550 } 5551 } 5552 5553 /* ââ telemetry packets travelling the live links ââ */ 5554 for (i = 0; i < edges.length; i++) { 5555 var ef = edgeFactor(i); 5556 if (ef <= 0.001) continue; 5557 a = nodes[edges[i][0]]; b = nodes[edges[i][1]]; 5558 var ph = (now / 1000 / 3.6 + i * 0.29) % 1; 5559 if (i % 2) ph = 1 - ph; 5560 packet(a.x, a.y, b.x, b.y, ph, ef); 5561 } 5562 if (routeOn > 0.001) { 5563 a = nodes[reroute[0]]; b = nodes[reroute[1]]; 5564 packet(a.x, a.y, b.x, b.y, (now / 1000 / 3.6 + 0.61) % 1, routeOn); 5565 } 5566 5567 /* ââ segmentation: hazard annulus, indigo ring, compass ticks ââ */ 5568 var P = nodes[ANOM]; 5569 if (hazA > 0.001) { 5570 g.save(); 5571 g.beginPath(); g.arc(P.x, P.y, RING_R - 5, 0, TAU); g.arc(P.x, P.y, NODE_R + 6, 0, TAU, true); g.clip(); 5572 g.strokeStyle = rzInk(0.10 * hazA); g.lineWidth = 2; 5573 g.beginPath(); 5574 for (var k = -104; k <= 104; k += 7) { g.moveTo(P.x + k - 52, P.y + 52); g.lineTo(P.x + k + 52, P.y - 52); } 5575 g.stroke();
5576 g.restore(); 5577 } 5578 if (ringP > 0 && ringA > 0.001) { 5579 g.strokeStyle = tint(pal.indigo, 0.9 * ringA); g.lineWidth = 1.5; 5580 g.beginPath(); g.arc(P.x, P.y, RING_R, -Math.PI / 2, -Math.PI / 2 + TAU * ringP); g.stroke(); 5581 if (ringP < 1) { 5582 var ex = P.x + Math.cos(-Math.PI / 2 + TAU * ringP) * RING_R, ey = P.y + Math.sin(-Math.PI / 2 + TAU * ringP) * RING_R; 5583 g.fillStyle = tint(pal.indigo, 0.95 * ringA); 5584 g.beginPath(); g.arc(ex, ey, 2.4, 0, TAU); g.fill(); 5585 } 5586 } 5587 if (tickA > 0.001) { 5588 g.strokeStyle = tint(pal.indigo, 0.8 * tickA); g.lineWidth = 1.2; 5589 g.beginPath(); 5590 for (i = 0; i < 4; i++) { 5591 var an = i * Math.PI / 2, c = Math.cos(an), s = Math.sin(an); 5592 g.moveTo(P.x + c * (RING_R + 5), P.y + s * (RING_R + 5)); g.lineTo(P.x + c * (RING_R + 11), P.y + s * (RING_R + 11)); 5593 } 5594 g.stroke(); 5595 } 5596 5597 /* ââ anomaly pulses (crimson, thin, expanding) ââ */ 5598 for (i = 0; i < 3; i++) { 5599 var ps = (t - (T_ANOM + i * 0.75)) / 1.7; 5600 if (ps <= 0 || ps >= 1) continue; 5601 var pr = NODE_R + 2 + 40 * cubicOut(ps); 5602 g.strokeStyle = tint(pal.crimson, 0.7 * (1 - ps) * (1 - ps)); g.lineWidth = 1.2; 5603 g.beginPath(); g.arc(P.x, P.y, pr, 0, TAU); g.stroke(); 5604 } 5605 5606 /* ââ nodes ââ */ 5607 for (i = 0; i < nodes.length; i++) { 5608 var n = nodes[i], r = n.hub ? NODE_R + 2 : NODE_R; 5609 var isA = (i === ANOM) ? anom : 0; 5610 if (n.hub) { 5611 g.strokeStyle = rzInk(0.13); g.lineWidth = 1; g.setLineDash([2, 4]); 5612 g.beginPath(); g.arc(n.x, n.y, r + 7, 0, TAU); g.stroke(); 5613 g.setLineDash([]); 5614 } 5615 g.fillStyle = pal.paper; 5616 g.beginPath(); g.arc(n.x, n.y, r, 0, TAU); g.fill(); 5617 g.strokeStyle = rzInk(0.2 * (1 - isA)); g.lineWidth = 1; 5618 g.beginPath(); g.arc(n.x, n.y, r, 0, TAU); g.stroke(); 5619 if (isA > 0.001) { 5620 g.strokeStyle = tint(pal.crimson, 0.85 * isA); g.lineWidth = 1 + 0.5 * isA; 5621 g.beginPath(); g.arc(n.x, n.y, r, 0, TAU); g.stroke(); 5622 g.fillStyle = pal.paper; 5623 g.beginPath(); g.arc(n.x + 16, n.y - 16, 4.5, 0, TAU); g.fill(); 5624 g.fillStyle = tint(pal.crimson, isA * breathe); 5625 g.beginPath(); g.arc(n.x + 16, n.y - 16, 2.6, 0, TAU); g.fill(); 5626 } 5627 g.save(); g.translate(n.x, n.y); 5628 g.strokeStyle = rzInk(0.82); g.fillStyle = rzInk(0.82); g.lineWidth = 1.3; 5629 n.glyph(); 5630 g.restore(); 5631 g.font = '600 9px "Oxanium", sans-serif'; g.fillStyle = rzInk(0.55); 5632 spaced(n.lbl, n.x, n.y + r + 13, 1, 'center'); 5633 } 5634 5635 /* ââ status chip near the pump (paper chip, crimson â indigo mark) ââ */ 5636 if (chipA > 0.001) { 5637 g.font = '8.5px "Courier New", monospace'; 5638 var s1 = 'ANOMALY · NODE 03', s2 = 'ISOLATED · SEG 03'; 5639 var tw = Math.max(spacedWidth(s1, 0.5), spacedWidth(s2, 0.5)); 5640 var cw = tw + 30, ch = 20, cx = 454 - cw, cy = 66 + 6 * (1 - chipA); 5641 g.globalAlpha = chipA; 5642 g.fillStyle = pal.paper; g.strokeStyle = rzInk(0.15); g.lineWidth = 1; 5643 g.beginPath(); tabRect(cx, cy, cw, ch); g.fill(); g.stroke(); 5644 g.fillStyle = tint(pal.crimson, (1 - isoF) * breathe); 5645 g.beginPath(); g.arc(cx + 11, cy + ch / 2, 2.6, 0, TAU); g.fill(); 5646 g.fillStyle = tint(pal.indigo, isoF); 5647 g.beginPath(); g.arc(cx + 11, cy + ch / 2, 2.6, 0, TAU); g.fill(); 5648 g.fillStyle = rzInk(0.72 * (1 - isoF)); spaced(s1, cx + 20, cy + ch / 2 + 0.5, 0.5, 'left'); 5649 g.fillStyle = rzInk(0.72 * isoF); spaced(s2, cx + 20, cy + ch / 2 + 0.5, 0.5, 'left'); 5650 g.globalAlpha = 1; 5651 } 5652 5653 /* ââ vitals chip: the patient trace never wavers ââ */ 5654 var vx = 26, vy = 396, vw = 428, vh = 50; 5655 g.fillStyle = pal.paper; g.strokeStyle = rzInk(0.15); g.lineWidth = 1; 5656 g.beginPath(); tabRect(vx, vy, vw, vh); g.fill(); g.stroke(); 5657 g.strokeStyle = tint(pal.lime, 0.95); g.lineWidth = 2; 5658 g.beginPath(); g.moveTo(vx + 4, vy + 1); g.lineTo(vx + 62, vy + 1); g.stroke(); 5659 g.font = '600 9px "Oxanium", sans-serif'; g.fillStyle = rzInk(0.55); 5660 spaced('VITALS', vx + 16, vy + 17, 1.3, 'left'); 5661 g.font = '9.5px "Courier New", monospace'; g.fillStyle = rzInk(0.82); 5662 spaced('72 BPM SPO2 98%', vx + 16, vy + 34, 0.4, 'left'); 5663 g.strokeStyle = rzInk(0.12); g.lineWidth = 1; 5664 g.beginPath(); g.moveTo(vx + 140.5, vy + 11); g.lineTo(vx + 140.5, vy + vh - 11); g.stroke(); 5665
5666 var sx0 = vx + 156, sx1 = vx + vw - 18, base = vy + 29, amp = 15; 5667 var period = 60 / 72, pxPerBeat = 112; 5668 g.strokeStyle = rzInk(0.08); g.lineWidth = 1; 5669 g.beginPath(); g.moveTo(sx0, base + 0.5); g.lineTo(sx1, base + 0.5); g.stroke(); 5670 var chunks = 10, span = (sx1 - sx0) / chunks, tsec = now / 1000; 5671 g.lineWidth = 1.5; 5672 for (i = 0; i < chunks; i++) { 5673 var ca = 0.28 + 0.72 * ((i + 1) / chunks); 5674 g.strokeStyle = tint(pal.lime, ca); 5675 g.beginPath(); 5676 var xs = sx0 + i * span, xe = xs + span; 5677 for (x = xs; x <= xe + 0.001; x += 1) { 5678 var phs = ((sx1 - x) / pxPerBeat) - tsec / period; 5679 phs = phs - Math.floor(phs); 5680 var yy = base - ecg(1 - phs) * amp; 5681 if (x === xs) g.moveTo(x, yy); else g.lineTo(x, yy); 5682 } 5683 g.stroke(); 5684 } 5685 var lp = (0 - tsec / period); lp = lp - Math.floor(lp); 5686 var ly = base - ecg(1 - lp) * amp; 5687 g.fillStyle = tint(pal.lime, 0.25); 5688 g.beginPath(); g.arc(sx1, ly, 5, 0, TAU); g.fill(); 5689 g.fillStyle = tint(pal.lime, 1); 5690 g.beginPath(); g.arc(sx1, ly, 2.3, 0, TAU); g.fill(); 5691 } 5692 5693 /* ââ scheduling: time-based loop, paused while hidden, one still under reduced motion ââ */ 5694 var reduce = false;
5695 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 5696 if (reduce) { draw(8400, 8.4); return; } 5697 5698 var t0 = performance.now(), raf = 0, running = false; 5699 function tick(now) { 5700 raf = requestAnimationFrame(tick); 5701 draw(now, ((now - t0) / 1000) % LOOP); 5702 } 5703 function start() { if (running) return; running = true; raf = requestAnimationFrame(tick); } 5704 function stop() { if (!running) return; running = false; cancelAnimationFrame(raf); } 5705 document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); }); 5706 if (!document.hidden) start(); 5707 })(); 5708 5709 /* ââ 20. STARTUPS/SAAS â Zero Trust Architecture âââââââââââââ */ 5710 (function () { 5711 /* ââ 20. STARTUPS/SAAS â Zero-trust gates âââââââââââââââââââââ 5712 Three concentric policy rings (IDENTITY · DEVICE · CONTEXT) 5713 around a core. Requests enter from the edge, hold at every 5714 gate for an accent pulse, and are either admitted (lime) or 5715 bounced (crimson). Time-based, eased, 14.4 s loop with a hold. 5716 Blueprint-on-white: ink lines via rzInk(), paper chips via the 5717 --rz-paper token, accents read from CSS variables per frame. */ 5718 var cv = document.getElementById('rz-visual-saas'); 5719 if (!cv) return; 5720 var W = 480, H = 480; 5721 var g = cv.getContext('2d'); 5722 if (!g) return; 5723 rzFitCanvas(cv, g, W, H); /* Hi-DPI backing store + zoom-aware transform */ 5724 5725 /* ââ geometry âââââââââââââââââââââââââââââââââââââââââââââââ */ 5726 var CX = 240, CY = 240; 5727 var CORE_R = 26, ENTRY_R = 232, DIAL_R = 190, ORBIT_R = 38; 5728 var TAU = Math.PI * 2; 5729 var GATES = [ 5730 { label: 'IDENTITY', r: 176, key: 'purple', dash: null }, 5731 { label: 'DEVICE', r: 128, key: 'indigo', dash: [6, 4] }, 5732 { label: 'CONTEXT', r: 80, key: 'yellow', dash: [1.5, 4] } 5733 ]; 5734 5735 /* ââ choreography (seconds) âââââââââââââââââââââââââââââââââ */ 5736 var LOOP = 14.4; 5737 var TRAVEL = [1.1, 0.8, 0.7], HOLD = 0.45, BOUNCE = 1.2, TO_CORE = 0.6, ABSORB = 0.45; 5738 var STAMP_LIFE = 2.4, TRAIL = 0.18, FADE_IN = 0.35; 5739 /* a: entry angle (deg, 0 = right, 90 = down) · s: start · deny: 0 = admitted, n = refused at gate n */ 5740 var REQS = [ 5741 { a: -40, s: 0.0, deny: 0, ms: '2.8' }, 5742 { a: 160, s: 1.6, deny: 1, ms: '' }, 5743 { a: 20, s: 2.8, deny: 0, ms: '3.4' }, 5744 { a: 205, s: 4.3, deny: 0, ms: '3.1' }, 5745 { a: -125, s: 5.7, deny: 3, ms: '' }, 5746 { a: 45, s: 7.2, deny: 0, ms: '2.9' }, 5747 { a: 130, s: 8.7, deny: 0, ms: '3.6' } 5748 ]; 5749 5750 function build(rq) { 5751 var segs = [], t = 0, r = ENTRY_R, k; 5752 rq.holdEnd = []; 5753 for (k = 0; k < GATES.length; k++) { 5754 segs.push({ type: 'travel', t0: t, d: TRAVEL[k], from: r, to: GATES[k].r, gate: k }); t += TRAVEL[k]; 5755 segs.push({ type: 'hold', t0: t, d: HOLD, at: GATES[k].r, gate: k }); t += HOLD; 5756 rq.holdEnd[k] = t; 5757 r = GATES[k].r; 5758 if (rq.deny === k + 1) { 5759 rq.resolve = t; 5760 segs.push({ type: 'bounce', t0: t, d: BOUNCE, from: r, to: r + 64, gate: k }); t += BOUNCE; 5761 break; 5762 } 5763 } 5764 if (!rq.deny) { 5765 segs.push({ type: 'travel', t0: t, d: TO_CORE, from: r, to: CORE_R + 4, gate: 3 }); t += TO_CORE; 5766 rq.resolve = t; 5767 segs.push({ type: 'absorb', t0: t, d: ABSORB, at: CORE_R + 4 }); t += ABSORB; 5768 } 5769 rq.segs = segs; rq.dur = t; 5770 var rad = rq.a * Math.PI / 180; 5771 rq.rad = rad; rq.cos = Math.cos(rad); rq.sin = Math.sin(rad); 5772 } 5773 for (var bi = 0; bi < REQS.length; bi++) build(REQS[bi]); 5774 5775 /* ââ easing âââââââââââââââââââââââââââââââââââââââââââââââââ */ 5776 function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; } 5777 function easeQ(p) { return p < 0.5 ? 16 * p * p * p * p * p : 1 - Math.pow(-2 * p + 2, 5) / 2; } 5778 function easeOutC(p) { return 1 - Math.pow(1 - p, 3); } 5779 5780 /* state of one request at local time lt (null when not on stage) */ 5781 function stateAt(rq, lt) { 5782 if (lt < 0) lt = 0; 5783 if (lt >= rq.dur) return null; 5784 var segs = rq.segs, i, s, p, st; 5785 for (i = 0; i < segs.length; i++) { 5786 s = segs[i]; 5787 if (lt < s.t0 + s.d || i === segs.length - 1) { 5788 p = clamp01((lt - s.t0) / s.d);
5789 st = { seg: s, p: p, r: s.at || 0, alpha: 1, tone: 'ink' }; 5790 if (s.type === 'travel') { st.r = s.from + (s.to - s.from) * easeQ(p); if (s.gate === 3) st.tone = 'allow'; } 5791 else if (s.type === 'bounce') { st.r = s.from + (s.to - s.from) * easeOutC(p); st.tone = 'deny'; st.alpha = p < 0.45 ? 1 : 1 - (p - 0.45) / 0.55; } 5792 else if (s.type === 'absorb') { st.r = s.at - 8 * p; st.tone = 'allow'; st.alpha = 1 - p; } 5793 if (lt < FADE_IN) st.alpha *= lt / FADE_IN; 5794 return st; 5795 } 5796 } 5797 return null; 5798 } 5799 5800 /* ââ drawing helpers ââââââââââââââââââââââââââââââââââââââââ */ 5801 function trackW(text, sp) { 5802 var w = 0, n = text.length, i; 5803 for (i = 0; i < n; i++) w += g.measureText(text.charAt(i)).width; 5804 return w + sp * (n - 1); 5805 } 5806 function track(text, x, y, sp, align) { 5807 var w = trackW(text, sp), n = text.length, i, cx = x, ch; 5808 if (align === 'center') cx = x - w / 2; else if (align === 'right') cx = x - w; 5809 g.textAlign = 'left'; 5810 for (i = 0; i < n; i++) { ch = text.charAt(i); g.fillText(ch, cx, y); cx += g.measureText(ch).width + sp; } 5811 return w; 5812 } 5813 function rr(x, y, w, h, r) { 5814 g.beginPath(); 5815 g.moveTo(x + r, y); g.lineTo(x + w - r, y); g.arcTo(x + w, y, x + w, y + r, r); 5816 g.lineTo(x + w, y + h - r); g.arcTo(x + w, y + h, x + w - r, y + h, r); 5817 g.lineTo(x + r, y + h); g.arcTo(x, y + h, x, y + h - r, r); 5818 g.lineTo(x, y + r); g.arcTo(x, y, x + r, y, r); g.closePath(); 5819 } 5820 function strokeA(col, a, lw) { g.strokeStyle = col; g.lineWidth = lw; g.globalAlpha = clamp01(a); g.stroke(); g.globalAlpha = 1; } 5821 function fillA(col, a) { g.fillStyle = col; g.globalAlpha = clamp01(a); g.fill(); g.globalAlpha = 1; } 5822 function circle(x, y, r) { g.beginPath(); g.arc(x, y, r, 0, TAU); } 5823 5824 /* ââ one composed frame at loop time t (seconds) ââââââââââââ */ 5825 function draw(t) { 5826 var root = document.documentElement, cs = null; 5827 try { cs = getComputedStyle(root); } catch (e) {} 5828 var dark = root.getAttribute('data-theme') === 'dark'; 5829 function tok(name, fb) { 5830 var v = ''; 5831 try { v = cs ? cs.getPropertyValue(name) : ''; } catch (e) {} 5832 v = (v || '').replace(/^\s+|\s+$/g, ''); 5833 return v || fb; 5834 } 5835 var AC = { 5836 purple: tok('--rz-purple', dark ? '#9a45ee' : '#7d14d7'), 5837 indigo: tok('--rz-indigo', dark ? '#7a56ff' : '#5801f5'), 5838 crimson: tok('--rz-crimson', dark ? '#ff3355' : '#f50034'), 5839 lime: tok('--rz-lime', dark ? '#8ef513' : '#85f403'), 5840 yellow: tok('--rz-yellow', dark ? '#f0ee2a' : '#e6e414') 5841 }; 5842 var PAPER = tok('--rz-paper', dark ? '#0e0f14' : '#ffffff'); 5843 var SWATCH = [AC.purple, AC.lime, AC.crimson, AC.indigo, AC.yellow]; 5844 function toneCol(tone) { return tone === 'allow' ? AC.lime : tone === 'deny' ? AC.crimson : rzInk(0.8); } 5845 5846 var i, k, rq, lt, st, px, py, x, y, a, len, u, col, gate, off, rt; 5847 5848 g.clearRect(0, 0, W, H); 5849 g.setLineDash([]); 5850 g.lineCap = 'butt'; 5851 g.textBaseline = 'alphabetic'; 5852 5853 /* corner crop-brackets (plate motif) */ 5854 g.strokeStyle = rzInk(0.28); g.lineWidth = 1.5; 5855 var ins = 8, bl = 12; 5856 g.beginPath(); 5857 g.moveTo(ins, ins + bl); g.lineTo(ins, ins); g.lineTo(ins + bl, ins); 5858 g.moveTo(W - ins - bl, ins); g.lineTo(W - ins, ins); g.lineTo(W - ins, ins + bl); 5859 g.moveTo(ins, H - ins - bl); g.lineTo(ins, H - ins); g.lineTo(ins + bl, H - ins); 5860 g.moveTo(W - ins - bl, H - ins); g.lineTo(W - ins, H - ins); g.lineTo(W - ins, H - ins - bl); 5861 g.stroke(); 5862 5863 /* perimeter dial: ruler ticks every 6°, long every 30° */ 5864 g.lineWidth = 1; 5865 for (i = 0; i < 60; i++) { 5866 a = i * 6 * Math.PI / 180; 5867 len = (i % 5 === 0) ? 8 : 4; 5868 g.beginPath(); 5869 g.moveTo(CX + Math.cos(a) * DIAL_R, CY + Math.sin(a) * DIAL_R); 5870 g.lineTo(CX + Math.cos(a) * (DIAL_R + len), CY + Math.sin(a) * (DIAL_R + len)); 5871 g.strokeStyle = rzInk(i % 5 === 0 ? 0.34 : 0.18); 5872 g.stroke(); 5873 } 5874 5875 /* policy rings: solid / dashed / dotted â line types, not colours */ 5876 for (k = 0; k < GATES.length; k++) { 5877 gate = GATES[k]; 5878 g.setLineDash(gate.dash || []); 5879 circle(CX, CY, gate.r); 5880 strokeA(rzInk(0.28), 1, 1); 5881 } 5882 g.setLineDash([]); 5883 5884 /* slow orbit around the core (continuous verification) */ 5885 g.save(); 5886 g.translate(CX, CY); g.rotate(t * TAU / 48); 5887 g.setLineDash([1.5, 5.5]); 5888 circle(0, 0, ORBIT_R); 5889 strokeA(rzInk(0.32), 1, 1);
5890 g.restore(); 5891 g.setLineDash([]); 5892 5893 /* ring labels: paper knockout on the ring, accent square, ink text */ 5894 g.font = "600 10px 'Oxanium', sans-serif"; 5895 g.textBaseline = 'middle'; 5896 for (k = 0; k < GATES.length; k++) { 5897 gate = GATES[k]; 5898 y = CY - gate.r; 5899 var tw = trackW(gate.label, 1.4), sq = 5, gap = 7, total = sq + gap + tw, x0 = CX - total / 2; 5900 g.fillStyle = PAPER; g.fillRect(x0 - 9, y - 8, total + 18, 16); 5901 g.fillStyle = AC[gate.key]; g.fillRect(x0, y - sq / 2, sq, sq); 5902 g.fillStyle = rzInk(0.78); 5903 track(gate.label, x0 + sq + gap, y + 0.5, 1.4, 'left'); 5904 } 5905 g.textBaseline = 'alphabetic'; 5906 5907 /* verdict stamps left on the rings (also from the previous cycle, so the loop seam is clean) */ 5908 for (i = 0; i < REQS.length; i++) { 5909 rq = REQS[i]; 5910 for (off = 0; off <= 1; off++) { 5911 lt = t - rq.s + off * LOOP; 5912 for (k = 0; k < rq.holdEnd.length; k++) { 5913 u = (lt - rq.holdEnd[k]) / STAMP_LIFE; 5914 if (u < 0 || u >= 1) continue; 5915 px = CX + rq.cos * GATES[k].r; py = CY + rq.sin * GATES[k].r; 5916 if (rq.deny === k + 1) { 5917 /* closed gate: crimson bar across the request path */ 5918 g.beginPath(); 5919 g.moveTo(px - rq.sin * 7, py + rq.cos * 7); g.lineTo(px + rq.sin * 7, py - rq.cos * 7); 5920 g.lineCap = 'round'; strokeA(AC.crimson, (1 - u) * 0.95, 2); g.lineCap = 'butt'; 5921 } else { 5922 circle(px, py, 2.4); fillA(AC[GATES[k].key], (1 - u) * 0.95); 5923 } 5924 } 5925 } 5926 } 5927 5928 /* live requests: gate pulses, core admission, trails, dots */ 5929 var coreFlash = 0; 5930 for (i = 0; i < REQS.length; i++) { 5931 rq = REQS[i]; 5932 lt = t - rq.s; 5933 st = stateAt(rq, lt); 5934 if (!st) continue; 5935 px = CX + rq.cos * st.r; py = CY + rq.sin * st.r; 5936 5937 if (st.seg.type === 'hold') { 5938 gate = GATES[st.seg.gate]; col = AC[gate.key]; 5939 /* accent arc lights on the ring around the crossing */ 5940 g.beginPath(); g.arc(CX, CY, gate.r, rq.rad - 0.16, rq.rad + 0.16); 5941 g.lineCap = 'round'; strokeA(col, Math.sin(Math.PI * st.p) * 0.95, 2); g.lineCap = 'butt'; 5942 /* tiny expanding halo */ 5943 circle(px, py, 4 + 12 * easeOutC(st.p)); strokeA(col, (1 - st.p) * 0.55, 1); 5944 } 5945 if (st.seg.type === 'absorb') { 5946 circle(CX, CY, CORE_R + 18 * easeOutC(st.p)); strokeA(AC.lime, (1 - st.p) * 0.8, 1.5); 5947 if (1 - st.p > coreFlash) coreFlash = 1 - st.p; 5948 } 5949 5950 /* short trail behind the dot */ 5951 var st2 = stateAt(rq, lt - TRAIL); 5952 if (st2 && Math.abs(st2.r - st.r) > 0.5) { 5953 g.beginPath(); 5954 g.moveTo(CX + rq.cos * st2.r, CY + rq.sin * st2.r); g.lineTo(px, py); 5955 g.lineCap = 'round'; strokeA(toneCol(st.tone), st.alpha * 0.4, 1.4); g.lineCap = 'butt'; 5956 } 5957 /* the request token: paper node, toned stroke */ 5958 circle(px, py, 3.8); 5959 fillA(PAPER, st.alpha); 5960 strokeA(toneCol(st.tone), st.alpha, 1.4); 5961 } 5962 5963 /* core */ 5964 circle(CX, CY, CORE_R); g.fillStyle = PAPER; g.fill(); strokeA(rzInk(0.6), 1, 1.2); 5965 circle(CX, CY - 5, 3.2); g.fillStyle = rzInk(0.85); g.fill(); 5966 if (coreFlash > 0) { circle(CX, CY - 5, 3.2); fillA(AC.lime, coreFlash); } 5967 g.font = "700 8px 'Oxanium', sans-serif"; g.textBaseline = 'middle'; g.fillStyle = rzInk(0.72); 5968 track('CORE', CX, CY + 10, 1.0, 'center'); 5969 g.textBaseline = 'alphabetic'; 5970 5971 /* header: eyebrow · five swatches · motto */ 5972 g.font = "600 10px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.62); 5973 var ew = track('ZERO TRUST', 26, 24, 1.4, 'left'); 5974 for (i = 0; i < 5; i++) { g.fillStyle = SWATCH[i]; g.fillRect(26 + ew + 12 + i * 8, 16, 5, 5); } 5975 g.font = "8px 'Courier New', Courier, monospace"; g.fillStyle = rzInk(0.42); 5976 track('NEVER TRUST · ALWAYS VERIFY', 26, 38, 0.6, 'left'); 5977 5978 /* policy chip (paper, 1px ink border) */ 5979 g.font = "600 9px 'Oxanium', sans-serif"; 5980 var ct = 'POLICY · 3 CHECKS', cw = trackW(ct, 1.2) + 22, ch = 20, cx0 = W - 26 - cw, cy0 = 14; 5981 rr(cx0, cy0, cw, ch, 4); g.fillStyle = PAPER; g.fill(); strokeA(rzInk(0.15), 1, 1); 5982 g.textBaseline = 'middle'; g.fillStyle = rzInk(0.72); 5983 track(ct, cx0 + 11, cy0 + ch / 2 + 0.5, 1.2, 'left'); 5984 g.textBaseline = 'alphabetic'; 5985 5986 /* footer: ruler + last verdict readout */ 5987 var ry = 444, rx0 = 26, rx1 = W - 26; 5988 g.beginPath(); g.moveTo(rx0, ry + 0.5); g.lineTo(rx1, ry + 0.5); strokeA(rzInk(0.18), 1, 1); 5989 g.beginPath(); 5990 for (x = rx0; x <= rx1; x += 8) { g.moveTo(x + 0.5, ry); g.lineTo(x + 0.5, ry - (((x - rx0) % 40 === 0) ? 6 : 3)); } 5991 strokeA(rzInk(0.28), 1, 1); 5992 5993 var last = null, lastT = -1; 5994 for (i = 0; i < REQS.length; i++) { rt = REQS[i].s + REQS[i].resolve; if (t >= rt && rt > lastT) { lastT = rt; last = REQS[i]; } }
5995 var vAlpha = 1; 5996 if (!last) last = REQS[REQS.length - 1]; else vAlpha = clamp01((t - lastT) / 0.4); 5997 g.font = "600 8px 'Oxanium', sans-serif"; g.fillStyle = rzInk(0.5); 5998 var lw = track('LAST VERDICT', rx0, 463, 1.2, 'left'); 5999 var vcol = last.deny ? AC.crimson : AC.lime; 6000 g.fillStyle = vcol; g.globalAlpha = vAlpha; g.fillRect(rx0 + lw + 10, 456, 5, 5); g.globalAlpha = 1; 6001 g.font = "9px 'Courier New', Courier, monospace"; g.fillStyle = rzInk(0.72 * vAlpha); 6002 track(last.deny ? 'DENY · ' + GATES[last.deny - 1].label : 'ALLOW · ' + last.ms + ' ms', rx0 + lw + 21, 463, 0.6, 'left'); 6003 g.font = "8px 'Courier New', Courier, monospace"; g.fillStyle = rzInk(0.45); 6004 track('TTL 300 s', rx1, 463, 0.6, 'right'); 6005 } 6006 6007 /* ââ scheduling âââââââââââââââââââââââââââââââââââââââââââââ */ 6008 var reduce = false; 6009 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 6010 if (reduce) { 6011 /* one composed still: an admission at the core, a refusal bouncing, a token mid-gate */ 6012 var still = function () { draw(4.75); }; 6013 still(); 6014 window.addEventListener('resize', still); 6015 window.addEventListener('load', still); 6016 document.addEventListener('rz-theme-change', still); 6017 document.addEventListener('rz-why-variant', still); 6018 if (document.fonts && document.fonts.ready && document.fonts.ready.then) document.fonts.ready.then(still, function () {}); 6019 return; 6020 } 6021 6022 function now() { return (window.performance && performance.now) ? performance.now() : Date.now(); } 6023 var t0 = now(), raf = 0, running = false; 6024 function frame() { 6025 raf = 0; 6026 if (!running) return; 6027 draw(((now() - t0) / 1000) % LOOP); 6028 raf = requestAnimationFrame(frame); 6029 } 6030 function start() { if (running) return; running = true; if (!raf) raf = requestAnimationFrame(frame); } 6031 function stop() { running = false; if (raf) { cancelAnimationFrame(raf); raf = 0; } } 6032 document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); }); 6033 if (!document.hidden) start(); 6034 })(); 6035 6036 /* ââ 21. CONSULTANCY BAND â Live SOC Operations Terminal âââââ */ 6037 (function () { 6038 var cv = document.getElementById('rz-consult-canvas'); 6039 if (!cv) return; 6040 var W = 360, H = 520; 6041 var g = cv.getContext('2d'); 6042 rzFitCanvas(cv, g, W, H); 6043 6044 var alerts = [ 6045 { time:'04:12', type:'RANSOMWARE PRE-EXEC', status:'NEUTRALIZED', sev:2 }, 6046 { time:'03:58', type:'C2 BEACON', status:'BLOCKED', sev:2 }, 6047 { time:'03:44', type:'LATERAL MOVEMENT', status:'CONTAINED', sev:2 }, 6048 { time:'03:31', type:'DATA EXFILTRATION', status:'STOPPED', sev:2 }, 6049 { time:'03:17', type:'BRUTEFORCE SSH', status:'CONTAINED', sev:1 }, 6050 { time:'03:02', type:'SQL INJECTION', status:'PATCHED', sev:1 }, 6051 { time:'02:49', type:'PORT SCAN', status:'DISMISSED', sev:0 },
6052 { time:'02:33', type:'PHISHING EMAIL', status:'QUARANTINED', sev:1 }, 6053 { time:'02:18', type:'PRIV ESCALATION', status:'BLOCKED', sev:2 }, 6054 { time:'02:05', type:'RECON ACTIVE', status:'MONITORING', sev:0 }, 6055 ]; 6056 6057 var metrics = [ 6058 { lbl:'MEAN TIME TO RESPOND', val:14, unit:'MIN', max:60, col:'rgba(74,222,128,0.85)' }, 6059 { lbl:'CONTAINMENT RATE', val:100, unit:'%', max:100, col:'rgba(74,222,128,0.85)' }, 6060 { lbl:'FALSE POSITIVE RATE', val:2, unit:'%', max:20, col:'rgba(74,222,128,0.85)' }, 6061 ]; 6062 6063 var tick = 0; 6064 var visibleAlerts = 0; 6065 var alertTimer = 0; 6066 var scrollOff = 0; 6067 var blinkOn = true; 6068 6069 // Uptime clock start 6070 var startTime = Date.now(); 6071 6072 function pad(n) { return n < 10 ? '0' + n : '' + n; } 6073 6074 function drawBar(bx, by, bw, bh, pct, col) { 6075 // bg track 6076 g.fillStyle = 'rgba(40,15,15,0.8)'; 6077 g.beginPath(); g.roundRect(bx, by, bw, bh, 3); g.fill(); 6078 // fill 6079 g.fillStyle = col; 6080 g.beginPath(); g.roundRect(bx, by, bw * pct, bh, 3); g.fill(); 6081 // shimmer 6082 g.fillStyle = 'rgba(255,255,255,0.06)'; 6083 g.beginPath(); g.roundRect(bx, by, bw * pct, bh * 0.4, 2); g.fill(); 6084 } 6085 6086 function frame() { 6087 tick++; 6088 g.clearRect(0, 0, W, H); 6089
6090 // ââ Background ââââââââââââââââââââââââââââââââââââââââââââââ 6091 g.fillStyle = 'rgba(16,15,20,0.97)'; 6092 g.beginPath(); g.roundRect(0, 0, W, H, 18); g.fill(); 6093 6094 // subtle scanlines 6095 for (var sy = 0; sy < H; sy += 4) { 6096 g.fillStyle = 'rgba(0,0,0,0.08)'; 6097 g.fillRect(0, sy, W, 1); 6098 } 6099 6100 // subtle red vignette 6101 var vig = g.createRadialGradient(W/2, H/2, H*0.3, W/2, H/2, H*0.8); 6102 vig.addColorStop(0, 'rgba(0,0,0,0)'); 6103 vig.addColorStop(1, 'rgba(125,20,215,0.07)'); 6104 g.fillStyle = vig; 6105 g.beginPath(); g.roundRect(0, 0, W, H, 18); g.fill(); 6106 6107 // ââ Header bar ââââââââââââââââââââââââââââââââââââââââââââââ 6108 g.fillStyle = 'rgba(125,20,215,0.14)'; 6109 g.beginPath(); g.roundRect(0, 0, W, 52, [18, 18, 0, 0]); g.fill(); 6110 g.strokeStyle = 'rgba(125,20,215,0.35)'; 6111 g.lineWidth = 0.8; 6112 g.beginPath(); g.moveTo(0, 52); g.lineTo(W, 52); g.stroke(); 6113 6114 // logo + title 6115 g.textAlign = 'left'; 6116 g.font = 'bold 11px "Courier New", monospace'; 6117 g.fillStyle = '#f4f2fb'; 6118 g.fillText('REACTIVEZERO', 16, 22); 6119 g.font = '7.5px "Courier New", monospace'; 6120 g.fillStyle = 'rgba(125,20,215,0.7)'; 6121 g.fillText('SOC OPERATIONS CENTER', 16, 36); 6122 6123 // live indicator 6124 var livePulse = 0.55 + 0.45 * Math.sin(tick / 18); 6125 g.beginPath(); g.arc(W - 18, 20, 5, 0, Math.PI * 2); 6126 g.fillStyle = 'rgba(74,222,128,' + livePulse + ')'; g.fill(); 6127 g.textAlign = 'right'; 6128 g.font = 'bold 7.5px "Courier New", monospace'; 6129 g.fillStyle = 'rgba(74,222,128,0.8)'; 6130 g.fillText('LIVE', W - 26, 23); 6131 6132 // uptime clock 6133 var elapsed = Math.floor((Date.now() - startTime) / 1000); 6134 var hh = Math.floor(elapsed / 3600), mm = Math.floor((elapsed % 3600) / 60), ss = elapsed % 60; 6135 g.font = '7px "Courier New", monospace'; 6136 g.fillStyle = 'rgba(125,20,215,0.5)'; 6137 g.fillText('UP ' + pad(hh) + ':' + pad(mm) + ':' + pad(ss), W - 14, 38); 6138 6139 // ââ Incident counter ââââââââââââââââââââââââââââââââââââââââ 6140 var IY = 68; 6141 g.strokeStyle = 'rgba(125,20,215,0.15)'; 6142 g.lineWidth = 0.7; 6143 g.strokeRect(10, IY, W - 20, 52); 6144 g.fillStyle = 'rgba(125,20,215,0.07)'; 6145 g.fillRect(10, IY, W - 20, 52); 6146 6147 g.textAlign = 'center'; 6148 g.font = 'bold 32px "Courier New", monospace'; 6149 g.fillStyle = 'rgba(74,222,128,0.9)'; 6150 g.fillText('47', W / 2, IY + 36); 6151 g.font = '7px "Courier New", monospace'; 6152 g.fillStyle = 'rgba(125,20,215,0.55)'; 6153 g.fillText('INCIDENTS HANDLED TODAY · 0 UNRESOLVED', W / 2, IY + 48); 6154 6155 // ââ Metrics âââââââââââââââââââââââââââââââââââââââââââââââââ 6156 var MY = 138; 6157 g.textAlign = 'left'; 6158 g.font = 'bold 7px "Courier New", monospace'; 6159 g.fillStyle = 'rgba(125,20,215,0.5)'; 6160 g.fillText('âââ PERFORMANCE METRICS', 14, MY); 6161 6162 metrics.forEach(function (m, i) { 6163 var my = MY + 18 + i * 38; 6164 g.font = '7.5px "Courier New", monospace'; 6165 g.fillStyle = 'rgba(180,155,155,0.75)'; 6166 g.textAlign = 'left'; 6167 g.fillText(m.lbl, 14, my); 6168 g.textAlign = 'right'; 6169 g.font = 'bold 9px "Courier New", monospace'; 6170 g.fillStyle = m.col; 6171 g.fillText(m.val + m.unit, W - 14, my); 6172 drawBar(14, my + 4, W - 28, 7, m.val / m.max, m.col); 6173 }); 6174 6175 // ââ Divider âââââââââââââââââââââââââââââââââââââââââââââââââ 6176 var DY = MY + 18 + metrics.length * 38 + 8; 6177 g.strokeStyle = 'rgba(125,20,215,0.2)'; 6178 g.lineWidth = 0.7; 6179 g.setLineDash([4, 4]); 6180 g.beginPath(); g.moveTo(14, DY); g.lineTo(W - 14, DY); g.stroke(); 6181 g.setLineDash([]); 6182 6183 // ââ Alert stream ââââââââââââââââââââââââââââââââââââââââââââ 6184 var ALY = DY + 14; 6185 g.textAlign = 'left'; 6186 g.font = 'bold 7px "Courier New", monospace'; 6187 g.fillStyle = 'rgba(125,20,215,0.5)'; 6188 g.fillText('âââ LIVE ALERT STREAM', 14, ALY); 6189 6190 // reveal one more alert every ~55 frames, cycle after all shown 6191 alertTimer++; 6192 if (alertTimer > 55) { 6193 alertTimer = 0; 6194 visibleAlerts = (visibleAlerts + 1) % (alerts.length + 1); 6195 } 6196 6197 var maxRows = 6; 6198 var shown = Math.min(visibleAlerts, alerts.length); 6199 var startIdx = Math.max(0, shown - maxRows); 6200 6201 for (var ai = startIdx; ai < shown; ai++) { 6202 var al = alerts[ai]; 6203 var row = ai - startIdx; 6204 var ay = ALY + 16 + row * 30; 6205 var isNew = (ai === shown - 1) && alertTimer < 20; 6206 6207 // row bg 6208 g.fillStyle = isNew ? 'rgba(125,20,215,0.12)' : 'rgba(20,8,8,0.5)'; 6209 g.beginPath(); g.roundRect(12, ay - 12, W - 24, 24, 4); g.fill(); 6210 if (isNew) { 6211 g.strokeStyle = 'rgba(125,20,215,0.5)'; g.lineWidth = 0.8; g.stroke(); 6212 } 6213 6214 // severity dot 6215 var dotCol = al.sev === 2 ? '#c56bff' : al.sev === 1 ? '#ffaa44' : 'rgba(150,130,130,0.6)'; 6216 g.beginPath(); g.arc(22, ay - 1, 4, 0, Math.PI * 2); 6217 g.fillStyle = dotCol; g.fill(); 6218 6219 // time 6220 g.textAlign = 'left'; 6221 g.font = '7.5px "Courier New", monospace'; 6222 g.fillStyle = 'rgba(130,115,160,0.6)'; 6223 g.fillText(al.time, 30, ay + 2); 6224 6225 // alert type 6226 g.font = 'bold 8px "Courier New", monospace'; 6227 g.fillStyle = isNew ? '#f4f2fb' : 'rgba(200,170,170,0.75)'; 6228 g.fillText(al.type, 68, ay + 2); 6229 6230 // status badge 6231 var statCol = al.sev === 2 ? 'rgba(74,222,128,0.85)' : al.sev === 1 ? 'rgba(74,222,128,0.7)' : 'rgba(130,120,120,0.6)'; 6232 g.textAlign = 'right'; 6233 g.font = 'bold 6.5px "Courier New", monospace'; 6234 g.fillStyle = statCol; 6235 g.fillText('â ' + al.status, W - 14, ay + 2); 6236 } 6237 6238 // blinking cursor on last row 6239 if (shown < alerts.length) { 6240 var curY = ALY + 16 + Math.min(shown, maxRows) * 30; 6241 if (tick % 40 < 25) { 6242 g.fillStyle = 'rgba(125,20,215,0.7)'; 6243 g.fillRect(14, curY - 11, 6, 14); 6244 } 6245 } 6246 6247 // ââ Footer ââââââââââââââââââââââââââââââââââââââââââââââââââ 6248 g.strokeStyle = 'rgba(125,20,215,0.15)'; 6249 g.lineWidth = 0.7; 6250 g.beginPath(); g.moveTo(0, H - 32); g.lineTo(W, H - 32); g.stroke(); 6251 6252 g.textAlign = 'center'; 6253 g.font = '7px "Courier New", monospace'; 6254 g.fillStyle = 'rgba(125,20,215,0.4)'; 6255 g.fillText('OSCP · CEH · CHFI · ISO27001', W / 2, H - 17); 6256 g.fillStyle = 'rgba(130,115,160,0.35)'; 6257 g.fillText('REACTIVEZERO · ROTTERDAM, THE NETHERLANDS', W / 2, H - 6); 6258 6259 requestAnimationFrame(frame); 6260 } 6261 6262 frame(); 6263 })(); 6264 6265 /* ââ 5. GLITCH: sync data-text with i18n âââââââââââââââââââââ */
6266 var heroH1 = document.querySelector('.section_header15 h1.heading-style-h1'); 6267 if (heroH1) { 6268 heroH1.setAttribute('data-text', heroH1.textContent.trim()); 6269 new MutationObserver(function () { 6270 heroH1.setAttribute('data-text', heroH1.textContent.trim()); 6271 }).observe(heroH1, { childList: true, subtree: true, characterData: true }); 6272 } 6273 6274 /* ââ 6. NAV DROPDOWNS: keep open while moving to the items ââââââ 6275 Webflow's hover dropdown closed the menu the instant the cursor left the 6276 narrow toggle â including the empty "dead zone" beside it â so items were 6277 unreachable. We take over hover control on desktop with a short close 6278 delay: leaving schedules a close, re-entering (e.g. landing on the list) 6279 cancels it. The list visibility is driven by the `rz-open` class (see 6280 redesign.css). On tablet/mobile (<992px) the menu stays click-toggled. */ 6281 (function () { 6282 var DESKTOP = '(min-width: 992px)'; 6283 var CLOSE_DELAY = 250; 6284 6285 Array.prototype.forEach.call( 6286 document.querySelectorAll('.navbar2_menu-dropdown'), 6287 function (dd) { 6288 var list = dd.querySelector('.navbar2_dropdown-list'); 6289 var toggle = dd.querySelector('.navbar2_dropdwn-toggle'); 6290 if (!list || !toggle) return; 6291 6292 var timer = null; 6293 6294 function isDesktop() { return window.matchMedia(DESKTOP).matches; } 6295 6296 function open() { 6297 if (!isDesktop()) return; 6298 clearTimeout(timer); 6299 dd.classList.add('rz-open'); 6300 list.classList.add('w--open'); 6301 toggle.classList.add('w--open'); 6302 toggle.setAttribute('aria-expanded', 'true'); 6303 } 6304 6305 function close() { 6306 dd.classList.remove('rz-open'); 6307 list.classList.remove('w--open'); 6308 toggle.classList.remove('w--open'); 6309 toggle.setAttribute('aria-expanded', 'false'); 6310 } 6311 6312 function scheduleClose() { 6313 if (!isDesktop()) return; 6314 clearTimeout(timer); 6315 timer = setTimeout(close, CLOSE_DELAY); 6316 } 6317 6318 dd.addEventListener('mouseenter', open); 6319 dd.addEventListener('mouseleave', scheduleClose); 6320 list.addEventListener('mouseenter', open); 6321 // Close immediately once a destination is chosen. 6322 list.addEventListener('click', function () { clearTimeout(timer); close(); }); 6323 } 6324 ); 6325 })(); 6326 6327})(); 6328 6329/* ==== RZ-VARIANT:creds START ==== */ 6330/* why-us "creds" visual: arm the CSS stagger and fire it on first view. 6331 No canvas, no globals beyond the init guard; static without JS. */ 6332(function () { 6333 'use strict'; 6334 if (window.__rzVCredsInit) return; 6335 window.__rzVCredsInit = true; 6336 6337 function addClass(el, name) { 6338 if (el.classList) el.classList.add(name); 6339 else if ((' ' + el.className + ' ').indexOf(' ' + name + ' ') < 0) el.className += ' ' + name; 6340 } 6341 6342 function init() { 6343 var root = document.querySelector('.rz-v-creds[data-variant="creds"]'); 6344 if (!root) return; 6345 6346 var reduce = false;
6347 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 6348 6349 if (reduce || !('IntersectionObserver' in window)) { 6350 addClass(root, 'rz-v-creds--in'); 6351 return; 6352 } 6353 6354 addClass(root, 'rz-v-creds--armed'); 6355 var io = new IntersectionObserver(function (entries) { 6356 for (var i = 0; i < entries.length; i++) { 6357 if (entries[i].isIntersecting) { 6358 addClass(root, 'rz-v-creds--in'); 6359 io.disconnect(); 6360 return; 6361 } 6362 } 6363 }, { threshold: 0.15 }); 6364 io.observe(root); 6365 } 6366 6367 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); 6368 else init(); 6369})(); 6370/* ==== RZ-VARIANT:creds END ==== */ 6371 6372 6373 6374/* ==== RZ-METHOD:pentest START ==== */ 6375/* ==== RZ-METHOD-inner:pentest â engagement track ==== */ 6376(function () { 6377 'use strict'; 6378 var root = document.querySelector('.rz-m-pentest[data-method="pentest"]'); 6379 if (!root) return; 6380 6381 var segs = [].slice.call(root.querySelectorAll('.rz-m-pentest-seg')); 6382 if (!segs.length) return; 6383 var btns = [], panels = []; 6384 for (var i = 0; i < segs.length; i++) { 6385 btns.push(segs[i].querySelector('.rz-m-pentest-seg-btn')); 6386 panels.push(segs[i].querySelector('.rz-m-pentest-panel')); 6387 if (!btns[i] || !panels[i]) return; 6388 } 6389 var grid = root.querySelector('.rz-m-pentest-segments'); 6390 var caret = root.querySelector('.rz-m-pentest-caret'); 6391 6392 var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; 6393 var reduce = !!(mq && mq.matches); 6394 var active = -1, touched = false, paused = false, inView = false; 6395 var hoverTimer = null, autoTimer = null, rafId = null; 6396 var STEP_MS = 4200, HOVER_MS = 110; 6397 6398 function addClass(el, c) { if (el.className.indexOf(c) < 0) el.className += (el.className ? ' ' : '') + c; } 6399 function removeClass(el, c) { el.className = el.className.replace(new RegExp('(^|\\s)' + c + '(?!\\S)', 'g'), ' ').replace(/\s+/g, ' ').replace(/^\s|\s$/g, ''); } 6400 6401 /* layout mode is decided by CSS (breakpoint lives there, not here) */ 6402 function mode() { 6403 var v = ''; 6404 try { v = window.getComputedStyle(root).getPropertyValue('--rz-m-pentest-mode') || ''; } catch (e) {} 6405 return v.replace(/\s/g, '') === 'track' ? 'track' : 'stack'; 6406 } 6407 6408 function placeCaret() { 6409 if (!caret) return; 6410 if (mode() !== 'track' || active < 0) { caret.hidden = true; return; } 6411 var b = btns[active]; 6412 var x = b.offsetLeft + b.offsetWidth / 2; 6413 var w = grid.clientWidth || 1; 6414 caret.hidden = false; 6415 caret.style.transform = 'translateX(' + Math.round(x) + 'px)'; 6416 if (x > w * 0.72) addClass(caret, 'is-flip'); else removeClass(caret, 'is-flip'); 6417 } 6418 6419 function setActive(idx) { 6420 active = idx; 6421 for (var k = 0; k < segs.length; k++) { 6422 var on = k === idx; 6423 if (on) addClass(segs[k], 'is-active'); else removeClass(segs[k], 'is-active'); 6424 btns[k].setAttribute('aria-expanded', on ? 'true' : 'false'); 6425 panels[k].hidden = !on; 6426 } 6427 placeCaret(); 6428 } 6429 6430 /* ââ auto-advance: the caret walks the track until the reader takes over ââ */ 6431 function stopAuto() { if (autoTimer) { clearTimeout(autoTimer); autoTimer = null; } } 6432 function scheduleAuto() { 6433 stopAuto(); 6434 if (reduce || touched || paused || !inView || mode() !== 'track') return; 6435 autoTimer = setTimeout(function () { 6436 autoTimer = null; 6437 if (document.hidden) { scheduleAuto(); return; } 6438 if (mode() === 'track' && !touched && !paused && inView) setActive((active + 1) % segs.length); 6439 scheduleAuto(); 6440 }, STEP_MS); 6441 } 6442 function userTookOver() { touched = true; stopAuto(); } 6443 6444 /* ââ wiring ââ */ 6445 function bind(i) { 6446 var b = btns[i]; 6447 b.addEventListener('click', function () { 6448 userTookOver(); 6449 if (mode() === 'stack' && active === i) setActive(-1); else setActive(i); 6450 }); 6451 b.addEventListener('mouseenter', function () { 6452 if (mode() !== 'track') return; 6453 if (hoverTimer) clearTimeout(hoverTimer); 6454 hoverTimer = setTimeout(function () { hoverTimer = null; userTookOver(); if (active !== i) setActive(i); }, HOVER_MS); 6455 }); 6456 b.addEventListener('mouseleave', function () { if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; } }); 6457 b.addEventListener('keydown', function (e) { 6458 var key = e.key || ''; 6459 var next = -1; 6460 if (key === 'ArrowRight' || key === 'ArrowDown') next = (i + 1) % segs.length; 6461 else if (key === 'ArrowLeft' || key === 'ArrowUp') next = (i - 1 + segs.length) % segs.length; 6462 else if (key === 'Home') next = 0; 6463 else if (key === 'End') next = segs.length - 1; 6464 if (next < 0) return; 6465 e.preventDefault(); 6466 userTookOver(); 6467 btns[next].focus(); 6468 if (mode() === 'track') setActive(next); 6469 }); 6470 } 6471 for (var j = 0; j < segs.length; j++) bind(j); 6472 6473 root.addEventListener('mouseenter', function () { paused = true; stopAuto(); }); 6474 root.addEventListener('mouseleave', function () { paused = false; scheduleAuto(); }); 6475 root.addEventListener('focusin', userTookOver); 6476 6477 var lastMode = null; 6478 function onResize() { 6479 if (rafId) return; 6480 rafId = (window.requestAnimationFrame || function (f) { return setTimeout(f, 16); })(function () { 6481 rafId = null; 6482 var m = mode(); 6483 if (m !== lastMode) { 6484 lastMode = m; 6485 if (m === 'track' && active < 0) setActive(0); 6486 } 6487 placeCaret(); 6488 scheduleAuto(); 6489 }); 6490 } 6491 window.addEventListener('resize', onResize); 6492 window.addEventListener('orientationchange', onResize); 6493 window.addEventListener('load', placeCaret); 6494 document.addEventListener('rz-theme-change', placeCaret); 6495 6496 /* ââ init: JS present â collapse to one open phase, arm the reveal ââ */ 6497 addClass(root, 'rz-m-pentest--js'); 6498 lastMode = mode(); 6499 setActive(0); 6500 if (!reduce) addClass(root, 'rz-m-pentest--armed'); 6501 6502 var revealed = false;
6503 function reveal() { 6504 if (revealed) return; 6505 revealed = true; 6506 addClass(root, 'rz-m-pentest--in'); 6507 placeCaret(); 6508 setTimeout(function () { removeClass(root, 'rz-m-pentest--armed'); }, 1600); 6509 scheduleAuto(); 6510 } 6511 var fallback = setTimeout(function () { inView = true; reveal(); }, 1500); 6512 6513 if ('IntersectionObserver' in window) { 6514 var io = new IntersectionObserver(function (entries) { 6515 for (var n = 0; n < entries.length; n++) { 6516 inView = entries[n].isIntersecting; 6517 if (inView) { clearTimeout(fallback); reveal(); scheduleAuto(); } else stopAuto(); 6518 } 6519 }, { threshold: 0.2 }); 6520 io.observe(root); 6521 } else { 6522 inView = true; 6523 reveal(); 6524 } 6525 if (typeof document.addEventListener === 'function') { 6526 document.addEventListener('visibilitychange', function () { if (!document.hidden) scheduleAuto(); else stopAuto(); }); 6527 } 6528})(); 6529/* ==== RZ-METHOD:pentest END ==== */ 6530 6531/* ==== RZ-METHOD:redteam START ==== */ 6532/* ============================================================ 6533 METHODOLOGY â RED TEAMING · "OPERATION DOSSIER" 6534 Timeline scrubber lights each ATT&CK column in sequence. 6535 ES5 IIFE · self-init · IntersectionObserver + 1.5s fallback · 6536 respects prefers-reduced-motion (shows final state) · 6537 all visible strings live in the DOM (i18n-safe). 6538 ============================================================ */ 6539(function () { 6540 'use strict'; 6541 if (window.__rzMRedteamInit) return; 6542 6543 var root = document.querySelector('.rz-m-redteam[data-method="redteam"]'); 6544 if (!root) return; 6545 window.__rzMRedteamInit = true; 6546 6547 var cols = [].slice.call(root.querySelectorAll('.rz-m-redteam-col')); 6548 var N = cols.length; 6549 if (!N) return; 6550 6551 var handle = root.querySelector('.rz-m-redteam-scrub-handle'); 6552 var readout = root.querySelector('.rz-m-redteam-scrub-readout'); 6553 var notches = [].slice.call(root.querySelectorAll('.rz-m-redteam-track-notches span')); 6554 var stRun = root.querySelector('.rz-m-redteam-status[data-state="run"]'); 6555 var stDone = root.querySelector('.rz-m-redteam-status[data-state="done"]'); 6556 6557 /* five equal accents, in the site's rotation order */ 6558 var ACCENTS = ['var(--rz-purple)', 'var(--rz-lime)', 'var(--rz-crimson)', 'var(--rz-indigo)', 'var(--rz-yellow)']; 6559 6560 var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; 6561 function reduced() { return !!(mq && mq.matches); } 6562 6563 var DWELL = 1500; /* per-phase dwell (ms) */ 6564 var DONE_HOLD = 2800; /* pause on the completed chain before looping */ 6565 6566 var cur = 0, timer = 0, started = false, paused = false, pauseTimer = 0; 6567 6568 function textOf(el) { return el ? el.textContent.replace(/\s+/g, ' ').trim() : ''; } 6569 function codeOf(c) { return textOf(c.querySelector('.rz-m-redteam-col-code')); } 6570 function titleOf(c) { return textOf(c.querySelector('.rz-m-redteam-col-title')); } 6571 6572 function setAccent(idx) { 6573 var a = ACCENTS[((idx % N) + N) % N % ACCENTS.length]; 6574 root.style.setProperty('--rt-active', idx < 0 ? 'var(--rz-ink)' : a); 6575 } 6576 6577 /* cumulative "kill chain" lighting up to `upto`, active marker on `activeIdx` */ 6578 function light(upto, activeIdx) { 6579 for (var i = 0; i < N; i++) { 6580 var lit = i <= upto; 6581 cols[i].classList.toggle('is-lit', lit); 6582 cols[i].classList.toggle('is-active', i === activeIdx); 6583 if (notches[i]) notches[i].classList.toggle('is-lit', lit); 6584 } 6585 } 6586 6587 function moveHandle(idx) { 6588 if (!handle) return; 6589 var pct = ((idx + 0.5) / N) * 100; 6590 handle.style.left = pct.toFixed(2) + '%'; 6591 } 6592 6593 function setReadout(idx, done) { 6594 if (!readout) return; 6595 if (done) { readout.textContent = codeOf(cols[N - 1]); return; } 6596 var c = cols[idx]; 6597 readout.textContent = codeOf(c) + ' · ' + titleOf(c); 6598 } 6599 6600 function status(done) { 6601 if (stRun) stRun.hidden = !!done; 6602 if (stDone) stDone.hidden = !done; 6603 } 6604 6605 function showPhase(idx) { 6606 setAccent(idx); 6607 light(idx, idx); 6608 moveHandle(idx); 6609 setReadout(idx, false); 6610 status(false); 6611 } 6612 6613 function finalState() { 6614 setAccent(-1); 6615 light(N - 1, -1); 6616 moveHandle(N - 1); 6617 setReadout(N - 1, true); 6618 status(true); 6619 } 6620 6621 function clearTimers() { 6622 if (timer) { clearTimeout(timer); timer = 0; } 6623 } 6624 6625 function run() { 6626 if (paused) return; 6627 clearTimers(); 6628 showPhase(cur); 6629 timer = setTimeout(function () { 6630 cur++; 6631 if (cur >= N) { 6632 finalState(); 6633 timer = setTimeout(function () { 6634 cur = 0; 6635 light(-1, -1); 6636 run(); 6637 }, DONE_HOLD); 6638 } else { 6639 run(); 6640 } 6641 }, DWELL); 6642 } 6643 6644 function start() { 6645 if (started) return; 6646 started = true; 6647 if (reduced()) { finalState(); return; } 6648 cur = 0; 6649 run(); 6650 } 6651 6652 /* header buttons: jump to a phase and pause the auto-play briefly */ 6653 function selectPhase(i) { 6654 paused = true; 6655 clearTimers(); 6656 cur = i; 6657 if (i >
6657= N - 1) { finalState(); } else { showPhase(i); } 6658 if (pauseTimer) clearTimeout(pauseTimer); 6659 pauseTimer = setTimeout(function () { 6660 paused = false; 6661 if (started && !reduced()) { cur = 0; light(-1, -1); run(); } 6662 }, 6000); 6663 } 6664 6665 cols.forEach(function (c, i) { 6666 var head = c.querySelector('.rz-m-redteam-col-head'); 6667 if (!head) return; 6668 head.addEventListener('click', function () { selectPhase(i); }); 6669 }); 6670 6671 /* start when scrolled into view, with a guaranteed fallback */ 6672 var io = null; 6673 if ('IntersectionObserver' in window) { 6674 io = new IntersectionObserver(function (entries) { 6675 for (var k = 0; k < entries.length; k++) { 6676 if (entries[k].isIntersecting) { start(); if (io) io.disconnect(); break; } 6677 } 6678 }, { threshold: 0.2 }); 6679 io.observe(root); 6680 } 6681 setTimeout(function () { start(); if (io) { io.disconnect(); io = null; } }, 1500); 6682 6683 /* if the motion preference flips to "reduce" mid-run, settle on final state */ 6684 if (mq) { 6685 var onMq = function () { if (mq.matches && started) { paused = true; clearTimers(); finalState(); } }; 6686 if (mq.addEventListener) mq.addEventListener('change', onMq); 6687 else if (mq.addListener) mq.addListener(onMq); 6688 } 6689})(); 6690/* ==== RZ-METHOD:redteam END ==== */ 6691 6692/* ==== RZ-METHOD:cloud START ==== */ 6693/* ââ METHODOLOGY (cloud) â draw-in reveal for the architecture blueprint ââ */ 6694(function () { 6695 'use strict'; 6696 var root = document.querySelector('.rz-m-cloud[data-method="cloud"]'); 6697 if (!root) return; 6698 6699 /* reduced motion: final state immediately, nothing armed */ 6700 var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; 6701 if (mq && mq.matches) { root.classList.add('is-in'); return; } 6702 6703 var shown = false; 6704 function show() { 6705 if (shown) return; 6706 shown = true; 6707 root.classList.add('is-in'); 6708 } 6709 6710 root.classList.add('is-armed'); 6711 6712 if ('IntersectionObserver' in window) { 6713 var io = new IntersectionObserver(function (entries) { 6714 for (var i = 0; i < entries.length; i++) { 6715 if (entries[i].isIntersecting) { show(); io.disconnect(); return; } 6716 } 6717 }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' }); 6718 io.observe(root); 6719 /* safety net: never leave the section hidden if the observer never fires */ 6720 setTimeout(show, 5000); 6721 } else { 6722 setTimeout(show, 1500); 6723 } 6724})(); 6725/* ==== RZ-METHOD:cloud END ==== */ 6726 6727/* ==== RZ-METHOD:codereview START ==== */ 6728/* ââ RZ-METHOD: codereview â staggered first-view reveal of the PR graph ââ */ 6729(function () { 6730 'use strict'; 6731 var root = document.querySelector('.rz-m-codereview[data-method="codereview"]'); 6732 if (!root) return; 6733 6734 var reduce = false;
6735 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 6736 6737 /* reduced motion or no IntersectionObserver â final state immediately, never animate */ 6738 if (reduce || !('IntersectionObserver' in window)) { 6739 root.classList.add('rz-m-codereview--in'); 6740 return; 6741 } 6742 6743 root.classList.add('rz-m-codereview--armed'); 6744 var shown = false, timer = null, io = null; 6745 6746 function show() { 6747 if (shown) return; 6748 shown = true; 6749 if (timer) { clearTimeout(timer); timer = null; } 6750 if (io) { try { io.disconnect(); } catch (e) {} io = null; } 6751 root.classList.add('rz-m-codereview--in'); 6752 } 6753 6754 try { 6755 io = new IntersectionObserver(function (entries) { 6756 for (var i = 0; i < entries.length; i++) { 6757 if (entries[i].isIntersecting) { show(); return; } 6758 } 6759 }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }); 6760 io.observe(root); 6761 } catch (e) { 6762 show(); 6763 return; 6764 } 6765 6766 /* safety net: content is never left hidden */ 6767 timer = setTimeout(show, 1500); 6768})(); 6769/* ==== RZ-METHOD:codereview END ==== */ 6770 6771/* ==== RZ-METHOD:phishing START ==== */ 6772/* ââ PHISHING METHODOLOGY â Campaign Funnel (self-init, ES5) âââââââââââ */ 6773(function () { 6774 'use strict'; 6775 var root = document.querySelector('.rz-m-phishing[data-method="phishing"]'); 6776 if (!root) return; 6777 6778 var reduce = false;
6779 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 6780 6781 /* ââ first-view reveal: armed only when we can drive it; content is 6782 never left hidden â an observer miss falls back to a timer. ââ */ 6783 var shown = false; 6784 function show() { 6785 if (shown) return; 6786 shown = true; 6787 root.classList.add('rz-m-phishing--in'); 6788 } 6789 if (!reduce && 'IntersectionObserver' in window) { 6790 root.classList.add('rz-m-phishing--armed'); 6791 var fired = false; 6792 var io = new IntersectionObserver(function (entries) { 6793 fired = true; 6794 for (var i = 0; i < entries.length; i++) { 6795 if (entries[i].isIntersecting) { show(); io.disconnect(); break; } 6796 } 6797 }, { threshold: 0.06, rootMargin: '0px 0px -8% 0px' }); 6798 io.observe(root); 6799 /* 1.5s: observer never reported â reveal anyway; 6s: hard cap regardless */ 6800 setTimeout(function () { if (!fired) show(); }, 1500); 6801 setTimeout(show, 6000); 6802 window.addEventListener('beforeprint', show); 6803 } else { 6804 show(); 6805 } 6806 6807 /* ââ cross-highlight: hovering a phase lights its funnel stage and 6808 vice-versa (pointer-only nicety; nothing depends on it). ââ */ 6809 var stages = root.querySelectorAll('.rz-m-phishing-stage'); 6810 var phases = root.querySelectorAll('.rz-m-phishing-phase'); 6811 function setHot(n, on) { 6812 var i; 6813 for (i = 0; i < stages.length; i++) { 6814 if (stages[i].getAttribute('data-stage') === n) stages[i].classList[on ? 'add' : 'remove']('rz-m-phishing-hot'); 6815 } 6816 for (i = 0; i < phases.length; i++) { 6817 if (phases[i].getAttribute('data-stage') === n) phases[i].classList[on ? 'add' : 'remove']('rz-m-phishing-hot'); 6818 } 6819 } 6820 function bind(list) { 6821 for (var i = 0; i < list.length; i++) { 6822 (function (el) { 6823 var n = el.getAttribute('data-stage'); 6824 el.addEventListener('mouseenter', function () { setHot(n, true); }); 6825 el.addEventListener('mouseleave', function () { setHot(n, false); }); 6826 })(list[i]); 6827 } 6828 } 6829 bind(stages); 6830 bind(phases); 6831})(); 6832/* ==== RZ-METHOD:phishing END ==== */ 6833 6834/* ==== RZ-METHOD:mobile START ==== */ 6835/* ââ MOBILE APP TESTING â "Device teardown" methodology block ââ 6836 Self-inits only if its root exists. Reveal-on-scroll with a 6837 1.5s fallback + no-IO + no-JS + reduced-motion paths so content 6838 is never left hidden. Cross-highlights layer planes <-> phase 6839 cards on hover/focus (pointer-only enhancement). ES5 IIFE. */ 6840(function () { 6841 'use strict'; 6842 if (window.rzMMobileInit) return; 6843 6844 function hasClass(c, k) { return new RegExp('(^|\\s)' + k + '(\\s|$)').test(c); } 6845 function addClass(el, k) { if (!hasClass(el.className, k)) el.className = (el.className + ' ' + k).replace(/\s+/g, ' ').replace(/^\s|\s$/g, ''); } 6846 function remClass(el, k) { el.className = el.className.replace(new RegExp('(^|\\s)' + k + '(?=\\s|$)', 'g'), ' ').replace(/\s+/g, ' ').replace(/^\s|\s$/g, ''); } 6847 6848 function init() { 6849 var root = document.querySelector('.rz-m-mobile[data-method="mobile"]'); 6850 if (!root || root.getAttribute('data-rz-m-ready')) return; 6851 root.setAttribute('data-rz-m-ready', '1'); 6852 6853 var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 6854 6855 /* Entrance is opt-in via .rz-m-js: without JS nothing is hidden. 6856 Under reduced motion we skip it entirely (final state shown). */ 6857 if (!reduce) { 6858 addClass(root, 'rz-m-js'); 6859 var shown = false; 6860 var show = function () { if (!shown) { shown = true; addClass(root, 'is-in'); } }; 6861 if (!('IntersectionObserver' in window)) { 6862 show(); 6863 } else { 6864 var io = new IntersectionObserver(function (entries) { 6865 for (var i = 0; i < entries.length; i++) { 6866 if (entries[i].isIntersecting) { io.disconnect(); show(); return; } 6867 } 6868 }, { threshold: 0.15 }); 6869 io.observe(root); 6870 setTimeout(function () { io.disconnect(); show(); }, 1500); /* fallback */ 6871 } 6872 } 6873 6874 /* Cross-highlight: hovering/focusing a plane lights its phase card, and vice versa. */ 6875 var planes = root.querySelectorAll('.rz-m-mobile-plane'); 6876 var phases = root.querySelectorAll('.rz-m-mobile-phase'); 6877 6878 function setActive(idx, on) { 6879 var i, el; 6880 for (i = 0;
6880 i < planes.length; i++) { el = planes[i]; if (el.getAttribute('data-layer') === idx) { on ? addClass(el, 'is-active') : remClass(el, 'is-active'); } } 6881 for (i = 0; i < phases.length; i++) { el = phases[i]; if (el.getAttribute('data-phase') === idx) { on ? addClass(el, 'is-active') : remClass(el, 'is-active'); } } 6882 } 6883 6884 function bind(list, attr) { 6885 for (var i = 0; i < list.length; i++) { 6886 (function (el) { 6887 var idx = el.getAttribute(attr); 6888 el.addEventListener('mouseenter', function () { setActive(idx, true); }); 6889 el.addEventListener('mouseleave', function () { setActive(idx, false); }); 6890 el.addEventListener('focusin', function () { setActive(idx, true); }); 6891 el.addEventListener('focusout', function () { setActive(idx, false); }); 6892 })(list[i]); 6893 } 6894 } 6895 bind(planes, 'data-layer'); 6896 bind(phases, 'data-phase'); 6897 } 6898 6899 window.rzMMobileInit = init; 6900 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); 6901 else init(); 6902})(); 6903/* ==== RZ-METHOD:mobile END ==== */ 6904 6905/* ==== RZ-METHOD:forensics START ==== */ 6906/* ââ METHOD · DIGITAL FORENSICS â chain-of-custody ledger reveal ââ */ 6907(function () { 6908 'use strict'; 6909 var root = document.querySelector('.rz-m-forensics[data-method="forensics"]'); 6910 if (!root) return; 6911 6912 var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; 6913 var reduce = !!(mq && mq.matches); 6914 var hashes = root.querySelectorAll('.rz-m-forensics-hash'); 6915 var live = root.querySelector('.rz-m-forensics-status[data-state="live"]'); 6916 var done = root.querySelector('.rz-m-forensics-status[data-state="done"]'); 6917 var HEX = '0123456789abcdef'; 6918 var revealed = false, sealed = false; 6919 6920 function setText(el, txt) { 6921 if (el.firstChild && el.firstChild.nodeType === 3 && !el.firstChild.nextSibling) { 6922 el.firstChild.nodeValue = txt; /* keep the same text node (i18n cache) */ 6923 } else { 6924 el.textContent = txt; 6925 } 6926 } 6927 6928 /* hex "settling" on a hash cell: non-hex glyphs (the ellipsis) stay put */ 6929 function settle(el, delay) { 6930 var target = el.getAttribute('data-hash') || el.textContent; 6931 var frame = 0, frames = 12; 6932 setTimeout(function step() { 6933 var out = '', i, c; 6934 for (i = 0; i < target.length; i++) { 6935 c = target.charAt(i); 6936 if (HEX.indexOf(c) === -1) { out += c; continue; } 6937 out += (Math.random() < frame / frames) ? c : HEX.charAt(Math.floor(Math.random() * 16)); 6938 } 6939 frame++; 6940 if (frame <= frames) { setText(el, out); setTimeout(step, 40); } 6941 else { setText(el, target); } 6942 }, delay); 6943 } 6944 6945 function markSealed() { 6946 if (sealed) return; 6947 sealed = true; 6948 root.classList.add('rz-m-forensics--sealed'); 6949 if (live) live.hidden = true; 6950 if (done) done.hidden = false; 6951 } 6952 6953 function reveal() { 6954 if (revealed) return; 6955 revealed = true; 6956 root.classList.add('rz-m-forensics--in'); 6957 if (reduce) { markSealed(); return; } 6958 var i; 6959 for (i = 0; i < hashes.length; i++) settle(hashes[i], 300 + i * 160); 6960 setTimeout(markSealed, 2000); 6961 } 6962 6963 /* reduced motion or no observer: final state straight away */ 6964 if (reduce || !('IntersectionObserver' in window)) { reveal(); return; } 6965 6966 root.classList.add('rz-m-forensics--armed'); 6967 if (live && done) { done.hidden = true; live.hidden = false; } 6968 6969 var io = new IntersectionObserver(function (entries) { 6970 var i; 6971 for (i = 0; i < entries.length; i++) { 6972 if (entries[i].isIntersecting) { reveal(); io.disconnect(); return; } 6973 } 6974 }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }); 6975 io.observe(root); 6976 6977 /* fallbacks: never leave the ledger hidden */ 6978 function inView() { 6979 var r = root.getBoundingClientRect(); 6980 var vh = window.innerHeight || document.documentElement.clientHeight; 6981 return r.top < vh * 0.92 && r.bottom > 0; 6982 } 6983 function onScroll() { 6984 if (revealed) { window.removeEventListener('scroll', onScroll); return; } 6985 if (inView()) reveal(); 6986 } 6987 setTimeout(function () { 6988 if (revealed) return; 6989 if (inView()) { reveal(); return; } 6990 window.addEventListener('scroll', onScroll, { passive: true }); 6991 window.addEventListener('resize', onScroll); 6992 setTimeout(reveal, 2500); /* hard stop: content is never left hidden */ 6993 }, 1500); 6994})();
6995/* ==== RZ-METHOD:forensics END ==== */ 6996 6997/* ==== RZ-METHOD:incident START ==== */ 6998/* ==== RZ-METHOD-inner:incident â Response Clock ==== */ 6999(function () { 7000 'use strict'; 7001 var root = document.querySelector('.rz-m-incident[data-method="incident"]'); 7002 if (!root) return; 7003 7004 var CX = 230, CY = 230, SECTOR = 60, N = 6; 7005 var PHASE_MS = 6000; /* sweep time per phase */ 7006 var DWELL_MS = 9000; /* hold after a user selection */ 7007 var EASE_MS = 550; /* hand travel on selection */ 7008 7009 var steps = [].slice.call(root.querySelectorAll('.rz-m-incident-step')); 7010 var tabs = [].slice.call(root.querySelectorAll('.rz-m-incident-tab')); 7011 var arcs = [].slice.call(root.querySelectorAll('.rz-m-incident-arc')); 7012 var rimNums = [].slice.call(root.querySelectorAll('.rz-m-incident-rim-num')); 7013 var readouts = [].slice.call(root.querySelectorAll('.rz-m-incident-dial-readout')); 7014 var wedges = [].slice.call(root.querySelectorAll('.rz-m-incident-wedge')); 7015 var hand = root.querySelector('.rz-m-incident-hand'); 7016 var trail = root.querySelector('.rz-m-incident-trail'); 7017 var cNum = root.querySelector('.rz-m-incident-centre-num'); 7018 var cTime = root.querySelector('.rz-m-incident-centre-time'); 7019 if (steps.length !== N || !hand) return; 7020 7021 var reduce = false;
7022 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 7023 var mqPhone = null; 7024 try { mqPhone = window.matchMedia ? window.matchMedia('(max-width: 767px)') : null; } catch (e) {} 7025 function stacked() { return !!(mqPhone && mqPhone.matches); } 7026 7027 /* illustrative readouts in minutes, from the markup */ 7028 var times = steps.map(function (li) { 7029 var t0 = li.getAttribute('data-rz-t0'), t1 = li.getAttribute('data-rz-t1'); 7030 return (t0 === '' || t0 === null) ? null : { t0: parseFloat(t0), t1: parseFloat(t1) }; 7031 }); 7032 7033 root.className += ' is-js'; 7034 7035 var phase = 0, angle = 30, hover = false, focused = false; 7036 var ease = null, dwellUntil = 0, raf = null, last = 0, lastTime = '', inView = true; 7037 7038 function pad(n) { return (n < 10 ? '0' : '') + n; } 7039 function fmt(min) { var m = Math.max(0, Math.floor(min)); return 'T+' + pad(Math.floor(m / 60)) + ':' + pad(m % 60); } 7040 function pt(r, deg) { var a = deg * Math.PI / 180; return (CX + r * Math.sin(a)).toFixed(2) + ' ' + (CY - r * Math.cos(a)).toFixed(2); } 7041 7042 function setPhase(i) { 7043 phase = i; 7044 var st = stacked(); 7045 steps.forEach(function (li, k) { 7046 var on = k === i; 7047 li.className = li.className.replace(/\s*\bis-active\b/g, '') + (on ? ' is-active' : ''); 7048 if (on) li.setAttribute('aria-current', 'step'); else li.removeAttribute('aria-current'); 7049 tabs[k].setAttribute('aria-expanded', (on || st) ? 'true' : 'false'); 7050 }); 7051 arcs.forEach(function (a, k) { 7052 a.className.baseVal = 'rz-m-incident-arc' + (k === i ? ' is-active' : (k < i ? ' is-done' : '')); 7053 }); 7054 rimNums.forEach(function (t, k) { t.className.baseVal = 'rz-m-incident-rim-num' + (k === i ? ' is-active' : ''); }); 7055 readouts.forEach(function (t, k) { t.className.baseVal = 'rz-m-incident-dial-readout' + (k === i ? ' is-active' : ''); }); 7056 if (cNum) cNum.textContent = pad(i + 1); 7057 } 7058 7059 function render() { 7060 hand.setAttribute('transform', 'rotate(' + angle.toFixed(2) + ' ' + CX + ' ' + CY + ')'); 7061 var s0 = phase * SECTOR, local = angle - s0; 7062 if (trail) { 7063 if (local > 0.5 && local < SECTOR) { 7064 var a1 = s0 + local, big = local > 180 ? 1 : 0; 7065 trail.setAttribute('d', 'M' + pt(150, s0) + ' A150 150 0 ' + big + ' 1 ' + pt(150, a1) + 7066 ' L' + pt(112, a1) + ' A112 112 0 ' + big + ' 0 ' + pt(112, s0) + ' Z'); 7067 } else trail.setAttribute('d', ''); 7068 } 7069 if (cTime) { 7070 var t = times[phase], txt; 7071 if (!t) { 7072 var span = steps[phase].querySelector('.rz-m-incident-tab-time'); 7073 txt = span ? span.textContent : ''; 7074 } else { 7075 var f = Math.min(1, Math.max(0, local / SECTOR)); 7076 txt = fmt(t.t0 + (t.t1 - t.t0) * f); 7077 } 7078 if (txt !== lastTime) { lastTime = txt; cTime.textContent = txt; } 7079 } 7080 } 7081 7082 function select(i, now) { 7083 if (i === phase && !ease) { dwellUntil = (now || performance.now()) + DWELL_MS; return; } 7084 setPhase(i); 7085 if (reduce) { angle = i * SECTOR + 30; render(); return; } 7086 var target = i * SECTOR + 3; 7087 var delta = ((target - angle + 540) % 360) - 180; 7088 ease = { from: angle, delta: delta, start: now || performance.now() }; 7089 if (!raf) start(); 7090 } 7091 7092 function frame(now) { 7093 raf = null; 7094 var dt = Math.min(64, now - (last || now)); last = now; 7095 if (ease) { 7096 var p = Math.min(1, (now - ease.start) / EASE_MS); 7097 var k = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; 7098 angle = (ease.from + ease.delta * k + 360) % 360; 7099 if (p >= 1) { ease = null; dwellUntil = now + DWELL_MS; } 7100 } else if (!hover && !focused && now >= dwellUntil) { 7101 angle = (angle + dt * SECTOR / PHASE_MS) % 360; 7102 var np = Math.floor(angle / SECTOR); 7103 if (np !== phase) setPhase(np); 7104 } 7105 render(); 7106 if (inView && !reduce) raf = requestAnimationFrame(frame); 7107 } 7108 function start() { if (!raf && !reduce) { last = 0; raf = requestAnimationFrame(frame); } } 7109 function stop() { if (raf) { cancelAnimationFrame(raf); raf = null; } } 7110 7111 /* ââ interaction ââ */
7112 tabs.forEach(function (btn, i) { 7113 btn.addEventListener('click', function () { select(i); }); 7114 btn.addEventListener('keydown', function (e) { 7115 var j = -1; 7116 if (e.key === 'ArrowDown' || e.key === 'ArrowRight') j = (i + 1) % N; 7117 else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') j = (i + N - 1) % N; 7118 else if (e.key === 'Home') j = 0; 7119 else if (e.key === 'End') j = N - 1; 7120 if (j >= 0) { e.preventDefault(); tabs[j].focus(); } 7121 }); 7122 }); 7123 wedges.forEach(function (w) { 7124 w.addEventListener('click', function () { select(parseInt(w.getAttribute('data-rz-phase'), 10) || 0); }); 7125 }); 7126 if (window.PointerEvent) { 7127 root.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') hover = true; }); 7128 root.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse') hover = false; }); 7129 } else { 7130 root.addEventListener('mouseenter', function () { hover = true; }); 7131 root.addEventListener('mouseleave', function () { hover = false; }); 7132 } 7133 root.addEventListener('focusin', function () { focused = true; }); 7134 root.addEventListener('focusout', function (e) { focused = !!(e.relatedTarget && root.contains(e.relatedTarget)); }); 7135 if (mqPhone) { 7136 var onMq = function () { setPhase(phase); }; 7137 if (mqPhone.addEventListener) mqPhone.addEventListener('change', onMq); 7138 else if (mqPhone.addListener) mqPhone.addListener(onMq); 7139 } 7140 7141 /* ââ arm (draw-in) + run only while in view; 1.5 s fallback so nothing stays hidden ââ */ 7142 var armed = false; 7143 function arm() { if (!armed) { armed = true; root.className += ' is-armed'; } } 7144 setTimeout(arm, 1500); 7145 if (reduce) arm(); 7146 7147 setPhase(0); 7148 dwellUntil = performance.now() + 2500; 7149 render(); 7150 7151 if ('IntersectionObserver' in window) { 7152 var io = new IntersectionObserver(function (entries) { 7153 inView = entries[0].isIntersecting; 7154 if (inView) { arm(); start(); } else stop(); 7155 }, { threshold: 0.08 }); 7156 io.observe(root); 7157 } else { arm(); start(); } 7158})(); 7159/* ==== RZ-METHOD-inner:incident END ==== */ 7160/* ==== RZ-METHOD:incident END ==== */ 7161 7162/* ==== RZ-METHOD:malware START ==== */ 7163/* ââ METHODOLOGY: malware-analysis.html â Sandbox Pipeline reveal ââ 7164 Arms the pipeline (cards + connectors hidden) only once JS is 7165 running, then reveals on first intersection. Safety net: a 1.5s 7166 timeout reveals regardless, and reduced-motion / no-IO browsers 7167 are shown the final state immediately. */ 7168(function () { 7169 'use strict'; 7170 var root = document.querySelector('.rz-m-malware[data-method="malware"]'); 7171 if (!root) return; 7172 7173 function addClass(el, c) { if ((' ' + el.className + ' ').indexOf(' ' + c + ' ') < 0) el.className += ' ' + c; } 7174 7175 var reduce = false;
7176 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 7177 7178 if (reduce || !('IntersectionObserver' in window)) { addClass(root, 'is-live'); return; } 7179 7180 addClass(root, 'is-armed'); 7181 7182 var io = null, done = false; 7183 function live() { 7184 if (done) return; 7185 done = true; 7186 addClass(root, 'is-live'); 7187 if (io) { try { io.disconnect(); } catch (e) {} } 7188 } 7189 7190 try { 7191 io = new IntersectionObserver(function (entries) { 7192 for (var i = 0; i < entries.length; i++) { 7193 if (entries[i].isIntersecting) { live(); break; } 7194 } 7195 }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' }); 7196 io.observe(root); 7197 } catch (e) { live(); } 7198 7199 window.setTimeout(live, 1500); /* never leave the section hidden */ 7200})(); 7201/* ==== RZ-METHOD:malware END ==== */ 7202 7203/* ==== RZ-METHOD:custom START ==== */ 7204/* ââ METHODOLOGY (custom) â modules snap into the blueprint board on first view ââ */ 7205(function () { 7206 'use strict'; 7207 var root = document.querySelector('.rz-m-custom[data-method="custom"]'); 7208 if (!root) return; 7209 7210 /* The built board is the CSS default; we only "disassemble" it (is-armed) when 7211 motion is allowed, then snap it together (is-in) once it scrolls into view. */ 7212 var mq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; 7213 if (mq && mq.matches) return; 7214 7215 var shown = false; 7216 function show() { 7217 if (shown) return; 7218 shown = true; 7219 root.classList.add('is-in'); 7220 window.removeEventListener('scroll', onScroll); 7221 window.removeEventListener('resize', onScroll); 7222 } 7223 function inView() { 7224 var r = root.getBoundingClientRect(); 7225 var vh = window.innerHeight || document.documentElement.clientHeight || 0; 7226 return r.bottom > 0 && r.top < vh * 0.92; 7227 } 7228 function onScroll() { if (inView()) show(); } 7229 7230 root.classList.add('is-armed'); 7231 7232 if ('IntersectionObserver' in window) { 7233 var io = new IntersectionObserver(function (entries) { 7234 for (var i = 0; i < entries.length; i++) { 7235 if (entries[i].isIntersecting) { show(); io.disconnect(); return; } 7236 } 7237 }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }); 7238 io.observe(root); 7239 /* safety net: the board is never left disassembled if the observer never fires */ 7240 setTimeout(show, 5000); 7241 } else { 7242 setTimeout(show, 1500); 7243 } 7244 /* active fallback alongside the observer: any scroll that brings it on screen assembles it */ 7245 window.addEventListener('scroll', onScroll, false); 7246 window.addEventListener('resize', onScroll, false); 7247})(); 7248/* ==== RZ-METHOD:custom END ==== */ 7249 7250/* ==== RZ-ABOUT START ==== */ 7251/* ==== RZ-PAGE:about START ==== */ 7252/* about.html: stagger the section reveals on first view and mark the active 7253 site language on the identity card. ES5, self-init only when .rz-about is 7254 present, static (nothing hidden) without JS or under reduced motion. */ 7255(function () { 7256 'use strict'; 7257 if (window.__rzAboutInit) return; 7258 window.__rzAboutInit = true; 7259 7260 function addClass(el, name) { 7261 if (el.classList) el.classList.add(name); 7262 else if ((' ' + el.className + ' ').indexOf(' ' + name + ' ') < 0) el.className += ' ' + name; 7263 } 7264 function removeClass(el, name) { 7265 if (el.classList) el.classList.remove(name); 7266 else el.className = (' ' + el.className + ' ').replace(' ' + name + ' ', ' ').replace(/^\s+|\s+$/g, ''); 7267 } 7268 7269 function init() { 7270 var root = document.querySelector('.rz-about[data-page="about"]'); 7271 if (!root) return; 7272 7273 /* ---- identity card: underline the active language (follows i18n.js via html[lang]) ---- */ 7274 var langs = root.querySelectorAll('.rz-about-idcard-lang'); 7275 function markLang() { 7276 var cur = (document.documentElement.getAttribute('lang') || 'en').split('-')[0].toLowerCase(); 7277 var known = false, i; 7278 for (i = 0; i < langs.length; i++) { 7279 if (langs[i].getAttribute('data-lang') === cur) known = true; 7280 } 7281 if (!known) cur = 'en'; 7282 for (i = 0; i < langs.length; i++) { 7283 if (langs[i].getAttribute('data-lang') === cur) addClass(langs[i], 'is-active'); 7284 else removeClass(langs[i], 'is-active'); 7285 } 7286 } 7287 if (langs.length) { 7288 markLang(); 7289 if (window.MutationObserver) { 7290 try { 7291 new MutationObserver(markLang).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] }); 7292 } catch (e) {} 7293 } 7294 } 7295 7296 /* ---- staggered first-view reveal per group ---- */ 7297 var reduce = false;
7298 try { reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) {} 7299 7300 var groups = root.querySelectorAll('[data-rz-about-reveal]'); 7301 if (reduce || !groups.length || !('IntersectionObserver' in window)) return; /* stays static */ 7302 7303 var g, j, items; 7304 for (g = 0; g < groups.length; g++) { 7305 items = groups[g].querySelectorAll('.rz-about-anim'); 7306 for (j = 0; j < items.length; j++) { 7307 if (items[j].style && items[j].style.setProperty) { 7308 items[j].style.setProperty('--rz-about-d', Math.min(j * 70, 560) + 'ms'); 7309 } 7310 } 7311 } 7312 addClass(root, 'rz-about--js'); 7313 7314 var io = new IntersectionObserver(function (entries) { 7315 for (var i = 0; i < entries.length; i++) { 7316 if (entries[i].isIntersecting) { 7317 addClass(entries[i].target, 'is-in'); 7318 io.unobserve(entries[i].target); 7319 } 7320 } 7321 }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' }); 7322 for (g = 0; g < groups.length; g++) io.observe(groups[g]); 7323 7324 /* safety net: never leave a group hidden (e.g. anchor jumps past it) */ 7325 window.setTimeout(function () { 7326 for (var k = 0; k < groups.length; k++) { 7327 var r = groups[k].getBoundingClientRect(); 7328 if (r.top < (window.innerHeight || document.documentElement.clientHeight)) addClass(groups[k], 'is-in'); 7329 } 7330 }, 1200); 7331 } 7332 7333 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); 7334 else init(); 7335})(); 7336/* ==== RZ-PAGE:about END ==== */ 7337/* ==== RZ-ABOUT END ==== */
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.