mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(studio): per-property-group intercept routing + drag/resize fixes (#1356)
* fix(core): per-property-group keyframe foundations
Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.
* fix(core): add split-into-property-groups and replace-with-keyframes mutations
Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
* fix(studio): per-property-group intercept routing + drag/resize fixes
Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y}
to position group, resize routes to scale group via data-hf-studio-original-width,
rotation routes to rotation group. Add resolveGroupTween helper, from-extend
with split-first-then-position-only pattern, autoKeyframeEnabled guards,
GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs.
This commit is contained in:
@@ -32,7 +32,7 @@ import {
|
||||
} from "./manualEditsTypes";
|
||||
import { roundRotationAngle } from "./manualEditsParsing";
|
||||
import { applyStudioMotionFromDom } from "./studioMotion";
|
||||
import { gsapAnimatesProperty, gsapAnimatesTransform } from "./gsapAnimatesProperty";
|
||||
import { gsapAnimatesProperty } from "./gsapAnimatesProperty";
|
||||
|
||||
/* ── Gesture tracking ─────────────────────────────────────────────── */
|
||||
let studioManualEditGestureId = 0;
|
||||
@@ -223,7 +223,6 @@ function isIdentityAfterTranslateStrip(m: DOMMatrix): boolean {
|
||||
return m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1;
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
function stripGsapTranslateFromTransform(element: HTMLElement): void {
|
||||
if (element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR)) return;
|
||||
const transform = element.style.getPropertyValue("transform");
|
||||
@@ -258,18 +257,6 @@ export function applyStudioPathOffset(
|
||||
): void {
|
||||
promoteInlineForTransform(element);
|
||||
writeStudioPathOffsetVars(element, offset, { updateBase: options.updateBase ?? true });
|
||||
if (gsapAnimatesTransform(element)) {
|
||||
// GSAP folded the CSS translate into its transform cache at init and owns
|
||||
// style.transform from then on — it zeroes the translate longhand exactly
|
||||
// once (at fold time) and never re-reads it. Writing translate here would
|
||||
// double-apply the offset on top of the baked transform. Keep translate
|
||||
// neutral in the live DOM and push the offset into GSAP's cache instead;
|
||||
// the var() expression is persisted to the source file by the patch
|
||||
// builder, where a reload re-folds it.
|
||||
element.style.setProperty("translate", "none");
|
||||
syncGsapOwnedTransformPosition(element);
|
||||
return;
|
||||
}
|
||||
element.style.setProperty(
|
||||
"translate",
|
||||
composeTranslateValue(
|
||||
@@ -281,24 +268,6 @@ export function applyStudioPathOffset(
|
||||
stripGsapTranslateFromTransform(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* After committing a new path offset on an element whose transform GSAP owns,
|
||||
* GSAP's internal cache still holds the pre-drag baked translate — the next
|
||||
* seek re-renders from that cache and snaps the element back. Push the new
|
||||
* offset into GSAP so live scrubbing matches what was persisted. (A page
|
||||
* reload re-initializes GSAP from the persisted CSS translate, so this is
|
||||
* only needed for the live session.)
|
||||
*/
|
||||
function syncGsapOwnedTransformPosition(element: HTMLElement): void {
|
||||
if (!gsapAnimatesTransform(element)) return;
|
||||
const win = element.ownerDocument.defaultView as
|
||||
| (Window & { gsap?: { set: (el: Element, vars: Record<string, unknown>) => void } })
|
||||
| null;
|
||||
if (!win?.gsap?.set) return;
|
||||
const { x, y } = readStudioPathOffset(element);
|
||||
win.gsap.set(element, { x, y });
|
||||
}
|
||||
|
||||
export function applyStudioPathOffsetDraft(
|
||||
element: HTMLElement,
|
||||
offset: { x: number; y: number },
|
||||
@@ -306,16 +275,36 @@ export function applyStudioPathOffsetDraft(
|
||||
promoteInlineForTransform(element);
|
||||
writeStudioPathOffsetVars(element, offset, { updateBase: false });
|
||||
|
||||
const isGsapAnimated = gsapAnimatesTransform(element);
|
||||
const isGsapAnimated = gsapAnimatesProperty(element, "x", "y");
|
||||
if (isGsapAnimated) {
|
||||
// GSAP owns style.transform (see applyStudioPathOffset): position via
|
||||
// gsap.set while the timeline is paused. Set translate:none explicitly to
|
||||
// prevent double-counting with the baked transform.
|
||||
element.style.setProperty("translate", "none");
|
||||
const win = element.ownerDocument.defaultView as
|
||||
| (Window & { gsap?: { set: (el: Element, vars: Record<string, unknown>) => void } })
|
||||
| (Window & {
|
||||
gsap?: {
|
||||
set: (el: Element, vars: Record<string, unknown>) => void;
|
||||
getProperty: (el: Element, prop: string) => number;
|
||||
};
|
||||
})
|
||||
| null;
|
||||
win?.gsap?.set(element, { x: offset.x, y: offset.y });
|
||||
if (win?.gsap) {
|
||||
const baseX = Number.parseFloat(element.getAttribute("data-hf-drag-gsap-base-x") ?? "");
|
||||
const baseY = Number.parseFloat(element.getAttribute("data-hf-drag-gsap-base-y") ?? "");
|
||||
const origX = Number.parseFloat(element.getAttribute("data-hf-drag-initial-offset-x") ?? "");
|
||||
const origY = Number.parseFloat(element.getAttribute("data-hf-drag-initial-offset-y") ?? "");
|
||||
const gsapBaseX = Number.isFinite(baseX)
|
||||
? baseX
|
||||
: (win.gsap.getProperty(element, "x") as number);
|
||||
const gsapBaseY = Number.isFinite(baseY)
|
||||
? baseY
|
||||
: (win.gsap.getProperty(element, "y") as number);
|
||||
if (!Number.isFinite(baseX))
|
||||
element.setAttribute("data-hf-drag-gsap-base-x", String(gsapBaseX));
|
||||
if (!Number.isFinite(baseY))
|
||||
element.setAttribute("data-hf-drag-gsap-base-y", String(gsapBaseY));
|
||||
const deltaX = offset.x - (Number.isFinite(origX) ? origX : 0);
|
||||
const deltaY = offset.y - (Number.isFinite(origY) ? origY : 0);
|
||||
win.gsap.set(element, { x: gsapBaseX + deltaX, y: gsapBaseY + deltaY });
|
||||
}
|
||||
} else {
|
||||
// Non-GSAP elements: use CSS translate as before.
|
||||
element.style.setProperty(
|
||||
@@ -551,14 +540,10 @@ function queryStudioElements(doc: Document, attr: string): HTMLElement[] {
|
||||
|
||||
function reapplyPathOffsets(doc: Document): void {
|
||||
for (const el of queryStudioElements(doc, STUDIO_PATH_OFFSET_ATTR)) {
|
||||
// Skip elements where GSAP owns the transform stack — GSAP bakes the
|
||||
// CSS translate into its transform and sets translate: none every tick
|
||||
// when it tweens ANY transform property (x/y, scale, rotation, ...).
|
||||
// Stripping/restoring would oscillate against GSAP's rendering and
|
||||
// double-apply the offset.
|
||||
if (gsapAnimatesTransform(el)) continue;
|
||||
const gsapSkip = gsapAnimatesProperty(el, "x", "y");
|
||||
const x = el.style.getPropertyValue(STUDIO_OFFSET_X_PROP);
|
||||
const y = el.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
|
||||
if (gsapSkip) continue;
|
||||
if (x || y) {
|
||||
applyStudioPathOffset(
|
||||
el,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
styleUsesStudioRotation,
|
||||
restoreInlineDisplay,
|
||||
} from "./manualEditsDom";
|
||||
import { gsapAnimatesTransform } from "./gsapAnimatesProperty";
|
||||
import {
|
||||
STUDIO_OFFSET_X_PROP,
|
||||
STUDIO_OFFSET_Y_PROP,
|
||||
@@ -88,23 +87,7 @@ export function captureStudioRotation(element: HTMLElement): StudioRotationSnaps
|
||||
};
|
||||
}
|
||||
|
||||
type GsapWindow = Window & {
|
||||
gsap?: {
|
||||
getProperty?: (el: Element, prop: string) => number | string;
|
||||
set?: (el: Element, vars: Record<string, unknown>) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export function captureStudioPathOffset(element: HTMLElement): StudioPathOffsetSnapshot {
|
||||
let gsapX: number | null = null;
|
||||
let gsapY: number | null = null;
|
||||
if (gsapAnimatesTransform(element)) {
|
||||
const win = element.ownerDocument.defaultView as GsapWindow | null;
|
||||
if (win?.gsap?.getProperty) {
|
||||
gsapX = Number(win.gsap.getProperty(element, "x")) || 0;
|
||||
gsapY = Number(win.gsap.getProperty(element, "y")) || 0;
|
||||
}
|
||||
}
|
||||
return {
|
||||
translate: element.style.getPropertyValue("translate"),
|
||||
x: element.style.getPropertyValue(STUDIO_OFFSET_X_PROP),
|
||||
@@ -112,8 +95,6 @@ export function captureStudioPathOffset(element: HTMLElement): StudioPathOffsetS
|
||||
marker: element.getAttribute(STUDIO_PATH_OFFSET_ATTR),
|
||||
originalTranslate: element.getAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR),
|
||||
originalInlineTranslate: element.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR),
|
||||
gsapX,
|
||||
gsapY,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -203,11 +184,20 @@ export function restoreStudioPathOffset(
|
||||
previous.originalInlineTranslate,
|
||||
);
|
||||
|
||||
// Draft positioning on GSAP-owned elements goes through gsap.set, which
|
||||
// mutates GSAP's transform cache — restore it alongside the inline styles.
|
||||
if (previous.gsapX != null || previous.gsapY != null) {
|
||||
const win = element.ownerDocument.defaultView as GsapWindow | null;
|
||||
win?.gsap?.set?.(element, { x: previous.gsapX ?? 0, y: previous.gsapY ?? 0 });
|
||||
// Restore GSAP x/y if a draft was applied via gsap.set during drag
|
||||
const baseX = element.getAttribute("data-hf-drag-gsap-base-x");
|
||||
const baseY = element.getAttribute("data-hf-drag-gsap-base-y");
|
||||
if (baseX != null || baseY != null) {
|
||||
const win = element.ownerDocument.defaultView as
|
||||
| (Window & { gsap?: { set: (el: Element, vars: Record<string, unknown>) => void } })
|
||||
| null;
|
||||
if (win?.gsap) {
|
||||
const x = Number.parseFloat(baseX ?? "0") || 0;
|
||||
const y = Number.parseFloat(baseY ?? "0") || 0;
|
||||
win.gsap.set(element, { x, y });
|
||||
}
|
||||
element.removeAttribute("data-hf-drag-gsap-base-x");
|
||||
element.removeAttribute("data-hf-drag-gsap-base-y");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,12 +101,4 @@ export interface StudioPathOffsetSnapshot {
|
||||
marker: string | null;
|
||||
originalTranslate: string | null;
|
||||
originalInlineTranslate: string | null;
|
||||
/**
|
||||
* GSAP's cached x/y at capture time, for elements whose transform GSAP
|
||||
* owns. Draft positioning mutates GSAP's cache (gsap.set), which inline
|
||||
* style restoration alone cannot undo. Null when GSAP does not own the
|
||||
* element's transform.
|
||||
*/
|
||||
gsapX: number | null;
|
||||
gsapY: number | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user