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 fbef78e..95ecf79 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);
- }
-};