@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
|
@@ -1,57 +1,151 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
type KeyboardEvent,
|
|
10
|
+
type PointerEvent,
|
|
11
|
+
} from "react";
|
|
2
12
|
import {
|
|
3
13
|
HF_AUDIO_FADE_IN_ATTR,
|
|
4
14
|
HF_AUDIO_FADE_OUT_ATTR,
|
|
5
15
|
clampFadesToDuration,
|
|
6
16
|
formatFadeSeconds,
|
|
17
|
+
type AudioFades,
|
|
7
18
|
} from "@hyperframes/core/audio-fade";
|
|
8
|
-
import type
|
|
19
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
9
20
|
import { Tooltip } from "../../components/ui";
|
|
10
21
|
import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
|
|
11
22
|
import { releasedOutsideWindow } from "./timelinePointerRelease";
|
|
23
|
+
import {
|
|
24
|
+
collectTimelineSnapTargets,
|
|
25
|
+
snapTimelineTime,
|
|
26
|
+
TIMELINE_SNAP_PX,
|
|
27
|
+
type TimelineSnapTarget,
|
|
28
|
+
type TimelineSnapType,
|
|
29
|
+
} from "./timelineSnapping";
|
|
12
30
|
|
|
13
31
|
type FadeEdge = "in" | "out";
|
|
32
|
+
type FadeDraft = { edge: FadeEdge; seconds: number } | null;
|
|
14
33
|
|
|
15
|
-
const
|
|
34
|
+
const TAB_WIDTH = 4;
|
|
35
|
+
const TAB_HEIGHT = 15;
|
|
16
36
|
const HANDLE_HIT = 24;
|
|
37
|
+
const TAB_INSET = 7;
|
|
38
|
+
const TAB_CENTER_IN_HIT = 8;
|
|
17
39
|
const HANDLE_Z_ABOVE_CLIP_CONTENT = 30;
|
|
18
40
|
const SUPPRESS_CLIP_NATIVE_TITLE = "";
|
|
19
41
|
/** Pixels of pointer travel before a press on the handle counts as a drag. */
|
|
20
42
|
const DRAG_THRESHOLD_PX = 2;
|
|
43
|
+
const DEFAULT_FADE_SECONDS = 0.5;
|
|
44
|
+
const SNAP_LABEL: Record<TimelineSnapType, string> = {
|
|
45
|
+
playhead: "playhead",
|
|
46
|
+
"clip-edge": "clip edge",
|
|
47
|
+
beat: "beat",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ClipFadeShape = AudioFades & { duration: number };
|
|
51
|
+
export const ClipFadesContext = createContext<ClipFadeShape | null>(null);
|
|
52
|
+
|
|
53
|
+
// Owned by TimelineClip so the handles and the waveform share the value under the pointer;
|
|
54
|
+
// the draft also bridges release until the save lands in the store.
|
|
55
|
+
export function useClipFadeDraft(el: TimelineElement) {
|
|
56
|
+
const [draft, setDraft] = useState<FadeDraft>(null);
|
|
57
|
+
const authoredIn = el.fadeIn ?? 0;
|
|
58
|
+
const authoredOut = el.fadeOut ?? 0;
|
|
59
|
+
const { fadeIn, fadeOut } = clampFadesToDuration(
|
|
60
|
+
{
|
|
61
|
+
fadeIn: draft?.edge === "in" ? draft.seconds : authoredIn,
|
|
62
|
+
fadeOut: draft?.edge === "out" ? draft.seconds : authoredOut,
|
|
63
|
+
},
|
|
64
|
+
el.duration,
|
|
65
|
+
);
|
|
66
|
+
const shape = useMemo(
|
|
67
|
+
() => ({ fadeIn, fadeOut, duration: el.duration }),
|
|
68
|
+
[fadeIn, fadeOut, el.duration],
|
|
69
|
+
);
|
|
70
|
+
return { draft, setDraft, shape };
|
|
71
|
+
}
|
|
21
72
|
|
|
22
73
|
interface TimelineClipFadesProps {
|
|
23
74
|
el: TimelineElement;
|
|
24
75
|
pps: number;
|
|
25
76
|
widthPx: number;
|
|
26
|
-
/** Handles show on hover/selection; the
|
|
77
|
+
/** Handles show on hover/selection; the ramps show whenever a fade is set. */
|
|
27
78
|
showHandles: boolean;
|
|
79
|
+
focusable?: boolean;
|
|
80
|
+
/** Audio clips draw the fade in their waveform; others get the shaded wedge. */
|
|
81
|
+
hasWaveform?: boolean;
|
|
82
|
+
fade: ReturnType<typeof useClipFadeDraft>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type KeyedFade = (current: number, step: number, limit: number) => number | null;
|
|
86
|
+
const grow: KeyedFade = (current, step) => current + step;
|
|
87
|
+
const shrink: KeyedFade = (current, step) => current - step;
|
|
88
|
+
const clear: KeyedFade = () => 0;
|
|
89
|
+
const FADE_KEYS: Record<string, KeyedFade> = {
|
|
90
|
+
ArrowRight: grow,
|
|
91
|
+
ArrowUp: grow,
|
|
92
|
+
ArrowLeft: shrink,
|
|
93
|
+
ArrowDown: shrink,
|
|
94
|
+
Home: clear,
|
|
95
|
+
Delete: clear,
|
|
96
|
+
Backspace: clear,
|
|
97
|
+
End: (_current, _step, limit) => limit,
|
|
98
|
+
Enter: (current) => (current === 0 ? DEFAULT_FADE_SECONDS : null),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function keyedFadeSeconds(key: string, shift: boolean, current: number, limit: number) {
|
|
102
|
+
return FADE_KEYS[key]?.(current, shift ? 1 : 0.1, limit) ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function topEdgeY(x: number, widthPx: number, heightPx: number, radiusPx: number): number {
|
|
106
|
+
const r = Math.min(radiusPx, widthPx / 2, heightPx / 2);
|
|
107
|
+
const d = x < r ? r - x : x > widthPx - r ? x - (widthPx - r) : 0;
|
|
108
|
+
return d > 0 ? r - Math.sqrt(Math.max(0, r * r - d * d)) : 0;
|
|
28
109
|
}
|
|
29
110
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
111
|
+
/** Slim tabs at each fade's end that drag `data-fade-in` / `data-fade-out`. */
|
|
112
|
+
// fallow-ignore-next-line complexity
|
|
113
|
+
export function TimelineClipFades({
|
|
114
|
+
el,
|
|
115
|
+
pps,
|
|
116
|
+
widthPx,
|
|
117
|
+
showHandles,
|
|
118
|
+
focusable = false,
|
|
119
|
+
hasWaveform = false,
|
|
120
|
+
fade,
|
|
121
|
+
}: TimelineClipFadesProps) {
|
|
32
122
|
const { onSetElementAttributeLive, onSetElementAttributeQuiet, onRevertElementAttributeLive } =
|
|
33
123
|
useTimelineEditContextOptional();
|
|
34
124
|
const canEdit = Boolean(onSetElementAttributeLive && onSetElementAttributeQuiet);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// the gap between release and the store re-reading the file, so the ramp
|
|
38
|
-
// does not snap back for a frame.
|
|
39
|
-
const [draft, setDraft] = useState<{ edge: FadeEdge; seconds: number } | null>(null);
|
|
125
|
+
const { setDraft } = fade;
|
|
126
|
+
const fades = fade.shape;
|
|
40
127
|
const [dragging, setDragging] = useState<FadeEdge | null>(null);
|
|
128
|
+
const [focused, setFocused] = useState<FadeEdge | null>(null);
|
|
129
|
+
const [snapType, setSnapType] = useState<TimelineSnapType | null>(null);
|
|
41
130
|
const authoredIn = el.fadeIn ?? 0;
|
|
42
131
|
const authoredOut = el.fadeOut ?? 0;
|
|
43
|
-
useEffect(() => setDraft(null), [authoredIn, authoredOut]);
|
|
44
|
-
|
|
45
|
-
const fades = clampFadesToDuration(
|
|
46
|
-
{
|
|
47
|
-
fadeIn: draft?.edge === "in" ? draft.seconds : authoredIn,
|
|
48
|
-
fadeOut: draft?.edge === "out" ? draft.seconds : authoredOut,
|
|
49
|
-
},
|
|
50
|
-
el.duration,
|
|
51
|
-
);
|
|
52
132
|
const inPx = Math.min(widthPx, fades.fadeIn * pps);
|
|
53
133
|
const outPx = Math.min(widthPx, fades.fadeOut * pps);
|
|
54
134
|
|
|
135
|
+
const visible =
|
|
136
|
+
fades.fadeIn > 0 || fades.fadeOut > 0 || showHandles || dragging !== null || focused !== null;
|
|
137
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
138
|
+
const [clipBox, setClipBox] = useState({ height: 0, radius: 0 });
|
|
139
|
+
useLayoutEffect(() => {
|
|
140
|
+
const clip = rootRef.current?.parentElement;
|
|
141
|
+
if (!clip) return;
|
|
142
|
+
const radius = parseFloat(getComputedStyle(clip).borderTopLeftRadius) || 0;
|
|
143
|
+
const height = clip.clientHeight;
|
|
144
|
+
setClipBox((box) =>
|
|
145
|
+
box.height === height && box.radius === radius ? box : { height, radius },
|
|
146
|
+
);
|
|
147
|
+
}, [widthPx, visible]);
|
|
148
|
+
|
|
55
149
|
const gesture = useRef<{
|
|
56
150
|
edge: FadeEdge;
|
|
57
151
|
pointerId: number;
|
|
@@ -60,11 +154,39 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
|
|
|
60
154
|
otherSeconds: number;
|
|
61
155
|
moved: boolean;
|
|
62
156
|
last: number;
|
|
157
|
+
snapTargets: TimelineSnapTarget[];
|
|
63
158
|
} | null>(null);
|
|
64
159
|
|
|
65
160
|
const attrFor = (edge: FadeEdge) =>
|
|
66
161
|
edge === "in" ? HF_AUDIO_FADE_IN_ATTR : HF_AUDIO_FADE_OUT_ATTR;
|
|
67
162
|
const attrText = (seconds: number) => (seconds > 0 ? formatFadeSeconds(seconds) : null);
|
|
163
|
+
const labelFor = (edge: FadeEdge) => (edge === "in" ? "Fade in" : "Fade out");
|
|
164
|
+
const currentSeconds = (edge: FadeEdge) =>
|
|
165
|
+
fade.draft?.edge === edge ? fade.draft.seconds : edge === "in" ? authoredIn : authoredOut;
|
|
166
|
+
const limitFor = (edge: FadeEdge) =>
|
|
167
|
+
Math.max(0, el.duration - currentSeconds(edge === "in" ? "out" : "in"));
|
|
168
|
+
|
|
169
|
+
// When the other fade makes the pair overrun the clip, neither a key nor a drag lowers this one.
|
|
170
|
+
const stepLimit = (current: number, other: number) =>
|
|
171
|
+
Math.max(0, el.duration - other, other > 0 ? current : 0);
|
|
172
|
+
const startSeconds = (edge: FadeEdge) => {
|
|
173
|
+
const current = currentSeconds(edge);
|
|
174
|
+
return Math.min(current, stepLimit(current, currentSeconds(edge === "in" ? "out" : "in")));
|
|
175
|
+
};
|
|
176
|
+
const dragLimit = (g: { otherSeconds: number; originSeconds: number }) =>
|
|
177
|
+
stepLimit(g.originSeconds, g.otherSeconds);
|
|
178
|
+
|
|
179
|
+
/** Moves the fade's end onto a playhead or clip edge within the timeline's snap radius. */
|
|
180
|
+
const snapSeconds = (g: NonNullable<typeof gesture.current>, seconds: number) => {
|
|
181
|
+
const knee = g.edge === "in" ? el.start + seconds : el.start + el.duration - seconds;
|
|
182
|
+
const snapped = snapTimelineTime(knee, g.snapTargets, TIMELINE_SNAP_PX / Math.max(pps, 1e-6));
|
|
183
|
+
if (!snapped.target) return { seconds, type: null };
|
|
184
|
+
const next = g.edge === "in" ? snapped.time - el.start : el.start + el.duration - snapped.time;
|
|
185
|
+
const limit = dragLimit(g);
|
|
186
|
+
return next >= 0 && next <= limit
|
|
187
|
+
? { seconds: next, type: snapped.target.type }
|
|
188
|
+
: { seconds, type: null };
|
|
189
|
+
};
|
|
68
190
|
|
|
69
191
|
const secondsAt = (clientX: number): number => {
|
|
70
192
|
const g = gesture.current;
|
|
@@ -72,7 +194,7 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
|
|
|
72
194
|
const deltaSeconds = (clientX - g.originClientX) / Math.max(pps, 1e-6);
|
|
73
195
|
// Fade-in grows to the right, fade-out grows to the left.
|
|
74
196
|
const raw = g.edge === "in" ? g.originSeconds + deltaSeconds : g.originSeconds - deltaSeconds;
|
|
75
|
-
const limit =
|
|
197
|
+
const limit = dragLimit(g);
|
|
76
198
|
const clamped = Math.min(limit, Math.max(0, raw));
|
|
77
199
|
return Math.round(clamped * 100) / 100;
|
|
78
200
|
};
|
|
@@ -83,14 +205,24 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
|
|
|
83
205
|
e.stopPropagation();
|
|
84
206
|
e.preventDefault();
|
|
85
207
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
208
|
+
flushKeys();
|
|
209
|
+
const store = usePlayerStore.getState();
|
|
86
210
|
gesture.current = {
|
|
87
211
|
edge,
|
|
88
212
|
pointerId: e.pointerId,
|
|
89
213
|
originClientX: e.clientX,
|
|
90
|
-
originSeconds: edge
|
|
91
|
-
otherSeconds: edge === "in" ?
|
|
214
|
+
originSeconds: startSeconds(edge),
|
|
215
|
+
otherSeconds: currentSeconds(edge === "in" ? "out" : "in"),
|
|
92
216
|
moved: false,
|
|
93
|
-
last: edge
|
|
217
|
+
last: startSeconds(edge),
|
|
218
|
+
snapTargets: store.timelineSnapEnabled
|
|
219
|
+
? collectTimelineSnapTargets({
|
|
220
|
+
elements: store.elements,
|
|
221
|
+
playheadTime: store.currentTime,
|
|
222
|
+
beatTimes: [],
|
|
223
|
+
excludeElementKey: el.key ?? el.id,
|
|
224
|
+
}).filter(({ time }) => time > el.start + 1e-3 && time < el.start + el.duration - 1e-3)
|
|
225
|
+
: [],
|
|
94
226
|
};
|
|
95
227
|
setDragging(edge);
|
|
96
228
|
};
|
|
@@ -100,7 +232,9 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
|
|
|
100
232
|
if (!g || g.pointerId !== e.pointerId) return;
|
|
101
233
|
if (!g.moved && Math.abs(e.clientX - g.originClientX) < DRAG_THRESHOLD_PX) return;
|
|
102
234
|
g.moved = true;
|
|
103
|
-
const seconds = secondsAt(e.clientX);
|
|
235
|
+
const { seconds: raw, type } = snapSeconds(g, secondsAt(e.clientX));
|
|
236
|
+
const seconds = Math.round(raw * 100) / 100;
|
|
237
|
+
setSnapType(type);
|
|
104
238
|
if (seconds === g.last) return;
|
|
105
239
|
g.last = seconds;
|
|
106
240
|
setDraft({ edge: g.edge, seconds });
|
|
@@ -115,6 +249,7 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
|
|
|
115
249
|
if (!g || g.pointerId !== e.pointerId) return null;
|
|
116
250
|
gesture.current = null;
|
|
117
251
|
setDragging(null);
|
|
252
|
+
setSnapType(null);
|
|
118
253
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
119
254
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
120
255
|
}
|
|
@@ -123,35 +258,69 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
|
|
|
123
258
|
|
|
124
259
|
/** Puts the live document back where the file has it and drops the draft. */
|
|
125
260
|
const revertGesture = (g: Gesture) => {
|
|
126
|
-
if (g.moved)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
261
|
+
if (!g.moved) return;
|
|
262
|
+
onSetElementAttributeLive?.(el, attrFor(g.edge), attrText(g.originSeconds));
|
|
263
|
+
onRevertElementAttributeLive?.(el, attrFor(g.edge));
|
|
130
264
|
setDraft(null);
|
|
131
265
|
};
|
|
132
266
|
|
|
267
|
+
// The save syncs the store before it settles, so its own draft can go then; a newer draft stays.
|
|
268
|
+
const commit = (edge: FadeEdge, seconds: number) => {
|
|
269
|
+
const draft = { edge, seconds };
|
|
270
|
+
setDraft(draft);
|
|
271
|
+
const settle = () => setDraft((current) => (current === draft ? null : current));
|
|
272
|
+
void onSetElementAttributeQuiet?.(el, attrFor(edge), attrText(seconds), labelFor(edge)).then(
|
|
273
|
+
settle,
|
|
274
|
+
settle,
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
|
|
133
278
|
const finish = (e: PointerEvent<HTMLDivElement>, cancelled: boolean) => {
|
|
134
279
|
const g = endGesture(e);
|
|
135
280
|
if (!g) return;
|
|
136
|
-
if (cancelled || !g.moved || releasedOutsideWindow(e))
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
281
|
+
if (cancelled || !g.moved || g.last === g.originSeconds || releasedOutsideWindow(e)) {
|
|
282
|
+
return revertGesture(g);
|
|
283
|
+
}
|
|
284
|
+
commit(g.edge, g.last);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const onHandleDoubleClick = (edge: FadeEdge) => {
|
|
288
|
+
commit(edge, currentSeconds(edge) > 0 ? 0 : Math.min(DEFAULT_FADE_SECONDS, limitFor(edge)));
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const keyBurst = useRef<{ edge: FadeEdge; seconds: number } | null>(null);
|
|
292
|
+
const flushKeys = () => {
|
|
293
|
+
const burst = keyBurst.current;
|
|
294
|
+
keyBurst.current = null;
|
|
295
|
+
if (burst) commit(burst.edge, burst.seconds);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// A held key previews live and saves once on release: one undo step per burst.
|
|
299
|
+
const onHandleKeyDown = (edge: FadeEdge) => (e: KeyboardEvent<HTMLDivElement>) => {
|
|
300
|
+
if (gesture.current || !canEdit) return;
|
|
301
|
+
const current = startSeconds(edge);
|
|
302
|
+
const limit = stepLimit(current, currentSeconds(edge === "in" ? "out" : "in"));
|
|
303
|
+
const next = keyedFadeSeconds(e.key, e.shiftKey, current, limit);
|
|
304
|
+
if (next === null) return;
|
|
305
|
+
e.preventDefault();
|
|
306
|
+
e.stopPropagation();
|
|
307
|
+
const seconds = Math.round(Math.min(limit, Math.max(0, next)) * 100) / 100;
|
|
308
|
+
if (seconds === current) return;
|
|
309
|
+
keyBurst.current = { edge, seconds };
|
|
310
|
+
setDraft({ edge, seconds });
|
|
311
|
+
onSetElementAttributeLive?.(el, attrFor(edge), attrText(seconds));
|
|
144
312
|
};
|
|
145
313
|
|
|
146
314
|
useEffect(() => {
|
|
147
315
|
if (dragging === null) return;
|
|
148
|
-
const cancelOnWindowEscape = (e: KeyboardEvent) => {
|
|
316
|
+
const cancelOnWindowEscape = (e: globalThis.KeyboardEvent) => {
|
|
149
317
|
const g = gesture.current;
|
|
150
318
|
if (e.key !== "Escape" || !g) return;
|
|
151
319
|
e.preventDefault();
|
|
152
320
|
e.stopPropagation();
|
|
153
321
|
gesture.current = null;
|
|
154
322
|
setDragging(null);
|
|
323
|
+
setSnapType(null);
|
|
155
324
|
revertGesture(g);
|
|
156
325
|
};
|
|
157
326
|
window.addEventListener("keydown", cancelOnWindowEscape, { capture: true });
|
|
@@ -160,21 +329,43 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
|
|
|
160
329
|
|
|
161
330
|
const showIn = fades.fadeIn > 0;
|
|
162
331
|
const showOut = fades.fadeOut > 0;
|
|
163
|
-
const handlesVisible = showHandles || dragging !== null;
|
|
164
|
-
if (!
|
|
332
|
+
const handlesVisible = showHandles || dragging !== null || focused !== null;
|
|
333
|
+
if (!visible) return null;
|
|
165
334
|
|
|
166
335
|
const hitWidth = Math.min(HANDLE_HIT, widthPx / 2);
|
|
167
|
-
const
|
|
336
|
+
const tabX = (edge: FadeEdge) => {
|
|
337
|
+
const knee = edge === "in" ? inPx : widthPx - outPx;
|
|
338
|
+
const inset = Math.min(TAB_INSET, widthPx / 2);
|
|
339
|
+
return Math.min(widthPx - inset, Math.max(inset, knee));
|
|
340
|
+
};
|
|
341
|
+
const boxLeft = (x: number) => Math.min(widthPx - hitWidth, Math.max(0, x - hitWidth / 2));
|
|
342
|
+
const [inX, outX] = [tabX("in"), tabX("out")];
|
|
343
|
+
const mid = (inX + outX) / 2;
|
|
344
|
+
// A handle with no 0.01 s step to move is not drawn, unless in use: its twin owns the spot.
|
|
345
|
+
const drawn = (edge: FadeEdge) =>
|
|
346
|
+
currentSeconds(edge) > 0 ||
|
|
347
|
+
Math.round(limitFor(edge) * 100) >= 1 ||
|
|
348
|
+
dragging === edge ||
|
|
349
|
+
focused === edge;
|
|
350
|
+
const overlap = drawn("in") && drawn("out") && boxLeft(inX) + hitWidth > boxLeft(outX);
|
|
351
|
+
const handleGeometry = (edge: FadeEdge) => {
|
|
352
|
+
const x = edge === "in" ? inX : outX;
|
|
353
|
+
// Boxes that would overlap meet at the midpoint between the two tabs.
|
|
354
|
+
const [left, right] = !overlap
|
|
355
|
+
? [boxLeft(x), boxLeft(x) + hitWidth]
|
|
356
|
+
: edge === "in"
|
|
357
|
+
? [Math.max(0, mid - hitWidth), mid]
|
|
358
|
+
: [mid, Math.min(widthPx, mid + hitWidth)];
|
|
359
|
+
const edgeY = topEdgeY(x, widthPx, clipBox.height, clipBox.radius);
|
|
360
|
+
return { left, width: right - left, tabLeft: x - left, top: edgeY + 1 - TAB_CENTER_IN_HIT };
|
|
361
|
+
};
|
|
362
|
+
const handleStyle = (geometry: { left: number; top: number; width: number }): CSSProperties => ({
|
|
168
363
|
position: "absolute",
|
|
169
|
-
top:
|
|
170
|
-
left:
|
|
171
|
-
width:
|
|
364
|
+
top: geometry.top,
|
|
365
|
+
left: geometry.left,
|
|
366
|
+
width: geometry.width,
|
|
172
367
|
height: HANDLE_HIT,
|
|
173
|
-
display: "flex",
|
|
174
|
-
alignItems: "center",
|
|
175
|
-
justifyContent: "center",
|
|
176
368
|
cursor: "ew-resize",
|
|
177
|
-
zIndex: HANDLE_Z_ABOVE_CLIP_CONTENT,
|
|
178
369
|
opacity: handlesVisible ? 1 : 0,
|
|
179
370
|
pointerEvents: handlesVisible && canEdit ? "auto" : "none",
|
|
180
371
|
touchAction: "none",
|
|
@@ -182,7 +373,16 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
|
|
|
182
373
|
});
|
|
183
374
|
|
|
184
375
|
return (
|
|
185
|
-
|
|
376
|
+
<div
|
|
377
|
+
ref={rootRef}
|
|
378
|
+
style={{
|
|
379
|
+
position: "absolute",
|
|
380
|
+
inset: 0,
|
|
381
|
+
borderRadius: "inherit",
|
|
382
|
+
pointerEvents: "none",
|
|
383
|
+
zIndex: HANDLE_Z_ABOVE_CLIP_CONTENT,
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
186
386
|
{(showIn || showOut) && (
|
|
187
387
|
<svg
|
|
188
388
|
aria-hidden="true"
|
|
@@ -193,119 +393,153 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
|
|
|
193
393
|
width: "100%",
|
|
194
394
|
height: "100%",
|
|
195
395
|
pointerEvents: "none",
|
|
196
|
-
zIndex: 5,
|
|
197
396
|
overflow: "hidden",
|
|
198
397
|
borderRadius: "inherit",
|
|
199
398
|
}}
|
|
200
399
|
viewBox={`0 0 ${Math.max(widthPx, 1)} 100`}
|
|
201
400
|
preserveAspectRatio="none"
|
|
202
401
|
>
|
|
203
|
-
{/* Keep the #4250 wedge geometry, but let the real waveform read through it. */}
|
|
204
402
|
{showIn && (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
fillOpacity={0.35}
|
|
211
|
-
/>
|
|
212
|
-
<line
|
|
213
|
-
x1={0}
|
|
214
|
-
y1={100}
|
|
215
|
-
x2={inPx}
|
|
216
|
-
y2={0}
|
|
217
|
-
stroke="var(--clip-handle)"
|
|
218
|
-
strokeOpacity={0.9}
|
|
219
|
-
strokeWidth={1.25}
|
|
220
|
-
vectorEffect="non-scaling-stroke"
|
|
221
|
-
/>
|
|
222
|
-
</>
|
|
403
|
+
<FadeRamp
|
|
404
|
+
testId="clip-fade-in"
|
|
405
|
+
wedge={hasWaveform ? null : `0,0 ${inPx},0 0,100`}
|
|
406
|
+
line={[0, 100, inPx, 0]}
|
|
407
|
+
/>
|
|
223
408
|
)}
|
|
224
409
|
{showOut && (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
fillOpacity={0.35}
|
|
231
|
-
/>
|
|
232
|
-
<line
|
|
233
|
-
x1={widthPx - outPx}
|
|
234
|
-
y1={0}
|
|
235
|
-
x2={widthPx}
|
|
236
|
-
y2={100}
|
|
237
|
-
stroke="var(--clip-handle)"
|
|
238
|
-
strokeOpacity={0.9}
|
|
239
|
-
strokeWidth={1.25}
|
|
240
|
-
vectorEffect="non-scaling-stroke"
|
|
241
|
-
/>
|
|
242
|
-
</>
|
|
410
|
+
<FadeRamp
|
|
411
|
+
testId="clip-fade-out"
|
|
412
|
+
wedge={hasWaveform ? null : `${widthPx - outPx},0 ${widthPx},0 ${widthPx},100`}
|
|
413
|
+
line={[widthPx - outPx, 0, widthPx, 100]}
|
|
414
|
+
/>
|
|
243
415
|
)}
|
|
244
416
|
</svg>
|
|
245
417
|
)}
|
|
246
|
-
{canEdit &&
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
418
|
+
{canEdit &&
|
|
419
|
+
(["in", "out"] as const).filter(drawn).map((edge) => {
|
|
420
|
+
const geometry = handleGeometry(edge);
|
|
421
|
+
return (
|
|
422
|
+
<FadeHandle
|
|
423
|
+
key={edge}
|
|
424
|
+
direction={edge}
|
|
425
|
+
clipName={el.label || el.id}
|
|
426
|
+
value={edge === "in" ? fades.fadeIn : fades.fadeOut}
|
|
427
|
+
max={Math.max(0, el.duration - (edge === "in" ? fades.fadeOut : fades.fadeIn))}
|
|
428
|
+
snapLabel={dragging === edge && snapType ? SNAP_LABEL[snapType] : null}
|
|
429
|
+
style={handleStyle(geometry)}
|
|
430
|
+
tabLeft={geometry.tabLeft}
|
|
431
|
+
tabTop={TAB_CENTER_IN_HIT - TAB_HEIGHT / 2}
|
|
432
|
+
focusable={focusable}
|
|
433
|
+
dragging={dragging === edge}
|
|
434
|
+
onPointerDown={onHandlePointerDown(edge)}
|
|
435
|
+
onPointerMove={onHandlePointerMove}
|
|
436
|
+
onPointerUp={(e) => finish(e, false)}
|
|
437
|
+
onPointerCancel={(e) => finish(e, true)}
|
|
438
|
+
onDoubleClick={() => onHandleDoubleClick(edge)}
|
|
439
|
+
onKeyDown={onHandleKeyDown(edge)}
|
|
440
|
+
onKeyUp={flushKeys}
|
|
441
|
+
onFocusChange={(on) => {
|
|
442
|
+
if (!on) flushKeys();
|
|
443
|
+
setFocused(on ? edge : null);
|
|
444
|
+
}}
|
|
445
|
+
/>
|
|
446
|
+
);
|
|
447
|
+
})}
|
|
448
|
+
</div>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** A 1 px dashed gain line; clips without a waveform also shade the gain they lose. */
|
|
453
|
+
function FadeRamp({
|
|
454
|
+
testId,
|
|
455
|
+
wedge,
|
|
456
|
+
line,
|
|
457
|
+
}: {
|
|
458
|
+
testId: string;
|
|
459
|
+
wedge: string | null;
|
|
460
|
+
line: [number, number, number, number];
|
|
461
|
+
}) {
|
|
462
|
+
const [x1, y1, x2, y2] = line;
|
|
463
|
+
return (
|
|
464
|
+
<>
|
|
465
|
+
{wedge && (
|
|
466
|
+
<polygon
|
|
467
|
+
data-testid={testId}
|
|
468
|
+
points={wedge}
|
|
469
|
+
fill="var(--timeline-fade-shade)"
|
|
470
|
+
fillOpacity={0.35}
|
|
471
|
+
/>
|
|
271
472
|
)}
|
|
473
|
+
<line
|
|
474
|
+
data-testid={wedge ? undefined : testId}
|
|
475
|
+
x1={x1}
|
|
476
|
+
y1={y1}
|
|
477
|
+
x2={x2}
|
|
478
|
+
y2={y2}
|
|
479
|
+
stroke="var(--clip-handle)"
|
|
480
|
+
strokeOpacity={0.55}
|
|
481
|
+
strokeWidth={1}
|
|
482
|
+
strokeDasharray="2 3"
|
|
483
|
+
vectorEffect="non-scaling-stroke"
|
|
484
|
+
/>
|
|
272
485
|
</>
|
|
273
486
|
);
|
|
274
487
|
}
|
|
275
488
|
|
|
276
489
|
function FadeHandle({
|
|
277
490
|
direction,
|
|
491
|
+
clipName,
|
|
278
492
|
value,
|
|
279
493
|
max,
|
|
494
|
+
snapLabel,
|
|
280
495
|
style,
|
|
496
|
+
tabLeft,
|
|
497
|
+
tabTop,
|
|
498
|
+
focusable,
|
|
281
499
|
dragging,
|
|
282
500
|
onPointerDown,
|
|
283
501
|
onPointerMove,
|
|
284
502
|
onPointerUp,
|
|
285
503
|
onPointerCancel,
|
|
504
|
+
onDoubleClick,
|
|
505
|
+
onKeyDown,
|
|
506
|
+
onKeyUp,
|
|
507
|
+
onFocusChange,
|
|
286
508
|
}: {
|
|
287
509
|
direction: "in" | "out";
|
|
510
|
+
clipName: string;
|
|
288
511
|
value: number;
|
|
289
512
|
max: number;
|
|
513
|
+
snapLabel: string | null;
|
|
290
514
|
style: CSSProperties;
|
|
515
|
+
tabLeft: number;
|
|
516
|
+
tabTop: number;
|
|
517
|
+
focusable: boolean;
|
|
291
518
|
dragging: boolean;
|
|
292
519
|
onPointerDown: (event: PointerEvent<HTMLDivElement>) => void;
|
|
293
520
|
onPointerMove: (event: PointerEvent<HTMLDivElement>) => void;
|
|
294
521
|
onPointerUp: (event: PointerEvent<HTMLDivElement>) => void;
|
|
295
522
|
onPointerCancel: (event: PointerEvent<HTMLDivElement>) => void;
|
|
523
|
+
onDoubleClick: () => void;
|
|
524
|
+
onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
525
|
+
onKeyUp: () => void;
|
|
526
|
+
onFocusChange: (focused: boolean) => void;
|
|
296
527
|
}) {
|
|
297
528
|
const label = direction === "in" ? "Fade in" : "Fade out";
|
|
529
|
+
const text = `${label} ${formatFadeSeconds(value)} s`;
|
|
298
530
|
return (
|
|
299
|
-
<Tooltip label={`${
|
|
531
|
+
<Tooltip label={snapLabel ? `${text}, snapped to ${snapLabel}` : text}>
|
|
300
532
|
<div
|
|
301
533
|
role="slider"
|
|
302
|
-
tabIndex={-1}
|
|
303
|
-
aria-label={label}
|
|
534
|
+
tabIndex={focusable ? 0 : -1}
|
|
535
|
+
aria-label={`${label}, ${clipName}`}
|
|
304
536
|
aria-valuemin={0}
|
|
305
537
|
aria-valuemax={max}
|
|
306
538
|
aria-valuenow={value}
|
|
307
539
|
aria-valuetext={`${formatFadeSeconds(value)}s`}
|
|
308
540
|
data-testid={`clip-fade-handle-${direction}`}
|
|
541
|
+
data-dragging={dragging ? "" : undefined}
|
|
542
|
+
className="timeline-fade-handle"
|
|
309
543
|
title={SUPPRESS_CLIP_NATIVE_TITLE}
|
|
310
544
|
style={style}
|
|
311
545
|
onPointerDown={onPointerDown}
|
|
@@ -313,30 +547,27 @@ function FadeHandle({
|
|
|
313
547
|
onPointerUp={onPointerUp}
|
|
314
548
|
onPointerCancel={onPointerCancel}
|
|
315
549
|
onClick={(e) => e.stopPropagation()}
|
|
316
|
-
onDoubleClick={(e) =>
|
|
550
|
+
onDoubleClick={(e) => {
|
|
551
|
+
e.stopPropagation();
|
|
552
|
+
onDoubleClick();
|
|
553
|
+
}}
|
|
554
|
+
onKeyDown={onKeyDown}
|
|
555
|
+
onKeyUp={onKeyUp}
|
|
556
|
+
onFocus={() => onFocusChange(true)}
|
|
557
|
+
onBlur={() => onFocusChange(false)}
|
|
317
558
|
>
|
|
318
|
-
<
|
|
559
|
+
<span
|
|
560
|
+
aria-hidden="true"
|
|
561
|
+
className="timeline-fade-tab"
|
|
562
|
+
style={{
|
|
563
|
+
left: tabLeft - TAB_WIDTH / 2,
|
|
564
|
+
top: tabTop,
|
|
565
|
+
width: TAB_WIDTH,
|
|
566
|
+
height: TAB_HEIGHT,
|
|
567
|
+
pointerEvents: "none",
|
|
568
|
+
}}
|
|
569
|
+
/>
|
|
319
570
|
</div>
|
|
320
571
|
</Tooltip>
|
|
321
572
|
);
|
|
322
573
|
}
|
|
323
|
-
|
|
324
|
-
function FadeDot({ active }: { active: boolean }) {
|
|
325
|
-
return (
|
|
326
|
-
<span
|
|
327
|
-
aria-hidden="true"
|
|
328
|
-
style={{
|
|
329
|
-
display: "block",
|
|
330
|
-
flexShrink: 0,
|
|
331
|
-
pointerEvents: "none",
|
|
332
|
-
width: HANDLE_SIZE,
|
|
333
|
-
height: HANDLE_SIZE,
|
|
334
|
-
borderRadius: "50%",
|
|
335
|
-
background: "var(--timeline-fade-dot)",
|
|
336
|
-
boxShadow: "0 0 0 1.5px var(--timeline-fade-dot-ring)",
|
|
337
|
-
transform: active ? "scale(1.15)" : undefined,
|
|
338
|
-
transition: "transform 80ms ease-out",
|
|
339
|
-
}}
|
|
340
|
-
/>
|
|
341
|
-
);
|
|
342
|
-
}
|