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>
165 lines
7.1 KiB
JavaScript
165 lines
7.1 KiB
JavaScript
import { useMemo } from 'react'
|
|
import * as THREE from 'three'
|
|
import { truckPath } from '../curves/truckPath'
|
|
import { clamp } from '../utils/helpers'
|
|
|
|
export const useCameraAnimation = (scrollProgress) => {
|
|
const cameraState = useMemo(() => {
|
|
// 1. Calculate the truck position corresponding to the current scroll progress
|
|
// Use the exact same piecewise mapping to keep camera follow 100% synchronized
|
|
let truckProgress = 0
|
|
if (scrollProgress < 0.14) {
|
|
truckProgress = 0.0
|
|
} else if (scrollProgress >= 0.14 && scrollProgress < 0.38) {
|
|
truckProgress = 0.5 * (scrollProgress - 0.14) / 0.24
|
|
} else if (scrollProgress >= 0.38 && scrollProgress < 0.50) {
|
|
truckProgress = 0.5
|
|
} else if (scrollProgress >= 0.50 && scrollProgress < 0.76) {
|
|
truckProgress = 0.5 + 0.5 * (scrollProgress - 0.50) / 0.26
|
|
} else {
|
|
truckProgress = 1.0
|
|
}
|
|
const truckPos = truckPath.getPoint(truckProgress)
|
|
|
|
const firstMileViewWhole = {
|
|
position: new THREE.Vector3(38.0, 15.0, -10.0),
|
|
target: new THREE.Vector3(24.377, 4.0, -39.303)
|
|
}
|
|
const firstMileViewFront = {
|
|
position: new THREE.Vector3(7.0, 3.0, -19.0),
|
|
target: new THREE.Vector3(15.5, 1.5, -26.5)
|
|
}
|
|
const midMileView = {
|
|
position: new THREE.Vector3(-7.0, 7.5, 8.0),
|
|
target: new THREE.Vector3(-19.146, 2.5, -9.0)
|
|
}
|
|
const lastMileViewClose = {
|
|
position: new THREE.Vector3(-3.5, 4.0, 15.0),
|
|
target: new THREE.Vector3(8.0, 2.0, 20.0)
|
|
}
|
|
const lastMileViewZoomedOut = {
|
|
position: new THREE.Vector3(-10.4, 5.2, 12.0),
|
|
target: new THREE.Vector3(8.0, 2.0, 20.0)
|
|
}
|
|
const analyticsView = {
|
|
position: new THREE.Vector3(-13.5, 5.0, 31.0),
|
|
target: new THREE.Vector3(-7.7, 3.5, 25.4)
|
|
}
|
|
|
|
// 3. Calculate local coordinate axes of the truck based on the spline tangent
|
|
const forward = truckPath.getTangent(truckProgress).normalize()
|
|
const up = new THREE.Vector3(0, 1, 0)
|
|
const right = new THREE.Vector3().crossVectors(forward, up).normalize()
|
|
|
|
// Cruise 1: Front-left follow perspective (facing the oncoming truck, zoomed out follow)
|
|
const cruise1Pos = truckPos.clone()
|
|
.addScaledVector(forward, 7.2)
|
|
.addScaledVector(up, 3.2)
|
|
.addScaledVector(right, -3.0)
|
|
const cruise1Target = truckPos.clone()
|
|
|
|
// Cruise 2: Front-right follow perspective (facing the oncoming truck, zoomed out follow)
|
|
const cruise2Pos = truckPos.clone()
|
|
.addScaledVector(forward, 7.2)
|
|
.addScaledVector(up, 3.2)
|
|
.addScaledVector(right, 3.0)
|
|
const cruise2Target = truckPos.clone()
|
|
|
|
const position = new THREE.Vector3()
|
|
const target = new THREE.Vector3()
|
|
|
|
// 4. Smoothly blend positions and targets depending on active scroll boundaries
|
|
if (scrollProgress < 0.04) {
|
|
// Step 1: Zoomed out overview of the whole building
|
|
position.copy(firstMileViewWhole.position)
|
|
target.copy(firstMileViewWhole.target)
|
|
}
|
|
else if (scrollProgress >= 0.04 && scrollProgress < 0.14) {
|
|
// Step 2: Camera moves to the front close-up view of the building
|
|
const alpha = (scrollProgress - 0.04) / 0.10
|
|
const smoothAlpha = alpha * alpha * (3 - 2 * alpha)
|
|
position.lerpVectors(firstMileViewWhole.position, firstMileViewFront.position, smoothAlpha)
|
|
target.lerpVectors(firstMileViewWhole.target, firstMileViewFront.target, smoothAlpha)
|
|
}
|
|
else if (scrollProgress >= 0.14 && scrollProgress < 0.18) {
|
|
// Step 3: Truck starts moving, camera blends to close follow tracking
|
|
const alpha = (scrollProgress - 0.14) / 0.04
|
|
const smoothAlpha = alpha * alpha * (3 - 2 * alpha)
|
|
position.lerpVectors(firstMileViewFront.position, cruise1Pos, smoothAlpha)
|
|
target.lerpVectors(firstMileViewFront.target, cruise1Target, smoothAlpha)
|
|
}
|
|
else if (scrollProgress >= 0.18 && scrollProgress < 0.34) {
|
|
// Cruise 1: Close follow tracking
|
|
position.copy(cruise1Pos)
|
|
target.copy(cruise1Target)
|
|
}
|
|
else if (scrollProgress >= 0.34 && scrollProgress < 0.38) {
|
|
// Blend: Cruise 1 Follow -> Mid Mile Building
|
|
const alpha = (scrollProgress - 0.34) / 0.04
|
|
const smoothAlpha = alpha * alpha * (3 - 2 * alpha)
|
|
position.lerpVectors(cruise1Pos, midMileView.position, smoothAlpha)
|
|
target.lerpVectors(cruise1Target, midMileView.target, smoothAlpha)
|
|
}
|
|
else if (scrollProgress >= 0.38 && scrollProgress < 0.50) {
|
|
// Mid Mile Building focus
|
|
position.copy(midMileView.position)
|
|
target.copy(midMileView.target)
|
|
}
|
|
else if (scrollProgress >= 0.50 && scrollProgress < 0.54) {
|
|
// Blend: Mid Mile Building -> Cruise 2 Follow
|
|
const alpha = (scrollProgress - 0.50) / 0.04
|
|
const smoothAlpha = alpha * alpha * (3 - 2 * alpha)
|
|
position.lerpVectors(midMileView.position, cruise2Pos, smoothAlpha)
|
|
target.lerpVectors(midMileView.target, cruise2Target, smoothAlpha)
|
|
}
|
|
else if (scrollProgress >= 0.54 && scrollProgress < 0.72) {
|
|
// Cruise 2: Close follow tracking
|
|
position.copy(cruise2Pos)
|
|
target.copy(cruise2Target)
|
|
}
|
|
else if (scrollProgress >= 0.72 && scrollProgress < 0.76) {
|
|
// Blend: Cruise 2 Follow -> Last Mile Building Close-up
|
|
const alpha = (scrollProgress - 0.72) / 0.04
|
|
const smoothAlpha = alpha * alpha * (3 - 2 * alpha)
|
|
position.lerpVectors(cruise2Pos, lastMileViewClose.position, smoothAlpha)
|
|
target.lerpVectors(cruise2Target, lastMileViewClose.target, smoothAlpha)
|
|
}
|
|
else if (scrollProgress >= 0.76 && scrollProgress < 0.92) {
|
|
// Last Mile Building Stop Sequence:
|
|
// - 0.76 to 0.80: Parked close-up view of the truck and building
|
|
// - 0.80 to 0.84: Zoom out transition back along the camera viewing axis
|
|
// - 0.84 to 0.92: Zoomed-out overview of the final delivery stage (card stays frozen here)
|
|
if (scrollProgress < 0.80) {
|
|
position.copy(lastMileViewClose.position)
|
|
target.copy(lastMileViewClose.target)
|
|
} else if (scrollProgress >= 0.80 && scrollProgress < 0.84) {
|
|
const alpha = (scrollProgress - 0.80) / 0.04
|
|
const smoothAlpha = alpha * alpha * (3 - 2 * alpha)
|
|
position.lerpVectors(lastMileViewClose.position, lastMileViewZoomedOut.position, smoothAlpha)
|
|
target.lerpVectors(lastMileViewClose.target, lastMileViewZoomedOut.target, smoothAlpha)
|
|
} else {
|
|
position.copy(lastMileViewZoomedOut.position)
|
|
target.copy(lastMileViewZoomedOut.target)
|
|
}
|
|
}
|
|
else if (scrollProgress >= 0.92 && scrollProgress < 0.96) {
|
|
// Blend: Last Mile Building Zoomed-Out -> Analytics Dashboard screen
|
|
const alpha = (scrollProgress - 0.92) / 0.04
|
|
const smoothAlpha = alpha * alpha * (3 - 2 * alpha)
|
|
position.lerpVectors(lastMileViewZoomedOut.position, analyticsView.position, smoothAlpha)
|
|
target.lerpVectors(lastMileViewZoomedOut.target, analyticsView.target, smoothAlpha)
|
|
}
|
|
else {
|
|
// Analytics Dashboard screen focus
|
|
position.copy(analyticsView.position)
|
|
target.copy(analyticsView.target)
|
|
}
|
|
|
|
return { position, target }
|
|
}, [scrollProgress])
|
|
|
|
return cameraState
|
|
}
|
|
|
|
export default useCameraAnimation
|