← back to Rentv Tour

pitch-set.mjs

141 lines

import fs from 'fs';
const W = 1920, H = 1080;
const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

// Each film: id, title, full VO (Steve voice), scenes[]. Scene types:
//   {t:'title', h, sub}  {t:'stat', n, label}  {t:'bul', h, items[]}  {t:'shot', img, label}  {t:'outro', h}
export const FILMS = {
  growth: {
    title: 'Growth Strategy',
    vo: "RENTV's growth plan is simple — own Western U.S. commercial real estate news. Step one: modernize the legacy site into a fast, mobile-first experience readers return to. Step two: grow the newsletter and subscriber base around daily deal coverage. Step three: an integrated social engine that pushes every deal to LinkedIn, X, and Instagram. And step four: search optimization that ranks RENTV above The Real Deal, Bisnow, and GlobeSt. News, audience, distribution, search — the flywheel that compounds.",
    scenes: [
      { t: 'title', h: 'Growth Strategy', sub: 'Own Western U.S. commercial real estate news' },
      { t: 'bul', h: 'The Plan', items: ['Modernize the site — fast & mobile-first', 'Grow the newsletter & subscriber base', 'Integrated social engine (LinkedIn · X · Instagram)', 'Search optimization to own the rank'] },
      { t: 'bul', h: 'Outrank', items: ['The Real Deal', 'Bisnow', 'GlobeSt', 'CoStar News'] },
      { t: 'shot', img: 'home', label: 'The rebuilt front page' },
      { t: 'outro', h: 'The flywheel that compounds' },
    ],
  },
  deepdive: {
    title: 'Sublease Deep-Dive',
    vo: "Sublease dot com is RENTV's biggest opportunity. Commercial sublease is a six hundred fifty billion dollar market — fragmented, opaque, and underserved. RENTV already owns the registry: three hundred eighty-two thousand brokers, thirty-four thousand firms, and a live sublease desk. The play — turn that data into the definitive sublease marketplace for the Western U.S., with a clear path to nearly six million in year-two revenue. The audience is here. The data is here. Sublease is the wedge.",
    scenes: [
      { t: 'title', h: 'Sublease Deep-Dive', sub: "RENTV's biggest opportunity" },
      { t: 'stat', n: '$650B', label: 'U.S. commercial sublease market — fragmented & underserved' },
      { t: 'stat', n: '382K / 34K', label: 'brokers / firms already in the RENTV registry' },
      { t: 'stat', n: '~$5.9M', label: 'projected year-two revenue' },
      { t: 'shot', img: 'desk', label: 'The live Broker & Owner Desk' },
      { t: 'outro', h: 'Sublease is the wedge' },
    ],
  },
  swot: {
    title: 'SWOT Brief',
    vo: "A quick, honest read on RENTV. Strengths — a real audience, decades of deal coverage, and a live intelligence registry competitors don't have. Weaknesses — a dated site and social channels that were underused. Opportunities — the sublease marketplace, integrated social distribution, and conference growth. Threats — well-funded rivals like CoStar, Bisnow, and The Real Deal moving faster. The verdict — RENTV's data moat is the edge. Modernize, distribute, and press it.",
    scenes: [
      { t: 'title', h: 'SWOT Brief', sub: 'An honest read on RENTV' },
      { t: 'bul', h: 'Strengths', items: ['A real, established audience', 'Decades of deal coverage', 'A live intelligence registry rivals lack'] },
      { t: 'bul', h: 'Weaknesses', items: ['A dated legacy site', 'Underused social channels'] },
      { t: 'bul', h: 'Opportunities', items: ['The sublease marketplace', 'Integrated social distribution', 'Conference growth'] },
      { t: 'bul', h: 'Threats', items: ['CoStar', 'Bisnow', 'The Real Deal — moving faster'] },
      { t: 'outro', h: 'Press the data moat' },
    ],
  },
  adplan: {
    title: 'Advertising & Media Plan',
    vo: "How RENTV wins attention. The channels are already connected — LinkedIn, X, and Instagram — feeding every deal and headline into an integrated social engine. Paid amplifies the winners: sponsored deal alerts, conference promotion, and retargeting the newsletter list. The content engine runs on what RENTV already produces — daily deals, market pulse, and The REview video platform. Measure, double down on what converts, and turn readers into subscribers, and subscribers into conference registrations.",
    scenes: [
      { t: 'title', h: 'Advertising & Media Plan', sub: 'How RENTV wins attention' },
      { t: 'bul', h: 'Channels — already connected', items: ['LinkedIn', 'X (Twitter)', 'Instagram'] },
      { t: 'bul', h: 'Paid amplifies the winners', items: ['Sponsored deal alerts', 'Conference promotion', 'Newsletter retargeting'] },
      { t: 'bul', h: 'Content engine (already produced)', items: ['Daily deals', 'Market Pulse', 'The REview video platform'] },
      { t: 'shot', img: 'social', label: 'The social command center' },
      { t: 'outro', h: 'Readers → subscribers → registrations' },
    ],
  },
};

