mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
* fix(docs): load the player from latest, not a pinned minor The catalog pages pinned the player CDN URL to a minor line, and that pin sat one line behind after the last release. Every page kept rendering, on the older build, so nothing surfaced it: the only symptom was that a fix published to npm never appeared on the docs. The generator derived its pin from the player's package.json, which is correct only if every page is regenerated on the release that moves it. That is the step that did not happen, and it has to happen across 175 generated pages plus three hand-written files for the pin to be true. A version carried in step across 178 places will be stale, and stale here is silent. Ask for latest instead and there is nothing to carry. This costs the ability to hold the docs back from a bad player release. Paid deliberately: the pin did not buy that either, it only delayed the good releases too. A test asserts no pinned version comes back, and fails if it stops finding the references at all, so it cannot pass by matching nothing. * refactor(scripts): list tracked files instead of walking the tree The pin guard hand-rolled a recursive directory walk with its own skip list and size cap, which the audit flagged: helpers living in a test file earn no coverage, so their complexity lands straight on the CRAP score. git already knows which files to read, and ignores node_modules and build output for us, so one call replaces the walker and both findings go away.
330 lines
13 KiB
Plaintext
330 lines
13 KiB
Plaintext
---
|
|
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"
|
|
---
|
|
|
|
import { InstallCommand } from "/snippets/install-command.jsx";
|
|
|
|
<iframe
|
|
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
|
title="motion-blur 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/motion-blur.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","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/motion-blur.png");document.body.appendChild(p)});<\/script></body></html>`}
|
|
/>
|
|
|
|
## Install
|
|
|
|
<InstallCommand command="npx hyperframes add motion-blur" item="motion-blur" />
|
|
|
|
That writes one file: `compositions/components/motion-blur.html`.
|
|
|
|
## Source
|
|
|
|
<Accordion title={`motion-blur.html`}>
|
|
|
|
```html
|
|
<!--
|
|
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;
|
|
-->
|
|
```
|
|
|
|
</Accordion>
|
|
|
|
## Usage
|
|
|
|
Paste the snippet into your composition, then call `attachMotionBlur()` after your GSAP tweens and before registering `window.__timelines`.
|
|
|
|
```html
|
|
<!-- Extend the timeline to data-duration before calling attachMotionBlur -->
|
|
tl.set(document.body, {}, DATA_DURATION);
|
|
|
|
attachMotionBlur("#my-box", tl, {
|
|
axis: "x", // "x" | "y" | "both"
|
|
blurMax: 40, // max blur radius in px (default 20)
|
|
});
|
|
|
|
window.__timelines = window.__timelines || {};
|
|
window.__timelines["my-composition"] = tl;
|
|
```
|
|
|
|
## How it works
|
|
|
|
Each target element gets its own SVG filter. On every timeline seek, `attachMotionBlur` samples the element's GSAP `x`/`y` position, computes velocity, and drives three SVG filter primitives:
|
|
|
|
1. **Ghost copies** — three faded, blurred copies of the element placed behind it at increasing offsets proportional to speed. Inherently one-sided: no forward blur.
|
|
2. **Top blur** — a small symmetric Gaussian at the current position so the element looks in-motion rather than crisp on top of the trail.
|
|
|
|
Blur scales linearly with velocity up to `blurMax`. Both the ghost trail and top blur clear automatically when the element decelerates to rest.
|
|
|
|
## Options
|
|
|
|
| Option | Default | Description |
|
|
| --- | --- | --- |
|
|
| `axis` | `"both"` | Motion axis — `"x"`, `"y"`, or `"both"` |
|
|
| `blurScale` | `0.008` | Blur per px/s of velocity |
|
|
| `blurMax` | `20` | Max blur radius on the motion axis (px) |
|
|
| `stretchScale` | `0.0002` | scaleX/Y added per px/s (requires `stretchMax > 0`) |
|
|
| `stretchMax` | `0` | Max stretch above 1.0 — disabled by default |
|
|
|
|
{/* hf:generated-footer */}
|
|
|
|
Tagged `effect` `motion-blur` `velocity` `animation` `physics`.
|
|
|
|
## Related topics
|
|
|
|
- [Browse the complete Catalog](/catalog)
|
|
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
|
- [Build a richer composition](/go-further)
|