Skip to content

refactor(studio): clip-relative GSAP animation positions#1448

Open
miguel-heygen wants to merge 11 commits into
mainfrom
refactor/clip-relative-gsap-positions
Open

refactor(studio): clip-relative GSAP animation positions#1448
miguel-heygen wants to merge 11 commits into
mainfrom
refactor/clip-relative-gsap-positions

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

Summary

Switches GSAP animation position storage from absolute timeline seconds to clip-relative offsets. Moving a clip on the timeline now automatically keeps animations in sync — zero GSAP mutations required.

Every major video editor (After Effects, Premiere, DaVinci Resolve, FCP, CapCut) stores keyframe times as offsets from clip start. This refactor brings HyperFrames in line with that model.

Changes

Runtime post-processing (packages/core/src/runtime/init.ts)

  • After timeline capture, walks all tweens via getChildren() and shifts startTime by target element's resolveStartForElement offset
  • Placed outside the iframe guard — runs in both Studio preview and production renders
  • Applied in both bindRootTimelineIfAvailable and rebindTimelineFromResolution
  • Guarded against double-application via _hfOffsetApplied stamp

Studio hooks (5 files)

  • addGsapAnimation: position=0 (not elStart)
  • useEnableKeyframes: position=0
  • commitKeyframeAtTimeImpl: absoluteTime - elStart
  • extendTweenAndAddKeyframe: subtract elStart at mutation
  • useGestureCommit: recStart - elStart

globalTimeCompilerresolveTweenStart, findTweenAtTime, absoluteToPercentageForAnimation, percentageToAbsoluteForAnimation now take elementStart parameter (default 0) so they return absolute time for clock comparisons

Keyframe cache — Simplified diamond position formula: clipTime = tweenPos + kf% * tweenDur then clipPct = clipTime / elDuration (no longer subtracts elStart since position is already clip-relative)

ParsersplitAnimationsInScript uses relSplitTime = splitTime - elementStart. Added targetSelector to applyUpdatesToCall for atomic selector+position updates. gsapAnimationsToKeyframes no longer subtracts elementStartTime.

Test plan

  • All 20 split/serialize tests pass with clip-relative expectations
  • Zero new test failures vs main (144 pre-existing in parser tests)
  • Studio typecheck clean, core typecheck clean
  • Full build passes across all packages
  • Route tests (17) pass

miguel-heygen and others added 4 commits June 14, 2026 21:53
Walk all tweens after timeline capture and shift startTime by the
target element's resolveStartForElement offset. This converts
clip-relative GSAP positions (offset from data-start) to absolute
timeline positions that GSAP's API expects.

Placed outside the iframe guard so it runs in both Studio preview
and production renders. Applied in both bindRootTimelineIfAvailable
and rebindTimelineFromResolution paths. Guarded against double-
application via _hfOffsetApplied stamp on each tween.

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
…dinates

Split comparisons now use relSplitTime (splitTime - elementStart)
instead of absolute splitTime. Animations reassigned to the new
element get their position adjusted to the new element's coordinate
system (pos - relSplitTime). Spanning animation clones start at
position 0 in the new element.

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
All Studio hooks now emit position as offset from element's data-start:
- addGsapAnimation: position=0 instead of elStart
- useEnableKeyframes: position=0 instead of elStart
- commitKeyframeAtTimeImpl: absoluteTime - elStart
- extendTweenAndAddKeyframe: subtract elStart at mutation
- useGestureCommit: recStart - elStart
- gsapRuntimeBridge: position=0

globalTimeCompiler functions take an elementStart parameter (default 0)
so resolveTweenStart returns absolute time for clock comparisons.

Keyframe cache formulas simplified — no longer subtract elStart since
position is already clip-relative. gsapSerialize inverse path no longer
subtracts elementStartTime.

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
Add targetSelector support to applyUpdatesToCall so split can update
both position and selector atomically (avoids stable-ID lookup failure
from sequential operations). Update split test expectations for
clip-relative coordinates. Update gsapAnimationsToKeyframes test for
the simplified inverse path.

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown

