feat(registry): bring back the video-primitive moves (#3169)

Restores the 208 catalog items reverted after their previews 404'd in
production, this time on the payload mechanism rather than the .html files
that caused the outage.

The generator no longer writes a preview document to docs/public. That writer,
and the machinery under it, existed only to produce files the docs host
discards, so it is gone rather than bypassed. Items now embed the composition
itself via a payload, which is what the previous change already does for the
items that were already in the catalog.

The variables explorer is parked, not restored: it drove its preview through
the same unpublished .html path, so it would have shown an empty frame. Items
that declare variables get the live player plus the static variables table, and
reconnecting the explorer to payloads is a follow-up.
This commit is contained in:
Miguel Ángel
2026-08-10 18:46:03 -04:00
committed by GitHub
parent 0f76305191
commit 9734578e60
1383 changed files with 255573 additions and 5760 deletions
+6 -2
View File
@@ -70,9 +70,13 @@ jobs:
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
# Find which blocks/components changed in this PR
# 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.
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)
+4 -1
View File
@@ -293,10 +293,13 @@ 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,6 +11,7 @@
"node_modules/",
"playground/",
"packages/producer/tests/**/src/vendor/**",
"docs/public/catalog/",
"registry/blocks/**/lib/*.iife.js",
"chrome/"
],
@@ -34,6 +35,9 @@
},
{
"files": ["docs/snippets/**/*.jsx", "docs/snippets/**/*.tsx"],
"globals": {
"CodeBlock": "readonly"
},
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
+958 -13
View File
@@ -3,6 +3,8 @@ title: "App Showcase"
description: "Fitness app product showcase with three floating smartphone screens"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="app-showcase preview"
@@ -12,30 +14,973 @@ description: "Fitness app product showcase with three floating smartphone screen
## Install
```bash Terminal
npx hyperframes add app-showcase
```
<InstallCommand command="npx hyperframes add app-showcase" />
That writes one file: `compositions/app-showcase.html`.
## Add it to your video
## Source
It runs for 5.5 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`app-showcase.html`}>
```html index.html
```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>
<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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+329 -11
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="apple-money-count preview"
@@ -12,30 +14,346 @@ description: "Apple-style finance counter that counts from $0 to $10,000, flashe
## Install
```bash Terminal
npx hyperframes add apple-money-count
```
<InstallCommand command="npx hyperframes add apple-money-count" />
That writes `compositions/apple-money-count.html`, plus 1 supporting file under `assets/`.
## Add it to your video
## Source
It runs for 5 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`apple-money-count.html`}>
```html index.html
```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>
<div
id="root"
data-composition-id="apple-money-count"
data-composition-src="compositions/apple-money-count.html"
data-root="true"
data-start="0"
data-duration="5"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
+699
View File
@@ -0,0 +1,699 @@
---
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";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="bar-chart-race preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/bar-chart-race.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/bar-chart-race.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## 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`.
## Variables
Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:
| Variable | Default | Accepts | What it does |
| --- | --- | --- | --- |
| `title` | `Streaming Subscribers by Service` | string | |
| `subtitle` | `Ranked by reported subscribers` | string | |
| `periods` | `2019, 2020, 2021, 2022, 2023, 2024` | string | |
| `series` | `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` | string | |
| `barCount` | `6` | 3 to 12, step 1 | |
| `periodDuration` | `2` | 0.4 to 6, step 0.1 | |
| `valuePrefix` | `$` | string | |
| `valueSuffix` | `M` | string | |
| `valueDecimals` | `0` | 0 to 3, step 1 | |
| `accent` | `#c8452d` | color | |
Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:
```html wrap
<div
data-composition-id="bar-chart-race"
data-composition-src="compositions/bar-chart-race.html"
data-variable-values='{"title":"Streaming Subscribers by Service","subtitle":"Ranked by reported subscribers","periods":"2019, 2020, 2021, 2022, 2023, 2024","series":"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","barCount":6,"periodDuration":2,"valuePrefix":"$","valueSuffix":"M","valueDecimals":0,"accent":"#c8452d"}'
></div>
```
## Source
<Accordion title={`bar-chart-race.html`}>
```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>
```
</Accordion>
{/* 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)
+891 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="beat-freeze-cut preview"
@@ -12,9 +14,7 @@ description: "A beat-driven speed ramp, freeze-frame hit, and hard-cut sequence
## Install
```bash Terminal
npx hyperframes add beat-freeze-cut
```
<InstallCommand command="npx hyperframes add beat-freeze-cut" />
That writes one file: `compositions/beat-freeze-cut.html`.
@@ -44,6 +44,894 @@ 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
+175 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="camcorder-hud preview"
@@ -12,9 +14,7 @@ description: "Responsive REC, battery, editable date placeholder, and timeline-d
## Install
```bash Terminal
npx hyperframes add camcorder-hud
```
<InstallCommand command="npx hyperframes add camcorder-hud" />
That writes one file: `compositions/camcorder-hud.html`.
@@ -37,6 +37,178 @@ 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`.
+434
View File
@@ -0,0 +1,434 @@
---
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";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="camera-dolly-zoom preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/camera-dolly-zoom.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/camera-dolly-zoom.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## 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.
## Variables
Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:
| Variable | Default | Accepts | What it does |
| --- | --- | --- | --- |
| `direction` | `out` | `out`, `in` | |
| `strength` | `2` | 1.1 to 4, step 0.1 | |
| `subjectDistance` | `1400` | 600px to 3000px, step 50px | |
| `easing` | `power2.inOut` | `none`, `sine.inOut`, `power1.inOut`, `power2.inOut`, `power3.inOut` | |
Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:
```html wrap
<div
data-composition-id="camera-dolly-zoom"
data-composition-src="compositions/camera-dolly-zoom.html"
data-variable-values='{"direction":"out","strength":2,"subjectDistance":1400,"easing":"power2.inOut"}'
></div>
```
## Source
<Accordion title={`camera-dolly-zoom.html`}>
```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>
```
</Accordion>
{/* 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)
+375 -11
View File
@@ -3,6 +3,8 @@ title: "Chromatic Radial Split"
description: "Shader transition with chromatic aberration radial split"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="chromatic-radial-split preview"
@@ -12,30 +14,392 @@ description: "Shader transition with chromatic aberration radial split"
## Install
```bash Terminal
npx hyperframes add chromatic-radial-split
```
<InstallCommand command="npx hyperframes add chromatic-radial-split" />
That writes one file: `compositions/chromatic-radial-split.html`.
## Add it to your video
## Source
It runs for 4 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`chromatic-radial-split.html`}>
```html index.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>
@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>
<div
data-composition-id="chromatic-radial-split"
data-composition-src="compositions/chromatic-radial-split.html"
id="root"
data-composition-id="main"
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+375 -11
View File
@@ -3,6 +3,8 @@ title: "Cinematic Zoom"
description: "Shader transition with dramatic zoom blur"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="cinematic-zoom preview"
@@ -12,30 +14,392 @@ description: "Shader transition with dramatic zoom blur"
## Install
```bash Terminal
npx hyperframes add cinematic-zoom
```
<InstallCommand command="npx hyperframes add cinematic-zoom" />
That writes one file: `compositions/cinematic-zoom.html`.
## Add it to your video
## Source
It runs for 4 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`cinematic-zoom.html`}>
```html index.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>
@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>
<div
data-composition-id="cinematic-zoom"
data-composition-src="compositions/cinematic-zoom.html"
id="root"
data-composition-id="main"
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+622 -11
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-3d-extrude preview"
@@ -12,30 +14,639 @@ description: "Syntax-highlighted code on a lit, beveled 3D slab that rotates thr
## Install
```bash Terminal
npx hyperframes add code-3d-extrude
```
<InstallCommand command="npx hyperframes add code-3d-extrude" />
That writes one file: `compositions/code-3d-extrude.html`.
## Add it to your video
## Source
It runs for 8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-3d-extrude.html`}>
```html index.html
```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>
<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"
></div>
>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
+825 -11
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-diff preview"
@@ -12,30 +14,842 @@ description: "An edit shown as a colored diff — removed lines collapse in red,
## Install
```bash Terminal
npx hyperframes add code-diff
```
<InstallCommand command="npx hyperframes add code-diff" />
That writes one file: `compositions/code-diff.html`.
## Add it to your video
## Source
It runs for 6 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-diff.html`}>
```html index.html
```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>
<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>
>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
+755 -12
View File
@@ -1,8 +1,10 @@
---
title: "Code Highlight Sweep"
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)."
description: "A highlight band sweeps across a target line while the surrounding context dims — draws the eye to one line."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-highlight preview"
@@ -12,30 +14,771 @@ description: "A highlight band sweeps across a target line while the surrounding
## Install
```bash Terminal
npx hyperframes add code-highlight
```
<InstallCommand command="npx hyperframes add code-highlight" />
That writes one file: `compositions/code-highlight.html`.
## Add it to your video
## Source
It runs for 5 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-highlight.html`}>
```html index.html
```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>
<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>
>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
File diff suppressed because it is too large Load Diff
+488 -11
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-particle-assemble preview"
@@ -12,30 +14,505 @@ description: "Thousands of GPU points scatter through space and fly to the exact
## Install
```bash Terminal
npx hyperframes add code-particle-assemble
```
<InstallCommand command="npx hyperframes add code-particle-assemble" />
That writes one file: `compositions/code-particle-assemble.html`.
## Add it to your video
## Source
It runs for 8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-particle-assemble.html`}>
```html index.html
```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>
<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"
></div>
>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
File diff suppressed because it is too large Load Diff
+544 -11
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-shader-dissolve preview"
@@ -12,30 +14,561 @@ description: "The code compiles into existence: a GPU fragment shader resolves i
## Install
```bash Terminal
npx hyperframes add code-shader-dissolve
```
<InstallCommand command="npx hyperframes add code-shader-dissolve" />
That writes one file: `compositions/code-shader-dissolve.html`.
## Add it to your video
## Source
It runs for 7 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-shader-dissolve.html`}>
```html index.html
```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>
<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"
></div>
>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
@@ -3,26 +3,284 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-basic preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-basic.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-basic.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-basic.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-basic
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-basic" />
That writes one file: `compositions/code-snippet-apple-terminal-basic.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-basic.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-basic"
data-composition-src="compositions/code-snippet-apple-terminal-basic.html"
@@ -34,8 +292,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* hf:generated-footer */}
@@ -3,26 +3,279 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-clear-dark preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-clear-dark.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-clear-dark.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-clear-dark.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-clear-dark
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-clear-dark" />
That writes one file: `compositions/code-snippet-apple-terminal-clear-dark.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-clear-dark.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-clear-dark"
data-composition-src="compositions/code-snippet-apple-terminal-clear-dark.html"
@@ -34,8 +287,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* hf:generated-footer */}
@@ -3,26 +3,279 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-clear-light preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-clear-light.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-clear-light.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-clear-light.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-clear-light
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-clear-light" />
That writes one file: `compositions/code-snippet-apple-terminal-clear-light.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-clear-light.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-clear-light"
data-composition-src="compositions/code-snippet-apple-terminal-clear-light.html"
@@ -34,8 +287,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* hf:generated-footer */}
@@ -3,26 +3,278 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-grass preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-grass.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-grass.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-grass.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-grass
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-grass" />
That writes one file: `compositions/code-snippet-apple-terminal-grass.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-grass.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-grass"
data-composition-src="compositions/code-snippet-apple-terminal-grass.html"
@@ -34,8 +286,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* hf:generated-footer */}
@@ -3,26 +3,283 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-homebrew preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-homebrew.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-homebrew.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-homebrew.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-homebrew
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-homebrew" />
That writes one file: `compositions/code-snippet-apple-terminal-homebrew.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-homebrew.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-homebrew"
data-composition-src="compositions/code-snippet-apple-terminal-homebrew.html"
@@ -34,8 +291,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* hf:generated-footer */}
@@ -3,26 +3,283 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-man-page preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-man-page.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-man-page.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-man-page.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-man-page
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-man-page" />
That writes one file: `compositions/code-snippet-apple-terminal-man-page.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-man-page.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-man-page"
data-composition-src="compositions/code-snippet-apple-terminal-man-page.html"
@@ -34,8 +291,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* hf:generated-footer */}
@@ -3,26 +3,279 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-novel preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-novel.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-novel.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-novel.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-novel
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-novel" />
That writes one file: `compositions/code-snippet-apple-terminal-novel.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-novel.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-novel"
data-composition-src="compositions/code-snippet-apple-terminal-novel.html"
@@ -34,8 +287,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* hf:generated-footer */}
@@ -3,26 +3,288 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-ocean preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-ocean.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-ocean.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-ocean.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-ocean
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-ocean" />
That writes one file: `compositions/code-snippet-apple-terminal-ocean.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-ocean.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-ocean"
data-composition-src="compositions/code-snippet-apple-terminal-ocean.html"
@@ -34,8 +296,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* hf:generated-footer */}
@@ -3,26 +3,282 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-pro preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-pro.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-pro.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-pro.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-pro
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-pro" />
That writes one file: `compositions/code-snippet-apple-terminal-pro.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-pro.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-pro"
data-composition-src="compositions/code-snippet-apple-terminal-pro.html"
@@ -34,8 +290,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* hf:generated-footer */}
@@ -3,26 +3,279 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-red-sands preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-red-sands.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-red-sands.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-red-sands.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-red-sands
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-red-sands" />
That writes one file: `compositions/code-snippet-apple-terminal-red-sands.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-red-sands.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-red-sands"
data-composition-src="compositions/code-snippet-apple-terminal-red-sands.html"
@@ -34,8 +287,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* hf:generated-footer */}
@@ -3,26 +3,285 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-silver-aerogel preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-silver-aerogel.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-silver-aerogel.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-silver-aerogel.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-silver-aerogel
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-silver-aerogel" />
That writes one file: `compositions/code-snippet-apple-terminal-silver-aerogel.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-silver-aerogel.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-silver-aerogel"
data-composition-src="compositions/code-snippet-apple-terminal-silver-aerogel.html"
@@ -34,8 +293,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* hf:generated-footer */}
@@ -3,26 +3,282 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-apple-terminal-solid-colors preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-solid-colors.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-solid-colors.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-solid-colors.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
```bash Terminal
npx hyperframes add code-snippet-apple-terminal-solid-colors
```
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-solid-colors" />
That writes one file: `compositions/code-snippet-apple-terminal-solid-colors.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-apple-terminal-solid-colors.html`}>
```html index.html
```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
<div
data-composition-id="code-snippet-apple-terminal-solid-colors"
data-composition-src="compositions/code-snippet-apple-terminal-solid-colors.html"
@@ -34,8 +290,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
></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.
## 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
{/* 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
+674 -11
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-snippet-flight preview"
@@ -12,30 +14,691 @@ description: "Discrete code snippets fly in from the side and assemble into a st
## Install
```bash Terminal
npx hyperframes add code-snippet-flight
```
<InstallCommand command="npx hyperframes add code-snippet-flight" />
That writes one file: `compositions/code-snippet-flight.html`.
## Add it to your video
## Source
It runs for 6 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-snippet-flight.html`}>
```html index.html
```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>
<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>
>
<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>
```
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.
</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>
```
{/* 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
+772 -11
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="code-typing preview"
@@ -12,30 +14,789 @@ description: "Token-streamed typing reveal with a caret that tracks the frontier
## Install
```bash Terminal
npx hyperframes add code-typing
```
<InstallCommand command="npx hyperframes add code-typing" />
That writes one file: `compositions/code-typing.html`.
## Add it to your video
## Source
It runs for 5 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`code-typing.html`}>
```html index.html
```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>
<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>
>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
+530
View File
@@ -0,0 +1,530 @@
---
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";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="cosmic-orb preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/cosmic-orb.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cosmic-orb.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## 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`.
## Variables
Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:
| Variable | Default | Accepts | What it does |
| --- | --- | --- | --- |
| `hue` | `268` | 0deg to 360deg, step 1deg | |
| `accent` | `196` | 0deg to 360deg, step 1deg | |
| `spin` | `0.035` | 0turns/s to 0.5turns/s, step 0.005turns/s | |
| `stars` | `1` | 0 to 2, step 0.05 | |
| `glow` | `1` | 0 to 2, step 0.05 | |
| `size` | `0.86` | 0.2 to 1.2, step 0.01 | |
| `pulse` | `0` | 0 to 1, step 0.01 | |
| `pulseEnvelope` | `` | string | |
| `backdrop` | `#04040a` | color | |
Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:
```html wrap
<div
data-composition-id="cosmic-orb"
data-composition-src="compositions/cosmic-orb.html"
data-variable-values='{"hue":268,"accent":196,"spin":0.035,"stars":1,"glow":1,"size":0.86,"pulse":0,"pulseEnvelope":"","backdrop":"#04040a"}'
></div>
```
## Source
<Accordion title={`cosmic-orb.html`}>
```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>
```
</Accordion>
{/* 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)
+386 -11
View File
@@ -3,6 +3,8 @@ title: "Cross Warp Morph"
description: "Shader transition with cross-warped morphing"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="cross-warp-morph preview"
@@ -12,30 +14,403 @@ description: "Shader transition with cross-warped morphing"
## Install
```bash Terminal
npx hyperframes add cross-warp-morph
```
<InstallCommand command="npx hyperframes add cross-warp-morph" />
That writes one file: `compositions/cross-warp-morph.html`.
## Add it to your video
## Source
It runs for 4 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`cross-warp-morph.html`}>
```html index.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>
@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>
<div
data-composition-id="cross-warp-morph"
data-composition-src="compositions/cross-warp-morph.html"
id="root"
data-composition-id="main"
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+477 -22
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="data-chart preview"
@@ -12,31 +14,10 @@ description: "Animated bar + line chart with staggered reveal, NYT-style typogra
## Install
```bash Terminal
npx hyperframes add data-chart
```
<InstallCommand command="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:
@@ -44,6 +25,480 @@ 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`.
+389 -11
View File
@@ -3,6 +3,8 @@ title: "Domain Warp Dissolve"
description: "Shader transition with fractal noise domain warping"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="domain-warp-dissolve preview"
@@ -12,30 +14,406 @@ description: "Shader transition with fractal noise domain warping"
## Install
```bash Terminal
npx hyperframes add domain-warp-dissolve
```
<InstallCommand command="npx hyperframes add domain-warp-dissolve" />
That writes one file: `compositions/domain-warp-dissolve.html`.
## Add it to your video
## Source
It runs for 4 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`domain-warp-dissolve.html`}>
```html index.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>
@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>
<div
data-composition-id="domain-warp-dissolve"
data-composition-src="compositions/domain-warp-dissolve.html"
id="root"
data-composition-id="main"
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+141 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="editorial-flash-overlay preview"
@@ -12,9 +14,7 @@ description: "Finite neutral-warm light layers for a seek-safe camera-flash cut
## Install
```bash Terminal
npx hyperframes add editorial-flash-overlay
```
<InstallCommand command="npx hyperframes add editorial-flash-overlay" />
That writes one file: `compositions/editorial-flash-overlay.html`.
@@ -37,6 +37,144 @@ 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`.
+375 -11
View File
@@ -3,6 +3,8 @@ title: "Flash Through White"
description: "Shader transition with white flash crossfade"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="flash-through-white preview"
@@ -12,30 +14,392 @@ description: "Shader transition with white flash crossfade"
## Install
```bash Terminal
npx hyperframes add flash-through-white
```
<InstallCommand command="npx hyperframes add flash-through-white" />
That writes one file: `compositions/flash-through-white.html`.
## Add it to your video
## Source
It runs for 4 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`flash-through-white.html`}>
```html index.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>
@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>
<div
data-composition-id="flash-through-white"
data-composition-src="compositions/flash-through-white.html"
id="root"
data-composition-id="main"
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+478 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="flowchart-vertical preview"
@@ -12,30 +14,493 @@ description: "Portrait animated decision tree with SVG connectors, sticky-note n
## Install
```bash Terminal
npx hyperframes add flowchart-vertical
```
<InstallCommand command="npx hyperframes add flowchart-vertical" />
That writes one file: `compositions/flowchart-vertical.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1440×2560. Paste this into your composition:
<Accordion title={`flowchart-vertical.html`}>
```html index.html
```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>
<div
id="flowchart-vertical"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+478 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="flowchart preview"
@@ -12,30 +14,493 @@ description: "Animated decision tree with SVG connectors, sticky-note nodes, cur
## Install
```bash Terminal
npx hyperframes add flowchart
```
<InstallCommand command="npx hyperframes add flowchart" />
That writes one file: `compositions/flowchart.html`.
## Add it to your video
## Source
It runs for 12 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`flowchart.html`}>
```html index.html
```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>
<div
id="flowchart"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+132 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="freeze-frame-dressing preview"
@@ -12,9 +14,7 @@ description: "Timeline-driven paper, tape, and flash dressing for a freeze-frame
## Install
```bash Terminal
npx hyperframes add freeze-frame-dressing
```
<InstallCommand command="npx hyperframes add freeze-frame-dressing" />
That writes one file: `compositions/freeze-frame-dressing.html`.
@@ -37,6 +37,135 @@ 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`.
+555
View File
@@ -0,0 +1,555 @@
---
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";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="gallery-tunnel preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/gallery-tunnel.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/gallery-tunnel.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## 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`.
## Variables
Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:
| Variable | Default | Accepts | What it does |
| --- | --- | --- | --- |
| `images` | `` | string | |
| `imageMix` | `0.5` | 0 to 1, step 0.05 | |
| `speed` | `3` | 0segments/s to 8segments/s, step 0.1segments/s | |
| `palette` | `#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200` | string | |
| `grid` | `2` | 1 to 4, step 1 | |
| `fill` | `0.5` | 0 to 1, step 0.05 | |
| `fog` | `14.25` | 3segments to 14.5segments, step 0.25segments | |
| `edgeColor` | `#B0B0B0` | color | |
| `edgeOpacity` | `0.5` | 0 to 1, step 0.05 | |
| `backdrop` | `#000000` | color | |
Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:
```html wrap
<div
data-composition-id="gallery-tunnel"
data-composition-src="compositions/gallery-tunnel.html"
data-variable-values='{"images":"","imageMix":0.5,"speed":3,"palette":"#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200","grid":2,"fill":0.5,"fog":14.25,"edgeColor":"#B0B0B0","edgeOpacity":0.5,"backdrop":"#000000"}'
></div>
```
## Source
<Accordion title={`gallery-tunnel.html`}>
```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>
```
</Accordion>
{/* 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)
+375 -11
View File
@@ -3,6 +3,8 @@ title: "Glitch"
description: "Shader transition with digital glitch artifacts"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="glitch preview"
@@ -12,30 +14,392 @@ description: "Shader transition with digital glitch artifacts"
## Install
```bash Terminal
npx hyperframes add glitch
```
<InstallCommand command="npx hyperframes add glitch" />
That writes one file: `compositions/glitch.html`.
## Add it to your video
## Source
It runs for 4 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`glitch.html`}>
```html index.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>
@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>
<div
data-composition-id="glitch"
data-composition-src="compositions/glitch.html"
id="root"
data-composition-id="main"
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+374 -11
View File
@@ -3,6 +3,8 @@ title: "Gravitational Lens"
description: "Shader transition with gravitational lensing distortion"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="gravitational-lens preview"
@@ -12,30 +14,391 @@ description: "Shader transition with gravitational lensing distortion"
## Install
```bash Terminal
npx hyperframes add gravitational-lens
```
<InstallCommand command="npx hyperframes add gravitational-lens" />
That writes one file: `compositions/gravitational-lens.html`.
## Add it to your video
## Source
It runs for 4 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`gravitational-lens.html`}>
```html index.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>
@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>
<div
data-composition-id="gravitational-lens"
data-composition-src="compositions/gravitational-lens.html"
id="root"
data-composition-id="main"
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+481
View File
@@ -0,0 +1,481 @@
---
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";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="halftone-field preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/halftone-field.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/halftone-field.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## 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`.
## Variables
Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:
| Variable | Default | Accepts | What it does |
| --- | --- | --- | --- |
| `frequency` | `2` | 1 to 10, step 0.1 | |
| `speed` | `5` | 0 to 10, step 0.1 | |
| `cellSize` | `54` | 1 to 100, step 1 | |
| `gamma` | `12` | 1 to 20, step 0.1 | |
| `paletteBias` | `2` | 0 to 20, step 0.5 | |
| `palette1` | `#00AAFF` | color | |
| `palette2` | `#C2FF67` | color | |
| `palette3` | `#6600FF` | color | |
| `palette4` | `#000000` | color | |
| `background` | `#000000` | color | |
| `quantize` | `false` | boolean | |
Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:
```html wrap
<div
data-composition-id="halftone-field"
data-composition-src="compositions/halftone-field.html"
data-variable-values='{"frequency":2,"speed":5,"cellSize":54,"gamma":12,"paletteBias":2,"palette1":"#00AAFF","palette2":"#C2FF67","palette3":"#6600FF","palette4":"#000000","background":"#000000","quantize":false}'
></div>
```
## Source
<Accordion title={`halftone-field.html`}>
```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>
```
</Accordion>
{/* 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)
+314 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="hw-frame preview"
@@ -12,9 +14,7 @@ description: "Media in a hand-drawn border with corner doodles and a handwritten
## Install
```bash Terminal
npx hyperframes add hw-frame
```
<InstallCommand command="npx hyperframes add hw-frame" />
That writes `compositions/hw-frame.html`, plus 1 supporting file under `assets/fonts/`.
@@ -37,6 +37,317 @@ 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`.
+205 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="hw-path-text preview"
@@ -12,9 +14,7 @@ description: "Handwriting flowing along any SVG path with a per-character reveal
## Install
```bash Terminal
npx hyperframes add hw-path-text
```
<InstallCommand command="npx hyperframes add hw-path-text" />
That writes `compositions/hw-path-text.html`, plus 1 supporting file under `assets/fonts/`.
@@ -37,6 +37,208 @@ 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`.
+329 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="hw-pipeline preview"
@@ -12,9 +14,7 @@ description: "Wobbled boxes with handwritten labels joined by curved connectors,
## Install
```bash Terminal
npx hyperframes add hw-pipeline
```
<InstallCommand command="npx hyperframes add hw-pipeline" />
That writes `compositions/hw-pipeline.html`, plus 1 supporting file under `assets/fonts/`.
@@ -37,6 +37,332 @@ 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`.
+164 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="hw-scribble-transition preview"
@@ -12,9 +14,7 @@ description: "Overlay transition: scribble bands accumulate to cover the frame w
## Install
```bash Terminal
npx hyperframes add hw-scribble-transition
```
<InstallCommand command="npx hyperframes add hw-scribble-transition" />
That writes one file: `compositions/hw-scribble-transition.html`.
@@ -37,6 +37,167 @@ 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`.
+297 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="hw-text-cloud preview"
@@ -12,9 +14,7 @@ description: "Hand-drawn speech bubble with a positionable tail: outline draws o
## Install
```bash Terminal
npx hyperframes add hw-text-cloud
```
<InstallCommand command="npx hyperframes add hw-text-cloud" />
That writes `compositions/hw-text-cloud.html`, plus 1 supporting file under `assets/fonts/`.
@@ -37,6 +37,300 @@ 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`.
+269 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="hw-title preview"
@@ -12,9 +14,7 @@ description: "Handwritten display text with a scalar-driven word-by-word highlig
## Install
```bash Terminal
npx hyperframes add hw-title
```
<InstallCommand command="npx hyperframes add hw-title" />
That writes `compositions/hw-title.html`, plus 1 supporting file under `assets/fonts/`.
@@ -37,6 +37,272 @@ 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`.
+299 -13
View File
@@ -3,6 +3,8 @@ title: "Instagram Follow"
description: "Animated Instagram follow overlay with profile card and follow button"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="instagram-follow preview"
@@ -12,30 +14,314 @@ description: "Animated Instagram follow overlay with profile card and follow but
## Install
```bash Terminal
npx hyperframes add instagram-follow
```
<InstallCommand command="npx hyperframes add instagram-follow" />
That writes `compositions/instagram-follow.html`, plus 1 supporting file under `assets/`.
## Add it to your video
## Source
It runs for 4.5 seconds at 1080×1920. Paste this into your composition:
<Accordion title={`instagram-follow.html`}>
```html index.html
```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>
<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"
></div>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
File diff suppressed because it is too large Load Diff
+374 -11
View File
@@ -3,6 +3,8 @@ title: "Light Leak"
description: "Shader transition with cinematic light leak overlay"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="light-leak preview"
@@ -12,30 +14,391 @@ description: "Shader transition with cinematic light leak overlay"
## Install
```bash Terminal
npx hyperframes add light-leak
```
<InstallCommand command="npx hyperframes add light-leak" />
That writes one file: `compositions/light-leak.html`.
## Add it to your video
## Source
It runs for 4 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`light-leak.html`}>
```html index.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>
@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>
<div
data-composition-id="light-leak"
data-composition-src="compositions/light-leak.html"
id="root"
data-composition-id="main"
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+637 -16
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="liquid-glass-context-menu preview"
@@ -12,36 +14,655 @@ description: "Frosted glass context menu panel drifting over an aurora shader ba
## Install
```bash Terminal
npx hyperframes add liquid-glass-context-menu
```
<InstallCommand command="npx hyperframes add liquid-glass-context-menu" />
That writes `compositions/liquid-glass-context-menu.html`, plus 1 supporting file under `compositions/lib/`.
<Warning>
<Danger>
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
Rendering from the CLI switches it on for you. [How it
works](/guides/html-in-canvas)
</Warning>
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>
## Add it to your video
## Source
It runs for 8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`liquid-glass-context-menu.html`}>
```html index.html
```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>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="liquid-glass-media-controls preview"
@@ -12,36 +14,716 @@ description: "Frosted glass media control panels spreading over an aurora shader
## Install
```bash Terminal
npx hyperframes add liquid-glass-media-controls
```
<InstallCommand command="npx hyperframes add liquid-glass-media-controls" />
That writes `compositions/liquid-glass-media-controls.html`, plus 1 supporting file under `compositions/lib/`.
<Warning>
<Danger>
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
Rendering from the CLI switches it on for you. [How it
works](/guides/html-in-canvas)
</Warning>
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>
## Add it to your video
## Source
It runs for 8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`liquid-glass-media-controls.html`}>
```html index.html
```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>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
+495 -16
View File
@@ -3,6 +3,8 @@ title: "Liquid Glass Notification"
description: "Frosted glass notification cards floating over an aurora shader background"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="liquid-glass-notification preview"
@@ -12,36 +14,513 @@ description: "Frosted glass notification cards floating over an aurora shader ba
## Install
```bash Terminal
npx hyperframes add liquid-glass-notification
```
<InstallCommand command="npx hyperframes add liquid-glass-notification" />
That writes `compositions/liquid-glass-notification.html`, plus 2 supporting files under `compositions/lib/` and `compositions/assets/`.
That writes `compositions/liquid-glass-notification.html`, plus 2 supporting files under `compositions/assets/` and `compositions/lib/`.
<Warning>
<Danger>
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
Rendering from the CLI switches it on for you. [How it
works](/guides/html-in-canvas)
</Warning>
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>
## Add it to your video
## Source
It runs for 8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`liquid-glass-notification.html`}>
```html index.html
```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>
<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"
></div>
>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
+773 -16
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="liquid-glass-widgets preview"
@@ -12,36 +14,791 @@ description: "Frosted glass stat cards, showcase panel and pill chips over an au
## Install
```bash Terminal
npx hyperframes add liquid-glass-widgets
```
<InstallCommand command="npx hyperframes add liquid-glass-widgets" />
That writes `compositions/liquid-glass-widgets.html`, plus 2 supporting files under `compositions/assets/` and `compositions/lib/`.
<Warning>
<Danger>
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
Rendering from the CLI switches it on for you. [How it
works](/guides/html-in-canvas)
</Warning>
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>
## Add it to your video
## Source
It runs for 8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`liquid-glass-widgets.html`}>
```html index.html
```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>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
+246 -22
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="logo-outro preview"
@@ -12,31 +14,10 @@ description: "Cinematic logo reveal with piece-by-piece assembly, glow bloom, ta
## Install
```bash Terminal
npx hyperframes add logo-outro
```
<InstallCommand command="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:
@@ -44,6 +25,249 @@ 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`.
+183 -13
View File
@@ -3,6 +3,8 @@ 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";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lower-third-bild preview"
@@ -12,30 +14,198 @@ description: "News-style lower third with tight-fit text boxes: white headline b
## Install
```bash Terminal
npx hyperframes add lower-third-bild
```
<InstallCommand command="npx hyperframes add lower-third-bild" />
That writes `compositions/lower-third-bild.html`, plus 2 supporting files under `compositions/assets/fonts/`.
## Add it to your video
## Variables
It runs for 8 seconds at 1920×1080. Paste this into your composition:
Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:
```html index.html
| Variable | Default | Accepts | What it does |
| --- | --- | --- | --- |
| `TXT_MAIN_1_Line` | `BILD EXKLUSIV` | string | |
| `TXT_SUB` | `Sonderbericht` | string | |
Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:
```html wrap
<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"
data-variable-values='{"TXT_MAIN_1_Line":"BILD EXKLUSIV","TXT_SUB":"Sonderbericht"}'
></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.
## Source
<Accordion title={`lower-third-bild.html`}>
```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>
```
</Accordion>
## Usage
After installing, add the block to your host 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>
```
{/* hf:generated-footer */}
+117 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lt-accent-underline preview"
@@ -12,30 +14,132 @@ description: "Cardless lower third for footage overlay: name rises, an accent ru
## Install
```bash Terminal
npx hyperframes add lt-accent-underline
```
<InstallCommand command="npx hyperframes add lt-accent-underline" />
That writes one file: `compositions/lt-accent-underline.html`.
## Add it to your video
## Source
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`lt-accent-underline.html`}>
```html index.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>
* {
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>
<div
id="root"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+119 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lt-bold-block preview"
@@ -12,30 +14,134 @@ description: "High-energy podcast lower third: solid dark block wipes in, upperc
## Install
```bash Terminal
npx hyperframes add lt-bold-block
```
<InstallCommand command="npx hyperframes add lt-bold-block" />
That writes one file: `compositions/lt-bold-block.html`.
## Add it to your video
## Source
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`lt-bold-block.html`}>
```html index.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>
* {
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>
<div
id="root"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+122 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lt-clean-bar preview"
@@ -12,30 +14,137 @@ description: "Minimal white-card lower third for podcasts/interviews: accent tab
## Install
```bash Terminal
npx hyperframes add lt-clean-bar
```
<InstallCommand command="npx hyperframes add lt-clean-bar" />
That writes one file: `compositions/lt-clean-bar.html`.
## Add it to your video
## Source
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`lt-clean-bar.html`}>
```html index.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>
* {
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>
<div
id="root"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+111 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lt-color-block preview"
@@ -12,30 +14,126 @@ description: "High-energy lower third: bold accent-color block slides in with ov
## Install
```bash Terminal
npx hyperframes add lt-color-block
```
<InstallCommand command="npx hyperframes add lt-color-block" />
That writes one file: `compositions/lt-color-block.html`.
## Add it to your video
## Source
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`lt-color-block.html`}>
```html index.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>
* {
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>
<div
id="root"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+121 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lt-dark-card preview"
@@ -12,30 +14,136 @@ description: "Charcoal card lower third for bright footage: name, drawn accent u
## Install
```bash Terminal
npx hyperframes add lt-dark-card
```
<InstallCommand command="npx hyperframes add lt-dark-card" />
That writes one file: `compositions/lt-dark-card.html`.
## Add it to your video
## Source
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`lt-dark-card.html`}>
```html index.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>
* {
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>
<div
id="root"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+124 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lt-kicker-name preview"
@@ -12,30 +14,139 @@ description: "Cardless lower third with an accent eyebrow/kicker tag, heavy name
## Install
```bash Terminal
npx hyperframes add lt-kicker-name
```
<InstallCommand command="npx hyperframes add lt-kicker-name" />
That writes one file: `compositions/lt-kicker-name.html`.
## Add it to your video
## Source
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`lt-kicker-name.html`}>
```html index.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>
* {
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>
<div
id="root"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+130 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lt-mask-reveal preview"
@@ -12,30 +14,145 @@ description: "Cardless lower third: an accent sweep crosses and clip-path-reveal
## Install
```bash Terminal
npx hyperframes add lt-mask-reveal
```
<InstallCommand command="npx hyperframes add lt-mask-reveal" />
That writes one file: `compositions/lt-mask-reveal.html`.
## Add it to your video
## Source
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`lt-mask-reveal.html`}>
```html index.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>
* {
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>
<div
id="root"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+489
View File
@@ -0,0 +1,489 @@
---
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";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lt-neon-border preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/lt-neon-border.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-neon-border.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## 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`.
## Variables
Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:
| Variable | Default | Accepts | What it does |
| --- | --- | --- | --- |
| `name` | `Dr. Maya Chen` | string | |
| `role` | `Host · Neuroscientist` | string | |
| `accent` | `#cc9149` | color | |
| `movement` | `step` | `step`, `glide` | |
| `speed` | `1.6` | 0.2corners/s to 6corners/s, step 0.1corners/s | |
| `cornerRadius` | `26` | 0px to 90px, step 1px | |
| `thickness` | `5` | 1px to 16px, step 1px | |
| `arcLength` | `22` | 5% to 100%, step 1% | |
| `glow` | `1` | 0x to 2x, step 0.05x | |
Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:
```html wrap
<div
data-composition-id="lt-neon-border"
data-composition-src="compositions/lt-neon-border.html"
data-variable-values='{"name":"Dr. Maya Chen","role":"Host · Neuroscientist","accent":"#cc9149","movement":"step","speed":1.6,"cornerRadius":26,"thickness":5,"arcLength":22,"glow":1}'
></div>
```
## Source
<Accordion title={`lt-neon-border.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>
* {
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>
```
</Accordion>
{/* 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)
+124 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lt-side-rule preview"
@@ -12,30 +14,139 @@ description: "Cardless lower third with a vertical accent bar; condensed display
## Install
```bash Terminal
npx hyperframes add lt-side-rule
```
<InstallCommand command="npx hyperframes add lt-side-rule" />
That writes one file: `compositions/lt-side-rule.html`.
## Add it to your video
## Source
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`lt-side-rule.html`}>
```html index.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>
* {
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>
<div
id="root"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+129 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lt-soft-pill preview"
@@ -12,30 +14,144 @@ description: "Rounded white pill lower third for podcasts/interviews: status dot
## Install
```bash Terminal
npx hyperframes add lt-soft-pill
```
<InstallCommand command="npx hyperframes add lt-soft-pill" />
That writes one file: `compositions/lt-soft-pill.html`.
## Add it to your video
## Source
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`lt-soft-pill.html`}>
```html index.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>
* {
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>
<div
id="root"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+119 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="lt-stack-bars preview"
@@ -12,30 +14,134 @@ description: "Two stacked bars: a dark name bar wipes from the left, an accent r
## Install
```bash Terminal
npx hyperframes add lt-stack-bars
```
<InstallCommand command="npx hyperframes add lt-stack-bars" />
That writes one file: `compositions/lt-stack-bars.html`.
## Add it to your video
## Source
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`lt-stack-bars.html`}>
```html index.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>
* {
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>
<div
id="root"
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>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+212 -13
View File
@@ -3,6 +3,8 @@ title: "macOS Notification"
description: "Animated macOS-style notification banner with app icon and message"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="macos-notification preview"
@@ -12,30 +14,227 @@ description: "Animated macOS-style notification banner with app icon and message
## Install
```bash Terminal
npx hyperframes add macos-notification
```
<InstallCommand command="npx hyperframes add macos-notification" />
That writes one file: `compositions/macos-notification.html`.
## Add it to your video
## Source
It runs for 5 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`macos-notification.html`}>
```html index.html
```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>
<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"
></div>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
File diff suppressed because it is too large Load Diff
+247 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="mk-background preview"
@@ -12,9 +14,7 @@ description: "Procedural soft-blob gradient backdrop in a minimalist presentatio
## Install
```bash Terminal
npx hyperframes add mk-background
```
<InstallCommand command="npx hyperframes add mk-background" />
That writes one file: `compositions/mk-background.html`.
@@ -37,6 +37,250 @@ 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`.
+168 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="mk-callout-highlight preview"
@@ -12,9 +14,7 @@ description: "A sentence whose word-by-word emphasis is driven by a single scala
## Install
```bash Terminal
npx hyperframes add mk-callout-highlight
```
<InstallCommand command="npx hyperframes add mk-callout-highlight" />
That writes one file: `compositions/mk-callout-highlight.html`.
@@ -37,6 +37,171 @@ 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,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="mk-clone-wall-transition preview"
@@ -12,9 +14,7 @@ description: "Overlay transition: a tiled word wall covers the frame, inverts vi
## Install
```bash Terminal
npx hyperframes add mk-clone-wall-transition
```
<InstallCommand command="npx hyperframes add mk-clone-wall-transition" />
That writes one file: `compositions/mk-clone-wall-transition.html`.
@@ -37,6 +37,240 @@ 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`.
+304 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="mk-line-graph preview"
@@ -12,9 +14,7 @@ description: "One or two SVG series draw on with dots popping and value labels r
## Install
```bash Terminal
npx hyperframes add mk-line-graph
```
<InstallCommand command="npx hyperframes add mk-line-graph" />
That writes one file: `compositions/mk-line-graph.html`.
@@ -37,6 +37,307 @@ 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`.
+245 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="mk-placeholder-grid preview"
@@ -12,9 +14,7 @@ description: "N-up rounded-corner media grid (2-up, 3-up, 4-up, 3x2, hero plus t
## Install
```bash Terminal
npx hyperframes add mk-placeholder-grid
```
<InstallCommand command="npx hyperframes add mk-placeholder-grid" />
That writes one file: `compositions/mk-placeholder-grid.html`.
@@ -37,6 +37,248 @@ 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`.
+197 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="mk-progress-stat preview"
@@ -12,9 +14,7 @@ description: "Big-numeral statistic with count-up, label, thin progress track fi
## Install
```bash Terminal
npx hyperframes add mk-progress-stat
```
<InstallCommand command="npx hyperframes add mk-progress-stat" />
That writes one file: `compositions/mk-progress-stat.html`.
@@ -37,6 +37,200 @@ 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`.
+234 -3
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="mk-specs-list preview"
@@ -12,9 +14,7 @@ description: "Left-aligned specs/traits checklist over footage or a board: rows
## Install
```bash Terminal
npx hyperframes add mk-specs-list
```
<InstallCommand command="npx hyperframes add mk-specs-list" />
That writes one file: `compositions/mk-specs-list.html`.
@@ -37,6 +37,237 @@ 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`.
+419 -13
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="news-ticker preview"
@@ -12,30 +14,434 @@ description: "Premium broadcast-style lower-third ticker with live label, headli
## Install
```bash Terminal
npx hyperframes add news-ticker
```
<InstallCommand command="npx hyperframes add news-ticker" />
That writes one file: `compositions/news-ticker.html`.
## Add it to your video
## Source
It runs for 7 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`news-ticker.html`}>
```html index.html
```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>
<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"
></div>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
+314 -11
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="north-korea-locked-down preview"
@@ -12,30 +14,331 @@ description: "Realistic map zoom into North Korea with a red scribble circle, lo
## Install
```bash Terminal
npx hyperframes add north-korea-locked-down
```
<InstallCommand command="npx hyperframes add north-korea-locked-down" />
That writes `compositions/north-korea-locked-down.html`, plus 1 supporting file under `assets/`.
## Add it to your video
## Source
It runs for 7 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`north-korea-locked-down.html`}>
```html index.html
```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>
<div
id="root"
data-composition-id="north-korea-locked-down"
data-composition-src="compositions/north-korea-locked-down.html"
data-root="true"
data-start="0"
data-duration="7"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
>
<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>
```
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.
</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>
```
{/* hf:generated-footer */}
+535 -12
View File
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="nyc-paris-flight preview"
@@ -12,30 +14,551 @@ description: "Apple-style realistic map animation with a plane flying from New Y
## Install
```bash Terminal
npx hyperframes add nyc-paris-flight
```
<InstallCommand command="npx hyperframes add nyc-paris-flight" />
That writes `compositions/nyc-paris-flight.html`, plus 2 supporting files under `assets/`.
## Add it to your video
## Source
It runs for 6 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`nyc-paris-flight.html`}>
```html index.html
```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>
<div
id="root"
data-composition-id="nyc-paris-flight"
data-composition-src="compositions/nyc-paris-flight.html"
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-start="0"
data-duration="6"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
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>
```
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.
</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>
```
{/* hf:generated-footer */}
@@ -3,6 +3,8 @@ 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
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="organic-light-leak-overlay preview"
@@ -12,9 +14,7 @@ description: "Finite CSS light-leak overlay for memory beats and motivated trans
## Install
```bash Terminal
npx hyperframes add organic-light-leak-overlay
```
<InstallCommand command="npx hyperframes add organic-light-leak-overlay" />
That writes one file: `compositions/organic-light-leak-overlay.html`.
@@ -37,6 +37,140 @@ 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`.
+603
View File
@@ -0,0 +1,603 @@
---
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";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="oscilloscope-trace preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/oscilloscope-trace.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/oscilloscope-trace.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## 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.
## Variables
Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:
| Variable | Default | Accepts | What it does |
| --- | --- | --- | --- |
| `waveform` | `sine` | `sine`, `square`, `triangle`, `data` | |
| `frequency` | `12` | 0.1Hz to 200Hz, step 0.1Hz | |
| `amplitude` | `3` | 0.1div to 4div, step 0.1div | |
| `persistenceMs` | `100` | 1ms to 1000ms, step 1ms | |
| `phosphorColor` | `#5dff8f` | color | |
| `sweepRate` | `5` | 0.25sweeps/s to 60sweeps/s, step 0.25sweeps/s | |
| `dataSeries` | `` | string | |
Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:
```html wrap
<div
data-composition-id="oscilloscope-trace"
data-composition-src="compositions/oscilloscope-trace.html"
data-variable-values='{"waveform":"sine","frequency":12,"amplitude":3,"persistenceMs":100,"phosphorColor":"#5dff8f","sweepRate":5,"dataSeries":""}'
></div>
```
## Source
<Accordion title={`oscilloscope-trace.html`}>
```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>
```
</Accordion>
{/* 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)
+708
View File
@@ -0,0 +1,708 @@
---
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";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="rack-focus preview"
loading="lazy"
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/rack-focus.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/rack-focus.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## 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`.
## Variables
Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:
| Variable | Default | Accepts | What it does |
| --- | --- | --- | --- |
| `nearfocus` | `1.2` | 0.2m to 100m, step 0.05m | |
| `farfocus` | `80` | 0.3m to 400m, step 0.5m | |
| `focallength` | `85` | 12mm to 300mm, step 1mm | |
| `aperture` | `1.8` | 0.95 to 22, step 0.05 | |
| `blades` | `6` | 3 to 14, step 1 | |
| `catseye` | `0.62` | 0 to 1, step 0.02 | |
| `pullstart` | `1.2` | 0s to 30s, step 0.05s | |
| `pullduration` | `3.2` | 0.1s to 30s, step 0.05s | |
| `pullease` | `power2.inOut` | string | |
| `bokeh` | `1` | 0 to 3, step 0.05 | |
| `backdrop` | `#05060a` | color | |
Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:
```html wrap
<div
data-composition-id="rack-focus"
data-composition-src="compositions/rack-focus.html"
data-variable-values='{"nearfocus":1.2,"farfocus":80,"focallength":85,"aperture":1.8,"blades":6,"catseye":0.62,"pullstart":1.2,"pullduration":3.2,"pullease":"power2.inOut","bokeh":1,"backdrop":"#05060a"}'
></div>
```
## Source
<Accordion title={`rack-focus.html`}>
```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>
```
</Accordion>
{/* 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)
+357 -13
View File
@@ -3,6 +3,8 @@ title: "Reddit Post Card"
description: "Animated Reddit post card overlay with upvotes and comments"
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="reddit-post preview"
@@ -12,30 +14,372 @@ description: "Animated Reddit post card overlay with upvotes and comments"
## Install
```bash Terminal
npx hyperframes add reddit-post
```
<InstallCommand command="npx hyperframes add reddit-post" />
That writes one file: `compositions/reddit-post.html`.
## Add it to your video
## Source
It runs for 5 seconds at 1920×1080. Paste this into your composition:
<Accordion title={`reddit-post.html`}>
```html index.html
```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;
box-sizing: border-box;
}
html,
body {
background: transparent;
overflow: hidden;
}
body {
width: 1920px;
height: 1080px;
font-family: "DM Sans", sans-serif;
}
[data-composition-id="reddit-post"] .reddit-card {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 860px;
background: #1a1a2e;
border-radius: 16px;
padding: 32px;
box-shadow: 0 12px 60px rgba(0, 0, 0, 0.6);
}
/* Header */
[data-composition-id="reddit-post"] .post-header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 24px;
}
[data-composition-id="reddit-post"] .subreddit-icon {
width: 48px;
height: 48px;
flex-shrink: 0;
}
[data-composition-id="reddit-post"] .subreddit-icon svg {
width: 48px;
height: 48px;
display: block;
}
[data-composition-id="reddit-post"] .post-meta {
display: flex;
flex-direction: column;
gap: 2px;
}
[data-composition-id="reddit-post"] .subreddit-name {
font-size: 28px;
font-weight: 700;
color: #d7dadc;
line-height: 1.3;
}
[data-composition-id="reddit-post"] .post-author {
font-size: 24px;
font-weight: 400;
color: #818384;
line-height: 1.3;
}
/* Content */
[data-composition-id="reddit-post"] .post-title {
font-size: 38px;
font-weight: 700;
color: #d7dadc;
line-height: 1.35;
margin-bottom: 16px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
[data-composition-id="reddit-post"] .post-body {
font-size: 28px;
font-weight: 400;
color: #818384;
line-height: 1.45;
margin-bottom: 28px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Action bar */
[data-composition-id="reddit-post"] .action-bar {
display: flex;
align-items: center;
gap: 28px;
padding-top: 20px;
border-top: 1px solid #343536;
}
[data-composition-id="reddit-post"] .vote-group {
display: flex;
align-items: center;
gap: 10px;
background: #272729;
border-radius: 24px;
padding: 8px 16px;
}
[data-composition-id="reddit-post"] .vote-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
[data-composition-id="reddit-post"] .vote-btn svg {
width: 28px;
height: 28px;
display: block;
}
[data-composition-id="reddit-post"] .vote-count {
font-size: 26px;
font-weight: 700;
color: #d7dadc;
min-width: 50px;
text-align: center;
}
[data-composition-id="reddit-post"] .action-btn {
display: flex;
align-items: center;
gap: 8px;
background: #272729;
border-radius: 24px;
padding: 10px 20px;
}
[data-composition-id="reddit-post"] .action-btn svg {
width: 26px;
height: 26px;
display: block;
}
[data-composition-id="reddit-post"] .action-btn span {
font-size: 24px;
font-weight: 400;
color: #818384;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="reddit-post"
data-composition-src="compositions/reddit-post.html"
data-start="0"
data-duration="5"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
data-start="0"
data-duration="5"
>
<div id="card" class="clip reddit-card" data-start="0" data-duration="5" data-track-index="0">
<!-- Header -->
<div class="post-header">
<div class="subreddit-icon">
<svg viewBox="0 0 48 48" fill="none">
<circle cx="24" cy="24" r="24" fill="#4A90D9" />
<text
x="24"
y="30"
text-anchor="middle"
font-family="DM Sans, sans-serif"
font-size="22"
font-weight="700"
fill="#fff"
>
r/
</text>
</svg>
</div>
<div class="post-meta">
<span class="subreddit-name">r/hyperframes</span>
<span class="post-author">u/placeholder_user &middot; 3h</span>
</div>
</div>
<!-- Content -->
<div class="post-title">Prompt to change this title to whatever you want</div>
<div class="post-body">
Prompt to change this body text, the subreddit, username, and vote count to match your
content.
</div>
<!-- Action bar -->
<div class="action-bar">
<div class="vote-group">
<div id="upvote-btn" class="vote-btn">
<svg viewBox="0 0 24 24" fill="none">
<path id="upvote-arrow" d="M12 4L3 14h5v6h8v-6h5L12 4z" fill="#818384" />
</svg>
</div>
<span id="vote-count" class="vote-count">4.2k</span>
<div class="vote-btn">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 20l9-10h-5V4H8v6H3l9 10z" fill="#818384" />
</svg>
</div>
</div>
<div class="action-btn">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="#818384"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
</svg>
<span>328</span>
</div>
<div class="action-btn">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="#818384"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 12v8a2 2 0 002 2h12a2 2 0 002-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>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var card = document.getElementById("card");
var upvoteBtn = document.getElementById("upvote-btn");
var upvoteArrow = document.getElementById("upvote-arrow");
var voteCount = document.getElementById("vote-count");
// Start off-screen below
gsap.set(card, { y: 400, opacity: 0 });
// Slide in from bottom
tl.to(
card,
{
y: 0,
opacity: 1,
duration: 0.6,
ease: "power3.out",
},
0.1,
);
// Upvote press-in
tl.to(
upvoteBtn,
{
scale: 0.85,
duration: 0.12,
ease: "power2.out",
},
1.5,
);
// Upvote release with spring bounce
tl.to(
upvoteBtn,
{
scale: 1,
duration: 0.4,
ease: "elastic.out(1, 0.4)",
},
1.62,
);
// Arrow turns orange
tl.to(
upvoteArrow,
{
attr: { fill: "#ff4500" },
duration: 0.12,
ease: "none",
},
1.62,
);
// Vote count turns orange + increments
tl.to(
voteCount,
{
color: "#ff4500",
duration: 0.12,
ease: "none",
onStart: function () {
voteCount.textContent = "4.3k";
},
},
1.62,
);
// Slide out to bottom
tl.to(
card,
{
y: 400,
opacity: 0,
duration: 0.3,
ease: "power3.in",
},
4.2,
);
window.__timelines["reddit-post"] = tl;
</script>
</body>
</html>
```
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.
</Accordion>
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="reddit-post" data-composition-src="compositions/reddit-post.html" data-start="0" data-duration="5" data-track-index="1" data-width="1920" data-height="1080"></div>
```
{/* hf:generated-footer */}

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