@leavepulse/ui 0.81.0 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/LpVideoTrim-Ci6IIHW0.css +8 -0
- package/dist/chunks/{ContextMenuItems-DLK4q-mm.js → ContextMenuItems-kRVTuSZd.js} +1 -1
- package/dist/chunks/{DropzoneLinkField-CcJu8De3.js → DropzoneLinkField-CvMUu8HQ.js} +1 -1
- package/dist/chunks/{FieldLeading-CldGLINn.js → FieldLeading-CNN_5Dp1.js} +1 -1
- package/dist/chunks/{FieldWrapper-Cj8ZgoKp.js → FieldWrapper-BBJPEVHr.js} +1 -1
- package/dist/chunks/{IconCloseButton-CV4iHRpy.js → IconCloseButton-C59_etp2.js} +1 -1
- package/dist/chunks/{LayoutCanvas-BQbq7Ekw.js → LayoutCanvas-BIY74SUe.js} +1 -1
- package/dist/chunks/{LayoutNode-BMedRk3g.js → LayoutNode-CTlKlhb-.js} +1 -1
- package/dist/chunks/{LpActionBar-C7g52Cyo.js → LpActionBar-Cowb-28D.js} +1 -1
- package/dist/chunks/{LpAddressInput-1uakhQk5.js → LpAddressInput-nTJlUQ6K.js} +7 -7
- package/dist/chunks/{LpAlert-DWFeR82L.js → LpAlert-DhZdjekN.js} +1 -1
- package/dist/chunks/{LpAppShell-BBINfN1v.js → LpAppShell-CK9hTgQ1.js} +4 -4
- package/dist/chunks/{LpAutocomplete-DY3LvpB9.js → LpAutocomplete--PaZkQoU.js} +4 -4
- package/dist/chunks/{LpAvatar-CEZmMcGh.js → LpAvatar-DVCld_L2.js} +1 -1
- package/dist/chunks/{LpBadge-C_VqLHtg.js → LpBadge-DXvpxk6z.js} +1 -1
- package/dist/chunks/{LpBottomNav-4I89sPDZ.js → LpBottomNav-COQiBNUN.js} +1 -1
- package/dist/chunks/{LpBreadcrumbs-1BBzrfxG.js → LpBreadcrumbs-Dt1O5G4O.js} +1 -1
- package/dist/chunks/{LpCalendar-D2dcXjUK.js → LpCalendar-C8Xlukd2.js} +1 -1
- package/dist/chunks/{LpCheckbox-CN-MEnbX.js → LpCheckbox-Dpybd3oV.js} +2 -2
- package/dist/chunks/{LpCodeBlock-Cj7zk4QE.js → LpCodeBlock-C1Hu5RyT.js} +2 -2
- package/dist/chunks/{LpCodeDisplay-COzosm77.js → LpCodeDisplay-BVruT3-A.js} +4 -4
- package/dist/chunks/{LpColorPicker-BezW6DX1.js → LpColorPicker-BK-NRaGK.js} +3 -3
- package/dist/chunks/{LpConfirmDialog-D9GkDMYr.js → LpConfirmDialog-2CQFKtqU.js} +2 -2
- package/dist/chunks/{LpConfirmHost-BH7kf0dm.js → LpConfirmHost-CTkSytVl.js} +1 -1
- package/dist/chunks/{LpContextMenu-bn5bc9t7.js → LpContextMenu-DcHPdW-t.js} +2 -2
- package/dist/chunks/{LpContextSwitcher-CGr6ZAuq.js → LpContextSwitcher-C_nnDbeI.js} +2 -2
- package/dist/chunks/{LpCopyButton-BSr6YaFR.js → LpCopyButton-De7_elaO.js} +1 -1
- package/dist/chunks/{LpDataList-Cv7ZUlyy.js → LpDataList-BjZPfQjO.js} +2 -2
- package/dist/chunks/{LpDatePicker-CR7vsb5M.js → LpDatePicker-wOrHd0fg.js} +5 -5
- package/dist/chunks/{LpDateRangePicker-B9fRO3G6.js → LpDateRangePicker-CVMvvo5t.js} +3 -3
- package/dist/chunks/{LpDisclosure-CE4mneaE.js → LpDisclosure-CtQ63LeO.js} +1 -1
- package/dist/chunks/{LpDrawer-DU_5MqBz.js → LpDrawer-TZBPzyOM.js} +1 -1
- package/dist/chunks/{LpDropdownMenu-_u6GPWhp.js → LpDropdownMenu-DL9LgkS8.js} +2 -2
- package/dist/chunks/{LpEditable-DYoMayjt.js → LpEditable-DuPE2tkl.js} +2 -2
- package/dist/chunks/{LpEmptyState-DpdHeSfz.js → LpEmptyState-W1U_tKg-.js} +1 -1
- package/dist/chunks/{LpFieldNotice-DONFCWV7.js → LpFieldNotice-BLKlYLwu.js} +1 -1
- package/dist/chunks/{LpFileTree-f5ktGqsN.js → LpFileTree-CEiRIpj1.js} +4 -4
- package/dist/chunks/{LpFileTreeNode-nbp-JwV9.js → LpFileTreeNode-C0292AU2.js} +4 -4
- package/dist/chunks/{LpFormField-Bz50_LZW.js → LpFormField-Bhgi0idH.js} +1 -1
- package/dist/chunks/{LpHintLayer-B6bA5Cb_.js → LpHintLayer-D8ZKsvsT.js} +1 -1
- package/dist/chunks/{LpHints-DyJ5hVcr.js → LpHints-DbIV-wk4.js} +3 -3
- package/dist/chunks/{LpIcon-BCQLuHSP.js → LpIcon-CytpcLmS.js} +1 -1
- package/dist/chunks/{LpInfraNode-BS1RXYAs.js → LpInfraNode-8cGoY87Q.js} +2 -2
- package/dist/chunks/{LpInput-BjbXdjcW.js → LpInput-P2C12X7s.js} +3 -3
- package/dist/chunks/{LpInspector-BIyJyM6r.js → LpInspector-DjfMa4tD.js} +4 -4
- package/dist/chunks/{LpLightbox-CSAJuUaJ.js → LpLightbox-BEQtCz0m.js} +2 -2
- package/dist/chunks/{LpLogInspector-OJEz1vod.js → LpLogInspector-CyxK1d-V.js} +2 -2
- package/dist/chunks/{LpLogViewer-CRLwQpk0.js → LpLogViewer-CZrc_HG7.js} +6 -6
- package/dist/chunks/{LpMapPicker-DW2EnrCJ.js → LpMapPicker-CbTfFcEu.js} +1 -1
- package/dist/chunks/{LpMediaGallery-BNEYctDP.js → LpMediaGallery-C_SwNR4y.js} +3 -3
- package/dist/chunks/{LpModal-DdTqdf4x.js → LpModal-B6Yt0G7E.js} +1 -1
- package/dist/chunks/{LpModalNav-BL7Dp0om.js → LpModalNav-CMYWEiUs.js} +3 -3
- package/dist/chunks/{LpNotificationBell-d0F6MWxD.js → LpNotificationBell-BJKPx0X1.js} +4 -4
- package/dist/chunks/{LpNotificationFeed-BIe5lapb.js → LpNotificationFeed-orWISVez.js} +4 -4
- package/dist/chunks/{LpNumberField-D5Dx-hzD.js → LpNumberField-C5MeLsoE.js} +3 -3
- package/dist/chunks/{LpOtpInput-BVl74Lfl.js → LpOtpInput-6SCQ64uK.js} +1 -1
- package/dist/chunks/{LpPagination-D0wb-Uru.js → LpPagination-CkwbdFa9.js} +1 -1
- package/dist/chunks/{LpPaletteTheme-DdilCBXA.js → LpPaletteTheme-RWgbWFOz.js} +2 -2
- package/dist/chunks/{LpPasswordInput-DUtqWMkA.js → LpPasswordInput-DJBRo5lt.js} +2 -2
- package/dist/chunks/{LpPhoneInput-CalRjkqc.js → LpPhoneInput-DcmcJwos.js} +3 -3
- package/dist/chunks/{LpPie-BVAEmVBT.js → LpPie-DmMlhgo2.js} +2 -2
- package/dist/chunks/{LpPipeline-Dk7_sRP1.js → LpPipeline-BXcp2shV.js} +2 -2
- package/dist/chunks/{LpQuota-DKx0IZYA.js → LpQuota-CtEiqRg4.js} +1 -1
- package/dist/chunks/{LpRadioGroup-E3SP6j9h.js → LpRadioGroup-Ubkk_eSq.js} +2 -2
- package/dist/chunks/{LpRangeCalendar-BFJFaen4.js → LpRangeCalendar-DCswVYbU.js} +1 -1
- package/dist/chunks/{LpRangeSlider-b6ipywRB.js → LpRangeSlider-DM1qy5Tp.js} +1 -1
- package/dist/chunks/{LpRepeater-BLrw62Bh.js → LpRepeater-BMo2Eabn.js} +2 -2
- package/dist/chunks/{LpSecretField-BV8BNjKc.js → LpSecretField-SSRGVqRc.js} +5 -5
- package/dist/chunks/{LpSection-CghNNxIv.js → LpSection-C3bTxD7t.js} +1 -1
- package/dist/chunks/{LpSegmented-DjvrHL_G.js → LpSegmented-C1DF6m6A.js} +2 -2
- package/dist/chunks/{LpSelect-DYPimzAZ.js → LpSelect-DvDr1VPn.js} +4 -4
- package/dist/chunks/{LpSidebar-D3S8rYFf.js → LpSidebar-Bql881r6.js} +4 -4
- package/dist/chunks/{LpSidebarNav-Dlse8vxT.js → LpSidebarNav-CKx2KJvr.js} +3 -3
- package/dist/chunks/{LpSidebarRow-Cqd_6tGG.js → LpSidebarRow-BMOL9kOG.js} +3 -3
- package/dist/chunks/{LpSlider-B9XIC75O.js → LpSlider-DsSMe4b5.js} +1 -1
- package/dist/chunks/{LpStat-kyLRmX-m.js → LpStat-CQYYDMS6.js} +2 -2
- package/dist/chunks/{LpStatus-szB2dWzM.js → LpStatus-g5nSJfU3.js} +1 -1
- package/dist/chunks/{LpStepMark-DP3CDSm9.js → LpStepMark-seVNOuNJ.js} +1 -1
- package/dist/chunks/{LpStepper-BS3iaJIz.js → LpStepper-D6qEXihZ.js} +1 -1
- package/dist/chunks/{LpSwitch-DRFy8RNr.js → LpSwitch-DM5CVm31.js} +1 -1
- package/dist/chunks/{LpTabFace-C9-OperL.js → LpTabFace-ZWwelqRU.js} +2 -2
- package/dist/chunks/{LpTableFilter-CEIwLupk.js → LpTableFilter-CWU9Dmk-.js} +2 -2
- package/dist/chunks/{LpTagsInput-ClDeF_aU.js → LpTagsInput-C2qVF2ax.js} +5 -5
- package/dist/chunks/{LpTextarea-DEY6pUBy.js → LpTextarea-C3EoV_xv.js} +1 -1
- package/dist/chunks/{LpThemeStudio-WY72Z1Db.js → LpThemeStudio-BIBahaIs.js} +5 -5
- package/dist/chunks/{LpThemeSwitcher-9uCH9c-K.js → LpThemeSwitcher-D4MqFLGS.js} +3 -3
- package/dist/chunks/{LpTimeField-BmgxCzQO.js → LpTimeField-DPOkbvyP.js} +2 -2
- package/dist/chunks/{LpToaster-DLKO-efn.js → LpToaster-Dj1VKxFv.js} +2 -2
- package/dist/chunks/{LpTopologyCanvas-B-PhPa7k.js → LpTopologyCanvas-Bspzwm7x.js} +4 -4
- package/dist/chunks/{LpTour-ODlBcat2.js → LpTour-CwpVdVN-.js} +1 -1
- package/dist/chunks/{LpTruncate--nhT0iUo.js → LpTruncate-JSaIW25q.js} +1 -1
- package/dist/chunks/{LpWindow-DJxE8Zl-.js → LpWindow-RVcP436Z.js} +2 -2
- package/dist/chunks/{LpWorkspaceSwitcher-Bpf41Bw5.js → LpWorkspaceSwitcher-D0WOz1Fg.js} +3 -3
- package/dist/chunks/{MenuTitle-CMjvLsvC.js → MenuTitle-D8Umab23.js} +1 -1
- package/dist/chunks/{OptionContent-DY8NaG8s.js → OptionContent-DCnX_KRK.js} +1 -1
- package/dist/chunks/{PickerField-CgfkaBgg.js → PickerField-ChjY27bm.js} +1 -1
- package/dist/chunks/{RevealToggle-CPMXxaKv.js → RevealToggle-MaL7PEjr.js} +1 -1
- package/dist/chunks/{TourCard-D4b3lMHi.js → TourCard-oXZSpHKb.js} +1 -1
- package/dist/chunks/{icons-C1vppR4f.js → icons-BOdqDUii.js} +1 -1
- package/dist/component-names.d.ts +1 -1
- package/dist/components/ContextMenuItems.vue.js +1 -1
- package/dist/components/DropzoneLinkField.vue.js +1 -1
- package/dist/components/FieldLeading.vue.js +1 -1
- package/dist/components/FieldWrapper.vue.js +1 -1
- package/dist/components/IconCloseButton.vue.js +1 -1
- package/dist/components/LayoutCanvas.vue.js +1 -1
- package/dist/components/LayoutNode.vue.js +1 -1
- package/dist/components/LpActionBar.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBadge.vue.js +1 -1
- package/dist/components/LpBottomNav.vue.js +1 -1
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpCalendar.vue.js +1 -1
- package/dist/components/LpChart.vue.js +2 -2
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpCodeDisplay.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +2 -2
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpContextSwitcher.vue.js +1 -1
- package/dist/components/LpCopyButton.vue.js +1 -1
- package/dist/components/LpDataList.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.js +1 -1
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpDock.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +2 -2
- package/dist/components/LpEditable.vue.js +1 -1
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFieldNotice.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpHintLayer.vue.js +1 -1
- package/dist/components/LpHints.vue.js +1 -1
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +1 -1
- package/dist/components/LpInfraNode.vue.js +1 -1
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpInspector.vue.js +1 -1
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLogInspector.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNotificationFeed.vue.js +1 -1
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpOtpInput.vue.js +1 -1
- package/dist/components/LpPagination.vue.js +1 -1
- package/dist/components/LpPaletteFacts.vue.js +1 -1
- package/dist/components/LpPaletteHeader.vue.js +2 -2
- package/dist/components/LpPaletteTheme.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPipeline.vue.js +1 -1
- package/dist/components/LpQuota.vue.js +1 -1
- package/dist/components/LpRadioGroup.vue.js +1 -1
- package/dist/components/LpRangeCalendar.vue.js +1 -1
- package/dist/components/LpRangeSlider.vue.js +1 -1
- package/dist/components/LpRepeater.vue.js +1 -1
- package/dist/components/LpSecretField.vue.js +1 -1
- package/dist/components/LpSection.vue.js +1 -1
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpSidebarRow.vue.js +1 -1
- package/dist/components/LpSlider.vue.js +1 -1
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpStatus.vue.js +1 -1
- package/dist/components/LpStepMark.vue.js +1 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpSwitch.vue.js +1 -1
- package/dist/components/LpTabFace.vue.js +1 -1
- package/dist/components/LpTable.vue.js +9 -9
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTabs.vue.js +2 -2
- package/dist/components/LpTagsInput.vue.js +1 -1
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeStudio.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpTour.vue.js +1 -1
- package/dist/components/LpTruncate.vue.js +1 -1
- package/dist/components/LpVideoTrim.vue.d.ts +68 -0
- package/dist/components/LpVideoTrim.vue.js +955 -0
- package/dist/components/LpWindow.vue.js +1 -1
- package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
- package/dist/components/MenuTitle.vue.js +1 -1
- package/dist/components/OptionContent.vue.js +1 -1
- package/dist/components/PickerField.vue.js +1 -1
- package/dist/components/RevealToggle.vue.js +1 -1
- package/dist/components/TourCard.vue.js +1 -1
- package/dist/components/chartWindow.js +4 -8
- package/dist/components/menuWhen.js +1 -1
- package/dist/components/videoFrames.d.ts +37 -0
- package/dist/components/videoFrames.js +134 -0
- package/dist/components/videoTrim.d.ts +75 -0
- package/dist/components/videoTrim.js +127 -0
- package/dist/composables/span.d.ts +10 -0
- package/dist/composables/span.js +15 -0
- package/dist/icons-all.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +95 -88
- package/package.json +1 -1
- package/src/component-names.ts +1 -0
- package/src/components/LpVideoTrim.vue +924 -0
- package/src/components/chartWindow.ts +4 -8
- package/src/components/videoFrames.ts +171 -0
- package/src/components/videoTrim.ts +198 -0
- package/src/composables/span.ts +21 -0
- package/src/index.ts +3 -0
- package/src/kit-icons.ts +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { clamp } from "../composables/clamp.js";
|
|
2
|
+
import { shiftSpan, zoomSpan } from "../composables/span.js";
|
|
2
3
|
const MIN_RANGE_POINTS = 2;
|
|
3
4
|
const SLOTS_PER_BUCKET = 2;
|
|
4
5
|
const LTTB_MIN_TARGET = 3;
|
|
@@ -16,16 +17,11 @@ function clampRange(range, count) {
|
|
|
16
17
|
}
|
|
17
18
|
function zoomRange(range, count, anchor, factor) {
|
|
18
19
|
const from = range ?? { start: 0, end: count - 1 };
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const at = from.start + span * clamp(anchor, 0, 1);
|
|
22
|
-
const start = at - (at - from.start) * (next / (span || 1));
|
|
23
|
-
return clampRange({ start, end: start + next }, count);
|
|
20
|
+
const at = from.start + (from.end - from.start) * clamp(anchor, 0, 1);
|
|
21
|
+
return clampRange(zoomSpan(from.start, from.end, at, factor, MIN_RANGE_POINTS - 1), count);
|
|
24
22
|
}
|
|
25
23
|
function panRange(range, count, delta) {
|
|
26
|
-
|
|
27
|
-
const start = Math.max(0, Math.min(count - 1 - span, range.start + delta));
|
|
28
|
-
return clampRange({ start, end: start + span }, count);
|
|
24
|
+
return clampRange(shiftSpan(range.start, range.end, delta, 0, count - 1), count);
|
|
29
25
|
}
|
|
30
26
|
function minMaxBuckets(series, slots) {
|
|
31
27
|
const count = series.reduce((n, values2) => Math.max(n, values2.length), 0);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface FrameRequest {
|
|
2
|
+
key: string;
|
|
3
|
+
time: number;
|
|
4
|
+
}
|
|
5
|
+
type VideoWithFrames = HTMLVideoElement & {
|
|
6
|
+
requestVideoFrameCallback?: (cb: () => void) => number;
|
|
7
|
+
fastSeek?: (time: number) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare class FrameSampler {
|
|
10
|
+
private readonly video;
|
|
11
|
+
private readonly size;
|
|
12
|
+
private readonly onFrame;
|
|
13
|
+
private queue;
|
|
14
|
+
private running;
|
|
15
|
+
private disposed;
|
|
16
|
+
private cache;
|
|
17
|
+
constructor(video: VideoWithFrames, size: () => {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
}, onFrame: (key: string, canvas: HTMLCanvasElement) => void);
|
|
21
|
+
/** A frame taken earlier, if it is still kept. */
|
|
22
|
+
cached(key: string): HTMLCanvasElement | undefined;
|
|
23
|
+
/** The frames wanted now, most wanted first; whatever was queued before is dropped. */
|
|
24
|
+
want(requests: FrameRequest[]): void;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
private pump;
|
|
27
|
+
private take;
|
|
28
|
+
private framePresented;
|
|
29
|
+
private once;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Seconds one still stands for at this zoom: the shortest power of two that
|
|
33
|
+
* keeps a still at least `minPx` wide. Stills sit at fixed points in time, so
|
|
34
|
+
* panning reuses the ones already taken and a zoom step of two keeps half.
|
|
35
|
+
*/
|
|
36
|
+
export declare function stillSpan(secondsPerPx: number, minPx: number): number;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const SEEK_TIMEOUT_MS = 4e3;
|
|
2
|
+
const FRAME_TIMEOUT_MS = 250;
|
|
3
|
+
const CACHE_LIMIT = 240;
|
|
4
|
+
class FrameSampler {
|
|
5
|
+
constructor(video, size, onFrame) {
|
|
6
|
+
this.video = video;
|
|
7
|
+
this.size = size;
|
|
8
|
+
this.onFrame = onFrame;
|
|
9
|
+
}
|
|
10
|
+
video;
|
|
11
|
+
size;
|
|
12
|
+
onFrame;
|
|
13
|
+
queue = [];
|
|
14
|
+
running = false;
|
|
15
|
+
disposed = false;
|
|
16
|
+
cache = /* @__PURE__ */ new Map();
|
|
17
|
+
/** A frame taken earlier, if it is still kept. */
|
|
18
|
+
cached(key) {
|
|
19
|
+
const hit = this.cache.get(key);
|
|
20
|
+
if (hit) {
|
|
21
|
+
this.cache.delete(key);
|
|
22
|
+
this.cache.set(key, hit);
|
|
23
|
+
}
|
|
24
|
+
return hit;
|
|
25
|
+
}
|
|
26
|
+
/** The frames wanted now, most wanted first; whatever was queued before is dropped. */
|
|
27
|
+
want(requests) {
|
|
28
|
+
this.queue = requests.filter((r) => !this.cache.has(r.key));
|
|
29
|
+
void this.pump();
|
|
30
|
+
}
|
|
31
|
+
dispose() {
|
|
32
|
+
this.disposed = true;
|
|
33
|
+
this.queue = [];
|
|
34
|
+
this.cache.clear();
|
|
35
|
+
}
|
|
36
|
+
async pump() {
|
|
37
|
+
if (this.running) return;
|
|
38
|
+
this.running = true;
|
|
39
|
+
try {
|
|
40
|
+
while (!this.disposed && this.queue.length) {
|
|
41
|
+
const next = this.queue.shift();
|
|
42
|
+
if (this.cache.has(next.key)) continue;
|
|
43
|
+
const canvas = await this.take(next.time);
|
|
44
|
+
if (this.disposed) return;
|
|
45
|
+
if (!canvas) continue;
|
|
46
|
+
this.cache.set(next.key, canvas);
|
|
47
|
+
if (this.cache.size > CACHE_LIMIT) this.cache.delete(this.cache.keys().next().value);
|
|
48
|
+
this.onFrame(next.key, canvas);
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
this.running = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async take(time) {
|
|
55
|
+
const video = this.video;
|
|
56
|
+
if (video.readyState < 1) {
|
|
57
|
+
const ready = await this.once("loadedmetadata", SEEK_TIMEOUT_MS);
|
|
58
|
+
if (!ready) return null;
|
|
59
|
+
}
|
|
60
|
+
const target = Math.min(Math.max(0, time), Number.isFinite(video.duration) ? Math.max(0, video.duration - 0.05) : time);
|
|
61
|
+
const seeked = this.once("seeked", SEEK_TIMEOUT_MS);
|
|
62
|
+
if (video.fastSeek) video.fastSeek(target);
|
|
63
|
+
else video.currentTime = target;
|
|
64
|
+
if (!await seeked) return null;
|
|
65
|
+
await this.framePresented();
|
|
66
|
+
if (this.disposed || !video.videoWidth) return null;
|
|
67
|
+
const { width, height } = this.size();
|
|
68
|
+
const ratio = Math.min(2, video.ownerDocument.defaultView?.devicePixelRatio ?? 1);
|
|
69
|
+
const canvas = video.ownerDocument.createElement("canvas");
|
|
70
|
+
canvas.width = Math.max(1, Math.round(width * ratio));
|
|
71
|
+
canvas.height = Math.max(1, Math.round(height * ratio));
|
|
72
|
+
const ctx = canvas.getContext("2d");
|
|
73
|
+
if (!ctx) return null;
|
|
74
|
+
const scale = Math.max(canvas.width / video.videoWidth, canvas.height / video.videoHeight);
|
|
75
|
+
const w = video.videoWidth * scale;
|
|
76
|
+
const h = video.videoHeight * scale;
|
|
77
|
+
try {
|
|
78
|
+
ctx.drawImage(video, (canvas.width - w) / 2, (canvas.height - h) / 2, w, h);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return canvas;
|
|
83
|
+
}
|
|
84
|
+
/*
|
|
85
|
+
* `seeked` says the position moved, not that its picture is ready to draw:
|
|
86
|
+
* Chromium can still hand the previous frame for a moment. Wait for the
|
|
87
|
+
* frame where the engine reports it, and a couple of paints where not.
|
|
88
|
+
*/
|
|
89
|
+
framePresented() {
|
|
90
|
+
const video = this.video;
|
|
91
|
+
const win = video.ownerDocument.defaultView;
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
let done = false;
|
|
94
|
+
const finish = () => {
|
|
95
|
+
if (done) return;
|
|
96
|
+
done = true;
|
|
97
|
+
resolve();
|
|
98
|
+
};
|
|
99
|
+
if (video.requestVideoFrameCallback) {
|
|
100
|
+
video.requestVideoFrameCallback(finish);
|
|
101
|
+
win?.setTimeout(finish, FRAME_TIMEOUT_MS);
|
|
102
|
+
} else if (win) {
|
|
103
|
+
win.requestAnimationFrame(() => win.requestAnimationFrame(finish));
|
|
104
|
+
} else finish();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
once(event, timeout) {
|
|
108
|
+
const video = this.video;
|
|
109
|
+
const win = video.ownerDocument.defaultView;
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const finish = (ok) => {
|
|
112
|
+
video.removeEventListener(event, onEvent);
|
|
113
|
+
video.removeEventListener("error", onError);
|
|
114
|
+
if (timer) win?.clearTimeout(timer);
|
|
115
|
+
resolve(ok);
|
|
116
|
+
};
|
|
117
|
+
const onEvent = () => finish(true);
|
|
118
|
+
const onError = () => finish(false);
|
|
119
|
+
video.addEventListener(event, onEvent);
|
|
120
|
+
video.addEventListener("error", onError);
|
|
121
|
+
const timer = win?.setTimeout(() => finish(false), timeout);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function stillSpan(secondsPerPx, minPx) {
|
|
126
|
+
const wanted = secondsPerPx * minPx;
|
|
127
|
+
let span = 1 / 8;
|
|
128
|
+
while (span < wanted && span < 8192) span *= 2;
|
|
129
|
+
return span;
|
|
130
|
+
}
|
|
131
|
+
export {
|
|
132
|
+
FrameSampler,
|
|
133
|
+
stillSpan
|
|
134
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type TimeRange = [number, number];
|
|
2
|
+
/** A rectangle on the video's frame, every value a fraction of it (0..1). */
|
|
3
|
+
export interface FrameRegion {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
w: number;
|
|
7
|
+
h: number;
|
|
8
|
+
}
|
|
9
|
+
export interface RangeLimits {
|
|
10
|
+
duration: number;
|
|
11
|
+
/** Shortest clip, seconds. */
|
|
12
|
+
minClip: number;
|
|
13
|
+
/** Longest clip, seconds; Infinity for none. */
|
|
14
|
+
maxClip: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A typed time, the inverse of `formatClipTime`: "75", "75.5", "1:15",
|
|
18
|
+
* "75:00.5", and "1:02:03.25" for whoever types the hours. A comma reads as
|
|
19
|
+
* the decimal point, as a Russian keyboard's numpad types it. Anything else,
|
|
20
|
+
* a negative or a seconds field past 59 after a colon, is null.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseClipTime(text: string): number | null;
|
|
23
|
+
/** A range that keeps every limit: inside the video, no shorter than minClip, no longer than maxClip. */
|
|
24
|
+
export declare function normalizeRange(range: TimeRange | null | undefined, limits: RangeLimits): TimeRange;
|
|
25
|
+
/**
|
|
26
|
+
* One end dragged to `t`. The dragged end stops at a limit and the other end
|
|
27
|
+
* stays put: a handle that pushed its partner would move the clip's other
|
|
28
|
+
* edge without the person touching it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function dragEnd(range: TimeRange, end: 0 | 1, t: number, limits: RangeLimits): TimeRange;
|
|
31
|
+
/**
|
|
32
|
+
* One end set to `t` on purpose (the I or O key, a typed time). That end goes
|
|
33
|
+
* where it was told, and the other follows when a limit asks for it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function setEnd(range: TimeRange, end: 0 | 1, t: number, limits: RangeLimits): TimeRange;
|
|
36
|
+
export interface TimeView {
|
|
37
|
+
start: number;
|
|
38
|
+
end: number;
|
|
39
|
+
}
|
|
40
|
+
/** The view zoomed by `factor` (above 1 zooms in) around `anchor`, which stays where it is on screen. */
|
|
41
|
+
export declare function zoomView(view: TimeView, anchor: number, factor: number, duration: number, minSpan: number): TimeView;
|
|
42
|
+
/** The view moved by `delta` seconds, held inside the video. */
|
|
43
|
+
export declare function shiftView(view: TimeView, delta: number, duration: number): TimeView;
|
|
44
|
+
/** The view moved just enough to show `t`, with `margin` of its span to spare. */
|
|
45
|
+
export declare function revealTime(view: TimeView, t: number, duration: number, margin?: number): TimeView;
|
|
46
|
+
/** Seconds between ruler marks, so that marks stand at least `minPx` apart. */
|
|
47
|
+
export declare function tickStep(span: number, width: number, minPx?: number): number;
|
|
48
|
+
/** The ruler's marks within the view. */
|
|
49
|
+
export declare function ticks(view: TimeView, step: number): number[];
|
|
50
|
+
/**
|
|
51
|
+
* Where the picture sits in a box that shows it whole (object-fit: contain):
|
|
52
|
+
* the region is drawn on the picture, not on the black bars around it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function containRect(boxWidth: number, boxHeight: number, videoWidth: number, videoHeight: number): {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
};
|
|
60
|
+
/** A region held inside the frame and no smaller than `minSize` a side. */
|
|
61
|
+
export declare function clampRegion(r: FrameRegion, minSize?: number): FrameRegion;
|
|
62
|
+
/** The region spanned by two corners, in either order, cut to the frame. */
|
|
63
|
+
export declare function regionBetween(ax: number, ay: number, bx: number, by: number): FrameRegion;
|
|
64
|
+
export type RegionHandle = "move" | "nw" | "ne" | "sw" | "se";
|
|
65
|
+
/** The region after one of its handles moved by (dx, dy), fractions of the frame. */
|
|
66
|
+
export declare function adjustRegion(r: FrameRegion, handle: RegionHandle, dx: number, dy: number, minSize?: number): FrameRegion;
|
|
67
|
+
/**
|
|
68
|
+
* Why the editor has no video to show, as its `error` event reports it:
|
|
69
|
+
* - "load": the address did not answer (network, 404, an expired session);
|
|
70
|
+
* - "decode": the file arrived but is damaged;
|
|
71
|
+
* - "unsupported": a format or codec this browser cannot play (H.265 in
|
|
72
|
+
* Chromium without hardware support, ProRes anywhere).
|
|
73
|
+
*/
|
|
74
|
+
export type VideoTrimErrorCause = "load" | "decode" | "unsupported";
|
|
75
|
+
export declare function mediaErrorCause(error: MediaError | null): VideoTrimErrorCause;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { clamp } from "../composables/clamp.js";
|
|
2
|
+
import { shiftSpan, zoomSpan } from "../composables/span.js";
|
|
3
|
+
const round = (t) => Math.round(t * 1e3) / 1e3;
|
|
4
|
+
function parseClipTime(text) {
|
|
5
|
+
const value = text.trim().replace(",", ".");
|
|
6
|
+
if (!/^\d+(:\d{1,2}){0,2}(\.\d*)?$/.test(value)) return null;
|
|
7
|
+
const parts = value.split(":");
|
|
8
|
+
let total = 0;
|
|
9
|
+
for (const [i, part] of parts.entries()) {
|
|
10
|
+
const n = Number(part);
|
|
11
|
+
if (!Number.isFinite(n)) return null;
|
|
12
|
+
if (i > 0 && n >= 60) return null;
|
|
13
|
+
total = total * 60 + n;
|
|
14
|
+
}
|
|
15
|
+
return total;
|
|
16
|
+
}
|
|
17
|
+
function limitsOf(l) {
|
|
18
|
+
const duration = Math.max(0, l.duration);
|
|
19
|
+
const min = clamp(l.minClip, 0, duration);
|
|
20
|
+
const max = Math.max(min, l.maxClip);
|
|
21
|
+
return { duration, min, max };
|
|
22
|
+
}
|
|
23
|
+
function normalizeRange(range, limits) {
|
|
24
|
+
const { duration, min, max } = limitsOf(limits);
|
|
25
|
+
if (!range || !Number.isFinite(range[0]) || !Number.isFinite(range[1])) return [0, round(Math.min(duration, max))];
|
|
26
|
+
let [a, b] = range[0] <= range[1] ? range : [range[1], range[0]];
|
|
27
|
+
a = clamp(a, 0, Math.max(0, duration - min));
|
|
28
|
+
b = clamp(b, a + min, Math.min(duration, a + max));
|
|
29
|
+
return [round(a), round(b)];
|
|
30
|
+
}
|
|
31
|
+
function dragEnd(range, end, t, limits) {
|
|
32
|
+
const { duration, min, max } = limitsOf(limits);
|
|
33
|
+
const [a, b] = range;
|
|
34
|
+
if (end === 0) return [round(clamp(t, Math.max(0, b - max), b - min)), b];
|
|
35
|
+
return [a, round(clamp(t, a + min, Math.min(duration, a + max)))];
|
|
36
|
+
}
|
|
37
|
+
function setEnd(range, end, t, limits) {
|
|
38
|
+
const { duration, min, max } = limitsOf(limits);
|
|
39
|
+
const [a, b] = range;
|
|
40
|
+
if (end === 0) {
|
|
41
|
+
const start = clamp(t, 0, duration - min);
|
|
42
|
+
return [round(start), round(clamp(b, start + min, Math.min(duration, start + max)))];
|
|
43
|
+
}
|
|
44
|
+
const stop = clamp(t, min, duration);
|
|
45
|
+
return [round(clamp(a, Math.max(0, stop - max), stop - min)), round(stop)];
|
|
46
|
+
}
|
|
47
|
+
function zoomView(view, anchor, factor, duration, minSpan) {
|
|
48
|
+
const span = view.end - view.start;
|
|
49
|
+
const next = zoomSpan(view.start, view.end, anchor, Math.min(1 / factor, duration / (span || 1)), Math.min(minSpan, duration));
|
|
50
|
+
return shiftView(next, 0, duration);
|
|
51
|
+
}
|
|
52
|
+
function shiftView(view, delta, duration) {
|
|
53
|
+
const span = Math.min(view.end - view.start, duration);
|
|
54
|
+
return shiftSpan(view.start, view.start + span, delta, 0, duration);
|
|
55
|
+
}
|
|
56
|
+
function revealTime(view, t, duration, margin = 0.1) {
|
|
57
|
+
const span = view.end - view.start;
|
|
58
|
+
const pad = span * margin;
|
|
59
|
+
if (t < view.start + pad) return shiftView(view, t - pad - view.start, duration);
|
|
60
|
+
if (t > view.end - pad) return shiftView(view, t + pad - view.end, duration);
|
|
61
|
+
return view;
|
|
62
|
+
}
|
|
63
|
+
const TICK_STEPS = [0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200];
|
|
64
|
+
function tickStep(span, width, minPx = 72) {
|
|
65
|
+
if (span <= 0 || width <= 0) return 1;
|
|
66
|
+
const perPx = span / width;
|
|
67
|
+
return TICK_STEPS.find((step) => step / perPx >= minPx) ?? TICK_STEPS[TICK_STEPS.length - 1];
|
|
68
|
+
}
|
|
69
|
+
function ticks(view, step) {
|
|
70
|
+
const out = [];
|
|
71
|
+
const first = Math.ceil(view.start / step - 1e-9) * step;
|
|
72
|
+
for (let t = first; t <= view.end + 1e-9 && out.length < 500; t += step) out.push(round(t));
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function containRect(boxWidth, boxHeight, videoWidth, videoHeight) {
|
|
76
|
+
if (!videoWidth || !videoHeight || !boxWidth || !boxHeight) return { x: 0, y: 0, width: boxWidth, height: boxHeight };
|
|
77
|
+
const scale = Math.min(boxWidth / videoWidth, boxHeight / videoHeight);
|
|
78
|
+
const width = videoWidth * scale;
|
|
79
|
+
const height = videoHeight * scale;
|
|
80
|
+
return { x: (boxWidth - width) / 2, y: (boxHeight - height) / 2, width, height };
|
|
81
|
+
}
|
|
82
|
+
const fix = (n) => Math.round(n * 1e4) / 1e4;
|
|
83
|
+
function clampRegion(r, minSize = 0.02) {
|
|
84
|
+
const w = clamp(r.w, minSize, 1);
|
|
85
|
+
const h = clamp(r.h, minSize, 1);
|
|
86
|
+
return { x: fix(clamp(r.x, 0, 1 - w)), y: fix(clamp(r.y, 0, 1 - h)), w: fix(w), h: fix(h) };
|
|
87
|
+
}
|
|
88
|
+
function regionBetween(ax, ay, bx, by) {
|
|
89
|
+
const x0 = clamp(Math.min(ax, bx), 0, 1);
|
|
90
|
+
const y0 = clamp(Math.min(ay, by), 0, 1);
|
|
91
|
+
const x1 = clamp(Math.max(ax, bx), 0, 1);
|
|
92
|
+
const y1 = clamp(Math.max(ay, by), 0, 1);
|
|
93
|
+
return { x: fix(x0), y: fix(y0), w: fix(x1 - x0), h: fix(y1 - y0) };
|
|
94
|
+
}
|
|
95
|
+
function adjustRegion(r, handle, dx, dy, minSize = 0.02) {
|
|
96
|
+
if (handle === "move") return clampRegion({ ...r, x: r.x + dx, y: r.y + dy }, minSize);
|
|
97
|
+
let x0 = r.x;
|
|
98
|
+
let y0 = r.y;
|
|
99
|
+
let x1 = r.x + r.w;
|
|
100
|
+
let y1 = r.y + r.h;
|
|
101
|
+
if (handle === "nw" || handle === "sw") x0 = clamp(x0 + dx, 0, x1 - minSize);
|
|
102
|
+
else x1 = clamp(x1 + dx, x0 + minSize, 1);
|
|
103
|
+
if (handle === "nw" || handle === "ne") y0 = clamp(y0 + dy, 0, y1 - minSize);
|
|
104
|
+
else y1 = clamp(y1 + dy, y0 + minSize, 1);
|
|
105
|
+
return { x: fix(x0), y: fix(y0), w: fix(x1 - x0), h: fix(y1 - y0) };
|
|
106
|
+
}
|
|
107
|
+
function mediaErrorCause(error) {
|
|
108
|
+
if (error?.code === 3) return "decode";
|
|
109
|
+
if (error?.code === 4) return "unsupported";
|
|
110
|
+
return "load";
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
adjustRegion,
|
|
114
|
+
clampRegion,
|
|
115
|
+
containRect,
|
|
116
|
+
dragEnd,
|
|
117
|
+
mediaErrorCause,
|
|
118
|
+
normalizeRange,
|
|
119
|
+
parseClipTime,
|
|
120
|
+
regionBetween,
|
|
121
|
+
revealTime,
|
|
122
|
+
setEnd,
|
|
123
|
+
shiftView,
|
|
124
|
+
tickStep,
|
|
125
|
+
ticks,
|
|
126
|
+
zoomView
|
|
127
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The window zoomed by `factor` (below 1 is in) around `anchor`, a position on the line; no wider than it was asked, no narrower than `minSpan`. */
|
|
2
|
+
export declare function zoomSpan(start: number, end: number, anchor: number, factor: number, minSpan: number): {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
};
|
|
6
|
+
/** The window moved by `delta`, its width kept, stopped at `lo` and `hi`. */
|
|
7
|
+
export declare function shiftSpan(start: number, end: number, delta: number, lo: number, hi: number): {
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function zoomSpan(start, end, anchor, factor, minSpan) {
|
|
2
|
+
const span = end - start;
|
|
3
|
+
const next = Math.max(minSpan, span * factor);
|
|
4
|
+
const from = anchor - (anchor - start) * (next / (span || 1));
|
|
5
|
+
return { start: from, end: from + next };
|
|
6
|
+
}
|
|
7
|
+
function shiftSpan(start, end, delta, lo, hi) {
|
|
8
|
+
const span = end - start;
|
|
9
|
+
const from = Math.max(lo, Math.min(hi - span, start + delta));
|
|
10
|
+
return { start: from, end: from + span };
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
shiftSpan,
|
|
14
|
+
zoomSpan
|
|
15
|
+
};
|
package/dist/icons-all.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -157,6 +157,9 @@ export { default as LpRangeSlider } from "./components/LpRangeSlider.vue";
|
|
|
157
157
|
export { default as LpVideoBackdrop } from "./components/LpVideoBackdrop.vue";
|
|
158
158
|
export { formatClipTime, isNativeLoop, planLoop, seamProgress, seamReached } from "./components/videoBackdrop";
|
|
159
159
|
export type { LoopPlan } from "./components/videoBackdrop";
|
|
160
|
+
export { default as LpVideoTrim } from "./components/LpVideoTrim.vue";
|
|
161
|
+
export { dragEnd, normalizeRange as normalizeTrimRange, parseClipTime, setEnd } from "./components/videoTrim";
|
|
162
|
+
export type { FrameRegion, TimeRange, VideoTrimErrorCause } from "./components/videoTrim";
|
|
160
163
|
export { default as LpSparkline } from "./components/LpSparkline.vue";
|
|
161
164
|
export { default as LpStat } from "./components/LpStat.vue";
|
|
162
165
|
export { default as LpStepper } from "./components/LpStepper.vue";
|