refactor: extract useTrainTransforms hook from SteamTrainJourney

- Create dedicated hook for train position and car transform calculations
- Move 71 lines of transform logic to useTrainTransforms.ts
- Extract train position useEffect, trainCars useMemo, and locomotiveOpacity useMemo
- Reduce SteamTrainJourney.tsx from 1023 to 959 lines (64 lines removed)
- Preserve all functionality exactly - no behavioral changes

Benefits:
- Separates transform calculations from presentation logic
- Makes train physics calculations easier to test
- Improves code organization and readability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-01 09:34:37 -05:00
parent 32abde107c
commit a2512d5738
2 changed files with 117 additions and 72 deletions

View File

@@ -4,6 +4,7 @@ import { useEffect, useRef, useState, useMemo, memo } from 'react'
import { useSpring, animated } from '@react-spring/web'
import { useSteamJourney } from '../../hooks/useSteamJourney'
import { usePassengerAnimations, type BoardingAnimation, type DisembarkingAnimation } from '../../hooks/usePassengerAnimations'
import { useTrainTransforms } from '../../hooks/useTrainTransforms'
import { useComplementRace } from '../../context/ComplementRaceContext'
import { RailroadTrackGenerator } from '../../lib/RailroadTrackGenerator'
import { PassengerCard } from '../PassengerCard'
@@ -101,11 +102,17 @@ export function SteamTrainJourney({ momentum, trainPosition, pressure, elapsedTi
leftRailPoints: string[]
rightRailPoints: string[]
} | null>(null)
const [trainTransform, setTrainTransform] = useState({ x: 50, y: 300, rotation: 0 })
const [stationPositions, setStationPositions] = useState<Array<{ x: number; y: number }>>([])
const [landmarks, setLandmarks] = useState<Landmark[]>([])
const [landmarkPositions, setLandmarkPositions] = useState<Array<{ x: number; y: number }>>([])
// Train transforms (extracted to hook)
const { trainTransform, trainCars, locomotiveOpacity, maxCars, carSpacing } = useTrainTransforms({
trainPosition,
trackGenerator,
pathRef
})
// Passenger animations (extracted to hook)
const { boardingAnimations, disembarkingAnimations } = usePassengerAnimations({
passengers: state.passengers,
@@ -227,77 +234,6 @@ export function SteamTrainJourney({ momentum, trainPosition, pressure, elapsedTi
}
}, [trackData, landmarks])
// Update train position and rotation
useEffect(() => {
if (pathRef.current) {
const transform = trackGenerator.getTrainTransform(pathRef.current, trainPosition)
setTrainTransform(transform)
}
}, [trainPosition, trackGenerator])
// Calculate train car transforms (each car follows behind the locomotive)
const maxCars = 5 // Maximum passengers per route
const carSpacing = 7 // Percentage of track between cars
const trainCars = useMemo(() => {
if (!pathRef.current) {
return Array.from({ length: maxCars }, () => ({ x: 0, y: 0, rotation: 0, position: 0, opacity: 0 }))
}
return Array.from({ length: maxCars }).map((_, carIndex) => {
// Calculate position for this car (behind the locomotive)
const carPosition = Math.max(0, trainPosition - (carIndex + 1) * carSpacing)
// Calculate opacity: fade in at left tunnel (3-8%), fade out at right tunnel (92-97%)
const fadeInStart = 3
const fadeInEnd = 8
const fadeOutStart = 92
const fadeOutEnd = 97
let opacity = 1 // Default to fully visible
// Fade in from left tunnel
if (carPosition <= fadeInStart) {
opacity = 0
} else if (carPosition < fadeInEnd) {
opacity = (carPosition - fadeInStart) / (fadeInEnd - fadeInStart)
}
// Fade out into right tunnel
else if (carPosition >= fadeOutEnd) {
opacity = 0
} else if (carPosition > fadeOutStart) {
opacity = 1 - ((carPosition - fadeOutStart) / (fadeOutEnd - fadeOutStart))
}
return {
...trackGenerator.getTrainTransform(pathRef.current!, carPosition),
position: carPosition,
opacity
}
})
}, [trainPosition, trackGenerator, maxCars, carSpacing])
// Calculate locomotive opacity (fade in/out through tunnels)
const locomotiveOpacity = useMemo(() => {
const fadeInStart = 3
const fadeInEnd = 8
const fadeOutStart = 92
const fadeOutEnd = 97
// Fade in from left tunnel
if (trainPosition <= fadeInStart) {
return 0
} else if (trainPosition < fadeInEnd) {
return (trainPosition - fadeInStart) / (fadeInEnd - fadeInStart)
}
// Fade out into right tunnel
else if (trainPosition >= fadeOutEnd) {
return 0
} else if (trainPosition > fadeOutStart) {
return 1 - ((trainPosition - fadeOutStart) / (fadeOutEnd - fadeOutStart))
}
return 1 // Default to fully visible
}, [trainPosition])
// Memoize filtered passenger lists to avoid recalculating on every render
const boardedPassengers = useMemo(() =>

View File

@@ -0,0 +1,109 @@
import { useEffect, useState, useMemo } from 'react'
import type { RailroadTrackGenerator } from '../lib/RailroadTrackGenerator'
interface TrainTransform {
x: number
y: number
rotation: number
}
interface TrainCarTransform extends TrainTransform {
position: number
opacity: number
}
interface UseTrainTransformsParams {
trainPosition: number
trackGenerator: RailroadTrackGenerator
pathRef: React.RefObject<SVGPathElement>
maxCars?: number
carSpacing?: number
}
export function useTrainTransforms({
trainPosition,
trackGenerator,
pathRef,
maxCars = 5,
carSpacing = 7
}: UseTrainTransformsParams) {
const [trainTransform, setTrainTransform] = useState<TrainTransform>({ x: 50, y: 300, rotation: 0 })
// Update train position and rotation
useEffect(() => {
if (pathRef.current) {
const transform = trackGenerator.getTrainTransform(pathRef.current, trainPosition)
setTrainTransform(transform)
}
}, [trainPosition, trackGenerator, pathRef])
// Calculate train car transforms (each car follows behind the locomotive)
const trainCars = useMemo((): TrainCarTransform[] => {
if (!pathRef.current) {
return Array.from({ length: maxCars }, () => ({ x: 0, y: 0, rotation: 0, position: 0, opacity: 0 }))
}
return Array.from({ length: maxCars }).map((_, carIndex) => {
// Calculate position for this car (behind the locomotive)
const carPosition = Math.max(0, trainPosition - (carIndex + 1) * carSpacing)
// Calculate opacity: fade in at left tunnel (3-8%), fade out at right tunnel (92-97%)
const fadeInStart = 3
const fadeInEnd = 8
const fadeOutStart = 92
const fadeOutEnd = 97
let opacity = 1 // Default to fully visible
// Fade in from left tunnel
if (carPosition <= fadeInStart) {
opacity = 0
} else if (carPosition < fadeInEnd) {
opacity = (carPosition - fadeInStart) / (fadeInEnd - fadeInStart)
}
// Fade out into right tunnel
else if (carPosition >= fadeOutEnd) {
opacity = 0
} else if (carPosition > fadeOutStart) {
opacity = 1 - ((carPosition - fadeOutStart) / (fadeOutEnd - fadeOutStart))
}
return {
...trackGenerator.getTrainTransform(pathRef.current!, carPosition),
position: carPosition,
opacity
}
})
}, [trainPosition, trackGenerator, pathRef, maxCars, carSpacing])
// Calculate locomotive opacity (fade in/out through tunnels)
const locomotiveOpacity = useMemo(() => {
const fadeInStart = 3
const fadeInEnd = 8
const fadeOutStart = 92
const fadeOutEnd = 97
// Fade in from left tunnel
if (trainPosition <= fadeInStart) {
return 0
} else if (trainPosition < fadeInEnd) {
return (trainPosition - fadeInStart) / (fadeInEnd - fadeInStart)
}
// Fade out into right tunnel
else if (trainPosition >= fadeOutEnd) {
return 0
} else if (trainPosition > fadeOutStart) {
return 1 - ((trainPosition - fadeOutStart) / (fadeOutEnd - fadeOutStart))
}
return 1 // Default to fully visible
}, [trainPosition])
return {
trainTransform,
trainCars,
locomotiveOpacity,
maxCars,
carSpacing
}
}