110 lines
2.6 KiB
TypeScript
110 lines
2.6 KiB
TypeScript
const PARTICLE_COUNT = 400;
|
|
const SPAWN_DURATION = 700;
|
|
const VX_RANGE = 0.5;
|
|
const VY_MIN = 0.3;
|
|
const VY_RANGE = 0.2;
|
|
const WIDTH_MIN = 3;
|
|
const WIDTH_RANGE = 4;
|
|
const HEIGHT_MIN = 2;
|
|
const HEIGHT_RANGE = 3;
|
|
const ROTATION_SPEED_RANGE = 0.15;
|
|
const DRIFT_STRENGTH = 0.03;
|
|
const GRAVITY = 0.007;
|
|
const VX_DAMPING = 0.995;
|
|
const SWAY_AMPLITUDE = 0.3;
|
|
const SWAY_SPEED_MIN = 0.02;
|
|
const SWAY_SPEED_RANGE = 0.03;
|
|
const OFFSCREEN_MARGIN = 10;
|
|
const TOTAL_HEIGHT = 192 * 2;
|
|
|
|
const COLORS = [
|
|
"#fb0018",
|
|
"#0059f3",
|
|
"#f3e300",
|
|
"#00fb00",
|
|
"#fb8afb",
|
|
"#fb9200",
|
|
];
|
|
|
|
export type ConfettiParticle = {
|
|
x: number;
|
|
y: number;
|
|
vx: number;
|
|
vy: number;
|
|
width: number;
|
|
height: number;
|
|
color: string;
|
|
rotation: number;
|
|
rotationSpeed: number;
|
|
swayPhase: number;
|
|
swaySpeed: number;
|
|
};
|
|
|
|
type Spawner = {
|
|
frame: number;
|
|
accumulator: number;
|
|
};
|
|
|
|
const particles: ConfettiParticle[] = [];
|
|
const spawners: Spawner[] = [];
|
|
|
|
const spawnParticle = () => {
|
|
particles.push({
|
|
x: Math.random() * LOGICAL_WIDTH,
|
|
y: -(30 + Math.random() * 20),
|
|
vx: (Math.random() - 0.5) * VX_RANGE,
|
|
vy: VY_MIN + Math.random() * VY_RANGE,
|
|
width: WIDTH_MIN + Math.random() * WIDTH_RANGE,
|
|
height: HEIGHT_MIN + Math.random() * HEIGHT_RANGE,
|
|
color: COLORS[Math.floor(Math.random() * COLORS.length)]!,
|
|
rotation: Math.random() * Math.PI * 2,
|
|
rotationSpeed: (Math.random() - 0.5) * ROTATION_SPEED_RANGE,
|
|
swayPhase: Math.random() * Math.PI * 2,
|
|
swaySpeed: SWAY_SPEED_MIN + Math.random() * SWAY_SPEED_RANGE,
|
|
});
|
|
};
|
|
|
|
const spawn = () => {
|
|
spawners.push({ frame: 0, accumulator: 0 });
|
|
};
|
|
|
|
const update = () => {
|
|
for (let s = spawners.length - 1; s >= 0; s -= 1) {
|
|
const spawner = spawners[s]!;
|
|
if (spawner.frame < SPAWN_DURATION) {
|
|
const progress = spawner.frame / SPAWN_DURATION;
|
|
const inv = 1 - progress;
|
|
const rate = inv * inv;
|
|
spawner.accumulator += rate * (PARTICLE_COUNT / SPAWN_DURATION) * 3;
|
|
|
|
const count = Math.floor(spawner.accumulator);
|
|
spawner.accumulator -= count;
|
|
|
|
for (let i = 0; i < count; i += 1) {
|
|
spawnParticle();
|
|
}
|
|
|
|
spawner.frame += 1;
|
|
} else {
|
|
spawners.splice(s, 1);
|
|
}
|
|
}
|
|
|
|
for (let i = particles.length - 1; i >= 0; i -= 1) {
|
|
const p = particles[i]!;
|
|
p.vy += GRAVITY;
|
|
p.vx += (Math.random() - 0.5) * DRIFT_STRENGTH;
|
|
p.vx *= VX_DAMPING;
|
|
p.swayPhase += p.swaySpeed;
|
|
p.x += p.vx + Math.sin(p.swayPhase) * SWAY_AMPLITUDE;
|
|
p.y += p.vy;
|
|
p.rotation += p.rotationSpeed;
|
|
|
|
if (p.y > TOTAL_HEIGHT + OFFSCREEN_MARGIN) {
|
|
particles.splice(i, 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const useConfetti = () => ({ particles, spawn, update });
|