@cut-workbench/core 0.4.0 → 0.4.2
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/chunks/{canvas-size-Ct61OYjW.cjs → canvas-size-DIABzEW2.cjs} +212 -44
- package/dist/chunks/canvas-size-DIABzEW2.cjs.map +1 -0
- package/dist/chunks/{canvas-size-CM2oKApi.js → canvas-size-Dlnng74w.js} +168 -30
- package/dist/chunks/canvas-size-Dlnng74w.js.map +1 -0
- package/dist/chunks/id-CJsdlGei.js +35 -0
- package/dist/chunks/id-CJsdlGei.js.map +1 -0
- package/dist/chunks/id-DaCxD4-5.cjs +70 -0
- package/dist/chunks/id-DaCxD4-5.cjs.map +1 -0
- package/dist/chunks/{snap-DyIuO2hU.cjs → snap-Cfk15JVg.cjs} +2 -22
- package/dist/chunks/{snap-DyIuO2hU.cjs.map → snap-Cfk15JVg.cjs.map} +1 -1
- package/dist/chunks/{snap-CBV9G4xq.js → snap-DpkVPVJN.js} +3 -17
- package/dist/chunks/{snap-CBV9G4xq.js.map → snap-DpkVPVJN.js.map} +1 -1
- package/dist/chunks/thumbnails-DEjnao5r.cjs +81 -0
- package/dist/chunks/thumbnails-DEjnao5r.cjs.map +1 -0
- package/dist/chunks/thumbnails-qH0dX_Zs.js +64 -0
- package/dist/chunks/thumbnails-qH0dX_Zs.js.map +1 -0
- package/dist/chunks/{time-CnMfSVYX.cjs → time-CYEd91qN.cjs} +1 -69
- package/dist/chunks/time-CYEd91qN.cjs.map +1 -0
- package/dist/chunks/{time-C_gvkVl1.js → time-ChF1GwCy.js} +2 -34
- package/dist/chunks/time-ChF1GwCy.js.map +1 -0
- package/dist/chunks/{waveform-Ie-G5Lle.cjs → waveform-BYwDdKpC.cjs} +34 -15
- package/dist/chunks/waveform-BYwDdKpC.cjs.map +1 -0
- package/dist/chunks/{waveform-DJkRtMct.js → waveform-uyGNE01H.js} +32 -13
- package/dist/chunks/waveform-uyGNE01H.js.map +1 -0
- package/dist/engine/index.cjs +668 -76
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.js +648 -57
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/webav/exporter.d.ts +3 -1
- package/dist/engine/webav/import-files.d.ts +17 -2
- package/dist/engine/webav/import-pipeline.d.ts +10 -0
- package/dist/engine/webav/types.d.ts +15 -0
- package/dist/index.cjs +18 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/lib/error.d.ts +6 -0
- package/dist/media/thumbnails.d.ts +8 -0
- package/dist/project/schema.d.ts +16 -4
- package/dist/timeline/index.cjs +10 -6
- package/dist/timeline/index.d.ts +1 -0
- package/dist/timeline/index.js +4 -4
- package/dist/timeline/normalize.d.ts +28 -0
- package/dist/timeline/ops.d.ts +0 -4
- package/dist/timeline/snap.d.ts +0 -2
- package/dist/types.d.ts +14 -2
- package/package.json +1 -1
- package/dist/chunks/canvas-size-CM2oKApi.js.map +0 -1
- package/dist/chunks/canvas-size-Ct61OYjW.cjs.map +0 -1
- package/dist/chunks/import-pipeline-A8KkqtZT.cjs +0 -610
- package/dist/chunks/import-pipeline-A8KkqtZT.cjs.map +0 -1
- package/dist/chunks/import-pipeline-DLIdgrIb.js +0 -521
- package/dist/chunks/import-pipeline-DLIdgrIb.js.map +0 -1
- package/dist/chunks/time-C_gvkVl1.js.map +0 -1
- package/dist/chunks/time-CnMfSVYX.cjs.map +0 -1
- package/dist/chunks/waveform-DJkRtMct.js.map +0 -1
- package/dist/chunks/waveform-Ie-G5Lle.cjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-size-Dlnng74w.js","names":[],"sources":["../../src/timeline/model.ts","../../src/timeline/normalize.ts","../../src/timeline/ops.ts","../../src/timeline/geometry.ts","../../src/timeline/canvas-size.ts"],"sourcesContent":["/**\n * 时间轴纯数据模型与工厂函数。\n *\n * 这里没有任何 WebAV / React 依赖,所有函数都是纯函数或纯构造。\n * 时间单位统一微秒(µs)。\n */\n\nimport { createId } from '../lib/id'\nimport { DEFAULT_CLIP_VOLUME, DEFAULT_STILL_DURATION_US } from '../constants'\nimport type { Project, TimelineClip, Track } from '../types'\nimport { asUs } from './time'\n\nexport function createTrack(kind: Track['kind'], name: string): Track {\n return {\n id: createId('track'),\n name,\n kind,\n locked: false,\n muted: false,\n hidden: false,\n }\n}\n\nexport function createEmptyProject(name = '未命名工程'): Project {\n return {\n id: createId('project'),\n name,\n width: 1280,\n height: 720,\n tracks: [createTrack('visual', '视频 1'), createTrack('audio', '音频 1')],\n clips: [],\n updatedAt: Date.now(),\n }\n}\n\nexport interface CreateClipInput {\n assetId: string\n trackId: string\n startUs: number\n durationUs: number\n inPointUs?: number\n assetWidth?: number\n assetHeight?: number\n}\n\n/** 新建 clip 的默认几何:等比铺满工程画面 */\nexport function createClip(input: CreateClipInput, project: Project): TimelineClip {\n const { assetWidth = 0, assetHeight = 0 } = input\n let w = project.width\n let h = project.height\n if (assetWidth > 0 && assetHeight > 0) {\n const scale = Math.min(project.width / assetWidth, project.height / assetHeight)\n w = Math.round(assetWidth * scale)\n h = Math.round(assetHeight * scale)\n }\n return {\n id: createId('clip'),\n trackId: input.trackId,\n assetId: input.assetId,\n startUs: asUs(input.startUs),\n durationUs: asUs(input.durationUs),\n inPointUs: asUs(input.inPointUs ?? 0),\n playbackRate: 1,\n volume: DEFAULT_CLIP_VOLUME,\n muted: false,\n opacity: 1,\n flip: null,\n transform: {\n x: Math.round((project.width - w) / 2),\n y: Math.round((project.height - h) / 2),\n w,\n h,\n angle: 0,\n },\n fadeInUs: asUs(0),\n fadeOutUs: asUs(0),\n }\n}\n\n/** 静态素材(图片 / 文字 / 字幕)需要有限时长 */\nexport function clampStillDuration(us: number): number {\n return us > 0 && Number.isFinite(us) ? us : DEFAULT_STILL_DURATION_US\n}\n\nexport function getClipEnd(clip: TimelineClip): number {\n return clip.startUs + clip.durationUs\n}\n\n/** 工程总时长(取所有 clip 结束时间的最大值) */\nexport function getProjectDuration(project: Project): number {\n return project.clips.reduce((max, c) => Math.max(max, getClipEnd(c)), 0)\n}\n\nexport function getTrack(project: Project, trackId: string): Track | undefined {\n return project.tracks.find((t) => t.id === trackId)\n}\n\nexport function isTrackAudible(project: Project, trackId: string): boolean {\n const track = getTrack(project, trackId)\n if (!track) return false\n return !track.muted && !track.hidden\n}\n\n/** 某轨道上按时间排序的 clip */\nexport function getTrackClips(project: Project, trackId: string): TimelineClip[] {\n return project.clips\n .filter((c) => c.trackId === trackId)\n .sort((a, b) => a.startUs - b.startUs)\n}\n\n/** 轨道内最大结束时间,用于「轨道末尾」吸附点 */\nexport function getTrackEnd(project: Project, trackId: string): number {\n return getTrackClips(project, trackId).reduce((max, c) => Math.max(max, getClipEnd(c)), 0)\n}\n\n/** 源素材被消费的时长 = 时间轴时长 × 速率 */\nexport function getSourceSpanUs(clip: TimelineClip): number {\n return Math.round(clip.durationUs * clip.playbackRate)\n}\n","/**\n * 工程数据的容错归一化。\n *\n * 为什么需要:工程 JSON 是**跨版本、跨来源**的长期文件 ——\n * - 宿主可能手工拼、用脚本生成、或在旧版本里存的(那时还没有 `aspectLocked`\n * 这类后加字段);\n * - 我们自己也持续加字段。\n *\n * 直接把外部 JSON 塞进 store 会让 UI 在 `clip.transform.x` 这类读取上抛\n * \"Cannot read properties of undefined\",而这种错误发生在渲染期,用户只能\n * 看到白屏。这里把所有可选字段补成默认值,并把明显损坏的数值收敛到合法范围,\n * 让「字段缺失」降级为「使用默认值」。\n *\n * 本文件是纯函数,不依赖 WebAV / React。\n */\n\nimport type { ClipTransform, MediaAsset, Project, SubtitleStyle, TextStyle, TimelineClip, Track } from '../types'\nimport { DEFAULT_CLIP_VOLUME, DEFAULT_STILL_DURATION_US } from '../constants'\nimport { createId } from '../lib/id'\nimport { asUs, clamp } from './time'\n\nconst DEFAULT_TEXT_STYLE: TextStyle = {\n fontSize: 64,\n color: '#ffffff',\n fontFamily: 'sans-serif',\n fontWeight: 600,\n fontStyle: 'normal',\n strokeColor: '#000000',\n strokeWidth: 0,\n shadowColor: 'rgba(0,0,0,0.6)',\n shadowBlur: 8,\n align: 'center',\n background: 'transparent',\n}\n\nconst DEFAULT_SUBTITLE_STYLE: SubtitleStyle = {\n color: '#ffffff',\n textBgColor: null,\n fontFamily: 'sans-serif',\n fontSize: 30,\n strokeStyle: '#000000',\n lineWidth: null,\n bottomOffset: 30,\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value != null && typeof value === 'object' && !Array.isArray(value)\n}\n\n/** 有限数值,否则用兜底值 */\nfunction num(value: unknown, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : fallback\n}\n\nfunction str(value: unknown, fallback: string): string {\n return typeof value === 'string' && value.length > 0 ? value : fallback\n}\n\n/** 补全 clip 的几何变换(缺 transform / 缺子字段都会兜底) */\nexport function normalizeTransform(\n raw: unknown,\n projectWidth: number,\n projectHeight: number,\n): ClipTransform {\n const source = isRecord(raw) ? raw : {}\n const w = Math.max(1, num(source.w, projectWidth))\n const h = Math.max(1, num(source.h, projectHeight))\n return {\n x: num(source.x, Math.round((projectWidth - w) / 2)),\n y: num(source.y, Math.round((projectHeight - h) / 2)),\n w,\n h,\n angle: num(source.angle, 0),\n }\n}\n\nfunction normalizeFlip(raw: unknown): TimelineClip['flip'] {\n return raw === 'horizontal' || raw === 'vertical' ? raw : null\n}\n\n/** 补全单个 clip;`id` 缺失时生成一个,保证后续操作可用 */\nexport function normalizeClip(raw: unknown, projectWidth: number, projectHeight: number): TimelineClip | null {\n if (!isRecord(raw)) return null\n const trackId = typeof raw.trackId === 'string' ? raw.trackId : ''\n const assetId = typeof raw.assetId === 'string' ? raw.assetId : ''\n // 没有轨道或素材引用的 clip 无法渲染,直接丢弃(调用方会收到一条「已跳过」)\n if (trackId === '' || assetId === '') return null\n\n const durationUs = Math.max(0, num(raw.durationUs, 0))\n const normalized: TimelineClip = {\n id: str(raw.id, createId('clip')),\n trackId,\n assetId,\n startUs: asUs(Math.max(0, num(raw.startUs, 0))),\n durationUs: asUs(durationUs),\n inPointUs: asUs(Math.max(0, num(raw.inPointUs, 0))),\n playbackRate: clamp(num(raw.playbackRate, 1), 0.1, 10),\n volume: clamp(num(raw.volume, DEFAULT_CLIP_VOLUME), 0, 4),\n muted: raw.muted === true,\n opacity: clamp(num(raw.opacity, 1), 0, 1),\n flip: normalizeFlip(raw.flip),\n transform: normalizeTransform(raw.transform, projectWidth, projectHeight),\n fadeInUs: asUs(Math.max(0, num(raw.fadeInUs, 0))),\n fadeOutUs: asUs(Math.max(0, num(raw.fadeOutUs, 0))),\n }\n if (raw.aspectLocked === true) normalized.aspectLocked = true\n if (typeof raw.text === 'string') normalized.text = raw.text\n if (isRecord(raw.textStyle)) normalized.textStyle = { ...DEFAULT_TEXT_STYLE, ...raw.textStyle } as TextStyle\n if (typeof raw.srt === 'string') normalized.srt = raw.srt\n if (isRecord(raw.subtitleStyle)) {\n normalized.subtitleStyle = { ...DEFAULT_SUBTITLE_STYLE, ...raw.subtitleStyle } as SubtitleStyle\n }\n return normalized\n}\n\nexport function normalizeTrack(raw: unknown, index: number): Track | null {\n if (!isRecord(raw)) return null\n const kind = raw.kind === 'audio' ? 'audio' : 'visual'\n return {\n id: str(raw.id, createId('track')),\n // 名称缺失时按类型 + 序号给一个可读的默认值\n name: str(raw.name, kind === 'audio' ? `音频 ${index + 1}` : `视频 ${index + 1}`),\n kind,\n locked: raw.locked === true,\n muted: raw.muted === true,\n hidden: raw.hidden === true,\n }\n}\n\n/** 补全工程;轨道为空时补一对默认轨道,否则 clip 无处可放 */\nexport function normalizeProject(raw: unknown): Project | null {\n if (!isRecord(raw)) return null\n const width = Math.max(2, Math.round(num(raw.width, 1280)))\n const height = Math.max(2, Math.round(num(raw.height, 720)))\n\n const tracks = (Array.isArray(raw.tracks) ? raw.tracks : [])\n .map((track, index) => normalizeTrack(track, index))\n .filter((track): track is Track => track != null)\n\n const effectiveTracks =\n tracks.length > 0\n ? tracks\n : [\n { id: createId('track'), name: '视频 1', kind: 'visual' as const, locked: false, muted: false, hidden: false },\n { id: createId('track'), name: '音频 1', kind: 'audio' as const, locked: false, muted: false, hidden: false },\n ]\n const trackIds = new Set(effectiveTracks.map((t) => t.id))\n\n const clips = (Array.isArray(raw.clips) ? raw.clips : [])\n .map((clip) => normalizeClip(clip, width, height))\n .filter((clip): clip is TimelineClip => clip != null)\n // 引用了不存在轨道的 clip 无家可归,丢弃而不是让同步器每帧报错\n .filter((clip) => trackIds.has(clip.trackId))\n\n return {\n id: str(raw.id, createId('project')),\n name: str(raw.name, '未命名工程'),\n width,\n height,\n tracks: effectiveTracks,\n clips,\n updatedAt: num(raw.updatedAt, Date.now()),\n }\n}\n\nconst KINDS = new Set<MediaAsset['kind']>(['video', 'audio', 'image', 'text', 'subtitle'])\n\n/** 补全素材元数据;`kind` 非法的条目直接丢弃(无法决定如何渲染) */\nexport function normalizeAsset(raw: unknown): MediaAsset | null {\n if (!isRecord(raw)) return null\n const kind = KINDS.has(raw.kind as MediaAsset['kind']) ? (raw.kind as MediaAsset['kind']) : null\n if (kind == null) return null\n const id = typeof raw.id === 'string' && raw.id !== '' ? raw.id : createId('asset')\n\n const asset: MediaAsset = {\n id,\n name: str(raw.name, kind === 'video' ? '未命名视频' : '未命名素材'),\n kind,\n mimeType: str(raw.mimeType, 'application/octet-stream'),\n size: Math.max(0, num(raw.size, 0)),\n durationUs: asUs(Math.max(0, num(raw.durationUs, 0))),\n width: Math.max(0, num(raw.width, 0)),\n height: Math.max(0, num(raw.height, 0)),\n hasAudio: raw.hasAudio === true,\n createdAt: num(raw.createdAt, Date.now()),\n }\n\n // 只在内容真实存在时带上,避免给文字素材塞空串后 UI 显示空块\n if (typeof raw.text === 'string') asset.text = raw.text\n if (isRecord(raw.textStyle)) asset.textStyle = { ...DEFAULT_TEXT_STYLE, ...raw.textStyle } as TextStyle\n if (typeof raw.srt === 'string') asset.srt = raw.srt\n if (isRecord(raw.subtitleStyle)) {\n asset.subtitleStyle = { ...DEFAULT_SUBTITLE_STYLE, ...raw.subtitleStyle } as SubtitleStyle\n }\n // 波形是纯派生数据:只接受合法数字数组\n if (Array.isArray(raw.waveform) && raw.waveform.every((v) => typeof v === 'number')) {\n asset.waveform = raw.waveform as number[]\n }\n if (typeof raw.sourcePath === 'string') asset.sourcePath = raw.sourcePath\n\n // 静态素材没有自然时长:存成 0 会导致时间轴上的片段时长为 0\n if (\n (kind === 'image' || kind === 'text' || kind === 'subtitle') &&\n asset.durationUs <= 0\n ) {\n asset.durationUs = asUs(DEFAULT_STILL_DURATION_US)\n }\n return asset\n}\n\nexport { DEFAULT_TEXT_STYLE, DEFAULT_SUBTITLE_STYLE }\n","/**\n * 时间轴编辑操作(纯函数)。\n *\n * 全部返回新的数组/对象,不修改入参;异步部分(如真正切割 IClip 实例)由\n * `core/webav/clip-registry.ts` 负责,模型层只处理时间与轨道数据。\n */\n\nimport type { TimelineClip, Track } from '../types'\nimport { asUs } from './time'\nimport { getClipEnd } from './model'\n\nexport type TrimEdge = 'start' | 'end'\n\n/**\n * 裁剪 clip 的边缘。\n *\n * - start:改变起点与源入点,保持右边缘不动\n * - end:只改变时长\n *\n * `minDurationUs` 保证不会裁成 0 长度。\n */\nexport function trimClipData(\n clip: TimelineClip,\n edge: TrimEdge,\n deltaUs: number,\n minDurationUs = 100_000,\n): TimelineClip {\n const end = getClipEnd(clip)\n\n if (edge === 'start') {\n const nextStart = Math.min(\n Math.max(0, clip.startUs + deltaUs),\n end - minDurationUs,\n )\n const applied = nextStart - clip.startUs\n const sourceDelta = Math.round(applied * clip.playbackRate)\n const nextInPoint = Math.max(0, clip.inPointUs + sourceDelta)\n return {\n ...clip,\n startUs: asUs(nextStart),\n inPointUs: asUs(nextInPoint),\n durationUs: asUs(end - nextStart),\n }\n }\n\n const nextDuration = Math.max(minDurationUs, clip.durationUs + deltaUs)\n return { ...clip, durationUs: asUs(nextDuration) }\n}\n\n/** 删除指定 id 的 clip */\nexport function removeClipsData(clips: TimelineClip[], ids: string[]): TimelineClip[] {\n const set = new Set(ids)\n return clips.filter((c) => !set.has(c.id))\n}\n\n/** 复制 clip,起点顺延 `offsetUs`,返回 { originalId, copy } 便于重建 IClip 实例 */\nexport function duplicateClipsData(\n clips: TimelineClip[],\n ids: string[],\n offsetUs: number,\n makeId: () => string,\n): Array<{ sourceId: string; clip: TimelineClip }> {\n const set = new Set(ids)\n return clips\n .filter((c) => set.has(c.id))\n .map((c) => ({\n sourceId: c.id,\n clip: {\n ...c,\n id: makeId(),\n startUs: asUs(c.startUs + offsetUs),\n transform: { ...c.transform },\n },\n }))\n}\n\n/** 自动排布:把同一轨道上的 clip 按顺序首尾相接(用于「磁吸排序」) */\nexport function compactTrackData(clips: TimelineClip[], trackId: string): TimelineClip[] {\n const onTrack = clips\n .filter((c) => c.trackId === trackId)\n .sort((a, b) => a.startUs - b.startUs)\n let cursor = 0\n const updated = new Map<string, number>()\n for (const c of onTrack) {\n updated.set(c.id, cursor)\n cursor += c.durationUs\n }\n return clips.map((c) => {\n const next = updated.get(c.id)\n return next == null ? c : { ...c, startUs: asUs(next) }\n })\n}\n\n/** 重新排序轨道(拖拽调整垂直顺序) */\nexport function reorderTracksData(tracks: Track[], trackId: string, toIndex: number): Track[] {\n const from = tracks.findIndex((t) => t.id === trackId)\n if (from < 0) return tracks\n const next = [...tracks]\n const [moved] = next.splice(from, 1)\n if (!moved) return tracks\n next.splice(Math.max(0, Math.min(next.length, toIndex)), 0, moved)\n return next\n}\n\n/** 判断两个区间是否重叠(半开区间) */\nexport function rangesOverlap(aStart: number, aEnd: number, bStart: number, bEnd: number): boolean {\n return aStart < bEnd && bStart < aEnd\n}\n\n/** 找出与给定区间在指定轨道上重叠的 clip(排除自身) */\nexport function findOverlaps(\n clips: TimelineClip[],\n trackId: string,\n startUs: number,\n endUs: number,\n excludeId?: string,\n): TimelineClip[] {\n return clips.filter(\n (c) =>\n c.trackId === trackId &&\n c.id !== excludeId &&\n rangesOverlap(startUs, endUs, c.startUs, getClipEnd(c)),\n )\n}\n","/**\n * 画布几何计算(纯函数)。\n *\n * 坐标系:工程分辨率像素(`project.width × project.height`),\n * 与 `clip.transform` 完全一致,也与导出坐标映射的输入一致。\n *\n * 为什么单独放 core:画布的命中测试、自绘选框的手柄布局、缩放反算\n * 这三处必须用同一套数学,否则「点中的框」和「画出的框」会错位。\n */\n\nimport type { ClipTransform } from '../types'\n\n/** 手柄方位。边用 4 个字母,角用两两组合,另加旋转 */\nexport type HandleKey = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'\n\nexport const RESIZE_HANDLES: HandleKey[] = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w']\n\n/** 最小尺寸(工程像素),与 WebAV 的内部控制点下限保持一致 */\nexport const MIN_TRANSFORM_SIZE = 10\n\nexport interface Point {\n x: number\n y: number\n}\n\nexport interface Rect {\n x: number\n y: number\n w: number\n h: number\n}\n\nexport function rectCenter(rect: Rect): Point {\n return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 }\n}\n\n/** 角度(度)→ 弧度 */\nexport function degToRad(deg: number): number {\n return (deg * Math.PI) / 180\n}\n\n/**\n * 把世界坐标点转换到矩形自身的局部坐标(以中心为原点、已抵消旋转)。\n *\n * 旋转方向与 Canvas `rotate()` 一致(顺时针为正),因此用 `-angle` 做逆变换。\n */\nexport function toLocalPoint(point: Point, transform: Pick<ClipTransform, 'x' | 'y' | 'w' | 'h' | 'angle'>): Point {\n const cx = transform.x + transform.w / 2\n const cy = transform.y + transform.h / 2\n const rad = -degToRad(transform.angle)\n const dx = point.x - cx\n const dy = point.y - cy\n return {\n x: dx * Math.cos(rad) - dy * Math.sin(rad),\n y: dx * Math.sin(rad) + dy * Math.cos(rad),\n }\n}\n\n/** 局部坐标(相对中心)→ 世界坐标 */\nexport function fromLocalPoint(local: Point, transform: Pick<ClipTransform, 'x' | 'y' | 'w' | 'h' | 'angle'>): Point {\n const cx = transform.x + transform.w / 2\n const cy = transform.y + transform.h / 2\n const rad = degToRad(transform.angle)\n return {\n x: cx + local.x * Math.cos(rad) - local.y * Math.sin(rad),\n y: cy + local.x * Math.sin(rad) + local.y * Math.cos(rad),\n }\n}\n\n/** 点是否落在(可能旋转的)矩形内 */\nexport function pointInTransform(point: Point, transform: ClipTransform): boolean {\n if (transform.w <= 0 || transform.h <= 0) return false\n const local = toLocalPoint(point, transform)\n return Math.abs(local.x) <= transform.w / 2 && Math.abs(local.y) <= transform.h / 2\n}\n\n/**\n * 按方位返回手柄在**局部坐标**中的位置(相对中心)。\n *\n * 边手柄固定一个方向为 0,角手柄两个方向都取半宽/半高。\n */\nexport function handleLocalOffset(handle: HandleKey, w: number, h: number): Point {\n const hw = w / 2\n const hh = h / 2\n const x = handle.includes('w') ? -hw : handle.includes('e') ? hw : 0\n const y = handle.includes('n') ? -hh : handle.includes('s') ? hh : 0\n return { x, y }\n}\n\n/**\n * 计算缩放后的 transform。\n *\n * 采用「固定对角/对边」的直觉行为:拖右边缘时左边缘不动,拖左上角时右下角不动。\n * 指针先被投影到局部坐标轴,因此素材旋转后手柄仍沿自身方向缩放(而不是屏幕方向)。\n *\n * @param start 拖动开始时的 transform 快照\n * @param handle 被拖动的方位\n * @param pointer 当前指针(工程坐标)\n * @param keepAspect\n * - `false`(默认):自由缩放\n * - `true`:所有手柄都等比(Shift 临时约束)\n * - `'corners'`:只有角手柄等比,边手柄仍单维自由\n * (片段自身「锁定宽高比」用这个 —— 若边手柄也等比,\n * 就会出现「拖右边、上下也跟着变」的诡异手感)\n */\nexport function resizeTransform(\n start: ClipTransform,\n handle: HandleKey,\n pointer: Point,\n keepAspect: boolean | 'corners' = false,\n): ClipTransform {\n const local = toLocalPoint(pointer, start)\n const hw = start.w / 2\n const hh = start.h / 2\n\n // 局部坐标下的四条边;拖动某条边只改它,其余固定不动(= 固定对角/对边)\n let left = -hw\n let right = hw\n let top = -hh\n let bottom = hh\n\n if (handle.includes('w')) left = Math.min(local.x, right - MIN_TRANSFORM_SIZE)\n if (handle.includes('e')) right = Math.max(local.x, left + MIN_TRANSFORM_SIZE)\n if (handle.includes('n')) top = Math.min(local.y, bottom - MIN_TRANSFORM_SIZE)\n if (handle.includes('s')) bottom = Math.max(local.y, top + MIN_TRANSFORM_SIZE)\n\n let nextW = right - left\n let nextH = bottom - top\n\n const isCorner = handle.length === 2\n const aspectActive = keepAspect === true || (keepAspect === 'corners' && isCorner)\n\n if (aspectActive && start.w > 0 && start.h > 0) {\n const ratio = start.w / start.h\n if (isCorner) {\n // 以宽为准反算高,再按被拖动的方向贴住不动的对边\n nextH = nextW / ratio\n if (handle.includes('n')) top = bottom - nextH\n else bottom = top + nextH\n } else if (handle === 'e' || handle === 'w') {\n nextH = nextW / ratio\n top = -nextH / 2\n bottom = nextH / 2\n } else {\n nextW = nextH * ratio\n left = -nextW / 2\n right = nextW / 2\n }\n }\n\n nextW = Math.max(MIN_TRANSFORM_SIZE, right - left)\n nextH = Math.max(MIN_TRANSFORM_SIZE, bottom - top)\n\n // 新中心(局部坐标)→ 世界坐标 → 左上角\n const localCenter = { x: (left + right) / 2, y: (top + bottom) / 2 }\n const worldCenter = fromLocalPoint(localCenter, start)\n return {\n x: worldCenter.x - nextW / 2,\n y: worldCenter.y - nextH / 2,\n w: nextW,\n h: nextH,\n angle: start.angle,\n }\n}\n\n/** 把角度归一化到 (-180, 180],避免旋转多圈后数值无限增大 */\nexport function normalizeAngle(deg: number): number {\n let a = deg % 360\n if (a > 180) a -= 360\n if (a <= -180) a += 360\n return a\n}\n\n/** 按手柄方位给出鼠标光标(已考虑素材自身旋转) */\nexport function handleCursor(handle: HandleKey | 'rotate', angleDeg: number): string {\n if (handle === 'rotate') return 'grab'\n const base: Record<HandleKey, number> = {\n n: 0,\n ne: 45,\n e: 90,\n se: 135,\n s: 180,\n sw: 225,\n w: 270,\n nw: 315,\n }\n const cursors = ['ns-resize', 'nesw-resize', 'ew-resize', 'nwse-resize']\n // 把素材旋转叠加到手柄方位上,再落到 4 档光标\n const deg = ((base[handle] + angleDeg) % 180 + 180) % 180\n const index = Math.round(deg / 45) % 4\n return cursors[index] ?? 'default'\n}\n\n/** 两个尺寸是否近似相等(避免浮点噪声导致无谓的 store 写入) */\nexport function sameTransform(a: ClipTransform, b: ClipTransform, epsilon = 0.5): boolean {\n return (\n Math.abs(a.x - b.x) < epsilon &&\n Math.abs(a.y - b.y) < epsilon &&\n Math.abs(a.w - b.w) < epsilon &&\n Math.abs(a.h - b.h) < epsilon &&\n Math.abs(a.angle - b.angle) < epsilon\n )\n}\n","/**\n * 画布(工程)尺寸与比例。\n *\n * 为什么需要单独一层:时间轴里所有 `clip.transform` 都以工程分辨率为坐标系,\n * 换比例时如果不做变换,1280×720 里铺满的素材放到 1080×1920 画布里就会\n * 只剩顶部一条 —— 这正是「换了比例看起来没生效 / 画面全是黑的」的成因。\n *\n * 因此尺寸变更分两种语义:\n * - `scale`(保持构图):把所有素材按同样的比例缩放并平移,视觉上「整幅画面\n * 被重新装进新画布」,适合横屏切竖屏、改分辨率。\n * - `keep`(保持素材原样):只换画布,素材坐标原封不动,适合「画布留白变多」。\n *\n * 纯函数,不依赖 React。\n */\n\nimport type { ClipTransform, Project, TimelineClip } from '../types'\n\nexport interface AspectPreset {\n id: string\n label: string\n hint: string\n width: number\n height: number\n}\n\n/**\n * 常用比例预设。\n *\n * 尺寸都取该比例下「编辑器默认档」的常见值:横屏 720p、竖屏 720×1280、\n * 方形 1080×1080、竖版 1080×1920、横版 1920×1080。用户也可以自定义。\n */\nexport const ASPECT_PRESETS: AspectPreset[] = [\n { id: '16:9', label: '16:9', hint: '横屏 1280×720', width: 1280, height: 720 },\n { id: '9:16', label: '9:16', hint: '竖屏 720×1280', width: 720, height: 1280 },\n { id: '1:1', label: '1:1', hint: '方形 1080×1080', width: 1080, height: 1080 },\n { id: '4:3', label: '4:3', hint: '标准 960×720', width: 960, height: 720 },\n { id: '3:4', label: '3:4', hint: '竖版 720×960', width: 720, height: 960 },\n { id: '4:5', label: '4:5', hint: '社交媒体 864×1080', width: 864, height: 1080 },\n { id: '21:9', label: '21:9', hint: '宽银幕 1680×720', width: 1680, height: 720 },\n]\n\nexport type SizeChangeMode = 'scale' | 'keep'\n\n/** 求最大公约数(用于把尺寸化简成比例) */\nfunction gcd(a: number, b: number): number {\n let x = Math.abs(Math.round(a))\n let y = Math.abs(Math.round(b))\n while (y > 0) {\n const t = x % y\n x = y\n y = t\n }\n return x || 1\n}\n\n/** `1920×1080` → `16:9`;无法化简时返回 `1920:1080` 形式 */\nexport function ratioLabel(width: number, height: number): string {\n if (width <= 0 || height <= 0) return '—'\n const divisor = gcd(width, height)\n return `${width / divisor}:${height / divisor}`\n}\n\n/** 保留两位小数,避免属性面板里出现 123.456789 */\nfunction round2(n: number): number {\n return Math.round(n * 100) / 100\n}\n\n/**\n * 把素材几何从 `from` 画布映射到 `to` 画布。\n *\n * 用**同一个**缩放系数(取宽高比中较小者),保证素材不变形:\n * 16:9 → 9:16 时画面按高度缩小、水平居中,两侧留黑 —— 与剪辑软件里\n * 「按比例适配」的行为一致。若分别按宽、高缩放,人物会被横向拉扁。\n */\nexport function remapTransform(\n transform: ClipTransform,\n from: { width: number; height: number },\n to: { width: number; height: number },\n): ClipTransform {\n const fromW = Math.max(1, from.width)\n const fromH = Math.max(1, from.height)\n const scale = Math.min(to.width / fromW, to.height / fromH)\n const centerX = (transform.x + transform.w / 2) * scale + (to.width - fromW * scale) / 2\n const centerY = (transform.y + transform.h / 2) * scale + (to.height - fromH * scale) / 2\n const w = transform.w * scale\n const h = transform.h * scale\n return {\n x: round2(centerX - w / 2),\n y: round2(centerY - h / 2),\n w: round2(w),\n h: round2(h),\n angle: transform.angle,\n }\n}\n\n/**\n * 根据模式生成新的 clips 数组。\n *\n * @param mode `scale` = 素材随画布等比缩放;`keep` = 素材坐标不动\n */\nexport function remapClipsForSize(\n clips: TimelineClip[],\n from: { width: number; height: number },\n to: { width: number; height: number },\n mode: SizeChangeMode,\n): TimelineClip[] {\n if (mode === 'keep') return clips\n return clips.map((clip) => ({\n ...clip,\n transform: remapTransform(clip.transform, from, to),\n }))\n}\n\n/** 判断工程是否已是非标尺寸(不在预设里) */\nexport function isCustomSize(project: Project): boolean {\n return !ASPECT_PRESETS.some((p) => p.width === project.width && p.height === project.height)\n}\n\n/** 尺寸合法化:不小于 16,偶数(编码器要求) */\nexport function normalizeSize(width: number, height: number): { width: number; height: number } {\n const even = (n: number) => Math.max(16, Math.round(n / 2) * 2)\n return { width: even(width), height: even(height) }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
2
|
+
// src/constants.ts
|
|
3
|
+
/**
|
|
4
|
+
* cut-core 自有常量。
|
|
5
|
+
*
|
|
6
|
+
* 只放「逻辑层必须知道」的数值:默认时长、默认音量、工程 schema 版本、
|
|
7
|
+
* 历史栈深度。UI 相关的量(缩放档位、面板高度、吸附像素阈值)留在宿主侧,
|
|
8
|
+
* 避免组件库被时间轴界面的调参牵着走。
|
|
9
|
+
*/
|
|
10
|
+
/** 图片 / 文字素材默认时长(微秒) */
|
|
11
|
+
var DEFAULT_STILL_DURATION_US = 5e6;
|
|
12
|
+
/** 默认音量,避免多轨直接相加削波 */
|
|
13
|
+
var DEFAULT_CLIP_VOLUME = .8;
|
|
14
|
+
/** 工程 schema 版本 */
|
|
15
|
+
var PROJECT_SCHEMA_VERSION = 1;
|
|
16
|
+
/** 撤销栈最大深度 */
|
|
17
|
+
var MAX_HISTORY_STEPS = 50;
|
|
18
|
+
/**
|
|
19
|
+
* 连续同质操作的合并时间窗口(毫秒)。
|
|
20
|
+
*
|
|
21
|
+
* 拖动滑块 / 画布内拖拽素材时会连续触发大量更新;窗口内相同 mergeKey 的记录
|
|
22
|
+
* 会被合并成一步撤销。取值兼顾「连续拖动算一次」与「停顿后再改算新一步」。
|
|
23
|
+
*/
|
|
24
|
+
var HISTORY_MERGE_WINDOW_MS = 800;
|
|
25
|
+
// src/lib/id.ts
|
|
26
|
+
/**
|
|
27
|
+
* 生成短 id。scope 仅用于可读性(例如 `clip_xxx`),不影响唯一性。
|
|
28
|
+
*/
|
|
29
|
+
function createId(scope) {
|
|
30
|
+
const id = nanoid(10);
|
|
31
|
+
return scope ? `${scope}_${id}` : id;
|
|
32
|
+
}
|
|
33
|
+
export { MAX_HISTORY_STEPS as a, HISTORY_MERGE_WINDOW_MS as i, DEFAULT_CLIP_VOLUME as n, PROJECT_SCHEMA_VERSION as o, DEFAULT_STILL_DURATION_US as r, createId as t };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=id-CJsdlGei.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id-CJsdlGei.js","names":[],"sources":["../../src/constants.ts","../../src/lib/id.ts"],"sourcesContent":["/**\n * cut-core 自有常量。\n *\n * 只放「逻辑层必须知道」的数值:默认时长、默认音量、工程 schema 版本、\n * 历史栈深度。UI 相关的量(缩放档位、面板高度、吸附像素阈值)留在宿主侧,\n * 避免组件库被时间轴界面的调参牵着走。\n */\n\n/** 图片 / 文字素材默认时长(微秒) */\nexport const DEFAULT_STILL_DURATION_US = 5_000_000\n\n/** 默认音量,避免多轨直接相加削波 */\nexport const DEFAULT_CLIP_VOLUME = 0.8\n\n/** 工程 schema 版本 */\nexport const PROJECT_SCHEMA_VERSION = 1\n\n/** 撤销栈最大深度 */\nexport const MAX_HISTORY_STEPS = 50\n\n/**\n * 连续同质操作的合并时间窗口(毫秒)。\n *\n * 拖动滑块 / 画布内拖拽素材时会连续触发大量更新;窗口内相同 mergeKey 的记录\n * 会被合并成一步撤销。取值兼顾「连续拖动算一次」与「停顿后再改算新一步」。\n */\nexport const HISTORY_MERGE_WINDOW_MS = 800\n","import { nanoid } from 'nanoid'\n\n/**\n * 生成短 id。scope 仅用于可读性(例如 `clip_xxx`),不影响唯一性。\n */\nexport function createId(scope?: string): string {\n const id = nanoid(10)\n return scope ? `${scope}_${id}` : id\n}\n"],"mappings":";;;;;;;;;;AASA,IAAa,4BAA4B;;AAGzC,IAAa,sBAAsB;;AAGnC,IAAa,yBAAyB;;AAGtC,IAAa,oBAAoB;;;;;;;AAQjC,IAAa,0BAA0B;;;;;ACrBvC,SAAgB,SAAS,OAAwB;CAC/C,MAAM,KAAK,OAAO,EAAE;CACpB,OAAO,QAAQ,GAAG,MAAM,GAAG,OAAO;AACpC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
let nanoid = require("nanoid");
|
|
2
|
+
// src/constants.ts
|
|
3
|
+
/**
|
|
4
|
+
* cut-core 自有常量。
|
|
5
|
+
*
|
|
6
|
+
* 只放「逻辑层必须知道」的数值:默认时长、默认音量、工程 schema 版本、
|
|
7
|
+
* 历史栈深度。UI 相关的量(缩放档位、面板高度、吸附像素阈值)留在宿主侧,
|
|
8
|
+
* 避免组件库被时间轴界面的调参牵着走。
|
|
9
|
+
*/
|
|
10
|
+
/** 图片 / 文字素材默认时长(微秒) */
|
|
11
|
+
var DEFAULT_STILL_DURATION_US = 5e6;
|
|
12
|
+
/** 默认音量,避免多轨直接相加削波 */
|
|
13
|
+
var DEFAULT_CLIP_VOLUME = .8;
|
|
14
|
+
/** 工程 schema 版本 */
|
|
15
|
+
var PROJECT_SCHEMA_VERSION = 1;
|
|
16
|
+
/** 撤销栈最大深度 */
|
|
17
|
+
var MAX_HISTORY_STEPS = 50;
|
|
18
|
+
/**
|
|
19
|
+
* 连续同质操作的合并时间窗口(毫秒)。
|
|
20
|
+
*
|
|
21
|
+
* 拖动滑块 / 画布内拖拽素材时会连续触发大量更新;窗口内相同 mergeKey 的记录
|
|
22
|
+
* 会被合并成一步撤销。取值兼顾「连续拖动算一次」与「停顿后再改算新一步」。
|
|
23
|
+
*/
|
|
24
|
+
var HISTORY_MERGE_WINDOW_MS = 800;
|
|
25
|
+
// src/lib/id.ts
|
|
26
|
+
/**
|
|
27
|
+
* 生成短 id。scope 仅用于可读性(例如 `clip_xxx`),不影响唯一性。
|
|
28
|
+
*/
|
|
29
|
+
function createId(scope) {
|
|
30
|
+
const id = (0, nanoid.nanoid)(10);
|
|
31
|
+
return scope ? `${scope}_${id}` : id;
|
|
32
|
+
}
|
|
33
|
+
Object.defineProperty(exports, "DEFAULT_CLIP_VOLUME", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
get: function() {
|
|
36
|
+
return DEFAULT_CLIP_VOLUME;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(exports, "DEFAULT_STILL_DURATION_US", {
|
|
40
|
+
enumerable: true,
|
|
41
|
+
get: function() {
|
|
42
|
+
return DEFAULT_STILL_DURATION_US;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
Object.defineProperty(exports, "HISTORY_MERGE_WINDOW_MS", {
|
|
46
|
+
enumerable: true,
|
|
47
|
+
get: function() {
|
|
48
|
+
return HISTORY_MERGE_WINDOW_MS;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
Object.defineProperty(exports, "MAX_HISTORY_STEPS", {
|
|
52
|
+
enumerable: true,
|
|
53
|
+
get: function() {
|
|
54
|
+
return MAX_HISTORY_STEPS;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
Object.defineProperty(exports, "PROJECT_SCHEMA_VERSION", {
|
|
58
|
+
enumerable: true,
|
|
59
|
+
get: function() {
|
|
60
|
+
return PROJECT_SCHEMA_VERSION;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
Object.defineProperty(exports, "createId", {
|
|
64
|
+
enumerable: true,
|
|
65
|
+
get: function() {
|
|
66
|
+
return createId;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=id-DaCxD4-5.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id-DaCxD4-5.cjs","names":[],"sources":["../../src/constants.ts","../../src/lib/id.ts"],"sourcesContent":["/**\n * cut-core 自有常量。\n *\n * 只放「逻辑层必须知道」的数值:默认时长、默认音量、工程 schema 版本、\n * 历史栈深度。UI 相关的量(缩放档位、面板高度、吸附像素阈值)留在宿主侧,\n * 避免组件库被时间轴界面的调参牵着走。\n */\n\n/** 图片 / 文字素材默认时长(微秒) */\nexport const DEFAULT_STILL_DURATION_US = 5_000_000\n\n/** 默认音量,避免多轨直接相加削波 */\nexport const DEFAULT_CLIP_VOLUME = 0.8\n\n/** 工程 schema 版本 */\nexport const PROJECT_SCHEMA_VERSION = 1\n\n/** 撤销栈最大深度 */\nexport const MAX_HISTORY_STEPS = 50\n\n/**\n * 连续同质操作的合并时间窗口(毫秒)。\n *\n * 拖动滑块 / 画布内拖拽素材时会连续触发大量更新;窗口内相同 mergeKey 的记录\n * 会被合并成一步撤销。取值兼顾「连续拖动算一次」与「停顿后再改算新一步」。\n */\nexport const HISTORY_MERGE_WINDOW_MS = 800\n","import { nanoid } from 'nanoid'\n\n/**\n * 生成短 id。scope 仅用于可读性(例如 `clip_xxx`),不影响唯一性。\n */\nexport function createId(scope?: string): string {\n const id = nanoid(10)\n return scope ? `${scope}_${id}` : id\n}\n"],"mappings":";;;;;;;;;;AASA,IAAa,4BAA4B;;AAGzC,IAAa,sBAAsB;;AAGnC,IAAa,yBAAyB;;AAGtC,IAAa,oBAAoB;;;;;;;AAQjC,IAAa,0BAA0B;;;;;ACrBvC,SAAgB,SAAS,OAAwB;CAC/C,MAAM,MAAA,GAAK,OAAA,OAAA,CAAO,EAAE;CACpB,OAAO,QAAQ,GAAG,MAAM,GAAG,OAAO;AACpC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_time = require("./time-
|
|
1
|
+
const require_time = require("./time-CYEd91qN.cjs");
|
|
2
2
|
// src/timeline/snap.ts
|
|
3
3
|
var DEFAULT_THRESHOLD_PX = 15;
|
|
4
4
|
var EMPTY_SNAP = {
|
|
@@ -117,20 +117,6 @@ function computeSnapWithHysteresis(input, releaseThresholdPx = 8) {
|
|
|
117
117
|
hits: [hit]
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
-
/** 二分查找:在已排序的时间数组中找最接近的值 */
|
|
121
|
-
function nearestSorted(sortedUs, targetUs) {
|
|
122
|
-
if (sortedUs.length === 0) return null;
|
|
123
|
-
let lo = 0;
|
|
124
|
-
let hi = sortedUs.length - 1;
|
|
125
|
-
while (lo < hi) {
|
|
126
|
-
const mid = lo + hi >> 1;
|
|
127
|
-
if ((sortedUs[mid] ?? 0) < targetUs) lo = mid + 1;
|
|
128
|
-
else hi = mid;
|
|
129
|
-
}
|
|
130
|
-
const hiVal = sortedUs[lo] ?? 0;
|
|
131
|
-
const loVal = sortedUs[Math.max(0, lo - 1)] ?? hiVal;
|
|
132
|
-
return Math.abs(hiVal - targetUs) <= Math.abs(targetUs - loVal) ? hiVal : loVal;
|
|
133
|
-
}
|
|
134
120
|
Object.defineProperty(exports, "EMPTY_SNAP", {
|
|
135
121
|
enumerable: true,
|
|
136
122
|
get: function() {
|
|
@@ -149,11 +135,5 @@ Object.defineProperty(exports, "computeSnapWithHysteresis", {
|
|
|
149
135
|
return computeSnapWithHysteresis;
|
|
150
136
|
}
|
|
151
137
|
});
|
|
152
|
-
Object.defineProperty(exports, "nearestSorted", {
|
|
153
|
-
enumerable: true,
|
|
154
|
-
get: function() {
|
|
155
|
-
return nearestSorted;
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
138
|
|
|
159
|
-
//# sourceMappingURL=snap-
|
|
139
|
+
//# sourceMappingURL=snap-Cfk15JVg.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snap-
|
|
1
|
+
{"version":3,"file":"snap-Cfk15JVg.cjs","names":["usToPx"],"sources":["../../src/timeline/snap.ts"],"sourcesContent":["/**\n * 吸附算法(纯函数)。\n *\n * 设计要点:\n * - 候选点在**像素**空间比较,避免缩放变化导致阈值语义漂移。\n * - 一次拖拽内可同时吸附「起点」和「终点」,但返回结果必须保留「是哪条边被吸住」。\n * 这是修一个真实 bug 的关键:旧实现把两条边都换算成「新的起点」,\n * 右侧裁剪时起点是固定的,于是 `duration = snappedStart - originStart`\n * 会算出「目标点与当前终点的差值」,片段被压成最短时长(视觉上闪到左边)。\n * - 吸附结果用「位移量 deltaUs」表达:无论调用方改的是 start 还是 duration,\n * 都只需要把 delta 加到对应的边所在的量上。\n * - 滞后(hysteresis):已吸附后,只有偏离目标超过 `releaseThresholdPx` 才脱离,\n * 且释放阈值必须**大于**吸附阈值,否则「吸住」在数学上不可能成立\n * (旧实现释放阈值 8px < 吸附阈值 15px,滞后分支实际永远走不到)。\n */\n\nimport type { Px } from './time'\nimport { usToPx } from './time'\n\nexport type SnapTargetKind = 'clip-start' | 'clip-end' | 'playhead' | 'zero' | 'track-end'\n\nexport interface SnapTarget {\n us: number\n kind: SnapTargetKind\n /** 产生该目标的实体 id(clip / track),用于 UI 高亮 */\n sourceId?: string\n}\n\nexport type SnapEdge = 'start' | 'end'\n\nexport interface SnapInput {\n /** 被拖动区间的起点(微秒) */\n startUs: number\n /** 被拖动区间的时长(微秒) */\n durationUs: number\n targets: SnapTarget[]\n /** 每秒像素 */\n pxPerSecond: number\n thresholdPx?: number\n /** 上一帧的吸附结果(用于滞后判断) */\n previous?: SnapResult | null\n /**\n * 参与吸附的边。默认两端都参与。\n *\n * 裁剪时只让被拖动的边参与:右侧裁剪传 ['end']。\n * 旧实现即使只传 ['end'] 也会把结果换算成「新起点」,导致整段位移,已修正。\n */\n edges?: SnapEdge[]\n}\n\nexport interface SnapEdgeHit {\n /** 被吸附的是拖动区间的哪一端 */\n edge: SnapEdge\n /** 吸附到的目标时间(微秒) */\n targetUs: number\n kind: SnapTargetKind\n sourceId?: string\n /** 该目标对应的吸附线像素位置 */\n linePx: Px\n /** 该边吸附前的原始位置(微秒) */\n edgeUs: number\n /** 需要施加的位移:targetUs - edgeUs */\n deltaUs: number\n /** 吸附前该边与目标的像素距离 */\n distPx: number\n /** 命中时拖动区间的新起点(微秒),便于 move 模式直接使用 */\n nextStartUs: number\n}\n\nexport interface SnapResult {\n /** 命中吸附的边;null 表示未吸附 */\n edge: SnapEdge | null\n /** 吸附目标时间;未吸附为 null */\n targetUs: number | null\n /** 需要施加的位移(微秒);未吸附为 0 */\n deltaUs: number\n /** 需要绘制的辅助线(像素) */\n guidePx: Px[]\n /** 所有命中的边(可能两端同时命中不同目标) */\n hits: SnapEdgeHit[]\n}\n\nconst DEFAULT_THRESHOLD_PX = 15\n\nexport const EMPTY_SNAP: SnapResult = {\n edge: null,\n targetUs: null,\n deltaUs: 0,\n guidePx: [],\n hits: [],\n}\n\nfunction pxOf(us: number, pxPerSecond: number): number {\n return usToPx(us, pxPerSecond)\n}\n\n/**\n * 计算吸附结果。\n *\n * @param input 拖动区间 + 目标 + 缩放\n * @returns 命中的边、位移量、辅助线;未命中时 edge 为 null\n */\nexport function computeSnap(input: SnapInput): SnapResult {\n const { startUs, durationUs, targets, pxPerSecond } = input\n const threshold = input.thresholdPx ?? DEFAULT_THRESHOLD_PX\n const endUs = startUs + durationUs\n const activeEdges = new Set<SnapEdge>(input.edges ?? ['start', 'end'])\n const candidates: Array<{ edge: SnapEdge; us: number }> = [\n { edge: 'start', us: startUs },\n { edge: 'end', us: endUs },\n ]\n const edges = candidates.filter((edgeInfo) => activeEdges.has(edgeInfo.edge))\n\n const hits: SnapEdgeHit[] = []\n\n for (const edgeInfo of edges) {\n const edgePx = pxOf(edgeInfo.us, pxPerSecond)\n let best: { target: SnapTarget; distPx: number; px: number } | null = null\n\n for (const target of targets) {\n const targetPx = pxOf(target.us, pxPerSecond)\n const distPx = Math.abs(targetPx - edgePx)\n if (distPx > threshold) continue\n if (best == null || distPx < best.distPx) {\n best = { target, distPx, px: targetPx }\n }\n }\n\n if (best == null) continue\n\n const deltaUs = best.target.us - edgeInfo.us\n hits.push({\n edge: edgeInfo.edge,\n targetUs: best.target.us,\n kind: best.target.kind,\n sourceId: best.target.sourceId,\n linePx: best.px as Px,\n edgeUs: edgeInfo.us,\n deltaUs,\n distPx: best.distPx,\n nextStartUs: startUs + deltaUs,\n })\n }\n\n if (hits.length === 0) return EMPTY_SNAP\n\n let primary = hits[0]!\n for (const hit of hits) {\n if (hit.distPx < primary.distPx) primary = hit\n }\n\n return {\n edge: primary.edge,\n targetUs: primary.targetUs,\n deltaUs: primary.deltaUs,\n guidePx: uniquePx(hits.map((h) => h.linePx)),\n hits,\n }\n}\n\nfunction uniquePx(list: Px[]): Px[] {\n const seen = new Set<number>()\n const out: Px[] = []\n for (const p of list) {\n const key = Math.round(p * 2) / 2\n if (seen.has(key)) continue\n seen.add(key)\n out.push(p)\n }\n return out\n}\n\n/**\n * 带滞后的吸附。\n *\n * 若当前帧没有命中任何目标,但上一帧吸附在同一目标上、且该边偏离目标仍未超过\n * `releaseThresholdPx`,则维持原吸附。`releaseThresholdPx` 必须大于吸附阈值,\n * 否则这个分支永远不可能生效(旧实现就是如此)。\n */\nexport function computeSnapWithHysteresis(\n input: SnapInput,\n releaseThresholdPx = 8,\n): SnapResult {\n const fresh = computeSnap(input)\n const prev = input.previous\n if (fresh.edge != null) return fresh\n if (prev == null || prev.edge == null || prev.targetUs == null) return fresh\n\n const active = new Set<SnapEdge>(input.edges ?? ['start', 'end'])\n if (!active.has(prev.edge)) return fresh\n\n const edgeUs = prev.edge === 'start' ? input.startUs : input.startUs + input.durationUs\n const targetUs = prev.targetUs\n const distPx = Math.abs(pxOf(targetUs, input.pxPerSecond) - pxOf(edgeUs, input.pxPerSecond))\n if (distPx > releaseThresholdPx) return fresh\n\n const previousHit =\n prev.hits.find((h) => h.edge === prev.edge && h.targetUs === targetUs) ?? prev.hits[0]\n const linePx = pxOf(targetUs, input.pxPerSecond) as Px\n const hit: SnapEdgeHit = {\n edge: prev.edge,\n targetUs,\n kind: previousHit?.kind ?? 'zero',\n sourceId: previousHit?.sourceId,\n linePx,\n edgeUs,\n deltaUs: targetUs - edgeUs,\n distPx,\n nextStartUs: input.startUs + (targetUs - edgeUs),\n }\n\n return {\n edge: prev.edge,\n targetUs,\n deltaUs: targetUs - edgeUs,\n guidePx: [linePx],\n hits: [hit],\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { p as usToPx } from "./time-
|
|
1
|
+
import { p as usToPx } from "./time-ChF1GwCy.js";
|
|
2
2
|
// src/timeline/snap.ts
|
|
3
3
|
var DEFAULT_THRESHOLD_PX = 15;
|
|
4
4
|
var EMPTY_SNAP = {
|
|
@@ -117,20 +117,6 @@ function computeSnapWithHysteresis(input, releaseThresholdPx = 8) {
|
|
|
117
117
|
hits: [hit]
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
-
|
|
121
|
-
function nearestSorted(sortedUs, targetUs) {
|
|
122
|
-
if (sortedUs.length === 0) return null;
|
|
123
|
-
let lo = 0;
|
|
124
|
-
let hi = sortedUs.length - 1;
|
|
125
|
-
while (lo < hi) {
|
|
126
|
-
const mid = lo + hi >> 1;
|
|
127
|
-
if ((sortedUs[mid] ?? 0) < targetUs) lo = mid + 1;
|
|
128
|
-
else hi = mid;
|
|
129
|
-
}
|
|
130
|
-
const hiVal = sortedUs[lo] ?? 0;
|
|
131
|
-
const loVal = sortedUs[Math.max(0, lo - 1)] ?? hiVal;
|
|
132
|
-
return Math.abs(hiVal - targetUs) <= Math.abs(targetUs - loVal) ? hiVal : loVal;
|
|
133
|
-
}
|
|
134
|
-
export { nearestSorted as i, computeSnap as n, computeSnapWithHysteresis as r, EMPTY_SNAP as t };
|
|
120
|
+
export { computeSnap as n, computeSnapWithHysteresis as r, EMPTY_SNAP as t };
|
|
135
121
|
|
|
136
|
-
//# sourceMappingURL=snap-
|
|
122
|
+
//# sourceMappingURL=snap-DpkVPVJN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snap-
|
|
1
|
+
{"version":3,"file":"snap-DpkVPVJN.js","names":[],"sources":["../../src/timeline/snap.ts"],"sourcesContent":["/**\n * 吸附算法(纯函数)。\n *\n * 设计要点:\n * - 候选点在**像素**空间比较,避免缩放变化导致阈值语义漂移。\n * - 一次拖拽内可同时吸附「起点」和「终点」,但返回结果必须保留「是哪条边被吸住」。\n * 这是修一个真实 bug 的关键:旧实现把两条边都换算成「新的起点」,\n * 右侧裁剪时起点是固定的,于是 `duration = snappedStart - originStart`\n * 会算出「目标点与当前终点的差值」,片段被压成最短时长(视觉上闪到左边)。\n * - 吸附结果用「位移量 deltaUs」表达:无论调用方改的是 start 还是 duration,\n * 都只需要把 delta 加到对应的边所在的量上。\n * - 滞后(hysteresis):已吸附后,只有偏离目标超过 `releaseThresholdPx` 才脱离,\n * 且释放阈值必须**大于**吸附阈值,否则「吸住」在数学上不可能成立\n * (旧实现释放阈值 8px < 吸附阈值 15px,滞后分支实际永远走不到)。\n */\n\nimport type { Px } from './time'\nimport { usToPx } from './time'\n\nexport type SnapTargetKind = 'clip-start' | 'clip-end' | 'playhead' | 'zero' | 'track-end'\n\nexport interface SnapTarget {\n us: number\n kind: SnapTargetKind\n /** 产生该目标的实体 id(clip / track),用于 UI 高亮 */\n sourceId?: string\n}\n\nexport type SnapEdge = 'start' | 'end'\n\nexport interface SnapInput {\n /** 被拖动区间的起点(微秒) */\n startUs: number\n /** 被拖动区间的时长(微秒) */\n durationUs: number\n targets: SnapTarget[]\n /** 每秒像素 */\n pxPerSecond: number\n thresholdPx?: number\n /** 上一帧的吸附结果(用于滞后判断) */\n previous?: SnapResult | null\n /**\n * 参与吸附的边。默认两端都参与。\n *\n * 裁剪时只让被拖动的边参与:右侧裁剪传 ['end']。\n * 旧实现即使只传 ['end'] 也会把结果换算成「新起点」,导致整段位移,已修正。\n */\n edges?: SnapEdge[]\n}\n\nexport interface SnapEdgeHit {\n /** 被吸附的是拖动区间的哪一端 */\n edge: SnapEdge\n /** 吸附到的目标时间(微秒) */\n targetUs: number\n kind: SnapTargetKind\n sourceId?: string\n /** 该目标对应的吸附线像素位置 */\n linePx: Px\n /** 该边吸附前的原始位置(微秒) */\n edgeUs: number\n /** 需要施加的位移:targetUs - edgeUs */\n deltaUs: number\n /** 吸附前该边与目标的像素距离 */\n distPx: number\n /** 命中时拖动区间的新起点(微秒),便于 move 模式直接使用 */\n nextStartUs: number\n}\n\nexport interface SnapResult {\n /** 命中吸附的边;null 表示未吸附 */\n edge: SnapEdge | null\n /** 吸附目标时间;未吸附为 null */\n targetUs: number | null\n /** 需要施加的位移(微秒);未吸附为 0 */\n deltaUs: number\n /** 需要绘制的辅助线(像素) */\n guidePx: Px[]\n /** 所有命中的边(可能两端同时命中不同目标) */\n hits: SnapEdgeHit[]\n}\n\nconst DEFAULT_THRESHOLD_PX = 15\n\nexport const EMPTY_SNAP: SnapResult = {\n edge: null,\n targetUs: null,\n deltaUs: 0,\n guidePx: [],\n hits: [],\n}\n\nfunction pxOf(us: number, pxPerSecond: number): number {\n return usToPx(us, pxPerSecond)\n}\n\n/**\n * 计算吸附结果。\n *\n * @param input 拖动区间 + 目标 + 缩放\n * @returns 命中的边、位移量、辅助线;未命中时 edge 为 null\n */\nexport function computeSnap(input: SnapInput): SnapResult {\n const { startUs, durationUs, targets, pxPerSecond } = input\n const threshold = input.thresholdPx ?? DEFAULT_THRESHOLD_PX\n const endUs = startUs + durationUs\n const activeEdges = new Set<SnapEdge>(input.edges ?? ['start', 'end'])\n const candidates: Array<{ edge: SnapEdge; us: number }> = [\n { edge: 'start', us: startUs },\n { edge: 'end', us: endUs },\n ]\n const edges = candidates.filter((edgeInfo) => activeEdges.has(edgeInfo.edge))\n\n const hits: SnapEdgeHit[] = []\n\n for (const edgeInfo of edges) {\n const edgePx = pxOf(edgeInfo.us, pxPerSecond)\n let best: { target: SnapTarget; distPx: number; px: number } | null = null\n\n for (const target of targets) {\n const targetPx = pxOf(target.us, pxPerSecond)\n const distPx = Math.abs(targetPx - edgePx)\n if (distPx > threshold) continue\n if (best == null || distPx < best.distPx) {\n best = { target, distPx, px: targetPx }\n }\n }\n\n if (best == null) continue\n\n const deltaUs = best.target.us - edgeInfo.us\n hits.push({\n edge: edgeInfo.edge,\n targetUs: best.target.us,\n kind: best.target.kind,\n sourceId: best.target.sourceId,\n linePx: best.px as Px,\n edgeUs: edgeInfo.us,\n deltaUs,\n distPx: best.distPx,\n nextStartUs: startUs + deltaUs,\n })\n }\n\n if (hits.length === 0) return EMPTY_SNAP\n\n let primary = hits[0]!\n for (const hit of hits) {\n if (hit.distPx < primary.distPx) primary = hit\n }\n\n return {\n edge: primary.edge,\n targetUs: primary.targetUs,\n deltaUs: primary.deltaUs,\n guidePx: uniquePx(hits.map((h) => h.linePx)),\n hits,\n }\n}\n\nfunction uniquePx(list: Px[]): Px[] {\n const seen = new Set<number>()\n const out: Px[] = []\n for (const p of list) {\n const key = Math.round(p * 2) / 2\n if (seen.has(key)) continue\n seen.add(key)\n out.push(p)\n }\n return out\n}\n\n/**\n * 带滞后的吸附。\n *\n * 若当前帧没有命中任何目标,但上一帧吸附在同一目标上、且该边偏离目标仍未超过\n * `releaseThresholdPx`,则维持原吸附。`releaseThresholdPx` 必须大于吸附阈值,\n * 否则这个分支永远不可能生效(旧实现就是如此)。\n */\nexport function computeSnapWithHysteresis(\n input: SnapInput,\n releaseThresholdPx = 8,\n): SnapResult {\n const fresh = computeSnap(input)\n const prev = input.previous\n if (fresh.edge != null) return fresh\n if (prev == null || prev.edge == null || prev.targetUs == null) return fresh\n\n const active = new Set<SnapEdge>(input.edges ?? ['start', 'end'])\n if (!active.has(prev.edge)) return fresh\n\n const edgeUs = prev.edge === 'start' ? input.startUs : input.startUs + input.durationUs\n const targetUs = prev.targetUs\n const distPx = Math.abs(pxOf(targetUs, input.pxPerSecond) - pxOf(edgeUs, input.pxPerSecond))\n if (distPx > releaseThresholdPx) return fresh\n\n const previousHit =\n prev.hits.find((h) => h.edge === prev.edge && h.targetUs === targetUs) ?? prev.hits[0]\n const linePx = pxOf(targetUs, input.pxPerSecond) as Px\n const hit: SnapEdgeHit = {\n edge: prev.edge,\n targetUs,\n kind: previousHit?.kind ?? 'zero',\n sourceId: previousHit?.sourceId,\n linePx,\n edgeUs,\n deltaUs: targetUs - edgeUs,\n distPx,\n nextStartUs: input.startUs + (targetUs - edgeUs),\n }\n\n return {\n edge: prev.edge,\n targetUs,\n deltaUs: targetUs - edgeUs,\n guidePx: [linePx],\n hits: [hit],\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-B2SfzNBj.cjs");
|
|
2
|
+
const require_time = require("./time-CYEd91qN.cjs");
|
|
3
|
+
// src/media/thumbnails.ts
|
|
4
|
+
var thumbnails_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
5
|
+
generateThumbnails: () => generateThumbnails,
|
|
6
|
+
regenerateThumbnails: () => regenerateThumbnails,
|
|
7
|
+
releaseThumbnails: () => releaseThumbnails
|
|
8
|
+
});
|
|
9
|
+
var cache = /* @__PURE__ */ new Map();
|
|
10
|
+
/**
|
|
11
|
+
* 生成等间隔缩略图条。
|
|
12
|
+
*
|
|
13
|
+
* @param clip MP4Clip 实例(调用方负责生命周期)
|
|
14
|
+
* @param assetId 缓存 key
|
|
15
|
+
* @param count 期望张数
|
|
16
|
+
* @param durationUs 素材总时长,用来算 step
|
|
17
|
+
* @param width 单张宽度
|
|
18
|
+
*/
|
|
19
|
+
async function generateThumbnails(clip, assetId, count, durationUs, width = 100) {
|
|
20
|
+
const cached = cache.get(assetId);
|
|
21
|
+
if (cached) return cached;
|
|
22
|
+
return regenerateThumbnails(clip, assetId, count, durationUs, width);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 绕过缓存重新抽帧。
|
|
26
|
+
*
|
|
27
|
+
* 重新打开工程后素材 id 不变,但旧的 `blob:` URL 已经失效;缓存 key 是 assetId,
|
|
28
|
+
* 直接走 `generateThumbnails` 会命中失效缓存(卡片显示破图/占位)。
|
|
29
|
+
* 这里先释放旧缓存再抽一次。
|
|
30
|
+
*/
|
|
31
|
+
async function regenerateThumbnails(clip, assetId, count, durationUs, width = 100) {
|
|
32
|
+
if (cache.has(assetId)) releaseThumbnails(assetId);
|
|
33
|
+
const stepUs = Math.max(Math.floor(durationUs / count), require_time.usToMs(200));
|
|
34
|
+
const raw = await clip.thumbnails(width, {
|
|
35
|
+
start: 0,
|
|
36
|
+
end: Math.max(0, durationUs - 1),
|
|
37
|
+
step: stepUs
|
|
38
|
+
});
|
|
39
|
+
const picked = [];
|
|
40
|
+
if (raw.length <= count) picked.push(...raw);
|
|
41
|
+
else {
|
|
42
|
+
const stride = raw.length / count;
|
|
43
|
+
for (let i = 0; i < count; i += 1) {
|
|
44
|
+
const item = raw[Math.min(raw.length - 1, Math.floor(i * stride))];
|
|
45
|
+
if (item) picked.push(item);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const frames = picked.map((item) => ({
|
|
49
|
+
ts: item.ts,
|
|
50
|
+
url: URL.createObjectURL(item.img)
|
|
51
|
+
}));
|
|
52
|
+
cache.set(assetId, frames);
|
|
53
|
+
return frames;
|
|
54
|
+
}
|
|
55
|
+
/** 释放某素材的缩略图 URL */
|
|
56
|
+
function releaseThumbnails(assetId) {
|
|
57
|
+
const frames = cache.get(assetId);
|
|
58
|
+
if (!frames) return;
|
|
59
|
+
for (const f of frames) URL.revokeObjectURL(f.url);
|
|
60
|
+
cache.delete(assetId);
|
|
61
|
+
}
|
|
62
|
+
Object.defineProperty(exports, "generateThumbnails", {
|
|
63
|
+
enumerable: true,
|
|
64
|
+
get: function() {
|
|
65
|
+
return generateThumbnails;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
Object.defineProperty(exports, "releaseThumbnails", {
|
|
69
|
+
enumerable: true,
|
|
70
|
+
get: function() {
|
|
71
|
+
return releaseThumbnails;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
Object.defineProperty(exports, "thumbnails_exports", {
|
|
75
|
+
enumerable: true,
|
|
76
|
+
get: function() {
|
|
77
|
+
return thumbnails_exports;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=thumbnails-DEjnao5r.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnails-DEjnao5r.cjs","names":["usToMs"],"sources":["../../src/media/thumbnails.ts"],"sourcesContent":["/**\r\n * 缩略图生成。`MP4Clip.thumbnails()` 封装 + 内存缓存。\r\n *\r\n * 注意:`thumbnails()` 默认每个关键帧一张;调用方若只要 N 张,应传 `step`。\r\n * 返回值是 Blob,转成 object URL 后要记得在素材移除时 revoke。\r\n */\r\n\r\nimport type { Us } from '../timeline/time'\r\nimport { usToMs } from '../timeline/time'\r\n\r\nexport interface ThumbnailFrame {\r\n ts: number\r\n url: string\r\n}\r\n\r\nconst cache = new Map<string, ThumbnailFrame[]>()\r\n\r\n/** 缩略图抽帧所需的最小接口(避免 core/media 直接依赖 @webav) */\r\nexport interface ThumbnailSource {\r\n thumbnails(\r\n imgWidth?: number,\r\n opts?: { start?: number; end?: number; step?: number },\r\n ): Promise<Array<{ ts: number; img: Blob }>>\r\n}\r\n\r\n/**\r\n * 生成等间隔缩略图条。\r\n *\r\n * @param clip MP4Clip 实例(调用方负责生命周期)\r\n * @param assetId 缓存 key\r\n * @param count 期望张数\r\n * @param durationUs 素材总时长,用来算 step\r\n * @param width 单张宽度\r\n */\r\nexport async function generateThumbnails(\r\n clip: ThumbnailSource,\r\n assetId: string,\r\n count: number,\r\n durationUs: number,\r\n width = 100,\r\n): Promise<ThumbnailFrame[]> {\r\n const cached = cache.get(assetId)\r\n if (cached) return cached\r\n return regenerateThumbnails(clip, assetId, count, durationUs, width)\r\n}\r\n\r\n/**\r\n * 绕过缓存重新抽帧。\r\n *\r\n * 重新打开工程后素材 id 不变,但旧的 `blob:` URL 已经失效;缓存 key 是 assetId,\r\n * 直接走 `generateThumbnails` 会命中失效缓存(卡片显示破图/占位)。\r\n * 这里先释放旧缓存再抽一次。\r\n */\r\nexport async function regenerateThumbnails(\r\n clip: ThumbnailSource,\r\n assetId: string,\r\n count: number,\r\n durationUs: number,\r\n width = 100,\r\n): Promise<ThumbnailFrame[]> {\r\n if (cache.has(assetId)) releaseThumbnails(assetId)\r\n\r\n const stepUs = Math.max(Math.floor(durationUs / count), usToMs(200))\r\n const raw = await clip.thumbnails(width, {\r\n start: 0,\r\n end: Math.max(0, durationUs - 1),\r\n step: stepUs,\r\n })\r\n\r\n // step 模式可能产生远多于 count 的张数(步长被内部按关键帧吸附)。\r\n // 这里做一次等距抽取,保证最多 count 张。\r\n const picked: Array<{ ts: number; img: Blob }> = []\r\n if (raw.length <= count) {\r\n picked.push(...raw)\r\n } else {\r\n const stride = raw.length / count\r\n for (let i = 0; i < count; i += 1) {\r\n const item = raw[Math.min(raw.length - 1, Math.floor(i * stride))]\r\n if (item) picked.push(item)\r\n }\r\n }\r\n\r\n const frames = picked.map((item) => ({\r\n ts: item.ts,\r\n url: URL.createObjectURL(item.img),\r\n }))\r\n cache.set(assetId, frames)\r\n return frames\r\n}\r\n\r\n/** 释放某素材的缩略图 URL */\r\nexport function releaseThumbnails(assetId: string): void {\r\n const frames = cache.get(assetId)\r\n if (!frames) return\r\n for (const f of frames) URL.revokeObjectURL(f.url)\r\n cache.delete(assetId)\r\n}\r\n\r\n/** 抽帧结果转成时间戳列表(Lab 使用) */\r\nexport function toTimestampList(frames: Array<{ ts: number }>): Us[] {\r\n return frames.map((f) => f.ts as Us)\r\n}\r\n"],"mappings":";;;;;;;;AAeA,IAAM,wBAAQ,IAAI,IAA8B;;;;;;;;;;AAmBhD,eAAsB,mBACpB,MACA,SACA,OACA,YACA,QAAQ,KACmB;CAC3B,MAAM,SAAS,MAAM,IAAI,OAAO;CAChC,IAAI,QAAQ,OAAO;CACnB,OAAO,qBAAqB,MAAM,SAAS,OAAO,YAAY,KAAK;AACrE;;;;;;;;AASA,eAAsB,qBACpB,MACA,SACA,OACA,YACA,QAAQ,KACmB;CAC3B,IAAI,MAAM,IAAI,OAAO,GAAG,kBAAkB,OAAO;CAEjD,MAAM,SAAS,KAAK,IAAI,KAAK,MAAM,aAAa,KAAK,GAAGA,aAAAA,OAAO,GAAG,CAAC;CACnE,MAAM,MAAM,MAAM,KAAK,WAAW,OAAO;EACvC,OAAO;EACP,KAAK,KAAK,IAAI,GAAG,aAAa,CAAC;EAC/B,MAAM;CACR,CAAC;CAID,MAAM,SAA2C,CAAC;CAClD,IAAI,IAAI,UAAU,OAChB,OAAO,KAAK,GAAG,GAAG;MACb;EACL,MAAM,SAAS,IAAI,SAAS;EAC5B,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,GAAG;GACjC,MAAM,OAAO,IAAI,KAAK,IAAI,IAAI,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM,CAAC;GAChE,IAAI,MAAM,OAAO,KAAK,IAAI;EAC5B;CACF;CAEA,MAAM,SAAS,OAAO,KAAK,UAAU;EACnC,IAAI,KAAK;EACT,KAAK,IAAI,gBAAgB,KAAK,GAAG;CACnC,EAAE;CACF,MAAM,IAAI,SAAS,MAAM;CACzB,OAAO;AACT;;AAGA,SAAgB,kBAAkB,SAAuB;CACvD,MAAM,SAAS,MAAM,IAAI,OAAO;CAChC,IAAI,CAAC,QAAQ;CACb,KAAK,MAAM,KAAK,QAAQ,IAAI,gBAAgB,EAAE,GAAG;CACjD,MAAM,OAAO,OAAO;AACtB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
|
|
2
|
+
import { f as usToMs } from "./time-ChF1GwCy.js";
|
|
3
|
+
// src/media/thumbnails.ts
|
|
4
|
+
var thumbnails_exports = /* @__PURE__ */ __exportAll({
|
|
5
|
+
generateThumbnails: () => generateThumbnails,
|
|
6
|
+
regenerateThumbnails: () => regenerateThumbnails,
|
|
7
|
+
releaseThumbnails: () => releaseThumbnails
|
|
8
|
+
});
|
|
9
|
+
var cache = /* @__PURE__ */ new Map();
|
|
10
|
+
/**
|
|
11
|
+
* 生成等间隔缩略图条。
|
|
12
|
+
*
|
|
13
|
+
* @param clip MP4Clip 实例(调用方负责生命周期)
|
|
14
|
+
* @param assetId 缓存 key
|
|
15
|
+
* @param count 期望张数
|
|
16
|
+
* @param durationUs 素材总时长,用来算 step
|
|
17
|
+
* @param width 单张宽度
|
|
18
|
+
*/
|
|
19
|
+
async function generateThumbnails(clip, assetId, count, durationUs, width = 100) {
|
|
20
|
+
const cached = cache.get(assetId);
|
|
21
|
+
if (cached) return cached;
|
|
22
|
+
return regenerateThumbnails(clip, assetId, count, durationUs, width);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 绕过缓存重新抽帧。
|
|
26
|
+
*
|
|
27
|
+
* 重新打开工程后素材 id 不变,但旧的 `blob:` URL 已经失效;缓存 key 是 assetId,
|
|
28
|
+
* 直接走 `generateThumbnails` 会命中失效缓存(卡片显示破图/占位)。
|
|
29
|
+
* 这里先释放旧缓存再抽一次。
|
|
30
|
+
*/
|
|
31
|
+
async function regenerateThumbnails(clip, assetId, count, durationUs, width = 100) {
|
|
32
|
+
if (cache.has(assetId)) releaseThumbnails(assetId);
|
|
33
|
+
const stepUs = Math.max(Math.floor(durationUs / count), usToMs(200));
|
|
34
|
+
const raw = await clip.thumbnails(width, {
|
|
35
|
+
start: 0,
|
|
36
|
+
end: Math.max(0, durationUs - 1),
|
|
37
|
+
step: stepUs
|
|
38
|
+
});
|
|
39
|
+
const picked = [];
|
|
40
|
+
if (raw.length <= count) picked.push(...raw);
|
|
41
|
+
else {
|
|
42
|
+
const stride = raw.length / count;
|
|
43
|
+
for (let i = 0; i < count; i += 1) {
|
|
44
|
+
const item = raw[Math.min(raw.length - 1, Math.floor(i * stride))];
|
|
45
|
+
if (item) picked.push(item);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const frames = picked.map((item) => ({
|
|
49
|
+
ts: item.ts,
|
|
50
|
+
url: URL.createObjectURL(item.img)
|
|
51
|
+
}));
|
|
52
|
+
cache.set(assetId, frames);
|
|
53
|
+
return frames;
|
|
54
|
+
}
|
|
55
|
+
/** 释放某素材的缩略图 URL */
|
|
56
|
+
function releaseThumbnails(assetId) {
|
|
57
|
+
const frames = cache.get(assetId);
|
|
58
|
+
if (!frames) return;
|
|
59
|
+
for (const f of frames) URL.revokeObjectURL(f.url);
|
|
60
|
+
cache.delete(assetId);
|
|
61
|
+
}
|
|
62
|
+
export { releaseThumbnails as n, thumbnails_exports as r, generateThumbnails as t };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=thumbnails-qH0dX_Zs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnails-qH0dX_Zs.js","names":[],"sources":["../../src/media/thumbnails.ts"],"sourcesContent":["/**\r\n * 缩略图生成。`MP4Clip.thumbnails()` 封装 + 内存缓存。\r\n *\r\n * 注意:`thumbnails()` 默认每个关键帧一张;调用方若只要 N 张,应传 `step`。\r\n * 返回值是 Blob,转成 object URL 后要记得在素材移除时 revoke。\r\n */\r\n\r\nimport type { Us } from '../timeline/time'\r\nimport { usToMs } from '../timeline/time'\r\n\r\nexport interface ThumbnailFrame {\r\n ts: number\r\n url: string\r\n}\r\n\r\nconst cache = new Map<string, ThumbnailFrame[]>()\r\n\r\n/** 缩略图抽帧所需的最小接口(避免 core/media 直接依赖 @webav) */\r\nexport interface ThumbnailSource {\r\n thumbnails(\r\n imgWidth?: number,\r\n opts?: { start?: number; end?: number; step?: number },\r\n ): Promise<Array<{ ts: number; img: Blob }>>\r\n}\r\n\r\n/**\r\n * 生成等间隔缩略图条。\r\n *\r\n * @param clip MP4Clip 实例(调用方负责生命周期)\r\n * @param assetId 缓存 key\r\n * @param count 期望张数\r\n * @param durationUs 素材总时长,用来算 step\r\n * @param width 单张宽度\r\n */\r\nexport async function generateThumbnails(\r\n clip: ThumbnailSource,\r\n assetId: string,\r\n count: number,\r\n durationUs: number,\r\n width = 100,\r\n): Promise<ThumbnailFrame[]> {\r\n const cached = cache.get(assetId)\r\n if (cached) return cached\r\n return regenerateThumbnails(clip, assetId, count, durationUs, width)\r\n}\r\n\r\n/**\r\n * 绕过缓存重新抽帧。\r\n *\r\n * 重新打开工程后素材 id 不变,但旧的 `blob:` URL 已经失效;缓存 key 是 assetId,\r\n * 直接走 `generateThumbnails` 会命中失效缓存(卡片显示破图/占位)。\r\n * 这里先释放旧缓存再抽一次。\r\n */\r\nexport async function regenerateThumbnails(\r\n clip: ThumbnailSource,\r\n assetId: string,\r\n count: number,\r\n durationUs: number,\r\n width = 100,\r\n): Promise<ThumbnailFrame[]> {\r\n if (cache.has(assetId)) releaseThumbnails(assetId)\r\n\r\n const stepUs = Math.max(Math.floor(durationUs / count), usToMs(200))\r\n const raw = await clip.thumbnails(width, {\r\n start: 0,\r\n end: Math.max(0, durationUs - 1),\r\n step: stepUs,\r\n })\r\n\r\n // step 模式可能产生远多于 count 的张数(步长被内部按关键帧吸附)。\r\n // 这里做一次等距抽取,保证最多 count 张。\r\n const picked: Array<{ ts: number; img: Blob }> = []\r\n if (raw.length <= count) {\r\n picked.push(...raw)\r\n } else {\r\n const stride = raw.length / count\r\n for (let i = 0; i < count; i += 1) {\r\n const item = raw[Math.min(raw.length - 1, Math.floor(i * stride))]\r\n if (item) picked.push(item)\r\n }\r\n }\r\n\r\n const frames = picked.map((item) => ({\r\n ts: item.ts,\r\n url: URL.createObjectURL(item.img),\r\n }))\r\n cache.set(assetId, frames)\r\n return frames\r\n}\r\n\r\n/** 释放某素材的缩略图 URL */\r\nexport function releaseThumbnails(assetId: string): void {\r\n const frames = cache.get(assetId)\r\n if (!frames) return\r\n for (const f of frames) URL.revokeObjectURL(f.url)\r\n cache.delete(assetId)\r\n}\r\n\r\n/** 抽帧结果转成时间戳列表(Lab 使用) */\r\nexport function toTimestampList(frames: Array<{ ts: number }>): Us[] {\r\n return frames.map((f) => f.ts as Us)\r\n}\r\n"],"mappings":";;;;;;;;AAeA,IAAM,wBAAQ,IAAI,IAA8B;;;;;;;;;;AAmBhD,eAAsB,mBACpB,MACA,SACA,OACA,YACA,QAAQ,KACmB;CAC3B,MAAM,SAAS,MAAM,IAAI,OAAO;CAChC,IAAI,QAAQ,OAAO;CACnB,OAAO,qBAAqB,MAAM,SAAS,OAAO,YAAY,KAAK;AACrE;;;;;;;;AASA,eAAsB,qBACpB,MACA,SACA,OACA,YACA,QAAQ,KACmB;CAC3B,IAAI,MAAM,IAAI,OAAO,GAAG,kBAAkB,OAAO;CAEjD,MAAM,SAAS,KAAK,IAAI,KAAK,MAAM,aAAa,KAAK,GAAG,OAAO,GAAG,CAAC;CACnE,MAAM,MAAM,MAAM,KAAK,WAAW,OAAO;EACvC,OAAO;EACP,KAAK,KAAK,IAAI,GAAG,aAAa,CAAC;EAC/B,MAAM;CACR,CAAC;CAID,MAAM,SAA2C,CAAC;CAClD,IAAI,IAAI,UAAU,OAChB,OAAO,KAAK,GAAG,GAAG;MACb;EACL,MAAM,SAAS,IAAI,SAAS;EAC5B,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,GAAG;GACjC,MAAM,OAAO,IAAI,KAAK,IAAI,IAAI,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM,CAAC;GAChE,IAAI,MAAM,OAAO,KAAK,IAAI;EAC5B;CACF;CAEA,MAAM,SAAS,OAAO,KAAK,UAAU;EACnC,IAAI,KAAK;EACT,KAAK,IAAI,gBAAgB,KAAK,GAAG;CACnC,EAAE;CACF,MAAM,IAAI,SAAS,MAAM;CACzB,OAAO;AACT;;AAGA,SAAgB,kBAAkB,SAAuB;CACvD,MAAM,SAAS,MAAM,IAAI,OAAO;CAChC,IAAI,CAAC,QAAQ;CACb,KAAK,MAAM,KAAK,QAAQ,IAAI,gBAAgB,EAAE,GAAG;CACjD,MAAM,OAAO,OAAO;AACtB"}
|