-
Notifications
You must be signed in to change notification settings - Fork 77
fix: smooth track map marker movement #688
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; | ||
| import { useEffect, useMemo, useRef, useState } from 'react'; | ||
| import { TrackDriver, TrackDrawing } from './TrackCanvas'; | ||
| import { getColor, getTailwindStyle } from '@irdashies/utils/colors'; | ||
| import { useCarIdxOffTrack } from '@irdashies/context'; | ||
| import { progressToFlatX, useProgressAnimation } from './useProgressAnimation'; | ||
|
|
||
| export interface FlatTrackMapCanvasProps { | ||
| trackDrawing: TrackDrawing; | ||
|
|
@@ -74,6 +75,17 @@ export const FlatTrackMapCanvas = ({ | |
| return colors; | ||
| }, [drivers, isMultiClass, highlightColor]); | ||
|
|
||
| const orderedDrivers = useMemo( | ||
| () => | ||
| drivers | ||
| .map((entry, interpolationIndex) => ({ | ||
| ...entry, | ||
| interpolationIndex, | ||
| })) | ||
| .sort((a, b) => Number(a.isPlayer) - Number(b.isPlayer)), | ||
| [drivers] | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| const canvas = canvasRef.current; | ||
| if (!canvas) return; | ||
|
|
@@ -121,7 +133,7 @@ export const FlatTrackMapCanvas = ({ | |
| }; | ||
| }, []); | ||
|
|
||
| useLayoutEffect(() => { | ||
| useProgressAnimation(drivers, (progressValues, count) => { | ||
| const canvas = canvasRef.current; | ||
| const ctx = canvas?.getContext('2d'); | ||
| if (!canvas || !ctx || canvasSize.width === 0) return; | ||
|
|
@@ -193,93 +205,81 @@ export const FlatTrackMapCanvas = ({ | |
|
|
||
| // Draw drivers | ||
| // Apply scale factor to match curved track map proportions | ||
| [...drivers] | ||
| .sort((a, b) => Number(a.isPlayer) - Number(b.isPlayer)) | ||
| .forEach(({ driver, progress, isPlayer, classPosition }) => { | ||
| let color = driverColors[driver.CarIdx]; | ||
| if (!color) return; | ||
|
|
||
| const x = HORIZONTAL_PADDING + progress * usableWidth; | ||
| const radius = | ||
| (isPlayer ? playerCircleSize : driverCircleSize) * circleScale; | ||
| const fontSize = radius * (trackmapFontSize / 100); | ||
| const originalColor = color.fill; | ||
| const livePosition = | ||
| driverLivePositions[driver.CarIdx] ?? classPosition; | ||
|
|
||
| // highlight leader? | ||
| if (!isPlayer && invertLeaderColor && livePosition === 1) { | ||
| color = { fill: 'white', text: originalColor }; | ||
| } | ||
| for (const orderedDriver of orderedDrivers) { | ||
| const { driver, isPlayer, classPosition, interpolationIndex } = | ||
| orderedDriver; | ||
| if (interpolationIndex >= count) continue; | ||
| let color = driverColors[driver.CarIdx]; | ||
| if (!color) continue; | ||
|
|
||
| // on pit road? | ||
| const onPitRoad = !!carIdxIsOnPitRoad?.[driver.CarIdx]; | ||
| if (onPitRoad) { | ||
| color = { fill: '#999999', text: 'white' }; | ||
| } | ||
| const x = progressToFlatX( | ||
| progressValues[interpolationIndex], | ||
| HORIZONTAL_PADDING, | ||
| usableWidth | ||
| ); | ||
| const radius = | ||
| (isPlayer ? playerCircleSize : driverCircleSize) * circleScale; | ||
| const fontSize = radius * (trackmapFontSize / 100); | ||
| const originalColor = color.fill; | ||
| const livePosition = driverLivePositions[driver.CarIdx] ?? classPosition; | ||
|
|
||
| ctx.fillStyle = color.fill; | ||
| ctx.beginPath(); | ||
| ctx.arc(x, centerY, radius, 0, 2 * Math.PI); | ||
| ctx.fill(); | ||
|
|
||
| // draw a border? | ||
| if (driversOffTrack[driver.CarIdx]) { | ||
| ctx.strokeStyle = getColor('yellow', 400); | ||
| ctx.lineWidth = 4; | ||
| ctx.stroke(); | ||
| } else if (!isPlayer && invertLeaderColor && livePosition === 1) { | ||
| ctx.strokeStyle = originalColor; | ||
| ctx.lineWidth = 2; | ||
| ctx.stroke(); | ||
| } | ||
| // highlight leader? | ||
| if (!isPlayer && invertLeaderColor && livePosition === 1) { | ||
| color = { fill: 'white', text: originalColor }; | ||
| } | ||
|
|
||
| // on pit road? | ||
| const onPitRoad = !!carIdxIsOnPitRoad?.[driver.CarIdx]; | ||
| if (onPitRoad) { | ||
| color = { fill: '#999999', text: 'white' }; | ||
| } | ||
|
|
||
| if (showCarNumbers) { | ||
| ctx.fillStyle = color.text; | ||
| ctx.font = `${fontSize}px sans-serif`; | ||
| ctx.textAlign = 'center'; | ||
| ctx.textBaseline = 'middle'; | ||
| let displayText; | ||
| if (onPitRoad) { | ||
| displayText = 'P'; | ||
| } else if (displayMode === 'livePosition') { | ||
| displayText = | ||
| livePosition !== undefined && livePosition > 0 | ||
| ? livePosition.toString() | ||
| : ''; | ||
| } else if (displayMode === 'sessionPosition') { | ||
| displayText = | ||
| classPosition !== undefined && classPosition > 0 | ||
| ? classPosition.toString() | ||
| : ''; | ||
| } else { | ||
| displayText = driver.CarNumber; | ||
| } | ||
| if (displayText) { | ||
| const m = ctx.measureText(displayText); | ||
| const visualOffset = | ||
| (m.actualBoundingBoxAscent - m.actualBoundingBoxDescent) / 2; | ||
| ctx.fillText(displayText, x, centerY + visualOffset); | ||
| } | ||
| ctx.fillStyle = color.fill; | ||
| ctx.beginPath(); | ||
| ctx.arc(x, centerY, radius, 0, 2 * Math.PI); | ||
| ctx.fill(); | ||
|
|
||
| // draw a border? | ||
| if (driversOffTrack[driver.CarIdx]) { | ||
| ctx.strokeStyle = getColor('yellow', 400); | ||
| ctx.lineWidth = 4; | ||
| ctx.stroke(); | ||
| } else if (!isPlayer && invertLeaderColor && livePosition === 1) { | ||
| ctx.strokeStyle = originalColor; | ||
| ctx.lineWidth = 2; | ||
| ctx.stroke(); | ||
| } | ||
|
|
||
| if (showCarNumbers) { | ||
| ctx.fillStyle = color.text; | ||
| ctx.font = `${fontSize}px sans-serif`; | ||
| ctx.textAlign = 'center'; | ||
| ctx.textBaseline = 'middle'; | ||
| let displayText; | ||
| if (onPitRoad) { | ||
| displayText = 'P'; | ||
| } else if (displayMode === 'livePosition') { | ||
| displayText = | ||
| livePosition !== undefined && livePosition > 0 | ||
| ? livePosition.toString() | ||
| : ''; | ||
| } else if (displayMode === 'sessionPosition') { | ||
| displayText = | ||
| classPosition !== undefined && classPosition > 0 | ||
| ? classPosition.toString() | ||
| : ''; | ||
| } else { | ||
| displayText = driver.CarNumber; | ||
| } | ||
| if (displayText) { | ||
| const m = ctx.measureText(displayText); | ||
| const visualOffset = | ||
| (m.actualBoundingBoxAscent - m.actualBoundingBoxDescent) / 2; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With the default AGENTS.md reference: AGENTS.md:L12-L18 Useful? React with 👍 / 👎. |
||
| ctx.fillText(displayText, x, centerY + visualOffset); | ||
| } | ||
| }); | ||
| }, [ | ||
| canvasSize, | ||
| drivers, | ||
| driverColors, | ||
| invertLeaderColor, | ||
| driversOffTrack, | ||
| showCarNumbers, | ||
| displayMode, | ||
| driverCircleSize, | ||
| playerCircleSize, | ||
| trackmapFontSize, | ||
| trackLineWidth, | ||
| trackOutlineWidth, | ||
| invertTrackColors, | ||
| driverLivePositions, | ||
| carIdxIsOnPitRoad, | ||
| ]); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| return ( | ||
| <div className="w-full h-full"> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the flat map contains any car on pit road, this branch constructs a new color object for that car on every animation frame; the inverted-leader branch above does the same when enabled. Moving this drawing code from telemetry updates into a display-refresh RAF turns those into per-frame allocations, contrary to R13.2 and likely to create avoidable garbage-collection pressure in full grids. Cache these colors or select scalar fill/text values without allocating objects inside the paint loop.
AGENTS.md reference: AGENTS.md:L12-L18
Useful? React with 👍 / 👎.