@hyperframes/studio 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-B_NnmU6q.js → index--lOAjFJl.js} +1 -1
- package/dist/assets/{index-ysftPins.js → index-BSyZKYmH.js} +204 -203
- package/dist/assets/{index-DsBhGFPe.js → index-Dgeszckd.js} +1 -1
- package/dist/assets/index-svYFaNuq.css +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.html +2 -2
- package/dist/index.js +3650 -2431
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/components/StudioRightPanel.tsx +7 -1
- package/src/components/editor/AnimationCard.tsx +6 -0
- package/src/components/editor/DomEditOverlay.tsx +4 -12
- package/src/components/editor/EaseCurveSection.tsx +175 -99
- package/src/components/editor/GsapAnimationSection.tsx +2 -0
- package/src/components/editor/KeyframeEaseList.tsx +67 -3
- package/src/components/editor/MarqueeOverlay.tsx +40 -0
- package/src/components/editor/OffCanvasIndicators.tsx +2 -7
- package/src/components/editor/PropertyPanel.tsx +23 -2
- package/src/components/editor/Transform3DCube.tsx +313 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +2 -0
- package/src/components/editor/gsapAnimationConstants.ts +11 -35
- package/src/components/editor/gsapAnimationHelpers.test.ts +1 -1
- package/src/components/editor/marqueeCommit.ts +63 -20
- package/src/components/editor/propertyPanel3dTransform.tsx +311 -92
- package/src/components/editor/propertyPanelHelpers.ts +46 -18
- package/src/components/editor/propertyPanelTimingSection.tsx +35 -2
- package/src/components/editor/transform3dProjection.test.ts +99 -0
- package/src/components/editor/transform3dProjection.ts +172 -0
- package/src/components/editor/useMotionPathData.ts +2 -1
- package/src/components/sidebar/AssetContextMenu.tsx +97 -0
- package/src/components/sidebar/AssetsTab.tsx +364 -266
- package/src/components/sidebar/AudioRow.tsx +202 -0
- package/src/components/sidebar/assetHelpers.ts +40 -0
- package/src/contexts/DomEditContext.tsx +8 -0
- package/src/hooks/gsapDragCommit.test.ts +9 -5
- package/src/hooks/gsapDragCommit.ts +129 -9
- package/src/hooks/gsapRuntimeBridge.ts +22 -0
- package/src/hooks/gsapRuntimeKeyframes.test.ts +47 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +63 -5
- package/src/hooks/gsapRuntimePatch.ts +162 -35
- package/src/hooks/useAnimatedPropertyCommit.ts +256 -78
- package/src/hooks/useDomEditCommits.ts +0 -2
- package/src/hooks/useDomEditSession.ts +24 -2
- package/src/hooks/useDomEditWiring.ts +3 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -31
- package/src/hooks/useGestureCommit.ts +0 -4
- package/src/hooks/useGsapAwareEditing.ts +31 -5
- package/src/hooks/useGsapKeyframeOps.ts +4 -1
- package/src/hooks/useGsapScriptCommits.ts +0 -12
- package/src/hooks/useGsapSelectionHandlers.ts +12 -9
- package/src/hooks/useGsapTweenCache.test.ts +45 -1
- package/src/hooks/useGsapTweenCache.ts +125 -42
- package/src/hooks/useMusicBeatAnalysis.ts +36 -21
- package/src/hooks/useRazorSplit.ts +0 -3
- package/src/utils/marqueeGeometry.test.ts +15 -98
- package/src/utils/marqueeGeometry.ts +1 -158
- package/dist/assets/index-wJZf6FK3.css +0 -1
- package/src/utils/editDebugLog.ts +0 -9
|
@@ -12,12 +12,23 @@ import { buildArcPath, type ArcPathConfig } from "@hyperframes/core/gsap-parser-
|
|
|
12
12
|
import { parsePercentageKeyframes, toAbsoluteTime } from "./gsapShared";
|
|
13
13
|
import { roundTo3 } from "../utils/rounding";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A GSAP tween's `vars` object — intentionally open: it mixes channel values
|
|
17
|
+
* (numbers), easing (strings), flags (booleans), nested keyframes (objects) and
|
|
18
|
+
* callbacks. Named so call sites read as "GSAP config", not an untyped escape hatch.
|
|
19
|
+
*/
|
|
20
|
+
export type GsapVars = Record<string, unknown>;
|
|
21
|
+
|
|
15
22
|
export interface RuntimeTween {
|
|
16
23
|
targets?: () => Element[];
|
|
17
|
-
vars?:
|
|
24
|
+
vars?: GsapVars;
|
|
18
25
|
duration?: () => number;
|
|
19
26
|
startTime?: () => number;
|
|
20
27
|
invalidate?: () => RuntimeTween;
|
|
28
|
+
/** Remove this tween from its parent timeline (GSAP `kill()`). */
|
|
29
|
+
kill?: () => void;
|
|
30
|
+
/** The timeline this tween lives in — used to re-insert a rebuilt tween. */
|
|
31
|
+
parent?: RuntimeTimeline;
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
export interface RuntimeTimeline {
|
|
@@ -25,6 +36,8 @@ export interface RuntimeTimeline {
|
|
|
25
36
|
duration?: () => number;
|
|
26
37
|
time?: () => number;
|
|
27
38
|
invalidate?: () => RuntimeTimeline;
|
|
39
|
+
/** Add a tween at an absolute position — used to rebuild a keyframe tween in place. */
|
|
40
|
+
to?: (targets: Element[], vars: GsapVars, position?: number) => RuntimeTween;
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
type Pct = { percentage: number; properties: Record<string, number | string> };
|
|
@@ -184,6 +197,26 @@ function varsCarryChannel(vars: Record<string, unknown> | undefined, channels: s
|
|
|
184
197
|
return false;
|
|
185
198
|
}
|
|
186
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Like `varsCarryChannel` but for a keyframe tween: the channels live inside the
|
|
202
|
+
* keyframe steps (`vars.keyframes`), not as own props of `vars`. Handles the object
|
|
203
|
+
* form (`{ "0%": {...} }`) and the array form (`[{...}, ...]`).
|
|
204
|
+
*/
|
|
205
|
+
function keyframeVarsCarryChannel(
|
|
206
|
+
vars: Record<string, unknown> | undefined,
|
|
207
|
+
channels: string[],
|
|
208
|
+
): boolean {
|
|
209
|
+
const kf = vars?.keyframes;
|
|
210
|
+
if (!kf || typeof kf !== "object") return false;
|
|
211
|
+
const steps = Array.isArray(kf) ? kf : Object.values(kf);
|
|
212
|
+
return steps.some(
|
|
213
|
+
(step) =>
|
|
214
|
+
step != null &&
|
|
215
|
+
typeof step === "object" &&
|
|
216
|
+
channels.some((ch) => Object.prototype.hasOwnProperty.call(step, ch)),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
187
220
|
/**
|
|
188
221
|
* Resolve the live tween targeting `selector` using the SAME all-timelines scan
|
|
189
222
|
* `readRuntimeKeyframes` uses, so read and write agree on "which tween". With
|
|
@@ -197,6 +230,7 @@ function varsCarryChannel(vars: Record<string, unknown> | undefined, channels: s
|
|
|
197
230
|
* on a rotation-only set). With no channel-matching set, it falls back to the
|
|
198
231
|
* first matching set (back-compat). `channels` is ignored for `kind: "keyframe"`.
|
|
199
232
|
*/
|
|
233
|
+
// fallow-ignore-next-line complexity
|
|
200
234
|
export function resolveRuntimeTween(
|
|
201
235
|
iframe: HTMLIFrameElement | null,
|
|
202
236
|
selector: string,
|
|
@@ -219,7 +253,12 @@ export function resolveRuntimeTween(
|
|
|
219
253
|
? [compositionId]
|
|
220
254
|
: Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
|
|
221
255
|
|
|
222
|
-
|
|
256
|
+
// Channels disambiguate co-located tweens for BOTH kinds: a `set` carries them as
|
|
257
|
+
// own vars props, a keyframe tween carries them inside its keyframe steps. An
|
|
258
|
+
// element can have a rotation keyframe tween AND a position keyframe tween; a
|
|
259
|
+
// rotation edit must land on the former. The reader passes no channels, so its
|
|
260
|
+
// playhead-containment path below is unchanged.
|
|
261
|
+
const wantChannels = channels && channels.length > 0 ? channels : null;
|
|
223
262
|
|
|
224
263
|
let first: ResolvedRuntimeTween | null = null;
|
|
225
264
|
let channelMatch: ResolvedRuntimeTween | null = null;
|
|
@@ -233,11 +272,15 @@ export function resolveRuntimeTween(
|
|
|
233
272
|
const isSet = !(dur > 0);
|
|
234
273
|
if (kind === "set" ? !isSet : isSet) continue;
|
|
235
274
|
if (wantChannels) {
|
|
236
|
-
|
|
275
|
+
const carries =
|
|
276
|
+
kind === "set"
|
|
277
|
+
? varsCarryChannel(tween.vars, wantChannels)
|
|
278
|
+
: keyframeVarsCarryChannel(tween.vars, wantChannels);
|
|
279
|
+
if (carries) {
|
|
237
280
|
if (channelMatch === null) channelMatch = { tween, timeline };
|
|
238
281
|
} else if (first === null) {
|
|
239
|
-
// A
|
|
240
|
-
// fallback, but never prefer it over a channel-matching
|
|
282
|
+
// A tween carrying only disjoint channels: remember as last-resort
|
|
283
|
+
// fallback, but never prefer it over a channel-matching one.
|
|
241
284
|
first = { tween, timeline };
|
|
242
285
|
}
|
|
243
286
|
continue;
|
|
@@ -252,14 +295,27 @@ export function resolveRuntimeTween(
|
|
|
252
295
|
return channelMatch ?? first;
|
|
253
296
|
}
|
|
254
297
|
|
|
298
|
+
/** Whether a read carries at least one of `channels` as a keyframe property. */
|
|
299
|
+
function readCarriesChannel(read: ReadTween, channels: string[]): boolean {
|
|
300
|
+
return read.keyframes.some((kf) => channels.some((c) => kf.properties[c] != null));
|
|
301
|
+
}
|
|
302
|
+
|
|
255
303
|
/**
|
|
256
304
|
* Read keyframes (incl. motionPath arcs) for one selector from the live timeline.
|
|
257
305
|
* Returns tween-relative percentages; callers convert to clip-relative.
|
|
306
|
+
*
|
|
307
|
+
* `requireChannels` restricts the scan to tweens whose read carries one of those
|
|
308
|
+
* properties — e.g. the motion-path overlay passes `["x","y"]` so it never picks
|
|
309
|
+
* up a co-located size/scale tween (which has no x/y and would blank the path
|
|
310
|
+
* whenever the playhead sits in that tween's range but outside the position
|
|
311
|
+
* tween's). Omitted → any keyframed tween qualifies (back-compat).
|
|
258
312
|
*/
|
|
313
|
+
// fallow-ignore-next-line complexity
|
|
259
314
|
export function readRuntimeKeyframes(
|
|
260
315
|
iframe: HTMLIFrameElement | null,
|
|
261
316
|
selector: string,
|
|
262
317
|
compositionId?: string,
|
|
318
|
+
requireChannels?: string[],
|
|
263
319
|
): ReadTween | null {
|
|
264
320
|
const timelines = timelinesOf(iframe);
|
|
265
321
|
if (!timelines) return null;
|
|
@@ -299,6 +355,7 @@ export function readRuntimeKeyframes(
|
|
|
299
355
|
if (isZeroDurationSet(dur)) continue; // skip hold/set tweens (see isZeroDurationSet)
|
|
300
356
|
const read = readTween(tween.vars);
|
|
301
357
|
if (!read) continue;
|
|
358
|
+
if (requireChannels && !readCarriesChannel(read, requireChannels)) continue;
|
|
302
359
|
if (firstRead === null) firstRead = read;
|
|
303
360
|
// Prefer the tween whose [start, start+dur] contains the playhead.
|
|
304
361
|
if (now != null) {
|
|
@@ -321,6 +378,7 @@ export function readRuntimeKeyframes(
|
|
|
321
378
|
* only a hold may remain, and resurrecting the deleted tween from the stale parse
|
|
322
379
|
* must be avoided.
|
|
323
380
|
*/
|
|
381
|
+
// fallow-ignore-next-line complexity
|
|
324
382
|
export function hasNonHoldTweenForElement(
|
|
325
383
|
iframe: HTMLIFrameElement | null,
|
|
326
384
|
selector: string,
|
|
@@ -13,13 +13,22 @@
|
|
|
13
13
|
* "Which tween" is resolved by the same all-timelines scan `readRuntimeKeyframes`
|
|
14
14
|
* uses (`resolveRuntimeTween`), so read and write agree on the target.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
resolveRuntimeTween,
|
|
18
|
+
type RuntimeTween,
|
|
19
|
+
type RuntimeTimeline,
|
|
20
|
+
} from "./gsapRuntimeKeyframes";
|
|
17
21
|
|
|
18
22
|
/** Value-only channels a `tl.set(...)` patch may touch. */
|
|
19
23
|
export interface SetPatchProps {
|
|
20
24
|
x?: number;
|
|
21
25
|
y?: number;
|
|
22
26
|
rotation?: number;
|
|
27
|
+
rotationX?: number;
|
|
28
|
+
rotationY?: number;
|
|
29
|
+
rotationZ?: number;
|
|
30
|
+
z?: number;
|
|
31
|
+
transformPerspective?: number;
|
|
23
32
|
scaleX?: number;
|
|
24
33
|
scaleY?: number;
|
|
25
34
|
scale?: number;
|
|
@@ -31,12 +40,28 @@ export type KeyframeStep = Record<string, number>;
|
|
|
31
40
|
|
|
32
41
|
export type RuntimeTweenChange =
|
|
33
42
|
| { kind: "set"; props: SetPatchProps }
|
|
34
|
-
| { kind: "keyframes"; keyframes: KeyframeStep[] }
|
|
43
|
+
| { kind: "keyframes"; keyframes: KeyframeStep[] }
|
|
44
|
+
// Edit ONE step (at `pct`) of an object-form keyframe tween — the form the studio
|
|
45
|
+
// writes (`keyframes: { "0%": {...} }`). GSAP pre-compiles those into sub-tweens
|
|
46
|
+
// and won't re-read `vars.keyframes` on `invalidate()`, so this REBUILDS the tween
|
|
47
|
+
// (kill + recreate at the same position) instead of mutating it. Lets a design-panel
|
|
48
|
+
// keyframe edit show instantly rather than soft-reloading the iframe (a flash).
|
|
49
|
+
| { kind: "keyframe-rebuild"; pct: number; props: KeyframeStep }
|
|
50
|
+
// Apply a base `gsap.set` value to the element directly (`gsap.set(el, props)`).
|
|
51
|
+
// A base set lives OFF the timeline, so there's no runtime tween to patch — but
|
|
52
|
+
// the element is static on these channels, so setting them immediately reflects
|
|
53
|
+
// the edit with no soft reload (no flash) and leaves no keyframe marker.
|
|
54
|
+
| { kind: "global-set"; props: SetPatchProps };
|
|
35
55
|
|
|
36
56
|
const SET_CHANNELS: Array<keyof SetPatchProps> = [
|
|
37
57
|
"x",
|
|
38
58
|
"y",
|
|
39
59
|
"rotation",
|
|
60
|
+
"rotationX",
|
|
61
|
+
"rotationY",
|
|
62
|
+
"rotationZ",
|
|
63
|
+
"z",
|
|
64
|
+
"transformPerspective",
|
|
40
65
|
"scaleX",
|
|
41
66
|
"scaleY",
|
|
42
67
|
"scale",
|
|
@@ -45,8 +70,42 @@ const SET_CHANNELS: Array<keyof SetPatchProps> = [
|
|
|
45
70
|
|
|
46
71
|
type IframeWindow = Window & {
|
|
47
72
|
__player?: { getTime?: () => number; seek?: (t: number) => void };
|
|
73
|
+
gsap?: { set?: (target: Element, vars: Record<string, number>) => void };
|
|
48
74
|
};
|
|
49
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Apply a base `gsap.set` value to the element in the live runtime. Returns `true`
|
|
78
|
+
* if applied. Used for off-timeline static holds (position / 3D transform) — there's
|
|
79
|
+
* no tween to patch, so we set the channels directly. Safe because the element is
|
|
80
|
+
* static on these channels (the caller only uses this for non-animated values).
|
|
81
|
+
*/
|
|
82
|
+
/** The props as finite numbers, or null if any value is non-finite / none present. */
|
|
83
|
+
function finiteNumericProps(props: SetPatchProps): Record<string, number> | null {
|
|
84
|
+
const numeric: Record<string, number> = {};
|
|
85
|
+
for (const [k, v] of Object.entries(props)) {
|
|
86
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return null;
|
|
87
|
+
numeric[k] = v;
|
|
88
|
+
}
|
|
89
|
+
return Object.keys(numeric).length > 0 ? numeric : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function applyGlobalSet(
|
|
93
|
+
iframe: HTMLIFrameElement,
|
|
94
|
+
selector: string,
|
|
95
|
+
props: SetPatchProps,
|
|
96
|
+
): boolean {
|
|
97
|
+
try {
|
|
98
|
+
const gsapLib = (iframe.contentWindow as IframeWindow | null)?.gsap;
|
|
99
|
+
const el = iframe.contentDocument?.querySelector(selector) ?? null;
|
|
100
|
+
const numeric = finiteNumericProps(props);
|
|
101
|
+
if (!gsapLib?.set || !el || !numeric) return false;
|
|
102
|
+
gsapLib.set(el, numeric);
|
|
103
|
+
return true;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
50
109
|
function playerOf(iframe: HTMLIFrameElement): IframeWindow["__player"] | null {
|
|
51
110
|
try {
|
|
52
111
|
return (iframe.contentWindow as IframeWindow | null)?.__player ?? null;
|
|
@@ -100,10 +159,95 @@ function patchKeyframes(tween: RuntimeTween, keyframes: KeyframeStep[]): boolean
|
|
|
100
159
|
return true;
|
|
101
160
|
}
|
|
102
161
|
|
|
162
|
+
/** The object-form keyframes map with `props` merged into the step at `pct`. */
|
|
163
|
+
function mergeKeyframeStep(
|
|
164
|
+
map: Record<string, Record<string, number>>,
|
|
165
|
+
pct: number,
|
|
166
|
+
props: KeyframeStep,
|
|
167
|
+
): Record<string, Record<string, number>> {
|
|
168
|
+
const next: Record<string, Record<string, number>> = {};
|
|
169
|
+
for (const [k, step] of Object.entries(map)) next[k] = { ...step };
|
|
170
|
+
// Match the existing percentage key numerically ("50%" ≡ pct 50), else add one.
|
|
171
|
+
let key: string | null = null;
|
|
172
|
+
for (const k of Object.keys(next)) {
|
|
173
|
+
const n = parseFloat(k);
|
|
174
|
+
if (Number.isFinite(n) && Math.abs(n - pct) < 0.05) {
|
|
175
|
+
key = k;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (key === null) key = `${pct}%`;
|
|
180
|
+
next[key] = { ...(next[key] ?? {}), ...props };
|
|
181
|
+
return next;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Rebuild an object-form keyframe tween with `props` merged into the step at `pct`,
|
|
186
|
+
* in place: kill the old tween and recreate it on the SAME parent timeline at the
|
|
187
|
+
* SAME position, with all other vars (duration, ease, repeat, …) preserved. This
|
|
188
|
+
* is the only way to reflect an object-form keyframe edit live — GSAP compiles
|
|
189
|
+
* those keyframes into sub-tweens at creation and ignores later `vars.keyframes`
|
|
190
|
+
* mutations. Declines (→ caller soft-reloads) for array-form, motionPath arcs,
|
|
191
|
+
* non-finite/dynamic values, or a tween whose parent/targets can't be resolved.
|
|
192
|
+
*/
|
|
193
|
+
// fallow-ignore-next-line complexity
|
|
194
|
+
function rebuildKeyframeTween(tween: RuntimeTween, pct: number, props: KeyframeStep): boolean {
|
|
195
|
+
const vars = tween.vars;
|
|
196
|
+
if (!vars || "motionPath" in vars) return false;
|
|
197
|
+
const kf = vars.keyframes;
|
|
198
|
+
if (!kf || typeof kf !== "object" || Array.isArray(kf)) return false;
|
|
199
|
+
for (const v of Object.values(props)) {
|
|
200
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return false;
|
|
201
|
+
}
|
|
202
|
+
const parent = tween.parent;
|
|
203
|
+
const targets = tween.targets?.();
|
|
204
|
+
if (!parent?.to || !targets || targets.length === 0) return false;
|
|
205
|
+
if (typeof tween.startTime !== "function" || typeof tween.kill !== "function") return false;
|
|
206
|
+
|
|
207
|
+
const next = mergeKeyframeStep(kf as Record<string, Record<string, number>>, pct, props);
|
|
208
|
+
const newVars = { ...vars, keyframes: next };
|
|
209
|
+
const position = tween.startTime();
|
|
210
|
+
tween.kill();
|
|
211
|
+
parent.to(targets, newVars, position);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The channels a change writes, for the resolver to disambiguate co-located tweens. */
|
|
216
|
+
function changeChannels(change: RuntimeTweenChange): string[] | undefined {
|
|
217
|
+
if (change.kind === "set") {
|
|
218
|
+
return Object.keys(change.props).filter(
|
|
219
|
+
(k) => change.props[k as keyof SetPatchProps] !== undefined,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (change.kind === "keyframe-rebuild") return Object.keys(change.props);
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Re-render the timeline at the current playhead after an in-place edit. */
|
|
227
|
+
function seekToCurrent(iframe: HTMLIFrameElement, timeline: RuntimeTimeline): void {
|
|
228
|
+
const player = playerOf(iframe);
|
|
229
|
+
const currentTime =
|
|
230
|
+
typeof player?.getTime === "function"
|
|
231
|
+
? player.getTime()
|
|
232
|
+
: typeof timeline.time === "function"
|
|
233
|
+
? timeline.time()
|
|
234
|
+
: 0;
|
|
235
|
+
player?.seek?.(Number.isFinite(currentTime) ? currentTime : 0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Apply `change` to the resolved tween. `true` if applied, `false` to soft-reload.
|
|
239
|
+
* `global-set` is handled before this (no tween) and never reaches here. */
|
|
240
|
+
function applyChange(tween: RuntimeTween, change: RuntimeTweenChange): boolean {
|
|
241
|
+
if (change.kind === "set") return patchSet(tween, change.props);
|
|
242
|
+
if (change.kind === "keyframes") return patchKeyframes(tween, change.keyframes);
|
|
243
|
+
if (change.kind === "keyframe-rebuild")
|
|
244
|
+
return rebuildKeyframeTween(tween, change.pct, change.props);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
103
248
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* (caller falls back to a soft reload).
|
|
249
|
+
* Edit one tween in `window.__timelines` in place + re-seek to the current playhead.
|
|
250
|
+
* Returns `true` on a confident patch, `false` otherwise (caller soft-reloads).
|
|
107
251
|
*/
|
|
108
252
|
export function patchRuntimeTweenInPlace(
|
|
109
253
|
iframe: HTMLIFrameElement | null,
|
|
@@ -112,46 +256,29 @@ export function patchRuntimeTweenInPlace(
|
|
|
112
256
|
compositionId?: string,
|
|
113
257
|
): boolean {
|
|
114
258
|
if (!iframe) return false;
|
|
259
|
+
// A base `gsap.set` has no timeline tween to resolve — apply the value straight
|
|
260
|
+
// to the element so the edit shows instantly (no soft reload, no flash).
|
|
261
|
+
if (change.kind === "global-set") return applyGlobalSet(iframe, selector, change.props);
|
|
115
262
|
try {
|
|
116
|
-
// For a `set` patch, hand the resolver the channels actually being written so
|
|
117
|
-
// it picks the set whose vars carry them — an element can have separate
|
|
118
|
-
// {x,y} and {rotation} sets, and a position patch must not corrupt the
|
|
119
|
-
// rotation set (channel-blind resolution would return the first match).
|
|
120
|
-
const channels =
|
|
121
|
-
change.kind === "set"
|
|
122
|
-
? Object.keys(change.props).filter(
|
|
123
|
-
(k) => change.props[k as keyof SetPatchProps] !== undefined,
|
|
124
|
-
)
|
|
125
|
-
: undefined;
|
|
126
263
|
const resolved = resolveRuntimeTween(
|
|
127
264
|
iframe,
|
|
128
265
|
selector,
|
|
129
266
|
change.kind === "set" ? "set" : "keyframe",
|
|
130
267
|
compositionId,
|
|
131
|
-
|
|
268
|
+
changeChannels(change),
|
|
132
269
|
);
|
|
133
270
|
if (!resolved) return false;
|
|
134
271
|
const { tween, timeline } = resolved;
|
|
135
272
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
timeline.invalidate?.();
|
|
146
|
-
|
|
147
|
-
const player = playerOf(iframe);
|
|
148
|
-
const currentTime =
|
|
149
|
-
typeof player?.getTime === "function"
|
|
150
|
-
? player.getTime()
|
|
151
|
-
: typeof timeline.time === "function"
|
|
152
|
-
? timeline.time()
|
|
153
|
-
: 0;
|
|
154
|
-
player?.seek?.(Number.isFinite(currentTime) ? currentTime : 0);
|
|
273
|
+
if (!applyChange(tween, change)) return false;
|
|
274
|
+
|
|
275
|
+
// A rebuild already recreated the tween; set/keyframes mutate vars in place, so
|
|
276
|
+
// invalidate to make GSAP re-read them on the next render. Either way, re-seek.
|
|
277
|
+
if (change.kind !== "keyframe-rebuild") {
|
|
278
|
+
tween.invalidate?.();
|
|
279
|
+
timeline.invalidate?.();
|
|
280
|
+
}
|
|
281
|
+
seekToCurrent(iframe, timeline);
|
|
155
282
|
return true;
|
|
156
283
|
} catch {
|
|
157
284
|
return false;
|