style: format remaining owned runtime modules

This commit is contained in:
Sameh Khamis committed 2026-09-15 07:26:32 -07:00
1 parent ae99843c14
commit 2aca7aecbb
94 files changed
+8865 -3151

No files matched your search

File diff suppressed because it is too large. Load diff
+264 -92
View File
@@ -49,22 +49,34 @@ export function clearWorldOverlayTextMeasureCache() {
/** Install the font-loading invalidation hooks once, when the API exists. */
export function installWorldOverlayFontInvalidation() {
if (_fontInvalidationInstalled || typeof document === 'undefined' || !document.fonts) return;
if (
_fontInvalidationInstalled ||
typeof document === 'undefined' ||
!document.fonts
)
return;
_fontInvalidationInstalled = true;
_observedFontSet = document.fonts;
const generation = ++_fontInvalidationGeneration;
Promise.resolve(document.fonts.ready)
.then(() => {
if (generation === _fontInvalidationGeneration) clearWorldOverlayTextMeasureCache();
if (generation === _fontInvalidationGeneration)
clearWorldOverlayTextMeasureCache();
})
.catch(() => {});
_observedFontSet.addEventListener?.('loadingdone', clearWorldOverlayTextMeasureCache);
_observedFontSet.addEventListener?.(
'loadingdone',
clearWorldOverlayTextMeasureCache,
);
}
/** Remove font hooks and cached measurements when the overlay host is destroyed. */
export function destroyWorldOverlayDraw() {
_fontInvalidationGeneration++;
_observedFontSet?.removeEventListener?.('loadingdone', clearWorldOverlayTextMeasureCache);
_observedFontSet?.removeEventListener?.(
'loadingdone',
clearWorldOverlayTextMeasureCache,
);
_observedFontSet = null;
_fontInvalidationInstalled = false;
clearWorldOverlayTextMeasureCache();
@@ -95,7 +107,8 @@ export function measureWorldOverlayText(ctx, text, font) {
}
fontCache.set(normalizedText, { width, usedAt: ++_textMeasureClock });
_textMeasureCacheSize++;
if (_textMeasureCacheSize > TEXT_MEASURE_CACHE_LIMIT) evictOldestTextMeasureEntry();
if (_textMeasureCacheSize > TEXT_MEASURE_CACHE_LIMIT)
evictOldestTextMeasureEntry();
return width;
}
@@ -113,7 +126,14 @@ export function getWorldOverlayTextMeasureCacheSize() {
* @param {number} h
* @param {number} radius
*/
export function roundedRectPath(path, x, y, w, h, radius = WORLD_OVERLAY_STYLE.radius) {
export function roundedRectPath(
path,
x,
y,
w,
h,
radius = WORLD_OVERLAY_STYLE.radius,
) {
if (typeof path.roundRect === 'function') {
path.roundRect(x, y, w, h, radius);
return;
@@ -137,11 +157,14 @@ export function roundedRectPath(path, x, y, w, h, radius = WORLD_OVERLAY_STYLE.r
* @param {number} [options.fadeStartRatio]
* @returns {number}
*/
export function distanceFade(distanceM, {
minDistance = 0,
maxDistance = Number.POSITIVE_INFINITY,
fadeStartRatio = 0.7,
} = {}) {
export function distanceFade(
distanceM,
{
minDistance = 0,
maxDistance = Number.POSITIVE_INFINITY,
fadeStartRatio = 0.7,
} = {},
) {
if (!Number.isFinite(distanceM)) return 0;
const min = Number.isFinite(minDistance) ? Math.max(0, minDistance) : 0;
if (distanceM < min) return 0;
@@ -192,17 +215,27 @@ export function altitudeScale(altitudeM, curve = null) {
const end = Number(curve.end);
const midValue = Number(curve.midValue);
const endValue = Number(curve.endValue);
if (!Number.isFinite(fullEnd) || !Number.isFinite(midEnd) || !Number.isFinite(end)
|| !Number.isFinite(midValue) || !Number.isFinite(endValue)) return 1;
if (
!Number.isFinite(fullEnd) ||
!Number.isFinite(midEnd) ||
!Number.isFinite(end) ||
!Number.isFinite(midValue) ||
!Number.isFinite(endValue)
)
return 1;
if (altitudeM <= fullEnd) return 1;
if (altitudeM <= midEnd) {
const span = Math.max(1, midEnd - fullEnd);
let progress = Math.max(0, Math.min(1, (altitudeM - fullEnd) / span));
if (curve.smoothToMid === true) progress = progress * progress * (3 - 2 * progress);
if (curve.smoothToMid === true)
progress = progress * progress * (3 - 2 * progress);
return 1 + (midValue - 1) * progress;
}
if (altitudeM >= end) return endValue;
const progress = Math.max(0, Math.min(1, (altitudeM - midEnd) / Math.max(1, end - midEnd)));
const progress = Math.max(
0,
Math.min(1, (altitudeM - midEnd) / Math.max(1, end - midEnd)),
);
return midValue + (endValue - midValue) * progress;
}
@@ -215,11 +248,14 @@ export function altitudeScale(altitudeM, curve = null) {
* @param {number} [options.fadeEnd]
* @returns {number}
*/
export function altitudeFade(altitudeM, {
minAltitude = Number.NEGATIVE_INFINITY,
fadeStart = Number.POSITIVE_INFINITY,
fadeEnd = Number.POSITIVE_INFINITY,
} = {}) {
export function altitudeFade(
altitudeM,
{
minAltitude = Number.NEGATIVE_INFINITY,
fadeStart = Number.POSITIVE_INFINITY,
fadeEnd = Number.POSITIVE_INFINITY,
} = {},
) {
if (!Number.isFinite(altitudeM)) return 1;
if (Number.isFinite(minAltitude) && altitudeM < minAltitude) return 0;
if (!Number.isFinite(fadeEnd)) return 1;
@@ -238,24 +274,32 @@ export function combinedOverlayAlpha({
altitudeFade: altitudeAlpha = 1,
keyholeEdgeFade = 1,
} = {}) {
return clampUnit(sourceAlpha)
* clampUnit(temporalFade)
* clampUnit(distanceAlpha)
* clampUnit(altitudeAlpha)
* clampUnit(keyholeEdgeFade);
return (
clampUnit(sourceAlpha) *
clampUnit(temporalFade) *
clampUnit(distanceAlpha) *
clampUnit(altitudeAlpha) *
clampUnit(keyholeEdgeFade)
);
}
function clampUnit(value) {
return Number.isFinite(Number(value)) ? Math.max(0, Math.min(1, Number(value))) : 1;
return Number.isFinite(Number(value))
? Math.max(0, Math.min(1, Number(value)))
: 1;
}
function trackDisplayText(entry) {
const title = entry?.title || '';
const detail = Array.isArray(entry?.details) ? entry.details[0] || '' : '';
if (entry._overlayTrackDisplayTitle !== title || entry._overlayTrackDisplayDetail !== detail) {
if (
entry._overlayTrackDisplayTitle !== title ||
entry._overlayTrackDisplayDetail !== detail
) {
entry._overlayTrackDisplayTitle = title;
entry._overlayTrackDisplayDetail = detail;
entry._overlayTrackDisplayText = title && detail ? `${title} · ${detail}` : title || detail;
entry._overlayTrackDisplayText =
title && detail ? `${title} · ${detail}` : title || detail;
}
return entry._overlayTrackDisplayText || '';
}
@@ -268,16 +312,23 @@ function trackDisplayText(entry) {
* @returns {{w:number,h:number,padX:number,padY:number,titleH:number,lineH:number,thumbW:number,thumbH:number}}
*/
export function measureOverlayEntry(ctx, entry, out = {}) {
const variant = entry?.selected ? 'selected' : String(entry?.variant || 'label');
const variant = entry?.selected
? 'selected'
: String(entry?.variant || 'label');
const details = Array.isArray(entry?.details) ? entry.details : [];
const selected = variant === 'selected';
const tracked = variant === 'tracked';
const tactical = entry?.cardStyle === 'tactical';
const titleFont = tracked
? WORLD_OVERLAY_STYLE.fontTrackedTitle
: selected ? WORLD_OVERLAY_STYLE.fontSelected : WORLD_OVERLAY_STYLE.fontTitle;
let titleWidth = measureWorldOverlayText(ctx, entry?.title || '',
variant === 'label' ? WORLD_OVERLAY_STYLE.fontLabel : titleFont);
: selected
? WORLD_OVERLAY_STYLE.fontSelected
: WORLD_OVERLAY_STYLE.fontTitle;
let titleWidth = measureWorldOverlayText(
ctx,
entry?.title || '',
variant === 'label' ? WORLD_OVERLAY_STYLE.fontLabel : titleFont,
);
if (variant === 'track' && details[0]) {
titleWidth = measureWorldOverlayText(
ctx,
@@ -292,16 +343,38 @@ export function measureOverlayEntry(ctx, entry, out = {}) {
measureWorldOverlayText(
ctx,
details[i],
tracked ? WORLD_OVERLAY_STYLE.fontTrackedDetail : WORLD_OVERLAY_STYLE.fontDetail,
tracked
? WORLD_OVERLAY_STYLE.fontTrackedDetail
: WORLD_OVERLAY_STYLE.fontDetail,
),
);
}
out.padX = tracked ? 13 : selected ? 12 : variant === 'label' || variant === 'track' ? 6 : 9;
out.padY = tracked ? 9 : selected ? 8 : variant === 'label' || variant === 'track' ? 4 : 6;
out.padX = tracked
? 13
: selected
? 12
: variant === 'label' || variant === 'track'
? 6
: 9;
out.padY = tracked
? 9
: selected
? 8
: variant === 'label' || variant === 'track'
? 4
: 6;
out.titleH = tactical
? (selected ? 14 : 12)
: tracked ? 13 : selected ? 15 : variant === 'label' || variant === 'track' ? 11 : 13;
? selected
? 14
: 12
: tracked
? 13
: selected
? 15
: variant === 'label' || variant === 'track'
? 11
: 13;
out.lineH = tracked ? 17 : selected ? 15 : 13;
out.thumbW = 0;
out.thumbH = 0;
@@ -325,7 +398,17 @@ export function measureOverlayEntry(ctx, entry, out = {}) {
return out;
}
function writePlacement(out, corner, x, y, w, h, anchorX, anchorY, signedLeaderOffset = 0) {
function writePlacement(
out,
corner,
x,
y,
w,
h,
anchorX,
anchorY,
signedLeaderOffset = 0,
) {
const placement = out || {};
placement.corner = corner;
placement.rect ||= {};
@@ -350,12 +433,17 @@ function writePlacement(out, corner, x, y, w, h, anchorX, anchorY, signedLeaderO
// pushed the card sideways off its anchor, which reads as "the card isn't
// attached to that camera".
placement.leadToX = anchorX;
placement.leadToY = corner === 'above' ? placement.rect.y + h : placement.rect.y;
placement.leadToY =
corner === 'above' ? placement.rect.y + h : placement.rect.y;
} else {
placement.leadFromX = anchorX;
placement.leadFromY = anchorY;
placement.leadToX = corner === 'left' ? placement.rect.x + w : placement.rect.x;
placement.leadToY = Math.max(placement.rect.y, Math.min(anchorY, placement.rect.y + h));
placement.leadToX =
corner === 'left' ? placement.rect.x + w : placement.rect.x;
placement.leadToY = Math.max(
placement.rect.y,
Math.min(anchorY, placement.rect.y + h),
);
}
return placement;
}
@@ -373,7 +461,10 @@ const VERTICAL_PLACEMENT_ORDERS = Object.freeze({
});
function clampPlacementCoordinate(value, size, viewportSize, margin = 4) {
return Math.max(margin, Math.min(value, Math.max(margin, viewportSize - size - margin)));
return Math.max(
margin,
Math.min(value, Math.max(margin, viewportSize - size - margin)),
);
}
/**
@@ -394,19 +485,22 @@ function clampPlacementCoordinate(value, size, viewportSize, margin = 4) {
* @param {Array<object>} [out]
* @returns {Array<object>}
*/
export function placementVariants({
anchorX,
anchorY,
width,
height,
viewportWidth,
viewportHeight,
gap = 12,
preferred = 'auto',
leaderOffset = 0,
verticalOnly = false,
viewportMargin = 4,
}, out = []) {
export function placementVariants(
{
anchorX,
anchorY,
width,
height,
viewportWidth,
viewportHeight,
gap = 12,
preferred = 'auto',
leaderOffset = 0,
verticalOnly = false,
viewportMargin = 4,
},
out = [],
) {
const automatic = anchorY - gap - height >= 4 ? 'above' : 'below';
const orders = verticalOnly ? VERTICAL_PLACEMENT_ORDERS : PLACEMENT_ORDERS;
const order = orders[preferred] || orders[automatic];
@@ -437,7 +531,9 @@ export function placementVariants({
height,
anchorX,
anchorY,
corner === 'above' || corner === 'left' ? negativeLeaderOffset : positiveLeaderOffset,
corner === 'above' || corner === 'left'
? negativeLeaderOffset
: positiveLeaderOffset,
);
}
out.length = order.length;
@@ -454,9 +550,15 @@ function drawLeader(ctx, placement, accent) {
if (placement.leaderOffset === 0) {
ctx.moveTo(placement.leadFromX, placement.leadFromY);
} else if (placement.corner === 'above' || placement.corner === 'below') {
ctx.moveTo(placement.leadFromX, placement.leadFromY + placement.leaderOffset);
ctx.moveTo(
placement.leadFromX,
placement.leadFromY + placement.leaderOffset,
);
} else {
ctx.moveTo(placement.leadFromX + placement.leaderOffset, placement.leadFromY);
ctx.moveTo(
placement.leadFromX + placement.leaderOffset,
placement.leadFromY,
);
}
ctx.lineTo(placement.leadToX, placement.leadToY);
ctx.stroke();
@@ -468,9 +570,13 @@ function drawCardChrome(ctx, entry, placement, selected = false) {
drawLeader(ctx, placement, accent);
ctx.beginPath();
roundedRectPath(ctx, x, y, w, h);
ctx.fillStyle = selected ? WORLD_OVERLAY_STYLE.selectedBackground : WORLD_OVERLAY_STYLE.background;
ctx.fillStyle = selected
? WORLD_OVERLAY_STYLE.selectedBackground
: WORLD_OVERLAY_STYLE.background;
ctx.fill();
ctx.strokeStyle = selected ? WORLD_OVERLAY_STYLE.selectedBorder : WORLD_OVERLAY_STYLE.border;
ctx.strokeStyle = selected
? WORLD_OVERLAY_STYLE.selectedBorder
: WORLD_OVERLAY_STYLE.border;
ctx.lineWidth = selected ? 1.25 : 1;
ctx.stroke();
ctx.fillStyle = accent;
@@ -482,7 +588,9 @@ function drawCardText(ctx, entry, placement, selected = false, topOffset = 0) {
const x = placement.rect.x + (selected ? 12 : 9);
let y = placement.rect.y + (selected ? 8 : 6) + topOffset;
ctx.fillStyle = WORLD_OVERLAY_STYLE.title;
ctx.font = selected ? WORLD_OVERLAY_STYLE.fontSelected : WORLD_OVERLAY_STYLE.fontTitle;
ctx.font = selected
? WORLD_OVERLAY_STYLE.fontSelected
: WORLD_OVERLAY_STYLE.fontTitle;
ctx.textBaseline = 'top';
ctx.fillText(String(entry.title || ''), x, y);
y += selected ? 15 : 13;
@@ -497,7 +605,8 @@ function drawCardText(ctx, entry, placement, selected = false, topOffset = 0) {
function colorWithAlpha(color, alpha) {
const text = String(color || WORLD_OVERLAY_STYLE.accent).trim();
const triplet = text.match(/^(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})$/);
if (triplet) return `rgba(${triplet[1]}, ${triplet[2]}, ${triplet[3]}, ${alpha})`;
if (triplet)
return `rgba(${triplet[1]}, ${triplet[2]}, ${triplet[3]}, ${alpha})`;
const hex = text.match(/^#([0-9a-f]{6})$/i);
if (hex) {
const value = Number.parseInt(hex[1], 16);
@@ -534,33 +643,57 @@ function leaderDrawRatio(entry) {
return Number.isFinite(ratio) ? Math.max(0.1, Math.min(0.9, ratio)) : 0.68;
}
export function leaderRevealProgress(entry, timestamp = globalThis.performance?.now?.() ?? Date.now()) {
const linear = Math.min(1, animationLinearProgress(entry, timestamp) / leaderDrawRatio(entry));
export function leaderRevealProgress(
entry,
timestamp = globalThis.performance?.now?.() ?? Date.now(),
) {
const linear = Math.min(
1,
animationLinearProgress(entry, timestamp) / leaderDrawRatio(entry),
);
return 1 - Math.pow(1 - linear, 3);
}
export function tacticalCardRevealAlpha(entry, timestamp = globalThis.performance?.now?.() ?? Date.now()) {
export function tacticalCardRevealAlpha(
entry,
timestamp = globalThis.performance?.now?.() ?? Date.now(),
) {
if (!(Math.max(0, Number(entry?.leaderAnimationMs) || 0) > 0)) return 1;
const start = leaderDrawRatio(entry);
const linear = Math.max(0, Math.min(1,
(animationLinearProgress(entry, timestamp) - start) / Math.max(0.1, 1 - start)));
const linear = Math.max(
0,
Math.min(
1,
(animationLinearProgress(entry, timestamp) - start) /
Math.max(0.1, 1 - start),
),
);
return linear * linear * (3 - 2 * linear);
}
function drawTacticalLeader(ctx, entry, placement, accent, timestamp) {
ctx.strokeStyle = accent;
ctx.lineWidth = (entry.leaderStyle === 'elbow' ? 1.5 : 1) / (placement.paintScale || 1);
ctx.lineWidth =
(entry.leaderStyle === 'elbow' ? 1.5 : 1) / (placement.paintScale || 1);
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath();
if (entry.leaderStyle !== 'elbow'
|| (placement.corner !== 'above' && placement.corner !== 'below')) {
if (
entry.leaderStyle !== 'elbow' ||
(placement.corner !== 'above' && placement.corner !== 'below')
) {
if (placement.leaderOffset === 0) {
ctx.moveTo(placement.leadFromX, placement.leadFromY);
} else if (placement.corner === 'above' || placement.corner === 'below') {
ctx.moveTo(placement.leadFromX, placement.leadFromY + placement.leaderOffset);
ctx.moveTo(
placement.leadFromX,
placement.leadFromY + placement.leaderOffset,
);
} else {
ctx.moveTo(placement.leadFromX + placement.leaderOffset, placement.leadFromY);
ctx.moveTo(
placement.leadFromX + placement.leaderOffset,
placement.leadFromY,
);
}
ctx.lineTo(placement.leadToX, placement.leadToY);
ctx.stroke();
@@ -575,7 +708,9 @@ function drawTacticalLeader(ctx, entry, placement, accent, timestamp) {
const anchorY = placement.leadFromY;
const horizontalLength = Math.abs(anchorX - cardX);
const verticalLength = Math.abs(anchorY - cardY);
let remaining = leaderRevealProgress(entry, timestamp) * (horizontalLength + verticalLength);
let remaining =
leaderRevealProgress(entry, timestamp) *
(horizontalLength + verticalLength);
ctx.moveTo(anchorX, anchorY);
if (remaining <= horizontalLength) {
const direction = Math.sign(cardX - anchorX) || -1;
@@ -584,7 +719,10 @@ function drawTacticalLeader(ctx, entry, placement, accent, timestamp) {
ctx.lineTo(cardX, anchorY);
remaining -= horizontalLength;
const direction = Math.sign(cardY - anchorY) || -1;
ctx.lineTo(cardX, anchorY + direction * Math.min(verticalLength, remaining));
ctx.lineTo(
cardX,
anchorY + direction * Math.min(verticalLength, remaining),
);
}
ctx.stroke();
}
@@ -600,9 +738,15 @@ export function paintTacticalCard(ctx, entry, placement, alpha = 1) {
ctx.save();
ctx.globalAlpha = alpha;
drawTacticalLeader(ctx, entry, placement, entry.leaderStyle === 'elbow'
? colorWithAlpha(entry.accent, 0.95)
: accentColors.leader, animationTimestamp);
drawTacticalLeader(
ctx,
entry,
placement,
entry.leaderStyle === 'elbow'
? colorWithAlpha(entry.accent, 0.95)
: accentColors.leader,
animationTimestamp,
);
ctx.globalAlpha = alpha * tacticalCardRevealAlpha(entry, animationTimestamp);
@@ -625,13 +769,19 @@ export function paintTacticalCard(ctx, entry, placement, alpha = 1) {
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic';
ctx.fillStyle = WORLD_OVERLAY_STYLE.title;
ctx.font = selected ? WORLD_OVERLAY_STYLE.fontSelected : WORLD_OVERLAY_STYLE.fontTitle;
ctx.font = selected
? WORLD_OVERLAY_STYLE.fontSelected
: WORLD_OVERLAY_STYLE.fontTitle;
const titleBaseline = y + layout.padY + layout.titleH - 2;
ctx.fillText(String(entry.title || ''), x + layout.padX, titleBaseline);
ctx.fillStyle = WORLD_OVERLAY_STYLE.detail;
ctx.font = WORLD_OVERLAY_STYLE.fontDetail;
for (let i = 0; i < details.length; i++) {
ctx.fillText(String(details[i]), x + layout.padX, titleBaseline + (i + 1) * layout.lineH);
ctx.fillText(
String(details[i]),
x + layout.padX,
titleBaseline + (i + 1) * layout.lineH,
);
}
ctx.restore();
return placement.rect;
@@ -645,7 +795,11 @@ export function paintLabel(ctx, entry, placement, alpha = 1) {
ctx.fillStyle = WORLD_OVERLAY_STYLE.title;
ctx.font = WORLD_OVERLAY_STYLE.fontLabel;
ctx.textBaseline = 'top';
ctx.fillText(String(entry.title || ''), placement.rect.x + 6, placement.rect.y + 4);
ctx.fillText(
String(entry.title || ''),
placement.rect.x + 6,
placement.rect.y + 4,
);
ctx.restore();
return placement.rect;
}
@@ -690,7 +844,11 @@ export function paintThumbnail(ctx, entry, placement, alpha = 1) {
ctx.save();
ctx.globalAlpha = alpha;
drawLeader(ctx, placement, entry.thumbnailLeaderColor || WORLD_OVERLAY_STYLE.leader);
drawLeader(
ctx,
placement,
entry.thumbnailLeaderColor || WORLD_OVERLAY_STYLE.leader,
);
ctx.beginPath();
roundedRectPath(ctx, x, y, w, h, Number(entry.thumbnailRadius) || 4);
ctx.fillStyle = entry.thumbnailBackground || WORLD_OVERLAY_STYLE.background;
@@ -711,13 +869,17 @@ export function paintThumbnail(ctx, entry, placement, alpha = 1) {
if (ruleHeight > 0) {
ctx.beginPath();
roundedRectPath(ctx, x, y, w, ruleHeight, Math.min(1, ruleHeight));
ctx.fillStyle = entry.thumbnailRuleColor || entry.accent || WORLD_OVERLAY_STYLE.accent;
ctx.fillStyle =
entry.thumbnailRuleColor || entry.accent || WORLD_OVERLAY_STYLE.accent;
ctx.fill();
}
const titleChars = Math.max(0, Math.floor(Number(entry.thumbnailTitleChars) || 0));
const title = entry._overlayThumbnailTitle
|| String(entry.title || '').toUpperCase();
const titleChars = Math.max(
0,
Math.floor(Number(entry.thumbnailTitleChars) || 0),
);
const title =
entry._overlayThumbnailTitle || String(entry.title || '').toUpperCase();
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic';
ctx.fillStyle = entry.thumbnailTitleColor || WORLD_OVERLAY_STYLE.title;
@@ -769,7 +931,11 @@ export function paintTracked(ctx, entry, placement, alpha = 1) {
ctx.fillStyle = WORLD_OVERLAY_STYLE.detail;
ctx.font = WORLD_OVERLAY_STYLE.fontTrackedDetail;
for (let i = 0; i < details.length; i++) {
ctx.fillText(String(details[i]), centerX, titleBaseline + (i + 1) * layout.lineH);
ctx.fillText(
String(details[i]),
centerX,
titleBaseline + (i + 1) * layout.lineH,
);
}
ctx.restore();
return placement.rect;
@@ -842,7 +1008,8 @@ export function paintDetectionCallout(ctx, callout, alpha = 1) {
ctx.globalAlpha = alpha;
ctx.fillStyle = callout.label;
ctx.font = callout.font;
if (callout.primary) ctx.fillText(callout.primary, callout.primaryX, callout.baseline);
if (callout.primary)
ctx.fillText(callout.primary, callout.primaryX, callout.baseline);
if (callout.micro) {
ctx.globalAlpha = alpha * 0.8;
ctx.font = callout.microFont;
@@ -852,11 +1019,16 @@ export function paintDetectionCallout(ctx, callout, alpha = 1) {
/** Dispatch a normalized entry to its pure variant painter. */
export function paintOverlayEntry(ctx, entry, placement, alpha = 1) {
if (entry.cardStyle === 'tactical') return paintTacticalCard(ctx, entry, placement, alpha);
if (entry.variant === 'tracked') return paintTracked(ctx, entry, placement, alpha);
if (entry.selected || entry.variant === 'selected') return paintSelected(ctx, entry, placement, alpha);
if (entry.variant === 'thumbnail') return paintThumbnail(ctx, entry, placement, alpha);
if (entry.cardStyle === 'tactical')
return paintTacticalCard(ctx, entry, placement, alpha);
if (entry.variant === 'tracked')
return paintTracked(ctx, entry, placement, alpha);
if (entry.selected || entry.variant === 'selected')
return paintSelected(ctx, entry, placement, alpha);
if (entry.variant === 'thumbnail')
return paintThumbnail(ctx, entry, placement, alpha);
if (entry.variant === 'card') return paintCard(ctx, entry, placement, alpha);
if (entry.variant === 'track') return paintTrack(ctx, entry, placement, alpha);
if (entry.variant === 'track')
return paintTrack(ctx, entry, placement, alpha);
return paintLabel(ctx, entry, placement, alpha);
}