@hyperframes/studio 0.6.108 → 0.6.110
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-BRjQ5a5_.js +418 -0
- package/dist/assets/{index-BVqybwMG.css → index-1C8oFiPi.css} +1 -1
- package/dist/assets/{index-Btg-jqxS.js → index-CXy_mWnP.js} +1 -1
- package/dist/assets/{index-tn9M43-U.js → index-CfiyaR7G.js} +1 -1
- package/dist/assets/{index-BgbVhDYd.js → index-D-3sGz65.js} +88 -88
- package/dist/index.html +2 -2
- package/package.json +5 -5
- package/src/components/editor/gsapAnimationConstants.ts +24 -0
- package/src/components/editor/gsapAnimationHelpers.test.ts +29 -0
- package/src/hooks/useGsapTweenCache.ts +0 -27
- package/src/hooks/useRazorSplit.ts +4 -10
- package/src/utils/timelineElementSplit.test.ts +61 -1
- package/src/utils/timelineElementSplit.ts +18 -0
- package/dist/assets/hyperframes-player-67pq7USK.js +0 -418
package/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>HyperFrames Studio</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-D-3sGz65.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-1C8oFiPi.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.110",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"@phosphor-icons/react": "^2.1.10",
|
|
34
34
|
"bpm-detective": "^2.0.5",
|
|
35
35
|
"mediabunny": "^1.45.3",
|
|
36
|
-
"@hyperframes/
|
|
37
|
-
"@hyperframes/core": "0.6.
|
|
38
|
-
"@hyperframes/
|
|
36
|
+
"@hyperframes/sdk": "0.6.110",
|
|
37
|
+
"@hyperframes/core": "0.6.110",
|
|
38
|
+
"@hyperframes/player": "0.6.110"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/react": "19",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"vite": "^6.4.2",
|
|
50
50
|
"vitest": "^3.2.4",
|
|
51
51
|
"zustand": "^5.0.0",
|
|
52
|
-
"@hyperframes/producer": "0.6.
|
|
52
|
+
"@hyperframes/producer": "0.6.110"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"react": "19",
|
|
@@ -20,6 +20,12 @@ export const PROP_LABELS: Record<string, string> = {
|
|
|
20
20
|
width: "Width",
|
|
21
21
|
height: "Height",
|
|
22
22
|
rotation: "Rotate",
|
|
23
|
+
z: "Move Z",
|
|
24
|
+
rotationX: "Rotate X",
|
|
25
|
+
rotationY: "Rotate Y",
|
|
26
|
+
rotationZ: "Rotate Z",
|
|
27
|
+
perspective: "Perspective",
|
|
28
|
+
transformOrigin: "Transform Origin",
|
|
23
29
|
opacity: "Opacity",
|
|
24
30
|
scale: "Scale",
|
|
25
31
|
scaleX: "Scale X",
|
|
@@ -46,6 +52,12 @@ export const PROP_UNITS: Record<string, string> = {
|
|
|
46
52
|
width: "px",
|
|
47
53
|
height: "px",
|
|
48
54
|
rotation: "°",
|
|
55
|
+
z: "px",
|
|
56
|
+
rotationX: "°",
|
|
57
|
+
rotationY: "°",
|
|
58
|
+
rotationZ: "°",
|
|
59
|
+
perspective: "px",
|
|
60
|
+
transformOrigin: "",
|
|
49
61
|
opacity: "%",
|
|
50
62
|
scale: "×",
|
|
51
63
|
scaleX: "×",
|
|
@@ -62,6 +74,13 @@ export const PROP_TOOLTIPS: Record<string, string> = {
|
|
|
62
74
|
scaleX: "Horizontal stretch (1 = normal)",
|
|
63
75
|
scaleY: "Vertical stretch (1 = normal)",
|
|
64
76
|
rotation: "Spin angle (360 = full rotation)",
|
|
77
|
+
z: "Move forward/back along the Z axis",
|
|
78
|
+
rotationX: "Rotate around the horizontal X axis",
|
|
79
|
+
rotationY: "Rotate around the vertical Y axis",
|
|
80
|
+
rotationZ: "Rotate around the screen-facing Z axis",
|
|
81
|
+
perspective:
|
|
82
|
+
"3D depth context for child elements; set it on a parent when rotating children in 3D",
|
|
83
|
+
transformOrigin: "Pivot point for transforms, for example center center or 50% 50%",
|
|
65
84
|
width: "Element width",
|
|
66
85
|
height: "Element height",
|
|
67
86
|
autoAlpha: "Like opacity but hides element completely at 0",
|
|
@@ -147,6 +166,11 @@ export const PROP_CONSTRAINTS: Record<string, { min?: number; max?: number; step
|
|
|
147
166
|
scaleX: { min: -10, max: 10, step: 0.01 },
|
|
148
167
|
scaleY: { min: -10, max: 10, step: 0.01 },
|
|
149
168
|
rotation: { step: 1 },
|
|
169
|
+
z: { step: 1 },
|
|
170
|
+
rotationX: { step: 1 },
|
|
171
|
+
rotationY: { step: 1 },
|
|
172
|
+
rotationZ: { step: 1 },
|
|
173
|
+
perspective: { min: 0, step: 1 },
|
|
150
174
|
skewX: { min: -90, max: 90, step: 1 },
|
|
151
175
|
skewY: { min: -90, max: 90, step: 1 },
|
|
152
176
|
width: { min: 0, step: 1 },
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { SUPPORTED_PROPS } from "@hyperframes/core/gsap-constants";
|
|
2
3
|
import { buildTweenSummary } from "./gsapAnimationHelpers";
|
|
4
|
+
import { PROP_LABELS } from "./gsapAnimationConstants";
|
|
3
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
6
|
|
|
5
7
|
function anim(overrides: Partial<GsapAnimation>): GsapAnimation {
|
|
@@ -23,6 +25,27 @@ describe("buildTweenSummary", () => {
|
|
|
23
25
|
expect(s).toContain("move x");
|
|
24
26
|
});
|
|
25
27
|
|
|
28
|
+
it("describes 3D transform tweens with labels and units", () => {
|
|
29
|
+
const s = buildTweenSummary(
|
|
30
|
+
anim({
|
|
31
|
+
properties: {
|
|
32
|
+
z: 120,
|
|
33
|
+
rotationX: 45,
|
|
34
|
+
rotationY: -30,
|
|
35
|
+
rotationZ: 90,
|
|
36
|
+
perspective: 800,
|
|
37
|
+
transformOrigin: "50% 50%",
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
expect(s).toContain("move z to 120px");
|
|
42
|
+
expect(s).toContain("rotate x to 45°");
|
|
43
|
+
expect(s).toContain("rotate y to -30°");
|
|
44
|
+
expect(s).toContain("rotate z to 90°");
|
|
45
|
+
expect(s).toContain("perspective to 800px");
|
|
46
|
+
expect(s).toContain("transform origin to 50% 50%");
|
|
47
|
+
});
|
|
48
|
+
|
|
26
49
|
it("describes a from tween", () => {
|
|
27
50
|
const s = buildTweenSummary(anim({ method: "from", properties: { opacity: 0 } }));
|
|
28
51
|
expect(s).toContain("enters from");
|
|
@@ -65,3 +88,9 @@ describe("buildTweenSummary", () => {
|
|
|
65
88
|
expect(s).toContain("no properties yet");
|
|
66
89
|
});
|
|
67
90
|
});
|
|
91
|
+
|
|
92
|
+
describe("PROP_LABELS", () => {
|
|
93
|
+
it("provides labels for every inspector-supported GSAP property", () => {
|
|
94
|
+
expect(SUPPORTED_PROPS.filter((prop) => !PROP_LABELS[prop])).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -366,13 +366,6 @@ export function usePopulateKeyframeCacheForFile(
|
|
|
366
366
|
const { setKeyframeCache } = usePlayerStore.getState();
|
|
367
367
|
clearKeyframeCacheForFile(sf);
|
|
368
368
|
const { elements } = usePlayerStore.getState();
|
|
369
|
-
console.log(
|
|
370
|
-
"[kf:static] elements in store:",
|
|
371
|
-
elements
|
|
372
|
-
.map((e) => e.domId)
|
|
373
|
-
.filter(Boolean)
|
|
374
|
-
.join(", "),
|
|
375
|
-
);
|
|
376
369
|
const mergedByElement = new Map<string, GsapKeyframesData>();
|
|
377
370
|
for (const anim of parsed.animations) {
|
|
378
371
|
const id = extractIdFromSelector(anim.targetSelector);
|
|
@@ -415,12 +408,6 @@ export function usePopulateKeyframeCacheForFile(
|
|
|
415
408
|
mergedByElement.set(id, { ...kfData, keyframes: clipKeyframes });
|
|
416
409
|
}
|
|
417
410
|
}
|
|
418
|
-
console.log(
|
|
419
|
-
"[kf:static] merged elements:",
|
|
420
|
-
[...mergedByElement.keys()].join(", "),
|
|
421
|
-
"kf counts:",
|
|
422
|
-
[...mergedByElement.entries()].map(([k, v]) => `${k}:${v.keyframes.length}`).join(", "),
|
|
423
|
-
);
|
|
424
411
|
for (const [id, kfData] of mergedByElement) {
|
|
425
412
|
setKeyframeCache(`${sf}#${id}`, kfData);
|
|
426
413
|
setKeyframeCache(id, kfData);
|
|
@@ -454,12 +441,6 @@ export function usePopulateKeyframeCacheForFile(
|
|
|
454
441
|
if (el.domId) clipById.set(el.domId, { start: el.start, duration: el.duration });
|
|
455
442
|
}
|
|
456
443
|
const scanned = scanAllRuntimeKeyframes(iframe, clipById);
|
|
457
|
-
console.log(
|
|
458
|
-
"[kf:runtime] scanned",
|
|
459
|
-
scanned.size,
|
|
460
|
-
"elements:",
|
|
461
|
-
[...scanned.keys()].join(", "),
|
|
462
|
-
);
|
|
463
444
|
if (scanned.size === 0) return false;
|
|
464
445
|
const { setKeyframeCache, keyframeCache } = usePlayerStore.getState();
|
|
465
446
|
for (const [id, data] of scanned) {
|
|
@@ -470,14 +451,6 @@ export function usePopulateKeyframeCacheForFile(
|
|
|
470
451
|
if (alreadyCached) {
|
|
471
452
|
continue;
|
|
472
453
|
}
|
|
473
|
-
console.log(
|
|
474
|
-
"[kf:runtime] adding runtime entry:",
|
|
475
|
-
id,
|
|
476
|
-
"kfs:",
|
|
477
|
-
data.keyframes.length,
|
|
478
|
-
"arc:",
|
|
479
|
-
!!data.arcPath,
|
|
480
|
-
);
|
|
481
454
|
const entry = {
|
|
482
455
|
format: "percentage" as const,
|
|
483
456
|
keyframes: data.keyframes,
|
|
@@ -5,10 +5,10 @@ import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
|
|
5
5
|
import { getTimelineElementLabel, collectHtmlIds } from "../utils/studioHelpers";
|
|
6
6
|
import { trackStudioRazorSplit } from "../telemetry/events";
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
canSplitElementAt,
|
|
9
|
+
selectSplittableElements,
|
|
9
10
|
buildPatchTarget,
|
|
10
11
|
readFileContent,
|
|
11
|
-
isSplitTimeWithinBounds,
|
|
12
12
|
} from "../utils/timelineElementSplit";
|
|
13
13
|
import type { RecordEditInput } from "./timelineEditingHelpers";
|
|
14
14
|
|
|
@@ -170,11 +170,7 @@ export function useRazorSplit({
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
const pid = projectIdRef.current;
|
|
173
|
-
if (!pid || !
|
|
174
|
-
|
|
175
|
-
if (!isSplitTimeWithinBounds(splitTime, element.start, element.duration)) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
173
|
+
if (!pid || !canSplitElementAt(element, splitTime)) return;
|
|
178
174
|
|
|
179
175
|
try {
|
|
180
176
|
const { targetPath, originalContent, patchedContent, changed, skippedSelectors } =
|
|
@@ -232,9 +228,7 @@ export function useRazorSplit({
|
|
|
232
228
|
const pid = projectIdRef.current;
|
|
233
229
|
if (!pid) return;
|
|
234
230
|
const { elements } = usePlayerStore.getState();
|
|
235
|
-
const splittable = elements
|
|
236
|
-
(el) => canSplitElement(el) && splitTime > el.start && splitTime < el.start + el.duration,
|
|
237
|
-
);
|
|
231
|
+
const splittable = selectSplittableElements(elements, splitTime);
|
|
238
232
|
if (splittable.length === 0) return;
|
|
239
233
|
|
|
240
234
|
try {
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import type { TimelineElement } from "../player/store/playerStore";
|
|
3
|
+
import {
|
|
4
|
+
SPLIT_BOUNDARY_EPSILON_S,
|
|
5
|
+
canSplitElementAt,
|
|
6
|
+
isSplitTimeWithinBounds,
|
|
7
|
+
selectSplittableElements,
|
|
8
|
+
} from "./timelineElementSplit";
|
|
9
|
+
|
|
10
|
+
function element(overrides: Partial<TimelineElement> = {}): TimelineElement {
|
|
11
|
+
return {
|
|
12
|
+
id: "el-1",
|
|
13
|
+
tag: "div",
|
|
14
|
+
start: 1,
|
|
15
|
+
duration: 4,
|
|
16
|
+
track: 0,
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
3
20
|
|
|
4
21
|
describe("isSplitTimeWithinBounds", () => {
|
|
5
22
|
const start = 1;
|
|
@@ -48,3 +65,46 @@ describe("isSplitTimeWithinBounds", () => {
|
|
|
48
65
|
expect(isSplitTimeWithinBounds(start + shortDuration / 2, start, shortDuration)).toBe(false);
|
|
49
66
|
});
|
|
50
67
|
});
|
|
68
|
+
|
|
69
|
+
describe("canSplitElementAt", () => {
|
|
70
|
+
it("accepts a splittable element at an interior time", () => {
|
|
71
|
+
expect(canSplitElementAt(element({ start: 1, duration: 4 }), 3)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("rejects a time inside the boundary epsilon", () => {
|
|
75
|
+
expect(
|
|
76
|
+
canSplitElementAt(element({ start: 1, duration: 4 }), 1 + SPLIT_BOUNDARY_EPSILON_S / 2),
|
|
77
|
+
).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects locked, implicit and sub-composition elements", () => {
|
|
81
|
+
expect(canSplitElementAt(element({ timelineLocked: true }), 3)).toBe(false);
|
|
82
|
+
expect(canSplitElementAt(element({ timingSource: "implicit" }), 3)).toBe(false);
|
|
83
|
+
expect(canSplitElementAt(element({ compositionSrc: "child.html" }), 3)).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("selectSplittableElements", () => {
|
|
88
|
+
it("excludes a clip shorter than two epsilons even when the time is inside it", () => {
|
|
89
|
+
// Regression: split-all used raw start < t < end, so a clip too short for
|
|
90
|
+
// the epsilon margin was still selected and produced a degenerate slice.
|
|
91
|
+
const tiny = element({ id: "tiny", start: 1, duration: SPLIT_BOUNDARY_EPSILON_S + 0.01 });
|
|
92
|
+
const interiorTime = tiny.start + tiny.duration / 2;
|
|
93
|
+
expect(interiorTime).toBeGreaterThan(tiny.start);
|
|
94
|
+
expect(interiorTime).toBeLessThan(tiny.start + tiny.duration);
|
|
95
|
+
expect(selectSplittableElements([tiny], interiorTime)).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("keeps only the elements whose epsilon-bounded range contains the time", () => {
|
|
99
|
+
const inside = element({ id: "inside", start: 0, duration: 4 });
|
|
100
|
+
const outside = element({ id: "outside", start: 5, duration: 4 });
|
|
101
|
+
const locked = element({ id: "locked", start: 0, duration: 4, timelineLocked: true });
|
|
102
|
+
const result = selectSplittableElements([inside, outside, locked], 2);
|
|
103
|
+
expect(result.map((el) => el.id)).toEqual(["inside"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("accepts an element at the exact lower clamp boundary", () => {
|
|
107
|
+
const el = element({ start: 2, duration: 4 });
|
|
108
|
+
expect(selectSplittableElements([el], 2 + SPLIT_BOUNDARY_EPSILON_S)).toEqual([el]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -30,3 +30,21 @@ export function canSplitElement(el: TimelineElement): boolean {
|
|
|
30
30
|
Number.isFinite(el.duration)
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* True when `el` can be split AND `splitTime` lies within its boundary epsilon.
|
|
36
|
+
* Shared by the single-clip and split-all razor paths so both honor the same
|
|
37
|
+
* minimum-distance rule (split-all previously used raw `>`/`<`, letting cuts
|
|
38
|
+
* land inside the epsilon margin and produce a degenerate slice).
|
|
39
|
+
*/
|
|
40
|
+
export function canSplitElementAt(el: TimelineElement, splitTime: number): boolean {
|
|
41
|
+
return canSplitElement(el) && isSplitTimeWithinBounds(splitTime, el.start, el.duration);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Elements that the split-all razor action can cut at `splitTime`. */
|
|
45
|
+
export function selectSplittableElements(
|
|
46
|
+
elements: TimelineElement[],
|
|
47
|
+
splitTime: number,
|
|
48
|
+
): TimelineElement[] {
|
|
49
|
+
return elements.filter((el) => canSplitElementAt(el, splitTime));
|
|
50
|
+
}
|