From 2ca8334c007b9ed4d737cb427541136aa1caf533 Mon Sep 17 00:00:00 2001 From: meelstorm Date: Fri, 17 Jul 2026 17:58:06 +0000 Subject: [PATCH] feat(frontend): LanderCanvas + App wiring cursor and state to WebSocket --- Frontend/src/App.tsx | 35 ++++++++++++++++- Frontend/src/LanderCanvas.tsx | 52 ++++++++++++++++++++++++++ Frontend/src/hooks/useAnimationLoop.ts | 17 +++++++++ 3 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 Frontend/src/LanderCanvas.tsx create mode 100644 Frontend/src/hooks/useAnimationLoop.ts diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index ee50146..a0de5ec 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -1,3 +1,34 @@ -export function App() { - return
Frontend placeholder
; +import { useGameSocket } from './hooks/useGameSocket'; +import { LanderCanvas } from './LanderCanvas'; + +// Vite dev server proxies `/ws` → `ws://localhost:5100`. In prod the backend +// serves the built static files, so same-origin works there too. +function buildWsUrl(): string { + const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${proto}//${window.location.host}/ws/game`; +} + +export function App() { + const { connected, state, sendCursor } = useGameSocket(buildWsUrl()); + + return ( + <> + + + + ); +} + +interface StatusBarProps { connected: boolean; step: number; } +function StatusBar({ connected, step }: StatusBarProps) { + return ( +
+ {' '} + {connected ? 'connected' : 'disconnected'} · step {step} +
+ ); } diff --git a/Frontend/src/LanderCanvas.tsx b/Frontend/src/LanderCanvas.tsx new file mode 100644 index 0000000..10fd66f --- /dev/null +++ b/Frontend/src/LanderCanvas.tsx @@ -0,0 +1,52 @@ +import { useEffect, useRef } from 'react'; +import type { StateFrame } from './protocol'; +import { renderFrame } from './render'; +import { useAnimationLoop } from './hooks/useAnimationLoop'; +import { useCursorSender } from './hooks/useCursorSender'; + +export interface LanderCanvasProps { + state: StateFrame | null; + onCursor: (x: number, y: number) => void; +} + +/** + * Full-viewport canvas. Redraws every animation frame reading the latest + * `state` prop (no interpolation). Mouse movement is captured on the canvas + * and forwarded through `onCursor`. + */ +export function LanderCanvas({ state, onCursor }: LanderCanvasProps) { + const canvasRef = useRef(null); + const stateRef = useRef(state); + stateRef.current = state; + + // Keep the canvas's backing store in sync with its CSS size + devicePixelRatio. + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const resize = () => { + const dpr = window.devicePixelRatio ?? 1; + const rect = canvas.getBoundingClientRect(); + canvas.width = Math.floor(rect.width * dpr); + canvas.height = Math.floor(rect.height * dpr); + }; + resize(); + window.addEventListener('resize', resize); + return () => window.removeEventListener('resize', resize); + }, []); + + useAnimationLoop(() => { + const canvas = canvasRef.current; + if (!canvas) return; + renderFrame(canvas, stateRef.current); + }); + + useCursorSender(canvasRef as React.RefObject, onCursor); + + return ( + + ); +} diff --git a/Frontend/src/hooks/useAnimationLoop.ts b/Frontend/src/hooks/useAnimationLoop.ts new file mode 100644 index 0000000..680a8d5 --- /dev/null +++ b/Frontend/src/hooks/useAnimationLoop.ts @@ -0,0 +1,17 @@ +import { useEffect, useRef } from 'react'; + +/** Calls `callback` once per browser frame. Runs while mounted. */ +export function useAnimationLoop(callback: (now: number) => void) { + const cbRef = useRef(callback); + cbRef.current = callback; + + useEffect(() => { + let rafId = 0; + const loop = (now: number) => { + cbRef.current(now); + rafId = requestAnimationFrame(loop); + }; + rafId = requestAnimationFrame(loop); + return () => cancelAnimationFrame(rafId); + }, []); +}