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,67 @@
# HyperFrame Schema Compliance Review
## Executive Summary
- Total files reviewed: 5
- Critical issues: 1
- Overall compliance status: NEEDS_WORK
## Critical Issues
### Empty Tween for Duration
- **File**: compositions/captions.html:98
- **Violation**: `tl.to({}, { duration: 2 });`
- **Schema Rule**: "**NEVER create empty tweens** like `tl.to({}, { duration: N })` just to set duration — use `data-duration` instead"
- **Impact**: Violates the declarative timing model. The framework uses `data-duration` to determine the composition's length on the master timeline.
## 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 (no circular refs, referenced clips have known duration)
- [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-Specific Reviews
### index.html
**Status**: COMPLIANT
- Correctly uses `data-composition-id`, `data-width`, `data-height`, and `data-duration`.
- Correctly loads sub-compositions using `data-composition-src`.
- Registers `master` timeline in `window.__timelines`.
- **Note**: The `video` element (line 67) uses an absolute `data-start="3"`. This is valid.
### compositions/intro-seq.html
**Status**: COMPLIANT
- Correctly uses `<template>` and `data-composition-id`.
- Dimensions and duration are explicitly set.
- Script is deterministic (uses fixed text and timing).
- Registers `intro-seq` timeline.
### compositions/grid-bg.html
**Status**: COMPLIANT
- Correctly uses `<template>` and `data-composition-id`.
- Dimensions and duration are explicitly set.
- Loops are finite and deterministic.
- Registers `grid-bg` timeline.
### compositions/captions.html
**Status**: HAS_ISSUES
- **Issue**: Line 98 uses an empty tween `tl.to({}, { duration: 2 })` to extend the timeline.
- **Fix**: Remove the empty tween. The duration is already correctly defined in the `data-duration="16.04"` attribute on the root `div` (line 2).
- **Issue**: The `data-duration` is set to `16.04`, but the last caption ends at `16.019`. This is fine as `data-duration` takes precedence.
### compositions/data-graphics.html
**Status**: COMPLIANT
- Correctly uses `<template>` and `data-composition-id`.
- Dimensions and duration are explicitly set.
- Counter animation is deterministic in its logic.
- Registers `data-graphics` timeline.
@@ -0,0 +1,111 @@
<template id="background-template">
<div data-composition-id="background" data-width="1920" data-height="1080" data-duration="19.04">
<div class="grid-container">
<div class="modular-grid"></div>
<div class="dot-matrix"></div>
<div class="scan-line"></div>
</div>
<style>
[data-composition-id="background"] {
width: 1920px;
height: 1080px;
background-color: #0A0A0A;
overflow: hidden;
position: relative;
}
[data-composition-id="background"] .grid-container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
/* Modular Grid Lines - Subtle and Technical */
[data-composition-id="background"] .modular-grid {
width: calc(100% + 100px); /* Extra width for drift */
height: 100%;
position: absolute;
top: 0;
left: 0;
background-image:
linear-gradient(to right, rgba(138, 138, 138, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(138, 138, 138, 0.05) 1px, transparent 1px);
background-size: 60px 60px;
}
/* Dot Matrix Pattern - Finer and Sophisticated */
[data-composition-id="background"] .dot-matrix {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-image: radial-gradient(rgba(138, 138, 138, 0.1) 1px, transparent 1px);
background-size: 20px 20px;
}
/* Scan Line - Subtle Texture */
[data-composition-id="background"] .scan-line {
width: 100%;
height: 100px; /* Taller for a softer gradient feel */
background: linear-gradient(to bottom,
transparent,
rgba(26, 26, 26, 0.1) 50%,
transparent
);
position: absolute;
top: -100px;
left: 0;
z-index: 10;
pointer-events: none;
}
</style>
<script>
(function() {
const tl = gsap.timeline({ paused: true });
const duration = 19.04;
// 1. Subtle Grid Drift
// Move 60px (one grid unit) horizontally over the full duration
tl.to('[data-composition-id="background"] .modular-grid', {
x: -60,
duration: duration,
ease: "none"
}, 0);
// 2. Subtle Scan Line Texture
// Dark gray/blue (#1A1A1A) at 0.1 opacity
const scanCycle = 8; // Slower, more sophisticated movement
const cycles = Math.ceil(duration / scanCycle);
for (let i = 0; i < cycles; i++) {
tl.fromTo('[data-composition-id="background"] .scan-line',
{ y: -100 },
{
y: 1180,
duration: scanCycle,
ease: "none"
},
i * scanCycle
);
}
// 3. Ambient Opacity Pulse
// Very subtle breathing effect for the dot matrix
tl.to('[data-composition-id="background"] .dot-matrix', {
opacity: 0.6,
duration: duration / 2,
ease: "sine.inOut",
yoyo: true,
repeat: 1
}, 0);
window.__timelines["background"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,101 @@
<template id="captions-template">
<div data-composition-id="captions" data-width="1920" data-height="1080" data-duration="16.04">
<div id="captions-container"></div>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@700&display=swap');
[data-composition-id="captions"] {
width: 1920px;
height: 1080px;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
background: transparent;
}
[data-composition-id="captions"] #captions-container {
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: flex-end; /* Align to bottom */
padding-bottom: 100px; /* Offset from the very bottom edge */
}
.caption-line {
position: absolute;
bottom: 15%; /* Positioned in the bottom 3rd */
left: 50%;
transform: translateX(-50%);
width: 90%; /* Prevent bleeding */
max-width: 1728px; /* 1920 * 0.9 */
font-family: 'Space Mono', monospace;
font-weight: 700;
font-size: 64px;
color: #33FF66;
text-transform: uppercase;
text-align: center;
opacity: 0;
line-height: 1.2;
white-space: normal; /* Allow wrapping if necessary, though groups are short */
word-wrap: break-word;
}
</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.839, '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.639}, {'text': 'Sixty-two', 'start': 4.659, 'end': 5.179}, {'text': 'percent', 'start': 5.299, 'end': 5.759}, {'text': 'said', 'start': 5.859, 'end': 5.98}, {'text': 'static', 'start': 6.099, '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.479}, {'text': 'look', 'start': 10.52, '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.86}, {'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.019}];
const container = document.getElementById('captions-container');
const tl = gsap.timeline({ paused: true });
// Grouping logic based on the provided transcript and timing
// We use the transcript token directly to ensure deterministic behavior
// and sync with the actual audio.
const captionGroups = [
{ text: 'WE ASKED WHAT YOU NEEDED.', start: 0.119, end: 1.839 },
{ text: 'FORTY-SEVEN PERCENT OF YOU SAID', start: 1.839, end: 3.24 },
{ text: 'MOTION GRAPHICS.', start: 3.24, end: 4.659 },
{ text: 'SIXTY-TWO PERCENT SAID STATIC', start: 4.659, end: 6.46 },
{ text: 'CONTENT WAS COSTING YOU ATTENTION,', start: 6.46, end: 8.699 },
{ text: 'AND THREE OUT OF FOUR', start: 8.699, end: 9.84 },
{ text: 'SAID YOU KNOW THE LOOK', start: 9.84, end: 10.739 },
{ text: 'YOU WANT BUT DON\'T HAVE', start: 10.739, end: 11.96 },
{ text: 'THE EDITING SKILLS TO GET', start: 11.96, end: 13.22 },
{ text: 'THERE. SO WE BUILT EDITOR', start: 13.22, end: 15.619 },
{ text: 'AGENT', start: 15.619, end: 16.019 }
];
captionGroups.forEach((group, index) => {
const div = document.createElement('div');
div.className = 'caption-line';
div.id = `caption-${index}`;
div.textContent = group.text;
container.appendChild(div);
// Show the caption
// Using a very short duration for a "cut" effect, or 0 for instant
tl.to(div, {
opacity: 1,
duration: 0.05,
ease: "none"
}, group.start);
// Hide the caption when the next one starts, or at its end if it's the last one
const hideTime = (index < captionGroups.length - 1) ? captionGroups[index + 1].start : group.end;
tl.to(div, {
opacity: 0,
duration: 0.05,
ease: "none"
}, hideTime);
});
window.__timelines["captions"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,326 @@
<template id="data-graphics-template">
<div data-composition-id="data-graphics" data-width="1920" data-height="1080" data-duration="16.04">
<div class="grid-container">
<!-- Graphic 1: 47% NEED MOTION GRAPHICS -->
<div id="graphic-1" class="graphic-box" style="opacity: 0;">
<div class="label">47% NEED MOTION GRAPHICS</div>
<div class="progress-bar-container">
<div class="progress-bar-bg"></div>
<div id="bar-1" class="progress-bar-fill"></div>
</div>
<div id="counter-1" class="counter">00%</div>
</div>
<!-- Graphic 2: 62% LOSE ATTENTION -->
<div id="graphic-2" class="graphic-box" style="opacity: 0;">
<div class="label">62% LOSE ATTENTION</div>
<div class="chart-container">
<div class="bar-group">
<div class="bar-label">STATIC</div>
<div class="bar-wrapper"><div id="bar-2-static" class="bar-fill gray"></div></div>
</div>
<div class="bar-group">
<div class="bar-label">MOTION</div>
<div id="counter-2" class="counter small">00%</div>
<div class="bar-wrapper"><div id="bar-2-motion" class="bar-fill green"></div></div>
</div>
</div>
</div>
<!-- Graphic 3: 75% LACK SKILLS -->
<div id="graphic-3" class="graphic-box" style="opacity: 0;">
<div class="label">75% LACK SKILLS</div>
<div class="radial-container">
<div class="radial-svg-wrapper">
<svg viewBox="0 0 100 100" class="radial-svg">
<circle cx="50" cy="50" r="45" class="radial-bg" />
<circle id="radial-fill" cx="50" cy="50" r="45" class="radial-progress" />
</svg>
</div>
<div id="counter-3" class="counter">00%</div>
</div>
</div>
</div>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
[data-composition-id="data-graphics"] {
background: transparent;
font-family: 'Space Mono', monospace;
text-transform: uppercase;
color: #8A8A8A;
overflow: hidden;
width: 1920px;
height: 1080px;
}
[data-composition-id="data-graphics"] .grid-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(24, 1fr);
grid-template-rows: repeat(12, 1fr);
padding: 40px;
gap: 0; /* Removed gap for precise grid snapping */
}
[data-composition-id="data-graphics"] .graphic-box {
grid-column: 18 / 25; /* Positioned on the right side for A-roll scale down */
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(138, 138, 138, 0.4);
padding: 30px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
[data-composition-id="data-graphics"] #graphic-1 { grid-row: 2 / 5; margin-bottom: 10px; }
[data-composition-id="data-graphics"] #graphic-2 { grid-row: 5 / 8; margin-bottom: 10px; }
[data-composition-id="data-graphics"] #graphic-3 { grid-row: 8 / 11; }
[data-composition-id="data-graphics"] .label {
font-size: 20px;
font-weight: 700;
margin-bottom: 20px;
letter-spacing: 1px;
color: rgba(255, 255, 255, 0.9);
}
[data-composition-id="data-graphics"] .counter {
font-size: 48px;
font-weight: 700;
color: rgba(51, 255, 102, 0.9); /* Softened green with opacity */
margin-top: 15px;
}
[data-composition-id="data-graphics"] .counter.small {
font-size: 32px;
margin-top: 0;
margin-bottom: 10px;
}
/* Progress Bar Styles */
[data-composition-id="data-graphics"] .progress-bar-container {
width: 100%;
height: 24px;
position: relative;
background: rgba(138, 138, 138, 0.1);
border: 1px solid rgba(138, 138, 138, 0.3);
}
[data-composition-id="data-graphics"] .progress-bar-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(90deg, rgba(138, 138, 138, 0.2), rgba(138, 138, 138, 0.2) 2px, transparent 2px, transparent 12px);
}
[data-composition-id="data-graphics"] .progress-bar-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: rgba(51, 255, 102, 0.8); /* Softened green */
width: 0%;
}
/* Bar Chart Styles */
[data-composition-id="data-graphics"] .chart-container {
width: 100%;
display: flex;
gap: 30px;
height: 140px;
align-items: flex-end;
}
[data-composition-id="data-graphics"] .bar-group {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
[data-composition-id="data-graphics"] .bar-label {
font-size: 12px;
margin-bottom: 10px;
color: rgba(138, 138, 138, 0.8);
}
[data-composition-id="data-graphics"] .bar-wrapper {
width: 100%;
height: 100px;
background: rgba(138, 138, 138, 0.1);
border: 1px solid rgba(138, 138, 138, 0.3);
position: relative;
}
[data-composition-id="data-graphics"] .bar-fill {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 0%;
}
[data-composition-id="data-graphics"] .bar-fill.green { background: rgba(51, 255, 102, 0.8); }
[data-composition-id="data-graphics"] .bar-fill.gray { background: rgba(138, 138, 138, 0.5); }
/* Radial Styles */
[data-composition-id="data-graphics"] .radial-container {
width: 100%;
display: flex;
align-items: center;
gap: 40px;
}
[data-composition-id="data-graphics"] .radial-svg-wrapper {
width: 110px;
height: 110px;
transform: rotate(-90deg);
}
[data-composition-id="data-graphics"] .radial-svg {
width: 100%;
height: 100%;
}
[data-composition-id="data-graphics"] .radial-bg {
fill: none;
stroke: rgba(138, 138, 138, 0.2);
stroke-width: 10;
stroke-dasharray: 4 4;
}
[data-composition-id="data-graphics"] .radial-progress {
fill: none;
stroke: rgba(51, 255, 102, 0.8);
stroke-width: 10;
stroke-dasharray: 282.74; /* 2 * PI * 45 */
stroke-dashoffset: 282.74;
stroke-linecap: round;
}
</style>
<script>
(function() {
const tl = gsap.timeline({ paused: true });
const REVEAL_DURATION = 0.8;
const EASE = "power2.out";
const graphicSelectors = [
'[data-composition-id="data-graphics"] #graphic-1',
'[data-composition-id="data-graphics"] #graphic-2',
'[data-composition-id="data-graphics"] #graphic-3'
];
// Helper for counter animation
function animateCounter(id, target, duration, startTime) {
const obj = { val: 0 };
const el = document.querySelector(`[data-composition-id="data-graphics"] #${id}`);
if (!el) return;
tl.to(obj, {
val: target,
duration: duration,
ease: EASE,
onUpdate: () => {
el.innerText = Math.floor(obj.val).toString().padStart(2, '0') + '%';
}
}, startTime);
}
gsap.set(graphicSelectors, { x: 24 });
// 1. 47% NEED MOTION GRAPHICS at 1.839s
tl.to('[data-composition-id="data-graphics"] #graphic-1', {
opacity: 1,
x: 0,
duration: 0.5,
ease: "power2.out"
}, 1.839);
tl.to('[data-composition-id="data-graphics"] #bar-1', {
width: '47%',
duration: REVEAL_DURATION,
ease: EASE
}, 1.839 + 0.2);
animateCounter('counter-1', 47, REVEAL_DURATION, 1.839 + 0.2);
// 2. 62% LOSE ATTENTION at 4.659s
tl.to('[data-composition-id="data-graphics"] #graphic-2', {
opacity: 1,
duration: 0.5,
ease: "power2.out"
}, 4.659);
tl.to('[data-composition-id="data-graphics"] #bar-2-static', {
height: '30%',
duration: REVEAL_DURATION,
ease: EASE
}, 4.659 + 0.2);
tl.to('[data-composition-id="data-graphics"] #bar-2-motion', {
height: '62%',
duration: REVEAL_DURATION,
ease: EASE
}, 4.659 + 0.4);
animateCounter('counter-2', 62, REVEAL_DURATION, 4.659 + 0.4);
// 3. 75% LACK SKILLS at 8.88s
tl.to('[data-composition-id="data-graphics"] #graphic-3', {
opacity: 1,
duration: 0.5,
ease: "power2.out"
}, 8.88);
const circumference = 282.74;
const offset = circumference - (0.75 * circumference);
tl.to('[data-composition-id="data-graphics"] #radial-fill', {
strokeDashoffset: offset,
duration: REVEAL_DURATION,
ease: EASE
}, 8.88 + 0.2);
animateCounter('counter-3', 75, REVEAL_DURATION, 8.88 + 0.2);
// A one-way settle keeps the UI alive without repeat/yoyo timeline noise.
const greenElements = [
'[data-composition-id="data-graphics"] .progress-bar-fill',
'[data-composition-id="data-graphics"] .bar-fill.green',
'[data-composition-id="data-graphics"] .radial-progress',
'[data-composition-id="data-graphics"] .counter'
];
gsap.set(greenElements, { opacity: 0.85 });
tl.to(greenElements, {
opacity: 1,
duration: 1.2,
stagger: 0.08,
ease: "none"
}, 2);
// Outro: Fade all out near the end
tl.to(['[data-composition-id="data-graphics"] #graphic-1', '[data-composition-id="data-graphics"] #graphic-2', '[data-composition-id="data-graphics"] #graphic-3'], {
opacity: 0,
y: 20,
duration: 0.5,
ease: "power2.in"
}, 15.5);
window.__timelines["data-graphics"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,87 @@
<template id="grid-bg-template">
<div data-composition-id="grid-bg" data-width="1920" data-height="1080" data-duration="19.04">
<div class="bg-container">
<div class="grid-overlay"></div>
<div class="scan-line"></div>
</div>
<style>
[data-composition-id="grid-bg"] .bg-container {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
background-color: #0A0A0A;
overflow: hidden;
}
[data-composition-id="grid-bg"] .grid-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* 40px grid pattern using linear-gradients */
background-image:
linear-gradient(to right, rgba(138, 138, 138, 0.1) 1px, transparent 1px),
linear-gradient(to bottom, rgba(138, 138, 138, 0.1) 1px, transparent 1px);
background-size: 40px 40px;
transform-origin: center center;
}
[data-composition-id="grid-bg"] .scan-line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background-color: #33FF66;
opacity: 0.2;
box-shadow: 0 0 10px 1px rgba(51, 255, 102, 0.3);
transform: translateY(-10px);
}
</style>
<script>
(function() {
const tl = gsap.timeline({ paused: true });
const totalDuration = 19.04;
const scanLineSelector = '[data-composition-id="grid-bg"] .scan-line';
const gridSelector = '[data-composition-id="grid-bg"] .grid-overlay';
const halfDuration = totalDuration / 2;
gsap.set(scanLineSelector, { y: -20 });
gsap.set(gridSelector, { scale: 1, opacity: 0.9 });
// 1. Single continuous scan keeps the texture moving without stacked fromTo loops.
tl.to(scanLineSelector, {
y: 1100,
duration: totalDuration,
ease: "none"
}, 0);
// 2. Gentle scale breathe across the full composition window.
tl.to(gridSelector, {
scale: 1.02,
duration: halfDuration,
ease: "sine.inOut"
}, 0);
tl.to(gridSelector, {
scale: 1,
duration: totalDuration - halfDuration,
ease: "sine.inOut"
}, halfDuration);
// 3. One-way opacity settle keeps the grid alive without repeat/yoyo artifacts.
tl.to(gridSelector, {
opacity: 1,
duration: 1.2,
ease: "none"
}, 0);
window.__timelines["grid-bg"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,138 @@
<template id="intro-seq-template">
<div data-composition-id="intro-seq" data-width="1920" data-height="1080" data-duration="3">
<div class="terminal-container">
<div id="line1" class="terminal-line"></div>
<div id="line2" class="terminal-line"></div>
</div>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
[data-composition-id="intro-seq"] {
background-color: #0A0A0A;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Space Mono', monospace;
width: 100%;
height: 100%;
color: #33FF66;
}
[data-composition-id="intro-seq"] .terminal-container {
display: flex;
flex-direction: column;
gap: 30px;
width: 80%;
max-width: 1200px;
}
[data-composition-id="intro-seq"] .terminal-line {
font-size: 72px;
font-weight: 700;
text-transform: uppercase;
min-height: 1.2em;
white-space: pre;
text-shadow: 0 0 10px rgba(51, 255, 102, 0.6), 0 0 20px rgba(51, 255, 102, 0.2);
display: flex;
align-items: center;
}
[data-composition-id="intro-seq"] .char {
opacity: 0;
display: inline-block;
}
[data-composition-id="intro-seq"] .cursor {
display: inline-block;
width: 0.6em;
height: 1.1em;
background-color: #33FF66;
margin-left: 5px;
box-shadow: 0 0 10px rgba(51, 255, 102, 0.8);
opacity: 0;
}
</style>
<script>
(function() {
const tl = gsap.timeline({ paused: true });
const line1Text = 'SYSTEM BOOTING...';
const line2Text = 'EDITOR AGENT v1.0';
const charDuration = 0.05;
const blinkDuration = 0.1;
const line1El = document.getElementById('line1');
const line2El = document.getElementById('line2');
function createLine(el, text, startTime) {
// Clear and create spans
el.innerHTML = '';
const chars = text.split('').map(char => {
const span = document.createElement('span');
span.className = 'char';
span.textContent = char === ' ' ? '\u00A0' : char;
el.appendChild(span);
return span;
});
const cursor = document.createElement('div');
cursor.className = 'cursor';
el.appendChild(cursor);
tl.set(cursor, {
display: 'inline-block',
opacity: 1
}, startTime);
// Animate characters
chars.forEach((char, i) => {
const time = startTime + (i * charDuration);
tl.to(char, {
opacity: 1,
duration: 0.01,
ease: "none"
}, time);
});
// Give the cursor a short deterministic settle instead of a repeat/yoyo blink.
const lineEndTime = startTime + (chars.length * charDuration);
tl.to(cursor, {
opacity: 0.25,
duration: blinkDuration,
ease: "none"
}, lineEndTime);
tl.to(cursor, {
opacity: 1,
duration: blinkDuration,
ease: "none"
}, lineEndTime + blinkDuration);
tl.set(cursor, {
opacity: 0
}, lineEndTime + (blinkDuration * 2));
return lineEndTime + 0.5;
}
// Line 1
const line1End = createLine(line1El, line1Text, 0.2);
// Line 2
createLine(line2El, line2Text, line1End);
// A subtle one-way settle keeps the intro alive without repeat/yoyo seek edge cases.
tl.fromTo('[data-composition-id="intro-seq"] .terminal-container', {
opacity: 0.92
}, {
opacity: 1,
duration: 0.6,
ease: "none"
}, 0);
window.__timelines["intro-seq"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,88 @@
<template id="intro-template">
<div data-composition-id="intro" data-width="1920" data-height="1080" data-duration="3">
<div class="terminal-container">
<div id="terminal-text" class="terminal-text"></div>
</div>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap');
[data-composition-id="intro"] {
background: transparent;
display: flex;
justify-content: center;
align-items: center;
font-family: 'JetBrains Mono', monospace;
}
[data-composition-id="intro"] .terminal-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
[data-composition-id="intro"] .terminal-text {
color: #FFFFFF;
font-size: 72px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 6px;
white-space: pre;
/* Subtle glow: #33FF66 at 0.2 opacity */
text-shadow: 0 0 15px rgba(51, 255, 102, 0.2);
display: flex;
}
[data-composition-id="intro"] .char {
opacity: 0;
display: inline-block;
}
</style>
<script>
(function() {
const text = "SYSTEM BOOT: EDITOR AGENT";
const container = document.getElementById('terminal-text');
// Clear and create character spans for precise animation
container.innerHTML = '';
const chars = text.split('').map(char => {
const span = document.createElement('span');
span.className = 'char';
span.textContent = char === ' ' ? '\u00A0' : char; // Use non-breaking space for layout
container.appendChild(span);
return span;
});
const charDuration = 0.04; // 40ms per character
const tl = gsap.timeline({ paused: true });
// Character-by-character reveal
// Using stagger for a more modern, sophisticated implementation
tl.to(chars, {
opacity: 1,
duration: 0.01, // Instant reveal per character
stagger: {
each: charDuration,
ease: "none" // Linear sequence
}
}, 0);
// Add a subtle "flicker" or "pulse" to the glow for sophistication
// This adds the "2-3 simultaneous motion sources" requirement
tl.to(container, {
textShadow: "0 0 25px rgba(51, 255, 102, 0.4)",
duration: 1.5,
ease: "sine.inOut",
yoyo: true,
repeat: 1 // Finite repeat to fit 3s duration
}, 0);
// Register the timeline
window.__timelines["intro"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,145 @@
<template id="main-template">
<div data-composition-id="main" data-width="1920" data-height="1080" data-duration="16.04">
<!-- A-roll Video -->
<video id="aroll"
src="https://gen-os-static.s3.us-east-2.amazonaws.com/astral_assets/uploaded_assets/008477bc_391124feb22641f19fd463e5aeb1aeef.mp4"
data-start="0"
data-duration="16.04"
data-track-index="1">
</video>
<!-- Captions Container -->
<div id="captions-container"></div>
<style>
[data-composition-id="main"] {
background-color: transparent;
width: 1920px;
height: 1080px;
position: relative;
overflow: hidden;
}
[data-composition-id="main"] #aroll {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 4px;
object-fit: cover;
width: 1920px;
height: 1080px;
/* Initial state for wipe reveal */
clip-path: inset(0 100% 0 0);
}
[data-composition-id="main"] #captions-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
z-index: 10;
}
.caption-line {
font-family: 'JetBrains Mono', monospace;
font-weight: bold;
font-size: 64px;
color: #FFFFFF;
text-transform: uppercase;
text-align: center;
opacity: 0;
position: absolute;
width: 100%;
padding: 0 100px;
/* Terminal aesthetic start position */
transform: translateY(10px);
}
</style>
<script>
(function() {
const tl = gsap.timeline({ paused: true });
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.839, '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.639}, {'text': 'Sixty-two', 'start': 4.659, 'end': 5.179}, {'text': 'percent', 'start': 5.299, 'end': 5.759}, {'text': 'said', 'start': 5.859, 'end': 5.98}, {'text': 'static', 'start': 6.099, '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.479}, {'text': 'look', 'start': 10.52, '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.86}, {'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.019}];
const TRANSITION_DURATION = 0.25; // 250ms
const container = document.querySelector('[data-composition-id="main"]');
const aroll = container.querySelector('#aroll');
const captionsContainer = container.querySelector('#captions-container');
// --- A-roll Choreography ---
// 1. At 0.0s (relative to composition start at 3s): A-roll enters with a clean left-to-right wipe reveal.
tl.to(aroll, {
clipPath: 'inset(0 0% 0 0)',
duration: TRANSITION_DURATION,
ease: 'none'
}, 0.0);
// 2. At 1.839s (relative to composition start at 3s): A-roll scales down and shifts.
// (4.839 - 3 = 1.839)
tl.to(aroll, {
scale: 0.6,
x: -384,
duration: TRANSITION_DURATION,
ease: 'none'
}, 1.839);
// 3. At 14.239s (relative to composition start at 3s): A-roll scales back up and recenters.
// (17.239 - 3 = 14.239)
tl.to(aroll, {
scale: 1,
x: 0,
duration: TRANSITION_DURATION,
ease: 'none'
}, 14.239);
// --- Captions Logic ---
function groupTranscript(transcript, maxWords) {
const groups = [];
for (let i = 0; i < transcript.length; i += maxWords) {
groups.push(transcript.slice(i, i + maxWords));
}
return groups;
}
const captionGroups = groupTranscript(TRANSCRIPT, 5);
captionGroups.forEach((group, index) => {
const startTime = group[0].start;
// Stay visible until the next group replaces them.
const endTime = (index < captionGroups.length - 1)
? captionGroups[index + 1][0].start
: 16.04; // End of composition
const text = group.map(w => w.text).join(' ');
const el = document.createElement('div');
el.className = 'caption-line';
el.innerText = text;
captionsContainer.appendChild(el);
// Caption transitions: slight vertical shift
tl.to(el, {
opacity: 1,
y: 0,
duration: 0.2,
ease: 'none'
}, startTime);
// Hide caption when next one starts
if (index < captionGroups.length - 1) {
tl.set(el, { opacity: 0 }, endTime);
}
});
window.__timelines["main"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,215 @@
<template id="stats-template">
<div data-composition-id="stats" data-width="1920" data-height="1080" data-duration="16.04">
<div class="grid-container">
<!-- Stat 1: Horizontal Progress Bar -->
<div id="stat-1" class="stat-box">
<div class="stat-header">
<div class="stat-value" id="val-1">0%</div>
<div class="stat-label">NEED MOTION GRAPHICS</div>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="fill-1"></div>
</div>
</div>
<!-- Stat 2: Circular Progress Ring (Sharp Segments) -->
<div id="stat-2" class="stat-box">
<div class="stat-header">
<div class="stat-value" id="val-2">0%</div>
<div class="stat-label">STATIC CONTENT LOSES ATTENTION</div>
</div>
<div class="progress-ring-container">
<svg viewBox="0 0 200 200" class="progress-ring">
<circle class="ring-bg" cx="100" cy="100" r="80" />
<circle class="ring-fill" id="fill-2" cx="100" cy="100" r="80" />
</svg>
</div>
</div>
<!-- Stat 3: Vertical Bar Chart -->
<div id="stat-3" class="stat-box">
<div class="stat-header">
<div class="stat-value" id="val-3">0%</div>
<div class="stat-label">LACK EDITING SKILLS</div>
</div>
<div class="bar-chart-container">
<div class="bar-chart-fill" id="fill-3"></div>
</div>
</div>
</div>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;800&display=swap');
[data-composition-id="stats"] {
font-family: 'JetBrains Mono', monospace;
text-transform: uppercase;
color: #FFFFFF;
background: transparent;
width: 1920px;
height: 1080px;
position: relative;
overflow: hidden;
}
[data-composition-id="stats"] .grid-container {
position: absolute;
right: 100px; /* Positioned on the RIGHT side */
top: 0;
width: 800px;
height: 1080px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 120px;
}
[data-composition-id="stats"] .stat-box {
opacity: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
transform: translateX(20px); /* Initial slide-in position */
}
[data-composition-id="stats"] .stat-header {
display: flex;
align-items: baseline;
gap: 20px;
margin-bottom: 20px;
}
[data-composition-id="stats"] .stat-value {
font-size: 80px;
font-weight: 800;
color: #FFFFFF;
line-height: 1;
}
[data-composition-id="stats"] .stat-label {
font-size: 32px;
font-weight: 400;
color: #8A8A8A;
letter-spacing: 1px;
max-width: 500px;
}
/* Stat 1: Horizontal Bar */
[data-composition-id="stats"] .progress-bar-container {
width: 600px;
height: 12px;
background: #0A0A0A;
}
[data-composition-id="stats"] .progress-bar-fill {
width: 0%;
height: 100%;
background: #FFFFFF;
}
/* Stat 2: Circular Ring */
[data-composition-id="stats"] .progress-ring-container {
width: 240px;
height: 240px;
}
[data-composition-id="stats"] .progress-ring {
transform: rotate(-90deg);
}
[data-composition-id="stats"] .ring-bg {
fill: none;
stroke: #0A0A0A;
stroke-width: 24;
}
[data-composition-id="stats"] .ring-fill {
fill: none;
stroke: #FFFFFF;
stroke-width: 24;
stroke-dasharray: 502.65; /* 2 * PI * 80 */
stroke-dashoffset: 502.65;
stroke-linecap: butt; /* Sharp segments */
}
/* Stat 3: Vertical Bar */
[data-composition-id="stats"] .bar-chart-container {
width: 60px;
height: 240px;
background: #0A0A0A;
position: relative;
}
[data-composition-id="stats"] .bar-chart-fill {
width: 100%;
height: 0%;
background: #FFFFFF;
position: absolute;
bottom: 0;
}
</style>
<script>
(function() {
const tl = gsap.timeline({ paused: true });
// Timestamps (relative to composition start at 3s)
const T1 = 1.839; // 4.839 - 3
const T2 = 4.659; // 7.659 - 3
const T3 = 8.88; // 11.88 - 3
const END = 16.04; // 19.04 - 3
const FADE_DUR = 0.2;
const SLIDE_DUR = 0.3;
const FILL_DUR = 0.3;
// Helper for number animation
function animateNumber(id, targetVal, startTime) {
const obj = { val: 0 };
tl.to(obj, {
val: targetVal,
duration: FILL_DUR,
ease: 'none',
onUpdate: function() {
const valEl = document.getElementById(id);
if (valEl) valEl.innerText = Math.floor(obj.val) + '%';
}
}, startTime);
}
// Stat 1 Animation
tl.to('#stat-1', { opacity: 1, duration: FADE_DUR, ease: 'none' }, T1);
tl.to('#stat-1', { x: 0, duration: SLIDE_DUR, ease: 'none' }, T1);
tl.to('#fill-1', { width: '47%', duration: FILL_DUR, ease: 'none' }, T1);
animateNumber('val-1', 47, T1);
// Hide Stat 1 when Stat 2 starts
tl.to('#stat-1', { opacity: 0, x: -20, duration: FADE_DUR, ease: 'none' }, T2);
// Stat 2 Animation
const ringCircumference = 2 * Math.PI * 80;
tl.to('#stat-2', { opacity: 1, duration: FADE_DUR, ease: 'none' }, T2);
tl.to('#stat-2', { x: 0, duration: SLIDE_DUR, ease: 'none' }, T2);
tl.to('#fill-2', {
strokeDashoffset: ringCircumference * (1 - 0.62),
duration: FILL_DUR,
ease: 'none'
}, T2);
animateNumber('val-2', 62, T2);
// Hide Stat 2 when Stat 3 starts
tl.to('#stat-2', { opacity: 0, x: -20, duration: FADE_DUR, ease: 'none' }, T3);
// Stat 3 Animation
tl.to('#stat-3', { opacity: 1, duration: FADE_DUR, ease: 'none' }, T3);
tl.to('#stat-3', { x: 0, duration: SLIDE_DUR, ease: 'none' }, T3);
tl.to('#fill-3', { height: '75%', duration: FILL_DUR, ease: 'none' }, T3);
animateNumber('val-3', 75, T3);
// Hide Stat 3 at the end
tl.to('#stat-3', { opacity: 0, x: -20, duration: FADE_DUR, ease: 'none' }, END - 0.5);
window.__timelines["stats"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,58 @@
# HyperFrames Design Review
## First Impression
This looks like a "hacker" template from a 2014 stock asset site. Its trying so hard to be "digital" and "techy" that it forgets to be actually sophisticated. The neon green on black is the most tired trope in the industry.
---
## CRITICAL Design Failures
### The "Matrix" Color Palette
**Where:** Global (`#33FF66` on `#0A0A0A`)
**What's wrong:** Using pure neon green on a near-black background is lazy. Its high-contrast in the worst way—it causes eye strain and looks like a parody of a terminal.
**Why it matters:** It lacks professional polish. Real high-end tech brands (like Stripe, Vercel, or even Apples developer materials) use more nuanced greens, subtle gradients, and varied opacities to create depth. This is just flat and aggressive.
**Fix it:** Shift the green to a more sophisticated mint or emerald (e.g., `#00FF9F` or `#4ADE80`). Introduce a secondary accent color like a deep slate blue or a muted purple to break the monotony.
### Typography Hierarchy is Non-Existent
**Where:** `compositions/captions.html` and `compositions/data-graphics.html`
**What's wrong:** Everything is screaming. The captions are 64px, the terminal lines are 72px, and the data counters are 48px. All of them are in 'Space Mono' or 'Courier New'.
**Why it matters:** When everything is bold, uppercase, and huge, nothing is important. The viewer doesn't know where to look. It feels like being shouted at by a calculator.
**Fix it:** Use a variable font. Mix weights. Make the labels in the data graphics much smaller (12-14px) and lighter. Use a clean sans-serif (like Inter or Geist) for the main captions and keep the mono font for the "data" elements only.
### The "Floating Box" Syndrome
**Where:** `compositions/data-graphics.html`
**What's wrong:** The data boxes are just floating on the right with a generic `backdrop-filter: blur(10px)`. They have no relationship to the grid behind them or the video next to them.
**Why it matters:** It looks like a UI overlay from a cheap mobile game. Theres no sense of "physicality" or integrated design.
**Fix it:** Align the boxes strictly to the background grid lines. Use "connector" lines or brackets that "anchor" the data to the video frame. Make the borders thinner (0.5px) and use multiple layers of subtle shadows to create real depth.
---
## Design Improvements
### Robotic Motion
**Where:** `index.html` (A-roll transitions)
**The problem:** The scale and position shifts of the video (`scale: 0.6`, `x: -400`) are basic linear-feeling movements. The "wipe" reveal is a standard `clip-path` that feels like a default PowerPoint transition.
**Make it better:** Use more aggressive easing (e.g., `expo.out` or `custom-elastic`). Add a slight "overshoot" to the scaling. When the video moves, maybe it should have a slight tilt or a "glitch" chromatic aberration effect during the transition to match the "system" theme.
### Static Grid Background
**Where:** `compositions/grid-bg.html`
**The problem:** A 40px grid is fine, but its just... there. The "scan line" is a single 2px bar moving top to bottom. Its predictable and boring.
**Make it better:** Add "data noise"—tiny flickering pixels, coordinate numbers that change in the corners of the grid, or subtle "interference" patterns. Make the grid lines vary in opacity so it doesn't look like a math notebook.
---
## What Actually Works
### The Terminal Boot Sequence
The `intro-seq.html` actually has a decent rhythm. The character-by-character reveal with the slight "blink" on the current character is a nice touch. Its the only part of the composition that feels like it had some thought put into the *feeling* of the animation rather than just the mechanics.
---
## Design Verdict
**Visual Impact:** 3/10 - Its a cliché "tech" look that fails to stand out.
**Color & Typography:** 2/10 - Monospaced fonts and neon green are the "Hello World" of design.
**Motion & Animation Feel:** 4/10 - Functional but lacks the "snap" and "juice" of professional motion design.
**Overall Aesthetic:** 3/10 - Amateurish. It looks like a developer's first attempt at "cool" UI.
**Bottom Line:** This needs a complete stylistic overhaul. Stop trying to look like a 90s movie version of a "hacker" and start looking like a modern, high-end SaaS product. Kill the neon green, fix the typography, and give the layout some room to breathe.
@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Digital Grid — Wim Crouwel 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;
background-color: #0A0A0A;
overflow: hidden;
font-family: 'Courier New', Courier, monospace;
}
#master-composition {
position: relative;
width: 1920px;
height: 1080px;
}
#aroll-video {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 1920px;
height: 1080px;
border-radius: 4px;
object-fit: cover;
clip-path: inset(0 100% 0 0); /* For left-to-right wipe */
}
</style>
</head>
<body>
<div id="master-composition" data-composition-id="master" data-width="1920" data-height="1080" data-duration="19.04">
<!-- Persistent Grid Background -->
<div id="grid-bg-comp"
data-composition-id="grid-bg"
data-composition-src="compositions/grid-bg.html"
data-start="0"
data-track-index="0">
</div>
<!-- Intro Sequence (0-3s) -->
<div id="intro-seq-comp"
data-composition-id="intro-seq"
data-composition-src="compositions/intro-seq.html"
data-start="0"
data-track-index="1"
data-duration="3">
</div>
<!-- Data Graphics Layer -->
<div id="data-graphics-comp"
data-composition-id="data-graphics"
data-composition-src="compositions/data-graphics.html"
data-start="3"
data-track-index="2">
</div>
<!-- A-roll Video (Starts after intro) -->
<video id="aroll-video"
data-start="3"
data-duration="16.04"
data-track-index="3"
src="https://gen-os-static.s3.us-east-2.amazonaws.com/astral_assets/uploaded_assets/3053bdbd_3b610ff665ee4d11858cba59dbf47b8c.mp4">
</video>
<!-- Captions Layer -->
<div id="captions-comp"
data-composition-id="captions"
data-composition-src="compositions/captions.html"
data-start="3"
data-track-index="4">
</div>
</div>
<script>
(function() {
const tl = gsap.timeline({ paused: true });
const video = document.getElementById('aroll-video');
// 1. A-roll entrance: Clean left-to-right wipe reveal at 3s
tl.to(video, {
clipPath: 'inset(0 0% 0 0)',
duration: 0.5,
ease: "power2.inOut"
}, 3);
// 2. First stat (47%) at 3 + 1.839 = 4.839s
// Trigger: A-roll scales down and shifts to left
tl.to(video, {
scale: 0.6,
x: -400,
duration: 0.4,
ease: "power2.inOut"
}, 4.839);
// 3. Second stat (62%) at 3 + 4.659 = 7.659s
// Stays scaled down
// 4. Next major narrative beat (75% lack skills) at 3 + 13.22 = 16.22s
// Trigger: A-roll scales back up to 100% and recenters after "editing skills to get there"
tl.to(video, {
scale: 1,
x: 0,
duration: 0.4,
ease: "power2.inOut"
}, 16.22);
// 5. Final beat (Editor Agent) at 3 + 14.22 = 17.22s
// Scale down again for final reveal? Or stay centered.
// Prompt says: "When the next major narrative beat begins: A-roll scales back up to 100% and recenters."
// "A-roll remains visible and on screen through the end of the video."
window.__timelines["master"] = tl;
})();
</script>
</body>
</html>