function emit(id, voDur) {
  const film = FILMS[id];
  const TOTAL = +Math.max(voDur + 0.5, 12).toFixed(2);
  const TITLE_D = 2.4, OUTRO_D = 2.9;
  const mids = film.scenes.filter(s => s.t !== 'title' && s.t !== 'outro');
  const midTotal = TOTAL - TITLE_D - OUTRO_D;
  const midD = midTotal / Math.max(mids.length, 1);
  const accent = '#57b0ff';
  const clips = [], tw = [];
  let t = 0, i = 0, mi = 0;
  for (const s of film.scenes) {
    const id2 = 's' + i;
    let dur;
    if (s.t === 'title') dur = TITLE_D;
    else if (s.t === 'outro') dur = OUTRO_D;
    else { dur = (mi === mids.length - 1) ? (TOTAL - OUTRO_D - t) : midD; mi++; }
    const st = t.toFixed(3), du = dur.toFixed(3);
    if (s.t === 'title' || s.t === 'outro') {
      clips.push(`<section id="${id2}" class="clip card" data-start="${st}" data-duration="${du}" data-track-index="1"><div class="ci">${s.t === 'title' ? `<div class="tag" id="${id2}t">RENTV · GROWTH DECK</div>` : ''}<h1 id="${id2}h">${esc(s.h)}</h1>${s.sub ? `<p id="${id2}p">${esc(s.sub)}</p>` : ''}</div></section>`);
      tw.push(`tl.from('#${id2}h',{y:34,autoAlpha:0,duration:0.6,ease:'power3.out'},${(t + 0.15).toFixed(3)});`);
      if (s.sub) tw.push(`tl.from('#${id2}p',{y:20,autoAlpha:0,duration:0.5},${(t + 0.35).toFixed(3)});`);
    } else if (s.t === 'stat') {
      clips.push(`<section id="${id2}" class="clip card" data-start="${st}" data-duration="${du}" data-track-index="1"><div class="ci"><div class="stat" id="${id2}n">${esc(s.n)}</div><p id="${id2}l" class="statl">${esc(s.label)}</p></div></section>`);
      tw.push(`tl.from('#${id2}n',{scale:0.8,autoAlpha:0,duration:0.6,ease:'back.out(1.5)'},${(t + 0.1).toFixed(3)});`);
      tw.push(`tl.from('#${id2}l',{y:18,autoAlpha:0,duration:0.5},${(t + 0.4).toFixed(3)});`);
    } else if (s.t === 'bul') {
      const lis = s.items.map((it, k) => `<li id="${id2}i${k}">${esc(it)}</li>`).join('');
      clips.push(`<section id="${id2}" class="clip card left" data-start="${st}" data-duration="${du}" data-track-index="1"><div class="ci wide"><h2 id="${id2}h" style="color:${accent}">${esc(s.h)}</h2><ul>${lis}</ul></div></section>`);
      tw.push(`tl.from('#${id2}h',{x:-24,autoAlpha:0,duration:0.5,ease:'power3.out'},${(t + 0.1).toFixed(3)});`);
      s.items.forEach((it, k) => tw.push(`tl.from('#${id2}i${k}',{x:-20,autoAlpha:0,duration:0.4},${(t + 0.35 + k * 0.28).toFixed(3)});`));
    } else if (s.t === 'shot') {
      clips.push(`<div id="${id2}" class="clip shot" data-start="${st}" data-duration="${du}" data-track-index="2"><img id="${id2}i" class="simg" src="assets/${s.img}.png" alt="${esc(s.label)}"></div>`);
      clips.push(`<div id="${id2}l" class="clip label" data-start="${st}" data-duration="${du}" data-track-index="3"><div class="li"><span class="txt">${esc(s.label)}</span></div></div>`);
      tw.push(`tl.from('#${id2}i',{opacity:0,duration:0.35},${st});`);
      tw.push(`tl.fromTo('#${id2}i',{scale:1.06},{scale:1.0,duration:${du},ease:'none'},${st});`);
      tw.push(`tl.from('#${id2}l',{y:22,autoAlpha:0,duration:0.4},${(t + 0.1).toFixed(3)});`);
    }
    t += dur; i++;
  }
  const html = `<!doctype html><html lang="en"><head>
<meta charset="UTF-8" /><meta name="viewport" content="width=1920, height=1080" />
<title>RENTV — ${esc(film.title)}</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}.card.left{place-items:center start}
  .ci{text-align:center;padding:0 130px}.ci.wide{text-align:left;padding:0 160px;max-width:1400px}
  .tag{font-size:19px;letter-spacing:.36em;font-weight:600;color:#57b0ff;margin-bottom:18px}
  .card h1{margin:0;color:#f3f7fc;font-size:82px;font-weight:500;letter-spacing:-.01em;line-height:1.05}
  .card p{margin:20px 0 0;color:#8ea3b8;font-size:30px;font-weight:300}
  .stat{color:#fff;font-size:150px;font-weight:700;letter-spacing:-.02em;line-height:1}
  .statl{color:#8ea3b8;font-size:30px;font-weight:300;margin-top:20px;max-width:900px}
  .ci.wide h2{font-size:30px;text-transform:uppercase;letter-spacing:.12em;margin:0 0 26px;font-weight:700}
  .ci.wide ul{margin:0;padding:0;list-style:none}
  .ci.wide li{color:#eef3f8;font-size:44px;font-weight:400;padding:12px 0 12px 40px;position:relative}
  .ci.wide li:before{content:'';position:absolute;left:0;top:26px;width:16px;height:16px;border-radius:4px;background:#57b0ff}
  .label{left:56px;bottom:60px;right:auto;top:auto;width:auto;height:auto}
  .li{display:flex}.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>
  <audio id="vo" src="assets/vo.m4a" data-start="0" data-volume="1"></audio>
${clips.map(c => '  ' + c).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('videos/pitch/index.html', html);
  return TOTAL;
}

const [cmd, a, b] = process.argv.slice(2);
if (cmd === 'ids') console.log(Object.keys(FILMS).join(' '));
else if (cmd === 'vo') process.stdout.write(FILMS[a].vo);
else if (cmd === 'title') process.stdout.write(FILMS[a].title);
else if (cmd === 'emit') console.log('TOTAL=' + emit(a, parseFloat(b)));