@dailies-studio/shot-editor 0.1.0 → 0.2.0
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/.vite/manifest.json +382 -0
- package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
- package/dist/assets/AgentPanel-DJEphah0.css +1 -0
- package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
- package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
- package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
- package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
- package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
- package/dist/assets/CurvePane-BjFVabKX.css +1 -0
- package/dist/assets/CurvePane-R22WzllJ.js +1 -0
- package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
- package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
- package/dist/assets/FontPicker-v0IOIArM.css +1 -0
- package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
- package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
- package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
- package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
- package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
- package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
- package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
- package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
- package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
- package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
- package/dist/assets/SoundMode-BmcZru3i.js +3 -0
- package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
- package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
- package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
- package/dist/assets/board-B88EPuLO.css +1 -0
- package/dist/assets/board-CL-he7SL.js +3 -0
- package/dist/assets/components-0oOSPN0l.js +1 -0
- package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
- package/dist/assets/googleFonts-BNon6M7X.js +1 -0
- package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
- package/dist/assets/index-4F5NQmI8.css +1 -0
- package/dist/assets/index-BGHQGV5z.js +117 -0
- package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/assets/libraryModel-CpIwfFha.js +1 -0
- package/dist/assets/lucide-6iK64tTB.js +9319 -0
- package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
- package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
- package/dist/assets/webmcp-BsB9gxWC.js +2 -0
- package/dist/index.html +34 -3
- package/package.json +12 -9
- package/src/addFamilies.ts +66 -0
- package/src/api.ts +1631 -55
- package/src/app/App.tsx +977 -198
- package/src/app/lazyPanel.tsx +137 -0
- package/src/app/lazySurfaces.tsx +203 -0
- package/src/audio/index.tsx +41 -10
- package/src/audio/meter.ts +485 -0
- package/src/audio/meterMath.ts +273 -0
- package/src/audio/meterStore.ts +155 -0
- package/src/audio/offlineMix.ts +289 -0
- package/src/audio/preview.ts +928 -118
- package/src/bridge.ts +15 -5
- package/src/bridgeTypes.ts +1611 -36
- package/src/channels.ts +17 -32
- package/src/fonts/googleFonts.ts +4 -0
- package/src/hostApi.ts +174 -0
- package/src/icons.tsx +770 -0
- package/src/layerWords.ts +61 -0
- package/src/main.tsx +56 -4
- package/src/panels/ResizeHandle.tsx +33 -43
- package/src/panels/agent/AgentDots.tsx +36 -0
- package/src/panels/agent/AgentPanel.tsx +119 -43
- package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
- package/src/panels/agent/agent.css +113 -97
- package/src/panels/agent/agentGate.ts +35 -0
- package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
- package/src/panels/code/CodeStudio.tsx +373 -0
- package/src/panels/code/FileTree.tsx +265 -0
- package/src/panels/code/codeBufferGuard.ts +52 -0
- package/src/panels/code/codeParts.tsx +321 -0
- package/src/panels/comments/CommentComposer.tsx +239 -0
- package/src/panels/comments/CommentThread.tsx +346 -0
- package/src/panels/comments/CommentsPanel.tsx +336 -0
- package/src/panels/comments/InboxButton.tsx +131 -0
- package/src/panels/comments/MentionMenu.tsx +98 -0
- package/src/panels/comments/parts.tsx +171 -0
- package/src/panels/export/ExportDialog.tsx +919 -96
- package/src/panels/export/ExportScrim.tsx +57 -0
- package/src/panels/files/AssetsTab.tsx +656 -0
- package/src/panels/files/FilePanel.tsx +1466 -216
- package/src/panels/files/LayerMarks.tsx +40 -0
- package/src/panels/files/LeftColumn.tsx +37 -0
- package/src/panels/files/McpSection.tsx +690 -47
- package/src/panels/files/MissingAssetsNotice.tsx +60 -0
- package/src/panels/files/assetDrag.ts +79 -0
- package/src/panels/files/resourceParts.tsx +255 -0
- package/src/panels/fonts/FontPicker.tsx +762 -0
- package/src/panels/fonts/faceCache.ts +207 -0
- package/src/panels/fonts/fontPicker.css +271 -0
- package/src/panels/fonts/pickerModel.ts +265 -0
- package/src/panels/fonts/pickerRequest.ts +46 -0
- package/src/panels/header/AccountAvatar.tsx +76 -0
- package/src/panels/header/AgentChip.tsx +77 -0
- package/src/panels/header/AppMenu.tsx +213 -0
- package/src/panels/header/Header.tsx +564 -84
- package/src/panels/header/HistoryControls.tsx +31 -5
- package/src/panels/header/PresenceStack.tsx +329 -0
- package/src/panels/header/ProjectTitle.tsx +232 -0
- package/src/panels/header/ShareButton.tsx +59 -0
- package/src/panels/header/StatusBadges.tsx +358 -27
- package/src/panels/header/SyncDeletedDialog.tsx +88 -0
- package/src/panels/header/ZoomControls.tsx +46 -15
- package/src/panels/header/agentHandoff.ts +52 -0
- package/src/panels/header/leave.ts +26 -0
- package/src/panels/help/HelpSection.tsx +189 -163
- package/src/panels/insert/InsertDrawer.tsx +723 -0
- package/src/panels/insert/iconSet.ts +109 -0
- package/src/panels/inspector/ExposeDot.tsx +117 -0
- package/src/panels/inspector/FramingRows.tsx +314 -44
- package/src/panels/inspector/InspectorFooter.tsx +91 -0
- package/src/panels/inspector/InspectorHead.tsx +425 -0
- package/src/panels/inspector/InspectorPanel.tsx +408 -124
- package/src/panels/inspector/KeyframeDot.tsx +373 -0
- package/src/panels/inspector/KeyframeSection.tsx +257 -0
- package/src/panels/inspector/MarkerSection.tsx +55 -11
- package/src/panels/inspector/ParamRow.tsx +84 -53
- package/src/panels/inspector/RightColumn.tsx +152 -0
- package/src/panels/inspector/TimeSection.tsx +866 -0
- package/src/panels/inspector/animate/EaseControl.tsx +244 -0
- package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
- package/src/panels/inspector/animate/ShotSection.tsx +562 -48
- package/src/panels/inspector/design/AlignRow.tsx +95 -31
- package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
- package/src/panels/inspector/design/ClonerSection.tsx +323 -62
- package/src/panels/inspector/design/ComponentSection.tsx +50 -43
- package/src/panels/inspector/design/CornerSection.tsx +45 -14
- package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
- package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
- package/src/panels/inspector/design/DeviceSection.tsx +210 -0
- package/src/panels/inspector/design/FillSection.tsx +83 -51
- package/src/panels/inspector/design/FontField.tsx +196 -0
- package/src/panels/inspector/design/HtmlSection.tsx +23 -12
- package/src/panels/inspector/design/InstanceSection.tsx +373 -154
- package/src/panels/inspector/design/MaskSection.tsx +84 -0
- package/src/panels/inspector/design/MaterialSection.tsx +177 -0
- package/src/panels/inspector/design/MediaSection.tsx +273 -120
- package/src/panels/inspector/design/MeshSection.tsx +910 -82
- package/src/panels/inspector/design/PaintRows.tsx +550 -0
- package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
- package/src/panels/inspector/design/ShadowSection.tsx +221 -48
- package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
- package/src/panels/inspector/design/StrokeSection.tsx +228 -43
- package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
- package/src/panels/inspector/design/TransformSection.tsx +244 -50
- package/src/panels/inspector/design/TypographySection.tsx +679 -110
- package/src/panels/inspector/layout.ts +295 -0
- package/src/panels/inspector/propValue.ts +199 -0
- package/src/panels/inspector/scene/CameraSection.tsx +232 -22
- package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
- package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
- package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
- package/src/panels/inspector/scene/SceneSection.tsx +219 -44
- package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
- package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
- package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
- package/src/panels/inspector/timeModel.ts +190 -0
- package/src/panels/inspector/useDocWrite.ts +438 -146
- package/src/panels/library/LibraryBrowser.tsx +435 -0
- package/src/panels/library/LibraryGrid.tsx +190 -0
- package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
- package/src/panels/library/LibraryRail.tsx +84 -0
- package/src/panels/library/LibrarySoundPanel.tsx +293 -0
- package/src/panels/library/SoundHistory.tsx +245 -0
- package/src/panels/library/StockSounds.tsx +390 -0
- package/src/panels/library/libraryModel.ts +147 -0
- package/src/panels/library/stockSearch.ts +77 -0
- package/src/panels/maskMarks.tsx +117 -0
- package/src/panels/material/MaterialCommentNode.tsx +54 -0
- package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
- package/src/panels/material/MaterialGraphMenu.tsx +45 -0
- package/src/panels/material/MaterialGroupNode.tsx +56 -0
- package/src/panels/material/MaterialLibrary.tsx +253 -0
- package/src/panels/material/MaterialNode.tsx +453 -0
- package/src/panels/material/MaterialNodePalette.tsx +349 -0
- package/src/panels/material/MaterialParameterControls.tsx +525 -0
- package/src/panels/material/MaterialRerouteNode.tsx +42 -0
- package/src/panels/material/MaterialWorkspace.tsx +501 -0
- package/src/panels/material/NodePreview.tsx +70 -0
- package/src/panels/material/flowModel.ts +441 -0
- package/src/panels/material/parameterValues.ts +231 -0
- package/src/panels/material/previewScheduler.ts +235 -0
- package/src/panels/material/reactFlow.css +6 -0
- package/src/panels/material/socketGlyph.ts +85 -0
- package/src/panels/material/viewportMemory.ts +58 -0
- package/src/panels/preferences/PreferencesDialog.tsx +215 -0
- package/src/panels/share/ShareComponentDialog.tsx +247 -0
- package/src/panels/stage/StageMenu.tsx +35 -5
- package/src/panels/stage/StagePanel.tsx +338 -23
- package/src/panels/stage/cellSlots.tsx +70 -0
- package/src/panels/storyboard/BoardCanvas.tsx +739 -0
- package/src/panels/storyboard/BoardCursors.tsx +158 -0
- package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
- package/src/panels/storyboard/BoardPlan.tsx +42 -0
- package/src/panels/storyboard/ComponentSheet.tsx +187 -0
- package/src/panels/storyboard/ComponentStage.tsx +235 -0
- package/src/panels/storyboard/MiniSketch.tsx +173 -0
- package/src/panels/storyboard/PartPicker.tsx +64 -0
- package/src/panels/storyboard/PlanPanel.tsx +617 -0
- package/src/panels/storyboard/ReferencePreview.tsx +217 -0
- package/src/panels/storyboard/ShotBrief.tsx +360 -0
- package/src/panels/storyboard/ShotCard.tsx +251 -0
- package/src/panels/storyboard/StagePreview.tsx +348 -0
- package/src/panels/storyboard/StillThumbs.tsx +162 -0
- package/src/panels/storyboard/StoryboardView.tsx +79 -0
- package/src/panels/storyboard/board.css +1065 -0
- package/src/panels/storyboard/board.ts +12 -0
- package/src/panels/storyboard/boardClipboard.ts +111 -0
- package/src/panels/storyboard/boardOrder.ts +68 -0
- package/src/panels/storyboard/boardRows.tsx +425 -0
- package/src/panels/storyboard/liveCap.ts +90 -0
- package/src/panels/storyboard/plan.css +295 -0
- package/src/panels/storyboard/posterCache.ts +133 -0
- package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
- package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
- package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
- package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
- package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
- package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
- package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
- package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
- package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
- package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
- package/src/panels/storyboard/shotboard/sketch.css +1392 -0
- package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
- package/src/panels/storyboard/storyboardDrop.ts +164 -0
- package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
- package/src/panels/studio/ComponentSidePanel.tsx +355 -0
- package/src/panels/studio/ComponentStudio.tsx +243 -0
- package/src/panels/studio/ComponentsHome.tsx +318 -0
- package/src/panels/studio/ComponentsPreview.tsx +179 -0
- package/src/panels/studio/EveryUse.tsx +244 -0
- package/src/panels/studio/ThisUsePanel.tsx +679 -0
- package/src/panels/studio/componentParts.tsx +341 -0
- package/src/panels/studio/components.ts +6 -0
- package/src/panels/studio/newCodeComponent.ts +66 -0
- package/src/panels/timeline/AudioFxButton.tsx +290 -0
- package/src/panels/timeline/AudioLane.tsx +674 -85
- package/src/panels/timeline/BlockRow.tsx +154 -85
- package/src/panels/timeline/ChannelRow.tsx +378 -129
- package/src/panels/timeline/CurvePane.tsx +508 -0
- package/src/panels/timeline/Filmstrip.tsx +361 -0
- package/src/panels/timeline/GenerateDrawer.tsx +407 -0
- package/src/panels/timeline/KeyStretch.tsx +115 -0
- package/src/panels/timeline/LifespanRow.tsx +730 -47
- package/src/panels/timeline/MarkerLane.tsx +46 -8
- package/src/panels/timeline/PlayButton.tsx +91 -0
- package/src/panels/timeline/PropertyPicker.tsx +254 -0
- package/src/panels/timeline/ShotRow.tsx +400 -0
- package/src/panels/timeline/SoundMode.tsx +1406 -0
- package/src/panels/timeline/TimelineDock.tsx +2338 -272
- package/src/panels/timeline/TransportVolume.tsx +163 -0
- package/src/panels/timeline/beatRuler.ts +74 -0
- package/src/panels/timeline/curveModel.ts +500 -0
- package/src/panels/timeline/duckUnderVoice.ts +68 -0
- package/src/panels/timeline/keyframeMenu.ts +138 -0
- package/src/panels/timeline/labelWidth.ts +49 -0
- package/src/panels/timeline/marquee.ts +155 -0
- package/src/panels/timeline/model.ts +1860 -199
- package/src/panels/timeline/snap.ts +125 -43
- package/src/panels/timeline/staggerPreview.tsx +92 -0
- package/src/panels/timeline/takeMeasure.ts +72 -0
- package/src/panels/timeline/useLive.ts +48 -0
- package/src/panels/timeline/viewport.ts +234 -0
- package/src/panels/toolbar/BackChip.tsx +116 -0
- package/src/panels/toolbar/CameraViewBar.tsx +118 -49
- package/src/panels/toolbar/CanvasBar.tsx +117 -0
- package/src/panels/toolbar/InsertButton.tsx +38 -0
- package/src/panels/toolbar/IsolationTrail.tsx +156 -0
- package/src/panels/toolbar/PanelToggles.tsx +86 -0
- package/src/panels/toolbar/ShadingBar.tsx +119 -26
- package/src/panels/toolbar/ToolDockBar.tsx +329 -80
- package/src/panels/toolbar/ToolFooter.tsx +102 -0
- package/src/panels/toolbar/ViewModeBar.tsx +247 -38
- package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
- package/src/panels/toolbar/useAddRows.tsx +326 -0
- package/src/panels/toolbar/useDismiss.ts +122 -0
- package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
- package/src/panels/versions/versionModel.ts +103 -0
- package/src/panels/virtualWindow.ts +271 -0
- package/src/shotTokens.css +151 -60
- package/src/stage/FrustumGizmo.tsx +504 -84
- package/src/stage/GridPlane.tsx +225 -16
- package/src/stage/NodePreviews.tsx +628 -0
- package/src/stage/PerfHud.tsx +437 -47
- package/src/stage/SplitRenderer.tsx +816 -169
- package/src/stage/StageApp.tsx +4274 -486
- package/src/stage/StageRootBoundary.tsx +114 -0
- package/src/stage/StageTools.tsx +651 -151
- package/src/stage/TransformGizmo.tsx +696 -0
- package/src/stage/adaptivePresentation.ts +145 -0
- package/src/stage/bridgeClient.ts +2522 -41
- package/src/stage/exportWorker.ts +9 -0
- package/src/stage/feed.ts +305 -14
- package/src/stage/fontPreview.ts +176 -0
- package/src/stage/frustumGeometry.ts +319 -1
- package/src/stage/furnitureTokens.ts +33 -0
- package/src/stage/gizmoMath.ts +508 -16
- package/src/stage/gpuTimer.ts +276 -0
- package/src/stage/instanceModules.ts +194 -33
- package/src/stage/interactions/move.ts +24 -5
- package/src/stage/isolation.ts +291 -33
- package/src/stage/jsSelfProfiler.ts +325 -0
- package/src/stage/main.tsx +116 -16
- package/src/stage/modalState.ts +222 -23
- package/src/stage/moduleEpoch.ts +60 -0
- package/src/stage/motionPathStage.ts +279 -0
- package/src/stage/motionPreview.ts +74 -0
- package/src/stage/overlays/Overlays.tsx +1112 -159
- package/src/stage/overlays/Rulers.tsx +495 -0
- package/src/stage/panes.ts +159 -10
- package/src/stage/project.ts +68 -3
- package/src/stage/renderScaleController.ts +730 -0
- package/src/stage/rulers.ts +129 -0
- package/src/stage/sceneExtent.ts +75 -0
- package/src/stage/shading.ts +20 -0
- package/src/stage/splineStage.ts +632 -0
- package/src/stage/stagePerformanceTelemetry.ts +717 -0
- package/src/stage/stageTheme.ts +36 -0
- package/src/stage/stageWarmup.ts +11 -0
- package/src/stage/textAutoSize.ts +124 -0
- package/src/stage/tier1Export.ts +641 -0
- package/src/stage/tokenColor.ts +14 -9
- package/src/stage/view.ts +56 -9
- package/src/stage/workingView.ts +277 -33
- package/src/state/accountPrefs.ts +261 -0
- package/src/state/agentActions.ts +173 -15
- package/src/state/agentActivity.ts +198 -0
- package/src/state/agentQuestion.ts +239 -0
- package/src/state/bootOpen.ts +42 -0
- package/src/state/cameraVerbs.ts +139 -0
- package/src/state/clientId.ts +6 -1
- package/src/state/comments.ts +1099 -0
- package/src/state/context.tsx +19 -1
- package/src/state/editorPrefs.ts +220 -0
- package/src/state/errorReports.ts +121 -0
- package/src/state/fieldWrite.ts +161 -0
- package/src/state/fontRecents.ts +70 -0
- package/src/state/fonts.ts +656 -0
- package/src/state/graphClipboard.ts +414 -0
- package/src/state/imageThumbs.ts +216 -0
- package/src/state/insert.ts +444 -67
- package/src/state/isolationTrail.ts +92 -0
- package/src/state/keyMoves.ts +134 -0
- package/src/state/keyframeClipboard.ts +177 -0
- package/src/state/keymap.ts +1153 -117
- package/src/state/layoutPrefs.ts +272 -0
- package/src/state/layouts.ts +205 -0
- package/src/state/measureMedia.ts +167 -0
- package/src/state/menu.ts +2341 -28
- package/src/state/motionStamp.ts +7 -0
- package/src/state/panelSize.ts +375 -5
- package/src/state/pendingChanges.ts +119 -0
- package/src/state/playHold.ts +78 -0
- package/src/state/playbackPreparation.ts +562 -0
- package/src/state/posterCapture.ts +242 -0
- package/src/state/presence.ts +254 -0
- package/src/state/projectComponents.ts +160 -0
- package/src/state/renderEvents.ts +298 -18
- package/src/state/runtimeErrors.ts +46 -0
- package/src/state/selection.ts +12 -0
- package/src/state/shortcuts.ts +1606 -0
- package/src/state/sketchGuide.ts +51 -0
- package/src/state/splineRefs.ts +87 -0
- package/src/state/stageHealth.ts +259 -0
- package/src/state/stageWatchdog.ts +47 -0
- package/src/state/stillStage.ts +330 -0
- package/src/state/stillThumbs.ts +474 -0
- package/src/state/store.ts +23591 -2982
- package/src/state/storyboardModel.ts +528 -0
- package/src/state/sync.ts +1257 -0
- package/src/state/syncCache.ts +159 -0
- package/src/state/triangles.ts +13 -0
- package/src/state/types.ts +1698 -55
- package/src/state/unsavedGuard.ts +49 -0
- package/src/state/viewMode.ts +200 -0
- package/src/state/webmcp.ts +1128 -0
- package/src/state/workspace.ts +1147 -0
- package/src/styles/assets-tab.css +304 -0
- package/src/styles/canvas.css +545 -0
- package/src/styles/comments.css +1435 -0
- package/src/styles/controls.css +162 -0
- package/src/styles/dialogs.css +390 -0
- package/src/styles/insert-drawer.css +586 -0
- package/src/styles/inspector-board.css +207 -0
- package/src/styles/inspector-object.css +486 -0
- package/src/styles/inspector-shot.css +252 -0
- package/src/styles/inspector.css +360 -0
- package/src/styles/left-column.css +1628 -0
- package/src/styles/library-preview.css +575 -0
- package/src/styles/library.css +512 -0
- package/src/styles/modes/code.css +569 -0
- package/src/styles/modes/component.css +1561 -0
- package/src/styles/modes/material.css +1201 -0
- package/src/styles/modes/storyboard.css +518 -0
- package/src/styles/share-component.css +49 -0
- package/src/styles/shell.css +653 -0
- package/src/styles/shot-brief.css +360 -0
- package/src/styles/states.css +76 -0
- package/src/styles/sync.css +380 -0
- package/src/styles/timeline-graph.css +384 -0
- package/src/styles/timeline-meters.css +89 -0
- package/src/styles/timeline-rows.css +1959 -0
- package/src/styles/timeline-sound.css +1003 -0
- package/src/styles/timeline.css +754 -0
- package/src/styles/tool-footer.css +525 -0
- package/src/styles/topbar.css +886 -0
- package/src/styles/versions.css +262 -0
- package/src/styles.css +72 -2921
- package/src/tools.ts +108 -23
- package/dist/assets/index-B7notge7.css +0 -1
- package/dist/assets/index-BBE6Ta9L.js +0 -52
- package/src/panels/files/icons.tsx +0 -118
- package/src/panels/header/DocSwitcher.tsx +0 -85
- package/src/panels/header/TransportBar.tsx +0 -86
- package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
- package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
- package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
- package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
- package/src/panels/inspector/animate/useBlocks.ts +0 -43
- package/src/panels/material/MaterialPicker.tsx +0 -172
- package/src/panels/timeline/SceneStrip.tsx +0 -145
- package/src/panels/toolbar/AddMenu.tsx +0 -230
- package/src/stage/MoveGizmo.tsx +0 -411
|
@@ -1,12 +1,42 @@
|
|
|
1
|
-
// The audio
|
|
1
|
+
// The audio lanes: one coloured band per clip, in whichever of the two homes
|
|
2
|
+
// holds it.
|
|
2
3
|
//
|
|
3
|
-
// ──
|
|
4
|
+
// ── TWO HOMES, TWO PLACES ON SCREEN ─────────────────────────────────────
|
|
4
5
|
//
|
|
5
|
-
// `doc.audio` is
|
|
6
|
-
// the
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// `doc.audio` is the BED — master-timed, spanning the cuts — and it draws here,
|
|
7
|
+
// at the bottom of the dock, where every NLE puts the music. `Scene3D.audio`
|
|
8
|
+
// (schema v13) is the sound a SHOT owns, and it draws inside that shot's group
|
|
9
|
+
// (`SceneAudioRow`), because that is what owning it means: it moves, duplicates
|
|
10
|
+
// and dies with the shot, and it is cut at the shot's end.
|
|
11
|
+
//
|
|
12
|
+
// Both use `AudioClipBar`, which is base-agnostic — it takes `offsetFrames`
|
|
13
|
+
// (the shot's master start, or 0 for the bed) exactly as `BlockRow` does, and
|
|
14
|
+
// every gesture below it is a signed DELTA, which means the same thing in
|
|
15
|
+
// either frame base. The one thing that is not base-agnostic is the snap
|
|
16
|
+
// anchor, which is always master, and that is what `offsetFrames` is added into.
|
|
17
|
+
//
|
|
18
|
+
// ── A THIRD SOUND EXISTS, AND IT IS DRAWN SOMEWHERE ELSE ────────────────
|
|
19
|
+
//
|
|
20
|
+
// A video layer's `video.sound` reaches the mix through `masterAudioClips` as a
|
|
21
|
+
// VIRTUAL clip whose id is the layer id and whose `MasterAudioClip.layerId` is
|
|
22
|
+
// set. It is deliberately absent from both lanes here: it is drawn in the video
|
|
23
|
+
// row itself (`LifespanRow`), under that recording's filmstrip, because a
|
|
24
|
+
// soundtrack that appears in two places is one an author can trim in the wrong
|
|
25
|
+
// one. Both lanes read their own record (`doc.audio`, `Scene3D.audio`), so this
|
|
26
|
+
// is a property of WHERE THEY READ rather than a filter they apply, and there
|
|
27
|
+
// is no `layerId` on an `AudioClipRef` for a filter to test.
|
|
28
|
+
//
|
|
29
|
+
// The one place the flatten IS read here is `duckCandidates`, which must offer
|
|
30
|
+
// the virtual clips: ducking a bed under a recording's dialogue is the case the
|
|
31
|
+
// feature exists for, and naming a source is not drawing a bar.
|
|
32
|
+
//
|
|
33
|
+
// ── WHY EACH IS ONE LANE AND NOT A TRACK PER CLIP ───────────────────────
|
|
34
|
+
//
|
|
35
|
+
// Neither record has a lane field, and the reference editor draws one music bed
|
|
36
|
+
// under the whole video. A lane-packer would be inventing a document concept
|
|
37
|
+
// the schema deliberately does not have, and the first thing it would need is
|
|
38
|
+
// somewhere to persist which lane a clip landed in. Clips overlap by drawing
|
|
39
|
+
// over each other, in document order.
|
|
10
40
|
//
|
|
11
41
|
// ── THE GESTURES ────────────────────────────────────────────────────────
|
|
12
42
|
//
|
|
@@ -15,41 +45,126 @@
|
|
|
15
45
|
// IN / OUT trim; the head trim moves `offsetFrames` with `frame` so the
|
|
16
46
|
// audio under the remaining bar does not slide.
|
|
17
47
|
// FADES the two corner handles, dragged along the bar.
|
|
18
|
-
// GAIN the horizontal rubber band, dragged vertically
|
|
48
|
+
// GAIN the horizontal rubber band, dragged vertically — and once the
|
|
49
|
+
// clip has a `gainTrack`, the band edits the KEY at the playhead
|
|
50
|
+
// and the flat line becomes the sampled envelope.
|
|
51
|
+
//
|
|
52
|
+
// GAIN is Sound mode's only (`mix`), with the duck, the gain key and the clip
|
|
53
|
+
// mute. Mixing lives in the Sound tab, so these rows stay slim: the waveform,
|
|
54
|
+
// move, trim, fades and the home switch.
|
|
19
55
|
//
|
|
20
56
|
// All of them go through `beginBlockDrag`/`endBlockDrag`, which is the dock's
|
|
21
57
|
// ONE history scrub: each gesture is one undo entry and Escape restores the
|
|
22
58
|
// base by reference. All of them snap through the shared `snap.ts` — there is
|
|
23
59
|
// no second snapper in this lane.
|
|
24
60
|
import { useEffect, useRef, useState } from "react";
|
|
25
|
-
import type { PointerEvent as ReactPointerEvent } from "react";
|
|
61
|
+
import type { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react";
|
|
26
62
|
import { drawClipWaveform, frameToPx, loadPeaks, startDrag } from "@dailies-studio/timeline";
|
|
27
63
|
import type { PeakData } from "@dailies-studio/timeline";
|
|
64
|
+
import { IconVolume, IconVolumeOff } from "@dailies-studio/ui";
|
|
28
65
|
import { framesToSeconds } from "@dailies-studio/engine";
|
|
29
66
|
import type { Rational } from "@dailies-studio/engine";
|
|
30
|
-
import {
|
|
31
|
-
|
|
67
|
+
import {
|
|
68
|
+
DUCK_DEFAULT_STRENGTH,
|
|
69
|
+
DUCK_MAX_DEPTH_DB,
|
|
70
|
+
MAX_AUDIO_GAIN,
|
|
71
|
+
audioGainSampler,
|
|
72
|
+
masterAudioClips,
|
|
73
|
+
} from "@dailies-studio/shot/doc";
|
|
74
|
+
import type { AudioClipRef, ShotAsset, ShotTrack } from "@dailies-studio/shot/doc";
|
|
75
|
+
import { useStudioTheme } from "@dailies-studio/ui/theme";
|
|
32
76
|
import { useShotEditor, useStore } from "../../state/context.js";
|
|
33
|
-
import {
|
|
77
|
+
import { ACCEPTED_AUDIO_EXTENSIONS } from "../storyboard/storyboardDrop.js";
|
|
78
|
+
import { AUDIO_ROW_H, audioClipView, frameDelta } from "./model.js";
|
|
34
79
|
import type { AudioDrag } from "./model.js";
|
|
35
80
|
import { isSnapSuppressed } from "./snap.js";
|
|
81
|
+
import { AudioFxButton } from "./AudioFxButton.js";
|
|
36
82
|
import type { SnapPress } from "./snap.js";
|
|
83
|
+
import { formatCombo } from "../../state/shortcuts.js";
|
|
84
|
+
|
|
85
|
+
const WAVE_COLOR_FALLBACK = "rgba(150, 205, 255, 0.7)";
|
|
86
|
+
/**
|
|
87
|
+
* Mirrors of timeline-rows.css, pinned to it by audioLane.test: `.audio-lane`'s
|
|
88
|
+
* height (with its 1px top seam), `.scene-audio-row`'s height, and
|
|
89
|
+
* `.audio-clip`'s inset top and bottom.
|
|
90
|
+
*
|
|
91
|
+
* ONE AUDIO ROW HEIGHT. A shot's own row is the boards' 32px lane too: inside
|
|
92
|
+
* a shot it is the only audio row on screen (see `AudioLane`'s shot-context
|
|
93
|
+
* early return), and two heights for one medium read as two features.
|
|
94
|
+
* `groupHeights` counts it through this constant.
|
|
95
|
+
*/
|
|
96
|
+
export const AUDIO_LANE_H = AUDIO_ROW_H;
|
|
97
|
+
export const SHOT_AUDIO_ROW_H = AUDIO_LANE_H;
|
|
98
|
+
export const AUDIO_CLIP_INSET = 4;
|
|
99
|
+
/**
|
|
100
|
+
* A clip bar's inner height per row kind, in px: what the waveform canvas, the
|
|
101
|
+
* fade ramp and the gain envelope are drawn into. The row less the bar's two
|
|
102
|
+
* insets and its 1px border; the master lane also pays its top seam.
|
|
103
|
+
*/
|
|
104
|
+
const MASTER_WAVE_H = AUDIO_LANE_H - 1 - 2 * AUDIO_CLIP_INSET - 2;
|
|
105
|
+
const SHOT_WAVE_H = SHOT_AUDIO_ROW_H - 2 * AUDIO_CLIP_INSET - 2;
|
|
37
106
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
107
|
+
/**
|
|
108
|
+
* What the Sound tab's file input offers and what the drop box says, from ONE
|
|
109
|
+
* list: the extensions the import accepts (storyboardDrop's mirror of the
|
|
110
|
+
* CLI's audio kinds). Extensions only, no MIME types: `audio/ogg` would also offer an
|
|
111
|
+
* `.opus` the import then refuses, and the picker should not offer a file the
|
|
112
|
+
* drop would turn away.
|
|
113
|
+
*/
|
|
114
|
+
export const AUDIO_FILE_ACCEPT = ACCEPTED_AUDIO_EXTENSIONS.join(",");
|
|
115
|
+
export const AUDIO_FORMAT_HINT = ACCEPTED_AUDIO_EXTENSIONS.map((ext) => ext.slice(1).toUpperCase()).join(" / ");
|
|
41
116
|
|
|
42
|
-
|
|
43
|
-
|
|
117
|
+
/** The empty row's drop box, shared by the master lane and a shot's own row so
|
|
118
|
+
* the two say the same sentence in the same shape. */
|
|
119
|
+
function AudioDropBox() {
|
|
120
|
+
return (
|
|
121
|
+
<span className="audio-empty">
|
|
122
|
+
<span className="audio-empty-copy">Drop an audio file, or press + to add sound</span>
|
|
123
|
+
<span className="audio-empty-formats">{AUDIO_FORMAT_HINT}</span>
|
|
124
|
+
</span>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function AudioLane(props: {
|
|
129
|
+
ppf: number;
|
|
130
|
+
labelWidth: number;
|
|
131
|
+
beginSnap?: SnapPress;
|
|
132
|
+
/**
|
|
133
|
+
* The SHOT CONTEXT's window, or absent in the scene context. Clips are
|
|
134
|
+
* MASTER-timed and stay so; this re-zeros the lane on the shot and keeps
|
|
135
|
+
* the clips that OVERLAP it.
|
|
136
|
+
*
|
|
137
|
+
* Overlap and not containment, and the difference is the common case: a music
|
|
138
|
+
* bed spans the whole video, so a containment test would empty the lane in
|
|
139
|
+
* every shot — in a tool whose launch deliverable is a 52-second music ad.
|
|
140
|
+
* A bed that starts before the shot is drawn from a negative x and its head is
|
|
141
|
+
* clipped by the track box, which is the honest read: it continues off-screen
|
|
142
|
+
* to the left because it continues before this shot.
|
|
143
|
+
*/
|
|
144
|
+
shotWindow?: { origin: number; len: number } | undefined;
|
|
145
|
+
}) {
|
|
146
|
+
const { ppf, labelWidth, beginSnap, shotWindow } = props;
|
|
147
|
+
const origin = shotWindow?.origin ?? 0;
|
|
44
148
|
const store = useStore();
|
|
45
149
|
const doc = useShotEditor((s) => s.doc);
|
|
46
150
|
const [error, setError] = useState<string | null>(null);
|
|
47
151
|
const [importing, setImporting] = useState(false);
|
|
48
152
|
const [over, setOver] = useState(false);
|
|
49
|
-
const fileRef = useRef<HTMLInputElement | null>(null);
|
|
50
153
|
|
|
51
154
|
if (doc === null) return null;
|
|
52
|
-
const clips = Object.values(doc.audio)
|
|
155
|
+
const clips = Object.values(doc.audio).filter((clip) => {
|
|
156
|
+
if (shotWindow === undefined) return true;
|
|
157
|
+
const start = Math.trunc(clip.frame);
|
|
158
|
+
const end = start + Math.max(1, Math.trunc(clip.durationFrames));
|
|
159
|
+
return end > shotWindow.origin && start < shotWindow.origin + shotWindow.len;
|
|
160
|
+
});
|
|
161
|
+
// INSIDE A SHOT WITH NO BED OVER IT, THE SHOT'S OWN ROW IS THE AUDIO ROW. That
|
|
162
|
+
// row already draws the drop box (`SceneAudioRow`'s `empty`), so an empty
|
|
163
|
+
// master lane under it was a second "Audio" row with the same box, one that
|
|
164
|
+
// drops into the bed instead of the shot. The Project keeps the master lane
|
|
165
|
+
// always: there it is the only way into the bed. Nothing sums this lane's
|
|
166
|
+
// height, so dropping it moves no spacer.
|
|
167
|
+
if (shotWindow !== undefined && clips.length === 0 && !importing) return null;
|
|
53
168
|
|
|
54
169
|
/**
|
|
55
170
|
* NEW CLIPS LAND AT THE PLAYHEAD, dropped ones included.
|
|
@@ -60,22 +175,16 @@ export function AudioLane(props: { ppf: number; labelWidth: number; beginSnap?:
|
|
|
60
175
|
* one would make "where does the thing I make appear" a question with two
|
|
61
176
|
* answers. The clip is draggable the moment it exists.
|
|
62
177
|
*/
|
|
63
|
-
const importFiles = (files: FileList | null): void =>
|
|
64
|
-
|
|
65
|
-
if (file === undefined) return;
|
|
66
|
-
setError(null);
|
|
67
|
-
setImporting(true);
|
|
68
|
-
void store
|
|
69
|
-
.importAudio(file)
|
|
70
|
-
.catch((e: unknown) => setError(e instanceof Error ? e.message : String(e)))
|
|
71
|
-
.finally(() => setImporting(false));
|
|
72
|
-
};
|
|
178
|
+
const importFiles = (files: FileList | null): void =>
|
|
179
|
+
importEach(store, files, undefined, setImporting, setError);
|
|
73
180
|
|
|
74
181
|
return (
|
|
75
182
|
<div
|
|
76
183
|
className={`audio-lane${over ? " is-over" : ""}`}
|
|
77
184
|
data-testid="audio-lane"
|
|
78
185
|
onDragOver={(e) => {
|
|
186
|
+
// Files only: a shot reorder or an asset drag is not an import.
|
|
187
|
+
if (!e.dataTransfer.types.includes("Files")) return;
|
|
79
188
|
e.preventDefault();
|
|
80
189
|
setOver(true);
|
|
81
190
|
}}
|
|
@@ -87,34 +196,13 @@ export function AudioLane(props: { ppf: number; labelWidth: number; beginSnap?:
|
|
|
87
196
|
}}
|
|
88
197
|
>
|
|
89
198
|
<div className="audio-label-cell" style={{ width: labelWidth }}>
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
title="Import audio at the playhead"
|
|
95
|
-
disabled={importing}
|
|
96
|
-
onClick={() => fileRef.current?.click()}
|
|
97
|
-
>
|
|
98
|
-
{importing ? "…" : "+"}
|
|
99
|
-
</button>
|
|
100
|
-
<input
|
|
101
|
-
ref={fileRef}
|
|
102
|
-
className="audio-file-input"
|
|
103
|
-
type="file"
|
|
104
|
-
accept="audio/*"
|
|
105
|
-
hidden
|
|
106
|
-
data-testid="audio-file-input"
|
|
107
|
-
onChange={(e) => {
|
|
108
|
-
importFiles(e.currentTarget.files);
|
|
109
|
-
// Cleared so re-picking the SAME file fires change again.
|
|
110
|
-
e.currentTarget.value = "";
|
|
111
|
-
}}
|
|
112
|
-
/>
|
|
199
|
+
{/* Sound's teal square, the swatch every track row leads with. */}
|
|
200
|
+
<span className="audio-swatch" aria-hidden="true" />
|
|
201
|
+
<span className="audio-lane-name">Sound</span>
|
|
202
|
+
<AddSoundButton importing={importing} />
|
|
113
203
|
</div>
|
|
114
204
|
<div className="audio-track">
|
|
115
|
-
{clips.length === 0 && !importing ?
|
|
116
|
-
<span className="audio-empty">Drop an audio file here, or press +</span>
|
|
117
|
-
) : null}
|
|
205
|
+
{clips.length === 0 && !importing ? <AudioDropBox /> : null}
|
|
118
206
|
{clips.map((clip) => (
|
|
119
207
|
<AudioClipBar
|
|
120
208
|
key={clip.id}
|
|
@@ -122,7 +210,10 @@ export function AudioLane(props: { ppf: number; labelWidth: number; beginSnap?:
|
|
|
122
210
|
asset={doc.assets[clip.asset]}
|
|
123
211
|
fps={doc.fps}
|
|
124
212
|
ppf={ppf}
|
|
213
|
+
offsetFrames={0}
|
|
214
|
+
originFrames={origin}
|
|
125
215
|
beginSnap={beginSnap}
|
|
216
|
+
waveH={MASTER_WAVE_H}
|
|
126
217
|
/>
|
|
127
218
|
))}
|
|
128
219
|
</div>
|
|
@@ -135,17 +226,340 @@ export function AudioLane(props: { ppf: number; labelWidth: number; beginSnap?:
|
|
|
135
226
|
);
|
|
136
227
|
}
|
|
137
228
|
|
|
138
|
-
|
|
229
|
+
/**
|
|
230
|
+
* A Sound row's "+": the Sound tab, where adding is Import, Library and
|
|
231
|
+
* Generate on one row per track. These rows keep the drop (a file dragged onto
|
|
232
|
+
* the lane still lands at the playhead), not a second picker.
|
|
233
|
+
*/
|
|
234
|
+
function AddSoundButton({ importing }: { importing: boolean }) {
|
|
235
|
+
const store = useStore();
|
|
236
|
+
return (
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
className="audio-add"
|
|
240
|
+
data-testid="audio-add"
|
|
241
|
+
aria-label="Add sound"
|
|
242
|
+
title="Add sound: opens the Sound tab"
|
|
243
|
+
disabled={importing}
|
|
244
|
+
onClick={() => store.setDockMode("sound")}
|
|
245
|
+
>
|
|
246
|
+
{importing ? "…" : "+"}
|
|
247
|
+
</button>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Import every file, one after another, so a multi-file drop loses nothing.
|
|
253
|
+
* Each store error already names its file, so the failures are shown together.
|
|
254
|
+
*/
|
|
255
|
+
export function importEach(
|
|
256
|
+
store: ReturnType<typeof useStore>,
|
|
257
|
+
files: FileList | null,
|
|
258
|
+
sceneId: string | undefined,
|
|
259
|
+
setImporting: (busy: boolean) => void,
|
|
260
|
+
setError: (message: string | null) => void,
|
|
261
|
+
trackId?: string,
|
|
262
|
+
): void {
|
|
263
|
+
// Copied now: the picker clears its input right after this returns.
|
|
264
|
+
const list = Array.from(files ?? []);
|
|
265
|
+
if (list.length === 0) return;
|
|
266
|
+
setError(null);
|
|
267
|
+
setImporting(true);
|
|
268
|
+
void (async () => {
|
|
269
|
+
const failed: string[] = [];
|
|
270
|
+
for (const file of list) {
|
|
271
|
+
try {
|
|
272
|
+
await store.importAudio(file, undefined, sceneId, trackId);
|
|
273
|
+
} catch (e) {
|
|
274
|
+
failed.push(e instanceof Error ? e.message : String(e));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
setError(failed.length === 0 ? null : failed.join(" "));
|
|
278
|
+
setImporting(false);
|
|
279
|
+
})();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A clip's name: its file name without the store's hash prefix. */
|
|
283
|
+
export function clipLabel(clip: AudioClipRef, asset: ShotAsset | undefined): string {
|
|
284
|
+
return asset?.path.replace(/^[0-9a-f]{8}-/, "") ?? clip.asset;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The duck popover's rows. Two clips cut from one file share a name, so a
|
|
288
|
+
* repeated name gets a count ("voiceover.wav · 2") to keep the rows apart. */
|
|
289
|
+
function duckRows(
|
|
290
|
+
clips: readonly AudioClipRef[],
|
|
291
|
+
assets: Readonly<Record<string, ShotAsset>>,
|
|
292
|
+
): { id: string; label: string }[] {
|
|
293
|
+
const labels = clips.map((c) => clipLabel(c, assets[c.asset]));
|
|
294
|
+
const seen = new Map<string, number>();
|
|
295
|
+
return clips.map((c, i) => {
|
|
296
|
+
const label = labels[i]!;
|
|
297
|
+
const n = (seen.get(label) ?? 0) + 1;
|
|
298
|
+
seen.set(label, n);
|
|
299
|
+
return { id: c.id, label: labels.indexOf(label) === labels.lastIndexOf(label) ? label : `${label} · ${n}` };
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* DUCKING, from the bar.
|
|
305
|
+
*
|
|
306
|
+
* The control is a popover rather than a drag, because a duck is a
|
|
307
|
+
* RELATIONSHIP: it names other clips, and there is no gesture on one bar that
|
|
308
|
+
* expresses "under these". The button stays visible at rest whether or not the
|
|
309
|
+
* clip ducks, which is the design system's rule about a capability's only
|
|
310
|
+
* affordance; what changes is whether it reads as armed.
|
|
311
|
+
*/
|
|
312
|
+
function DuckButton({
|
|
313
|
+
clip,
|
|
314
|
+
candidates,
|
|
315
|
+
store,
|
|
316
|
+
}: {
|
|
317
|
+
clip: AudioClipRef;
|
|
318
|
+
/** Every OTHER clip in the document, named. A duck may name a clip in either
|
|
319
|
+
* home, which is why this is the flatten and not the lane. */
|
|
320
|
+
candidates: readonly { id: string; label: string }[];
|
|
321
|
+
store: ReturnType<typeof useStore>;
|
|
322
|
+
}) {
|
|
323
|
+
const [open, setOpen] = useState(false);
|
|
324
|
+
const duck = clip.duck;
|
|
325
|
+
const sources = duck?.sources ?? [];
|
|
326
|
+
const strength = duck?.strength ?? DUCK_DEFAULT_STRENGTH;
|
|
327
|
+
|
|
328
|
+
const toggle = (id: string): void => {
|
|
329
|
+
const next = sources.includes(id) ? sources.filter((s) => s !== id) : [...sources, id];
|
|
330
|
+
store.setAudioDuck(clip.id, next, duck?.strength);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<span className="audio-duck">
|
|
335
|
+
<button
|
|
336
|
+
type="button"
|
|
337
|
+
className="audio-clip-btn"
|
|
338
|
+
aria-label={duck === undefined ? "Duck under a voice" : "Ducking"}
|
|
339
|
+
aria-pressed={duck !== undefined}
|
|
340
|
+
aria-expanded={open}
|
|
341
|
+
data-testid={`audio-duck-${clip.id}`}
|
|
342
|
+
title={
|
|
343
|
+
duck === undefined
|
|
344
|
+
? "Duck under a voice: hold this clip down while another one speaks"
|
|
345
|
+
: `Ducking under ${sources.length} clip(s) at ${Math.round(strength * 100)}%`
|
|
346
|
+
}
|
|
347
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
348
|
+
onClick={() => setOpen((v) => !v)}
|
|
349
|
+
>
|
|
350
|
+
⌄
|
|
351
|
+
</button>
|
|
352
|
+
{open && (
|
|
353
|
+
<div className="audio-duck-pop" onPointerDown={(e) => e.stopPropagation()}>
|
|
354
|
+
{candidates.length === 0 ? (
|
|
355
|
+
<p className="audio-duck-empty">
|
|
356
|
+
Nothing to duck under yet. Add the narration, then come back.
|
|
357
|
+
</p>
|
|
358
|
+
) : (
|
|
359
|
+
<>
|
|
360
|
+
<p className="audio-duck-title">Hold this down while these speak</p>
|
|
361
|
+
{candidates.map((other) => (
|
|
362
|
+
<label key={other.id} className="audio-duck-row">
|
|
363
|
+
<input
|
|
364
|
+
type="checkbox"
|
|
365
|
+
checked={sources.includes(other.id)}
|
|
366
|
+
onChange={() => toggle(other.id)}
|
|
367
|
+
/>
|
|
368
|
+
<span>{other.label}</span>
|
|
369
|
+
</label>
|
|
370
|
+
))}
|
|
371
|
+
<label className="audio-duck-row audio-duck-strength">
|
|
372
|
+
<span>Depth</span>
|
|
373
|
+
<input
|
|
374
|
+
type="range"
|
|
375
|
+
min={0.05}
|
|
376
|
+
max={1}
|
|
377
|
+
step={0.05}
|
|
378
|
+
value={strength}
|
|
379
|
+
disabled={sources.length === 0}
|
|
380
|
+
aria-label="Duck depth"
|
|
381
|
+
onChange={(e) => store.setAudioDuck(clip.id, sources, Number(e.currentTarget.value))}
|
|
382
|
+
/>
|
|
383
|
+
{/* The dB, because that is the unit the decision is made in.
|
|
384
|
+
0.25 is 6 dB, which is the default for a reason. */}
|
|
385
|
+
<span className="audio-duck-db">{Math.round(strength * DUCK_MAX_DEPTH_DB)} dB</span>
|
|
386
|
+
</label>
|
|
387
|
+
</>
|
|
388
|
+
)}
|
|
389
|
+
</div>
|
|
390
|
+
)}
|
|
391
|
+
</span>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* A SHOT'S OWN AUDIO, drawn inside that shot's group.
|
|
397
|
+
*
|
|
398
|
+
* Last in the group on purpose: video above, audio below, the way the master
|
|
399
|
+
* bed sits under every scene group in the dock. The clips are scene-local and
|
|
400
|
+
* `offsetFrames` puts them back on the ruler's master base.
|
|
401
|
+
*
|
|
402
|
+
* DRAWN ONLY WHEN THE SHOT HAS SOME, unless the caller asks for the empty
|
|
403
|
+
* row (`empty`). Over the Project the master lane is always present because it
|
|
404
|
+
* teaches the import (see `TimelineDock`'s `laneShown`). The boards also draw a drop box in
|
|
405
|
+
* a shot's own audio row, and `empty` is that row: the same box as the master
|
|
406
|
+
* lane's, dropping into this shot. The dock passes it inside an entered shot,
|
|
407
|
+
* where it is the one audio row on screen. It is opt-in because the dock's
|
|
408
|
+
* group height sum (`groupHeights`) adds `SHOT_AUDIO_ROW_H` only when the shot
|
|
409
|
+
* has audio OR is the entered shot, the same predicate the dock passes `empty`
|
|
410
|
+
* on, and a row drawn without that count would push every group under it off
|
|
411
|
+
* its spacer. Over the Project, the way in is a drop on this row once a clip is
|
|
412
|
+
* here, or the clip's own ⇥ button, which is the shortest path from "I
|
|
413
|
+
* imported some music" to "this whoosh belongs to that cut". The `+` opens the
|
|
414
|
+
* Sound tab, the one place sound is added from a picker.
|
|
415
|
+
*/
|
|
416
|
+
export function SceneAudioRow(props: {
|
|
417
|
+
sceneId: string;
|
|
418
|
+
ppf: number;
|
|
419
|
+
labelWidth: number;
|
|
420
|
+
/** The shot's start on the master timeline. */
|
|
421
|
+
offsetFrames: number;
|
|
422
|
+
/** The shot context's re-zero, 0 in the scene context. */
|
|
423
|
+
originFrames: number;
|
|
424
|
+
beginSnap?: SnapPress;
|
|
425
|
+
/** Draw the row and its drop box even with no clips. The caller must count
|
|
426
|
+
* the row's height whenever it passes this (see above). */
|
|
427
|
+
empty?: boolean;
|
|
428
|
+
}) {
|
|
429
|
+
const { sceneId, ppf, labelWidth, offsetFrames, originFrames, beginSnap, empty = false } = props;
|
|
430
|
+
const store = useStore();
|
|
431
|
+
const doc = useShotEditor((s) => s.doc);
|
|
432
|
+
const [over, setOver] = useState(false);
|
|
433
|
+
const [importing, setImporting] = useState(false);
|
|
434
|
+
const [error, setError] = useState<string | null>(null);
|
|
435
|
+
|
|
436
|
+
const scene = doc?.scenesById[sceneId];
|
|
437
|
+
const clips = Object.values(scene?.audio ?? {});
|
|
438
|
+
if (doc === null || scene === undefined || (clips.length === 0 && !importing && !empty)) return null;
|
|
439
|
+
|
|
440
|
+
// The SHOT is the home, and the playhead is converted into its base by the
|
|
441
|
+
// store — this row never does frame arithmetic of its own.
|
|
442
|
+
const importFiles = (files: FileList | null): void =>
|
|
443
|
+
importEach(store, files, sceneId, setImporting, setError);
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<div
|
|
447
|
+
className={`scene-audio-row${over ? " is-over" : ""}`}
|
|
448
|
+
data-testid={`scene-audio-${sceneId}`}
|
|
449
|
+
data-scene-audio={sceneId}
|
|
450
|
+
onDragOver={(e) => {
|
|
451
|
+
if (!e.dataTransfer.types.includes("Files")) return;
|
|
452
|
+
e.preventDefault();
|
|
453
|
+
setOver(true);
|
|
454
|
+
}}
|
|
455
|
+
onDragLeave={() => setOver(false)}
|
|
456
|
+
onDrop={(e) => {
|
|
457
|
+
e.preventDefault();
|
|
458
|
+
// STOPPED, so the master lane's own drop handler does not also fire and
|
|
459
|
+
// make a second clip in the other home from one file.
|
|
460
|
+
e.stopPropagation();
|
|
461
|
+
setOver(false);
|
|
462
|
+
importFiles(e.dataTransfer?.files ?? null);
|
|
463
|
+
}}
|
|
464
|
+
>
|
|
465
|
+
<div className="scene-audio-label" style={{ width: labelWidth }}>
|
|
466
|
+
<span className="audio-swatch" aria-hidden="true" />
|
|
467
|
+
<span className="scene-audio-name">Sound</span>
|
|
468
|
+
<AddSoundButton importing={importing} />
|
|
469
|
+
</div>
|
|
470
|
+
<div className="audio-track">
|
|
471
|
+
{clips.length === 0 && !importing ? <AudioDropBox /> : null}
|
|
472
|
+
{clips.map((clip) => (
|
|
473
|
+
<AudioClipBar
|
|
474
|
+
key={clip.id}
|
|
475
|
+
clip={clip}
|
|
476
|
+
asset={doc.assets[clip.asset]}
|
|
477
|
+
fps={doc.fps}
|
|
478
|
+
ppf={ppf}
|
|
479
|
+
offsetFrames={offsetFrames}
|
|
480
|
+
originFrames={originFrames}
|
|
481
|
+
beginSnap={beginSnap}
|
|
482
|
+
ownedBy={sceneId}
|
|
483
|
+
limitFrames={Math.max(0, Math.trunc(scene.durationFrames))}
|
|
484
|
+
waveH={SHOT_WAVE_H}
|
|
485
|
+
/>
|
|
486
|
+
))}
|
|
487
|
+
</div>
|
|
488
|
+
{error === null ? null : (
|
|
489
|
+
<p className="audio-error" role="status">
|
|
490
|
+
{error}
|
|
491
|
+
</p>
|
|
492
|
+
)}
|
|
493
|
+
</div>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function AudioClipBar(props: {
|
|
139
498
|
clip: AudioClipRef;
|
|
140
499
|
asset: ShotAsset | undefined;
|
|
141
500
|
fps: Rational;
|
|
142
501
|
ppf: number;
|
|
502
|
+
/** Where this clip's frame base starts on the MASTER timeline: 0 for a bed,
|
|
503
|
+
* the shot's start for a clip its shot owns. `BlockRow`'s prop, for the same
|
|
504
|
+
* reason and with the same meaning. */
|
|
505
|
+
offsetFrames: number;
|
|
506
|
+
/** The shot context's re-zero. DRAWING ONLY: every gesture below is a signed
|
|
507
|
+
* delta, which means the same thing in either base. */
|
|
508
|
+
originFrames: number;
|
|
143
509
|
beginSnap: SnapPress | undefined;
|
|
510
|
+
/** Absent in the master lane. Present for a clip a shot owns, so the home
|
|
511
|
+
* button can say which way it would move. */
|
|
512
|
+
ownedBy?: string;
|
|
513
|
+
/**
|
|
514
|
+
* The owning shot's length, so the bar can mark what will NOT be heard.
|
|
515
|
+
*
|
|
516
|
+
* `masterAudioClips` cuts an owned clip at its shot's end, and the document
|
|
517
|
+
* keeps the authored length so growing the shot brings the tail back. That
|
|
518
|
+
* means the bar and the mixer legitimately disagree, and a bar that drew the
|
|
519
|
+
* authored 60 frames with nothing to say about the last 10 would be the exact
|
|
520
|
+
* silent divergence this lane exists to prevent — you would see audio, hear
|
|
521
|
+
* none, and have nothing to look at.
|
|
522
|
+
*/
|
|
523
|
+
limitFrames?: number;
|
|
524
|
+
/** The bar's inner height in this row (`MASTER_WAVE_H` or `SHOT_WAVE_H`):
|
|
525
|
+
* the waveform, the fade ramp and the envelope all draw into it. */
|
|
526
|
+
waveH: number;
|
|
527
|
+
/**
|
|
528
|
+
* Sound mode's rows: which track row is under this screen y, or undefined
|
|
529
|
+
* off every row. Present only there. A body drag then carries the clip
|
|
530
|
+
* across rows in the SAME scrub as its time move, so one undo puts both back.
|
|
531
|
+
*/
|
|
532
|
+
rowAt?: (clientY: number) => string | undefined;
|
|
533
|
+
onContextMenu?: (e: ReactMouseEvent<HTMLElement>) => void;
|
|
534
|
+
/** A short tag drawn after the name (Sound mode's "Shot 02"). */
|
|
535
|
+
badge?: string;
|
|
536
|
+
/**
|
|
537
|
+
* Sound mode's bar: the mixing controls too (the gain band and its keys, the
|
|
538
|
+
* duck, the gain key, the clip mute). Absent in Rows mode, where the bar
|
|
539
|
+
* moves, trims, fades and changes home, and draws the envelope it plays
|
|
540
|
+
* without editing it.
|
|
541
|
+
*/
|
|
542
|
+
mix?: boolean;
|
|
144
543
|
}) {
|
|
145
|
-
const { clip, asset, fps, ppf, beginSnap } = props;
|
|
544
|
+
const { clip, asset, fps, ppf, offsetFrames, originFrames, beginSnap, ownedBy, limitFrames, waveH, rowAt } = props;
|
|
545
|
+
const mix = props.mix === true;
|
|
146
546
|
const store = useStore();
|
|
547
|
+
const doc = useShotEditor((s) => s.doc);
|
|
548
|
+
const selected = useShotEditor((s) => s.selectedAudio.includes(clip.id));
|
|
147
549
|
const c = audioClipView(clip);
|
|
148
|
-
|
|
550
|
+
// THE FLATTEN, so a bed can duck under narration a SHOT owns. Reading the
|
|
551
|
+
// lane instead would offer only the clips drawn beside this one, which is
|
|
552
|
+
// the commonest case and not the only one.
|
|
553
|
+
const duckCandidates =
|
|
554
|
+
doc === null || !mix
|
|
555
|
+
? []
|
|
556
|
+
: duckRows(
|
|
557
|
+
masterAudioClips(doc)
|
|
558
|
+
.map((m) => m.clip)
|
|
559
|
+
.filter((other) => other.id !== clip.id),
|
|
560
|
+
doc.assets,
|
|
561
|
+
);
|
|
562
|
+
const left = frameToPx(offsetFrames + c.frame - originFrames, ppf);
|
|
149
563
|
const width = Math.max(8, frameToPx(c.durationFrames, ppf));
|
|
150
564
|
|
|
151
565
|
/**
|
|
@@ -161,25 +575,44 @@ function AudioClipBar(props: {
|
|
|
161
575
|
(e: ReactPointerEvent<HTMLElement>): void => {
|
|
162
576
|
e.preventDefault();
|
|
163
577
|
e.stopPropagation();
|
|
578
|
+
if (e.button !== 0) return;
|
|
579
|
+
// THE PICK. A press on a clip already in a multi-pick keeps the pick, so
|
|
580
|
+
// the ⌥ stagger below has every clip to rank; ⇧ toggles.
|
|
581
|
+
const state = store.getState();
|
|
582
|
+
if (e.shiftKey) store.selectAudio([clip.id], "toggle");
|
|
583
|
+
else if (!state.selectedAudio.includes(clip.id)) store.selectAudio([clip.id]);
|
|
584
|
+
const picked = store.getState().selectedAudio;
|
|
585
|
+
// ⌥ on a multi-pick in Sound mode STAGGERS the pick by rank; on one clip
|
|
586
|
+
// it slips the source, as it always has.
|
|
587
|
+
const stagger = mode === "body" && e.altKey && rowAt !== undefined && picked.length > 1;
|
|
164
588
|
// Read AT THE PRESS: a gesture whose meaning changed under a key held
|
|
165
589
|
// mid-drag would slip a clip the author was only moving.
|
|
166
590
|
const effective: AudioDrag = mode === "body" && e.altKey ? "slip" : mode;
|
|
167
591
|
const startX = e.clientX;
|
|
168
592
|
const snap = beginSnap?.({ audioClipId: clip.id }, isSnapSuppressed(e));
|
|
593
|
+
// MASTER frames, because every target on the snapper's list is: a clip a
|
|
594
|
+
// shot owns is numbered from that shot, so its own base has to be added
|
|
595
|
+
// in or the whole gesture snaps to the wrong side of the video.
|
|
169
596
|
const anchor =
|
|
170
|
-
|
|
597
|
+
offsetFrames +
|
|
598
|
+
(effective === "out"
|
|
171
599
|
? c.frame + c.durationFrames
|
|
172
600
|
: effective === "fade-in"
|
|
173
601
|
? c.frame + c.fadeInFrames
|
|
174
602
|
: effective === "fade-out"
|
|
175
603
|
? c.frame + c.durationFrames - c.fadeOutFrames
|
|
176
|
-
: c.frame;
|
|
604
|
+
: c.frame);
|
|
177
605
|
store.beginBlockDrag();
|
|
178
606
|
startDrag(e, {
|
|
179
607
|
onMove(ev) {
|
|
180
608
|
const raw = frameDelta(ev.clientX - startX, ppf);
|
|
609
|
+
if (stagger) {
|
|
610
|
+
store.dragAudioStagger(picked, raw);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
181
613
|
const delta = snap === undefined ? raw : snap(anchor + raw) - anchor;
|
|
182
|
-
|
|
614
|
+
const row = effective === "body" ? rowAt?.(ev.clientY) : undefined;
|
|
615
|
+
store.dragAudio(clip.id, effective, delta, row);
|
|
183
616
|
},
|
|
184
617
|
onEnd: (commit) => store.endBlockDrag(commit),
|
|
185
618
|
});
|
|
@@ -192,7 +625,7 @@ function AudioClipBar(props: {
|
|
|
192
625
|
e.stopPropagation();
|
|
193
626
|
const startY = e.clientY;
|
|
194
627
|
const from = c.gain;
|
|
195
|
-
const height = e.currentTarget.parentElement?.clientHeight ??
|
|
628
|
+
const height = e.currentTarget.parentElement?.clientHeight ?? waveH;
|
|
196
629
|
store.beginBlockDrag();
|
|
197
630
|
startDrag(e, {
|
|
198
631
|
onMove(ev) {
|
|
@@ -205,38 +638,103 @@ function AudioClipBar(props: {
|
|
|
205
638
|
|
|
206
639
|
const fadeInPx = frameToPx(c.fadeInFrames, ppf);
|
|
207
640
|
const fadeOutPx = frameToPx(c.fadeOutFrames, ppf);
|
|
641
|
+
const track = clip.gainTrack;
|
|
642
|
+
/** Frames past the shot's end — audible in the document, silent in the mix. */
|
|
643
|
+
const overrun = limitFrames === undefined ? 0 : Math.max(0, c.frame + c.durationFrames - limitFrames);
|
|
208
644
|
|
|
209
645
|
return (
|
|
210
646
|
<div
|
|
211
|
-
className={`audio-clip${c.muted ? " is-muted" : ""}`}
|
|
647
|
+
className={`audio-clip${c.muted ? " is-muted" : ""}${selected ? " is-selected" : ""}`}
|
|
212
648
|
data-audio-clip={clip.id}
|
|
213
649
|
style={{ left, width }}
|
|
214
650
|
onPointerDown={drag("body")}
|
|
215
|
-
|
|
651
|
+
onContextMenu={props.onContextMenu}
|
|
652
|
+
title={formatCombo(`${asset?.path ?? clip.asset} · drag to move it and its markers, ⌥drag to slip the source, ⌘drag to ignore snapping`)}
|
|
216
653
|
>
|
|
217
|
-
<AudioWaveform clip={c} asset={asset} fps={fps} widthPx={width} />
|
|
654
|
+
<AudioWaveform clip={c} asset={asset} fps={fps} widthPx={width} heightPx={waveH} />
|
|
218
655
|
|
|
219
656
|
{/* The fade ramps, drawn as ONE polyline over the whole bar so the two
|
|
220
657
|
fades and the flat middle cannot disagree about the level between
|
|
221
658
|
them. Inert: the corner handles below take the pointer. */}
|
|
222
659
|
{c.fadeInFrames > 0 || c.fadeOutFrames > 0 ? (
|
|
223
|
-
<svg className="audio-fade-curve" viewBox={`0 0 ${width} ${
|
|
660
|
+
<svg className="audio-fade-curve" viewBox={`0 0 ${width} ${waveH}`} preserveAspectRatio="none" aria-hidden="true">
|
|
224
661
|
<polyline
|
|
225
|
-
points={`0,${
|
|
662
|
+
points={`0,${waveH} ${fadeInPx},0 ${Math.max(fadeInPx, width - fadeOutPx)},0 ${width},${waveH}`}
|
|
226
663
|
fill="none"
|
|
227
664
|
/>
|
|
228
665
|
</svg>
|
|
229
666
|
) : null}
|
|
230
667
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
668
|
+
{/* THE GAIN, one control in two shapes. A clip with no automation gets
|
|
669
|
+
the flat rubber band it always had. One with a `gainTrack` gets the
|
|
670
|
+
ENVELOPE the mixer will actually apply, sampled through the engine so
|
|
671
|
+
the line on screen and the samples in the file are the same function —
|
|
672
|
+
and the band then edits the key under the playhead rather than a
|
|
673
|
+
static number the engine would ignore. */}
|
|
674
|
+
{!mix ? (
|
|
675
|
+
track === undefined ? null : (
|
|
676
|
+
<svg
|
|
677
|
+
className="audio-gain-curve"
|
|
678
|
+
data-testid={`audio-envelope-${clip.id}`}
|
|
679
|
+
viewBox={`0 0 ${width} ${waveH}`}
|
|
680
|
+
preserveAspectRatio="none"
|
|
681
|
+
aria-hidden="true"
|
|
682
|
+
>
|
|
683
|
+
<polyline points={envelopePoints(track, c, fps, width, ppf, waveH)} fill="none" />
|
|
684
|
+
</svg>
|
|
685
|
+
)
|
|
686
|
+
) : track === undefined ? (
|
|
687
|
+
<span
|
|
688
|
+
className="audio-gain-band"
|
|
689
|
+
data-handle="audio-gain"
|
|
690
|
+
style={{ bottom: `${(c.gain / MAX_AUDIO_GAIN) * 100}%` }}
|
|
691
|
+
onPointerDown={dragGain}
|
|
692
|
+
title={`Gain ${c.gain.toFixed(2)}: drag up or down`}
|
|
693
|
+
/>
|
|
694
|
+
) : (
|
|
695
|
+
<>
|
|
696
|
+
<svg
|
|
697
|
+
className="audio-gain-curve"
|
|
698
|
+
data-testid={`audio-envelope-${clip.id}`}
|
|
699
|
+
viewBox={`0 0 ${width} ${waveH}`}
|
|
700
|
+
preserveAspectRatio="none"
|
|
701
|
+
aria-hidden="true"
|
|
702
|
+
>
|
|
703
|
+
<polyline points={envelopePoints(track, c, fps, width, ppf, waveH)} fill="none" />
|
|
704
|
+
</svg>
|
|
705
|
+
<span className="audio-gain-band is-automated" data-handle="audio-gain" onPointerDown={dragGain} />
|
|
706
|
+
{track.keyframes.map((k) => (
|
|
707
|
+
<span
|
|
708
|
+
key={k.id}
|
|
709
|
+
className="audio-gain-key"
|
|
710
|
+
data-audio-gain-key={k.id}
|
|
711
|
+
style={{
|
|
712
|
+
left: frameToPx(k.frame, ppf),
|
|
713
|
+
bottom: `${(gainOf(k.value) / MAX_AUDIO_GAIN) * 100}%`,
|
|
714
|
+
}}
|
|
715
|
+
title={`Gain ${gainOf(k.value).toFixed(2)} at +${k.frame}f`}
|
|
716
|
+
/>
|
|
717
|
+
))}
|
|
718
|
+
</>
|
|
719
|
+
)}
|
|
238
720
|
|
|
239
|
-
|
|
721
|
+
{/* WHAT WILL NOT BE HEARD. Hatched rather than cut off, because the
|
|
722
|
+
document still holds these frames: lengthening the shot brings them
|
|
723
|
+
back, and a bar that simply ended at the boundary would give the
|
|
724
|
+
author nothing to drag. */}
|
|
725
|
+
{overrun > 0 ? (
|
|
726
|
+
<span
|
|
727
|
+
className="audio-overrun"
|
|
728
|
+
data-testid={`audio-overrun-${clip.id}`}
|
|
729
|
+
style={{ left: frameToPx(c.durationFrames - overrun, ppf) }}
|
|
730
|
+
title={`${overrun}f past the end of this shot: kept, but not heard. Lengthen the shot or trim the clip.`}
|
|
731
|
+
/>
|
|
732
|
+
) : null}
|
|
733
|
+
|
|
734
|
+
<span className="audio-clip-name">
|
|
735
|
+
{clipLabel(clip, asset)}
|
|
736
|
+
{props.badge === undefined ? null : <span className="audio-clip-badge">{props.badge}</span>}
|
|
737
|
+
</span>
|
|
240
738
|
|
|
241
739
|
<span
|
|
242
740
|
className="audio-handle audio-handle-in"
|
|
@@ -266,16 +764,53 @@ function AudioClipBar(props: {
|
|
|
266
764
|
/>
|
|
267
765
|
|
|
268
766
|
<span className="audio-clip-buttons">
|
|
767
|
+
{mix ? <AudioFxButton clip={clip} asset={asset} store={store} /> : null}
|
|
768
|
+
{mix ? <DuckButton clip={clip} candidates={duckCandidates} store={store} /> : null}
|
|
769
|
+
{/* THE HOME SWITCH. One bit, so a target rather than a drop zone: a
|
|
770
|
+
cross-lane drag would have to hit-test another lane mid-capture to
|
|
771
|
+
carry the same information. Attaching picks the shot the clip
|
|
772
|
+
STARTS in, which is the only shot it can be said to belong to. */}
|
|
269
773
|
<button
|
|
270
774
|
type="button"
|
|
271
775
|
className="audio-clip-btn"
|
|
272
|
-
aria-label={
|
|
273
|
-
|
|
776
|
+
aria-label={ownedBy === undefined ? "Attach to the shot under it" : "Move to the master bed"}
|
|
777
|
+
title={
|
|
778
|
+
ownedBy === undefined
|
|
779
|
+
? "Attach to the shot it starts in: it will move, duplicate and end with that shot"
|
|
780
|
+
: "Move to the master bed: it will span the cuts and stop following this shot"
|
|
781
|
+
}
|
|
274
782
|
onPointerDown={(e) => e.stopPropagation()}
|
|
275
|
-
onClick={() => store.
|
|
783
|
+
onClick={() => store.moveAudioHome(clip.id)}
|
|
276
784
|
>
|
|
277
|
-
{
|
|
785
|
+
{ownedBy === undefined ? "⇥" : "⇤"}
|
|
278
786
|
</button>
|
|
787
|
+
{/* THE AUTOMATION KEY, at the playhead. A toggle, because the same
|
|
788
|
+
button is both states and a second control for "remove" would be a
|
|
789
|
+
control that is disabled almost always. */}
|
|
790
|
+
{mix ? (
|
|
791
|
+
<button
|
|
792
|
+
type="button"
|
|
793
|
+
className="audio-clip-btn"
|
|
794
|
+
aria-label="Keyframe the gain at the playhead"
|
|
795
|
+
title="Keyframe the gain at the playhead: the first key changes nothing, it just makes the level automatable"
|
|
796
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
797
|
+
onClick={() => store.toggleAudioGainKey(clip.id)}
|
|
798
|
+
>
|
|
799
|
+
◆
|
|
800
|
+
</button>
|
|
801
|
+
) : null}
|
|
802
|
+
{mix ? (
|
|
803
|
+
<button
|
|
804
|
+
type="button"
|
|
805
|
+
className="audio-clip-btn"
|
|
806
|
+
aria-label={c.muted ? "Unmute clip" : "Mute clip"}
|
|
807
|
+
aria-pressed={c.muted}
|
|
808
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
809
|
+
onClick={() => store.toggleAudioMute(clip.id)}
|
|
810
|
+
>
|
|
811
|
+
{c.muted ? <IconVolumeOff /> : <IconVolume />}
|
|
812
|
+
</button>
|
|
813
|
+
) : null}
|
|
279
814
|
<button
|
|
280
815
|
type="button"
|
|
281
816
|
className="audio-clip-btn"
|
|
@@ -298,13 +833,23 @@ function AudioClipBar(props: {
|
|
|
298
833
|
* decimation out of it — nothing here refetches. (Not the asset id: that is a
|
|
299
834
|
* filename slug this lane mints per document, so it repeats across shots.)
|
|
300
835
|
*/
|
|
301
|
-
function AudioWaveform(props: {
|
|
836
|
+
export function AudioWaveform(props: {
|
|
302
837
|
clip: { offsetFrames: number; durationFrames: number };
|
|
303
|
-
|
|
838
|
+
/**
|
|
839
|
+
* The asset, by IDENTITY. The fetch below keys its effect on this prop, so a
|
|
840
|
+
* caller must hand it the same object across renders — `{ path }` built in
|
|
841
|
+
* the render would refetch and re-`setData` forever. The row models carry the
|
|
842
|
+
* resolved `ShotAsset` for exactly that reason.
|
|
843
|
+
*/
|
|
844
|
+
asset: { path: string } | undefined;
|
|
304
845
|
fps: Rational;
|
|
305
846
|
widthPx: number;
|
|
847
|
+
/** Canvas px: the inner height of the box it is drawn into, which differs
|
|
848
|
+
* per row (the master lane, a shot's audio row, a video row's sound strip). */
|
|
849
|
+
heightPx: number;
|
|
306
850
|
}) {
|
|
307
|
-
const { clip, asset, fps, widthPx } = props;
|
|
851
|
+
const { clip, asset, fps, widthPx, heightPx } = props;
|
|
852
|
+
const { resolved: resolvedTheme } = useStudioTheme();
|
|
308
853
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
309
854
|
const [data, setData] = useState<PeakData | null>(null);
|
|
310
855
|
|
|
@@ -326,15 +871,59 @@ function AudioWaveform(props: {
|
|
|
326
871
|
useEffect(() => {
|
|
327
872
|
const canvas = canvasRef.current;
|
|
328
873
|
if (canvas === null || data === null || data.peaks.length === 0) return;
|
|
874
|
+
const waveColor = getComputedStyle(canvas).getPropertyValue("--audio-waveform").trim();
|
|
329
875
|
drawClipWaveform(canvas, {
|
|
330
876
|
data,
|
|
331
877
|
fromSeconds: framesToSeconds(clip.offsetFrames, fps),
|
|
332
878
|
toSeconds: framesToSeconds(clip.offsetFrames + clip.durationFrames, fps),
|
|
333
879
|
widthPx,
|
|
334
|
-
heightPx
|
|
335
|
-
color:
|
|
880
|
+
heightPx,
|
|
881
|
+
color: waveColor || WAVE_COLOR_FALLBACK,
|
|
336
882
|
});
|
|
337
|
-
}, [data, widthPx, fps, clip.offsetFrames, clip.durationFrames]);
|
|
883
|
+
}, [data, widthPx, heightPx, fps, clip.offsetFrames, clip.durationFrames, resolvedTheme]);
|
|
338
884
|
|
|
339
885
|
return <canvas className="audio-wave" ref={canvasRef} aria-hidden="true" />;
|
|
340
886
|
}
|
|
887
|
+
|
|
888
|
+
/** A track value as a gain. Guarded rather than cast: the track shape allows a
|
|
889
|
+
* vector, and a validator problem must not become a NaN in a `points` list
|
|
890
|
+
* that then removes the whole polyline. */
|
|
891
|
+
function gainOf(value: unknown): number {
|
|
892
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 1;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* The envelope polyline, in the bar's own pixel space.
|
|
897
|
+
*
|
|
898
|
+
* SAMPLED THROUGH THE ENGINE, one point per drawn pixel rather than one per
|
|
899
|
+
* keyframe, because the segments between keys are eased (and may be springs)
|
|
900
|
+
* and a straight line between two keys would draw a curve the mixer is not
|
|
901
|
+
* going to apply. `audioGainSampler` is the same function the preview and both
|
|
902
|
+
* export mixes take their values from, so the line and the samples cannot
|
|
903
|
+
* disagree — which is the whole reason this is not a local lerp.
|
|
904
|
+
*
|
|
905
|
+
* The FADES are deliberately not folded in. They already have their own
|
|
906
|
+
* polyline over the bar, and multiplying the two would draw one line the author
|
|
907
|
+
* could not take either control back off.
|
|
908
|
+
*/
|
|
909
|
+
function envelopePoints(
|
|
910
|
+
track: ShotTrack,
|
|
911
|
+
clip: { durationFrames: number },
|
|
912
|
+
fps: Rational,
|
|
913
|
+
widthPx: number,
|
|
914
|
+
ppf: number,
|
|
915
|
+
heightPx: number,
|
|
916
|
+
): string {
|
|
917
|
+
const sample = audioGainSampler(track, 0, fps);
|
|
918
|
+
if (sample === undefined) return "";
|
|
919
|
+
// One sample per pixel, capped: a bar zoomed to thousands of pixels does not
|
|
920
|
+
// need thousands of points to read as a curve, and this runs per render.
|
|
921
|
+
const steps = Math.max(2, Math.min(240, Math.round(widthPx)));
|
|
922
|
+
const out: string[] = [];
|
|
923
|
+
for (let i = 0; i <= steps; i++) {
|
|
924
|
+
const frame = (i / steps) * clip.durationFrames;
|
|
925
|
+
const y = heightPx * (1 - gainOf(sample(frame) ?? 1) / MAX_AUDIO_GAIN);
|
|
926
|
+
out.push(`${frameToPx(frame, ppf)},${y}`);
|
|
927
|
+
}
|
|
928
|
+
return out.join(" ");
|
|
929
|
+
}
|