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