/** * 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, currentPct: number, iframe: HTMLIFrameElement | null, callbacks: GsapDragCommitCallbacks, label: string, ): Promise { // 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, 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 }, ); }