@hyperframes/studio 0.7.70 → 0.7.72
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-DpKeYLEo.js → hyperframes-player-Bjf2HPzR.js} +1 -1
- package/dist/assets/index-BgZMle9I.css +1 -0
- package/dist/assets/{index-DXjAllII.js → index-Ch1hbJ3e.js} +1 -1
- package/dist/assets/{index-CbX-xoud.js → index-Ct_pxETK.js} +1 -1
- package/dist/assets/index-DCyWLHnx.js +428 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.html +2 -2
- package/dist/index.js +5383 -3804
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/StudioLeftSidebar.tsx +2 -3
- package/src/components/StudioRightPanel.tsx +10 -4
- package/src/components/editor/AnimationCard.test.tsx +100 -0
- package/src/components/editor/AnimationCard.tsx +5 -2
- package/src/components/editor/EaseCurveSection.test.tsx +348 -0
- package/src/components/editor/EaseCurveSection.tsx +444 -237
- package/src/components/editor/EaseParamFields.test.tsx +188 -0
- package/src/components/editor/EaseParamFields.tsx +246 -0
- package/src/components/editor/PropertyPanel.test.tsx +9 -7
- package/src/components/editor/PropertyPanelFlat.tsx +74 -88
- package/src/components/editor/colorGradingScopePatch.test.ts +3 -3
- package/src/components/editor/easeCurveSvg.tsx +65 -0
- package/src/components/editor/easePresetLibrary.test.ts +114 -0
- package/src/components/editor/easePresetLibrary.ts +49 -0
- package/src/components/editor/gsapAnimationConstants.ts +15 -13
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +73 -40
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +85 -66
- package/src/components/editor/propertyPanelFlatEffectControl.tsx +96 -0
- package/src/components/editor/propertyPanelFlatEffectSpecs.ts +305 -0
- package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +331 -0
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +503 -0
- package/src/components/editor/propertyPanelFlatOverlaysSection.test.tsx +113 -0
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +108 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +5 -5
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -0
- package/src/components/editor/propertyPanelPresetPreview.ts +36 -0
- package/src/components/editor/propertyPanelTypes.ts +13 -0
- package/src/components/editor/useColorGradingController.test.ts +235 -30
- package/src/components/editor/useColorGradingController.ts +46 -88
- package/src/components/editor/useColorGradingPreviews.ts +307 -0
- package/src/components/nle/NLEContext.tsx +1 -1
- package/src/contexts/PanelLayoutContext.tsx +3 -6
- package/src/hooks/useBlockCatalog.ts +31 -13
- package/src/hooks/useBlockHandlers.ts +22 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +15 -0
- package/src/hooks/useGsapScriptCommits.ts +14 -1
- package/src/hooks/usePanelLayout.test.ts +68 -1
- package/src/hooks/usePanelLayout.ts +72 -34
- package/src/player/components/PlayerControls.tsx +68 -229
- package/src/player/components/ShortcutsPanel.tsx +2 -4
- package/src/player/components/SpeedMenu.tsx +1 -2
- package/src/telemetry/events.test.ts +9 -0
- package/src/telemetry/events.ts +5 -0
- package/src/utils/blockInstaller.test.ts +67 -0
- package/src/utils/blockInstaller.ts +162 -109
- package/src/utils/studioUiPreferences.test.ts +5 -0
- package/src/utils/studioUiPreferences.ts +8 -0
- package/dist/assets/index-CAeU317U.js +0 -428
- package/dist/assets/index-Ceyz8Qt2.css +0 -1
- package/src/player/components/PlayerControls.test.ts +0 -20
- package/src/player/components/useSeekBarDrag.ts +0 -169
|
@@ -23,6 +23,15 @@ import {
|
|
|
23
23
|
FlatColorGradingAccessory,
|
|
24
24
|
FlatColorGradingSection,
|
|
25
25
|
} from "./propertyPanelFlatColorGradingSection";
|
|
26
|
+
import {
|
|
27
|
+
activeColorGradingEffectCount,
|
|
28
|
+
FlatEffectsAccessory,
|
|
29
|
+
FlatEffectsSection,
|
|
30
|
+
} from "./propertyPanelFlatEffectsSection";
|
|
31
|
+
import {
|
|
32
|
+
deriveMediaOverlayPlacement,
|
|
33
|
+
FlatOverlaysSection,
|
|
34
|
+
} from "./propertyPanelFlatOverlaysSection";
|
|
26
35
|
|
|
27
36
|
type EditingSections = ReturnType<typeof resolveEditingSections>;
|
|
28
37
|
|
|
@@ -34,11 +43,7 @@ type FlatGroupDescriptor = {
|
|
|
34
43
|
content: ReactNode;
|
|
35
44
|
};
|
|
36
45
|
|
|
37
|
-
//
|
|
38
|
-
// satisfy FlatMotionSection's required-callback shape when the effect list is
|
|
39
|
-
// gated off (showEffects === false, so none of these are ever invoked). Keeps
|
|
40
|
-
// the gated-off path free of `!` non-null assertions — the real, narrowed
|
|
41
|
-
// handlers flow through only when the double-gate below passes.
|
|
46
|
+
// Required callback shape for the gated-off Motion effect list.
|
|
42
47
|
const EMPTY_GSAP_EFFECT_HANDLERS = {
|
|
43
48
|
onAddAnimation: () => {},
|
|
44
49
|
onUpdateProperty: () => {},
|
|
@@ -48,15 +53,7 @@ const EMPTY_GSAP_EFFECT_HANDLERS = {
|
|
|
48
53
|
onRemoveProperty: () => {},
|
|
49
54
|
};
|
|
50
55
|
|
|
51
|
-
/**
|
|
52
|
-
* The flat "Ledger" inspector shell (design_handoff_studio_inspector).
|
|
53
|
-
*
|
|
54
|
-
* Extracted from PropertyPanel so that file stays under the 600-LOC gate
|
|
55
|
-
* (same one-directional-import precedent as FlatTextSection). Rendered only
|
|
56
|
-
* when STUDIO_FLAT_INSPECTOR_ENABLED is on; owns the one-open group state.
|
|
57
|
-
*
|
|
58
|
-
* The Text/Style/Layout/Motion/Media/Grade groups share the one-open accordion.
|
|
59
|
-
*/
|
|
56
|
+
/** The flat inspector shell with one shared open-group state. */
|
|
60
57
|
// fallow-ignore-next-line complexity
|
|
61
58
|
export function PropertyPanelFlat({
|
|
62
59
|
element,
|
|
@@ -93,6 +90,7 @@ export function PropertyPanelFlat({
|
|
|
93
90
|
onAskAgent,
|
|
94
91
|
onToggleElementHidden,
|
|
95
92
|
onImportAssets,
|
|
93
|
+
onAddMediaOverlay,
|
|
96
94
|
onImportFonts,
|
|
97
95
|
recordingState,
|
|
98
96
|
recordingDuration,
|
|
@@ -113,10 +111,7 @@ export function PropertyPanelFlat({
|
|
|
113
111
|
currentTime,
|
|
114
112
|
animIdForProp,
|
|
115
113
|
gsapRuntimeValues,
|
|
116
|
-
//
|
|
117
|
-
// (non-flat) panel, but the flat path recomputes its own basis below via
|
|
118
|
-
// deriveElementTiming so it agrees with Motion's Timing row — ignore the
|
|
119
|
-
// parent's naive `elDuration ?? 1` fallback.
|
|
114
|
+
// The flat path derives timing consistently with its Motion section.
|
|
120
115
|
elStart: _elStart,
|
|
121
116
|
elDuration: _elDuration,
|
|
122
117
|
onCommitAnimatedProperty,
|
|
@@ -164,6 +159,7 @@ export function PropertyPanelFlat({
|
|
|
164
159
|
| "onAskAgent"
|
|
165
160
|
| "onToggleElementHidden"
|
|
166
161
|
| "onImportAssets"
|
|
162
|
+
| "onAddMediaOverlay"
|
|
167
163
|
| "onImportFonts"
|
|
168
164
|
| "fontAssets"
|
|
169
165
|
| "gsapAnimations"
|
|
@@ -187,9 +183,6 @@ export function PropertyPanelFlat({
|
|
|
187
183
|
| "recordingDuration"
|
|
188
184
|
| "onToggleRecording"
|
|
189
185
|
> &
|
|
190
|
-
// Layout-group values (Plan 3a Task 5). All are derived locals or handlers in
|
|
191
|
-
// PropertyPanel; compose their exact shapes from FlatLayoutSection's own props
|
|
192
|
-
// via Pick so a signature change there propagates here instead of drifting.
|
|
193
186
|
Pick<
|
|
194
187
|
Parameters<typeof FlatLayoutSection>[0],
|
|
195
188
|
| "displayX"
|
|
@@ -227,12 +220,7 @@ export function PropertyPanelFlat({
|
|
|
227
220
|
onCopyElementInfo: () => void;
|
|
228
221
|
currentTime: number;
|
|
229
222
|
}) {
|
|
230
|
-
//
|
|
231
|
-
// (Text if text-editable, else Style if style-editable, else none open) so a
|
|
232
|
-
// style-only element doesn't start with everything collapsed. Only runs on
|
|
233
|
-
// mount — PropertyPanel.tsx keys <PropertyPanelFlat> by element identity so
|
|
234
|
-
// switching the selection re-mounts this component and re-derives the
|
|
235
|
-
// default instead of preserving stale state across unrelated elements.
|
|
223
|
+
// PropertyPanel keys this component by selection, so the default is per element.
|
|
236
224
|
const [openGroupId, setOpenGroupId] = useState<string>(() =>
|
|
237
225
|
isTextEditableSelection(element)
|
|
238
226
|
? "text"
|
|
@@ -243,33 +231,16 @@ export function PropertyPanelFlat({
|
|
|
243
231
|
: "layout",
|
|
244
232
|
);
|
|
245
233
|
|
|
246
|
-
//
|
|
247
|
-
// their header/body gets the fast entrance animation (hf-flat-group-enter)
|
|
248
|
-
// and no one else's does. Deliberately NOT derived from remounting alone:
|
|
249
|
-
// FlatGroupHeader instances are keyed by group id and React normally
|
|
250
|
-
// preserves them across re-renders, but toggling a non-adjacent group still
|
|
251
|
-
// shifts the untouched collapsed siblings between the before/after-open
|
|
252
|
-
// slices below, and Chromium restarts a CSS animation on that kind of
|
|
253
|
-
// position shift even though nothing about the sibling actually changed.
|
|
254
|
-
// Gating on these ids (cleared shortly after the 120ms CSS animation
|
|
255
|
-
// finishes) keeps the animation scoped to only the groups that actually
|
|
256
|
-
// just toggled. Two ids, not one: the clicked (newly-opening/closing) group
|
|
257
|
-
// AND whichever group was open immediately before the click and got
|
|
258
|
-
// implicitly closed by it — both freshly-mounted headers need to animate.
|
|
234
|
+
// Animate only the groups that changed during this toggle cycle.
|
|
259
235
|
const [justToggledIds, setJustToggledIds] = useState<string[]>([]);
|
|
260
236
|
const justToggledTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
237
|
+
const panelBodyRef = useRef<HTMLDivElement>(null);
|
|
261
238
|
useEffect(() => {
|
|
262
239
|
return () => {
|
|
263
240
|
if (justToggledTimeoutRef.current) clearTimeout(justToggledTimeoutRef.current);
|
|
264
241
|
};
|
|
265
242
|
}, []);
|
|
266
243
|
|
|
267
|
-
// Grade group state. Called unconditionally (React rules-of-hooks) even when
|
|
268
|
-
// sections.colorGrading is false — unlike the legacy ColorGradingSection,
|
|
269
|
-
// which is only mounted when the section is active, PropertyPanelFlat is not
|
|
270
|
-
// remounted per-section so the hook must run every render. Shares one state
|
|
271
|
-
// object between the group's header accessory (compare/status/reset) and its
|
|
272
|
-
// body (the FlatColorGradingSection controls).
|
|
273
244
|
const colorGradingController = useColorGradingController({
|
|
274
245
|
projectId,
|
|
275
246
|
element,
|
|
@@ -281,9 +252,7 @@ export function PropertyPanelFlat({
|
|
|
281
252
|
const isTextEditable = isTextEditableSelection(element);
|
|
282
253
|
const elementKind = sections.media ? "media" : element.textFields.length > 0 ? "text" : "other";
|
|
283
254
|
const toggleOpen = (groupId: string) => {
|
|
284
|
-
|
|
285
|
-
// openGroupId), so the group that's about to be implicitly closed can be
|
|
286
|
-
// tracked too — not just the one the user clicked.
|
|
255
|
+
const isOpening = openGroupId !== groupId;
|
|
287
256
|
const previousOpenGroupId = openGroupId;
|
|
288
257
|
setOpenGroupId((current) => (current === groupId ? "" : groupId));
|
|
289
258
|
const implicitlyClosedId =
|
|
@@ -291,34 +260,20 @@ export function PropertyPanelFlat({
|
|
|
291
260
|
setJustToggledIds(implicitlyClosedId ? [groupId, implicitlyClosedId] : [groupId]);
|
|
292
261
|
if (justToggledTimeoutRef.current) clearTimeout(justToggledTimeoutRef.current);
|
|
293
262
|
justToggledTimeoutRef.current = setTimeout(() => setJustToggledIds([]), 200);
|
|
263
|
+
if (isOpening) {
|
|
264
|
+
requestAnimationFrame(() =>
|
|
265
|
+
panelBodyRef.current
|
|
266
|
+
?.querySelector<HTMLElement>('[data-flat-group-open="true"]')
|
|
267
|
+
?.scrollIntoView?.({ block: "start" }),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
294
270
|
};
|
|
295
|
-
// Basis for the Layout keyframe gutter (X/Y/W/H/Angle + 3D Transform) —
|
|
296
|
-
// must agree with Motion's Timing row (FlatTimingRow), which infers the
|
|
297
|
-
// range from animations when there's no explicit data-duration. Computed
|
|
298
|
-
// here (not threaded from PropertyPanel) both to keep that file under its
|
|
299
|
-
// 600-LOC gate and because element/gsapAnimations are already in scope.
|
|
300
271
|
const { start: elStart, duration: elDuration } = deriveElementTiming(element, gsapAnimations);
|
|
301
|
-
// Trivial percentage→time seek, derived here rather than threaded from
|
|
302
|
-
// PropertyPanel (keeps that file under its 600-LOC gate).
|
|
303
272
|
const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration);
|
|
304
|
-
//
|
|
305
|
-
// seekFromKfPct above — recomputed here (not threaded as `currentPct` from
|
|
306
|
-
// PropertyPanel, which still derives it against its own naive basis for the
|
|
307
|
-
// legacy panel) so KeyframeNavigation's diamond active-state and prev/next
|
|
308
|
-
// arrow targeting agree with where a keyframe click actually seeks to
|
|
309
|
-
// (follow-up fix to 684ec4e87, which corrected the seek basis but left this
|
|
310
|
-
// one still naive).
|
|
273
|
+
// Use the same timing basis for seeking and active keyframe state.
|
|
311
274
|
const currentPct = elDuration > 0 ? ((currentTime - elStart) / elDuration) * 100 : 0;
|
|
312
275
|
|
|
313
|
-
//
|
|
314
|
-
// • Timing (sections.timing) shows via resolveEditingSections, same as today.
|
|
315
|
-
// • The effect-card list shows only when STUDIO_GSAP_PANEL_ENABLED is on AND
|
|
316
|
-
// all five edit handlers are present (identical to PropertyPanel's legacy
|
|
317
|
-
// `<GsapAnimationSection>` guard).
|
|
318
|
-
// Computing the narrowed handler bundle inside the `&&`-guarded ternary lets
|
|
319
|
-
// TypeScript prove each handler non-undefined without a `!` assertion; the
|
|
320
|
-
// noop bundle only fills the type when the gate is off (never invoked, since
|
|
321
|
-
// FlatMotionSection guards every call behind showEffects).
|
|
276
|
+
// Match the legacy Motion gate while preserving TypeScript narrowing.
|
|
322
277
|
const showMotionTiming = Boolean(sections.timing);
|
|
323
278
|
const gsapEffectHandlers =
|
|
324
279
|
STUDIO_GSAP_PANEL_ENABLED &&
|
|
@@ -347,9 +302,6 @@ export function PropertyPanelFlat({
|
|
|
347
302
|
const showMotionEffects = gsapEffectHandlers !== null;
|
|
348
303
|
const showMotionGroup = showMotionTiming || showMotionEffects;
|
|
349
304
|
|
|
350
|
-
// Ordered group descriptors — one per FlatGroup this panel renders, gated by
|
|
351
|
-
// the same conditions the inline JSX used. Split below into before-open/
|
|
352
|
-
// open/after-open regions for the one-open accordion.
|
|
353
305
|
const groups: FlatGroupDescriptor[] = [];
|
|
354
306
|
if (isTextEditable) {
|
|
355
307
|
groups.push({
|
|
@@ -372,8 +324,6 @@ export function PropertyPanelFlat({
|
|
|
372
324
|
});
|
|
373
325
|
}
|
|
374
326
|
if (showEditableSections) {
|
|
375
|
-
// Number.isFinite guard (not `|| 1`): opacity 0 is a real value — an
|
|
376
|
-
// invisible element must summarize as 0%, not 100%.
|
|
377
327
|
const opacityValue = parseFloat(styles.opacity ?? "1");
|
|
378
328
|
const opacityPct = Math.round((Number.isFinite(opacityValue) ? opacityValue : 1) * 100);
|
|
379
329
|
groups.push({
|
|
@@ -398,8 +348,6 @@ export function PropertyPanelFlat({
|
|
|
398
348
|
groups.push({
|
|
399
349
|
id: "layout",
|
|
400
350
|
title: "Layout",
|
|
401
|
-
// No scrub accessory: FlatRow/CommitField has no pointer-drag scrubbing
|
|
402
|
-
// (wheel/arrow keys only) — advertising "drag values to scrub" here lies.
|
|
403
351
|
summary: `${formatPxMetricValue(displayX)},${formatPxMetricValue(displayY)} · ${Math.round(displayW)}×${Math.round(displayH)}`,
|
|
404
352
|
content: (
|
|
405
353
|
<FlatLayoutSection
|
|
@@ -470,15 +418,56 @@ export function PropertyPanelFlat({
|
|
|
470
418
|
assets={assets}
|
|
471
419
|
onImportAssets={onImportAssets}
|
|
472
420
|
onCommitColorGrading={colorGradingController.commitColorGrading}
|
|
421
|
+
onPreviewColorGrading={colorGradingController.previewColorGrading}
|
|
473
422
|
applyScope={colorGradingController.applyScope}
|
|
474
423
|
applyBusy={colorGradingController.applyBusy}
|
|
475
424
|
onSetApplyScope={colorGradingController.setApplyScope}
|
|
476
425
|
onApplyToScope={() => void colorGradingController.applyToScope()}
|
|
477
426
|
onApplyScopeAvailable={Boolean(onApplyColorGradingScope)}
|
|
478
427
|
mediaMetadata={colorGradingController.mediaMetadata}
|
|
428
|
+
presetPreviews={colorGradingController.presetPreviews}
|
|
429
|
+
onRequestPresetPreviews={colorGradingController.requestPresetPreviews}
|
|
430
|
+
/>
|
|
431
|
+
),
|
|
432
|
+
});
|
|
433
|
+
const activeEffects = activeColorGradingEffectCount(colorGradingController.grading);
|
|
434
|
+
const effectsProps = {
|
|
435
|
+
grading: colorGradingController.grading,
|
|
436
|
+
onCommitColorGrading: colorGradingController.commitColorGrading,
|
|
437
|
+
};
|
|
438
|
+
groups.push({
|
|
439
|
+
id: "effects",
|
|
440
|
+
title: "Effects",
|
|
441
|
+
accessory: <FlatEffectsAccessory {...effectsProps} />,
|
|
442
|
+
summary: activeEffects ? `${activeEffects} active` : "none",
|
|
443
|
+
content: (
|
|
444
|
+
<FlatEffectsSection
|
|
445
|
+
{...effectsProps}
|
|
446
|
+
previews={colorGradingController.effectPreviews}
|
|
447
|
+
presetPreviews={colorGradingController.presetPreviews}
|
|
448
|
+
onPreviewColorGrading={colorGradingController.previewColorGrading}
|
|
449
|
+
onRequestEffectPreviews={colorGradingController.requestEffectPreviews}
|
|
450
|
+
onRequestPresetPreviews={colorGradingController.requestPresetPreviews}
|
|
479
451
|
/>
|
|
480
452
|
),
|
|
481
453
|
});
|
|
454
|
+
if (onAddMediaOverlay) {
|
|
455
|
+
groups.push({
|
|
456
|
+
id: "overlays",
|
|
457
|
+
title: "Overlays",
|
|
458
|
+
summary: "add layer",
|
|
459
|
+
content: (
|
|
460
|
+
<FlatOverlaysSection
|
|
461
|
+
onAddOverlay={(blockName) =>
|
|
462
|
+
onAddMediaOverlay(
|
|
463
|
+
blockName,
|
|
464
|
+
deriveMediaOverlayPlacement(element, { start: elStart, duration: elDuration }),
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
/>
|
|
468
|
+
),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
482
471
|
}
|
|
483
472
|
if (sections.media) {
|
|
484
473
|
groups.push({
|
|
@@ -499,13 +488,6 @@ export function PropertyPanelFlat({
|
|
|
499
488
|
});
|
|
500
489
|
}
|
|
501
490
|
|
|
502
|
-
// Fixed-headers + scrollable-open-section layout (design_handoff
|
|
503
|
-
// scrollable-open-section, replaces the prior sticky-stacking mechanism):
|
|
504
|
-
// collapsed headers before/after the open group render in normal document
|
|
505
|
-
// flow and never move. Only the open group's own body content scrolls, in
|
|
506
|
-
// a dedicated region between the two fixed header stacks. When no group is
|
|
507
|
-
// open, every group is just a collapsed header — there's no scrollable
|
|
508
|
-
// middle region at all, since nothing is expanded.
|
|
509
491
|
const openIndex = groups.findIndex((g) => g.id === openGroupId);
|
|
510
492
|
const beforeOpen = openIndex === -1 ? groups : groups.slice(0, openIndex);
|
|
511
493
|
const openGroup = openIndex === -1 ? null : groups[openIndex];
|
|
@@ -532,7 +514,11 @@ export function PropertyPanelFlat({
|
|
|
532
514
|
showUngroup={Boolean(onUngroup && element.dataAttributes["hf-group"] != null)}
|
|
533
515
|
/>
|
|
534
516
|
</DesignPanelInputProvider>
|
|
535
|
-
<div
|
|
517
|
+
<div
|
|
518
|
+
ref={panelBodyRef}
|
|
519
|
+
data-flat-panel-body="true"
|
|
520
|
+
className="flex min-h-0 flex-1 flex-col overflow-y-auto"
|
|
521
|
+
>
|
|
536
522
|
{beforeOpen.map((g) => (
|
|
537
523
|
<DesignPanelInputProvider key={g.id} section={slugifyDesignInput(g.title)}>
|
|
538
524
|
<FlatGroupHeader
|
|
@@ -546,7 +532,7 @@ export function PropertyPanelFlat({
|
|
|
546
532
|
))}
|
|
547
533
|
{openGroup && (
|
|
548
534
|
<DesignPanelInputProvider section={slugifyDesignInput(openGroup.title)}>
|
|
549
|
-
<div data-flat-group-open="true" className="flex min-h-
|
|
535
|
+
<div data-flat-group-open="true" className="flex min-h-[180px] flex-none flex-col">
|
|
550
536
|
<FlatGroupHeader
|
|
551
537
|
title={openGroup.title}
|
|
552
538
|
isOpen
|
|
@@ -5,15 +5,15 @@ describe("patchMediaColorGradingInHtml", () => {
|
|
|
5
5
|
it("adds color grading to video and image tags only", () => {
|
|
6
6
|
const { html, count } = patchMediaColorGradingInHtml(
|
|
7
7
|
`<div><video id="v"></video><img id="i" /><audio id="a"></audio></div>`,
|
|
8
|
-
`{"preset":"
|
|
8
|
+
`{"preset":"clean-studio"}`,
|
|
9
9
|
);
|
|
10
10
|
|
|
11
11
|
expect(count).toBe(2);
|
|
12
12
|
expect(html).toContain(
|
|
13
|
-
`video id="v" data-color-grading="{"preset":"
|
|
13
|
+
`video id="v" data-color-grading="{"preset":"clean-studio"}"`,
|
|
14
14
|
);
|
|
15
15
|
expect(html).toContain(
|
|
16
|
-
`img id="i" data-color-grading="{"preset":"
|
|
16
|
+
`img id="i" data-color-grading="{"preset":"clean-studio"}"`,
|
|
17
17
|
);
|
|
18
18
|
expect(html).toContain(`<audio id="a"></audio>`);
|
|
19
19
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { evaluateWiggleEase, parseWiggleEase } from "@hyperframes/core/wiggle-ease";
|
|
2
|
+
import { evaluateSpringEase, parseSpringBounce } from "@hyperframes/core/spring-ease";
|
|
3
|
+
import { resolveEaseCurveTuple } from "./gsapAnimationConstants";
|
|
4
|
+
|
|
5
|
+
export function sampledPath(
|
|
6
|
+
samples: number,
|
|
7
|
+
x: (progress: number) => number,
|
|
8
|
+
y: (value: number) => number,
|
|
9
|
+
evaluate: (progress: number) => number,
|
|
10
|
+
): string {
|
|
11
|
+
return Array.from({ length: samples + 1 }, (_, index) => {
|
|
12
|
+
const progress = index / samples;
|
|
13
|
+
return `${index === 0 ? "M" : "L"}${x(progress)},${y(evaluate(progress))}`;
|
|
14
|
+
}).join(" ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function holdCurvePath(left: number, bottom: number, right: number, top: number): string {
|
|
18
|
+
return `M${left},${bottom} L${right},${bottom} L${right},${top}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function MiniCurveSvg({
|
|
22
|
+
ease,
|
|
23
|
+
active,
|
|
24
|
+
size = 24,
|
|
25
|
+
}: {
|
|
26
|
+
ease: string;
|
|
27
|
+
active: boolean;
|
|
28
|
+
size?: number;
|
|
29
|
+
}) {
|
|
30
|
+
const springBounce = parseSpringBounce(ease);
|
|
31
|
+
const wiggle = parseWiggleEase(ease);
|
|
32
|
+
const curve = resolveEaseCurveTuple(ease);
|
|
33
|
+
const [x1, y1, x2, y2] = curve;
|
|
34
|
+
const s = size;
|
|
35
|
+
const p = size / 8;
|
|
36
|
+
const g = s - p * 2;
|
|
37
|
+
const sx = (px: number) => p + g * px;
|
|
38
|
+
const sy = (py: number) => s - p - g * py;
|
|
39
|
+
const d =
|
|
40
|
+
ease === "hold"
|
|
41
|
+
? holdCurvePath(p, s - p, s - p, p)
|
|
42
|
+
: wiggle
|
|
43
|
+
? sampledPath(64, sx, sy, (progress) =>
|
|
44
|
+
evaluateWiggleEase(progress, wiggle.wiggles, wiggle.type, wiggle.amplitude),
|
|
45
|
+
)
|
|
46
|
+
: springBounce !== null
|
|
47
|
+
? sampledPath(
|
|
48
|
+
24,
|
|
49
|
+
sx,
|
|
50
|
+
(value) => sy(value / 1.2),
|
|
51
|
+
(progress) => evaluateSpringEase(progress, springBounce),
|
|
52
|
+
)
|
|
53
|
+
: `M${p},${s - p} C${sx(x1)},${sy(y1)} ${sx(x2)},${sy(y2)} ${s - p},${p}`;
|
|
54
|
+
return (
|
|
55
|
+
<svg width={s} height={s} viewBox={`0 0 ${s} ${s}`}>
|
|
56
|
+
<path
|
|
57
|
+
d={d}
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke={active ? "#3CE6AC" : "#737373"}
|
|
60
|
+
strokeWidth="1.5"
|
|
61
|
+
strokeLinecap="round"
|
|
62
|
+
/>
|
|
63
|
+
</svg>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Imports the parsers SOURCE (not the published subpath) so newly-added eases
|
|
2
|
+
// like circ.inOut resolve before the parsers dist is rebuilt.
|
|
3
|
+
import { SUPPORTED_EASES } from "../../../../parsers/src/gsapConstants";
|
|
4
|
+
import { parseSpringBounce } from "@hyperframes/core/spring-ease";
|
|
5
|
+
import { parseWiggleEase } from "@hyperframes/core/wiggle-ease";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { EASE_PRESETS, easePresetLabel } from "./easePresetLibrary";
|
|
8
|
+
import { resolveEaseCurveTuple } from "./gsapAnimationConstants";
|
|
9
|
+
|
|
10
|
+
const CUSTOM_EASE_PATTERN =
|
|
11
|
+
/^custom\(M0,0 C-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?,-?\d+(?:\.\d+)? 1,1\)$/;
|
|
12
|
+
|
|
13
|
+
describe("EASE_PRESETS", () => {
|
|
14
|
+
it("contains the complete 32-preset Graphs library with unique fields", () => {
|
|
15
|
+
expect(EASE_PRESETS).toHaveLength(32);
|
|
16
|
+
expect(new Set(EASE_PRESETS.map(({ id }) => id))).toHaveLength(32);
|
|
17
|
+
expect(new Set(EASE_PRESETS.map(({ label }) => label))).toHaveLength(32);
|
|
18
|
+
expect(new Set(EASE_PRESETS.map(({ ease }) => ease))).toHaveLength(32);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("preserves the required standard, Flow, and Bounce mappings", () => {
|
|
22
|
+
const easeByLabel = Object.fromEntries(EASE_PRESETS.map(({ label, ease }) => [label, ease]));
|
|
23
|
+
|
|
24
|
+
expect(easeByLabel).toMatchObject({
|
|
25
|
+
Linear: "none",
|
|
26
|
+
"Ease In": "power1.in",
|
|
27
|
+
"Quad In": "power2.in",
|
|
28
|
+
"Cubic In": "power3.in",
|
|
29
|
+
"Ease Out": "power1.out",
|
|
30
|
+
"Quad Out": "power2.out",
|
|
31
|
+
"Cubic Out": "power3.out",
|
|
32
|
+
"Ease In & Out": "power1.inOut",
|
|
33
|
+
"Quad Ease": "power2.inOut",
|
|
34
|
+
"Cubic Ease": "power3.inOut",
|
|
35
|
+
"Circular Ease": "circ.inOut",
|
|
36
|
+
"Ease In Back": "back.in",
|
|
37
|
+
"Ease Out Back": "back.out",
|
|
38
|
+
"Flow 1": "wiggle(1,easeInOut,0.20)",
|
|
39
|
+
"Flow 2": "wiggle(2,easeInOut,0.15)",
|
|
40
|
+
"Flow 3": "wiggle(3,easeInOut,0.12)",
|
|
41
|
+
"Flow 4": "wiggle(4,easeInOut,0.10)",
|
|
42
|
+
"Flow 5": "wiggle(5,easeInOut,0.08)",
|
|
43
|
+
"Flow 6": "wiggle(6,easeInOut,0.07)",
|
|
44
|
+
"Flow 7": "wiggle(7,easeInOut,0.06)",
|
|
45
|
+
"Bounce 1": "wiggle(4,easeOut,0.22)",
|
|
46
|
+
"Bounce 2": "wiggle(6,easeOut,0.26)",
|
|
47
|
+
"Bounce 3": "wiggle(9,uniform,0.32)",
|
|
48
|
+
"Bounce 4": "wiggle(5,anticipate,0.28)",
|
|
49
|
+
Hold: "hold",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const flows = EASE_PRESETS.filter(({ id }) => id.startsWith("flow-"));
|
|
53
|
+
expect(flows.map(({ label }) => label)).toEqual([
|
|
54
|
+
"Flow 1",
|
|
55
|
+
"Flow 2",
|
|
56
|
+
"Flow 3",
|
|
57
|
+
"Flow 4",
|
|
58
|
+
"Flow 5",
|
|
59
|
+
"Flow 6",
|
|
60
|
+
"Flow 7",
|
|
61
|
+
]);
|
|
62
|
+
expect(flows.every(({ ease }) => parseWiggleEase(ease) !== null)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("uses supported or valid custom eases that all resolve to finite geometry", () => {
|
|
66
|
+
for (const preset of EASE_PRESETS) {
|
|
67
|
+
expect(
|
|
68
|
+
SUPPORTED_EASES.includes(preset.ease as (typeof SUPPORTED_EASES)[number]) ||
|
|
69
|
+
CUSTOM_EASE_PATTERN.test(preset.ease) ||
|
|
70
|
+
parseSpringBounce(preset.ease) !== null ||
|
|
71
|
+
parseWiggleEase(preset.ease) !== null,
|
|
72
|
+
`${preset.label} has unsupported ease ${preset.ease}`,
|
|
73
|
+
).toBe(true);
|
|
74
|
+
expect(resolveEaseCurveTuple(preset.ease).every(Number.isFinite)).toBe(true);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("assigns every preset a valid kind", () => {
|
|
79
|
+
const kinds = new Set(["curve", "spring", "wiggle"]);
|
|
80
|
+
|
|
81
|
+
for (const preset of EASE_PRESETS) {
|
|
82
|
+
expect(kinds.has(preset.kind)).toBe(true);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("uses valid bounce values for spring presets", () => {
|
|
87
|
+
for (const preset of EASE_PRESETS.filter(({ kind }) => kind === "spring")) {
|
|
88
|
+
const bounce = parseSpringBounce(preset.ease);
|
|
89
|
+
|
|
90
|
+
expect(bounce).not.toBeNull();
|
|
91
|
+
expect(bounce).toBeGreaterThanOrEqual(0);
|
|
92
|
+
expect(bounce).toBeLessThanOrEqual(1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("uses parseable eases for wiggle presets", () => {
|
|
97
|
+
for (const preset of EASE_PRESETS.filter(({ kind }) => kind === "wiggle")) {
|
|
98
|
+
expect(parseWiggleEase(preset.ease)).not.toBeNull();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("preserves stable preset ids", () => {
|
|
103
|
+
for (const id of ["flow-7", "hold", "rebound-in"]) {
|
|
104
|
+
expect(EASE_PRESETS.some((preset) => preset.id === id)).toBe(true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("resolves preset labels by exact ease", () => {
|
|
109
|
+
expect(easePresetLabel("spring(0.6)")).toBe("Bouncy");
|
|
110
|
+
expect(easePresetLabel("back.in")).toBe("Ease In Back");
|
|
111
|
+
expect(easePresetLabel("wiggle(9,uniform,0.32)")).toBe("Bounce 3");
|
|
112
|
+
expect(easePresetLabel("custom(x)")).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const EASE_PRESETS = [
|
|
2
|
+
{ id: "linear", label: "Linear", ease: "none", kind: "curve" },
|
|
3
|
+
{ id: "ease-in", label: "Ease In", ease: "power1.in", kind: "curve" },
|
|
4
|
+
{ id: "quad-in", label: "Quad In", ease: "power2.in", kind: "curve" },
|
|
5
|
+
{ id: "cubic-in", label: "Cubic In", ease: "power3.in", kind: "curve" },
|
|
6
|
+
{ id: "ease-out", label: "Ease Out", ease: "power1.out", kind: "curve" },
|
|
7
|
+
{ id: "quad-out", label: "Quad Out", ease: "power2.out", kind: "curve" },
|
|
8
|
+
{ id: "cubic-out", label: "Cubic Out", ease: "power3.out", kind: "curve" },
|
|
9
|
+
{ id: "ease", label: "Ease In & Out", ease: "power1.inOut", kind: "curve" },
|
|
10
|
+
{ id: "quad-ease", label: "Quad Ease", ease: "power2.inOut", kind: "curve" },
|
|
11
|
+
{ id: "cubic-ease", label: "Cubic Ease", ease: "power3.inOut", kind: "curve" },
|
|
12
|
+
{ id: "circular-ease", label: "Circular Ease", ease: "circ.inOut", kind: "curve" },
|
|
13
|
+
{ id: "rebound-in", label: "Ease In Back", ease: "back.in", kind: "curve" },
|
|
14
|
+
{ id: "rebound-out", label: "Ease Out Back", ease: "back.out", kind: "curve" },
|
|
15
|
+
{ id: "flow-1", label: "Flow 1", ease: "wiggle(1,easeInOut,0.20)", kind: "wiggle" },
|
|
16
|
+
{ id: "flow-2", label: "Flow 2", ease: "wiggle(2,easeInOut,0.15)", kind: "wiggle" },
|
|
17
|
+
{ id: "flow-3", label: "Flow 3", ease: "wiggle(3,easeInOut,0.12)", kind: "wiggle" },
|
|
18
|
+
{ id: "flow-4", label: "Flow 4", ease: "wiggle(4,easeInOut,0.10)", kind: "wiggle" },
|
|
19
|
+
{ id: "flow-5", label: "Flow 5", ease: "wiggle(5,easeInOut,0.08)", kind: "wiggle" },
|
|
20
|
+
{ id: "flow-6", label: "Flow 6", ease: "wiggle(6,easeInOut,0.07)", kind: "wiggle" },
|
|
21
|
+
{ id: "flow-7", label: "Flow 7", ease: "wiggle(7,easeInOut,0.06)", kind: "wiggle" },
|
|
22
|
+
{ id: "bounce-1", label: "Bounce 1", ease: "wiggle(4,easeOut,0.22)", kind: "wiggle" },
|
|
23
|
+
{ id: "bounce-2", label: "Bounce 2", ease: "wiggle(6,easeOut,0.26)", kind: "wiggle" },
|
|
24
|
+
{ id: "bounce-3", label: "Bounce 3", ease: "wiggle(9,uniform,0.32)", kind: "wiggle" },
|
|
25
|
+
{ id: "bounce-4", label: "Bounce 4", ease: "wiggle(5,anticipate,0.28)", kind: "wiggle" },
|
|
26
|
+
{ id: "hold", label: "Hold", ease: "hold", kind: "curve" },
|
|
27
|
+
{
|
|
28
|
+
id: "rebound-ease",
|
|
29
|
+
label: "Ease In & Out Back",
|
|
30
|
+
ease: "back.inOut",
|
|
31
|
+
kind: "curve",
|
|
32
|
+
},
|
|
33
|
+
{ id: "expo-in", label: "Expo In", ease: "expo.in", kind: "curve" },
|
|
34
|
+
{ id: "expo-out", label: "Expo Out", ease: "expo.out", kind: "curve" },
|
|
35
|
+
// Runtime spring(bounce) approximates Figma stiffness and damping with bounce alone.
|
|
36
|
+
{ id: "spring-gentle", label: "Gentle", ease: "spring(0.15)", kind: "spring" },
|
|
37
|
+
{ id: "spring-quick", label: "Quick", ease: "spring(0.4)", kind: "spring" },
|
|
38
|
+
{ id: "spring-bouncy", label: "Bouncy", ease: "spring(0.6)", kind: "spring" },
|
|
39
|
+
{ id: "spring-slow", label: "Slow", ease: "spring(0.25)", kind: "spring" },
|
|
40
|
+
] as const satisfies ReadonlyArray<{
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
ease: string;
|
|
44
|
+
kind: "curve" | "spring" | "wiggle";
|
|
45
|
+
}>;
|
|
46
|
+
|
|
47
|
+
export function easePresetLabel(ease: string): string | null {
|
|
48
|
+
return EASE_PRESETS.find((preset) => preset.ease === ease)?.label ?? null;
|
|
49
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { controlPointsForGsapEase } from "./studioMotion";
|
|
1
|
+
import { controlPointsForGsapEase, parseStudioCustomEaseData } from "./studioMotion";
|
|
2
2
|
|
|
3
3
|
export const METHOD_LABELS: Record<string, string> = {
|
|
4
4
|
set: "Set",
|
|
@@ -116,9 +116,14 @@ export const EASE_CURVES: Record<string, [number, number, number, number]> = {
|
|
|
116
116
|
"back.out": [0.34, 1.56, 0.64, 1],
|
|
117
117
|
"back.in": [0.36, 0, 0.66, -0.56],
|
|
118
118
|
"back.inOut": [0.68, -0.55, 0.27, 1.55],
|
|
119
|
+
"circ.inOut": [0.785, 0.135, 0.15, 0.86],
|
|
119
120
|
"expo.out": [0.16, 1, 0.3, 1],
|
|
120
121
|
"expo.in": [0.7, 0, 0.84, 0],
|
|
121
122
|
"expo.inOut": [0.87, 0, 0.13, 1],
|
|
123
|
+
"bounce.out": [0.34, 1.56, 0.64, 0.74],
|
|
124
|
+
"bounce.in": [0.36, 0.26, 0.66, -0.56],
|
|
125
|
+
"elastic.out(1,0.3)": [0.16, 1.45, 0.28, 0.82],
|
|
126
|
+
"elastic.inOut(1,0.3)": [0.68, -0.55, 0.32, 1.55],
|
|
122
127
|
// After Effects polarity: "in" eases into the keyframe (slow END, CP2 y=1),
|
|
123
128
|
// "out" eases out of it (slow START, CP1 y=0). Matches the "(AE)" labels.
|
|
124
129
|
"ae-ease": [0.333, 0, 0.667, 1],
|
|
@@ -126,18 +131,15 @@ export const EASE_CURVES: Record<string, [number, number, number, number]> = {
|
|
|
126
131
|
"ae-ease-out": [0.333, 0, 0.667, 0.667],
|
|
127
132
|
};
|
|
128
133
|
|
|
129
|
-
export function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const nums = data.match(/[\d.]+/g)?.map(Number);
|
|
139
|
-
if (!nums || nums.length < 6) return controlPointsForGsapEase("power2.out");
|
|
140
|
-
return { x1: nums[2], y1: nums[3], x2: nums[4], y2: nums[5] };
|
|
134
|
+
export function resolveEaseCurveTuple(ease: string): [number, number, number, number] {
|
|
135
|
+
if (ease.startsWith("custom(")) {
|
|
136
|
+
const points = parseStudioCustomEaseData(ease.match(/^custom\((.+)\)$/)?.[1]);
|
|
137
|
+
if (points) return [points.x1, points.y1, points.x2, points.y2];
|
|
138
|
+
}
|
|
139
|
+
const curve = EASE_CURVES[ease];
|
|
140
|
+
if (curve) return curve;
|
|
141
|
+
const points = controlPointsForGsapEase(ease);
|
|
142
|
+
return [points.x1, points.y1, points.x2, points.y2];
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
export const PERCENT_PROPS = new Set(["opacity", "autoAlpha"]);
|