perf(action-points): pool confetti into one shared canvas + single rAF
Each section/page completion previously appended a new full-viewport (100vw x 100vh, z-index 99999) canvas with its own requestAnimationFrame loop. Completing several sections in a row stacked multiple fullscreen layers, each independently repainting the whole viewport at 60fps — the real cause of click-induced slowdown (the change handler itself is ~1ms). Now: one shared canvas, one rAF loop, particles pooled across completions. The loop stops and the canvas is removed once no particles remain, so there is no idle fullscreen compositing layer. Resize keeps it sized. Verified live: 1 canvas after two back-to-back section completions, 0 canvases after the animation ends, green pills still white-on-green.
This commit is contained in:
parent
69f69be5e4
commit
fbc488cc95
1 changed files with 98 additions and 42 deletions
|
|
@ -447,75 +447,131 @@
|
|||
}
|
||||
|
||||
// ── Confetti ───────────────────────────────────────
|
||||
function fireConfetti(intensity) {
|
||||
if (REDUCED_MOTION) return;
|
||||
// Single shared canvas + one rAF loop. Multiple completions add particles
|
||||
// to the same pool instead of stacking N fullscreen canvases (each of which
|
||||
// would independently repaint the whole viewport at 60fps — the cause of
|
||||
// click-induced slowdown). The loop fully stops when no particles remain,
|
||||
// and the canvas is removed so there is no idle fullscreen layer.
|
||||
var _confettiCanvas = null;
|
||||
var _confettiCtx = null;
|
||||
var _confettiParticles = [];
|
||||
var _confettiRAF = 0;
|
||||
|
||||
var CONFETTI_COLORS = [
|
||||
"#4a8262", "#a8c5b6", "#3d6b52", "#e9dfc6", "#92ad9e", "#d4e8dc",
|
||||
];
|
||||
|
||||
function _ensureConfettiCanvas() {
|
||||
if (_confettiCanvas) return;
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.style.cssText =
|
||||
"position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:99999;";
|
||||
document.body.appendChild(canvas);
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
var ctx = canvas.getContext("2d");
|
||||
_confettiCanvas = canvas;
|
||||
_confettiCtx = canvas.getContext("2d");
|
||||
}
|
||||
|
||||
function _destroyConfettiCanvas() {
|
||||
if (_confettiCanvas) {
|
||||
_confettiCanvas.remove();
|
||||
_confettiCanvas = null;
|
||||
_confettiCtx = null;
|
||||
}
|
||||
_confettiParticles = [];
|
||||
_confettiRAF = 0;
|
||||
}
|
||||
|
||||
function _confettiTick(now) {
|
||||
var ctx = _confettiCtx;
|
||||
var canvas = _confettiCanvas;
|
||||
if (!ctx || !canvas) {
|
||||
_confettiRAF = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
var alive = 0;
|
||||
for (var i = 0; i < _confettiParticles.length; i++) {
|
||||
var p = _confettiParticles[i];
|
||||
var elapsed = now - p.start;
|
||||
var progress = elapsed / p.duration;
|
||||
if (progress >= 1) continue; // dead — skip
|
||||
alive++;
|
||||
|
||||
p.x += p.vx;
|
||||
p.vy += 0.12;
|
||||
p.y += p.vy;
|
||||
p.rot += p.rotV;
|
||||
var fade = progress > 0.7 ? 1 - (progress - 0.7) / 0.3 : 1;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(p.x, p.y);
|
||||
ctx.rotate((p.rot * Math.PI) / 180);
|
||||
ctx.globalAlpha = fade;
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
if (alive === 0) {
|
||||
_destroyConfettiCanvas();
|
||||
return;
|
||||
}
|
||||
|
||||
// Periodically compact the array so dead particles don't accumulate.
|
||||
if (_confettiParticles.length > 400) {
|
||||
_confettiParticles = _confettiParticles.filter(function (p) {
|
||||
return now - p.start < p.duration;
|
||||
});
|
||||
}
|
||||
|
||||
_confettiRAF = requestAnimationFrame(_confettiTick);
|
||||
}
|
||||
|
||||
function fireConfetti(intensity) {
|
||||
if (REDUCED_MOTION) return;
|
||||
|
||||
_ensureConfettiCanvas();
|
||||
var canvas = _confettiCanvas;
|
||||
|
||||
var COLORS = ["#4a8262", "#a8c5b6", "#3d6b52", "#e9dfc6", "#92ad9e", "#d4e8dc"];
|
||||
var count = intensity === "page" ? 140 : 55;
|
||||
var duration = intensity === "page" ? 3200 : 2200;
|
||||
var drift = intensity === "page" ? 1.8 : 1.2;
|
||||
var start = performance.now();
|
||||
|
||||
var particles = [];
|
||||
for (var i = 0; i < count; i++) {
|
||||
particles.push({
|
||||
_confettiParticles.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: -(Math.random() * 60 + 10),
|
||||
w: Math.random() * 7 + 2,
|
||||
h: Math.random() * 4 + 1.5,
|
||||
color: COLORS[Math.floor(Math.random() * COLORS.length)],
|
||||
color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
|
||||
vx: (Math.random() - 0.5) * drift,
|
||||
vy: Math.random() * 2 + 1.2,
|
||||
rot: Math.random() * 360,
|
||||
rotV: (Math.random() - 0.5) * 8,
|
||||
opacity: 1,
|
||||
start: start,
|
||||
duration: duration,
|
||||
});
|
||||
}
|
||||
|
||||
var start = performance.now();
|
||||
|
||||
function frame(now) {
|
||||
var elapsed = now - start;
|
||||
var progress = elapsed / duration;
|
||||
if (progress >= 1) {
|
||||
canvas.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Fade out in last 30%
|
||||
var fade = progress > 0.7 ? 1 - (progress - 0.7) / 0.3 : 1;
|
||||
|
||||
particles.forEach(function (p) {
|
||||
p.x += p.vx;
|
||||
p.vy += 0.12;
|
||||
p.y += p.vy;
|
||||
p.rot += p.rotV;
|
||||
p.opacity = fade;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(p.x, p.y);
|
||||
ctx.rotate((p.rot * Math.PI) / 180);
|
||||
ctx.globalAlpha = p.opacity;
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
// Start the single loop only if it isn't already running.
|
||||
if (!_confettiRAF) {
|
||||
_confettiRAF = requestAnimationFrame(_confettiTick);
|
||||
}
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
// Keep the shared canvas sized to the viewport.
|
||||
window.addEventListener("resize", function () {
|
||||
if (_confettiCanvas) {
|
||||
_confettiCanvas.width = window.innerWidth;
|
||||
_confettiCanvas.height = window.innerHeight;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Sound ──────────────────────────────────────────
|
||||
var _audioCtx = null;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue