feat(registry): text-match-cut - #4276
jmoran-heygen wants to merge 2 commits into
Conversation
…e on a single 85 % curve Video primitive reversed from an After Effects tutorial (Editing Snapped, "The Match Cut Technique Every After Effects User Needs"), measured per frame at 60 fps. Both texts ride one keyframe pair, cubic-bezier(.85, 0, .15, 1) over 2 s, and hard-swap at the speed peak (1.0 s). A `mode` variable picks the property that carries the cut: scale down (the tutorial), scale up, tracking in, or a same-direction slide in four directions. Optional cyan-to-blue gradient sweep on its own 87 % curve, navy or transparent background. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jrusso1020
left a comment
There was a problem hiding this comment.
Approve at 7a098147980eb843eb030d9ad99e3f5c26820d22. Well-built component and a clean integration — no blocking findings. Two notes below, neither one a reason to hold.
What I verified independently
Ran, not read off the description:
node scripts/lint-registry-items.mjs text-match-cut→Registry item lint passed — 1 item(s), 0 errors., exit 0.node scripts/check-docs-catalog.mjs→ exit 0, four PASS lines:409 pages before, 410 after (1 legitimately new: /catalog/components/text-match-cut),409 gallery items, all present in the sidebar and on disk; no duplicate sidebar entries, no.html/.js/.cssunderdocs/public, andcatalog-gallery.jsxclean. That settles the maintainer's consistency and count claims.- The two numbers in the description that are checkable at source both hold at
W = 1920:D_GRAD = (2316 / 1920) * Wis the stated 2316 px of travel, andXG_CUT = CX - (12 / 1920) * Wis 960 − 12 = 948 px, the stated centre at the cut.bez(0.85, x)withp1x = a,p2x = 1 - aand the y-curve3(1-t)t² + t³iscubic-bezier(.85, 0, .15, 1)as claimed. - Placement: the item lands in
motion-effects / Effectswhile its registryfamilyistransitions, which looks wrong until you count it — all 10 other components withfamily: "transitions"land in exactly the same group/section. Consistent, not misfiled. - Conventions all match the registry: GSAP pinned to
3.14.2(365 of 372 uses repo-wide),window.__timelinesregistration (380 files),data-fps="30"on the root (272 roots),document.getElementById("root")(117 components), andpreview.mode: "player"withposter: null(229 of 409 items have that exact shape).
Three multi-mount hazards I went looking for and found already handled — recording them so nobody re-runs the check. The demo puts four mounts in one document, and the demo's own outer container is also id="root", so document.getElementById("root") inside a mount looks like it should resolve to the demo shell, and window.__timelines["text-match-cut"] looks like four writers to one key. packages/core/src/compiler/compositionScoping.ts closes both: the scoped document proxy intercepts getElementById and accepts a global hit only when __hfContains puts it inside this mount's root (an ancestor fails that test and falls through to the scoped lookup), and __hfTimelineRegistryProxy namespaces the timeline registry per composition.
Note 1 — this is the first component in the registry that reads dataset.fps, and the value it reads is the declared rate, not the render rate
text-match-cut is 1 of 221 components whose script reads root.dataset.fps (line 239); every other component's animation is fps-independent by construction. The smear is deliberately proportional to px-per-frame — dp is divided by FPS — so the value matters here in a way it never has before.
data-fps flows one way: packages/cli/src/utils/compositionFps.ts reads it so hyperframes render can default --fps to it. Nothing writes it back, so when --fps is passed explicitly (or a cloud backend picks from {24, 30, 60}), the attribute still says 30 and FPS reads 30. Rendered at 60, the smear is about twice what the model intends, capped by Math.min(6, …).
To be fair about whose problem this is: window.__hyperframes exposes only getVariables, so there is no API that would let the component do better today. The declared rate is the best available source, and nothing else in the registry is affected because nothing else looks. Worth knowing as the first consumer of a value the runtime does not actually publish.
Note 2 — landing order against #4274
Heads-up rather than a defect, since you plan to merge on approval. This PR moves registry.json's catalogArtifact.revision from eda107ee… to cf2a7c8f… and rewrites local-vectors.{json,bin}. #4274 (catalog item removals) mutates the same corpus hash, and scripts/catalog/check-artifact-coverage.ts verifies the revision, not just per-item coverage — its own docblock: "every corpus change still invalidates the published revision so stale vectors cannot pass the gate unnoticed."
So whichever of the two lands second goes red on catalog-index-coverage until the artifact is regenerated, and regeneration needs the embedding model locally — CI verifies freshness, it does not fix it. Sequencing thing, not a code change.
What I did not verify
No Chrome run and no oxfmt --check / check:catalog-drift / verify:catalog-payloads from me — this worktree has no node_modules and I borrowed only typescript to run the docs gate. The four-mount cut-timing and backward-seek assertions are taken as reported, not reproduced.
— Rames
|
Superseded by #4288 (signed re-land on current main). |
Pull request was closed
What
A new component,
text-match-cut: one line of text becomes another in the middle of a move. Both texts ride a single keyframe pair and hard-swap at the speed peak, so the cut lands at peak velocity with no crossfade.Reversed from an After Effects tutorial (Editing Snapped, The Match Cut Technique Every After Effects User Needs), measured per frame at 60 fps rather than taken from the narration:
cubic-bezier(.85, 0, .15, 1)over 2 s (fit rmse 0.0015), split at exactly frame 60 of 120Variables
text_atext_bmodesweepbackgroundEnvelope: IN 2.0 s (the move), elastic HOLD, OUT 0.3 s. Sync point
hard-cutat 1.0 s.Checks
hyperframes checkon the demo: 0 errors (see maintainer validation below for the current CLI limitation).node scripts/lint-registry-items.mjs: passes for this item (the two existing failures,vfx-text-cursorandcaption-emoji-pop, are untouched)generate-registry-items,generate-catalog-payloads --only text-match-cut,generate-catalog-pagesrun;oxfmt --checkcleandemo.htmlmounts four modes in a 2×2 grid with non-default copy; every tile cuts on the same framePreview
hyperframes.dev project: https://hyperframes.dev/p/bf7a04d1-4647-400e-b20b-32338d7884a7 (claim pending on my side, will flip to public once claimed).
Maintainer generated and uploaded the catalog poster and MP4; links and verification below.
🤖 Generated with Claude Code
Maintainer integration and validation
Head
7a098147980eb843eb030d9ad99e3f5c26820d22merges current main without changing Jake's component, demo, or variable metadata. Regenerated payload/search vectors, preserved current navigation structure, and added the missing gallery-data entry. Fork branch updated without force-push; merge commit is signed.c85bc9597ffce894bf50560f8a19315368e21f1f7b099004f1931369f5d3d3cb.checkcommand does NOT supply a current full-layout pass: it rejects the dimensionless registry snippet as a standalone root; feeding it compiled HTML instead trips lint on bundled GSAP/runtime code. Named registry lint and actual render/Chrome assertions are the measured evidence, not a claimed all-green generic check.Poster: https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/text-match-cut.png
Video: https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/text-match-cut.mp4