fix(studio): keyframe bug fixes — gate delete hooks, fix value corruption, gesture recording (#1314)

- Gate stripStudioEditsFromTarget/bakeVisibilityOnDelete behind a
  stripStudioEdits flag on the delete mutation type so they only fire on
  user-initiated deletes, not on internal delete-then-recreate drags.

- Add bakeVisibilityOnDelete to the remove-all-keyframes handler so
  elements with CSS opacity:0 stay visible after collapsing keyframes.

- Fix integer rounding in readAllAnimatedProperties: use 3-decimal
  precision for visual properties (opacity, scale, rotation) instead of
  Math.round which corrupted mid-fade values to 0.

- Guard VISUAL_BASELINE against cross-tween contamination by querying
  __timelines for properties animated by other tweens on the same element.

- Harden bakeVisibilityOnDelete: reverse-scan keyframes for the last one
  containing opacity, guard against relative values (+=/-=/*=), and add
  Number.isFinite check.

- Fix falsy-zero doubling in drag commit: replace || fallback with
  Number.isFinite so a base GSAP position of 0 is correctly preserved.

- Fix gesture recording sign inversion: remove pointerElementOffset
  subtraction from dx/dy formula and instead apply it once to basePosition
  so the element center tracks the pointer.

- Fix TypeScript build errors in gsapSoftReload.ts (6 double-casts).

- Strip all diagnostic logs from production code.
This commit is contained in:
Miguel Ángel
2026-06-10 18:53:06 -04:00
committed by GitHub
parent 3a72aa528d
commit f0b499b582
38 changed files with 1641 additions and 821 deletions
@@ -36,15 +36,46 @@ interface CommitAnimatedPropertyDeps {
bumpGsapCache: () => void;
}
function computePercentage(selection: DomEditSelection): number {
function computePercentage(selection: DomEditSelection, anim?: GsapAnimation): number {
const currentTime = usePlayerStore.getState().currentTime;
const tweenPos = typeof anim?.position === "number" ? anim.position : 0;
const tweenDur = anim?.duration ?? 0;
if (tweenDur > 0) {
return Math.max(
0,
Math.min(100, Math.round(((currentTime - tweenPos) / tweenDur) * 1000) / 10),
);
}
const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
const currentTime = usePlayerStore.getState().currentTime;
return elDuration > 0
? Math.max(0, Math.min(100, Math.round(((currentTime - elStart) / elDuration) * 1000) / 10))
: 0;
}
function pickBestAnimation(
animations: GsapAnimation[],
selector: string | null,
): GsapAnimation | undefined {
if (animations.length <= 1) return animations[0];
const currentTime = usePlayerStore.getState().currentTime;
const scored = animations.map((a) => {
let score = 0;
if (a.keyframes) score += 10;
// Prefer single-element selectors over comma-separated groups
if (selector && a.targetSelector === selector) score += 5;
else if (a.targetSelector.includes(",")) score -= 3;
// Prefer tweens active at the current time
const pos = typeof a.position === "number" ? a.position : 0;
const dur = a.duration ?? 0;
if (currentTime >= pos - 0.05 && currentTime <= pos + dur + 0.05) score += 8;
return { anim: a, score };
});
scored.sort((a, b) => b.score - a.score);
return scored[0]?.anim;
}
function selectorFor(selection: DomEditSelection): string | null {
if (selection.id) return `#${selection.id}`;
if (selection.selector) return selection.selector;
@@ -70,10 +101,8 @@ export function useAnimatedPropertyCommit(deps: CommitAnimatedPropertyDeps) {
const iframe = previewIframeRef.current;
const selector = selectorFor(selection);
const pct = computePercentage(selection);
let anim: GsapAnimation | undefined =
selectedGsapAnimations.find((a) => a.keyframes) ?? selectedGsapAnimations[0];
let anim: GsapAnimation | undefined = pickBestAnimation(selectedGsapAnimations, selector);
// Case 3: No animation — create one first
if (!anim) {
@@ -97,6 +126,8 @@ export function useAnimatedPropertyCommit(deps: CommitAnimatedPropertyDeps) {
);
}
const pct = computePercentage(selection, anim);
// Read all currently animated properties from runtime for backfill
const runtimeProps = selector ? readAllAnimatedProperties(iframe, selector, anim) : {};
@@ -112,15 +143,26 @@ export function useAnimatedPropertyCommit(deps: CommitAnimatedPropertyDeps) {
}
backfillDefaults[property] = typeof value === "number" ? value : value;
const existingKf = anim.keyframes?.keyframes.some(
(kf) => Math.abs(kf.percentage - pct) < 0.05,
);
await gsapCommitMutation(
selection,
{
type: "add-keyframe",
animationId: anim.id,
percentage: pct,
properties,
backfillDefaults,
},
existingKf
? {
type: "update-keyframe",
animationId: anim.id,
percentage: pct,
properties,
}
: {
type: "add-keyframe",
animationId: anim.id,
percentage: pct,
properties,
backfillDefaults,
},
{ label: `Edit ${property} (keyframe ${pct}%)`, softReload: true },
);
},