175 lines
5.7 KiB
JavaScript
175 lines
5.7 KiB
JavaScript
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}`);
|