/** * THE SCREEN's base content: procedural "clean programming". * Sterile SMPTE-ish test pattern + camcorder overlay, ported from the prototype's * makeBaseTexture and widened to 16:9. This is what the factory's output is painted * ONTO — it must be bland enough that any corruption reads instantly. * * Cost control: the pattern is static, so we only repaint when a visible glyph changes * (timecode ticks ~1/sec, chyron toggles once). Motion comes from the shader's slow * drift, not from re-uploading 640x360 pixels every frame. */ export const BASE_W = 640; export const BASE_H = 360; export interface BaseFeed { canvas: HTMLCanvasElement; /** Repaint if anything visible changed. Returns true when the texture needs re-upload. */ update(timeMs: number, idle: boolean): boolean; } function timecode(timeMs: number): string { // Tape-style running timecode, starting at a suspiciously specific point. const total = Math.floor(timeMs / 1000) + 754; // 0:12:34 const h = Math.floor(total / 3600); const m = Math.floor((total % 3600) / 60); const s = total % 60; return `SP ${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; } export function createBaseFeed(): BaseFeed { const canvas = document.createElement('canvas'); canvas.width = BASE_W; canvas.height = BASE_H; const g = canvas.getContext('2d')!; let lastTC = ''; let lastIdle: boolean | null = null; function paint(tc: string, idle: boolean) { const bars = ['#c0c0c0', '#c0c000', '#00c0c0', '#00c000', '#c000c0', '#c00000', '#0000c0']; const bw = BASE_W / bars.length; bars.forEach((col, i) => { g.fillStyle = col; g.fillRect(i * bw, 0, bw + 1, BASE_H * 0.62); }); // castellations strip const casts = ['#0000c0', '#131313', '#c000c0', '#131313', '#00c0c0', '#131313', '#c0c0c0']; casts.forEach((col, i) => { g.fillStyle = col; g.fillRect(i * bw, BASE_H * 0.62, bw + 1, BASE_H * 0.1); }); // pluge / ramp g.fillStyle = '#101010'; g.fillRect(0, BASE_H * 0.72, BASE_W, BASE_H * 0.28); const ramp = g.createLinearGradient(0, 0, BASE_W, 0); ramp.addColorStop(0, '#000'); ramp.addColorStop(1, '#fff'); g.fillStyle = ramp; g.fillRect(BASE_W * 0.55, BASE_H * 0.8, BASE_W * 0.4, 18); // camcorder overlay g.fillStyle = '#eee'; g.font = "bold 22px 'Courier New', monospace"; g.fillText('PLAY ▶', 20, BASE_H * 0.86); g.fillText(tc, 20, BASE_H * 0.95); g.font = "bold 18px 'Courier New', monospace"; g.fillStyle = '#f4f4f4'; g.fillText('WIMVEE FKTRY', BASE_W * 0.55, BASE_H * 0.95); g.fillStyle = '#ff4444'; g.beginPath(); g.arc(BASE_W - 26, BASE_H * 0.83, 7, 0, Math.PI * 2); g.fill(); // Idle chyron: OSHA-poster deadpan. Disappears forever once the factory ships. if (idle) { g.fillStyle = 'rgba(8,8,12,0.82)'; g.fillRect(0, BASE_H * 0.5, BASE_W, 34); g.fillStyle = '#d8ffd8'; g.font = "bold 20px 'Courier New', monospace"; g.fillText('NOTHING IS WRONG', 18, BASE_H * 0.5 + 24); } } return { canvas, update(timeMs: number, idle: boolean) { const tc = timecode(timeMs); if (tc === lastTC && idle === lastIdle) return false; lastTC = tc; lastIdle = idle; paint(tc, idle); return true; }, }; }