mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 13:07:11 +00:00
sync/hyperframes-codegen-50ebf6ff
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
faa4913c9e |
chore(catalog): publish generated catalog (#4340)
* chore(catalog): publish generated catalog * chore(catalog): publish generated catalog * chore(catalog): publish generated catalog * chore(catalog): publish generated catalog * chore(catalog): publish generated catalog * chore(catalog): publish generated catalog * chore(catalog): publish generated catalog * chore(catalog): publish generated catalog * chore(catalog): publish generated catalog * chore(catalog): publish generated catalog * chore(catalog): publish generated catalog --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
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> |
||
|
|
82f49262ec |
feat(registry): blur every transform driver, and ship the reference case as shutter-slam (#4038)
The motion-blur snippet integrated translation only. Its output stage was one SVG filter per target whose copies were feOffset, and feOffset only translates, so a beat that only scaled or only turned sampled a displacement of zero and rendered sharp. No SVG filter primitive can give a copy its own affine transform, so the sampling loop was never the problem: the output stage was. Copies are now DOM clones of the element in an isolation:isolate group, each at 1/N opacity with mix-blend-mode plus-lighter, each carrying the element's resolved transform at its sample time. plus-lighter adds premultiplied colour, so N + 1 copies at 1/N sum to the average the shutter integral asks for; the isolation is load-bearing rather than tidy, because without a transparent backdrop of its own the first copy adds onto the page and blows a light background out to white. Reading the resolved matrix instead of a list of named GSAP properties is what makes every driver work at once: translation, scale, rotation, 3D rotation and skew arrive in the same one value. Measured against the After Effects export the shutter model was fitted to, per beat, previous stage to this one: scale +0.62 dB, rotate X +0.43, rotate Y +0.84, rotate Z +1.15, translate X -0.18, translate Y -0.04. The two small translate regressions are inherent and not tunable: the filter offset one rasterisation so every copy was pixel-identical, where each copy is now rasterised independently at its own fractional transform with its own subpixel antialiasing. Three consequences of the new stage, each carrying its own fix: - mix-blend-mode flattens preserve-3d, so a copy cannot inherit the parent's 3D context. Each copy carries the parent's perspective as its own first transform function instead. - A copy is styled by nothing that selected the original, since dropping the id also drops every #id rule that gave it size, colour and font. Each copy carries its own resolved style inline, read once and replayed. Those values are px, so a container-relative element needs them again when its box changes: a ResizeObserver re-snapshots rather than paying a subtree walk every frame. - A beat that moved and faded at once kept a full-strength smear behind a vanishing element. The element's opacity now rides the group; the 1/N weight stays on the copies. The axis option is gone. It selected which of x and y to sample, a question the resolved matrix does not have. shutter-slam is the new component: one word through six single-property beats, two translate slams, a scale punch, then a full turn about each axis, which exercises every driver the model covers in one mount. Easing is power2.inOut because it measured best, not because it was the default. A bezier fitted to the reference's own bbox centres looks like the better curve and renders worse: a blurred bbox centre is the shutter window's average rather than the instant, so on an accelerating segment the fit measures its own blur. Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
9aa60a844f |
fix(registry): make motion-blur match After Effects shutter-based blur (#4008)
* fix(registry): make motion-blur match After Effects shutter-based blur The motion-blur catalog snippet estimated speed from the x/y delta between consecutive timeline updates and drew three one-sided ghost copies with decaying alpha plus a Gaussian on top. After Effects does temporal supersampling: each frame integrates the layer over a shutter window (shutter angle, shutter phase, samples per frame), producing a symmetric box smear of length speed x shutter time. The snippet now does the same inside the page. After every timeline update a deferred pass seeks the GSAP timeline to N sub-frame times across the shutter window (events suppressed), reads each target's x/y, restores the frame time, and drives an SVG filter that averages N feOffset copies with equal weight (feComposite arithmetic, 1/N, sRGB). Defaults follow AE: shutterAngle 180, shutterPhase -90, samplesPerFrame 16; fps comes from the render config, then the root data-fps, then 30. Displacements are mapped through the inverse of the element's own scale/rotation; percent x/y values are resolved to px; SVG targets use their bbox; an existing inline filter is kept and the blur appended to it. The pass runs in a microtask rather than inside the tracker tween's onUpdate, because seeking GSAP from within its own render reads stale tween state and suppresses the composition's callbacks on forward seeks. The heuristic options blurScale/blurMax/stretchScale/stretchMax are removed. Reference: an AE-algorithm reconstruction of the same composition rendered without the component at an effective 960 fps and averaged over the 16 sub-frames of the 180 degree window per output frame. Against it (1920x1080, 30 fps, solid + text translating linear then power2.inOut): old mean PSNR 53.2 dB (min 26.7), SSIM 0.9930; new mean PSNR 72.4 dB (min 52.6), SSIM 0.9998. The 10-90 edge ramp at 66.7 px/frame is 26.8 px in the reference, 27.5 px new, 14.5/6.3 px (one-sided) old; at the ease peak 79.3 vs 79.5 vs 7.9/17.7 px, with smear centres matching to 0.1 px. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(registry): drop render-config fps probe from motion-blur and refresh its docs The snippet resolved fps by reconstructing the renderer's private seek config key so the compiler's inline-script guard would not strip it. A public catalog snippet must not depend on that key, so fps is now the explicit option, then the root data-fps, then 30; renders with an fps override pass fps explicitly. The catalog page's Usage, How it works and Options sections still described the old velocity heuristic and are rewritten for the shutter model. The 1/N weight keeps full precision. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(registry): match the After Effects reference export's shutter window The shutter model landed on this branch was built against a reconstruction of After Effects' algorithm, not against After Effects. A real 1920x1080 / 30 fps export of translating text disagrees with it on three points, all measurable off the export itself. The window is two frames wide, not half a frame. On the measured frame the outermost duplicate on the trailing side sits 128 px back while the previous frame is 125.8 px back, and the outermost leading duplicate sits 189 px ahead while the next frame is 184.9 px ahead: the window ends land on the neighbouring frames. Defaults become shutterAngle 720, shutterPhase -360. Duplicates sit on the sub-interval boundaries, both ends included, not at sub-interval centres. The staircase across a stroke steps every 15.6 px on a 125.8 px displacement and every 23.0 px on a 184.9 px one, i.e. an eighth of a frame, with the run closing exactly on the neighbouring frame's position. samplesPerFrame now means sub-intervals of the window, so there are N+1 copies at k/N rather than N copies at (k+0.5)/N. The frame-time instance is drawn sharp over the smear. At full ink the core bounding box is the whole word at every frame and total ink rises to 1.44x the unblurred frame's, neither of which an average can produce. A final feComposite over of SourceGraphic reproduces it. Each duplicate stays at 1/N: the staircase plateaus step by 0.063 +- 0.002 of the sharp text intensity, flat, with no taper toward the window edges, and triangle weighting scores 1.6 dB worse against the export. Verified by replaying the export's translating section as a composition whose sprite is the export's own rest-frame ink and whose eight legs follow its own measured per-frame positions, rendered at 1920x1080 / 30 fps and compared frame by frame over the text band. Mean PSNR against the export rises from 24.19 dB (min 17.07) to 32.51 dB (min 27.57) across 72 frames, and from 19.83 dB (min 17.07) to 32.03 dB (min 27.57) across the 26 frames that actually move. Ghost extents match to a few px and the rendered staircase plateaus match the export's to two decimal places. The export's later scale, skew, flip and 3D rotation sections are out of reach here: integrating anything but translation inside the shutter is engine work tracked in issue #4010. The filter region now clamps its bounds through zero. It was derived from the duplicate offsets alone, which was correct while the output was nothing but duplicates; the sharp instance draws at offset zero, so a window sitting entirely on one side of the frame time - any shutterPhase above 0 or below -shutterAngle - produced a region starting inside the element's own box and cut the instance off. The defaults straddle the frame time and were never affected, so the measured numbers above are unchanged. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
9734578e60 |
feat(registry): bring back the video-primitive moves (#3169)
Restores the 208 catalog items reverted after their previews 404'd in production, this time on the payload mechanism rather than the .html files that caused the outage. The generator no longer writes a preview document to docs/public. That writer, and the machinery under it, existed only to produce files the docs host discards, so it is gone rather than bypassed. Items now embed the composition itself via a payload, which is what the previous change already does for the items that were already in the catalog. The variables explorer is parked, not restored: it drove its preview through the same unpublished .html path, so it would have shown an empty frame. Items that declare variables get the live player plus the static variables table, and reconnecting the explorer to payloads is a follow-up. |
||
|
|
536165b6ef |
feat(catalog): play the real composition on catalog pages (#3168)
* feat(catalog): play the real composition on catalog pages Catalog previews were uploaded MP4s, published by hand to the CDN. Each page now embeds the composition itself, running in <hyperframes-player>, so a preview is the block rather than a recording of it. The composition is delivered as JSON under docs/public/catalog, and the player is mounted inside an iframe. Both are forced: the docs host publishes only JSON and images out of docs/public, and its MDX renderer strips unknown custom elements, so a player written into the page never reaches the DOM and an .html payload 404s in production. Assets are inlined as data URIs, which sidesteps the file-type restriction for fonts, scripts and models alike. 164 of 168 items build a payload; the rest keep their MP4 and say so. * chore: rebuild docs preview * chore: route isolation probe * chore: drop the route isolation probe page * chore: probe which asset types the docs host publishes * refactor(catalog): host preview assets instead of inlining them Fonts were being base64'd into every payload that used them, so a handful of files cost tens of megabytes in the repository to say the same thing over and over. Assets are now written once, content-addressed, under docs/public/catalog/assets and linked. Which types can be hosted was settled by fetching one file of each from a deployed preview: woff2, wav, mp4, svg and the image formats are published, glb is not. Types the host drops still travel inside the payload, because a link that 404s is worse than a larger payload. Also fixes the four type errors the scripts typecheck caught, all of them unchecked index access on a split() result. * refactor(catalog): share preview assets instead of repeating them * style: format the asset module |