Files
pihkaal-me/app/components/NDS2D.vue
2026-02-19 19:30:33 +01:00

627 lines
12 KiB
Vue

<script setup lang="ts">
import gsap from "gsap";
const ndsScale = ref(1);
const showHelp = ref(false);
const helpTimeline = ref<gsap.core.Timeline | null>(null);
const updateScale = () => {
const scaleX = (window.innerWidth - 40) / 235;
const scaleY = (window.innerHeight - 40) / 431;
ndsScale.value = Math.min(scaleX, scaleY);
};
const animateHelpLabels = async (yoyo = false) => {
if (showHelp.value) return;
showHelp.value = true;
helpTimeline.value?.kill();
await nextTick();
const timeline = gsap
.timeline()
.fromTo(
".nds2d-hints-container",
{ opacity: 0 },
{ opacity: 1, duration: 0.2, ease: "power1.out" },
)
.to(
".nds2d-help-btn",
{ color: "#ffffff", opacity: 1, duration: 0.2, ease: "power1.out" },
"<",
)
.to(".nds2d-hints-container", {
opacity: 0,
duration: 0.2,
ease: "power1.in",
delay: 3,
})
.to(
".nds2d-help-btn",
{ color: "#666666", opacity: 0.5, duration: 0.2, ease: "power1.in" },
"<",
)
.call(() => {
showHelp.value = false;
});
if (yoyo) {
timeline.to(".nds2d-help-btn", {
color: "#ffffff",
opacity: 1,
duration: 0.3,
repeat: 5,
yoyo: true,
delay: 0.3,
});
}
helpTimeline.value = timeline;
};
onMounted(async () => {
updateScale();
window.addEventListener("resize", updateScale);
if (ndsScale.value >= 1) {
await animateHelpLabels(true);
}
});
useKeyDown(async ({ key }) => {
if (key.toLocaleLowerCase() === "h") {
await animateHelpLabels();
}
});
onUnmounted(() => {
window.removeEventListener("resize", updateScale);
});
</script>
<template>
<div class="nds2d-container">
<div class="nds2d" :style="{ scale: ndsScale }">
<div class="nds2d-top-screen">
<div class="nds2d-speaker-hole nds2d-sh1"></div>
<div class="nds2d-speaker-hole nds2d-sh2"></div>
<div class="nds2d-screen nds2d-screen-top">
<slot name="topScreen" />
</div>
<div class="nds2d-speaker-hole nds2d-sh3"></div>
<div class="nds2d-speaker-hole nds2d-sh4"></div>
</div>
<div class="nds2d-hinge">
<div class="nds2d-mic"></div>
<div class="nds2d-light"></div>
</div>
<div class="nds2d-bottom-screen">
<div class="nds2d-screen nds2d-screen-bottom">
<slot name="bottomScreen" />
</div>
<div class="nds2d-d-pad-shadow"></div>
<div class="nds2d-d-pad">
<div class="nds2d-d-pad-base"></div>
<div class="nds2d-d-pad-light"></div>
<div class="nds2d-d-pad-tip nds2d-tip-up"></div>
<div class="nds2d-d-pad-tip nds2d-tip-down"></div>
<div class="nds2d-d-pad-tip nds2d-tip-left"></div>
<div class="nds2d-d-pad-tip nds2d-tip-right"></div>
<div class="nds2d-d-pad-marker nds2d-dpm-up"></div>
<div class="nds2d-d-pad-marker nds2d-dpm-down"></div>
<div class="nds2d-d-pad-marker nds2d-dpm-left"></div>
<div class="nds2d-d-pad-marker nds2d-dpm-right"></div>
</div>
<div class="nds2d-button nds2d-btn-x">X</div>
<div class="nds2d-button nds2d-btn-a">A</div>
<div class="nds2d-button nds2d-btn-b">B</div>
<div class="nds2d-button nds2d-btn-y">Y</div>
<div class="nds2d-small-button nds2d-start"></div>
<div class="nds2d-small-button nds2d-select"></div>
<div v-if="showHelp" class="nds2d-hints-container">
<div class="nds2d-hint nds2d-hint-dpad">Arrows</div>
<div class="nds2d-hint nds2d-hint-x">{{ mapNDSToKey("X") }}</div>
<div class="nds2d-hint nds2d-hint-a">{{ mapNDSToKey("A") }}</div>
<div class="nds2d-hint nds2d-hint-b">{{ mapNDSToKey("B") }}</div>
<div class="nds2d-hint nds2d-hint-y">{{ mapNDSToKey("Y") }}</div>
<div class="nds2d-hint nds2d-hint-start">
{{ mapNDSToKey("START") }}
</div>
<div class="nds2d-hint nds2d-hint-select">
{{ mapNDSToKey("SELECT") }}
</div>
</div>
</div>
</div>
<button class="nds2d-help-btn" @click="animateHelpLabels()">?</button>
</div>
</template>
<style scoped lang="css">
.nds2d-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
background: #181818;
}
.nds2d {
--primary: #1a1a1a;
--shadow: #111;
--black: #050505;
width: 414px;
height: 431px;
position: absolute;
}
.nds2d-top-screen {
position: absolute;
width: 100%;
height: 46.5%;
border-radius: 15px 15px 5px 5px;
background: var(--primary);
box-shadow:
inset 10px 10px 12px -2px var(--shadow),
inset -2px 5px 3px 1px var(--shadow),
inset -5px 5px 5px 2px #444,
0px -1px 2px 2px #111;
}
.nds2d-screen {
position: absolute;
width: 51.5%;
height: 83.5%;
border-radius: 3px;
top: 9.5%;
left: 24%;
box-shadow: 0px 0px 3px 0px #111;
}
.nds2d-screen :deep(canvas) {
position: absolute;
width: 91%;
height: 87%;
left: 4%;
top: 6%;
box-shadow: 0px 0px 1px 2px rgba(0, 0, 0, 0.5);
border: none !important;
}
.nds2d-speaker-hole {
position: absolute;
width: 5px;
height: 5px;
background: var(--black);
border-radius: 2.5px;
}
.nds2d-speaker-hole::after {
position: absolute;
content: "";
height: 5px;
width: 5px;
border-radius: 2.5px;
background: var(--black);
left: 16px;
}
.nds2d-speaker-hole::before {
position: absolute;
content: "";
height: 5px;
width: 5px;
border-radius: 2.5px;
background: var(--black);
left: 31px;
}
.nds2d-sh1 {
left: 30px;
top: 94px;
}
.nds2d-sh2 {
left: 30px;
top: 110px;
}
.nds2d-sh3 {
left: 346px;
top: 95px;
}
.nds2d-sh4 {
left: 346px;
top: 110px;
}
.nds2d-hinge {
position: absolute;
width: 99.5%;
top: 46.5%;
height: 28px;
border-radius: 25px;
z-index: 3;
background: linear-gradient(
0deg,
rgba(8, 8, 8) 0%,
rgba(20, 20, 20, 1) 10%,
rgba(32, 32, 32, 1) 65%,
rgba(20, 20, 20, 1) 100%
);
box-shadow: 0px 5px 10px -1px #080808;
}
.nds2d-hinge::after {
position: absolute;
content: "";
width: 2px;
height: 100%;
background: #222;
left: 49px;
}
.nds2d-hinge::before {
position: absolute;
content: "";
width: 2px;
height: 100%;
background: #222;
left: 364px;
}
.nds2d-mic {
position: absolute;
width: 4px;
height: 10px;
border-radius: 2px;
background: var(--black);
left: 49.5%;
top: 8px;
}
.nds2d-light {
position: absolute;
height: 67%;
width: 4px;
top: 12%;
border-radius: 10px;
right: 25px;
background: linear-gradient(
0deg,
rgba(40, 40, 40, 1) 0%,
rgba(80, 80, 80, 1) 65%,
rgba(40, 40, 40, 1) 100%
);
}
.nds2d-light::after {
content: "";
position: absolute;
height: 100%;
width: 4px;
top: 0;
border-radius: 10px;
left: 8px;
background: linear-gradient(
0deg,
rgba(92, 107, 73, 1) 0%,
rgba(211, 212, 183, 1) 65%,
rgba(123, 142, 98, 1) 100%
);
}
.nds2d-bottom-screen {
position: absolute;
width: 99.5%;
height: 50%;
bottom: 0;
border-radius: 3px 3px 14px 14px;
background: var(--primary);
box-shadow: 0px 1px 2px 2px #111;
}
.nds2d-bottom-screen .nds2d-screen {
top: 24px;
height: 168px;
width: 52%;
background: var(--primary);
z-index: 1;
}
.nds2d-bottom-screen .nds2d-screen::before {
content: "";
position: absolute;
width: 102%;
height: 109%;
left: -1%;
top: -7%;
z-index: -1;
border-radius: 5px;
box-shadow:
10px 7px 10px 3px #111,
-6px 4px 5px 2px var(--primary),
-12px 4px 8px 3px #333;
}
.nds2d-d-pad-shadow {
position: absolute;
left: 19px;
top: 61px;
width: 57px;
height: 57px;
background: var(--shadow);
filter: drop-shadow(0 3px 2px #111);
clip-path: path(
"M 22.5 0 Q 19 0 19 3.5 L 19 19 L 3.5 19 Q 0 19 0 22.5 L 0 34.5 Q 0 38 3.5 38 L 19 38 L 19 53.5 Q 19 57 22.5 57 L 34.5 57 Q 38 57 38 53.5 L 38 38 L 53.5 38 Q 57 38 57 34.5 L 57 22.5 Q 57 19 53.5 19 L 38 19 L 38 3.5 Q 38 0 34.5 0 Z"
);
}
.nds2d-d-pad {
position: absolute;
left: 20px;
top: 62px;
width: 55px;
height: 55px;
clip-path: path(
"M 22 0 Q 19 0 19 3 L 19 19 L 3 19 Q 0 19 0 22 L 0 33 Q 0 36 3 36 L 19 36 L 19 52 Q 19 55 22 55 L 33 55 Q 36 55 36 52 L 36 36 L 52 36 Q 55 36 55 33 L 55 22 Q 55 19 52 19 L 36 19 L 36 3 Q 36 0 33 0 Z"
);
}
.nds2d-d-pad-base {
position: absolute;
width: 100%;
height: 100%;
background: #333;
}
.nds2d-d-pad-light {
position: absolute;
width: 100%;
height: 100%;
box-shadow: inset 2px 2px 3px -1px #444;
}
.nds2d-d-pad-tip {
position: absolute;
}
.nds2d-tip-up {
left: 19px;
top: 0;
width: 17px;
height: 19px;
box-shadow: inset 0 2px 3px -1px #444;
}
.nds2d-tip-down {
left: 19px;
bottom: 0;
width: 17px;
height: 19px;
box-shadow: inset 0 -2px 3px -1px #444;
}
.nds2d-tip-left {
left: 0;
top: 19px;
width: 19px;
height: 17px;
box-shadow: inset 2px 0 3px -1px #444;
}
.nds2d-tip-right {
right: 0;
top: 19px;
width: 19px;
height: 17px;
box-shadow: inset -2px 0 3px -1px #444;
}
.nds2d-d-pad-marker {
position: absolute;
background: #999;
}
.nds2d-dpm-up,
.nds2d-dpm-down {
width: 2px;
height: 11px;
left: 26px;
}
.nds2d-dpm-up {
top: 5px;
}
.nds2d-dpm-down {
bottom: 5px;
}
.nds2d-dpm-left,
.nds2d-dpm-right {
width: 11px;
height: 2px;
top: 26px;
}
.nds2d-dpm-left {
left: 5px;
}
.nds2d-dpm-right {
right: 5px;
}
/* Buttons */
.nds2d-button {
user-select: none;
position: absolute;
width: 21px;
height: 21px;
padding-top: 1px;
border: 1px solid var(--shadow);
border-radius: 50%;
z-index: 5;
font-size: 0.7rem;
font-weight: 400;
background: #333;
color: #999;
box-shadow:
3px 2px 3px -2px #111,
inset 2px 2px 3px -1px #444;
}
.nds2d-btn-x {
right: 35px;
top: 45px;
}
.nds2d-btn-a {
right: 12px;
top: 69px;
}
.nds2d-btn-y {
right: 59px;
top: 69px;
}
.nds2d-btn-b {
right: 35px;
top: 92px;
}
.nds2d-small-button {
user-select: none;
position: absolute;
width: 11px;
height: 11px;
border: 1px solid var(--shadow);
border-radius: 50%;
bottom: 53px;
right: 70px;
background: #333;
box-shadow:
3px 2px 3px -2px #111,
inset 2px 2px 3px -1px #444;
}
.nds2d-select {
bottom: 29px;
right: 70px;
}
.nds2d-start::after {
position: absolute;
content: "START";
font-size: 7px;
color: #999;
left: 15px;
top: 0px;
}
.nds2d-select::after {
position: absolute;
content: "SELECT";
font-size: 7px;
color: #999;
left: 15px;
top: 0px;
}
.nds2d-hints-container {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 10;
}
.nds2d-hint {
position: absolute;
font-size: 7px;
color: #fff;
background: rgba(0, 0, 0, 0.7);
padding: 1px 3px;
border-radius: 2px;
white-space: nowrap;
pointer-events: none;
}
.nds2d-hint-dpad {
left: 47px;
top: 46px;
transform: translateX(-50%);
}
.nds2d-hint-x,
.nds2d-hint-a,
.nds2d-hint-b,
.nds2d-hint-y {
width: 15px;
text-align: center;
}
.nds2d-hint-x {
right: 38px;
top: 32px;
}
.nds2d-hint-a {
right: 15px;
top: 56px;
}
.nds2d-hint-b {
right: 38px;
top: 79px;
}
.nds2d-hint-y {
right: 62px;
top: 56px;
}
.nds2d-hint-start {
left: 330px;
bottom: 53px;
transform: translateX(-100%);
}
.nds2d-hint-select {
left: 330px;
bottom: 28px;
transform: translateX(-100%);
}
.nds2d-help-btn {
position: fixed;
bottom: 16px;
left: 16px;
width: 30px;
height: 30px;
border: none;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
color: #666;
font-size: 18px;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.2s;
user-select: none;
}
.nds2d-help-btn:hover {
opacity: 1;
}
</style>