feat(how-it-works): integrate scroll-driven 3D experience
Migrate the standalone Vite + React Three Fiber experience into the existing Next.js site as the body of the How It Works page, replacing the Miles3 / WhyChooseDoormile / TheDoormileWay content sections while preserving the Elementor hero, global Header/Footer, layout, routing and SEO. - New self-contained module: src/modules/how-it-works-3d/ (R3F scene, hooks, zustand store, animations, curves, constants, utils, scoped CSS). App.jsx → Experience3D.jsx; 3d_scene.jsx → models/Scene3D.jsx. - 32MB GLB moved to public/models/3d_scene_final.glb; useGLTF paths updated. - Client-only entry via dynamic ssr:false loader (Experience3DLoader). - Self-managed fixed pin (tall section + absolute stage toggled absolute(top)→fixed→absolute(bottom) from ScrollTrigger pin state), mirroring the site's StrategySection, since the fixed header + ancestor overflow:hidden break CSS sticky / GSAP pin. - experience.css fully scoped under .dm-hiw-3d to avoid colliding with the site's Elementor CSS. - Global Lenis disabled on /how-it-works; module runs its own tuned Lenis; jump-to-section scroll math made spacer-relative. - Added zustand + maath; ESLint-ignored the ported module. Rendering fixes (root causes found by driving headless Chrome): - Bump three 0.171 → 0.184 to match @react-three/fiber@9.6 / drei@10.7 / postprocessing@6.39 (0.171 silently failed to render this GLB and caused the EffectComposer getContextAttributes().alpha crash). Other 3D routes verified. - EffectComposer: Bloom + Vignette only. SSAO needs a NormalPass (v3 dropped the old `disableNormalPass`), and that extra full-scene pass exhausted the WebGL context on this heavy scene. - Cap Canvas dpr to [1,1.5] to bound framebuffer memory on retina displays. - Defer Canvas mount via IntersectionObserver (mountScene), matching StrategySection, to ease StrictMode/first-render GPU pressure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
159
src/modules/how-it-works-3d/components/TruckAnimation.jsx
Normal file
159
src/modules/how-it-works-3d/components/TruckAnimation.jsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
import { useSceneStore } from '../store/useSceneStore'
|
||||
import { useTruckMovement } from '../hooks/useTruckMovement'
|
||||
import { animateWheels } from '../animations/wheelAnimation'
|
||||
import { easing } from 'maath'
|
||||
import { truckPath } from '../curves/truckPath'
|
||||
|
||||
export default function TruckAnimation({ truckRef, wheelRefs }) {
|
||||
const scrollProgress = useSceneStore((state) => state.scrollProgress)
|
||||
const activeSection = useSceneStore((state) => state.activeSection)
|
||||
const setTruckProgress = useSceneStore((state) => state.setTruckProgress)
|
||||
|
||||
const { truckProgress } = useTruckMovement(scrollProgress)
|
||||
|
||||
const initialized = useRef(false)
|
||||
|
||||
// Sync truck progress to the global store
|
||||
useEffect(() => {
|
||||
setTruckProgress(truckProgress)
|
||||
}, [truckProgress, setTruckProgress])
|
||||
|
||||
// Float trackers for 1D progress and direction detection
|
||||
const dampedProgressRef = useRef(0)
|
||||
const lastScrollProgressRef = useRef(0)
|
||||
const isReversingRef = useRef(false)
|
||||
|
||||
// Tracker for smooth 180-degree yaw rotation (prevents glitches by pivoting Y rotation angle directly)
|
||||
const extraRotationRef = useRef(0)
|
||||
|
||||
// Track wheel rotation accumulation
|
||||
const accumulatedRotationRef = useRef(0)
|
||||
const lastDampedProgressRef = useRef(0)
|
||||
|
||||
|
||||
|
||||
useFrame((state, delta) => {
|
||||
if (!truckRef.current) return
|
||||
|
||||
// Detect scroll direction changes from the actual page scroll progress
|
||||
const deltaScroll = scrollProgress - lastScrollProgressRef.current
|
||||
if (deltaScroll < -0.0001) {
|
||||
isReversingRef.current = true
|
||||
} else if (deltaScroll > 0.0001) {
|
||||
isReversingRef.current = false
|
||||
}
|
||||
lastScrollProgressRef.current = scrollProgress
|
||||
|
||||
// Ensure correct parent-child structure and orientation for the truck (runs reactively on re-renders)
|
||||
const innerGroup = truckRef.current.children[0]
|
||||
if (innerGroup && truckRef.current.children.length > 1) {
|
||||
const siblings = [...truckRef.current.children].slice(1)
|
||||
siblings.forEach((sibling) => {
|
||||
innerGroup.attach(sibling)
|
||||
})
|
||||
|
||||
innerGroup.rotation.set(0, -Math.PI / 2, 0)
|
||||
|
||||
// Disable frustum culling on all child meshes so the truck/shadow is always visible
|
||||
truckRef.current.traverse((child) => {
|
||||
if (child.isMesh) {
|
||||
child.frustumCulled = false
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Run one-time state initialization for progress trackers
|
||||
if (!initialized.current) {
|
||||
dampedProgressRef.current = truckProgress
|
||||
lastDampedProgressRef.current = truckProgress
|
||||
lastScrollProgressRef.current = scrollProgress
|
||||
isReversingRef.current = false
|
||||
extraRotationRef.current = 0
|
||||
|
||||
const position = truckPath.getPoint(dampedProgressRef.current)
|
||||
let lookAtTargetVector
|
||||
if (dampedProgressRef.current >= 0.99) {
|
||||
const tangent = truckPath.getTangent(1.0)
|
||||
const endPoint = truckPath.getPoint(1.0)
|
||||
lookAtTargetVector = new THREE.Vector3().copy(endPoint).addScaledVector(tangent, 1.0)
|
||||
} else {
|
||||
const ahead = Math.min(dampedProgressRef.current + 0.01, 1.0)
|
||||
lookAtTargetVector = truckPath.getPoint(ahead)
|
||||
}
|
||||
|
||||
truckRef.current.position.copy(position)
|
||||
if (truckRef.current.position.distanceToSquared(lookAtTargetVector) > 0.0001) {
|
||||
truckRef.current.lookAt(lookAtTargetVector)
|
||||
}
|
||||
|
||||
initialized.current = true
|
||||
}
|
||||
|
||||
// Smoothly damp the 1D progress scalar along the curve path
|
||||
easing.damp(dampedProgressRef, 'current', truckProgress, 0.30, delta)
|
||||
|
||||
// Evaluate the 3D position and orientation directly on the spline curve
|
||||
const position = truckPath.getPoint(dampedProgressRef.current)
|
||||
|
||||
let lookAtTargetVector
|
||||
if (dampedProgressRef.current >= 0.99) {
|
||||
const tangent = truckPath.getTangent(1.0)
|
||||
const endPoint = truckPath.getPoint(1.0)
|
||||
lookAtTargetVector = new THREE.Vector3().copy(endPoint).addScaledVector(tangent, 1.0)
|
||||
} else {
|
||||
const ahead = Math.min(dampedProgressRef.current + 0.01, 1.0)
|
||||
lookAtTargetVector = truckPath.getPoint(ahead)
|
||||
}
|
||||
|
||||
// Update position and base forward rotation directly (ensures 100% spline compliance, zero corner cutting)
|
||||
truckRef.current.position.copy(position)
|
||||
if (truckRef.current.position.distanceToSquared(lookAtTargetVector) > 0.0001) {
|
||||
truckRef.current.lookAt(lookAtTargetVector)
|
||||
}
|
||||
|
||||
// Determine target extra rotation:
|
||||
// - 0 radians when moving forward
|
||||
// - Math.PI radians (180 degrees) when reversing
|
||||
// We disable U-turns at the extreme start and end of the path to keep the truck stable at warehouse/delivery spots
|
||||
let targetExtraRotation = 0
|
||||
if (dampedProgressRef.current > 0.05 && dampedProgressRef.current < 0.95) {
|
||||
if (isReversingRef.current) {
|
||||
targetExtraRotation = Math.PI
|
||||
}
|
||||
}
|
||||
|
||||
// Smoothly damp the extra rotation angle directly (prevents pitch/roll glitches or 3D target collapse)
|
||||
easing.damp(extraRotationRef, 'current', targetExtraRotation, 0.20, delta)
|
||||
|
||||
// Apply the yaw pivot around the local vertical axis
|
||||
truckRef.current.rotateY(extraRotationRef.current)
|
||||
|
||||
// Calculate progress delta for wheels and audio
|
||||
const deltaDamped = Math.abs(dampedProgressRef.current - lastDampedProgressRef.current)
|
||||
lastDampedProgressRef.current = dampedProgressRef.current
|
||||
|
||||
// Accumulate wheel rotation based on absolute movement delta so they always roll forward locally
|
||||
const isMoving = dampedProgressRef.current > 0.001 && dampedProgressRef.current < 0.999
|
||||
if (isMoving) {
|
||||
accumulatedRotationRef.current += deltaDamped * 250 // spinFactor
|
||||
}
|
||||
|
||||
// Spin wheels
|
||||
animateWheels(wheelRefs, accumulatedRotationRef.current)
|
||||
|
||||
|
||||
|
||||
// Add engine vibration to the inner group to prevent coordinate pollution on the root group
|
||||
if (truckRef.current.children && truckRef.current.children[0]) {
|
||||
const innerGroup = truckRef.current.children[0]
|
||||
innerGroup.position.y = Math.sin(state.clock.getElapsedTime() * 45) * 0.003
|
||||
}
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user