Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 85 additions & 85 deletions src/frontend/components/TrackMap/FlatTrackMapCanvas.tsx
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse pit-road colors in the RAF loop

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cache text metrics outside the animation paint loop

With the default showCarNumbers setting, moving this drawing callback onto RAF makes measureText allocate a TextMetrics object for every visible driver on every paint (and the curved map does the same in drawDrivers), producing thousands of short-lived objects per second for a full grid. This leaves a substantial per-frame allocation source in the newly introduced hot path despite R13.2; cache the vertical offset by font/display text and refresh it only when snapshot or appearance inputs change.

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">
Expand Down
Loading