Summary
In the Studio canvas, after moving an element with a plain click-drag (pointerdown → move → mouseup), the very next button-less pointermove (just moving the cursor over the canvas, no click) throws the element far off-screen. This is live-preview-only: the persisted source stays correct (the element keeps its committed position; a page refresh restores the on-screen render). It is independent of composition markup (reproduced across compositions) and present identically in 0.6.112 and 0.6.121. No keyboard shortcut is involved — this is the ordinary drag-to-reposition workflow.
Affected versions
Confirmed present in 0.6.112 and 0.6.121. A function-level comparison of the served dist/studio/assets/index-*.js between the two versions shows the drag/gesture/calibration code is byte-identical modulo minified identifier names — i.e. this is not a recent regression, and a version rollback does not help.
Primary bug — element flies off-screen on a button-less mousemove after a normal drag
Steps to reproduce
- Select an element in the canvas.
- Drag it with a normal click-drag (pointerdown → move → mouseup — the move is committed).
- Without clicking anything, move the mouse over the canvas.
Expected: nothing happens (no active drag).
Actual: the element is flung far off-screen. The Inspector still shows the correct, committed X/Y and the source --hf-studio-offset is unchanged — only the live preview is wrong. A page refresh restores the correct render.
Mechanism (confirmed by a real puppeteer repro: button genuinely held during the drag, player paused)
- The drag itself is clean: the move is applied live through the CSS
translate property (--hf-studio-offset); the selection box tracks the element 1:1; no source write happens during the move. On mouseup the commit fires exactly one PUT of the composition file, followed by a softReload of the preview.
- The off-screen flight is driven by a GSAP
transform matrix (e.g. matrix(1,0,0,1,-6067,-5054)) — i.e. the gesture-recording live-apply path (gsap.set after translate:none), not the committed --hf-studio-offset (which stays correct).
- Root cause: the commit's
softReload does not tear down the gesture/live-apply machinery — a document-level button-less pointermove handler (+ its RAF) survives the reload with a stale startPointer. The next mouse move computes delta = pointer − startPointer from that stale anchor → a huge translation → the element leaves the screen.
- Control: with no prior drag, a button-less mousemove does nothing (
transform: none). The bug therefore requires a drag + commit + softReload to "arm" the surviving handler. This is a softReload teardown defect, not an inherent cost of source persistence (SDK-cutover).
Note on workaround: we verified that in-place mitigations do not clear the stale handler — clicking to deselect, re-selecting the element, and seeking/advancing the playhead all still result in the off-screen flight. Only a full page refresh reliably tears down the RAF/listener and restores a clean state.
Secondary bug — selection box desyncs from the element (~50px) during gesture-recording (r) only
Kept for completeness. This one does NOT occur in a normal drag — it is observed only in the gesture-recording mode entered with the r key.
Steps to reproduce
- Select an element, press
r to start gesture recording.
- Move the mouse to drag the element, grabbing it off-center.
Expected: the element follows the selection box 1:1.
Actual: the element renders offset from the selection box by ~the grab distance (≈50px for a ~90px element). On stop it snaps back into the box.
Mechanism (read from the served bundle)
On the first move the recorder does basePosition += pointerElementOffset (pointerElementOffset = pointer − elementCenter), so the live apply teleports the element center under the cursor — but the recorded sample subtracts the same offset back out (r.x -= cssVarOffset.x + pointerElementOffset.x / scale). The persisted coordinate is clean while the live element carries the grab offset; the overlay tracks the clean coordinate → constant desync = the grab offset.
Suggested fixes
- Primary: on the post-commit
softReload, fully tear down the gesture/live-apply session — cancel the RAF and removeEventListener the document pointermove handler (this is exactly what a manual refresh does). And/or ignore button-less pointermove deltas outside an explicit recording session. The skipReload path that already exists in the bundle (commit without reload, re-render in place) would also avoid the reload that strands the handler.
- Secondary (
r desync): make the live apply and the recorded sample use the same coordinate — don't add pointerElementOffset to basePosition for the live render (or subtract it before applying, not only before recording).
Notes
- Live-preview only; source persistence is always correct (refresh restores).
- Not markup-dependent (reproduced across compositions; the affected compositions use plain absolute clips with
translate: var(--hf-studio-offset-x/y)).
- Render is unaffected: the headless deterministic render path involves no mouse gestures, so the exported video never carries this defect.
Environment
- Self-hosted Studio, Node 22, Linux, Chrome
- Served over HTTPS behind a reverse proxy
- Versions 0.6.112 and 0.6.121 (identical behavior)
Summary
In the Studio canvas, after moving an element with a plain click-drag (pointerdown → move → mouseup), the very next button-less
pointermove(just moving the cursor over the canvas, no click) throws the element far off-screen. This is live-preview-only: the persisted source stays correct (the element keeps its committed position; a page refresh restores the on-screen render). It is independent of composition markup (reproduced across compositions) and present identically in 0.6.112 and 0.6.121. No keyboard shortcut is involved — this is the ordinary drag-to-reposition workflow.Affected versions
Confirmed present in 0.6.112 and 0.6.121. A function-level comparison of the served
dist/studio/assets/index-*.jsbetween the two versions shows the drag/gesture/calibration code is byte-identical modulo minified identifier names — i.e. this is not a recent regression, and a version rollback does not help.Primary bug — element flies off-screen on a button-less mousemove after a normal drag
Steps to reproduce
Expected: nothing happens (no active drag).
Actual: the element is flung far off-screen. The Inspector still shows the correct, committed X/Y and the source
--hf-studio-offsetis unchanged — only the live preview is wrong. A page refresh restores the correct render.Mechanism (confirmed by a real puppeteer repro: button genuinely held during the drag, player paused)
translateproperty (--hf-studio-offset); the selection box tracks the element 1:1; no source write happens during the move. Onmouseupthe commit fires exactly onePUTof the composition file, followed by asoftReloadof the preview.transformmatrix (e.g.matrix(1,0,0,1,-6067,-5054)) — i.e. the gesture-recording live-apply path (gsap.setaftertranslate:none), not the committed--hf-studio-offset(which stays correct).softReloaddoes not tear down the gesture/live-apply machinery — a document-level button-lesspointermovehandler (+ its RAF) survives the reload with a stalestartPointer. The next mouse move computesdelta = pointer − startPointerfrom that stale anchor → a huge translation → the element leaves the screen.transform: none). The bug therefore requires a drag + commit + softReload to "arm" the surviving handler. This is a softReload teardown defect, not an inherent cost of source persistence (SDK-cutover).Note on workaround: we verified that in-place mitigations do not clear the stale handler — clicking to deselect, re-selecting the element, and seeking/advancing the playhead all still result in the off-screen flight. Only a full page refresh reliably tears down the RAF/listener and restores a clean state.
Secondary bug — selection box desyncs from the element (~50px) during gesture-recording (
r) onlySteps to reproduce
rto start gesture recording.Expected: the element follows the selection box 1:1.
Actual: the element renders offset from the selection box by ~the grab distance (≈50px for a ~90px element). On stop it snaps back into the box.
Mechanism (read from the served bundle)
On the first move the recorder does
basePosition += pointerElementOffset(pointerElementOffset = pointer − elementCenter), so the live apply teleports the element center under the cursor — but the recorded sample subtracts the same offset back out (r.x -= cssVarOffset.x + pointerElementOffset.x / scale). The persisted coordinate is clean while the live element carries the grab offset; the overlay tracks the clean coordinate → constant desync = the grab offset.Suggested fixes
softReload, fully tear down the gesture/live-apply session — cancel the RAF andremoveEventListenerthe documentpointermovehandler (this is exactly what a manual refresh does). And/or ignore button-lesspointermovedeltas outside an explicit recording session. TheskipReloadpath that already exists in the bundle (commit without reload, re-render in place) would also avoid the reload that strands the handler.rdesync): make the live apply and the recorded sample use the same coordinate — don't addpointerElementOffsettobasePositionfor the live render (or subtract it before applying, not only before recording).Notes
translate: var(--hf-studio-offset-x/y)).Environment