import { mkdir, rm } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { spawn } from 'node:child_process'; import { once } from 'node:events'; const root = resolve(import.meta.dirname, '..'); const outputPath = resolve(root, 'media/hdr-landing-prism.png'); const intermediatePath = resolve(root, 'media/.hdr-landing-prism-source.mp4'); const width = 1440; const height = 810; const frame = Buffer.alloc(width * height * 6); function mulberry(seed) { return () => { seed += 0x6d2b79f5; let value = seed; value = Math.imul(value ^ (value >>> 15), value | 1); value ^= value + Math.imul(value ^ (value >>> 7), value | 61); return ((value ^ (value >>> 14)) >>> 0) / 4294967296; }; } const random = mulberry(0x0a0d06); const colors = { cyan: [0.08, 0.98, 1.0], blue: [0.22, 0.42, 1.0], magenta: [1.0, 0.12, 0.92], amber: [1.0, 0.78, 0.18], lime: [0.72, 1.0, 0.1], white: [1.0, 1.0, 1.0] }; function mix(a, b, t) { return [ a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t ]; } function addPoint(x, y, radius, color, strength) { const minX = Math.max(0, Math.floor(x - radius)); const maxX = Math.min(width - 1, Math.ceil(x + radius)); const minY = Math.max(0, Math.floor(y - radius)); const maxY = Math.min(height - 1, Math.ceil(y + radius)); const r2 = radius * radius; for (let py = minY; py <= maxY; py += 1) { const dy = py - y; for (let px = minX; px <= maxX; px += 1) { const dx = px - x; const d2 = dx * dx + dy * dy; if (d2 > r2) continue; const falloff = 1 - d2 / r2; const bloom = falloff * falloff * strength; const offset = (py * width + px) * 6; for (let channel = 0; channel < 3; channel += 1) { const current = frame.readUInt16LE(offset + channel * 2); const value = Math.min(65535, current + Math.round(65535 * color[channel] * bloom)); frame.writeUInt16LE(value, offset + channel * 2); } } } } function addLine(x1, y1, x2, y2, radius, color, strength, steps = 220) { for (let i = 0; i <= steps; i += 1) { const t = i / steps; const x = x1 + (x2 - x1) * t; const y = y1 + (y2 - y1) * t; const taper = Math.sin(Math.PI * t); addPoint(x, y, radius * (0.28 + taper * 0.72), color, strength * (0.18 + taper * 0.82)); } } function addCurve(offsetY, radius, colorA, colorB, strength) { const steps = 420; for (let i = 0; i <= steps; i += 1) { const t = i / steps; const x = width * (0.08 + t * 0.86); const arc = Math.sin(Math.PI * t); const ripple = Math.sin(t * Math.PI * 5.2 + offsetY * 0.018) * 0.035; const y = height * (0.70 - arc * 0.34 + ripple) + offsetY; const color = t < 0.5 ? mix(colorA, colors.white, t * 0.34) : mix(colors.white, colorB, (t - 0.5) * 0.68); addPoint(x, y, radius * (0.22 + arc * 0.78), color, strength * (0.15 + arc * 0.85)); } } frame.fill(0); for (let i = 0; i < 360; i += 1) { const x = random() * width; const y = random() * height; const radius = 0.7 + random() * 1.8; const color = [colors.cyan, colors.blue, colors.magenta, colors.amber, colors.white][Math.floor(random() * 5)]; const strength = 0.05 + random() * 0.16; addPoint(x, y, radius * 2.2, color, strength); } for (let i = 0; i < 140; i += 1) { const t = i / 139; const x = width * (0.18 + t * 0.68); const y = height * (0.51 + Math.sin(t * Math.PI * 2.5) * 0.09 + (random() - 0.5) * 0.05); const color = mix(colors.blue, colors.magenta, t); addPoint(x, y, 4 + random() * 9, color, 0.08 + random() * 0.18); } addCurve(-68, 34, colors.cyan, colors.magenta, 0.26); addCurve(-22, 26, colors.blue, colors.amber, 0.20); addCurve(38, 18, colors.lime, colors.cyan, 0.13); addPoint(width * 0.56, height * 0.43, 108, colors.white, 0.42); addPoint(width * 0.56, height * 0.43, 220, colors.cyan, 0.12); addPoint(width * 0.56, height * 0.43, 320, colors.magenta, 0.07); addLine(width * 0.16, height * 0.76, width * 0.78, height * 0.24, 9, colors.cyan, 0.22, 180); addLine(width * 0.30, height * 0.84, width * 0.88, height * 0.33, 5, colors.amber, 0.14, 160); for (let i = 0; i < 9; i += 1) { const x = width * (0.12 + random() * 0.72); const y = height * (0.20 + random() * 0.52); const color = [colors.cyan, colors.magenta, colors.amber][Math.floor(random() * 3)]; addPoint(x, y, 18 + random() * 38, color, 0.10 + random() * 0.14); } await mkdir(dirname(outputPath), { recursive: true }); const encode = spawn('ffmpeg', [ '-y', '-f', 'rawvideo', '-pix_fmt', 'rgb48le', '-s:v', `${width}x${height}`, '-r', '1', '-i', 'pipe:0', '-an', '-vf', 'format=yuv420p10le', '-c:v', 'libx265', '-profile:v', 'main10', '-preset', 'medium', '-crf', '8', '-tag:v', 'hvc1', '-movflags', '+faststart', '-color_primaries', 'bt2020', '-color_trc', 'smpte2084', '-colorspace', 'bt2020nc', '-x265-params', 'hdr10=1:repeat-headers=1:colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:master-display=G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1):max-cll=1400,260', intermediatePath ], { stdio: ['pipe', 'inherit', 'inherit'] }); encode.stdin.end(frame); const [encodeCode] = await once(encode, 'close'); if (encodeCode !== 0) throw new Error(`ffmpeg intermediate exited with code ${encodeCode}`); const extract = spawn('ffmpeg', [ '-y', '-i', intermediatePath, '-frames:v', '1', '-update', '1', '-c:v', 'png', '-pred', 'mixed', '-color_primaries', 'bt2020', '-color_trc', 'smpte2084', outputPath ], { stdio: ['ignore', 'inherit', 'inherit'] }); const [extractCode] = await once(extract, 'close'); await rm(intermediatePath, { force: true }); if (extractCode !== 0) throw new Error(`ffmpeg png exited with code ${extractCode}`); console.log(`Wrote ${outputPath}`);