235 lines
6.2 KiB
Vue
235 lines
6.2 KiB
Vue
<script lang="ts" setup>
|
|
const canvas = useTemplateRef("canvas");
|
|
|
|
const renderCallbacks = new Map<RenderCallback, number>();
|
|
const screenClickCallbacks = new Set<ScreenClickCallback>();
|
|
const screenMouseDownCallbacks = new Set<ScreenClickCallback>();
|
|
const screenMouseWheelCallbacks = new Set<ScreenMouseWheelCallback>();
|
|
|
|
let ctx: CanvasRenderingContext2D | null = null;
|
|
let animationFrameId: number | null = null;
|
|
let lastFrameTime = 0;
|
|
let lastRealFrameTime = 0;
|
|
|
|
const registerRenderCallback = (callback: RenderCallback, zIndex = 0) => {
|
|
renderCallbacks.set(callback, zIndex);
|
|
return () => renderCallbacks.delete(callback);
|
|
};
|
|
|
|
const registerScreenClickCallback = (callback: ScreenClickCallback) => {
|
|
screenClickCallbacks.add(callback);
|
|
return () => screenClickCallbacks.delete(callback);
|
|
};
|
|
|
|
const registerScreenMouseDownCallback = (callback: ScreenClickCallback) => {
|
|
screenMouseDownCallbacks.add(callback);
|
|
return () => screenMouseDownCallbacks.delete(callback);
|
|
};
|
|
|
|
const registerScreenMouseWheelCallback = (
|
|
callback: ScreenMouseWheelCallback,
|
|
) => {
|
|
screenMouseWheelCallbacks.add(callback);
|
|
return () => screenMouseWheelCallbacks.delete(callback);
|
|
};
|
|
|
|
const handleCanvasClick = (event: MouseEvent) => {
|
|
if (!canvas.value) return;
|
|
|
|
const rect = canvas.value.getBoundingClientRect();
|
|
const scaleX = LOGICAL_WIDTH / rect.width;
|
|
const scaleY = LOGICAL_HEIGHT / rect.height;
|
|
|
|
const x = (event.clientX - rect.left) * scaleX;
|
|
const y = (event.clientY - rect.top) * scaleY;
|
|
|
|
for (const callback of screenClickCallbacks) {
|
|
callback(x, y);
|
|
}
|
|
};
|
|
|
|
const handleCanvasMouseDown = (event: MouseEvent) => {
|
|
if (!canvas.value) return;
|
|
|
|
const rect = canvas.value.getBoundingClientRect();
|
|
const scaleX = LOGICAL_WIDTH / rect.width;
|
|
const scaleY = LOGICAL_HEIGHT / rect.height;
|
|
|
|
const x = (event.clientX - rect.left) * scaleX;
|
|
const y = (event.clientY - rect.top) * scaleY;
|
|
|
|
for (const callback of screenMouseDownCallbacks) {
|
|
callback(x, y);
|
|
}
|
|
};
|
|
|
|
const handleCanvasWheel = (event: WheelEvent) => {
|
|
for (const callback of screenMouseWheelCallbacks) {
|
|
callback(event.deltaY, event.deltaX);
|
|
}
|
|
};
|
|
|
|
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;
|
|
|
|
let direction: string;
|
|
if (absDeltaX > absDeltaY) {
|
|
direction = deltaX > 0 ? "RIGHT" : "LEFT";
|
|
} else {
|
|
direction = 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;
|
|
|
|
swipeStartX = touch.clientX;
|
|
swipeStartY = touch.clientY;
|
|
|
|
canvas.value?.dispatchEvent(
|
|
new MouseEvent("mousedown", {
|
|
clientX: touch.clientX,
|
|
clientY: touch.clientY,
|
|
}),
|
|
);
|
|
};
|
|
|
|
const handleTouchEnd = (event: TouchEvent) => {
|
|
const touch = event.changedTouches[0];
|
|
if (!touch) return;
|
|
|
|
dispatchSwipe(touch.clientX, touch.clientY);
|
|
document.dispatchEvent(
|
|
new MouseEvent("mouseup", {
|
|
clientX: touch.clientX,
|
|
clientY: touch.clientY,
|
|
}),
|
|
);
|
|
};
|
|
|
|
let mouseSwiping = false;
|
|
|
|
const handleSwipeMouseDown = (event: MouseEvent) => {
|
|
mouseSwiping = true;
|
|
swipeStartX = event.clientX;
|
|
swipeStartY = event.clientY;
|
|
};
|
|
|
|
const handleSwipeMouseUp = (event: MouseEvent) => {
|
|
if (!mouseSwiping) return;
|
|
mouseSwiping = false;
|
|
dispatchSwipe(event.clientX, event.clientY);
|
|
};
|
|
|
|
const renderFrame = (timestamp: number) => {
|
|
if (!ctx) return;
|
|
|
|
const deltaTime = timestamp - lastFrameTime;
|
|
lastFrameTime = timestamp;
|
|
|
|
const start = Date.now();
|
|
|
|
// render
|
|
ctx.save();
|
|
ctx.scale(SCREEN_SCALE, SCREEN_SCALE);
|
|
|
|
ctx.clearRect(0, 0, LOGICAL_WIDTH, LOGICAL_HEIGHT);
|
|
|
|
const sortedCallbacks = Array.from(renderCallbacks.entries())
|
|
.sort((a, b) => a[1] - b[1])
|
|
.map(([callback]) => callback);
|
|
|
|
for (const callback of sortedCallbacks) {
|
|
ctx.save();
|
|
|
|
try {
|
|
callback(ctx, deltaTime, lastRealFrameTime);
|
|
} catch (error: unknown) {
|
|
console.error(error);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
ctx.restore();
|
|
|
|
lastRealFrameTime = Date.now() - start;
|
|
|
|
animationFrameId = requestAnimationFrame(renderFrame);
|
|
};
|
|
|
|
provide("registerRenderCallback", registerRenderCallback);
|
|
provide("registerScreenClickCallback", registerScreenClickCallback);
|
|
provide("registerScreenMouseDownCallback", registerScreenMouseDownCallback);
|
|
provide("registerScreenMouseWheelCallback", registerScreenMouseWheelCallback);
|
|
|
|
onMounted(() => {
|
|
if (!canvas.value) throw new Error("Missing canvas");
|
|
|
|
ctx = canvas.value.getContext("2d");
|
|
if (!ctx) throw new Error("Missing 2d context");
|
|
|
|
ctx.imageSmoothingEnabled = false;
|
|
|
|
canvas.value.addEventListener("click", handleCanvasClick);
|
|
canvas.value.addEventListener("mousedown", handleCanvasMouseDown);
|
|
canvas.value.addEventListener("wheel", handleCanvasWheel, { passive: true });
|
|
canvas.value.addEventListener("touchstart", handleTouchStart, {
|
|
passive: true,
|
|
});
|
|
canvas.value.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
canvas.value.addEventListener("mousedown", handleSwipeMouseDown);
|
|
canvas.value.addEventListener("mouseup", handleSwipeMouseUp);
|
|
|
|
animationFrameId = requestAnimationFrame(renderFrame);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (animationFrameId !== null) {
|
|
cancelAnimationFrame(animationFrameId);
|
|
}
|
|
|
|
if (canvas.value) {
|
|
canvas.value.removeEventListener("click", handleCanvasClick);
|
|
canvas.value.removeEventListener("mousedown", handleCanvasMouseDown);
|
|
canvas.value.removeEventListener("wheel", handleCanvasWheel);
|
|
canvas.value.removeEventListener("touchstart", handleTouchStart);
|
|
canvas.value.removeEventListener("touchend", handleTouchEnd);
|
|
canvas.value.removeEventListener("mousedown", handleSwipeMouseDown);
|
|
canvas.value.removeEventListener("mouseup", handleSwipeMouseUp);
|
|
}
|
|
});
|
|
|
|
defineExpose({
|
|
canvas,
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<canvas
|
|
ref="canvas"
|
|
:width="SCREEN_WIDTH"
|
|
:height="SCREEN_HEIGHT"
|
|
:style="{
|
|
margin: '0',
|
|
border: '1px solid red',
|
|
}"
|
|
/>
|
|
|
|
<slot v-if="canvas" />
|
|
</template>
|