@hyperframes/studio 0.6.53 → 0.6.55
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-D6EwK2hA.js +138 -0
- package/dist/assets/index-ZdgB8MFr.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/components/StudioRightPanel.tsx +18 -0
- package/src/components/editor/AnimationCard.tsx +325 -0
- package/src/components/editor/EaseCurveSection.tsx +213 -0
- package/src/components/editor/GsapAnimationSection.tsx +112 -0
- package/src/components/editor/PropertyPanel.tsx +48 -18
- package/src/components/editor/domEditingTypes.ts +2 -0
- package/src/components/editor/gsapAnimationConstants.ts +130 -0
- package/src/components/editor/manualEditingAvailability.ts +6 -0
- package/src/components/editor/manualEdits.test.ts +101 -0
- package/src/components/editor/manualEdits.ts +22 -9
- package/src/components/editor/manualEditsDom.ts +22 -21
- package/src/components/editor/manualOffsetDrag.test.ts +35 -22
- package/src/components/editor/manualOffsetDrag.ts +1 -7
- package/src/components/editor/propertyPanelPrimitives.tsx +6 -1
- package/src/contexts/DomEditContext.tsx +27 -0
- package/src/hooks/useDomEditSession.ts +100 -2
- package/src/hooks/useDomSelection.ts +8 -0
- package/src/hooks/useGsapScriptCommits.ts +303 -0
- package/src/hooks/useGsapTweenCache.test.ts +49 -0
- package/src/hooks/useGsapTweenCache.ts +108 -0
- package/src/hooks/usePreviewPersistence.ts +1 -0
- package/dist/assets/index-B2mn12z0.css +0 -1
- package/dist/assets/index-CZNoIjSE.js +0 -138
|
@@ -516,3 +516,104 @@ describe("studio manual edits", () => {
|
|
|
516
516
|
expect(frames).toHaveLength(0);
|
|
517
517
|
});
|
|
518
518
|
});
|
|
519
|
+
|
|
520
|
+
describe("applyStudioPathOffset sets correct attribute name", () => {
|
|
521
|
+
it("sets data-hf-studio-path-offset without double data- prefix", () => {
|
|
522
|
+
const window = new Window();
|
|
523
|
+
const el = window.document.createElement("div");
|
|
524
|
+
window.document.body.append(el);
|
|
525
|
+
|
|
526
|
+
applyStudioPathOffset(el, { x: 100, y: 50 });
|
|
527
|
+
|
|
528
|
+
expect(el.getAttribute("data-hf-studio-path-offset")).toBe("true");
|
|
529
|
+
expect(el.getAttribute("data-data-hf-studio-path-offset")).toBeNull();
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("stores offset in CSS vars alongside the attribute marker", () => {
|
|
533
|
+
const window = new Window();
|
|
534
|
+
const el = window.document.createElement("div");
|
|
535
|
+
window.document.body.append(el);
|
|
536
|
+
|
|
537
|
+
applyStudioPathOffset(el, { x: 50, y: 25 });
|
|
538
|
+
|
|
539
|
+
expect(el.getAttribute("data-hf-studio-path-offset")).toBe("true");
|
|
540
|
+
expect(el.style.getPropertyValue(STUDIO_OFFSET_X_PROP)).toBe("50px");
|
|
541
|
+
expect(el.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)).toBe("25px");
|
|
542
|
+
expect(el.style.getPropertyValue("translate")).toContain(STUDIO_OFFSET_X_PROP);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("corrects offset applied on top of legacy double-prefix element", () => {
|
|
546
|
+
const window = new Window();
|
|
547
|
+
const el = window.document.createElement("div");
|
|
548
|
+
el.setAttribute("data-data-hf-studio-path-offset", "true");
|
|
549
|
+
el.style.setProperty(STUDIO_OFFSET_X_PROP, "200px");
|
|
550
|
+
el.style.setProperty(STUDIO_OFFSET_Y_PROP, "-30px");
|
|
551
|
+
window.document.body.append(el);
|
|
552
|
+
|
|
553
|
+
applyStudioPathOffset(el, { x: 200, y: -30 });
|
|
554
|
+
|
|
555
|
+
expect(el.getAttribute("data-hf-studio-path-offset")).toBe("true");
|
|
556
|
+
expect(readStudioPathOffset(el)).toEqual({ x: 200, y: -30 });
|
|
557
|
+
expect(el.style.getPropertyValue("translate")).toContain(STUDIO_OFFSET_X_PROP);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
describe("applyStudioPathOffset strips GSAP double-counted translate", () => {
|
|
562
|
+
it("strips GSAP transform translate when applying offset", () => {
|
|
563
|
+
const window = new Window();
|
|
564
|
+
const element = window.document.createElement("div");
|
|
565
|
+
window.document.body.append(element);
|
|
566
|
+
|
|
567
|
+
// Simulate GSAP having baked translate into the transform matrix
|
|
568
|
+
element.style.setProperty("transform", "matrix(1, 0, 0, 1, 200, 0)");
|
|
569
|
+
|
|
570
|
+
applyStudioPathOffset(element, { x: 200, y: 0 });
|
|
571
|
+
|
|
572
|
+
// The transform translate should be stripped (GSAP's 200px removed)
|
|
573
|
+
const transform = element.style.getPropertyValue("transform");
|
|
574
|
+
if (transform && transform !== "none") {
|
|
575
|
+
const m = new window.DOMMatrix(transform);
|
|
576
|
+
expect(m.m41).toBe(0);
|
|
577
|
+
expect(m.m42).toBe(0);
|
|
578
|
+
}
|
|
579
|
+
// The offset should be stored in CSS vars
|
|
580
|
+
expect(readStudioPathOffset(element).x).toBe(200);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("subtracts only the studio offset from GSAP transform, preserving animation values", () => {
|
|
584
|
+
const window = new Window();
|
|
585
|
+
const element = window.document.createElement("div");
|
|
586
|
+
window.document.body.append(element);
|
|
587
|
+
|
|
588
|
+
// GSAP has scale + baked translate (offset 50) + animation contribution (-70)
|
|
589
|
+
// Total m42 = 50 + (-70) = -20
|
|
590
|
+
element.style.setProperty("transform", "matrix(0.5, 0, 0, 0.5, 0, -20)");
|
|
591
|
+
|
|
592
|
+
applyStudioPathOffset(element, { x: 0, y: 50 });
|
|
593
|
+
|
|
594
|
+
const transform = element.style.getPropertyValue("transform");
|
|
595
|
+
if (transform && transform !== "none") {
|
|
596
|
+
const m = new window.DOMMatrix(transform);
|
|
597
|
+
expect(m.a).toBeCloseTo(0.5);
|
|
598
|
+
expect(m.d).toBeCloseTo(0.5);
|
|
599
|
+
// Only the studio offset (50) is subtracted, animation contribution (-70) preserved
|
|
600
|
+
expect(m.m41).toBe(0);
|
|
601
|
+
expect(m.m42).toBe(-70);
|
|
602
|
+
}
|
|
603
|
+
expect(readStudioPathOffset(element).y).toBe(50);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("offset survives repeated applyStudioPathOffset calls without drift", () => {
|
|
607
|
+
const window = new Window();
|
|
608
|
+
const element = window.document.createElement("div");
|
|
609
|
+
window.document.body.append(element);
|
|
610
|
+
|
|
611
|
+
// Apply offset 3 times with same value (simulates reapply hook firing multiple times)
|
|
612
|
+
applyStudioPathOffset(element, { x: 100, y: -20 });
|
|
613
|
+
applyStudioPathOffset(element, { x: 100, y: -20 });
|
|
614
|
+
applyStudioPathOffset(element, { x: 100, y: -20 });
|
|
615
|
+
|
|
616
|
+
expect(readStudioPathOffset(element).x).toBe(100);
|
|
617
|
+
expect(readStudioPathOffset(element).y).toBe(-20);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
@@ -3,9 +3,7 @@ export {
|
|
|
3
3
|
STUDIO_OFFSET_X_PROP,
|
|
4
4
|
STUDIO_OFFSET_Y_PROP,
|
|
5
5
|
STUDIO_WIDTH_PROP,
|
|
6
|
-
STUDIO_HEIGHT_PROP,
|
|
7
6
|
STUDIO_ROTATION_PROP,
|
|
8
|
-
type StudioManualEditSeekWindow,
|
|
9
7
|
type StudioBoxSizeSnapshot,
|
|
10
8
|
type StudioRotationSnapshot,
|
|
11
9
|
type StudioPathOffsetSnapshot,
|
|
@@ -20,7 +18,6 @@ export {
|
|
|
20
18
|
readStudioPathOffset,
|
|
21
19
|
readStudioBoxSize,
|
|
22
20
|
readStudioRotation,
|
|
23
|
-
readGsapTranslateFromTransform,
|
|
24
21
|
applyStudioPathOffset,
|
|
25
22
|
applyStudioPathOffsetDraft,
|
|
26
23
|
applyStudioBoxSize,
|
|
@@ -28,8 +25,6 @@ export {
|
|
|
28
25
|
applyStudioRotation,
|
|
29
26
|
applyStudioRotationDraft,
|
|
30
27
|
reapplyPositionEditsAfterSeek,
|
|
31
|
-
buildMotionPatches,
|
|
32
|
-
buildClearMotionPatches,
|
|
33
28
|
} from "./manualEditsDom";
|
|
34
29
|
|
|
35
30
|
export {
|
|
@@ -51,7 +46,6 @@ import {
|
|
|
51
46
|
STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP,
|
|
52
47
|
} from "./manualEditsTypes";
|
|
53
48
|
import { finiteNumber } from "./manualEditsParsing";
|
|
54
|
-
import { isStudioManualEditGestureActive } from "./manualEditsDom";
|
|
55
49
|
|
|
56
50
|
/* ── Seek/play reapply wrappers ───────────────────────────────────── */
|
|
57
51
|
function markWrapped(fn: (...args: unknown[]) => unknown): void {
|
|
@@ -262,6 +256,28 @@ export function installStudioManualEditSeekReapply(win: Window, apply: () => voi
|
|
|
262
256
|
wrapApplyAfterFunction(studioWin, timeline, "pause") || wrappedNamedTimelinePause;
|
|
263
257
|
}
|
|
264
258
|
|
|
259
|
+
// Auto-wrap timelines registered AFTER this install runs. GSAP compositions
|
|
260
|
+
// register via `window.__timelines[id] = tl` which may happen after the
|
|
261
|
+
// Studio hook runs. The Proxy intercepts new registrations and wraps
|
|
262
|
+
// seek/play/pause immediately, closing the gap that causes translate doubling.
|
|
263
|
+
if (studioWin.__timelines && !(studioWin.__timelines as Record<string, unknown>).__proxied) {
|
|
264
|
+
const original = studioWin.__timelines;
|
|
265
|
+
studioWin.__timelines = new Proxy(original, {
|
|
266
|
+
set(target, prop, value) {
|
|
267
|
+
target[prop as string] = value;
|
|
268
|
+
if (typeof value === "object" && value !== null) {
|
|
269
|
+
const tl = value as Record<string, unknown>;
|
|
270
|
+
wrapSeekReapplyFunction(studioWin, tl, "seek");
|
|
271
|
+
wrapPlayReapplyFunction(studioWin, tl, "play");
|
|
272
|
+
wrapApplyAfterFunction(studioWin, tl, "pause");
|
|
273
|
+
studioWin.__hfStudioManualEditsApply?.();
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
(studioWin.__timelines as Record<string, unknown>).__proxied = true;
|
|
279
|
+
}
|
|
280
|
+
|
|
265
281
|
if (isStudioManualEditPlaybackActive(studioWin)) {
|
|
266
282
|
startStudioManualEditPlaybackReapply(studioWin);
|
|
267
283
|
}
|
|
@@ -280,6 +296,3 @@ export function installStudioManualEditSeekReapply(win: Window, apply: () => voi
|
|
|
280
296
|
wrappedNamedTimelinePause
|
|
281
297
|
);
|
|
282
298
|
}
|
|
283
|
-
|
|
284
|
-
// Re-export for internal use (seek hooks need this)
|
|
285
|
-
export { isStudioManualEditGestureActive };
|
|
@@ -48,7 +48,7 @@ export function endStudioManualEditGesture(element: HTMLElement, token?: string)
|
|
|
48
48
|
element.removeAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
function isStudioManualEditGestureActive(element: HTMLElement): boolean {
|
|
52
52
|
return element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -213,26 +213,15 @@ function writeStudioPathOffsetVars(
|
|
|
213
213
|
|
|
214
214
|
// GSAP 3.x reads the resolved CSS `translate` individual property at initialization and bakes it
|
|
215
215
|
// into element.style.transform (as a matrix) on every seek. When the studio's reapply hook also
|
|
216
|
-
// writes `translate`, both properties compose additively, doubling the visual offset.
|
|
217
|
-
//
|
|
216
|
+
// writes `translate`, both properties compose additively, doubling the visual offset.
|
|
217
|
+
//
|
|
218
|
+
// This helper subtracts only the baked studio offset from m41/m42, preserving any GSAP animation
|
|
219
|
+
// contribution (e.g. a tween animating y: -20). The studio offset is read from the CSS custom
|
|
220
|
+
// properties which tell us exactly how much was baked from the CSS translate.
|
|
218
221
|
function isIdentityAfterTranslateStrip(m: DOMMatrix): boolean {
|
|
219
222
|
return m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1;
|
|
220
223
|
}
|
|
221
224
|
|
|
222
|
-
export function readGsapTranslateFromTransform(element: HTMLElement): { x: number; y: number } {
|
|
223
|
-
const transform = element.style.getPropertyValue("transform");
|
|
224
|
-
if (!transform || transform === "none") return { x: 0, y: 0 };
|
|
225
|
-
const DOMMatrixCtor = (element.ownerDocument.defaultView as (Window & typeof globalThis) | null)
|
|
226
|
-
?.DOMMatrix;
|
|
227
|
-
if (!DOMMatrixCtor) return { x: 0, y: 0 };
|
|
228
|
-
try {
|
|
229
|
-
const m = new DOMMatrixCtor(transform);
|
|
230
|
-
return { x: m.m41, y: m.m42 };
|
|
231
|
-
} catch {
|
|
232
|
-
return { x: 0, y: 0 };
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
225
|
function stripGsapTranslateFromTransform(element: HTMLElement): void {
|
|
237
226
|
const transform = element.style.getPropertyValue("transform");
|
|
238
227
|
if (!transform || transform === "none") return;
|
|
@@ -242,9 +231,11 @@ function stripGsapTranslateFromTransform(element: HTMLElement): void {
|
|
|
242
231
|
try {
|
|
243
232
|
const m = new DOMMatrixCtor(transform);
|
|
244
233
|
if (m.m41 === 0 && m.m42 === 0) return;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
234
|
+
const offsetX = readPxCustomProperty(element, STUDIO_OFFSET_X_PROP);
|
|
235
|
+
const offsetY = readPxCustomProperty(element, STUDIO_OFFSET_Y_PROP);
|
|
236
|
+
m.m41 -= offsetX;
|
|
237
|
+
m.m42 -= offsetY;
|
|
238
|
+
if (Math.abs(m.m41) < 0.01 && Math.abs(m.m42) < 0.01 && isIdentityAfterTranslateStrip(m)) {
|
|
248
239
|
element.style.removeProperty("transform");
|
|
249
240
|
} else {
|
|
250
241
|
element.style.setProperty("transform", m.toString());
|
|
@@ -493,9 +484,19 @@ export {
|
|
|
493
484
|
function queryStudioElements(doc: Document, attr: string): HTMLElement[] {
|
|
494
485
|
const ctor = doc.defaultView?.HTMLElement;
|
|
495
486
|
if (!ctor) return [];
|
|
496
|
-
|
|
487
|
+
const elements = Array.from(doc.querySelectorAll(`[${attr}="true"]`)).filter(
|
|
497
488
|
(el): el is HTMLElement => el instanceof ctor,
|
|
498
489
|
);
|
|
490
|
+
// Handle legacy HTML files where attributes were persisted with a double data- prefix
|
|
491
|
+
const legacyAttr = `data-${attr}`;
|
|
492
|
+
for (const el of doc.querySelectorAll(`[${legacyAttr}="true"]`)) {
|
|
493
|
+
if (el instanceof ctor && !el.hasAttribute(attr)) {
|
|
494
|
+
el.setAttribute(attr, "true");
|
|
495
|
+
el.removeAttribute(legacyAttr);
|
|
496
|
+
elements.push(el);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return elements;
|
|
499
500
|
}
|
|
500
501
|
|
|
501
502
|
function reapplyPathOffsets(doc: Document): void {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Window } from "happy-dom";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import {
|
|
4
|
+
applyManualOffsetDragCommit,
|
|
4
5
|
applyManualOffsetDragMatrix,
|
|
5
6
|
createManualOffsetDragMember,
|
|
7
|
+
endManualOffsetDragMembers,
|
|
6
8
|
invertManualOffsetDragMatrix,
|
|
7
9
|
measureManualOffsetDragScreenToOffsetMatrix,
|
|
8
10
|
resolveManualOffsetForPointerDelta,
|
|
@@ -140,8 +142,8 @@ describe("measureManualOffsetDragScreenToOffsetMatrix", () => {
|
|
|
140
142
|
});
|
|
141
143
|
});
|
|
142
144
|
|
|
143
|
-
describe("createManualOffsetDragMember
|
|
144
|
-
it("
|
|
145
|
+
describe("createManualOffsetDragMember uses raw CSS var offset", () => {
|
|
146
|
+
it("ignores GSAP transform — initialOffset comes from CSS vars only", () => {
|
|
145
147
|
const window = new Window();
|
|
146
148
|
const element = window.document.createElement("div");
|
|
147
149
|
window.document.body.append(element);
|
|
@@ -164,14 +166,18 @@ describe("createManualOffsetDragMember GSAP translate compensation", () => {
|
|
|
164
166
|
expect(result.ok).toBe(true);
|
|
165
167
|
if (!result.ok) return;
|
|
166
168
|
expect(result.member.initialOffset.x).toBe(0);
|
|
167
|
-
expect(result.member.initialOffset.y).toBe(
|
|
169
|
+
expect(result.member.initialOffset.y).toBe(0);
|
|
168
170
|
});
|
|
169
171
|
|
|
170
|
-
it("
|
|
172
|
+
it("reads only the CSS var offset, not GSAP transform", () => {
|
|
171
173
|
const window = new Window();
|
|
172
174
|
const element = window.document.createElement("div");
|
|
173
175
|
window.document.body.append(element);
|
|
174
176
|
|
|
177
|
+
element.style.setProperty(STUDIO_OFFSET_X_PROP, "30px");
|
|
178
|
+
element.style.setProperty(STUDIO_OFFSET_Y_PROP, "10px");
|
|
179
|
+
element.style.setProperty("transform", "translate(50px, -15px)");
|
|
180
|
+
|
|
175
181
|
element.getBoundingClientRect = () => {
|
|
176
182
|
const offsetX = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)) || 0;
|
|
177
183
|
const offsetY = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)) || 0;
|
|
@@ -187,35 +193,42 @@ describe("createManualOffsetDragMember GSAP translate compensation", () => {
|
|
|
187
193
|
|
|
188
194
|
expect(result.ok).toBe(true);
|
|
189
195
|
if (!result.ok) return;
|
|
190
|
-
expect(result.member.initialOffset.x).toBe(
|
|
191
|
-
expect(result.member.initialOffset.y).toBe(
|
|
196
|
+
expect(result.member.initialOffset.x).toBe(30);
|
|
197
|
+
expect(result.member.initialOffset.y).toBe(10);
|
|
192
198
|
});
|
|
193
199
|
|
|
194
|
-
it("
|
|
200
|
+
it("does not accumulate drift across multiple drag cycles", () => {
|
|
195
201
|
const window = new Window();
|
|
196
202
|
const element = window.document.createElement("div");
|
|
197
203
|
window.document.body.append(element);
|
|
198
204
|
|
|
199
|
-
element.style.setProperty(STUDIO_OFFSET_X_PROP, "30px");
|
|
200
|
-
element.style.setProperty(STUDIO_OFFSET_Y_PROP, "10px");
|
|
201
|
-
element.style.setProperty("transform", "translate(50px, -15px)");
|
|
202
|
-
|
|
203
205
|
element.getBoundingClientRect = () => {
|
|
204
206
|
const offsetX = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)) || 0;
|
|
205
207
|
const offsetY = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)) || 0;
|
|
206
208
|
return new window.DOMRect(10 + offsetX, 20 + offsetY, 100, 50);
|
|
207
209
|
};
|
|
208
210
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
211
|
+
// Simulate GSAP baking a translate into transform each cycle
|
|
212
|
+
for (let cycle = 0; cycle < 3; cycle++) {
|
|
213
|
+
element.style.setProperty("transform", `translate(${50 * (cycle + 1)}px, 0px)`);
|
|
214
|
+
|
|
215
|
+
const result = createManualOffsetDragMember({
|
|
216
|
+
key: "test",
|
|
217
|
+
selection: { element } as never,
|
|
218
|
+
element,
|
|
219
|
+
rect: { left: 10, top: 20, width: 100, height: 50, editScaleX: 1, editScaleY: 1 },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(result.ok).toBe(true);
|
|
223
|
+
if (!result.ok) return;
|
|
224
|
+
// initialOffset should always be the CSS var value, never inflated by GSAP transform
|
|
225
|
+
const currentRawX =
|
|
226
|
+
Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)) || 0;
|
|
227
|
+
expect(result.member.initialOffset.x).toBe(currentRawX);
|
|
228
|
+
|
|
229
|
+
// Simulate drag commit: apply a small offset
|
|
230
|
+
applyManualOffsetDragCommit(result.member, 10, 0);
|
|
231
|
+
endManualOffsetDragMembers([result.member]);
|
|
232
|
+
}
|
|
220
233
|
});
|
|
221
234
|
});
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
beginStudioManualEditGesture,
|
|
6
6
|
captureStudioPathOffset,
|
|
7
7
|
endStudioManualEditGesture,
|
|
8
|
-
readGsapTranslateFromTransform,
|
|
9
8
|
readStudioPathOffset,
|
|
10
9
|
restoreStudioPathOffset,
|
|
11
10
|
type StudioPathOffsetSnapshot,
|
|
@@ -232,12 +231,7 @@ export function createManualOffsetDragMember(input: {
|
|
|
232
231
|
element: HTMLElement;
|
|
233
232
|
rect: ManualOffsetDragRect;
|
|
234
233
|
}): ManualOffsetDragMemberResult {
|
|
235
|
-
const
|
|
236
|
-
const gsapTranslate = readGsapTranslateFromTransform(input.element);
|
|
237
|
-
const initialOffset = {
|
|
238
|
-
x: rawOffset.x + gsapTranslate.x,
|
|
239
|
-
y: rawOffset.y + gsapTranslate.y,
|
|
240
|
-
};
|
|
234
|
+
const initialOffset = readStudioPathOffset(input.element);
|
|
241
235
|
const initialPathOffset = captureStudioPathOffset(input.element);
|
|
242
236
|
const gestureToken = beginStudioManualEditGesture(input.element);
|
|
243
237
|
const measured = measureManualOffsetDragScreenToOffsetMatrix(input.element, initialOffset);
|
|
@@ -103,6 +103,8 @@ export function MetricField({
|
|
|
103
103
|
disabled,
|
|
104
104
|
liveCommit,
|
|
105
105
|
scrub,
|
|
106
|
+
suffix,
|
|
107
|
+
tooltip,
|
|
106
108
|
onCommit,
|
|
107
109
|
}: {
|
|
108
110
|
label: string;
|
|
@@ -110,6 +112,8 @@ export function MetricField({
|
|
|
110
112
|
disabled?: boolean;
|
|
111
113
|
liveCommit?: boolean;
|
|
112
114
|
scrub?: boolean;
|
|
115
|
+
suffix?: string;
|
|
116
|
+
tooltip?: string;
|
|
113
117
|
onCommit: (nextValue: string) => void;
|
|
114
118
|
}) {
|
|
115
119
|
const scrubRef = useRef<{ startX: number; startValue: number; pointerId: number } | null>(null);
|
|
@@ -151,7 +155,7 @@ export function MetricField({
|
|
|
151
155
|
: ({ className: "flex-shrink-0 text-[11px] font-medium text-neutral-500" } as const);
|
|
152
156
|
|
|
153
157
|
return (
|
|
154
|
-
<div className={FIELD}>
|
|
158
|
+
<div className={FIELD} title={tooltip}>
|
|
155
159
|
<div className="flex min-w-0 items-center gap-3">
|
|
156
160
|
<span {...scrubProps}>{label}</span>
|
|
157
161
|
<CommitField
|
|
@@ -160,6 +164,7 @@ export function MetricField({
|
|
|
160
164
|
liveCommit={liveCommit}
|
|
161
165
|
onCommit={onCommit}
|
|
162
166
|
/>
|
|
167
|
+
{suffix && <span className="flex-shrink-0 text-[10px] text-neutral-600">{suffix}</span>}
|
|
163
168
|
</div>
|
|
164
169
|
</div>
|
|
165
170
|
);
|
|
@@ -53,6 +53,15 @@ export function DomEditProvider({
|
|
|
53
53
|
setAgentModalOpen,
|
|
54
54
|
setAgentPromptSelectionContext,
|
|
55
55
|
setAgentModalAnchorPoint,
|
|
56
|
+
selectedGsapAnimations,
|
|
57
|
+
gsapMultipleTimelines,
|
|
58
|
+
gsapUnsupportedTimelinePattern,
|
|
59
|
+
handleGsapUpdateProperty,
|
|
60
|
+
handleGsapUpdateMeta,
|
|
61
|
+
handleGsapDeleteAnimation,
|
|
62
|
+
handleGsapAddAnimation,
|
|
63
|
+
handleGsapAddProperty,
|
|
64
|
+
handleGsapRemoveProperty,
|
|
56
65
|
},
|
|
57
66
|
children,
|
|
58
67
|
}: {
|
|
@@ -101,6 +110,15 @@ export function DomEditProvider({
|
|
|
101
110
|
setAgentModalOpen,
|
|
102
111
|
setAgentPromptSelectionContext,
|
|
103
112
|
setAgentModalAnchorPoint,
|
|
113
|
+
selectedGsapAnimations,
|
|
114
|
+
gsapMultipleTimelines,
|
|
115
|
+
gsapUnsupportedTimelinePattern,
|
|
116
|
+
handleGsapUpdateProperty,
|
|
117
|
+
handleGsapUpdateMeta,
|
|
118
|
+
handleGsapDeleteAnimation,
|
|
119
|
+
handleGsapAddAnimation,
|
|
120
|
+
handleGsapAddProperty,
|
|
121
|
+
handleGsapRemoveProperty,
|
|
104
122
|
}),
|
|
105
123
|
[
|
|
106
124
|
domEditSelection,
|
|
@@ -143,6 +161,15 @@ export function DomEditProvider({
|
|
|
143
161
|
setAgentModalOpen,
|
|
144
162
|
setAgentPromptSelectionContext,
|
|
145
163
|
setAgentModalAnchorPoint,
|
|
164
|
+
selectedGsapAnimations,
|
|
165
|
+
gsapMultipleTimelines,
|
|
166
|
+
gsapUnsupportedTimelinePattern,
|
|
167
|
+
handleGsapUpdateProperty,
|
|
168
|
+
handleGsapUpdateMeta,
|
|
169
|
+
handleGsapDeleteAnimation,
|
|
170
|
+
handleGsapAddAnimation,
|
|
171
|
+
handleGsapAddProperty,
|
|
172
|
+
handleGsapRemoveProperty,
|
|
146
173
|
],
|
|
147
174
|
);
|
|
148
175
|
return <DomEditContext value={stable}>{children}</DomEditContext>;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import type { TimelineElement } from "../player";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
5
|
+
STUDIO_GSAP_PANEL_ENABLED,
|
|
6
|
+
} from "../components/editor/manualEditingAvailability";
|
|
4
7
|
import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
|
|
8
|
+
import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
|
|
5
9
|
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
|
6
10
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
7
11
|
import type { RightPanelTab } from "../utils/studioHelpers";
|
|
@@ -11,6 +15,8 @@ import { useAskAgentModal } from "./useAskAgentModal";
|
|
|
11
15
|
import { useDomSelection } from "./useDomSelection";
|
|
12
16
|
import { usePreviewInteraction } from "./usePreviewInteraction";
|
|
13
17
|
import { useDomEditCommits } from "./useDomEditCommits";
|
|
18
|
+
import { useGsapScriptCommits } from "./useGsapScriptCommits";
|
|
19
|
+
import { useGsapAnimationsForElement, useGsapCacheVersion } from "./useGsapTweenCache";
|
|
14
20
|
|
|
15
21
|
// ── Types ──
|
|
16
22
|
|
|
@@ -185,6 +191,39 @@ export function useDomEditSession({
|
|
|
185
191
|
onClickToSource,
|
|
186
192
|
});
|
|
187
193
|
|
|
194
|
+
// ── GSAP script editing ──
|
|
195
|
+
|
|
196
|
+
const { version: gsapCacheVersion, bump: bumpGsapCache } = useGsapCacheVersion();
|
|
197
|
+
|
|
198
|
+
const {
|
|
199
|
+
animations: selectedGsapAnimations,
|
|
200
|
+
multipleTimelines: gsapMultipleTimelines,
|
|
201
|
+
unsupportedTimelinePattern: gsapUnsupportedTimelinePattern,
|
|
202
|
+
} = useGsapAnimationsForElement(
|
|
203
|
+
STUDIO_GSAP_PANEL_ENABLED ? (projectId ?? null) : null,
|
|
204
|
+
domEditSelection?.sourceFile || activeCompPath || "index.html",
|
|
205
|
+
domEditSelection
|
|
206
|
+
? { id: domEditSelection.id ?? null, selector: domEditSelection.selector ?? null }
|
|
207
|
+
: null,
|
|
208
|
+
gsapCacheVersion,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const {
|
|
212
|
+
updateGsapProperty,
|
|
213
|
+
updateGsapMeta,
|
|
214
|
+
deleteGsapAnimation,
|
|
215
|
+
addGsapAnimation,
|
|
216
|
+
addGsapProperty,
|
|
217
|
+
removeGsapProperty,
|
|
218
|
+
} = useGsapScriptCommits({
|
|
219
|
+
projectIdRef,
|
|
220
|
+
activeCompPath,
|
|
221
|
+
editHistory,
|
|
222
|
+
domEditSaveTimestampRef,
|
|
223
|
+
reloadPreview,
|
|
224
|
+
onCacheInvalidate: bumpGsapCache,
|
|
225
|
+
});
|
|
226
|
+
|
|
188
227
|
// ── Commit handlers (delegated to useDomEditCommits) ──
|
|
189
228
|
|
|
190
229
|
const {
|
|
@@ -224,7 +263,53 @@ export function useDomEditSession({
|
|
|
224
263
|
buildDomSelectionFromTarget,
|
|
225
264
|
});
|
|
226
265
|
|
|
227
|
-
|
|
266
|
+
const handleGsapUpdateProperty = useCallback(
|
|
267
|
+
(animId: string, prop: string, value: number | string) => {
|
|
268
|
+
if (!domEditSelection) return;
|
|
269
|
+
updateGsapProperty(domEditSelection, animId, prop, value);
|
|
270
|
+
},
|
|
271
|
+
[domEditSelection, updateGsapProperty],
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const handleGsapUpdateMeta = useCallback(
|
|
275
|
+
(animId: string, updates: { duration?: number; ease?: string; position?: number }) => {
|
|
276
|
+
if (!domEditSelection) return;
|
|
277
|
+
updateGsapMeta(domEditSelection, animId, updates);
|
|
278
|
+
},
|
|
279
|
+
[domEditSelection, updateGsapMeta],
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const handleGsapDeleteAnimation = useCallback(
|
|
283
|
+
(animId: string) => {
|
|
284
|
+
if (!domEditSelection) return;
|
|
285
|
+
deleteGsapAnimation(domEditSelection, animId);
|
|
286
|
+
},
|
|
287
|
+
[domEditSelection, deleteGsapAnimation],
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const handleGsapAddAnimation = useCallback(
|
|
291
|
+
(method: "to" | "from" | "set") => {
|
|
292
|
+
if (!domEditSelection) return;
|
|
293
|
+
addGsapAnimation(domEditSelection, method, currentTime);
|
|
294
|
+
},
|
|
295
|
+
[domEditSelection, addGsapAnimation, currentTime],
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const handleGsapAddProperty = useCallback(
|
|
299
|
+
(animId: string, prop: string) => {
|
|
300
|
+
if (!domEditSelection) return;
|
|
301
|
+
addGsapProperty(domEditSelection, animId, prop);
|
|
302
|
+
},
|
|
303
|
+
[domEditSelection, addGsapProperty],
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const handleGsapRemoveProperty = useCallback(
|
|
307
|
+
(animId: string, prop: string) => {
|
|
308
|
+
if (!domEditSelection) return;
|
|
309
|
+
removeGsapProperty(domEditSelection, animId, prop);
|
|
310
|
+
},
|
|
311
|
+
[domEditSelection, removeGsapProperty],
|
|
312
|
+
);
|
|
228
313
|
|
|
229
314
|
// Sync selection from preview document on load / refresh
|
|
230
315
|
// eslint-disable-next-line no-restricted-syntax
|
|
@@ -243,6 +328,8 @@ export function useDomEditSession({
|
|
|
243
328
|
}
|
|
244
329
|
if (!doc) return;
|
|
245
330
|
|
|
331
|
+
reapplyPositionEditsAfterSeek(doc);
|
|
332
|
+
|
|
246
333
|
const nextElement = findElementForSelection(doc, currentSelection, activeCompPath);
|
|
247
334
|
if (!nextElement) {
|
|
248
335
|
applyDomSelection(null, { revealPanel: false });
|
|
@@ -345,5 +432,16 @@ export function useDomEditSession({
|
|
|
345
432
|
setAgentModalOpen,
|
|
346
433
|
setAgentPromptSelectionContext,
|
|
347
434
|
setAgentModalAnchorPoint,
|
|
435
|
+
|
|
436
|
+
// GSAP script editing
|
|
437
|
+
selectedGsapAnimations,
|
|
438
|
+
gsapMultipleTimelines,
|
|
439
|
+
gsapUnsupportedTimelinePattern,
|
|
440
|
+
handleGsapUpdateProperty,
|
|
441
|
+
handleGsapUpdateMeta,
|
|
442
|
+
handleGsapDeleteAnimation,
|
|
443
|
+
handleGsapAddAnimation,
|
|
444
|
+
handleGsapAddProperty,
|
|
445
|
+
handleGsapRemoveProperty,
|
|
348
446
|
};
|
|
349
447
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
resolveDomEditSelection,
|
|
17
17
|
type DomEditSelection,
|
|
18
18
|
} from "../components/editor/domEditing";
|
|
19
|
+
import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
|
|
19
20
|
|
|
20
21
|
// ── Types ──
|
|
21
22
|
|
|
@@ -218,6 +219,11 @@ export function useDomSelection({
|
|
|
218
219
|
) => {
|
|
219
220
|
const iframe = previewIframeRef.current;
|
|
220
221
|
if (!iframe || captionEditMode) return null;
|
|
222
|
+
try {
|
|
223
|
+
if (iframe.contentDocument) reapplyPositionEditsAfterSeek(iframe.contentDocument);
|
|
224
|
+
} catch {
|
|
225
|
+
/* cross-origin guard */
|
|
226
|
+
}
|
|
221
227
|
const target = getPreviewTargetFromPointer(iframe, clientX, clientY, activeCompPath);
|
|
222
228
|
if (!target) return null;
|
|
223
229
|
return buildDomSelectionFromTarget(target, {
|
|
@@ -245,6 +251,8 @@ export function useDomSelection({
|
|
|
245
251
|
}
|
|
246
252
|
if (!doc) return null;
|
|
247
253
|
|
|
254
|
+
reapplyPositionEditsAfterSeek(doc);
|
|
255
|
+
|
|
248
256
|
const targetElement = findElementForTimelineElement(doc, element, {
|
|
249
257
|
activeCompositionPath: activeCompPath,
|
|
250
258
|
compIdToSrc,
|