@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.
Files changed (268) hide show
  1. package/package.json +3 -1
  2. package/src/ControlsInfoModal.tsx +251 -61
  3. package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
  4. package/src/__tests__/adapter.test.ts +59 -1
  5. package/src/__tests__/schema-assignability.test.ts +93 -0
  6. package/src/__tests__/schema.test.ts +15 -0
  7. package/src/__tests__/video-adapter-contract.test.ts +128 -5
  8. package/src/carousel/AddElementMenu.tsx +10 -4
  9. package/src/carousel/CarouselEditor.tsx +46 -8
  10. package/src/carousel/CarouselRenderModal.tsx +15 -10
  11. package/src/carousel/OverlayPicker.tsx +10 -3
  12. package/src/carousel/SlidePropertyPanel.tsx +43 -21
  13. package/src/components/FilmstripScrubber.tsx +277 -0
  14. package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
  15. package/src/engine/__tests__/audio-clock.test.ts +655 -0
  16. package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
  17. package/src/engine/__tests__/batch-planner.test.ts +298 -0
  18. package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
  19. package/src/engine/__tests__/demux-ranged.test.ts +495 -0
  20. package/src/engine/__tests__/demux-truncated.test.ts +136 -0
  21. package/src/engine/__tests__/demux.test.ts +305 -0
  22. package/src/engine/__tests__/eligibility.test.ts +146 -0
  23. package/src/engine/__tests__/engine-recovery.test.ts +263 -0
  24. package/src/engine/__tests__/engine.test.ts +326 -0
  25. package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
  26. package/src/engine/__tests__/frame-server.test.ts +326 -0
  27. package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
  28. package/src/engine/__tests__/media-loader.test.ts +100 -0
  29. package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
  30. package/src/engine/__tests__/scheduler.test.ts +1310 -0
  31. package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
  32. package/src/engine/__tests__/scrub-source.test.ts +195 -0
  33. package/src/engine/__tests__/source-host.test.ts +455 -0
  34. package/src/engine/__tests__/time-stretch.test.ts +182 -0
  35. package/src/engine/audio-clock.ts +1179 -0
  36. package/src/engine/audio-worklet-source.ts +160 -0
  37. package/src/engine/batch-planner.ts +308 -0
  38. package/src/engine/debug-hud.tsx +54 -0
  39. package/src/engine/decode-worker-source.ts +142 -0
  40. package/src/engine/demux.ts +900 -0
  41. package/src/engine/eligibility.ts +140 -0
  42. package/src/engine/frame-server.ts +644 -0
  43. package/src/engine/index.ts +950 -0
  44. package/src/engine/media-loader.ts +380 -0
  45. package/src/engine/mp4box.d.ts +214 -0
  46. package/src/engine/scheduler.ts +1324 -0
  47. package/src/engine/scrub-resolve.ts +66 -0
  48. package/src/engine/scrub-source.ts +496 -0
  49. package/src/engine/time-stretch.ts +224 -0
  50. package/src/index.ts +83 -1
  51. package/src/preview/OverlayPreview.tsx +2 -24
  52. package/src/schema.ts +146 -3
  53. package/src/state/__tests__/use-project-sync.test.tsx +56 -0
  54. package/src/state/use-project-sync.ts +17 -0
  55. package/src/test-setup.ts +32 -0
  56. package/src/text/FontPicker.tsx +85 -51
  57. package/src/text/TextFormattingToolbar.tsx +6 -1
  58. package/src/text/__tests__/FontPicker.options.test.ts +43 -0
  59. package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
  60. package/src/theme.ts +108 -3
  61. package/src/types.ts +601 -22
  62. package/src/ui/Loader.tsx +64 -0
  63. package/src/ui/NumberField.tsx +254 -0
  64. package/src/ui/Slider.tsx +128 -0
  65. package/src/ui/Tooltip.tsx +118 -0
  66. package/src/ui/__tests__/NumberField.test.tsx +298 -0
  67. package/src/ui/__tests__/Slider.test.tsx +150 -0
  68. package/src/ui/__tests__/Tooltip.test.tsx +105 -0
  69. package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
  70. package/src/ui/badge.tsx +12 -4
  71. package/src/ui/index.ts +6 -0
  72. package/src/ui/input.tsx +1 -1
  73. package/src/ui/select.tsx +1 -1
  74. package/src/ui/switch.tsx +12 -2
  75. package/src/ui/textarea.tsx +1 -1
  76. package/src/ui/usePersistentState.ts +63 -0
  77. package/src/video/AudioPolishModal.tsx +983 -0
  78. package/src/video/CaptionListPanel.test.tsx +944 -0
  79. package/src/video/CaptionListPanel.tsx +1106 -0
  80. package/src/video/CaptionRegenModal.tsx +37 -9
  81. package/src/video/CaptionSpecimen.tsx +160 -0
  82. package/src/video/CaptionStyleGallery.tsx +466 -0
  83. package/src/video/CommandPalette.tsx +165 -0
  84. package/src/video/ImageToneMenu.tsx +137 -0
  85. package/src/video/OverlayInspector.tsx +977 -0
  86. package/src/video/RenderModal.tsx +1035 -62
  87. package/src/video/VersionCompare.tsx +258 -0
  88. package/src/video/VersionPanel.tsx +117 -42
  89. package/src/video/VideoEditor.tsx +2229 -243
  90. package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
  91. package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
  92. package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
  93. package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
  94. package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
  95. package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
  96. package/src/video/__tests__/CommandPalette.test.tsx +119 -0
  97. package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
  98. package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
  99. package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
  100. package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
  101. package/src/video/__tests__/VersionCompare.test.tsx +182 -0
  102. package/src/video/__tests__/VersionPanel.test.tsx +279 -0
  103. package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
  104. package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
  105. package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
  106. package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
  107. package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
  108. package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
  109. package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
  110. package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
  111. package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
  112. package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
  113. package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
  114. package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
  115. package/src/video/__tests__/VideoEditor.test.tsx +403 -50
  116. package/src/video/__tests__/audioMagnet.test.ts +158 -0
  117. package/src/video/__tests__/audioPolish.test.ts +1202 -0
  118. package/src/video/__tests__/captionActiveWord.test.ts +107 -0
  119. package/src/video/__tests__/captionLanes.test.ts +262 -0
  120. package/src/video/__tests__/captionPositioning.test.tsx +8 -3
  121. package/src/video/__tests__/captionWordFloor.test.ts +228 -0
  122. package/src/video/__tests__/clipboard-ops.test.ts +430 -0
  123. package/src/video/__tests__/cuts.insert.test.ts +244 -0
  124. package/src/video/__tests__/cuts.test.ts +1213 -34
  125. package/src/video/__tests__/export-limits.test.ts +195 -0
  126. package/src/video/__tests__/hover-scrub.test.ts +163 -0
  127. package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
  128. package/src/video/__tests__/keyframeOps.test.ts +643 -0
  129. package/src/video/__tests__/keymap.test.tsx +171 -0
  130. package/src/video/__tests__/render-progress.test.tsx +20 -4
  131. package/src/video/__tests__/shuttle.test.ts +210 -0
  132. package/src/video/__tests__/source-preview.test.ts +60 -0
  133. package/src/video/__tests__/timecode.test.ts +77 -0
  134. package/src/video/__tests__/use-report-context.test.tsx +101 -0
  135. package/src/video/audioMagnet.ts +72 -0
  136. package/src/video/audioPolish.ts +774 -0
  137. package/src/video/captionActiveWord.ts +74 -0
  138. package/src/video/captionLanes.ts +202 -0
  139. package/src/video/captionRepair.ts +9 -5
  140. package/src/video/captionStyleDefaults.ts +100 -0
  141. package/src/video/captionWordFloor.ts +83 -0
  142. package/src/video/clipboard-ops.ts +377 -0
  143. package/src/video/cuts.ts +713 -37
  144. package/src/video/export-limits.ts +102 -0
  145. package/src/video/hover-scrub.ts +102 -0
  146. package/src/video/imageTone.ts +58 -0
  147. package/src/video/imageToneExamples.ts +10 -0
  148. package/src/video/keyframeOps.ts +384 -0
  149. package/src/video/keymap.ts +146 -0
  150. package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
  151. package/src/video/panels/LeftPanelTabs.tsx +187 -0
  152. package/src/video/panels/OverlayContentPanel.tsx +351 -0
  153. package/src/video/panels/TabNav.tsx +60 -0
  154. package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
  155. package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
  156. package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
  157. package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
  158. package/src/video/preview/CaptionPreview.tsx +57 -69
  159. package/src/video/preview/EngineSurface.tsx +103 -0
  160. package/src/video/preview/OverlayItemsLayer.tsx +315 -103
  161. package/src/video/preview/PreviewPlayer.tsx +337 -56
  162. package/src/video/preview/SocialPreviewMenu.tsx +214 -0
  163. package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
  164. package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
  165. package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
  166. package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
  167. package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
  168. package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
  169. package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
  170. package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
  171. package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
  172. package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
  173. package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
  174. package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
  175. package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
  176. package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
  177. package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
  178. package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
  179. package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
  180. package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
  181. package/src/video/preview/audio-context.ts +111 -0
  182. package/src/video/preview/captionDragState.ts +91 -1
  183. package/src/video/preview/proxySupport.ts +86 -0
  184. package/src/video/preview/transformStyle.ts +22 -12
  185. package/src/video/preview/useDragOverlay.ts +92 -18
  186. package/src/video/preview/useEnginePlayback.ts +625 -0
  187. package/src/video/preview/useVideoPlayback.ts +211 -167
  188. package/src/video/sdrCurves.ts +56 -0
  189. package/src/video/shuttle.ts +159 -0
  190. package/src/video/source-preview.ts +66 -0
  191. package/src/video/timecode.ts +59 -0
  192. package/src/video/timeline/EditableSegment.tsx +1 -1
  193. package/src/video/timeline/Scrubber.tsx +46 -174
  194. package/src/video/timeline/SpeedControl.tsx +95 -0
  195. package/src/video/timeline/Timeline.tsx +1061 -328
  196. package/src/video/timeline/TimelineContext.ts +17 -10
  197. package/src/video/timeline/TrackGutter.tsx +560 -0
  198. package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
  199. package/src/video/timeline/VolumeControl.tsx +113 -0
  200. package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
  201. package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
  202. package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
  203. package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
  204. package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
  205. package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
  206. package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
  207. package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
  208. package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
  209. package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
  210. package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
  211. package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
  212. package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
  213. package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
  214. package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
  215. package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
  216. package/src/video/timeline/__tests__/placement.test.ts +278 -0
  217. package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
  218. package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
  219. package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
  220. package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
  221. package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
  222. package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
  223. package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
  224. package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
  225. package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
  226. package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
  227. package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
  228. package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
  229. package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
  230. package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
  231. package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
  232. package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
  233. package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
  234. package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
  235. package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
  236. package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
  237. package/src/video/timeline/canvas/clip-bands.ts +56 -0
  238. package/src/video/timeline/canvas/draw.ts +2187 -0
  239. package/src/video/timeline/canvas/fade-curve.ts +111 -0
  240. package/src/video/timeline/canvas/filmstrips.ts +418 -0
  241. package/src/video/timeline/canvas/hit-test.ts +501 -0
  242. package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
  243. package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
  244. package/src/video/timeline/canvas/snap.ts +232 -0
  245. package/src/video/timeline/canvas/viewport.ts +457 -0
  246. package/src/video/timeline/canvas/waveforms.ts +664 -0
  247. package/src/video/timeline/makeCaptionEdit.ts +5 -1
  248. package/src/video/timeline/multiSelectOps.ts +214 -55
  249. package/src/video/timeline/placement.ts +282 -0
  250. package/src/video/timeline/timeline-model.ts +919 -0
  251. package/src/video/timeline/useItemDragDrop.ts +151 -178
  252. package/src/video/timeline/useTimelineZoom.ts +25 -60
  253. package/src/video/timeline/utils.ts +0 -12
  254. package/src/video/use-report-context.ts +75 -0
  255. package/src/video/preview/OverlayPropsModal.tsx +0 -292
  256. package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
  257. package/src/video/timeline/AudioTrackRow.tsx +0 -404
  258. package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
  259. package/src/video/timeline/CaptionTrackRow.tsx +0 -235
  260. package/src/video/timeline/PlayheadLine.tsx +0 -18
  261. package/src/video/timeline/TranscriptModal.tsx +0 -70
  262. package/src/video/timeline/TranscriptPanel.tsx +0 -273
  263. package/src/video/timeline/VisualTrackRow.tsx +0 -300
  264. package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
  265. package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
  266. package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
  267. package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
  268. 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
+ }