mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Port ALPR camera visuals into the shared layer
Add camera badges, gradient direction wedges and animated tactical selection cards while preserving bounded loading and layer lifecycle. Align the ALPR data-layer header and retain native pick ownership for overlay badges. Co-authored-by: manjunath22466 <[email protected]>
This commit is contained in:
1 parent
687edc4da7
commit
87629ac175
21 files changed
+951
-97
No files matched your search
@@ -466,6 +466,16 @@ export function normalizeOverlayEntry(sourceId, entry) {
|
||||
leaderOffsetPx: Number.isFinite(Number(entry.leaderOffsetPx))
|
||||
? Math.max(0, Number(entry.leaderOffsetPx))
|
||||
: 0,
|
||||
leaderStyle: entry.leaderStyle === 'elbow' ? 'elbow' : 'straight',
|
||||
leaderAnimationMs: Number.isFinite(Number(entry.leaderAnimationMs))
|
||||
? Math.max(0, Number(entry.leaderAnimationMs))
|
||||
: 0,
|
||||
leaderAnimationStartedAt: Number.isFinite(Number(entry.leaderAnimationStartedAt))
|
||||
? Number(entry.leaderAnimationStartedAt)
|
||||
: 0,
|
||||
leaderDrawRatio: Number.isFinite(Number(entry.leaderDrawRatio))
|
||||
? Math.max(0.1, Math.min(0.9, Number(entry.leaderDrawRatio)))
|
||||
: 0.68,
|
||||
anchorRadiusPx: Number.isFinite(Number(entry.anchorRadiusPx))
|
||||
? Math.max(0, Number(entry.anchorRadiusPx))
|
||||
: 0,
|
||||
@@ -1421,6 +1431,19 @@ function activeFadeCount(timestamp) {
|
||||
return count;
|
||||
}
|
||||
|
||||
function activeLeaderAnimationCount(timestamp) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < _sourceList.length; i++) {
|
||||
const source = _sourceList[i];
|
||||
if (!sourceActive(source)) continue;
|
||||
for (const entry of source.entries.values()) {
|
||||
if (entry.leaderAnimationMs > 0
|
||||
&& timestamp - entry.leaderAnimationStartedAt < entry.leaderAnimationMs) count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function overlayHasPaintWork(timestamp = nowMs()) {
|
||||
if (activeCustomPaintLaneCount() > 0) return true;
|
||||
if (activeEntryCount() > 0) return true;
|
||||
@@ -2177,8 +2200,8 @@ function drawWorldOverlay() {
|
||||
_diagnostics.projectionMs = nowMs() - projectionStarted;
|
||||
solveDomains(timestamp);
|
||||
paintFrame(keyhole);
|
||||
const fadesRemaining = activeFadeCount(timestamp);
|
||||
if (fadesRemaining > 0) _viewer.scene.requestRender?.();
|
||||
const animationsRemaining = activeFadeCount(timestamp) + activeLeaderAnimationCount(timestamp);
|
||||
if (animationsRemaining > 0) _viewer.scene.requestRender?.();
|
||||
}
|
||||
|
||||
function createDevFacade() {
|
||||
|
||||
@@ -521,6 +521,74 @@ function tacticalAccentColors(entry) {
|
||||
return colors;
|
||||
}
|
||||
|
||||
function animationLinearProgress(entry, timestamp) {
|
||||
const duration = Math.max(0, Number(entry?.leaderAnimationMs) || 0);
|
||||
if (!duration) return 1;
|
||||
const startedAt = Number(entry?.leaderAnimationStartedAt);
|
||||
if (!Number.isFinite(startedAt)) return 1;
|
||||
return Math.max(0, Math.min(1, (timestamp - startedAt) / duration));
|
||||
}
|
||||
|
||||
function leaderDrawRatio(entry) {
|
||||
const ratio = Number(entry?.leaderDrawRatio);
|
||||
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));
|
||||
return 1 - Math.pow(1 - linear, 3);
|
||||
}
|
||||
|
||||
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)));
|
||||
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.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
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);
|
||||
} else {
|
||||
ctx.moveTo(placement.leadFromX + placement.leaderOffset, placement.leadFromY);
|
||||
}
|
||||
ctx.lineTo(placement.leadToX, placement.leadToY);
|
||||
ctx.stroke();
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y, w, h } = placement.rect;
|
||||
const cardX = x + Math.max(18, Math.min(w - 18, w * 0.22));
|
||||
const cardY = placement.corner === 'above' ? y + h : y;
|
||||
const anchorRadius = Math.abs(placement.leaderOffset);
|
||||
const anchorX = placement.leadFromX - anchorRadius;
|
||||
const anchorY = placement.leadFromY;
|
||||
const horizontalLength = Math.abs(anchorX - cardX);
|
||||
const verticalLength = Math.abs(anchorY - cardY);
|
||||
let remaining = leaderRevealProgress(entry, timestamp) * (horizontalLength + verticalLength);
|
||||
ctx.moveTo(anchorX, anchorY);
|
||||
if (remaining <= horizontalLength) {
|
||||
const direction = Math.sign(cardX - anchorX) || -1;
|
||||
ctx.lineTo(anchorX + direction * remaining, anchorY);
|
||||
} else {
|
||||
ctx.lineTo(cardX, anchorY);
|
||||
remaining -= horizontalLength;
|
||||
const direction = Math.sign(cardY - anchorY) || -1;
|
||||
ctx.lineTo(cardX, anchorY + direction * Math.min(verticalLength, remaining));
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/** Paint the legacy FIRMS/vessel tactical card inside the shared host. */
|
||||
export function paintTacticalCard(ctx, entry, placement, alpha = 1) {
|
||||
const selected = entry.selected || entry.variant === 'selected';
|
||||
@@ -528,21 +596,15 @@ export function paintTacticalCard(ctx, entry, placement, alpha = 1) {
|
||||
const layout = entry._overlayLayout || {};
|
||||
const { x, y, w, h } = placement.rect;
|
||||
const accentColors = tacticalAccentColors(entry);
|
||||
const animationTimestamp = globalThis.performance?.now?.() ?? Date.now();
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
|
||||
ctx.strokeStyle = accentColors.leader;
|
||||
ctx.lineWidth = 1 / (placement.paintScale || 1);
|
||||
ctx.beginPath();
|
||||
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);
|
||||
} else {
|
||||
ctx.moveTo(placement.leadFromX + placement.leaderOffset, placement.leadFromY);
|
||||
}
|
||||
ctx.lineTo(placement.leadToX, placement.leadToY);
|
||||
ctx.stroke();
|
||||
drawTacticalLeader(ctx, entry, placement, entry.leaderStyle === 'elbow'
|
||||
? colorWithAlpha(entry.accent, 0.95)
|
||||
: accentColors.leader, animationTimestamp);
|
||||
|
||||
ctx.globalAlpha = alpha * tacticalCardRevealAlpha(entry, animationTimestamp);
|
||||
|
||||
ctx.beginPath();
|
||||
roundedRectPath(ctx, x, y, w, h, 4);
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
paintThumbnail,
|
||||
paintTrack,
|
||||
paintTracked,
|
||||
leaderRevealProgress,
|
||||
tacticalCardRevealAlpha,
|
||||
placementVariants,
|
||||
roundedRectPath,
|
||||
} from './worldOverlayDraw.js';
|
||||
@@ -486,6 +488,34 @@ test('shared tactical painter preserves FIRMS card metrics and top-rule treatmen
|
||||
assert.equal(ctx.calls.at(-1)[0], 'restore');
|
||||
});
|
||||
|
||||
test('selected elbow leader reveals from the glyph before its card fades in', () => {
|
||||
const animation = {
|
||||
leaderAnimationMs: 1000, leaderAnimationStartedAt: 100, leaderDrawRatio: 0.7,
|
||||
};
|
||||
assert.equal(leaderRevealProgress(animation, 100), 0);
|
||||
assert.ok(leaderRevealProgress(animation, 450) > 0.8);
|
||||
assert.equal(leaderRevealProgress(animation, 800), 1);
|
||||
assert.equal(tacticalCardRevealAlpha(animation, 799), 0);
|
||||
assert.ok(tacticalCardRevealAlpha(animation, 950) > 0.4);
|
||||
assert.equal(tacticalCardRevealAlpha(animation, 1100), 1);
|
||||
const ctx = mockContext();
|
||||
const entry = {
|
||||
variant: 'selected', selected: true, cardStyle: 'tactical', title: 'ALPR-2516',
|
||||
details: ['OSM MAPPED'], accent: '#ff6474', leaderStyle: 'elbow',
|
||||
leaderAnimationMs: 1, leaderAnimationStartedAt: 0,
|
||||
};
|
||||
entry._overlayLayout = measureOverlayEntry(ctx, entry, {});
|
||||
const placement = placementVariants({
|
||||
anchorX: 240, anchorY: 180, width: entry._overlayLayout.w, height: entry._overlayLayout.h,
|
||||
viewportWidth: 500, viewportHeight: 300, gap: 20, leaderOffset: 30, verticalOnly: true,
|
||||
})[0];
|
||||
paintTacticalCard(ctx, entry, placement, 1);
|
||||
assert.deepEqual(ctx.calls.find(([name]) => name === 'moveTo'), ['moveTo', 210, 180]);
|
||||
const lines = ctx.calls.filter(([name]) => name === 'lineTo');
|
||||
assert.ok(lines.length >= 2, 'leader draws horizontally from the glyph, then vertically to the card');
|
||||
assert.ok(ctx.calls.some(([name, value]) => name === 'strokeStyle' && value === 'rgba(255, 100, 116, 0.95)'));
|
||||
});
|
||||
|
||||
test('track display text is cached across measure and paint and invalidates on content change', () => {
|
||||
const ctx = mockContext();
|
||||
const entry = { variant: 'track', title: 'UAL123', details: ['450 KT'] };
|
||||
|
||||
Reference in new issue
Block a user