Fallow audit report

Found 12 findings.

Dead code (3)
Severity Rule Location Description
major fallow/duplicate-export packages/studio/src/hooks/timelineEditingHelpers.ts:9 Export 'RecordEditInput' appears in multiple modules
major fallow/unused-type packages/studio/src/hooks/useTimelineEditing.ts:35 Type export 'RecordEditInput' is never imported by other modules
major fallow/duplicate-export packages/studio/src/hooks/useTimelineEditing.ts:35 Export 'RecordEditInput' appears in multiple modules
Duplication (2)
Severity Rule Location Description
minor fallow/code-duplication packages/studio/src/components/editor/PropertyPanel.tsx:177 Code clone group 1 (51 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelTransformCommit.ts:76 Code clone group 1 (51 lines, 2 instances)
Health (7)
Severity Rule Location Description
minor fallow/high-crap-score packages/studio/src/components/editor/PropertyPanel.tsx:161 'commitManualOffset' has CRAP score 37.1 (threshold: 30.0, cyclomatic 11)
major fallow/high-crap-score packages/studio/src/components/editor/PropertyPanel.tsx:274 '<arrow>' has CRAP score 97.0 (threshold: 30.0, cyclomatic 19)
minor fallow/high-crap-score packages/studio/src/hooks/timelineEditingHelpers.ts:44 'patchIframeDomTiming' has CRAP score 31.6 (threshold: 30.0, cyclomatic 10)
minor fallow/high-crap-score packages/studio/src/hooks/timelineEditingHelpers.ts:64 'resolveResizePlaybackStart' has CRAP score 31.6 (threshold: 30.0, cyclomatic 10)
critical fallow/high-crap-score packages/studio/src/hooks/useTimelineEditing.ts:186 'handleTimelineElementDelete' has CRAP score 132.0 (threshold: 30.0, cyclomatic 11)
critical fallow/high-crap-score packages/studio/src/hooks/useTimelineEditing.ts:260 'handleTimelineAssetDrop' has CRAP score 156.0 (threshold: 30.0, cyclomatic 12)
critical fallow/high-crap-score packages/studio/src/hooks/useTimelineEditing.ts:342 'handleTimelineFileDrop' has CRAP score 110.0 (threshold: 30.0, cyclomatic 10)

Generated by fallow.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Request-changes (scoped) — round-trip mismatch + zero-coverage runtime mutation

I've focused this pass on the canonical-rubric + Claude-generated-code lens; deferring HF-runtime semantics (Studio drag flow, scrub clock, render-time tween targeting) to Via.

What ships
A storage-format refactor for GSAP animation positions: clip-relative offsets instead of absolute timeline seconds. Studio hooks emit position = absoluteTime - elStart on commit; globalTimeCompiler rehydrates absolute via a new elementStart parameter (default 0); and a new applyClipRelativeOffsets() walks the captured timeline post-bind, shifting each tween's startTime by resolveStartForElement(target). 14 files, +105/-67.

Migration / forward-compat verdict — blocker
There is no read-side migration. Existing GSAP scripts on disk store absolute positions (that's what main writes today). Once this PR lands, runtime sees those absolute positions and applyClipRelativeOffsets shifts them by elStart again. A clip at data-start=5s with tl.set("#el", {...}, 5) becomes effective t=10. No version discriminator, no script-fingerprint check, no opt-out attribute.

Worse — round-trip is broken even for new output:
keyframesToGsapAnimations (gsapSerialize.ts:255) still computes position = elementStartTime + prevKf.time (absolute).
generators/hyperframes.ts:192 calls it with element.startTime, so the canonical HF generator produces a script with absolute positions.
• That script is then serialized and runtime-shifted again → double-shift, every newly-generated HF file.
• The inverse, gsapAnimationsToKeyframes, was updated to drop the subtraction but still takes elementStartTime as a required parameter and silently ignores it (gsapSerialize.ts:280-306). External SDK consumers will pass it and get wrong-shape data.

Two of these are public-API exports from @hyperframes/core (src/index.ts:79-80). Either:

  1. Update keyframesToGsapAnimations to emit position = kf.time (matching the new contract), and drop the elementStartTime parameter from both functions (breaking minor-version bump), OR
  2. Make applyClipRelativeOffsets conditional on a script-level opt-in marker (e.g. // @hf:clip-relative comment or an attribute on the timeline element), so old scripts keep absolute semantics and new ones get the shift.

Without one of these, every existing project + every generator-emitted script breaks on first reload after merge.

Determinism contract
No new Date.now() / performance.now() / Math.random() / fetch() introduced in render-time paths — verified by grep over the touched runtime + studio files. The two pre-existing Date.now() / performance.now() refs in init.ts are unrelated to this PR.

Claude-generated-code lens findings
_updateAnimationSelector (gsapParser.ts:1328) is renamed from updateAnimationSelector with a leading underscore and has zero callers. The sole call site was rewritten to use updateAnimationInScript({position, targetSelector}). Dead code — should be deleted, not stub-renamed. (Fallow already flags 2 dead exports in gsapShared.ts for the same reason — getIframeDocument, toAbsoluteTime.)
gsapKeyframeCommit.ts:27 declares const elStart = ... then gsapKeyframeCommit.ts:64 redeclares the identical expression inside the else branch. Shadowing; harmless but sloppy — drop the inner one.
applyClipRelativeOffsets (init.ts:989-1015) silently skips non-HTMLElement targets via target instanceof HTMLElement. SVG tweens (<svg>, <g>, animated paths/text — common in HF compositions) will not be offset-shifted, while sibling HTMLElement tweens are. Use Element + duck-type getAttribute("data-start"), or explicitly cast to Element and call resolveStartForElement which already handles non-HTML.
applyClipRelativeOffsets only checks targets[0]gsap.to([".a", ".b"], ...) with mixed clip ancestors applies element-0's offset to all. Flag for Via — may not be a real-world pattern.
_hfOffsetApplied flag is not set when the target is rejected (non-HTMLElement / no targets fn), so every re-bind re-walks those tweens. Minor perf only.

Test coverage gaps
Zero tests for applyClipRelativeOffsets. init.test.ts (1098 lines, 30+ cases) is untouched. The single load-bearing runtime mutation in this PR has no unit test pinning: idempotency via _hfOffsetApplied, the SVG-target skip, the no-data-start fallback, the rebind-doesn't-double-shift contract. This is the highest-risk omission in the diff.
No round-trip test. No test loads an absolute-position script, runs it through the runtime, and asserts identical render output to a clip-relative script. This is the canonical regression for storage-format changes and is exactly the test that would have caught the keyframesToGsapAnimations mismatch above.
No "moving a clip = zero GSAP mutations" pin. The PR's headline claim ("zero GSAP mutations required") has no test — there's no drag clip; assert mutation count === 0 assertion anywhere. If a regression reintroduces a GSAP rewrite on drag, nothing fails.
• Test changes are limited to gsapParser.test.ts — the parser/serializer corpus. The studio hooks (drag, gesture, keyframe, enable-keyframes) and the runtime path are unverified.

Cross-PR coherence with recent HF stack
Checked open PRs — #1439 (Vance, drag-keyframes-with-snapping) touches Timeline.tsx / TimelineCanvas.tsx / TimelineClipDiamonds.tsx, no overlap with this PR's hook surfaces. No other in-flight conflict spotted.

Concerns
gsapAnimationsToKeyframes no longer uses elementStartTime but keeps it as a required positional parameter. Either drop it (breaking minor bump, clean) or mark deprecated with a runtime warn. Silent-ignore is the worst option for a public export.
_hfOffsetApplied is a string-keyed underscore-prefix marker on GSAP tween objects. If GSAP ever uses the same prefix internally, collision. Switch to a Symbol or a WeakSet<Tween> outside the function scope — both prevent leakage + collision.
• The mutation runs in bindRootTimelineIfAvailable and rebindTimelineFromResolution, both of which can fire during user edits. If a user changes data-start on a clip and Studio re-binds with the same timeline instance (early return at init.ts:1036 / 1322), the offsets from the old data-start stay applied. Via to confirm rebind-on-edit semantics.

Nits
applyClipRelativeOffsets error handler logs to swallow("runtime.init.clipRelativeOffsets", err) — fine, but consider whether silently swallowing a mass-mutation failure leaves the user with half-shifted tweens. Worth a console.warn in dev.
roundTo3(newStart - elStart) in gsapDragCommit.ts:122 — if elStart floats by 0.0001 from the on-disk value, the new position drifts. Minor; the previous absolute-position formula had the same risk.

Via-lane callouts
• Confirm Studio rebind-on-data-start-edit semantics — does same-instance early return leave stale offsets?
• Confirm SVG-target tweens (paths, masks, text) aren't load-bearing in current HF templates. If they are, the HTMLElement filter is a render bug not a nit.
• Confirm the headline claim ("moving a clip on the timeline now keeps animations in sync automatically with zero GSAP mutations") — i.e., that the prior version did emit per-drag GSAP mutations and this version genuinely emits zero. The PR body asserts this; no test pins it.

CI
Mostly green; Fallow audit failed with 78 findings (2 dead exports, 39 duplications, 37 health/CRAP). The dead exports + _updateAnimationSelector are introduced or aggravated by this PR. Regression shards + Windows render + CodeQL still in progress at review time.

Review by Rames D Jusso

miguel-heygen and others added 2 commits June 14, 2026 22:22
…tart

- Runtime: gate applyClipRelativeOffsets on data-position-mode='relative'
  marker so existing absolute-position compositions are unaffected
- Runtime: widen target filter from HTMLElement to Element (SVG support)
- Generator: keyframesToGsapAnimations now emits clip-relative positions
  (no longer adds elementStartTime)
- Mutation engine: stamps data-position-mode='relative' on script tag
  when writing mutations
- globalTimeCompiler: resolveTweenStart adds elementStart to the
  resolvedStart branch (was returning clip-relative as if absolute)
- Remove dead _updateAnimationSelector function
- Mark unused elementStartTime params with underscore prefix
- Update test expectations for clip-relative positions

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
The regex approach only matched data-hyperframes-gsap script tags which
real blocks don't use. Switch to block.document.querySelector to stamp
data-position-mode='relative' on the actual GSAP script element
regardless of its existing attributes.

Add two tests: sentinel appears after mutation on a plain <script> tag,
and sentinel is not duplicated on subsequent mutations.

Fix elStart shadowing in gsapKeyframeCommit.ts (remove duplicate decl).

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Request-changes (scoped) — R1 blocker fix is conceptually right but the marker is never emitted, breaking newly-generated projects

R2 fix-up (f1cf2498) targets the right places — generator path, marker gate, resolvedStart correctness, dead-code removal, Element widening. Five of six R1 items resolve cleanly. The sixth, the round-trip / migration fix, doesn't actually close because the gating marker has no emit site outside the studio file-edit path, and the regex that's supposed to inject it doesn't match the generator's <script> tag.

R1 status

• ✅ BLOCKER 1 — Round-trip generator/runtime mismatch (partial / still open). Generator now writes clip-relative (gsapSerialize.ts:254position = prevKf ? prevKf.time : kf.time, no more elementStartTime + …). Runtime now gates on a marker (init.ts:991if (!document.querySelector("script[data-position-mode='relative']")) return). Studio file-edit route auto-injects on first save (files.ts:1180-1185). Conceptually this closes the double-shift: old absolute scripts (no marker) → runtime skips offset → render unchanged; new clip-relative scripts (marker present) → runtime adds elStart. But see new finding below — the marker injection is dead-code-path for the canonical generator output, so the "blocker resolved" claim doesn't hold in practice.

• ❌ CONCERN 1 — Zero test coverage on applyClipRelativeOffsets. init.test.ts still 1098 lines, untouched between R1 HEAD (70aae418) and R2 HEAD (f1cf2498). No tests for: marker-gate (!document.querySelector(...) early-return), _hfOffsetApplied idempotency, Element-widened SVG path, no-data-start fallback, rebind double-shift. The single load-bearing runtime mutation in this PR remains unverified.

• ❌ CONCERN 2 — "Zero GSAP mutations" headline claim has no test pin. Still no drag clip → assert mutation count === 0 test. PR's central thesis remains unverified.

• ✅ CONCERN 3 — gsapAnimationsToKeyframes ignores elementStartTime. Parameter renamed to _elementStartTime on both keyframesToGsapAnimations (gsapSerialize.ts:241) AND gsapAnimationsToKeyframes (gsapSerialize.ts:281), signaling intentional ignore via TS convention. Companion test (gsapParser.test.ts:587-617) pins clip-relative behavior — gsapAnimationsToKeyframes(animations, 2) returns keyframes[0].time === 0, keyframes[1].time === 1, ignoring the 2. Public-API signatures unchanged (no minor-version bump needed). Acceptable — though a @deprecated JSDoc would be clearer for SDK consumers than the underscore-prefix convention.

• ✅ CONCERN 4 — Dead _updateAnimationSelector. Deleted (gsapParser.ts diff shows -22 lines). The two updateAnimationSelector refs in the file are now comment-only references.

• ✅ CONCERN 5 — applyClipRelativeOffsets SVG filter. target instanceof HTMLElement widened to target instanceof Element (init.ts:1006). SVG tweens now offset-shifted alongside HTMLElement tweens. Via's lane judgment unblocked.

What changed at R2

6 files, +15/-31 across one commit (f1cf2498):
gsapSerialize.ts — generator path emits clip-relative, params renamed _elementStartTime.
gsapParser.ts_updateAnimationSelector deleted.
gsapParser.test.ts — assertions updated to clip-relative expectations.
init.ts — marker gate added, HTMLElementElement.
files.ts — studio mutation route auto-injects data-position-mode="relative" on save.
globalTimeCompiler.tsresolvedStart path now adds elementStart (matches the new clip-relative storage; was double-shift risk on studio side).

New findings

NEW BLOCKER — marker injection regex never matches the generator's <script> tag. files.ts:1182 searches for (<script\b[^>]*data-hyperframes-gsap[^>]*)> — anchoring on a data-hyperframes-gsap attribute. grep -rn "data-hyperframes-gsap" across the whole repo returns exactly one hit: this regex itself. No generator, exporter, or template emits a <script data-hyperframes-gsap> tag. generateHyperframesHtml (hyperframes.ts:354-360) emits a bare <script>${gsapScript}</script>. Consequence: every newly-generated HF project ships with clip-relative positions but NO marker; runtime gates off; animations fire at the start of the timeline instead of being shifted to elStart. The "old-format-unchanged" half of the migration works (those scripts also lack the marker, so they skip the offset and render correctly with absolute positions), but the "new-format-shifted" half is broken end-to-end. Either (a) change the regex anchor to the actual <script> boundary that generateHyperframesHtml emits — but that tag has no attributes to anchor on, so this needs a new emit site in the generator AND a tightened regex — or (b) emit data-position-mode="relative" directly in generateHyperframesHtml at hyperframes.ts:357 (<script data-position-mode="relative">), and emit it from every other gsap-script writer (CLI scaffold, capture, blank template). The single-place injection in files.ts only covers user-edited scripts going through the studio mutation API, not the canonical generation path. This is more severe than R1's double-shift because it silently breaks render correctness for net-new projects with no error signal.

Sub-finding — same marker gap affects Studio's resolveTweenStart semantics. globalTimeCompiler.ts:30 unconditionally adds elementStart + animation.resolvedStart. There's no marker-gate on the studio side. If a legacy absolute-position script is loaded into Studio and re-rendered, the timeline view will compute wrong tween times (absolute + elStart). This is fine as long as Studio always re-saves on first edit (re-emitting clip-relative + marker), but the marker emit on save is broken per the previous finding, so this regression surface is real for any legacy project opened in Studio.

resolvedStart change is consistent with the new contract. gsapParser.ts:1043 sets anim.resolvedStart = Math.max(0, start) where start comes from the parsed position. After this PR, generator writes clip-relative positions → resolvedStart holds clip-relative time → elementStart + resolvedStart produces absolute. Coherent for new format; matches the runtime contract. The fix is correct.

Via-lane callouts (unchanged from R1)

data-start-edit + same-instance rebind: still no test; same concern.
• SVG-target prevalence: deferral resolved — filter widened to Element, so this is no longer a render-bug risk.
• Headline "zero GSAP mutations" claim: still unpinned by test.

CI

• Required checks green at HEAD: Test, Typecheck, Lint, Format, CLI smoke, Build, Studio: load smoke, SDK unit+contract+smoke, Render on windows-latest, Tests on windows-latest, Perf shards (drift/fps/load/parity/scrub), Test: runtime contract, Preview parity, CodeQL, File size check.
Fallow audit: still failing (85 findings now, was 78 at R1). Net +7 findings post-R2; the _updateAnimationSelector rename was removed but other duplications/complexity findings persist. Not introduced by this PR's core change.
regression-shards (shard-1, shard-2): pending at review time. Worth waiting on — they could surface the marker-gate regression empirically.

Recommended next steps

  1. Emit data-position-mode="relative" directly in generateHyperframesHtml (hyperframes.ts:357) and any other script-emitting path (CLI scaffold, capture, blank template, the studio play.ts runtime tag). Verify with grep -rn "<script>" packages/ that every HF gsap-script writer carries the marker.
  2. Tighten the files.ts injection regex to a generator-emitted anchor (e.g. the bare <script> block matching the gsap-script position), or drop it entirely if all emit sites now write the marker directly.
  3. Add a runtime test pinning the marker gate: load marker'd + un-marker'd timeline, assert applyClipRelativeOffsets is no-op for the latter.
  4. Add a round-trip test: render canonical generator output through the runtime, assert effective tween start times match elStart + clip-relative-position.

Review by Rames D Jusso

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R2 correction.

My R2 above was posted against f1cf2498 while HEAD had advanced to 953715e8 ~2 min earlier — sequencing artifact on my side, my apologies for the noise.

Re-verifying at 953715e8:

BLOCKER 1 (sentinel emit path) — ✅ resolved. packages/core/src/studio-api/routes/files.ts:1182 now uses block.document.querySelector("script:not([src])") + setAttribute("data-position-mode", "relative") via the DOM API. Plain <script> tags (no data-hyperframes-gsap anchor required) get the marker stamped. Two pinning tests landed in files.test.ts:545-595:

  • "stamps data-position-mode='relative' on the script tag after any mutation" — uses a plain <script> tag, exercises the gsap-mutations route, asserts the attribute is present. This is exactly the case my R2 said wouldn't be covered.
  • "does not duplicate data-position-mode on subsequent mutations" — idempotency.

Updated R1-item status:

  • BLOCKER 1 ✅ at 953715e8
  • CONCERN 1 (no applyClipRelativeOffsets runtime tests) ❌ still open
  • CONCERN 2 (zero-GSAP-mutations headline claim has no test pin) ❌ still open
  • CONCERN 3-5 ✅ as previously noted

Revised verdict: approve-with-concerns. The 2 remaining items are non-blocking, but both are on the load-bearing runtime path — worth pinning before stamp.

Review by Rames D Jusso

miguel-heygen and others added 2 commits June 14, 2026 22:38
Extract the tween-walking logic into a standalone testable module.
9 tests covering: offset shift, zero-offset no-op, multi-tween,
multi-element, idempotency on rebind, no-targets skip, null timeline,
and clip-relative offset preservation.

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
Generator (hyperframes.ts) now emits data-position-mode='relative'
on the GSAP script tag. Mutation engine uses block.scriptElement
(the actual GSAP script found by extractGsapScriptBlock) instead of
querySelector('script:not([src])') which could match a non-GSAP
data-setup script on blocks with multiple inline scripts.

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R3 — all R1 items resolved. Verified at HEAD d36d9895.

BLOCKER 1 (generator-runtime contract) — ✅ closed at the source. packages/core/src/generators/hyperframes.ts:357 now emits <script data-position-mode="relative"> directly. Net-new HF projects ship with the sentinel from the canonical generator path; runtime gate doesn't depend on the studio-api mutation route to stamp it. No migration discriminator needed because the marker is structurally inseparable from emit.

CONCERN 1 (no applyClipRelativeOffsets runtime tests) — ✅ resolved. Function extracted to packages/core/src/runtime/clipRelativeOffsets.ts (+32) with 9 tests in clipRelativeOffsets.test.ts covering:

  • Offset application (shifts startTime by element's offset)
  • Zero-offset no-op
  • Same-element multi-tween consistency
  • Different elements with different offsets
  • Idempotency on rebind (the load-bearing invariant — see CONCERN 2 below)
  • Skip on tweens without targets fn
  • Null/undefined timeline no-ops
  • Position-greater-than-zero preserves both clip-start AND tween-relative offset

CONCERN 2 (zero-GSAP-mutations headline pin) — ✅ resolved differently. The "doesn't double-apply on rebind" idempotency test is the operationally-meaningful shape of the zero-mutation claim — it proves the runtime walker emits no redundant tween-position updates when the timeline rebinds. Crediting per resolved-differently (not the prescribed "drag clip → assert GSAP mutation count = 0" test, but the underlying invariant).

Bonus (not in R1 ask): files.ts:extractGsapScriptBlock now exposes scriptElement directly, and the mutation route stamps via block.scriptElement.setAttribute(...) instead of querySelector("script:not([src])"). Tighter — won't pick up a bystander plain <script> tag if the project has multiples.

CONCERN 3-5 — already ✅ in the prior R2 correction.

Revised verdict: approve. All R1 items closed (3 directly, 1 resolved-differently). The architectural shape (clip-relative storage matching AE/Premiere model) + the generator/runtime contract are both clean. CI status pending — worth confirming regression-shards lands green before stamping.

Review by Rames D Jusso

…ive positions

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
vanceingalls
vanceingalls previously approved these changes Jun 14, 2026

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

miguel-heygen and others added 2 commits June 14, 2026 18:53
…ly unconditionally

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
Reverts the clip-relative position model and replaces it with absolute
positions that Studio rewrites atomically when clips are dragged or
left-edge-resized.

- Remove applyClipRelativeOffsets runtime machinery and sentinel gate
- Revert Studio hooks to write absolute positions
- Revert globalTimeCompiler, AnimationCard, parser, keyframe cache
- Add shiftGsapPositionsInHtml for atomic position shifting on drag/resize

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
@miguel-heygen miguel-heygen changed the base branch from main to 06-14-feat_studio_drag_keyframes_with_live_beat_snapping June 14, 2026 23:56
@vanceingalls vanceingalls force-pushed the 06-14-feat_studio_drag_keyframes_with_live_beat_snapping branch 3 times, most recently from 79c14d7 to 3ac1da4 Compare June 15, 2026 00:18
Base automatically changed from 06-14-feat_studio_drag_keyframes_with_live_beat_snapping to main June 15, 2026 00:23
@vanceingalls vanceingalls dismissed their stale review June 15, 2026 00:23

The base branch was changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants