initial code (#2)

* feat: initial code port from hyperframes-internal

Port all OSS-ready packages from the internal monorepo:
- @hyperframes/core — shared types, HTML generation, GSAP utilities, runtime
- @hyperframes/cli — CLI for creating, previewing, and rendering compositions
- @hyperframes/engine — framework-agnostic rendering engine (BeginFrame + FFmpeg)
- @hyperframes/producer — video rendering pipeline (Puppeteer + FFmpeg)
- @hyperframes/ui-player — browser-based video player component
- @hyperframes/studio — composition editor (React frontend + Hono backend)

Includes regression test suite with Docker-based test harness.

All HeyGen-internal references, deployment infrastructure, and
proprietary assets have been removed. Package names migrated
from @app/* to @hyperframes/*.

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

* fix: scrub internal codenames and stale references from OSS port

- Replace static.heygen.ai runtime URLs in test fixtures
- Remove internal CDN publish script (publish-hyperframe-runtime.ts)
- Replace sandbox-studio, sandbox-interceptor, __magicEditRuntime
  with neutral names (studio, hyperframe-runtime, __hyperframeRuntime)
- Fix stale Vault API / localhost references in docs
- Remove broken deprecated_studio link

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

* fix: remove remaining internal codenames and stale references

- Delete stale producer README.md and PIPELINE.md (referenced nonexistent files)
- Replace "Cerberus" codename with "HyperFrames" in test design reviews
- Replace magic-edit postMessage identifiers with hf-preview/hf-parent
- Rename debug-magic-edit-timeline.ts to debug-timeline.ts
- Replace "Motion Cut" with "HyperFrames" in Timeline comments
- Fix studio/CLI references to nonexistent archive package
  (use local data/projects/ dir, stub render proxy)

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-03-21 22:43:56 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 10621e7903
commit 9f8e5ba5a1
401 changed files with 54545 additions and 2 deletions
@@ -0,0 +1,63 @@
# HyperFrame Schema Compliance Review
## Executive Summary
- Total files reviewed: 3
- Critical issues: 0
- Overall compliance status: PASS
## Critical Issues
None. The compositions follow the deterministic requirements and essential timeline registration rules.
## Compliance Checklist
- [x] All compositions have `data-width` and `data-height` attributes
- [x] All timelines are finite with duration > 0
- [x] All compositions registered in `window.__timelines`
- [x] No use of `Math.random()`, `Date.now()`, or non-deterministic code
- [x] Primitive clips have required data attributes (`id`, `data-start`, `data-track`)
- [x] `data-duration` specified for all `<img>` clips (N/A - no images used)
- [x] No manual media playback control (`video.play()`, `audio.pause()`, etc.)
- [x] No manual clip mounting/unmounting in scripts
- [x] Relative timing references are valid (N/A - absolute timing used)
- [x] Clips on same track don't overlap in time
- [x] Reusable compositions in separate HTML files
- [x] Composition files use `<template>` tags
- [x] External compositions loaded via `data-composition-src`
- [x] All script-animated content wrapped in compositions
- [x] No infinite or zero-duration timelines
## File Reviews
### index.html
**Status**: COMPLIANT
**Observations**:
- Correctly defines the root composition `main-video`.
- Uses `data-width="1920"` and `data-height="1080"` as requested for Landscape orientation.
- Properly registers `window.__timelines["main-video"]`.
- Loads sub-compositions using `data-composition-src`.
- Primitive clips (`video`, `audio`) have required `id`, `data-start`, and `data-track`.
- Track assignments are clean: Track 0 (BG), Track 1 (Video), Track 2 (Graphics), Track 3 (Captions), Track 4 (Audio).
### compositions/captions.html
**Status**: COMPLIANT
**Observations**:
- Uses `<template>` tag correctly.
- Root element has `data-composition-id`, `data-width`, `data-height`, and `data-duration`.
- Script is deterministic, using a fixed `TRANSCRIPT` array.
- Correctly registers `window.__timelines["captions"]`.
- Note: The `data-composition-id` in the HTML is `captions`, and the script registers `captions`. In `index.html`, the container has `data-composition-id="captions-comp"`. The framework typically matches the registration to the ID of the element being instantiated. Since the template root has `data-composition-id="captions"`, this is the ID that should be registered.
### compositions/graphics.html
**Status**: COMPLIANT
**Observations**:
- Uses `<template>` tag correctly.
- Root element has `data-composition-id`, `data-width`, `data-height`, and `data-duration`.
- Animations are deterministic GSAP tweens.
- Correctly registers `window.__timelines["graphics-comp"]`.
- Layout is well-coordinated with the A-roll positioning defined in the master timeline.
## Recommendations
- **Consistency**: In `index.html`, the captions container uses `data-composition-id="captions-comp"`, but the template inside `captions.html` uses `data-composition-id="captions"`. While the framework handles the swap during instantiation, keeping these IDs identical (e.g., both `captions`) improves maintainability.
- **Deterministic Logic**: The use of `(function() { ... })()` in sub-compositions is a good practice for scoping.
@@ -0,0 +1,94 @@
<template id="captions-template">
<div data-composition-id="captions" data-width="1920" data-height="1080" data-duration="17">
<div id="captions-container"></div>
<style>
[data-composition-id="captions"] {
width: 1920px;
height: 1080px;
position: relative; /* Changed from flex to absolute positioning for precision */
pointer-events: none;
background: transparent;
}
[data-composition-id="captions"] #captions-container {
position: absolute;
bottom: 54px; /* 5% of 1080px */
left: 0;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
[data-composition-id="captions"] .caption-line {
position: absolute;
bottom: 0;
/* No left: 50% here, we rely on the container's flex centering */
background-color: #000000; /* Dark solid background strip */
color: #FFFFFF; /* White */
font-family: 'Futura', 'DIN Alternate', 'Arial Black', sans-serif;
font-weight: 900;
font-size: 56px;
padding: 15px 40px;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0;
white-space: nowrap;
display: inline-block;
z-index: 100;
}
</style>
<script>
(function() {
const TRANSCRIPT = [{'text': 'We', 'start': 0.119, 'end': 0.259}, {'text': 'asked', 'start': 0.319, 'end': 0.479}, {'text': 'what', 'start': 0.519, 'end': 0.659}, {'text': 'you', 'start': 0.699, 'end': 0.819}, {'text': 'needed.', 'start': 0.859, 'end': 1.819}, {'text': 'Forty-seven', 'start': 1.86, 'end': 2.299}, {'text': 'percent', 'start': 2.399, 'end': 2.679}, {'text': 'of', 'start': 2.7, 'end': 2.799}, {'text': 'you', 'start': 2.839, 'end': 2.939}, {'text': 'said', 'start': 3.039, 'end': 3.179}, {'text': 'motion', 'start': 3.24, 'end': 3.559}, {'text': 'graphics,', 'start': 3.579, 'end': 4.599}, {'text': 'sixty-two', 'start': 4.679, 'end': 5.179}, {'text': 'percent', 'start': 5.299, 'end': 5.759}, {'text': 'said', 'start': 5.859, 'end': 5.98}, {'text': 'static', 'start': 6.079, 'end': 6.399}, {'text': 'content', 'start': 6.46, 'end': 6.879}, {'text': 'was', 'start': 6.92, 'end': 7.079}, {'text': 'costing', 'start': 7.099, 'end': 7.48}, {'text': 'you', 'start': 7.5, 'end': 7.579}, {'text': 'attention,', 'start': 7.679, 'end': 8.659}, {'text': 'and', 'start': 8.699, 'end': 8.86}, {'text': 'three', 'start': 8.88, 'end': 9.06}, {'text': 'out', 'start': 9.079, 'end': 9.18}, {'text': 'of', 'start': 9.199, 'end': 9.34}, {'text': 'four', 'start': 9.38, 'end': 9.799}, {'text': 'said', 'start': 9.84, 'end': 10.0}, {'text': 'you', 'start': 10.019, 'end': 10.159}, {'text': 'know', 'start': 10.179, 'end': 10.36}, {'text': 'the', 'start': 10.38, 'end': 10.42}, {'text': 'look', 'start': 10.519, 'end': 10.699}, {'text': 'you', 'start': 10.739, 'end': 10.859}, {'text': 'want,', 'start': 10.98, 'end': 11.34}, {'text': 'but', 'start': 11.359, 'end': 11.52}, {'text': "don't", 'start': 11.56, 'end': 11.779}, {'text': 'have', 'start': 11.819, 'end': 11.94}, {'text': 'the', 'start': 11.96, 'end': 12.06}, {'text': 'editing', 'start': 12.079, 'end': 12.4}, {'text': 'skills', 'start': 12.52, 'end': 12.859}, {'text': 'to', 'start': 12.88, 'end': 13.0}, {'text': 'get', 'start': 13.019, 'end': 13.18}, {'text': 'there.', 'start': 13.22, 'end': 14.22}, {'text': 'So', 'start': 14.239, 'end': 14.399}, {'text': 'we', 'start': 14.42, 'end': 14.52}, {'text': 'built', 'start': 14.619, 'end': 14.88}, {'text': 'Editor', 'start': 15.079, 'end': 15.42}, {'text': 'Agent.', 'start': 15.619, 'end': 16.02}];
const container = document.getElementById('captions-container');
const tl = gsap.timeline({ paused: true });
const lines = [];
let currentLine = [];
TRANSCRIPT.forEach((word, index) => {
currentLine.push(word);
if (currentLine.length === 5 || index === TRANSCRIPT.length - 1) {
lines.push({
words: [...currentLine],
text: currentLine.map(w => w.text).join(' ').toUpperCase()
});
currentLine = [];
}
});
lines.forEach((line, index) => {
const startTime = line.words[0].start;
const endTime = line.words[line.words.length - 1].end;
const lineEl = document.createElement('div');
lineEl.className = 'caption-line';
lineEl.textContent = line.text;
container.appendChild(lineEl);
// Center the line element relative to the container
gsap.set(lineEl, { left: '50%', xPercent: -50 });
tl.to(lineEl, {
opacity: 1,
y: -20,
duration: 0.2,
ease: 'power2.out'
}, startTime);
tl.to(lineEl, {
opacity: 0,
y: -40,
duration: 0.1,
ease: 'power2.in'
}, endTime);
});
window.__timelines["captions"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,239 @@
<template id="graphics-template">
<div data-composition-id="graphics-comp" data-width="1920" data-height="1080" data-duration="17">
<!-- Intro Elements -->
<div class="intro-container">
<div class="black-circle"></div>
<div class="intro-text">ASKED & ANSWERED</div>
</div>
<!-- Stat 1: 47% NEED MOTION (A-roll is at right: x:700, y:216) -->
<div class="stat-container stat-1">
<div class="vermillion-square"></div>
<div class="stat-number">47%</div>
<div class="stat-subtext">NEED MOTION</div>
</div>
<!-- Stat 2: 62% INEFFECTIVE (A-roll is at left: x:68, y:216) -->
<div class="stat-container stat-2">
<div class="white-circle">
<div class="stat-number">62%</div>
<div class="stat-subtext">INEFFECTIVE</div>
</div>
</div>
<!-- Stat 3: 75% LACK SKILLS (A-roll is centered small: x:480, y:450) -->
<div class="stat-container stat-3">
<div class="black-rectangle"></div>
<div class="stat-number">75%</div>
<div class="stat-subtext">LACK SKILLS</div>
</div>
<!-- Outro: EDITOR AGENT -->
<div class="outro-container">
<div class="outro-circle"></div>
<div class="outro-text">EDITOR AGENT</div>
</div>
<style>
[data-composition-id="graphics-comp"] {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
font-family: 'Futura', 'DIN Alternate', 'Arial Black', sans-serif;
overflow: hidden;
}
/* Common Styles */
.intro-container, .stat-container, .outro-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
pointer-events: none;
}
.stat-number {
font-size: 200px;
font-weight: 900;
line-height: 1;
z-index: 2;
}
.stat-subtext {
position: absolute;
font-size: 60px;
font-weight: 900;
z-index: 3;
text-align: center;
width: 800px;
}
/* Intro Styles - Centered behind A-roll (A-roll is full screen) */
.black-circle {
width: 800px;
height: 800px;
background-color: #000000;
border-radius: 50%;
position: absolute;
}
.intro-text {
font-size: 100px;
font-weight: 900;
color: #D0021B;
z-index: 2;
text-align: center;
width: 1000px;
}
/* Stat 1 Styles (Left side) - A-roll is at x:700 */
.stat-1 .vermillion-square {
width: 600px;
height: 600px;
background-color: #D0021B;
position: absolute;
left: 50px;
top: 240px;
}
.stat-1 .stat-number {
color: #FFFFFF;
position: absolute;
left: 100px;
top: 400px;
}
.stat-1 .stat-subtext {
color: #000000;
left: 100px;
top: 650px;
text-align: left;
}
/* Stat 2 Styles (Right side) - A-roll is at x:68 */
.stat-2 {
opacity: 0;
pointer-events: none;
}
.stat-2 .white-circle {
width: 550px; /* Scaled down from 650px to avoid clipping */
height: 550px;
background-color: #D0021B; /* Vermillion background */
border-radius: 50%;
position: absolute;
right: 100px; /* Moved further right to avoid A-roll */
top: 265px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.stat-2 .stat-number {
color: #000000; /* Black text */
position: relative;
right: auto;
top: auto;
font-size: 180px; /* Slightly adjusted for better fit in circle */
}
.stat-2 .stat-subtext {
color: #000000; /* Black text */
position: relative;
right: auto;
top: auto;
text-align: center;
width: auto;
font-size: 50px; /* Slightly adjusted for better fit in circle */
}
/* Stat 3 Styles (Top Center) - A-roll is at y:450 */
.stat-3 .black-rectangle {
width: 1000px;
height: 350px;
background-color: #000000;
position: absolute;
top: 50px;
}
.stat-3 .stat-number {
color: #D0021B;
position: absolute;
top: 100px;
}
.stat-3 .stat-subtext {
color: #FFFFFF;
top: 300px;
}
/* Outro Styles */
.outro-circle {
width: 900px;
height: 900px;
background-color: #D0021B;
border-radius: 50%;
position: absolute;
}
.outro-text {
font-size: 150px;
font-weight: 900;
color: #000000;
z-index: 2;
text-align: center;
width: 1000px;
}
</style>
<script>
(function() {
const TRANSCRIPT = [{'text': 'We', 'start': 0.119, 'end': 0.259}, {'text': 'asked', 'start': 0.319, 'end': 0.479}, {'text': 'what', 'start': 0.519, 'end': 0.659}, {'text': 'you', 'start': 0.699, 'end': 0.819}, {'text': 'needed.', 'start': 0.859, 'end': 1.819}, {'text': 'Forty-seven', 'start': 1.86, 'end': 2.299}, {'text': 'percent', 'start': 2.399, 'end': 2.679}, {'text': 'of', 'start': 2.7, 'end': 2.799}, {'text': 'you', 'start': 2.839, 'end': 2.939}, {'text': 'said', 'start': 3.039, 'end': 3.179}, {'text': 'motion', 'start': 3.24, 'end': 3.559}, {'text': 'graphics,', 'start': 3.579, 'end': 4.599}, {'text': 'sixty-two', 'start': 4.679, 'end': 5.179}, {'text': 'percent', 'start': 5.299, 'end': 5.759}, {'text': 'said', 'start': 5.859, 'end': 5.98}, {'text': 'static', 'start': 6.079, 'end': 6.399}, {'text': 'content', 'start': 6.46, 'end': 6.879}, {'text': 'was', 'start': 6.92, 'end': 7.079}, {'text': 'costing', 'start': 7.099, 'end': 7.48}, {'text': 'you', 'start': 7.5, 'end': 7.579}, {'text': 'attention,', 'start': 7.679, 'end': 8.659}, {'text': 'and', 'start': 8.699, 'end': 8.86}, {'text': 'three', 'start': 8.88, 'end': 9.06}, {'text': 'out', 'start': 9.079, 'end': 9.18}, {'text': 'of', 'start': 9.199, 'end': 9.34}, {'text': 'four', 'start': 9.38, 'end': 9.799}, {'text': 'said', 'start': 9.84, 'end': 10.0}, {'text': 'you', 'start': 10.019, 'end': 10.159}, {'text': 'know', 'start': 10.179, 'end': 10.36}, {'text': 'the', 'start': 10.38, 'end': 10.42}, {'text': 'look', 'start': 10.519, 'end': 10.699}, {'text': 'you', 'start': 10.739, 'end': 10.859}, {'text': 'want,', 'start': 10.98, 'end': 11.34}, {'text': 'but', 'start': 11.359, 'end': 11.52}, {'text': "don't", 'start': 11.56, 'end': 11.779}, {'text': 'have', 'start': 11.819, 'end': 11.94}, {'text': 'the', 'start': 11.96, 'end': 12.06}, {'text': 'editing', 'start': 12.079, 'end': 12.4}, {'text': 'skills', 'start': 12.52, 'end': 12.859}, {'text': 'to', 'start': 12.88, 'end': 13.0}, {'text': 'get', 'start': 13.019, 'end': 13.18}, {'text': 'there.', 'start': 13.22, 'end': 14.22}, {'text': 'So', 'start': 14.239, 'end': 14.399}, {'text': 'we', 'start': 14.42, 'end': 14.52}, {'text': 'built', 'start': 14.619, 'end': 14.88}, {'text': 'Editor', 'start': 15.079, 'end': 15.42}, {'text': 'Agent.', 'start': 15.619, 'end': 16.02}];
const tl = gsap.timeline({ paused: true });
// 1. Intro (0-1.8s)
tl.set('.intro-container', { opacity: 1 }, 0);
tl.from('.black-circle', { scale: 0, duration: 0.3, ease: 'power2.out' }, 0);
tl.from('.intro-text', { x: -100, opacity: 0, duration: 0.3, ease: 'power2.out' }, 0.1);
tl.set('.intro-container', { opacity: 0 }, 1.8);
// 2. Stat 1 (1.86s - 4.6s) - "Forty-seven percent"
const stat1Time = 1.86;
tl.set('.stat-1', { opacity: 1 }, stat1Time);
tl.from('.stat-1 .vermillion-square', { x: -1000, duration: 0.3, ease: 'power2.out' }, stat1Time);
tl.from('.stat-1 .stat-number', { opacity: 0, scale: 0.8, duration: 0.2, ease: 'power2.out' }, stat1Time + 0.1);
tl.from('.stat-1 .stat-subtext', { opacity: 0, y: 20, duration: 0.2, ease: 'power2.out' }, stat1Time + 0.2);
tl.set('.stat-1', { opacity: 0 }, 4.6);
// 3. Stat 2 (4.679s - 8.8s) - "Sixty-two percent"
const stat2Time = 4.679;
tl.set('.stat-2', { opacity: 1 }, stat2Time);
tl.from('.stat-2 .white-circle', { scale: 0, duration: 0.3, ease: 'power2.out' }, stat2Time);
tl.set('.stat-2', { opacity: 0 }, 8.8);
// 4. Stat 3 (8.88s - 14.2s) - "Three out of four"
const stat3Time = 8.88;
tl.set('.stat-3', { opacity: 1 }, stat3Time);
tl.from('.stat-3 .black-rectangle', { width: 0, duration: 0.3, ease: 'power2.out' }, stat3Time);
tl.from('.stat-3 .stat-number', { opacity: 0, y: -30, duration: 0.2, ease: 'power2.out' }, stat3Time + 0.1);
tl.from('.stat-3 .stat-subtext', { opacity: 0, y: 20, duration: 0.2, ease: 'power2.out' }, stat3Time + 0.2);
tl.set('.stat-3', { opacity: 0 }, 14.2);
// 5. Outro (14.239s - 17s) - "So we built Editor Agent"
tl.set('.outro-container', { opacity: 1 }, 14.239);
tl.from('.outro-circle', { scale: 0, duration: 0.3, ease: 'power2.out' }, 14.239);
tl.from('.outro-text', { scale: 1.5, opacity: 0, duration: 0.3, ease: 'power2.out' }, 14.3);
window.__timelines["graphics-comp"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,61 @@
# HyperFrames Design Review
## First Impression
This looks like a PowerPoint presentation from a 2010 corporate retreat that's trying way too hard to be "modern." Its a chaotic mess of geometric shapes and primary colors that lacks any sense of sophisticated art direction.
---
## CRITICAL Design Failures
Issues that make this look unprofessional or straight-up ugly. These MUST be fixed.
### The "Primary School" Color Palette
**Where:** `compositions/graphics.html`
**What's wrong:** You're using `#D0021B` (a generic, aggressive red), pure black `#000000`, and pure white `#FFFFFF`. Its the most basic, uninspired color combination possible. It lacks depth, vibration, or any sense of modern aesthetic.
**Why it matters:** It looks cheap and amateur. High-end design uses nuanced tones, not the default colors from a 1995 Windows paint bucket.
**Fix it:** Move to a more sophisticated palette. Try a deep crimson or a vibrant coral instead of that "stop sign" red. Use an off-white or a very light gray (`#F5F5F5`) instead of pure white to reduce eye strain and look more "editorial."
### Typography Hierarchy is Non-Existent
**Where:** `compositions/graphics.html` and `compositions/captions.html`
**What's wrong:** Everything is just "BIG AND BOLD." You're using `font-weight: 900` for everything from the 200px stats to the 56px captions. When everything is shouting, nothing is heard.
**Why it matters:** There's no visual path for the eye to follow. The viewer is just bombarded with heavy blocks of text.
**Fix it:** Introduce contrast in weight. Use a lighter weight (300 or 400) for subtext and captions to let the big numbers actually pop. Vary the tracking (letter-spacing) more intentionally.
### The "Floating Box" Caption Aesthetic
**Where:** `compositions/captions.html`
**What's wrong:** A solid black rectangle with white text slapped on the bottom. Its the "default" look of every low-budget social media video.
**Why it matters:** It obscures the composition and looks like an afterthought rather than an integrated design element.
**Fix it:** Remove the solid black background. Use a subtle text shadow or a very slight gradient overlay if readability is an issue. Or, better yet, integrate the captions into the geometric style of the rest of the video.
---
## Design Improvements
Things that aren't broken but are boring, lazy, or could be significantly better.
### Robotic Motion Curves
**Where:** `index.html` and `compositions/graphics.html`
**The problem:** You're using `power2.out` for almost everything. Its the "safe" choice, which makes it the "boring" choice. The transitions feel mechanical and predictable.
**Make it better:** Use more expressive easing. Try `expo.out` for faster, snappier entrances, or `back.out(1.7)` for a slight overshoot that gives the geometric shapes some "weight" and personality.
### Static Background Void
**Where:** `index.html` (`#bg-shapes`)
**The problem:** You have a layer for background shapes that is completely empty. The video just sits on a flat white or black void when it's scaled down.
**Make it better:** Actually use that layer. Add some subtle, slow-moving geometric patterns or a slight grain texture to give the composition some "atmosphere" so the A-roll doesn't look like it's floating in a vacuum.
---
## What Actually Works
The layout shifts in `index.html` (moving the A-roll from right to left to center) show a basic understanding of composition and screen real estate. Its the only thing keeping this from being a total disaster. The timing of the transitions (0.3s) is also decent—its quick enough to keep the pace up.
---
## Design Verdict
**Visual Impact:** 3/10 - Its loud, but not in a good way. Its the visual equivalent of someone shouting "LOOK AT ME" while wearing a neon vest.
**Color & Typography:** 2/10 - Lazy, default choices that scream "I didn't want to spend time on this."
**Motion & Animation Feel:** 4/10 - Functional but generic. No soul, no rhythm.
**Overall Aesthetic:** 3/10 - Amateur corporate template vibes.
**Bottom Line:** I wouldn't show this to a client unless I wanted to be fired. It needs a complete overhaul of its visual identity—start by picking a real color palette and learning that "bold" isn't the only font weight that exists.
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Geometric Bold - Ikko Tanaka Style</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
body, html {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background-color: #FFFFFF;
}
#main-canvas {
width: 100%;
height: 100%;
position: relative;
}
.aroll-container {
position: absolute;
overflow: hidden;
border-radius: 12px;
background: #000;
}
#aroll-video {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Background Shapes Layer */
#bg-shapes {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
</style>
</head>
<body>
<div id="main-canvas" data-composition-id="main-video" data-width="1920" data-height="1080" data-duration="17">
<video id="aroll-video"
data-start="0"
data-track-index="1"
class="aroll-container"
style="width: 1920px; height: 1080px; top: 0; left: 0; position: absolute; overflow: hidden; border-radius: 12px; object-fit: cover; background: #000;"
src="https://gen-os-static.s3.us-east-2.amazonaws.com/astral_assets/uploaded_assets/743011f1_e3c6afe3b7aa42369ce01e4961c60e22.mp4">
</video>
<!-- Background Shapes Layer -->
<div id="bg-shapes" data-composition-id="bg-layer" data-width="1920" data-height="1080" data-start="0" data-duration="17" data-track-index="0">
<script>
const bgTl = gsap.timeline({ paused: true });
window.__timelines["bg-layer"] = bgTl;
</script>
</div>
<!-- Intro / Graphics Layer -->
<div id="graphics-layer"
data-composition-id="graphics-comp"
data-composition-src="compositions/graphics.html"
data-start="0"
data-duration="17"
data-track-index="2">
</div>
<!-- Captions Layer -->
<div id="captions-layer"
data-composition-id="captions-comp"
data-composition-src="compositions/captions.html"
data-start="0"
data-duration="17"
data-track-index="3">
</div>
<!-- Transition SFX -->
<audio id="click-sfx" data-start="0" data-track-index="4" src="https://gen-os-static.s3.us-east-2.amazonaws.com/astral_assets/generated_assets/853dda23_70261e8b676444bfbfe147e34803851e.mp3"></audio>
<script>
const tl = gsap.timeline({ paused: true });
const aroll = document.getElementById('aroll-video');
// --- A-ROLL FRAMING LOGIC (LANDSCAPE 1920x1080) ---
// 0-3s: Intro - Slide in from right & Scale. Full screen.
tl.fromTo(aroll,
{ x: 1920, scale: 0.8 },
{ x: 0, scale: 1, duration: 0.3, ease: "power2.out" }, 0);
// Moment 1: 1.86s (Transition to first stat: "Forty-seven percent")
// Scale down to 60%, reposition to right side (next to graphic)
tl.to(aroll, {
width: 1152, height: 648, x: 700, y: 216,
duration: 0.3, ease: "power2.out"
}, 1.86);
// Moment 2: 4.679s (Transition to second stat: "Sixty-two percent")
// Shift to left side (next to graphic)
tl.to(aroll, {
x: 68, y: 216,
duration: 0.3, ease: "power2.out"
}, 4.679);
// Moment 3: 8.88s (Transition to third stat: "Three out of four")
// Center and scale down (graphic will be above/below)
tl.to(aroll, {
width: 960, height: 540, x: 480, y: 450,
duration: 0.3, ease: "power2.out"
}, 8.88);
// Moment 4: 14.239s (Closing/Editor Agent Reveal: "So we built...")
// Full screen zoom center
tl.to(aroll, {
width: 1920, height: 1080, x: 0, y: 0, scale: 1,
duration: 0.3, ease: "power2.out"
}, 14.239);
window.__timelines["main-video"] = tl;
</script>
</div>
</body>
</html>