@hyperframes/studio 0.8.106 → 0.8.108
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/{hyperframes-player-dl6K6ABp.js → hyperframes-player-BakGew45.js} +1 -1
- package/dist/assets/{index-ooaZZhKf.js → index-C5dyUHwk.js} +1 -1
- package/dist/assets/{index-CMiYVGv5.js → index-C842tcVa.js} +203 -208
- package/dist/assets/{index-DKT5NF1Y.js → index-CPe7d_wO.js} +1 -1
- package/dist/assets/{index-lrT-gfHa.css → index-ncVJ5IVu.css} +1 -1
- package/dist/chunk-6LUNLTHR.js +611 -0
- package/dist/chunk-6LUNLTHR.js.map +1 -0
- package/dist/{chunk-ICTZJPQQ.js → chunk-AOWVEVEU.js} +3 -494
- package/dist/chunk-AOWVEVEU.js.map +1 -0
- package/dist/{chunk-46XP7J3B.js → chunk-PQFEBX32.js} +5 -3
- package/dist/chunk-PQFEBX32.js.map +1 -0
- package/dist/{domEditingLayers-AQBDFNUC.js → domEditingLayers-JQPQGGT7.js} +3 -3
- package/dist/index.d.ts +377 -12
- package/dist/index.html +2 -2
- package/dist/index.js +13445 -11874
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/App.tsx +6 -3
- package/src/agentToolsExport.test.ts +8 -0
- package/src/captions/keyboard.test.ts +6 -0
- package/src/components/EditorShell.tsx +2 -0
- package/src/components/StudioRightPanels.tsx +2 -0
- package/src/components/TimelineToolbar.test.tsx +27 -0
- package/src/components/TimelineToolbar.tsx +2 -5
- package/src/components/editor/CropPresetBar.tsx +69 -0
- package/src/components/editor/DomEditCropHandles.test.tsx +44 -0
- package/src/components/editor/DomEditCropHandles.tsx +123 -95
- package/src/components/editor/DomEditOverlay.tsx +12 -18
- package/src/components/editor/PropertyPanel.tsx +1 -14
- package/src/components/editor/PropertyPanelFlat.tsx +3 -6
- package/src/components/editor/SnapToolbar.test.tsx +32 -1
- package/src/components/editor/SnapToolbar.tsx +3 -4
- package/src/components/editor/carveOutput.ts +54 -0
- package/src/components/editor/cropPresetStore.ts +33 -0
- package/src/components/editor/cropPresets.test.ts +41 -0
- package/src/components/editor/cropPresets.ts +24 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
- package/src/components/editor/domEditingLayers.test.ts +21 -0
- package/src/components/editor/domEditingLayers.ts +2 -0
- package/src/components/editor/dragUndoPaint.test.ts +71 -0
- package/src/components/editor/fontAssets.test.ts +12 -22
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +96 -33
- package/src/components/editor/gsapAnimatesProperty.ts +3 -0
- package/src/components/editor/manualEdits.test.ts +4 -3
- package/src/components/editor/manualEditsDom.ts +28 -5
- package/src/components/editor/manualEditsDomPatches.ts +5 -1
- package/src/components/editor/manualOffsetDrag.ts +15 -1
- package/src/components/editor/mediaAudioEdits.test.ts +271 -0
- package/src/components/editor/mediaAudioEdits.ts +282 -0
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +32 -4
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +3 -1
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +290 -87
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +17 -20
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +17 -27
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useDomEditNudge.test.tsx +134 -2
- package/src/components/editor/useDomEditNudge.ts +46 -23
- package/src/components/editor/useDomEditOverlayGestures.ts +17 -6
- package/src/components/editor/useFxCarve.ts +3 -61
- package/src/components/editor/useFxCarveGrouping.test.ts +13 -0
- package/src/components/editor/useFxCarveGrouping.ts +8 -3
- package/src/components/nle/AudioMeterStrip.test.tsx +53 -0
- package/src/components/nle/AudioMeterStrip.tsx +60 -2
- package/src/components/nle/useTimelineEditCallbacks.ts +4 -0
- package/src/components/panels/SlideshowPanel.test.ts +27 -0
- package/src/components/panels/SlideshowPanel.tsx +12 -7
- package/src/contexts/DomEditContext.test.tsx +62 -0
- package/src/contexts/DomEditContext.tsx +94 -77
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/hooks/canvasQuickSaves.test.tsx +72 -7
- package/src/hooks/domEditCommitTypes.ts +10 -0
- package/src/hooks/domEditPersistFailure.ts +12 -1
- package/src/hooks/domStyleCommit.fonts.test.ts +5 -4
- package/src/hooks/domStyleCommit.ts +2 -8
- package/src/hooks/gsapDragCommit.ts +3 -8
- package/src/hooks/gsapDragPositionCommit.ts +6 -1
- package/src/hooks/gsapEditOutcome.ts +2 -1
- package/src/hooks/gsapResizeElementSize.test.tsx +136 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +3 -1
- package/src/hooks/gsapResizeIntercept.test.ts +3 -11
- package/src/hooks/gsapResizeIntercept.ts +6 -2
- package/src/hooks/gsapResizeMixedTween.test.ts +1 -2
- package/src/hooks/gsapRuntimeBridge.test.ts +55 -0
- package/src/hooks/gsapRuntimeBridge.ts +5 -3
- package/src/hooks/gsapRuntimeKeyframes.ts +1 -3
- package/src/hooks/inspectorSaveReload.test.tsx +107 -0
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +16 -1
- package/src/hooks/useAppHotkeys.test.ts +17 -0
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -1
- package/src/hooks/useAppHotkeys.ts +3 -3
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +17 -0
- package/src/hooks/useAutomationSelectionKeyboard.ts +3 -9
- package/src/hooks/useDomEditAttributeCommits.ts +71 -29
- package/src/hooks/useDomEditCommits.test.tsx +172 -92
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditCommitsHelpers.ts +4 -6
- package/src/hooks/useDomEditPersist.ts +28 -13
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -3
- package/src/hooks/useDomEditTextCommits.ts +4 -8
- package/src/hooks/useDomGeometryCommits.ts +11 -2
- package/src/hooks/useEditHistoryActions.paint.test.tsx +163 -3
- package/src/hooks/useEditHistoryActions.ts +13 -4
- package/src/hooks/useElementPicker.test.ts +62 -13
- package/src/hooks/useElementPicker.ts +10 -2
- package/src/hooks/useFreezeFrame.test.ts +56 -0
- package/src/hooks/useFreezeFrame.ts +121 -0
- package/src/hooks/useGsapAwareEditing.ts +6 -6
- package/src/hooks/useGsapScriptCommits.test.tsx +19 -0
- package/src/hooks/useGsapScriptCommits.ts +5 -4
- package/src/hooks/useInlineTextEdit.test.tsx +27 -0
- package/src/hooks/useInlineTextEdit.ts +9 -7
- package/src/hooks/useKeyframeKeyboard.test.tsx +15 -0
- package/src/hooks/useKeyframeKeyboard.ts +2 -8
- package/src/hooks/usePersistentEditHistory.test.ts +9 -6
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/usePreviewPersistence.ts +6 -0
- package/src/hooks/useRazorSplit.ts +14 -1
- package/src/hooks/useRenderClipContent.test.ts +102 -0
- package/src/hooks/useRenderClipContent.ts +30 -23
- package/src/hooks/useTimelineEditing.ts +2 -2
- package/src/hooks/useTrackPendingTimelineEdit.ts +2 -6
- package/src/index.ts +8 -0
- package/src/player/components/AudibleVideoClipContent.tsx +29 -0
- package/src/player/components/ClipBadges.test.tsx +82 -0
- package/src/player/components/ClipBadges.tsx +67 -0
- package/src/player/components/ClipContextMenu.tsx +22 -0
- package/src/player/components/ClipPeakMarks.test.tsx +41 -0
- package/src/player/components/ClipPeakMarks.tsx +71 -0
- package/src/player/components/Timeline.test.ts +0 -21
- package/src/player/components/Timeline.tsx +0 -1
- package/src/player/components/TimelineClip.tsx +2 -0
- package/src/player/components/TimelineFxButton.test.tsx +2 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +37 -0
- package/src/player/components/TimelineTrackHeader.tsx +12 -25
- package/src/player/components/clipMenuAudioItems.test.tsx +125 -0
- package/src/player/components/clipMenuAudioItems.tsx +108 -0
- package/src/player/components/clipMenuDuck.test.ts +117 -0
- package/src/player/components/clipMenuDuck.ts +142 -0
- package/src/player/components/clipMenuNormalize.test.ts +95 -0
- package/src/player/components/clipMenuNormalize.ts +96 -0
- package/src/player/components/clipMenuToolItems.test.tsx +139 -0
- package/src/player/components/clipMenuToolItems.tsx +235 -0
- package/src/player/components/clipPeakRuns.test.ts +38 -0
- package/src/player/components/clipPeakRuns.ts +59 -0
- package/src/player/components/clipToolAttrs.test.ts +161 -0
- package/src/player/components/clipToolAttrs.ts +187 -0
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineLayout.ts +0 -28
- package/src/player/components/useClipToolState.ts +92 -0
- package/src/player/hooks/usePlaybackKeyboard.test.ts +43 -0
- package/src/player/lib/playbackShortcuts.ts +3 -7
- package/src/player/lib/timelineElementHelpers.test.ts +16 -0
- package/src/player/lib/timelineElementHelpers.ts +25 -2
- package/src/player/lib/timelineViewportBudgets.test.ts +2 -0
- package/src/player/lib/timelineViewportBudgets.ts +4 -0
- package/src/timelineChromeExports.test.tsx +7 -0
- package/src/utils/audioMeterMath.test.ts +37 -0
- package/src/utils/audioMeterMath.ts +9 -3
- package/src/utils/exportPeakCeiling.ts +1 -0
- package/src/utils/gsapSoftReload.ts +3 -2
- package/src/utils/gsapUndoRestore.browser.test.ts +94 -0
- package/src/utils/gsapUndoRestore.test.ts +145 -1
- package/src/utils/gsapUndoRestore.ts +70 -21
- package/src/utils/previewRelativePath.ts +19 -0
- package/src/utils/sourcePatcher.ts +2 -2
- package/src/utils/studioFontHelpers.ts +2 -23
- package/src/utils/studioHelpers.test.ts +30 -0
- package/src/utils/studioHelpers.ts +18 -4
- package/src/utils/studioPendingEdits.test.ts +129 -0
- package/src/utils/studioPendingEdits.ts +61 -18
- package/src/utils/timelineInspector.test.ts +33 -2
- package/src/utils/timelineInspector.ts +16 -4
- package/src/utils/typingTarget.test.ts +13 -0
- package/src/utils/typingTarget.ts +5 -2
- package/src/webmcp/useStudioAgentTools.ts +3 -1
- package/dist/chunk-46XP7J3B.js.map +0 -1
- package/dist/chunk-DI4LCJHL.js +0 -102
- package/dist/chunk-DI4LCJHL.js.map +0 -1
- package/dist/chunk-ICTZJPQQ.js.map +0 -1
- /package/dist/{domEditingLayers-AQBDFNUC.js.map → domEditingLayers-JQPQGGT7.js.map} +0 -0
|
@@ -14,38 +14,56 @@ const SOURCE =
|
|
|
14
14
|
'<!doctype html><html><head></head><body><div data-hf-id="hf-card" style="color: red">Card</div>' +
|
|
15
15
|
'<div id="clip" class="clip" data-start="1" data-duration="5" data-track-index="0"></div></body></html>';
|
|
16
16
|
|
|
17
|
-
// The server patches whatever is on disk; the writer refuses a stale base
|
|
18
|
-
// The first write can be held mid-flight.
|
|
17
|
+
// The server patches whatever is on disk, a font rule included; the writer refuses a stale base
|
|
18
|
+
// like If-Match does. The first write, a patch or a whole file, can be held mid-flight.
|
|
19
19
|
function fakeProject() {
|
|
20
20
|
let disk = SOURCE;
|
|
21
21
|
const refused: string[] = [];
|
|
22
22
|
let writes = 0;
|
|
23
|
+
let wholeFileWrites = 0;
|
|
23
24
|
let release = () => {};
|
|
24
25
|
const firstWrite = new Promise<void>((resolve) => (release = resolve));
|
|
26
|
+
const holdFirst = async () => {
|
|
27
|
+
if (++writes === 1) await firstWrite;
|
|
28
|
+
};
|
|
25
29
|
vi.stubGlobal(
|
|
26
30
|
"fetch",
|
|
27
31
|
vi.fn(async (input: string, init?: RequestInit) => {
|
|
28
32
|
if (input.includes("/file-mutations/patch-element/")) {
|
|
29
|
-
|
|
33
|
+
await holdFirst();
|
|
34
|
+
const { operations, fontFaceCss } = JSON.parse(String(init?.body)) as {
|
|
30
35
|
operations: Array<{ property?: string; value?: string }>;
|
|
36
|
+
fontFaceCss?: string;
|
|
31
37
|
};
|
|
32
38
|
for (const op of operations) {
|
|
33
39
|
if (op.property === "color") disk = disk.replace(/color: \w+/, `color: ${op.value}`);
|
|
34
40
|
}
|
|
41
|
+
if (fontFaceCss && !disk.includes(fontFaceCss))
|
|
42
|
+
disk = disk.replace(
|
|
43
|
+
"</head>",
|
|
44
|
+
`<style data-hf-studio-fonts="true">${fontFaceCss}</style></head>`,
|
|
45
|
+
);
|
|
35
46
|
return Response.json({ ok: true, changed: true, matched: true, content: disk });
|
|
36
47
|
}
|
|
37
48
|
return Response.json({ content: disk });
|
|
38
49
|
}),
|
|
39
50
|
);
|
|
40
51
|
const writeProjectFile = async (path: string, content: string, expected?: string) => {
|
|
41
|
-
|
|
52
|
+
wholeFileWrites += 1;
|
|
53
|
+
await holdFirst();
|
|
42
54
|
if (expected !== undefined && expected !== disk && content !== disk) {
|
|
43
55
|
refused.push(path);
|
|
44
56
|
throw new Error(`409 conflict on ${path}`);
|
|
45
57
|
}
|
|
46
58
|
disk = content;
|
|
47
59
|
};
|
|
48
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
writeProjectFile,
|
|
62
|
+
refused,
|
|
63
|
+
release: () => release(),
|
|
64
|
+
read: () => disk,
|
|
65
|
+
wholeFileWrites: () => wholeFileWrites,
|
|
66
|
+
};
|
|
49
67
|
}
|
|
50
68
|
|
|
51
69
|
type Project = ReturnType<typeof fakeProject>;
|
|
@@ -158,7 +176,7 @@ afterEach(() => {
|
|
|
158
176
|
});
|
|
159
177
|
|
|
160
178
|
describe("canvas edit commits share the file queue", () => {
|
|
161
|
-
it("lands a second canvas commit started while the first one's font
|
|
179
|
+
it("lands a second canvas commit started while the first one's font save is held", async () => {
|
|
162
180
|
const project = fakeProject();
|
|
163
181
|
const { hook, selection, toasts } = mountCanvasCommits(project);
|
|
164
182
|
const font = {
|
|
@@ -174,8 +192,55 @@ describe("canvas edit commits share the file queue", () => {
|
|
|
174
192
|
);
|
|
175
193
|
|
|
176
194
|
expect(toasts).toEqual([]);
|
|
177
|
-
expect(project.read()
|
|
195
|
+
expect(project.read().match(/@font-face/g)).toHaveLength(1);
|
|
178
196
|
expect(project.read()).toContain("color: green");
|
|
197
|
+
expect(project.wholeFileWrites(), "the font rides the edit's own patch").toBe(0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("writes a batch's prepared content over the patch and saves it once", async () => {
|
|
201
|
+
const project = fakeProject();
|
|
202
|
+
const { hook, selection, toasts } = mountCanvasCommits(project);
|
|
203
|
+
project.release();
|
|
204
|
+
|
|
205
|
+
const saved = await hook.handleDomAttributeBatchCommit(
|
|
206
|
+
selection,
|
|
207
|
+
[{ type: "attribute", property: "data-note", value: "1" }],
|
|
208
|
+
{
|
|
209
|
+
label: "Edit with prepared content",
|
|
210
|
+
prepareContent: (html) => html.replace("</head>", '<meta data-prepared="1"></head>'),
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(saved).toBe(true);
|
|
215
|
+
expect(toasts).toEqual([]);
|
|
216
|
+
expect(project.read().match(/data-prepared="1"/g)).toHaveLength(1);
|
|
217
|
+
expect(project.wholeFileWrites()).toBe(1);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("reports a failed follow-up write as a failed commit, not as saved", async () => {
|
|
221
|
+
const project = fakeProject();
|
|
222
|
+
project.release();
|
|
223
|
+
const failing = {
|
|
224
|
+
...project,
|
|
225
|
+
writeProjectFile: async () => {
|
|
226
|
+
throw new Error("409 conflict");
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
const { hook, selection, toasts } = mountCanvasCommits(failing);
|
|
230
|
+
|
|
231
|
+
const saved = await hook.handleDomAttributeBatchCommit(
|
|
232
|
+
selection,
|
|
233
|
+
[{ type: "attribute", property: "data-note", value: "1" }],
|
|
234
|
+
{
|
|
235
|
+
label: "Edit with prepared content",
|
|
236
|
+
prepareContent: (html) => html.replace("</head>", '<meta data-prepared="1"></head>'),
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(saved).toBe(false);
|
|
241
|
+
expect(toasts).toHaveLength(1);
|
|
242
|
+
expect(toasts[0]).toContain("couldn't finish updating");
|
|
243
|
+
expect(project.read()).not.toContain("data-prepared");
|
|
179
244
|
});
|
|
180
245
|
|
|
181
246
|
it("lands a canvas commit started while a timeline save is writing", async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
2
|
+
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
|
2
3
|
import type { PatchOperation, PatchTarget } from "../utils/sourcePatcher";
|
|
3
4
|
|
|
4
5
|
export interface DomEditPatchBatch {
|
|
@@ -53,7 +54,16 @@ export type PersistDomEditOperations = (
|
|
|
53
54
|
coalesceKey?: string;
|
|
54
55
|
coalesceMs?: number;
|
|
55
56
|
skipRefresh?: boolean;
|
|
57
|
+
deferRender?: boolean;
|
|
58
|
+
importedFont?: ImportedFontAsset;
|
|
56
59
|
prepareContent?: (html: string, sourceFile: string) => string;
|
|
57
60
|
shouldSave?: () => boolean;
|
|
58
61
|
},
|
|
59
62
|
) => Promise<DomEditPersistOutcome | undefined>;
|
|
63
|
+
|
|
64
|
+
/** Several data-* and HTML attribute ops on one element as ONE persist and ONE undo entry. */
|
|
65
|
+
export type CommitDomAttributeBatch = (
|
|
66
|
+
selection: DomEditSelection,
|
|
67
|
+
operations: PatchOperation[],
|
|
68
|
+
options: { label: string; prepareContent?: (html: string) => string },
|
|
69
|
+
) => Promise<boolean>;
|
|
@@ -19,6 +19,15 @@ export class DomEditPersistUnsafeValueError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export class DomEditPersistPreparedWriteError extends Error {
|
|
23
|
+
readonly alreadyToasted = true;
|
|
24
|
+
|
|
25
|
+
constructor(targetPath: string) {
|
|
26
|
+
super(`The edit saved but its follow-up write to ${targetPath} did not`);
|
|
27
|
+
this.name = "DomEditPersistPreparedWriteError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
export type DomEditPersistFailureSelection = Pick<
|
|
23
32
|
DomEditSelection,
|
|
24
33
|
"label" | "hfId" | "id" | "selector" | "selectorIndex" | "sourceFile"
|
|
@@ -48,7 +57,9 @@ function getSelectionLabel(selection: DomEditPersistFailureSelection): string {
|
|
|
48
57
|
|
|
49
58
|
export function wasAlreadyToasted(error: unknown): boolean {
|
|
50
59
|
return (
|
|
51
|
-
(error instanceof DomEditPersistUnsafeValueError ||
|
|
60
|
+
(error instanceof DomEditPersistUnsafeValueError ||
|
|
61
|
+
error instanceof DomEditPersistPreparedWriteError ||
|
|
62
|
+
error instanceof StudioSaveHttpError) &&
|
|
52
63
|
error.alreadyToasted
|
|
53
64
|
);
|
|
54
65
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { expect, it } from "vitest";
|
|
3
3
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
4
4
|
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
|
5
|
+
import { importedFontFaceCssFor } from "../utils/studioFontHelpers";
|
|
5
6
|
import { commitDomStyles, type DomStyleCommitContext } from "./domStyleCommit";
|
|
6
7
|
|
|
7
8
|
const selection = (id: string): DomEditSelection =>
|
|
@@ -28,7 +29,7 @@ const poppins = (file: string, weight: string): ImportedFontAsset => ({
|
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
it("each font a host hands over is saved as its own face, with the weight and style it draws", async () => {
|
|
31
|
-
|
|
32
|
+
const faces: string[] = [];
|
|
32
33
|
let listed: ImportedFontAsset | null = null;
|
|
33
34
|
const context: DomStyleCommitContext = {
|
|
34
35
|
activeCompPath: "index.html",
|
|
@@ -39,16 +40,16 @@ it("each font a host hands over is saved as its own face, with the weight and st
|
|
|
39
40
|
persistDomEditOperations: (async (
|
|
40
41
|
_selection: DomEditSelection,
|
|
41
42
|
_operations: unknown,
|
|
42
|
-
options?: {
|
|
43
|
+
options?: { importedFont?: ImportedFontAsset },
|
|
43
44
|
) => {
|
|
44
|
-
|
|
45
|
+
if (options?.importedFont)
|
|
46
|
+
faces.push(importedFontFaceCssFor(options.importedFont, "index.html"));
|
|
45
47
|
}) as unknown as DomStyleCommitContext["persistDomEditOperations"],
|
|
46
48
|
};
|
|
47
49
|
listed = poppins("Poppins-SemiBold.ttf", "600");
|
|
48
50
|
await commitDomStyles(context, selection("title"), { "font-family": "Poppins" });
|
|
49
51
|
listed = poppins("Poppins-Regular.ttf", "400");
|
|
50
52
|
await commitDomStyles(context, selection("subtitle"), { "font-family": "Poppins" });
|
|
51
|
-
const faces = html.match(/@font-face[^}]*}/g) ?? [];
|
|
52
53
|
expect(faces).toHaveLength(2);
|
|
53
54
|
expect(faces[0]).toContain('Poppins-SemiBold.ttf"); font-weight: 600; font-style: normal;');
|
|
54
55
|
expect(faces[1]).toContain('Poppins-Regular.ttf"); font-weight: 400; font-style: normal;');
|
|
@@ -4,11 +4,7 @@ import {
|
|
|
4
4
|
isManualGeometryStyleProperty,
|
|
5
5
|
normalizeDomEditStyleValue,
|
|
6
6
|
} from "../utils/studioHelpers";
|
|
7
|
-
import {
|
|
8
|
-
injectPreviewGoogleFont,
|
|
9
|
-
injectPreviewImportedFont,
|
|
10
|
-
ensureImportedFontFace,
|
|
11
|
-
} from "../utils/studioFontHelpers";
|
|
7
|
+
import { injectPreviewGoogleFont, injectPreviewImportedFont } from "../utils/studioFontHelpers";
|
|
12
8
|
import {
|
|
13
9
|
buildDomEditStylePatchOperation,
|
|
14
10
|
findElementForSelection,
|
|
@@ -105,9 +101,7 @@ export async function commitDomStyles(
|
|
|
105
101
|
label: "Edit layer style",
|
|
106
102
|
// Inline styles are already live, so a reload would only blank the preview.
|
|
107
103
|
skipRefresh: true,
|
|
108
|
-
|
|
109
|
-
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
|
110
|
-
: undefined,
|
|
104
|
+
importedFont: importedFont ?? undefined,
|
|
111
105
|
}),
|
|
112
106
|
shouldRevert: ownsAny,
|
|
113
107
|
revert: () => {
|
|
@@ -273,14 +273,9 @@ export async function commitStaticGsapRotation(
|
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* tween: one keyframe at the playhead % renders NaN/0 at every other frame, so
|
|
280
|
-
* the element collapses/disappears (worst when resized off the 0% mark). A `set`
|
|
281
|
-
* holds the size at all times. Re-resizing an element that already has a size
|
|
282
|
-
* `set` UPDATES it in place with one `update-properties`; a new element
|
|
283
|
-
* gets one `add` with `method:"set"`.
|
|
276
|
+
* A static resize as `tl.set("#el",{width,height})`, only where the script already writes the size
|
|
277
|
+
* (else it is CSS). A set, not a one-stop keyframe tween, which renders NaN/0 off its keyframe.
|
|
278
|
+
* Updates an existing size set in place, else adds one.
|
|
284
279
|
*/
|
|
285
280
|
export async function commitStaticGsapSize(
|
|
286
281
|
selection: DomEditSelection,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
2
|
+
import { PROPERTY_GROUPS } from "@hyperframes/parsers/gsap-constants";
|
|
2
3
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
4
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
4
5
|
import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
|
|
@@ -179,7 +180,11 @@ async function commitFlatViaKeyframes(
|
|
|
179
180
|
if (runtime && ts !== null) {
|
|
180
181
|
const { gsapLib, el, mainTl } = runtime;
|
|
181
182
|
const draggedValues: Record<string, number> = {};
|
|
182
|
-
|
|
183
|
+
const channelsTheClearWipes = new Set([
|
|
184
|
+
...Object.keys(properties),
|
|
185
|
+
...PROPERTY_GROUPS.position,
|
|
186
|
+
]);
|
|
187
|
+
for (const key of channelsTheClearWipes) {
|
|
183
188
|
const v = Number(gsapLib.getProperty(el, key));
|
|
184
189
|
if (Number.isFinite(v)) draggedValues[key] = v;
|
|
185
190
|
}
|
|
@@ -44,7 +44,8 @@ export type GsapEditOutcome =
|
|
|
44
44
|
ownsDragOffset?: boolean;
|
|
45
45
|
}
|
|
46
46
|
| { status: "blocked"; reason: GsapEditBlockReason; detail?: GsapEditBlockDetail }
|
|
47
|
-
| { status: "element-offset" }
|
|
47
|
+
| { status: "element-offset" }
|
|
48
|
+
| { status: "element-size" };
|
|
48
49
|
|
|
49
50
|
export const GSAP_EDIT_BLOCK_COPY: Record<GsapEditBlockReason, string> = {
|
|
50
51
|
"no-selector": "This layer needs a stable selector before Studio can save the edit.",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
|
+
import { applyStudioBoxSizeDraft } from "../components/editor/manualEdits";
|
|
8
|
+
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
9
|
+
import { useDomGeometryCommits } from "./useDomGeometryCommits";
|
|
10
|
+
import { useGsapAwareEditing } from "./useGsapAwareEditing";
|
|
11
|
+
|
|
12
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
document.body.innerHTML = "";
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// The bench's hold fixture: an off-timeline `gsap.set` positions #target; its size is stylesheet CSS.
|
|
19
|
+
const positionHold = {
|
|
20
|
+
id: "#target-set-0-position",
|
|
21
|
+
targetSelector: "#target",
|
|
22
|
+
propertyGroup: "position",
|
|
23
|
+
method: "set",
|
|
24
|
+
properties: { x: 40, y: 20 },
|
|
25
|
+
position: 0,
|
|
26
|
+
resolvedStart: 0,
|
|
27
|
+
duration: 0,
|
|
28
|
+
global: true,
|
|
29
|
+
} as unknown as GsapAnimation;
|
|
30
|
+
|
|
31
|
+
const widthTween = {
|
|
32
|
+
id: "#target-to-0-size",
|
|
33
|
+
targetSelector: "#target",
|
|
34
|
+
propertyGroup: "size",
|
|
35
|
+
method: "to",
|
|
36
|
+
properties: { width: 400, height: 300 },
|
|
37
|
+
position: 0,
|
|
38
|
+
resolvedStart: 0,
|
|
39
|
+
duration: 4,
|
|
40
|
+
} as unknown as GsapAnimation;
|
|
41
|
+
|
|
42
|
+
function mount(animations: GsapAnimation[]) {
|
|
43
|
+
const element = Object.assign(document.createElement("div"), {
|
|
44
|
+
id: "target",
|
|
45
|
+
_gsap: { renderTransform: () => {} },
|
|
46
|
+
});
|
|
47
|
+
// GSAP's own inline mask on an element whose transform it renders.
|
|
48
|
+
element.style.setProperty("scale", "none");
|
|
49
|
+
document.body.append(element);
|
|
50
|
+
const gsap = { getProperty: () => 0, set: vi.fn() };
|
|
51
|
+
const iframe = {
|
|
52
|
+
contentWindow: { gsap, __timelines: {} },
|
|
53
|
+
contentDocument: document,
|
|
54
|
+
} as unknown as HTMLIFrameElement;
|
|
55
|
+
const selection = { element, id: "target", selector: "#target" } as unknown as DomEditSelection;
|
|
56
|
+
const commitMutation = vi.fn().mockResolvedValue(undefined);
|
|
57
|
+
const commitPatch = vi.fn().mockResolvedValue(undefined);
|
|
58
|
+
let resize!: ReturnType<typeof useGsapAwareEditing>["handleGsapAwareBoxSizeCommit"];
|
|
59
|
+
function Harness() {
|
|
60
|
+
const { stageElementPositionOffset, handleDomBoxSizeCommit } = useDomGeometryCommits({
|
|
61
|
+
showToast: vi.fn(),
|
|
62
|
+
commitPositionPatchToHtml: commitPatch,
|
|
63
|
+
readOnlyPreview: false,
|
|
64
|
+
});
|
|
65
|
+
resize = useGsapAwareEditing({
|
|
66
|
+
domEditSelection: selection,
|
|
67
|
+
selectedGsapAnimations: animations,
|
|
68
|
+
gsapCommitMutation: commitMutation,
|
|
69
|
+
previewIframeRef: { current: iframe },
|
|
70
|
+
showToast: vi.fn(),
|
|
71
|
+
bumpGsapCache: vi.fn(),
|
|
72
|
+
makeFetchFallback: () => async () => animations,
|
|
73
|
+
trackGsapInteractionFailure: vi.fn(),
|
|
74
|
+
stageElementPositionOffset,
|
|
75
|
+
handleDomBoxSizeCommit,
|
|
76
|
+
handleDomRotationCommit: vi.fn(),
|
|
77
|
+
commitPositionPatchToHtml: commitPatch,
|
|
78
|
+
addGsapAnimation: vi.fn(),
|
|
79
|
+
convertToKeyframes: vi.fn(),
|
|
80
|
+
setArcPath: vi.fn(),
|
|
81
|
+
updateArcSegment: vi.fn(),
|
|
82
|
+
}).handleGsapAwareBoxSizeCommit;
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const root = mountReactHarness(<Harness />);
|
|
86
|
+
const size = { width: 340, height: 227 };
|
|
87
|
+
applyStudioBoxSizeDraft(element, size);
|
|
88
|
+
return { selection, size, commitMutation, commitPatch, resize, root };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const writesSize = (mutation: { properties?: object; keyframes?: Array<{ properties: object }> }) =>
|
|
92
|
+
[mutation.properties, ...(mutation.keyframes ?? []).map((k) => k.properties)].some(
|
|
93
|
+
(props) => !!props && ("width" in props || "height" in props),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
describe("resizing an element GSAP positions", () => {
|
|
97
|
+
it("saves a size the script never writes as the element's CSS width/height, not a tl.set", async () => {
|
|
98
|
+
const h = mount([positionHold]);
|
|
99
|
+
|
|
100
|
+
await act(() => h.resize(h.selection, h.size, { x: -50, y: -33.5 }));
|
|
101
|
+
|
|
102
|
+
const mutations = h.commitMutation.mock.calls.map((call) => call[1]);
|
|
103
|
+
expect(mutations.filter(writesSize)).toEqual([]);
|
|
104
|
+
// The anchor still moves the GSAP hold, in the same undo step as the CSS size.
|
|
105
|
+
expect(mutations.some((m) => m.animationId === positionHold.id)).toBe(true);
|
|
106
|
+
const [, patches, options] = h.commitPatch.mock.calls.at(-1)!;
|
|
107
|
+
expect(patches).toEqual(
|
|
108
|
+
expect.arrayContaining([
|
|
109
|
+
{ type: "inline-style", property: "width", value: "340px" },
|
|
110
|
+
{ type: "inline-style", property: "height", value: "227px" },
|
|
111
|
+
]),
|
|
112
|
+
);
|
|
113
|
+
expect(patches.filter((p: { property: string }) => p.property === "scale")).toEqual([]);
|
|
114
|
+
expect(options.coalesceKey).toBe(h.commitMutation.mock.calls[0]![2].coalesceKey);
|
|
115
|
+
// Saved before the GSAP write and without its own render, so the GSAP reload renders last.
|
|
116
|
+
expect(options.deferRender).toBe(true);
|
|
117
|
+
expect(h.commitPatch.mock.invocationCallOrder.at(-1)).toBeLessThan(
|
|
118
|
+
h.commitMutation.mock.invocationCallOrder[0]!,
|
|
119
|
+
);
|
|
120
|
+
act(() => h.root.unmount());
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("still saves the size through GSAP when a tween animates it", async () => {
|
|
124
|
+
const h = mount([positionHold, widthTween]);
|
|
125
|
+
|
|
126
|
+
await act(() => h.resize(h.selection, h.size, { x: -50, y: -33.5 }));
|
|
127
|
+
|
|
128
|
+
const mutations = h.commitMutation.mock.calls.map((call) => call[1]);
|
|
129
|
+
expect(mutations.filter(writesSize).length).toBeGreaterThan(0);
|
|
130
|
+
const cssSize = h.commitPatch.mock.calls
|
|
131
|
+
.flatMap((call) => call[1])
|
|
132
|
+
.filter((p: { property: string }) => p.property === "width" || p.property === "height");
|
|
133
|
+
expect(cssSize).toEqual([]);
|
|
134
|
+
act(() => h.root.unmount());
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -238,7 +238,9 @@ async function runCase(testCase: Case): Promise<string[]> {
|
|
|
238
238
|
async () => animations,
|
|
239
239
|
);
|
|
240
240
|
|
|
241
|
-
const { scale, size } = committed(commitMutation.mock.calls);
|
|
241
|
+
const { scale, size: written } = committed(commitMutation.mock.calls);
|
|
242
|
+
const size =
|
|
243
|
+
outcome.status === "element-size" ? { w: testCase.drop.w, h: testCase.drop.h } : written;
|
|
242
244
|
const settled = renderRect(
|
|
243
245
|
{ box: size ?? testCase.box, pos: { ...live.pos }, scale: scale ?? testCase.liveScale },
|
|
244
246
|
testCase.rotation,
|
|
@@ -529,7 +529,7 @@ function titleSelection(): DomEditSelection {
|
|
|
529
529
|
return { id: "title", selector: "#title", element: el } as DomEditSelection;
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
-
it("
|
|
532
|
+
it("hands the size to the element's CSS when its only tween is a fade", async () => {
|
|
533
533
|
const fade = {
|
|
534
534
|
id: "#title-to-0-visual",
|
|
535
535
|
targetSelector: "#title",
|
|
@@ -550,14 +550,6 @@ it("writes a plain size on an element whose only tween is a fade", async () => {
|
|
|
550
550
|
commitMutation,
|
|
551
551
|
);
|
|
552
552
|
|
|
553
|
-
expect(handled).
|
|
554
|
-
|
|
555
|
-
expect(mutations.filter((m) => /keyframe/i.test(m.type))).toEqual([]);
|
|
556
|
-
expect(mutations).toContainEqual({
|
|
557
|
-
type: "add",
|
|
558
|
-
targetSelector: "#title",
|
|
559
|
-
method: "set",
|
|
560
|
-
position: 0,
|
|
561
|
-
properties: { width: 424, height: 237 },
|
|
562
|
-
});
|
|
553
|
+
expect(handled).toEqual({ status: "element-size" });
|
|
554
|
+
expect(commitMutation).not.toHaveBeenCalled();
|
|
563
555
|
});
|
|
@@ -38,6 +38,7 @@ import { animationWritesAnyProperty, type GsapEditOutcome } from "./gsapEditOutc
|
|
|
38
38
|
import { preflightGsapResizeIntercept, resizeRoute } from "./gsapResizePreflight";
|
|
39
39
|
|
|
40
40
|
const IDENTITY_ONE_PROPS = new Set(["opacity", "autoAlpha", "scale", "scaleX", "scaleY"]);
|
|
41
|
+
const SIZE_PROPS = new Set(["width", "height"]);
|
|
41
42
|
|
|
42
43
|
/** Build identity (zero / one) values for each property in `source`. */
|
|
43
44
|
function synthesizeIdentityProps(
|
|
@@ -101,7 +102,7 @@ export async function tryGsapResizeIntercept(
|
|
|
101
102
|
const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
|
|
102
103
|
const outcome = preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnimations);
|
|
103
104
|
if (outcome.status === "blocked") return outcome;
|
|
104
|
-
const { resizeGroup, resizeProperties, workingAnimations } = resizeRoute(
|
|
105
|
+
const { allKnownAnimations, resizeGroup, resizeProperties, workingAnimations } = resizeRoute(
|
|
105
106
|
animations,
|
|
106
107
|
fetchedAnimations,
|
|
107
108
|
);
|
|
@@ -131,6 +132,10 @@ export async function tryGsapResizeIntercept(
|
|
|
131
132
|
size,
|
|
132
133
|
});
|
|
133
134
|
if (!anim || isInstantHold(anim)) {
|
|
135
|
+
const scriptWritesSize = allKnownAnimations.some((a) =>
|
|
136
|
+
animationWritesAnyProperty(a, SIZE_PROPS),
|
|
137
|
+
);
|
|
138
|
+
if (!scriptWritesSize) return { status: "element-size" };
|
|
134
139
|
const sel = selectorFromSelection(selection) ?? writeTargetSelector(selection);
|
|
135
140
|
if (!sel) return { status: "blocked", reason: "no-selector" };
|
|
136
141
|
// A scale hold is not a size hold.
|
|
@@ -541,7 +546,6 @@ export async function tryGsapResizeIntercept(
|
|
|
541
546
|
return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
|
|
542
547
|
}
|
|
543
548
|
|
|
544
|
-
const SIZE_PROPS = new Set(["width", "height"]);
|
|
545
549
|
const backfillDefaults: Record<string, number> = {};
|
|
546
550
|
for (const k of Object.keys(runtimeProps)) {
|
|
547
551
|
if (SIZE_PROPS.has(k)) continue;
|
|
@@ -163,6 +163,5 @@ it("leaves the drag offset to the caller when it commits size, not scale", async
|
|
|
163
163
|
async () => [scaleHold],
|
|
164
164
|
);
|
|
165
165
|
|
|
166
|
-
expect(outcome.status).toBe("
|
|
167
|
-
expect(outcome.status === "persisted" && outcome.ownsDragOffset).not.toBe(true);
|
|
166
|
+
expect(outcome.status).toBe("element-size");
|
|
168
167
|
});
|
|
@@ -567,3 +567,58 @@ describe("tryGsapDragIntercept — motion paths", () => {
|
|
|
567
567
|
);
|
|
568
568
|
});
|
|
569
569
|
});
|
|
570
|
+
|
|
571
|
+
describe("tryGsapDragIntercept — position self-heal", () => {
|
|
572
|
+
const xyTween = {
|
|
573
|
+
...stalePositionAnim,
|
|
574
|
+
id: "#puck-b-to-0-position",
|
|
575
|
+
position: 0,
|
|
576
|
+
resolvedStart: 0,
|
|
577
|
+
};
|
|
578
|
+
const centringSet = {
|
|
579
|
+
id: "#puck-b-set-0-position",
|
|
580
|
+
targetSelector: "#puck-b",
|
|
581
|
+
propertyGroup: "position",
|
|
582
|
+
method: "set",
|
|
583
|
+
properties: { xPercent: -50, yPercent: -50 },
|
|
584
|
+
position: 0,
|
|
585
|
+
duration: 0,
|
|
586
|
+
} as unknown as GsapAnimation;
|
|
587
|
+
const xySet = { ...centringSet, properties: { x: 10, y: 20 } } as GsapAnimation;
|
|
588
|
+
const liveTween = {
|
|
589
|
+
targets: () => [{ id: "puck-b" }],
|
|
590
|
+
vars: { x: -180, y: -60, duration: 2 },
|
|
591
|
+
duration: () => 2,
|
|
592
|
+
startTime: () => 0,
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
async function drag(second: GsapAnimation) {
|
|
596
|
+
usePlayerStore.setState({ currentTime: 1 });
|
|
597
|
+
const animations = [second, xyTween];
|
|
598
|
+
const commitMutation = vi.fn();
|
|
599
|
+
await tryGsapDragIntercept(
|
|
600
|
+
selection,
|
|
601
|
+
{ x: -50, y: 30 },
|
|
602
|
+
animations,
|
|
603
|
+
fakeIframe("puck-b", [liveTween]),
|
|
604
|
+
commitMutation,
|
|
605
|
+
async () => animations,
|
|
606
|
+
);
|
|
607
|
+
return commitMutation.mock.calls.map(([, mutation]) => mutation);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
it("keeps an xPercent/yPercent centring set and moves the x/y tween", async () => {
|
|
611
|
+
const mutations = await drag(centringSet);
|
|
612
|
+
expect(mutations.map((m) => m.type)).not.toContain("consolidate-position-writes");
|
|
613
|
+
expect(mutations.length).toBeGreaterThan(0);
|
|
614
|
+
expect(mutations.every((m) => m.animationId === xyTween.id)).toBe(true);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it("consolidates a second x/y write", async () => {
|
|
618
|
+
const mutations = await drag(xySet);
|
|
619
|
+
expect(mutations[0]).toMatchObject({
|
|
620
|
+
type: "consolidate-position-writes",
|
|
621
|
+
keepAnimationId: xyTween.id,
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* easing, or seek position.
|
|
10
10
|
*/
|
|
11
11
|
import type { GsapAnimation, PropertyGroupName } from "@hyperframes/core/gsap-parser";
|
|
12
|
+
import { isXYPositionWrite } from "@hyperframes/parsers/gsap-constants";
|
|
12
13
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
13
14
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
14
15
|
|
|
@@ -245,8 +246,7 @@ export async function tryGsapDragIntercept(
|
|
|
245
246
|
// the live keyframed/real tween if present (else any), strip the rest, so the
|
|
246
247
|
// commit below updates ONE write instead of fighting duplicates.
|
|
247
248
|
let workingAnimations = animations;
|
|
248
|
-
const isPosWrite = (a: GsapAnimation) =>
|
|
249
|
-
a.targetSelector === selector && a.propertyGroup === "position";
|
|
249
|
+
const isPosWrite = (a: GsapAnimation) => a.targetSelector === selector && isXYPositionWrite(a);
|
|
250
250
|
if (animations.filter(isPosWrite).length > 1 && fetchFallbackAnimations) {
|
|
251
251
|
const fresh = await fetchFallbackAnimations();
|
|
252
252
|
const dupes = fresh.filter(isPosWrite);
|
|
@@ -331,7 +331,9 @@ export async function tryGsapDragIntercept(
|
|
|
331
331
|
const fresh = await fetchFallbackAnimations();
|
|
332
332
|
const freshMatch = fresh.find(
|
|
333
333
|
(a) =>
|
|
334
|
-
a.targetSelector === posAnim!.targetSelector &&
|
|
334
|
+
a.targetSelector === posAnim!.targetSelector &&
|
|
335
|
+
a.propertyGroup === posAnim!.propertyGroup &&
|
|
336
|
+
isXYPositionWrite(a) === isXYPositionWrite(posAnim!),
|
|
335
337
|
);
|
|
336
338
|
if (freshMatch && freshMatch.id !== posAnim.id) {
|
|
337
339
|
posAnim = freshMatch;
|
|
@@ -12,6 +12,7 @@ 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
|
import { BOX_SIZE_STYLE_PROPS } from "../components/editor/manualEditsDomPatches";
|
|
15
|
+
import { gsapRendersTransform } from "../components/editor/gsapAnimatesProperty";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* A GSAP tween's `vars` object — intentionally open: it mixes channel values
|
|
@@ -446,9 +447,6 @@ export const ROTATION_CHANNELS: string[] = [
|
|
|
446
447
|
...["rotate", "rotateX", "rotateY", "rotateZ"],
|
|
447
448
|
];
|
|
448
449
|
|
|
449
|
-
const gsapRendersTransform = (el: Element) =>
|
|
450
|
-
!!(el as { _gsap?: { renderTransform?: unknown } })._gsap?.renderTransform;
|
|
451
|
-
|
|
452
450
|
/** GSAP owns this element's position: a tween or hold writes it, or GSAP already renders its
|
|
453
451
|
* transform (a CSS translate would then apply twice). Everything else moves by plain CSS. */
|
|
454
452
|
export function gsapWritesPosition(el: Element): boolean {
|