Skip to content

Commit b5c2776

Browse files
committed
fix(studio): keep a keyframe edit from copying other tweens' channels
1 parent c4dd776 commit b5c2776

2 files changed

Lines changed: 51 additions & 90 deletions

File tree

‎packages/studio/src/hooks/gsapRuntimeReaders.test.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @vitest-environment jsdom
22
import { afterEach, describe, expect, it } from "vitest";
3+
import { gsap } from "gsap";
34
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
45
import {
56
COLOR_GRADING_SOURCE_HIDDEN_ATTR,
@@ -109,3 +110,50 @@ describe("color-grading opacity truth", () => {
109110
expect(readGsapProperty(iframe, "#clip", "opacity")).toBe(0.3);
110111
});
111112
});
113+
114+
describe("readAllAnimatedProperties with a sibling tween", () => {
115+
it("leaves a sibling tween's rotation to that tween after an opacity keyframe edit", () => {
116+
const el = document.createElement("div");
117+
el.id = "clip";
118+
document.body.appendChild(el);
119+
const build = (keyframes: Record<string, Record<string, number>>) => {
120+
const tl = gsap.timeline({ paused: true });
121+
tl.to(el, { duration: 4, ease: "none", keyframes }, 0);
122+
tl.to(el, { rotation: 90, duration: 3 }, 0);
123+
return tl;
124+
};
125+
const live = build({ "0%": { opacity: 0 }, "100%": { opacity: 1 } });
126+
live.seek(2);
127+
const iframe = {
128+
contentWindow: { __timelines: { main: live }, gsap },
129+
contentDocument: document,
130+
} as unknown as HTMLIFrameElement;
131+
const anim = {
132+
id: "#clip-to-0-visual",
133+
targetSelector: "#clip",
134+
method: "to",
135+
properties: {},
136+
keyframes: {
137+
keyframes: [
138+
{ percentage: 0, properties: { opacity: 0 } },
139+
{ percentage: 100, properties: { opacity: 1 } },
140+
],
141+
},
142+
} as unknown as GsapAnimation;
143+
144+
// commitKeyframeProps: these values plus the edit at the playhead, and backfilled into the rest.
145+
const read = readAllAnimatedProperties(iframe, "#clip", anim);
146+
live.kill();
147+
gsap.set(el, { clearProps: "all" });
148+
const edited = build({
149+
"0%": { ...read, opacity: 0 },
150+
"50%": { ...read, opacity: 0.3 },
151+
"100%": { ...read, opacity: 1 },
152+
});
153+
edited.seek(3.2);
154+
edited.seek(3.5);
155+
156+
expect(gsap.getProperty(el, "rotation")).toBe(90);
157+
edited.kill();
158+
});
159+
});

‎packages/studio/src/hooks/gsapRuntimeReaders.ts‎

Lines changed: 3 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
HF_COLOR_GRADING_CANVAS_ID_PREFIX,
99
} from "@hyperframes/core/color-grading";
1010
import { getIframeGsap, queryIframeElement, type IframeGsap } from "./gsapShared";
11+
import { gsapWritesChannels } from "./gsapRuntimeKeyframes";
1112
import { roundTo3 } from "../utils/rounding";
1213

