diff --git a/3d_scene_final.jsx b/3d_scene_final.jsx new file mode 100644 index 0000000..e69de29 diff --git a/public/models/3d_scene_final.glb b/public/models/3d_scene_final.glb index c7eb88f..a7a7526 100644 Binary files a/public/models/3d_scene_final.glb and b/public/models/3d_scene_final.glb differ diff --git a/src/modules/how-it-works-3d/Experience3D.jsx b/src/modules/how-it-works-3d/Experience3D.jsx index 6486089..852b374 100644 --- a/src/modules/how-it-works-3d/Experience3D.jsx +++ b/src/modules/how-it-works-3d/Experience3D.jsx @@ -66,6 +66,17 @@ export default function Experience3D() { return () => io.disconnect() }, []) + // Refresh ScrollTrigger when the scene actually mounts. WebGL canvas mounting + // can block the main thread and shift elements, so a refresh here is critical. + useEffect(() => { + if (mountScene) { + const timer = setTimeout(() => { + ScrollTrigger.refresh() + }, 150) + return () => clearTimeout(timer) + } + }, [mountScene]) + // Own Lenis instance (global Lenis is gated off for this route). useEffect(() => { const lenis = new Lenis({ @@ -77,18 +88,14 @@ export default function Experience3D() { setLenis(lenis) lenis.on('scroll', ScrollTrigger.update) - let rafId - function raf(time) { - lenis.raf(time) - rafId = requestAnimationFrame(raf) - } - rafId = requestAnimationFrame(raf) - + // Drive Lenis using GSAP's ticker to ensure synchronization with ScrollTrigger + const tickerCb = (time) => lenis.raf(time * 1000) + gsap.ticker.add(tickerCb) gsap.ticker.lagSmoothing(0) ScrollTrigger.refresh() return () => { - cancelAnimationFrame(rafId) + gsap.ticker.remove(tickerCb) lenis.destroy() setLenis(null) } diff --git a/src/modules/how-it-works-3d/components/ScrollRig.jsx b/src/modules/how-it-works-3d/components/ScrollRig.jsx index 25a39cc..9d798bf 100644 --- a/src/modules/how-it-works-3d/components/ScrollRig.jsx +++ b/src/modules/how-it-works-3d/components/ScrollRig.jsx @@ -3,7 +3,6 @@ import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' import { useSceneStore } from '../store/useSceneStore' import { animateDashboard } from '../animations/dashboardAnimation' -import { playRevealChime } from '../utils/audioHelper' gsap.registerPlugin(ScrollTrigger) @@ -53,7 +52,6 @@ export default function ScrollRig({ dashboardRefs, onPinState }) { } if (section !== activeSectionRef.current) { - playRevealChime() activeSectionRef.current = section } @@ -79,8 +77,13 @@ export default function ScrollRig({ dashboardRefs, onPinState }) { } }, }) + const refreshTimeout = setTimeout(() => { + ScrollTrigger.refresh() + }, 150) + return () => { trigger.kill() + clearTimeout(refreshTimeout) } }, [setScrollProgress, setActiveSection, dashboardRefs, lenis, onPinState]) diff --git a/src/modules/how-it-works-3d/components/TruckAnimation.jsx b/src/modules/how-it-works-3d/components/TruckAnimation.jsx index 76666c6..750c092 100644 --- a/src/modules/how-it-works-3d/components/TruckAnimation.jsx +++ b/src/modules/how-it-works-3d/components/TruckAnimation.jsx @@ -32,10 +32,8 @@ export default function TruckAnimation({ truckRef, wheelRefs }) { // Track wheel rotation accumulation const accumulatedRotationRef = useRef(0) const lastDampedProgressRef = useRef(0) - - - useFrame((state, delta) => { + if (!truckRef.current) return // r3f can emit delta === 0 (coincident frames, the first frame, or after a @@ -78,10 +76,12 @@ export default function TruckAnimation({ truckRef, wheelRefs }) { // Run one-time state initialization for progress trackers if (!initialized.current) { dampedProgressRef.current = truckProgress + dampedProgressRef.current_velocity = 0 lastDampedProgressRef.current = truckProgress lastScrollProgressRef.current = scrollProgress isReversingRef.current = false extraRotationRef.current = 0 + extraRotationRef.current_velocity = 0 const position = truckPath.getPoint(dampedProgressRef.current) let lookAtTargetVector @@ -146,6 +146,13 @@ export default function TruckAnimation({ truckRef, wheelRefs }) { // Smoothly damp the extra rotation angle directly (prevents pitch/roll glitches or 3D target collapse) easing.damp(extraRotationRef, 'current', targetExtraRotation, 0.20, dt) + // Defensive: reset extra rotation if it becomes NaN + if (!Number.isFinite(extraRotationRef.current)) { + extraRotationRef.current = targetExtraRotation + extraRotationRef.current_velocity = 0 + if (extraRotationRef.__damp) extraRotationRef.__damp = {} + } + // Apply the yaw pivot around the local vertical axis truckRef.current.rotateY(extraRotationRef.current) diff --git a/src/modules/how-it-works-3d/models/Scene3D.jsx b/src/modules/how-it-works-3d/models/Scene3D.jsx index d119eb1..af6824e 100644 --- a/src/modules/how-it-works-3d/models/Scene3D.jsx +++ b/src/modules/how-it-works-3d/models/Scene3D.jsx @@ -5,7 +5,7 @@ Auto-generated by: https://github.com/pmndrs/gltfjsx import React, { useRef } from 'react' import { useGLTF } from '@react-three/drei' -export function Model(props) { +export function Model({ truckRef, wheelRefs, dashboardRefs, ...props }) { const { nodes, materials } = useGLTF('/models/3d_scene_final.glb') return ( @@ -93,10 +93,16 @@ export function Model(props) { + + - + @@ -420,7 +433,7 @@ export function Model(props) { /> @@ -433,7 +446,7 @@ export function Model(props) { /> @@ -446,7 +459,7 @@ export function Model(props) { /> @@ -7526,20 +7539,12 @@ export function Model(props) { material={materials['DOORMILE bright red signage']} position={[-18.384, 4.82, -4.46]} /> - - - + { - if (isUnlocked) return; - - const AudioContextClass = window.AudioContext || window.webkitAudioContext; - if (!AudioContextClass) return; - - if (!audioContext) { - audioContext = new AudioContextClass(); - } - - // Resume context if suspended (browser autoplay policy) - if (audioContext.state === 'suspended') { - audioContext.resume().then(() => { - isUnlocked = true; - cleanupListeners(); - }).catch(() => {}); - } else { - isUnlocked = true; - cleanupListeners(); - } -}; - -const cleanupListeners = () => { - window.removeEventListener('click', initAudio); - window.removeEventListener('keydown', initAudio); - window.removeEventListener('touchstart', initAudio); - window.removeEventListener('wheel', initAudio); -}; - -// Add listeners for early activation -if (typeof window !== 'undefined') { - window.addEventListener('click', initAudio, { passive: true }); - window.addEventListener('keydown', initAudio, { passive: true }); - window.addEventListener('touchstart', initAudio, { passive: true }); - window.addEventListener('wheel', initAudio, { passive: true }); -} - -// Play a high-tech UI chime sound for card reveal -export const playRevealChime = () => { - try { - const AudioContextClass = window.AudioContext || window.webkitAudioContext; - if (!AudioContextClass) return; - - if (!audioContext) { - audioContext = new AudioContextClass(); - } - - if (audioContext.state === 'suspended') { - audioContext.resume().catch(() => {}); - } - - const now = audioContext.currentTime; - - // Master Volume node with exponential decay - const masterGain = audioContext.createGain(); - masterGain.gain.setValueAtTime(0, now); - masterGain.gain.linearRampToValueAtTime(0.15, now + 0.04); // subtle fade-in to avoid clicking - masterGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.4); // smooth tail decay - - // Warm base oscillator (triangle wave) - const baseOsc = audioContext.createOscillator(); - baseOsc.type = 'triangle'; - baseOsc.frequency.setValueAtTime(329.63, now); // E4 pitch - baseOsc.frequency.exponentialRampToValueAtTime(523.25, now + 0.25); // Slide up to C5 - - // High harmonic chime oscillator (sine wave) - const chimeOsc = audioContext.createOscillator(); - chimeOsc.type = 'sine'; - chimeOsc.frequency.setValueAtTime(659.25, now); // E5 pitch - chimeOsc.frequency.exponentialRampToValueAtTime(1046.50, now + 0.25); // Slide up to C6 - - // Connect nodes - baseOsc.connect(masterGain); - chimeOsc.connect(masterGain); - masterGain.connect(audioContext.destination); - - // Play oscillators - baseOsc.start(now); - baseOsc.stop(now + 0.4); - chimeOsc.start(now); - chimeOsc.stop(now + 0.4); - } catch (error) { - console.warn('Playback of reveal chime failed:', error); - } -};