← 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, '&').replace(/</g, '<').replace(/>/g, '>');
// 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'))));