diff --git a/public/models/Textures/colormap-car.png b/public/models/Textures/colormap-car.png new file mode 100644 index 0000000..5c4b185 Binary files /dev/null and b/public/models/Textures/colormap-car.png differ diff --git a/public/models/Textures/colormap-road.png b/public/models/Textures/colormap-road.png new file mode 100644 index 0000000..7c87ab9 Binary files /dev/null and b/public/models/Textures/colormap-road.png differ diff --git a/public/models/cone.glb b/public/models/cone.glb new file mode 100644 index 0000000..fda51e9 Binary files /dev/null and b/public/models/cone.glb differ diff --git a/public/models/construction-barrier.glb b/public/models/construction-barrier.glb new file mode 100644 index 0000000..2e3236b Binary files /dev/null and b/public/models/construction-barrier.glb differ diff --git a/public/models/light-curved.glb b/public/models/light-curved.glb new file mode 100644 index 0000000..330d600 Binary files /dev/null and b/public/models/light-curved.glb differ diff --git a/public/models/radio.glb b/public/models/radio.glb new file mode 100644 index 0000000..6a810e8 Binary files /dev/null and b/public/models/radio.glb differ diff --git a/public/models/sign-highway-wide.glb b/public/models/sign-highway-wide.glb new file mode 100644 index 0000000..8171a0f Binary files /dev/null and b/public/models/sign-highway-wide.glb differ diff --git a/public/models/sign-highway.glb b/public/models/sign-highway.glb new file mode 100644 index 0000000..3e42954 Binary files /dev/null and b/public/models/sign-highway.glb differ diff --git a/public/models/speaker.glb b/public/models/speaker.glb new file mode 100644 index 0000000..2d259ce Binary files /dev/null and b/public/models/speaker.glb differ diff --git a/public/models/speakerSmall.glb b/public/models/speakerSmall.glb new file mode 100644 index 0000000..5673177 Binary files /dev/null and b/public/models/speakerSmall.glb differ diff --git a/public/models/televisionVintage.glb b/public/models/televisionVintage.glb new file mode 100644 index 0000000..93d8795 Binary files /dev/null and b/public/models/televisionVintage.glb differ diff --git a/public/models/van.glb b/public/models/van.glb new file mode 100644 index 0000000..f90bb1f Binary files /dev/null and b/public/models/van.glb differ diff --git a/src/scene/Bus.tsx b/src/scene/Bus.tsx index 4cae2b8..4a5ef09 100644 --- a/src/scene/Bus.tsx +++ b/src/scene/Bus.tsx @@ -1,13 +1,28 @@ import { useRef } from 'react' import { useFrame } from '@react-three/fiber' +import { Clone, useGLTF } from '@react-three/drei' import { Group, Vector3 } from 'three' import { pointAt, tangentAt } from './path' import { useRide } from '../ride/store' const ahead = new Vector3() +// Kenney Car Kit van: body ~2.75 units long natively (wheel-front z=+0.76 +// to body rear z=-1.4). Scale it up to match the old ~4.6-unit footprint. +const VAN_SCALE = 1.7 +// wheel-front-{left,right} nodes sit at z=+0.76 (native), the body rear at +// z=-1.4 — the model's own "nose" points down its local +Z axis. For a +// plain (non-camera) Object3D, Object3D.lookAt orients local +Z toward the +// look target, so this group's +Z is already the direction of travel after +// the lookAt below: the van needs no corrective yaw. +const VAN_FORWARD_CORRECTION = 0 + +// roof top sits at ~2.3 world units after scaling (body top 1.35 * 1.7) +const ROOF_Y = 2.3 + export function Bus() { const ref = useRef(null) + const { scene } = useGLTF('/models/van.glb') useFrame((state) => { const t = useRide.getState().t const g = ref.current! @@ -18,43 +33,19 @@ export function Bus() { }) return ( - {/* body, two-tone Électron Fou paint job */} - - - - - - - - - {/* windshield */} - - - - - {/* wheels */} - {([[-1.1, 1.5], [1.1, 1.5], [-1.1, -1.5], [1.1, -1.5]] as const).map(([x, z], i) => ( - - - - - ))} - {/* headlights */} - {[-0.7, 0.7].map((x, i) => ( - - - - - ))} - {/* crazy antenna: a zap sphere on a pole */} - + + {/* crazy antenna: a zap sphere on a pole, mounted on the roof — the one + fun accent kept from the old hand-built bus */} + - + ) } + +useGLTF.preload('/models/van.glb') diff --git a/src/scene/World.tsx b/src/scene/World.tsx index 913e4c2..22a9d0b 100644 --- a/src/scene/World.tsx +++ b/src/scene/World.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { Instance, Instances } from '@react-three/drei' +import { Clone, Instance, Instances, useGLTF } from '@react-three/drei' import { TubeGeometry, Vector3 } from 'three' import { busPath, pointAt, tangentAt } from './path' import { mulberry32 } from './rng' @@ -10,6 +10,28 @@ interface Item { scale: [number, number, number] } +interface Lamp { + pos: [number, number, number] + rotY: number +} + +const LAMP_TS = [0.06, 0.18, 0.3, 0.42, 0.55, 0.68, 0.8, 0.92] +const LAMP_SCALE = 6 +const LAMP_OFFSET = 5 + +function lampLayout(): Lamp[] { + return LAMP_TS.map((t, i) => { + const p = pointAt(t) + const tan = tangentAt(t) + const n = new Vector3(-tan.z, 0, tan.x) // unit normal, tangent is already unit length + const side = i % 2 === 0 ? 1 : -1 + const pos = p.clone().addScaledVector(n, side * LAMP_OFFSET) + // face the lamp's curved arm back toward the road regardless of path curvature + const rotY = Math.atan2(side * n.x, side * n.z) + return { pos: [pos.x, 0, pos.z], rotY } + }) +} + function layout(seed: number, count: number, minOff: number, maxOff: number): Item[] { const rng = mulberry32(seed) const items: Item[] = [] @@ -35,7 +57,9 @@ export function World() { const caps = useMemo(() => layout(4242, 50, 8, 38), []) const resistors = useMemo(() => layout(90210, 50, 7, 36), []) const vias = useMemo(() => layout(555, 80, 6, 44), []) + const lamps = useMemo(() => lampLayout(), []) const trace = useMemo(() => new TubeGeometry(busPath, 200, 0.5, 6, false), []) + const { scene: lampScene } = useGLTF('/models/light-curved.glb') return ( @@ -80,6 +104,12 @@ export function World() { ))} + {/* street lamps lining the road */} + {lamps.map((l, i) => ( + + ))} ) } + +useGLTF.preload('/models/light-curved.glb') diff --git a/src/scene/stops/StopElectronFou.tsx b/src/scene/stops/StopElectronFou.tsx index 185ceb9..5338c10 100644 --- a/src/scene/stops/StopElectronFou.tsx +++ b/src/scene/stops/StopElectronFou.tsx @@ -1,14 +1,46 @@ import { useRef } from 'react' import { useFrame } from '@react-three/fiber' +import { Clone, useGLTF } from '@react-three/drei' import { Mesh, MeshStandardMaterial } from 'three' import { StopPanel, stopAnchor } from './StopPanel' import { STOPS } from '../../content/stops' import { MUSIC } from '../../content/music' +// Kenney furniture-kit pieces are authored with a corner origin (spanning +// +X, +Y, -Z from [0,0,0]) rather than a centered pivot — these helpers +// re-center each clone at a given world position. +function centered(w: number, h: number, d: number, x: number, yBottom: number, z: number): [number, number, number] { + return [x - w / 2, yBottom, z + d / 2] +} + +const SPEAKER_SCALE = 5 +const SPK_W = 0.148 * SPEAKER_SCALE +const SPK_H = 0.6364 * SPEAKER_SCALE +const SPK_D = 0.148 * SPEAKER_SCALE +const COL_GAP = SPK_W * 1.08 +const COL_X = [-COL_GAP / 2, COL_GAP / 2] +const ROW_Y = [0, SPK_H] +const STACK_TOP = ROW_Y[1] + SPK_H + +const SMALL_SCALE = 4.4 +const SMALL_W = 0.148 * SMALL_SCALE +const SMALL_D = 0.1332 * SMALL_SCALE + +const GEAR_SCALE = 3.2 +const RADIO_W = 0.315 * GEAR_SCALE +const RADIO_D = 0.0975 * GEAR_SCALE +const TV_W = 0.41 * GEAR_SCALE +const TV_D = 0.27 * GEAR_SCALE + +const TUBE_Y = STACK_TOP + 1.0 + export function StopElectronFou() { const tubes = useRef>([]) - const needles = useRef>([]) - const ampPos = stopAnchor(STOPS[2], 12) + const ampPos = stopAnchor(STOPS[2], 8) + const { scene: speakerScene } = useGLTF('/models/speaker.glb') + const { scene: speakerSmallScene } = useGLTF('/models/speakerSmall.glb') + const { scene: radioScene } = useGLTF('/models/radio.glb') + const { scene: tvScene } = useGLTF('/models/televisionVintage.glb') useFrame((state) => { const time = state.clock.elapsedTime @@ -17,9 +49,6 @@ export function StopElectronFou() { const mat = m.material as MeshStandardMaterial mat.emissiveIntensity = 1.4 + Math.sin(time * 4 + i * 1.7) * 0.9 }) - needles.current.forEach((m, i) => { - if (m) m.rotation.z = Math.sin(time * 6 + i) * 0.5 - }) }) return ( @@ -29,35 +58,48 @@ export function StopElectronFou() {

