151 lines
3.7 KiB
Vue
151 lines
3.7 KiB
Vue
<script setup lang="ts">
|
|
import gsap from "gsap";
|
|
|
|
const store = useProjectsStore();
|
|
|
|
const { assets } = useAssets();
|
|
|
|
const CLICK_RADIUS = 22;
|
|
|
|
const BUTTONS = {
|
|
prev: {
|
|
position: [36, 100],
|
|
image: assets.projects.bottomScreen.prevPressed,
|
|
},
|
|
quit: {
|
|
position: [88, 156],
|
|
image: assets.projects.bottomScreen.quitPressed,
|
|
},
|
|
link: {
|
|
position: [168, 156],
|
|
image: assets.projects.bottomScreen.linkPressed,
|
|
},
|
|
next: {
|
|
position: [220, 100],
|
|
image: assets.projects.bottomScreen.nextPressed,
|
|
},
|
|
} as const satisfies Record<
|
|
string,
|
|
{ position: Point; image: HTMLImageElement }
|
|
>;
|
|
|
|
type ButtonType = keyof typeof BUTTONS;
|
|
|
|
type ButtonAnimation = {
|
|
type: ButtonType;
|
|
position: Point;
|
|
showButton: boolean;
|
|
showSmallCircle: boolean;
|
|
showBigCircle: boolean;
|
|
};
|
|
|
|
let currentAnimation: ButtonAnimation | null = null;
|
|
|
|
const circleContains = (
|
|
[cx, cy]: Point,
|
|
[x, y]: Point,
|
|
radius: number,
|
|
): boolean => Math.sqrt(Math.pow(x - cx, 2) + Math.pow(y - cy, 2)) < radius;
|
|
|
|
const startButtonAnimation = (type: ButtonType) => {
|
|
const anim: ButtonAnimation = {
|
|
type,
|
|
position: BUTTONS[type].position,
|
|
showButton: true,
|
|
showSmallCircle: true,
|
|
showBigCircle: false,
|
|
};
|
|
currentAnimation = anim;
|
|
|
|
const SMALL_CIRCLE_DURATION = 0.07;
|
|
const BIG_CIRCLE_DURATION = 0.09;
|
|
const POST_DURATION = 0.07;
|
|
|
|
gsap
|
|
.timeline()
|
|
.to(anim, { duration: SMALL_CIRCLE_DURATION })
|
|
.call(() => {
|
|
anim.showSmallCircle = false;
|
|
anim.showBigCircle = true;
|
|
})
|
|
.to(anim, { duration: BIG_CIRCLE_DURATION })
|
|
.call(() => {
|
|
anim.showBigCircle = false;
|
|
})
|
|
.to(anim, { duration: POST_DURATION })
|
|
.call(() => {
|
|
currentAnimation = null;
|
|
});
|
|
};
|
|
|
|
useScreenClick((x, y) => {
|
|
if (currentAnimation || store.isIntro || store.isOutro) return;
|
|
|
|
const project = store.projects[store.currentProject];
|
|
if (circleContains(BUTTONS.prev.position, [x, y], CLICK_RADIUS)) {
|
|
startButtonAnimation("prev");
|
|
store.scrollProjects("left");
|
|
} else if (circleContains(BUTTONS.next.position, [x, y], CLICK_RADIUS)) {
|
|
startButtonAnimation("next");
|
|
store.scrollProjects("right");
|
|
} else if (circleContains(BUTTONS.quit.position, [x, y], CLICK_RADIUS)) {
|
|
startButtonAnimation("quit");
|
|
store.animateOutro();
|
|
} else if (
|
|
circleContains(BUTTONS.link.position, [x, y], CLICK_RADIUS) &&
|
|
project?.link
|
|
) {
|
|
startButtonAnimation("link");
|
|
// TODO: show confirmation popup before opening the link, like "you are about to navigate to [...]"
|
|
// store.visitProject();
|
|
}
|
|
});
|
|
|
|
useRender((ctx) => {
|
|
if (currentAnimation?.showButton) {
|
|
const image = BUTTONS[currentAnimation.type].image;
|
|
ctx.drawImage(
|
|
image!,
|
|
currentAnimation.position[0] - 14,
|
|
currentAnimation.position[1] - 14,
|
|
);
|
|
}
|
|
|
|
if (currentAnimation?.showSmallCircle) {
|
|
ctx.drawImage(
|
|
assets.projects.bottomScreen.circleSmall,
|
|
currentAnimation.position[0] - 28,
|
|
currentAnimation.position[1] - 28,
|
|
);
|
|
}
|
|
|
|
if (currentAnimation?.showBigCircle) {
|
|
ctx.drawImage(
|
|
assets.projects.bottomScreen.circleBig,
|
|
currentAnimation.position[0] - 44,
|
|
currentAnimation.position[1] - 44,
|
|
);
|
|
}
|
|
|
|
ctx.fillStyle = `rgba(0, 0, 0, ${store.isIntro ? store.intro.fadeOpacity : store.isOutro ? store.outro.fadeOpacity : 0})`;
|
|
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
|
});
|
|
useKeyDown((key) => {
|
|
if (currentAnimation || store.isIntro || store.isOutro) return;
|
|
switch (key) {
|
|
case "NDS_LEFT":
|
|
startButtonAnimation("prev");
|
|
store.scrollProjects("left");
|
|
break;
|
|
case "NDS_RIGHT":
|
|
startButtonAnimation("next");
|
|
store.scrollProjects("right");
|
|
break;
|
|
}
|
|
});
|
|
|
|
defineOptions({
|
|
render: () => null,
|
|
});
|
|
</script>
|