@aicut/core 0.5.0 → 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 +66 -0
- package/dist/chunk-H6AY6NW4.js +123 -0
- package/dist/chunk-H6AY6NW4.js.map +1 -0
- package/dist/chunk-WTCK3XQ6.js +93 -0
- package/dist/chunk-WTCK3XQ6.js.map +1 -0
- package/dist/{i18n-B-DFWgKe.d.cts → i18n-B24k4XVG.d.cts} +34 -1
- package/dist/{i18n-B-DFWgKe.d.ts → i18n-B24k4XVG.d.ts} +34 -1
- package/dist/index.cjs +1917 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +348 -25
- package/dist/index.d.ts +348 -25
- package/dist/index.js +1779 -86
- package/dist/index.js.map +1 -1
- package/dist/lighting/index.cjs +24 -6
- package/dist/lighting/index.cjs.map +1 -1
- package/dist/lighting/index.d.cts +2 -2
- package/dist/lighting/index.d.ts +2 -2
- package/dist/lighting/index.js +1 -1
- package/dist/playback/webcodecs/index.cjs +113 -6
- package/dist/playback/webcodecs/index.cjs.map +1 -1
- package/dist/playback/webcodecs/index.d.cts +18 -2
- package/dist/playback/webcodecs/index.d.ts +18 -2
- package/dist/playback/webcodecs/index.js +46 -6
- package/dist/playback/webcodecs/index.js.map +1 -1
- package/dist/{types-DvKlxylu.d.cts → types-BbZjOQLz.d.ts} +34 -1
- package/dist/{types-rwZx6FxE.d.ts → types-CjvRUPtZ.d.cts} +34 -1
- package/dist/types-CmS-UIEr.d.cts +137 -0
- package/dist/types-CmS-UIEr.d.ts +137 -0
- package/package.json +1 -1
- package/styles/theme.css +351 -0
- package/dist/chunk-CCDON7CU.js +0 -87
- package/dist/chunk-CCDON7CU.js.map +0 -1
- package/dist/types-CHplD9V5.d.cts +0 -70
- package/dist/types-CHplD9V5.d.ts +0 -70
package/README.md
CHANGED
|
@@ -266,6 +266,72 @@ setTimelineMetrics({ trackHeight: 36, rulerHeight: 20 });
|
|
|
266
266
|
|
|
267
267
|
`TRACK_HEIGHT` and `RULER_HEIGHT` are ESM live bindings — re-reading them after the setter returns the updated values.
|
|
268
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
|
+
|
|
269
335
|
## Lighting picker (opt-in sub-entry)
|
|
270
336
|
|
|
271
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":";AAgFO,IAAM,QAAA,GAAmB;AAAA,EAC9B,IAAA,EAAM,gBAAA;AAAA,EACN,IAAA,EAAM,sBAAA;AAAA,EACN,KAAA,EAAO,WAAA;AAAA,EACP,QAAA,EAAU,oBAAA;AAAA,EACV,SAAA,EAAW,qBAAA;AAAA,EACX,SAAA,EAAW,sBAAA;AAAA,EACX,UAAA,EAAY,oBAAA;AAAA,EACZ,IAAA,EAAM,MAAA;AAAA,EACN,WAAA,EAAa,eAAA;AAAA,EACb,YAAA,EAAc,cAAA;AAAA,EACd,OAAA,EAAS,UAAA;AAAA,EACT,MAAA,EAAQ,SAAA;AAAA,EACR,KAAA,EAAO,4BAAA;AAAA,EACP,WAAA,EAAa,0BAAA;AAAA,EACb,cAAA,EAAgB,6BAAA;AAAA,EAChB,aAAA,EAAe,wBAAA;AAAA,EACf,WAAA,EAAa,sBAAA;AAAA,EACb,kBAAA,EAAoB,UAAA;AAAA,EACpB,mBAAA,EAAqB,GAAA;AAAA,EACrB,mBAAA,EAAqB,GAAA;AAAA,EACrB,uBAAA,EAAyB,OAAA;AAAA,EACzB,wBAAA,EAA0B,QAAA;AAAA,EAC1B,kBAAA,EAAoB,gBAAA;AAAA,EACpB,uBAAA,EACE,yDAAA;AAAA,EACF,wBAAA,EAA0B,uBAAA;AAAA,EAC1B,0BAAA,EAA4B,qDAAA;AAAA,EAC5B,wBAAA,EAA0B,cAAA;AAAA,EAC1B,uBAAA,EAAyB,GAAA;AAAA,EACzB,oBAAA,EAAsB,QAAA;AAAA,EACtB,oBAAA,EAAsB,SAAA;AAAA,EACtB,qBAAA,EAAuB,UAAA;AAAA,EACvB,uBAAA,EAAyB,aAAA;AAAA,EACzB,cAAA,EAAgB,iBAAA;AAAA,EAChB,mBAAA,EAAqB,uBAAA;AAAA,EACrB,QAAA,EAAU,aAAA;AAAA,EACV,eAAA,EAAiB,WAAA;AAAA,EACjB,eAAA,EAAiB;AACnB;AAGO,IAAM,QAAA,GAAmB;AAAA,EAC9B,IAAA,EAAM,wBAAA;AAAA,EACN,IAAA,EAAM,8BAAA;AAAA,EACN,KAAA,EAAO,kBAAA;AAAA,EACP,QAAA,EAAU,8BAAA;AAAA,EACV,SAAA,EAAW,8BAAA;AAAA,EACX,SAAA,EAAW,qCAAA;AAAA,EACX,UAAA,EAAY,0BAAA;AAAA,EACZ,IAAA,EAAM,cAAA;AAAA,EACN,WAAA,EAAa,0BAAA;AAAA,EACb,YAAA,EAAc,0BAAA;AAAA,EACd,OAAA,EAAS,cAAA;AAAA,EACT,MAAA,EAAQ,cAAA;AAAA,EACR,KAAA,EAAO,oEAAA;AAAA,EACP,WAAA,EAAa,gCAAA;AAAA,EACb,cAAA,EAAgB,4CAAA;AAAA,EAChB,aAAA,EAAe,0CAAA;AAAA,EACf,WAAA,EAAa,0CAAA;AAAA,EACb,kBAAA,EAAoB,oBAAA;AAAA,EACpB,mBAAA,EAAqB,gBAAA;AAAA,EACrB,mBAAA,EAAqB,gBAAA;AAAA,EACrB,uBAAA,EAAyB,cAAA;AAAA,EACzB,wBAAA,EAA0B,cAAA;AAAA,EAC1B,kBAAA,EAAoB,0BAAA;AAAA,EACpB,uBAAA,EACE,6GAAA;AAAA,EACF,wBAAA,EAA0B,4CAAA;AAAA,EAC1B,0BAAA,EAA4B,4FAAA;AAAA,EAC5B,wBAAA,EAA0B,8DAAA;AAAA,EAC1B,uBAAA,EAAyB,QAAA;AAAA,EACzB,oBAAA,EAAsB,cAAA;AAAA,EACtB,oBAAA,EAAsB,cAAA;AAAA,EACtB,qBAAA,EAAuB,cAAA;AAAA,EACvB,uBAAA,EAAyB,0BAAA;AAAA,EACzB,cAAA,EAAgB,0BAAA;AAAA,EAChB,mBAAA,EAAqB,gCAAA;AAAA,EACrB,QAAA,EAAU,sBAAA;AAAA,EACV,eAAA,EAAiB,kBAAA;AAAA,EACjB,eAAA,EAAiB;AACnB;AAGO,SAAS,YAAY,OAAA,EAA8C;AACxE,EAAA,OAAO,UAAU,EAAE,GAAG,QAAA,EAAU,GAAG,SAAQ,GAAI,QAAA;AACjD;AAOO,SAAS,WAAA,CACd,UACA,IAAA,EACQ;AACR,EAAA,OAAO,QAAA,CAAS,OAAA;AAAA,IAAQ,YAAA;AAAA,IAAc,CAAC,CAAA,EAAG,CAAA,KACxC,CAAA,IAAK,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,CAAC,CAAC,CAAA,GAAI,CAAA,CAAA,EAAI,CAAC,CAAA,CAAA;AAAA,GACrC;AACF;;;ACvKA,IAAM,UAAA,GAA0C;AAAA,EAC9C,KAAA,EAAO,eAAA;AAAA,EACP,SAAA,EAAW,mBAAA;AAAA,EACX,OAAA,EAAS,iBAAA;AAAA,EACT,IAAA,EAAM,cAAA;AAAA,EACN,KAAA,EAAO,eAAA;AAAA,EACP,IAAA,EAAM,cAAA;AAAA,EACN,OAAA,EAAS,iBAAA;AAAA,EACT,OAAA,EAAS,iBAAA;AAAA,EACT,IAAA,EAAM,cAAA;AAAA,EACN,KAAA,EAAO,eAAA;AAAA,EACP,UAAA,EAAY,qBAAA;AAAA,EACZ,cAAA,EAAgB,yBAAA;AAAA,EAChB,YAAA,EAAc,uBAAA;AAAA,EACd,aAAA,EAAe,wBAAA;AAAA,EACf,cAAA,EAAgB,yBAAA;AAAA,EAChB,SAAA,EAAW,oBAAA;AAAA,EACX,QAAA,EAAU,mBAAA;AAAA,EACV,QAAA,EAAU,mBAAA;AAAA,EACV,QAAA,EAAU;AACZ,CAAA;AAEO,SAAS,UAAA,CAAW,MAAmB,KAAA,EAAgC;AAC5E,EAAA,IAAI,CAAC,KAAA,EAAO;AACZ,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,EAAyB;AAC1D,IAAA,MAAM,MAAA,GAAS,WAAW,GAAG,CAAA;AAC7B,IAAA,MAAM,KAAA,GAAQ,MAAM,GAAG,CAAA;AACvB,IAAA,IAAI,UAAU,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,QAAQ,KAAK,CAAA;AAAA,EAC3D;AACF","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"]}
|
|
@@ -15,7 +15,6 @@ interface Locale {
|
|
|
15
15
|
split: string;
|
|
16
16
|
trimLeft: string;
|
|
17
17
|
trimRight: string;
|
|
18
|
-
speedComingSoon: string;
|
|
19
18
|
playPause: string;
|
|
20
19
|
fullscreen: string;
|
|
21
20
|
snap: string;
|
|
@@ -26,6 +25,40 @@ interface Locale {
|
|
|
26
25
|
zoomOut: string;
|
|
27
26
|
zoomIn: string;
|
|
28
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;
|
|
29
62
|
exitFullscreen: string;
|
|
30
63
|
exitFullscreenTitle: string;
|
|
31
64
|
/** Phantom row that appears under the last track during a drag. */
|
|
@@ -15,7 +15,6 @@ interface Locale {
|
|
|
15
15
|
split: string;
|
|
16
16
|
trimLeft: string;
|
|
17
17
|
trimRight: string;
|
|
18
|
-
speedComingSoon: string;
|
|
19
18
|
playPause: string;
|
|
20
19
|
fullscreen: string;
|
|
21
20
|
snap: string;
|
|
@@ -26,6 +25,40 @@ interface Locale {
|
|
|
26
25
|
zoomOut: string;
|
|
27
26
|
zoomIn: string;
|
|
28
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;
|
|
29
62
|
exitFullscreen: string;
|
|
30
63
|
exitFullscreenTitle: string;
|
|
31
64
|
/** Phantom row that appears under the last track during a drag. */
|