{MUSIC.blurb}

♫ {MUSIC.listenLabel} - {/* the giant tube amp behind the panel */} + {/* the wall of speakers behind the panel */} - - - - - - - - - {[-3.2, -1.1, 1.1, 3.2].map((x, i) => ( - { tubes.current[i] = el }} position={[x, 5.2, 0]}> - + {ROW_Y.map((y, ri) => + COL_X.map((x, ci) => ( + + )), + )} + + + + {/* pulsing emissive tubes rising behind the stack — demoscene heartbeat */} + {[-0.9, -0.32, 0.32, 0.9].map((x, i) => ( + { tubes.current[i] = el }} position={[x, TUBE_Y, -0.5]}> + ))} - {[-2, 2].map((x, i) => ( - - - - - - { needles.current[i] = el }} position={[0, -0.2, 0.05]}> - - - - - ))} ) } + +useGLTF.preload('/models/speaker.glb') +useGLTF.preload('/models/speakerSmall.glb') +useGLTF.preload('/models/radio.glb') +useGLTF.preload('/models/televisionVintage.glb') diff --git a/src/scene/stops/StopGreetings.tsx b/src/scene/stops/StopGreetings.tsx index 3992293..9403547 100644 --- a/src/scene/stops/StopGreetings.tsx +++ b/src/scene/stops/StopGreetings.tsx @@ -1,22 +1,61 @@ -import { StopPanel } from './StopPanel' +import { useMemo } from 'react' +import { Clone, useGLTF } from '@react-three/drei' +import { StopPanel, stopAnchor } from './StopPanel' import { STOPS } from '../../content/stops' import { GREETINGS } from '../../content/greetings' import { useRide } from '../../ride/store' +// "end of the road" wink: cones + barriers clustered near the terminus +const CONES = [ + { dx: 3, dz: 1, scale: 1.4, rotY: 0.2 }, + { dx: 3.8, dz: -0.4, scale: 1.2, rotY: 1.1 }, + { dx: 2.6, dz: -1.6, scale: 1.5, rotY: 2.4 }, +] as const +const BARRIERS = [ + { dx: 4.6, dz: 0.4, scale: 7.5, rotY: -0.4 }, + { dx: 4.9, dz: -1.4, scale: 7.2, rotY: 0.6 }, +] as const + export function StopGreetings() { + const base = useMemo(() => stopAnchor(STOPS[3]), []) + const { scene: coneScene } = useGLTF('/models/cone.glb') + const { scene: barrierScene } = useGLTF('/models/construction-barrier.glb') return ( - -

