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,62 @@
# HyperFrame Schema Compliance Review
## Executive Summary
- Total files reviewed: 6
- Critical issues: 0
- Overall compliance status: PASS
## Critical Issues
None found.
## 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
- [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
### index.html
**Status**: COMPLIANT
**Issues Found**:
- None.
### compositions/background.html
**Status**: COMPLIANT
**Issues Found**:
- None.
### compositions/captions.html
**Status**: COMPLIANT
**Issues Found**:
- None.
### compositions/aroll.html
**Status**: COMPLIANT
**Issues Found**:
- None.
### compositions/mograph_overlays.html
**Status**: COMPLIANT
**Issues Found**:
- None.
### compositions/hero_moment.html
**Status**: COMPLIANT
**Issues Found**:
- None.
@@ -0,0 +1,188 @@
<template id="aroll-template">
<div data-composition-id="aroll" data-width="1080" data-height="1920" data-duration="14">
<!-- Grid Background -->
<div class="grid-container">
<div class="grid-line horizontal" style="top: 20%;"></div>
<div class="grid-line horizontal" style="top: 40%;"></div>
<div class="grid-line horizontal" style="top: 60%;"></div>
<div class="grid-line horizontal" style="top: 80%;"></div>
<div class="grid-line vertical" style="left: 20%;"></div>
<div class="grid-line vertical" style="left: 40%;"></div>
<div class="grid-line vertical" style="left: 60%;"></div>
<div class="grid-line vertical" style="left: 80%;"></div>
</div>
<!-- Video Container -->
<div class="video-frame">
<div class="video-mask">
<video
id="aroll-video"
src="https://gen-os-static.s3.us-east-2.amazonaws.com/astral_assets/uploaded_assets/c10d5d5c_5e0cbe68f361481f8d2c77b028e84504.mp4"
data-start="0"
data-duration="14"
data-track-index="0"
crossorigin="anonymous"
></video>
</div>
<!-- Diagonal Rule Lines for Wipe Effect -->
<div class="wipe-line wipe-line-1"></div>
<div class="wipe-line wipe-line-2"></div>
</div>
<style>
[data-composition-id="aroll"] {
background: transparent;
font-family: sans-serif;
overflow: hidden;
width: 1080px;
height: 1920px;
position: relative;
}
/* Grid Styling */
[data-composition-id="aroll"] .grid-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.2;
pointer-events: none;
}
[data-composition-id="aroll"] .grid-line {
position: absolute;
background: #444;
}
[data-composition-id="aroll"] .grid-line.horizontal {
width: 100%;
height: 1px;
}
[data-composition-id="aroll"] .grid-line.vertical {
width: 1px;
height: 100%;
}
/* Video Frame Styling */
[data-composition-id="aroll"] .video-frame {
position: absolute;
/* Full width and height for impact, no padding */
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #111;
overflow: hidden;
border: none;
transform: translate(0, 0) scale(1);
z-index: 10;
}
[data-composition-id="aroll"] .video-mask {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
[data-composition-id="aroll"] video {
width: 100%;
height: 100%;
object-fit: cover; /* Crop portrait video into the frame */
display: block;
}
/* Wipe Lines */
[data-composition-id="aroll"] .wipe-line {
position: absolute;
background: #fff;
z-index: 10;
pointer-events: none;
width: 200%;
height: 4px;
left: -50%;
opacity: 0;
}
[data-composition-id="aroll"] .wipe-line-1 {
top: 0;
transform: rotate(45deg) translateY(-500px);
}
[data-composition-id="aroll"] .wipe-line-2 {
bottom: 0;
transform: rotate(45deg) translateY(500px);
}
</style>
<script>
(function() {
const tl = gsap.timeline({ paused: true });
const frame = document.querySelector('[data-composition-id="aroll"] .video-frame');
const wipe1 = document.querySelector('[data-composition-id="aroll"] .wipe-line-1');
const wipe2 = document.querySelector('[data-composition-id="aroll"] .wipe-line-2');
// 1. Entrance & Scale Down (0s - 1.5s)
// Starts full screen and scales down slightly to establish the grid
tl.fromTo(frame, {
scale: 1.1,
opacity: 0
}, {
scale: 1.0, /* Fixed scale to fill frame better */
opacity: 1,
duration: 1.5,
ease: 'power4.out'
}, 0);
tl.fromTo(wipe1,
{ opacity: 1, y: -500 },
{ y: 1500, duration: 1.5, ease: 'power2.inOut' },
0
);
tl.fromTo(wipe2,
{ opacity: 1, y: 500 },
{ y: -1500, duration: 1.5, ease: 'power2.inOut' },
0.1
);
tl.to([wipe1, wipe2], { opacity: 0, duration: 0.3 }, 1.5);
// 2. Ambient Motion (1.5s - 7.319s)
tl.to(frame, {
y: '+=15',
duration: 3,
ease: 'sine.inOut',
repeat: 1,
yoyo: true
}, 1.5);
// 3. Hero Moment Exit (7.319s - 8.319s)
tl.to(frame, {
scale: 0,
opacity: 0,
duration: 0.8,
ease: 'power3.in'
}, 7.319);
// 4. Hero Moment Return (12.239s - 13.739s)
tl.to(frame, {
scale: 1.0, /* Fixed return scale */
opacity: 1,
duration: 1.2,
ease: 'back.out(1.2)'
}, 12.239);
// 5. Final Exit (13.8s - 14s)
tl.to(frame, {
opacity: 0,
scale: 0.6,
duration: 0.5,
ease: 'power2.in'
}, 13.5);
window.__timelines["aroll"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,144 @@
<template id="background-template">
<div data-composition-id="background" data-width="1080" data-height="1920" data-duration="14">
<div class="paper-texture"></div>
<!-- Grid Lines Container -->
<div class="grid-container">
<!-- Horizontal Lines -->
<div class="line horizontal h1" style="top: 15%; height: 2px;"></div>
<div class="line horizontal h2" style="top: 45%; height: 6px;"></div>
<div class="line horizontal h3" style="top: 85%; height: 1px;"></div>
<!-- Vertical Lines -->
<div class="line vertical v1" style="left: 20%; width: 4px;"></div>
<div class="line vertical v2" style="left: 65%; width: 2px;"></div>
<div class="line vertical v3" style="left: 80%; width: 8px;"></div>
</div>
<!-- Bauhaus Red Circle -->
<div class="circle-element"></div>
<style>
[data-composition-id="background"] {
width: 1080px;
height: 1920px;
background-color: transparent;
position: relative;
overflow: hidden;
font-family: 'Helvetica', 'Arial', sans-serif;
}
/* Subtle Paper Texture */
[data-composition-id="background"] .paper-texture {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.1; /* Increased for tactile feel */
pointer-events: none;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
}
[data-composition-id="background"] .grid-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
[data-composition-id="background"] .line {
position: absolute;
background-color: #000000;
}
[data-composition-id="background"] .horizontal {
width: 100%;
left: 0;
transform: scaleX(0);
transform-origin: left center;
}
[data-composition-id="background"] .vertical {
height: 100%;
top: 0;
transform: scaleY(0);
transform-origin: center top;
}
[data-composition-id="background"] .circle-element {
position: absolute;
width: 400px;
height: 400px;
background-color: #BE1E2D;
border-radius: 50%;
top: 25%;
left: 60%;
transform: scale(0);
z-index: 10;
}
</style>
<script>
(function() {
const tl = gsap.timeline({ paused: true });
// 1. Grid Lines Animation (0s - 1s)
// Staggered entrance for horizontal lines
tl.to('[data-composition-id="background"] .horizontal', {
scaleX: 1,
duration: 0.8,
stagger: 0.15,
ease: "power4.inOut"
}, 0);
// Staggered entrance for vertical lines
tl.to('[data-composition-id="background"] .vertical', {
scaleY: 1,
duration: 0.8,
stagger: 0.1,
ease: "power4.inOut"
}, 0.2);
// 2. Circular Element Animation (0.5s - 1.2s)
tl.to('[data-composition-id="background"] .circle-element', {
scale: 1,
duration: 0.7,
ease: "circ.out"
}, 0.5);
// 3. Ambient Motion (1s - 14s)
// Subtle drift for the circle
tl.to('[data-composition-id="background"] .circle-element', {
x: -30,
y: 20,
duration: 6.5,
yoyo: true,
repeat: 1,
ease: "sine.inOut"
}, 1);
// Subtle weight shift for lines (simulating paper expansion/contraction or focus shift)
tl.to('[data-composition-id="background"] .h2', {
height: "8px",
duration: 6,
yoyo: true,
repeat: 1,
ease: "sine.inOut"
}, 1);
tl.to('[data-composition-id="background"] .v3', {
width: "12px",
duration: 5,
yoyo: true,
repeat: 1,
ease: "sine.inOut"
}, 3);
// Register the timeline
window.__timelines["background"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,187 @@
<template id="captions-template">
<div data-composition-id="captions" data-width="1080" data-height="1920" data-duration="14">
<div class="captions-container">
<!-- Groups will be injected here -->
</div>
<style>
[data-composition-id="captions"] {
background: transparent;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; /* Akzidenz-Grotesk/Futura vibe */
color: #000000;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
overflow: hidden;
}
[data-composition-id="captions"] .captions-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-end; /* Align to bottom */
padding-bottom: 30%; /* Position 30% from bottom of screen */
}
[data-composition-id="captions"] .caption-group {
position: absolute;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
width: 85%; /* Slightly narrower to force wrapping */
height: 250px; /* Reduced height to strictly enforce 2 lines */
overflow: hidden; /* Enforce at most 2 lines */
opacity: 0;
visibility: hidden;
text-transform: uppercase; /* Tschichold often used uppercase for impact */
letter-spacing: -0.02em;
line-height: 1.1; /* Increased line height for breathing room */
text-align: center;
}
[data-composition-id="captions"] .word {
display: inline-block;
margin: 5px 15px; /* Added vertical margin */
font-size: 80px; /* Supporting words standardized */
font-weight: 300; /* Lighter weight for supporting words */
white-space: nowrap;
letter-spacing: -0.04em;
}
/* Extreme scale and weight for keywords */
[data-composition-id="captions"] .word.bold {
font-size: 120px; /* Reduced scale variance for better fit */
font-weight: 900;
color: #BE1E2D; /* Bauhaus Red for emphasis */
}
/* Extreme weight for emphasis on keywords */
[data-composition-id="captions"] .word.bold {
font-weight: 900;
}
/* Grid rail lines (decorative Tschichold elements) */
[data-composition-id="captions"] .rail {
position: absolute;
background: #000000;
opacity: 0.05;
pointer-events: none;
}
[data-composition-id="captions"] .rail-h {
width: 100%;
height: 2px;
left: 0;
}
[data-composition-id="captions"] .rail-v {
width: 2px;
height: 100%;
top: 0;
}
</style>
<script>
(function() {
const TRANSCRIPT = [{'text': 'We', 'start': 0.14, 'end': 0.239}, {'text': 'asked', 'start': 0.28, 'end': 0.459}, {'text': 'what', 'start': 0.5, 'end': 0.619}, {'text': 'you', 'start': 0.659, 'end': 0.779}, {'text': 'needed.', 'start': 0.8, 'end': 1.179}, {'text': 'Forty-seven', 'start': 1.199, 'end': 1.619}, {'text': 'percent', 'start': 1.699, 'end': 1.96}, {'text': 'of', 'start': 1.979, 'end': 2.059}, {'text': 'you', 'start': 2.099, 'end': 2.299}, {'text': 'said', 'start': 2.319, 'end': 2.499}, {'text': 'motion', 'start': 2.559, 'end': 2.839}, {'text': 'graphics,', 'start': 2.899, 'end': 3.759}, {'text': 'sixty-two', 'start': 3.799, 'end': 4.159}, {'text': 'percent', 'start': 4.239, 'end': 4.559}, {'text': 'said', 'start': 4.639, 'end': 4.799}, {'text': 'static', 'start': 4.859, 'end': 5.199}, {'text': 'content', 'start': 5.239, 'end': 5.639}, {'text': 'was', 'start': 5.679, 'end': 5.779}, {'text': 'costing', 'start': 5.859, 'end': 6.219}, {'text': 'you', 'start': 6.259, 'end': 6.339}, {'text': 'attention,', 'start': 6.379, 'end': 7.299}, {'text': 'and', 'start': 7.319, 'end': 7.439}, {'text': 'three', 'start': 7.48, 'end': 7.599}, {'text': 'out', 'start': 7.679, 'end': 7.779}, {'text': 'of', 'start': 7.799, 'end': 7.879}, {'text': 'four', 'start': 7.94, 'end': 8.139}, {'text': 'said', 'start': 8.279, 'end': 8.46}, {'text': 'you', 'start': 8.519, 'end': 8.6}, {'text': 'know', 'start': 8.619, 'end': 8.739}, {'text': 'the', 'start': 8.8, 'end': 8.88}, {'text': 'look', 'start': 8.92, 'end': 9.079}, {'text': 'you', 'start': 9.119, 'end': 9.259}, {'text': 'want', 'start': 9.279, 'end': 9.619}, {'text': 'but', 'start': 9.779, 'end': 9.88}, {'text': "don't", 'start': 9.92, 'end': 10.1}, {'text': 'have', 'start': 10.159, 'end': 10.3}, {'text': 'the', 'start': 10.3, 'end': 10.42}, {'text': 'editing', 'start': 10.5, 'end': 10.779}, {'text': 'skills', 'start': 10.86, 'end': 11.139}, {'text': 'to', 'start': 11.159, 'end': 11.239}, {'text': 'get', 'start': 11.279, 'end': 11.439}, {'text': 'there.', 'start': 11.46, 'end': 12.239}, {'text': 'So', 'start': 12.3, 'end': 12.38}, {'text': 'we', 'start': 12.399, 'end': 12.519}, {'text': 'built', 'start': 12.539, 'end': 12.759}, {'text': 'Editor', 'start': 12.84, 'end': 13.119}, {'text': 'Agent.', 'start': 13.239, 'end': 13.84}];
const groups = [
{ text: "We asked what you needed.", start: 0.14, end: 1.179 },
{ text: "Forty-seven percent of you said motion graphics,", start: 1.199, end: 3.759 },
{ text: "sixty-two percent said static content was costing you attention,", start: 3.799, end: 7.299 },
{ text: "and three out of four said you know the look you want", start: 7.319, end: 9.619 },
{ text: "but don't have the editing skills to get there.", start: 9.779, end: 12.239 },
{ text: "So we built Editor Agent.", start: 12.3, end: 13.84 }
];
const container = document.querySelector('[data-composition-id="captions"] .captions-container');
const tl = gsap.timeline({ paused: true });
// Add background "rails" for Tschichold aesthetic - reduced to 2-3 strategic lines
const railPositions = [
{ type: 'h', pos: '30%' },
{ type: 'h', pos: '70%' },
{ type: 'v', pos: '25%' }
];
railPositions.forEach(rail => {
const el = document.createElement('div');
el.className = `rail rail-${rail.type}`;
if (rail.type === 'h') {
el.style.top = rail.pos;
} else {
el.style.left = rail.pos;
}
container.appendChild(el);
});
const keywords = ["asked", "needed", "forty-seven", "percent", "motion", "graphics", "sixty-two", "static", "content", "attention", "three", "four", "editing", "skills", "editor", "agent"];
groups.forEach((group, index) => {
const groupEl = document.createElement('div');
groupEl.className = 'caption-group';
groupEl.id = 'group-' + index;
const words = group.text.split(' ');
words.forEach(wordText => {
const span = document.createElement('span');
span.className = 'word';
const cleanWord = wordText.toLowerCase().replace(/[^\w]/g, '');
if (keywords.includes(cleanWord)) {
span.classList.add('bold');
}
span.textContent = wordText;
groupEl.appendChild(span);
});
container.appendChild(groupEl);
const groupWords = groupEl.querySelectorAll('.word');
// Entrance
tl.set(groupEl, { visibility: 'visible' }, group.start);
tl.to(groupEl, { opacity: 1, duration: 0.1 }, group.start);
// Words slide into alignment along "rails"
tl.from(groupWords, {
x: (i) => (i % 2 === 0 ? -150 : 150),
opacity: 0,
duration: 0.8,
stagger: 0.04,
ease: "power4.out"
}, group.start);
// Exit
const exitTime = group.end;
tl.to(groupEl, {
opacity: 0,
duration: 0.2,
ease: "power2.in"
}, exitTime);
tl.set(groupEl, { visibility: 'hidden' }, exitTime + 0.2);
});
// Ambient motion for the rails
tl.to('.rail-h', {
scaleX: 0.9,
opacity: 0.03,
duration: 14,
ease: "none"
}, 0);
tl.to('.rail-v', {
scaleY: 0.9,
opacity: 0.03,
duration: 14,
ease: "none"
}, 0);
window.__timelines["captions"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,300 @@
<template id="hero-moment-template">
<div data-composition-id="hero-moment" data-width="1080" data-height="1920" data-duration="14">
<div class="background">
<div class="paper-texture"></div>
<div class="grid-lines"></div>
<div class="accent-circle"></div>
</div>
<div class="content-layer">
<div class="stat-container">
<div class="stat-number">75%</div>
<div class="stat-fraction">(3 out of 4)</div>
</div>
<div class="text-container">
<div class="line line-1">LACK</div>
<div class="line line-2">THE ESSENTIAL</div>
<div class="line line-3">SKILLS</div>
</div>
<div class="geometric-elements">
<div class="rule-line horizontal"></div>
<div class="rule-line vertical"></div>
<div class="square-accent"></div>
</div>
</div>
<style>
[data-composition-id="hero-moment"] {
background-color: transparent;
color: #000000;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
width: 1080px;
height: 1920px;
position: relative;
}
[data-composition-id="hero-moment"] .paper-texture {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.05; /* Standardized to 0.05 */
pointer-events: none;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
z-index: 0;
}
[data-composition-id="hero-moment"] .background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
[data-composition-id="hero-moment"] .grid-lines {
position: absolute;
width: 200%;
height: 200%;
top: -50%;
left: -50%;
background-image:
linear-gradient(to right, rgba(0,0,0,0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(0,0,0,0.05) 1px, transparent 1px);
background-size: 100px 100px;
transform: rotate(-15deg);
}
[data-composition-id="hero-moment"] .accent-circle {
position: absolute;
width: 800px;
height: 800px;
border: 2px solid #BE1E2D;
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
opacity: 0;
}
[data-composition-id="hero-moment"] .content-layer {
position: relative;
z-index: 1;
width: 900px;
height: 1600px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
[data-composition-id="hero-moment"] .stat-container {
position: absolute;
top: 10%;
left: 50%;
transform: translateX(-50%);
text-align: center;
}
[data-composition-id="hero-moment"] .stat-number {
font-size: 280px; /* Reduced massiveness for better hierarchy */
font-weight: 300; /* Slightly more weight for legibility */
line-height: 0.8;
color: #BE1E2D;
letter-spacing: -10px;
opacity: 0;
}
[data-composition-id="hero-moment"] .stat-fraction {
font-size: 48px; /* Slightly larger supporting text */
font-weight: 300;
margin-top: 10px;
letter-spacing: 12px; /* More tracking */
text-transform: uppercase;
opacity: 0;
}
[data-composition-id="hero-moment"] .text-container {
position: absolute;
bottom: 15%;
left: 50%;
transform: translateX(-50%);
text-align: center;
width: 100%;
}
[data-composition-id="hero-moment"] .line {
font-size: 120px; /* Increased for impact */
font-weight: 800;
line-height: 0.8; /* Tighter line height */
margin-bottom: 10px;
letter-spacing: 4px; /* More tracking */
opacity: 0;
}
[data-composition-id="hero-moment"] .line-2 {
font-size: 60px;
font-weight: 400;
color: #BE1E2D;
}
[data-composition-id="hero-moment"] .geometric-elements {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
top: 0;
left: 0;
}
[data-composition-id="hero-moment"] .rule-line {
position: absolute;
background-color: #000000;
opacity: 0;
}
[data-composition-id="hero-moment"] .rule-line.horizontal {
width: 0%;
height: 4px;
top: 45%;
left: 0;
}
[data-composition-id="hero-moment"] .rule-line.vertical {
width: 4px;
height: 0%;
top: 0;
left: 50%;
}
[data-composition-id="hero-moment"] .square-accent {
position: absolute;
width: 60px;
height: 60px;
background-color: #BE1E2D;
top: 43.5%;
left: 47.5%;
transform: scale(0);
opacity: 0;
}
</style>
<script>
(function() {
const TRANSCRIPT = [{'text': 'We', 'start': 0.14, 'end': 0.239}, {'text': 'asked', 'start': 0.28, 'end': 0.459}, {'text': 'what', 'start': 0.5, 'end': 0.619}, {'text': 'you', 'start': 0.659, 'end': 0.779}, {'text': 'needed.', 'start': 0.8, 'end': 1.179}, {'text': 'Forty-seven', 'start': 1.199, 'end': 1.619}, {'text': 'percent', 'start': 1.699, 'end': 1.96}, {'text': 'of', 'start': 1.979, 'end': 2.059}, {'text': 'you', 'start': 2.099, 'end': 2.299}, {'text': 'said', 'start': 2.319, 'end': 2.499}, {'text': 'motion', 'start': 2.559, 'end': 2.839}, {'text': 'graphics,', 'start': 2.899, 'end': 3.759}, {'text': 'sixty-two', 'start': 3.799, 'end': 4.159}, {'text': 'percent', 'start': 4.239, 'end': 4.559}, {'text': 'said', 'start': 4.639, 'end': 4.799}, {'text': 'static', 'start': 4.859, 'end': 5.199}, {'text': 'content', 'start': 5.239, 'end': 5.639}, {'text': 'was', 'start': 5.679, 'end': 5.779}, {'text': 'costing', 'start': 5.859, 'end': 6.219}, {'text': 'you', 'start': 6.259, 'end': 6.339}, {'text': 'attention,', 'start': 6.379, 'end': 7.299}, {'text': 'and', 'start': 7.319, 'end': 7.439}, {'text': 'three', 'start': 7.48, 'end': 7.599}, {'text': 'out', 'start': 7.679, 'end': 7.779}, {'text': 'of', 'start': 7.799, 'end': 7.879}, {'text': 'four', 'start': 7.94, 'end': 8.139}, {'text': 'said', 'start': 8.279, 'end': 8.46}, {'text': 'you', 'start': 8.519, 'end': 8.6}, {'text': 'know', 'start': 8.619, 'end': 8.739}, {'text': 'the', 'start': 8.8, 'end': 8.88}, {'text': 'look', 'start': 8.92, 'end': 9.079}, {'text': 'you', 'start': 9.119, 'end': 9.259}, {'text': 'want', 'start': 9.279, 'end': 9.619}, {'text': 'but', 'start': 9.779, 'end': 9.88}, {'text': "don't", 'start': 9.92, 'end': 10.1}, {'text': 'have', 'start': 10.159, 'end': 10.3}, {'text': 'the', 'start': 10.3, 'end': 10.42}, {'text': 'editing', 'start': 10.5, 'end': 10.779}, {'text': 'skills', 'start': 10.86, 'end': 11.139}, {'text': 'to', 'start': 11.159, 'end': 11.239}, {'text': 'get', 'start': 11.279, 'end': 11.439}, {'text': 'there.', 'start': 11.46, 'end': 12.239}, {'text': 'So', 'start': 12.3, 'end': 12.38}, {'text': 'we', 'start': 12.399, 'end': 12.519}, {'text': 'built', 'start': 12.539, 'end': 12.759}, {'text': 'Editor', 'start': 12.84, 'end': 13.119}, {'text': 'Agent.', 'start': 13.239, 'end': 13.84}];
const tl = gsap.timeline({ paused: true });
// Helper to find word timing
function getWordTime(word) {
const found = TRANSCRIPT.find(w => w.text.toLowerCase().replace(/[^\w]/g, '') === word.toLowerCase());
return found ? found.start : 0;
}
// 1. Entrance (Starts at 7.319s)
tl.to('[data-composition-id="hero-moment"] .accent-circle', {
opacity: 0.2,
scale: 1.2,
duration: 1,
ease: 'power2.out'
}, 7.319);
tl.to('[data-composition-id="hero-moment"] .rule-line.horizontal', {
width: '100%',
opacity: 0.3,
duration: 0.8,
ease: 'expo.out'
}, 7.4);
tl.to('[data-composition-id="hero-moment"] .rule-line.vertical', {
height: '100%',
opacity: 0.3,
duration: 0.8,
ease: 'expo.out'
}, 7.5);
tl.to('[data-composition-id="hero-moment"] .square-accent', {
scale: 1,
opacity: 1,
duration: 0.5,
ease: 'back.out(1.7)'
}, 7.8);
// 2. Kinetic Typography synced to transcript
// "75%" (three out of four)
const timeThree = getWordTime('three');
tl.to('[data-composition-id="hero-moment"] .stat-number', {
opacity: 1,
duration: 0.8,
ease: 'power4.out'
}, timeThree);
tl.to('[data-composition-id="hero-moment"] .stat-fraction', {
opacity: 0.8,
duration: 0.8,
ease: 'power2.out'
}, timeThree + 0.2);
// "lack"
const timeSkills = getWordTime('skills');
tl.to('[data-composition-id="hero-moment"] .line-1', {
opacity: 1,
duration: 0.6,
ease: 'back.out(1.2)'
}, timeSkills - 0.4);
// "essential"
tl.to('[data-composition-id="hero-moment"] .line-2', {
opacity: 1,
duration: 0.6,
ease: 'back.out(1.2)'
}, timeSkills - 0.2);
// "skills"
tl.to('[data-composition-id="hero-moment"] .line-3', {
opacity: 1,
duration: 0.6,
ease: 'back.out(1.2)'
}, timeSkills);
// 3. Ambient Motion (Continuous)
tl.to('[data-composition-id="hero-moment"] .grid-lines', {
x: '+=50',
y: '+=50',
duration: 14,
ease: 'none'
}, 0);
tl.to('[data-composition-id="hero-moment"] .accent-circle', {
scale: 1.3,
duration: 14,
ease: 'sine.inOut'
}, 0);
// 4. Exit (Starts at 12.239s)
tl.to('[data-composition-id="hero-moment"] .content-layer, [data-composition-id="hero-moment"] .geometric-elements', {
opacity: 0,
y: 50,
duration: 0.6,
ease: 'power3.in'
}, 12.239);
tl.to('[data-composition-id="hero-moment"] .accent-circle', {
scale: 2,
opacity: 0,
duration: 0.8,
ease: 'power2.in'
}, 12.239);
window.__timelines["hero-moment"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,226 @@
<template id="mograph-overlays-template">
<div data-composition-id="mograph-overlays" data-width="1080" data-height="1920" data-duration="14">
<div id="mograph-container">
<!-- Ambient Elements -->
<div class="diagonal-line line-1"></div>
<div class="diagonal-line line-2"></div>
<div class="marker marker-tl"></div>
<div class="marker marker-tr"></div>
<div class="marker marker-bl"></div>
<div class="marker marker-br"></div>
<!-- Alignment Indicators -->
<div class="alignment-h"></div>
<div class="alignment-v"></div>
</div>
<style>
[data-composition-id="mograph-overlays"] {
position: absolute;
top: 0;
left: 0;
width: 1080px;
height: 1920px;
overflow: hidden;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #000000;
}
[data-composition-id="mograph-overlays"] #mograph-container {
position: relative;
width: 100%;
height: 100%;
}
/* Diagonal Lines - Modernist Red */
[data-composition-id="mograph-overlays"] .diagonal-line {
position: absolute;
background: #BE1E2D;
height: 1px;
opacity: 0.2;
}
[data-composition-id="mograph-overlays"] .line-1 {
width: 600px;
top: 150px;
left: -150px;
transform: rotate(45deg);
}
[data-composition-id="mograph-overlays"] .line-2 {
width: 800px;
bottom: 200px;
right: -200px;
transform: rotate(-45deg);
}
/* Geometric Markers - Clean Lines */
[data-composition-id="mograph-overlays"] .marker {
position: absolute;
width: 30px;
height: 30px;
border: 1px solid #000000;
opacity: 0.4;
}
[data-composition-id="mograph-overlays"] .marker-tl { top: 60px; left: 60px; border-right: 0; border-bottom: 0; }
[data-composition-id="mograph-overlays"] .marker-tr { top: 60px; right: 60px; border-left: 0; border-bottom: 0; }
[data-composition-id="mograph-overlays"] .marker-bl { bottom: 60px; left: 60px; border-right: 0; border-top: 0; }
[data-composition-id="mograph-overlays"] .marker-br { bottom: 60px; right: 60px; border-left: 0; border-top: 0; }
/* Alignment Indicators - Subtle Grid */
[data-composition-id="mograph-overlays"] .alignment-h {
position: absolute;
top: 50%;
left: 80px;
right: 80px;
height: 1px;
background: rgba(0,0,0,0.05);
}
[data-composition-id="mograph-overlays"] .alignment-v {
position: absolute;
left: 50%;
top: 80px;
bottom: 80px;
width: 1px;
background: rgba(0,0,0,0.05);
}
/* Dynamic Graphic - Modernist Museum Style */
[data-composition-id="mograph-overlays"] .dynamic-graphic {
position: absolute;
opacity: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 20px;
}
[data-composition-id="mograph-overlays"] .graphic-value {
font-size: 80px;
font-weight: 300; /* Lighter weight for museum feel */
line-height: 0.9;
color: #000000;
letter-spacing: -0.04em;
}
[data-composition-id="mograph-overlays"] .graphic-label {
font-size: 18px;
font-weight: 500;
letter-spacing: 6px;
margin-top: 15px;
color: #BE1E2D; /* Accent color for label */
text-transform: uppercase;
}
[data-composition-id="mograph-overlays"] .graphic-line {
width: 0;
height: 1px;
background: #000000;
margin-top: 10px;
}
</style>
<script>
(function() {
const TRANSCRIPT = [{'text': 'We', 'start': 0.14, 'end': 0.239}, {'text': 'asked', 'start': 0.28, 'end': 0.459}, {'text': 'what', 'start': 0.5, 'end': 0.619}, {'text': 'you', 'start': 0.659, 'end': 0.779}, {'text': 'needed.', 'start': 0.8, 'end': 1.179}, {'text': 'Forty-seven', 'start': 1.199, 'end': 1.619}, {'text': 'percent', 'start': 1.699, 'end': 1.96}, {'text': 'of', 'start': 1.979, 'end': 2.059}, {'text': 'you', 'start': 2.099, 'end': 2.299}, {'text': 'said', 'start': 2.319, 'end': 2.499}, {'text': 'motion', 'start': 2.559, 'end': 2.839}, {'text': 'graphics,', 'start': 2.899, 'end': 3.759}, {'text': 'sixty-two', 'start': 3.799, 'end': 4.159}, {'text': 'percent', 'start': 4.239, 'end': 4.559}, {'text': 'said', 'start': 4.639, 'end': 4.799}, {'text': 'static', 'start': 4.859, 'end': 5.199}, {'text': 'content', 'start': 5.239, 'end': 5.639}, {'text': 'was', 'start': 5.679, 'end': 5.779}, {'text': 'costing', 'start': 5.859, 'end': 6.219}, {'text': 'you', 'start': 6.259, 'end': 6.339}, {'text': 'attention,', 'start': 6.379, 'end': 7.299}, {'text': 'and', 'start': 7.319, 'end': 7.439}, {'text': 'three', 'start': 7.48, 'end': 7.599}, {'text': 'out', 'start': 7.679, 'end': 7.779}, {'text': 'of', 'start': 7.799, 'end': 7.879}, {'text': 'four', 'start': 7.94, 'end': 8.139}, {'text': 'said', 'start': 8.279, 'end': 8.46}, {'text': 'you', 'start': 8.519, 'end': 8.6}, {'text': 'know', 'start': 8.619, 'end': 8.739}, {'text': 'the', 'start': 8.8, 'end': 8.88}, {'text': 'look', 'start': 8.92, 'end': 9.079}, {'text': 'you', 'start': 9.119, 'end': 9.259}, {'text': 'want', 'start': 9.279, 'end': 9.619}, {'text': 'but', 'start': 9.779, 'end': 9.88}, {'text': "don't", 'start': 9.92, 'end': 10.1}, {'text': 'have', 'start': 10.159, 'end': 10.3}, {'text': 'the', 'start': 10.3, 'end': 10.42}, {'text': 'editing', 'start': 10.5, 'end': 10.779}, {'text': 'skills', 'start': 10.86, 'end': 11.139}, {'text': 'to', 'start': 11.159, 'end': 11.239}, {'text': 'get', 'start': 11.279, 'end': 11.439}, {'text': 'there.', 'start': 11.46, 'end': 12.239}, {'text': 'So', 'start': 12.3, 'end': 12.38}, {'text': 'we', 'start': 12.399, 'end': 12.519}, {'text': 'built', 'start': 12.539, 'end': 12.759}, {'text': 'Editor', 'start': 12.84, 'end': 13.119}, {'text': 'Agent.', 'start': 13.239, 'end': 13.84}];
const tl = gsap.timeline({ paused: true });
// Face Avoidance Area: x:300-776, y:320-796
// Safe zones: Right side (x > 800), Top (y < 300), Bottom (y > 800)
// 1. Ambient Motion (Continuous & Subtle)
tl.to('.line-1', { x: 30, y: 10, duration: 14, ease: 'none' }, 0);
tl.to('.line-2', { x: -30, y: -10, duration: 14, ease: 'none' }, 0);
tl.from('.marker', {
scale: 0.9,
opacity: 0,
duration: 1.5,
stagger: 0.3,
ease: 'power2.out'
}, 0);
// 2. Transcript-Synced Graphics
// Updated content based on design review
const triggerWords = [
{ word: "47%", label: "USER SURVEY" },
{ word: "static", label: "CONTENT ENGAGEMENT" },
{ word: "editing", label: "TECHNICAL SKILLS" },
{ word: "Editor", label: "SYSTEM ARCHITECTURE" }
];
triggerWords.forEach((trigger, index) => {
const wordData = TRANSCRIPT.find(w => w.text.toLowerCase().includes(trigger.word.toLowerCase()));
if (wordData) {
const startTime = wordData.start;
const duration = 3.0;
// Deterministic positioning outside face area (Vertical Safe Zones)
// Face: x:300-776, y:320-796 (approx in vertical)
const positions = [
{ x: 50, y: 1500 }, // Bottom Left
{ x: 600, y: 1600 }, // Bottom Right
{ x: 100, y: 100 }, // Top Left
{ x: 500, y: 50 } // Top Right
];
const pos = positions[index % positions.length];
// Create a unique graphic for each trigger
const graphicId = `graphic-${index}`;
const graphicHtml = `
<div id="${graphicId}" class="dynamic-graphic" style="left: ${pos.x}px; top: ${pos.y}px;">
<div class="graphic-value">${trigger.word.toUpperCase()}</div>
<div class="graphic-label">${trigger.label}</div>
<div class="graphic-line"></div>
</div>
`;
document.getElementById('mograph-container').insertAdjacentHTML('beforeend', graphicHtml);
const graphicSelector = `#${graphicId}`;
// Entrance - Clean and Sophisticated
tl.to(graphicSelector, {
opacity: 1,
duration: 1.0,
ease: 'power3.out'
}, startTime);
tl.from(`${graphicSelector} .graphic-value`, {
x: -20,
opacity: 0,
duration: 1.2,
ease: 'power4.out'
}, startTime + 0.1);
tl.to(`${graphicSelector} .graphic-line`, {
width: 250,
duration: 1.5,
ease: 'expo.out'
}, startTime + 0.4);
// Exit - Smooth Fade
tl.to(graphicSelector, {
opacity: 0,
x: 20,
duration: 0.8,
ease: 'power2.in'
}, startTime + duration);
}
});
// 3. Subtle Grid/Alignment pulses
tl.to('.alignment-h, .alignment-v', {
opacity: 0.2,
duration: 0.5,
repeat: 3,
yoyo: true,
ease: 'power1.inOut'
}, 1);
window.__timelines["mograph-overlays"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,61 @@
# HyperFrames Design Review
## First Impression
This looks like a Bauhaus-inspired museum exhibit had a head-on collision with a TikTok ad. Its trying to be Jan Tschichold, but its currently closer to "I just discovered the Helvetica font and the color red."
---
## CRITICAL Design Failures
Issues that make this look unprofessional or straight-up ugly. These MUST be fixed.
### The "Floating Head" Problem
**Where:** `compositions/aroll.html`
**What's wrong:** The A-Roll video is set to `object-fit: cover` and fills the entire 1080x1920 frame. While bold, it completely ignores the "safe zones" for the face. The `mograph_overlays.html` then proceeds to slap text directly over where the speaker's face likely is.
**Why it matters:** You're literally covering the speaker's mouth or eyes with "USER SURVEY" and "47%". It looks like a mistake, not a design choice.
**Fix it:** Scale the A-Roll video down or use a mask that respects the rule of thirds. If you're going full-screen, your overlays need to be pushed to the extreme margins.
### Caption Chaos
**Where:** `compositions/captions.html`
**What's wrong:** You have words jumping from `font-size: 50px` to `140px` with a `line-height: 0.9`. In a 9:16 portrait format, this is going to cause massive, ugly overlaps and erratic wrapping.
**Why it matters:** Readability is the first casualty of "cool" design. If the viewer has to squint or decode which word comes next because they're stacked like a game of Tetris, you've failed.
**Fix it:** Standardize the scale variance. 50px to 140px is too wide a gap. Try 80px to 120px. Increase the `line-height` to at least `1.1` to give the descenders some breathing room.
### The "Dead" Background
**Where:** `compositions/background.html`
**What's wrong:** A `#FAF5E8` (off-white) background with a `0.05` opacity noise filter is basically just a flat, boring beige on most mobile screens. The grid lines are too thin (`1px`, `2px`) to feel intentional; they just look like screen artifacts.
**Why it matters:** It lacks depth. It feels like a default template rather than a premium "New Typography" aesthetic.
**Fix it:** Increase the noise opacity to `0.1` or `0.15`. Make the grid lines bolder—use `4px` and `8px` consistently to create a real sense of structure.
---
## Design Improvements
Things that aren't broken but are boring, lazy, or could be significantly better.
### Robotic Motion
**Where:** `compositions/mograph_overlays.html`
**The problem:** The diagonal lines and markers just "drift" linearly. Its the most basic GSAP animation possible.
**Make it better:** Add some "snap" to the entrance. Use `expo.out` or `back.out` for the markers. Give the lines a slight pulse in opacity or thickness to make them feel like they're reacting to the audio, not just floating in space.
### Hero Moment Hierarchy
**Where:** `compositions/hero_moment.html`
**The problem:** The "75%" is `280px` while the supporting text is `120px`. Its a bit of a "shouting match" between elements.
**Make it better:** Lean into the Tschichold asymmetry. Move the "75%" off-center. Use a much heavier weight for the number and a much lighter, tracked-out weight for the text. Contrast is your friend; stop making everything "big."
---
## What Actually Works
The use of **Bauhaus Red (#BE1E2D)** against the off-white is a classic for a reason. When it hits, it hits hard. The "rail" concept in the captions is a genuine nod to modernist layout principles—it just needs better execution. The "wipe-line" entrance in the A-Roll is actually quite sophisticated and provides a nice "editorial" feel.
---
## Design Verdict
**Visual Impact:** 6/10 - It has a clear direction, but it's playing it too safe.
**Color & Typography:** 5/10 - Good palette, but the typographic hierarchy is a mess of competing scales.
**Motion & Animation Feel:** 4/10 - Too much linear drifting; needs more rhythmic "pop."
**Overall Aesthetic:** 5/10 - It looks like a student project trying to mimic a master.
**Bottom Line:** Its not a disaster, but its forgettable. Youve got the ingredients for a high-end editorial look, but youre currently serving it on a paper plate. Fix the face-blocking overlays and the caption scaling before you show this to anyone with a pulse.
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jan Tschichold Typography Video</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: 1080px;
height: 1920px;
overflow: hidden;
background-color: #FAF5E8;
}
#master-composition {
width: 1080px;
height: 1920px;
position: relative;
z-index: 0;
}
#bg-comp { z-index: 1; }
#aroll-comp { z-index: 2; }
#hero-comp { z-index: 3; }
#mograph-comp { z-index: 4; }
#captions-comp { z-index: 5; }
</style>
</head>
<body>
<div id="master-composition" data-composition-id="master" data-width="1080" data-height="1920" data-duration="14">
<!-- Background Layer -->
<div id="bg-comp"
data-composition-id="background"
data-composition-src="compositions/background.html"
data-start="0"
data-duration="14"
data-track-index="0">
</div>
<!-- A-Roll Video Layer -->
<div id="aroll-comp"
data-composition-id="aroll"
data-composition-src="compositions/aroll.html"
data-start="0"
data-duration="14"
data-track-index="1">
</div>
<!-- Hero Moment (Full Canvas) -->
<div id="hero-comp"
data-composition-id="hero-moment"
data-composition-src="compositions/hero_moment.html"
data-start="0"
data-duration="14"
data-track-index="2">
</div>
<!-- Motion Graphic Overlays -->
<div id="mograph-comp"
data-composition-id="mograph-overlays"
data-composition-src="compositions/mograph_overlays.html"
data-start="0"
data-duration="14"
data-track-index="3">
</div>
<!-- Captions Layer -->
<div id="captions-comp"
data-composition-id="captions"
data-composition-src="compositions/captions.html"
data-start="0"
data-duration="14"
data-track-index="4">
</div>
</div>
<script>
window.__timelines = {};
const masterTL = gsap.timeline({ paused: true });
window.__timelines["master"] = masterTL;
</script>
</body>
</html>