@hyperframes/studio 0.5.4 → 0.6.0-alpha.1

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.
Files changed (74) hide show
  1. package/dist/assets/hyperframes-player-Cd8vYWxP.js +198 -0
  2. package/dist/assets/index-D04_ZoMm.js +107 -0
  3. package/dist/assets/index-UWFaHilT.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/App.tsx +2621 -170
  7. package/src/components/LintModal.tsx +3 -4
  8. package/src/components/editor/DomEditOverlay.test.ts +241 -0
  9. package/src/components/editor/DomEditOverlay.tsx +1300 -0
  10. package/src/components/editor/MotionPanel.tsx +651 -0
  11. package/src/components/editor/PropertyPanel.test.ts +67 -0
  12. package/src/components/editor/PropertyPanel.tsx +2891 -207
  13. package/src/components/editor/TimelineLayerPanel.test.ts +42 -0
  14. package/src/components/editor/TimelineLayerPanel.tsx +113 -0
  15. package/src/components/editor/colorValue.test.ts +82 -0
  16. package/src/components/editor/colorValue.ts +175 -0
  17. package/src/components/editor/domEditing.test.ts +872 -0
  18. package/src/components/editor/domEditing.ts +993 -0
  19. package/src/components/editor/floatingPanel.test.ts +34 -0
  20. package/src/components/editor/floatingPanel.ts +54 -0
  21. package/src/components/editor/fontAssets.ts +32 -0
  22. package/src/components/editor/fontCatalog.ts +126 -0
  23. package/src/components/editor/gradientValue.test.ts +89 -0
  24. package/src/components/editor/gradientValue.ts +445 -0
  25. package/src/components/editor/manualEditingAvailability.test.ts +120 -0
  26. package/src/components/editor/manualEditingAvailability.ts +60 -0
  27. package/src/components/editor/manualEdits.test.ts +945 -0
  28. package/src/components/editor/manualEdits.ts +1397 -0
  29. package/src/components/editor/manualOffsetDrag.test.ts +140 -0
  30. package/src/components/editor/manualOffsetDrag.ts +307 -0
  31. package/src/components/editor/studioMotion.test.ts +355 -0
  32. package/src/components/editor/studioMotion.ts +632 -0
  33. package/src/components/nle/NLELayout.tsx +27 -4
  34. package/src/components/nle/NLEPreview.tsx +50 -5
  35. package/src/components/renders/RenderQueue.tsx +13 -62
  36. package/src/components/renders/useRenderQueue.ts +6 -30
  37. package/src/components/sidebar/AssetsTab.tsx +3 -4
  38. package/src/components/sidebar/CompositionsTab.test.ts +16 -1
  39. package/src/components/sidebar/CompositionsTab.tsx +117 -45
  40. package/src/components/sidebar/LeftSidebar.tsx +140 -125
  41. package/src/hooks/usePersistentEditHistory.test.ts +256 -0
  42. package/src/hooks/usePersistentEditHistory.ts +337 -0
  43. package/src/icons/SystemIcons.tsx +2 -0
  44. package/src/player/components/CompositionThumbnail.test.ts +19 -0
  45. package/src/player/components/CompositionThumbnail.tsx +50 -13
  46. package/src/player/components/EditModal.tsx +5 -20
  47. package/src/player/components/Player.tsx +18 -2
  48. package/src/player/components/Timeline.test.ts +20 -0
  49. package/src/player/components/Timeline.tsx +103 -21
  50. package/src/player/components/TimelineClip.test.ts +92 -0
  51. package/src/player/components/TimelineClip.tsx +241 -7
  52. package/src/player/components/timelineEditing.test.ts +16 -3
  53. package/src/player/components/timelineEditing.ts +10 -3
  54. package/src/player/hooks/useTimelinePlayer.test.ts +148 -19
  55. package/src/player/hooks/useTimelinePlayer.ts +287 -16
  56. package/src/player/store/playerStore.ts +2 -0
  57. package/src/utils/clipboard.test.ts +89 -0
  58. package/src/utils/clipboard.ts +57 -0
  59. package/src/utils/editHistory.test.ts +244 -0
  60. package/src/utils/editHistory.ts +218 -0
  61. package/src/utils/editHistoryStorage.test.ts +37 -0
  62. package/src/utils/editHistoryStorage.ts +99 -0
  63. package/src/utils/mediaTypes.ts +1 -1
  64. package/src/utils/sourcePatcher.test.ts +128 -1
  65. package/src/utils/sourcePatcher.ts +130 -18
  66. package/src/utils/studioFileHistory.test.ts +156 -0
  67. package/src/utils/studioFileHistory.ts +61 -0
  68. package/src/utils/timelineAssetDrop.test.ts +31 -11
  69. package/src/utils/timelineAssetDrop.ts +22 -2
  70. package/src/utils/timelineInspector.test.ts +79 -0
  71. package/src/utils/timelineInspector.ts +116 -0
  72. package/dist/assets/hyperframes-player-CEnWY28J.js +0 -417
  73. package/dist/assets/index-04Mp2wOn.css +0 -1
  74. package/dist/assets/index-960mgQMI.js +0 -93
