Back to skills

Agent Skill

Video Generation (Kinetic Typography & Motion Graphics)

hyperframes-video

Create animated videos with kinetic typography, text animations, motion graphics, product intros, and launch videos. Renders HTML/CSS/GSAP to MP4 using HyperFrames. Best for any video with animated text, titles, or UI mockups.

O-mega.aiGenerationHTMLVideoAnimationHyperframesKinetic-typographyText-animationMotion-graphicsMp4GsapHtmlVideo-generationLaunch-videoProduct-intro

installs

o-mega.ai/internal

by o-mega.ai

Score

9.0

/ 10

Installs

Repo Stars

Last Updated

0d ago

Fresh

Quality Ratio

95%

Description

Verified

Language

HTML

First Published

Feb 2026

Platforms

1

Skill Definition

VIDEO ANIMATION MISSION (MANDATORY READING)

DELIVERABLE TYPE: ANIMATED VIDEO (.mp4 file)

YOU ARE CREATING A PROGRAMMATIC VIDEO ANIMATION using HyperFrames (HTML/CSS/GSAP to MP4). This is for kinetic typography, motion graphics, text animations, and promotional videos.

DO NOT CREATE: a website, a React app, or an HTML page meant to be opened in a browser. MUST CREATE: A rendered .mp4 video file using HyperFrames.

If you create an HTML file without rendering it to video, YOU HAVE FAILED THE MISSION. The ONLY acceptable output is: /home/user/output/video.mp4


WHAT HYPERFRAMES CAN DO

HyperFrames renders HTML/CSS + GSAP animations to MP4. Anything you can build with HTML and CSS, you can animate into a video. No React, no build step. Plain HTML with data attributes for timing, GSAP for animation.

YOU CAN BUILD ANY UI FROM SCRATCH

  • Chat interfaces with messages appearing
  • Dashboard UIs with charts and metrics
  • Browser windows with content loading
  • Mobile app mockups with interactions
  • Code editors with typing animations
  • Product demos, launch videos, promotional content

If the request is for a product demo, chatbot promo, or app showcase, BUILD the UI with HTML/CSS. No screenshots required (though you can use them too).


COLOR & DESIGN SYSTEM

When choosing colors, follow this system (unless the user specifies otherwise):

