mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 23:29:50 +00:00
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:
co-authored by
Claude Opus 4.6
parent
10621e7903
commit
9f8e5ba5a1
@@ -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. It’s 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. It’s 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 Apple’s 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. There’s 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 it’s just... there. The "scan line" is a single 2px bar moving top to bottom. It’s 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. It’s 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 - It’s 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>
|
||||
Reference in New Issue
Block a user