@aicut/core 0.4.3 → 0.6.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @aicut/core
2
2
 
3
- > Framework-agnostic engine for the AiCut video editor — canvas timeline, plain-JSON projects, zero runtime deps.
3
+ > Framework-agnostic engine for the AiCut video editor — canvas timeline, plain-JSON projects, pluggable playback. Main entry has zero runtime deps; opt-in sub-entries bundle their own (three.js for `/lighting`, mp4box.js for `/webcodecs`).
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@aicut/core.svg)](https://www.npmjs.com/package/@aicut/core)
6
6
  [![License](https://img.shields.io/npm/l/@aicut/core.svg)](./LICENSE)
@@ -155,6 +155,183 @@ editor.toolbarRight.appendChild(customIconBtn);
155
155
 
156
156
  The standalone `Timeline` also exposes `toolbarLeft` / `toolbarRight` when constructed with `toolbar: true`.
157
157
 
158
+ ## Playback engine
159
+
160
+ The Editor talks to playback through a single interface (`PlaybackEngine`).
161
+ The default engine is `HtmlVideoEngine` — one hidden `<video>` per source,
162
+ swapped at clip boundaries. Zero deps, works in every browser, but seek
163
+ snaps to the nearest keyframe (the browser owns the decode pipeline).
164
+
165
+ For frame-accurate scrubbing, multi-track compositing, transitions, or a
166
+ custom render pipeline (WebGL compositor, IPC bridge to a native player,
167
+ WebRTC stream consumer), pass your own factory:
168
+
169
+ ```ts
170
+ import {
171
+ Editor,
172
+ type PlaybackEngine,
173
+ type PlaybackEngineFactory,
174
+ } from "@aicut/core";
175
+
176
+ const myFactory: PlaybackEngineFactory = ({ host, project }) => {
177
+ // host: a div the editor owns. Mount whatever surface you need.
178
+ // project: the initial Project — pre-warm decoders, etc.
179
+ return new MyEngine(host, project); // implements PlaybackEngine
180
+ };
181
+
182
+ Editor.create({ container, project, playbackEngine: myFactory });
183
+ ```
184
+
185
+ The contract — every engine implements this exactly:
186
+
187
+ ```ts
188
+ interface PlaybackEngine {
189
+ setProject(next: Project): void;
190
+ play(): void;
191
+ pause(): void;
192
+ isPlaying(): boolean;
193
+ getTime(): Ms;
194
+ seek(timeMs: Ms): void;
195
+ destroy(): void;
196
+
197
+ // Optional event hooks — Editor assigns these after construction.
198
+ onTimeUpdate?: (ms: Ms) => void;
199
+ onEnded?: () => void;
200
+ onError?: (err: Error) => void;
201
+ onReady?: () => void;
202
+ onSourceMetadata?: (sourceId: string, durationMs: Ms) => void;
203
+ }
204
+ ```
205
+
206
+ Engines that can't emit a particular event (e.g. no audio metadata)
207
+ simply never call that hook. The Editor re-emits engine events as its
208
+ own `time` / `pause` / `error` / `ready` / `change` events, so your host
209
+ code is unaffected by which engine is in use.
210
+
211
+ ### Bundled engines
212
+
213
+ | Engine | Where | Decoder | Renderer | Cost |
214
+ | --- | --- | --- | --- | --- |
215
+ | `HtmlVideoEngine` | main | browser | raw `<video>` | 0 deps |
216
+ | `CanvasCompositorEngine` | main | browser | `ctx.drawImage` | 0 deps |
217
+ | `WebCodecsEngine` | `@aicut/core/webcodecs` | `VideoDecoder` (frame-accurate) | `ctx.drawImage(VideoFrame)` | bundles mp4box.js (~200 KB) |
218
+
219
+ The WebCodecs path is on its own sub-entry so consumers who don't ask for it pay nothing for the demuxer. Feature-detect before constructing:
220
+
221
+ ```ts
222
+ import {
223
+ WebCodecsEngine,
224
+ isWebCodecsSupported,
225
+ } from "@aicut/core/webcodecs";
226
+
227
+ const factory: PlaybackEngineFactory = isWebCodecsSupported()
228
+ ? (opts) => new WebCodecsEngine({ ...opts, debug: true })
229
+ : htmlVideoEngineFactory;
230
+
231
+ Editor.create({ container, project, playbackEngine: factory });
232
+ ```
233
+
234
+ `WebCodecsEngine` v1 covers single-track MP4/MOV playback (H.264 / HEVC / VP9 / AV1 — whatever the browser's `VideoDecoder` supports). Multi-track compositing, audio, transitions land in follow-up releases on the same surface.
235
+
236
+ ## Timeline density
237
+
238
+ Defaults are tuned for desktop. For compact viewports (laptop side panels, embedded editors), shrink the bottom area and / or row height:
239
+
240
+ ```ts
241
+ Editor.create({
242
+ container,
243
+ project,
244
+ timelineHeight: 160, // outer height of the bottom timeline area
245
+ // (default 240). Scrolls internally when
246
+ // tracks overflow.
247
+ trackHeight: 40, // each track row (default 56). Affects clip
248
+ // body + thumbnail strip.
249
+ rulerHeight: 22, // time-label strip (default 24).
250
+ });
251
+ ```
252
+
253
+ | Option | Default | Useful range | Notes |
254
+ | --- | --- | --- | --- |
255
+ | `timelineHeight` | 240 | 120 – 480 | Outer height of `.aicut-timeline`. Reactive in the React + Vue wrappers — swap any time. Internal scroll appears when tracks don't fit. |
256
+ | `trackHeight` | 56 | 28 – 96 | Per-row pixel height. Applied process-wide via `setTimelineMetrics` (see below). Re-apply by remounting the editor. |
257
+ | `rulerHeight` | 24 | 18 – 36 | Time-label strip height. Same lifecycle as `trackHeight`. |
258
+
259
+ For runtime control without an editor option, call the underlying setter directly:
260
+
261
+ ```ts
262
+ import { setTimelineMetrics } from "@aicut/core";
263
+
264
+ setTimelineMetrics({ trackHeight: 36, rulerHeight: 20 });
265
+ ```
266
+
267
+ `TRACK_HEIGHT` and `RULER_HEIGHT` are ESM live bindings — re-reading them after the setter returns the updated values.
268
+
269
+ ## Keyframes (per-clip panX / panY / scale animation)
270
+
271
+ Off by default. Flip on, and **all three** playback engines (HTML5 / Canvas / WebCodecs) interpolate per-clip transforms between adjacent keyframes — the bread-and-butter motion-template feature (think CapCut "动画").
272
+
273
+ ```ts
274
+ const editor = Editor.create({
275
+ container,
276
+ project,
277
+ keyframes: { enabled: true },
278
+ });
279
+
280
+ // Per-property — pin only the props you care about, others ride the
281
+ // static base (Clip.panX / Clip.panY / Clip.scale).
282
+ editor.addKeyframe("clip-1", "scale", { time: 0, value: 1 });
283
+ editor.addKeyframe("clip-1", "scale", { time: 2000, value: 2.5, easing: "easeInOut" });
284
+ editor.addKeyframe("clip-1", "scale", { time: 4000, value: 1 });
285
+ editor.setKeyframeValue("clip-1", kfId, 1.8); // tweak one kf's value
286
+ editor.setKeyframeEasing("clip-1", kfId, "easeOut");
287
+ editor.moveKeyframe("clip-1", kfId, 1500); // shift to t=1.5s
288
+ editor.removeKeyframe("clip-1", kfId);
289
+
290
+ // Toolbar-style "K at playhead" — one click drops kfs for all 3 props.
291
+ editor.setSelection("clip-1");
292
+ editor.toggleKeyframeAtPlayhead();
293
+ ```
294
+
295
+ The data lives on `Clip.keyframes: Keyframe[]`, one entry per animated property at one moment in clip-local time:
296
+
297
+ ```ts
298
+ type KeyframeProp = "panX" | "panY" | "scale";
299
+ type EasingKind = "linear" | "easeIn" | "easeOut" | "easeInOut";
300
+
301
+ interface Keyframe {
302
+ id: string;
303
+ prop: KeyframeProp;
304
+ time: Ms; // clip-local; 0 = clip's `in`
305
+ value: number; // CSS px for pan, multiplier for scale
306
+ easing?: EasingKind; // optional outgoing curve; omitted = linear
307
+ }
308
+
309
+ interface Clip {
310
+ // ...
311
+ panX?: number; panY?: number; scale?: number; // static base
312
+ keyframes?: Keyframe[];
313
+ }
314
+ ```
315
+
316
+ | Behavior | |
317
+ | --- | --- |
318
+ | **Per-property model** | `panX` / `panY` / `scale` animate independently. Pre-easing tuple-keyframes (`{time, x, y, scale}`) auto-migrate via `normalizeProject`. |
319
+ | **Easing** | 4 cubic curves stored on the leaving kf (AE / Premiere / CapCut convention). `editor.setKeyframesEasingAtTime(clipId, time, "easeInOut")` updates all 3 props at a moment atomically. |
320
+ | **Drag-burst undo** | `editor.beginInteraction() / endInteraction()` coalesce a 30+ tick pointermove drag into ONE history entry. Hosts wrap continuous gestures with these for clean ⌘Z. |
321
+ | **Snap** | Each keyframe contributes a timeline-absolute target — dragging snaps to other keyframes / clip edges / playhead. |
322
+ | **Lossless split** | `splitClipAt` mid-segment inserts interpolated boundary keyframes per property so cutting and not moving the halves plays back identically to the un-cut clip. |
323
+ | **All engines animate** | HTML5 (CSS transform on wrapper div), Canvas + WebCodecs (`ctx.clip()` + `ctx.translate/scale`), and the backend exporter all share the same interpolation. |
324
+ | **Backend export** | `@aicut/backend-ts` and `@aicut/backend-go` both compile keyframes to ffmpeg `t`-expressions in `scale=…:eval=frame` + `overlay=…:eval=frame` filters. Pass `output: { width, height, fps }` in the request — required for the kf path. |
325
+
326
+ Read the live transform anywhere (e.g. host-rendered thumbnails) via the pure helper:
327
+
328
+ ```ts
329
+ import { getEffectiveTransform, getTransformAtTimelineTime } from "@aicut/core";
330
+
331
+ const t = getEffectiveTransform(clip, localMs);
332
+ // → { panX: 100, panY: 0, scale: 1.5 }
333
+ ```
334
+
158
335
  ## Lighting picker (opt-in sub-entry)
159
336
 
160
337
  A separate component for AI-relighting workflows — drag a light dot around a 3D sphere wrapping a subject frame, control brightness / color / direction. Three.js is bundled only on this sub-entry, so consumers of the video editor pay zero bytes for it.
@@ -0,0 +1,123 @@
1
+ // src/i18n.ts
2
+ var localeEn = {
3
+ undo: "Undo (\u2318Z)",
4
+ redo: "Redo (\u21E7\u2318Z)",
5
+ split: "Split (K)",
6
+ trimLeft: "Trim left edge (Q)",
7
+ trimRight: "Trim right edge (W)",
8
+ playPause: "Play / Pause (Space)",
9
+ fullscreen: "Fullscreen preview",
10
+ snap: "Snap",
11
+ snapOnTitle: "Turn off snap",
12
+ snapOffTitle: "Turn on snap",
13
+ zoomOut: "Zoom out",
14
+ zoomIn: "Zoom in",
15
+ reset: "Reset edits (keep sources)",
16
+ keyframeAdd: "Add keyframe at playhead",
17
+ keyframeRemove: "Remove keyframe at playhead",
18
+ seekClipStart: "Jump to clip start (I)",
19
+ seekClipEnd: "Jump to clip end (O)",
20
+ keyframePanelTitle: "Keyframe",
21
+ keyframePanelLabelX: "X",
22
+ keyframePanelLabelY: "Y",
23
+ keyframePanelLabelScale: "Scale",
24
+ keyframePanelLabelEasing: "Easing",
25
+ keyframePanelReset: "Reset to 0 0 1",
26
+ keyframePanelResetTitle: "Pin this keyframe to identity (panX=0, panY=0, scale=1)",
27
+ keyframePanelBadgePinned: "Pinned at this moment",
28
+ keyframePanelBadgeAnimated: "Animated \u2014 but not pinned at this exact moment",
29
+ keyframePanelBadgeStatic: "Static value",
30
+ keyframePanelTimeSuffix: "s",
31
+ keyframeEasingLinear: "Linear",
32
+ keyframeEasingEaseIn: "Ease in",
33
+ keyframeEasingEaseOut: "Ease out",
34
+ keyframeEasingEaseInOut: "Ease in-out",
35
+ exitFullscreen: "Exit fullscreen",
36
+ exitFullscreenTitle: "Exit fullscreen (Esc)",
37
+ newTrack: "+ New track",
38
+ videoTrackLabel: "Video {n}",
39
+ audioTrackLabel: "Audio {n}"
40
+ };
41
+ var localeZh = {
42
+ undo: "\u64A4\u9500 (\u2318Z)",
43
+ redo: "\u91CD\u505A (\u21E7\u2318Z)",
44
+ split: "\u5206\u5272 (K)",
45
+ trimLeft: "\u5411\u5DE6\u88C1\u526A (Q)",
46
+ trimRight: "\u5411\u53F3\u88C1\u526A (W)",
47
+ playPause: "\u64AD\u653E / \u6682\u505C (Space)",
48
+ fullscreen: "\u5168\u5C4F\u9884\u89C8",
49
+ snap: "\u5438\u9644",
50
+ snapOnTitle: "\u5173\u95ED\u5438\u9644",
51
+ snapOffTitle: "\u5F00\u542F\u5438\u9644",
52
+ zoomOut: "\u7F29\u5C0F",
53
+ zoomIn: "\u653E\u5927",
54
+ reset: "\u91CD\u7F6E\u7F16\u8F91\uFF08\u4FDD\u7559\u89C6\u9891\u6E90\uFF09",
55
+ keyframeAdd: "\u6DFB\u52A0\u5173\u952E\u5E27",
56
+ keyframeRemove: "\u5220\u9664\u5F53\u524D\u5173\u952E\u5E27",
57
+ seekClipStart: "\u8DF3\u5230\u7247\u6BB5\u8D77\u70B9 (I)",
58
+ seekClipEnd: "\u8DF3\u5230\u7247\u6BB5\u672B\u5C3E (O)",
59
+ keyframePanelTitle: "\u5173\u952E\u5E27",
60
+ keyframePanelLabelX: "X \u4F4D\u79FB",
61
+ keyframePanelLabelY: "Y \u4F4D\u79FB",
62
+ keyframePanelLabelScale: "\u7F29\u653E",
63
+ keyframePanelLabelEasing: "\u7F13\u52A8",
64
+ keyframePanelReset: "\u91CD\u7F6E\u4E3A 0 0 1",
65
+ keyframePanelResetTitle: "\u5C06\u8BE5\u5173\u952E\u5E27\u91CD\u7F6E\u4E3A\u521D\u59CB\u59FF\u6001\uFF08panX=0, panY=0, scale=1\uFF09",
66
+ keyframePanelBadgePinned: "\u5DF2\u5728\u8BE5\u65F6\u523B\u56FA\u5B9A",
67
+ keyframePanelBadgeAnimated: "\u6574\u6BB5\u6709\u52A8\u753B\uFF0C\u4F46\u5F53\u524D\u65F6\u523B\u6CA1\u6709\u9501\u70B9",
68
+ keyframePanelBadgeStatic: "\u672A\u52A8\u753B\uFF08\u6CBF\u7528\u9759\u6001\u503C\uFF09",
69
+ keyframePanelTimeSuffix: "\u79D2",
70
+ keyframeEasingLinear: "\u7EBF\u6027",
71
+ keyframeEasingEaseIn: "\u7F13\u5165",
72
+ keyframeEasingEaseOut: "\u7F13\u51FA",
73
+ keyframeEasingEaseInOut: "\u7F13\u5165\u7F13\u51FA",
74
+ exitFullscreen: "\u9000\u51FA\u5168\u5C4F",
75
+ exitFullscreenTitle: "\u9000\u51FA\u5168\u5C4F (Esc)",
76
+ newTrack: "+ \u65B0\u8F68\u9053",
77
+ videoTrackLabel: "\u89C6\u9891 {n}",
78
+ audioTrackLabel: "\u97F3\u9891 {n}"
79
+ };
80
+ function mergeLocale(partial) {
81
+ return partial ? { ...localeEn, ...partial } : localeEn;
82
+ }
83
+ function formatLabel(template, vars) {
84
+ return template.replace(
85
+ /\{(\w+)\}/g,
86
+ (_, k) => k in vars ? String(vars[k]) : `{${k}}`
87
+ );
88
+ }
89
+
90
+ // src/theme.ts
91
+ var THEME_VARS = {
92
+ brand: "--color-brand",
93
+ secondary: "--color-secondary",
94
+ surface: "--color-surface",
95
+ dark: "--color-dark",
96
+ muted: "--color-muted",
97
+ card: "--color-card",
98
+ success: "--color-success",
99
+ warning: "--color-warning",
100
+ info: "--color-info",
101
+ error: "--color-error",
102
+ controlsBg: "--aicut-controls-bg",
103
+ controlsBorder: "--aicut-controls-border",
104
+ controlsText: "--aicut-controls-text",
105
+ controlsHover: "--aicut-controls-hover",
106
+ controlsActive: "--aicut-controls-active",
107
+ previewBg: "--aicut-preview-bg",
108
+ radiusSm: "--aicut-radius-sm",
109
+ radiusMd: "--aicut-radius-md",
110
+ radiusLg: "--aicut-radius-lg"
111
+ };
112
+ function applyTheme(root, theme) {
113
+ if (!theme) return;
114
+ for (const key of Object.keys(theme)) {
115
+ const cssVar = THEME_VARS[key];
116
+ const value = theme[key];
117
+ if (cssVar && value) root.style.setProperty(cssVar, value);
118
+ }
119
+ }
120
+
121
+ export { applyTheme, formatLabel, localeEn, localeZh, mergeLocale };
122
+ //# sourceMappingURL=chunk-H6AY6NW4.js.map
123
+ //# sourceMappingURL=chunk-H6AY6NW4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/i18n.ts","../src/theme.ts"],"names":[],"mappings":"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","file":"chunk-H6AY6NW4.js","sourcesContent":["/**\n * UI strings the editor paints into the DOM (toolbar tooltips, the\n * fullscreen exit button) and onto the timeline canvas (phantom new-\n * track label, track header labels). Every user-visible literal in\n * `@aicut/core` flows through this interface — there are no hidden\n * hard-coded translations elsewhere in the library.\n *\n * Defaults to English. Hosts that want Chinese (or any other locale)\n * pass `locale: localeZh` to `Editor.create` / `Timeline.create`, or\n * override individual keys with `locale: { undo: \"撤销\" }`.\n */\nexport interface Locale {\n // Toolbar tooltips\n undo: string;\n redo: string;\n split: string;\n trimLeft: string;\n trimRight: string;\n playPause: string;\n fullscreen: string;\n snap: string;\n /** Title shown on the snap button when snap is ON (clicking turns OFF). */\n snapOnTitle: string;\n /** Title shown when snap is OFF (clicking turns ON). */\n snapOffTitle: string;\n zoomOut: string;\n zoomIn: string;\n reset: string;\n /** Toolbar tooltip when no keyframe exists at the playhead. */\n keyframeAdd: string;\n /** Toolbar tooltip when one exists — clicking removes it. */\n keyframeRemove: string;\n /** Toolbar tooltip — jump the playhead to the selected clip's start. */\n seekClipStart: string;\n /** Toolbar tooltip — jump the playhead to the selected clip's end. */\n seekClipEnd: string;\n\n // Keyframe panel chrome\n /** Header text on the keyframe parameter panel. */\n keyframePanelTitle: string;\n /** Row label for the X-translation numeric input. */\n keyframePanelLabelX: string;\n /** Row label for the Y-translation numeric input. */\n keyframePanelLabelY: string;\n /** Row label for the scale numeric input. */\n keyframePanelLabelScale: string;\n /** Row label for the easing dropdown. */\n keyframePanelLabelEasing: string;\n /** Reset button label — pins this kf to identity (0, 0, 1). */\n keyframePanelReset: string;\n /** Reset button tooltip. */\n keyframePanelResetTitle: string;\n /** Badge tooltip — kf for THIS prop is pinned at this moment. */\n keyframePanelBadgePinned: string;\n /** Badge tooltip — prop has kfs elsewhere but not at this moment. */\n keyframePanelBadgeAnimated: string;\n /** Badge tooltip — prop has no kfs (riding the static base). */\n keyframePanelBadgeStatic: string;\n /** Time display suffix — appended after the seconds value. */\n keyframePanelTimeSuffix: string;\n // Easing dropdown options (curve names)\n keyframeEasingLinear: string;\n keyframeEasingEaseIn: string;\n keyframeEasingEaseOut: string;\n keyframeEasingEaseInOut: string;\n\n // Fullscreen exit overlay\n exitFullscreen: string;\n exitFullscreenTitle: string;\n\n // Timeline canvas labels\n /** Phantom row that appears under the last track during a drag. */\n newTrack: string;\n /** Track header — `{n}` is replaced with the 1-based track index. */\n videoTrackLabel: string;\n /** Same template format as videoTrackLabel. */\n audioTrackLabel: string;\n}\n\n/** English. The library default — chosen over Chinese as the OSS norm. */\nexport const localeEn: Locale = {\n undo: \"Undo (⌘Z)\",\n redo: \"Redo (⇧⌘Z)\",\n split: \"Split (K)\",\n trimLeft: \"Trim left edge (Q)\",\n trimRight: \"Trim right edge (W)\",\n playPause: \"Play / Pause (Space)\",\n fullscreen: \"Fullscreen preview\",\n snap: \"Snap\",\n snapOnTitle: \"Turn off snap\",\n snapOffTitle: \"Turn on snap\",\n zoomOut: \"Zoom out\",\n zoomIn: \"Zoom in\",\n reset: \"Reset edits (keep sources)\",\n keyframeAdd: \"Add keyframe at playhead\",\n keyframeRemove: \"Remove keyframe at playhead\",\n seekClipStart: \"Jump to clip start (I)\",\n seekClipEnd: \"Jump to clip end (O)\",\n keyframePanelTitle: \"Keyframe\",\n keyframePanelLabelX: \"X\",\n keyframePanelLabelY: \"Y\",\n keyframePanelLabelScale: \"Scale\",\n keyframePanelLabelEasing: \"Easing\",\n keyframePanelReset: \"Reset to 0 0 1\",\n keyframePanelResetTitle:\n \"Pin this keyframe to identity (panX=0, panY=0, scale=1)\",\n keyframePanelBadgePinned: \"Pinned at this moment\",\n keyframePanelBadgeAnimated: \"Animated — but not pinned at this exact moment\",\n keyframePanelBadgeStatic: \"Static value\",\n keyframePanelTimeSuffix: \"s\",\n keyframeEasingLinear: \"Linear\",\n keyframeEasingEaseIn: \"Ease in\",\n keyframeEasingEaseOut: \"Ease out\",\n keyframeEasingEaseInOut: \"Ease in-out\",\n exitFullscreen: \"Exit fullscreen\",\n exitFullscreenTitle: \"Exit fullscreen (Esc)\",\n newTrack: \"+ New track\",\n videoTrackLabel: \"Video {n}\",\n audioTrackLabel: \"Audio {n}\",\n};\n\n/** Simplified Chinese. */\nexport const localeZh: Locale = {\n undo: \"撤销 (⌘Z)\",\n redo: \"重做 (⇧⌘Z)\",\n split: \"分割 (K)\",\n trimLeft: \"向左裁剪 (Q)\",\n trimRight: \"向右裁剪 (W)\",\n playPause: \"播放 / 暂停 (Space)\",\n fullscreen: \"全屏预览\",\n snap: \"吸附\",\n snapOnTitle: \"关闭吸附\",\n snapOffTitle: \"开启吸附\",\n zoomOut: \"缩小\",\n zoomIn: \"放大\",\n reset: \"重置编辑(保留视频源)\",\n keyframeAdd: \"添加关键帧\",\n keyframeRemove: \"删除当前关键帧\",\n seekClipStart: \"跳到片段起点 (I)\",\n seekClipEnd: \"跳到片段末尾 (O)\",\n keyframePanelTitle: \"关键帧\",\n keyframePanelLabelX: \"X 位移\",\n keyframePanelLabelY: \"Y 位移\",\n keyframePanelLabelScale: \"缩放\",\n keyframePanelLabelEasing: \"缓动\",\n keyframePanelReset: \"重置为 0 0 1\",\n keyframePanelResetTitle:\n \"将该关键帧重置为初始姿态(panX=0, panY=0, scale=1)\",\n keyframePanelBadgePinned: \"已在该时刻固定\",\n keyframePanelBadgeAnimated: \"整段有动画,但当前时刻没有锁点\",\n keyframePanelBadgeStatic: \"未动画(沿用静态值)\",\n keyframePanelTimeSuffix: \"秒\",\n keyframeEasingLinear: \"线性\",\n keyframeEasingEaseIn: \"缓入\",\n keyframeEasingEaseOut: \"缓出\",\n keyframeEasingEaseInOut: \"缓入缓出\",\n exitFullscreen: \"退出全屏\",\n exitFullscreenTitle: \"退出全屏 (Esc)\",\n newTrack: \"+ 新轨道\",\n videoTrackLabel: \"视频 {n}\",\n audioTrackLabel: \"音频 {n}\",\n};\n\n/** Spread defaults under host overrides — host can supply a partial. */\nexport function mergeLocale(partial: Partial<Locale> | undefined): Locale {\n return partial ? { ...localeEn, ...partial } : localeEn;\n}\n\n/**\n * Replace `{key}` placeholders in a template. We only need `{n}`\n * substitution today; the implementation is generic so additional\n * keys (e.g. `{name}`) won't need a second pass.\n */\nexport function formatLabel(\n template: string,\n vars: Record<string, string | number>,\n): string {\n return template.replace(/\\{(\\w+)\\}/g, (_, k) =>\n k in vars ? String(vars[k]) : `{${k}}`,\n );\n}\n","import type { Theme } from \"./types.js\";\n\n/**\n * Map `Theme` keys to the CSS custom property they write.\n *\n * Brand/palette keys share names with iqvise's globals.css so hosts\n * that already define `--color-brand` etc. at the page level get the\n * editor in their palette for free — `theme` props ONLY needed when\n * scoping to this editor instance.\n *\n * Chrome keys keep the `--aicut-controls-*` prefix because they have\n * no analogue in the host palette.\n */\nconst THEME_VARS: Record<keyof Theme, string> = {\n brand: \"--color-brand\",\n secondary: \"--color-secondary\",\n surface: \"--color-surface\",\n dark: \"--color-dark\",\n muted: \"--color-muted\",\n card: \"--color-card\",\n success: \"--color-success\",\n warning: \"--color-warning\",\n info: \"--color-info\",\n error: \"--color-error\",\n controlsBg: \"--aicut-controls-bg\",\n controlsBorder: \"--aicut-controls-border\",\n controlsText: \"--aicut-controls-text\",\n controlsHover: \"--aicut-controls-hover\",\n controlsActive: \"--aicut-controls-active\",\n previewBg: \"--aicut-preview-bg\",\n radiusSm: \"--aicut-radius-sm\",\n radiusMd: \"--aicut-radius-md\",\n radiusLg: \"--aicut-radius-lg\",\n};\n\nexport function applyTheme(root: HTMLElement, theme: Theme | undefined): void {\n if (!theme) return;\n for (const key of Object.keys(theme) as Array<keyof Theme>) {\n const cssVar = THEME_VARS[key];\n const value = theme[key];\n if (cssVar && value) root.style.setProperty(cssVar, value);\n }\n}\n"]}
@@ -0,0 +1,93 @@
1
+ // src/keyframes/types.ts
2
+ var IDENTITY_TRANSFORM = {
3
+ panX: 0,
4
+ panY: 0,
5
+ scale: 1
6
+ };
7
+ function isIdentityTransform(t) {
8
+ return Math.abs(t.panX) < 1e-3 && Math.abs(t.panY) < 1e-3 && Math.abs(t.scale - 1) < 1e-4;
9
+ }
10
+
11
+ // src/keyframes/interpolate.ts
12
+ function applyEasing(t, easing) {
13
+ switch (easing) {
14
+ case "linear":
15
+ return t;
16
+ case "easeIn":
17
+ return t * t * t;
18
+ case "easeOut": {
19
+ const u = 1 - t;
20
+ return 1 - u * u * u;
21
+ }
22
+ case "easeInOut":
23
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
24
+ }
25
+ }
26
+ function defaultFor(prop) {
27
+ return prop === "scale" ? 1 : 0;
28
+ }
29
+ function staticValue(clip, prop) {
30
+ const v = clip[prop];
31
+ return v ?? defaultFor(prop);
32
+ }
33
+ function keyframesForProp(kfs, prop) {
34
+ const out = [];
35
+ for (const k of kfs) if (k.prop === prop) out.push(k);
36
+ out.sort((a, b) => a.time - b.time);
37
+ return out;
38
+ }
39
+ function hasKeyframesForProp(clip, prop) {
40
+ return clip.keyframes?.some((k) => k.prop === prop) ?? false;
41
+ }
42
+ function interpolateProp(clip, prop, localMs) {
43
+ if (!clip.keyframes || clip.keyframes.length === 0) {
44
+ return staticValue(clip, prop);
45
+ }
46
+ const arr = keyframesForProp(clip.keyframes, prop);
47
+ if (arr.length === 0) return staticValue(clip, prop);
48
+ if (arr.length === 1) return arr[0].value;
49
+ const first = arr[0];
50
+ const last = arr[arr.length - 1];
51
+ if (localMs <= first.time) return first.value;
52
+ if (localMs >= last.time) return last.value;
53
+ for (let i = 0; i < arr.length - 1; i += 1) {
54
+ const a = arr[i];
55
+ const b = arr[i + 1];
56
+ if (localMs >= a.time && localMs <= b.time) {
57
+ if (b.time === a.time) return a.value;
58
+ const rawT = (localMs - a.time) / (b.time - a.time);
59
+ const eased = applyEasing(rawT, a.easing ?? "linear");
60
+ return a.value + (b.value - a.value) * eased;
61
+ }
62
+ }
63
+ return last.value;
64
+ }
65
+ function getEffectiveTransform(clip, localMs) {
66
+ if ((!clip.keyframes || clip.keyframes.length === 0) && clip.panX === void 0 && clip.panY === void 0 && clip.scale === void 0) {
67
+ return IDENTITY_TRANSFORM;
68
+ }
69
+ return {
70
+ panX: interpolateProp(clip, "panX", localMs),
71
+ panY: interpolateProp(clip, "panY", localMs),
72
+ scale: interpolateProp(clip, "scale", localMs)
73
+ };
74
+ }
75
+ function getTransformAtTimelineTime(clip, timelineMs) {
76
+ return getEffectiveTransform(clip, timelineMs - clip.start);
77
+ }
78
+ function upsertKeyframe(kfs, prop, time, value, idFactory) {
79
+ const existing = kfs ?? [];
80
+ const idx = existing.findIndex(
81
+ (k) => k.prop === prop && Math.abs(k.time - time) < 16
82
+ );
83
+ if (idx >= 0) {
84
+ const next = existing.slice();
85
+ next[idx] = { ...next[idx], value };
86
+ return next;
87
+ }
88
+ return [...existing, { id: idFactory(), prop, time, value }];
89
+ }
90
+
91
+ export { IDENTITY_TRANSFORM, getEffectiveTransform, getTransformAtTimelineTime, hasKeyframesForProp, interpolateProp, isIdentityTransform, upsertKeyframe };
92
+ //# sourceMappingURL=chunk-WTCK3XQ6.js.map
93
+ //# sourceMappingURL=chunk-WTCK3XQ6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/keyframes/types.ts","../src/keyframes/interpolate.ts"],"names":[],"mappings":"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","file":"chunk-WTCK3XQ6.js","sourcesContent":["/**\n * The transform a clip's content is rendered with at a given moment.\n * Engines apply this INSIDE a fixed output frame: `panX` / `panY`\n * translate the content (in CSS px), `scale` resizes it around the\n * output frame's center. Anything pushed outside the frame is\n * clipped. Identity = `{ panX: 0, panY: 0, scale: 1 }`.\n */\nexport interface EffectiveTransform {\n panX: number;\n panY: number;\n scale: number;\n}\n\n/** Identity transform — no pan, no scaling (content fills the output frame). */\nexport const IDENTITY_TRANSFORM: EffectiveTransform = {\n panX: 0,\n panY: 0,\n scale: 1,\n};\n\n/** True when a transform is effectively identity (within FP slop). */\nexport function isIdentityTransform(t: EffectiveTransform): boolean {\n return (\n Math.abs(t.panX) < 0.001 &&\n Math.abs(t.panY) < 0.001 &&\n Math.abs(t.scale - 1) < 0.0001\n );\n}\n","import type { Clip, EasingKind, Keyframe, KeyframeProp, Ms } from \"../types.js\";\nimport {\n IDENTITY_TRANSFORM,\n type EffectiveTransform,\n} from \"./types.js\";\n\n/**\n * Map normalized time t∈[0,1] to eased t' via cubic easing. Cubic was\n * picked over quadratic because the start / end \"stickiness\" is more\n * pronounced — when the user picks `easeIn` they typically want the\n * motion to clearly hold at the start, not just barely slow down.\n *\n * `easing` is the leaving keyframe's outgoing curve — matches AE /\n * Premiere / CapCut convention so the user only sets it once per kf,\n * not once per segment.\n */\nexport function applyEasing(t: number, easing: EasingKind): number {\n switch (easing) {\n case \"linear\":\n return t;\n case \"easeIn\":\n return t * t * t;\n case \"easeOut\": {\n const u = 1 - t;\n return 1 - u * u * u;\n }\n case \"easeInOut\":\n return t < 0.5\n ? 4 * t * t * t\n : 1 - Math.pow(-2 * t + 2, 3) / 2;\n }\n}\n\n/** Default value when a property has neither static base nor keyframes. */\nfunction defaultFor(prop: KeyframeProp): number {\n return prop === \"scale\" ? 1 : 0;\n}\n\n/** Static fallback for `prop` on a clip. */\nfunction staticValue(clip: Clip, prop: KeyframeProp): number {\n const v = clip[prop];\n return v ?? defaultFor(prop);\n}\n\n/**\n * Sub-keyframes for a single property in time order. Cheap on small\n * arrays (a few keyframes per clip in practice) — no caching needed.\n */\nexport function keyframesForProp(\n kfs: Keyframe[],\n prop: KeyframeProp,\n): Keyframe[] {\n const out: Keyframe[] = [];\n for (const k of kfs) if (k.prop === prop) out.push(k);\n out.sort((a, b) => a.time - b.time);\n return out;\n}\n\n/** True when the clip has any keyframes pinning this property. */\nexport function hasKeyframesForProp(clip: Clip, prop: KeyframeProp): boolean {\n return clip.keyframes?.some((k) => k.prop === prop) ?? false;\n}\n\n/**\n * Interpolate one property at the given clip-local time.\n *\n * Rules per property:\n * - No keyframes for this prop → static base (or 0 / 1).\n * - Before first keyframe → first keyframe's value (held).\n * - After last keyframe → last keyframe's value (held).\n * - Between two → linear interpolation.\n */\nexport function interpolateProp(\n clip: Clip,\n prop: KeyframeProp,\n localMs: Ms,\n): number {\n if (!clip.keyframes || clip.keyframes.length === 0) {\n return staticValue(clip, prop);\n }\n const arr = keyframesForProp(clip.keyframes, prop);\n if (arr.length === 0) return staticValue(clip, prop);\n if (arr.length === 1) return arr[0]!.value;\n const first = arr[0]!;\n const last = arr[arr.length - 1]!;\n if (localMs <= first.time) return first.value;\n if (localMs >= last.time) return last.value;\n for (let i = 0; i < arr.length - 1; i += 1) {\n const a = arr[i]!;\n const b = arr[i + 1]!;\n if (localMs >= a.time && localMs <= b.time) {\n if (b.time === a.time) return a.value;\n const rawT = (localMs - a.time) / (b.time - a.time);\n // Easing belongs to the LEAVING keyframe (a) — the curve from\n // a→b is shaped by a.easing. Undefined / missing field\n // gracefully degrades to \"linear\" so old projects look the same.\n const eased = applyEasing(rawT, a.easing ?? \"linear\");\n return a.value + (b.value - a.value) * eased;\n }\n }\n return last.value;\n}\n\n/**\n * Effective transform = all three properties evaluated together. The\n * engine applies this to the content inside the fixed output frame:\n * scale around frame center, then translate by (panX, panY).\n */\nexport function getEffectiveTransform(\n clip: Clip,\n localMs: Ms,\n): EffectiveTransform {\n if (\n (!clip.keyframes || clip.keyframes.length === 0) &&\n clip.panX === undefined &&\n clip.panY === undefined &&\n clip.scale === undefined\n ) {\n return IDENTITY_TRANSFORM;\n }\n return {\n panX: interpolateProp(clip, \"panX\", localMs),\n panY: interpolateProp(clip, \"panY\", localMs),\n scale: interpolateProp(clip, \"scale\", localMs),\n };\n}\n\n/** Same as `getEffectiveTransform` but takes timeline-absolute time. */\nexport function getTransformAtTimelineTime(\n clip: Clip,\n timelineMs: Ms,\n): EffectiveTransform {\n return getEffectiveTransform(clip, timelineMs - clip.start);\n}\n\n/**\n * Insert a keyframe for `prop` at `time`, or update value if one\n * already exists at the same (rounded) time. Returns the next array.\n *\n * The 16ms tolerance handles \"click button while seeking\" — two\n * keyframes 1 frame apart get coalesced. Reference behavior.\n */\nexport function upsertKeyframe(\n kfs: Keyframe[] | undefined,\n prop: KeyframeProp,\n time: Ms,\n value: number,\n idFactory: () => string,\n): Keyframe[] {\n const existing = kfs ?? [];\n const idx = existing.findIndex(\n (k) => k.prop === prop && Math.abs(k.time - time) < 16,\n );\n if (idx >= 0) {\n const next = existing.slice();\n next[idx] = { ...next[idx]!, value };\n return next;\n }\n return [...existing, { id: idFactory(), prop, time, value }];\n}\n\n/** Drop every keyframe for one prop on a clip. */\nexport function removeKeyframesForProp(\n kfs: Keyframe[] | undefined,\n prop: KeyframeProp,\n): Keyframe[] {\n return (kfs ?? []).filter((k) => k.prop !== prop);\n}\n"]}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * UI strings the editor paints into the DOM (toolbar tooltips, the
3
+ * fullscreen exit button) and onto the timeline canvas (phantom new-
4
+ * track label, track header labels). Every user-visible literal in
5
+ * `@aicut/core` flows through this interface — there are no hidden
6
+ * hard-coded translations elsewhere in the library.
7
+ *
8
+ * Defaults to English. Hosts that want Chinese (or any other locale)
9
+ * pass `locale: localeZh` to `Editor.create` / `Timeline.create`, or
10
+ * override individual keys with `locale: { undo: "撤销" }`.
11
+ */
12
+ interface Locale {
13
+ undo: string;
14
+ redo: string;
15
+ split: string;
16
+ trimLeft: string;
17
+ trimRight: string;
18
+ playPause: string;
19
+ fullscreen: string;
20
+ snap: string;
21
+ /** Title shown on the snap button when snap is ON (clicking turns OFF). */
22
+ snapOnTitle: string;
23
+ /** Title shown when snap is OFF (clicking turns ON). */
24
+ snapOffTitle: string;
25
+ zoomOut: string;
26
+ zoomIn: string;
27
+ reset: string;
28
+ /** Toolbar tooltip when no keyframe exists at the playhead. */
29
+ keyframeAdd: string;
30
+ /** Toolbar tooltip when one exists — clicking removes it. */
31
+ keyframeRemove: string;
32
+ /** Toolbar tooltip — jump the playhead to the selected clip's start. */
33
+ seekClipStart: string;
34
+ /** Toolbar tooltip — jump the playhead to the selected clip's end. */
35
+ seekClipEnd: string;
36
+ /** Header text on the keyframe parameter panel. */
37
+ keyframePanelTitle: string;
38
+ /** Row label for the X-translation numeric input. */
39
+ keyframePanelLabelX: string;
40
+ /** Row label for the Y-translation numeric input. */
41
+ keyframePanelLabelY: string;
42
+ /** Row label for the scale numeric input. */
43
+ keyframePanelLabelScale: string;
44
+ /** Row label for the easing dropdown. */
45
+ keyframePanelLabelEasing: string;
46
+ /** Reset button label — pins this kf to identity (0, 0, 1). */
47
+ keyframePanelReset: string;
48
+ /** Reset button tooltip. */
49
+ keyframePanelResetTitle: string;
50
+ /** Badge tooltip — kf for THIS prop is pinned at this moment. */
51
+ keyframePanelBadgePinned: string;
52
+ /** Badge tooltip — prop has kfs elsewhere but not at this moment. */
53
+ keyframePanelBadgeAnimated: string;
54
+ /** Badge tooltip — prop has no kfs (riding the static base). */
55
+ keyframePanelBadgeStatic: string;
56
+ /** Time display suffix — appended after the seconds value. */
57
+ keyframePanelTimeSuffix: string;
58
+ keyframeEasingLinear: string;
59
+ keyframeEasingEaseIn: string;
60
+ keyframeEasingEaseOut: string;
61
+ keyframeEasingEaseInOut: string;
62
+ exitFullscreen: string;
63
+ exitFullscreenTitle: string;
64
+ /** Phantom row that appears under the last track during a drag. */
65
+ newTrack: string;
66
+ /** Track header — `{n}` is replaced with the 1-based track index. */
67
+ videoTrackLabel: string;
68
+ /** Same template format as videoTrackLabel. */
69
+ audioTrackLabel: string;
70
+ }
71
+ /** English. The library default — chosen over Chinese as the OSS norm. */
72
+ declare const localeEn: Locale;
73
+ /** Simplified Chinese. */
74
+ declare const localeZh: Locale;
75
+ /** Spread defaults under host overrides — host can supply a partial. */
76
+ declare function mergeLocale(partial: Partial<Locale> | undefined): Locale;
77
+ /**
78
+ * Replace `{key}` placeholders in a template. We only need `{n}`
79
+ * substitution today; the implementation is generic so additional
80
+ * keys (e.g. `{name}`) won't need a second pass.
81
+ */
82
+ declare function formatLabel(template: string, vars: Record<string, string | number>): string;
83
+
84
+ export { type Locale as L, localeZh as a, formatLabel as f, localeEn as l, mergeLocale as m };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * UI strings the editor paints into the DOM (toolbar tooltips, the
3
+ * fullscreen exit button) and onto the timeline canvas (phantom new-
4
+ * track label, track header labels). Every user-visible literal in
5
+ * `@aicut/core` flows through this interface — there are no hidden
6
+ * hard-coded translations elsewhere in the library.
7
+ *
8
+ * Defaults to English. Hosts that want Chinese (or any other locale)
9
+ * pass `locale: localeZh` to `Editor.create` / `Timeline.create`, or
10
+ * override individual keys with `locale: { undo: "撤销" }`.
11
+ */
12
+ interface Locale {
13
+ undo: string;
14
+ redo: string;
15
+ split: string;
16
+ trimLeft: string;
17
+ trimRight: string;
18
+ playPause: string;
19
+ fullscreen: string;
20
+ snap: string;
21
+ /** Title shown on the snap button when snap is ON (clicking turns OFF). */
22
+ snapOnTitle: string;
23
+ /** Title shown when snap is OFF (clicking turns ON). */
24
+ snapOffTitle: string;
25
+ zoomOut: string;
26
+ zoomIn: string;
27
+ reset: string;
28
+ /** Toolbar tooltip when no keyframe exists at the playhead. */
29
+ keyframeAdd: string;
30
+ /** Toolbar tooltip when one exists — clicking removes it. */
31
+ keyframeRemove: string;
32
+ /** Toolbar tooltip — jump the playhead to the selected clip's start. */
33
+ seekClipStart: string;
34
+ /** Toolbar tooltip — jump the playhead to the selected clip's end. */
35
+ seekClipEnd: string;
36
+ /** Header text on the keyframe parameter panel. */
37
+ keyframePanelTitle: string;
38
+ /** Row label for the X-translation numeric input. */
39
+ keyframePanelLabelX: string;
40
+ /** Row label for the Y-translation numeric input. */
41
+ keyframePanelLabelY: string;
42
+ /** Row label for the scale numeric input. */
43
+ keyframePanelLabelScale: string;
44
+ /** Row label for the easing dropdown. */
45
+ keyframePanelLabelEasing: string;
46
+ /** Reset button label — pins this kf to identity (0, 0, 1). */
47
+ keyframePanelReset: string;
48
+ /** Reset button tooltip. */
49
+ keyframePanelResetTitle: string;
50
+ /** Badge tooltip — kf for THIS prop is pinned at this moment. */
51
+ keyframePanelBadgePinned: string;
52
+ /** Badge tooltip — prop has kfs elsewhere but not at this moment. */
53
+ keyframePanelBadgeAnimated: string;
54
+ /** Badge tooltip — prop has no kfs (riding the static base). */
55
+ keyframePanelBadgeStatic: string;
56
+ /** Time display suffix — appended after the seconds value. */
57
+ keyframePanelTimeSuffix: string;
58
+ keyframeEasingLinear: string;
59
+ keyframeEasingEaseIn: string;
60
+ keyframeEasingEaseOut: string;
61
+ keyframeEasingEaseInOut: string;
62
+ exitFullscreen: string;
63
+ exitFullscreenTitle: string;
64
+ /** Phantom row that appears under the last track during a drag. */
65
+ newTrack: string;
66
+ /** Track header — `{n}` is replaced with the 1-based track index. */
67
+ videoTrackLabel: string;
68
+ /** Same template format as videoTrackLabel. */
69
+ audioTrackLabel: string;
70
+ }
71
+ /** English. The library default — chosen over Chinese as the OSS norm. */
72
+ declare const localeEn: Locale;
73
+ /** Simplified Chinese. */
74
+ declare const localeZh: Locale;
75
+ /** Spread defaults under host overrides — host can supply a partial. */
76
+ declare function mergeLocale(partial: Partial<Locale> | undefined): Locale;
77
+ /**
78
+ * Replace `{key}` placeholders in a template. We only need `{n}`
79
+ * substitution today; the implementation is generic so additional
80
+ * keys (e.g. `{name}`) won't need a second pass.
81
+ */
82
+ declare function formatLabel(template: string, vars: Record<string, string | number>): string;
83
+
84
+ export { type Locale as L, localeZh as a, formatLabel as f, localeEn as l, mergeLocale as m };