From cc133d5734d957bedfa9d7af9b1fa8b80174b331 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:24:39 +0200
Subject: [PATCH] feat: mount 3d app shell with camera rig
Add busPath curve helpers (pointAt/tangentAt), CameraRig as the
sole consumer of useRide.frame(), and App.tsx wiring the R3F
Canvas at low dpr for chunky pixels. Replace the boot.ts stub
with the real React root mount and append fixed-canvas CSS.
---
src/App.tsx | 23 +++++++++++++++++++++++
src/boot.ts | 6 +++++-
src/scene/CameraRig.tsx | 23 +++++++++++++++++++++++
src/scene/path.ts | 22 ++++++++++++++++++++++
src/style.css | 3 +++
5 files changed, 76 insertions(+), 1 deletion(-)
create mode 100644 src/App.tsx
create mode 100644 src/scene/CameraRig.tsx
create mode 100644 src/scene/path.ts
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..24e703f
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,23 @@
+import { Canvas } from '@react-three/fiber'
+import { CameraRig } from './scene/CameraRig'
+import { useRideInput } from './ride/useRideInput'
+
+export default function App() {
+ useRideInput()
+ return (
+
+
+
+ )
+}
diff --git a/src/boot.ts b/src/boot.ts
index 84a783a..e2bcf40 100644
--- a/src/boot.ts
+++ b/src/boot.ts
@@ -1,3 +1,7 @@
+import { createRoot } from 'react-dom/client'
+import { createElement } from 'react'
+import App from './App'
+
export function boot(): void {
- console.log('demo boot placeholder — replaced by Task 5')
+ createRoot(document.getElementById('root')!).render(createElement(App))
}
diff --git a/src/scene/CameraRig.tsx b/src/scene/CameraRig.tsx
new file mode 100644
index 0000000..bc704f1
--- /dev/null
+++ b/src/scene/CameraRig.tsx
@@ -0,0 +1,23 @@
+import { useFrame } from '@react-three/fiber'
+import { Vector3 } from 'three'
+import { pointAt, tangentAt } from './path'
+import { useRide } from '../ride/store'
+
+const eye = new Vector3()
+const look = new Vector3()
+
+export function CameraRig() {
+ useFrame((state, dt) => {
+ useRide.getState().frame(Math.min(dt, 0.1))
+ const t = useRide.getState().t
+ const p = pointAt(t)
+ const tan = tangentAt(t)
+ eye.copy(p).addScaledVector(tan, -11)
+ eye.y += 6.5
+ eye.x += Math.sin(state.clock.elapsedTime * 0.4) * 0.6 // gentle sway
+ look.copy(p).addScaledVector(tan, 6)
+ state.camera.position.lerp(eye, Math.min(1, dt * 4))
+ state.camera.lookAt(look)
+ })
+ return null
+}
diff --git a/src/scene/path.ts b/src/scene/path.ts
new file mode 100644
index 0000000..93aad5e
--- /dev/null
+++ b/src/scene/path.ts
@@ -0,0 +1,22 @@
+import { CatmullRomCurve3, Vector3 } from 'three'
+
+const POINTS: Array<[number, number, number]> = [
+ [0, 0, 0], [18, 0, -6], [34, 0, 4], [52, 0, -2],
+ [70, 0, -14], [88, 0, -6], [104, 0, 6], [122, 0, 0],
+ [140, 0, -10], [158, 0, -2], [176, 0, 8], [196, 0, 0],
+]
+
+export const busPath = new CatmullRomCurve3(
+ POINTS.map((p) => new Vector3(...p)),
+ false,
+ 'catmullrom',
+ 0.3,
+)
+
+export function pointAt(t: number): Vector3 {
+ return busPath.getPointAt(Math.min(1, Math.max(0, t)))
+}
+
+export function tangentAt(t: number): Vector3 {
+ return busPath.getTangentAt(Math.min(1, Math.max(0, t)))
+}
diff --git a/src/style.css b/src/style.css
index 4f83ce5..d4369f4 100644
--- a/src/style.css
+++ b/src/style.css
@@ -30,3 +30,6 @@ body.demo-on #fallback {
clip-path: inset(50%);
white-space: nowrap;
}
+
+#demo { position: fixed; inset: 0; }
+#demo canvas { image-rendering: pixelated; width: 100%; height: 100%; }