perf(dashboard): cache particle glow sprites instead of per-frame gradients
The dashboard particle preset built a fresh createRadialGradient + arc-fill for all 50 glows every frame. But each glow's gradient is a fixed size/colour for the particle's life — only the pulse ALPHA changes per frame, and canvas multiplies image alpha by ctx.globalAlpha. So bake the gradient once into a per-particle offscreen canvas (full alpha) and drawImage it each frame with globalAlpha = pulse (times the incoming globalAlpha, so transition fades stay identical). Rebuilt only when the accent colour changes. Pixel-identical output: same colour, same linear falloff, same source-over; sprite rendered at ceil(glowSize) then downscaled to exact glowSize. Drops 50 gradient allocations + arc-fills per frame to 50 cached blits. Scoped to the dashboard preset only (smallest blast radius); other presets untouched.
This commit is contained in:
parent
4b58d8079e
commit
b73389adea
1 changed files with 33 additions and 8 deletions
|
|
@ -101,6 +101,27 @@
|
|||
// Each preset: { count, init(p, i), update(p, time, i), draw(p, ctx, accent, time),
|
||||
// optional: initExtras(), drawGlobal(ctx, particles, accent, time, extras) }
|
||||
|
||||
// Pre-rendered glow sprite. A dashboard particle's glow is a radial gradient
|
||||
// (its accent colour -> transparent) of a SIZE that's fixed for the particle's
|
||||
// life — the only thing changing each frame is the pulse ALPHA. Since canvas
|
||||
// multiplies fillStyle/image alpha by ctx.globalAlpha, we can bake the gradient
|
||||
// ONCE into an offscreen canvas at full alpha and drawImage it each frame with
|
||||
// globalAlpha = pulse, instead of building a fresh createRadialGradient + arc-fill
|
||||
// for all 50 particles every frame. Output is pixel-identical; just far cheaper.
|
||||
function buildGlowSprite(col, glowSize) {
|
||||
const s = Math.max(1, Math.ceil(glowSize));
|
||||
const c = document.createElement('canvas');
|
||||
c.width = s * 2;
|
||||
c.height = s * 2;
|
||||
const g = c.getContext('2d');
|
||||
const grad = g.createRadialGradient(s, s, 0, s, s, s);
|
||||
grad.addColorStop(0, `rgba(${col}, 1)`);
|
||||
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
g.fillStyle = grad;
|
||||
g.fillRect(0, 0, s * 2, s * 2);
|
||||
return c;
|
||||
}
|
||||
|
||||
const PRESETS = {
|
||||
|
||||
// ── DASHBOARD — network nodes, connections, data packets, shooting stars ──
|
||||
|
|
@ -130,16 +151,20 @@
|
|||
const pulse = 0.5 + 0.5 * Math.sin(time * 1.5 + p.phase);
|
||||
const col = shiftAccent(accent, p.hueShift);
|
||||
|
||||
// Glow — hubs get bigger, brighter glow
|
||||
// Glow — hubs get bigger, brighter glow. Cached sprite blit (alpha at
|
||||
// blit time); pixel-identical to the old per-frame radial gradient.
|
||||
const glowSize = p.isHub ? p.radius * 6 : p.radius * 4;
|
||||
const glowAlpha = p.isHub ? (0.18 + pulse * 0.12) : (0.10 + pulse * 0.07);
|
||||
const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowSize);
|
||||
glow.addColorStop(0, `rgba(${col}, ${glowAlpha})`);
|
||||
glow.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, glowSize, 0, Math.PI * 2);
|
||||
ctx.fillStyle = glow;
|
||||
ctx.fill();
|
||||
if (!p._glowCanvas || p._glowCol !== col || p._glowR !== glowSize) {
|
||||
p._glowCanvas = buildGlowSprite(col, glowSize); // rebuilt only on accent change
|
||||
p._glowCol = col;
|
||||
p._glowR = glowSize;
|
||||
}
|
||||
// Multiply by the incoming globalAlpha so transition fades match exactly.
|
||||
const _glowPrevAlpha = ctx.globalAlpha;
|
||||
ctx.globalAlpha = _glowPrevAlpha * glowAlpha;
|
||||
ctx.drawImage(p._glowCanvas, p.x - glowSize, p.y - glowSize, glowSize * 2, glowSize * 2);
|
||||
ctx.globalAlpha = _glowPrevAlpha;
|
||||
|
||||
// Core
|
||||
const coreAlpha = p.isHub ? (0.5 + pulse * 0.3) : (0.3 + pulse * 0.2);
|
||||
|
|
|
|||
Loading…
Reference in a new issue