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