diff --git a/app/components/Common/Confetti.vue b/app/components/Common/Confetti.vue index 13c9dc3..0269f7c 100644 --- a/app/components/Common/Confetti.vue +++ b/app/components/Common/Confetti.vue @@ -6,10 +6,10 @@ const props = defineProps<{ const confetti = useConfetti(); const { onRender } = useScreen(); -onRender((ctx) => { +onRender((ctx, deltaTime) => { let offset: number; if (props.screen === "top") { - confetti.update(); + confetti.update(deltaTime); offset = 0; } else { offset = LOGICAL_HEIGHT; diff --git a/app/components/Settings/BottomScreen/Menus/TouchScreen/TapTap.vue b/app/components/Settings/BottomScreen/Menus/TouchScreen/TapTap.vue index f37b807..be7b597 100644 --- a/app/components/Settings/BottomScreen/Menus/TouchScreen/TapTap.vue +++ b/app/components/Settings/BottomScreen/Menus/TouchScreen/TapTap.vue @@ -12,16 +12,17 @@ const { onRender, onClick } = useScreen(); const BAR_HEIGHT = 24; const MAX_RADIUS = 27; const MIN_RADIUS = 3; -const BASE_SHRINK_SPEED = 0.09; -const MAX_SHRINK_SPEED = 0.14; -const BASE_SPAWN_INTERVAL = 90; -const MIN_SPAWN_INTERVAL = 45; +const BASE_SHRINK_SPEED = 0.18; +const MAX_SHRINK_SPEED = 0.28; +const BASE_SPAWN_INTERVAL = 45; +const MIN_SPAWN_INTERVAL = 23; const DIFFICULTY_SCORE_CAP = 100; const RING_STROKE_WIDTH = 5; -const RING_EXPAND_SPEED = 0.3; -const RING_FADE_SPEED = 0.05; +const RING_EXPAND_SPEED = 0.6; +const RING_FADE_SPEED = 0.1; const MAX_LIVES = 3; -const CROSSHAIR_MOVE_SPEED = 5; +const CROSSHAIR_MOVE_SPEED = 10; +const FRAME_TIME = 1000 / 60; const getDifficulty = () => { const progress = Math.min(score / DIFFICULTY_SCORE_CAP, 1); @@ -120,7 +121,7 @@ const animateIntro = async () => { const animateOutro = async () => { isAnimating.value = true; targetX = 0; - targetY = LOGICAL_HEIGHT * 2 - 20; + targetY = LOGICAL_HEIGHT; await gsap .timeline({ @@ -200,12 +201,12 @@ const handleActivateA = async () => { } }; -const moveTowards = (current: number, target: number) => { +const moveTowards = (current: number, target: number, dt: number) => { if (current === target) return current; const direction = Math.sign(target - current); return ( current + - direction * Math.min(CROSSHAIR_MOVE_SPEED, Math.abs(target - current)) + direction * Math.min(CROSSHAIR_MOVE_SPEED * dt, Math.abs(target - current)) ); }; @@ -303,15 +304,17 @@ onClick((mx, my) => { } }); -onRender((ctx) => { +onRender((ctx, deltaTime) => { + const dt = deltaTime / FRAME_TIME; + // update crosshair position in all modes except paused if (state.value !== "paused") { if (horizontalFirst) { - if (x !== targetX) x = moveTowards(x, targetX); - else y = moveTowards(y, targetY); + if (x !== targetX) x = moveTowards(x, targetX, dt); + else y = moveTowards(y, targetY, dt); } else { - if (y !== targetY) y = moveTowards(y, targetY); - else x = moveTowards(x, targetX); + if (y !== targetY) y = moveTowards(y, targetY, dt); + else x = moveTowards(x, targetX, dt); } } @@ -320,15 +323,15 @@ onRender((ctx) => { const { shrinkSpeed, spawnInterval } = getDifficulty(); // spawn circles - spawnTimer++; - if (spawnTimer >= spawnInterval) { + spawnTimer += deltaTime; + if (spawnTimer >= spawnInterval * FRAME_TIME) { spawnCircle(); spawnTimer = 0; } // update circles and rings circles = circles.filter((circle) => { - circle.radius -= shrinkSpeed; + circle.radius -= shrinkSpeed * dt; if (circle.radius < MIN_RADIUS) { lives--; if (lives <= 0) { @@ -341,8 +344,8 @@ onRender((ctx) => { }); rings = rings.filter((ring) => { - ring.radius += RING_EXPAND_SPEED; - ring.alpha -= RING_FADE_SPEED; + ring.radius += RING_EXPAND_SPEED * dt; + ring.alpha -= RING_FADE_SPEED * dt; return ring.alpha > 0; }); } diff --git a/app/composables/useConfetti.ts b/app/composables/useConfetti.ts index bd4c575..2f99089 100644 --- a/app/composables/useConfetti.ts +++ b/app/composables/useConfetti.ts @@ -1,21 +1,22 @@ const PARTICLE_COUNT = 400; -const SPAWN_DURATION = 700; -const VX_RANGE = 0.5; -const VY_MIN = 0.3; -const VY_RANGE = 0.2; +const SPAWN_DURATION = 350; +const VX_RANGE = 1.0; +const VY_MIN = 0.6; +const VY_RANGE = 0.4; 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 ROTATION_SPEED_RANGE = 0.3; +const DRIFT_STRENGTH = 0.06; +const GRAVITY = 0.014; +const VX_DAMPING = 0.988; +const SWAY_AMPLITUDE = 0.6; +const SWAY_SPEED_MIN = 0.04; +const SWAY_SPEED_RANGE = 0.06; const OFFSCREEN_MARGIN = 10; const TOTAL_HEIGHT = 192 * 2; +const FRAME_TIME = 1000 / 60; const COLORS = [ "#fb0018", @@ -41,7 +42,7 @@ export type ConfettiParticle = { }; type Spawner = { - frame: number; + elapsed: number; accumulator: number; particleCount: number; duration: number; @@ -67,18 +68,25 @@ const spawnParticle = () => { }; const spawn = (particleCount = PARTICLE_COUNT, duration = SPAWN_DURATION) => { - spawners.push({ frame: 0, accumulator: 0, particleCount, duration }); + spawners.push({ + elapsed: 0, + accumulator: 0, + particleCount, + duration: duration * FRAME_TIME, + }); }; -const update = () => { +const update = (deltaTime: number) => { + const dt = deltaTime / FRAME_TIME; + for (let s = spawners.length - 1; s >= 0; s -= 1) { const spawner = spawners[s]!; - if (spawner.frame < spawner.duration) { - const progress = spawner.frame / spawner.duration; + if (spawner.elapsed < spawner.duration) { + const progress = spawner.elapsed / spawner.duration; const inv = 1 - progress; const rate = inv * inv; spawner.accumulator += - rate * (spawner.particleCount / spawner.duration) * 3; + rate * (spawner.particleCount / spawner.duration) * 3 * deltaTime; const count = Math.floor(spawner.accumulator); spawner.accumulator -= count; @@ -87,7 +95,7 @@ const update = () => { spawnParticle(); } - spawner.frame += 1; + spawner.elapsed += deltaTime; } else { spawners.splice(s, 1); } @@ -95,13 +103,13 @@ const update = () => { 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; + p.vy += GRAVITY * dt; + p.vx += (Math.random() - 0.5) * DRIFT_STRENGTH * dt; + p.vx *= Math.pow(VX_DAMPING, dt); + p.swayPhase += p.swaySpeed * dt; + p.x += (p.vx + Math.sin(p.swayPhase) * SWAY_AMPLITUDE) * dt; + p.y += p.vy * dt; + p.rotation += p.rotationSpeed * dt; if (p.y > TOTAL_HEIGHT + OFFSCREEN_MARGIN) { particles.splice(i, 1); diff --git a/app/stores/achievements.ts b/app/stores/achievements.ts index a4e88b8..cc4082e 100644 --- a/app/stores/achievements.ts +++ b/app/stores/achievements.ts @@ -66,7 +66,7 @@ export const useAchievementsStore = defineStore("achievements", () => { if (storage.value.unlocked.length === ACHIEVEMENTS.length) { confetti.spawn(); } else { - confetti.spawn(50, 350); + confetti.spawn(50, 175); } return true;