feat(registry): add VFX and Captions block categories

Add 12 new registry blocks across two new categories:

**VFX Blocks (7):**
- vfx-text-cursor: dramatic text reveal with cursor glow and chromatic shadows
- vfx-liquid-background: organic liquid simulation with vertex displacement
- vfx-iphone-device: real GLTF iPhone + MacBook with live HTML screens
- vfx-magnetic: magnetic field particle visualization
- vfx-portal: dimension breach portal transition
- vfx-liquid-glass: Voronoi glass fracture with parallax reveal
- vfx-shatter: glass shatter with physics-driven fragments

**Caption Blocks (5):**
- captions-slam: bold uppercase scale-pop with back.out overshoot
- captions-karaoke: word-by-word color reveal on frosted glass pill
- captions-minimal: clean Netflix-subtitle style fade
- captions-bounce: playful elastic bounce with colorful word pills
- captions-cinematic: elegant slow fade with letter-spacing animation

All blocks include registry-item.json and are registered in registry.json.
VFX blocks use experimental stability, caption blocks use stable.
This commit is contained in:
Miguel Ángel
2026-05-06 00:35:02 -07:00
parent cd277ca312
commit a68191efe1
29 changed files with 5576 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Captions Bounce</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;800&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: #000;
}
#captions-bounce {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: linear-gradient(180deg, #0e0c18 0%, #130f22 50%, #0e0c18 100%);
}
#captions-bounce::before {
content: '';
position: absolute;
bottom: -80px;
left: 50%;
transform: translateX(-50%);
width: 1000px;
height: 400px;
background: radial-gradient(ellipse, rgba(168, 85, 247, 0.04) 0%, transparent 70%);
pointer-events: none;
}
.caption-group {
position: absolute;
bottom: 130px;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
opacity: 0;
visibility: hidden;
will-change: opacity;
font-variant-numeric: tabular-nums;
}
.caption-group .word-pill {
display: inline-block;
font-family: 'Inter', system-ui, sans-serif;
font-weight: 800;
font-size: 64px;
line-height: 1;
color: #ffffff;
padding: 16px 32px;
border-radius: 16px;
opacity: 0;
transform: translateY(40px) scale(0.5);
will-change: transform, opacity;
}
/* Alternating pill colors */
.word-pill-0 { background: rgba(0, 212, 255, 0.85); }
.word-pill-1 { background: rgba(139, 92, 246, 0.85); }
.word-pill-2 { background: rgba(16, 185, 129, 0.85); }
.driver {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
opacity: 0;
}
</style>
</head>
<body>
<div
id="captions-bounce"
data-composition-id="captions-bounce"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="13"
data-root="true"
>
<div class="driver" data-track-index="0"></div>
</div>
<script>
(function () {
var GROUPS = [
{ text: "Write HTML", start: 0.3, end: 1.8 },
{ text: "render video", start: 2.0, end: 3.5 },
{ text: "No timeline editors", start: 3.8, end: 5.5 },
{ text: "no After Effects", start: 5.7, end: 7.2 },
{ text: "Ship 10x faster", start: 7.5, end: 9.5 },
{ text: "with HyperFrames", start: 9.8, end: 12.0 }
];
var PILL_COLORS = ["word-pill-0", "word-pill-1", "word-pill-2"];
var root = document.getElementById("captions-bounce");
// Build caption group elements with per-word pill spans
GROUPS.forEach(function (group, gi) {
var div = document.createElement("div");
div.className = "caption-group";
div.id = "cg-" + gi;
var words = group.text.split(" ");
words.forEach(function (word, wi) {
var span = document.createElement("span");
span.className = "word-pill " + PILL_COLORS[wi % PILL_COLORS.length];
span.id = "cg-" + gi + "-w" + wi;
span.textContent = word;
div.appendChild(span);
});
root.appendChild(div);
});
// Build timeline
var tl = gsap.timeline({ paused: true });
GROUPS.forEach(function (group, gi) {
var el = "#cg-" + gi;
var words = group.text.split(" ");
var staggerDelay = 0.08;
// Show group container
tl.set(el, { opacity: 1, visibility: "visible" }, group.start);
// Bounce each word in with elastic ease, staggered
words.forEach(function (word, wi) {
var wordEl = "#cg-" + gi + "-w" + wi;
var wordStart = group.start + wi * staggerDelay;
tl.to(wordEl,
{ opacity: 1, y: 0, scale: 1, duration: 0.6, ease: "elastic.out(1, 0.4)" },
wordStart
);
});
// Fade out the whole group
tl.to(el,
{ opacity: 0, duration: 0.15, ease: "power2.in" },
group.end - 0.15
);
// Hard kill: reset all word pills and hide group
words.forEach(function (word, wi) {
var wordEl = "#cg-" + gi + "-w" + wi;
tl.set(wordEl, { opacity: 0, y: 40, scale: 0.5 }, group.end);
});
tl.set(el, { opacity: 0, visibility: "hidden" }, group.end);
});
window.__timelines = window.__timelines || [];
window.__timelines.push(tl);
})();
</script>
</body>
</html>
@@ -0,0 +1,14 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "captions-bounce",
"type": "hyperframes:block",
"title": "Captions Bounce",
"description": "Playful social-media style captions with elastic bounce-in per word on alternating colorful pill backgrounds",
"stability": "stable",
"dimensions": { "width": 1920, "height": 1080 },
"duration": 13,
"tags": ["captions", "bounce"],
"files": [
{ "path": "captions-bounce.html", "target": "compositions/captions-bounce.html", "type": "hyperframes:composition" }
]
}
@@ -0,0 +1,182 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Captions Cinematic</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:ital,wght@0,300;0,400;1,300&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: #000;
}
#captions-cinematic {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: linear-gradient(180deg, #0a0908 0%, #0f0d0a 50%, #0a0908 100%);
}
/* Warm vignette */
#captions-cinematic::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, transparent 50%, rgba(0, 0, 0, 0.4) 100%);
pointer-events: none;
}
.caption-wrapper {
position: absolute;
bottom: 140px;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
opacity: 0;
visibility: hidden;
will-change: opacity;
}
.caption-line {
position: relative;
width: 120px;
height: 1px;
background: rgba(245, 240, 232, 0.4);
transform: scaleX(0);
will-change: transform;
}
.caption-text {
font-family: 'Inter', system-ui, sans-serif;
font-weight: 300;
font-style: italic;
font-size: 48px;
line-height: 1.4;
color: #f5f0e8;
letter-spacing: 4px;
text-align: center;
opacity: 0;
will-change: opacity, letter-spacing;
font-variant-numeric: tabular-nums;
}
.driver {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
opacity: 0;
}
</style>
</head>
<body>
<div
id="captions-cinematic"
data-composition-id="captions-cinematic"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="13"
data-root="true"
>
<div class="driver" data-track-index="0"></div>
</div>
<script>
(function () {
var GROUPS = [
{ text: "Write HTML", start: 0.3, end: 1.8 },
{ text: "render video", start: 2.0, end: 3.5 },
{ text: "No timeline editors", start: 3.8, end: 5.5 },
{ text: "no After Effects", start: 5.7, end: 7.2 },
{ text: "Ship 10x faster", start: 7.5, end: 9.5 },
{ text: "with HyperFrames", start: 9.8, end: 12.0 }
];
var root = document.getElementById("captions-cinematic");
// Build caption group elements: line + text
GROUPS.forEach(function (group, gi) {
var wrapper = document.createElement("div");
wrapper.className = "caption-wrapper";
wrapper.id = "cg-wrap-" + gi;
var line = document.createElement("div");
line.className = "caption-line";
line.id = "cg-line-" + gi;
var text = document.createElement("div");
text.className = "caption-text";
text.id = "cg-" + gi;
text.textContent = group.text;
wrapper.appendChild(line);
wrapper.appendChild(text);
root.appendChild(wrapper);
});
// Build timeline
var tl = gsap.timeline({ paused: true });
GROUPS.forEach(function (group, gi) {
var wrapEl = "#cg-wrap-" + gi;
var lineEl = "#cg-line-" + gi;
var textEl = "#cg-" + gi;
// Show wrapper
tl.set(wrapEl, { opacity: 1, visibility: "visible" }, group.start);
// Line extends from center
tl.fromTo(lineEl,
{ scaleX: 0 },
{ scaleX: 1, duration: 0.4, ease: "power2.out", transformOrigin: "center" },
group.start
);
// Text fades in with letter-spacing settling
tl.fromTo(textEl,
{ opacity: 0, letterSpacing: "8px" },
{ opacity: 1, letterSpacing: "4px", duration: 0.6, ease: "power1.out" },
group.start + 0.1
);
// Text fades out slowly
tl.to(textEl,
{ opacity: 0, duration: 0.4, ease: "power1.in" },
group.end - 0.4
);
// Line contracts
tl.to(lineEl,
{ scaleX: 0, duration: 0.3, ease: "power2.in" },
group.end - 0.3
);
// Hard kill
tl.set(wrapEl, { opacity: 0, visibility: "hidden" }, group.end);
tl.set(textEl, { opacity: 0 }, group.end);
tl.set(lineEl, { scaleX: 0 }, group.end);
});
window.__timelines = window.__timelines || [];
window.__timelines.push(tl);
})();
</script>
</body>
</html>
@@ -0,0 +1,14 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "captions-cinematic",
"type": "hyperframes:block",
"title": "Captions Cinematic",
"description": "Elegant film-title captions with slow fade, letter-spacing animation, warm tint, and extending horizontal accent line",
"stability": "stable",
"dimensions": { "width": 1920, "height": 1080 },
"duration": 13,
"tags": ["captions", "cinematic"],
"files": [
{ "path": "captions-cinematic.html", "target": "compositions/captions-cinematic.html", "type": "hyperframes:composition" }
]
}
+171
View File
@@ -0,0 +1,171 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Captions Karaoke</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;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: #000;
}
#captions-karaoke {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: linear-gradient(180deg, #0b0d15 0%, #101320 50%, #0b0d15 100%);
}
#captions-karaoke::before {
content: '';
position: absolute;
bottom: -60px;
left: 50%;
transform: translateX(-50%);
width: 800px;
height: 400px;
background: radial-gradient(ellipse, rgba(139, 92, 246, 0.05) 0%, transparent 70%);
pointer-events: none;
}
.caption-group {
position: absolute;
bottom: 120px;
left: 50%;
transform: translateX(-50%);
text-align: center;
font-family: 'Inter', system-ui, sans-serif;
font-weight: 700;
font-size: 72px;
line-height: 1.2;
white-space: nowrap;
opacity: 0;
visibility: hidden;
will-change: opacity;
font-variant-numeric: tabular-nums;
/* Frosted glass pill */
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 24px;
padding: 24px 56px;
}
.caption-group .word {
display: inline-block;
color: rgba(255, 255, 255, 0.3);
margin: 0 8px;
will-change: color, transform;
transform-origin: center bottom;
}
.driver {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
opacity: 0;
}
</style>
</head>
<body>
<div
id="captions-karaoke"
data-composition-id="captions-karaoke"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="13"
data-root="true"
>
<div class="driver" data-track-index="0"></div>
</div>
<script>
(function () {
var GROUPS = [
{ text: "Write HTML", start: 0.3, end: 1.8 },
{ text: "render video", start: 2.0, end: 3.5 },
{ text: "No timeline editors", start: 3.8, end: 5.5 },
{ text: "no After Effects", start: 5.7, end: 7.2 },
{ text: "Ship 10x faster", start: 7.5, end: 9.5 },
{ text: "with HyperFrames", start: 9.8, end: 12.0 }
];
var root = document.getElementById("captions-karaoke");
// Build caption group elements with per-word spans
GROUPS.forEach(function (group, gi) {
var div = document.createElement("div");
div.className = "caption-group";
div.id = "cg-" + gi;
var words = group.text.split(" ");
words.forEach(function (word, wi) {
var span = document.createElement("span");
span.className = "word";
span.id = "cg-" + gi + "-w" + wi;
span.textContent = word;
div.appendChild(span);
});
root.appendChild(div);
});
// Build timeline
var tl = gsap.timeline({ paused: true });
GROUPS.forEach(function (group, gi) {
var el = "#cg-" + gi;
var words = group.text.split(" ");
var totalWords = words.length;
var groupDuration = group.end - group.start;
// Show group (dimmed words visible)
tl.set(el, { opacity: 1, visibility: "visible" }, group.start);
// Highlight each word as its time arrives
words.forEach(function (word, wi) {
var wordStart = group.start + (wi / totalWords) * groupDuration;
var wordEl = "#cg-" + gi + "-w" + wi;
tl.to(wordEl,
{ color: "#ffffff", scale: 1.05, duration: 0.15, ease: "power2.out" },
wordStart
);
});
// Fade out group
tl.to(el,
{ opacity: 0, duration: 0.15, ease: "power2.in" },
group.end - 0.15
);
// Hard kill: reset all words and hide group
words.forEach(function (word, wi) {
var wordEl = "#cg-" + gi + "-w" + wi;
tl.set(wordEl, { color: "rgba(255,255,255,0.3)", scale: 1 }, group.end);
});
tl.set(el, { opacity: 0, visibility: "hidden" }, group.end);
});
window.__timelines = window.__timelines || [];
window.__timelines.push(tl);
})();
</script>
</body>
</html>
@@ -0,0 +1,14 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "captions-karaoke",
"type": "hyperframes:block",
"title": "Captions Karaoke",
"description": "Word-by-word color reveal with frosted glass pill background, dimmed-to-highlighted karaoke progression",
"stability": "stable",
"dimensions": { "width": 1920, "height": 1080 },
"duration": 13,
"tags": ["captions", "karaoke"],
"files": [
{ "path": "captions-karaoke.html", "target": "compositions/captions-karaoke.html", "type": "hyperframes:composition" }
]
}
+134
View File
@@ -0,0 +1,134 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Captions Minimal</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&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: #000;
}
#captions-minimal {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: linear-gradient(180deg, #0c0e16 0%, #10121a 50%, #0c0e16 100%);
}
#captions-minimal::before {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 300px;
background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
pointer-events: none;
}
.caption-group {
position: absolute;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
text-align: center;
font-family: 'Inter', system-ui, sans-serif;
font-weight: 500;
font-size: 56px;
line-height: 1.3;
color: #ffffff;
white-space: nowrap;
opacity: 0;
visibility: hidden;
will-change: transform, opacity;
font-variant-numeric: tabular-nums;
/* Dark pill */
background: rgba(0, 0, 0, 0.55);
border-radius: 12px;
padding: 16px 48px;
}
.driver {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
opacity: 0;
}
</style>
</head>
<body>
<div
id="captions-minimal"
data-composition-id="captions-minimal"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="13"
data-root="true"
>
<div class="driver" data-track-index="0"></div>
</div>
<script>
(function () {
var GROUPS = [
{ text: "Write HTML", start: 0.3, end: 1.8 },
{ text: "render video", start: 2.0, end: 3.5 },
{ text: "No timeline editors", start: 3.8, end: 5.5 },
{ text: "no After Effects", start: 5.7, end: 7.2 },
{ text: "Ship 10x faster", start: 7.5, end: 9.5 },
{ text: "with HyperFrames", start: 9.8, end: 12.0 }
];
var root = document.getElementById("captions-minimal");
// Build caption group elements
GROUPS.forEach(function (group, gi) {
var div = document.createElement("div");
div.className = "caption-group";
div.id = "cg-" + gi;
div.textContent = group.text;
root.appendChild(div);
});
// Build timeline
var tl = gsap.timeline({ paused: true });
GROUPS.forEach(function (group, gi) {
var el = "#cg-" + gi;
// Fade in with slight upward motion
tl.set(el, { visibility: "visible" }, group.start);
tl.from(el,
{ y: 15, opacity: 0, duration: 0.3, ease: "power2.out" },
group.start
);
// Fade out with slight upward drift
tl.to(el,
{ y: -5, opacity: 0, duration: 0.2, ease: "power2.in" },
group.end - 0.2
);
tl.set(el, { opacity: 0, visibility: "hidden" }, group.end);
});
window.__timelines = window.__timelines || [];
window.__timelines.push(tl);
})();
</script>
</body>
</html>
@@ -0,0 +1,14 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "captions-minimal",
"type": "hyperframes:block",
"title": "Captions Minimal",
"description": "Clean, professional Netflix-subtitle style captions with soft fade and y-offset on a dark pill background",
"stability": "stable",
"dimensions": { "width": 1920, "height": 1080 },
"duration": 13,
"tags": ["captions", "minimal"],
"files": [
{ "path": "captions-minimal.html", "target": "compositions/captions-minimal.html", "type": "hyperframes:composition" }
]
}
+161
View File
@@ -0,0 +1,161 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Captions Slam</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;700;900&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: #000;
}
#captions-slam {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: linear-gradient(180deg, #0a0c14 0%, #0d1020 50%, #0a0c14 100%);
}
/* Subtle radial glow in center-bottom */
#captions-slam::before {
content: '';
position: absolute;
bottom: -100px;
left: 50%;
transform: translateX(-50%);
width: 900px;
height: 500px;
background: radial-gradient(ellipse, rgba(0, 212, 255, 0.04) 0%, transparent 70%);
pointer-events: none;
}
.caption-group {
position: absolute;
bottom: 140px;
left: 0;
right: 0;
text-align: center;
font-family: 'Inter', system-ui, sans-serif;
font-weight: 900;
font-size: 96px;
line-height: 1.1;
text-transform: uppercase;
color: #ffffff;
text-shadow: 0 0 40px rgba(255, 255, 255, 0.15), 0 0 80px rgba(255, 255, 255, 0.05);
letter-spacing: 2px;
opacity: 0;
visibility: hidden;
will-change: transform, opacity;
font-variant-numeric: tabular-nums;
}
.caption-group .word-accent {
color: #00d4ff;
display: inline-block;
transform: scale(1.3);
text-shadow: 0 0 30px rgba(0, 212, 255, 0.4), 0 0 60px rgba(0, 212, 255, 0.15);
}
.caption-group .word-gradient {
display: inline;
background: linear-gradient(90deg, #00d4ff 0%, #a855f7 50%, #ec4899 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Driver clip */
.driver {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
opacity: 0;
}
</style>
</head>
<body>
<div
id="captions-slam"
data-composition-id="captions-slam"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="13"
data-root="true"
>
<div class="driver" data-track-index="0"></div>
</div>
<script>
(function () {
var GROUPS = [
{ text: "Write HTML", start: 0.3, end: 1.8 },
{ text: "render video", start: 2.0, end: 3.5 },
{ text: "No timeline editors", start: 3.8, end: 5.5 },
{ text: "no After Effects", start: 5.7, end: 7.2 },
{ text: "Ship 10x faster", start: 7.5, end: 9.5 },
{ text: "with HyperFrames", start: 9.8, end: 12.0 }
];
var root = document.getElementById("captions-slam");
// Build caption group elements
GROUPS.forEach(function (group, gi) {
var div = document.createElement("div");
div.className = "caption-group";
div.id = "cg-" + gi;
// Apply per-word styling
var words = group.text.split(" ");
var html = words.map(function (word) {
if (word === "10x") {
return '<span class="word-accent">' + word + '</span>';
}
if (word === "HyperFrames") {
return '<span class="word-gradient">' + word + '</span>';
}
return word;
}).join(" ");
div.innerHTML = html;
root.appendChild(div);
});
// Build timeline
var tl = gsap.timeline({ paused: true });
GROUPS.forEach(function (group, gi) {
var el = "#cg-" + gi;
tl.fromTo(el,
{ scale: 0, opacity: 0, visibility: "visible" },
{ scale: 1, opacity: 1, duration: 0.15, ease: "back.out(1.7)" },
group.start
);
tl.to(el,
{ scale: 1.05, opacity: 0, duration: 0.12, ease: "power2.in" },
group.end - 0.12
);
tl.set(el, { opacity: 0, visibility: "hidden" }, group.end);
});
window.__timelines = window.__timelines || [];
window.__timelines.push(tl);
})();
</script>
</body>
</html>
@@ -0,0 +1,14 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "captions-slam",
"type": "hyperframes:block",
"title": "Captions Slam",
"description": "Bold, punchy slam captions with back-eased overshoot scale-in, uppercase Inter 900, and per-word accent styling",
"stability": "stable",
"dimensions": { "width": 1920, "height": 1080 },
"duration": 13,
"tags": ["captions", "slam"],
"files": [
{ "path": "captions-slam.html", "target": "compositions/captions-slam.html", "type": "hyperframes:composition" }
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.
Binary file not shown.
@@ -0,0 +1,41 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "vfx-iphone-device",
"type": "hyperframes:block",
"title": "iPhone & MacBook 3D Showcase",
"description": "Real GLTF iPhone 15 Pro Max and MacBook Pro models with live HTML-in-Canvas screen content, morphing glass lens, product review camera choreography, and 360° turntable.",
"stability": "experimental",
"dimensions": {
"width": 1920,
"height": 1080
},
"duration": 15,
"tags": ["vfx", "3d", "device", "iphone", "macbook", "gltf", "html-in-canvas"],
"files": [
{
"path": "vfx-iphone-device.html",
"target": "compositions/vfx-iphone-device.html",
"type": "hyperframes:composition"
},
{
"path": "models/iphone.glb",
"target": "models/iphone.glb",
"type": "asset"
},
{
"path": "models/macbook.glb",
"target": "models/macbook.glb",
"type": "asset"
},
{
"path": "models/hyperframes-mobile.png",
"target": "models/hyperframes-mobile.png",
"type": "asset"
},
{
"path": "models/hyperframes-desktop.png",
"target": "models/hyperframes-desktop.png",
"type": "asset"
}
]
}
+402
View File
@@ -0,0 +1,402 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1920, height=1080">
<title>Devices Canvas — HTML-in-Canvas API Showcase</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>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/loaders/DRACOLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/loaders/GLTFLoader.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#050508;overflow:hidden}
#root{position:relative;width:1920px;height:1080px;overflow:hidden;background:#050508}
</style>
</head>
<body>
<div id="root" data-composition-id="devices-canvas" data-width="1920" data-height="1080" data-start="0" data-duration="15" data-root="true">
<!-- LIVE HTML captured every frame via drawElementImage — Remotion can't nest these -->
<canvas id="cap-phone" layoutsubtree width="390" height="844" style="position:absolute;top:0;left:0;width:390px;height:844px;z-index:-1;pointer-events:none">
<div id="phone-html" style="width:390px;height:844px;overflow:hidden;font-family:Inter,system-ui,sans-serif;color:#e2e8f0">
<div id="phone-scroll" style="background:linear-gradient(180deg,#0a0a12,#0f0f1a);transform:translateY(0)">
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid rgba(255,255,255,0.06)"><span style="font-weight:800;font-size:14px;background:linear-gradient(135deg,#34d399,#3b82f6);-webkit-background-clip:text;-webkit-text-fill-color:transparent">HyperFrames</span><span style="font-size:10px;font-weight:600;padding:5px 12px;border-radius:6px;background:#10b981;color:#fff">Log in</span></div>
<div id="phone-title" style="font-size:26px;font-weight:800;letter-spacing:-0.5px;padding:18px 18px 6px;line-height:1.1;opacity:0">Community Playground</div>
<div style="font-size:11px;color:rgba(255,255,255,0.4);padding:0 18px 14px">Made with HyperFrames</div>
<div style="display:flex;gap:6px;padding:0 18px 14px"><span style="font-size:9px;font-weight:600;padding:4px 10px;border-radius:16px;background:rgba(16,185,129,0.15);color:#10b981;border:1px solid rgba(16,185,129,0.3)">Examples</span><span id="phone-count" style="font-size:9px;font-weight:600;padding:4px 10px;border-radius:16px;border:1px solid rgba(255,255,255,0.1);color:rgba(255,255,255,0.5)">Catalog 0</span></div>
<div id="phone-cards" style="display:grid;gap:10px;padding:0 18px 18px"></div>
</div>
</div>
</canvas>
<canvas id="cap-laptop" layoutsubtree width="1440" height="900" style="position:absolute;top:0;left:0;width:1440px;height:900px;z-index:-1;pointer-events:none">
<div id="laptop-html" style="width:1440px;height:900px;overflow:hidden;font-family:Inter,system-ui,sans-serif;color:#e2e8f0">
<div id="laptop-scroll" style="background:linear-gradient(180deg,#0a0a12,#0f0f1a);transform:translateY(0)">
<div style="display:flex;align-items:center;justify-content:space-between;padding:18px 36px;border-bottom:1px solid rgba(255,255,255,0.06)"><span style="font-weight:800;font-size:17px;background:linear-gradient(135deg,#34d399,#3b82f6);-webkit-background-clip:text;-webkit-text-fill-color:transparent">HyperFrames</span><div style="display:flex;gap:20px;font-size:12px;color:rgba(255,255,255,0.5)"><span>Docs</span><span>Playground</span><span>Pricing</span></div><span style="font-size:11px;font-weight:600;padding:6px 14px;border-radius:6px;background:#10b981;color:#fff">Log in</span></div>
<div id="laptop-title" style="font-size:40px;font-weight:800;letter-spacing:-1px;padding:28px 36px 10px;opacity:0">Community Playground</div>
<div style="font-size:13px;color:rgba(255,255,255,0.4);padding:0 36px 18px">Create videos with HTML — powered by HyperFrames</div>
<div style="display:flex;gap:8px;padding:0 36px 20px"><span style="font-size:10px;font-weight:600;padding:5px 12px;border-radius:16px;background:rgba(16,185,129,0.15);color:#10b981;border:1px solid rgba(16,185,129,0.3)">Examples</span><span id="laptop-count" style="font-size:10px;font-weight:600;padding:5px 12px;border-radius:16px;border:1px solid rgba(255,255,255,0.1);color:rgba(255,255,255,0.5)">Catalog 0</span></div>
<div id="laptop-cards" style="display:grid;grid-template-columns:repeat(3,1fr);gap:14px;padding:0 36px 36px"></div>
</div>
</div>
</canvas>
<canvas id="theater" width="1920" height="1080" style="position:absolute;top:0;left:0;width:1920px;height:1080px"></canvas>
<div id="driver" class="clip" data-start="0" data-duration="15" data-track-index="0" style="position:absolute;width:1px;height:1px;opacity:0;pointer-events:none"></div>
</div>
<script>
var W = 1920, H = 1080, PI = Math.PI;
// ── Renderer ───────────────────────────────────────────────────────
var renderer = new THREE.WebGLRenderer({ canvas: document.getElementById("theater"), antialias: true, preserveDrawingBuffer: true });
renderer.setSize(W, H, false); renderer.setPixelRatio(1);
renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.15;
renderer.outputEncoding = THREE.sRGBEncoding; renderer.setClearColor(0x050508, 1);
var scene = new THREE.Scene(); scene.background = new THREE.Color(0x050508);
var camera = new THREE.PerspectiveCamera(34, W / H, 0.1, 100);
// ── Environment Map ────────────────────────────────────────────────
var eC = document.createElement("canvas"); eC.width = 1024; eC.height = 512;
var ec = eC.getContext("2d");
var eg = ec.createLinearGradient(0, 0, 1024, 512);
eg.addColorStop(0, "#020307"); eg.addColorStop(0.15, "#1a1a2e"); eg.addColorStop(0.3, "#ffffff");
eg.addColorStop(0.45, "#0a0a1e"); eg.addColorStop(0.6, "#ffffff"); eg.addColorStop(0.75, "#1e1e2e");
eg.addColorStop(1, "#020307"); ec.fillStyle = eg; ec.fillRect(0, 0, 1024, 512);
ec.globalCompositeOperation = "screen";
for (var ei = 0; ei < 7; ei++) {
ec.fillStyle = "rgba(255,255,255," + (ei % 2 === 0 ? 0.15 : 0.08) + ")";
ec.fillRect((ei % 3) * 100, 60 + ei * 56, 700, 3 + (ei % 2) * 2);
}
var eT = new THREE.CanvasTexture(eC); eT.mapping = THREE.EquirectangularReflectionMapping;
var pm = new THREE.PMREMGenerator(renderer);
scene.environment = pm.fromEquirectangular(eT).texture; eT.dispose(); pm.dispose();
// ── Background ─────────────────────────────────────────────────────
scene.add(new THREE.Mesh(new THREE.PlaneGeometry(22, 14), new THREE.ShaderMaterial({
uniforms: {},
vertexShader: "varying vec2 v;void main(){v=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}",
fragmentShader: "varying vec2 v;void main(){vec3 c=vec3(0.02,0.02,0.03);c+=vec3(0.05,0.02,0.1)*exp(-pow(length(v-vec2(0.3,0.55)),2.0)*5.0);c+=vec3(0.02,0.05,0.1)*exp(-pow(length(v-vec2(0.7,0.45)),2.0)*5.0);gl_FragColor=vec4(c,1.0);}",
depthWrite: false
}))).position.z = -6;
var floor = new THREE.Mesh(new THREE.PlaneGeometry(30, 30), new THREE.MeshStandardMaterial({ color: 0x050508, metalness: 0.95, roughness: 0.18 }));
floor.rotation.x = -PI / 2; floor.position.y = -2.5; scene.add(floor);
scene.add(new THREE.AmbientLight(0x222233, 0.5));
var kL = new THREE.DirectionalLight(0xffffff, 2.2); kL.position.set(-3, 4, 5); scene.add(kL);
var rL = new THREE.PointLight(0x6366f1, 3, 15); rL.position.set(4, 2, 3); scene.add(rL);
var wL = new THREE.PointLight(0xffc8a8, 1.5, 12); wL.position.set(-3, -1, 4); scene.add(wL);
// ── Live HTML → WebGL Texture (drawElementImage, Remotion can't nest these) ─
var capPhoneCvs = document.getElementById("cap-phone");
var capPhoneCtx = capPhoneCvs.getContext("2d");
var phoneHtmlEl = document.getElementById("phone-html");
var phoneScrollEl = document.getElementById("phone-scroll");
var capLapCvs = document.getElementById("cap-laptop");
var capLapCtx = capLapCvs.getContext("2d");
var lapHtmlEl = document.getElementById("laptop-html");
var lapScrollEl = document.getElementById("laptop-scroll");
var apiOk = (function() {
var tc = document.createElement("canvas");
if (!("layoutSubtree" in tc)) return false;
tc.setAttribute("layoutsubtree", "");
var ctx = tc.getContext("2d");
return ctx && typeof ctx.drawElementImage === "function";
})();
var paintReady = false;
// Generate card HTML for both screens
var cardData = [
{ bg: "linear-gradient(135deg,#1e3a5f,#2563eb)", text: "100M+ AI team", title: "Notion Showcase", meta: "15s · 1920×1080" },
{ bg: "linear-gradient(135deg,#4a1942,#db2777)", text: "Design System v4", title: "Dribbble Showcase", meta: "12s · 1920×1080" },
{ bg: "linear-gradient(135deg,#451a03,#f59e0b)", text: "$1.9T Market Cap", title: "Stripe Showcase", meta: "10s · 1920×1080" },
{ bg: "linear-gradient(135deg,#064e3b,#10b981)", text: "Ship faster with AI", title: "Vercel Promo", meta: "20s · 1080×1920" },
{ bg: "linear-gradient(135deg,#1e3a5f,#3b82f6)", text: "Privacy App", title: "iPhone App Store", meta: "8s · 1080×1920" },
{ bg: "linear-gradient(135deg,#4a1942,#a855f7)", text: "Audio Visualizer", title: "Spotify Remix", meta: "15s · 1920×1080" },
{ bg: "linear-gradient(135deg,#064e3b,#059669)", text: "Code Animation", title: "Terminal Demo", meta: "12s · 1920×1080" },
{ bg: "linear-gradient(135deg,#451a03,#d97706)", text: "Launch Video", title: "Product Hunt", meta: "30s · 1920×1080" },
{ bg: "linear-gradient(135deg,#1e3a5f,#6366f1)", text: "3D Devices", title: "Device Mockups", meta: "15s · 1920×1080" }
];
function makeCardHTML(cd, isMobile) {
var fs = isMobile ? 12 : 16;
return '<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:' + (isMobile ? 10 : 12) + 'px;overflow:hidden;opacity:0" class="anim-card">'
+ '<div style="width:100%;aspect-ratio:16/9;background:' + cd.bg + ';display:flex;align-items:center;justify-content:center"><span style="font-size:' + fs + 'px;font-weight:700;color:rgba(255,255,255,0.7)">' + cd.text + '</span></div>'
+ '<div style="padding:' + (isMobile ? '8px 10px' : '10px 14px') + '"><div style="font-size:' + (isMobile ? 11 : 13) + 'px;font-weight:600;margin-bottom:2px">' + cd.title + '</div><div style="font-size:' + (isMobile ? 9 : 10) + 'px;color:rgba(255,255,255,0.3)">' + cd.meta + '</div></div></div>';
}
var phoneCardsEl = document.getElementById("phone-cards");
var laptopCardsEl = document.getElementById("laptop-cards");
for (var ci = 0; ci < cardData.length; ci++) {
if (ci < 6) phoneCardsEl.innerHTML += makeCardHTML(cardData[ci], true);
laptopCardsEl.innerHTML += makeCardHTML(cardData[ci], false);
}
var phoneTex = new THREE.CanvasTexture(capPhoneCvs);
phoneTex.minFilter = THREE.LinearFilter; phoneTex.magFilter = THREE.LinearFilter; phoneTex.generateMipmaps = false;
var lapTex = new THREE.CanvasTexture(capLapCvs);
lapTex.minFilter = THREE.LinearFilter; lapTex.magFilter = THREE.LinearFilter; lapTex.generateMipmaps = false;
function captureScreens() {
if (!apiOk || !paintReady) return;
try {
capPhoneCtx.clearRect(0, 0, 390, 844);
capPhoneCtx.drawElementImage(phoneHtmlEl, 0, 0, 390, 844);
capLapCtx.clearRect(0, 0, 1440, 900);
capLapCtx.drawElementImage(lapHtmlEl, 0, 0, 1440, 900);
phoneTex.needsUpdate = true;
lapTex.needsUpdate = true;
} catch (e) {}
}
// ── Turntable Group (both devices rotate together for 360° orbit) ──
var turntable = new THREE.Group();
scene.add(turntable);
// ── Model Groups ───────────────────────────────────────────────────
var phoneGrp = new THREE.Group(); turntable.add(phoneGrp);
var lapGrp = new THREE.Group(); turntable.add(lapGrp);
var ready = 0;
var draco = new THREE.DRACOLoader();
draco.setDecoderPath("https://www.gstatic.com/draco/versioned/decoders/1.5.6/");
var loader = new THREE.GLTFLoader();
loader.setDRACOLoader(draco);
// ── Morphing Glass Lens (single, shared between devices) ───────────
var glassMat = new THREE.MeshPhysicalMaterial({
color: 0xffffff, metalness: 0, roughness: 0.02,
transmission: 1, thickness: 1.8, ior: 1.45,
attenuationColor: new THREE.Color(0xe8f4ff), attenuationDistance: 10,
clearcoat: 1, clearcoatRoughness: 0.03,
specularIntensity: 1, envMapIntensity: 2.5,
transparent: true, opacity: 0.85, side: THREE.DoubleSide
});
var glassGeos = [
new THREE.SphereGeometry(0.22, 32, 32),
new THREE.IcosahedronGeometry(0.22, 1),
new THREE.OctahedronGeometry(0.25, 0),
new THREE.DodecahedronGeometry(0.22, 0),
new THREE.TetrahedronGeometry(0.28, 0),
new THREE.TorusGeometry(0.16, 0.07, 16, 32)
];
var glassLens = new THREE.Mesh(glassGeos[0], glassMat);
glassLens.renderOrder = 10;
// iPhone
loader.load("models/iphone.glb", function(gltf) {
var m = gltf.scene;
var box = new THREE.Box3().setFromObject(m);
var sz = box.getSize(new THREE.Vector3());
var s = 3.2 / sz.y; m.scale.setScalar(s);
box.setFromObject(m); m.position.sub(box.getCenter(new THREE.Vector3()));
m.traverse(function(child) {
if (!child.isMesh) return;
if (child.material && child.material.emissiveMap) {
child.material = new THREE.MeshBasicMaterial({ map: phoneTex });
}
});
scene.add(glassLens);
phoneGrp.add(m);
ready++; if (ready === 2) onReady();
});
// MacBook
loader.load("models/macbook.glb", function(gltf) {
var m = gltf.scene;
var box = new THREE.Box3().setFromObject(m);
var sz = box.getSize(new THREE.Vector3());
var s = 4.5 / Math.max(sz.x, sz.z); m.scale.setScalar(s);
box.setFromObject(m); m.position.sub(box.getCenter(new THREE.Vector3()));
m.traverse(function(child) {
if (child.isMesh && (child.name === "matte" || child.name === "Matte")) {
child.material = new THREE.MeshBasicMaterial({ map: lapTex });
}
});
// Apple logo on back of lid — exact coords from model inspection:
// back mesh center: (0, 0.04, -1.54), matte center: (0, 0.15, -1.56)
// Back surface faces -Z. Logo at z = -1.63 (outside back surface), facing -Z.
var lc = document.createElement("canvas"); lc.width = 256; lc.height = 256;
var lx = lc.getContext("2d"); lx.clearRect(0, 0, 256, 256);
lx.save(); lx.translate(128, 128); lx.scale(11, 11); lx.translate(-12, -13);
lx.fillStyle = "#b8b8bc";
lx.fill(new Path2D("M18.71,19.5C17.88,20.49 17,21.4 15.66,21.42C14.29,21.45 13.73,20.56 12.18,20.56C10.63,20.56 9.99,21.4 8.74,21.45C7.44,21.5 6.47,20.36 5.62,19.39C3.89,17.39 2.59,13.78 4.37,11.37C5.25,10.17 6.61,9.42 8.08,9.4C9.4,9.37 10.62,10.35 11.43,10.35C12.24,10.35 13.72,9.17 15.32,9.35C15.98,9.38 17.66,9.61 18.72,11.17C18.62,11.23 16.39,12.52 16.42,15.19C16.45,18.37 19.18,19.35 19.21,19.36ZM15.23,7.7C15.96,6.82 16.43,5.6 16.31,4.38C15.25,4.42 13.97,5.09 13.22,5.96C12.55,6.74 11.98,7.99 12.12,9.18C13.29,9.27 14.5,8.57 15.23,7.7Z"));
lx.restore();
var logoTex = new THREE.CanvasTexture(lc); logoTex.minFilter = THREE.LinearFilter;
var appleLogoMesh = new THREE.Mesh(
new THREE.PlaneGeometry(0.55, 0.55),
new THREE.MeshBasicMaterial({ map: logoTex, transparent: true, depthWrite: false, side: THREE.DoubleSide })
);
appleLogoMesh.position.set(0, 0.1, -1.63);
appleLogoMesh.rotation.y = PI;
m.add(appleLogoMesh);
lapGrp.add(m);
ready++; if (ready === 2) onReady();
});
function onReady() {
requestAnimationFrame(function() { requestAnimationFrame(function() {
paintReady = true;
captureScreens();
window.__timelines = window.__timelines || {};
window.__timelines["devices-canvas"] = tl;
}); });
}
// ── Cubic Bezier Implementation ────────────────────────────────────
function cbz(p1x, p1y, p2x, p2y) {
var ax = 3*p1x - 3*p2x + 1, bx = 3*p2x - 6*p1x, cx = 3*p1x;
var ay = 3*p1y - 3*p2y + 1, by = 3*p2y - 6*p1y, cy = 3*p1y;
return function(t) {
var s = t;
for (var i = 0; i < 12; i++) {
var x = ((ax*s + bx)*s + cx)*s - t;
var dx = (3*ax*s + 2*bx)*s + cx;
if (Math.abs(dx) < 1e-10) break;
s -= x / dx;
}
s = Math.max(0, Math.min(1, s));
return ((ay*s + by)*s + cy)*s;
};
}
var enterEase = cbz(0.16, 1.0, 0.3, 1.0);
var spinEase = cbz(0.76, 0.0, 0.18, 1.0);
var driftEase = cbz(0.45, 0.0, 0.15, 1.0);
var snapEase = cbz(0.22, 1.15, 0.36, 1.0);
var breatheEase = cbz(0.37, 0.0, 0.63, 1.0);
// ── Animation State ────────────────────────────────────────────────
var S = {
phX: 0, phY: 0.1, phZ: 0.5,
phRX: 0, phRY: PI, phRZ: 0,
lpX: 10, lpY: -0.2, lpZ: -0.6,
lpRX: 0, lpRY: 0, lpRZ: 0,
cX: 0, cY: 0.15, cZ: 2.8,
clX: 0, clY: 0.1, clZ: 0,
drift: 0, turn: 0, scroll: 0, cardReveal: 0, titleFade: 0, counter: 0, p: 0
};
function render() {
var d = S.drift;
// ── Animate live HTML content (captured to texture every frame) ──
// Title fade-in
document.getElementById("phone-title").style.opacity = S.titleFade;
document.getElementById("laptop-title").style.opacity = S.titleFade;
// Counter
var cnt = Math.round(S.counter);
document.getElementById("phone-count").textContent = "Catalog " + cnt;
document.getElementById("laptop-count").textContent = "Catalog " + cnt;
// Staggered card reveal
var allCards = document.querySelectorAll(".anim-card");
for (var ci2 = 0; ci2 < allCards.length; ci2++) {
var cardProgress = Math.max(0, Math.min(1, (S.cardReveal * allCards.length - ci2) * 1.5));
allCards[ci2].style.opacity = cardProgress;
allCards[ci2].style.transform = "translateY(" + ((1 - cardProgress) * 15) + "px)";
}
// Scroll
phoneScrollEl.style.transform = "translateY(-" + (S.scroll * 500) + "px)";
lapScrollEl.style.transform = "translateY(-" + (S.scroll * 300) + "px)";
// Re-capture HTML to texture
captureScreens();
phoneGrp.position.set(S.phX + Math.sin(d * 0.5) * 0.015, S.phY + Math.sin(d * 0.7) * 0.008, S.phZ + Math.cos(d * 0.4) * 0.008);
phoneGrp.rotation.set(S.phRX, S.phRY, S.phRZ);
lapGrp.position.set(S.lpX - Math.sin(d * 0.4) * 0.012, S.lpY + Math.sin(d * 0.6) * 0.006, S.lpZ + Math.cos(d * 0.35) * 0.008);
lapGrp.rotation.set(S.lpRX, S.lpRY, S.lpRZ);
turntable.rotation.y = S.turn;
// Single morphing glass — orbits around device edges, never overlaps screen
var t = tl.time();
var geoIdx = Math.floor(d * 0.8) % glassGeos.length;
if (glassLens.geometry !== glassGeos[geoIdx]) glassLens.geometry = glassGeos[geoIdx];
glassLens.rotation.set(d * 0.7, d * 0.9, d * 0.4);
// Glass glides across screen surface — hide during rotations to avoid clipping
glassLens.visible = (t < 3.3 || (t >= 9.5 && t < 12.5) || t >= 13.5);
var gx, gy, gz;
if (t < 7) {
// Slide across iPhone screen face
var px = phoneGrp.position.x, py = phoneGrp.position.y, pz = phoneGrp.position.z;
gx = px + Math.sin(d * 1.4) * 0.35;
gy = py + Math.cos(d * 1.1) * 0.6;
gz = pz + 0.15;
} else if (t < 11) {
// Slide across MacBook screen face
var lx2 = lapGrp.position.x, ly2 = lapGrp.position.y, lz2 = lapGrp.position.z;
gx = lx2 + Math.sin(d * 1.2) * 1.2;
gy = ly2 + 0.3 + Math.cos(d * 0.9) * 0.6;
gz = lz2 + 0.2;
} else {
// Glide between both screens
var mx = (phoneGrp.position.x + lapGrp.position.x) * 0.5;
var my = (phoneGrp.position.y + lapGrp.position.y) * 0.5;
gx = mx + Math.sin(d * 1.0) * 1.5;
gy = my + 0.2 + Math.cos(d * 0.8) * 0.5;
gz = 0.3;
}
glassLens.position.set(gx, gy, gz);
camera.position.set(S.cX, S.cY, S.cZ);
camera.lookAt(S.clX, S.clY, S.clZ);
renderer.render(scene, camera);
}
// ── GSAP Timeline — Product Review Edit ────────────────────────────
// __timelines created in onReady() after GLTF models load
var tl = gsap.timeline({ paused: true });
tl.to(S, { p: 1, drift: 15, duration: 15, ease: "none", onUpdate: render }, 0);
// ── Act 1 (03.5s): iPhone FRONT — hero close-up showing hyperframes.dev
tl.to(S, { phRY: PI + 0.06, phRX: 0.015, duration: 1.5, ease: breatheEase }, 0.3);
tl.to(S, { phRY: PI - 0.04, phRX: -0.01, duration: 1.5, ease: breatheEase }, 1.8);
// ── Act 2 (3.57s): iPhone spins 180° to show BACK (camera, titanium, Apple logo)
tl.to(S, { phRY: PI * 2, duration: 2.2, ease: spinEase }, 3.5);
tl.to(S, { cZ: 4.8, cY: 0.25, duration: 2, ease: driftEase }, 3.5);
// Hold back view — slight orbit to appreciate hardware
tl.to(S, { phRY: PI * 2 + 0.15, phRX: 0.04, cX: 0.15, duration: 1.5, ease: breatheEase }, 5.5);
// ── Act 3 (79s): iPhone spins back to front, then exits LEFT off screen
tl.to(S, { phRY: PI * 3, duration: 1.8, ease: snapEase }, 7);
tl.to(S, { cZ: 3.5, cY: 0.2, duration: 1.5, ease: driftEase }, 7);
tl.to(S, { phX: -6, phZ: 0.3, duration: 2, ease: spinEase }, 8);
// ── Act 4 (911s): MacBook enters from right, hero close-up
tl.to(S, { lpX: 0, duration: 2, ease: enterEase }, 9);
tl.to(S, { cX: 0, clX: 0, cZ: 5, cY: 0.3, clY: -0.1, duration: 2, ease: driftEase }, 9);
tl.to(S, { lpRY: 0.06, duration: 1.5, ease: breatheEase }, 10);
// ── Act 5 (1112.5s): iPhone returns, both settle for turntable
tl.to(S, { phX: -2.0, duration: 1.5, ease: enterEase }, 11);
tl.to(S, { lpX: 2.2, duration: 1.5, ease: breatheEase }, 11);
tl.to(S, { cX: 0.1, clX: 0.1, cZ: 7.5, cY: 0.5, clY: -0.1, duration: 1.5, ease: driftEase }, 11);
tl.to(S, { phRY: PI * 3, lpRY: 0, duration: 1, ease: breatheEase }, 11);
// ── Act 6 (12.515s): 360° turntable — both devices rotate together
tl.to(S, { turn: PI * 2, duration: 2.5, ease: spinEase }, 12.5);
// ── Live HTML Animations (captured to 3D textures every frame) ──
// Title fade in
tl.to(S, { titleFade: 1, duration: 0.8, ease: breatheEase }, 0.3);
// Counter animates up
tl.to(S, { counter: 43, duration: 2, ease: driftEase }, 0.5);
// Cards stagger in (0→1 reveals all cards sequentially)
tl.to(S, { cardReveal: 1, duration: 3, ease: breatheEase }, 0.8);
// Scroll during hero shots
tl.to(S, { scroll: 0.6, duration: 3, ease: breatheEase }, 1.5);
tl.to(S, { scroll: 0, duration: 1.5, ease: driftEase }, 7);
// Scroll on MacBook reveal
tl.to(S, { scroll: 0.8, duration: 3, ease: breatheEase }, 9.5);
tl.to(S, { scroll: 0.3, duration: 2, ease: driftEase }, 13);
// __timelines registered in onReady() after GLTF models load
</script>
</body>
</html>
@@ -0,0 +1,21 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "vfx-liquid-background",
"type": "hyperframes:block",
"title": "Liquid Background",
"description": "Organic liquid simulation with vertex displacement on a subdivided plane. HTML content floats above rippling fluid surface with real-time wave dynamics.",
"stability": "experimental",
"dimensions": {
"width": 1920,
"height": 1080
},
"duration": 12,
"tags": ["vfx", "liquid", "webgl", "displacement", "background"],
"files": [
{
"path": "vfx-liquid-background.html",
"target": "compositions/vfx-liquid-background.html",
"type": "hyperframes:composition"
}
]
}
@@ -0,0 +1,839 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
</head>
<body style="margin:0;overflow:hidden;">
<div
id="root"
data-composition-id="liquid-html"
data-start="0"
data-duration="12"
data-width="1920"
data-height="1080"
>
<!-- ═══════════════ PANEL A: Fintech Dashboard ═══════════════ -->
<div id="panel-a" data-panel-a style="position:absolute;left:0;top:0;width:1920px;height:1080px;opacity:0;pointer-events:none;">
<div style="width:100%;height:100%;background:#0a0e17;color:#e2e8f0;font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;padding:60px 80px;display:flex;flex-direction:column;">
<!-- Top bar -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:48px;">
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,#6366f1,#8b5cf6);"></div>
<span style="font-size:22px;font-weight:700;letter-spacing:-0.02em;">Vault</span>
</div>
<div style="display:flex;gap:32px;font-size:15px;color:#64748b;">
<span style="color:#e2e8f0;">Portfolio</span>
<span>Markets</span>
<span>Activity</span>
<span>Settings</span>
</div>
<div style="width:40px;height:40px;border-radius:50%;background:#1e293b;border:2px solid #334155;"></div>
</div>
<!-- Balance section -->
<div style="margin-bottom:40px;">
<div style="font-size:14px;color:#64748b;letter-spacing:0.08em;text-transform:uppercase;margin-bottom:8px;">Total Balance</div>
<div style="display:flex;align-items:baseline;gap:16px;">
<span style="font-size:72px;font-weight:800;letter-spacing:-0.03em;color:#f8fafc;">$2,847,391.24</span>
<span style="padding:6px 14px;border-radius:20px;background:rgba(34,197,94,0.15);color:#4ade80;font-size:16px;font-weight:600;">+12.4%</span>
</div>
</div>
<!-- Chart area -->
<div style="flex:1;min-height:0;margin-bottom:40px;position:relative;">
<svg viewBox="0 0 1760 360" style="width:100%;height:100%;" preserveAspectRatio="none">
<defs>
<linearGradient id="chart-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#6366f1" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#6366f1" stop-opacity="0"/>
</linearGradient>
</defs>
<path d="M0,280 C150,260 300,180 450,200 C600,220 700,120 880,140 C1060,160 1150,60 1320,80 C1490,100 1600,40 1760,30 L1760,360 L0,360 Z" fill="url(#chart-grad)"/>
<path d="M0,280 C150,260 300,180 450,200 C600,220 700,120 880,140 C1060,160 1150,60 1320,80 C1490,100 1600,40 1760,30" fill="none" stroke="#6366f1" stroke-width="3"/>
</svg>
</div>
<!-- Holdings row -->
<div style="display:flex;gap:24px;">
<div style="flex:1;background:#111827;border-radius:16px;padding:24px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:10px;">
<div style="width:32px;height:32px;border-radius:8px;background:#1e293b;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:#94a3b8;">AA</div>
<div><div style="font-weight:600;font-size:16px;">AAPL</div><div style="font-size:13px;color:#64748b;">Apple Inc.</div></div>
</div>
<span style="color:#4ade80;font-size:14px;font-weight:600;">+2.31%</span>
</div>
<div style="font-size:28px;font-weight:700;">$189.84</div>
</div>
<div style="flex:1;background:#111827;border-radius:16px;padding:24px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:10px;">
<div style="width:32px;height:32px;border-radius:8px;background:#1e293b;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:#94a3b8;">NV</div>
<div><div style="font-weight:600;font-size:16px;">NVDA</div><div style="font-size:13px;color:#64748b;">NVIDIA Corp.</div></div>
</div>
<span style="color:#4ade80;font-size:14px;font-weight:600;">+5.67%</span>
</div>
<div style="font-size:28px;font-weight:700;">$721.33</div>
</div>
<div style="flex:1;background:#111827;border-radius:16px;padding:24px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:10px;">
<div style="width:32px;height:32px;border-radius:8px;background:#1e293b;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:#94a3b8;">TS</div>
<div><div style="font-weight:600;font-size:16px;">TSLA</div><div style="font-size:13px;color:#64748b;">Tesla Inc.</div></div>
</div>
<span style="color:#f87171;font-size:14px;font-weight:600;">-1.42%</span>
</div>
<div style="font-size:28px;font-weight:700;">$248.50</div>
</div>
</div>
<!-- Bottom nav -->
<div style="display:flex;justify-content:center;gap:48px;margin-top:32px;padding-top:24px;border-top:1px solid #1e293b;">
<div style="display:flex;flex-direction:column;align-items:center;gap:4px;color:#6366f1;">
<div style="width:24px;height:24px;border-radius:6px;background:#6366f1;opacity:0.2;"></div>
<span style="font-size:12px;font-weight:500;">Home</span>
</div>
<div style="display:flex;flex-direction:column;align-items:center;gap:4px;color:#64748b;">
<div style="width:24px;height:24px;border-radius:6px;background:#334155;"></div>
<span style="font-size:12px;">Trade</span>
</div>
<div style="display:flex;flex-direction:column;align-items:center;gap:4px;color:#64748b;">
<div style="width:24px;height:24px;border-radius:6px;background:#334155;"></div>
<span style="font-size:12px;">Wallet</span>
</div>
<div style="display:flex;flex-direction:column;align-items:center;gap:4px;color:#64748b;">
<div style="width:24px;height:24px;border-radius:6px;background:#334155;"></div>
<span style="font-size:12px;">Profile</span>
</div>
</div>
</div>
</div>
<!-- ═══════════════ PANEL B: Social Feed ═══════════════ -->
<div id="panel-b" data-panel-b style="position:absolute;left:0;top:0;width:1920px;height:1080px;opacity:0;pointer-events:none;">
<div style="width:100%;height:100%;background:#0f1419;color:#e7e9ea;font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;padding:60px 80px;display:flex;flex-direction:column;">
<!-- Profile header -->
<div style="display:flex;align-items:center;gap:16px;margin-bottom:32px;padding-bottom:28px;border-bottom:1px solid #2f3336;">
<div style="width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,#1d9bf0,#7856ff);"></div>
<div>
<div style="font-size:22px;font-weight:700;">Sarah Chen</div>
<div style="font-size:16px;color:#71767b;">@sarahchen</div>
</div>
<div style="margin-left:auto;padding:8px 20px;border-radius:20px;background:#1d9bf0;color:#fff;font-size:15px;font-weight:600;">Follow</div>
</div>
<!-- Compose -->
<div style="display:flex;align-items:center;gap:16px;padding:20px 0;margin-bottom:24px;border-bottom:1px solid #2f3336;">
<div style="width:44px;height:44px;border-radius:50%;background:#1e293b;"></div>
<span style="font-size:20px;color:#71767b;">What's happening?</span>
</div>
<!-- Post 1 -->
<div style="padding:24px 0;border-bottom:1px solid #2f3336;">
<div style="display:flex;gap:14px;">
<div style="width:44px;height:44px;border-radius:50%;background:linear-gradient(135deg,#1d9bf0,#7856ff);flex-shrink:0;"></div>
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<span style="font-weight:700;font-size:16px;">Sarah Chen</span>
<span style="color:#71767b;font-size:15px;">@sarahchen · 2h</span>
</div>
<div style="font-size:17px;line-height:1.5;margin-bottom:14px;">Just shipped the new real-time collaboration feature. The CRDT sync engine handles 50k concurrent edits without breaking a sweat. This is what engineering craft looks like.</div>
<div style="display:flex;gap:48px;color:#71767b;font-size:14px;">
<span>42 replies</span>
<span style="color:#00ba7c;">1.2K retweets</span>
<span style="color:#f91880;">8.4K likes</span>
</div>
</div>
</div>
</div>
<!-- Post 2 -->
<div style="padding:24px 0;border-bottom:1px solid #2f3336;">
<div style="display:flex;gap:14px;">
<div style="width:44px;height:44px;border-radius:50%;background:linear-gradient(135deg,#ff6b35,#ffc107);flex-shrink:0;"></div>
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<span style="font-weight:700;font-size:16px;">Alex Rivera</span>
<span style="color:#71767b;font-size:15px;">@alexrivera · 4h</span>
</div>
<div style="font-size:17px;line-height:1.5;margin-bottom:14px;">The secret to great developer tools: make the easy things instant and the hard things possible. Stop adding config options and start removing them.</div>
<div style="display:flex;gap:48px;color:#71767b;font-size:14px;">
<span>18 replies</span>
<span style="color:#00ba7c;">847 retweets</span>
<span style="color:#f91880;">3.1K likes</span>
</div>
</div>
</div>
</div>
<!-- Post 3 -->
<div style="padding:24px 0;">
<div style="display:flex;gap:14px;">
<div style="width:44px;height:44px;border-radius:50%;background:linear-gradient(135deg,#10b981,#06b6d4);flex-shrink:0;"></div>
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<span style="font-weight:700;font-size:16px;">Maya Patel</span>
<span style="color:#71767b;font-size:15px;">@mayapatel · 6h</span>
</div>
<div style="font-size:17px;line-height:1.5;margin-bottom:14px;">Benchmarked our new rendering pipeline against the previous version. 4.7x faster on large scenes. Sometimes the best optimization is rewriting the architecture from scratch.</div>
<div style="display:flex;gap:48px;color:#71767b;font-size:14px;">
<span>31 replies</span>
<span style="color:#00ba7c;">2.3K retweets</span>
<span style="color:#f91880;">11K likes</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════ THREE.JS CANVAS ═══════════════ -->
<canvas id="liquid-canvas" class="clip" data-start="0" data-duration="12" data-track-index="0"
style="position:absolute;left:0;top:0;width:1920px;height:1080px;z-index:10;"></canvas>
<!-- ═══════════════ STYLES ═══════════════ -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=block');
[data-composition-id="liquid-html"] {
background: #000;
position: relative;
overflow: hidden;
}
</style>
<!-- ═══════════════ SCRIPTS ═══════════════ -->
<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>
<script>
window.__timelines = window.__timelines || {};
setTimeout(function() {
// ── Capture panel textures via html2canvas-like rasterization ──
// We use hidden canvases + drawImage of the panels for texture source.
// Since drawElementImage requires chrome flags, we rasterize the panels
// into canvas textures using SVG foreignObject serialization.
var W = 1920;
var H = 1080;
var canvas = document.getElementById('liquid-canvas');
// ── Rasterize an HTML panel into a canvas texture ──
function rasterizePanel(panelEl) {
return new Promise(function(resolve) {
// Clone the panel, make it visible for serialization
var clone = panelEl.cloneNode(true);
clone.style.opacity = '1';
clone.style.position = 'relative';
clone.style.left = '0';
clone.style.top = '0';
var svgNS = 'http://www.w3.org/2000/svg';
var xhtmlNS = 'http://www.w3.org/1999/xhtml';
var svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('width', W);
svg.setAttribute('height', H);
svg.setAttribute('xmlns', svgNS);
var fo = document.createElementNS(svgNS, 'foreignObject');
fo.setAttribute('width', '100%');
fo.setAttribute('height', '100%');
// Wrap in a body-like container with the font
var wrapper = document.createElementNS(xhtmlNS, 'div');
wrapper.setAttribute('xmlns', xhtmlNS);
wrapper.style.cssText = 'width:' + W + 'px;height:' + H + 'px;margin:0;padding:0;overflow:hidden;';
wrapper.innerHTML = clone.innerHTML;
fo.appendChild(wrapper);
svg.appendChild(fo);
var svgData = new XMLSerializer().serializeToString(svg);
var img = new Image();
var blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
var url = URL.createObjectURL(blob);
img.onload = function() {
var c = document.createElement('canvas');
c.width = W;
c.height = H;
var ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0, W, H);
URL.revokeObjectURL(url);
resolve(c);
};
img.onerror = function() {
// Fallback: solid color canvas
var c = document.createElement('canvas');
c.width = W;
c.height = H;
var ctx = c.getContext('2d');
ctx.fillStyle = panelEl.id === 'panel-a' ? '#0a0e17' : '#0f1419';
ctx.fillRect(0, 0, W, H);
URL.revokeObjectURL(url);
resolve(c);
};
img.src = url;
});
}
// ── Fallback: create panel textures from direct canvas drawing ──
function drawPanelATexture() {
var c = document.createElement('canvas');
c.width = W; c.height = H;
var ctx = c.getContext('2d');
// Background
ctx.fillStyle = '#0a0e17';
ctx.fillRect(0, 0, W, H);
// Logo
var grad = ctx.createLinearGradient(80, 60, 116, 96);
grad.addColorStop(0, '#6366f1');
grad.addColorStop(1, '#8b5cf6');
roundRect(ctx, 80, 60, 36, 36, 10, grad);
ctx.fillStyle = '#e2e8f0';
ctx.font = '700 22px Inter, sans-serif';
ctx.fillText('Vault', 126, 86);
// Nav
ctx.font = '400 15px Inter, sans-serif';
ctx.fillStyle = '#e2e8f0';
ctx.fillText('Portfolio', 600, 82);
ctx.fillStyle = '#64748b';
ctx.fillText('Markets', 700, 82);
ctx.fillText('Activity', 800, 82);
ctx.fillText('Settings', 900, 82);
// Balance label
ctx.fillStyle = '#64748b';
ctx.font = '400 14px Inter, sans-serif';
ctx.letterSpacing = '0.08em';
ctx.fillText('TOTAL BALANCE', 80, 150);
// Balance amount
ctx.fillStyle = '#f8fafc';
ctx.font = '800 72px Inter, sans-serif';
ctx.fillText('$2,847,391.24', 80, 230);
// Badge
ctx.fillStyle = 'rgba(34,197,94,0.15)';
roundRect(ctx, 780, 195, 100, 32, 16, 'rgba(34,197,94,0.15)');
ctx.fillStyle = '#4ade80';
ctx.font = '600 16px Inter, sans-serif';
ctx.fillText('+12.4%', 793, 217);
// Chart area - gradient fill
ctx.beginPath();
ctx.moveTo(80, 500);
ctx.bezierCurveTo(280, 480, 460, 400, 640, 420);
ctx.bezierCurveTo(820, 440, 920, 340, 1100, 360);
ctx.bezierCurveTo(1280, 380, 1370, 280, 1540, 300);
ctx.bezierCurveTo(1710, 320, 1780, 260, 1840, 250);
ctx.lineTo(1840, 600);
ctx.lineTo(80, 600);
ctx.closePath();
var chartGrad = ctx.createLinearGradient(0, 250, 0, 600);
chartGrad.addColorStop(0, 'rgba(99,102,241,0.4)');
chartGrad.addColorStop(1, 'rgba(99,102,241,0)');
ctx.fillStyle = chartGrad;
ctx.fill();
// Chart line
ctx.beginPath();
ctx.moveTo(80, 500);
ctx.bezierCurveTo(280, 480, 460, 400, 640, 420);
ctx.bezierCurveTo(820, 440, 920, 340, 1100, 360);
ctx.bezierCurveTo(1280, 380, 1370, 280, 1540, 300);
ctx.bezierCurveTo(1710, 320, 1780, 260, 1840, 250);
ctx.strokeStyle = '#6366f1';
ctx.lineWidth = 3;
ctx.stroke();
// Holding cards
var cardY = 660;
var cards = [
{ sym: 'AAPL', name: 'Apple Inc.', price: '$189.84', change: '+2.31%', color: '#4ade80' },
{ sym: 'NVDA', name: 'NVIDIA Corp.', price: '$721.33', change: '+5.67%', color: '#4ade80' },
{ sym: 'TSLA', name: 'Tesla Inc.', price: '$248.50', change: '-1.42%', color: '#f87171' }
];
var cardW = 540;
for (var i = 0; i < 3; i++) {
var cx = 80 + i * (cardW + 24);
roundRect(ctx, cx, cardY, cardW, 150, 16, '#111827');
ctx.fillStyle = '#1e293b';
roundRect(ctx, cx + 24, cardY + 24, 32, 32, 8, '#1e293b');
ctx.fillStyle = '#94a3b8';
ctx.font = '700 12px Inter, sans-serif';
ctx.fillText(cards[i].sym.substring(0, 2), cx + 32, cardY + 46);
ctx.fillStyle = '#e2e8f0';
ctx.font = '600 16px Inter, sans-serif';
ctx.fillText(cards[i].sym, cx + 68, cardY + 40);
ctx.fillStyle = '#64748b';
ctx.font = '400 13px Inter, sans-serif';
ctx.fillText(cards[i].name, cx + 68, cardY + 58);
ctx.fillStyle = cards[i].color;
ctx.font = '600 14px Inter, sans-serif';
ctx.fillText(cards[i].change, cx + cardW - 80, cardY + 44);
ctx.fillStyle = '#e2e8f0';
ctx.font = '700 28px Inter, sans-serif';
ctx.fillText(cards[i].price, cx + 24, cardY + 120);
}
// Bottom nav bar
ctx.strokeStyle = '#1e293b';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(80, 880);
ctx.lineTo(1840, 880);
ctx.stroke();
var navItems = ['Home', 'Trade', 'Wallet', 'Profile'];
var navCx = W / 2 - (navItems.length - 1) * 48 / 2;
for (var n = 0; n < navItems.length; n++) {
var nx = navCx + (n - 1.5) * 96;
ctx.fillStyle = n === 0 ? 'rgba(99,102,241,0.2)' : '#334155';
roundRect(ctx, nx - 12, 900, 24, 24, 6, ctx.fillStyle);
ctx.fillStyle = n === 0 ? '#6366f1' : '#64748b';
ctx.font = (n === 0 ? '500' : '400') + ' 12px Inter, sans-serif';
ctx.fillText(navItems[n], nx - 12, 944);
}
return c;
}
function drawPanelBTexture() {
var c = document.createElement('canvas');
c.width = W; c.height = H;
var ctx = c.getContext('2d');
// Background
ctx.fillStyle = '#0f1419';
ctx.fillRect(0, 0, W, H);
// Profile header
var profileGrad = ctx.createLinearGradient(80, 60, 136, 116);
profileGrad.addColorStop(0, '#1d9bf0');
profileGrad.addColorStop(1, '#7856ff');
ctx.beginPath();
ctx.arc(108, 88, 28, 0, Math.PI * 2);
ctx.fillStyle = profileGrad;
ctx.fill();
ctx.fillStyle = '#e7e9ea';
ctx.font = '700 22px Inter, sans-serif';
ctx.fillText('Sarah Chen', 150, 82);
ctx.fillStyle = '#71767b';
ctx.font = '400 16px Inter, sans-serif';
ctx.fillText('@sarahchen', 150, 104);
// Follow button
roundRect(ctx, 1720, 68, 100, 40, 20, '#1d9bf0');
ctx.fillStyle = '#fff';
ctx.font = '600 15px Inter, sans-serif';
ctx.fillText('Follow', 1744, 94);
// Separator
ctx.strokeStyle = '#2f3336';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(80, 140);
ctx.lineTo(1840, 140);
ctx.stroke();
// Compose
ctx.beginPath();
ctx.arc(102, 178, 22, 0, Math.PI * 2);
ctx.fillStyle = '#1e293b';
ctx.fill();
ctx.fillStyle = '#71767b';
ctx.font = '400 20px Inter, sans-serif';
ctx.fillText("What's happening?", 140, 184);
ctx.beginPath();
ctx.moveTo(80, 216);
ctx.lineTo(1840, 216);
ctx.strokeStyle = '#2f3336';
ctx.stroke();
// Posts
var posts = [
{
name: 'Sarah Chen', handle: '@sarahchen', time: '2h',
grad: ['#1d9bf0', '#7856ff'],
text: 'Just shipped the new real-time collaboration feature. The CRDT sync engine handles 50k concurrent edits without breaking a sweat.',
replies: '42', retweets: '1.2K', likes: '8.4K'
},
{
name: 'Alex Rivera', handle: '@alexrivera', time: '4h',
grad: ['#ff6b35', '#ffc107'],
text: 'The secret to great developer tools: make the easy things instant and the hard things possible. Stop adding config options.',
replies: '18', retweets: '847', likes: '3.1K'
},
{
name: 'Maya Patel', handle: '@mayapatel', time: '6h',
grad: ['#10b981', '#06b6d4'],
text: 'Benchmarked our new rendering pipeline against the previous version. 4.7x faster on large scenes. Architecture rewrite pays off.',
replies: '31', retweets: '2.3K', likes: '11K'
}
];
var py = 240;
for (var p = 0; p < posts.length; p++) {
var post = posts[p];
// Avatar
var ag = ctx.createLinearGradient(80, py + 4, 124, py + 48);
ag.addColorStop(0, post.grad[0]);
ag.addColorStop(1, post.grad[1]);
ctx.beginPath();
ctx.arc(102, py + 26, 22, 0, Math.PI * 2);
ctx.fillStyle = ag;
ctx.fill();
// Name and handle
ctx.fillStyle = '#e7e9ea';
ctx.font = '700 16px Inter, sans-serif';
ctx.fillText(post.name, 140, py + 20);
ctx.fillStyle = '#71767b';
ctx.font = '400 15px Inter, sans-serif';
ctx.fillText(post.handle + ' · ' + post.time, 140 + ctx.measureText(post.name).width + 10, py + 20);
// Post text
ctx.fillStyle = '#e7e9ea';
ctx.font = '400 17px Inter, sans-serif';
wrapText(ctx, post.text, 140, py + 50, 1680, 26);
// Engagement
var ey = py + 120;
ctx.font = '400 14px Inter, sans-serif';
ctx.fillStyle = '#71767b';
ctx.fillText(post.replies + ' replies', 140, ey);
ctx.fillStyle = '#00ba7c';
ctx.fillText(post.retweets + ' retweets', 340, ey);
ctx.fillStyle = '#f91880';
ctx.fillText(post.likes + ' likes', 560, ey);
// Separator
py += 160;
if (p < 2) {
ctx.beginPath();
ctx.moveTo(80, py);
ctx.lineTo(1840, py);
ctx.strokeStyle = '#2f3336';
ctx.stroke();
}
py += 20;
}
return c;
}
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
var words = text.split(' ');
var line = '';
for (var n = 0; n < words.length; n++) {
var testLine = line + words[n] + ' ';
if (ctx.measureText(testLine).width > maxWidth && n > 0) {
ctx.fillText(line.trim(), x, y);
line = words[n] + ' ';
y += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line.trim(), x, y);
}
function roundRect(ctx, x, y, w, h, r, fill) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
}
// ── Build textures (canvas-drawn fallback — works everywhere) ──
var canvasA = drawPanelATexture();
var canvasB = drawPanelBTexture();
// ═══════════════════════════════════════════════════════
// THREE.JS LIQUID SIMULATION
// ═══════════════════════════════════════════════════════
var renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: false
});
renderer.setSize(W, H, false);
renderer.setPixelRatio(1);
renderer.setClearColor(0x000000, 1);
var scene = new THREE.Scene();
// Camera: slight perspective to show surface deformation
var camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 100);
camera.position.set(0, 0, 2.8);
camera.lookAt(0, 0, 0);
// Create textures from panel canvases
var textureA = new THREE.CanvasTexture(canvasA);
textureA.minFilter = THREE.LinearFilter;
textureA.magFilter = THREE.LinearFilter;
var textureB = new THREE.CanvasTexture(canvasB);
textureB.minFilter = THREE.LinearFilter;
textureB.magFilter = THREE.LinearFilter;
// ── Shader material ──
var vertexShader = [
'uniform float uTime;',
'uniform float uWaveIntensity;',
'uniform float uRippleIntensity;',
'varying vec2 vUv;',
'varying float vDisplacement;',
'',
'void main() {',
' vUv = uv;',
' vec3 pos = position;',
'',
' // Wave 1: large slow horizontal wave',
' float wave1 = sin(pos.x * 3.0 + uTime * 1.2) * 0.04 * uWaveIntensity;',
'',
' // Wave 2: medium diagonal wave',
' float wave2 = sin((pos.x + pos.y) * 5.0 + uTime * 1.8) * 0.025 * uWaveIntensity;',
'',
' // Wave 3: small fast ripples',
' float wave3 = sin(pos.x * 12.0 - uTime * 3.0) * sin(pos.y * 10.0 + uTime * 2.5) * 0.015 * uRippleIntensity;',
'',
' float displacement = wave1 + wave2 + wave3;',
' pos.z += displacement;',
' vDisplacement = displacement;',
'',
' gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);',
'}'
].join('\n');
var fragmentShader = [
'uniform sampler2D uTextureA;',
'uniform sampler2D uTextureB;',
'uniform float uTime;',
'uniform float uMix;',
'uniform float uWaveIntensity;',
'uniform float uRippleIntensity;',
'varying vec2 vUv;',
'varying float vDisplacement;',
'',
'void main() {',
' // Distort UVs to match vertex displacement for consistency',
' vec2 distortedUv = vUv;',
' distortedUv.x += sin(vUv.y * 10.0 + uTime * 1.8) * 0.008 * uWaveIntensity;',
' distortedUv.y += sin(vUv.x * 8.0 + uTime * 1.2) * 0.006 * uWaveIntensity;',
'',
' // Small ripple distortion',
' distortedUv.x += sin(vUv.x * 20.0 - uTime * 3.0) * 0.003 * uRippleIntensity;',
' distortedUv.y += cos(vUv.y * 18.0 + uTime * 2.5) * 0.003 * uRippleIntensity;',
'',
' vec4 colorA = texture2D(uTextureA, distortedUv);',
' vec4 colorB = texture2D(uTextureB, distortedUv);',
'',
' // Crossfade between panels',
' vec4 color = mix(colorA, colorB, uMix);',
'',
' // Subtle specular highlight from displacement',
' float highlight = vDisplacement * 3.0;',
' color.rgb += vec3(highlight * 0.15, highlight * 0.12, highlight * 0.2);',
'',
' // Slight vignette',
' float vignette = 1.0 - 0.3 * length((vUv - 0.5) * 1.4);',
' color.rgb *= vignette;',
'',
' gl_FragColor = color;',
'}'
].join('\n');
var uniforms = {
uTime: { value: 0.0 },
uTextureA: { value: textureA },
uTextureB: { value: textureB },
uMix: { value: 0.0 },
uWaveIntensity: { value: 0.0 },
uRippleIntensity: { value: 0.0 }
};
var material = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: uniforms,
side: THREE.FrontSide
});
// Subdivided plane: 100x60 segments for smooth deformation
// Size matches camera frustum at z=0 for the perspective camera
var planeW = 4.8;
var planeH = planeW * (H / W);
var geometry = new THREE.PlaneGeometry(planeW, planeH, 100, 60);
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// ── Render function ──
function renderAt(t) {
uniforms.uTime.value = t;
renderer.render(scene, camera);
}
// ═══════════════════════════════════════════════════════
// GSAP DRIVER TIMELINE
// ═══════════════════════════════════════════════════════
var tl = gsap.timeline({ paused: true });
// Phase 1 (0-2s): Panel A appears, gentle waves begin
tl.to(uniforms.uWaveIntensity, {
value: 1.0,
duration: 2,
ease: 'power2.out'
}, 0);
tl.to(uniforms.uRippleIntensity, {
value: 0.6,
duration: 2.5,
ease: 'power2.out'
}, 0.5);
// Phase 2 (2-4.5s): Waves flowing, camera tilts slightly
tl.to(camera.rotation, {
x: 0.06,
y: -0.04,
duration: 2.5,
ease: 'power1.inOut'
}, 2);
// Phase 3 (4.5-5.5s): Waves intensify for transition
tl.to(uniforms.uWaveIntensity, {
value: 2.5,
duration: 1.0,
ease: 'power2.in'
}, 4.5);
tl.to(uniforms.uRippleIntensity, {
value: 2.0,
duration: 1.0,
ease: 'power2.in'
}, 4.5);
// Phase 4 (5-7s): Crossfade from Panel A to Panel B
tl.to(uniforms.uMix, {
value: 1.0,
duration: 2.0,
ease: 'power2.inOut'
}, 5);
// Phase 5 (6.5-8.5s): Waves settle back down
tl.to(uniforms.uWaveIntensity, {
value: 1.0,
duration: 2.0,
ease: 'power2.out'
}, 6.5);
tl.to(uniforms.uRippleIntensity, {
value: 0.5,
duration: 2.0,
ease: 'power2.out'
}, 6.5);
// Camera returns to center
tl.to(camera.rotation, {
x: -0.03,
y: 0.03,
duration: 2.5,
ease: 'power1.inOut'
}, 6.5);
// Phase 6 (9-11s): Gentle breathing waves on Panel B
tl.to(uniforms.uWaveIntensity, {
value: 0.7,
duration: 2.0,
ease: 'sine.inOut'
}, 9);
tl.to(uniforms.uRippleIntensity, {
value: 0.3,
duration: 2.0,
ease: 'sine.inOut'
}, 9);
// Phase 7 (10.5-12s): Waves fade to still
tl.to(uniforms.uWaveIntensity, {
value: 0.0,
duration: 1.5,
ease: 'power2.inOut'
}, 10.5);
tl.to(uniforms.uRippleIntensity, {
value: 0.0,
duration: 1.5,
ease: 'power2.inOut'
}, 10.5);
tl.to(camera.rotation, {
x: 0,
y: 0,
duration: 1.5,
ease: 'power2.inOut'
}, 10.5);
// Driver tween: runs the full 12s, drives uTime and renders each frame
tl.to({ frame: 0 }, {
frame: 1,
duration: 12,
ease: 'none',
onUpdate: function() {
var time = tl.time();
uniforms.uTime.value = time;
renderer.render(scene, camera);
}
}, 0);
window.__timelines['liquid-html'] = tl;
// ── hf-seek integration for Three.js adapter ──
window.addEventListener('hf-seek', function(e) {
var t = e.detail.time;
tl.seek(t);
uniforms.uTime.value = t;
renderer.render(scene, camera);
});
// Initial render
renderAt(0);
}, 0);
</script>
</div>
</body>
</html>
@@ -0,0 +1,14 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "vfx-liquid-glass",
"type": "hyperframes:block",
"title": "Liquid Glass",
"description": "VFX composition block",
"stability": "experimental",
"dimensions": { "width": 1920, "height": 1080 },
"duration": 20,
"tags": ["vfx", "html-in-canvas", "webgl"],
"files": [
{ "path": "vfx-liquid-glass.html", "target": "compositions/vfx-liquid-glass.html", "type": "hyperframes:composition" }
]
}
+518
View File
@@ -0,0 +1,518 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1920, height=1080">
<title>Liquid Glass Parallax</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<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>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/shaders/CopyShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #030407; overflow: hidden; font-family: 'Inter', system-ui, sans-serif; }
#root { position: relative; width: 1920px; height: 1080px; overflow: hidden; background: #030407; }
.text-source {
width: 1920px;
height: 1080px;
display: grid;
place-items: center;
background: linear-gradient(180deg, #030407 0%, #070910 52%, #030407 100%);
text-align: center;
padding: 0 60px;
}
.text-source h1 {
font-size: 172px;
font-weight: 900;
line-height: 0.88;
color: rgba(248, 249, 253, 0.96);
text-shadow: 0 0 40px rgba(255, 255, 255, 0.22);
letter-spacing: -4px;
max-width: 1600px;
}
</style>
</head>
<body>
<div id="root"
data-composition-id="liquid-glass"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="20"
data-root="true">
<canvas id="cap-text" layoutsubtree width="1920" height="1080"
style="position:absolute;top:0;left:0;width:1920px;height:1080px;z-index:-1;pointer-events:none">
<div class="text-source">
<h1>Liquid Glass Design</h1>
</div>
</canvas>
<canvas id="theater" width="1920" height="1080"
style="position:absolute;top:0;left:0;width:1920px;height:1080px"></canvas>
<div id="driver" class="clip"
data-start="0" data-duration="20" 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 = 20, ASPECT = W / H;
var TEXT_Z = -3.08;
// ── Seeded PRNG ────────────────────────────────────────────────────
function seededRandom(seed) {
var v = seed >>> 0;
return function() {
v += 0x6D2B79F5;
var m = v;
m = Math.imul(m ^ (m >>> 15), m | 1);
m ^= m + Math.imul(m ^ (m >>> 7), m | 61);
return ((m ^ (m >>> 14)) >>> 0) / 4294967296;
};
}
// ── Voronoi (self-contained, no d3) ────────────────────────────────
function polygonArea(pts) {
var a = 0;
for (var i = 0; i < pts.length; i++) {
var j = (i + 1) % pts.length;
a += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
}
return a * 0.5;
}
function polygonCentroid(pts) {
var sa = polygonArea(pts) || 1;
var cx = 0, cy = 0;
for (var i = 0; i < pts.length; i++) {
var j = (i + 1) % pts.length;
var f = pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
cx += (pts[i][0] + pts[j][0]) * f;
cy += (pts[i][1] + pts[j][1]) * f;
}
return { x: cx / (6 * sa), y: cy / (6 * sa) };
}
function clipPoly(poly, px, py, nx, ny) {
var out = [];
for (var i = 0; i < poly.length; i++) {
var c = poly[i], n = poly[(i + 1) % poly.length];
var dc = (c[0] - px) * nx + (c[1] - py) * ny;
var dn = (n[0] - px) * nx + (n[1] - py) * ny;
if (dc <= 0) out.push(c);
if ((dc <= 0) !== (dn <= 0)) {
var t = dc / (dc - dn);
out.push([c[0] + t * (n[0] - c[0]), c[1] + t * (n[1] - c[1])]);
}
}
return out;
}
function computeVoronoi(points, xmin, ymin, xmax, ymax) {
var cells = [];
for (var i = 0; i < points.length; i++) {
var px = points[i][0], py = points[i][1];
var poly = [[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]];
for (var j = 0; j < points.length; j++) {
if (i === j) continue;
var qx = points[j][0], qy = points[j][1];
var mx = (px + qx) * 0.5, my = (py + qy) * 0.5;
poly = clipPoly(poly, mx, my, qx - px, qy - py);
if (poly.length < 3) break;
}
cells.push(poly.length >= 3 ? poly : null);
}
return cells;
}
// ── Feature Detection ──────────────────────────────────────────────
function canCapture() {
var tc = document.createElement("canvas");
if (!("layoutSubtree" in tc)) return false;
tc.setAttribute("layoutsubtree", "");
var ctx = tc.getContext("2d");
return ctx && typeof ctx.drawElementImage === "function";
}
var apiOk = canCapture();
// ── Capture backdrop ───────────────────────────────────────────────
var capCanvas = document.getElementById("cap-text");
var capCtx = capCanvas.getContext("2d");
var textEl = capCanvas.querySelector(".text-source");
function captureBackdrop() {
capCtx.clearRect(0, 0, W, H);
if (apiOk) {
capCtx.drawElementImage(textEl, 0, 0, W, H);
} else {
var g = capCtx.createLinearGradient(0, 0, W, H);
g.addColorStop(0, "#030407");
g.addColorStop(0.5, "#070910");
g.addColorStop(1, "#030407");
capCtx.fillStyle = g;
capCtx.fillRect(0, 0, W, H);
capCtx.textAlign = "center";
capCtx.textBaseline = "middle";
capCtx.font = "900 172px Inter, system-ui, sans-serif";
capCtx.shadowColor = "rgba(255,255,255,0.22)";
capCtx.shadowBlur = 20;
capCtx.fillStyle = "rgba(248,249,253,0.94)";
capCtx.fillText("Liquid Glass", W / 2, H / 2 - 80);
capCtx.fillText("Design", W / 2, H / 2 + 90);
}
}
// ── Three.js Renderer ──────────────────────────────────────────────
var theaterCanvas = document.getElementById("theater");
var renderer = new THREE.WebGLRenderer({
canvas: theaterCanvas,
antialias: true,
alpha: false,
powerPreference: "high-performance",
preserveDrawingBuffer: true
});
renderer.setSize(W, H, false);
renderer.setPixelRatio(1);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.08;
renderer.setClearColor(0x030407, 1);
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x030407);
// ── Environment Map (procedural) ───────────────────────────────────
var envCanvas = document.createElement("canvas");
envCanvas.width = 1024;
envCanvas.height = 512;
var ectx = envCanvas.getContext("2d");
var eg = ectx.createLinearGradient(0, 0, 1024, 512);
eg.addColorStop(0, "#020307");
eg.addColorStop(0.18, "#ffffff");
eg.addColorStop(0.24, "#7fdcff");
eg.addColorStop(0.42, "#07101c");
eg.addColorStop(0.58, "#ffffff");
eg.addColorStop(0.64, "#ffc4a6");
eg.addColorStop(0.82, "#0b0c12");
eg.addColorStop(1, "#020307");
ectx.fillStyle = eg;
ectx.fillRect(0, 0, 1024, 512);
ectx.globalCompositeOperation = "screen";
var envRng = seededRandom(999);
for (var ei = 0; ei < 9; ei++) {
var ey = 36 + ei * 52;
ectx.fillStyle = "rgba(255,255,255," + (ei % 3 === 0 ? 0.22 : 0.1) + ")";
ectx.fillRect((ei % 2) * 120, ey, 1024 * 0.72, 4 + (ei % 3) * 2);
}
var envTex = new THREE.CanvasTexture(envCanvas);
envTex.mapping = THREE.EquirectangularReflectionMapping;
var pmrem = new THREE.PMREMGenerator(renderer);
var envMap = pmrem.fromEquirectangular(envTex);
scene.environment = envMap.texture;
envTex.dispose();
pmrem.dispose();
// ── Camera ─────────────────────────────────────────────────────────
var CAM_Z = 9.35;
var camera = new THREE.PerspectiveCamera(34, ASPECT, 0.1, 80);
camera.position.set(0, 0, CAM_Z);
// ── Scene Groups ───────────────────────────────────────────────────
var rig = new THREE.Group();
scene.add(rig);
var textRig = new THREE.Group();
rig.add(textRig);
// ── Backdrop Plane (text behind glass) ─────────────────────────────
var backdropTex = new THREE.CanvasTexture(capCanvas);
backdropTex.minFilter = THREE.LinearFilter;
backdropTex.magFilter = THREE.LinearFilter;
backdropTex.generateMipmaps = false;
function viewportAtZ(z) {
var d = camera.position.z - z;
var h = 2 * Math.tan(THREE.MathUtils.degToRad(camera.fov * 0.5)) * d;
return { w: h * camera.aspect, h: h };
}
var vp = viewportAtZ(TEXT_Z);
var backdropMesh = new THREE.Mesh(
new THREE.PlaneGeometry(vp.w * 1.18, vp.h * 1.18),
new THREE.MeshBasicMaterial({ map: backdropTex, depthWrite: true, depthTest: true })
);
backdropMesh.position.z = TEXT_Z;
backdropMesh.renderOrder = -4;
textRig.add(backdropMesh);
// ── Light Bands ────────────────────────────────────────────────────
var bandSpecs = [
[-2.8, 1.45, -1.78, 3.2, 0.035, 0.13, 0x9edaff, 0.24],
[2.7, -1.24, -1.76, 3.0, 0.032, -0.1, 0xffd1b5, 0.18],
[0.0, 2.05, -1.82, 5.2, 0.026, 0.02, 0xffffff, 0.13],
[-0.55, -1.92, -1.8, 4.2, 0.024, -0.03, 0xc7f6ff, 0.1]
];
bandSpecs.forEach(function(s) {
var m = new THREE.Mesh(
new THREE.PlaneGeometry(s[3], s[4]),
new THREE.MeshBasicMaterial({ color: s[6], transparent: true, opacity: s[7], blending: THREE.AdditiveBlending, depthWrite: false })
);
m.position.set(s[0], s[1], s[2]);
m.rotation.z = s[5];
rig.add(m);
});
// ── Glass Shards (Voronoi fracture) ────────────────────────────────
var LAYOUT = { width: 9.7, height: 5.78, cols: 9, rows: 6, seed: 5297 };
var rng = seededRandom(LAYOUT.seed);
var seedPoints = [];
var cw = LAYOUT.width / LAYOUT.cols, ch = LAYOUT.height / LAYOUT.rows;
for (var row = 0; row < LAYOUT.rows; row++) {
for (var col = 0; col < LAYOUT.cols; col++) {
var cp = 1 - Math.min(0.62, Math.hypot((col + 0.5) / LAYOUT.cols - 0.5, (row + 0.5) / LAYOUT.rows - 0.5));
var jx = (rng() - 0.5) * cw * (0.58 + cp * 0.16);
var jy = (rng() - 0.5) * ch * (0.58 + cp * 0.16);
seedPoints.push([(col + 0.5) * cw + jx, (row + 0.5) * ch + jy]);
}
}
var voronoiCells = computeVoronoi(seedPoints, 0, 0, LAYOUT.width, LAYOUT.height);
var glassRig = new THREE.Group();
glassRig.scale.setScalar(1.025);
glassRig.position.set(0, 0, 0.08);
rig.add(glassRig);
var panes = [];
var GAP_SCALE = 0.954;
for (var pi = 0; pi < voronoiCells.length; pi++) {
var cell = voronoiCells[pi];
if (!cell || cell.length < 3) continue;
var worldPts = cell.map(function(p) { return [p[0] - LAYOUT.width / 2, LAYOUT.height / 2 - p[1]]; });
var area = Math.abs(polygonArea(worldPts));
if (area < 0.034) continue;
var cent = polygonCentroid(worldPts);
var rad = new THREE.Vector2(cent.x, cent.y);
var radLen = Math.max(rad.length(), 0.001);
rad.divideScalar(radLen);
var localPts = worldPts.map(function(p) { return [(p[0] - cent.x) * GAP_SCALE, (p[1] - cent.y) * GAP_SCALE]; });
var outward = 0.046 + rng() * 0.105;
var depthLift = (area / (LAYOUT.width * LAYOUT.height)) * 1.42;
var phase = pi * 0.71 + rng() * 4;
var depth = 0.18 + rng() * 0.1;
var bevel = Math.min(0.108, Math.sqrt(area) * 0.095);
var roughness = pi % 7 === 0 ? 0.08 + rng() * 0.035 : 0.012 + rng() * 0.038;
var thickness = 1.45 + rng() * 1.25;
// Geometry
var shape = new THREE.Shape();
localPts.forEach(function(p, idx) { idx === 0 ? shape.moveTo(p[0], p[1]) : shape.lineTo(p[0], p[1]); });
shape.closePath();
var geo = new THREE.ExtrudeGeometry(shape, { depth: depth, bevelEnabled: true, bevelThickness: bevel, bevelSize: bevel, bevelSegments: 9, curveSegments: 2, steps: 2 });
geo.center();
geo.computeVertexNormals();
// Material
var mat = new THREE.MeshPhysicalMaterial({
color: 0xf7fbff,
metalness: 0,
roughness: roughness,
transmission: 1,
thickness: thickness,
ior: 1.55,
attenuationColor: new THREE.Color(0xe5f8ff),
attenuationDistance: 12,
clearcoat: 1,
clearcoatRoughness: Math.min(0.12, roughness + 0.035),
specularIntensity: 1,
envMapIntensity: 2.1,
transparent: true,
opacity: 0.76,
side: THREE.DoubleSide
});
var pane = new THREE.Group();
var mesh = new THREE.Mesh(geo, mat);
mesh.renderOrder = 4 + pi;
pane.add(mesh);
// Edge highlights (white + cyan + pink for chromatic aberration look)
var edgeGeo = new THREE.EdgesGeometry(geo, 18);
var whiteEdge = new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: roughness > 0.08 ? 0.32 : 0.44, blending: THREE.AdditiveBlending, depthWrite: false }));
pane.add(whiteEdge);
var cyanEdge = new THREE.LineSegments(edgeGeo.clone(), new THREE.LineBasicMaterial({ color: 0x72e8ff, transparent: true, opacity: 0.18, blending: THREE.AdditiveBlending, depthWrite: false }));
cyanEdge.position.set(0.008, -0.002, -0.008);
pane.add(cyanEdge);
var pinkEdge = new THREE.LineSegments(edgeGeo.clone(), new THREE.LineBasicMaterial({ color: 0xff6f8d, transparent: true, opacity: 0.14, blending: THREE.AdditiveBlending, depthWrite: false }));
pinkEdge.position.set(-0.009, 0.003, 0.006);
pane.add(pinkEdge);
var basePos = new THREE.Vector3(cent.x + rad.x * outward, cent.y + rad.y * outward, 0.28 + depthLift + rng() * 0.18);
var baseRot = new THREE.Euler((rng() - 0.5) * 0.074, (rng() - 0.5) * 0.086, (rng() - 0.5) * 0.052);
pane.position.copy(basePos);
pane.rotation.copy(baseRot);
pane.userData = {
basePos: basePos.clone(),
baseRot: { x: baseRot.x, y: baseRot.y, z: baseRot.z },
phase: phase,
speed: 0.28 + rng() * 0.16,
drift: new THREE.Vector3(0.02 + rng() * 0.026, 0.018 + rng() * 0.024, 0.068 + rng() * 0.092),
rotDrift: new THREE.Vector3(0.016 + rng() * 0.026, 0.018 + rng() * 0.03, 0.008 + rng() * 0.016),
ptrWeight: 0.04 + rng() * 0.052,
rad: rad.clone(),
baseOpacity: 0.76,
baseRoughness: roughness,
glassMat: mat,
edgeW: whiteEdge.material,
edgeC: cyanEdge.material,
edgeP: pinkEdge.material,
edgeWBase: whiteEdge.material.opacity,
edgeCBase: cyanEdge.material.opacity,
edgePBase: pinkEdge.material.opacity
};
panes.push(pane);
glassRig.add(pane);
}
// ── Lighting ───────────────────────────────────────────────────────
scene.add(new THREE.HemisphereLight(0xe8f0ff, 0x07040b, 1.1));
var keyLight = new THREE.DirectionalLight(0xffffff, 3.4);
keyLight.position.set(-3.8, 4.4, 5.6);
scene.add(keyLight);
var edgeLight = new THREE.PointLight(0x8bdcff, 14, 12);
edgeLight.position.set(3.1, 1.4, 3.4);
scene.add(edgeLight);
var warmLight = new THREE.PointLight(0xffc8a8, 7, 12);
warmLight.position.set(-4.4, -2.2, 3.2);
scene.add(warmLight);
// ── Post-Processing ────────────────────────────────────────────────
var GrainShader = {
uniforms: { tDiffuse: { value: null }, time: { value: 0 }, strength: { value: 0.024 } },
vertexShader: "varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }",
fragmentShader: [
"uniform sampler2D tDiffuse; uniform float time; uniform float strength; varying vec2 vUv;",
"float n(vec2 v){ return fract(sin(dot(v+time,vec2(12.9898,78.233)))*43758.5453); }",
"void main(){",
" vec4 c=texture2D(tDiffuse,vUv);",
" c.rgb+=(n(vUv*vec2(1520.0,940.0))-0.5)*strength;",
" c.rgb=pow(max(c.rgb,vec3(0.0)),vec3(0.985));",
" gl_FragColor=c;",
"}"
].join("\n")
};
var composer = new THREE.EffectComposer(renderer);
composer.addPass(new THREE.RenderPass(scene, camera));
var bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(W, H), 0.32, 0.4, 0.86);
composer.addPass(bloomPass);
var grainPass = new THREE.ShaderPass(GrainShader);
composer.addPass(grainPass);
// ── Pointer State (GSAP-animated, replaces mouse) ──────────────────
var ptr = { x: 0.16, y: 0.04 };
var REVEAL_R = 2.18;
var PTR_WORLD = new THREE.Vector2();
function updatePtrWorld() {
PTR_WORLD.set(ptr.x * 4.86, ptr.y * 2.84);
}
// ── Render Function (deterministic, seekable) ──────────────────────
function renderScene(elapsed) {
updatePtrWorld();
camera.position.x = ptr.x * 0.38;
camera.position.y = -ptr.y * 0.27;
camera.position.z = CAM_Z;
camera.lookAt(0, 0, 0);
textRig.position.x = -ptr.x * 0.56;
textRig.position.y = ptr.y * 0.36;
textRig.position.z = Math.sin(elapsed * 0.28) * 0.018;
textRig.rotation.x = ptr.y * 0.028;
textRig.rotation.y = ptr.x * 0.042;
glassRig.rotation.x = -ptr.y * 0.064;
glassRig.rotation.y = ptr.x * 0.078;
edgeLight.position.x = 3.1 + ptr.x * 0.9;
edgeLight.position.y = 1.4 + ptr.y * 0.55;
warmLight.position.x = -4.4 - ptr.x * 0.65;
for (var i = 0; i < panes.length; i++) {
var pane = panes[i];
var d = pane.userData;
var t = elapsed * d.speed + d.phase;
var ptrDist = PTR_WORLD.distanceTo(new THREE.Vector2(d.basePos.x, d.basePos.y));
var reveal = Math.pow(Math.max(0, 1 - ptrDist / REVEAL_R), 2);
var shatter = reveal * 0.36;
var awayX = d.basePos.x - PTR_WORLD.x;
var awayY = d.basePos.y - PTR_WORLD.y;
var awayLen = Math.max(Math.hypot(awayX, awayY), 0.001);
var revealSpread = reveal * 0.24;
var shatterSpread = shatter * 0.68;
var pSlide = d.ptrWeight * 1.9;
pane.position.x = d.basePos.x + Math.sin(t * 0.42) * d.drift.x + ptr.x * pSlide + (awayX / awayLen) * (revealSpread + shatterSpread);
pane.position.y = d.basePos.y + Math.cos(t * 0.38) * d.drift.y - ptr.y * pSlide * 0.72 + (awayY / awayLen) * (revealSpread + shatterSpread * 0.84);
pane.position.z = d.basePos.z + Math.sin(t * 0.52 + i) * d.drift.z + reveal * 0.32 + shatter * 0.74;
pane.rotation.x = d.baseRot.x + Math.sin(t * 0.46) * d.rotDrift.x - ptr.y * d.ptrWeight * 1.15 + reveal * (awayY / awayLen) * 0.16 + shatter * (awayY / awayLen) * 0.58;
pane.rotation.y = d.baseRot.y + Math.cos(t * 0.43) * d.rotDrift.y + ptr.x * d.ptrWeight * 1.38 - reveal * (awayX / awayLen) * 0.2 - shatter * (awayX / awayLen) * 0.66;
pane.rotation.z = d.baseRot.z + Math.sin(t * 0.34) * d.rotDrift.z + reveal * Math.sin(t + i) * 0.028 + shatter * (d.rad.x * awayY - d.rad.y * awayX) * 0.18;
d.glassMat.opacity = d.baseOpacity + shatter * 0.14;
d.glassMat.roughness = Math.min(0.14, d.baseRoughness + shatter * 0.048);
d.edgeW.opacity = d.edgeWBase + shatter * 0.34;
d.edgeC.opacity = d.edgeCBase + shatter * 0.22;
d.edgeP.opacity = d.edgePBase + shatter * 0.2;
}
grainPass.uniforms.time.value = elapsed;
composer.render();
}
// ── GSAP Timeline ──────────────────────────────────────────────────
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
tl.to({ _: 0 }, { _: 1, duration: DURATION, ease: "none" }, 0);
// Pointer choreography — smooth wandering reveal
tl.set(ptr, { x: 0.16, y: 0.04 }, 0);
tl.to(ptr, { x: 0.45, y: -0.15, duration: 3.5, ease: "sine.inOut" }, 0.5);
tl.to(ptr, { x: -0.35, y: 0.25, duration: 3.5, ease: "sine.inOut" }, 4);
tl.to(ptr, { x: 0.15, y: -0.35, duration: 3, ease: "sine.inOut" }, 7.5);
tl.to(ptr, { x: -0.45, y: 0.05, duration: 3, ease: "sine.inOut" }, 10.5);
tl.to(ptr, { x: 0.3, y: 0.2, duration: 3, ease: "sine.inOut" }, 13.5);
tl.to(ptr, { x: -0.1, y: -0.1, duration: 2.5, ease: "sine.inOut" }, 16.5);
tl.to(ptr, { x: 0.16, y: 0.04, duration: 1, ease: "power2.inOut" }, 19);
tl.eventCallback("onUpdate", function() {
renderScene(tl.time());
});
window.__timelines["liquid-glass"] = tl;
setTimeout(function() {
captureBackdrop();
backdropTex.needsUpdate = true;
tl.seek(0);
}, 0);
</script>
</body>
</html>
@@ -0,0 +1,14 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "vfx-magnetic",
"type": "hyperframes:block",
"title": "Magnetic",
"description": "VFX composition block",
"stability": "experimental",
"dimensions": { "width": 1920, "height": 1080 },
"duration": 15,
"tags": ["vfx", "html-in-canvas", "webgl"],
"files": [
{ "path": "vfx-magnetic.html", "target": "compositions/vfx-magnetic.html", "type": "hyperframes:composition" }
]
}
+342
View File
@@ -0,0 +1,342 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1920, height=1080">
<title>Magnetic Cursor — HTML-in-Canvas</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: #0c0c0c; overflow: hidden; font-family: system-ui, -apple-system, sans-serif; color: #e8e8e8; }
#root { position: relative; width: 1920px; height: 1080px; overflow: hidden; background: #0c0c0c; }
canvas { display: block; }
.page-content {
width: 1920px; height: 1080px; overflow: hidden;
font-family: system-ui, -apple-system, sans-serif; color: #e8e8e8;
display: flex; align-items: center; justify-content: center;
}
.page-inner { max-width: 900px; padding: 0 48px; }
.heading {
font-size: 64px; font-weight: 700; line-height: 1.15;
letter-spacing: -0.03em; color: #f0f0f0; margin-bottom: 32px;
}
.heading em { font-style: normal; color: #70c7ed; }
.body-text {
font-size: 22px; line-height: 1.7;
color: rgba(255,255,255,0.6); margin-bottom: 24px;
}
.body-text:last-of-type { margin-bottom: 48px; }
.hint {
font-size: 15px; color: rgba(255,255,255,0.3);
font-family: ui-monospace, 'SF Mono', monospace;
margin-bottom: 48px; letter-spacing: 0.02em;
}
.card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.card {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 14px; padding: 24px;
}
.card-icon {
width: 40px; height: 40px; border-radius: 10px;
margin-bottom: 14px; display: flex; align-items: center;
justify-content: center; font-size: 20px;
background: rgba(112,199,237,0.12);
}
.card-title { font-size: 16px; font-weight: 600; color: rgba(255,255,255,0.85); margin-bottom: 8px; }
.card-desc { font-size: 14px; line-height: 1.5; color: rgba(255,255,255,0.4); }
/* Subtitles */
.subtitle-bar {
position: absolute; bottom: 60px; left: 50%; transform: translateX(-50%);
z-index: 10; pointer-events: none;
}
.subtitle {
font-size: 28px; font-weight: 500; color: white; text-align: center;
background: rgba(0,0,0,0.7); padding: 12px 32px; border-radius: 8px;
backdrop-filter: blur(8px); white-space: nowrap; opacity: 0;
font-family: system-ui, -apple-system, sans-serif;
}
/* Cursor dot */
.cursor-dot {
position: absolute; width: 20px; height: 20px;
border-radius: 50%; background: rgba(112,199,237,0.8);
box-shadow: 0 0 20px rgba(112,199,237,0.5), 0 0 40px rgba(112,199,237,0.3);
z-index: 10; pointer-events: none; transform: translate(-50%, -50%);
opacity: 0;
}
</style>
</head>
<body>
<div id="root"
data-composition-id="magnetic-cursor"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="15"
data-root="true">
<canvas id="gl-canvas" layoutsubtree width="1920" height="1080"
style="position:absolute;top:0;left:0;width:1920px;height:1080px">
<div class="page-content" id="content" style="width:1920px;height:1080px">
<div class="page-inner">
<h1 class="heading">Pixels bend toward<br>your <em>cursor</em></h1>
<p class="body-text">
Move your mouse across this page. Every pixel is drawn through a WebGL shader
that warps space toward your cursor position, like a magnet pulling iron filings
beneath a thin rubber sheet.
</p>
<p class="body-text">
The distortion follows a Gaussian falloff — strong at close range, tapering
smoothly to nothing. The text remains readable even under deformation.
</p>
<p class="hint">HTML-in-Canvas API + WebGL2 shader distortion</p>
<div class="card-grid">
<div class="card">
<div class="card-icon">&#9889;</div>
<div class="card-title">Real-time</div>
<div class="card-desc">60fps distortion on live DOM content via texElementImage2D</div>
</div>
<div class="card">
<div class="card-icon">&#128268;</div>
<div class="card-title">Native API</div>
<div class="card-desc">No html2canvas. Browser's own rasterizer at full fidelity.</div>
</div>
<div class="card">
<div class="card-icon">&#127912;</div>
<div class="card-title">GPU Shaders</div>
<div class="card-desc">Gaussian warp + chromatic aberration in one fragment shader pass.</div>
</div>
</div>
</div>
</div>
</canvas>
<!-- Visible cursor dot -->
<div class="cursor-dot" id="cursor-dot"></div>
<!-- Subtitles -->
<div class="subtitle-bar">
<div class="subtitle" id="sub1">The HTML-in-Canvas API renders live DOM as a WebGL texture</div>
<div class="subtitle" id="sub2">A magnetic shader warps every pixel toward the cursor</div>
<div class="subtitle" id="sub3">Chromatic aberration splits RGB channels at the distortion site</div>
<div class="subtitle" id="sub4">Impossible without native drawElementImage — no polyfill exists</div>
</div>
<div id="driver" class="clip"
data-start="0" data-duration="15" data-track-index="0"
style="position:absolute;width:1px;height:1px;opacity:0;pointer-events:none">
</div>
</div>
<script>
var W = 1920, H = 1080;
var canvas = document.getElementById("gl-canvas");
var content = document.getElementById("content");
var cursorDot = document.getElementById("cursor-dot");
function isSupported() {
var tc = document.createElement("canvas");
if (!("layoutSubtree" in tc)) return false;
tc.setAttribute("layoutsubtree", "");
var ctx = tc.getContext("2d");
return ctx && typeof ctx.drawElementImage === "function";
}
// ── WebGL2 Setup ─────────────────────────────────────────────────
var gl = canvas.getContext("webgl2", { alpha: false, preserveDrawingBuffer: true });
function compile(type, src) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) console.error(gl.getShaderInfoLog(s));
return s;
}
function link(vsSrc, fsSrc) {
var p = gl.createProgram();
gl.attachShader(p, compile(gl.VERTEX_SHADER, vsSrc));
gl.attachShader(p, compile(gl.FRAGMENT_SHADER, fsSrc));
gl.linkProgram(p);
return p;
}
var VS = [
"#version 300 es",
"in vec2 a_pos;",
"out vec2 v_uv;",
"void main() {",
" v_uv = vec2(a_pos.x * 0.5 + 0.5, 0.5 - a_pos.y * 0.5);",
" gl_Position = vec4(a_pos, 0.0, 1.0);",
"}"
].join("\n");
var FS = [
"#version 300 es",
"precision highp float;",
"in vec2 v_uv;",
"out vec4 fragColor;",
"uniform sampler2D u_content;",
"uniform vec2 u_mouse;",
"uniform vec2 u_resolution;",
"uniform float u_repel;",
"uniform float u_strength;",
"",
"void main() {",
" vec2 uv = v_uv;",
" float aspect = u_resolution.x / u_resolution.y;",
" vec2 delta = u_mouse - uv;",
" vec2 aspectDelta = delta * vec2(aspect, 1.0);",
" float dist = length(aspectDelta);",
" float strength = exp(-dist * dist * 20.0) * 0.04 * u_strength;",
" float direction = mix(1.0, -1.0, u_repel);",
" vec2 displaced = uv + delta * strength * direction;",
" displaced = clamp(displaced, vec2(0.0), vec2(1.0));",
" float aberration = strength * 0.6;",
" vec2 aberDir = normalize(delta + 0.0001) * aberration;",
" float r = texture(u_content, displaced + aberDir * 0.3).r;",
" float g = texture(u_content, displaced).g;",
" float b = texture(u_content, displaced - aberDir * 0.3).b;",
" fragColor = vec4(r, g, b, 1.0);",
"}"
].join("\n");
var prog = link(VS, FS);
// Quad
var vao = gl.createVertexArray();
gl.bindVertexArray(vao);
var buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
var aPos = gl.getAttribLocation(prog, "a_pos");
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
// Texture
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);
// Uniforms
gl.useProgram(prog);
var U = {};
["u_content","u_mouse","u_resolution","u_repel","u_strength"].forEach(function(n) {
U[n] = gl.getUniformLocation(prog, n);
});
// ── Capture + Render ─────────────────────────────────────────────
var captured = false;
function captureContent() {
if (!isSupported()) return;
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, content);
captured = true;
}
// Also support drawElementImage fallback via 2D context capture
function captureVia2D() {
var tc = document.createElement("canvas");
tc.setAttribute("layoutsubtree", "");
tc.width = W; tc.height = H;
// Can't use drawElementImage from a different canvas — skip fallback
}
function render(mouseX, mouseY, repel, strength) {
gl.viewport(0, 0, W, H);
gl.useProgram(prog);
gl.bindVertexArray(vao);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.uniform1i(U.u_content, 0);
gl.uniform2f(U.u_mouse, mouseX, mouseY);
gl.uniform2f(U.u_resolution, W, H);
gl.uniform1f(U.u_repel, repel);
gl.uniform1f(U.u_strength, strength);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
// ── Animation State ──────────────────────────────────────────────
var S = {
mouseX: 0.5, mouseY: 0.5,
repel: 0, strength: 0,
progress: 0
};
// ── GSAP Timeline ────────────────────────────────────────────────
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// Driver tween
tl.to(S, {
progress: 1, duration: 15, ease: "none",
onUpdate: function() {
// Update cursor dot position
cursorDot.style.left = (S.mouseX * W) + "px";
cursorDot.style.top = (S.mouseY * H) + "px";
render(S.mouseX, S.mouseY, S.repel, S.strength);
}
}, 0);
// Cursor appears
tl.to(cursorDot, { opacity: 1, duration: 0.3 }, 0.5);
tl.to(S, { strength: 1, duration: 0.5, ease: "power2.out" }, 0.5);
// Cursor path: smooth figure-8 across the page
// Phase 1: sweep across heading
tl.to(S, { mouseX: 0.35, mouseY: 0.3, duration: 2, ease: "power1.inOut" }, 1);
tl.to(S, { mouseX: 0.6, mouseY: 0.35, duration: 2, ease: "power1.inOut" }, 3);
// Phase 2: circle around text
tl.to(S, { mouseX: 0.4, mouseY: 0.5, duration: 1.5, ease: "sine.inOut" }, 5);
tl.to(S, { mouseX: 0.55, mouseY: 0.55, duration: 1.5, ease: "sine.inOut" }, 6.5);
// Phase 3: repel pulse
tl.to(S, { repel: 1, duration: 0.2, ease: "power4.out" }, 8);
tl.to(S, { repel: 0, duration: 0.5, ease: "power2.out" }, 8.5);
// Phase 4: sweep across cards
tl.to(S, { mouseX: 0.3, mouseY: 0.75, duration: 2, ease: "power1.inOut" }, 9);
tl.to(S, { mouseX: 0.65, mouseY: 0.72, duration: 2, ease: "power1.inOut" }, 11);
// Phase 5: exit
tl.to(S, { mouseX: 0.8, mouseY: 0.2, duration: 1.5, ease: "power2.inOut" }, 13);
tl.to(S, { strength: 0, duration: 0.5 }, 14);
tl.to(cursorDot, { opacity: 0, duration: 0.3 }, 14.2);
// Subtitles
tl.to("#sub1", { opacity: 1, duration: 0.3 }, 1);
tl.to("#sub1", { opacity: 0, duration: 0.3 }, 3.5);
tl.to("#sub2", { opacity: 1, duration: 0.3 }, 4);
tl.to("#sub2", { opacity: 0, duration: 0.3 }, 6.5);
tl.to("#sub3", { opacity: 1, duration: 0.3 }, 7.5);
tl.to("#sub3", { opacity: 0, duration: 0.3 }, 9.5);
tl.to("#sub4", { opacity: 1, duration: 0.3 }, 10.5);
tl.to("#sub4", { opacity: 0, duration: 0.3 }, 13);
window.__timelines["magnetic-cursor"] = tl;
// Paint event for texture upload
canvas.onpaint = function() {
if (!captured) {
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, content);
captured = true;
}
};
setTimeout(function() {
if (isSupported()) {
canvas.requestPaint();
}
tl.seek(0);
}, 0);
</script>
</body>
</html>
@@ -0,0 +1,14 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "vfx-portal",
"type": "hyperframes:block",
"title": "Portal",
"description": "VFX composition block",
"stability": "experimental",
"dimensions": { "width": 1920, "height": 1080 },
"duration": 10,
"tags": ["vfx", "html-in-canvas", "webgl"],
"files": [
{ "path": "vfx-portal.html", "target": "compositions/vfx-portal.html", "type": "hyperframes:composition" }
]
}
+653
View File
@@ -0,0 +1,653 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1920, height=1080">
<title>HTML Portal Transition</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<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; font-family: 'Inter', system-ui, sans-serif; }
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000;
}
.panel {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
}
/* ── Shared Site Layout ──────────────────────────────── */
.site-wrap {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28px 64px;
}
.logo { font-size: 24px; font-weight: 800; letter-spacing: -0.5px; }
.nav { display: flex; gap: 36px; }
.nav span { font-size: 15px; font-weight: 500; }
.nav-cta {
font-size: 14px;
font-weight: 600;
padding: 10px 24px;
border-radius: 8px;
border: none;
cursor: pointer;
}
.hero {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 24px;
padding: 0 200px;
}
.hero-title {
font-size: 72px;
font-weight: 800;
letter-spacing: -2px;
line-height: 1.05;
}
.hero-sub {
font-size: 20px;
line-height: 1.6;
max-width: 640px;
}
.hero-btn {
display: inline-block;
padding: 16px 40px;
border-radius: 12px;
font-size: 17px;
font-weight: 700;
border: none;
cursor: pointer;
letter-spacing: -0.2px;
}
.cards-row {
display: flex;
gap: 24px;
padding: 0 64px 56px;
}
.feature-card {
flex: 1;
padding: 32px;
border-radius: 16px;
}
.card-icon {
width: 44px;
height: 44px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
margin-bottom: 16px;
}
.card-title { font-size: 18px; font-weight: 700; margin-bottom: 6px; }
.card-desc { font-size: 14px; line-height: 1.5; }
/* ── Light Mode ──────────────────────────────────────── */
.light .site-wrap { background: #fafafa; color: #111; }
.light .logo { color: #111; }
.light .nav span { color: #555; }
.light .nav-cta { background: #7c3aed; color: #fff; }
.light .hero-title { color: #111; }
.light .hero-sub { color: #666; }
.light .hero-btn { background: #7c3aed; color: #fff; }
.light .feature-card {
background: #fff;
border: 1px solid #e5e7eb;
}
.light .card-icon { background: #f3f0ff; color: #7c3aed; }
.light .card-title { color: #111; }
.light .card-desc { color: #777; }
/* ── Dark Mode ───────────────────────────────────────── */
.dark .site-wrap { background: #0a0a0f; color: #f0f0f5; }
.dark .logo {
background: linear-gradient(135deg, #a78bfa, #c084fc);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.dark .nav span { color: rgba(255,255,255,0.5); }
.dark .nav-cta {
background: linear-gradient(135deg, #7c3aed, #a855f7);
color: #fff;
box-shadow: 0 0 24px rgba(124,58,237,0.3);
}
.dark .hero-title { color: #f0f0f5; }
.dark .hero-sub { color: rgba(255,255,255,0.5); }
.dark .hero-btn {
background: linear-gradient(135deg, #7c3aed, #a855f7);
color: #fff;
box-shadow: 0 0 32px rgba(124,58,237,0.35);
}
.dark .feature-card {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
box-shadow: 0 0 20px rgba(124,58,237,0.06);
}
.dark .card-icon {
background: rgba(124,58,237,0.15);
color: #a78bfa;
}
.dark .card-title { color: #f0f0f5; }
.dark .card-desc { color: rgba(255,255,255,0.45); }
#error-overlay {
position: absolute;
inset: 0;
background: #000;
display: none;
align-items: center;
justify-content: center;
z-index: 100;
flex-direction: column;
gap: 16px;
color: #fff;
}
#error-overlay.visible { display: flex; }
#error-overlay h2 { font-size: 24px; font-weight: 600; }
#error-overlay p { font-size: 14px; color: rgba(255,255,255,0.5); max-width: 480px; text-align: center; line-height: 1.6; }
#error-overlay code { background: rgba(255,255,255,0.1); padding: 2px 8px; border-radius: 4px; font-size: 13px; }
</style>
</head>
<body>
<div id="root"
data-composition-id="portal-transition"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="10"
data-root="true">
<!-- Light mode panel (front) -->
<canvas id="cap-light" layoutsubtree width="1920" height="1080"
style="position:absolute;top:0;left:0;width:1920px;height:1080px;z-index:-1;pointer-events:none">
<section class="panel light" id="panel-light"
data-panel-start="0" data-panel-duration="10"
style="width:1920px;height:1080px">
<div class="site-wrap">
<div class="topbar">
<div class="logo">Acme</div>
<div class="nav">
<span>Features</span>
<span>Pricing</span>
<span>Blog</span>
<button class="nav-cta">Sign In</button>
</div>
</div>
<div class="hero">
<h1 class="hero-title">Build products<br>users love</h1>
<p class="hero-sub">Ship faster with a platform that handles the hard parts. Analytics, infrastructure, and developer tools — all in one place.</p>
<button class="hero-btn">Get Started Free</button>
</div>
<div class="cards-row">
<div class="feature-card">
<div class="card-icon">&#9889;</div>
<div class="card-title">Lightning Fast</div>
<div class="card-desc">Sub-millisecond response times with edge deployment across 40+ regions worldwide.</div>
</div>
<div class="feature-card">
<div class="card-icon">&#128274;</div>
<div class="card-title">Secure by Default</div>
<div class="card-desc">SOC2 certified. End-to-end encryption. Role-based access out of the box.</div>
</div>
<div class="feature-card">
<div class="card-icon">&#128268;</div>
<div class="card-title">Plug and Play</div>
<div class="card-desc">200+ integrations. Connect your existing stack in minutes, not weeks.</div>
</div>
<div class="feature-card">
<div class="card-icon">&#128200;</div>
<div class="card-title">Real-time Insights</div>
<div class="card-desc">Live dashboards, anomaly detection, and AI-powered recommendations.</div>
</div>
</div>
</div>
</section>
</canvas>
<!-- Dark mode panel (back) -->
<canvas id="cap-dark" layoutsubtree width="1920" height="1080"
style="position:absolute;top:0;left:0;width:1920px;height:1080px;z-index:-1;pointer-events:none">
<section class="panel dark" id="panel-dark"
data-panel-start="0" data-panel-duration="10"
style="width:1920px;height:1080px">
<div class="site-wrap">
<div class="topbar">
<div class="logo">Acme</div>
<div class="nav">
<span>Features</span>
<span>Pricing</span>
<span>Blog</span>
<button class="nav-cta">Sign In</button>
</div>
</div>
<div class="hero">
<h1 class="hero-title">Build products<br>users love</h1>
<p class="hero-sub">Ship faster with a platform that handles the hard parts. Analytics, infrastructure, and developer tools — all in one place.</p>
<button class="hero-btn">Get Started Free</button>
</div>
<div class="cards-row">
<div class="feature-card">
<div class="card-icon">&#9889;</div>
<div class="card-title">Lightning Fast</div>
<div class="card-desc">Sub-millisecond response times with edge deployment across 40+ regions worldwide.</div>
</div>
<div class="feature-card">
<div class="card-icon">&#128274;</div>
<div class="card-title">Secure by Default</div>
<div class="card-desc">SOC2 certified. End-to-end encryption. Role-based access out of the box.</div>
</div>
<div class="feature-card">
<div class="card-icon">&#128268;</div>
<div class="card-title">Plug and Play</div>
<div class="card-desc">200+ integrations. Connect your existing stack in minutes, not weeks.</div>
</div>
<div class="feature-card">
<div class="card-icon">&#128200;</div>
<div class="card-title">Real-time Insights</div>
<div class="card-desc">Live dashboards, anomaly detection, and AI-powered recommendations.</div>
</div>
</div>
</div>
</section>
</canvas>
<!-- Three.js render target -->
<canvas id="theater" width="1920" height="1080"
style="position:absolute;top:0;left:0;width:1920px;height:1080px"></canvas>
<div id="error-overlay">
<h2>HTML-in-Canvas Required</h2>
<p>This composition requires the experimental CanvasDrawElement API. Enable it in Chrome or Brave:</p>
<p><code>chrome://flags/#canvas-draw-element</code></p>
</div>
<!-- Driver clip for HyperFrames timeline sync -->
<div id="driver"
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>
var theaterCanvas = document.getElementById("theater");
var W = 1920, H = 1080;
// ── Feature Detection ──────────────────────────────────────────────
function isSupported() {
if (!("layoutSubtree" in document.createElement("canvas"))) return false;
var tc = document.createElement("canvas");
tc.setAttribute("layoutsubtree", "");
var ctx = tc.getContext("2d");
return ctx && typeof ctx.drawElementImage === "function";
}
if (!isSupported()) {
document.getElementById("error-overlay").classList.add("visible");
}
// ── Seeded PRNG (mulberry32) ───────────────────────────────────────
function mulberry32(seed) {
return function() {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
var t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = t + Math.imul(t ^ (t >>> 7), 61 | t) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
var rng = mulberry32(42);
// ── Canvas Capture ─────────────────────────────────────────────────
var capLight = document.getElementById("cap-light");
var capDark = document.getElementById("cap-dark");
var ctxLight = capLight.getContext("2d");
var ctxDark = capDark.getContext("2d");
var panelLight = document.getElementById("panel-light");
var panelDark = document.getElementById("panel-dark");
function capturePanels() {
ctxLight.clearRect(0, 0, W, H);
ctxLight.drawElementImage(panelLight, 0, 0, W, H);
ctxDark.clearRect(0, 0, W, H);
ctxDark.drawElementImage(panelDark, 0, 0, W, H);
lightTexture.needsUpdate = true;
darkTexture.needsUpdate = true;
}
// ── Three.js Scene ─────────────────────────────────────────────────
var renderer = new THREE.WebGLRenderer({
canvas: theaterCanvas,
antialias: false,
alpha: false,
powerPreference: "high-performance",
preserveDrawingBuffer: true
});
renderer.setSize(W, H, false);
renderer.setPixelRatio(1);
renderer.setClearColor(0x000000, 1);
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(50, W / H, 0.1, 100);
camera.position.set(0, 0, 2.8);
camera.lookAt(0, 0, 0);
// Calculate quad dimensions to fill the camera frustum at z=0
var vFov = camera.fov * Math.PI / 180;
var quadH = 2 * Math.tan(vFov / 2) * camera.position.z;
var quadW = quadH * (W / H);
// ── Textures from Captured Canvases ────────────────────────────────
var lightTexture = new THREE.CanvasTexture(capLight);
lightTexture.minFilter = THREE.LinearFilter;
lightTexture.magFilter = THREE.LinearFilter;
lightTexture.generateMipmaps = false;
var darkTexture = new THREE.CanvasTexture(capDark);
darkTexture.minFilter = THREE.LinearFilter;
darkTexture.magFilter = THREE.LinearFilter;
darkTexture.generateMipmaps = false;
// ── Back Quad: Dark Mode (sits behind) ─────────────────────────────
var darkGeo = new THREE.PlaneGeometry(quadW, quadH);
var darkMat = new THREE.MeshBasicMaterial({ map: darkTexture, side: THREE.FrontSide });
var darkQuad = new THREE.Mesh(darkGeo, darkMat);
darkQuad.position.z = -0.01;
scene.add(darkQuad);
// ── Front Quad: Light Mode with Portal Shader ──────────────────────
var portalUniforms = {
tLight: { value: lightTexture },
portalRadius: { value: 0.0 },
portalCenter: { value: new THREE.Vector2(0.5, 0.5) },
edgeWidth: { value: 0.06 },
glowColor: { value: new THREE.Vector3(0.486, 0.227, 0.929) },
glowColor2: { value: new THREE.Vector3(0.290, 0.565, 1.0) },
chromaticStrength: { value: 0.025 },
distortionStrength: { value: 0.04 },
time: { value: 0.0 },
energyIntensity: { value: 1.0 }
};
var portalVertShader = [
"varying vec2 vUv;",
"void main() {",
" vUv = uv;",
" gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
"}"
].join("\n");
var portalFragShader = [
"uniform sampler2D tLight;",
"uniform float portalRadius;",
"uniform vec2 portalCenter;",
"uniform float edgeWidth;",
"uniform vec3 glowColor;",
"uniform vec3 glowColor2;",
"uniform float chromaticStrength;",
"uniform float distortionStrength;",
"uniform float time;",
"uniform float energyIntensity;",
"varying vec2 vUv;",
"",
"float hash(vec2 p) {",
" return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);",
"}",
"",
"float noise(vec2 p) {",
" vec2 i = floor(p);",
" vec2 f = fract(p);",
" f = f * f * (3.0 - 2.0 * f);",
" float a = hash(i);",
" float b = hash(i + vec2(1.0, 0.0));",
" float c = hash(i + vec2(0.0, 1.0));",
" float d = hash(i + vec2(1.0, 1.0));",
" return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);",
"}",
"",
"void main() {",
" vec2 uv = vUv;",
" vec2 aspect = vec2(" + (W / H).toFixed(4) + ", 1.0);",
" vec2 centered = (uv - portalCenter) * aspect;",
" float dist = length(centered);",
"",
" // Noise-modulated edge for organic tear look",
" float angle = atan(centered.y, centered.x);",
" float edgeNoise = noise(vec2(angle * 3.0, time * 2.0)) * 0.03 * energyIntensity;",
" float portalEdge = portalRadius + edgeNoise;",
"",
" // Distortion near portal edge: warp UVs radially",
" float edgeDist = abs(dist - portalEdge);",
" float distortMask = smoothstep(edgeWidth * 1.5, 0.0, edgeDist) * distortionStrength;",
" vec2 distortDir = normalize(centered + 0.0001);",
" vec2 distortedUv = uv + distortDir * distortMask * sin(angle * 8.0 + time * 5.0);",
"",
" // Chromatic aberration near portal edge",
" float chromaMask = smoothstep(edgeWidth * 2.0, 0.0, edgeDist) * chromaticStrength;",
" vec2 chromaOffset = distortDir * chromaMask;",
" float r = texture2D(tLight, distortedUv + chromaOffset).r;",
" float g = texture2D(tLight, distortedUv).g;",
" float b = texture2D(tLight, distortedUv - chromaOffset).b;",
" vec3 lightColor = vec3(r, g, b);",
"",
" // Portal mask: inside = transparent (show dark behind), outside = light",
" float mask = smoothstep(portalEdge - 0.008, portalEdge + 0.008, dist);",
"",
" // Glow ring at portal edge",
" float glowInner = smoothstep(edgeWidth, 0.0, edgeDist);",
" float glowOuter = smoothstep(edgeWidth * 2.5, edgeWidth * 0.5, edgeDist);",
" float glow = glowInner * glowOuter * energyIntensity;",
"",
" // Animated color cycling along the ring",
" float colorPhase = fract(angle / 6.2832 + time * 0.4);",
" vec3 ringColor = mix(glowColor, glowColor2, sin(colorPhase * 6.2832) * 0.5 + 0.5);",
"",
" // Secondary energy sparks",
" float sparks = noise(vec2(angle * 12.0, time * 8.0));",
" sparks = pow(sparks, 4.0) * glowInner * energyIntensity * 2.0;",
"",
" vec3 finalColor = lightColor * mask;",
" finalColor += ringColor * glow * 2.5;",
" finalColor += vec3(1.0, 0.9, 1.0) * sparks;",
"",
" // When portal is large enough, alpha fades out completely",
" float alpha = mask + glow * 0.5 + sparks * 0.3;",
" alpha = clamp(alpha, 0.0, 1.0);",
"",
" gl_FragColor = vec4(finalColor, alpha);",
"}"
].join("\n");
var frontGeo = new THREE.PlaneGeometry(quadW, quadH);
var frontMat = new THREE.ShaderMaterial({
uniforms: portalUniforms,
vertexShader: portalVertShader,
fragmentShader: portalFragShader,
transparent: true,
side: THREE.FrontSide,
depthWrite: false
});
var frontQuad = new THREE.Mesh(frontGeo, frontMat);
frontQuad.position.z = 0.0;
scene.add(frontQuad);
// ── Animation State ────────────────────────────────────────────────
var animState = {
portalRadius: 0.0,
cameraZ: 2.8,
energyIntensity: 0.0,
chromaticStrength: 0.0,
distortionStrength: 0.0,
edgeWidth: 0.06,
time: 0.0
};
// ── GSAP Timeline ──────────────────────────────────────────────────
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
// Phase 1: Hold on light mode (0 - 1.5s)
// Subtle energy buildup at center before portal opens
tl.to(animState, {
energyIntensity: 0.3,
duration: 1.2,
ease: "power2.in"
}, 0.3);
// Phase 2: Portal tears open (1.5 - 5.0s)
tl.to(animState, {
portalRadius: 0.85,
duration: 3.5,
ease: "power2.inOut"
}, 1.5);
tl.to(animState, {
energyIntensity: 1.0,
duration: 0.6,
ease: "power3.out"
}, 1.5);
tl.to(animState, {
chromaticStrength: 0.035,
duration: 2.0,
ease: "power2.in"
}, 1.5);
tl.to(animState, {
distortionStrength: 0.06,
duration: 2.5,
ease: "power2.inOut"
}, 1.5);
tl.to(animState, {
edgeWidth: 0.08,
duration: 2.0,
ease: "sine.inOut"
}, 2.0);
// Phase 3: Camera pushes through portal (3.5 - 6.5s)
tl.to(animState, {
cameraZ: 1.4,
duration: 3.0,
ease: "power2.inOut"
}, 3.5);
// Phase 4: Portal overshoots to cover full screen (5.0 - 7.0s)
tl.to(animState, {
portalRadius: 1.8,
duration: 2.0,
ease: "power3.in"
}, 5.0);
// Energy peaks then fades as we emerge
tl.to(animState, {
energyIntensity: 1.5,
duration: 0.8,
ease: "power2.in"
}, 5.0);
tl.to(animState, {
energyIntensity: 0.0,
duration: 1.2,
ease: "power2.out"
}, 6.2);
// Chromatic aberration peaks during push-through
tl.to(animState, {
chromaticStrength: 0.06,
duration: 1.0,
ease: "power3.in"
}, 5.5);
tl.to(animState, {
chromaticStrength: 0.0,
duration: 1.0,
ease: "power2.out"
}, 6.8);
// Distortion fades
tl.to(animState, {
distortionStrength: 0.0,
duration: 1.0,
ease: "power2.out"
}, 6.5);
// Phase 5: Settle in dark mode (7.0 - 10s)
// Camera eases to final position
tl.to(animState, {
cameraZ: 2.8,
duration: 2.5,
ease: "power2.out"
}, 7.0);
// ── Render Loop via Timeline ───────────────────────────────────────
tl.to({ v: 0 }, {
v: 1,
duration: 10,
ease: "none",
onUpdate: function() {
var t = tl.time();
animState.time = t;
// Update portal shader uniforms
portalUniforms.portalRadius.value = animState.portalRadius;
portalUniforms.edgeWidth.value = animState.edgeWidth;
portalUniforms.chromaticStrength.value = animState.chromaticStrength;
portalUniforms.distortionStrength.value = animState.distortionStrength;
portalUniforms.time.value = t;
portalUniforms.energyIntensity.value = animState.energyIntensity;
// Update camera Z for push-through effect
camera.position.z = animState.cameraZ;
// Recalculate quad scale to always fill the view at current camera Z
var currentVFov = camera.fov * Math.PI / 180;
var currentH = 2 * Math.tan(currentVFov / 2) * camera.position.z;
var currentW = currentH * (W / H);
frontQuad.scale.set(currentW / quadW, currentH / quadH, 1);
darkQuad.scale.set(currentW / quadW, currentH / quadH, 1);
renderer.render(scene, camera);
}
}, 0);
window.__timelines["portal-transition"] = tl;
// ── Deferred Init ──────────────────────────────────────────────────
setTimeout(function() {
if (isSupported()) {
capturePanels();
}
tl.seek(0);
}, 0);
</script>
</body>
</html>
@@ -0,0 +1,14 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "vfx-shatter",
"type": "hyperframes:block",
"title": "Shatter",
"description": "VFX composition block",
"stability": "experimental",
"dimensions": { "width": 1920, "height": 1080 },
"duration": 12,
"tags": ["vfx", "html-in-canvas", "webgl"],
"files": [
{ "path": "vfx-shatter.html", "target": "compositions/vfx-shatter.html", "type": "hyperframes:composition" }
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "vfx-text-cursor",
"type": "hyperframes:block",
"title": "VFX Text Cursor",
"description": "Dramatic text reveal with cursor glow, chromatic shadow rays, and directional lighting on a black stage. Canvas-based shader post-processing with spectral color edges.",
"stability": "experimental",
"dimensions": {
"width": 1920,
"height": 1080
},
"duration": 8,
"tags": ["vfx", "text", "shader", "cursor", "chromatic"],
"files": [
{
"path": "vfx-text-cursor.html",
"target": "compositions/vfx-text-cursor.html",
"type": "hyperframes:composition"
}
]
}
+681
View File
@@ -0,0 +1,681 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>VFX Text Cursor</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
</head>
<body style="margin:0;overflow:hidden;">
<div id="vfx-text-cursor" data-composition-id="vfx-text-cursor" data-start="0" data-duration="8" data-width="1920" data-height="1080" data-root="true">
<canvas id="vfx-canvas" width="1920" height="1080" aria-hidden="true"></canvas>
<div class="stage-logo-wrap" aria-hidden="true">
<svg class="stage-logo" width="263" height="79" viewBox="0 0 263 79" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 16.6738H4.96V23.3838H11.53V16.6738H16.49V35.4538H11.53V27.6738H4.96V35.4538H0V16.6738Z" fill="white"/>
<path d="M17.8899 28.0137C17.8899 23.4237 21.2399 20.0237 25.7999 20.0237C30.0099 20.0237 32.9399 23.0837 32.9399 27.5637C32.9399 28.2037 32.8599 28.8237 32.8299 29.3337H22.6099C23.0399 31.1337 24.6199 32.1237 27.1399 32.1237C28.7199 32.1237 30.2199 31.7237 31.2999 31.0237V34.6737C30.1699 35.3937 28.2999 35.8537 26.3599 35.8537C21.2599 35.8537 17.8799 32.6037 17.8799 28.0237L17.8899 28.0137ZM28.5699 26.1937C28.4899 24.5337 27.3899 23.5637 25.7499 23.5637C24.1099 23.5637 22.9599 24.5337 22.6099 26.1937H28.5699Z" fill="white"/>
<path d="M37.85 33.8638L32 20.4238H37.29L40.46 28.1538L43.6 20.4238H48.48L40.33 39.5738H35.26L37.86 33.8638H37.85Z" fill="white"/>
<path d="M48.1797 26.1138C48.1797 20.4238 52.7097 16.2438 58.9397 16.2438C61.2997 16.2438 63.5297 16.8038 64.8197 17.7238V21.9338C63.5597 20.9138 61.6797 20.2938 59.7497 20.2938C55.7797 20.2938 53.2597 22.6238 53.2597 26.1438C53.2597 29.6638 55.5697 31.8838 58.8897 31.8838C59.6397 31.8838 60.4997 31.7538 61.1997 31.5038V27.8838H57.4697V24.2338H65.5197V33.7038C63.6697 35.0938 61.1997 35.8738 58.5997 35.8738C52.5397 35.8738 48.1897 31.7438 48.1897 26.1138H48.1797Z" fill="white"/>
<path d="M66.6604 28.0137C66.6604 23.4237 70.0104 20.0237 74.5704 20.0237C78.7804 20.0237 81.7104 23.0837 81.7104 27.5637C81.7104 28.2037 81.6304 28.8237 81.6004 29.3337H71.3804C71.8104 31.1337 73.3904 32.1237 75.9104 32.1237C77.4904 32.1237 78.9904 31.7237 80.0704 31.0237V34.6737C78.9404 35.3937 77.0704 35.8537 75.1304 35.8537C70.0304 35.8537 66.6504 32.6037 66.6504 28.0237L66.6604 28.0137ZM77.3404 26.1937C77.2604 24.5337 76.1604 23.5637 74.5204 23.5637C72.8804 23.5637 71.7304 24.5337 71.3804 26.1937H77.3404Z" fill="white"/>
<path d="M82.9697 20.4238H87.8497V22.4138C88.7597 20.9638 90.3497 20.1038 92.3297 20.1038C95.4997 20.1038 97.3997 22.3338 97.3997 25.8138V35.4438H92.5197V26.8638C92.5197 25.0438 91.8197 24.1838 90.4797 24.1838C88.9497 24.1838 87.8497 25.3638 87.8497 27.2638V35.4438H82.9697V20.4238Z" fill="white"/>
<path d="M195.219 26.1937L213.529 38.9937C216.009 40.7237 220.239 38.7637 221.009 35.5337L228.419 4.33374C229.189 1.10374 225.879 -0.856262 222.589 0.873738L198.199 13.6737C192.649 16.5837 191.059 23.2837 195.219 26.1937Z" fill="url(#paint0_linear_2204_3044)"/>
<path d="M256.97 25.9638L232.58 38.7638C229.28 40.4938 225.98 38.5338 226.75 35.3038L234.16 4.10376C234.93 0.873757 239.16 -1.08624 241.64 0.643757L259.95 13.4438C264.12 16.3538 262.52 23.0538 256.97 25.9638Z" fill="url(#paint1_linear_2204_3044)"/>
<path d="M252.115 72.502C250.192 72.502 248.268 72.0838 246.93 71.3728V65.6853C247.808 66.3126 248.937 66.689 250.066 66.689C251.739 66.689 252.784 65.8526 252.784 64.5562C252.784 63.8452 252.492 63.0925 251.655 62.0888L249.899 59.956C248.728 58.4923 248.226 56.9031 248.226 55.2721C248.226 51.0065 251.864 48.0791 256.883 48.0791C258.723 48.0791 260.438 48.4555 261.65 49.0828V54.7703C260.898 54.2266 259.768 53.8503 258.723 53.8503C257.092 53.8503 256.046 54.6867 256.046 55.8994C256.046 56.6522 256.423 57.405 257.259 58.4086L258.974 60.4996C260.187 62.047 260.73 63.5943 260.73 65.2671C260.73 69.5746 257.05 72.502 252.115 72.502Z" fill="white"/>
<path d="M236.338 72.6269C228.392 72.6269 223.123 67.5667 223.123 60.4155C223.123 53.2643 228.351 47.9531 235.46 47.9531C242.026 47.9531 246.584 52.7206 246.584 59.7045C246.584 60.7082 246.459 61.6701 246.417 62.4647H230.483C231.152 65.2666 233.62 66.8139 237.551 66.8139C240.018 66.8139 242.36 66.1866 244.033 65.0993V70.7868C242.277 71.916 239.349 72.6269 236.338 72.6269ZM230.483 57.5717H239.767C239.642 54.9789 237.927 53.4734 235.376 53.4734C232.867 53.4734 231.027 54.9789 230.483 57.5717Z" fill="white"/>
<path d="M184.998 72.0001V48.5809H192.609V51.6756C193.989 49.4173 196.373 48.0791 199.426 48.0791C202.521 48.0791 204.779 49.5428 206.075 52.052C207.497 49.5846 210.132 48.0791 213.352 48.0791C218.245 48.0791 221.214 51.592 221.214 57.0286V72.0001H213.603V58.5341C213.603 55.774 212.557 54.4357 210.634 54.4357C208.459 54.4357 206.912 56.234 206.912 59.0778V72.0001H199.3V58.5341C199.3 55.774 198.255 54.4357 196.373 54.4357C194.157 54.4357 192.609 56.234 192.609 59.0778V72.0001H184.998Z" fill="white"/>
<path d="M167.26 72.4602C161.154 72.4602 156.596 67.2745 156.596 60.3324C156.596 53.3066 161.196 48.0791 167.302 48.0791C170.438 48.0791 173.031 49.4173 174.536 51.8429V48.5809H182.148V72.0001H174.536V68.4873C173.031 71.0801 170.438 72.4602 167.26 72.4602ZM169.393 66.1872C172.78 66.1872 174.536 63.3434 174.536 60.876V59.6632C174.536 57.2377 172.78 54.4357 169.393 54.4357C166.298 54.4357 164.207 56.7777 164.207 60.2905C164.207 63.7616 166.298 66.1872 169.393 66.1872Z" fill="white"/>
<path d="M141.258 72V48.5808H148.827V52.9719C149.831 50.0445 151.796 48.2881 154.306 48.2881C154.975 48.2881 155.602 48.4135 156.02 48.6645V55.8993C155.435 55.5648 154.724 55.4393 153.887 55.4393C150.751 55.4393 148.869 57.6558 148.869 61.2941V72H141.258Z" fill="white"/>
<path d="M122.643 71.9996V42.7256H139.371V49.124H130.379V54.477H138.911V60.5827H130.379V71.9996H122.643Z" fill="white"/>
<path d="M105.645 72V48.5808H113.214V52.9719C114.218 50.0445 116.183 48.2881 118.692 48.2881C119.361 48.2881 119.989 48.4135 120.407 48.6645V55.8993C119.822 55.5648 119.111 55.4393 118.274 55.4393C115.138 55.4393 113.256 57.6558 113.256 61.2941V72H105.645Z" fill="white"/>
<path d="M93.4397 72.6269C85.4939 72.6269 80.2246 67.5667 80.2246 60.4155C80.2246 53.2643 85.4521 47.9531 92.5615 47.9531C99.1273 47.9531 103.686 52.7206 103.686 59.7045C103.686 60.7082 103.56 61.6701 103.518 62.4647H87.5849C88.2541 65.2666 90.7214 66.8139 94.6525 66.8139C97.1199 66.8139 99.4618 66.1866 101.135 65.0993V70.7868C99.3782 71.916 96.4508 72.6269 93.4397 72.6269ZM87.5849 57.5717H96.869C96.7435 54.9789 95.0289 53.4734 92.4779 53.4734C89.9687 53.4734 88.1286 54.9789 87.5849 57.5717Z" fill="white"/>
<path d="M53.3711 78.4404V48.5809H60.9823V52.052C62.446 49.501 65.0807 48.0791 68.3008 48.0791C74.3647 48.0791 78.9649 53.3066 78.9649 60.2487C78.9649 67.2745 74.3229 72.4602 68.259 72.4602C65.0807 72.4602 62.446 71.1637 60.9823 68.78V78.4404H53.3711ZM66.1262 66.1872C69.2627 66.1872 71.3119 63.7616 71.3119 60.2905C71.3119 56.8195 69.2627 54.4357 66.1262 54.4357C62.7388 54.4357 60.9823 57.2795 60.9823 59.7051V60.9178C60.9823 63.3434 62.7388 66.1872 66.1262 66.1872Z" fill="white"/>
<path d="M31.9606 78.4405L36.0171 69.5329L26.9004 48.5811H35.1389L40.0737 60.6252L44.9666 48.5811H52.5779L39.8646 78.4405H31.9606Z" fill="white"/>
<path d="M0 71.9996V42.7256H7.7367V53.1806H17.9826V42.7256H25.7193V71.9996H17.9826V59.8718H7.7367V71.9996H0Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_2204_3044" x1="225.869" y1="-3.06048e-05" x2="222.845" y2="37.4821" gradientUnits="userSpaceOnUse">
<stop stop-color="#06E3FA"/>
<stop offset="1" stop-color="#4FDB5E"/>
</linearGradient>
<linearGradient id="paint1_linear_2204_3044" x1="230.87" y1="39" x2="244.661" y2="6.30303" gradientUnits="userSpaceOnUse">
<stop stop-color="#06E3FA"/>
<stop offset="1" stop-color="#4FDB5E"/>
</linearGradient>
</defs>
</svg>
</div>
<div id="next-card" aria-hidden="true">
<h1 id="phrase-a" class="card-text">
<span><span class="headline-word html-word"><span class="html-type"></span><span class="html-caret">|</span></span> <span class="headline-word supporting-word">in</span></span>
<span><span class="headline-word supporting-word canvas-word">canvas</span></span>
</h1>
<h2 id="phrase-b" class="card-text availability">
<span>Now available</span>
<span>in HyperFrames</span>
</h2>
</div>
<!-- Driver clip -->
<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>
<style>
@import url("https://fonts.googleapis.com/css2?family=Big+Shoulders+Display:wght@800;900&display=block");
[data-composition-id="vfx-text-cursor"] {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000000;
font-family: Inter, "Arial Black", Arial, sans-serif;
}
[data-composition-id="vfx-text-cursor"] #vfx-canvas {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
z-index: 1;
}
[data-composition-id="vfx-text-cursor"] .stage-logo-wrap {
position: absolute;
left: 420px;
top: 369.8px;
z-index: 2;
width: 1080px;
height: 324.4px;
opacity: 0;
filter: drop-shadow(0 0 22px rgba(73, 242, 255, 0.12));
}
[data-composition-id="vfx-text-cursor"] .stage-logo {
display: block;
width: 100%;
height: auto;
}
[data-composition-id="vfx-text-cursor"] #next-card {
position: absolute;
left: 120px;
top: 67.5px;
width: 1680px;
height: 945px;
padding: 96px 126px;
box-sizing: border-box;
border-radius: 56px;
background: linear-gradient(
160deg,
rgba(255, 255, 255, 0.11) 0%,
rgba(255, 255, 255, 0.045) 42%,
rgba(255, 255, 255, 0.018) 70%,
rgba(255, 255, 255, 0.075) 100%
);
backdrop-filter: blur(14px) saturate(1.12);
-webkit-backdrop-filter: blur(14px) saturate(1.12);
border: 1px solid rgba(255, 255, 255, 0.17);
box-shadow:
0 30px 90px rgba(0, 0, 0, 0.42),
inset 0 1px 0 rgba(255, 255, 255, 0.22);
transform-origin: 50% 50%;
z-index: 3;
opacity: 0;
overflow: hidden;
}
[data-composition-id="vfx-text-cursor"] #next-card::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 18% 78%, rgba(73, 242, 255, 0.14), transparent 38%),
radial-gradient(circle at 82% 22%, rgba(255, 79, 216, 0.11), transparent 34%);
pointer-events: none;
}
[data-composition-id="vfx-text-cursor"] #next-card::after {
content: "";
position: absolute;
inset: 1px;
border-radius: 55px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
pointer-events: none;
}
[data-composition-id="vfx-text-cursor"] .card-text {
position: absolute;
z-index: 1;
left: 126px;
right: 126px;
top: 96px;
bottom: 96px;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 292px;
line-height: 0.88;
font-weight: 900;
letter-spacing: 0;
text-align: center;
text-shadow:
-5px 0 18px rgba(73, 242, 255, 0.38),
5px 0 18px rgba(255, 79, 216, 0.28),
0 24px 80px rgba(0, 0, 0, 0.58);
}
[data-composition-id="vfx-text-cursor"] .card-text > span {
display: block;
white-space: nowrap;
}
[data-composition-id="vfx-text-cursor"] .headline-word {
display: inline-block;
}
[data-composition-id="vfx-text-cursor"] .html-word {
position: relative;
width: 1.78em;
margin-right: 0.08em;
text-align: left;
color: #f6ff7a;
font-family: "Big Shoulders Display", Impact, "Arial Black", sans-serif;
font-weight: 900;
letter-spacing: 0.015em;
text-transform: uppercase;
text-shadow:
-6px 0 16px rgba(73, 242, 255, 0.48),
6px 0 16px rgba(255, 79, 216, 0.34),
0 0 24px rgba(246, 255, 122, 0.34),
0 24px 72px rgba(0, 0, 0, 0.56);
}
[data-composition-id="vfx-text-cursor"] .html-type,
[data-composition-id="vfx-text-cursor"] .html-caret {
display: inline-block;
}
[data-composition-id="vfx-text-cursor"] .html-caret {
position: absolute;
left: 0;
top: 0;
width: auto;
margin-left: 0;
opacity: 0;
color: #f6ff7a;
text-shadow:
0 0 12px rgba(246, 255, 122, 0.7),
-4px 0 14px rgba(73, 242, 255, 0.36),
4px 0 14px rgba(255, 79, 216, 0.26);
}
[data-composition-id="vfx-text-cursor"] .canvas-word {
color: #ffffff;
text-shadow:
-4px 0 18px rgba(246, 255, 122, 0.26),
5px 0 18px rgba(255, 79, 216, 0.25),
0 22px 72px rgba(0, 0, 0, 0.58);
}
[data-composition-id="vfx-text-cursor"] .availability {
font-size: 178px;
line-height: 0.94;
}
</style>
<script>
window.__timelines = window.__timelines || {};
(function () {
var WIDTH = 1920;
var HEIGHT = 1080;
var LOGO_VIEWBOX = { width: 263, height: 79 };
var LOGO_WIDTH = 1080;
var LOGO_SCALE = LOGO_WIDTH / LOGO_VIEWBOX.width;
var LOGO_HEIGHT = LOGO_VIEWBOX.height * LOGO_SCALE;
var LOGO_X = (WIDTH - LOGO_WIDTH) * 0.5;
var LOGO_Y = (HEIGHT - LOGO_HEIGHT) * 0.5 - 8;
var root = document.querySelector('[data-composition-id="vfx-text-cursor"]');
var canvas = root.querySelector("#vfx-canvas");
var nextCard = root.querySelector("#next-card");
var phraseA = root.querySelector("#phrase-a");
var phraseB = root.querySelector("#phrase-b");
var htmlWord = root.querySelector(".html-word");
var htmlType = root.querySelector(".html-type");
var htmlCaret = root.querySelector(".html-caret");
var supportingWords = Array.from(root.querySelectorAll(".supporting-word"));
var stageLogo = root.querySelector(".stage-logo-wrap");
var gl = canvas.getContext("webgl", { premultipliedAlpha: false, preserveDrawingBuffer: true });
var state = { progress: 0, logoY: 760, effectMix: 0, logoScale: 1 };
var mouse = { x: 0, y: HEIGHT * 0.5 };
var entryEase = function(t) { return -0.806 * t * t + 1.806 * t; };
var htmlText = "HTML";
function getTitleOffset() {
var cardRect = nextCard.getBoundingClientRect();
htmlType.textContent = htmlText;
var htmlRect = htmlType.getBoundingClientRect();
htmlType.textContent = "";
return {
x: cardRect.left + cardRect.width * 0.5 - (htmlRect.left + htmlRect.width * 0.5),
y: cardRect.top + cardRect.height * 0.5 - (htmlRect.top + htmlRect.height * 0.5)
};
}
function addTextCardSequence(tl, htmlStart) {
var typeStep = 0.085;
var titleHold = 0.25;
var typeEnd = htmlStart + (htmlText.length - 1) * typeStep;
var slideStart = typeEnd + titleHold;
var slideEnd = slideStart + 0.05;
var titleOffset = getTitleOffset();
tl.call(function() {
htmlType.textContent = "";
htmlCaret.style.left = "0px";
}, [], 0);
tl.set(htmlWord, { x: titleOffset.x, y: titleOffset.y, scale: 1.8, transformOrigin: "50% 50%" }, 0);
tl.set(htmlCaret, { opacity: 0 }, 0);
tl.set(htmlCaret, { opacity: 1 }, htmlStart - 0.05);
for (var i = 1; i <= htmlText.length; i += 1) {
(function(idx) {
tl.call(function() {
htmlType.textContent = htmlText.slice(0, idx);
htmlCaret.style.left = htmlType.offsetWidth + "px";
if (idx === htmlText.length) {
htmlCaret.style.opacity = "0";
}
}, [], htmlStart + (idx - 1) * typeStep);
})(i);
}
tl.to(htmlWord, {
x: 0,
y: 0,
scale: 1,
duration: 0.05,
ease: "power2.out"
}, slideStart);
tl.to(supportingWords, {
opacity: 1,
y: 0,
scale: 1,
filter: "blur(0px)",
duration: 0.42,
stagger: 0.08,
ease: "expo.out"
}, slideEnd);
return slideEnd + 0.42 + (supportingWords.length - 1) * 0.08;
}
if (!gl) {
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, WIDTH, HEIGHT);
var tl = gsap.timeline({ paused: true });
tl.set(nextCard, { opacity: 0, scale: 1.45, filter: "blur(14px)" }, 0);
tl.set(phraseA, { opacity: 1, x: 0, y: 0, scale: 1, filter: "blur(0px)" }, 0);
tl.set(supportingWords, { opacity: 0, y: 54, scale: 0.96, filter: "blur(10px)" }, 0);
tl.set(phraseB, { opacity: 0, x: 1480, y: 0, scale: 1, filter: "blur(8px)" }, 0);
tl.to(state, { logoY: 0, duration: 1, ease: "power4.out" }, 0);
tl.set(nextCard, { opacity: 1, scale: 1.45, filter: "blur(14px)" }, 3.1);
tl.to(nextCard, { scale: 0.88, filter: "blur(0px)", duration: 0.9, ease: entryEase }, 3.1);
tl.to(nextCard, { scale: 0.84, filter: "blur(1.5px)", duration: 0.4, ease: "none" }, 4.0);
var phraseAReady = addTextCardSequence(tl, 3.15);
var phraseAExit = phraseAReady + 0.5;
var phraseBStart = phraseAExit + 0.36;
tl.to(phraseA, { x: -1480, filter: "blur(8px)", duration: 0.36, ease: "power2.in" }, phraseAExit);
tl.set(phraseA, { opacity: 0 }, phraseBStart);
tl.set(phraseB, { opacity: 1, x: 1480, y: 0, scale: 1, filter: "blur(8px)" }, phraseBStart);
tl.to(phraseB, { x: 0, filter: "blur(0px)", duration: 0.48, ease: "expo.out" }, phraseBStart);
window.__timelines["vfx-text-cursor"] = tl;
return;
}
var vertexSource = [
"attribute vec2 position;",
"varying vec2 vUv;",
"void main() {",
" vUv = position * 0.5 + 0.5;",
" gl_Position = vec4(position, 0.0, 1.0);",
"}"
].join("\n");
var fragmentSource = [
"precision highp float;",
"varying vec2 vUv;",
"uniform vec2 resolution;",
"uniform vec2 mouse;",
"uniform float logoY;",
"uniform float logoScale;",
"uniform float effectMix;",
"uniform sampler2D src;",
"uniform sampler2D colorSrc;",
"",
"#define PI 3.141593",
"#define SAMPLES 112.0",
"",
"float hash(vec2 p) {",
" return fract(sin(dot(p, vec2(489.0, 589.0))) * 492.0) * 2.0 - 1.0;",
"}",
"",
"float hash(vec3 p) {",
" return fract(sin(dot(p, vec3(489.0, 589.0, 58.0))) * 492.0) * 2.0 - 1.0;",
"}",
"",
"vec2 hash2(vec3 p) {",
" return vec2(hash(p), hash(p + 1.0));",
"}",
"",
"vec4 readMask(vec2 uv) {",
" if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {",
" return vec4(0.0);",
" }",
" return texture2D(src, uv);",
"}",
"",
"vec4 readColor(vec2 uv) {",
" if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {",
" return vec4(0.0);",
" }",
" return texture2D(colorSrc, uv);",
"}",
"",
"vec3 spectrum(float x) {",
" return cos((x - vec3(0.0, 0.5, 1.0)) * vec3(0.6, 1.0, 0.5) * PI);",
"}",
"",
"void main() {",
" vec2 uv = vUv;",
" vec2 logoCenter = vec2(0.5, 0.5);",
" vec2 logoUv = (uv - logoCenter) / logoScale + logoCenter + vec2(0.0, logoY / resolution.y);",
" float mask = readMask(logoUv).r;",
" vec4 logo = readColor(logoUv);",
" vec2 p = uv * 2.0 - 1.0;",
" p.x *= resolution.x / resolution.y;",
"",
" vec2 mp = mouse / resolution;",
" mp.y = 1.0 - mp.y;",
" mp = mp * 2.0 - 1.0;",
" mp.x *= resolution.x / resolution.y;",
"",
" vec2 rp = p;",
" vec2 d = (mp - p) / SAMPLES;",
" float acc = 0.0;",
"",
" for (float i = 0.0; i < SAMPLES; i++) {",
" rp += d;",
" rp += hash2(vec3(rp, i)) * 0.5 / SAMPLES;",
"",
" vec2 uv2 = rp;",
" uv2.x /= resolution.x / resolution.y;",
" uv2 = uv2 * 0.5 + 0.5;",
" vec2 rayLogoUv = (uv2 - logoCenter) / logoScale + logoCenter + vec2(0.0, logoY / resolution.y);",
" acc += readMask(rayLogoUv).r / SAMPLES;",
" }",
"",
" vec4 c = vec4(0.0, 0.0, 0.0, 1.0);",
" c -= acc * 0.24;",
" c += vec4(spectrum(cos(acc * 4.9)), 1.0) * acc * 3.65;",
" c.rgb = max(c.rgb - 0.035, 0.0);",
" c.rgb *= 1.16;",
" float cursorX = mouse.x / resolution.x;",
" float sweep = exp(-pow((uv.x - cursorX) * 6.2, 2.0));",
" float textProximity = smoothstep(0.006, 0.34, acc);",
" float longRay = smoothstep(0.004, 0.18, acc);",
" vec3 sweepColor = vec3(0.14, 0.65, 0.85) + spectrum(cursorX * 1.8) * 0.28;",
" c.rgb += sweepColor * sweep * (0.34 + textProximity * 2.35);",
" c.rgb += spectrum(cursorX + acc * 2.0) * longRay * 0.58;",
" c.rgb += logo.rgb * acc * 1.18;",
" c.rgb += logo.rgb * sweep * mask * 0.92;",
" c.rgb *= effectMix;",
" c.rgb = mix(c.rgb, logo.rgb, smoothstep(0.02, 0.75, logo.a));",
" c.rgb += vec3(1.0) * smoothstep(0.04, 0.38, mask) * 0.12 * effectMix;",
" float grain = hash(vec3(uv.xyy + mouse.x * 0.0007));",
" float logoDust = smoothstep(0.02, 0.38, acc) + smoothstep(0.03, 0.22, mask);",
" c.rgb += grain * 0.07 * (0.42 + logoDust) * effectMix;",
" c.rgb += spectrum(grain + cursorX) * abs(grain) * 0.045 * smoothstep(0.02, 0.5, acc) * effectMix;",
" c.a = 1.0;",
" gl_FragColor = c;",
"}"
].join("\n");
function compile(type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(shader));
}
return shader;
}
var program = gl.createProgram();
gl.attachShader(program, compile(gl.VERTEX_SHADER, vertexSource));
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragmentSource));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program));
}
var mask = document.createElement("canvas");
mask.width = WIDTH;
mask.height = HEIGHT;
var maskCtx = mask.getContext("2d");
var colorMask = document.createElement("canvas");
colorMask.width = WIDTH;
colorMask.height = HEIGHT;
var colorCtx = colorMask.getContext("2d");
function parseOffset(value) {
if (!value) return 0;
return value.indexOf("%") > -1 ? parseFloat(value) / 100 : parseFloat(value);
}
function makeFill(ctx, fill) {
if (!fill || fill === "none") return null;
var match = fill.match(/^url\(#(.+)\)$/);
if (!match) return fill;
var gradient = root.querySelector("#" + match[1]);
if (!gradient) return "#ffffff";
var x1 = parseFloat(gradient.getAttribute("x1") || "0");
var y1 = parseFloat(gradient.getAttribute("y1") || "0");
var x2 = parseFloat(gradient.getAttribute("x2") || "0");
var y2 = parseFloat(gradient.getAttribute("y2") || "0");
var canvasGradient = ctx.createLinearGradient(x1, y1, x2, y2);
gradient.querySelectorAll("stop").forEach(function (stop) {
var offset = parseOffset(stop.getAttribute("offset") || "0");
var color = stop.getAttribute("stop-color") || "#ffffff";
canvasGradient.addColorStop(Math.max(0, Math.min(1, offset)), color);
});
return canvasGradient;
}
function drawLogoMasks() {
maskCtx.clearRect(0, 0, WIDTH, HEIGHT);
colorCtx.clearRect(0, 0, WIDTH, HEIGHT);
maskCtx.save();
colorCtx.save();
maskCtx.translate(LOGO_X, LOGO_Y);
colorCtx.translate(LOGO_X, LOGO_Y);
maskCtx.scale(LOGO_SCALE, LOGO_SCALE);
colorCtx.scale(LOGO_SCALE, LOGO_SCALE);
maskCtx.fillStyle = "#fff";
root.querySelectorAll(".stage-logo path[d]").forEach(function (path) {
var shape = new Path2D(path.getAttribute("d"));
var fill = makeFill(colorCtx, path.getAttribute("fill"));
maskCtx.fill(shape);
if (fill) {
colorCtx.fillStyle = fill;
colorCtx.fill(shape);
}
});
maskCtx.restore();
colorCtx.restore();
}
drawLogoMasks();
function createTexture(source) {
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
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);
return texture;
}
var texture = createTexture(mask);
var colorTexture = createTexture(colorMask);
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1
]), gl.STATIC_DRAW);
var positionLoc = gl.getAttribLocation(program, "position");
var resolutionLoc = gl.getUniformLocation(program, "resolution");
var mouseLoc = gl.getUniformLocation(program, "mouse");
var logoYLoc = gl.getUniformLocation(program, "logoY");
var logoScaleLoc = gl.getUniformLocation(program, "logoScale");
var effectMixLoc = gl.getUniformLocation(program, "effectMix");
var srcLoc = gl.getUniformLocation(program, "src");
var colorSrcLoc = gl.getUniformLocation(program, "colorSrc");
function render() {
mouse.x = state.progress * WIDTH;
mouse.y = HEIGHT * 0.5;
gl.viewport(0, 0, WIDTH, HEIGHT);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(srcLoc, 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, colorTexture);
gl.uniform1i(colorSrcLoc, 1);
gl.uniform2f(resolutionLoc, WIDTH, HEIGHT);
gl.uniform2f(mouseLoc, mouse.x, mouse.y);
gl.uniform1f(logoYLoc, state.logoY);
gl.uniform1f(logoScaleLoc, state.logoScale);
gl.uniform1f(effectMixLoc, state.effectMix);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
render();
var tl = gsap.timeline({
paused: true,
onUpdate: render
});
tl.set(canvas, { opacity: 1, filter: "blur(0px)" }, 0);
tl.set(nextCard, { opacity: 0, scale: 1.45, filter: "blur(14px)" }, 0);
tl.set(phraseA, { opacity: 1, x: 0, y: 0, scale: 1, filter: "blur(0px)" }, 0);
tl.set(supportingWords, { opacity: 0, y: 54, scale: 0.96, filter: "blur(10px)" }, 0);
tl.set(phraseB, { opacity: 0, x: 1480, y: 0, scale: 1, filter: "blur(8px)" }, 0);
tl.set(state, { logoY: 760, effectMix: 0, progress: 0, logoScale: 1 }, 0);
tl.to(state, {
logoY: 0,
duration: 1,
ease: "power4.out",
onUpdate: render
}, 0);
tl.to(state, {
effectMix: 1,
duration: 0.34,
ease: "power3.out",
onUpdate: render
}, 0.72);
tl.to(state, {
progress: 1,
duration: 2.28,
ease: "none",
onUpdate: render
}, 0.72);
tl.to(state, {
logoScale: 0.4,
duration: 0.2,
ease: "expo.in",
onUpdate: render
}, 2.9);
tl.to(canvas, {
opacity: 0,
filter: "blur(16px)",
duration: 0.2,
ease: "expo.in"
}, 2.9);
tl.set(nextCard, { opacity: 1, scale: 1.45, filter: "blur(14px)" }, 3.1);
tl.to(nextCard, {
scale: 0.88,
filter: "blur(0px)",
duration: 0.9,
ease: entryEase
}, 3.1);
tl.to(nextCard, {
scale: 0.84,
filter: "blur(1.5px)",
duration: 0.4,
ease: "none"
}, 4.0);
var phraseAReady = addTextCardSequence(tl, 3.15);
var phraseAExit = phraseAReady + 0.5;
var phraseBStart = phraseAExit + 0.36;
tl.to(phraseA, {
x: -1480,
filter: "blur(8px)",
duration: 0.36,
ease: "power2.in"
}, phraseAExit);
tl.set(phraseA, { opacity: 0 }, phraseBStart);
tl.set(phraseB, {
opacity: 1,
x: 1480,
y: 0,
scale: 1,
filter: "blur(8px)"
}, phraseBStart);
tl.to(phraseB, {
x: 0,
filter: "blur(0px)",
duration: 0.48,
ease: "expo.out"
}, phraseBStart);
window.__timelines["vfx-text-cursor"] = tl;
})();
</script>
</div>
</body>
</html>