import { Renderer, UiState } from './engine/renderer' import { Input } from './ui/input' import { DomOverlay } from './ui/overlay' import { Router, Route } from './router' import { PAGES, PROJECTS_SCENE, RESUME_SCENE } from './data/pages' const DPR_CAP = 2 export class App { private canvas: HTMLCanvasElement private renderer = new Renderer() private input!: Input private overlay!: DomOverlay private router = new Router() private dpr = 1 private res = new Float32Array(2) private mouse = new Float32Array(2) private scene = 0 private prevScene = -1 private startTime = 0 private rafId = 0 private running = false private stemMultipliers = new Float32Array(32) constructor(canvas: HTMLCanvasElement) { this.canvas = canvas } async start(): Promise { this.input = new Input() // Build SceneContent[] from PAGES for the overlay const scenes = PAGES.map((p) => ({ title: p.title, subtitle: p.subtitle, body: p.body, })) this.overlay = new DomOverlay(scenes, { onNavClick: (s) => { this.router.navigate(Router.sceneToBasePath(s)) }, onButtonClick: () => { const next = (this.scene + 1) % 4 this.router.navigate(Router.sceneToBasePath(next)) }, onNavigate: (path) => { this.router.navigate(path) }, }) this.router.onChange((route) => this.applyRoute(route)) const initial = this.router.start() this.applyRoute(initial) this.router.replace(Router.sceneToBasePath(this.scene)) this.resize() const ok = await this.renderer.init(this.canvas) if (!ok) return false this.startTime = performance.now() this.running = true this.loop() return true } stop(): void { this.running = false cancelAnimationFrame(this.rafId) } // ---- routing ---- private applyRoute(route: Route): void { this.scene = route.scene this.overlay.setScene(this.scene) if (route.scene === PROJECTS_SCENE && 'view' in route) { if (route.view === 'category' && 'catId' in route && route.catId) { this.overlay.showCategory(route.catId) } else if ( route.view === 'detail' && 'catId' in route && route.catId && 'projectId' in route && route.projectId ) { this.overlay.showProjectDetail(route.projectId, route.catId) } else { this.overlay.showCategoryGrid() } } else if (route.scene === RESUME_SCENE) { this.overlay.showResume() } } // ---- rendering ---- private resize(): void { this.dpr = Math.min(window.devicePixelRatio || 1, DPR_CAP) const w = Math.floor(window.innerWidth * this.dpr) const h = Math.floor(window.innerHeight * this.dpr) if (this.canvas.width !== w || this.canvas.height !== h) { this.canvas.width = w this.canvas.height = h this.res[0] = w this.res[1] = h if (this.renderer.ready) this.renderer.resize() } } private computeStemMultipliers(time: number): void { const sm = this.stemMultipliers const colorBase = [0.0, 0.6, 1.2, 2.4] const speedBase = [0.0, 0.2, 0.1, 0.15] const scene = this.scene sm[0] = speedBase[scene] sm[2] = colorBase[scene] * 0.2 sm[3] = 0.3 + 0.3 * Math.sin(time * 0.5) sm[4] = colorBase[scene] * 0.4 sm[8] = 0.1 } private loop = (): void => { if (!this.running) return this.rafId = requestAnimationFrame(this.loop) const now = performance.now() const time = (now - this.startTime) / 1000 this.resize() const mx = this.input.x * this.dpr const my = this.res[1] - this.input.y * this.dpr this.mouse[0] = mx this.mouse[1] = my if (this.scene !== this.prevScene) { this.prevScene = this.scene } this.computeStemMultipliers(time) const state: UiState = { resolution: this.res, time, scene: this.scene, mouse: this.mouse, stemMultipliers: this.stemMultipliers, } this.renderer.render(state) } }