@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,19 +1,24 @@
|
|
|
1
|
-
// A layer's LIFESPAN bar: one rounded bar per layer, at its master position
|
|
1
|
+
// A layer's LIFESPAN bar: one rounded bar per layer, at its master position —
|
|
2
|
+
// and, for a `kind: "video"` layer, that bar is a CLIP: a filmstrip over the
|
|
3
|
+
// recording's own waveform.
|
|
2
4
|
//
|
|
3
|
-
// ── THE
|
|
5
|
+
// ── THE LIFESPAN IS THE LAYER ───────────────────────────────────────────
|
|
4
6
|
//
|
|
5
|
-
// The disclosure triangle expands this row into one
|
|
6
|
-
//
|
|
7
|
-
// diamonds, the Figma/Lottie read. The channel rows are views, never storage:
|
|
8
|
-
// every gesture on one drags the whole BLOCK, in one undo entry, and the
|
|
9
|
-
// preset's name lives in the span's tooltip and the Animate tab. The block is
|
|
10
|
-
// still where this timeline's DOCUMENT stops.
|
|
7
|
+
// The disclosure triangle expands this row into one row per tracked property
|
|
8
|
+
// (ChannelRow.tsx), with keyframe diamonds, the Figma/Lottie read.
|
|
11
9
|
//
|
|
12
10
|
// ── THREE GESTURES, AND WHY THE THIRD IS NOT OPTIONAL ───────────────────
|
|
13
11
|
//
|
|
14
|
-
// BODY drags the bar AND every
|
|
15
|
-
// IN / OUT move one end and touch no
|
|
16
|
-
// SLIP (⌥ held) moves every
|
|
12
|
+
// BODY drags the bar AND every keyframe on the layer with it.
|
|
13
|
+
// IN / OUT move one end and touch no keyframe.
|
|
14
|
+
// SLIP (⌥ held) moves every keyframe and leaves the bar exactly where it is.
|
|
15
|
+
//
|
|
16
|
+
// ⇧ MEANS NOTHING ON THESE THREE, and that is a decision rather than an
|
|
17
|
+
// omission. ⌥ has been the slip on every row kind since this dock shipped, so
|
|
18
|
+
// a chord that merely adds ⇧ is a slip an author already had in their hands:
|
|
19
|
+
// reading it as some other gesture would retime an ordinary layer from a key
|
|
20
|
+
// combination whose meaning nobody announced changing. ⇧⌥ is therefore the
|
|
21
|
+
// slip too, everywhere except a clip, where the chord is new (below).
|
|
17
22
|
//
|
|
18
23
|
// Body-versus-edge is After Effects verbatim, and no tool makes it a preference
|
|
19
24
|
// — so there is no setting here either. SLIP is the one every two-gesture
|
|
@@ -26,140 +31,818 @@
|
|
|
26
31
|
// restores the document by reference — every capture re-derives from the base,
|
|
27
32
|
// which is also why the delta below is measured from the press and never
|
|
28
33
|
// accumulated.
|
|
34
|
+
//
|
|
35
|
+
// ── AND WHAT A VIDEO BAR ADDS ───────────────────────────────────────────
|
|
36
|
+
//
|
|
37
|
+
// The same three words mean the clip ops for a video layer (`writeLifespan`
|
|
38
|
+
// dispatches; this file only names the gesture), plus two the other kinds have
|
|
39
|
+
// no use for:
|
|
40
|
+
//
|
|
41
|
+
// ⇧⌥ BODY slides THIS bar alone, out of a selection that would otherwise
|
|
42
|
+
// travel with it. A CLIP GESTURE ONLY: on every other kind the
|
|
43
|
+
// chord stays the slip it has always been.
|
|
44
|
+
// ROLL the handle on the waveform half of an abutting out edge: it moves
|
|
45
|
+
// the SEAM, so this clip's tail and the next clip's head trade
|
|
46
|
+
// frames and nothing around the pair shifts.
|
|
47
|
+
//
|
|
48
|
+
// On every kind, the body drag carries the whole selection when the pressed bar
|
|
49
|
+
// is in it, which is what selecting three bars in order to move them together
|
|
50
|
+
// means. ⇧-click adds a bar to the selection or takes it out.
|
|
51
|
+
//
|
|
52
|
+
// ── AND WHY THE BAR IS DOM, LIKE EVERY OTHER BAR IN THIS DOCK ───────────
|
|
53
|
+
//
|
|
54
|
+
// Two canvases hang inside it — `Filmstrip` for the picture and `AudioWaveform`
|
|
55
|
+
// for the sound, both pixel data with no DOM spelling — and everything else,
|
|
56
|
+
// the bar, the edge, the held-frame hatch, the handles and the label, stays
|
|
57
|
+
// elements, because that is what the rest of the dock is and what its
|
|
58
|
+
// hit-testing, its hover and its tests all assume.
|
|
29
59
|
import { frameToPx, startDrag } from "@dailies-studio/timeline";
|
|
30
|
-
import
|
|
60
|
+
import {
|
|
61
|
+
IconChevron,
|
|
62
|
+
IconChevronDown,
|
|
63
|
+
IconEye,
|
|
64
|
+
IconEyeOff,
|
|
65
|
+
IconLock,
|
|
66
|
+
IconUnlock,
|
|
67
|
+
IconVolume,
|
|
68
|
+
IconVolumeOff,
|
|
69
|
+
} from "@dailies-studio/ui";
|
|
70
|
+
import type { CSSProperties, PointerEvent as ReactPointerEvent } from "react";
|
|
71
|
+
import type { Rational } from "@dailies-studio/engine";
|
|
72
|
+
import { firstKeyframeFrame, isDefId, legalRollRange, legalTrimRange, setVideoSound } from "@dailies-studio/shot/doc";
|
|
73
|
+
import type { Layer3D, VideoLayer3D } from "@dailies-studio/shot/doc";
|
|
74
|
+
import { MASK_ICON, iconFor, isComponentGlyph } from "../../icons.js";
|
|
75
|
+
import { MaskBracket } from "../maskMarks.js";
|
|
76
|
+
import type { MaskMarks } from "../maskMarks.js";
|
|
31
77
|
import { useShotEditor, useStore } from "../../state/context.js";
|
|
32
|
-
import
|
|
33
|
-
import {
|
|
78
|
+
import { AudioWaveform } from "./AudioLane.js";
|
|
79
|
+
import { Filmstrip } from "./Filmstrip.js";
|
|
80
|
+
import type { HeldSpan, LifespanDrag, MasterRow, VideoRow } from "./model.js";
|
|
81
|
+
import {
|
|
82
|
+
CLIP_NAME_GAP_PX,
|
|
83
|
+
VIDEO_EDGE_H,
|
|
84
|
+
VIDEO_PICTURE_H,
|
|
85
|
+
VIDEO_ROW_H,
|
|
86
|
+
VIDEO_WAVE_H,
|
|
87
|
+
clipNamePlace,
|
|
88
|
+
clipRowOf,
|
|
89
|
+
frameDelta,
|
|
90
|
+
lifespanOf,
|
|
91
|
+
trimHandlePx,
|
|
92
|
+
} from "./model.js";
|
|
93
|
+
import { StaggerChip, StaggerGhost, setStaggerPreview, useStaggerGhost, useStaggerReadout } from "./staggerPreview.js";
|
|
34
94
|
import type { SnapPress } from "./snap.js";
|
|
35
95
|
import { isSnapSuppressed } from "./snap.js";
|
|
96
|
+
import { useLive } from "./useLive.js";
|
|
97
|
+
import { formatCombo } from "../../state/shortcuts.js";
|
|
98
|
+
import { PeerDots } from "../header/PresenceStack.js";
|
|
36
99
|
|
|
37
100
|
/** ⌥ turns a body drag into a slip. Alt is the modifier both AE and Premiere
|
|
38
101
|
* put on the timeline's third gesture, and it does not collide with the snap
|
|
39
102
|
* suppressor (⌘/ctrl) — so a slip can be snapped or not, like every other
|
|
40
|
-
* drag.
|
|
103
|
+
* drag.
|
|
104
|
+
*
|
|
105
|
+
* ⇧ IS NOT READ HERE. Requiring `!shiftKey` would have silently taken ⇧⌥ away
|
|
106
|
+
* from every row kind that already had it as a slip; the one row where the
|
|
107
|
+
* chord means something else is the clip, and `gestureOf` is where that fork
|
|
108
|
+
* lives so it can be read against the row's kind in one place. */
|
|
41
109
|
function isSlip(event: { altKey: boolean }): boolean {
|
|
42
110
|
return event.altKey;
|
|
43
111
|
}
|
|
44
112
|
|
|
113
|
+
/** ⇧⌥ is the slide: the same body move, on this bar alone. A CLIP gesture, and
|
|
114
|
+
* `gestureOf` is the only caller because it is the only reader that knows
|
|
115
|
+
* whether this row is one. */
|
|
116
|
+
function isSolo(event: { altKey: boolean; shiftKey: boolean }): boolean {
|
|
117
|
+
return event.altKey && event.shiftKey;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* THE HELD-FRAME HATCH.
|
|
122
|
+
*
|
|
123
|
+
* A frozen tail looks EXACTLY like footage on a filmstrip — the last picture,
|
|
124
|
+
* repeated, is still a picture — so an author who cannot see it ships a shot
|
|
125
|
+
* that stops moving three seconds before it ends. Diagonal hatching over both
|
|
126
|
+
* strips is the mark, because it survives whatever the tile under it happens to
|
|
127
|
+
* be: a light frame, a dark frame, or no strip at all.
|
|
128
|
+
*
|
|
129
|
+
* Built from `--timeline-ink` rather than a fixed colour, so it reads in both
|
|
130
|
+
* appearances against both the picture and the waveform.
|
|
131
|
+
*/
|
|
132
|
+
const HELD_HATCH =
|
|
133
|
+
"repeating-linear-gradient(135deg," +
|
|
134
|
+
" color-mix(in srgb, var(--timeline-ink) 34%, transparent) 0 1.5px," +
|
|
135
|
+
" color-mix(in srgb, var(--timeline-ink) 6%, transparent) 1.5px 5px)";
|
|
136
|
+
|
|
45
137
|
export function LifespanRow({
|
|
46
138
|
row,
|
|
139
|
+
layer,
|
|
47
140
|
sceneId,
|
|
48
141
|
sceneStart,
|
|
142
|
+
originFrames = 0,
|
|
49
143
|
ppf,
|
|
144
|
+
fps,
|
|
50
145
|
labelWidth,
|
|
146
|
+
laneW,
|
|
51
147
|
expanded,
|
|
148
|
+
revealed = false,
|
|
52
149
|
onToggle,
|
|
53
150
|
beginSnap,
|
|
151
|
+
mask,
|
|
54
152
|
}: {
|
|
55
153
|
row: MasterRow;
|
|
154
|
+
/**
|
|
155
|
+
* The row's own layer, for its KIND GLYPH. A dock row and an outliner row
|
|
156
|
+
* naming the same element drew two different things: a name here, a name and
|
|
157
|
+
* its icon there. The glyph is the taxonomy, so it belongs to both.
|
|
158
|
+
*
|
|
159
|
+
* Optional because `MasterRow` is the dock's projection and carries no layer:
|
|
160
|
+
* the caller resolves it out of the container the row lives in, and a row
|
|
161
|
+
* whose layer has just been deleted under a render draws its name alone
|
|
162
|
+
* rather than throwing.
|
|
163
|
+
*/
|
|
164
|
+
layer?: Layer3D | undefined;
|
|
56
165
|
sceneId: string;
|
|
57
|
-
/** The scene's master start. Drawing
|
|
58
|
-
* DELTA, which is the same number in both clocks. */
|
|
166
|
+
/** The scene's master start. Drawing AND the snap conversion below: the store
|
|
167
|
+
* takes a scene-local DELTA, which is the same number in both clocks. */
|
|
59
168
|
sceneStart: number;
|
|
169
|
+
/** The shot context's re-zero. DRAWING ONLY — never the snap conversion, or a
|
|
170
|
+
* drag inside a shot would snap `start` frames from where it should. */
|
|
171
|
+
originFrames?: number;
|
|
60
172
|
ppf: number;
|
|
173
|
+
/** The container's rate. A video row turns source FRAMES into the seconds its
|
|
174
|
+
* strip and its peaks are cut on; every other row ignores it. */
|
|
175
|
+
fps: Rational;
|
|
61
176
|
labelWidth: number;
|
|
177
|
+
/** The track's drawn width, so a name too long for its clip can go beside it
|
|
178
|
+
* on the side that has room (`clipNamePlace`). Absent: the name stays in. */
|
|
179
|
+
laneW?: number;
|
|
62
180
|
expanded: boolean;
|
|
181
|
+
/** A pick elsewhere just revealed this row: flash it once (`is-revealed`). */
|
|
182
|
+
revealed?: boolean;
|
|
63
183
|
onToggle(): void;
|
|
64
184
|
beginSnap?: SnapPress;
|
|
185
|
+
/** The row's mask marks (gap 7): the glyph, the bracket and the bar's mode
|
|
186
|
+
* label, read by `maskMarksOf` like the outline's. */
|
|
187
|
+
mask?: MaskMarks | undefined;
|
|
65
188
|
}) {
|
|
66
189
|
const store = useStore();
|
|
190
|
+
// MEMBERSHIP, not the array. Subscribing to `s.selection` woke every lifespan
|
|
191
|
+
// row on screen for every selection change, when the only rows whose output
|
|
192
|
+
// moved are the two whose membership flipped. The whole selection is read at
|
|
193
|
+
// the PRESS instead, off the store, which is the only moment the drag handler
|
|
194
|
+
// below wants it.
|
|
67
195
|
const selected = useShotEditor((s) => s.selection.includes(row.layerId));
|
|
196
|
+
const ghostFrom = useStaggerGhost(row.layerId);
|
|
197
|
+
const readout = useStaggerReadout(row.layerId);
|
|
68
198
|
const { lifespan } = row;
|
|
199
|
+
// THE ONE PREDICATE, and `expandedLayerHeight` in TimelineDock reads the same
|
|
200
|
+
// call: see `clipRowOf` for why the height and the render cannot be allowed
|
|
201
|
+
// two answers. Read once here, for the class, the box and the body alike.
|
|
202
|
+
const video = clipRowOf(layer, row);
|
|
69
203
|
|
|
70
204
|
/** Selecting a row in another scene NAVIGATES first: the stage shows one
|
|
71
205
|
* scene, so handles on a layer three scenes away would be handles on
|
|
72
206
|
* nothing — and `select` orders within the current scene, so the selection
|
|
73
|
-
* would resolve to empty.
|
|
74
|
-
|
|
207
|
+
* would resolve to empty.
|
|
208
|
+
*
|
|
209
|
+
* ⇧ (without ⌥, which is the slide) toggles this row in a selection of the
|
|
210
|
+
* same container, as in every design tool. Pressing a row that is already
|
|
211
|
+
* selected keeps the selection, so the group a drag carried is still the
|
|
212
|
+
* selection after it lands. */
|
|
213
|
+
const pick = (e?: { shiftKey: boolean; altKey: boolean }): void => {
|
|
214
|
+
const s = store.getState();
|
|
215
|
+
const here = s.editingDef === null ? sceneId === s.sceneId : sceneId === s.editingDef;
|
|
216
|
+
if (here && e?.shiftKey === true && !e.altKey) {
|
|
217
|
+
store.select(
|
|
218
|
+
s.selection.includes(row.layerId)
|
|
219
|
+
? s.selection.filter((id) => id !== row.layerId)
|
|
220
|
+
: [...s.selection, row.layerId],
|
|
221
|
+
);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
75
224
|
store.focusScene(sceneId);
|
|
76
|
-
store.select([row.layerId]);
|
|
225
|
+
if (!(here && s.selection.includes(row.layerId))) store.select([row.layerId]);
|
|
77
226
|
};
|
|
78
227
|
|
|
79
228
|
const drag =
|
|
80
|
-
(mode:
|
|
229
|
+
(mode: "body" | "in" | "out" | "roll") =>
|
|
81
230
|
(e: ReactPointerEvent<HTMLElement>): void => {
|
|
82
231
|
e.preventDefault();
|
|
83
232
|
e.stopPropagation();
|
|
84
|
-
|
|
233
|
+
// WHAT IS MOVING is decided BEFORE the selection changes. Pressing a bar
|
|
234
|
+
// that is already selected moves the group and keeps it selected;
|
|
235
|
+
// pressing any other bar selects it and moves it alone, which is what
|
|
236
|
+
// every NLE does.
|
|
237
|
+
//
|
|
238
|
+
// NOT INSIDE A DEFINITION: a group move goes through `shiftLayer`, which
|
|
239
|
+
// only finds layers in `doc.scenesById`, so there the plain "body" moves
|
|
240
|
+
// the pressed bar. This list is also the snap exclusion below, so it must
|
|
241
|
+
// name exactly the bars that move.
|
|
242
|
+
//
|
|
243
|
+
// A ROLL IS THE OTHER TWO-BAR GESTURE, and it is the exclusion half of
|
|
244
|
+
// this list rather than the group half. A seam IS the partner's `in` and
|
|
245
|
+
// this bar's `out` at one frame, so a roll that named only its own row
|
|
246
|
+
// offered the snapper a target sitting exactly on the frame being
|
|
247
|
+
// dragged: the pull won every comparison and the drag stuck where it
|
|
248
|
+
// already was. `gestureOf` returns the roll before it ever reads this
|
|
249
|
+
// list, so widening it here cannot turn a roll into a group move.
|
|
250
|
+
//
|
|
251
|
+
// The partner's OTHER edge and its keys go with it, which is the same
|
|
252
|
+
// ceiling the pressed row has always paid: the exclusion's grain is a
|
|
253
|
+
// layer, not a frame.
|
|
254
|
+
const carried =
|
|
255
|
+
mode === "roll"
|
|
256
|
+
? video?.rollPartner == null
|
|
257
|
+
? [row.layerId]
|
|
258
|
+
: [row.layerId, video.rollPartner]
|
|
259
|
+
: mode === "body" && selected && !isDefId(sceneId)
|
|
260
|
+
? store.getState().selection
|
|
261
|
+
: [row.layerId];
|
|
262
|
+
pick(e);
|
|
85
263
|
const startX = e.clientX;
|
|
86
264
|
// Read at the PRESS: the pose the drag is measured from, and the two
|
|
87
265
|
// modifiers. A gesture whose meaning changed under a held key mid-drag
|
|
88
266
|
// would retime an animation the author was only moving.
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
267
|
+
const effective = gestureOf(mode, e, video ?? null, carried);
|
|
268
|
+
if (effective === null) return;
|
|
269
|
+
// ── WHERE THIS EDGE MAY LEGALLY LAND ──────────────────────────────
|
|
270
|
+
//
|
|
271
|
+
// A clip's edge is bounded by its FOOTAGE as well as by its scene, and
|
|
272
|
+
// `trimLayer` refuses rather than clamps when the constraints do not
|
|
273
|
+
// intersect. The range is read ONCE, at the press, and spends itself
|
|
274
|
+
// twice: it bounds the pointer's own travel below, and it goes to the
|
|
275
|
+
// snapper, which drops every target outside it at collection time. That
|
|
276
|
+
// second half is why an edge cannot stick to a guide the op would then
|
|
277
|
+
// clamp away from.
|
|
278
|
+
//
|
|
279
|
+
// A SEAM IS BOUNDED THE SAME WAY, through `legalRollRange`, and it is
|
|
280
|
+
// wired for the same half: `rollLayers` already clamps the frame it
|
|
281
|
+
// writes, so without this the document was right and the GUIDE was free
|
|
282
|
+
// to point at a frame the op would then clamp away from, which reads as
|
|
283
|
+
// a broken drag even when what lands is legal.
|
|
284
|
+
const doc = store.getState().doc;
|
|
285
|
+
const legal =
|
|
286
|
+
doc === null || video === undefined
|
|
287
|
+
? null
|
|
288
|
+
: mode === "in" || mode === "out"
|
|
289
|
+
? legalTrimRange(doc, sceneId, row.layerId, mode)
|
|
290
|
+
: mode === "roll" && video.rollPartner !== null
|
|
291
|
+
? legalRollRange(doc, sceneId, row.layerId, video.rollPartner)
|
|
292
|
+
: null;
|
|
293
|
+
const clampLegal = (sceneFrame: number): number =>
|
|
294
|
+
legal === null || legal.empty === true
|
|
295
|
+
? sceneFrame
|
|
296
|
+
: Math.max(legal.min, Math.min(legal.max, sceneFrame));
|
|
297
|
+
// EVERY BAR THIS GESTURE CARRIES is excluded from the target list, not
|
|
298
|
+
// just the pressed one: a bar sits exactly on its own edge, so any one of
|
|
299
|
+
// the others would win every comparison and pin the drag at zero travel.
|
|
300
|
+
//
|
|
301
|
+
// A STAGGER DOES NOT SNAP (Paper page 23): each bar moves a different
|
|
302
|
+
// distance, so a guide under the pointer would describe none of them.
|
|
303
|
+
const stagger = typeof effective !== "string" && "stagger" in effective;
|
|
304
|
+
const snap = stagger
|
|
305
|
+
? undefined
|
|
306
|
+
: beginSnap?.(
|
|
307
|
+
carried.length > 1
|
|
308
|
+
? { sceneId, layerId: row.layerId, layerIds: carried }
|
|
309
|
+
: { sceneId, layerId: row.layerId },
|
|
310
|
+
isSnapSuppressed(e),
|
|
311
|
+
// MASTER frames, because that is the base the targets are collected
|
|
312
|
+
// in. An empty range survives the shift as an empty one, so
|
|
313
|
+
// "nowhere legal to go" still filters everything out.
|
|
314
|
+
legal === null ? undefined : { min: legal.min + sceneStart, max: legal.max + sceneStart },
|
|
315
|
+
);
|
|
316
|
+
// WHAT THE POINTER IS CARRYING is what snaps, and for a slip that is
|
|
317
|
+
// never the bar: the bar does not move. A non-video slip carries its
|
|
318
|
+
// earliest keyframe; a video slip carries the source's own t=0 crossing, so
|
|
319
|
+
// "put the start of the take on this beat" is the alignment the pull
|
|
320
|
+
// offers. Everything else carries the edge it is dragging.
|
|
95
321
|
const anchor =
|
|
96
|
-
|
|
322
|
+
mode === "out" || mode === "roll"
|
|
97
323
|
? lifespan.out
|
|
98
|
-
: effective
|
|
99
|
-
?
|
|
100
|
-
:
|
|
324
|
+
: effective !== "slip"
|
|
325
|
+
? lifespan.in
|
|
326
|
+
: video === undefined
|
|
327
|
+
? firstKeyframeFrame(layer?.tracks)
|
|
328
|
+
: video.offsetFrames;
|
|
329
|
+
// The ghosts: each moving bar's `in` at the press, in the lane's own
|
|
330
|
+
// drawing clock, which is the clock every row draws its bar in. A stagger
|
|
331
|
+
// never runs inside a definition (`carried` is one bar there), so the
|
|
332
|
+
// container is a scene.
|
|
333
|
+
const scene = doc?.scenesById[sceneId];
|
|
334
|
+
const from: Record<string, number> = {};
|
|
335
|
+
if (stagger && scene !== undefined)
|
|
336
|
+
for (const id of carried) {
|
|
337
|
+
const layer = scene.layersById[id];
|
|
338
|
+
if (layer !== undefined) from[id] = sceneStart - originFrames + lifespanOf(layer, scene.durationFrames).in;
|
|
339
|
+
}
|
|
340
|
+
if (stagger) setStaggerPreview({ from, grabbed: row.layerId, step: 0 });
|
|
101
341
|
store.beginBlockDrag();
|
|
102
342
|
startDrag(e, {
|
|
103
343
|
onMove(ev) {
|
|
104
344
|
const raw = frameDelta(ev.clientX - startX, ppf);
|
|
345
|
+
const want = clampLegal(anchor + raw);
|
|
105
346
|
// A signed delta into master space and straight back, so the store
|
|
106
347
|
// still only ever sees a scene-local number.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
348
|
+
//
|
|
349
|
+
// NO SECOND CLAMP around the snapper: every target it can answer with
|
|
350
|
+
// is inside the range now, and its other answer is the rounded
|
|
351
|
+
// candidate, which is bounded on the line above and cannot round out
|
|
352
|
+
// of a range whose ends are integers.
|
|
353
|
+
const landed = snap === undefined || !Number.isFinite(anchor) ? want : snap(want + sceneStart) - sceneStart;
|
|
354
|
+
store.dragLifespan(sceneId, row.layerId, effective, landed - anchor);
|
|
355
|
+
if (stagger) setStaggerPreview({ from, grabbed: row.layerId, step: Math.round(landed - anchor) });
|
|
356
|
+
},
|
|
357
|
+
onEnd: (commit) => {
|
|
358
|
+
if (stagger) setStaggerPreview(null);
|
|
359
|
+
store.endBlockDrag(commit);
|
|
112
360
|
},
|
|
113
|
-
onEnd: (commit) => store.endBlockDrag(commit),
|
|
114
361
|
});
|
|
115
362
|
};
|
|
116
363
|
|
|
117
|
-
|
|
364
|
+
// Named once and used twice, for `useLive`'s reason: the frame the bar is
|
|
365
|
+
// DRAWN at and the frame it is judged live at must be one arithmetic.
|
|
366
|
+
const startLane = sceneStart - originFrames + lifespan.in;
|
|
367
|
+
const live = useLive(startLane, startLane + (lifespan.out - lifespan.in), originFrames);
|
|
368
|
+
const left = frameToPx(startLane, ppf);
|
|
118
369
|
const width = Math.max(8, frameToPx(lifespan.out - lifespan.in, ppf));
|
|
370
|
+
// A NAME IS NEVER CUT SHORT. A video clip draws its own name over its picture.
|
|
371
|
+
const namePlace = video !== undefined || laneW === undefined ? "inside" : clipNamePlace(left, width, row.label, laneW);
|
|
372
|
+
/**
|
|
373
|
+
* THE ADAPTIVE GRAB ZONE IS A CLIP AFFORDANCE, and it stays scoped to one.
|
|
374
|
+
*
|
|
375
|
+
* `.lifespan-handle { width: 8px }` in styles.css is what every bar in this
|
|
376
|
+
* dock has always been grabbed by. Setting a width inline on every row
|
|
377
|
+
* silently retuned the hit target on kinds that have nothing to do with
|
|
378
|
+
* video AND left that rule dead for all of them, which is a stylesheet that
|
|
379
|
+
* describes a different machine than the code.
|
|
380
|
+
*
|
|
381
|
+
* ponytail: "two 8px handles ARE a six-frame bar" is true of every kind, so
|
|
382
|
+
* widening this is defensible. It is a deliberate change to every row's hit
|
|
383
|
+
* target and it has to retire the CSS literal in the same move; it is not
|
|
384
|
+
* this fix, and half of it is worse than neither half.
|
|
385
|
+
*/
|
|
386
|
+
const clipHandle: CSSProperties | undefined =
|
|
387
|
+
video === undefined ? undefined : { width: trimHandlePx(width) };
|
|
388
|
+
const barStyle: CSSProperties =
|
|
389
|
+
video === undefined
|
|
390
|
+
? { left, width }
|
|
391
|
+
: // The clip bar owns its own box: no padding, no border and no vertical
|
|
392
|
+
// inset, so the 32 + 1 + 22 budget in model.ts is exactly the row's
|
|
393
|
+
// content height. Selection still shows, because `.is-selected` is an
|
|
394
|
+
// OUTLINE rather than a border.
|
|
395
|
+
{
|
|
396
|
+
left,
|
|
397
|
+
width,
|
|
398
|
+
top: 0,
|
|
399
|
+
bottom: 0,
|
|
400
|
+
padding: 0,
|
|
401
|
+
border: 0,
|
|
402
|
+
borderRadius: 4,
|
|
403
|
+
flexDirection: "column",
|
|
404
|
+
alignItems: "stretch",
|
|
405
|
+
};
|
|
119
406
|
|
|
120
407
|
return (
|
|
121
408
|
<div
|
|
122
|
-
className={`lifespan-row${selected ? " is-selected" : ""}
|
|
409
|
+
className={`lifespan-row${selected ? " is-selected" : ""}${live ? " is-live" : ""}${video === undefined ? "" : " is-video"}${revealed ? " is-revealed" : ""}${
|
|
410
|
+
mask?.off === true ? " is-mask-off" : ""
|
|
411
|
+
}`}
|
|
123
412
|
data-lifespan={row.layerId}
|
|
124
413
|
data-row-scene={sceneId}
|
|
414
|
+
style={video === undefined ? undefined : { height: VIDEO_ROW_H }}
|
|
415
|
+
// The stage lights this row's layer while the cursor is here:
|
|
416
|
+
// identity-diffed in the store, one SVG repaint stage-side.
|
|
417
|
+
onPointerEnter={() => store.hoverLayer(row.layerId)}
|
|
418
|
+
onPointerLeave={() => store.hoverLayer(null)}
|
|
125
419
|
>
|
|
126
|
-
<div
|
|
420
|
+
<div
|
|
421
|
+
className="lifespan-label-cell"
|
|
422
|
+
style={{ width: labelWidth, "--depth": row.depth, "--mask-inset": `${mask?.inset ?? 0}px` } as CSSProperties}
|
|
423
|
+
>
|
|
424
|
+
{mask === undefined ? null : <MaskBracket segments={mask.segments} base="var(--space-2)" />}
|
|
127
425
|
<button
|
|
128
426
|
type="button"
|
|
129
427
|
className="lifespan-disclosure"
|
|
130
428
|
aria-expanded={expanded}
|
|
131
|
-
aria-label={expanded ? `Hide ${row.label}'s
|
|
429
|
+
aria-label={expanded ? `Hide ${row.label}'s keyframes` : `Show ${row.label}'s keyframes`}
|
|
132
430
|
onClick={onToggle}
|
|
133
431
|
>
|
|
134
|
-
{expanded ?
|
|
432
|
+
{expanded ? <IconChevronDown /> : <IconChevron />}
|
|
135
433
|
</button>
|
|
434
|
+
{/* OUTSIDE the name button, the rule FilePanel's own kind glyph keeps:
|
|
435
|
+
the button's textContent is the layer's label and nothing else, which
|
|
436
|
+
is what a screen reader announces and what the tests compare. */}
|
|
437
|
+
{layer === undefined ? null : (
|
|
438
|
+
<span
|
|
439
|
+
className={`layer-kind${
|
|
440
|
+
mask?.icon === "mask" ? " is-mask" : mask?.icon == null && isComponentGlyph(layer) ? " is-component" : ""
|
|
441
|
+
}`}
|
|
442
|
+
aria-hidden="true"
|
|
443
|
+
>
|
|
444
|
+
{mask?.icon == null ? iconFor(layer) : MASK_ICON}
|
|
445
|
+
</span>
|
|
446
|
+
)}
|
|
136
447
|
<button type="button" className="lifespan-name" onClick={pick}>
|
|
137
448
|
{row.label}
|
|
138
449
|
</button>
|
|
450
|
+
{/* The mask's mode, the outline's badge (Paper page 25). */}
|
|
451
|
+
{mask?.badge == null ? null : (
|
|
452
|
+
<span className="mask-badge" title={mask.badge}>
|
|
453
|
+
{mask.badge}
|
|
454
|
+
</span>
|
|
455
|
+
)}
|
|
456
|
+
{/* Who else holds this layer (Paper page 21), a dot each. */}
|
|
457
|
+
<PeerDots at={(p) => p.sel.includes(row.layerId)} />
|
|
458
|
+
{layer === undefined ? null : <RowSwitches layer={layer} video={video} sceneId={sceneId} />}
|
|
139
459
|
</div>
|
|
140
460
|
<div className="lifespan-lane">
|
|
461
|
+
{ghostFrom === null ? null : (
|
|
462
|
+
<StaggerGhost fromFrame={ghostFrom} width={width} shift={startLane - ghostFrom} ppf={ppf} />
|
|
463
|
+
)}
|
|
141
464
|
<div
|
|
142
|
-
className={`lifespan-bar${selected ? " is-selected" : ""}
|
|
143
|
-
|
|
465
|
+
className={`lifespan-bar${live ? " is-live" : ""}${selected ? " is-selected" : ""}${
|
|
466
|
+
video === undefined ? "" : " is-video"
|
|
467
|
+
}${mask?.badge == null ? "" : " is-mask"}`}
|
|
468
|
+
style={barStyle}
|
|
144
469
|
data-bar={row.layerId}
|
|
145
470
|
onPointerDown={drag("body")}
|
|
146
|
-
title={
|
|
471
|
+
title={formatCombo(
|
|
472
|
+
video === undefined
|
|
473
|
+
? `${row.label} · ${lifespan.in}–${lifespan.out} · drag to move it and its keys, ⌥drag to slip the keys alone or to stagger a selection, ⌘drag to ignore snapping`
|
|
474
|
+
: `${row.label} · ${lifespan.in}–${lifespan.out} · drag to move the clip, ⌥drag to slip the footage under it or to stagger a selection, ⇧⌥drag to move this clip alone, ⌘drag to ignore snapping`,
|
|
475
|
+
)}
|
|
147
476
|
>
|
|
477
|
+
{video === undefined || layer?.kind !== "video" ? null : (
|
|
478
|
+
<ClipBody
|
|
479
|
+
layer={layer}
|
|
480
|
+
video={video}
|
|
481
|
+
fps={fps}
|
|
482
|
+
ppf={ppf}
|
|
483
|
+
leftPx={left}
|
|
484
|
+
widthPx={width}
|
|
485
|
+
labelWidth={labelWidth}
|
|
486
|
+
lifespanIn={lifespan.in}
|
|
487
|
+
label={row.label}
|
|
488
|
+
/>
|
|
489
|
+
)}
|
|
148
490
|
<span
|
|
149
491
|
className="lifespan-handle lifespan-handle-in"
|
|
150
492
|
data-handle="lifespan-in"
|
|
493
|
+
style={clipHandle}
|
|
151
494
|
onPointerDown={drag("in")}
|
|
152
495
|
aria-hidden="true"
|
|
153
496
|
/>
|
|
154
|
-
<span className="lifespan-bar-name">{row.label}</span>
|
|
497
|
+
{video === undefined && namePlace === "inside" ? <span className="lifespan-bar-name">{row.label}</span> : null}
|
|
498
|
+
{mask?.badge == null ? null : <span className="lifespan-bar-mask">mask · {mask.badge}</span>}
|
|
499
|
+
{/* THE OUT EDGE CARRIES TWO GESTURES WHEN THERE IS A SEAM, so they
|
|
500
|
+
are split by HEIGHT rather than stacked on one 3-to-10px strip:
|
|
501
|
+
the trim keeps the picture half, the roll takes the waveform half.
|
|
502
|
+
Each target is then at least 22px tall, and the seam is where an
|
|
503
|
+
author already looks for a rolling edit. With no abutting
|
|
504
|
+
neighbour the trim handle is full height, as on any other bar. */}
|
|
155
505
|
<span
|
|
156
506
|
className="lifespan-handle lifespan-handle-out"
|
|
157
507
|
data-handle="lifespan-out"
|
|
508
|
+
style={video?.rollPartner == null ? clipHandle : { ...clipHandle, bottom: VIDEO_WAVE_H }}
|
|
158
509
|
onPointerDown={drag("out")}
|
|
159
510
|
aria-hidden="true"
|
|
160
511
|
/>
|
|
512
|
+
{video?.rollPartner == null ? null : (
|
|
513
|
+
<span
|
|
514
|
+
className="lifespan-handle lifespan-handle-roll"
|
|
515
|
+
data-handle="lifespan-roll"
|
|
516
|
+
style={{ ...clipHandle, right: 0, top: VIDEO_PICTURE_H + VIDEO_EDGE_H, cursor: "ew-resize" }}
|
|
517
|
+
onPointerDown={drag("roll")}
|
|
518
|
+
aria-hidden="true"
|
|
519
|
+
/>
|
|
520
|
+
)}
|
|
161
521
|
</div>
|
|
522
|
+
{namePlace === "inside" ? null : (
|
|
523
|
+
// Beside the clip, not in it: after it, or before it at the track's
|
|
524
|
+
// end. Not a drag target, so a press lands on the lane as on any gap.
|
|
525
|
+
<span
|
|
526
|
+
className={`lifespan-bar-name is-outside is-${namePlace}`}
|
|
527
|
+
style={
|
|
528
|
+
namePlace === "after"
|
|
529
|
+
? { left: left + width + CLIP_NAME_GAP_PX }
|
|
530
|
+
: { left: left - CLIP_NAME_GAP_PX, transform: "translateX(-100%)" }
|
|
531
|
+
}
|
|
532
|
+
>
|
|
533
|
+
{row.label}
|
|
534
|
+
</span>
|
|
535
|
+
)}
|
|
536
|
+
{readout === null ? null : <StaggerChip step={readout.step} left={left + width} />}
|
|
162
537
|
</div>
|
|
163
538
|
</div>
|
|
164
539
|
);
|
|
165
540
|
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Which document write a press means, or null when it means nothing.
|
|
544
|
+
*
|
|
545
|
+
* Kept out of the handler so the mapping is one expression a reader can check
|
|
546
|
+
* against the header's table, and so the two modifier predicates are read
|
|
547
|
+
* exactly once, at the press.
|
|
548
|
+
*
|
|
549
|
+
* Null only for a roll with no partner, which the handle's own render condition
|
|
550
|
+
* already prevents: it is guarded here as well because the two conditions would
|
|
551
|
+
* otherwise have to stay in step by hand, and the cost of a stale one is a roll
|
|
552
|
+
* against an empty id.
|
|
553
|
+
*/
|
|
554
|
+
function gestureOf(
|
|
555
|
+
mode: "body" | "in" | "out" | "roll",
|
|
556
|
+
event: { altKey: boolean; shiftKey: boolean },
|
|
557
|
+
video: VideoRow | null,
|
|
558
|
+
carried: readonly string[],
|
|
559
|
+
): LifespanDrag | null {
|
|
560
|
+
if (mode === "roll") return video?.rollPartner == null ? null : { roll: video.rollPartner };
|
|
561
|
+
if (mode !== "body") return mode;
|
|
562
|
+
// THE CLIP FORK IS FIRST AND IT IS THE ONLY ONE ⇧ REACHES. Off a clip the
|
|
563
|
+
// chord falls through to the slip below, which is what ⌥ with or without ⇧
|
|
564
|
+
// has always done on an ordinary row — and `writeLifespan` routes "shift" to
|
|
565
|
+
// `shiftLayer` above its own video fork, so leaking the word here would
|
|
566
|
+
// rewrite a rect's window from a gesture that used to move only its keys.
|
|
567
|
+
if (video !== null && isSolo(event)) return "shift";
|
|
568
|
+
// ⌥ ON A SELECTION FANS IT OUT; on one bar it is still the slip.
|
|
569
|
+
if (isSlip(event)) return carried.length > 1 ? { stagger: carried } : "slip";
|
|
570
|
+
// A group move needs the ids; one bar does not, and the word form is what
|
|
571
|
+
// every existing test already speaks.
|
|
572
|
+
return carried.length > 1 ? { move: carried } : "body";
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* The picture, the edge and the wave, stacked.
|
|
577
|
+
*
|
|
578
|
+
* The EDGE is a line and not a lighter fill on purpose: in the light theme
|
|
579
|
+
* `--panel-raised` is `--panel`, so a surface step up between the two strips
|
|
580
|
+
* would be invisible. A 1px rule is what reads in both appearances.
|
|
581
|
+
*/
|
|
582
|
+
function ClipBody({
|
|
583
|
+
layer,
|
|
584
|
+
video,
|
|
585
|
+
fps,
|
|
586
|
+
ppf,
|
|
587
|
+
leftPx,
|
|
588
|
+
widthPx,
|
|
589
|
+
labelWidth,
|
|
590
|
+
lifespanIn,
|
|
591
|
+
label,
|
|
592
|
+
}: {
|
|
593
|
+
layer: VideoLayer3D;
|
|
594
|
+
video: VideoRow;
|
|
595
|
+
fps: Rational;
|
|
596
|
+
ppf: number;
|
|
597
|
+
/** The bar's left edge in LANE pixels, exactly as the row placed it. */
|
|
598
|
+
leftPx: number;
|
|
599
|
+
widthPx: number;
|
|
600
|
+
labelWidth: number;
|
|
601
|
+
/** The bar's own left edge, in scene frames, so the held stretches can be
|
|
602
|
+
* placed relative to it. */
|
|
603
|
+
lifespanIn: number;
|
|
604
|
+
label: string;
|
|
605
|
+
}) {
|
|
606
|
+
// ── THE AUDIBLE SPAN IS NOT THE BAR ────────────────────────────────────
|
|
607
|
+
//
|
|
608
|
+
// A recording's inline sound starts where the FOOTAGE starts, not where the
|
|
609
|
+
// window opens: `declaredVideoSoundClip` (timeline.ts) places it at
|
|
610
|
+
// `max(lifetime.in, video.offsetFrames)` and runs it to `lifetime.out`, and
|
|
611
|
+
// every mixer reads that one function. Handing the waveform the whole bar
|
|
612
|
+
// drew peaks across the lead hatch, which is the exact stretch that has no
|
|
613
|
+
// sound in it — the picture and the sound disagreeing about where the take
|
|
614
|
+
// begins, on the one row built to show that they agree.
|
|
615
|
+
//
|
|
616
|
+
// The same expression off the same two fields, rather than a second answer:
|
|
617
|
+
// `video.sound.durationFrames` IS that function's `out - from` over this same
|
|
618
|
+
// window, so the span ends on the bar's own out edge and needs no clamp here.
|
|
619
|
+
// A window that never opens is dropped one step earlier, as a null `sound`.
|
|
620
|
+
const soundFrom = Math.max(lifespanIn, Math.trunc(video.offsetFrames));
|
|
621
|
+
return (
|
|
622
|
+
<>
|
|
623
|
+
{/* THE STRIP'S CONTAINING BLOCK, and that is what this span is for.
|
|
624
|
+
`Filmstrip` places itself absolutely against its positioned ancestor
|
|
625
|
+
and fills it top to bottom; against the BAR that would be all 56px and
|
|
626
|
+
the waveform would be under footage. Against this it is the 32 the
|
|
627
|
+
picture owns. `overflow: hidden` is what cuts a column that straddles
|
|
628
|
+
either end, which is the clip the plan's own header relies on. */}
|
|
629
|
+
<span
|
|
630
|
+
className="lifespan-clip-picture"
|
|
631
|
+
style={{ height: VIDEO_PICTURE_H, position: "relative", overflow: "hidden", flex: "none" }}
|
|
632
|
+
>
|
|
633
|
+
<Filmstrip
|
|
634
|
+
layer={layer}
|
|
635
|
+
asset={video.pictureAsset}
|
|
636
|
+
fps={fps}
|
|
637
|
+
lifetimeIn={lifespanIn}
|
|
638
|
+
ppf={ppf}
|
|
639
|
+
barLeftPx={leftPx}
|
|
640
|
+
barWidthPx={widthPx}
|
|
641
|
+
labelWidth={labelWidth}
|
|
642
|
+
/>
|
|
643
|
+
{/* POSITIONED, because the strip is: CSS paints a positioned descendant
|
|
644
|
+
over in-flow content whatever the document order, so a plain span
|
|
645
|
+
here would vanish under the footage. `Filmstrip`'s own mount note
|
|
646
|
+
says so, and this is the row keeping that side of the contract. */}
|
|
647
|
+
<span
|
|
648
|
+
className="lifespan-bar-name"
|
|
649
|
+
style={{
|
|
650
|
+
position: "relative",
|
|
651
|
+
// BLOCK, because `.lifespan-bar-name`'s `overflow: hidden` and
|
|
652
|
+
// `text-overflow: ellipsis` do nothing on an inline box. In an
|
|
653
|
+
// ordinary bar it is a flex ITEM and therefore already blockified;
|
|
654
|
+
// here it is a child of the picture strip and is not.
|
|
655
|
+
display: "block",
|
|
656
|
+
lineHeight: `${VIDEO_PICTURE_H}px`,
|
|
657
|
+
padding: "0 6px",
|
|
658
|
+
}}
|
|
659
|
+
>
|
|
660
|
+
{label}
|
|
661
|
+
</span>
|
|
662
|
+
</span>
|
|
663
|
+
<span
|
|
664
|
+
className="lifespan-clip-edge"
|
|
665
|
+
aria-hidden="true"
|
|
666
|
+
style={{ height: VIDEO_EDGE_H, background: "var(--bar-rest-line)", flex: "none" }}
|
|
667
|
+
/>
|
|
668
|
+
<span className="lifespan-clip-sound" style={{ height: VIDEO_WAVE_H, position: "relative", flex: "none" }}>
|
|
669
|
+
{video.sound === null ? null : (
|
|
670
|
+
// POSITIONED, and that is the whole mechanism: `.audio-wave` is
|
|
671
|
+
// `position: absolute; inset: 0`, so the canvas fills its nearest
|
|
672
|
+
// positioned ancestor whatever pixel width it was drawn at. The box
|
|
673
|
+
// is the audible span; `widthPx` only says how many pixels of peaks
|
|
674
|
+
// to put in it.
|
|
675
|
+
<span
|
|
676
|
+
data-clip-wave=""
|
|
677
|
+
style={{
|
|
678
|
+
position: "absolute",
|
|
679
|
+
top: 0,
|
|
680
|
+
bottom: 0,
|
|
681
|
+
left: frameToPx(soundFrom - lifespanIn, ppf),
|
|
682
|
+
width: frameToPx(video.sound.durationFrames, ppf),
|
|
683
|
+
}}
|
|
684
|
+
>
|
|
685
|
+
<AudioWaveform
|
|
686
|
+
clip={video.sound}
|
|
687
|
+
asset={video.soundAsset}
|
|
688
|
+
fps={fps}
|
|
689
|
+
widthPx={frameToPx(video.sound.durationFrames, ppf)}
|
|
690
|
+
heightPx={VIDEO_WAVE_H}
|
|
691
|
+
/>
|
|
692
|
+
</span>
|
|
693
|
+
)}
|
|
694
|
+
</span>
|
|
695
|
+
{/* OVER BOTH STRIPS, because the held picture is held in the waveform's
|
|
696
|
+
half of the bar too — there is simply no sound out there either. */}
|
|
697
|
+
<HeldMark span={video.lead} lifespanIn={lifespanIn} ppf={ppf} which="lead" />
|
|
698
|
+
<HeldMark span={video.hold} lifespanIn={lifespanIn} ppf={ppf} which="hold" />
|
|
699
|
+
</>
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** One frozen stretch, hatched, with a rule on the crossing itself so the exact
|
|
704
|
+
* frame the picture stops on is readable and not merely implied. */
|
|
705
|
+
function HeldMark({
|
|
706
|
+
span,
|
|
707
|
+
lifespanIn,
|
|
708
|
+
ppf,
|
|
709
|
+
which,
|
|
710
|
+
}: {
|
|
711
|
+
span: HeldSpan | null;
|
|
712
|
+
lifespanIn: number;
|
|
713
|
+
ppf: number;
|
|
714
|
+
which: "lead" | "hold";
|
|
715
|
+
}) {
|
|
716
|
+
if (span === null) return null;
|
|
717
|
+
const from = frameToPx(span.from - lifespanIn, ppf);
|
|
718
|
+
const to = frameToPx(span.to - lifespanIn, ppf);
|
|
719
|
+
// The rule sits on the crossing, which is the INNER end of the stretch: the
|
|
720
|
+
// lead freezes up to its `to` and the hold from its `from`.
|
|
721
|
+
const rule = which === "lead" ? "borderRight" : "borderLeft";
|
|
722
|
+
return (
|
|
723
|
+
<span
|
|
724
|
+
className={`lifespan-held is-${which}`}
|
|
725
|
+
data-held={which}
|
|
726
|
+
aria-hidden="true"
|
|
727
|
+
title={
|
|
728
|
+
which === "lead"
|
|
729
|
+
? "The recording has not started yet: this stretch is its first frame, frozen"
|
|
730
|
+
: "The recording has run out: this stretch is its last frame, frozen"
|
|
731
|
+
}
|
|
732
|
+
style={{
|
|
733
|
+
position: "absolute",
|
|
734
|
+
top: 0,
|
|
735
|
+
bottom: 0,
|
|
736
|
+
left: from,
|
|
737
|
+
width: Math.max(1, to - from),
|
|
738
|
+
background: HELD_HATCH,
|
|
739
|
+
[rule]: "1px solid color-mix(in srgb, var(--timeline-ink) 45%, transparent)",
|
|
740
|
+
pointerEvents: "none",
|
|
741
|
+
}}
|
|
742
|
+
/>
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* The switches in a row's label cell: every row carries eye and lock, a clip
|
|
748
|
+
* with sound adds mute.
|
|
749
|
+
*
|
|
750
|
+
* EYE and LOCK are the layer's own flags and MUTE is the recording's inline
|
|
751
|
+
* sound. Mute is a different switch from the eye by design: `hidden` is the
|
|
752
|
+
* picture, `video.sound.muted` is the audio, which is the split every NLE draws
|
|
753
|
+
* and the split `videoSoundClip` already reads.
|
|
754
|
+
*
|
|
755
|
+
* ── ALL THREE NAME THEIR SUBJECT, AND THE EYE USED NOT TO ──────────────
|
|
756
|
+
*
|
|
757
|
+
* The outliner writes its two flags as `select` + `toggleFlag`, and that is
|
|
758
|
+
* sound THERE: its rows are the current container's by construction, so the
|
|
759
|
+
* container `toggleFlag` resolves for itself (`editingDef ?? state.sceneId`) is
|
|
760
|
+
* the one the row is in. This dock is one MASTER strip. `state.sceneId` follows
|
|
761
|
+
* the PLAYHEAD, so a row here is routinely in another scene, and the same pair
|
|
762
|
+
* copied over addressed a container the row is not in: `select` dropped an id
|
|
763
|
+
* that resolves in neither home and cleared the selection, `toggleFlag` then
|
|
764
|
+
* had nothing to write, and in a document whose ids were not minted against
|
|
765
|
+
* `usedIdsOf` it wrote to the playhead scene's layer of the same id instead.
|
|
766
|
+
*
|
|
767
|
+
* So each switch hands the verb this ROW's container and this row's layer, the
|
|
768
|
+
* way the bar's own drags hand `dragLifespan` its `sceneId`. Mute already did;
|
|
769
|
+
* it is now the shape all three share. Pressing one no longer moves the
|
|
770
|
+
* selection or the playhead either, which is what "a switch is not a
|
|
771
|
+
* navigation" means on a strip that shows the whole video at once.
|
|
772
|
+
*
|
|
773
|
+
* SOLO IS NOT HERE, and that is not an omission. Soloing is MONITORING: it
|
|
774
|
+
* changes what the author hears now and must never reach the document, so it
|
|
775
|
+
* belongs in the store's workspace-prefs family beside the other "where the
|
|
776
|
+
* author is standing" state. This file cannot add it — see the handoff note on
|
|
777
|
+
* store.ts — and a button that toggles nothing is worse than no button.
|
|
778
|
+
*/
|
|
779
|
+
function RowSwitches({
|
|
780
|
+
layer,
|
|
781
|
+
video,
|
|
782
|
+
sceneId,
|
|
783
|
+
}: {
|
|
784
|
+
layer: Layer3D;
|
|
785
|
+
video: VideoRow | undefined;
|
|
786
|
+
sceneId: string;
|
|
787
|
+
}) {
|
|
788
|
+
const store = useStore();
|
|
789
|
+
const hidden = layer.hidden === true;
|
|
790
|
+
const locked = layer.locked === true;
|
|
791
|
+
return (
|
|
792
|
+
<>
|
|
793
|
+
<button
|
|
794
|
+
type="button"
|
|
795
|
+
className="layer-flag"
|
|
796
|
+
data-switch="hidden"
|
|
797
|
+
aria-pressed={hidden}
|
|
798
|
+
aria-label={`${hidden ? "Show" : "Hide"} ${layer.label}`}
|
|
799
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
800
|
+
onClick={() => store.toggleFlag("hidden", { containerId: sceneId, layerIds: [layer.id] })}
|
|
801
|
+
>
|
|
802
|
+
{hidden ? <IconEyeOff /> : <IconEye />}
|
|
803
|
+
</button>
|
|
804
|
+
<button
|
|
805
|
+
type="button"
|
|
806
|
+
className="layer-flag"
|
|
807
|
+
data-switch="locked"
|
|
808
|
+
aria-pressed={locked}
|
|
809
|
+
aria-label={`${locked ? "Unlock" : "Lock"} ${layer.label}`}
|
|
810
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
811
|
+
onClick={() => store.toggleFlag("locked", { containerId: sceneId, layerIds: [layer.id] })}
|
|
812
|
+
>
|
|
813
|
+
{locked ? <IconLock /> : <IconUnlock />}
|
|
814
|
+
</button>
|
|
815
|
+
{/* Only when there IS a soundtrack. A recording that declares none has no
|
|
816
|
+
capability here to keep an affordance visible for. */}
|
|
817
|
+
{video === undefined || !video.hasSound || layer.kind !== "video" ? null : (
|
|
818
|
+
<button
|
|
819
|
+
type="button"
|
|
820
|
+
className="layer-flag"
|
|
821
|
+
data-switch="muted"
|
|
822
|
+
aria-pressed={video.muted}
|
|
823
|
+
aria-label={`${video.muted ? "Unmute" : "Mute"} ${layer.label}`}
|
|
824
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
825
|
+
onClick={() => {
|
|
826
|
+
if (layer.video.sound === undefined) return;
|
|
827
|
+
// THE RECORD IS RE-READ INSIDE THE EDIT, never the one this row
|
|
828
|
+
// closed over at render. `rowLayer` is the BASE layer (TimelineDock
|
|
829
|
+
// reads `layersById` directly), so the gain here is authored and not
|
|
830
|
+
// a sample of a keyframed curve. That hazard is `SoundRows`'s, and
|
|
831
|
+
// `baseSoundOf` in TimeSection.tsx answers it. The hazard HERE is
|
|
832
|
+
// staleness: a mute pressed after another panel committed a fade
|
|
833
|
+
// would spread this render's record back over it. Scene-addressed
|
|
834
|
+
// like `setVideoSound` itself, so a definition container answers
|
|
835
|
+
// undefined rather than handing back a record the op would refuse.
|
|
836
|
+
store.edit((doc) => {
|
|
837
|
+
const base = doc.scenesById[sceneId]?.layersById[layer.id];
|
|
838
|
+
if (base === undefined || base.kind !== "video" || base.video.sound === undefined) return doc;
|
|
839
|
+
return setVideoSound(doc, sceneId, layer.id, { ...base.video.sound, muted: !video.muted });
|
|
840
|
+
});
|
|
841
|
+
}}
|
|
842
|
+
>
|
|
843
|
+
{video.muted ? <IconVolumeOff /> : <IconVolume />}
|
|
844
|
+
</button>
|
|
845
|
+
)}
|
|
846
|
+
</>
|
|
847
|
+
);
|
|
848
|
+
}
|