mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-12 20:47:30 +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>
646 lines
25 KiB
Plaintext
646 lines
25 KiB
Plaintext
---
|
|
title: "Kinetic Center Build"
|
|
description: "Words enter from the right and push the growing phrase left until the final line locks centered."
|
|
---
|
|
|
|
import { InstallCommand } from "/snippets/install-command.jsx";
|
|
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
|
|
|
## Install
|
|
|
|
<InstallCommand command="npx hyperframes add kinetic-center-build" item="kinetic-center-build" />
|
|
|
|
That writes one file: `compositions/components/kinetic-center-build.html`.
|
|
|
|
<VariablesExplorer
|
|
previewSrc="/public/catalog/components/kinetic-center-build.json"
|
|
compositionId="kinetic-center-build"
|
|
compositionSrc="compositions/components/kinetic-center-build.html"
|
|
variables={[{"id":"text","type":"string","role":"content","label":"Text","description":"Short phrase that builds one word at a time.","default":"Words push left."},{"id":"font_size","type":"number","role":"style","label":"Font size","description":"Maximum text size in pixels. Longer phrases shrink to stay inside the frame.","default":72,"min":36,"max":160,"step":2,"unit":"px"},{"id":"entry_offset","type":"number","role":"motion","label":"Entry offset","description":"Horizontal distance each new word travels before it lands.","default":88,"min":20,"max":160,"step":4,"unit":"px"},{"id":"color","type":"color","role":"style","label":"Color","description":"Text color for the full phrase.","default":"#171717"},{"id":"font_weight","type":"enum","role":"style","label":"Font weight","description":"Weight used by every word in the phrase.","default":"600","options":[{"value":"500","label":"Medium"},{"value":"600","label":"Semibold"},{"value":"700","label":"Bold"},{"value":"800","label":"Extra bold"}]}]}
|
|
>
|
|
|
|
```html kinetic-center-build.html
|
|
<!doctype html>
|
|
<!--
|
|
Kinetic Center Build
|
|
|
|
Words enter from the right while the existing phrase shifts left, keeping
|
|
every intermediate phrase centered until the final line locks in place.
|
|
|
|
Native HyperFrames port prepared through Hyfrme from:
|
|
https://github.com/Remocn/remocn/blob/ea730a20b4ab09430ee7292aebc847c002375151/registry/remocn/kinetic-center-build/index.tsx
|
|
|
|
MIT License
|
|
|
|
Copyright (c) 2026 Remocn
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
-->
|
|
<html
|
|
lang="en"
|
|
data-composition-id="kinetic-center-build"
|
|
data-composition-duration="2"
|
|
data-composition-variables='[
|
|
{ "id": "text", "type": "string", "role": "content", "label": "Text", "description": "Short phrase that builds one word at a time.", "default": "Words push left." },
|
|
{ "id": "font_size", "type": "number", "role": "style", "label": "Font size", "description": "Maximum text size in pixels. Longer phrases shrink to stay inside the frame.", "default": 72, "min": 36, "max": 160, "step": 2, "unit": "px" },
|
|
{ "id": "entry_offset", "type": "number", "role": "motion", "label": "Entry offset", "description": "Horizontal distance each new word travels before it lands.", "default": 88, "min": 20, "max": 160, "step": 4, "unit": "px" },
|
|
{ "id": "color", "type": "color", "role": "style", "label": "Color", "description": "Text color for the full phrase.", "default": "#171717" },
|
|
{ "id": "font_weight", "type": "enum", "role": "style", "label": "Font weight", "description": "Weight used by every word in the phrase.", "default": "600", "options": [{ "value": "500", "label": "Medium" }, { "value": "600", "label": "Semibold" }, { "value": "700", "label": "Bold" }, { "value": "800", "label": "Extra bold" }] }
|
|
]'
|
|
>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>Kinetic Center Build</title>
|
|
</head>
|
|
<body>
|
|
<template>
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://fonts.googleapis.com/css2?family=Geist:wght@500;600;700;800&display=block"
|
|
/>
|
|
|
|
<style>
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
#root {
|
|
position: absolute;
|
|
inset: 0;
|
|
overflow: hidden;
|
|
container-type: size;
|
|
isolation: isolate;
|
|
color: var(--color, #171717);
|
|
font-family: var(--font-display, Geist, system-ui, sans-serif);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.kcb-clip,
|
|
.kcb-stage {
|
|
position: absolute;
|
|
inset: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.kcb-stage {
|
|
letter-spacing: -0.03em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.kcb-word {
|
|
--kcb-blur: 0px;
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
display: inline-block;
|
|
color: inherit;
|
|
line-height: 1;
|
|
white-space: nowrap;
|
|
opacity: 0;
|
|
filter: blur(var(--kcb-blur));
|
|
backface-visibility: hidden;
|
|
will-change: transform, filter, opacity;
|
|
}
|
|
</style>
|
|
|
|
<div id="root" data-composition-id="kinetic-center-build" data-duration="2" data-fps="30">
|
|
<div
|
|
id="kcb-clip"
|
|
class="kcb-clip clip"
|
|
data-start="0"
|
|
data-duration="2"
|
|
data-track-index="0"
|
|
>
|
|
<div id="kcb-stage" class="kcb-stage" role="img"></div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomEase.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
"use strict";
|
|
|
|
var root = document.getElementById("root");
|
|
var stage = document.getElementById("kcb-stage");
|
|
var vars =
|
|
window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: {};
|
|
|
|
function numberBetween(value, fallback, min, max) {
|
|
var parsed = Number(value);
|
|
if (!Number.isFinite(parsed)) return fallback;
|
|
return Math.min(max, Math.max(min, parsed));
|
|
}
|
|
|
|
var text = typeof vars.text === "string" ? vars.text.trim() : "";
|
|
if (!text) text = "Words push left.";
|
|
|
|
var words = text.split(/\s+/);
|
|
var requestedFontSize = numberBetween(vars.font_size, 72, 36, 160);
|
|
var entryOffset = numberBetween(vars.entry_offset, 88, 20, 160);
|
|
var fontWeight =
|
|
vars.font_weight === "500" || vars.font_weight === "700" || vars.font_weight === "800"
|
|
? vars.font_weight
|
|
: "600";
|
|
|
|
stage.style.fontWeight = fontWeight;
|
|
stage.setAttribute("aria-label", text);
|
|
|
|
var canvas = document.createElement("canvas");
|
|
var context = canvas.getContext("2d");
|
|
var measureFont = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
var gapRatio = 10 / 72;
|
|
|
|
function measure(fontSize) {
|
|
if (!context) {
|
|
return words.map(function (word) {
|
|
return word.length * fontSize * 0.55;
|
|
});
|
|
}
|
|
context.font = fontWeight + " " + fontSize + "px " + measureFont;
|
|
return words.map(function (word) {
|
|
return context.measureText(word).width;
|
|
});
|
|
}
|
|
|
|
var fontSize = requestedFontSize;
|
|
var widths = measure(fontSize);
|
|
var gap = fontSize * gapRatio;
|
|
var fullWidth = widths.reduce(
|
|
function (total, width) {
|
|
return total + width;
|
|
},
|
|
gap * Math.max(0, words.length - 1),
|
|
);
|
|
var maxWidth = Math.max(1, root.clientWidth * 0.9);
|
|
|
|
if (fullWidth > maxWidth) {
|
|
fontSize = Math.max(24, fontSize * (maxWidth / fullWidth));
|
|
widths = measure(fontSize);
|
|
gap = fontSize * gapRatio;
|
|
}
|
|
|
|
stage.style.fontSize = fontSize + "px";
|
|
|
|
var positionsAt = [];
|
|
for (var count = 1; count <= words.length; count += 1) {
|
|
var total = gap * (count - 1);
|
|
for (var widthIndex = 0; widthIndex < count; widthIndex += 1) {
|
|
total += widths[widthIndex];
|
|
}
|
|
|
|
var cursor = -total / 2;
|
|
var positions = [];
|
|
for (var positionIndex = 0; positionIndex < count; positionIndex += 1) {
|
|
positions.push(cursor + widths[positionIndex] / 2);
|
|
cursor += widths[positionIndex] + gap;
|
|
}
|
|
positionsAt.push(positions);
|
|
}
|
|
|
|
var elements = words.map(function (word, index) {
|
|
var element = document.createElement("span");
|
|
element.id = "kcb-word-" + index;
|
|
element.className = "kcb-word";
|
|
element.textContent = word;
|
|
element.setAttribute("aria-hidden", "true");
|
|
stage.appendChild(element);
|
|
return element;
|
|
});
|
|
|
|
gsap.registerPlugin(CustomEase);
|
|
var entryEase = CustomEase.create("kcb-entry-ease", "M0,0 C0.2,0.8 0.2,1 1,1");
|
|
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "2"));
|
|
var firstDuration = 10 / 30;
|
|
var pushDuration = 13 / 30;
|
|
var authoredBuildDuration =
|
|
firstDuration + Math.max(0, words.length - 1) * pushDuration;
|
|
var availableBuildDuration = Math.max(0.25, duration - 0.5);
|
|
var timingScale = Math.min(1, availableBuildDuration / authoredBuildDuration);
|
|
firstDuration *= timingScale;
|
|
pushDuration *= timingScale;
|
|
|
|
var tl = gsap.timeline({ paused: true });
|
|
|
|
elements.forEach(function (element, index) {
|
|
var targetX = positionsAt[index][index];
|
|
var start = index === 0 ? 0 : firstDuration + (index - 1) * pushDuration;
|
|
var moveDuration = index === 0 ? firstDuration : pushDuration;
|
|
var fromX = index === 0 ? targetX : targetX + entryOffset;
|
|
var fromY = index === 0 ? 6 : 0;
|
|
|
|
tl.fromTo(
|
|
element,
|
|
{
|
|
x: fromX,
|
|
y: fromY,
|
|
xPercent: -50,
|
|
yPercent: -50,
|
|
scale: 0.992,
|
|
opacity: 0,
|
|
"--kcb-blur": "3.5px",
|
|
},
|
|
{
|
|
x: targetX,
|
|
y: 0,
|
|
xPercent: -50,
|
|
yPercent: -50,
|
|
scale: 1,
|
|
opacity: 1,
|
|
"--kcb-blur": "0px",
|
|
duration: moveDuration,
|
|
ease: entryEase,
|
|
immediateRender: index === 0,
|
|
},
|
|
start,
|
|
);
|
|
|
|
if (index === 0) return;
|
|
|
|
for (var priorIndex = 0; priorIndex < index; priorIndex += 1) {
|
|
var priorElement = elements[priorIndex];
|
|
var nextX = positionsAt[index][priorIndex];
|
|
var blurPeak = start + moveDuration / 2;
|
|
|
|
tl.to(priorElement, { x: nextX, duration: moveDuration, ease: entryEase }, start);
|
|
tl.to(
|
|
priorElement,
|
|
{ "--kcb-blur": "0.8px", duration: moveDuration / 2, ease: "none" },
|
|
start,
|
|
);
|
|
tl.to(
|
|
priorElement,
|
|
{ "--kcb-blur": "0px", duration: moveDuration / 2, ease: "none" },
|
|
blurPeak,
|
|
);
|
|
}
|
|
});
|
|
|
|
tl.seek(0);
|
|
window.__timelines = window.__timelines || {};
|
|
window.__timelines["kinetic-center-build"] = tl;
|
|
})();
|
|
</script>
|
|
</div>
|
|
</template>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
</VariablesExplorer>
|
|
|
|
## Paste it into your composition
|
|
|
|
Open `compositions/components/kinetic-center-build.html` and copy what is inside into your own composition.
|
|
|
|
A component has no size or duration of its own. It takes both from the composition
|
|
you paste it into.
|
|
|
|
## Variables
|
|
|
|
Every one of these has a default, so the piece works untouched. Set the ones you
|
|
want to change on the element:
|
|
|
|
| Variable | Default | Accepts | What it does |
|
|
| --- | --- | --- | --- |
|
|
| `text` | `Words push left.` | string | Short phrase that builds one word at a time. |
|
|
| `font_size` | `72` | 36px to 160px, step 2px | Maximum text size in pixels. Longer phrases shrink to stay inside the frame. |
|
|
| `entry_offset` | `88` | 20px to 160px, step 4px | Horizontal distance each new word travels before it lands. |
|
|
| `color` | `#171717` | color | Text color for the full phrase. |
|
|
| `font_weight` | `600` | `500`, `600`, `700`, `800` | Weight used by every word in the phrase. |
|
|
|
|
Set them with `data-variable-values` on the element that mounts it. These are the
|
|
defaults, so this behaves exactly like the preview above until you change one:
|
|
|
|
```html wrap
|
|
<div
|
|
data-composition-id="kinetic-center-build"
|
|
data-composition-src="compositions/components/kinetic-center-build.html"
|
|
data-variable-values='{"text":"Words push left.","font_size":72,"entry_offset":88,"color":"#171717","font_weight":"600"}'
|
|
></div>
|
|
```
|
|
|
|
## Source
|
|
|
|
<Accordion title="kinetic-center-build.html">
|
|
|
|
```html
|
|
<!doctype html>
|
|
<!--
|
|
Kinetic Center Build
|
|
|
|
Words enter from the right while the existing phrase shifts left, keeping
|
|
every intermediate phrase centered until the final line locks in place.
|
|
|
|
Native HyperFrames port prepared through Hyfrme from:
|
|
https://github.com/Remocn/remocn/blob/ea730a20b4ab09430ee7292aebc847c002375151/registry/remocn/kinetic-center-build/index.tsx
|
|
|
|
MIT License
|
|
|
|
Copyright (c) 2026 Remocn
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
-->
|
|
<html
|
|
lang="en"
|
|
data-composition-id="kinetic-center-build"
|
|
data-composition-duration="2"
|
|
data-composition-variables='[
|
|
{ "id": "text", "type": "string", "role": "content", "label": "Text", "description": "Short phrase that builds one word at a time.", "default": "Words push left." },
|
|
{ "id": "font_size", "type": "number", "role": "style", "label": "Font size", "description": "Maximum text size in pixels. Longer phrases shrink to stay inside the frame.", "default": 72, "min": 36, "max": 160, "step": 2, "unit": "px" },
|
|
{ "id": "entry_offset", "type": "number", "role": "motion", "label": "Entry offset", "description": "Horizontal distance each new word travels before it lands.", "default": 88, "min": 20, "max": 160, "step": 4, "unit": "px" },
|
|
{ "id": "color", "type": "color", "role": "style", "label": "Color", "description": "Text color for the full phrase.", "default": "#171717" },
|
|
{ "id": "font_weight", "type": "enum", "role": "style", "label": "Font weight", "description": "Weight used by every word in the phrase.", "default": "600", "options": [{ "value": "500", "label": "Medium" }, { "value": "600", "label": "Semibold" }, { "value": "700", "label": "Bold" }, { "value": "800", "label": "Extra bold" }] }
|
|
]'
|
|
>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>Kinetic Center Build</title>
|
|
</head>
|
|
<body>
|
|
<template>
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://fonts.googleapis.com/css2?family=Geist:wght@500;600;700;800&display=block"
|
|
/>
|
|
|
|
<style>
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
#root {
|
|
position: absolute;
|
|
inset: 0;
|
|
overflow: hidden;
|
|
container-type: size;
|
|
isolation: isolate;
|
|
color: var(--color, #171717);
|
|
font-family: var(--font-display, Geist, system-ui, sans-serif);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.kcb-clip,
|
|
.kcb-stage {
|
|
position: absolute;
|
|
inset: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.kcb-stage {
|
|
letter-spacing: -0.03em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.kcb-word {
|
|
--kcb-blur: 0px;
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
display: inline-block;
|
|
color: inherit;
|
|
line-height: 1;
|
|
white-space: nowrap;
|
|
opacity: 0;
|
|
filter: blur(var(--kcb-blur));
|
|
backface-visibility: hidden;
|
|
will-change: transform, filter, opacity;
|
|
}
|
|
</style>
|
|
|
|
<div id="root" data-composition-id="kinetic-center-build" data-duration="2" data-fps="30">
|
|
<div
|
|
id="kcb-clip"
|
|
class="kcb-clip clip"
|
|
data-start="0"
|
|
data-duration="2"
|
|
data-track-index="0"
|
|
>
|
|
<div id="kcb-stage" class="kcb-stage" role="img"></div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomEase.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
"use strict";
|
|
|
|
var root = document.getElementById("root");
|
|
var stage = document.getElementById("kcb-stage");
|
|
var vars =
|
|
window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: {};
|
|
|
|
function numberBetween(value, fallback, min, max) {
|
|
var parsed = Number(value);
|
|
if (!Number.isFinite(parsed)) return fallback;
|
|
return Math.min(max, Math.max(min, parsed));
|
|
}
|
|
|
|
var text = typeof vars.text === "string" ? vars.text.trim() : "";
|
|
if (!text) text = "Words push left.";
|
|
|
|
var words = text.split(/\s+/);
|
|
var requestedFontSize = numberBetween(vars.font_size, 72, 36, 160);
|
|
var entryOffset = numberBetween(vars.entry_offset, 88, 20, 160);
|
|
var fontWeight =
|
|
vars.font_weight === "500" || vars.font_weight === "700" || vars.font_weight === "800"
|
|
? vars.font_weight
|
|
: "600";
|
|
|
|
stage.style.fontWeight = fontWeight;
|
|
stage.setAttribute("aria-label", text);
|
|
|
|
var canvas = document.createElement("canvas");
|
|
var context = canvas.getContext("2d");
|
|
var measureFont = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
var gapRatio = 10 / 72;
|
|
|
|
function measure(fontSize) {
|
|
if (!context) {
|
|
return words.map(function (word) {
|
|
return word.length * fontSize * 0.55;
|
|
});
|
|
}
|
|
context.font = fontWeight + " " + fontSize + "px " + measureFont;
|
|
return words.map(function (word) {
|
|
return context.measureText(word).width;
|
|
});
|
|
}
|
|
|
|
var fontSize = requestedFontSize;
|
|
var widths = measure(fontSize);
|
|
var gap = fontSize * gapRatio;
|
|
var fullWidth = widths.reduce(
|
|
function (total, width) {
|
|
return total + width;
|
|
},
|
|
gap * Math.max(0, words.length - 1),
|
|
);
|
|
var maxWidth = Math.max(1, root.clientWidth * 0.9);
|
|
|
|
if (fullWidth > maxWidth) {
|
|
fontSize = Math.max(24, fontSize * (maxWidth / fullWidth));
|
|
widths = measure(fontSize);
|
|
gap = fontSize * gapRatio;
|
|
}
|
|
|
|
stage.style.fontSize = fontSize + "px";
|
|
|
|
var positionsAt = [];
|
|
for (var count = 1; count <= words.length; count += 1) {
|
|
var total = gap * (count - 1);
|
|
for (var widthIndex = 0; widthIndex < count; widthIndex += 1) {
|
|
total += widths[widthIndex];
|
|
}
|
|
|
|
var cursor = -total / 2;
|
|
var positions = [];
|
|
for (var positionIndex = 0; positionIndex < count; positionIndex += 1) {
|
|
positions.push(cursor + widths[positionIndex] / 2);
|
|
cursor += widths[positionIndex] + gap;
|
|
}
|
|
positionsAt.push(positions);
|
|
}
|
|
|
|
var elements = words.map(function (word, index) {
|
|
var element = document.createElement("span");
|
|
element.id = "kcb-word-" + index;
|
|
element.className = "kcb-word";
|
|
element.textContent = word;
|
|
element.setAttribute("aria-hidden", "true");
|
|
stage.appendChild(element);
|
|
return element;
|
|
});
|
|
|
|
gsap.registerPlugin(CustomEase);
|
|
var entryEase = CustomEase.create("kcb-entry-ease", "M0,0 C0.2,0.8 0.2,1 1,1");
|
|
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "2"));
|
|
var firstDuration = 10 / 30;
|
|
var pushDuration = 13 / 30;
|
|
var authoredBuildDuration =
|
|
firstDuration + Math.max(0, words.length - 1) * pushDuration;
|
|
var availableBuildDuration = Math.max(0.25, duration - 0.5);
|
|
var timingScale = Math.min(1, availableBuildDuration / authoredBuildDuration);
|
|
firstDuration *= timingScale;
|
|
pushDuration *= timingScale;
|
|
|
|
var tl = gsap.timeline({ paused: true });
|
|
|
|
elements.forEach(function (element, index) {
|
|
var targetX = positionsAt[index][index];
|
|
var start = index === 0 ? 0 : firstDuration + (index - 1) * pushDuration;
|
|
var moveDuration = index === 0 ? firstDuration : pushDuration;
|
|
var fromX = index === 0 ? targetX : targetX + entryOffset;
|
|
var fromY = index === 0 ? 6 : 0;
|
|
|
|
tl.fromTo(
|
|
element,
|
|
{
|
|
x: fromX,
|
|
y: fromY,
|
|
xPercent: -50,
|
|
yPercent: -50,
|
|
scale: 0.992,
|
|
opacity: 0,
|
|
"--kcb-blur": "3.5px",
|
|
},
|
|
{
|
|
x: targetX,
|
|
y: 0,
|
|
xPercent: -50,
|
|
yPercent: -50,
|
|
scale: 1,
|
|
opacity: 1,
|
|
"--kcb-blur": "0px",
|
|
duration: moveDuration,
|
|
ease: entryEase,
|
|
immediateRender: index === 0,
|
|
},
|
|
start,
|
|
);
|
|
|
|
if (index === 0) return;
|
|
|
|
for (var priorIndex = 0; priorIndex < index; priorIndex += 1) {
|
|
var priorElement = elements[priorIndex];
|
|
var nextX = positionsAt[index][priorIndex];
|
|
var blurPeak = start + moveDuration / 2;
|
|
|
|
tl.to(priorElement, { x: nextX, duration: moveDuration, ease: entryEase }, start);
|
|
tl.to(
|
|
priorElement,
|
|
{ "--kcb-blur": "0.8px", duration: moveDuration / 2, ease: "none" },
|
|
start,
|
|
);
|
|
tl.to(
|
|
priorElement,
|
|
{ "--kcb-blur": "0px", duration: moveDuration / 2, ease: "none" },
|
|
blurPeak,
|
|
);
|
|
}
|
|
});
|
|
|
|
tl.seek(0);
|
|
window.__timelines = window.__timelines || {};
|
|
window.__timelines["kinetic-center-build"] = tl;
|
|
})();
|
|
</script>
|
|
</div>
|
|
</template>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
</Accordion>
|
|
|
|
{/* hf:generated-footer */}
|
|
|
|
Tagged `motion-primitive` `text-effects` `kinetic-type` `reveal` `remocn-port`.
|
|
|
|
Created by [Remocn](https://github.com/Remocn/remocn).
|
|
|
|
## Related topics
|
|
|
|
- [Browse the complete Catalog](/catalog)
|
|
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
|
- [Build a richer composition](/go-further)
|