Files
doormile_react/src/modules/how-it-works-3d/hooks/useCameraAnimation.js
Aravind R 3d53f82e7b 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>
2026-06-08 20:47:10 +05:30

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