Greetings

-

Respect éternel à :

-
    - {GREETINGS.map((g) => ( -
  • {g}
  • - ))} -
-

Code + design : Clément Saillant. Aucun cookie n'a été blessé.

- -
+ <> + +

Greetings

+

Respect éternel à :

+
    + {GREETINGS.map((g) => ( +
  • {g}
  • + ))} +
+

Code + design : Clément Saillant. Aucun cookie n'a été blessé.

+ +
+ {CONES.map((c, i) => ( + + ))} + {BARRIERS.map((b, i) => ( + + ))} + ) } + +useGLTF.preload('/models/cone.glb') +useGLTF.preload('/models/construction-barrier.glb') diff --git a/src/scene/stops/StopIdentities.tsx b/src/scene/stops/StopIdentities.tsx index 481e053..bd5deac 100644 --- a/src/scene/stops/StopIdentities.tsx +++ b/src/scene/stops/StopIdentities.tsx @@ -1,16 +1,41 @@ -import { StopPanel } from './StopPanel' +import { useMemo } from 'react' +import { Clone, useGLTF } from '@react-three/drei' +import { StopPanel, stopAnchor } from './StopPanel' import { STOPS } from '../../content/stops' import { LINKS } from '../../content/links' +// small decorative sign forest behind the panel — décor only, the real +// links stay in the Html panel below +const FOREST = [ + { dx: -3.5, dz: -2.5, scale: 4.2, rotY: 0.4 }, + { dx: 4, dz: -3.5, scale: 5.5, rotY: -0.9 }, + { dx: 1.5, dz: -5, scale: 3.4, rotY: 1.8 }, +] as const + export function StopIdentities() { + const base = useMemo(() => stopAnchor(STOPS[1]), []) + const { scene } = useGLTF('/models/sign-highway.glb') return ( - -

