/**
* Recipes for the sample clips. Each builds a real, seekable MP4 out of
* ffmpeg's generative sources, so a fresh install has something to play.
*
* Every recipe takes a `variant` number and uses it to pick colours, seeds and
* coordinates, so two videos built from the same recipe never look alike.
*/
export const FONT = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf';
const esc = (text) => text.replace(/[\\:']/g, (m) => `\\${m}`);
const at = (list, variant) => list[variant % list.length];
/** A dimmed title-safe band, so captions stay readable over busy sources. */
const scrim = 'drawbox=x=0:y=ih*0.34:w=iw:h=ih*0.32:[email protected]:t=fill';
function line(text, { size, y, from, hold }) {
return [
`drawtext=fontfile=${FONT}`,
`text='${esc(text)}'`,
`fontsize=${size}`,
'fontcolor=white',
'borderw=2',
'[email protected]',
'x=(w-text_w)/2',
`y=${y}`,
`alpha='if(lt(t,${from}),0,if(lt(t,${from + 0.5}),(t-${from})/0.5,1))'`,
].join(':');
}
/** Title + subtitle, fading up over the band. */
const titleBlock = (title, sub) => [
scrim,
line(title, { size: 46, y: 'h*0.41', from: 0.5, hold: 4 }),
line(sub, { size: 27, y: 'h*0.53', from: 1.1, hold: 3.4 }),
];
const ticker = (text) => [
`drawtext=fontfile=${FONT}`,
`text='${esc(text)}'`,
'fontsize=25',
'fontcolor=white',
"x='w - mod(t*130, w+text_w)'",
'y=h-72',
].join(':');
/* Palettes reused across recipes so the whole catalogue feels related. */
const DUOTONE = [
'colorchannelmixer=rr=0.18:rg=0.62:rb=0.86:gr=0.10:gg=0.34:gb=0.72:br=0.34:bg=0.16:bb=0.52', // deep blue
'colorchannelmixer=rr=0.86:rg=0.52:rb=0.14:gr=0.46:gg=0.30:gb=0.10:br=0.16:bg=0.20:bb=0.34', // amber
'colorchannelmixer=rr=0.20:rg=0.70:rb=0.40:gr=0.14:gg=0.62:gb=0.44:br=0.30:bg=0.36:bb=0.30', // mint
'colorchannelmixer=rr=0.72:rg=0.28:rb=0.62:gr=0.22:gg=0.18:gb=0.48:br=0.60:bg=0.34:bb=0.66', // magenta
];
/** Interesting places to sit in the Mandelbrot set. */
const MANDEL_SPOTS = [
{ x: -0.743643887037151, y: 0.13182590420533, scale: 0.0006 },
{ x: -0.1592, y: -1.0317, scale: 0.0022 },
{ x: 0.001643721971153, y: -0.822467633298876, scale: 0.0009 },
{ x: -1.749277, y: 0.000005, scale: 0.0015 },
{ x: -0.235125, y: 0.827215, scale: 0.0011 },
];
export const RECIPES = {
/** A slow drift through a different corner of the Mandelbrot set each time. */
mandel: (title, sub, v) => {
const spot = at(MANDEL_SPOTS, v);
return [
`mandelbrot=size=1280x720:rate=30:maxiter=260:start_x=${spot.x}:start_y=${spot.y}` +
`:start_scale=${spot.scale}:end_scale=${spot.scale / 2.2}:inner=period:outer=iteration_count`,
`hue=h='${(v * 67) % 360}+t*22':s=1.1`,
...titleBlock(title, sub),
];
},
/** Soft colour fields, like a graded background plate. */
flow: (title, sub, v) => {
const sets = [
{ c: ['#0c2a4d', '#1c6b7a', '#f0a04b', '#3b1f4d'], type: 'linear' },
{ c: ['#2b1b3d', '#7b3f61', '#e4914f', '#12303f'], type: 'radial' },
{ c: ['#08313a', '#2f7d6b', '#c6d97f', '#123a55'], type: 'circular' },
];
const set = at(sets, v);
const colors = set.c.map((hex, i) => `c${i}=${hex}`).join(':');
return [
`gradients=size=1280x720:rate=30:duration=%D:${colors}:n=4:type=${set.type}:speed=0.0${4 + (v % 4)}`,
'gblur=sigma=22',
...titleBlock(title, sub),
];
},
/** Tighter, higher-contrast gradient with a spiral sweep. */
spiral: (title, sub, v) => {
const pairs = [
['#111a2b', '#f5a524'],
['#1a1030', '#3dd9a4'],
['#2a0f18', '#ff5d73'],
];
const [a, b] = at(pairs, v);
return [
`gradients=size=1280x720:rate=30:duration=%D:c0=${a}:c1=${b}:n=2:type=spiral:speed=0.09`,
'gblur=sigma=4',
...titleBlock(title, sub),
];
},
/** Conway's life, scaled up hard so the cells read as pixels. */
life: (title, sub, v) => {
const skins = [
{ life: '#48d1a0', death: '#0d1b2a', ratio: 0.22 },
{ life: '#f5a524', death: '#1a1220', ratio: 0.3 },
{ life: '#7aa2f7', death: '#101426', ratio: 0.18 },
];
const skin = at(skins, v);
return [
`life=size=360x200:rate=30:mold=14:life_color=${skin.life}:death_color=${skin.death}` +
`:ratio=${skin.ratio}:random_seed=${1000 + v * 37}`,
'scale=1280:720:flags=neighbor',
...titleBlock(title, sub),
];
},
/** Sierpinski, alternating between the carpet and the triangle. */
carpet: (title, sub, v) => [
`sierpinski=size=1280x720:rate=30:type=${v % 2 ? 'triangle' : 'carpet'}:seed=${500 + v * 13}:jump=${100 + v * 20}`,
at(DUOTONE, v),
'eq=brightness=0.04:contrast=1.15',
...titleBlock(title, sub),
],
/** One-dimensional cellular automata, scrolling upward. */
auto: (title, sub, v) => {
const rules = [110, 30, 90, 150, 54];
return [
`cellauto=size=1280x720:rate=30:rule=${at(rules, v)}:scroll=1` +
`:random_fill_ratio=0.1${v % 7}:random_seed=${200 + v * 29}`,
at(DUOTONE, v + 1),
...titleBlock(title, sub),
];
},
/** Test pattern pushed through a heavy grade until it reads as footage. */
grade: (title, sub, v) => [
'testsrc2=size=1280x720:rate=30:duration=%D',
'boxblur=22:2',
at(DUOTONE, v + 2),
`hue=h='${(v * 53) % 360}+t*10':s=1.25`,
'eq=saturation=1.2:brightness=-0.04:contrast=1.1',
'vignette=PI/4',
...titleBlock(title, sub),
],
/** A news bulletin: flat backdrop, lower third, scrolling strap. */
bulletin: (title, sub, v) => {
const beds = [
{ bg: '#101826', band: '#c8102e' },
{ bg: '#0d1f1a', band: '#f5a524' },
];
const bed = at(beds, v);
return [
`color=c=${bed.bg}:size=1280x720:rate=30:duration=%D`,
`drawbox=x=0:y=ih-104:w=iw:h=104:color=${bed.band}@0.92:t=fill`,
`drawbox=x=0:y=ih-108:w=iw:h=4:[email protected]:t=fill`,
line(title, { size: 50, y: 'h*0.36', from: 0.4, hold: 5 }),
line(sub, { size: 28, y: 'h*0.48', from: 1.0, hold: 4.4 }),
ticker(sub),
];
},
};
/** Audio beds, so the clips are not silent. */
export const TONES = {
chime: 'sine=frequency=523.25:beep_factor=4:duration=%D,volume=0.12',
hum: 'sine=frequency=110:duration=%D,volume=0.08',
pulse: 'sine=frequency=330:beep_factor=2:duration=%D,volume=0.10',
quiet: 'anullsrc=channel_layout=stereo:sample_rate=44100:duration=%D',
};