mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-06 14:47:29 +00:00
The generated catalog pages rendered the "Source" accordion title as a
JSX expression, `<Accordion title={`name.html`}>`. Mintlify's search
indexer stringifies expression attribute nodes rather than evaluating
them, so every one of the 399 catalog pages surfaced its Source section
in search results as "[object Object]" (visible on the Scroll Feed page,
among others).
The template literal carried no escaping benefit: every title is a
single-segment `name.html` path, and a double-quoted JSX string handles
it directly. Introduce `mdxStringAttribute`, which emits a plain
`name="value"` attribute and escapes `& " < { }` as HTML character
references (MDX decodes references inside quoted attribute values, and
unlike JSON.stringify it does not rely on backslash escapes, which JSX
strings do not honour). Use it for both the Source accordion title and
the preview iframe title, and regenerate the catalog pages.
Regenerated with `npx tsx scripts/generate-catalog-pages.ts`; the only
change across the 399 pages is the one Accordion title line each.
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
279 lines
8.9 KiB
Plaintext
279 lines
8.9 KiB
Plaintext
---
|
|
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"
|
|
---
|
|
|
|
import { InstallCommand } from "/snippets/install-command.jsx";
|
|
|
|
## Install
|
|
|
|
<InstallCommand command="npx hyperframes add morph-text" item="morph-text" />
|
|
|
|
That writes one file: `compositions/components/morph-text.html`.
|
|
|
|
<iframe
|
|
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
|
title="morph-text preview"
|
|
loading="lazy"
|
|
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@latest/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/components/morph-text.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");document.body.appendChild(p)});<\/script></body></html>`}
|
|
/>
|
|
|
|
## Source
|
|
|
|
<Accordion title="morph-text.html">
|
|
|
|
```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: var(--bg, #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 = "var(--font-display, " + wA.font + ")";
|
|
elA.style.color = "var(--fg, " + wA.color + ")";
|
|
|
|
elB.textContent = wB.text;
|
|
elB.style.fontFamily = "var(--font-display, " + wB.font + ")";
|
|
elB.style.color = "var(--fg, " + 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 = "var(--font-display, " + words[0].font + ")";
|
|
elA.style.color = "var(--fg, " + 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>
|
|
```
|
|
|
|
</Accordion>
|
|
|
|
## Usage
|
|
|
|
Open `compositions/components/morph-text.html` and paste its contents into your composition.
|
|
|
|
Edit the `<ol id="morph-words">` list to change the statements that cycle through. Each `<li>` accepts `data-font` (CSS font-family) and `data-color` (hex) attributes. Adjust `data-morph-speed` (seconds per transition) and `data-morph-pause` (hold time) on the root element.
|
|
|
|
{/* hf:generated-footer */}
|
|
|
|
Tagged `text` `text-effect` `typography` `morph` `gooey` `kinetic` `animation`.
|
|
|
|
## Related topics
|
|
|
|
- [Browse the complete Catalog](/catalog)
|
|
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
|
- [Build a richer composition](/go-further)
|