feat: screen manager

This commit is contained in:
2025-12-13 18:28:53 +01:00
parent 33f918995b
commit 92f5c83e36
5 changed files with 118 additions and 21 deletions

View File

@@ -1,5 +1,7 @@
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { ScreenManager } from "./screen-manager";
import { HomeScreen } from "./screens/home-screen";
const SCREEN_SOURCE_TEX_SIZE = 1024;
const TOP_SCREEN_SOURCE_TEX_HEIGHT = SCREEN_SOURCE_TEX_SIZE / 404;
@@ -21,19 +23,23 @@ const createScreenCanvas = () => {
return canvas;
};
type Screen = {
type PhysicalScreen = {
mesh: THREE.Mesh;
canvas: HTMLCanvasElement;
texture: THREE.CanvasTexture;
};
export class NDS extends THREE.Object3D {
private botScreen: Screen | null = null;
private topScreen: Screen | null = null;
private botScreen: PhysicalScreen | null = null;
private topScreen: PhysicalScreen | null = null;
private screenManager: ScreenManager;
public constructor(camera: THREE.Camera, domElement: HTMLCanvasElement) {
super();
// Initialize screen manager
this.screenManager = new ScreenManager(new HomeScreen());
const loader = new GLTFLoader();
// load model
loader.load("/nintendo-ds/scene.gltf", ({ scene: model }) => {
@@ -101,10 +107,10 @@ export class NDS extends THREE.Object3D {
emissiveIntensity: 0.5,
});
this.botScreen = { mesh: topScreenMesh, canvas, texture };
this.botScreen = { mesh: botScreenMesh, canvas, texture };
}
domElement.addEventListener("mousemove", (event) => {
domElement.addEventListener("click", (event) => {
const rect = domElement.getBoundingClientRect();
const raycaster = new THREE.Raycaster();
@@ -116,27 +122,17 @@ export class NDS extends THREE.Object3D {
camera,
);
const intersects = raycaster.intersectObjects([
topScreenMesh,
botScreenMesh,
]);
const intersects = raycaster.intersectObject(botScreenMesh);
if (intersects.length > 0) {
const intersection = intersects[0];
const mesh = intersection.object as THREE.Mesh;
const uv = intersection.uv;
const uv = intersects[0].uv;
if (uv) {
const x = Math.floor(uv.x * 256);
const y = Math.floor(
mesh === topScreenMesh
? uv.y * TOP_SCREEN_SOURCE_TEX_HEIGHT * 192
: // invert coords only for bottom screen
192 - (1 - uv.y) * BOT_SCREEN_SOURCE_TEX_HEIGHT * 192,
192 - (1 - uv.y) * BOT_SCREEN_SOURCE_TEX_HEIGHT * 192,
);
x;
y;
this.screenManager.handleTouch(x, y);
}
}
});
@@ -146,10 +142,17 @@ export class NDS extends THREE.Object3D {
}
public update(): void {
if (this.topScreen) {
if (!this.topScreen || !this.botScreen) return;
{
const ctx = this.topScreen.canvas.getContext("2d")!;
this.screenManager.renderTop(ctx);
this.topScreen.texture.needsUpdate = true;
}
if (this.botScreen) {
{
const ctx = this.botScreen.canvas.getContext("2d")!;
this.screenManager.renderBottom(ctx);
this.botScreen.texture.needsUpdate = true;
}
}

35
src/screen-manager.ts Normal file
View File

@@ -0,0 +1,35 @@
import type { Screen, ScreenContext } from "./screen";
export class ScreenManager {
private currentScreen: Screen;
private context: ScreenContext;
constructor(initialScreen: Screen) {
this.currentScreen = initialScreen;
this.context = {
navigate: (screen: Screen) => {
this.currentScreen = screen;
},
};
}
renderTop(ctx: CanvasRenderingContext2D) {
ctx.clearRect(0, 0, 256, 192);
ctx.save();
this.currentScreen.renderTop(ctx);
ctx.restore();
}
renderBottom(ctx: CanvasRenderingContext2D) {
ctx.clearRect(0, 0, 256, 192);
ctx.save();
this.currentScreen.renderBottom(ctx);
ctx.restore();
}
handleTouch(x: number, y: number) {
if (this.currentScreen.handleTouch) {
this.currentScreen.handleTouch(x, y, this.context);
}
}
}

9
src/screen.ts Normal file
View File

@@ -0,0 +1,9 @@
export interface ScreenContext {
navigate: (screen: Screen) => void;
}
export interface Screen {
renderTop(ctx: CanvasRenderingContext2D): void;
renderBottom(ctx: CanvasRenderingContext2D): void;
handleTouch?(x: number, y: number, context: ScreenContext): void;
}

View File

@@ -0,0 +1,25 @@
import type { Screen, ScreenContext } from "../screen";
import { HomeScreen } from "./home-screen";
export class ContactScreen implements Screen {
renderTop(ctx: CanvasRenderingContext2D) {
ctx.fillStyle = "#ffffff";
ctx.font = "10px NDS10";
ctx.textBaseline = "top";
ctx.fillText("CONTACT", 1, 1);
}
renderBottom(ctx: CanvasRenderingContext2D) {
ctx.fillStyle = "#ffffff";
ctx.font = "10px NDS10";
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
ctx.fillText("< Back", 1, 191);
}
handleTouch(x: number, y: number, context: ScreenContext): void {
if (x >= 0 && x <= 51 && y >= 178 && y <= 192) {
context.navigate(new HomeScreen());
}
}
}

View File

@@ -0,0 +1,25 @@
import type { Screen, ScreenContext } from "../screen";
import { ContactScreen } from "./contact-screen";
export class HomeScreen implements Screen {
renderTop(ctx: CanvasRenderingContext2D) {
ctx.fillStyle = "#ffffff";
ctx.font = "10px NDS10";
ctx.textBaseline = "top";
ctx.fillText("HOME", 1, 1);
}
renderBottom(ctx: CanvasRenderingContext2D) {
ctx.fillStyle = "#ffffff";
ctx.font = "10px NDS10";
ctx.textAlign = "right";
ctx.textBaseline = "bottom";
ctx.fillText("Contact >", 255, 191);
}
handleTouch(x: number, y: number, context: ScreenContext): void {
if (x >= 205 && x <= 256 && y >= 178 && y <= 192) {
context.navigate(new ContactScreen());
}
}
}