fix(cctv): test the still fit, redraw on size change, share the matte color

This commit is contained in:
Daniel Slay committed 2026-10-19 10:39:41 -04:00
1 parent 5a87fb3b15
commit 1959b33c48
4 files changed
+62 -8

No files matched your search

+1
View File
@@ -555,6 +555,7 @@
"src/data/cctvNswSource.test.mjs",
"src/data/cctvTxdotSource.test.mjs",
"src/layers/cctv/source.test.mjs",
"src/layers/cctv/frames.test.mjs",
"src/data/alprCameras.js",
"src/data/alprCameras.test.mjs",
"src/data/dataCredits.test.mjs",
+21 -8
View File
@@ -3,6 +3,7 @@ import {
FRAME_SIGNATURE_H,
PROJECTION_CANVAS_WIDTH,
PROJECTION_CANVAS_HEIGHT,
PROJECTION_MATTE_COLOR,
PROJECTION_TEXTURE_SWAP_MS,
ACTIVE_FRAME_REFRESH_MS,
FRAME_ENDPOINT,
@@ -12,7 +13,16 @@ import {
PLACEHOLDER_REPAINT_MS,
} 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)
return {
x: 0,
@@ -204,7 +214,7 @@ export function createFrames({ state: layerState, services, parts, source }) {
ctx.clearRect(0, 0, w, h);
const g = ctx.createLinearGradient(0, 0, w, h);
g.addColorStop(0, '#05111a');
g.addColorStop(1, '#01070c');
g.addColorStop(1, PROJECTION_MATTE_COLOR);
ctx.fillStyle = g;
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
// two Cesium needs to rebind, which IS the periodic white flash from the
// 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;
if (signature !== null && signature === runtime.lastFrameSignature) {
// Identical pixels — leave the canvas, and therefore the bound
@@ -350,17 +367,13 @@ export function createFrames({ state: layerState, services, parts, source }) {
return;
}
runtime.lastFrameSignature = signature;
runtime.ctx.fillStyle = '#01070c';
runtime.ctx.fillStyle = PROJECTION_MATTE_COLOR;
runtime.ctx.fillRect(
0,
0,
PROJECTION_CANVAS_WIDTH,
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.canvasStamp = (runtime.canvasStamp || 0) + 1;
runtime.lastPlaceholderPaintAt = 0;
+37
View File
@@ -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);
});
+3
View File
@@ -35,6 +35,9 @@ export const PROJECTION_CANVAS_WIDTH = 1920;
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).
// 64x36 keeps the 16:9 aspect and reads ~9 KB per check versus the 8.3 MB a
// full-resolution compare would touch.