@@ -0,0 +1,140 @@
1
+ import { Window } from "happy-dom";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ applyManualOffsetDragMatrix,
5
+ invertManualOffsetDragMatrix,
6
+ measureManualOffsetDragScreenToOffsetMatrix,
7
+ resolveManualOffsetForPointerDelta,
8
+ type ManualOffsetDragMatrix,
9
+ } from "./manualOffsetDrag";
10
+ import { STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP } from "./manualEdits";
11
+
12
+ function expectMatrixClose(actual: ManualOffsetDragMatrix, expected: ManualOffsetDragMatrix): void {
13
+ expect(actual.a).toBeCloseTo(expected.a, 6);
14
+ expect(actual.b).toBeCloseTo(expected.b, 6);
15
+ expect(actual.c).toBeCloseTo(expected.c, 6);
16
+ expect(actual.d).toBeCloseTo(expected.d, 6);
17
+ }
18
+
19
+ describe("manual offset drag matrix helpers", () => {
20
+ it("inverts identity movement", () => {
21
+ const inverse = invertManualOffsetDragMatrix({ a: 1, b: 0, c: 0, d: 1 });
22
+ if (!inverse) throw new Error("identity matrix should be invertible");
23
+
24
+ expectMatrixClose(inverse, { a: 1, b: 0, c: 0, d: 1 });
25
+ });
26
+
27
+ it("maps screen movement through a rotated coordinate system", () => {
28
+ const screenToOffset = invertManualOffsetDragMatrix({ a: 0, b: 1, c: -1, d: 0 });
29
+ if (!screenToOffset) throw new Error("rotation matrix should be invertible");
30
+
31
+ const offsetDelta = applyManualOffsetDragMatrix(screenToOffset, { x: 0, y: 10 });
32
+
33
+ expect(offsetDelta.x).toBeCloseTo(10, 6);
34
+ expect(offsetDelta.y).toBeCloseTo(0, 6);
35
+ });
36
+
37
+ it("rejects singular movement matrices", () => {
38
+ expect(invertManualOffsetDragMatrix({ a: 1, b: 1, c: 2, d: 2 })).toBeNull();
39
+ });
40
+
41
+ it("resolves final offsets from the measured inverse matrix", () => {
42
+ const offsetToScreen = { a: 2, b: 3, c: -1, d: 4 };
43
+ const screenToOffset = invertManualOffsetDragMatrix(offsetToScreen);
44
+ if (!screenToOffset) throw new Error("fixture matrix should be invertible");
45
+
46
+ const nextOffset = resolveManualOffsetForPointerDelta({
47
+ initialOffset: { x: 5, y: -2 },
48
+ screenToOffset,
49
+ dx: 7,
50
+ dy: 11,
51
+ });
52
+ const screenDelta = applyManualOffsetDragMatrix(offsetToScreen, {
53
+ x: nextOffset.x - 5,
54
+ y: nextOffset.y + 2,
55
+ });
56
+
57
+ expect(screenDelta.x).toBeCloseTo(7, 6);
58
+ expect(screenDelta.y).toBeCloseTo(11, 6);
59
+ });
60
+ });
61
+
62
+ describe("measureManualOffsetDragScreenToOffsetMatrix", () => {
63
+ it("measures the element center response and restores probe styles", () => {
64
+ const window = new Window();
65
+ const element = window.document.createElement("div");
66
+ window.document.body.append(element);
67
+
68
+ element.getBoundingClientRect = () => {
69
+ const offsetX = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)) || 0;
70
+ const offsetY = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)) || 0;
71
+ return new window.DOMRect(10 + 2 * offsetX - offsetY, 20 + 3 * offsetX + 4 * offsetY, 12, 8);
72
+ };
73
+
74
+ const measured = measureManualOffsetDragScreenToOffsetMatrix(element, { x: 0, y: 0 });
75
+ if (!measured.ok) throw new Error(measured.reason);
76
+
77
+ const expected = invertManualOffsetDragMatrix({ a: 2, b: 3, c: -1, d: 4 });
78
+ if (!expected) throw new Error("fixture matrix should be invertible");
79
+
80
+ expectMatrixClose(measured.matrix, expected);
81
+ expect(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)).toBe("");
82
+ expect(element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)).toBe("");
83
+ expect(element.style.getPropertyValue("translate")).toBe("");
84
+ });
85
+
86
+ it("measures movement in parent viewport pixels when the element is inside a scaled iframe", () => {
87
+ const window = new Window();
88
+ const iframe = window.document.createElement("iframe");
89
+ window.document.body.append(iframe);
90
+ const iframeWindow = iframe.contentWindow;
91
+ const iframeDocument = iframe.contentDocument;
92
+ if (!iframeWindow || !iframeDocument) throw new Error("iframe fixture failed to initialize");
93
+
94
+ Object.defineProperty(iframeWindow, "frameElement", {
95
+ configurable: true,
96
+ value: iframe,
97
+ });
98
+ Object.defineProperty(iframeWindow, "innerWidth", {
99
+ configurable: true,
100
+ value: 200,
101
+ });
102
+ Object.defineProperty(iframeWindow, "innerHeight", {
103
+ configurable: true,
104
+ value: 100,
105
+ });
106
+ iframe.getBoundingClientRect = () => new window.DOMRect(50, 40, 100, 50);
107
+
108
+ const element = iframeDocument.createElement("div");
109
+ iframeDocument.body.append(element);
110
+ element.getBoundingClientRect = () => {
111
+ const offsetX = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)) || 0;
112
+ const offsetY = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)) || 0;
113
+ return new iframeWindow.DOMRect(20 + offsetX, 30 + offsetY, 40, 20);
114
+ };
115
+
116
+ const measured = measureManualOffsetDragScreenToOffsetMatrix(element, { x: 0, y: 0 });
117
+ if (!measured.ok) throw new Error(measured.reason);
118
+
119
+ expectMatrixClose(measured.matrix, { a: 2, b: -0, c: -0, d: 2 });
120
+
121
+ const nextOffset = resolveManualOffsetForPointerDelta({
122
+ initialOffset: { x: 0, y: 0 },
123
+ screenToOffset: measured.matrix,
124
+ dx: 50,
125
+ dy: 25,
126
+ });
127
+ expect(nextOffset).toEqual({ x: 100, y: 50 });
128
+ });
129
+
130
+ it("rejects elements whose movement response cannot be measured", () => {
131
+ const window = new Window();
132
+ const element = window.document.createElement("div");
133
+ window.document.body.append(element);
134
+ element.getBoundingClientRect = () => new window.DOMRect(10, 20, 12, 8);
135
+
136
+ const measured = measureManualOffsetDragScreenToOffsetMatrix(element, { x: 0, y: 0 });
137
+
138
+ expect(measured.ok).toBe(false);
139
+ });
140
+ });
@@ -0,0 +1,307 @@
1
+ import type { DomEditSelection } from "./domEditing";
2
+ import {
3
+ applyStudioPathOffset,
4
+ applyStudioPathOffsetDraft,
5
+ beginStudioManualEditGesture,
6
+ captureStudioPathOffset,
7
+ endStudioManualEditGesture,
8
+ readStudioPathOffset,
9
+ restoreStudioPathOffset,
10
+ type StudioPathOffsetSnapshot,
11
+ } from "./manualEdits";
12
+
13
+ const DEFAULT_OFFSET_PROBE_PX = 100;
14
+ const MIN_PROBE_VECTOR_LENGTH_PX = 0.01;
15
+ const MIN_MATRIX_DETERMINANT = 0.000001;
16
+
17
+ export interface ManualOffsetDragMatrix {
18
+ a: number;
19
+ b: number;
20
+ c: number;
21
+ d: number;
22
+ }
23
+
24
+ export interface ManualOffsetDragRect {
25
+ left: number;
26
+ top: number;
27
+ width: number;
28
+ height: number;
29
+ editScaleX: number;
30
+ editScaleY: number;
31
+ }
32
+
33
+ export interface ManualOffsetDragMember {
34
+ key: string;
35
+ selection: DomEditSelection;
36
+ element: HTMLElement;
37
+ initialOffset: { x: number; y: number };
38
+ initialPathOffset: StudioPathOffsetSnapshot;
39
+ gestureToken: string;
40
+ screenToOffset: ManualOffsetDragMatrix;
41
+ originRect: ManualOffsetDragRect;
42
+ }
43
+
44
+ export type ManualOffsetDragMemberResult =
45
+ | { ok: true; member: ManualOffsetDragMember }
46
+ | { ok: false; reason: string; selection: DomEditSelection };
47
+
48
+ type Point = { x: number; y: number };
49
+
50
+ function finitePoint(point: Point): boolean {
51
+ return Number.isFinite(point.x) && Number.isFinite(point.y);
52
+ }
53
+
54
+ function vectorLength(point: Point): number {
55
+ return Math.hypot(point.x, point.y);
56
+ }
57
+
58
+ function finiteRect(rect: DOMRect): boolean {
59
+ return (
60
+ Number.isFinite(rect.left) &&
61
+ Number.isFinite(rect.top) &&
62
+ Number.isFinite(rect.width) &&
63
+ Number.isFinite(rect.height)
64
+ );
65
+ }
66
+
67
+ function readViewportSize(win: Window): { width: number; height: number } {
68
+ const docEl = win.document.documentElement;
69
+ const width = win.innerWidth || docEl.clientWidth || 1;
70
+ const height = win.innerHeight || docEl.clientHeight || 1;
71
+ return {
72
+ width: width > 0 ? width : 1,
73
+ height: height > 0 ? height : 1,
74
+ };
75
+ }
76
+
77
+ function getFrameElement(win: Window): HTMLElement | null {
78
+ try {
79
+ const frameElement = win.frameElement;
80
+ if (!frameElement) return null;
81
+ const ownerWin = frameElement.ownerDocument.defaultView;
82
+ const htmlElement = ownerWin?.HTMLElement;
83
+ return htmlElement && frameElement instanceof htmlElement ? frameElement : null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function getRectCenter(element: HTMLElement): Point | null {
90
+ const rect = element.getBoundingClientRect();
91
+ if (!finiteRect(rect) || (rect.width <= 0 && rect.height <= 0)) {
92
+ return null;
93
+ }
94
+
95
+ let point = {
96
+ x: rect.left + rect.width / 2,
97
+ y: rect.top + rect.height / 2,
98
+ };
99
+
100
+ let win: Window | null = element.ownerDocument.defaultView;
101
+ while (win) {
102
+ const frameElement = getFrameElement(win);
103
+ if (!frameElement) break;
104
+
105
+ const frameRect = frameElement.getBoundingClientRect();
106
+ if (!finiteRect(frameRect) || frameRect.width <= 0 || frameRect.height <= 0) return null;
107
+
108
+ const viewport = readViewportSize(win);
109
+ point = {
110
+ x: frameRect.left + point.x * (frameRect.width / viewport.width),
111
+ y: frameRect.top + point.y * (frameRect.height / viewport.height),
112
+ };
113
+ win = frameElement.ownerDocument.defaultView;
114
+ }
115
+
116
+ return point;
117
+ }
118
+
119
+ export function invertManualOffsetDragMatrix(
120
+ matrix: ManualOffsetDragMatrix,
121
+ ): ManualOffsetDragMatrix | null {
122
+ const determinant = matrix.a * matrix.d - matrix.b * matrix.c;
123
+ if (!Number.isFinite(determinant) || Math.abs(determinant) < MIN_MATRIX_DETERMINANT) {
124
+ return null;
125
+ }
126
+
127
+ return {
128
+ a: matrix.d / determinant,
129
+ b: -matrix.b / determinant,
130
+ c: -matrix.c / determinant,
131
+ d: matrix.a / determinant,
132
+ };
133
+ }
134
+
135
+ export function applyManualOffsetDragMatrix(matrix: ManualOffsetDragMatrix, point: Point): Point {
136
+ return {
137
+ x: matrix.a * point.x + matrix.c * point.y,
138
+ y: matrix.b * point.x + matrix.d * point.y,
139
+ };
140
+ }
141
+
142
+ export function measureManualOffsetDragScreenToOffsetMatrix(
143
+ element: HTMLElement,
144
+ initialOffset: { x: number; y: number },
145
+ options: { probeSize?: number } = {},
146
+ ): { ok: true; matrix: ManualOffsetDragMatrix } | { ok: false; reason: string } {
147
+ const probeSize = options.probeSize ?? DEFAULT_OFFSET_PROBE_PX;
148
+ if (!Number.isFinite(probeSize) || probeSize <= 0) {
149
+ return { ok: false, reason: "Invalid movement probe size." };
150
+ }
151
+
152
+ const snapshot = captureStudioPathOffset(element);
153
+ try {
154
+ applyStudioPathOffsetDraft(element, initialOffset);
155
+ const origin = getRectCenter(element);
156
+ if (!origin) {
157
+ return { ok: false, reason: "Element has no measurable box." };
158
+ }
159
+
160
+ applyStudioPathOffsetDraft(element, {
161
+ x: initialOffset.x + probeSize,
162
+ y: initialOffset.y,
163
+ });
164
+ const probeX = getRectCenter(element);
165
+ if (!probeX) {
166
+ return { ok: false, reason: "Element X movement could not be measured." };
167
+ }
168
+
169
+ applyStudioPathOffsetDraft(element, {
170
+ x: initialOffset.x,
171
+ y: initialOffset.y + probeSize,
172
+ });
173
+ const probeY = getRectCenter(element);
174
+ if (!probeY) {
175
+ return { ok: false, reason: "Element Y movement could not be measured." };
176
+ }
177
+
178
+ const xColumn = {
179
+ x: (probeX.x - origin.x) / probeSize,
180
+ y: (probeX.y - origin.y) / probeSize,
181
+ };
182
+ const yColumn = {
183
+ x: (probeY.x - origin.x) / probeSize,
184
+ y: (probeY.y - origin.y) / probeSize,
185
+ };
186
+ if (
187
+ !finitePoint(xColumn) ||
188
+ !finitePoint(yColumn) ||
189
+ vectorLength(xColumn) < MIN_PROBE_VECTOR_LENGTH_PX ||
190
+ vectorLength(yColumn) < MIN_PROBE_VECTOR_LENGTH_PX
191
+ ) {
192
+ return { ok: false, reason: "Element movement response is too small to measure." };
193
+ }
194
+
195
+ const offsetToScreen = {
196
+ a: xColumn.x,
197
+ b: xColumn.y,
198
+ c: yColumn.x,
199
+ d: yColumn.y,
200
+ };
201
+ const screenToOffset = invertManualOffsetDragMatrix(offsetToScreen);
202
+ if (!screenToOffset) {
203
+ return { ok: false, reason: "Element movement response is not invertible." };
204
+ }
205
+
206
+ return { ok: true, matrix: screenToOffset };
207
+ } finally {
208
+ restoreStudioPathOffset(element, snapshot);
209
+ }
210
+ }
211
+
212
+ export function resolveManualOffsetForPointerDelta(input: {
213
+ initialOffset: { x: number; y: number };
214
+ screenToOffset: ManualOffsetDragMatrix;
215
+ dx: number;
216
+ dy: number;
217
+ }): { x: number; y: number } {
218
+ const offsetDelta = applyManualOffsetDragMatrix(input.screenToOffset, {
219
+ x: input.dx,
220
+ y: input.dy,
221
+ });
222
+ return {
223
+ x: input.initialOffset.x + offsetDelta.x,
224
+ y: input.initialOffset.y + offsetDelta.y,
225
+ };
226
+ }
227
+
228
+ export function createManualOffsetDragMember(input: {
229
+ key: string;
230
+ selection: DomEditSelection;
231
+ element: HTMLElement;
232
+ rect: ManualOffsetDragRect;
233
+ }): ManualOffsetDragMemberResult {
234
+ const initialOffset = readStudioPathOffset(input.element);
235
+ const initialPathOffset = captureStudioPathOffset(input.element);
236
+ const gestureToken = beginStudioManualEditGesture(input.element);
237
+ const measured = measureManualOffsetDragScreenToOffsetMatrix(input.element, initialOffset);
238
+ if (!measured.ok) {
239
+ restoreStudioPathOffset(input.element, initialPathOffset);
240
+ endStudioManualEditGesture(input.element, gestureToken);
241
+ return { ok: false, reason: measured.reason, selection: input.selection };
242
+ }
243
+
244
+ return {
245
+ ok: true,
246
+ member: {
247
+ key: input.key,
248
+ selection: input.selection,
249
+ element: input.element,
250
+ initialOffset,
251
+ initialPathOffset,
252
+ gestureToken,
253
+ screenToOffset: measured.matrix,
254
+ originRect: input.rect,
255
+ },
256
+ };
257
+ }
258
+
259
+ export function resolveManualOffsetDragMemberOffset(
260
+ member: ManualOffsetDragMember,
261
+ dx: number,
262
+ dy: number,
263
+ ): { x: number; y: number } {
264
+ return resolveManualOffsetForPointerDelta({
265
+ initialOffset: member.initialOffset,
266
+ screenToOffset: member.screenToOffset,
267
+ dx,
268
+ dy,
269
+ });
270
+ }
271
+
272
+ export function applyManualOffsetDragDraft(
273
+ member: ManualOffsetDragMember,
274
+ dx: number,
275
+ dy: number,
276
+ ): { x: number; y: number } {
277
+ const offset = resolveManualOffsetDragMemberOffset(member, dx, dy);
278
+ applyStudioPathOffsetDraft(member.element, offset);
279
+ return offset;
280
+ }
281
+
282
+ export function applyManualOffsetDragCommit(
283
+ member: ManualOffsetDragMember,
284
+ dx: number,
285
+ dy: number,
286
+ ): { x: number; y: number } {
287
+ const offset = resolveManualOffsetDragMemberOffset(member, dx, dy);
288
+ applyStudioPathOffset(member.element, offset);
289
+ return offset;
290
+ }
291
+
292
+ export function restoreManualOffsetDragMember(member: ManualOffsetDragMember): void {
293
+ restoreStudioPathOffset(member.element, member.initialPathOffset);
294
+ endStudioManualEditGesture(member.element, member.gestureToken);
295
+ }
296
+
297
+ export function restoreManualOffsetDragMembers(members: ManualOffsetDragMember[]): void {
298
+ for (const member of members) {
299
+ restoreManualOffsetDragMember(member);
300
+ }
301
+ }
302
+
303
+ export function endManualOffsetDragMembers(members: ManualOffsetDragMember[]): void {
304
+ for (const member of members) {
305
+ endStudioManualEditGesture(member.element, member.gestureToken);
306
+ }
307
+ }