Files
hyperframes/docs/catalog
752b552789 feat(registry): blur an element by marking it, not by calling a function (#4045)
attachMotionBlur(el, tl) has an ordering contract: it has to run after every
tween is defined and before the composition registers its timeline. Getting it
wrong makes the blur silently do nothing, which is the worst shape a failure can
take in a renderer nobody watches. An attribute has no order to get wrong.

data-hf-motion-blur on any element is now the contract. Empty means defaults;
anything else is a JSON object of the same options. The element finds its own
timeline: its nearest [data-composition-id] ancestor names the key, and the
snippet reads window.__timelines[key]. attachMotionBlur stays exported for a
target built after the page has settled.

claim(el) is the only thing that attaches, and two triggers call it. An accessor
on window.__timelines wraps the registry in a Proxy whose set trap sweeps on
every per-key registration, which is what attaches before the renderer captures
frame zero. A bounded poll, 32 ms by 250 ticks, reads the registry directly and
needs no cooperation from whoever wrote it. The poll is not belt and braces: a
registry reference captured before the snippet installs bypasses the trap
outright, and a composition that mounts its DOM after registering its timeline
has no marked element to sweep at trap time. It runs its whole budget every time
rather than stopping the first tick nothing is pending, because on an ordinary
page the top-level target is claimed on the registration write, so an early exit
kills the poll one tick in, before any late composition arrives.

Every refusal is by name, once, and never silent: a value that is not JSON, a
value that parses but is not an options object, an option name that is not one
of the four, an option value that is not a finite number, a target inside
another target, and a second timeline registered under one key. Parsing runs
before the timeline lookup, so a malformed value is reported as malformed rather
than as a composition that never registered.

Two properties of the primitive this change made load-bearing:

- Frame rate is resolved from the target's own composition root, not from the
  first root in the document. The document-wide lookup was survivable while
  every caller could pass fps explicitly; the declarative form has no per-call
  argument. Two compositions on one page at different rates now each get their
  own shutter window, and one call naming both says it cannot.
- An attach is all-or-nothing. The mark, the copies and the resize observer go
  in together and come back out together, so a throw partway through leaves the
  element untouched rather than marked blurred with copies in the page and no
  tracker driving them. The sweep is also bounded per element, because the write
  trap runs it inside the author's own registration statement.

Copies are stripped of data-hf-motion-blur, data-composition-id and data-fps,
on the clone and every descendant: the first would make a copy a target of its
own, and the other two would let a copy win the fps lookup, since copies are
inserted before the original.

skills/hyperframes-animation/references/motion-blur.md is the agent-facing half,
mirroring how colour grading is surfaced. It leads with when NOT to blur,
because the attribute makes blurring everything trivial and a smear on motion
that was never fast enough reads as a soft, cheap render.

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-authored-by: Vance Ingalls <vance@heygen.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-18 22:55:15 -07:00
..