All checks were successful
Build and Push Docker Image / build (push) Successful in 3m37s
685 lines
18 KiB
Vue
685 lines
18 KiB
Vue
<script setup lang="ts">
|
|
import { useLoop, useTresContext } from "@tresjs/core";
|
|
import * as THREE from "three";
|
|
import gsap from "gsap";
|
|
import { mapNDSToKey } from "~/utils/input";
|
|
|
|
const INTRO_ANIMATION = {
|
|
MODEL_SPIN_DURATION: 3,
|
|
LID_OPEN_DURATION: 2,
|
|
LID_OPEN_OVERLAP: 1.5,
|
|
|
|
CAMERA_START_POSITION: new THREE.Vector3(0, 15, 2.6),
|
|
CAMERA_START_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-90), 0, 0),
|
|
CAMERA_END_POSITION: new THREE.Vector3(0, 14, 10),
|
|
CAMERA_END_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-55), 0, 0),
|
|
CAMERA_DURATION: 3,
|
|
|
|
LID_CLOSED_ROTATION: THREE.MathUtils.degToRad(120),
|
|
LID_OPENED_ROTATION: THREE.MathUtils.degToRad(-30),
|
|
};
|
|
|
|
const props = defineProps<{
|
|
topScreenCanvas: HTMLCanvasElement | null;
|
|
bottomScreenCanvas: HTMLCanvasElement | null;
|
|
}>();
|
|
|
|
const { assets } = useAssets();
|
|
const app = useAppStore();
|
|
|
|
const model = assets.models.nitendoDs.model.clone(true);
|
|
|
|
let topScreenTexture: THREE.CanvasTexture | null = null;
|
|
let bottomScreenTexture: THREE.CanvasTexture | null = null;
|
|
|
|
/// meshes ///
|
|
// screens
|
|
const TOP_SCREEN = "top_screen";
|
|
const BOTTOM_SCREEN = "bottom_screen";
|
|
const LID = "lid";
|
|
|
|
// buttons
|
|
const CROSS_BUTTON = "button_pad";
|
|
const X_BUTTON = "button_x";
|
|
const A_BUTTON = "button_a";
|
|
const Y_BUTTON = "button_y";
|
|
const B_BUTTON = "button_b";
|
|
const SELECT_BUTTON = "button_select";
|
|
const START_BUTTON = "button_start";
|
|
|
|
const meshes = new Map<string, THREE.Mesh>();
|
|
|
|
const requireMesh = (key: string): THREE.Mesh => {
|
|
const mesh = meshes.get(key);
|
|
if (!mesh) throw new Error(`Missing mesh '${key}'`);
|
|
|
|
return mesh;
|
|
};
|
|
|
|
const { camera, renderer, scene } = useTresContext();
|
|
|
|
model.scale.set(100, 100, 100);
|
|
|
|
watch(
|
|
() => camera.activeCamera.value,
|
|
(cam) => {
|
|
if (cam) {
|
|
app.setCamera(cam);
|
|
|
|
if (app.booted) {
|
|
cam.position.copy(INTRO_ANIMATION.CAMERA_END_POSITION);
|
|
cam.rotation.copy(INTRO_ANIMATION.CAMERA_END_ROTATION);
|
|
} else {
|
|
cam.position.copy(INTRO_ANIMATION.CAMERA_START_POSITION);
|
|
cam.rotation.copy(INTRO_ANIMATION.CAMERA_START_ROTATION);
|
|
}
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
meshes.clear();
|
|
model.traverse((child) => {
|
|
if (child instanceof THREE.Mesh) {
|
|
meshes.set(child.name, child);
|
|
}
|
|
});
|
|
|
|
const lidMesh = requireMesh(LID);
|
|
const topScreenMesh = requireMesh(TOP_SCREEN);
|
|
|
|
if (app.booted) {
|
|
lidMesh.rotation.x = INTRO_ANIMATION.LID_OPENED_ROTATION;
|
|
topScreenMesh.rotation.x = INTRO_ANIMATION.LID_OPENED_ROTATION;
|
|
} else {
|
|
lidMesh.rotation.x = INTRO_ANIMATION.LID_CLOSED_ROTATION;
|
|
topScreenMesh.rotation.x = INTRO_ANIMATION.LID_CLOSED_ROTATION;
|
|
}
|
|
|
|
const hasAnimated = ref(app.booted);
|
|
|
|
const animateIntro = () => {
|
|
if (hasAnimated.value) return;
|
|
hasAnimated.value = true;
|
|
|
|
assets.audio.whoosh.play();
|
|
|
|
gsap
|
|
.timeline()
|
|
.to(
|
|
camera.activeCamera.value.position,
|
|
{
|
|
x: INTRO_ANIMATION.CAMERA_END_POSITION.x,
|
|
y: INTRO_ANIMATION.CAMERA_END_POSITION.y,
|
|
z: INTRO_ANIMATION.CAMERA_END_POSITION.z,
|
|
duration: INTRO_ANIMATION.CAMERA_DURATION,
|
|
ease: "power2.inOut",
|
|
},
|
|
0,
|
|
)
|
|
.to(
|
|
camera.activeCamera.value.rotation,
|
|
{
|
|
x: INTRO_ANIMATION.CAMERA_END_ROTATION.x,
|
|
y: INTRO_ANIMATION.CAMERA_END_ROTATION.y,
|
|
z: INTRO_ANIMATION.CAMERA_END_ROTATION.z,
|
|
duration: INTRO_ANIMATION.CAMERA_DURATION,
|
|
ease: "power2.inOut",
|
|
},
|
|
0,
|
|
)
|
|
.to(
|
|
model.rotation,
|
|
{
|
|
y: Math.PI * 2,
|
|
duration: INTRO_ANIMATION.MODEL_SPIN_DURATION,
|
|
ease: "power2.inOut",
|
|
},
|
|
0,
|
|
)
|
|
.to(
|
|
lidMesh.rotation,
|
|
{
|
|
x: INTRO_ANIMATION.LID_OPENED_ROTATION,
|
|
duration: INTRO_ANIMATION.LID_OPEN_DURATION,
|
|
ease: "power2.out",
|
|
},
|
|
INTRO_ANIMATION.MODEL_SPIN_DURATION - INTRO_ANIMATION.LID_OPEN_OVERLAP,
|
|
)
|
|
.to(
|
|
topScreenMesh.rotation,
|
|
{
|
|
x: INTRO_ANIMATION.LID_OPENED_ROTATION,
|
|
duration: INTRO_ANIMATION.LID_OPEN_DURATION,
|
|
ease: "power2.out",
|
|
},
|
|
"<",
|
|
);
|
|
};
|
|
|
|
watch(
|
|
() => [props.topScreenCanvas, props.bottomScreenCanvas],
|
|
() => {
|
|
if (!props.topScreenCanvas || !props.bottomScreenCanvas) return;
|
|
|
|
const webglRenderer = renderer.instance;
|
|
if (!(webglRenderer instanceof THREE.WebGLRenderer)) return;
|
|
|
|
const createScreenTexture = (
|
|
canvas: HTMLCanvasElement,
|
|
): THREE.CanvasTexture => {
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|
texture.minFilter = THREE.LinearFilter;
|
|
texture.magFilter = THREE.LinearFilter;
|
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
texture.wrapS = THREE.RepeatWrapping;
|
|
texture.repeat.x = -1;
|
|
texture.anisotropy = webglRenderer.capabilities.getMaxAnisotropy();
|
|
texture.generateMipmaps = false;
|
|
return texture;
|
|
};
|
|
|
|
topScreenTexture = createScreenTexture(props.topScreenCanvas);
|
|
bottomScreenTexture = createScreenTexture(props.bottomScreenCanvas);
|
|
|
|
requireMesh(TOP_SCREEN).material = new THREE.MeshStandardMaterial({
|
|
map: topScreenTexture,
|
|
emissive: new THREE.Color(0x222222),
|
|
emissiveIntensity: 0.5,
|
|
});
|
|
|
|
requireMesh(BOTTOM_SCREEN).material = new THREE.MeshStandardMaterial({
|
|
map: bottomScreenTexture,
|
|
emissive: new THREE.Color(0x222222),
|
|
emissiveIntensity: 0.5,
|
|
});
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
const { onRender, onBeforeRender } = useLoop();
|
|
|
|
const LAG_FPS_THRESHOLD = 40;
|
|
const LAG_DURATION_SECS = 5;
|
|
let lagSeconds = 0;
|
|
let lagCheckDone = false;
|
|
let lastFrameTime = Date.now();
|
|
|
|
const HINT_SPRITE_SCALE = 2;
|
|
const HINT_SPRITE_DPR = 4;
|
|
|
|
const makeHintSprite = (text: string): THREE.Sprite => {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = 256 * HINT_SPRITE_DPR;
|
|
canvas.height = 64 * HINT_SPRITE_DPR;
|
|
|
|
const ctx = canvas.getContext("2d")!;
|
|
|
|
ctx.scale(HINT_SPRITE_DPR, HINT_SPRITE_DPR);
|
|
ctx.font = "32px monospace";
|
|
ctx.fillStyle = "#666666";
|
|
|
|
const padding = 12;
|
|
const textWidth = ctx.measureText(text).width + padding * 2;
|
|
ctx.roundRect((256 - textWidth) / 2, 12, textWidth, 40, 6);
|
|
ctx.fill();
|
|
ctx.fillStyle = "#000000";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(text, 128, 32);
|
|
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|
const material = new THREE.SpriteMaterial({
|
|
map: texture,
|
|
transparent: true,
|
|
depthTest: false,
|
|
});
|
|
const sprite = new THREE.Sprite(material);
|
|
sprite.scale.set(HINT_SPRITE_SCALE, HINT_SPRITE_SCALE * 0.25, 1);
|
|
|
|
return sprite;
|
|
};
|
|
|
|
const HINTS: Record<string, { label: string; offset: THREE.Vector3 }> = {
|
|
[X_BUTTON]: {
|
|
label: mapNDSToKey("X"),
|
|
offset: new THREE.Vector3(-0.3, 1.1, 0.0),
|
|
},
|
|
[A_BUTTON]: {
|
|
label: mapNDSToKey("A"),
|
|
offset: new THREE.Vector3(-0.4, 1.2, 0.0),
|
|
},
|
|
[Y_BUTTON]: {
|
|
label: mapNDSToKey("Y"),
|
|
offset: new THREE.Vector3(-0.3, 1.2, 0.0),
|
|
},
|
|
[B_BUTTON]: {
|
|
label: mapNDSToKey("B"),
|
|
offset: new THREE.Vector3(-0.4, 1.3, 0.0),
|
|
},
|
|
[SELECT_BUTTON]: {
|
|
label: mapNDSToKey("SELECT"),
|
|
offset: new THREE.Vector3(-0.7, 0.05, 0.0),
|
|
},
|
|
[START_BUTTON]: {
|
|
label: mapNDSToKey("START"),
|
|
offset: new THREE.Vector3(-0.75, 0.15, 0.0),
|
|
},
|
|
[CROSS_BUTTON]: {
|
|
label: "← ↑ → ↓",
|
|
offset: new THREE.Vector3(0.75, 2.3, 0.0),
|
|
},
|
|
};
|
|
|
|
const helpSprites = new THREE.Group();
|
|
helpSprites.renderOrder = 999;
|
|
|
|
const buildHelpSprites = () => {
|
|
helpSprites.clear();
|
|
for (const [meshName, { label, offset }] of Object.entries(HINTS)) {
|
|
const mesh = meshes.get(meshName);
|
|
if (!mesh) continue;
|
|
|
|
const sprite = makeHintSprite(label);
|
|
const worldPos = new THREE.Vector3();
|
|
mesh.getWorldPosition(worldPos);
|
|
sprite.position.copy(worldPos.add(offset));
|
|
|
|
helpSprites.add(sprite);
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => app.hintsVisible,
|
|
(show) => {
|
|
if (show) {
|
|
buildHelpSprites();
|
|
helpSprites.visible = true;
|
|
for (const child of helpSprites.children) {
|
|
const sprite = child as THREE.Sprite;
|
|
gsap.fromTo(
|
|
sprite.material,
|
|
{ opacity: 0 },
|
|
{ opacity: 1, duration: 0.2, ease: "power1.out" },
|
|
);
|
|
}
|
|
} else {
|
|
for (const child of helpSprites.children) {
|
|
const sprite = child as THREE.Sprite;
|
|
gsap.to(sprite.material, {
|
|
opacity: 0,
|
|
duration: 0.2,
|
|
ease: "power1.in",
|
|
onComplete: () => {
|
|
helpSprites.visible = false;
|
|
},
|
|
});
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
helpSprites.visible = app.hintsVisible;
|
|
watch(
|
|
scene,
|
|
(s) => {
|
|
if (!s) return;
|
|
s.add(helpSprites);
|
|
if (app.hintsVisible) buildHelpSprites();
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
const physicalButtonsDown = new Set<string>();
|
|
let mousePressedButton: string | null = null;
|
|
|
|
useKeyDown(({ ndsButton }) => {
|
|
if (ndsButton) {
|
|
physicalButtonsDown.add(ndsButton);
|
|
}
|
|
});
|
|
|
|
useKeyUp(({ ndsButton }) => {
|
|
if (ndsButton) {
|
|
physicalButtonsDown.delete(ndsButton);
|
|
}
|
|
});
|
|
|
|
// lag detection
|
|
onBeforeRender(() => {
|
|
if (!lagCheckDone) {
|
|
const now = Date.now();
|
|
const delta = (now - lastFrameTime) / 1000;
|
|
lastFrameTime = now;
|
|
|
|
if (document.hidden || delta > 0.5) {
|
|
lagSeconds = 0;
|
|
return;
|
|
}
|
|
|
|
const fps = 1 / delta;
|
|
if (fps < LAG_FPS_THRESHOLD) {
|
|
lagSeconds += delta;
|
|
if (lagSeconds >= LAG_DURATION_SECS) {
|
|
lagCheckDone = true;
|
|
app.lagDetected = true;
|
|
}
|
|
} else {
|
|
lagSeconds = 0;
|
|
}
|
|
}
|
|
});
|
|
|
|
// upate screens
|
|
onRender(() => {
|
|
if (topScreenTexture) topScreenTexture.needsUpdate = true;
|
|
if (bottomScreenTexture) bottomScreenTexture.needsUpdate = true;
|
|
});
|
|
|
|
// update physical buttons
|
|
onBeforeRender(({ delta }) => {
|
|
// cross
|
|
const crossButton = meshes.get(CROSS_BUTTON);
|
|
if (crossButton) {
|
|
const PRESS_ANGLE = Math.PI / 28;
|
|
const PRESS_SPEED = 150;
|
|
|
|
const targetRotation = new THREE.Vector3(0);
|
|
if (physicalButtonsDown.has("UP")) targetRotation.setX(-PRESS_ANGLE);
|
|
if (physicalButtonsDown.has("RIGHT")) targetRotation.setZ(-PRESS_ANGLE);
|
|
if (physicalButtonsDown.has("DOWN")) targetRotation.setX(PRESS_ANGLE);
|
|
if (physicalButtonsDown.has("LEFT")) targetRotation.setZ(PRESS_ANGLE);
|
|
|
|
const lerpFactor = Math.min(delta * PRESS_SPEED, 1);
|
|
const currentRotation = new THREE.Vector3().setFromEuler(
|
|
crossButton.rotation,
|
|
);
|
|
currentRotation.lerp(targetRotation, lerpFactor);
|
|
crossButton.rotation.setFromVector3(currentRotation);
|
|
}
|
|
|
|
// action buttons
|
|
const PRESS_SPEED = 100;
|
|
|
|
const ACTION_BUTTONS = [
|
|
{ mesh: meshes.get(X_BUTTON), name: "X", offset: -0.0012 },
|
|
{ mesh: meshes.get(A_BUTTON), name: "A", offset: -0.0012 },
|
|
{ mesh: meshes.get(Y_BUTTON), name: "Y", offset: -0.0012 },
|
|
{ mesh: meshes.get(B_BUTTON), name: "B", offset: -0.0012 },
|
|
{ mesh: meshes.get(SELECT_BUTTON), name: "SELECT", offset: -0.0007 },
|
|
{ mesh: meshes.get(START_BUTTON), name: "START", offset: -0.0007 },
|
|
] as const;
|
|
|
|
for (const { mesh, name, offset } of ACTION_BUTTONS) {
|
|
if (!mesh) continue;
|
|
|
|
const targetY = physicalButtonsDown.has(name) ? offset : 0;
|
|
const lerpFactor = Math.min(delta * PRESS_SPEED, 1);
|
|
mesh.position.y += (targetY - mesh.position.y) * lerpFactor;
|
|
}
|
|
});
|
|
|
|
const pressButton = (button: string) => {
|
|
if (mousePressedButton) {
|
|
physicalButtonsDown.delete(mousePressedButton);
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keyup", { key: `NDS_${mousePressedButton}` }),
|
|
);
|
|
}
|
|
|
|
physicalButtonsDown.add(button);
|
|
mousePressedButton = button;
|
|
|
|
window.dispatchEvent(new KeyboardEvent("keydown", { key: `NDS_${button}` }));
|
|
};
|
|
|
|
const raycast = (clientX: number, clientY: number) => {
|
|
const domElement = renderer.instance.domElement;
|
|
const rect = domElement.getBoundingClientRect();
|
|
|
|
const raycaster = new THREE.Raycaster();
|
|
raycaster.setFromCamera(
|
|
new THREE.Vector2(
|
|
((clientX - rect.left) / rect.width) * 2 - 1,
|
|
-((clientY - rect.top) / rect.height) * 2 + 1,
|
|
),
|
|
camera.activeCamera.value,
|
|
);
|
|
|
|
const intersects = raycaster.intersectObjects(model.children, true);
|
|
return intersects[0];
|
|
};
|
|
|
|
const getScreenCanvas = (name: string) => {
|
|
if (name === TOP_SCREEN) return props.topScreenCanvas;
|
|
if (name === BOTTOM_SCREEN) return props.bottomScreenCanvas;
|
|
return null;
|
|
};
|
|
|
|
const toScreenCoords = (
|
|
canvas: HTMLCanvasElement,
|
|
intersection: THREE.Intersection,
|
|
) => {
|
|
if (!intersection.uv) return null;
|
|
|
|
const logicalX = (1 - intersection.uv.x) * LOGICAL_WIDTH;
|
|
const logicalY = (1 - intersection.uv.y) * LOGICAL_HEIGHT;
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
return {
|
|
clientX: (logicalX / LOGICAL_WIDTH) * rect.width + rect.left,
|
|
clientY: (logicalY / LOGICAL_HEIGHT) * rect.height + rect.top,
|
|
};
|
|
};
|
|
|
|
const dispatchScreenEvent = (
|
|
type: string,
|
|
canvas: HTMLCanvasElement,
|
|
intersection: THREE.Intersection,
|
|
) => {
|
|
const coords = toScreenCoords(canvas, intersection);
|
|
if (!coords) return;
|
|
|
|
canvas.dispatchEvent(
|
|
new MouseEvent(type, { bubbles: true, cancelable: true, ...coords }),
|
|
);
|
|
};
|
|
|
|
const BUTTON_MAP = {
|
|
[X_BUTTON]: "X",
|
|
[A_BUTTON]: "A",
|
|
[Y_BUTTON]: "Y",
|
|
[B_BUTTON]: "B",
|
|
[SELECT_BUTTON]: "SELECT",
|
|
[START_BUTTON]: "START",
|
|
} as const;
|
|
|
|
const handleInteraction = (
|
|
clientX: number,
|
|
clientY: number,
|
|
onScreen: (
|
|
canvas: HTMLCanvasElement,
|
|
intersection: THREE.Intersection,
|
|
) => void,
|
|
) => {
|
|
const intersection = raycast(clientX, clientY);
|
|
if (!intersection?.uv) return;
|
|
|
|
switch (intersection.object.name) {
|
|
case TOP_SCREEN:
|
|
case BOTTOM_SCREEN: {
|
|
const canvas = getScreenCanvas(intersection.object.name);
|
|
if (canvas) onScreen(canvas, intersection);
|
|
break;
|
|
}
|
|
|
|
case CROSS_BUTTON: {
|
|
const localPos = intersection.point
|
|
.clone()
|
|
.sub(intersection.object.getWorldPosition(new THREE.Vector3()));
|
|
|
|
const MIN_DIST = 0.45;
|
|
let button: string | null = null;
|
|
|
|
if (localPos.z <= -MIN_DIST) button = "UP";
|
|
else if (localPos.x >= MIN_DIST) button = "RIGHT";
|
|
else if (localPos.z >= MIN_DIST) button = "DOWN";
|
|
else if (localPos.x <= -MIN_DIST) button = "LEFT";
|
|
|
|
if (button) pressButton(button);
|
|
break;
|
|
}
|
|
|
|
default: {
|
|
const button =
|
|
BUTTON_MAP[intersection.object.name as keyof typeof BUTTON_MAP];
|
|
if (button) pressButton(button);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
const releaseButton = () => {
|
|
if (!mousePressedButton) return;
|
|
|
|
physicalButtonsDown.delete(mousePressedButton);
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keyup", { key: `NDS_${mousePressedButton}` }),
|
|
);
|
|
mousePressedButton = null;
|
|
};
|
|
|
|
const handleMouseDown = (event: MouseEvent) => {
|
|
handleInteraction(event.clientX, event.clientY, (canvas, intersection) => {
|
|
dispatchScreenEvent("mousedown", canvas, intersection);
|
|
});
|
|
};
|
|
|
|
const handleClick = (event: MouseEvent) => {
|
|
if (!hasAnimated.value) return;
|
|
|
|
const intersection = raycast(event.clientX, event.clientY);
|
|
if (!intersection?.uv) return;
|
|
|
|
const canvas = getScreenCanvas(intersection.object.name);
|
|
if (canvas) dispatchScreenEvent("click", canvas, intersection);
|
|
};
|
|
|
|
const handleMouseUp = (event: MouseEvent) => {
|
|
releaseButton();
|
|
|
|
const intersection = raycast(event.clientX, event.clientY);
|
|
if (!intersection?.uv) return;
|
|
|
|
const canvas = getScreenCanvas(intersection.object.name);
|
|
if (canvas) dispatchScreenEvent("mouseup", canvas, intersection);
|
|
};
|
|
|
|
const SWIPE_THRESHOLD = 30;
|
|
let swipeStartX = 0;
|
|
let swipeStartY = 0;
|
|
|
|
const dispatchSwipe = (endX: number, endY: number) => {
|
|
const deltaX = endX - swipeStartX;
|
|
const deltaY = endY - swipeStartY;
|
|
const absDeltaX = Math.abs(deltaX);
|
|
const absDeltaY = Math.abs(deltaY);
|
|
|
|
if (Math.max(absDeltaX, absDeltaY) < SWIPE_THRESHOLD) return;
|
|
|
|
const direction =
|
|
absDeltaX > absDeltaY
|
|
? deltaX > 0
|
|
? "RIGHT"
|
|
: "LEFT"
|
|
: deltaY > 0
|
|
? "DOWN"
|
|
: "UP";
|
|
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keydown", { key: `NDS_SWIPE_${direction}` }),
|
|
);
|
|
};
|
|
|
|
const handleTouchStart = (event: TouchEvent) => {
|
|
const touch = event.touches[0];
|
|
if (!touch) return;
|
|
|
|
event.preventDefault();
|
|
swipeStartX = touch.clientX;
|
|
swipeStartY = touch.clientY;
|
|
|
|
handleInteraction(touch.clientX, touch.clientY, (canvas, intersection) => {
|
|
dispatchScreenEvent("mousedown", canvas, intersection);
|
|
});
|
|
};
|
|
|
|
const handleTouchEnd = (event: TouchEvent) => {
|
|
releaseButton();
|
|
|
|
const touch = event.changedTouches[0];
|
|
if (!touch) return;
|
|
|
|
dispatchSwipe(touch.clientX, touch.clientY);
|
|
|
|
const intersection = raycast(touch.clientX, touch.clientY);
|
|
if (!intersection?.uv) return;
|
|
|
|
const canvas = getScreenCanvas(intersection.object.name);
|
|
if (canvas) {
|
|
dispatchScreenEvent("click", canvas, intersection);
|
|
}
|
|
|
|
document.dispatchEvent(
|
|
new MouseEvent("mouseup", {
|
|
clientX: touch.clientX,
|
|
clientY: touch.clientY,
|
|
}),
|
|
);
|
|
};
|
|
|
|
onMounted(() => {
|
|
app.ready = true;
|
|
|
|
if (app.userHasInteracted) {
|
|
animateIntro();
|
|
}
|
|
|
|
if (renderer) {
|
|
renderer.instance.domElement.addEventListener("mousedown", handleMouseDown);
|
|
renderer.instance.domElement.addEventListener("click", handleClick);
|
|
renderer.instance.domElement.addEventListener("mouseup", handleMouseUp);
|
|
renderer.instance.domElement.addEventListener(
|
|
"touchstart",
|
|
handleTouchStart,
|
|
);
|
|
renderer.instance.domElement.addEventListener("touchend", handleTouchEnd);
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (renderer) {
|
|
renderer.instance.domElement.removeEventListener(
|
|
"mousedown",
|
|
handleMouseDown,
|
|
);
|
|
renderer.instance.domElement.removeEventListener("click", handleClick);
|
|
renderer.instance.domElement.removeEventListener("mouseup", handleMouseUp);
|
|
renderer.instance.domElement.removeEventListener(
|
|
"touchstart",
|
|
handleTouchStart,
|
|
);
|
|
renderer.instance.domElement.removeEventListener(
|
|
"touchend",
|
|
handleTouchEnd,
|
|
);
|
|
}
|
|
topScreenTexture?.dispose();
|
|
bottomScreenTexture?.dispose();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<primitive :object="model" />
|
|
</template>
|