@hyperframes/studio 0.7.63 → 0.7.65
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-BO2UXx-E.js → hyperframes-player-D2zuA3cx.js} +1 -1
- package/dist/assets/{index-C_7k3kwr.js → index-BqX4YLnH.js} +190 -190
- package/dist/assets/{index-_72wCKSP.js → index-DwwsYjuk.js} +1 -1
- package/dist/assets/{index-C0A0DlLk.js → index-hLCqiyjx.js} +1 -1
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/{domEditingLayers-2CKWGEHS.js → domEditingLayers-AT7G6F4L.js} +2 -1
- package/dist/domEditingLayers-AT7G6F4L.js.map +1 -0
- package/dist/index.html +1 -1
- package/dist/index.js +171 -53
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.js +2 -0
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +9 -8
- package/src/components/StudioRightPanel.tsx +2 -2
- package/src/components/editor/keyframeRetime.test.ts +56 -17
- package/src/components/editor/keyframeRetime.ts +48 -4
- package/src/components/editor/manualEditingAvailability.ts +7 -0
- package/src/components/editor/manualEditsTypes.ts +1 -1
- package/src/components/nle/useTimelineEditCallbacks.test.ts +85 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +52 -12
- package/src/components/panels/VariablesPanel.tsx +3 -1
- package/src/contexts/FileManagerContext.tsx +1 -0
- package/src/hooks/timelineEditingHelpers.test.ts +26 -0
- package/src/hooks/timelineEditingHelpers.ts +10 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +1 -0
- package/src/hooks/useGsapKeyframeOps.ts +2 -0
- package/src/hooks/useGsapPropertyDebounce.ts +1 -0
- package/src/hooks/useRazorSplit.ts +1 -0
- package/src/hooks/useTimelineEditing.test.tsx +1 -0
- package/src/hooks/useTimelineGroupEditing.ts +2 -0
- package/src/player/index.ts +0 -2
- package/src/utils/sdkCutover.familyGate.test.ts +47 -0
- package/src/utils/sdkCutover.test.ts +1 -0
- package/src/utils/sdkCutover.ts +95 -38
- package/src/utils/sdkCutoverPolicy.report.ts +3 -0
- package/src/utils/sdkCutoverPolicy.test.ts +38 -0
- package/src/utils/sdkCutoverPolicy.ts +98 -0
- /package/dist/{domEditingLayers-2CKWGEHS.js.map → chunk-PZ5AY32C.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/styles/tailwind-preset.shared.js","../../src/styles/tailwind-preset.ts"],"sourcesContent":["const studioPreset = {\n theme: {\n extend: {\n colors: {\n studio: {\n bg: \"#0a0a0a\",\n surface: \"#141414\",\n border: \"#262626\",\n text: \"#e5e5e5\",\n muted: \"#737373\",\n accent: \"#3CE6AC\",\n },\n panel: {\n bg: \"#0C0C0E\",\n // Open inspector-section body — slightly lighter than headers (bg)\n // so the recessed scrollable region reads distinct.\n \"bg-inset\": \"#121214\",\n input: \"#161618\",\n surface: \"#18181B\",\n hover: \"#27272A\",\n border: \"#1E1E1E\",\n \"border-input\": \"#27272A\",\n hairline: \"#1A1A1C\",\n \"text-0\": \"#FAFAFA\",\n \"text-1\": \"#E4E4E7\",\n \"text-2\": \"#A1A1AA\",\n \"text-3\": \"#71717A\",\n \"text-4\": \"#52525B\",\n \"text-5\": \"#3F3F46\",\n accent: \"#3CE6AC\",\n danger: \"#EF4444\",\n media: \"#00E3FF\",\n container: \"#F5A623\",\n },\n },\n },\n },\n plugins: [],\n};\n\nexport default studioPreset;\n","import studioPreset from \"./tailwind-preset.shared.js\";\n\nexport default studioPreset;\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/styles/tailwind-preset.shared.js","../../src/styles/tailwind-preset.ts"],"sourcesContent":["const studioPreset = {\n theme: {\n extend: {\n colors: {\n studio: {\n bg: \"#0a0a0a\",\n surface: \"#141414\",\n border: \"#262626\",\n text: \"#e5e5e5\",\n muted: \"#737373\",\n accent: \"#3CE6AC\",\n },\n panel: {\n bg: \"#0C0C0E\",\n // Open inspector-section body — slightly lighter than headers (bg)\n // so the recessed scrollable region reads distinct.\n \"bg-inset\": \"#121214\",\n input: \"#161618\",\n surface: \"#18181B\",\n hover: \"#27272A\",\n border: \"#1E1E1E\",\n \"border-input\": \"#27272A\",\n hairline: \"#1A1A1C\",\n \"text-0\": \"#FAFAFA\",\n \"text-1\": \"#E4E4E7\",\n \"text-2\": \"#A1A1AA\",\n \"text-3\": \"#71717A\",\n \"text-4\": \"#52525B\",\n \"text-5\": \"#3F3F46\",\n accent: \"#3CE6AC\",\n danger: \"#EF4444\",\n media: \"#00E3FF\",\n container: \"#F5A623\",\n },\n },\n },\n },\n plugins: [],\n};\n\nexport default studioPreset;\n","import studioPreset from \"./tailwind-preset.shared.js\";\n\nexport default studioPreset;\n"],"mappings":";;;AAAA,IAAM,eAAe;AAAA,EACnB,OAAO;AAAA,IACL,QAAQ;AAAA,MACN,QAAQ;AAAA,QACN,QAAQ;AAAA,UACN,IAAI;AAAA,UACJ,SAAS;AAAA,UACT,QAAQ;AAAA,UACR,MAAM;AAAA,UACN,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,QACA,OAAO;AAAA,UACL,IAAI;AAAA;AAAA;AAAA,UAGJ,YAAY;AAAA,UACZ,OAAO;AAAA,UACP,SAAS;AAAA,UACT,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,gBAAgB;AAAA,UAChB,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,OAAO;AAAA,UACP,WAAW;AAAA,QACb;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS,CAAC;AACZ;AAEA,IAAO,iCAAQ;;;ACtCf,IAAO,0BAAQ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.65",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"gsap": "^3.13.0",
|
|
47
47
|
"marked": "^14.1.4",
|
|
48
48
|
"mediabunny": "^1.45.3",
|
|
49
|
-
"@hyperframes/
|
|
50
|
-
"@hyperframes/
|
|
51
|
-
"@hyperframes/
|
|
52
|
-
"@hyperframes/studio-server": "0.7.
|
|
53
|
-
"@hyperframes/sdk": "0.7.
|
|
49
|
+
"@hyperframes/parsers": "0.7.65",
|
|
50
|
+
"@hyperframes/player": "0.7.65",
|
|
51
|
+
"@hyperframes/core": "0.7.65",
|
|
52
|
+
"@hyperframes/studio-server": "0.7.65",
|
|
53
|
+
"@hyperframes/sdk": "0.7.65"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/react": "19",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"vite": "^6.4.2",
|
|
66
66
|
"vitest": "^3.2.4",
|
|
67
67
|
"zustand": "^5.0.0",
|
|
68
|
-
"@hyperframes/producer": "0.7.
|
|
68
|
+
"@hyperframes/producer": "0.7.65"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"react": "19",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"build": "vite build && tsup",
|
|
78
78
|
"typecheck": "tsc --noEmit",
|
|
79
79
|
"test": "vitest run",
|
|
80
|
-
"test:watch": "vitest"
|
|
80
|
+
"test:watch": "vitest",
|
|
81
|
+
"report:sdk-cutover": "bun src/utils/sdkCutoverPolicy.report.ts"
|
|
81
82
|
}
|
|
82
83
|
}
|
|
@@ -5,7 +5,7 @@ import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPane
|
|
|
5
5
|
import { BlockParamsPanel } from "./editor/BlockParamsPanel";
|
|
6
6
|
import { RenderQueue } from "./renders/RenderQueue";
|
|
7
7
|
import { SlideshowPanel } from "./panels/SlideshowPanel";
|
|
8
|
-
import { VariablesPanel } from "./panels/VariablesPanel";
|
|
8
|
+
import { VariablesPanel, type StudioEditPersistenceProps } from "./panels/VariablesPanel";
|
|
9
9
|
import { PanelTabButton } from "./PanelTabButton";
|
|
10
10
|
import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
|
|
11
11
|
import type { RenderJob } from "./renders/useRenderQueue";
|
|
@@ -35,7 +35,7 @@ import type { BackgroundRemovalProgress } from "./editor/propertyPanelTypes";
|
|
|
35
35
|
import { timelineKeysForSelections, type ToggleHiddenHandler } from "../utils/studioHelpers";
|
|
36
36
|
import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize";
|
|
37
37
|
|
|
38
|
-
export interface StudioRightPanelProps {
|
|
38
|
+
export interface StudioRightPanelProps extends StudioEditPersistenceProps {
|
|
39
39
|
designPanelActive: boolean;
|
|
40
40
|
activeBlockParams?: {
|
|
41
41
|
blockName: string;
|
|
@@ -9,6 +9,7 @@ const KEYFRAMES: RetimeKeyframe[] = [
|
|
|
9
9
|
{ percentage: 100, properties: { x: 100 }, ease: "power2.in" },
|
|
10
10
|
];
|
|
11
11
|
const WINDOW = { tweenStart: 2, tweenDuration: 4 };
|
|
12
|
+
const LEFT_BOUNDARY_DROP = { ...WINDOW, dropAbsTime: 0.5 };
|
|
12
13
|
|
|
13
14
|
describe("resolveKeyframeRetime — move (within the tween window)", () => {
|
|
14
15
|
it("re-keys an interior keyframe to the tween-% of the drop", () => {
|
|
@@ -32,15 +33,53 @@ describe("resolveKeyframeRetime — move (within the tween window)", () => {
|
|
|
32
33
|
expect(r.kind).toBe("noop");
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
it("
|
|
36
|
+
it("shortens a flat tween when its synthesized end diamond moves left", () => {
|
|
36
37
|
const r = resolveKeyframeRetime({
|
|
37
38
|
...WINDOW,
|
|
38
39
|
keyframes: [],
|
|
39
40
|
draggedTweenPct: 100,
|
|
40
|
-
dropAbsTime: 5,
|
|
41
|
+
dropAbsTime: 5,
|
|
41
42
|
});
|
|
42
|
-
expect(r.kind).toBe("
|
|
43
|
-
expect(r.
|
|
43
|
+
expect(r.kind).toBe("resize");
|
|
44
|
+
expect(r.position).toBe(2);
|
|
45
|
+
expect(r.duration).toBe(3);
|
|
46
|
+
expect(r.pctRemap).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("moves a flat tween's start while preserving its absolute end", () => {
|
|
50
|
+
const r = resolveKeyframeRetime({
|
|
51
|
+
...WINDOW,
|
|
52
|
+
keyframes: [],
|
|
53
|
+
draggedTweenPct: 0,
|
|
54
|
+
dropAbsTime: 3,
|
|
55
|
+
});
|
|
56
|
+
expect(r.kind).toBe("resize");
|
|
57
|
+
expect(r.position).toBe(3);
|
|
58
|
+
expect(r.duration).toBe(3);
|
|
59
|
+
expect(r.pctRemap).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("no-ops an unexpected interior percentage on a flat tween", () => {
|
|
63
|
+
expect(
|
|
64
|
+
resolveKeyframeRetime({
|
|
65
|
+
...WINDOW,
|
|
66
|
+
keyframes: [],
|
|
67
|
+
draggedTweenPct: 50,
|
|
68
|
+
dropAbsTime: 5,
|
|
69
|
+
}).kind,
|
|
70
|
+
).toBe("noop");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("no-ops instead of rounding a sub-10ms flat tween to zero", () => {
|
|
74
|
+
expect(
|
|
75
|
+
resolveKeyframeRetime({
|
|
76
|
+
tweenStart: 2,
|
|
77
|
+
tweenDuration: 4,
|
|
78
|
+
keyframes: [],
|
|
79
|
+
draggedTweenPct: 100,
|
|
80
|
+
dropAbsTime: 2.0004,
|
|
81
|
+
}).kind,
|
|
82
|
+
).toBe("noop");
|
|
44
83
|
});
|
|
45
84
|
});
|
|
46
85
|
|
|
@@ -67,10 +106,9 @@ describe("resolveKeyframeRetime — resize (past the tween boundary)", () => {
|
|
|
67
106
|
|
|
68
107
|
it("extends the FIRST keyframe before the start, shifting position earlier", () => {
|
|
69
108
|
const r = resolveKeyframeRetime({
|
|
70
|
-
...
|
|
109
|
+
...LEFT_BOUNDARY_DROP,
|
|
71
110
|
keyframes: KEYFRAMES,
|
|
72
111
|
draggedTweenPct: 0,
|
|
73
|
-
dropAbsTime: 0.5, // before start (2) → move position back + grow duration
|
|
74
112
|
});
|
|
75
113
|
expect(r.kind).toBe("resize");
|
|
76
114
|
expect(r.position).toBeCloseTo(0.5, 5);
|
|
@@ -102,10 +140,9 @@ describe("resolveKeyframeRetime — single keyframe (both first and last)", () =
|
|
|
102
140
|
|
|
103
141
|
it("resizes left before the start", () => {
|
|
104
142
|
const r = resolveKeyframeRetime({
|
|
105
|
-
...
|
|
143
|
+
...LEFT_BOUNDARY_DROP,
|
|
106
144
|
keyframes: lone,
|
|
107
145
|
draggedTweenPct: 100,
|
|
108
|
-
dropAbsTime: 0.5,
|
|
109
146
|
});
|
|
110
147
|
expect(r.kind).toBe("resize");
|
|
111
148
|
expect(r.position).toBeCloseTo(0.5, 5);
|
|
@@ -127,14 +164,16 @@ describe("resolveKeyframeRetime — guards", () => {
|
|
|
127
164
|
).toBe("noop");
|
|
128
165
|
});
|
|
129
166
|
|
|
130
|
-
it("
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
).toBe(
|
|
167
|
+
it("extends a flat tween when its synthesized end diamond moves right", () => {
|
|
168
|
+
const r = resolveKeyframeRetime({
|
|
169
|
+
...WINDOW,
|
|
170
|
+
keyframes: [],
|
|
171
|
+
draggedTweenPct: 100,
|
|
172
|
+
dropAbsTime: 8,
|
|
173
|
+
});
|
|
174
|
+
expect(r.kind).toBe("resize");
|
|
175
|
+
expect(r.position).toBe(2);
|
|
176
|
+
expect(r.duration).toBe(6);
|
|
177
|
+
expect(r.pctRemap).toEqual([]);
|
|
139
178
|
});
|
|
140
179
|
});
|
|
@@ -51,11 +51,40 @@ export interface KeyframeRetimeResult {
|
|
|
51
51
|
const NOOP_EPSILON_PCT = 0.1;
|
|
52
52
|
/** Slack (seconds) for the within-tween boundary test. */
|
|
53
53
|
const EPSILON_TIME = 1e-4;
|
|
54
|
+
/** Smallest authored tween window; avoids sub-millisecond/round-to-zero durations. */
|
|
55
|
+
const MIN_TWEEN_DURATION = 0.01;
|
|
54
56
|
|
|
55
57
|
const round3 = (n: number) => Math.round(n * 1000) / 1000;
|
|
56
58
|
const round1 = (n: number) => Math.round(n * 10) / 10; // 0.1% precision
|
|
57
59
|
const clamp = (n: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, n));
|
|
58
60
|
|
|
61
|
+
/** Resolve timing for a flat tween's synthesized start/end diamond. */
|
|
62
|
+
function resolveFlatTweenBoundaryRetime(opts: {
|
|
63
|
+
keyframeCount: number;
|
|
64
|
+
draggedTweenPct: number;
|
|
65
|
+
tweenStart: number;
|
|
66
|
+
tweenEnd: number;
|
|
67
|
+
dropAbsTime: number;
|
|
68
|
+
}): KeyframeRetimeResult | null {
|
|
69
|
+
const { keyframeCount, draggedTweenPct, tweenStart, tweenEnd, dropAbsTime } = opts;
|
|
70
|
+
if (keyframeCount > 0) return null;
|
|
71
|
+
// Flat tweens have only synthesized boundary diamonds. Never delegate an
|
|
72
|
+
// unexpected interior percentage to the authored-keyframe move path.
|
|
73
|
+
if (draggedTweenPct !== 0 && draggedTweenPct !== 100) return { kind: "noop" };
|
|
74
|
+
const draggedTime = draggedTweenPct === 0 ? tweenStart : tweenEnd;
|
|
75
|
+
if (Math.abs(dropAbsTime - draggedTime) <= EPSILON_TIME) return { kind: "noop" };
|
|
76
|
+
const newStart = draggedTweenPct === 0 ? dropAbsTime : tweenStart;
|
|
77
|
+
const newEnd = draggedTweenPct === 100 ? dropAbsTime : tweenEnd;
|
|
78
|
+
const newDuration = newEnd - newStart;
|
|
79
|
+
if (newDuration < MIN_TWEEN_DURATION) return { kind: "noop" };
|
|
80
|
+
return {
|
|
81
|
+
kind: "resize",
|
|
82
|
+
position: round3(newStart),
|
|
83
|
+
duration: round3(newDuration),
|
|
84
|
+
pctRemap: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
59
88
|
/**
|
|
60
89
|
* Decide move vs resize for a dragged keyframe and, for resize, return the new
|
|
61
90
|
* tween window + remapped keyframes.
|
|
@@ -76,20 +105,35 @@ export function resolveKeyframeRetime(opts: {
|
|
|
76
105
|
if (tweenDuration <= 0) return { kind: "noop" };
|
|
77
106
|
const tweenEnd = tweenStart + tweenDuration;
|
|
78
107
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
108
|
+
// A flat tween's synthesized diamonds are its real start/end boundaries —
|
|
109
|
+
// there is no authored keyframe node to re-key. Moving either boundary must
|
|
110
|
+
// therefore resize the tween window while keeping the opposite endpoint at
|
|
111
|
+
// its absolute time. Returning `resize` with an empty remap lets the caller
|
|
112
|
+
// update the flat tween's position/duration instead of sending a keyframe
|
|
113
|
+
// mutation that would silently no-op.
|
|
114
|
+
const flatBoundary = resolveFlatTweenBoundaryRetime({
|
|
115
|
+
keyframeCount: keyframes.length,
|
|
116
|
+
draggedTweenPct,
|
|
117
|
+
tweenStart,
|
|
118
|
+
tweenEnd,
|
|
119
|
+
dropAbsTime,
|
|
120
|
+
});
|
|
121
|
+
if (flatBoundary) return flatBoundary;
|
|
122
|
+
|
|
123
|
+
// Within the tween window → plain move (re-key the tween-%).
|
|
81
124
|
if (dropAbsTime >= tweenStart - EPSILON_TIME && dropAbsTime <= tweenEnd + EPSILON_TIME) {
|
|
82
125
|
const toTweenPct = clamp(((dropAbsTime - tweenStart) / tweenDuration) * 100, 0, 100);
|
|
83
126
|
if (Math.abs(toTweenPct - draggedTweenPct) < NOOP_EPSILON_PCT) return { kind: "noop" };
|
|
84
127
|
return { kind: "move", toTweenPct };
|
|
85
128
|
}
|
|
86
129
|
|
|
87
|
-
// Boundary resize needs the real keyframes to remap
|
|
130
|
+
// Boundary resize needs the real keyframes to remap. Flat start/end boundaries
|
|
131
|
+
// were handled above.
|
|
88
132
|
if (keyframes.length === 0) return { kind: "noop" };
|
|
89
133
|
|
|
90
134
|
const newStart = Math.min(dropAbsTime, tweenStart);
|
|
91
135
|
const newEnd = Math.max(dropAbsTime, tweenEnd);
|
|
92
|
-
const newDuration = Math.max(
|
|
136
|
+
const newDuration = Math.max(MIN_TWEEN_DURATION, newEnd - newStart);
|
|
93
137
|
|
|
94
138
|
// The dragged keyframe is the one whose tween-% is closest to draggedTweenPct.
|
|
95
139
|
let draggedIdx = 0;
|
|
@@ -87,6 +87,12 @@ export const STUDIO_SDK_CUTOVER_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
87
87
|
false,
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
+
/** Explicit per-operation-family canary selection; the master flag alone enables nothing. */
|
|
91
|
+
export const STUDIO_SDK_CUTOVER_FAMILIES = resolveEnabledSdkFamilies(
|
|
92
|
+
env,
|
|
93
|
+
STUDIO_SDK_CUTOVER_ENABLED,
|
|
94
|
+
);
|
|
95
|
+
|
|
90
96
|
// Resolver-parity tripwire (telemetry-only, decoupled from cutover).
|
|
91
97
|
// Runs the SDK resolver alongside any edit and emits sdk_resolver_shadow on
|
|
92
98
|
// divergence. Default true; disable via VITE_STUDIO_SDK_RESOLVER_SHADOW_ENABLED=false.
|
|
@@ -109,3 +115,4 @@ export const STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
109
115
|
);
|
|
110
116
|
|
|
111
117
|
export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
|
|
118
|
+
import { resolveEnabledSdkFamilies } from "../../utils/sdkCutoverPolicy";
|
|
@@ -5,7 +5,7 @@ export {
|
|
|
5
5
|
STUDIO_WIDTH_PROP,
|
|
6
6
|
STUDIO_HEIGHT_PROP,
|
|
7
7
|
STUDIO_MANUAL_EDIT_GESTURE_ATTR,
|
|
8
|
-
} from "@hyperframes/
|
|
8
|
+
} from "@hyperframes/core/editing/draft-markers";
|
|
9
9
|
export const STUDIO_ROTATION_PROP = "--hf-studio-rotation";
|
|
10
10
|
|
|
11
11
|
/* ── Internal DOM attribute names ─────────────────────────────────── */
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveTimelineKeyframeTarget } from "./useTimelineEditCallbacks";
|
|
3
|
+
|
|
4
|
+
const FLAT_ANIMATION = {
|
|
5
|
+
id: "#title-fromTo-2900",
|
|
6
|
+
propertyGroup: null,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
describe("resolveTimelineKeyframeTarget", () => {
|
|
10
|
+
it("resolves a synthesized diamond when one mixed flat tween is selected", () => {
|
|
11
|
+
expect(
|
|
12
|
+
resolveTimelineKeyframeTarget(
|
|
13
|
+
8.75,
|
|
14
|
+
[{ percentage: 8.75, tweenPercentage: 100 }],
|
|
15
|
+
[FLAT_ANIMATION],
|
|
16
|
+
),
|
|
17
|
+
).toEqual({ animId: "#title-fromTo-2900", tweenPct: 100 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("keeps multiple ungrouped flat tweens unresolved instead of guessing", () => {
|
|
21
|
+
expect(
|
|
22
|
+
resolveTimelineKeyframeTarget(
|
|
23
|
+
8.75,
|
|
24
|
+
[{ percentage: 8.75, tweenPercentage: 100 }],
|
|
25
|
+
[FLAT_ANIMATION, { id: "#title-to-2900", propertyGroup: null }],
|
|
26
|
+
),
|
|
27
|
+
).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("keeps multiple flat tweens in the same property group unresolved", () => {
|
|
31
|
+
expect(
|
|
32
|
+
resolveTimelineKeyframeTarget(
|
|
33
|
+
50,
|
|
34
|
+
[{ percentage: 50, tweenPercentage: 100, propertyGroup: "position" }],
|
|
35
|
+
[
|
|
36
|
+
{ id: "position-a", propertyGroup: "position" },
|
|
37
|
+
{ id: "position-b", propertyGroup: "position" },
|
|
38
|
+
],
|
|
39
|
+
),
|
|
40
|
+
).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("keeps a keyframed and flat tween in the same property group unresolved", () => {
|
|
44
|
+
expect(
|
|
45
|
+
resolveTimelineKeyframeTarget(
|
|
46
|
+
50,
|
|
47
|
+
[{ percentage: 50, tweenPercentage: 25, propertyGroup: "position" }],
|
|
48
|
+
[
|
|
49
|
+
{ id: "position-keyframed", propertyGroup: "position", keyframes: {} },
|
|
50
|
+
{ id: "position-flat", propertyGroup: "position" },
|
|
51
|
+
],
|
|
52
|
+
),
|
|
53
|
+
).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("keeps multiple ungrouped keyframed tweens unresolved", () => {
|
|
57
|
+
expect(
|
|
58
|
+
resolveTimelineKeyframeTarget(
|
|
59
|
+
50,
|
|
60
|
+
[{ percentage: 50, tweenPercentage: 25 }],
|
|
61
|
+
[
|
|
62
|
+
{ id: "ungrouped-a", keyframes: {} },
|
|
63
|
+
{ id: "ungrouped-b", keyframes: {} },
|
|
64
|
+
],
|
|
65
|
+
),
|
|
66
|
+
).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("does not infer an animation when the rendered diamond misses the cache", () => {
|
|
70
|
+
expect(resolveTimelineKeyframeTarget(60, [], [FLAT_ANIMATION])).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("resolves the sole candidate in an explicit property group", () => {
|
|
74
|
+
expect(
|
|
75
|
+
resolveTimelineKeyframeTarget(
|
|
76
|
+
50,
|
|
77
|
+
[{ percentage: 50, tweenPercentage: 25, propertyGroup: "position" }],
|
|
78
|
+
[
|
|
79
|
+
{ id: "opacity", propertyGroup: "opacity" },
|
|
80
|
+
{ id: "position", propertyGroup: "position" },
|
|
81
|
+
],
|
|
82
|
+
),
|
|
83
|
+
).toEqual({ animId: "position", tweenPct: 25 });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -36,6 +36,41 @@ export interface TimelineEditCallbackDeps {
|
|
|
36
36
|
handleRazorSplitAll: (splitTime: number) => Promise<void> | void;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
interface TimelineKeyframeTargetAnimation {
|
|
40
|
+
id: string;
|
|
41
|
+
propertyGroup?: string | null;
|
|
42
|
+
keyframes?: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TimelineCachedKeyframe {
|
|
46
|
+
percentage: number;
|
|
47
|
+
tweenPercentage?: number;
|
|
48
|
+
propertyGroup?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a rendered timeline diamond back to the animation that authored it.
|
|
53
|
+
* Flat tweens use synthesized diamonds, so a mixed flat tween may have neither
|
|
54
|
+
* a property group nor real keyframes. The cache currently carries a property
|
|
55
|
+
* group, not an animation id, so resolution is safe only when that group has a
|
|
56
|
+
* single candidate. Ambiguous candidates remain unresolved rather than
|
|
57
|
+
* retiming an arbitrary tween.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveTimelineKeyframeTarget(
|
|
60
|
+
pct: number,
|
|
61
|
+
keyframes: ReadonlyArray<TimelineCachedKeyframe>,
|
|
62
|
+
animations: ReadonlyArray<TimelineKeyframeTargetAnimation>,
|
|
63
|
+
): { animId: string; tweenPct: number } | null {
|
|
64
|
+
const kf = keyframes.find((item) => Math.abs(item.percentage - pct) < 0.2);
|
|
65
|
+
if (!kf) return null;
|
|
66
|
+
const group = kf?.propertyGroup;
|
|
67
|
+
const candidates = group
|
|
68
|
+
? animations.filter((animation) => animation.propertyGroup === group)
|
|
69
|
+
: animations.filter((animation) => !animation.propertyGroup);
|
|
70
|
+
const animation = candidates.length === 1 ? candidates[0] : undefined;
|
|
71
|
+
return animation ? { animId: animation.id, tweenPct: kf.tweenPercentage ?? pct } : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
/**
|
|
40
75
|
* Builds the timeline edit callback bag (move/resize/split/razor plus the
|
|
41
76
|
* keyframe-diamond callbacks) provided to `<Timeline>` via TimelineEditProvider.
|
|
@@ -76,12 +111,7 @@ export function useTimelineEditCallbacks({
|
|
|
76
111
|
// fallow-ignore-next-line complexity
|
|
77
112
|
(pct: number): { animId: string; tweenPct: number } | null => {
|
|
78
113
|
const cached = usePlayerStore.getState().keyframeCache.get(domEditSelection?.id ?? "");
|
|
79
|
-
|
|
80
|
-
const group = kf?.propertyGroup;
|
|
81
|
-
const anim =
|
|
82
|
-
(group ? selectedGsapAnimations.find((a) => a.propertyGroup === group) : undefined) ??
|
|
83
|
-
selectedGsapAnimations.find((a) => a.keyframes);
|
|
84
|
-
return anim ? { animId: anim.id, tweenPct: kf?.tweenPercentage ?? pct } : null;
|
|
114
|
+
return resolveTimelineKeyframeTarget(pct, cached?.keyframes ?? [], selectedGsapAnimations);
|
|
85
115
|
},
|
|
86
116
|
[domEditSelection?.id, selectedGsapAnimations],
|
|
87
117
|
);
|
|
@@ -154,12 +184,22 @@ export function useTimelineEditCallbacks({
|
|
|
154
184
|
decision.position != null &&
|
|
155
185
|
decision.duration != null
|
|
156
186
|
) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
if (anim.keyframes) {
|
|
188
|
+
handleGsapResizeKeyframedTween(
|
|
189
|
+
target.animId,
|
|
190
|
+
decision.position,
|
|
191
|
+
decision.duration,
|
|
192
|
+
decision.pctRemap,
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
// resize-keyframed-tween requires an authored `keyframes` AST node
|
|
196
|
+
// and intentionally no-ops for a flat tween. Update its real tween
|
|
197
|
+
// window through the metadata writer (and SDK cutover path) instead.
|
|
198
|
+
handleGsapUpdateMeta(target.animId, {
|
|
199
|
+
position: decision.position,
|
|
200
|
+
duration: decision.duration,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
163
203
|
}
|
|
164
204
|
},
|
|
165
205
|
onChangeKeyframeEase: (_elId: string, _pct: number, ease: string) => {
|
|
@@ -31,7 +31,7 @@ function shellSingleQuote(value: string): string {
|
|
|
31
31
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
interface
|
|
34
|
+
export interface StudioEditPersistenceProps {
|
|
35
35
|
sdkSession: Composition | null;
|
|
36
36
|
publishSdkSession: PublishSdkSession;
|
|
37
37
|
reloadPreview: () => void;
|
|
@@ -43,6 +43,8 @@ interface VariablesPanelProps {
|
|
|
43
43
|
}) => Promise<void>;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
type VariablesPanelProps = StudioEditPersistenceProps;
|
|
47
|
+
|
|
46
48
|
function formatIssue(issue: VariableValidationIssue): string {
|
|
47
49
|
switch (issue.kind) {
|
|
48
50
|
case "undeclared":
|
|
@@ -142,6 +142,32 @@ describe("applyTimelineStackingReorder", () => {
|
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
describe("patchIframeDomTiming", () => {
|
|
145
|
+
it("patches a top-level composition host in its parent source file", () => {
|
|
146
|
+
const iframe = makeIframeWith(`
|
|
147
|
+
<div data-composition-id="root">
|
|
148
|
+
<div
|
|
149
|
+
id="scene-host"
|
|
150
|
+
data-composition-id="scene"
|
|
151
|
+
data-composition-src="compositions/scene.html"
|
|
152
|
+
data-start="2"
|
|
153
|
+
></div>
|
|
154
|
+
</div>
|
|
155
|
+
`);
|
|
156
|
+
const host = iframe.contentDocument?.getElementById("scene-host");
|
|
157
|
+
const target = el({
|
|
158
|
+
id: "scene-host",
|
|
159
|
+
tag: "div",
|
|
160
|
+
kind: "composition",
|
|
161
|
+
domId: "scene-host",
|
|
162
|
+
sourceFile: "index.html",
|
|
163
|
+
compositionSrc: "compositions/scene.html",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
patchIframeDomTiming(iframe, target, [["data-start", "9"]], "index.html");
|
|
167
|
+
|
|
168
|
+
expect(host?.getAttribute("data-start")).toBe("9");
|
|
169
|
+
});
|
|
170
|
+
|
|
145
171
|
it("resolves selectorIndex within the element's source file", () => {
|
|
146
172
|
const iframe = makeIframeWith(`
|
|
147
173
|
<div data-composition-id="root">
|
|
@@ -13,7 +13,10 @@ import { saveProjectFilesWithHistory, type RecordEditInput } from "../utils/stud
|
|
|
13
13
|
import type { TimelineZIndexReorderCommit } from "./useTimelineEditingTypes";
|
|
14
14
|
import { setCompositionDurationToContent } from "../utils/timelineAssetDrop";
|
|
15
15
|
import { readFileContent } from "./timelineTimingSync";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
findElementForSelection,
|
|
18
|
+
findElementForTimelineElement,
|
|
19
|
+
} from "../components/editor/domEditingElement";
|
|
17
20
|
export { deleteSelectedKeyframes } from "./deleteSelectedKeyframes";
|
|
18
21
|
export { readFileContent };
|
|
19
22
|
function isHTMLElement(element: Element | null): element is HTMLElement {
|
|
@@ -137,6 +140,12 @@ export function findTimelineElementInIframe(
|
|
|
137
140
|
try {
|
|
138
141
|
const doc = iframe?.contentDocument;
|
|
139
142
|
if (!doc) return null;
|
|
143
|
+
if (element.kind === "composition" && element.compositionSrc) {
|
|
144
|
+
return findElementForTimelineElement(doc, element, {
|
|
145
|
+
activeCompositionPath,
|
|
146
|
+
isMasterView: true,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
140
149
|
return findElementForSelection(
|
|
141
150
|
doc,
|
|
142
151
|
{
|
|
@@ -25,6 +25,7 @@ function renderKeyframeOps(over: {
|
|
|
25
25
|
}) {
|
|
26
26
|
const captured: { api: HookApi | null } = { api: null };
|
|
27
27
|
function Probe() {
|
|
28
|
+
// fallow-ignore-next-line code-duplication
|
|
28
29
|
captured.api = useGsapKeyframeOps({
|
|
29
30
|
activeCompPath: "index.html",
|
|
30
31
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test doubles
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// fallow-ignore-file code-duplication
|
|
2
|
+
// Add/remove operation-family transaction shapes stay parallel until SDK graduation.
|
|
1
3
|
import { useCallback } from "react";
|
|
2
4
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
5
|
import type { Composition } from "@hyperframes/sdk";
|
|
@@ -84,6 +84,7 @@ export function useGsapPropertyDebounce(
|
|
|
84
84
|
const mutation = { type: "update-property", animationId, property, value };
|
|
85
85
|
const label = `Edit GSAP ${property}`;
|
|
86
86
|
try {
|
|
87
|
+
// fallow-ignore-next-line code-duplication
|
|
87
88
|
const { sdkSession, sdkDeps, activeCompPath } = sdkRef.current ?? {};
|
|
88
89
|
if (sdkSession && sdkDeps) {
|
|
89
90
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
@@ -9,6 +9,7 @@ import type { RecordEditInput } from "./timelineEditingHelpers";
|
|
|
9
9
|
|
|
10
10
|
interface UseRazorSplitOptions {
|
|
11
11
|
projectId: string | null;
|
|
12
|
+
// fallow-ignore-next-line code-duplication
|
|
12
13
|
activeCompPath: string | null;
|
|
13
14
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
14
15
|
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// fallow-ignore-file code-duplication
|
|
2
|
+
// Move/resize operation families remain parallel until SDK graduation.
|
|
1
3
|
import { useCallback, type MutableRefObject, type RefObject } from "react";
|
|
2
4
|
import type { Composition } from "@hyperframes/sdk";
|
|
3
5
|
import type { TimelineElement } from "../player";
|
package/src/player/index.ts
CHANGED
|
@@ -11,8 +11,6 @@ export { resolveIframe } from "./lib/timelineDOM";
|
|
|
11
11
|
|
|
12
12
|
// Store
|
|
13
13
|
export { usePlayerStore, liveTime } from "./store/playerStore";
|
|
14
|
-
// Public library surface; external consumers are invisible to the workspace analyzer.
|
|
15
|
-
// fallow-ignore-next-line unused-exports
|
|
16
14
|
export type { SelectElementOptions, TimelineElement, ZoomMode } from "./store/playerStore";
|
|
17
15
|
|
|
18
16
|
// Utils
|