(0);
const totalPoints = points.length;
const currentPoint = points[currentIndex] || points[0];
@@ -149,6 +190,20 @@ export function WebTelemetryReplayModal({ ride, onClose }: WebTelemetryReplayMod
lastTimeRef.current = null;
indexRef.current = clamped;
setCurrentIndex(clamped);
+
+ const pt = points[clamped];
+ if (pt && riderMarkerRef.current && typeof window !== 'undefined') {
+ const L = (window as any).L;
+ updateRiderMarker(riderMarkerRef.current, pt.latitude, pt.longitude, pt.heading || 0, L);
+ if (mapInstanceRef.current) {
+ const map = mapInstanceRef.current;
+ const bounds = map.getBounds?.();
+ if (bounds && !bounds.contains([pt.latitude, pt.longitude])) {
+ lastPanTimeRef.current = typeof performance !== 'undefined' ? performance.now() : Date.now();
+ map.panTo([pt.latitude, pt.longitude], { animate: true, duration: 0.5 });
+ }
+ }
+ }
};
// Dynamically load Leaflet library and CSS with multi-CDN fallback
@@ -476,19 +531,7 @@ export function WebTelemetryReplayModal({ ride, onClose }: WebTelemetryReplayMod
// Live rider position marker
if (points[0]) {
- const riderIcon = L.divIcon({
- className: 'rider-replay-marker',
- html: `
-
- `,
- iconSize: [32, 32],
- iconAnchor: [16, 16],
- });
-
+ const riderIcon = createRiderIcon(L, points[0].heading || 0);
const riderMarker = L.marker([points[0].latitude, points[0].longitude], {
icon: riderIcon,
zIndexOffset: 1000,
@@ -536,37 +579,30 @@ export function WebTelemetryReplayModal({ ride, onClose }: WebTelemetryReplayMod
};
}, [leafletReady, points, corners]);
- // Update Rider Beacon LatLng & Heading during playback or scrub
+ // Update Rider Beacon LatLng & Heading during scrub or state change
useEffect(() => {
if (!riderMarkerRef.current || !currentPoint || typeof window === 'undefined') return;
const L = (window as any).L;
if (!L) return;
- riderMarkerRef.current.setLatLng([currentPoint.latitude, currentPoint.longitude]);
-
- const newIcon = L.divIcon({
- className: 'rider-replay-marker',
- html: `
-
- `,
- iconSize: [32, 32],
- iconAnchor: [16, 16],
- });
- riderMarkerRef.current.setIcon(newIcon);
-
- // Pan map to follow if out of current bounds while playing
- if (isPlaying && mapInstanceRef.current) {
- const map = mapInstanceRef.current;
- const bounds = map.getBounds();
- if (bounds && !bounds.contains([currentPoint.latitude, currentPoint.longitude])) {
- map.panTo([currentPoint.latitude, currentPoint.longitude], { animate: true, duration: 0.5 });
- }
+ if (!isPlaying) {
+ const pos =
+ playbackCarryRef.current > 0
+ ? interpolateTelemetryPosition(points, indexRef.current, playbackCarryRef.current)
+ : {
+ latitude: currentPoint.latitude,
+ longitude: currentPoint.longitude,
+ heading: currentPoint.heading || 0,
+ };
+ updateRiderMarker(
+ riderMarkerRef.current,
+ pos.latitude,
+ pos.longitude,
+ pos.heading,
+ L
+ );
}
- }, [currentPoint, isPlaying]);
+ }, [currentPoint, isPlaying, points]);
// Invalidate map size when tab switches to map
useEffect(() => {
@@ -646,7 +682,6 @@ export function WebTelemetryReplayModal({ ride, onClose }: WebTelemetryReplayMod
if (!isPlaying) {
if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
lastTimeRef.current = null;
- playbackCarryRef.current = 0;
return;
}
@@ -661,6 +696,26 @@ export function WebTelemetryReplayModal({ ride, onClose }: WebTelemetryReplayMod
const { index, remainingMs } = advancePlaybackIndex(points, indexRef.current, budget);
playbackCarryRef.current = index >= totalPoints - 1 ? 0 : remainingMs;
+ // Continuous 60 FPS interpolation for smooth marker animation
+ const pos = interpolateTelemetryPosition(points, index, remainingMs);
+
+ if (riderMarkerRef.current && typeof window !== 'undefined') {
+ const L = (window as any).L;
+ updateRiderMarker(riderMarkerRef.current, pos.latitude, pos.longitude, pos.heading, L);
+
+ // Pan map to follow if out of current bounds while playing (throttled to 600ms to allow smooth 0.5s pan animation to complete without 60 FPS thrashing)
+ if (mapInstanceRef.current) {
+ const map = mapInstanceRef.current;
+ const bounds = map.getBounds?.();
+ if (bounds && !bounds.contains([pos.latitude, pos.longitude])) {
+ if (timestamp - lastPanTimeRef.current >= 600) {
+ lastPanTimeRef.current = timestamp;
+ map.panTo([pos.latitude, pos.longitude], { animate: true, duration: 0.5 });
+ }
+ }
+ }
+ }
+
if (index !== indexRef.current) {
indexRef.current = index;
setCurrentIndex(index);
@@ -1221,7 +1276,7 @@ export function WebTelemetryReplayModal({ ride, onClose }: WebTelemetryReplayMod
{/* Playback Speed Multiplier */}
- {[0.5, 1.0, 2.0, 5.0].map((spd) => (
+ {[0.5, 1.0, 2.0, 5.0, 10.0].map((spd) => (