@hyperframes/studio 0.6.96 → 0.6.98
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-DgsMQSvV.js +418 -0
- package/dist/assets/index-B62bDCQv.css +1 -0
- package/dist/assets/index-Ce3pBm_I.js +252 -0
- package/dist/assets/{index-BWFaypdT.js → index-D-ET9M0b.js} +1 -1
- package/dist/assets/index-D-bS9Dxx.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +7 -5
- package/src/App.tsx +182 -177
- package/src/captions/store.ts +11 -11
- package/src/components/StudioHeader.tsx +4 -4
- package/src/components/StudioLeftSidebar.tsx +2 -2
- package/src/components/StudioPreviewArea.tsx +225 -183
- package/src/components/StudioRightPanel.tsx +3 -3
- package/src/components/TimelineToolbar.tsx +25 -0
- package/src/components/editor/DomEditOverlay.tsx +2 -5
- package/src/components/editor/EaseCurveSection.tsx +2 -3
- package/src/components/editor/GestureTrailOverlay.tsx +4 -3
- package/src/components/editor/LayersPanel.tsx +3 -9
- package/src/components/editor/PropertyPanel.tsx +20 -61
- package/src/components/editor/colorValue.ts +3 -1
- package/src/components/editor/domEditOverlayGestures.ts +54 -1
- package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
- package/src/components/editor/gradientValue.ts +3 -3
- package/src/components/editor/keyframeMove.test.ts +101 -0
- package/src/components/editor/keyframeMove.ts +151 -0
- package/src/components/editor/manualEditsDom.ts +0 -12
- package/src/components/editor/propertyPanelHelpers.ts +10 -38
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -5
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -6
- package/src/components/editor/propertyPanelTransformCommit.ts +129 -0
- package/src/components/editor/studioMotionOps.test.ts +1 -1
- package/src/components/editor/studioMotionOps.ts +2 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +1 -46
- package/src/components/nle/NLELayout.tsx +1 -24
- package/src/components/sidebar/BlocksTab.tsx +2 -2
- package/src/contexts/DomEditContext.tsx +134 -31
- package/src/contexts/StudioContext.tsx +90 -40
- package/src/contexts/TimelineEditContext.tsx +47 -0
- package/src/hooks/domEditCommitTypes.ts +14 -0
- package/src/hooks/gsapDragCommit.ts +9 -24
- package/src/hooks/gsapKeyframeCacheHelpers.ts +2 -1
- package/src/hooks/gsapKeyframeCommit.ts +5 -15
- package/src/hooks/gsapRuntimeBridge.ts +18 -52
- package/src/hooks/gsapRuntimeKeyframes.ts +8 -57
- package/src/hooks/gsapRuntimeReaders.ts +19 -26
- package/src/hooks/gsapScriptCommitHelpers.ts +1 -11
- package/src/hooks/gsapScriptCommitTypes.ts +58 -0
- package/src/hooks/gsapShared.ts +157 -0
- package/src/hooks/timelineEditingHelpers.ts +63 -2
- package/src/hooks/useAnimatedPropertyCommit.ts +3 -25
- package/src/hooks/useAppHotkeys.ts +299 -377
- package/src/hooks/useConsoleErrorCapture.ts +33 -5
- package/src/hooks/useDomEditCommits.ts +35 -293
- package/src/hooks/useDomEditPositionPatchCommit.ts +1 -1
- package/src/hooks/useDomEditSession.ts +78 -249
- package/src/hooks/useDomEditTextCommits.ts +1 -1
- package/src/hooks/useDomEditWiring.ts +255 -0
- package/src/hooks/useDomGeometryCommits.ts +181 -0
- package/src/hooks/useDomSelection.ts +10 -27
- package/src/hooks/useEditorSave.ts +82 -0
- package/src/hooks/useElementLifecycleOps.ts +177 -0
- package/src/hooks/useEnableKeyframes.ts +10 -15
- package/src/hooks/useFileManager.ts +32 -114
- package/src/hooks/useFileTree.ts +80 -0
- package/src/hooks/useGestureCommit.ts +7 -5
- package/src/hooks/useGestureRecording.ts +1 -1
- package/src/hooks/useGsapAnimationOps.ts +122 -0
- package/src/hooks/useGsapArcPathOps.ts +61 -0
- package/src/hooks/useGsapAwareEditing.ts +242 -0
- package/src/hooks/useGsapKeyframeOps.ts +167 -0
- package/src/hooks/useGsapPropertyDebounce.ts +135 -0
- package/src/hooks/useGsapScriptCommits.ts +58 -570
- package/src/hooks/useGsapSelectionHandlers.ts +22 -9
- package/src/hooks/useGsapTweenCache.ts +35 -29
- package/src/hooks/useLintModal.ts +7 -0
- package/src/hooks/useMusicBeatAnalysis.ts +152 -0
- package/src/hooks/useRazorSplit.ts +1 -1
- package/src/hooks/useRenderClipContent.ts +46 -21
- package/src/hooks/useTimelineEditing.ts +48 -4
- package/src/player/components/AudioWaveform.tsx +29 -4
- package/src/player/components/BeatStrip.tsx +166 -0
- package/src/player/components/Timeline.tsx +39 -18
- package/src/player/components/TimelineCanvas.tsx +52 -12
- package/src/player/components/TimelineClipDiamonds.tsx +130 -20
- package/src/player/components/TimelinePropertyRows.tsx +8 -2
- package/src/player/components/TimelineRuler.tsx +36 -2
- package/src/player/components/timelineEditing.ts +30 -5
- package/src/player/components/useTimelineClipDrag.ts +155 -4
- package/src/player/components/useTimelinePlayhead.ts +30 -1
- package/src/player/hooks/useTimelinePlayer.ts +47 -45
- package/src/player/lib/mediaProbe.ts +46 -3
- package/src/player/lib/playbackScrub.ts +16 -0
- package/src/player/lib/timelineDOM.ts +10 -2
- package/src/player/lib/timelineIframeHelpers.ts +89 -0
- package/src/player/store/playerStore.ts +92 -33
- package/src/utils/beatEditActions.ts +109 -0
- package/src/utils/beatEditing.ts +136 -0
- package/src/utils/clipboardPayload.ts +3 -2
- package/src/utils/compositionPatterns.ts +2 -0
- package/src/utils/keyframeSelection.test.ts +45 -0
- package/src/utils/keyframeSelection.ts +29 -0
- package/src/utils/rounding.ts +9 -0
- package/src/utils/studioHelpers.ts +5 -2
- package/src/utils/studioUrlState.ts +2 -1
- package/src/utils/timelineAssetDrop.ts +6 -5
- package/src/utils/timelineInspector.ts +15 -100
- package/dist/assets/hyperframes-player-0esDKGRk.js +0 -418
- package/dist/assets/index-B0twsRu0.css +0 -1
- package/dist/assets/index-BA979yF1.js +0 -251
- package/src/components/editor/DopesheetStrip.tsx +0 -141
- package/src/components/editor/StaggerControls.tsx +0 -61
- package/src/components/editor/TimelineLayerPanel.test.ts +0 -42
- package/src/components/editor/TimelineLayerPanel.tsx +0 -15
- package/src/components/nle/TimelineEditorNotice.tsx +0 -133
- package/src/hooks/gsapRuntimePreview.ts +0 -19
- package/src/player/components/timelineUtils.ts +0 -211
- package/src/utils/audioBeatDetection.ts +0 -58
- package/src/utils/keyframeSnapping.test.ts +0 -74
- package/src/utils/keyframeSnapping.ts +0 -63
- package/src/utils/timelineInspector.test.ts +0 -79
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { computeSnapThreshold, snapKeyframe } from "./keyframeSnapping";
|
|
3
|
-
|
|
4
|
-
describe("snapKeyframe", () => {
|
|
5
|
-
test("snaps to frame boundary", () => {
|
|
6
|
-
const result = snapKeyframe(0.34, { fps: 30, keyframeTimes: [], threshold: 0.05 });
|
|
7
|
-
expect(result.snapType).toBe("frame");
|
|
8
|
-
expect(Math.abs(result.snappedTime - 1 / 3)).toBeLessThan(0.01);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test("snaps to cross-element keyframe when closest", () => {
|
|
12
|
-
const result = snapKeyframe(1.005, { fps: 30, keyframeTimes: [1.0], threshold: 0.05 });
|
|
13
|
-
expect(result.snapType).toBe("keyframe");
|
|
14
|
-
expect(result.snappedTime).toBe(1.0);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("keyframe snap wins tie with frame at same position", () => {
|
|
18
|
-
const result = snapKeyframe(1.0, { fps: 30, keyframeTimes: [1.0], threshold: 0.05 });
|
|
19
|
-
expect(result.snapType).toBe("keyframe");
|
|
20
|
-
expect(result.snappedTime).toBe(1.0);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("snaps to beat marker when closer than frame", () => {
|
|
24
|
-
const result = snapKeyframe(2.49, {
|
|
25
|
-
fps: 30,
|
|
26
|
-
keyframeTimes: [],
|
|
27
|
-
beatTimes: [2.5],
|
|
28
|
-
threshold: 0.05,
|
|
29
|
-
});
|
|
30
|
-
expect(result.snapType).toBe("beat");
|
|
31
|
-
expect(result.snappedTime).toBe(2.5);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("disabled returns raw time", () => {
|
|
35
|
-
const result = snapKeyframe(1.5, {
|
|
36
|
-
fps: 30,
|
|
37
|
-
keyframeTimes: [1.5],
|
|
38
|
-
threshold: 0.05,
|
|
39
|
-
disabled: true,
|
|
40
|
-
});
|
|
41
|
-
expect(result.snapType).toBeNull();
|
|
42
|
-
expect(result.snappedTime).toBe(1.5);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("no snap when outside threshold", () => {
|
|
46
|
-
const result = snapKeyframe(1.5, {
|
|
47
|
-
fps: 30,
|
|
48
|
-
keyframeTimes: [0.5],
|
|
49
|
-
threshold: 0.05,
|
|
50
|
-
});
|
|
51
|
-
expect(result.snapType).toBe("frame");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("empty beat times is graceful", () => {
|
|
55
|
-
const result = snapKeyframe(0.5, {
|
|
56
|
-
fps: 30,
|
|
57
|
-
keyframeTimes: [],
|
|
58
|
-
beatTimes: [],
|
|
59
|
-
threshold: 0.05,
|
|
60
|
-
});
|
|
61
|
-
expect(result.snapType).toBe("frame");
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe("computeSnapThreshold", () => {
|
|
66
|
-
test("returns threshold based on pixels per second", () => {
|
|
67
|
-
const threshold = computeSnapThreshold(100, 5);
|
|
68
|
-
expect(threshold).toBe(0.05);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("fallback for zero pixels per second", () => {
|
|
72
|
-
expect(computeSnapThreshold(0)).toBe(0.1);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
export type SnapType = "frame" | "keyframe" | "beat" | null;
|
|
2
|
-
|
|
3
|
-
export interface SnapResult {
|
|
4
|
-
snappedTime: number;
|
|
5
|
-
snapType: SnapType;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function snapKeyframe(
|
|
9
|
-
time: number,
|
|
10
|
-
options: {
|
|
11
|
-
fps: number;
|
|
12
|
-
keyframeTimes: number[];
|
|
13
|
-
beatTimes?: number[];
|
|
14
|
-
threshold: number;
|
|
15
|
-
disabled?: boolean;
|
|
16
|
-
},
|
|
17
|
-
): SnapResult {
|
|
18
|
-
if (options.disabled) return { snappedTime: time, snapType: null };
|
|
19
|
-
|
|
20
|
-
const { fps, keyframeTimes, beatTimes = [], threshold } = options;
|
|
21
|
-
|
|
22
|
-
let bestDist = threshold;
|
|
23
|
-
let bestTime = time;
|
|
24
|
-
let bestType: SnapType = null;
|
|
25
|
-
|
|
26
|
-
// Priority: cross-element keyframes > beat markers > frame boundaries
|
|
27
|
-
// Higher priority snaps use strict < so they win on equal distance
|
|
28
|
-
if (fps > 0) {
|
|
29
|
-
const frameDuration = 1 / fps;
|
|
30
|
-
const nearestFrame = Math.round(time / frameDuration) * frameDuration;
|
|
31
|
-
const dist = Math.abs(time - nearestFrame);
|
|
32
|
-
if (dist < bestDist) {
|
|
33
|
-
bestDist = dist;
|
|
34
|
-
bestTime = nearestFrame;
|
|
35
|
-
bestType = "frame";
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
for (const bt of beatTimes) {
|
|
40
|
-
const dist = Math.abs(time - bt);
|
|
41
|
-
if (dist <= bestDist) {
|
|
42
|
-
bestDist = dist;
|
|
43
|
-
bestTime = bt;
|
|
44
|
-
bestType = "beat";
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
for (const kt of keyframeTimes) {
|
|
49
|
-
const dist = Math.abs(time - kt);
|
|
50
|
-
if (dist <= bestDist) {
|
|
51
|
-
bestDist = dist;
|
|
52
|
-
bestTime = kt;
|
|
53
|
-
bestType = "keyframe";
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return { snappedTime: bestTime, snapType: bestType };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function computeSnapThreshold(pixelsPerSecond: number, baseThresholdPx: number = 5): number {
|
|
61
|
-
if (pixelsPerSecond <= 0) return 0.1;
|
|
62
|
-
return baseThresholdPx / pixelsPerSecond;
|
|
63
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { Window } from "happy-dom";
|
|
3
|
-
import {
|
|
4
|
-
canInspectTimelineElement,
|
|
5
|
-
getTimelineLayerVisibilityInPreview,
|
|
6
|
-
getTimelineElementKey,
|
|
7
|
-
isAudioTimelineElement,
|
|
8
|
-
isTimelineElementActiveAtTime,
|
|
9
|
-
isTimelineLayerVisibleInPreview,
|
|
10
|
-
shouldShowTimelineInspectorBounds,
|
|
11
|
-
} from "./timelineInspector";
|
|
12
|
-
|
|
13
|
-
function createDocument(markup: string): Document {
|
|
14
|
-
const window = new Window();
|
|
15
|
-
window.document.body.innerHTML = markup;
|
|
16
|
-
return window.document;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function attachVisibleBox(element: HTMLElement) {
|
|
20
|
-
Object.defineProperty(element, "getBoundingClientRect", {
|
|
21
|
-
configurable: true,
|
|
22
|
-
value: () => ({
|
|
23
|
-
bottom: 34,
|
|
24
|
-
height: 24,
|
|
25
|
-
left: 10,
|
|
26
|
-
right: 90,
|
|
27
|
-
top: 10,
|
|
28
|
-
width: 80,
|
|
29
|
-
x: 10,
|
|
30
|
-
y: 10,
|
|
31
|
-
toJSON: () => ({}),
|
|
32
|
-
}),
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe("timeline inspector", () => {
|
|
37
|
-
it("keeps visual clips inspectable and audio-only clips out of the visual panel", () => {
|
|
38
|
-
expect(canInspectTimelineElement({ tag: "section" })).toBe(true);
|
|
39
|
-
expect(canInspectTimelineElement({ tag: "video", src: "assets/demo.mp4" })).toBe(true);
|
|
40
|
-
expect(canInspectTimelineElement({ tag: "audio" })).toBe(false);
|
|
41
|
-
expect(canInspectTimelineElement({ tag: "div", src: "assets/narration.mp3" })).toBe(false);
|
|
42
|
-
expect(isAudioTimelineElement({ tag: "sfx" })).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("uses stable timeline keys and only shows bounds at clip edges", () => {
|
|
46
|
-
expect(getTimelineElementKey({ id: "card", key: "index.html#card" })).toBe("index.html#card");
|
|
47
|
-
expect(shouldShowTimelineInspectorBounds(2, { start: 2, duration: 4 })).toBe(true);
|
|
48
|
-
expect(shouldShowTimelineInspectorBounds(6, { start: 2, duration: 4 })).toBe(true);
|
|
49
|
-
expect(shouldShowTimelineInspectorBounds(4, { start: 2, duration: 4 })).toBe(false);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("keeps selected layer bounds visible only while the clip is active", () => {
|
|
53
|
-
expect(isTimelineElementActiveAtTime(1.99, { start: 2, duration: 4 }, 0)).toBe(false);
|
|
54
|
-
expect(isTimelineElementActiveAtTime(2, { start: 2, duration: 4 }, 0)).toBe(true);
|
|
55
|
-
expect(isTimelineElementActiveAtTime(4, { start: 2, duration: 4 }, 0)).toBe(true);
|
|
56
|
-
expect(isTimelineElementActiveAtTime(6, { start: 2, duration: 4 }, 0)).toBe(true);
|
|
57
|
-
expect(isTimelineElementActiveAtTime(6.01, { start: 2, duration: 4 }, 0)).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("uses composite visibility for nested layers", () => {
|
|
61
|
-
const hiddenDoc = createDocument(`<div style="opacity: 0"><span id="label">Label</span></div>`);
|
|
62
|
-
const hiddenLabel = hiddenDoc.getElementById("label") as HTMLElement;
|
|
63
|
-
attachVisibleBox(hiddenLabel);
|
|
64
|
-
expect(isTimelineLayerVisibleInPreview(hiddenLabel)).toBe(false);
|
|
65
|
-
|
|
66
|
-
const visibleDoc = createDocument(
|
|
67
|
-
`<div style="opacity: 1"><span id="label">Label</span></div>`,
|
|
68
|
-
);
|
|
69
|
-
const visibleLabel = visibleDoc.getElementById("label") as HTMLElement;
|
|
70
|
-
attachVisibleBox(visibleLabel);
|
|
71
|
-
expect(isTimelineLayerVisibleInPreview(visibleLabel)).toBe(true);
|
|
72
|
-
expect(getTimelineLayerVisibilityInPreview(visibleLabel)).toMatchObject({
|
|
73
|
-
compositeOpacity: 1,
|
|
74
|
-
hasBox: true,
|
|
75
|
-
inViewport: true,
|
|
76
|
-
visible: true,
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
});
|