Files
doormile_react/src/modules/how-it-works-3d/components/TruckAnimation.jsx
2026-06-09 15:32:58 +05:30

183 lines
7.4 KiB
JavaScript

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
// r3f can emit delta === 0 (coincident frames, the first frame, or after a
// long main-thread block while the 32MB scene parses). maath's easing.damp
// divides by delta internally, so a 0 yields NaN/Infinity that poisons the
// damper's stored velocity — and from then on truckPath.getPoint(NaN) throws
// "Cannot read properties of undefined (reading 'x')". Clamp delta to a safe
// positive range before any damping.
const dt = Number.isFinite(delta) && delta > 0 ? Math.min(delta, 0.1) : 1 / 60
// 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
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
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, dt)
// Defensive: keep the spline parameter a finite value in [0,1]. getPoint(NaN)
// or an out-of-range t reads an undefined curve point and throws.
if (!Number.isFinite(dampedProgressRef.current)) {
dampedProgressRef.current = truckProgress
if (dampedProgressRef.__damp) dampedProgressRef.__damp = {} // clear any poisoned velocity
}
dampedProgressRef.current = THREE.MathUtils.clamp(dampedProgressRef.current, 0, 1)
// 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, 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)
// 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
}