feat: 全员一致时增加 confetti 粒子特效、庆祝音效和默契度文案

This commit is contained in:
2026-02-24 22:03:49 +08:00
parent 43d3ff0fa3
commit 801e922bb6
5 changed files with 159 additions and 11 deletions
+96
View File
@@ -0,0 +1,96 @@
import confetti from "canvas-confetti";
type ConfettiFn = confetti.CreateTypes;
let _cannon: ConfettiFn | null = null;
function getCannon(): ConfettiFn {
if (_cannon) return _cannon;
const canvas = document.createElement("canvas");
canvas.style.cssText =
"position:fixed;inset:0;width:100vw;height:100vh;z-index:2147483647;pointer-events:none";
document.body.appendChild(canvas);
canvas.width = canvas.offsetWidth * (window.devicePixelRatio || 1);
canvas.height = canvas.offsetHeight * (window.devicePixelRatio || 1);
_cannon = confetti.create(canvas, { resize: true });
return _cannon;
}
export function fireCelebration() {
const fire = getCannon();
const colors = ["#10b981", "#f59e0b", "#ef4444", "#6366f1", "#ec4899"];
fire({
particleCount: 80,
spread: 100,
origin: { y: 0.35 },
colors,
startVelocity: 45,
ticks: 200,
});
const end = Date.now() + 2500;
const frame = () => {
if (Date.now() > end) return;
fire({
particleCount: 3,
angle: 60,
spread: 55,
origin: { x: 0, y: 0.6 },
colors,
startVelocity: 35,
ticks: 150,
});
fire({
particleCount: 3,
angle: 120,
spread: 55,
origin: { x: 1, y: 0.6 },
colors,
startVelocity: 35,
ticks: 150,
});
requestAnimationFrame(frame);
};
setTimeout(frame, 300);
}
export function playChime() {
try {
const ctx = new AudioContext();
const gain = ctx.createGain();
gain.connect(ctx.destination);
gain.gain.setValueAtTime(0.15, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 1.2);
const notes = [523.25, 659.25, 783.99, 1046.5]; // C5, E5, G5, C6
notes.forEach((freq, i) => {
const osc = ctx.createOscillator();
osc.type = "sine";
osc.frequency.value = freq;
const noteGain = ctx.createGain();
noteGain.connect(gain);
const start = ctx.currentTime + i * 0.12;
noteGain.gain.setValueAtTime(0, start);
noteGain.gain.linearRampToValueAtTime(0.3, start + 0.03);
noteGain.gain.exponentialRampToValueAtTime(0.001, start + 0.6);
osc.connect(noteGain);
osc.start(start);
osc.stop(start + 0.6);
});
setTimeout(() => ctx.close(), 2000);
} catch {
// Audio not available — silent fallback
}
}