feat(projects): confirmation popup before opening link
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Background from "./Background.vue";
|
import Background from "./Background.vue";
|
||||||
import Buttons from "./Buttons.vue";
|
import Buttons from "./Buttons.vue";
|
||||||
|
import LinkConfirmationPopup from "./LinkConfirmationPopup.vue";
|
||||||
|
|
||||||
const store = useProjectsStore();
|
const store = useProjectsStore();
|
||||||
|
|
||||||
@@ -23,5 +24,6 @@ watch(
|
|||||||
<template v-if="!store.loading">
|
<template v-if="!store.loading">
|
||||||
<Background />
|
<Background />
|
||||||
<Buttons />
|
<Buttons />
|
||||||
|
<LinkConfirmationPopup v-if="store.showConfirmationPopup" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ const circleContains = (
|
|||||||
radius: number,
|
radius: number,
|
||||||
): boolean => Math.sqrt(Math.pow(x - cx, 2) + Math.pow(y - cy, 2)) < radius;
|
): boolean => Math.sqrt(Math.pow(x - cx, 2) + Math.pow(y - cy, 2)) < radius;
|
||||||
|
|
||||||
|
const SMALL_CIRCLE_DURATION = 0.07;
|
||||||
|
const BIG_CIRCLE_DURATION = 0.09;
|
||||||
|
const POST_DURATION = 0.07;
|
||||||
|
|
||||||
const startButtonAnimation = (type: ButtonType) => {
|
const startButtonAnimation = (type: ButtonType) => {
|
||||||
const anim: ButtonAnimation = {
|
const anim: ButtonAnimation = {
|
||||||
type,
|
type,
|
||||||
@@ -57,10 +61,6 @@ const startButtonAnimation = (type: ButtonType) => {
|
|||||||
};
|
};
|
||||||
currentAnimation = anim;
|
currentAnimation = anim;
|
||||||
|
|
||||||
const SMALL_CIRCLE_DURATION = 0.07;
|
|
||||||
const BIG_CIRCLE_DURATION = 0.09;
|
|
||||||
const POST_DURATION = 0.07;
|
|
||||||
|
|
||||||
gsap
|
gsap
|
||||||
.timeline()
|
.timeline()
|
||||||
.to(anim, { duration: SMALL_CIRCLE_DURATION })
|
.to(anim, { duration: SMALL_CIRCLE_DURATION })
|
||||||
@@ -79,9 +79,14 @@ const startButtonAnimation = (type: ButtonType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useScreenClick((x, y) => {
|
useScreenClick((x, y) => {
|
||||||
if (currentAnimation || store.isIntro || store.isOutro) return;
|
if (
|
||||||
|
currentAnimation ||
|
||||||
|
store.isIntro ||
|
||||||
|
store.isOutro ||
|
||||||
|
store.showConfirmationPopup
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
const project = store.projects[store.currentProject];
|
|
||||||
if (circleContains(BUTTONS.prev.position, [x, y], CLICK_RADIUS)) {
|
if (circleContains(BUTTONS.prev.position, [x, y], CLICK_RADIUS)) {
|
||||||
startButtonAnimation("prev");
|
startButtonAnimation("prev");
|
||||||
store.scrollProjects("left");
|
store.scrollProjects("left");
|
||||||
@@ -91,13 +96,12 @@ useScreenClick((x, y) => {
|
|||||||
} else if (circleContains(BUTTONS.quit.position, [x, y], CLICK_RADIUS)) {
|
} else if (circleContains(BUTTONS.quit.position, [x, y], CLICK_RADIUS)) {
|
||||||
startButtonAnimation("quit");
|
startButtonAnimation("quit");
|
||||||
store.animateOutro();
|
store.animateOutro();
|
||||||
} else if (
|
} else if (circleContains(BUTTONS.link.position, [x, y], CLICK_RADIUS)) {
|
||||||
circleContains(BUTTONS.link.position, [x, y], CLICK_RADIUS) &&
|
|
||||||
project?.link
|
|
||||||
) {
|
|
||||||
startButtonAnimation("link");
|
startButtonAnimation("link");
|
||||||
// TODO: show confirmation popup before opening the link, like "you are about to navigate to [...]"
|
setTimeout(
|
||||||
// store.visitProject();
|
() => (store.showConfirmationPopup = true),
|
||||||
|
1000 * (SMALL_CIRCLE_DURATION + BIG_CIRCLE_DURATION + POST_DURATION),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,7 +135,13 @@ useRender((ctx) => {
|
|||||||
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||||
});
|
});
|
||||||
useKeyDown((key) => {
|
useKeyDown((key) => {
|
||||||
if (currentAnimation || store.isIntro || store.isOutro) return;
|
if (
|
||||||
|
currentAnimation ||
|
||||||
|
store.isIntro ||
|
||||||
|
store.isOutro ||
|
||||||
|
store.showConfirmationPopup
|
||||||
|
)
|
||||||
|
return;
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "NDS_LEFT":
|
case "NDS_LEFT":
|
||||||
startButtonAnimation("prev");
|
startButtonAnimation("prev");
|
||||||
@@ -141,6 +151,14 @@ useKeyDown((key) => {
|
|||||||
startButtonAnimation("next");
|
startButtonAnimation("next");
|
||||||
store.scrollProjects("right");
|
store.scrollProjects("right");
|
||||||
break;
|
break;
|
||||||
|
case "NDS_A":
|
||||||
|
case "NDS_START":
|
||||||
|
startButtonAnimation("link");
|
||||||
|
setTimeout(
|
||||||
|
() => (store.showConfirmationPopup = true),
|
||||||
|
1000 * (SMALL_CIRCLE_DURATION + BIG_CIRCLE_DURATION + POST_DURATION),
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
157
app/components/Projects/BottomScreen/LinkConfirmationPopup.vue
Normal file
157
app/components/Projects/BottomScreen/LinkConfirmationPopup.vue
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import gsap from "gsap";
|
||||||
|
|
||||||
|
const { assets } = useAssets();
|
||||||
|
const store = useProjectsStore();
|
||||||
|
|
||||||
|
const TEXT_SLOW = 0.05;
|
||||||
|
const TEXT_FAST = 0.01;
|
||||||
|
const TEXT_Y = 151;
|
||||||
|
const YES_Y = 107;
|
||||||
|
const NO_Y = 123;
|
||||||
|
|
||||||
|
let selectedOption: "yes" | "no" = "yes";
|
||||||
|
const textProgress = ref(0);
|
||||||
|
let waitingForNdsARelease = false;
|
||||||
|
let timeline: gsap.core.Timeline | null = null;
|
||||||
|
|
||||||
|
const text = computed(() => {
|
||||||
|
const project = store.projects[store.currentProject]!;
|
||||||
|
return $t("projects.linkConformationPopup.text", {
|
||||||
|
url: project.link.replace(/^https?:\/\//, ""),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const animateText = (speed: number) => {
|
||||||
|
timeline?.kill();
|
||||||
|
const remaining = (1 - textProgress.value) * text.value.length;
|
||||||
|
timeline = gsap.timeline().to(textProgress, {
|
||||||
|
value: 1,
|
||||||
|
duration: remaining * speed,
|
||||||
|
ease: "none",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
animateText(TEXT_SLOW);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => timeline?.kill());
|
||||||
|
|
||||||
|
useKeyDown((key) => {
|
||||||
|
if (!store.showConfirmationPopup) return;
|
||||||
|
|
||||||
|
if (textProgress.value < 1 && key === "NDS_A") {
|
||||||
|
waitingForNdsARelease = true;
|
||||||
|
animateText(TEXT_FAST);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textProgress.value < 1 || waitingForNdsARelease) return;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "NDS_UP":
|
||||||
|
selectedOption = "yes";
|
||||||
|
break;
|
||||||
|
case "NDS_DOWN":
|
||||||
|
selectedOption = "no";
|
||||||
|
break;
|
||||||
|
case "NDS_A":
|
||||||
|
case "NDS_START":
|
||||||
|
setTimeout(() => {
|
||||||
|
if (selectedOption === "yes") store.visitProject();
|
||||||
|
store.showConfirmationPopup = false;
|
||||||
|
}, 5);
|
||||||
|
break;
|
||||||
|
case "NDS_B":
|
||||||
|
setTimeout(() => {
|
||||||
|
store.showConfirmationPopup = false;
|
||||||
|
}, 5);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyUp((key) => {
|
||||||
|
if (store.showConfirmationPopup && key === "NDS_A") {
|
||||||
|
waitingForNdsARelease = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useScreenClick((x, y) => {
|
||||||
|
if (
|
||||||
|
!store.showConfirmationPopup ||
|
||||||
|
textProgress.value < 1 ||
|
||||||
|
waitingForNdsARelease
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const ACTIVATION_DELAY = 50;
|
||||||
|
|
||||||
|
if (rectContains([198, 105, 50, 14], [x, y])) {
|
||||||
|
selectedOption = "yes";
|
||||||
|
setTimeout(() => {
|
||||||
|
store.visitProject();
|
||||||
|
store.showConfirmationPopup = false;
|
||||||
|
}, ACTIVATION_DELAY);
|
||||||
|
} else if (rectContains([198, 121, 50, 14], [x, y])) {
|
||||||
|
selectedOption = "no";
|
||||||
|
setTimeout(() => (store.showConfirmationPopup = false), ACTIVATION_DELAY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const drawTextWithShadow = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
text: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
) => {
|
||||||
|
ctx.fillStyle = "#a2a2aa";
|
||||||
|
ctx.fillText(text, x + 1, y);
|
||||||
|
ctx.fillText(text, x + 1, y + 1);
|
||||||
|
ctx.fillText(text, x, y + 1);
|
||||||
|
ctx.fillStyle = "#515159";
|
||||||
|
ctx.fillText(text, x, y);
|
||||||
|
};
|
||||||
|
|
||||||
|
useRender((ctx) => {
|
||||||
|
if (!store.showConfirmationPopup) return;
|
||||||
|
|
||||||
|
// frame
|
||||||
|
ctx.strokeStyle = "#0000007f";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.strokeRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||||
|
|
||||||
|
// text
|
||||||
|
ctx.drawImage(assets.projects.bottomScreen.popupTextBackground, 2, 146);
|
||||||
|
ctx.font = "16px Pokemon DP Pro";
|
||||||
|
ctx.textBaseline = "top";
|
||||||
|
const displayedText = text.value.slice(
|
||||||
|
0,
|
||||||
|
Math.floor(textProgress.value * text.value.length),
|
||||||
|
);
|
||||||
|
drawTextWithShadow(ctx, displayedText, 15, TEXT_Y);
|
||||||
|
|
||||||
|
// choice box
|
||||||
|
if (textProgress.value >= 1) {
|
||||||
|
ctx.drawImage(assets.projects.bottomScreen.popupChoiceBackground, 194, 98);
|
||||||
|
|
||||||
|
const y = selectedOption === "yes" ? YES_Y : NO_Y;
|
||||||
|
ctx.drawImage(assets.projects.bottomScreen.popupSelector, 200, y);
|
||||||
|
|
||||||
|
drawTextWithShadow(
|
||||||
|
ctx,
|
||||||
|
$t("projects.linkConformationPopup.yes").toUpperCase(),
|
||||||
|
207,
|
||||||
|
YES_Y - 3,
|
||||||
|
);
|
||||||
|
drawTextWithShadow(
|
||||||
|
ctx,
|
||||||
|
$t("projects.linkConformationPopup.no").toUpperCase(),
|
||||||
|
207,
|
||||||
|
NO_Y - 3,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineOptions({ render: () => null });
|
||||||
|
</script>
|
||||||
@@ -24,6 +24,7 @@ export const useProjectsStore = defineStore("projects", {
|
|||||||
|
|
||||||
isIntro: true,
|
isIntro: true,
|
||||||
isOutro: false,
|
isOutro: false,
|
||||||
|
showConfirmationPopup: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|||||||
@@ -32,5 +32,12 @@
|
|||||||
"title": "Touch Screen",
|
"title": "Touch Screen",
|
||||||
"description": "TODO"
|
"description": "TODO"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"linkConformationPopup": {
|
||||||
|
"yes": "yes",
|
||||||
|
"no": "no",
|
||||||
|
"text": "Open {url}?"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 90 B |
BIN
public/images/projects/bottom-screen/popup-selector.webp
Normal file
BIN
public/images/projects/bottom-screen/popup-selector.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 B |
BIN
public/images/projects/bottom-screen/popup-text-background.webp
Normal file
BIN
public/images/projects/bottom-screen/popup-text-background.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 B |
Reference in New Issue
Block a user