feat(nds): 3d touch support

This commit is contained in:
2026-02-12 22:31:13 +01:00
parent 161b22259a
commit a5424993b3
2 changed files with 131 additions and 42 deletions

View File

@@ -274,15 +274,15 @@ const pressButton = (button: string) => {
window.dispatchEvent(new KeyboardEvent("keydown", { key: `NDS_${button}` })); window.dispatchEvent(new KeyboardEvent("keydown", { key: `NDS_${button}` }));
}; };
const raycast = (event: MouseEvent) => { const raycast = (clientX: number, clientY: number) => {
const domElement = renderer.instance.domElement; const domElement = renderer.instance.domElement;
const rect = domElement.getBoundingClientRect(); const rect = domElement.getBoundingClientRect();
const raycaster = new THREE.Raycaster(); const raycaster = new THREE.Raycaster();
raycaster.setFromCamera( raycaster.setFromCamera(
new THREE.Vector2( new THREE.Vector2(
((event.clientX - rect.left) / rect.width) * 2 - 1, ((clientX - rect.left) / rect.width) * 2 - 1,
-((event.clientY - rect.top) / rect.height) * 2 + 1, -((clientY - rect.top) / rect.height) * 2 + 1,
), ),
camera.activeCamera.value, camera.activeCamera.value,
); );
@@ -297,41 +297,80 @@ const getScreenCanvas = (name: string) => {
return null; return null;
}; };
const dispatchScreenEvent = ( const toScreenCoords = (
type: string,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
intersection: THREE.Intersection, intersection: THREE.Intersection,
) => { ) => {
if (!intersection.uv) return; if (!intersection.uv) return null;
const logicalX = (1 - intersection.uv.x) * LOGICAL_WIDTH; const logicalX = (1 - intersection.uv.x) * LOGICAL_WIDTH;
const logicalY = (1 - intersection.uv.y) * LOGICAL_HEIGHT; const logicalY = (1 - intersection.uv.y) * LOGICAL_HEIGHT;
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
canvas.dispatchEvent( return {
new MouseEvent(type, {
bubbles: true,
cancelable: true,
clientX: (logicalX / LOGICAL_WIDTH) * rect.width + rect.left, clientX: (logicalX / LOGICAL_WIDTH) * rect.width + rect.left,
clientY: (logicalY / LOGICAL_HEIGHT) * rect.height + rect.top, 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 dispatchScreenTouchEvent = (
type: string,
canvas: HTMLCanvasElement,
intersection: THREE.Intersection,
) => {
const coords = toScreenCoords(canvas, intersection);
if (!coords) return;
const touch = new Touch({ identifier: 0, target: canvas, ...coords });
canvas.dispatchEvent(
new TouchEvent(type, {
bubbles: true,
cancelable: true,
touches: type === "touchend" ? [] : [touch],
changedTouches: [touch],
}), }),
); );
}; };
const handleMouseDown = (event: MouseEvent) => { const BUTTON_MAP = {
if (!hasAnimated.value) { [X_BUTTON]: "X",
animateIntro(); [A_BUTTON]: "A",
return; [Y_BUTTON]: "Y",
} [B_BUTTON]: "B",
[SELECT_BUTTON]: "SELECT",
[START_BUTTON]: "START",
} as const;
const intersection = raycast(event); const handleInteraction = (
clientX: number,
clientY: number,
onScreen: (
canvas: HTMLCanvasElement,
intersection: THREE.Intersection,
) => void,
) => {
const intersection = raycast(clientX, clientY);
if (!intersection?.uv) return; if (!intersection?.uv) return;
switch (intersection.object.name) { switch (intersection.object.name) {
case TOP_SCREEN: case TOP_SCREEN:
case BOTTOM_SCREEN: { case BOTTOM_SCREEN: {
const canvas = getScreenCanvas(intersection.object.name); const canvas = getScreenCanvas(intersection.object.name);
if (canvas) dispatchScreenEvent("mousedown", canvas, intersection); if (canvas) onScreen(canvas, intersection);
break; break;
} }
@@ -352,47 +391,84 @@ const handleMouseDown = (event: MouseEvent) => {
break; break;
} }
case X_BUTTON: default: {
case A_BUTTON: const button =
case Y_BUTTON: BUTTON_MAP[intersection.object.name as keyof typeof BUTTON_MAP];
case B_BUTTON:
case SELECT_BUTTON:
case START_BUTTON: {
const BUTTON_MAP = {
[X_BUTTON]: "X",
[A_BUTTON]: "A",
[Y_BUTTON]: "Y",
[B_BUTTON]: "B",
[SELECT_BUTTON]: "SELECT",
[START_BUTTON]: "START",
} as const;
const button = BUTTON_MAP[intersection.object.name];
if (button) pressButton(button); if (button) pressButton(button);
break; break;
} }
} }
}; };
const releaseButton = () => {
if (!mousePressedButton) return;
physicalButtonsDown.delete(mousePressedButton);
window.dispatchEvent(
new KeyboardEvent("keyup", { key: `NDS_${mousePressedButton}` }),
);
mousePressedButton = null;
};
const handleMouseDown = (event: MouseEvent) => {
if (!hasAnimated.value) {
animateIntro();
return;
}
handleInteraction(event.clientX, event.clientY, (canvas, intersection) => {
dispatchScreenEvent("mousedown", canvas, intersection);
});
};
const handleClick = (event: MouseEvent) => { const handleClick = (event: MouseEvent) => {
if (!hasAnimated.value) return; if (!hasAnimated.value) return;
const intersection = raycast(event); const intersection = raycast(event.clientX, event.clientY);
if (!intersection?.uv) return; if (!intersection?.uv) return;
const canvas = getScreenCanvas(intersection.object.name); const canvas = getScreenCanvas(intersection.object.name);
if (canvas) dispatchScreenEvent("click", canvas, intersection); if (canvas) dispatchScreenEvent("click", canvas, intersection);
}; };
const handleMouseUp = () => { const handleMouseUp = (event: MouseEvent) => {
if (mousePressedButton) { releaseButton();
physicalButtonsDown.delete(mousePressedButton);
window.dispatchEvent( const intersection = raycast(event.clientX, event.clientY);
new KeyboardEvent("keyup", { key: `NDS_${mousePressedButton}` }), if (!intersection?.uv) return;
);
mousePressedButton = null; const canvas = getScreenCanvas(intersection.object.name);
if (canvas) dispatchScreenEvent("mouseup", canvas, intersection);
};
const handleTouchStart = (event: TouchEvent) => {
const touch = event.touches[0];
if (!touch) return;
if (!hasAnimated.value) {
animateIntro();
return;
}
handleInteraction(touch.clientX, touch.clientY, (canvas, intersection) => {
dispatchScreenEvent("mousedown", canvas, intersection);
dispatchScreenTouchEvent("touchstart", canvas, intersection);
});
};
const handleTouchEnd = (event: TouchEvent) => {
releaseButton();
const touch = event.changedTouches[0];
if (!touch) return;
const intersection = raycast(touch.clientX, touch.clientY);
if (!intersection?.uv) return;
const canvas = getScreenCanvas(intersection.object.name);
if (canvas) {
dispatchScreenEvent("click", canvas, intersection);
dispatchScreenTouchEvent("touchend", canvas, intersection);
} }
}; };
@@ -403,6 +479,11 @@ onMounted(() => {
renderer.instance.domElement.addEventListener("mousedown", handleMouseDown); renderer.instance.domElement.addEventListener("mousedown", handleMouseDown);
renderer.instance.domElement.addEventListener("click", handleClick); renderer.instance.domElement.addEventListener("click", handleClick);
renderer.instance.domElement.addEventListener("mouseup", handleMouseUp); renderer.instance.domElement.addEventListener("mouseup", handleMouseUp);
renderer.instance.domElement.addEventListener(
"touchstart",
handleTouchStart,
);
renderer.instance.domElement.addEventListener("touchend", handleTouchEnd);
} }
}); });
@@ -414,6 +495,14 @@ onUnmounted(() => {
); );
renderer.instance.domElement.removeEventListener("click", handleClick); renderer.instance.domElement.removeEventListener("click", handleClick);
renderer.instance.domElement.removeEventListener("mouseup", handleMouseUp); renderer.instance.domElement.removeEventListener("mouseup", handleMouseUp);
renderer.instance.domElement.removeEventListener(
"touchstart",
handleTouchStart,
);
renderer.instance.domElement.removeEventListener(
"touchend",
handleTouchEnd,
);
} }
topScreenTexture?.dispose(); topScreenTexture?.dispose();
bottomScreenTexture?.dispose(); bottomScreenTexture?.dispose();

