mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 16:42:27 +00:00
feat(registry): add kinetic center build component (#3269)
* feat(registry): add kinetic center build component * chore: regenerate search vectors for kinetic-center-build Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(registry): load all kinetic center build font weights * chore(catalog): regenerate local search vectors --------- Co-authored-by: miga-heygen <miguel.sierra_miga@heygen.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
miga-heygen
Miguel Ángel
parent
aab3bfcd10
commit
f498be94c2
@@ -0,0 +1,645 @@
|
||||
---
|
||||
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";
|
||||
|
||||
<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>
|
||||
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add kinetic-center-build" item="kinetic-center-build" />
|
||||
|
||||
That writes one file: `compositions/components/kinetic-center-build.html`.
|
||||
|
||||
## 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)
|
||||
Reference in New Issue
Block a user