mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
fix(cctv): test the still fit, redraw on size change, share the matte color
This commit is contained in:
4 files changed
+62
-8
No files matched your search
@@ -555,6 +555,7 @@
|
|||||||
"src/data/cctvNswSource.test.mjs",
|
"src/data/cctvNswSource.test.mjs",
|
||||||
"src/data/cctvTxdotSource.test.mjs",
|
"src/data/cctvTxdotSource.test.mjs",
|
||||||
"src/layers/cctv/source.test.mjs",
|
"src/layers/cctv/source.test.mjs",
|
||||||
|
"src/layers/cctv/frames.test.mjs",
|
||||||
"src/data/alprCameras.js",
|
"src/data/alprCameras.js",
|
||||||
"src/data/alprCameras.test.mjs",
|
"src/data/alprCameras.test.mjs",
|
||||||
"src/data/dataCredits.test.mjs",
|
"src/data/dataCredits.test.mjs",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
FRAME_SIGNATURE_H,
|
FRAME_SIGNATURE_H,
|
||||||
PROJECTION_CANVAS_WIDTH,
|
PROJECTION_CANVAS_WIDTH,
|
||||||
PROJECTION_CANVAS_HEIGHT,
|
PROJECTION_CANVAS_HEIGHT,
|
||||||
|
PROJECTION_MATTE_COLOR,
|
||||||
PROJECTION_TEXTURE_SWAP_MS,
|
PROJECTION_TEXTURE_SWAP_MS,
|
||||||
ACTIVE_FRAME_REFRESH_MS,
|
ACTIVE_FRAME_REFRESH_MS,
|
||||||
FRAME_ENDPOINT,
|
FRAME_ENDPOINT,
|
||||||
@@ -12,7 +13,16 @@ import {
|
|||||||
PLACEHOLDER_REPAINT_MS,
|
PLACEHOLDER_REPAINT_MS,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
|
||||||
function fitToProjection(sourceW, sourceH) {
|
/**
|
||||||
|
* Contain-fits a source frame into the projection canvas: scaled to the
|
||||||
|
* largest size that fits, centered, with the margins left for the matte fill.
|
||||||
|
* Falls back to the full canvas when either dimension is missing or zero.
|
||||||
|
*
|
||||||
|
* @param {number} sourceW - Source width in pixels.
|
||||||
|
* @param {number} sourceH - Source height in pixels.
|
||||||
|
* @returns {{x:number,y:number,w:number,h:number}} Draw rectangle in canvas pixels.
|
||||||
|
*/
|
||||||
|
export function fitToProjection(sourceW, sourceH) {
|
||||||
if (!sourceW || !sourceH)
|
if (!sourceW || !sourceH)
|
||||||
return {
|
return {
|
||||||
x: 0,
|
x: 0,
|
||||||
@@ -204,7 +214,7 @@ export function createFrames({ state: layerState, services, parts, source }) {
|
|||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
const g = ctx.createLinearGradient(0, 0, w, h);
|
const g = ctx.createLinearGradient(0, 0, w, h);
|
||||||
g.addColorStop(0, '#05111a');
|
g.addColorStop(0, '#05111a');
|
||||||
g.addColorStop(1, '#01070c');
|
g.addColorStop(1, PROJECTION_MATTE_COLOR);
|
||||||
ctx.fillStyle = g;
|
ctx.fillStyle = g;
|
||||||
ctx.fillRect(0, 0, w, h);
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
|
||||||
@@ -342,7 +352,14 @@ export function createFrames({ state: layerState, services, parts, source }) {
|
|||||||
// base color (planeMaterial color = WHITE, alpha .95) for the frame or
|
// base color (planeMaterial color = WHITE, alpha .95) for the frame or
|
||||||
// two Cesium needs to rebind, which IS the periodic white flash from the
|
// two Cesium needs to rebind, which IS the periodic white flash from the
|
||||||
// owner field tests (2026-07-04 and 2026-07-30).
|
// owner field tests (2026-07-04 and 2026-07-30).
|
||||||
const signature = projectionFrameSignature(runtime);
|
const fit = fitToProjection(
|
||||||
|
runtime.image.naturalWidth,
|
||||||
|
runtime.image.naturalHeight,
|
||||||
|
);
|
||||||
|
// Pixels are hashed at a fixed size, so the fit carries the aspect.
|
||||||
|
const pixels = projectionFrameSignature(runtime);
|
||||||
|
const signature =
|
||||||
|
pixels === null ? null : `${pixels}:${fit.w}x${fit.h}`;
|
||||||
runtime.drawnImageStamp = runtime.imageStamp;
|
runtime.drawnImageStamp = runtime.imageStamp;
|
||||||
if (signature !== null && signature === runtime.lastFrameSignature) {
|
if (signature !== null && signature === runtime.lastFrameSignature) {
|
||||||
// Identical pixels — leave the canvas, and therefore the bound
|
// Identical pixels — leave the canvas, and therefore the bound
|
||||||
@@ -350,17 +367,13 @@ export function createFrames({ state: layerState, services, parts, source }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runtime.lastFrameSignature = signature;
|
runtime.lastFrameSignature = signature;
|
||||||
runtime.ctx.fillStyle = '#01070c';
|
runtime.ctx.fillStyle = PROJECTION_MATTE_COLOR;
|
||||||
runtime.ctx.fillRect(
|
runtime.ctx.fillRect(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
PROJECTION_CANVAS_WIDTH,
|
PROJECTION_CANVAS_WIDTH,
|
||||||
PROJECTION_CANVAS_HEIGHT,
|
PROJECTION_CANVAS_HEIGHT,
|
||||||
);
|
);
|
||||||
const fit = fitToProjection(
|
|
||||||
runtime.image.naturalWidth,
|
|
||||||
runtime.image.naturalHeight,
|
|
||||||
);
|
|
||||||
runtime.ctx.drawImage(runtime.image, fit.x, fit.y, fit.w, fit.h);
|
runtime.ctx.drawImage(runtime.image, fit.x, fit.y, fit.w, fit.h);
|
||||||
runtime.canvasStamp = (runtime.canvasStamp || 0) + 1;
|
runtime.canvasStamp = (runtime.canvasStamp || 0) + 1;
|
||||||
runtime.lastPlaceholderPaintAt = 0;
|
runtime.lastPlaceholderPaintAt = 0;
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// src/layers/cctv/frames.test.mjs
|
||||||
|
// Still-frame fit (#1201): stills draw at their source aspect, centered on the
|
||||||
|
// 1920x1080 projection canvas, instead of being stretched to fill it.
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { fitToProjection } from './frames.js';
|
||||||
|
|
||||||
|
const FULL_CANVAS = { x: 0, y: 0, w: 1920, h: 1080 };
|
||||||
|
|
||||||
|
test('a 4:3 still is pillarboxed at full height', () => {
|
||||||
|
assert.deepEqual(fitToProjection(640, 480), {
|
||||||
|
x: 240,
|
||||||
|
y: 0,
|
||||||
|
w: 1440,
|
||||||
|
h: 1080,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a source wider than 16:9 is letterboxed at full width', () => {
|
||||||
|
assert.deepEqual(fitToProjection(3840, 1080), {
|
||||||
|
x: 0,
|
||||||
|
y: 270,
|
||||||
|
w: 1920,
|
||||||
|
h: 540,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a 16:9 still fills the canvas', () => {
|
||||||
|
assert.deepEqual(fitToProjection(1920, 1080), FULL_CANVAS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a missing or zero dimension falls back to the full canvas', () => {
|
||||||
|
assert.deepEqual(fitToProjection(0, 480), FULL_CANVAS);
|
||||||
|
assert.deepEqual(fitToProjection(640, 0), FULL_CANVAS);
|
||||||
|
assert.deepEqual(fitToProjection(640, undefined), FULL_CANVAS);
|
||||||
|
assert.deepEqual(fitToProjection(undefined, undefined), FULL_CANVAS);
|
||||||
|
});
|
||||||
@@ -35,6 +35,9 @@ export const PROJECTION_CANVAS_WIDTH = 1920;
|
|||||||
|
|
||||||
export const PROJECTION_CANVAS_HEIGHT = 1080;
|
export const PROJECTION_CANVAS_HEIGHT = 1080;
|
||||||
|
|
||||||
|
/** Matte behind fitted stills; the placeholder gradient's dark end. */
|
||||||
|
export const PROJECTION_MATTE_COLOR = '#01070c';
|
||||||
|
|
||||||
// Downsample grid for the unchanged-frame signature (drawProjectionFrame).
|
// Downsample grid for the unchanged-frame signature (drawProjectionFrame).
|
||||||
// 64x36 keeps the 16:9 aspect and reads ~9 KB per check versus the 8.3 MB a
|
// 64x36 keeps the 16:9 aspect and reads ~9 KB per check versus the 8.3 MB a
|
||||||
// full-resolution compare would touch.
|
// full-resolution compare would touch.
|
||||||
|
|||||||
Reference in new issue
Block a user