mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:40:58 +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:
@@ -11,8 +11,6 @@ import {
|
||||
resolveTweenStart,
|
||||
resolveTweenDuration,
|
||||
} from "../utils/globalTimeCompiler";
|
||||
import { readAllAnimatedProperties } from "./gsapRuntimeReaders";
|
||||
|
||||
export interface GsapDragCommitCallbacks {
|
||||
commitMutation: (
|
||||
selection: DomEditSelection,
|
||||
@@ -114,7 +112,6 @@ async function extendTweenAndAddKeyframe(
|
||||
const newStart = Math.min(targetTime, tweenStart);
|
||||
const newEnd = Math.max(targetTime, tweenEnd);
|
||||
const newDuration = Math.max(0.01, newEnd - newStart);
|
||||
|
||||
const existingKfs = anim.keyframes?.keyframes ?? [];
|
||||
const remappedKfs: Array<{ percentage: number; properties: Record<string, number | string> }> =
|
||||
[];
|
||||
@@ -126,20 +123,15 @@ async function extendTweenAndAddKeyframe(
|
||||
|
||||
const targetPct = Math.round(((targetTime - newStart) / newDuration) * 1000) / 10;
|
||||
remappedKfs.push({ percentage: targetPct, properties });
|
||||
|
||||
remappedKfs.sort((a, b) => a.percentage - b.percentage);
|
||||
|
||||
await callbacks.commitMutation(
|
||||
selection,
|
||||
{ type: "delete", animationId: anim.id },
|
||||
{ label: "Extend tween range", skipReload: true },
|
||||
);
|
||||
|
||||
const selector = anim.targetSelector;
|
||||
await callbacks.commitMutation(
|
||||
selection,
|
||||
{
|
||||
type: "add-with-keyframes",
|
||||
targetSelector: selector,
|
||||
type: "replace-with-keyframes",
|
||||
animationId: anim.id,
|
||||
targetSelector: anim.targetSelector,
|
||||
position: Math.round(newStart * 1000) / 1000,
|
||||
duration: Math.round(newDuration * 1000) / 1000,
|
||||
keyframes: remappedKfs,
|
||||
@@ -156,8 +148,9 @@ async function commitKeyframedPosition(
|
||||
callbacks: GsapDragCommitCallbacks,
|
||||
beforeReload?: () => void,
|
||||
): Promise<void> {
|
||||
const pct = computeCurrentPercentage(selection, anim);
|
||||
|
||||
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
||||
const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
|
||||
if (activeKeyframePct != null) setActiveKeyframePct(null);
|
||||
await callbacks.commitMutation(
|
||||
selection,
|
||||
{
|
||||
@@ -182,10 +175,11 @@ async function commitFlatViaKeyframes(
|
||||
callbacks: GsapDragCommitCallbacks,
|
||||
beforeReload?: () => void,
|
||||
): Promise<void> {
|
||||
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
||||
await callbacks.commitMutation(
|
||||
selection,
|
||||
{ type: "convert-to-keyframes", animationId: anim.id },
|
||||
{ label: "Convert to keyframes for drag", skipReload: true },
|
||||
{ label: "Convert to keyframes for drag", skipReload: true, coalesceKey },
|
||||
);
|
||||
|
||||
const pct = computeCurrentPercentage(selection, anim);
|
||||
@@ -198,7 +192,7 @@ async function commitFlatViaKeyframes(
|
||||
percentage: pct,
|
||||
properties,
|
||||
},
|
||||
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload },
|
||||
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload, coalesceKey },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -243,19 +237,20 @@ export async function commitGsapPositionFromDrag(
|
||||
el.removeAttribute("data-hf-drag-initial-offset-y");
|
||||
};
|
||||
|
||||
const ct = usePlayerStore.getState().currentTime;
|
||||
if (anim.keyframes) {
|
||||
const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
|
||||
const effectiveAnim = newId ? { ...anim, id: newId } : anim;
|
||||
const runtimeProps = readAllAnimatedProperties(iframe, selector, anim);
|
||||
const dragProps: Record<string, number> = { x: newX, y: newY };
|
||||
|
||||
const ct = usePlayerStore.getState().currentTime;
|
||||
const ts = resolveTweenStart(effectiveAnim);
|
||||
const td = resolveTweenDuration(effectiveAnim);
|
||||
if (ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01)) {
|
||||
const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
||||
if (outsideRange) {
|
||||
await extendTweenAndAddKeyframe(
|
||||
selection,
|
||||
effectiveAnim,
|
||||
{ ...runtimeProps, x: newX, y: newY },
|
||||
dragProps,
|
||||
ct,
|
||||
ts,
|
||||
td,
|
||||
@@ -263,32 +258,126 @@ export async function commitGsapPositionFromDrag(
|
||||
restoreOffset,
|
||||
);
|
||||
} else {
|
||||
await commitKeyframedPosition(
|
||||
selection,
|
||||
effectiveAnim,
|
||||
{ ...runtimeProps, x: newX, y: newY },
|
||||
callbacks,
|
||||
restoreOffset,
|
||||
);
|
||||
await commitKeyframedPosition(selection, effectiveAnim, dragProps, callbacks, restoreOffset);
|
||||
}
|
||||
} else if (anim.method === "from" || anim.method === "fromTo") {
|
||||
await callbacks.commitMutation(
|
||||
selection,
|
||||
{
|
||||
type: "convert-to-keyframes",
|
||||
animationId: anim.id,
|
||||
resolvedFromValues: { x: newX, y: newY },
|
||||
},
|
||||
{ label: "Move layer (keyframe rest)", softReload: true, beforeReload: restoreOffset },
|
||||
);
|
||||
const ct = usePlayerStore.getState().currentTime;
|
||||
const ts = resolveTweenStart(anim);
|
||||
const td = resolveTweenDuration(anim);
|
||||
const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
||||
const dragProps: Record<string, number> = { x: newX, y: newY };
|
||||
|
||||
if (outsideRange && ts !== null) {
|
||||
// Split the original from() tween into property groups first.
|
||||
await callbacks.commitMutation(
|
||||
selection,
|
||||
{ type: "split-into-property-groups", animationId: anim.id },
|
||||
{ label: "Split from() for drag", skipReload: true },
|
||||
);
|
||||
|
||||
// Check if a position-group tween already exists (e.g. from gesture recording).
|
||||
// If so, extend it instead of creating a duplicate.
|
||||
const allAnims = await (async () => {
|
||||
const pid = selection.sourceFile || "index.html";
|
||||
try {
|
||||
const r = await fetch(
|
||||
`/api/projects/${encodeURIComponent(window.location.hash.match(/project\/([^?/]+)/)?.[1] ?? "")}/gsap-animations/${encodeURIComponent(pid)}`,
|
||||
);
|
||||
if (!r.ok) return [];
|
||||
const parsed = await r.json();
|
||||
return (parsed?.animations ?? []) as GsapAnimation[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
const existingPosAnim = allAnims.find(
|
||||
(a) => a.propertyGroup === "position" && a.targetSelector === anim.targetSelector,
|
||||
);
|
||||
|
||||
if (existingPosAnim?.keyframes) {
|
||||
// Extend the existing position tween
|
||||
const posTs = resolveTweenStart(existingPosAnim);
|
||||
const posTd = resolveTweenDuration(existingPosAnim);
|
||||
if (posTs !== null) {
|
||||
await extendTweenAndAddKeyframe(
|
||||
selection,
|
||||
existingPosAnim,
|
||||
{ x: newX, y: newY },
|
||||
ct,
|
||||
posTs,
|
||||
posTd,
|
||||
callbacks,
|
||||
restoreOffset,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No existing position tween — create one
|
||||
const newStart = Math.min(ct, ts);
|
||||
const newEnd = Math.max(ct, ts + td);
|
||||
const newDuration = Math.max(0.01, newEnd - newStart);
|
||||
const dragBefore = ct < ts;
|
||||
const origStartPct = Math.round(((ts - newStart) / newDuration) * 1000) / 10;
|
||||
const origEndPct = Math.round(((ts + td - newStart) / newDuration) * 1000) / 10;
|
||||
|
||||
const keyframes: Array<{ percentage: number; properties: Record<string, number | string> }> =
|
||||
[];
|
||||
if (dragBefore) {
|
||||
keyframes.push({ percentage: 0, properties: { x: newX, y: newY } });
|
||||
if (origStartPct > 0.5 && origStartPct < 99.5) {
|
||||
keyframes.push({ percentage: origStartPct, properties: { x: 0, y: 0 } });
|
||||
}
|
||||
keyframes.push({ percentage: 100, properties: { x: 0, y: 0 } });
|
||||
} else {
|
||||
keyframes.push({ percentage: 0, properties: { x: 0, y: 0 } });
|
||||
if (origEndPct > 0.5 && origEndPct < 99.5) {
|
||||
keyframes.push({ percentage: origEndPct, properties: { x: 0, y: 0 } });
|
||||
}
|
||||
keyframes.push({ percentage: 100, properties: { x: newX, y: newY } });
|
||||
}
|
||||
keyframes.sort((a, b) => a.percentage - b.percentage);
|
||||
|
||||
await callbacks.commitMutation(
|
||||
selection,
|
||||
{
|
||||
type: "add-with-keyframes",
|
||||
targetSelector: anim.targetSelector,
|
||||
position: Math.round(newStart * 1000) / 1000,
|
||||
duration: Math.round(newDuration * 1000) / 1000,
|
||||
keyframes,
|
||||
},
|
||||
{ label: "Move layer (from extended)", softReload: true, beforeReload: restoreOffset },
|
||||
);
|
||||
} else {
|
||||
// Inside tween range: convert then add keyframe at current time
|
||||
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
||||
await callbacks.commitMutation(
|
||||
selection,
|
||||
{
|
||||
type: "convert-to-keyframes",
|
||||
animationId: anim.id,
|
||||
},
|
||||
{ label: "Convert from() for drag", skipReload: true, coalesceKey },
|
||||
);
|
||||
const pct = computeCurrentPercentage(selection, anim);
|
||||
await callbacks.commitMutation(
|
||||
selection,
|
||||
{
|
||||
type: "add-keyframe",
|
||||
animationId: anim.id,
|
||||
percentage: pct,
|
||||
properties: dragProps,
|
||||
},
|
||||
{
|
||||
label: `Move layer (keyframe ${pct}%)`,
|
||||
softReload: true,
|
||||
beforeReload: restoreOffset,
|
||||
coalesceKey,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const runtimeProps = readAllAnimatedProperties(iframe, selector, anim);
|
||||
await commitFlatViaKeyframes(
|
||||
selection,
|
||||
anim,
|
||||
{ ...runtimeProps, x: newX, y: newY },
|
||||
callbacks,
|
||||
restoreOffset,
|
||||
);
|
||||
await commitFlatViaKeyframes(selection, anim, { x: newX, y: newY }, callbacks, restoreOffset);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user