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

// 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`);