← back to Rentv Master Reel

master-gen-long.mjs

87 lines

import fs from 'fs';
import { SCENES } from './master-gen.mjs';
const W = 1920, H = 1080;
const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

// full-page pixel heights (from the capture agent)
const HEIGHT = {
  home: 2645, article: 2470, review: 3292, markets: 1497, pulse: 1713, cretalk: 2712,
  blog: 1099, versions: 1084, 'v-wire': 1893, 'v-broadsheet': 2409, 'v-terminal': 1238,
  'v-magazine': 1080, 'v-dashboard': 1080, 'admin-index': 1080, 'admin-command': 1133,
  'admin-kanban': 1080, publish: 1080, desk: 1092, social: 1080,
  'theme-midnight': 2645, 'theme-salmon': 2645,
};
const PAN_SPEED = 33;   // px/sec (lower = slower/longer)
const HOLD = 3.0;       // seconds for short pages
const PAN_CAP = 46;     // max pan-extra seconds per scene

// scaled full-page height when displayed at 1920 wide (captures are 1920 wide already)
function panExtra(cap) {
  const h = HEIGHT[cap] || 1080;
  if (h <= 1140) return HOLD;
  return Math.min((h - H) / PAN_SPEED, PAN_CAP);
}

// per-scene durations from the block audio durations (blockDurs) + pan extra
export function durations(blockDurs) {
  return SCENES.map(([id, cap], i) => +(blockDurs[i] + panExtra(cap)).toFixed(3));
}
export function panExtras() { return SCENES.map(([id, cap]) => +panExtra(cap).toFixed(3)); }

function emit(durs) {
  const clips = [], tw = [];
  let t = 0;
  const TOTAL = +durs.reduce((a, b) => a + b, 0).toFixed(2);
  SCENES.forEach(([id, cap, label], i) => {
    const d = durs[i], st = t.toFixed(3), du = d.toFixed(3);
    const h = HEIGHT[cap] || 1080;
    const travel = Math.max(0, h - H);
    clips.push(`  <div id="s${i}" class="clip shot" data-start="${st}" data-duration="${du}" data-track-index="1"><img id="s${i}i" class="fp" src="media/fullpage/${cap}.png" alt="${esc(label)}"></div>`);
    clips.push(`  <div id="s${i}l" class="clip label" data-start="${st}" data-duration="${du}" data-track-index="2"><div class="li"><span class="num">${String(i + 1).padStart(2, '0')}</span><span class="txt">${esc(label)}</span></div></div>`);
    tw.push(`tl.from('#s${i}i',{opacity:0,duration:0.5,ease:'power1.out'},${st});`);
    if (travel > 40) {
      tw.push(`tl.fromTo('#s${i}i',{y:0},{y:${-travel},duration:${du},ease:'none'},${st});`);
    } else {
      tw.push(`tl.fromTo('#s${i}i',{scale:1.04},{scale:1.0,duration:${du},ease:'none'},${st});`);
    }
    tw.push(`tl.from('#s${i}l',{y:24,autoAlpha:0,duration:0.5,ease:'power2.out'},${(t + 0.2).toFixed(3)});`);
    t += d;
  });
  const html = `<!doctype html><html lang="en"><head>
<meta charset="UTF-8" /><meta name="viewport" content="width=1920, height=1080" />
<title>RENTV — Master Reel</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
  body{margin:0;background:#070b10;font-family:system-ui,-apple-system,"Helvetica Neue",Arial,sans-serif}
  #root{position:relative;width:${W}px;height:${H}px;overflow:hidden}
  .clip{position:absolute;inset:0}
  #bg{background:#0b1017}
  .shot{overflow:hidden}
  .fp{width:${W}px;height:auto;display:block;will-change:transform}
  .label{left:56px;bottom:60px;right:auto;top:auto;width:auto;height:auto}
  .li{display:flex;align-items:center;gap:14px}
  .li .num{color:#fff;font-size:18px;font-weight:700;letter-spacing:.06em;padding:9px 13px;border-radius:8px;background:#0a6cff}
  .li .txt{color:#fff;font-size:30px;font-weight:500;background:rgba(9,15,24,.92);padding:9px 20px;border-radius:10px}
</style></head><body>
  <div id="root" data-composition-id="main" data-start="0" data-width="${W}" data-height="${H}" data-duration="${TOTAL}" data-fps="30">
  <div id="bg" class="clip" data-start="0" data-duration="${TOTAL}" data-track-index="0"></div>
  <audio id="vo" src="media/narration-long.m4a" data-start="0" data-volume="1"></audio>
  <audio id="bgm" src="media/bgm.m4a" data-start="0" data-volume="0.12"></audio>
${clips.join('\n')}
  </div>
  <script>
    window.__timelines = window.__timelines || {};
    const tl = gsap.timeline({ paused: true });
${tw.map(x => '    ' + x).join('\n')}
    window.__timelines['main'] = tl;
  </script>
</body></html>`;
  fs.writeFileSync('index.html', html);
  return TOTAL;
}

const [cmd, a] = process.argv.slice(2);
if (cmd === 'panextras') console.log(JSON.stringify(panExtras()));
else if (cmd === 'durs') console.log(JSON.stringify(durations(JSON.parse(fs.readFileSync(a, 'utf8')))));
else if (cmd === 'emit') console.log('TOTAL=' + emit(JSON.parse(fs.readFileSync(a, 'utf8'))));