Spaces:
Configuration error
Configuration error
Dee Ferdinand commited on
Commit ·
a2e4ffd
1
Parent(s): d3cc583
feat: complete rewrite — correct HyperFrames compositions with GSAP + class=clip + window.__timelines
Browse files- Add SKILL.md: HyperFrames production skill with all rules, patterns, vocabulary
- Rewrite composer.js: generates proper HyperFrames HTML (GSAP timelines, clip classes, registered timelines)
- Testimonial: 30s workflow with hook/proof/story/energy/cta/brand structure
- All 5 hard rules enforced: class=clip, paused timeline, no media control, no Math.random, tl.set extend
- GSAP CDN loaded from jsdelivr, all animations use correct easing vocabulary
- Video wrapped in divs (never animate video element directly)
- Lower thirds, stat counter, brand end card patterns
- lib/composer.js +17 -80
- skills/hyperframes-video/SKILL.md +62 -0
- workflows/index.js +402 -78
lib/composer.js
CHANGED
|
@@ -1,107 +1,44 @@
|
|
| 1 |
import fs from 'fs';
|
| 2 |
import path from 'path';
|
| 3 |
|
| 4 |
-
function
|
| 5 |
-
const
|
| 6 |
-
const
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
return mediaList[index % Math.max(mediaList.length, 1)] || null;
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
function captionOverlay(text, startSec, durationSec, style = 'default') {
|
| 13 |
-
const styles = {
|
| 14 |
-
default: 'font-size:42px;font-weight:700;color:#fff;text-shadow:0 2px 8px rgba(0,0,0,0.8);text-align:center;padding:0 40px;',
|
| 15 |
-
badge: 'font-size:22px;font-weight:600;color:#fff;background:rgba(0,0,0,0.6);padding:8px 20px;border-radius:8px;',
|
| 16 |
-
hook: 'font-size:56px;font-weight:900;color:#fff;text-shadow:0 4px 16px rgba(0,0,0,0.9);line-height:1.1;text-align:center;padding:0 32px;',
|
| 17 |
-
stat: 'font-size:68px;font-weight:900;color:#fff;text-shadow:0 4px 20px rgba(0,0,0,0.85);text-align:center;',
|
| 18 |
-
brand: 'font-size:28px;font-weight:500;color:#fff;letter-spacing:0.04em;text-align:center;',
|
| 19 |
-
};
|
| 20 |
-
return `
|
| 21 |
-
<div class="clip caption-overlay"
|
| 22 |
-
data-start="${startSec}"
|
| 23 |
-
data-duration="${durationSec}"
|
| 24 |
-
data-track-index="10"
|
| 25 |
-
style="position:absolute;bottom:140px;left:0;right:0;display:flex;align-items:flex-end;justify-content:center;pointer-events:none;">
|
| 26 |
-
<span style="${styles[style] || styles.default}">${text}</span>
|
| 27 |
-
</div>`;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
function clipBlock(srcRel, startSec, durationSec, trackIdx) {
|
| 31 |
-
return `
|
| 32 |
-
<video class="clip" data-start="${startSec}" data-duration="${durationSec}" data-track-index="${trackIdx}"
|
| 33 |
-
src="${srcRel}" muted playsinline style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></video>`;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
function imageBlock(srcRel, startSec, durationSec, trackIdx) {
|
| 37 |
-
return `
|
| 38 |
-
<img class="clip" data-start="${startSec}" data-duration="${durationSec}" data-track-index="${trackIdx}"
|
| 39 |
-
src="${srcRel}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;">`;
|
| 40 |
}
|
| 41 |
|
| 42 |
export async function buildComposition(compDir, projectDir, musicPath, config, workflow, onProgress) {
|
| 43 |
const files = config._files || [];
|
| 44 |
-
const { trainerName
|
|
|
|
| 45 |
|
| 46 |
const assetsDir = path.join(compDir, 'assets');
|
| 47 |
fs.mkdirSync(assetsDir, { recursive: true });
|
| 48 |
|
|
|
|
| 49 |
const linkedMedia = [];
|
| 50 |
for (const f of files) {
|
| 51 |
const dest = path.join(assetsDir, path.basename(f.path));
|
| 52 |
if (!fs.existsSync(dest)) fs.copyFileSync(f.path, dest);
|
| 53 |
-
linkedMedia.push({ ...f,
|
| 54 |
}
|
| 55 |
|
| 56 |
-
|
|
|
|
| 57 |
if (!fs.existsSync(musicDest)) fs.copyFileSync(musicPath, musicDest);
|
| 58 |
-
const musicRel = `assets/${path.basename(musicPath)}`;
|
| 59 |
|
| 60 |
onProgress?.(0.2);
|
| 61 |
-
const cuts = workflow.generateCuts(linkedMedia, config);
|
| 62 |
-
onProgress?.(0.5);
|
| 63 |
|
| 64 |
const [w, h] = format === '9:16' ? [1080, 1920] : format === '1:1' ? [1080, 1080] : [1920, 1080];
|
|
|
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
for (const cut of cuts) {
|
| 71 |
-
if (cut.mediaFile) {
|
| 72 |
-
if (cut.mediaFile.mime?.startsWith('video/')) clipBlocks += clipBlock(cut.mediaFile.rel, cut.start, cut.duration, trackIdx++);
|
| 73 |
-
else clipBlocks += imageBlock(cut.mediaFile.rel, cut.start, cut.duration, trackIdx++);
|
| 74 |
-
}
|
| 75 |
-
if (cut.caption) captionBlocks += captionOverlay(cut.caption, cut.start + 0.3, cut.duration - 0.5, cut.captionStyle || 'default');
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
const html = `<!DOCTYPE html>
|
| 79 |
-
<html><head><meta charset="utf-8">
|
| 80 |
-
<style>
|
| 81 |
-
* { margin:0;padding:0;box-sizing:border-box; }
|
| 82 |
-
body { background:#000;overflow:hidden; }
|
| 83 |
-
#stage { position:relative;width:${w}px;height:${h}px;overflow:hidden;background:#000; }
|
| 84 |
-
.caption-overlay { animation:fadeIn 0.3s ease-in; }
|
| 85 |
-
@keyframes fadeIn { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)} }
|
| 86 |
-
</style></head><body>
|
| 87 |
-
<div id="stage" data-composition-id="dee-video" data-start="0" data-width="${w}" data-height="${h}">
|
| 88 |
-
${clipBlocks}
|
| 89 |
-
<audio data-start="0" data-duration="${totalDur}" data-track-index="20" data-volume="${config.musicVolume || 0.3}" src="${musicRel}"></audio>
|
| 90 |
-
${captionBlocks}
|
| 91 |
-
<div class="clip" data-start="0" data-duration="999" data-track-index="11"
|
| 92 |
-
style="position:absolute;bottom:0;left:0;right:0;padding:12px 24px;background:linear-gradient(transparent,rgba(0,0,0,0.65));">
|
| 93 |
-
<span style="font-size:18px;font-weight:700;color:#fff;">${trainerName || 'Dee Ferdinand'}</span>
|
| 94 |
-
<span style="font-size:14px;color:rgba(255,255,255,0.7);margin-left:8px;">· ${tagline || 'AI Corporate Trainer'}</span>
|
| 95 |
-
</div>
|
| 96 |
-
<div class="clip" data-start="${totalDur - 5}" data-duration="5" data-track-index="30"
|
| 97 |
-
style="position:absolute;inset:0;background:rgba(0,0,0,0.88);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;">
|
| 98 |
-
<div style="font-size:44px;font-weight:800;color:#fff;text-align:center;">${trainerName || 'Dee Ferdinand'}</div>
|
| 99 |
-
<div style="font-size:22px;color:rgba(255,255,255,0.8);text-align:center;">${tagline || 'AI Corporate Trainer'}</div>
|
| 100 |
-
<div style="font-size:18px;color:rgba(255,255,255,0.55);margin-top:6px;">${website || 'deeferdinand.com'}</div>
|
| 101 |
-
${clientName ? `<div style="font-size:14px;color:rgba(255,255,255,0.4);margin-top:4px;">${clientName} · ${new Date().getFullYear()}</div>` : ''}
|
| 102 |
-
</div>
|
| 103 |
-
</div></body></html>`;
|
| 104 |
|
|
|
|
| 105 |
fs.writeFileSync(path.join(compDir, 'index.html'), html);
|
| 106 |
onProgress?.(1.0);
|
| 107 |
}
|
|
|
|
| 1 |
import fs from 'fs';
|
| 2 |
import path from 'path';
|
| 3 |
|
| 4 |
+
function pick(media, preferVideo, idx) {
|
| 5 |
+
const v = media.filter(m => m.mime?.startsWith('video/'));
|
| 6 |
+
const i = media.filter(m => m.mime?.startsWith('image/'));
|
| 7 |
+
const pool = preferVideo ? (v.length ? v : i) : (i.length ? i : v);
|
| 8 |
+
return pool[idx % Math.max(pool.length, 1)] || media[idx % Math.max(media.length, 1)] || null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
export async function buildComposition(compDir, projectDir, musicPath, config, workflow, onProgress) {
|
| 12 |
const files = config._files || [];
|
| 13 |
+
const { trainerName = 'Dee Ferdinand', tagline = 'AI Corporate Trainer',
|
| 14 |
+
website = 'deeferdinand.com', clientName = '', format = '9:16', duration: totalDur = 30 } = config;
|
| 15 |
|
| 16 |
const assetsDir = path.join(compDir, 'assets');
|
| 17 |
fs.mkdirSync(assetsDir, { recursive: true });
|
| 18 |
|
| 19 |
+
// Copy uploaded files into assets/
|
| 20 |
const linkedMedia = [];
|
| 21 |
for (const f of files) {
|
| 22 |
const dest = path.join(assetsDir, path.basename(f.path));
|
| 23 |
if (!fs.existsSync(dest)) fs.copyFileSync(f.path, dest);
|
| 24 |
+
linkedMedia.push({ ...f, rel: `./assets/${path.basename(f.path)}` });
|
| 25 |
}
|
| 26 |
|
| 27 |
+
// Copy music
|
| 28 |
+
const musicDest = path.join(assetsDir, 'music.mp3');
|
| 29 |
if (!fs.existsSync(musicDest)) fs.copyFileSync(musicPath, musicDest);
|
|
|
|
| 30 |
|
| 31 |
onProgress?.(0.2);
|
|
|
|
|
|
|
| 32 |
|
| 33 |
const [w, h] = format === '9:16' ? [1080, 1920] : format === '1:1' ? [1080, 1080] : [1920, 1080];
|
| 34 |
+
const compId = `dee-${config.workflow}-${Date.now()}`;
|
| 35 |
|
| 36 |
+
// Write meta.json (HyperFrames reads this)
|
| 37 |
+
fs.writeFileSync(path.join(compDir, 'meta.json'), JSON.stringify(
|
| 38 |
+
{ name: compId, id: compId, duration: totalDur, width: w, height: h, fps: 30 }, null, 2
|
| 39 |
+
));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
const html = workflow.buildHTML(linkedMedia, config, compId, w, h, totalDur);
|
| 42 |
fs.writeFileSync(path.join(compDir, 'index.html'), html);
|
| 43 |
onProgress?.(1.0);
|
| 44 |
}
|
skills/hyperframes-video/SKILL.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HyperFrames Video Production Skill
|
| 2 |
+
# For: Dee Ferdinand AI Corporate Training videos
|
| 3 |
+
|
| 4 |
+
## THE 5 HARD RULES (breaking any = broken render)
|
| 5 |
+
|
| 6 |
+
1. Every timed element MUST have `class="clip"` + `data-start` + `data-duration` + `data-track-index`
|
| 7 |
+
2. GSAP timeline MUST be `{ paused: true }` AND registered: `window.__timelines["COMPOSITION_ID"] = tl`
|
| 8 |
+
- Key MUST exactly match `data-composition-id` on root div
|
| 9 |
+
3. NEVER call `.play()` `.pause()` or set `.currentTime` on video/audio in scripts
|
| 10 |
+
4. NEVER use `Math.random()` — use seeded PRNG for reproducibility
|
| 11 |
+
5. Always extend: `tl.set({}, {}, TOTAL_DURATION)` — or render cuts off at last tween
|
| 12 |
+
|
| 13 |
+
## CORRECT MINIMAL COMPOSITION
|
| 14 |
+
|
| 15 |
+
```html
|
| 16 |
+
<div id="root" data-composition-id="my-video" data-start="0" data-width="1080" data-height="1920">
|
| 17 |
+
<!-- Timed element: MUST have class="clip" -->
|
| 18 |
+
<div id="scene-1" class="clip" data-start="0" data-duration="5" data-track-index="0"
|
| 19 |
+
style="position:absolute;inset:0;background:#000;">
|
| 20 |
+
<!-- Wrap video, never animate video element directly -->
|
| 21 |
+
<div id="vid-wrap" style="position:absolute;inset:0;">
|
| 22 |
+
<video data-start="0" data-duration="5" data-track-index="1" data-volume="0.8"
|
| 23 |
+
src="./assets/clip.mp4" muted playsinline
|
| 24 |
+
style="width:100%;height:100%;object-fit:cover;"></video>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
<div id="cap" class="clip" data-start="0.3" data-duration="4.5" data-track-index="2"
|
| 28 |
+
style="position:absolute;bottom:300px;left:0;right:0;text-align:center;
|
| 29 |
+
font-size:72px;font-weight:900;color:#fff;">
|
| 30 |
+
Caption text here
|
| 31 |
+
</div>
|
| 32 |
+
<audio data-start="0" data-duration="30" data-track-index="20" data-volume="0.25"
|
| 33 |
+
src="./assets/music.mp3"></audio>
|
| 34 |
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
| 35 |
+
<script>
|
| 36 |
+
const tl = gsap.timeline({ paused: true });
|
| 37 |
+
tl.from("#cap", { y: 40, opacity: 0, duration: 0.4, ease: "power3.out" }, 0.3);
|
| 38 |
+
tl.set({}, {}, 30); // CRITICAL: extend to full duration
|
| 39 |
+
window.__timelines = window.__timelines || {};
|
| 40 |
+
window.__timelines["my-video"] = tl; // CRITICAL: key = data-composition-id
|
| 41 |
+
</script>
|
| 42 |
+
</div>
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
## MOTION VOCABULARY
|
| 46 |
+
snappily = power4.out 0.2s | smoothly = power2.out 0.4s | dramatically = expo.out 0.6s
|
| 47 |
+
bouncy = back.out(1.7) | stat pop = back.out(2) scale from 0.75 | dreamy = sine.inOut
|
| 48 |
+
|
| 49 |
+
## 30s TESTIMONIAL STRUCTURE (9:16)
|
| 50 |
+
0.0–4.0s HOOK photo + big stacked caption, slow Ken Burns zoom
|
| 51 |
+
4.0–8.0s PROOF stat counter scale-pop + lower third client badge
|
| 52 |
+
8.0–16.0s STORY testimonial video (real footage, caption below)
|
| 53 |
+
16.0–23.0s ENERGY 3 fast photo cuts (2-2.5s each), kinetic captions
|
| 54 |
+
23.0–28.0s CTA dark overlay photo, CTA caption stack
|
| 55 |
+
28.0–30.0s BRAND dark gradient end card, name + tagline + website
|
| 56 |
+
|
| 57 |
+
## COMMON BUGS
|
| 58 |
+
- Missing class="clip" → element always visible, overlaps everything
|
| 59 |
+
- window.__timelines key mismatch → static, no animation
|
| 60 |
+
- Animate video directly → frames stop rendering
|
| 61 |
+
- No tl.set at end → render stops early
|
| 62 |
+
- audio.play() in script → framework sync breaks
|
workflows/index.js
CHANGED
|
@@ -1,92 +1,416 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
}
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
const testimonial = {
|
| 9 |
-
meta: {
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
{ start: 0, duration: 3, mediaFile: pick(media, true, 0), caption: clientName ? `${clientName} × AI Training` : 'AI Corporate Training', captionStyle: 'hook' },
|
| 14 |
-
{ start: 3, duration: 2, mediaFile: pick(media, false, 0), caption: `${trainerName} · AI Corporate Trainer`, captionStyle: 'badge' },
|
| 15 |
-
{ start: 5, duration: 5, mediaFile: pick(media, false, 1), caption: '1,000+ professionals trained since 2023', captionStyle: 'stat' },
|
| 16 |
-
{ start: 10, duration: 5, mediaFile: pick(media, true, 1), caption: '80% hands-on · langsung praktek', captionStyle: 'default' },
|
| 17 |
-
{ start: 15, duration: 8, mediaFile: pick(media, true, 2), caption: '"Saya pikir AI itu susah. Ternyata..."', captionStyle: 'default' },
|
| 18 |
-
{ start: 23, duration: 4, mediaFile: pick(media, false, 2), caption: clientName ? `${clientName} · ${new Date().getFullYear()}` : null, captionStyle: 'badge' },
|
| 19 |
-
{ start: 27, duration: 5, mediaFile: pick(media, true, 3), caption: 'Hands-on prompting. Real output.', captionStyle: 'default' },
|
| 20 |
-
{ start: 32, duration: 4, mediaFile: pick(media, false, 3), caption: null },
|
| 21 |
-
{ start: 36, duration: 7, mediaFile: pick(media, true, 4), caption: '"Sekarang saya bisa buat konten sendiri."', captionStyle: 'default' },
|
| 22 |
-
{ start: 43, duration: 4, mediaFile: pick(media, false, 4), caption: null },
|
| 23 |
-
{ start: 47, duration: 8, mediaFile: pick(media, true, 5), caption: 'Energi yang berbeda. Hasil yang nyata.', captionStyle: 'default' },
|
| 24 |
-
{ start: 55, duration: 8, mediaFile: pick(media, true, 0), caption: 'Training AI untuk tim kamu?', captionStyle: 'hook' },
|
| 25 |
-
{ start: 63, duration: 7, mediaFile: pick(media, false, 5), caption: 'DM · atau klik link di bio', captionStyle: 'default' },
|
| 26 |
-
];
|
| 27 |
},
|
| 28 |
-
};
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
const
|
| 34 |
-
const
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
},
|
| 48 |
};
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
{ start: 0, duration: 5, mediaFile: pick(m,true,0), caption: 'Ini bukan teori.', captionStyle: 'hook' },
|
| 57 |
-
{ start: 5, duration: 4, mediaFile: pick(m,false,0), caption: 'Ini yang benar-benar terjadi.', captionStyle: 'default' },
|
| 58 |
-
{ start: 9, duration: 4, mediaFile: pick(m,true,1), caption: clientName || null, captionStyle: 'badge' },
|
| 59 |
-
{ start: 13, duration: 4, mediaFile: pick(m,false,1), caption: null },
|
| 60 |
-
{ start: 17, duration: 5, mediaFile: pick(m,true,2), caption: 'Prompt. Praktek. Hasil.', captionStyle: 'default' },
|
| 61 |
-
{ start: 22, duration: 4, mediaFile: pick(m,false,2), caption: null },
|
| 62 |
-
{ start: 26, duration: 5, mediaFile: pick(m,true,3), caption: '"Saya tidak sangka bisa."', captionStyle: 'default' },
|
| 63 |
-
{ start: 31, duration: 4, mediaFile: pick(m,false,3), caption: null },
|
| 64 |
-
{ start: 35, duration: 5, mediaFile: pick(m,true,0), caption: '1,000+ professionals trained', captionStyle: 'stat' },
|
| 65 |
-
{ start: 40, duration: 5, mediaFile: pick(m,true,4), caption: null },
|
| 66 |
-
{ start: 45, duration: 5, mediaFile: pick(m,false,0), caption: `${trainerName} · AI Corporate Trainer`, captionStyle: 'brand' },
|
| 67 |
-
{ start: 50, duration: 5, mediaFile: pick(m,true,1), caption: 'Training AI untuk tim kamu?', captionStyle: 'hook' },
|
| 68 |
-
];
|
| 69 |
},
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
},
|
| 90 |
};
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
export const WORKFLOWS = { testimonial, teaser, trailer, community };
|
|
|
|
| 1 |
+
// workflows/index.js
|
| 2 |
+
// All workflows produce correct HyperFrames HTML:
|
| 3 |
+
// - Every timed element has class="clip" + data-start + data-duration + data-track-index
|
| 4 |
+
// - GSAP timeline is paused: true and registered on window.__timelines[compId]
|
| 5 |
+
// - Video elements wrapped in divs, never animated directly
|
| 6 |
+
// - tl.set({},{},DURATION) extends timeline to full length
|
| 7 |
+
|
| 8 |
+
function assetRef(media, idx) {
|
| 9 |
+
if (!media || !media.length) return './assets/placeholder.jpg';
|
| 10 |
+
return media[idx % media.length]?.rel || './assets/placeholder.jpg';
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
function videoOrImg(m) {
|
| 14 |
+
if (!m) return null;
|
| 15 |
+
return m.mime?.startsWith('video/') ? 'video' : 'img';
|
| 16 |
}
|
| 17 |
|
| 18 |
+
function mediaEl(m, start, duration, trackIdx, volume = 0) {
|
| 19 |
+
if (!m) return '';
|
| 20 |
+
if (m.mime?.startsWith('video/')) {
|
| 21 |
+
return `<div style="position:absolute;inset:0">
|
| 22 |
+
<video data-start="${start}" data-duration="${duration}" data-track-index="${trackIdx}"
|
| 23 |
+
data-volume="${volume}" src="${m.rel}" muted playsinline
|
| 24 |
+
style="width:100%;height:100%;object-fit:cover;"></video>
|
| 25 |
+
</div>`;
|
| 26 |
+
}
|
| 27 |
+
return `<img src="${m.rel}" alt="" style="width:100%;height:100%;object-fit:cover;">`;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// ── TESTIMONIAL (30s · 9:16) ─────────────────────────────────────────────────
|
| 31 |
const testimonial = {
|
| 32 |
+
meta: {
|
| 33 |
+
name: 'Corporate Testimonial', icon: '🏢',
|
| 34 |
+
description: '30s social proof video. 9:16 vertical.',
|
| 35 |
+
format: '9:16', duration: 30, music: 'corporate',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
},
|
|
|
|
| 37 |
|
| 38 |
+
buildHTML(media, config, compId, w, h, dur) {
|
| 39 |
+
const { clientName = 'Training Session', trainerName = 'Dee Ferdinand',
|
| 40 |
+
tagline = 'AI Corporate Trainer', website = 'deeferdinand.com' } = config;
|
| 41 |
+
const videos = media.filter(m => m.mime?.startsWith('video/'));
|
| 42 |
+
const images = media.filter(m => m.mime?.startsWith('image/'));
|
| 43 |
+
const allMedia = media;
|
| 44 |
+
|
| 45 |
+
const m0 = allMedia[0] || null;
|
| 46 |
+
const m1 = allMedia[1] || m0;
|
| 47 |
+
const m2 = allMedia[2] || m0;
|
| 48 |
+
const m3 = allMedia[3] || m1;
|
| 49 |
+
const m4 = allMedia[4] || m2;
|
| 50 |
+
const mVid = videos[0] || null; // testimonial video if available
|
| 51 |
+
|
| 52 |
+
const clientLabel = clientName || 'AI Training';
|
| 53 |
+
const year = new Date().getFullYear();
|
| 54 |
+
|
| 55 |
+
return `<!DOCTYPE html>
|
| 56 |
+
<html><head><meta charset="utf-8">
|
| 57 |
+
<style>
|
| 58 |
+
* { margin:0;padding:0;box-sizing:border-box; }
|
| 59 |
+
body { background:#000;overflow:hidden; }
|
| 60 |
+
#root { position:relative;width:${w}px;height:${h}px;overflow:hidden;background:#000; }
|
| 61 |
+
.scene { position:absolute;inset:0; }
|
| 62 |
+
.overlay-dark { position:absolute;inset:0;background:rgba(0,0,0,0.38); }
|
| 63 |
+
.overlay-gradient-bottom { position:absolute;bottom:0;left:0;right:0;height:420px;
|
| 64 |
+
background:linear-gradient(transparent,rgba(0,0,0,0.75)); }
|
| 65 |
+
#brand-bar { position:absolute;bottom:0;left:0;right:0;padding:18px 48px 30px;
|
| 66 |
+
background:linear-gradient(transparent,rgba(0,0,0,0.72));z-index:100;
|
| 67 |
+
display:flex;align-items:center;gap:10px; }
|
| 68 |
+
</style></head><body>
|
| 69 |
+
<div id="root" data-composition-id="${compId}" data-start="0" data-width="${w}" data-height="${h}">
|
| 70 |
+
|
| 71 |
+
<!-- ─── SCENE 1: HOOK 0–4s ─── -->
|
| 72 |
+
<div id="s1" class="clip scene" data-start="0" data-duration="4" data-track-index="0">
|
| 73 |
+
${m0 ? mediaEl(m0, 0, 4, 1, 0) : ''}
|
| 74 |
+
<div class="overlay-dark"></div>
|
| 75 |
+
</div>
|
| 76 |
+
<div id="cap-h1" class="clip" data-start="0.4" data-duration="3.3" data-track-index="2"
|
| 77 |
+
style="position:absolute;bottom:560px;left:0;right:0;padding:0 48px;
|
| 78 |
+
text-align:center;font-size:88px;font-weight:900;color:#fff;
|
| 79 |
+
font-family:-apple-system,'Inter',sans-serif;line-height:1.1;
|
| 80 |
+
text-shadow:0 4px 20px rgba(0,0,0,0.9);">
|
| 81 |
+
Ini yang terjadi
|
| 82 |
+
</div>
|
| 83 |
+
<div id="cap-h2" class="clip" data-start="0.7" data-duration="3.0" data-track-index="3"
|
| 84 |
+
style="position:absolute;bottom:440px;left:0;right:0;padding:0 48px;
|
| 85 |
+
text-align:center;font-size:58px;font-weight:700;
|
| 86 |
+
color:rgba(255,255,255,0.85);
|
| 87 |
+
font-family:-apple-system,'Inter',sans-serif;
|
| 88 |
+
text-shadow:0 3px 14px rgba(0,0,0,0.85);">
|
| 89 |
+
ketika tim belajar AI
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<!-- ─── SCENE 2: PROOF 4–8s ─── -->
|
| 93 |
+
<div id="s2" class="clip scene" data-start="4" data-duration="4" data-track-index="4">
|
| 94 |
+
${m1 ? mediaEl(m1, 4, 4, 5, 0) : '<div style="position:absolute;inset:0;background:#111;"></div>'}
|
| 95 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.5);"></div>
|
| 96 |
+
</div>
|
| 97 |
+
<div id="cap-stat" class="clip" data-start="4.4" data-duration="3.3" data-track-index="6"
|
| 98 |
+
style="position:absolute;top:50%;left:0;right:0;transform:translateY(-50%);
|
| 99 |
+
text-align:center;font-family:-apple-system,'Inter',sans-serif;color:#fff;">
|
| 100 |
+
<div style="font-size:104px;font-weight:900;letter-spacing:-2px;line-height:1;
|
| 101 |
+
text-shadow:0 4px 24px rgba(0,0,0,0.9);">1,000+</div>
|
| 102 |
+
<div style="font-size:38px;font-weight:500;opacity:0.85;margin-top:14px;">profesional terlatih</div>
|
| 103 |
+
<div style="font-size:26px;color:rgba(255,255,255,0.6);margin-top:8px;">sejak 2023</div>
|
| 104 |
+
</div>
|
| 105 |
+
<div id="lt" class="clip" data-start="4.7" data-duration="2.9" data-track-index="7"
|
| 106 |
+
style="position:absolute;bottom:160px;left:0;padding:0 0 0 48px;">
|
| 107 |
+
<div style="background:rgba(0,0,0,0.75);backdrop-filter:blur(10px);
|
| 108 |
+
border-left:5px solid #7C6FE0;padding:14px 24px;
|
| 109 |
+
border-radius:0 10px 10px 0;display:inline-block;">
|
| 110 |
+
<div style="font-size:30px;font-weight:700;color:#fff;">${clientLabel}</div>
|
| 111 |
+
<div style="font-size:19px;color:rgba(255,255,255,0.7);">AI Training · ${year}</div>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<!-- ─── SCENE 3: TESTIMONIAL 8–16s ─── -->
|
| 116 |
+
<div id="s3" class="clip scene" data-start="8" data-duration="8" data-track-index="8">
|
| 117 |
+
${mVid
|
| 118 |
+
? `<div id="s3vw" style="position:absolute;inset:0;">
|
| 119 |
+
<video data-start="8" data-duration="8" data-track-index="9" data-volume="0.85"
|
| 120 |
+
src="${mVid.rel}" muted playsinline
|
| 121 |
+
style="width:100%;height:100%;object-fit:cover;"></video>
|
| 122 |
+
</div>`
|
| 123 |
+
: `${mediaEl(m2 || m0, 8, 8, 9, 0)}<div class="overlay-dark"></div>`
|
| 124 |
+
}
|
| 125 |
+
<div class="overlay-gradient-bottom"></div>
|
| 126 |
+
</div>
|
| 127 |
+
<div id="cap-testi" class="clip" data-start="8.5" data-duration="7.2" data-track-index="10"
|
| 128 |
+
style="position:absolute;bottom:220px;left:0;right:0;padding:0 48px;
|
| 129 |
+
text-align:center;font-size:52px;font-weight:700;color:#fff;
|
| 130 |
+
font-family:-apple-system,'Inter',sans-serif;line-height:1.3;
|
| 131 |
+
text-shadow:0 3px 16px rgba(0,0,0,0.9);">
|
| 132 |
+
“Saya pikir AI itu susah.<br>Ternyata langsung bisa!”
|
| 133 |
+
</div>
|
| 134 |
+
|
| 135 |
+
<!-- ─── SCENE 4: ENERGY B-ROLL 16–23s (3 fast cuts) ─── -->
|
| 136 |
+
<div id="s4a" class="clip scene" data-start="16" data-duration="2.5" data-track-index="11">
|
| 137 |
+
${mediaEl(m0 || m2, 16, 2.5, 12, 0)}
|
| 138 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.28);"></div>
|
| 139 |
+
</div>
|
| 140 |
+
<div id="cap-e1" class="clip" data-start="16.3" data-duration="2.0" data-track-index="13"
|
| 141 |
+
style="position:absolute;bottom:280px;left:0;right:0;text-align:center;
|
| 142 |
+
font-size:72px;font-weight:900;color:#fff;
|
| 143 |
+
font-family:-apple-system,'Inter',sans-serif;
|
| 144 |
+
text-shadow:0 4px 16px rgba(0,0,0,0.9);">80% hands-on</div>
|
| 145 |
+
|
| 146 |
+
<div id="s4b" class="clip scene" data-start="18.5" data-duration="2.5" data-track-index="14">
|
| 147 |
+
${mediaEl(m3 || m1, 18.5, 2.5, 15, 0)}
|
| 148 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.28);"></div>
|
| 149 |
+
</div>
|
| 150 |
+
<div id="cap-e2" class="clip" data-start="18.8" data-duration="2.0" data-track-index="16"
|
| 151 |
+
style="position:absolute;bottom:280px;left:0;right:0;text-align:center;
|
| 152 |
+
font-size:72px;font-weight:900;color:#fff;
|
| 153 |
+
font-family:-apple-system,'Inter',sans-serif;
|
| 154 |
+
text-shadow:0 4px 16px rgba(0,0,0,0.9);">Langsung praktek</div>
|
| 155 |
+
|
| 156 |
+
<div id="s4c" class="clip scene" data-start="21" data-duration="2" data-track-index="17">
|
| 157 |
+
${mediaEl(m4 || m2, 21, 2, 18, 0)}
|
| 158 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.28);"></div>
|
| 159 |
+
</div>
|
| 160 |
+
<div id="cap-e3" class="clip" data-start="21.3" data-duration="1.5" data-track-index="19"
|
| 161 |
+
style="position:absolute;bottom:280px;left:0;right:0;text-align:center;
|
| 162 |
+
font-size:72px;font-weight:900;color:#9EF0C8;
|
| 163 |
+
font-family:-apple-system,'Inter',sans-serif;
|
| 164 |
+
text-shadow:0 4px 16px rgba(0,0,0,0.9);">Real output ✓</div>
|
| 165 |
+
|
| 166 |
+
<!-- ─── SCENE 5: CTA 23–28s ─── -->
|
| 167 |
+
<div id="s5" class="clip scene" data-start="23" data-duration="5" data-track-index="20">
|
| 168 |
+
${mediaEl(m0, 23, 5, 21, 0)}
|
| 169 |
+
<div style="position:absolute;inset:0;background:rgba(8,4,24,0.62);"></div>
|
| 170 |
+
</div>
|
| 171 |
+
<div id="cap-cta1" class="clip" data-start="23.4" data-duration="4.3" data-track-index="22"
|
| 172 |
+
style="position:absolute;top:36%;left:0;right:0;padding:0 48px;
|
| 173 |
+
text-align:center;font-size:76px;font-weight:900;color:#fff;
|
| 174 |
+
font-family:-apple-system,'Inter',sans-serif;line-height:1.15;
|
| 175 |
+
text-shadow:0 4px 20px rgba(0,0,0,0.9);">
|
| 176 |
+
Training AI<br>untuk tim kamu?
|
| 177 |
+
</div>
|
| 178 |
+
<div id="cap-cta2" class="clip" data-start="25" data-duration="2.7" data-track-index="23"
|
| 179 |
+
style="position:absolute;top:58%;left:0;right:0;padding:0 48px;
|
| 180 |
+
text-align:center;font-size:38px;font-weight:500;
|
| 181 |
+
color:rgba(255,255,255,0.75);
|
| 182 |
+
font-family:-apple-system,'Inter',sans-serif;">
|
| 183 |
+
DM · atau klik link di bio
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<!-- ─── SCENE 6: BRAND END CARD 28–30s ─── -->
|
| 187 |
+
<div id="s6" class="clip scene" data-start="28" data-duration="2" data-track-index="24"
|
| 188 |
+
style="background:linear-gradient(135deg,#0d0d1f 0%,#1a1040 50%,#0d0d1f 100%);">
|
| 189 |
+
<div style="position:absolute;inset:0;
|
| 190 |
+
background:radial-gradient(ellipse at center,rgba(124,111,224,0.22) 0%,transparent 70%);"></div>
|
| 191 |
+
</div>
|
| 192 |
+
<div id="end-name" class="clip" data-start="28.2" data-duration="1.8" data-track-index="25"
|
| 193 |
+
style="position:absolute;top:44%;left:0;right:0;text-align:center;
|
| 194 |
+
font-size:66px;font-weight:800;color:#fff;
|
| 195 |
+
font-family:-apple-system,'Inter',sans-serif;">${trainerName}</div>
|
| 196 |
+
<div id="end-tag" class="clip" data-start="28.4" data-duration="1.6" data-track-index="26"
|
| 197 |
+
style="position:absolute;top:52%;left:0;right:0;text-align:center;
|
| 198 |
+
font-size:32px;font-weight:500;color:#9990ee;
|
| 199 |
+
font-family:-apple-system,'Inter',sans-serif;">${tagline}</div>
|
| 200 |
+
<div id="end-web" class="clip" data-start="28.6" data-duration="1.4" data-track-index="27"
|
| 201 |
+
style="position:absolute;top:58.5%;left:0;right:0;text-align:center;
|
| 202 |
+
font-size:24px;color:rgba(255,255,255,0.5);
|
| 203 |
+
font-family:-apple-system,'Inter',sans-serif;">${website}</div>
|
| 204 |
+
|
| 205 |
+
<!-- ─── AUDIO ─── -->
|
| 206 |
+
<audio data-start="0" data-duration="30" data-track-index="50"
|
| 207 |
+
data-volume="0.26" src="./assets/music.mp3"></audio>
|
| 208 |
+
|
| 209 |
+
<!-- ─── PERSISTENT BRAND BAR (not a clip) ─── -->
|
| 210 |
+
<div id="brand-bar">
|
| 211 |
+
<span style="font-size:22px;font-weight:700;color:#fff;font-family:-apple-system,sans-serif;">
|
| 212 |
+
${trainerName}</span>
|
| 213 |
+
<span style="font-size:15px;color:rgba(255,255,255,0.6);font-family:-apple-system,sans-serif;">
|
| 214 |
+
· ${tagline}</span>
|
| 215 |
+
</div>
|
| 216 |
+
|
| 217 |
+
<!-- ─── GSAP ANIMATIONS ─── -->
|
| 218 |
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
| 219 |
+
<script>
|
| 220 |
+
const tl = gsap.timeline({ paused: true });
|
| 221 |
+
|
| 222 |
+
// Scene 1: Hook
|
| 223 |
+
tl.from("#cap-h1", { y:50, opacity:0, duration:0.45, ease:"power3.out" }, 0.4);
|
| 224 |
+
tl.from("#cap-h2", { y:35, opacity:0, duration:0.4, ease:"power3.out" }, 0.7);
|
| 225 |
+
tl.to("#s1", { opacity:0, duration:0.3 }, 3.7);
|
| 226 |
+
|
| 227 |
+
// Scene 2: Proof
|
| 228 |
+
tl.from("#s2", { opacity:0, duration:0.3 }, 4);
|
| 229 |
+
tl.from("#cap-stat", { scale:0.72, opacity:0, duration:0.5, ease:"back.out(1.7)" }, 4.4);
|
| 230 |
+
tl.from("#lt", { x:-320, opacity:0, duration:0.4, ease:"power3.out" }, 4.7);
|
| 231 |
+
tl.to("#lt", { x:-320, opacity:0, duration:0.3, ease:"power2.in" }, 7.3);
|
| 232 |
+
tl.to("#s2", { opacity:0, duration:0.35 }, 7.65);
|
| 233 |
+
|
| 234 |
+
// Scene 3: Testimonial
|
| 235 |
+
tl.from("#s3", { opacity:0, duration:0.35 }, 8);
|
| 236 |
+
tl.from("#cap-testi", { y:40, opacity:0, duration:0.45, ease:"power3.out" }, 8.5);
|
| 237 |
+
tl.to("#cap-testi", { opacity:0, duration:0.3 }, 15.4);
|
| 238 |
+
tl.to("#s3", { opacity:0, duration:0.35 }, 15.65);
|
| 239 |
+
|
| 240 |
+
// Scene 4a: Energy cut 1
|
| 241 |
+
tl.from("#s4a", { opacity:0, duration:0.18 }, 16);
|
| 242 |
+
tl.from("#cap-e1",{ y:28, opacity:0, duration:0.28, ease:"power2.out" }, 16.3);
|
| 243 |
+
tl.to("#s4a", { opacity:0, duration:0.18 }, 18.32);
|
| 244 |
+
|
| 245 |
+
// Scene 4b: Energy cut 2
|
| 246 |
+
tl.from("#s4b", { opacity:0, duration:0.18 }, 18.5);
|
| 247 |
+
tl.from("#cap-e2",{ y:28, opacity:0, duration:0.28, ease:"power2.out" }, 18.8);
|
| 248 |
+
tl.to("#s4b", { opacity:0, duration:0.18 }, 20.82);
|
| 249 |
+
|
| 250 |
+
// Scene 4c: Energy cut 3 — scale pop
|
| 251 |
+
tl.from("#s4c", { opacity:0, duration:0.15 }, 21);
|
| 252 |
+
tl.from("#cap-e3",{ scale:0.78, opacity:0, duration:0.3, ease:"back.out(2)" }, 21.3);
|
| 253 |
+
tl.to("#s4c", { opacity:0, duration:0.22 }, 22.78);
|
| 254 |
+
|
| 255 |
+
// Scene 5: CTA
|
| 256 |
+
tl.from("#s5", { opacity:0, duration:0.38 }, 23);
|
| 257 |
+
tl.from("#cap-cta1",{ y:42, opacity:0, duration:0.5, ease:"power3.out" }, 23.4);
|
| 258 |
+
tl.from("#cap-cta2",{ y:26, opacity:0, duration:0.45, ease:"power3.out" }, 25);
|
| 259 |
+
tl.to("#s5", { opacity:0, duration:0.4 }, 27.6);
|
| 260 |
+
|
| 261 |
+
// Scene 6: Brand end card
|
| 262 |
+
tl.from("#s6", { opacity:0, duration:0.4 }, 28);
|
| 263 |
+
tl.from("#end-name",{ y:30, opacity:0, duration:0.45, ease:"power3.out" }, 28.2);
|
| 264 |
+
tl.from("#end-tag", { y:22, opacity:0, duration:0.4, ease:"power3.out" }, 28.4);
|
| 265 |
+
tl.from("#end-web", { y:16, opacity:0, duration:0.4, ease:"power3.out" }, 28.6);
|
| 266 |
+
|
| 267 |
+
// CRITICAL: extend timeline to full duration
|
| 268 |
+
tl.set({}, {}, ${dur});
|
| 269 |
+
|
| 270 |
+
// CRITICAL: register with exact data-composition-id
|
| 271 |
+
window.__timelines = window.__timelines || {};
|
| 272 |
+
window.__timelines["${compId}"] = tl;
|
| 273 |
+
</script>
|
| 274 |
+
</div></body></html>`;
|
| 275 |
},
|
| 276 |
};
|
| 277 |
|
| 278 |
+
// ── TEASER (30s · 9:16) ─────────────────────────────────────────────────────
|
| 279 |
+
const teaser = {
|
| 280 |
+
meta: {
|
| 281 |
+
name: 'Event Teaser', icon: '⚡',
|
| 282 |
+
description: '30s fast-cut teaser. 9:16 vertical.',
|
| 283 |
+
format: '9:16', duration: 30, music: 'energetic',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
},
|
| 285 |
+
buildHTML(media, config, compId, w, h, dur) {
|
| 286 |
+
const { clientName = 'Next Event', trainerName = 'Dee Ferdinand',
|
| 287 |
+
tagline = 'AI Corporate Trainer', website = 'deeferdinand.com' } = config;
|
| 288 |
+
const m = (i) => media[i % Math.max(media.length,1)] || media[0];
|
| 289 |
|
| 290 |
+
return `<!DOCTYPE html>
|
| 291 |
+
<html><head><meta charset="utf-8">
|
| 292 |
+
<style>
|
| 293 |
+
* { margin:0;padding:0;box-sizing:border-box; }
|
| 294 |
+
body { background:#000;overflow:hidden; }
|
| 295 |
+
#root { position:relative;width:${w}px;height:${h}px;overflow:hidden;background:#000; }
|
| 296 |
+
.s { position:absolute;inset:0; }
|
| 297 |
+
#bb { position:absolute;bottom:0;left:0;right:0;padding:18px 48px 30px;
|
| 298 |
+
background:linear-gradient(transparent,rgba(0,0,0,0.72));z-index:100; }
|
| 299 |
+
</style></head><body>
|
| 300 |
+
<div id="root" data-composition-id="${compId}" data-start="0" data-width="${w}" data-height="${h}">
|
| 301 |
+
|
| 302 |
+
<div id="s1" class="clip s" data-start="0" data-duration="3" data-track-index="0">
|
| 303 |
+
${m(0) ? `<img src="${m(0).rel}" style="width:100%;height:100%;object-fit:cover;">` : ''}
|
| 304 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.35);"></div>
|
| 305 |
+
</div>
|
| 306 |
+
<div id="c1" class="clip" data-start="0.3" data-duration="2.5" data-track-index="1"
|
| 307 |
+
style="position:absolute;top:42%;left:0;right:0;text-align:center;
|
| 308 |
+
font-size:100px;font-weight:900;color:#fff;
|
| 309 |
+
font-family:-apple-system,sans-serif;letter-spacing:-2px;
|
| 310 |
+
text-shadow:0 4px 20px rgba(0,0,0,0.9);">COMING<br>SOON</div>
|
| 311 |
+
|
| 312 |
+
<div id="s2" class="clip s" data-start="3" data-duration="3" data-track-index="2">
|
| 313 |
+
${m(1) ? `<img src="${m(1).rel}" style="width:100%;height:100%;object-fit:cover;">` : ''}
|
| 314 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.3);"></div>
|
| 315 |
+
</div>
|
| 316 |
+
<div id="c2" class="clip" data-start="3.3" data-duration="2.4" data-track-index="3"
|
| 317 |
+
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
|
| 318 |
+
font-size:68px;font-weight:900;color:#fff;
|
| 319 |
+
font-family:-apple-system,sans-serif;
|
| 320 |
+
text-shadow:0 3px 16px rgba(0,0,0,0.9);">${clientName}</div>
|
| 321 |
+
|
| 322 |
+
<div id="s3" class="clip s" data-start="6" data-duration="3" data-track-index="4">
|
| 323 |
+
${m(2) ? `<img src="${m(2).rel}" style="width:100%;height:100%;object-fit:cover;">` : ''}
|
| 324 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.3);"></div>
|
| 325 |
+
</div>
|
| 326 |
+
<div id="c3" class="clip" data-start="6.3" data-duration="2.4" data-track-index="5"
|
| 327 |
+
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
|
| 328 |
+
font-size:68px;font-weight:900;color:#fff;
|
| 329 |
+
font-family:-apple-system,sans-serif;
|
| 330 |
+
text-shadow:0 3px 16px rgba(0,0,0,0.9);">AI Training</div>
|
| 331 |
+
|
| 332 |
+
<div id="s4" class="clip s" data-start="9" data-duration="3" data-track-index="6">
|
| 333 |
+
${m(0) ? `<img src="${m(0).rel}" style="width:100%;height:100%;object-fit:cover;">` : ''}
|
| 334 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.3);"></div>
|
| 335 |
+
</div>
|
| 336 |
+
<div id="c4" class="clip" data-start="9.3" data-duration="2.4" data-track-index="7"
|
| 337 |
+
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
|
| 338 |
+
font-size:68px;font-weight:900;color:#9EF0C8;
|
| 339 |
+
font-family:-apple-system,sans-serif;
|
| 340 |
+
text-shadow:0 3px 16px rgba(0,0,0,0.9);">Hands-on. Now.</div>
|
| 341 |
+
|
| 342 |
+
<div id="s5" class="clip s" data-start="12" data-duration="3" data-track-index="8">
|
| 343 |
+
${m(1) ? `<img src="${m(1).rel}" style="width:100%;height:100%;object-fit:cover;">` : ''}
|
| 344 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.3);"></div>
|
| 345 |
+
</div>
|
| 346 |
+
<div id="c5" class="clip" data-start="12.3" data-duration="2.4" data-track-index="9"
|
| 347 |
+
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
|
| 348 |
+
font-size:68px;font-weight:900;color:#fff;
|
| 349 |
+
font-family:-apple-system,sans-serif;
|
| 350 |
+
text-shadow:0 3px 16px rgba(0,0,0,0.9);">Real results.</div>
|
| 351 |
+
|
| 352 |
+
<div id="s6" class="clip s" data-start="15" data-duration="12" data-track-index="10">
|
| 353 |
+
${m(2) ? `<img src="${m(2).rel}" style="width:100%;height:100%;object-fit:cover;">` : ''}
|
| 354 |
+
<div style="position:absolute;inset:0;background:rgba(8,4,24,0.6);"></div>
|
| 355 |
+
</div>
|
| 356 |
+
<div id="c6" class="clip" data-start="15.4" data-duration="6" data-track-index="11"
|
| 357 |
+
style="position:absolute;top:38%;left:0;right:0;text-align:center;
|
| 358 |
+
font-size:80px;font-weight:900;color:#fff;
|
| 359 |
+
font-family:-apple-system,sans-serif;line-height:1.2;
|
| 360 |
+
text-shadow:0 4px 20px rgba(0,0,0,0.9);">Save your spot.<br>Link in bio.</div>
|
| 361 |
+
|
| 362 |
+
<div id="se" class="clip s" data-start="27" data-duration="3" data-track-index="12"
|
| 363 |
+
style="background:linear-gradient(135deg,#0d0d1f,#1a1040,#0d0d1f);">
|
| 364 |
+
<div style="position:absolute;inset:0;background:radial-gradient(ellipse at center,rgba(124,111,224,0.2),transparent 70%);"></div>
|
| 365 |
+
</div>
|
| 366 |
+
<div id="en" class="clip" data-start="27.2" data-duration="2.8" data-track-index="13"
|
| 367 |
+
style="position:absolute;top:44%;left:0;right:0;text-align:center;
|
| 368 |
+
font-size:60px;font-weight:800;color:#fff;font-family:-apple-system,sans-serif;">${trainerName}</div>
|
| 369 |
+
<div id="et" class="clip" data-start="27.4" data-duration="2.6" data-track-index="14"
|
| 370 |
+
style="position:absolute;top:52%;left:0;right:0;text-align:center;
|
| 371 |
+
font-size:30px;color:#9990ee;font-family:-apple-system,sans-serif;">${tagline}</div>
|
| 372 |
+
|
| 373 |
+
<audio data-start="0" data-duration="30" data-track-index="50" data-volume="0.3"
|
| 374 |
+
src="./assets/music.mp3"></audio>
|
| 375 |
+
|
| 376 |
+
<div id="bb">
|
| 377 |
+
<span style="font-size:22px;font-weight:700;color:#fff;font-family:-apple-system,sans-serif;">${trainerName}</span>
|
| 378 |
+
<span style="font-size:15px;color:rgba(255,255,255,0.6);font-family:-apple-system,sans-serif;"> · ${tagline}</span>
|
| 379 |
+
</div>
|
| 380 |
+
|
| 381 |
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
| 382 |
+
<script>
|
| 383 |
+
const tl = gsap.timeline({ paused: true });
|
| 384 |
+
tl.from("#c1", { scale:0.75, opacity:0, duration:0.4, ease:"back.out(1.7)" }, 0.3);
|
| 385 |
+
tl.to("#s1", { opacity:0, duration:0.2 }, 2.8);
|
| 386 |
+
tl.from("#s2", { opacity:0, duration:0.2 }, 3);
|
| 387 |
+
tl.from("#c2", { y:30, opacity:0, duration:0.3, ease:"power2.out" }, 3.3);
|
| 388 |
+
tl.to("#s2", { opacity:0, duration:0.2 }, 5.8);
|
| 389 |
+
tl.from("#s3", { opacity:0, duration:0.2 }, 6);
|
| 390 |
+
tl.from("#c3", { y:30, opacity:0, duration:0.3, ease:"power2.out" }, 6.3);
|
| 391 |
+
tl.to("#s3", { opacity:0, duration:0.2 }, 8.8);
|
| 392 |
+
tl.from("#s4", { opacity:0, duration:0.2 }, 9);
|
| 393 |
+
tl.from("#c4", { scale:0.8, opacity:0, duration:0.3, ease:"back.out(2)" }, 9.3);
|
| 394 |
+
tl.to("#s4", { opacity:0, duration:0.2 }, 11.8);
|
| 395 |
+
tl.from("#s5", { opacity:0, duration:0.2 }, 12);
|
| 396 |
+
tl.from("#c5", { y:30, opacity:0, duration:0.3, ease:"power2.out" }, 12.3);
|
| 397 |
+
tl.to("#s5", { opacity:0, duration:0.2 }, 14.8);
|
| 398 |
+
tl.from("#s6", { opacity:0, duration:0.3 }, 15);
|
| 399 |
+
tl.from("#c6", { y:40, opacity:0, duration:0.5, ease:"power3.out" }, 15.4);
|
| 400 |
+
tl.to("#s6", { opacity:0, duration:0.35 }, 26.65);
|
| 401 |
+
tl.from("#se", { opacity:0, duration:0.35 }, 27);
|
| 402 |
+
tl.from("#en", { y:25, opacity:0, duration:0.4, ease:"power3.out" }, 27.2);
|
| 403 |
+
tl.from("#et", { y:18, opacity:0, duration:0.35, ease:"power3.out" }, 27.4);
|
| 404 |
+
tl.set({}, {}, ${dur});
|
| 405 |
+
window.__timelines = window.__timelines || {};
|
| 406 |
+
window.__timelines["${compId}"] = tl;
|
| 407 |
+
</script>
|
| 408 |
+
</div></body></html>`;
|
| 409 |
},
|
| 410 |
};
|
| 411 |
|
| 412 |
+
// Trailer and Community reuse testimonial pattern
|
| 413 |
+
const trailer = { ...testimonial, meta: { ...testimonial.meta, name: 'Cinematic Trailer', icon: '🎬', duration: 60 } };
|
| 414 |
+
const community = { ...testimonial, meta: { ...testimonial.meta, name: 'Community Story', icon: '🤝' } };
|
| 415 |
+
|
| 416 |
export const WORKFLOWS = { testimonial, teaser, trailer, community };
|