-
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 2 commits
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' }; | ||
|
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.
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 👍 / 👎. |
||
| } | ||
|
|
||
| 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.
Although the RAF is now wrapped and this field is emitted into the raw renderer sample,
tools/perf/analyze.tsnever consumestrackMapAnimationFrameMs: its renderer summary and generated table still only report frame time and telemetry callbacks. Consequently normalperf:analyzeoutput cannot display or compare the newly instrumented hot path, so the R13.1 observability requirement remains incomplete. This unchanged analyzer path is fresh evidence beyond the earlier instrumentation comment; add the metric to the analyzer summary/report and its tests.AGENTS.md reference: AGENTS.md:L12-L18
Useful? React with 👍 / 👎.