@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
|
@@ -19,19 +19,29 @@
|
|
|
19
19
|
* reliably. Instead the rendered caption's bounding box is *measured* and a
|
|
20
20
|
* separate interactive selection box is drawn over it (z 50, the same layer the
|
|
21
21
|
* overlay handles use). That box is the only interactive element: clicking it
|
|
22
|
-
* selects the segment
|
|
23
|
-
*
|
|
22
|
+
* selects the target segment, dragging it moves the segment (offsetX/offsetY),
|
|
23
|
+
* and dragging a corner scales it (scale).
|
|
24
|
+
*
|
|
25
|
+
* Captions have lanes (rows), so several segments can be on screen at once and
|
|
26
|
+
* the templates paint all of them. There is still exactly ONE selection box:
|
|
27
|
+
* it addresses the segment the operator already selected when that segment is
|
|
28
|
+
* on screen, otherwise the highest-lane one (the caption painted last, hence
|
|
29
|
+
* on top). It is measured from that segment's own `data-caption-id` subtree,
|
|
30
|
+
* so it wraps one caption rather than the union of everything on screen.
|
|
24
31
|
*/
|
|
25
32
|
|
|
26
33
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
34
|
+
import { activeCaptionSegments } from '@bycrux/timeline-core'
|
|
27
35
|
import type { Captions } from '../../schema'
|
|
28
36
|
import type { OverlayFactory } from '../../types'
|
|
29
37
|
import OverlayErrorBoundary from '../../carousel/OverlayErrorBoundary'
|
|
38
|
+
import { ensureGoogleFontsLoaded } from '../../lib/google-fonts'
|
|
30
39
|
import type { CaptionEditPatch } from '../timeline/makeCaptionEdit'
|
|
31
40
|
import {
|
|
32
41
|
captionDragGeometry,
|
|
33
42
|
captionDragPatch,
|
|
34
43
|
hasEscapedClickSlop,
|
|
44
|
+
measureCaptionContentRect,
|
|
35
45
|
readCaptionGeometry,
|
|
36
46
|
type CaptionDragState,
|
|
37
47
|
type CaptionGeometry,
|
|
@@ -69,53 +79,6 @@ export function clampVisible(pos: number, size: number, extent: number): number
|
|
|
69
79
|
return Math.max(min, Math.min(max, pos))
|
|
70
80
|
}
|
|
71
81
|
|
|
72
|
-
interface Rect { left: number; top: number; width: number; height: number }
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Union of the client rects of every painted text run (and replaced element)
|
|
76
|
-
* under `root`.
|
|
77
|
-
*
|
|
78
|
-
* Deliberately NOT `root.getBoundingClientRect()`: the template's own outermost
|
|
79
|
-
* element is a frame-sized `position: fixed; inset: 0` box (captionOuterStyle),
|
|
80
|
-
* so its rect — and the rect of any wrapper we put around it — is the entire
|
|
81
|
-
* preview, which would put the selection box around the whole frame. Walking to
|
|
82
|
-
* the text nodes is the only markup-agnostic way to find where the caption
|
|
83
|
-
* actually paints, and it costs nothing on a subtree this small.
|
|
84
|
-
*
|
|
85
|
-
* Returns null when nothing is painted (e.g. `pop` renders no word between two
|
|
86
|
-
* word windows), which is the signal to hide the selection box entirely.
|
|
87
|
-
*/
|
|
88
|
-
export function measureCaptionContentRect(root: HTMLElement): Rect | null {
|
|
89
|
-
const doc = root.ownerDocument
|
|
90
|
-
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT)
|
|
91
|
-
const range = doc.createRange()
|
|
92
|
-
let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity
|
|
93
|
-
|
|
94
|
-
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
95
|
-
let rect: DOMRect | null = null
|
|
96
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
97
|
-
if (!node.nodeValue?.trim()) continue
|
|
98
|
-
range.selectNodeContents(node)
|
|
99
|
-
rect = range.getBoundingClientRect()
|
|
100
|
-
} else {
|
|
101
|
-
// Replaced elements paint without text nodes. Everything else is a
|
|
102
|
-
// container whose box is either the frame-sized outer wrapper or a
|
|
103
|
-
// full-width anchor box — including them would defeat the point.
|
|
104
|
-
const tag = (node as Element).tagName?.toUpperCase()
|
|
105
|
-
if (tag !== 'IMG' && tag !== 'SVG' && tag !== 'VIDEO' && tag !== 'CANVAS') continue
|
|
106
|
-
rect = (node as Element).getBoundingClientRect()
|
|
107
|
-
}
|
|
108
|
-
if (!rect || (!rect.width && !rect.height)) continue
|
|
109
|
-
left = Math.min(left, rect.left)
|
|
110
|
-
top = Math.min(top, rect.top)
|
|
111
|
-
right = Math.max(right, rect.right)
|
|
112
|
-
bottom = Math.max(bottom, rect.bottom)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (!Number.isFinite(left)) return null
|
|
116
|
-
return { left, top, width: right - left, height: bottom - top }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
82
|
interface CaptionPreviewProps {
|
|
120
83
|
track: Captions
|
|
121
84
|
currentTime: number
|
|
@@ -124,7 +87,7 @@ interface CaptionPreviewProps {
|
|
|
124
87
|
resolveCaptionTemplate?: (style: string) => string
|
|
125
88
|
/** Currently-selected caption segment id (shared with the timeline row). */
|
|
126
89
|
selectedCaptionId?: string
|
|
127
|
-
/** Click on the selection box — selects the segment
|
|
90
|
+
/** Click on the selection box — selects the segment the box is addressing.
|
|
128
91
|
* Accepts null for symmetry with the timeline row's copy of this callback
|
|
129
92
|
* (VideoEditor supplies one handler to both); the preview only ever passes
|
|
130
93
|
* an id, since it has no deselect affordance of its own. */
|
|
@@ -187,33 +150,54 @@ export default function CaptionPreview({
|
|
|
187
150
|
const frame = Math.round(currentTime * fps)
|
|
188
151
|
const lastSeg = track.segments[track.segments.length - 1]
|
|
189
152
|
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
153
|
+
// EVERY segment the template is showing, lane-ascending. `activeCaptionSegments`
|
|
154
|
+
// quantizes to `frame / fps` (not raw currentTime) and applies the templates'
|
|
155
|
+
// own `>= start && < end` predicate, so the selection box can never address a
|
|
156
|
+
// segment that is not on screen. Captions have lanes, so more than one can be
|
|
157
|
+
// painted at once — the templates draw them all.
|
|
158
|
+
const activeSegs = activeCaptionSegments(track, currentTime, fps)
|
|
159
|
+
|
|
160
|
+
// The ONE segment the selection box addresses. With several captions on
|
|
161
|
+
// screen "the active one" is ambiguous, so: the operator's current selection
|
|
162
|
+
// if it is among them, otherwise the highest-lane one — the caption painted
|
|
163
|
+
// last, and therefore the one visually on top at that instant.
|
|
164
|
+
const targetSeg =
|
|
165
|
+
(selectedCaptionId ? activeSegs.find(s => s.id === selectedCaptionId) : undefined)
|
|
166
|
+
?? activeSegs[activeSegs.length - 1]
|
|
167
|
+
?? null
|
|
195
168
|
|
|
196
169
|
// Interactivity requires a stable segment id (backfilled by VideoEditor) and a
|
|
197
170
|
// host that wired at least one caption callback. Without both, the layer stays
|
|
198
171
|
// exactly as it was: a passive, click-through caption overlay.
|
|
199
|
-
const
|
|
200
|
-
const canInteract = !!
|
|
201
|
-
const isSelected = canInteract && selectedCaptionId ===
|
|
172
|
+
const targetId = targetSeg?.id
|
|
173
|
+
const canInteract = !!targetId && !!(onSelectCaption || onCaptionSegmentChange)
|
|
174
|
+
const isSelected = canInteract && selectedCaptionId === targetId
|
|
202
175
|
|
|
203
176
|
// The box unmounts when the playhead leaves a segment, so a pending
|
|
204
177
|
// mouseleave never arrives and the hover hairline would carry over onto the
|
|
205
178
|
// next segment's box. Drop it whenever the target segment changes; a pointer
|
|
206
179
|
// that really is over the new box gets its mouseenter back on the next move.
|
|
207
|
-
useEffect(() => { setHovered(false) }, [
|
|
180
|
+
useEffect(() => { setHovered(false) }, [targetId])
|
|
208
181
|
|
|
209
182
|
// Theme props for the template: everything on the track except style/segments
|
|
210
|
-
// (handled separately) and googleFonts
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
183
|
+
// (handled separately) and googleFonts — not a template prop, but the editor
|
|
184
|
+
// must still LOAD it (see the effect below) so preview and render use the same
|
|
185
|
+
// face. Normalize the legacy lowercase `fontsize` key to the camelCase
|
|
186
|
+
// `fontSize` templates actually read.
|
|
187
|
+
const { style: _style, segments: _segments, googleFonts, fontsize, ...theme } = track
|
|
214
188
|
const themeProps: Record<string, unknown> = { ...theme }
|
|
215
189
|
if (fontsize != null) themeProps.fontSize = fontsize
|
|
216
190
|
|
|
191
|
+
// Inject the caption track's Google Fonts so the preview paints the same face
|
|
192
|
+
// the renderer fetches (render.js hands `captions.googleFonts` to
|
|
193
|
+
// bundleComponent, which injects the same stylesheet into the Puppeteer page).
|
|
194
|
+
// Without this, `captions.fontFamily` reaches the template in both paths but
|
|
195
|
+
// only the render has the font file — a silent preview/render divergence.
|
|
196
|
+
// Depend on the joined list rather than the array: `track` is a fresh object
|
|
197
|
+
// on every project edit, and String() flattens both the typed string[] and the
|
|
198
|
+
// bare string persisted projects occasionally carry (see google-fonts.ts).
|
|
199
|
+
useEffect(() => { ensureGoogleFontsLoaded(googleFonts) }, [String(googleFonts)])
|
|
200
|
+
|
|
217
201
|
// Live drag preview: overlay the in-flight geometry onto the dragged segment
|
|
218
202
|
// only. The templates read offsetX/offsetY/scale straight off the segment, so
|
|
219
203
|
// this is all it takes for the caption to follow the cursor.
|
|
@@ -265,7 +249,7 @@ export default function CaptionPreview({
|
|
|
265
249
|
}, [drag, scale])
|
|
266
250
|
|
|
267
251
|
function startGesture(type: CaptionDragState['type'], e: React.MouseEvent) {
|
|
268
|
-
if (!
|
|
252
|
+
if (!targetId) return
|
|
269
253
|
// Keep the mousedown from starting a text selection and from reaching the
|
|
270
254
|
// player's play/pause layer underneath.
|
|
271
255
|
e.preventDefault()
|
|
@@ -273,11 +257,11 @@ export default function CaptionPreview({
|
|
|
273
257
|
// Select and begin the gesture in the same press: unlike overlays (selected
|
|
274
258
|
// from the timeline) the preview box IS the caption's selection affordance,
|
|
275
259
|
// so requiring a separate click first would just cost a click.
|
|
276
|
-
if (selectedCaptionId !==
|
|
260
|
+
if (selectedCaptionId !== targetId) onSelectCaption?.(targetId)
|
|
277
261
|
if (!onCaptionSegmentChange) return
|
|
278
|
-
const g = readCaptionGeometry(
|
|
262
|
+
const g = readCaptionGeometry(targetSeg)
|
|
279
263
|
setDrag({
|
|
280
|
-
id:
|
|
264
|
+
id: targetId, type,
|
|
281
265
|
initX: e.clientX, initY: e.clientY,
|
|
282
266
|
initOffsetX: g.offsetX, initOffsetY: g.offsetY, initScale: g.scale,
|
|
283
267
|
})
|
|
@@ -295,7 +279,11 @@ export default function CaptionPreview({
|
|
|
295
279
|
const root = wrapRef.current
|
|
296
280
|
const content = contentRef.current
|
|
297
281
|
if (!box || !root || !content) return
|
|
298
|
-
|
|
282
|
+
// Measure the TARGET segment's own subtree, not the whole layer: with two
|
|
283
|
+
// captions on screen the union of both would wrap a box around the pair.
|
|
284
|
+
// A template that emits no `data-caption-id` marker falls back to the whole
|
|
285
|
+
// root inside measureCaptionContentRect, i.e. the pre-lane behaviour.
|
|
286
|
+
const rect = measureCaptionContentRect(content, targetId)
|
|
299
287
|
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
300
288
|
box.style.display = 'none'
|
|
301
289
|
return
|
|
@@ -307,7 +295,7 @@ export default function CaptionPreview({
|
|
|
307
295
|
const h = rect.height + BOX_PAD * 2
|
|
308
296
|
// Keep a grabbable sliver of the box inside the frame. offsetX/offsetY are
|
|
309
297
|
// unbounded and there is no numeric reset control for captions (unlike
|
|
310
|
-
// overlays,
|
|
298
|
+
// overlays, whose Transform panel has one), so a segment dragged fully past
|
|
311
299
|
// the frame edge would have BOTH its text and its selection box clipped
|
|
312
300
|
// away by this wrapper's `overflow-hidden` — leaving no way to drag it back
|
|
313
301
|
// short of hand-editing project.json. Dragging is delta-based (see
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP4 T6/T7 — the engine's picture surface: one canvas, the Preparing
|
|
3
|
+
* placeholder for the state where there is nothing to paint yet, and the
|
|
4
|
+
* debug HUD.
|
|
5
|
+
*
|
|
6
|
+
* It renders INSIDE `PreviewPlayer`'s existing transform container, in the
|
|
7
|
+
* position the two `<video>` slots occupy on the legacy path, with the same
|
|
8
|
+
* `z-index: 1` the active slot carries. The container keeps owning
|
|
9
|
+
* `scale`/`offsetX`/`offsetY` in CSS (plan decision 3: transform container,
|
|
10
|
+
* sourceCrop semantics, z-order and handles stay in CSS unchanged), so nothing
|
|
11
|
+
* about drag, resize or the selection outline changes between the two paths.
|
|
12
|
+
*
|
|
13
|
+
* The canvas is deliberately style-only-sized: `engine.attach()` sets the
|
|
14
|
+
* BACKING STORE to the project's design canvas and the painter re-reads
|
|
15
|
+
* `canvas.width`/`height` on every paint, so the element can be stretched to
|
|
16
|
+
* the frame box without touching the engine. `sourceCrop` is NOT applied here —
|
|
17
|
+
* the engine reproduces `sourceCropStyle.ts`'s algebra as a `drawImage` source
|
|
18
|
+
* rect (`scheduler.ts`'s `sourceCropDrawPlan`), so a second CSS crop on top
|
|
19
|
+
* would double-apply it.
|
|
20
|
+
*
|
|
21
|
+
* ── The Preparing placeholder (T7) ──
|
|
22
|
+
* Reachable ONLY for a clip that loses its proxy AFTER engine mode already
|
|
23
|
+
* started for this session — a mid-session addition, or a proxy that is still
|
|
24
|
+
* encoding, or one that failed to load/decode (`scheduler.ts`'s `picture ===
|
|
25
|
+
* 'preparing'` branch routes all three through the same state). A project
|
|
26
|
+
* that was ineligible at LOAD stays on the legacy player for the whole session
|
|
27
|
+
* (plan decision 2, enforced in `PreviewPlayer.tsx`'s `useEngineMode`) and
|
|
28
|
+
* never mounts this component at all — so "ineligible at load" can never reach
|
|
29
|
+
* this placeholder either.
|
|
30
|
+
*
|
|
31
|
+
* Shown only after `PREPARING_SHOW_DELAY_MS` of SUSTAINED `preparing` — an
|
|
32
|
+
* ordinary boundary swap can pass through `preparing` for a single tick while
|
|
33
|
+
* the next clip's session finishes attaching (prewarm usually lands it before
|
|
34
|
+
* the cut, but not always), and a placeholder that blinked on and off for one
|
|
35
|
+
* frame at every such swap would read as a glitch rather than as information.
|
|
36
|
+
* A proxy that is genuinely still encoding, or genuinely failed, is preparing
|
|
37
|
+
* for whole seconds — comfortably past the delay either way.
|
|
38
|
+
*/
|
|
39
|
+
import { useEffect, useState } from 'react'
|
|
40
|
+
import type { EngineStats, Picture } from '../../engine'
|
|
41
|
+
import DebugHud from '../../engine/debug-hud'
|
|
42
|
+
import { Loader } from '../../ui/Loader'
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* How long `picture` must stay `preparing` before the placeholder shows.
|
|
46
|
+
* Short enough that a genuinely-stuck clip still reads as "preparing" almost
|
|
47
|
+
* immediately; long enough that a one-tick transitional `preparing` during a
|
|
48
|
+
* boundary swap never paints at all (React's effect cleanup cancels the timer
|
|
49
|
+
* the instant `picture` moves on — see the effect below).
|
|
50
|
+
*/
|
|
51
|
+
const PREPARING_SHOW_DELAY_MS = 200
|
|
52
|
+
|
|
53
|
+
interface EngineSurfaceProps {
|
|
54
|
+
/**
|
|
55
|
+
* The hook's `attachCanvas`. Passed straight to `ref`, so it MUST be stable —
|
|
56
|
+
* a fresh identity each render makes React detach (`null`) and re-attach on
|
|
57
|
+
* every tick, which would unbind the engine's painter sixty times a second.
|
|
58
|
+
*/
|
|
59
|
+
attach: (canvas: HTMLCanvasElement | null) => void
|
|
60
|
+
picture: Picture
|
|
61
|
+
/** Why the picture is `preparing`, when it is. Shown as a hover title only — some reasons (decode error text, the src-precedence message) are not end-user copy. */
|
|
62
|
+
reason?: string
|
|
63
|
+
/** `engine.debugHud`, threaded from `PreviewPlayer`'s `engine` prop. Absent/false: no HUD, no polling. */
|
|
64
|
+
debugHud?: boolean
|
|
65
|
+
/** The hook's `getStats`. Read by the HUD on its own timer; unused when `debugHud` is falsy. */
|
|
66
|
+
getStats: () => EngineStats | null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default function EngineSurface({ attach, picture, reason, debugHud, getStats }: EngineSurfaceProps) {
|
|
70
|
+
const [showPreparing, setShowPreparing] = useState(false)
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (picture !== 'preparing') {
|
|
74
|
+
setShowPreparing(false)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
const timer = setTimeout(() => setShowPreparing(true), PREPARING_SHOW_DELAY_MS)
|
|
78
|
+
// Cleanup fires the instant `picture` changes away from `preparing` (or on
|
|
79
|
+
// unmount) — a swap that resolves inside the delay window never shows.
|
|
80
|
+
return () => clearTimeout(timer)
|
|
81
|
+
}, [picture])
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
<canvas
|
|
86
|
+
ref={attach}
|
|
87
|
+
data-montaj-engine-canvas=""
|
|
88
|
+
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', zIndex: 1 }}
|
|
89
|
+
/>
|
|
90
|
+
{showPreparing && (
|
|
91
|
+
<div
|
|
92
|
+
className="montaj-engine-preparing absolute inset-0 flex flex-col items-center justify-center gap-2 text-white/80"
|
|
93
|
+
style={{ zIndex: 2 }}
|
|
94
|
+
title={reason}
|
|
95
|
+
>
|
|
96
|
+
<Loader size="md" />
|
|
97
|
+
<span className="text-sm">Preparing preview…</span>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
{debugHud && <DebugHud getStats={getStats} />}
|
|
101
|
+
</>
|
|
102
|
+
)
|
|
103
|
+
}
|