glytch/fktry/src/screen/overlays.ts
type-two 7f9de9c8b1 [screen] shipment stamps + scram wipe, debug hook gated to DEV
One stamp per shipped event (not per unit), 0.4s, sized by count, in the item's
chip colour. Composited in screen space so a melting feed never drags the placard.
Scram events get the one-frame TRACKING LOST bar, positioned from entity+tick so
a run always wipes the same way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:22:32 +10:00

109 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Transient overlays composited ON the feed (in screen space, so corruption never drags
* them): the shipment stamp and the scram bar.
*
* Both are painted into small canvases and uploaded only when their event fires — never
* per frame. The stamp is small (256x128) precisely so the per-event upload stays cheap.
*/
export const STAMP_W = 256;
export const STAMP_H = 128;
export const SCRAM_W = 512;
export const SCRAM_H = 64;
export interface Overlays {
stampCanvas: HTMLCanvasElement;
scramCanvas: HTMLCanvasElement;
/** Repaint the stamp for a landed shipment. OSHA-placard deadpan, in the item's chip colour. */
paintStamp(name: string, count: number, color: string): void;
}
function roundRect(g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
g.beginPath();
g.moveTo(x + r, y);
g.arcTo(x + w, y, x + w, y + h, r);
g.arcTo(x + w, y + h, x, y + h, r);
g.arcTo(x, y + h, x, y, r);
g.arcTo(x, y, x + w, y, r);
g.closePath();
}
function paintScramBar(g: CanvasRenderingContext2D) {
// A white tape-tracking bar with the deadpan legend punched out of it.
g.clearRect(0, 0, SCRAM_W, SCRAM_H);
g.fillStyle = '#f6f6f2';
g.fillRect(0, 0, SCRAM_W, SCRAM_H);
// torn edges: the bar should not look like a clean rectangle
g.fillStyle = 'rgba(0,0,0,0)';
g.globalCompositeOperation = 'destination-out';
for (let x = 0; x < SCRAM_W; x += 7) {
const t = ((x * 37) % 11) / 11;
g.fillRect(x, 0, 7, 3 + t * 5);
g.fillRect(x, SCRAM_H - (3 + (1 - t) * 5), 7, 3 + (1 - t) * 5);
}
g.globalCompositeOperation = 'source-over';
g.fillStyle = '#0a0a0c';
g.font = "bold 30px 'Courier New', monospace";
g.textBaseline = 'middle';
const label = 'TRACKING LOST';
const w = g.measureText(label).width;
g.fillText(label, (SCRAM_W - w) / 2, SCRAM_H / 2);
}
export function createOverlays(): Overlays {
const stampCanvas = document.createElement('canvas');
stampCanvas.width = STAMP_W;
stampCanvas.height = STAMP_H;
const sg = stampCanvas.getContext('2d')!;
const scramCanvas = document.createElement('canvas');
scramCanvas.width = SCRAM_W;
scramCanvas.height = SCRAM_H;
paintScramBar(scramCanvas.getContext('2d')!);
return {
stampCanvas,
scramCanvas,
paintStamp(name: string, count: number, color: string) {
sg.clearRect(0, 0, STAMP_W, STAMP_H);
sg.save();
// Stamped by hand, badly: a few degrees off true.
sg.translate(STAMP_W / 2, STAMP_H / 2);
sg.rotate(-0.045);
sg.translate(-STAMP_W / 2, -STAMP_H / 2);
// dark backing so the legend survives on top of white SMPTE bars
sg.fillStyle = 'rgba(6,6,10,0.72)';
roundRect(sg, 12, 22, STAMP_W - 24, STAMP_H - 44, 6);
sg.fill();
sg.strokeStyle = color;
sg.lineWidth = 5;
roundRect(sg, 12, 22, STAMP_W - 24, STAMP_H - 44, 6);
sg.stroke();
sg.fillStyle = color;
sg.textBaseline = 'middle';
// Long codex names have to fit the placard — shrink until they do.
let size = 22;
sg.font = `bold ${size}px 'Courier New', monospace`;
while (sg.measureText(name).width > STAMP_W - 48 && size > 9) {
size -= 1;
sg.font = `bold ${size}px 'Courier New', monospace`;
}
const nw = sg.measureText(name).width;
sg.fillText(name, (STAMP_W - nw) / 2, STAMP_H / 2 - 12);
const qty = `×${count}`;
sg.font = "bold 26px 'Courier New', monospace";
const qw = sg.measureText(qty).width;
sg.fillText(qty, (STAMP_W - qw) / 2, STAMP_H / 2 + 20);
sg.fillStyle = 'rgba(255,255,255,0.5)';
sg.font = "9px 'Courier New', monospace";
sg.fillText('SHIPPED / NO QUESTIONS', 22, 34);
sg.restore();
},
};
}