patx/youtube-clone

/**
 * 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',
};