feat(settings/touchScreen/tapTap): intro and outro animations

This commit is contained in:
2026-02-06 15:48:16 +01:00
parent 7ae46508b2
commit e95aec0b7d

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { useLocalStorage } from "@vueuse/core";
import gsap from "gsap";
const store = useSettingsStore();
const achievements = useAchievementsStore();
@@ -65,6 +66,58 @@ const highScore = useLocalStorage("taptap_high_score", 0);
let score = 0;
let isNewBest = false;
const AREA_FADE_DURATION = 0.2;
const SCORE_OFFSET = -20;
const SCORE_DURATION = 0.15;
const animation = reactive({
areaOpacity: 0,
circlesOpacity: 1,
scoreOffsetY: SCORE_OFFSET,
});
const animateIntro = async () => {
await gsap
.timeline()
.to(
animation,
{ areaOpacity: 1, duration: AREA_FADE_DURATION, ease: "none" },
0,
)
.to(
animation,
{ scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
AREA_FADE_DURATION,
);
};
const animateOutro = async () => {
targetX = 0;
targetY = LOGICAL_HEIGHT * 2 - 20;
await gsap
.timeline()
.to(
animation,
{ areaOpacity: 0, duration: AREA_FADE_DURATION, ease: "none" },
0,
)
.to(
animation,
{ circlesOpacity: 0, duration: AREA_FADE_DURATION, ease: "none" },
0,
)
.to(
animation,
{ scoreOffsetY: SCORE_OFFSET, duration: SCORE_DURATION, ease: "none" },
0,
);
};
onMounted(() => {
animateIntro();
});
const handleActivateB = () => {
if (state.value === "playing") {
state.value = "paused";
@@ -72,8 +125,9 @@ const handleActivateB = () => {
text: $t("settings.touchScreen.tapTap.quitConfirmation"),
bLabel: $t("common.no"),
aLabel: $t("common.yes"),
onClosed: (choice) => {
onClosed: async (choice) => {
if (choice === "confirm") {
await animateOutro();
store.closeSubMenu();
} else {
state.value = "playing";
@@ -81,11 +135,11 @@ const handleActivateB = () => {
},
});
} else {
store.closeSubMenu();
animateOutro().then(() => store.closeSubMenu());
}
};
const handleActivateA = () => {
const handleActivateA = async () => {
if (state.value === "playing") {
state.value = "paused";
confirmationModal.open({
@@ -100,6 +154,13 @@ const handleActivateA = () => {
}
},
});
} else if (state.value === "waiting") {
await gsap.to(animation, {
areaOpacity: 0,
duration: AREA_FADE_DURATION,
ease: "none",
});
resetGame();
} else {
resetGame();
}
@@ -163,10 +224,11 @@ const showDeathScreen = () => {
text,
bLabel: $t("common.quit"),
aLabel: $t("common.restart"),
onClosed: (choice) => {
onClosed: async (choice) => {
if (choice === "confirm") {
resetGame();
} else {
await animateOutro();
store.closeSubMenu();
}
},
@@ -254,6 +316,8 @@ onRender((ctx) => {
// draw start instructions
if (state.value === "waiting") {
ctx.save();
ctx.globalAlpha = animation.areaOpacity;
ctx.fillStyle = "#fbfbfb";
ctx.textBaseline = "top";
ctx.fillRect(32, 112, 191, 31);
@@ -267,11 +331,13 @@ onRender((ctx) => {
123,
191,
);
ctx.restore();
}
if (state.value !== "waiting") {
// draw circles and rings
for (const circle of circles) {
ctx.globalAlpha = animation.circlesOpacity;
ctx.fillStyle = "#fb0000";
fillCirclePixelated(ctx, circle.x, circle.y, circle.radius);
@@ -280,7 +346,7 @@ onRender((ctx) => {
}
for (const ring of rings) {
ctx.globalAlpha = ring.alpha;
ctx.globalAlpha = ring.alpha * animation.circlesOpacity;
ctx.fillStyle = "#fb0000";
strokeCirclePixelated(
ctx,
@@ -310,25 +376,26 @@ onRender((ctx) => {
}, 0);
onRender((ctx) => {
ctx.translate(0, animation.scoreOffsetY);
drawButton(
ctx,
$t("settings.touchScreen.tapTap.score", { score }),
10,
3,
2,
88,
);
drawButton(
ctx,
$t("settings.touchScreen.tapTap.best", { best: highScore.value }),
108,
3,
2,
88,
);
drawButton(
ctx,
lives > 0 ? ICONS.HEART.repeat(lives) : ICONS.SAD,
206,
3,
2,
40,
);
}, 110);