@bycrux/editor 0.11.2 → 1.0.1
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/package.json +3 -1
- package/src/ControlsInfoModal.tsx +251 -61
- package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
- package/src/__tests__/adapter.test.ts +59 -1
- package/src/__tests__/schema-assignability.test.ts +93 -0
- package/src/__tests__/schema.test.ts +15 -0
- package/src/__tests__/video-adapter-contract.test.ts +128 -5
- package/src/carousel/AddElementMenu.tsx +10 -4
- package/src/carousel/CarouselEditor.tsx +46 -8
- package/src/carousel/CarouselRenderModal.tsx +15 -10
- package/src/carousel/OverlayPicker.tsx +10 -3
- package/src/carousel/SlidePropertyPanel.tsx +43 -21
- package/src/components/FilmstripScrubber.tsx +277 -0
- package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
- package/src/engine/__tests__/audio-clock.test.ts +655 -0
- package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
- package/src/engine/__tests__/batch-planner.test.ts +298 -0
- package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
- package/src/engine/__tests__/demux-ranged.test.ts +495 -0
- package/src/engine/__tests__/demux-truncated.test.ts +136 -0
- package/src/engine/__tests__/demux.test.ts +305 -0
- package/src/engine/__tests__/eligibility.test.ts +146 -0
- package/src/engine/__tests__/engine-recovery.test.ts +263 -0
- package/src/engine/__tests__/engine.test.ts +326 -0
- package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
- package/src/engine/__tests__/frame-server.test.ts +326 -0
- package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
- package/src/engine/__tests__/media-loader.test.ts +100 -0
- package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
- package/src/engine/__tests__/scheduler.test.ts +1310 -0
- package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
- package/src/engine/__tests__/scrub-source.test.ts +195 -0
- package/src/engine/__tests__/source-host.test.ts +455 -0
- package/src/engine/__tests__/time-stretch.test.ts +182 -0
- package/src/engine/audio-clock.ts +1179 -0
- package/src/engine/audio-worklet-source.ts +160 -0
- package/src/engine/batch-planner.ts +308 -0
- package/src/engine/debug-hud.tsx +54 -0
- package/src/engine/decode-worker-source.ts +142 -0
- package/src/engine/demux.ts +900 -0
- package/src/engine/eligibility.ts +140 -0
- package/src/engine/frame-server.ts +644 -0
- package/src/engine/index.ts +950 -0
- package/src/engine/media-loader.ts +380 -0
- package/src/engine/mp4box.d.ts +214 -0
- package/src/engine/scheduler.ts +1324 -0
- package/src/engine/scrub-resolve.ts +66 -0
- package/src/engine/scrub-source.ts +496 -0
- package/src/engine/time-stretch.ts +224 -0
- package/src/index.ts +83 -1
- package/src/preview/OverlayPreview.tsx +2 -24
- package/src/schema.ts +146 -3
- package/src/state/__tests__/use-project-sync.test.tsx +56 -0
- package/src/state/use-project-sync.ts +17 -0
- package/src/test-setup.ts +32 -0
- package/src/text/FontPicker.tsx +85 -51
- package/src/text/TextFormattingToolbar.tsx +6 -1
- package/src/text/__tests__/FontPicker.options.test.ts +43 -0
- package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
- package/src/theme.ts +108 -3
- package/src/types.ts +601 -22
- package/src/ui/Loader.tsx +64 -0
- package/src/ui/NumberField.tsx +254 -0
- package/src/ui/Slider.tsx +128 -0
- package/src/ui/Tooltip.tsx +118 -0
- package/src/ui/__tests__/NumberField.test.tsx +298 -0
- package/src/ui/__tests__/Slider.test.tsx +150 -0
- package/src/ui/__tests__/Tooltip.test.tsx +105 -0
- package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
- package/src/ui/badge.tsx +12 -4
- package/src/ui/index.ts +6 -0
- package/src/ui/input.tsx +1 -1
- package/src/ui/select.tsx +1 -1
- package/src/ui/switch.tsx +12 -2
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/usePersistentState.ts +63 -0
- package/src/video/AudioPolishModal.tsx +983 -0
- package/src/video/CaptionListPanel.test.tsx +944 -0
- package/src/video/CaptionListPanel.tsx +1106 -0
- package/src/video/CaptionRegenModal.tsx +37 -9
- package/src/video/CaptionSpecimen.tsx +160 -0
- package/src/video/CaptionStyleGallery.tsx +466 -0
- package/src/video/CommandPalette.tsx +165 -0
- package/src/video/ImageToneMenu.tsx +137 -0
- package/src/video/OverlayInspector.tsx +977 -0
- package/src/video/RenderModal.tsx +1035 -62
- package/src/video/VersionCompare.tsx +258 -0
- package/src/video/VersionPanel.tsx +117 -42
- package/src/video/VideoEditor.tsx +2229 -243
- package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
- package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
- package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
- package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
- package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
- package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
- package/src/video/__tests__/CommandPalette.test.tsx +119 -0
- package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
- package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
- package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
- package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
- package/src/video/__tests__/VersionCompare.test.tsx +182 -0
- package/src/video/__tests__/VersionPanel.test.tsx +279 -0
- package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
- package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
- package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
- package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
- package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
- package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
- package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
- package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
- package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
- package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
- package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
- package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
- package/src/video/__tests__/VideoEditor.test.tsx +403 -50
- package/src/video/__tests__/audioMagnet.test.ts +158 -0
- package/src/video/__tests__/audioPolish.test.ts +1202 -0
- package/src/video/__tests__/captionActiveWord.test.ts +107 -0
- package/src/video/__tests__/captionLanes.test.ts +262 -0
- package/src/video/__tests__/captionPositioning.test.tsx +8 -3
- package/src/video/__tests__/captionWordFloor.test.ts +228 -0
- package/src/video/__tests__/clipboard-ops.test.ts +430 -0
- package/src/video/__tests__/cuts.insert.test.ts +244 -0
- package/src/video/__tests__/cuts.test.ts +1213 -34
- package/src/video/__tests__/export-limits.test.ts +195 -0
- package/src/video/__tests__/hover-scrub.test.ts +163 -0
- package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
- package/src/video/__tests__/keyframeOps.test.ts +643 -0
- package/src/video/__tests__/keymap.test.tsx +171 -0
- package/src/video/__tests__/render-progress.test.tsx +20 -4
- package/src/video/__tests__/shuttle.test.ts +210 -0
- package/src/video/__tests__/source-preview.test.ts +60 -0
- package/src/video/__tests__/timecode.test.ts +77 -0
- package/src/video/__tests__/use-report-context.test.tsx +101 -0
- package/src/video/audioMagnet.ts +72 -0
- package/src/video/audioPolish.ts +774 -0
- package/src/video/captionActiveWord.ts +74 -0
- package/src/video/captionLanes.ts +202 -0
- package/src/video/captionRepair.ts +9 -5
- package/src/video/captionStyleDefaults.ts +100 -0
- package/src/video/captionWordFloor.ts +83 -0
- package/src/video/clipboard-ops.ts +377 -0
- package/src/video/cuts.ts +713 -37
- package/src/video/export-limits.ts +102 -0
- package/src/video/hover-scrub.ts +102 -0
- package/src/video/imageTone.ts +58 -0
- package/src/video/imageToneExamples.ts +10 -0
- package/src/video/keyframeOps.ts +384 -0
- package/src/video/keymap.ts +146 -0
- package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
- package/src/video/panels/LeftPanelTabs.tsx +187 -0
- package/src/video/panels/OverlayContentPanel.tsx +351 -0
- package/src/video/panels/TabNav.tsx +60 -0
- package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
- package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
- package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
- package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
- package/src/video/preview/CaptionPreview.tsx +57 -69
- package/src/video/preview/EngineSurface.tsx +103 -0
- package/src/video/preview/OverlayItemsLayer.tsx +315 -103
- package/src/video/preview/PreviewPlayer.tsx +337 -56
- package/src/video/preview/SocialPreviewMenu.tsx +214 -0
- package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
- package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
- package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
- package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
- package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
- package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
- package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
- package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
- package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
- package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
- package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
- package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
- package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
- package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
- package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
- package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
- package/src/video/preview/audio-context.ts +111 -0
- package/src/video/preview/captionDragState.ts +91 -1
- package/src/video/preview/proxySupport.ts +86 -0
- package/src/video/preview/transformStyle.ts +22 -12
- package/src/video/preview/useDragOverlay.ts +92 -18
- package/src/video/preview/useEnginePlayback.ts +625 -0
- package/src/video/preview/useVideoPlayback.ts +211 -167
- package/src/video/sdrCurves.ts +56 -0
- package/src/video/shuttle.ts +159 -0
- package/src/video/source-preview.ts +66 -0
- package/src/video/timecode.ts +59 -0
- package/src/video/timeline/EditableSegment.tsx +1 -1
- package/src/video/timeline/Scrubber.tsx +46 -174
- package/src/video/timeline/SpeedControl.tsx +95 -0
- package/src/video/timeline/Timeline.tsx +1061 -328
- package/src/video/timeline/TimelineContext.ts +17 -10
- package/src/video/timeline/TrackGutter.tsx +560 -0
- package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
- package/src/video/timeline/VolumeControl.tsx +113 -0
- package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
- package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
- package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
- package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
- package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
- package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
- package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
- package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
- package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
- package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
- package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
- package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
- package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
- package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
- package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
- package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
- package/src/video/timeline/__tests__/placement.test.ts +278 -0
- package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
- package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
- package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
- package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
- package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
- package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
- package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
- package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
- package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
- package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
- package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
- package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
- package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
- package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
- package/src/video/timeline/canvas/clip-bands.ts +56 -0
- package/src/video/timeline/canvas/draw.ts +2187 -0
- package/src/video/timeline/canvas/fade-curve.ts +111 -0
- package/src/video/timeline/canvas/filmstrips.ts +418 -0
- package/src/video/timeline/canvas/hit-test.ts +501 -0
- package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
- package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
- package/src/video/timeline/canvas/snap.ts +232 -0
- package/src/video/timeline/canvas/viewport.ts +457 -0
- package/src/video/timeline/canvas/waveforms.ts +664 -0
- package/src/video/timeline/makeCaptionEdit.ts +5 -1
- package/src/video/timeline/multiSelectOps.ts +214 -55
- package/src/video/timeline/placement.ts +282 -0
- package/src/video/timeline/timeline-model.ts +919 -0
- package/src/video/timeline/useItemDragDrop.ts +151 -178
- package/src/video/timeline/useTimelineZoom.ts +25 -60
- package/src/video/timeline/utils.ts +0 -12
- package/src/video/use-report-context.ts +75 -0
- package/src/video/preview/OverlayPropsModal.tsx +0 -292
- package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
- package/src/video/timeline/AudioTrackRow.tsx +0 -404
- package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
- package/src/video/timeline/CaptionTrackRow.tsx +0 -235
- package/src/video/timeline/PlayheadLine.tsx +0 -18
- package/src/video/timeline/TranscriptModal.tsx +0 -70
- package/src/video/timeline/TranscriptPanel.tsx +0 -273
- package/src/video/timeline/VisualTrackRow.tsx +0 -300
- package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
- package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
- package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
- package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
- package/src/video/timeline/__tests__/useItemDragDrop.test.ts +0 -72
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Source-capped export resolution/fps ceiling for a project. Pure — the export
|
|
2
|
+
// dialog uses this to decide which resolution/fps tiers are worth offering, so
|
|
3
|
+
// we never upscale past what the source footage actually carries.
|
|
4
|
+
|
|
5
|
+
import type { EditorProject as Project } from '../schema'
|
|
6
|
+
import { trackItems } from './timeline/timeline-model'
|
|
7
|
+
|
|
8
|
+
/** Short-side "class" a tier is named after — 720p / 1080p / 1440p / 2160p. */
|
|
9
|
+
export const STANDARD_RESOLUTION_TIERS = [720, 1080, 1440, 2160] as const
|
|
10
|
+
|
|
11
|
+
export const STANDARD_FPS_TIERS = [24, 30, 60] as const
|
|
12
|
+
|
|
13
|
+
// ── Internal helpers ────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** Max, across video items with known source dims, of `min(sourceWidth, sourceHeight)`.
|
|
16
|
+
* Undefined when no video item carries usable dims. Goes through `trackItems()`
|
|
17
|
+
* (not `project.tracks` directly) so a project still on disk in the legacy
|
|
18
|
+
* `VisualItem[][]` shape — a bare array of item arrays, with no `.items` to
|
|
19
|
+
* read — is tolerated the same way every other reader tolerates it. */
|
|
20
|
+
function maxSourceShortSide(project: Project): number | undefined {
|
|
21
|
+
let max: number | undefined
|
|
22
|
+
for (const items of trackItems(project)) {
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
if (item.type !== 'video') continue
|
|
25
|
+
const { sourceWidth: sw, sourceHeight: sh } = item
|
|
26
|
+
if (!sw || !sh || sw <= 0 || sh <= 0) continue
|
|
27
|
+
const short = Math.min(sw, sh)
|
|
28
|
+
if (max === undefined || short > max) max = short
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return max
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Converts a short-side tier to [w,h], preserving the project's aspect and
|
|
35
|
+
* orientation. Long side is rounded to the nearest EVEN integer (encoders
|
|
36
|
+
* demand even dims). */
|
|
37
|
+
function tierToResolution(tier: number, project: Project): [number, number] {
|
|
38
|
+
const [pw, ph] = project.settings.resolution
|
|
39
|
+
const shortP = Math.min(pw, ph)
|
|
40
|
+
const longP = Math.max(pw, ph)
|
|
41
|
+
if (shortP === 0) return [tier, tier]
|
|
42
|
+
|
|
43
|
+
const ratio = longP / shortP
|
|
44
|
+
const longSide = Math.round((tier * ratio) / 2) * 2
|
|
45
|
+
return pw <= ph ? [tier, longSide] : [longSide, tier]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/** Source-capped export resolution ceiling as [w,h]. Falls back to
|
|
51
|
+
* `project.settings.resolution` verbatim when no video clip carries source
|
|
52
|
+
* dims, or when the source is smaller than the smallest standard tier. */
|
|
53
|
+
export function maxExportResolution(project: Project): [number, number] {
|
|
54
|
+
const maxShort = maxSourceShortSide(project)
|
|
55
|
+
if (maxShort === undefined) return project.settings.resolution
|
|
56
|
+
|
|
57
|
+
const tier = [...STANDARD_RESOLUTION_TIERS].reverse().find(t => t <= maxShort)
|
|
58
|
+
if (tier === undefined) return project.settings.resolution
|
|
59
|
+
|
|
60
|
+
return tierToResolution(tier, project)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Source-capped export fps ceiling. No per-clip source-fps exists in the
|
|
64
|
+
* schema, so this is just the project's own setting (default 30). */
|
|
65
|
+
export function maxExportFps(project: Project): number {
|
|
66
|
+
return project.settings.fps ?? 30
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Every resolution tier offerable in the export dialog, as [w,h] preserving
|
|
70
|
+
* project aspect, filtered to the source cap and ordered ascending. Always
|
|
71
|
+
* non-empty — falls back to the project's current resolution when the source
|
|
72
|
+
* is below every standard tier. */
|
|
73
|
+
export function availableResolutionTiers(project: Project): Array<[number, number]> {
|
|
74
|
+
const maxShort = maxSourceShortSide(project)
|
|
75
|
+
const capShort = maxShort ?? Math.min(...project.settings.resolution)
|
|
76
|
+
|
|
77
|
+
const tiers = STANDARD_RESOLUTION_TIERS.filter(t => t <= capShort).map(t => tierToResolution(t, project))
|
|
78
|
+
return tiers.length > 0 ? tiers : [project.settings.resolution]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Every fps tier offerable in the export dialog, filtered to the source cap
|
|
82
|
+
* and ordered ascending. Always non-empty. */
|
|
83
|
+
export function availableFpsTiers(project: Project): number[] {
|
|
84
|
+
const cap = maxExportFps(project)
|
|
85
|
+
const tiers = STANDARD_FPS_TIERS.filter(f => f <= cap)
|
|
86
|
+
return tiers.length > 0 ? tiers : [cap]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The tier from `availableResolutionTiers` matching the project's CURRENT
|
|
90
|
+
* `settings.resolution` (by short-side), or the nearest smaller one.
|
|
91
|
+
* Convenience for the export dialog's default selection. */
|
|
92
|
+
export function currentResolutionTier(project: Project): [number, number] | undefined {
|
|
93
|
+
const tiers = availableResolutionTiers(project)
|
|
94
|
+
if (tiers.length === 0) return undefined
|
|
95
|
+
|
|
96
|
+
const currentShort = Math.min(...project.settings.resolution)
|
|
97
|
+
const exact = tiers.find(([w, h]) => Math.min(w, h) === currentShort)
|
|
98
|
+
if (exact) return exact
|
|
99
|
+
|
|
100
|
+
const smaller = [...tiers].reverse().find(([w, h]) => Math.min(w, h) <= currentShort)
|
|
101
|
+
return smaller ?? tiers[0] // current is below every offered tier — nearest is the smallest one
|
|
102
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react'
|
|
2
|
+
import type { PlaybackClock } from './playback-clock'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The preview-axis override: "show the frame at THIS time instead of the
|
|
6
|
+
* playhead's", or null for "show the playhead's frame".
|
|
7
|
+
*
|
|
8
|
+
* Separate from the playback clock on purpose. The clock is the truth — it is
|
|
9
|
+
* where playback resumes, what the red playhead draws from, and what the
|
|
10
|
+
* scrubber and transport move. Hover-scrubbing must not touch any of that; it
|
|
11
|
+
* only changes which frame the preview paints, and only while the pointer is
|
|
12
|
+
* over the timeline. Writing hover into the clock instead would drag the red
|
|
13
|
+
* playhead and the scrubber handle around with the mouse, and Space would then
|
|
14
|
+
* start playing from wherever the pointer happened to rest.
|
|
15
|
+
*
|
|
16
|
+
* An external store rather than React state because it is written on every
|
|
17
|
+
* mousemove: only `PreviewPlayer` subscribes, so the editor shell, the timeline
|
|
18
|
+
* and the caption row don't re-render as the pointer travels.
|
|
19
|
+
*/
|
|
20
|
+
export interface HoverScrub {
|
|
21
|
+
get(): number | null
|
|
22
|
+
set(t: number | null): void
|
|
23
|
+
subscribe(cb: () => void): () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createHoverScrub(): HoverScrub {
|
|
27
|
+
let time: number | null = null
|
|
28
|
+
const subs = new Set<() => void>()
|
|
29
|
+
return {
|
|
30
|
+
get: () => time,
|
|
31
|
+
set(t) {
|
|
32
|
+
// The no-op guard matters: `clock.subscribe` clears this store on every
|
|
33
|
+
// playback tick (see VideoEditor), which would otherwise notify ~60
|
|
34
|
+
// times a second forever.
|
|
35
|
+
if (t === time) return
|
|
36
|
+
time = t
|
|
37
|
+
subs.forEach((cb) => cb())
|
|
38
|
+
},
|
|
39
|
+
subscribe(cb) {
|
|
40
|
+
subs.add(cb)
|
|
41
|
+
return () => { subs.delete(cb) }
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Stable snapshot for an absent store, so `useSyncExternalStore` isn't handed
|
|
47
|
+
* a fresh closure per render when a host omits hover-scrubbing entirely. */
|
|
48
|
+
const NO_SUBSCRIBE = () => () => {}
|
|
49
|
+
const NO_TIME = () => null
|
|
50
|
+
|
|
51
|
+
export function useHoverScrubTime(store?: HoverScrub): number | null {
|
|
52
|
+
return useSyncExternalStore(
|
|
53
|
+
store?.subscribe ?? NO_SUBSCRIBE,
|
|
54
|
+
store?.get ?? NO_TIME,
|
|
55
|
+
store?.get ?? NO_TIME,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The playback→editor half of the clock bridge, gated on hover-preview.
|
|
61
|
+
*
|
|
62
|
+
* Showing a hovered frame means SEEKING the source to it, and a seek runs a
|
|
63
|
+
* tick that reports the new position back (`useEnginePlayback`'s `emitTime`,
|
|
64
|
+
* the legacy hook's `onTimeUpdate`). Wired straight to `clock.set`, that makes
|
|
65
|
+
* the red playhead chase the yellow cursor across the timeline — the preview
|
|
66
|
+
* would be honest but the playhead would have silently moved, which is the one
|
|
67
|
+
* thing the preview axis promises it will not do.
|
|
68
|
+
*
|
|
69
|
+
* So while an override stands the picture is display-only and the clock is left
|
|
70
|
+
* alone. `handOverToHover` drops the override the moment playback starts, so
|
|
71
|
+
* playback's own emissions always get through.
|
|
72
|
+
*/
|
|
73
|
+
export function gateTimeSink(clock: PlaybackClock, hover?: HoverScrub): (t: number) => void {
|
|
74
|
+
return (t: number) => {
|
|
75
|
+
if (hover?.get() != null) return
|
|
76
|
+
clock.set(t)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Play from the yellow line. Called when playback starts: the source is already
|
|
82
|
+
* parked on the hovered frame (that is how it got on screen), so playback
|
|
83
|
+
* continues from there — this makes the RED playhead say so too, instead of
|
|
84
|
+
* leaving it stranded where it sat before the hover.
|
|
85
|
+
*
|
|
86
|
+
* Writing the clock is what makes the hand-over safe rather than cosmetic: with
|
|
87
|
+
* the override gone the preview falls back to the clock, and a clock still
|
|
88
|
+
* holding the pre-hover position reads as an external scrub and seeks the
|
|
89
|
+
* engine backwards mid-playback (`useEnginePlayback`'s scrub effect has no
|
|
90
|
+
* transport guard). Setting it to the hovered time keeps mirror and clock
|
|
91
|
+
* agreeing, so that seek never fires.
|
|
92
|
+
*
|
|
93
|
+
* Returns whether a hand-over happened — false when nothing was hovered, which
|
|
94
|
+
* is every ordinary press of play.
|
|
95
|
+
*/
|
|
96
|
+
export function handOverToHover(clock: PlaybackClock, hover?: HoverScrub): boolean {
|
|
97
|
+
const t = hover?.get()
|
|
98
|
+
if (t == null) return false
|
|
99
|
+
hover!.set(null)
|
|
100
|
+
clock.set(t)
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image tone modes: how overlay images are color-mapped into HDR renders.
|
|
3
|
+
*
|
|
4
|
+
* Background: HDR (HLG/PQ) projects reinterpret the overlay raster as HDR
|
|
5
|
+
* signal at composite time, which lands CSS graphics (cards, captions) at full
|
|
6
|
+
* signal brightness. Web images are sRGB and must be pre-converted; the tone
|
|
7
|
+
* mode decides how the converted image relates to the graphics around it.
|
|
8
|
+
*
|
|
9
|
+
* Keep ids in sync with lib/normalize_image.py::TONE_MODES and
|
|
10
|
+
* montaj_assets/render/render.js::IMAGE_TONE_MODES.
|
|
11
|
+
*/
|
|
12
|
+
export type ImageTone = 'vivid' | 'broadcast' | 'punchy' | 'raw'
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_IMAGE_TONE: ImageTone = 'vivid'
|
|
15
|
+
|
|
16
|
+
export interface ImageToneInfo {
|
|
17
|
+
id: ImageTone
|
|
18
|
+
label: string
|
|
19
|
+
/** One-line summary shown under the label. Plain English, no jargon. */
|
|
20
|
+
summary: string
|
|
21
|
+
/** Secondary detail for tooltips or expanded views. */
|
|
22
|
+
detail: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const IMAGE_TONES: ImageToneInfo[] = [
|
|
26
|
+
{
|
|
27
|
+
id: 'vivid',
|
|
28
|
+
label: 'Vivid',
|
|
29
|
+
summary: 'True colors at full brightness, matching your cards and captions.',
|
|
30
|
+
detail:
|
|
31
|
+
'Faithful color conversion with image whites raised to the same level as '
|
|
32
|
+
+ 'overlay graphics. The default, and usually what you want.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'punchy',
|
|
36
|
+
label: 'Punchy',
|
|
37
|
+
summary: 'The legacy contrast, with the color errors corrected.',
|
|
38
|
+
detail:
|
|
39
|
+
'Keeps the darker, contrasty image tone older renders had, but fixes the '
|
|
40
|
+
+ 'oversaturated hues. Closest to pre-3.9.3 videos without their color bug.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'broadcast',
|
|
44
|
+
label: 'Broadcast',
|
|
45
|
+
summary: 'TV-standard brightness (BT.2408). Accurate but dimmer than your graphics.',
|
|
46
|
+
detail:
|
|
47
|
+
'Maps image white to the 203-nit broadcast graphics level. Technically '
|
|
48
|
+
+ 'correct for TV delivery; images read muted next to full-brightness cards.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'raw',
|
|
52
|
+
label: 'Raw',
|
|
53
|
+
summary: 'No conversion. The old oversaturated look.',
|
|
54
|
+
detail:
|
|
55
|
+
'Images are dropped into the HDR frame unconverted, shifting hues and '
|
|
56
|
+
+ 'boosting saturation (yellows go orange). The pre-3.9.3 behavior.',
|
|
57
|
+
},
|
|
58
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// AUTO-GENERATED example thumbnails for the image tone picker.
|
|
2
|
+
// A generic synthetic photo (no brands, no people) converted through the
|
|
3
|
+
// real lib/normalize_image.py per tone mode, composited on a raster-white
|
|
4
|
+
// card, reinterpreted as HLG, and tonemapped to SDR for display.
|
|
5
|
+
export const TONE_EXAMPLES: Record<string, string> = {
|
|
6
|
+
vivid: 'data:image/jpeg;base64,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',
|
|
7
|
+
broadcast: 'data:image/jpeg;base64,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',
|
|
8
|
+
punchy: 'data:image/jpeg;base64,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',
|
|
9
|
+
raw: 'data:image/jpeg;base64,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',
|
|
10
|
+
}
|