← back to Rentv Tour
generate-reel.mjs
84 lines
import fs from 'fs';
const W = 1920, H = 1080;
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
// Curated hero montage — silent (the Steve-avatar PIP clip carries the voice).
const SHOTS = [
['01-home.png', 'Live CRE News', 'FRONT END'],
['02-markets.png', 'Markets', 'FRONT END'],
['03-deals.png', 'Deals', 'FRONT END'],
['04-pulse.png', 'Market Pulse', 'FRONT END'],
['05-review.png', 'The REview', 'FRONT END'],
['06-backend.png', 'Admin Backend', 'BACKEND'],
['07-desk.png', 'Broker & Owner Desk', 'BACKEND'],
['08-audience.png', 'Audience & CRM', 'BACKEND'],
['09-press.png', 'Video Library', 'BACKEND'],
];
const clips = [], tweens = [];
let t = 0;
const TITLE = 2.2, OUTRO = 2.7, SHOT = 2.35;
// title
clips.push(` <section id="title" class="clip card" data-start="0" data-duration="${TITLE}" data-track-index="1">
<div class="ci"><div class="tag" id="title-tag">RENTV.COM</div><h1 id="title-h">The unified platform</h1></div></section>`);
tweens.push(`tl.from('#title-h',{y:34,autoAlpha:0,duration:0.6,ease:'power3.out'},0.15);`);
tweens.push(`tl.from('#title-tag',{y:-16,autoAlpha:0,duration:0.5},0.05);`);
t = TITLE;
SHOTS.forEach((s, i) => {
const [file, label, tag] = s;
const st = t + i * SHOT;
const accent = tag === 'BACKEND' ? '#c8a15a' : '#0a6cff';
clips.push(` <div id="s${i}" class="clip shot" data-start="${st.toFixed(3)}" data-duration="${SHOT}" data-track-index="2"><img id="s${i}-img" class="simg" src="assets/${file}" alt="${esc(label)}"></div>`);
clips.push(` <div id="s${i}-l" class="clip label" data-start="${st.toFixed(3)}" data-duration="${SHOT}" data-track-index="3"><div id="s${i}-li" class="li"><span class="num" style="background:${accent}">${tag}</span><span class="txt">${esc(label)}</span></div></div>`);
tweens.push(`tl.from('#s${i}-img',{opacity:0,duration:0.3,ease:'power1.out'},${st.toFixed(3)});`);
tweens.push(`tl.fromTo('#s${i}-img',{scale:1.07},{scale:1.0,duration:${SHOT},ease:'none'},${st.toFixed(3)});`);
tweens.push(`tl.from('#s${i}-li',{y:22,autoAlpha:0,duration:0.35,ease:'power2.out'},${(st + 0.05).toFixed(3)});`);
});
t = TITLE + SHOTS.length * SHOT;
clips.push(` <section id="outro" class="clip card" data-start="${t.toFixed(3)}" data-duration="${OUTRO}" data-track-index="1">
<div class="ci"><h1 id="outro-h">rentv.agentabrams.com</h1><p id="outro-p">One unified app · news + intelligence</p></div></section>`);
tweens.push(`tl.from('#outro-h',{y:30,autoAlpha:0,duration:0.6,ease:'power3.out'},${t.toFixed(3)});`);
tweens.push(`tl.from('#outro-p',{y:18,autoAlpha:0,duration:0.5},${(t + 0.25).toFixed(3)});`);
const TOTAL = +(t + OUTRO).toFixed(2);
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:radial-gradient(120% 120% at 50% 0%,#0d1725 0%,#070b10 60%)}
.shot{overflow:hidden}
.simg{width:100%;height:100%;object-fit:cover;object-position:top center;display:block;will-change:transform,opacity}
.card{display:grid;place-items:center}
.ci{text-align:center;padding:0 120px}
.card .tag{font-size:22px;letter-spacing:.42em;font-weight:600;color:#57b0ff;margin-bottom:20px}
.card h1{margin:0;color:#f3f7fc;font-size:96px;font-weight:500;letter-spacing:-.01em}
.card p{margin:20px 0 0;color:#8ea3b8;font-size:30px;font-weight:300}
.label{left:56px;bottom:64px;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:600;letter-spacing:.08em;padding:8px 13px;border-radius:8px}
.li .txt{color:#fff;font-size:30px;font-weight:500;background:rgba(9,15,24,.9);padding:8px 18px;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>
${clips.join('\n')}
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
${tweens.map(x => ' ' + x).join('\n')}
window.__timelines['main'] = tl;
</script>
</body></html>
`;
fs.writeFileSync('videos/reel/index.html', html);
console.log(`reel: ${TOTAL}s, ${SHOTS.length} shots`);