183 lines
7.4 KiB
JavaScript
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
|
|
}
|