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}` }));
};
const raycast = (event: MouseEvent) => {
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(
((event.clientX - rect.left) / rect.width) * 2 - 1,
-((event.clientY - rect.top) / rect.height) * 2 + 1,
((clientX - rect.left) / rect.width) * 2 - 1,
-((clientY - rect.top) / rect.height) * 2 + 1,
),
camera.activeCamera.value,
);
@@ -297,41 +297,80 @@ const getScreenCanvas = (name: string) => {
return null;
};
const dispatchScreenEvent = (
type: string,
const toScreenCoords = (
canvas: HTMLCanvasElement,
intersection: THREE.Intersection,
) => {
if (!intersection.uv) return;
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, {
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,
clientX: (logicalX / LOGICAL_WIDTH) * rect.width + rect.left,
clientY: (logicalY / LOGICAL_HEIGHT) * rect.height + rect.top,
touches: type === "touchend" ? [] : [touch],
changedTouches: [touch],
}),
);
};
const handleMouseDown = (event: MouseEvent) => {
if (!hasAnimated.value) {
animateIntro();
return;
}
const BUTTON_MAP = {
[X_BUTTON]: "X",
[A_BUTTON]: "A",
[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;
switch (intersection.object.name) {
case TOP_SCREEN:
case BOTTOM_SCREEN: {
const canvas = getScreenCanvas(intersection.object.name);
if (canvas) dispatchScreenEvent("mousedown", canvas, intersection);
if (canvas) onScreen(canvas, intersection);
break;
}
@@ -352,47 +391,84 @@ const handleMouseDown = (event: MouseEvent) => {
break;
}
case X_BUTTON:
case A_BUTTON:
case Y_BUTTON:
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];
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) => {
if (!hasAnimated.value) {
animateIntro();
return;
}
handleInteraction(event.clientX, event.clientY, (canvas, intersection) => {
dispatchScreenEvent("mousedown", canvas, intersection);
});
};
const handleClick = (event: MouseEvent) => {
if (!hasAnimated.value) return;
const intersection = raycast(event);
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 = () => {
if (mousePressedButton) {
physicalButtonsDown.delete(mousePressedButton);
const handleMouseUp = (event: MouseEvent) => {
releaseButton();
window.dispatchEvent(
new KeyboardEvent("keyup", { key: `NDS_${mousePressedButton}` }),
);
const intersection = raycast(event.clientX, event.clientY);
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("click", handleClick);
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("mouseup", handleMouseUp);
renderer.instance.domElement.removeEventListener(
"touchstart",
handleTouchStart,
);
renderer.instance.domElement.removeEventListener(
"touchend",
handleTouchEnd,
);
}
topScreenTexture?.dispose();
bottomScreenTexture?.dispose();

View File

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