@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,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP4 T5 — `sourceCrop` parity between the engine's `drawImage` rect and the
|
|
3
|
+
* legacy `<video>` CSS.
|
|
4
|
+
*
|
|
5
|
+
* `sourceCropStyle.ts` and its tests are the spec (plan decision 3: "the file
|
|
6
|
+
* and its tests stay as the spec"), so this suite does not re-derive the
|
|
7
|
+
* expected numbers — it runs `sourceCropVideoStyle` for each vector, works out
|
|
8
|
+
* where on the frame that CSS actually puts the crop sub-rect, and asserts the
|
|
9
|
+
* engine's destination rect lands in the same place. If either implementation
|
|
10
|
+
* drifts, the equality breaks; a shared bug in a shared formula cannot hide
|
|
11
|
+
* here because the two formulas are written differently (CSS oversizes the
|
|
12
|
+
* whole video and translates it; the canvas draws only the sub-rect).
|
|
13
|
+
*
|
|
14
|
+
* The vector table starts with the four cases `sourceCropStyle.test.ts` already
|
|
15
|
+
* pins, then adds the ones that file does not cover: the opposite aspect
|
|
16
|
+
* relationship, an offset crop, and the invalid/degenerate inputs.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, expect, it } from 'vitest'
|
|
19
|
+
import { sourceCropVideoStyle } from '../../video/preview/sourceCropStyle'
|
|
20
|
+
import { containFitPlan, drawPlanFor, sourceCropDrawPlan } from '../scheduler'
|
|
21
|
+
|
|
22
|
+
interface Vector {
|
|
23
|
+
name: string
|
|
24
|
+
crop: { x: number; y: number; w: number; h: number }
|
|
25
|
+
sourceWidth: number
|
|
26
|
+
sourceHeight: number
|
|
27
|
+
frameWidth: number
|
|
28
|
+
frameHeight: number
|
|
29
|
+
/** The engine decodes a PROXY — a different resolution, same aspect. */
|
|
30
|
+
codedWidth: number
|
|
31
|
+
codedHeight: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Vectors that must produce a crop on BOTH paths. */
|
|
35
|
+
const CROPPING: Vector[] = [
|
|
36
|
+
{
|
|
37
|
+
// sourceCropStyle.test.ts: "center vertical-strip crop of a landscape
|
|
38
|
+
// source fills a portrait frame".
|
|
39
|
+
name: 'centre vertical strip, landscape source → portrait frame',
|
|
40
|
+
crop: { x: 0.25, y: 0, w: 0.5, h: 1 },
|
|
41
|
+
sourceWidth: 1920,
|
|
42
|
+
sourceHeight: 1080,
|
|
43
|
+
frameWidth: 1080,
|
|
44
|
+
frameHeight: 1920,
|
|
45
|
+
codedWidth: 1280,
|
|
46
|
+
codedHeight: 720,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
// sourceCropStyle.test.ts: "crop region matching frame aspect fills the
|
|
50
|
+
// frame exactly (no letterbox)".
|
|
51
|
+
name: 'crop matching frame aspect, no letterbox',
|
|
52
|
+
crop: { x: 0.1, y: 0.1, w: 0.8, h: 0.8 },
|
|
53
|
+
sourceWidth: 1080,
|
|
54
|
+
sourceHeight: 1920,
|
|
55
|
+
frameWidth: 1080,
|
|
56
|
+
frameHeight: 1920,
|
|
57
|
+
codedWidth: 720,
|
|
58
|
+
codedHeight: 1280,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
// The `cropAspect < frameAspect` branch, which neither existing vector
|
|
62
|
+
// exercises: a tall sliver of a portrait source inside a landscape frame,
|
|
63
|
+
// so the crop fills the frame's HEIGHT and pillarboxes horizontally.
|
|
64
|
+
name: 'tall sliver, portrait source → landscape frame (pillarboxed)',
|
|
65
|
+
crop: { x: 0.4, y: 0, w: 0.2, h: 1 },
|
|
66
|
+
sourceWidth: 1080,
|
|
67
|
+
sourceHeight: 1920,
|
|
68
|
+
frameWidth: 1920,
|
|
69
|
+
frameHeight: 1080,
|
|
70
|
+
codedWidth: 406,
|
|
71
|
+
codedHeight: 720,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'offset crop in both axes',
|
|
75
|
+
crop: { x: 0.3, y: 0.45, w: 0.4, h: 0.35 },
|
|
76
|
+
sourceWidth: 3840,
|
|
77
|
+
sourceHeight: 2160,
|
|
78
|
+
frameWidth: 1080,
|
|
79
|
+
frameHeight: 1920,
|
|
80
|
+
codedWidth: 1280,
|
|
81
|
+
codedHeight: 720,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'crop touching the far edge',
|
|
85
|
+
crop: { x: 0.5, y: 0.5, w: 0.5, h: 0.5 },
|
|
86
|
+
sourceWidth: 1920,
|
|
87
|
+
sourceHeight: 1080,
|
|
88
|
+
frameWidth: 1080,
|
|
89
|
+
frameHeight: 1080,
|
|
90
|
+
codedWidth: 1280,
|
|
91
|
+
codedHeight: 720,
|
|
92
|
+
},
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
/** Vectors both paths must decline to crop. */
|
|
96
|
+
const NO_CROP: Vector[] = [
|
|
97
|
+
{
|
|
98
|
+
// sourceCropStyle.test.ts: "returns null for a full-frame (default) crop".
|
|
99
|
+
name: 'full-frame default crop',
|
|
100
|
+
crop: { x: 0, y: 0, w: 1, h: 1 },
|
|
101
|
+
sourceWidth: 1920,
|
|
102
|
+
sourceHeight: 1080,
|
|
103
|
+
frameWidth: 1080,
|
|
104
|
+
frameHeight: 1920,
|
|
105
|
+
codedWidth: 1280,
|
|
106
|
+
codedHeight: 720,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
// sourceCropStyle.test.ts: "returns null without source dims". THE parity
|
|
110
|
+
// guard: KNOWN-DIVERGENCES entry 8 — render drops the crop filter entirely
|
|
111
|
+
// when the item carries no dims, and both preview paths follow it.
|
|
112
|
+
name: 'no item source dims',
|
|
113
|
+
crop: { x: 0.25, y: 0, w: 0.5, h: 1 },
|
|
114
|
+
sourceWidth: 0,
|
|
115
|
+
sourceHeight: 0,
|
|
116
|
+
frameWidth: 1080,
|
|
117
|
+
frameHeight: 1920,
|
|
118
|
+
codedWidth: 1280,
|
|
119
|
+
codedHeight: 720,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'zero-width crop',
|
|
123
|
+
crop: { x: 0.25, y: 0, w: 0, h: 1 },
|
|
124
|
+
sourceWidth: 1920,
|
|
125
|
+
sourceHeight: 1080,
|
|
126
|
+
frameWidth: 1080,
|
|
127
|
+
frameHeight: 1920,
|
|
128
|
+
codedWidth: 1280,
|
|
129
|
+
codedHeight: 720,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'negative-height crop',
|
|
133
|
+
crop: { x: 0.25, y: 0, w: 0.5, h: -0.2 },
|
|
134
|
+
sourceWidth: 1920,
|
|
135
|
+
sourceHeight: 1080,
|
|
136
|
+
frameWidth: 1080,
|
|
137
|
+
frameHeight: 1920,
|
|
138
|
+
codedWidth: 1280,
|
|
139
|
+
codedHeight: 720,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'unmeasured frame box',
|
|
143
|
+
crop: { x: 0.25, y: 0, w: 0.5, h: 1 },
|
|
144
|
+
sourceWidth: 1920,
|
|
145
|
+
sourceHeight: 1080,
|
|
146
|
+
frameWidth: 0,
|
|
147
|
+
frameHeight: 0,
|
|
148
|
+
codedWidth: 1280,
|
|
149
|
+
codedHeight: 720,
|
|
150
|
+
},
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Where the CSS actually puts the crop sub-rect, in frame pixels.
|
|
155
|
+
*
|
|
156
|
+
* The `<video>` is sized `width`/`height` and positioned `left`/`top`, all as
|
|
157
|
+
* percentages of the frame; the crop sub-rect is `crop.x/y/w/h` of THAT box
|
|
158
|
+
* (the element is `object-fit: fill`, so source ratios map linearly onto it).
|
|
159
|
+
*/
|
|
160
|
+
function cssCropBox(
|
|
161
|
+
style: Record<string, unknown>,
|
|
162
|
+
crop: { x: number; y: number; w: number; h: number },
|
|
163
|
+
frameWidth: number,
|
|
164
|
+
frameHeight: number,
|
|
165
|
+
) {
|
|
166
|
+
const pct = (v: unknown) => parseFloat(String(v)) / 100
|
|
167
|
+
const videoW = pct(style.width) * frameWidth
|
|
168
|
+
const videoH = pct(style.height) * frameHeight
|
|
169
|
+
const left = pct(style.left) * frameWidth
|
|
170
|
+
const top = pct(style.top) * frameHeight
|
|
171
|
+
return {
|
|
172
|
+
x: left + crop.x * videoW,
|
|
173
|
+
y: top + crop.y * videoH,
|
|
174
|
+
w: crop.w * videoW,
|
|
175
|
+
h: crop.h * videoH,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
describe('sourceCropDrawPlan — parity with sourceCropStyle.ts', () => {
|
|
180
|
+
for (const v of CROPPING) {
|
|
181
|
+
it(`places the crop sub-rect exactly where the CSS does: ${v.name}`, () => {
|
|
182
|
+
const style = sourceCropVideoStyle({
|
|
183
|
+
crop: v.crop,
|
|
184
|
+
sourceWidth: v.sourceWidth,
|
|
185
|
+
sourceHeight: v.sourceHeight,
|
|
186
|
+
frameWidth: v.frameWidth,
|
|
187
|
+
frameHeight: v.frameHeight,
|
|
188
|
+
})
|
|
189
|
+
expect(style).not.toBeNull()
|
|
190
|
+
|
|
191
|
+
const plan = sourceCropDrawPlan(v)
|
|
192
|
+
expect(plan).not.toBeNull()
|
|
193
|
+
|
|
194
|
+
const css = cssCropBox(
|
|
195
|
+
style as unknown as Record<string, unknown>,
|
|
196
|
+
v.crop,
|
|
197
|
+
v.frameWidth,
|
|
198
|
+
v.frameHeight,
|
|
199
|
+
)
|
|
200
|
+
// 1e-6 of a frame pixel: the two formulas are algebraically identical, so
|
|
201
|
+
// only float association separates them.
|
|
202
|
+
expect(plan!.dx).toBeCloseTo(css.x, 6)
|
|
203
|
+
expect(plan!.dy).toBeCloseTo(css.y, 6)
|
|
204
|
+
expect(plan!.dw).toBeCloseTo(css.w, 6)
|
|
205
|
+
expect(plan!.dh).toBeCloseTo(css.h, 6)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it(`reads the crop sub-rect out of the decoded frame: ${v.name}`, () => {
|
|
209
|
+
const plan = sourceCropDrawPlan(v)!
|
|
210
|
+
// Source rect is expressed against the DECODED (proxy) dims, not the
|
|
211
|
+
// item's original `sourceWidth`/`sourceHeight` — `sourceCrop` is stored
|
|
212
|
+
// as ratios of the source precisely so it survives the re-encode.
|
|
213
|
+
expect(plan.sx).toBeCloseTo(v.crop.x * v.codedWidth, 9)
|
|
214
|
+
expect(plan.sy).toBeCloseTo(v.crop.y * v.codedHeight, 9)
|
|
215
|
+
expect(plan.sw).toBeCloseTo(v.crop.w * v.codedWidth, 9)
|
|
216
|
+
expect(plan.sh).toBeCloseTo(v.crop.h * v.codedHeight, 9)
|
|
217
|
+
// …and it stays inside the frame.
|
|
218
|
+
expect(plan.sx + plan.sw).toBeLessThanOrEqual(v.codedWidth + 1e-9)
|
|
219
|
+
expect(plan.sy + plan.sh).toBeLessThanOrEqual(v.codedHeight + 1e-9)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it(`fits the crop inside the frame with at most one letterbox axis: ${v.name}`, () => {
|
|
223
|
+
const plan = sourceCropDrawPlan(v)!
|
|
224
|
+
expect(plan.dx).toBeGreaterThanOrEqual(-1e-9)
|
|
225
|
+
expect(plan.dy).toBeGreaterThanOrEqual(-1e-9)
|
|
226
|
+
expect(plan.dx + plan.dw).toBeLessThanOrEqual(v.frameWidth + 1e-9)
|
|
227
|
+
expect(plan.dy + plan.dh).toBeLessThanOrEqual(v.frameHeight + 1e-9)
|
|
228
|
+
// Contain-fit: exactly one axis is flush unless the aspects match.
|
|
229
|
+
const flushW = Math.abs(plan.dw - v.frameWidth) < 1e-9
|
|
230
|
+
const flushH = Math.abs(plan.dh - v.frameHeight) < 1e-9
|
|
231
|
+
expect(flushW || flushH).toBe(true)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const v of NO_CROP) {
|
|
236
|
+
it(`declines to crop wherever the CSS declines: ${v.name}`, () => {
|
|
237
|
+
const style = sourceCropVideoStyle({
|
|
238
|
+
crop: v.crop,
|
|
239
|
+
sourceWidth: v.sourceWidth,
|
|
240
|
+
sourceHeight: v.sourceHeight,
|
|
241
|
+
frameWidth: v.frameWidth,
|
|
242
|
+
frameHeight: v.frameHeight,
|
|
243
|
+
})
|
|
244
|
+
expect(style).toBeNull()
|
|
245
|
+
expect(sourceCropDrawPlan(v)).toBeNull()
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
it('declines when the item carries no sourceCrop at all', () => {
|
|
250
|
+
expect(
|
|
251
|
+
sourceCropDrawPlan({
|
|
252
|
+
crop: undefined,
|
|
253
|
+
sourceWidth: 1920,
|
|
254
|
+
sourceHeight: 1080,
|
|
255
|
+
codedWidth: 1280,
|
|
256
|
+
codedHeight: 720,
|
|
257
|
+
frameWidth: 1080,
|
|
258
|
+
frameHeight: 1920,
|
|
259
|
+
}),
|
|
260
|
+
).toBeNull()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('declines when the decoded frame has no dims yet', () => {
|
|
264
|
+
expect(
|
|
265
|
+
sourceCropDrawPlan({
|
|
266
|
+
crop: { x: 0.25, y: 0, w: 0.5, h: 1 },
|
|
267
|
+
sourceWidth: 1920,
|
|
268
|
+
sourceHeight: 1080,
|
|
269
|
+
codedWidth: 0,
|
|
270
|
+
codedHeight: 0,
|
|
271
|
+
frameWidth: 1080,
|
|
272
|
+
frameHeight: 1920,
|
|
273
|
+
}),
|
|
274
|
+
).toBeNull()
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
describe('containFitPlan — the no-crop default', () => {
|
|
279
|
+
it('letterboxes a landscape source into a portrait frame', () => {
|
|
280
|
+
// PreviewPlayer's `baseVideoStyle` fallback is `objectFit: 'contain'` over
|
|
281
|
+
// the whole frame; this is that, as a rect.
|
|
282
|
+
const plan = containFitPlan(1280, 720, 1080, 1920)
|
|
283
|
+
expect(plan.sx).toBe(0)
|
|
284
|
+
expect(plan.sy).toBe(0)
|
|
285
|
+
expect(plan.sw).toBe(1280)
|
|
286
|
+
expect(plan.sh).toBe(720)
|
|
287
|
+
expect(plan.dw).toBeCloseTo(1080, 9)
|
|
288
|
+
expect(plan.dh).toBeCloseTo(607.5, 9)
|
|
289
|
+
expect(plan.dx).toBeCloseTo(0, 9)
|
|
290
|
+
expect(plan.dy).toBeCloseTo((1920 - 607.5) / 2, 9)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('pillarboxes a portrait source into a landscape frame', () => {
|
|
294
|
+
const plan = containFitPlan(720, 1280, 1920, 1080)
|
|
295
|
+
expect(plan.dh).toBeCloseTo(1080, 9)
|
|
296
|
+
expect(plan.dw).toBeCloseTo(607.5, 9)
|
|
297
|
+
expect(plan.dy).toBeCloseTo(0, 9)
|
|
298
|
+
expect(plan.dx).toBeCloseTo((1920 - 607.5) / 2, 9)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('fills exactly when the aspects match', () => {
|
|
302
|
+
const plan = containFitPlan(720, 1280, 1080, 1920)
|
|
303
|
+
expect(plan.dx).toBeCloseTo(0, 9)
|
|
304
|
+
expect(plan.dy).toBeCloseTo(0, 9)
|
|
305
|
+
expect(plan.dw).toBeCloseTo(1080, 9)
|
|
306
|
+
expect(plan.dh).toBeCloseTo(1920, 9)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('degrades to an empty rect rather than NaN when nothing is measured', () => {
|
|
310
|
+
expect(containFitPlan(0, 0, 1080, 1920)).toEqual({
|
|
311
|
+
sx: 0,
|
|
312
|
+
sy: 0,
|
|
313
|
+
sw: 0,
|
|
314
|
+
sh: 0,
|
|
315
|
+
dx: 0,
|
|
316
|
+
dy: 0,
|
|
317
|
+
dw: 0,
|
|
318
|
+
dh: 0,
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
describe('drawPlanFor — crop when it can, contain when it cannot', () => {
|
|
324
|
+
const item = {
|
|
325
|
+
sourceCrop: { x: 0.25, y: 0, w: 0.5, h: 1 },
|
|
326
|
+
sourceWidth: 1920,
|
|
327
|
+
sourceHeight: 1080,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
it('uses the crop rect when the item is fully specified', () => {
|
|
331
|
+
expect(drawPlanFor(item, 1280, 720, 1080, 1920)).toEqual(
|
|
332
|
+
sourceCropDrawPlan({
|
|
333
|
+
crop: item.sourceCrop,
|
|
334
|
+
sourceWidth: item.sourceWidth,
|
|
335
|
+
sourceHeight: item.sourceHeight,
|
|
336
|
+
codedWidth: 1280,
|
|
337
|
+
codedHeight: 720,
|
|
338
|
+
frameWidth: 1080,
|
|
339
|
+
frameHeight: 1920,
|
|
340
|
+
}),
|
|
341
|
+
)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('falls back to contain-fit when the item has a crop but no dims', () => {
|
|
345
|
+
const plan = drawPlanFor({ sourceCrop: item.sourceCrop }, 1280, 720, 1080, 1920)
|
|
346
|
+
expect(plan).toEqual(containFitPlan(1280, 720, 1080, 1920))
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('falls back to contain-fit when the item has no crop', () => {
|
|
350
|
+
const plan = drawPlanFor({ sourceWidth: 1920, sourceHeight: 1080 }, 1280, 720, 1080, 1920)
|
|
351
|
+
expect(plan).toEqual(containFitPlan(1280, 720, 1080, 1920))
|
|
352
|
+
})
|
|
353
|
+
})
|