@hyperframes/studio 0.6.110 → 0.6.111
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-B7S86vK1.js +370 -0
- package/dist/assets/index-DP8pPIk2.css +1 -0
- package/dist/assets/{index-CfiyaR7G.js → index-_IV-vm9l.js} +1 -1
- package/dist/assets/{index-CXy_mWnP.js → index-x0c2-zQN.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -5
- package/src/App.tsx +133 -141
- package/src/components/StudioHeader.tsx +44 -0
- package/src/components/StudioOverlays.tsx +70 -0
- package/src/components/editor/SourceEditor.tsx +19 -16
- package/src/components/editor/manualEditingAvailability.ts +25 -7
- package/src/components/storyboard/FramePoster.tsx +56 -0
- package/src/components/storyboard/StoryboardDirection.tsx +45 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +262 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +88 -0
- package/src/components/storyboard/StoryboardGrid.tsx +33 -0
- package/src/components/storyboard/StoryboardLoaded.tsx +136 -0
- package/src/components/storyboard/StoryboardScriptPanel.tsx +26 -0
- package/src/components/storyboard/StoryboardSourceEditor.tsx +231 -0
- package/src/components/storyboard/StoryboardStatusLegend.tsx +21 -0
- package/src/components/storyboard/StoryboardView.tsx +78 -0
- package/src/components/storyboard/frameStatus.ts +36 -0
- package/src/contexts/ViewModeContext.tsx +98 -0
- package/src/hooks/gsapScriptCommitTypes.ts +4 -7
- package/src/hooks/sdkSelfWriteRegistry.test.ts +67 -0
- package/src/hooks/sdkSelfWriteRegistry.ts +77 -0
- package/src/hooks/timelineEditingHelpers.ts +2 -0
- package/src/hooks/useAppHotkeys.ts +20 -0
- package/src/hooks/useDomEditCommits.ts +43 -14
- package/src/hooks/useDomEditSession.test.ts +41 -0
- package/src/hooks/useDomEditSession.ts +38 -6
- package/src/hooks/useDomGeometryCommits.ts +5 -9
- package/src/hooks/useElementLifecycleOps.ts +30 -0
- package/src/hooks/useGsapAnimationOps.ts +78 -51
- package/src/hooks/useGsapKeyframeOps.ts +127 -43
- package/src/hooks/useGsapPropertyDebounce.test.ts +70 -0
- package/src/hooks/useGsapPropertyDebounce.ts +201 -23
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +85 -0
- package/src/hooks/useGsapScriptCommits.ts +95 -40
- package/src/hooks/useSdkSession.test.ts +12 -0
- package/src/hooks/useSdkSession.ts +91 -25
- package/src/hooks/useStoryboard.ts +80 -0
- package/src/hooks/useTimelineEditing.ts +163 -68
- package/src/utils/gsapSoftReload.ts +14 -0
- package/src/utils/sdkCutover.gate.test.ts +61 -0
- package/src/utils/sdkCutover.test.ts +839 -0
- package/src/utils/sdkCutover.ts +465 -0
- package/src/utils/sdkOpMapping.ts +43 -0
- package/src/utils/sdkResolverShadow.test.ts +366 -0
- package/src/utils/sdkResolverShadow.ts +313 -0
- package/dist/assets/index-1C8oFiPi.css +0 -1
- package/dist/assets/index-D-3sGz65.js +0 -296
- package/src/utils/sdkShadow.test.ts +0 -606
- package/src/utils/sdkShadow.ts +0 -517
- package/src/utils/sdkShadowGsapFidelity.ts +0 -296
- package/src/utils/sdkShadowGsapKeyframe.test.ts +0 -265
- package/src/utils/sdkShadowGsapKeyframe.ts +0 -257
- package/src/utils/sdkShadowNumeric.ts +0 -11
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GSAP keyframe-op shadow (serialize round-trip diff). New module for the Stage 7
|
|
3
|
-
* shadow-parity push — kept out of sdkShadow.ts / sdkShadowGsapFidelity.ts so the
|
|
4
|
-
* shared files stay untouched (only additive imports) and the studio 600-line cap
|
|
5
|
-
* holds.
|
|
6
|
-
*
|
|
7
|
-
* Unlike tweens, the SDK exposes NO keyframe reader on ElementSnapshot, so there
|
|
8
|
-
* is no existence-parity path here. Instead we compare the two writers' output:
|
|
9
|
-
* open a fresh SDK doc from the server's pre-op file, dispatch the equivalent
|
|
10
|
-
* keyframe op, serialize, and diff the SDK's GSAP script against the server's
|
|
11
|
-
* resulting script.
|
|
12
|
-
*
|
|
13
|
-
* gsapFidelityMismatches (reused) matches tweens by resolved target element +
|
|
14
|
-
* method + position and diffs tween-level fields — but it does NOT look inside a
|
|
15
|
-
* tween's `keyframes` array. Keyframe drift therefore needs a dedicated diff,
|
|
16
|
-
* layered on top of the reused tween-level diff, matched by the GSAP animation id.
|
|
17
|
-
*
|
|
18
|
-
* SDK mapping (main, pre PR #1498 percentage-variant):
|
|
19
|
-
* add → addGsapKeyframe{animationId, position: percentage, value: properties}
|
|
20
|
-
* remove → removeGsapKeyframe{animationId, keyframeIndex} — the studio op is
|
|
21
|
-
* percentage-based, so we resolve percentage → index against the pre-op
|
|
22
|
-
* script (KF_PERCENT_TOLERANCE, aligned with the writer ~0.001) and
|
|
23
|
-
* no-op on ambiguity (duplicate-percentage keyframes can't be told
|
|
24
|
-
* apart by percentage — landmine from PR #1498).
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { openComposition } from "@hyperframes/sdk";
|
|
28
|
-
import type { EditOp } from "@hyperframes/sdk";
|
|
29
|
-
import { parseGsapScriptAcorn } from "@hyperframes/core/gsap-parser-acorn";
|
|
30
|
-
import type { GsapPercentageKeyframe } from "@hyperframes/core/gsap-parser";
|
|
31
|
-
import { STUDIO_SDK_SHADOW_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
32
|
-
import { trackStudioEvent } from "./studioTelemetry";
|
|
33
|
-
import type { SdkShadowMismatch } from "./sdkShadow";
|
|
34
|
-
import {
|
|
35
|
-
extractGsapScript,
|
|
36
|
-
gsapFidelityMismatches,
|
|
37
|
-
makeSelectorResolver,
|
|
38
|
-
} from "./sdkShadowGsapFidelity";
|
|
39
|
-
|
|
40
|
-
// Match the GSAP writer's percentage equality tolerance so a remove resolves to
|
|
41
|
-
// the same keyframe the server would pick (writer rounds to ~3 decimals).
|
|
42
|
-
const KF_PERCENT_TOLERANCE = 0.001;
|
|
43
|
-
|
|
44
|
-
export type ShadowKeyframeOp =
|
|
45
|
-
| {
|
|
46
|
-
kind: "add";
|
|
47
|
-
animationId: string;
|
|
48
|
-
percentage: number;
|
|
49
|
-
properties: Record<string, number | string>;
|
|
50
|
-
}
|
|
51
|
-
| { kind: "remove"; animationId: string; percentage: number };
|
|
52
|
-
|
|
53
|
-
// ─── percentage → SDK op mapping ──────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
function findAnimationKeyframes(
|
|
56
|
-
script: string,
|
|
57
|
-
animationId: string,
|
|
58
|
-
): GsapPercentageKeyframe[] | null {
|
|
59
|
-
const parsed = parseGsapScriptAcorn(script);
|
|
60
|
-
// Match the writer's locateAnimationWithFallback (gsapParser.ts): a from/fromTo
|
|
61
|
-
// tween's derived id may be normalized to "-to-" on write, so fall back to the
|
|
62
|
-
// converted id when the exact one isn't found — otherwise the keyframe diff
|
|
63
|
-
// goes blind (both scripts resolve null → falsely "clean") on converted tweens.
|
|
64
|
-
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
|
|
65
|
-
const anim =
|
|
66
|
-
parsed.animations.find((a) => a.id === animationId) ??
|
|
67
|
-
parsed.animations.find((a) => a.id === convertedId);
|
|
68
|
-
return anim?.keyframes?.keyframes ?? null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface KeyframeRemoveResolution {
|
|
72
|
-
/** Resolved 0-based index, or null when it can't be safely resolved. */
|
|
73
|
-
keyframeIndex: number | null;
|
|
74
|
-
/** Why no index — for telemetry when keyframeIndex is null. */
|
|
75
|
-
reason?: "no_keyframes" | "not_found" | "ambiguous";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Resolve a percentage-based remove to a keyframe index against the pre-op
|
|
80
|
-
* script. Returns null index (with a reason) when there are no keyframes, the
|
|
81
|
-
* percentage matches none, or — per the PR #1498 landmine — more than one
|
|
82
|
-
* keyframe shares the percentage (can't be disambiguated by percentage alone).
|
|
83
|
-
* Pure + exported so the mapping is unit-testable without an SDK session.
|
|
84
|
-
*/
|
|
85
|
-
export function resolveKeyframeIndexByPercentage(
|
|
86
|
-
script: string | null | undefined,
|
|
87
|
-
animationId: string,
|
|
88
|
-
percentage: number,
|
|
89
|
-
): KeyframeRemoveResolution {
|
|
90
|
-
if (!script) return { keyframeIndex: null, reason: "no_keyframes" };
|
|
91
|
-
const kfs = findAnimationKeyframes(script, animationId);
|
|
92
|
-
if (!kfs || kfs.length === 0) return { keyframeIndex: null, reason: "no_keyframes" };
|
|
93
|
-
const matches: number[] = [];
|
|
94
|
-
for (let i = 0; i < kfs.length; i++) {
|
|
95
|
-
if (Math.abs(kfs[i]?.percentage - percentage) <= KF_PERCENT_TOLERANCE) matches.push(i);
|
|
96
|
-
}
|
|
97
|
-
if (matches.length === 0) return { keyframeIndex: null, reason: "not_found" };
|
|
98
|
-
if (matches.length > 1) return { keyframeIndex: null, reason: "ambiguous" };
|
|
99
|
-
return { keyframeIndex: matches[0] };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Map a studio keyframe op to the SDK EditOp. For a remove this needs the pre-op
|
|
104
|
-
* script to resolve percentage → index; returns null (with a reason) when the
|
|
105
|
-
* index can't be safely resolved so the caller can emit a no-op-with-reason
|
|
106
|
-
* event instead of dispatching the wrong keyframe.
|
|
107
|
-
*/
|
|
108
|
-
export function keyframeOpToEditOp(
|
|
109
|
-
op: ShadowKeyframeOp,
|
|
110
|
-
beforeScript: string | null | undefined,
|
|
111
|
-
): { op: EditOp } | { op: null; reason: string } {
|
|
112
|
-
if (op.kind === "add") {
|
|
113
|
-
return {
|
|
114
|
-
op: {
|
|
115
|
-
type: "addGsapKeyframe",
|
|
116
|
-
animationId: op.animationId,
|
|
117
|
-
position: op.percentage,
|
|
118
|
-
value: op.properties,
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
const resolved = resolveKeyframeIndexByPercentage(beforeScript, op.animationId, op.percentage);
|
|
123
|
-
if (resolved.keyframeIndex === null) {
|
|
124
|
-
return { op: null, reason: resolved.reason ?? "unresolved" };
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
op: {
|
|
128
|
-
type: "removeGsapKeyframe",
|
|
129
|
-
animationId: op.animationId,
|
|
130
|
-
keyframeIndex: resolved.keyframeIndex,
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ─── Keyframe-aware fidelity diff ─────────────────────────────────────────────
|
|
136
|
-
|
|
137
|
-
function canonicalKeyframe(kf: GsapPercentageKeyframe): string {
|
|
138
|
-
const props: Record<string, unknown> = {};
|
|
139
|
-
for (const key of Object.keys(kf.properties).sort()) {
|
|
140
|
-
const v = kf.properties[key];
|
|
141
|
-
props[key] =
|
|
142
|
-
typeof v === "string" && v.trim() !== "" && !Number.isNaN(Number(v)) ? Number(v) : v;
|
|
143
|
-
}
|
|
144
|
-
return JSON.stringify({ pct: Math.round(kf.percentage * 1000) / 1000, ease: kf.ease, props });
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function canonicalKeyframes(kfs: GsapPercentageKeyframe[] | null): string {
|
|
148
|
-
if (!kfs) return "[]";
|
|
149
|
-
return JSON.stringify(
|
|
150
|
-
[...kfs].sort((a, b) => a.percentage - b.percentage).map(canonicalKeyframe),
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Diff two GSAP scripts for a keyframe op: the reused tween-level diff PLUS a
|
|
156
|
-
* keyframe-array comparison for the targeted animation (which the tween-level
|
|
157
|
-
* diff doesn't inspect). Reports a `keyframes` value_mismatch when the SDK and
|
|
158
|
-
* server keyframe arrays diverge canonically.
|
|
159
|
-
*/
|
|
160
|
-
export function gsapKeyframeFidelityMismatches(
|
|
161
|
-
sdkScript: string,
|
|
162
|
-
serverScript: string,
|
|
163
|
-
animationId: string,
|
|
164
|
-
resolveSelector?: (sel: string) => string,
|
|
165
|
-
): SdkShadowMismatch[] {
|
|
166
|
-
const mismatches = gsapFidelityMismatches(sdkScript, serverScript, resolveSelector);
|
|
167
|
-
const sdkKfs = findAnimationKeyframes(sdkScript, animationId);
|
|
168
|
-
const serverKfs = findAnimationKeyframes(serverScript, animationId);
|
|
169
|
-
const sdkCanon = canonicalKeyframes(sdkKfs);
|
|
170
|
-
const serverCanon = canonicalKeyframes(serverKfs);
|
|
171
|
-
if (sdkCanon !== serverCanon) {
|
|
172
|
-
mismatches.push({
|
|
173
|
-
kind: "value_mismatch",
|
|
174
|
-
hfId: animationId,
|
|
175
|
-
property: "keyframes",
|
|
176
|
-
expected: serverCanon,
|
|
177
|
-
actual: sdkCanon,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
return mismatches;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ─── Telemetry runner ─────────────────────────────────────────────────────────
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Shadow a GSAP keyframe op: open a fresh SDK doc from the server's pre-op file,
|
|
187
|
-
* apply the equivalent keyframe op, serialize, and diff against the server's
|
|
188
|
-
* resulting script. Emits sdk_shadow_dispatch op: "gsap_keyframe". Async,
|
|
189
|
-
* fire-and-forget; server stays authoritative. No-op when shadow is disabled.
|
|
190
|
-
*/
|
|
191
|
-
export async function runShadowGsapKeyframeFidelity(
|
|
192
|
-
beforeHtml: string | null | undefined,
|
|
193
|
-
op: ShadowKeyframeOp,
|
|
194
|
-
serverScript: string | null | undefined,
|
|
195
|
-
): Promise<void> {
|
|
196
|
-
if (!STUDIO_SDK_SHADOW_ENABLED) return;
|
|
197
|
-
// No server script to diff against → skip the (costly) openComposition.
|
|
198
|
-
if (!serverScript || !beforeHtml) return;
|
|
199
|
-
const beforeScript = extractGsapScript(beforeHtml);
|
|
200
|
-
const mapped = keyframeOpToEditOp(op, beforeScript);
|
|
201
|
-
if (mapped.op === null) {
|
|
202
|
-
// Ambiguous / not-found percentage: don't dispatch the wrong keyframe.
|
|
203
|
-
trackStudioEvent("sdk_shadow_dispatch", {
|
|
204
|
-
op: "gsap_keyframe",
|
|
205
|
-
dispatched: false,
|
|
206
|
-
reason: mapped.reason,
|
|
207
|
-
mismatchCount: 0,
|
|
208
|
-
});
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
const editOp = mapped.op;
|
|
212
|
-
try {
|
|
213
|
-
const session = await openComposition(beforeHtml);
|
|
214
|
-
const verdict = session.can(editOp);
|
|
215
|
-
if (!verdict.ok) {
|
|
216
|
-
trackStudioEvent("sdk_shadow_dispatch", {
|
|
217
|
-
op: "gsap_keyframe",
|
|
218
|
-
dispatched: false,
|
|
219
|
-
reason: "cannot_dispatch",
|
|
220
|
-
code: verdict.code,
|
|
221
|
-
mismatchCount: 0,
|
|
222
|
-
});
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
session.batch(() => session.dispatch(editOp));
|
|
226
|
-
const sdkScript = extractGsapScript(session.serialize());
|
|
227
|
-
if (sdkScript == null) {
|
|
228
|
-
trackStudioEvent("sdk_shadow_dispatch", {
|
|
229
|
-
op: "gsap_keyframe",
|
|
230
|
-
dispatched: false,
|
|
231
|
-
reason: "no_sdk_script",
|
|
232
|
-
mismatchCount: 0,
|
|
233
|
-
});
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const mismatches = gsapKeyframeFidelityMismatches(
|
|
237
|
-
sdkScript,
|
|
238
|
-
serverScript,
|
|
239
|
-
op.animationId,
|
|
240
|
-
makeSelectorResolver(beforeHtml),
|
|
241
|
-
);
|
|
242
|
-
trackStudioEvent("sdk_shadow_dispatch", {
|
|
243
|
-
op: "gsap_keyframe",
|
|
244
|
-
dispatched: true,
|
|
245
|
-
mismatchCount: mismatches.length,
|
|
246
|
-
mismatches: JSON.stringify(mismatches),
|
|
247
|
-
});
|
|
248
|
-
} catch (err) {
|
|
249
|
-
trackStudioEvent("sdk_shadow_dispatch", {
|
|
250
|
-
op: "gsap_keyframe",
|
|
251
|
-
dispatched: false,
|
|
252
|
-
reason: "fidelity_error",
|
|
253
|
-
error: String(err),
|
|
254
|
-
mismatchCount: 0,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Relative-epsilon numeric equality shared by the shadow diffs (timing parity +
|
|
3
|
-
* GSAP value fidelity). Both writers round-trip durations/positions through JS
|
|
4
|
-
* number formatting, so a value like 3.1 can read back as 3.0999999999999996.
|
|
5
|
-
* Treat values within 1e-6 * max(1, |a|, |b|) as equal — tight enough that a
|
|
6
|
-
* real 2 vs 1 (or 0.5 vs 0.49) still flags, loose enough to absorb float noise.
|
|
7
|
-
*/
|
|
8
|
-
export function relEqual(a: number, b: number): boolean {
|
|
9
|
-
if (a === b) return true;
|
|
10
|
-
return Math.abs(a - b) <= 1e-6 * Math.max(1, Math.abs(a), Math.abs(b));
|
|
11
|
-
}
|