1314
/**
@@ -50,27 +51,6 @@ export function readGsapProperty(
5051
}
5152

5253
export const POSITION_PROPS = new Set(["x", "y", "xPercent", "yPercent"]);
53-
const GSAP_CONFIG_KEYS = new Set([
54-
"duration",
55-
"ease",
56-
"delay",
57-
"stagger",
58-
"id",
59-
"onComplete",
60-
"onUpdate",
61-
"onStart",
62-
"onRepeat",
63-
"repeat",
64-
"yoyo",
65-
"repeatDelay",
66-
"paused",
67-
"immediateRender",
68-
"lazy",
69-
"overwrite",
70-
"keyframes",
71-
"parent",
72-
]);
73-
7454
export function readAllAnimatedProperties(
7555
iframe: HTMLIFrameElement | null,
7656
selector: string,
@@ -115,74 +95,7 @@ export function readAllAnimatedProperties(
11595
}
11696
}
11797

118-
const otherTweenProps = new Set<string>();
119-
try {
120-
const win = iframe.contentWindow as unknown as { __timelines?: Record<string, unknown> };
121-
const timelines = win.__timelines;
122-
if (timelines) {
123-
for (const tl of Object.values(timelines)) {
124-
const tlObj = tl as {
125-
getChildren?: (
126-
deep: boolean,
127-
) => Array<{ targets?: () => Element[]; vars?: Record<string, unknown> }>;
128-
};
129-
if (!tlObj?.getChildren) continue;
130-
for (const child of tlObj.getChildren(true)) {
131-
if (typeof child.targets !== "function") continue;
132-
const targets = child.targets();
133-
if (!targets.includes(el)) continue;
134-
const vars = child.vars;
135-
if (!vars) continue;
136-
for (const k of Object.keys(vars)) {
137-
if (!GSAP_CONFIG_KEYS.has(k) && inGroup(k)) otherTweenProps.add(k);
138-
}
139-
}
140-
}
141-
}
142-
} catch {}
143-
for (const p of groupedPropKeys) otherTweenProps.delete(p);
144-
145-
// Tier 1: Transform + visual properties with universal CSS defaults.
146-
// Safe to compare against hardcoded values — these are always 0 or 1
147-
// regardless of the element's stylesheet.
148-
const UNIVERSAL_BASELINE: Record<string, number> = {
149-
opacity: 1,
150-
scale: 1,
151-
scaleX: 1,
152-
scaleY: 1,
153-
scaleZ: 1,
154-
rotation: 0,
155-
rotationX: 0,
156-
rotationY: 0,
157-
skewX: 0,
158-
skewY: 0,
159-
z: 0,
160-
xPercent: 0,
161-
yPercent: 0,
162-
transformPerspective: 0,
163-
blur: 0,
164-
brightness: 1,
165-
contrast: 1,
166-
saturate: 1,
167-
hueRotate: 0,
168-
grayscale: 0,
169-
sepia: 0,
170-
invert: 0,
171-
};
172-
// Collect all properties that ANY tween on this element explicitly targets.
173-
// Only capture baseline values for these — GSAP reports non-default values
174-
// (scaleZ=0, brightness=0) for untouched properties, polluting keyframes.
175-
const allTweenedProps = new Set([...groupedPropKeys, ...otherTweenProps]);
176-
for (const [prop, defaultVal] of Object.entries(UNIVERSAL_BASELINE)) {
177-
if (prop in result) continue;
178-
if (!allTweenedProps.has(prop)) continue;
179-
const val = readLiveGsapValue(gsap, el, prop);
180-
if (Number.isFinite(val) && Math.round(val * 1000) !== Math.round(defaultVal * 1000)) {
181-
result[prop] = roundTo3(val);
182-
}
183-
}
184-
185-
// Tier 2: Element-dependent properties — their "default" depends on the
98+
// Element-dependent properties — their "default" depends on the
18699
// stylesheet, so we compare GSAP's runtime value against the element's
187100
// computed CSS value. Only capture if GSAP has actively changed it.
188101
const COMPUTED_BASELINE = [
@@ -209,7 +122,7 @@ export function readAllAnimatedProperties(
209122
for (const prop of COMPUTED_BASELINE) {
210123
if (prop in result) continue;
211124
if (!inGroup(prop)) continue;
212-
if (otherTweenProps.has(prop)) continue;
125+
if (gsapWritesChannels(el, [prop])) continue;
213126
const gsapVal = Number(gsap.getProperty(el, prop));
214127
if (!Number.isFinite(gsapVal)) continue;
215128
let cssVal = NaN;

0 commit comments

Comments
 (0)