From fc5621bd24874fa481bbf9d195935891ab4bfca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:50:24 +0200 Subject: [PATCH] feat: add sine scroller and tracker panel hud Implement Task 10: HUD components for LE BUS demoscene page. - Scroller.tsx: canvas-2D sine-wave text animation at bottom - TrackerPanel.tsx: play/mute control panel (top-right) - modPlayer.ts: interface stubs (ModState, ModPlayer) - CSS styling for both components appended to style.css --- src/App.tsx | 4 ++++ src/audio/modPlayer.ts | 8 ++++++++ src/hud/Scroller.tsx | 42 ++++++++++++++++++++++++++++++++++++++++ src/hud/TrackerPanel.tsx | 39 +++++++++++++++++++++++++++++++++++++ src/style.css | 23 ++++++++++++++++++++++ 5 files changed, 116 insertions(+) create mode 100644 src/audio/modPlayer.ts create mode 100644 src/hud/Scroller.tsx create mode 100644 src/hud/TrackerPanel.tsx diff --git a/src/App.tsx b/src/App.tsx index cb5e90f..14054e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,8 @@ import { Passengers } from './scene/Passengers' import { Sky } from './scene/Sky' import { Stops } from './scene/stops/Stops' import { useRideInput } from './ride/useRideInput' +import { Scroller } from './hud/Scroller' +import { TrackerPanel } from './hud/TrackerPanel' export default function App() { useRideInput() @@ -26,6 +28,8 @@ export default function App() { + + ) } diff --git a/src/audio/modPlayer.ts b/src/audio/modPlayer.ts new file mode 100644 index 0000000..32fbd58 --- /dev/null +++ b/src/audio/modPlayer.ts @@ -0,0 +1,8 @@ +export type ModState = 'idle' | 'loading' | 'playing' | 'paused' | 'unavailable' + +export interface ModPlayer { + state: ModState + toggle(): Promise + setMuted(muted: boolean): void + onChange(fn: (s: ModState) => void): void +} diff --git a/src/hud/Scroller.tsx b/src/hud/Scroller.tsx new file mode 100644 index 0000000..dc4d55a --- /dev/null +++ b/src/hud/Scroller.tsx @@ -0,0 +1,42 @@ +import { useEffect, useRef } from 'react' +import { SCROLLER_TEXT } from '../content/greetings' + +const CHAR_W = 26 +const SPEED = 2.2 + +export function Scroller() { + const ref = useRef(null) + useEffect(() => { + const cv = ref.current! + const ctx = cv.getContext('2d')! + let raf = 0 + let x = window.innerWidth + const resize = () => { + cv.width = window.innerWidth + cv.height = 72 + } + resize() + window.addEventListener('resize', resize) + const total = SCROLLER_TEXT.length * CHAR_W + const loop = (now: number) => { + ctx.clearRect(0, 0, cv.width, cv.height) + ctx.font = 'bold 40px "Courier New", monospace' + ctx.fillStyle = '#ffd23f' + x -= SPEED + if (x < -total) x = window.innerWidth + for (let i = 0; i < SCROLLER_TEXT.length; i++) { + const cx = x + i * CHAR_W + if (cx < -CHAR_W || cx > cv.width) continue + const cy = 42 + Math.sin(cx * 0.018 + now * 0.004) * 14 + ctx.fillText(SCROLLER_TEXT[i], cx, cy) + } + raf = requestAnimationFrame(loop) + } + raf = requestAnimationFrame(loop) + return () => { + cancelAnimationFrame(raf) + window.removeEventListener('resize', resize) + } + }, []) + return