Files
hyperframes/packages/studio/src/hooks/gsapWholePropertyOffsetCommit.ts
T
Miguel Ángel 5d59835446 fix(studio): static-position drag no longer freezes an element beside an animated rotation (#2016)
## What

Brief description of the change.

## Why

Why is this change needed?

## How

How was this implemented? Any notable design decisions?

## Test plan

How was this tested?

- [ ] Unit tests added/updated
- [ ] Manual testing performed
- [ ] Documentation updated (if applicable)
2026-07-07 03:45:17 -04:00

87 lines
3.4 KiB
TypeScript

/**
* commitWholePropertyOffset — extracted from gsapDragCommit.ts to keep file
* sizes under the 600-line limit (mirrors gsapDragPositionCommit.ts).
*/
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import type { DomEditSelection } from "../components/editor/domEditingTypes";
import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
import { roundTo3 } from "../utils/rounding";
import { PROPERTY_DEFAULTS } from "./gsapShared";
import { synthesizeFlatTweenKeyframes } from "./gsapTweenSynth";
import { materializeIfDynamic, type GsapDragCommitCallbacks } from "./gsapDragCommit";
/**
* Generic sibling of commitWholePathOffset for property groups other than
* position (rotation, size, scale) — shift every keyframe of `anim` by the
* same delta for each key in `newValues`, preserving the tween's shape. The
* delta is computed against the keyframe NEAREST `currentPct` (the one an
* ordinary edit would otherwise overwrite), not the live DOM: by the time a
* drag gesture reaches its commit step, the preview has often already
* gsap.set the dragged property to its new value, so the DOM can no longer
* tell "old" from "new".
*/
export async function commitWholePropertyOffset(
selection: DomEditSelection,
anim: GsapAnimation,
newValues: Record<string, number>,
currentPct: number,
iframe: HTMLIFrameElement | null,
callbacks: GsapDragCommitCallbacks,
label: string,
): Promise<void> {
// fallow-ignore-next-line code-duplication
let effectiveAnim = anim;
if (anim.keyframes) {
const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
if (newId) effectiveAnim = { ...anim, id: newId };
}
const ts = resolveTweenStart(effectiveAnim);
const td = resolveTweenDuration(effectiveAnim);
const ease = effectiveAnim.keyframes?.easeEach ?? effectiveAnim.ease;
const keys = Object.keys(newValues);
const at = (props: Record<string, number | string>, key: string) =>
typeof props[key] === "number" ? (props[key] as number) : (PROPERTY_DEFAULTS[key] ?? 0);
const kfs =
effectiveAnim.keyframes?.keyframes ?? synthesizeFlatTweenKeyframes(effectiveAnim)?.keyframes;
if (!kfs || kfs.length === 0) {
// A `to()`/`from()` collapsed to a zero-duration immediateRender hold (what
// removeAllKeyframesFromScript leaves behind) has no shape to preserve —
// just persist the flat value instead of replacing with an empty keyframe list.
await callbacks.commitMutation(
selection,
{ type: "update-properties", animationId: effectiveAnim.id, properties: newValues },
{ label, softReload: true },
);
return;
}
const nearest = kfs.reduce((best, kf) =>
Math.abs(kf.percentage - currentPct) < Math.abs(best.percentage - currentPct) ? kf : best,
);
const shifted = kfs.map((kf) => {
const properties = { ...kf.properties };
for (const key of keys) {
properties[key] = roundTo3(
at(properties, key) + (newValues[key]! - at(nearest.properties, key)),
);
}
return { percentage: kf.percentage, properties, ...(kf.ease ? { ease: kf.ease } : {}) };
});
await callbacks.commitMutation(
selection,
{
type: "replace-with-keyframes",
animationId: effectiveAnim.id,
targetSelector: effectiveAnim.targetSelector,
position: roundTo3(ts ?? 0),
duration: roundTo3(td || 1),
keyframes: shifted,
ease,
},
{ label, softReload: true },
);
}