@hyperframes/studio 0.8.104 → 0.8.106
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-DAoaajuM.js → hyperframes-player-dl6K6ABp.js} +1 -1
- package/dist/assets/index-CMiYVGv5.js +399 -0
- package/dist/assets/{index-WV9LroAD.js → index-DKT5NF1Y.js} +1 -1
- package/dist/assets/index-lrT-gfHa.css +1 -0
- package/dist/assets/{index-BDK4rTMb.js → index-ooaZZhKf.js} +1 -1
- package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
- package/dist/chunk-EEXSYY5F.js.map +1 -0
- package/dist/{chunk-HIGHVWYD.js → chunk-ICTZJPQQ.js} +3 -2
- package/dist/chunk-ICTZJPQQ.js.map +1 -0
- package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
- package/dist/chunk-NCUBAVGK.js.map +1 -0
- package/dist/index.css +6 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.html +11 -2
- package/dist/index.js +10391 -9836
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui.js +2 -2
- package/package.json +9 -9
- package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
- package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
- package/src/captions/components/CaptionTimeline.tsx +3 -3
- package/src/captions/keyboard.test.ts +6 -0
- package/src/captions/keyboard.ts +2 -1
- package/src/components/AskAgentModal.tsx +1 -1
- package/src/components/LintModal.tsx +35 -35
- package/src/components/StudioErrorBoundary.tsx +1 -1
- package/src/components/StudioHeader.dom.test.tsx +35 -13
- package/src/components/StudioHeader.tsx +6 -2
- package/src/components/StudioToast.tsx +7 -16
- package/src/components/ThemeToggle.tsx +99 -0
- package/src/components/TimelineToolPicker.tsx +1 -1
- package/src/components/TimelineToolbar.tsx +11 -11
- package/src/components/dock/Dock.tsx +1 -1
- package/src/components/dock/dock.css +9 -1
- package/src/components/editor/AnimationCard.tsx +5 -5
- package/src/components/editor/AnimationCardParts.tsx +1 -1
- package/src/components/editor/BlockParamsPanel.tsx +3 -3
- package/src/components/editor/BorderRadiusEditor.tsx +8 -15
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +37 -5
- package/src/components/editor/DomEditRotateHandle.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +14 -19
- package/src/components/editor/EaseModeControls.tsx +2 -2
- package/src/components/editor/EaseParamFields.tsx +8 -8
- package/src/components/editor/FileTree.tsx +1 -1
- package/src/components/editor/FileTreeIcons.tsx +47 -26
- package/src/components/editor/FileTreeNodes.tsx +11 -8
- package/src/components/editor/GestureRecordControl.tsx +3 -3
- package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
- package/src/components/editor/GsapAnimationSection.tsx +2 -2
- package/src/components/editor/InlineTextToolbar.tsx +4 -4
- package/src/components/editor/InspectorHeaderActions.tsx +1 -1
- package/src/components/editor/KeyframeDiamond.tsx +6 -4
- package/src/components/editor/KeyframeNavigation.tsx +4 -4
- package/src/components/editor/LayersPanel.tsx +3 -3
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
- package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +5 -5
- package/src/components/editor/SourceEditor.tsx +2 -6
- package/src/components/editor/TimelineFxPopover.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +1 -1
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
- package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
- package/src/components/editor/easeCurveSvg.tsx +1 -1
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
- package/src/components/editor/manualEditsDom.ts +36 -3
- package/src/components/editor/manualEditsDomPatches.test.ts +2 -86
- package/src/components/editor/manualEditsDomPatches.ts +0 -30
- package/src/components/editor/manualEditsSnapshot.ts +1 -0
- package/src/components/editor/marqueeCommit.test.tsx +69 -1
- package/src/components/editor/marqueeCommit.ts +6 -1
- package/src/components/editor/overlayFrameLoop.test.ts +27 -0
- package/src/components/editor/overlayFrameLoop.ts +21 -0
- package/src/components/editor/persistSeam.integration.test.ts +0 -18
- package/src/components/editor/plainTranslate.ts +34 -3
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
- package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
- package/src/components/editor/propertyPanelFill.tsx +6 -6
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
- package/src/components/editor/propertyPanelFont.test.tsx +80 -3
- package/src/components/editor/propertyPanelFont.tsx +71 -48
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +2 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
- package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
- package/src/components/editor/propertyPanelHelpers.ts +1 -2
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
- package/src/components/editor/propertyPanelValueTier.ts +1 -1
- package/src/components/editor/studioCodeTheme.test.ts +20 -0
- package/src/components/editor/studioCodeTheme.ts +99 -0
- package/src/components/editor/useDomEditNudge.test.tsx +31 -0
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useInlineTextEditing.tsx +1 -1
- package/src/components/editor/useMotionPathData.test.tsx +64 -0
- package/src/components/editor/useMotionPathData.ts +5 -1
- package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
- package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
- package/src/components/nle/AudioMeterStrip.tsx +2 -2
- package/src/components/nle/NLEPreview.test.ts +8 -4
- package/src/components/nle/PreviewOverlays.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/PreviewPoster.tsx +29 -1
- package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
- package/src/components/panels/SlideshowPanel.tsx +4 -4
- package/src/components/panels/SlideshowSubPanels.tsx +19 -21
- package/src/components/panels/VariablesBindElement.tsx +1 -1
- package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +4 -6
- package/src/components/panels/VariablesRowAction.tsx +1 -1
- package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
- package/src/components/renders/RenderQueue.tsx +2 -2
- package/src/components/renders/RenderQueueItem.tsx +3 -3
- package/src/components/sidebar/AssetCard.tsx +3 -3
- package/src/components/sidebar/AssetContextMenu.tsx +7 -7
- package/src/components/sidebar/AssetsTab.tsx +6 -6
- package/src/components/sidebar/AudioRow.tsx +5 -3
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
- package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
- package/src/components/sidebar/CompositionsTab.tsx +65 -12
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/sidebar/SidebarLintButton.tsx +3 -3
- package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
- package/src/components/timelineToolbarStyles.ts +3 -3
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/HyperframesLogo.tsx +19 -18
- package/src/components/ui/Input.tsx +1 -1
- package/src/components/ui/Menu.tsx +4 -2
- package/src/hooks/appHotkeysDispatch.ts +2 -1
- package/src/hooks/elementOffsetStager.ts +8 -7
- package/src/hooks/previewPromotionSelection.test.tsx +180 -0
- package/src/hooks/useAppHotkeys.test.ts +32 -0
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useClipboard.source.test.ts +141 -23
- package/src/hooks/useClipboard.ts +154 -123
- package/src/hooks/useDomEditCommits.ts +0 -3
- package/src/hooks/useDomEditSession.test.tsx +76 -72
- package/src/hooks/useDomEditSession.ts +7 -43
- package/src/hooks/useDomGeometryCommit.test.tsx +4 -1
- package/src/hooks/useDomGeometryCommit.ts +0 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +177 -32
- package/src/hooks/useDomGeometryCommits.ts +4 -49
- package/src/hooks/useGsapTweenCache.ts +1 -2
- package/src/hooks/useInlineTextEdit.ts +16 -2
- package/src/hooks/useLivePreviewIframe.ts +18 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
- package/src/hooks/usePreviewPersistence.ts +2 -5
- package/src/hooks/useRenderClipContent.ts +3 -3
- package/src/index.ts +2 -0
- package/src/livePreviewIframeExport.test.tsx +33 -0
- package/src/main.tsx +4 -1
- package/src/player/components/AudioWaveform.test.tsx +36 -15
- package/src/player/components/AudioWaveform.tsx +24 -7
- package/src/player/components/ClipContextMenu.tsx +1 -1
- package/src/player/components/CompositionThumbnail.tsx +23 -11
- package/src/player/components/EditModal.tsx +6 -6
- package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +3 -3
- package/src/player/components/ShortcutsPanel.tsx +8 -20
- package/src/player/components/TimelineCanvas.tsx +1 -1
- package/src/player/components/TimelineClip.test.tsx +38 -0
- package/src/player/components/TimelineClip.tsx +16 -8
- package/src/player/components/TimelineClipFades.test.tsx +376 -74
- package/src/player/components/TimelineClipFades.tsx +368 -137
- package/src/player/components/TimelineEmptyState.tsx +2 -2
- package/src/player/components/TimelineLanes.tsx +0 -1
- package/src/player/components/TrackClipCount.tsx +1 -1
- package/src/player/components/VolumeControl.tsx +1 -1
- package/src/player/components/timelineClipChildren.tsx +1 -1
- package/src/player/components/timelineMotionStyles.test.ts +6 -22
- package/src/player/components/trackHeaderLabelRows.tsx +16 -16
- package/src/player/components/useTimelineKeyboardActor.ts +1 -1
- package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
- package/src/player/hooks/useShadowPreviewReload.ts +72 -5
- package/src/player/lib/automationStoreSync.test.ts +18 -1
- package/src/player/lib/automationStoreSync.ts +29 -17
- package/src/player/lib/thumbnailScheduler.ts +4 -5
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/player/sceneSwap.ts +18 -0
- package/src/styles/components.css +26 -0
- package/src/styles/hex-baseline.json +7 -19
- package/src/styles/studio.css +4 -7
- package/src/styles/tailwind-preset.shared.js +2 -1
- package/src/styles/theme.css +214 -147
- package/src/styles/theme.test.ts +13 -9
- package/src/utils/blockCategories.ts +41 -9
- package/src/utils/clipboardPayload.ts +12 -1
- package/src/utils/studioPendingEdits.ts +21 -0
- package/src/utils/studioTheme.test.tsx +141 -0
- package/src/utils/studioTheme.ts +34 -0
- package/src/utils/studioUiPreferences.ts +3 -0
- package/src/utils/typingTarget.test.ts +8 -1
- package/src/utils/typingTarget.ts +5 -0
- package/dist/assets/index-CQN3M56N.css +0 -1
- package/dist/assets/index-De2aO4XM.js +0 -399
- package/dist/chunk-62DDWOSH.js.map +0 -1
- package/dist/chunk-6GAO75IU.js.map +0 -1
- package/dist/chunk-HIGHVWYD.js.map +0 -1
- package/src/hooks/gsapTargetCache.ts +0 -62
|
@@ -63,13 +63,13 @@ export function TimelineEmptyState({
|
|
|
63
63
|
strokeWidth="1.5"
|
|
64
64
|
strokeLinecap="round"
|
|
65
65
|
strokeLinejoin="round"
|
|
66
|
-
className="text-
|
|
66
|
+
className="text-accent-ink shrink-0"
|
|
67
67
|
>
|
|
68
68
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
69
69
|
<polyline points="7 10 12 15 17 10" />
|
|
70
70
|
<line x1="12" y1="15" x2="12" y2="3" />
|
|
71
71
|
</svg>
|
|
72
|
-
<span className="text-[13px] text-
|
|
72
|
+
<span className="text-[13px] text-accent-ink">Drop media files to import</span>
|
|
73
73
|
</>
|
|
74
74
|
) : (
|
|
75
75
|
<>
|
|
@@ -9,7 +9,7 @@ export function TrackClipCount({ clipCount }: { clipCount: number }) {
|
|
|
9
9
|
<span
|
|
10
10
|
aria-label={`${clipCount} clips`}
|
|
11
11
|
title={`${clipCount} clips`}
|
|
12
|
-
className="shrink-0 rounded-full bg-
|
|
12
|
+
className="shrink-0 rounded-full bg-hover px-1 text-[9px] leading-[14px] tabular-nums text-text-muted"
|
|
13
13
|
>
|
|
14
14
|
{clipCount}
|
|
15
15
|
</span>
|
|
@@ -93,7 +93,7 @@ export const VolumeControl = memo(function VolumeControl({
|
|
|
93
93
|
aria-label={muteLabel}
|
|
94
94
|
aria-pressed={silent}
|
|
95
95
|
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-30 ${
|
|
96
|
-
silent ? "text-
|
|
96
|
+
silent ? "text-accent-ink" : "text-neutral-500 hover:text-neutral-200"
|
|
97
97
|
}`}
|
|
98
98
|
>
|
|
99
99
|
<VolumeIcon muted={audioMuted} volume={audioVolume} />
|
|
@@ -22,7 +22,7 @@ function ClipLintDot({ element }: { element: TimelineElement }) {
|
|
|
22
22
|
if (!lint || lint.count === 0) return null;
|
|
23
23
|
return (
|
|
24
24
|
<span
|
|
25
|
-
className="absolute w-1.5 h-1.5 rounded-full bg-
|
|
25
|
+
className="absolute w-1.5 h-1.5 rounded-full bg-warning-ink"
|
|
26
26
|
style={{ top: 7, right: 7 }}
|
|
27
27
|
title={lint.messages.join("\n")}
|
|
28
28
|
/>
|
|
@@ -39,18 +39,15 @@ function themeTokenValue(token: string): string {
|
|
|
39
39
|
|
|
40
40
|
describe("timeline motion styles", () => {
|
|
41
41
|
it.each([
|
|
42
|
-
["--timeline-track-label", "
|
|
43
|
-
["--timeline-tick-text", "
|
|
44
|
-
["--timeline-border-strong", "
|
|
45
|
-
["--timeline-group-member-tint", "
|
|
46
|
-
["--timeline-overlay-text", "
|
|
47
|
-
["--timeline-
|
|
48
|
-
["--timeline-playhead-glow", "rgba(60, 230, 172, 0.14)"],
|
|
49
|
-
["--timeline-playhead-shadow", "rgba(0, 0, 0, 0.55)"],
|
|
42
|
+
["--timeline-track-label", "var(--color-text-muted)"],
|
|
43
|
+
["--timeline-tick-text", "var(--color-text-2)"],
|
|
44
|
+
["--timeline-border-strong", "color-mix(in oklab, var(--color-text-0) 20%, transparent)"],
|
|
45
|
+
["--timeline-group-member-tint", "color-mix(in oklab, var(--color-text-0) 3.5%, transparent)"],
|
|
46
|
+
["--timeline-overlay-text", "var(--color-text-2)"],
|
|
47
|
+
["--timeline-playhead-glow", "color-mix(in oklab, var(--color-accent) 14%, transparent)"],
|
|
50
48
|
])("keeps the migrated default for %s", (token, expected) => {
|
|
51
49
|
expect(themeTokenValue(token)).toBe(expected);
|
|
52
50
|
});
|
|
53
|
-
|
|
54
51
|
it("keeps clip motion reduced-motion gated and layout safe", () => {
|
|
55
52
|
const mediaStart = studioCss.indexOf("@media (prefers-reduced-motion: no-preference)");
|
|
56
53
|
expect(mediaStart).toBeGreaterThanOrEqual(0);
|
|
@@ -93,13 +90,7 @@ describe("timeline motion styles", () => {
|
|
|
93
90
|
expect(timelineClipLabelRule).toContain("text-overflow: ellipsis");
|
|
94
91
|
expect(timelineClipLabelRule).toContain("max-width: calc(100% - 22px)");
|
|
95
92
|
expect(timelineClipTimecodeRule).toContain("color: var(--timeline-tick-text)");
|
|
96
|
-
expect(themeCss).toContain("--timeline-clip-bg: rgba(255, 255, 255, 0.12)");
|
|
97
|
-
expect(themeCss).toContain("--timeline-clip-border: rgba(255, 255, 255, 0.22)");
|
|
98
|
-
expect(themeCss).toContain("--timeline-track-label: rgba(255, 255, 255, 0.5)");
|
|
99
|
-
expect(themeCss).toContain("--timeline-tick-text: rgba(255, 255, 255, 0.34)");
|
|
100
|
-
expect(themeCss).toContain("--timeline-clip-label-active: rgba(232, 255, 247, 0.95)");
|
|
101
93
|
expect(themeCss).toContain("--timeline-clip-label-shadow: 0 1px 2px rgba(0, 0, 0, 0.85)");
|
|
102
|
-
expect(themeCss).toContain("--timeline-clip-selection: rgba(255, 255, 255, 0.85)");
|
|
103
94
|
expect(themeCss).toContain("--timeline-clip-selection-highlight: rgb(255 255 255 / 0.55)");
|
|
104
95
|
expect(themeCss).toContain("--timeline-clip-radius: 8px");
|
|
105
96
|
expect(themeCss).toContain("--timeline-clip-audio-radius: 21px");
|
|
@@ -108,18 +99,11 @@ describe("timeline motion styles", () => {
|
|
|
108
99
|
expect(themeCss).toContain("--timeline-clip-chip-bg: rgba(15, 22, 18, 0.73)");
|
|
109
100
|
expect(themeCss).toContain("--timeline-clip-chip-audio-bg: rgba(25, 16, 38, 0.7)");
|
|
110
101
|
expect(themeCss).toContain("--timeline-clip-chip-text: #ffffff");
|
|
111
|
-
expect(themeCss).toContain("--timeline-row-bg: #edf0ed");
|
|
112
102
|
expect(themeCss).toContain("--timeline-clip-shadow: 0 2px 6px rgb(0 0 0 / 0.12)");
|
|
113
|
-
expect(themeCss).toContain("--timeline-clip-selection: #0f766e");
|
|
114
|
-
expect(themeCss).toContain("--timeline-clip-audio-bg: rgba(167, 139, 250, 0.16)");
|
|
115
|
-
expect(themeCss).toContain("--timeline-clip-audio-border: rgba(167, 139, 250, 0.4)");
|
|
116
|
-
expect(themeCss).toContain("--timeline-clip-audio-bg-hover: rgba(167, 139, 250, 0.24)");
|
|
117
|
-
expect(themeCss).toContain("--timeline-clip-audio-bg-dragging: rgba(60, 52, 84, 0.96)");
|
|
118
103
|
expect(audioClipRule).toContain("background-color: var(--timeline-clip-audio-bg)");
|
|
119
104
|
expect(audioClipRule).toContain("border-color: var(--timeline-clip-audio-border)");
|
|
120
105
|
expect(audioClipHoverRule).toContain("background-color: var(--timeline-clip-audio-bg-hover)");
|
|
121
106
|
expect(audioClipDraggingRule).toContain("var(--timeline-clip-audio-bg-dragging)");
|
|
122
|
-
expect(themeCss).toContain("--timeline-clip-selection: rgba(255, 255, 255, 0.85)");
|
|
123
107
|
expect(selectedTimelineClipRule).toContain(
|
|
124
108
|
"outline: 1.5px solid var(--timeline-clip-selection)",
|
|
125
109
|
);
|
|
@@ -38,8 +38,8 @@ function PropertyGroupNavigation({
|
|
|
38
38
|
// target is met with a centered transparent ::before overlay instead of a
|
|
39
39
|
// bigger box; focus-visible matches every other control in this header.
|
|
40
40
|
const CHEVRON_BUTTON_CLASS =
|
|
41
|
-
"relative h-5 w-3 border-0 bg-transparent p-0 text-
|
|
42
|
-
"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-
|
|
41
|
+
"relative h-5 w-3 border-0 bg-transparent p-0 text-text-2 hover:text-text-0 disabled:text-text-off " +
|
|
42
|
+
"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-ring " +
|
|
43
43
|
"before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 " +
|
|
44
44
|
"before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']";
|
|
45
45
|
const seekTo = (keyframe: { percentage: number } | null) => {
|
|
@@ -120,7 +120,7 @@ export function PropertyGroupHeaderRow({
|
|
|
120
120
|
tabIndex={rovingTargetId === timelinePropertyRowId(elementId, lane.group) ? 0 : -1}
|
|
121
121
|
data-property-group={lane.group}
|
|
122
122
|
data-timeline-lane-top={getTimelineLaneTop(laneIndex)}
|
|
123
|
-
className="absolute left-0 flex items-center gap-1 overflow-hidden px-1.5 text-[10px] text-
|
|
123
|
+
className="absolute left-0 flex items-center gap-1 overflow-hidden px-1.5 text-[10px] text-text-2"
|
|
124
124
|
style={{
|
|
125
125
|
top: getTimelineLaneTop(laneIndex),
|
|
126
126
|
// The header column narrows to contentOrigin whenever that is under
|
|
@@ -134,12 +134,12 @@ export function PropertyGroupHeaderRow({
|
|
|
134
134
|
{/* Tree connector: vertical spine (top-half on the last lane) + branch tick. */}
|
|
135
135
|
<span className="relative h-full w-3 shrink-0" aria-hidden="true">
|
|
136
136
|
<span
|
|
137
|
-
className="absolute left-1.5 top-0 w-px bg-
|
|
137
|
+
className="absolute left-1.5 top-0 w-px bg-text-0/15"
|
|
138
138
|
style={{ height: isLastLane ? "50%" : "100%" }}
|
|
139
139
|
/>
|
|
140
|
-
<span className="absolute left-1.5 top-1/2 h-px w-1.5 bg-
|
|
140
|
+
<span className="absolute left-1.5 top-1/2 h-px w-1.5 bg-text-0/15" />
|
|
141
141
|
</span>
|
|
142
|
-
<span className="w-[46px] shrink-0 truncate text-
|
|
142
|
+
<span className="w-[46px] shrink-0 truncate text-text-0" title={label}>
|
|
143
143
|
{label}
|
|
144
144
|
</span>
|
|
145
145
|
<PropertyGroupNavigation
|
|
@@ -154,7 +154,7 @@ export function PropertyGroupHeaderRow({
|
|
|
154
154
|
aria-label={`${navigation.currentKeyframe ? "Remove" : "Add"} ${label} keyframe`}
|
|
155
155
|
title={`${navigation.currentKeyframe ? "Remove" : "Add"} ${label} keyframe`}
|
|
156
156
|
// h-6 w-6 = the 24x24 WCAG 2.2 minimum target; the ◆ glyph stays 11px.
|
|
157
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-
|
|
157
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-accent-ink focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-ring"
|
|
158
158
|
onClick={(event) => {
|
|
159
159
|
// Same as the disclosure caret and the eye: a control in the label
|
|
160
160
|
// column owns its click, it does not also hit the track row behind it.
|
|
@@ -168,7 +168,7 @@ export function PropertyGroupHeaderRow({
|
|
|
168
168
|
</button>
|
|
169
169
|
</PropertyGroupNavigation>
|
|
170
170
|
<span
|
|
171
|
-
className="min-w-0 flex-1 truncate text-right tabular-nums text-
|
|
171
|
+
className="min-w-0 flex-1 truncate text-right tabular-nums text-text-muted"
|
|
172
172
|
title={valueReadout(lane.group, values)}
|
|
173
173
|
>
|
|
174
174
|
{valueReadout(lane.group, values)}
|
|
@@ -231,7 +231,7 @@ export function AutomationLaneHeaderRow({
|
|
|
231
231
|
<div
|
|
232
232
|
data-automation-lane-label={label}
|
|
233
233
|
data-timeline-lane-top={top}
|
|
234
|
-
className="absolute left-0 flex items-center gap-1 overflow-hidden px-1.5 text-[10px] text-
|
|
234
|
+
className="absolute left-0 flex items-center gap-1 overflow-hidden px-1.5 text-[10px] text-text-2"
|
|
235
235
|
style={{
|
|
236
236
|
top,
|
|
237
237
|
width: columnWidth,
|
|
@@ -243,10 +243,10 @@ export function AutomationLaneHeaderRow({
|
|
|
243
243
|
tick at the name's own height. */}
|
|
244
244
|
<span className="relative h-full w-3 shrink-0" aria-hidden="true">
|
|
245
245
|
<span
|
|
246
|
-
className="absolute left-1.5 top-0 w-px bg-
|
|
246
|
+
className="absolute left-1.5 top-0 w-px bg-text-0/15"
|
|
247
247
|
style={{ height: isLastLane ? "50%" : "100%" }}
|
|
248
248
|
/>
|
|
249
|
-
<span className="absolute left-1.5 top-1/2 h-px w-1.5 bg-
|
|
249
|
+
<span className="absolute left-1.5 top-1/2 h-px w-1.5 bg-text-0/15" />
|
|
250
250
|
</span>
|
|
251
251
|
{/* Two lines: what the effect is, then which knob the envelope drives. On
|
|
252
252
|
one line a band's own name was the first thing truncated in a column this
|
|
@@ -262,7 +262,7 @@ export function AutomationLaneHeaderRow({
|
|
|
262
262
|
aria-label={onReveal ? `Show ${label} in the effect rack` : undefined}
|
|
263
263
|
title={onReveal ? `Show ${label} in the effect rack` : label}
|
|
264
264
|
disabled={!onReveal}
|
|
265
|
-
className="flex min-w-0 flex-1 flex-col justify-center rounded-sm border-0 bg-transparent p-0 text-left leading-tight enabled:hover:text-
|
|
265
|
+
className="flex min-w-0 flex-1 flex-col justify-center rounded-sm border-0 bg-transparent p-0 text-left leading-tight enabled:hover:text-text-0 focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-ring"
|
|
266
266
|
onPointerDown={(event) => event.stopPropagation()}
|
|
267
267
|
onClick={(event) => {
|
|
268
268
|
// The label column owns its click; it does not also fall through to
|
|
@@ -271,13 +271,13 @@ export function AutomationLaneHeaderRow({
|
|
|
271
271
|
onReveal?.();
|
|
272
272
|
}}
|
|
273
273
|
>
|
|
274
|
-
<span data-automation-lane-name="" className="truncate font-mono text-[9px] text-
|
|
274
|
+
<span data-automation-lane-name="" className="truncate font-mono text-[9px] text-text-2">
|
|
275
275
|
{name}
|
|
276
276
|
</span>
|
|
277
277
|
{param ? (
|
|
278
278
|
<span
|
|
279
279
|
data-automation-lane-param=""
|
|
280
|
-
className="truncate font-mono text-[9px] text-
|
|
280
|
+
className="truncate font-mono text-[9px] text-text-muted"
|
|
281
281
|
>
|
|
282
282
|
{param}
|
|
283
283
|
</span>
|
|
@@ -285,7 +285,7 @@ export function AutomationLaneHeaderRow({
|
|
|
285
285
|
{alsoAutomatedBy ? (
|
|
286
286
|
<span
|
|
287
287
|
data-automation-lane-also=""
|
|
288
|
-
className="truncate text-[9px] text-
|
|
288
|
+
className="truncate text-[9px] text-warning-ink"
|
|
289
289
|
title={`${alsoAutomatedBy} is also fading this — the two multiply.`}
|
|
290
290
|
>
|
|
291
291
|
{alsoAutomatedBy} is also fading this.
|
|
@@ -311,7 +311,7 @@ export function AutomationLaneHeaderRow({
|
|
|
311
311
|
aria-label={`Remove ${label} automation`}
|
|
312
312
|
title={`Remove ${label} automation`}
|
|
313
313
|
// h-6 w-6 is the 24x24 WCAG 2.2 target; the glyph stays small.
|
|
314
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-
|
|
314
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-text-muted hover:text-text-0 focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-ring"
|
|
315
315
|
onPointerDown={(event) => event.stopPropagation()}
|
|
316
316
|
onClick={(event) => {
|
|
317
317
|
// A control in the label column owns its click; it does not also hit
|
|
@@ -22,7 +22,7 @@ function eventTarget(event: FocusEvent | KeyboardEvent): HTMLElement | null {
|
|
|
22
22
|
// Header actions stay native Tab stops because they have no row-level shortcut.
|
|
23
23
|
// The nearest interactive ancestor wins so their events never masquerade as row events.
|
|
24
24
|
const target = event.target.closest<HTMLElement>(
|
|
25
|
-
"button, input, select, textarea, a[href], [contenteditable], [data-timeline-focus-id]",
|
|
25
|
+
"button, input, select, textarea, a[href], [contenteditable], [role='slider'], [data-timeline-focus-id]",
|
|
26
26
|
);
|
|
27
27
|
return target?.dataset.timelineFocusId && event.currentTarget.contains(target) ? target : null;
|
|
28
28
|
}
|
|
@@ -118,3 +118,30 @@ export function attachIframeAdapter(
|
|
|
118
118
|
attachIframeWindow(api, win);
|
|
119
119
|
return adapter;
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
export function makePreview(body: string, query = ""): HTMLIFrameElement {
|
|
123
|
+
const iframe = makeFakeIframe(makeAdapterWindow().win);
|
|
124
|
+
iframe.src = `http://localhost/api/projects/demo/preview${query}`;
|
|
125
|
+
iframe.contentDocument!.body.innerHTML = body;
|
|
126
|
+
return iframe;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function mountPlayerWithPreview(live: HTMLIFrameElement) {
|
|
130
|
+
const harness = renderTimelinePlayerHarness();
|
|
131
|
+
act(() => {
|
|
132
|
+
harness.getApi().iframeRef.current = live;
|
|
133
|
+
harness.getApi().onIframeLoad();
|
|
134
|
+
});
|
|
135
|
+
return harness;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function paintShadow(getApi: () => TimelinePlayerApi, shadow: HTMLIFrameElement) {
|
|
139
|
+
const slot = getApi().previewSlots.find((s) => s.role === "shadow");
|
|
140
|
+
if (!slot) throw new Error("no shadow reload is pending");
|
|
141
|
+
act(() => getApi().setShadowIframeNode(shadow));
|
|
142
|
+
await act(async () => {
|
|
143
|
+
getApi().onShadowIframeLoad(slot.gen);
|
|
144
|
+
getApi().onShadowReadyChange(slot.gen, true);
|
|
145
|
+
});
|
|
146
|
+
return slot.gen;
|
|
147
|
+
}
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import { useCallback, useRef, useState } from "react";
|
|
5
5
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
6
|
+
import {
|
|
7
|
+
afterStudioManualEditGestures,
|
|
8
|
+
isStudioManualEditGestureLiveIn,
|
|
9
|
+
studioManualEditSavesIn,
|
|
10
|
+
} from "../../components/editor/manualEditsDom";
|
|
6
11
|
import { logReload } from "../../utils/reloadDebug";
|
|
7
12
|
import {
|
|
8
13
|
useTimelineSyncCallbacks,
|
|
@@ -14,6 +19,8 @@ import {
|
|
|
14
19
|
} from "./useTimelineSyncCallbacks";
|
|
15
20
|
import type { IframeWindow, PlaybackAdapter } from "../lib/playbackTypes";
|
|
16
21
|
import { thumbnailScheduler } from "../lib/thumbnailScheduler";
|
|
22
|
+
import { announcePreviewPromoted } from "../sceneSwap";
|
|
23
|
+
import { afterStudioPendingEdits, isStudioEditSaving } from "../../utils/studioPendingEdits";
|
|
17
24
|
import { usePlayerStore } from "../store/playerStore";
|
|
18
25
|
|
|
19
26
|
// One wait budget for a shadow: the player's 8s asset cap plus its 0.42s loader fade
|
|
@@ -78,12 +85,18 @@ export function useShadowPreviewReload({
|
|
|
78
85
|
// The shadow still owed a wait budget, so a hidden tab can resume it when it becomes visible.
|
|
79
86
|
const budgetGenRef = useRef<number | null>(null);
|
|
80
87
|
const cancelPendingLoadRef = useRef<() => void>(() => {});
|
|
88
|
+
const stopHoldRef = useRef<() => void>(() => {});
|
|
89
|
+
const shadowUrlRef = useRef("");
|
|
90
|
+
const savesAtRequestRef = useRef(0);
|
|
91
|
+
const beginShadowReloadRef = useRef<(url: string) => void>(() => {});
|
|
92
|
+
const promoteWhenReadyRef = useRef<(gen: number) => void>(() => {});
|
|
81
93
|
const [previewSlots, setPreviewSlots] = useState<PreviewIframeSlot[]>([{ gen: 0, role: "live" }]);
|
|
82
94
|
|
|
83
95
|
const stopPendingShadow = useCallback(() => {
|
|
84
96
|
clearTimeout(readyTimerRef.current);
|
|
85
97
|
budgetGenRef.current = null;
|
|
86
98
|
cancelPendingLoadRef.current();
|
|
99
|
+
stopHoldRef.current();
|
|
87
100
|
pendingCommitRef.current = null;
|
|
88
101
|
visuallyReadyGenRef.current = null;
|
|
89
102
|
}, []);
|
|
@@ -106,12 +119,58 @@ export function useShadowPreviewReload({
|
|
|
106
119
|
[stopPendingShadow, isRefreshingRef, pendingSeekRef],
|
|
107
120
|
);
|
|
108
121
|
|
|
122
|
+
const gestureSavesOnScreen = useCallback(() => {
|
|
123
|
+
const liveDoc = iframeRef.current?.contentDocument;
|
|
124
|
+
return liveDoc ? studioManualEditSavesIn(liveDoc) : 0;
|
|
125
|
+
}, [iframeRef]);
|
|
126
|
+
|
|
127
|
+
const waitThenRetry = useCallback(
|
|
128
|
+
(gen: number, why: string, wait: (retry: () => void) => () => void) => {
|
|
129
|
+
clearTimeout(readyTimerRef.current);
|
|
130
|
+
budgetGenRef.current = null;
|
|
131
|
+
stopHoldRef.current();
|
|
132
|
+
logReload("shadow-held", { gen, why });
|
|
133
|
+
stopHoldRef.current = wait(() => {
|
|
134
|
+
logReload("shadow-hold-ended", { gen, why });
|
|
135
|
+
promoteWhenReadyRef.current(gen);
|
|
136
|
+
});
|
|
137
|
+
return true;
|
|
138
|
+
},
|
|
139
|
+
[],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Nothing replaces the preview under a gesture, and a shadow requested before a gesture's save
|
|
143
|
+
// shows the file before it: once that save has landed, a fresh one loads instead.
|
|
144
|
+
const heldOrStale = useCallback(
|
|
145
|
+
(gen: number) => {
|
|
146
|
+
const liveDoc = iframeRef.current?.contentDocument;
|
|
147
|
+
if (liveDoc && isStudioManualEditGestureLiveIn(liveDoc))
|
|
148
|
+
return waitThenRetry(gen, "gesture", (retry) =>
|
|
149
|
+
afterStudioManualEditGestures(liveDoc, retry),
|
|
150
|
+
);
|
|
151
|
+
if (gestureSavesOnScreen() === savesAtRequestRef.current) return false;
|
|
152
|
+
if (isStudioEditSaving()) return waitThenRetry(gen, "save", afterStudioPendingEdits);
|
|
153
|
+
logReload("shadow-reloaded-after-gesture-save", { gen });
|
|
154
|
+
const url = new URL(shadowUrlRef.current, window.location.origin);
|
|
155
|
+
url.searchParams.set("_t", String(Date.now()));
|
|
156
|
+
beginShadowReloadRef.current(url.toString());
|
|
157
|
+
return true;
|
|
158
|
+
},
|
|
159
|
+
[iframeRef, gestureSavesOnScreen, waitThenRetry],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const shadowReadyToPromote = useCallback((gen: number) => {
|
|
163
|
+
const shadow = shadowIframeRef.current;
|
|
164
|
+
const pending = pendingCommitRef.current;
|
|
165
|
+
const ready = pending?.gen === gen && visuallyReadyGenRef.current === gen;
|
|
166
|
+
return shadow && pending && ready && gen === shadowGenRef.current ? { shadow, pending } : null;
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
109
169
|
const promoteWhenReady = useCallback(
|
|
110
170
|
(gen: number) => {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
if (!shadow || !pending || !ready || gen !== shadowGenRef.current) return;
|
|
171
|
+
const promotable = shadowReadyToPromote(gen);
|
|
172
|
+
if (!promotable || heldOrStale(gen)) return;
|
|
173
|
+
const { shadow, pending } = promotable;
|
|
115
174
|
stopPendingShadow();
|
|
116
175
|
// The live frame kept playing, stopped at the end or was seeked while the shadow loaded.
|
|
117
176
|
const live = getAdapter();
|
|
@@ -119,6 +178,7 @@ export function useShadowPreviewReload({
|
|
|
119
178
|
const playing = usePlayerStore.getState().isPlaying;
|
|
120
179
|
live?.pause();
|
|
121
180
|
// The store takes the new document's timeline only now that it is the one on screen, and reads it there.
|
|
181
|
+
const retired = iframeRef.current;
|
|
122
182
|
iframeRef.current = shadow;
|
|
123
183
|
pending.commit();
|
|
124
184
|
shadowIframeRef.current = null;
|
|
@@ -126,17 +186,21 @@ export function useShadowPreviewReload({
|
|
|
126
186
|
applyPreviewAudioState();
|
|
127
187
|
setPreviewSlots((prev) => planShadowPromotion(prev, gen));
|
|
128
188
|
if (liveTime != null) handOverPlaybackRef.current(liveTime, playing);
|
|
189
|
+
announcePreviewPromoted({ retired, live: shadow });
|
|
129
190
|
onPromotedRef.current?.();
|
|
130
191
|
thumbnailScheduler.setPreviewReloading(false);
|
|
131
192
|
},
|
|
132
193
|
[
|
|
133
194
|
stopPendingShadow,
|
|
195
|
+
shadowReadyToPromote,
|
|
196
|
+
heldOrStale,
|
|
134
197
|
getAdapter,
|
|
135
198
|
iframeRef,
|
|
136
199
|
attachIframeShortcutListeners,
|
|
137
200
|
applyPreviewAudioState,
|
|
138
201
|
],
|
|
139
202
|
);
|
|
203
|
+
promoteWhenReadyRef.current = promoteWhenReady;
|
|
140
204
|
|
|
141
205
|
const getShadowAdapter = useCallback(() => getAdapter(shadowIframeRef.current), [getAdapter]);
|
|
142
206
|
const isCurrentShadow = useCallback((gen?: number) => gen === shadowGenRef.current, []);
|
|
@@ -216,13 +280,16 @@ export function useShadowPreviewReload({
|
|
|
216
280
|
stopPendingShadow();
|
|
217
281
|
budgetGenRef.current = gen;
|
|
218
282
|
budgetsSpentRef.current = 0;
|
|
283
|
+
shadowUrlRef.current = url;
|
|
284
|
+
savesAtRequestRef.current = gestureSavesOnScreen();
|
|
219
285
|
armReadyTimer(gen);
|
|
220
286
|
setPreviewSlots((prev) => planShadowReload(prev, gen, url));
|
|
221
287
|
// Thumbnails of the edit wait for the new preview instead of competing with it.
|
|
222
288
|
thumbnailScheduler.setPreviewReloading(true);
|
|
223
289
|
},
|
|
224
|
-
[stopPendingShadow, armReadyTimer],
|
|
290
|
+
[stopPendingShadow, armReadyTimer, gestureSavesOnScreen],
|
|
225
291
|
);
|
|
292
|
+
beginShadowReloadRef.current = beginShadowReload;
|
|
226
293
|
|
|
227
294
|
// Composition switch (not an edit reload): drop any in-flight shadow.
|
|
228
295
|
const resetPreviewSlots = useCallback(() => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { syncStoredAutomationFromPreview } from "./automationStoreSync";
|
|
3
|
+
import { syncStoredAutomationFromPreview, syncStoredElementAttribute } from "./automationStoreSync";
|
|
4
4
|
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
5
5
|
|
|
6
6
|
const TWO_POINTS = '{"version":1,"lanes":[{"target":"volume","points":[{"t":0,"v":1}]}]}';
|
|
@@ -32,7 +32,24 @@ beforeEach(() => {
|
|
|
32
32
|
usePlayerStore.getState().reset();
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
describe("syncStoredElementAttribute", () => {
|
|
36
|
+
it("records a saved fade on the stored clip, so the timeline draws what the file holds", () => {
|
|
37
|
+
usePlayerStore.setState({ elements: [el({ fadeIn: 1 })] });
|
|
38
|
+
syncStoredElementAttribute(el(), "data-fade-in", "2.5");
|
|
39
|
+
expect(usePlayerStore.getState().elements[0]?.fadeIn).toBe(2.5);
|
|
40
|
+
syncStoredElementAttribute(el(), "data-fade-in", null);
|
|
41
|
+
expect(usePlayerStore.getState().elements[0]?.fadeIn).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
35
45
|
describe("syncStoredAutomationFromPreview", () => {
|
|
46
|
+
it("reads back a fade an undo restored on the preview", () => {
|
|
47
|
+
usePlayerStore.setState({ elements: [el({ fadeOut: 2 })] });
|
|
48
|
+
syncStoredAutomationFromPreview(previewWith({ "data-fade-in": "0.6" }));
|
|
49
|
+
const stored = usePlayerStore.getState().elements[0];
|
|
50
|
+
expect([stored?.fadeIn, stored?.fadeOut]).toEqual([0.6, undefined]);
|
|
51
|
+
});
|
|
52
|
+
|
|
36
53
|
it("reads the element the timeline bound, not an earlier same-id copy in a sub-composition", () => {
|
|
37
54
|
usePlayerStore.setState({ elements: [el({ hfId: "hf-root" })] });
|
|
38
55
|
const doc = document.implementation.createHTMLDocument("preview");
|
|
@@ -16,12 +16,18 @@
|
|
|
16
16
|
|
|
17
17
|
import { HF_AUDIO_AUTOMATION_ATTR } from "@hyperframes/core/audio-automation";
|
|
18
18
|
import { HF_AUDIO_FX_ATTR } from "@hyperframes/core/audio-fx";
|
|
19
|
+
import {
|
|
20
|
+
HF_AUDIO_FADE_IN_ATTR,
|
|
21
|
+
HF_AUDIO_FADE_OUT_ATTR,
|
|
22
|
+
readElementFades,
|
|
23
|
+
readFadeSeconds,
|
|
24
|
+
} from "@hyperframes/core/audio-fade";
|
|
19
25
|
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
20
26
|
import { groupInfoFor } from "./timelineGroupInfo";
|
|
21
27
|
import { getTimelineElementIdentity, previewElementFinder } from "./timelineElementHelpers";
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
|
-
* Re-read every element's automation
|
|
30
|
+
* Re-read every element's automation, FX-chain and fade attributes from the preview
|
|
25
31
|
* document, for a change that reached the DOM without going through this store.
|
|
26
32
|
*
|
|
27
33
|
* That is undo and redo. A soft restore patches the reverted attributes onto the
|
|
@@ -34,21 +40,21 @@ import { getTimelineElementIdentity, previewElementFinder } from "./timelineElem
|
|
|
34
40
|
* reverted is only known by looking.
|
|
35
41
|
*/
|
|
36
42
|
/**
|
|
37
|
-
* What an element's
|
|
43
|
+
* What an element's six synced fields SHOULD read, given the preview.
|
|
38
44
|
*
|
|
39
|
-
* Its own
|
|
45
|
+
* Its own four come off its node; the other two are its copy of what its group
|
|
40
46
|
* carries. The timeline derives a group's lanes and chain from these mirrors,
|
|
41
|
-
* never from the group element
|
|
42
|
-
*
|
|
43
|
-
* group node through the ordinary element path, which used to refresh an
|
|
44
|
-
* element's own two fields and nothing else. So the group's row went on reading
|
|
45
|
-
* the value it was born with, and its `∿` never appeared.
|
|
47
|
+
* never from the group element, and automating a group writes `data-automation`
|
|
48
|
+
* on the group node through the ordinary element path, so both halves are re-read.
|
|
46
49
|
*/
|
|
47
50
|
function syncedFields(doc: Document, element: TimelineElement, node: Element) {
|
|
48
51
|
const group = element.audioGroup ? groupInfoFor(doc, element.audioGroup) : null;
|
|
52
|
+
const fades = readElementFades(node);
|
|
49
53
|
return {
|
|
50
54
|
automation: node.getAttribute(HF_AUDIO_AUTOMATION_ATTR) ?? undefined,
|
|
51
55
|
fxChain: node.getAttribute(HF_AUDIO_FX_ATTR) ?? undefined,
|
|
56
|
+
fadeIn: storedFade(fades.fadeIn),
|
|
57
|
+
fadeOut: storedFade(fades.fadeOut),
|
|
52
58
|
audioGroupAutomation: group?.automation,
|
|
53
59
|
audioGroupFxChain: group?.fxChain,
|
|
54
60
|
};
|
|
@@ -74,27 +80,33 @@ export function syncStoredAutomationFromPreview(doc: Document | null | undefined
|
|
|
74
80
|
});
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
/** Discovery's rule (applyFadeMetadataFromElement): a fade of 0 is no fade. */
|
|
84
|
+
const storedFade = (seconds: number) => (seconds > 0 ? seconds : undefined);
|
|
85
|
+
|
|
86
|
+
const STORED_FIELD: Record<string, (value: string | null) => Partial<TimelineElement>> = {
|
|
87
|
+
[HF_AUDIO_AUTOMATION_ATTR]: (value) => ({ automation: value ?? undefined }),
|
|
88
|
+
[HF_AUDIO_FX_ATTR]: (value) => ({ fxChain: value ?? undefined }),
|
|
89
|
+
[HF_AUDIO_FADE_IN_ATTR]: (value) => ({ fadeIn: storedFade(readFadeSeconds(value)) }),
|
|
90
|
+
[HF_AUDIO_FADE_OUT_ATTR]: (value) => ({ fadeOut: storedFade(readFadeSeconds(value)) }),
|
|
80
91
|
};
|
|
81
92
|
|
|
82
|
-
/** Record a saved automation
|
|
93
|
+
/** Record a saved automation, FX-chain or fade value on one element's stored copy. */
|
|
83
94
|
export function syncStoredElementAttribute(
|
|
84
95
|
target: TimelineElement,
|
|
85
96
|
attr: string,
|
|
86
97
|
value: string | null,
|
|
87
98
|
): void {
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
99
|
+
const fields = STORED_FIELD[attr]?.(value);
|
|
100
|
+
if (!fields) return;
|
|
90
101
|
const key = getTimelineElementIdentity(target);
|
|
91
|
-
const
|
|
102
|
+
const same = (element: TimelineElement) =>
|
|
103
|
+
(Object.keys(fields) as (keyof TimelineElement)[]).every((k) => element[k] === fields[k]);
|
|
92
104
|
usePlayerStore.setState((state) => {
|
|
93
105
|
let changed = false;
|
|
94
106
|
const elements = state.elements.map((element) => {
|
|
95
|
-
if (getTimelineElementIdentity(element) !== key || element
|
|
107
|
+
if (getTimelineElementIdentity(element) !== key || same(element)) return element;
|
|
96
108
|
changed = true;
|
|
97
|
-
return { ...element,
|
|
109
|
+
return { ...element, ...fields };
|
|
98
110
|
});
|
|
99
111
|
return changed ? { elements } : {};
|
|
100
112
|
});
|
|
@@ -23,6 +23,8 @@ export interface ThumbnailRequest {
|
|
|
23
23
|
priority: ThumbnailPriority;
|
|
24
24
|
/** Rich work is paused while the timeline is fast-scrolling. */
|
|
25
25
|
rich?: boolean;
|
|
26
|
+
/** For work whose result nobody reads: it is dropped, not cached, once its last lease ends. */
|
|
27
|
+
discardWhenReleased?: boolean;
|
|
26
28
|
load: (signal: AbortSignal) => Promise<ThumbnailLoadedResult>;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -214,10 +216,7 @@ export class ThumbnailScheduler {
|
|
|
214
216
|
if (!scrolling) this.pump();
|
|
215
217
|
}
|
|
216
218
|
|
|
217
|
-
/**
|
|
218
|
-
* Hold server-rendered composition thumbnails while the preview loads a new document: both
|
|
219
|
-
* are served by the same Studio server, and the preview is what the person is waiting for.
|
|
220
|
-
*/
|
|
219
|
+
/** Holds composition renders while the preview loads: same server, and the preview is what the person awaits. */
|
|
221
220
|
setPreviewReloading(reloading: boolean): void {
|
|
222
221
|
if (this.previewReloading === reloading) return;
|
|
223
222
|
this.previewReloading = reloading;
|
|
@@ -362,7 +361,7 @@ export class ThumbnailScheduler {
|
|
|
362
361
|
const byteBudget = isWaveform
|
|
363
362
|
? this.budgets.waveformCacheBytes
|
|
364
363
|
: this.budgets.thumbnailCacheBytes;
|
|
365
|
-
entry.cached = entry.weight <= byteBudget;
|
|
364
|
+
entry.cached = !entry.request.discardWhenReleased && entry.weight <= byteBudget;
|
|
366
365
|
if (!entry.cached) return;
|
|
367
366
|
if (isWaveform) this.waveformCacheBytes += entry.weight;
|
|
368
367
|
else this.cacheBytes += entry.weight;
|
|
@@ -23,7 +23,7 @@ describe("timeline viewport budgets", () => {
|
|
|
23
23
|
constrainedLongTaskLimitMs: 300,
|
|
24
24
|
posterCoverageRatio: 0.9,
|
|
25
25
|
supportedFixtureFallbackRatio: 0.02,
|
|
26
|
-
scrollSamplesPerRun:
|
|
26
|
+
scrollSamplesPerRun: 63,
|
|
27
27
|
warmupRuns: 3,
|
|
28
28
|
measuredRuns: 5,
|
|
29
29
|
requiredPassingRuns: 4,
|
|
@@ -99,7 +99,7 @@ export const TIMELINE_VIEWPORT_BUDGETS: Readonly<TimelineViewportBudgets> = Obje
|
|
|
99
99
|
richPreviewP95Ms: 750,
|
|
100
100
|
constrainedRichPreviewP95Ms: 1_200,
|
|
101
101
|
supportedFixtureFallbackRatio: 0.02,
|
|
102
|
-
scrollSamplesPerRun:
|
|
102
|
+
scrollSamplesPerRun: 63,
|
|
103
103
|
warmupRuns: 3,
|
|
104
104
|
measuredRuns: 5,
|
|
105
105
|
requiredPassingRuns: 4,
|
package/src/player/sceneSwap.ts
CHANGED
|
@@ -17,6 +17,24 @@ export function onPreviewContentReplaced(
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const PREVIEW_PROMOTED = "hf-preview-promoted";
|
|
21
|
+
export type PreviewPromotion = { retired: HTMLIFrameElement | null; live: HTMLIFrameElement };
|
|
22
|
+
|
|
23
|
+
export function announcePreviewPromoted(promotion: PreviewPromotion): void {
|
|
24
|
+
promotion.live.ownerDocument.dispatchEvent(
|
|
25
|
+
new CustomEvent(PREVIEW_PROMOTED, { detail: promotion }),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function onPreviewPromoted(
|
|
30
|
+
doc: Document,
|
|
31
|
+
listener: (promotion: PreviewPromotion) => void,
|
|
32
|
+
): () => void {
|
|
33
|
+
const handle = (event: Event) => listener((event as CustomEvent<PreviewPromotion>).detail);
|
|
34
|
+
doc.addEventListener(PREVIEW_PROMOTED, handle);
|
|
35
|
+
return () => doc.removeEventListener(PREVIEW_PROMOTED, handle);
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
export function markScenesStale(iframe: HTMLIFrameElement | null, files: readonly string[]): void {
|
|
21
39
|
const doc = iframe?.contentDocument;
|
|
22
40
|
const meta = doc?.querySelector<HTMLMetaElement>('meta[name="hf-scene-parts"]');
|