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>
109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
/**
|
||
* 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();
|
||
},
|
||
};
|
||
}
|