@hyperframes/studio 0.6.60 → 0.6.61
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/{index-DG5-N9Mj.js → index-BdDNthf4.js} +31 -30
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/hooks/useDomEditSession.ts +1 -0
- package/src/hooks/useGsapScriptCommits.ts +11 -1
- package/src/utils/gsapSoftReload.test.ts +104 -0
- package/src/utils/gsapSoftReload.ts +89 -0
package/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BdDNthf4.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-B5EnhVCT.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.61",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@codemirror/view": "6.40.0",
|
|
32
32
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
33
|
"mediabunny": "^1.45.3",
|
|
34
|
-
"@hyperframes/core": "0.6.
|
|
35
|
-
"@hyperframes/player": "0.6.
|
|
34
|
+
"@hyperframes/core": "0.6.61",
|
|
35
|
+
"@hyperframes/player": "0.6.61"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/react": "19",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"vite": "^6.4.2",
|
|
47
47
|
"vitest": "^3.2.4",
|
|
48
48
|
"zustand": "^5.0.0",
|
|
49
|
-
"@hyperframes/producer": "0.6.
|
|
49
|
+
"@hyperframes/producer": "0.6.61"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": "19",
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef } from "react";
|
|
|
2
2
|
import type { ParsedGsap } from "@hyperframes/core/gsap-parser";
|
|
3
3
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
4
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
5
|
+
import { applySoftReload } from "../utils/gsapSoftReload";
|
|
5
6
|
|
|
6
7
|
const PROPERTY_DEFAULTS: Record<string, number> = {
|
|
7
8
|
opacity: 1,
|
|
@@ -45,6 +46,7 @@ interface MutationResult {
|
|
|
45
46
|
parsed?: ParsedGsap;
|
|
46
47
|
before?: string;
|
|
47
48
|
after?: string;
|
|
49
|
+
scriptText?: string;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
async function mutateGsapScript(
|
|
@@ -71,6 +73,7 @@ async function mutateGsapScript(
|
|
|
71
73
|
interface GsapScriptCommitsParams {
|
|
72
74
|
projectIdRef: React.MutableRefObject<string | null>;
|
|
73
75
|
activeCompPath: string | null;
|
|
76
|
+
previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
74
77
|
editHistory: {
|
|
75
78
|
recordEdit: (entry: {
|
|
76
79
|
label: string;
|
|
@@ -90,6 +93,7 @@ const DEBOUNCE_MS = 150;
|
|
|
90
93
|
export function useGsapScriptCommits({
|
|
91
94
|
projectIdRef,
|
|
92
95
|
activeCompPath,
|
|
96
|
+
previewIframeRef,
|
|
93
97
|
editHistory,
|
|
94
98
|
domEditSaveTimestampRef,
|
|
95
99
|
reloadPreview,
|
|
@@ -131,13 +135,18 @@ export function useGsapScriptCommits({
|
|
|
131
135
|
|
|
132
136
|
onCacheInvalidate();
|
|
133
137
|
|
|
134
|
-
if (
|
|
138
|
+
if (options.softReload && result.scriptText) {
|
|
139
|
+
if (!applySoftReload(previewIframeRef.current, result.scriptText)) {
|
|
140
|
+
reloadPreview();
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
135
143
|
reloadPreview();
|
|
136
144
|
}
|
|
137
145
|
},
|
|
138
146
|
[
|
|
139
147
|
projectIdRef,
|
|
140
148
|
activeCompPath,
|
|
149
|
+
previewIframeRef,
|
|
141
150
|
editHistory,
|
|
142
151
|
domEditSaveTimestampRef,
|
|
143
152
|
reloadPreview,
|
|
@@ -156,6 +165,7 @@ export function useGsapScriptCommits({
|
|
|
156
165
|
{
|
|
157
166
|
label: `Edit GSAP ${property}`,
|
|
158
167
|
coalesceKey: `gsap:${animationId}:${property}`,
|
|
168
|
+
softReload: true,
|
|
159
169
|
},
|
|
160
170
|
);
|
|
161
171
|
}, [commitMutation]);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, vi } from "vitest";
|
|
4
|
+
import { applySoftReload } from "./gsapSoftReload";
|
|
5
|
+
|
|
6
|
+
const SCRIPT_TEXT = `
|
|
7
|
+
window.__timelines = window.__timelines || {};
|
|
8
|
+
const tl = gsap.timeline({ paused: true });
|
|
9
|
+
tl.to("#box", { opacity: 0.8 });
|
|
10
|
+
window.__timelines["root"] = tl;
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
function buildMockIframe(overrides: Record<string, unknown> = {}) {
|
|
14
|
+
const scriptEl = document.createElement("script");
|
|
15
|
+
scriptEl.textContent =
|
|
16
|
+
'const tl = gsap.timeline({ paused: true }); tl.to("#box", { opacity: 0.5 });';
|
|
17
|
+
const container = document.createElement("div");
|
|
18
|
+
container.appendChild(scriptEl);
|
|
19
|
+
|
|
20
|
+
const mockTimeline = { kill: vi.fn(), pause: vi.fn() };
|
|
21
|
+
const contentWindow = {
|
|
22
|
+
gsap: { timeline: vi.fn() },
|
|
23
|
+
__hfForceTimelineRebind: vi.fn(),
|
|
24
|
+
__timelines: { root: mockTimeline } as Record<string, typeof mockTimeline>,
|
|
25
|
+
__player: { getTime: () => 2.0, seek: vi.fn() },
|
|
26
|
+
__hfStudioManualEditsApply: vi.fn(),
|
|
27
|
+
__hfSuppressSceneMutations: undefined as undefined | (<T>(fn: () => T) => T),
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const contentDocument = {
|
|
32
|
+
querySelectorAll: (sel: string) => (sel === "script:not([src])" ? [scriptEl] : []),
|
|
33
|
+
createElement: (tag: string) => document.createElement(tag),
|
|
34
|
+
body: container,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
iframe: { contentWindow, contentDocument } as unknown as HTMLIFrameElement,
|
|
39
|
+
contentWindow,
|
|
40
|
+
mockTimeline,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("applySoftReload", () => {
|
|
45
|
+
it("returns false when iframe is null", () => {
|
|
46
|
+
expect(applySoftReload(null, SCRIPT_TEXT)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns false when scriptText is empty", () => {
|
|
50
|
+
const { iframe } = buildMockIframe();
|
|
51
|
+
expect(applySoftReload(iframe, "")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns false when gsap is not on iframe window", () => {
|
|
55
|
+
const { iframe } = buildMockIframe({ gsap: undefined });
|
|
56
|
+
expect(applySoftReload(iframe, SCRIPT_TEXT)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns false when __hfForceTimelineRebind is missing", () => {
|
|
60
|
+
const { iframe } = buildMockIframe({ __hfForceTimelineRebind: undefined });
|
|
61
|
+
expect(applySoftReload(iframe, SCRIPT_TEXT)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("kills existing timelines, rebinds, and re-seeks on success", () => {
|
|
65
|
+
const { iframe, contentWindow, mockTimeline } = buildMockIframe();
|
|
66
|
+
const result = applySoftReload(iframe, SCRIPT_TEXT);
|
|
67
|
+
expect(result).toBe(true);
|
|
68
|
+
expect(mockTimeline.kill).toHaveBeenCalled();
|
|
69
|
+
expect(contentWindow.__hfForceTimelineRebind).toHaveBeenCalled();
|
|
70
|
+
expect(contentWindow.__player.seek).toHaveBeenCalledWith(2.0);
|
|
71
|
+
expect(contentWindow.__hfStudioManualEditsApply).toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("wraps execution in __hfSuppressSceneMutations when available", () => {
|
|
75
|
+
let suppressionCalled = false;
|
|
76
|
+
const { iframe } = buildMockIframe({
|
|
77
|
+
__hfSuppressSceneMutations: <T>(fn: () => T): T => {
|
|
78
|
+
suppressionCalled = true;
|
|
79
|
+
return fn();
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const result = applySoftReload(iframe, SCRIPT_TEXT);
|
|
83
|
+
expect(result).toBe(true);
|
|
84
|
+
expect(suppressionCalled).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns false when multiple GSAP scripts exist (ambiguous)", () => {
|
|
88
|
+
const script1 = document.createElement("script");
|
|
89
|
+
script1.textContent = "const tl = gsap.timeline({ paused: true });";
|
|
90
|
+
const script2 = document.createElement("script");
|
|
91
|
+
script2.textContent = 'tl.to("#other", { x: 10 });';
|
|
92
|
+
const container = document.createElement("div");
|
|
93
|
+
container.appendChild(script1);
|
|
94
|
+
container.appendChild(script2);
|
|
95
|
+
|
|
96
|
+
const { iframe } = buildMockIframe();
|
|
97
|
+
(iframe as unknown as { contentDocument: unknown }).contentDocument = {
|
|
98
|
+
querySelectorAll: (sel: string) => (sel === "script:not([src])" ? [script1, script2] : []),
|
|
99
|
+
createElement: (tag: string) => document.createElement(tag),
|
|
100
|
+
body: container,
|
|
101
|
+
};
|
|
102
|
+
expect(applySoftReload(iframe, SCRIPT_TEXT)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
type IframeWindow = Window & {
|
|
2
|
+
__timelines?: Record<string, { kill?: () => void; pause?: () => void }>;
|
|
3
|
+
__player?: { getTime?: () => number; seek?: (t: number) => void };
|
|
4
|
+
__hfForceTimelineRebind?: () => void;
|
|
5
|
+
__hfSuppressSceneMutations?: <T>(fn: () => T) => T;
|
|
6
|
+
__hfStudioManualEditsApply?: () => void;
|
|
7
|
+
gsap?: { timeline?: (...args: unknown[]) => unknown };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function isGsapScript(text: string): boolean {
|
|
11
|
+
return (
|
|
12
|
+
text.includes("gsap.timeline") ||
|
|
13
|
+
text.includes("__timelines") ||
|
|
14
|
+
text.includes(".to(") ||
|
|
15
|
+
text.includes(".set(")
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function findGsapScriptElements(doc: Document): HTMLScriptElement[] {
|
|
20
|
+
const results: HTMLScriptElement[] = [];
|
|
21
|
+
const scripts = doc.querySelectorAll<HTMLScriptElement>("script:not([src])");
|
|
22
|
+
for (const script of scripts) {
|
|
23
|
+
if (isGsapScript(script.textContent || "")) results.push(script);
|
|
24
|
+
}
|
|
25
|
+
return results;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Replace the GSAP script in the live iframe without reloading. This preserves
|
|
30
|
+
* the WebGL context and shader transition cache.
|
|
31
|
+
*
|
|
32
|
+
* Scoped to root-document GSAP scripts only — scripts inside `<template>`
|
|
33
|
+
* elements (sub-compositions) are not visible to `querySelectorAll` and will
|
|
34
|
+
* fall back to a full iframe reload.
|
|
35
|
+
*
|
|
36
|
+
* Returns false (triggering a full reload fallback) when:
|
|
37
|
+
* - The iframe or GSAP runtime isn't available
|
|
38
|
+
* - Multiple GSAP scripts are found (ambiguous which to replace)
|
|
39
|
+
* - No matching GSAP script element exists in the live DOM
|
|
40
|
+
*/
|
|
41
|
+
export function applySoftReload(iframe: HTMLIFrameElement | null, scriptText: string): boolean {
|
|
42
|
+
if (!iframe || !scriptText) return false;
|
|
43
|
+
|
|
44
|
+
const win = iframe.contentWindow as IframeWindow | null;
|
|
45
|
+
const doc = iframe.contentDocument;
|
|
46
|
+
if (!win || !doc) return false;
|
|
47
|
+
if (!win.gsap || !win.__hfForceTimelineRebind) return false;
|
|
48
|
+
|
|
49
|
+
const gsapScripts = findGsapScriptElements(doc);
|
|
50
|
+
if (gsapScripts.length !== 1) return false;
|
|
51
|
+
const oldScriptEl = gsapScripts[0]!;
|
|
52
|
+
|
|
53
|
+
const currentTime = win.__player?.getTime?.() ?? 0;
|
|
54
|
+
|
|
55
|
+
const doReload = () => {
|
|
56
|
+
const timelines = win.__timelines;
|
|
57
|
+
if (timelines) {
|
|
58
|
+
for (const key of Object.keys(timelines)) {
|
|
59
|
+
try {
|
|
60
|
+
timelines[key]?.kill?.();
|
|
61
|
+
} catch {}
|
|
62
|
+
delete timelines[key];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
oldScriptEl.remove();
|
|
67
|
+
const newScript = doc.createElement("script");
|
|
68
|
+
// IIFE prevents const/let redeclaration errors across consecutive edits.
|
|
69
|
+
// Top-level declarations are scoped to the IIFE; window.* assignments
|
|
70
|
+
// (e.g. window.__timelines["root"] = tl) still reach the global scope.
|
|
71
|
+
newScript.textContent = `(function(){${scriptText}\n})();`;
|
|
72
|
+
doc.body.appendChild(newScript);
|
|
73
|
+
|
|
74
|
+
win.__hfForceTimelineRebind?.();
|
|
75
|
+
win.__player?.seek?.(currentTime);
|
|
76
|
+
win.__hfStudioManualEditsApply?.();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (win.__hfSuppressSceneMutations) {
|
|
81
|
+
win.__hfSuppressSceneMutations(doReload);
|
|
82
|
+
} else {
|
|
83
|
+
doReload();
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|