feat: add confetti
This commit is contained in:
34
app/components/Common/Confetti.vue
Normal file
34
app/components/Common/Confetti.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
screen: "top" | "bottom";
|
||||
}>();
|
||||
|
||||
const confetti = useConfetti();
|
||||
const { onRender } = useScreen();
|
||||
|
||||
onRender((ctx) => {
|
||||
let offset: number;
|
||||
if (props.screen === "top") {
|
||||
confetti.update();
|
||||
offset = 0;
|
||||
} else {
|
||||
offset = LOGICAL_HEIGHT;
|
||||
}
|
||||
|
||||
for (const p of confetti.particles) {
|
||||
const localY = p.y - offset;
|
||||
if (localY < -10 || localY > LOGICAL_HEIGHT + 10) continue;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(p.x, localY);
|
||||
ctx.rotate(p.rotation);
|
||||
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.fillRect(-p.width / 2, -p.height / 2, p.width, p.height);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
defineOptions({ render: () => null });
|
||||
</script>
|
||||
109
app/composables/useConfetti.ts
Normal file
109
app/composables/useConfetti.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
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 });
|
||||
@@ -103,6 +103,7 @@ useKeyUp((key) => {
|
||||
<AchievementsTopScreen v-else-if="app.screen === 'achievements'" />
|
||||
|
||||
<AchievementsNotification />
|
||||
<CommonConfetti screen="top" />
|
||||
</Screen>
|
||||
</div>
|
||||
<div>
|
||||
@@ -114,6 +115,8 @@ useKeyUp((key) => {
|
||||
<SettingsBottomScreen v-else-if="app.screen === 'settings'" />
|
||||
<GalleryBottomScreen v-else-if="app.screen === 'gallery'" />
|
||||
<AchievementsBottomScreen v-else-if="app.screen === 'achievements'" />
|
||||
|
||||
<CommonConfetti screen="bottom" />
|
||||
</Screen>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user