Revert "feat(registry): the video-primitive moves, documented and customisable (#3090)" (#3162)

This reverts commit 3b53bfd2f7.
This commit is contained in:
Miguel Ángel
2026-08-10 14:47:47 -04:00
committed by GitHub
parent 3b53bfd2f7
commit c86d4013f5
2328 changed files with 5906 additions and 458525 deletions
+2 -6
View File
@@ -70,13 +70,9 @@ jobs:
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
# Find which blocks/components changed in this PR. The trailing slash
# in the grep matters: an item is a directory, and a file sitting
# directly under registry/components (CATALOG.md) does not match the
# sed, so without it the whole path survives and is passed to the
# renderer as if it were an item name.
# Find which blocks/components changed in this PR
CHANGED_ITEMS=$(git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD -- registry/blocks/ registry/components/ \
| grep -E '^registry/(blocks|components)/[^/]+/' \
| grep -E '^registry/(blocks|components)/' \
| sed 's|^registry/[^/]*/\([^/]*\)/.*|\1|' \
| sort -u)
+1 -4
View File
@@ -293,13 +293,10 @@ jobs:
node-version: 22
- uses: ./.github/actions/prepare-ffmpeg-bin
- run: bash scripts/ci/install-workspace-dependencies.sh
- run: bun run test:scripts
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
- run: bun run --cwd packages/core build
- run: bun run --cwd packages/core build:hyperframes-runtime
# After the builds: the preview scripts drive the producer, which imports
# @hyperframes/core from dist, so running these first fails on a missing
# module rather than on anything the test is actually checking.
- run: bun run test:scripts
- run: bun run --filter '!@hyperframes/producer' test
producer-source-tests:
-4
View File
@@ -11,7 +11,6 @@
"node_modules/",
"playground/",
"packages/producer/tests/**/src/vendor/**",
"docs/public/catalog/",
"registry/blocks/**/lib/*.iife.js",
"chrome/"
],
@@ -35,9 +34,6 @@
},
{
"files": ["docs/snippets/**/*.jsx", "docs/snippets/**/*.tsx"],
"globals": {
"CodeBlock": "readonly"
},
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
+13 -13
View File
@@ -24,7 +24,7 @@
},
"packages/aws-lambda": {
"name": "@hyperframes/aws-lambda",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"@aws-sdk/client-s3": "^3.700.0",
"@aws-sdk/client-sfn": "^3.700.0",
@@ -57,7 +57,7 @@
},
"packages/cli": {
"name": "@hyperframes/cli",
"version": "0.7.103",
"version": "0.7.78",
"bin": {
"hyperframes": "./bin/hyperframes.mjs",
},
@@ -107,7 +107,7 @@
},
"packages/core": {
"name": "@hyperframes/core",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"@chenglou/pretext": "^0.0.5",
"@hyperframes/lint": "workspace:*",
@@ -132,7 +132,7 @@
},
"packages/engine": {
"name": "@hyperframes/engine",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"@hono/node-server": "^2.0.5",
"@hyperframes/core": "workspace:^",
@@ -151,7 +151,7 @@
},
"packages/gcp-cloud-run": {
"name": "@hyperframes/gcp-cloud-run",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"@google-cloud/storage": "^7.14.0",
"@google-cloud/workflows": "^4.2.0",
@@ -172,7 +172,7 @@
},
"packages/lint": {
"name": "@hyperframes/lint",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"@hyperframes/parsers": "workspace:*",
"htmlparser2": "^10.1.0",
@@ -190,7 +190,7 @@
},
"packages/parsers": {
"name": "@hyperframes/parsers",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"@babel/parser": "^7.27.0",
"acorn": "^8.17.0",
@@ -210,7 +210,7 @@
},
"packages/player": {
"name": "@hyperframes/player",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"@hyperframes/core": "workspace:*",
},
@@ -225,7 +225,7 @@
},
"packages/producer": {
"name": "@hyperframes/producer",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"@fontsource/archivo-black": "^5.2.8",
"@fontsource/eb-garamond": "^5.2.7",
@@ -270,7 +270,7 @@
},
"packages/sdk": {
"name": "@hyperframes/sdk",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"@hyperframes/core": "workspace:*",
"@hyperframes/parsers": "workspace:*",
@@ -300,7 +300,7 @@
},
"packages/shader-transitions": {
"name": "@hyperframes/shader-transitions",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"html2canvas": "^1.4.1",
},
@@ -312,7 +312,7 @@
},
"packages/studio": {
"name": "@hyperframes/studio",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"@codemirror/autocomplete": "^6.20.1",
"@codemirror/commands": "^6.10.3",
@@ -362,7 +362,7 @@
},
"packages/studio-server": {
"name": "@hyperframes/studio-server",
"version": "0.7.103",
"version": "0.7.78",
"dependencies": {
"@hyperframes/core": "workspace:*",
"@hyperframes/parsers": "workspace:*",
+14 -959
View File
@@ -3,979 +3,34 @@ title: "App Showcase"
description: "Fitness app product showcase with three floating smartphone screens"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/app-showcase.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="App Showcase preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/app-showcase.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/app-showcase.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add app-showcase" />
```bash Terminal
npx hyperframes add app-showcase
```
That writes one file: `compositions/app-showcase.html`.
## Source
## Add it to your video
<Accordion title={`app-showcase.html`}>
It runs for 5.5 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>app-showcase</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background-color: #0a0a0f;
}
</style>
</head>
<body>
```html index.html
<div
data-composition-id="app-showcase"
data-width="1920"
data-height="1080"
data-composition-src="compositions/app-showcase.html"
data-start="0"
data-duration="5.5"
>
<!-- Background -->
<div class="s3-bg" id="s3Bg"></div>
<div class="s3-bg-glow" id="s3BgGlow"></div>
<!-- Stage wrapper for ambient scale -->
<div class="s3-stage" id="s3Stage">
<!-- ═══════════════════════════════════════
LEFT PHONE — Lifestyle / Motivational
═══════════════════════════════════════ -->
<div class="s3-phone s3-phone-left" id="s3PhoneLeft">
<div class="s3-bezel">
<div class="s3-notch"></div>
<div class="s3-screen s3-screen-left">
<!-- Abstract exercise shapes -->
<div class="left-shapes">
<svg viewBox="0 0 304 400" xmlns="http://www.w3.org/2000/svg" fill="none">
<!-- Abstract running figure — geometric -->
<circle cx="152" cy="80" r="28" fill="#271f15" opacity="0.15" />
<line
x1="152"
y1="108"
x2="152"
y2="200"
stroke="#271f15"
stroke-width="6"
stroke-linecap="round"
opacity="0.15"
/>
<line
x1="152"
y1="145"
x2="115"
y2="175"
stroke="#271f15"
stroke-width="6"
stroke-linecap="round"
opacity="0.15"
/>
<line
x1="152"
y1="145"
x2="195"
y2="165"
stroke="#271f15"
stroke-width="6"
stroke-linecap="round"
opacity="0.15"
/>
<line
x1="152"
y1="200"
x2="120"
y2="260"
stroke="#271f15"
stroke-width="6"
stroke-linecap="round"
opacity="0.15"
/>
<line
x1="152"
y1="200"
x2="190"
y2="255"
stroke="#271f15"
stroke-width="6"
stroke-linecap="round"
opacity="0.15"
/>
<!-- Decorative arcs -->
<path
d="M60 300 Q152 240 244 300"
stroke="#e4fa72"
stroke-width="3"
fill="none"
opacity="0.5"
/>
<path
d="M40 330 Q152 260 264 330"
stroke="#e4fa72"
stroke-width="2"
fill="none"
opacity="0.3"
/>
</svg>
</div>
<div class="left-bottom">
<div class="left-tagline" id="s3Tagline">Unleash Full Potential</div>
<div class="left-cta">START NOW</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════
CENTER PHONE — Dashboard (Hero)
═══════════════════════════════════════ -->
<div class="s3-phone s3-phone-center" id="s3PhoneCenter">
<div class="s3-bezel">
<div class="s3-notch"></div>
<div class="s3-screen s3-screen-center">
<!-- Header -->
<div class="center-header">
<div class="center-avatar">JM</div>
<div class="center-user-info">
<div class="center-name">James Medrano</div>
<div class="center-subtitle">Premium Member</div>
</div>
<div class="center-bell">
<svg viewBox="0 0 20 20" width="20" height="20" fill="none">
<path
d="M10 2a5 5 0 00-5 5v3l-1.5 2.5h13L15 10V7a5 5 0 00-5-5z"
stroke="#7c857c"
stroke-width="1.4"
stroke-linejoin="round"
/>
<path
d="M8 16.5a2 2 0 004 0"
stroke="#7c857c"
stroke-width="1.4"
stroke-linecap="round"
/>
<circle cx="14" cy="5" r="3" fill="#e4fa72" />
</svg>
</div>
</div>
<!-- Weekly Goal -->
<div class="center-goal">
<div class="goal-label">Weekly Goal</div>
<div class="goal-ring-wrap">
<svg class="goal-ring" viewBox="0 0 140 140">
<circle
class="ring-track"
cx="70"
cy="70"
r="52"
fill="none"
stroke="#333"
stroke-width="10"
/>
<circle
class="ring-fill"
id="s3Ring"
cx="70"
cy="70"
r="52"
fill="none"
stroke="#e4fa72"
stroke-width="10"
stroke-linecap="round"
transform="rotate(-90 70 70)"
/>
</svg>
<div class="goal-count">
<span class="goal-num">2</span>
<span class="goal-sep">/</span>
<span class="goal-den">3</span>
</div>
</div>
<div class="goal-days">
<span class="day done">M</span>
<span class="day done">T</span>
<span class="day">W</span>
<span class="day">T</span>
<span class="day">F</span>
<span class="day">S</span>
<span class="day">S</span>
</div>
</div>
<!-- Your Progress -->
<div class="center-progress">
<div class="progress-header">
<span class="progress-title">Your Progress</span>
<span class="progress-link">See all</span>
</div>
<div class="progress-bars">
<div class="bar-row">
<div class="bar-icon">
<svg viewBox="0 0 16 16" width="14" height="14" fill="none">
<path
d="M8 2l1.5 3H13l-2.5 2 1 3L8 8.5 4.5 10l1-3L3 5h3.5z"
fill="#e4fa72"
/>
</svg>
</div>
<div class="bar-info">
<span class="bar-name">Running</span>
<span class="bar-val">5.2 km</span>
</div>
<div class="bar-track"><div class="bar-value bar-v1" id="s3Bar1"></div></div>
</div>
<div class="bar-row">
<div class="bar-icon">
<svg viewBox="0 0 16 16" width="14" height="14" fill="none">
<circle cx="8" cy="8" r="5" stroke="#e4fa72" stroke-width="2" />
</svg>
</div>
<div class="bar-info">
<span class="bar-name">Cycling</span>
<span class="bar-val">12.8 km</span>
</div>
<div class="bar-track"><div class="bar-value bar-v2" id="s3Bar2"></div></div>
</div>
<div class="bar-row">
<div class="bar-icon">
<svg viewBox="0 0 16 16" width="14" height="14" fill="none">
<rect
x="3"
y="6"
width="10"
height="6"
rx="1"
stroke="#e4fa72"
stroke-width="1.5"
/>
<path d="M6 6V4a2 2 0 014 0v2" stroke="#e4fa72" stroke-width="1.5" />
</svg>
</div>
<div class="bar-info">
<span class="bar-name">Strength</span>
<span class="bar-val">45 min</span>
</div>
<div class="bar-track"><div class="bar-value bar-v3" id="s3Bar3"></div></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════
RIGHT PHONE — Calories Data
═══════════════════════════════════════ -->
<div class="s3-phone s3-phone-right" id="s3PhoneRight">
<div class="s3-bezel">
<div class="s3-notch"></div>
<div class="s3-screen s3-screen-right">
<div class="right-header">
<div class="right-title">Burned Calories</div>
<div class="right-value">740 <span class="right-unit">kcal</span></div>
</div>
<div class="right-graph-wrap">
<svg class="right-graph" viewBox="0 0 260 130" xmlns="http://www.w3.org/2000/svg">
<!-- Grid lines -->
<line
x1="10"
y1="25"
x2="250"
y2="25"
stroke="#d9e0bc"
stroke-width="0.5"
opacity="0.5"
/>
<line
x1="10"
y1="55"
x2="250"
y2="55"
stroke="#d9e0bc"
stroke-width="0.5"
opacity="0.5"
/>
<line
x1="10"
y1="85"
x2="250"
y2="85"
stroke="#d9e0bc"
stroke-width="0.5"
opacity="0.5"
/>
<line
x1="10"
y1="115"
x2="250"
y2="115"
stroke="#d9e0bc"
stroke-width="0.5"
opacity="0.5"
/>
<!-- Area fill -->
<path
class="graph-area"
id="s3GraphArea"
d="M10,105 L50,80 90,90 130,45 170,60 210,25 250,35 250,115 10,115 Z"
fill="#e4fa72"
opacity="0.15"
/>
<!-- Line -->
<polyline
class="graph-line"
id="s3GraphLine"
points="10,105 50,80 90,90 130,45 170,60 210,25 250,35"
fill="none"
stroke="#e4fa72"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<!-- Dots -->
<circle class="graph-dot" cx="10" cy="105" r="3.5" fill="#e4fa72" />
<circle class="graph-dot" cx="50" cy="80" r="3.5" fill="#e4fa72" />
<circle class="graph-dot" cx="90" cy="90" r="3.5" fill="#e4fa72" />
<circle class="graph-dot" cx="130" cy="45" r="3.5" fill="#e4fa72" />
<circle class="graph-dot" cx="170" cy="60" r="3.5" fill="#e4fa72" />
<circle class="graph-dot" cx="210" cy="25" r="3.5" fill="#e4fa72" />
<circle class="graph-dot" cx="250" cy="35" r="3.5" fill="#e4fa72" />
</svg>
<div class="right-labels">
<span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span
><span>Sat</span><span>Sun</span>
</div>
</div>
<div class="right-stats">
<div class="stat-pill">
<span class="stat-dot" style="background: #e4fa72"></span>
<span class="stat-label">Active</span>
<span class="stat-num">540</span>
</div>
<div class="stat-pill">
<span class="stat-dot" style="background: #7c857c"></span>
<span class="stat-label">Rest</span>
<span class="stat-num">200</span>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap");
[data-composition-id="app-showcase"] {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
font-family: "DM Sans", sans-serif;
color: #271f15;
}
/* ── Background ── */
[data-composition-id="app-showcase"] .s3-bg {
position: absolute;
inset: 0;
background: linear-gradient(160deg, #f1f2ec 0%, #e8ecda 40%, #f1f2ec 100%);
}
[data-composition-id="app-showcase"] .s3-bg-glow {
position: absolute;
width: 900px;
height: 900px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: radial-gradient(circle, rgba(228, 250, 114, 0.18) 0%, transparent 70%);
border-radius: 50%;
}
/* ── Stage ── */
[data-composition-id="app-showcase"] .s3-stage {
position: absolute;
inset: 0;
transform-origin: center center;
}
/* ── Phones ── */
[data-composition-id="app-showcase"] .s3-phone {
position: absolute;
width: 320px;
height: 650px;
top: 215px;
left: 800px;
}
[data-composition-id="app-showcase"] .s3-phone-center {
z-index: 30;
}
[data-composition-id="app-showcase"] .s3-phone-left {
z-index: 20;
}
[data-composition-id="app-showcase"] .s3-phone-right {
z-index: 20;
}
/* ── Bezel ── */
[data-composition-id="app-showcase"] .s3-bezel {
width: 100%;
height: 100%;
border-radius: 42px;
background: #271f15;
padding: 8px;
box-shadow:
0 24px 64px rgba(39, 31, 21, 0.35),
0 4px 16px rgba(39, 31, 21, 0.15);
position: relative;
overflow: hidden;
}
[data-composition-id="app-showcase"] .s3-notch {
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 26px;
background: #271f15;
border-radius: 0 0 18px 18px;
z-index: 5;
}
/* ── Screens ── */
[data-composition-id="app-showcase"] .s3-screen {
width: 100%;
height: 100%;
border-radius: 36px;
overflow: hidden;
position: relative;
}
/* Left screen */
[data-composition-id="app-showcase"] .s3-screen-left {
background: linear-gradient(160deg, #d9e0bc 0%, #c8d4a0 50%, #b8c78a 100%);
display: flex;
flex-direction: column;
justify-content: flex-end;
}
[data-composition-id="app-showcase"] .left-shapes {
position: absolute;
top: 40px;
left: 0;
width: 100%;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
[data-composition-id="app-showcase"] .left-shapes svg {
width: 200px;
height: 280px;
}
[data-composition-id="app-showcase"] .left-bottom {
padding: 0 28px 40px;
position: relative;
z-index: 2;
}
[data-composition-id="app-showcase"] .left-tagline {
font-size: 28px;
font-weight: 700;
color: #271f15;
line-height: 1.15;
margin-bottom: 16px;
letter-spacing: -0.02em;
}
[data-composition-id="app-showcase"] .left-cta {
display: inline-block;
background: #271f15;
color: #e4fa72;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.08em;
padding: 10px 28px;
border-radius: 24px;
}
/* Center screen */
[data-composition-id="app-showcase"] .s3-screen-center {
background: #1a1a1a;
padding: 42px 22px 22px;
display: flex;
flex-direction: column;
gap: 18px;
}
[data-composition-id="app-showcase"] .center-header {
display: flex;
align-items: center;
gap: 10px;
}
[data-composition-id="app-showcase"] .center-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #e4fa72, #d9e0bc);
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
font-weight: 700;
color: #271f15;
flex-shrink: 0;
}
[data-composition-id="app-showcase"] .center-user-info {
flex: 1;
}
[data-composition-id="app-showcase"] .center-name {
font-size: 18px;
font-weight: 700;
color: #f1f2ec;
line-height: 1.2;
}
[data-composition-id="app-showcase"] .center-subtitle {
font-size: 12px;
font-weight: 400;
color: #7c857c;
margin-top: 1px;
}
[data-composition-id="app-showcase"] .center-bell {
flex-shrink: 0;
}
/* Weekly Goal */
[data-composition-id="app-showcase"] .center-goal {
background: #242424;
border-radius: 20px;
padding: 18px 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
[data-composition-id="app-showcase"] .goal-label {
font-size: 15px;
font-weight: 500;
color: #7c857c;
align-self: flex-start;
}
[data-composition-id="app-showcase"] .goal-ring-wrap {
position: relative;
width: 130px;
height: 130px;
}
[data-composition-id="app-showcase"] .goal-ring {
width: 100%;
height: 100%;
}
[data-composition-id="app-showcase"] .ring-track {
opacity: 0.3;
}
[data-composition-id="app-showcase"] .goal-count {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
}
[data-composition-id="app-showcase"] .goal-num {
font-size: 36px;
font-weight: 700;
color: #e4fa72;
}
[data-composition-id="app-showcase"] .goal-sep {
font-size: 24px;
font-weight: 400;
color: #7c857c;
margin: 0 2px;
}
[data-composition-id="app-showcase"] .goal-den {
font-size: 24px;
font-weight: 400;
color: #7c857c;
}
[data-composition-id="app-showcase"] .goal-days {
display: flex;
gap: 8px;
}
[data-composition-id="app-showcase"] .day {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 500;
color: #7c857c;
background: #1a1a1a;
}
[data-composition-id="app-showcase"] .day.done {
background: #e4fa72;
color: #271f15;
font-weight: 700;
}
/* Progress section */
[data-composition-id="app-showcase"] .center-progress {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
[data-composition-id="app-showcase"] .progress-header {
display: flex;
justify-content: space-between;
align-items: center;
}
[data-composition-id="app-showcase"] .progress-title {
font-size: 15px;
font-weight: 500;
color: #f1f2ec;
}
[data-composition-id="app-showcase"] .progress-link {
font-size: 12px;
font-weight: 400;
color: #e4fa72;
}
[data-composition-id="app-showcase"] .progress-bars {
display: flex;
flex-direction: column;
gap: 12px;
}
[data-composition-id="app-showcase"] .bar-row {
display: flex;
align-items: center;
gap: 10px;
}
[data-composition-id="app-showcase"] .bar-icon {
width: 28px;
height: 28px;
border-radius: 8px;
background: #2a2a2a;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
[data-composition-id="app-showcase"] .bar-info {
width: 70px;
flex-shrink: 0;
}
[data-composition-id="app-showcase"] .bar-name {
display: block;
font-size: 12px;
font-weight: 500;
color: #f1f2ec;
line-height: 1.2;
}
[data-composition-id="app-showcase"] .bar-val {
display: block;
font-size: 10px;
font-weight: 400;
color: #7c857c;
}
[data-composition-id="app-showcase"] .bar-track {
flex: 1;
height: 8px;
background: #2a2a2a;
border-radius: 4px;
overflow: hidden;
}
[data-composition-id="app-showcase"] .bar-value {
height: 100%;
border-radius: 4px;
background: #e4fa72;
width: 0%;
}
/* Right screen */
[data-composition-id="app-showcase"] .s3-screen-right {
background: #f1f2ec;
padding: 42px 20px 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
[data-composition-id="app-showcase"] .right-header {
display: flex;
flex-direction: column;
gap: 4px;
}
[data-composition-id="app-showcase"] .right-title {
font-size: 14px;
font-weight: 500;
color: #7c857c;
}
[data-composition-id="app-showcase"] .right-value {
font-size: 40px;
font-weight: 700;
color: #271f15;
line-height: 1;
letter-spacing: -0.03em;
}
[data-composition-id="app-showcase"] .right-unit {
font-size: 20px;
font-weight: 400;
color: #7c857c;
}
[data-composition-id="app-showcase"] .right-graph-wrap {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
[data-composition-id="app-showcase"] .right-graph {
width: 100%;
flex: 1;
}
[data-composition-id="app-showcase"] .right-labels {
display: flex;
justify-content: space-between;
padding: 0 6px;
font-size: 10px;
font-weight: 400;
color: #7c857c;
}
[data-composition-id="app-showcase"] .right-stats {
display: flex;
gap: 12px;
}
[data-composition-id="app-showcase"] .stat-pill {
display: flex;
align-items: center;
gap: 6px;
background: #e8ecda;
border-radius: 20px;
padding: 8px 14px;
}
[data-composition-id="app-showcase"] .stat-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
[data-composition-id="app-showcase"] .stat-label {
font-size: 12px;
font-weight: 400;
color: #7c857c;
}
[data-composition-id="app-showcase"] .stat-num {
font-size: 13px;
font-weight: 700;
color: #271f15;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
(function () {
var S = '[data-composition-id="app-showcase"] ';
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// ── SVG setup ──
var graphLine = document.querySelector(S + "#s3GraphLine");
var graphLen = graphLine.getTotalLength();
gsap.set(graphLine, { strokeDasharray: graphLen, strokeDashoffset: graphLen });
var graphArea = document.querySelector(S + "#s3GraphArea");
gsap.set(graphArea, { opacity: 0 });
var graphDots = document.querySelectorAll(S + ".graph-dot");
gsap.set(graphDots, { opacity: 0, scale: 0 });
var CIRC = 2 * 3.14159265 * 52;
gsap.set(S + "#s3Ring", { strokeDasharray: CIRC, strokeDashoffset: CIRC });
// ── Initial states ──
gsap.set(S + ".s3-bg", { opacity: 0 });
gsap.set(S + ".s3-bg-glow", { opacity: 0 });
// Center phone: starts scaled down + hidden
gsap.set(S + "#s3PhoneCenter", { opacity: 0, scale: 0.85 });
// Side phones: start stacked behind center (same position), hidden
gsap.set(S + "#s3PhoneLeft", { opacity: 0, x: 0, rotation: 0 });
gsap.set(S + "#s3PhoneRight", { opacity: 0, x: 0, rotation: 0 });
gsap.set(S + ".bar-value", { width: "0%" });
gsap.set(S + "#s3Tagline", { opacity: 0, y: 20 });
gsap.set(S + ".left-cta", { opacity: 0, y: 12 });
// ════════════════════════════════════
// BUILD PHASE (02s)
// ════════════════════════════════════
// Background
tl.to(S + ".s3-bg", { opacity: 1, duration: 0.8, ease: "power1.out" }, 0);
tl.to(S + ".s3-bg-glow", { opacity: 1, duration: 1.2, ease: "power1.out" }, 0.2);
// Center phone — scales up first
tl.to(
S + "#s3PhoneCenter",
{
scale: 1,
opacity: 1,
duration: 0.7,
ease: "back.out(1.4)",
},
0.2,
);
// Side phones: opacity matches center (0.7s, back.out), fan starts at 0.1
tl.to(S + "#s3PhoneLeft", { opacity: 1, duration: 0.7, ease: "back.out(1.4)" }, 0.3);
tl.to(
S + "#s3PhoneLeft",
{
x: -340,
rotation: -12,
duration: 1.2,
ease: "expo.out",
},
0.3,
);
tl.to(S + "#s3PhoneRight", { opacity: 1, duration: 0.7, ease: "back.out(1.4)" }, 0.4);
tl.to(
S + "#s3PhoneRight",
{
x: 340,
rotation: 12,
duration: 1.2,
ease: "expo.out",
},
0.4,
);
// ════════════════════════════════════
// BREATHE PHASE (24s)
// ════════════════════════════════════
// Progress ring fills to 2/3
tl.to(
S + "#s3Ring",
{
strokeDashoffset: CIRC * (1 - 2 / 3),
duration: 1.2,
ease: "circ.out",
},
1.8,
);
// Activity bars stagger in
tl.to(S + "#s3Bar1", { width: "75%", duration: 0.8, ease: "power3.out" }, 2.1);
tl.to(S + "#s3Bar2", { width: "50%", duration: 0.8, ease: "power3.out" }, 2.3);
tl.to(S + "#s3Bar3", { width: "90%", duration: 0.8, ease: "power3.out" }, 2.5);
// Graph line draws on
tl.to(
graphLine,
{
strokeDashoffset: 0,
duration: 1.4,
ease: "sine.inOut",
},
2.0,
);
// Graph area fades in
tl.to(graphArea, { opacity: 0.15, duration: 0.8, ease: "power1.out" }, 2.6);
// Graph dots pop in
tl.to(
graphDots,
{
opacity: 1,
scale: 1,
duration: 0.3,
ease: "back.out(2)",
stagger: { each: 0.06, ease: "power2.in" },
},
3.0,
);
// Tagline and CTA on left phone
tl.to(
S + "#s3Tagline",
{
opacity: 1,
y: 0,
duration: 0.6,
ease: "power2.out",
},
2.2,
);
tl.to(
S + ".left-cta",
{
opacity: 1,
y: 0,
duration: 0.5,
ease: "power2.out",
},
2.5,
);
// Ambient slow scale push
tl.to(
S + "#s3Stage",
{
scale: 1.02,
duration: 3.5,
ease: "sine.inOut",
},
2.0,
);
// ════════════════════════════════════
// RESOLVE PHASE (45.5s)
// ════════════════════════════════════
// Subtle glow pulse on lime accent elements
tl.to(S + ".ring-fill", { opacity: 0.65, duration: 0.6, ease: "sine.inOut" }, 4.0);
tl.to(S + ".ring-fill", { opacity: 1, duration: 0.6, ease: "sine.inOut" }, 4.6);
tl.to(S + ".bar-value", { opacity: 0.7, duration: 0.6, ease: "sine.inOut" }, 4.1);
tl.to(S + ".bar-value", { opacity: 1, duration: 0.6, ease: "sine.inOut" }, 4.7);
tl.to(graphLine, { opacity: 0.65, duration: 0.6, ease: "sine.inOut" }, 4.05);
tl.to(graphLine, { opacity: 1, duration: 0.6, ease: "sine.inOut" }, 4.65);
window.__timelines["app-showcase"] = tl;
})();
</script>
</div>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="app-showcase" data-composition-src="compositions/app-showcase.html" data-start="0" data-duration="5.5" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+12 -330
View File
@@ -3,352 +3,34 @@ title: "Apple Money Count"
description: "Apple-style finance counter that counts from $0 to $10,000, flashes green, and bursts money icons with sound."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/apple-money-count.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Apple Money Count preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/apple-money-count.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/apple-money-count.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add apple-money-count" />
```bash Terminal
npx hyperframes add apple-money-count
```
That writes `compositions/apple-money-count.html`, plus 1 supporting file under `assets/`.
## Source
## Add it to your video
<Accordion title={`apple-money-count.html`}>
It runs for 5 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--bg, #fdfefe);
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--bg, #fdfefe);
color: var(--fg, #111315);
font-family: var(
--font-display,
-apple-system,
BlinkMacSystemFont,
"Helvetica Neue",
Arial,
sans-serif
);
}
.green-flash {
position: absolute;
inset: 0;
z-index: 1;
background: var(--accent, #30d158);
opacity: 0;
}
.stage-content {
position: relative;
z-index: 5;
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
padding: var(--space-3, 120px) var(--space-3, 160px);
}
.amount-wrap {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-width: 1080px;
min-height: 260px;
}
.amount {
position: relative;
color: var(--fg, #111315);
font-size: 190px;
font-weight: 900;
line-height: 0.9;
letter-spacing: 0;
font-variant-numeric: tabular-nums;
text-align: center;
white-space: nowrap;
text-shadow:
0 3px 0 var(--bg, rgba(255, 255, 255, 0.58)),
0 18px 36px var(--fg, rgba(17, 19, 21, 0.14)),
0 42px 92px var(--fg, rgba(17, 19, 21, 0.1));
will-change: transform, color, opacity, text-shadow;
}
.money-burst {
position: absolute;
inset: 0;
z-index: 8;
pointer-events: none;
}
.money-icon {
position: absolute;
left: 50%;
top: 50%;
display: grid;
place-items: center;
opacity: 0;
filter: drop-shadow(0 20px 24px var(--brand, rgba(7, 84, 31, 0.2)));
will-change: transform, opacity, filter;
}
.money-icon.bill {
width: 96px;
height: 52px;
border-radius: var(--radius, 10px);
background: var(--accent, #30d158);
box-shadow:
inset 0 0 0 3px var(--brand, rgba(7, 84, 31, 0.2)),
inset 0 -10px 18px var(--brand, rgba(7, 84, 31, 0.1)),
0 16px 26px var(--brand, rgba(7, 84, 31, 0.2)),
0 34px 64px var(--brand, rgba(7, 84, 31, 0.16));
}
.money-icon.bill::before {
position: absolute;
inset: var(--space-1, 9px) var(--space-2, 16px);
border: 2px solid var(--border, rgba(7, 84, 31, 0.28));
border-radius: 999px;
content: "";
}
.money-icon.coin {
width: 64px;
height: 64px;
border-radius: 50%;
background: radial-gradient(
circle at 34% 30%,
var(--accent-2, #fff7a6) 0%,
var(--accent, #ffd54f) 40%,
var(--brand, #d9a514) 100%
);
box-shadow:
inset 0 0 0 4px var(--brand, rgba(111, 76, 0, 0.12)),
inset 0 -8px 14px var(--brand, rgba(111, 76, 0, 0.1)),
0 16px 30px var(--brand, rgba(111, 76, 0, 0.18)),
0 34px 58px var(--brand, rgba(111, 76, 0, 0.12));
}
.money-icon .mark {
position: relative;
z-index: 1;
color: var(--fg, #07541f);
font-size: 30px;
font-weight: 900;
line-height: 1;
}
.money-icon.coin .mark {
color: var(--fg, #6f4c00);
font-size: 28px;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="apple-money-count"
data-root="true"
data-composition-src="compositions/apple-money-count.html"
data-start="0"
data-duration="5"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<div id="greenFlash" class="green-flash"></div>
<div class="stage-content">
<div class="amount-wrap">
<div id="amount" class="amount">$0</div>
</div>
</div>
<div id="moneyBurst" class="money-burst" data-layout-allow-overflow></div>
<audio
id="sfxTrack"
data-start="0"
data-duration="5"
data-track-index="9"
src="assets/sfx-production.wav"
data-volume="0.92"
></audio>
</div>
<script>
window.__timelines = window.__timelines || {};
const amount = document.querySelector("#amount");
const moneyBurst = document.querySelector("#moneyBurst");
const countState = { value: 0 };
function formatMoney(value) {
const bounded = Math.max(0, Math.min(10000, value));
const rounded = Math.round(bounded);
return "$" + rounded.toLocaleString("en-US");
}
function renderAmount(value) {
amount.textContent = formatMoney(value);
}
const moneyIcons = [];
const moneySpecs = [];
for (let i = 0; i < 62; i += 1) {
const icon = document.createElement("div");
const isCoin = i % 3 === 0;
icon.className = "money-icon " + (isCoin ? "coin" : "bill");
icon.innerHTML = '<span class="mark">$</span>';
moneyBurst.appendChild(icon);
moneyIcons.push(icon);
const angle = i * 2.399963229728653;
const ring = i % 5;
const radiusX = 260 + ring * 145 + (i % 7) * 12;
const radiusY = 160 + ring * 78 + (i % 6) * 12;
const offsetX = (i % 4) * 24 - 36;
const offsetY = (i % 5) * 18 - 36;
const x = Math.max(-865, Math.min(865, Math.cos(angle) * radiusX + offsetX));
const y = Math.max(-465, Math.min(465, Math.sin(angle * 1.13) * radiusY + offsetY));
moneySpecs.push({
x,
y,
delay: (i % 8) * 0.025,
duration: 0.74 + (i % 5) * 0.045,
fadeDelay: (i % 5) * 0.05,
rotation: ((i * 43) % 160) - 80,
scale: isCoin ? 0.72 + (i % 4) * 0.08 : 0.68 + (i % 5) * 0.07,
});
}
renderAmount(0);
const tl = gsap.timeline({
paused: true,
defaults: { ease: "power3.out" },
});
gsap.set(moneyIcons, {
x: 0,
y: 0,
xPercent: -50,
yPercent: -50,
opacity: 0,
scale: 0.18,
transformOrigin: "50% 50%",
});
tl.from(
"#amount",
{
y: 26,
opacity: 0,
scale: 0.985,
duration: 0.45,
ease: "power2.out",
},
0,
);
tl.to(
countState,
{
value: 10000,
duration: 3.16,
ease: "none",
onUpdate: () => renderAmount(countState.value),
onComplete: () => renderAmount(10000),
},
0,
);
tl.to(
"#amount",
{
color: "var(--accent, #30d158)",
textShadow:
"0 3px 0 var(--bg, rgba(255,255,255,0.52)), 0 18px 40px var(--accent, rgba(48,209,88,0.3)), 0 46px 96px var(--brand, rgba(7,84,31,0.2))",
duration: 0.18,
},
3.16,
);
tl.to("#amount", { scale: 1.06, duration: 0.16, ease: "back.out(2.2)" }, 3.16);
tl.to("#amount", { scale: 1, duration: 0.23, ease: "power2.out" }, 3.33);
tl.to("#greenFlash", { opacity: 0.34, duration: 0.08, ease: "none" }, 3.16);
tl.to("#greenFlash", { opacity: 0.22, duration: 0.76, ease: "none" }, 3.24);
tl.to("#greenFlash", { opacity: 0, duration: 0.16, ease: "power2.out" }, 4);
moneyIcons.forEach((icon, i) => {
const spec = moneySpecs[i];
tl.to(
icon,
{
x: spec.x,
y: spec.y,
rotation: spec.rotation,
scale: spec.scale,
opacity: 1,
duration: spec.duration,
ease: "power4.out",
},
3.28 + spec.delay,
);
tl.to(
icon,
{
y: spec.y + (spec.y < 0 ? -58 : 58),
scale: spec.scale * 0.54,
opacity: 0,
duration: 0.38,
ease: "power2.in",
},
4.18 + spec.fadeDelay,
);
});
tl.to("#amount", { opacity: 0, scale: 0.985, duration: 0.28, ease: "power2.in" }, 4.36);
window.__timelines["apple-money-count"] = tl;
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="apple-money-count" data-composition-src="compositions/apple-money-count.html" data-start="0" data-duration="5" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
-660
View File
@@ -1,660 +0,0 @@
---
title: "Bar Chart Race"
description: "Ranked bars grow and overtake each other as a time series advances, with the axis rescaling as values climb and the accent handing over to whoever leads"
---
import { InstallCommand } from "/snippets/install-command.jsx";
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
<VariablesExplorer
previewSrc="/public/catalog/preview/blocks/bar-chart-race.html"
compositionId="bar-chart-race"
compositionSrc="compositions/bar-chart-race.html"
variables={[{"id":"title","type":"string","label":"Title","default":"Streaming Subscribers by Service"},{"id":"subtitle","type":"string","label":"Subtitle","default":"Ranked by reported subscribers"},{"id":"periods","type":"string","label":"Period labels (comma separated)","default":"2019, 2020, 2021, 2022, 2023, 2024"},{"id":"series","type":"string","label":"Series (one per line: Name: v1, v2, ...)","default":"Northwind: 42, 58, 71, 96, 118, 131\nCobalt: 30, 46, 68, 92, 126, 168\nFerry: 55, 62, 66, 70, 74, 79\nMarlow: 18, 33, 52, 61, 88, 104\nAster: 25, 28, 44, 58, 63, 72\nPell: 12, 20, 39, 47, 55, 90\nQuill: 8, 11, 15, 24, 40, 66\nDunmore: 35, 37, 38, 40, 42, 44"},{"id":"barCount","type":"number","label":"Bars shown","default":6,"min":3,"max":12,"step":1},{"id":"periodDuration","type":"number","label":"Seconds per period","default":2,"min":0.4,"max":6,"step":0.1},{"id":"valuePrefix","type":"string","label":"Value prefix","default":"$"},{"id":"valueSuffix","type":"string","label":"Value suffix","default":"M"},{"id":"valueDecimals","type":"number","label":"Value decimals","default":0,"min":0,"max":3,"step":1},{"id":"accent","type":"color","label":"Leader accent","default":"#c8452d"}]}
>
```html bar-chart-race.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Bar Chart Race</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background-color: #f5f3ef;
}
</style>
</head>
<body>
<div
id="bcr-root"
data-composition-id="bar-chart-race"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="12"
data-composition-variables='[
{"id":"title","type":"string","label":"Title","default":"Streaming Subscribers by Service"},
{"id":"subtitle","type":"string","label":"Subtitle","default":"Ranked by reported subscribers"},
{"id":"periods","type":"string","label":"Period labels (comma separated)","default":"2019, 2020, 2021, 2022, 2023, 2024"},
{"id":"series","type":"string","label":"Series (one per line: Name: v1, v2, ...)","default":"Northwind: 42, 58, 71, 96, 118, 131\nCobalt: 30, 46, 68, 92, 126, 168\nFerry: 55, 62, 66, 70, 74, 79\nMarlow: 18, 33, 52, 61, 88, 104\nAster: 25, 28, 44, 58, 63, 72\nPell: 12, 20, 39, 47, 55, 90\nQuill: 8, 11, 15, 24, 40, 66\nDunmore: 35, 37, 38, 40, 42, 44"},
{"id":"barCount","type":"number","label":"Bars shown","default":6,"min":3,"max":12,"step":1},
{"id":"periodDuration","type":"number","label":"Seconds per period","default":2,"min":0.4,"max":6,"step":0.1},
{"id":"valuePrefix","type":"string","label":"Value prefix","default":"$"},
{"id":"valueSuffix","type":"string","label":"Value suffix","default":"M"},
{"id":"valueDecimals","type":"number","label":"Value decimals","default":0,"min":0,"max":3,"step":1},
{"id":"accent","type":"color","label":"Leader accent","default":"#c8452d"}
]'
>
<div id="bcr-stage" class="clip" data-start="0" data-duration="12" data-track-index="0">
<div id="bcr-bg"></div>
<div id="bcr-head">
<div id="bcr-head-left">
<h1 id="bcr-title">Streaming Subscribers by Service</h1>
<p id="bcr-subtitle">Ranked by reported subscribers</p>
</div>
<div id="bcr-head-right">
<span id="bcr-period-caption">Period</span>
<span id="bcr-period">2019</span>
</div>
</div>
<!-- Rows live here, one per series, positioned by rank. -->
<div id="bcr-plot"></div>
<!-- Axis is painted above the rows: the tick labels sliding left is the
visible evidence that the scale is rescaling as values climb. -->
<div id="bcr-axis"></div>
<p id="bcr-source">Placeholder data</p>
</div>
<style>
#bcr-root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
font-family: Inter, sans-serif;
color: #1f1d1b;
}
#bcr-stage {
position: absolute;
inset: 0;
}
/* Scene fill goes on a full-bleed child, never the composition root. */
#bcr-bg {
position: absolute;
inset: 0;
background-color: #f5f3ef;
}
#bcr-head {
position: absolute;
top: 58px;
left: 96px;
width: 1728px;
display: flex;
align-items: flex-start;
justify-content: space-between;
}
#bcr-title {
font-size: 46px;
font-weight: 700;
letter-spacing: -0.015em;
line-height: 1.1;
}
#bcr-subtitle {
margin-top: 10px;
font-size: 19px;
font-weight: 400;
color: #6b6560;
}
#bcr-head-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
#bcr-period-caption {
font-size: 15px;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #6b6560;
}
#bcr-period {
margin-top: 4px;
font-size: 72px;
font-weight: 700;
line-height: 1;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
#bcr-plot {
position: absolute;
left: 0;
top: 236px;
width: 1920px;
height: 760px;
overflow: hidden;
}
.bcr-row {
position: absolute;
left: 0;
top: 0;
width: 1920px;
}
/* Rows are transparent so both bars of an overtake stay visible sliding
past each other; only the two TEXT blocks carry an opaque backing, so
the front row's label covers the one it crosses instead of tangling
with it. */
.bcr-name {
position: absolute;
left: 96px;
width: 272px;
top: 50%;
transform: translateY(-50%);
text-align: right;
font-size: 26px;
font-weight: 600;
line-height: 1.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background-color: #f5f3ef;
}
.bcr-bar {
position: absolute;
left: 400px;
top: 50%;
transform: translateY(-50%);
border-radius: 3px;
background-color: #1f1d1b;
}
.bcr-value {
position: absolute;
left: 404px;
top: 50%;
padding: 0 12px;
font-size: 26px;
font-weight: 600;
line-height: 1.7;
font-variant-numeric: tabular-nums;
white-space: nowrap;
background-color: #f5f3ef;
}
#bcr-axis {
position: absolute;
inset: 0;
z-index: 5000;
}
.bcr-tick-line {
position: absolute;
top: 232px;
left: 0;
width: 1px;
height: 768px;
background-color: rgba(31, 29, 27, 0.11);
}
.bcr-tick-line.bcr-tick-zero {
background-color: rgba(31, 29, 27, 0.5);
}
.bcr-tick-label {
position: absolute;
top: 198px;
left: 0;
font-size: 17px;
font-weight: 500;
font-variant-numeric: tabular-nums;
color: #6b6560;
white-space: nowrap;
}
#bcr-source {
position: absolute;
left: 96px;
top: 1014px;
font-size: 16px;
color: #6b6560;
}
</style>
<script>
(function () {
"use strict";
// ---- variables -------------------------------------------------
// The declaration attribute is the single source of truth: when the
// runtime is absent (raw file open) we parse the same attribute.
function readVars() {
// Read the declaration off THIS block's own root, never
// document.documentElement: mounted as a sub-composition the block's
// <html> is discarded and documentElement is the HOST's, so a
// documentElement read returns the wrong element (or null) and the
// block renders with no series at all. Runtime / host / --variables
// values are layered on top as overrides, so the block does not
// depend on the runtime being injected before this script runs.
var out = {};
var root = document.getElementById("bcr-root");
try {
JSON.parse(root.getAttribute("data-composition-variables")).forEach(function (d) {
out[d.id] = d.default;
});
} catch (err) {
// Declaration unreadable; the per-variable literal fallbacks below
// still produce a renderable composition.
}
var hf = window.__hyperframes;
var overrides =
hf && typeof hf.getVariables === "function"
? hf.getVariables()
: window.__hfVariables;
if (overrides && typeof overrides === "object") {
for (var key in overrides) {
if (overrides[key] !== undefined) out[key] = overrides[key];
}
}
return out;
}
function num(value, fallback, lo, hi) {
var n = typeof value === "number" ? value : Number.parseFloat(value);
if (!Number.isFinite(n)) n = fallback;
return Math.min(hi, Math.max(lo, n));
}
function str(value, fallback) {
return typeof value === "string" && value.length > 0 ? value : fallback;
}
var V = readVars();
var ACCENT = str(V.accent, "#c8452d");
var BAR_COLOR = "#1f1d1b";
var BAR_COUNT = Math.round(num(V.barCount, 6, 1, 24));
var PERIOD_DURATION = num(V.periodDuration, 2, 0.1, 30);
var DECIMALS = Math.round(num(V.valueDecimals, 0, 0, 6));
var PREFIX = typeof V.valuePrefix === "string" ? V.valuePrefix : "";
var SUFFIX = typeof V.valueSuffix === "string" ? V.valueSuffix : "";
// ---- data format ------------------------------------------------
// Wide table, one series per line: "Name: v1, v2, v3".
// `;` is accepted as a line separator so the whole table survives a
// single-line text input. Short rows hold their last value.
function parsePeriods(text) {
return String(text)
.split(",")
.map(function (s) {
return s.trim();
})
.filter(function (s) {
return s.length > 0;
});
}
function parseSeries(text, periodCount) {
var out = [];
var lines = String(text).split(/[\n;]+/);
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
var split = line.indexOf(":");
if (split < 0) continue;
var label = line.slice(0, split).trim();
if (!label) continue;
var values = line
.slice(split + 1)
.split(",")
.map(function (s) {
return Number.parseFloat(s.trim());
})
.filter(function (n) {
return Number.isFinite(n);
});
if (values.length === 0) continue;
while (values.length < periodCount) values.push(values[values.length - 1]);
out.push({ label: label, values: values.slice(0, periodCount) });
}
return out;
}
var PERIODS = parsePeriods(str(V.periods, "2019, 2020, 2021, 2022, 2023, 2024"));
if (PERIODS.length === 0) PERIODS = ["1"];
var SERIES = parseSeries(str(V.series, ""), PERIODS.length);
var T = PERIODS.length;
var N = SERIES.length;
// ---- closed-form state ------------------------------------------
// k = 10 interpolated keyframes per period (Bostock's number). Rank is
// computed at each keyframe from the interpolated value and baked once;
// a bar's on-screen row is then solved FROM rank, so two bars can never
// cross without swapping. Everything below is a pure function of t.
var K = 10;
var KF_DURATION = PERIOD_DURATION / K;
var KF_COUNT = T > 1 ? (T - 1) * K + 1 : 1;
var RACE_SECONDS = T > 1 ? (T - 1) * PERIOD_DURATION : 0;
function clamp(v, lo, hi) {
return v < lo ? lo : v > hi ? hi : v;
}
function valueAt(t, series) {
if (T < 2) return series.values[0];
var u = t / PERIOD_DURATION;
var i = clamp(Math.floor(u), 0, T - 2);
var f = clamp(u - i, 0, 1);
return series.values[i] + (series.values[i + 1] - series.values[i]) * f;
}
// ranks[m][j] = integer rank of series j at keyframe m (0 = leader).
var RANKS = [];
for (var m = 0; m < KF_COUNT; m++) {
var tm = m * KF_DURATION;
var order = SERIES.map(function (s, j) {
return { j: j, v: valueAt(tm, s) };
});
// Descending by value; index breaks ties so the sort is total and
// therefore identical on every evaluation.
order.sort(function (a, b) {
return b.v - a.v || a.j - b.j;
});
var row = new Array(N);
for (var r = 0; r < order.length; r++) row[order[r].j] = r;
RANKS.push(row);
}
function smoothstep(x) {
return x * x * (3 - 2 * x);
}
// Continuous row position solved from the baked ranks. A rank change
// between two adjacent keyframes IS the swap, and it plays out over one
// keyframe interval (PERIOD_DURATION / 10) instead of snapping.
function rankPosAt(t, j) {
if (KF_COUNT < 2) return RANKS[0][j];
var m = t / KF_DURATION;
var m0 = clamp(Math.floor(m), 0, KF_COUNT - 2);
var e = smoothstep(clamp(m - m0, 0, 1));
var a = RANKS[m0][j];
var b = RANKS[m0 + 1][j];
return a + (b - a) * e;
}
// ---- formatting --------------------------------------------------
function formatValue(v) {
return (
PREFIX +
v.toLocaleString("en-US", {
minimumFractionDigits: DECIMALS,
maximumFractionDigits: DECIMALS,
}) +
SUFFIX
);
}
function hexToRgb(hex) {
var h = String(hex).trim().replace("#", "");
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
var n = Number.parseInt(h, 16);
if (!Number.isFinite(n)) return [31, 29, 27];
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
// Accent is the only colour in the piece and it carries exactly one
// meaning: this bar currently leads. It is binary, not blended — a
// half-accent bar would assert "half leading", which is not a fact.
// It hands over at the instant the two values cross, which is also the
// instant the two bars are the same length, so the handover lands on
// the overtake instead of drifting near it. Flat fill, never a
// gradient: a gradient inside a bar makes the longest bar the palest
// exactly where the eye is comparing lengths.
function hexToCss(hex) {
var rgb = hexToRgb(hex);
return "rgb(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ")";
}
var BAR_FILL = hexToCss(BAR_COLOR);
var ACCENT_FILL = hexToCss(ACCENT);
function niceStep(x) {
var e = Math.pow(10, Math.floor(Math.log10(x)));
var f = x / e;
return e * (f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10);
}
// ---- geometry ----------------------------------------------------
var TRACK_X = 400;
var TRACK_W = 1180;
var PLOT_H = 760;
var PITCH = PLOT_H / BAR_COUNT;
var BAR_H = Math.max(12, PITCH * 0.62);
var TICK_POOL = 12;
// ---- DOM ---------------------------------------------------------
var plot = document.getElementById("bcr-plot");
var axis = document.getElementById("bcr-axis");
var periodEl = document.getElementById("bcr-period");
document.getElementById("bcr-title").textContent = str(
V.title,
"Streaming Subscribers by Service",
);
document.getElementById("bcr-subtitle").textContent = str(
V.subtitle,
"Ranked by reported subscribers",
);
var rows = SERIES.map(function (s) {
var row = document.createElement("div");
row.className = "bcr-row";
row.style.height = PITCH + "px";
// Declared, not silenced: two rows mid-overtake genuinely occupy the
// same band for ~200ms. Readability is handled by the opaque chip on
// each label, so the front one is always legible.
var name = document.createElement("div");
name.className = "bcr-name";
name.setAttribute("data-layout-allow-overlap", "");
name.textContent = s.label;
var bar = document.createElement("div");
bar.className = "bcr-bar";
bar.style.height = BAR_H + "px";
var value = document.createElement("div");
value.className = "bcr-value";
value.setAttribute("data-layout-allow-overlap", "");
row.appendChild(name);
row.appendChild(bar);
row.appendChild(value);
plot.appendChild(row);
return { row: row, bar: bar, value: value };
});
var ticks = [];
for (var k = 0; k < TICK_POOL; k++) {
var line = document.createElement("div");
line.className = "bcr-tick-line" + (k === 0 ? " bcr-tick-zero" : "");
var label = document.createElement("div");
label.className = "bcr-tick-label";
axis.appendChild(line);
axis.appendChild(label);
ticks.push({ line: line, label: label });
}
// ---- render ------------------------------------------------------
function render(t) {
if (N === 0) return;
var values = SERIES.map(function (s) {
return valueAt(t, s);
});
// Paint order follows the CURRENT value, not the row position: the
// bar that has just edged ahead comes forward, so at the instant two
// bars are superimposed the frame shows the one that is overtaking
// (its bar and its label together) rather than the one it passed.
var byValue = values
.map(function (v, i) {
return i;
})
.sort(function (a, b) {
return values[b] - values[a] || a - b;
});
var zRank = new Array(values.length);
for (var q = 0; q < byValue.length; q++) zRank[byValue[q]] = q;
var leader = byValue[0];
var maxValue = values[leader];
// Headroom keeps the leader off the frame edge; the domain is a
// continuous function of t, so the axis glides instead of stepping.
var scaleMax = Math.max(maxValue * 1.06, 1e-6);
var step = niceStep(scaleMax / 4);
for (var i = 0; i < TICK_POOL; i++) {
var tv = i * step;
var visible = tv <= scaleMax;
var tx = TRACK_X + (tv / scaleMax) * TRACK_W;
ticks[i].line.style.transform = "translateX(" + tx.toFixed(2) + "px)";
ticks[i].line.style.opacity = visible ? "1" : "0";
ticks[i].label.style.transform =
"translateX(" + tx.toFixed(2) + "px) translateX(-50%)";
ticks[i].label.style.opacity = visible ? "1" : "0";
ticks[i].label.textContent = visible ? formatValue(tv) : "";
}
for (var j = 0; j < rows.length; j++) {
var rankPos = rankPosAt(t, j);
var barW = (values[j] / scaleMax) * TRACK_W;
var el = rows[j];
el.row.style.transform = "translateY(" + (rankPos * PITCH).toFixed(2) + "px)";
// Fades out only as it slides past the last visible slot.
el.row.style.opacity = clamp(BAR_COUNT - rankPos, 0, 1).toFixed(3);
el.row.style.zIndex = String(1000 - zRank[j]);
el.bar.style.width = barW.toFixed(2) + "px";
el.bar.style.backgroundColor = j === leader ? ACCENT_FILL : BAR_FILL;
el.value.style.transform = "translate(" + barW.toFixed(2) + "px, -50%)";
el.value.textContent = formatValue(values[j]);
}
var pIndex = clamp(Math.floor(t / PERIOD_DURATION), 0, T - 1);
periodEl.textContent = PERIODS[pIndex];
}
// ---- timeline ----------------------------------------------------
// A property SETTER drives the per-frame work: tl.eventCallback("onUpdate")
// does not fire on tl.seek(), a tweened accessor does.
var driver = { _t: 0 };
Object.defineProperty(driver, "t", {
get: function () {
return this._t;
},
set: function (value) {
this._t = value;
render(value);
},
});
var tl = gsap.timeline({ paused: true });
if (RACE_SECONDS > 0) {
tl.to(driver, { t: RACE_SECONDS, duration: RACE_SECONDS, ease: "none" }, 0);
}
render(0);
window.__timelines = window.__timelines || {};
window.__timelines["bar-chart-race"] = tl;
})();
</script>
</div>
</body>
</html>
```
</VariablesExplorer>
## Install
<InstallCommand command="npx hyperframes add bar-chart-race" />
That writes one file: `compositions/bar-chart-race.html`.
## Add it to your video
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="bar-chart-race"
data-composition-src="compositions/bar-chart-race.html"
data-start="0"
data-duration="12"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Change how it looks
Set these CSS variables on the block:
- `title` — Title. Defaults to `Streaming Subscribers by Service`.
- `subtitle` — Subtitle. Defaults to `Ranked by reported subscribers`.
- `periods` — Period labels (comma separated). Defaults to `2019, 2020, 2021, 2022, 2023, 2024`.
- `series` — Series (one per line: Name: v1, v2, ...). Defaults to `Northwind: 42, 58, 71, 96, 118, 131
Cobalt: 30, 46, 68, 92, 126, 168
Ferry: 55, 62, 66, 70, 74, 79
Marlow: 18, 33, 52, 61, 88, 104
Aster: 25, 28, 44, 58, 63, 72
Pell: 12, 20, 39, 47, 55, 90
Quill: 8, 11, 15, 24, 40, 66
Dunmore: 35, 37, 38, 40, 42, 44`.
- `barCount` — Bars shown. Defaults to `6`.
- `periodDuration` — Seconds per period. Defaults to `2`.
- `valuePrefix` — Value prefix. Defaults to `$`.
- `valueSuffix` — Value suffix. Defaults to `M`.
- `valueDecimals` — Value decimals. Defaults to `0`.
- `accent` — Leader accent. Defaults to `#c8452d`.
{/* hf:generated-footer */}
Tagged `data` `chart` `statistics` `ranking`.
## Related topics
- [Browse the complete Catalog](/catalog)
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
- [Build a richer composition](/go-further)
+4 -892
View File
@@ -3,13 +3,13 @@ title: "Beat Freeze Cut"
description: "A beat-driven speed ramp, freeze-frame hit, and hard-cut sequence for music-led promos and montages."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/beat-freeze-cut.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Beat Freeze Cut preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/beat-freeze-cut.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/beat-freeze-cut.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add beat-freeze-cut" />
```bash Terminal
npx hyperframes add beat-freeze-cut
```
That writes one file: `compositions/beat-freeze-cut.html`.
@@ -39,894 +39,6 @@ Set these CSS variables on the block:
- `--bg-color` — Background. Defaults to `#0B0D10`.
- `--accent-color` — Accent. Defaults to `#00E5C7`.
## Source
<Accordion title={`beat-freeze-cut.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Beat Freeze Cut</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600;700;800;900&display=block"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: #0b0d10;
}
/*
EDIT / STUDIO PARAMS
────────────────────
Studio color params (CSS custom properties on the root):
--bg-color default #0B0D10
--accent-color default #00E5C7
Numeric / string params (beat times, labels, intensity) are NOT in the
formal registry params schema today (only color keys are used by
existing blocks). Edit the CONFIG object in the script below instead.
*/
#bfc-root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
--bg-color: #0b0d10;
--accent-color: #00e5c7;
--bfc-ink: #f4f6f8;
--bfc-muted: #a0a8b0;
--bfc-panel: #14181f;
--bfc-panel-2: #1a2029;
--bfc-line: rgba(255, 255, 255, 0.08);
font-family:
"Inter",
system-ui,
-apple-system,
sans-serif;
color: var(--bfc-ink);
}
#bfc-bg {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 80% 60% at 50% 40%, rgba(0, 229, 199, 0.07) 0%, transparent 55%),
radial-gradient(
ellipse 50% 40% at 80% 80%,
rgba(255, 255, 255, 0.03) 0%,
transparent 50%
),
var(--bg-color);
}
#bfc-grid {
position: absolute;
inset: 0;
opacity: 0.22;
background-image:
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
background-size: 80px 80px;
mask-image: radial-gradient(ellipse 70% 60% at 50% 45%, #000 20%, transparent 75%);
pointer-events: none;
}
#bfc-stage {
position: absolute;
inset: 0;
transform-origin: 50% 48%;
will-change: transform, filter;
}
#bfc-shot-a,
#bfc-shot-b {
position: absolute;
inset: 0;
overflow: hidden;
}
#bfc-shot-b {
visibility: hidden;
opacity: 0;
}
/* ── Shot A: single product card hero ── */
#bfc-a-crop {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
transform-origin: 50% 50%;
}
#bfc-a-card {
position: relative;
width: 680px;
height: 720px;
border-radius: 36px;
background:
linear-gradient(160deg, rgba(255, 255, 255, 0.06) 0%, transparent 40%),
linear-gradient(180deg, var(--bfc-panel-2) 0%, var(--bfc-panel) 100%);
border: 1px solid var(--bfc-line);
box-shadow:
0 40px 120px rgba(0, 0, 0, 0.55),
0 0 0 1px rgba(0, 229, 199, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
overflow: hidden;
transform-origin: 50% 50%;
}
#bfc-a-card-glow {
position: absolute;
left: 50%;
top: 28%;
width: 420px;
height: 420px;
margin-left: -210px;
margin-top: -210px;
border-radius: 50%;
background: radial-gradient(circle, rgba(0, 229, 199, 0.28) 0%, transparent 68%);
opacity: 0.85;
pointer-events: none;
}
#bfc-a-wave {
position: absolute;
left: 64px;
right: 64px;
top: 120px;
height: 220px;
}
#bfc-a-wave svg {
width: 100%;
height: 100%;
display: block;
}
#bfc-a-wave-path {
fill: none;
stroke: var(--accent-color);
stroke-width: 4;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0.95;
}
#bfc-a-wave-fill {
fill: var(--accent-color);
opacity: 0.12;
}
#bfc-a-bars {
position: absolute;
left: 80px;
right: 80px;
bottom: 160px;
height: 96px;
display: flex;
align-items: flex-end;
gap: 10px;
}
.bfc-bar {
flex: 1;
border-radius: 6px 6px 2px 2px;
background: linear-gradient(180deg, var(--accent-color) 0%, rgba(0, 229, 199, 0.25) 100%);
transform-origin: 50% 100%;
height: 40%;
}
#bfc-a-meta {
position: absolute;
left: 64px;
right: 64px;
bottom: 56px;
display: flex;
align-items: center;
justify-content: space-between;
}
#bfc-a-kicker {
font-size: 18px;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--bfc-muted);
}
#bfc-a-pill {
font-size: 15px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #04120f;
background: var(--accent-color);
padding: 8px 16px;
border-radius: 999px;
}
/* ── Shot B: split layout, higher info density ── */
#bfc-b-layout {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 48px;
padding: 100px 110px;
align-items: center;
}
#bfc-b-left {
display: flex;
flex-direction: column;
gap: 28px;
transform-origin: 0% 50%;
}
#bfc-b-eyebrow {
font-size: 20px;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--accent-color);
}
#bfc-b-title {
font-size: 128px;
font-weight: 900;
line-height: 0.92;
letter-spacing: -0.04em;
text-transform: uppercase;
color: var(--bfc-ink);
}
#bfc-b-sub {
font-size: 28px;
font-weight: 500;
color: var(--bfc-muted);
max-width: 520px;
line-height: 1.35;
}
#bfc-b-right {
display: flex;
flex-direction: column;
gap: 18px;
}
.bfc-b-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 28px 32px;
border-radius: 20px;
background: linear-gradient(135deg, var(--bfc-panel-2), var(--bfc-panel));
border: 1px solid var(--bfc-line);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.35);
}
.bfc-b-card-label {
font-size: 18px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--bfc-muted);
}
.bfc-b-card-value {
font-size: 42px;
font-weight: 800;
letter-spacing: -0.03em;
color: var(--bfc-ink);
}
.bfc-b-card-value.accent {
color: var(--accent-color);
}
#bfc-b-accent-bar {
width: 120px;
height: 6px;
border-radius: 999px;
background: var(--accent-color);
opacity: 0.9;
}
/* ── Intro / transient typography ── */
#bfc-intro {
position: absolute;
left: 0;
right: 0;
top: 72px;
display: flex;
justify-content: center;
pointer-events: none;
z-index: 20;
opacity: 0;
visibility: hidden;
}
#bfc-intro-label {
font-size: 18px;
font-weight: 700;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--bfc-muted);
padding: 10px 18px;
border: 1px solid var(--bfc-line);
border-radius: 999px;
background: rgba(11, 13, 16, 0.55);
backdrop-filter: blur(8px);
}
#bfc-hit-type {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 30;
opacity: 0;
visibility: hidden;
}
#bfc-hit-type-text {
font-size: 160px;
font-weight: 900;
letter-spacing: -0.04em;
text-transform: uppercase;
color: var(--bfc-ink);
text-shadow:
0 0 40px rgba(0, 229, 199, 0.35),
0 8px 40px rgba(0, 0, 0, 0.55);
-webkit-text-stroke: 2px rgba(0, 0, 0, 0.35);
paint-order: stroke fill;
}
/* ── Freeze frame treatment ── */
#bfc-freeze {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 25;
opacity: 0;
visibility: hidden;
}
#bfc-freeze-outline {
position: absolute;
inset: 48px;
border: 3px solid var(--accent-color);
border-radius: 8px;
box-shadow:
0 0 0 1px rgba(0, 229, 199, 0.25),
inset 0 0 0 1px rgba(0, 229, 199, 0.15),
0 0 48px rgba(0, 229, 199, 0.2);
opacity: 0;
}
#bfc-freeze-flash {
position: absolute;
inset: 0;
background: #ffffff;
opacity: 0;
mix-blend-mode: screen;
}
#bfc-freeze-contour {
position: absolute;
inset: 0;
background:
linear-gradient(90deg, transparent 0%, rgba(0, 229, 199, 0.08) 48%, transparent 52%),
radial-gradient(ellipse 40% 55% at 50% 48%, transparent 40%, rgba(0, 229, 199, 0.18) 100%);
opacity: 0;
mix-blend-mode: screen;
}
#bfc-freeze-badge {
position: absolute;
top: 80px;
right: 96px;
font-size: 16px;
font-weight: 800;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #04120f;
background: var(--accent-color);
padding: 10px 18px;
border-radius: 6px;
opacity: 0;
}
/* ── Hard-cut connector (directional stretch / blur) ── */
#bfc-cut {
position: absolute;
inset: -8% -20%;
pointer-events: none;
z-index: 40;
opacity: 0;
visibility: hidden;
background: linear-gradient(
90deg,
transparent 0%,
rgba(0, 229, 199, 0.12) 35%,
rgba(255, 255, 255, 0.55) 50%,
rgba(0, 229, 199, 0.12) 65%,
transparent 100%
);
filter: blur(18px);
transform-origin: 50% 50%;
}
#bfc-vignette {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 15;
background: radial-gradient(
ellipse 75% 70% at 50% 50%,
transparent 45%,
rgba(0, 0, 0, 0.55) 100%
);
opacity: 0.85;
}
#bfc-driver {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
</style>
</head>
<body>
<div
id="bfc-root"
data-composition-id="beat-freeze-cut"
data-start="0"
data-duration="6"
data-width="1920"
data-height="1080"
>
<div id="bfc-bg" aria-hidden="true"></div>
<div id="bfc-grid" aria-hidden="true"></div>
<div id="bfc-stage">
<!-- Shot A -->
<div id="bfc-shot-a">
<div id="bfc-a-crop" data-layout-allow-overflow>
<div id="bfc-a-card" data-layout-allow-overflow>
<div id="bfc-a-card-glow" aria-hidden="true"></div>
<div id="bfc-a-wave" aria-hidden="true">
<svg viewBox="0 0 592 220" preserveAspectRatio="none">
<path
id="bfc-a-wave-fill"
d="M0,160 C40,150 60,90 100,100 C140,110 160,40 200,55 C240,70 260,150 300,140 C340,130 360,30 400,45 C440,60 460,130 500,120 C540,110 560,70 592,80 L592,220 L0,220 Z"
/>
<path
id="bfc-a-wave-path"
d="M0,160 C40,150 60,90 100,100 C140,110 160,40 200,55 C240,70 260,150 300,140 C340,130 360,30 400,45 C440,60 460,130 500,120 C540,110 560,70 592,80"
/>
</svg>
</div>
<div id="bfc-a-bars" aria-hidden="true">
<div class="bfc-bar" id="bfc-bar-0" style="height: 34%"></div>
<div class="bfc-bar" id="bfc-bar-1" style="height: 58%"></div>
<div class="bfc-bar" id="bfc-bar-2" style="height: 42%"></div>
<div class="bfc-bar" id="bfc-bar-3" style="height: 78%"></div>
<div class="bfc-bar" id="bfc-bar-4" style="height: 52%"></div>
<div class="bfc-bar" id="bfc-bar-5" style="height: 88%"></div>
<div class="bfc-bar" id="bfc-bar-6" style="height: 46%"></div>
<div class="bfc-bar" id="bfc-bar-7" style="height: 64%"></div>
<div class="bfc-bar" id="bfc-bar-8" style="height: 36%"></div>
<div class="bfc-bar" id="bfc-bar-9" style="height: 72%"></div>
<div class="bfc-bar" id="bfc-bar-10" style="height: 48%"></div>
<div class="bfc-bar" id="bfc-bar-11" style="height: 92%"></div>
</div>
<div id="bfc-a-meta">
<span id="bfc-a-kicker">MUSIC PROMO</span>
<span id="bfc-a-pill">LIVE</span>
</div>
</div>
</div>
</div>
<!-- Shot B -->
<div id="bfc-shot-b">
<div id="bfc-b-layout">
<div id="bfc-b-left">
<div id="bfc-b-eyebrow">Next shot</div>
<div id="bfc-b-title">HARD<br />CUT</div>
<div id="bfc-b-accent-bar" aria-hidden="true"></div>
<div id="bfc-b-sub">
Beat-locked freeze, then cut. Built for music-led promos and montages.
</div>
</div>
<div id="bfc-b-right">
<div class="bfc-b-card" id="bfc-b-card-0">
<span class="bfc-b-card-label">Ramp</span>
<span class="bfc-b-card-value accent">1.35→2.2</span>
</div>
<div class="bfc-b-card" id="bfc-b-card-1">
<span class="bfc-b-card-label">Freeze</span>
<span class="bfc-b-card-value">0.8s</span>
</div>
<div class="bfc-b-card" id="bfc-b-card-2">
<span class="bfc-b-card-label">Cut</span>
<span class="bfc-b-card-value accent">3.00</span>
</div>
</div>
</div>
</div>
</div>
<div id="bfc-vignette" aria-hidden="true"></div>
<div id="bfc-intro" aria-hidden="true">
<span id="bfc-intro-label">ON THE BEAT</span>
</div>
<div id="bfc-hit-type" aria-hidden="true">
<span id="bfc-hit-type-text">DROP</span>
</div>
<div id="bfc-freeze" aria-hidden="true">
<div id="bfc-freeze-flash"></div>
<div id="bfc-freeze-contour"></div>
<div id="bfc-freeze-outline"></div>
<div id="bfc-freeze-badge">FREEZE</div>
</div>
<div id="bfc-cut" aria-hidden="true"></div>
<div id="bfc-driver" class="clip" data-start="0" data-duration="6" data-track-index="0"></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
// ── EDIT ME (not Studio params — see HTML comment) ──────────────
var CONFIG = {
primaryLabel: "DROP",
secondaryLabel: "ON THE BEAT",
beat1: 0.7,
beat2: 2.2,
freezeTime: 2.2,
intensity: 1.0,
};
// ────────────────────────────────────────────────────────────────
var DURATION = 6;
var intensity = Math.max(0.6, Math.min(1.4, Number(CONFIG.intensity) || 1));
var beat1 = Number(CONFIG.beat1) || 0.7;
var beat2 = Number(CONFIG.beat2) || 2.2;
var freezeAt = Number(CONFIG.freezeTime) || beat2;
var cutAt = 3.0;
var beat3 = 4.7;
var holdAt = 5.2;
var root = document.getElementById("bfc-root");
var stage = document.getElementById("bfc-stage");
var shotA = document.getElementById("bfc-shot-a");
var shotB = document.getElementById("bfc-shot-b");
var aCrop = document.getElementById("bfc-a-crop");
var aCard = document.getElementById("bfc-a-card");
var intro = document.getElementById("bfc-intro");
var introLabel = document.getElementById("bfc-intro-label");
var hitType = document.getElementById("bfc-hit-type");
var hitText = document.getElementById("bfc-hit-type-text");
var freeze = document.getElementById("bfc-freeze");
var freezeOutline = document.getElementById("bfc-freeze-outline");
var freezeFlash = document.getElementById("bfc-freeze-flash");
var freezeContour = document.getElementById("bfc-freeze-contour");
var freezeBadge = document.getElementById("bfc-freeze-badge");
var cutFx = document.getElementById("bfc-cut");
var bLeft = document.getElementById("bfc-b-left");
var bCards = [
document.getElementById("bfc-b-card-0"),
document.getElementById("bfc-b-card-1"),
document.getElementById("bfc-b-card-2"),
];
var bars = [];
for (var bi = 0; bi < 12; bi++) {
bars.push(document.getElementById("bfc-bar-" + bi));
}
// Apply editable labels
if (hitText) hitText.textContent = CONFIG.primaryLabel || "DROP";
if (introLabel) introLabel.textContent = CONFIG.secondaryLabel || "ON THE BEAT";
// Accent color from CSS var → freeze/glow that use hardcoded rgba fallbacks
// stay coherent because stroke/fill already use var(--accent-color).
var tl = gsap.timeline({ paused: true });
// Initial states (no CSS transform conflicts on animated props)
gsap.set(stage, { scale: 1, x: 0, y: 0, filter: "blur(0px) brightness(1)" });
gsap.set(aCrop, { scale: 1, x: 0, y: 0 });
// Card visible at t=0 so catalog snapshots are never a black frame
gsap.set(aCard, { scale: 0.9, y: 20, opacity: 1 });
gsap.set(shotA, { opacity: 1, visibility: "visible" });
gsap.set(shotB, { opacity: 0, visibility: "hidden" });
gsap.set(intro, { opacity: 0, visibility: "hidden", y: -12 });
gsap.set(hitType, { opacity: 0, visibility: "hidden", scale: 1.15 });
gsap.set(freeze, { opacity: 0, visibility: "hidden" });
gsap.set([freezeOutline, freezeFlash, freezeContour, freezeBadge], { opacity: 0 });
gsap.set(cutFx, { opacity: 0, visibility: "hidden", scaleX: 0.4, x: -200 });
gsap.set(bLeft, { opacity: 0, x: -48 });
gsap.set(bCards, { opacity: 0, x: 40 });
gsap.set(bars, { scaleY: 0.55 });
// Full duration anchor (seek-safe length)
tl.to({}, { duration: DURATION, ease: "none" }, 0);
// ── 0.000.70: intro push-in + secondary type ──
tl.to(aCard, { scale: 1, y: 0, duration: 0.55, ease: "power3.out" }, 0);
tl.to(stage, { scale: 1 + 0.04 * intensity, duration: 0.7, ease: "power1.out" }, 0);
tl.to(bars, { scaleY: 1, duration: 0.5, ease: "power2.out", stagger: 0.02 }, 0.12);
tl.set(intro, { visibility: "visible" }, 0.12);
tl.to(intro, { opacity: 1, y: 0, duration: 0.28, ease: "power2.out" }, 0.12);
tl.to(intro, { opacity: 0, y: -8, duration: 0.18, ease: "power2.in" }, 0.52);
tl.set(intro, { visibility: "hidden", opacity: 0 }, 0.7);
// ── 0.701.35: beat 1 — hard reframe ──
tl.to(
aCrop,
{
scale: 1 + 0.18 * intensity,
x: -80 * intensity,
y: 24 * intensity,
duration: 0.22,
ease: "power4.out",
},
beat1,
);
tl.to(
stage,
{
scale: 1.08 * intensity > 1.2 ? 1.2 : 1 + 0.08 * intensity,
duration: 0.28,
ease: "expo.out",
},
beat1,
);
tl.to(
bars,
{
scaleY: function (i) {
// Deterministic bar pattern (no Math.random)
var pattern = [0.55, 1.15, 0.7, 1.35, 0.9, 1.4, 0.65, 1.1, 0.5, 1.25, 0.8, 1.45];
return pattern[i % pattern.length];
},
duration: 0.18,
ease: "power3.out",
stagger: 0.01,
},
beat1,
);
// ── 1.352.20: speed ramp into freeze ──
var rampStart = 1.35;
var rampDur = Math.max(0.2, freezeAt - rampStart);
tl.to(
aCrop,
{
scale: 1.28 * (0.9 + 0.1 * intensity),
x: 28 * intensity,
y: -12 * intensity,
duration: rampDur,
ease: "power3.in",
},
rampStart,
);
tl.to(
stage,
{
scale: 1.12,
filter: "blur(0px) brightness(1.08)",
duration: rampDur,
ease: "power2.in",
},
rampStart,
);
tl.to(
bars,
{
scaleY: 1.55,
duration: rampDur * 0.85,
ease: "power2.in",
stagger: { each: 0.012, from: "center" },
},
rampStart,
);
// ── 2.203.00: freeze hit (snap-on for seek/snapshot stability) ──
tl.set(freeze, { visibility: "visible", opacity: 1 }, freezeAt);
tl.set(freezeOutline, { opacity: 1 }, freezeAt);
tl.set(freezeContour, { opacity: 0.85 }, freezeAt);
tl.set(freezeBadge, { opacity: 1 }, freezeAt);
tl.set(hitType, { visibility: "visible", opacity: 1, scale: 1 }, freezeAt);
// Hold crop/stage where ramp ended
tl.to(aCrop, { duration: 0.01, ease: "none" }, freezeAt);
tl.to(
stage,
{
scale: 1.12,
filter: "blur(0px) brightness(1.15)",
duration: 0.05,
ease: "power4.out",
},
freezeAt,
);
// Single exposure flash (not a strobe sequence)
tl.fromTo(
freezeFlash,
{ opacity: 0.55 * intensity },
{ opacity: 0, duration: 0.28, ease: "power2.out" },
freezeAt,
);
tl.to(
hitType,
{ opacity: 0, scale: 0.96, duration: 0.2, ease: "power2.in" },
freezeAt + 0.55,
);
tl.set(hitType, { visibility: "hidden", opacity: 0 }, freezeAt + 0.78);
// Soft settle of freeze dressing before cut
tl.to(
[freezeOutline, freezeContour, freezeBadge],
{ opacity: 0.75, duration: 0.35, ease: "sine.out" },
freezeAt + 0.35,
);
// ── 3.003.35: hard cut connector ──
tl.set(cutFx, { visibility: "visible" }, cutAt);
tl.fromTo(
cutFx,
{ opacity: 0, scaleX: 0.35, x: -280 },
{
opacity: 0.95,
scaleX: 1.6,
x: 120,
duration: 0.12,
ease: "power4.in",
},
cutAt,
);
tl.to(
stage,
{
filter: "blur(14px) brightness(1.35)",
scaleX: 1.35,
scaleY: 0.92,
duration: 0.12,
ease: "power3.in",
},
cutAt,
);
// Switch shots mid-cut — shot B content lands immediately (no black gap)
var switchAt = cutAt + 0.14;
tl.set(shotA, { opacity: 0, visibility: "hidden" }, switchAt);
tl.set(shotB, { opacity: 1, visibility: "visible" }, switchAt);
tl.set(hitType, { opacity: 0, visibility: "hidden" }, switchAt);
tl.set(
[freeze, freezeOutline, freezeFlash, freezeContour, freezeBadge],
{ opacity: 0, visibility: "hidden" },
switchAt,
);
tl.set(freeze, { visibility: "hidden" }, switchAt);
// Shot B content visible on switch (offset settles after)
tl.set(bLeft, { opacity: 1, x: -24 }, switchAt);
tl.set(bCards, { opacity: 1, x: 24 }, switchAt);
tl.to(
stage,
{
filter: "blur(0px) brightness(1)",
scale: 1,
scaleX: 1,
scaleY: 1,
x: 0,
y: 0,
duration: 0.18,
ease: "power3.out",
},
switchAt,
);
tl.to(cutFx, { opacity: 0, x: 360, duration: 0.16, ease: "power2.out" }, cutAt + 0.16);
tl.set(cutFx, { visibility: "hidden", opacity: 0 }, cutAt + 0.34);
// ── after switch: shot B settle (no scale overlap with recover) ──
tl.to(bLeft, { x: 0, duration: 0.4, ease: "power3.out" }, switchAt);
tl.to(
bCards,
{
x: 0,
duration: 0.38,
ease: "power3.out",
stagger: 0.06,
},
switchAt + 0.04,
);
tl.to(stage, { scale: 1.02, duration: 1.17, ease: "sine.out" }, switchAt + 0.18);
// ── 4.705.20: final beat punch-in ──
tl.to(
stage,
{
scale: 1.08 * (0.9 + 0.1 * intensity),
duration: 0.14,
ease: "power4.out",
},
beat3,
);
tl.to(
bCards[1],
{
scale: 1.04,
duration: 0.12,
ease: "power3.out",
},
beat3,
);
tl.to(
bCards[1],
{
scale: 1,
duration: 0.22,
ease: "power2.out",
},
beat3 + 0.14,
);
// Micro freeze feel: brief hold of scale
tl.to(
stage,
{ scale: 1.08 * (0.9 + 0.1 * intensity), duration: 0.2, ease: "none" },
beat3 + 0.14,
);
// ── 5.206.00: hold readable end card (≥0.8s) ──
tl.to(stage, { scale: 1.03, duration: 0.35, ease: "power2.out" }, holdAt);
// Ensure transient layers stay dead through end
tl.set(intro, { opacity: 0, visibility: "hidden" }, holdAt);
tl.set(hitType, { opacity: 0, visibility: "hidden" }, holdAt);
tl.set(freeze, { opacity: 0, visibility: "hidden" }, holdAt);
tl.set(cutFx, { opacity: 0, visibility: "hidden" }, holdAt);
window.__timelines["beat-freeze-cut"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `transition` `music` `beat-driven` `freeze-frame`.
File diff suppressed because it is too large Load Diff
+4 -176
View File
@@ -3,13 +3,13 @@ title: "Camcorder HUD"
description: "Responsive REC, battery, editable date placeholder, and timeline-driven counter overlay for creator-camera and camcorder treatments"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/camcorder-hud.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Camcorder HUD preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/camcorder-hud.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/camcorder-hud.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add camcorder-hud" />
```bash Terminal
npx hyperframes add camcorder-hud
```
That writes one file: `compositions/camcorder-hud.html`.
@@ -32,178 +32,6 @@ It runs for 4 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`camcorder-hud.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: transparent;
}
#ch-hud {
--hf-camcorder-color: #fff;
--hf-camcorder-accent: #f12c2c;
--hf-camcorder-inset: 5vmin;
--hf-camcorder-font-size: 3.7vmin;
position: absolute;
inset: 0;
color: var(--hf-camcorder-color);
font-family: monospace;
font-size: var(--hf-camcorder-font-size);
font-weight: 700;
line-height: 1.25;
pointer-events: none;
-webkit-text-stroke: 0.12vmin rgb(0 0 0 / 92%);
text-shadow: 0 0.2vmin 0.6vmin rgb(0 0 0 / 90%);
}
#ch-hud .ch-corner {
position: absolute;
}
#ch-hud .ch-record {
top: var(--hf-camcorder-inset);
left: var(--hf-camcorder-inset);
display: flex;
align-items: center;
gap: 1.8vmin;
}
#ch-hud .ch-record-dot {
width: 2.2vmin;
height: 2.2vmin;
border-radius: 50%;
background: var(--hf-camcorder-accent);
box-shadow: 0 0 0.9vmin color-mix(in srgb, var(--hf-camcorder-accent) 55%, transparent);
}
#ch-hud .ch-battery {
top: var(--hf-camcorder-inset);
right: var(--hf-camcorder-inset);
width: 7vmin;
height: 3.5vmin;
border: 0.35vmin solid currentcolor;
}
#ch-hud .ch-battery::before {
position: absolute;
top: 0.7vmin;
left: 0.7vmin;
width: 4.7vmin;
height: 1.4vmin;
background: currentcolor;
content: "";
}
#ch-hud .ch-battery::after {
position: absolute;
top: 0.7vmin;
right: -1vmin;
width: 0.7vmin;
height: 1.4vmin;
background: currentcolor;
content: "";
}
#ch-hud .ch-date {
bottom: var(--hf-camcorder-inset);
left: var(--hf-camcorder-inset);
}
#ch-hud .ch-counter {
right: var(--hf-camcorder-inset);
bottom: var(--hf-camcorder-inset);
text-align: right;
}
#ch-hud .ch-mode {
display: block;
margin-bottom: 0.7vmin;
font-size: 2.6vmin;
}
</style>
</head>
<body>
<div
id="camcorder-hud"
data-composition-id="camcorder-hud"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<div id="ch-hud" data-hf-counter-start="0" aria-hidden="true">
<div class="ch-corner ch-record"><span class="ch-record-dot"></span><span>REC</span></div>
<div class="ch-corner ch-battery"></div>
<div class="ch-corner ch-date">JAN 01 2000<br />12:00 AM</div>
<div class="ch-corner ch-counter">
<span class="ch-mode">SP · 16:9</span><span class="ch-counter-value">00:00:00</span>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var hud = document.getElementById("ch-hud");
var counter = hud && hud.querySelector(".ch-counter-value");
var recordDot = hud && hud.querySelector(".ch-record-dot");
var host = hud && hud.closest("[data-composition-id]");
if (!hud || !counter || !recordDot || !host) {
throw new Error("Camcorder HUD could not find its composition host");
}
var compositionId = host.getAttribute("data-composition-id");
var duration = Number(host.getAttribute("data-duration")) || 4;
var counterStart = Number(hud.getAttribute("data-hf-counter-start")) || 0;
var state = { seconds: 0 };
function formatCounter(value) {
var seconds = Math.max(0, Math.floor(value));
var hours = Math.floor(seconds / 3600);
var minutes = Math.floor((seconds % 3600) / 60);
var remainder = seconds % 60;
return [hours, minutes, remainder]
.map(function (part) {
return String(part).padStart(2, "0");
})
.join(":");
}
function updateHud() {
counter.textContent = formatCounter(counterStart + state.seconds);
recordDot.style.opacity = Math.floor(state.seconds / 0.55) % 2 === 0 ? "1" : "0.25";
}
var tl = gsap.timeline({ paused: true });
tl.to(state, { seconds: duration, duration: duration, ease: "none", onUpdate: updateHud }, 0);
updateHud();
window.__timelines[compositionId] = tl;
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `camcorder` `camera` `recording` `hud` `overlay` `creator` `media-treatment-overlay`.
-408
View File
@@ -1,408 +0,0 @@
---
title: "Dolly Zoom (Vertigo Shot)"
description: "Camera tracks toward or away from the subject while the field of view changes the opposite way, so the subject holds its frame size while the background collapses behind it or rushes in. Focal length is solved from dolly distance every frame against d * tan(FOV/2) = const, not keyframed beside it. Requires depth: the subject sits on the z = 0 plane and the content behind it must be layered or genuinely 3D (translateZ at two or more distinct depths) -- a flat element gains nothing from this move."
---
import { InstallCommand } from "/snippets/install-command.jsx";
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
<VariablesExplorer
previewSrc="/public/catalog/preview/blocks/camera-dolly-zoom.html"
compositionId="camera-dolly-zoom"
compositionSrc="compositions/camera-dolly-zoom.html"
variables={[{"id":"direction","type":"enum","label":"Dolly direction","default":"out","options":[{"value":"out","label":"Dolly out + zoom in (background rushes in)"},{"value":"in","label":"Dolly in + zoom out (background falls away)"}]},{"id":"strength","type":"number","label":"Move strength (end / start distance ratio)","default":2,"min":1.1,"max":4,"step":0.1},{"id":"subjectDistance","type":"number","label":"Camera-to-subject distance at the start of the move","default":1400,"min":600,"max":3000,"step":50,"unit":"px"},{"id":"easing","type":"enum","label":"Dolly easing","default":"power2.inOut","options":[{"value":"none","label":"Linear"},{"value":"sine.inOut","label":"Sine in-out"},{"value":"power1.inOut","label":"Power1 in-out"},{"value":"power2.inOut","label":"Power2 in-out"},{"value":"power3.inOut","label":"Power3 in-out"}]}]}
>
```html camera-dolly-zoom.html
<!doctype html>
<html
lang="en"
data-composition-variables='[
{
"id": "direction",
"type": "enum",
"label": "Dolly direction",
"default": "out",
"options": [
{ "value": "out", "label": "Dolly out + zoom in (background rushes in)" },
{ "value": "in", "label": "Dolly in + zoom out (background falls away)" }
]
},
{
"id": "strength",
"type": "number",
"label": "Move strength (end / start distance ratio)",
"default": 2,
"min": 1.1,
"max": 4,
"step": 0.1
},
{
"id": "subjectDistance",
"type": "number",
"label": "Camera-to-subject distance at the start of the move",
"default": 1400,
"min": 600,
"max": 3000,
"step": 50,
"unit": "px"
},
{
"id": "easing",
"type": "enum",
"label": "Dolly easing",
"default": "power2.inOut",
"options": [
{ "value": "none", "label": "Linear" },
{ "value": "sine.inOut", "label": "Sine in-out" },
{ "value": "power1.inOut", "label": "Power1 in-out" },
{ "value": "power2.inOut", "label": "Power2 in-out" },
{ "value": "power3.inOut", "label": "Power3 in-out" }
]
}
]'
>
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
}
</style>
</head>
<body>
<!--
===================================================================
DOLLY ZOOM (Vertigo / Hitchcock shot)
===================================================================
The camera tracks toward or away from the subject while the field of
view changes the opposite way, so the SUBJECT HOLDS ITS FRAME SIZE
while the background collapses behind it or rushes in.
THE CONSTRAINT (this is the whole effect, and it is exact, not taste):
d * tan(FOV / 2) = constant
where d is the camera-to-subject distance. Get it wrong and the shot
reads as a clumsy zoom. So focal length is SOLVED from distance every
frame; the two are never keyframed side by side.
In CSS 3D, `perspective: P px` IS the focal length in pixels, and it
also places the camera exactly P px in front of the z = 0 plane, so
tan(FOV / 2) = (viewportWidth / 2) / P. Substituting collapses the
invariant to a single ratio:
P(t) / d(t) = SUBJECT_SCALE (constant)
Everything the rig animates falls out of that one line (see the script).
-------------------------------------------------------------------
WHAT THIS PRIMITIVE REQUIRES OF ITS CONTENT
-------------------------------------------------------------------
DEPTH. A flat element gains nothing from a dolly zoom -- with one
layer there is no background to collapse and the move is invisible.
Content dropped into #dz-rig MUST be layered or genuinely 3D:
* The SUBJECT sits on the z = 0 plane -- `translateZ(0)`. That is
the plane the solve holds; anything you want frame-locked goes
there and nowhere else.
* BACKGROUND layers get `translateZ(-N px)`, N > 0, at least two or
three distinct depths (this demo ships five arches plus a back
wall). More separation = stronger effect.
* Everything inside #dz-rig must keep `transform-style: preserve-3d`
on its ancestors and must NOT sit behind `overflow: hidden`, or
the browser flattens the scene and the depth disappears.
* Keep every layer at a depth where `d + N > 0` for the whole move,
i.e. no layer in front of the camera's closest approach.
-------------------------------------------------------------------
DETERMINISM
-------------------------------------------------------------------
State at frame N is computed from N alone. d(t) is a lerp of the
eased tween progress, P is solved from d, and both are written by a
property setter on the tweened driver object. Nothing accumulates,
nothing reads a clock, and no frame depends on the frame before it.
(The setter matters: `tl.eventCallback("onUpdate", ...)` does NOT
fire on `tl.seek()`, so anything driven that way freezes on frame 0.)
-->
<div
id="dz-root"
data-composition-id="camera-dolly-zoom"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<style>
#dz-root {
width: 1920px;
height: 1080px;
position: relative;
overflow: hidden;
background: #0b1020;
font-family: Inter, sans-serif;
}
/* The camera. `perspective` is the focal length and is solved from
the dolly distance every frame -- see the script. */
#dz-stage {
position: absolute;
inset: 0;
perspective-origin: 50% 50%;
}
/* The rig root. Carries the camera's z offset so the subject plane
always sits exactly `d` in front of the camera. */
#dz-rig {
position: absolute;
inset: 0;
transform-style: preserve-3d;
}
/* --- authored depth layers ------------------------------------ */
.dz-arch {
position: absolute;
left: -140px;
top: -80px;
width: 2200px;
height: 1240px;
border: 16px solid #465280;
border-radius: 8px;
transform-style: preserve-3d;
}
#dz-backwall {
position: absolute;
left: -3040px;
top: -1710px;
width: 8000px;
height: 4500px;
background: #10162c;
transform-style: preserve-3d;
}
/* Bright doorway on the back wall -- the layer whose on-screen size
changes while the subject's does not. */
#dz-portal {
position: absolute;
left: 3300px;
top: 1850px;
width: 1400px;
height: 1500px;
background: #cfe4ff;
}
/* --- the subject: z = 0, the plane the solve holds ------------- */
#dz-subject {
position: absolute;
left: 840px;
top: 300px;
width: 240px;
height: 620px;
transform-style: preserve-3d;
}
.dz-fig-head {
width: 120px;
height: 120px;
margin: 0 auto;
border-radius: 50%;
background: #f2b134;
}
.dz-fig-body {
width: 240px;
height: 470px;
margin-top: 30px;
border-radius: 120px 120px 10px 10px;
background: #f2b134;
}
/* --- flat overlay (outside the 3D rig) ------------------------- */
.dz-label {
position: absolute;
left: 64px;
bottom: 56px;
padding: 14px 22px;
border-radius: 8px;
background: #0b1020;
color: #e9eeff;
font-size: 26px;
letter-spacing: 0.14em;
text-transform: uppercase;
}
</style>
<div id="dz-stage" class="clip" data-start="0" data-duration="4" data-track-index="0">
<div id="dz-rig">
<div id="dz-backwall" data-layout-allow-overflow style="transform: translateZ(-3200px)">
<div id="dz-portal"></div>
</div>
<div
class="dz-arch"
data-layout-allow-overflow
style="transform: translateZ(-2360px); border-color: #384166"
></div>
<div
class="dz-arch"
data-layout-allow-overflow
style="transform: translateZ(-1720px); border-color: #465280"
></div>
<div
class="dz-arch"
data-layout-allow-overflow
style="transform: translateZ(-1120px); border-color: #57669c"
></div>
<div
class="dz-arch"
data-layout-allow-overflow
style="transform: translateZ(-560px); border-color: #6b7cb8"
></div>
<div
class="dz-arch"
data-layout-allow-overflow
style="transform: translateZ(0px); border-color: #8194d4"
></div>
<div id="dz-subject" style="transform: translateZ(0px)">
<div class="dz-fig-head"></div>
<div class="dz-fig-body"></div>
</div>
</div>
</div>
<div class="dz-label">Dolly zoom &middot; d &times; tan(fov/2) = const</div>
</div>
<script>
window.__timelines = window.__timelines || {};
(function () {
var DEFAULTS = {
direction: "out",
strength: 2,
subjectDistance: 1400,
easing: "power2.inOut",
};
var declared =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
var vars = Object.assign({}, DEFAULTS, declared);
function clamp(value, lo, hi, fallback) {
var n = Number(value);
if (!isFinite(n)) return fallback;
return Math.min(hi, Math.max(lo, n));
}
function oneOf(value, allowed, fallback) {
return allowed.indexOf(String(value)) >= 0 ? String(value) : fallback;
}
var direction = oneOf(vars.direction, ["in", "out"], DEFAULTS.direction);
var strength = clamp(vars.strength, 1.1, 4, DEFAULTS.strength);
var ease = oneOf(
vars.easing,
["none", "sine.inOut", "power1.inOut", "power2.inOut", "power3.inOut"],
DEFAULTS.easing,
);
// d0 = camera-to-subject distance at the start of the move (px).
// d1 = distance at the end. "out" pulls the camera back and zooms in;
// "in" pushes the camera forward and zooms out.
var d0 = clamp(vars.subjectDistance, 600, 3000, DEFAULTS.subjectDistance);
var d1 = direction === "in" ? d0 / strength : d0 * strength;
// ---- the solve --------------------------------------------------
// Subject size is fixed iff d * tan(FOV/2) = const. With CSS 3D,
// tan(FOV/2) = (width/2) / perspective, so that invariant is exactly
// perspective / distance = SUBJECT_SCALE. Focal length is therefore
// derived from the dolly, never animated alongside it.
var SUBJECT_SCALE = 1; // subject renders 1:1 => its plane is z = 0
var stage = document.getElementById("dz-stage");
var rig = document.getElementById("dz-rig");
function applyCamera(u) {
var d = d0 + (d1 - d0) * u; // dolly -- pure function of u
var P = SUBJECT_SCALE * d; // focal length solved from d
var camZ = P - d; // put the subject plane at camera distance d
stage.style.perspective = P + "px";
rig.style.transform = "translateZ(" + camZ + "px)";
}
// GSAP suppresses timeline-level onUpdate during seek(), so the camera
// is driven by a property setter on the tweened object instead: that
// fires on every render, seeks included.
var camera = {
_u: 0,
get u() {
return this._u;
},
set u(value) {
this._u = value;
applyCamera(value);
},
};
applyCamera(0);
var tl = gsap.timeline({ paused: true });
tl.to(camera, { u: 1, duration: 4, ease: ease }, 0);
window.__timelines["camera-dolly-zoom"] = tl;
})();
</script>
</body>
</html>
```
</VariablesExplorer>
## Install
<InstallCommand command="npx hyperframes add camera-dolly-zoom" />
That writes one file: `compositions/camera-dolly-zoom.html`.
## Add it to your video
It runs for 4 seconds at 1920×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="camera-dolly-zoom"
data-composition-src="compositions/camera-dolly-zoom.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
Tagged `camera` `depth` `3d` `cinematic`.
## Related topics
- [Browse the complete Catalog](/catalog)
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
- [Build a richer composition](/go-further)
+12 -376
View File
@@ -3,398 +3,34 @@ title: "Chromatic Radial Split"
description: "Shader transition with chromatic aberration radial split"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/chromatic-radial-split.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Chromatic Radial Split preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/chromatic-radial-split.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/chromatic-radial-split.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add chromatic-radial-split" />
```bash Terminal
npx hyperframes add chromatic-radial-split
```
That writes one file: `compositions/chromatic-radial-split.html`.
## Source
## Add it to your video
<Accordion title={`chromatic-radial-split.html`}>
It runs for 4 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="main"
data-composition-id="chromatic-radial-split"
data-composition-src="compositions/chromatic-radial-split.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<style>
[data-composition-id="main"] {
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #22223b;
}
/* Blueprint grid */
.bp-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
/* Layout */
.bp-left {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1200px;
display: flex;
align-items: center;
justify-content: center;
}
.bp-right {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 720px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px 60px 60px 40px;
}
.bp-divider {
position: absolute;
left: 1200px;
top: 60px;
bottom: 60px;
width: 1px;
background: #7678ed;
opacity: 0.15;
}
.bp-number {
font-family: "Space Mono", monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.4em;
text-transform: uppercase;
position: absolute;
top: 40px;
left: 60px;
color: #7678ed;
}
/* Right panel typography */
.bp-name {
font-family: "Bebas Neue", sans-serif;
font-size: 38px;
letter-spacing: 0.08em;
margin-bottom: 20px;
color: #7678ed;
}
.bp-plabel {
font-family: "Space Mono", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.3em;
text-transform: uppercase;
margin-bottom: 10px;
color: #7678ed;
opacity: 0.5;
}
.bp-prompt {
font-family: "JetBrains Mono", monospace;
font-size: 20px;
line-height: 1.6;
padding: 16px 20px;
border-radius: 6px;
margin-bottom: 20px;
color: #7678ed;
background: rgba(118, 120, 237, 0.06);
border: 1px solid rgba(118, 120, 237, 0.12);
}
.bp-desc {
font-family: "Lato", sans-serif;
font-size: 17px;
line-height: 1.6;
color: #9a8c98;
opacity: 0.55;
}
/* GL scenes (hidden, just for texture capture) */
.scene {
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.scene-label {
font-family: "Bebas Neue", sans-serif;
font-size: 140px;
letter-spacing: 0.06em;
opacity: 0.12;
}
</style>
<div class="bp-grid"></div>
<div class="bp-divider"></div>
<div class="bp-number">08 / 14</div>
<!-- GL demo scenes (captured as textures, then hidden) -->
<div id="s1" class="scene" style="background: #22223b">
<div class="scene-label" style="color: #7678ed">SCENE A</div>
</div>
<div id="s2" class="scene" style="background: #7678ed; opacity: 0">
<div class="scene-label" style="color: #22223b">SCENE B</div>
</div>
<!-- GL canvas (left panel only) -->
<canvas
id="gl-canvas"
width="1200"
height="1080"
style="
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
z-index: 10;
pointer-events: none;
display: none;
"
>
</canvas>
<!-- Right panel metadata -->
<div class="bp-right">
<div class="bp-name">Chromatic Radial Split</div>
<div class="bp-plabel">Prompt</div>
<div class="bp-prompt">"use chromatic radial split shader transition"</div>
<div class="bp-desc">
RGB channels separate and converge radially. From-scene splits outward, to-scene converges
inward.
</div>
</div>
<div
id="driver"
class="clip"
data-start="0"
data-duration="4"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var glCanvas = document.getElementById("gl-canvas");
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
if (!gl) {
console.warn("WebGL not available");
window.__timelines["main"] = gsap.timeline({ paused: true });
} else {
gl.viewport(0, 0, 1200, 1080);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
var sceneTextures = {};
function captureScene(sceneId) {
var scene = document.getElementById(sceneId);
var origOpacity = scene.style.opacity;
var origZ = scene.style.zIndex;
scene.style.opacity = "1";
scene.style.zIndex = "999";
scene.offsetHeight;
var c = document.createElement("canvas");
c.width = 1200;
c.height = 1080;
var ctx = c.getContext("2d");
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
ctx.fillRect(0, 0, 1200, 1080);
var sr = scene.getBoundingClientRect();
var els = scene.querySelectorAll("*");
for (var i = 0; i < els.length; i++) {
var el = els[i],
cs = window.getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
var r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
var x = r.left - sr.left,
y = r.top - sr.top,
w = r.width,
h = r.height;
ctx.save();
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
var bg = cs.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
ctx.fillStyle = bg;
ctx.fillRect(x, y, w, h);
}
var hasChildEls = el.querySelector("div,span,img,video");
var text = "";
for (var j = 0; j < el.childNodes.length; j++)
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
text = text.trim();
if (text && !hasChildEls) {
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
ctx.fillStyle = cs.color;
if (cs.letterSpacing && cs.letterSpacing !== "normal")
ctx.letterSpacing = cs.letterSpacing;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x + w / 2, y + h / 2);
}
ctx.restore();
}
scene.style.opacity = origOpacity;
scene.style.zIndex = origZ;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
sceneTextures[sceneId] = tex;
}
var vertSrc =
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
var quadBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW,
);
function compileShader(src, type) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error("Shader:", gl.getShaderInfoLog(s));
return s;
}
function mkProg(fragSrc) {
var p = gl.createProgram();
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
console.error("Link:", gl.getProgramInfoLog(p));
return p;
}
var H =
"precision mediump float;" +
"varying vec2 v_uv;" +
"uniform sampler2D u_from, u_to;" +
"uniform float u_progress;" +
"uniform vec2 u_resolution;\n";
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
var progTrans = mkProg(
H +
"void main(){vec2 c=v_uv-.5;float fromShift=u_progress*.06;float fr=texture2D(u_from,clamp(v_uv+c*fromShift,0.,1.)).r;float fg=texture2D(u_from,v_uv).g;float fb=texture2D(u_from,clamp(v_uv-c*fromShift,0.,1.)).b;vec3 fromSplit=vec3(fr,fg,fb);float toShift=(1.-u_progress)*.06;float tr=texture2D(u_to,clamp(v_uv-c*toShift,0.,1.)).r;float tg=texture2D(u_to,v_uv).g;float tb=texture2D(u_to,clamp(v_uv+c*toShift,0.,1.)).b;vec3 toSplit=vec3(tr,tg,tb);gl_FragColor=vec4(mix(fromSplit,toSplit,u_progress),1.);}",
);
function renderShader(prog, texFrom, texTo, progress) {
gl.useProgram(prog);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texFrom);
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texTo);
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
var pos = gl.getAttribLocation(prog, "a_pos");
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function easeInOut(p) {
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
}
captureScene("s1");
captureScene("s2");
glCanvas.style.display = "block";
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
document.querySelectorAll(".scene").forEach(function (s) {
s.style.opacity = "0";
});
var tl = gsap.timeline({
paused: true,
onUpdate: function () {
var t = tl.time();
if (t >= 1.0 && t < 3.0) {
renderShader(
progTrans,
sceneTextures["s1"],
sceneTextures["s2"],
easeInOut((t - 1.0) / 2.0),
);
} else if (t >= 3.0) {
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
} else {
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
}
},
});
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
window.__timelines["main"] = tl;
} // end if (gl)
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="chromatic-radial-split" data-composition-src="compositions/chromatic-radial-split.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+12 -376
View File
@@ -3,398 +3,34 @@ title: "Cinematic Zoom"
description: "Shader transition with dramatic zoom blur"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/cinematic-zoom.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Cinematic Zoom preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cinematic-zoom.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cinematic-zoom.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add cinematic-zoom" />
```bash Terminal
npx hyperframes add cinematic-zoom
```
That writes one file: `compositions/cinematic-zoom.html`.
## Source
## Add it to your video
<Accordion title={`cinematic-zoom.html`}>
It runs for 4 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="main"
data-composition-id="cinematic-zoom"
data-composition-src="compositions/cinematic-zoom.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<style>
[data-composition-id="main"] {
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #3d348b;
}
/* Blueprint grid */
.bp-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
/* Layout */
.bp-left {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1200px;
display: flex;
align-items: center;
justify-content: center;
}
.bp-right {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 720px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px 60px 60px 40px;
}
.bp-divider {
position: absolute;
left: 1200px;
top: 60px;
bottom: 60px;
width: 1px;
background: #f7b801;
opacity: 0.15;
}
.bp-number {
font-family: "Space Mono", monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.4em;
text-transform: uppercase;
position: absolute;
top: 40px;
left: 60px;
color: #f7b801;
}
/* Right panel typography */
.bp-name {
font-family: "Bebas Neue", sans-serif;
font-size: 38px;
letter-spacing: 0.08em;
margin-bottom: 20px;
color: #f7b801;
}
.bp-plabel {
font-family: "Space Mono", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.3em;
text-transform: uppercase;
margin-bottom: 10px;
color: #f7b801;
opacity: 0.5;
}
.bp-prompt {
font-family: "JetBrains Mono", monospace;
font-size: 20px;
line-height: 1.6;
padding: 16px 20px;
border-radius: 6px;
margin-bottom: 20px;
color: #f7b801;
background: rgba(247, 184, 1, 0.06);
border: 1px solid rgba(247, 184, 1, 0.12);
}
.bp-desc {
font-family: "Lato", sans-serif;
font-size: 17px;
line-height: 1.6;
color: #a0a0c0;
opacity: 0.55;
}
/* GL scenes (hidden, just for texture capture) */
.scene {
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.scene-label {
font-family: "Bebas Neue", sans-serif;
font-size: 140px;
letter-spacing: 0.06em;
opacity: 0.12;
}
</style>
<div class="bp-grid"></div>
<div class="bp-divider"></div>
<div class="bp-number">07 / 14</div>
<!-- GL demo scenes (captured as textures, then hidden) -->
<div id="s1" class="scene" style="background: #3d348b">
<div class="scene-label" style="color: #f7b801">SCENE A</div>
</div>
<div id="s2" class="scene" style="background: #f7b801; opacity: 0">
<div class="scene-label" style="color: #3d348b">SCENE B</div>
</div>
<!-- GL canvas (left panel only) -->
<canvas
id="gl-canvas"
width="1200"
height="1080"
style="
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
z-index: 10;
pointer-events: none;
display: none;
"
>
</canvas>
<!-- Right panel metadata -->
<div class="bp-right">
<div class="bp-name">Cinematic Zoom</div>
<div class="bp-plabel">Prompt</div>
<div class="bp-prompt">"use cinematic zoom shader transition"</div>
<div class="bp-desc">
Both scenes zoom-blur with per-channel radial offset. From zooms outward, to zooms inward
from tight.
</div>
</div>
<div
id="driver"
class="clip"
data-start="0"
data-duration="4"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var glCanvas = document.getElementById("gl-canvas");
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
if (!gl) {
console.warn("WebGL not available");
window.__timelines["main"] = gsap.timeline({ paused: true });
} else {
gl.viewport(0, 0, 1200, 1080);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
var sceneTextures = {};
function captureScene(sceneId) {
var scene = document.getElementById(sceneId);
var origOpacity = scene.style.opacity;
var origZ = scene.style.zIndex;
scene.style.opacity = "1";
scene.style.zIndex = "999";
scene.offsetHeight;
var c = document.createElement("canvas");
c.width = 1200;
c.height = 1080;
var ctx = c.getContext("2d");
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
ctx.fillRect(0, 0, 1200, 1080);
var sr = scene.getBoundingClientRect();
var els = scene.querySelectorAll("*");
for (var i = 0; i < els.length; i++) {
var el = els[i],
cs = window.getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
var r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
var x = r.left - sr.left,
y = r.top - sr.top,
w = r.width,
h = r.height;
ctx.save();
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
var bg = cs.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
ctx.fillStyle = bg;
ctx.fillRect(x, y, w, h);
}
var hasChildEls = el.querySelector("div,span,img,video");
var text = "";
for (var j = 0; j < el.childNodes.length; j++)
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
text = text.trim();
if (text && !hasChildEls) {
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
ctx.fillStyle = cs.color;
if (cs.letterSpacing && cs.letterSpacing !== "normal")
ctx.letterSpacing = cs.letterSpacing;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x + w / 2, y + h / 2);
}
ctx.restore();
}
scene.style.opacity = origOpacity;
scene.style.zIndex = origZ;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
sceneTextures[sceneId] = tex;
}
var vertSrc =
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
var quadBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW,
);
function compileShader(src, type) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error("Shader:", gl.getShaderInfoLog(s));
return s;
}
function mkProg(fragSrc) {
var p = gl.createProgram();
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
console.error("Link:", gl.getProgramInfoLog(p));
return p;
}
var H =
"precision mediump float;" +
"varying vec2 v_uv;" +
"uniform sampler2D u_from, u_to;" +
"uniform float u_progress;" +
"uniform vec2 u_resolution;\n";
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
var progTrans = mkProg(
H +
"void main(){vec2 d=v_uv-vec2(.5);float fromS=u_progress*.08;float toS=(1.-u_progress)*.06;float fr=0.,fg=0.,fb=0.;for(int i=0;i<12;i++){float f=float(i)/12.;fr+=texture2D(u_from,v_uv-d*(fromS*1.06)*f).r;fg+=texture2D(u_from,v_uv-d*fromS*f).g;fb+=texture2D(u_from,v_uv-d*(fromS*.94)*f).b;}vec3 fromBl=vec3(fr,fg,fb)/12.;float tr=0.,tg=0.,tb=0.;for(int i=0;i<12;i++){float f=float(i)/12.;tr+=texture2D(u_to,v_uv+d*(toS*1.06)*f).r;tg+=texture2D(u_to,v_uv+d*toS*f).g;tb+=texture2D(u_to,v_uv+d*(toS*.94)*f).b;}vec3 toBl=vec3(tr,tg,tb)/12.;gl_FragColor=vec4(mix(fromBl,toBl,u_progress),1.);}",
);
function renderShader(prog, texFrom, texTo, progress) {
gl.useProgram(prog);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texFrom);
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texTo);
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
var pos = gl.getAttribLocation(prog, "a_pos");
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function easeInOut(p) {
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
}
captureScene("s1");
captureScene("s2");
glCanvas.style.display = "block";
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
document.querySelectorAll(".scene").forEach(function (s) {
s.style.opacity = "0";
});
var tl = gsap.timeline({
paused: true,
onUpdate: function () {
var t = tl.time();
if (t >= 1.0 && t < 3.0) {
renderShader(
progTrans,
sceneTextures["s1"],
sceneTextures["s2"],
easeInOut((t - 1.0) / 2.0),
);
} else if (t >= 3.0) {
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
} else {
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
}
},
});
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
window.__timelines["main"] = tl;
} // end if (gl)
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="cinematic-zoom" data-composition-src="compositions/cinematic-zoom.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+12 -623
View File
@@ -3,645 +3,34 @@ title: "Code 3D Extrude"
description: "Syntax-highlighted code on a lit, beveled 3D slab that rotates through real space and settles to a readable rest — true WebGL depth and lighting, not a 2D transform."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-3d-extrude.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code 3D Extrude preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-3d-extrude.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-3d-extrude.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-3d-extrude" />
```bash Terminal
npx hyperframes add code-3d-extrude
```
That writes one file: `compositions/code-3d-extrude.html`.
## Source
## Add it to your video
<Accordion title={`code-3d-extrude.html`}>
It runs for 8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Code 3D Extrude</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #05070b;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
}
#gl {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
display: block;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="code-3d-extrude"
data-composition-src="compositions/code-3d-extrude.html"
data-start="0"
data-duration="8"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<canvas id="gl" width="1920" height="1080"></canvas>
</div>
<script>
window.__TOKENS = {
feature: {
lang: "js",
theme: "github-dark",
bg: "#24292e",
fg: "#e1e4e8",
states: [
{
code: 'async function loadConfig(path) {\n const raw = await readFile(path, "utf8")\n const config = JSON.parse(raw)\n return validate(config)\n}',
tokens: [
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-0",
content: "async",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-1",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-2",
content: "function",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-3",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-4",
content: "loadConfig",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-5",
content: "(",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-6",
content: "path",
color: "#FFAB70",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-7",
content: ") {",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-8",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-9",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-10",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-11",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-12",
content: "raw",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-13",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-14",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-15",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-16",
content: "await",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-17",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-18",
content: "readFile",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-19",
content: "(path,",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-20",
content: " ",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-21",
content: '"utf8"',
color: "#9ECBFF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-22",
content: ")",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-23",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-24",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-25",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-26",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-27",
content: "config",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-28",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-29",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-30",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-31",
content: "JSON",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-32",
content: ".",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-33",
content: "parse",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-34",
content: "(raw)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-35",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-36",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-37",
content: "return",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-38",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-39",
content: "validate",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-40",
content: "(config)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-41",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-42",
content: "}",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-43",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
],
},
],
},
};
window.__BLOCK = {
id: "code-3d-extrude",
effect: "extrude",
seq: "feature",
duration: 8,
seed: 7,
};
</script>
<script>
// hf-code-anim-gl.js — WebGL code-animation engine for the "crazy" Code Animations blocks.
//
// Bridges the baked Shiki tokens (window.__TOKENS) into Three.js: the tokens are drawn once
// to an offscreen 2D canvas (a CanvasTexture), which then drives a lit 3D slab, a dissolve
// shader, or a GPU particle system. Determinism per the project contract:
// - render only via tl.eventCallback("onUpdate", …) + one initial render() — never rAF
// - all randomness from a seeded mulberry32 — never Math.random / Date.now
// - canvas drawn once after document.fonts.ready (final glyph metrics)
// - paused GSAP timeline registered on window.__timelines[id]; HF seeks it per frame
//
// Each block sets window.__BLOCK = { id, effect, seq, duration, seed } and includes this engine.
(function () {
"use strict";
var T = window.__TOKENS || {};
// ---- seeded PRNG (no Math.random) -----------------------------------------
function mulberry32(a) {
return function () {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// ---- split a token state into lines of {content,color} --------------------
function splitLines(state) {
var lines = [[]];
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
lines.push([]);
continue;
}
lines[lines.length - 1].push(t);
}
while (lines.length && lines[lines.length - 1].length === 0) lines.pop();
return lines;
}
// ---- THE BRIDGE: draw baked tokens to an offscreen 2D canvas ---------------
// Returns { canvas, W, H, lines, lineH, padY, fontPx } so effects can sample it.
function drawCodeToCanvas(state, opts) {
opts = opts || {};
var fontPx = opts.fontPx || 36;
var lineH = opts.lineH || 54;
var padX = opts.padX || 70;
var padY = opts.padY || 60;
var weight = opts.weight || 500;
var font = weight + " " + fontPx + 'px "JetBrains Mono", monospace';
var lines = splitLines(state);
var measure = document.createElement("canvas").getContext("2d");
measure.font = font;
var maxW = 0;
for (var i = 0; i < lines.length; i++) {
var w = 0;
for (var j = 0; j < lines[i].length; j++)
w += measure.measureText(lines[i][j].content).width;
if (w > maxW) maxW = w;
}
var W = Math.max(2, Math.ceil(maxW + padX * 2));
var H = Math.max(2, Math.ceil(lines.length * lineH + padY * 2));
var canvas = document.createElement("canvas");
canvas.width = W;
canvas.height = H;
var ctx = canvas.getContext("2d");
if (opts.transparent) {
ctx.clearRect(0, 0, W, H);
} else {
ctx.fillStyle = opts.bg || "#0b0f17";
ctx.fillRect(0, 0, W, H);
}
ctx.textBaseline = "top";
ctx.font = font;
for (var li = 0; li < lines.length; li++) {
var x = padX;
var y = padY + li * lineH;
for (var k = 0; k < lines[li].length; k++) {
var tok = lines[li][k];
ctx.fillStyle = tok.color;
ctx.fillText(tok.content, x, y);
x += ctx.measureText(tok.content).width;
}
}
return {
canvas: canvas,
W: W,
H: H,
lines: lines.length,
lineH: lineH,
padY: padY,
fontPx: fontPx,
};
}
function fxExtrude(THREE, scene, camera, renderer, state, st, tl, dur) {
scene.background = new THREE.Color(0x05070b);
var code = drawCodeToCanvas(state, {
fontPx: 38,
lineH: 56,
padX: 80,
padY: 70,
weight: 600,
});
var tex = new THREE.CanvasTexture(code.canvas);
tex.minFilter = THREE.LinearFilter;
tex.magFilter = THREE.LinearFilter;
tex.generateMipmaps = false;
var aspect = code.W / code.H;
var slabH = 6.2;
var slabW = slabH * aspect;
var depth = 0.5;
// beveled slab: rounded extruded rectangle
var hw = slabW / 2;
var hh = slabH / 2;
var r = 0.18;
var shape = new THREE.Shape();
shape.moveTo(-hw + r, -hh);
shape.lineTo(hw - r, -hh);
shape.quadraticCurveTo(hw, -hh, hw, -hh + r);
shape.lineTo(hw, hh - r);
shape.quadraticCurveTo(hw, hh, hw - r, hh);
shape.lineTo(-hw + r, hh);
shape.quadraticCurveTo(-hw, hh, -hw, hh - r);
shape.lineTo(-hw, -hh + r);
shape.quadraticCurveTo(-hw, -hh, -hw + r, -hh);
var geo = new THREE.ExtrudeGeometry(shape, {
depth: depth,
bevelEnabled: true,
bevelThickness: 0.12,
bevelSize: 0.1,
bevelSegments: 3,
steps: 1,
});
geo.center();
// UVs from ExtrudeGeometry don't map the texture nicely to the face; remap front-face UVs from XY bounds.
geo.computeBoundingBox();
var bb = geo.boundingBox;
var sizeX = bb.max.x - bb.min.x;
var sizeY = bb.max.y - bb.min.y;
var pos = geo.attributes.position;
var uv = geo.attributes.uv;
for (var vi = 0; vi < pos.count; vi++) {
uv.setXY(vi, (pos.getX(vi) - bb.min.x) / sizeX, (pos.getY(vi) - bb.min.y) / sizeY);
}
uv.needsUpdate = true;
// Face is an unlit "screen" — shows the exact vivid Shiki colors (no PBR/emissive wash).
// The 3D read comes from the lit, beveled EDGES + perspective, like a glowing monitor.
var faceMat = new THREE.MeshBasicMaterial({ map: tex });
var edgeMat = new THREE.MeshStandardMaterial({
color: 0x141d2b,
roughness: 0.3,
metalness: 0.4,
});
// ExtrudeGeometry groups: 0 = front+back faces, 1 = sides. Front shows the code.
var slab = new THREE.Mesh(geo, [faceMat, edgeMat]);
scene.add(slab);
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
var key = new THREE.DirectionalLight(0xbcd4ff, 1.25);
key.position.set(5, 7, 9);
scene.add(key);
var rim = new THREE.DirectionalLight(0x4fffa0, 0.45);
rim.position.set(-7, -3, 5);
scene.add(rim);
camera.position.set(0, 0, 10);
// fit camera distance so the WHOLE slab fits (width-limited for wide code), ~78% fill
var vfov = (camera.fov * Math.PI) / 180;
var screenAspect = 1920 / 1080;
var fitH = slabH / 2 / Math.tan(vfov / 2) / 0.78;
var fitW = slabW / 2 / (Math.tan(vfov / 2) * screenAspect) / 0.78;
var fitZ = Math.max(fitH, fitW);
st.rotY = -1.0;
st.rotX = 0.4;
st.camZ = fitZ + 6;
tl.to(st, { rotY: 0, rotX: 0, camZ: fitZ, duration: dur * 0.6, ease: "power3.out" }, 0);
// gentle breathing drift at rest (subtle — stays readable)
tl.to(
st,
{ rotY: 0.06, rotX: -0.02, duration: dur * 0.4, ease: "sine.inOut" },
dur * 0.6,
);
return function () {
slab.rotation.y = st.rotY;
slab.rotation.x = st.rotX;
camera.position.z = st.camZ;
camera.lookAt(0, 0, 0);
// boot loop's render() issues the single renderer.render() — don't double-render here
};
}
// ---- dispatch + boot ------------------------------------------------------
function buildEffect(THREE, scene, camera, renderer, spec, st, tl) {
var state = T[spec.seq].states[0];
var dur = spec.duration;
if (spec.effect === "extrude")
return fxExtrude(THREE, scene, camera, renderer, state, st, tl, dur);
return function () {};
}
// Free GPU resources (renderer, geometries, materials, textures) on page teardown.
// One-shot renders exit per job so nothing leaks there; this guards long-lived
// Studio/player sessions that reload. Fires only on unload — never during capture.
function disposeGL(renderer, scene) {
scene.traverse(function (o) {
if (o.geometry && o.geometry.dispose) o.geometry.dispose();
var mats = o.material ? (Array.isArray(o.material) ? o.material : [o.material]) : [];
mats.forEach(function (m) {
if (m.map && m.map.dispose) m.map.dispose();
if (
m.uniforms &&
m.uniforms.uTex &&
m.uniforms.uTex.value &&
m.uniforms.uTex.value.dispose
) {
m.uniforms.uTex.value.dispose();
}
if (m.dispose) m.dispose();
});
});
if (renderer.dispose) renderer.dispose();
}
function go() {
var spec = window.__BLOCK;
var root = document.getElementById("root");
var glcanvas = document.getElementById("gl");
var W = 1920;
var H = 1080;
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
window.__timelines[spec.id] = tl; // register synchronously
var THREE = window.THREE;
var renderer = new THREE.WebGLRenderer({
canvas: glcanvas,
antialias: true,
alpha: false,
preserveDrawingBuffer: true,
});
renderer.setPixelRatio(1);
renderer.setSize(W, H, false);
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 200);
var st = {};
document.fonts.ready.then(function () {
var renderFn = buildEffect(THREE, scene, camera, renderer, spec, st, tl);
function render() {
renderFn();
renderer.render(scene, camera);
}
tl.eventCallback("onUpdate", render);
var dur = parseFloat(root.dataset.duration) || tl.duration();
tl.to({}, { duration: dur }, 0); // pad to full composition length
render(); // paint frame 0
window.addEventListener("beforeunload", function () {
disposeGL(renderer, scene);
});
});
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
else go();
})();
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="code-3d-extrude" data-composition-src="compositions/code-3d-extrude.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+12 -826
View File
@@ -3,848 +3,34 @@ title: "Code Diff"
description: "An edit shown as a colored diff — removed lines collapse in red, added lines expand in green."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-diff.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Diff preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-diff.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-diff.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-diff" />
```bash Terminal
npx hyperframes add code-diff
```
That writes one file: `compositions/code-diff.html`.
## Source
## Add it to your video
<Accordion title={`code-diff.html`}>
It runs for 6 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Code Diff</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #05070b;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
font-family: "JetBrains Mono", monospace;
color: #e6edf3;
}
.bg-field {
position: absolute;
inset: 0;
background: radial-gradient(1200px 700px at 50% 18%, #0e1726 0%, #070b12 55%, #05070b 100%);
}
.bg-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(88, 166, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(88, 166, 255, 0.05) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: radial-gradient(1100px 620px at 50% 42%, #000 0%, transparent 78%);
}
.bg-glow {
position: absolute;
width: 900px;
height: 900px;
border-radius: 50%;
filter: blur(120px);
opacity: 0.5;
}
.bg-glow.a {
background: #1f6feb55;
left: -180px;
top: -200px;
}
.bg-glow.b {
background: #2ea04355;
right: -200px;
bottom: -260px;
}
.editor {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 1380px;
height: 800px;
background: #0b0f17;
border: 1px solid #1d2733;
border-radius: 16px;
box-shadow:
0 40px 120px rgba(0, 0, 0, 0.6),
0 2px 0 rgba(255, 255, 255, 0.03) inset;
overflow: hidden;
display: flex;
flex-direction: column;
}
.titlebar {
height: 56px;
flex: 0 0 56px;
display: flex;
align-items: center;
gap: 16px;
padding: 0 22px;
background: linear-gradient(#11161f, #0c111a);
border-bottom: 1px solid #1b2430;
}
.dots {
display: flex;
gap: 10px;
}
.dot {
width: 14px;
height: 14px;
border-radius: 50%;
}
.dot.r {
background: #ff5f57;
}
.dot.y {
background: #febc2e;
}
.dot.g {
background: #28c840;
}
.filename {
font-size: 19px;
color: #8b98a9;
letter-spacing: 0.2px;
}
.filename .accent {
color: #d6e2f0;
}
.code-surface {
position: relative;
flex: 1 1 auto;
overflow: hidden;
}
.scene {
position: absolute;
inset: 0;
opacity: 0;
will-change: opacity;
}
.code {
position: absolute;
top: 38px;
left: 0;
right: 0;
padding: 0 44px 0 104px;
font-size: 30px;
line-height: 46px;
font-variant-ligatures: none;
tab-size: 2;
}
.gutter {
position: absolute;
top: 38px;
left: 0;
width: 84px;
padding-right: 18px;
font-size: 30px;
line-height: 46px;
text-align: right;
color: #828c9b;
user-select: none;
}
.ln {
display: block;
height: 46px;
}
.line {
display: block;
min-height: 46px;
white-space: pre;
position: relative;
z-index: 1;
}
.tok {
display: inline-block;
white-space: pre;
will-change: transform, opacity;
}
.caret {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 32px;
margin-top: 6px;
background: #58a6ff;
border-radius: 1px;
z-index: 3;
}
.hl-box {
position: absolute;
background: rgba(88, 166, 255, 0.16);
border-left: 3px solid #58a6ff;
border-radius: 6px;
z-index: 0;
overflow: hidden;
}
.diff-line {
display: block;
min-height: 0;
line-height: 46px;
white-space: pre;
position: relative;
overflow: hidden;
padding-left: 14px;
}
.diff-sign {
display: inline-block;
width: 22px;
color: #41506220;
}
.diff-del {
background: rgba(248, 81, 73, 0.12);
box-shadow: inset 3px 0 #f85149;
}
.diff-add {
background: rgba(63, 185, 80, 0.12);
box-shadow: inset 3px 0 #3fb950;
}
.diff-del .diff-sign {
color: #f85149;
}
.diff-add .diff-sign {
color: #3fb950;
}
.flight-block {
position: absolute;
white-space: pre;
font-size: 27px;
line-height: 40px;
padding: 11px 22px;
border-radius: 11px;
background: #0f1722;
border: 1px solid #233044;
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.5);
will-change: transform, opacity;
}
.flight-target {
position: absolute;
border: 1.5px dashed #2a3a4f;
border-radius: 14px;
opacity: 0;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="code-diff"
data-composition-src="compositions/code-diff.html"
data-start="0"
data-duration="6"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<div class="bg-field" data-layout-ignore></div>
<div class="bg-grid" data-layout-ignore></div>
<div class="bg-glow a" data-layout-ignore></div>
<div class="bg-glow b" data-layout-ignore></div>
<div class="editor" id="editor" data-layout-ignore>
<div class="titlebar">
<div class="dots">
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
</div>
<span class="filename"><span class="accent">greet.js</span> — Code Diff</span>
</div>
<div class="code-surface" id="surface"></div>
</div>
</div>
<script>
window.__TOKENS = {
diff: {
lang: "js",
theme: "github-dark",
bg: "#24292e",
fg: "#e1e4e8",
states: [
{
code: 'function greet(name) {\n console.log("hi " + name)\n}',
tokens: [
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-0",
content: "function",
color: "#F97583",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-1",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-2",
content: "greet",
color: "#B392F0",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-3",
content: "(",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-4",
content: "name",
color: "#FFAB70",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-5",
content: ") {",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-6",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-7",
content: " ",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-8",
content: "console.",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-9",
content: "log",
color: "#B392F0",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-10",
content: "(",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-11",
content: '"hi "',
color: "#9ECBFF",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-12",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-13",
content: "+",
color: "#F97583",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-14",
content: " ",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-15",
content: "name)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-16",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-17",
content: "}",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-18",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
],
},
{
code: 'function greet(name, lang) {\n const msg = translate("hi", lang)\n console.log(`${msg} ${name}`)\n}',
tokens: [
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-0",
content: "function",
color: "#F97583",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-1",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-2",
content: "greet",
color: "#B392F0",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-3",
content: "(",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-4",
content: "name",
color: "#FFAB70",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-5",
content: ",",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-6",
content: " ",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-7",
content: "lang",
color: "#FFAB70",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-5",
content: ") {",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-6",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-7",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-11",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-12",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-13",
content: "msg",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-14",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-15",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-16",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-17",
content: "translate",
color: "#B392F0",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-10",
content: "(",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-19",
content: '"hi"',
color: "#9ECBFF",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-20",
content: ", lang)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-21",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-22",
content: " ",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-23",
content: "console.",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-9",
content: "log",
color: "#B392F0",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-25",
content: "(",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-26",
content: "`${",
color: "#9ECBFF",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-27",
content: "msg",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-28",
content: "} ${",
color: "#9ECBFF",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-29",
content: "name",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-30",
content: "}`",
color: "#9ECBFF",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-31",
content: ")",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-16",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-17",
content: "}",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-34",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
],
},
],
},
};
window.__BLOCK = { id: "code-diff", effect: "diff", seq: "diff", duration: 6 };
</script>
<script>
// hf-code-anim.js — self-contained code-animation engine for HyperFrames registry blocks.
//
// Reuses Shiki Magic Move's baked output (window.__TOKENS, baked at author time) and
// drives the animation as a PAUSED GSAP timeline the HyperFrames engine seeks per frame —
// NOT CSS transitions (those break under non-sequential frame seeking). FLIP measurement
// runs once inside document.fonts.ready (glyph metrics must be final). Each block sets
// window.__BLOCK = { id, effect, seq, line?, duration } and includes this engine; the
// engine builds one scene that fills the composition and registers window.__timelines[id].
(function () {
"use strict";
var T = window.__TOKENS || {};
function lineDiv() {
var d = document.createElement("div");
d.className = "line";
return d;
}
// Render a code state's keyed tokens into codeEl as lines of inline-block .tok spans.
function renderState(codeEl, state) {
codeEl.textContent = "";
var keyToEl = new Map();
var line = lineDiv();
codeEl.appendChild(line);
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
line = lineDiv();
codeEl.appendChild(line);
continue;
}
var span = document.createElement("span");
span.className = "tok";
span.textContent = t.content;
span.style.color = t.color;
if (t.fontStyle & 1) span.style.fontStyle = "italic";
if (t.fontStyle & 2) span.style.fontWeight = "700";
line.appendChild(span);
keyToEl.set(t.key, span);
}
var last = codeEl.lastChild;
if (last && last.className === "line" && last.children.length === 0)
codeEl.removeChild(last);
return keyToEl;
}
function rectsOf(keyToEl) {
var m = new Map();
keyToEl.forEach(function (el, k) {
var r = el.getBoundingClientRect();
m.set(k, { left: r.left, top: r.top });
});
return m;
}
function makeScene(surface, withGutter) {
var scene = document.createElement("div");
scene.className = "scene";
if (withGutter) {
var g = document.createElement("div");
g.className = "gutter";
scene.appendChild(g);
}
var code = document.createElement("div");
code.className = "code";
scene.appendChild(code);
surface.appendChild(scene);
return { scene: scene, code: code, gutter: scene.querySelector(".gutter") };
}
function fillGutter(g, n) {
var html = "";
for (var i = 1; i <= n; i++) html += '<span class="ln">' + i + "</span>";
g.innerHTML = html;
}
function splitLines(state) {
var lines = [{ text: "", tokens: [] }];
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
lines.push({ text: "", tokens: [] });
continue;
}
var cur = lines[lines.length - 1];
cur.tokens.push(t);
cur.text += t.content;
}
if (lines.length && lines[lines.length - 1].tokens.length === 0) lines.pop();
return lines;
}
function lineDiff(a, b) {
var n = a.length;
var m = b.length;
var dp = [];
for (var i = 0; i <= n; i++) dp.push(new Array(m + 1).fill(0));
for (var i = n - 1; i >= 0; i--) {
for (var j = m - 1; j >= 0; j--) {
dp[i][j] =
a[i].text === b[j].text
? dp[i + 1][j + 1] + 1
: Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
var ops = [];
var i = 0;
var j = 0;
while (i < n && j < m) {
if (a[i].text === b[j].text) {
ops.push({ type: "same", line: b[j] });
i++;
j++;
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
ops.push({ type: "del", line: a[i] });
i++;
} else {
ops.push({ type: "add", line: b[j] });
j++;
}
}
while (i < n) ops.push({ type: "del", line: a[i++] });
while (j < m) ops.push({ type: "add", line: b[j++] });
return ops;
}
// ---- single-scene effect builders (fade in, play, hold to end) -------------
var FADE = 0.45;
function diff(tl, surface, states, start) {
var DEL = 0.55;
var ADD = 0.6;
var ops = lineDiff(splitLines(states[0]), splitLines(states[1]));
var parts = makeScene(surface, false);
var code = parts.code;
var delEls = [];
var addEls = [];
ops.forEach(function (op) {
var ln = document.createElement("div");
ln.className =
"diff-line" +
(op.type === "del" ? " diff-del" : op.type === "add" ? " diff-add" : "");
var sign = document.createElement("span");
sign.className = "diff-sign";
sign.textContent = op.type === "del" ? "- " : op.type === "add" ? "+ " : " ";
ln.appendChild(sign);
op.line.tokens.forEach(function (t) {
var s = document.createElement("span");
s.className = "tok";
s.textContent = t.content;
s.style.color = t.color;
ln.appendChild(s);
});
code.appendChild(ln);
if (op.type === "del") delEls.push(ln);
if (op.type === "add") addEls.push(ln);
});
tl.set(parts.scene, { opacity: 0 }, 0);
tl.to(parts.scene, { opacity: 1, duration: FADE, ease: "power1.out" }, start);
var atDel = start + FADE + 0.3;
delEls.forEach(function (el, k) {
tl.fromTo(
el,
{ height: 46, opacity: 1 },
{ height: 0, opacity: 0, duration: DEL, ease: "power2.inOut" },
atDel + k * 0.08,
);
});
var atAdd = atDel + DEL + 0.15;
addEls.forEach(function (el, k) {
tl.set(el, { height: 0, opacity: 0 }, 0);
tl.fromTo(
el,
{ height: 0, opacity: 0 },
{ height: 46, opacity: 1, duration: ADD, ease: "power2.out" },
atAdd + k * 0.12,
);
});
}
function buildEffect(tl, surface, spec) {
var start = 0.45;
var seq = T[spec.seq];
if (spec.effect === "diff") diff(tl, surface, seq.states, start);
}
function go() {
var spec = window.__BLOCK;
var surface = document.getElementById("surface");
var root = document.getElementById("root");
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// Build inside fonts.ready (glyph metrics must be final), then register.
// Register ONLY after the timeline is fully built: the runtime's
// sub-composition readiness gate treats "key present" as "ready" and
// nests the child once. An empty timeline registered before this build
// would be nested empty and never re-nested → blank render when used as
// a sub-composition. See the contract in engine frameCapture.ts.
document.fonts.ready.then(function () {
tl.from("#editor", { opacity: 0, scale: 0.985, duration: 0.5, ease: "power2.out" }, 0);
buildEffect(tl, surface, spec);
var dur = parseFloat(root.dataset.duration) || tl.duration();
tl.to({}, { duration: dur }, 0); // pad to full composition length
window.__timelines["code-diff"] = tl; // register AFTER setup completes
if (typeof window.__hfForceTimelineRebind === "function") {
window.__hfForceTimelineRebind(); // re-nest now that we are populated
}
});
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
else go();
})();
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="code-diff" data-composition-src="compositions/code-diff.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+13 -756
View File
@@ -1,779 +1,36 @@
---
title: "Code Highlight Sweep"
description: "A highlight band sweeps across a target line while the surrounding context dims — draws the eye to one line."
description: "A highlight band sweeps across a target line while the surrounding context dims — draws the eye to one line. `line` is 0-based: `line: 1` targets the second displayed line (unlike code-scroll, whose target is 1-based)."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-highlight.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Highlight Sweep preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-highlight.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-highlight.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-highlight" />
```bash Terminal
npx hyperframes add code-highlight
```
That writes one file: `compositions/code-highlight.html`.
## Source
## Add it to your video
<Accordion title={`code-highlight.html`}>
It runs for 5 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Code Highlight Sweep</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #05070b;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
font-family: "JetBrains Mono", monospace;
color: #e6edf3;
}
.bg-field {
position: absolute;
inset: 0;
background: radial-gradient(1200px 700px at 50% 18%, #0e1726 0%, #070b12 55%, #05070b 100%);
}
.bg-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(88, 166, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(88, 166, 255, 0.05) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: radial-gradient(1100px 620px at 50% 42%, #000 0%, transparent 78%);
}
.bg-glow {
position: absolute;
width: 900px;
height: 900px;
border-radius: 50%;
filter: blur(120px);
opacity: 0.5;
}
.bg-glow.a {
background: #1f6feb55;
left: -180px;
top: -200px;
}
.bg-glow.b {
background: #2ea04355;
right: -200px;
bottom: -260px;
}
.editor {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 1380px;
height: 800px;
background: #0b0f17;
border: 1px solid #1d2733;
border-radius: 16px;
box-shadow:
0 40px 120px rgba(0, 0, 0, 0.6),
0 2px 0 rgba(255, 255, 255, 0.03) inset;
overflow: hidden;
display: flex;
flex-direction: column;
}
.titlebar {
height: 56px;
flex: 0 0 56px;
display: flex;
align-items: center;
gap: 16px;
padding: 0 22px;
background: linear-gradient(#11161f, #0c111a);
border-bottom: 1px solid #1b2430;
}
.dots {
display: flex;
gap: 10px;
}
.dot {
width: 14px;
height: 14px;
border-radius: 50%;
}
.dot.r {
background: #ff5f57;
}
.dot.y {
background: #febc2e;
}
.dot.g {
background: #28c840;
}
.filename {
font-size: 19px;
color: #8b98a9;
letter-spacing: 0.2px;
}
.filename .accent {
color: #d6e2f0;
}
.code-surface {
position: relative;
flex: 1 1 auto;
overflow: hidden;
}
.scene {
position: absolute;
inset: 0;
opacity: 0;
will-change: opacity;
}
.code {
position: absolute;
top: 38px;
left: 0;
right: 0;
padding: 0 44px 0 104px;
font-size: 30px;
line-height: 46px;
font-variant-ligatures: none;
tab-size: 2;
}
.gutter {
position: absolute;
top: 38px;
left: 0;
width: 84px;
padding-right: 18px;
font-size: 30px;
line-height: 46px;
text-align: right;
color: #828c9b;
user-select: none;
}
.ln {
display: block;
height: 46px;
}
.line {
display: block;
min-height: 46px;
white-space: pre;
position: relative;
z-index: 1;
}
.tok {
display: inline-block;
white-space: pre;
will-change: transform, opacity;
}
.caret {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 32px;
margin-top: 6px;
background: #58a6ff;
border-radius: 1px;
z-index: 3;
}
.hl-box {
position: absolute;
background: rgba(88, 166, 255, 0.16);
border-left: 3px solid #58a6ff;
border-radius: 6px;
z-index: 0;
overflow: hidden;
}
.diff-line {
display: block;
min-height: 0;
line-height: 46px;
white-space: pre;
position: relative;
overflow: hidden;
padding-left: 14px;
}
.diff-sign {
display: inline-block;
width: 22px;
color: #41506220;
}
.diff-del {
background: rgba(248, 81, 73, 0.12);
box-shadow: inset 3px 0 #f85149;
}
.diff-add {
background: rgba(63, 185, 80, 0.12);
box-shadow: inset 3px 0 #3fb950;
}
.diff-del .diff-sign {
color: #f85149;
}
.diff-add .diff-sign {
color: #3fb950;
}
.flight-block {
position: absolute;
white-space: pre;
font-size: 27px;
line-height: 40px;
padding: 11px 22px;
border-radius: 11px;
background: #0f1722;
border: 1px solid #233044;
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.5);
will-change: transform, opacity;
}
.flight-target {
position: absolute;
border: 1.5px dashed #2a3a4f;
border-radius: 14px;
opacity: 0;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="code-highlight"
data-composition-src="compositions/code-highlight.html"
data-start="0"
data-duration="5"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<div class="bg-field" data-layout-ignore></div>
<div class="bg-grid" data-layout-ignore></div>
<div class="bg-glow a" data-layout-ignore></div>
<div class="bg-glow b" data-layout-ignore></div>
<div class="editor" id="editor" data-layout-ignore>
<div class="titlebar">
<div class="dots">
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
</div>
<span class="filename"
><span class="accent">loadConfig.js</span> — Code Highlight Sweep</span
>
</div>
<div class="code-surface" id="surface"></div>
</div>
</div>
<script>
window.__TOKENS = {
feature: {
lang: "js",
theme: "github-dark",
bg: "#24292e",
fg: "#e1e4e8",
states: [
{
code: 'async function loadConfig(path) {\n const raw = await readFile(path, "utf8")\n const config = JSON.parse(raw)\n return validate(config)\n}',
tokens: [
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-0",
content: "async",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-1",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-2",
content: "function",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-3",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-4",
content: "loadConfig",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-5",
content: "(",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-6",
content: "path",
color: "#FFAB70",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-7",
content: ") {",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-8",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-9",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-10",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-11",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-12",
content: "raw",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-13",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-14",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-15",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-16",
content: "await",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-17",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-18",
content: "readFile",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-19",
content: "(path,",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-20",
content: " ",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-21",
content: '"utf8"',
color: "#9ECBFF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-22",
content: ")",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-23",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-24",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-25",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-26",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-27",
content: "config",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-28",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-29",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-30",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-31",
content: "JSON",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-32",
content: ".",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-33",
content: "parse",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-34",
content: "(raw)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-35",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-36",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-37",
content: "return",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-38",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-39",
content: "validate",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-40",
content: "(config)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-41",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-42",
content: "}",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-43",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
],
},
],
},
};
window.__BLOCK = {
id: "code-highlight",
effect: "highlight",
seq: "feature",
line: 1,
duration: 5,
};
</script>
<script>
// hf-code-anim.js — self-contained code-animation engine for HyperFrames registry blocks.
//
// Reuses Shiki Magic Move's baked output (window.__TOKENS, baked at author time) and
// drives the animation as a PAUSED GSAP timeline the HyperFrames engine seeks per frame —
// NOT CSS transitions (those break under non-sequential frame seeking). FLIP measurement
// runs once inside document.fonts.ready (glyph metrics must be final). Each block sets
// window.__BLOCK = { id, effect, seq, line?, duration } and includes this engine; the
// engine builds one scene that fills the composition and registers window.__timelines[id].
(function () {
"use strict";
var T = window.__TOKENS || {};
function lineDiv() {
var d = document.createElement("div");
d.className = "line";
return d;
}
// Render a code state's keyed tokens into codeEl as lines of inline-block .tok spans.
function renderState(codeEl, state) {
codeEl.textContent = "";
var keyToEl = new Map();
var line = lineDiv();
codeEl.appendChild(line);
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
line = lineDiv();
codeEl.appendChild(line);
continue;
}
var span = document.createElement("span");
span.className = "tok";
span.textContent = t.content;
span.style.color = t.color;
if (t.fontStyle & 1) span.style.fontStyle = "italic";
if (t.fontStyle & 2) span.style.fontWeight = "700";
line.appendChild(span);
keyToEl.set(t.key, span);
}
var last = codeEl.lastChild;
if (last && last.className === "line" && last.children.length === 0)
codeEl.removeChild(last);
return keyToEl;
}
function rectsOf(keyToEl) {
var m = new Map();
keyToEl.forEach(function (el, k) {
var r = el.getBoundingClientRect();
m.set(k, { left: r.left, top: r.top });
});
return m;
}
function makeScene(surface, withGutter) {
var scene = document.createElement("div");
scene.className = "scene";
if (withGutter) {
var g = document.createElement("div");
g.className = "gutter";
scene.appendChild(g);
}
var code = document.createElement("div");
code.className = "code";
scene.appendChild(code);
surface.appendChild(scene);
return { scene: scene, code: code, gutter: scene.querySelector(".gutter") };
}
function fillGutter(g, n) {
var html = "";
for (var i = 1; i <= n; i++) html += '<span class="ln">' + i + "</span>";
g.innerHTML = html;
}
function splitLines(state) {
var lines = [{ text: "", tokens: [] }];
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
lines.push({ text: "", tokens: [] });
continue;
}
var cur = lines[lines.length - 1];
cur.tokens.push(t);
cur.text += t.content;
}
if (lines.length && lines[lines.length - 1].tokens.length === 0) lines.pop();
return lines;
}
function lineDiff(a, b) {
var n = a.length;
var m = b.length;
var dp = [];
for (var i = 0; i <= n; i++) dp.push(new Array(m + 1).fill(0));
for (var i = n - 1; i >= 0; i--) {
for (var j = m - 1; j >= 0; j--) {
dp[i][j] =
a[i].text === b[j].text
? dp[i + 1][j + 1] + 1
: Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
var ops = [];
var i = 0;
var j = 0;
while (i < n && j < m) {
if (a[i].text === b[j].text) {
ops.push({ type: "same", line: b[j] });
i++;
j++;
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
ops.push({ type: "del", line: a[i] });
i++;
} else {
ops.push({ type: "add", line: b[j] });
j++;
}
}
while (i < n) ops.push({ type: "del", line: a[i++] });
while (j < m) ops.push({ type: "add", line: b[j++] });
return ops;
}
// ---- single-scene effect builders (fade in, play, hold to end) -------------
var FADE = 0.45;
function highlight(tl, surface, state, start, targetIdx) {
var SWEEP = 0.9;
var parts = makeScene(surface, true);
var code = parts.code;
renderState(code, state);
var lines = code.querySelectorAll(".line");
fillGutter(parts.gutter, lines.length);
var base = code.getBoundingClientRect();
var idx = Math.max(0, Math.min(lines.length - 1, targetIdx));
var lr = lines[idx].getBoundingClientRect();
var box = document.createElement("div");
box.className = "hl-box";
box.style.left = lr.left - base.left - 8 + "px";
box.style.top = lr.top - base.top - 1 + "px";
box.style.height = lr.height + 2 + "px";
code.insertBefore(box, code.firstChild);
tl.set(parts.scene, { opacity: 0 }, 0);
tl.to(parts.scene, { opacity: 1, duration: FADE, ease: "power1.out" }, start);
var at = start + FADE + 0.2;
tl.fromTo(
box,
{ width: 0, opacity: 0 },
{ width: lr.width + 18, opacity: 1, duration: SWEEP, ease: "power2.inOut" },
at,
);
for (var i = 0; i < lines.length; i++) {
if (i === idx) continue;
tl.fromTo(
lines[i],
{ opacity: 1 },
{ opacity: 0.45, duration: SWEEP, ease: "power2.inOut" },
at,
);
}
}
function buildEffect(tl, surface, spec) {
var start = 0.45;
var seq = T[spec.seq];
if (spec.effect === "highlight")
highlight(tl, surface, seq.states[0], start, spec.line || 0);
}
function go() {
var spec = window.__BLOCK;
var surface = document.getElementById("surface");
var root = document.getElementById("root");
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// Build inside fonts.ready (glyph metrics must be final), then register.
// Register ONLY after the timeline is fully built: the runtime's
// sub-composition readiness gate treats "key present" as "ready" and
// nests the child once. An empty timeline registered before this build
// would be nested empty and never re-nested → blank render when used as
// a sub-composition. See the contract in engine frameCapture.ts.
document.fonts.ready.then(function () {
tl.from("#editor", { opacity: 0, scale: 0.985, duration: 0.5, ease: "power2.out" }, 0);
buildEffect(tl, surface, spec);
var dur = parseFloat(root.dataset.duration) || tl.duration();
tl.to({}, { duration: dur }, 0); // pad to full composition length
window.__timelines["code-highlight"] = tl; // register AFTER setup completes
if (typeof window.__hfForceTimelineRebind === "function") {
window.__hfForceTimelineRebind(); // re-nest now that we are populated
}
});
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
else go();
})();
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="code-highlight" data-composition-src="compositions/code-highlight.html" data-start="0" data-duration="5" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
File diff suppressed because it is too large Load Diff
+12 -489
View File
@@ -3,511 +3,34 @@ title: "Code Particle Assemble"
description: "Thousands of GPU points scatter through space and fly to the exact glyph pixels of the code, resolving into readable syntax-highlighted text — a particle system, not a token tween."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-particle-assemble.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Particle Assemble preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-particle-assemble.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-particle-assemble.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-particle-assemble" />
```bash Terminal
npx hyperframes add code-particle-assemble
```
That writes one file: `compositions/code-particle-assemble.html`.
## Source
## Add it to your video
<Accordion title={`code-particle-assemble.html`}>
It runs for 8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Code Particle Assemble</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #05070b;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
}
#gl {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
display: block;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="code-particle-assemble"
data-composition-src="compositions/code-particle-assemble.html"
data-start="0"
data-duration="8"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<canvas id="gl" width="1920" height="1080"></canvas>
</div>
<script>
window.__TOKENS = {
assemble: {
lang: "js",
theme: "github-dark",
bg: "#24292e",
fg: "#e1e4e8",
states: [
{
code: "const app = pipe(\n parse,\n optimize,\n emit,\n)",
tokens: [
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-0",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-1",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-2",
content: "app",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-3",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-4",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-5",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-6",
content: "pipe",
color: "#B392F0",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-7",
content: "(",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-8",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-9",
content: " ",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-10",
content: "parse,",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-11",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-12",
content: " ",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-13",
content: "optimize,",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-14",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-15",
content: " ",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-16",
content: "emit,",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-17",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-18",
content: ")",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-19",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
],
},
],
},
};
window.__BLOCK = {
id: "code-particle-assemble",
effect: "particles",
seq: "assemble",
duration: 8,
seed: 23,
};
</script>
<script>
// hf-code-anim-gl.js — WebGL code-animation engine for the "crazy" Code Animations blocks.
//
// Bridges the baked Shiki tokens (window.__TOKENS) into Three.js: the tokens are drawn once
// to an offscreen 2D canvas (a CanvasTexture), which then drives a lit 3D slab, a dissolve
// shader, or a GPU particle system. Determinism per the project contract:
// - render only via tl.eventCallback("onUpdate", …) + one initial render() — never rAF
// - all randomness from a seeded mulberry32 — never Math.random / Date.now
// - canvas drawn once after document.fonts.ready (final glyph metrics)
// - paused GSAP timeline registered on window.__timelines[id]; HF seeks it per frame
//
// Each block sets window.__BLOCK = { id, effect, seq, duration, seed } and includes this engine.
(function () {
"use strict";
var T = window.__TOKENS || {};
// ---- seeded PRNG (no Math.random) -----------------------------------------
function mulberry32(a) {
return function () {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// ---- split a token state into lines of {content,color} --------------------
function splitLines(state) {
var lines = [[]];
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
lines.push([]);
continue;
}
lines[lines.length - 1].push(t);
}
while (lines.length && lines[lines.length - 1].length === 0) lines.pop();
return lines;
}
// ---- THE BRIDGE: draw baked tokens to an offscreen 2D canvas ---------------
// Returns { canvas, W, H, lines, lineH, padY, fontPx } so effects can sample it.
function drawCodeToCanvas(state, opts) {
opts = opts || {};
var fontPx = opts.fontPx || 36;
var lineH = opts.lineH || 54;
var padX = opts.padX || 70;
var padY = opts.padY || 60;
var weight = opts.weight || 500;
var font = weight + " " + fontPx + 'px "JetBrains Mono", monospace';
var lines = splitLines(state);
var measure = document.createElement("canvas").getContext("2d");
measure.font = font;
var maxW = 0;
for (var i = 0; i < lines.length; i++) {
var w = 0;
for (var j = 0; j < lines[i].length; j++)
w += measure.measureText(lines[i][j].content).width;
if (w > maxW) maxW = w;
}
var W = Math.max(2, Math.ceil(maxW + padX * 2));
var H = Math.max(2, Math.ceil(lines.length * lineH + padY * 2));
var canvas = document.createElement("canvas");
canvas.width = W;
canvas.height = H;
var ctx = canvas.getContext("2d");
if (opts.transparent) {
ctx.clearRect(0, 0, W, H);
} else {
ctx.fillStyle = opts.bg || "#0b0f17";
ctx.fillRect(0, 0, W, H);
}
ctx.textBaseline = "top";
ctx.font = font;
for (var li = 0; li < lines.length; li++) {
var x = padX;
var y = padY + li * lineH;
for (var k = 0; k < lines[li].length; k++) {
var tok = lines[li][k];
ctx.fillStyle = tok.color;
ctx.fillText(tok.content, x, y);
x += ctx.measureText(tok.content).width;
}
}
return {
canvas: canvas,
W: W,
H: H,
lines: lines.length,
lineH: lineH,
padY: padY,
fontPx: fontPx,
};
}
function fxParticles(THREE, scene, camera, renderer, state, st, tl, dur, seed) {
scene.background = new THREE.Color(0x05070b);
var code = drawCodeToCanvas(state, {
fontPx: 46,
lineH: 70,
padX: 70,
padY: 64,
weight: 700,
});
var cw = code.W;
var ch = code.H;
var data = code.canvas.getContext("2d").getImageData(0, 0, cw, ch).data;
var rng = mulberry32(((seed || 1) * 2654435761) >>> 0);
var worldW = 13.0;
var worldH = worldW * (ch / cw);
// sample glyph pixels (brighter than the dark bg) on a fixed grid
var step = 2;
var tx = [];
var col = [];
for (var y = 0; y < ch; y += step) {
for (var x = 0; x < cw; x += step) {
var idx = (y * cw + x) * 4;
var r = data[idx];
var g = data[idx + 1];
var b = data[idx + 2];
if (r + g + b < 120) continue; // skip background
tx.push((x / cw - 0.5) * worldW, -(y / ch - 0.5) * worldH, 0);
col.push(r / 255, g / 255, b / 255);
}
}
var n = tx.length / 3;
var positions = new Float32Array(n * 3);
var aOrigin = new Float32Array(n * 3);
var aTarget = new Float32Array(tx);
var aColor = new Float32Array(col);
var aDelay = new Float32Array(n);
for (var i = 0; i < n; i++) {
var sx = (rng() * 2 - 1) * worldW * 1.15;
var sy = (rng() * 2 - 1) * worldH * 1.7;
var sz = (rng() * 2 - 1) * 9.0 - 2.0;
aOrigin[i * 3] = sx;
aOrigin[i * 3 + 1] = sy;
aOrigin[i * 3 + 2] = sz;
positions[i * 3] = sx;
positions[i * 3 + 1] = sy;
positions[i * 3 + 2] = sz;
aDelay[i] = rng();
}
var geo = new THREE.BufferGeometry();
geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geo.setAttribute("aOrigin", new THREE.BufferAttribute(aOrigin, 3));
geo.setAttribute("aTarget", new THREE.BufferAttribute(aTarget, 3));
geo.setAttribute("aColor", new THREE.BufferAttribute(aColor, 3));
geo.setAttribute("aDelay", new THREE.BufferAttribute(aDelay, 1));
var uniforms = { uProgress: { value: 0 }, uSize: { value: 3.4 } };
var mat = new THREE.ShaderMaterial({
uniforms: uniforms,
transparent: true,
depthTest: false,
depthWrite: false,
blending: THREE.NormalBlending,
vertexShader: [
"attribute vec3 aOrigin; attribute vec3 aTarget; attribute vec3 aColor; attribute float aDelay;",
"uniform float uProgress; uniform float uSize;",
"varying vec3 vColor; varying float vP;",
"void main(){",
" float span = 0.62;",
" float p = clamp((uProgress - aDelay*(1.0-span)) / span, 0.0, 1.0);",
" p = p*p*(3.0-2.0*p);",
" vec3 pos = mix(aOrigin, aTarget, p);",
" vec4 mv = modelViewMatrix * vec4(pos, 1.0);",
" gl_Position = projectionMatrix * mv;",
" gl_PointSize = uSize * (60.0 / -mv.z) * (0.45 + 0.55*p);",
" vColor = aColor; vP = p;",
"}",
].join("\n"),
fragmentShader: [
"precision highp float;",
"varying vec3 vColor; varying float vP;",
"void main(){",
" vec2 d = gl_PointCoord - 0.5;",
" float r2 = dot(d, d);",
" if (r2 > 0.25) discard;",
" float a = smoothstep(0.25, 0.02, r2);",
// brighter & whiter in flight, resolves to true color as it lands
" vec3 c = mix(vColor + vec3(0.32)*(1.0-vP), vColor, vP);",
" gl_FragColor = vec4(c, a * (0.7 + 0.3*vP));",
"}",
].join("\n"),
});
var pts = new THREE.Points(geo, mat);
scene.add(pts);
camera.position.set(0, 0, 10);
var vfov = (camera.fov * Math.PI) / 180;
var sa = 1920 / 1080;
var fitH = worldH / 2 / Math.tan(vfov / 2) / 0.78;
var fitW = worldW / 2 / (Math.tan(vfov / 2) * sa) / 0.78;
camera.position.z = Math.max(fitH, fitW);
camera.lookAt(0, 0, 0);
tl.to(uniforms.uProgress, { value: 1.0, duration: dur * 0.72, ease: "power2.out" }, 0);
// uProgress is tweened by GSAP; the boot loop's render() draws once per frame.
return function () {};
}
// ---- dispatch + boot ------------------------------------------------------
function buildEffect(THREE, scene, camera, renderer, spec, st, tl) {
var state = T[spec.seq].states[0];
var dur = spec.duration;
if (spec.effect === "particles")
return fxParticles(THREE, scene, camera, renderer, state, st, tl, dur, spec.seed);
return function () {};
}
// Free GPU resources (renderer, geometries, materials, textures) on page teardown.
// One-shot renders exit per job so nothing leaks there; this guards long-lived
// Studio/player sessions that reload. Fires only on unload — never during capture.
function disposeGL(renderer, scene) {
scene.traverse(function (o) {
if (o.geometry && o.geometry.dispose) o.geometry.dispose();
var mats = o.material ? (Array.isArray(o.material) ? o.material : [o.material]) : [];
mats.forEach(function (m) {
if (m.map && m.map.dispose) m.map.dispose();
if (
m.uniforms &&
m.uniforms.uTex &&
m.uniforms.uTex.value &&
m.uniforms.uTex.value.dispose
) {
m.uniforms.uTex.value.dispose();
}
if (m.dispose) m.dispose();
});
});
if (renderer.dispose) renderer.dispose();
}
function go() {
var spec = window.__BLOCK;
var root = document.getElementById("root");
var glcanvas = document.getElementById("gl");
var W = 1920;
var H = 1080;
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
window.__timelines[spec.id] = tl; // register synchronously
var THREE = window.THREE;
var renderer = new THREE.WebGLRenderer({
canvas: glcanvas,
antialias: true,
alpha: false,
preserveDrawingBuffer: true,
});
renderer.setPixelRatio(1);
renderer.setSize(W, H, false);
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 200);
var st = {};
document.fonts.ready.then(function () {
var renderFn = buildEffect(THREE, scene, camera, renderer, spec, st, tl);
function render() {
renderFn();
renderer.render(scene, camera);
}
tl.eventCallback("onUpdate", render);
var dur = parseFloat(root.dataset.duration) || tl.duration();
tl.to({}, { duration: dur }, 0); // pad to full composition length
render(); // paint frame 0
window.addEventListener("beforeunload", function () {
disposeGL(renderer, scene);
});
});
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
else go();
})();
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="code-particle-assemble" data-composition-src="compositions/code-particle-assemble.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
File diff suppressed because it is too large Load Diff
+12 -545
View File
@@ -3,567 +3,34 @@ title: "Code Shader Dissolve"
description: "The code compiles into existence: a GPU fragment shader resolves it out of seeded noise with a chromatic dissolve front and edge glow, then holds crisp."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-shader-dissolve.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Shader Dissolve preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-shader-dissolve.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-shader-dissolve.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-shader-dissolve" />
```bash Terminal
npx hyperframes add code-shader-dissolve
```
That writes one file: `compositions/code-shader-dissolve.html`.
## Source
## Add it to your video
<Accordion title={`code-shader-dissolve.html`}>
It runs for 7 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Code Shader Dissolve</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #05070b;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
}
#gl {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
display: block;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="code-shader-dissolve"
data-composition-src="compositions/code-shader-dissolve.html"
data-start="0"
data-duration="7"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<canvas id="gl" width="1920" height="1080"></canvas>
</div>
<script>
window.__TOKENS = {
compile: {
lang: "js",
theme: "github-dark",
bg: "#24292e",
fg: "#e1e4e8",
states: [
{
code: "function compile(src) {\n const ast = parse(src)\n const ir = optimize(ast)\n return emit(ir)\n}",
tokens: [
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-0",
content: "function",
color: "#F97583",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-1",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-2",
content: "compile",
color: "#B392F0",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-3",
content: "(",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-4",
content: "src",
color: "#FFAB70",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-5",
content: ") {",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-6",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-7",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-8",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-9",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-10",
content: "ast",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-11",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-12",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-13",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-14",
content: "parse",
color: "#B392F0",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-15",
content: "(src)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-16",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-17",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-18",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-19",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-20",
content: "ir",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-21",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-22",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-23",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-24",
content: "optimize",
color: "#B392F0",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-25",
content: "(ast)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-26",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-27",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-28",
content: "return",
color: "#F97583",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-29",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-30",
content: "emit",
color: "#B392F0",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-31",
content: "(ir)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-32",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-33",
content: "}",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-34",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
],
},
],
},
};
window.__BLOCK = {
id: "code-shader-dissolve",
effect: "dissolve",
seq: "compile",
duration: 7,
seed: 11,
};
</script>
<script>
// hf-code-anim-gl.js — WebGL code-animation engine for the "crazy" Code Animations blocks.
//
// Bridges the baked Shiki tokens (window.__TOKENS) into Three.js: the tokens are drawn once
// to an offscreen 2D canvas (a CanvasTexture), which then drives a lit 3D slab, a dissolve
// shader, or a GPU particle system. Determinism per the project contract:
// - render only via tl.eventCallback("onUpdate", …) + one initial render() — never rAF
// - all randomness from a seeded mulberry32 — never Math.random / Date.now
// - canvas drawn once after document.fonts.ready (final glyph metrics)
// - paused GSAP timeline registered on window.__timelines[id]; HF seeks it per frame
//
// Each block sets window.__BLOCK = { id, effect, seq, duration, seed } and includes this engine.
(function () {
"use strict";
var T = window.__TOKENS || {};
// ---- seeded PRNG (no Math.random) -----------------------------------------
function mulberry32(a) {
return function () {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// ---- split a token state into lines of {content,color} --------------------
function splitLines(state) {
var lines = [[]];
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
lines.push([]);
continue;
}
lines[lines.length - 1].push(t);
}
while (lines.length && lines[lines.length - 1].length === 0) lines.pop();
return lines;
}
// ---- THE BRIDGE: draw baked tokens to an offscreen 2D canvas ---------------
// Returns { canvas, W, H, lines, lineH, padY, fontPx } so effects can sample it.
function drawCodeToCanvas(state, opts) {
opts = opts || {};
var fontPx = opts.fontPx || 36;
var lineH = opts.lineH || 54;
var padX = opts.padX || 70;
var padY = opts.padY || 60;
var weight = opts.weight || 500;
var font = weight + " " + fontPx + 'px "JetBrains Mono", monospace';
var lines = splitLines(state);
var measure = document.createElement("canvas").getContext("2d");
measure.font = font;
var maxW = 0;
for (var i = 0; i < lines.length; i++) {
var w = 0;
for (var j = 0; j < lines[i].length; j++)
w += measure.measureText(lines[i][j].content).width;
if (w > maxW) maxW = w;
}
var W = Math.max(2, Math.ceil(maxW + padX * 2));
var H = Math.max(2, Math.ceil(lines.length * lineH + padY * 2));
var canvas = document.createElement("canvas");
canvas.width = W;
canvas.height = H;
var ctx = canvas.getContext("2d");
if (opts.transparent) {
ctx.clearRect(0, 0, W, H);
} else {
ctx.fillStyle = opts.bg || "#0b0f17";
ctx.fillRect(0, 0, W, H);
}
ctx.textBaseline = "top";
ctx.font = font;
for (var li = 0; li < lines.length; li++) {
var x = padX;
var y = padY + li * lineH;
for (var k = 0; k < lines[li].length; k++) {
var tok = lines[li][k];
ctx.fillStyle = tok.color;
ctx.fillText(tok.content, x, y);
x += ctx.measureText(tok.content).width;
}
}
return {
canvas: canvas,
W: W,
H: H,
lines: lines.length,
lineH: lineH,
padY: padY,
fontPx: fontPx,
};
}
function fxDissolve(THREE, scene, camera, renderer, state, st, tl, dur, seed) {
scene.background = new THREE.Color(0x05070b);
var code = drawCodeToCanvas(state, {
fontPx: 40,
lineH: 60,
padX: 90,
padY: 80,
weight: 600,
});
var tex = new THREE.CanvasTexture(code.canvas);
tex.minFilter = THREE.LinearFilter;
tex.magFilter = THREE.LinearFilter;
tex.generateMipmaps = false;
var aspect = code.W / code.H;
var planeH = 6.6;
var planeW = planeH * aspect;
var uniforms = {
uTex: { value: tex },
uProgress: { value: 0 },
uSeed: { value: (seed || 1) * 13.37 },
};
var mat = new THREE.ShaderMaterial({
uniforms: uniforms,
transparent: true,
vertexShader: [
"varying vec2 vUv;",
"void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }",
].join("\n"),
fragmentShader: [
"precision highp float;",
"uniform sampler2D uTex;",
"uniform float uProgress;",
"uniform float uSeed;",
"varying vec2 vUv;",
"float hash(vec2 p){ p=fract(p*vec2(123.34,456.21)); p+=dot(p,p+45.32); return fract(p.x*p.y); }",
"float noise(vec2 p){ vec2 i=floor(p),f=fract(p); float a=hash(i),b=hash(i+vec2(1.,0.)),c=hash(i+vec2(0.,1.)),d=hash(i+vec2(1.,1.)); vec2 u=f*f*(3.-2.*f); return mix(mix(a,b,u.x),mix(c,d,u.x),u.y); }",
"void main(){",
" vec2 uv = vUv;",
// multi-octave seeded noise field
" float n = noise(uv*16.0+uSeed)*0.6 + noise(uv*48.0+uSeed*1.7)*0.3 + noise(uv*120.0+uSeed*2.3)*0.1;",
" float edge = 0.12;",
" float reveal = smoothstep(n-edge, n+edge, uProgress);", // 0 hidden -> 1 shown
// chromatic fringe strongest at the dissolving edge
" float band = reveal*(1.0-reveal)*4.0;",
" float off = band*0.010;",
" float r = texture2D(uTex, uv+vec2(off,0.0)).r;",
" float g = texture2D(uTex, uv).g;",
" float b = texture2D(uTex, uv-vec2(off,0.0)).b;",
" vec3 codeCol = vec3(r,g,b);",
// additive cyan edge glow along the dissolve front
" vec3 glow = vec3(0.30,0.72,1.0) * band * 0.9;",
" vec3 outc = codeCol*reveal + glow;",
" float a = max(reveal, band*0.8);",
" gl_FragColor = vec4(outc, a);",
"}",
].join("\n"),
});
var plane = new THREE.Mesh(new THREE.PlaneGeometry(planeW, planeH), mat);
scene.add(plane);
camera.position.set(0, 0, 10);
var vfov = (camera.fov * Math.PI) / 180;
var screenAspect = 1920 / 1080;
var fitH = planeH / 2 / Math.tan(vfov / 2) / 0.82;
var fitW = planeW / 2 / (Math.tan(vfov / 2) * screenAspect) / 0.82;
camera.position.z = Math.max(fitH, fitW);
camera.lookAt(0, 0, 0);
// compile in, then hold crisp. Overshoot to 1.15 so even max-noise pixels
// (n up to ~1.0, edge 0.12) fully resolve — no leftover specks at rest.
tl.to(uniforms.uProgress, { value: 1.15, duration: dur * 0.62, ease: "power1.inOut" }, 0);
// uProgress is tweened by GSAP; the boot loop's render() draws once per frame.
return function () {};
}
// ---- dispatch + boot ------------------------------------------------------
function buildEffect(THREE, scene, camera, renderer, spec, st, tl) {
var state = T[spec.seq].states[0];
var dur = spec.duration;
if (spec.effect === "dissolve")
return fxDissolve(THREE, scene, camera, renderer, state, st, tl, dur, spec.seed);
return function () {};
}
// Free GPU resources (renderer, geometries, materials, textures) on page teardown.
// One-shot renders exit per job so nothing leaks there; this guards long-lived
// Studio/player sessions that reload. Fires only on unload — never during capture.
function disposeGL(renderer, scene) {
scene.traverse(function (o) {
if (o.geometry && o.geometry.dispose) o.geometry.dispose();
var mats = o.material ? (Array.isArray(o.material) ? o.material : [o.material]) : [];
mats.forEach(function (m) {
if (m.map && m.map.dispose) m.map.dispose();
if (
m.uniforms &&
m.uniforms.uTex &&
m.uniforms.uTex.value &&
m.uniforms.uTex.value.dispose
) {
m.uniforms.uTex.value.dispose();
}
if (m.dispose) m.dispose();
});
});
if (renderer.dispose) renderer.dispose();
}
function go() {
var spec = window.__BLOCK;
var root = document.getElementById("root");
var glcanvas = document.getElementById("gl");
var W = 1920;
var H = 1080;
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
window.__timelines[spec.id] = tl; // register synchronously
var THREE = window.THREE;
var renderer = new THREE.WebGLRenderer({
canvas: glcanvas,
antialias: true,
alpha: false,
preserveDrawingBuffer: true,
});
renderer.setPixelRatio(1);
renderer.setSize(W, H, false);
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 200);
var st = {};
document.fonts.ready.then(function () {
var renderFn = buildEffect(THREE, scene, camera, renderer, spec, st, tl);
function render() {
renderFn();
renderer.render(scene, camera);
}
tl.eventCallback("onUpdate", render);
var dur = parseFloat(root.dataset.duration) || tl.duration();
tl.to({}, { duration: dur }, 0); // pad to full composition length
render(); // paint frame 0
window.addEventListener("beforeunload", function () {
disposeGL(renderer, scene);
});
});
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
else go();
})();
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="code-shader-dissolve" data-composition-src="compositions/code-shader-dissolve.html" data-start="0" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,279 +3,21 @@ title: "Code Snippet - Apple Terminal Basic"
description: "Apple Terminal Basic profile: white background, black text, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-basic.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Basic preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-basic.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-basic" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-basic
```
That writes one file: `compositions/code-snippet-apple-terminal-basic.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-basic.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Basic</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #ececec;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e8e8e8 0%, #d8d8d8 100%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(0, 0, 0, 0.25),
0 10px 30px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: #ececec;
display: flex;
align-items: center;
padding: 0 14px;
gap: 0;
flex-shrink: 0;
border-bottom: 1px solid #d0d0d0;
position: relative;
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #333333;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 500;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: #ffffff;
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #000000;
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #000000;
}
.output-line.bold {
font-weight: bold;
color: #000000;
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: #000000;
font-weight: bold;
user-select: none;
}
.typed-command {
color: #000000;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #000000;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-basic"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">bash — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line">Last login: Mon Jun 2 09:14:22 on ttys001</span>
<span class="output-line">total 48</span>
<span class="output-line bold">drwxr-xr-x 8 user staff 256 Jun 2 09:10 .</span>
<span class="output-line">drwxr-xr-x 42 user staff 1344 Jun 1 18:22 ..</span>
<span class="output-line">-rw-r--r-- 1 user staff 872 May 30 14:05 README.md</span>
<span class="output-line bold">drwxr-xr-x 5 user staff 160 Jun 2 09:10 dist</span>
<span class="output-line bold"
>drwxr-xr-x 12 user staff 384 Jun 2 09:10 node_modules</span
>
<span class="output-line">-rw-r--r-- 1 user staff 1204 Jun 2 09:08 package.json</span>
<span class="output-line bold">drwxr-xr-x 6 user staff 192 Jun 2 09:10 src</span>
<span class="output-line">-rw-r--r-- 1 user staff 348 May 29 11:42 tsconfig.json</span>
</div>
<div class="input-line">
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "ls -la";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
// Phase 1 (00.5s): hold on empty prompt
tl.set(typedEl, { text: "" }, 0);
// Phase 2 (0.52s): type command char by char
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
// Phase 3 (2.2s): hide cursor, show first output line (login message)
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.set(outputLines[0], { opacity: 1 }, 2.3);
// Phase 4 (2.5s): clear command, show ls output line by line
tl.add(() => {
typedEl.textContent = "";
}, 2.5);
outputLines.forEach((line, i) => {
if (i === 0) return; // already shown
tl.set(line, { opacity: 1 }, 2.8 + i * 0.08);
});
// Phase 5 (4s): new prompt with blinking cursor
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.0);
// Phase 6 (4.210s): blink cursor 3 times then stop
const blinkTimes = [4.2, 4.6, 5.0, 5.4, 5.8, 6.2];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 6.6);
window.__timelines["code-snippet-apple-terminal-basic"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-basic"
data-composition-src="compositions/code-snippet-apple-terminal-basic.html"
@@ -287,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Basic profile — white background, black text, solid cursor
- macOS window chrome with traffic light buttons (close, minimize, fullscreen) and centered title bar
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Cursor blinks three times at the end then holds steady
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,274 +3,21 @@ title: "Code Snippet - Apple Terminal Clear Dark"
description: "Apple Terminal Clear Dark profile: semi-transparent dark background, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-clear-dark.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Clear Dark preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-clear-dark.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-clear-dark" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-clear-dark
```
That writes one file: `compositions/code-snippet-apple-terminal-clear-dark.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-clear-dark.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Clear Dark</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #1a1a1a;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a1a 0%, #111111 100%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(0, 0, 0, 0.7),
0 10px 30px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
padding: 0 14px;
flex-shrink: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
position: relative;
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 500;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: rgba(0, 0, 0, 0.7);
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #ffffff;
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #ffffff;
}
.output-line.bold {
font-weight: bold;
color: #ffffff;
}
.output-line.dim {
color: rgba(255, 255, 255, 0.6);
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: #888888;
font-weight: bold;
user-select: none;
}
.typed-command {
color: #ffffff;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #888888;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-clear-dark"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">bash — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys002</span>
<span class="output-line">Scanning dependencies...</span>
<span class="output-line">Found 3 vulnerabilities (1 moderate, 2 low)</span>
<span class="output-line dim"> package: lodash@4.17.20</span>
<span class="output-line dim"> severity: moderate</span>
<span class="output-line dim"> fix: lodash@4.17.21</span>
<span class="output-line dim"> package: minimist@1.2.5</span>
<span class="output-line dim"> severity: low</span>
<span class="output-line dim"> fix: minimist@1.2.6</span>
<span class="output-line bold">Run `npm audit fix` to fix them.</span>
</div>
<div class="input-line">
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "npm audit";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
tl.set(typedEl, { text: "" }, 0);
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.set(outputLines[0], { opacity: 1 }, 2.3);
tl.add(() => {
typedEl.textContent = "";
}, 2.5);
outputLines.forEach((line, i) => {
if (i === 0) return;
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
});
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.2);
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 6.8);
window.__timelines["code-snippet-apple-terminal-clear-dark"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-clear-dark"
data-composition-src="compositions/code-snippet-apple-terminal-clear-dark.html"
@@ -282,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Clear Dark profile — semi-transparent black background with white text
- macOS window chrome with traffic light buttons and centered title bar
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Cursor blinks three times at the end then holds steady
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,274 +3,21 @@ title: "Code Snippet - Apple Terminal Clear Light"
description: "Apple Terminal Clear Light profile: semi-transparent light background, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-clear-light.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Clear Light preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-clear-light.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-clear-light" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-clear-light
```
That writes one file: `compositions/code-snippet-apple-terminal-clear-light.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-clear-light.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Clear Light</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(0, 0, 0, 0.18),
0 10px 30px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: rgba(255, 255, 255, 0.4);
display: flex;
align-items: center;
padding: 0 14px;
flex-shrink: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
position: relative;
backdrop-filter: blur(10px);
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #333333;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 500;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: rgba(255, 255, 255, 0.7);
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #000000;
backdrop-filter: blur(8px);
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #000000;
}
.output-line.bold {
font-weight: bold;
}
.output-line.muted {
color: #666666;
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: #888888;
font-weight: bold;
user-select: none;
}
.typed-command {
color: #000000;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #888888;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-clear-light"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">bash — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line muted">Last login: Mon Jun 2 09:14:22 on ttys003</span>
<span class="output-line bold">git log --oneline -8</span>
<span class="output-line">a3f2c19 fix: correct off-by-one in pagination</span>
<span class="output-line">b7e8d04 feat: add dark mode toggle</span>
<span class="output-line">c1a9f55 refactor: extract auth helpers</span>
<span class="output-line">d4e2b31 test: add unit tests for parser</span>
<span class="output-line">e6c8a72 docs: update API reference</span>
<span class="output-line">f9b1d44 chore: bump dependencies</span>
<span class="output-line">2a7e890 feat: implement search endpoint</span>
<span class="output-line">3c5f012 init: project bootstrap</span>
</div>
<div class="input-line">
<span class="prompt">user@Mac myproject % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "git log --oneline -8";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
tl.set(typedEl, { text: "" }, 0);
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.set(outputLines[0], { opacity: 1 }, 2.3);
tl.add(() => {
typedEl.textContent = "";
}, 2.5);
outputLines.forEach((line, i) => {
if (i === 0) return;
tl.set(line, { opacity: 1 }, 2.8 + i * 0.09);
});
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac myproject % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.0);
const blinkTimes = [4.2, 4.6, 5.0, 5.4, 5.8, 6.2];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 6.6);
window.__timelines["code-snippet-apple-terminal-clear-light"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-clear-light"
data-composition-src="compositions/code-snippet-apple-terminal-clear-light.html"
@@ -282,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Clear Light profile — semi-transparent white background with black text
- macOS window chrome with traffic light buttons and centered title bar
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Cursor blinks three times at the end then holds steady
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,273 +3,21 @@ title: "Code Snippet - Apple Terminal Grass"
description: "Apple Terminal Grass profile: black background with green text, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-grass.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Grass preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-grass.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-grass" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-grass
```
That writes one file: `compositions/code-snippet-apple-terminal-grass.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-grass.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Grass</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000000;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #0a1a0a 0%, #000000 70%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(0, 193, 0, 0.15),
0 10px 30px rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: #1c1c1c;
display: flex;
align-items: center;
padding: 0 14px;
flex-shrink: 0;
border-bottom: 1px solid #00c100;
position: relative;
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #00c100;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 500;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: #000000;
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #00c100;
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #00c100;
}
.output-line.bold {
font-weight: bold;
color: #00c100;
}
.output-line.dim {
color: #006600;
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: #00c100;
font-weight: bold;
user-select: none;
}
.typed-command {
color: #00c100;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #00c100;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-grass"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">bash — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys004</span>
<span class="output-line">Running test suite...</span>
<span class="output-line dim"> ✓ should initialize with defaults (2ms)</span>
<span class="output-line dim"> ✓ should parse config correctly (4ms)</span>
<span class="output-line dim"> ✓ should handle empty input (1ms)</span>
<span class="output-line dim"> ✓ should reject invalid tokens (3ms)</span>
<span class="output-line dim"> ✓ should emit events on change (5ms)</span>
<span class="output-line dim"> ✓ should clean up on destroy (2ms)</span>
<span class="output-line bold"> 6 passing (18ms)</span>
</div>
<div class="input-line">
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "npm test";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
tl.set(typedEl, { text: "" }, 0);
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.set(outputLines[0], { opacity: 1 }, 2.3);
tl.add(() => {
typedEl.textContent = "";
}, 2.5);
outputLines.forEach((line, i) => {
if (i === 0) return;
tl.set(line, { opacity: 1 }, 2.8 + i * 0.12);
});
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.0);
const blinkTimes = [4.2, 4.6, 5.0, 5.4, 5.8, 6.2];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 6.6);
window.__timelines["code-snippet-apple-terminal-grass"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-grass"
data-composition-src="compositions/code-snippet-apple-terminal-grass.html"
@@ -281,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Grass profile — black background with #00C100 green text
- macOS window chrome with green-accented title bar and traffic light buttons
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Cursor blinks three times at the end then holds steady
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,278 +3,21 @@ title: "Code Snippet - Apple Terminal Homebrew"
description: "Apple Terminal Homebrew profile: black background with bright green text and lime cursor, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-homebrew.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Homebrew preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-homebrew.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-homebrew" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-homebrew
```
That writes one file: `compositions/code-snippet-apple-terminal-homebrew.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-homebrew.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Homebrew</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000000;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #0d1a0d 0%, #000000 70%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(0, 207, 0, 0.18),
0 10px 30px rgba(0, 0, 0, 0.85);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: #1c1c1c;
display: flex;
align-items: center;
padding: 0 14px;
flex-shrink: 0;
border-bottom: 1px solid #006900;
position: relative;
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #00cf00;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 500;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: #000000;
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #00cf00;
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #00cf00;
}
.output-line.bold {
font-weight: bold;
color: #00de00;
}
.output-line.dim {
color: #006600;
}
.output-line.bright {
color: #48f800;
font-weight: bold;
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: #48f800;
font-weight: bold;
user-select: none;
}
.typed-command {
color: #00cf00;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #48f800;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-homebrew"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">bash — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys005</span>
<span class="output-line">==> Updating Homebrew...</span>
<span class="output-line dim">Already up-to-date.</span>
<span class="output-line">==> Outdated formulae</span>
<span class="output-line">node 20.11.0 -> 22.3.0</span>
<span class="output-line">python 3.11.6 -> 3.12.4</span>
<span class="output-line">git 2.43.0 -> 2.45.2</span>
<span class="output-line">ffmpeg 6.1.1 -> 7.0.0</span>
<span class="output-line bright">==> Run `brew upgrade` to update 4 formulae.</span>
</div>
<div class="input-line">
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "brew outdated";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
tl.set(typedEl, { text: "" }, 0);
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.set(outputLines[0], { opacity: 1 }, 2.3);
tl.add(() => {
typedEl.textContent = "";
}, 2.5);
outputLines.forEach((line, i) => {
if (i === 0) return;
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
});
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.2);
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 6.8);
window.__timelines["code-snippet-apple-terminal-homebrew"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-homebrew"
data-composition-src="compositions/code-snippet-apple-terminal-homebrew.html"
@@ -286,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Homebrew profile — black background with #00CF00 text and #48F800 cursor
- macOS window chrome with traffic light buttons and centered title bar
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Cursor blinks three times at the end then holds steady
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,278 +3,21 @@ title: "Code Snippet - Apple Terminal Man Page"
description: "Apple Terminal Man Page profile: pale yellow background with black text, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-man-page.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Man Page preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-man-page.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-man-page" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-man-page
```
That writes one file: `compositions/code-snippet-apple-terminal-man-page.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-man-page.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Man Page</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #ede87a 0%, #ddd566 100%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(0, 0, 0, 0.2),
0 10px 30px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: #f0e680;
display: flex;
align-items: center;
padding: 0 14px;
flex-shrink: 0;
border-bottom: 1px solid #c8be40;
position: relative;
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #333333;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 500;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: #fef49c;
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #000000;
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #000000;
}
.output-line.bold {
font-weight: bold;
}
.output-line.indent {
padding-left: 24px;
color: #222222;
}
.output-line.section {
font-weight: bold;
margin-top: 2px;
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: #000000;
font-weight: bold;
user-select: none;
}
.typed-command {
color: #000000;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #000000;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-man-page"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">man — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line section">CURL(1) General Commands Manual CURL(1)</span>
<span class="output-line"></span>
<span class="output-line section">NAME</span>
<span class="output-line indent">curl -- transfer a URL</span>
<span class="output-line"></span>
<span class="output-line section">SYNOPSIS</span>
<span class="output-line indent">curl [options / URLs]</span>
<span class="output-line"></span>
<span class="output-line section">DESCRIPTION</span>
<span class="output-line indent"
>curl is a tool for transferring data from or to a server using URLs.</span
>
</div>
<div class="input-line">
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "man curl";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
tl.set(typedEl, { text: "" }, 0);
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.add(() => {
typedEl.textContent = "";
}, 2.4);
outputLines.forEach((line, i) => {
tl.set(line, { opacity: 1 }, 2.5 + i * 0.1);
});
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.0);
const blinkTimes = [4.2, 4.6, 5.0, 5.4, 5.8, 6.2];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 6.6);
window.__timelines["code-snippet-apple-terminal-man-page"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-man-page"
data-composition-src="compositions/code-snippet-apple-terminal-man-page.html"
@@ -286,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Man Page profile — pale yellow background with black text
- Shows a realistic man page output structure with sections and indented content
- macOS window chrome with traffic light buttons and centered title bar
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,274 +3,21 @@ title: "Code Snippet - Apple Terminal Novel"
description: "Apple Terminal Novel profile: warm parchment background with dark brown text, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-novel.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Novel preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-novel.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-novel" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-novel
```
That writes one file: `compositions/code-snippet-apple-terminal-novel.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-novel.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Novel</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #c9a86c 0%, #b8955a 100%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(62, 41, 0, 0.3),
0 10px 30px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: #c9a86c;
display: flex;
align-items: center;
padding: 0 14px;
flex-shrink: 0;
border-bottom: 1px solid #a88040;
position: relative;
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #3e2900;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 600;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: #dfc18a;
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #3e2900;
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #3e2900;
}
.output-line.bold {
font-weight: bold;
color: #3e2900;
}
.output-line.muted {
color: #7a5a20;
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: #3e2900;
font-weight: bold;
user-select: none;
}
.typed-command {
color: #3e2900;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #3e2900;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-novel"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">bash — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line muted">Last login: Mon Jun 2 09:14:22 on ttys006</span>
<span class="output-line muted">Python 3.12.4 (main, Jun 6 2024, 18:26:44)</span>
<span class="output-line muted"
>Type "help", "copyright", "credits" or "license" for more information.</span
>
<span class="output-line bold">>>> import hyperframes</span>
<span class="output-line bold">>>> hf = hyperframes.Composition("intro.html")</span>
<span class="output-line">>>> hf.duration</span>
<span class="output-line">12.0</span>
<span class="output-line">>>> hf.dimensions</span>
<span class="output-line">Dimensions(width=1920, height=1080)</span>
</div>
<div class="input-line">
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "python3";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
tl.set(typedEl, { text: "" }, 0);
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.set(outputLines[0], { opacity: 1 }, 2.3);
tl.add(() => {
typedEl.textContent = "";
}, 2.5);
outputLines.forEach((line, i) => {
if (i === 0) return;
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
});
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.2);
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 6.8);
window.__timelines["code-snippet-apple-terminal-novel"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-novel"
data-composition-src="compositions/code-snippet-apple-terminal-novel.html"
@@ -282,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Novel profile — warm parchment background with #3E2900 dark brown text
- macOS window chrome with traffic light buttons and centered title bar
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Cursor blinks three times at the end then holds steady
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,283 +3,21 @@ title: "Code Snippet - Apple Terminal Ocean"
description: "Apple Terminal Ocean profile: deep blue background with white text, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-ocean.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Ocean preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-ocean.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-ocean" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-ocean
```
That writes one file: `compositions/code-snippet-apple-terminal-ocean.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-ocean.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Ocean</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #1a3f9e;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #224fbe 0%, #0e2878 100%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(0, 0, 0, 0.6),
0 10px 30px rgba(34, 79, 190, 0.3);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: #1a3f9e;
display: flex;
align-items: center;
padding: 0 14px;
flex-shrink: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
position: relative;
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 500;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: #224fbe;
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #ffffff;
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #ffffff;
}
.output-line.bold {
font-weight: bold;
color: #ffffff;
}
.output-line.dim {
color: rgba(255, 255, 255, 0.65);
}
.output-line.success {
color: #7fffb3;
font-weight: bold;
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: rgba(255, 255, 255, 0.8);
font-weight: bold;
user-select: none;
}
.typed-command {
color: #ffffff;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #ffffff;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-ocean"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">bash — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys007</span>
<span class="output-line bold">docker ps</span>
<span class="output-line dim">CONTAINER ID IMAGE COMMAND CREATED</span>
<span class="output-line"
>a1b2c3d4e5f6 nginx:latest "/docker-entrypoint.…" 2 hours ago</span
>
<span class="output-line"
>b2c3d4e5f6a7 postgres:15 "docker-entrypoint.s…" 2 hours ago</span
>
<span class="output-line"
>c3d4e5f6a7b8 redis:alpine "docker-entrypoint.s…" 2 hours ago</span
>
<span class="output-line success">3 containers running</span>
<span class="output-line dim">Use 'docker logs CONTAINER' to view output</span>
</div>
<div class="input-line">
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "docker ps";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
tl.set(typedEl, { text: "" }, 0);
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.set(outputLines[0], { opacity: 1 }, 2.3);
tl.add(() => {
typedEl.textContent = "";
}, 2.5);
outputLines.forEach((line, i) => {
if (i === 0) return;
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
});
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.2);
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 6.8);
window.__timelines["code-snippet-apple-terminal-ocean"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-ocean"
data-composition-src="compositions/code-snippet-apple-terminal-ocean.html"
@@ -291,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Ocean profile — #224FBE blue background with white text
- macOS window chrome with traffic light buttons and centered title bar
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Cursor blinks three times at the end then holds steady
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,277 +3,21 @@ title: "Code Snippet - Apple Terminal Pro"
description: "Apple Terminal Pro profile: black background with grey text and lime green cursor, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-pro.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Pro preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-pro.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-pro" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-pro
```
That writes one file: `compositions/code-snippet-apple-terminal-pro.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-pro.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Pro</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000000;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #141414 0%, #000000 70%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(136, 255, 103, 0.08),
0 10px 30px rgba(0, 0, 0, 0.9);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: #1c1c1c;
display: flex;
align-items: center;
padding: 0 14px;
flex-shrink: 0;
border-bottom: 1px solid #333333;
position: relative;
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #bbbbbb;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 500;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: #000000;
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #bbbbbb;
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #bbbbbb;
}
.output-line.bold {
font-weight: bold;
color: #ffffff;
}
.output-line.accent {
color: #88ff67;
font-weight: bold;
}
.output-line.dim {
color: #666666;
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: #88ff67;
font-weight: bold;
user-select: none;
}
.typed-command {
color: #bbbbbb;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #88ff67;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-pro"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">bash — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys008</span>
<span class="output-line dim">Building production bundle...</span>
<span class="output-line"> vite v5.3.1 building for production...</span>
<span class="output-line"> ✓ 142 modules transformed.</span>
<span class="output-line"> dist/index.html 0.48 kB</span>
<span class="output-line"> dist/assets/index-Cv3rN.js 284.12 kB</span>
<span class="output-line"> dist/assets/index-Du4mP.css 42.08 kB</span>
<span class="output-line accent"> ✓ built in 2.31s</span>
</div>
<div class="input-line">
<span class="prompt">user@Mac myapp % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "npm run build";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
tl.set(typedEl, { text: "" }, 0);
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.set(outputLines[0], { opacity: 1 }, 2.3);
tl.add(() => {
typedEl.textContent = "";
}, 2.5);
outputLines.forEach((line, i) => {
if (i === 0) return;
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
});
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac myapp % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.2);
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 6.8);
window.__timelines["code-snippet-apple-terminal-pro"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-pro"
data-composition-src="compositions/code-snippet-apple-terminal-pro.html"
@@ -285,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Pro profile — black background, #BBBBBB grey text, #88FF67 lime cursor
- macOS window chrome with traffic light buttons and centered title bar
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Cursor blinks three times at the end then holds steady
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,274 +3,21 @@ title: "Code Snippet - Apple Terminal Red Sands"
description: "Apple Terminal Red Sands profile: deep red background with sandy text, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-red-sands.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Red Sands preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-red-sands.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-red-sands" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-red-sands
```
That writes one file: `compositions/code-snippet-apple-terminal-red-sands.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-red-sands.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Red Sands</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #7b140e;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #8a1a12 0%, #4a0a06 100%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(0, 0, 0, 0.7),
0 10px 30px rgba(123, 20, 14, 0.4);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: #5e0f0a;
display: flex;
align-items: center;
padding: 0 14px;
flex-shrink: 0;
border-bottom: 1px solid #8b2018;
position: relative;
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #ffe1b9;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 500;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: #7b140e;
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #ffe1b9;
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #ffe1b9;
}
.output-line.bold {
font-weight: bold;
color: #ffffff;
}
.output-line.dim {
color: #c4916a;
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: #ffe1b9;
font-weight: bold;
user-select: none;
}
.typed-command {
color: #ffe1b9;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #ffffff;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-red-sands"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">bash — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys009</span>
<span class="output-line">Fetching https://api.example.com/status</span>
<span class="output-line dim"> HTTP/2 200</span>
<span class="output-line dim"> content-type: application/json</span>
<span class="output-line dim"> x-request-id: a3f9c21e-04b2-4d78</span>
<span class="output-line">{</span>
<span class="output-line"> "status": "healthy",</span>
<span class="output-line"> "version": "2.4.1",</span>
<span class="output-line bold"> "uptime": "14d 6h 32m"</span>
<span class="output-line">}</span>
</div>
<div class="input-line">
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "curl -s api.example.com/status | jq";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
tl.set(typedEl, { text: "" }, 0);
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.set(outputLines[0], { opacity: 1 }, 2.3);
tl.add(() => {
typedEl.textContent = "";
}, 2.5);
outputLines.forEach((line, i) => {
if (i === 0) return;
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
});
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.4);
const blinkTimes = [4.6, 5.0, 5.4, 5.8, 6.2, 6.6];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 7.0);
window.__timelines["code-snippet-apple-terminal-red-sands"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-red-sands"
data-composition-src="compositions/code-snippet-apple-terminal-red-sands.html"
@@ -282,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Red Sands profile — #7B140E red background with #FFE1B9 sandy text
- macOS window chrome with traffic light buttons and centered title bar
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Cursor blinks three times at the end then holds steady
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,280 +3,21 @@ title: "Code Snippet - Apple Terminal Silver Aerogel"
description: "Apple Terminal Silver Aerogel profile: dark grey background with white text, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-silver-aerogel.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Silver Aerogel preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-silver-aerogel.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-silver-aerogel" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-silver-aerogel
```
That writes one file: `compositions/code-snippet-apple-terminal-silver-aerogel.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-silver-aerogel.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Silver Aerogel</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #2a2a2a;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #3a3a3a 0%, #1c1c1c 100%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(0, 0, 0, 0.6),
0 10px 30px rgba(232, 232, 232, 0.05);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: #2a2a2a;
display: flex;
align-items: center;
padding: 0 14px;
flex-shrink: 0;
border-bottom: 1px solid #444444;
position: relative;
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 500;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: #3d3d3d;
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #ffffff;
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #ffffff;
}
.output-line.bold {
font-weight: bold;
color: #ffffff;
}
.output-line.dim {
color: #aaaaaa;
}
.output-line.success {
color: #7fffb3;
}
.output-line.warn {
color: #ffd700;
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: #e8e8e8;
font-weight: bold;
user-select: none;
}
.typed-command {
color: #ffffff;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #e8e8e8;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-silver-aerogel"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">bash — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys010</span>
<span class="output-line">Checking system resources...</span>
<span class="output-line">Filesystem Size Used Avail Use%</span>
<span class="output-line">/dev/disk3s1s1 460G 142G 302G 32%</span>
<span class="output-line dim">/dev/disk3s6 460G 5.0G 302G 2%</span>
<span class="output-line">/dev/disk3s2 460G 11G 302G 4%</span>
<span class="output-line warn">Memory: 14.2 GB / 16.0 GB used (89%)</span>
<span class="output-line success">CPU: 8-core, 12% load average</span>
</div>
<div class="input-line">
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "df -h && vm_stat";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
tl.set(typedEl, { text: "" }, 0);
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.set(outputLines[0], { opacity: 1 }, 2.3);
tl.add(() => {
typedEl.textContent = "";
}, 2.5);
outputLines.forEach((line, i) => {
if (i === 0) return;
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
});
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.2);
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 6.8);
window.__timelines["code-snippet-apple-terminal-silver-aerogel"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-silver-aerogel"
data-composition-src="compositions/code-snippet-apple-terminal-silver-aerogel.html"
@@ -288,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Silver Aerogel profile — #3D3D3D dark grey background with white text
- macOS window chrome with traffic light buttons and centered title bar
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Cursor blinks three times at the end then holds steady
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,277 +3,21 @@ title: "Code Snippet - Apple Terminal Solid Colors"
description: "Apple Terminal Solid Colors profile: deep purple background with white text, per-character typing animation of a shell session."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-solid-colors.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Solid Colors preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-solid-colors.mp4" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-solid-colors" />
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-solid-colors
```
That writes one file: `compositions/code-snippet-apple-terminal-solid-colors.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-apple-terminal-solid-colors.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Apple Terminal Solid Colors</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #4c0099;
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
}
#scene {
width: 1920px;
height: 1080px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #5a00b3 0%, #2e0060 100%);
}
.terminal-window {
width: 1400px;
height: 700px;
border-radius: 10px;
overflow: hidden;
box-shadow:
0 30px 80px rgba(0, 0, 0, 0.7),
0 10px 30px rgba(76, 0, 153, 0.4);
display: flex;
flex-direction: column;
}
.titlebar {
height: 38px;
background: #3a0077;
display: flex;
align-items: center;
padding: 0 14px;
flex-shrink: 0;
border-bottom: 1px solid #6600bb;
position: relative;
}
.traffic-lights {
display: flex;
gap: 8px;
align-items: center;
}
.traffic-lights span {
width: 13px;
height: 13px;
border-radius: 50%;
display: inline-block;
}
.traffic-lights .close {
background: #ff5f57;
border: 1px solid #e0443e;
}
.traffic-lights .minimize {
background: #ffbd2e;
border: 1px solid #dfa123;
}
.traffic-lights .fullscreen {
background: #28c840;
border: 1px solid #1aab29;
}
.window-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
font-weight: 500;
letter-spacing: -0.1px;
}
.terminal-canvas {
flex: 1;
background: #4c0099;
padding: 18px 20px;
overflow: hidden;
font-size: 14px;
line-height: 1.6;
color: #ffffff;
}
.output-lines {
margin-bottom: 4px;
}
.output-line {
display: block;
white-space: pre;
opacity: 0;
color: #ffffff;
}
.output-line.bold {
font-weight: bold;
color: #ffffff;
}
.output-line.dim {
color: rgba(255, 255, 255, 0.55);
}
.output-line.accent {
color: #cc99ff;
font-weight: bold;
}
.input-line {
display: flex;
align-items: center;
white-space: pre;
}
.prompt {
color: #cc99ff;
font-weight: bold;
user-select: none;
}
.typed-command {
color: #ffffff;
}
.cursor-block {
display: inline-block;
width: 9px;
height: 16px;
background: #ffffff;
vertical-align: text-bottom;
margin-left: 1px;
}
</style>
</head>
<body>
<div
id="scene"
data-composition-id="code-snippet-apple-terminal-solid-colors"
data-width="1920"
data-height="1080"
>
<div class="terminal-window">
<div class="titlebar">
<div class="traffic-lights">
<span class="close"></span>
<span class="minimize"></span>
<span class="fullscreen"></span>
</div>
<span class="window-title">bash — 80×24</span>
</div>
<div class="terminal-canvas">
<div class="output-lines">
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys011</span>
<span class="output-line">Initializing HyperFrames project...</span>
<span class="output-line dim"> Creating project structure</span>
<span class="output-line dim"> Copying template files</span>
<span class="output-line dim"> Installing dependencies</span>
<span class="output-line"> added 142 packages in 3.4s</span>
<span class="output-line accent"> ✓ Project ready at ./my-video</span>
<span class="output-line bold"> Run: cd my-video && npx hyperframes preview</span>
</div>
<div class="input-line">
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
><span class="cursor-block"></span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const command = "npx hyperframes init my-video";
const typedEl = document.querySelector(".typed-command");
const cursorEl = document.querySelector(".cursor-block");
const outputLines = document.querySelectorAll(".output-line");
const tl = gsap.timeline({ paused: true });
tl.set(typedEl, { text: "" }, 0);
const charDuration = 1.5 / command.length;
command.split("").forEach((char, i) => {
tl.add(
() => {
typedEl.textContent = command.slice(0, i + 1);
},
0.5 + i * charDuration,
);
});
tl.set(cursorEl, { opacity: 0 }, 2.2);
tl.set(outputLines[0], { opacity: 1 }, 2.3);
tl.add(() => {
typedEl.textContent = "";
}, 2.5);
outputLines.forEach((line, i) => {
if (i === 0) return;
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
});
tl.add(() => {
const inputLine = document.querySelector(".input-line");
const newPrompt = document.createElement("div");
newPrompt.className = "input-line";
newPrompt.innerHTML =
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
inputLine.style.display = "none";
inputLine.parentNode.appendChild(newPrompt);
}, 4.2);
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
blinkTimes.forEach((t, i) => {
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
}, t);
});
tl.add(() => {
const fc = document.getElementById("final-cursor");
if (fc) fc.style.opacity = "1";
}, 6.8);
window.__timelines["code-snippet-apple-terminal-solid-colors"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
```html index.html
<div
data-composition-id="code-snippet-apple-terminal-solid-colors"
data-composition-src="compositions/code-snippet-apple-terminal-solid-colors.html"
@@ -285,14 +29,8 @@ After installing, add the block to your host composition:
></div>
```
## Features
- Faithful recreation of macOS Terminal.app Solid Colors profile — #4C0099 deep purple background with white text
- macOS window chrome with traffic light buttons and centered title bar
- Per-character GSAP typing animation — command types out character by character
- Output lines reveal sequentially after command executes
- Cursor blinks three times at the end then holds steady
- Self-contained HTML — no external assets, only GSAP CDN
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+12 -675
View File
@@ -3,697 +3,34 @@ title: "Code Snippet Flight"
description: "Discrete code snippets fly in from the side and assemble into a stacked program, staggered. Block-level FLIP."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-snippet-flight.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet Flight preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-flight.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-flight.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-snippet-flight" />
```bash Terminal
npx hyperframes add code-snippet-flight
```
That writes one file: `compositions/code-snippet-flight.html`.
## Source
## Add it to your video
<Accordion title={`code-snippet-flight.html`}>
It runs for 6 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Code Snippet Flight</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #05070b;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
font-family: "JetBrains Mono", monospace;
color: #e6edf3;
}
.bg-field {
position: absolute;
inset: 0;
background: radial-gradient(1200px 700px at 50% 18%, #0e1726 0%, #070b12 55%, #05070b 100%);
}
.bg-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(88, 166, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(88, 166, 255, 0.05) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: radial-gradient(1100px 620px at 50% 42%, #000 0%, transparent 78%);
}
.bg-glow {
position: absolute;
width: 900px;
height: 900px;
border-radius: 50%;
filter: blur(120px);
opacity: 0.5;
}
.bg-glow.a {
background: #1f6feb55;
left: -180px;
top: -200px;
}
.bg-glow.b {
background: #2ea04355;
right: -200px;
bottom: -260px;
}
.editor {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 1380px;
height: 800px;
background: #0b0f17;
border: 1px solid #1d2733;
border-radius: 16px;
box-shadow:
0 40px 120px rgba(0, 0, 0, 0.6),
0 2px 0 rgba(255, 255, 255, 0.03) inset;
overflow: hidden;
display: flex;
flex-direction: column;
}
.titlebar {
height: 56px;
flex: 0 0 56px;
display: flex;
align-items: center;
gap: 16px;
padding: 0 22px;
background: linear-gradient(#11161f, #0c111a);
border-bottom: 1px solid #1b2430;
}
.dots {
display: flex;
gap: 10px;
}
.dot {
width: 14px;
height: 14px;
border-radius: 50%;
}
.dot.r {
background: #ff5f57;
}
.dot.y {
background: #febc2e;
}
.dot.g {
background: #28c840;
}
.filename {
font-size: 19px;
color: #8b98a9;
letter-spacing: 0.2px;
}
.filename .accent {
color: #d6e2f0;
}
.code-surface {
position: relative;
flex: 1 1 auto;
overflow: hidden;
}
.scene {
position: absolute;
inset: 0;
opacity: 0;
will-change: opacity;
}
.code {
position: absolute;
top: 38px;
left: 0;
right: 0;
padding: 0 44px 0 104px;
font-size: 30px;
line-height: 46px;
font-variant-ligatures: none;
tab-size: 2;
}
.gutter {
position: absolute;
top: 38px;
left: 0;
width: 84px;
padding-right: 18px;
font-size: 30px;
line-height: 46px;
text-align: right;
color: #828c9b;
user-select: none;
}
.ln {
display: block;
height: 46px;
}
.line {
display: block;
min-height: 46px;
white-space: pre;
position: relative;
z-index: 1;
}
.tok {
display: inline-block;
white-space: pre;
will-change: transform, opacity;
}
.caret {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 32px;
margin-top: 6px;
background: #58a6ff;
border-radius: 1px;
z-index: 3;
}
.hl-box {
position: absolute;
background: rgba(88, 166, 255, 0.16);
border-left: 3px solid #58a6ff;
border-radius: 6px;
z-index: 0;
overflow: hidden;
}
.diff-line {
display: block;
min-height: 0;
line-height: 46px;
white-space: pre;
position: relative;
overflow: hidden;
padding-left: 14px;
}
.diff-sign {
display: inline-block;
width: 22px;
color: #41506220;
}
.diff-del {
background: rgba(248, 81, 73, 0.12);
box-shadow: inset 3px 0 #f85149;
}
.diff-add {
background: rgba(63, 185, 80, 0.12);
box-shadow: inset 3px 0 #3fb950;
}
.diff-del .diff-sign {
color: #f85149;
}
.diff-add .diff-sign {
color: #3fb950;
}
.flight-block {
position: absolute;
white-space: pre;
font-size: 27px;
line-height: 40px;
padding: 11px 22px;
border-radius: 11px;
background: #0f1722;
border: 1px solid #233044;
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.5);
will-change: transform, opacity;
}
.flight-target {
position: absolute;
border: 1.5px dashed #2a3a4f;
border-radius: 14px;
opacity: 0;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="code-snippet-flight"
data-composition-src="compositions/code-snippet-flight.html"
data-start="0"
data-duration="6"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<div class="bg-field" data-layout-ignore></div>
<div class="bg-grid" data-layout-ignore></div>
<div class="bg-glow a" data-layout-ignore></div>
<div class="bg-glow b" data-layout-ignore></div>
<div class="editor" id="editor" data-layout-ignore>
<div class="titlebar">
<div class="dots">
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
</div>
<span class="filename"
><span class="accent">pipeline.js</span> — Code Snippet Flight</span
>
</div>
<div class="code-surface" id="surface"></div>
</div>
</div>
<script>
window.__TOKENS = {
flight: {
lang: "js",
theme: "github-dark",
bg: "#24292e",
fg: "#e1e4e8",
states: [
{
code: "const x = parse(input)",
tokens: [
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-0",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-1",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-2",
content: "x",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-3",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-4",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-5",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-6",
content: "parse",
color: "#B392F0",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-7",
content: "(input)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-8",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
],
},
{
code: "const y = transform(x)",
tokens: [
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-0",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-1",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "ulDeJnFQa2Vy-61y5zTTA7sNfq7d4aUawNMAxQ6sVgg-2",
content: "y",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-3",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-4",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-5",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "ulDeJnFQa2Vy-61y5zTTA7sNfq7d4aUawNMAxQ6sVgg-6",
content: "transform",
color: "#B392F0",
fontStyle: 0,
},
{
key: "ulDeJnFQa2Vy-61y5zTTA7sNfq7d4aUawNMAxQ6sVgg-7",
content: "(x)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "ulDeJnFQa2Vy-61y5zTTA7sNfq7d4aUawNMAxQ6sVgg-8",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
],
},
{
code: "const z = render(y)",
tokens: [
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-0",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-1",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "wFuhzZNydG-3inU2s1h1gU8Kivya8CAP9gl9S4MBQj4-2",
content: "z",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-3",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-4",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-5",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "wFuhzZNydG-3inU2s1h1gU8Kivya8CAP9gl9S4MBQj4-6",
content: "render",
color: "#B392F0",
fontStyle: 0,
},
{
key: "wFuhzZNydG-3inU2s1h1gU8Kivya8CAP9gl9S4MBQj4-7",
content: "(y)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "wFuhzZNydG-3inU2s1h1gU8Kivya8CAP9gl9S4MBQj4-8",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
],
},
],
},
};
window.__BLOCK = { id: "code-snippet-flight", effect: "flight", seq: "flight", duration: 6 };
</script>
<script>
// hf-code-anim.js — self-contained code-animation engine for HyperFrames registry blocks.
//
// Reuses Shiki Magic Move's baked output (window.__TOKENS, baked at author time) and
// drives the animation as a PAUSED GSAP timeline the HyperFrames engine seeks per frame —
// NOT CSS transitions (those break under non-sequential frame seeking). FLIP measurement
// runs once inside document.fonts.ready (glyph metrics must be final). Each block sets
// window.__BLOCK = { id, effect, seq, line?, duration } and includes this engine; the
// engine builds one scene that fills the composition and registers window.__timelines[id].
(function () {
"use strict";
var T = window.__TOKENS || {};
function lineDiv() {
var d = document.createElement("div");
d.className = "line";
return d;
}
// Render a code state's keyed tokens into codeEl as lines of inline-block .tok spans.
function renderState(codeEl, state) {
codeEl.textContent = "";
var keyToEl = new Map();
var line = lineDiv();
codeEl.appendChild(line);
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
line = lineDiv();
codeEl.appendChild(line);
continue;
}
var span = document.createElement("span");
span.className = "tok";
span.textContent = t.content;
span.style.color = t.color;
if (t.fontStyle & 1) span.style.fontStyle = "italic";
if (t.fontStyle & 2) span.style.fontWeight = "700";
line.appendChild(span);
keyToEl.set(t.key, span);
}
var last = codeEl.lastChild;
if (last && last.className === "line" && last.children.length === 0)
codeEl.removeChild(last);
return keyToEl;
}
function rectsOf(keyToEl) {
var m = new Map();
keyToEl.forEach(function (el, k) {
var r = el.getBoundingClientRect();
m.set(k, { left: r.left, top: r.top });
});
return m;
}
function makeScene(surface, withGutter) {
var scene = document.createElement("div");
scene.className = "scene";
if (withGutter) {
var g = document.createElement("div");
g.className = "gutter";
scene.appendChild(g);
}
var code = document.createElement("div");
code.className = "code";
scene.appendChild(code);
surface.appendChild(scene);
return { scene: scene, code: code, gutter: scene.querySelector(".gutter") };
}
function fillGutter(g, n) {
var html = "";
for (var i = 1; i <= n; i++) html += '<span class="ln">' + i + "</span>";
g.innerHTML = html;
}
function splitLines(state) {
var lines = [{ text: "", tokens: [] }];
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
lines.push({ text: "", tokens: [] });
continue;
}
var cur = lines[lines.length - 1];
cur.tokens.push(t);
cur.text += t.content;
}
if (lines.length && lines[lines.length - 1].tokens.length === 0) lines.pop();
return lines;
}
function lineDiff(a, b) {
var n = a.length;
var m = b.length;
var dp = [];
for (var i = 0; i <= n; i++) dp.push(new Array(m + 1).fill(0));
for (var i = n - 1; i >= 0; i--) {
for (var j = m - 1; j >= 0; j--) {
dp[i][j] =
a[i].text === b[j].text
? dp[i + 1][j + 1] + 1
: Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
var ops = [];
var i = 0;
var j = 0;
while (i < n && j < m) {
if (a[i].text === b[j].text) {
ops.push({ type: "same", line: b[j] });
i++;
j++;
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
ops.push({ type: "del", line: a[i] });
i++;
} else {
ops.push({ type: "add", line: b[j] });
j++;
}
}
while (i < n) ops.push({ type: "del", line: a[i++] });
while (j < m) ops.push({ type: "add", line: b[j++] });
return ops;
}
// ---- single-scene effect builders (fade in, play, hold to end) -------------
var FADE = 0.45;
function flight(tl, surface, states, start) {
var FLY = 0.95;
var STAGGER = 0.26;
var GAP = 20;
var parts = makeScene(surface, false);
parts.code.remove();
var scene = parts.scene;
var target = document.createElement("div");
target.className = "flight-target";
scene.appendChild(target);
var W = surface.clientWidth;
var H = surface.clientHeight;
var blocks = states.map(function (st) {
var blk = document.createElement("div");
blk.className = "flight-block";
renderState(blk, st);
scene.appendChild(blk);
return blk;
});
var n = blocks.length;
// Pass 1: measure natural size, then make every chip the SAME width (symmetry).
var maxW = 0;
var chipH = 0;
blocks.forEach(function (blk) {
var r = blk.getBoundingClientRect();
if (r.width > maxW) maxW = r.width;
if (r.height > chipH) chipH = r.height;
});
blocks.forEach(function (blk) {
blk.style.width = maxW + "px";
});
// Pass 2: evenly-stacked, left-aligned, group centered in the surface.
var stackH = n * chipH + (n - 1) * GAP;
var destLeft = Math.round((W - maxW) / 2);
var topY = Math.round((H - stackH) / 2);
blocks.forEach(function (blk, i) {
var destY = topY + i * (chipH + GAP);
blk.style.left = destLeft + "px";
blk.style.top = destY + "px";
// fly straight in from the left to the shared left edge — clean, no skew
var srcX = destLeft - W * 0.32;
tl.fromTo(
blk,
{ x: srcX - destLeft, opacity: 0, scale: 0.96 },
{ x: 0, opacity: 1, scale: 1, duration: FLY, ease: "power3.out" },
start + FADE + i * STAGGER,
);
});
// Even-padded panel tightly wrapping the uniform stack.
var padX = 30;
var padY = 24;
target.style.left = destLeft - padX + "px";
target.style.top = topY - padY + "px";
target.style.width = maxW + padX * 2 + "px";
target.style.height = stackH + padY * 2 + "px";
var landAt = start + FADE + (n - 1) * STAGGER + FLY;
tl.set(scene, { opacity: 1 }, 0);
tl.to(target, { opacity: 1, duration: 0.5, ease: "power1.out" }, landAt - 0.5);
}
function buildEffect(tl, surface, spec) {
var start = 0.45;
var seq = T[spec.seq];
if (spec.effect === "flight") flight(tl, surface, seq.states, start);
}
function go() {
var spec = window.__BLOCK;
var surface = document.getElementById("surface");
var root = document.getElementById("root");
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
window.__timelines["code-snippet-flight"] = tl; // register synchronously
document.fonts.ready.then(function () {
tl.from("#editor", { opacity: 0, scale: 0.985, duration: 0.5, ease: "power2.out" }, 0);
buildEffect(tl, surface, spec);
var dur = parseFloat(root.dataset.duration) || tl.duration();
tl.to({}, { duration: dur }, 0); // pad to full composition length
});
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
else go();
})();
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="code-snippet-flight" data-composition-src="compositions/code-snippet-flight.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+12 -773
View File
@@ -3,795 +3,34 @@ title: "Code Typing"
description: "Token-streamed typing reveal with a caret that tracks the frontier — deterministic, no CSS animation."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/code-typing.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Typing preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-typing.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-typing.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add code-typing" />
```bash Terminal
npx hyperframes add code-typing
```
That writes one file: `compositions/code-typing.html`.
## Source
## Add it to your video
<Accordion title={`code-typing.html`}>
It runs for 5 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Code Typing</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #05070b;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
font-family: "JetBrains Mono", monospace;
color: #e6edf3;
}
.bg-field {
position: absolute;
inset: 0;
background: radial-gradient(1200px 700px at 50% 18%, #0e1726 0%, #070b12 55%, #05070b 100%);
}
.bg-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(88, 166, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(88, 166, 255, 0.05) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: radial-gradient(1100px 620px at 50% 42%, #000 0%, transparent 78%);
}
.bg-glow {
position: absolute;
width: 900px;
height: 900px;
border-radius: 50%;
filter: blur(120px);
opacity: 0.5;
}
.bg-glow.a {
background: #1f6feb55;
left: -180px;
top: -200px;
}
.bg-glow.b {
background: #2ea04355;
right: -200px;
bottom: -260px;
}
.editor {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 1380px;
height: 800px;
background: #0b0f17;
border: 1px solid #1d2733;
border-radius: 16px;
box-shadow:
0 40px 120px rgba(0, 0, 0, 0.6),
0 2px 0 rgba(255, 255, 255, 0.03) inset;
overflow: hidden;
display: flex;
flex-direction: column;
}
.titlebar {
height: 56px;
flex: 0 0 56px;
display: flex;
align-items: center;
gap: 16px;
padding: 0 22px;
background: linear-gradient(#11161f, #0c111a);
border-bottom: 1px solid #1b2430;
}
.dots {
display: flex;
gap: 10px;
}
.dot {
width: 14px;
height: 14px;
border-radius: 50%;
}
.dot.r {
background: #ff5f57;
}
.dot.y {
background: #febc2e;
}
.dot.g {
background: #28c840;
}
.filename {
font-size: 19px;
color: #8b98a9;
letter-spacing: 0.2px;
}
.filename .accent {
color: #d6e2f0;
}
.code-surface {
position: relative;
flex: 1 1 auto;
overflow: hidden;
}
.scene {
position: absolute;
inset: 0;
opacity: 0;
will-change: opacity;
}
.code {
position: absolute;
top: 38px;
left: 0;
right: 0;
padding: 0 44px 0 104px;
font-size: 30px;
line-height: 46px;
font-variant-ligatures: none;
tab-size: 2;
}
.gutter {
position: absolute;
top: 38px;
left: 0;
width: 84px;
padding-right: 18px;
font-size: 30px;
line-height: 46px;
text-align: right;
color: #828c9b;
user-select: none;
}
.ln {
display: block;
height: 46px;
}
.line {
display: block;
min-height: 46px;
white-space: pre;
position: relative;
z-index: 1;
}
.tok {
display: inline-block;
white-space: pre;
will-change: transform, opacity;
}
.caret {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 32px;
margin-top: 6px;
background: #58a6ff;
border-radius: 1px;
z-index: 3;
}
.hl-box {
position: absolute;
background: rgba(88, 166, 255, 0.16);
border-left: 3px solid #58a6ff;
border-radius: 6px;
z-index: 0;
overflow: hidden;
}
.diff-line {
display: block;
min-height: 0;
line-height: 46px;
white-space: pre;
position: relative;
overflow: hidden;
padding-left: 14px;
}
.diff-sign {
display: inline-block;
width: 22px;
color: #41506220;
}
.diff-del {
background: rgba(248, 81, 73, 0.12);
box-shadow: inset 3px 0 #f85149;
}
.diff-add {
background: rgba(63, 185, 80, 0.12);
box-shadow: inset 3px 0 #3fb950;
}
.diff-del .diff-sign {
color: #f85149;
}
.diff-add .diff-sign {
color: #3fb950;
}
.flight-block {
position: absolute;
white-space: pre;
font-size: 27px;
line-height: 40px;
padding: 11px 22px;
border-radius: 11px;
background: #0f1722;
border: 1px solid #233044;
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.5);
will-change: transform, opacity;
}
.flight-target {
position: absolute;
border: 1.5px dashed #2a3a4f;
border-radius: 14px;
opacity: 0;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="code-typing"
data-composition-src="compositions/code-typing.html"
data-start="0"
data-duration="5"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<div class="bg-field" data-layout-ignore></div>
<div class="bg-grid" data-layout-ignore></div>
<div class="bg-glow a" data-layout-ignore></div>
<div class="bg-glow b" data-layout-ignore></div>
<div class="editor" id="editor" data-layout-ignore>
<div class="titlebar">
<div class="dots">
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
</div>
<span class="filename"><span class="accent">loadConfig.js</span> — Code Typing</span>
</div>
<div class="code-surface" id="surface"></div>
</div>
</div>
<script>
window.__TOKENS = {
feature: {
lang: "js",
theme: "github-dark",
bg: "#24292e",
fg: "#e1e4e8",
states: [
{
code: 'async function loadConfig(path) {\n const raw = await readFile(path, "utf8")\n const config = JSON.parse(raw)\n return validate(config)\n}',
tokens: [
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-0",
content: "async",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-1",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-2",
content: "function",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-3",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-4",
content: "loadConfig",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-5",
content: "(",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-6",
content: "path",
color: "#FFAB70",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-7",
content: ") {",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-8",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-9",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-10",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-11",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-12",
content: "raw",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-13",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-14",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-15",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-16",
content: "await",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-17",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-18",
content: "readFile",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-19",
content: "(path,",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-20",
content: " ",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-21",
content: '"utf8"',
color: "#9ECBFF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-22",
content: ")",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-23",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-24",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-25",
content: "const",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-26",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-27",
content: "config",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-28",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-29",
content: "=",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-30",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-31",
content: "JSON",
color: "#79B8FF",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-32",
content: ".",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-33",
content: "parse",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-34",
content: "(raw)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-35",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-36",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-37",
content: "return",
color: "#F97583",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-38",
content: " ",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-39",
content: "validate",
color: "#B392F0",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-40",
content: "(config)",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-41",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-42",
content: "}",
color: "#E1E4E8",
fontStyle: 0,
},
{
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-43",
content: "\n",
color: "#e1e4e8",
fontStyle: 0,
},
],
},
],
},
};
window.__BLOCK = { id: "code-typing", effect: "typing", seq: "feature", duration: 5 };
</script>
<script>
// hf-code-anim.js — self-contained code-animation engine for HyperFrames registry blocks.
//
// Reuses Shiki Magic Move's baked output (window.__TOKENS, baked at author time) and
// drives the animation as a PAUSED GSAP timeline the HyperFrames engine seeks per frame —
// NOT CSS transitions (those break under non-sequential frame seeking). FLIP measurement
// runs once inside document.fonts.ready (glyph metrics must be final). Each block sets
// window.__BLOCK = { id, effect, seq, line?, duration } and includes this engine; the
// engine builds one scene that fills the composition and registers window.__timelines[id].
(function () {
"use strict";
var T = window.__TOKENS || {};
function lineDiv() {
var d = document.createElement("div");
d.className = "line";
return d;
}
// Render a code state's keyed tokens into codeEl as lines of inline-block .tok spans.
function renderState(codeEl, state) {
codeEl.textContent = "";
var keyToEl = new Map();
var line = lineDiv();
codeEl.appendChild(line);
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
line = lineDiv();
codeEl.appendChild(line);
continue;
}
var span = document.createElement("span");
span.className = "tok";
span.textContent = t.content;
span.style.color = t.color;
if (t.fontStyle & 1) span.style.fontStyle = "italic";
if (t.fontStyle & 2) span.style.fontWeight = "700";
line.appendChild(span);
keyToEl.set(t.key, span);
}
var last = codeEl.lastChild;
if (last && last.className === "line" && last.children.length === 0)
codeEl.removeChild(last);
return keyToEl;
}
function rectsOf(keyToEl) {
var m = new Map();
keyToEl.forEach(function (el, k) {
var r = el.getBoundingClientRect();
m.set(k, { left: r.left, top: r.top });
});
return m;
}
function makeScene(surface, withGutter) {
var scene = document.createElement("div");
scene.className = "scene";
if (withGutter) {
var g = document.createElement("div");
g.className = "gutter";
scene.appendChild(g);
}
var code = document.createElement("div");
code.className = "code";
scene.appendChild(code);
surface.appendChild(scene);
return { scene: scene, code: code, gutter: scene.querySelector(".gutter") };
}
function fillGutter(g, n) {
var html = "";
for (var i = 1; i <= n; i++) html += '<span class="ln">' + i + "</span>";
g.innerHTML = html;
}
function splitLines(state) {
var lines = [{ text: "", tokens: [] }];
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
lines.push({ text: "", tokens: [] });
continue;
}
var cur = lines[lines.length - 1];
cur.tokens.push(t);
cur.text += t.content;
}
if (lines.length && lines[lines.length - 1].tokens.length === 0) lines.pop();
return lines;
}
function lineDiff(a, b) {
var n = a.length;
var m = b.length;
var dp = [];
for (var i = 0; i <= n; i++) dp.push(new Array(m + 1).fill(0));
for (var i = n - 1; i >= 0; i--) {
for (var j = m - 1; j >= 0; j--) {
dp[i][j] =
a[i].text === b[j].text
? dp[i + 1][j + 1] + 1
: Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
var ops = [];
var i = 0;
var j = 0;
while (i < n && j < m) {
if (a[i].text === b[j].text) {
ops.push({ type: "same", line: b[j] });
i++;
j++;
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
ops.push({ type: "del", line: a[i] });
i++;
} else {
ops.push({ type: "add", line: b[j] });
j++;
}
}
while (i < n) ops.push({ type: "del", line: a[i++] });
while (j < m) ops.push({ type: "add", line: b[j++] });
return ops;
}
// ---- single-scene effect builders (fade in, play, hold to end) -------------
var FADE = 0.45;
function typing(tl, surface, state, start) {
var PER = 0.028; // seconds per character — smooth, fast typewriter
var parts = makeScene(surface, true);
var code = parts.code;
// Per-CHARACTER render (granular reveal reads smoother than per-token pops).
code.textContent = "";
var line = document.createElement("div");
line.className = "line";
code.appendChild(line);
var chars = [];
for (var i = 0; i < state.tokens.length; i++) {
var t = state.tokens[i];
if (t.content === "\n") {
line = document.createElement("div");
line.className = "line";
code.appendChild(line);
continue;
}
for (var c = 0; c < t.content.length; c++) {
var span = document.createElement("span");
span.className = "tok";
span.textContent = t.content[c];
span.style.color = t.color;
if (t.fontStyle & 1) span.style.fontStyle = "italic";
if (t.fontStyle & 2) span.style.fontWeight = "700";
line.appendChild(span);
chars.push({ el: span, ws: t.content[c] === " " });
}
}
var last = code.lastChild;
if (last && last.className === "line" && last.children.length === 0)
code.removeChild(last);
fillGutter(parts.gutter, code.querySelectorAll(".line").length);
var base = code.getBoundingClientRect();
var caret = document.createElement("div");
caret.className = "caret";
code.appendChild(caret);
tl.set(parts.scene, { opacity: 0 }, 0);
tl.to(parts.scene, { opacity: 1, duration: FADE, ease: "power1.out" }, start);
var t0 = start + FADE;
if (chars.length) {
var r0 = chars[0].el.getBoundingClientRect();
tl.set(caret, { x: r0.left - base.left, y: r0.top - base.top, opacity: 1 }, 0);
}
chars.forEach(function (c, i) {
var at = t0 + i * PER;
tl.set(c.el, { opacity: 0 }, 0);
// soft per-char fade (overlaps neighbors → smooth wave); whitespace appears instantly
tl.to(c.el, { opacity: 1, duration: c.ws ? 0.01 : 0.12, ease: "power1.out" }, at);
// caret glides at constant speed to each char's right edge (linear = no stop-start jitter)
var r = c.el.getBoundingClientRect();
tl.to(
caret,
{ x: r.right - base.left, y: r.top - base.top, duration: PER, ease: "none" },
at,
);
});
}
function buildEffect(tl, surface, spec) {
var start = 0.45;
var seq = T[spec.seq];
if (spec.effect === "typing") typing(tl, surface, seq.states[0], start);
}
function go() {
var spec = window.__BLOCK;
var surface = document.getElementById("surface");
var root = document.getElementById("root");
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// Build inside fonts.ready (glyph metrics must be final), then register.
// Register ONLY after the timeline is fully built: the runtime's
// sub-composition readiness gate treats "key present" as "ready" and
// nests the child once. An empty timeline registered before this build
// would be nested empty and never re-nested → blank render when used as
// a sub-composition. See the contract in engine frameCapture.ts.
document.fonts.ready.then(function () {
tl.from("#editor", { opacity: 0, scale: 0.985, duration: 0.5, ease: "power2.out" }, 0);
buildEffect(tl, surface, spec);
var dur = parseFloat(root.dataset.duration) || tl.duration();
tl.to({}, { duration: dur }, 0); // pad to full composition length
window.__timelines["code-typing"] = tl; // register AFTER setup completes
if (typeof window.__hfForceTimelineRebind === "function") {
window.__hfForceTimelineRebind(); // re-nest now that we are populated
}
});
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
else go();
})();
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="code-typing" data-composition-src="compositions/code-typing.html" data-start="0" data-duration="5" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
-499
View File
@@ -1,499 +0,0 @@
---
title: "Cosmic Orb"
description: "An iridescent soap-bubble planet: a slowly spinning sphere of nebula bands, dust lanes and twinkling stars wrapped in a refracting glass limb with chromatic aberration at the edge."
---
import { InstallCommand } from "/snippets/install-command.jsx";
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
<VariablesExplorer
previewSrc="/public/catalog/preview/blocks/cosmic-orb.html"
compositionId="cosmic-orb"
compositionSrc="compositions/cosmic-orb.html"
variables={[{"id":"hue","type":"number","label":"Nebula hue","default":268,"min":0,"max":360,"step":1,"unit":"deg"},{"id":"accent","type":"number","label":"Accent hue","default":196,"min":0,"max":360,"step":1,"unit":"deg"},{"id":"spin","type":"number","label":"Spin rate","default":0.035,"min":0,"max":0.5,"step":0.005,"unit":"turns/s"},{"id":"stars","type":"number","label":"Star density","default":1,"min":0,"max":2,"step":0.05},{"id":"glow","type":"number","label":"Glow intensity","default":1,"min":0,"max":2,"step":0.05},{"id":"size","type":"number","label":"Orb size","default":0.86,"min":0.2,"max":1.2,"step":0.01},{"id":"pulse","type":"number","label":"Beat pulse","default":0,"min":0,"max":1,"step":0.01},{"id":"pulseEnvelope","type":"string","label":"Beat pulse envelope (comma-separated 0-1 samples)","default":"","placeholder":"0,0.2,1,0.4,0"},{"id":"backdrop","type":"color","label":"Backdrop","default":"#04040a"}]}
>
```html cosmic-orb.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Cosmic Orb</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
overflow: hidden;
}
#co-root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
#co-backdrop {
position: absolute;
inset: 0;
background: #04040a;
}
#co-canvas {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
}
</style>
</head>
<body>
<div
id="co-root"
data-composition-id="cosmic-orb"
data-root="true"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="10"
data-composition-variables='[
{"id":"hue","type":"number","label":"Nebula hue","default":268,"min":0,"max":360,"step":1,"unit":"deg"},
{"id":"accent","type":"number","label":"Accent hue","default":196,"min":0,"max":360,"step":1,"unit":"deg"},
{"id":"spin","type":"number","label":"Spin rate","default":0.035,"min":0,"max":0.5,"step":0.005,"unit":"turns/s"},
{"id":"stars","type":"number","label":"Star density","default":1,"min":0,"max":2,"step":0.05},
{"id":"glow","type":"number","label":"Glow intensity","default":1,"min":0,"max":2,"step":0.05},
{"id":"size","type":"number","label":"Orb size","default":0.86,"min":0.2,"max":1.2,"step":0.01},
{"id":"pulse","type":"number","label":"Beat pulse","default":0,"min":0,"max":1,"step":0.01},
{"id":"pulseEnvelope","type":"string","label":"Beat pulse envelope (comma-separated 0-1 samples)","default":"","placeholder":"0,0.2,1,0.4,0"},
{"id":"backdrop","type":"color","label":"Backdrop","default":"#04040a"}
]'
>
<div id="co-backdrop"></div>
<canvas id="co-canvas" width="1920" height="1080"></canvas>
<!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->
<div
id="co-drv"
class="clip"
data-start="0"
data-duration="10"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
var DUR = 10;
var W = 1920;
var H = 1080;
var ROOT = document.getElementById("co-root");
var CS = getComputedStyle(ROOT);
// `data-composition-variables` is the single owner of every declared
// default — parse it rather than repeating each default a second time
// in JS. `window.__hyperframes` is NOT guaranteed to exist yet when this
// inline script runs, so it can only ever be an override, never the
// source. Precedence: host CSS custom property, runtime variable bag,
// declared default.
var DECL = {};
JSON.parse(ROOT.getAttribute("data-composition-variables") || "[]").forEach(function (v) {
DECL[v.id] = v.default;
});
var HF = window.__hyperframes;
var V = (HF && HF.getVariables && HF.getVariables()) || {};
function raw(id) {
var css = CS.getPropertyValue("--" + id.toLowerCase()).trim();
if (css !== "") return css;
if (V[id] !== undefined && V[id] !== "") return V[id];
return DECL[id];
}
function num(id) {
var n = parseFloat(raw(id));
return isFinite(n) ? n : 0;
}
function str(id) {
var s = raw(id);
return typeof s === "string" ? s.trim() : "";
}
var HUE = num("hue");
var ACCENT = num("accent");
var SPIN = num("spin");
var STARS = num("stars");
var GLOW = num("glow");
var SIZE = num("size");
var PULSE = num("pulse");
var BACKDROP = str("backdrop");
document.getElementById("co-backdrop").style.background = BACKDROP;
// Beat hook. A precomputed envelope (comma-separated 0-1 samples spread
// evenly across the clip) is sampled closed-form at time t — no audio
// analysis, no accumulator. Empty envelope => the flat `pulse` value.
var ENV = str("pulseEnvelope")
.split(",")
.map(function (s) {
return parseFloat(s);
})
.filter(function (n) {
return isFinite(n);
});
function pulseAt(t) {
if (ENV.length === 0) return PULSE;
if (ENV.length === 1) return ENV[0];
var x = Math.min(1, Math.max(0, t / DUR)) * (ENV.length - 1);
var i = Math.floor(x);
var j = Math.min(i + 1, ENV.length - 1);
return ENV[i] + (ENV[j] - ENV[i]) * (x - i);
}
var canvas = document.getElementById("co-canvas");
var gl =
canvas.getContext("webgl", {
alpha: true,
antialias: false,
depth: false,
stencil: false,
preserveDrawingBuffer: true,
powerPreference: "high-performance",
}) ||
canvas.getContext("experimental-webgl", {
alpha: true,
preserveDrawingBuffer: true,
});
var VERT = [
"attribute vec2 aPos;",
"void main() { gl_Position = vec4(aPos, 0.0, 1.0); }",
].join("\n");
var FRAG = [
"precision highp float;",
"uniform vec2 uRes;",
"uniform float uTime;",
"uniform float uRadius;",
"uniform float uSpin;",
"uniform float uHue;",
"uniform float uAccent;",
"uniform float uStars;",
"uniform float uGlow;",
"uniform float uPulse;",
"const float TAU = 6.28318531;",
"",
"// One cheap hash for every random-looking thing in here.",
"float h1(float x) { return fract(sin(x * 127.1) * 43758.5453); }",
"float h2(vec2 p) { return h1(dot(p, vec2(1.0, 157.31))); }",
"",
"vec3 hue2rgb(float h) {",
" return clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);",
"}",
"",
"vec3 rotX(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(p.x, c * p.y - s * p.z, s * p.y + c * p.z); }",
"vec3 rotY(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(c * p.x + s * p.z, p.y, -s * p.x + c * p.z); }",
"vec3 rotZ(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(c * p.x - s * p.y, s * p.x + c * p.y, p.z); }",
"",
"vec3 safeDir(vec3 v) {",
" float l = dot(v, v);",
" if (l < 1e-6) return vec3(0.0, 0.0, -1.0);",
" return v * inversesqrt(l);",
"}",
"",
"// Fixed axial tilt, then spin about the tilted polar axis.",
"vec3 toBody(vec3 n, float spin) { return rotY(rotZ(rotX(n, -0.34), 0.42), -spin); }",
"vec2 lonlat(vec3 d) { return vec2(atan(d.x, d.z), asin(clamp(d.y, -1.0, 1.0))); }",
"",
"float dens(float base) { return clamp(1.0 - (1.0 - base) * uStars, 0.0, 1.0); }",
"",
"// One star size-class: cell grid, seeded jitter, soft round core.",
"float starField(vec2 q, float freq, float base, float rad) {",
" vec2 g = q * freq;",
" vec2 id = floor(g);",
" float h = h2(id);",
" if (h < dens(base)) return 0.0;",
" vec2 c = vec2(h2(id + vec2(3.7, 1.3)), h2(id + vec2(9.1, 5.5)));",
" float d = length(fract(g) - c);",
" float core = smoothstep(rad, 0.0, d);",
" // Twinkle: a per-star phase driven straight off uTime, no accumulator.",
" float tw = 0.62 + 0.38 * sin(uTime * 2.4 + h * 53.7);",
" return core * core * (0.45 + 0.55 * h1(h * 91.7)) * tw;",
"}",
"",
"// 4-point diffraction glare, only worth paying for on the brightest class.",
"float starGlare(vec2 q, float freq, float base) {",
" vec2 g = q * freq;",
" vec2 id = floor(g);",
" float h = h2(id);",
" if (h < dens(base)) return 0.0;",
" vec2 c = vec2(h2(id + vec2(3.7, 1.3)), h2(id + vec2(9.1, 5.5)));",
" vec2 dv = abs(fract(g) - c);",
" float hx = exp(-dv.x * 70.0) * exp(-dv.y * 9.0);",
" float hy = exp(-dv.y * 70.0) * exp(-dv.x * 9.0);",
" return max(hx, hy) * 0.45;",
"}",
"",
"// Three size-classes at ~6 / 11 / 19 cell frequency.",
"float starsAll(vec2 q, float gain) {",
" float s = starField(q, 6.0, 0.958, 0.20);",
" s += starGlare(q, 6.0, 0.958);",
" s += starField(q, 11.0, 0.920, 0.18) * 0.95;",
" s += starField(q, 19.0, 0.865, 0.17) * 0.75;",
" return s * gain;",
"}",
"",
"vec3 nebula(vec2 q) {",
" // Two sin-field turbulence layers.",
" float t1 = sin(q.x * 3.1 + sin(q.y * 2.7) * 1.9) * sin(q.y * 4.3 + sin(q.x * 1.7) * 2.3);",
" float t2 = sin(q.x * 7.9 + sin(q.y * 5.1) * 1.3) * sin(q.y * 9.7 + sin(q.x * 4.4) * 1.1);",
" float turb = t1 * 0.66 + t2 * 0.34;",
"",
" // Galaxy bands, warped by the turbulence.",
" float band = sin(q.y * 3.2 + turb * 1.7 + q.x * 0.55);",
" float bands = pow(max(0.0, band * 0.5 + 0.5), 3.2);",
"",
" // Dust lanes: broad darkened swathes, not a thin curve.",
" float lane = sin(q.y * 2.9 + q.x * 1.3 + t1 * 1.6);",
" float dust = 0.22 + 0.78 * smoothstep(-0.45, 0.75, lane);",
"",
" // Core bulge.",
" float dc = length(vec2((q.x - 0.55) * 0.5, (q.y + 0.12) * 1.1));",
" float core = exp(-dc * dc * 2.6);",
"",
" // Two pocket glows.",
" float dA = length(vec2((q.x + 1.95) * 0.62, (q.y - 0.52) * 1.45));",
" float dB = length(vec2((q.x - 2.35) * 0.55, (q.y + 0.78) * 1.30));",
" float pA = exp(-dA * dA * 4.0);",
" float pB = exp(-dB * dB * 5.0);",
"",
" vec3 cNeb = hue2rgb(uHue / 360.0);",
" vec3 cAcc = hue2rgb(uAccent / 360.0);",
" vec3 cWarm = hue2rgb(uHue / 360.0 + 0.07);",
"",
" vec3 col = mix(cNeb, cWarm, 0.5 + 0.5 * turb) * bands * dust * 0.17;",
" col += mix(cNeb, vec3(1.0), 0.50) * core * dust * 0.13;",
" col += mix(cAcc, vec3(1.0), 0.60) * pA * 0.30 * uGlow;",
" col += mix(cNeb, vec3(1.0), 0.50) * pB * 0.24 * uGlow;",
" return col;",
"}",
"",
"void main() {",
" vec2 p = (gl_FragCoord.xy - uRes * 0.5) / uRadius;",
" float r = length(p);",
" float aa = 1.4 / uRadius;",
" float spin = TAU * uSpin * uTime;",
"",
" vec3 cNeb = hue2rgb(uHue / 360.0);",
" vec3 cAcc = hue2rgb(uAccent / 360.0);",
"",
" float mask = smoothstep(1.0 + aa, 1.0 - aa, r);",
" float halo = exp(-max(0.0, r - 1.0) * 13.0) * (1.0 - mask);",
"",
" vec3 body = vec3(0.0);",
" if (mask > 0.001) {",
" float rc = min(r, 1.0);",
" // Analytic sphere: the normal falls straight out of z = sqrt(1 - r^2).",
" float z = sqrt(max(0.0, 1.0 - rc * rc));",
" vec3 N = vec3(p.x, p.y, z);",
" vec2 q = lonlat(toBody(safeDir(N), spin));",
"",
" float gain = 0.9 + 2.6 * uPulse;",
" body = nebula(q);",
" body += vec3(1.0) * starsAll(q, gain) * (0.95 + 0.15 * uGlow);",
"",
" // Limb darkening.",
" body *= mix(0.32, 1.0, pow(z, 0.5));",
"",
" // Refract-sampled back layer, split per channel: the chromatic",
" // aberration that reads as glass at the limb.",
" vec3 I = vec3(0.0, 0.0, -1.0);",
" vec3 dR = safeDir(refract(I, N, 1.0 / 1.09));",
" vec3 dG = safeDir(refract(I, N, 1.0 / 1.13));",
" vec3 dB = safeDir(refract(I, N, 1.0 / 1.17));",
" vec2 qR = lonlat(toBody(dR, spin));",
" vec2 qG = lonlat(toBody(dG, spin));",
" vec2 qB = lonlat(toBody(dB, spin));",
" vec3 back = vec3(starsAll(qR, gain), starsAll(qG, gain), starsAll(qB, gain));",
" float fres = pow(1.0 - z, 1.8);",
" body += back * fres * 2.2;",
" body += nebula(qG) * fres * 0.8;",
"",
" // Aurora sheen riding the glass shell.",
" float ang = atan(p.y, p.x);",
" float sheen = smoothstep(0.72, 0.99, r) * smoothstep(1.0, 0.90, r);",
" float w = 0.5 + 0.5 * sin(ang * 2.0 + spin * 1.6);",
" body += (mix(cNeb, cAcc, w) * 0.55 + vec3(0.16)) * sheen * 0.30 * uGlow;",
"",
" // Three speculars, fixed in screen space: the key light does not spin.",
" vec2 a1 = p - vec2(-0.34, 0.62);",
" vec2 a2 = p - vec2(0.58, 0.60);",
" vec2 a3 = p - vec2(0.24, -0.80);",
" float sp = exp(-dot(a1, a1) * 30.0) * 0.30;",
" sp += exp(-dot(a2, a2) * 150.0) * 0.80;",
" sp += exp(-dot(a3, a3) * 70.0) * 0.30;",
" body += vec3(1.0) * sp * (0.30 + 0.70 * pow(1.0 - z, 1.5)) * uGlow;",
"",
" // Fresnel void glow hugging the inside of the rim.",
" body += mix(cAcc, vec3(1.0), 0.4) * pow(1.0 - z, 6.0) * 0.30 * uGlow;",
" }",
"",
" vec3 haloCol = mix(cNeb, cAcc, 0.35);",
" vec3 outCol = body * mask + haloCol * halo * 0.22 * uGlow;",
" float outA = clamp(mask + halo * 0.16 * uGlow, 0.0, 1.0);",
" gl_FragColor = vec4(clamp(outCol, 0.0, 4.0), outA);",
"}",
].join("\n");
var uni = {};
var ready = false;
function compile(type, src) {
var sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
throw new Error("cosmic-orb shader: " + gl.getShaderInfoLog(sh));
}
return sh;
}
if (gl) {
var prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
throw new Error("cosmic-orb link: " + gl.getProgramInfoLog(prog));
}
gl.useProgram(prog);
var buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
var loc = gl.getAttribLocation(prog, "aPos");
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
[
"uRes",
"uTime",
"uRadius",
"uSpin",
"uHue",
"uAccent",
"uStars",
"uGlow",
"uPulse",
].forEach(function (n) {
uni[n] = gl.getUniformLocation(prog, n);
});
gl.viewport(0, 0, W, H);
gl.uniform2f(uni.uRes, W, H);
gl.uniform1f(uni.uRadius, (SIZE * H) / 2);
gl.uniform1f(uni.uSpin, SPIN);
gl.uniform1f(uni.uHue, HUE);
gl.uniform1f(uni.uAccent, ACCENT);
gl.uniform1f(uni.uStars, STARS);
gl.uniform1f(uni.uGlow, GLOW);
ready = true;
}
// Every frame is computed from t alone: spin is spin_rate * t, the
// pulse is sampled from the envelope at t. No accumulators, no clocks.
function draw(t) {
if (!ready) return;
gl.uniform1f(uni.uTime, t);
gl.uniform1f(uni.uPulse, pulseAt(t));
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// The canvas is repainted from a property SETTER, not from onUpdate:
// gsap's seek(t) suppresses events by default, so an onUpdate callback
// silently never fires on a scrub and the canvas freezes on frame 0.
// Tweened values are always written during render, suppressed or not,
// so this fires on every seek — and hands us the frame time directly.
var driver = { _t: 0 };
Object.defineProperty(driver, "t", {
get: function () {
return this._t;
},
set: function (v) {
this._t = v;
draw(v);
},
});
tl.to(driver, { t: DUR, duration: DUR, ease: "none" }, 0);
window.__timelines["cosmic-orb"] = tl;
draw(0);
})();
</script>
</body>
</html>
```
</VariablesExplorer>
## Install
<InstallCommand command="npx hyperframes add cosmic-orb" />
That writes one file: `compositions/cosmic-orb.html`.
## Add it to your video
It runs for 10 seconds at 1920×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="cosmic-orb"
data-composition-src="compositions/cosmic-orb.html"
data-start="0"
data-duration="10"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Change how it looks
Set these CSS variables on the block:
- `--hue` — Nebula hue. Defaults to `268`.
- `--accent` — Accent hue. Defaults to `196`.
- `--spin` — Spin rate (turns per second). Defaults to `0.035`.
- `--stars` — Star density. Defaults to `1`.
- `--glow` — Glow intensity. Defaults to `1`.
- `--size` — Orb size (fraction of frame height). Defaults to `0.86`.
- `--pulse` — Beat pulse (0-1, flat value when no envelope is set). Defaults to `0`.
- `--pulseenvelope` — Beat pulse envelope: comma-separated 0-1 samples spread evenly across the clip. Defaults to ``.
- `--backdrop` — Backdrop. Defaults to `#04040a`.
{/* hf:generated-footer */}
Tagged `webgl` `shader` `background` `showcase` `3d`.
## Related topics
- [Browse the complete Catalog](/catalog)
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
- [Build a richer composition](/go-further)
+12 -387
View File
@@ -3,409 +3,34 @@ title: "Cross Warp Morph"
description: "Shader transition with cross-warped morphing"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/cross-warp-morph.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Cross Warp Morph preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cross-warp-morph.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cross-warp-morph.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add cross-warp-morph" />
```bash Terminal
npx hyperframes add cross-warp-morph
```
That writes one file: `compositions/cross-warp-morph.html`.
## Source
## Add it to your video
<Accordion title={`cross-warp-morph.html`}>
It runs for 4 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="main"
data-composition-id="cross-warp-morph"
data-composition-src="compositions/cross-warp-morph.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<style>
[data-composition-id="main"] {
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #283618;
}
/* Blueprint grid */
.bp-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
/* Layout */
.bp-left {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1200px;
display: flex;
align-items: center;
justify-content: center;
}
.bp-right {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 720px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px 60px 60px 40px;
}
.bp-divider {
position: absolute;
left: 1200px;
top: 60px;
bottom: 60px;
width: 1px;
background: #a7c957;
opacity: 0.15;
}
.bp-number {
font-family: "Space Mono", monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.4em;
text-transform: uppercase;
position: absolute;
top: 40px;
left: 60px;
color: #a7c957;
}
/* Right panel typography */
.bp-name {
font-family: "Bebas Neue", sans-serif;
font-size: 38px;
letter-spacing: 0.08em;
margin-bottom: 20px;
color: #a7c957;
}
.bp-plabel {
font-family: "Space Mono", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.3em;
text-transform: uppercase;
margin-bottom: 10px;
color: #a7c957;
opacity: 0.5;
}
.bp-prompt {
font-family: "JetBrains Mono", monospace;
font-size: 20px;
line-height: 1.6;
padding: 16px 20px;
border-radius: 6px;
margin-bottom: 20px;
color: #a7c957;
background: rgba(167, 201, 87, 0.06);
border: 1px solid rgba(167, 201, 87, 0.12);
}
.bp-desc {
font-family: "Lato", sans-serif;
font-size: 17px;
line-height: 1.6;
color: #8aaa9a;
opacity: 0.55;
}
/* GL scenes (hidden, just for texture capture) */
.scene {
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.scene-label {
font-family: "Bebas Neue", sans-serif;
font-size: 140px;
letter-spacing: 0.06em;
opacity: 0.12;
}
</style>
<div class="bp-grid"></div>
<div class="bp-divider"></div>
<div class="bp-number">13 / 14</div>
<!-- GL demo scenes (captured as textures, then hidden) -->
<div id="s1" class="scene" style="background: #283618">
<div class="scene-label" style="color: #a7c957">SCENE A</div>
</div>
<div id="s2" class="scene" style="background: #a7c957; opacity: 0">
<div class="scene-label" style="color: #283618">SCENE B</div>
</div>
<!-- GL canvas (left panel only) -->
<canvas
id="gl-canvas"
width="1200"
height="1080"
style="
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
z-index: 10;
pointer-events: none;
display: none;
"
>
</canvas>
<!-- Right panel metadata -->
<div class="bp-right">
<div class="bp-name">Cross-Warp Morph</div>
<div class="bp-plabel">Prompt</div>
<div class="bp-prompt">"use cross-warp morph shader transition"</div>
<div class="bp-desc">
Both scenes displace along a shared FBM noise field in opposite directions. Noise-driven
blend boundary.
</div>
</div>
<div
id="driver"
class="clip"
data-start="0"
data-duration="4"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var glCanvas = document.getElementById("gl-canvas");
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
if (!gl) {
console.warn("WebGL not available");
window.__timelines["main"] = gsap.timeline({ paused: true });
} else {
gl.viewport(0, 0, 1200, 1080);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
var sceneTextures = {};
function captureScene(sceneId) {
var scene = document.getElementById(sceneId);
var origOpacity = scene.style.opacity;
var origZ = scene.style.zIndex;
scene.style.opacity = "1";
scene.style.zIndex = "999";
scene.offsetHeight;
var c = document.createElement("canvas");
c.width = 1200;
c.height = 1080;
var ctx = c.getContext("2d");
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
ctx.fillRect(0, 0, 1200, 1080);
var sr = scene.getBoundingClientRect();
var els = scene.querySelectorAll("*");
for (var i = 0; i < els.length; i++) {
var el = els[i],
cs = window.getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
var r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
var x = r.left - sr.left,
y = r.top - sr.top,
w = r.width,
h = r.height;
ctx.save();
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
var bg = cs.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
ctx.fillStyle = bg;
ctx.fillRect(x, y, w, h);
}
var hasChildEls = el.querySelector("div,span,img,video");
var text = "";
for (var j = 0; j < el.childNodes.length; j++)
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
text = text.trim();
if (text && !hasChildEls) {
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
ctx.fillStyle = cs.color;
if (cs.letterSpacing && cs.letterSpacing !== "normal")
ctx.letterSpacing = cs.letterSpacing;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x + w / 2, y + h / 2);
}
ctx.restore();
}
scene.style.opacity = origOpacity;
scene.style.zIndex = origZ;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
sceneTextures[sceneId] = tex;
}
var vertSrc =
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
var quadBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW,
);
function compileShader(src, type) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error("Shader:", gl.getShaderInfoLog(s));
return s;
}
function mkProg(fragSrc) {
var p = gl.createProgram();
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
console.error("Link:", gl.getProgramInfoLog(p));
return p;
}
var H =
"precision mediump float;" +
"varying vec2 v_uv;" +
"uniform sampler2D u_from, u_to;" +
"uniform float u_progress;" +
"uniform vec2 u_resolution;\n";
var NQ =
"float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}" +
"float vnoise(vec2 p){vec2 i=floor(p),f=fract(p);" +
"f=f*f*f*(f*(f*6.-15.)+10.);" +
"return mix(mix(hash(i),hash(i+vec2(1,0)),f.x)," +
"mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);}" +
"float fbm(vec2 p){float v=0.,a=.5;" +
"mat2 R=mat2(.8,.6,-.6,.8);" +
"for(int i=0;i<5;i++){v+=a*vnoise(p);p=R*p*2.02;a*=.5;}return v;}";
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
var progTrans = mkProg(
H +
NQ +
"void main(){vec2 disp=vec2(fbm(v_uv*3.),fbm(v_uv*3.+vec2(7.3,3.7)))-.5;vec2 fromUv=clamp(v_uv+disp*u_progress*.5,0.,1.);vec2 toUv=clamp(v_uv-disp*(1.-u_progress)*.5,0.,1.);vec4 A=texture2D(u_from,fromUv);vec4 B=texture2D(u_to,toUv);float n=fbm(v_uv*4.+vec2(3.1,1.7));float blend=smoothstep(.4,.6,n+u_progress*1.2-.6);gl_FragColor=mix(A,B,blend);}",
);
function renderShader(prog, texFrom, texTo, progress) {
gl.useProgram(prog);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texFrom);
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texTo);
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
var pos = gl.getAttribLocation(prog, "a_pos");
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function easeInOut(p) {
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
}
captureScene("s1");
captureScene("s2");
glCanvas.style.display = "block";
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
document.querySelectorAll(".scene").forEach(function (s) {
s.style.opacity = "0";
});
var tl = gsap.timeline({
paused: true,
onUpdate: function () {
var t = tl.time();
if (t >= 1.0 && t < 3.0) {
renderShader(
progTrans,
sceneTextures["s1"],
sceneTextures["s2"],
easeInOut((t - 1.0) / 2.0),
);
} else if (t >= 3.0) {
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
} else {
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
}
},
});
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
window.__timelines["main"] = tl;
} // end if (gl)
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="cross-warp-morph" data-composition-src="compositions/cross-warp-morph.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+23 -478
View File
@@ -3,16 +3,35 @@ title: "Data Chart"
description: "Animated bar + line chart with staggered reveal, NYT-style typography, and value labels"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/data-chart.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Data Chart preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/data-chart.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/data-chart.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add data-chart" />
```bash Terminal
npx hyperframes add data-chart
```
That writes one file: `compositions/data-chart.html`.
## Add it to your video
It runs for 15 seconds at 1920×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="data-chart"
data-composition-src="compositions/data-chart.html"
data-start="0"
data-duration="15"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Change the colors
Set these CSS variables on the block:
@@ -20,480 +39,6 @@ Set these CSS variables on the block:
- `--bg-color` — Background. Defaults to `#faf9f6`.
- `--text-color` — Text color. Defaults to `#333333`.
## Source
<Accordion title={`data-chart.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Data Chart</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&family=Libre+Franklin:wght@300;400;600&display=block"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background-color: #faf9f6;
}
</style>
</head>
<body>
<div
id="data-chart"
data-composition-id="data-chart"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="15"
>
<div class="chart-container">
<!-- Header Section -->
<div class="header">
<h1 class="headline">Monthly Revenue vs. Conversion Rate</h1>
<p class="subtitle">JanJun 2024, in thousands</p>
<div class="key">
<div class="key-item">
<div class="key-box revenue-box"></div>
<span class="key-text">Revenue</span>
</div>
<div class="key-item">
<div class="key-line conversion-line"></div>
<span class="key-text">Conversion Rate</span>
</div>
</div>
</div>
<!-- SVG Chart Area -->
<svg class="chart-svg" viewBox="0 0 1600 700" preserveAspectRatio="xMidYMid meet">
<!-- Gridlines Group -->
<g class="gridlines"></g>
<!-- X-Axis Labels -->
<g class="x-axis-labels"></g>
<!-- Bars Group (Revenue) -->
<g class="bars-group"></g>
<!-- Bar Labels Group -->
<g class="bar-labels-group"></g>
<!-- Line Group (Conversion Rate) -->
<g class="line-group">
<path class="conversion-path" fill="none" stroke="#326FA8" stroke-width="1.5" />
<circle class="line-dot" r="5" fill="#326FA8" style="opacity: 0" />
</g>
<!-- Line Labels Group -->
<g class="line-labels-group"></g>
<!-- Baseline -->
<line x1="100" y1="600" x2="1500" y2="600" stroke="black" stroke-width="1" />
</svg>
<!-- Source Line -->
<div class="source">Source: Internal analytics</div>
</div>
<style>
[data-composition-id="data-chart"] {
width: 1920px;
height: 1080px;
background-color: #faf9f6;
font-family: "Libre Franklin", sans-serif;
color: #333;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
[data-composition-id="data-chart"] .chart-container {
width: 1600px;
height: 900px;
display: flex;
flex-direction: column;
position: relative;
transform: scale(1.15);
transform-origin: center center;
}
[data-composition-id="data-chart"] .header {
margin-bottom: 40px;
text-align: left;
}
[data-composition-id="data-chart"] .headline {
font-family: "Libre Baskerville", serif;
font-size: 42px;
margin: 0;
padding: 0;
clip-path: inset(0 100% 0 0);
white-space: nowrap;
}
[data-composition-id="data-chart"] .subtitle {
font-size: 16px;
color: #666;
margin: 8px 0 16px 0;
clip-path: inset(0 100% 0 0);
white-space: nowrap;
}
[data-composition-id="data-chart"] .key {
display: flex;
gap: 24px;
opacity: 0;
}
[data-composition-id="data-chart"] .key-item {
display: flex;
align-items: center;
gap: 8px;
}
[data-composition-id="data-chart"] .key-box {
width: 10px;
height: 10px;
background-color: #5c5c5c;
}
[data-composition-id="data-chart"] .key-line {
width: 16px;
height: 1.5px;
background-color: #326fa8;
}
[data-composition-id="data-chart"] .key-text {
font-size: 13px;
color: #666;
}
[data-composition-id="data-chart"] .chart-svg {
width: 100%;
height: 600px;
overflow: visible;
}
[data-composition-id="data-chart"] .source {
position: absolute;
bottom: 0;
left: 0;
font-size: 11px;
color: #999;
opacity: 0;
}
[data-composition-id="data-chart"] .grid-line {
stroke: #e8e8e8;
stroke-width: 0.5px;
opacity: 0;
}
[data-composition-id="data-chart"] .axis-label {
font-size: 14px;
fill: #666;
text-anchor: middle;
}
[data-composition-id="data-chart"] .bar {
fill: #5c5c5c;
}
[data-composition-id="data-chart"] .bar-label {
font-size: 14px;
fill: #333;
text-anchor: middle;
opacity: 0;
}
[data-composition-id="data-chart"] .line-label {
font-size: 14px;
fill: #326fa8;
font-weight: 600;
text-anchor: middle;
opacity: 0;
}
</style>
<script>
(function () {
const svg = document.querySelector('[data-composition-id="data-chart"] .chart-svg');
const tl = gsap.timeline({ paused: true });
// Data
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
const revenueData = [8, 12, 15, 11, 18, 22];
const conversionData = [2.1, 2.8, 3.2, 2.9, 3.8, 4.2];
// Chart Dimensions
const chartWidth = 1400;
const chartHeight = 500;
const startX = 100;
const startY = 600;
const barWidth = 80;
const spacing = (chartWidth - barWidth * months.length) / (months.length + 1);
// Scales
const maxRevenue = 25;
const maxConversion = 5;
const getBarY = (val) => startY - (val / maxRevenue) * chartHeight;
const getLineY = (val) => startY - (val / maxConversion) * chartHeight;
const getX = (i) => startX + spacing + i * (barWidth + spacing) + barWidth / 2;
// Setup Elements
const gridGroup = svg.querySelector(".gridlines");
const xAxisGroup = svg.querySelector(".x-axis-labels");
const barsGroup = svg.querySelector(".bars-group");
const barLabelsGroup = svg.querySelector(".bar-labels-group");
const lineLabelsGroup = svg.querySelector(".line-labels-group");
const conversionPath = svg.querySelector(".conversion-path");
const lineDot = svg.querySelector(".line-dot");
// Create Gridlines
for (let i = 1; i <= 5; i++) {
const y = startY - i * (chartHeight / 5);
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", startX);
line.setAttribute("y1", y);
line.setAttribute("x2", startX + chartWidth);
line.setAttribute("y2", y);
line.setAttribute("class", "grid-line");
gridGroup.appendChild(line);
}
// Create X-Axis Labels & Bars & Bar Labels
const linePoints = [];
months.forEach((month, i) => {
const x = getX(i);
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
label.setAttribute("x", x);
label.setAttribute("y", startY + 30);
label.setAttribute("class", "axis-label");
label.textContent = month;
xAxisGroup.appendChild(label);
const barHeight = (revenueData[i] / maxRevenue) * chartHeight;
const bar = document.createElementNS("http://www.w3.org/2000/svg", "rect");
bar.setAttribute("x", x - barWidth / 2);
bar.setAttribute("y", startY);
bar.setAttribute("width", barWidth);
bar.setAttribute("height", 0);
bar.setAttribute("class", "bar");
barsGroup.appendChild(bar);
const barLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
barLabel.setAttribute("x", x);
barLabel.setAttribute("y", getBarY(revenueData[i]) - 10);
barLabel.setAttribute("class", "bar-label");
barLabel.textContent = `$${revenueData[i]}K`;
barLabelsGroup.appendChild(barLabel);
linePoints.push(`${x},${getLineY(conversionData[i])}`);
const lineLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
lineLabel.setAttribute("x", x);
lineLabel.setAttribute("y", getLineY(conversionData[i]) - 15);
lineLabel.setAttribute("class", "line-label");
lineLabel.textContent = `${conversionData[i]}%`;
lineLabelsGroup.appendChild(lineLabel);
});
// Set Path Data
conversionPath.setAttribute("d", `M ${linePoints.join(" L ")}`);
const pathLength = conversionPath.getTotalLength();
conversionPath.style.strokeDasharray = pathLength;
conversionPath.style.strokeDashoffset = pathLength;
// --- ANIMATION TIMELINE ---
tl.to(
'[data-composition-id="data-chart"] .headline',
{
clipPath: "inset(0 0% 0 0)",
duration: 1.2,
ease: "power1.inOut",
},
0,
);
tl.to(
'[data-composition-id="data-chart"] .subtitle',
{
clipPath: "inset(0 0% 0 0)",
duration: 1.2,
ease: "power1.inOut",
},
0.2,
);
tl.to(
'[data-composition-id="data-chart"] .key',
{
opacity: 1,
duration: 0.4,
ease: "power2.out",
},
1.0,
);
tl.to(
'[data-composition-id="data-chart"] .grid-line',
{
opacity: 1,
duration: 0.5,
stagger: 0.25,
ease: "power2.out",
},
0.5,
);
// Bars
const bars = barsGroup.querySelectorAll(".bar");
const barLabels = barLabelsGroup.querySelectorAll(".bar-label");
bars.forEach((bar, i) => {
const barHeight = (revenueData[i] / maxRevenue) * chartHeight;
const startTime = 1.5 + i * 0.5;
tl.to(
bar,
{
attr: { height: barHeight, y: startY - barHeight },
duration: 0.8,
ease: "power2.out",
},
startTime,
);
tl.to(
barLabels[i],
{
opacity: 1,
duration: 0.3,
ease: "power2.out",
},
startTime + 0.8,
);
});
// Line
const lastBarStartTime = 1.5 + (months.length - 1) * 0.5;
const lineStartTime = lastBarStartTime + 0.5;
tl.to(
conversionPath,
{
strokeDashoffset: 0,
duration: 3,
ease: "none",
},
lineStartTime,
);
// Line Dot & Labels
tl.set(
lineDot,
{
opacity: 1,
attr: { cx: getX(0), cy: getLineY(conversionData[0]) },
},
lineStartTime,
);
const lineLabels = lineLabelsGroup.querySelectorAll(".line-label");
months.forEach((_, i) => {
if (i > 0) {
const x = getX(i);
const y = getLineY(conversionData[i]);
tl.to(
lineDot,
{
attr: { cx: x, cy: y },
duration: 3 / (months.length - 1),
ease: "none",
},
lineStartTime + ((i - 1) / (months.length - 1)) * 3,
);
}
const labelTime = lineStartTime + (i / (months.length - 1)) * 3 + 0.2;
lineLabels[i].textContent = "0.0%";
tl.to(
lineLabels[i],
{
opacity: 1,
duration: 0.1,
ease: "power2.out",
},
labelTime,
);
const dummy = { val: 0 };
tl.to(
dummy,
{
val: conversionData[i],
duration: 0.8,
ease: "power2.out",
onUpdate: () => {
lineLabels[i].textContent = dummy.val.toFixed(1) + "%";
},
},
labelTime,
);
});
// Source Line
tl.to(
'[data-composition-id="data-chart"] .source',
{
opacity: 1,
duration: 0.5,
ease: "power2.out",
},
">+0.5",
);
window.__timelines = window.__timelines || {};
window.__timelines["data-chart"] = tl;
})();
</script>
</div>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="data-chart" data-composition-src="compositions/data-chart.html" data-start="0" data-duration="15" data-track-index="1" data-width="1920" data-height="1080"></div>
```
{/* hf:generated-footer */}
Tagged `data` `chart` `statistics`.
+12 -390
View File
@@ -3,412 +3,34 @@ title: "Domain Warp Dissolve"
description: "Shader transition with fractal noise domain warping"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/domain-warp-dissolve.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Domain Warp Dissolve preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/domain-warp-dissolve.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/domain-warp-dissolve.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add domain-warp-dissolve" />
```bash Terminal
npx hyperframes add domain-warp-dissolve
```
That writes one file: `compositions/domain-warp-dissolve.html`.
## Source
## Add it to your video
<Accordion title={`domain-warp-dissolve.html`}>
It runs for 4 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="main"
data-composition-id="domain-warp-dissolve"
data-composition-src="compositions/domain-warp-dissolve.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<style>
[data-composition-id="main"] {
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #0d1b2a;
}
/* Blueprint grid */
.bp-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
/* Layout */
.bp-left {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1200px;
display: flex;
align-items: center;
justify-content: center;
}
.bp-right {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 720px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px 60px 60px 40px;
}
.bp-divider {
position: absolute;
left: 1200px;
top: 60px;
bottom: 60px;
width: 1px;
background: #00f5d4;
opacity: 0.15;
}
.bp-number {
font-family: "Space Mono", monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.4em;
text-transform: uppercase;
position: absolute;
top: 40px;
left: 60px;
color: #00f5d4;
}
/* Right panel typography */
.bp-name {
font-family: "Bebas Neue", sans-serif;
font-size: 38px;
letter-spacing: 0.08em;
margin-bottom: 20px;
color: #00f5d4;
}
.bp-plabel {
font-family: "Space Mono", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.3em;
text-transform: uppercase;
margin-bottom: 10px;
color: #00f5d4;
opacity: 0.5;
}
.bp-prompt {
font-family: "JetBrains Mono", monospace;
font-size: 20px;
line-height: 1.6;
padding: 16px 20px;
border-radius: 6px;
margin-bottom: 20px;
color: #00f5d4;
background: rgba(0, 245, 212, 0.06);
border: 1px solid rgba(0, 245, 212, 0.12);
}
.bp-desc {
font-family: "Lato", sans-serif;
font-size: 17px;
line-height: 1.6;
color: #8aa8c0;
opacity: 0.55;
}
/* GL scenes (hidden, just for texture capture) */
.scene {
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.scene-label {
font-family: "Bebas Neue", sans-serif;
font-size: 140px;
letter-spacing: 0.06em;
opacity: 0.12;
}
</style>
<div class="bp-grid"></div>
<div class="bp-divider"></div>
<div class="bp-number">01 / 14</div>
<!-- GL demo scenes (captured as textures, then hidden) -->
<div id="s1" class="scene" style="background: #0d1b2a">
<div class="scene-label" style="color: #00f5d4">SCENE A</div>
</div>
<div id="s2" class="scene" style="background: #00f5d4; opacity: 0">
<div class="scene-label" style="color: #0d1b2a">SCENE B</div>
</div>
<!-- GL canvas (left panel only) -->
<canvas
id="gl-canvas"
width="1200"
height="1080"
style="
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
z-index: 10;
pointer-events: none;
display: none;
"
>
</canvas>
<!-- Right panel metadata -->
<div class="bp-right">
<div class="bp-name">Domain Warp Dissolve</div>
<div class="bp-plabel">Prompt</div>
<div class="bp-prompt">"use domain warp dissolve shader transition"</div>
<div class="bp-desc">
Cascaded fbm(p + fbm(p)) displaces both scenes along a warp field. Iridescent
cosine-palette edge glow.
</div>
</div>
<div
id="driver"
class="clip"
data-start="0"
data-duration="4"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var glCanvas = document.getElementById("gl-canvas");
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
if (!gl) {
console.warn("WebGL not available");
window.__timelines["main"] = gsap.timeline({ paused: true });
} else {
gl.viewport(0, 0, 1200, 1080);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
var sceneTextures = {};
function captureScene(sceneId) {
var scene = document.getElementById(sceneId);
var origOpacity = scene.style.opacity;
var origZ = scene.style.zIndex;
scene.style.opacity = "1";
scene.style.zIndex = "999";
scene.offsetHeight;
var c = document.createElement("canvas");
c.width = 1200;
c.height = 1080;
var ctx = c.getContext("2d");
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
ctx.fillRect(0, 0, 1200, 1080);
var sr = scene.getBoundingClientRect();
var els = scene.querySelectorAll("*");
for (var i = 0; i < els.length; i++) {
var el = els[i],
cs = window.getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
var r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
var x = r.left - sr.left,
y = r.top - sr.top,
w = r.width,
h = r.height;
ctx.save();
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
var bg = cs.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
ctx.fillStyle = bg;
ctx.fillRect(x, y, w, h);
}
var hasChildEls = el.querySelector("div,span,img,video");
var text = "";
for (var j = 0; j < el.childNodes.length; j++)
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
text = text.trim();
if (text && !hasChildEls) {
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
ctx.fillStyle = cs.color;
if (cs.letterSpacing && cs.letterSpacing !== "normal")
ctx.letterSpacing = cs.letterSpacing;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x + w / 2, y + h / 2);
}
ctx.restore();
}
scene.style.opacity = origOpacity;
scene.style.zIndex = origZ;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
sceneTextures[sceneId] = tex;
}
var vertSrc =
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
var quadBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW,
);
function compileShader(src, type) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error("Shader:", gl.getShaderInfoLog(s));
return s;
}
function mkProg(fragSrc) {
var p = gl.createProgram();
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
console.error("Link:", gl.getProgramInfoLog(p));
return p;
}
var H =
"precision mediump float;" +
"varying vec2 v_uv;" +
"uniform sampler2D u_from, u_to;" +
"uniform float u_progress;" +
"uniform vec2 u_resolution;\n";
var NQ =
"float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}" +
"float vnoise(vec2 p){vec2 i=floor(p),f=fract(p);" +
"f=f*f*f*(f*(f*6.-15.)+10.);" +
"return mix(mix(hash(i),hash(i+vec2(1,0)),f.x)," +
"mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);}" +
"float fbm(vec2 p){float v=0.,a=.5;" +
"mat2 R=mat2(.8,.6,-.6,.8);" +
"for(int i=0;i<5;i++){v+=a*vnoise(p);p=R*p*2.02;a*=.5;}return v;}";
var CP =
"vec3 palette(float t,vec3 a,vec3 b,vec3 c,vec3 d){" + "return a+b*cos(6.2832*(c*t+d));}";
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
var progTrans = mkProg(
H +
NQ +
CP +
"void main(){vec2 q=vec2(fbm(v_uv*3.),fbm(v_uv*3.+vec2(5.2,1.3)));vec2 r=vec2(fbm(v_uv*3.+q*4.+vec2(1.7,9.2)),fbm(v_uv*3.+q*4.+vec2(8.3,2.8)));float n=fbm(v_uv*3.+r*2.);vec2 warpDir=(q-.5)*.4;vec4 A=texture2D(u_from,clamp(v_uv+warpDir*u_progress,0.,1.));vec4 B=texture2D(u_to,clamp(v_uv-warpDir*(1.-u_progress),0.,1.));float e=smoothstep(u_progress-.08,u_progress+.08,n);float ed=abs(n-u_progress);float em=smoothstep(.1,0.,ed)*(1.-step(1.,u_progress));vec3 ec=palette(ed*8.,vec3(.5),vec3(.5),vec3(1.),vec3(0.,.33,.67));gl_FragColor=vec4(mix(B,A,e).rgb+ec*em*2.,1.);}",
);
function renderShader(prog, texFrom, texTo, progress) {
gl.useProgram(prog);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texFrom);
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texTo);
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
var pos = gl.getAttribLocation(prog, "a_pos");
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function easeInOut(p) {
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
}
captureScene("s1");
captureScene("s2");
glCanvas.style.display = "block";
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
document.querySelectorAll(".scene").forEach(function (s) {
s.style.opacity = "0";
});
var tl = gsap.timeline({
paused: true,
onUpdate: function () {
var t = tl.time();
if (t >= 1.0 && t < 3.0) {
renderShader(
progTrans,
sceneTextures["s1"],
sceneTextures["s2"],
easeInOut((t - 1.0) / 2.0),
);
} else if (t >= 3.0) {
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
} else {
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
}
},
});
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
window.__timelines["main"] = tl;
} // end if (gl)
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="domain-warp-dissolve" data-composition-src="compositions/domain-warp-dissolve.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+4 -142
View File
@@ -3,13 +3,13 @@ title: "Editorial Flash Overlay"
description: "Finite neutral-warm light layers for a seek-safe camera-flash cut or social reveal"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/editorial-flash-overlay.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Editorial Flash Overlay preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/editorial-flash-overlay.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/editorial-flash-overlay.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add editorial-flash-overlay" />
```bash Terminal
npx hyperframes add editorial-flash-overlay
```
That writes one file: `compositions/editorial-flash-overlay.html`.
@@ -32,144 +32,6 @@ It runs for 4 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`editorial-flash-overlay.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: transparent;
}
#ef-flash {
--hf-editorial-flash-wash: 255 253 250;
--hf-editorial-flash-warm: 255 174 105;
--hf-editorial-flash-cool: 183 218 255;
position: absolute;
inset: -12%;
overflow: hidden;
pointer-events: none;
}
#ef-flash .ef-layer {
position: absolute;
inset: 0;
opacity: 0;
will-change: opacity, transform;
}
#ef-wash {
background: rgb(var(--hf-editorial-flash-wash));
}
#ef-core {
inset: -18%;
background: radial-gradient(
ellipse 68% 92% at 37% 47%,
rgb(255 255 255) 0%,
rgb(255 255 255 / 98%) 20%,
rgb(var(--hf-editorial-flash-wash) / 86%) 45%,
rgb(var(--hf-editorial-flash-warm) / 32%) 67%,
transparent 86%
);
mix-blend-mode: screen;
transform-origin: 38% 48%;
}
#ef-sweep {
inset: -28%;
background: linear-gradient(
108deg,
transparent 21%,
rgb(var(--hf-editorial-flash-warm) / 10%) 38%,
rgb(var(--hf-editorial-flash-wash) / 90%) 49%,
rgb(255 255 255 / 98%) 53%,
rgb(var(--hf-editorial-flash-cool) / 24%) 62%,
transparent 79%
);
mix-blend-mode: screen;
}
</style>
</head>
<body>
<div
id="editorial-flash-overlay"
data-composition-id="editorial-flash-overlay"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<div id="ef-flash" aria-hidden="true">
<div id="ef-wash" class="ef-layer"></div>
<div id="ef-core" class="ef-layer" data-layout-allow-overflow></div>
<div id="ef-sweep" class="ef-layer" data-layout-allow-overflow></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var flash = document.getElementById("ef-flash");
var host = flash && flash.closest("[data-composition-id]");
if (!host) throw new Error("Editorial Flash could not find its composition host");
var compositionId = host.getAttribute("data-composition-id");
var duration = Number(host.getAttribute("data-duration")) || 4;
var hit = Math.max(0.12, duration * 0.58);
var tl = gsap.timeline({ paused: true });
tl.to({}, { duration: duration, ease: "none" }, 0);
tl.to("#ef-wash", { opacity: 0.92, duration: 0.04, ease: "power4.in" }, hit);
tl.fromTo(
"#ef-core",
{ opacity: 0, scale: 0.86, rotation: -5 },
{ opacity: 1, scale: 1, rotation: -5, duration: 0.05, ease: "power4.in" },
hit - 0.01,
);
tl.fromTo(
"#ef-sweep",
{ opacity: 0, xPercent: -12, rotation: -2 },
{ opacity: 0.9, xPercent: 8, rotation: -2, duration: 0.04, ease: "power4.in" },
hit,
);
tl.to("#ef-wash", { opacity: 0, duration: 0.18, ease: "power3.out" }, hit + 0.04);
tl.to(
"#ef-core",
{ opacity: 0, scale: 1.18, rotation: -5, duration: 0.34, ease: "power2.out" },
hit + 0.04,
);
tl.to(
"#ef-sweep",
{ opacity: 0, xPercent: 28, rotation: -2, duration: 0.3, ease: "power2.out" },
hit + 0.04,
);
window.__timelines[compositionId] = tl;
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `flash` `editorial` `social` `light` `transition` `overlay` `media-treatment-overlay`.
+12 -376
View File
@@ -3,398 +3,34 @@ title: "Flash Through White"
description: "Shader transition with white flash crossfade"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/flash-through-white.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Flash Through White preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flash-through-white.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flash-through-white.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add flash-through-white" />
```bash Terminal
npx hyperframes add flash-through-white
```
That writes one file: `compositions/flash-through-white.html`.
## Source
## Add it to your video
<Accordion title={`flash-through-white.html`}>
It runs for 4 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="main"
data-composition-id="flash-through-white"
data-composition-src="compositions/flash-through-white.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<style>
[data-composition-id="main"] {
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #03071e;
}
/* Blueprint grid */
.bp-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
/* Layout */
.bp-left {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1200px;
display: flex;
align-items: center;
justify-content: center;
}
.bp-right {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 720px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px 60px 60px 40px;
}
.bp-divider {
position: absolute;
left: 1200px;
top: 60px;
bottom: 60px;
width: 1px;
background: #ffba08;
opacity: 0.15;
}
.bp-number {
font-family: "Space Mono", monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.4em;
text-transform: uppercase;
position: absolute;
top: 40px;
left: 60px;
color: #ffba08;
}
/* Right panel typography */
.bp-name {
font-family: "Bebas Neue", sans-serif;
font-size: 38px;
letter-spacing: 0.08em;
margin-bottom: 20px;
color: #ffba08;
}
.bp-plabel {
font-family: "Space Mono", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.3em;
text-transform: uppercase;
margin-bottom: 10px;
color: #ffba08;
opacity: 0.5;
}
.bp-prompt {
font-family: "JetBrains Mono", monospace;
font-size: 20px;
line-height: 1.6;
padding: 16px 20px;
border-radius: 6px;
margin-bottom: 20px;
color: #ffba08;
background: rgba(255, 186, 8, 0.06);
border: 1px solid rgba(255, 186, 8, 0.12);
}
.bp-desc {
font-family: "Lato", sans-serif;
font-size: 17px;
line-height: 1.6;
color: #888;
opacity: 0.55;
}
/* GL scenes (hidden, just for texture capture) */
.scene {
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.scene-label {
font-family: "Bebas Neue", sans-serif;
font-size: 140px;
letter-spacing: 0.06em;
opacity: 0.12;
}
</style>
<div class="bp-grid"></div>
<div class="bp-divider"></div>
<div class="bp-number">12 / 14</div>
<!-- GL demo scenes (captured as textures, then hidden) -->
<div id="s1" class="scene" style="background: #03071e">
<div class="scene-label" style="color: #ffba08">SCENE A</div>
</div>
<div id="s2" class="scene" style="background: #ffba08; opacity: 0">
<div class="scene-label" style="color: #03071e">SCENE B</div>
</div>
<!-- GL canvas (left panel only) -->
<canvas
id="gl-canvas"
width="1200"
height="1080"
style="
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
z-index: 10;
pointer-events: none;
display: none;
"
>
</canvas>
<!-- Right panel metadata -->
<div class="bp-right">
<div class="bp-name">Flash Through White</div>
<div class="bp-plabel">Prompt</div>
<div class="bp-prompt">"use flash through white shader transition"</div>
<div class="bp-desc">
Both scenes brighten to a white midpoint. Works on dark backgrounds where
color-dip-to-black is invisible.
</div>
</div>
<div
id="driver"
class="clip"
data-start="0"
data-duration="4"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var glCanvas = document.getElementById("gl-canvas");
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
if (!gl) {
console.warn("WebGL not available");
window.__timelines["main"] = gsap.timeline({ paused: true });
} else {
gl.viewport(0, 0, 1200, 1080);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
var sceneTextures = {};
function captureScene(sceneId) {
var scene = document.getElementById(sceneId);
var origOpacity = scene.style.opacity;
var origZ = scene.style.zIndex;
scene.style.opacity = "1";
scene.style.zIndex = "999";
scene.offsetHeight;
var c = document.createElement("canvas");
c.width = 1200;
c.height = 1080;
var ctx = c.getContext("2d");
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
ctx.fillRect(0, 0, 1200, 1080);
var sr = scene.getBoundingClientRect();
var els = scene.querySelectorAll("*");
for (var i = 0; i < els.length; i++) {
var el = els[i],
cs = window.getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
var r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
var x = r.left - sr.left,
y = r.top - sr.top,
w = r.width,
h = r.height;
ctx.save();
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
var bg = cs.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
ctx.fillStyle = bg;
ctx.fillRect(x, y, w, h);
}
var hasChildEls = el.querySelector("div,span,img,video");
var text = "";
for (var j = 0; j < el.childNodes.length; j++)
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
text = text.trim();
if (text && !hasChildEls) {
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
ctx.fillStyle = cs.color;
if (cs.letterSpacing && cs.letterSpacing !== "normal")
ctx.letterSpacing = cs.letterSpacing;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x + w / 2, y + h / 2);
}
ctx.restore();
}
scene.style.opacity = origOpacity;
scene.style.zIndex = origZ;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
sceneTextures[sceneId] = tex;
}
var vertSrc =
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
var quadBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW,
);
function compileShader(src, type) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error("Shader:", gl.getShaderInfoLog(s));
return s;
}
function mkProg(fragSrc) {
var p = gl.createProgram();
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
console.error("Link:", gl.getProgramInfoLog(p));
return p;
}
var H =
"precision mediump float;" +
"varying vec2 v_uv;" +
"uniform sampler2D u_from, u_to;" +
"uniform float u_progress;" +
"uniform vec2 u_resolution;\n";
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
var progTrans = mkProg(
H +
"void main(){vec4 A=texture2D(u_from,v_uv),B=texture2D(u_to,v_uv);float toWhite=smoothstep(0.,.45,u_progress);vec3 fromC=mix(A.rgb,vec3(1.),toWhite);float fromWhite=1.-smoothstep(.5,1.,u_progress);vec3 toC=mix(B.rgb,vec3(1.),fromWhite);gl_FragColor=vec4(mix(fromC,toC,smoothstep(.35,.65,u_progress)),1.);}",
);
function renderShader(prog, texFrom, texTo, progress) {
gl.useProgram(prog);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texFrom);
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texTo);
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
var pos = gl.getAttribLocation(prog, "a_pos");
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function easeInOut(p) {
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
}
captureScene("s1");
captureScene("s2");
glCanvas.style.display = "block";
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
document.querySelectorAll(".scene").forEach(function (s) {
s.style.opacity = "0";
});
var tl = gsap.timeline({
paused: true,
onUpdate: function () {
var t = tl.time();
if (t >= 1.0 && t < 3.0) {
renderShader(
progTrans,
sceneTextures["s1"],
sceneTextures["s2"],
easeInOut((t - 1.0) / 2.0),
);
} else if (t >= 3.0) {
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
} else {
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
}
},
});
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
window.__timelines["main"] = tl;
} // end if (gl)
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="flash-through-white" data-composition-src="compositions/flash-through-white.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+14 -479
View File
@@ -3,499 +3,34 @@ title: "Flowchart Vertical"
description: "Portrait animated decision tree with SVG connectors, sticky-note nodes, cursor interaction, and typing correction"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/flowchart-vertical.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Flowchart Vertical preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flowchart-vertical.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flowchart-vertical.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add flowchart-vertical" />
```bash Terminal
npx hyperframes add flowchart-vertical
```
That writes one file: `compositions/flowchart-vertical.html`.
## Source
## Add it to your video
<Accordion title={`flowchart-vertical.html`}>
It runs for 12 seconds at 1440×2560. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1440, height=2560" />
<title>Flowchart Vertical</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=block"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1440px;
height: 2560px;
overflow: hidden;
background-color: #ffffff;
}
</style>
</head>
<body>
```html index.html
<div
id="flowchart-vertical"
data-composition-id="flowchart-vertical"
data-width="1440"
data-height="2560"
data-composition-src="compositions/flowchart-vertical.html"
data-start="0"
data-duration="12"
>
<div class="canvas">
<svg class="connectors" width="1440" height="2560" viewBox="0 0 1440 2560">
<!-- Level 1 to 2 -->
<path
id="path-1-L"
class="connector"
d="M 720 210 L 720 360 L 440 360 L 440 530"
fill="none"
stroke="#333"
stroke-width="4"
/>
<path
id="path-1-R"
class="connector"
d="M 720 210 L 720 360 L 1000 360 L 1000 530"
fill="none"
stroke="#333"
stroke-width="4"
/>
<!-- Level 2 to 3 (Left side) -->
<path
id="path-2-LL"
class="connector"
d="M 440 590 L 440 760 L 280 760 L 280 930"
fill="none"
stroke="#333"
stroke-width="4"
/>
<path
id="path-2-LR"
class="connector"
d="M 440 590 L 440 760 L 600 760 L 600 930"
fill="none"
stroke="#333"
stroke-width="4"
/>
<!-- Level 2 to 3 (Right side) -->
<path
id="path-2-RL"
class="connector"
d="M 1000 590 L 1000 760 L 840 760 L 840 930"
fill="none"
stroke="#333"
stroke-width="4"
/>
<path
id="path-2-RR"
class="connector"
d="M 1000 590 L 1000 760 L 1160 760 L 1160 930"
fill="none"
stroke="#333"
stroke-width="4"
/>
</svg>
<div id="label-yes" class="connector-label">Yes</div>
<div id="label-not-sure" class="connector-label">Not sure</div>
<!-- Level 1 -->
<div id="node-root" class="node yellow" style="left: 720px; top: 180px">
Should I learn to code?
</div>
<!-- Level 2 -->
<div id="node-yes" class="node green" style="left: 440px; top: 560px">Yes</div>
<div id="node-not-sure" class="node peach" style="left: 1000px; top: 560px">Not sure</div>
<!-- Level 3 -->
<div id="node-python" class="node lavender" style="left: 280px; top: 960px">
<div style="position: relative; display: inline-block">
<span id="python-text">Start with Pythom</span>
<svg class="squiggle-container" width="64" height="6" viewBox="0 0 64 6">
<path
id="squiggle"
d="M 0 3 Q 2 0 4 3 T 8 3 T 12 3 T 16 3 T 20 3 T 24 3 T 28 3 T 32 3 T 36 3 T 40 3 T 44 3 T 48 3 T 52 3 T 56 3 T 60 3 T 64 3"
fill="none"
stroke="#E53935"
stroke-width="2"
/>
</svg>
</div>
</div>
<div id="node-nocode" class="node blue" style="left: 600px; top: 960px">
Try no-code first
</div>
<div id="node-website" class="node pink" style="left: 840px; top: 960px">
Build a personal website
</div>
<div id="node-course" class="node yellow" style="left: 1160px; top: 960px">
Take a free intro course
</div>
<!-- Cursor -->
<div id="cursor" class="cursor-container">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z"
fill="white"
stroke="black"
/>
</svg>
<div class="cursor-tag">You</div>
</div>
<!-- Emoji -->
<div id="emoji-thumb" class="emoji">👍</div>
<!-- Overlay -->
<div id="fade-overlay"></div>
</div>
<style>
#flowchart-vertical {
width: 1440px;
height: 2560px;
background-color: #ffffff;
background-image: radial-gradient(#e5e5e5 1px, transparent 1px);
background-size: 20px 20px;
position: absolute;
overflow: hidden;
font-family: "Inter", sans-serif;
}
#flowchart-vertical .canvas {
width: 100%;
height: 100%;
position: absolute;
transform: none;
transform-origin: 720px 640px;
}
#flowchart-vertical .node {
position: absolute;
transform: translate(-50%, -50%) scale(0);
padding: 16px 24px;
border-radius: 12px;
font-size: 18px;
font-weight: 500;
color: #333;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
white-space: nowrap;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
#flowchart-vertical .node.yellow {
background-color: #e8d44d;
}
#flowchart-vertical .node.green {
background-color: #c2e8a0;
}
#flowchart-vertical .node.peach {
background-color: #f5c5a3;
}
#flowchart-vertical .node.lavender {
background-color: #d4c5f9;
}
#flowchart-vertical .node.blue {
background-color: #a8d8f0;
}
#flowchart-vertical .node.pink {
background-color: #f8b4c8;
}
#flowchart-vertical .connectors {
position: absolute;
top: 0;
left: 0;
z-index: 5;
pointer-events: none;
}
#flowchart-vertical .connector {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
}
#flowchart-vertical .connector-label {
position: absolute;
font-size: 14px;
font-weight: 600;
color: #333;
background: white;
padding: 2px 6px;
border-radius: 4px;
opacity: 0;
z-index: 6;
transform: translate(-50%, -50%);
}
#flowchart-vertical #label-yes {
left: 580px;
top: 420px;
}
#flowchart-vertical #label-not-sure {
left: 860px;
top: 420px;
}
#flowchart-vertical .cursor-container {
position: absolute;
left: 1440px;
top: 2560px;
z-index: 100;
pointer-events: none;
display: flex;
align-items: flex-start;
}
#flowchart-vertical .cursor-tag {
background-color: #9747ff;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-left: 4px;
margin-top: 12px;
}
#flowchart-vertical .squiggle-container {
position: absolute;
right: 0;
bottom: -4px;
opacity: 0;
}
#flowchart-vertical .emoji {
position: absolute;
font-size: 32px;
left: 390px;
top: 960px;
transform: translate(-50%, -50%) scale(0);
z-index: 20;
}
#flowchart-vertical #fade-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: white;
opacity: 0;
z-index: 1000;
pointer-events: none;
}
#flowchart-vertical .selection-border {
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 2px solid #0b84f3;
border-radius: 16px;
pointer-events: none;
opacity: 0;
}
#flowchart-vertical .text-highlight {
background-color: #d0e4ff;
}
</style>
<script>
(function () {
const tl = gsap.timeline({ paused: true });
const S = "#flowchart-vertical";
const nodePython = document.querySelector(S + " #node-python");
const pythonText = document.querySelector(S + " #python-text");
// 1. Root node scales in
tl.to(S + " #node-root", { scale: 1, duration: 0.4, ease: "power2.out" });
// 2. Hold
tl.addLabel("hold1", "+=0.6");
// 3. Connectors draw
tl.to(
S + " .connector#path-1-L, " + S + " .connector#path-1-R",
{ strokeDashoffset: 0, duration: 0.5, ease: "none" },
"hold1",
);
tl.to(
S + " #label-yes, " + S + " #label-not-sure",
{ opacity: 1, duration: 0.2 },
"hold1+=0.25",
);
// 4. Level 2 nodes
tl.to(S + " #node-yes", { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, "hold1+=0.4");
tl.to(
S + " #node-not-sure",
{ scale: 1, duration: 0.4, ease: "back.out(1.7)" },
"hold1+=0.6",
);
// 5. Hold
tl.addLabel("hold2", "+=0.5");
// 6. Level 2→3 connectors
tl.to(
S +
" .connector#path-2-LL, " +
S +
" .connector#path-2-LR, " +
S +
" .connector#path-2-RL, " +
S +
" .connector#path-2-RR",
{ strokeDashoffset: 0, duration: 0.4, ease: "none" },
"hold2",
);
// 7. Leaf nodes
const leafNodes = [
S + " #node-python",
S + " #node-nocode",
S + " #node-website",
S + " #node-course",
];
leafNodes.forEach((id, i) => {
tl.to(
id,
{ scale: 1, duration: 0.4, ease: "back.out(1.7)" },
`hold2+=${0.3 + i * 0.15}`,
);
});
tl.to(S + " .squiggle-container", { opacity: 1, duration: 0.1 }, "hold2+=0.3");
// 8. Hold
tl.addLabel("hold3", "+=0.8");
// 9. Cursor drifts in
tl.to(S + " #cursor", { x: -1110, y: -1560, duration: 1, ease: "power1.inOut" }, "hold3");
// 10. Cursor clicks — selection border
tl.add(() => {
if (!nodePython) return;
if (!nodePython.querySelector(".selection-border")) {
const border = document.createElement("div");
border.className = "selection-border";
nodePython.appendChild(border);
}
gsap.set(S + " .selection-border", { opacity: 1 });
}, "hold3+=1");
tl.to(
S + " #cursor",
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" },
"hold3+=1",
);
// 11. Hold
tl.addLabel("hold4", "+=0.3");
// 12. Double-click to highlight "Pythom"
tl.to(
S + " #cursor",
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 3, overwrite: "auto" },
"hold4",
);
tl.add(() => {
if (!pythonText) return;
pythonText.innerHTML = 'Start with <span class="text-highlight">Pythom</span>';
}, "hold4+=0.1");
// 13. Hold
tl.addLabel("hold5", "+=0.2");
// 14. Type correction: "Pythom" → "Python"
const typingStart = tl.labels["hold5"];
const words = ["P", "Py", "Pyt", "Pyth", "Pytho", "Python"];
words.forEach((word, i) => {
tl.add(
() => {
if (pythonText)
pythonText.innerHTML = `Start with <span class="text-highlight">${word}</span>`;
},
typingStart + i * 0.05,
);
});
const typingEnd = typingStart + words.length * 0.05;
tl.add(() => {
if (pythonText) pythonText.innerHTML = "Start with Python";
gsap.set(S + " .squiggle-container", { opacity: 0 });
}, typingEnd);
// 15. Hold
tl.addLabel("hold6", typingEnd + 0.5);
// 16. Cursor clicks away to deselect
tl.to(S + " #cursor", { x: -1050, y: -1520, duration: 0.3, overwrite: "auto" }, "hold6");
tl.to(
S + " #cursor",
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" },
"hold6+=0.3",
);
tl.to(S + " .selection-border", { opacity: 0, duration: 0.1 }, "hold6+=0.3");
// 17. Emoji pop
tl.to(
S + " #emoji-thumb",
{ scale: 1, duration: 0.15, ease: "back.out(2)" },
"hold6+=0.4",
);
// 18. Hold
tl.addLabel("hold7", "+=2");
// 19. Fade out
tl.to(S + " #fade-overlay", { opacity: 1, duration: 0.5 }, "hold7");
window.__timelines = window.__timelines || {};
window.__timelines["flowchart-vertical"] = tl;
})();
</script>
</div>
</body>
</html>
data-track-index="1"
data-width="1440"
data-height="2560"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="flowchart-vertical" data-composition-src="compositions/flowchart-vertical.html" data-start="0" data-duration="12" data-track-index="1" data-width="1440" data-height="2560"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+14 -479
View File
@@ -3,499 +3,34 @@ title: "Flowchart"
description: "Animated decision tree with SVG connectors, sticky-note nodes, cursor interaction, and typing correction"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/flowchart.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Flowchart preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flowchart.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flowchart.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add flowchart" />
```bash Terminal
npx hyperframes add flowchart
```
That writes one file: `compositions/flowchart.html`.
## Source
## Add it to your video
<Accordion title={`flowchart.html`}>
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Flowchart</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=block"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background-color: #ffffff;
}
</style>
</head>
<body>
```html index.html
<div
id="flowchart"
data-composition-id="flowchart"
data-width="1920"
data-height="1080"
data-composition-src="compositions/flowchart.html"
data-start="0"
data-duration="12"
>
<div class="canvas">
<svg class="connectors" width="1920" height="1080" viewBox="0 0 1920 1080">
<!-- Level 1 to 2 -->
<path
id="path-1-L"
class="connector"
d="M 960 130 L 960 200 L 600 200 L 600 270"
fill="none"
stroke="#333"
stroke-width="4"
/>
<path
id="path-1-R"
class="connector"
d="M 960 130 L 960 200 L 1320 200 L 1320 270"
fill="none"
stroke="#333"
stroke-width="4"
/>
<!-- Level 2 to 3 (Left side) -->
<path
id="path-2-LL"
class="connector"
d="M 600 330 L 600 400 L 400 400 L 400 470"
fill="none"
stroke="#333"
stroke-width="4"
/>
<path
id="path-2-LR"
class="connector"
d="M 600 330 L 600 400 L 800 400 L 800 470"
fill="none"
stroke="#333"
stroke-width="4"
/>
<!-- Level 2 to 3 (Right side) -->
<path
id="path-2-RL"
class="connector"
d="M 1320 330 L 1320 400 L 1120 400 L 1120 470"
fill="none"
stroke="#333"
stroke-width="4"
/>
<path
id="path-2-RR"
class="connector"
d="M 1320 330 L 1320 400 L 1520 400 L 1520 470"
fill="none"
stroke="#333"
stroke-width="4"
/>
</svg>
<div id="label-yes" class="connector-label">Yes</div>
<div id="label-not-sure" class="connector-label">Not sure</div>
<!-- Level 1 -->
<div id="node-root" class="node yellow" style="left: 960px; top: 100px">
Should I learn to code?
</div>
<!-- Level 2 -->
<div id="node-yes" class="node green" style="left: 600px; top: 300px">Yes</div>
<div id="node-not-sure" class="node peach" style="left: 1320px; top: 300px">Not sure</div>
<!-- Level 3 -->
<div id="node-python" class="node lavender" style="left: 400px; top: 500px">
<div style="position: relative; display: inline-block">
<span id="python-text">Start with Pythom</span>
<svg class="squiggle-container" width="64" height="6" viewBox="0 0 64 6">
<path
id="squiggle"
d="M 0 3 Q 2 0 4 3 T 8 3 T 12 3 T 16 3 T 20 3 T 24 3 T 28 3 T 32 3 T 36 3 T 40 3 T 44 3 T 48 3 T 52 3 T 56 3 T 60 3 T 64 3"
fill="none"
stroke="#E53935"
stroke-width="2"
/>
</svg>
</div>
</div>
<div id="node-nocode" class="node blue" style="left: 800px; top: 500px">
Try no-code first
</div>
<div id="node-website" class="node pink" style="left: 1120px; top: 500px">
Build a personal website
</div>
<div id="node-course" class="node yellow" style="left: 1520px; top: 500px">
Take a free intro course
</div>
<!-- Cursor -->
<div id="cursor" class="cursor-container">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z"
fill="white"
stroke="black"
/>
</svg>
<div class="cursor-tag">You</div>
</div>
<!-- Emoji -->
<div id="emoji-thumb" class="emoji">👍</div>
<!-- Overlay -->
<div id="fade-overlay"></div>
</div>
<style>
[data-composition-id="flowchart"] {
width: 1920px;
height: 1080px;
background-color: #ffffff;
background-image: radial-gradient(#e5e5e5 1px, transparent 1px);
background-size: 20px 20px;
position: absolute;
overflow: hidden;
font-family: "Inter", sans-serif;
}
[data-composition-id="flowchart"] .canvas {
width: 100%;
height: 100%;
position: absolute;
transform: scale(1.2);
transform-origin: 960px 300px;
}
[data-composition-id="flowchart"] .node {
position: absolute;
transform: translate(-50%, -50%) scale(0);
padding: 16px 24px;
border-radius: 12px;
font-size: 18px;
font-weight: 500;
color: #333;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
white-space: nowrap;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
[data-composition-id="flowchart"] .node.yellow {
background-color: #e8d44d;
}
[data-composition-id="flowchart"] .node.green {
background-color: #c2e8a0;
}
[data-composition-id="flowchart"] .node.peach {
background-color: #f5c5a3;
}
[data-composition-id="flowchart"] .node.lavender {
background-color: #d4c5f9;
}
[data-composition-id="flowchart"] .node.blue {
background-color: #a8d8f0;
}
[data-composition-id="flowchart"] .node.pink {
background-color: #f8b4c8;
}
[data-composition-id="flowchart"] .connectors {
position: absolute;
top: 0;
left: 0;
z-index: 5;
pointer-events: none;
}
[data-composition-id="flowchart"] .connector {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
}
[data-composition-id="flowchart"] .connector-label {
position: absolute;
font-size: 14px;
font-weight: 600;
color: #333;
background: white;
padding: 2px 6px;
border-radius: 4px;
opacity: 0;
z-index: 6;
transform: translate(-50%, -50%);
}
[data-composition-id="flowchart"] #label-yes {
left: 780px;
top: 230px;
}
[data-composition-id="flowchart"] #label-not-sure {
left: 1140px;
top: 230px;
}
[data-composition-id="flowchart"] .cursor-container {
position: absolute;
left: 1920px;
top: 1080px;
z-index: 100;
pointer-events: none;
display: flex;
align-items: flex-start;
}
[data-composition-id="flowchart"] .cursor-tag {
background-color: #9747ff;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-left: 4px;
margin-top: 12px;
}
[data-composition-id="flowchart"] .squiggle-container {
position: absolute;
right: 0;
bottom: -4px;
opacity: 0;
}
[data-composition-id="flowchart"] .emoji {
position: absolute;
font-size: 32px;
left: 510px;
top: 500px;
transform: translate(-50%, -50%) scale(0);
z-index: 20;
}
[data-composition-id="flowchart"] #fade-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: white;
opacity: 0;
z-index: 1000;
pointer-events: none;
}
[data-composition-id="flowchart"] .selection-border {
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 2px solid #0b84f3;
border-radius: 16px;
pointer-events: none;
opacity: 0;
}
[data-composition-id="flowchart"] .text-highlight {
background-color: #d0e4ff;
}
</style>
<script>
(function () {
const tl = gsap.timeline({ paused: true });
const S = '[data-composition-id="flowchart"]';
const nodePython = document.querySelector(S + " #node-python");
const pythonText = document.querySelector(S + " #python-text");
// 1. Root node scales in
tl.to(S + " #node-root", { scale: 1, duration: 0.4, ease: "power2.out" });
// 2. Hold
tl.addLabel("hold1", "+=0.6");
// 3. Connectors draw
tl.to(
S + " .connector#path-1-L, " + S + " .connector#path-1-R",
{ strokeDashoffset: 0, duration: 0.5, ease: "none" },
"hold1",
);
tl.to(
S + " #label-yes, " + S + " #label-not-sure",
{ opacity: 1, duration: 0.2 },
"hold1+=0.25",
);
// 4. Level 2 nodes
tl.to(S + " #node-yes", { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, "hold1+=0.4");
tl.to(
S + " #node-not-sure",
{ scale: 1, duration: 0.4, ease: "back.out(1.7)" },
"hold1+=0.6",
);
// 5. Hold
tl.addLabel("hold2", "+=0.5");
// 6. Level 2→3 connectors
tl.to(
S +
" .connector#path-2-LL, " +
S +
" .connector#path-2-LR, " +
S +
" .connector#path-2-RL, " +
S +
" .connector#path-2-RR",
{ strokeDashoffset: 0, duration: 0.4, ease: "none" },
"hold2",
);
// 7. Leaf nodes
const leafNodes = [
S + " #node-python",
S + " #node-nocode",
S + " #node-website",
S + " #node-course",
];
leafNodes.forEach((id, i) => {
tl.to(
id,
{ scale: 1, duration: 0.4, ease: "back.out(1.7)" },
`hold2+=${0.3 + i * 0.15}`,
);
});
tl.to(S + " .squiggle-container", { opacity: 1, duration: 0.1 }, "hold2+=0.3");
// 8. Hold
tl.addLabel("hold3", "+=0.8");
// 9. Cursor drifts in
tl.to(S + " #cursor", { x: -1470, y: -540, duration: 1, ease: "power1.inOut" }, "hold3");
// 10. Cursor clicks — selection border
tl.add(() => {
if (!nodePython) return;
if (!nodePython.querySelector(".selection-border")) {
const border = document.createElement("div");
border.className = "selection-border";
nodePython.appendChild(border);
}
gsap.set(S + " .selection-border", { opacity: 1 });
}, "hold3+=1");
tl.to(
S + " #cursor",
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" },
"hold3+=1",
);
// 11. Hold
tl.addLabel("hold4", "+=0.3");
// 12. Double-click to highlight "Pythom"
tl.to(
S + " #cursor",
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 3, overwrite: "auto" },
"hold4",
);
tl.add(() => {
if (!pythonText) return;
pythonText.innerHTML = 'Start with <span class="text-highlight">Pythom</span>';
}, "hold4+=0.1");
// 13. Hold
tl.addLabel("hold5", "+=0.2");
// 14. Type correction: "Pythom" → "Python"
const typingStart = tl.labels["hold5"];
const words = ["P", "Py", "Pyt", "Pyth", "Pytho", "Python"];
words.forEach((word, i) => {
tl.add(
() => {
if (pythonText)
pythonText.innerHTML = `Start with <span class="text-highlight">${word}</span>`;
},
typingStart + i * 0.05,
);
});
const typingEnd = typingStart + words.length * 0.05;
tl.add(() => {
if (pythonText) pythonText.innerHTML = "Start with Python";
gsap.set(S + " .squiggle-container", { opacity: 0 });
}, typingEnd);
// 15. Hold
tl.addLabel("hold6", typingEnd + 0.5);
// 16. Cursor clicks away to deselect
tl.to(S + " #cursor", { x: -1420, y: -500, duration: 0.3, overwrite: "auto" }, "hold6");
tl.to(
S + " #cursor",
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" },
"hold6+=0.3",
);
tl.to(S + " .selection-border", { opacity: 0, duration: 0.1 }, "hold6+=0.3");
// 17. Emoji pop
tl.to(
S + " #emoji-thumb",
{ scale: 1, duration: 0.15, ease: "back.out(2)" },
"hold6+=0.4",
);
// 18. Hold
tl.addLabel("hold7", "+=2");
// 19. Fade out
tl.to(S + " #fade-overlay", { opacity: 1, duration: 0.5 }, "hold7");
window.__timelines = window.__timelines || {};
window.__timelines["flowchart"] = tl;
})();
</script>
</div>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="flowchart" data-composition-src="compositions/flowchart.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+4 -133
View File
@@ -3,13 +3,13 @@ title: "Freeze-Frame Dressing"
description: "Timeline-driven paper, tape, and flash dressing for a freeze-frame or background-removed subject"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/freeze-frame-dressing.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Freeze-Frame Dressing preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/freeze-frame-dressing.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/freeze-frame-dressing.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add freeze-frame-dressing" />
```bash Terminal
npx hyperframes add freeze-frame-dressing
```
That writes one file: `compositions/freeze-frame-dressing.html`.
@@ -32,135 +32,6 @@ It runs for 4 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`freeze-frame-dressing.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: transparent;
}
#hf-freeze-paper {
position: absolute;
inset: 0;
opacity: 0;
background:
linear-gradient(115deg, rgb(248 84 73 / 86%) 0 16%, transparent 16%),
linear-gradient(345deg, rgb(255 221 82 / 92%) 0 17%, transparent 17%),
linear-gradient(rgb(244 240 224 / 78%), rgb(244 240 224 / 78%));
clip-path: polygon(2% 8%, 97% 2%, 94% 94%, 6% 98%);
mix-blend-mode: screen;
pointer-events: none;
}
.hf-freeze-tape {
position: absolute;
width: 12.5vmin;
height: 2.9vmin;
opacity: 0;
background: rgb(255 236 168 / 76%);
border: 1px solid rgb(110 91 55 / 18%);
box-shadow: 0 0.3vmin 0.8vmin rgb(20 16 10 / 18%);
pointer-events: none;
}
#hf-freeze-tape-a {
top: 8%;
left: 12.5%;
transform: rotate(-8deg);
}
#hf-freeze-tape-b {
right: 12.5%;
bottom: 7%;
transform: rotate(7deg);
}
#hf-freeze-flash {
position: absolute;
inset: 0;
opacity: 0;
background: #fff;
pointer-events: none;
}
</style>
</head>
<body>
<div
id="freeze-frame-dressing"
data-composition-id="freeze-frame-dressing"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<div id="hf-freeze-paper" aria-hidden="true"></div>
<div id="hf-freeze-tape-a" class="hf-freeze-tape" aria-hidden="true"></div>
<div id="hf-freeze-tape-b" class="hf-freeze-tape" aria-hidden="true"></div>
<div id="hf-freeze-flash" aria-hidden="true"></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var paper = document.getElementById("hf-freeze-paper");
var host = paper && paper.closest("[data-composition-id]");
if (!host) throw new Error("Freeze-Frame Dressing could not find its composition host");
var compositionId = host.getAttribute("data-composition-id");
var duration = Number(host.getAttribute("data-duration")) || 4;
var hit = Math.max(0.1, duration * 0.24);
var exit = Math.max(hit + 0.7, duration - 0.3);
var tl = gsap.timeline({ paused: true });
tl.to({}, { duration: duration, ease: "none" }, 0);
tl.to("#hf-freeze-flash", { opacity: 0.95, duration: 0.06, ease: "power2.out" }, hit);
tl.to("#hf-freeze-flash", { opacity: 0, duration: 0.24, ease: "power2.in" }, hit + 0.06);
tl.to("#hf-freeze-paper", { opacity: 0.64, duration: 0.18, ease: "power2.out" }, hit + 0.04);
tl.fromTo(
"#hf-freeze-tape-a",
{ opacity: 0, scale: 1.18 },
{ opacity: 1, scale: 1, duration: 0.24, ease: "power2.out" },
hit + 0.3,
);
tl.fromTo(
"#hf-freeze-tape-b",
{ opacity: 0, scale: 1.18 },
{ opacity: 1, scale: 1, duration: 0.24, ease: "power2.out" },
hit + 0.42,
);
tl.to(
["#hf-freeze-paper", "#hf-freeze-tape-a", "#hf-freeze-tape-b"],
{ opacity: 0, duration: 0.24, ease: "power2.in" },
exit,
);
window.__timelines[compositionId] = tl;
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `freeze-frame` `cutout` `scrapbook` `paper` `social` `overlay` `media-treatment-overlay`.
-523
View File
@@ -1,523 +0,0 @@
---
title: "Gallery Tunnel"
description: "An infinite corridor of panels: a square tunnel receding to a vanishing point, its floor, ceiling and walls tiled with saturated colour slabs and caller-supplied images that swell toward camera and exit past you, the far end dissolving into fog."
---
import { InstallCommand } from "/snippets/install-command.jsx";
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
<VariablesExplorer
previewSrc="/public/catalog/preview/blocks/gallery-tunnel.html"
compositionId="gallery-tunnel"
compositionSrc="compositions/gallery-tunnel.html"
variables={[{"id":"images","type":"string","label":"Image slots: comma-separated paths, tiled into the panels (e.g. assets/shot-1.png,assets/shot-2.png)","default":"","placeholder":"assets/shot-1.png,assets/shot-2.png"},{"id":"imageMix","type":"number","label":"Image share of filled slots (0 = all colour, 1 = all images)","default":0.5,"min":0,"max":1,"step":0.05},{"id":"speed","type":"number","label":"Flight speed","default":3,"min":0,"max":8,"step":0.1,"unit":"segments/s"},{"id":"palette","type":"string","label":"Slab palette: comma-separated hex colours","default":"#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200"},{"id":"grid","type":"number","label":"Grid density: slots per wall face per axis (2 = 4 per face, 16 per segment)","default":2,"min":1,"max":4,"step":1},{"id":"fill","type":"number","label":"Slot fill rate","default":0.5,"min":0,"max":1,"step":0.05},{"id":"fog","type":"number","label":"Fog distance: depth at which the corridor reaches pure backdrop","default":14.25,"min":3,"max":14.5,"step":0.25,"unit":"segments"},{"id":"edgeColor","type":"color","label":"Edge line colour","default":"#B0B0B0"},{"id":"edgeOpacity","type":"number","label":"Edge line opacity","default":0.5,"min":0,"max":1,"step":0.05},{"id":"backdrop","type":"color","label":"Backdrop and fog colour","default":"#000000"}]}
>
```html gallery-tunnel.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Gallery Tunnel</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
overflow: hidden;
}
#gt-root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
#gt-backdrop {
position: absolute;
inset: 0;
background: #000000;
}
#gt-canvas {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
}
/* Off-screen decode well: the <img> elements live in the DOM so the page
load event waits for them, but they are never composited themselves —
WebGL reads them as textures. The well is clipped to 1px; the images
inside keep their natural layout size on purpose, because texImage2D
uploads an <img> at its LAID-OUT size — sizing them down with CSS
uploads a 1x1 texture and every panel samples one dark pixel. */
#gt-assets {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
</style>
</head>
<body>
<!--
Content slots. Every panel in the corridor is a slot, and `images` fills a
share of them with your own artwork — drop the files in the project's
assets/ and list them, either by editing the `images` default below or from
the host composition:
<div
data-composition-id="gallery-tunnel"
data-composition-src="compositions/gallery-tunnel.html"
data-start="0" data-duration="12" data-track-index="0"
data-width="1920" data-height="1080"
style="--images: assets/shot-1.png, assets/shot-2.png, assets/shot-3.png; --imagemix: 0.7"
></div>
Any number of images works; they are dealt to slots by hash, so one image
recurs at different depths rather than appearing once. Each is contain-fit
inside its slot, so a 16:9 screenshot keeps its aspect. `imageMix` sets the
image-vs-colour split (1 = every filled slot is an image), `fill` how many
slots are filled at all.
-->
<div
id="gt-root"
data-composition-id="gallery-tunnel"
data-root="true"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="12"
data-composition-variables='[
{"id":"images","type":"string","label":"Image slots: comma-separated paths, tiled into the panels (e.g. assets/shot-1.png,assets/shot-2.png)","default":"","placeholder":"assets/shot-1.png,assets/shot-2.png"},
{"id":"imageMix","type":"number","label":"Image share of filled slots (0 = all colour, 1 = all images)","default":0.5,"min":0,"max":1,"step":0.05},
{"id":"speed","type":"number","label":"Flight speed","default":3,"min":0,"max":8,"step":0.1,"unit":"segments/s"},
{"id":"palette","type":"string","label":"Slab palette: comma-separated hex colours","default":"#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200"},
{"id":"grid","type":"number","label":"Grid density: slots per wall face per axis (2 = 4 per face, 16 per segment)","default":2,"min":1,"max":4,"step":1},
{"id":"fill","type":"number","label":"Slot fill rate","default":0.5,"min":0,"max":1,"step":0.05},
{"id":"fog","type":"number","label":"Fog distance: depth at which the corridor reaches pure backdrop","default":14.25,"min":3,"max":14.5,"step":0.25,"unit":"segments"},
{"id":"edgeColor","type":"color","label":"Edge line colour","default":"#B0B0B0"},
{"id":"edgeOpacity","type":"number","label":"Edge line opacity","default":0.5,"min":0,"max":1,"step":0.05},
{"id":"backdrop","type":"color","label":"Backdrop and fog colour","default":"#000000"}
]'
>
<div id="gt-backdrop"></div>
<canvas id="gt-canvas" width="1920" height="1080"></canvas>
<div id="gt-assets" aria-hidden="true" data-layout-allow-overflow></div>
<!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->
<div
id="gt-drv"
class="clip"
data-start="0"
data-duration="12"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
var DUR = 12;
var W = 1920;
var H = 1080;
// Fixed tunnel geometry, measured off the reference.
var TUNNEL_WIDTH = 2;
var TUNNEL_HEIGHT = 1.8;
var SEGMENT_DEPTH = 1;
var NUM_SEGMENTS = 15;
var LINE_RADIUS = 0.003;
var FOV = 75;
var INSET = 0.94; // slab gutter inside its cell, so tiles read as discrete
var SKIN = 0.002; // push slabs just behind the wall plane so edges stay on top
var ROOT = document.getElementById("gt-root");
var CS = getComputedStyle(ROOT);
// `data-composition-variables` is the single owner of every declared
// default — parse it rather than repeating each default a second time
// in JS. `window.__hyperframes` is NOT guaranteed to exist yet when this
// inline script runs, so it can only ever be an override, never the
// source. Precedence: host CSS custom property, runtime variable bag,
// declared default.
var DECL = {};
JSON.parse(ROOT.getAttribute("data-composition-variables") || "[]").forEach(function (v) {
DECL[v.id] = v.default;
});
var HF = window.__hyperframes;
var V = (HF && HF.getVariables && HF.getVariables()) || {};
function raw(id) {
var css = CS.getPropertyValue("--" + id.toLowerCase()).trim();
if (css !== "") return css;
if (V[id] !== undefined && V[id] !== "") return V[id];
return DECL[id];
}
function num(id) {
var n = parseFloat(raw(id));
return isFinite(n) ? n : 0;
}
function str(id) {
var s = raw(id);
return typeof s === "string" ? s.trim() : "";
}
function list(id) {
return str(id)
.split(",")
.map(function (s) {
return s.trim();
})
.filter(function (s) {
return s !== "";
});
}
var SPEED = num("speed");
var GRID = Math.max(1, Math.min(4, Math.round(num("grid"))));
var FILL = num("fill");
var IMAGE_MIX = num("imageMix");
var FOG_FAR = Math.max(3, Math.min(14.5, num("fog")));
var EDGE_OPACITY = num("edgeOpacity");
var EDGE_COLOR = str("edgeColor");
var BACKDROP = str("backdrop");
var PALETTE = list("palette");
var IMAGE_SRCS = list("images");
document.getElementById("gt-backdrop").style.background = BACKDROP;
// How far behind the camera a segment travels before it recycles. Tying
// it to the fog distance is the whole trick: the recycle boundary lands
// at exactly FOG_FAR, so a segment reappears already fogged to pure
// backdrop and nothing ever pops in at the far end. One owner for the
// invariant — move the fog and the boundary follows.
var BACK = Math.max(0, Math.min(1.5, NUM_SEGMENTS * SEGMENT_DEPTH - FOG_FAR));
// Deterministic 2D integer hash — the only source of "randomness".
// Content at frame N is a pure function of (absolute segment, slot).
function hash2(a, b) {
var h = Math.imul(a | 0, 0x27d4eb2d) ^ Math.imul(b | 0, 0x165667b1);
h = Math.imul(h ^ (h >>> 15), 0x2545f491);
h ^= h >>> 13;
return (h >>> 0) / 4294967296;
}
var THREEJS = window.THREE;
var canvas = document.getElementById("gt-canvas");
var renderer = new THREEJS.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: false,
preserveDrawingBuffer: true,
});
renderer.setPixelRatio(1);
renderer.setSize(W, H, false);
renderer.setClearColor(new THREEJS.Color(BACKDROP), 1);
var scene = new THREEJS.Scene();
scene.fog = new THREEJS.Fog(new THREEJS.Color(BACKDROP), 1, FOG_FAR * SEGMENT_DEPTH);
// Camera sits still at the origin looking down -z; the corridor moves.
// Same picture as a moving camera, and the modulo keeps every coordinate
// bounded no matter how long the clip runs.
var camera = new THREEJS.PerspectiveCamera(FOV, W / H, 0.05, 40);
camera.position.set(0, 0, 0);
camera.lookAt(0, 0, -1);
var PLANE = new THREEJS.PlaneGeometry(1, 1);
var COLOR_MATS = PALETTE.map(function (hex) {
return new THREEJS.MeshBasicMaterial({ color: new THREEJS.Color(hex), fog: true });
});
if (COLOR_MATS.length === 0) {
COLOR_MATS.push(new THREEJS.MeshBasicMaterial({ color: 0xffffff, fog: true }));
}
// Caller-supplied content slots. Real <img> elements (so the page load
// event waits for the decode) used directly as WebGL textures.
var assets = document.getElementById("gt-assets");
var IMAGES = IMAGE_SRCS.map(function (src, k) {
var el = document.createElement("img");
el.id = "gt-img-" + k;
el.alt = "";
el.src = src;
assets.appendChild(el);
var tex = new THREEJS.Texture(el);
tex.minFilter = THREEJS.LinearFilter;
tex.magFilter = THREEJS.LinearFilter;
tex.generateMipmaps = false;
var rec = {
aspect: 0,
mat: new THREEJS.MeshBasicMaterial({ map: tex, fog: true }),
};
function ready() {
if (!el.naturalWidth) return;
rec.aspect = el.naturalWidth / el.naturalHeight;
tex.needsUpdate = true;
}
el.addEventListener("load", ready);
ready(); // a cache hit can complete before the listener is attached
return rec;
});
var edgeMat = new THREEJS.MeshBasicMaterial({
color: new THREEJS.Color(EDGE_COLOR),
fog: true,
transparent: true,
opacity: EDGE_OPACITY,
});
var HW = TUNNEL_WIDTH / 2;
var HH = TUNNEL_HEIGHT / 2;
var cellZ = SEGMENT_DEPTH / GRID;
var cellX = TUNNEL_WIDTH / GRID;
var cellY = TUNNEL_HEIGHT / GRID;
// Four faces of a segment box. `place` maps a cell to a world position
// inside the segment (origin at the segment's near boundary, growing
// toward -z); `rot` orients the unit plane into that face, inward-facing.
// `cw`/`ch` are the cell's extent in the plane's own local axes.
var FACES = [
{
// floor
rot: [-Math.PI / 2, 0, 0],
cw: cellX,
ch: cellZ,
place: function (iu, iz) {
return [-HW + (iu + 0.5) * cellX, -HH - SKIN, -(iz + 0.5) * cellZ];
},
},
{
// ceiling
rot: [Math.PI / 2, 0, 0],
cw: cellX,
ch: cellZ,
place: function (iu, iz) {
return [-HW + (iu + 0.5) * cellX, HH + SKIN, -(iz + 0.5) * cellZ];
},
},
{
// left wall
rot: [0, Math.PI / 2, 0],
cw: cellZ,
ch: cellY,
place: function (iu, iz) {
return [-HW - SKIN, -HH + (iu + 0.5) * cellY, -(iz + 0.5) * cellZ];
},
},
{
// right wall
rot: [0, -Math.PI / 2, 0],
cw: cellZ,
ch: cellY,
place: function (iu, iz) {
return [HW + SKIN, -HH + (iu + 0.5) * cellY, -(iz + 0.5) * cellZ];
},
},
];
var UP = new THREEJS.Vector3(0, 1, 0);
// Edges are solid tube geometry, not lines: a cylinder thickens with
// perspective the way a real moulding does, which is the whole reason
// the corridor reads as built rather than drawn.
function tube(group, ax, ay, az, bx, by, bz) {
var dx = bx - ax,
dy = by - ay,
dz = bz - az;
var len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len < 1e-6) return;
var mesh = new THREEJS.Mesh(
new THREEJS.CylinderGeometry(LINE_RADIUS, LINE_RADIUS, len, 6, 1),
edgeMat,
);
mesh.position.set((ax + bx) / 2, (ay + by) / 2, (az + bz) / 2);
mesh.quaternion.setFromUnitVectors(UP, new THREEJS.Vector3(dx / len, dy / len, dz / len));
group.add(mesh);
}
function buildEdges(group) {
var D = SEGMENT_DEPTH;
var k, j, u, z;
// The four corner runs (and any interior wall divisions) come from the
// side walls; floor/ceiling only contribute their interior lines, so
// corners are drawn once and never double-blend at 50% opacity.
for (k = 1; k < GRID; k++) {
u = -HW + k * cellX;
tube(group, u, -HH, 0, u, -HH, -D);
tube(group, u, HH, 0, u, HH, -D);
}
for (k = 0; k <= GRID; k++) {
u = -HH + k * cellY;
tube(group, -HW, u, 0, -HW, u, -D);
tube(group, HW, u, 0, HW, u, -D);
}
// Ring lines at each depth boundary. The far boundary belongs to the
// next segment, so stop one short and the tiling stays seamless.
for (j = 0; j < GRID; j++) {
z = -j * cellZ;
tube(group, -HW, -HH, z, HW, -HH, z);
tube(group, -HW, HH, z, HW, HH, z);
tube(group, -HW, -HH, z, -HW, HH, z);
tube(group, HW, -HH, z, HW, HH, z);
}
}
var segs = [];
for (var i = 0; i < NUM_SEGMENTS; i++) {
var group = new THREEJS.Group();
var panels = [];
for (var f = 0; f < FACES.length; f++) {
var face = FACES[f];
for (var iz = 0; iz < GRID; iz++) {
for (var iu = 0; iu < GRID; iu++) {
var mesh = new THREEJS.Mesh(PLANE, COLOR_MATS[0]);
var p = face.place(iu, iz);
mesh.position.set(p[0], p[1], p[2]);
mesh.rotation.set(face.rot[0], face.rot[1], face.rot[2]);
mesh.visible = false;
group.add(mesh);
panels.push({ mesh: mesh, cw: face.cw * INSET, ch: face.ch * INSET });
}
}
}
buildEdges(group);
scene.add(group);
segs.push({ group: group, panels: panels });
}
// Contain-fit so a supplied screenshot keeps its aspect inside the slot
// instead of being squashed to the cell (wall cells are portrait).
function fit(p, ar) {
if (!ar) {
p.mesh.scale.set(p.cw, p.ch, 1);
return;
}
if (ar > p.cw / p.ch) p.mesh.scale.set(p.cw, p.cw / ar, 1);
else p.mesh.scale.set(p.ch * ar, p.ch, 1);
}
// Everything about frame t is computed from t alone. `d` is the distance
// flown in segments; `a` is the ABSOLUTE segment number occupying pool
// slot i right now, which is what content hashes and the every-other
// rhythm key off — so a segment carries the same panels every time it is
// looked at, no matter how the timeline is scrubbed.
function draw(t) {
var d = SPEED * t;
for (var i = 0; i < NUM_SEGMENTS; i++) {
var seg = segs[i];
var a = i + NUM_SEGMENTS * Math.ceil((d - i - BACK) / NUM_SEGMENTS);
seg.group.position.z = -(a - d) * SEGMENT_DEPTH;
var lit = a % 2 === 0;
var panels = seg.panels;
for (var s = 0; s < panels.length; s++) {
var p = panels[s];
if (!lit || hash2(a, s * 4 + 1) >= FILL) {
p.mesh.visible = false;
continue;
}
p.mesh.visible = true;
if (IMAGES.length > 0 && hash2(a, s * 4 + 2) < IMAGE_MIX) {
var im = IMAGES[Math.floor(hash2(a, s * 4 + 4) * IMAGES.length) % IMAGES.length];
p.mesh.material = im.mat;
fit(p, im.aspect);
} else {
var ci = Math.floor(hash2(a, s * 4 + 3) * COLOR_MATS.length) % COLOR_MATS.length;
p.mesh.material = COLOR_MATS[ci];
p.mesh.scale.set(p.cw, p.ch, 1);
}
}
}
renderer.render(scene, camera);
}
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// Repaint from a property SETTER, not from onUpdate: gsap's seek(t)
// suppresses events by default, so an onUpdate callback silently never
// fires on a scrub and the canvas freezes on frame 0. Tweened values are
// always written during render, suppressed or not.
var driver = { _t: 0 };
Object.defineProperty(driver, "t", {
get: function () {
return this._t;
},
set: function (v) {
this._t = v;
draw(v);
},
});
tl.to(driver, { t: DUR, duration: DUR, ease: "none" }, 0);
window.__timelines["gallery-tunnel"] = tl;
draw(0);
})();
</script>
</body>
</html>
```
</VariablesExplorer>
## Install
<InstallCommand command="npx hyperframes add gallery-tunnel" />
That writes one file: `compositions/gallery-tunnel.html`.
## Add it to your video
It runs for 12 seconds at 1920×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="gallery-tunnel"
data-composition-src="compositions/gallery-tunnel.html"
data-start="0"
data-duration="12"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Change how it looks
Set these CSS variables on the block:
- `--images` — Image slots: comma-separated paths, tiled into the panels (e.g. assets/shot-1.png,assets/shot-2.png). Defaults to ``.
- `--imagemix` — Image share of filled slots (0 = all colour, 1 = all images). Defaults to `0.5`.
- `--speed` — Flight speed (segments per second). Defaults to `3`.
- `--palette` — Slab palette: comma-separated hex colours. Defaults to `#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200`.
- `--grid` — Grid density: slots per wall face per axis (2 = 4 per face, 16 per segment). Defaults to `2`.
- `--fill` — Slot fill rate. Defaults to `0.5`.
- `--fog` — Fog distance in segments: depth at which the corridor reaches pure backdrop. Defaults to `14.25`.
- `--edgecolor` — Edge line colour. Defaults to `#B0B0B0`.
- `--edgeopacity` — Edge line opacity. Defaults to `0.5`.
- `--backdrop` — Backdrop and fog colour. Defaults to `#000000`.
{/* hf:generated-footer */}
Tagged `webgl` `3d` `background` `showcase` `depth` `loop`.
## Related topics
- [Browse the complete Catalog](/catalog)
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
- [Build a richer composition](/go-further)
+12 -376
View File
@@ -3,398 +3,34 @@ title: "Glitch"
description: "Shader transition with digital glitch artifacts"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/glitch.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Glitch preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/glitch.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/glitch.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add glitch" />
```bash Terminal
npx hyperframes add glitch
```
That writes one file: `compositions/glitch.html`.
## Source
## Add it to your video
<Accordion title={`glitch.html`}>
It runs for 4 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="main"
data-composition-id="glitch"
data-composition-src="compositions/glitch.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<style>
[data-composition-id="main"] {
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #293241;
}
/* Blueprint grid */
.bp-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
/* Layout */
.bp-left {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1200px;
display: flex;
align-items: center;
justify-content: center;
}
.bp-right {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 720px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px 60px 60px 40px;
}
.bp-divider {
position: absolute;
left: 1200px;
top: 60px;
bottom: 60px;
width: 1px;
background: #ee6c4d;
opacity: 0.15;
}
.bp-number {
font-family: "Space Mono", monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.4em;
text-transform: uppercase;
position: absolute;
top: 40px;
left: 60px;
color: #ee6c4d;
}
/* Right panel typography */
.bp-name {
font-family: "Bebas Neue", sans-serif;
font-size: 38px;
letter-spacing: 0.08em;
margin-bottom: 20px;
color: #ee6c4d;
}
.bp-plabel {
font-family: "Space Mono", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.3em;
text-transform: uppercase;
margin-bottom: 10px;
color: #ee6c4d;
opacity: 0.5;
}
.bp-prompt {
font-family: "JetBrains Mono", monospace;
font-size: 20px;
line-height: 1.6;
padding: 16px 20px;
border-radius: 6px;
margin-bottom: 20px;
color: #ee6c4d;
background: rgba(238, 108, 77, 0.06);
border: 1px solid rgba(238, 108, 77, 0.12);
}
.bp-desc {
font-family: "Lato", sans-serif;
font-size: 17px;
line-height: 1.6;
color: #98c1d9;
opacity: 0.55;
}
/* GL scenes (hidden, just for texture capture) */
.scene {
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.scene-label {
font-family: "Bebas Neue", sans-serif;
font-size: 140px;
letter-spacing: 0.06em;
opacity: 0.12;
}
</style>
<div class="bp-grid"></div>
<div class="bp-divider"></div>
<div class="bp-number">09 / 14</div>
<!-- GL demo scenes (captured as textures, then hidden) -->
<div id="s1" class="scene" style="background: #293241">
<div class="scene-label" style="color: #ee6c4d">SCENE A</div>
</div>
<div id="s2" class="scene" style="background: #ee6c4d; opacity: 0">
<div class="scene-label" style="color: #293241">SCENE B</div>
</div>
<!-- GL canvas (left panel only) -->
<canvas
id="gl-canvas"
width="1200"
height="1080"
style="
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
z-index: 10;
pointer-events: none;
display: none;
"
>
</canvas>
<!-- Right panel metadata -->
<div class="bp-right">
<div class="bp-name">Glitch</div>
<div class="bp-plabel">Prompt</div>
<div class="bp-prompt">"use glitch shader transition"</div>
<div class="bp-desc">
Scan lines, block scramble, chromatic aberration, brightness flicker, and color
posterization.
</div>
</div>
<div
id="driver"
class="clip"
data-start="0"
data-duration="4"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var glCanvas = document.getElementById("gl-canvas");
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
if (!gl) {
console.warn("WebGL not available");
window.__timelines["main"] = gsap.timeline({ paused: true });
} else {
gl.viewport(0, 0, 1200, 1080);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
var sceneTextures = {};
function captureScene(sceneId) {
var scene = document.getElementById(sceneId);
var origOpacity = scene.style.opacity;
var origZ = scene.style.zIndex;
scene.style.opacity = "1";
scene.style.zIndex = "999";
scene.offsetHeight;
var c = document.createElement("canvas");
c.width = 1200;
c.height = 1080;
var ctx = c.getContext("2d");
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
ctx.fillRect(0, 0, 1200, 1080);
var sr = scene.getBoundingClientRect();
var els = scene.querySelectorAll("*");
for (var i = 0; i < els.length; i++) {
var el = els[i],
cs = window.getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
var r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
var x = r.left - sr.left,
y = r.top - sr.top,
w = r.width,
h = r.height;
ctx.save();
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
var bg = cs.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
ctx.fillStyle = bg;
ctx.fillRect(x, y, w, h);
}
var hasChildEls = el.querySelector("div,span,img,video");
var text = "";
for (var j = 0; j < el.childNodes.length; j++)
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
text = text.trim();
if (text && !hasChildEls) {
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
ctx.fillStyle = cs.color;
if (cs.letterSpacing && cs.letterSpacing !== "normal")
ctx.letterSpacing = cs.letterSpacing;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x + w / 2, y + h / 2);
}
ctx.restore();
}
scene.style.opacity = origOpacity;
scene.style.zIndex = origZ;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
sceneTextures[sceneId] = tex;
}
var vertSrc =
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
var quadBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW,
);
function compileShader(src, type) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error("Shader:", gl.getShaderInfoLog(s));
return s;
}
function mkProg(fragSrc) {
var p = gl.createProgram();
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
console.error("Link:", gl.getProgramInfoLog(p));
return p;
}
var H =
"precision mediump float;" +
"varying vec2 v_uv;" +
"uniform sampler2D u_from, u_to;" +
"uniform float u_progress;" +
"uniform vec2 u_resolution;\n";
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
var progTrans = mkProg(
H +
"float rand(vec2 co){return fract(sin(dot(co,vec2(12.9898,78.233)))*43758.5453);}void main(){float inten=u_progress*(1.-u_progress)*4.;float lineY=floor(v_uv.y*60.)/60.;float lineDisp=(rand(vec2(lineY,floor(u_progress*17.)))-.5)*.18*inten;vec2 block=floor(v_uv*vec2(12.,8.));float br=rand(block+vec2(floor(u_progress*11.)));float ba=step(.83,br)*inten;vec2 bd=(vec2(rand(block*2.1),rand(block*3.7))-.5)*.35*ba;vec2 uv=clamp(v_uv+vec2(lineDisp,0.)+bd,0.,1.);float shift=inten*.035;float r=texture2D(u_from,uv+vec2(shift,0.)).r;float g=texture2D(u_from,uv).g;float b=texture2D(u_from,uv-vec2(shift,0.)).b;vec3 col=vec3(r,g,b);col-=step(.5,fract(v_uv.y*u_resolution.y*.5))*.05*inten;col*=1.+(rand(vec2(floor(u_progress*23.)))-.5)*.3*inten;float levels=mix(256.,8.,inten*.5);col=floor(col*levels)/levels;gl_FragColor=mix(vec4(col,1.),texture2D(u_to,v_uv),u_progress);}",
);
function renderShader(prog, texFrom, texTo, progress) {
gl.useProgram(prog);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texFrom);
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texTo);
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
var pos = gl.getAttribLocation(prog, "a_pos");
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function easeInOut(p) {
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
}
captureScene("s1");
captureScene("s2");
glCanvas.style.display = "block";
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
document.querySelectorAll(".scene").forEach(function (s) {
s.style.opacity = "0";
});
var tl = gsap.timeline({
paused: true,
onUpdate: function () {
var t = tl.time();
if (t >= 1.0 && t < 3.0) {
renderShader(
progTrans,
sceneTextures["s1"],
sceneTextures["s2"],
easeInOut((t - 1.0) / 2.0),
);
} else if (t >= 3.0) {
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
} else {
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
}
},
});
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
window.__timelines["main"] = tl;
} // end if (gl)
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="glitch" data-composition-src="compositions/glitch.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+12 -375
View File
@@ -3,397 +3,34 @@ title: "Gravitational Lens"
description: "Shader transition with gravitational lensing distortion"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/gravitational-lens.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Gravitational Lens preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/gravitational-lens.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/gravitational-lens.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add gravitational-lens" />
```bash Terminal
npx hyperframes add gravitational-lens
```
That writes one file: `compositions/gravitational-lens.html`.
## Source
## Add it to your video
<Accordion title={`gravitational-lens.html`}>
It runs for 4 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="main"
data-composition-id="gravitational-lens"
data-composition-src="compositions/gravitational-lens.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<style>
[data-composition-id="main"] {
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #10002b;
}
/* Blueprint grid */
.bp-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
/* Layout */
.bp-left {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1200px;
display: flex;
align-items: center;
justify-content: center;
}
.bp-right {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 720px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px 60px 60px 40px;
}
.bp-divider {
position: absolute;
left: 1200px;
top: 60px;
bottom: 60px;
width: 1px;
background: #f20089;
opacity: 0.15;
}
.bp-number {
font-family: "Space Mono", monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.4em;
text-transform: uppercase;
position: absolute;
top: 40px;
left: 60px;
color: #f20089;
}
/* Right panel typography */
.bp-name {
font-family: "Bebas Neue", sans-serif;
font-size: 38px;
letter-spacing: 0.08em;
margin-bottom: 20px;
color: #f20089;
}
.bp-plabel {
font-family: "Space Mono", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.3em;
text-transform: uppercase;
margin-bottom: 10px;
color: #f20089;
opacity: 0.5;
}
.bp-prompt {
font-family: "JetBrains Mono", monospace;
font-size: 20px;
line-height: 1.6;
padding: 16px 20px;
border-radius: 6px;
margin-bottom: 20px;
color: #f20089;
background: rgba(242, 0, 137, 0.06);
border: 1px solid rgba(242, 0, 137, 0.12);
}
.bp-desc {
font-family: "Lato", sans-serif;
font-size: 17px;
line-height: 1.6;
color: #a080a0;
opacity: 0.55;
}
/* GL scenes (hidden, just for texture capture) */
.scene {
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.scene-label {
font-family: "Bebas Neue", sans-serif;
font-size: 140px;
letter-spacing: 0.06em;
opacity: 0.12;
}
</style>
<div class="bp-grid"></div>
<div class="bp-divider"></div>
<div class="bp-number">06 / 14</div>
<!-- GL demo scenes (captured as textures, then hidden) -->
<div id="s1" class="scene" style="background: #10002b">
<div class="scene-label" style="color: #f20089">SCENE A</div>
</div>
<div id="s2" class="scene" style="background: #f20089; opacity: 0">
<div class="scene-label" style="color: #10002b">SCENE B</div>
</div>
<!-- GL canvas (left panel only) -->
<canvas
id="gl-canvas"
width="1200"
height="1080"
style="
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
z-index: 10;
pointer-events: none;
display: none;
"
>
</canvas>
<!-- Right panel metadata -->
<div class="bp-right">
<div class="bp-name">Gravitational Lens</div>
<div class="bp-plabel">Prompt</div>
<div class="bp-prompt">"use gravitational lens shader transition"</div>
<div class="bp-desc">
Content warps toward a gravity well with chromatic aberration and event horizon darkening.
</div>
</div>
<div
id="driver"
class="clip"
data-start="0"
data-duration="4"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var glCanvas = document.getElementById("gl-canvas");
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
if (!gl) {
console.warn("WebGL not available");
window.__timelines["main"] = gsap.timeline({ paused: true });
} else {
gl.viewport(0, 0, 1200, 1080);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
var sceneTextures = {};
function captureScene(sceneId) {
var scene = document.getElementById(sceneId);
var origOpacity = scene.style.opacity;
var origZ = scene.style.zIndex;
scene.style.opacity = "1";
scene.style.zIndex = "999";
scene.offsetHeight;
var c = document.createElement("canvas");
c.width = 1200;
c.height = 1080;
var ctx = c.getContext("2d");
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
ctx.fillRect(0, 0, 1200, 1080);
var sr = scene.getBoundingClientRect();
var els = scene.querySelectorAll("*");
for (var i = 0; i < els.length; i++) {
var el = els[i],
cs = window.getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
var r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
var x = r.left - sr.left,
y = r.top - sr.top,
w = r.width,
h = r.height;
ctx.save();
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
var bg = cs.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
ctx.fillStyle = bg;
ctx.fillRect(x, y, w, h);
}
var hasChildEls = el.querySelector("div,span,img,video");
var text = "";
for (var j = 0; j < el.childNodes.length; j++)
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
text = text.trim();
if (text && !hasChildEls) {
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
ctx.fillStyle = cs.color;
if (cs.letterSpacing && cs.letterSpacing !== "normal")
ctx.letterSpacing = cs.letterSpacing;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x + w / 2, y + h / 2);
}
ctx.restore();
}
scene.style.opacity = origOpacity;
scene.style.zIndex = origZ;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
sceneTextures[sceneId] = tex;
}
var vertSrc =
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
var quadBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW,
);
function compileShader(src, type) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error("Shader:", gl.getShaderInfoLog(s));
return s;
}
function mkProg(fragSrc) {
var p = gl.createProgram();
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
console.error("Link:", gl.getProgramInfoLog(p));
return p;
}
var H =
"precision mediump float;" +
"varying vec2 v_uv;" +
"uniform sampler2D u_from, u_to;" +
"uniform float u_progress;" +
"uniform vec2 u_resolution;\n";
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
var progTrans = mkProg(
H +
"void main(){vec4 B=texture2D(u_to,v_uv);vec2 uv=v_uv-.5;float dist=length(uv);float pull=u_progress*2.;float warpStr=pull*.3/(dist+.1);vec2 warped=clamp(v_uv-uv*warpStr,0.,1.);vec4 A=texture2D(u_from,warped);float horizon=smoothstep(0.,.3,dist/(1.-u_progress*.85+.001));float shift=pull*.02/(dist+.2);float r=texture2D(u_from,clamp(v_uv-uv*(warpStr+shift),0.,1.)).r;float b=texture2D(u_from,clamp(v_uv-uv*(warpStr-shift),0.,1.)).b;vec3 lensed=vec3(r,A.g,b)*horizon;gl_FragColor=vec4(mix(lensed,B.rgb,smoothstep(.3,.9,u_progress)),1.);}",
);
function renderShader(prog, texFrom, texTo, progress) {
gl.useProgram(prog);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texFrom);
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texTo);
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
var pos = gl.getAttribLocation(prog, "a_pos");
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function easeInOut(p) {
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
}
captureScene("s1");
captureScene("s2");
glCanvas.style.display = "block";
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
document.querySelectorAll(".scene").forEach(function (s) {
s.style.opacity = "0";
});
var tl = gsap.timeline({
paused: true,
onUpdate: function () {
var t = tl.time();
if (t >= 1.0 && t < 3.0) {
renderShader(
progTrans,
sceneTextures["s1"],
sceneTextures["s2"],
easeInOut((t - 1.0) / 2.0),
);
} else if (t >= 3.0) {
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
} else {
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
}
},
});
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
window.__timelines["main"] = tl;
} // end if (gl)
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="gravitational-lens" data-composition-src="compositions/gravitational-lens.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
-448
View File
@@ -1,448 +0,0 @@
---
title: "Halftone Field"
description: "A full-bleed halftone light field: a grid of dots whose size and colour track a slow flowing simplex-noise field, so broad bands of light sweep across a mostly-black frame like a giant LED wall showing a plasma. Built to run under type."
---
import { InstallCommand } from "/snippets/install-command.jsx";
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
<VariablesExplorer
previewSrc="/public/catalog/preview/blocks/halftone-field.html"
compositionId="halftone-field"
compositionSrc="compositions/halftone-field.html"
variables={[{"id":"frequency","type":"number","label":"Field frequency","default":2,"min":1,"max":10,"step":0.1},{"id":"speed","type":"number","label":"Flow speed","default":5,"min":0,"max":10,"step":0.1},{"id":"cellSize","type":"number","label":"Cell size","default":54,"min":1,"max":100,"step":1},{"id":"gamma","type":"number","label":"Gamma (midtone crush)","default":12,"min":1,"max":20,"step":0.1},{"id":"paletteBias","type":"number","label":"Palette bias","default":2,"min":0,"max":20,"step":0.5},{"id":"palette1","type":"color","label":"Palette stop 1 (lows)","default":"#00AAFF"},{"id":"palette2","type":"color","label":"Palette stop 2","default":"#C2FF67"},{"id":"palette3","type":"color","label":"Palette stop 3","default":"#6600FF"},{"id":"palette4","type":"color","label":"Palette stop 4 (crests)","default":"#000000"},{"id":"background","type":"color","label":"Background","default":"#000000"},{"id":"quantize","type":"boolean","label":"Quantise to 30fps (stepped)","default":false}]}
>
```html halftone-field.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Halftone Field</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
overflow: hidden;
}
#hlf-root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
#hlf-backdrop {
position: absolute;
inset: 0;
background: #000000;
}
#hlf-canvas {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
}
</style>
</head>
<body>
<div
id="hlf-root"
data-composition-id="halftone-field"
data-root="true"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="10"
data-composition-variables='[
{"id":"frequency","type":"number","label":"Field frequency","default":2,"min":1,"max":10,"step":0.1},
{"id":"speed","type":"number","label":"Flow speed","default":5,"min":0,"max":10,"step":0.1},
{"id":"cellSize","type":"number","label":"Cell size","default":54,"min":1,"max":100,"step":1},
{"id":"gamma","type":"number","label":"Gamma (midtone crush)","default":12,"min":1,"max":20,"step":0.1},
{"id":"paletteBias","type":"number","label":"Palette bias","default":2,"min":0,"max":20,"step":0.5},
{"id":"palette1","type":"color","label":"Palette stop 1 (lows)","default":"#00AAFF"},
{"id":"palette2","type":"color","label":"Palette stop 2","default":"#C2FF67"},
{"id":"palette3","type":"color","label":"Palette stop 3","default":"#6600FF"},
{"id":"palette4","type":"color","label":"Palette stop 4 (crests)","default":"#000000"},
{"id":"background","type":"color","label":"Background","default":"#000000"},
{"id":"quantize","type":"boolean","label":"Quantise to 30fps (stepped)","default":false}
]'
>
<div id="hlf-backdrop"></div>
<canvas id="hlf-canvas" width="1920" height="1080"></canvas>
<!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->
<div
id="hlf-drv"
class="clip"
data-start="0"
data-duration="10"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
var DUR = 10;
var W = 1920;
var H = 1080;
var V = (window.__hyperframes && window.__hyperframes.getVariables()) || {};
var CS = getComputedStyle(document.getElementById("hlf-root"));
// The runtime defines every declared variable as `--<id>` on the root,
// so a host stylesheet can override one there too. Read the custom
// property first, fall back to the declared value when it is unset.
function raw(id) {
var css = CS.getPropertyValue("--" + id.toLowerCase()).trim();
return css !== "" ? css : V[id];
}
function num(id, fallback) {
var n = parseFloat(raw(id));
return isFinite(n) ? n : fallback;
}
function bool(id) {
var v = raw(id);
return v === true || /^(true|1|on|yes)$/i.test(String(v));
}
// #RGB / #RRGGBB -> [r, g, b] in 0..1. Anything else falls back.
function rgb(id, fallback) {
var s = String(raw(id) || "").trim();
var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
if (!m) s = fallback;
else s = "#" + m[1];
var hex = s.slice(1);
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
var n = parseInt(hex, 16);
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
}
// The reference exposes 1-10 / 1-100 / 1-20 dials and maps them onto
// the ranges the shader actually wants.
function mapLinear(x, a1, a2, b1, b2) {
return b1 + ((x - a1) * (b2 - b1)) / (a2 - a1);
}
var FREQ = mapLinear(num("frequency", 2), 1, 10, 0.3, 6);
var SPEED = num("speed", 5) * 0.05;
var CELL = mapLinear(num("cellSize", 54), 1, 100, 6, 60);
var GAMMA = mapLinear(num("gamma", 12), 1, 20, 0.5, 8);
var BIAS = num("paletteBias", 2) * 0.05;
var PAL = [
rgb("palette1", "#00AAFF"),
rgb("palette2", "#C2FF67"),
rgb("palette3", "#6600FF"),
rgb("palette4", "#000000"),
];
var BG = rgb("background", "#000000");
var QUANTIZE = bool("quantize");
document.getElementById("hlf-backdrop").style.background =
"rgb(" +
BG.map(function (c) {
return Math.round(c * 255);
}).join(",") +
")";
var canvas = document.getElementById("hlf-canvas");
var gl =
canvas.getContext("webgl", {
alpha: false,
antialias: false,
depth: false,
stencil: false,
preserveDrawingBuffer: true,
powerPreference: "high-performance",
}) ||
canvas.getContext("experimental-webgl", {
alpha: false,
preserveDrawingBuffer: true,
});
var VERT = "attribute vec2 aPos;\nvoid main() { gl_Position = vec4(aPos, 0.0, 1.0); }";
// Single pass. The reference renders the colour field into a target and
// samples it at each cell centre, but the field is analytic, so
// evaluating it at the cell centre here is the same value without the
// round trip through a texture.
var FRAG = `
precision highp float;
uniform vec2 uRes;
uniform float uTime;
uniform float uFreq;
uniform float uSpeed;
uniform float uCell;
uniform float uGamma;
uniform float uBias;
uniform vec3 uPal0;
uniform vec3 uPal1;
uniform vec3 uPal2;
uniform vec3 uPal3;
uniform vec3 uBg;
// 3D simplex noise — Ashima Arts / Stefan Gustavson (MIT).
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
float snoise(vec3 v) {
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i);
vec4 p = permute(permute(permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0)) +
i.y + vec4(0.0, i1.y, i2.y, 1.0)) +
i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
m = m * m;
return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}
// Full-saturation, full-value HSV: only the hue ramp survives.
vec3 hue2rgb(float h) {
return clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
}
// Piecewise-linear ramp across the four palette stops.
vec3 ramp(float v) {
float x = clamp(v, 0.0, 1.0) * 3.0;
vec3 c = mix(uPal0, uPal1, clamp(x, 0.0, 1.0));
c = mix(c, uPal2, clamp(x - 1.0, 0.0, 1.0));
c = mix(c, uPal3, clamp(x - 2.0, 0.0, 1.0));
return c;
}
void main() {
// One sample per cell, taken at the cell centre.
vec2 cell = floor(gl_FragCoord.xy / uCell);
vec2 centre = (cell + 0.5) * uCell;
vec2 st = centre / uRes;
st.x *= uRes.x / uRes.y;
float hue = abs(snoise(vec3(st * uFreq, uTime * uSpeed)));
vec3 field = hue2rgb(hue);
// The gamma is the whole look: it crushes the midtones so most of the frame
// stays background and only the crests of the field open up.
float gray = pow(dot(field, vec3(0.2126, 0.7152, 0.0722)), uGamma);
float v = clamp(gray + uBias, 0.0, 1.0);
float radius = v * 0.5 * uCell;
float d = length(gl_FragCoord.xy - centre);
float mask = 1.0 - smoothstep(radius - 1.0, radius + 1.0, d);
gl_FragColor = vec4(mix(uBg, ramp(v), mask), 1.0);
}
`;
var uni = {};
var ready = false;
function compile(type, src) {
var sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
throw new Error("halftone-field shader: " + gl.getShaderInfoLog(sh));
}
return sh;
}
if (gl) {
var prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
throw new Error("halftone-field link: " + gl.getProgramInfoLog(prog));
}
gl.useProgram(prog);
var buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
var loc = gl.getAttribLocation(prog, "aPos");
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
[
"uRes",
"uTime",
"uFreq",
"uSpeed",
"uCell",
"uGamma",
"uBias",
"uPal0",
"uPal1",
"uPal2",
"uPal3",
"uBg",
].forEach(function (n) {
uni[n] = gl.getUniformLocation(prog, n);
});
gl.viewport(0, 0, W, H);
gl.uniform2f(uni.uRes, W, H);
gl.uniform1f(uni.uFreq, FREQ);
gl.uniform1f(uni.uSpeed, SPEED);
gl.uniform1f(uni.uCell, CELL);
gl.uniform1f(uni.uGamma, GAMMA);
gl.uniform1f(uni.uBias, BIAS);
gl.uniform3f(uni.uPal0, PAL[0][0], PAL[0][1], PAL[0][2]);
gl.uniform3f(uni.uPal1, PAL[1][0], PAL[1][1], PAL[1][2]);
gl.uniform3f(uni.uPal2, PAL[2][0], PAL[2][1], PAL[2][2]);
gl.uniform3f(uni.uPal3, PAL[3][0], PAL[3][1], PAL[3][2]);
gl.uniform3f(uni.uBg, BG[0], BG[1], BG[2]);
ready = true;
}
// Every frame is computed from t alone — the field is a closed-form
// function of position and time. No accumulator, no clock, no PRNG.
// `quantize` snaps t to a 30fps grid for the stepped LED-wall look.
function draw(t) {
if (!ready) return;
gl.uniform1f(uni.uTime, QUANTIZE ? Math.floor(t * 30) / 30 : t);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush();
}
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// The canvas is repainted from a property SETTER, not from onUpdate:
// gsap's seek(t) suppresses events by default, so an onUpdate callback
// silently never fires on a scrub and the canvas freezes on frame 0.
// Tweened values are always written during render, suppressed or not,
// so this fires on every seek — and hands us the frame time directly.
var driver = { _t: 0 };
Object.defineProperty(driver, "t", {
get: function () {
return this._t;
},
set: function (v) {
this._t = v;
draw(v);
},
});
tl.to(driver, { t: DUR, duration: DUR, ease: "none" }, 0);
window.__timelines["halftone-field"] = tl;
draw(0);
})();
</script>
</body>
</html>
```
</VariablesExplorer>
## Install
<InstallCommand command="npx hyperframes add halftone-field" />
That writes one file: `compositions/halftone-field.html`.
## Add it to your video
It runs for 10 seconds at 1920×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="halftone-field"
data-composition-src="compositions/halftone-field.html"
data-start="0"
data-duration="10"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Change how it looks
Set these CSS variables on the block:
- `--frequency` — Field frequency (1 = broad swells, 10 = fine mottle). Defaults to `2`.
- `--speed` — Flow speed (0 freezes the field). Defaults to `5`.
- `--cellsize` — Cell size (dot pitch, 1 = 6px, 100 = 60px). Defaults to `54`.
- `--gamma` — Gamma: midtone crush, the whole look. Low values flatten it to an even grey dot field. Defaults to `12`.
- `--palettebias` — Palette bias: floor under every dot's size and palette position. 0 lets the darks go fully black. Defaults to `2`.
- `--palette1` — Palette stop 1 (lows). Defaults to `#00AAFF`.
- `--palette2` — Palette stop 2. Defaults to `#C2FF67`.
- `--palette3` — Palette stop 3. Defaults to `#6600FF`.
- `--palette4` — Palette stop 4 (crests). Defaults to `#000000`.
- `--background` — Background. Defaults to `#000000`.
- `--quantize` — Quantise to 30fps for a stepped LED-wall look. Defaults to `false`. Options: `false`, `true`.
{/* hf:generated-footer */}
Tagged `webgl` `shader` `background` `texture` `halftone`.
## Related topics
- [Browse the complete Catalog](/catalog)
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
- [Build a richer composition](/go-further)
+4 -315
View File
@@ -3,13 +3,13 @@ title: "Sketched Frame"
description: "Media in a hand-drawn border with corner doodles and a handwritten caption; the unit tilts slightly and boils (images inside the block; keep live footage in the host)"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/hw-frame.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Sketched Frame preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-frame.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-frame.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add hw-frame" />
```bash Terminal
npx hyperframes add hw-frame
```
That writes `compositions/hw-frame.html`, plus 1 supporting file under `assets/fonts/`.
@@ -32,317 +32,6 @@ It runs for 8 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`hw-frame.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@font-face {
font-family: "Caveat";
src: url("assets/fonts/Caveat-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#hw-fr-root {
--hw-font-print: "Caveat", cursive;
--hw-font-script: "Caveat", cursive;
--hw-ink: #f4f2ec;
--hw-accent-warm: #ffb020;
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #1b1a1e;
font-family: var(--hw-font-print, "Caveat", cursive);
}
#hw-fr-media {
position: absolute;
overflow: hidden;
border-radius: 10px;
}
#hw-fr-media img,
#hw-fr-media .hw-fr-fill {
width: 100%;
height: 100%;
object-fit: cover;
}
.hw-fr-fill {
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 110px;
color: rgba(244, 242, 236, 0.9);
background: linear-gradient(150deg, #35604a 0%, #14231c 100%);
}
#hw-fr-svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
}
#hw-fr-svg path {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
#hw-fr-border {
stroke: var(--hw-ink);
stroke-width: 7;
}
.hw-fr-doodle {
stroke: var(--hw-accent-warm);
stroke-width: 5;
}
#hw-fr-caption {
position: absolute;
width: 100%;
text-align: center;
font-weight: 700;
font-size: 58px;
color: var(--hw-ink);
opacity: 0;
text-shadow: 0 2px 14px rgba(0, 0, 0, 0.4);
}
</style>
</head>
<body>
<div
id="hw-fr-root"
data-composition-id="hw-frame"
data-start="0"
data-duration="8"
data-width="1920"
data-height="1080"
>
<div id="hw-fr-media"></div>
<svg id="hw-fr-svg" viewBox="0 0 1920 1080"><path id="hw-fr-border"></path></svg>
<div id="hw-fr-caption"></div>
<div
id="hw-fr-drv"
class="clip"
data-start="0"
data-duration="8"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters ----
A sketched frame: content inside a hand-drawn border with
corner doodles + optional caption. IMAGES ONLY inside this block
(learning: the runtime never seeks media elements inside
sub-compositions) — for live footage keep the media element in
the HOST root positioned over the documented media slot below;
layering two framed clips = instant PiP. */
var CONFIG = {
src: null, // IMAGE path; null -> stand-in gradient (demo)
slot: { x: 480, y: 180, w: 960, h: 620 }, // the media window (documented for host-side footage)
caption: "our trip!",
doodles: true, // corner sparks
tilt: -1.2, // deg — the whole framed unit sits slightly rotated
seed: 14,
boil: { enabled: true, amp: 1.6, rot: 0.35, frameDrop: 3 },
animationIn: true,
animationOut: true,
};
var DUR = 8;
function clamp(v, lo, hi) {
return Math.min(hi, Math.max(lo, v));
}
var IN = clamp(DUR * 0.08, 0.3, 0.8);
var OUT = clamp(DUR * 0.06, 0.25, 0.6);
/* hw family runtime */
window.hwOnUpdate = function (tl, fn) {
if (!tl.__hwRenders) {
tl.__hwRenders = [];
tl.eventCallback("onUpdate", function () {
for (var i = 0; i < tl.__hwRenders.length; i++) tl.__hwRenders[i]();
});
}
tl.__hwRenders.push(fn);
fn();
};
window.hwHash = function (n, seed) {
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
return (x - Math.floor(x)) * 2 - 1;
};
window.hwBoil = function (tl, target, opts) {
opts = opts || {};
var amp = opts.amp !== undefined ? opts.amp : 1.6;
var rot = opts.rot !== undefined ? opts.rot : 0.5;
var fps = opts.fps || 30;
var drop = opts.frameDrop || 3;
var seed = opts.seed || 1;
var els = gsap.utils.toArray(target);
window.hwOnUpdate(tl, function () {
var step = Math.floor((tl.time() * fps) / drop);
for (var i = 0; i < els.length; i++) {
gsap.set(els[i], {
x: window.hwHash(step * 3 + i * 97, seed) * amp,
y: window.hwHash(step * 3 + 1 + i * 97, seed) * amp,
rotation: (opts.baseRot || 0) + window.hwHash(step * 3 + 2 + i * 97, seed) * rot,
});
}
});
};
window.hwWobbleRect = function (w, h, r, seed, amp) {
amp = amp === undefined ? 3.5 : amp;
function wob(n) {
return window.hwHash(n, seed) * amp;
}
var d = "M " + (r + wob(1)) + " " + wob(2);
d += " L " + (w / 2 + wob(3)) + " " + wob(4);
d += " L " + (w - r + wob(5)) + " " + wob(6);
d += " Q " + (w + wob(7)) + " " + wob(8) + " " + (w + wob(9)) + " " + (r + wob(10));
d += " L " + (w + wob(11)) + " " + (h / 2 + wob(12));
d += " L " + (w + wob(13)) + " " + (h - r + wob(14));
d +=
" Q " +
(w + wob(15)) +
" " +
(h + wob(16)) +
" " +
(w - r + wob(17)) +
" " +
(h + wob(18));
d += " L " + (w / 2 + wob(19)) + " " + (h + wob(20));
d += " L " + (r + wob(21)) + " " + (h + wob(22));
d += " Q " + wob(23) + " " + (h + wob(24)) + " " + wob(25) + " " + (h - r + wob(26));
d += " L " + wob(27) + " " + (h / 2 + wob(28));
d += " L " + wob(29) + " " + (r + wob(30));
d += " Q " + wob(31) + " " + wob(32) + " " + (r + wob(33)) + " " + wob(34);
return d;
};
var S = CONFIG.slot;
var media = document.getElementById("hw-fr-media");
media.style.left = S.x + "px";
media.style.top = S.y + "px";
media.style.width = S.w + "px";
media.style.height = S.h + "px";
if (CONFIG.src) {
var img = document.createElement("img");
img.src = CONFIG.src;
media.appendChild(img);
} else {
var d = document.createElement("div");
d.className = "hw-fr-fill";
d.textContent = "photo";
media.appendChild(d);
}
/* border drawn slightly outside the slot */
var NS = "http://www.w3.org/2000/svg";
var border = document.getElementById("hw-fr-border");
var m = 16;
border.setAttribute("d", window.hwWobbleRect(S.w + m * 2, S.h + m * 2, 18, CONFIG.seed, 5));
border.setAttribute("transform", "translate(" + (S.x - m) + " " + (S.y - m) + ")");
/* corner doodles: little three-spark bursts */
var svg = document.getElementById("hw-fr-svg");
var doodles = [];
if (CONFIG.doodles) {
[
[S.x - 40, S.y - 34],
[S.x + S.w + 40, S.y - 26],
[S.x + S.w + 36, S.y + S.h + 30],
].forEach(function (pt, i) {
var p = document.createElementNS(NS, "path");
p.setAttribute("class", "hw-fr-doodle");
p.setAttribute("id", "hw-fr-doodle-" + i);
var dd = "";
for (var k = 0; k < 3; k++) {
var a = -0.6 - k * 0.5 + window.hwHash(i * 9 + k, CONFIG.seed) * 0.2;
var l = 26 + window.hwHash(i * 9 + k + 4, CONFIG.seed) * 8;
dd +=
"M " +
pt[0] +
" " +
pt[1] +
" l " +
(Math.cos(a) * l).toFixed(1) +
" " +
(Math.sin(a) * l).toFixed(1) +
" ";
}
p.setAttribute("d", dd);
svg.appendChild(p);
doodles.push(p);
});
}
var caption = document.getElementById("hw-fr-caption");
caption.textContent = CONFIG.caption || "";
caption.style.top = S.y + S.h + 42 + "px";
var tl = gsap.timeline({ paused: true });
if (CONFIG.animationIn) {
gsap.set(media, { opacity: 0, scale: 0.96 });
tl.to(media, { opacity: 1, scale: 1, duration: IN * 1.4, ease: "power2.out" }, IN * 0.8);
}
var blen = border.getTotalLength();
gsap.set(border, { strokeDasharray: blen, strokeDashoffset: blen });
tl.to(border, { strokeDashoffset: 0, duration: 0.9, ease: "power2.inOut" }, 0.2);
doodles.forEach(function (p, i) {
var l = p.getTotalLength();
gsap.set(p, { strokeDasharray: l, strokeDashoffset: l });
tl.to(p, { strokeDashoffset: 0, duration: 0.3, ease: "power2.out" }, 1.1 + i * 0.12);
});
if (CONFIG.caption) tl.to(caption, { opacity: 1, duration: 0.4, ease: "power2.out" }, 1.4);
/* whole framed unit tilts + boils together */
var unit = [media, svg, caption];
gsap.set(unit, { rotation: CONFIG.tilt, transformOrigin: "50% 50%" });
if (CONFIG.boil.enabled) {
window.hwBoil(tl, unit, {
amp: CONFIG.boil.amp,
rot: CONFIG.boil.rot,
frameDrop: CONFIG.boil.frameDrop,
seed: CONFIG.seed,
baseRot: CONFIG.tilt,
});
}
if (CONFIG.animationOut) {
tl.to(unit, { opacity: 0, duration: OUT, ease: "power2.in" }, DUR - OUT - 0.05);
}
tl.set("#hw-fr-root", { visibility: "hidden" }, DUR - 0.02);
window.__timelines["hw-frame"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `handwritten` `frame` `media`.
+4 -206
View File
@@ -3,13 +3,13 @@ title: "Text Along a Path"
description: "Handwriting flowing along any SVG path with a per-character reveal; the path is the only geometry input"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/hw-path-text.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Text Along a Path preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-path-text.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-path-text.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add hw-path-text" />
```bash Terminal
npx hyperframes add hw-path-text
```
That writes `compositions/hw-path-text.html`, plus 1 supporting file under `assets/fonts/`.
@@ -32,208 +32,6 @@ It runs for 6 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`hw-path-text.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@font-face {
font-family: "Caveat";
src: url("assets/fonts/Caveat-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#hw-pt-root {
--hw-font-print: "Caveat", cursive;
--hw-font-script: "Caveat", cursive;
--hw-ink: #f4f2ec;
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent;
font-family: var(--hw-font-print, "Caveat", cursive);
}
#hw-pt-svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
}
/* The textPath carrier is geometry only — without fill:none it inherits
SVG's default black fill and renders as a solid blob under the text. */
#hw-pt-path {
fill: none;
}
#hw-pt-guide {
fill: none;
stroke: rgba(244, 242, 236, 0.22);
stroke-width: 3;
stroke-dasharray: 2 14;
stroke-linecap: round;
}
#hw-pt-svg text {
font-family: var(--hw-font-print, "Caveat", cursive);
font-weight: 700;
fill: var(--hw-ink);
}
#hw-pt-svg tspan {
opacity: 0;
}
</style>
</head>
<body>
<div
id="hw-pt-root"
data-composition-id="hw-path-text"
data-start="0"
data-duration="6"
data-width="1920"
data-height="1080"
>
<svg id="hw-pt-svg" viewBox="0 0 1920 1080">
<path id="hw-pt-path" d=""></path>
<path id="hw-pt-guide" d=""></path>
<text id="hw-pt-text" font-size="92">
<textPath id="hw-pt-tp" href="#hw-pt-path" startOffset="4%"></textPath>
</text>
</svg>
<div
id="hw-pt-drv"
class="clip"
data-start="0"
data-duration="6"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters ----
Handwriting along any path — custom path text, natively via
SVG textPath (no Fusion overlay). CONFIG.d is the whole design. */
var CONFIG = {
text: "come on this wild journey with me...",
d: "M 220 700 C 520 480, 900 820, 1230 560 S 1700 380, 1780 420",
fontSize: 92,
showGuide: true, // faint dotted guide under the writing
typeCharDelay: 0.05,
seed: 12,
boil: { enabled: true, amp: 1.4, rot: 0.3, frameDrop: 3 },
animationOut: true,
};
var DUR = 6;
function clamp(v, lo, hi) {
return Math.min(hi, Math.max(lo, v));
}
var OUT = clamp(DUR * 0.06, 0.25, 0.6);
/* hw family runtime */
window.hwOnUpdate = function (tl, fn) {
if (!tl.__hwRenders) {
tl.__hwRenders = [];
tl.eventCallback("onUpdate", function () {
for (var i = 0; i < tl.__hwRenders.length; i++) tl.__hwRenders[i]();
});
}
tl.__hwRenders.push(fn);
fn();
};
window.hwHash = function (n, seed) {
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
return (x - Math.floor(x)) * 2 - 1;
};
window.hwBoil = function (tl, target, opts) {
opts = opts || {};
var amp = opts.amp !== undefined ? opts.amp : 1.6;
var rot = opts.rot !== undefined ? opts.rot : 0.5;
var fps = opts.fps || 30;
var drop = opts.frameDrop || 3;
var seed = opts.seed || 1;
var els = gsap.utils.toArray(target);
window.hwOnUpdate(tl, function () {
var step = Math.floor((tl.time() * fps) / drop);
for (var i = 0; i < els.length; i++) {
gsap.set(els[i], {
x: window.hwHash(step * 3 + i * 97, seed) * amp,
y: window.hwHash(step * 3 + 1 + i * 97, seed) * amp,
rotation: (opts.baseRot || 0) + window.hwHash(step * 3 + 2 + i * 97, seed) * rot,
});
}
});
};
document.getElementById("hw-pt-path").setAttribute("d", CONFIG.d);
if (CONFIG.showGuide) document.getElementById("hw-pt-guide").setAttribute("d", CONFIG.d);
document.getElementById("hw-pt-text").setAttribute("font-size", CONFIG.fontSize);
/* per-char tspans along the path */
var NS = "http://www.w3.org/2000/svg";
var tp = document.getElementById("hw-pt-tp");
Array.from(CONFIG.text).forEach(function (ch) {
var t = document.createElementNS(NS, "tspan");
t.textContent = ch;
tp.appendChild(t);
});
var tl = gsap.timeline({ paused: true });
/* guide draws first, then the handwriting types along it */
var guide = document.getElementById("hw-pt-guide");
if (CONFIG.showGuide) {
var glen = guide.getTotalLength();
gsap.set(guide, { strokeDasharray: "2 14", strokeDashoffset: glen });
tl.to(guide, { strokeDashoffset: 0, duration: 0.9, ease: "power2.inOut" }, 0.2);
}
var chars = tp.querySelectorAll("tspan");
var delay = Math.min(CONFIG.typeCharDelay, 2.6 / Math.max(1, chars.length));
tl.to(chars, { opacity: 1, duration: 0.03, stagger: delay, ease: "none" }, 0.7);
if (CONFIG.boil.enabled) {
window.hwBoil(tl, "#hw-pt-svg", {
amp: CONFIG.boil.amp,
rot: CONFIG.boil.rot,
frameDrop: CONFIG.boil.frameDrop,
seed: CONFIG.seed,
});
}
if (CONFIG.animationOut) {
tl.to("#hw-pt-svg", { opacity: 0, duration: OUT, ease: "power2.in" }, DUR - OUT - 0.05);
}
tl.set("#hw-pt-root", { visibility: "hidden" }, DUR - 0.02);
window.__timelines["hw-path-text"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `handwritten` `typography` `path` `overlay`.
+4 -330
View File
@@ -3,13 +3,13 @@ title: "Hand-Drawn Flowchart"
description: "Wobbled boxes with handwritten labels joined by curved connectors, drawing on in sequence from a node list"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/hw-pipeline.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Hand-Drawn Flowchart preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-pipeline.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-pipeline.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add hw-pipeline" />
```bash Terminal
npx hyperframes add hw-pipeline
```
That writes `compositions/hw-pipeline.html`, plus 1 supporting file under `assets/fonts/`.
@@ -32,332 +32,6 @@ It runs for 7 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`hw-pipeline.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@font-face {
font-family: "Caveat";
src: url("assets/fonts/Caveat-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#hw-pl-root {
--hw-font-print: "Caveat", cursive;
--hw-font-script: "Caveat", cursive;
--hw-ink: #f4f2ec;
/* Warm marker accent, matching hw-frame / hw-title / hw-underline. The
previous #6d6dff read as a default UI blue against the family's ink. */
--hw-accent: #ffb020;
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent; /* overlays footage or boards */
font-family: var(--hw-font-print, "Caveat", cursive);
}
#hw-pl-svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
}
#hw-pl-svg path {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.hw-pl-box {
stroke: var(--hw-ink);
stroke-width: 6;
}
.hw-pl-conn {
stroke: var(--hw-accent);
stroke-width: 5;
}
.hw-pl-label {
position: absolute;
text-align: center;
font-weight: 700;
color: var(--hw-ink);
white-space: nowrap;
opacity: 0;
text-shadow: 0 2px 14px rgba(0, 0, 0, 0.4);
}
</style>
</head>
<body>
<div
id="hw-pl-root"
data-composition-id="hw-pipeline"
data-start="0"
data-duration="7"
data-width="1920"
data-height="1080"
>
<svg id="hw-pl-svg" viewBox="0 0 1920 1080"></svg>
<div id="hw-pl-labels"></div>
<div
id="hw-pl-drv"
class="clip"
data-start="0"
data-duration="7"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters ----
A flowchart "pipeline": wobbled boxes joined by curved
connectors, drawing on in sequence, all boiling. */
var CONFIG = {
nodes: [{ label: "Idea" }, { label: "Record" }, { label: "Shine!" }],
boxW: 320,
boxH: 170,
gap: 190, // px between boxes
y: 455, // top of the row; x centers automatically
fontSize: 56,
seed: 11,
boil: { enabled: true, amp: 1.6, rot: 0.4, frameDrop: 3 },
animationOut: true,
};
var DUR = 7;
function clamp(v, lo, hi) {
return Math.min(hi, Math.max(lo, v));
}
var OUT = clamp(DUR * 0.06, 0.25, 0.6);
/* hw family runtime */
window.hwOnUpdate = function (tl, fn) {
if (!tl.__hwRenders) {
tl.__hwRenders = [];
tl.eventCallback("onUpdate", function () {
for (var i = 0; i < tl.__hwRenders.length; i++) tl.__hwRenders[i]();
});
}
tl.__hwRenders.push(fn);
fn();
};
window.hwHash = function (n, seed) {
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
return (x - Math.floor(x)) * 2 - 1;
};
window.hwBoil = function (tl, target, opts) {
opts = opts || {};
var amp = opts.amp !== undefined ? opts.amp : 1.6;
var rot = opts.rot !== undefined ? opts.rot : 0.5;
var fps = opts.fps || 30;
var drop = opts.frameDrop || 3;
var seed = opts.seed || 1;
var els = gsap.utils.toArray(target);
window.hwOnUpdate(tl, function () {
var step = Math.floor((tl.time() * fps) / drop);
for (var i = 0; i < els.length; i++) {
gsap.set(els[i], {
x: window.hwHash(step * 3 + i * 97, seed) * amp,
y: window.hwHash(step * 3 + 1 + i * 97, seed) * amp,
rotation: (opts.baseRot || 0) + window.hwHash(step * 3 + 2 + i * 97, seed) * rot,
});
}
});
};
window.hwWobbleRect = function (w, h, r, seed, amp) {
amp = amp === undefined ? 3.5 : amp;
function wob(n) {
return window.hwHash(n, seed) * amp;
}
var d = "M " + (r + wob(1)) + " " + wob(2);
d += " L " + (w / 2 + wob(3)) + " " + wob(4);
d += " L " + (w - r + wob(5)) + " " + wob(6);
d += " Q " + (w + wob(7)) + " " + wob(8) + " " + (w + wob(9)) + " " + (r + wob(10));
d += " L " + (w + wob(11)) + " " + (h / 2 + wob(12));
d += " L " + (w + wob(13)) + " " + (h - r + wob(14));
d +=
" Q " +
(w + wob(15)) +
" " +
(h + wob(16)) +
" " +
(w - r + wob(17)) +
" " +
(h + wob(18));
d += " L " + (w / 2 + wob(19)) + " " + (h + wob(20));
d += " L " + (r + wob(21)) + " " + (h + wob(22));
d += " Q " + wob(23) + " " + (h + wob(24)) + " " + wob(25) + " " + (h - r + wob(26));
d += " L " + wob(27) + " " + (h / 2 + wob(28));
d += " L " + wob(29) + " " + (r + wob(30));
d += " Q " + wob(31) + " " + wob(32) + " " + (r + wob(33)) + " " + wob(34);
return d;
};
var NS = "http://www.w3.org/2000/svg";
var svg = document.getElementById("hw-pl-svg");
var labels = document.getElementById("hw-pl-labels");
var n = CONFIG.nodes.length;
var totalW = n * CONFIG.boxW + (n - 1) * CONFIG.gap;
var x0 = (1920 - totalW) / 2;
var boxes = [],
conns = [],
labelEls = [];
CONFIG.nodes.forEach(function (node, i) {
var bx = x0 + i * (CONFIG.boxW + CONFIG.gap);
/* outer g owns the translate (GSAP transforms REPLACE the SVG
transform attribute — never boil the element that carries it);
inner g is the boil target */
var gOuter = document.createElementNS(NS, "g");
gOuter.setAttribute("transform", "translate(" + bx + " " + CONFIG.y + ")");
var g = document.createElementNS(NS, "g");
g.setAttribute("id", "hw-pl-box-g-" + i);
gOuter.appendChild(g);
var p = document.createElementNS(NS, "path");
p.setAttribute("class", "hw-pl-box");
p.setAttribute("id", "hw-pl-box-" + i);
p.setAttribute("d", window.hwWobbleRect(CONFIG.boxW, CONFIG.boxH, 24, CONFIG.seed + i));
g.appendChild(p);
svg.appendChild(gOuter);
boxes.push(p);
var label = document.createElement("div");
label.className = "hw-pl-label";
label.id = "hw-pl-label-" + i;
label.textContent = node.label;
label.style.left = bx + "px";
label.style.top = CONFIG.y + CONFIG.boxH / 2 - CONFIG.fontSize * 0.62 + "px";
label.style.width = CONFIG.boxW + "px";
label.style.fontSize = CONFIG.fontSize + "px";
labels.appendChild(label);
labelEls.push(label);
if (i < n - 1) {
var cx = bx + CONFIG.boxW,
cy = CONFIG.y + CONFIG.boxH / 2;
var ex = cx + CONFIG.gap,
ey = cy;
var c = document.createElementNS(NS, "path");
c.setAttribute("class", "hw-pl-conn");
c.setAttribute("id", "hw-pl-conn-" + i);
var sx = cx + 10,
tx = ex - 10;
var qx = cx + CONFIG.gap / 2 + window.hwHash(i * 9 + 2, CONFIG.seed) * 16,
qy = cy - 46 - window.hwHash(i * 9 + 3, CONFIG.seed) * 12;
/* Barbs follow the curve's incoming tangent. A quadratic's tangent
at its end is (end - control); barbs fixed to horizontal left the
head detached from a curve that arrives descending. Symmetric
length and angle, so it reads as one arrowhead. */
var ang = Math.atan2(ey - qy, tx - qx) + Math.PI;
var BARB = 26,
SPREAD = 0.42;
var b1x = tx + BARB * Math.cos(ang - SPREAD),
b1y = ey + BARB * Math.sin(ang - SPREAD);
var b2x = tx + BARB * Math.cos(ang + SPREAD),
b2y = ey + BARB * Math.sin(ang + SPREAD);
c.setAttribute(
"d",
"M " +
sx +
" " +
cy +
" Q " +
qx +
" " +
qy +
" " +
tx +
" " +
ey +
" M " +
tx +
" " +
ey +
" L " +
b1x +
" " +
b1y +
" M " +
tx +
" " +
ey +
" L " +
b2x +
" " +
b2y,
);
svg.appendChild(c);
conns.push(c);
}
});
var tl = gsap.timeline({ paused: true });
/* sequence: box draws -> label -> connector -> next box... */
var t = 0.3;
CONFIG.nodes.forEach(function (_, i) {
var p = boxes[i];
var len = p.getTotalLength();
gsap.set(p, { strokeDasharray: len, strokeDashoffset: len });
tl.to(p, { strokeDashoffset: 0, duration: 0.65, ease: "power2.inOut" }, t);
tl.to(labelEls[i], { opacity: 1, duration: 0.35, ease: "power2.out" }, t + 0.45);
t += 0.7;
if (i < conns.length) {
var c = conns[i];
var cl = c.getTotalLength();
gsap.set(c, { strokeDasharray: cl, strokeDashoffset: cl });
tl.to(c, { strokeDashoffset: 0, duration: 0.45, ease: "power2.inOut" }, t);
t += 0.5;
}
});
if (CONFIG.boil.enabled) {
var targets = boxes.map(function (_, i) {
return "#hw-pl-box-g-" + i;
});
window.hwBoil(tl, targets.concat([".hw-pl-conn", ".hw-pl-label"]), {
amp: CONFIG.boil.amp,
rot: CONFIG.boil.rot,
frameDrop: CONFIG.boil.frameDrop,
seed: CONFIG.seed,
});
}
if (CONFIG.animationOut) {
tl.to([svg, labels], { opacity: 0, duration: OUT, ease: "power2.in" }, DUR - OUT - 0.05);
}
tl.set("#hw-pl-root", { visibility: "hidden" }, DUR - 0.02);
window.__timelines["hw-pipeline"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `handwritten` `diagram` `data-viz` `overlay`.
+4 -165
View File
@@ -3,13 +3,13 @@ title: "Scribble Transition"
description: "Overlay transition: scribble bands accumulate to cover the frame with a solid backstop, the host cuts beneath, and the scribble clears"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/hw-scribble-transition.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Scribble Transition preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-scribble-transition.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-scribble-transition.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add hw-scribble-transition" />
```bash Terminal
npx hyperframes add hw-scribble-transition
```
That writes one file: `compositions/hw-scribble-transition.html`.
@@ -32,167 +32,6 @@ It runs for 2.2 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`hw-scribble-transition.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#hw-st-root {
/* Warm marker accent, consistent with the rest of the hw family.
Override per use via CONFIG.color. */
--hw-accent: #ffb020;
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent;
}
#hw-st-svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
#hw-st-svg path {
fill: none;
stroke: var(--hw-accent);
stroke-linecap: round;
stroke-linejoin: round;
}
/* solid backstop that guarantees full coverage at the midpoint */
#hw-st-solid {
position: absolute;
inset: 0;
background: var(--hw-accent);
opacity: 0;
}
</style>
</head>
<body>
<div
id="hw-st-root"
data-composition-id="hw-scribble-transition"
data-start="0"
data-duration="2.2"
data-width="1920"
data-height="1080"
>
<svg id="hw-st-svg" viewBox="0 0 1920 1080" data-layout-allow-overflow></svg>
<div id="hw-st-solid"></div>
<div
id="hw-st-drv"
class="clip"
data-start="0"
data-duration="2.2"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters ----
Overlay-transition (the mk-clone-wall model): fat scribble strokes
sweep across and cover the frame, a solid backstop guarantees full
coverage at the midpoint (the host hard-cuts beneath, ~0.9-1.4s
local), then everything clears. Place spanning the host's cut. */
var CONFIG = {
color: null, // css color; null = --hw-accent (warm marker)
bands: 7, // horizontal scribble bands
strokeScale: 1.0,
seed: 17,
};
var W = 1920,
H = 1080,
DUR = 2.2;
window.hwHash = function (n, seed) {
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
return (x - Math.floor(x)) * 2 - 1;
};
var root = document.getElementById("hw-st-root");
if (CONFIG.color) root.style.setProperty("--hw-accent", CONFIG.color);
var NS = "http://www.w3.org/2000/svg";
var svg = document.getElementById("hw-st-svg");
/* fat zigzag bands, each covering a horizontal slice edge-to-edge */
var bandH = H / CONFIG.bands;
var paths = [];
for (var b = 0; b < CONFIG.bands; b++) {
var y0 = b * bandH + bandH / 2;
var d = "M -80 " + (y0 + window.hwHash(b * 17, CONFIG.seed) * 20).toFixed(1);
var zigs = 7;
for (var i = 1; i <= zigs; i++) {
var x = (W + 160) * (i / zigs) - 80;
var y =
y0 + (i % 2 ? -1 : 1) * bandH * 0.32 + window.hwHash(b * 17 + i, CONFIG.seed) * 16;
d += " L " + x.toFixed(1) + " " + y.toFixed(1);
}
var p = document.createElementNS(NS, "path");
p.setAttribute("d", d);
p.setAttribute("stroke-width", Math.ceil(bandH * 0.85 * CONFIG.strokeScale));
p.setAttribute("id", "hw-st-band-" + b);
svg.appendChild(p);
paths.push(p);
}
var tl = gsap.timeline({ paused: true });
/* 0-0.85: bands scribble on (staggered, alternating direction) */
paths.forEach(function (p, i) {
var len = p.getTotalLength();
gsap.set(p, { strokeDasharray: len, strokeDashoffset: i % 2 ? -len : len });
tl.to(p, { strokeDashoffset: 0, duration: 0.55, ease: "power2.in" }, 0.04 * i);
});
/* 0.8: solid backstop snaps in beneath the finished scribble — full
coverage guaranteed 0.85-1.35 (the host's cut window) */
tl.to("#hw-st-solid", { opacity: 1, duration: 0.12, ease: "none" }, 0.8);
/* 1.35-1.95: clear — bands scribble off the other way, backstop lifts */
tl.to("#hw-st-solid", { opacity: 0, duration: 0.15, ease: "none" }, 1.42);
paths.forEach(function (p, i) {
var len = p.getTotalLength();
tl.to(
p,
{ strokeDashoffset: i % 2 ? len : -len, duration: 0.5, ease: "power2.out" },
1.42 + 0.035 * i,
);
});
/* hard kill the moment the last band clears (learning: kill roots
when the final fade completes, not at DUR) */
tl.set(root, { visibility: "hidden" }, 2.17);
window.__timelines["hw-scribble-transition"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `handwritten` `transition` `wipe`.
+4 -298
View File
@@ -3,13 +3,13 @@ title: "Speech Bubble"
description: "Hand-drawn speech bubble with a positionable tail: outline draws on, paper fill fades in, handwritten text types per character; boils"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/hw-text-cloud.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Speech Bubble preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-text-cloud.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-text-cloud.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add hw-text-cloud" />
```bash Terminal
npx hyperframes add hw-text-cloud
```
That writes `compositions/hw-text-cloud.html`, plus 1 supporting file under `assets/fonts/`.
@@ -32,300 +32,6 @@ It runs for 6 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`hw-text-cloud.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@font-face {
font-family: "Caveat";
src: url("assets/fonts/Caveat-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#hw-tc-root {
/* ---- hw tokens ---- */
--hw-font-print: "Caveat", cursive;
--hw-font-script: "Caveat", cursive;
--hw-ink: #f4f2ec;
--hw-ink-dark: #26241f;
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent; /* overlays footage */
font-family: var(--hw-font-print, "Caveat", cursive);
}
#hw-tc-wrap {
position: absolute;
pointer-events: none;
}
#hw-tc-wrap svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
}
#hw-tc-fill {
fill: var(--hw-ink);
opacity: 0;
}
#hw-tc-stroke {
fill: none;
stroke: var(--hw-ink-dark);
stroke-width: 4;
stroke-linecap: round;
stroke-linejoin: round;
}
#hw-tc-text {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-weight: 700;
color: var(--hw-ink-dark);
line-height: 1.05;
}
#hw-tc-text .ch {
opacity: 0;
}
</style>
</head>
<body>
<div
id="hw-tc-root"
data-composition-id="hw-text-cloud"
data-start="0"
data-duration="6"
data-width="1920"
data-height="1080"
>
<div id="hw-tc-wrap">
<svg id="hw-tc-svg">
<path id="hw-tc-fill"></path>
<path id="hw-tc-stroke"></path>
</svg>
<div id="hw-tc-text"></div>
</div>
<div
id="hw-tc-drv"
class="clip"
data-start="0"
data-duration="6"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters ----
Speech bubble with a movable tip (pointable at any subject): wobbled
cloud, draw-on outline, paper fill, per-char handwritten text. */
var CONFIG = {
text: "what are you looking at?",
x: 620, // wrapper left
y: 180, // wrapper top
w: 560,
h: 300,
fontSize: 58,
tip: { side: "bottom-left", len: 90 }, // "bottom-left"|"bottom-right"
seed: 6,
boil: { enabled: true, amp: 1.6, rot: 0.4, frameDrop: 3 },
typeCharDelay: 0.03,
animationIn: true,
animationOut: true,
};
var DUR = 6;
function clamp(v, lo, hi) {
return Math.min(hi, Math.max(lo, v));
}
var OUT = clamp(DUR * 0.06, 0.25, 0.6);
/* hw family runtime */
window.hwOnUpdate = function (tl, fn) {
if (!tl.__hwRenders) {
tl.__hwRenders = [];
tl.eventCallback("onUpdate", function () {
for (var i = 0; i < tl.__hwRenders.length; i++) tl.__hwRenders[i]();
});
}
tl.__hwRenders.push(fn);
fn();
};
window.hwHash = function (n, seed) {
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
return (x - Math.floor(x)) * 2 - 1;
};
window.hwBoil = function (tl, target, opts) {
opts = opts || {};
var amp = opts.amp !== undefined ? opts.amp : 1.6;
var rot = opts.rot !== undefined ? opts.rot : 0.5;
var fps = opts.fps || 30;
var drop = opts.frameDrop || 3;
var seed = opts.seed || 1;
var els = gsap.utils.toArray(target);
window.hwOnUpdate(tl, function () {
var step = Math.floor((tl.time() * fps) / drop);
for (var i = 0; i < els.length; i++) {
gsap.set(els[i], {
x: window.hwHash(step * 3 + i * 97, seed) * amp,
y: window.hwHash(step * 3 + 1 + i * 97, seed) * amp,
rotation: (opts.baseRot || 0) + window.hwHash(step * 3 + 2 + i * 97, seed) * rot,
});
}
});
};
/* wobbled cloud with a tail tip */
function cloudPath(W, H, tip, seed) {
var cx = W / 2,
cy = H / 2 - 14,
rx = W / 2 - 12,
ry = H / 2 - 34;
var N = 16,
pts = [];
for (var i = 0; i < N; i++) {
var a = (i / N) * Math.PI * 2;
var wr = 1 + window.hwHash(i * 11 + 3, seed) * 0.06;
pts.push([cx + Math.cos(a) * rx * wr, cy + Math.sin(a) * ry * wr]);
}
var mx = (pts[0][0] + pts[N - 1][0]) / 2,
my = (pts[0][1] + pts[N - 1][1]) / 2;
var d = "M" + mx.toFixed(1) + " " + my.toFixed(1);
for (var j = 0; j < N; j++) {
var p = pts[j],
q = pts[(j + 1) % N];
d +=
" Q" +
p[0].toFixed(1) +
" " +
p[1].toFixed(1) +
" " +
((p[0] + q[0]) / 2).toFixed(1) +
" " +
((p[1] + q[1]) / 2).toFixed(1);
}
d += "Z";
/* tail */
var left = tip.side !== "bottom-right";
var bx = cx + (left ? -rx * 0.45 : rx * 0.45),
by = cy + ry * 0.92;
var tx = bx + (left ? -tip.len * 0.5 : tip.len * 0.5),
ty = by + tip.len;
d +=
" M " +
(bx - 26).toFixed(1) +
" " +
(by - 6).toFixed(1) +
" Q " +
(bx - 8 + window.hwHash(31, seed) * 6).toFixed(1) +
" " +
((by + ty) / 2).toFixed(1) +
" " +
tx.toFixed(1) +
" " +
ty.toFixed(1) +
" Q " +
(bx + 18).toFixed(1) +
" " +
((by + ty) / 2).toFixed(1) +
" " +
(bx + 30).toFixed(1) +
" " +
(by - 2).toFixed(1);
return d;
}
var wrap = document.getElementById("hw-tc-wrap");
wrap.style.left = CONFIG.x + "px";
wrap.style.top = CONFIG.y + "px";
wrap.style.width = CONFIG.w + "px";
wrap.style.height = CONFIG.h + "px";
var svg = document.getElementById("hw-tc-svg");
svg.setAttribute("viewBox", "0 0 " + CONFIG.w + " " + CONFIG.h);
var d = cloudPath(CONFIG.w, CONFIG.h, CONFIG.tip, CONFIG.seed);
document.getElementById("hw-tc-fill").setAttribute("d", d);
document.getElementById("hw-tc-stroke").setAttribute("d", d);
var text = document.getElementById("hw-tc-text");
text.style.left = CONFIG.w * 0.12 + "px";
text.style.top = CONFIG.h * 0.1 + "px";
text.style.width = CONFIG.w * 0.76 + "px";
text.style.height = CONFIG.h * 0.62 + "px";
text.style.fontSize = CONFIG.fontSize + "px";
var inner = document.createElement("div");
Array.from(CONFIG.text).forEach(function (ch) {
var s = document.createElement("span");
s.className = "ch";
s.textContent = ch;
inner.appendChild(s);
});
text.appendChild(inner);
var tl = gsap.timeline({ paused: true });
/* draw outline, fade fill, type text */
var stroke = document.getElementById("hw-tc-stroke");
var len = stroke.getTotalLength();
gsap.set(stroke, { strokeDasharray: len, strokeDashoffset: len });
var t0 = CONFIG.animationIn ? 0.3 : 0;
if (CONFIG.animationIn) {
tl.to(stroke, { strokeDashoffset: 0, duration: 0.8, ease: "power2.inOut" }, t0);
tl.to("#hw-tc-fill", { opacity: 0.94, duration: 0.4, ease: "power2.out" }, t0 + 0.55);
} else {
gsap.set(stroke, { strokeDashoffset: 0 });
gsap.set("#hw-tc-fill", { opacity: 0.94 });
}
var chars = text.querySelectorAll(".ch");
var delay = Math.min(CONFIG.typeCharDelay, 1.8 / Math.max(1, chars.length));
tl.to(chars, { opacity: 1, duration: 0.02, stagger: delay, ease: "none" }, t0 + 0.9);
if (CONFIG.boil.enabled) {
window.hwBoil(tl, "#hw-tc-wrap", {
amp: CONFIG.boil.amp,
rot: CONFIG.boil.rot,
frameDrop: CONFIG.boil.frameDrop,
seed: CONFIG.seed,
});
}
if (CONFIG.animationOut) {
tl.to(wrap, { opacity: 0, duration: OUT, ease: "power2.in" }, DUR - OUT - 0.05);
}
tl.set("#hw-tc-root", { visibility: "hidden" }, DUR - 0.02);
window.__timelines["hw-text-cloud"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `handwritten` `speech-bubble` `overlay`.
+4 -270
View File
@@ -3,13 +3,13 @@ title: "Handwritten Title"
description: "Handwritten display text with a scalar-driven word-by-word highlight sweep and a seeded squiggle underline sized to the text; boils"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/hw-title.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Handwritten Title preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-title.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-title.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add hw-title" />
```bash Terminal
npx hyperframes add hw-title
```
That writes `compositions/hw-title.html`, plus 1 supporting file under `assets/fonts/`.
@@ -32,272 +32,6 @@ It runs for 6 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`hw-title.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@font-face {
font-family: "Caveat";
src: url("assets/fonts/Caveat-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#hw-t-root {
/* ---- hw tokens ---- */
--hw-font-print: "Caveat", cursive;
--hw-font-script: "Caveat", cursive;
--hw-ink: #f4f2ec;
--hw-ink-dark: #26241f;
--hw-accent-warm: #ffb020;
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent; /* overlays footage or a board */
font-family: var(--hw-font-print, "Caveat", cursive);
}
#hw-t-wrap {
position: absolute;
pointer-events: none;
}
#hw-t-line {
font-weight: 700;
line-height: 1.1;
white-space: nowrap;
text-shadow: 0 2px 18px rgba(0, 0, 0, 0.4);
}
#hw-t-line .w {
display: inline;
}
#hw-t-under {
overflow: visible;
display: block;
}
#hw-t-under path {
fill: none;
stroke: var(--hw-accent-warm);
stroke-width: 7;
stroke-linecap: round;
}
</style>
</head>
<body>
<div
id="hw-t-root"
data-composition-id="hw-title"
data-start="0"
data-duration="6"
data-width="1920"
data-height="1080"
>
<div id="hw-t-wrap">
<div id="hw-t-line"></div>
<svg id="hw-t-under" width="10" height="34"><path id="hw-t-under-path"></path></svg>
</div>
<div
id="hw-t-drv"
class="clip"
data-start="0"
data-duration="6"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters ----
Handwritten title with the highlight-sweep scalar (the shared
mk-callout-highlight mechanic) + squiggle underline + the boil.
Demonstrates the dispatcher composing sweep + boil on one
timeline (learning: one onUpdate — everything through hwOnUpdate). */
var CONFIG = {
text: "shine like the star you are",
fontSize: 120,
scheme: "chalk", // "chalk" (white, over footage) | "ink" (dark, boards)
sweep: { enabled: true, start: 0.8, end: 3.2 },
underline: { enabled: true, wobble: 6 },
boil: { enabled: true, amp: 1.4, rot: 0.3, frameDrop: 3 },
seed: 8,
x: null, // null = centered
y: null,
animationIn: true,
animationOut: true,
};
var DUR = 6;
function clamp(v, lo, hi) {
return Math.min(hi, Math.max(lo, v));
}
var IN = clamp(DUR * 0.08, 0.3, 0.8);
var OUT = clamp(DUR * 0.06, 0.25, 0.6);
/* hw family runtime */
window.hwOnUpdate = function (tl, fn) {
if (!tl.__hwRenders) {
tl.__hwRenders = [];
tl.eventCallback("onUpdate", function () {
for (var i = 0; i < tl.__hwRenders.length; i++) tl.__hwRenders[i]();
});
}
tl.__hwRenders.push(fn);
fn();
};
window.hwHash = function (n, seed) {
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
return (x - Math.floor(x)) * 2 - 1;
};
window.hwBoil = function (tl, target, opts) {
opts = opts || {};
var amp = opts.amp !== undefined ? opts.amp : 1.6;
var rot = opts.rot !== undefined ? opts.rot : 0.5;
var fps = opts.fps || 30;
var drop = opts.frameDrop || 3;
var seed = opts.seed || 1;
var els = gsap.utils.toArray(target);
window.hwOnUpdate(tl, function () {
var step = Math.floor((tl.time() * fps) / drop);
for (var i = 0; i < els.length; i++) {
gsap.set(els[i], {
x: window.hwHash(step * 3 + i * 97, seed) * amp,
y: window.hwHash(step * 3 + 1 + i * 97, seed) * amp,
rotation: (opts.baseRot || 0) + window.hwHash(step * 3 + 2 + i * 97, seed) * rot,
});
}
});
};
var FULL = CONFIG.scheme === "ink" ? "#26241f" : "#f4f2ec";
var FAINT = CONFIG.scheme === "ink" ? "rgba(38,36,31,0.3)" : "rgba(244,242,236,0.32)";
var wrap = document.getElementById("hw-t-wrap");
var line = document.getElementById("hw-t-line");
line.style.fontSize = CONFIG.fontSize + "px";
/* word spans */
var wordEls = [];
CONFIG.text.split(/\s+/).forEach(function (w, i) {
if (i > 0) line.appendChild(document.createTextNode(" "));
var span = document.createElement("span");
span.className = "w";
span.textContent = w;
span.style.color = CONFIG.sweep.enabled ? FAINT : FULL;
line.appendChild(span);
wordEls.push(span);
});
/* measure + position; build squiggle underline to match width */
var rect = line.getBoundingClientRect();
wrap.style.left =
(CONFIG.x === null ? Math.round((1920 - rect.width) / 2) : CONFIG.x) + "px";
wrap.style.top =
(CONFIG.y === null ? Math.round((1080 - rect.height) / 2) - 20 : CONFIG.y) + "px";
var underPath = document.getElementById("hw-t-under-path");
if (CONFIG.underline.enabled) {
var uw = Math.round(rect.width);
var svg = document.getElementById("hw-t-under");
svg.setAttribute("width", uw);
svg.setAttribute("viewBox", "0 0 " + uw + " 34");
var segs = Math.max(6, Math.round(uw / 90));
var d = "M 2 " + (16 + window.hwHash(0, CONFIG.seed) * 4).toFixed(1);
for (var i = 1; i <= segs; i++) {
var x = (uw / segs) * i;
var y = 16 + window.hwHash(i * 5 + 1, CONFIG.seed) * CONFIG.underline.wobble;
var cxp = x - uw / segs / 2 + window.hwHash(i * 5 + 2, CONFIG.seed) * 10;
var cyp = 16 + (i % 2 ? -1 : 1) * (8 + window.hwHash(i * 5 + 3, CONFIG.seed) * 4);
d +=
" Q " +
cxp.toFixed(1) +
" " +
cyp.toFixed(1) +
" " +
x.toFixed(1) +
" " +
y.toFixed(1);
}
underPath.setAttribute("d", d);
} else {
document.getElementById("hw-t-under").style.display = "none";
}
var tl = gsap.timeline({ paused: true });
if (CONFIG.animationIn) {
gsap.set(wrap, { opacity: 0 });
tl.to(wrap, { opacity: 1, duration: IN, ease: "power2.out" }, 0.1);
}
/* the sweep scalar — registered through the dispatcher so it
coexists with the boil on the same timeline */
if (CONFIG.sweep.enabled) {
var mix = gsap.utils.interpolate(FAINT, FULL);
var st = { p: 0 };
window.hwOnUpdate(tl, function () {
var k = st.p * wordEls.length;
for (var i = 0; i < wordEls.length; i++) {
var f = Math.max(0, Math.min(1, k - i));
wordEls[i].style.color = mix(f);
}
});
tl.to(
st,
{ p: 1, duration: CONFIG.sweep.end - CONFIG.sweep.start, ease: "none" },
CONFIG.sweep.start,
);
}
/* underline draws as the sweep lands */
if (CONFIG.underline.enabled) {
var len = underPath.getTotalLength();
gsap.set(underPath, { strokeDasharray: len, strokeDashoffset: len });
var uAt = CONFIG.sweep.enabled ? CONFIG.sweep.end - 0.2 : 0.8;
tl.to(underPath, { strokeDashoffset: 0, duration: 0.7, ease: "power2.inOut" }, uAt);
}
if (CONFIG.boil.enabled) {
window.hwBoil(tl, "#hw-t-wrap", {
amp: CONFIG.boil.amp,
rot: CONFIG.boil.rot,
frameDrop: CONFIG.boil.frameDrop,
seed: CONFIG.seed,
});
}
if (CONFIG.animationOut) {
tl.to(wrap, { opacity: 0, duration: OUT, ease: "power2.in" }, DUR - OUT - 0.05);
}
tl.set("#hw-t-root", { visibility: "hidden" }, DUR - 0.02);
window.__timelines["hw-title"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `handwritten` `typography` `title-card` `overlay`.
+14 -300
View File
@@ -3,320 +3,34 @@ title: "Instagram Follow"
description: "Animated Instagram follow overlay with profile card and follow button"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/instagram-follow.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Instagram Follow preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/instagram-follow.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/instagram-follow.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add instagram-follow" />
```bash Terminal
npx hyperframes add instagram-follow
```
That writes `compositions/instagram-follow.html`, plus 1 supporting file under `assets/`.
## Source
## Add it to your video
<Accordion title={`instagram-follow.html`}>
It runs for 4.5 seconds at 1080×1920. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;700&display=block"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1080px;
height: 1920px;
font-family: "DM Sans", sans-serif;
}
[data-composition-id="instagram-follow"] .lower-third {
position: absolute;
bottom: 160px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 30px;
background: #1a1a1a;
border-radius: 75px;
padding: 25px 40px 25px 25px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.4);
}
[data-composition-id="instagram-follow"] .avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
border: 3px solid #333;
}
[data-composition-id="instagram-follow"] .profile-info {
display: flex;
flex-direction: column;
gap: 2px;
margin-right: 20px;
}
[data-composition-id="instagram-follow"] .name-row {
display: flex;
align-items: center;
gap: 10px;
}
[data-composition-id="instagram-follow"] .display-name {
font-size: 42px;
font-weight: 700;
color: #ffffff;
line-height: 1.3;
letter-spacing: -0.01em;
}
[data-composition-id="instagram-follow"] .verified-badge {
width: 34px;
height: 34px;
flex-shrink: 0;
}
[data-composition-id="instagram-follow"] .verified-badge svg {
width: 34px;
height: 34px;
display: block;
}
[data-composition-id="instagram-follow"] .handle {
font-size: 28px;
font-weight: 400;
color: #a0a0a0;
line-height: 1.3;
}
[data-composition-id="instagram-follow"] .follower-count {
font-size: 25px;
font-weight: 400;
color: #737373;
line-height: 1.3;
}
[data-composition-id="instagram-follow"] .follow-btn {
position: relative;
width: 250px;
height: 80px;
border-radius: 40px;
background: #0095f6;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
[data-composition-id="instagram-follow"] .btn-text {
position: absolute;
font-family: "DM Sans", sans-serif;
font-size: 30px;
font-weight: 700;
color: #ffffff;
letter-spacing: 0.02em;
white-space: nowrap;
}
[data-composition-id="instagram-follow"] .btn-text-follow {
opacity: 1;
}
[data-composition-id="instagram-follow"] .btn-text-following {
opacity: 0;
display: flex;
align-items: center;
gap: 8px;
}
[data-composition-id="instagram-follow"] .chevron-icon {
display: inline-block;
width: 18px;
height: 18px;
}
[data-composition-id="instagram-follow"] .chevron-icon svg {
width: 18px;
height: 18px;
display: block;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="instagram-follow"
data-composition-src="compositions/instagram-follow.html"
data-start="0"
data-duration="4.5"
data-track-index="1"
data-width="1080"
data-height="1920"
data-start="0"
data-duration="4.5"
>
<div
id="card"
class="clip lower-third"
data-start="0"
data-duration="4.5"
data-track-index="0"
>
<img class="avatar" src="assets/avatar.jpg" alt="HeyGen" crossorigin="anonymous" />
<div class="profile-info">
<div class="name-row">
<div class="display-name">HeyGen</div>
<span class="verified-badge"
><svg viewBox="0 0 40 40" fill="none">
<circle cx="20" cy="20" r="20" fill="#0095F6" />
<path d="M17.5 27.5L10 20l2.5-2.5 5 5 10-10L30 15 17.5 27.5z" fill="#fff" /></svg
></span>
</div>
<div class="handle">@heygen_official</div>
<div class="follower-count">47.5K followers</div>
</div>
<div id="follow-btn" class="follow-btn">
<span id="btn-follow" class="btn-text btn-text-follow">Follow</span>
<span id="btn-following" class="btn-text btn-text-following">
<span>Following</span>
<span class="chevron-icon"
><svg
viewBox="0 0 24 24"
fill="none"
stroke="#fff"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9"></polyline></svg
></span>
</span>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var card = document.getElementById("card");
var btn = document.getElementById("follow-btn");
var textFollow = document.getElementById("btn-follow");
var textFollowing = document.getElementById("btn-following");
// Start off-screen
gsap.set(card, { y: 300, opacity: 0 });
// Slide in from bottom
tl.to(
card,
{
y: 0,
opacity: 1,
duration: 0.5,
ease: "power3.out",
},
0.1,
);
// Hold, then button press-in
tl.to(
btn,
{
scale: 0.92,
duration: 0.15,
ease: "power2.out",
},
1.0,
);
// Button release with spring bounce
tl.to(
btn,
{
scale: 1,
duration: 0.4,
ease: "elastic.out(1, 0.4)",
},
1.15,
);
// Button background: IG blue → dark gray
tl.to(
btn,
{
backgroundColor: "#2f2f2f",
duration: 0.12,
ease: "none",
},
1.15,
);
// Fade out "Follow", fade in "Following"
tl.to(
textFollow,
{
opacity: 0,
duration: 0.08,
ease: "none",
},
1.15,
);
tl.to(
textFollowing,
{
opacity: 1,
duration: 0.08,
ease: "none",
},
1.18,
);
// Slide out to bottom
tl.to(
card,
{
y: 300,
opacity: 0,
duration: 0.25,
ease: "power3.in",
},
3.8,
);
window.__timelines["instagram-follow"] = tl;
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="instagram-follow" data-composition-src="compositions/instagram-follow.html" data-start="0" data-duration="4.5" data-track-index="1" data-width="1080" data-height="1920"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
File diff suppressed because it is too large Load Diff
+12 -375
View File
@@ -3,397 +3,34 @@ title: "Light Leak"
description: "Shader transition with cinematic light leak overlay"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/light-leak.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Light Leak preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/light-leak.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/light-leak.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add light-leak" />
```bash Terminal
npx hyperframes add light-leak
```
That writes one file: `compositions/light-leak.html`.
## Source
## Add it to your video
<Accordion title={`light-leak.html`}>
It runs for 4 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="main"
data-composition-id="light-leak"
data-composition-src="compositions/light-leak.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<style>
[data-composition-id="main"] {
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
background: #001524;
}
/* Blueprint grid */
.bp-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
/* Layout */
.bp-left {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1200px;
display: flex;
align-items: center;
justify-content: center;
}
.bp-right {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 720px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px 60px 60px 40px;
}
.bp-divider {
position: absolute;
left: 1200px;
top: 60px;
bottom: 60px;
width: 1px;
background: #fb8b24;
opacity: 0.15;
}
.bp-number {
font-family: "Space Mono", monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.4em;
text-transform: uppercase;
position: absolute;
top: 40px;
left: 60px;
color: #fb8b24;
}
/* Right panel typography */
.bp-name {
font-family: "Bebas Neue", sans-serif;
font-size: 38px;
letter-spacing: 0.08em;
margin-bottom: 20px;
color: #fb8b24;
}
.bp-plabel {
font-family: "Space Mono", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.3em;
text-transform: uppercase;
margin-bottom: 10px;
color: #fb8b24;
opacity: 0.5;
}
.bp-prompt {
font-family: "JetBrains Mono", monospace;
font-size: 20px;
line-height: 1.6;
padding: 16px 20px;
border-radius: 6px;
margin-bottom: 20px;
color: #fb8b24;
background: rgba(251, 139, 36, 0.06);
border: 1px solid rgba(251, 139, 36, 0.12);
}
.bp-desc {
font-family: "Lato", sans-serif;
font-size: 17px;
line-height: 1.6;
color: #708d81;
opacity: 0.55;
}
/* GL scenes (hidden, just for texture capture) */
.scene {
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.scene-label {
font-family: "Bebas Neue", sans-serif;
font-size: 140px;
letter-spacing: 0.06em;
opacity: 0.12;
}
</style>
<div class="bp-grid"></div>
<div class="bp-divider"></div>
<div class="bp-number">14 / 14</div>
<!-- GL demo scenes (captured as textures, then hidden) -->
<div id="s1" class="scene" style="background: #001524">
<div class="scene-label" style="color: #fb8b24">SCENE A</div>
</div>
<div id="s2" class="scene" style="background: #fb8b24; opacity: 0">
<div class="scene-label" style="color: #001524">SCENE B</div>
</div>
<!-- GL canvas (left panel only) -->
<canvas
id="gl-canvas"
width="1200"
height="1080"
style="
position: absolute;
top: 0;
left: 0;
width: 1200px;
height: 1080px;
z-index: 10;
pointer-events: none;
display: none;
"
>
</canvas>
<!-- Right panel metadata -->
<div class="bp-right">
<div class="bp-name">Light Leak</div>
<div class="bp-plabel">Prompt</div>
<div class="bp-prompt">"use light leak shader transition"</div>
<div class="bp-desc">
Beer-Lambert exponential falloff with ACES tone mapping and directional flare streak.
</div>
</div>
<div
id="driver"
class="clip"
data-start="0"
data-duration="4"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
window.__timelines = window.__timelines || {};
var glCanvas = document.getElementById("gl-canvas");
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
if (!gl) {
console.warn("WebGL not available");
window.__timelines["main"] = gsap.timeline({ paused: true });
} else {
gl.viewport(0, 0, 1200, 1080);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
var sceneTextures = {};
function captureScene(sceneId) {
var scene = document.getElementById(sceneId);
var origOpacity = scene.style.opacity;
var origZ = scene.style.zIndex;
scene.style.opacity = "1";
scene.style.zIndex = "999";
scene.offsetHeight;
var c = document.createElement("canvas");
c.width = 1200;
c.height = 1080;
var ctx = c.getContext("2d");
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
ctx.fillRect(0, 0, 1200, 1080);
var sr = scene.getBoundingClientRect();
var els = scene.querySelectorAll("*");
for (var i = 0; i < els.length; i++) {
var el = els[i],
cs = window.getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
var r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
var x = r.left - sr.left,
y = r.top - sr.top,
w = r.width,
h = r.height;
ctx.save();
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
var bg = cs.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
ctx.fillStyle = bg;
ctx.fillRect(x, y, w, h);
}
var hasChildEls = el.querySelector("div,span,img,video");
var text = "";
for (var j = 0; j < el.childNodes.length; j++)
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
text = text.trim();
if (text && !hasChildEls) {
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
ctx.fillStyle = cs.color;
if (cs.letterSpacing && cs.letterSpacing !== "normal")
ctx.letterSpacing = cs.letterSpacing;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x + w / 2, y + h / 2);
}
ctx.restore();
}
scene.style.opacity = origOpacity;
scene.style.zIndex = origZ;
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
sceneTextures[sceneId] = tex;
}
var vertSrc =
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
var quadBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW,
);
function compileShader(src, type) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error("Shader:", gl.getShaderInfoLog(s));
return s;
}
function mkProg(fragSrc) {
var p = gl.createProgram();
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
console.error("Link:", gl.getProgramInfoLog(p));
return p;
}
var H =
"precision mediump float;" +
"varying vec2 v_uv;" +
"uniform sampler2D u_from, u_to;" +
"uniform float u_progress;" +
"uniform vec2 u_resolution;\n";
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
var progTrans = mkProg(
H +
"vec3 aces(vec3 x){return clamp((x*(2.51*x+.03))/(x*(2.43*x+.59)+.14),0.,1.);}void main(){vec4 A=texture2D(u_from,v_uv),B=texture2D(u_to,v_uv);vec2 lp=vec2(1.3,-.2);float dist=length(v_uv-lp);float leak=clamp(exp(-dist*1.8)*u_progress*4.,0.,1.);vec3 warmColor=mix(vec3(1.,.5,.15),vec3(1.,.9,.75),dist*.7);float flare=exp(-abs(v_uv.y-(-.2+v_uv.x*.3))*15.)*leak*.3;vec3 overexposed=A.rgb+warmColor*leak*3.+vec3(1.,.8,.5)*flare;overexposed=aces(overexposed);gl_FragColor=vec4(mix(overexposed,B.rgb,smoothstep(.15,.85,u_progress)),1.);}",
);
function renderShader(prog, texFrom, texTo, progress) {
gl.useProgram(prog);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texFrom);
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texTo);
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
var pos = gl.getAttribLocation(prog, "a_pos");
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
function easeInOut(p) {
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
}
captureScene("s1");
captureScene("s2");
glCanvas.style.display = "block";
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
document.querySelectorAll(".scene").forEach(function (s) {
s.style.opacity = "0";
});
var tl = gsap.timeline({
paused: true,
onUpdate: function () {
var t = tl.time();
if (t >= 1.0 && t < 3.0) {
renderShader(
progTrans,
sceneTextures["s1"],
sceneTextures["s2"],
easeInOut((t - 1.0) / 2.0),
);
} else if (t >= 3.0) {
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
} else {
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
}
},
});
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
window.__timelines["main"] = tl;
} // end if (gl)
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="light-leak" data-composition-src="compositions/light-leak.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+17 -638
View File
@@ -3,661 +3,40 @@ title: "Liquid Glass Context Menu"
description: "Frosted glass context menu panel drifting over an aurora shader background"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/liquid-glass-context-menu.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Liquid Glass Context Menu preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-context-menu.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-context-menu.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add liquid-glass-context-menu" />
```bash Terminal
npx hyperframes add liquid-glass-context-menu
```
That writes `compositions/liquid-glass-context-menu.html`, plus 1 supporting file under `compositions/lib/`.
<Danger>
<Warning>
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
Without it this item's screen renders black. Rendering from the CLI switches
it on for you. [How it works](/guides/html-in-canvas)
</Danger>
Rendering from the CLI switches it on for you. [How it
works](/guides/html-in-canvas)
</Warning>
## Source
## Add it to your video
<Accordion title={`liquid-glass-context-menu.html`}>
It runs for 8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Liquid Glass Context Menu</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="lib/liquid-glass.iife.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0218;
overflow: hidden;
font-family: "Inter", system-ui, sans-serif;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
#three-canvas {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
z-index: 0;
}
#glass-canvas {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
z-index: 1;
}
/* ── Glass Panel ── */
.menu-panel {
position: absolute;
width: 390px;
height: 506px;
background: rgba(255, 255, 255, 0.15);
--lg-blur: 0.56;
--lg-refraction: 0.52;
--lg-corner-radius: 30;
--lg-z-radius: 48;
--lg-specular: 0.26;
--lg-fresnel: 1;
--lg-edge-highlight: 0.2;
--lg-chrom-aberration: 0.035;
--lg-tint: 0.88;
--lg-brightness: 0.54;
--lg-shadow-opacity: 0;
--lg-shadow-spread: 0;
--lg-shadow-offset-y: 0;
--lg-saturation: -0.24;
}
#glass-menu {
top: 270px;
left: 1980px;
}
.file-panel {
position: absolute;
top: 372px;
left: 500px;
width: 360px;
height: 104px;
background: rgba(255, 255, 255, 0.11);
--lg-blur: 0.52;
--lg-refraction: 0.48;
--lg-corner-radius: 28;
--lg-z-radius: 36;
--lg-specular: 0.28;
--lg-fresnel: 0.94;
--lg-edge-highlight: 0.18;
--lg-chrom-aberration: 0.03;
--lg-tint: 0.74;
--lg-brightness: 0.34;
--lg-shadow-opacity: 0;
--lg-shadow-spread: 0;
--lg-shadow-offset-y: 0;
--lg-saturation: -0.18;
}
.submenu-panel {
position: absolute;
top: 456px;
left: 1980px;
width: 230px;
height: 58px;
background: rgba(255, 255, 255, 0.11);
--lg-blur: 0.52;
--lg-refraction: 0.5;
--lg-corner-radius: 20;
--lg-z-radius: 28;
--lg-specular: 0.3;
--lg-fresnel: 0.96;
--lg-edge-highlight: 0.18;
--lg-chrom-aberration: 0.03;
--lg-tint: 0.74;
--lg-brightness: 0.36;
--lg-shadow-opacity: 0;
--lg-shadow-spread: 0;
--lg-shadow-offset-y: 0;
--lg-saturation: -0.2;
}
/* ── Text Overlay ── */
.text-overlay {
position: absolute;
z-index: 2;
pointer-events: none;
}
.context-file {
position: absolute;
top: 340px;
left: 500px;
z-index: 2;
width: 360px;
height: 104px;
display: flex;
align-items: center;
gap: 18px;
padding: 18px 22px;
border-radius: 26px;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.095) 0%,
rgba(255, 255, 255, 0.036) 38%,
rgba(255, 255, 255, 0.006) 100%
);
box-shadow: none;
}
.file-icon {
width: 58px;
height: 68px;
flex-shrink: 0;
border-radius: 16px;
background:
linear-gradient(135deg, rgba(6, 227, 250, 0.92), rgba(79, 219, 94, 0.85)),
rgba(255, 255, 255, 0.12);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.34);
}
.file-meta {
display: flex;
flex-direction: column;
gap: 4px;
}
.file-title {
color: rgba(5, 17, 28, 0.9);
font-size: 21px;
font-weight: 780;
letter-spacing: 0;
text-shadow:
0 1px 0 rgba(255, 255, 255, 0.24),
0 10px 20px rgba(255, 255, 255, 0.14);
}
.file-subtitle {
color: rgba(5, 17, 28, 0.72);
font-size: 15px;
font-weight: 650;
text-shadow:
0 1px 0 rgba(255, 255, 255, 0.2),
0 8px 18px rgba(255, 255, 255, 0.12);
}
.pointer {
position: absolute;
top: 395px;
left: 778px;
z-index: 4;
width: 34px;
height: 44px;
border-radius: 2px;
background: #fff;
clip-path: polygon(0 0, 0 100%, 31% 73%, 47% 100%, 62% 93%, 46% 67%, 82% 67%);
filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.4));
}
.click-ring {
position: absolute;
top: 390px;
left: 770px;
z-index: 3;
width: 46px;
height: 46px;
border: 2px solid rgba(255, 255, 255, 0.75);
border-radius: 50%;
opacity: 0;
}
.menu-text {
width: 390px;
height: 506px;
display: flex;
flex-direction: column;
padding: 18px 0;
background: transparent;
border-radius: 30px;
box-shadow: none;
overflow: hidden;
}
#text-menu {
top: 270px;
left: 1980px;
}
.menu-highlight {
position: absolute;
top: 17px;
left: 14px;
z-index: 0;
width: 362px;
height: 52px;
border-radius: 17px;
background:
radial-gradient(circle at 24% 22%, rgba(255, 255, 255, 0.48), transparent 42%),
linear-gradient(90deg, rgba(255, 255, 255, 0.29), rgba(255, 255, 255, 0.12));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.38),
inset 0 -1px 0 rgba(255, 255, 255, 0.1);
opacity: 0;
}
.menu-item {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 16px;
padding: 14px 28px;
font-size: 18px;
font-weight: 650;
color: rgb(0, 7, 14);
cursor: default;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.26);
}
.menu-item svg {
width: 22px;
height: 22px;
flex-shrink: 0;
stroke: rgb(0, 7, 14);
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.menu-sep {
height: 1px;
margin: 10px 28px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.24), transparent);
}
.menu-footer {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 26px;
margin-top: auto;
}
.menu-footer-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 650;
color: rgba(0, 7, 14, 0.9);
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.24);
}
.menu-footer-item svg {
width: 24px;
height: 24px;
stroke: rgba(0, 7, 14, 0.84);
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.menu-kbd {
margin-left: auto;
font-size: 14px;
font-weight: 650;
color: rgba(0, 7, 14, 0.82);
font-variant-numeric: tabular-nums;
}
.submenu-preview {
position: absolute;
top: 456px;
left: 1980px;
z-index: 2;
width: 230px;
height: 58px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 20px;
background: transparent;
box-shadow: none;
color: rgba(0, 7, 14, 0.92);
font-size: 15px;
font-weight: 760;
opacity: 0;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="liquid-glass-context-menu"
data-composition-src="compositions/liquid-glass-context-menu.html"
data-start="0"
data-duration="8"
data-track-index="1"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="8"
data-root="true"
>
<canvas id="three-canvas" width="1920" height="1080"></canvas>
<canvas id="glass-canvas" layoutsubtree width="1920" height="1080">
<div id="glass-file" class="file-panel liquid-glass"></div>
<div id="glass-menu" class="menu-panel liquid-glass"></div>
<div id="glass-submenu" class="submenu-panel liquid-glass"></div>
</canvas>
<div id="context-file" class="context-file">
<div class="file-icon"></div>
<div class="file-meta">
<div class="file-title">liquid-glass.mov</div>
<div class="file-subtitle">Preview render - 08s</div>
</div>
</div>
<div id="click-ring" class="click-ring"></div>
<div id="pointer" class="pointer"></div>
<div id="text-menu" class="text-overlay menu-text">
<div id="menu-highlight" class="menu-highlight"></div>
<div class="menu-item">
<svg viewBox="0 0 24 24">
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
<polyline points="16 6 12 2 8 6" />
<line x1="12" y1="2" x2="12" y2="15" />
</svg>
<span>Share</span>
</div>
<div class="menu-item">
<svg viewBox="0 0 24 24">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg>
<span>Add to Bookmarks</span>
</div>
<div class="menu-item">
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14" /></svg>
<span>Add Bookmark to...</span>
</div>
<div class="menu-sep"></div>
<div class="menu-item">
<svg viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="9" y1="3" x2="9" y2="21" />
</svg>
<span>New Tab</span>
<span class="menu-kbd">&#8984;T</span>
</div>
<div class="menu-item">
<svg viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<path d="M9 3v18" />
<path d="M14 9l3 3-3 3" />
</svg>
<span>New Private Tab</span>
<span class="menu-kbd">&#8679;&#8984;N</span>
</div>
<div class="menu-sep"></div>
<div class="menu-footer">
<div class="menu-footer-item">
<svg viewBox="0 0 24 24">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg>
<span>Bookmarks</span>
</div>
<div class="menu-footer-item">
<svg viewBox="0 0 24 24">
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
</svg>
<span>All Tabs</span>
</div>
</div>
</div>
<div id="submenu-preview" class="submenu-preview">Open in New Tab</div>
<div
id="driver"
class="clip"
data-start="0"
data-duration="8"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
var W = 1920,
H = 1080,
DURATION = 8;
/* ── Three.js Aurora Shader ─────────────────────────────────────── */
var vs = "varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); }";
var fs = [
"precision highp float;",
"varying vec2 vUv;",
"uniform float uTime;",
"vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}",
"vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}",
"vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}",
"vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}",
"float snoise(vec3 v){",
" const vec2 C=vec2(1.0/6.0,1.0/3.0);const vec4 D=vec4(0.0,0.5,1.0,2.0);",
" vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx);",
" vec3 g=step(x0.yzx,x0.xyz);vec3 l=1.0-g;",
" vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy);",
" vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy;",
" i=mod289(i);",
" vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));",
" float n_=0.142857142857;vec3 ns=n_*D.wyz-D.xzx;",
" vec4 j=p-49.0*floor(p*ns.z*ns.z);vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.0*x_);",
" vec4 x=x_*ns.x+ns.yyyy;vec4 y=y_*ns.x+ns.yyyy;vec4 h=1.0-abs(x)-abs(y);",
" vec4 b0=vec4(x.xy,y.xy);vec4 b1=vec4(x.zw,y.zw);",
" vec4 s0=floor(b0)*2.0+1.0;vec4 s1=floor(b1)*2.0+1.0;",
" vec4 sh=-step(h,vec4(0.0));",
" vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;",
" vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y);vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w);",
" vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));",
" p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;",
" vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);m=m*m;",
" return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));",
"}",
"void main(){",
" vec2 uv=vUv;float ar=1920.0/1080.0;vec2 p=vec2(uv.x*ar,uv.y);float t=uTime*0.06;",
" float wx=snoise(vec3(p*1.4,t*0.5))*0.2;float wy=snoise(vec3(p*1.4+100.0,t*0.5))*0.2;",
" vec2 wp=p+vec2(wx,wy);",
" vec3 base=mix(vec3(0.10,0.02,0.22),vec3(0.04,0.10,0.25),smoothstep(0.0,1.0,uv.x*0.6+uv.y*0.4));",
" float angle=0.65;float ridgeUv=dot(wp,vec2(cos(angle),sin(angle)));",
" float ridge1=sin(ridgeUv*8.0+t*2.0+snoise(vec3(wp*0.8,t))*1.5);",
" float ridge2=sin(ridgeUv*5.5-t*1.4+snoise(vec3(wp*1.2+50.0,t*0.7))*2.0);",
" float ridge3=sin(ridgeUv*12.0+t*3.0+snoise(vec3(wp*0.6+80.0,t*0.4))*1.0);",
" float crease1=pow(abs(ridge1),0.35)*sign(ridge1)*0.5+0.5;",
" float crease2=pow(abs(ridge2),0.4)*sign(ridge2)*0.5+0.5;",
" float crease3=pow(abs(ridge3),0.5)*sign(ridge3)*0.5+0.5;",
" vec3 warm=mix(vec3(0.9,0.4,0.08),vec3(0.95,0.65,0.15),crease1);",
" float warmZone=smoothstep(0.15,0.75,uv.x+uv.y*0.4+snoise(vec3(uv*2.0,t))*0.3);",
" vec3 cool=mix(vec3(0.02,0.4,0.55),vec3(0.25,0.7,0.82),crease2);",
" float coolZone=smoothstep(0.25,0.85,1.2-uv.x+uv.y*0.5+snoise(vec3(uv*2.0+40.0,t*0.8))*0.25);",
" vec3 hot=mix(vec3(0.8,0.1,0.4),vec3(0.6,0.08,0.65),crease3);",
" float hotZone=smoothstep(0.5,0.9,snoise(vec3(uv*2.5+20.0,t*0.6))*0.5+0.5);",
" vec3 col=base;",
" col=mix(col,warm,warmZone*0.75*smoothstep(0.3,0.7,crease1));",
" col=mix(col,cool,coolZone*0.65*smoothstep(0.2,0.65,crease2));",
" col=mix(col,hot,hotZone*0.5*smoothstep(0.35,0.8,crease3));",
" float edgeGlow=pow(1.0-abs(ridge1),6.0)*0.4+pow(1.0-abs(ridge2),5.0)*0.25;",
" col+=edgeGlow*mix(warm,cool,uv.x)*0.8;",
" col*=1.0-pow(abs(ridge1),4.0)*0.15;",
" float vig=1.0-smoothstep(0.5,1.5,length((uv-0.5)*vec2(1.3,1.0)));",
" col*=0.65+vig*0.5;",
" float luma=dot(col,vec3(0.299,0.587,0.114));col=mix(vec3(luma),col,1.5);",
" col=clamp(col,0.0,1.0);col=pow(col,vec3(0.9));",
" gl_FragColor=vec4(col,1.0);",
"}",
].join("\n");
/* ── Three.js Setup ─────────────────────────────────────────────── */
var threeCanvas = document.getElementById("three-canvas");
var renderer = new THREE.WebGLRenderer({ canvas: threeCanvas, alpha: false });
renderer.setSize(W, H, false);
renderer.setPixelRatio(1);
var scene = new THREE.Scene();
var camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
var uniforms = { uTime: { value: 0 } };
var mat = new THREE.ShaderMaterial({
vertexShader: vs,
fragmentShader: fs,
uniforms: uniforms,
});
var quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), mat);
scene.add(quad);
/* ── Glass Canvas ───────────────────────────────────────────────── */
var glassCanvas = document.getElementById("glass-canvas");
var ctx = glassCanvas.getContext("2d");
var lg = new LiquidGlass.LiquidGlassCanvas(glassCanvas);
var lgReady = false;
function renderGlass(time) {
if (!lgReady) return;
uniforms.uTime.value = time;
renderer.render(scene, camera);
ctx.clearRect(0, 0, W, H);
ctx.drawImage(threeCanvas, 0, 0, W, H);
lg.renderGlassElements();
}
function requestGlassRender(time) {
if (glassCanvas.requestPaint) {
glassCanvas.onpaint = function () {
renderGlass(time);
};
glassCanvas.requestPaint();
return;
}
renderGlass(time);
}
lg.waitForInit().then(function (ok) {
if (!ok) return;
lgReady = true;
requestGlassRender(0);
});
/* ── GSAP Timeline ──────────────────────────────────────────────── */
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var gm = document.getElementById("glass-menu");
var gf = document.getElementById("glass-file");
var gsm = document.getElementById("glass-submenu");
var tm = document.getElementById("text-menu");
var pointer = document.getElementById("pointer");
var fileCard = document.getElementById("context-file");
gsap.set([gm, tm], {
left: 860,
top: 270,
opacity: 0,
scale: 0.82,
y: -8,
transformOrigin: "0% 18%",
});
// glass (#glass-file) is html-in-canvas — stays on left/top (exempt, no stutter)
gsap.set(gf, { top: 372, opacity: 1, scale: 1, transformOrigin: "78% 54%" });
// plain DOM (#context-file) — CSS resting top is 340px, so top:372 becomes y:32
gsap.set(fileCard, { y: 32, opacity: 1, scale: 1, transformOrigin: "78% 54%" });
gsap.set([gsm, "#submenu-preview"], {
left: 1980,
opacity: 0,
scale: 0.86,
x: -12,
transformOrigin: "0% 50%",
});
gsap.set(".menu-item", { y: -4, opacity: 0 });
// #menu-highlight plain DOM — CSS resting top:17; top:17 seed is y:0, tweens use y
gsap.set("#menu-highlight", { y: 0, opacity: 0 });
tl.to(gf, { top: 340, duration: 0.38, ease: "power3.out" }, 0.1);
tl.to(fileCard, { y: 0, duration: 0.38, ease: "power3.out" }, 0.1);
// #pointer plain DOM — CSS resting left:778 top:395; convert left/top → x/y offsets
tl.fromTo(
pointer,
{ x: -158, y: 125 },
{ x: 0, y: 0, duration: 0.55, ease: "power3.out" },
0.35,
);
tl.set("#click-ring", { left: 756, top: 372, scale: 0.72 }, 0.84);
tl.to("#click-ring", { opacity: 0.7, scale: 1.18, duration: 0.16, ease: "power2.out" }, 0.86);
tl.to("#click-ring", { opacity: 0, scale: 1.8, duration: 0.28, ease: "power2.out" }, 1.02);
tl.to(
[gf, fileCard],
{ scale: 0.985, duration: 0.08, ease: "power2.out", yoyo: true, repeat: 1 },
0.86,
);
tl.to([gm, tm], { opacity: 1, scale: 1, y: 0, duration: 0.36, ease: "back.out(1.08)" }, 0.98);
tl.to(
".menu-item",
{ y: 0, opacity: 1, duration: 0.24, ease: "power3.out", stagger: 0.026 },
1.07,
);
tl.to("#menu-highlight", { opacity: 1, duration: 0.12, ease: "none" }, 1.35);
tl.to("#menu-highlight", { y: 52, duration: 0.22, ease: "power2.out" }, 2.0);
tl.to(pointer, { x: 350, y: 73, duration: 0.46, ease: "power3.inOut" }, 2.48);
tl.to("#menu-highlight", { y: 170, duration: 0.22, ease: "power2.out" }, 2.58);
tl.set([gsm, "#submenu-preview"], { left: 1236 }, 3.04);
tl.fromTo(
[gsm, "#submenu-preview"],
{ scale: 0.86, opacity: 0, x: -12 },
{ scale: 1, opacity: 1, x: 0, duration: 0.24, ease: "back.out(1.04)" },
3.08,
);
tl.to(pointer, { x: 540, y: 89, duration: 0.5, ease: "power3.inOut" }, 3.32);
tl.set("#click-ring", { left: 1295, top: 461, scale: 0.72 }, 3.86);
tl.to(
"#click-ring",
{ opacity: 0.72, scale: 1.08, duration: 0.12, ease: "power2.out" },
3.88,
);
tl.to("#click-ring", { opacity: 0, scale: 1.58, duration: 0.24, ease: "power2.out" }, 4.0);
tl.to(
[gsm, "#submenu-preview"],
{ scale: 0.965, duration: 0.09, ease: "power2.out", yoyo: true, repeat: 1 },
3.92,
);
tl.to(pointer, { opacity: 0, duration: 0.35, ease: "power2.out" }, 5.85);
/* Duration driver */
tl.to({ v: 0 }, { v: 1, duration: DURATION, ease: "none" }, 0);
tl.eventCallback("onUpdate", function () {
requestGlassRender(tl.time());
});
window.__timelines["liquid-glass-context-menu"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="liquid-glass-context-menu" data-composition-src="compositions/liquid-glass-context-menu.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,722 +3,40 @@ title: "Liquid Glass Media Controls"
description: "Frosted glass media control panels spreading over an aurora shader background"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/liquid-glass-media-controls.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Liquid Glass Media Controls preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-media-controls.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-media-controls.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add liquid-glass-media-controls" />
```bash Terminal
npx hyperframes add liquid-glass-media-controls
```
That writes `compositions/liquid-glass-media-controls.html`, plus 1 supporting file under `compositions/lib/`.
<Danger>
<Warning>
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
Without it this item's screen renders black. Rendering from the CLI switches
it on for you. [How it works](/guides/html-in-canvas)
</Danger>
Rendering from the CLI switches it on for you. [How it
works](/guides/html-in-canvas)
</Warning>
## Source
## Add it to your video
<Accordion title={`liquid-glass-media-controls.html`}>
It runs for 8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Liquid Glass Media Controls</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="lib/liquid-glass.iife.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0218;
overflow: hidden;
font-family: "Inter", system-ui, sans-serif;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
#three-canvas {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
z-index: 0;
}
#glass-canvas {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
z-index: 1;
}
/* ── Glass Panels ── */
.glass-panel {
position: absolute;
background: rgba(255, 255, 255, 0.055);
--lg-blur: 0.42;
--lg-refraction: 0.92;
--lg-corner-radius: 36;
--lg-z-radius: 58;
--lg-specular: 0.46;
--lg-fresnel: 1.15;
--lg-edge-highlight: 0.34;
--lg-chrom-aberration: 0.08;
--lg-shadow-opacity: 0;
--lg-shadow-spread: 0;
--lg-shadow-offset-y: 0;
--lg-saturation: 1.1;
}
.search-panel {
width: 640px;
height: 72px;
--lg-corner-radius: 24;
}
.toggle-panel {
width: 640px;
height: 64px;
--lg-corner-radius: 44;
}
.media-panel {
width: 640px;
height: 116px;
--lg-corner-radius: 44;
}
.slider-panel {
width: 640px;
height: 76px;
--lg-corner-radius: 28;
}
#glass-search {
top: 300px;
left: 640px;
}
#glass-toggle {
top: 390px;
left: 640px;
}
#glass-media {
top: 476px;
left: 640px;
}
#glass-slider {
top: 616px;
left: 640px;
}
/* ── Text Overlays ── */
.text-overlay {
position: absolute;
z-index: 2;
pointer-events: none;
}
/* Search bar */
.search-text {
width: 640px;
height: 72px;
display: flex;
align-items: center;
gap: 16px;
padding: 0 28px;
background: transparent;
border-radius: 24px;
box-shadow: none;
}
.search-text svg {
width: 22px;
height: 22px;
stroke: rgba(255, 255, 255, 0.86);
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
flex-shrink: 0;
}
.search-label {
font-size: 19px;
font-weight: 650;
color: #fff;
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.42);
}
.search-caret {
width: 2px;
height: 25px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
opacity: 0;
}
#text-search {
top: 300px;
left: 640px;
}
/* Toggle row */
.toggle-text {
width: 640px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
gap: 0;
position: relative;
padding: 6px;
background: transparent;
border-radius: 44px;
box-shadow: none;
}
.toggle-active-pill {
position: absolute;
top: 6px;
left: 6px;
z-index: 0;
width: 209px;
height: 52px;
border-radius: 999px;
background:
radial-gradient(circle at 28% 18%, rgba(255, 255, 255, 0.32), transparent 42%),
rgba(255, 255, 255, 0.14);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.32),
inset 0 -1px 0 rgba(255, 255, 255, 0.09);
}
.toggle-item {
flex: 1;
position: relative;
z-index: 1;
text-align: center;
font-size: 16px;
font-weight: 700;
color: rgba(255, 255, 255, 0.96);
line-height: 52px;
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.42);
}
.toggle-item.active {
color: #fff;
}
#text-toggle {
top: 390px;
left: 640px;
}
/* Media pill */
.media-text {
width: 640px;
height: 116px;
display: flex;
align-items: center;
gap: 22px;
padding: 0 24px;
background: transparent;
border-radius: 44px;
box-shadow: none;
}
.album-art {
position: relative;
overflow: hidden;
width: 78px;
height: 78px;
border-radius: 20px;
background:
radial-gradient(circle at 70% 28%, rgba(255, 255, 255, 0.78), transparent 16%),
conic-gradient(from 210deg, #06e3fa, #4fdb5e, #f59e0b, #ec4899, #06e3fa);
flex-shrink: 0;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.28),
0 12px 26px rgba(0, 0, 0, 0.24);
}
.album-sheen {
position: absolute;
top: -10px;
left: -70px;
width: 38px;
height: 110px;
background: rgba(255, 255, 255, 0.34);
filter: blur(8px);
transform: rotate(24deg);
}
.media-info {
display: flex;
flex-direction: column;
gap: 7px;
flex: 1;
min-width: 0;
}
.media-title {
font-size: 22px;
font-weight: 700;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.42);
}
.media-artist {
font-size: 16px;
font-weight: 600;
color: rgba(255, 255, 255, 0.84);
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.42);
}
.media-meta-row {
display: flex;
align-items: center;
gap: 9px;
}
.live-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #4ade80;
box-shadow: 0 0 14px rgba(74, 222, 128, 0.76);
}
.sound-wave {
display: flex;
align-items: end;
gap: 3px;
height: 16px;
}
.sound-wave span {
width: 3px;
height: 7px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.74);
}
.transport {
display: flex;
align-items: center;
gap: 24px;
margin-left: auto;
padding-right: 4px;
}
.transport svg {
width: 28px;
height: 28px;
fill: #fff;
stroke: none;
}
.transport .play-btn svg {
width: 38px;
height: 38px;
}
#text-media {
top: 476px;
left: 640px;
}
/* Brightness slider */
.slider-text {
width: 640px;
height: 76px;
display: flex;
align-items: center;
gap: 18px;
padding: 0 28px;
background: transparent;
border-radius: 28px;
box-shadow: none;
}
.slider-text svg {
width: 26px;
height: 26px;
stroke: rgba(255, 255, 255, 0.88);
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
flex-shrink: 0;
}
.slider-track {
flex: 1;
height: 8px;
background: rgba(255, 255, 255, 0.24);
border-radius: 4px;
position: relative;
}
.slider-fill {
position: absolute;
top: 0;
left: 0;
width: 34%;
height: 100%;
background: #fff;
border-radius: 3px;
box-shadow: 0 0 14px rgba(255, 255, 255, 0.5);
}
.slider-pct {
font-size: 17px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
font-variant-numeric: tabular-nums;
width: 42px;
text-align: right;
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.42);
}
#text-slider {
top: 616px;
left: 640px;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="liquid-glass-media-controls"
data-composition-src="compositions/liquid-glass-media-controls.html"
data-start="0"
data-duration="8"
data-track-index="1"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="8"
data-root="true"
>
<canvas id="three-canvas" width="1920" height="1080"></canvas>
<canvas id="glass-canvas" layoutsubtree width="1920" height="1080">
<div id="glass-search" class="glass-panel search-panel liquid-glass"></div>
<div id="glass-toggle" class="glass-panel toggle-panel liquid-glass"></div>
<div id="glass-media" class="glass-panel media-panel liquid-glass"></div>
<div id="glass-slider" class="glass-panel slider-panel liquid-glass"></div>
</canvas>
<!-- Search bar -->
<div id="text-search" class="text-overlay search-text">
<svg viewBox="0 0 24 24">
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<span id="search-label" class="search-label">Search</span>
<span id="search-caret" class="search-caret"></span>
</div>
<!-- Toggle row -->
<div id="text-toggle" class="text-overlay toggle-text">
<div id="toggle-pill" class="toggle-active-pill"></div>
<div class="toggle-item active">All</div>
<div class="toggle-item">Favorites</div>
<div class="toggle-item">Recent</div>
</div>
<!-- Media pill -->
<div id="text-media" class="text-overlay media-text">
<div class="album-art"><span id="album-sheen" class="album-sheen"></span></div>
<div class="media-info">
<div class="media-title">Midnight Drive</div>
<div class="media-meta-row">
<span id="live-dot" class="live-dot"></span>
<div class="media-artist">Glass Resonance</div>
<div id="sound-wave" class="sound-wave">
<span></span><span></span><span></span><span></span>
</div>
</div>
</div>
<div class="transport">
<span>
<svg viewBox="0 0 24 24">
<polygon points="19 20 9 12 19 4" />
<line x1="5" y1="4" x2="5" y2="20" stroke="#fff" stroke-width="2" />
</svg>
</span>
<span class="play-btn">
<svg viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21" /></svg>
</span>
<span>
<svg viewBox="0 0 24 24">
<polygon points="5 4 15 12 5 20" />
<line x1="19" y1="4" x2="19" y2="20" stroke="#fff" stroke-width="2" />
</svg>
</span>
</div>
</div>
<!-- Brightness slider -->
<div id="text-slider" class="text-overlay slider-text">
<svg viewBox="0 0 24 24">
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
<div class="slider-track"><div id="slider-fill" class="slider-fill"></div></div>
<span id="slider-pct" class="slider-pct">34%</span>
</div>
<div
id="driver"
class="clip"
data-start="0"
data-duration="8"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
var W = 1920,
H = 1080,
DURATION = 8;
/* ── Three.js Aurora Shader ─────────────────────────────────────── */
var vs = "varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); }";
var fs = [
"precision highp float;",
"varying vec2 vUv;",
"uniform float uTime;",
"vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}",
"vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}",
"vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}",
"vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}",
"float snoise(vec3 v){",
" const vec2 C=vec2(1.0/6.0,1.0/3.0);const vec4 D=vec4(0.0,0.5,1.0,2.0);",
" vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx);",
" vec3 g=step(x0.yzx,x0.xyz);vec3 l=1.0-g;",
" vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy);",
" vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy;",
" i=mod289(i);",
" vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));",
" float n_=0.142857142857;vec3 ns=n_*D.wyz-D.xzx;",
" vec4 j=p-49.0*floor(p*ns.z*ns.z);vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.0*x_);",
" vec4 x=x_*ns.x+ns.yyyy;vec4 y=y_*ns.x+ns.yyyy;vec4 h=1.0-abs(x)-abs(y);",
" vec4 b0=vec4(x.xy,y.xy);vec4 b1=vec4(x.zw,y.zw);",
" vec4 s0=floor(b0)*2.0+1.0;vec4 s1=floor(b1)*2.0+1.0;",
" vec4 sh=-step(h,vec4(0.0));",
" vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;",
" vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y);vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w);",
" vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));",
" p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;",
" vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);m=m*m;",
" return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));",
"}",
"void main(){",
" vec2 uv=vUv;float ar=1920.0/1080.0;vec2 p=vec2(uv.x*ar,uv.y);float t=uTime*0.06;",
" float wx=snoise(vec3(p*1.4,t*0.5))*0.2;float wy=snoise(vec3(p*1.4+100.0,t*0.5))*0.2;",
" vec2 wp=p+vec2(wx,wy);",
" vec3 base=mix(vec3(0.10,0.02,0.22),vec3(0.04,0.10,0.25),smoothstep(0.0,1.0,uv.x*0.6+uv.y*0.4));",
" float angle=0.65;float ridgeUv=dot(wp,vec2(cos(angle),sin(angle)));",
" float ridge1=sin(ridgeUv*8.0+t*2.0+snoise(vec3(wp*0.8,t))*1.5);",
" float ridge2=sin(ridgeUv*5.5-t*1.4+snoise(vec3(wp*1.2+50.0,t*0.7))*2.0);",
" float ridge3=sin(ridgeUv*12.0+t*3.0+snoise(vec3(wp*0.6+80.0,t*0.4))*1.0);",
" float crease1=pow(abs(ridge1),0.35)*sign(ridge1)*0.5+0.5;",
" float crease2=pow(abs(ridge2),0.4)*sign(ridge2)*0.5+0.5;",
" float crease3=pow(abs(ridge3),0.5)*sign(ridge3)*0.5+0.5;",
" vec3 warm=mix(vec3(0.9,0.4,0.08),vec3(0.95,0.65,0.15),crease1);",
" float warmZone=smoothstep(0.15,0.75,uv.x+uv.y*0.4+snoise(vec3(uv*2.0,t))*0.3);",
" vec3 cool=mix(vec3(0.02,0.4,0.55),vec3(0.25,0.7,0.82),crease2);",
" float coolZone=smoothstep(0.25,0.85,1.2-uv.x+uv.y*0.5+snoise(vec3(uv*2.0+40.0,t*0.8))*0.25);",
" vec3 hot=mix(vec3(0.8,0.1,0.4),vec3(0.6,0.08,0.65),crease3);",
" float hotZone=smoothstep(0.5,0.9,snoise(vec3(uv*2.5+20.0,t*0.6))*0.5+0.5);",
" vec3 col=base;",
" col=mix(col,warm,warmZone*0.75*smoothstep(0.3,0.7,crease1));",
" col=mix(col,cool,coolZone*0.65*smoothstep(0.2,0.65,crease2));",
" col=mix(col,hot,hotZone*0.5*smoothstep(0.35,0.8,crease3));",
" float edgeGlow=pow(1.0-abs(ridge1),6.0)*0.4+pow(1.0-abs(ridge2),5.0)*0.25;",
" col+=edgeGlow*mix(warm,cool,uv.x)*0.8;",
" col*=1.0-pow(abs(ridge1),4.0)*0.15;",
" float vig=1.0-smoothstep(0.5,1.5,length((uv-0.5)*vec2(1.3,1.0)));",
" col*=0.65+vig*0.5;",
" float luma=dot(col,vec3(0.299,0.587,0.114));col=mix(vec3(luma),col,1.5);",
" col=clamp(col,0.0,1.0);col=pow(col,vec3(0.9));",
" gl_FragColor=vec4(col,1.0);",
"}",
].join("\n");
/* ── Three.js Setup ─────────────────────────────────────────────── */
var threeCanvas = document.getElementById("three-canvas");
var renderer = new THREE.WebGLRenderer({ canvas: threeCanvas, alpha: false });
renderer.setSize(W, H, false);
renderer.setPixelRatio(1);
var scene = new THREE.Scene();
var camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
var uniforms = { uTime: { value: 0 } };
var shaderMat = new THREE.ShaderMaterial({
vertexShader: vs,
fragmentShader: fs,
uniforms: uniforms,
});
var quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), shaderMat);
scene.add(quad);
/* ── Glass Canvas ───────────────────────────────────────────────── */
var glassCanvas = document.getElementById("glass-canvas");
var ctx = glassCanvas.getContext("2d");
var lg = new LiquidGlass.LiquidGlassCanvas(glassCanvas);
var lgReady = false;
function renderGlass(time) {
if (!lgReady) return;
uniforms.uTime.value = time;
renderer.render(scene, camera);
ctx.clearRect(0, 0, W, H);
ctx.drawImage(threeCanvas, 0, 0, W, H);
lg.renderGlassElements();
}
function requestGlassRender(time) {
if (glassCanvas.requestPaint) {
glassCanvas.onpaint = function () {
renderGlass(time);
};
glassCanvas.requestPaint();
return;
}
renderGlass(time);
}
lg.waitForInit().then(function (ok) {
if (!ok) return;
lgReady = true;
requestGlassRender(0);
});
/* ── GSAP Timeline ──────────────────────────────────────────────── */
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var gs = document.getElementById("glass-search");
var gt = document.getElementById("glass-toggle");
var gm = document.getElementById("glass-media");
var gsl = document.getElementById("glass-slider");
var ts = document.getElementById("text-search");
var tt = document.getElementById("text-toggle");
var tm = document.getElementById("text-media");
var tsl = document.getElementById("text-slider");
var searchLabel = document.getElementById("search-label");
var sliderPct = document.getElementById("slider-pct");
var sliderValue = { value: 34 };
// Glass panels live inside <canvas layoutsubtree> (html-in-canvas): the canvas lib
// reads their sub-pixel computed top/left, so animating layout props on them does
// NOT stutter and is exempt from gsap_non_transform_motion. They keep top/left.
// Plain-DOM text overlays DO stutter on layout props, so they use transforms (x/y).
// CSS resting tops — search 300, toggle 390, media 476, slider 616 — are the
// transform base; the original set seeded top:1160, i.e. y = 1160 - restingTop.
gsap.set([gs, gt, gm, gsl], { top: 1160, opacity: 1 });
gsap.set(ts, { y: 860, opacity: 1 });
gsap.set(tt, { y: 770, opacity: 1 });
gsap.set(tm, { y: 684, opacity: 1 });
gsap.set(tsl, { y: 544, opacity: 1 });
gsap.set("#sound-wave span", { height: 7 });
tl.to(gs, { top: 300, duration: 0.48, ease: "power3.out" }, 0.12);
tl.to(ts, { y: 0, duration: 0.48, ease: "power3.out" }, 0.12);
tl.to(gt, { top: 390, duration: 0.44, ease: "power3.out" }, 0.22);
tl.to(tt, { y: 0, duration: 0.44, ease: "power3.out" }, 0.22);
tl.to(gm, { top: 476, duration: 0.44, ease: "power3.out" }, 0.32);
tl.to(tm, { y: 0, duration: 0.44, ease: "power3.out" }, 0.32);
tl.to(gsl, { top: 616, duration: 0.46, ease: "power3.out" }, 0.42);
tl.to(tsl, { y: 0, duration: 0.46, ease: "power3.out" }, 0.42);
tl.call(
function () {
searchLabel.textContent = "Search Music";
},
null,
0.9,
);
tl.to(
"#search-caret",
{ opacity: 1, duration: 0.1, yoyo: true, repeat: 5, ease: "none" },
0.94,
);
tl.call(
function () {
searchLabel.textContent = "Midnight Drive";
},
null,
1.55,
);
tl.to("#toggle-pill", { x: 209, duration: 0.24, ease: "power2.out" }, 1.82);
tl.to(
".play-btn",
{ scale: 0.86, duration: 0.12, ease: "power2.out", yoyo: true, repeat: 1 },
2.18,
);
tl.to("#album-sheen", { x: 182, duration: 0.75, ease: "power2.out" }, 2.2);
tl.to(
"#sound-wave span",
{ height: 15, duration: 0.22, ease: "power2.inOut", stagger: 0.05, yoyo: true, repeat: 12 },
2.2,
);
tl.to(
sliderValue,
{
value: 82,
duration: 4.4,
ease: "none",
onUpdate: function () {
sliderPct.textContent = Math.round(sliderValue.value) + "%";
},
},
1.2,
);
tl.to("#slider-fill", { width: "82%", duration: 4.4, ease: "none" }, 1.2);
tl.to(
"#live-dot",
{ scale: 1.45, duration: 0.24, ease: "power2.out", yoyo: true, repeat: 4 },
2.6,
);
tl.to("#toggle-pill", { x: 419, duration: 0.24, ease: "power2.out" }, 4.15);
// Settle + exit. Split so glass panels keep relative top (exempt, html-in-canvas)
// and plain-DOM text overlays use the relative transform equivalent (y). Inline
// array literals so the linter can resolve each group's targets; a pre-declared
// array variable resolves to __unresolved__ and would lose the canvas exemption.
tl.to(
[gs, gt, gm, gsl],
{ top: "-=8", duration: 0.22, ease: "power2.out", yoyo: true, repeat: 1 },
5.55,
);
tl.to(
[ts, tt, tm, tsl],
{ y: "-=8", duration: 0.22, ease: "power2.out", yoyo: true, repeat: 1 },
5.55,
);
tl.to([gs, gt, gm, gsl], { top: "+=760", duration: 0.42, ease: "power2.in" }, 6.72);
tl.to([ts, tt, tm, tsl], { y: "+=760", duration: 0.42, ease: "power2.in" }, 6.72);
/* Duration driver */
tl.to({ v: 0 }, { v: 1, duration: DURATION, ease: "none" }, 0);
tl.eventCallback("onUpdate", function () {
requestGlassRender(tl.time());
});
window.__timelines["liquid-glass-media-controls"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="liquid-glass-media-controls" data-composition-src="compositions/liquid-glass-media-controls.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+17 -496
View File
@@ -3,519 +3,40 @@ title: "Liquid Glass Notification"
description: "Frosted glass notification cards floating over an aurora shader background"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/liquid-glass-notification.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Liquid Glass Notification preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-notification.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-notification.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add liquid-glass-notification" />
```bash Terminal
npx hyperframes add liquid-glass-notification
```
That writes `compositions/liquid-glass-notification.html`, plus 2 supporting files under `compositions/assets/` and `compositions/lib/`.
That writes `compositions/liquid-glass-notification.html`, plus 2 supporting files under `compositions/lib/` and `compositions/assets/`.
<Danger>
<Warning>
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
Without it this item's screen renders black. Rendering from the CLI switches
it on for you. [How it works](/guides/html-in-canvas)
</Danger>
Rendering from the CLI switches it on for you. [How it
works](/guides/html-in-canvas)
</Warning>
## Source
## Add it to your video
<Accordion title={`liquid-glass-notification.html`}>
It runs for 8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="lib/liquid-glass.iife.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000;
}
body {
font-family: "Inter", sans-serif;
color: #fff;
}
#three-canvas {
position: absolute;
inset: 0;
z-index: 0;
display: block;
}
#glass-canvas {
position: absolute;
inset: 0;
z-index: 1;
display: block;
width: 1920px;
height: 1080px;
}
.text-overlay {
position: absolute;
inset: 0;
z-index: 2;
pointer-events: none;
}
.glass-panel {
position: absolute;
width: 520px;
height: 118px;
background: rgba(255, 255, 255, 0.06);
--lg-blur: 0.22;
--lg-refraction: 0.6;
--lg-corner-radius: 36;
--lg-z-radius: 38;
--lg-specular: 0.2;
--lg-fresnel: 0.9;
--lg-edge-highlight: 0.1;
--lg-chrom-aberration: 0.05;
--lg-shadow-opacity: 0.35;
--lg-shadow-spread: 14;
--lg-shadow-offset-y: 4;
--lg-saturation: 0.25;
}
.glass-panel.p1 {
top: 60px;
left: 1340px;
}
.glass-panel.p2 {
top: 196px;
left: 1340px;
}
.notif-card {
position: absolute;
width: 520px;
height: 118px;
padding: 18px 24px;
display: flex;
align-items: center;
gap: 16px;
color: #fff;
}
.notif-card.c1 {
top: 60px;
left: 1340px;
}
.notif-card.c2 {
top: 196px;
left: 1340px;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 19px;
font-weight: 800;
flex-shrink: 0;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.35),
0 10px 24px rgba(0, 0, 0, 0.24);
}
.avatar-ja {
width: 128px;
border-radius: 20px;
background: rgba(2, 4, 10, 0.72);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
0 10px 24px rgba(0, 0, 0, 0.24);
}
.hyperframes-logo {
width: 102px;
height: auto;
display: block;
}
.avatar-kl {
background: linear-gradient(135deg, #ff7ab6, #8b5cf6);
}
.notif-body {
flex: 1;
min-width: 0;
}
.notif-header {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 3px;
}
.notif-name {
font-size: 20px;
font-weight: 700;
}
.notif-time {
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.55);
}
.notif-text {
font-size: 17px;
font-weight: 500;
color: rgba(255, 255, 255, 0.88);
line-height: 1.32;
}
.notif-progress {
position: absolute;
right: 26px;
bottom: 15px;
left: 168px;
height: 3px;
overflow: hidden;
border-radius: 999px;
background: rgba(255, 255, 255, 0.18);
}
.notif-progress-fill {
width: 0%;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #06e3fa, #4fdb5e);
box-shadow: 0 0 16px rgba(79, 219, 94, 0.55);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #06e3fa;
box-shadow: 0 0 16px rgba(6, 227, 250, 0.65);
}
.notif-actions {
position: absolute;
right: 22px;
bottom: 13px;
display: flex;
gap: 8px;
opacity: 0;
}
.action-chip {
padding: 5px 11px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.16);
color: rgba(255, 255, 255, 0.94);
font-size: 12px;
font-weight: 750;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
.typing-dots {
display: inline-flex;
gap: 3px;
margin-left: 5px;
transform: translateY(-1px);
}
.typing-dots span {
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.72);
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="liquid-glass-notification"
data-composition-src="compositions/liquid-glass-notification.html"
data-start="0"
data-duration="8"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<canvas id="three-canvas"></canvas>
<canvas id="glass-canvas" layoutsubtree width="1920" height="1080">
<div class="glass-panel liquid-glass p1" id="gp1"></div>
<div class="glass-panel liquid-glass p2" id="gp2"></div>
</canvas>
<div class="text-overlay">
<div class="notif-card c1" id="txt1">
<div class="avatar avatar-ja">
<img
class="hyperframes-logo"
src="assets/hyperframes-logo-dark.svg"
alt="HyperFrames"
/>
</div>
<div class="notif-body">
<div class="notif-header">
<span class="notif-name">Render complete</span>
<span class="status-dot" id="status-dot"></span>
<span class="notif-time" id="render-status">rendering</span>
</div>
<div class="notif-text" id="render-copy">
Liquid glass preview is encoding with GPU capture.
</div>
<div class="notif-progress">
<div class="notif-progress-fill" id="render-progress"></div>
</div>
</div>
</div>
<div class="notif-card c2" id="txt2">
<div class="avatar avatar-kl">M</div>
<div class="notif-body">
<div class="notif-header">
<span class="notif-name">Messages</span>
<span class="notif-time">now</span>
</div>
<div class="notif-text">
Miguel: can it feel alive but still native?
<span class="typing-dots" id="typing-dots"
><span></span><span></span><span></span
></span>
</div>
<div class="notif-actions" id="reply-actions">
<span class="action-chip">Reply</span>
<span class="action-chip">Mark Read</span>
</div>
</div>
</div>
</div>
</div>
<script>
(function () {
var threeCanvas = document.getElementById("three-canvas");
var threeRenderer = new THREE.WebGLRenderer({
canvas: threeCanvas,
antialias: false,
preserveDrawingBuffer: true,
});
threeRenderer.setSize(1920, 1080);
threeRenderer.setPixelRatio(1);
var threeScene = new THREE.Scene();
var threeCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
var fs = [
"precision highp float;",
"varying vec2 vUv;",
"uniform float uTime;",
"vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}",
"vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}",
"vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}",
"vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}",
"float snoise(vec3 v){",
" const vec2 C=vec2(1.0/6.0,1.0/3.0);const vec4 D=vec4(0.0,0.5,1.0,2.0);",
" vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx);",
" vec3 g=step(x0.yzx,x0.xyz);vec3 l=1.0-g;",
" vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy);",
" vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy;",
" i=mod289(i);",
" vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));",
" float n_=0.142857142857;vec3 ns=n_*D.wyz-D.xzx;",
" vec4 j=p-49.0*floor(p*ns.z*ns.z);vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.0*x_);",
" vec4 x=x_*ns.x+ns.yyyy;vec4 y=y_*ns.x+ns.yyyy;vec4 h=1.0-abs(x)-abs(y);",
" vec4 b0=vec4(x.xy,y.xy);vec4 b1=vec4(x.zw,y.zw);",
" vec4 s0=floor(b0)*2.0+1.0;vec4 s1=floor(b1)*2.0+1.0;",
" vec4 sh=-step(h,vec4(0.0));",
" vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;",
" vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y);vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w);",
" vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));",
" p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;",
" vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);m=m*m;",
" return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));",
"}",
"void main(){",
" vec2 uv=vUv;float ar=1920.0/1080.0;vec2 p=vec2(uv.x*ar,uv.y);float t=uTime*0.06;",
" float wx=snoise(vec3(p*1.4,t*0.5))*0.2;float wy=snoise(vec3(p*1.4+100.0,t*0.5))*0.2;",
" vec2 wp=p+vec2(wx,wy);",
" vec3 base=mix(vec3(0.10,0.02,0.22),vec3(0.04,0.10,0.25),smoothstep(0.0,1.0,uv.x*0.6+uv.y*0.4));",
" float angle=0.65;float ridgeUv=dot(wp,vec2(cos(angle),sin(angle)));",
" float ridge1=sin(ridgeUv*8.0+t*2.0+snoise(vec3(wp*0.8,t))*1.5);",
" float ridge2=sin(ridgeUv*5.5-t*1.4+snoise(vec3(wp*1.2+50.0,t*0.7))*2.0);",
" float ridge3=sin(ridgeUv*12.0+t*3.0+snoise(vec3(wp*0.6+80.0,t*0.4))*1.0);",
" float crease1=pow(abs(ridge1),0.35)*sign(ridge1)*0.5+0.5;",
" float crease2=pow(abs(ridge2),0.4)*sign(ridge2)*0.5+0.5;",
" float crease3=pow(abs(ridge3),0.5)*sign(ridge3)*0.5+0.5;",
" vec3 warm=mix(vec3(0.9,0.4,0.08),vec3(0.95,0.65,0.15),crease1);",
" float warmZone=smoothstep(0.15,0.75,uv.x+uv.y*0.4+snoise(vec3(uv*2.0,t))*0.3);",
" vec3 cool=mix(vec3(0.02,0.4,0.55),vec3(0.25,0.7,0.82),crease2);",
" float coolZone=smoothstep(0.25,0.85,1.2-uv.x+uv.y*0.5+snoise(vec3(uv*2.0+40.0,t*0.8))*0.25);",
" vec3 hot=mix(vec3(0.8,0.1,0.4),vec3(0.6,0.08,0.65),crease3);",
" float hotZone=smoothstep(0.5,0.9,snoise(vec3(uv*2.5+20.0,t*0.6))*0.5+0.5);",
" vec3 col=base;",
" col=mix(col,warm,warmZone*0.75*smoothstep(0.3,0.7,crease1));",
" col=mix(col,cool,coolZone*0.65*smoothstep(0.2,0.65,crease2));",
" col=mix(col,hot,hotZone*0.5*smoothstep(0.35,0.8,crease3));",
" float edgeGlow=pow(1.0-abs(ridge1),6.0)*0.4+pow(1.0-abs(ridge2),5.0)*0.25;",
" col+=edgeGlow*mix(warm,cool,uv.x)*0.8;",
" col*=1.0-pow(abs(ridge1),4.0)*0.15;",
" float vig=1.0-smoothstep(0.5,1.5,length((uv-0.5)*vec2(1.3,1.0)));",
" col*=0.65+vig*0.5;",
" float luma=dot(col,vec3(0.299,0.587,0.114));col=mix(vec3(luma),col,1.5);",
" col=clamp(col,0.0,1.0);col=pow(col,vec3(0.9));",
" gl_FragColor=vec4(col,1.0);",
"}",
].join("\n");
var uniforms = { uTime: { value: 0.0 } };
threeScene.add(
new THREE.Mesh(
new THREE.PlaneGeometry(2, 2),
new THREE.ShaderMaterial({
vertexShader:
"varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); }",
fragmentShader: fs,
uniforms: uniforms,
}),
),
);
threeRenderer.render(threeScene, threeCamera);
var glassCanvas = document.getElementById("glass-canvas");
var glassCtx = glassCanvas.getContext("2d");
var lg = new LiquidGlass.LiquidGlassCanvas(glassCanvas);
function renderFrame(time) {
uniforms.uTime.value = time;
threeRenderer.render(threeScene, threeCamera);
if (!lg.isReady) return;
glassCtx.clearRect(0, 0, 1920, 1080);
glassCtx.drawImage(threeCanvas, 0, 0, 1920, 1080);
lg.renderGlassElements();
}
var lgReady = false;
lg.waitForInit().then(function (ok) {
if (!ok) return;
lgReady = true;
glassCanvas.onpaint = function () {
renderFrame(0);
};
if (glassCanvas.requestPaint) glassCanvas.requestPaint();
});
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
tl.to(
{},
{
duration: 8,
ease: "none",
onUpdate: function () {
var t = tl.time();
uniforms.uTime.value = t;
threeRenderer.render(threeScene, threeCamera);
if (lgReady) {
glassCanvas.onpaint = function () {
glassCtx.clearRect(0, 0, 1920, 1080);
glassCtx.drawImage(threeCanvas, 0, 0, 1920, 1080);
lg.renderGlassElements();
};
if (glassCanvas.requestPaint) glassCanvas.requestPaint();
}
},
},
0,
);
var renderStatus = document.getElementById("render-status");
var renderCopy = document.getElementById("render-copy");
gsap.set(["#gp1", "#gp2"], { left: "1980px", opacity: 0 });
gsap.set(["#txt1", "#txt2"], { x: 640, opacity: 0 });
gsap.set("#reply-actions", { y: 8, opacity: 0 });
gsap.set("#typing-dots span", { opacity: 0.35 });
tl.to("#gp1", { left: "1326px", opacity: 1, duration: 0.38, ease: "power3.out" }, 0.18);
tl.to("#txt1", { x: -14, opacity: 1, duration: 0.38, ease: "power3.out" }, 0.18);
tl.to("#gp1", { left: "1340px", duration: 0.16, ease: "power2.out" }, 0.56);
tl.to("#txt1", { x: 0, duration: 0.16, ease: "power2.out" }, 0.56);
tl.to("#gp2", { left: "1326px", opacity: 1, duration: 0.36, ease: "power3.out" }, 0.82);
tl.to("#txt2", { x: -14, opacity: 1, duration: 0.36, ease: "power3.out" }, 0.82);
tl.to("#gp2", { left: "1340px", duration: 0.16, ease: "power2.out" }, 1.18);
tl.to("#txt2", { x: 0, duration: 0.16, ease: "power2.out" }, 1.18);
tl.to("#render-progress", { width: "100%", duration: 3.4, ease: "none" }, 0.82);
tl.to(
"#status-dot",
{ scale: 1.45, duration: 0.18, ease: "power2.out", yoyo: true, repeat: 1 },
1.1,
);
tl.to(
"#typing-dots span",
{
opacity: 1,
y: -2,
duration: 0.22,
ease: "power2.out",
stagger: 0.08,
yoyo: true,
repeat: 5,
},
1.2,
);
tl.to("#gp2", { top: "+=10", duration: 0.18, ease: "power2.out" }, 2.55);
tl.to("#txt2", { y: "+=10", duration: 0.18, ease: "power2.out" }, 2.55);
tl.to("#gp2", { top: "-=10", duration: 0.22, ease: "power2.out" }, 2.73);
tl.to("#txt2", { y: "-=10", duration: 0.22, ease: "power2.out" }, 2.73);
tl.to("#reply-actions", { y: 0, opacity: 1, duration: 0.24, ease: "power2.out" }, 2.72);
tl.to(
"#status-dot",
{
backgroundColor: "#4ade80",
boxShadow: "0 0 18px rgba(74, 222, 128, 0.8)",
duration: 0.15,
},
4.25,
);
tl.call(
function () {
renderStatus.textContent = "complete";
renderCopy.textContent = "liquid-glass-notification.mp4 is ready.";
},
null,
4.25,
);
tl.to(
"#gp1",
{ top: "-=8", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 },
4.25,
);
tl.to(
"#txt1",
{ y: "-=8", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 },
4.25,
);
tl.to("#gp1", { left: "1980px", opacity: 0, duration: 0.42, ease: "power2.in" }, 6.72);
tl.to("#txt1", { x: 640, opacity: 0, duration: 0.42, ease: "power2.in" }, 6.72);
tl.to("#gp2", { left: "1980px", opacity: 0, duration: 0.42, ease: "power2.in" }, 6.9);
tl.to("#txt2", { x: 640, opacity: 0, duration: 0.42, ease: "power2.in" }, 6.9);
window.__timelines["liquid-glass-notification"] = tl;
})();
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="liquid-glass-notification" data-composition-src="compositions/liquid-glass-notification.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+17 -774
View File
@@ -3,797 +3,40 @@ title: "Liquid Glass Widgets"
description: "Frosted glass stat cards, showcase panel and pill chips over an aurora shader background"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/liquid-glass-widgets.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Liquid Glass Widgets preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-widgets.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-widgets.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add liquid-glass-widgets" />
```bash Terminal
npx hyperframes add liquid-glass-widgets
```
That writes `compositions/liquid-glass-widgets.html`, plus 2 supporting files under `compositions/assets/` and `compositions/lib/`.
<Danger>
<Warning>
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
Without it this item's screen renders black. Rendering from the CLI switches
it on for you. [How it works](/guides/html-in-canvas)
</Danger>
Rendering from the CLI switches it on for you. [How it
works](/guides/html-in-canvas)
</Warning>
## Source
## Add it to your video
<Accordion title={`liquid-glass-widgets.html`}>
It runs for 8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Liquid Glass Widgets</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="lib/liquid-glass.iife.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0218;
overflow: hidden;
font-family: "Inter", system-ui, sans-serif;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
#three-canvas {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
z-index: 0;
}
#glass-canvas {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
z-index: 1;
}
/* ── Glass Panels ── */
.glass-panel {
position: absolute;
background: rgba(255, 255, 255, 0.025);
--lg-blur: 0.42;
--lg-refraction: 0.82;
--lg-corner-radius: 28;
--lg-z-radius: 46;
--lg-specular: 0.34;
--lg-fresnel: 1.25;
--lg-edge-highlight: 0.26;
--lg-chrom-aberration: 0.08;
--lg-shadow-opacity: 0;
--lg-shadow-spread: 0;
--lg-shadow-offset-y: 0;
--lg-saturation: 0.38;
}
.stat-card {
width: 220px;
height: 220px;
}
.showcase-card {
width: 500px;
height: 320px;
}
.pill-chip {
width: 130px;
height: 36px;
--lg-corner-radius: 40;
}
#glass-stat1 {
top: 300px;
left: 200px;
}
#glass-stat2 {
top: 300px;
left: 460px;
}
#glass-stat3 {
top: 300px;
left: 720px;
}
#glass-showcase {
top: 380px;
left: 1100px;
}
#glass-pill1 {
top: 720px;
left: 300px;
}
#glass-pill2 {
top: 720px;
left: 460px;
}
#glass-pill3 {
top: 720px;
left: 620px;
}
/* ── Text Overlays ── */
.text-overlay {
position: absolute;
z-index: 2;
pointer-events: none;
}
/* Stat cards */
.stat-text {
width: 220px;
height: 220px;
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 22px;
border-radius: 28px;
background:
linear-gradient(
150deg,
rgba(255, 255, 255, 0.28),
rgba(255, 255, 255, 0.06) 46%,
rgba(255, 255, 255, 0.16)
),
linear-gradient(180deg, rgba(9, 18, 31, 0.2), rgba(255, 255, 255, 0.04));
border: 1px solid rgba(255, 255, 255, 0.36);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.5),
inset 0 -18px 38px rgba(255, 255, 255, 0.08);
overflow: hidden;
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.38);
}
.stat-text::before,
.showcase-text::before,
.pill-text::before {
content: "";
position: absolute;
inset: 1px;
border-radius: inherit;
pointer-events: none;
background: linear-gradient(
120deg,
rgba(255, 255, 255, 0.44),
transparent 28%,
transparent 66%,
rgba(255, 255, 255, 0.2)
);
opacity: 0.32;
mix-blend-mode: screen;
}
.stat-label {
position: relative;
font-size: 14px;
font-weight: 650;
color: #fff;
text-transform: uppercase;
letter-spacing: 1.5px;
}
.stat-value {
position: relative;
font-size: 52px;
font-weight: 700;
color: #fff;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.stat-unit {
position: relative;
font-size: 24px;
font-weight: 500;
color: #fff;
}
.mini-meter {
position: relative;
width: 140px;
height: 6px;
overflow: hidden;
border-radius: 999px;
background: rgba(255, 255, 255, 0.28);
}
.mini-meter-fill {
width: 0%;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #06e3fa, #4fdb5e);
box-shadow: 0 0 12px rgba(79, 219, 94, 0.55);
}
.battery-shell {
position: relative;
width: 118px;
height: 40px;
padding: 4px;
border: 2px solid rgba(255, 255, 255, 0.66);
border-radius: 12px;
}
.battery-shell::after {
content: "";
position: absolute;
top: 12px;
right: -9px;
width: 5px;
height: 14px;
border-radius: 0 5px 5px 0;
background: rgba(255, 255, 255, 0.66);
}
.battery-fill {
width: 72%;
height: 100%;
border-radius: 8px;
background: linear-gradient(90deg, #4ade80, #06e3fa);
box-shadow: 0 0 18px rgba(74, 222, 128, 0.45);
}
.steps-ring {
width: 106px;
height: 106px;
display: grid;
place-items: center;
border-radius: 50%;
background: conic-gradient(#a78bfa 0deg, #06e3fa 180deg, rgba(255, 255, 255, 0.18) 180deg);
}
.steps-ring-inner {
width: 78px;
height: 78px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(7, 10, 18, 0.36);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.22),
inset 0 -8px 20px rgba(0, 0, 0, 0.14);
color: #fff;
font-size: 30px;
font-weight: 800;
font-variant-numeric: tabular-nums;
}
#text-stat1 {
top: 300px;
left: 200px;
}
#text-stat2 {
top: 300px;
left: 460px;
}
#text-stat3 {
top: 300px;
left: 720px;
}
/* Showcase card */
.showcase-text {
width: 500px;
height: 320px;
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 32px;
border-radius: 28px;
background:
radial-gradient(circle at 18% 12%, rgba(255, 255, 255, 0.38), transparent 24%),
linear-gradient(
135deg,
rgba(255, 255, 255, 0.24),
rgba(255, 255, 255, 0.07) 52%,
rgba(255, 255, 255, 0.18)
);
border: 1px solid rgba(255, 255, 255, 0.38);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.5),
inset 0 -24px 48px rgba(255, 255, 255, 0.08);
overflow: hidden;
text-shadow: 0 2px 18px rgba(0, 0, 0, 0.4);
}
.showcase-icon {
position: relative;
width: 168px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
}
.showcase-logo {
width: 150px;
height: auto;
display: block;
}
.showcase-title {
position: relative;
font-size: 28px;
font-weight: 700;
color: #fff;
text-align: center;
line-height: 1.2;
}
.showcase-desc {
position: relative;
font-size: 15px;
font-weight: 500;
color: #fff;
text-align: center;
line-height: 1.5;
max-width: 380px;
}
.showcase-status {
position: relative;
display: flex;
align-items: center;
gap: 8px;
color: #fff;
font-size: 14px;
font-weight: 700;
}
.showcase-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #4ade80;
box-shadow: 0 0 14px rgba(74, 222, 128, 0.7);
}
.showcase-progress {
position: relative;
width: 300px;
height: 7px;
overflow: hidden;
border-radius: 999px;
background: rgba(255, 255, 255, 0.18);
}
.showcase-progress-fill {
width: 18%;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #06e3fa, #4fdb5e);
}
#text-showcase {
top: 380px;
left: 1100px;
}
/* Pill chips */
.pill-text {
width: 130px;
height: 36px;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border-radius: 40px;
background: linear-gradient(130deg, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0.1));
border: 1px solid rgba(255, 255, 255, 0.34);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.48);
overflow: hidden;
}
.pill-dot {
position: relative;
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.dot-green {
background: #4ade80;
}
.dot-blue {
background: #60a5fa;
}
.dot-violet {
background: #a78bfa;
}
.pill-label {
position: relative;
font-size: 13px;
font-weight: 600;
color: #fff;
}
#text-pill1 {
top: 720px;
left: 300px;
}
#text-pill2 {
top: 720px;
left: 460px;
}
#text-pill3 {
top: 720px;
left: 620px;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="liquid-glass-widgets"
data-composition-src="compositions/liquid-glass-widgets.html"
data-start="0"
data-duration="8"
data-track-index="1"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="8"
data-root="true"
>
<canvas id="three-canvas" width="1920" height="1080"></canvas>
<canvas id="glass-canvas" layoutsubtree width="1920" height="1080">
<div id="glass-stat1" class="glass-panel stat-card liquid-glass"></div>
<div id="glass-stat2" class="glass-panel stat-card liquid-glass"></div>
<div id="glass-stat3" class="glass-panel stat-card liquid-glass"></div>
<div id="glass-showcase" class="glass-panel showcase-card liquid-glass"></div>
<div id="glass-pill1" class="glass-panel pill-chip liquid-glass"></div>
<div id="glass-pill2" class="glass-panel pill-chip liquid-glass"></div>
<div id="glass-pill3" class="glass-panel pill-chip liquid-glass"></div>
</canvas>
<!-- Stat cards -->
<div id="text-stat1" class="text-overlay stat-text">
<span class="stat-label">Temperature</span>
<span class="stat-value"
><span id="temp-value">21</span><span class="stat-unit">&deg;</span></span
>
<div class="mini-meter"><div id="temp-meter" class="mini-meter-fill"></div></div>
</div>
<div id="text-stat2" class="text-overlay stat-text">
<span class="stat-label">Battery</span>
<div class="battery-shell"><div id="battery-fill" class="battery-fill"></div></div>
<span class="stat-value"
><span id="battery-value">87</span><span class="stat-unit">%</span></span
>
</div>
<div id="text-stat3" class="text-overlay stat-text">
<span class="stat-label">Steps</span>
<div id="steps-ring" class="steps-ring">
<div id="steps-value" class="steps-ring-inner">8.2K</div>
</div>
</div>
<!-- Showcase card -->
<div id="text-showcase" class="text-overlay showcase-text">
<div class="showcase-icon">
<img class="showcase-logo" src="assets/hyperframes-logo-dark.svg" alt="HyperFrames" />
</div>
<div class="showcase-title">Preview pipeline</div>
<div class="showcase-desc">
Write HTML, render video. Ship compositions at the speed of code.
</div>
<div class="showcase-status">
<span class="showcase-dot" id="showcase-dot"></span
><span id="showcase-status">Encoding frames</span>
</div>
<div class="showcase-progress">
<div id="showcase-progress" class="showcase-progress-fill"></div>
</div>
</div>
<!-- Pill chips -->
<div id="text-pill1" class="text-overlay pill-text">
<span class="pill-dot dot-green"></span>
<span class="pill-label">Connected</span>
</div>
<div id="text-pill2" class="text-overlay pill-text">
<span class="pill-dot dot-blue"></span>
<span class="pill-label">Bluetooth</span>
</div>
<div id="text-pill3" class="text-overlay pill-text">
<span class="pill-dot dot-violet"></span>
<span class="pill-label">Focus</span>
</div>
<div
id="driver"
class="clip"
data-start="0"
data-duration="8"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
var W = 1920,
H = 1080,
DURATION = 8;
/* ── Three.js Aurora Shader ─────────────────────────────────────── */
var vs = "varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); }";
var fs = [
"precision highp float;",
"varying vec2 vUv;",
"uniform float uTime;",
"vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}",
"vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}",
"vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}",
"vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}",
"float snoise(vec3 v){",
" const vec2 C=vec2(1.0/6.0,1.0/3.0);const vec4 D=vec4(0.0,0.5,1.0,2.0);",
" vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx);",
" vec3 g=step(x0.yzx,x0.xyz);vec3 l=1.0-g;",
" vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy);",
" vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy;",
" i=mod289(i);",
" vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));",
" float n_=0.142857142857;vec3 ns=n_*D.wyz-D.xzx;",
" vec4 j=p-49.0*floor(p*ns.z*ns.z);vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.0*x_);",
" vec4 x=x_*ns.x+ns.yyyy;vec4 y=y_*ns.x+ns.yyyy;vec4 h=1.0-abs(x)-abs(y);",
" vec4 b0=vec4(x.xy,y.xy);vec4 b1=vec4(x.zw,y.zw);",
" vec4 s0=floor(b0)*2.0+1.0;vec4 s1=floor(b1)*2.0+1.0;",
" vec4 sh=-step(h,vec4(0.0));",
" vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;",
" vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y);vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w);",
" vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));",
" p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;",
" vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);m=m*m;",
" return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));",
"}",
"void main(){",
" vec2 uv=vUv;float ar=1920.0/1080.0;vec2 p=vec2(uv.x*ar,uv.y);float t=uTime*0.06;",
" float wx=snoise(vec3(p*1.4,t*0.5))*0.2;float wy=snoise(vec3(p*1.4+100.0,t*0.5))*0.2;",
" vec2 wp=p+vec2(wx,wy);",
" vec3 base=mix(vec3(0.10,0.02,0.22),vec3(0.04,0.10,0.25),smoothstep(0.0,1.0,uv.x*0.6+uv.y*0.4));",
" float angle=0.65;float ridgeUv=dot(wp,vec2(cos(angle),sin(angle)));",
" float ridge1=sin(ridgeUv*8.0+t*2.0+snoise(vec3(wp*0.8,t))*1.5);",
" float ridge2=sin(ridgeUv*5.5-t*1.4+snoise(vec3(wp*1.2+50.0,t*0.7))*2.0);",
" float ridge3=sin(ridgeUv*12.0+t*3.0+snoise(vec3(wp*0.6+80.0,t*0.4))*1.0);",
" float crease1=pow(abs(ridge1),0.35)*sign(ridge1)*0.5+0.5;",
" float crease2=pow(abs(ridge2),0.4)*sign(ridge2)*0.5+0.5;",
" float crease3=pow(abs(ridge3),0.5)*sign(ridge3)*0.5+0.5;",
" vec3 warm=mix(vec3(0.9,0.4,0.08),vec3(0.95,0.65,0.15),crease1);",
" float warmZone=smoothstep(0.15,0.75,uv.x+uv.y*0.4+snoise(vec3(uv*2.0,t))*0.3);",
" vec3 cool=mix(vec3(0.02,0.4,0.55),vec3(0.25,0.7,0.82),crease2);",
" float coolZone=smoothstep(0.25,0.85,1.2-uv.x+uv.y*0.5+snoise(vec3(uv*2.0+40.0,t*0.8))*0.25);",
" vec3 hot=mix(vec3(0.8,0.1,0.4),vec3(0.6,0.08,0.65),crease3);",
" float hotZone=smoothstep(0.5,0.9,snoise(vec3(uv*2.5+20.0,t*0.6))*0.5+0.5);",
" vec3 col=base;",
" col=mix(col,warm,warmZone*0.75*smoothstep(0.3,0.7,crease1));",
" col=mix(col,cool,coolZone*0.65*smoothstep(0.2,0.65,crease2));",
" col=mix(col,hot,hotZone*0.5*smoothstep(0.35,0.8,crease3));",
" float edgeGlow=pow(1.0-abs(ridge1),6.0)*0.4+pow(1.0-abs(ridge2),5.0)*0.25;",
" col+=edgeGlow*mix(warm,cool,uv.x)*0.8;",
" col*=1.0-pow(abs(ridge1),4.0)*0.15;",
" float vig=1.0-smoothstep(0.5,1.5,length((uv-0.5)*vec2(1.3,1.0)));",
" col*=0.65+vig*0.5;",
" float luma=dot(col,vec3(0.299,0.587,0.114));col=mix(vec3(luma),col,1.5);",
" col=clamp(col,0.0,1.0);col=pow(col,vec3(0.9));",
" gl_FragColor=vec4(col,1.0);",
"}",
].join("\n");
/* ── Three.js Setup ─────────────────────────────────────────────── */
var threeCanvas = document.getElementById("three-canvas");
var renderer = new THREE.WebGLRenderer({ canvas: threeCanvas, alpha: false });
renderer.setSize(W, H, false);
renderer.setPixelRatio(1);
var scene = new THREE.Scene();
var camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
var uniforms = { uTime: { value: 0 } };
var shaderMat = new THREE.ShaderMaterial({
vertexShader: vs,
fragmentShader: fs,
uniforms: uniforms,
});
var quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), shaderMat);
scene.add(quad);
/* ── Glass Canvas ───────────────────────────────────────────────── */
var glassCanvas = document.getElementById("glass-canvas");
var ctx = glassCanvas.getContext("2d");
var lg = new LiquidGlass.LiquidGlassCanvas(glassCanvas);
var lgReady = false;
function renderGlass(time) {
if (!lgReady) return;
uniforms.uTime.value = time;
renderer.render(scene, camera);
ctx.clearRect(0, 0, W, H);
ctx.drawImage(threeCanvas, 0, 0, W, H);
lg.renderGlassElements();
}
function requestGlassRender(time) {
if (glassCanvas.requestPaint) {
glassCanvas.onpaint = function () {
renderGlass(time);
};
glassCanvas.requestPaint();
return;
}
renderGlass(time);
}
lg.waitForInit().then(function (ok) {
if (!ok) return;
lgReady = true;
requestGlassRender(0);
});
/* ── GSAP Timeline ──────────────────────────────────────────────── */
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
/* Glass + text element pairs */
var s1g = document.getElementById("glass-stat1");
var s2g = document.getElementById("glass-stat2");
var s3g = document.getElementById("glass-stat3");
var scg = document.getElementById("glass-showcase");
var p1g = document.getElementById("glass-pill1");
var p2g = document.getElementById("glass-pill2");
var p3g = document.getElementById("glass-pill3");
var s1t = document.getElementById("text-stat1");
var s2t = document.getElementById("text-stat2");
var s3t = document.getElementById("text-stat3");
var sct = document.getElementById("text-showcase");
var p1t = document.getElementById("text-pill1");
var p2t = document.getElementById("text-pill2");
var p3t = document.getElementById("text-pill3");
var tempValue = document.getElementById("temp-value");
var batteryValue = document.getElementById("battery-value");
var stepsValue = document.getElementById("steps-value");
var stepsRing = document.getElementById("steps-ring");
var showcaseStatus = document.getElementById("showcase-status");
var tempCounter = { value: 21 };
var batteryCounter = { value: 87 };
var stepsCounter = { value: 8.2 };
// Glass panels (rasterized by the html-in-canvas API) animate top — the API
// reads sub-pixel getComputedStyle().top, so layout-prop motion does not
// stutter and the lint rule exempts them. Plain-DOM text overlays DO stutter on
// layout props, so they animate the transform equivalent (y). y is relative to
// each text element's CSS resting top (stats 300, showcase 380, pills 720), so the
// initial seed to visual 1160 is y = 1160 - restingTop.
var glassPanels = [s1g, s2g, s3g, scg, p1g, p2g, p3g];
var statText = [s1t, s2t, s3t];
var showcaseText = sct;
var pillText = [p1t, p2t, p3t];
gsap.set(glassPanels, { opacity: 1, top: 1160 });
gsap.set(statText, { opacity: 1, y: 860 }); // 1160 - 300
gsap.set(showcaseText, { opacity: 1, y: 780 }); // 1160 - 380
gsap.set(pillText, { opacity: 1, y: 440 }); // 1160 - 720
tl.to(s1g, { top: 300, duration: 0.46, ease: "power3.out" }, 0.12);
tl.to(s1t, { y: 0, duration: 0.46, ease: "power3.out" }, 0.12); // 300 - 300
tl.to(s2g, { top: 300, duration: 0.46, ease: "power3.out" }, 0.24);
tl.to(s2t, { y: 0, duration: 0.46, ease: "power3.out" }, 0.24); // 300 - 300
tl.to(s3g, { top: 300, duration: 0.46, ease: "power3.out" }, 0.36);
tl.to(s3t, { y: 0, duration: 0.46, ease: "power3.out" }, 0.36); // 300 - 300
tl.to(scg, { top: 380, duration: 0.5, ease: "power3.out" }, 0.5);
tl.to(sct, { y: 0, duration: 0.5, ease: "power3.out" }, 0.5); // 380 - 380
tl.to(p1g, { top: 720, duration: 0.32, ease: "power3.out" }, 0.9);
tl.to(p1t, { y: 0, duration: 0.32, ease: "power3.out" }, 0.9); // 720 - 720
tl.to(p2g, { top: 720, duration: 0.32, ease: "power3.out" }, 1.0);
tl.to(p2t, { y: 0, duration: 0.32, ease: "power3.out" }, 1.0); // 720 - 720
tl.to(p3g, { top: 720, duration: 0.32, ease: "power3.out" }, 1.1);
tl.to(p3t, { y: 0, duration: 0.32, ease: "power3.out" }, 1.1); // 720 - 720
tl.to("#temp-meter", { width: "76%", duration: 1.7, ease: "power2.out" }, 0.9);
tl.to(
tempCounter,
{
value: 25,
duration: 2.2,
ease: "power2.out",
onUpdate: function () {
tempValue.textContent = Math.round(tempCounter.value);
},
},
0.9,
);
tl.to("#battery-fill", { width: "91%", duration: 2.4, ease: "power2.out" }, 1.05);
tl.to(
batteryCounter,
{
value: 91,
duration: 2.4,
ease: "power2.out",
onUpdate: function () {
batteryValue.textContent = Math.round(batteryCounter.value);
},
},
1.05,
);
tl.to(
stepsCounter,
{
value: 9.1,
duration: 2.6,
ease: "power2.out",
onUpdate: function () {
var degrees = Math.round(stepsCounter.value * 31.4);
stepsValue.textContent = stepsCounter.value.toFixed(1) + "K";
stepsRing.style.background =
"conic-gradient(#a78bfa 0deg, #06e3fa " +
degrees +
"deg, rgba(255, 255, 255, 0.18) " +
degrees +
"deg)";
},
},
1.2,
);
tl.to("#showcase-progress", { width: "92%", duration: 4.1, ease: "none" }, 1.2);
tl.to(
"#showcase-dot",
{ scale: 1.5, duration: 0.25, ease: "power2.out", yoyo: true, repeat: 7 },
1.4,
);
tl.call(
function () {
showcaseStatus.textContent = "Preview ready";
},
null,
5.2,
);
tl.to(scg, { top: "-=10", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 }, 5.2);
tl.to(sct, { y: "-=10", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 }, 5.2);
tl.to(
".pill-dot",
{ scale: 1.55, duration: 0.2, ease: "power2.out", stagger: 0.12, yoyo: true, repeat: 3 },
2.2,
);
tl.to(
[s1g, s2g, s3g],
{ top: "-=12", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 },
5.7,
);
tl.to(
[s1t, s2t, s3t],
{ y: "-=12", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 },
5.7,
);
tl.to(
[s1g, s2g, s3g, scg, p1g, p2g, p3g],
{ top: "+=760", duration: 0.42, ease: "power2.in" },
6.74,
);
tl.to(
[s1t, s2t, s3t, sct, p1t, p2t, p3t],
{ y: "+=760", duration: 0.42, ease: "power2.in" },
6.74,
);
/* Duration driver */
tl.to({ v: 0 }, { v: 1, duration: DURATION, ease: "none" }, 0);
tl.eventCallback("onUpdate", function () {
requestGlassRender(tl.time());
});
window.__timelines["liquid-glass-widgets"] = tl;
</script>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="liquid-glass-widgets" data-composition-src="compositions/liquid-glass-widgets.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+23 -247
View File
@@ -3,16 +3,35 @@ title: "Logo Outro"
description: "Cinematic logo reveal with piece-by-piece assembly, glow bloom, tagline fade-in, and URL pill"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/logo-outro.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Logo Outro preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/logo-outro.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/logo-outro.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add logo-outro" />
```bash Terminal
npx hyperframes add logo-outro
```
That writes one file: `compositions/logo-outro.html`.
## Add it to your video
It runs for 6 seconds at 1920×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="logo-outro"
data-composition-src="compositions/logo-outro.html"
data-start="0"
data-duration="6"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Change the colors
Set these CSS variables on the block:
@@ -20,249 +39,6 @@ Set these CSS variables on the block:
- `--bg-color` — Background. Defaults to `#0a0a0f`.
- `--accent-color` — Accent. Defaults to `#1a1a1f`.
## Source
<Accordion title={`logo-outro.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Logo Outro</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=block"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #0a0a0f;
}
</style>
</head>
<body>
<div
id="logo-outro"
data-composition-id="logo-outro"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="6"
>
<div class="canvas">
<!-- Glow effect behind the logo -->
<div class="logo-glow"></div>
<!-- Logo Container -->
<div class="logo-container">
<svg viewBox="0 0 160 240" width="160" height="240" class="logo-svg">
<!-- Replace these paths with your own logo SVG -->
<path
class="logo-piece logo-tl"
d="M 0 40 C 0 17.9 17.9 0 40 0 L 80 0 L 80 80 L 40 80 C 17.9 80 0 62.1 0 40 Z"
fill="#F24E1E"
/>
<path
class="logo-piece logo-tr"
d="M 80 0 L 120 0 C 142.1 0 160 17.9 160 40 C 160 62.1 142.1 80 120 80 L 80 80 L 80 0 Z"
fill="#A259FF"
/>
<path
class="logo-piece logo-ml"
d="M 0 120 C 0 97.9 17.9 80 40 80 L 80 80 L 80 160 L 40 160 C 17.9 160 0 142.1 0 120 Z"
fill="#FF7262"
/>
<circle class="logo-piece logo-mr" cx="120" cy="120" r="40" fill="#1ABCFE" />
<path
class="logo-piece logo-bl"
d="M 0 200 C 0 177.9 17.9 160 40 160 L 80 160 L 80 200 C 80 222.1 62.1 240 40 240 C 17.9 240 0 222.1 0 200 Z"
fill="#0ACF83"
/>
</svg>
</div>
<div class="text-container">
<div class="tagline">Nothing great is made alone.</div>
<div class="url-pill">
<span class="url-text">figma.com</span>
</div>
</div>
</div>
<style>
[data-composition-id="logo-outro"] .canvas {
width: 1920px;
height: 1080px;
background: #0a0a0f;
position: relative;
overflow: hidden;
font-family: "Inter", sans-serif;
}
[data-composition-id="logo-outro"] .logo-container {
position: absolute;
left: 880px;
top: 380px;
width: 160px;
height: 240px;
z-index: 2;
}
[data-composition-id="logo-outro"] .logo-piece {
transform-origin: center center;
opacity: 0;
}
[data-composition-id="logo-outro"] .logo-glow {
position: absolute;
left: 760px;
top: 300px;
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(162, 89, 255, 0.25) 0%, rgba(10, 10, 15, 0) 70%);
border-radius: 50%;
opacity: 0;
pointer-events: none;
z-index: 1;
}
[data-composition-id="logo-outro"] .text-container {
position: absolute;
top: 720px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
z-index: 3;
}
[data-composition-id="logo-outro"] .tagline {
font-size: 44px;
font-weight: 500;
color: white;
opacity: 0;
margin-bottom: 32px;
text-align: center;
}
[data-composition-id="logo-outro"] .url-pill {
background: #1a1a1f;
border: 1px solid #2c2c2c;
border-radius: 100px;
padding: 8px 24px;
opacity: 0;
display: flex;
align-items: center;
justify-content: center;
}
[data-composition-id="logo-outro"] .url-text {
font-size: 18px;
color: #666;
letter-spacing: 0.5px;
}
</style>
<script>
(function () {
const tl = gsap.timeline({ paused: true });
const pieces = {
tl: '[data-composition-id="logo-outro"] .logo-tl',
tr: '[data-composition-id="logo-outro"] .logo-tr',
ml: '[data-composition-id="logo-outro"] .logo-ml',
mr: '[data-composition-id="logo-outro"] .logo-mr',
bl: '[data-composition-id="logo-outro"] .logo-bl',
};
const pieceArray = [pieces.tl, pieces.tr, pieces.ml, pieces.mr, pieces.bl];
const logoGlow = '[data-composition-id="logo-outro"] .logo-glow';
const tagline = '[data-composition-id="logo-outro"] .tagline';
const urlPill = '[data-composition-id="logo-outro"] .url-pill';
// 1. Pieces slide in from the right with ripple stagger
pieceArray.forEach((piece, i) => {
tl.fromTo(
piece,
{ x: 1920, opacity: 0 },
{ x: 0, opacity: 1, duration: 1.2, ease: "power4.out" },
i * 0.08,
);
});
// 2. Expand and close
const expandTime = 1.2;
tl.to(
pieceArray,
{ scale: 1.15, duration: 0.5, ease: "back.out(1.7)", stagger: 0.02 },
expandTime,
);
tl.to(pieceArray, { scale: 1, duration: 0.4, ease: "power2.inOut" }, expandTime + 0.3);
// 3. Glow bloom
tl.fromTo(
logoGlow,
{ opacity: 0, scale: 0.5 },
{ opacity: 1, scale: 1.2, duration: 0.4, ease: "power2.out" },
expandTime,
);
tl.to(
logoGlow,
{ opacity: 0, scale: 1.8, duration: 1.2, ease: "power2.out" },
expandTime + 0.4,
);
// 4. Tagline and URL pill fade in
const textStartTime = expandTime + 0.8;
tl.fromTo(
tagline,
{ opacity: 0, y: 20 },
{ opacity: 1, y: 0, duration: 1, ease: "power3.out" },
textStartTime,
);
tl.fromTo(
urlPill,
{ opacity: 0, y: 15 },
{ opacity: 1, y: 0, duration: 0.8, ease: "power3.out" },
textStartTime + 0.4,
);
window.__timelines = window.__timelines || {};
window.__timelines["logo-outro"] = tl;
})();
</script>
</div>
</body>
</html>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="logo-outro" data-composition-src="compositions/logo-outro.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
```
{/* hf:generated-footer */}
Tagged `branding` `outro` `logo`.
+19 -170
View File
@@ -3,186 +3,35 @@ title: "Lower Third — BILD Style"
description: "News-style lower third with tight-fit text boxes: white headline bar with red drop-shadow, red sub-line with white drop-shadow."
---
import { InstallCommand } from "/snippets/install-command.jsx";
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
<VariablesExplorer
previewSrc="/public/catalog/preview/blocks/lower-third-bild.html"
compositionId="lower-third-bild"
compositionSrc="compositions/lower-third-bild.html"
variables={[{"id":"TXT_MAIN_1_Line","type":"string","default":"BILD EXKLUSIV"},{"id":"TXT_SUB","type":"string","default":"Sonderbericht"}]}
>
```html lower-third-bild.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
@font-face {
font-family: "Barlow Condensed";
font-style: normal;
font-weight: 700;
font-display: block;
src: url("assets/fonts/BarlowCondensed-Bold.woff2") format("woff2");
}
@font-face {
font-family: "Barlow Condensed";
font-style: normal;
font-weight: 800;
font-display: block;
src: url("assets/fonts/BarlowCondensed-ExtraBold.woff2") format("woff2");
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
}
#lb-root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent;
}
#lb-main-outer {
position: absolute;
bottom: 215px;
left: 80px;
padding-right: 14px;
padding-bottom: 14px;
}
#lb-main-text {
font-family: "Barlow Condensed", sans-serif;
font-size: 88px;
font-weight: 800;
line-height: 1.05;
color: #212529;
background: #ffffff;
padding: 10px 22px 8px;
white-space: nowrap;
text-transform: uppercase;
letter-spacing: 0.02em;
box-shadow: 12px 12px 0 0 #dd0000;
}
#lb-sub-outer {
position: absolute;
bottom: 135px;
left: 80px;
padding-right: 10px;
padding-bottom: 10px;
}
#lb-sub-text {
font-family: "Barlow Condensed", sans-serif;
font-size: 48px;
font-weight: 700;
line-height: 1.1;
color: #ffffff;
background: #dd0000;
padding: 6px 18px 5px;
white-space: nowrap;
text-transform: uppercase;
letter-spacing: 0.04em;
box-shadow: 8px 8px 0 0 #ffffff;
}
</style>
</head>
<body>
<div
id="lb-root"
data-composition-id="lower-third-bild"
data-composition-variables='{"TXT_MAIN_1_Line":{"type":"string","default":"BILD EXKLUSIV"},"TXT_SUB":{"type":"string","default":"Sonderbericht"}}'
data-start="0"
data-duration="8"
data-width="1920"
data-height="1080"
>
<div
id="lb-drv"
class="clip"
data-start="0"
data-duration="8"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
<div id="lb-main-outer">
<div id="lb-main-text"></div>
</div>
<div id="lb-sub-outer">
<div id="lb-sub-text"></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
var vars =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
var mainText = (vars.TXT_MAIN_1_Line || "BILD EXKLUSIV").trim();
var subText = (vars.TXT_SUB || "Sonderbericht").trim();
var dur = parseFloat(document.getElementById("lb-drv").dataset.duration) || 8;
document.getElementById("lb-main-text").textContent = mainText;
document.getElementById("lb-sub-text").textContent = subText || " ";
var mainOuter = document.getElementById("lb-main-outer");
var subOuter = document.getElementById("lb-sub-outer");
var tl = gsap.timeline({ paused: true });
gsap.set([mainOuter, subOuter], { opacity: 1 });
tl.set([mainOuter, subOuter], { opacity: 0, visibility: "hidden" }, dur);
window.__timelines["lower-third-bild"] = tl;
})();
</script>
</div>
</body>
</html>
```
</VariablesExplorer>
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lower-third-bild.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lower-third-bild.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add lower-third-bild" />
```bash Terminal
npx hyperframes add lower-third-bild
```
That writes `compositions/lower-third-bild.html`, plus 2 supporting files under `compositions/assets/fonts/`.
## Usage
## Add it to your video
After installing, add the block to your host composition:
It runs for 8 seconds at 1920×1080. Paste this into your composition:
```html
<div data-composition-id="lower-third-bild" data-composition-src="compositions/lower-third-bild.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
```html index.html
<div
data-composition-id="lower-third-bild"
data-composition-src="compositions/lower-third-bild.html"
data-start="0"
data-duration="8"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
Tagged `broadcast` `lower-third` `news` `overlay`.
+14 -118
View File
@@ -3,138 +3,34 @@ title: "Lower Third — Accent Underline"
description: "Cardless lower third for footage overlay: name rises, an accent rule draws left-to-right, role fades in; text-shadowed for legibility"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/lt-accent-underline.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Accent Underline preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-accent-underline.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-accent-underline.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add lt-accent-underline" />
```bash Terminal
npx hyperframes add lt-accent-underline
```
That writes one file: `compositions/lt-accent-underline.html`.
## Source
## Add it to your video
<Accordion title={`lt-accent-underline.html`}>
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
}
#root .lt {
position: absolute;
left: 130px;
bottom: 120px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
}
#root .lt-name {
font-family: "Oswald", sans-serif;
font-size: 76px;
font-weight: 700;
color: #ffffff;
line-height: 0.96;
letter-spacing: 0.005em;
text-transform: uppercase;
white-space: nowrap;
text-shadow: 0 2px 22px rgba(0, 0, 0, 0.45);
}
#root .lt-rule {
height: 6px;
width: 100%;
background: #46e5b7;
border-radius: 3px;
transform-origin: 0% 50%;
}
#root .lt-role {
font-family: "Space Mono", monospace;
font-size: 28px;
font-weight: 400;
color: #e7eaf0;
line-height: 1.2;
letter-spacing: 0.04em;
white-space: nowrap;
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.45);
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="lt-accent-underline"
data-width="1920"
data-height="1080"
data-composition-src="compositions/lt-accent-underline.html"
data-start="0"
data-duration="4.8"
>
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
<div id="lt" class="lt">
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
<div id="lt-rule" class="lt-rule"></div>
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var wrap = document.getElementById("lt");
var nameEl = document.getElementById("lt-name");
var rule = document.getElementById("lt-rule");
var role = document.getElementById("lt-role");
// Name slides up, accent rule draws left→right, role fades up under it.
gsap.set(nameEl, { y: 28, opacity: 0 });
gsap.set(rule, { scaleX: 0 });
gsap.set(role, { y: 16, opacity: 0 });
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.55, ease: "power3.out" }, 0.1);
tl.to(rule, { scaleX: 1, duration: 0.5, ease: "power4.out" }, 0.3);
tl.to(role, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.46);
// Exit: role fades, rule retracts, name lifts, then hard kill.
tl.to(role, { opacity: 0, duration: 0.3, ease: "power2.in" }, 4.25);
tl.to(rule, { scaleX: 0, duration: 0.3, ease: "power2.in" }, 4.3);
tl.to(nameEl, { y: -16, opacity: 0, duration: 0.32, ease: "power2.in" }, 4.35);
tl.set(wrap, { visibility: "hidden" }, 4.75);
window.__timelines["lt-accent-underline"] = tl;
</script>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="lt-accent-underline" data-composition-src="compositions/lt-accent-underline.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+14 -120
View File
@@ -3,140 +3,34 @@ title: "Lower Third — Bold Block"
description: "High-energy podcast lower third: solid dark block wipes in, uppercase name slams up, accent tag pops"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/lt-bold-block.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Bold Block preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-bold-block.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-bold-block.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add lt-bold-block" />
```bash Terminal
npx hyperframes add lt-bold-block
```
That writes one file: `compositions/lt-bold-block.html`.
## Source
## Add it to your video
<Accordion title={`lt-bold-block.html`}>
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
}
#root .lt {
position: absolute;
left: 120px;
bottom: 116px;
}
#root .lt-block {
background: #141518;
padding: 18px 34px 22px 30px;
display: inline-flex;
flex-direction: column;
gap: 12px;
will-change: clip-path;
}
#root .lt-name {
font-family: "Archivo Black", sans-serif;
font-size: 58px;
font-weight: 400;
color: #ffffff;
line-height: 0.98;
letter-spacing: -0.015em;
text-transform: uppercase;
white-space: nowrap;
}
#root .lt-tagwrap {
overflow: hidden;
}
#root .lt-tag {
display: inline-block;
background: #ffd23f;
color: #141518;
font-family: "Space Mono", monospace;
font-size: 24px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 7px 16px;
white-space: nowrap;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="lt-bold-block"
data-width="1920"
data-height="1080"
data-composition-src="compositions/lt-bold-block.html"
data-start="0"
data-duration="4.8"
>
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
<div id="lt" class="lt">
<div id="lt-block" class="lt-block">
<div id="lt-name" class="lt-name">Maya Chen</div>
<div class="lt-tagwrap">
<span id="lt-tag" class="lt-tag">Host · Neuroscientist</span>
</div>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var wrap = document.getElementById("lt");
var block = document.getElementById("lt-block");
var nameEl = document.getElementById("lt-name");
var tag = document.getElementById("lt-tag");
// Solid block wipes in from the left, name slams up inside it, tag pops up after.
gsap.set(block, { clipPath: "inset(0 100% 0 0)" });
gsap.set(nameEl, { y: 30, opacity: 0 });
gsap.set(tag, { y: 40 });
tl.to(block, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "power4.out" }, 0.1);
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.42, ease: "back.out(1.6)" }, 0.26);
tl.to(tag, { y: 0, duration: 0.45, ease: "back.out(2)" }, 0.5);
// Exit: block wipes out to the left, then hard kill.
tl.to(block, { clipPath: "inset(0 0 0 100%)", duration: 0.4, ease: "power3.in" }, 4.3);
tl.set(wrap, { visibility: "hidden" }, 4.75);
window.__timelines["lt-bold-block"] = tl;
</script>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="lt-bold-block" data-composition-src="compositions/lt-bold-block.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+14 -123
View File
@@ -3,143 +3,34 @@ title: "Lower Third — Clean Bar"
description: "Minimal white-card lower third for podcasts/interviews: accent tab, name + role, clip-wipe entrance"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/lt-clean-bar.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Clean Bar preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-clean-bar.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-clean-bar.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add lt-clean-bar" />
```bash Terminal
npx hyperframes add lt-clean-bar
```
That writes one file: `compositions/lt-clean-bar.html`.
## Source
## Add it to your video
<Accordion title={`lt-clean-bar.html`}>
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
font-family: "Montserrat", sans-serif;
}
#root .lt {
position: absolute;
left: 120px;
bottom: 110px;
display: flex;
align-items: stretch;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 44px rgba(15, 17, 21, 0.18);
will-change: clip-path;
}
#root .lt-tab {
width: 12px;
background: #ff5a36;
flex-shrink: 0;
}
#root .lt-body {
background: #ffffff;
padding: 22px 40px 24px 30px;
display: flex;
flex-direction: column;
gap: 7px;
}
#root .lt-name {
font-size: 52px;
font-weight: 700;
color: #0f1115;
line-height: 1.06;
letter-spacing: -0.015em;
white-space: nowrap;
}
#root .lt-role {
font-size: 27px;
font-weight: 400;
color: #5a6170;
line-height: 1.2;
letter-spacing: 0.01em;
white-space: nowrap;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="lt-clean-bar"
data-width="1920"
data-height="1080"
data-composition-src="compositions/lt-clean-bar.html"
data-start="0"
data-duration="4.8"
>
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
<div id="lt" class="lt">
<div id="lt-tab" class="lt-tab"></div>
<div class="lt-body">
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var card = document.getElementById("lt");
var tab = document.getElementById("lt-tab");
var nameEl = document.getElementById("lt-name");
var role = document.getElementById("lt-role");
// Card wipes in from the left (clip-path), accent tab grows, text rises with a small stagger.
gsap.set(card, { clipPath: "inset(0 100% 0 0)" });
gsap.set(tab, { scaleY: 0, transformOrigin: "50% 0%" });
gsap.set(nameEl, { y: 22, opacity: 0 });
gsap.set(role, { y: 22, opacity: 0 });
tl.to(card, { clipPath: "inset(0 0% 0 0)", duration: 0.55, ease: "power3.out" }, 0.1);
tl.to(tab, { scaleY: 1, duration: 0.45, ease: "power2.out" }, 0.28);
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.34);
tl.to(role, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.44);
// Exit: settle out, then hard kill the inner element.
tl.to(card, { y: 18, opacity: 0, duration: 0.35, ease: "power2.in" }, 4.3);
tl.set(card, { visibility: "hidden" }, 4.75);
window.__timelines["lt-clean-bar"] = tl;
</script>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="lt-clean-bar" data-composition-src="compositions/lt-clean-bar.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+14 -112
View File
@@ -3,132 +3,34 @@ title: "Lower Third — Color Block"
description: "High-energy lower third: bold accent-color block slides in with overshoot, condensed name + mono role"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/lt-color-block.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Color Block preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-color-block.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-color-block.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add lt-color-block" />
```bash Terminal
npx hyperframes add lt-color-block
```
That writes one file: `compositions/lt-color-block.html`.
## Source
## Add it to your video
<Accordion title={`lt-color-block.html`}>
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
}
#root .lt {
position: absolute;
left: 120px;
bottom: 116px;
}
#root .lt-block {
background: #2756ff;
padding: 20px 36px 24px 32px;
box-shadow: 0 16px 44px rgba(39, 86, 255, 0.34);
display: inline-flex;
flex-direction: column;
gap: 8px;
}
#root .lt-name {
font-family: "League Gothic", sans-serif;
font-size: 84px;
font-weight: 400;
color: #ffffff;
line-height: 0.86;
letter-spacing: 0.01em;
text-transform: uppercase;
white-space: nowrap;
}
#root .lt-role {
font-family: "Space Mono", monospace;
font-size: 24px;
font-weight: 400;
color: rgba(255, 255, 255, 0.86);
line-height: 1.2;
letter-spacing: 0.05em;
white-space: nowrap;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="lt-color-block"
data-width="1920"
data-height="1080"
data-composition-src="compositions/lt-color-block.html"
data-start="0"
data-duration="4.8"
>
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
<div id="lt" class="lt">
<div id="lt-block" class="lt-block">
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var wrap = document.getElementById("lt");
var block = document.getElementById("lt-block");
var nameEl = document.getElementById("lt-name");
var role = document.getElementById("lt-role");
// Color block slides in from the left and overshoots slightly; name and role rise inside it.
gsap.set(block, { x: -80, opacity: 0 });
gsap.set(nameEl, { y: 24, opacity: 0 });
gsap.set(role, { y: 18, opacity: 0 });
tl.to(block, { x: 0, opacity: 1, duration: 0.5, ease: "back.out(1.4)" }, 0.1);
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.42, ease: "power3.out" }, 0.3);
tl.to(role, { y: 0, opacity: 1, duration: 0.42, ease: "power3.out" }, 0.4);
// Exit: slide out left, hard kill.
tl.to(block, { x: -60, opacity: 0, duration: 0.35, ease: "power2.in" }, 4.3);
tl.set(wrap, { visibility: "hidden" }, 4.75);
window.__timelines["lt-color-block"] = tl;
</script>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="lt-color-block" data-composition-src="compositions/lt-color-block.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+14 -122
View File
@@ -3,142 +3,34 @@ title: "Lower Third — Dark Card"
description: "Charcoal card lower third for bright footage: name, drawn accent underline, role; slide-up entrance"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/lt-dark-card.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Dark Card preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-dark-card.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-dark-card.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add lt-dark-card" />
```bash Terminal
npx hyperframes add lt-dark-card
```
That writes one file: `compositions/lt-dark-card.html`.
## Source
## Add it to your video
<Accordion title={`lt-dark-card.html`}>
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
font-family: "Montserrat", sans-serif;
}
#root .lt {
position: absolute;
left: 120px;
bottom: 110px;
}
#root .lt-card {
background: #16181d;
border-radius: 14px;
padding: 24px 38px 26px 32px;
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
gap: 12px;
}
#root .lt-name {
font-size: 48px;
font-weight: 700;
color: #ffffff;
line-height: 1.02;
letter-spacing: -0.015em;
white-space: nowrap;
}
#root .lt-underline {
height: 4px;
width: 100%;
background: #f5b942;
border-radius: 2px;
transform-origin: 0% 50%;
}
#root .lt-role {
font-size: 25px;
font-weight: 400;
color: #aeb6c2;
line-height: 1.2;
letter-spacing: 0.02em;
white-space: nowrap;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="lt-dark-card"
data-width="1920"
data-height="1080"
data-composition-src="compositions/lt-dark-card.html"
data-start="0"
data-duration="4.8"
>
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
<div id="lt" class="lt">
<div id="lt-card" class="lt-card">
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
<div id="lt-underline" class="lt-underline"></div>
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var wrap = document.getElementById("lt");
var card = document.getElementById("lt-card");
var nameEl = document.getElementById("lt-name");
var underline = document.getElementById("lt-underline");
var role = document.getElementById("lt-role");
// Card slides up from below, name fades, accent underline draws, role fades.
gsap.set(card, { y: 60, opacity: 0 });
gsap.set(nameEl, { y: 14, opacity: 0 });
gsap.set(underline, { scaleX: 0 });
gsap.set(role, { opacity: 0 });
tl.to(card, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.1);
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.45, ease: "power3.out" }, 0.26);
tl.to(underline, { scaleX: 1, duration: 0.5, ease: "power4.out" }, 0.42);
tl.to(role, { opacity: 1, duration: 0.45, ease: "power2.out" }, 0.56);
// Exit: slide down + hard kill.
tl.to(card, { y: 24, opacity: 0, duration: 0.35, ease: "power2.in" }, 4.3);
tl.set(wrap, { visibility: "hidden" }, 4.75);
window.__timelines["lt-dark-card"] = tl;
</script>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="lt-dark-card" data-composition-src="compositions/lt-dark-card.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+14 -125
View File
@@ -3,145 +3,34 @@ title: "Lower Third — Kicker Name"
description: "Cardless lower third with an accent eyebrow/kicker tag, heavy name, and a drawn baseline; for footage"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/lt-kicker-name.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Kicker Name preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-kicker-name.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-kicker-name.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add lt-kicker-name" />
```bash Terminal
npx hyperframes add lt-kicker-name
```
That writes one file: `compositions/lt-kicker-name.html`.
## Source
## Add it to your video
<Accordion title={`lt-kicker-name.html`}>
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
}
#root .lt {
position: absolute;
left: 130px;
bottom: 122px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
}
#root .lt-kickwrap {
overflow: hidden;
}
#root .lt-kicker {
display: inline-block;
font-family: "Space Mono", monospace;
font-size: 22px;
font-weight: 700;
color: #141518;
background: #46e5b7;
padding: 6px 14px;
letter-spacing: 0.1em;
text-transform: uppercase;
white-space: nowrap;
}
#root .lt-name {
font-family: "Archivo Black", sans-serif;
font-size: 70px;
font-weight: 400;
color: #ffffff;
line-height: 0.98;
letter-spacing: -0.015em;
text-transform: uppercase;
white-space: nowrap;
text-shadow: 0 2px 22px rgba(0, 0, 0, 0.5);
}
#root .lt-baseline {
height: 5px;
width: 100%;
background: #46e5b7;
border-radius: 3px;
transform-origin: 0% 50%;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="lt-kicker-name"
data-width="1920"
data-height="1080"
data-composition-src="compositions/lt-kicker-name.html"
data-start="0"
data-duration="4.8"
>
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
<div id="lt" class="lt">
<div class="lt-kickwrap">
<span id="lt-kicker" class="lt-kicker">Episode 47</span>
</div>
<div id="lt-name" class="lt-name">Maya Chen</div>
<div id="lt-baseline" class="lt-baseline"></div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var wrap = document.getElementById("lt");
var kicker = document.getElementById("lt-kicker");
var nameEl = document.getElementById("lt-name");
var baseline = document.getElementById("lt-baseline");
// Kicker drops in, name slams up, accent baseline draws.
gsap.set(kicker, { y: -40 });
gsap.set(nameEl, { y: 34, opacity: 0 });
gsap.set(baseline, { scaleX: 0 });
tl.to(kicker, { y: 0, duration: 0.42, ease: "back.out(2)" }, 0.1);
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.45, ease: "back.out(1.5)" }, 0.32);
tl.to(baseline, { scaleX: 1, duration: 0.5, ease: "power4.out" }, 0.5);
// Exit: baseline retracts, text lifts, hard kill.
tl.to(baseline, { scaleX: 0, duration: 0.3, ease: "power2.in" }, 4.25);
tl.to(nameEl, { y: -16, opacity: 0, duration: 0.32, ease: "power2.in" }, 4.3);
tl.to(kicker, { y: -40, duration: 0.3, ease: "power2.in" }, 4.32);
tl.set(wrap, { visibility: "hidden" }, 4.75);
window.__timelines["lt-kicker-name"] = tl;
</script>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="lt-kicker-name" data-composition-src="compositions/lt-kicker-name.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+14 -131
View File
@@ -3,151 +3,34 @@ title: "Lower Third — Mask Reveal"
description: "Cardless lower third: an accent sweep crosses and clip-path-reveals a heavy name, role fades up; for footage"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/lt-mask-reveal.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Mask Reveal preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-mask-reveal.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-mask-reveal.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add lt-mask-reveal" />
```bash Terminal
npx hyperframes add lt-mask-reveal
```
That writes one file: `compositions/lt-mask-reveal.html`.
## Source
## Add it to your video
<Accordion title={`lt-mask-reveal.html`}>
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
font-family: "Montserrat", sans-serif;
}
#root .lt {
position: absolute;
left: 130px;
bottom: 122px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
}
#root .lt-namewrap {
position: relative;
overflow: hidden;
padding: 2px 0;
}
#root .lt-name {
font-size: 72px;
font-weight: 900;
color: #ffffff;
line-height: 0.98;
letter-spacing: -0.02em;
white-space: nowrap;
text-shadow: 0 2px 22px rgba(0, 0, 0, 0.45);
will-change: clip-path;
}
#root .lt-sweep {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 8px;
background: #ffd23f;
}
#root .lt-role {
font-size: 28px;
font-weight: 500;
color: #e7eaf0;
line-height: 1.2;
letter-spacing: 0.02em;
white-space: nowrap;
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.45);
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="lt-mask-reveal"
data-width="1920"
data-height="1080"
data-composition-src="compositions/lt-mask-reveal.html"
data-start="0"
data-duration="4.8"
>
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
<div id="lt" class="lt">
<div class="lt-namewrap">
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
<div id="lt-sweep" class="lt-sweep"></div>
</div>
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var wrap = document.getElementById("lt");
var nameEl = document.getElementById("lt-name");
var sweep = document.getElementById("lt-sweep");
var role = document.getElementById("lt-role");
// An accent sweep crosses left→right; the name is revealed by a clip-path wipe behind it; role fades up.
// Sweep travel: left:"100%" was parent-relative (offsetParent = .lt-namewrap). Since the sweep is
// only 8px wide, xPercent would travel just 8px, so translate by the container width in pixels instead.
// The .lt-namewrap shrink-wraps the name "Dr. Maya Chen" to 536px (no horizontal padding/border, so
// clientWidth == content width), which is exactly what left:"100%" resolved to.
var SWEEP_TRAVEL = 536;
gsap.set(nameEl, { clipPath: "inset(0 100% 0 0)" });
gsap.set(sweep, { x: 0, opacity: 0 });
gsap.set(role, { y: 14, opacity: 0 });
tl.to(sweep, { opacity: 1, duration: 0.12, ease: "none" }, 0.1);
tl.to(sweep, { x: SWEEP_TRAVEL, duration: 0.55, ease: "power2.inOut" }, 0.12);
tl.to(nameEl, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "power2.inOut" }, 0.16);
tl.to(sweep, { opacity: 0, duration: 0.15, ease: "none" }, 0.6);
tl.to(role, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.55);
// Exit: name un-wipes, role fades, hard kill.
tl.to(role, { opacity: 0, duration: 0.3, ease: "power2.in" }, 4.25);
tl.to(nameEl, { clipPath: "inset(0 0 0 100%)", duration: 0.4, ease: "power2.in" }, 4.3);
tl.set(wrap, { visibility: "hidden" }, 4.75);
window.__timelines["lt-mask-reveal"] = tl;
</script>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="lt-mask-reveal" data-composition-src="compositions/lt-mask-reveal.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
-458
View File
@@ -1,458 +0,0 @@
---
title: "Neon Border"
description: "Two arcs of light travel the edge of a rounded panel and snap corner to corner, trailing a soft three-layer bloom. A lower third for a name and role."
---
import { InstallCommand } from "/snippets/install-command.jsx";
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
<VariablesExplorer
previewSrc="/public/catalog/preview/blocks/lt-neon-border.html"
compositionId="lt-neon-border"
compositionSrc="compositions/lt-neon-border.html"
variables={[{"id":"name","type":"string","label":"Name","default":"Dr. Maya Chen"},{"id":"role","type":"string","label":"Role","default":"Host · Neuroscientist"},{"id":"accent","type":"color","label":"Accent","default":"#cc9149"},{"id":"movement","type":"enum","label":"Movement","default":"step","options":[{"value":"step","label":"Step (snap at corners)"},{"value":"glide","label":"Glide (smooth)"}]},{"id":"speed","type":"number","label":"Speed","default":1.6,"min":0.2,"max":6,"step":0.1,"unit":"corners/s"},{"id":"cornerRadius","type":"number","label":"Corner radius","default":26,"min":0,"max":90,"step":1,"unit":"px"},{"id":"thickness","type":"number","label":"Thickness","default":5,"min":1,"max":16,"step":1,"unit":"px"},{"id":"arcLength","type":"number","label":"Arc length","default":22,"min":5,"max":100,"step":1,"unit":"%"},{"id":"glow","type":"number","label":"Glow","default":1,"min":0,"max":2,"step":0.05,"unit":"x"}]}
>
```html lt-neon-border.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
font-family: "Inter", sans-serif;
}
#root .nb-wrap {
position: absolute;
left: 50px;
bottom: 56px;
width: 1120px;
height: 510px;
}
#root .nb-svg {
position: absolute;
inset: 0;
display: block;
}
/* The frame's inner box, inset by the bloom padding so the copy lands
inside the traced rectangle. */
#root .nb-panel {
position: absolute;
left: 150px;
top: 150px;
width: 820px;
height: 210px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 14px;
padding: 0 52px;
}
#root .nb-name {
display: block;
font-size: 54px;
font-weight: 700;
color: #ffffff;
line-height: 1;
letter-spacing: -0.015em;
white-space: nowrap;
text-shadow: 0 2px 20px rgba(0, 0, 0, 0.75);
}
#root .nb-role {
display: block;
font-size: 25px;
font-weight: 400;
color: #d5dae3;
line-height: 1.2;
letter-spacing: 0.06em;
text-transform: uppercase;
white-space: nowrap;
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.75);
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="lt-neon-border"
data-composition-variables='[
{"id":"name","type":"string","label":"Name","default":"Dr. Maya Chen"},
{"id":"role","type":"string","label":"Role","default":"Host · Neuroscientist"},
{"id":"accent","type":"color","label":"Accent","default":"#cc9149"},
{"id":"movement","type":"enum","label":"Movement","default":"step","options":[{"value":"step","label":"Step (snap at corners)"},{"value":"glide","label":"Glide (smooth)"}]},
{"id":"speed","type":"number","label":"Speed","default":1.6,"min":0.2,"max":6,"step":0.1,"unit":"corners/s"},
{"id":"cornerRadius","type":"number","label":"Corner radius","default":26,"min":0,"max":90,"step":1,"unit":"px"},
{"id":"thickness","type":"number","label":"Thickness","default":5,"min":1,"max":16,"step":1,"unit":"px"},
{"id":"arcLength","type":"number","label":"Arc length","default":22,"min":5,"max":100,"step":1,"unit":"%"},
{"id":"glow","type":"number","label":"Glow","default":1,"min":0,"max":2,"step":0.05,"unit":"x"}
]'
data-start="0"
data-duration="4.8"
data-width="1920"
data-height="1080"
>
<div id="nb-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
<div id="nb-wrap" class="nb-wrap">
<svg
id="nb-svg"
class="nb-svg"
width="1120"
height="510"
viewBox="0 0 1120 510"
fill="none"
aria-hidden="true"
>
<defs>
<g id="nb-seg-a-glow"></g>
<g id="nb-seg-a-core"></g>
<g id="nb-seg-b-glow"></g>
<g id="nb-seg-b-core"></g>
</defs>
<g transform="translate(150,150)">
<!-- Wide bloom sits UNDER the panel so the halo reads outside the
frame and the interior stays hardware-clean. -->
<g id="nb-bloom-under"></g>
<path id="nb-fill" d="" fill="rgba(9,11,15,0.78)" />
<path id="nb-rail" d="" fill="none" stroke-linejoin="round" />
<g id="nb-bloom-over"></g>
</g>
</svg>
<div id="nb-panel" class="nb-panel">
<span id="nb-name" class="nb-name" data-var-text="name">Dr. Maya Chen</span>
<span id="nb-role" class="nb-role" data-var-text="role">Host · Neuroscientist</span>
</div>
</div>
</div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
var SVG_NS = "http://www.w3.org/2000/svg";
var W = 820; // traced rectangle width
var H = 210; // traced rectangle height
var DUR = 4.8;
var vars =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
function num(v, fallback) {
var n = typeof v === "number" ? v : parseFloat(v);
return isFinite(n) ? n : fallback;
}
var accent = /^#[0-9a-fA-F]{6}$/.test(String(vars.accent))
? String(vars.accent)
: "#cc9149";
var mode = vars.movement === "glide" ? "glide" : "step";
var speed = Math.max(0.05, num(vars.speed, 1.6)); // corners per second
var radius = Math.min(Math.min(W, H) / 2, Math.max(0, num(vars.cornerRadius, 26)));
var thickness = Math.max(0.5, num(vars.thickness, 5));
var arcFrac = Math.min(1, Math.max(0.02, num(vars.arcLength, 22) / 100));
var glow = Math.max(0, num(vars.glow, 1));
// Hot core: the accent lifted most of the way to white, the way a real
// filament reads brighter than its halo.
function towardWhite(hex, amount) {
var v = parseInt(hex.slice(1), 16);
var out = "#";
for (var s = 16; s >= 0; s -= 8) {
var c = (v >> s) & 255;
var m = Math.round(c + (255 - c) * amount);
out += (m < 16 ? "0" : "") + m.toString(16);
}
return out;
}
var hot = towardWhite(accent, 0.78);
// ---- geometry -------------------------------------------------------
// Rounded rect, clockwise, starting where the top edge leaves the
// top-left corner arc.
var r = radius;
var arc = "A " + r + " " + r + " 0 0 1 ";
var d = [
"M " + r + " 0",
"H " + (W - r),
arc + W + " " + r,
"V " + (H - r),
arc + (W - r) + " " + H,
"H " + r,
arc + "0 " + (H - r),
"V " + r,
arc + r + " 0",
"Z",
].join(" ");
var fillEl = document.getElementById("nb-fill");
var railEl = document.getElementById("nb-rail");
fillEl.setAttribute("d", d);
railEl.setAttribute("d", d);
railEl.setAttribute("stroke", accent);
railEl.setAttribute("stroke-width", String(thickness));
railEl.setAttribute("stroke-opacity", "0.14");
// Analytic perimeter, then rescaled to whatever length the renderer
// actually measures for the path so dash distances line up exactly.
var SW = W - 2 * r; // straight run, horizontal
var SH = H - 2 * r; // straight run, vertical
var AC = (Math.PI * r) / 2; // one corner arc
var P_ANALYTIC = 2 * SW + 2 * SH + 4 * AC;
var P = railEl.getTotalLength() || P_ANALYTIC;
var K = P / P_ANALYTIC;
// Distance along the path of the MIDPOINT of each corner arc, so the
// light parks centred on the corner rather than skidding past it.
var CORNERS = [
(SW + AC / 2) * K, // top-right
(SW + SH + 1.5 * AC) * K, // bottom-right
(2 * SW + SH + 2.5 * AC) * K, // bottom-left
(2 * SW + 2 * SH + 3.5 * AC) * K, // top-left
];
/** Absolute path distance of corner k, for any integer k (k may be
* negative or beyond one lap). Pure function of k — no accumulator. */
function cornerPos(k) {
var lap = Math.floor(k / 4);
var i = k - lap * 4;
return CORNERS[i] + lap * P;
}
// ---- easing ---------------------------------------------------------
// The step curve overshoots (y2 = 1.05); that overshoot IS the snap.
function cubicBezier(x1, y1, x2, y2) {
function axis(t, a, b) {
var u = 1 - t;
return 3 * u * u * t * a + 3 * u * t * t * b + t * t * t;
}
return function (x) {
if (x <= 0) return 0;
if (x >= 1) return 1;
var lo = 0;
var hi = 1;
var t = x;
// Fixed iteration count: same input, same output, every run.
for (var i = 0; i < 32; i++) {
t = (lo + hi) / 2;
if (axis(t, x1, x2) < x) lo = t;
else hi = t;
}
return axis(t, y1, y2);
};
}
var ease =
mode === "glide" ? cubicBezier(0.65, 0, 0.35, 1) : cubicBezier(0.72, 0.16, 0.18, 1.05);
var beat = 1 / speed; // seconds per corner hop
/** Head position (path distance) at time t. floor/frac of t/beat is the
* closed form of the reference's `stepT += dt` accumulator. */
function headAt(t) {
var total = t / beat;
var k = Math.floor(total);
var u = total - k;
var a = cornerPos(k);
var b = cornerPos(k + 1);
return a + (b - a) * ease(u);
}
// ---- arc construction ----------------------------------------------
// Each arc is a run of abutting dash segments whose opacity falls off
// behind the head; that ramp is the taper. Geometry lives once in
// <defs> and is shared by every bloom layer through <use>.
function buildSegments(groupId, count, falloff) {
var g = document.getElementById(groupId);
var segs = [];
for (var i = 0; i < count; i++) {
var p = document.createElementNS(SVG_NS, "path");
p.setAttribute("d", d);
p.setAttribute("fill", "none");
p.setAttribute("stroke-linecap", i === 0 ? "round" : "butt");
p.setAttribute("opacity", String(Math.pow(1 - i / count, falloff)));
g.appendChild(p);
segs.push(p);
}
return segs;
}
var GLOW_SEGS = 20;
var CORE_SEGS = 26;
var glowLen = arcFrac * P;
var coreLen = 0.34 * glowLen;
var arcs = [
{
offset: 0,
glow: buildSegments("nb-seg-a-glow", GLOW_SEGS, 2.6),
core: buildSegments("nb-seg-a-core", CORE_SEGS, 1.4),
},
{
// Half a lap behind: the diametrically opposite corner. The pairing
// is most of why the effect reads as designed rather than random.
offset: 2,
glow: buildSegments("nb-seg-b-glow", GLOW_SEGS, 2.6),
core: buildSegments("nb-seg-b-core", CORE_SEGS, 1.4),
},
];
// Three stacked bloom layers (soft / medium / wide) plus the filament.
// `spread` widens the stroke before it is blurred — that pairing, not
// the blur alone, is what makes the halo read as emitted light.
var MAX_REACH = 36;
var LAYERS = [
{ blur: 30, spread: 1, opacity: 0.18, stroke: accent, deep: true },
{ blur: 15, spread: 0.6, opacity: 0.3, stroke: accent, deep: true },
{ blur: 8, spread: 0.3, opacity: 0.5, stroke: accent, deep: true },
{ blur: 4, spread: 0.05, opacity: 0.9, stroke: hot, core: true },
{ blur: 0, spread: -0.012, opacity: 1, stroke: "#ffffff", core: true },
];
var under = document.getElementById("nb-bloom-under");
var over = document.getElementById("nb-bloom-over");
["a", "b"].forEach(function (tag) {
LAYERS.forEach(function (layer) {
var u = document.createElementNS(SVG_NS, "use");
u.setAttribute("href", "#nb-seg-" + tag + "-" + (layer.core ? "core" : "glow"));
u.setAttribute("stroke", layer.stroke);
u.setAttribute(
"stroke-width",
String(Math.max(0.5, thickness + 2 * layer.spread * MAX_REACH * glow)),
);
u.setAttribute("opacity", String(Math.min(1, layer.opacity * glow)));
if (layer.blur > 0) u.style.filter = "blur(" + layer.blur + "px)";
(layer.deep ? under : over).appendChild(u);
});
});
/** Lay a segment run out behind `head`. Every value derives from head. */
function layout(segs, head, length) {
var step = length / segs.length;
for (var i = 0; i < segs.length; i++) {
var start = head - (i + 1) * step;
start = ((start % P) + P) % P;
segs[i].setAttribute("stroke-dasharray", step + " " + (P - step));
segs[i].setAttribute("stroke-dashoffset", String(-start));
}
}
function render(t) {
for (var i = 0; i < arcs.length; i++) {
var head = headAt(t + arcs[i].offset * beat);
layout(arcs[i].glow, head, glowLen);
layout(arcs[i].core, head, coreLen);
}
}
// ---- timeline -------------------------------------------------------
var wrap = document.getElementById("nb-wrap");
var panel = document.getElementById("nb-panel");
var nameEl = document.getElementById("nb-name");
var roleEl = document.getElementById("nb-role");
var tl = gsap.timeline({ paused: true });
gsap.set(wrap, { opacity: 0, scale: 0.965, transformOrigin: "0% 100%" });
gsap.set(nameEl, { y: 18, opacity: 0 });
gsap.set(roleEl, { y: 12, opacity: 0 });
// The driver: linear, spans the whole clip, and every arc position is
// recomputed from its own time value. Seeking to frame N gives the same
// state as playing to frame N because nothing carries over.
var driver = { t: 0 };
tl.to(
driver,
{
t: DUR,
duration: DUR,
ease: "none",
onUpdate: function () {
render(driver.t);
},
},
0,
);
tl.to(wrap, { opacity: 1, scale: 1, duration: 0.55, ease: "power3.out" }, 0.08);
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.3);
tl.to(roleEl, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.44);
tl.to(roleEl, { y: -10, opacity: 0, duration: 0.3, ease: "power2.in" }, 4.24);
tl.to(nameEl, { y: -14, opacity: 0, duration: 0.32, ease: "power2.in" }, 4.3);
tl.to(wrap, { opacity: 0, scale: 0.985, duration: 0.36, ease: "power2.in" }, 4.36);
tl.set(panel, { visibility: "hidden" }, 4.76);
render(0);
window.__timelines["lt-neon-border"] = tl;
})();
</script>
</body>
</html>
```
</VariablesExplorer>
## Install
<InstallCommand command="npx hyperframes add lt-neon-border" />
That writes one file: `compositions/lt-neon-border.html`.
## Add it to your video
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="lt-neon-border"
data-composition-src="compositions/lt-neon-border.html"
data-start="0"
data-duration="4.8"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Change how it looks
Set these CSS variables on the block:
- `undefined` — Name. Defaults to `Dr. Maya Chen`.
- `undefined` — Role. Defaults to `Host · Neuroscientist`.
- `undefined` — Accent. Defaults to `#cc9149`.
- `undefined` — Movement. Defaults to `step`. Options: `step`, `glide`.
- `undefined` — Speed. Defaults to `1.6`.
- `undefined` — Corner radius. Defaults to `26`.
- `undefined` — Thickness. Defaults to `5`.
- `undefined` — Arc length. Defaults to `22`.
- `undefined` — Glow. Defaults to `1`.
{/* hf:generated-footer */}
Tagged `lower-third` `glow` `loop` `video-primitive`.
## Related topics
- [Browse the complete Catalog](/catalog)
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
- [Build a richer composition](/go-further)
+14 -125
View File
@@ -3,145 +3,34 @@ title: "Lower Third — Side Rule"
description: "Cardless lower third with a vertical accent bar; condensed display name + mono role, text-shadowed for footage"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/lt-side-rule.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Side Rule preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-side-rule.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-side-rule.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add lt-side-rule" />
```bash Terminal
npx hyperframes add lt-side-rule
```
That writes one file: `compositions/lt-side-rule.html`.
## Source
## Add it to your video
<Accordion title={`lt-side-rule.html`}>
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
}
#root .lt {
position: absolute;
left: 130px;
bottom: 120px;
display: flex;
align-items: stretch;
gap: 26px;
}
#root .lt-bar {
width: 8px;
background: #46e5b7;
border-radius: 4px;
transform-origin: 50% 0%;
flex-shrink: 0;
}
#root .lt-text {
display: flex;
flex-direction: column;
justify-content: center;
gap: 12px;
}
#root .lt-name {
font-family: "League Gothic", sans-serif;
font-size: 92px;
font-weight: 400;
color: #ffffff;
line-height: 0.86;
letter-spacing: 0.01em;
text-transform: uppercase;
white-space: nowrap;
text-shadow: 0 2px 22px rgba(0, 0, 0, 0.5);
}
#root .lt-role {
font-family: "JetBrains Mono", monospace;
font-size: 26px;
font-weight: 400;
color: #e7eaf0;
line-height: 1.2;
letter-spacing: 0.04em;
white-space: nowrap;
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.5);
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="lt-side-rule"
data-width="1920"
data-height="1080"
data-composition-src="compositions/lt-side-rule.html"
data-start="0"
data-duration="4.8"
>
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
<div id="lt" class="lt">
<div id="lt-bar" class="lt-bar"></div>
<div class="lt-text">
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var wrap = document.getElementById("lt");
var bar = document.getElementById("lt-bar");
var nameEl = document.getElementById("lt-name");
var role = document.getElementById("lt-role");
// Vertical accent bar draws top→down, name and role slide in from the left.
gsap.set(bar, { scaleY: 0 });
gsap.set(nameEl, { x: -24, opacity: 0 });
gsap.set(role, { x: -24, opacity: 0 });
tl.to(bar, { scaleY: 1, duration: 0.5, ease: "power3.out" }, 0.1);
tl.to(nameEl, { x: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.26);
tl.to(role, { x: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.38);
// Exit: bar retracts, text lifts, hard kill.
tl.to(role, { opacity: 0, duration: 0.3, ease: "power2.in" }, 4.25);
tl.to(nameEl, { y: -14, opacity: 0, duration: 0.32, ease: "power2.in" }, 4.3);
tl.to(bar, { scaleY: 0, duration: 0.32, ease: "power2.in" }, 4.32);
tl.set(wrap, { visibility: "hidden" }, 4.75);
window.__timelines["lt-side-rule"] = tl;
</script>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="lt-side-rule" data-composition-src="compositions/lt-side-rule.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+14 -130
View File
@@ -3,150 +3,34 @@ title: "Lower Third — Soft Pill"
description: "Rounded white pill lower third for podcasts/interviews: status dot, name + role, scale-pop entrance"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/lt-soft-pill.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Soft Pill preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-soft-pill.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-soft-pill.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add lt-soft-pill" />
```bash Terminal
npx hyperframes add lt-soft-pill
```
That writes one file: `compositions/lt-soft-pill.html`.
## Source
## Add it to your video
<Accordion title={`lt-soft-pill.html`}>
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
font-family: "Montserrat", sans-serif;
}
#root .lt {
position: absolute;
left: 120px;
bottom: 120px;
}
#root .lt-pill {
display: flex;
align-items: center;
gap: 22px;
background: #ffffff;
border-radius: 999px;
padding: 20px 40px 20px 26px;
box-shadow: 0 16px 46px rgba(15, 17, 21, 0.2);
transform-origin: 0% 100%;
will-change: transform;
}
#root .lt-dot {
width: 18px;
height: 18px;
border-radius: 50%;
background: #ff5a36;
flex-shrink: 0;
}
#root .lt-text {
display: flex;
flex-direction: column;
gap: 4px;
}
#root .lt-name {
font-size: 46px;
font-weight: 700;
color: #0f1115;
line-height: 1.04;
letter-spacing: -0.015em;
white-space: nowrap;
}
#root .lt-role {
font-size: 25px;
font-weight: 400;
color: #5a6170;
line-height: 1.2;
letter-spacing: 0.01em;
white-space: nowrap;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="lt-soft-pill"
data-width="1920"
data-height="1080"
data-composition-src="compositions/lt-soft-pill.html"
data-start="0"
data-duration="4.8"
>
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
<div id="lt" class="lt">
<div id="lt-pill" class="lt-pill">
<div id="lt-dot" class="lt-dot"></div>
<div class="lt-text">
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
</div>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var wrap = document.getElementById("lt");
var pill = document.getElementById("lt-pill");
var dot = document.getElementById("lt-dot");
var nameEl = document.getElementById("lt-name");
var role = document.getElementById("lt-role");
// Pill scales up from the lower-left anchor, dot pulses in, text settles.
gsap.set(pill, { scale: 0.88, y: 18, opacity: 0 });
gsap.set(dot, { scale: 0 });
gsap.set([nameEl, role], { opacity: 0, x: -10 });
tl.to(pill, { scale: 1, y: 0, opacity: 1, duration: 0.55, ease: "back.out(1.7)" }, 0.1);
tl.to(dot, { scale: 1, duration: 0.4, ease: "back.out(3)" }, 0.4);
tl.to(nameEl, { opacity: 1, x: 0, duration: 0.45, ease: "power3.out" }, 0.42);
tl.to(role, { opacity: 1, x: 0, duration: 0.45, ease: "power3.out" }, 0.5);
// Exit: settle down + hard kill.
tl.to(pill, { scale: 0.94, y: 14, opacity: 0, duration: 0.35, ease: "power2.in" }, 4.3);
tl.set(wrap, { visibility: "hidden" }, 4.75);
window.__timelines["lt-soft-pill"] = tl;
</script>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="lt-soft-pill" data-composition-src="compositions/lt-soft-pill.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+14 -120
View File
@@ -3,140 +3,34 @@ title: "Lower Third — Stack Bars"
description: "Two stacked bars: a dark name bar wipes from the left, an accent role bar wipes from the right"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/lt-stack-bars.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Stack Bars preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-stack-bars.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-stack-bars.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add lt-stack-bars" />
```bash Terminal
npx hyperframes add lt-stack-bars
```
That writes one file: `compositions/lt-stack-bars.html`.
## Source
## Add it to your video
<Accordion title={`lt-stack-bars.html`}>
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
}
#root .lt {
position: absolute;
left: 120px;
bottom: 116px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
#root .lt-namebar {
background: #141518;
padding: 14px 30px;
will-change: clip-path;
}
#root .lt-name {
font-family: "Archivo Black", sans-serif;
font-size: 52px;
font-weight: 400;
color: #ffffff;
line-height: 1;
letter-spacing: -0.015em;
text-transform: uppercase;
white-space: nowrap;
}
#root .lt-rolebar {
background: #ffd23f;
padding: 9px 22px;
margin-left: 14px;
will-change: clip-path;
}
#root .lt-role {
font-family: "Space Mono", monospace;
font-size: 23px;
font-weight: 700;
color: #141518;
line-height: 1;
letter-spacing: 0.06em;
text-transform: uppercase;
white-space: nowrap;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="lt-stack-bars"
data-width="1920"
data-height="1080"
data-composition-src="compositions/lt-stack-bars.html"
data-start="0"
data-duration="4.8"
>
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
<div id="lt" class="lt">
<div id="lt-namebar" class="lt-namebar">
<div id="lt-name" class="lt-name">Maya Chen</div>
</div>
<div id="lt-rolebar" class="lt-rolebar">
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var wrap = document.getElementById("lt");
var namebar = document.getElementById("lt-namebar");
var rolebar = document.getElementById("lt-rolebar");
// Name bar wipes in from the left; role bar wipes in from the right (opposing).
gsap.set(namebar, { clipPath: "inset(0 100% 0 0)" });
gsap.set(rolebar, { clipPath: "inset(0 0 0 100%)" });
tl.to(namebar, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "power4.out" }, 0.1);
tl.to(rolebar, { clipPath: "inset(0 0% 0 0%)", duration: 0.5, ease: "power4.out" }, 0.34);
// Exit: both wipe back out, hard kill.
tl.to(rolebar, { clipPath: "inset(0 0 0 100%)", duration: 0.32, ease: "power3.in" }, 4.25);
tl.to(namebar, { clipPath: "inset(0 100% 0 0)", duration: 0.36, ease: "power3.in" }, 4.32);
tl.set(wrap, { visibility: "hidden" }, 4.75);
window.__timelines["lt-stack-bars"] = tl;
</script>
</body>
</html>
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="lt-stack-bars" data-composition-src="compositions/lt-stack-bars.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+14 -213
View File
@@ -3,233 +3,34 @@ title: "macOS Notification"
description: "Animated macOS-style notification banner with app icon and message"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/macos-notification.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="macOS Notification preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/macos-notification.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/macos-notification.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add macos-notification" />
```bash Terminal
npx hyperframes add macos-notification
```
That writes one file: `compositions/macos-notification.html`.
## Source
## Add it to your video
<Accordion title={`macos-notification.html`}>
It runs for 5 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=block"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
font-family: "Inter", sans-serif;
}
[data-composition-id="macos-notification"] .notification-card {
position: absolute;
top: 24px;
right: 24px;
width: 380px;
background: rgba(30, 30, 30, 0.82);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
border-radius: 16px;
padding: 16px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
inset 0 0 0 0.5px rgba(255, 255, 255, 0.1);
display: flex;
gap: 12px;
align-items: flex-start;
}
[data-composition-id="macos-notification"] .app-icon {
width: 36px;
height: 36px;
border-radius: 8px;
flex-shrink: 0;
overflow: hidden;
}
[data-composition-id="macos-notification"] .app-icon svg {
width: 36px;
height: 36px;
display: block;
}
[data-composition-id="macos-notification"] .notification-content {
flex: 1;
min-width: 0;
}
[data-composition-id="macos-notification"] .notification-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2px;
}
[data-composition-id="macos-notification"] .app-name {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
letter-spacing: 0.02em;
}
[data-composition-id="macos-notification"] .time-label {
font-size: 13px;
font-weight: 400;
color: rgba(255, 255, 255, 0.35);
}
[data-composition-id="macos-notification"] .notification-title {
font-size: 15px;
font-weight: 600;
color: #ffffff;
line-height: 1.3;
margin-bottom: 2px;
}
[data-composition-id="macos-notification"] .notification-body {
font-size: 13px;
font-weight: 400;
color: rgba(255, 255, 255, 0.6);
line-height: 1.35;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="macos-notification"
data-composition-src="compositions/macos-notification.html"
data-start="0"
data-duration="5"
data-track-index="1"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="5"
>
<div
id="notification"
class="clip notification-card"
data-start="0"
data-duration="5"
data-track-index="0"
>
<div class="app-icon">
<svg viewBox="0 0 36 36" fill="none">
<defs>
<linearGradient
id="icon-grad"
x1="0"
y1="0"
x2="36"
y2="36"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stop-color="#6366f1" />
<stop offset="100%" stop-color="#8b5cf6" />
</linearGradient>
</defs>
<rect width="36" height="36" rx="8" fill="url(#icon-grad)" />
<text
x="18"
y="23"
text-anchor="middle"
font-family="Inter, sans-serif"
font-size="14"
font-weight="700"
fill="#fff"
>
HF
</text>
</svg>
</div>
<div class="notification-content">
<div class="notification-header">
<span class="app-name">HyperFrames</span>
<span class="time-label">now</span>
</div>
<div class="notification-title">Prompt to replace this title</div>
<div class="notification-body">
Prompt to change this body text, the app icon, and the app name to match your content.
</div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var notification = document.getElementById("notification");
// Start off-screen to the right
gsap.set(notification, { x: 420, opacity: 0 });
// Slide in from right
tl.to(
notification,
{
x: 0,
opacity: 1,
duration: 0.5,
ease: "power3.out",
},
0.2,
);
// Slide out to right
tl.to(
notification,
{
x: 420,
opacity: 0,
duration: 0.3,
ease: "power3.in",
},
4.2,
);
window.__timelines["macos-notification"] = tl;
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="macos-notification" data-composition-src="compositions/macos-notification.html" data-start="0" data-duration="5" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
File diff suppressed because it is too large Load Diff
+4 -248
View File
@@ -3,13 +3,13 @@ title: "Minimal Gradient Background"
description: "Procedural soft-blob gradient backdrop in a minimalist presentation style: two radial blobs drifting over a base color, with an animatable rounded-card bar mask and optional frosted-glass mode"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/mk-background.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Minimal Gradient Background preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-background.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-background.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add mk-background" />
```bash Terminal
npx hyperframes add mk-background
```
That writes one file: `compositions/mk-background.html`.
@@ -32,250 +32,6 @@ It runs for 10 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`mk-background.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#mk-bg-root {
/* ---- mk tokens: override these in the host to re-skin the family ---- */
--mk-font: "Inter", -apple-system, sans-serif;
--mk-stage: #f5f5f7;
--mk-base: #ffffff;
--mk-blob-1: #ff7ac8;
--mk-blob-2: #45d6c8;
--mk-stage-dark: #000000;
--mk-base-dark: #0a0a0c;
--mk-blob-1-dark: #7d5cff;
--mk-blob-2-dark: #2d1b69;
--mk-radius-card: 40px;
--mk-shadow: 0 30px 80px rgba(0, 0, 0, 0.18);
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: var(--mk-stage);
}
#mk-bg-root.mk-dark {
background: var(--mk-stage-dark);
}
/* The "Bar" — an animatable rounded-rect mask. Full-bleed by default,
animatable down to a floating card over the stage. */
#mk-bg-card {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
border-radius: 0;
background: var(--mk-base);
box-shadow: var(--mk-shadow);
}
.mk-dark #mk-bg-card {
background: var(--mk-base-dark);
}
/* Soft radial blobs — the procedural "mesh gradient". Two oversized
radial gradients with a long falloff, drifting slowly. */
.mk-bg-blob {
position: absolute;
border-radius: 50%;
will-change: transform;
}
#mk-bg-blob1 {
background: radial-gradient(
circle closest-side,
var(--mk-blob-1) 0%,
rgba(255, 255, 255, 0) 72%
);
}
#mk-bg-blob2 {
background: radial-gradient(
circle closest-side,
var(--mk-blob-2) 0%,
rgba(255, 255, 255, 0) 72%
);
}
.mk-dark #mk-bg-blob1 {
background: radial-gradient(
circle closest-side,
var(--mk-blob-1-dark) 0%,
rgba(0, 0, 0, 0) 72%
);
}
.mk-dark #mk-bg-blob2 {
background: radial-gradient(
circle closest-side,
var(--mk-blob-2-dark) 0%,
rgba(0, 0, 0, 0) 72%
);
}
</style>
</head>
<body>
<div
id="mk-bg-root"
data-composition-id="mk-background"
data-start="0"
data-duration="10"
data-width="1920"
data-height="1080"
>
<div id="mk-bg-card">
<div id="mk-bg-blob1" class="mk-bg-blob" data-layout-allow-overflow></div>
<div id="mk-bg-blob2" class="mk-bg-blob" data-layout-allow-overflow></div>
</div>
<div
id="mk-bg-drv"
class="clip"
data-start="0"
data-duration="10"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters (inspector-style CONFIG) ---- */
var CONFIG = {
scheme: "light", // "light" | "dark"
animationIn: true, // fade/settle entrance
animationOut: true, // fade exit
drift: true, // ambient blob motion
// Soft BG blobs: center as fraction of canvas, radius in px
blob1: { x: 0.28, y: 0.32, r: 950, opacity: 0.85 },
blob2: { x: 0.74, y: 0.66, r: 1100, opacity: 0.8 },
// Bar mask (rounded-rect card). demo:true plays full-bleed -> card -> full-bleed
bar: {
demo: true, // full-bleed -> rounded card -> full-bleed showcase
x: 240,
y: 140,
width: 1440,
height: 800,
roundness: 40,
},
// Content opacity + frost only matter with footage beneath the block
contentOpacity: 1,
frostBlur: 0, // px; e.g. 24 for the frosted-card look over footage
};
var W = 1920,
H = 1080,
DUR = 10;
var root = document.getElementById("mk-bg-root");
var card = document.getElementById("mk-bg-card");
var blob1 = document.getElementById("mk-bg-blob1");
var blob2 = document.getElementById("mk-bg-blob2");
if (CONFIG.scheme === "dark") root.classList.add("mk-dark");
function placeBlob(el, b) {
var d = b.r * 2;
el.style.width = d + "px";
el.style.height = d + "px";
el.style.left = b.x * W - b.r + "px";
el.style.top = b.y * H - b.r + "px";
el.style.opacity = String(b.opacity);
}
placeBlob(blob1, CONFIG.blob1);
placeBlob(blob2, CONFIG.blob2);
card.style.opacity = String(CONFIG.contentOpacity);
if (CONFIG.frostBlur > 0) {
card.style.backdropFilter = "blur(" + CONFIG.frostBlur + "px) saturate(1.4)";
}
var tl = gsap.timeline({ paused: true });
/* entrance — fade + blobs settle (power3, no bounce) */
if (CONFIG.animationIn) {
tl.from(root, { opacity: 0, duration: 0.7, ease: "power2.out" }, 0);
tl.from([blob1, blob2], { scale: 1.14, duration: 1.2, ease: "power3.out" }, 0);
}
/* ambient drift — slow sine yoyo, seek-safe timeline tweens */
if (CONFIG.drift) {
tl.to(
blob1,
{ x: 70, y: 42, duration: 2.35, ease: "sine.inOut", yoyo: true, repeat: 3 },
0.3,
);
tl.to(
blob2,
{ x: -84, y: -36, duration: 2.35, ease: "sine.inOut", yoyo: true, repeat: 3 },
0.3,
);
}
/* bar-mask demo: full-bleed -> rounded card over the stage -> full-bleed.
Offset via x/y transforms, not top/left: layout properties snap to
integer device pixels and stutter under seek-by-frame capture. The
card's CSS base is top:0/left:0, so the transform values match 1:1. */
if (CONFIG.bar.demo) {
tl.to(
card,
{
y: CONFIG.bar.y,
x: CONFIG.bar.x,
width: CONFIG.bar.width,
height: CONFIG.bar.height,
borderRadius: CONFIG.bar.roundness,
duration: 1.1,
ease: "power3.inOut",
},
3.8,
);
tl.to(
card,
{
y: 0,
x: 0,
width: W,
height: H,
borderRadius: 0,
duration: 1.0,
ease: "power3.inOut",
},
7.6,
);
}
/* exit + hard kill */
if (CONFIG.animationOut) {
tl.to(root, { opacity: 0, duration: 0.4, ease: "power2.in" }, DUR - 0.45);
}
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
window.__timelines["mk-background"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `background` `gradient` `minimal` `professional` `presentation`.
+4 -169
View File
@@ -3,13 +3,13 @@ title: "Word-Sweep Highlight"
description: "A sentence whose word-by-word emphasis is driven by a single scalar — tween it to sync the highlight with a voiceover"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/mk-callout-highlight.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Word-Sweep Highlight preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-callout-highlight.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-callout-highlight.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add mk-callout-highlight" />
```bash Terminal
npx hyperframes add mk-callout-highlight
```
That writes one file: `compositions/mk-callout-highlight.html`.
@@ -32,171 +32,6 @@ It runs for 8 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`mk-callout-highlight.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#mk-ch-root {
/* ---- mk tokens: override in the host to re-skin the family ---- */
--mk-font: "Inter", -apple-system, sans-serif;
--mk-ink: #1d1d1f;
/* faint = transient pre-sweep tier (tweens up to full ink) — distinct from the family --mk-ink-dim */
--mk-ink-faint: #bcbcc2;
--mk-ink-dark: #f5f5f7;
--mk-ink-faint-dark: rgba(245, 245, 247, 0.32);
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent; /* overlays footage or mk-background */
}
#mk-ch-box {
position: absolute;
font-family: var(--mk-font);
font-weight: 500;
letter-spacing: -0.015em;
}
.mk-ch-word {
display: inline;
}
</style>
</head>
<body>
<div
id="mk-ch-root"
data-composition-id="mk-callout-highlight"
data-start="0"
data-duration="8"
data-width="1920"
data-height="1080"
>
<div id="mk-ch-box" data-layout-allow-occlusion></div>
<div
id="mk-ch-drv"
class="clip"
data-start="0"
data-duration="8"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters (inspector-style CONFIG) ----
The mechanic: one scalar (highlight progress 0..1) sweeps word-by-word
emphasis across the sentence — a "highlight amount"
slider. Tween the scalar over the speaker's sentence to follow speech. */
var CONFIG = {
scheme: "light", // "light" (ink on light bg) | "dark" (white on footage)
text: "Functionality was our goal from the very beginning",
fontSize: 60,
lineHeight: 1.3,
maxWidth: 700, // px; sentence wraps inside this
x: 1030, // box left edge; null = centered horizontally
y: null, // box top edge; null = centered vertically
align: "left",
sweep: { start: 1.2, end: 6.2 }, // block-local seconds, linear
animationIn: true, // fade/rise entrance before the sweep
animationOut: true,
};
var DUR = 8;
var root = document.getElementById("mk-ch-root");
var box = document.getElementById("mk-ch-box");
var FULL = CONFIG.scheme === "dark" ? "#f5f5f7" : "#1d1d1f";
var FAINT = CONFIG.scheme === "dark" ? "rgba(245,245,247,0.32)" : "#bcbcc2";
box.style.fontSize = CONFIG.fontSize + "px";
box.style.lineHeight = String(CONFIG.lineHeight);
box.style.maxWidth = CONFIG.maxWidth + "px";
box.style.textAlign = CONFIG.align;
/* build word spans */
var wordEls = [];
CONFIG.text.split(/\s+/).forEach(function (w, i) {
if (i > 0) box.appendChild(document.createTextNode(" "));
var span = document.createElement("span");
span.id = "mk-ch-w-" + i;
span.className = "mk-ch-word";
span.textContent = w;
span.style.color = FAINT;
box.appendChild(span);
wordEls.push(span);
});
/* position (measure after building) */
var rect = box.getBoundingClientRect();
box.style.left =
(CONFIG.x === null ? Math.round((1920 - rect.width) / 2) : CONFIG.x) + "px";
box.style.top =
(CONFIG.y === null ? Math.round((1080 - rect.height) / 2) : CONFIG.y) + "px";
/* state proxy: GSAP animates st.p, render derives every word color from it */
var st = { p: 0 };
var mix = gsap.utils.interpolate(FAINT, FULL);
function render() {
var k = st.p * wordEls.length;
for (var i = 0; i < wordEls.length; i++) {
var f = Math.max(0, Math.min(1, k - i));
wordEls[i].style.color = mix(f);
}
}
var tl = gsap.timeline({ paused: true });
if (CONFIG.animationIn) {
gsap.set(box, { opacity: 0, y: 24 });
tl.to(box, { opacity: 1, y: 0, duration: 0.7, ease: "power3.out" }, 0.3);
}
/* the highlight sweep — one scalar, linear so it tracks speech evenly.
To follow a real voiceover, replace with several tweens between
per-phrase progress values (keyframes on the slider). */
tl.to(
st,
{ p: 1, duration: CONFIG.sweep.end - CONFIG.sweep.start, ease: "none" },
CONFIG.sweep.start,
);
if (CONFIG.animationOut) {
tl.to(box, { opacity: 0, y: -16, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
}
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
tl.eventCallback("onUpdate", render);
render();
window.__timelines["mk-callout-highlight"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `typography` `karaoke` `captions` `minimal`.
@@ -3,13 +3,13 @@ title: "Text Clone Wall Transition"
description: "Overlay transition: a tiled word wall covers the frame, inverts via difference blending, and clears — hard-cut the content beneath while covered"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/mk-clone-wall-transition.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Text Clone Wall Transition preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-clone-wall-transition.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-clone-wall-transition.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add mk-clone-wall-transition" />
```bash Terminal
npx hyperframes add mk-clone-wall-transition
```
That writes one file: `compositions/mk-clone-wall-transition.html`.
@@ -32,240 +32,6 @@ It runs for 2.4 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`mk-clone-wall-transition.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#mk-cw-root {
/* ---- mk tokens: override in the host to re-skin the family ---- */
--mk-font: "Inter", -apple-system, sans-serif;
--mk-ink: #1d1d1f;
--mk-paper: #ffffff;
--mk-blob-1: #ff7ac8;
--mk-blob-2: #45d6c8;
--mk-radius-card: 40px;
--mk-shadow: 0 30px 80px rgba(0, 0, 0, 0.22);
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent;
}
/* the wall: paper sheet + tiled word rows + inversion layer, exits as one */
#mk-cw-wall {
position: absolute;
inset: 0;
background: var(--mk-paper);
/* no overflow:hidden — the composition root clips; isolation keeps the
difference blend scoped to the wall's own tiles */
isolation: isolate;
}
.mk-cw-row {
position: absolute;
white-space: nowrap;
font-family: var(--mk-font);
font-weight: 600;
letter-spacing: -0.02em;
color: var(--mk-ink);
line-height: 1;
}
.mk-cw-tile {
display: inline-block;
}
/* inversion sweep — a paper-colored rect in difference blend:
a paper-colored rect XORs the wall, flipping paper<->ink as it arrives */
#mk-cw-invert {
position: absolute;
inset: 0;
background: var(--mk-paper);
mix-blend-mode: difference;
}
/* the outgoing frame shrinking to a rounded card mid-transition */
#mk-cw-card {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
border-radius: 0;
overflow: hidden;
background: linear-gradient(
120deg,
#fdfbfd 0%,
var(--mk-blob-1) 38%,
var(--mk-blob-2) 100%
);
box-shadow: var(--mk-shadow);
transform-origin: 50% 50%;
}
#mk-cw-card img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
</head>
<body>
<div
id="mk-cw-root"
data-composition-id="mk-clone-wall-transition"
data-start="0"
data-duration="2.4"
data-width="1920"
data-height="1080"
>
<div id="mk-cw-wall" data-layout-allow-overflow>
<div id="mk-cw-tiles" data-layout-allow-overflow></div>
<div id="mk-cw-invert" data-layout-allow-overflow></div>
</div>
<div id="mk-cw-card" data-layout-allow-overflow></div>
<div
id="mk-cw-drv"
class="clip"
data-start="0"
data-duration="2.4"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters (inspector-style CONFIG) ----
Overlay-transition model: place this block spanning the host's cut
point (e.g. start 0.9s before the cut). The wall fully covers the
frame from ~0.6s to ~1.9s local — hard-cut the content beneath
anywhere in that window. */
var CONFIG = {
// no scheme param — scheme-agnostic; re-skin via the --mk-* tokens
word: "HyperFrames", // the cloned word
fontSize: 240, // px — the tiled text size
spreadX: 90, // px gap between tiles (tile spread X)
spreadY: 60, // px gap between rows (Spread Y)
brickOffset: true, // alternate rows offset by half a tile
card: {
enabled: true, // outgoing-frame card. In real use set src to a
src: null, // still of the outgoing clip; null = family gradient
},
driftX: -46, // slow wall drift during the hold
};
var W = 1920,
H = 1080,
DUR = 2.4;
var root = document.getElementById("mk-cw-root");
var wall = document.getElementById("mk-cw-wall");
var tilesBox = document.getElementById("mk-cw-tiles");
var invert = document.getElementById("mk-cw-invert");
var card = document.getElementById("mk-cw-card");
/* build the tile wall — rows of repeated words, brick-offset */
var rowH = CONFIG.fontSize + CONFIG.spreadY;
var rows = Math.ceil(H / rowH) + 1;
/* rough tile width estimate to fill each row past both edges */
var tileW = CONFIG.word.length * CONFIG.fontSize * 0.58 + CONFIG.spreadX;
var perRow = Math.ceil(W / tileW) + 2;
for (var r = 0; r < rows; r++) {
var row = document.createElement("div");
row.id = "mk-cw-row-" + r;
row.className = "mk-cw-row";
/* rows intentionally run past the canvas and sit beneath the card */
row.setAttribute("data-layout-allow-overflow", "");
row.setAttribute("data-layout-allow-occlusion", "");
row.style.fontSize = CONFIG.fontSize + "px";
row.style.top = r * rowH - CONFIG.fontSize * 0.35 + "px";
var offset = CONFIG.brickOffset && r % 2 === 1 ? -Math.round(tileW / 2) : 0;
row.style.left = offset - tileW + "px";
for (var c = 0; c < perRow + 1; c++) {
var tile = document.createElement("span");
tile.className = "mk-cw-tile";
tile.textContent = CONFIG.word;
tile.style.marginRight = CONFIG.spreadX + "px";
row.appendChild(tile);
}
tilesBox.appendChild(row);
}
if (CONFIG.card.enabled && CONFIG.card.src) {
var img = document.createElement("img");
img.src = CONFIG.card.src;
card.appendChild(img);
}
if (!CONFIG.card.enabled) card.style.display = "none";
var tl = gsap.timeline({ paused: true });
/* The wall sits in place from t=0, fully hidden beneath the
full-bleed card — the card's shrink IS the wall's reveal. */
/* 0.20.95 outgoing frame shrinks to a rounded card on the wall.
x/y rather than left/top — layout properties snap to integer device
pixels under seek-by-frame capture. Base is top:0/left:0, so the
values carry over unchanged, and the later scale composes cleanly. */
if (CONFIG.card.enabled) {
tl.to(
card,
{
y: 330,
x: 640,
width: 640,
height: 420,
borderRadius: 40,
duration: 0.75,
ease: "power3.inOut",
},
0.2,
);
/* 1.351.75 card is put away */
tl.to(card, { scale: 0.6, opacity: 0, duration: 0.4, ease: "power2.in" }, 1.35);
}
/* slow drift keeps the wall alive during the hold */
tl.to(tilesBox, { x: CONFIG.driftX, duration: 1.8, ease: "sine.inOut" }, 0.3);
/* 0.851.5 inversion sweeps in and STAYS — wall flips ink<->paper */
gsap.set(invert, { x: -W });
tl.to(invert, { x: 0, duration: 0.65, ease: "power3.inOut" }, 0.85);
/* 1.92.35 the inverted wall fades off, revealing the cut beneath */
tl.to(wall, { opacity: 0, duration: 0.45, ease: "power2.inOut" }, 1.9);
/* hard kills — wall and card die the moment their fades complete */
if (CONFIG.card.enabled) tl.set(card, { visibility: "hidden" }, 1.76);
tl.set(root, { visibility: "hidden" }, 2.36);
window.__timelines["mk-clone-wall-transition"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `transition` `typography` `wipe`.
+4 -305
View File
@@ -3,13 +3,13 @@ title: "Minimal Line Graph"
description: "One or two SVG series draw on with dots popping and value labels riding the draw front; dim baseline axis, x-labels, color-dot legend"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/mk-line-graph.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Minimal Line Graph preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-line-graph.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-line-graph.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add mk-line-graph" />
```bash Terminal
npx hyperframes add mk-line-graph
```
That writes one file: `compositions/mk-line-graph.html`.
@@ -32,307 +32,6 @@ It runs for 7 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`mk-line-graph.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#mk-lg-root {
/* ---- mk tokens: override in the host to re-skin the family ---- */
--mk-font: "Inter", -apple-system, sans-serif;
--mk-ink: #1d1d1f;
--mk-ink-dim: #6e6e73;
--mk-accent: #0071e3;
--mk-paper: #ffffff;
--mk-blob-2: #45d6c8;
--mk-axis: rgba(29, 29, 31, 0.22);
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent; /* overlays footage or mk-background */
font-family: var(--mk-font);
}
#mk-lg-svg {
position: absolute;
inset: 0;
}
.mk-lg-line {
fill: none;
stroke-width: 5;
stroke-linecap: round;
stroke-linejoin: round;
}
.mk-lg-dot {
stroke-width: 4;
fill: var(--mk-paper);
}
.mk-lg-axis {
stroke: var(--mk-axis);
stroke-width: 2;
}
.mk-lg-value {
position: absolute;
font-weight: 600;
font-size: 30px;
letter-spacing: -0.01em;
color: var(--mk-ink);
font-variant-numeric: tabular-nums;
/* Horizontal centring only. The label's vertical offset is baked into
`top` instead of a CSS translate, because the entrance tween animates
`y` and GSAP rewrites this transform, dropping any vertical shift. */
transform: translate(-50%, 0);
line-height: 38px;
white-space: nowrap;
}
.mk-lg-xlabel {
position: absolute;
font-weight: 400;
font-size: 26px;
color: var(--mk-ink-dim);
transform: translateX(-50%);
white-space: nowrap;
}
.mk-lg-legend {
position: absolute;
display: flex;
gap: 40px;
}
.mk-lg-legend-item {
display: flex;
align-items: center;
gap: 12px;
font-weight: 500;
font-size: 30px;
color: var(--mk-ink-dim);
}
.mk-lg-legend-dot {
width: 14px;
height: 14px;
border-radius: 50%;
}
</style>
</head>
<body>
<div
id="mk-lg-root"
data-composition-id="mk-line-graph"
data-start="0"
data-duration="7"
data-width="1920"
data-height="1080"
>
<svg id="mk-lg-svg" viewBox="0 0 1920 1080"></svg>
<div id="mk-lg-labels"></div>
<div
id="mk-lg-drv"
class="clip"
data-start="0"
data-duration="7"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters (inspector-style CONFIG) ----
A minimal presentation line graph: 12 series draw on, dots pop, value labels
ride their points, axes dimmer than labels (clear hierarchy). */
var CONFIG = {
// no scheme param — scheme-agnostic; re-skin via the --mk-* tokens
series: [
{ name: "Renders", color: null /* accent */, values: [12, 26, 22, 38, 44, 58] },
{ name: "Projects", color: null /* blob-2 */, values: [8, 14, 18, 16, 28, 36] },
],
xLabels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
showValues: true,
area: { x: 160, y: 240, w: 980, h: 500 }, // plot rectangle
animationIn: true,
animationOut: true,
};
var DUR = 7;
var NS = "http://www.w3.org/2000/svg";
var root = document.getElementById("mk-lg-root");
var svg = document.getElementById("mk-lg-svg");
var labels = document.getElementById("mk-lg-labels");
var A = CONFIG.area;
var accent = getComputedStyle(root).getPropertyValue("--mk-accent").trim();
var blob2 = getComputedStyle(root).getPropertyValue("--mk-blob-2").trim();
if (CONFIG.series[1] && !CONFIG.series[1].color) CONFIG.series[1].color = blob2;
/* scale: 0..max(all values)*1.15 into the plot rect */
var max = 0;
CONFIG.series.forEach(function (s) {
s.values.forEach(function (v) {
if (v > max) max = v;
});
});
max *= 1.15;
var n = CONFIG.series[0].values.length;
function px(i) {
return A.x + (i / (n - 1)) * A.w;
}
function py(v) {
return A.y + A.h - (v / max) * A.h;
}
/* baseline axis */
var axis = document.createElementNS(NS, "line");
axis.setAttribute("class", "mk-lg-axis");
axis.setAttribute("x1", A.x - 20);
axis.setAttribute("y1", A.y + A.h);
axis.setAttribute("x2", A.x + A.w + 20);
axis.setAttribute("y2", A.y + A.h);
axis.id = "mk-lg-axis";
svg.appendChild(axis);
/* series paths, dots, value labels */
var paths = [],
dots = [],
vals = [];
CONFIG.series.forEach(function (s, si) {
var color = s.color || accent;
var d = s.values
.map(function (v, i) {
return (i === 0 ? "M" : "L") + px(i) + " " + py(v);
})
.join(" ");
var path = document.createElementNS(NS, "path");
path.setAttribute("class", "mk-lg-line");
path.setAttribute("d", d);
path.setAttribute("stroke", color);
path.id = "mk-lg-path-" + si;
svg.appendChild(path);
paths.push(path);
s.values.forEach(function (v, i) {
var dot = document.createElementNS(NS, "circle");
dot.setAttribute("class", "mk-lg-dot");
dot.setAttribute("cx", px(i));
dot.setAttribute("cy", py(v));
dot.setAttribute("r", 9);
dot.setAttribute("stroke", color);
dot.id = "mk-lg-dot-" + si + "-" + i;
svg.appendChild(dot);
dots.push({ el: dot, si: si, i: i });
if (CONFIG.showValues) {
var val = document.createElement("div");
/* Series after the first label downward. With every series
labelling upward the numbers collide wherever the lines run
close together. GAP clears the dot radius plus line stroke. */
var below = si > 0;
var GAP = 22;
val.className = "mk-lg-value";
val.id = "mk-lg-val-" + si + "-" + i;
val.textContent = v;
val.style.left = px(i) + "px";
val.style.top = py(v) + (below ? GAP : -(GAP + 38)) + "px";
labels.appendChild(val);
vals.push({ el: val, si: si, i: i });
}
});
});
/* x labels + legend */
CONFIG.xLabels.slice(0, n).forEach(function (t, i) {
var xl = document.createElement("div");
xl.className = "mk-lg-xlabel";
xl.id = "mk-lg-xl-" + i;
xl.textContent = t;
xl.style.left = px(i) + "px";
xl.style.top = A.y + A.h + 24 + "px";
labels.appendChild(xl);
});
var legend = document.createElement("div");
legend.className = "mk-lg-legend";
legend.id = "mk-lg-legend";
legend.style.left = A.x + "px";
legend.style.top = A.y + A.h + 90 + "px";
CONFIG.series.forEach(function (s) {
var item = document.createElement("div");
item.className = "mk-lg-legend-item";
var dot = document.createElement("span");
dot.className = "mk-lg-legend-dot";
dot.style.background = s.color || accent;
item.appendChild(dot);
item.appendChild(document.createTextNode(s.name));
legend.appendChild(item);
});
labels.appendChild(legend);
var tl = gsap.timeline({ paused: true });
var DRAW = 1.3;
/* entrance: axis + x labels settle first */
gsap.set([axis, legend], { opacity: 0 });
tl.to(axis, { opacity: 1, duration: 0.5, ease: "power2.out" }, 0.2);
CONFIG.xLabels.slice(0, n).forEach(function (t, i) {
var xl = document.getElementById("mk-lg-xl-" + i);
gsap.set(xl, { opacity: 0, y: 10 });
tl.to(xl, { opacity: 1, y: 0, duration: 0.4, ease: "power2.out" }, 0.25 + i * 0.05);
});
/* draw each series, dots + values riding the draw front */
CONFIG.series.forEach(function (s, si) {
var t0 = (CONFIG.animationIn ? 0.5 : 0) + si * 0.35;
var path = paths[si];
var len = path.getTotalLength();
gsap.set(path, { strokeDasharray: len, strokeDashoffset: len });
tl.to(path, { strokeDashoffset: 0, duration: DRAW, ease: "power2.inOut" }, t0);
s.values.forEach(function (v, i) {
var dot = document.getElementById("mk-lg-dot-" + si + "-" + i);
var td = t0 + (i / (n - 1)) * DRAW * 0.92;
gsap.set(dot, { scale: 0, transformOrigin: "50% 50%" });
tl.to(dot, { scale: 1, duration: 0.35, ease: "back.out(1.2)" }, td);
if (CONFIG.showValues) {
var val = document.getElementById("mk-lg-val-" + si + "-" + i);
gsap.set(val, { opacity: 0, y: 8 });
tl.to(val, { opacity: 1, y: 0, duration: 0.35, ease: "power2.out" }, td + 0.06);
}
});
});
tl.to(legend, { opacity: 1, duration: 0.5, ease: "power2.out" }, 2.3);
/* exit + hard kill */
if (CONFIG.animationOut) {
tl.to([svg, labels], { opacity: 0, y: -20, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
}
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
window.__timelines["mk-line-graph"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `data-viz` `chart` `graph` `minimal`.
+4 -246
View File
@@ -3,13 +3,13 @@ title: "Rounded Media Grid"
description: "N-up rounded-corner media grid (2-up, 3-up, 4-up, 3x2, hero plus two): per-cell inside-scale framing, staggered box-in entrance, slow settle"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/mk-placeholder-grid.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Rounded Media Grid preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-placeholder-grid.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-placeholder-grid.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add mk-placeholder-grid" />
```bash Terminal
npx hyperframes add mk-placeholder-grid
```
That writes one file: `compositions/mk-placeholder-grid.html`.
@@ -32,248 +32,6 @@ It runs for 8 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`mk-placeholder-grid.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#mk-pg-root {
/* ---- mk tokens: override in the host to re-skin the family ---- */
--mk-font: "Inter", -apple-system, sans-serif;
--mk-ink-dim: #6e6e73;
--mk-paper: #ffffff;
--mk-blob-1: #ff7ac8;
--mk-blob-2: #45d6c8;
--mk-accent-dark: #7d5cff;
--mk-blob-1-soft: #ffd3ec;
--mk-blob-2-soft: #d9fff8;
--mk-accent-dark-soft: #cabdff;
--mk-well-warm: #ffb86b;
--mk-well-warm-soft: #ffe3c2;
--mk-well-sky: #5cb8ff;
--mk-well-sky-soft: #cfe9ff;
--mk-well-rose: #ff8f8f;
--mk-well-rose-soft: #ffd6d6;
--mk-well-num: rgba(255, 255, 255, 0.9);
--mk-radius-card: 40px;
--mk-shadow: 0 24px 64px rgba(0, 0, 0, 0.16);
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent; /* pair with mk-background beneath for the gradient backdrop */
}
.mk-pg-cell {
position: absolute;
border-radius: var(--mk-radius-card);
overflow: hidden;
background: var(--mk-paper);
box-shadow: var(--mk-shadow);
}
.mk-pg-media {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* empty drop-zone well: soft gradient + index numeral */
.mk-pg-well {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.mk-pg-well-num {
font-family: var(--mk-font);
font-weight: 600;
font-size: 88px;
letter-spacing: -0.02em;
color: var(--mk-well-num);
}
</style>
</head>
<body>
<div
id="mk-pg-root"
data-composition-id="mk-placeholder-grid"
data-start="0"
data-duration="8"
data-width="1920"
data-height="1080"
>
<div id="mk-pg-grid"></div>
<div
id="mk-pg-drv"
class="clip"
data-start="0"
data-duration="8"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters (inspector-style CONFIG) ---- */
var CONFIG = {
// no scheme param — scheme-agnostic; re-skin via the --mk-* tokens
layout: "1+2", // "2up" | "3up" | "4up" | "3x2" | "1+2" (hero left + two stacked)
margin: 120, // outer margin, px
gap: 40, // px between cells
radius: 40, // cell corner radius (rounded-edge look)
// One entry per cell (in layout order). src: image path, or null for a
// styled drop-zone well. insideScale: media scale inside the mask —
// an "inside scale" convention (1 = cover-fit).
cells: [
{ src: null, insideScale: 1 },
{ src: null, insideScale: 1 },
{ src: null, insideScale: 1 },
],
kenBurns: true, // slow media settle inside the mask
stagger: 0.15, // "box, box, box" entrance
animationIn: true,
animationOut: true,
};
/* decorative fills for empty wells (cycled) */
var WELL_FILLS = [
"linear-gradient(135deg, var(--mk-blob-1) 0%, var(--mk-blob-1-soft) 100%)",
"linear-gradient(135deg, var(--mk-blob-2) 0%, var(--mk-blob-2-soft) 100%)",
"linear-gradient(135deg, var(--mk-accent-dark) 0%, var(--mk-accent-dark-soft) 100%)",
"linear-gradient(135deg, var(--mk-well-warm) 0%, var(--mk-well-warm-soft) 100%)",
"linear-gradient(135deg, var(--mk-well-sky) 0%, var(--mk-well-sky-soft) 100%)",
"linear-gradient(135deg, var(--mk-well-rose) 0%, var(--mk-well-rose-soft) 100%)",
];
var W = 1920,
H = 1080,
DUR = 8;
function rects(layout, m, g) {
var iw = W - 2 * m,
ih = H - 2 * m;
function grid(cols, rows) {
var cw = (iw - (cols - 1) * g) / cols,
ch = (ih - (rows - 1) * g) / rows,
out = [];
for (var r = 0; r < rows; r++)
for (var c = 0; c < cols; c++)
out.push({ x: m + c * (cw + g), y: m + r * (ch + g), w: cw, h: ch });
return out;
}
if (layout === "2up") return grid(2, 1);
if (layout === "3up") return grid(3, 1);
if (layout === "4up") return grid(2, 2);
if (layout === "3x2") return grid(3, 2);
/* "1+2": hero left (58%), two stacked right */
var w1 = Math.round((iw - g) * 0.58),
w2 = iw - g - w1,
h2 = (ih - g) / 2;
return [
{ x: m, y: m, w: w1, h: ih },
{ x: m + w1 + g, y: m, w: w2, h: h2 },
{ x: m + w1 + g, y: m + h2 + g, w: w2, h: h2 },
];
}
var root = document.getElementById("mk-pg-root");
var grid = document.getElementById("mk-pg-grid");
var R = rects(CONFIG.layout, CONFIG.margin, CONFIG.gap);
var cells = [],
medias = [];
R.forEach(function (r, i) {
var conf = CONFIG.cells[i] || {};
var cell = document.createElement("div");
cell.id = "mk-pg-cell-" + i;
cell.className = "mk-pg-cell";
cell.style.left = r.x + "px";
cell.style.top = r.y + "px";
cell.style.width = r.w + "px";
cell.style.height = r.h + "px";
cell.style.borderRadius = CONFIG.radius + "px";
var inner;
if (conf.src) {
inner = document.createElement("img");
inner.className = "mk-pg-media";
inner.src = conf.src;
} else {
inner = document.createElement("div");
inner.className = "mk-pg-well";
inner.style.background = WELL_FILLS[i % WELL_FILLS.length];
var num = document.createElement("span");
num.className = "mk-pg-well-num";
num.textContent = String(i + 1);
inner.appendChild(num);
}
inner.id = "mk-pg-inner-" + i;
inner.setAttribute("data-layout-allow-overflow", ""); // ken-burns scales past the mask by design
cell.appendChild(inner);
grid.appendChild(cell);
cells.push(cell);
medias.push(inner);
gsap.set(inner, { scale: conf.insideScale || 1 });
});
var tl = gsap.timeline({ paused: true });
cells.forEach(function (cell, i) {
var t0 = (CONFIG.animationIn ? 0.3 : 0) + i * CONFIG.stagger;
if (CONFIG.animationIn) {
gsap.set(cell, { opacity: 0, scale: 0.92, y: 28 });
tl.to(cell, { opacity: 1, scale: 1, y: 0, duration: 0.65, ease: "power3.out" }, t0);
}
if (CONFIG.kenBurns) {
var base = (CONFIG.cells[i] && CONFIG.cells[i].insideScale) || 1;
tl.fromTo(
medias[i],
{ scale: base * 1.06 },
{ scale: base, duration: DUR - t0 - 0.6, ease: "sine.out" },
t0,
);
}
});
/* exit + hard kill */
if (CONFIG.animationOut) {
tl.to(grid, { opacity: 0, y: -20, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
}
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
window.__timelines["mk-placeholder-grid"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `placeholder` `media` `grid` `minimal`.
+4 -198
View File
@@ -3,13 +3,13 @@ title: "Count-Up Stat Card"
description: "Big-numeral statistic with count-up, label, thin progress track filling to value/max, and caption; light and dark schemes"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/mk-progress-stat.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Count-Up Stat Card preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-progress-stat.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-progress-stat.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add mk-progress-stat" />
```bash Terminal
npx hyperframes add mk-progress-stat
```
That writes one file: `compositions/mk-progress-stat.html`.
@@ -32,200 +32,6 @@ It runs for 7 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`mk-progress-stat.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#mk-ps-root {
/* ---- mk tokens: override in the host to re-skin the family ---- */
--mk-font: "Inter", -apple-system, sans-serif;
--mk-ink: #1d1d1f;
--mk-ink-dim: #6e6e73;
--mk-ink-dark: #f5f5f7;
--mk-ink-dim-dark: #a1a1a6;
--mk-accent: #0071e3;
--mk-accent-dark: #7d5cff;
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent; /* overlays footage or mk-background */
}
#mk-ps-group {
position: absolute;
font-family: var(--mk-font);
}
#mk-ps-num {
font-weight: 600;
font-size: 190px;
line-height: 1;
letter-spacing: -0.03em;
color: var(--mk-ink);
font-variant-numeric: tabular-nums;
}
#mk-ps-label {
margin-top: 10px;
font-weight: 500;
font-size: 42px;
letter-spacing: -0.01em;
color: var(--mk-ink);
}
#mk-ps-track {
position: relative;
margin-top: 28px;
height: 6px;
border-radius: 3px;
background: rgba(29, 29, 31, 0.1);
overflow: hidden;
}
#mk-ps-fill {
position: absolute;
inset: 0;
border-radius: 3px;
background: var(--mk-accent);
transform-origin: left center;
}
#mk-ps-caption {
margin-top: 22px;
font-weight: 400;
font-size: 30px;
color: var(--mk-ink-dim);
}
.mk-dark #mk-ps-num,
.mk-dark #mk-ps-label {
color: var(--mk-ink-dark);
}
.mk-dark #mk-ps-caption {
color: var(--mk-ink-dim-dark);
}
.mk-dark #mk-ps-track {
background: rgba(245, 245, 247, 0.18);
}
.mk-dark #mk-ps-fill {
background: var(--mk-accent-dark);
}
</style>
</head>
<body>
<div
id="mk-ps-root"
data-composition-id="mk-progress-stat"
data-start="0"
data-duration="7"
data-width="1920"
data-height="1080"
>
<div id="mk-ps-group">
<div id="mk-ps-num">0</div>
<div id="mk-ps-label"></div>
<div id="mk-ps-track"><div id="mk-ps-fill"></div></div>
<div id="mk-ps-caption"></div>
</div>
<div
id="mk-ps-drv"
class="clip"
data-start="0"
data-duration="7"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters (inspector-style CONFIG) ----
A "22 · Goals reached" progress card: big count-up
numeral, label, thin accent track filling to value/max, caption. */
var CONFIG = {
scheme: "light", // "light" | "dark"
value: 22,
suffix: "", // e.g. "%", "K"
label: "Goals reached",
caption: "Great job, we are getting closer!",
max: 30, // track fills to value/max
trackWidth: 480, // px
x: 1300, // group left edge; null = centered
y: 250, // group top edge; null = centered
animationIn: true,
animationOut: true,
};
var DUR = 7;
var root = document.getElementById("mk-ps-root");
var group = document.getElementById("mk-ps-group");
var num = document.getElementById("mk-ps-num");
var fill = document.getElementById("mk-ps-fill");
if (CONFIG.scheme === "dark") root.classList.add("mk-dark");
document.getElementById("mk-ps-label").textContent = CONFIG.label;
document.getElementById("mk-ps-caption").textContent = CONFIG.caption;
document.getElementById("mk-ps-track").style.width = CONFIG.trackWidth + "px";
/* position (measure after filling text) */
var rect = group.getBoundingClientRect();
group.style.left =
(CONFIG.x === null ? Math.round((1920 - rect.width) / 2) : CONFIG.x) + "px";
group.style.top =
(CONFIG.y === null ? Math.round((1080 - rect.height) / 2) : CONFIG.y) + "px";
var tl = gsap.timeline({ paused: true });
if (CONFIG.animationIn) {
gsap.set(group, { opacity: 0, y: 28 });
tl.to(group, { opacity: 1, y: 0, duration: 0.7, ease: "power3.out" }, 0.3);
}
/* count-up + track fill on the same ease (the data-viz recipe) */
var st = { v: 0 };
tl.to(
st,
{
v: CONFIG.value,
duration: 1.6,
ease: "power2.out",
onUpdate: function () {
num.textContent = Math.round(st.v) + CONFIG.suffix;
},
},
0.5,
);
gsap.set(fill, { scaleX: 0 });
tl.to(fill, { scaleX: CONFIG.value / CONFIG.max, duration: 1.6, ease: "power2.out" }, 0.5);
if (CONFIG.animationOut) {
tl.to(group, { opacity: 0, y: -20, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
}
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
window.__timelines["mk-progress-stat"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `data-viz` `stat` `minimal`.
+4 -235
View File
@@ -3,13 +3,13 @@ title: "Specs Checklist"
description: "Left-aligned specs/traits checklist over footage or a board: rows slide in staggered with an accent underline sweep; light and dark schemes"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/mk-specs-list.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Specs Checklist preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-specs-list.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-specs-list.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add mk-specs-list" />
```bash Terminal
npx hyperframes add mk-specs-list
```
That writes one file: `compositions/mk-specs-list.html`.
@@ -32,237 +32,6 @@ It runs for 8 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`mk-specs-list.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
#mk-sl-root {
/* ---- mk tokens: override in the host to re-skin the family ---- */
--mk-font: "Inter", -apple-system, sans-serif;
--mk-ink: #1d1d1f;
--mk-ink-dim: #6e6e73;
--mk-ink-dark: #f5f5f7;
--mk-ink-dim-dark: #a1a1a6;
--mk-accent: #0071e3;
--mk-accent-dark: #7d5cff;
--mk-hairline: 2px;
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent; /* overlays footage or mk-background */
}
#mk-sl-scrim {
position: absolute;
inset: 0;
background: linear-gradient(90deg, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0) 62%);
opacity: 0;
}
#mk-sl-col {
position: absolute;
display: flex;
flex-direction: column;
}
.mk-sl-row {
font-family: var(--mk-font);
line-height: 1;
opacity: 0;
}
.mk-sl-text {
display: flex;
align-items: baseline;
gap: 18px;
white-space: nowrap;
}
.mk-sl-label {
font-weight: 600;
font-size: 46px;
letter-spacing: -0.015em;
color: var(--mk-ink);
}
.mk-sl-value {
font-weight: 400;
font-size: 40px;
letter-spacing: -0.01em;
color: var(--mk-ink-dim);
}
.mk-sl-line {
position: relative;
margin-top: 22px;
height: var(--mk-hairline);
background: rgba(0, 0, 0, 0.1);
border-radius: 2px;
overflow: hidden;
}
.mk-sl-line-fill {
position: absolute;
inset: 0;
background: var(--mk-accent);
transform-origin: left center;
}
/* dark scheme: white rows for use over footage (overlay look) */
.mk-dark .mk-sl-label {
color: var(--mk-ink-dark);
}
.mk-dark .mk-sl-value {
color: var(--mk-ink-dim-dark);
}
.mk-dark .mk-sl-line {
background: rgba(255, 255, 255, 0.18);
}
.mk-dark .mk-sl-line-fill {
background: var(--mk-accent-dark);
}
</style>
</head>
<body>
<div
id="mk-sl-root"
data-composition-id="mk-specs-list"
data-start="0"
data-duration="8"
data-width="1920"
data-height="1080"
>
<div id="mk-sl-scrim"></div>
<div id="mk-sl-col"></div>
<div
id="mk-sl-drv"
class="clip"
data-start="0"
data-duration="8"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
/* ---- published parameters (inspector-style CONFIG) ---- */
var CONFIG = {
scheme: "light", // "light" (ink rows) | "dark" (white rows, for footage)
rows: [
{ label: "Design", value: "Minimal, clean lines" },
{ label: "Typography", value: "Inter SemiBold" },
{ label: "Motion", value: "Restrained, no bounce" },
{ label: "Color", value: "Stage + one accent" },
{ label: "Shapes", value: "Capsules & rounded cards" },
],
underline: true, // accent sweep under each row
lineWidth: 660, // px
scrim: 0, // 01 left-edge dark scrim for readability over footage
x: 160, // column left edge
rowGap: 44, // px between rows
stagger: 0.18, // s between row entrances
animationIn: true,
animationOut: true,
};
var DUR = 8;
var root = document.getElementById("mk-sl-root");
var col = document.getElementById("mk-sl-col");
var scrim = document.getElementById("mk-sl-scrim");
if (CONFIG.scheme === "dark") root.classList.add("mk-dark");
scrim.style.opacity = String(CONFIG.scrim);
/* build rows */
CONFIG.rows.forEach(function (r, i) {
var row = document.createElement("div");
row.id = "mk-sl-row-" + i;
row.className = "mk-sl-row";
if (i > 0) row.style.marginTop = CONFIG.rowGap + "px";
var text = document.createElement("div");
text.className = "mk-sl-text";
var label = document.createElement("span");
label.className = "mk-sl-label";
label.textContent = r.label;
text.appendChild(label);
if (r.value) {
var value = document.createElement("span");
value.className = "mk-sl-value";
value.textContent = r.value;
text.appendChild(value);
}
row.appendChild(text);
if (CONFIG.underline) {
var line = document.createElement("div");
line.className = "mk-sl-line";
line.style.width = CONFIG.lineWidth + "px";
var fill = document.createElement("div");
fill.id = "mk-sl-fill-" + i;
fill.className = "mk-sl-line-fill";
line.appendChild(fill);
row.appendChild(line);
}
col.appendChild(row);
});
/* center the column vertically */
var colH =
CONFIG.rows.length * (46 + 22 + (CONFIG.underline ? 2 : 0)) +
(CONFIG.rows.length - 1) * CONFIG.rowGap;
col.style.left = CONFIG.x + "px";
col.style.top = Math.round((1080 - colH) / 2) + "px";
var tl = gsap.timeline({ paused: true });
CONFIG.rows.forEach(function (r, i) {
var row = document.getElementById("mk-sl-row-" + i);
var t0 = (CONFIG.animationIn ? 0.3 : 0) + i * CONFIG.stagger;
if (CONFIG.animationIn) {
gsap.set(row, { opacity: 0, x: -36 });
tl.to(row, { opacity: 1, x: 0, duration: 0.7, ease: "power3.out" }, t0);
} else {
gsap.set(row, { opacity: 1 });
}
if (CONFIG.underline) {
var fill = document.getElementById("mk-sl-fill-" + i);
gsap.set(fill, { scaleX: 0 });
tl.to(fill, { scaleX: 1, duration: 0.8, ease: "power2.inOut" }, t0 + 0.15);
}
});
/* exit + hard kill */
if (CONFIG.animationOut) {
tl.to(col, { opacity: 0, y: -20, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
tl.to(scrim, { opacity: 0, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
}
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
window.__timelines["mk-specs-list"] = tl;
})();
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `data-viz` `list` `minimal` `professional`.
+14 -420
View File
@@ -3,440 +3,34 @@ title: "News Ticker"
description: "Premium broadcast-style lower-third ticker with live label, headline ribbon, and scrolling news crawl."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/news-ticker.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="News Ticker preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/news-ticker.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/news-ticker.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add news-ticker" />
```bash Terminal
npx hyperframes add news-ticker
```
That writes one file: `compositions/news-ticker.html`.
## Source
## Add it to your video
<Accordion title={`news-ticker.html`}>
It runs for 7 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>News Ticker</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: transparent;
}
body {
font-family: "Inter", sans-serif;
}
#nt-root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
color: #f8fafc;
background: transparent;
}
#nt-root .nt-shell {
position: absolute;
left: 96px;
right: 96px;
bottom: 72px;
height: 216px;
filter: drop-shadow(0 28px 44px rgba(0, 0, 0, 0.32));
}
#nt-root .nt-panel {
position: absolute;
inset: 0;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 28px;
background:
linear-gradient(180deg, rgba(21, 31, 48, 0.96), rgba(7, 13, 24, 0.98)),
linear-gradient(90deg, rgba(201, 31, 45, 0.72), transparent 38%);
}
#nt-root .nt-panel::before {
position: absolute;
inset: 0;
content: "";
background:
radial-gradient(circle at 14% 0%, rgba(255, 255, 255, 0.2), transparent 26%),
linear-gradient(
110deg,
transparent 0 42%,
rgba(255, 255, 255, 0.06) 42% 43%,
transparent 43% 100%
);
opacity: 0.72;
pointer-events: none;
}
#nt-root .nt-top-row {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 126px;
display: flex;
align-items: stretch;
}
#nt-root .nt-brand {
width: 294px;
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
background: linear-gradient(135deg, #e11d2e, #9f1220);
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.16);
}
#nt-root .nt-live-dot {
width: 18px;
height: 18px;
border-radius: 999px;
background: #ffffff;
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.55);
}
#nt-root .nt-brand-text {
display: flex;
flex-direction: column;
gap: 4px;
text-transform: uppercase;
}
#nt-root .nt-live {
font-size: 22px;
font-weight: 900;
line-height: 1;
letter-spacing: 0.16em;
}
#nt-root .nt-desk {
font-size: 15px;
font-weight: 700;
line-height: 1;
letter-spacing: 0.22em;
color: #ffffff;
}
#nt-root .nt-headline-wrap {
position: relative;
flex: 1;
display: flex;
align-items: center;
padding: 0 250px 0 44px;
overflow: hidden;
}
#nt-root .nt-headline {
position: relative;
z-index: 1;
font-size: 48px;
font-weight: 850;
line-height: 1.02;
letter-spacing: 0;
white-space: nowrap;
color: #ffffff;
text-shadow: 0 2px 14px rgba(0, 0, 0, 0.34);
}
#nt-root .nt-headline-accent {
color: #ff4455;
}
#nt-root .nt-time {
position: absolute;
top: 25px;
right: 38px;
padding: 10px 16px;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.74);
font-size: 18px;
font-weight: 700;
line-height: 1;
letter-spacing: 0.08em;
font-variant-numeric: tabular-nums;
}
#nt-root .nt-bottom-row {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 90px;
display: flex;
align-items: center;
border-top: 1px solid rgba(255, 255, 255, 0.11);
background: rgba(2, 6, 13, 0.58);
}
#nt-root .nt-label {
width: 294px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-right: 1px solid rgba(255, 255, 255, 0.14);
background: #5f0712;
color: #ffffff;
font-size: 24px;
font-weight: 900;
line-height: 1;
letter-spacing: 0.14em;
text-transform: uppercase;
}
#nt-root .nt-ticker-window {
position: relative;
flex: 1;
height: 100%;
overflow: hidden;
background: #050b15;
}
#nt-root .nt-ticker-window::before,
#nt-root .nt-ticker-window::after {
position: absolute;
top: 0;
bottom: 0;
z-index: 2;
width: 90px;
content: "";
pointer-events: none;
}
#nt-root .nt-ticker-window::before {
left: 0;
background: linear-gradient(90deg, #050b15, transparent);
}
#nt-root .nt-ticker-window::after {
right: 0;
background: linear-gradient(270deg, #050b15, transparent);
}
#nt-root .nt-ticker-track {
position: absolute;
left: 34px;
top: 0;
height: 100%;
display: flex;
align-items: center;
gap: 34px;
white-space: nowrap;
will-change: transform;
}
#nt-root .nt-item {
display: inline-flex;
align-items: center;
gap: 14px;
padding: 13px 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 999px;
background: #08111f;
color: #ffffff;
font-size: 31px;
font-weight: 650;
line-height: 1;
letter-spacing: 0;
}
#nt-root .nt-separator {
width: 9px;
height: 9px;
border-radius: 999px;
background: #e11d2e;
box-shadow: 0 0 22px rgba(225, 29, 46, 0.75);
}
#nt-root .nt-metric {
padding: 4px 8px;
border-radius: 8px;
background: #063344;
color: #c8f8ff;
font-weight: 850;
font-variant-numeric: tabular-nums;
}
#nt-root .nt-flash {
position: absolute;
top: 0;
bottom: 0;
left: -260px;
z-index: 3;
width: 210px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.18), transparent);
transform: skewX(-16deg);
}
</style>
</head>
<body>
```html index.html
<div
id="nt-root"
data-composition-id="news-ticker"
data-composition-src="compositions/news-ticker.html"
data-start="0"
data-duration="7"
data-track-index="1"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="7"
>
<section
id="nt-shell"
class="clip nt-shell"
data-start="0"
data-duration="7"
data-track-index="0"
>
<div class="nt-panel">
<div id="nt-flash" class="nt-flash"></div>
<div class="nt-top-row">
<div id="nt-brand" class="nt-brand">
<div id="nt-live-dot" class="nt-live-dot"></div>
<div class="nt-brand-text">
<div class="nt-live">Live</div>
<div class="nt-desk">Global Desk</div>
</div>
</div>
<div class="nt-headline-wrap">
<h1 id="nt-headline" class="nt-headline">
AI video tools move from <span class="nt-headline-accent">prototype</span> to
production
</h1>
<div id="nt-time" class="nt-time">09:41 UTC</div>
</div>
</div>
<div class="nt-bottom-row">
<div id="nt-label" class="nt-label">Breaking</div>
<div class="nt-ticker-window">
<div id="nt-ticker-track" class="nt-ticker-track">
<span class="nt-item"
><span class="nt-separator"></span>Creator teams shorten launch cycles by
<span class="nt-metric">38%</span></span
>
<span class="nt-item"
><span class="nt-separator"></span>Rendering queues clear in under
<span class="nt-metric">2 min</span></span
>
<span class="nt-item"
><span class="nt-separator"></span>New catalog blocks trending across product
demos</span
>
<span class="nt-item"
><span class="nt-separator"></span>Studio editors adopt HTML-first motion
systems</span
>
<span class="nt-item"
><span class="nt-separator"></span>Creator teams shorten launch cycles by
<span class="nt-metric">38%</span></span
>
<span class="nt-item"
><span class="nt-separator"></span>Rendering queues clear in under
<span class="nt-metric">2 min</span></span
>
</div>
</div>
</div>
</div>
</section>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var shell = document.getElementById("nt-shell");
var brand = document.getElementById("nt-brand");
var headline = document.getElementById("nt-headline");
var label = document.getElementById("nt-label");
var tickerTrack = document.getElementById("nt-ticker-track");
var liveDot = document.getElementById("nt-live-dot");
var flash = document.getElementById("nt-flash");
var time = document.getElementById("nt-time");
gsap.set(shell, { y: 260, opacity: 0 });
gsap.set(brand, { x: -80, opacity: 0 });
gsap.set(headline, { y: 26, opacity: 0, clipPath: "inset(0 100% 0 0)" });
gsap.set(label, { y: 18, opacity: 0 });
gsap.set(time, { y: -10, opacity: 0 });
gsap.set(tickerTrack, { x: 1260 });
tl.to(shell, { y: 0, opacity: 1, duration: 0.52, ease: "power4.out" }, 0);
tl.to(brand, { x: 0, opacity: 1, duration: 0.42, ease: "power3.out" }, 0.08);
tl.to(
headline,
{
y: 0,
opacity: 1,
clipPath: "inset(0 0% 0 0)",
duration: 0.62,
ease: "power3.out",
},
0.18,
);
tl.to(time, { y: 0, opacity: 1, duration: 0.32, ease: "power2.out" }, 0.5);
tl.to(label, { y: 0, opacity: 1, duration: 0.28, ease: "power2.out" }, 0.55);
tl.to(flash, { x: 2040, duration: 0.86, ease: "power2.inOut" }, 0.34);
tl.to(
tickerTrack,
{
x: -1120,
duration: 6.05,
ease: "none",
},
0.62,
);
tl.to(
liveDot,
{
scale: 1.32,
boxShadow: "0 0 0 18px rgba(255,255,255,0)",
duration: 0.42,
repeat: 11,
yoyo: true,
ease: "sine.inOut",
},
0.58,
);
tl.to(flash, { x: 4080, duration: 0.72, ease: "power2.inOut" }, 3.42);
tl.to(shell, { y: 170, opacity: 0, duration: 0.34, ease: "power3.in" }, 6.62);
window.__timelines["news-ticker"] = tl;
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="news-ticker" data-composition-src="compositions/news-ticker.html" data-start="0" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+12 -315
View File
@@ -3,337 +3,34 @@ title: "North Korea Locked Down"
description: "Realistic map zoom into North Korea with a red scribble circle, locked-down pop-up label, and reddish editorial wash."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/north-korea-locked-down.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="North Korea Locked Down preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/north-korea-locked-down.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/north-korea-locked-down.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add north-korea-locked-down" />
```bash Terminal
npx hyperframes add north-korea-locked-down
```
That writes `compositions/north-korea-locked-down.html`, plus 1 supporting file under `assets/`.
## Source
## Add it to your video
<Accordion title={`north-korea-locked-down.html`}>
It runs for 7 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #eef3f4;
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #eef3f4;
color: #151515;
}
.camera {
position: absolute;
inset: 0;
z-index: 0;
transform-origin: 50% 51%;
will-change: transform, filter;
}
.map {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.paper-depth {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 52% 50%, rgba(255, 255, 255, 0.08), transparent 28%),
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(10, 20, 24, 0.12));
mix-blend-mode: multiply;
pointer-events: none;
}
.annotation {
position: absolute;
left: 652px;
top: 235px;
z-index: 4;
width: 560px;
height: 392px;
overflow: visible;
transform-origin: center;
will-change: transform, opacity;
}
.scribble {
position: absolute;
inset: 0;
width: 560px;
height: 392px;
overflow: visible;
}
.scribble path {
fill: none;
stroke: #e21d2f;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 8px 12px rgba(226, 29, 47, 0.24));
}
#circle-a {
stroke-width: 12;
stroke-dasharray: 980;
stroke-dashoffset: 980;
}
#circle-b {
stroke-width: 6;
opacity: 0.82;
stroke-dasharray: 910;
stroke-dashoffset: 910;
}
.label {
position: absolute;
left: 50%;
top: -146px;
transform: translateX(-50%) scale(0.82);
transform-origin: 50% 100%;
min-width: 370px;
padding: 18px 26px 20px;
border-radius: 18px;
color: #ffffff;
background: #111111;
box-shadow:
0 22px 54px rgba(25, 4, 7, 0.24),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
font-size: 60px;
line-height: 0.9;
font-weight: 900;
letter-spacing: 0;
text-align: center;
text-transform: uppercase;
opacity: 0;
}
.label::after {
content: "";
position: absolute;
left: 50%;
bottom: -16px;
width: 32px;
height: 32px;
background: #111111;
transform: translateX(-50%) rotate(45deg);
}
.red-wash {
position: absolute;
inset: 0;
z-index: 2;
background:
radial-gradient(
circle at 50% 48%,
rgba(255, 59, 48, 0.3),
rgba(255, 59, 48, 0.06) 42%,
rgba(79, 0, 8, 0.22) 100%
),
linear-gradient(90deg, rgba(99, 0, 12, 0.12), rgba(255, 59, 48, 0.2));
opacity: 0;
mix-blend-mode: multiply;
pointer-events: none;
}
.scanline {
position: absolute;
inset: 0;
z-index: 3;
opacity: 0;
background: repeating-linear-gradient(
0deg,
rgba(65, 0, 0, 0.12) 0,
rgba(65, 0, 0, 0.12) 1px,
transparent 1px,
transparent 7px
);
mix-blend-mode: multiply;
pointer-events: none;
}
.corner {
position: absolute;
z-index: 6;
width: 84px;
height: 84px;
border-color: rgba(17, 17, 17, 0.62);
opacity: 0;
}
.corner.tl {
left: 86px;
top: 78px;
border-top: 5px solid;
border-left: 5px solid;
}
.corner.tr {
right: 86px;
top: 78px;
border-top: 5px solid;
border-right: 5px solid;
}
.corner.bl {
left: 86px;
bottom: 78px;
border-bottom: 5px solid;
border-left: 5px solid;
}
.corner.br {
right: 86px;
bottom: 78px;
border-bottom: 5px solid;
border-right: 5px solid;
}
.attribution {
position: absolute;
right: 28px;
bottom: 22px;
z-index: 7;
color: rgba(17, 17, 17, 0.44);
font-size: 19px;
font-weight: 650;
opacity: 0.7;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="north-korea-locked-down"
data-root="true"
data-composition-src="compositions/north-korea-locked-down.html"
data-start="0"
data-duration="7"
data-track-index="1"
data-width="1920"
data-height="1080"
>
<div id="camera" class="camera">
<img class="map" src="assets/korea-map.png" alt="" />
<div class="paper-depth"></div>
</div>
<div id="annotation" class="annotation">
<div id="label" class="label">LOCKED<br />DOWN</div>
<svg class="scribble" viewBox="0 0 560 392" aria-hidden="true" data-layout-ignore>
<path
id="circle-a"
d="M 74 211 C 42 119 118 42 254 36 C 410 30 524 111 508 222 C 494 320 350 377 208 346 C 94 322 37 278 74 211"
/>
<path
id="circle-b"
d="M 92 231 C 33 150 105 55 243 44 C 408 31 529 126 501 244 C 480 334 335 370 197 342 C 83 319 47 269 92 231"
/>
</svg>
</div>
<div id="redWash" class="red-wash"></div>
<div id="scanline" class="scanline"></div>
<div class="corner tl"></div>
<div class="corner tr"></div>
<div class="corner bl"></div>
<div class="corner br"></div>
<div class="attribution">© OpenStreetMap contributors © CARTO</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({
paused: true,
defaults: { overwrite: "auto" },
});
tl.fromTo(
"#camera",
{ scale: 0.92, x: -70, y: 18, filter: "saturate(0.92) contrast(0.98)" },
{
scale: 2.12,
x: -56,
y: -22,
filter: "saturate(1.08) contrast(1.04)",
duration: 3.18,
ease: "expo.inOut",
},
0,
);
tl.to("#camera", { scale: 2.2, x: -44, y: -34, duration: 0.42, ease: "back.out(1.5)" }, 3.18);
tl.from(
".corner",
{ opacity: 0, scale: 0.72, duration: 0.28, stagger: 0.035, ease: "power3.out" },
0.42,
);
tl.to(".corner", { opacity: 0.72, duration: 0.18, ease: "power2.out" }, 1.2);
tl.fromTo(
"#annotation",
{ opacity: 0, scale: 0.92, x: 4, y: -4 },
{ opacity: 1, scale: 1, x: 0, y: 0, duration: 0.16, ease: "power2.out" },
3.12,
);
tl.to("#circle-a", { strokeDashoffset: 0, duration: 0.62, ease: "power2.out" }, 3.24);
tl.to("#circle-b", { strokeDashoffset: 0, duration: 0.44, ease: "power3.out" }, 3.46);
tl.to("#label", { opacity: 1, scale: 1, duration: 0.28, ease: "back.out(2.1)" }, 3.78);
tl.to("#redWash", { opacity: 1, duration: 0.24, ease: "power2.out" }, 3.82);
tl.to("#scanline", { opacity: 0.44, duration: 0.18, ease: "none" }, 3.86);
tl.to(
"#annotation",
{ scale: 1.045, duration: 0.14, yoyo: true, repeat: 1, ease: "sine.inOut" },
4.1,
);
tl.to("#camera", { scale: 2.06, x: -82, y: -8, duration: 2.35, ease: "sine.inOut" }, 4.2);
tl.to("#redWash", { opacity: 0.82, duration: 2.35, ease: "sine.inOut" }, 4.2);
tl.to(".corner", { opacity: 0.38, duration: 0.72, ease: "sine.inOut" }, 5.82);
tl.to("#label", { y: -6, duration: 0.4, yoyo: true, repeat: 1, ease: "sine.inOut" }, 5.94);
window.__timelines["north-korea-locked-down"] = tl;
</script>
</body>
</html>
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="north-korea-locked-down" data-composition-src="compositions/north-korea-locked-down.html" data-start="0" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
+13 -536
View File
@@ -3,557 +3,34 @@ title: "NYC Paris Flight"
description: "Apple-style realistic map animation with a plane flying from New York to Paris, marker circle, landing pop, and sound effects."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/nyc-paris-flight.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="NYC Paris Flight preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/nyc-paris-flight.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/nyc-paris-flight.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add nyc-paris-flight" />
```bash Terminal
npx hyperframes add nyc-paris-flight
```
That writes `compositions/nyc-paris-flight.html`, plus 2 supporting files under `assets/`.
## Source
## Add it to your video
<Accordion title={`nyc-paris-flight.html`}>
It runs for 6 seconds at 1920×1080. Paste this into your composition:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #f5f5f7;
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #f5f5f7;
color: #1d1d1f;
}
.stage {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #f5f5f7;
}
.map-plane {
position: absolute;
inset: 0;
transform-origin: 52% 48%;
will-change: transform, opacity;
}
.map-image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.soft-ocean {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 50% 24%, rgba(0, 113, 227, 0.18), rgba(0, 113, 227, 0) 34%),
linear-gradient(180deg, rgba(245, 245, 247, 0.05), rgba(245, 245, 247, 0.3));
mix-blend-mode: multiply;
opacity: 0.72;
}
.route-svg,
.doodle-svg {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
overflow: visible;
}
.route-glow {
fill: none;
stroke: rgba(0, 113, 227, 0.23);
stroke-width: 24;
stroke-linecap: round;
stroke-dasharray: 2000;
stroke-dashoffset: 2000;
filter: blur(4px);
}
.route-path {
fill: none;
stroke: #0071e3;
stroke-width: 9;
stroke-linecap: round;
stroke-dasharray: 2000;
stroke-dashoffset: 2000;
filter: drop-shadow(0 10px 20px rgba(0, 113, 227, 0.22));
}
.route-thread {
fill: none;
stroke: rgba(255, 255, 255, 0.86);
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 2000;
stroke-dashoffset: 2000;
}
.route-tick {
position: absolute;
width: 14px;
height: 14px;
margin: -7px 0 0 -7px;
border-radius: 999px;
background: #ffffff;
border: 3px solid #0071e3;
box-shadow: 0 8px 26px rgba(0, 113, 227, 0.32);
opacity: 0;
}
.pin {
position: absolute;
width: 26px;
height: 26px;
border-radius: 999px;
background: #ffffff;
border: 7px solid #0071e3;
box-shadow:
0 15px 34px rgba(0, 0, 0, 0.18),
0 0 0 10px rgba(0, 113, 227, 0.12);
transform: translate(-50%, -50%);
transform-origin: center;
will-change: transform, opacity;
}
.pin::after {
content: "";
position: absolute;
inset: -20px;
border-radius: 999px;
border: 2px solid rgba(0, 113, 227, 0.25);
}
.pin-label {
position: absolute;
top: 32px;
min-width: 142px;
padding: 12px 16px 13px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(255, 255, 255, 0.88);
color: #1d1d1f;
font-size: 22px;
font-weight: 800;
line-height: 1;
letter-spacing: 0;
box-shadow: 0 18px 38px rgba(29, 29, 31, 0.16);
backdrop-filter: blur(18px);
white-space: nowrap;
}
.pin-label span {
display: block;
margin-top: 7px;
color: #6e6e73;
font-size: 13px;
font-weight: 700;
}
#nyc-pin {
left: 308.41px;
top: 584.16px;
}
#nyc-pin .pin-label {
left: 28px;
}
#paris-pin {
left: 1611.59px;
top: 387.84px;
}
#paris-pin .pin-label {
right: 28px;
}
.plane {
position: absolute;
left: 0;
top: 0;
width: 112px;
height: 112px;
offset-path: path("M 308.41 584.158 C 650 520 1120 245 1611.59 387.842");
offset-distance: 0%;
offset-rotate: auto;
offset-anchor: 50% 50%;
transform-origin: center;
filter: drop-shadow(0 22px 22px rgba(0, 0, 0, 0.24));
opacity: 1;
will-change: offset-distance, transform, opacity;
}
.plane-core {
position: absolute;
inset: 0;
transform-origin: center;
will-change: transform;
}
.plane svg {
width: 100%;
height: 100%;
overflow: visible;
}
.doodle-line {
fill: none;
stroke: #ff3b30;
stroke-width: 13;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 900;
stroke-dashoffset: 900;
filter: drop-shadow(0 10px 14px rgba(255, 59, 48, 0.18));
}
.doodle-line.secondary {
stroke-width: 6;
opacity: 0.82;
}
.paris-pulse {
position: absolute;
left: 1611.59px;
top: 387.84px;
width: 126px;
height: 126px;
margin: -63px 0 0 -63px;
border-radius: 999px;
border: 4px solid rgba(255, 59, 48, 0.34);
opacity: 0;
}
.landing-badge {
position: absolute;
left: 1500px;
top: 502px;
padding: 15px 20px 16px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.9);
color: #d70015;
font-size: 24px;
font-weight: 900;
line-height: 1;
letter-spacing: 0;
box-shadow: 0 20px 44px rgba(29, 29, 31, 0.14);
opacity: 0;
transform: scale(0.78);
transform-origin: center;
}
.title-stack {
position: absolute;
left: 82px;
top: 70px;
z-index: 6;
max-width: 780px;
padding: 30px 34px 33px;
border-radius: 30px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(255, 255, 255, 0.86);
box-shadow: 0 28px 70px rgba(29, 29, 31, 0.12);
backdrop-filter: blur(22px);
}
.eyebrow {
color: #6e6e73;
font-size: 19px;
font-weight: 800;
letter-spacing: 0;
}
.headline {
margin-top: 11px;
color: #1d1d1f;
font-size: 84px;
font-weight: 900;
line-height: 0.92;
letter-spacing: 0;
}
.headline span {
color: #0071e3;
}
.route-meta {
display: flex;
align-items: center;
gap: 14px;
margin-top: 18px;
color: #6e6e73;
font-size: 25px;
font-weight: 800;
}
.route-meta i {
display: block;
width: 54px;
height: 6px;
border-radius: 999px;
background: #0071e3;
}
.attribution {
position: absolute;
right: 28px;
bottom: 24px;
z-index: 5;
padding: 8px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.68);
color: rgba(29, 29, 31, 0.76);
font-size: 14px;
font-weight: 700;
line-height: 1;
}
.white-screen {
position: absolute;
inset: 0;
z-index: 30;
background: #ffffff;
opacity: 0;
}
</style>
</head>
<body>
```html index.html
<div
id="root"
data-composition-id="nyc-paris-flight"
data-start="0"
data-duration="6"
data-width="1920"
data-height="1080"
>
<div
id="flight-scene"
class="clip stage"
data-start="0"
data-duration="6"
data-track-index="0"
>
<div id="map-plane" class="map-plane" data-layout-allow-overflow>
<img class="map-image" src="assets/map-nyc-paris.png" alt="" />
<div class="soft-ocean" data-layout-ignore></div>
<svg class="route-svg" viewBox="0 0 1920 1080" aria-hidden="true" data-layout-ignore>
<path
id="route-glow"
class="route-glow"
d="M 308.41 584.158 C 650 520 1120 245 1611.59 387.842"
/>
<path
id="route-path"
class="route-path"
d="M 308.41 584.158 C 650 520 1120 245 1611.59 387.842"
/>
<path
id="route-thread"
class="route-thread"
d="M 308.41 584.158 C 650 520 1120 245 1611.59 387.842"
/>
</svg>
<div class="route-tick" style="left: 481.8px; top: 539.7px"></div>
<div class="route-tick" style="left: 647.6px; top: 486.5px"></div>
<div class="route-tick" style="left: 824.8px; top: 430.6px"></div>
<div class="route-tick" style="left: 1011.5px; top: 382.4px"></div>
<div class="route-tick" style="left: 1206.1px; top: 352.3px"></div>
<div class="route-tick" style="left: 1406.7px; top: 350.7px"></div>
<div id="nyc-pin" class="pin">
<div class="pin-label">New York<span>JFK / NYC</span></div>
</div>
<div id="paris-pin" class="pin">
<div class="pin-label">Paris<span>CDG / FR</span></div>
</div>
<div id="plane" class="plane" aria-hidden="true">
<div id="plane-core" class="plane-core">
<svg viewBox="-64 -64 128 128">
<path
d="M 58 0 C 52 -10 37 -15 15 -16 L -18 -56 C -23 -62 -34 -58 -32 -49 L -21 -15 L -48 -12 C -56 -11 -60 -6 -60 0 C -60 6 -56 11 -48 12 L -21 15 L -32 49 C -34 58 -23 62 -18 56 L 15 16 C 37 15 52 10 58 0 Z"
fill="#ffffff"
/>
<path
d="M 54 0 C 46 -6 33 -9 14 -10 L -19 -50 C -21 -53 -25 -52 -25 -47 L -15 -10 L -49 -6 C -53 -5 -55 -2 -55 0 C -55 2 -53 5 -49 6 L -15 10 L -25 47 C -25 52 -21 53 -19 50 L 14 10 C 33 9 46 6 54 0 Z"
fill="#0071e3"
/>
<circle cx="18" cy="0" r="6" fill="#f5f5f7" opacity="0.95" />
</svg>
</div>
</div>
<svg class="doodle-svg" viewBox="0 0 1920 1080" aria-hidden="true" data-layout-ignore>
<path
id="doodle-a"
class="doodle-line"
d="M 1510 382 C 1505 323 1560 287 1627 300 C 1700 315 1740 358 1720 416 C 1705 468 1624 486 1556 462 C 1518 449 1494 417 1510 382"
/>
<path
id="doodle-b"
class="doodle-line secondary"
d="M 1518 396 C 1488 356 1528 307 1605 296 C 1688 284 1748 338 1730 397 C 1713 453 1641 480 1570 463 C 1526 453 1504 427 1518 396"
/>
</svg>
<div id="paris-pulse" class="paris-pulse"></div>
<div id="landing-badge" class="landing-badge">ARRIVED</div>
</div>
<div class="title-stack">
<div class="eyebrow">Six-second transatlantic hop</div>
<div class="headline">NEW YORK <span>TO</span> PARIS</div>
<div class="route-meta">
<i></i>
<p>5,837 km compressed</p>
</div>
</div>
<div class="attribution">© OpenStreetMap contributors © CARTO</div>
<div id="white-screen" class="white-screen"></div>
</div>
<audio
id="sfx"
data-composition-src="compositions/nyc-paris-flight.html"
data-start="0"
data-duration="6"
data-track-index="1"
src="assets/sfx-mix.wav"
data-volume="0.95"
></audio>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({
paused: true,
defaults: { overwrite: "auto" },
});
tl.from(
"#map-plane",
{ scale: 1.1, x: -38, y: 20, opacity: 0.94, duration: 1.05, ease: "power2.out" },
0.16,
);
tl.from(
".title-stack",
{ y: 28, scale: 0.97, opacity: 0, duration: 0.52, ease: "expo.out" },
0.24,
);
tl.from(".eyebrow", { x: -22, opacity: 0, duration: 0.36, ease: "power3.out" }, 0.32);
tl.from(".headline", { y: 34, opacity: 0, duration: 0.5, ease: "back.out(1.45)" }, 0.4);
tl.from(".route-meta", { x: -26, opacity: 0, duration: 0.38, ease: "circ.out" }, 0.58);
tl.from("#nyc-pin", { scale: 0.12, opacity: 0, duration: 0.34, ease: "back.out(2.8)" }, 0.7);
tl.from(
"#paris-pin",
{ scale: 0.12, opacity: 0, duration: 0.34, ease: "elastic.out(1, 0.5)" },
0.86,
);
tl.from("#plane", { scale: 0.48, opacity: 0, duration: 0.3, ease: "back.out(2.2)" }, 1.08);
tl.to("#route-glow", { strokeDashoffset: 0, duration: 3.1, ease: "power1.inOut" }, 1.16);
tl.to("#route-path", { strokeDashoffset: 0, duration: 3.05, ease: "power2.inOut" }, 1.2);
tl.to("#route-thread", { strokeDashoffset: 0, duration: 2.92, ease: "sine.inOut" }, 1.26);
tl.to("#plane", { offsetDistance: "100%", duration: 3.25, ease: "sine.inOut" }, 1.17);
tl.to(
"#plane-core",
{ scale: 1.06, duration: 0.2, repeat: 12, yoyo: true, ease: "sine.inOut" },
1.22,
);
tl.to("#map-plane", { scale: 1.025, duration: 3.2, ease: "sine.inOut" }, 1.28);
gsap.utils.toArray(".route-tick").forEach((tick, index) => {
tl.fromTo(
tick,
{ scale: 0.2, opacity: 0 },
{ scale: 1, opacity: 1, duration: 0.16, ease: "back.out(3)" },
1.34 + index * 0.44,
);
tl.to(
tick,
{ scale: 0.6, opacity: 0.18, duration: 0.34, ease: "power2.out" },
1.48 + index * 0.44,
);
});
tl.from(
"#paris-pulse",
{ scale: 0.52, opacity: 0, duration: 0.26, ease: "back.out(2.4)" },
4.06,
);
tl.to("#doodle-a", { strokeDashoffset: 0, duration: 0.54, ease: "power2.out" }, 4.18);
tl.to("#doodle-b", { strokeDashoffset: 0, duration: 0.36, ease: "power3.out" }, 4.36);
tl.to("#paris-pulse", { scale: 1.5, opacity: 0, duration: 0.58, ease: "power3.out" }, 4.64);
tl.to("#plane-core", { scale: 1.23, duration: 0.16, ease: "back.out(3)" }, 4.96);
tl.to("#plane-core", { scale: 0.9, duration: 0.12, ease: "power2.in" }, 5.12);
tl.to("#plane-core", { scale: 1, duration: 0.22, ease: "elastic.out(1, 0.45)" }, 5.24);
tl.to(
"#landing-badge",
{ scale: 1, opacity: 1, duration: 0.28, ease: "back.out(2.4)" },
5.02,
);
tl.to(
"#landing-badge",
{ scale: 1.06, duration: 0.16, yoyo: true, repeat: 1, ease: "sine.inOut" },
5.3,
);
tl.set("#white-screen", { opacity: 1 }, 5.5);
window.__timelines["nyc-paris-flight"] = tl;
</script>
</body>
</html>
data-width="1920"
data-height="1080"
></div>
```
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="nyc-paris-flight" data-composition-src="compositions/nyc-paris-flight.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
@@ -3,13 +3,13 @@ title: "Organic Light Leak Overlay"
description: "Finite CSS light-leak overlay for memory beats and motivated transitions"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe src="/public/catalog/preview/blocks/organic-light-leak-overlay.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Organic Light Leak Overlay preview" />
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/organic-light-leak-overlay.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/organic-light-leak-overlay.png" autoPlay muted loop playsInline />
## Install
<InstallCommand command="npx hyperframes add organic-light-leak-overlay" />
```bash Terminal
npx hyperframes add organic-light-leak-overlay
```
That writes one file: `compositions/organic-light-leak-overlay.html`.
@@ -32,140 +32,6 @@ It runs for 4 seconds at 1920×1080. Paste this into your composition:
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Source
<Accordion title={`organic-light-leak-overlay.html`}>
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: transparent;
}
#hf-light-leak {
position: absolute;
inset: -16%;
opacity: 0;
overflow: hidden;
pointer-events: none;
mix-blend-mode: screen;
}
#hf-light-leak .hf-leak-layer {
position: absolute;
inset: -20%;
will-change: opacity, transform;
}
#hf-light-leak .hf-leak-amber {
background:
radial-gradient(ellipse 52% 110% at 10% 58%, rgb(255 241 197 / 92%), transparent 45%),
radial-gradient(ellipse 72% 120% at 24% 54%, rgb(255 126 57 / 78%), transparent 66%);
filter: blur(2.8vmin);
}
#hf-light-leak .hf-leak-red {
background: radial-gradient(
ellipse 58% 105% at 78% 34%,
rgb(255 79 61 / 72%),
rgb(255 44 92 / 28%) 48%,
transparent 76%
);
filter: blur(4vmin);
}
#hf-light-leak .hf-leak-gold {
background: linear-gradient(
104deg,
transparent 24%,
rgb(255 144 62 / 30%) 40%,
rgb(255 247 205 / 72%) 51%,
rgb(255 124 48 / 22%) 62%,
transparent 78%
);
filter: blur(2vmin);
}
</style>
</head>
<body>
<div
id="organic-light-leak-overlay"
data-composition-id="organic-light-leak-overlay"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<div id="hf-light-leak" aria-hidden="true">
<div class="hf-leak-layer hf-leak-amber"></div>
<div class="hf-leak-layer hf-leak-red"></div>
<div class="hf-leak-layer hf-leak-gold"></div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var leak = document.getElementById("hf-light-leak");
var host = leak && leak.closest("[data-composition-id]");
if (!host) throw new Error("Organic Light Leak could not find its composition host");
var compositionId = host.getAttribute("data-composition-id");
var duration = Number(host.getAttribute("data-duration")) || 4;
var tl = gsap.timeline({ paused: true });
tl.to({}, { duration: duration, ease: "none" }, 0);
tl.fromTo(
leak,
{ opacity: 0 },
{ opacity: 0.9, duration: duration * 0.24, ease: "sine.out" },
duration * 0.08,
);
tl.to(leak, { opacity: 0.46, duration: duration * 0.2, ease: "sine.inOut" }, duration * 0.42);
tl.to(leak, { opacity: 0, duration: duration * 0.26, ease: "sine.in" }, duration * 0.68);
tl.fromTo(
".hf-leak-amber",
{ xPercent: -20, scale: 0.92, rotation: -4 },
{ xPercent: 18, scale: 1.1, rotation: 2, duration: duration, ease: "sine.inOut" },
0,
);
tl.fromTo(
".hf-leak-red",
{ xPercent: 20, yPercent: -8, scale: 1.08 },
{ xPercent: -16, yPercent: 8, scale: 0.94, duration: duration, ease: "sine.inOut" },
0,
);
tl.fromTo(
".hf-leak-gold",
{ xPercent: -28, rotation: -6 },
{ xPercent: 24, rotation: 4, duration: duration, ease: "sine.inOut" },
0,
);
window.__timelines[compositionId] = tl;
</script>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `light-leak` `film` `memory` `transition` `overlay` `media-treatment-overlay`.
-574
View File
@@ -1,574 +0,0 @@
---
title: "Oscilloscope Trace"
description: "CRT oscilloscope beam sweeping a waveform across a 10x8 graticule, with closed-form phosphor persistence: the tail decays as exp(-age/tau) and brightens where the beam slows"
---
import { InstallCommand } from "/snippets/install-command.jsx";
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
<VariablesExplorer
previewSrc="/public/catalog/preview/blocks/oscilloscope-trace.html"
compositionId="oscilloscope-trace"
compositionSrc="compositions/oscilloscope-trace.html"
variables={[{"id":"waveform","type":"enum","label":"Waveform","default":"sine","options":[{"value":"sine","label":"Sine"},{"value":"square","label":"Square"},{"value":"triangle","label":"Triangle"},{"value":"data","label":"Data series"}]},{"id":"frequency","type":"number","label":"Signal frequency","unit":"Hz","default":12,"min":0.1,"max":200,"step":0.1},{"id":"amplitude","type":"number","label":"Amplitude","unit":"div","default":3,"min":0.1,"max":4,"step":0.1},{"id":"persistenceMs","type":"number","label":"Phosphor persistence (tau)","unit":"ms","default":100,"min":1,"max":1000,"step":1},{"id":"phosphorColor","type":"color","label":"Phosphor colour","default":"#5dff8f"},{"id":"sweepRate","type":"number","label":"Sweep rate","unit":"sweeps/s","default":5,"min":0.25,"max":60,"step":0.25},{"id":"dataSeries","type":"string","label":"Data series (waveform=data)","default":"","placeholder":"0,0.4,0.9,0.3,-0.6,-1,-0.2,0.5"}]}
>
```html oscilloscope-trace.html
<!doctype html>
<html
lang="en"
data-composition-variables='[
{"id":"waveform","type":"enum","label":"Waveform","default":"sine","options":[{"value":"sine","label":"Sine"},{"value":"square","label":"Square"},{"value":"triangle","label":"Triangle"},{"value":"data","label":"Data series"}]},
{"id":"frequency","type":"number","label":"Signal frequency","unit":"Hz","default":12,"min":0.1,"max":200,"step":0.1},
{"id":"amplitude","type":"number","label":"Amplitude","unit":"div","default":3,"min":0.1,"max":4,"step":0.1},
{"id":"persistenceMs","type":"number","label":"Phosphor persistence (tau)","unit":"ms","default":100,"min":1,"max":1000,"step":1},
{"id":"phosphorColor","type":"color","label":"Phosphor colour","default":"#5dff8f"},
{"id":"sweepRate","type":"number","label":"Sweep rate","unit":"sweeps/s","default":5,"min":0.25,"max":60,"step":0.25},
{"id":"dataSeries","type":"string","label":"Data series (waveform=data)","default":"","placeholder":"0,0.4,0.9,0.3,-0.6,-1,-0.2,0.5"}
]'
>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Oscilloscope Trace</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #05070a;
}
#os-root {
position: relative;
width: 1920px;
height: 1080px;
font-family: "JetBrains Mono", ui-monospace, monospace;
}
#os-bg {
position: absolute;
inset: 0;
background: radial-gradient(120% 120% at 30% 25%, #0d141b 0%, #05070a 68%);
}
#os-screen {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
display: block;
}
/* Purely decorative CRT falloff — no timing, no motion. */
#os-vignette {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(
78% 78% at 34% 50%,
rgba(0, 0, 0, 0) 55%,
rgba(0, 0, 0, 0.55) 100%
);
}
#os-readout {
position: absolute;
left: 1250px;
top: 150px;
width: 520px;
color: #6f8794;
font-size: 26px;
line-height: 1.15;
letter-spacing: 0.04em;
}
#os-readout .os-title {
color: #b9ccd6;
font-size: 34px;
letter-spacing: 0.22em;
padding-bottom: 26px;
border-bottom: 2px solid #1d2a33;
margin-bottom: 26px;
}
#os-readout .os-row {
display: flex;
justify-content: space-between;
padding: 13px 0;
}
#os-readout .os-val {
color: #d7e6ee;
}
#os-readout .os-note {
margin-top: 28px;
font-size: 20px;
color: #6a7b85;
line-height: 1.5;
}
</style>
</head>
<body>
<div
id="os-root"
data-composition-id="oscilloscope-trace"
data-start="0"
data-duration="6"
data-width="1920"
data-height="1080"
>
<div id="os-bg"></div>
<canvas
id="os-screen"
class="clip"
width="1920"
height="1080"
data-start="0"
data-duration="6"
data-track-index="0"
></canvas>
<div id="os-vignette"></div>
<div id="os-readout" class="clip" data-start="0" data-duration="6" data-track-index="1">
<div class="os-title">OSCILLOSCOPE</div>
<div class="os-row"><span>SOURCE</span><span class="os-val" id="os-r-wave">SINE</span></div>
<div class="os-row"><span>FREQ</span><span class="os-val" id="os-r-freq">-</span></div>
<div class="os-row"><span>TIMEBASE</span><span class="os-val" id="os-r-time">-</span></div>
<div class="os-row"><span>VERT</span><span class="os-val" id="os-r-amp">-</span></div>
<div class="os-row"><span>PHOSPHOR</span><span class="os-val" id="os-r-phos">-</span></div>
<div class="os-note" id="os-r-note"></div>
</div>
</div>
<script>
(function () {
"use strict";
// ---------------------------------------------------------------
// Oscilloscope trace with phosphor persistence.
//
// Two pieces of physical truth drive the look:
//
// 1. PERSISTENCE. The phosphor keeps emitting after the beam has
// passed, decaying as exp(-age/tau). Decay constants by EIA
// phosphor class (research entry F#10):
// P11/P31 - 0.01-1 ms (what a real bench scope uses: no
// visible afterglow at video rates)
// P1 - 80-150 ms (the "vintage glow" look; tau ~= 100 ms
// is the entry's recommended default)
// P33 - > 1 s
// P2/P7 - 30 s .. ~1 min (radar territory, out of range here)
// The tail window is 3*tau: after three time constants ~5% of the
// brightness remains, which the entry gives as the safe cutoff.
//
// 2. BEAM-VELOCITY BRIGHTNESS. A trace is brighter where the beam
// moves slower, because the same deposited energy is spread over
// a shorter path. Each sub-step deposits a fixed amount of energy
// (constant dt), so surface brightness goes as 1/segment-length.
// Flat parts of the waveform are bright; fast vertical edges are
// faint. Qualitative law only - the research entry states it with
// no proportionality constant, so the reference length below is
// the physical minimum (pure horizontal sweep motion), not a
// fudge factor.
//
// SWEEP RATE PROVENANCE. The phosphor decay constants above are
// measured; the sweep rate is NOT - the research entry gives no
// timebase figure. The default of 5 sweeps/s is authored: across the
// 10-division graticule it works out to 20 ms/div, a real value from
// the standard 1-2-5 timebase sequence, picked because it puts a few
// cycles of the default signal on screen. Treat it as a dial, not as
// a measurement.
//
// NO FEEDBACK BUFFER. The canvas is cleared every frame and the tail
// is recomputed by evaluating the beam curve backwards in time from
// the current frame: P(t - k*dt) for k = 0..K. Frame N depends only
// on N, so seeking anywhere is exact rather than approximate.
// ---------------------------------------------------------------
var COMP_ID = "oscilloscope-trace";
var DURATION = 6;
// Screen geometry: a 10x8 division graticule of SQUARE divisions,
// the standard CRT scope face.
var DIV = 100;
var DIVS_X = 10;
var DIVS_Y = 8;
var SW = DIV * DIVS_X;
var SH = DIV * DIVS_Y;
var SX = 140;
var SY = 140;
var CY = SY + SH / 2;
// Tail window in time constants (research entry F#10: 3*tau leaves
// ~5% weight, stated there as the safe cutoff).
var TAIL_TAUS = 3;
// Sub-step count. The entry suggests K ~= 24 steps across the window,
// which is enough to quantise the DECAY but far too coarse spatially:
// at 12 Hz the beam would advance 0.15 of a cycle per step and the
// trace would render as a polygon. So K is derived from how fast the
// beam actually moves - keep each sub-step under SEG_TARGET_PX of
// travel. That target and the clamps are MY numbers, not measured
// ones; the clamp bounds per-frame cost when persistence is long.
var SEG_TARGET_PX = 3;
var K_MIN = 120;
var K_MAX = 8000;
// Band-limit for the square wave. A real generator + a real scope
// front end both have finite bandwidth, so the edge is steep but not
// instantaneous; a mathematical step would make the vertical edge one
// sub-step long and the velocity law would erase it entirely.
var SQUARE_SHARPNESS = 8;
function readVariables() {
var api = window.__hyperframes && window.__hyperframes.getVariables;
if (typeof api === "function") return api() || {};
// Standalone fallback (raw file opened without the runtime): read
// the same declaration the runtime reads, so defaults have exactly
// one home.
var out = {};
try {
var raw = document.documentElement.getAttribute("data-composition-variables");
var decls = JSON.parse(raw || "[]");
for (var i = 0; i < decls.length; i++) out[decls[i].id] = decls[i].default;
} catch (err) {
/* declaration missing or malformed - fall through to hard defaults */
}
return out;
}
function num(value, fallback, min, max) {
var n = typeof value === "number" ? value : parseFloat(value);
if (!isFinite(n)) n = fallback;
return Math.min(max, Math.max(min, n));
}
var V = readVariables();
var waveform =
["sine", "square", "triangle", "data"].indexOf(String(V.waveform)) >= 0
? String(V.waveform)
: "sine";
var frequency = num(V.frequency, 12, 0.1, 200);
var amplitude = num(V.amplitude, 3, 0.1, 4);
var tau = num(V.persistenceMs, 100, 1, 1000) / 1000;
var sweepRate = num(V.sweepRate, 5, 0.25, 60);
var phosphor = parseColor(V.phosphorColor, [93, 255, 143]);
// waveform="data" replays a supplied series instead of a synthetic
// shape: comma/whitespace separated numbers, clamped to -1..1, linearly
// interpolated, one full pass of the series per `frequency` cycle.
// Empty or unparseable -> falls back to sine.
var series = String(V.dataSeries == null ? "" : V.dataSeries)
.split(/[\s,]+/)
.map(parseFloat)
.filter(function (n) {
return isFinite(n);
})
.map(function (n) {
return Math.min(1, Math.max(-1, n));
});
if (waveform === "data" && series.length < 2) waveform = "sine";
function parseColor(value, fallback) {
var m = /^#?([0-9a-f]{6})$/i.exec(String(value == null ? "" : value).trim());
if (!m) return fallback;
var v = parseInt(m[1], 16);
return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
}
function rgba(c, a) {
return "rgba(" + c[0] + "," + c[1] + "," + c[2] + "," + a.toFixed(4) + ")";
}
// --- the signal: pure function of time, no state ------------------
function wave(t) {
var p = frequency * t; // cycles elapsed
if (waveform === "square") {
return Math.tanh(SQUARE_SHARPNESS * Math.sin(2 * Math.PI * p));
}
if (waveform === "triangle") {
return (2 / Math.PI) * Math.asin(Math.sin(2 * Math.PI * p));
}
if (waveform === "data") {
var frac = p - Math.floor(p);
var pos = frac * (series.length - 1);
var i = Math.floor(pos);
var f = pos - i;
var a = series[i];
var b = series[Math.min(series.length - 1, i + 1)];
return a + (b - a) * f;
}
return Math.sin(2 * Math.PI * p);
}
/** Horizontal sweep position, 0..1 across the graticule. */
function sweepU(t) {
var s = t * sweepRate;
return s - Math.floor(s);
}
function beamX(t) {
return SX + sweepU(t) * SW;
}
function beamY(t) {
return CY - wave(t) * amplitude * DIV;
}
// Upper bound on beam speed, in px/s, used to pick the sub-step count.
// Horizontal is the constant sweep; vertical is the waveform's steepest
// slope, which differs per shape - a band-limited square is
// SQUARE_SHARPNESS times steeper at its edge than a sine of the same
// frequency, and under-sampling exactly there is what turns the edge
// into a polygon.
var slopeBound;
if (waveform === "square") {
slopeBound = SQUARE_SHARPNESS * 2 * Math.PI * frequency;
} else if (waveform === "triangle") {
slopeBound = 4 * frequency;
} else if (waveform === "data") {
var maxStep = 0;
for (var si = 1; si < series.length; si++) {
maxStep = Math.max(maxStep, Math.abs(series[si] - series[si - 1]));
}
slopeBound = maxStep * (series.length - 1) * frequency;
} else {
slopeBound = 2 * Math.PI * frequency;
}
var beamSpeedMax = Math.sqrt(
Math.pow(SW * sweepRate, 2) + Math.pow(slopeBound * amplitude * DIV, 2),
);
// --- graticule (drawn once to an offscreen canvas) ----------------
var grat = document.createElement("canvas");
grat.width = 1920;
grat.height = 1080;
(function drawGraticule() {
var g = grat.getContext("2d");
g.fillStyle = "#04080a";
g.fillRect(SX, SY, SW, SH);
g.strokeStyle = "rgba(120,180,160,0.16)";
g.lineWidth = 1;
for (var i = 1; i < DIVS_X; i++) {
g.beginPath();
g.moveTo(SX + i * DIV + 0.5, SY);
g.lineTo(SX + i * DIV + 0.5, SY + SH);
g.stroke();
}
for (var j = 1; j < DIVS_Y; j++) {
g.beginPath();
g.moveTo(SX, SY + j * DIV + 0.5);
g.lineTo(SX + SW, SY + j * DIV + 0.5);
g.stroke();
}
// Centre axes carry the fine 0.2-division ticks, as on a real face.
g.strokeStyle = "rgba(150,205,185,0.32)";
var cx = SX + SW / 2 + 0.5;
var cy = CY + 0.5;
g.beginPath();
g.moveTo(cx, SY);
g.lineTo(cx, SY + SH);
g.moveTo(SX, cy);
g.lineTo(SX + SW, cy);
g.stroke();
g.strokeStyle = "rgba(150,205,185,0.42)";
for (var k = 1; k < DIVS_X * 5; k++) {
var x = SX + (k * DIV) / 5 + 0.5;
g.beginPath();
g.moveTo(x, cy - 9);
g.lineTo(x, cy + 9);
g.stroke();
}
for (var m = 1; m < DIVS_Y * 5; m++) {
var y = SY + (m * DIV) / 5 + 0.5;
g.beginPath();
g.moveTo(cx - 9, y);
g.lineTo(cx + 9, y);
g.stroke();
}
g.strokeStyle = "rgba(160,215,195,0.55)";
g.lineWidth = 2;
g.strokeRect(SX + 1, SY + 1, SW - 2, SH - 2);
})();
// --- per-frame paint ----------------------------------------------
var canvas = document.getElementById("os-screen");
var ctx = canvas.getContext("2d");
function draw(t) {
ctx.globalCompositeOperation = "source-over";
ctx.clearRect(0, 0, 1920, 1080);
ctx.drawImage(grat, 0, 0);
var windowS = TAIL_TAUS * tau;
var K = Math.max(
K_MIN,
Math.min(K_MAX, Math.round((windowS * beamSpeedMax) / SEG_TARGET_PX)),
);
var dt = windowS / K;
// Length the beam covers in one sub-step with zero vertical motion.
// That is the slowest the beam can ever move, so it is the maximum
// brightness reference and the velocity weight never exceeds 1.
var lRef = SW * sweepRate * dt;
ctx.globalCompositeOperation = "lighter";
ctx.lineCap = "round";
// Oldest -> newest, so the bright head lands on top.
var prevX = 0;
var prevY = 0;
var prevU = 0;
var havePrev = false;
for (var k = K; k >= 0; k--) {
var tk = t - k * dt;
if (tk < 0) {
// Before frame 0 the beam had not been switched on yet.
havePrev = false;
continue;
}
var u = sweepU(tk);
var x = SX + u * SW;
var y = beamY(tk);
if (havePrev && u >= prevU) {
var dx = x - prevX;
var dy = y - prevY;
var len = Math.sqrt(dx * dx + dy * dy);
// Beam-velocity law: brightness ~ 1 / path length per unit time.
var vel = len > 1e-6 ? Math.min(1, lRef / len) : 1;
var decay = Math.exp((-k * dt) / tau);
var a = decay * vel;
if (a > 0.002) {
ctx.beginPath();
ctx.moveTo(prevX, prevY);
ctx.lineTo(x, y);
ctx.strokeStyle = rgba(phosphor, a * 0.13);
ctx.lineWidth = 13;
ctx.stroke();
ctx.strokeStyle = rgba(phosphor, a * 0.9);
ctx.lineWidth = 3;
ctx.stroke();
if (a > 0.55) {
// Overdriven phosphor saturates towards white at the head.
ctx.strokeStyle =
"rgba(255,255,255," + (((a - 0.55) / 0.45) * 0.7).toFixed(4) + ")";
ctx.lineWidth = 1.4;
ctx.stroke();
}
}
}
// Retrace is blanked on a real scope: when u wraps, drop the
// connecting segment instead of drawing a line back across.
prevX = x;
prevY = y;
prevU = u;
havePrev = true;
}
// Beam head: the spot itself, brightest point on the screen.
var hx = beamX(t);
var hy = beamY(t);
var glow = ctx.createRadialGradient(hx, hy, 0, hx, hy, 22);
glow.addColorStop(0, "rgba(255,255,255,0.95)");
glow.addColorStop(0.22, rgba(phosphor, 0.85));
glow.addColorStop(1, rgba(phosphor, 0));
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(hx, hy, 22, 0, Math.PI * 2);
ctx.fill();
ctx.globalCompositeOperation = "source-over";
}
// --- readout --------------------------------------------------------
(function fillReadout() {
// Nearest EIA phosphor class for the chosen tau (bands from the
// research entry). Labelling, not simulation.
var tauMs = tau * 1000;
var cls = tauMs <= 1 ? "P31" : tauMs <= 150 ? "P1" : "P33";
var msPerDiv = 1000 / (sweepRate * DIVS_X);
function set(id, text) {
document.getElementById(id).textContent = text;
}
set("os-r-wave", waveform.toUpperCase());
set("os-r-freq", frequency.toFixed(frequency < 10 ? 2 : 1) + " Hz");
set(
"os-r-time",
(msPerDiv >= 10 ? msPerDiv.toFixed(0) : msPerDiv.toFixed(2)) + " ms/div",
);
set("os-r-amp", amplitude.toFixed(1) + " div pk");
set("os-r-phos", cls + " tau " + tauMs.toFixed(0) + " ms");
set(
"os-r-note",
"Trace brightness falls as exp(-age/tau) behind the beam, and rises where the beam slows.",
);
})();
// --- timeline -------------------------------------------------------
//
// tl.eventCallback("onUpdate", ...) is NOT usable here: the runtime
// seeks with suppressEvents, so the callback never fires on a seek and
// the canvas would keep whatever the last played frame drew. A tweened
// property with an accessor is applied by GSAP on every render,
// including suppressed ones, so the repaint is driven from the setter.
var beam = { t: 0 };
var driver = {};
Object.defineProperty(driver, "t", {
get: function () {
return beam.t;
},
set: function (value) {
beam.t = value;
draw(value);
},
});
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
tl.to(driver, { t: DURATION, duration: DURATION, ease: "none", lazy: false }, 0);
window.__timelines[COMP_ID] = tl;
draw(0);
})();
</script>
</body>
</html>
```
</VariablesExplorer>
## Install
<InstallCommand command="npx hyperframes add oscilloscope-trace" />
That writes one file: `compositions/oscilloscope-trace.html`.
## Add it to your video
It runs for 6 seconds at 1920×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="oscilloscope-trace"
data-composition-src="compositions/oscilloscope-trace.html"
data-start="0"
data-duration="6"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
{/* hf:generated-footer */}
Tagged `instrument` `data` `showcase` `retro`.
## Related topics
- [Browse the complete Catalog](/catalog)
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
- [Build a richer composition](/go-further)
-675
View File
@@ -1,675 +0,0 @@
---
title: "Rack Focus"
description: "A focus pull with real aperture bokeh: the focal plane travels from a near subject to a far one, so one resolves as the other blows into hard-edged polygon discs that clip to a cat's eye toward the corners. A focus pull only reads against content at two clearly separated depths, so this block carries its own depth-layered night exterior and puts both subject depths on variables."
---
import { InstallCommand } from "/snippets/install-command.jsx";
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
<VariablesExplorer
previewSrc="/public/catalog/preview/blocks/rack-focus.html"
compositionId="rack-focus"
compositionSrc="compositions/rack-focus.html"
variables={[{"id":"nearfocus","type":"number","label":"Near focal distance","default":1.2,"min":0.2,"max":100,"step":0.05,"unit":"m"},{"id":"farfocus","type":"number","label":"Far focal distance","default":80,"min":0.3,"max":400,"step":0.5,"unit":"m"},{"id":"focallength","type":"number","label":"Focal length","default":85,"min":12,"max":300,"step":1,"unit":"mm"},{"id":"aperture","type":"number","label":"Aperture (f-number)","default":1.8,"min":0.95,"max":22,"step":0.05},{"id":"blades","type":"number","label":"Aperture blades (bokeh shape)","default":6,"min":3,"max":14,"step":1},{"id":"catseye","type":"number","label":"Cat eye clipping at the corners","default":0.62,"min":0,"max":1,"step":0.02},{"id":"pullstart","type":"number","label":"Pull start","default":1.2,"min":0,"max":30,"step":0.05,"unit":"s"},{"id":"pullduration","type":"number","label":"Pull duration","default":3.2,"min":0.1,"max":30,"step":0.05,"unit":"s"},{"id":"pullease","type":"string","label":"Pull easing (GSAP ease)","default":"power2.inOut","placeholder":"power2.inOut"},{"id":"bokeh","type":"number","label":"Bokeh exposure","default":1,"min":0,"max":3,"step":0.05},{"id":"backdrop","type":"color","label":"Backdrop","default":"#05060a"}]}
>
```html rack-focus.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Rack Focus</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<!--
RACK FOCUS: a focus pull with real aperture bokeh.
WHAT THIS NEEDS TO READ AT ALL
------------------------------
A focus pull is a depth effect. It only reads when the frame holds
content at two clearly separated depths: something near the lens and
something far behind it. Point it at flat, single-plane content and
nothing happens, exactly like a dolly zoom on a flat card.
This block therefore carries its own depth-layered scene, a night
exterior built from light sources spread from 1.15 m to 90 m, so it
works standalone. Every depth is a variable: `nearfocus` is where the
pull starts, `farfocus` is where it ends, and the scene's two subjects
sit at those depths. Change them and the subjects move with them.
WHY THIS IS NOT A BLUR
----------------------
A CSS/Gaussian blur softens everything uniformly. A real defocus turns
each point of light into an image of the APERTURE, scaled by its circle
of confusion, so a bright point becomes a hard-edged polygon disc whose
size grows with distance from the focal plane, and which clips to a
cat's-eye toward the frame corners. Every light in this scene is
splatted as an aperture-shaped sprite at its own circle of confusion.
CIRCLE OF CONFUSION, from three.js BokehShader2 (MIT), Martins Upitis
--------------------------------------------------------------------
Read from examples/jsm/shaders/BokehShader2.js:
float CoC = 0.03; // circle of confusion in mm
// (35mm film = 0.03mm)
float f = focalLength; // mm
float d = fDepth * 1000.0; // focal plane in mm
float o = depth * 1000.0; // object depth in mm
float a = (o * f) / (o - f);
float b = (d * f) / (d - f);
float c = (d - f) / (d * fstop * CoC);
blur = abs(a - b) * c;
`blur` comes out in units of that 0.03 mm acceptable-sharpness circle,
so the defocus DIAMETER on the sensor is `blur * 0.03` mm, which this
block converts to pixels with the sensor width. Same constants, same
formula, independently written, no code copied. This agrees with the
Zeiss thin-lens form CoC = (f²/N)·|1/S - 1/U| for S >> f.
Aperture shape is the community-standard regular-polygon boundary
d(θ) = cos(π/n) / cos(mod(θ, 2π/n) - π/n), n = blade count (real
irises ship 5, 6, 8 or 9 blades). Cat's-eye clipping is the aperture
intersected with two barrel openings offset along the radial direction,
which is the actual mechanism of mechanical vignetting.
DETERMINISM
-----------
State at frame N is computed from N. The focal distance is a closed-form
function of t, every circle of confusion follows from a static depth and
that focal distance, and the scene point cloud is built once from a
seeded PRNG. No accumulation, no clocks, no unseeded randomness.
-->
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
overflow: hidden;
}
#rf-root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
}
#rf-backdrop {
position: absolute;
inset: 0;
background: #05060a;
}
#rf-canvas {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
}
</style>
</head>
<body>
<div
id="rf-root"
data-composition-id="rack-focus"
data-root="true"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="6"
data-composition-variables='[
{"id":"nearfocus","type":"number","label":"Near focal distance","default":1.2,"min":0.2,"max":100,"step":0.05,"unit":"m"},
{"id":"farfocus","type":"number","label":"Far focal distance","default":80,"min":0.3,"max":400,"step":0.5,"unit":"m"},
{"id":"focallength","type":"number","label":"Focal length","default":85,"min":12,"max":300,"step":1,"unit":"mm"},
{"id":"aperture","type":"number","label":"Aperture (f-number)","default":1.8,"min":0.95,"max":22,"step":0.05},
{"id":"blades","type":"number","label":"Aperture blades (bokeh shape)","default":6,"min":3,"max":14,"step":1},
{"id":"catseye","type":"number","label":"Cat eye clipping at the corners","default":0.62,"min":0,"max":1,"step":0.02},
{"id":"pullstart","type":"number","label":"Pull start","default":1.2,"min":0,"max":30,"step":0.05,"unit":"s"},
{"id":"pullduration","type":"number","label":"Pull duration","default":3.2,"min":0.1,"max":30,"step":0.05,"unit":"s"},
{"id":"pullease","type":"string","label":"Pull easing (GSAP ease)","default":"power2.inOut","placeholder":"power2.inOut"},
{"id":"bokeh","type":"number","label":"Bokeh exposure","default":1,"min":0,"max":3,"step":0.05},
{"id":"backdrop","type":"color","label":"Backdrop","default":"#05060a"}
]'
>
<div id="rf-backdrop"></div>
<canvas id="rf-canvas" width="1920" height="1080"></canvas>
<!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->
<div
id="rf-drv"
class="clip"
data-start="0"
data-duration="6"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
var DUR = 6;
var W = 1920;
var H = 1080;
// 36mm-wide sensor, 16:9 active area. Pixels per millimetre is the
// only thing the projection needs, and it is the same on both axes.
var SENSOR_W_MM = 36;
var PX_PER_MM = W / SENSOR_W_MM;
// BokehShader2's acceptable-sharpness circle, in mm (35mm film).
var COC_MM = 0.03;
// Defocus is clamped so a wildly out-of-range focus setting cannot
// splat sprites the size of the frame. BokehShader2 clamps the same
// quantity with its `maxblur` uniform.
var MAX_COC_R_PX = 0.1 * H;
// Smallest sprite half-width. Below roughly one pixel a splat is an
// aliasing machine, so points in focus bottom out here.
var MIN_R_PX = 0.75;
var V = (window.__hyperframes && window.__hyperframes.getVariables()) || {};
var CS = getComputedStyle(document.getElementById("rf-root"));
// The runtime defines every declared variable as `--<slug>` on the
// root (packages/core/src/tokenSlug.ts), so a host stylesheet can
// override one there too. Read the custom property first, fall back
// to the declared value when it is unset.
function raw(id) {
var css = CS.getPropertyValue("--" + id.toLowerCase()).trim();
return css !== "" ? css : V[id];
}
function num(id, fallback) {
var n = parseFloat(raw(id));
return isFinite(n) ? n : fallback;
}
var NEAR = num("nearfocus", 1.2);
var FAR = num("farfocus", 80);
var FOCAL = num("focallength", 85);
var FSTOP = num("aperture", 1.8);
var BLADES = Math.max(3, Math.round(num("blades", 6)));
var CATSEYE = num("catseye", 0.62);
var PULL_START = num("pullstart", 1.2);
var PULL_DUR = Math.max(0.001, num("pullduration", 3.2));
var BOKEH = num("bokeh", 1);
var EASE_NAME = String(raw("pullease") || "power2.inOut");
var BACKDROP =
typeof raw("backdrop") === "string" && raw("backdrop") ? raw("backdrop") : "#05060a";
document.getElementById("rf-backdrop").style.background = BACKDROP;
var EASE = gsap.parseEase(EASE_NAME) || gsap.parseEase("power2.inOut");
// A focus ring is roughly linear in dioptres, not in metres: a rack
// from 1.2m to 80m spends its first millimetre of barrel rotation
// crossing most of the distance. Interpolating 1/distance is what
// makes the pull travel evenly instead of snapping to the far plane.
function focusAt(t) {
var u = Math.min(1, Math.max(0, (t - PULL_START) / PULL_DUR));
var e = EASE(u);
var inv = 1 / NEAR + (1 / FAR - 1 / NEAR) * e;
return 1 / inv;
}
// ── Scene ────────────────────────────────────────────────────────
// A night exterior: a string of practical lights right in front of
// the lens, a lit city block far behind it, and scattered lights
// through every depth between so the pull reads as a continuous
// travel rather than a cut between two planes.
function mulberry32(a) {
return function () {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
var rnd = mulberry32(0x5eed1a3);
function rr(lo, hi) {
return lo + (hi - lo) * rnd();
}
// x, y in metres (y up, origin on the optical axis), z in metres,
// r0 = the light's own physical radius in metres, b = peak
// brightness when perfectly in focus (values above 1 are highlights
// that clip, which is exactly why they stay visible once spread
// across a bokeh disc), rgb = colour.
var P = [];
function light(x, y, z, r0, b, c) {
P.push(x, y, z, r0, b, c[0], c[1], c[2]);
}
var TUNGSTEN = [1.0, 0.74, 0.45];
var FILAMENT = [1.0, 0.9, 0.74];
var WIRE = [0.86, 0.74, 0.6];
var WARM_WIN = [1.0, 0.79, 0.52];
var COOL_WIN = [0.6, 0.75, 1.0];
var SIGN_A = [0.35, 0.95, 1.0];
var SIGN_B = [1.0, 0.42, 0.72];
// Half the frame's width, in metres, at depth z. Both subjects are
// laid out against the reference framing (85mm, 1.2m / 80m) and then
// scaled by this, so retuning the lens or either focal distance moves
// the subjects with the frame instead of pushing them out of it.
function halfW(z) {
return (0.5 * SENSOR_W_MM * z) / FOCAL;
}
// Near subject: a catenary string of bulbs at `nearfocus`. The wire is
// what makes "sharp" unmistakable, a one-pixel line either resolves
// or it does not, and the filament inside each bulb is the second cue.
var NEAR_Z = NEAR;
var NS = halfW(NEAR_Z) / 0.25412;
var X_END = 0.4;
var SAG_A = 0.3;
var SAG = 0.1;
var COSH_END = Math.cosh(X_END / SAG_A);
function stringY(x) {
var s = (COSH_END - Math.cosh(x / SAG_A)) / (COSH_END - 1);
return 0.075 - SAG * s - 0.035 * (x / X_END);
}
function stringZ(x) {
return NEAR_Z + 0.05 * (x / X_END);
}
for (var i = 0; i < 1100; i++) {
var wx = -0.42 + (0.84 * i) / 1099;
light(wx * NS, stringY(wx) * NS, stringZ(wx), 0.0006 * NS, 1.4, WIRE);
}
for (var k = -5; k <= 5; k++) {
var bx = k * 0.085;
var by = stringY(bx) - 0.011;
var bz = stringZ(bx);
light(bx * NS, by * NS, bz, 0.006 * NS, 26, TUNGSTEN);
light((bx - 0.0022) * NS, by * NS, bz, 0.0006 * NS, 7, FILAMENT);
light(bx * NS, (by - 0.0022) * NS, bz, 0.0006 * NS, 7, FILAMENT);
light((bx + 0.0022) * NS, by * NS, bz, 0.0006 * NS, 7, FILAMENT);
}
// Far subject: three lit towers plus a dense LED sign strip, sitting
// around `farfocus`. The sign's pitch is fine enough that it only
// resolves into separate lamps when focus actually arrives.
var FAR_Z = FAR;
var FS = halfW(FAR_Z) / 16.941;
function tower(cx, hw, topY, botY, cols, rows, z, lit) {
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
if (rnd() > lit) continue;
var x = cx - hw + (2 * hw * (c + 0.5)) / cols;
var y = botY + ((topY - botY) * (r + 0.5)) / rows;
var cool = rnd() < 0.28;
light(x, y, z + rr(-0.4, 0.4) * FS, 0.24 * FS, rr(9, 20), cool ? COOL_WIN : WARM_WIN);
}
}
}
tower(-9.5 * FS, 3.5 * FS, 9.6 * FS, -4.0 * FS, 6, 14, FAR_Z * 1.03, 0.34);
tower(2.0 * FS, 4.5 * FS, 6.4 * FS, -4.0 * FS, 8, 12, FAR_Z * 0.98, 0.3);
tower(12.5 * FS, 3.0 * FS, 11.0 * FS, -4.0 * FS, 5, 15, FAR_Z * 1.08, 0.36);
for (var s = 0; s < 40; s++) {
var sx = (-5.5 + (11 * s) / 39) * FS;
var mixc = s / 39;
var sc = [
SIGN_A[0] + (SIGN_B[0] - SIGN_A[0]) * mixc,
SIGN_A[1] + (SIGN_B[1] - SIGN_A[1]) * mixc,
SIGN_A[2] + (SIGN_B[2] - SIGN_A[2]) * mixc,
];
light(sx, -5.0 * FS, FAR_Z * 0.99, 0.1 * FS, 7, sc);
light(sx, -5.55 * FS, FAR_Z * 0.99, 0.1 * FS, 7, sc);
}
// Everything between. Depth is drawn log-uniform between the two
// subjects and the screen position is uniform, so the mid-ground
// stays evenly spread whatever the two focal distances are.
var Z_LO = Math.min(NEAR_Z, FAR_Z) * 1.9;
var Z_HI = Math.max(NEAR_Z, FAR_Z) * 0.85;
for (var m = 0; m < 90; m++) {
var z = Z_LO * Math.pow(Z_HI / Z_LO, rnd());
var halfWm = (0.5 * SENSOR_W_MM * z) / FOCAL;
var halfHm = (halfWm * H) / W;
var warm = rnd() < 0.66;
light(
rr(-1.05, 1.05) * halfWm,
rr(-1.0, 0.75) * halfHm,
z,
rr(0.006, 0.05) * (z / 12),
rr(5, 18),
warm ? WARM_WIN : COOL_WIN,
);
}
// ── GL ───────────────────────────────────────────────────────────
var canvas = document.getElementById("rf-canvas");
var gl =
canvas.getContext("webgl", {
alpha: true,
antialias: false,
depth: false,
stencil: false,
preserveDrawingBuffer: true,
powerPreference: "high-performance",
}) ||
canvas.getContext("experimental-webgl", {
alpha: true,
preserveDrawingBuffer: true,
});
var VERT = [
"precision highp float;",
"attribute vec2 aCorner;", // -1..1 quad corner
"attribute vec3 aPos;", // metres, y up, z away from the lens
"attribute vec2 aSize;", // x = own radius (m), y = in-focus peak
"attribute vec3 aColor;",
"uniform vec2 uRes;",
"uniform float uPxPerMm;",
"uniform float uFocal;", // mm
"uniform float uFocus;", // metres
"uniform float uFstop;",
"uniform float uCoCmm;",
"uniform float uMaxR;",
"uniform float uMinR;",
"uniform float uCatsEye;",
"uniform float uBokeh;",
"varying vec2 vQ;",
"varying vec3 vColor;",
"varying float vGain;",
"varying float vShape;",
"varying float vAA;",
"varying vec2 vRadial;",
"varying float vCat;",
"void main() {",
" float z = max(aPos.z, 0.001);",
" float ppm = uPxPerMm * uFocal / z;", // pixels per metre at this depth
" vec2 centre = uRes * 0.5 + aPos.xy * ppm;",
" float r0 = max(aSize.x * ppm, uMinR);",
"",
" // BokehShader2's circle of confusion, in units of uCoCmm.",
" float f = uFocal;",
" float d = uFocus * 1000.0;",
" float o = z * 1000.0;",
" float a = (o * f) / max(o - f, 1e-4);",
" float b = (d * f) / max(d - f, 1e-4);",
" float c = (d - f) / max(d * uFstop * uCoCmm, 1e-6);",
" float blur = abs(a - b) * c;",
" // -> defocus diameter in mm -> pixels -> radius.",
" float cocR = min(blur * uCoCmm * uPxPerMm * 0.5, uMaxR);",
"",
" // A finite source convolved with the defocus disc: radii add in",
" // quadrature. Flux is conserved, so peak brightness falls as the",
" // inverse square of the radius. That single term is why an",
" // in-focus lamp clips to white and a defocused one is a readable",
" // disc instead of a smear.",
" float R = sqrt(r0 * r0 + cocR * cocR);",
" vGain = aSize.y * uBokeh * (r0 * r0) / (R * R);",
" // Near focus the sprite is the lamp (round); far from it the",
" // sprite is an image of the aperture (polygonal).",
" vShape = (cocR * cocR) / (cocR * cocR + r0 * r0);",
" vAA = 1.0 / max(R, 0.5);",
" vColor = aColor;",
" vQ = aCorner;",
"",
" // Mechanical vignetting: the barrel openings clip the aperture",
" // harder the further the sprite sits from the optical axis.",
" vec2 off = centre - uRes * 0.5;",
" float rad = length(off);",
" vRadial = rad > 1.0 ? off / rad : vec2(1.0, 0.0);",
" vCat = uCatsEye * min(1.0, rad / (length(uRes) * 0.5));",
"",
" // A light spread thin enough to land under half a display code",
" // value contributes nothing but fill rate. Culling it here is what",
" // keeps a 900-point wire from splatting 900 invisible discs the",
" // moment it goes out of focus.",
" if (vGain < 0.0015) {",
" gl_Position = vec4(2.0, 2.0, 2.0, 1.0);",
" return;",
" }",
"",
" vec2 p = centre + aCorner * R;",
" gl_Position = vec4((p / uRes) * 2.0 - 1.0, 0.0, 1.0);",
"}",
].join("\n");
var FRAG = [
"precision highp float;",
"varying vec2 vQ;",
"varying vec3 vColor;",
"varying float vGain;",
"varying float vShape;",
"varying float vAA;",
"varying vec2 vRadial;",
"varying float vCat;",
"uniform float uBlades;",
"const float PI = 3.14159265;",
"void main() {",
" float r = length(vQ);",
" if (r > 1.0) discard;",
" float th = r > 1e-5 ? atan(vQ.y, vQ.x) : 0.0;",
"",
" // Regular-polygon aperture boundary: circumradius 1 at a blade",
" // vertex, cos(PI/n) at a blade midpoint.",
" float n = uBlades;",
" float seg = 2.0 * PI / n;",
" float poly = cos(PI / n) / cos(mod(th, seg) - PI / n);",
" float bound = mix(1.0, poly, vShape);",
"",
" float cov = smoothstep(bound, bound - vAA, r);",
" // Two offset barrel openings cut the disc from opposite sides,",
" // which is what turns a corner bokeh into a cat's eye.",
" float cut = vCat * vShape;",
" cov *= smoothstep(1.0, 1.0 - vAA, length(vQ - vRadial * cut));",
" cov *= smoothstep(1.0, 1.0 - vAA, length(vQ + vRadial * cut));",
"",
" gl_FragColor = vec4(vColor * (vGain * cov), 1.0);",
"}",
].join("\n");
var uni = {};
var ready = false;
var count = 0;
function compile(type, src) {
var sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
throw new Error("rack-focus shader: " + gl.getShaderInfoLog(sh));
}
return sh;
}
function hexToRgb(hex) {
var h = String(hex).trim().replace("#", "");
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
var v = parseInt(h, 16);
if (!isFinite(v)) return [0.02, 0.024, 0.039];
return [((v >> 16) & 255) / 255, ((v >> 8) & 255) / 255, (v & 255) / 255];
}
var BG = hexToRgb(BACKDROP);
if (gl) {
var prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
throw new Error("rack-focus link: " + gl.getProgramInfoLog(prog));
}
gl.useProgram(prog);
// Six vertices per light: two triangles carrying the same point
// payload and four distinct corner offsets.
var CORNERS = [
[-1, -1],
[1, -1],
[1, 1],
[-1, -1],
[1, 1],
[-1, 1],
];
var n = P.length / 8;
count = n * 6;
var STRIDE = 10;
var data = new Float32Array(count * STRIDE);
var w = 0;
for (var pi = 0; pi < n; pi++) {
var o = pi * 8;
for (var ci = 0; ci < 6; ci++) {
data[w++] = CORNERS[ci][0];
data[w++] = CORNERS[ci][1];
data[w++] = P[o];
data[w++] = P[o + 1];
data[w++] = P[o + 2];
data[w++] = P[o + 3];
data[w++] = P[o + 4];
data[w++] = P[o + 5];
data[w++] = P[o + 6];
data[w++] = P[o + 7];
}
}
var buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
var BYTES = STRIDE * 4;
[
["aCorner", 2, 0],
["aPos", 3, 8],
["aSize", 2, 20],
["aColor", 3, 28],
].forEach(function (spec) {
var loc = gl.getAttribLocation(prog, spec[0]);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, spec[1], gl.FLOAT, false, BYTES, spec[2]);
});
[
"uRes",
"uPxPerMm",
"uFocal",
"uFocus",
"uFstop",
"uCoCmm",
"uMaxR",
"uMinR",
"uCatsEye",
"uBlades",
"uBokeh",
].forEach(function (nm) {
uni[nm] = gl.getUniformLocation(prog, nm);
});
gl.viewport(0, 0, W, H);
gl.uniform2f(uni.uRes, W, H);
gl.uniform1f(uni.uPxPerMm, PX_PER_MM);
gl.uniform1f(uni.uFocal, FOCAL);
gl.uniform1f(uni.uFstop, FSTOP);
gl.uniform1f(uni.uCoCmm, COC_MM);
gl.uniform1f(uni.uMaxR, MAX_COC_R_PX);
gl.uniform1f(uni.uMinR, MIN_R_PX);
gl.uniform1f(uni.uCatsEye, CATSEYE);
gl.uniform1f(uni.uBlades, BLADES);
gl.uniform1f(uni.uBokeh, BOKEH);
// Light adds to light. Overlapping bokeh discs are brighter where
// they cross, which is the whole texture of a bokeh field.
gl.disable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
ready = true;
}
// Every frame is computed from t alone: the focal distance is a
// closed-form function of t, and each sprite's circle of confusion
// falls out of its own static depth and that distance.
function draw(t) {
if (!ready) return;
gl.uniform1f(uni.uFocus, focusAt(t));
gl.clearColor(BG[0], BG[1], BG[2], 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, count);
gl.flush();
}
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// The canvas is repainted from a property SETTER, not from onUpdate:
// gsap's seek(t) suppresses events by default, so an onUpdate callback
// silently never fires on a scrub and the canvas freezes on frame 0.
// Tweened values are always written during render, suppressed or not,
// so this fires on every seek, and hands us the frame time directly.
var driver = { _t: 0 };
Object.defineProperty(driver, "t", {
get: function () {
return this._t;
},
set: function (v) {
this._t = v;
draw(v);
},
});
tl.to(driver, { t: DUR, duration: DUR, ease: "none" }, 0);
window.__timelines["rack-focus"] = tl;
draw(0);
})();
</script>
</body>
</html>
```
</VariablesExplorer>
## Install
<InstallCommand command="npx hyperframes add rack-focus" />
That writes one file: `compositions/rack-focus.html`.
## Add it to your video
It runs for 6 seconds at 1920×1080. Paste this into your composition:
```html index.html
<div
data-composition-id="rack-focus"
data-composition-src="compositions/rack-focus.html"
data-start="0"
data-duration="6"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
```
Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
## Change how it looks
Set these CSS variables on the block:
- `--nearfocus` — Near focal distance in metres (where the pull starts and the near subject sits). Defaults to `1.2`.
- `--farfocus` — Far focal distance in metres (where the pull ends and the far subject sits). Defaults to `80`.
- `--focallength` — Focal length in mm (longer lens = shallower depth of field). Defaults to `85`.
- `--aperture` — Aperture f-number (lower = bigger bokeh). Defaults to `1.8`.
- `--blades` — Aperture blade count, the bokeh polygon's sides. Defaults to `6`.
- `--catseye` — Cat's-eye clipping at the frame corners (mechanical vignetting). Defaults to `0.62`.
- `--pullstart` — Seconds held on the near subject before the pull starts. Defaults to `1.2`.
- `--pullduration` — Pull duration in seconds. Defaults to `3.2`.
- `--pullease` — Pull easing (any GSAP ease name). Defaults to `power2.inOut`.
- `--bokeh` — Bokeh exposure. Defaults to `1`.
- `--backdrop` — Backdrop. Defaults to `#05060a`.
{/* hf:generated-footer */}
Tagged `webgl` `shader` `camera` `depth` `cinematic` `showcase`.
## Related topics
- [Browse the complete Catalog](/catalog)
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
- [Build a richer composition](/go-further)

Some files were not shown because too many files have changed in this diff Show More