@hyperframes/studio 0.8.185 → 0.8.188
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-CoOdf435.js → hyperframes-player-DwWIKKZG.js} +1 -1
- package/dist/assets/{index-CR85xg2x.js → index-Cjwz7_13.js} +1 -1
- package/dist/assets/{index-C_6rTpaB.js → index-M8lEpS0Q.js} +131 -131
- package/dist/index.html +1 -1
- package/dist/index.js +34 -21
- package/package.json +8 -8
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +57 -22
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +29 -25
- package/src/components/editor/useVolumeAutomation.test.tsx +2 -8
- package/src/components/editor/useVolumeAutomation.ts +2 -7
- package/src/components/renders/useRenderQueue.ts +8 -0
- package/src/components/renders/useRenderQueueMotionBlur.test.tsx +61 -0
package/dist/index.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
document.documentElement.dataset.theme = "paper";
|
|
15
15
|
}
|
|
16
16
|
</script>
|
|
17
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
+
<script type="module" crossorigin src="/assets/index-M8lEpS0Q.js"></script>
|
|
18
18
|
<link rel="stylesheet" crossorigin href="/assets/index-CESvLzz0.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
package/dist/index.js
CHANGED
|
@@ -66847,7 +66847,7 @@ function useVolumeAutomation(element, currentTime, onSetAttributeQuiet, linkedSp
|
|
|
66847
66847
|
if (!linkedSpeed) return writeOwn(edit);
|
|
66848
66848
|
void linkedSpeed.commitAttribute(
|
|
66849
66849
|
HF_AUDIO_AUTOMATION_ATTR2,
|
|
66850
|
-
(
|
|
66850
|
+
(raw) => automationAttrValue(edit(readPanelAutomation(raw, void 0))) || null,
|
|
66851
66851
|
"Edit speed"
|
|
66852
66852
|
);
|
|
66853
66853
|
};
|
|
@@ -66862,10 +66862,7 @@ function useVolumeAutomation(element, currentTime, onSetAttributeQuiet, linkedSp
|
|
|
66862
66862
|
onCommitAt: (v) => writeLane((a) => withPointAt(a, target, clipTimeSec, v))
|
|
66863
66863
|
};
|
|
66864
66864
|
};
|
|
66865
|
-
const
|
|
66866
|
-
const parsed = raw ? Number(raw) : 1;
|
|
66867
|
-
const current = Number.isFinite(parsed) ? parsed : 1;
|
|
66868
|
-
const volume = laneBinding(VOLUME_TARGET3, current, writeOwn);
|
|
66865
|
+
const volume = laneBinding(VOLUME_TARGET3, 1, writeOwn);
|
|
66869
66866
|
const rateAttr = Number.parseFloat(element.dataAttributes?.["playback-rate"] ?? "");
|
|
66870
66867
|
const rate = laneBinding(
|
|
66871
66868
|
RATE_TARGET2,
|
|
@@ -67048,8 +67045,8 @@ function FlatMediaSection({
|
|
|
67048
67045
|
const isImage = element.tagName === "img";
|
|
67049
67046
|
const isVisualMedia = isVideo || isImage;
|
|
67050
67047
|
const el = element.element;
|
|
67051
|
-
const
|
|
67052
|
-
const
|
|
67048
|
+
const gain = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
|
|
67049
|
+
const volume = volumeAutomated && automatedVolumeValue !== void 0 ? automatedVolumeValue : 1;
|
|
67053
67050
|
const { mediaStart, mediaStartAttr } = readClipInPoint(element.dataAttributes);
|
|
67054
67051
|
const constantRate = Number.parseFloat(element.dataAttributes["playback-rate"] ?? "1") || 1;
|
|
67055
67052
|
const playbackRate = rate?.automated && rate.automatedValue !== void 0 ? rate.automatedValue : constantRate;
|
|
@@ -67183,6 +67180,25 @@ function FlatMediaSection({
|
|
|
67183
67180
|
] })
|
|
67184
67181
|
] }),
|
|
67185
67182
|
(isVideo || isAudio) && /* @__PURE__ */ jsxs134(Fragment48, { children: [
|
|
67183
|
+
/* @__PURE__ */ jsx176(
|
|
67184
|
+
FlatSlider,
|
|
67185
|
+
{
|
|
67186
|
+
label: "Gain",
|
|
67187
|
+
value: audioGainToFaderPosition3(gain),
|
|
67188
|
+
min: AUDIO_GAIN_FADER_MIN3,
|
|
67189
|
+
max: AUDIO_GAIN_FADER_MAX3,
|
|
67190
|
+
tier: gain === 1 ? "default" : "explicitCustom",
|
|
67191
|
+
displayValue: audioGainToText3(gain),
|
|
67192
|
+
centerTick: true,
|
|
67193
|
+
onCommit: (next) => void onSetAttribute("volume", formatAudioGain4(audioFaderPositionToGain3(next))),
|
|
67194
|
+
onCommitText: (text) => {
|
|
67195
|
+
const next = parseGainInput(text);
|
|
67196
|
+
if (next === null) return false;
|
|
67197
|
+
void onSetAttribute("volume", formatAudioGain4(next));
|
|
67198
|
+
return true;
|
|
67199
|
+
}
|
|
67200
|
+
}
|
|
67201
|
+
),
|
|
67186
67202
|
/* @__PURE__ */ jsxs134(
|
|
67187
67203
|
"div",
|
|
67188
67204
|
{
|
|
@@ -67193,25 +67209,17 @@ function FlatMediaSection({
|
|
|
67193
67209
|
FlatSlider,
|
|
67194
67210
|
{
|
|
67195
67211
|
label: "Volume",
|
|
67196
|
-
value:
|
|
67212
|
+
value: audioGainToFaderPosition3(volume),
|
|
67197
67213
|
min: AUDIO_GAIN_FADER_MIN3,
|
|
67198
67214
|
max: AUDIO_GAIN_FADER_MAX3,
|
|
67199
67215
|
tier: volume === 1 ? "default" : "explicitCustom",
|
|
67200
67216
|
displayValue: audioGainToText3(volume),
|
|
67201
67217
|
centerTick: true,
|
|
67202
|
-
onCommit: (next) =>
|
|
67203
|
-
const gain = audioFaderPositionToGain3(next);
|
|
67204
|
-
if (volumeAutomated) {
|
|
67205
|
-
onCommitVolumeAt?.(gain);
|
|
67206
|
-
} else {
|
|
67207
|
-
void onSetAttribute("volume", formatAudioGain4(gain));
|
|
67208
|
-
}
|
|
67209
|
-
},
|
|
67218
|
+
onCommit: (next) => onCommitVolumeAt?.(audioFaderPositionToGain3(next)),
|
|
67210
67219
|
onCommitText: (text) => {
|
|
67211
|
-
const
|
|
67212
|
-
if (
|
|
67213
|
-
|
|
67214
|
-
else void onSetAttribute("volume", formatAudioGain4(gain));
|
|
67220
|
+
const next = parseGainInput(text);
|
|
67221
|
+
if (next === null) return false;
|
|
67222
|
+
onCommitVolumeAt?.(next);
|
|
67215
67223
|
return true;
|
|
67216
67224
|
}
|
|
67217
67225
|
}
|
|
@@ -72983,6 +72991,7 @@ function useRightPanelIntent() {
|
|
|
72983
72991
|
}
|
|
72984
72992
|
|
|
72985
72993
|
// src/components/renders/useRenderQueue.ts
|
|
72994
|
+
import { htmlInCanvasForm } from "@hyperframes/core/html-in-canvas";
|
|
72986
72995
|
import { useState as useState109, useEffect as useEffect106, useCallback as useCallback95, useRef as useRef119, useMemo as useMemo69 } from "react";
|
|
72987
72996
|
|
|
72988
72997
|
// src/components/renders/serverError.ts
|
|
@@ -73206,6 +73215,9 @@ function markFeedbackDismissed() {
|
|
|
73206
73215
|
}
|
|
73207
73216
|
|
|
73208
73217
|
// src/components/renders/useRenderQueue.ts
|
|
73218
|
+
function canPreviewMotionBlur() {
|
|
73219
|
+
return typeof document !== "undefined" && htmlInCanvasForm() !== null;
|
|
73220
|
+
}
|
|
73209
73221
|
function hiddenIdsKey(projectId) {
|
|
73210
73222
|
return `hf-studio-hidden-renders:${projectId}`;
|
|
73211
73223
|
}
|
|
@@ -73320,7 +73332,8 @@ function useRenderQueue(projectId, activeCompPathRef) {
|
|
|
73320
73332
|
const body = {
|
|
73321
73333
|
fps,
|
|
73322
73334
|
quality,
|
|
73323
|
-
format
|
|
73335
|
+
format,
|
|
73336
|
+
previewMotionBlur: canPreviewMotionBlur()
|
|
73324
73337
|
};
|
|
73325
73338
|
if (browserTelemetryAllowed()) {
|
|
73326
73339
|
body.telemetryDistinctId = getAnonymousId();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.188",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"dockview-react": "8.3.1",
|
|
60
60
|
"gsap": "^3.13.0",
|
|
61
61
|
"mediabunny": "^1.45.3",
|
|
62
|
-
"@hyperframes/
|
|
63
|
-
"@hyperframes/
|
|
64
|
-
"@hyperframes/shader-transitions": "0.8.
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/sdk": "0.8.
|
|
62
|
+
"@hyperframes/player": "0.8.188",
|
|
63
|
+
"@hyperframes/core": "0.8.188",
|
|
64
|
+
"@hyperframes/shader-transitions": "0.8.188",
|
|
65
|
+
"@hyperframes/studio-server": "0.8.188",
|
|
66
|
+
"@hyperframes/parsers": "0.8.188",
|
|
67
|
+
"@hyperframes/sdk": "0.8.188"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.188"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -9,6 +9,7 @@ import type { DomEditSelection } from "./domEditing";
|
|
|
9
9
|
import type { PatchOperation } from "../../utils/sourcePatcher";
|
|
10
10
|
import { formatTimingValue } from "./propertyPanelHelpers";
|
|
11
11
|
import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration";
|
|
12
|
+
import { audioGainToFaderPosition } from "@hyperframes/core/audio-gain";
|
|
12
13
|
|
|
13
14
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
14
15
|
|
|
@@ -163,7 +164,7 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
163
164
|
act(() => root.unmount());
|
|
164
165
|
});
|
|
165
166
|
|
|
166
|
-
it("commits +12 dB of boost from the upper half of the
|
|
167
|
+
it("commits +12 dB of boost from the upper half of the gain fader", () => {
|
|
167
168
|
const onSetAttribute = vi.fn();
|
|
168
169
|
const element = makeVideoElement({ dataAttributes: { volume: "1" } });
|
|
169
170
|
const host = document.createElement("div");
|
|
@@ -182,7 +183,7 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
182
183
|
/>,
|
|
183
184
|
);
|
|
184
185
|
});
|
|
185
|
-
const volumeTrack = host.
|
|
186
|
+
const volumeTrack = host.querySelector('[data-flat-slider-track="true"][aria-label="Gain"]')!;
|
|
186
187
|
Object.defineProperty(volumeTrack, "getBoundingClientRect", {
|
|
187
188
|
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
|
188
189
|
});
|
|
@@ -197,10 +198,8 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
197
198
|
act(() => root.unmount());
|
|
198
199
|
});
|
|
199
200
|
|
|
200
|
-
it("
|
|
201
|
-
const
|
|
202
|
-
const onCommitVolumeAt = vi.fn();
|
|
203
|
-
const element = makeVideoElement({ dataAttributes: { volume: "1" } });
|
|
201
|
+
it("shows the clip's gain and its lane's level as two sliders", () => {
|
|
202
|
+
const element = makeVideoElement({ dataAttributes: { volume: "0.5" } });
|
|
204
203
|
const host = document.createElement("div");
|
|
205
204
|
document.body.append(host);
|
|
206
205
|
const root = createRoot(host);
|
|
@@ -211,28 +210,62 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
211
210
|
element={element}
|
|
212
211
|
styles={{}}
|
|
213
212
|
onSetStyle={vi.fn()}
|
|
214
|
-
onSetAttribute={
|
|
213
|
+
onSetAttribute={vi.fn()}
|
|
215
214
|
onSetHtmlAttribute={vi.fn()}
|
|
216
215
|
onSetAttributeBatch={vi.fn()}
|
|
217
216
|
volumeAutomated
|
|
218
|
-
|
|
217
|
+
automatedVolumeValue={0.25}
|
|
219
218
|
/>,
|
|
220
219
|
);
|
|
221
220
|
});
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
act(() => {
|
|
227
|
-
volumeTrack.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 100 }));
|
|
228
|
-
volumeTrack.dispatchEvent(new MouseEvent("pointerup", { bubbles: true, clientX: 100 }));
|
|
229
|
-
});
|
|
230
|
-
expect(onCommitVolumeAt).toHaveBeenCalledTimes(1);
|
|
231
|
-
expect(onCommitVolumeAt.mock.calls[0][0]).toBeCloseTo(3.981072, 6);
|
|
232
|
-
expect(onSetAttribute).not.toHaveBeenCalledWith("volume", expect.anything());
|
|
221
|
+
const [gain, volume] = host.querySelectorAll('[data-flat-slider-track="true"]');
|
|
222
|
+
expect(labelsOf(host).slice(0, 2)).toEqual(["Gain", "Volume"]);
|
|
223
|
+
expect(gain?.getAttribute("aria-valuenow")).toBe(String(audioGainToFaderPosition(0.5)));
|
|
224
|
+
expect(volume?.getAttribute("aria-valuenow")).toBe(String(audioGainToFaderPosition(0.25)));
|
|
233
225
|
act(() => root.unmount());
|
|
234
226
|
});
|
|
235
227
|
|
|
228
|
+
it.each([true, false])(
|
|
229
|
+
"writes Volume through the lane, never data-volume (automated: %s)",
|
|
230
|
+
(automated) => {
|
|
231
|
+
const onSetAttribute = vi.fn();
|
|
232
|
+
const onCommitVolumeAt = vi.fn();
|
|
233
|
+
const element = makeVideoElement({ dataAttributes: { volume: "1" } });
|
|
234
|
+
const host = document.createElement("div");
|
|
235
|
+
document.body.append(host);
|
|
236
|
+
const root = createRoot(host);
|
|
237
|
+
act(() => {
|
|
238
|
+
root.render(
|
|
239
|
+
<FlatMediaSection
|
|
240
|
+
projectDir={null}
|
|
241
|
+
element={element}
|
|
242
|
+
styles={{}}
|
|
243
|
+
onSetStyle={vi.fn()}
|
|
244
|
+
onSetAttribute={onSetAttribute}
|
|
245
|
+
onSetHtmlAttribute={vi.fn()}
|
|
246
|
+
onSetAttributeBatch={vi.fn()}
|
|
247
|
+
volumeAutomated={automated}
|
|
248
|
+
onCommitVolumeAt={onCommitVolumeAt}
|
|
249
|
+
/>,
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
const volumeTrack = host.querySelector(
|
|
253
|
+
'[data-flat-slider-track="true"][aria-label="Volume"]',
|
|
254
|
+
)!;
|
|
255
|
+
Object.defineProperty(volumeTrack, "getBoundingClientRect", {
|
|
256
|
+
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
|
257
|
+
});
|
|
258
|
+
act(() => {
|
|
259
|
+
volumeTrack.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 100 }));
|
|
260
|
+
volumeTrack.dispatchEvent(new MouseEvent("pointerup", { bubbles: true, clientX: 100 }));
|
|
261
|
+
});
|
|
262
|
+
expect(onCommitVolumeAt).toHaveBeenCalledTimes(1);
|
|
263
|
+
expect(onCommitVolumeAt.mock.calls[0][0]).toBeCloseTo(3.981072, 6);
|
|
264
|
+
expect(onSetAttribute).not.toHaveBeenCalledWith("volume", expect.anything());
|
|
265
|
+
act(() => root.unmount());
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
|
|
236
269
|
it("commits a new rate value on slider track pointerdown", () => {
|
|
237
270
|
const onSetAttribute = vi.fn();
|
|
238
271
|
const element = makeVideoElement();
|
|
@@ -252,7 +285,7 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
252
285
|
/>,
|
|
253
286
|
);
|
|
254
287
|
});
|
|
255
|
-
const rateTrack = host.
|
|
288
|
+
const rateTrack = host.querySelector('[data-flat-slider-track="true"][aria-label="Speed"]')!;
|
|
256
289
|
Object.defineProperty(rateTrack, "getBoundingClientRect", {
|
|
257
290
|
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
|
258
291
|
});
|
|
@@ -284,7 +317,9 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
284
317
|
/>,
|
|
285
318
|
);
|
|
286
319
|
});
|
|
287
|
-
const mediaStartTrack = host.
|
|
320
|
+
const mediaStartTrack = host.querySelector(
|
|
321
|
+
'[data-flat-slider-track="true"][aria-label="Media start"]',
|
|
322
|
+
)!;
|
|
288
323
|
Object.defineProperty(mediaStartTrack, "getBoundingClientRect", {
|
|
289
324
|
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
|
290
325
|
});
|
|
@@ -465,7 +500,7 @@ describe("FlatMediaSection — audio clips", () => {
|
|
|
465
500
|
it("shows Fade in / Fade out rows for audio, and for video only when it carries audio", () => {
|
|
466
501
|
const audio = renderWithRate(makeAudioElement());
|
|
467
502
|
expect(labelsOf(audio.host)).toEqual(
|
|
468
|
-
expect.arrayContaining(["Volume", "Speed", "Media start", "Fade in", "Fade out"]),
|
|
503
|
+
expect.arrayContaining(["Gain", "Volume", "Speed", "Media start", "Fade in", "Fade out"]),
|
|
469
504
|
);
|
|
470
505
|
act(() => audio.root.unmount());
|
|
471
506
|
|
|
@@ -64,7 +64,7 @@ export function FlatMediaSection({
|
|
|
64
64
|
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
|
65
65
|
onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
|
|
66
66
|
onSetAttributeBatch: CommitDomAttributeBatch;
|
|
67
|
-
/** A volume lane in the timeline drives the level; the slider writes a keyframe
|
|
67
|
+
/** A volume lane in the timeline drives the level; the Volume slider writes a keyframe. */
|
|
68
68
|
volumeAutomated?: boolean;
|
|
69
69
|
onAutomateVolume?: () => void;
|
|
70
70
|
onRemoveVolumeAutomation?: () => void;
|
|
@@ -88,14 +88,10 @@ export function FlatMediaSection({
|
|
|
88
88
|
const isVisualMedia = isVideo || isImage;
|
|
89
89
|
const el = element.element;
|
|
90
90
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
const volume =
|
|
95
|
-
volumeAutomated && automatedVolumeValue !== undefined
|
|
96
|
-
? automatedVolumeValue
|
|
97
|
-
: (parseNumericValue(element.dataAttributes.volume ?? "") ?? 1);
|
|
98
|
-
const volumeFaderPosition = audioGainToFaderPosition(volume);
|
|
91
|
+
// Gain is `data-volume`, ahead of the effects; Volume is the lane's level at the
|
|
92
|
+
// playhead, unity without one. What plays is gain times level, as in an editor.
|
|
93
|
+
const gain = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
|
|
94
|
+
const volume = volumeAutomated && automatedVolumeValue !== undefined ? automatedVolumeValue : 1;
|
|
99
95
|
const { mediaStart, mediaStartAttr } = readClipInPoint(element.dataAttributes);
|
|
100
96
|
const constantRate = Number.parseFloat(element.dataAttributes["playback-rate"] ?? "1") || 1;
|
|
101
97
|
const playbackRate =
|
|
@@ -246,9 +242,25 @@ export function FlatMediaSection({
|
|
|
246
242
|
)}
|
|
247
243
|
{(isVideo || isAudio) && (
|
|
248
244
|
<>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
245
|
+
<FlatSlider
|
|
246
|
+
label="Gain"
|
|
247
|
+
value={audioGainToFaderPosition(gain)}
|
|
248
|
+
min={AUDIO_GAIN_FADER_MIN}
|
|
249
|
+
max={AUDIO_GAIN_FADER_MAX}
|
|
250
|
+
tier={gain === 1 ? "default" : "explicitCustom"}
|
|
251
|
+
displayValue={audioGainToText(gain)}
|
|
252
|
+
centerTick
|
|
253
|
+
onCommit={(next) =>
|
|
254
|
+
void onSetAttribute("volume", formatAudioGain(audioFaderPositionToGain(next)))
|
|
255
|
+
}
|
|
256
|
+
onCommitText={(text) => {
|
|
257
|
+
const next = parseGainInput(text);
|
|
258
|
+
if (next === null) return false;
|
|
259
|
+
void onSetAttribute("volume", formatAudioGain(next));
|
|
260
|
+
return true;
|
|
261
|
+
}}
|
|
262
|
+
/>
|
|
263
|
+
{/* A commit writes a keyframe at the playhead, starting a lane when there is none. */}
|
|
252
264
|
<div
|
|
253
265
|
className="hf-volume-row flex items-center gap-1"
|
|
254
266
|
data-volume-automated={volumeAutomated ? "" : undefined}
|
|
@@ -256,25 +268,17 @@ export function FlatMediaSection({
|
|
|
256
268
|
<div className="min-w-0 flex-1">
|
|
257
269
|
<FlatSlider
|
|
258
270
|
label="Volume"
|
|
259
|
-
value={
|
|
271
|
+
value={audioGainToFaderPosition(volume)}
|
|
260
272
|
min={AUDIO_GAIN_FADER_MIN}
|
|
261
273
|
max={AUDIO_GAIN_FADER_MAX}
|
|
262
274
|
tier={volume === 1 ? "default" : "explicitCustom"}
|
|
263
275
|
displayValue={audioGainToText(volume)}
|
|
264
276
|
centerTick
|
|
265
|
-
onCommit={(next) =>
|
|
266
|
-
const gain = audioFaderPositionToGain(next);
|
|
267
|
-
if (volumeAutomated) {
|
|
268
|
-
onCommitVolumeAt?.(gain);
|
|
269
|
-
} else {
|
|
270
|
-
void onSetAttribute("volume", formatAudioGain(gain));
|
|
271
|
-
}
|
|
272
|
-
}}
|
|
277
|
+
onCommit={(next) => onCommitVolumeAt?.(audioFaderPositionToGain(next))}
|
|
273
278
|
onCommitText={(text) => {
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
else void onSetAttribute("volume", formatAudioGain(gain));
|
|
279
|
+
const next = parseGainInput(text);
|
|
280
|
+
if (next === null) return false;
|
|
281
|
+
onCommitVolumeAt?.(next);
|
|
278
282
|
return true;
|
|
279
283
|
}}
|
|
280
284
|
/>
|
|
@@ -59,22 +59,16 @@ describe("useVolumeAutomation", () => {
|
|
|
59
59
|
expect(bind({ volume: "0.55", automation }).binding.volumeAutomated).toBe(false);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it("seeds a new lane at
|
|
62
|
+
it("seeds a new lane at unity, which data-volume then multiplies", () => {
|
|
63
63
|
// Automating a track must not change how loud it is.
|
|
64
64
|
const { binding, onSetAttributeQuiet } = bind({ volume: "0.55" });
|
|
65
65
|
act(() => binding.onAutomateVolume());
|
|
66
66
|
expect(onSetAttributeQuiet).toHaveBeenCalledWith(
|
|
67
67
|
"data-automation",
|
|
68
|
-
JSON.stringify({ version: 1, lanes: [{ target: "volume", points: [{ t: 0, v:
|
|
68
|
+
JSON.stringify({ version: 1, lanes: [{ target: "volume", points: [{ t: 0, v: 1 }] }] }),
|
|
69
69
|
);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
it("treats a missing data-volume as unity", () => {
|
|
73
|
-
const { binding, onSetAttributeQuiet } = bind({});
|
|
74
|
-
act(() => binding.onAutomateVolume());
|
|
75
|
-
expect(writtenAutomation(onSetAttributeQuiet).lanes[0].points[0].v).toBe(1);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
72
|
it("keeps FX lanes when adding the volume one", () => {
|
|
79
73
|
const automation = JSON.stringify({
|
|
80
74
|
version: 1,
|
|
@@ -108,13 +108,8 @@ export function useVolumeAutomation(
|
|
|
108
108
|
onCommitAt: (v: number) => writeLane((a) => withPointAt(a, target, clipTimeSec, v)),
|
|
109
109
|
};
|
|
110
110
|
};
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
// empty value as unity.
|
|
114
|
-
const raw = element.dataAttributes?.["volume"];
|
|
115
|
-
const parsed = raw ? Number(raw) : 1;
|
|
116
|
-
const current = Number.isFinite(parsed) ? parsed : 1;
|
|
117
|
-
const volume = laneBinding(VOLUME_TARGET, current, writeOwn);
|
|
111
|
+
// Unity: `data-volume` multiplies the lane, so seeding it there would play it twice.
|
|
112
|
+
const volume = laneBinding(VOLUME_TARGET, 1, writeOwn);
|
|
118
113
|
const rateAttr = Number.parseFloat(element.dataAttributes?.["playback-rate"] ?? "");
|
|
119
114
|
const rate = laneBinding(
|
|
120
115
|
RATE_TARGET,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { htmlInCanvasForm } from "@hyperframes/core/html-in-canvas";
|
|
1
2
|
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
2
3
|
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
3
4
|
import type { CanvasResolution } from "@hyperframes/parsers";
|
|
@@ -10,6 +11,11 @@ import { ffmpegInstallMessage, useFfmpegStatus } from "./useFfmpegStatus";
|
|
|
10
11
|
import { requestStudioFeedback, type FeedbackContext } from "../feedback/feedbackTrigger";
|
|
11
12
|
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
12
13
|
|
|
14
|
+
// WYSIWYG: the preview iframe runs in this browser, so a render blurs only if this one can.
|
|
15
|
+
function canPreviewMotionBlur(): boolean {
|
|
16
|
+
return typeof document !== "undefined" && htmlInCanvasForm() !== null;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
export interface RenderJob {
|
|
14
20
|
id: string;
|
|
15
21
|
status: "rendering" | "complete" | "failed" | "cancelled";
|
|
@@ -238,10 +244,12 @@ export function useRenderQueue(
|
|
|
238
244
|
gpu?: boolean;
|
|
239
245
|
telemetryDistinctId?: string;
|
|
240
246
|
telemetryOptOut?: boolean;
|
|
247
|
+
previewMotionBlur: boolean;
|
|
241
248
|
} = {
|
|
242
249
|
fps,
|
|
243
250
|
quality,
|
|
244
251
|
format,
|
|
252
|
+
previewMotionBlur: canPreviewMotionBlur(),
|
|
245
253
|
};
|
|
246
254
|
// The id is MINTED by getAnonymousId(), so calling it unconditionally
|
|
247
255
|
// created a telemetry identity for a profile that had opted out — and
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// WYSIWYG: a render only blurs what the Studio preview could show, and only the browser knows
|
|
4
|
+
// whether it has html-in-canvas. The request body is how it tells the server.
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { startRenderAndReadBody, type MountedQueue } from "./renderQueueTestHarness";
|
|
8
|
+
|
|
9
|
+
vi.mock("../../telemetry/events", () => ({ trackStudioRenderStart: vi.fn() }));
|
|
10
|
+
|
|
11
|
+
const { useRenderQueue } = await import("./useRenderQueue");
|
|
12
|
+
|
|
13
|
+
let queue: MountedQueue | null = null;
|
|
14
|
+
|
|
15
|
+
async function startRenderBody(): Promise<Record<string, unknown>> {
|
|
16
|
+
const started = await startRenderAndReadBody(useRenderQueue, {
|
|
17
|
+
opts: { fps: 30, quality: "standard", format: "mp4" },
|
|
18
|
+
});
|
|
19
|
+
queue = started.queue;
|
|
20
|
+
return started.body;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stubHtmlInCanvas(members: string[]): void {
|
|
24
|
+
vi.stubGlobal(
|
|
25
|
+
"CanvasRenderingContext2D",
|
|
26
|
+
class {
|
|
27
|
+
drawElementImage(): void {}
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
const proto = HTMLCanvasElement.prototype as unknown as Record<string, unknown>;
|
|
31
|
+
for (const name of ["requestPaint", ...members]) proto[name] = () => {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
queue?.unmount();
|
|
36
|
+
queue = null;
|
|
37
|
+
document.body.innerHTML = "";
|
|
38
|
+
for (const name of ["requestPaint", "layoutSubtree", "updateElementGeometry"]) {
|
|
39
|
+
delete (HTMLCanvasElement.prototype as unknown as Record<string, unknown>)[name];
|
|
40
|
+
}
|
|
41
|
+
vi.unstubAllGlobals();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("render request previewMotionBlur", () => {
|
|
45
|
+
it("is false in a browser without html-in-canvas", async () => {
|
|
46
|
+
expect((await startRenderBody())["previewMotionBlur"]).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it.each([
|
|
50
|
+
["the layoutsubtree form", "layoutSubtree"],
|
|
51
|
+
["the content=drawable form", "updateElementGeometry"],
|
|
52
|
+
])("is true in a browser with html-in-canvas in %s", async (_name, member) => {
|
|
53
|
+
stubHtmlInCanvas([member]);
|
|
54
|
+
expect((await startRenderBody())["previewMotionBlur"]).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("is false when html-in-canvas has neither known form", async () => {
|
|
58
|
+
stubHtmlInCanvas([]);
|
|
59
|
+
expect((await startRenderBody())["previewMotionBlur"]).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|