Color Structure:

  • Primary: The main brand/accent color (choose something modern and vibrant)
  • Secondary: A complementary color that pairs well with primary
  • Neutral: Use primary color at 10-15% opacity for subtle backgrounds, or a desaturated version
  • Background: Dark backgrounds work well for video (#0a0a0f, #0f172a, or near-black)
  • Text: High contrast against background (white or very light gray)

If user/company preferences are provided, use those colors. Otherwise, choose a cohesive modern palette.

Palette principles:

  • Secondary should complement, not compete with primary
  • Use neutral tones for supporting elements (borders, subtle backgrounds)
  • Gradients can add depth but use sparingly
  • Ensure sufficient contrast for readability

HYPERFRAMES CORE RULES (MANDATORY)

Three rules govern every HyperFrames composition:

Rule 1: Root element attributes

The root element MUST have data-composition-id, data-width, and data-height:

<div id="root" data-composition-id="omega-video"
 data-start="0" data-width="1920" data-height="1080">

Rule 2: Timed elements need clip class + data attributes

Every visible timed element needs data-start, data-duration, data-track-index, and class="clip":

<h1 id="title" class="clip"
 data-start="0" data-duration="5" data-track-index="0"
 style="font-size: 72px; color: white;">
 Hello World
</h1>

EXCEPTION: <video> and <audio> elements do NOT get class="clip".

Rule 3: GSAP timeline registration

GSAP timelines MUST be created with { paused: true } and registered on window.__timelines with a key matching the data-composition-id:

const tl = gsap.timeline({ paused: true });
tl.from("#title", { opacity: 0, y: -50, duration: 1 }, 0);
window.__timelines = window.__timelines || {};
window.__timelines ["omega-video"] = tl;

DATA ATTRIBUTES REFERENCE

AttributeApplies ToRequiredPurpose
data-composition-idRoot divYesUnique ID, must match window.__timelines key
data-widthRoot divYesCanvas width in pixels (e.g., 1920)
data-heightRoot divYesCanvas height in pixels (e.g., 1080)
data-startAll clipsYesStart time in seconds, or clip ID for relative timing
data-durationimg, div clipsYes for imagesDuration in seconds
data-track-indexAll clipsYesLayer ordering (higher = on top)
data-media-startvideo, audioNoTrim/offset into source media
data-volumevideo, audioNoVolume 0 to 1
class="clip"img, divYesVisibility lifecycle (NOT on video/audio)

Relative timing (clips can reference other clips):

<h1 id="title" class="clip" data-start="0" data-duration="3" data-track-index="0">Title</h1>
<p id="subtitle" class="clip" data-start="title" data-duration="3" data-track-index="1">After title ends</p>
<p id="delayed" class="clip" data-start="title + 1.5" data-duration="2" data-track-index="2">1.5s gap after title</p>

GSAP ANIMATION PATTERNS

GSAP is loaded via CDN in the HTML. Use tl.to(), tl.from(), tl.fromTo(), tl.set() with absolute position parameter (3rd argument).

TEXT ANIMATION APPROACHES

// APPROACH 1: Fade + rise (clean, professional)
tl.from("#title", { opacity: 0, y: 50, duration: 0.8, ease: "power2.out" }, 0);

// APPROACH 2: Character stagger (energetic, playful)
// Wrap each character in a span with class "char" in the HTML
tl.from(".title-char", {
 opacity: 0, y: 30, rotation: 15,
 duration: 0.5, stagger: 0.03, ease: "back.out(1.7)"
}, 0);

// APPROACH 3: Scale pop (impactful, attention-grabbing)
tl.from("#headline", { scale: 0, opacity: 0, duration: 0.6, ease: "back.out(2)" }, 1);

// APPROACH 4: Slide from direction (cinematic)
tl.from("#text-left", { x: -200, opacity: 0, duration: 0.7, ease: "power3.out" }, 0.5);
tl.from("#text-right", { x: 200, opacity: 0, duration: 0.7, ease: "power3.out" }, 0.5);

// APPROACH 5: Typewriter (code-like, technical)
tl.fromTo("#typewriter",
 { clipPath: "inset(0 100% 0 0)" },
 { clipPath: "inset(0 0% 0 0)", duration: 2, ease: "steps(30)" }, 0);

// APPROACH 6: Bounce in (playful)
tl.from("#bouncy", { y: -300, duration: 1, ease: "bounce.out" }, 0);

// APPROACH 7: Word-by-word reveal
tl.from(".word", { opacity: 0, y: 20, duration: 0.4, stagger: 0.15, ease: "power2.out" }, 0);

SCENE TRANSITIONS

// Fade out scene 1, fade in scene 2
tl.to("#scene1", { opacity: 0, duration: 0.5 }, 4.5);
tl.from("#scene2", { opacity: 0, duration: 0.5 }, 5);

// Slide transition
tl.to("#scene1", { x: -1920, duration: 0.6, ease: "power2.inOut" }, 4);
tl.from("#scene2", { x: 1920, duration: 0.6, ease: "power2.inOut" }, 4);

// Scale zoom transition
tl.to("#scene1", { scale: 1.5, opacity: 0, duration: 0.5, ease: "power2.in" }, 4);
tl.from("#scene2", { scale: 0.8, opacity: 0, duration: 0.5, ease: "power2.out" }, 4.5);

EASING REFERENCE

  • Smooth: "power2.out" (professional, default choice)
  • Snappy: "power3.out" (quick, decisive)
  • Bouncy: "back.out(1.7)" (overshoots then settles)
  • Elastic: "elastic.out(1, 0.3)" (wobbly settle)
  • Dramatic: "power4.inOut" (slow start, slow end)

BUILDING UIs FROM SCRATCH

Build entire interfaces with HTML/CSS. No framework needed:

<!-- Chat interface -->
<div id="chat-msg-1" class="clip" data-start="1" data-duration="8" data-track-index="1"
 style="position:absolute; left:100px; bottom:300px; background:#2563eb;
 border-radius:16px; padding:12px 18px; color:white; font-size:24px; max-width:500px;">
 How can I help you today?
</div>

<!-- Browser window frame -->
<div id="browser" class="clip" data-start="0" data-duration="10" data-track-index="0"
 style="position:absolute; top:80px; left:160px; width:1600px; border-radius:12px;
 overflow:hidden; box-shadow:0 25px 50px rgba(0,0,0,0.4);">
 <div style="background:#333; padding:8px 16px; display:flex; gap:8px; align-items:center;">
 <div style="width:12px; height:12px; border-radius:50%; background:#ff5f57;"></div>
 <div style="width:12px; height:12px; border-radius:50%; background:#ffbd2e;"></div>
 <div style="width:12px; height:12px; border-radius:50%; background:#28c840;"></div>
 <div style="flex:1; background:#222; border-radius:4px; padding:4px 12px; color:#888; font-size:14px;">example.com</div>
 </div>
 <div style="background:#1a1a1a; height:600px; padding:40px;"><!-- Content --></div>
</div>

<!-- Dashboard metric card -->
<div id="metric" class="clip" data-start="0.5" data-duration="5" data-track-index="1"
 style="position:absolute; top:200px; left:200px; background:rgba(255,255,255,0.05);
 border:1px solid rgba(255,255,255,0.1); border-radius:16px; padding:24px; width:300px;">
 <div style="color:#888; font-size:14px;">Monthly Revenue</div>
 <div style="color:#fff; font-size:48px; font-weight:700;">$142K</div>
 <div style="color:#22c55e; font-size:16px;">+23% from last month</div>
</div>

ANIMATING UI MOCKUPS (with images)

// Fly-in with perspective
tl.from("#screenshot", {
 rotateX: 30, y: 200, scale: 0.5, opacity: 0,
 duration: 1.2, ease: "power3.out", transformPerspective: 1000
}, 0);

AUDIO & MEDIA

<!-- Background music -->
<audio data-start="0" data-track-index="10" data-volume="0.3" src="./assets/music.mp3"></audio>

<!-- Video clip (NO class="clip") -->
<video id="demo" data-start="3" data-duration="10" data-track-index="0" src="./assets/demo.mp4"></video>

<!-- Image (YES class="clip") -->
<img id="logo" class="clip" data-start="0" data-duration="5" data-track-index="5"
 src="./assets/logo.png" style="position:absolute; top:40px; right:40px; width:120px;" />

Do NOT call video.play(), video.pause(), or set currentTime. HyperFrames controls playback.


COMMON MISTAKES (AVOID THESE)

  1. Missing class="clip" on timed divs/images: Element won't respect timing.
  2. Adding class="clip" to video/audio: Don't. Only visible non-media elements get it.
  3. Timeline key mismatch: window.__timelines ["X"] MUST match data-composition-id="X".
  4. Non-paused timeline: Always use gsap.timeline({ paused: true }).
  5. Animating video dimensions directly: Wrap in a div, animate the wrapper.
  6. Calling video.play()/pause(): Never. Framework manages media playback.
  7. Timeline shorter than composition: Extend with tl.set({}, {}, totalSeconds).

TECHNICAL REQUIREMENTS

  1. NO PYTHON f-strings: Use triple-quoted strings and .replace() instead.
  2. TARGET FILE: /home/user/project/index.html
  3. ASSETS: Copy to /home/user/project/assets/, reference as ./assets/filename.png.
  4. COMPOSITION ID: Must be "omega-video".
  5. GSAP: Injected automatically by the builder script. Do NOT add a <script src="...gsap..."> tag. Just use gsap directly in your <script> blocks.
  6. RENDERING: Playwright opens the HTML, seeks the GSAP timeline frame by frame, captures screenshots, and FFmpeg encodes them to MP4. Do NOT use npx hyperframes render (Chrome binary issues in sandbox).

BUILDER SCRIPT PATTERN (MANDATORY)

Use this exact script structure. ONLY modify the HTML_CONTENT variable. Do NOT rewrite the rendering/encoding logic. Do NOT use npx hyperframes render.

import os, json, subprocess, shutil, glob, asyncio
import nest_asyncio
nest_asyncio.apply() # MUST be called before anything else (E2B runs inside asyncio)
import imageio_ffmpeg
from playwright.async_api import async_playwright

PROJECT_DIR = "/home/user/project"
INDEX_FILE = os.path.join(PROJECT_DIR, "index.html")
OUTPUT_VIDEO = "/home/user/output/video.mp4"
FRAMES_DIR = os.path.join(PROJECT_DIR, "frames")
FFMPEG = imageio_ffmpeg.get_ffmpeg_exe()
FPS = 30

os.makedirs(os.path.join(PROJECT_DIR, "assets"), exist_ok=True)
os.makedirs("/home/user/output", exist_ok=True)
os.makedirs(FRAMES_DIR, exist_ok=True)

# Load GSAP library (pre-downloaded to avoid CDN timeouts)
GSAP_PATH = "/home/user/project/lib/gsap.min.js"
GSAP_JS = ""
if os.path.exists(GSAP_PATH):
 with open(GSAP_PATH) as f:
 GSAP_JS = f.read()
else:
 # Fallback: download now
 import urllib.request
 GSAP_JS = urllib.request.urlopen("https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js").read().decode()

# ============================================================
# ONLY EDIT THIS SECTION: Your HTML/CSS/GSAP composition
# NOTE: GSAP is injected inline automatically. Do NOT add a
# <script src="...gsap..."> tag. Just use gsap directly.
# ============================================================
HTML_CONTENT = """
<div id="root" data-composition-id="omega-video"
 data-start="0" data-width="1920" data-height="1080"
 style="background: #0a0a0f; font-family: 'Inter', -apple-system, sans-serif;
 width: 1920px; height: 1080px; position: relative; overflow: hidden;">

 <h1 id="title"
 style="position: absolute; top: 50%%; left: 50%%; transform: translate(-50%%, -50%%);
 font-size: 96px; font-weight: 800; color: white; text-align: center;">
 Your Title Here
 </h1>

 <script>
 const tl = gsap.timeline({ paused: true });
 tl.from("#title", { opacity: 0, y: 60, duration: 0.8, ease: "power2.out" }, 0.2);
 tl.to("#title", { opacity: 0, y: -40, duration: 0.5, ease: "power2.in" }, 4);
 window.__timelines = window.__timelines || {};
 window.__timelines ["omega-video"] = tl;
 </script>
</div>
"""
# ============================================================
# END OF EDITABLE SECTION. Do not modify below.
# ============================================================

# Inject GSAP inline before the first <script> tag
HTML_CONTENT = HTML_CONTENT.replace(
 "<script>",
 "<script>" + GSAP_JS + "</script><script>",
 1 # Only replace the first occurrence
)

with open(INDEX_FILE, "w") as f:
 f.write(HTML_CONTENT)

# Render: Playwright captures GSAP timeline frame by frame
# IMPORTANT: E2B runs inside asyncio, so we MUST use async_playwright (not sync)
async def render_frames():
 async with async_playwright() as p:
 browser = await p.chromium.launch(args= ["--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage"])
 page = await browser.new_page(viewport={"width": 1920, "height": 1080})
 await page.goto("file://" + os.path.abspath(INDEX_FILE), wait_until="networkidle")
 await page.wait_for_function("window.__timelines && window.__timelines ['omega-video']", timeout=15000)

 duration = await page.evaluate("window.__timelines ['omega-video'].duration()")
 total_frames = int(duration * FPS)
 print("Timeline: " + str(round(duration, 2)) + "s, " + str(total_frames) + " frames at " + str(FPS) + "fps")

 for i in range(total_frames):
 await page.evaluate("window.__timelines ['omega-video'].seek(" + str(i / FPS) + ")")
 await page.wait_for_timeout(16)
 frame_path = os.path.join(FRAMES_DIR, "frame_" + str(i).zfill(5) + ".png")
 root = await page.query_selector("#root")
 if root:
 await root.screenshot(path=frame_path)
 else:
 await page.screenshot(path=frame_path, clip={"x": 0, "y": 0, "width": 1920, "height": 1080})
 if i %% 30 == 0:
 print("Frame " + str(i) + "/" + str(total_frames))

 await browser.close()
 print("Captured all " + str(total_frames) + " frames.")

# Run the async renderer (nest_asyncio already applied at top, so asyncio.run works in E2B)
asyncio.run(render_frames())

# Encode to MP4
subprocess.run( [
 FFMPEG, "-y", "-framerate", str(FPS),
 "-i", os.path.join(FRAMES_DIR, "frame_%%05d.png"),
 "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-preset", "medium",
 OUTPUT_VIDEO
], capture_output=True, text=True, timeout=120)

for f in glob.glob(os.path.join(FRAMES_DIR, "*.png")):
 os.remove(f)

if os.path.exists(OUTPUT_VIDEO) and os.path.getsize(OUTPUT_VIDEO) > 10000:
 print(json.dumps({"success": True, "summary": "Video rendered (" + str(os.path.getsize(OUTPUT_VIDEO) // 1024) + " KB).", "output_files": ["video.mp4"]}))
else:
 print(json.dumps({"success": False, "summary": "No video produced."}))

How to Use