View File

@@ -178,7 +178,7 @@ onMounted(() => {
}); });
canvas.value.addEventListener("touchend", handleTouchEnd, { passive: true }); canvas.value.addEventListener("touchend", handleTouchEnd, { passive: true });
canvas.value.addEventListener("mousedown", handleSwipeMouseDown); canvas.value.addEventListener("mousedown", handleSwipeMouseDown);
document.addEventListener("mouseup", handleSwipeMouseUp); canvas.value.addEventListener("mouseup", handleSwipeMouseUp);
animationFrameId = requestAnimationFrame(renderFrame); animationFrameId = requestAnimationFrame(renderFrame);
}); });
@@ -195,8 +195,8 @@ onUnmounted(() => {
canvas.value.removeEventListener("touchstart", handleTouchStart); canvas.value.removeEventListener("touchstart", handleTouchStart);
canvas.value.removeEventListener("touchend", handleTouchEnd); canvas.value.removeEventListener("touchend", handleTouchEnd);
canvas.value.removeEventListener("mousedown", handleSwipeMouseDown); canvas.value.removeEventListener("mousedown", handleSwipeMouseDown);
canvas.value.removeEventListener("mouseup", handleSwipeMouseUp);
} }
document.removeEventListener("mouseup", handleSwipeMouseUp);
}); });
defineExpose({ defineExpose({