@hyperframes/studio 0.8.133 → 0.8.135
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-Cjcwveg0.js → hyperframes-player-DFv9heE6.js} +1 -1
- package/dist/assets/{index-CwgWmfgx.js → index-Buy06mZy.js} +1 -1
- package/dist/assets/{index-ME-Z5jh6.js → index-CTp4loy9.js} +1 -1
- package/dist/assets/{index-Dj21zXWA.js → index-N_4Ij4X6.js} +184 -183
- package/dist/assets/{index-D11atkBt.css → index-syMK0_9C.css} +1 -1
- package/dist/{chunk-5VSXRGIX.js → chunk-NM7BXNZ7.js} +1 -1
- package/dist/chunk-NM7BXNZ7.js.map +1 -0
- package/dist/{chunk-FOEID72M.js → chunk-QRLKWENQ.js} +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.html +2 -2
- package/dist/index.js +911 -691
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/dist/ui.d.ts +3 -2
- package/dist/ui.js +1 -1
- package/package.json +8 -8
- package/src/components/editor/AnimationCard.test.tsx +60 -0
- package/src/components/editor/AnimationCard.tsx +16 -6
- package/src/components/editor/KeyframeEaseList.tsx +4 -1
- package/src/components/editor/LayersPanel.tsx +39 -47
- package/src/components/editor/domEditInlineText.ts +1 -1
- package/src/components/editor/gsapAnimationHelpers.test.ts +29 -0
- package/src/components/editor/gsapAnimationHelpers.ts +34 -0
- package/src/components/editor/layerKind.test.ts +38 -0
- package/src/components/editor/layerKind.ts +19 -0
- package/src/components/editor/manualEditsDom.ts +5 -1
- package/src/components/editor/plainTranslate.test.ts +21 -2
- package/src/components/editor/plainTranslate.ts +7 -0
- package/src/components/editor/useDomEditNudge.test.tsx +48 -27
- package/src/components/ui/cn.ts +2 -1
- package/src/contexts/DomEditContext.tsx +3 -1
- package/src/hooks/draggedGsapPosition.test.ts +34 -0
- package/src/hooks/draggedGsapPosition.ts +40 -15
- package/src/hooks/gsapDragCommit.ts +17 -10
- package/src/hooks/gsapDragPositionCommit.ts +8 -9
- package/src/hooks/gsapPlaybackTestHarness.ts +28 -0
- package/src/hooks/gsapResizeIntercept.test.ts +72 -1
- package/src/hooks/gsapResizeIntercept.ts +84 -17
- package/src/hooks/gsapRuntimeBridge.ts +4 -1
- package/src/hooks/gsapRuntimePatch.ts +4 -0
- package/src/hooks/gsapShared.test.ts +11 -0
- package/src/hooks/gsapShared.ts +1 -1
- package/src/hooks/gsapTrailingKeyframes.realGsap.test.ts +44 -0
- package/src/hooks/keyframeEase.realGsap.test.ts +37 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +33 -0
- package/src/hooks/useAnimatedPropertyCommit.ts +3 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +21 -0
- package/src/hooks/useDomGeometryCommits.ts +6 -1
- package/src/hooks/useEditHistoryActions.test.tsx +20 -0
- package/src/hooks/useEditHistoryActions.ts +3 -0
- package/src/hooks/useEnableKeyframes.ts +32 -37
- package/src/hooks/useGestureCommit.test.tsx +89 -0
- package/src/hooks/useGestureCommit.ts +26 -9
- package/src/hooks/useGsapAwareEditing.test.tsx +1 -0
- package/src/hooks/useGsapAwareEditing.ts +10 -2
- package/src/icons/SystemIcons.tsx +6 -0
- package/src/layersPanelExport.test.tsx +45 -4
- package/src/utils/elementGsap.ts +6 -5
- package/src/utils/gsapKeyframeEases.ts +2 -0
- package/src/utils/gsapLiveRetime.cache.test.ts +39 -0
- package/src/utils/gsapLiveRetime.realGsap.test.ts +39 -15
- package/src/utils/gsapLiveRetime.ts +39 -31
- package/src/utils/gsapSoftReload.test.ts +98 -14
- package/src/utils/gsapSoftReload.ts +14 -2
- package/src/utils/gsapUndoRestore.test.ts +43 -2
- package/src/utils/gsapUndoRestore.ts +4 -2
- package/src/utils/softReloadTargets.ts +56 -0
- package/src/utils/studioPendingEdits.test.ts +123 -0
- package/src/utils/studioPendingEdits.ts +39 -3
- package/dist/chunk-5VSXRGIX.js.map +0 -1
- /package/dist/{chunk-FOEID72M.js.map → chunk-QRLKWENQ.js.map} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// fallow-ignore-file code-duplication
|
|
1
2
|
import { trackStudioEvent } from "../../utils/studioTelemetry";
|
|
2
3
|
// @vitest-environment happy-dom
|
|
3
4
|
import React, { act } from "react";
|
|
@@ -7,7 +8,11 @@ import { installReactActEnvironment, makeSelection } from "../../hooks/domSelect
|
|
|
7
8
|
import { useDomEditNudge, type UseDomEditNudgeParams } from "./useDomEditNudge";
|
|
8
9
|
import { CANVAS_NUDGE_COMMIT_DEBOUNCE_MS, CANVAS_NUDGE_STEP_PX } from "./domEditNudge";
|
|
9
10
|
import { __resetForTests } from "../../utils/canvasNudgeGate";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
flushStudioPendingEdits,
|
|
13
|
+
hasStudioPendingEdits,
|
|
14
|
+
trackedStudioEdit,
|
|
15
|
+
} from "../../utils/studioPendingEdits";
|
|
11
16
|
import type { DomEditSelection } from "./domEditing";
|
|
12
17
|
import type { OverlayRect } from "./domEditOverlayGeometry";
|
|
13
18
|
|
|
@@ -392,23 +397,32 @@ describe("useDomEditNudge pauses playback before it snapshots the timelines", ()
|
|
|
392
397
|
});
|
|
393
398
|
});
|
|
394
399
|
|
|
400
|
+
/** A burst-ready Harness on a fresh element, under fake timers. */
|
|
401
|
+
function mountBurstHarness(
|
|
402
|
+
id: string,
|
|
403
|
+
onPathOffsetCommit: UseDomEditNudgeParams["onPathOffsetCommitRef"]["current"],
|
|
404
|
+
) {
|
|
405
|
+
__resetForTests();
|
|
406
|
+
vi.useFakeTimers();
|
|
407
|
+
const root = createRoot(document.body.appendChild(document.createElement("div")));
|
|
408
|
+
const element = document.body.appendChild(document.createElement("div"));
|
|
409
|
+
element.id = id;
|
|
410
|
+
act(() => {
|
|
411
|
+
root.render(
|
|
412
|
+
React.createElement(Harness, {
|
|
413
|
+
selection: makeSelection("Dot", element),
|
|
414
|
+
onPathOffsetCommit,
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
return root;
|
|
419
|
+
}
|
|
420
|
+
|
|
395
421
|
describe("useDomEditNudge — undo right after a burst", () => {
|
|
396
422
|
it("undo's drain commits a burst still inside its debounce and waits for its save", async () => {
|
|
397
|
-
__resetForTests();
|
|
398
|
-
vi.useFakeTimers();
|
|
399
|
-
const root = createRoot(document.body.appendChild(document.createElement("div")));
|
|
400
|
-
const element = document.body.appendChild(document.createElement("div"));
|
|
401
|
-
element.id = "dot-undo";
|
|
402
423
|
let saved!: () => void;
|
|
403
424
|
const commit = vi.fn(() => new Promise<void>((resolve) => (saved = resolve)));
|
|
404
|
-
|
|
405
|
-
root.render(
|
|
406
|
-
React.createElement(Harness, {
|
|
407
|
-
selection: makeSelection("Dot", element),
|
|
408
|
-
onPathOffsetCommit: commit,
|
|
409
|
-
}),
|
|
410
|
-
);
|
|
411
|
-
});
|
|
425
|
+
const root = mountBurstHarness("dot-undo", commit);
|
|
412
426
|
act(() => dispatchArrowRight());
|
|
413
427
|
|
|
414
428
|
let drained = false;
|
|
@@ -424,21 +438,9 @@ describe("useDomEditNudge — undo right after a burst", () => {
|
|
|
424
438
|
});
|
|
425
439
|
|
|
426
440
|
it("counts the burst as a pending edit from its first key until its save lands", async () => {
|
|
427
|
-
__resetForTests();
|
|
428
|
-
vi.useFakeTimers();
|
|
429
|
-
const root = createRoot(document.body.appendChild(document.createElement("div")));
|
|
430
|
-
const element = document.body.appendChild(document.createElement("div"));
|
|
431
|
-
element.id = "dot-pending";
|
|
432
441
|
let saved!: () => void;
|
|
433
442
|
const commit = vi.fn(() => new Promise<void>((resolve) => (saved = resolve)));
|
|
434
|
-
|
|
435
|
-
root.render(
|
|
436
|
-
React.createElement(Harness, {
|
|
437
|
-
selection: makeSelection("Dot", element),
|
|
438
|
-
onPathOffsetCommit: commit,
|
|
439
|
-
}),
|
|
440
|
-
);
|
|
441
|
-
});
|
|
443
|
+
const root = mountBurstHarness("dot-pending", commit);
|
|
442
444
|
expect(hasStudioPendingEdits()).toBe(false);
|
|
443
445
|
act(() => dispatchArrowRight());
|
|
444
446
|
expect(hasStudioPendingEdits()).toBe(true);
|
|
@@ -452,6 +454,25 @@ describe("useDomEditNudge — undo right after a burst", () => {
|
|
|
452
454
|
});
|
|
453
455
|
});
|
|
454
456
|
|
|
457
|
+
describe("useDomEditNudge — a Design-panel edit during a burst", () => {
|
|
458
|
+
it("commits the burst before the panel edit, so one undo takes back the panel edit", async () => {
|
|
459
|
+
const order: string[] = [];
|
|
460
|
+
const root = mountBurstHarness("dot-panel", async () => void order.push("nudge"));
|
|
461
|
+
try {
|
|
462
|
+
act(() => dispatchArrowRight());
|
|
463
|
+
const panelEdit = trackedStudioEdit(async () => void order.push("width"), {
|
|
464
|
+
afterOlderSaves: true,
|
|
465
|
+
})();
|
|
466
|
+
vi.useRealTimers();
|
|
467
|
+
await panelEdit;
|
|
468
|
+
expect(order).toEqual(["nudge", "width"]);
|
|
469
|
+
} finally {
|
|
470
|
+
vi.useRealTimers();
|
|
471
|
+
act(() => root.unmount());
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
455
476
|
describe("useDomEditNudge — a commit that throws", () => {
|
|
456
477
|
it("still ends the burst's pending edit, so undo and export never wait on it", async () => {
|
|
457
478
|
__resetForTests();
|
package/src/components/ui/cn.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* invents (`text-step-*`, `ctl`/`ctl-sm`/`ctl-lg`) so overrides replace, not stack.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { CnFunction } from "cn";
|
|
6
7
|
import { createCn } from "cn/config";
|
|
7
8
|
|
|
8
9
|
/** `text-step-11`, `text-step-9`, … — the type scale from `theme.css`. */
|
|
@@ -11,7 +12,7 @@ const isTypeStep = (value: string) => /^step-\d+$/.test(value);
|
|
|
11
12
|
/** `ctl`, `ctl-sm`, `ctl-lg` — the control heights from `theme.css`. */
|
|
12
13
|
const isControlSize = (value: string) => /^ctl(-sm|-lg)?$/.test(value);
|
|
13
14
|
|
|
14
|
-
export const cn = createCn({
|
|
15
|
+
export const cn: CnFunction = createCn({
|
|
15
16
|
extend: {
|
|
16
17
|
classGroups: {
|
|
17
18
|
"font-size": [{ text: [isTypeStep] }],
|
|
@@ -107,7 +107,9 @@ function trackEditCommits(actions: DomEditActionsValue): DomEditActionsValue {
|
|
|
107
107
|
const tracked: Record<string, unknown> = { ...actions };
|
|
108
108
|
for (const key of Object.keys(SAVES_AN_EDIT) as Array<keyof typeof SAVES_AN_EDIT>) {
|
|
109
109
|
if (!SAVES_AN_EDIT[key]) continue;
|
|
110
|
-
tracked[key] = trackedStudioEdit(actions[key] as (...args: unknown[]) => unknown
|
|
110
|
+
tracked[key] = trackedStudioEdit(actions[key] as (...args: unknown[]) => unknown, {
|
|
111
|
+
afterOlderSaves: true,
|
|
112
|
+
});
|
|
111
113
|
}
|
|
112
114
|
return tracked as unknown as DomEditActionsValue;
|
|
113
115
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { expect, it, vi } from "vitest";
|
|
3
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
|
+
import { freezeDragStamp, restoreDragOffset } from "./draggedGsapPosition";
|
|
5
|
+
import { commitStaticGsapPosition } from "./gsapDragCommit";
|
|
6
|
+
|
|
7
|
+
function stamp(element: HTMLElement, base: { x: number; y: number }) {
|
|
8
|
+
element.setAttribute("data-hf-drag-gsap-base-x", String(base.x));
|
|
9
|
+
element.setAttribute("data-hf-drag-gsap-base-y", String(base.y));
|
|
10
|
+
element.setAttribute("data-hf-drag-initial-offset-x", "0");
|
|
11
|
+
element.setAttribute("data-hf-drag-initial-offset-y", "0");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// A resize released at x 60 moves its anchor by -50; the next drag presses before that write is planned.
|
|
15
|
+
it("a commit that outlives its gesture uses the gesture's stamp, not the next one's", async () => {
|
|
16
|
+
const element = document.createElement("div");
|
|
17
|
+
stamp(element, { x: 60, y: 37.5 });
|
|
18
|
+
const frozen = freezeDragStamp(element);
|
|
19
|
+
stamp(element, { x: 10, y: 4 });
|
|
20
|
+
const commitMutation = vi.fn();
|
|
21
|
+
|
|
22
|
+
await commitStaticGsapPosition(
|
|
23
|
+
{ id: "target", selector: "#target", element } as DomEditSelection,
|
|
24
|
+
{ x: -50, y: -33.5 },
|
|
25
|
+
{ x: 0, y: 0 },
|
|
26
|
+
"#target",
|
|
27
|
+
null,
|
|
28
|
+
{ commitMutation, stamp: frozen },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(commitMutation.mock.calls[0]![1].properties).toEqual({ x: 10, y: 4 });
|
|
32
|
+
restoreDragOffset(element, frozen);
|
|
33
|
+
expect(element.getAttribute("data-hf-drag-initial-offset-x")).toBe("0");
|
|
34
|
+
});
|
|
@@ -36,10 +36,35 @@ export function readCssRotation(element: HTMLElement, withRotate = true): number
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export interface DragStamp {
|
|
40
|
+
origX: number;
|
|
41
|
+
origY: number;
|
|
42
|
+
baseX: number;
|
|
43
|
+
baseY: number;
|
|
44
|
+
frozen?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The drag-start attributes as they are now; NaN for an absent base. */
|
|
48
|
+
export function readDragStamp(element: HTMLElement): DragStamp {
|
|
49
|
+
const read = (name: string) => Number.parseFloat(element.getAttribute(name) ?? "");
|
|
50
|
+
return {
|
|
51
|
+
origX: read("data-hf-drag-initial-offset-x") || 0,
|
|
52
|
+
origY: read("data-hf-drag-initial-offset-y") || 0,
|
|
53
|
+
baseX: read("data-hf-drag-gsap-base-x"),
|
|
54
|
+
baseY: read("data-hf-drag-gsap-base-y"),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A gesture's stamp read at its release, for commits that finish after the next gesture re-stamps the element. */
|
|
59
|
+
export const freezeDragStamp = (element: HTMLElement): DragStamp => ({
|
|
60
|
+
...readDragStamp(element),
|
|
61
|
+
frozen: true,
|
|
62
|
+
});
|
|
63
|
+
|
|
39
64
|
/**
|
|
40
65
|
* Translate a studio drag offset into absolute GSAP x/y, accounting for the
|
|
41
66
|
* element's rotation and its drag-start base pose. Reads the drag-start
|
|
42
|
-
*
|
|
67
|
+
* stamp (`readDragStamp` by default) set by `createManualOffsetDragMember`
|
|
43
68
|
* (`data-hf-drag-initial-offset-*`, `data-hf-drag-gsap-base-*`); `fallbackBase`
|
|
44
69
|
* is used when the base attributes are absent (e.g. a static element that GSAP
|
|
45
70
|
* hasn't given an x/y yet).
|
|
@@ -52,22 +77,19 @@ export function computeDraggedGsapPosition(
|
|
|
52
77
|
element: HTMLElement,
|
|
53
78
|
studioOffset: { x: number; y: number },
|
|
54
79
|
fallbackBase: { x: number; y: number },
|
|
80
|
+
stamp: DragStamp = readDragStamp(element),
|
|
55
81
|
): { newX: number; newY: number; baseGsapX: number; baseGsapY: number } {
|
|
56
82
|
const rotStyle = element.style.getPropertyValue("--hf-studio-rotation");
|
|
57
83
|
const rotDeg = Number.parseFloat(rotStyle) || 0;
|
|
58
84
|
const rad = (-rotDeg * Math.PI) / 180;
|
|
59
85
|
const cos = Math.cos(rad);
|
|
60
86
|
const sin = Math.sin(rad);
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const deltaX = studioOffset.x - origX;
|
|
64
|
-
const deltaY = studioOffset.y - origY;
|
|
87
|
+
const deltaX = studioOffset.x - stamp.origX;
|
|
88
|
+
const deltaY = studioOffset.y - stamp.origY;
|
|
65
89
|
const adjX = deltaX * cos - deltaY * sin;
|
|
66
90
|
const adjY = deltaX * sin + deltaY * cos;
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const baseGsapX = Number.isFinite(parsedBaseX) ? parsedBaseX : fallbackBase.x;
|
|
70
|
-
const baseGsapY = Number.isFinite(parsedBaseY) ? parsedBaseY : fallbackBase.y;
|
|
91
|
+
const baseGsapX = Number.isFinite(stamp.baseX) ? stamp.baseX : fallbackBase.x;
|
|
92
|
+
const baseGsapY = Number.isFinite(stamp.baseY) ? stamp.baseY : fallbackBase.y;
|
|
71
93
|
return {
|
|
72
94
|
newX: roundTo3(baseGsapX + adjX),
|
|
73
95
|
newY: roundTo3(baseGsapY + adjY),
|
|
@@ -76,12 +98,15 @@ export function computeDraggedGsapPosition(
|
|
|
76
98
|
};
|
|
77
99
|
}
|
|
78
100
|
|
|
79
|
-
/** Puts the drag's preview offset back once the written position renders instead.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
/** Puts the drag's preview offset back once the written position renders instead. A frozen stamp's
|
|
102
|
+
* gesture is over: the attributes belong to the one after it. */
|
|
103
|
+
export function restoreDragOffset(
|
|
104
|
+
element: HTMLElement,
|
|
105
|
+
stamp: DragStamp = readDragStamp(element),
|
|
106
|
+
): void {
|
|
107
|
+
if (stamp.frozen) return;
|
|
108
|
+
element.style.setProperty("--hf-studio-offset-x", `${stamp.origX}px`);
|
|
109
|
+
element.style.setProperty("--hf-studio-offset-y", `${stamp.origY}px`);
|
|
85
110
|
element.removeAttribute("data-hf-drag-initial-offset-x");
|
|
86
111
|
element.removeAttribute("data-hf-drag-initial-offset-y");
|
|
87
112
|
}
|
|
@@ -12,7 +12,12 @@ import {
|
|
|
12
12
|
} from "../utils/globalTimeCompiler";
|
|
13
13
|
import { roundTo3, roundToLayoutPx } from "../utils/rounding";
|
|
14
14
|
import { computeElementPercentage, keyframeEases, writeTargetSelector } from "./gsapShared";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
computeDraggedGsapPosition,
|
|
17
|
+
readDragStamp,
|
|
18
|
+
restoreDragOffset,
|
|
19
|
+
type DragStamp,
|
|
20
|
+
} from "./draggedGsapPosition";
|
|
16
21
|
import type { CommitMutation } from "./gsapScriptCommitTypes";
|
|
17
22
|
import { isGestureTransactionCommit, runGestureTransaction } from "./gestureTransaction";
|
|
18
23
|
import { setPatchFromUpdateProperty } from "./gsapDragStaticSetHelpers";
|
|
@@ -26,6 +31,8 @@ export {
|
|
|
26
31
|
export interface GsapDragCommitCallbacks {
|
|
27
32
|
commitMutation: CommitMutation;
|
|
28
33
|
fetchAnimations?: () => Promise<GsapAnimation[]>;
|
|
34
|
+
/** The gesture's drag stamp, when its commit can outlive it; else read from the element. */
|
|
35
|
+
stamp?: DragStamp;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/**
|
|
@@ -169,7 +176,12 @@ export async function commitStaticGsapPosition(
|
|
|
169
176
|
existingSet: GsapAnimation | null,
|
|
170
177
|
callbacks: GsapDragCommitCallbacks,
|
|
171
178
|
): Promise<void> {
|
|
172
|
-
const { newX, newY } = computeDraggedGsapPosition(
|
|
179
|
+
const { newX, newY } = computeDraggedGsapPosition(
|
|
180
|
+
selection.element,
|
|
181
|
+
studioOffset,
|
|
182
|
+
gsapPos,
|
|
183
|
+
callbacks.stamp,
|
|
184
|
+
);
|
|
173
185
|
if (existingSet) {
|
|
174
186
|
if (existingSet.keyframes) {
|
|
175
187
|
// Keyframed zero-duration hold (drag-path corruption): can't update-property
|
|
@@ -344,22 +356,17 @@ export async function commitWholePathOffset(
|
|
|
344
356
|
callbacks: GsapDragCommitCallbacks,
|
|
345
357
|
): Promise<void> {
|
|
346
358
|
const el = selection.element;
|
|
359
|
+
const stamp = callbacks.stamp ?? readDragStamp(el);
|
|
347
360
|
const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
|
|
348
361
|
el,
|
|
349
362
|
studioOffset,
|
|
350
363
|
gsapPos,
|
|
364
|
+
stamp,
|
|
351
365
|
);
|
|
352
366
|
const deltaX = newX - baseGsapX;
|
|
353
367
|
// fallow-ignore-next-line code-duplication
|
|
354
368
|
const deltaY = newY - baseGsapY;
|
|
355
|
-
const
|
|
356
|
-
const origY = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-y") ?? "") || 0;
|
|
357
|
-
const restoreOffset = () => {
|
|
358
|
-
el.style.setProperty("--hf-studio-offset-x", `${origX}px`);
|
|
359
|
-
el.style.setProperty("--hf-studio-offset-y", `${origY}px`);
|
|
360
|
-
el.removeAttribute("data-hf-drag-initial-offset-x");
|
|
361
|
-
el.removeAttribute("data-hf-drag-initial-offset-y");
|
|
362
|
-
};
|
|
369
|
+
const restoreOffset = () => restoreDragOffset(el, stamp);
|
|
363
370
|
|
|
364
371
|
// fallow-ignore-next-line code-duplication
|
|
365
372
|
let effectiveAnim = anim;
|
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
import { KEYFRAME_PCT_MATCH, playsNear } from "./gsapShared";
|
|
10
10
|
import { progressAtTime, runEaseOf, timeAtProgress } from "../utils/gsapKeyframeEases";
|
|
11
11
|
import { roundTo3 } from "../utils/rounding";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
computeDraggedGsapPosition,
|
|
14
|
+
readDragStamp,
|
|
15
|
+
restoreDragOffset,
|
|
16
|
+
} from "./draggedGsapPosition";
|
|
13
17
|
import {
|
|
14
18
|
type GsapDragCommitCallbacks,
|
|
15
19
|
computeCurrentPercentage,
|
|
@@ -117,20 +121,15 @@ export async function commitGsapPositionFromDrag(
|
|
|
117
121
|
callbacks: GsapDragCommitCallbacks,
|
|
118
122
|
): Promise<GsapEditOutcome> {
|
|
119
123
|
const el = selection.element;
|
|
124
|
+
const stamp = callbacks.stamp ?? readDragStamp(el);
|
|
120
125
|
// fallow-ignore-next-line code-duplication
|
|
121
126
|
const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
|
|
122
127
|
el,
|
|
123
128
|
studioOffset,
|
|
124
129
|
gsapPos,
|
|
130
|
+
stamp,
|
|
125
131
|
);
|
|
126
|
-
const
|
|
127
|
-
const origY = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-y") ?? "") || 0;
|
|
128
|
-
const restoreOffset = () => {
|
|
129
|
-
el.style.setProperty("--hf-studio-offset-x", `${origX}px`);
|
|
130
|
-
el.style.setProperty("--hf-studio-offset-y", `${origY}px`);
|
|
131
|
-
el.removeAttribute("data-hf-drag-initial-offset-x");
|
|
132
|
-
el.removeAttribute("data-hf-drag-initial-offset-y");
|
|
133
|
-
};
|
|
132
|
+
const restoreOffset = () => restoreDragOffset(el, stamp);
|
|
134
133
|
|
|
135
134
|
if (anim.arcPath?.enabled) {
|
|
136
135
|
const { activeKeyframePct, currentTime, setActiveKeyframePct } = usePlayerStore.getState();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { gsap } from "gsap";
|
|
2
|
+
|
|
3
|
+
type KeyframeMutation = {
|
|
4
|
+
duration: number;
|
|
5
|
+
ease?: string;
|
|
6
|
+
easeEach?: string;
|
|
7
|
+
keyframes: Array<{
|
|
8
|
+
percentage: number;
|
|
9
|
+
properties: Record<string, number | string>;
|
|
10
|
+
ease?: string;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Plays a replace-with-keyframes mutation in real GSAP and reads `x` at `time` seconds. */
|
|
15
|
+
export function xAtTime(mutation: KeyframeMutation, time: number): number {
|
|
16
|
+
const target = { x: 0, y: 0, opacity: 1 };
|
|
17
|
+
const frames: Record<string, unknown> = mutation.easeEach ? { easeEach: mutation.easeEach } : {};
|
|
18
|
+
for (const kf of mutation.keyframes) {
|
|
19
|
+
frames[`${kf.percentage}%`] = { ...kf.properties, ...(kf.ease ? { ease: kf.ease } : {}) };
|
|
20
|
+
}
|
|
21
|
+
const vars = {
|
|
22
|
+
duration: mutation.duration,
|
|
23
|
+
keyframes: frames,
|
|
24
|
+
...(mutation.ease ? { ease: mutation.ease } : {}),
|
|
25
|
+
};
|
|
26
|
+
gsap.timeline({ paused: true }).to(target, vars, 0).seek(time);
|
|
27
|
+
return target.x;
|
|
28
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
|
-
import { afterEach, expect, it, vi } from "vitest";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
4
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
5
5
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
@@ -182,6 +182,7 @@ it("reuses the ownership parse instead of fetching a resolved size group twice",
|
|
|
182
182
|
await expect(
|
|
183
183
|
tryGsapResizeIntercept(
|
|
184
184
|
selection,
|
|
185
|
+
// fallow-ignore-next-line code-duplication
|
|
185
186
|
{ width: 344, height: 344 },
|
|
186
187
|
[],
|
|
187
188
|
null,
|
|
@@ -573,6 +574,76 @@ it("hands the size to the element's CSS when its only tween is a fade", async ()
|
|
|
573
574
|
expect(commitMutation).not.toHaveBeenCalled();
|
|
574
575
|
});
|
|
575
576
|
|
|
577
|
+
describe("the first resize of a keyframed element under auto-record", () => {
|
|
578
|
+
const positionKeys = {
|
|
579
|
+
id: "#title-to-1-position",
|
|
580
|
+
targetSelector: "#title",
|
|
581
|
+
propertyGroup: "position",
|
|
582
|
+
method: "to",
|
|
583
|
+
properties: {},
|
|
584
|
+
keyframes: { keyframes: [{ percentage: 0, properties: { x: 300, y: 200 } }] },
|
|
585
|
+
position: 1,
|
|
586
|
+
resolvedStart: 1,
|
|
587
|
+
duration: 3,
|
|
588
|
+
} as unknown as GsapAnimation;
|
|
589
|
+
|
|
590
|
+
it("writes a size key at the playhead in one mutation and hands the draft size to GSAP", async () => {
|
|
591
|
+
usePlayerStore.setState({ currentTime: 2 });
|
|
592
|
+
const commitMutation = vi.fn();
|
|
593
|
+
const selection = titleSelection();
|
|
594
|
+
selection.element.setAttribute("data-hf-studio-box-size", "true");
|
|
595
|
+
|
|
596
|
+
// fallow-ignore-next-line code-duplication
|
|
597
|
+
const handled = await tryGsapResizeIntercept(
|
|
598
|
+
selection,
|
|
599
|
+
{ width: 424.2, height: 237 },
|
|
600
|
+
[positionKeys],
|
|
601
|
+
null,
|
|
602
|
+
commitMutation,
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
expect(handled).toEqual({ status: "persisted" });
|
|
606
|
+
expect(commitMutation.mock.calls.map((call) => call[1])).toEqual([
|
|
607
|
+
{
|
|
608
|
+
type: "add-with-keyframes",
|
|
609
|
+
targetSelector: "#title",
|
|
610
|
+
position: 2,
|
|
611
|
+
duration: 1,
|
|
612
|
+
keyframes: [{ percentage: 0, properties: { width: 424, height: 237 } }],
|
|
613
|
+
},
|
|
614
|
+
]);
|
|
615
|
+
expect(selection.element.hasAttribute("data-hf-studio-box-size")).toBe(false);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const fadeKeys = {
|
|
619
|
+
...positionKeys,
|
|
620
|
+
id: "#title-to-1-visual",
|
|
621
|
+
propertyGroup: "visual",
|
|
622
|
+
keyframes: { keyframes: [{ percentage: 0, properties: { opacity: 0 } }] },
|
|
623
|
+
} as unknown as GsapAnimation;
|
|
624
|
+
|
|
625
|
+
it.each([
|
|
626
|
+
["with auto-record off", positionKeys, false],
|
|
627
|
+
["on an element whose only keyframes fade it", fadeKeys, true],
|
|
628
|
+
])("hands the size to CSS %s", async (_, keyed, autoKeyframeEnabled) => {
|
|
629
|
+
usePlayerStore.setState({ autoKeyframeEnabled });
|
|
630
|
+
const commitMutation = vi.fn();
|
|
631
|
+
try {
|
|
632
|
+
const handled = await tryGsapResizeIntercept(
|
|
633
|
+
titleSelection(),
|
|
634
|
+
{ width: 424, height: 237 },
|
|
635
|
+
[keyed],
|
|
636
|
+
null,
|
|
637
|
+
commitMutation,
|
|
638
|
+
);
|
|
639
|
+
expect(handled).toEqual({ status: "element-size" });
|
|
640
|
+
expect(commitMutation).not.toHaveBeenCalled();
|
|
641
|
+
} finally {
|
|
642
|
+
usePlayerStore.setState({ autoKeyframeEnabled: true });
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
576
647
|
it.each([
|
|
577
648
|
["stays put", { x: 0, y: 0 }, { width: 300 }, undefined],
|
|
578
649
|
["moves", { x: -40, y: 0 }, { width: 300, x: 10, y: 0 }, true],
|