Update How It Works 3D page #1
0
3d_scene_final.jsx
Normal file
0
3d_scene_final.jsx
Normal file
Binary file not shown.
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<group {...props} dispose={null}>
|
||||
@@ -93,10 +93,16 @@ export function Model(props) {
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={nodes.DoorMile_Logo_FirstWarehouse.geometry}
|
||||
material={materials.DoorMile_Logo_Image_Material}
|
||||
position={[-8.676, 0.035, -5.18]}
|
||||
rotation={[0, -0.121, 0]}
|
||||
geometry={nodes.Uploaded_DoorMile_Logo_FirstWarehouse.geometry}
|
||||
material={materials.DoorMile_Uploaded_Red_Logo_Material}
|
||||
/>
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={nodes.Uploaded_DoorMile_Logo_FrontFascia.geometry}
|
||||
material={materials.DoorMile_Uploaded_White_Logo_Material}
|
||||
position={[-7.462, 2.155, -1.793]}
|
||||
scale={0.597}
|
||||
/>
|
||||
<mesh
|
||||
castShadow
|
||||
@@ -164,6 +170,13 @@ export function Model(props) {
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={nodes.ABC_Factory_HubSign_TextPanel.geometry}
|
||||
material={materials.ABC_Factory_TextPanel_Material}
|
||||
position={[0, -0.141, 0]}
|
||||
/>
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
@@ -171,7 +184,7 @@ export function Model(props) {
|
||||
material={materials.Dark_Asphalt}
|
||||
position={[0.013, -0.077, -0.026]}
|
||||
/>
|
||||
<group ref={props.truckRef} position={[14.891, 0.284, -25.037]} rotation={[-Math.PI / 2, 0, -2.318]} scale={0.66}>
|
||||
<group ref={truckRef} position={[14.891, 0.284, -25.037]} rotation={[-Math.PI / 2, 0, -2.318]} scale={0.66}>
|
||||
<group rotation={[Math.PI / 2, 0, 0]}>
|
||||
<group position={[0.013, 0.67, 0.006]} rotation={[0, -Math.PI / 2, 0]} scale={1.155}>
|
||||
<mesh
|
||||
@@ -407,7 +420,7 @@ export function Model(props) {
|
||||
/>
|
||||
</group>
|
||||
<group
|
||||
ref={props.wheelRefs && props.wheelRefs[0]}
|
||||
ref={wheelRefs?.[0]}
|
||||
position={[1.873, 0.356, -0.899]}
|
||||
rotation={[-Math.PI / 2, 0, -Math.PI]}
|
||||
scale={[1.059, 1.044, 1.059]}>
|
||||
@@ -420,7 +433,7 @@ export function Model(props) {
|
||||
/>
|
||||
</group>
|
||||
<group
|
||||
ref={props.wheelRefs && props.wheelRefs[1]}
|
||||
ref={wheelRefs?.[1]}
|
||||
position={[1.873, 0.356, 0.91]}
|
||||
rotation={[Math.PI / 2, 0, Math.PI]}
|
||||
scale={[1.059, 1.044, 1.059]}>
|
||||
@@ -433,7 +446,7 @@ export function Model(props) {
|
||||
/>
|
||||
</group>
|
||||
<group
|
||||
ref={props.wheelRefs && props.wheelRefs[2]}
|
||||
ref={wheelRefs?.[2]}
|
||||
position={[-1.472, 0.356, -0.876]}
|
||||
rotation={[-Math.PI / 2, 0, -Math.PI]}
|
||||
scale={[1.059, 0.662, 1.059]}>
|
||||
@@ -446,7 +459,7 @@ export function Model(props) {
|
||||
/>
|
||||
</group>
|
||||
<group
|
||||
ref={props.wheelRefs && props.wheelRefs[3]}
|
||||
ref={wheelRefs?.[3]}
|
||||
position={[-1.472, 0.356, 0.886]}
|
||||
rotation={[Math.PI / 2, 0, Math.PI]}
|
||||
scale={[1.059, 0.662, 1.059]}>
|
||||
@@ -7526,20 +7539,12 @@ export function Model(props) {
|
||||
material={materials['DOORMILE bright red signage']}
|
||||
position={[-18.384, 4.82, -4.46]}
|
||||
/>
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={nodes.white_DOORMILE_brand_letters.geometry}
|
||||
material={materials['crisp white lettering and lines']}
|
||||
position={[-18.384, 5.08, -4.285]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
/>
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={nodes.white_in_transit_hub_operations_subtitle.geometry}
|
||||
material={materials['crisp white lettering and lines']}
|
||||
position={[-18.384, 4.5, -4.282]}
|
||||
position={[-18.384, 4.5, -4.33]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
/>
|
||||
<mesh
|
||||
@@ -10592,24 +10597,7 @@ export function Model(props) {
|
||||
rotation={[Math.PI, -1.081, Math.PI]}
|
||||
scale={0.608}
|
||||
/>
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={nodes.large_white_DOORMILE_letters.geometry}
|
||||
material={materials['white raised sign lettering']}
|
||||
position={[11.723, 4.419, 13.869]}
|
||||
rotation={[Math.PI / 2, 0, 2.061]}
|
||||
scale={0.608}
|
||||
/>
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={nodes.white_delivery_to_customer_tagline.geometry}
|
||||
material={materials['white raised sign lettering']}
|
||||
position={[11.718, 4.078, 13.866]}
|
||||
rotation={[Math.PI / 2, 0, 2.061]}
|
||||
scale={0.608}
|
||||
/>
|
||||
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
let audioContext = null;
|
||||
let isUnlocked = false;
|
||||
|
||||
// Initialize and unlock audio context
|
||||
export const initAudio = () => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user