@hyperframes/studio 0.8.105 → 0.8.106
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-tnfCiVdR.js → hyperframes-player-dl6K6ABp.js} +1 -1
- package/dist/assets/{index-DEDhPmPD.js → index-CMiYVGv5.js} +202 -202
- package/dist/assets/{index-zpe332hb.js → index-DKT5NF1Y.js} +1 -1
- package/dist/assets/index-lrT-gfHa.css +1 -0
- package/dist/assets/{index-Ca6LZlVb.js → index-ooaZZhKf.js} +1 -1
- package/dist/{chunk-QADHJFDF.js → chunk-ICTZJPQQ.js} +2 -1
- package/dist/chunk-ICTZJPQQ.js.map +1 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.html +11 -2
- package/dist/index.js +6601 -6022
- package/dist/index.js.map +1 -1
- package/dist/player.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +9 -9
- package/src/captions/keyboard.test.ts +6 -0
- package/src/captions/keyboard.ts +2 -1
- package/src/components/StudioHeader.dom.test.tsx +31 -9
- package/src/components/StudioHeader.tsx +4 -1
- package/src/components/ThemeToggle.tsx +99 -0
- package/src/components/editor/DomEditOverlay.tsx +37 -5
- package/src/components/editor/SourceEditor.tsx +2 -6
- package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
- package/src/components/editor/manualEditsDom.ts +33 -0
- package/src/components/editor/marqueeCommit.test.tsx +69 -1
- package/src/components/editor/marqueeCommit.ts +6 -1
- package/src/components/editor/overlayFrameLoop.test.ts +27 -0
- package/src/components/editor/overlayFrameLoop.ts +21 -0
- package/src/components/editor/propertyPanelFont.test.tsx +80 -3
- package/src/components/editor/propertyPanelFont.tsx +69 -46
- package/src/components/editor/propertyPanelHelpers.ts +1 -2
- package/src/components/editor/studioCodeTheme.test.ts +20 -0
- package/src/components/editor/studioCodeTheme.ts +99 -0
- package/src/components/editor/useDomEditNudge.test.tsx +31 -0
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useInlineTextEditing.tsx +1 -1
- package/src/components/editor/useMotionPathData.test.tsx +64 -0
- package/src/components/editor/useMotionPathData.ts +5 -1
- package/src/hooks/appHotkeysDispatch.ts +2 -1
- package/src/hooks/previewPromotionSelection.test.tsx +180 -0
- package/src/hooks/useAppHotkeys.test.ts +32 -0
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useClipboard.source.test.ts +99 -19
- package/src/hooks/useClipboard.ts +145 -124
- package/src/hooks/useDomEditSession.test.tsx +76 -72
- package/src/hooks/useDomEditSession.ts +7 -43
- package/src/hooks/useGsapTweenCache.ts +1 -2
- package/src/hooks/useInlineTextEdit.ts +16 -2
- package/src/hooks/useLivePreviewIframe.ts +18 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
- package/src/hooks/usePreviewPersistence.ts +2 -5
- package/src/hooks/useRenderClipContent.ts +3 -3
- package/src/index.ts +2 -0
- package/src/livePreviewIframeExport.test.tsx +33 -0
- package/src/main.tsx +4 -1
- package/src/player/components/AudioWaveform.test.tsx +36 -15
- package/src/player/components/AudioWaveform.tsx +24 -7
- package/src/player/components/TimelineClip.test.tsx +38 -0
- package/src/player/components/TimelineClip.tsx +14 -6
- package/src/player/components/TimelineClipFades.test.tsx +376 -74
- package/src/player/components/TimelineClipFades.tsx +368 -137
- package/src/player/components/useTimelineKeyboardActor.ts +1 -1
- package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
- package/src/player/hooks/useShadowPreviewReload.ts +72 -5
- package/src/player/lib/automationStoreSync.test.ts +18 -1
- package/src/player/lib/automationStoreSync.ts +29 -17
- package/src/player/sceneSwap.ts +18 -0
- package/src/styles/components.css +26 -0
- package/src/styles/studio.css +6 -0
- package/src/styles/theme.css +10 -0
- package/src/utils/clipboardPayload.ts +10 -1
- package/src/utils/studioPendingEdits.ts +21 -0
- package/src/utils/studioTheme.test.tsx +141 -0
- package/src/utils/studioTheme.ts +34 -0
- package/src/utils/studioUiPreferences.ts +3 -0
- package/src/utils/typingTarget.test.ts +8 -1
- package/src/utils/typingTarget.ts +5 -0
- package/dist/assets/index-Cre9X4RS.css +0 -1
- package/dist/chunk-QADHJFDF.js.map +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
|
|
1
|
+
import { memo, useCallback, useContext, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { fadeGain } from "@hyperframes/core/audio-fade";
|
|
2
3
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
4
|
import { useThumbnailLease } from "../../hooks/useThumbnailLease";
|
|
4
5
|
import { createThumbnailKey, type ThumbnailPriority } from "../lib/thumbnailScheduler";
|
|
5
6
|
import { decimatePeaks, loudnessToOpacity } from "./audioWaveformPeaks";
|
|
7
|
+
import { ClipFadesContext, type ClipFadeShape } from "./TimelineClipFades";
|
|
6
8
|
|
|
7
9
|
export interface AudioWaveformProps {
|
|
8
10
|
audioUrl: string;
|
|
@@ -22,7 +24,10 @@ export interface AudioWaveformProps {
|
|
|
22
24
|
|
|
23
25
|
const BAR_STEP = 3;
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
export const rendersWaveform = (el: { tag: string }) => el.tag === "audio";
|
|
28
|
+
const FADE_GHOST_OPACITY = 0.27;
|
|
29
|
+
|
|
30
|
+
type BarGeometry = { x: number; width: number; height: number; gain: number };
|
|
26
31
|
|
|
27
32
|
function paintWaveformBars(
|
|
28
33
|
context: CanvasRenderingContext2D,
|
|
@@ -33,11 +38,18 @@ function paintWaveformBars(
|
|
|
33
38
|
amplitudes: readonly number[],
|
|
34
39
|
) {
|
|
35
40
|
bars.forEach((bar, index) => {
|
|
36
|
-
const
|
|
41
|
+
const opacity = loudnessToOpacity(amplitudes[index] ?? 0);
|
|
37
42
|
context.fillStyle = `rgb(${waveformBaselineRgb})`;
|
|
38
43
|
context.fillRect(bar.x, height - 2, bar.width, 2);
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
const paint = (alpha: number, top: number, barHeight: number) => {
|
|
45
|
+
context.fillStyle = `rgba(${waveformBarRgb},${alpha.toFixed(2)})`;
|
|
46
|
+
context.fillRect(bar.x, top, bar.width, barHeight);
|
|
47
|
+
};
|
|
48
|
+
const faded = bar.height * bar.gain;
|
|
49
|
+
if (faded > 0) paint(opacity, height - faded, faded);
|
|
50
|
+
if (faded < bar.height) {
|
|
51
|
+
paint(opacity * FADE_GHOST_OPACITY, height - bar.height, bar.height - faded);
|
|
52
|
+
}
|
|
41
53
|
});
|
|
42
54
|
}
|
|
43
55
|
|
|
@@ -47,6 +59,7 @@ export function drawWaveformCanvas(
|
|
|
47
59
|
muted: boolean,
|
|
48
60
|
trimStartFraction: number,
|
|
49
61
|
trimEndFraction: number,
|
|
62
|
+
fades: ClipFadeShape | null = null,
|
|
50
63
|
) {
|
|
51
64
|
const width = Math.max(1, canvas.clientWidth);
|
|
52
65
|
const height = Math.max(1, canvas.clientHeight);
|
|
@@ -67,6 +80,9 @@ export function drawWaveformCanvas(
|
|
|
67
80
|
x: (index * width) / amplitudes.length,
|
|
68
81
|
width: Math.max(1, width / amplitudes.length),
|
|
69
82
|
height: Math.max(3, amplitude * height),
|
|
83
|
+
gain: fades
|
|
84
|
+
? fadeGain(((index + 0.5) / amplitudes.length) * fades.duration, fades.duration, fades)
|
|
85
|
+
: 1,
|
|
70
86
|
}));
|
|
71
87
|
const channelToken = muted ? "--timeline-waveform-muted-rgb" : "--timeline-waveform-bar-rgb";
|
|
72
88
|
const waveformBarRgb = getComputedStyle(canvas).getPropertyValue(channelToken);
|
|
@@ -179,11 +195,12 @@ export const AudioWaveform = memo(function AudioWaveform({
|
|
|
179
195
|
const peaks =
|
|
180
196
|
snapshot.status === "ready" && snapshot.value.kind === "waveform" ? snapshot.value.peaks : null;
|
|
181
197
|
|
|
198
|
+
const fades = useContext(ClipFadesContext);
|
|
182
199
|
const draw = useCallback(() => {
|
|
183
200
|
const canvas = canvasRef.current;
|
|
184
201
|
if (!canvas || !peaks) return;
|
|
185
|
-
drawWaveformCanvas(canvas, peaks, muted, trimStartFraction ?? 0, trimEndFraction ?? 1);
|
|
186
|
-
}, [muted, peaks, trimEndFraction, trimStartFraction]);
|
|
202
|
+
drawWaveformCanvas(canvas, peaks, muted, trimStartFraction ?? 0, trimEndFraction ?? 1, fades);
|
|
203
|
+
}, [fades, muted, peaks, trimEndFraction, trimStartFraction]);
|
|
187
204
|
|
|
188
205
|
const setCanvasRef = useCallback(
|
|
189
206
|
(canvas: HTMLCanvasElement | null) => {
|
|
@@ -5,6 +5,7 @@ import { createRoot } from "react-dom/client";
|
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import type { TimelineElement } from "../store/playerStore";
|
|
7
7
|
import { TimelineClip } from "./TimelineClip";
|
|
8
|
+
import { ClipFadesContext } from "./TimelineClipFades";
|
|
8
9
|
import type { TimelineEditCapabilities } from "./timelineEditing";
|
|
9
10
|
import { defaultTimelineTheme, type TimelineTheme } from "./timelineTheme";
|
|
10
11
|
|
|
@@ -67,6 +68,43 @@ function renderClip({
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
describe("TimelineClip", () => {
|
|
71
|
+
it("hands its content the clip's fades, and none to a clip the mixer does not hear", () => {
|
|
72
|
+
const seen: unknown[] = [];
|
|
73
|
+
function Probe() {
|
|
74
|
+
seen.push(React.useContext(ClipFadesContext));
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const render = (element: TimelineElement) => {
|
|
78
|
+
const host = document.createElement("div");
|
|
79
|
+
document.body.append(host);
|
|
80
|
+
const root = createRoot(host);
|
|
81
|
+
act(() => {
|
|
82
|
+
root.render(
|
|
83
|
+
<TimelineClip
|
|
84
|
+
el={element}
|
|
85
|
+
pps={100}
|
|
86
|
+
clipY={0}
|
|
87
|
+
isSelected={false}
|
|
88
|
+
isHovered={false}
|
|
89
|
+
hasCustomContent
|
|
90
|
+
capabilities={capabilities}
|
|
91
|
+
isComposition={false}
|
|
92
|
+
onHoverStart={vi.fn()}
|
|
93
|
+
onHoverEnd={vi.fn()}
|
|
94
|
+
onClick={vi.fn()}
|
|
95
|
+
onDoubleClick={vi.fn()}
|
|
96
|
+
>
|
|
97
|
+
<Probe />
|
|
98
|
+
</TimelineClip>,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
act(() => root.unmount());
|
|
102
|
+
};
|
|
103
|
+
render({ id: "music", tag: "audio", start: 0, duration: 4, track: 0, fadeIn: 1, fadeOut: 0.5 });
|
|
104
|
+
render({ id: "title", tag: "div", start: 0, duration: 4, track: 0, fadeIn: 1 });
|
|
105
|
+
expect(seen).toEqual([{ fadeIn: 1, fadeOut: 0.5, duration: 4 }, null]);
|
|
106
|
+
});
|
|
107
|
+
|
|
70
108
|
it("renders the clip label above custom content without showing default timecode", () => {
|
|
71
109
|
const { host, root } = renderClip({
|
|
72
110
|
element: { id: "hero", label: "Hero", tag: "div", start: 1, duration: 1, track: 0 },
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
import type { TimelineEditCapabilities } from "./timelineEditing";
|
|
10
10
|
import { isAudioTimelineElement } from "../../utils/timelineInspector";
|
|
11
11
|
import { timelineClipFocusId } from "./timelineNavigationIdentity";
|
|
12
|
-
import { TimelineClipFades } from "./TimelineClipFades";
|
|
12
|
+
import { ClipFadesContext, TimelineClipFades, useClipFadeDraft } from "./TimelineClipFades";
|
|
13
|
+
import { rendersWaveform } from "./AudioWaveform";
|
|
13
14
|
|
|
14
15
|
interface TimelineClipProps {
|
|
15
16
|
el: TimelineElement;
|
|
@@ -82,6 +83,8 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
82
83
|
"--clip-handle": theme.handleColor,
|
|
83
84
|
} as CSSProperties;
|
|
84
85
|
const isAudioClip = isAudioTimelineElement(el);
|
|
86
|
+
const hasFades = (isAudioClip || Boolean(el.hasAudio)) && !isGestureActor;
|
|
87
|
+
const fade = useClipFadeDraft(el);
|
|
85
88
|
const clipClassName = [
|
|
86
89
|
"timeline-clip",
|
|
87
90
|
"absolute",
|
|
@@ -206,16 +209,21 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
206
209
|
{startLabel}-{endLabel}s
|
|
207
210
|
</span>
|
|
208
211
|
)}
|
|
209
|
-
{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
<ClipFadesContext.Provider value={hasFades ? fade.shape : null}>
|
|
213
|
+
{children}
|
|
214
|
+
</ClipFadesContext.Provider>
|
|
215
|
+
{/* Fade handles for anything the mixer hears: audio clips and videos marked
|
|
216
|
+
data-has-audio. They write data-fade-in/out, the timeline half of the
|
|
217
|
+
inspector's Fade rows. */}
|
|
218
|
+
{hasFades && (
|
|
214
219
|
<TimelineClipFades
|
|
215
220
|
el={el}
|
|
216
221
|
pps={pps}
|
|
217
222
|
widthPx={widthPx}
|
|
218
223
|
showHandles={(isHovered || isSelected) && !isDragging}
|
|
224
|
+
focusable={isSelected}
|
|
225
|
+
hasWaveform={rendersWaveform(el)}
|
|
226
|
+
fade={fade}
|
|
219
227
|
/>
|
|
220
228
|
)}
|
|
221
229
|
</button>
|