[object Object]

← back to Dw Marketing Reels

reels: 15/30s runtime + open on a product image (no blank IG index thumbnail)

692459d30ba7f1d3b7b725c4ba333c47f78b24c0 · 2026-08-10 11:47:36 -0700 · Steve

Steve 2026-08-10: every reel must be exactly 15s or 30s and its FIRST frame must be
a real product image, never a blank card — Instagram uses frame 0 as the grid/Reels
('index mode') thumbnail, and the old solid-cream intro (text fading in from opacity 0)
rendered a blank cream tile. Now: REEL_TARGET (15|30, default 30) derives PER so total
lands exactly on target; the intro is image-backed (items[0] full-bleed + scrim + title
overlay, image visible at frame 0 with a gentle ken-burns). Verified: 30.0s render, frame0
is a product photo. REEL_TARGET=15 for a 15s cut.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 692459d30ba7f1d3b7b725c4ba333c47f78b24c0
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Aug 10 11:47:36 2026 -0700

    reels: 15/30s runtime + open on a product image (no blank IG index thumbnail)
    
    Steve 2026-08-10: every reel must be exactly 15s or 30s and its FIRST frame must be
    a real product image, never a blank card — Instagram uses frame 0 as the grid/Reels
    ('index mode') thumbnail, and the old solid-cream intro (text fading in from opacity 0)
    rendered a blank cream tile. Now: REEL_TARGET (15|30, default 30) derives PER so total
    lands exactly on target; the intro is image-backed (items[0] full-bleed + scrim + title
    overlay, image visible at frame 0 with a gentle ken-burns). Verified: 30.0s render, frame0
    is a product photo. REEL_TARGET=15 for a 15s cut.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/build-reel.mjs | 35 +++++++++++++++++++++++++++--------
 1 file changed, 27 insertions(+), 8 deletions(-)

diff --git a/scripts/build-reel.mjs b/scripts/build-reel.mjs
index 452e2af..599f456 100644
--- a/scripts/build-reel.mjs
+++ b/scripts/build-reel.mjs
@@ -11,8 +11,11 @@ import { LEAK_DENY, safe } from './leak-deny.mjs';
 const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
 const WS = join(ROOT, 'hf-workspace');
 const N = Number(process.env.REEL_COUNT || 6);          // products per reel
-const PER = Number(process.env.REEL_SECS || 2.4);        // seconds per product
-const INTRO = 2.0, OUTRO = 2.6;
+// HARD runtime spec (Steve 2026-08-10): every reel is exactly 15s or 30s. PER is
+// DERIVED from the target so the total lands on it; INTRO/OUTRO are fixed. Default 30s.
+const TARGET = [15, 30].includes(Number(process.env.REEL_TARGET)) ? Number(process.env.REEL_TARGET) : 30;
+const INTRO = 2.5, OUTRO = 2.5;
+const PER = Math.max(1.5, +(((TARGET - INTRO - OUTRO) / N)).toFixed(3));  // seconds per product
 const SLUG = (process.env.REEL_SLUG || 'new-arrivals').replace(/[^a-z0-9-]+/gi, '-').toLowerCase(); // output filename prefix
 
 // Reel theming — all env-overridable, defaulting to the generic "New Arrivals" reel.
@@ -135,17 +138,30 @@ async function main() {
   .card .sub { font-size:34px; letter-spacing:.16em; text-transform:uppercase; color:#5a544c; }
   .card .rule { width:120px; height:2px; background:var(--gold); margin:44px 0; }
   .card .url { position:absolute; bottom:150px; font-size:30px; letter-spacing:.24em; text-transform:uppercase; color:var(--ink); }
+  /* image-backed intro — the FIRST frame must be a product image, never a blank card,
+     so the Instagram grid / Reels-tab ("index mode") thumbnail is a real product. */
+  .intro-img { background:var(--ink); }
+  .intro-img .introph { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; transform-origin:center; }
+  .intro-img .introscrim { position:absolute; inset:0; background:linear-gradient(180deg,rgba(0,0,0,.42) 0%,rgba(0,0,0,.15) 42%,rgba(0,0,0,.60) 100%); }
+  .intro-img .introtext { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; }
+  .intro-img .kicker { color:#e8c98f; }
+  .intro-img .big { color:#fff; text-shadow:0 2px 24px rgba(0,0,0,.6); margin:26px 60px; }
+  .intro-img .sub { color:#f4efe6; }
 </style>
 </head>
 <body>
   <div id="main-composition" data-composition-id="main-video"
        data-width="1080" data-height="1920" data-start="0" data-duration="${total}">
 
-    <div class="card" id="intro">
-      <div class="kicker">${esc(THEME.kicker)}</div>
-      <div class="big">${esc(THEME.title)}</div>
-      <div class="rule"></div>
-      <div class="sub">${esc(introSub)}</div>
+    <div class="card intro-img" id="intro">
+      <img class="introph" src="${esc(items[0]._local)}" alt="${esc(items[0].pattern)}" />
+      <div class="introscrim"></div>
+      <div class="introtext">
+        <div class="kicker">${esc(THEME.kicker)}</div>
+        <div class="big">${esc(THEME.title)}</div>
+        <div class="rule"></div>
+        <div class="sub">${esc(introSub)}</div>
+      </div>
     </div>
 
     ${slides}
@@ -160,7 +176,10 @@ async function main() {
       const tl = gsap.timeline({ paused: true });
       window.__timelines = window.__timelines || {};
 
-      // intro
+      // intro — the product image is visible at frame 0 (NO opacity:0 on .introph), so the
+      // IG index-mode thumbnail is a real product; a gentle ken-burns + text fade rides on top.
+      gsap.set("#intro .introph", { scale: 1.08 });
+      tl.to("#intro .introph", { scale: 1.0, duration: ${INTRO}, ease: "none" }, 0);
       gsap.set("#intro .big", { y: 40, opacity: 0 });
       gsap.set("#intro .kicker, #intro .sub, #intro .rule", { opacity: 0 });
       tl.to("#intro .kicker", { opacity: 1, duration: 0.5 }, 0.2);

← c2672a4 Fix Majilite flip book Shop CTA -> /collections/all/majilite  ·  back to Dw Marketing Reels  ·  Majilite flip book: real walls+furniture room renders in ver 105bde3 →