chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
@@ -0,0 +1,83 @@
|
||||
<!--
|
||||
Blend Difference — auto-inverting captions via mix-blend-mode.
|
||||
|
||||
Text color inverts per-pixel against whatever is behind it:
|
||||
white stays white on dark areas, flips to black on light areas.
|
||||
On color video, white inverts to the complement (blue → orange,
|
||||
red → cyan, green → magenta).
|
||||
|
||||
Setup:
|
||||
1. The composition root (or a shared ancestor of both the video
|
||||
and the caption layer) MUST have `isolation: isolate` so the
|
||||
blend operates against sibling content, not the page background.
|
||||
2. Add class="blend-difference" to any caption container.
|
||||
3. Set caption text color to white. The blend mode handles the rest.
|
||||
|
||||
Works on any element — divs, spans, SVG text, even images.
|
||||
|
||||
Customize:
|
||||
- --blend-caption-color: base text color (default white)
|
||||
- Change blend mode via --blend-mode to 'exclusion' for a softer effect
|
||||
|
||||
Variants:
|
||||
- .blend-difference → standard per-pixel inversion
|
||||
- .blend-difference-soft → exclusion mode, less harsh contrast
|
||||
- .blend-difference-screen → text glows on dark, fades on light
|
||||
-->
|
||||
|
||||
<style>
|
||||
.blend-difference {
|
||||
mix-blend-mode: var(--blend-mode, difference);
|
||||
color: var(--blend-caption-color, white);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blend-difference-soft {
|
||||
mix-blend-mode: exclusion;
|
||||
color: var(--blend-caption-color, white);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blend-difference-screen {
|
||||
mix-blend-mode: screen;
|
||||
color: var(--blend-caption-color, white);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--
|
||||
Composition setup example:
|
||||
|
||||
<div data-composition-id="root" ... style="isolation: isolate;">
|
||||
|
||||
<video id="bg" data-start="0" data-duration="30" data-track-index="0"
|
||||
src="video.mp4" muted playsinline></video>
|
||||
|
||||
<div class="clip blend-difference" data-start="0" data-duration="5" data-track-index="1"
|
||||
style="position: absolute; inset: 0; z-index: 10;
|
||||
display: flex; align-items: center; justify-content: center;">
|
||||
<span style="font-size: 120px; font-weight: 800; text-transform: uppercase;">
|
||||
YOUR CAPTION
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Timeline integration — animate captions normally, blend mode is passive:
|
||||
|
||||
tl.from(".caption", {
|
||||
y: 50, opacity: 0, duration: 0.6, ease: "expo.out"
|
||||
}, 0.2);
|
||||
|
||||
|
||||
Notes:
|
||||
- isolation: isolate on the composition root is REQUIRED.
|
||||
Without it, blend mode composes against the page background
|
||||
(usually white or black) and you get no inversion.
|
||||
- Works with any GSAP animation — the blend composites every frame.
|
||||
- For caption containers with multiple text elements, apply the
|
||||
class to the shared parent, not each text element individually.
|
||||
- On pure black backgrounds, white text stays white (difference
|
||||
of white and black = white). The effect is most visible when
|
||||
the background has varied luminance or color.
|
||||
-->
|
||||
@@ -0,0 +1,83 @@
|
||||
<!--
|
||||
Blend Difference — auto-inverting captions via mix-blend-mode.
|
||||
|
||||
Text color inverts per-pixel against whatever is behind it:
|
||||
white stays white on dark areas, flips to black on light areas.
|
||||
On color video, white inverts to the complement (blue → orange,
|
||||
red → cyan, green → magenta).
|
||||
|
||||
Setup:
|
||||
1. The composition root (or a shared ancestor of both the video
|
||||
and the caption layer) MUST have `isolation: isolate` so the
|
||||
blend operates against sibling content, not the page background.
|
||||
2. Add class="blend-difference" to any caption container.
|
||||
3. Set caption text color to white. The blend mode handles the rest.
|
||||
|
||||
Works on any element — divs, spans, SVG text, even images.
|
||||
|
||||
Customize:
|
||||
- --blend-caption-color: base text color (default white)
|
||||
- Change blend mode via --blend-mode to 'exclusion' for a softer effect
|
||||
|
||||
Variants:
|
||||
- .blend-difference → standard per-pixel inversion
|
||||
- .blend-difference-soft → exclusion mode, less harsh contrast
|
||||
- .blend-difference-screen → text glows on dark, fades on light
|
||||
-->
|
||||
|
||||
<style>
|
||||
.blend-difference {
|
||||
mix-blend-mode: var(--blend-mode, difference);
|
||||
color: var(--blend-caption-color, white);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blend-difference-soft {
|
||||
mix-blend-mode: exclusion;
|
||||
color: var(--blend-caption-color, white);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blend-difference-screen {
|
||||
mix-blend-mode: screen;
|
||||
color: var(--blend-caption-color, white);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--
|
||||
Composition setup example:
|
||||
|
||||
<div data-composition-id="root" ... style="isolation: isolate;">
|
||||
|
||||
<video id="bg" data-start="0" data-duration="30" data-track-index="0"
|
||||
src="video.mp4" muted playsinline></video>
|
||||
|
||||
<div class="clip blend-difference" data-start="0" data-duration="5" data-track-index="1"
|
||||
style="position: absolute; inset: 0; z-index: 10;
|
||||
display: flex; align-items: center; justify-content: center;">
|
||||
<span style="font-size: 120px; font-weight: 800; text-transform: uppercase;">
|
||||
YOUR CAPTION
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Timeline integration — animate captions normally, blend mode is passive:
|
||||
|
||||
tl.from(".caption", {
|
||||
y: 50, opacity: 0, duration: 0.6, ease: "expo.out"
|
||||
}, 0.2);
|
||||
|
||||
|
||||
Notes:
|
||||
- isolation: isolate on the composition root is REQUIRED.
|
||||
Without it, blend mode composes against the page background
|
||||
(usually white or black) and you get no inversion.
|
||||
- Works with any GSAP animation — the blend composites every frame.
|
||||
- For caption containers with multiple text elements, apply the
|
||||
class to the shared parent, not each text element individually.
|
||||
- On pure black backgrounds, white text stays white (difference
|
||||
of white and black = white). The effect is most visible when
|
||||
the background has varied luminance or color.
|
||||
-->
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-blend-difference",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Blend Difference",
|
||||
"description": "Auto-inverting text using mix-blend-mode: difference — flips between white and black per-pixel against the background",
|
||||
"tags": ["text", "text-effect", "effect", "blend-mode", "contrast", "inversion"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-blend-difference.html",
|
||||
"target": "compositions/components/caption-blend-difference.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Clip Wipe</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=Poppins:wght@800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#clip-wipe {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#wp-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.wp-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#wp-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.wp-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 0 100px;
|
||||
visibility: hidden;
|
||||
}
|
||||
.wp-word {
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 88px;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
will-change: clip-path, color, opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="clip-wipe"
|
||||
data-composition-id="caption-clip-wipe"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="wp-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="wp-overlay"></div>
|
||||
<div id="wp-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var KEYWORDS = new Set([8, 12, 15, 24, 27]); // HyperFrames, HTML, professional, code, cinema.
|
||||
|
||||
var RAW_GROUPS = [
|
||||
[0, 3],
|
||||
[4, 7],
|
||||
[8, 8],
|
||||
[9, 12],
|
||||
[13, 15],
|
||||
[16, 16],
|
||||
[17, 19],
|
||||
[20, 22],
|
||||
[23, 24],
|
||||
[25, 27],
|
||||
];
|
||||
var GROUPS = RAW_GROUPS.map(function (pair, gi) {
|
||||
var ws = pair[0],
|
||||
we = pair[1];
|
||||
var nextStart = gi + 1 < RAW_GROUPS.length ? WORDS[RAW_GROUPS[gi + 1][0]].start : 8.0;
|
||||
var gEnd = Math.min(WORDS[we].end + 0.5, nextStart - 0.05);
|
||||
return { wordStart: ws, wordEnd: we, start: WORDS[ws].start, end: gEnd };
|
||||
});
|
||||
|
||||
var container = document.getElementById("wp-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "wp-group";
|
||||
grp.id = "wp-grp-" + gi;
|
||||
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var computedSize = fitFontSize(groupText, 88, "800", "Poppins", 1720);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var span = document.createElement("span");
|
||||
span.className = "wp-word";
|
||||
span.id = "wp-w-" + wi;
|
||||
span.textContent = w.text.toUpperCase();
|
||||
span.style.fontSize = computedSize + "px";
|
||||
grp.appendChild(span);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible" }, g.start);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var wordEl = document.getElementById("wp-w-" + wi);
|
||||
var isKW = KEYWORDS.has(wi);
|
||||
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ clipPath: "inset(0 0% 0 0)", duration: 0.3, ease: "power2.out" },
|
||||
w.start,
|
||||
);
|
||||
if (isKW) {
|
||||
tl.to(wordEl, { color: "#FFD700", duration: 0.05 }, w.start + 0.1);
|
||||
}
|
||||
tl.to(wordEl, { color: "rgba(255,255,255,0.4)", duration: 0.2 }, w.end);
|
||||
});
|
||||
|
||||
var wordEls = grp.querySelectorAll(".wp-word");
|
||||
tl.to(
|
||||
wordEls,
|
||||
{
|
||||
clipPath: "inset(0 0% 0 100%)",
|
||||
duration: 0.25,
|
||||
stagger: 0.04,
|
||||
ease: "power2.in",
|
||||
},
|
||||
g.end - 0.2,
|
||||
);
|
||||
tl.set(grp, { visibility: "hidden", opacity: 0 }, g.end);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-clip-wipe"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,241 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Clip Wipe</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=Poppins:wght@800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#clip-wipe {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#wp-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.wp-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#wp-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.wp-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 0 100px;
|
||||
visibility: hidden;
|
||||
}
|
||||
.wp-word {
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 88px;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
will-change: clip-path, color, opacity;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="clip-wipe"
|
||||
data-composition-id="caption-clip-wipe"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="wp-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="wp-overlay"></div>
|
||||
<div id="wp-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var KEYWORDS = new Set([8, 12, 15, 24, 27]); // HyperFrames, HTML, professional, code, cinema.
|
||||
|
||||
var RAW_GROUPS = [
|
||||
[0, 3],
|
||||
[4, 7],
|
||||
[8, 8],
|
||||
[9, 12],
|
||||
[13, 15],
|
||||
[16, 16],
|
||||
[17, 19],
|
||||
[20, 22],
|
||||
[23, 24],
|
||||
[25, 27],
|
||||
];
|
||||
var GROUPS = RAW_GROUPS.map(function (pair, gi) {
|
||||
var ws = pair[0],
|
||||
we = pair[1];
|
||||
var nextStart = gi + 1 < RAW_GROUPS.length ? WORDS[RAW_GROUPS[gi + 1][0]].start : 8.0;
|
||||
var gEnd = Math.min(WORDS[we].end + 0.5, nextStart - 0.05);
|
||||
return { wordStart: ws, wordEnd: we, start: WORDS[ws].start, end: gEnd };
|
||||
});
|
||||
|
||||
var container = document.getElementById("wp-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "wp-group";
|
||||
grp.id = "wp-grp-" + gi;
|
||||
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var computedSize = fitFontSize(groupText, 88, "800", "Poppins", 1720);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var span = document.createElement("span");
|
||||
span.className = "wp-word";
|
||||
span.id = "wp-w-" + wi;
|
||||
span.textContent = w.text.toUpperCase();
|
||||
span.style.fontSize = computedSize + "px";
|
||||
grp.appendChild(span);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible" }, g.start);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var wordEl = document.getElementById("wp-w-" + wi);
|
||||
var isKW = KEYWORDS.has(wi);
|
||||
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ clipPath: "inset(0 0% 0 0)", duration: 0.3, ease: "power2.out" },
|
||||
w.start,
|
||||
);
|
||||
if (isKW) {
|
||||
tl.to(wordEl, { color: "#FFD700", duration: 0.05 }, w.start + 0.1);
|
||||
}
|
||||
tl.to(wordEl, { color: "rgba(255,255,255,0.4)", duration: 0.2 }, w.end);
|
||||
});
|
||||
|
||||
var wordEls = grp.querySelectorAll(".wp-word");
|
||||
tl.to(
|
||||
wordEls,
|
||||
{
|
||||
clipPath: "inset(0 0% 0 100%)",
|
||||
duration: 0.25,
|
||||
stagger: 0.04,
|
||||
ease: "power2.in",
|
||||
},
|
||||
g.end - 0.2,
|
||||
);
|
||||
tl.set(grp, { visibility: "hidden", opacity: 0 }, g.end);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-clip-wipe"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-clip-wipe",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Clip Wipe",
|
||||
"description": "Left-to-right clip-path wipe reveal per word",
|
||||
"tags": ["captions", "caption-style", "wipe", "clip-path", "reveal"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-clip-wipe.html",
|
||||
"target": "compositions/components/caption-clip-wipe.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-clip-wipe/preview-v2.mp4",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-clip-wipe.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Editorial Emphasis</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&family=Playfair+Display:ital,wght@0,800;1,800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
#editorial-emphasis {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#bg-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.safe-zone {
|
||||
position: absolute;
|
||||
top: 580px;
|
||||
left: 60px;
|
||||
width: 1800px;
|
||||
height: 430px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.caption-block {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
transform-origin: 0% 0%;
|
||||
will-change: transform, opacity;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 14px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.caption-line + .caption-line {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.word {
|
||||
display: inline-block;
|
||||
color: #f5f0d0;
|
||||
text-shadow:
|
||||
0 2px 12px rgba(0, 0, 0, 0.6),
|
||||
0 4px 24px rgba(0, 0, 0, 0.35);
|
||||
will-change: transform, opacity;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.word--normal {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 86px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.word--italic {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 86px;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.word--emphasis {
|
||||
font-family: "Playfair Display", serif;
|
||||
font-size: 180px;
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
line-height: 0.9;
|
||||
color: #f5f0d0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="editorial-emphasis"
|
||||
data-composition-id="caption-editorial-emphasis"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="bg-video"
|
||||
muted
|
||||
playsinline
|
||||
preload="auto"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var SAFE_WIDTH = 1800;
|
||||
var ENTRY_DUR = 0.1;
|
||||
var SLIDE_DUR = 0.2;
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
// Generic transcript words
|
||||
var W = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
// Block layout: [wordIndex, type] — n=normal, i=italic, e=emphasis(large)
|
||||
// Emphasis words: HyperFrames(8), HTML(12), professional(15), code(24), cinema.(27)
|
||||
var BLOCKS = [
|
||||
{
|
||||
line1: [
|
||||
[0, "n"],
|
||||
[1, "n"],
|
||||
[2, "n"],
|
||||
],
|
||||
line2: [
|
||||
[3, "n"],
|
||||
[4, "n"],
|
||||
],
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[5, "n"],
|
||||
[6, "n"],
|
||||
],
|
||||
line2: [[7, "e"]],
|
||||
},
|
||||
{ line1: [[8, "e"]], line2: null },
|
||||
{
|
||||
line1: [
|
||||
[9, "n"],
|
||||
[10, "n"],
|
||||
[11, "n"],
|
||||
],
|
||||
line2: [[12, "e"]],
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[13, "n"],
|
||||
[14, "n"],
|
||||
],
|
||||
line2: [[15, "e"]],
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[16, "n"],
|
||||
[17, "n"],
|
||||
],
|
||||
line2: null,
|
||||
},
|
||||
{
|
||||
line1: [[18, "n"]],
|
||||
line2: [
|
||||
[19, "n"],
|
||||
[20, "n"],
|
||||
],
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[21, "n"],
|
||||
[22, "n"],
|
||||
],
|
||||
line2: null,
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[23, "i"],
|
||||
[24, "e"],
|
||||
],
|
||||
line2: null,
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[25, "n"],
|
||||
[26, "n"],
|
||||
],
|
||||
line2: [[27, "e"]],
|
||||
},
|
||||
];
|
||||
|
||||
var CLASS_MAP = { n: "word word--normal", i: "word word--italic", e: "word word--emphasis" };
|
||||
|
||||
function computeLineSize(linePairs) {
|
||||
var hasEmphasis = linePairs.some(function (p) {
|
||||
return p[1] === "e";
|
||||
});
|
||||
if (hasEmphasis) {
|
||||
var lineText = linePairs
|
||||
.map(function (p) {
|
||||
return W[p[0]].text;
|
||||
})
|
||||
.join(" ");
|
||||
return fitFontSize(lineText, 180, "800", "Playfair Display", SAFE_WIDTH);
|
||||
}
|
||||
var lineText = linePairs
|
||||
.map(function (p) {
|
||||
return W[p[0]].text;
|
||||
})
|
||||
.join(" ");
|
||||
return fitFontSize(lineText, 86, "400", "Inter", SAFE_WIDTH);
|
||||
}
|
||||
|
||||
function buildBlocks() {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
var el = document.createElement("div");
|
||||
el.className = "caption-block";
|
||||
el.id = "b" + bi;
|
||||
|
||||
var l1Size = computeLineSize(block.line1);
|
||||
var l1 = document.createElement("div");
|
||||
l1.className = "caption-line";
|
||||
l1.id = "b" + bi + "L1";
|
||||
block.line1.forEach(function (pair, wi) {
|
||||
var span = document.createElement("span");
|
||||
span.className = CLASS_MAP[pair[1]];
|
||||
span.id = "b" + bi + "L1w" + wi;
|
||||
span.textContent = W[pair[0]].text;
|
||||
if (pair[1] === "e") {
|
||||
span.style.fontSize = l1Size + "px";
|
||||
} else {
|
||||
span.style.fontSize = l1Size + "px";
|
||||
}
|
||||
l1.appendChild(span);
|
||||
});
|
||||
el.appendChild(l1);
|
||||
|
||||
if (block.line2) {
|
||||
var l2Size = computeLineSize(block.line2);
|
||||
var l2 = document.createElement("div");
|
||||
l2.className = "caption-line";
|
||||
l2.id = "b" + bi + "L2";
|
||||
block.line2.forEach(function (pair, wi) {
|
||||
var span = document.createElement("span");
|
||||
span.className = CLASS_MAP[pair[1]];
|
||||
span.id = "b" + bi + "L2w" + wi;
|
||||
span.textContent = W[pair[0]].text;
|
||||
if (pair[1] === "e") {
|
||||
span.style.fontSize = l2Size + "px";
|
||||
} else {
|
||||
span.style.fontSize = l2Size + "px";
|
||||
}
|
||||
l2.appendChild(span);
|
||||
});
|
||||
el.appendChild(l2);
|
||||
}
|
||||
|
||||
stage.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
buildBlocks();
|
||||
|
||||
// Scale blocks that exceed safe width
|
||||
(function fitBlocks() {
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
var el = document.getElementById("b" + bi);
|
||||
el.style.opacity = "1";
|
||||
el.style.position = "relative";
|
||||
var lines = el.querySelectorAll(".caption-line");
|
||||
var maxW = 0;
|
||||
lines.forEach(function (line) {
|
||||
if (line.scrollWidth > maxW) maxW = line.scrollWidth;
|
||||
});
|
||||
if (maxW > SAFE_WIDTH) {
|
||||
var s = SAFE_WIDTH / maxW;
|
||||
el.style.transform = "scale(" + s + ")";
|
||||
}
|
||||
el.style.opacity = "0";
|
||||
el.style.position = "";
|
||||
});
|
||||
})();
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var allEls = BLOCKS.map(function (_, i) {
|
||||
return document.getElementById("b" + i);
|
||||
});
|
||||
|
||||
function blockStart(bi) {
|
||||
return W[BLOCKS[bi].line1[0][0]].start;
|
||||
}
|
||||
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
var el = allEls[bi];
|
||||
var start = blockStart(bi);
|
||||
var nextStart = bi < BLOCKS.length - 1 ? blockStart(bi + 1) : DURATION;
|
||||
|
||||
allEls.forEach(function (other, oi) {
|
||||
if (oi !== bi) tl.set(other, { opacity: 0 }, start);
|
||||
});
|
||||
tl.set(el, { opacity: 1 }, start);
|
||||
|
||||
block.line1.forEach(function (pair, wi) {
|
||||
var wordEl = document.getElementById("b" + bi + "L1w" + wi);
|
||||
var wordStart = W[pair[0]].start;
|
||||
tl.set(wordEl, { opacity: 0, scale: 1.12, transformOrigin: "0% 100%" }, start);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ opacity: 1, scale: 1, duration: ENTRY_DUR, ease: "power2.out" },
|
||||
wordStart,
|
||||
);
|
||||
});
|
||||
|
||||
if (block.line2) {
|
||||
var hasEmphasis = block.line2.some(function (p) {
|
||||
return p[1] === "e";
|
||||
});
|
||||
var l2El = document.getElementById("b" + bi + "L2");
|
||||
if (hasEmphasis) {
|
||||
var l2Start = W[block.line2[0][0]].start;
|
||||
tl.set(l2El, { opacity: 0, x: -1920 }, start);
|
||||
tl.to(l2El, { opacity: 1, x: 0, duration: SLIDE_DUR, ease: "power2.out" }, l2Start);
|
||||
} else {
|
||||
tl.set(l2El, { opacity: 1 }, start);
|
||||
block.line2.forEach(function (pair, wi) {
|
||||
var wordEl = document.getElementById("b" + bi + "L2w" + wi);
|
||||
var wordStart = W[pair[0]].start;
|
||||
tl.set(wordEl, { opacity: 0, scale: 1.12, transformOrigin: "0% 100%" }, start);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ opacity: 1, scale: 1, duration: ENTRY_DUR, ease: "power2.out" },
|
||||
wordStart,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tl.set(el, { opacity: 0 }, nextStart);
|
||||
});
|
||||
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["caption-editorial-emphasis"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,430 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Editorial Emphasis</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&family=Playfair+Display:ital,wght@0,800;1,800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
#editorial-emphasis {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
#bg-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.safe-zone {
|
||||
position: absolute;
|
||||
top: 580px;
|
||||
left: 60px;
|
||||
width: 1800px;
|
||||
height: 430px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.caption-block {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
transform-origin: 0% 0%;
|
||||
will-change: transform, opacity;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 14px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.caption-line + .caption-line {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.word {
|
||||
display: inline-block;
|
||||
color: #f5f0d0;
|
||||
text-shadow:
|
||||
0 2px 12px rgba(0, 0, 0, 0.6),
|
||||
0 4px 24px rgba(0, 0, 0, 0.35);
|
||||
will-change: transform, opacity;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.word--normal {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 86px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.word--italic {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 86px;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.word--emphasis {
|
||||
font-family: "Playfair Display", serif;
|
||||
font-size: 180px;
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
line-height: 0.9;
|
||||
color: #f5f0d0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="editorial-emphasis"
|
||||
data-composition-id="caption-editorial-emphasis"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="bg-video"
|
||||
muted
|
||||
playsinline
|
||||
preload="auto"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var SAFE_WIDTH = 1800;
|
||||
var ENTRY_DUR = 0.1;
|
||||
var SLIDE_DUR = 0.2;
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
// Generic transcript words
|
||||
var W = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
// Block layout: [wordIndex, type] — n=normal, i=italic, e=emphasis(large)
|
||||
// Emphasis words: HyperFrames(8), HTML(12), professional(15), code(24), cinema.(27)
|
||||
var BLOCKS = [
|
||||
{
|
||||
line1: [
|
||||
[0, "n"],
|
||||
[1, "n"],
|
||||
[2, "n"],
|
||||
],
|
||||
line2: [
|
||||
[3, "n"],
|
||||
[4, "n"],
|
||||
],
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[5, "n"],
|
||||
[6, "n"],
|
||||
],
|
||||
line2: [[7, "e"]],
|
||||
},
|
||||
{ line1: [[8, "e"]], line2: null },
|
||||
{
|
||||
line1: [
|
||||
[9, "n"],
|
||||
[10, "n"],
|
||||
[11, "n"],
|
||||
],
|
||||
line2: [[12, "e"]],
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[13, "n"],
|
||||
[14, "n"],
|
||||
],
|
||||
line2: [[15, "e"]],
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[16, "n"],
|
||||
[17, "n"],
|
||||
],
|
||||
line2: null,
|
||||
},
|
||||
{
|
||||
line1: [[18, "n"]],
|
||||
line2: [
|
||||
[19, "n"],
|
||||
[20, "n"],
|
||||
],
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[21, "n"],
|
||||
[22, "n"],
|
||||
],
|
||||
line2: null,
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[23, "i"],
|
||||
[24, "e"],
|
||||
],
|
||||
line2: null,
|
||||
},
|
||||
{
|
||||
line1: [
|
||||
[25, "n"],
|
||||
[26, "n"],
|
||||
],
|
||||
line2: [[27, "e"]],
|
||||
},
|
||||
];
|
||||
|
||||
var CLASS_MAP = { n: "word word--normal", i: "word word--italic", e: "word word--emphasis" };
|
||||
|
||||
function computeLineSize(linePairs) {
|
||||
var hasEmphasis = linePairs.some(function (p) {
|
||||
return p[1] === "e";
|
||||
});
|
||||
if (hasEmphasis) {
|
||||
var lineText = linePairs
|
||||
.map(function (p) {
|
||||
return W[p[0]].text;
|
||||
})
|
||||
.join(" ");
|
||||
return fitFontSize(lineText, 180, "800", "Playfair Display", SAFE_WIDTH);
|
||||
}
|
||||
var lineText = linePairs
|
||||
.map(function (p) {
|
||||
return W[p[0]].text;
|
||||
})
|
||||
.join(" ");
|
||||
return fitFontSize(lineText, 86, "400", "Inter", SAFE_WIDTH);
|
||||
}
|
||||
|
||||
function buildBlocks() {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
var el = document.createElement("div");
|
||||
el.className = "caption-block";
|
||||
el.id = "b" + bi;
|
||||
|
||||
var l1Size = computeLineSize(block.line1);
|
||||
var l1 = document.createElement("div");
|
||||
l1.className = "caption-line";
|
||||
l1.id = "b" + bi + "L1";
|
||||
block.line1.forEach(function (pair, wi) {
|
||||
var span = document.createElement("span");
|
||||
span.className = CLASS_MAP[pair[1]];
|
||||
span.id = "b" + bi + "L1w" + wi;
|
||||
span.textContent = W[pair[0]].text;
|
||||
if (pair[1] === "e") {
|
||||
span.style.fontSize = l1Size + "px";
|
||||
} else {
|
||||
span.style.fontSize = l1Size + "px";
|
||||
}
|
||||
l1.appendChild(span);
|
||||
});
|
||||
el.appendChild(l1);
|
||||
|
||||
if (block.line2) {
|
||||
var l2Size = computeLineSize(block.line2);
|
||||
var l2 = document.createElement("div");
|
||||
l2.className = "caption-line";
|
||||
l2.id = "b" + bi + "L2";
|
||||
block.line2.forEach(function (pair, wi) {
|
||||
var span = document.createElement("span");
|
||||
span.className = CLASS_MAP[pair[1]];
|
||||
span.id = "b" + bi + "L2w" + wi;
|
||||
span.textContent = W[pair[0]].text;
|
||||
if (pair[1] === "e") {
|
||||
span.style.fontSize = l2Size + "px";
|
||||
} else {
|
||||
span.style.fontSize = l2Size + "px";
|
||||
}
|
||||
l2.appendChild(span);
|
||||
});
|
||||
el.appendChild(l2);
|
||||
}
|
||||
|
||||
stage.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
buildBlocks();
|
||||
|
||||
// Scale blocks that exceed safe width
|
||||
(function fitBlocks() {
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
var el = document.getElementById("b" + bi);
|
||||
el.style.opacity = "1";
|
||||
el.style.position = "relative";
|
||||
var lines = el.querySelectorAll(".caption-line");
|
||||
var maxW = 0;
|
||||
lines.forEach(function (line) {
|
||||
if (line.scrollWidth > maxW) maxW = line.scrollWidth;
|
||||
});
|
||||
if (maxW > SAFE_WIDTH) {
|
||||
var s = SAFE_WIDTH / maxW;
|
||||
el.style.transform = "scale(" + s + ")";
|
||||
}
|
||||
el.style.opacity = "0";
|
||||
el.style.position = "";
|
||||
});
|
||||
})();
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var allEls = BLOCKS.map(function (_, i) {
|
||||
return document.getElementById("b" + i);
|
||||
});
|
||||
|
||||
function blockStart(bi) {
|
||||
return W[BLOCKS[bi].line1[0][0]].start;
|
||||
}
|
||||
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
var el = allEls[bi];
|
||||
var start = blockStart(bi);
|
||||
var nextStart = bi < BLOCKS.length - 1 ? blockStart(bi + 1) : DURATION;
|
||||
|
||||
allEls.forEach(function (other, oi) {
|
||||
if (oi !== bi) tl.set(other, { opacity: 0 }, start);
|
||||
});
|
||||
tl.set(el, { opacity: 1 }, start);
|
||||
|
||||
block.line1.forEach(function (pair, wi) {
|
||||
var wordEl = document.getElementById("b" + bi + "L1w" + wi);
|
||||
var wordStart = W[pair[0]].start;
|
||||
tl.set(wordEl, { opacity: 0, scale: 1.12, transformOrigin: "0% 100%" }, start);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ opacity: 1, scale: 1, duration: ENTRY_DUR, ease: "power2.out" },
|
||||
wordStart,
|
||||
);
|
||||
});
|
||||
|
||||
if (block.line2) {
|
||||
var hasEmphasis = block.line2.some(function (p) {
|
||||
return p[1] === "e";
|
||||
});
|
||||
var l2El = document.getElementById("b" + bi + "L2");
|
||||
if (hasEmphasis) {
|
||||
var l2Start = W[block.line2[0][0]].start;
|
||||
tl.set(l2El, { opacity: 0, x: -1920 }, start);
|
||||
tl.to(l2El, { opacity: 1, x: 0, duration: SLIDE_DUR, ease: "power2.out" }, l2Start);
|
||||
} else {
|
||||
tl.set(l2El, { opacity: 1 }, start);
|
||||
block.line2.forEach(function (pair, wi) {
|
||||
var wordEl = document.getElementById("b" + bi + "L2w" + wi);
|
||||
var wordStart = W[pair[0]].start;
|
||||
tl.set(wordEl, { opacity: 0, scale: 1.12, transformOrigin: "0% 100%" }, start);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ opacity: 1, scale: 1, duration: ENTRY_DUR, ease: "power2.out" },
|
||||
wordStart,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tl.set(el, { opacity: 0 }, nextStart);
|
||||
});
|
||||
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["caption-editorial-emphasis"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-editorial-emphasis",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Editorial Emphasis",
|
||||
"description": "Dual-font system with dramatic size contrast for emphasis words",
|
||||
"tags": ["captions", "caption-style", "editorial", "typography", "emphasis"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-editorial-emphasis.html",
|
||||
"target": "compositions/components/caption-editorial-emphasis.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-editorial-emphasis/preview.mp4?v=1779051416",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-editorial-emphasis.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Emoji Pop</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=Gabarito:wght@900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
font-family: "Gabarito", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#emoji-pop {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#avatar-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.safe-zone {
|
||||
position: absolute;
|
||||
bottom: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1400px;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1400px;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transform-origin: 50% 50%;
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.caption-emoji {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, calc(-50% - 70px));
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 56px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
width: max-content;
|
||||
max-width: 1400px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
color: #ffffff;
|
||||
font-family: "Gabarito", Arial, sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 72px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
text-transform: uppercase;
|
||||
-webkit-text-stroke: 3px #000000;
|
||||
paint-order: stroke fill;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="emoji-pop"
|
||||
data-composition-id="caption-emoji-pop"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="avatar-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var SAFE_ZONE_WIDTH = 1400;
|
||||
var CAPTION_FONT_SIZE = 72;
|
||||
var MAX_WORDS_PER_GROUP = 3;
|
||||
var WORD_SPACING = 14;
|
||||
var PAUSE_THRESHOLD = 0.1;
|
||||
var ENTRY_FRAMES = 4;
|
||||
var EXIT_FRAMES = 3;
|
||||
var FPS = 30;
|
||||
var ENTRY_DURATION = ENTRY_FRAMES / FPS;
|
||||
var EXIT_DURATION = EXIT_FRAMES / FPS;
|
||||
var FONT_WIDTH_SAFETY = 1.14;
|
||||
var PRIMARY_COLOR = "#FFFFFF";
|
||||
var ACCENT_COLORS = ["#FF76FF", "#FF0002", "#B2F7FF"];
|
||||
var KEYWORDS = new Set(["hyperframes", "html", "professional", "code", "cinema"]);
|
||||
var TRANSITION_WORDS = new Set([
|
||||
"the",
|
||||
"a",
|
||||
"an",
|
||||
"and",
|
||||
"or",
|
||||
"but",
|
||||
"is",
|
||||
"are",
|
||||
"was",
|
||||
"were",
|
||||
"to",
|
||||
"of",
|
||||
"in",
|
||||
"on",
|
||||
"at",
|
||||
"for",
|
||||
"with",
|
||||
"by",
|
||||
"from",
|
||||
"as",
|
||||
"it",
|
||||
"its",
|
||||
"this",
|
||||
"that",
|
||||
"these",
|
||||
"those",
|
||||
"be",
|
||||
"been",
|
||||
"have",
|
||||
"has",
|
||||
"had",
|
||||
"do",
|
||||
"does",
|
||||
"did",
|
||||
"will",
|
||||
"would",
|
||||
"could",
|
||||
"should",
|
||||
"may",
|
||||
"might",
|
||||
"must",
|
||||
"can",
|
||||
"if",
|
||||
"then",
|
||||
"so",
|
||||
"just",
|
||||
"not",
|
||||
"no",
|
||||
"yes",
|
||||
"up",
|
||||
"out",
|
||||
"about",
|
||||
]);
|
||||
|
||||
// Emoji map for key words
|
||||
var WORD_EMOJI = {
|
||||
hyperframes: "🎬",
|
||||
html: "💻",
|
||||
professional: "✨",
|
||||
code: "⌨️",
|
||||
cinema: "🎥",
|
||||
video: "📹",
|
||||
frame: "🖼️",
|
||||
};
|
||||
|
||||
var TRANSCRIPT = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
function normalizeWords(rawWords) {
|
||||
return rawWords
|
||||
.map(function (item) {
|
||||
var text = String(item.word || item.text || "").trim();
|
||||
var clean = text.toLowerCase().replace(/[^a-z]/g, "");
|
||||
return {
|
||||
text: text,
|
||||
start: Math.max(0, Number(item.start) || 0),
|
||||
end: Math.min(DURATION, Math.max(Number(item.start) || 0, Number(item.end) || 0)),
|
||||
emoji: WORD_EMOJI[clean] || null,
|
||||
};
|
||||
})
|
||||
.filter(function (word) {
|
||||
return word.text.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function seededRandom(seed) {
|
||||
var x = Math.sin(seed * 9999) * 10000;
|
||||
return x - Math.floor(x);
|
||||
}
|
||||
|
||||
function assignWordColors(words, groupIndex) {
|
||||
return words.map(function (word, index) {
|
||||
var clean = word.text.toLowerCase().replace(/[^a-z]/g, "");
|
||||
if (KEYWORDS.has(clean)) {
|
||||
var ci = Math.floor(seededRandom(groupIndex * 23 + index * 31) * ACCENT_COLORS.length);
|
||||
return ACCENT_COLORS[ci];
|
||||
}
|
||||
if (TRANSITION_WORDS.has(clean)) return PRIMARY_COLOR;
|
||||
var rand = seededRandom(groupIndex * 13 + index);
|
||||
if (rand > 0.55) {
|
||||
var ci2 = Math.floor(rand * ACCENT_COLORS.length) % ACCENT_COLORS.length;
|
||||
return ACCENT_COLORS[ci2];
|
||||
}
|
||||
return PRIMARY_COLOR;
|
||||
});
|
||||
}
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var measureCanvas = document.createElement("canvas");
|
||||
var measureContext = measureCanvas.getContext("2d");
|
||||
|
||||
function measureWord(word, fontSize) {
|
||||
measureContext.font = "900 " + fontSize + "px Gabarito";
|
||||
return measureContext.measureText(word.text.toUpperCase()).width * FONT_WIDTH_SAFETY;
|
||||
}
|
||||
|
||||
function measureLineWidth(words, fontSize) {
|
||||
return words.reduce(function (total, word, index) {
|
||||
return total + measureWord(word, fontSize) + (index ? WORD_SPACING : 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function fitsLine(words) {
|
||||
return measureLineWidth(words, CAPTION_FONT_SIZE) <= SAFE_ZONE_WIDTH;
|
||||
}
|
||||
|
||||
function makeGroups(words) {
|
||||
var groups = [];
|
||||
var current = [];
|
||||
words.forEach(function (word, index) {
|
||||
var nextWord = words[index + 1];
|
||||
var testGroup = current.concat([word]);
|
||||
if (testGroup.length > MAX_WORDS_PER_GROUP || !fitsLine(testGroup)) {
|
||||
if (current.length) groups.push(makeGroup(current, groups.length));
|
||||
current = [word];
|
||||
return;
|
||||
}
|
||||
current.push(word);
|
||||
var punctuation = /[,.:!?]$/.test(word.text);
|
||||
var pause = nextWord ? nextWord.start - word.end : 0;
|
||||
if (
|
||||
!nextWord ||
|
||||
punctuation ||
|
||||
pause >= PAUSE_THRESHOLD ||
|
||||
current.length >= MAX_WORDS_PER_GROUP
|
||||
) {
|
||||
groups.push(makeGroup(current, groups.length));
|
||||
current = [];
|
||||
}
|
||||
});
|
||||
if (current.length) groups.push(makeGroup(current, groups.length));
|
||||
return groups;
|
||||
}
|
||||
|
||||
function makeGroup(words, groupIndex) {
|
||||
var colors = assignWordColors(words, groupIndex);
|
||||
var emoji = null;
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
if (words[i].emoji) {
|
||||
emoji = words[i].emoji;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
words: words.slice(),
|
||||
colors: colors,
|
||||
emoji: emoji,
|
||||
start: words[0].start,
|
||||
end: words[words.length - 1].end,
|
||||
};
|
||||
}
|
||||
|
||||
function shadowForColor(color) {
|
||||
var hex = color.replace("#", "");
|
||||
var r = parseInt(hex.slice(0, 2), 16);
|
||||
var g = parseInt(hex.slice(2, 4), 16);
|
||||
var b = parseInt(hex.slice(4, 6), 16);
|
||||
return (
|
||||
"0 4px 8px rgba(0,0,0,0.7), 0 0 2px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",1), 0 0 8px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",0.6)"
|
||||
);
|
||||
}
|
||||
|
||||
function buildCaptions(groups) {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
groups.forEach(function (group, groupIndex) {
|
||||
var groupText = group.words
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var computedSize = fitFontSize(
|
||||
groupText,
|
||||
CAPTION_FONT_SIZE,
|
||||
"900",
|
||||
"Gabarito",
|
||||
SAFE_ZONE_WIDTH,
|
||||
);
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.id = "caption-group-" + groupIndex;
|
||||
groupEl.className = "caption-group";
|
||||
|
||||
if (group.emoji) {
|
||||
var emojiEl = document.createElement("div");
|
||||
emojiEl.className = "caption-emoji";
|
||||
emojiEl.textContent = group.emoji;
|
||||
groupEl.appendChild(emojiEl);
|
||||
}
|
||||
|
||||
var lineEl = document.createElement("div");
|
||||
lineEl.className = "caption-line";
|
||||
group.words.forEach(function (word, wordIndex) {
|
||||
var color = group.colors[wordIndex] || PRIMARY_COLOR;
|
||||
var wordEl = document.createElement("span");
|
||||
wordEl.className = "caption-word";
|
||||
wordEl.textContent = word.text.toUpperCase();
|
||||
wordEl.style.color = color;
|
||||
wordEl.style.fontSize = computedSize + "px";
|
||||
wordEl.style.textShadow = shadowForColor(color);
|
||||
lineEl.appendChild(wordEl);
|
||||
});
|
||||
|
||||
groupEl.appendChild(lineEl);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
}
|
||||
|
||||
var WORDS = normalizeWords(TRANSCRIPT);
|
||||
var GROUPS = makeGroups(WORDS);
|
||||
buildCaptions(GROUPS);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var visibleStarts = GROUPS.map(function (group, groupIndex) {
|
||||
var prev = GROUPS[groupIndex - 1];
|
||||
var availableGap = prev ? Math.max(0, group.start - prev.end) : ENTRY_DURATION;
|
||||
var entryLead = Math.min(ENTRY_DURATION, availableGap);
|
||||
return Math.max(0, group.start - entryLead);
|
||||
});
|
||||
|
||||
var allGroupEls = GROUPS.map(function (_, i) {
|
||||
return document.getElementById("caption-group-" + i);
|
||||
});
|
||||
|
||||
GROUPS.forEach(function (group, groupIndex) {
|
||||
var groupEl = allGroupEls[groupIndex];
|
||||
var visibleStart = visibleStarts[groupIndex];
|
||||
var visibleEnd = groupIndex < GROUPS.length - 1 ? visibleStarts[groupIndex + 1] : DURATION;
|
||||
var exitStart = Math.max(visibleStart, visibleEnd - EXIT_DURATION);
|
||||
|
||||
allGroupEls.forEach(function (otherEl, otherIndex) {
|
||||
if (otherIndex !== groupIndex) tl.set(otherEl, { opacity: 0 }, visibleStart);
|
||||
});
|
||||
|
||||
tl.set(groupEl, { opacity: 0, scaleX: 0.8, scaleY: 1 }, visibleStart);
|
||||
tl.to(
|
||||
groupEl,
|
||||
{ opacity: 1, scaleX: 1, duration: ENTRY_DURATION, ease: "power3.out", force3D: true },
|
||||
visibleStart,
|
||||
);
|
||||
tl.to(
|
||||
groupEl,
|
||||
{ scaleX: 0.75, opacity: 0.8, duration: EXIT_DURATION, ease: "power2.in", force3D: true },
|
||||
exitStart,
|
||||
);
|
||||
tl.set(groupEl, { opacity: 0 }, visibleEnd);
|
||||
});
|
||||
|
||||
window.__timelines["caption-emoji-pop"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,484 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Emoji Pop</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=Gabarito:wght@900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
font-family: "Gabarito", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#emoji-pop {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
#avatar-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.safe-zone {
|
||||
position: absolute;
|
||||
bottom: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1400px;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1400px;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transform-origin: 50% 50%;
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.caption-emoji {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, calc(-50% - 70px));
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
font-size: 56px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
width: max-content;
|
||||
max-width: 1400px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
color: #ffffff;
|
||||
font-family: "Gabarito", Arial, sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 72px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
text-transform: uppercase;
|
||||
-webkit-text-stroke: 3px #000000;
|
||||
paint-order: stroke fill;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="emoji-pop"
|
||||
data-composition-id="caption-emoji-pop"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="avatar-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var SAFE_ZONE_WIDTH = 1400;
|
||||
var CAPTION_FONT_SIZE = 72;
|
||||
var MAX_WORDS_PER_GROUP = 3;
|
||||
var WORD_SPACING = 14;
|
||||
var PAUSE_THRESHOLD = 0.1;
|
||||
var ENTRY_FRAMES = 4;
|
||||
var EXIT_FRAMES = 3;
|
||||
var FPS = 30;
|
||||
var ENTRY_DURATION = ENTRY_FRAMES / FPS;
|
||||
var EXIT_DURATION = EXIT_FRAMES / FPS;
|
||||
var FONT_WIDTH_SAFETY = 1.14;
|
||||
var PRIMARY_COLOR = "#FFFFFF";
|
||||
var ACCENT_COLORS = ["#FF76FF", "#FF0002", "#B2F7FF"];
|
||||
var KEYWORDS = new Set(["hyperframes", "html", "professional", "code", "cinema"]);
|
||||
var TRANSITION_WORDS = new Set([
|
||||
"the",
|
||||
"a",
|
||||
"an",
|
||||
"and",
|
||||
"or",
|
||||
"but",
|
||||
"is",
|
||||
"are",
|
||||
"was",
|
||||
"were",
|
||||
"to",
|
||||
"of",
|
||||
"in",
|
||||
"on",
|
||||
"at",
|
||||
"for",
|
||||
"with",
|
||||
"by",
|
||||
"from",
|
||||
"as",
|
||||
"it",
|
||||
"its",
|
||||
"this",
|
||||
"that",
|
||||
"these",
|
||||
"those",
|
||||
"be",
|
||||
"been",
|
||||
"have",
|
||||
"has",
|
||||
"had",
|
||||
"do",
|
||||
"does",
|
||||
"did",
|
||||
"will",
|
||||
"would",
|
||||
"could",
|
||||
"should",
|
||||
"may",
|
||||
"might",
|
||||
"must",
|
||||
"can",
|
||||
"if",
|
||||
"then",
|
||||
"so",
|
||||
"just",
|
||||
"not",
|
||||
"no",
|
||||
"yes",
|
||||
"up",
|
||||
"out",
|
||||
"about",
|
||||
]);
|
||||
|
||||
// Emoji map for key words
|
||||
var WORD_EMOJI = {
|
||||
hyperframes: "🎬",
|
||||
html: "💻",
|
||||
professional: "✨",
|
||||
code: "⌨️",
|
||||
cinema: "🎥",
|
||||
video: "📹",
|
||||
frame: "🖼️",
|
||||
};
|
||||
|
||||
var TRANSCRIPT = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
function normalizeWords(rawWords) {
|
||||
return rawWords
|
||||
.map(function (item) {
|
||||
var text = String(item.word || item.text || "").trim();
|
||||
var clean = text.toLowerCase().replace(/[^a-z]/g, "");
|
||||
return {
|
||||
text: text,
|
||||
start: Math.max(0, Number(item.start) || 0),
|
||||
end: Math.min(DURATION, Math.max(Number(item.start) || 0, Number(item.end) || 0)),
|
||||
emoji: WORD_EMOJI[clean] || null,
|
||||
};
|
||||
})
|
||||
.filter(function (word) {
|
||||
return word.text.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function seededRandom(seed) {
|
||||
var x = Math.sin(seed * 9999) * 10000;
|
||||
return x - Math.floor(x);
|
||||
}
|
||||
|
||||
function assignWordColors(words, groupIndex) {
|
||||
return words.map(function (word, index) {
|
||||
var clean = word.text.toLowerCase().replace(/[^a-z]/g, "");
|
||||
if (KEYWORDS.has(clean)) {
|
||||
var ci = Math.floor(seededRandom(groupIndex * 23 + index * 31) * ACCENT_COLORS.length);
|
||||
return ACCENT_COLORS[ci];
|
||||
}
|
||||
if (TRANSITION_WORDS.has(clean)) return PRIMARY_COLOR;
|
||||
var rand = seededRandom(groupIndex * 13 + index);
|
||||
if (rand > 0.55) {
|
||||
var ci2 = Math.floor(rand * ACCENT_COLORS.length) % ACCENT_COLORS.length;
|
||||
return ACCENT_COLORS[ci2];
|
||||
}
|
||||
return PRIMARY_COLOR;
|
||||
});
|
||||
}
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var measureCanvas = document.createElement("canvas");
|
||||
var measureContext = measureCanvas.getContext("2d");
|
||||
|
||||
function measureWord(word, fontSize) {
|
||||
measureContext.font = "900 " + fontSize + "px Gabarito";
|
||||
return measureContext.measureText(word.text.toUpperCase()).width * FONT_WIDTH_SAFETY;
|
||||
}
|
||||
|
||||
function measureLineWidth(words, fontSize) {
|
||||
return words.reduce(function (total, word, index) {
|
||||
return total + measureWord(word, fontSize) + (index ? WORD_SPACING : 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function fitsLine(words) {
|
||||
return measureLineWidth(words, CAPTION_FONT_SIZE) <= SAFE_ZONE_WIDTH;
|
||||
}
|
||||
|
||||
function makeGroups(words) {
|
||||
var groups = [];
|
||||
var current = [];
|
||||
words.forEach(function (word, index) {
|
||||
var nextWord = words[index + 1];
|
||||
var testGroup = current.concat([word]);
|
||||
if (testGroup.length > MAX_WORDS_PER_GROUP || !fitsLine(testGroup)) {
|
||||
if (current.length) groups.push(makeGroup(current, groups.length));
|
||||
current = [word];
|
||||
return;
|
||||
}
|
||||
current.push(word);
|
||||
var punctuation = /[,.:!?]$/.test(word.text);
|
||||
var pause = nextWord ? nextWord.start - word.end : 0;
|
||||
if (
|
||||
!nextWord ||
|
||||
punctuation ||
|
||||
pause >= PAUSE_THRESHOLD ||
|
||||
current.length >= MAX_WORDS_PER_GROUP
|
||||
) {
|
||||
groups.push(makeGroup(current, groups.length));
|
||||
current = [];
|
||||
}
|
||||
});
|
||||
if (current.length) groups.push(makeGroup(current, groups.length));
|
||||
return groups;
|
||||
}
|
||||
|
||||
function makeGroup(words, groupIndex) {
|
||||
var colors = assignWordColors(words, groupIndex);
|
||||
var emoji = null;
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
if (words[i].emoji) {
|
||||
emoji = words[i].emoji;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
words: words.slice(),
|
||||
colors: colors,
|
||||
emoji: emoji,
|
||||
start: words[0].start,
|
||||
end: words[words.length - 1].end,
|
||||
};
|
||||
}
|
||||
|
||||
function shadowForColor(color) {
|
||||
var hex = color.replace("#", "");
|
||||
var r = parseInt(hex.slice(0, 2), 16);
|
||||
var g = parseInt(hex.slice(2, 4), 16);
|
||||
var b = parseInt(hex.slice(4, 6), 16);
|
||||
return (
|
||||
"0 4px 8px rgba(0,0,0,0.7), 0 0 2px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",1), 0 0 8px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",0.6)"
|
||||
);
|
||||
}
|
||||
|
||||
function buildCaptions(groups) {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
groups.forEach(function (group, groupIndex) {
|
||||
var groupText = group.words
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var computedSize = fitFontSize(
|
||||
groupText,
|
||||
CAPTION_FONT_SIZE,
|
||||
"900",
|
||||
"Gabarito",
|
||||
SAFE_ZONE_WIDTH,
|
||||
);
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.id = "caption-group-" + groupIndex;
|
||||
groupEl.className = "caption-group";
|
||||
|
||||
if (group.emoji) {
|
||||
var emojiEl = document.createElement("div");
|
||||
emojiEl.className = "caption-emoji";
|
||||
emojiEl.textContent = group.emoji;
|
||||
groupEl.appendChild(emojiEl);
|
||||
}
|
||||
|
||||
var lineEl = document.createElement("div");
|
||||
lineEl.className = "caption-line";
|
||||
group.words.forEach(function (word, wordIndex) {
|
||||
var color = group.colors[wordIndex] || PRIMARY_COLOR;
|
||||
var wordEl = document.createElement("span");
|
||||
wordEl.className = "caption-word";
|
||||
wordEl.textContent = word.text.toUpperCase();
|
||||
wordEl.style.color = color;
|
||||
wordEl.style.fontSize = computedSize + "px";
|
||||
wordEl.style.textShadow = shadowForColor(color);
|
||||
lineEl.appendChild(wordEl);
|
||||
});
|
||||
|
||||
groupEl.appendChild(lineEl);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
}
|
||||
|
||||
var WORDS = normalizeWords(TRANSCRIPT);
|
||||
var GROUPS = makeGroups(WORDS);
|
||||
buildCaptions(GROUPS);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var visibleStarts = GROUPS.map(function (group, groupIndex) {
|
||||
var prev = GROUPS[groupIndex - 1];
|
||||
var availableGap = prev ? Math.max(0, group.start - prev.end) : ENTRY_DURATION;
|
||||
var entryLead = Math.min(ENTRY_DURATION, availableGap);
|
||||
return Math.max(0, group.start - entryLead);
|
||||
});
|
||||
|
||||
var allGroupEls = GROUPS.map(function (_, i) {
|
||||
return document.getElementById("caption-group-" + i);
|
||||
});
|
||||
|
||||
GROUPS.forEach(function (group, groupIndex) {
|
||||
var groupEl = allGroupEls[groupIndex];
|
||||
var visibleStart = visibleStarts[groupIndex];
|
||||
var visibleEnd = groupIndex < GROUPS.length - 1 ? visibleStarts[groupIndex + 1] : DURATION;
|
||||
var exitStart = Math.max(visibleStart, visibleEnd - EXIT_DURATION);
|
||||
|
||||
allGroupEls.forEach(function (otherEl, otherIndex) {
|
||||
if (otherIndex !== groupIndex) tl.set(otherEl, { opacity: 0 }, visibleStart);
|
||||
});
|
||||
|
||||
tl.set(groupEl, { opacity: 0, scaleX: 0.8, scaleY: 1 }, visibleStart);
|
||||
tl.to(
|
||||
groupEl,
|
||||
{ opacity: 1, scaleX: 1, duration: ENTRY_DURATION, ease: "power3.out", force3D: true },
|
||||
visibleStart,
|
||||
);
|
||||
tl.to(
|
||||
groupEl,
|
||||
{ scaleX: 0.75, opacity: 0.8, duration: EXIT_DURATION, ease: "power2.in", force3D: true },
|
||||
exitStart,
|
||||
);
|
||||
tl.set(groupEl, { opacity: 0 }, visibleEnd);
|
||||
});
|
||||
|
||||
window.__timelines["caption-emoji-pop"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-emoji-pop",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Emoji Pop",
|
||||
"description": "Emoji integration with stroked text and horizontal squeeze entrance",
|
||||
"tags": ["captions", "caption-style", "emoji", "social"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-emoji-pop.html",
|
||||
"target": "compositions/components/caption-emoji-pop.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-emoji-pop/preview.mp4?v=1779051416",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-emoji-pop.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Glitch RGB</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=Space+Grotesk:wght@700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#caption-glitch-rgb {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#gl-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.gl-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0.08) 0%,
|
||||
rgba(0, 0, 0, 0.16) 54%,
|
||||
rgba(0, 0, 0, 0.5) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
.gl-scanlines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0px,
|
||||
transparent 3px,
|
||||
rgba(0, 0, 0, 0.16) 3px,
|
||||
rgba(0, 0, 0, 0.16) 4px
|
||||
);
|
||||
opacity: 0.55;
|
||||
}
|
||||
#gl-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.gl-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 0 100px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.gl-word {
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 88px;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.03em;
|
||||
line-height: 0.96;
|
||||
text-shadow: 0 5px 18px rgba(0, 0, 0, 0.52);
|
||||
will-change: transform, text-shadow;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="caption-glitch-rgb"
|
||||
data-composition-id="caption-glitch-rgb"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="gl-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="gl-overlay"></div>
|
||||
<div class="gl-scanlines"></div>
|
||||
<div id="gl-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var TRANSCRIPT = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
function buildGroups(words) {
|
||||
var groups = [];
|
||||
var start = 0;
|
||||
var chars = 0;
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
chars += words[i].text.length + 1;
|
||||
var endsSentence = /[.!?]$/.test(words[i].text);
|
||||
var nextGap = i + 1 < words.length ? words[i + 1].start - words[i].end : 0;
|
||||
var full = i - start >= 3 || chars >= 21;
|
||||
if (endsSentence || nextGap > 0.28 || full || i === words.length - 1) {
|
||||
groups.push([start, i]);
|
||||
start = i + 1;
|
||||
chars = 0;
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function glitchPlan(word, nextWord) {
|
||||
var nextStart = nextWord ? nextWord.start : word.end + 0.3;
|
||||
var available = Math.min(word.end, nextStart) - word.start - 0.02;
|
||||
var total = Math.min(0.24, Math.max(0.08, available));
|
||||
return {
|
||||
d1: total * 0.35,
|
||||
d2: total * 0.65,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldAftershock(word) {
|
||||
return /\d|\$|%/.test(word.text) || word.text.replace(/[^\w]/g, "").length >= 7;
|
||||
}
|
||||
|
||||
var RAW_GROUPS = buildGroups(TRANSCRIPT);
|
||||
var GROUPS = RAW_GROUPS.map(function (pair, gi) {
|
||||
var ws = pair[0];
|
||||
var we = pair[1];
|
||||
var nextStart =
|
||||
gi + 1 < RAW_GROUPS.length ? TRANSCRIPT[RAW_GROUPS[gi + 1][0]].start : 8.0;
|
||||
var end = Math.min(TRANSCRIPT[we].end + 0.24, nextStart - 0.04);
|
||||
return { wordStart: ws, wordEnd: we, start: TRANSCRIPT[ws].start, end: end };
|
||||
});
|
||||
|
||||
var BASE_FONT = 88;
|
||||
var container = document.getElementById("gl-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = TRANSCRIPT.slice(g.wordStart, g.wordEnd + 1);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "gl-group";
|
||||
grp.id = "gl-grp-" + gi;
|
||||
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var computedSize = fitFontSize(groupText, BASE_FONT, "700", "Space Grotesk", 1720);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var span = document.createElement("span");
|
||||
span.className = "gl-word";
|
||||
span.id = "gl-w-" + wi;
|
||||
span.textContent = w.text.toUpperCase();
|
||||
span.style.fontSize = computedSize + "px";
|
||||
grp.appendChild(span);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible" }, g.start);
|
||||
tl.fromTo(
|
||||
grp,
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.12, ease: "power2.out" },
|
||||
g.start,
|
||||
);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var wordEl = document.getElementById("gl-w-" + wi);
|
||||
var plan = glitchPlan(w, TRANSCRIPT[wi + 1]);
|
||||
|
||||
var rng = mulberry32(wi * 777 + 13);
|
||||
var travel = -(8 + rng() * 8);
|
||||
var shadowMag = 5 + rng() * 7;
|
||||
var sm = shadowMag.toFixed(1);
|
||||
var nextWord = TRANSCRIPT[wi + 1];
|
||||
var nextStart = nextWord ? nextWord.start : g.end;
|
||||
var aftershockEnd = Math.min(g.end - 0.08, nextStart - 0.04);
|
||||
|
||||
tl.to(
|
||||
wordEl,
|
||||
{
|
||||
x: travel,
|
||||
textShadow:
|
||||
sm + "px 0 #ff003c, -" + sm + "px 0 #00e5ff, 0 5px 18px rgba(0,0,0,0.52)",
|
||||
duration: plan.d1,
|
||||
ease: "none",
|
||||
},
|
||||
w.start,
|
||||
);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{
|
||||
x: 0,
|
||||
textShadow: "0 5px 18px rgba(0,0,0,0.52)",
|
||||
duration: plan.d2,
|
||||
ease: "power3.out",
|
||||
},
|
||||
w.start + plan.d1,
|
||||
);
|
||||
|
||||
if (shouldAftershock(w) && aftershockEnd - w.end > 0.22) {
|
||||
var pulseStart = w.end + 0.08;
|
||||
var pulseIndex = 0;
|
||||
while (pulseStart + 0.1 < aftershockEnd) {
|
||||
var pulseTravel = -(3 + rng() * 5);
|
||||
var pulseShadow = (3 + rng() * 5).toFixed(1);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{
|
||||
x: pulseTravel,
|
||||
textShadow:
|
||||
pulseShadow +
|
||||
"px 0 #ff003c, -" +
|
||||
pulseShadow +
|
||||
"px 0 #00e5ff, 0 5px 18px rgba(0,0,0,0.52)",
|
||||
duration: 0.045,
|
||||
ease: "none",
|
||||
},
|
||||
pulseStart,
|
||||
);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{
|
||||
x: 0,
|
||||
textShadow: "0 5px 18px rgba(0,0,0,0.52)",
|
||||
duration: 0.075,
|
||||
ease: "power3.out",
|
||||
},
|
||||
pulseStart + 0.045,
|
||||
);
|
||||
pulseIndex++;
|
||||
pulseStart += pulseIndex % 2 === 0 ? 0.18 : 0.24;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, duration: 0.1, ease: "power2.in" }, g.end - 0.1);
|
||||
tl.set(grp, { opacity: 0, visibility: "hidden" }, g.end);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-glitch-rgb"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,344 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Glitch RGB</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=Space+Grotesk:wght@700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#caption-glitch-rgb {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#gl-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.gl-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0.08) 0%,
|
||||
rgba(0, 0, 0, 0.16) 54%,
|
||||
rgba(0, 0, 0, 0.5) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
.gl-scanlines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0px,
|
||||
transparent 3px,
|
||||
rgba(0, 0, 0, 0.16) 3px,
|
||||
rgba(0, 0, 0, 0.16) 4px
|
||||
);
|
||||
opacity: 0.55;
|
||||
}
|
||||
#gl-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.gl-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 0 100px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.gl-word {
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 88px;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.03em;
|
||||
line-height: 0.96;
|
||||
text-shadow: 0 5px 18px rgba(0, 0, 0, 0.52);
|
||||
will-change: transform, text-shadow;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="caption-glitch-rgb"
|
||||
data-composition-id="caption-glitch-rgb"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="gl-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="gl-overlay"></div>
|
||||
<div class="gl-scanlines"></div>
|
||||
<div id="gl-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var TRANSCRIPT = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
function buildGroups(words) {
|
||||
var groups = [];
|
||||
var start = 0;
|
||||
var chars = 0;
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
chars += words[i].text.length + 1;
|
||||
var endsSentence = /[.!?]$/.test(words[i].text);
|
||||
var nextGap = i + 1 < words.length ? words[i + 1].start - words[i].end : 0;
|
||||
var full = i - start >= 3 || chars >= 21;
|
||||
if (endsSentence || nextGap > 0.28 || full || i === words.length - 1) {
|
||||
groups.push([start, i]);
|
||||
start = i + 1;
|
||||
chars = 0;
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function glitchPlan(word, nextWord) {
|
||||
var nextStart = nextWord ? nextWord.start : word.end + 0.3;
|
||||
var available = Math.min(word.end, nextStart) - word.start - 0.02;
|
||||
var total = Math.min(0.24, Math.max(0.08, available));
|
||||
return {
|
||||
d1: total * 0.35,
|
||||
d2: total * 0.65,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldAftershock(word) {
|
||||
return /\d|\$|%/.test(word.text) || word.text.replace(/[^\w]/g, "").length >= 7;
|
||||
}
|
||||
|
||||
var RAW_GROUPS = buildGroups(TRANSCRIPT);
|
||||
var GROUPS = RAW_GROUPS.map(function (pair, gi) {
|
||||
var ws = pair[0];
|
||||
var we = pair[1];
|
||||
var nextStart =
|
||||
gi + 1 < RAW_GROUPS.length ? TRANSCRIPT[RAW_GROUPS[gi + 1][0]].start : 8.0;
|
||||
var end = Math.min(TRANSCRIPT[we].end + 0.24, nextStart - 0.04);
|
||||
return { wordStart: ws, wordEnd: we, start: TRANSCRIPT[ws].start, end: end };
|
||||
});
|
||||
|
||||
var BASE_FONT = 88;
|
||||
var container = document.getElementById("gl-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = TRANSCRIPT.slice(g.wordStart, g.wordEnd + 1);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "gl-group";
|
||||
grp.id = "gl-grp-" + gi;
|
||||
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var computedSize = fitFontSize(groupText, BASE_FONT, "700", "Space Grotesk", 1720);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var span = document.createElement("span");
|
||||
span.className = "gl-word";
|
||||
span.id = "gl-w-" + wi;
|
||||
span.textContent = w.text.toUpperCase();
|
||||
span.style.fontSize = computedSize + "px";
|
||||
grp.appendChild(span);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible" }, g.start);
|
||||
tl.fromTo(
|
||||
grp,
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.12, ease: "power2.out" },
|
||||
g.start,
|
||||
);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var wordEl = document.getElementById("gl-w-" + wi);
|
||||
var plan = glitchPlan(w, TRANSCRIPT[wi + 1]);
|
||||
|
||||
var rng = mulberry32(wi * 777 + 13);
|
||||
var travel = -(8 + rng() * 8);
|
||||
var shadowMag = 5 + rng() * 7;
|
||||
var sm = shadowMag.toFixed(1);
|
||||
var nextWord = TRANSCRIPT[wi + 1];
|
||||
var nextStart = nextWord ? nextWord.start : g.end;
|
||||
var aftershockEnd = Math.min(g.end - 0.08, nextStart - 0.04);
|
||||
|
||||
tl.to(
|
||||
wordEl,
|
||||
{
|
||||
x: travel,
|
||||
textShadow:
|
||||
sm + "px 0 #ff003c, -" + sm + "px 0 #00e5ff, 0 5px 18px rgba(0,0,0,0.52)",
|
||||
duration: plan.d1,
|
||||
ease: "none",
|
||||
},
|
||||
w.start,
|
||||
);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{
|
||||
x: 0,
|
||||
textShadow: "0 5px 18px rgba(0,0,0,0.52)",
|
||||
duration: plan.d2,
|
||||
ease: "power3.out",
|
||||
},
|
||||
w.start + plan.d1,
|
||||
);
|
||||
|
||||
if (shouldAftershock(w) && aftershockEnd - w.end > 0.22) {
|
||||
var pulseStart = w.end + 0.08;
|
||||
var pulseIndex = 0;
|
||||
while (pulseStart + 0.1 < aftershockEnd) {
|
||||
var pulseTravel = -(3 + rng() * 5);
|
||||
var pulseShadow = (3 + rng() * 5).toFixed(1);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{
|
||||
x: pulseTravel,
|
||||
textShadow:
|
||||
pulseShadow +
|
||||
"px 0 #ff003c, -" +
|
||||
pulseShadow +
|
||||
"px 0 #00e5ff, 0 5px 18px rgba(0,0,0,0.52)",
|
||||
duration: 0.045,
|
||||
ease: "none",
|
||||
},
|
||||
pulseStart,
|
||||
);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{
|
||||
x: 0,
|
||||
textShadow: "0 5px 18px rgba(0,0,0,0.52)",
|
||||
duration: 0.075,
|
||||
ease: "power3.out",
|
||||
},
|
||||
pulseStart + 0.045,
|
||||
);
|
||||
pulseIndex++;
|
||||
pulseStart += pulseIndex % 2 === 0 ? 0.18 : 0.24;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, duration: 0.1, ease: "power2.in" }, g.end - 0.1);
|
||||
tl.set(grp, { opacity: 0, visibility: "hidden" }, g.end);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-glitch-rgb"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-glitch-rgb",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Glitch RGB",
|
||||
"description": "RGB chromatic aberration with CRT scanline overlay",
|
||||
"tags": ["captions", "caption-style", "glitch", "cyber", "tech"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-glitch-rgb.html",
|
||||
"target": "compositions/components/caption-glitch-rgb.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-glitch-rgb/preview-v2.mp4",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-glitch-rgb.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Gradient Fill</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=Montserrat:wght@900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#gradient-fill {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#gr-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.gr-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 0 80px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.gr-word {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 100px;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1.15;
|
||||
padding-bottom: 0.05em;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
background-size: 350% 100%;
|
||||
background-position: 100% 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="gradient-fill"
|
||||
data-composition-id="caption-gradient-fill"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="gr-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div id="gr-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var GROUPS = [
|
||||
{ wordStart: 0, wordEnd: 3, start: 0.0, end: 1.05 },
|
||||
{ wordStart: 4, wordEnd: 7, start: 1.1, end: 2.05 },
|
||||
{ wordStart: 8, wordEnd: 8, start: 2.1, end: 2.6 },
|
||||
{ wordStart: 9, wordEnd: 12, start: 2.65, end: 3.5 },
|
||||
{ wordStart: 13, wordEnd: 14, start: 3.55, end: 3.9 },
|
||||
{ wordStart: 15, wordEnd: 16, start: 3.95, end: 4.75 },
|
||||
{ wordStart: 17, wordEnd: 18, start: 4.9, end: 5.4 },
|
||||
{ wordStart: 19, wordEnd: 22, start: 5.5, end: 6.2 },
|
||||
{ wordStart: 23, wordEnd: 24, start: 6.35, end: 6.75 },
|
||||
{ wordStart: 25, wordEnd: 27, start: 6.8, end: 7.8 },
|
||||
];
|
||||
|
||||
var SIRI_GRAD =
|
||||
"linear-gradient(90deg, " +
|
||||
"#fe9f1b 0%, #f76e49 10%, #ff2063 20%, " +
|
||||
"#fd56cb 30%, #ef7aff 40%, #fe9f1b 50%, " +
|
||||
"white 50.5%, white 100%)";
|
||||
|
||||
var container = document.getElementById("gr-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text;
|
||||
})
|
||||
.join(" ");
|
||||
var fontSize = fitFontSize(groupText, 100, "900", "'Montserrat', sans-serif", 1760);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "gr-group";
|
||||
grp.id = "gr-grp-" + gi;
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var span = document.createElement("span");
|
||||
span.className = "gr-word";
|
||||
span.id = "gr-w-" + wi;
|
||||
span.textContent = w.text;
|
||||
span.style.fontSize = fontSize + "px";
|
||||
span.style.background = SIRI_GRAD;
|
||||
span.style.backgroundSize = "350% 100%";
|
||||
span.style.backgroundPosition = "100% 0";
|
||||
span.style.webkitBackgroundClip = "text";
|
||||
span.style.backgroundClip = "text";
|
||||
grp.appendChild(span);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible", opacity: 1 }, g.start);
|
||||
|
||||
var lastWordEnd = groupWords[groupWords.length - 1].end;
|
||||
var nextStart = gi < GROUPS.length - 1 ? GROUPS[gi + 1].start : lastWordEnd + 0.5;
|
||||
var gap = nextStart - lastWordEnd;
|
||||
var fadeDur = Math.min(0.25, gap * 0.8);
|
||||
var fadeStart = nextStart - fadeDur;
|
||||
var groupEnd = nextStart;
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var el = document.getElementById("gr-w-" + wi);
|
||||
var dur = w.end - w.start;
|
||||
|
||||
tl.set(el, { backgroundPosition: "100% 0", scale: 1 }, g.start);
|
||||
tl.set(el, { scale: 1.04 }, w.start);
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ backgroundPosition: "45% 0" },
|
||||
{ backgroundPosition: "0% 0", duration: dur, ease: "none" },
|
||||
w.start,
|
||||
);
|
||||
tl.set(el, { backgroundPosition: "100% 0" }, w.end);
|
||||
tl.to(el, { scale: 1, duration: 0.15, ease: "power2.out" }, w.end);
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, duration: fadeDur }, fadeStart);
|
||||
tl.set(grp, { visibility: "hidden" }, groupEnd);
|
||||
});
|
||||
|
||||
window.__timelines["caption-gradient-fill"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,226 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Gradient Fill</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=Montserrat:wght@900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#gradient-fill {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#gr-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.gr-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 0 80px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.gr-word {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 100px;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1.15;
|
||||
padding-bottom: 0.05em;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
background-size: 350% 100%;
|
||||
background-position: 100% 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="gradient-fill"
|
||||
data-composition-id="caption-gradient-fill"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="gr-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div id="gr-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var GROUPS = [
|
||||
{ wordStart: 0, wordEnd: 3, start: 0.0, end: 1.05 },
|
||||
{ wordStart: 4, wordEnd: 7, start: 1.1, end: 2.05 },
|
||||
{ wordStart: 8, wordEnd: 8, start: 2.1, end: 2.6 },
|
||||
{ wordStart: 9, wordEnd: 12, start: 2.65, end: 3.5 },
|
||||
{ wordStart: 13, wordEnd: 14, start: 3.55, end: 3.9 },
|
||||
{ wordStart: 15, wordEnd: 16, start: 3.95, end: 4.75 },
|
||||
{ wordStart: 17, wordEnd: 18, start: 4.9, end: 5.4 },
|
||||
{ wordStart: 19, wordEnd: 22, start: 5.5, end: 6.2 },
|
||||
{ wordStart: 23, wordEnd: 24, start: 6.35, end: 6.75 },
|
||||
{ wordStart: 25, wordEnd: 27, start: 6.8, end: 7.8 },
|
||||
];
|
||||
|
||||
var SIRI_GRAD =
|
||||
"linear-gradient(90deg, " +
|
||||
"#fe9f1b 0%, #f76e49 10%, #ff2063 20%, " +
|
||||
"#fd56cb 30%, #ef7aff 40%, #fe9f1b 50%, " +
|
||||
"white 50.5%, white 100%)";
|
||||
|
||||
var container = document.getElementById("gr-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text;
|
||||
})
|
||||
.join(" ");
|
||||
var fontSize = fitFontSize(groupText, 100, "900", "'Montserrat', sans-serif", 1760);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "gr-group";
|
||||
grp.id = "gr-grp-" + gi;
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var span = document.createElement("span");
|
||||
span.className = "gr-word";
|
||||
span.id = "gr-w-" + wi;
|
||||
span.textContent = w.text;
|
||||
span.style.fontSize = fontSize + "px";
|
||||
span.style.background = SIRI_GRAD;
|
||||
span.style.backgroundSize = "350% 100%";
|
||||
span.style.backgroundPosition = "100% 0";
|
||||
span.style.webkitBackgroundClip = "text";
|
||||
span.style.backgroundClip = "text";
|
||||
grp.appendChild(span);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible", opacity: 1 }, g.start);
|
||||
|
||||
var lastWordEnd = groupWords[groupWords.length - 1].end;
|
||||
var nextStart = gi < GROUPS.length - 1 ? GROUPS[gi + 1].start : lastWordEnd + 0.5;
|
||||
var gap = nextStart - lastWordEnd;
|
||||
var fadeDur = Math.min(0.25, gap * 0.8);
|
||||
var fadeStart = nextStart - fadeDur;
|
||||
var groupEnd = nextStart;
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var el = document.getElementById("gr-w-" + wi);
|
||||
var dur = w.end - w.start;
|
||||
|
||||
tl.set(el, { backgroundPosition: "100% 0", scale: 1 }, g.start);
|
||||
tl.set(el, { scale: 1.04 }, w.start);
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ backgroundPosition: "45% 0" },
|
||||
{ backgroundPosition: "0% 0", duration: dur, ease: "none" },
|
||||
w.start,
|
||||
);
|
||||
tl.set(el, { backgroundPosition: "100% 0" }, w.end);
|
||||
tl.to(el, { scale: 1, duration: 0.15, ease: "power2.out" }, w.end);
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, duration: fadeDur }, fadeStart);
|
||||
tl.set(grp, { visibility: "hidden" }, groupEnd);
|
||||
});
|
||||
|
||||
window.__timelines["caption-gradient-fill"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-gradient-fill",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Gradient Fill",
|
||||
"description": "Gradient-clipped text with elastic bounce entrance",
|
||||
"tags": ["captions", "caption-style", "gradient", "colorful"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-gradient-fill.html",
|
||||
"target": "compositions/components/caption-gradient-fill.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-gradient-fill/preview-v2.mp4",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-gradient-fill.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Highlight</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=Montserrat:wght@800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#highlight {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#hl-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.hl-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#hl-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hl-group {
|
||||
position: absolute;
|
||||
bottom: 140px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 0 100px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.hl-word {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 80px;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
padding: 6px 12px 8px;
|
||||
text-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
|
||||
transform-origin: 50% 58%;
|
||||
will-change: transform, filter;
|
||||
}
|
||||
.hl-word-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, #ff1745 0%, #df1238 100%);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 30px rgba(229, 20, 58, 0.32);
|
||||
opacity: 0;
|
||||
transform: scaleX(0);
|
||||
transform-origin: 0% 50%;
|
||||
z-index: -1;
|
||||
}
|
||||
.hl-word-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="highlight"
|
||||
data-composition-id="caption-highlight"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="hl-overlay"></div>
|
||||
<div id="hl-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var RAW_GROUPS = [
|
||||
[0, 3],
|
||||
[4, 7],
|
||||
[8, 8],
|
||||
[9, 12],
|
||||
[13, 15],
|
||||
[16, 16],
|
||||
[17, 19],
|
||||
[20, 22],
|
||||
[23, 24],
|
||||
[25, 27],
|
||||
];
|
||||
var GROUPS = RAW_GROUPS.map(function (pair, gi) {
|
||||
var ws = pair[0],
|
||||
we = pair[1];
|
||||
var nextStart = gi + 1 < RAW_GROUPS.length ? WORDS[RAW_GROUPS[gi + 1][0]].start : 8.0;
|
||||
var gEnd = Math.min(WORDS[we].end + 0.5, nextStart - 0.05);
|
||||
return { wordStart: ws, wordEnd: we, start: WORDS[ws].start, end: gEnd };
|
||||
});
|
||||
|
||||
var container = document.getElementById("hl-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "hl-group";
|
||||
grp.id = "hl-grp-" + gi;
|
||||
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var computedSize = fitFontSize(groupText, 80, "800", "Montserrat", 1620);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var wordEl = document.createElement("span");
|
||||
wordEl.className = "hl-word";
|
||||
wordEl.id = "hl-w-" + wi;
|
||||
wordEl.style.fontSize = computedSize + "px";
|
||||
|
||||
var bgEl = document.createElement("span");
|
||||
bgEl.className = "hl-word-bg";
|
||||
bgEl.id = "hl-bg-" + wi;
|
||||
|
||||
var textEl = document.createElement("span");
|
||||
textEl.className = "hl-word-text";
|
||||
textEl.textContent = w.text.toUpperCase();
|
||||
|
||||
wordEl.appendChild(bgEl);
|
||||
wordEl.appendChild(textEl);
|
||||
grp.appendChild(wordEl);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible" }, g.start);
|
||||
tl.fromTo(
|
||||
grp,
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.12, ease: "power2.out" },
|
||||
g.start,
|
||||
);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var bgEl = document.getElementById("hl-bg-" + wi);
|
||||
var wordEl = document.getElementById("hl-w-" + wi);
|
||||
|
||||
tl.to(bgEl, { opacity: 1, scaleX: 1, duration: 0.15, ease: "power2.out" }, w.start);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ filter: "brightness(1.05)", duration: 0.08, ease: "power2.out" },
|
||||
w.start,
|
||||
);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ filter: "brightness(1)", duration: 0.16, ease: "power2.out" },
|
||||
w.start + 0.08,
|
||||
);
|
||||
tl.to(bgEl, { opacity: 0, scaleX: 1.02, duration: 0.1, ease: "power2.in" }, w.end);
|
||||
tl.set(bgEl, { scaleX: 0 }, w.end + 0.1);
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, duration: 0.1, ease: "power2.in" }, g.end - 0.1);
|
||||
tl.set(grp, { opacity: 0, visibility: "hidden" }, g.end);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-highlight"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,259 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Highlight</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=Montserrat:wght@800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#highlight {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#hl-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.hl-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#hl-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hl-group {
|
||||
position: absolute;
|
||||
bottom: 140px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 0 100px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.hl-word {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 80px;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
padding: 6px 12px 8px;
|
||||
text-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
|
||||
transform-origin: 50% 58%;
|
||||
will-change: transform, filter;
|
||||
}
|
||||
.hl-word-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, #ff1745 0%, #df1238 100%);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 30px rgba(229, 20, 58, 0.32);
|
||||
opacity: 0;
|
||||
transform: scaleX(0);
|
||||
transform-origin: 0% 50%;
|
||||
z-index: -1;
|
||||
}
|
||||
.hl-word-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="highlight"
|
||||
data-composition-id="caption-highlight"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="hl-overlay"></div>
|
||||
<div id="hl-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var RAW_GROUPS = [
|
||||
[0, 3],
|
||||
[4, 7],
|
||||
[8, 8],
|
||||
[9, 12],
|
||||
[13, 15],
|
||||
[16, 16],
|
||||
[17, 19],
|
||||
[20, 22],
|
||||
[23, 24],
|
||||
[25, 27],
|
||||
];
|
||||
var GROUPS = RAW_GROUPS.map(function (pair, gi) {
|
||||
var ws = pair[0],
|
||||
we = pair[1];
|
||||
var nextStart = gi + 1 < RAW_GROUPS.length ? WORDS[RAW_GROUPS[gi + 1][0]].start : 8.0;
|
||||
var gEnd = Math.min(WORDS[we].end + 0.5, nextStart - 0.05);
|
||||
return { wordStart: ws, wordEnd: we, start: WORDS[ws].start, end: gEnd };
|
||||
});
|
||||
|
||||
var container = document.getElementById("hl-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "hl-group";
|
||||
grp.id = "hl-grp-" + gi;
|
||||
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var computedSize = fitFontSize(groupText, 80, "800", "Montserrat", 1620);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var wordEl = document.createElement("span");
|
||||
wordEl.className = "hl-word";
|
||||
wordEl.id = "hl-w-" + wi;
|
||||
wordEl.style.fontSize = computedSize + "px";
|
||||
|
||||
var bgEl = document.createElement("span");
|
||||
bgEl.className = "hl-word-bg";
|
||||
bgEl.id = "hl-bg-" + wi;
|
||||
|
||||
var textEl = document.createElement("span");
|
||||
textEl.className = "hl-word-text";
|
||||
textEl.textContent = w.text.toUpperCase();
|
||||
|
||||
wordEl.appendChild(bgEl);
|
||||
wordEl.appendChild(textEl);
|
||||
grp.appendChild(wordEl);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible" }, g.start);
|
||||
tl.fromTo(
|
||||
grp,
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.12, ease: "power2.out" },
|
||||
g.start,
|
||||
);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var bgEl = document.getElementById("hl-bg-" + wi);
|
||||
var wordEl = document.getElementById("hl-w-" + wi);
|
||||
|
||||
tl.to(bgEl, { opacity: 1, scaleX: 1, duration: 0.15, ease: "power2.out" }, w.start);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ filter: "brightness(1.05)", duration: 0.08, ease: "power2.out" },
|
||||
w.start,
|
||||
);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ filter: "brightness(1)", duration: 0.16, ease: "power2.out" },
|
||||
w.start + 0.08,
|
||||
);
|
||||
tl.to(bgEl, { opacity: 0, scaleX: 1.02, duration: 0.1, ease: "power2.in" }, w.end);
|
||||
tl.set(bgEl, { scaleX: 0 }, w.end + 0.1);
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, duration: 0.1, ease: "power2.in" }, g.end - 0.1);
|
||||
tl.set(grp, { opacity: 0, visibility: "hidden" }, g.end);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-highlight"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-highlight",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Highlight",
|
||||
"description": "Red background sweep behind each active word, TikTok-style",
|
||||
"tags": ["captions", "caption-style", "highlight", "social", "tiktok"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-highlight.html",
|
||||
"target": "compositions/components/caption-highlight.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-highlight/preview-v2.mp4",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-highlight.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Kinetic 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=Anton&display=swap" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#kinetic-slam {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#kt-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.kt-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#kt-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.kt-word {
|
||||
font-family: "Anton", sans-serif;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="kinetic-slam"
|
||||
data-composition-id="caption-kinetic-slam"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="kt-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="kt-overlay"></div>
|
||||
<div id="kt-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
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 WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var KEYWORDS = new Set([8, 12, 15, 24, 27]); // HyperFrames, HTML, professional, code, cinema.
|
||||
|
||||
var container = document.getElementById("kt-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
WORDS.forEach(function (w, wi) {
|
||||
var el = document.createElement("div");
|
||||
el.className = "kt-word";
|
||||
el.id = "kt-w-" + wi;
|
||||
el.textContent = w.text.toUpperCase();
|
||||
el.style.fontSize =
|
||||
fitFontSize(w.text.toUpperCase(), 220, "400", "'Anton', sans-serif", 1720) + "px";
|
||||
if (KEYWORDS.has(wi)) el.style.color = "#FFD700";
|
||||
container.appendChild(el);
|
||||
|
||||
gsap.set(el, { yPercent: -50 });
|
||||
|
||||
var entranceMode = wi % 4;
|
||||
tl.set(el, { visibility: "visible" }, w.start);
|
||||
if (entranceMode === 0) {
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ y: -120, opacity: 0 },
|
||||
{ y: 0, opacity: 1, duration: 0.22, ease: "back.out(1.7)" },
|
||||
w.start,
|
||||
);
|
||||
} else if (entranceMode === 1) {
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ x: -300, opacity: 0 },
|
||||
{ x: 0, opacity: 1, duration: 0.2, ease: "expo.out" },
|
||||
w.start,
|
||||
);
|
||||
} else if (entranceMode === 2) {
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ x: 300, opacity: 0 },
|
||||
{ x: 0, opacity: 1, duration: 0.2, ease: "expo.out" },
|
||||
w.start,
|
||||
);
|
||||
} else {
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ scale: 0.4, opacity: 0 },
|
||||
{ scale: 1, opacity: 1, duration: 0.24, ease: "back.out(2.2)" },
|
||||
w.start,
|
||||
);
|
||||
}
|
||||
|
||||
tl.to(el, { opacity: 0, duration: 0.1, ease: "power2.in" }, w.end);
|
||||
tl.set(el, { opacity: 0, visibility: "hidden" }, w.end + 0.1);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-kinetic-slam"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,209 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Kinetic 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=Anton&display=swap" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#kinetic-slam {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#kt-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.kt-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#kt-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.kt-word {
|
||||
font-family: "Anton", sans-serif;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="kinetic-slam"
|
||||
data-composition-id="caption-kinetic-slam"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="kt-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="kt-overlay"></div>
|
||||
<div id="kt-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
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 WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var KEYWORDS = new Set([8, 12, 15, 24, 27]); // HyperFrames, HTML, professional, code, cinema.
|
||||
|
||||
var container = document.getElementById("kt-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
WORDS.forEach(function (w, wi) {
|
||||
var el = document.createElement("div");
|
||||
el.className = "kt-word";
|
||||
el.id = "kt-w-" + wi;
|
||||
el.textContent = w.text.toUpperCase();
|
||||
el.style.fontSize =
|
||||
fitFontSize(w.text.toUpperCase(), 220, "400", "'Anton', sans-serif", 1720) + "px";
|
||||
if (KEYWORDS.has(wi)) el.style.color = "#FFD700";
|
||||
container.appendChild(el);
|
||||
|
||||
gsap.set(el, { yPercent: -50 });
|
||||
|
||||
var entranceMode = wi % 4;
|
||||
tl.set(el, { visibility: "visible" }, w.start);
|
||||
if (entranceMode === 0) {
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ y: -120, opacity: 0 },
|
||||
{ y: 0, opacity: 1, duration: 0.22, ease: "back.out(1.7)" },
|
||||
w.start,
|
||||
);
|
||||
} else if (entranceMode === 1) {
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ x: -300, opacity: 0 },
|
||||
{ x: 0, opacity: 1, duration: 0.2, ease: "expo.out" },
|
||||
w.start,
|
||||
);
|
||||
} else if (entranceMode === 2) {
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ x: 300, opacity: 0 },
|
||||
{ x: 0, opacity: 1, duration: 0.2, ease: "expo.out" },
|
||||
w.start,
|
||||
);
|
||||
} else {
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ scale: 0.4, opacity: 0 },
|
||||
{ scale: 1, opacity: 1, duration: 0.24, ease: "back.out(2.2)" },
|
||||
w.start,
|
||||
);
|
||||
}
|
||||
|
||||
tl.to(el, { opacity: 0, duration: 0.1, ease: "power2.in" }, w.end);
|
||||
tl.set(el, { opacity: 0, visibility: "hidden" }, w.end + 0.1);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-kinetic-slam"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-kinetic-slam",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Kinetic Slam",
|
||||
"description": "Full-screen single-word display with alternating entrance directions",
|
||||
"tags": ["captions", "caption-style", "kinetic", "typography", "slam"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-kinetic-slam.html",
|
||||
"target": "compositions/components/caption-kinetic-slam.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-kinetic-slam/preview.mp4?v=1779051416",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-kinetic-slam.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Matrix Decode</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=Space+Grotesk:wght@700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#matrix-decode {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#sc-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.sc-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#sc-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.sc-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 0 80px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.sc-word-wrap {
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 96px;
|
||||
color: #00ff41;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
}
|
||||
.sc-real {
|
||||
display: none;
|
||||
}
|
||||
.sc-scr0 {
|
||||
display: none;
|
||||
}
|
||||
.sc-scr1 {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="matrix-decode"
|
||||
data-composition-id="caption-matrix-decode"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="sc-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="sc-overlay"></div>
|
||||
<div id="sc-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
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 ALPHANUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
function scrambleChars(seed) {
|
||||
var rng = mulberry32(seed);
|
||||
var result = "";
|
||||
for (var i = 0; i < 4; i++) {
|
||||
result += ALPHANUM[Math.floor(rng() * ALPHANUM.length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var GROUPS = [
|
||||
{ wordStart: 0, wordEnd: 3, start: 0.0, end: 1.35 },
|
||||
{ wordStart: 4, wordEnd: 7, start: 1.1, end: 2.35 },
|
||||
{ wordStart: 8, wordEnd: 8, start: 2.1, end: 2.9 },
|
||||
{ wordStart: 9, wordEnd: 12, start: 2.65, end: 3.8 },
|
||||
{ wordStart: 13, wordEnd: 15, start: 3.55, end: 4.7 },
|
||||
{ wordStart: 16, wordEnd: 16, start: 3.95, end: 5.0 },
|
||||
{ wordStart: 17, wordEnd: 19, start: 4.9, end: 5.7 },
|
||||
{ wordStart: 20, wordEnd: 22, start: 5.65, end: 6.5 },
|
||||
{ wordStart: 23, wordEnd: 24, start: 6.35, end: 7.05 },
|
||||
{ wordStart: 25, wordEnd: 27, start: 6.8, end: 7.9 },
|
||||
];
|
||||
|
||||
var container = document.getElementById("sc-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text;
|
||||
})
|
||||
.join(" ");
|
||||
var fontSize = fitFontSize(groupText, 96, "700", "'Space Grotesk', sans-serif", 1760);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "sc-group";
|
||||
grp.id = "sc-grp-" + gi;
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var wordWrap = document.createElement("span");
|
||||
wordWrap.className = "sc-word-wrap";
|
||||
wordWrap.id = "sc-ww-" + wi;
|
||||
wordWrap.style.fontSize = fontSize + "px";
|
||||
|
||||
var real = document.createElement("span");
|
||||
real.className = "sc-real";
|
||||
real.id = "sc-real-" + wi;
|
||||
real.textContent = w.text;
|
||||
wordWrap.appendChild(real);
|
||||
|
||||
var scr0 = document.createElement("span");
|
||||
scr0.className = "sc-scr0";
|
||||
scr0.id = "sc-scr0-" + wi;
|
||||
scr0.textContent = scrambleChars(wi * 1000 + 0);
|
||||
wordWrap.appendChild(scr0);
|
||||
|
||||
var scr1 = document.createElement("span");
|
||||
scr1.className = "sc-scr1";
|
||||
scr1.id = "sc-scr1-" + wi;
|
||||
scr1.textContent = scrambleChars(wi * 1000 + 1);
|
||||
wordWrap.appendChild(scr1);
|
||||
|
||||
grp.appendChild(wordWrap);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible", opacity: 0 }, g.start);
|
||||
tl.fromTo(grp, { opacity: 0 }, { opacity: 1, duration: 0.1 }, g.start);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var realEl = document.getElementById("sc-real-" + wi);
|
||||
var scr0El = document.getElementById("sc-scr0-" + wi);
|
||||
var scr1El = document.getElementById("sc-scr1-" + wi);
|
||||
|
||||
tl.set(scr0El, { display: "inline" }, w.start);
|
||||
tl.set(scr1El, { display: "inline" }, w.start + 0.1);
|
||||
tl.set(scr0El, { display: "none" }, w.start + 0.1);
|
||||
tl.set(realEl, { display: "inline" }, w.start + 0.2);
|
||||
tl.set(scr1El, { display: "none" }, w.start + 0.2);
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, duration: 0.12 }, g.end - 0.12);
|
||||
tl.set(grp, { opacity: 0, visibility: "hidden" }, g.end);
|
||||
});
|
||||
|
||||
window.__timelines["caption-matrix-decode"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,265 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Matrix Decode</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=Space+Grotesk:wght@700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
#matrix-decode {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
#sc-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.sc-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#sc-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.sc-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 0 80px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.sc-word-wrap {
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 96px;
|
||||
color: #00ff41;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
}
|
||||
.sc-real {
|
||||
display: none;
|
||||
}
|
||||
.sc-scr0 {
|
||||
display: none;
|
||||
}
|
||||
.sc-scr1 {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="matrix-decode"
|
||||
data-composition-id="caption-matrix-decode"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="sc-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="sc-overlay"></div>
|
||||
<div id="sc-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
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 ALPHANUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
function scrambleChars(seed) {
|
||||
var rng = mulberry32(seed);
|
||||
var result = "";
|
||||
for (var i = 0; i < 4; i++) {
|
||||
result += ALPHANUM[Math.floor(rng() * ALPHANUM.length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var GROUPS = [
|
||||
{ wordStart: 0, wordEnd: 3, start: 0.0, end: 1.35 },
|
||||
{ wordStart: 4, wordEnd: 7, start: 1.1, end: 2.35 },
|
||||
{ wordStart: 8, wordEnd: 8, start: 2.1, end: 2.9 },
|
||||
{ wordStart: 9, wordEnd: 12, start: 2.65, end: 3.8 },
|
||||
{ wordStart: 13, wordEnd: 15, start: 3.55, end: 4.7 },
|
||||
{ wordStart: 16, wordEnd: 16, start: 3.95, end: 5.0 },
|
||||
{ wordStart: 17, wordEnd: 19, start: 4.9, end: 5.7 },
|
||||
{ wordStart: 20, wordEnd: 22, start: 5.65, end: 6.5 },
|
||||
{ wordStart: 23, wordEnd: 24, start: 6.35, end: 7.05 },
|
||||
{ wordStart: 25, wordEnd: 27, start: 6.8, end: 7.9 },
|
||||
];
|
||||
|
||||
var container = document.getElementById("sc-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text;
|
||||
})
|
||||
.join(" ");
|
||||
var fontSize = fitFontSize(groupText, 96, "700", "'Space Grotesk', sans-serif", 1760);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "sc-group";
|
||||
grp.id = "sc-grp-" + gi;
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var wordWrap = document.createElement("span");
|
||||
wordWrap.className = "sc-word-wrap";
|
||||
wordWrap.id = "sc-ww-" + wi;
|
||||
wordWrap.style.fontSize = fontSize + "px";
|
||||
|
||||
var real = document.createElement("span");
|
||||
real.className = "sc-real";
|
||||
real.id = "sc-real-" + wi;
|
||||
real.textContent = w.text;
|
||||
wordWrap.appendChild(real);
|
||||
|
||||
var scr0 = document.createElement("span");
|
||||
scr0.className = "sc-scr0";
|
||||
scr0.id = "sc-scr0-" + wi;
|
||||
scr0.textContent = scrambleChars(wi * 1000 + 0);
|
||||
wordWrap.appendChild(scr0);
|
||||
|
||||
var scr1 = document.createElement("span");
|
||||
scr1.className = "sc-scr1";
|
||||
scr1.id = "sc-scr1-" + wi;
|
||||
scr1.textContent = scrambleChars(wi * 1000 + 1);
|
||||
wordWrap.appendChild(scr1);
|
||||
|
||||
grp.appendChild(wordWrap);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible", opacity: 0 }, g.start);
|
||||
tl.fromTo(grp, { opacity: 0 }, { opacity: 1, duration: 0.1 }, g.start);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var realEl = document.getElementById("sc-real-" + wi);
|
||||
var scr0El = document.getElementById("sc-scr0-" + wi);
|
||||
var scr1El = document.getElementById("sc-scr1-" + wi);
|
||||
|
||||
tl.set(scr0El, { display: "inline" }, w.start);
|
||||
tl.set(scr1El, { display: "inline" }, w.start + 0.1);
|
||||
tl.set(scr0El, { display: "none" }, w.start + 0.1);
|
||||
tl.set(realEl, { display: "inline" }, w.start + 0.2);
|
||||
tl.set(scr1El, { display: "none" }, w.start + 0.2);
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, duration: 0.12 }, g.end - 0.12);
|
||||
tl.set(grp, { opacity: 0, visibility: "hidden" }, g.end);
|
||||
});
|
||||
|
||||
window.__timelines["caption-matrix-decode"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-matrix-decode",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Matrix Decode",
|
||||
"description": "Character scramble animation before text reveal",
|
||||
"tags": ["captions", "caption-style", "matrix", "scramble", "decode"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-matrix-decode.html",
|
||||
"target": "compositions/components/caption-matrix-decode.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-matrix-decode/preview-v2.mp4",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-matrix-decode.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Neon Accent</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=Montserrat:wght@800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
font-family: "Montserrat", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#neon-accent {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#avatar-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.safe-zone {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1400px;
|
||||
height: 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1400px;
|
||||
height: 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transform-origin: 50% 50%;
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.caption-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
width: max-content;
|
||||
max-width: 1400px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
color: #ffffff;
|
||||
font-family: "Montserrat", Arial, sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 88px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.caption-word.accent {
|
||||
font-size: 106px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="neon-accent"
|
||||
data-composition-id="caption-neon-accent"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="avatar-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var SAFE_ZONE_WIDTH = 1400;
|
||||
var BASE_FONT_SIZE = 88;
|
||||
var ACCENT_FONT_SIZE = 106;
|
||||
var MIN_FONT_SIZE = 62;
|
||||
var MAX_WORDS_PER_GROUP = 4;
|
||||
var WORD_SPACING = 16;
|
||||
var PAUSE_THRESHOLD = 0.1;
|
||||
var ENTRY_DURATION = 7 / 30;
|
||||
var WIGGLE_DISTANCE = 14;
|
||||
var WIGGLE_CYCLE = 2 / 1.3;
|
||||
var FONT_WIDTH_SAFETY = 1.14;
|
||||
var PRIMARY_COLOR = "#FFFFFF";
|
||||
var ACCENT_COLORS = ["#53FF01", "#FF0002", "#FCFF00"];
|
||||
var TRANSITION_WORDS = new Set([
|
||||
"the",
|
||||
"a",
|
||||
"an",
|
||||
"and",
|
||||
"or",
|
||||
"but",
|
||||
"is",
|
||||
"are",
|
||||
"was",
|
||||
"were",
|
||||
"to",
|
||||
"of",
|
||||
"in",
|
||||
"on",
|
||||
"at",
|
||||
"for",
|
||||
"with",
|
||||
"by",
|
||||
"from",
|
||||
"as",
|
||||
"it",
|
||||
"its",
|
||||
"this",
|
||||
"that",
|
||||
"these",
|
||||
"those",
|
||||
"be",
|
||||
"been",
|
||||
"have",
|
||||
"has",
|
||||
"had",
|
||||
"do",
|
||||
"does",
|
||||
"did",
|
||||
"will",
|
||||
"would",
|
||||
"could",
|
||||
"should",
|
||||
"may",
|
||||
"might",
|
||||
"must",
|
||||
"can",
|
||||
"if",
|
||||
"then",
|
||||
"so",
|
||||
"just",
|
||||
"not",
|
||||
"no",
|
||||
"yes",
|
||||
"up",
|
||||
"out",
|
||||
"about",
|
||||
"lets",
|
||||
"you",
|
||||
"write",
|
||||
"render",
|
||||
]);
|
||||
var KEYWORDS = new Set(["hyperframes", "html", "professional", "code", "cinema"]);
|
||||
|
||||
var TRANSCRIPT = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
gsap.registerPlugin && gsap.registerPlugin();
|
||||
|
||||
function cleanText(text) {
|
||||
return String(text || "").trim();
|
||||
}
|
||||
|
||||
function normalizeWords(rawWords) {
|
||||
return rawWords
|
||||
.map(function (item) {
|
||||
return {
|
||||
text: cleanText(item.word || item.text),
|
||||
start: Math.max(0, Number(item.start) || 0),
|
||||
end: Math.min(DURATION, Math.max(Number(item.start) || 0, Number(item.end) || 0)),
|
||||
};
|
||||
})
|
||||
.filter(function (word) {
|
||||
return word.text.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
var measureCanvas = document.createElement("canvas");
|
||||
var measureContext = measureCanvas.getContext("2d");
|
||||
|
||||
function seededRandom(seed) {
|
||||
var x = Math.sin(seed * 9999) * 10000;
|
||||
return x - Math.floor(x);
|
||||
}
|
||||
|
||||
function isKeyword(text) {
|
||||
return KEYWORDS.has(text.toLowerCase().replace(/[^a-z]/g, ""));
|
||||
}
|
||||
|
||||
function assignWordColors(words, groupIndex) {
|
||||
return words.map(function (word) {
|
||||
if (isKeyword(word.text)) {
|
||||
var colorIndex = Math.floor(seededRandom(groupIndex * 23 + 7) * ACCENT_COLORS.length);
|
||||
return ACCENT_COLORS[colorIndex];
|
||||
}
|
||||
var clean = word.text.toLowerCase().replace(/[^a-z']/g, "");
|
||||
if (TRANSITION_WORDS.has(clean)) return PRIMARY_COLOR;
|
||||
var rand = seededRandom(groupIndex * 7 + words.indexOf(word));
|
||||
if (rand > 0.6) {
|
||||
var ci = Math.floor(rand * ACCENT_COLORS.length) % ACCENT_COLORS.length;
|
||||
return ACCENT_COLORS[ci];
|
||||
}
|
||||
return PRIMARY_COLOR;
|
||||
});
|
||||
}
|
||||
|
||||
function measureWord(word, color, fontSize) {
|
||||
var size = color === PRIMARY_COLOR ? fontSize : fontSize * 1.2;
|
||||
measureContext.font = "800 " + size + "px Montserrat";
|
||||
return measureContext.measureText(word.text.toUpperCase()).width * FONT_WIDTH_SAFETY;
|
||||
}
|
||||
|
||||
function measureLineWidth(words, colors, fontSize) {
|
||||
return words.reduce(function (total, word, index) {
|
||||
return (
|
||||
total +
|
||||
measureWord(word, colors[index] || PRIMARY_COLOR, fontSize) +
|
||||
(index ? WORD_SPACING : 0)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function splitLinesForGroup(words, colors, fontSize) {
|
||||
var maxLineWidth = SAFE_ZONE_WIDTH;
|
||||
if (
|
||||
words.some(function (word, i) {
|
||||
return measureWord(word, colors[i] || PRIMARY_COLOR, fontSize) > maxLineWidth;
|
||||
})
|
||||
)
|
||||
return null;
|
||||
if (measureLineWidth(words, colors, fontSize) <= maxLineWidth)
|
||||
return [{ words: words, colors: colors }];
|
||||
var best = null;
|
||||
var bestDiff = Infinity;
|
||||
for (var split = 1; split < words.length; split++) {
|
||||
var fw = words.slice(0, split),
|
||||
sw = words.slice(split);
|
||||
var fc = colors.slice(0, split),
|
||||
sc = colors.slice(split);
|
||||
var fW = measureLineWidth(fw, fc, fontSize);
|
||||
var sW = measureLineWidth(sw, sc, fontSize);
|
||||
if (fW <= maxLineWidth && sW <= maxLineWidth) {
|
||||
var diff = Math.abs(fW - sW);
|
||||
if (diff < bestDiff) {
|
||||
best = [
|
||||
{ words: fw, colors: fc },
|
||||
{ words: sw, colors: sc },
|
||||
];
|
||||
bestDiff = diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function makeGroups(words) {
|
||||
var groups = [];
|
||||
var current = [];
|
||||
words.forEach(function (word, index) {
|
||||
var nextWord = words[index + 1];
|
||||
var testGroup = current.concat([word]);
|
||||
var exceedsWords = testGroup.length > MAX_WORDS_PER_GROUP;
|
||||
var colors = assignWordColors(testGroup, groups.length);
|
||||
var exceedsFit = !splitLinesForGroup(testGroup, colors, BASE_FONT_SIZE);
|
||||
if (exceedsWords || exceedsFit) {
|
||||
if (current.length) groups.push(makeGroup(current, groups.length));
|
||||
current = [word];
|
||||
return;
|
||||
}
|
||||
current.push(word);
|
||||
var punctuation = /[,.:!?]$/.test(word.text);
|
||||
var pause = nextWord ? nextWord.start - word.end : 0;
|
||||
if (
|
||||
!nextWord ||
|
||||
punctuation ||
|
||||
pause >= PAUSE_THRESHOLD ||
|
||||
current.length >= MAX_WORDS_PER_GROUP
|
||||
) {
|
||||
groups.push(makeGroup(current, groups.length));
|
||||
current = [];
|
||||
}
|
||||
});
|
||||
if (current.length) groups.push(makeGroup(current, groups.length));
|
||||
return groups;
|
||||
}
|
||||
|
||||
function makeGroup(words, groupIndex) {
|
||||
var colors = assignWordColors(words, groupIndex);
|
||||
var groupText = words
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var fontSize = fitFontSize(groupText, BASE_FONT_SIZE, "800", "Montserrat", SAFE_ZONE_WIDTH);
|
||||
while (fontSize > MIN_FONT_SIZE && !splitLinesForGroup(words, colors, fontSize)) {
|
||||
fontSize -= 2;
|
||||
}
|
||||
return {
|
||||
words: words.slice(),
|
||||
colors: colors,
|
||||
fontSize: fontSize,
|
||||
lines: splitLinesForGroup(words, colors, fontSize) || [{ words: words, colors: colors }],
|
||||
start: words[0].start,
|
||||
end: words[words.length - 1].end,
|
||||
};
|
||||
}
|
||||
|
||||
function shadowForColor(color) {
|
||||
var hex = color.replace("#", "");
|
||||
var r = parseInt(hex.slice(0, 2), 16);
|
||||
var g = parseInt(hex.slice(2, 4), 16);
|
||||
var b = parseInt(hex.slice(4, 6), 16);
|
||||
var glow =
|
||||
"0 0 4px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",1), 0 0 10px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",0.9), 0 0 20px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",0.7), 0 0 40px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",0.4)";
|
||||
return "0 8px 16px rgba(0,0,0,0.8), 0 16px 40px rgba(0,0,0,0.6), " + glow;
|
||||
}
|
||||
|
||||
function buildCaptions(groups) {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
groups.forEach(function (group, groupIndex) {
|
||||
var groupEl = document.createElement("div");
|
||||
var copyEl = document.createElement("div");
|
||||
groupEl.id = "caption-group-" + groupIndex;
|
||||
groupEl.className = "caption-group";
|
||||
copyEl.className = "caption-copy";
|
||||
group.lines.slice(0, 2).forEach(function (line) {
|
||||
var lineEl = document.createElement("div");
|
||||
lineEl.className = "caption-line";
|
||||
line.words.forEach(function (word, wordIndex) {
|
||||
var color = line.colors[wordIndex] || PRIMARY_COLOR;
|
||||
var wordEl = document.createElement("span");
|
||||
wordEl.className = color === PRIMARY_COLOR ? "caption-word" : "caption-word accent";
|
||||
wordEl.textContent = word.text.toUpperCase();
|
||||
wordEl.style.color = color;
|
||||
wordEl.style.fontSize =
|
||||
color === PRIMARY_COLOR ? group.fontSize + "px" : group.fontSize * 1.2 + "px";
|
||||
wordEl.style.textShadow = shadowForColor(color);
|
||||
lineEl.appendChild(wordEl);
|
||||
});
|
||||
copyEl.appendChild(lineEl);
|
||||
});
|
||||
groupEl.appendChild(copyEl);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
}
|
||||
|
||||
var WORDS = normalizeWords(TRANSCRIPT);
|
||||
var GROUPS = makeGroups(WORDS);
|
||||
buildCaptions(GROUPS);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var visibleStarts = GROUPS.map(function (group, groupIndex) {
|
||||
var prev = GROUPS[groupIndex - 1];
|
||||
var availableGap = prev ? Math.max(0, group.start - prev.end) : ENTRY_DURATION;
|
||||
var entryLead = Math.min(ENTRY_DURATION, availableGap);
|
||||
return Math.max(0, group.start - entryLead);
|
||||
});
|
||||
|
||||
var allGroupEls = GROUPS.map(function (_, i) {
|
||||
return document.getElementById("caption-group-" + i);
|
||||
});
|
||||
|
||||
GROUPS.forEach(function (group, groupIndex) {
|
||||
var groupEl = allGroupEls[groupIndex];
|
||||
var visibleStart = visibleStarts[groupIndex];
|
||||
var visibleEnd = groupIndex < GROUPS.length - 1 ? visibleStarts[groupIndex + 1] : DURATION;
|
||||
var direction = groupIndex % 2 === 0 ? -1 : 1;
|
||||
var D = WIGGLE_DISTANCE * direction;
|
||||
var wiggleCycles = Math.max(1, Math.ceil((visibleEnd - visibleStart) / WIGGLE_CYCLE));
|
||||
|
||||
allGroupEls.forEach(function (otherEl, otherIndex) {
|
||||
if (otherIndex !== groupIndex) tl.set(otherEl, { opacity: 0 }, visibleStart);
|
||||
});
|
||||
|
||||
tl.set(groupEl, { opacity: 0, scale: 0.65, x: 0, y: 0 }, visibleStart);
|
||||
tl.to(
|
||||
groupEl,
|
||||
{ opacity: 1, scale: 1, duration: ENTRY_DURATION, ease: "power3.out", force3D: true },
|
||||
visibleStart,
|
||||
);
|
||||
tl.to(
|
||||
groupEl,
|
||||
{
|
||||
x: D,
|
||||
y: D,
|
||||
duration: WIGGLE_CYCLE / 2,
|
||||
ease: "sine.inOut",
|
||||
yoyo: true,
|
||||
repeat: Math.max(0, wiggleCycles * 2 - 1),
|
||||
force3D: true,
|
||||
},
|
||||
visibleStart,
|
||||
);
|
||||
tl.set(groupEl, { opacity: 0 }, visibleEnd);
|
||||
});
|
||||
|
||||
window.__timelines["caption-neon-accent"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,531 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Neon Accent</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=Montserrat:wght@800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
font-family: "Montserrat", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#neon-accent {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
#avatar-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.safe-zone {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1400px;
|
||||
height: 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1400px;
|
||||
height: 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transform-origin: 50% 50%;
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.caption-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
width: max-content;
|
||||
max-width: 1400px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
color: #ffffff;
|
||||
font-family: "Montserrat", Arial, sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 88px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.caption-word.accent {
|
||||
font-size: 106px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="neon-accent"
|
||||
data-composition-id="caption-neon-accent"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="avatar-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var SAFE_ZONE_WIDTH = 1400;
|
||||
var BASE_FONT_SIZE = 88;
|
||||
var ACCENT_FONT_SIZE = 106;
|
||||
var MIN_FONT_SIZE = 62;
|
||||
var MAX_WORDS_PER_GROUP = 4;
|
||||
var WORD_SPACING = 16;
|
||||
var PAUSE_THRESHOLD = 0.1;
|
||||
var ENTRY_DURATION = 7 / 30;
|
||||
var WIGGLE_DISTANCE = 14;
|
||||
var WIGGLE_CYCLE = 2 / 1.3;
|
||||
var FONT_WIDTH_SAFETY = 1.14;
|
||||
var PRIMARY_COLOR = "#FFFFFF";
|
||||
var ACCENT_COLORS = ["#53FF01", "#FF0002", "#FCFF00"];
|
||||
var TRANSITION_WORDS = new Set([
|
||||
"the",
|
||||
"a",
|
||||
"an",
|
||||
"and",
|
||||
"or",
|
||||
"but",
|
||||
"is",
|
||||
"are",
|
||||
"was",
|
||||
"were",
|
||||
"to",
|
||||
"of",
|
||||
"in",
|
||||
"on",
|
||||
"at",
|
||||
"for",
|
||||
"with",
|
||||
"by",
|
||||
"from",
|
||||
"as",
|
||||
"it",
|
||||
"its",
|
||||
"this",
|
||||
"that",
|
||||
"these",
|
||||
"those",
|
||||
"be",
|
||||
"been",
|
||||
"have",
|
||||
"has",
|
||||
"had",
|
||||
"do",
|
||||
"does",
|
||||
"did",
|
||||
"will",
|
||||
"would",
|
||||
"could",
|
||||
"should",
|
||||
"may",
|
||||
"might",
|
||||
"must",
|
||||
"can",
|
||||
"if",
|
||||
"then",
|
||||
"so",
|
||||
"just",
|
||||
"not",
|
||||
"no",
|
||||
"yes",
|
||||
"up",
|
||||
"out",
|
||||
"about",
|
||||
"lets",
|
||||
"you",
|
||||
"write",
|
||||
"render",
|
||||
]);
|
||||
var KEYWORDS = new Set(["hyperframes", "html", "professional", "code", "cinema"]);
|
||||
|
||||
var TRANSCRIPT = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
gsap.registerPlugin && gsap.registerPlugin();
|
||||
|
||||
function cleanText(text) {
|
||||
return String(text || "").trim();
|
||||
}
|
||||
|
||||
function normalizeWords(rawWords) {
|
||||
return rawWords
|
||||
.map(function (item) {
|
||||
return {
|
||||
text: cleanText(item.word || item.text),
|
||||
start: Math.max(0, Number(item.start) || 0),
|
||||
end: Math.min(DURATION, Math.max(Number(item.start) || 0, Number(item.end) || 0)),
|
||||
};
|
||||
})
|
||||
.filter(function (word) {
|
||||
return word.text.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
var measureCanvas = document.createElement("canvas");
|
||||
var measureContext = measureCanvas.getContext("2d");
|
||||
|
||||
function seededRandom(seed) {
|
||||
var x = Math.sin(seed * 9999) * 10000;
|
||||
return x - Math.floor(x);
|
||||
}
|
||||
|
||||
function isKeyword(text) {
|
||||
return KEYWORDS.has(text.toLowerCase().replace(/[^a-z]/g, ""));
|
||||
}
|
||||
|
||||
function assignWordColors(words, groupIndex) {
|
||||
return words.map(function (word) {
|
||||
if (isKeyword(word.text)) {
|
||||
var colorIndex = Math.floor(seededRandom(groupIndex * 23 + 7) * ACCENT_COLORS.length);
|
||||
return ACCENT_COLORS[colorIndex];
|
||||
}
|
||||
var clean = word.text.toLowerCase().replace(/[^a-z']/g, "");
|
||||
if (TRANSITION_WORDS.has(clean)) return PRIMARY_COLOR;
|
||||
var rand = seededRandom(groupIndex * 7 + words.indexOf(word));
|
||||
if (rand > 0.6) {
|
||||
var ci = Math.floor(rand * ACCENT_COLORS.length) % ACCENT_COLORS.length;
|
||||
return ACCENT_COLORS[ci];
|
||||
}
|
||||
return PRIMARY_COLOR;
|
||||
});
|
||||
}
|
||||
|
||||
function measureWord(word, color, fontSize) {
|
||||
var size = color === PRIMARY_COLOR ? fontSize : fontSize * 1.2;
|
||||
measureContext.font = "800 " + size + "px Montserrat";
|
||||
return measureContext.measureText(word.text.toUpperCase()).width * FONT_WIDTH_SAFETY;
|
||||
}
|
||||
|
||||
function measureLineWidth(words, colors, fontSize) {
|
||||
return words.reduce(function (total, word, index) {
|
||||
return (
|
||||
total +
|
||||
measureWord(word, colors[index] || PRIMARY_COLOR, fontSize) +
|
||||
(index ? WORD_SPACING : 0)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function splitLinesForGroup(words, colors, fontSize) {
|
||||
var maxLineWidth = SAFE_ZONE_WIDTH;
|
||||
if (
|
||||
words.some(function (word, i) {
|
||||
return measureWord(word, colors[i] || PRIMARY_COLOR, fontSize) > maxLineWidth;
|
||||
})
|
||||
)
|
||||
return null;
|
||||
if (measureLineWidth(words, colors, fontSize) <= maxLineWidth)
|
||||
return [{ words: words, colors: colors }];
|
||||
var best = null;
|
||||
var bestDiff = Infinity;
|
||||
for (var split = 1; split < words.length; split++) {
|
||||
var fw = words.slice(0, split),
|
||||
sw = words.slice(split);
|
||||
var fc = colors.slice(0, split),
|
||||
sc = colors.slice(split);
|
||||
var fW = measureLineWidth(fw, fc, fontSize);
|
||||
var sW = measureLineWidth(sw, sc, fontSize);
|
||||
if (fW <= maxLineWidth && sW <= maxLineWidth) {
|
||||
var diff = Math.abs(fW - sW);
|
||||
if (diff < bestDiff) {
|
||||
best = [
|
||||
{ words: fw, colors: fc },
|
||||
{ words: sw, colors: sc },
|
||||
];
|
||||
bestDiff = diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function makeGroups(words) {
|
||||
var groups = [];
|
||||
var current = [];
|
||||
words.forEach(function (word, index) {
|
||||
var nextWord = words[index + 1];
|
||||
var testGroup = current.concat([word]);
|
||||
var exceedsWords = testGroup.length > MAX_WORDS_PER_GROUP;
|
||||
var colors = assignWordColors(testGroup, groups.length);
|
||||
var exceedsFit = !splitLinesForGroup(testGroup, colors, BASE_FONT_SIZE);
|
||||
if (exceedsWords || exceedsFit) {
|
||||
if (current.length) groups.push(makeGroup(current, groups.length));
|
||||
current = [word];
|
||||
return;
|
||||
}
|
||||
current.push(word);
|
||||
var punctuation = /[,.:!?]$/.test(word.text);
|
||||
var pause = nextWord ? nextWord.start - word.end : 0;
|
||||
if (
|
||||
!nextWord ||
|
||||
punctuation ||
|
||||
pause >= PAUSE_THRESHOLD ||
|
||||
current.length >= MAX_WORDS_PER_GROUP
|
||||
) {
|
||||
groups.push(makeGroup(current, groups.length));
|
||||
current = [];
|
||||
}
|
||||
});
|
||||
if (current.length) groups.push(makeGroup(current, groups.length));
|
||||
return groups;
|
||||
}
|
||||
|
||||
function makeGroup(words, groupIndex) {
|
||||
var colors = assignWordColors(words, groupIndex);
|
||||
var groupText = words
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var fontSize = fitFontSize(groupText, BASE_FONT_SIZE, "800", "Montserrat", SAFE_ZONE_WIDTH);
|
||||
while (fontSize > MIN_FONT_SIZE && !splitLinesForGroup(words, colors, fontSize)) {
|
||||
fontSize -= 2;
|
||||
}
|
||||
return {
|
||||
words: words.slice(),
|
||||
colors: colors,
|
||||
fontSize: fontSize,
|
||||
lines: splitLinesForGroup(words, colors, fontSize) || [{ words: words, colors: colors }],
|
||||
start: words[0].start,
|
||||
end: words[words.length - 1].end,
|
||||
};
|
||||
}
|
||||
|
||||
function shadowForColor(color) {
|
||||
var hex = color.replace("#", "");
|
||||
var r = parseInt(hex.slice(0, 2), 16);
|
||||
var g = parseInt(hex.slice(2, 4), 16);
|
||||
var b = parseInt(hex.slice(4, 6), 16);
|
||||
var glow =
|
||||
"0 0 4px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",1), 0 0 10px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",0.9), 0 0 20px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",0.7), 0 0 40px rgba(" +
|
||||
r +
|
||||
"," +
|
||||
g +
|
||||
"," +
|
||||
b +
|
||||
",0.4)";
|
||||
return "0 8px 16px rgba(0,0,0,0.8), 0 16px 40px rgba(0,0,0,0.6), " + glow;
|
||||
}
|
||||
|
||||
function buildCaptions(groups) {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
groups.forEach(function (group, groupIndex) {
|
||||
var groupEl = document.createElement("div");
|
||||
var copyEl = document.createElement("div");
|
||||
groupEl.id = "caption-group-" + groupIndex;
|
||||
groupEl.className = "caption-group";
|
||||
copyEl.className = "caption-copy";
|
||||
group.lines.slice(0, 2).forEach(function (line) {
|
||||
var lineEl = document.createElement("div");
|
||||
lineEl.className = "caption-line";
|
||||
line.words.forEach(function (word, wordIndex) {
|
||||
var color = line.colors[wordIndex] || PRIMARY_COLOR;
|
||||
var wordEl = document.createElement("span");
|
||||
wordEl.className = color === PRIMARY_COLOR ? "caption-word" : "caption-word accent";
|
||||
wordEl.textContent = word.text.toUpperCase();
|
||||
wordEl.style.color = color;
|
||||
wordEl.style.fontSize =
|
||||
color === PRIMARY_COLOR ? group.fontSize + "px" : group.fontSize * 1.2 + "px";
|
||||
wordEl.style.textShadow = shadowForColor(color);
|
||||
lineEl.appendChild(wordEl);
|
||||
});
|
||||
copyEl.appendChild(lineEl);
|
||||
});
|
||||
groupEl.appendChild(copyEl);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
}
|
||||
|
||||
var WORDS = normalizeWords(TRANSCRIPT);
|
||||
var GROUPS = makeGroups(WORDS);
|
||||
buildCaptions(GROUPS);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var visibleStarts = GROUPS.map(function (group, groupIndex) {
|
||||
var prev = GROUPS[groupIndex - 1];
|
||||
var availableGap = prev ? Math.max(0, group.start - prev.end) : ENTRY_DURATION;
|
||||
var entryLead = Math.min(ENTRY_DURATION, availableGap);
|
||||
return Math.max(0, group.start - entryLead);
|
||||
});
|
||||
|
||||
var allGroupEls = GROUPS.map(function (_, i) {
|
||||
return document.getElementById("caption-group-" + i);
|
||||
});
|
||||
|
||||
GROUPS.forEach(function (group, groupIndex) {
|
||||
var groupEl = allGroupEls[groupIndex];
|
||||
var visibleStart = visibleStarts[groupIndex];
|
||||
var visibleEnd = groupIndex < GROUPS.length - 1 ? visibleStarts[groupIndex + 1] : DURATION;
|
||||
var direction = groupIndex % 2 === 0 ? -1 : 1;
|
||||
var D = WIGGLE_DISTANCE * direction;
|
||||
var wiggleCycles = Math.max(1, Math.ceil((visibleEnd - visibleStart) / WIGGLE_CYCLE));
|
||||
|
||||
allGroupEls.forEach(function (otherEl, otherIndex) {
|
||||
if (otherIndex !== groupIndex) tl.set(otherEl, { opacity: 0 }, visibleStart);
|
||||
});
|
||||
|
||||
tl.set(groupEl, { opacity: 0, scale: 0.65, x: 0, y: 0 }, visibleStart);
|
||||
tl.to(
|
||||
groupEl,
|
||||
{ opacity: 1, scale: 1, duration: ENTRY_DURATION, ease: "power3.out", force3D: true },
|
||||
visibleStart,
|
||||
);
|
||||
tl.to(
|
||||
groupEl,
|
||||
{
|
||||
x: D,
|
||||
y: D,
|
||||
duration: WIGGLE_CYCLE / 2,
|
||||
ease: "sine.inOut",
|
||||
yoyo: true,
|
||||
repeat: Math.max(0, wiggleCycles * 2 - 1),
|
||||
force3D: true,
|
||||
},
|
||||
visibleStart,
|
||||
);
|
||||
tl.set(groupEl, { opacity: 0 }, visibleEnd);
|
||||
});
|
||||
|
||||
window.__timelines["caption-neon-accent"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-neon-accent",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Neon Accent",
|
||||
"description": "Multi-color neon glow accents with wiggle drift animation",
|
||||
"tags": ["captions", "caption-style", "neon", "glow", "accent"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-neon-accent.html",
|
||||
"target": "compositions/components/caption-neon-accent.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-neon-accent/preview.mp4?v=1779051416",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-neon-accent.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Neon Glow</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=Outfit:wght@900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#neon-glow {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#ne-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.ne-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#ne-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ne-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 0 80px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.ne-word {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 96px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(0, 255, 240, 0.14);
|
||||
display: inline-block;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="neon-glow"
|
||||
data-composition-id="caption-neon-glow"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="ne-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="ne-overlay"></div>
|
||||
<div id="ne-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var GROUPS = [
|
||||
{ wordStart: 0, wordEnd: 3, start: 0.0, end: 1.05 },
|
||||
{ wordStart: 4, wordEnd: 7, start: 1.1, end: 2.05 },
|
||||
{ wordStart: 8, wordEnd: 8, start: 2.1, end: 2.6 },
|
||||
{ wordStart: 9, wordEnd: 12, start: 2.65, end: 3.5 },
|
||||
{ wordStart: 13, wordEnd: 15, start: 3.55, end: 4.4 },
|
||||
{ wordStart: 16, wordEnd: 16, start: 3.95, end: 4.75 },
|
||||
{ wordStart: 17, wordEnd: 19, start: 4.9, end: 5.4 },
|
||||
{ wordStart: 20, wordEnd: 22, start: 5.65, end: 6.2 },
|
||||
{ wordStart: 23, wordEnd: 24, start: 6.35, end: 6.75 },
|
||||
{ wordStart: 25, wordEnd: 27, start: 6.8, end: 7.8 },
|
||||
];
|
||||
|
||||
// Keywords get pink glow; others get cyan glow
|
||||
var KEYWORDS = new Set([8, 12, 15, 24, 27]); // HyperFrames, HTML, professional, code, cinema.
|
||||
|
||||
var container = document.getElementById("ne-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var fontSize = fitFontSize(groupText, 96, "900", "'Outfit', sans-serif", 1760);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "ne-group";
|
||||
grp.id = "ne-grp-" + gi;
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var span = document.createElement("span");
|
||||
span.className = "ne-word";
|
||||
span.id = "ne-w-" + wi;
|
||||
span.textContent = w.text;
|
||||
span.style.fontSize = fontSize + "px";
|
||||
grp.appendChild(span);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible", opacity: 0 }, g.start);
|
||||
tl.fromTo(grp, { opacity: 0 }, { opacity: 1, duration: 0.05, ease: "steps(1)" }, g.start);
|
||||
tl.fromTo(grp, { x: -8 }, { x: 0, duration: 0.14, ease: "power3.out" }, g.start + 0.05);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var el = document.getElementById("ne-w-" + wi);
|
||||
var isKW = KEYWORDS.has(wi);
|
||||
var activeColor = isKW ? "#FF0099" : "#00FFF0";
|
||||
tl.to(
|
||||
el,
|
||||
{
|
||||
color: activeColor,
|
||||
textShadow:
|
||||
"0 0 10px " +
|
||||
activeColor +
|
||||
", 0 0 35px " +
|
||||
activeColor +
|
||||
", 0 0 90px " +
|
||||
activeColor,
|
||||
duration: 0.06,
|
||||
},
|
||||
w.start,
|
||||
);
|
||||
tl.to(el, { color: "rgba(0,255,240,0.14)", textShadow: "none", duration: 0.06 }, w.end);
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, x: 6, duration: 0.1, ease: "steps(1)" }, g.end - 0.1);
|
||||
tl.set(grp, { opacity: 0, visibility: "hidden" }, g.end);
|
||||
});
|
||||
|
||||
window.__timelines["caption-neon-glow"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,231 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Neon Glow</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=Outfit:wght@900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
#neon-glow {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
#ne-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.ne-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#ne-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ne-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 0 80px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.ne-word {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 96px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(0, 255, 240, 0.14);
|
||||
display: inline-block;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="neon-glow"
|
||||
data-composition-id="caption-neon-glow"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="ne-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="ne-overlay"></div>
|
||||
<div id="ne-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var GROUPS = [
|
||||
{ wordStart: 0, wordEnd: 3, start: 0.0, end: 1.05 },
|
||||
{ wordStart: 4, wordEnd: 7, start: 1.1, end: 2.05 },
|
||||
{ wordStart: 8, wordEnd: 8, start: 2.1, end: 2.6 },
|
||||
{ wordStart: 9, wordEnd: 12, start: 2.65, end: 3.5 },
|
||||
{ wordStart: 13, wordEnd: 15, start: 3.55, end: 4.4 },
|
||||
{ wordStart: 16, wordEnd: 16, start: 3.95, end: 4.75 },
|
||||
{ wordStart: 17, wordEnd: 19, start: 4.9, end: 5.4 },
|
||||
{ wordStart: 20, wordEnd: 22, start: 5.65, end: 6.2 },
|
||||
{ wordStart: 23, wordEnd: 24, start: 6.35, end: 6.75 },
|
||||
{ wordStart: 25, wordEnd: 27, start: 6.8, end: 7.8 },
|
||||
];
|
||||
|
||||
// Keywords get pink glow; others get cyan glow
|
||||
var KEYWORDS = new Set([8, 12, 15, 24, 27]); // HyperFrames, HTML, professional, code, cinema.
|
||||
|
||||
var container = document.getElementById("ne-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var fontSize = fitFontSize(groupText, 96, "900", "'Outfit', sans-serif", 1760);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "ne-group";
|
||||
grp.id = "ne-grp-" + gi;
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var span = document.createElement("span");
|
||||
span.className = "ne-word";
|
||||
span.id = "ne-w-" + wi;
|
||||
span.textContent = w.text;
|
||||
span.style.fontSize = fontSize + "px";
|
||||
grp.appendChild(span);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
|
||||
tl.set(grp, { visibility: "visible", opacity: 0 }, g.start);
|
||||
tl.fromTo(grp, { opacity: 0 }, { opacity: 1, duration: 0.05, ease: "steps(1)" }, g.start);
|
||||
tl.fromTo(grp, { x: -8 }, { x: 0, duration: 0.14, ease: "power3.out" }, g.start + 0.05);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var el = document.getElementById("ne-w-" + wi);
|
||||
var isKW = KEYWORDS.has(wi);
|
||||
var activeColor = isKW ? "#FF0099" : "#00FFF0";
|
||||
tl.to(
|
||||
el,
|
||||
{
|
||||
color: activeColor,
|
||||
textShadow:
|
||||
"0 0 10px " +
|
||||
activeColor +
|
||||
", 0 0 35px " +
|
||||
activeColor +
|
||||
", 0 0 90px " +
|
||||
activeColor,
|
||||
duration: 0.06,
|
||||
},
|
||||
w.start,
|
||||
);
|
||||
tl.to(el, { color: "rgba(0,255,240,0.14)", textShadow: "none", duration: 0.06 }, w.end);
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, x: 6, duration: 0.1, ease: "steps(1)" }, g.end - 0.1);
|
||||
tl.set(grp, { opacity: 0, visibility: "hidden" }, g.end);
|
||||
});
|
||||
|
||||
window.__timelines["caption-neon-glow"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-neon-glow",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Neon Glow",
|
||||
"description": "Cyan and magenta neon glow with keyword accent colors",
|
||||
"tags": ["captions", "caption-style", "neon", "glow", "gaming"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-neon-glow.html",
|
||||
"target": "compositions/components/caption-neon-glow.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-neon-glow/preview.mp4?v=1779051416",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-neon-glow.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Parallax Layers</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=Instrument+Serif:ital@0;1&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
font-family: "Instrument Serif", serif;
|
||||
}
|
||||
|
||||
#parallax-layers {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#bg-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Behind captions: big red 3D text */
|
||||
.behind-caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Front captions */
|
||||
.front-caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 30;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.behind-safe-zone {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.front-safe-zone {
|
||||
position: absolute;
|
||||
top: 700px;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.caption-block {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0;
|
||||
transform-origin: 50% 50%;
|
||||
will-change: transform, opacity;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.caption-line--behind {
|
||||
transform: scaleY(2.8);
|
||||
transform-origin: 50% 0%;
|
||||
}
|
||||
|
||||
.word {
|
||||
display: inline-block;
|
||||
will-change: transform, opacity;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.word--behind {
|
||||
font-family: "Instrument Serif", serif;
|
||||
font-size: 220px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 0.9;
|
||||
color: #e50914;
|
||||
-webkit-text-stroke: 5px #e50914;
|
||||
text-shadow: 2px 4px 4px #a30610;
|
||||
}
|
||||
|
||||
.word--front-normal {
|
||||
font-family: "Instrument Serif", serif;
|
||||
font-size: 130px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
color: #eeeeee;
|
||||
text-shadow:
|
||||
0 2px 12px rgba(0, 0, 0, 0.6),
|
||||
0 4px 24px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.word--front-italic {
|
||||
font-family: "Instrument Serif", serif;
|
||||
font-size: 130px;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
color: #eeeeee;
|
||||
text-shadow:
|
||||
0 2px 12px rgba(0, 0, 0, 0.6),
|
||||
0 4px 24px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="parallax-layers"
|
||||
data-composition-id="caption-parallax-layers"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="bg-video"
|
||||
muted
|
||||
playsinline
|
||||
preload="auto"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="behind-caption-layer" aria-hidden="true">
|
||||
<div id="behind-stage" class="behind-safe-zone"></div>
|
||||
</div>
|
||||
|
||||
<div class="front-caption-layer" aria-hidden="true">
|
||||
<div id="front-stage" class="front-safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var BEHIND_WIDTH = 1800;
|
||||
var FRONT_WIDTH = 1750;
|
||||
var ENTRY_DUR = 0.1;
|
||||
var SLIDE_DUR = 0.2;
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var W = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
// behind=red 3D emphasis words, front=normal white words
|
||||
var BLOCKS = [
|
||||
{ behind: null, front: [[0], [1], [2]] },
|
||||
{ behind: [[3]], front: [[4], [5]] },
|
||||
{ behind: [[6]], front: [[7]] },
|
||||
{ behind: [[8]], front: [[9], [10], [11]] },
|
||||
{ behind: [[12]], front: [[13], [14]] },
|
||||
{ behind: [[15]], front: [[16]] },
|
||||
{ behind: null, front: [[17], [18]] },
|
||||
{ behind: null, front: [[19], [20], [21], [22]] },
|
||||
{ behind: [[24]], front: [[23], [25], [26]] },
|
||||
{ behind: [[27]], front: null },
|
||||
];
|
||||
|
||||
var behindStage = document.getElementById("behind-stage");
|
||||
var frontStage = document.getElementById("front-stage");
|
||||
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
var bEl = document.createElement("div");
|
||||
bEl.className = "caption-block";
|
||||
bEl.id = "bb" + bi;
|
||||
if (block.behind) {
|
||||
var behindText = block.behind
|
||||
.map(function (wi_arr) {
|
||||
return W[wi_arr[0]].text;
|
||||
})
|
||||
.join(" ");
|
||||
var behindSize = fitFontSize(behindText, 220, "400", "Instrument Serif", BEHIND_WIDTH);
|
||||
var bLine = document.createElement("div");
|
||||
bLine.className = "caption-line caption-line--behind";
|
||||
block.behind.forEach(function (wi_arr, wi) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "word word--behind";
|
||||
span.id = "bb" + bi + "w" + wi;
|
||||
span.textContent = W[wi_arr[0]].text;
|
||||
span.style.fontSize = behindSize + "px";
|
||||
bLine.appendChild(span);
|
||||
});
|
||||
bEl.appendChild(bLine);
|
||||
}
|
||||
behindStage.appendChild(bEl);
|
||||
|
||||
var fEl = document.createElement("div");
|
||||
fEl.className = "caption-block";
|
||||
fEl.id = "fb" + bi;
|
||||
if (block.front) {
|
||||
var frontText = block.front
|
||||
.map(function (wi_arr) {
|
||||
return W[wi_arr[0]].text;
|
||||
})
|
||||
.join(" ");
|
||||
var frontSize = fitFontSize(frontText, 130, "400", "Instrument Serif", FRONT_WIDTH);
|
||||
var fLine = document.createElement("div");
|
||||
fLine.className = "caption-line";
|
||||
block.front.forEach(function (wi_arr, wi) {
|
||||
var span = document.createElement("span");
|
||||
var isItalic = wi_arr[0] === 7 || wi_arr[0] === 18; // frame., timeline.
|
||||
span.className = "word " + (isItalic ? "word--front-italic" : "word--front-normal");
|
||||
span.id = "fb" + bi + "w" + wi;
|
||||
span.textContent = W[wi_arr[0]].text;
|
||||
span.style.fontSize = frontSize + "px";
|
||||
fLine.appendChild(span);
|
||||
});
|
||||
fEl.appendChild(fLine);
|
||||
}
|
||||
frontStage.appendChild(fEl);
|
||||
});
|
||||
|
||||
var allBehind = BLOCKS.map(function (_, i) {
|
||||
return document.getElementById("bb" + i);
|
||||
});
|
||||
var allFront = BLOCKS.map(function (_, i) {
|
||||
return document.getElementById("fb" + i);
|
||||
});
|
||||
|
||||
function applyBehindScales() {
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
if (block.behind) {
|
||||
var line = allBehind[bi].querySelector(".caption-line");
|
||||
if (line) {
|
||||
var natW = line.scrollWidth;
|
||||
if (natW > 0) {
|
||||
var s = BEHIND_WIDTH / natW;
|
||||
line.style.transform = "scaleX(" + s + ") scaleY(2.8)";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
applyBehindScales();
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
function blockStart(bi) {
|
||||
var b = BLOCKS[bi];
|
||||
var firstIdx = b.behind ? b.behind[0][0] : b.front[0][0];
|
||||
return W[firstIdx].start;
|
||||
}
|
||||
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
var start = blockStart(bi);
|
||||
var nextStart = bi < BLOCKS.length - 1 ? blockStart(bi + 1) : DURATION;
|
||||
var bEl = allBehind[bi];
|
||||
var fEl = allFront[bi];
|
||||
|
||||
allBehind.forEach(function (other, oi) {
|
||||
if (oi !== bi) tl.set(other, { opacity: 0 }, start);
|
||||
});
|
||||
allFront.forEach(function (other, oi) {
|
||||
if (oi !== bi) tl.set(other, { opacity: 0 }, start);
|
||||
});
|
||||
|
||||
if (block.behind) {
|
||||
tl.set(bEl, { opacity: 1 }, start);
|
||||
block.behind.forEach(function (wi_arr, wi) {
|
||||
var wordEl = document.getElementById("bb" + bi + "w" + wi);
|
||||
var wordStart = W[wi_arr[0]].start;
|
||||
tl.set(wordEl, { opacity: 0 }, start);
|
||||
tl.to(wordEl, { opacity: 1, duration: ENTRY_DUR, ease: "power2.out" }, wordStart);
|
||||
});
|
||||
}
|
||||
|
||||
if (block.front) {
|
||||
tl.set(fEl, { opacity: 1 }, start);
|
||||
block.front.forEach(function (wi_arr, wi) {
|
||||
var wordEl = document.getElementById("fb" + bi + "w" + wi);
|
||||
var wordStart = W[wi_arr[0]].start;
|
||||
tl.set(wordEl, { opacity: 0 }, start);
|
||||
tl.set(wordEl, { opacity: 1 }, wordStart);
|
||||
});
|
||||
}
|
||||
|
||||
tl.set(bEl, { opacity: 0 }, nextStart);
|
||||
tl.set(fEl, { opacity: 0 }, nextStart);
|
||||
});
|
||||
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["caption-parallax-layers"] = tl;
|
||||
|
||||
document.fonts.ready.then(function () {
|
||||
applyBehindScales();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,373 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Parallax Layers</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=Instrument+Serif:ital@0;1&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
font-family: "Instrument Serif", serif;
|
||||
}
|
||||
|
||||
#parallax-layers {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
#bg-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Behind captions: big red 3D text */
|
||||
.behind-caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Front captions */
|
||||
.front-caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 30;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.behind-safe-zone {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.front-safe-zone {
|
||||
position: absolute;
|
||||
top: 700px;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.caption-block {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0;
|
||||
transform-origin: 50% 50%;
|
||||
will-change: transform, opacity;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.caption-line--behind {
|
||||
transform: scaleY(2.8);
|
||||
transform-origin: 50% 0%;
|
||||
}
|
||||
|
||||
.word {
|
||||
display: inline-block;
|
||||
will-change: transform, opacity;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.word--behind {
|
||||
font-family: "Instrument Serif", serif;
|
||||
font-size: 220px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 0.9;
|
||||
color: #e50914;
|
||||
-webkit-text-stroke: 5px #e50914;
|
||||
text-shadow: 2px 4px 4px #a30610;
|
||||
}
|
||||
|
||||
.word--front-normal {
|
||||
font-family: "Instrument Serif", serif;
|
||||
font-size: 130px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
color: #eeeeee;
|
||||
text-shadow:
|
||||
0 2px 12px rgba(0, 0, 0, 0.6),
|
||||
0 4px 24px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.word--front-italic {
|
||||
font-family: "Instrument Serif", serif;
|
||||
font-size: 130px;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
color: #eeeeee;
|
||||
text-shadow:
|
||||
0 2px 12px rgba(0, 0, 0, 0.6),
|
||||
0 4px 24px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="parallax-layers"
|
||||
data-composition-id="caption-parallax-layers"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="bg-video"
|
||||
muted
|
||||
playsinline
|
||||
preload="auto"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="behind-caption-layer" aria-hidden="true">
|
||||
<div id="behind-stage" class="behind-safe-zone"></div>
|
||||
</div>
|
||||
|
||||
<div class="front-caption-layer" aria-hidden="true">
|
||||
<div id="front-stage" class="front-safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var BEHIND_WIDTH = 1800;
|
||||
var FRONT_WIDTH = 1750;
|
||||
var ENTRY_DUR = 0.1;
|
||||
var SLIDE_DUR = 0.2;
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var W = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
// behind=red 3D emphasis words, front=normal white words
|
||||
var BLOCKS = [
|
||||
{ behind: null, front: [[0], [1], [2]] },
|
||||
{ behind: [[3]], front: [[4], [5]] },
|
||||
{ behind: [[6]], front: [[7]] },
|
||||
{ behind: [[8]], front: [[9], [10], [11]] },
|
||||
{ behind: [[12]], front: [[13], [14]] },
|
||||
{ behind: [[15]], front: [[16]] },
|
||||
{ behind: null, front: [[17], [18]] },
|
||||
{ behind: null, front: [[19], [20], [21], [22]] },
|
||||
{ behind: [[24]], front: [[23], [25], [26]] },
|
||||
{ behind: [[27]], front: null },
|
||||
];
|
||||
|
||||
var behindStage = document.getElementById("behind-stage");
|
||||
var frontStage = document.getElementById("front-stage");
|
||||
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
var bEl = document.createElement("div");
|
||||
bEl.className = "caption-block";
|
||||
bEl.id = "bb" + bi;
|
||||
if (block.behind) {
|
||||
var behindText = block.behind
|
||||
.map(function (wi_arr) {
|
||||
return W[wi_arr[0]].text;
|
||||
})
|
||||
.join(" ");
|
||||
var behindSize = fitFontSize(behindText, 220, "400", "Instrument Serif", BEHIND_WIDTH);
|
||||
var bLine = document.createElement("div");
|
||||
bLine.className = "caption-line caption-line--behind";
|
||||
block.behind.forEach(function (wi_arr, wi) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "word word--behind";
|
||||
span.id = "bb" + bi + "w" + wi;
|
||||
span.textContent = W[wi_arr[0]].text;
|
||||
span.style.fontSize = behindSize + "px";
|
||||
bLine.appendChild(span);
|
||||
});
|
||||
bEl.appendChild(bLine);
|
||||
}
|
||||
behindStage.appendChild(bEl);
|
||||
|
||||
var fEl = document.createElement("div");
|
||||
fEl.className = "caption-block";
|
||||
fEl.id = "fb" + bi;
|
||||
if (block.front) {
|
||||
var frontText = block.front
|
||||
.map(function (wi_arr) {
|
||||
return W[wi_arr[0]].text;
|
||||
})
|
||||
.join(" ");
|
||||
var frontSize = fitFontSize(frontText, 130, "400", "Instrument Serif", FRONT_WIDTH);
|
||||
var fLine = document.createElement("div");
|
||||
fLine.className = "caption-line";
|
||||
block.front.forEach(function (wi_arr, wi) {
|
||||
var span = document.createElement("span");
|
||||
var isItalic = wi_arr[0] === 7 || wi_arr[0] === 18; // frame., timeline.
|
||||
span.className = "word " + (isItalic ? "word--front-italic" : "word--front-normal");
|
||||
span.id = "fb" + bi + "w" + wi;
|
||||
span.textContent = W[wi_arr[0]].text;
|
||||
span.style.fontSize = frontSize + "px";
|
||||
fLine.appendChild(span);
|
||||
});
|
||||
fEl.appendChild(fLine);
|
||||
}
|
||||
frontStage.appendChild(fEl);
|
||||
});
|
||||
|
||||
var allBehind = BLOCKS.map(function (_, i) {
|
||||
return document.getElementById("bb" + i);
|
||||
});
|
||||
var allFront = BLOCKS.map(function (_, i) {
|
||||
return document.getElementById("fb" + i);
|
||||
});
|
||||
|
||||
function applyBehindScales() {
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
if (block.behind) {
|
||||
var line = allBehind[bi].querySelector(".caption-line");
|
||||
if (line) {
|
||||
var natW = line.scrollWidth;
|
||||
if (natW > 0) {
|
||||
var s = BEHIND_WIDTH / natW;
|
||||
line.style.transform = "scaleX(" + s + ") scaleY(2.8)";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
applyBehindScales();
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
function blockStart(bi) {
|
||||
var b = BLOCKS[bi];
|
||||
var firstIdx = b.behind ? b.behind[0][0] : b.front[0][0];
|
||||
return W[firstIdx].start;
|
||||
}
|
||||
|
||||
BLOCKS.forEach(function (block, bi) {
|
||||
var start = blockStart(bi);
|
||||
var nextStart = bi < BLOCKS.length - 1 ? blockStart(bi + 1) : DURATION;
|
||||
var bEl = allBehind[bi];
|
||||
var fEl = allFront[bi];
|
||||
|
||||
allBehind.forEach(function (other, oi) {
|
||||
if (oi !== bi) tl.set(other, { opacity: 0 }, start);
|
||||
});
|
||||
allFront.forEach(function (other, oi) {
|
||||
if (oi !== bi) tl.set(other, { opacity: 0 }, start);
|
||||
});
|
||||
|
||||
if (block.behind) {
|
||||
tl.set(bEl, { opacity: 1 }, start);
|
||||
block.behind.forEach(function (wi_arr, wi) {
|
||||
var wordEl = document.getElementById("bb" + bi + "w" + wi);
|
||||
var wordStart = W[wi_arr[0]].start;
|
||||
tl.set(wordEl, { opacity: 0 }, start);
|
||||
tl.to(wordEl, { opacity: 1, duration: ENTRY_DUR, ease: "power2.out" }, wordStart);
|
||||
});
|
||||
}
|
||||
|
||||
if (block.front) {
|
||||
tl.set(fEl, { opacity: 1 }, start);
|
||||
block.front.forEach(function (wi_arr, wi) {
|
||||
var wordEl = document.getElementById("fb" + bi + "w" + wi);
|
||||
var wordStart = W[wi_arr[0]].start;
|
||||
tl.set(wordEl, { opacity: 0 }, start);
|
||||
tl.set(wordEl, { opacity: 1 }, wordStart);
|
||||
});
|
||||
}
|
||||
|
||||
tl.set(bEl, { opacity: 0 }, nextStart);
|
||||
tl.set(fEl, { opacity: 0 }, nextStart);
|
||||
});
|
||||
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["caption-parallax-layers"] = tl;
|
||||
|
||||
document.fonts.ready.then(function () {
|
||||
applyBehindScales();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-parallax-layers",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Parallax Layers",
|
||||
"description": "Behind-subject 3D text layering with vertical stretch effect",
|
||||
"tags": ["captions", "caption-style", "parallax", "3d", "depth"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-parallax-layers.html",
|
||||
"target": "compositions/components/caption-parallax-layers.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-parallax-layers/preview.mp4?v=1779051692",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-parallax-layers.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Particle Burst</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=Outfit:wght@900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#particle-burst {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#bs-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.bs-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#bs-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.bs-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 0 100px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.bs-word {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 88px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
display: inline-block;
|
||||
letter-spacing: 0.05em;
|
||||
line-height: 1;
|
||||
}
|
||||
.bs-particle {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
left: 960px;
|
||||
bottom: 180px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="particle-burst"
|
||||
data-composition-id="caption-particle-burst"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="bs-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="bs-overlay"></div>
|
||||
<div id="bs-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
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 WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
// Keywords get particle burst + gold color
|
||||
var KEYWORDS = new Set([8, 12, 15, 24, 27]); // HyperFrames, HTML, professional, code, cinema.
|
||||
|
||||
var RAW_GROUPS = [
|
||||
[0, 3],
|
||||
[4, 7],
|
||||
[8, 8],
|
||||
[9, 12],
|
||||
[13, 15],
|
||||
[16, 16],
|
||||
[17, 19],
|
||||
[20, 22],
|
||||
[23, 24],
|
||||
[25, 27],
|
||||
];
|
||||
var GROUPS = RAW_GROUPS.map(function (pair, gi) {
|
||||
var ws = pair[0],
|
||||
we = pair[1];
|
||||
var nextStart = gi + 1 < RAW_GROUPS.length ? WORDS[RAW_GROUPS[gi + 1][0]].start : 8.0;
|
||||
var gEnd = Math.min(WORDS[we].end + 0.35, nextStart - 0.05);
|
||||
return { wordStart: ws, wordEnd: we, start: WORDS[ws].start, end: gEnd };
|
||||
});
|
||||
|
||||
var PARTICLE_COLORS = [
|
||||
"#FFD700",
|
||||
"#FF6B35",
|
||||
"#FF3C78",
|
||||
"#9B5DE5",
|
||||
"#00BBF9",
|
||||
"#00F5D4",
|
||||
"#ffffff",
|
||||
"#F15BB5",
|
||||
];
|
||||
var NUM_PARTICLES = 10;
|
||||
|
||||
var container = document.getElementById("bs-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var fontSize = fitFontSize(groupText, 88, "900", "'Outfit', sans-serif", 1720);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "bs-group";
|
||||
grp.id = "bs-grp-" + gi;
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var span = document.createElement("span");
|
||||
span.className = "bs-word";
|
||||
span.id = "bs-w-" + wi;
|
||||
span.textContent = w.text.toUpperCase();
|
||||
span.style.fontSize = fontSize + "px";
|
||||
grp.appendChild(span);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
});
|
||||
|
||||
// Create particles for keyword words
|
||||
var particleData = {};
|
||||
WORDS.forEach(function (w, wi) {
|
||||
if (!KEYWORDS.has(wi)) return;
|
||||
var rng = mulberry32(wi * 137 + 99);
|
||||
particleData[wi] = [];
|
||||
for (var p = 0; p < NUM_PARTICLES; p++) {
|
||||
var angle = (p / NUM_PARTICLES) * Math.PI * 2 + rng() * 0.6;
|
||||
var dist = 120 + rng() * 200;
|
||||
var sz = 4 + rng() * 8;
|
||||
var dx = Math.cos(angle) * dist;
|
||||
var dy = -Math.sin(angle) * dist;
|
||||
var dot = document.createElement("div");
|
||||
dot.className = "bs-particle";
|
||||
dot.id = "bs-p-" + wi + "-" + p;
|
||||
dot.style.backgroundColor = PARTICLE_COLORS[p % PARTICLE_COLORS.length];
|
||||
dot.style.width = sz.toFixed(1) + "px";
|
||||
dot.style.height = sz.toFixed(1) + "px";
|
||||
container.appendChild(dot);
|
||||
particleData[wi].push({ el: dot, dx: dx, dy: dy });
|
||||
}
|
||||
});
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var grp = document.getElementById("bs-grp-" + gi);
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
|
||||
tl.set(grp, { visibility: "visible" }, g.start);
|
||||
tl.fromTo(
|
||||
grp,
|
||||
{ opacity: 0, scale: 0.92 },
|
||||
{ opacity: 1, scale: 1, duration: 0.2, ease: "back.out(1.5)" },
|
||||
g.start,
|
||||
);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var wordEl = document.getElementById("bs-w-" + wi);
|
||||
var isKW = KEYWORDS.has(wi);
|
||||
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ color: isKW ? "#FFD700" : "#ffffff", scale: isKW ? 1.12 : 1.05, duration: 0.08 },
|
||||
w.start,
|
||||
);
|
||||
tl.to(wordEl, { color: "rgba(255,255,255,0.4)", scale: 1.0, duration: 0.1 }, w.end);
|
||||
|
||||
if (isKW && particleData[wi]) {
|
||||
particleData[wi].forEach(function (pd, pi) {
|
||||
tl.set(pd.el, { x: 0, y: 0, opacity: 0 }, w.start);
|
||||
tl.to(
|
||||
pd.el,
|
||||
{ x: pd.dx, y: pd.dy, opacity: 1, duration: 0.12, ease: "power3.out" },
|
||||
w.start + pi * 0.018,
|
||||
);
|
||||
tl.to(
|
||||
pd.el,
|
||||
{ opacity: 0, duration: 0.45, ease: "power1.in" },
|
||||
w.start + 0.12 + pi * 0.018,
|
||||
);
|
||||
tl.set(pd.el, { opacity: 0, x: 0, y: 0 }, w.start + 0.7);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, duration: 0.14, ease: "power2.in" }, g.end - 0.14);
|
||||
tl.set(grp, { opacity: 0, visibility: "hidden" }, g.end);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-particle-burst"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,311 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Particle Burst</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=Outfit:wght@900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#particle-burst {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
#bs-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.bs-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#bs-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.bs-group {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 0 100px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.bs-word {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 88px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
display: inline-block;
|
||||
letter-spacing: 0.05em;
|
||||
line-height: 1;
|
||||
}
|
||||
.bs-particle {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
left: 960px;
|
||||
bottom: 180px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="particle-burst"
|
||||
data-composition-id="caption-particle-burst"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="bs-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="bs-overlay"></div>
|
||||
<div id="bs-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
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 WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
// Keywords get particle burst + gold color
|
||||
var KEYWORDS = new Set([8, 12, 15, 24, 27]); // HyperFrames, HTML, professional, code, cinema.
|
||||
|
||||
var RAW_GROUPS = [
|
||||
[0, 3],
|
||||
[4, 7],
|
||||
[8, 8],
|
||||
[9, 12],
|
||||
[13, 15],
|
||||
[16, 16],
|
||||
[17, 19],
|
||||
[20, 22],
|
||||
[23, 24],
|
||||
[25, 27],
|
||||
];
|
||||
var GROUPS = RAW_GROUPS.map(function (pair, gi) {
|
||||
var ws = pair[0],
|
||||
we = pair[1];
|
||||
var nextStart = gi + 1 < RAW_GROUPS.length ? WORDS[RAW_GROUPS[gi + 1][0]].start : 8.0;
|
||||
var gEnd = Math.min(WORDS[we].end + 0.35, nextStart - 0.05);
|
||||
return { wordStart: ws, wordEnd: we, start: WORDS[ws].start, end: gEnd };
|
||||
});
|
||||
|
||||
var PARTICLE_COLORS = [
|
||||
"#FFD700",
|
||||
"#FF6B35",
|
||||
"#FF3C78",
|
||||
"#9B5DE5",
|
||||
"#00BBF9",
|
||||
"#00F5D4",
|
||||
"#ffffff",
|
||||
"#F15BB5",
|
||||
];
|
||||
var NUM_PARTICLES = 10;
|
||||
|
||||
var container = document.getElementById("bs-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
var groupText = groupWords
|
||||
.map(function (w) {
|
||||
return w.text.toUpperCase();
|
||||
})
|
||||
.join(" ");
|
||||
var fontSize = fitFontSize(groupText, 88, "900", "'Outfit', sans-serif", 1720);
|
||||
var grp = document.createElement("div");
|
||||
grp.className = "bs-group";
|
||||
grp.id = "bs-grp-" + gi;
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var span = document.createElement("span");
|
||||
span.className = "bs-word";
|
||||
span.id = "bs-w-" + wi;
|
||||
span.textContent = w.text.toUpperCase();
|
||||
span.style.fontSize = fontSize + "px";
|
||||
grp.appendChild(span);
|
||||
});
|
||||
|
||||
container.appendChild(grp);
|
||||
});
|
||||
|
||||
// Create particles for keyword words
|
||||
var particleData = {};
|
||||
WORDS.forEach(function (w, wi) {
|
||||
if (!KEYWORDS.has(wi)) return;
|
||||
var rng = mulberry32(wi * 137 + 99);
|
||||
particleData[wi] = [];
|
||||
for (var p = 0; p < NUM_PARTICLES; p++) {
|
||||
var angle = (p / NUM_PARTICLES) * Math.PI * 2 + rng() * 0.6;
|
||||
var dist = 120 + rng() * 200;
|
||||
var sz = 4 + rng() * 8;
|
||||
var dx = Math.cos(angle) * dist;
|
||||
var dy = -Math.sin(angle) * dist;
|
||||
var dot = document.createElement("div");
|
||||
dot.className = "bs-particle";
|
||||
dot.id = "bs-p-" + wi + "-" + p;
|
||||
dot.style.backgroundColor = PARTICLE_COLORS[p % PARTICLE_COLORS.length];
|
||||
dot.style.width = sz.toFixed(1) + "px";
|
||||
dot.style.height = sz.toFixed(1) + "px";
|
||||
container.appendChild(dot);
|
||||
particleData[wi].push({ el: dot, dx: dx, dy: dy });
|
||||
}
|
||||
});
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var grp = document.getElementById("bs-grp-" + gi);
|
||||
var groupWords = WORDS.slice(g.wordStart, g.wordEnd + 1);
|
||||
|
||||
tl.set(grp, { visibility: "visible" }, g.start);
|
||||
tl.fromTo(
|
||||
grp,
|
||||
{ opacity: 0, scale: 0.92 },
|
||||
{ opacity: 1, scale: 1, duration: 0.2, ease: "back.out(1.5)" },
|
||||
g.start,
|
||||
);
|
||||
|
||||
groupWords.forEach(function (w, i) {
|
||||
var wi = g.wordStart + i;
|
||||
var wordEl = document.getElementById("bs-w-" + wi);
|
||||
var isKW = KEYWORDS.has(wi);
|
||||
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ color: isKW ? "#FFD700" : "#ffffff", scale: isKW ? 1.12 : 1.05, duration: 0.08 },
|
||||
w.start,
|
||||
);
|
||||
tl.to(wordEl, { color: "rgba(255,255,255,0.4)", scale: 1.0, duration: 0.1 }, w.end);
|
||||
|
||||
if (isKW && particleData[wi]) {
|
||||
particleData[wi].forEach(function (pd, pi) {
|
||||
tl.set(pd.el, { x: 0, y: 0, opacity: 0 }, w.start);
|
||||
tl.to(
|
||||
pd.el,
|
||||
{ x: pd.dx, y: pd.dy, opacity: 1, duration: 0.12, ease: "power3.out" },
|
||||
w.start + pi * 0.018,
|
||||
);
|
||||
tl.to(
|
||||
pd.el,
|
||||
{ opacity: 0, duration: 0.45, ease: "power1.in" },
|
||||
w.start + 0.12 + pi * 0.018,
|
||||
);
|
||||
tl.set(pd.el, { opacity: 0, x: 0, y: 0 }, w.start + 0.7);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
tl.to(grp, { opacity: 0, duration: 0.14, ease: "power2.in" }, g.end - 0.14);
|
||||
tl.set(grp, { opacity: 0, visibility: "hidden" }, g.end);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-particle-burst"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-particle-burst",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Particle Burst",
|
||||
"description": "Keyword words trigger colored particle explosions",
|
||||
"tags": ["captions", "caption-style", "particles", "burst", "effects"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-particle-burst.html",
|
||||
"target": "compositions/components/caption-particle-burst.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-particle-burst/preview.mp4?v=1779051416",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-particle-burst.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Pill 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=Poppins:wght@700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
font-family: "Poppins", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#pill-karaoke {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#avatar-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.safe-zone {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1600px;
|
||||
height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1600px;
|
||||
height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.caption-pill {
|
||||
width: auto;
|
||||
max-width: 1600px;
|
||||
padding: 18px 60px 20px;
|
||||
border-radius: 22px;
|
||||
background: #e7e5e7;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
text-align: center;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.caption-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
color: #a6a6a6;
|
||||
font-family: "Poppins", Arial, sans-serif;
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
line-height: 1.16;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
line-height: 1.16;
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="pill-karaoke"
|
||||
data-composition-id="caption-pill-karaoke"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="avatar-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var SAFE_ZONE_WIDTH = 1400;
|
||||
var PILL_MAX_WIDTH = 1400;
|
||||
var PILL_MIN_PADDING_X = 30;
|
||||
var PILL_MAX_PADDING_X = 60;
|
||||
var FONT_WIDTH_SAFETY = 1.18;
|
||||
var FONT_FAMILY = "Poppins";
|
||||
var FONT_WEIGHT = "700";
|
||||
var BASE_FONT_SIZE = 72;
|
||||
var MIN_FONT_SIZE = 52;
|
||||
var MAX_WORDS_PER_GROUP = 4;
|
||||
var WORD_SPACING = 16;
|
||||
var PAUSE_THRESHOLD = 0.1;
|
||||
var GROUP_END_BUFFER = 0.3;
|
||||
var COLOR_INACTIVE = "#A6A6A6";
|
||||
var COLOR_ACTIVE = "#1C1E1D";
|
||||
var COLOR_FADE_DURATION = 0.1;
|
||||
var WORD_FADE_LEAD = 0.05;
|
||||
|
||||
var TRANSCRIPT = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
function normalizeWords(rawWords) {
|
||||
return rawWords
|
||||
.map(function (item) {
|
||||
var text = String(item.word || item.text || "").trim();
|
||||
return {
|
||||
text: text,
|
||||
start: Math.max(0, Number(item.start) || 0),
|
||||
end: Math.min(DURATION, Math.max(Number(item.start) || 0, Number(item.end) || 0)),
|
||||
};
|
||||
})
|
||||
.filter(function (w) {
|
||||
return w.text.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
var measureCanvas = document.createElement("canvas");
|
||||
var measureContext = measureCanvas.getContext("2d");
|
||||
|
||||
function measureWordWidth(text, fontSize) {
|
||||
measureContext.font = FONT_WEIGHT + " " + fontSize + "px " + FONT_FAMILY;
|
||||
return measureContext.measureText(text).width * FONT_WIDTH_SAFETY;
|
||||
}
|
||||
|
||||
function measureLineWidth(words, fontSize) {
|
||||
return words.reduce(function (total, word, index) {
|
||||
return (
|
||||
total + measureWordWidth(word.text.toLowerCase(), fontSize) + (index ? WORD_SPACING : 0)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function splitLinesForGroup(words, fontSize) {
|
||||
var maxLineWidth = PILL_MAX_WIDTH - PILL_MIN_PADDING_X * 2;
|
||||
if (
|
||||
words.some(function (w) {
|
||||
return measureWordWidth(w.text.toLowerCase(), fontSize) > maxLineWidth;
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (measureLineWidth(words, fontSize) <= maxLineWidth) return [words];
|
||||
var bestSplit = null;
|
||||
var bestImbalance = Infinity;
|
||||
for (var split = 1; split < words.length; split++) {
|
||||
var firstLine = words.slice(0, split);
|
||||
var secondLine = words.slice(split);
|
||||
var firstWidth = measureLineWidth(firstLine, fontSize);
|
||||
var secondWidth = measureLineWidth(secondLine, fontSize);
|
||||
if (firstWidth <= maxLineWidth && secondWidth <= maxLineWidth) {
|
||||
var imbalance = Math.abs(firstWidth - secondWidth);
|
||||
if (imbalance < bestImbalance) {
|
||||
bestSplit = [firstLine, secondLine];
|
||||
bestImbalance = imbalance;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestSplit;
|
||||
}
|
||||
|
||||
function fitsInTwoLinesAtSize(words, fontSize) {
|
||||
return splitLinesForGroup(words, fontSize) !== null;
|
||||
}
|
||||
|
||||
function planPillLayout(words, fontSize) {
|
||||
var lines = splitLinesForGroup(words, fontSize) || [words];
|
||||
var widestLine = lines.reduce(function (max, lineWords) {
|
||||
return Math.max(max, measureLineWidth(lineWords, fontSize));
|
||||
}, 0);
|
||||
var availablePadding = Math.floor((PILL_MAX_WIDTH - widestLine) / 2);
|
||||
var padX = Math.max(PILL_MIN_PADDING_X, Math.min(PILL_MAX_PADDING_X, availablePadding));
|
||||
var pillWidth = Math.min(PILL_MAX_WIDTH, Math.ceil(widestLine + padX * 2));
|
||||
return { lines: lines, padX: padX, pillWidth: pillWidth };
|
||||
}
|
||||
|
||||
function makeGroups(words) {
|
||||
var groups = [];
|
||||
var current = [];
|
||||
words.forEach(function (word, index) {
|
||||
var testGroup = current.concat([word]);
|
||||
var nextWord = words[index + 1];
|
||||
var wouldExceedMaxWords = testGroup.length > MAX_WORDS_PER_GROUP;
|
||||
var wouldExceedTwoLines = !fitsInTwoLinesAtSize(testGroup, BASE_FONT_SIZE);
|
||||
if (wouldExceedMaxWords || wouldExceedTwoLines) {
|
||||
if (current.length) {
|
||||
groups.push({
|
||||
words: current.slice(),
|
||||
start: current[0].start,
|
||||
end: current[current.length - 1].end,
|
||||
});
|
||||
}
|
||||
current = [word];
|
||||
return;
|
||||
}
|
||||
current.push(word);
|
||||
var hasPunctuation = /[,.:!?]$/.test(word.text);
|
||||
var pauseDuration = nextWord ? nextWord.start - word.end : 0;
|
||||
var hasNaturalPause = pauseDuration >= PAUSE_THRESHOLD;
|
||||
var maxWordsReached = current.length >= MAX_WORDS_PER_GROUP;
|
||||
if (maxWordsReached || !nextWord || hasPunctuation || hasNaturalPause) {
|
||||
groups.push({
|
||||
words: current.slice(),
|
||||
start: current[0].start,
|
||||
end: current[current.length - 1].end,
|
||||
});
|
||||
current = [];
|
||||
}
|
||||
});
|
||||
if (current.length)
|
||||
groups.push({
|
||||
words: current.slice(),
|
||||
start: current[0].start,
|
||||
end: current[current.length - 1].end,
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
function fontSizeForGroup(words) {
|
||||
var size = BASE_FONT_SIZE;
|
||||
while (size > MIN_FONT_SIZE && !fitsInTwoLinesAtSize(words, size)) {
|
||||
size -= 2;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
function buildCaptions(groups) {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
groups.forEach(function (group, groupIndex) {
|
||||
var groupEl = document.createElement("div");
|
||||
var pillEl = document.createElement("div");
|
||||
var copyEl = document.createElement("div");
|
||||
var fontSize = fontSizeForGroup(group.words);
|
||||
groupEl.id = "caption-group-" + groupIndex;
|
||||
groupEl.className = "caption-group";
|
||||
pillEl.className = "caption-pill";
|
||||
copyEl.className = "caption-copy";
|
||||
copyEl.style.fontSize = fontSize + "px";
|
||||
var indexedWords = group.words.map(function (word, wordIndex) {
|
||||
return { text: word.text, start: word.start, end: word.end, wordIndex: wordIndex };
|
||||
});
|
||||
var layout = planPillLayout(indexedWords, fontSize);
|
||||
pillEl.style.width = layout.pillWidth + "px";
|
||||
pillEl.style.paddingLeft = layout.padX + "px";
|
||||
pillEl.style.paddingRight = layout.padX + "px";
|
||||
layout.lines.slice(0, 2).forEach(function (lineWords) {
|
||||
var lineEl = document.createElement("div");
|
||||
lineEl.className = "caption-line";
|
||||
lineWords.forEach(function (word) {
|
||||
var wordEl = document.createElement("span");
|
||||
wordEl.id = "caption-word-" + groupIndex + "-" + word.wordIndex;
|
||||
wordEl.className = "caption-word";
|
||||
wordEl.textContent = word.text.toLowerCase();
|
||||
lineEl.appendChild(wordEl);
|
||||
});
|
||||
copyEl.appendChild(lineEl);
|
||||
});
|
||||
pillEl.appendChild(copyEl);
|
||||
groupEl.appendChild(pillEl);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
}
|
||||
|
||||
var WORDS = normalizeWords(TRANSCRIPT);
|
||||
var GROUPS = makeGroups(WORDS);
|
||||
buildCaptions(GROUPS);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, groupIndex) {
|
||||
var groupEl = document.getElementById("caption-group-" + groupIndex);
|
||||
var nextGroup = GROUPS[groupIndex + 1];
|
||||
var isLastGroup = groupIndex === GROUPS.length - 1;
|
||||
var effectiveEnd = isLastGroup
|
||||
? DURATION
|
||||
: Math.min(nextGroup.start, group.end + GROUP_END_BUFFER);
|
||||
var visibleStart = Math.max(0, group.start);
|
||||
var visibleEnd = Math.max(visibleStart + 0.01, effectiveEnd);
|
||||
|
||||
tl.set(groupEl, { opacity: 1 }, visibleStart);
|
||||
tl.set(groupEl, { opacity: 0 }, visibleEnd);
|
||||
|
||||
group.words.forEach(function (word, wordIndex) {
|
||||
var wordEl = document.getElementById("caption-word-" + groupIndex + "-" + wordIndex);
|
||||
var isFirstWord = wordIndex === 0;
|
||||
var wordStart = Math.max(visibleStart, word.start - WORD_FADE_LEAD);
|
||||
tl.set(wordEl, { color: isFirstWord ? COLOR_ACTIVE : COLOR_INACTIVE }, visibleStart);
|
||||
if (isFirstWord) return;
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ color: COLOR_ACTIVE, duration: COLOR_FADE_DURATION, ease: "none" },
|
||||
wordStart,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
window.__timelines["caption-pill-karaoke"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,399 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Pill 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=Poppins:wght@700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
font-family: "Poppins", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#pill-karaoke {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
#avatar-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.safe-zone {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1600px;
|
||||
height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1600px;
|
||||
height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.caption-pill {
|
||||
width: auto;
|
||||
max-width: 1600px;
|
||||
padding: 18px 60px 20px;
|
||||
border-radius: 22px;
|
||||
background: #e7e5e7;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
text-align: center;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.caption-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
color: #a6a6a6;
|
||||
font-family: "Poppins", Arial, sans-serif;
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
line-height: 1.16;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
line-height: 1.16;
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="pill-karaoke"
|
||||
data-composition-id="caption-pill-karaoke"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="avatar-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var SAFE_ZONE_WIDTH = 1400;
|
||||
var PILL_MAX_WIDTH = 1400;
|
||||
var PILL_MIN_PADDING_X = 30;
|
||||
var PILL_MAX_PADDING_X = 60;
|
||||
var FONT_WIDTH_SAFETY = 1.18;
|
||||
var FONT_FAMILY = "Poppins";
|
||||
var FONT_WEIGHT = "700";
|
||||
var BASE_FONT_SIZE = 72;
|
||||
var MIN_FONT_SIZE = 52;
|
||||
var MAX_WORDS_PER_GROUP = 4;
|
||||
var WORD_SPACING = 16;
|
||||
var PAUSE_THRESHOLD = 0.1;
|
||||
var GROUP_END_BUFFER = 0.3;
|
||||
var COLOR_INACTIVE = "#A6A6A6";
|
||||
var COLOR_ACTIVE = "#1C1E1D";
|
||||
var COLOR_FADE_DURATION = 0.1;
|
||||
var WORD_FADE_LEAD = 0.05;
|
||||
|
||||
var TRANSCRIPT = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
function normalizeWords(rawWords) {
|
||||
return rawWords
|
||||
.map(function (item) {
|
||||
var text = String(item.word || item.text || "").trim();
|
||||
return {
|
||||
text: text,
|
||||
start: Math.max(0, Number(item.start) || 0),
|
||||
end: Math.min(DURATION, Math.max(Number(item.start) || 0, Number(item.end) || 0)),
|
||||
};
|
||||
})
|
||||
.filter(function (w) {
|
||||
return w.text.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
var measureCanvas = document.createElement("canvas");
|
||||
var measureContext = measureCanvas.getContext("2d");
|
||||
|
||||
function measureWordWidth(text, fontSize) {
|
||||
measureContext.font = FONT_WEIGHT + " " + fontSize + "px " + FONT_FAMILY;
|
||||
return measureContext.measureText(text).width * FONT_WIDTH_SAFETY;
|
||||
}
|
||||
|
||||
function measureLineWidth(words, fontSize) {
|
||||
return words.reduce(function (total, word, index) {
|
||||
return (
|
||||
total + measureWordWidth(word.text.toLowerCase(), fontSize) + (index ? WORD_SPACING : 0)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function splitLinesForGroup(words, fontSize) {
|
||||
var maxLineWidth = PILL_MAX_WIDTH - PILL_MIN_PADDING_X * 2;
|
||||
if (
|
||||
words.some(function (w) {
|
||||
return measureWordWidth(w.text.toLowerCase(), fontSize) > maxLineWidth;
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (measureLineWidth(words, fontSize) <= maxLineWidth) return [words];
|
||||
var bestSplit = null;
|
||||
var bestImbalance = Infinity;
|
||||
for (var split = 1; split < words.length; split++) {
|
||||
var firstLine = words.slice(0, split);
|
||||
var secondLine = words.slice(split);
|
||||
var firstWidth = measureLineWidth(firstLine, fontSize);
|
||||
var secondWidth = measureLineWidth(secondLine, fontSize);
|
||||
if (firstWidth <= maxLineWidth && secondWidth <= maxLineWidth) {
|
||||
var imbalance = Math.abs(firstWidth - secondWidth);
|
||||
if (imbalance < bestImbalance) {
|
||||
bestSplit = [firstLine, secondLine];
|
||||
bestImbalance = imbalance;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestSplit;
|
||||
}
|
||||
|
||||
function fitsInTwoLinesAtSize(words, fontSize) {
|
||||
return splitLinesForGroup(words, fontSize) !== null;
|
||||
}
|
||||
|
||||
function planPillLayout(words, fontSize) {
|
||||
var lines = splitLinesForGroup(words, fontSize) || [words];
|
||||
var widestLine = lines.reduce(function (max, lineWords) {
|
||||
return Math.max(max, measureLineWidth(lineWords, fontSize));
|
||||
}, 0);
|
||||
var availablePadding = Math.floor((PILL_MAX_WIDTH - widestLine) / 2);
|
||||
var padX = Math.max(PILL_MIN_PADDING_X, Math.min(PILL_MAX_PADDING_X, availablePadding));
|
||||
var pillWidth = Math.min(PILL_MAX_WIDTH, Math.ceil(widestLine + padX * 2));
|
||||
return { lines: lines, padX: padX, pillWidth: pillWidth };
|
||||
}
|
||||
|
||||
function makeGroups(words) {
|
||||
var groups = [];
|
||||
var current = [];
|
||||
words.forEach(function (word, index) {
|
||||
var testGroup = current.concat([word]);
|
||||
var nextWord = words[index + 1];
|
||||
var wouldExceedMaxWords = testGroup.length > MAX_WORDS_PER_GROUP;
|
||||
var wouldExceedTwoLines = !fitsInTwoLinesAtSize(testGroup, BASE_FONT_SIZE);
|
||||
if (wouldExceedMaxWords || wouldExceedTwoLines) {
|
||||
if (current.length) {
|
||||
groups.push({
|
||||
words: current.slice(),
|
||||
start: current[0].start,
|
||||
end: current[current.length - 1].end,
|
||||
});
|
||||
}
|
||||
current = [word];
|
||||
return;
|
||||
}
|
||||
current.push(word);
|
||||
var hasPunctuation = /[,.:!?]$/.test(word.text);
|
||||
var pauseDuration = nextWord ? nextWord.start - word.end : 0;
|
||||
var hasNaturalPause = pauseDuration >= PAUSE_THRESHOLD;
|
||||
var maxWordsReached = current.length >= MAX_WORDS_PER_GROUP;
|
||||
if (maxWordsReached || !nextWord || hasPunctuation || hasNaturalPause) {
|
||||
groups.push({
|
||||
words: current.slice(),
|
||||
start: current[0].start,
|
||||
end: current[current.length - 1].end,
|
||||
});
|
||||
current = [];
|
||||
}
|
||||
});
|
||||
if (current.length)
|
||||
groups.push({
|
||||
words: current.slice(),
|
||||
start: current[0].start,
|
||||
end: current[current.length - 1].end,
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
function fontSizeForGroup(words) {
|
||||
var size = BASE_FONT_SIZE;
|
||||
while (size > MIN_FONT_SIZE && !fitsInTwoLinesAtSize(words, size)) {
|
||||
size -= 2;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
function buildCaptions(groups) {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
groups.forEach(function (group, groupIndex) {
|
||||
var groupEl = document.createElement("div");
|
||||
var pillEl = document.createElement("div");
|
||||
var copyEl = document.createElement("div");
|
||||
var fontSize = fontSizeForGroup(group.words);
|
||||
groupEl.id = "caption-group-" + groupIndex;
|
||||
groupEl.className = "caption-group";
|
||||
pillEl.className = "caption-pill";
|
||||
copyEl.className = "caption-copy";
|
||||
copyEl.style.fontSize = fontSize + "px";
|
||||
var indexedWords = group.words.map(function (word, wordIndex) {
|
||||
return { text: word.text, start: word.start, end: word.end, wordIndex: wordIndex };
|
||||
});
|
||||
var layout = planPillLayout(indexedWords, fontSize);
|
||||
pillEl.style.width = layout.pillWidth + "px";
|
||||
pillEl.style.paddingLeft = layout.padX + "px";
|
||||
pillEl.style.paddingRight = layout.padX + "px";
|
||||
layout.lines.slice(0, 2).forEach(function (lineWords) {
|
||||
var lineEl = document.createElement("div");
|
||||
lineEl.className = "caption-line";
|
||||
lineWords.forEach(function (word) {
|
||||
var wordEl = document.createElement("span");
|
||||
wordEl.id = "caption-word-" + groupIndex + "-" + word.wordIndex;
|
||||
wordEl.className = "caption-word";
|
||||
wordEl.textContent = word.text.toLowerCase();
|
||||
lineEl.appendChild(wordEl);
|
||||
});
|
||||
copyEl.appendChild(lineEl);
|
||||
});
|
||||
pillEl.appendChild(copyEl);
|
||||
groupEl.appendChild(pillEl);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
}
|
||||
|
||||
var WORDS = normalizeWords(TRANSCRIPT);
|
||||
var GROUPS = makeGroups(WORDS);
|
||||
buildCaptions(GROUPS);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, groupIndex) {
|
||||
var groupEl = document.getElementById("caption-group-" + groupIndex);
|
||||
var nextGroup = GROUPS[groupIndex + 1];
|
||||
var isLastGroup = groupIndex === GROUPS.length - 1;
|
||||
var effectiveEnd = isLastGroup
|
||||
? DURATION
|
||||
: Math.min(nextGroup.start, group.end + GROUP_END_BUFFER);
|
||||
var visibleStart = Math.max(0, group.start);
|
||||
var visibleEnd = Math.max(visibleStart + 0.01, effectiveEnd);
|
||||
|
||||
tl.set(groupEl, { opacity: 1 }, visibleStart);
|
||||
tl.set(groupEl, { opacity: 0 }, visibleEnd);
|
||||
|
||||
group.words.forEach(function (word, wordIndex) {
|
||||
var wordEl = document.getElementById("caption-word-" + groupIndex + "-" + wordIndex);
|
||||
var isFirstWord = wordIndex === 0;
|
||||
var wordStart = Math.max(visibleStart, word.start - WORD_FADE_LEAD);
|
||||
tl.set(wordEl, { color: isFirstWord ? COLOR_ACTIVE : COLOR_INACTIVE }, visibleStart);
|
||||
if (isFirstWord) return;
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ color: COLOR_ACTIVE, duration: COLOR_FADE_DURATION, ease: "none" },
|
||||
wordStart,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
window.__timelines["caption-pill-karaoke"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-pill-karaoke",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Pill Karaoke",
|
||||
"description": "Pill-shaped container with per-word karaoke color highlight",
|
||||
"tags": ["captions", "caption-style", "karaoke", "pill"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-pill-karaoke.html",
|
||||
"target": "compositions/components/caption-pill-karaoke.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-pill-karaoke/preview.mp4?v=1779051416",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-pill-karaoke.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Texture</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=Anton&display=swap" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
.hf-texture-text {
|
||||
color: #fff;
|
||||
-webkit-mask-size: var(--mask-size, cover);
|
||||
mask-size: var(--mask-size, cover);
|
||||
-webkit-mask-position: var(--mask-position, center);
|
||||
mask-position: var(--mask-position, center);
|
||||
-webkit-mask-mode: luminance;
|
||||
mask-mode: luminance;
|
||||
filter: contrast(var(--mask-contrast, 1));
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#texture-lava {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#lv-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.lv-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#lv-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lv-texture-override {
|
||||
-webkit-mask-size: 200% 200%;
|
||||
mask-size: 200% 200%;
|
||||
-webkit-mask-position: 0% 50%;
|
||||
mask-position: 0% 50%;
|
||||
-webkit-mask-mode: luminance;
|
||||
mask-mode: luminance;
|
||||
filter: contrast(1.2);
|
||||
}
|
||||
|
||||
.lv-shadow {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
filter: drop-shadow(0 4px 24px rgba(255, 100, 20, 0.55));
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.lv-word {
|
||||
font-family: "Anton", sans-serif;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
color: #ffd0a0;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="caption-texture"
|
||||
data-composition-id="caption-texture"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-composition-variables='{"texture":{"type":"string","default":"lava"}}'
|
||||
>
|
||||
<video
|
||||
id="lv-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="lv-overlay"></div>
|
||||
<div id="lv-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var vars =
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {};
|
||||
var TEXTURE = vars.texture || "lava";
|
||||
var TEXTURE_URL = "compositions/components/" + TEXTURE + ".png";
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var KEYWORDS = new Set([8, 12, 15, 24, 27]);
|
||||
|
||||
var container = document.getElementById("lv-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
WORDS.forEach(function (w, wi) {
|
||||
var txt = w.text.toUpperCase();
|
||||
var fontSize = fitFontSize(txt, 200, "400", "'Anton', sans-serif", 1720);
|
||||
|
||||
var shadow = document.createElement("div");
|
||||
shadow.className = "lv-shadow";
|
||||
shadow.id = "lv-shadow-" + wi;
|
||||
gsap.set(shadow, { yPercent: -50 });
|
||||
|
||||
var wordEl = document.createElement("div");
|
||||
wordEl.className = "hf-texture-text lv-texture-override lv-word";
|
||||
wordEl.id = "lv-word-" + wi;
|
||||
wordEl.textContent = txt;
|
||||
wordEl.style.fontSize = fontSize + "px";
|
||||
wordEl.style.webkitMaskImage = "url('" + TEXTURE_URL + "')";
|
||||
wordEl.style.maskImage = "url('" + TEXTURE_URL + "')";
|
||||
if (KEYWORDS.has(wi)) wordEl.style.color = "#FFAA44";
|
||||
|
||||
shadow.appendChild(wordEl);
|
||||
container.appendChild(shadow);
|
||||
|
||||
var dur = Math.max(w.end - w.start + 0.3, 0.4);
|
||||
|
||||
tl.set(shadow, { visibility: "visible", opacity: 0 }, w.start);
|
||||
tl.fromTo(
|
||||
shadow,
|
||||
{ opacity: 0, scale: 0.88 },
|
||||
{ opacity: 1, scale: 1, duration: 0.18, ease: "power3.out" },
|
||||
w.start,
|
||||
);
|
||||
tl.set(wordEl, { "--mask-position": "0% 50%" }, w.start);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ "--mask-position": "100% 50%", duration: dur, ease: "sine.inOut" },
|
||||
w.start,
|
||||
);
|
||||
|
||||
tl.to(shadow, { opacity: 0, scale: 1.08, duration: 0.1, ease: "power2.in" }, w.end);
|
||||
tl.set(shadow, { opacity: 0, visibility: "hidden" }, w.end + 0.1);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-texture"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,226 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Texture</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=Anton&display=swap" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
.hf-texture-text {
|
||||
color: #fff;
|
||||
-webkit-mask-size: var(--mask-size, cover);
|
||||
mask-size: var(--mask-size, cover);
|
||||
-webkit-mask-position: var(--mask-position, center);
|
||||
mask-position: var(--mask-position, center);
|
||||
-webkit-mask-mode: luminance;
|
||||
mask-mode: luminance;
|
||||
filter: contrast(var(--mask-contrast, 1));
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #0a0805;
|
||||
}
|
||||
#texture-lava {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #0a0805;
|
||||
}
|
||||
#lv-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.lv-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#lv-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lv-texture-override {
|
||||
-webkit-mask-size: 200% 200%;
|
||||
mask-size: 200% 200%;
|
||||
-webkit-mask-position: 0% 50%;
|
||||
mask-position: 0% 50%;
|
||||
-webkit-mask-mode: luminance;
|
||||
mask-mode: luminance;
|
||||
filter: contrast(1.2);
|
||||
}
|
||||
|
||||
.lv-shadow {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
filter: drop-shadow(0 4px 24px rgba(255, 100, 20, 0.55));
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.lv-word {
|
||||
font-family: "Anton", sans-serif;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
color: #ffd0a0;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="caption-texture"
|
||||
data-composition-id="caption-texture"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-composition-variables='{"texture":{"type":"string","default":"lava"}}'
|
||||
>
|
||||
<video
|
||||
id="lv-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
<div class="lv-overlay"></div>
|
||||
<div id="lv-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var vars =
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {};
|
||||
var TEXTURE = vars.texture || "lava";
|
||||
var TEXTURE_URL = "compositions/components/" + TEXTURE + ".png";
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var WORDS = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
var KEYWORDS = new Set([8, 12, 15, 24, 27]);
|
||||
|
||||
var container = document.getElementById("lv-container");
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
WORDS.forEach(function (w, wi) {
|
||||
var txt = w.text.toUpperCase();
|
||||
var fontSize = fitFontSize(txt, 200, "400", "'Anton', sans-serif", 1720);
|
||||
|
||||
var shadow = document.createElement("div");
|
||||
shadow.className = "lv-shadow";
|
||||
shadow.id = "lv-shadow-" + wi;
|
||||
gsap.set(shadow, { yPercent: -50 });
|
||||
|
||||
var wordEl = document.createElement("div");
|
||||
wordEl.className = "hf-texture-text lv-texture-override lv-word";
|
||||
wordEl.id = "lv-word-" + wi;
|
||||
wordEl.textContent = txt;
|
||||
wordEl.style.fontSize = fontSize + "px";
|
||||
wordEl.style.webkitMaskImage = "url('" + TEXTURE_URL + "')";
|
||||
wordEl.style.maskImage = "url('" + TEXTURE_URL + "')";
|
||||
if (KEYWORDS.has(wi)) wordEl.style.color = "#FFAA44";
|
||||
|
||||
shadow.appendChild(wordEl);
|
||||
container.appendChild(shadow);
|
||||
|
||||
var dur = Math.max(w.end - w.start + 0.3, 0.4);
|
||||
|
||||
tl.set(shadow, { visibility: "visible", opacity: 0 }, w.start);
|
||||
tl.fromTo(
|
||||
shadow,
|
||||
{ opacity: 0, scale: 0.88 },
|
||||
{ opacity: 1, scale: 1, duration: 0.18, ease: "power3.out" },
|
||||
w.start,
|
||||
);
|
||||
tl.set(wordEl, { "--mask-position": "0% 50%" }, w.start);
|
||||
tl.to(
|
||||
wordEl,
|
||||
{ "--mask-position": "100% 50%", duration: dur, ease: "sine.inOut" },
|
||||
w.start,
|
||||
);
|
||||
|
||||
tl.to(shadow, { opacity: 0, scale: 1.08, duration: 0.1, ease: "power2.in" }, w.end);
|
||||
tl.set(shadow, { opacity: 0, visibility: "hidden" }, w.end + 0.1);
|
||||
});
|
||||
|
||||
tl.seek(0);
|
||||
window.__timelines["caption-texture"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 98 KiB |
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-texture",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Texture",
|
||||
"description": "Flowing texture mask over large uppercase text — ships with 6 textures (lava, marble, metal, wood, concrete, rock), configurable via the texture variable",
|
||||
"tags": ["captions", "caption-style", "texture", "mask"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-texture.html",
|
||||
"target": "compositions/components/caption-texture.html",
|
||||
"type": "hyperframes:snippet"
|
||||
},
|
||||
{
|
||||
"path": "lava.png",
|
||||
"target": "compositions/components/lava.png",
|
||||
"type": "hyperframes:asset"
|
||||
},
|
||||
{
|
||||
"path": "marble-012.png",
|
||||
"target": "compositions/components/marble-012.png",
|
||||
"type": "hyperframes:asset"
|
||||
},
|
||||
{
|
||||
"path": "metal-046-b.png",
|
||||
"target": "compositions/components/metal-046-b.png",
|
||||
"type": "hyperframes:asset"
|
||||
},
|
||||
{
|
||||
"path": "wood-066.png",
|
||||
"target": "compositions/components/wood-066.png",
|
||||
"type": "hyperframes:asset"
|
||||
},
|
||||
{
|
||||
"path": "concrete-042-a.png",
|
||||
"target": "compositions/components/concrete-042-a.png",
|
||||
"type": "hyperframes:asset"
|
||||
},
|
||||
{
|
||||
"path": "rock-058.png",
|
||||
"target": "compositions/components/rock-058.png",
|
||||
"type": "hyperframes:asset"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-texture-lava/preview.mp4?v=1779051416",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-texture.png"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 82 KiB |
@@ -0,0 +1,416 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Weight Shift</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=Montserrat:wght@300;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
font-family: "Montserrat", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#weight-shift {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#avatar-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.safe-zone {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1400px;
|
||||
height: 278px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1400px;
|
||||
height: 278px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transform-origin: 50% 50%;
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.caption-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
width: max-content;
|
||||
max-width: 1400px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
color: #ffffff;
|
||||
font-family: "Montserrat", Arial, sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 72px;
|
||||
letter-spacing: -0.03em;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="weight-shift"
|
||||
data-composition-id="caption-weight-shift"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="avatar-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var SAFE_ZONE_WIDTH = 1400;
|
||||
var MAX_LINE_WIDTH = SAFE_ZONE_WIDTH - 40;
|
||||
var CAPTION_FONT_SIZE = 72;
|
||||
var MAX_WORDS_PER_GROUP = 4;
|
||||
var WORD_SPACING = 12;
|
||||
var PAUSE_THRESHOLD = 0.1;
|
||||
var ENTRY_DURATION = 100 / 1000;
|
||||
var WEIGHT_TRANSITION = 100 / 1000;
|
||||
var FONT_WIDTH_SAFETY = 1.14;
|
||||
var FONT_WEIGHT_LIGHT = 300;
|
||||
var FONT_WEIGHT_BOLD = 700;
|
||||
|
||||
var TRANSCRIPT = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
function normalizeWords(rawWords) {
|
||||
return rawWords
|
||||
.map(function (item) {
|
||||
return {
|
||||
text: String(item.word || item.text || "").trim(),
|
||||
start: Math.max(0, Number(item.start) || 0),
|
||||
end: Math.min(DURATION, Math.max(Number(item.start) || 0, Number(item.end) || 0)),
|
||||
};
|
||||
})
|
||||
.filter(function (word) {
|
||||
return word.text.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var measureCanvas = document.createElement("canvas");
|
||||
var measureContext = measureCanvas.getContext("2d");
|
||||
measureContext.font = "700 " + CAPTION_FONT_SIZE + "px Montserrat";
|
||||
|
||||
function measureWord(word) {
|
||||
return measureContext.measureText(word.text.toLowerCase()).width * FONT_WIDTH_SAFETY;
|
||||
}
|
||||
|
||||
function measureLineWidth(words) {
|
||||
return words.reduce(function (total, word, index) {
|
||||
return total + measureWord(word) + (index ? WORD_SPACING : 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function fitsInTwoLines(words) {
|
||||
return measureLineWidth(words) <= MAX_LINE_WIDTH * 1.8;
|
||||
}
|
||||
|
||||
function splitLines(words) {
|
||||
if (words.length < 2) {
|
||||
return [{ words: words, startIndex: 0 }];
|
||||
}
|
||||
var bestSplit = Math.ceil(words.length / 2);
|
||||
var bestDiff = Infinity;
|
||||
for (var s = 1; s < words.length; s++) {
|
||||
var w1 = measureLineWidth(words.slice(0, s));
|
||||
var w2 = measureLineWidth(words.slice(s));
|
||||
if (w1 <= MAX_LINE_WIDTH && w2 <= MAX_LINE_WIDTH) {
|
||||
var diff = Math.abs(w1 - w2);
|
||||
if (diff < bestDiff) {
|
||||
bestDiff = diff;
|
||||
bestSplit = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = [];
|
||||
var line1 = words.slice(0, bestSplit);
|
||||
var line2 = words.slice(bestSplit);
|
||||
if (line1.length) result.push({ words: line1, startIndex: 0 });
|
||||
if (line2.length) result.push({ words: line2, startIndex: line1.length });
|
||||
return result;
|
||||
}
|
||||
|
||||
function avoidSingleWordGroups(groups) {
|
||||
var out = [];
|
||||
groups.forEach(function (group) {
|
||||
if (
|
||||
group.words.length === 1 &&
|
||||
out.length > 0 &&
|
||||
out[out.length - 1].words.length < MAX_WORDS_PER_GROUP
|
||||
) {
|
||||
out[out.length - 1] = makeGroup(out[out.length - 1].words.concat(group.words));
|
||||
} else {
|
||||
out.push(group);
|
||||
}
|
||||
});
|
||||
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
if (out[i].words.length !== 1) continue;
|
||||
if (i + 1 < out.length && out[i + 1].words.length < MAX_WORDS_PER_GROUP) {
|
||||
out[i] = makeGroup(out[i].words.concat(out[i + 1].words));
|
||||
out.splice(i + 1, 1);
|
||||
} else if (i > 0 && out[i - 1].words.length < MAX_WORDS_PER_GROUP) {
|
||||
out[i - 1] = makeGroup(out[i - 1].words.concat(out[i].words));
|
||||
out.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function makeGroups(words) {
|
||||
var groups = [];
|
||||
var current = [];
|
||||
words.forEach(function (word, index) {
|
||||
var nextWord = words[index + 1];
|
||||
var testGroup = current.concat([word]);
|
||||
if (testGroup.length > MAX_WORDS_PER_GROUP || !fitsInTwoLines(testGroup)) {
|
||||
if (current.length) groups.push(makeGroup(current));
|
||||
current = [word];
|
||||
return;
|
||||
}
|
||||
current.push(word);
|
||||
var punctuation = /[,.:!?]$/.test(word.text);
|
||||
var pause = nextWord ? nextWord.start - word.end : 0;
|
||||
if (
|
||||
!nextWord ||
|
||||
punctuation ||
|
||||
pause >= PAUSE_THRESHOLD ||
|
||||
current.length >= MAX_WORDS_PER_GROUP
|
||||
) {
|
||||
groups.push(makeGroup(current));
|
||||
current = [];
|
||||
}
|
||||
});
|
||||
if (current.length) groups.push(makeGroup(current));
|
||||
return avoidSingleWordGroups(groups);
|
||||
}
|
||||
|
||||
function makeGroup(words) {
|
||||
return {
|
||||
words: words.slice(),
|
||||
lines: splitLines(words),
|
||||
start: words[0].start,
|
||||
end: words[words.length - 1].end,
|
||||
};
|
||||
}
|
||||
|
||||
function buildCaptions(groups) {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
groups.forEach(function (group, groupIndex) {
|
||||
var groupText = group.words
|
||||
.map(function (w) {
|
||||
return w.text.toLowerCase();
|
||||
})
|
||||
.join(" ");
|
||||
var computedSize = fitFontSize(
|
||||
groupText,
|
||||
CAPTION_FONT_SIZE,
|
||||
"700",
|
||||
"Montserrat",
|
||||
MAX_LINE_WIDTH,
|
||||
);
|
||||
var groupEl = document.createElement("div");
|
||||
var copyEl = document.createElement("div");
|
||||
groupEl.id = "caption-group-" + groupIndex;
|
||||
groupEl.className = "caption-group";
|
||||
copyEl.className = "caption-copy";
|
||||
group.lines.forEach(function (line, lineIndex) {
|
||||
var lineEl = document.createElement("div");
|
||||
lineEl.className = "caption-line";
|
||||
lineEl.id = "caption-group-" + groupIndex + "-line-" + lineIndex;
|
||||
lineEl.style.fontWeight = lineIndex === 0 ? FONT_WEIGHT_BOLD : FONT_WEIGHT_LIGHT;
|
||||
lineEl.style.fontSize = computedSize + "px";
|
||||
line.words.forEach(function (word, wordIndex) {
|
||||
var wordEl = document.createElement("span");
|
||||
wordEl.textContent = word.text.toLowerCase();
|
||||
wordEl.style.display = "inline-block";
|
||||
lineEl.appendChild(wordEl);
|
||||
});
|
||||
copyEl.appendChild(lineEl);
|
||||
});
|
||||
groupEl.appendChild(copyEl);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
}
|
||||
|
||||
var WORDS = normalizeWords(TRANSCRIPT);
|
||||
var GROUPS = makeGroups(WORDS);
|
||||
buildCaptions(GROUPS);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var visibleStarts = GROUPS.map(function (group, groupIndex) {
|
||||
var prev = GROUPS[groupIndex - 1];
|
||||
var availableGap = prev ? Math.max(0, group.start - prev.end) : ENTRY_DURATION;
|
||||
var entryLead = Math.min(ENTRY_DURATION, availableGap);
|
||||
return Math.max(0, group.start - entryLead);
|
||||
});
|
||||
|
||||
var allGroupEls = GROUPS.map(function (_, i) {
|
||||
return document.getElementById("caption-group-" + i);
|
||||
});
|
||||
|
||||
GROUPS.forEach(function (group, groupIndex) {
|
||||
var groupEl = allGroupEls[groupIndex];
|
||||
var visibleStart = visibleStarts[groupIndex];
|
||||
var visibleEnd = groupIndex < GROUPS.length - 1 ? visibleStarts[groupIndex + 1] : DURATION;
|
||||
|
||||
allGroupEls.forEach(function (otherEl, otherIndex) {
|
||||
if (otherIndex !== groupIndex) tl.set(otherEl, { opacity: 0 }, visibleStart);
|
||||
});
|
||||
|
||||
tl.set(groupEl, { opacity: 0, scale: 0.85 }, visibleStart);
|
||||
tl.to(
|
||||
groupEl,
|
||||
{ opacity: 1, scale: 1, duration: ENTRY_DURATION, ease: "power3.out", force3D: true },
|
||||
visibleStart,
|
||||
);
|
||||
|
||||
if (group.lines.length === 2) {
|
||||
var line1El = document.getElementById("caption-group-" + groupIndex + "-line-0");
|
||||
var line2El = document.getElementById("caption-group-" + groupIndex + "-line-1");
|
||||
var line2FirstWord = group.lines[1].words[0];
|
||||
var switchTime = line2FirstWord.start;
|
||||
|
||||
tl.to(
|
||||
line1El,
|
||||
{ fontWeight: FONT_WEIGHT_LIGHT, duration: WEIGHT_TRANSITION, ease: "power2.out" },
|
||||
switchTime,
|
||||
);
|
||||
tl.to(
|
||||
line2El,
|
||||
{ fontWeight: FONT_WEIGHT_BOLD, duration: WEIGHT_TRANSITION, ease: "power2.out" },
|
||||
switchTime,
|
||||
);
|
||||
}
|
||||
|
||||
tl.set(groupEl, { opacity: 0 }, visibleEnd);
|
||||
});
|
||||
|
||||
window.__timelines["caption-weight-shift"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,414 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Weight Shift</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=Montserrat:wght@300;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
font-family: "Montserrat", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#weight-shift {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
#avatar-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.safe-zone {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1400px;
|
||||
height: 278px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1400px;
|
||||
height: 278px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transform-origin: 50% 50%;
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.caption-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
width: max-content;
|
||||
max-width: 1400px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
color: #ffffff;
|
||||
font-family: "Montserrat", Arial, sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 72px;
|
||||
letter-spacing: -0.03em;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="weight-shift"
|
||||
data-composition-id="caption-weight-shift"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-fps="30"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video
|
||||
id="avatar-video"
|
||||
muted
|
||||
playsinline
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
data-has-audio="false"
|
||||
></video>
|
||||
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="safe-zone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var DURATION = 8;
|
||||
var SAFE_ZONE_WIDTH = 1400;
|
||||
var MAX_LINE_WIDTH = SAFE_ZONE_WIDTH - 40;
|
||||
var CAPTION_FONT_SIZE = 72;
|
||||
var MAX_WORDS_PER_GROUP = 4;
|
||||
var WORD_SPACING = 12;
|
||||
var PAUSE_THRESHOLD = 0.1;
|
||||
var ENTRY_DURATION = 100 / 1000;
|
||||
var WEIGHT_TRANSITION = 100 / 1000;
|
||||
var FONT_WIDTH_SAFETY = 1.14;
|
||||
var FONT_WEIGHT_LIGHT = 300;
|
||||
var FONT_WEIGHT_BOLD = 700;
|
||||
|
||||
var TRANSCRIPT = [
|
||||
{ text: "Every", start: 0.0, end: 0.3 },
|
||||
{ text: "great", start: 0.3, end: 0.55 },
|
||||
{ text: "video", start: 0.55, end: 0.85 },
|
||||
{ text: "starts", start: 0.85, end: 1.1 },
|
||||
{ text: "with", start: 1.1, end: 1.25 },
|
||||
{ text: "a", start: 1.25, end: 1.35 },
|
||||
{ text: "single", start: 1.35, end: 1.65 },
|
||||
{ text: "frame.", start: 1.65, end: 2.0 },
|
||||
{ text: "HyperFrames", start: 2.1, end: 2.65 },
|
||||
{ text: "lets", start: 2.65, end: 2.85 },
|
||||
{ text: "you", start: 2.85, end: 2.95 },
|
||||
{ text: "write", start: 2.95, end: 3.15 },
|
||||
{ text: "HTML", start: 3.15, end: 3.55 },
|
||||
{ text: "and", start: 3.55, end: 3.65 },
|
||||
{ text: "render", start: 3.65, end: 3.95 },
|
||||
{ text: "professional", start: 3.95, end: 4.45 },
|
||||
{ text: "video.", start: 4.45, end: 4.8 },
|
||||
{ text: "No", start: 4.9, end: 5.05 },
|
||||
{ text: "timeline.", start: 5.05, end: 5.45 },
|
||||
{ text: "No", start: 5.5, end: 5.65 },
|
||||
{ text: "drag", start: 5.65, end: 5.85 },
|
||||
{ text: "and", start: 5.85, end: 5.95 },
|
||||
{ text: "drop.", start: 5.95, end: 6.25 },
|
||||
{ text: "Just", start: 6.35, end: 6.55 },
|
||||
{ text: "code", start: 6.55, end: 6.8 },
|
||||
{ text: "that", start: 6.8, end: 6.95 },
|
||||
{ text: "becomes", start: 6.95, end: 7.3 },
|
||||
{ text: "cinema.", start: 7.3, end: 7.7 },
|
||||
];
|
||||
|
||||
function normalizeWords(rawWords) {
|
||||
return rawWords
|
||||
.map(function (item) {
|
||||
return {
|
||||
text: String(item.word || item.text || "").trim(),
|
||||
start: Math.max(0, Number(item.start) || 0),
|
||||
end: Math.min(DURATION, Math.max(Number(item.start) || 0, Number(item.end) || 0)),
|
||||
};
|
||||
})
|
||||
.filter(function (word) {
|
||||
return word.text.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
var _fitCanvas = document.createElement("canvas");
|
||||
var _fitCtx = _fitCanvas.getContext("2d");
|
||||
function fitFontSize(text, baseFontSize, fontWeight, fontFamily, maxWidth) {
|
||||
var size = baseFontSize;
|
||||
var minSize = Math.floor(baseFontSize * 0.45);
|
||||
while (size > minSize) {
|
||||
_fitCtx.font = fontWeight + " " + size + "px " + fontFamily;
|
||||
if (_fitCtx.measureText(text).width <= maxWidth) return size;
|
||||
size -= 2;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
var measureCanvas = document.createElement("canvas");
|
||||
var measureContext = measureCanvas.getContext("2d");
|
||||
measureContext.font = "700 " + CAPTION_FONT_SIZE + "px Montserrat";
|
||||
|
||||
function measureWord(word) {
|
||||
return measureContext.measureText(word.text.toLowerCase()).width * FONT_WIDTH_SAFETY;
|
||||
}
|
||||
|
||||
function measureLineWidth(words) {
|
||||
return words.reduce(function (total, word, index) {
|
||||
return total + measureWord(word) + (index ? WORD_SPACING : 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function fitsInTwoLines(words) {
|
||||
return measureLineWidth(words) <= MAX_LINE_WIDTH * 1.8;
|
||||
}
|
||||
|
||||
function splitLines(words) {
|
||||
if (words.length < 2) {
|
||||
return [{ words: words, startIndex: 0 }];
|
||||
}
|
||||
var bestSplit = Math.ceil(words.length / 2);
|
||||
var bestDiff = Infinity;
|
||||
for (var s = 1; s < words.length; s++) {
|
||||
var w1 = measureLineWidth(words.slice(0, s));
|
||||
var w2 = measureLineWidth(words.slice(s));
|
||||
if (w1 <= MAX_LINE_WIDTH && w2 <= MAX_LINE_WIDTH) {
|
||||
var diff = Math.abs(w1 - w2);
|
||||
if (diff < bestDiff) {
|
||||
bestDiff = diff;
|
||||
bestSplit = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = [];
|
||||
var line1 = words.slice(0, bestSplit);
|
||||
var line2 = words.slice(bestSplit);
|
||||
if (line1.length) result.push({ words: line1, startIndex: 0 });
|
||||
if (line2.length) result.push({ words: line2, startIndex: line1.length });
|
||||
return result;
|
||||
}
|
||||
|
||||
function avoidSingleWordGroups(groups) {
|
||||
var out = [];
|
||||
groups.forEach(function (group) {
|
||||
if (
|
||||
group.words.length === 1 &&
|
||||
out.length > 0 &&
|
||||
out[out.length - 1].words.length < MAX_WORDS_PER_GROUP
|
||||
) {
|
||||
out[out.length - 1] = makeGroup(out[out.length - 1].words.concat(group.words));
|
||||
} else {
|
||||
out.push(group);
|
||||
}
|
||||
});
|
||||
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
if (out[i].words.length !== 1) continue;
|
||||
if (i + 1 < out.length && out[i + 1].words.length < MAX_WORDS_PER_GROUP) {
|
||||
out[i] = makeGroup(out[i].words.concat(out[i + 1].words));
|
||||
out.splice(i + 1, 1);
|
||||
} else if (i > 0 && out[i - 1].words.length < MAX_WORDS_PER_GROUP) {
|
||||
out[i - 1] = makeGroup(out[i - 1].words.concat(out[i].words));
|
||||
out.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function makeGroups(words) {
|
||||
var groups = [];
|
||||
var current = [];
|
||||
words.forEach(function (word, index) {
|
||||
var nextWord = words[index + 1];
|
||||
var testGroup = current.concat([word]);
|
||||
if (testGroup.length > MAX_WORDS_PER_GROUP || !fitsInTwoLines(testGroup)) {
|
||||
if (current.length) groups.push(makeGroup(current));
|
||||
current = [word];
|
||||
return;
|
||||
}
|
||||
current.push(word);
|
||||
var punctuation = /[,.:!?]$/.test(word.text);
|
||||
var pause = nextWord ? nextWord.start - word.end : 0;
|
||||
if (
|
||||
!nextWord ||
|
||||
punctuation ||
|
||||
pause >= PAUSE_THRESHOLD ||
|
||||
current.length >= MAX_WORDS_PER_GROUP
|
||||
) {
|
||||
groups.push(makeGroup(current));
|
||||
current = [];
|
||||
}
|
||||
});
|
||||
if (current.length) groups.push(makeGroup(current));
|
||||
return avoidSingleWordGroups(groups);
|
||||
}
|
||||
|
||||
function makeGroup(words) {
|
||||
return {
|
||||
words: words.slice(),
|
||||
lines: splitLines(words),
|
||||
start: words[0].start,
|
||||
end: words[words.length - 1].end,
|
||||
};
|
||||
}
|
||||
|
||||
function buildCaptions(groups) {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
groups.forEach(function (group, groupIndex) {
|
||||
var groupText = group.words
|
||||
.map(function (w) {
|
||||
return w.text.toLowerCase();
|
||||
})
|
||||
.join(" ");
|
||||
var computedSize = fitFontSize(
|
||||
groupText,
|
||||
CAPTION_FONT_SIZE,
|
||||
"700",
|
||||
"Montserrat",
|
||||
MAX_LINE_WIDTH,
|
||||
);
|
||||
var groupEl = document.createElement("div");
|
||||
var copyEl = document.createElement("div");
|
||||
groupEl.id = "caption-group-" + groupIndex;
|
||||
groupEl.className = "caption-group";
|
||||
copyEl.className = "caption-copy";
|
||||
group.lines.forEach(function (line, lineIndex) {
|
||||
var lineEl = document.createElement("div");
|
||||
lineEl.className = "caption-line";
|
||||
lineEl.id = "caption-group-" + groupIndex + "-line-" + lineIndex;
|
||||
lineEl.style.fontWeight = lineIndex === 0 ? FONT_WEIGHT_BOLD : FONT_WEIGHT_LIGHT;
|
||||
lineEl.style.fontSize = computedSize + "px";
|
||||
line.words.forEach(function (word, wordIndex) {
|
||||
var wordEl = document.createElement("span");
|
||||
wordEl.textContent = word.text.toLowerCase();
|
||||
wordEl.style.display = "inline-block";
|
||||
lineEl.appendChild(wordEl);
|
||||
});
|
||||
copyEl.appendChild(lineEl);
|
||||
});
|
||||
groupEl.appendChild(copyEl);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
}
|
||||
|
||||
var WORDS = normalizeWords(TRANSCRIPT);
|
||||
var GROUPS = makeGroups(WORDS);
|
||||
buildCaptions(GROUPS);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var visibleStarts = GROUPS.map(function (group, groupIndex) {
|
||||
var prev = GROUPS[groupIndex - 1];
|
||||
var availableGap = prev ? Math.max(0, group.start - prev.end) : ENTRY_DURATION;
|
||||
var entryLead = Math.min(ENTRY_DURATION, availableGap);
|
||||
return Math.max(0, group.start - entryLead);
|
||||
});
|
||||
|
||||
var allGroupEls = GROUPS.map(function (_, i) {
|
||||
return document.getElementById("caption-group-" + i);
|
||||
});
|
||||
|
||||
GROUPS.forEach(function (group, groupIndex) {
|
||||
var groupEl = allGroupEls[groupIndex];
|
||||
var visibleStart = visibleStarts[groupIndex];
|
||||
var visibleEnd = groupIndex < GROUPS.length - 1 ? visibleStarts[groupIndex + 1] : DURATION;
|
||||
|
||||
allGroupEls.forEach(function (otherEl, otherIndex) {
|
||||
if (otherIndex !== groupIndex) tl.set(otherEl, { opacity: 0 }, visibleStart);
|
||||
});
|
||||
|
||||
tl.set(groupEl, { opacity: 0, scale: 0.85 }, visibleStart);
|
||||
tl.to(
|
||||
groupEl,
|
||||
{ opacity: 1, scale: 1, duration: ENTRY_DURATION, ease: "power3.out", force3D: true },
|
||||
visibleStart,
|
||||
);
|
||||
|
||||
if (group.lines.length === 2) {
|
||||
var line1El = document.getElementById("caption-group-" + groupIndex + "-line-0");
|
||||
var line2El = document.getElementById("caption-group-" + groupIndex + "-line-1");
|
||||
var line2FirstWord = group.lines[1].words[0];
|
||||
var switchTime = line2FirstWord.start;
|
||||
|
||||
tl.to(
|
||||
line1El,
|
||||
{ fontWeight: FONT_WEIGHT_LIGHT, duration: WEIGHT_TRANSITION, ease: "power2.out" },
|
||||
switchTime,
|
||||
);
|
||||
tl.to(
|
||||
line2El,
|
||||
{ fontWeight: FONT_WEIGHT_BOLD, duration: WEIGHT_TRANSITION, ease: "power2.out" },
|
||||
switchTime,
|
||||
);
|
||||
}
|
||||
|
||||
tl.set(groupEl, { opacity: 0 }, visibleEnd);
|
||||
});
|
||||
|
||||
window.__timelines["caption-weight-shift"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "caption-weight-shift",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Weight Shift",
|
||||
"description": "Elegant font-weight transition between caption lines",
|
||||
"tags": ["captions", "caption-style", "minimal", "typography"],
|
||||
"files": [
|
||||
{
|
||||
"path": "caption-weight-shift.html",
|
||||
"target": "compositions/components/caption-weight-shift.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/registry/components/caption-weight-shift/preview-v2.mp4",
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/caption-weight-shift.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Grain Overlay — Demo</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="demo"
|
||||
data-composition-id="grain-overlay-demo"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="5"
|
||||
>
|
||||
<!-- Background content to demonstrate the overlay -->
|
||||
<div class="demo-bg">
|
||||
<div style="text-align: center; z-index: 1">
|
||||
<div
|
||||
class="demo-title"
|
||||
style="font-size: 80px; font-weight: 700; color: #2c1e16; opacity: 0"
|
||||
>
|
||||
Grain Overlay
|
||||
</div>
|
||||
<div
|
||||
class="demo-subtitle"
|
||||
style="font-size: 32px; color: #5a4a3a; margin-top: 20px; opacity: 0"
|
||||
>
|
||||
Adds warmth and analog character
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The grain overlay component -->
|
||||
<div
|
||||
id="grain-overlay"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
"
|
||||
>
|
||||
<div class="grain-texture"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.demo-bg {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: linear-gradient(135deg, #f5f0e0 0%, #e8d8b8 50%, #d4c4a0 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: "Inter", sans-serif;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes hf-grain-noise {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
10% {
|
||||
transform: translate(-5%, -5%);
|
||||
}
|
||||
20% {
|
||||
transform: translate(-10%, 5%);
|
||||
}
|
||||
30% {
|
||||
transform: translate(5%, -10%);
|
||||
}
|
||||
40% {
|
||||
transform: translate(-5%, 15%);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-10%, 5%);
|
||||
}
|
||||
60% {
|
||||
transform: translate(15%, 0);
|
||||
}
|
||||
70% {
|
||||
transform: translate(0, 10%);
|
||||
}
|
||||
80% {
|
||||
transform: translate(-15%, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate(10%, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
#grain-overlay .grain-texture {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||
opacity: 0.15;
|
||||
animation: hf-grain-noise 0.5s steps(1) infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.to(
|
||||
".demo-title",
|
||||
{
|
||||
opacity: 1,
|
||||
y: -10,
|
||||
duration: 1,
|
||||
ease: "power3.out",
|
||||
},
|
||||
0.5,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".demo-subtitle",
|
||||
{
|
||||
opacity: 1,
|
||||
y: -5,
|
||||
duration: 0.8,
|
||||
ease: "power3.out",
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["grain-overlay-demo"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
<!--
|
||||
Grain Overlay — animated film grain texture.
|
||||
|
||||
Usage: paste this snippet into your composition's HTML.
|
||||
The overlay covers the full viewport with pointer-events: none
|
||||
so it sits on top of all content without blocking interaction.
|
||||
|
||||
Customize:
|
||||
- opacity: adjust .grain-texture opacity (default 0.15)
|
||||
- speed: change animation duration (default 0.5s)
|
||||
- texture: swap the background URL for a different pattern
|
||||
- z-index: adjust #grain-overlay z-index to layer correctly
|
||||
-->
|
||||
|
||||
<div
|
||||
id="grain-overlay"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
"
|
||||
>
|
||||
<div class="grain-texture"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes hf-grain-noise {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
10% {
|
||||
transform: translate(-5%, -5%);
|
||||
}
|
||||
20% {
|
||||
transform: translate(-10%, 5%);
|
||||
}
|
||||
30% {
|
||||
transform: translate(5%, -10%);
|
||||
}
|
||||
40% {
|
||||
transform: translate(-5%, 15%);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-10%, 5%);
|
||||
}
|
||||
60% {
|
||||
transform: translate(15%, 0);
|
||||
}
|
||||
70% {
|
||||
transform: translate(0, 10%);
|
||||
}
|
||||
80% {
|
||||
transform: translate(-15%, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate(10%, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
#grain-overlay .grain-texture {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||
opacity: 0.15;
|
||||
animation: hf-grain-noise 0.5s steps(1) infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "grain-overlay",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Grain Overlay",
|
||||
"description": "Animated film grain texture overlay using CSS keyframes — adds warmth and analog character to any composition",
|
||||
"tags": ["texture", "grain", "overlay", "film"],
|
||||
"files": [
|
||||
{
|
||||
"path": "grain-overlay.html",
|
||||
"target": "compositions/components/grain-overlay.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/grain-overlay.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Grid Pixelate Wipe — Demo</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="demo"
|
||||
data-composition-id="grid-pixelate-wipe-demo"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="8"
|
||||
>
|
||||
<!-- Scene A -->
|
||||
<div id="scene-a" class="demo-scene scene-a">
|
||||
<div style="text-align: center; color: white">
|
||||
<div style="font-size: 80px; font-weight: 800">Scene A</div>
|
||||
<div style="font-size: 32px; margin-top: 20px; opacity: 0.7">
|
||||
The grid wipe dissolves this away
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scene B (hidden initially) -->
|
||||
<div id="scene-b" class="demo-scene scene-b">
|
||||
<div style="text-align: center; color: white">
|
||||
<div style="font-size: 80px; font-weight: 800">Scene B</div>
|
||||
<div style="font-size: 32px; margin-top: 20px; opacity: 0.7">Revealed from the grid</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid Pixelate Wipe overlay -->
|
||||
<div
|
||||
id="grid-pixelate-overlay"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 999;
|
||||
display: grid;
|
||||
"
|
||||
></div>
|
||||
|
||||
<style>
|
||||
#grid-pixelate-overlay {
|
||||
--grid-color: black;
|
||||
}
|
||||
|
||||
#grid-pixelate-overlay .grid-cell {
|
||||
background: var(--grid-color);
|
||||
transform: scale(0);
|
||||
transform-origin: center center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.demo-scene {
|
||||
position: absolute;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
.scene-a {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.scene-b {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const COLS = 16;
|
||||
const ROWS = 9;
|
||||
const overlay = document.getElementById("grid-pixelate-overlay");
|
||||
|
||||
overlay.style.gridTemplateColumns = `repeat(${COLS}, 1fr)`;
|
||||
overlay.style.gridTemplateRows = `repeat(${ROWS}, 1fr)`;
|
||||
|
||||
for (let i = 0; i < COLS * ROWS; i++) {
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "grid-cell";
|
||||
overlay.appendChild(cell);
|
||||
}
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Scene A visible for 2 seconds
|
||||
// Grid covers screen (transition out of A)
|
||||
tl.to(
|
||||
".grid-cell",
|
||||
{
|
||||
scale: 1,
|
||||
duration: 0.6,
|
||||
stagger: { amount: 0.6, from: "center" },
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
2.0,
|
||||
);
|
||||
|
||||
// Swap scenes at midpoint
|
||||
tl.set("#scene-a", { opacity: 0 }, 3.0);
|
||||
tl.set("#scene-b", { opacity: 1 }, 3.0);
|
||||
|
||||
// Grid reveals scene B
|
||||
tl.to(
|
||||
".grid-cell",
|
||||
{
|
||||
scale: 0,
|
||||
duration: 0.6,
|
||||
stagger: { amount: 0.6, from: "edges" },
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
3.0,
|
||||
);
|
||||
|
||||
// Second transition: back to A using random pattern
|
||||
tl.to(
|
||||
".grid-cell",
|
||||
{
|
||||
scale: 1,
|
||||
duration: 0.5,
|
||||
stagger: { amount: 0.5, from: "random" },
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
5.5,
|
||||
);
|
||||
|
||||
tl.set("#scene-b", { opacity: 0 }, 6.3);
|
||||
tl.set("#scene-a", { opacity: 1 }, 6.3);
|
||||
|
||||
tl.to(
|
||||
".grid-cell",
|
||||
{
|
||||
scale: 0,
|
||||
duration: 0.5,
|
||||
stagger: { amount: 0.5, from: "random" },
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
6.3,
|
||||
);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["grid-pixelate-wipe-demo"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
<!--
|
||||
Grid Pixelate Wipe — transition effect.
|
||||
|
||||
Usage: place this overlay in your composition. To transition between
|
||||
scenes, animate .grid-cell scale from 0→1 (cover) then 1→0 (reveal)
|
||||
using GSAP stagger with a "from" pattern.
|
||||
|
||||
Customize:
|
||||
- COLS / ROWS: grid density (default 16×9), edit in the script below
|
||||
- --grid-color: cell fill color (default black)
|
||||
- Stagger "from": "center" for radial, "edges" for inward, "random" for scatter
|
||||
-->
|
||||
|
||||
<div
|
||||
id="grid-pixelate-overlay"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 999;
|
||||
display: grid;
|
||||
"
|
||||
></div>
|
||||
|
||||
<style>
|
||||
#grid-pixelate-overlay {
|
||||
--grid-color: black;
|
||||
}
|
||||
|
||||
#grid-pixelate-overlay .grid-cell {
|
||||
background: var(--grid-color);
|
||||
transform: scale(0);
|
||||
transform-origin: center center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const COLS = 16;
|
||||
const ROWS = 9;
|
||||
const overlay = document.getElementById("grid-pixelate-overlay");
|
||||
if (!overlay) return;
|
||||
|
||||
overlay.style.gridTemplateColumns = `repeat(${COLS}, 1fr)`;
|
||||
overlay.style.gridTemplateRows = `repeat(${ROWS}, 1fr)`;
|
||||
|
||||
for (let i = 0; i < COLS * ROWS; i++) {
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "grid-cell";
|
||||
overlay.appendChild(cell);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Timeline integration example:
|
||||
|
||||
// Cover screen with grid (transition out of scene A)
|
||||
tl.to("#grid-pixelate-overlay .grid-cell", {
|
||||
scale: 1,
|
||||
duration: 0.6,
|
||||
stagger: { amount: 0.6, from: "center" },
|
||||
ease: "power2.inOut",
|
||||
}, 3.0);
|
||||
|
||||
// Swap scene content at midpoint
|
||||
tl.set("#scene-a", { opacity: 0 }, 3.6);
|
||||
tl.set("#scene-b", { opacity: 1 }, 3.6);
|
||||
|
||||
// Reveal scene B by removing grid
|
||||
tl.to("#grid-pixelate-overlay .grid-cell", {
|
||||
scale: 0,
|
||||
duration: 0.6,
|
||||
stagger: { amount: 0.6, from: "edges" },
|
||||
ease: "power2.inOut",
|
||||
}, 3.6);
|
||||
-->
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "grid-pixelate-wipe",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Grid Pixelate Wipe",
|
||||
"description": "Transition effect where the screen dissolves into a grid of squares that fade out with staggered timing — use between scenes",
|
||||
"tags": ["transition", "wipe", "grid", "pixelate"],
|
||||
"files": [
|
||||
{
|
||||
"path": "grid-pixelate-wipe.html",
|
||||
"target": "compositions/components/grid-pixelate-wipe.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/grid-pixelate-wipe.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
<!-- demo.html — used by scripts/generate-catalog-previews.ts to render the
|
||||
catalog preview video. Not installed by `hyperframes add morph-text`.
|
||||
Keep in sync with morph-text.html. -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Morph Text</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=Figtree:wght@900&display=block"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#morph-text {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
|
||||
/* Word list: hidden source of truth — edit <li> items in Studio. */
|
||||
#morph-words {
|
||||
display: none;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#morph-filter {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#morph-wrapper {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
width: 1920px;
|
||||
height: 210px;
|
||||
filter: drop-shadow(0 10px 44px rgba(0, 0, 0, 0.5))
|
||||
drop-shadow(0 3px 10px rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
#morph-display {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
#morph-a,
|
||||
#morph-b {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-size: 120pt;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="morph-text"
|
||||
data-composition-id="morph-text"
|
||||
data-timeline-locked
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="15"
|
||||
data-fps="30"
|
||||
data-morph-speed="1"
|
||||
data-morph-pause="1.5"
|
||||
>
|
||||
<!--
|
||||
Edit words here — one <li> per word or phrase.
|
||||
data-font : CSS font-family (e.g. "'Figtree', sans-serif")
|
||||
data-color : text color hex (e.g. "#000000")
|
||||
Adjust data-morph-speed (seconds per morph) and data-morph-pause (hold time) above.
|
||||
-->
|
||||
<ol id="morph-words" aria-hidden="true">
|
||||
<li data-font="'Figtree', sans-serif" data-color="#000000">Do more with less.</li>
|
||||
<li data-font="'Figtree', sans-serif" data-color="#000000">Built for what's next.</li>
|
||||
<li data-font="'Figtree', sans-serif" data-color="#000000">Fast. Focused. Powerful.</li>
|
||||
<li data-font="'Figtree', sans-serif" data-color="#000000">Your work, amplified.</li>
|
||||
<li data-font="'Figtree', sans-serif" data-color="#000000">Start free today.</li>
|
||||
</ol>
|
||||
|
||||
<svg id="morph-filter" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<filter id="morph-threshold">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="1 0 0 0 0
|
||||
0 1 0 0 0
|
||||
0 0 1 0 0
|
||||
0 0 0 255 -100"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div id="morph-wrapper">
|
||||
<div id="morph-display">
|
||||
<span id="morph-a"></span>
|
||||
<span id="morph-b"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var container = document.getElementById("morph-text");
|
||||
var display = document.getElementById("morph-display");
|
||||
var elA = document.getElementById("morph-a");
|
||||
var elB = document.getElementById("morph-b");
|
||||
|
||||
var words = Array.from(document.querySelectorAll("#morph-words li"))
|
||||
.map(function (el) {
|
||||
return {
|
||||
text: el.textContent.trim(),
|
||||
font: el.dataset.font || "'Figtree', sans-serif",
|
||||
color: el.dataset.color || "#111111",
|
||||
};
|
||||
})
|
||||
.filter(function (w) {
|
||||
return w.text;
|
||||
});
|
||||
|
||||
if (words.length < 2) {
|
||||
words = [
|
||||
{ text: "Morph", font: "'Figtree', sans-serif", color: "#111111" },
|
||||
{ text: "Text", font: "'Figtree', sans-serif", color: "#111111" },
|
||||
];
|
||||
}
|
||||
|
||||
var morphSpeed = parseFloat(container.dataset.morphSpeed || "1");
|
||||
var morphPause = parseFloat(container.dataset.morphPause || "1.5");
|
||||
var duration = parseFloat(container.dataset.duration || "9");
|
||||
var stepDur = morphSpeed + morphPause;
|
||||
var totalWordDur = words.length * stepDur;
|
||||
|
||||
function applyMorph(T) {
|
||||
var t = T % totalWordDur;
|
||||
var stepIdx = Math.floor(t / stepDur);
|
||||
var stepT = t - stepIdx * stepDur;
|
||||
var frac = Math.min(stepT / morphSpeed, 1);
|
||||
|
||||
var wA = words[stepIdx % words.length];
|
||||
var wB = words[(stepIdx + 1) % words.length];
|
||||
|
||||
elA.textContent = wA.text;
|
||||
elA.style.fontFamily = wA.font;
|
||||
elA.style.color = wA.color;
|
||||
|
||||
elB.textContent = wB.text;
|
||||
elB.style.fontFamily = wB.font;
|
||||
elB.style.color = wB.color;
|
||||
|
||||
if (frac <= 0) {
|
||||
display.style.filter = "none";
|
||||
elA.style.filter = "";
|
||||
elA.style.opacity = "1";
|
||||
elB.style.filter = "";
|
||||
elB.style.opacity = "0";
|
||||
} else if (frac >= 1) {
|
||||
display.style.filter = "none";
|
||||
elA.style.filter = "";
|
||||
elA.style.opacity = "0";
|
||||
elB.style.filter = "";
|
||||
elB.style.opacity = "1";
|
||||
} else {
|
||||
display.style.filter = "url(#morph-threshold)";
|
||||
var blurB = Math.max(0, Math.min(8 / frac - 8, 100));
|
||||
var blurA = Math.max(0, Math.min(8 / (1 - frac) - 8, 100));
|
||||
elB.style.filter = "blur(" + blurB + "px)";
|
||||
elB.style.opacity = frac;
|
||||
elA.style.filter = "blur(" + blurA + "px)";
|
||||
elA.style.opacity = 1 - frac;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
elA.textContent = words[0].text;
|
||||
elA.style.fontFamily = words[0].font;
|
||||
elA.style.color = words[0].color;
|
||||
elA.style.opacity = "1";
|
||||
display.style.filter = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
var proxy = { t: 0 };
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.to(proxy, {
|
||||
t: duration,
|
||||
duration: duration,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
applyMorph(proxy.t);
|
||||
},
|
||||
});
|
||||
|
||||
applyMorph(0);
|
||||
tl.seek(0);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["morph-text"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,234 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Morph Text</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=Figtree:wght@900&display=block"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#morph-text {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
|
||||
/* Word list: hidden source of truth — edit <li> items in Studio. */
|
||||
#morph-words {
|
||||
display: none;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#morph-filter {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#morph-wrapper {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
width: 1920px;
|
||||
height: 210px;
|
||||
filter: drop-shadow(0 10px 44px rgba(0, 0, 0, 0.5))
|
||||
drop-shadow(0 3px 10px rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
#morph-display {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
#morph-a,
|
||||
#morph-b {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-size: 120pt;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="morph-text"
|
||||
data-composition-id="morph-text"
|
||||
data-timeline-locked
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="15"
|
||||
data-fps="30"
|
||||
data-morph-speed="1"
|
||||
data-morph-pause="1.5"
|
||||
>
|
||||
<!--
|
||||
Edit words here — one <li> per word or phrase.
|
||||
data-font : CSS font-family (e.g. "'Figtree', sans-serif")
|
||||
data-color : text color hex (e.g. "#000000")
|
||||
Adjust data-morph-speed (seconds per morph) and data-morph-pause (hold time) above.
|
||||
-->
|
||||
<ol id="morph-words" aria-hidden="true">
|
||||
<li data-font="'Figtree', sans-serif" data-color="#000000">Do more with less.</li>
|
||||
<li data-font="'Figtree', sans-serif" data-color="#000000">Built for what's next.</li>
|
||||
<li data-font="'Figtree', sans-serif" data-color="#000000">Fast. Focused. Powerful.</li>
|
||||
<li data-font="'Figtree', sans-serif" data-color="#000000">Your work, amplified.</li>
|
||||
<li data-font="'Figtree', sans-serif" data-color="#000000">Start free today.</li>
|
||||
</ol>
|
||||
|
||||
<svg id="morph-filter" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<filter id="morph-threshold">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="1 0 0 0 0
|
||||
0 1 0 0 0
|
||||
0 0 1 0 0
|
||||
0 0 0 255 -100"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div id="morph-wrapper">
|
||||
<div id="morph-display">
|
||||
<span id="morph-a"></span>
|
||||
<span id="morph-b"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var container = document.getElementById("morph-text");
|
||||
var display = document.getElementById("morph-display");
|
||||
var elA = document.getElementById("morph-a");
|
||||
var elB = document.getElementById("morph-b");
|
||||
|
||||
var words = Array.from(document.querySelectorAll("#morph-words li"))
|
||||
.map(function (el) {
|
||||
return {
|
||||
text: el.textContent.trim(),
|
||||
font: el.dataset.font || "'Figtree', sans-serif",
|
||||
color: el.dataset.color || "#111111",
|
||||
};
|
||||
})
|
||||
.filter(function (w) {
|
||||
return w.text;
|
||||
});
|
||||
|
||||
if (words.length < 2) {
|
||||
words = [
|
||||
{ text: "Morph", font: "'Figtree', sans-serif", color: "#111111" },
|
||||
{ text: "Text", font: "'Figtree', sans-serif", color: "#111111" },
|
||||
];
|
||||
}
|
||||
|
||||
var morphSpeed = parseFloat(container.dataset.morphSpeed || "1");
|
||||
var morphPause = parseFloat(container.dataset.morphPause || "1.5");
|
||||
var duration = parseFloat(container.dataset.duration || "9");
|
||||
var stepDur = morphSpeed + morphPause;
|
||||
var totalWordDur = words.length * stepDur;
|
||||
|
||||
function applyMorph(T) {
|
||||
var t = T % totalWordDur;
|
||||
var stepIdx = Math.floor(t / stepDur);
|
||||
var stepT = t - stepIdx * stepDur;
|
||||
var frac = Math.min(stepT / morphSpeed, 1);
|
||||
|
||||
var wA = words[stepIdx % words.length];
|
||||
var wB = words[(stepIdx + 1) % words.length];
|
||||
|
||||
elA.textContent = wA.text;
|
||||
elA.style.fontFamily = wA.font;
|
||||
elA.style.color = wA.color;
|
||||
|
||||
elB.textContent = wB.text;
|
||||
elB.style.fontFamily = wB.font;
|
||||
elB.style.color = wB.color;
|
||||
|
||||
if (frac <= 0) {
|
||||
display.style.filter = "none";
|
||||
elA.style.filter = "";
|
||||
elA.style.opacity = "1";
|
||||
elB.style.filter = "";
|
||||
elB.style.opacity = "0";
|
||||
} else if (frac >= 1) {
|
||||
display.style.filter = "none";
|
||||
elA.style.filter = "";
|
||||
elA.style.opacity = "0";
|
||||
elB.style.filter = "";
|
||||
elB.style.opacity = "1";
|
||||
} else {
|
||||
display.style.filter = "url(#morph-threshold)";
|
||||
var blurB = Math.max(0, Math.min(8 / frac - 8, 100));
|
||||
var blurA = Math.max(0, Math.min(8 / (1 - frac) - 8, 100));
|
||||
elB.style.filter = "blur(" + blurB + "px)";
|
||||
elB.style.opacity = frac;
|
||||
elA.style.filter = "blur(" + blurA + "px)";
|
||||
elA.style.opacity = 1 - frac;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
elA.textContent = words[0].text;
|
||||
elA.style.fontFamily = words[0].font;
|
||||
elA.style.color = words[0].color;
|
||||
elA.style.opacity = "1";
|
||||
display.style.filter = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
var proxy = { t: 0 };
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.to(proxy, {
|
||||
t: duration,
|
||||
duration: duration,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
applyMorph(proxy.t);
|
||||
},
|
||||
});
|
||||
|
||||
applyMorph(0);
|
||||
tl.seek(0);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["morph-text"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "morph-text",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Morph Text",
|
||||
"description": "Gooey text morph — cycles through an editable word list using SVG threshold + GSAP-driven blur for a fluid, satisfying transition effect",
|
||||
"tags": ["text", "text-effect", "typography", "morph", "gooey", "kinetic", "animation"],
|
||||
"files": [
|
||||
{
|
||||
"path": "morph-text.html",
|
||||
"target": "compositions/components/morph-text.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"video": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/morph-text.mp4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Motion Blur — Demo</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #06060a;
|
||||
}
|
||||
|
||||
#demo {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
font-family: "SF Mono", ui-monospace, monospace;
|
||||
background: #06060a;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 0 72px 20px;
|
||||
border-bottom: 1px solid #0f0f18;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
color: #1e1e2a;
|
||||
}
|
||||
.header-sub {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.25em;
|
||||
text-transform: uppercase;
|
||||
color: #14141c;
|
||||
}
|
||||
|
||||
.group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 184px;
|
||||
}
|
||||
|
||||
.group::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 72px;
|
||||
right: 72px;
|
||||
height: 1px;
|
||||
background: #0d0d15;
|
||||
}
|
||||
|
||||
.dur {
|
||||
position: absolute;
|
||||
left: 72px;
|
||||
top: 0;
|
||||
width: 90px;
|
||||
height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.dur-num {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.04em;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
}
|
||||
.dur-unit {
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.25em;
|
||||
text-transform: uppercase;
|
||||
color: #252532;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.row-hint {
|
||||
position: absolute;
|
||||
left: 176px;
|
||||
width: 44px;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 8px;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: #161620;
|
||||
}
|
||||
|
||||
.spd {
|
||||
position: absolute;
|
||||
right: 72px;
|
||||
top: 0;
|
||||
width: 100px;
|
||||
height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.spd-label {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
color: #181822;
|
||||
}
|
||||
|
||||
.pill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 200px;
|
||||
height: 72px;
|
||||
border-radius: 36px;
|
||||
will-change: transform, filter;
|
||||
}
|
||||
|
||||
.word {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 88px;
|
||||
height: 72px;
|
||||
line-height: 72px;
|
||||
white-space: nowrap;
|
||||
font-size: 56px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
will-change: transform, filter;
|
||||
}
|
||||
|
||||
.inner-div {
|
||||
position: absolute;
|
||||
left: 220px;
|
||||
right: 200px;
|
||||
top: 80px;
|
||||
height: 1px;
|
||||
background: #0a0a12;
|
||||
}
|
||||
|
||||
.track-a,
|
||||
.track-b {
|
||||
position: absolute;
|
||||
left: 220px;
|
||||
right: 200px;
|
||||
height: 1px;
|
||||
background: #0c0c14;
|
||||
}
|
||||
.track-a {
|
||||
top: 35px;
|
||||
}
|
||||
.track-b {
|
||||
top: 123px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="demo"
|
||||
data-composition-id="motion-blur-demo"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
>
|
||||
<div class="header">
|
||||
<span class="header-title">Motion Blur — Velocity Showcase</span>
|
||||
<span class="header-sub">feGaussianBlur + scaleX · blur ∝ velocity</span>
|
||||
</div>
|
||||
|
||||
<div class="group" style="top: 80px">
|
||||
<div class="dur"><span class="dur-num">3.0</span><span class="dur-unit">seconds</span></div>
|
||||
<div class="row-hint" style="top: 0">shape</div>
|
||||
<div class="row-hint" style="top: 88px">text</div>
|
||||
<div class="track-a"></div>
|
||||
<div class="inner-div"></div>
|
||||
<div class="track-b"></div>
|
||||
<div
|
||||
class="pill"
|
||||
id="s1"
|
||||
style="
|
||||
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
||||
box-shadow: 0 0 32px rgba(96, 165, 250, 0.18);
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
class="word"
|
||||
id="t1"
|
||||
style="color: #3b82f6; text-shadow: 0 0 40px rgba(96, 165, 250, 0.3)"
|
||||
>
|
||||
DRIFT
|
||||
</div>
|
||||
<div class="spd"><span class="spd-label">slow</span></div>
|
||||
</div>
|
||||
|
||||
<div class="group" style="top: 264px">
|
||||
<div class="dur"><span class="dur-num">1.5</span><span class="dur-unit">seconds</span></div>
|
||||
<div class="row-hint" style="top: 0">shape</div>
|
||||
<div class="row-hint" style="top: 88px">text</div>
|
||||
<div class="track-a"></div>
|
||||
<div class="inner-div"></div>
|
||||
<div class="track-b"></div>
|
||||
<div
|
||||
class="pill"
|
||||
id="s2"
|
||||
style="
|
||||
background: linear-gradient(135deg, #34d399, #059669);
|
||||
box-shadow: 0 0 32px rgba(52, 211, 153, 0.18);
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
class="word"
|
||||
id="t2"
|
||||
style="color: #10b981; text-shadow: 0 0 40px rgba(52, 211, 153, 0.3)"
|
||||
>
|
||||
GLIDE
|
||||
</div>
|
||||
<div class="spd"><span class="spd-label">medium</span></div>
|
||||
</div>
|
||||
|
||||
<div class="group" style="top: 448px">
|
||||
<div class="dur"><span class="dur-num">0.7</span><span class="dur-unit">seconds</span></div>
|
||||
<div class="row-hint" style="top: 0">shape</div>
|
||||
<div class="row-hint" style="top: 88px">text</div>
|
||||
<div class="track-a"></div>
|
||||
<div class="inner-div"></div>
|
||||
<div class="track-b"></div>
|
||||
<div
|
||||
class="pill"
|
||||
id="s3"
|
||||
style="
|
||||
background: linear-gradient(135deg, #c084fc, #7c3aed);
|
||||
box-shadow: 0 0 32px rgba(192, 132, 252, 0.18);
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
class="word"
|
||||
id="t3"
|
||||
style="color: #a855f7; text-shadow: 0 0 40px rgba(192, 132, 252, 0.3)"
|
||||
>
|
||||
RUSH
|
||||
</div>
|
||||
<div class="spd"><span class="spd-label">fast</span></div>
|
||||
</div>
|
||||
|
||||
<div class="group" style="top: 632px">
|
||||
<div class="dur">
|
||||
<span class="dur-num">0.35</span><span class="dur-unit">seconds</span>
|
||||
</div>
|
||||
<div class="row-hint" style="top: 0">shape</div>
|
||||
<div class="row-hint" style="top: 88px">text</div>
|
||||
<div class="track-a"></div>
|
||||
<div class="inner-div"></div>
|
||||
<div class="track-b"></div>
|
||||
<div
|
||||
class="pill"
|
||||
id="s4"
|
||||
style="
|
||||
background: linear-gradient(135deg, #fb923c, #ea580c);
|
||||
box-shadow: 0 0 32px rgba(251, 146, 60, 0.18);
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
class="word"
|
||||
id="t4"
|
||||
style="color: #f97316; text-shadow: 0 0 40px rgba(251, 146, 60, 0.3)"
|
||||
>
|
||||
BLAST
|
||||
</div>
|
||||
<div class="spd"><span class="spd-label">faster</span></div>
|
||||
</div>
|
||||
|
||||
<div class="group" style="top: 816px">
|
||||
<div class="dur"><span class="dur-num">0.2</span><span class="dur-unit">seconds</span></div>
|
||||
<div class="row-hint" style="top: 0">shape</div>
|
||||
<div class="row-hint" style="top: 88px">text</div>
|
||||
<div class="track-a"></div>
|
||||
<div class="inner-div"></div>
|
||||
<div class="track-b"></div>
|
||||
<div
|
||||
class="pill"
|
||||
id="s5"
|
||||
style="
|
||||
background: linear-gradient(135deg, #f87171, #dc2626);
|
||||
box-shadow: 0 0 32px rgba(248, 113, 113, 0.18);
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
class="word"
|
||||
id="t5"
|
||||
style="color: #ef4444; text-shadow: 0 0 40px rgba(248, 113, 113, 0.3)"
|
||||
>
|
||||
WARP
|
||||
</div>
|
||||
<div class="spd"><span class="spd-label">extreme</span></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
if (!window._hfMbUid) window._hfMbUid = 0;
|
||||
var ns = "http://www.w3.org/2000/svg";
|
||||
|
||||
window.attachMotionBlur = function (selector, tl, opts) {
|
||||
opts = opts || {};
|
||||
var blurScale = opts.blurScale !== undefined ? opts.blurScale : 0.008;
|
||||
var blurMax = opts.blurMax !== undefined ? opts.blurMax : 20;
|
||||
var stretchScale = opts.stretchScale !== undefined ? opts.stretchScale : 0.0002;
|
||||
var stretchMax = opts.stretchMax !== undefined ? opts.stretchMax : 0;
|
||||
var axis = opts.axis || "both";
|
||||
|
||||
var items = Array.isArray(selector) ? selector : [selector];
|
||||
var targets = items.reduce(function (acc, s) {
|
||||
if (typeof s === "string") {
|
||||
document.querySelectorAll(s).forEach(function (el) {
|
||||
acc.push(el);
|
||||
});
|
||||
} else if (s instanceof Element) {
|
||||
acc.push(s);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
var GHOST_CFG = [
|
||||
{ frac: 0.25, slope: 0.55 },
|
||||
{ frac: 0.55, slope: 0.28 },
|
||||
{ frac: 1.0, slope: 0.1 },
|
||||
];
|
||||
|
||||
var state = targets.map(function (el) {
|
||||
var uid = "hf-mb-" + window._hfMbUid++;
|
||||
var svg = document.createElementNS(ns, "svg");
|
||||
svg.setAttribute("style", "position:absolute;width:0;height:0;overflow:hidden;");
|
||||
|
||||
var filter = document.createElementNS(ns, "filter");
|
||||
filter.id = uid;
|
||||
filter.setAttribute("x", "-110%");
|
||||
filter.setAttribute("y", "-25%");
|
||||
filter.setAttribute("width", "260%");
|
||||
filter.setAttribute("height", "150%");
|
||||
|
||||
var ghosts = GHOST_CFG.map(function (cfg, gi) {
|
||||
var feOff = document.createElementNS(ns, "feOffset");
|
||||
feOff.setAttribute("in", "SourceGraphic");
|
||||
feOff.setAttribute("dx", "0");
|
||||
feOff.setAttribute("dy", "0");
|
||||
feOff.setAttribute("result", "go" + gi);
|
||||
|
||||
var feGB = document.createElementNS(ns, "feGaussianBlur");
|
||||
feGB.setAttribute("in", "go" + gi);
|
||||
feGB.setAttribute("stdDeviation", "0 0");
|
||||
feGB.setAttribute("result", "gb" + gi);
|
||||
|
||||
var feCT = document.createElementNS(ns, "feComponentTransfer");
|
||||
feCT.setAttribute("in", "gb" + gi);
|
||||
feCT.setAttribute("result", "gf" + gi);
|
||||
var feFuncA = document.createElementNS(ns, "feFuncA");
|
||||
feFuncA.setAttribute("type", "linear");
|
||||
feFuncA.setAttribute("slope", String(cfg.slope));
|
||||
feCT.appendChild(feFuncA);
|
||||
|
||||
filter.appendChild(feOff);
|
||||
filter.appendChild(feGB);
|
||||
filter.appendChild(feCT);
|
||||
return { feOff: feOff, feGB: feGB, frac: cfg.frac };
|
||||
});
|
||||
|
||||
var feTopBlur = document.createElementNS(ns, "feGaussianBlur");
|
||||
feTopBlur.setAttribute("in", "SourceGraphic");
|
||||
feTopBlur.setAttribute("stdDeviation", "0 0");
|
||||
feTopBlur.setAttribute("result", "top");
|
||||
filter.appendChild(feTopBlur);
|
||||
|
||||
var feMerge = document.createElementNS(ns, "feMerge");
|
||||
for (var mi = GHOST_CFG.length - 1; mi >= 0; mi--) {
|
||||
var mn = document.createElementNS(ns, "feMergeNode");
|
||||
mn.setAttribute("in", "gf" + mi);
|
||||
feMerge.appendChild(mn);
|
||||
}
|
||||
var mnTop = document.createElementNS(ns, "feMergeNode");
|
||||
mnTop.setAttribute("in", "top");
|
||||
feMerge.appendChild(mnTop);
|
||||
filter.appendChild(feMerge);
|
||||
|
||||
svg.appendChild(filter);
|
||||
document.body.appendChild(svg);
|
||||
|
||||
return {
|
||||
el: el,
|
||||
ghosts: ghosts,
|
||||
feTopBlur: feTopBlur,
|
||||
filterId: uid,
|
||||
prevX: parseFloat(gsap.getProperty(el, "x")) || 0,
|
||||
prevY: parseFloat(gsap.getProperty(el, "y")) || 0,
|
||||
prevTime: tl.time(),
|
||||
};
|
||||
});
|
||||
|
||||
var _proxy = { t: 0 };
|
||||
tl.to(
|
||||
_proxy,
|
||||
{
|
||||
t: 1,
|
||||
duration: Math.max(tl.duration(), 0.1),
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
var time = tl.time();
|
||||
|
||||
state.forEach(function (s) {
|
||||
var x = parseFloat(gsap.getProperty(s.el, "x")) || 0;
|
||||
var y = parseFloat(gsap.getProperty(s.el, "y")) || 0;
|
||||
var dt = time - s.prevTime;
|
||||
|
||||
if (dt > 0.0005) {
|
||||
var vx = axis !== "y" ? (x - s.prevX) / dt : 0;
|
||||
var vy = axis !== "x" ? (y - s.prevY) / dt : 0;
|
||||
|
||||
var bx = Math.min(Math.abs(vx) * blurScale, blurMax);
|
||||
var by = Math.min(Math.abs(vy) * blurScale, blurMax);
|
||||
var bxFinal = axis !== "y" ? bx : Math.max(by * 0.08, 0.4);
|
||||
var byFinal = axis !== "x" ? by : Math.max(bx * 0.08, 0.4);
|
||||
var active = bx > 0.3 || by > 0.3;
|
||||
|
||||
if (active) {
|
||||
s.el.style.filter = "url(#" + s.filterId + ")";
|
||||
s.ghosts.forEach(function (g) {
|
||||
var dx =
|
||||
axis !== "y" ? (vx >= 0 ? -bxFinal * g.frac : bxFinal * g.frac) : 0;
|
||||
var dy =
|
||||
axis !== "x" ? (vy >= 0 ? -byFinal * g.frac : byFinal * g.frac) : 0;
|
||||
g.feOff.setAttribute("dx", dx.toFixed(2));
|
||||
g.feOff.setAttribute("dy", dy.toFixed(2));
|
||||
var gbx =
|
||||
axis !== "y"
|
||||
? (bxFinal * g.frac * 0.5).toFixed(2)
|
||||
: Math.max(byFinal * g.frac * 0.04, 0.4).toFixed(2);
|
||||
var gby =
|
||||
axis !== "x"
|
||||
? (byFinal * g.frac * 0.5).toFixed(2)
|
||||
: Math.max(bxFinal * g.frac * 0.04, 0.4).toFixed(2);
|
||||
g.feGB.setAttribute("stdDeviation", gbx + " " + gby);
|
||||
});
|
||||
s.feTopBlur.setAttribute(
|
||||
"stdDeviation",
|
||||
(bxFinal * 0.15).toFixed(2) + " " + (byFinal * 0.15).toFixed(2),
|
||||
);
|
||||
if (stretchMax > 0) {
|
||||
var sx = 1 + Math.min(Math.abs(vx) * stretchScale, stretchMax);
|
||||
var sy = 1 + Math.min(Math.abs(vy) * stretchScale, stretchMax);
|
||||
var ox = axis !== "y" ? (vx >= 0 ? "100% 50%" : "0% 50%") : "50% 50%";
|
||||
var oy = axis !== "x" ? (vy >= 0 ? "50% 100%" : "50% 0%") : "50% 50%";
|
||||
gsap.set(s.el, {
|
||||
scaleX: axis !== "y" ? sx : 1,
|
||||
scaleY: axis !== "x" ? sy : 1,
|
||||
transformOrigin: axis === "x" ? ox : axis === "y" ? oy : "50% 50%",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
s.el.style.filter = "";
|
||||
s.ghosts.forEach(function (g) {
|
||||
g.feOff.setAttribute("dx", "0");
|
||||
g.feOff.setAttribute("dy", "0");
|
||||
g.feGB.setAttribute("stdDeviation", "0 0");
|
||||
});
|
||||
s.feTopBlur.setAttribute("stdDeviation", "0 0");
|
||||
if (stretchMax > 0) {
|
||||
gsap.set(s.el, { scaleX: 1, scaleY: 1, transformOrigin: "50% 50%" });
|
||||
}
|
||||
}
|
||||
|
||||
s.prevX = x;
|
||||
s.prevY = y;
|
||||
s.prevTime = time;
|
||||
} else if (dt < -0.0005) {
|
||||
s.el.style.filter = "";
|
||||
s.ghosts.forEach(function (g) {
|
||||
g.feOff.setAttribute("dx", "0");
|
||||
g.feOff.setAttribute("dy", "0");
|
||||
g.feGB.setAttribute("stdDeviation", "0 0");
|
||||
});
|
||||
s.feTopBlur.setAttribute("stdDeviation", "0 0");
|
||||
if (stretchMax > 0) {
|
||||
gsap.set(s.el, { scaleX: 1, scaleY: 1, transformOrigin: "50% 50%" });
|
||||
}
|
||||
s.prevX = x;
|
||||
s.prevY = y;
|
||||
s.prevTime = time;
|
||||
}
|
||||
// dt ≈ 0: double-fire — skip.
|
||||
});
|
||||
},
|
||||
},
|
||||
0,
|
||||
);
|
||||
};
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
gsap.set(["#s1", "#s2", "#s3", "#s4", "#s5", "#t1", "#t2", "#t3", "#t4", "#t5"], {
|
||||
x: -400,
|
||||
});
|
||||
|
||||
tl.to(["#s1", "#t1"], { x: 1640, duration: 3.0, ease: "power3.inOut" }, 0);
|
||||
tl.to(["#s2", "#t2"], { x: 1640, duration: 1.5, ease: "power3.inOut" }, 0);
|
||||
tl.to(["#s3", "#t3"], { x: 1640, duration: 0.7, ease: "power3.inOut" }, 0);
|
||||
tl.to(["#s4", "#t4"], { x: 1640, duration: 0.35, ease: "power3.inOut" }, 0);
|
||||
tl.to(["#s5", "#t5"], { x: 1640, duration: 0.2, ease: "power3.inOut" }, 0);
|
||||
|
||||
tl.set(document.body, {}, 4);
|
||||
|
||||
attachMotionBlur(
|
||||
["#s1", "#s2", "#s3", "#s4", "#s5", "#t1", "#t2", "#t3", "#t4", "#t5"],
|
||||
tl,
|
||||
{ axis: "x", blurMax: 80, stretchMax: 0 },
|
||||
);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["motion-blur-demo"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,255 @@
|
||||
<!--
|
||||
Motion Blur — velocity-driven directional motion blur.
|
||||
|
||||
Usage: paste this snippet into your composition, then call
|
||||
attachMotionBlur() with any element animated by your GSAP timeline.
|
||||
|
||||
The snippet hooks into the timeline's onUpdate callback, tracks the
|
||||
GSAP x/y position of each target frame-by-frame, and applies:
|
||||
1. Ghost copies of the element at increasing backward offsets with
|
||||
decreasing opacity — inherently one-sided, no forward blur component.
|
||||
2. A small Gaussian blur at the current position so the element looks
|
||||
in-motion (blurry) rather than sharp on top of the trail.
|
||||
3. An optional scaleX/Y stretch in the direction of travel (off by
|
||||
default; enable via stretchMax > 0 if you want the effect).
|
||||
|
||||
Both effects clear automatically when the element decelerates to rest.
|
||||
|
||||
Requirements:
|
||||
- Elements must be animated via GSAP x/y (transform), not left/top.
|
||||
- Call attachMotionBlur() AFTER defining all tweens, before
|
||||
window.__timelines registration.
|
||||
- GSAP must be loaded before this snippet executes.
|
||||
|
||||
API:
|
||||
attachMotionBlur(selector, timeline, options?)
|
||||
|
||||
Options:
|
||||
blurScale — directional blur per px/s of velocity (default 0.008)
|
||||
blurMax — max blur radius on the motion axis in px (default 20)
|
||||
stretchScale — scaleX/Y added per px/s (default 0.0002)
|
||||
stretchMax — max stretch factor above 1.0 (default 0, disabled)
|
||||
axis — "x" | "y" | "both" (default "both")
|
||||
-->
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
if (!window._hfMbUid) window._hfMbUid = 0;
|
||||
|
||||
window.attachMotionBlur = function (selector, tl, opts) {
|
||||
opts = opts || {};
|
||||
var blurScale = opts.blurScale !== undefined ? opts.blurScale : 0.008;
|
||||
var blurMax = opts.blurMax !== undefined ? opts.blurMax : 20;
|
||||
var stretchScale = opts.stretchScale !== undefined ? opts.stretchScale : 0.0002;
|
||||
var stretchMax = opts.stretchMax !== undefined ? opts.stretchMax : 0;
|
||||
var axis = opts.axis || "both";
|
||||
|
||||
var items = Array.isArray(selector) ? selector : [selector];
|
||||
var targets = items.reduce(function (acc, s) {
|
||||
if (typeof s === "string") {
|
||||
document.querySelectorAll(s).forEach(function (el) {
|
||||
acc.push(el);
|
||||
});
|
||||
} else if (s instanceof Element) {
|
||||
acc.push(s);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// One SVG filter per target. Ghost copies placed behind → inherently one-sided.
|
||||
// Small top Gaussian makes element look blurry at current position, not sharp.
|
||||
var ns = "http://www.w3.org/2000/svg";
|
||||
var GHOST_CFG = [
|
||||
{ frac: 0.25, slope: 0.55 },
|
||||
{ frac: 0.55, slope: 0.28 },
|
||||
{ frac: 1.0, slope: 0.1 },
|
||||
];
|
||||
|
||||
var state = targets.map(function (el) {
|
||||
var uid = "hf-mb-" + window._hfMbUid++;
|
||||
var svg = document.createElementNS(ns, "svg");
|
||||
svg.setAttribute("style", "position:absolute;width:0;height:0;overflow:hidden;");
|
||||
|
||||
var filter = document.createElementNS(ns, "filter");
|
||||
filter.id = uid;
|
||||
filter.setAttribute("x", "-110%");
|
||||
filter.setAttribute("y", "-25%");
|
||||
filter.setAttribute("width", "260%");
|
||||
filter.setAttribute("height", "150%");
|
||||
|
||||
var ghosts = GHOST_CFG.map(function (cfg, gi) {
|
||||
var feOff = document.createElementNS(ns, "feOffset");
|
||||
feOff.setAttribute("in", "SourceGraphic");
|
||||
feOff.setAttribute("dx", "0");
|
||||
feOff.setAttribute("dy", "0");
|
||||
feOff.setAttribute("result", "go" + gi);
|
||||
|
||||
var feGB = document.createElementNS(ns, "feGaussianBlur");
|
||||
feGB.setAttribute("in", "go" + gi);
|
||||
feGB.setAttribute("stdDeviation", "0 0");
|
||||
feGB.setAttribute("result", "gb" + gi);
|
||||
|
||||
var feCT = document.createElementNS(ns, "feComponentTransfer");
|
||||
feCT.setAttribute("in", "gb" + gi);
|
||||
feCT.setAttribute("result", "gf" + gi);
|
||||
var feFuncA = document.createElementNS(ns, "feFuncA");
|
||||
feFuncA.setAttribute("type", "linear");
|
||||
feFuncA.setAttribute("slope", String(cfg.slope));
|
||||
feCT.appendChild(feFuncA);
|
||||
|
||||
filter.appendChild(feOff);
|
||||
filter.appendChild(feGB);
|
||||
filter.appendChild(feCT);
|
||||
return { feOff: feOff, feGB: feGB, frac: cfg.frac };
|
||||
});
|
||||
|
||||
var feTopBlur = document.createElementNS(ns, "feGaussianBlur");
|
||||
feTopBlur.setAttribute("in", "SourceGraphic");
|
||||
feTopBlur.setAttribute("stdDeviation", "0 0");
|
||||
feTopBlur.setAttribute("result", "top");
|
||||
filter.appendChild(feTopBlur);
|
||||
|
||||
var feMerge = document.createElementNS(ns, "feMerge");
|
||||
for (var mi = GHOST_CFG.length - 1; mi >= 0; mi--) {
|
||||
var mn = document.createElementNS(ns, "feMergeNode");
|
||||
mn.setAttribute("in", "gf" + mi);
|
||||
feMerge.appendChild(mn);
|
||||
}
|
||||
var mnTop = document.createElementNS(ns, "feMergeNode");
|
||||
mnTop.setAttribute("in", "top");
|
||||
feMerge.appendChild(mnTop);
|
||||
filter.appendChild(feMerge);
|
||||
|
||||
svg.appendChild(filter);
|
||||
document.body.appendChild(svg);
|
||||
|
||||
return {
|
||||
el: el,
|
||||
ghosts: ghosts,
|
||||
feTopBlur: feTopBlur,
|
||||
filterId: uid,
|
||||
prevX: parseFloat(gsap.getProperty(el, "x")) || 0,
|
||||
prevY: parseFloat(gsap.getProperty(el, "y")) || 0,
|
||||
prevTime: tl.time(),
|
||||
};
|
||||
});
|
||||
|
||||
// tl.eventCallback("onUpdate") is not available in the HyperFrames renderer —
|
||||
// the runtime proxies the timeline object. Tween onUpdate fires on every seek.
|
||||
var _proxy = { t: 0 };
|
||||
tl.to(
|
||||
_proxy,
|
||||
{
|
||||
t: 1,
|
||||
duration: Math.max(tl.duration(), 0.1),
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
var time = tl.time();
|
||||
|
||||
state.forEach(function (s) {
|
||||
var x = parseFloat(gsap.getProperty(s.el, "x")) || 0;
|
||||
var y = parseFloat(gsap.getProperty(s.el, "y")) || 0;
|
||||
var dt = time - s.prevTime;
|
||||
|
||||
if (dt > 0.0005) {
|
||||
var vx = axis !== "y" ? (x - s.prevX) / dt : 0;
|
||||
var vy = axis !== "x" ? (y - s.prevY) / dt : 0;
|
||||
|
||||
var bx = Math.min(Math.abs(vx) * blurScale, blurMax);
|
||||
var by = Math.min(Math.abs(vy) * blurScale, blurMax);
|
||||
var bxFinal = axis !== "y" ? bx : Math.max(by * 0.08, 0.4);
|
||||
var byFinal = axis !== "x" ? by : Math.max(bx * 0.08, 0.4);
|
||||
var active = bx > 0.3 || by > 0.3;
|
||||
|
||||
if (active) {
|
||||
s.el.style.filter = "url(#" + s.filterId + ")";
|
||||
s.ghosts.forEach(function (g) {
|
||||
var dx = axis !== "y" ? (vx >= 0 ? -bxFinal * g.frac : bxFinal * g.frac) : 0;
|
||||
var dy = axis !== "x" ? (vy >= 0 ? -byFinal * g.frac : byFinal * g.frac) : 0;
|
||||
g.feOff.setAttribute("dx", dx.toFixed(2));
|
||||
g.feOff.setAttribute("dy", dy.toFixed(2));
|
||||
var gbx =
|
||||
axis !== "y"
|
||||
? (bxFinal * g.frac * 0.5).toFixed(2)
|
||||
: Math.max(byFinal * g.frac * 0.04, 0.4).toFixed(2);
|
||||
var gby =
|
||||
axis !== "x"
|
||||
? (byFinal * g.frac * 0.5).toFixed(2)
|
||||
: Math.max(bxFinal * g.frac * 0.04, 0.4).toFixed(2);
|
||||
g.feGB.setAttribute("stdDeviation", gbx + " " + gby);
|
||||
});
|
||||
s.feTopBlur.setAttribute(
|
||||
"stdDeviation",
|
||||
(bxFinal * 0.15).toFixed(2) + " " + (byFinal * 0.15).toFixed(2),
|
||||
);
|
||||
|
||||
if (stretchMax > 0) {
|
||||
var sx = 1 + Math.min(Math.abs(vx) * stretchScale, stretchMax);
|
||||
var sy = 1 + Math.min(Math.abs(vy) * stretchScale, stretchMax);
|
||||
var ox = axis !== "y" ? (vx >= 0 ? "100% 50%" : "0% 50%") : "50% 50%";
|
||||
var oy = axis !== "x" ? (vy >= 0 ? "50% 100%" : "50% 0%") : "50% 50%";
|
||||
gsap.set(s.el, {
|
||||
scaleX: axis !== "y" ? sx : 1,
|
||||
scaleY: axis !== "x" ? sy : 1,
|
||||
transformOrigin: axis === "x" ? ox : axis === "y" ? oy : "50% 50%",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
s.el.style.filter = "";
|
||||
s.ghosts.forEach(function (g) {
|
||||
g.feOff.setAttribute("dx", "0");
|
||||
g.feOff.setAttribute("dy", "0");
|
||||
g.feGB.setAttribute("stdDeviation", "0 0");
|
||||
});
|
||||
s.feTopBlur.setAttribute("stdDeviation", "0 0");
|
||||
if (stretchMax > 0) {
|
||||
gsap.set(s.el, { scaleX: 1, scaleY: 1, transformOrigin: "50% 50%" });
|
||||
}
|
||||
}
|
||||
|
||||
s.prevX = x;
|
||||
s.prevY = y;
|
||||
s.prevTime = time;
|
||||
} else if (dt < -0.0005) {
|
||||
s.el.style.filter = "";
|
||||
s.ghosts.forEach(function (g) {
|
||||
g.feOff.setAttribute("dx", "0");
|
||||
g.feOff.setAttribute("dy", "0");
|
||||
g.feGB.setAttribute("stdDeviation", "0 0");
|
||||
});
|
||||
s.feTopBlur.setAttribute("stdDeviation", "0 0");
|
||||
if (stretchMax > 0) {
|
||||
gsap.set(s.el, { scaleX: 1, scaleY: 1, transformOrigin: "50% 50%" });
|
||||
}
|
||||
s.prevX = x;
|
||||
s.prevY = y;
|
||||
s.prevTime = time;
|
||||
}
|
||||
// dt ≈ 0: double-fire — skip.
|
||||
});
|
||||
},
|
||||
},
|
||||
0,
|
||||
);
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Timeline integration example:
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.fromTo("#my-box", { x: -100 }, { x: 1700, duration: 1.2, ease: "power3.inOut" }, 0.5);
|
||||
|
||||
// Extend to data-duration so seeks past the last tween reach the blur callback.
|
||||
tl.set(document.body, {}, DATA_DURATION);
|
||||
|
||||
// Call AFTER tweens, BEFORE window.__timelines registration.
|
||||
// attachMotionBlur adds a tracking tween with onUpdate — must be called after
|
||||
// tl.set()/tl.to() have established the final timeline duration.
|
||||
attachMotionBlur("#my-box", tl, { axis: "x" });
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["my-composition"] = tl;
|
||||
-->
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "motion-blur",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Motion Blur",
|
||||
"description": "Velocity-driven motion blur — samples element position each frame and applies a one-sided SVG feGaussianBlur ghost trail proportional to speed",
|
||||
"tags": ["effect", "motion-blur", "velocity", "animation", "physics"],
|
||||
"files": [
|
||||
{
|
||||
"path": "motion-blur.html",
|
||||
"target": "compositions/components/motion-blur.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/motion-blur.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Parallax Unzoom — Demo</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;600;800;900&display=swap"
|
||||
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 {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #1a1a20;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.demo-canvas {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.parallax-unzoom-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 360px);
|
||||
grid-template-rows: repeat(3, 341px);
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.parallax-unzoom-card {
|
||||
border-radius: 22px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-shadow: 0 12px 36px rgba(20, 20, 30, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-glyph {
|
||||
font-size: 92px;
|
||||
font-weight: 800;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pu-c0 {
|
||||
background: linear-gradient(135deg, #3a8b6f, #1d4d3d);
|
||||
}
|
||||
.pu-c1 {
|
||||
background: linear-gradient(135deg, #8b4a3a, #4a2418);
|
||||
}
|
||||
.pu-c2 {
|
||||
background: linear-gradient(135deg, #c89868, #8a6038);
|
||||
}
|
||||
.pu-c3 {
|
||||
background: linear-gradient(135deg, #3a6088, #1a2f4a);
|
||||
}
|
||||
.pu-c4 {
|
||||
background: linear-gradient(135deg, #b03838, #6a1818);
|
||||
}
|
||||
.pu-c5 {
|
||||
background: linear-gradient(135deg, #bcbcbc, #6e6e6e);
|
||||
}
|
||||
.pu-c6 {
|
||||
background: linear-gradient(135deg, #8a5fa0, #4a2860);
|
||||
}
|
||||
.pu-c7-focus {
|
||||
background: linear-gradient(135deg, #f5c93f, #d99a18);
|
||||
}
|
||||
.pu-c8 {
|
||||
background: linear-gradient(135deg, #d8d8d8, #9a9a9a);
|
||||
}
|
||||
.pu-c9 {
|
||||
background: linear-gradient(135deg, #5fa37e, #2a6a4a);
|
||||
}
|
||||
.pu-c10 {
|
||||
background: linear-gradient(135deg, #d1b66a, #8a7028);
|
||||
}
|
||||
.pu-c11 {
|
||||
background: linear-gradient(135deg, #4f7fc0, #1f3f80);
|
||||
}
|
||||
.pu-c12 {
|
||||
background: linear-gradient(135deg, #5a3a8f, #2a1858);
|
||||
}
|
||||
.pu-c13 {
|
||||
background: linear-gradient(135deg, #b85a3a, #6a2818);
|
||||
}
|
||||
.pu-c14 {
|
||||
background: linear-gradient(135deg, #f0f0e8, #c8c8be);
|
||||
}
|
||||
|
||||
.parallax-unzoom-card[data-pu-focus="true"] {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.focus-label {
|
||||
font-weight: 900;
|
||||
font-size: 56px;
|
||||
letter-spacing: -0.04em;
|
||||
color: #1a1208;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.focus-label small {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.18em;
|
||||
margin-bottom: 14px;
|
||||
color: rgba(26, 18, 8, 0.7);
|
||||
}
|
||||
|
||||
.hero-headline {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 68px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
color: #1a1a1a;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.parallax-unzoom-grid {
|
||||
--pu-progress: 0;
|
||||
--pu-focus-scale: 6;
|
||||
--pu-sibling-push: 900px;
|
||||
--pu-sibling-fade: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.parallax-unzoom-card {
|
||||
transform-origin: center center;
|
||||
will-change: transform, opacity;
|
||||
transform: translate3d(
|
||||
calc(var(--pu-dx, 0px) * (1 - var(--pu-progress))),
|
||||
calc(var(--pu-dy, 0px) * (1 - var(--pu-progress))),
|
||||
0
|
||||
)
|
||||
scale(calc(0.7 + 0.3 * var(--pu-progress)));
|
||||
opacity: calc(var(--pu-progress) * var(--pu-sibling-fade));
|
||||
}
|
||||
|
||||
.parallax-unzoom-card[data-pu-focus="true"] {
|
||||
z-index: 10;
|
||||
transform: translate3d(0, 0, 0)
|
||||
scale(calc(var(--pu-focus-scale) - (var(--pu-focus-scale) - 1) * var(--pu-progress)));
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="demo"
|
||||
data-composition-id="parallax-unzoom-demo"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="6"
|
||||
data-start="0"
|
||||
>
|
||||
<div class="demo-canvas">
|
||||
<div class="hero-headline">Inspired by how people discover.</div>
|
||||
|
||||
<div class="parallax-unzoom-grid">
|
||||
<div class="parallax-unzoom-card pu-c0" data-pu-row="0" data-pu-col="0">
|
||||
<span class="card-glyph">◇</span>
|
||||
</div>
|
||||
<div class="parallax-unzoom-card pu-c1" data-pu-row="0" data-pu-col="1">
|
||||
<span class="card-glyph">◐</span>
|
||||
</div>
|
||||
<div class="parallax-unzoom-card pu-c2" data-pu-row="0" data-pu-col="2">
|
||||
<span class="card-glyph">◯</span>
|
||||
</div>
|
||||
<div class="parallax-unzoom-card pu-c3" data-pu-row="0" data-pu-col="3">
|
||||
<span class="card-glyph">◑</span>
|
||||
</div>
|
||||
<div class="parallax-unzoom-card pu-c4" data-pu-row="0" data-pu-col="4">
|
||||
<span class="card-glyph">▲</span>
|
||||
</div>
|
||||
|
||||
<div class="parallax-unzoom-card pu-c5" data-pu-row="1" data-pu-col="0">
|
||||
<span class="card-glyph">✦</span>
|
||||
</div>
|
||||
<div class="parallax-unzoom-card pu-c6" data-pu-row="1" data-pu-col="1">
|
||||
<span class="card-glyph">❖</span>
|
||||
</div>
|
||||
<div
|
||||
class="parallax-unzoom-card pu-c7-focus"
|
||||
data-pu-row="1"
|
||||
data-pu-col="2"
|
||||
data-pu-focus="true"
|
||||
>
|
||||
<div class="focus-label">
|
||||
<small>EBAY PLAYBOOK</small>
|
||||
DISCOVER
|
||||
</div>
|
||||
</div>
|
||||
<div class="parallax-unzoom-card pu-c8" data-pu-row="1" data-pu-col="3">
|
||||
<span class="card-glyph">✧</span>
|
||||
</div>
|
||||
<div class="parallax-unzoom-card pu-c9" data-pu-row="1" data-pu-col="4">
|
||||
<span class="card-glyph">◈</span>
|
||||
</div>
|
||||
|
||||
<div class="parallax-unzoom-card pu-c10" data-pu-row="2" data-pu-col="0">
|
||||
<span class="card-glyph">▼</span>
|
||||
</div>
|
||||
<div class="parallax-unzoom-card pu-c11" data-pu-row="2" data-pu-col="1">
|
||||
<span class="card-glyph">◆</span>
|
||||
</div>
|
||||
<div class="parallax-unzoom-card pu-c12" data-pu-row="2" data-pu-col="2">
|
||||
<span class="card-glyph">●</span>
|
||||
</div>
|
||||
<div class="parallax-unzoom-card pu-c13" data-pu-row="2" data-pu-col="3">
|
||||
<span class="card-glyph">◭</span>
|
||||
</div>
|
||||
<div class="parallax-unzoom-card pu-c14" data-pu-row="2" data-pu-col="4">
|
||||
<span class="card-glyph" style="color: rgba(40, 40, 40, 0.7)">◇</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const grids = document.querySelectorAll(".parallax-unzoom-grid");
|
||||
grids.forEach((grid) => {
|
||||
const cards = Array.from(grid.querySelectorAll(".parallax-unzoom-card"));
|
||||
if (cards.length === 0) return;
|
||||
let focal = cards.find((c) => c.dataset.puFocus === "true");
|
||||
if (!focal) {
|
||||
focal = cards[Math.floor(cards.length / 2)];
|
||||
focal.dataset.puFocus = "true";
|
||||
}
|
||||
const focusRow = Number(focal.dataset.puRow ?? 0);
|
||||
const focusCol = Number(focal.dataset.puCol ?? 0);
|
||||
const push =
|
||||
parseFloat(getComputedStyle(grid).getPropertyValue("--pu-sibling-push")) || 900;
|
||||
cards.forEach((card) => {
|
||||
if (card === focal) return;
|
||||
const row = Number(card.dataset.puRow ?? 0);
|
||||
const col = Number(card.dataset.puCol ?? 0);
|
||||
const dCol = col - focusCol;
|
||||
const dRow = row - focusRow;
|
||||
const mag = Math.hypot(dCol, dRow) || 1;
|
||||
const nx = (dCol / mag) * push * mag;
|
||||
const ny = (dRow / mag) * push * mag;
|
||||
card.style.setProperty("--pu-dx", `${nx}px`);
|
||||
card.style.setProperty("--pu-dy", `${ny}px`);
|
||||
});
|
||||
});
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.to(
|
||||
".hero-headline",
|
||||
{
|
||||
opacity: 0,
|
||||
duration: 0.5,
|
||||
ease: "power2.in",
|
||||
},
|
||||
0.3,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
".parallax-unzoom-grid",
|
||||
{ "--pu-progress": 0 },
|
||||
{
|
||||
"--pu-progress": 1,
|
||||
duration: 3.0,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
0.5,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
".parallax-unzoom-card:not([data-pu-focus='true'])",
|
||||
{ opacity: 0 },
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 1.5,
|
||||
ease: "power2.out",
|
||||
stagger: { each: 0.02, from: "center" },
|
||||
},
|
||||
2.0,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".focus-label",
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 1.0,
|
||||
ease: "power2.out",
|
||||
},
|
||||
2.5,
|
||||
);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["parallax-unzoom-demo"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,103 @@
|
||||
<!--
|
||||
Parallax Unzoom — focus card scales down from full frame while siblings
|
||||
parallax IN to form a grid. The reverse of parallax-zoom.
|
||||
|
||||
Pairs naturally with parallax-zoom: zoom INTO a card in scene 1, then
|
||||
unzoom OUT of it in scene 2 to reveal a fresh grid underneath.
|
||||
|
||||
Usage:
|
||||
1. Wrap your card grid with class="parallax-unzoom-grid".
|
||||
2. Mark each card with class="parallax-unzoom-card".
|
||||
3. Add data-pu-row="0|1|2|..." and data-pu-col="0|1|2|..." per card.
|
||||
4. Mark the card that starts at full size with data-pu-focus="true".
|
||||
5. Drive --pu-progress 0 → 1 on the .parallax-unzoom-grid from your
|
||||
composition's paused GSAP timeline (see example at bottom).
|
||||
|
||||
Tunable CSS variables on .parallax-unzoom-grid:
|
||||
- --pu-focus-scale (default 6) starting scale of the focus card
|
||||
- --pu-sibling-push (default 900px) outward distance siblings start at
|
||||
- --pu-sibling-fade (default 1) sibling opacity falloff (0–1)
|
||||
-->
|
||||
|
||||
<style>
|
||||
.parallax-unzoom-grid {
|
||||
--pu-progress: 0;
|
||||
--pu-focus-scale: 6;
|
||||
--pu-sibling-push: 900px;
|
||||
--pu-sibling-fade: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.parallax-unzoom-card {
|
||||
transform-origin: center center;
|
||||
will-change: transform, opacity;
|
||||
transform: translate3d(
|
||||
calc(var(--pu-dx, 0px) * (1 - var(--pu-progress))),
|
||||
calc(var(--pu-dy, 0px) * (1 - var(--pu-progress))),
|
||||
0
|
||||
)
|
||||
scale(calc(0.7 + 0.3 * var(--pu-progress)));
|
||||
opacity: calc(var(--pu-progress) * var(--pu-sibling-fade));
|
||||
}
|
||||
|
||||
.parallax-unzoom-card[data-pu-focus="true"] {
|
||||
z-index: 10;
|
||||
transform: translate3d(0, 0, 0)
|
||||
scale(calc(var(--pu-focus-scale) - (var(--pu-focus-scale) - 1) * var(--pu-progress)));
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const grids = document.querySelectorAll(".parallax-unzoom-grid");
|
||||
|
||||
grids.forEach((grid) => {
|
||||
const cards = Array.from(grid.querySelectorAll(".parallax-unzoom-card"));
|
||||
if (cards.length === 0) return;
|
||||
|
||||
let focal = cards.find((c) => c.dataset.puFocus === "true");
|
||||
if (!focal) {
|
||||
focal = cards[Math.floor(cards.length / 2)];
|
||||
focal.dataset.puFocus = "true";
|
||||
}
|
||||
|
||||
const focusRow = Number(focal.dataset.puRow ?? 0);
|
||||
const focusCol = Number(focal.dataset.puCol ?? 0);
|
||||
const push = parseFloat(getComputedStyle(grid).getPropertyValue("--pu-sibling-push")) || 900;
|
||||
|
||||
cards.forEach((card) => {
|
||||
if (card === focal) return;
|
||||
const row = Number(card.dataset.puRow ?? 0);
|
||||
const col = Number(card.dataset.puCol ?? 0);
|
||||
const dCol = col - focusCol;
|
||||
const dRow = row - focusRow;
|
||||
const mag = Math.hypot(dCol, dRow) || 1;
|
||||
const nx = (dCol / mag) * push * mag;
|
||||
const ny = (dRow / mag) * push * mag;
|
||||
card.style.setProperty("--pu-dx", `${nx}px`);
|
||||
card.style.setProperty("--pu-dy", `${ny}px`);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Timeline integration example (paste into your composition's GSAP timeline):
|
||||
|
||||
tl.fromTo(
|
||||
".parallax-unzoom-grid",
|
||||
{ "--pu-progress": 0 },
|
||||
{
|
||||
"--pu-progress": 1,
|
||||
duration: 2.4,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
startTime, // start time in seconds
|
||||
);
|
||||
|
||||
Chaining with parallax-zoom example:
|
||||
// Scene 1 (0-3s): zoom IN to the focal card via parallax-zoom
|
||||
// Scene 2 (3-6s): unzoom OUT of a NEW grid via parallax-unzoom,
|
||||
// with the focal card pre-positioned to match scene 1
|
||||
-->
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "parallax-unzoom",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Parallax Unzoom",
|
||||
"description": "Reveal transition — focus card scales down from full frame as siblings parallax in to form a grid (reverse of parallax-zoom)",
|
||||
"tags": ["transition", "reveal", "unzoom", "parallax", "grid", "hero"],
|
||||
"files": [
|
||||
{
|
||||
"path": "parallax-unzoom.html",
|
||||
"target": "compositions/components/parallax-unzoom.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/parallax-unzoom.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Parallax Zoom — Demo</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;600;800;900&display=swap"
|
||||
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 {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #1a1a20;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.demo-canvas {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.parallax-zoom-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 360px);
|
||||
grid-template-rows: repeat(3, 341px);
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.parallax-zoom-card {
|
||||
border-radius: 22px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-shadow: 0 12px 36px rgba(20, 20, 30, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.card-glyph {
|
||||
font-size: 92px;
|
||||
font-weight: 800;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pz-c0 {
|
||||
background: linear-gradient(135deg, #3a8b6f, #1d4d3d);
|
||||
}
|
||||
.pz-c1 {
|
||||
background: linear-gradient(135deg, #8b4a3a, #4a2418);
|
||||
}
|
||||
.pz-c2 {
|
||||
background: linear-gradient(135deg, #c89868, #8a6038);
|
||||
}
|
||||
.pz-c3 {
|
||||
background: linear-gradient(135deg, #3a6088, #1a2f4a);
|
||||
}
|
||||
.pz-c4 {
|
||||
background: linear-gradient(135deg, #b03838, #6a1818);
|
||||
}
|
||||
.pz-c5 {
|
||||
background: linear-gradient(135deg, #bcbcbc, #6e6e6e);
|
||||
}
|
||||
.pz-c6 {
|
||||
background: linear-gradient(135deg, #8a5fa0, #4a2860);
|
||||
}
|
||||
.pz-c7-focus {
|
||||
background: linear-gradient(135deg, #f5c93f, #d99a18);
|
||||
}
|
||||
.pz-c8 {
|
||||
background: linear-gradient(135deg, #d8d8d8, #9a9a9a);
|
||||
}
|
||||
.pz-c9 {
|
||||
background: linear-gradient(135deg, #5fa37e, #2a6a4a);
|
||||
}
|
||||
.pz-c10 {
|
||||
background: linear-gradient(135deg, #d1b66a, #8a7028);
|
||||
}
|
||||
.pz-c11 {
|
||||
background: linear-gradient(135deg, #4f7fc0, #1f3f80);
|
||||
}
|
||||
.pz-c12 {
|
||||
background: linear-gradient(135deg, #5a3a8f, #2a1858);
|
||||
}
|
||||
.pz-c13 {
|
||||
background: linear-gradient(135deg, #b85a3a, #6a2818);
|
||||
}
|
||||
.pz-c14 {
|
||||
background: linear-gradient(135deg, #f0f0e8, #c8c8be);
|
||||
}
|
||||
|
||||
.parallax-zoom-card[data-pz-focus="true"] {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.focus-label {
|
||||
font-weight: 900;
|
||||
font-size: 56px;
|
||||
letter-spacing: -0.04em;
|
||||
color: #1a1208;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.focus-label small {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.18em;
|
||||
margin-bottom: 14px;
|
||||
color: rgba(26, 18, 8, 0.7);
|
||||
}
|
||||
|
||||
.zoom-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, #f5c93f, #d99a18);
|
||||
opacity: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.hero-headline {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 68px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
color: #1a1a1a;
|
||||
opacity: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.parallax-zoom-grid {
|
||||
--pz-progress: 0;
|
||||
--pz-focus-scale: 6;
|
||||
--pz-sibling-push: 900px;
|
||||
--pz-sibling-fade: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.parallax-zoom-card {
|
||||
transform-origin: center center;
|
||||
will-change: transform, opacity;
|
||||
transform: translate3d(
|
||||
calc(var(--pz-dx, 0px) * var(--pz-progress)),
|
||||
calc(var(--pz-dy, 0px) * var(--pz-progress)),
|
||||
0
|
||||
)
|
||||
scale(calc(1 - var(--pz-progress) * 0.4));
|
||||
}
|
||||
|
||||
.parallax-zoom-card[data-pz-focus="true"] {
|
||||
z-index: 10;
|
||||
transform: translate3d(0, 0, 0)
|
||||
scale(calc(1 + var(--pz-progress) * (var(--pz-focus-scale) - 1)));
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="demo"
|
||||
data-composition-id="parallax-zoom-demo"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="6"
|
||||
data-start="0"
|
||||
>
|
||||
<div class="demo-canvas">
|
||||
<div class="zoom-backdrop"></div>
|
||||
<div class="hero-headline">Inspired by how people discover.</div>
|
||||
|
||||
<div class="parallax-zoom-grid">
|
||||
<div class="parallax-zoom-card pz-c0" data-pz-row="0" data-pz-col="0">
|
||||
<span class="card-glyph">◇</span>
|
||||
</div>
|
||||
<div class="parallax-zoom-card pz-c1" data-pz-row="0" data-pz-col="1">
|
||||
<span class="card-glyph">◐</span>
|
||||
</div>
|
||||
<div class="parallax-zoom-card pz-c2" data-pz-row="0" data-pz-col="2">
|
||||
<span class="card-glyph">◯</span>
|
||||
</div>
|
||||
<div class="parallax-zoom-card pz-c3" data-pz-row="0" data-pz-col="3">
|
||||
<span class="card-glyph">◑</span>
|
||||
</div>
|
||||
<div class="parallax-zoom-card pz-c4" data-pz-row="0" data-pz-col="4">
|
||||
<span class="card-glyph">▲</span>
|
||||
</div>
|
||||
|
||||
<div class="parallax-zoom-card pz-c5" data-pz-row="1" data-pz-col="0">
|
||||
<span class="card-glyph">✦</span>
|
||||
</div>
|
||||
<div class="parallax-zoom-card pz-c6" data-pz-row="1" data-pz-col="1">
|
||||
<span class="card-glyph">❖</span>
|
||||
</div>
|
||||
<div
|
||||
class="parallax-zoom-card pz-c7-focus"
|
||||
data-pz-row="1"
|
||||
data-pz-col="2"
|
||||
data-pz-focus="true"
|
||||
>
|
||||
<div class="focus-label">
|
||||
<small>EBAY PLAYBOOK</small>
|
||||
DISCOVER
|
||||
</div>
|
||||
</div>
|
||||
<div class="parallax-zoom-card pz-c8" data-pz-row="1" data-pz-col="3">
|
||||
<span class="card-glyph">✧</span>
|
||||
</div>
|
||||
<div class="parallax-zoom-card pz-c9" data-pz-row="1" data-pz-col="4">
|
||||
<span class="card-glyph">◈</span>
|
||||
</div>
|
||||
|
||||
<div class="parallax-zoom-card pz-c10" data-pz-row="2" data-pz-col="0">
|
||||
<span class="card-glyph">▼</span>
|
||||
</div>
|
||||
<div class="parallax-zoom-card pz-c11" data-pz-row="2" data-pz-col="1">
|
||||
<span class="card-glyph">◆</span>
|
||||
</div>
|
||||
<div class="parallax-zoom-card pz-c12" data-pz-row="2" data-pz-col="2">
|
||||
<span class="card-glyph">●</span>
|
||||
</div>
|
||||
<div class="parallax-zoom-card pz-c13" data-pz-row="2" data-pz-col="3">
|
||||
<span class="card-glyph">◭</span>
|
||||
</div>
|
||||
<div class="parallax-zoom-card pz-c14" data-pz-row="2" data-pz-col="4">
|
||||
<span class="card-glyph" style="color: rgba(40, 40, 40, 0.7)">◇</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const grids = document.querySelectorAll(".parallax-zoom-grid");
|
||||
grids.forEach((grid) => {
|
||||
const cards = Array.from(grid.querySelectorAll(".parallax-zoom-card"));
|
||||
if (cards.length === 0) return;
|
||||
let focal = cards.find((c) => c.dataset.pzFocus === "true");
|
||||
if (!focal) {
|
||||
focal = cards[Math.floor(cards.length / 2)];
|
||||
focal.dataset.pzFocus = "true";
|
||||
}
|
||||
const focusRow = Number(focal.dataset.pzRow ?? 0);
|
||||
const focusCol = Number(focal.dataset.pzCol ?? 0);
|
||||
const push =
|
||||
parseFloat(getComputedStyle(grid).getPropertyValue("--pz-sibling-push")) || 900;
|
||||
cards.forEach((card) => {
|
||||
if (card === focal) return;
|
||||
const row = Number(card.dataset.pzRow ?? 0);
|
||||
const col = Number(card.dataset.pzCol ?? 0);
|
||||
const dCol = col - focusCol;
|
||||
const dRow = row - focusRow;
|
||||
const mag = Math.hypot(dCol, dRow) || 1;
|
||||
const nx = (dCol / mag) * push * mag;
|
||||
const ny = (dRow / mag) * push * mag;
|
||||
card.style.setProperty("--pz-dx", `${nx}px`);
|
||||
card.style.setProperty("--pz-dy", `${ny}px`);
|
||||
});
|
||||
});
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.to(
|
||||
".parallax-zoom-card",
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.5,
|
||||
ease: "power2.out",
|
||||
stagger: { each: 0.03, from: "center" },
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
".parallax-zoom-grid",
|
||||
{ "--pz-progress": 0 },
|
||||
{
|
||||
"--pz-progress": 1,
|
||||
duration: 3.0,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
1.7,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".parallax-zoom-card:not([data-pz-focus='true'])",
|
||||
{
|
||||
opacity: 0,
|
||||
duration: 1.0,
|
||||
ease: "power2.in",
|
||||
},
|
||||
3.5,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".focus-label",
|
||||
{
|
||||
opacity: 0,
|
||||
duration: 1.0,
|
||||
ease: "power2.in",
|
||||
},
|
||||
2.5,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".hero-headline",
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.7,
|
||||
ease: "power2.out",
|
||||
},
|
||||
4.2,
|
||||
);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["parallax-zoom-demo"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,99 @@
|
||||
<!--
|
||||
Parallax Zoom — center card scales up while siblings parallax outward.
|
||||
|
||||
Inspired by the eBay Playbook hero transition: a grid of cards collapses
|
||||
as the focal card grows to fill the frame.
|
||||
|
||||
Usage:
|
||||
1. Wrap your card grid with class="parallax-zoom-grid".
|
||||
2. Mark each card with class="parallax-zoom-card".
|
||||
3. Add data-pz-row="0|1|2|..." and data-pz-col="0|1|2|..." per card
|
||||
so the snippet knows each card's grid offset.
|
||||
4. Mark the focal card with data-pz-focus="true" — this is the one
|
||||
that scales up to fill the frame.
|
||||
5. Drive --pz-progress 0 → 1 on the .parallax-zoom-grid from your
|
||||
composition's paused GSAP timeline (see example at bottom).
|
||||
|
||||
Tunable CSS variables on .parallax-zoom-grid:
|
||||
- --pz-focus-scale (default 6) maximum scale of the focused card
|
||||
- --pz-sibling-push (default 900px) outward translation strength
|
||||
- --pz-sibling-fade (default 1) sibling opacity falloff (0–1)
|
||||
-->
|
||||
|
||||
<style>
|
||||
.parallax-zoom-grid {
|
||||
--pz-progress: 0;
|
||||
--pz-focus-scale: 6;
|
||||
--pz-sibling-push: 900px;
|
||||
--pz-sibling-fade: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.parallax-zoom-card {
|
||||
transform-origin: center center;
|
||||
will-change: transform, opacity;
|
||||
transform: translate3d(
|
||||
calc(var(--pz-dx, 0px) * var(--pz-progress)),
|
||||
calc(var(--pz-dy, 0px) * var(--pz-progress)),
|
||||
0
|
||||
)
|
||||
scale(calc(1 - var(--pz-progress) * 0.4));
|
||||
opacity: calc(1 - var(--pz-progress) * var(--pz-sibling-fade));
|
||||
}
|
||||
|
||||
.parallax-zoom-card[data-pz-focus="true"] {
|
||||
z-index: 10;
|
||||
transform: translate3d(0, 0, 0)
|
||||
scale(calc(1 + var(--pz-progress) * (var(--pz-focus-scale) - 1)));
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const grids = document.querySelectorAll(".parallax-zoom-grid");
|
||||
|
||||
grids.forEach((grid) => {
|
||||
const cards = Array.from(grid.querySelectorAll(".parallax-zoom-card"));
|
||||
if (cards.length === 0) return;
|
||||
|
||||
let focal = cards.find((c) => c.dataset.pzFocus === "true");
|
||||
if (!focal) {
|
||||
focal = cards[Math.floor(cards.length / 2)];
|
||||
focal.dataset.pzFocus = "true";
|
||||
}
|
||||
|
||||
const focusRow = Number(focal.dataset.pzRow ?? 0);
|
||||
const focusCol = Number(focal.dataset.pzCol ?? 0);
|
||||
const push = parseFloat(getComputedStyle(grid).getPropertyValue("--pz-sibling-push")) || 900;
|
||||
|
||||
cards.forEach((card) => {
|
||||
if (card === focal) return;
|
||||
const row = Number(card.dataset.pzRow ?? 0);
|
||||
const col = Number(card.dataset.pzCol ?? 0);
|
||||
const dCol = col - focusCol;
|
||||
const dRow = row - focusRow;
|
||||
const mag = Math.hypot(dCol, dRow) || 1;
|
||||
const nx = (dCol / mag) * push * mag;
|
||||
const ny = (dRow / mag) * push * mag;
|
||||
card.style.setProperty("--pz-dx", `${nx}px`);
|
||||
card.style.setProperty("--pz-dy", `${ny}px`);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Timeline integration example (paste into your composition's GSAP timeline):
|
||||
|
||||
tl.fromTo(
|
||||
".parallax-zoom-grid",
|
||||
{ "--pz-progress": 0 },
|
||||
{
|
||||
"--pz-progress": 1,
|
||||
duration: 2.4,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
1.0, // start time in seconds
|
||||
);
|
||||
-->
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "parallax-zoom",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Parallax Zoom",
|
||||
"description": "Center card scales up to fill the frame while siblings parallax outward — inspired by the eBay Playbook hero transition",
|
||||
"tags": ["transition", "zoom", "parallax", "grid", "hero"],
|
||||
"files": [
|
||||
{
|
||||
"path": "parallax-zoom.html",
|
||||
"target": "compositions/components/parallax-zoom.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/parallax-zoom.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Shimmer Sweep — Demo</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="demo"
|
||||
data-composition-id="shimmer-sweep-demo"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="6"
|
||||
>
|
||||
<div class="demo-canvas">
|
||||
<!-- Example 1: Large heading -->
|
||||
<div class="shimmer-sweep-target" style="--shimmer-color: rgba(255, 255, 255, 0.5)">
|
||||
<div
|
||||
class="demo-heading"
|
||||
style="font-size: 120px; font-weight: 800; color: #ffffff; opacity: 0"
|
||||
>
|
||||
AI-Powered
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example 2: Subheading -->
|
||||
<div class="shimmer-sweep-target" style="--shimmer-color: rgba(100, 200, 255, 0.4)">
|
||||
<div
|
||||
class="demo-subheading"
|
||||
style="font-size: 48px; font-weight: 400; color: #888888; opacity: 0"
|
||||
>
|
||||
Video generation, reimagined
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example 3: Pill badge -->
|
||||
<div
|
||||
class="shimmer-sweep-target"
|
||||
style="--shimmer-color: rgba(255, 255, 255, 0.3); --shimmer-width: 30%"
|
||||
>
|
||||
<div
|
||||
class="demo-pill"
|
||||
style="
|
||||
background: linear-gradient(135deg, #1a1a2e, #2a2a4e);
|
||||
border: 1px solid #333;
|
||||
border-radius: 100px;
|
||||
padding: 16px 40px;
|
||||
font-size: 20px;
|
||||
color: #aaa;
|
||||
opacity: 0;
|
||||
"
|
||||
>
|
||||
Try it free →
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shimmer Sweep component -->
|
||||
<style>
|
||||
.demo-canvas {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: #0a0a0f;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 60px;
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
.shimmer-sweep-target {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.shimmer-sweep-target .shimmer-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
var(--shimmer-angle, 120deg),
|
||||
transparent 0%,
|
||||
transparent calc(var(--shimmer-pos, -20%) - var(--shimmer-width, 20%) / 2),
|
||||
var(--shimmer-color, rgba(255, 255, 255, 0.6)) var(--shimmer-pos, -20%),
|
||||
transparent calc(var(--shimmer-pos, -20%) + var(--shimmer-width, 20%) / 2),
|
||||
transparent 100%
|
||||
);
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// Inject shimmer masks
|
||||
document.querySelectorAll(".shimmer-sweep-target").forEach((el) => {
|
||||
if (!el.querySelector(".shimmer-mask")) {
|
||||
const mask = document.createElement("div");
|
||||
mask.className = "shimmer-mask";
|
||||
el.appendChild(mask);
|
||||
}
|
||||
});
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Fade in elements
|
||||
tl.to(".demo-heading", { opacity: 1, duration: 0.6, ease: "power2.out" }, 0.3);
|
||||
tl.to(".demo-subheading", { opacity: 1, duration: 0.6, ease: "power2.out" }, 0.6);
|
||||
tl.to(".demo-pill", { opacity: 1, duration: 0.6, ease: "power2.out" }, 0.9);
|
||||
|
||||
// First shimmer sweep across all targets
|
||||
tl.fromTo(
|
||||
".shimmer-sweep-target",
|
||||
{ "--shimmer-pos": "-20%" },
|
||||
{
|
||||
"--shimmer-pos": "120%",
|
||||
duration: 1.2,
|
||||
ease: "power2.inOut",
|
||||
stagger: 0.15,
|
||||
},
|
||||
1.5,
|
||||
);
|
||||
|
||||
// Second sweep
|
||||
tl.fromTo(
|
||||
".shimmer-sweep-target",
|
||||
{ "--shimmer-pos": "-20%" },
|
||||
{
|
||||
"--shimmer-pos": "120%",
|
||||
duration: 1.0,
|
||||
ease: "power2.inOut",
|
||||
stagger: 0.1,
|
||||
},
|
||||
3.5,
|
||||
);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["shimmer-sweep-demo"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "shimmer-sweep",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Shimmer Sweep",
|
||||
"description": "Animated light sweep across text or elements using a CSS gradient mask — ideal for AI accents and premium reveals",
|
||||
"tags": ["text", "shimmer", "highlight", "effect"],
|
||||
"files": [
|
||||
{
|
||||
"path": "shimmer-sweep.html",
|
||||
"target": "compositions/components/shimmer-sweep.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"preview": {
|
||||
"poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/shimmer-sweep.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<!--
|
||||
Shimmer Sweep — animated light pass across text or elements.
|
||||
|
||||
Usage: place this snippet AFTER your .shimmer-sweep-target elements.
|
||||
Wrap your text element with class="shimmer-sweep-target" and
|
||||
paste this snippet into your composition. The GSAP timeline drives
|
||||
the sweep position for deterministic frame-by-frame rendering.
|
||||
|
||||
Customize:
|
||||
- --shimmer-color: highlight color (default rgba(255,255,255,0.6))
|
||||
- --shimmer-width: highlight band width as % (default 20%)
|
||||
- --shimmer-angle: sweep angle (default 120deg)
|
||||
- Timeline duration/ease: adjust in the script below
|
||||
-->
|
||||
|
||||
<style>
|
||||
.shimmer-sweep-target {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.shimmer-sweep-target .shimmer-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
var(--shimmer-angle, 120deg),
|
||||
transparent 0%,
|
||||
transparent calc(var(--shimmer-pos, -20%) - var(--shimmer-width, 20%) / 2),
|
||||
var(--shimmer-color, rgba(255, 255, 255, 0.6)) var(--shimmer-pos, -20%),
|
||||
transparent calc(var(--shimmer-pos, -20%) + var(--shimmer-width, 20%) / 2),
|
||||
transparent 100%
|
||||
);
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// Auto-inject .shimmer-mask into every .shimmer-sweep-target
|
||||
document.querySelectorAll(".shimmer-sweep-target").forEach((el) => {
|
||||
if (!el.querySelector(".shimmer-mask")) {
|
||||
const mask = document.createElement("div");
|
||||
mask.className = "shimmer-mask";
|
||||
el.appendChild(mask);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Timeline integration example (add to your composition's GSAP timeline):
|
||||
|
||||
// Single sweep from left to right
|
||||
tl.fromTo(".shimmer-sweep-target", {
|
||||
"--shimmer-pos": "-20%",
|
||||
}, {
|
||||
"--shimmer-pos": "120%",
|
||||
duration: 1.2,
|
||||
ease: "power2.inOut",
|
||||
stagger: 0.15,
|
||||
}, startTime);
|
||||
-->
|
||||
@@ -0,0 +1,286 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Texture Mask Text - Demo</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #111;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="demo"
|
||||
data-composition-id="texture-mask-text-demo"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="5"
|
||||
data-start="0"
|
||||
>
|
||||
<div class="demo-canvas">
|
||||
<div class="header">
|
||||
<div class="title">Texture Mask Text</div>
|
||||
<div class="count">66 ambientCG luminance masks</div>
|
||||
</div>
|
||||
<div class="groups" id="texture-groups"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#demo {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
font-family:
|
||||
Arial Black,
|
||||
Arial,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.hf-texture-text {
|
||||
color: #fff;
|
||||
-webkit-mask-size: var(--mask-size, cover);
|
||||
mask-size: var(--mask-size, cover);
|
||||
-webkit-mask-position: var(--mask-position, center);
|
||||
mask-position: var(--mask-position, center);
|
||||
-webkit-mask-mode: luminance;
|
||||
mask-mode: luminance;
|
||||
filter: contrast(var(--mask-contrast, 1));
|
||||
}
|
||||
|
||||
.demo-canvas {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: #111;
|
||||
padding: 42px 44px 38px;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 84px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 34px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.groups {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 22px 24px;
|
||||
width: 1832px;
|
||||
height: 898px;
|
||||
}
|
||||
|
||||
.group-panel {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.74);
|
||||
}
|
||||
|
||||
.group-count {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.52);
|
||||
}
|
||||
|
||||
.group-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px 10px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
min-width: 0;
|
||||
height: 66px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 100%;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.shadow-wrap {
|
||||
width: 100%;
|
||||
filter: drop-shadow(2px 3px 4px rgba(0, 0, 0, 0.46));
|
||||
}
|
||||
|
||||
.sample {
|
||||
width: 100%;
|
||||
font-family:
|
||||
Impact,
|
||||
Arial Black,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-size: 31px;
|
||||
line-height: 0.9;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const textureMaskTextGroups = [
|
||||
[
|
||||
"Masonry",
|
||||
"brick|Brick|MORTAR,bricks-104|Bricks 104|BRICK,bricks-102|Bricks 102|BRICK,bricks-101|Bricks 101|BRICK,bricks-075-a|Bricks 075 A|BRICK,concrete|Concrete|SCORED,concrete-034|Concrete 034|CONCRETE,concrete-047-a|Concrete 047 A|CONCRETE,concrete-046|Concrete 046|CONCRETE,concrete-042-a|Concrete 042 A|CONCRETE,plaster-001|Plaster 001|PLASTER,painted-plaster-017|Painted Plaster 017|PLASTER",
|
||||
],
|
||||
[
|
||||
"Stone",
|
||||
"rock|Rock|RUINED,rock-063|Rock 063|ROCK,rock-058|Rock 058|ROCK,onyx|Onyx|VEINED,marble-012|Marble 012|MARBLE,marble-016|Marble 016|MARBLE,travertine-009|Travertine 009|STONE,paving-stones-150|Paving Stones 150|STONE,paving-stones-138|Paving Stones 138|STONE,tiles-138|Tiles 138|TILE",
|
||||
],
|
||||
[
|
||||
"Ground / Road",
|
||||
"ground-103|Ground 103|GROUND,ground-037|Ground 037|GROUND,ground-054|Ground 054|GROUND,ground-104|Ground 104|GROUND,ground-068|Ground 068|GROUND,ground-080|Ground 080|GROUND,road-012-a|Road 012 A|ROAD,road-008-a|Road 008 A|ROAD,road-007|Road 007|ROAD,road-013-a|Road 013 A|ROAD,road-012-b|Road 012 B|ROAD,road-009-c|Road 009 C|ROAD,asphalt-031|Asphalt 031|ASPHALT",
|
||||
],
|
||||
[
|
||||
"Wood",
|
||||
"wood|Wood|SLICED,wood-094|Wood 094|WOOD,wood-092|Wood 092|WOOD,wood-051|Wood 051|WOOD,wood-066|Wood 066|WOOD,wood-049|Wood 049|WOOD,wood-058|Wood 058|WOOD,wood-floor-051|Wood Floor 051|FLOOR,wood-floor-064|Wood Floor 064|FLOOR,wood-floor-070|Wood Floor 070|FLOOR,bark-014|Bark 014|BARK",
|
||||
],
|
||||
[
|
||||
"Metal",
|
||||
"metal|Metal|FADING,metal-049-a|Metal 049 A|METAL,metal-055-a|Metal 055 A|METAL,metal-046-b|Metal 046 B|METAL,metal-061-b|Metal 061 B|METAL,metal-048-a|Metal 048 A|METAL,metal-032|Metal 032|METAL,metal-041-a|Metal 041 A|METAL,metal-038|Metal 038|METAL,diamond-plate-009|Diamond Plate 009|PLATE",
|
||||
],
|
||||
[
|
||||
"Organic / Soft",
|
||||
"lava|Lava|MOLTEN,grass-005|Grass 005|GRASS,grass-001|Grass 001|GRASS,grass-004|Grass 004|GRASS,carpet|Carpet|WOVEN,fabric-083|Fabric 083|FABRIC,snow|Snow|THAW,snow-015|Snow 015|SNOW,leather-037|Leather 037|LEATHER,fabric-080|Fabric 080|FABRIC",
|
||||
],
|
||||
];
|
||||
const textureGroups = document.getElementById("texture-groups");
|
||||
for (const [groupName, rows] of textureMaskTextGroups) {
|
||||
const items = rows.split(",").map((row) => {
|
||||
const [slug, title, sample] = row.split("|");
|
||||
return { slug, title, sample };
|
||||
});
|
||||
const panel = document.createElement("section");
|
||||
panel.className = "group-panel";
|
||||
|
||||
const groupHeader = document.createElement("div");
|
||||
groupHeader.className = "group-header";
|
||||
|
||||
const name = document.createElement("div");
|
||||
name.className = "group-name";
|
||||
name.textContent = groupName;
|
||||
|
||||
const count = document.createElement("div");
|
||||
count.className = "group-count";
|
||||
count.textContent = `${items.length} masks`;
|
||||
|
||||
groupHeader.append(name, count);
|
||||
|
||||
const groupGrid = document.createElement("div");
|
||||
groupGrid.className = "group-grid";
|
||||
|
||||
for (const item of items) {
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "cell";
|
||||
|
||||
const label = document.createElement("div");
|
||||
label.className = "label";
|
||||
label.textContent = item.title;
|
||||
|
||||
const shadowWrap = document.createElement("div");
|
||||
shadowWrap.className = "shadow-wrap";
|
||||
|
||||
const sample = document.createElement("div");
|
||||
sample.className = "hf-texture-text sample";
|
||||
sample.style.webkitMaskImage = `url("masks/${item.slug}.png")`;
|
||||
sample.style.maskImage = `url("masks/${item.slug}.png")`;
|
||||
sample.textContent = item.sample;
|
||||
|
||||
shadowWrap.appendChild(sample);
|
||||
cell.append(label, shadowWrap);
|
||||
groupGrid.appendChild(cell);
|
||||
}
|
||||
|
||||
panel.append(groupHeader, groupGrid);
|
||||
textureGroups.appendChild(panel);
|
||||
}
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
const textureMaskTextTimeline = {
|
||||
play() {},
|
||||
pause() {},
|
||||
seek() {},
|
||||
time() {
|
||||
return 0;
|
||||
},
|
||||
duration() {
|
||||
return 5;
|
||||
},
|
||||
add() {},
|
||||
paused() {},
|
||||
set() {},
|
||||
};
|
||||
window.__timelines["texture-mask-text-demo"] = textureMaskTextTimeline;
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 104 KiB |