@hyperframes/studio 0.8.193 → 0.8.195
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-BMcUdmDb.js → hyperframes-player-DnIPEARk.js} +1 -1
- package/dist/assets/{index-Ci6GuloR.js → index-CVppCm_X.js} +1 -1
- package/dist/assets/{index-Ca-mgTnS.js → index-Dkt4c1MV.js} +161 -161
- package/dist/index.html +1 -1
- package/dist/index.js +26 -12
- package/package.json +8 -8
- package/src/components/editor/useLayerRevealOverride.test.ts +28 -0
- package/src/components/editor/useLayerRevealOverride.ts +12 -7
- package/src/components/panels/MotionBlurPanel.test.tsx +12 -3
- package/src/components/panels/motionBlurPanelModel.test.ts +13 -3
- package/src/components/panels/motionBlurPanelModel.ts +4 -2
- package/src/hooks/useGestureRecording.test.tsx +42 -0
- package/src/hooks/useGestureRecording.ts +6 -1
- package/src/utils/gsapSoftReload.test.ts +61 -0
- package/src/utils/gsapSoftReload.ts +10 -3
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-Dkt4c1MV.js"></script>
|
|
18
18
|
<link rel="stylesheet" crossorigin href="/assets/index-CYbvOVp-.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
package/dist/index.js
CHANGED
|
@@ -17608,7 +17608,7 @@ async function writeProjectFilesWithHistoryInQueue({
|
|
|
17608
17608
|
|
|
17609
17609
|
// src/utils/gsapSoftReload.ts
|
|
17610
17610
|
import { COLOR_GRADING_SOURCE_HIDDEN_ATTR as COLOR_GRADING_SOURCE_HIDDEN_ATTR2 } from "@hyperframes/core/color-grading";
|
|
17611
|
-
import { motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
|
|
17611
|
+
import { GSAP_BASE_URL_META, motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
|
|
17612
17612
|
|
|
17613
17613
|
// src/utils/authoredSource.ts
|
|
17614
17614
|
import { ensureHfIds, isCompositionTemplate } from "@hyperframes/parsers/hf-ids";
|
|
@@ -17870,6 +17870,12 @@ function fileDocs(ownFile, written, nestedFiles) {
|
|
|
17870
17870
|
|
|
17871
17871
|
// src/utils/gsapSoftReload.ts
|
|
17872
17872
|
var pendingReadyRetry = null;
|
|
17873
|
+
function previewMotionPathUrl(doc, version) {
|
|
17874
|
+
const baseUrl = doc.head.querySelector(
|
|
17875
|
+
`meta[name="${GSAP_BASE_URL_META}"]`
|
|
17876
|
+
)?.content;
|
|
17877
|
+
return motionPathPluginUrl(version, baseUrl);
|
|
17878
|
+
}
|
|
17873
17879
|
function ensureMotionPathPluginLoaded(iframe) {
|
|
17874
17880
|
if (pendingReadyRetry) window.removeEventListener("message", pendingReadyRetry);
|
|
17875
17881
|
pendingReadyRetry = null;
|
|
@@ -17900,7 +17906,7 @@ function ensureMotionPathPluginLoaded(iframe) {
|
|
|
17900
17906
|
try {
|
|
17901
17907
|
win.__hfMotionPathPluginLoading = true;
|
|
17902
17908
|
const pluginScript = doc.createElement("script");
|
|
17903
|
-
pluginScript.src =
|
|
17909
|
+
pluginScript.src = previewMotionPathUrl(doc, win.gsap?.version);
|
|
17904
17910
|
const finalize = () => {
|
|
17905
17911
|
win.__hfMotionPathPluginLoading = false;
|
|
17906
17912
|
try {
|
|
@@ -18086,7 +18092,7 @@ function applySoftReload(iframe, scriptText, options = {}) {
|
|
|
18086
18092
|
}
|
|
18087
18093
|
win.__hfMotionPathPluginLoading = true;
|
|
18088
18094
|
const pluginScript = doc.createElement("script");
|
|
18089
|
-
pluginScript.src =
|
|
18095
|
+
pluginScript.src = previewMotionPathUrl(doc, win.gsap?.version);
|
|
18090
18096
|
pluginScript.onload = () => {
|
|
18091
18097
|
win.__hfMotionPathPluginLoading = false;
|
|
18092
18098
|
executeScript();
|
|
@@ -43350,6 +43356,7 @@ import { useCallback as useCallback61 } from "react";
|
|
|
43350
43356
|
|
|
43351
43357
|
// src/components/editor/useLayerRevealOverride.ts
|
|
43352
43358
|
import { useCallback as useCallback60, useEffect as useEffect56, useRef as useRef73 } from "react";
|
|
43359
|
+
import { revealClip } from "@hyperframes/core/runtime/clip-reveal";
|
|
43353
43360
|
var LAYER_REVEAL_LIFT_Z = "2147483000";
|
|
43354
43361
|
var LAYER_REVEAL_PENDING_COMMIT_ATTR = "data-hf-studio-reveal-pending-commit";
|
|
43355
43362
|
var revealCommitSequence = 0;
|
|
@@ -43430,9 +43437,10 @@ function rollbackLayerRevealCommit(element, ownership) {
|
|
|
43430
43437
|
}
|
|
43431
43438
|
}
|
|
43432
43439
|
function restoreIfOurs(el, property, applied, prior) {
|
|
43433
|
-
if (applied == null) return;
|
|
43434
|
-
if (el.style.getPropertyValue(property) !== applied) return;
|
|
43440
|
+
if (applied == null) return false;
|
|
43441
|
+
if (el.style.getPropertyValue(property) !== applied) return false;
|
|
43435
43442
|
restoreInline(el, property, prior);
|
|
43443
|
+
return true;
|
|
43436
43444
|
}
|
|
43437
43445
|
function readHideSignals(el, win) {
|
|
43438
43446
|
const computed = win.getComputedStyle(el);
|
|
@@ -43450,20 +43458,21 @@ function revealNode(el, win, needs) {
|
|
|
43450
43458
|
opacity: el.style.opacity
|
|
43451
43459
|
};
|
|
43452
43460
|
const applied = {};
|
|
43461
|
+
let putMarkBack;
|
|
43453
43462
|
if (needs.display) {
|
|
43454
43463
|
el.style.removeProperty("display");
|
|
43455
43464
|
if (win.getComputedStyle(el).display === "none") el.style.display = "block";
|
|
43456
43465
|
applied.display = el.style.display;
|
|
43457
43466
|
}
|
|
43458
43467
|
if (needs.visibility) {
|
|
43459
|
-
|
|
43468
|
+
putMarkBack = revealClip(el);
|
|
43460
43469
|
applied.visibility = "visible";
|
|
43461
43470
|
}
|
|
43462
43471
|
if (needs.opacity) {
|
|
43463
43472
|
el.style.opacity = "1";
|
|
43464
43473
|
applied.opacity = "1";
|
|
43465
43474
|
}
|
|
43466
|
-
return { element: el, priors, applied };
|
|
43475
|
+
return { element: el, priors, applied, putMarkBack };
|
|
43467
43476
|
}
|
|
43468
43477
|
function revealHiddenChain(element) {
|
|
43469
43478
|
const doc = element.ownerDocument;
|
|
@@ -43532,10 +43541,11 @@ function useLayerRevealOverride({
|
|
|
43532
43541
|
const state = stateRef.current;
|
|
43533
43542
|
stateRef.current = null;
|
|
43534
43543
|
if (!state) return;
|
|
43535
|
-
for (const { element, priors, applied } of state.nodes) {
|
|
43544
|
+
for (const { element, priors, applied, putMarkBack } of state.nodes) {
|
|
43536
43545
|
if (!element.isConnected) continue;
|
|
43537
43546
|
restoreIfOurs(element, "display", applied.display, priors.display);
|
|
43538
|
-
restoreIfOurs(element, "visibility", applied.visibility, priors.visibility)
|
|
43547
|
+
if (restoreIfOurs(element, "visibility", applied.visibility, priors.visibility))
|
|
43548
|
+
putMarkBack?.();
|
|
43539
43549
|
restoreIfOurs(element, "opacity", applied.opacity, priors.opacity);
|
|
43540
43550
|
}
|
|
43541
43551
|
if (state.lift && state.base.isConnected) restoreLiftedElement(state.base, state.lift);
|
|
@@ -90880,6 +90890,7 @@ var StudioHeader = memo46(function StudioHeader2({
|
|
|
90880
90890
|
import { useState as useState136, useCallback as useCallback164, useRef as useRef169, useEffect as useEffect136 } from "react";
|
|
90881
90891
|
|
|
90882
90892
|
// src/hooks/useGestureRecording.ts
|
|
90893
|
+
import { revealClip as revealClip2 } from "@hyperframes/core/runtime/clip-reveal";
|
|
90883
90894
|
import { useCallback as useCallback163, useEffect as useEffect135, useRef as useRef168, useState as useState135 } from "react";
|
|
90884
90895
|
function readBasePosition(element, iframeEl) {
|
|
90885
90896
|
let baseOpacity = 1;
|
|
@@ -90920,7 +90931,8 @@ function connectGsapRuntime(element, iframeEl, elementEndTime) {
|
|
|
90920
90931
|
startTime: win.__player?.getTime() ?? 0,
|
|
90921
90932
|
maxSeekTime: elementEndTime != null && elementEndTime < tlDuration ? elementEndTime : tlDuration,
|
|
90922
90933
|
savedVisibility: element.style.visibility,
|
|
90923
|
-
savedTranslate: element.style.getPropertyValue("translate")
|
|
90934
|
+
savedTranslate: element.style.getPropertyValue("translate"),
|
|
90935
|
+
putMarkBack: null
|
|
90924
90936
|
};
|
|
90925
90937
|
}
|
|
90926
90938
|
} catch {
|
|
@@ -90932,7 +90944,8 @@ function applyRuntimePreview(runtime, time, properties) {
|
|
|
90932
90944
|
runtime.timeline.seek(seekTime);
|
|
90933
90945
|
runtime.element.style.setProperty("translate", "none");
|
|
90934
90946
|
runtime.gsap.set(runtime.element, { ...properties });
|
|
90935
|
-
runtime.element
|
|
90947
|
+
const putMarkBack = revealClip2(runtime.element);
|
|
90948
|
+
runtime.putMarkBack ??= putMarkBack;
|
|
90936
90949
|
liveTime.notify(seekTime);
|
|
90937
90950
|
usePlayerStore.getState().setCurrentTime(seekTime);
|
|
90938
90951
|
}
|
|
@@ -91008,6 +91021,7 @@ function releaseRuntimePreview(r) {
|
|
|
91008
91021
|
if (!runtime) return;
|
|
91009
91022
|
const { element, savedVisibility, savedTranslate } = runtime;
|
|
91010
91023
|
element.style.visibility = savedVisibility;
|
|
91024
|
+
runtime.putMarkBack?.();
|
|
91011
91025
|
element.style.setProperty("translate", savedTranslate || "");
|
|
91012
91026
|
try {
|
|
91013
91027
|
runtime.gsap.set(runtime.element, {
|
|
@@ -98684,7 +98698,7 @@ function levelOf(settings) {
|
|
|
98684
98698
|
return settings.shutterDeg > STRONG_ABOVE ? "strong" : "subtle";
|
|
98685
98699
|
}
|
|
98686
98700
|
function patchForLevel(level) {
|
|
98687
|
-
return level === "off" ? { enabled: false } : { enabled: true, ...LEVEL_SETTINGS[level] };
|
|
98701
|
+
return level === "off" ? { enabled: false, events: false } : { enabled: true, events: true, ...LEVEL_SETTINGS[level] };
|
|
98688
98702
|
}
|
|
98689
98703
|
|
|
98690
98704
|
// src/components/panels/MotionBlurPanel.tsx
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.195",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"dockview-react": "8.3.1",
|
|
61
61
|
"gsap": "^3.13.0",
|
|
62
62
|
"mediabunny": "^1.45.3",
|
|
63
|
-
"@hyperframes/
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/core": "0.8.195",
|
|
64
|
+
"@hyperframes/parsers": "0.8.195",
|
|
65
|
+
"@hyperframes/sdk": "0.8.195",
|
|
66
|
+
"@hyperframes/player": "0.8.195",
|
|
67
|
+
"@hyperframes/shader-transitions": "0.8.195",
|
|
68
|
+
"@hyperframes/studio-server": "0.8.195"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"vite": "^6.4.2",
|
|
83
83
|
"vitest": "^4.1.11",
|
|
84
84
|
"zustand": "^5.0.0",
|
|
85
|
-
"@hyperframes/producer": "0.8.
|
|
85
|
+
"@hyperframes/producer": "0.8.195"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"react": "19",
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
readTimelineElementZIndex,
|
|
19
19
|
} from "../../player/lib/timelineElementHelpers";
|
|
20
20
|
import { installReactActEnvironment } from "../../hooks/domSelectionTestHarness";
|
|
21
|
+
import { CLIP_HIDDEN_ATTR } from "@hyperframes/core/runtime/clip-reveal";
|
|
21
22
|
|
|
22
23
|
installReactActEnvironment();
|
|
23
24
|
|
|
@@ -149,6 +150,33 @@ describe("useLayerRevealOverride — delayed reveal ownership", () => {
|
|
|
149
150
|
act(() => root.unmount());
|
|
150
151
|
});
|
|
151
152
|
|
|
153
|
+
it("lifts the runtime's hidden-clip mark with the visibility it forces, and puts both back", () => {
|
|
154
|
+
const host = document.createElement("div");
|
|
155
|
+
const clip = makeEl("2", "absolute");
|
|
156
|
+
clip.style.visibility = "hidden";
|
|
157
|
+
clip.setAttribute(CLIP_HIDDEN_ATTR, "");
|
|
158
|
+
document.body.appendChild(host);
|
|
159
|
+
const root = createRoot(host);
|
|
160
|
+
let scheduleReveal: ((element: HTMLElement, delayMs: number) => void) | undefined;
|
|
161
|
+
|
|
162
|
+
function Harness({ isPlaying }: { isPlaying: boolean }) {
|
|
163
|
+
({ scheduleReveal } = useLayerRevealOverride({ isPlaying, selectedElement: clip }));
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
act(() => root.render(React.createElement(Harness, { isPlaying: false })));
|
|
168
|
+
act(() => scheduleReveal!(clip, 0));
|
|
169
|
+
act(() => vi.advanceTimersByTime(0));
|
|
170
|
+
expect([clip.style.visibility, clip.hasAttribute(CLIP_HIDDEN_ATTR)]).toEqual([
|
|
171
|
+
"visible",
|
|
172
|
+
false,
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
act(() => root.render(React.createElement(Harness, { isPlaying: true })));
|
|
176
|
+
expect([clip.style.visibility, clip.hasAttribute(CLIP_HIDDEN_ATTR)]).toEqual(["hidden", true]);
|
|
177
|
+
act(() => root.unmount());
|
|
178
|
+
});
|
|
179
|
+
|
|
152
180
|
it("revalidates the current selection before a delayed reveal runs", () => {
|
|
153
181
|
const host = document.createElement("div");
|
|
154
182
|
const selected = makeEl("2", "absolute");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { revealClip } from "@hyperframes/core/runtime/clip-reveal";
|
|
2
3
|
import {
|
|
3
4
|
LAYER_REVEAL_PRIOR_POSITION_ATTR,
|
|
4
5
|
LAYER_REVEAL_PRIOR_Z_ATTR,
|
|
@@ -15,6 +16,7 @@ interface RevealedNode {
|
|
|
15
16
|
priors: { display: string; visibility: string; opacity: string };
|
|
16
17
|
/** Values THIS override wrote — restore only while they are still in place. */
|
|
17
18
|
applied: { display?: string; visibility?: string; opacity?: string };
|
|
19
|
+
putMarkBack?: () => void;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
interface RevealLift {
|
|
@@ -158,10 +160,11 @@ function restoreIfOurs(
|
|
|
158
160
|
property: "display" | "visibility" | "opacity",
|
|
159
161
|
applied: string | undefined,
|
|
160
162
|
prior: string,
|
|
161
|
-
):
|
|
162
|
-
if (applied == null) return;
|
|
163
|
-
if (el.style.getPropertyValue(property) !== applied) return;
|
|
163
|
+
): boolean {
|
|
164
|
+
if (applied == null) return false;
|
|
165
|
+
if (el.style.getPropertyValue(property) !== applied) return false;
|
|
164
166
|
restoreInline(el, property, prior);
|
|
167
|
+
return true;
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
/** What hides this node at the current frame, per computed style. */
|
|
@@ -187,6 +190,7 @@ function revealNode(
|
|
|
187
190
|
opacity: el.style.opacity,
|
|
188
191
|
};
|
|
189
192
|
const applied: RevealedNode["applied"] = {};
|
|
193
|
+
let putMarkBack: RevealedNode["putMarkBack"];
|
|
190
194
|
if (needs.display) {
|
|
191
195
|
// Prefer whatever the stylesheet says once the inline hide is lifted;
|
|
192
196
|
// only force block when the sheet itself hides it.
|
|
@@ -195,14 +199,14 @@ function revealNode(
|
|
|
195
199
|
applied.display = el.style.display;
|
|
196
200
|
}
|
|
197
201
|
if (needs.visibility) {
|
|
198
|
-
|
|
202
|
+
putMarkBack = revealClip(el);
|
|
199
203
|
applied.visibility = "visible";
|
|
200
204
|
}
|
|
201
205
|
if (needs.opacity) {
|
|
202
206
|
el.style.opacity = "1";
|
|
203
207
|
applied.opacity = "1";
|
|
204
208
|
}
|
|
205
|
-
return { element: el, priors, applied };
|
|
209
|
+
return { element: el, priors, applied, putMarkBack };
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
/** Walk `element` → body, force-revealing every hiding node; returns the touched nodes. */
|
|
@@ -317,10 +321,11 @@ export function useLayerRevealOverride({
|
|
|
317
321
|
const state = stateRef.current;
|
|
318
322
|
stateRef.current = null;
|
|
319
323
|
if (!state) return;
|
|
320
|
-
for (const { element, priors, applied } of state.nodes) {
|
|
324
|
+
for (const { element, priors, applied, putMarkBack } of state.nodes) {
|
|
321
325
|
if (!element.isConnected) continue;
|
|
322
326
|
restoreIfOurs(element, "display", applied.display, priors.display);
|
|
323
|
-
restoreIfOurs(element, "visibility", applied.visibility, priors.visibility)
|
|
327
|
+
if (restoreIfOurs(element, "visibility", applied.visibility, priors.visibility))
|
|
328
|
+
putMarkBack?.();
|
|
324
329
|
restoreIfOurs(element, "opacity", applied.opacity, priors.opacity);
|
|
325
330
|
}
|
|
326
331
|
if (state.lift && state.base.isConnected) restoreLiftedElement(state.base, state.lift);
|
|
@@ -130,20 +130,29 @@ describe("MotionBlurPanel", () => {
|
|
|
130
130
|
expect(html).toContain('data-motion-blur-samples="64"');
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
it("Off removes data-motion-blur and leaves
|
|
133
|
+
it("Off removes data-motion-blur and its events, and leaves the other settings on the root", async () => {
|
|
134
134
|
const host = await mount(
|
|
135
135
|
' data-motion-blur="on" data-motion-blur-shutter="720" data-motion-blur-samples="64" data-motion-blur-tier="filter" data-motion-blur-events="on"',
|
|
136
136
|
);
|
|
137
137
|
await click(radio(host, "Off"));
|
|
138
138
|
const html = files["index.html"] ?? "";
|
|
139
139
|
expect(html).not.toContain('data-motion-blur="on"');
|
|
140
|
-
|
|
140
|
+
expect(html).not.toContain("data-motion-blur-events");
|
|
141
|
+
for (const kept of ['shutter="720"', 'samples="64"', 'tier="filter"']) {
|
|
141
142
|
expect(html).toContain(`data-motion-blur-${kept}`);
|
|
142
143
|
}
|
|
143
144
|
});
|
|
144
145
|
|
|
146
|
+
it("turns the events attribute on with Subtle and Strong, so callback-driven motion is found", async () => {
|
|
147
|
+
const host = await mount();
|
|
148
|
+
await click(radio(host, "Subtle"));
|
|
149
|
+
expect(files["index.html"]).toContain('data-motion-blur-events="on"');
|
|
150
|
+
await click(radio(host, "Strong"));
|
|
151
|
+
expect(files["index.html"]).toContain('data-motion-blur-events="on"');
|
|
152
|
+
});
|
|
153
|
+
|
|
145
154
|
it("writes nothing when the chosen level is already set", async () => {
|
|
146
|
-
const host = await mount(' data-motion-blur="on"');
|
|
155
|
+
const host = await mount(' data-motion-blur="on" data-motion-blur-events="on"');
|
|
147
156
|
await click(radio(host, "Subtle"));
|
|
148
157
|
expect(writeProjectFile).not.toHaveBeenCalled();
|
|
149
158
|
});
|
|
@@ -130,9 +130,19 @@ describe("levelOf and patchForLevel", () => {
|
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
it("sets Subtle to the default blur and Strong to the maximum, and leaves the rest alone for Off", () => {
|
|
133
|
-
expect(patchForLevel("subtle")).toEqual({
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
expect(patchForLevel("subtle")).toEqual({
|
|
134
|
+
enabled: true,
|
|
135
|
+
events: true,
|
|
136
|
+
shutterDeg: 240,
|
|
137
|
+
samples: null,
|
|
138
|
+
});
|
|
139
|
+
expect(patchForLevel("strong")).toEqual({
|
|
140
|
+
enabled: true,
|
|
141
|
+
events: true,
|
|
142
|
+
shutterDeg: 720,
|
|
143
|
+
samples: 64,
|
|
144
|
+
});
|
|
145
|
+
expect(patchForLevel("off")).toEqual({ enabled: false, events: false });
|
|
136
146
|
});
|
|
137
147
|
|
|
138
148
|
it("reads back as the level that was chosen", () => {
|
|
@@ -92,7 +92,9 @@ export function levelOf(settings: MotionBlurSettings): BlurLevel {
|
|
|
92
92
|
return settings.shutterDeg > STRONG_ABOVE ? "strong" : "subtle";
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
/** What choosing `level` changes; Off leaves the shutter, samples and
|
|
95
|
+
/** What choosing `level` changes; blur on turns events on too, and Off leaves the shutter, samples and the rest alone. */
|
|
96
96
|
export function patchForLevel(level: BlurLevel): Partial<MotionBlurSettings> {
|
|
97
|
-
return level === "off"
|
|
97
|
+
return level === "off"
|
|
98
|
+
? { enabled: false, events: false }
|
|
99
|
+
: { enabled: true, events: true, ...LEVEL_SETTINGS[level] };
|
|
98
100
|
}
|
|
@@ -4,6 +4,7 @@ import React, { act } from "react";
|
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
6
6
|
import { useGestureRecording, type GestureSample } from "./useGestureRecording";
|
|
7
|
+
import { CLIP_HIDDEN_ATTR } from "@hyperframes/core/runtime/clip-reveal";
|
|
7
8
|
|
|
8
9
|
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
9
10
|
|
|
@@ -65,6 +66,47 @@ describe("useGestureRecording", () => {
|
|
|
65
66
|
act(() => root.unmount());
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
it("lifts the runtime's hidden-clip mark while previewing a hidden clip and puts it back on release", () => {
|
|
70
|
+
const animationFrames: FrameRequestCallback[] = [];
|
|
71
|
+
vi.stubGlobal(
|
|
72
|
+
"requestAnimationFrame",
|
|
73
|
+
vi.fn((callback: FrameRequestCallback) => animationFrames.push(callback)),
|
|
74
|
+
);
|
|
75
|
+
vi.stubGlobal("cancelAnimationFrame", vi.fn());
|
|
76
|
+
const iframe = document.createElement("iframe");
|
|
77
|
+
document.body.append(iframe);
|
|
78
|
+
const element = iframe.contentDocument!.createElement("div");
|
|
79
|
+
element.id = "card";
|
|
80
|
+
element.style.visibility = "hidden";
|
|
81
|
+
element.setAttribute(CLIP_HIDDEN_ATTR, "");
|
|
82
|
+
iframe.contentDocument!.body.append(element);
|
|
83
|
+
Reflect.set(iframe.contentWindow!, "gsap", { getProperty: vi.fn(() => 0), set: vi.fn() });
|
|
84
|
+
Reflect.set(iframe.contentWindow!, "__timelines", {
|
|
85
|
+
main: { duration: () => 5, seek: vi.fn() },
|
|
86
|
+
});
|
|
87
|
+
Reflect.set(iframe.contentWindow!, "__player", { getTime: () => 1 });
|
|
88
|
+
|
|
89
|
+
let recording: ReturnType<typeof useGestureRecording> | null = null;
|
|
90
|
+
function Harness() {
|
|
91
|
+
recording = useGestureRecording();
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const root = mountReactHarness(<Harness />);
|
|
95
|
+
act(() => recording?.startRecording(element, iframe, 4));
|
|
96
|
+
document.dispatchEvent(new MouseEvent("pointermove", { bubbles: true, clientX: 10 }));
|
|
97
|
+
act(() => animationFrames.shift()?.(1));
|
|
98
|
+
expect([element.style.visibility, element.hasAttribute(CLIP_HIDDEN_ATTR)]).toEqual([
|
|
99
|
+
"visible",
|
|
100
|
+
false,
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
act(() => root.unmount());
|
|
104
|
+
expect([element.style.visibility, element.hasAttribute(CLIP_HIDDEN_ATTR)]).toEqual([
|
|
105
|
+
"hidden",
|
|
106
|
+
true,
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
|
|
68
110
|
it("releases the preview runtime when unmounted during a recording", () => {
|
|
69
111
|
const cancelAnimationFrame = vi.fn();
|
|
70
112
|
vi.stubGlobal(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { revealClip } from "@hyperframes/core/runtime/clip-reveal";
|
|
1
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
3
|
import { usePlayerStore, liveTime } from "../player/store/playerStore";
|
|
3
4
|
import { frameToSeconds, secondsToFrame } from "../player/lib/time";
|
|
@@ -36,6 +37,7 @@ interface GsapRuntime {
|
|
|
36
37
|
maxSeekTime: number;
|
|
37
38
|
savedVisibility: string;
|
|
38
39
|
savedTranslate: string;
|
|
40
|
+
putMarkBack: (() => void) | null;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
// ---------------------------------------------------------------------------
|
|
@@ -107,6 +109,7 @@ function connectGsapRuntime(
|
|
|
107
109
|
elementEndTime != null && elementEndTime < tlDuration ? elementEndTime : tlDuration,
|
|
108
110
|
savedVisibility: element.style.visibility,
|
|
109
111
|
savedTranslate: element.style.getPropertyValue("translate"),
|
|
112
|
+
putMarkBack: null,
|
|
110
113
|
};
|
|
111
114
|
}
|
|
112
115
|
} catch {
|
|
@@ -124,7 +127,8 @@ function applyRuntimePreview(
|
|
|
124
127
|
runtime.timeline.seek(seekTime);
|
|
125
128
|
runtime.element.style.setProperty("translate", "none");
|
|
126
129
|
runtime.gsap.set(runtime.element, { ...properties });
|
|
127
|
-
runtime.element
|
|
130
|
+
const putMarkBack = revealClip(runtime.element);
|
|
131
|
+
runtime.putMarkBack ??= putMarkBack;
|
|
128
132
|
liveTime.notify(seekTime);
|
|
129
133
|
usePlayerStore.getState().setCurrentTime(seekTime);
|
|
130
134
|
}
|
|
@@ -251,6 +255,7 @@ function releaseRuntimePreview(r: RecordingRefs): void {
|
|
|
251
255
|
if (!runtime) return;
|
|
252
256
|
const { element, savedVisibility, savedTranslate } = runtime;
|
|
253
257
|
element.style.visibility = savedVisibility;
|
|
258
|
+
runtime.putMarkBack?.();
|
|
254
259
|
element.style.setProperty("translate", savedTranslate || "");
|
|
255
260
|
try {
|
|
256
261
|
runtime.gsap.set(runtime.element, {
|
|
@@ -318,6 +318,28 @@ window.__timelines["root"] = tl;`;
|
|
|
318
318
|
expect(contentWindow.__hfMotionPathPluginLoading).toBe(false);
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
+
it("loads a missing soft-reload plugin from the iframe mirror and reports its failure", () => {
|
|
322
|
+
const { iframe, contentWindow } = buildMockIframe({
|
|
323
|
+
gsap: { timeline: vi.fn(), registerPlugin: vi.fn(), version: "3.14.2" },
|
|
324
|
+
});
|
|
325
|
+
const head = iframe.contentDocument!.head;
|
|
326
|
+
const meta = document.createElement("meta");
|
|
327
|
+
meta.name = "hyperframes-gsap-base-url";
|
|
328
|
+
meta.content = "http://localhost:8080/vendor/npm";
|
|
329
|
+
head.appendChild(meta);
|
|
330
|
+
const onAsyncFailure = vi.fn();
|
|
331
|
+
expect(applySoftReload(iframe, MOTION_PATH_SCRIPT_TEXT, { onAsyncFailure })).toBe("applied");
|
|
332
|
+
const plugin = head.querySelector("script");
|
|
333
|
+
expect(plugin?.src).toBe(
|
|
334
|
+
"http://localhost:8080/vendor/npm/gsap@3.14.2/dist/MotionPathPlugin.min.js",
|
|
335
|
+
);
|
|
336
|
+
plugin?.onerror?.(new Event("error"));
|
|
337
|
+
expect(onAsyncFailure).toHaveBeenCalledTimes(1);
|
|
338
|
+
expect(contentWindow.__hfForceTimelineRebind).not.toHaveBeenCalled();
|
|
339
|
+
expect(contentWindow.__hfMotionPathPluginLoading).toBe(false);
|
|
340
|
+
expect(head.querySelectorAll("script")).toHaveLength(1);
|
|
341
|
+
});
|
|
342
|
+
|
|
321
343
|
it('returns "cannot-soft-reload" when multiple GSAP scripts exist (ambiguous)', () => {
|
|
322
344
|
const script1 = document.createElement("script");
|
|
323
345
|
script1.textContent = "const tl = gsap.timeline({ paused: true });";
|
|
@@ -490,6 +512,45 @@ describe("ensureMotionPathPluginLoaded", () => {
|
|
|
490
512
|
);
|
|
491
513
|
});
|
|
492
514
|
|
|
515
|
+
it("preloads from the iframe mirror once, retaining the runtime version", () => {
|
|
516
|
+
const { iframe, appendedScripts, contentWindow, registerPlugin } = buildBootstrapIframe({
|
|
517
|
+
gsap: { version: "3.14.2", registerPlugin: vi.fn() },
|
|
518
|
+
});
|
|
519
|
+
const meta = document.createElement("meta");
|
|
520
|
+
meta.name = "hyperframes-gsap-base-url";
|
|
521
|
+
meta.content = "http://localhost:8080/vendor/npm";
|
|
522
|
+
iframe.contentDocument!.head.appendChild(meta);
|
|
523
|
+
ensureMotionPathPluginLoaded(iframe);
|
|
524
|
+
ensureMotionPathPluginLoaded(iframe);
|
|
525
|
+
expect(appendedScripts).toHaveLength(1);
|
|
526
|
+
expect(appendedScripts[0]!.src).toBe(
|
|
527
|
+
"http://localhost:8080/vendor/npm/gsap@3.14.2/dist/MotionPathPlugin.min.js",
|
|
528
|
+
);
|
|
529
|
+
appendedScripts[0]!.onerror?.(new Event("error"));
|
|
530
|
+
expect(contentWindow.__hfMotionPathPluginLoading).toBe(false);
|
|
531
|
+
expect(registerPlugin).not.toHaveBeenCalled();
|
|
532
|
+
expect(appendedScripts).toHaveLength(1);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it.each([
|
|
536
|
+
["3.15.0", "/api/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js"],
|
|
537
|
+
["3.14.2", "https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/MotionPathPlugin.min.js"],
|
|
538
|
+
])(
|
|
539
|
+
"preloads gsap %s from the host's bundled copy only when it holds that version",
|
|
540
|
+
(version, url) => {
|
|
541
|
+
const { iframe, appendedScripts } = buildBootstrapIframe({
|
|
542
|
+
gsap: { version, registerPlugin: vi.fn() },
|
|
543
|
+
});
|
|
544
|
+
const meta = document.createElement("meta");
|
|
545
|
+
meta.name = "hyperframes-gsap-base-url";
|
|
546
|
+
meta.content = "/api/vendor/npm";
|
|
547
|
+
iframe.contentDocument!.head.appendChild(meta);
|
|
548
|
+
ensureMotionPathPluginLoaded(iframe);
|
|
549
|
+
expect(appendedScripts).toHaveLength(1);
|
|
550
|
+
expect(appendedScripts[0]!.getAttribute("src")).toBe(url);
|
|
551
|
+
},
|
|
552
|
+
);
|
|
553
|
+
|
|
493
554
|
it("appends the plugin script once and registers it on load", () => {
|
|
494
555
|
const { iframe, contentWindow, appendedScripts, registerPlugin } = buildBootstrapIframe();
|
|
495
556
|
ensureMotionPathPluginLoaded(iframe);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
|
|
2
|
-
import { motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
|
|
2
|
+
import { GSAP_BASE_URL_META, motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
|
|
3
3
|
import { findAuthoredElement } from "./authoredSource";
|
|
4
4
|
import { applyAuthoredInlineOpacity, readStampedAuthoredOpacity } from "./authoredOpacity";
|
|
5
5
|
import {
|
|
@@ -50,6 +50,13 @@ type IframeWindow = Window & {
|
|
|
50
50
|
// At most one preview waits for its runtime; a newer load replaces it, so a frame that never boots is not kept alive.
|
|
51
51
|
let pendingReadyRetry: ((event: MessageEvent) => void) | null = null;
|
|
52
52
|
|
|
53
|
+
function previewMotionPathUrl(doc: Document, version?: string): string {
|
|
54
|
+
const baseUrl = doc.head.querySelector<HTMLMetaElement>(
|
|
55
|
+
`meta[name="${GSAP_BASE_URL_META}"]`,
|
|
56
|
+
)?.content;
|
|
57
|
+
return motionPathPluginUrl(version, baseUrl);
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
export function ensureMotionPathPluginLoaded(iframe: HTMLIFrameElement | null): void {
|
|
54
61
|
if (pendingReadyRetry) window.removeEventListener("message", pendingReadyRetry);
|
|
55
62
|
pendingReadyRetry = null;
|
|
@@ -86,7 +93,7 @@ export function ensureMotionPathPluginLoaded(iframe: HTMLIFrameElement | null):
|
|
|
86
93
|
try {
|
|
87
94
|
win.__hfMotionPathPluginLoading = true;
|
|
88
95
|
const pluginScript = doc.createElement("script");
|
|
89
|
-
pluginScript.src =
|
|
96
|
+
pluginScript.src = previewMotionPathUrl(doc, win.gsap?.version);
|
|
90
97
|
const finalize = () => {
|
|
91
98
|
win.__hfMotionPathPluginLoading = false;
|
|
92
99
|
try {
|
|
@@ -480,7 +487,7 @@ export function applySoftReload(
|
|
|
480
487
|
}
|
|
481
488
|
win.__hfMotionPathPluginLoading = true;
|
|
482
489
|
const pluginScript = doc.createElement("script");
|
|
483
|
-
pluginScript.src =
|
|
490
|
+
pluginScript.src = previewMotionPathUrl(doc, win.gsap?.version);
|
|
484
491
|
pluginScript.onload = () => {
|
|
485
492
|
win.__hfMotionPathPluginLoading = false;
|
|
486
493
|
executeScript();
|