Identités

- {LINKS.map((l) => ( - - → {l.label} - + <> + +

Identités

+ {LINKS.map((l) => ( + + → {l.label} + + ))} +
+ {FOREST.map((f, i) => ( + ))} -
+ ) } + +useGLTF.preload('/models/sign-highway.glb') diff --git a/src/scene/stops/StopPanel.tsx b/src/scene/stops/StopPanel.tsx index fad4c5a..28d921f 100644 --- a/src/scene/stops/StopPanel.tsx +++ b/src/scene/stops/StopPanel.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { useMemo } from 'react' -import { Html, Text } from '@react-three/drei' +import { Clone, Html, Text, useGLTF } from '@react-three/drei' import { Vector3 } from 'three' import type { Stop } from '../../content/stops' import { pointAt, tangentAt } from '../path' @@ -13,6 +13,31 @@ export function stopAnchor(stop: Stop, lateral = 6): Vector3 { return p.clone().addScaledVector(n, lateral) } +// sign-highway-wide.glb: twin posts (0..0.707 tall) framing a board around +// the 0.41-0.53 band, board front face on its native -X side. Scaled up so +// the board reads clearly above the Html panel (which stays untouched). +const SIGN_SCALE = 8 +const BOARD_Y = 0.5 * SIGN_SCALE +const BOARD_Z = 0.11 * SIGN_SCALE + +const LAMP_SCALE = 6 + +// small deterministic hash so each stop's lamp gets a stable yaw variation +function hash(id: string): number { + let h = 0 + for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0 + return h +} + +// face the lamp's curved arm back toward the road, plus a per-stop wobble +function lampYaw(stop: Stop): number { + const tan = tangentAt(stop.t) + const n = new Vector3(-tan.z, 0, tan.x) + const base = Math.atan2(n.x, n.z) + const wobble = ((hash(stop.id) % 1000) / 1000 - 0.5) * 0.6 + return base + wobble +} + export function StopPanel({ stop, children, width = 340 }: { stop: Stop children: ReactNode @@ -20,24 +45,24 @@ export function StopPanel({ stop, children, width = 340 }: { }) { const pos = useMemo(() => stopAnchor(stop), [stop]) const active = useRide((s) => s.stop?.id === stop.id) + const { scene: signScene } = useGLTF('/models/sign-highway-wide.glb') + const { scene: lampScene } = useGLTF('/models/light-curved.glb') return ( - {/* pole */} - - - - - {/* bus-stop sign */} - - - - - + {/* bus-stop sign: billboard-on-posts (native board faces its local + -X; rotate 90° so the face reads along +Z toward the road) */} + + {stop.title}
{children}
+ {/* street lamp beside the stop */} +
) } + +useGLTF.preload('/models/sign-highway-wide.glb') +useGLTF.preload('/models/light-curved.glb')