-
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 3 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,85 @@ 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; | ||
| const 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; | ||
| let fillColor = color.fill; | ||
| let textColor = color.text; | ||
| 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) { | ||
| fillColor = 'white'; | ||
| textColor = originalColor; | ||
| } | ||
|
|
||
| // on pit road? | ||
| const onPitRoad = !!carIdxIsOnPitRoad?.[driver.CarIdx]; | ||
| if (onPitRoad) { | ||
| fillColor = '#999999'; | ||
| textColor = '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 = fillColor; | ||
| 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 = textColor; | ||
| 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 👍 / 👎.