@elah/editor 0.4.1 → 1.0.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 +241 -179
- package/dist/editor/AssetPanel/AssetPanel.js +1 -1
- package/dist/editor/Preview/MediaTransformOverlay.d.ts +1 -1
- package/dist/editor/Preview/MediaTransformOverlay.js +245 -44
- package/dist/editor/Preview/Preview.js +99 -24
- package/dist/editor/Preview/PreviewLoadingOverlay.d.ts +1 -0
- package/dist/editor/Preview/PreviewLoadingOverlay.js +18 -0
- package/dist/editor/Preview/ShapeOverlay.d.ts +1 -1
- package/dist/editor/Preview/ShapeOverlay.js +83 -37
- package/dist/editor/Preview/StageBorder.js +2 -2
- package/dist/editor/Preview/TextOverlay.d.ts +1 -1
- package/dist/editor/Preview/TextOverlay.js +143 -34
- package/dist/editor/SourcePanel/SourcePanel.js +1 -1
- package/dist/editor/index.d.ts +2 -2
- package/dist/editor/index.js +1 -1
- package/dist/index.d.ts +30 -14
- package/dist/index.js +18 -10
- package/dist/styles.css +1 -1
- package/package.json +4 -4
- package/src/styles/tokens.css +11 -0
- package/dist/editor/EditorProvider.d.ts +0 -14
- package/dist/editor/EditorProvider.js +0 -81
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
2
|
+
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { computeContainViewport } from '@elah/core';
|
|
4
|
-
import { useTimelineEngine, useSelectionStore, usePlaybackStore
|
|
4
|
+
import { useTimelineEngine, useSelectionStore, usePlaybackStore } from '@elah/react';
|
|
5
5
|
import { useResolvedScene } from '../useResolvedScene';
|
|
6
6
|
const DEFAULT_SCALE = 0.5;
|
|
7
7
|
const DEFAULT_CENTER = 0.5;
|
|
@@ -17,9 +17,11 @@ function baseTransform(t) {
|
|
|
17
17
|
scale: t?.scale ?? DEFAULT_SCALE,
|
|
18
18
|
rotation: t?.rotation ?? 0,
|
|
19
19
|
anchor: t?.anchor ?? { x: 0.5, y: 0.5 },
|
|
20
|
+
scaleX: t?.scaleX ?? 1,
|
|
21
|
+
scaleY: t?.scaleY ?? 1,
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
|
-
export function ShapeOverlay() {
|
|
24
|
+
export const ShapeOverlay = memo(function ShapeOverlay() {
|
|
23
25
|
const engine = useTimelineEngine();
|
|
24
26
|
const scene = useResolvedScene();
|
|
25
27
|
const isPlaying = usePlaybackStore((s) => s.isPlaying);
|
|
@@ -48,14 +50,17 @@ export function ShapeOverlay() {
|
|
|
48
50
|
const shortSide = Math.min(stage.width, stage.height);
|
|
49
51
|
return scene.shapes.map((clip) => {
|
|
50
52
|
const t = baseTransform(clip.transform);
|
|
51
|
-
const
|
|
53
|
+
const baseSideStage = t.scale * shortSide;
|
|
54
|
+
const widthStage = baseSideStage * (t.scaleX ?? 1);
|
|
55
|
+
const heightStage = baseSideStage * (t.scaleY ?? 1);
|
|
52
56
|
const cxStage = t.x * stage.width;
|
|
53
57
|
const cyStage = t.y * stage.height;
|
|
54
58
|
const centerScreenX = fit.x + cxStage * scale;
|
|
55
59
|
const centerScreenY = fit.y + cyStage * scale;
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
60
|
+
const rawW = widthStage * scale;
|
|
61
|
+
const rawH = heightStage * scale;
|
|
62
|
+
const w = Math.max(rawW, MIN_BOX_PX);
|
|
63
|
+
const h = Math.max(rawH, MIN_BOX_PX);
|
|
59
64
|
const left = centerScreenX - w / 2;
|
|
60
65
|
const top = centerScreenY - h / 2;
|
|
61
66
|
return {
|
|
@@ -67,6 +72,13 @@ export function ShapeOverlay() {
|
|
|
67
72
|
});
|
|
68
73
|
}, [scene.shapes, stage.width, stage.height, fit, scale]);
|
|
69
74
|
const ownsSelection = useMemo(() => items.some((it) => selectedClipIds.has(it.clip.id)), [items, selectedClipIds]);
|
|
75
|
+
const readAuthored = useCallback((clip) => {
|
|
76
|
+
const found = engine.findClip(clip.id);
|
|
77
|
+
if (!found) {
|
|
78
|
+
return { trackId: clip.trackId, base: baseTransform(clip.transform) };
|
|
79
|
+
}
|
|
80
|
+
return { trackId: found.trackId, base: baseTransform(found.clip.transform) };
|
|
81
|
+
}, [engine]);
|
|
70
82
|
const handlePointerMove = useCallback((e) => {
|
|
71
83
|
const g = gestureRef.current;
|
|
72
84
|
if (!g)
|
|
@@ -79,10 +91,37 @@ export function ShapeOverlay() {
|
|
|
79
91
|
engine.previewClip(g.id, g.trackId, { transform: { ...g.base, x, y } });
|
|
80
92
|
}
|
|
81
93
|
else {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
94
|
+
const h = g.handle;
|
|
95
|
+
const halfW0 = g.halfW0;
|
|
96
|
+
const halfH0 = g.halfH0;
|
|
97
|
+
const dxStage = (e.clientX - g.startClientX) / scale;
|
|
98
|
+
const dyStage = (e.clientY - g.startClientY) / scale;
|
|
99
|
+
const shortSide = Math.min(g.stageW, g.stageH);
|
|
100
|
+
const minHalf = (MIN_SCALE * shortSide) / 2;
|
|
101
|
+
const maxHalf = (MAX_SCALE * shortSide) / 2;
|
|
102
|
+
let newHalfW = h.sx !== 0 ? clamp(halfW0 + h.sx * dxStage, minHalf, maxHalf) : halfW0;
|
|
103
|
+
let newHalfH = h.sy !== 0 ? clamp(halfH0 + h.sy * dyStage, minHalf, maxHalf) : halfH0;
|
|
104
|
+
if (e.shiftKey) {
|
|
105
|
+
const ratioW = newHalfW / halfW0;
|
|
106
|
+
const ratioH = newHalfH / halfH0;
|
|
107
|
+
const ratio = h.sx !== 0 && h.sy !== 0 ? Math.max(ratioW, ratioH) : h.sx !== 0 ? ratioW : ratioH;
|
|
108
|
+
newHalfW = clamp(halfW0 * ratio, minHalf, maxHalf);
|
|
109
|
+
newHalfH = clamp(halfH0 * ratio, minHalf, maxHalf);
|
|
110
|
+
}
|
|
111
|
+
const centerX = g.anchorStageX + h.sx * newHalfW;
|
|
112
|
+
const centerY = g.anchorStageY + h.sy * newHalfH;
|
|
113
|
+
const baseHalf = (g.base.scale * shortSide) / 2;
|
|
114
|
+
const scaleX = newHalfW / Math.max(1, baseHalf);
|
|
115
|
+
const scaleY = newHalfH / Math.max(1, baseHalf);
|
|
116
|
+
engine.previewClip(g.id, g.trackId, {
|
|
117
|
+
transform: {
|
|
118
|
+
...g.base,
|
|
119
|
+
x: centerX / g.stageW,
|
|
120
|
+
y: centerY / g.stageH,
|
|
121
|
+
scaleX,
|
|
122
|
+
scaleY,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
86
125
|
}
|
|
87
126
|
}, [engine, scale]);
|
|
88
127
|
const endGesture = useCallback((e) => {
|
|
@@ -101,42 +140,45 @@ export function ShapeOverlay() {
|
|
|
101
140
|
if (!selectedClipIds.has(item.clip.id))
|
|
102
141
|
selectClip(item.clip.id);
|
|
103
142
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
143
|
+
const authored = readAuthored(item.clip);
|
|
104
144
|
gestureRef.current = {
|
|
105
145
|
type: 'move',
|
|
106
146
|
id: item.clip.id,
|
|
107
|
-
trackId:
|
|
108
|
-
base:
|
|
147
|
+
trackId: authored.trackId,
|
|
148
|
+
base: authored.base,
|
|
109
149
|
stageW: stage.width,
|
|
110
150
|
stageH: stage.height,
|
|
111
151
|
startClientX: e.clientX,
|
|
112
152
|
startClientY: e.clientY,
|
|
113
|
-
centerClientX: 0,
|
|
114
|
-
centerClientY: 0,
|
|
115
|
-
startDist: 0,
|
|
116
153
|
};
|
|
117
|
-
}, [selectedClipIds, selectClip, stage.width, stage.height]);
|
|
118
|
-
const beginResize = useCallback((e, item) => {
|
|
154
|
+
}, [selectedClipIds, selectClip, stage.width, stage.height, readAuthored]);
|
|
155
|
+
const beginResize = useCallback((e, item, handle) => {
|
|
119
156
|
e.stopPropagation();
|
|
120
157
|
if (!selectedClipIds.has(item.clip.id))
|
|
121
158
|
selectClip(item.clip.id);
|
|
122
159
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const
|
|
160
|
+
const { trackId, base } = readAuthored(item.clip);
|
|
161
|
+
const shortSide = Math.min(stage.width, stage.height);
|
|
162
|
+
const halfW0 = (base.scale * shortSide * (base.scaleX ?? 1)) / 2;
|
|
163
|
+
const halfH0 = (base.scale * shortSide * (base.scaleY ?? 1)) / 2;
|
|
164
|
+
const centerX = base.x * stage.width;
|
|
165
|
+
const centerY = base.y * stage.height;
|
|
126
166
|
gestureRef.current = {
|
|
127
167
|
type: 'resize',
|
|
128
168
|
id: item.clip.id,
|
|
129
|
-
trackId
|
|
130
|
-
base
|
|
169
|
+
trackId,
|
|
170
|
+
base,
|
|
131
171
|
stageW: stage.width,
|
|
132
172
|
stageH: stage.height,
|
|
133
173
|
startClientX: e.clientX,
|
|
134
174
|
startClientY: e.clientY,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
175
|
+
handle: { sx: handle.sx, sy: handle.sy },
|
|
176
|
+
anchorStageX: centerX - handle.sx * halfW0,
|
|
177
|
+
anchorStageY: centerY - handle.sy * halfH0,
|
|
178
|
+
halfW0,
|
|
179
|
+
halfH0,
|
|
138
180
|
};
|
|
139
|
-
}, [selectedClipIds, selectClip, stage.width, stage.height]);
|
|
181
|
+
}, [selectedClipIds, selectClip, stage.width, stage.height, readAuthored]);
|
|
140
182
|
return (_jsxs("div", { ref: rootRef, className: "absolute inset-0 z-[3] pointer-events-none overflow-hidden", children: [!isPlaying && ownsSelection && (_jsx("div", { className: "absolute inset-0 pointer-events-auto", onPointerDown: () => clearSelection() })), !isPlaying &&
|
|
141
183
|
items.map((item) => {
|
|
142
184
|
const { clip, rect } = item;
|
|
@@ -157,23 +199,27 @@ export function ShapeOverlay() {
|
|
|
157
199
|
touchAction: 'none',
|
|
158
200
|
};
|
|
159
201
|
return (_jsx("div", { style: boxStyle, onPointerDown: (e) => beginMove(e, item), onPointerMove: handlePointerMove, onPointerUp: endGesture, onPointerCancel: endGesture, children: selected &&
|
|
160
|
-
|
|
202
|
+
HANDLES.map((handle) => (_jsx("div", { onPointerDown: (e) => beginResize(e, item, handle), onPointerMove: handlePointerMove, onPointerUp: endGesture, onPointerCancel: endGesture, style: {
|
|
161
203
|
position: 'absolute',
|
|
162
|
-
...
|
|
204
|
+
...handle.pos,
|
|
163
205
|
width: 10,
|
|
164
206
|
height: 10,
|
|
165
207
|
background: 'var(--elah-selection-handle, #fff)',
|
|
166
208
|
border: '1px solid var(--elah-selection-color, #4c9aff)',
|
|
167
209
|
borderRadius: 2,
|
|
168
|
-
cursor:
|
|
210
|
+
cursor: handle.cursor,
|
|
169
211
|
pointerEvents: 'auto',
|
|
170
212
|
touchAction: 'none',
|
|
171
|
-
} },
|
|
213
|
+
} }, handle.key))) }, clip.id));
|
|
172
214
|
})] }));
|
|
173
|
-
}
|
|
174
|
-
const
|
|
175
|
-
{ key: 'nw', pos: { left: -5, top: -5 }, cursor: 'nwse-resize' },
|
|
176
|
-
{ key: '
|
|
177
|
-
{ key: '
|
|
178
|
-
{ key: '
|
|
215
|
+
});
|
|
216
|
+
const HANDLES = [
|
|
217
|
+
{ key: 'nw', sx: -1, sy: -1, pos: { left: -5, top: -5 }, cursor: 'nwse-resize' },
|
|
218
|
+
{ key: 'n', sx: 0, sy: -1, pos: { left: '50%', top: -5, transform: 'translateX(-50%)' }, cursor: 'ns-resize' },
|
|
219
|
+
{ key: 'ne', sx: 1, sy: -1, pos: { right: -5, top: -5 }, cursor: 'nesw-resize' },
|
|
220
|
+
{ key: 'e', sx: 1, sy: 0, pos: { right: -5, top: '50%', transform: 'translateY(-50%)' }, cursor: 'ew-resize' },
|
|
221
|
+
{ key: 'se', sx: 1, sy: 1, pos: { right: -5, bottom: -5 }, cursor: 'nwse-resize' },
|
|
222
|
+
{ key: 's', sx: 0, sy: 1, pos: { left: '50%', bottom: -5, transform: 'translateX(-50%)' }, cursor: 'ns-resize' },
|
|
223
|
+
{ key: 'sw', sx: -1, sy: 1, pos: { left: -5, bottom: -5 }, cursor: 'nesw-resize' },
|
|
224
|
+
{ key: 'w', sx: -1, sy: 0, pos: { left: -5, top: '50%', transform: 'translateY(-50%)' }, cursor: 'ew-resize' },
|
|
179
225
|
];
|
|
@@ -23,8 +23,8 @@ export function StageBorder() {
|
|
|
23
23
|
top: fit.y,
|
|
24
24
|
width: fit.width,
|
|
25
25
|
height: fit.height,
|
|
26
|
-
border: '1px solid var(--elah-stage-border, rgba(
|
|
27
|
-
boxShadow: 'var(--elah-stage-glow, 0 0 20px rgba(
|
|
26
|
+
border: '1px solid var(--elah-stage-border, rgba(0, 194, 255, 0.45))',
|
|
27
|
+
boxShadow: 'var(--elah-stage-glow, 0 0 20px rgba(0, 194, 255, 0.08))',
|
|
28
28
|
boxSizing: 'border-box',
|
|
29
29
|
} })) }));
|
|
30
30
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare const TextOverlay: import("react").NamedExoticComponent<object>;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
-
import { computeTextLayout, computeContainViewport, SIDE_MARGIN
|
|
4
|
-
import { useTimelineEngine, useSelectionStore, usePlaybackStore
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { computeTextLayout, computeContainViewport, SIDE_MARGIN } from '@elah/core';
|
|
4
|
+
import { useTimelineEngine, useSelectionStore, usePlaybackStore } from '@elah/react';
|
|
5
5
|
import { useResolvedScene } from '../useResolvedScene';
|
|
6
6
|
const MIN_FONT_SIZE = 6;
|
|
7
7
|
const MAX_FONT_SIZE = 4000;
|
|
8
8
|
const MIN_BOX_PX = 28;
|
|
9
9
|
const BOX_PAD = 4;
|
|
10
|
+
const ROTATE_STEM_PX = 20;
|
|
11
|
+
const ROTATE_KNOB_PX = 14;
|
|
12
|
+
const ROTATE_SNAP_STEP = Math.PI / 12;
|
|
13
|
+
const ROTATE_SNAP_EPS = Math.PI / 60;
|
|
10
14
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
11
15
|
const clamp01 = (v) => clamp(v, 0, 1);
|
|
12
16
|
function makeTransform(x, y, base) {
|
|
@@ -18,7 +22,11 @@ function makeTransform(x, y, base) {
|
|
|
18
22
|
anchor: base?.anchor ?? { x: 0.5, y: 0.5 },
|
|
19
23
|
};
|
|
20
24
|
}
|
|
21
|
-
|
|
25
|
+
function unscaleFontSize(layout, resolved) {
|
|
26
|
+
const s = resolved?.scale ?? 1;
|
|
27
|
+
return s > 0 ? layout.style.fontSize / s : layout.style.fontSize;
|
|
28
|
+
}
|
|
29
|
+
export const TextOverlay = memo(function TextOverlay() {
|
|
22
30
|
const engine = useTimelineEngine();
|
|
23
31
|
const scene = useResolvedScene();
|
|
24
32
|
const isPlaying = usePlaybackStore((s) => s.isPlaying);
|
|
@@ -60,16 +68,45 @@ export function TextOverlay() {
|
|
|
60
68
|
return scene.texts.map((clip) => {
|
|
61
69
|
const layout = computeTextLayout(measurer, clip, stage);
|
|
62
70
|
const centerScreenX = fit.x + layout.center.x * stage.width * scale;
|
|
71
|
+
const centerScreenY = fit.y + layout.center.y * stage.height * scale;
|
|
63
72
|
const rawLeft = fit.x + layout.box.x * scale;
|
|
64
73
|
const rawW = layout.box.width * scale;
|
|
65
74
|
const w = Math.max(rawW, MIN_BOX_PX);
|
|
66
75
|
const left = rawW < MIN_BOX_PX ? centerScreenX - w / 2 : rawLeft;
|
|
67
76
|
const h = Math.max(layout.box.height * scale, MIN_BOX_PX);
|
|
68
77
|
const top = fit.y + layout.box.y * scale;
|
|
69
|
-
|
|
78
|
+
const rotation = clip.transform?.rotation ?? 0;
|
|
79
|
+
return {
|
|
80
|
+
clip,
|
|
81
|
+
layout,
|
|
82
|
+
centerScreenX,
|
|
83
|
+
centerScreenY,
|
|
84
|
+
rotation,
|
|
85
|
+
rect: { left, top, width: w, height: h },
|
|
86
|
+
};
|
|
70
87
|
});
|
|
71
88
|
}, [scene.texts, stage, fit, scale, getMeasurer]);
|
|
72
89
|
const ownsSelection = useMemo(() => items.some((it) => selectedClipIds.has(it.clip.id)), [items, selectedClipIds]);
|
|
90
|
+
const readAuthored = useCallback((clip, layout) => {
|
|
91
|
+
const found = engine.findClip(clip.id);
|
|
92
|
+
if (!found) {
|
|
93
|
+
return {
|
|
94
|
+
trackId: clip.trackId,
|
|
95
|
+
transform: clip.transform,
|
|
96
|
+
center: { ...layout.center },
|
|
97
|
+
fontSize: layout.style.fontSize,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const authored = found.clip;
|
|
101
|
+
return {
|
|
102
|
+
trackId: found.trackId,
|
|
103
|
+
transform: authored.transform,
|
|
104
|
+
center: authored.transform
|
|
105
|
+
? { x: authored.transform.x, y: authored.transform.y }
|
|
106
|
+
: { x: 0.5, y: 0.5 },
|
|
107
|
+
fontSize: authored.fontSize ?? unscaleFontSize(layout, clip.transform),
|
|
108
|
+
};
|
|
109
|
+
}, [engine]);
|
|
73
110
|
const handlePointerMove = useCallback((e) => {
|
|
74
111
|
const g = gestureRef.current;
|
|
75
112
|
if (!g)
|
|
@@ -83,12 +120,30 @@ export function TextOverlay() {
|
|
|
83
120
|
transform: makeTransform(nx, ny, g.startTransform),
|
|
84
121
|
});
|
|
85
122
|
}
|
|
86
|
-
else {
|
|
123
|
+
else if (g.type === 'resize') {
|
|
87
124
|
const dist = Math.hypot(e.clientX - g.centerClientX, e.clientY - g.centerClientY);
|
|
88
125
|
const ratio = g.startDist > 0 ? dist / g.startDist : 1;
|
|
89
126
|
const fontSize = clamp(Math.round(g.startFontSize * ratio), MIN_FONT_SIZE, MAX_FONT_SIZE);
|
|
90
127
|
engine.previewClip(g.id, g.trackId, { fontSize });
|
|
91
128
|
}
|
|
129
|
+
else {
|
|
130
|
+
const angle = Math.atan2(e.clientY - g.centerClientY, e.clientX - g.centerClientX);
|
|
131
|
+
let rotation = g.startRotation + (angle - g.startAngle);
|
|
132
|
+
if (e.shiftKey) {
|
|
133
|
+
rotation = Math.round(rotation / ROTATE_SNAP_STEP) * ROTATE_SNAP_STEP;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const nearest = Math.round(rotation / (Math.PI / 2)) * (Math.PI / 2);
|
|
137
|
+
if (Math.abs(rotation - nearest) < ROTATE_SNAP_EPS)
|
|
138
|
+
rotation = nearest;
|
|
139
|
+
}
|
|
140
|
+
engine.previewClip(g.id, g.trackId, {
|
|
141
|
+
transform: {
|
|
142
|
+
...makeTransform(g.startCenter.x, g.startCenter.y, g.startTransform),
|
|
143
|
+
rotation,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
92
147
|
}, [engine, scale, stage.width, stage.height]);
|
|
93
148
|
const endGesture = useCallback((e) => {
|
|
94
149
|
const g = gestureRef.current;
|
|
@@ -99,7 +154,7 @@ export function TextOverlay() {
|
|
|
99
154
|
if (target.hasPointerCapture?.(e.pointerId)) {
|
|
100
155
|
target.releasePointerCapture(e.pointerId);
|
|
101
156
|
}
|
|
102
|
-
engine.commitInteraction(g.type === 'move' ? 'Move text' : 'Resize text');
|
|
157
|
+
engine.commitInteraction(g.type === 'move' ? 'Move text' : g.type === 'rotate' ? 'Rotate text' : 'Resize text');
|
|
103
158
|
}, [engine]);
|
|
104
159
|
const beginMove = useCallback((e, clip, layout) => {
|
|
105
160
|
if (editingId === clip.id)
|
|
@@ -108,44 +163,75 @@ export function TextOverlay() {
|
|
|
108
163
|
if (!selectedClipIds.has(clip.id))
|
|
109
164
|
selectClip(clip.id);
|
|
110
165
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
166
|
+
const authored = readAuthored(clip, layout);
|
|
111
167
|
gestureRef.current = {
|
|
112
168
|
type: 'move',
|
|
113
169
|
id: clip.id,
|
|
114
|
-
trackId:
|
|
170
|
+
trackId: authored.trackId,
|
|
115
171
|
startClientX: e.clientX,
|
|
116
172
|
startClientY: e.clientY,
|
|
117
|
-
startCenter:
|
|
118
|
-
startTransform:
|
|
173
|
+
startCenter: authored.center,
|
|
174
|
+
startTransform: authored.transform,
|
|
119
175
|
centerClientX: 0,
|
|
120
176
|
centerClientY: 0,
|
|
121
177
|
startDist: 0,
|
|
122
|
-
startFontSize:
|
|
178
|
+
startFontSize: authored.fontSize,
|
|
179
|
+
startAngle: 0,
|
|
180
|
+
startRotation: 0,
|
|
123
181
|
};
|
|
124
|
-
}, [editingId, selectedClipIds, selectClip]);
|
|
182
|
+
}, [editingId, selectedClipIds, selectClip, readAuthored]);
|
|
125
183
|
const beginResize = useCallback((e, item) => {
|
|
126
184
|
e.stopPropagation();
|
|
127
|
-
const { clip, layout, centerScreenX } = item;
|
|
185
|
+
const { clip, layout, centerScreenX, centerScreenY } = item;
|
|
128
186
|
if (!selectedClipIds.has(clip.id))
|
|
129
187
|
selectClip(clip.id);
|
|
130
188
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
131
189
|
const rootRect = rootRef.current?.getBoundingClientRect();
|
|
132
|
-
const centerScreenY = fit.y + layout.center.y * stage.height * scale;
|
|
133
190
|
const centerClientX = (rootRect?.left ?? 0) + centerScreenX;
|
|
134
191
|
const centerClientY = (rootRect?.top ?? 0) + centerScreenY;
|
|
192
|
+
const authored = readAuthored(clip, layout);
|
|
135
193
|
gestureRef.current = {
|
|
136
194
|
type: 'resize',
|
|
137
195
|
id: clip.id,
|
|
138
|
-
trackId:
|
|
196
|
+
trackId: authored.trackId,
|
|
139
197
|
startClientX: e.clientX,
|
|
140
198
|
startClientY: e.clientY,
|
|
141
|
-
startCenter:
|
|
142
|
-
startTransform:
|
|
199
|
+
startCenter: authored.center,
|
|
200
|
+
startTransform: authored.transform,
|
|
143
201
|
centerClientX,
|
|
144
202
|
centerClientY,
|
|
145
203
|
startDist: Math.hypot(e.clientX - centerClientX, e.clientY - centerClientY),
|
|
146
|
-
startFontSize:
|
|
204
|
+
startFontSize: authored.fontSize,
|
|
205
|
+
startAngle: 0,
|
|
206
|
+
startRotation: 0,
|
|
147
207
|
};
|
|
148
|
-
}, [selectedClipIds, selectClip,
|
|
208
|
+
}, [selectedClipIds, selectClip, readAuthored]);
|
|
209
|
+
const beginRotate = useCallback((e, item) => {
|
|
210
|
+
e.stopPropagation();
|
|
211
|
+
const { clip, layout, centerScreenX, centerScreenY } = item;
|
|
212
|
+
if (!selectedClipIds.has(clip.id))
|
|
213
|
+
selectClip(clip.id);
|
|
214
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
215
|
+
const rootRect = rootRef.current?.getBoundingClientRect();
|
|
216
|
+
const centerClientX = (rootRect?.left ?? 0) + centerScreenX;
|
|
217
|
+
const centerClientY = (rootRect?.top ?? 0) + centerScreenY;
|
|
218
|
+
const authored = readAuthored(clip, layout);
|
|
219
|
+
gestureRef.current = {
|
|
220
|
+
type: 'rotate',
|
|
221
|
+
id: clip.id,
|
|
222
|
+
trackId: authored.trackId,
|
|
223
|
+
startClientX: e.clientX,
|
|
224
|
+
startClientY: e.clientY,
|
|
225
|
+
startCenter: authored.center,
|
|
226
|
+
startTransform: authored.transform,
|
|
227
|
+
centerClientX,
|
|
228
|
+
centerClientY,
|
|
229
|
+
startDist: 0,
|
|
230
|
+
startFontSize: authored.fontSize,
|
|
231
|
+
startAngle: Math.atan2(e.clientY - centerClientY, e.clientX - centerClientX),
|
|
232
|
+
startRotation: authored.transform?.rotation ?? 0,
|
|
233
|
+
};
|
|
234
|
+
}, [selectedClipIds, selectClip, readAuthored]);
|
|
149
235
|
const startEditing = useCallback((clip) => {
|
|
150
236
|
selectClip(clip.id);
|
|
151
237
|
setEditText(clip.content);
|
|
@@ -185,22 +271,45 @@ export function TextOverlay() {
|
|
|
185
271
|
cursor: isEditing ? 'text' : 'move',
|
|
186
272
|
pointerEvents: isEditing ? 'none' : 'auto',
|
|
187
273
|
touchAction: 'none',
|
|
274
|
+
transform: item.rotation ? `rotate(${item.rotation}rad)` : undefined,
|
|
275
|
+
transformOrigin: `${item.centerScreenX - (rect.left - BOX_PAD)}px ${item.centerScreenY - (rect.top - BOX_PAD)}px`,
|
|
188
276
|
};
|
|
189
|
-
return (
|
|
277
|
+
return (_jsxs("div", { style: boxStyle, onPointerDown: (e) => beginMove(e, clip, layout), onPointerMove: handlePointerMove, onPointerUp: endGesture, onPointerCancel: endGesture, onDoubleClick: (e) => {
|
|
190
278
|
e.stopPropagation();
|
|
191
279
|
startEditing(clip);
|
|
192
|
-
}, children: selected && !isEditing &&
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
280
|
+
}, children: [selected && !isEditing && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
|
|
281
|
+
position: 'absolute',
|
|
282
|
+
left: '50%',
|
|
283
|
+
top: -ROTATE_STEM_PX,
|
|
284
|
+
width: 1,
|
|
285
|
+
height: ROTATE_STEM_PX,
|
|
286
|
+
background: 'var(--elah-selection-color, #4c9aff)',
|
|
287
|
+
pointerEvents: 'none',
|
|
288
|
+
} }), _jsx("div", { onPointerDown: (e) => beginRotate(e, item), onPointerMove: handlePointerMove, onPointerUp: endGesture, onPointerCancel: endGesture, title: "Rotate (Shift: 15\u00B0 steps)", style: {
|
|
289
|
+
position: 'absolute',
|
|
290
|
+
left: '50%',
|
|
291
|
+
top: -(ROTATE_STEM_PX + ROTATE_KNOB_PX),
|
|
292
|
+
transform: 'translateX(-50%)',
|
|
293
|
+
width: ROTATE_KNOB_PX,
|
|
294
|
+
height: ROTATE_KNOB_PX,
|
|
295
|
+
background: 'var(--elah-selection-handle, #fff)',
|
|
296
|
+
border: '1px solid var(--elah-selection-color, #4c9aff)',
|
|
297
|
+
borderRadius: '50%',
|
|
298
|
+
cursor: 'grab',
|
|
299
|
+
pointerEvents: 'auto',
|
|
300
|
+
touchAction: 'none',
|
|
301
|
+
} })] })), selected && !isEditing && CORNERS.map((corner) => (_jsx("div", { onPointerDown: (e) => beginResize(e, item), onPointerMove: handlePointerMove, onPointerUp: endGesture, onPointerCancel: endGesture, style: {
|
|
302
|
+
position: 'absolute',
|
|
303
|
+
...corner.pos,
|
|
304
|
+
width: 10,
|
|
305
|
+
height: 10,
|
|
306
|
+
background: 'var(--elah-selection-handle, #fff)',
|
|
307
|
+
border: '1px solid var(--elah-selection-color, #4c9aff)',
|
|
308
|
+
borderRadius: 2,
|
|
309
|
+
cursor: corner.cursor,
|
|
310
|
+
pointerEvents: 'auto',
|
|
311
|
+
touchAction: 'none',
|
|
312
|
+
} }, corner.key)))] }, clip.id));
|
|
204
313
|
}), !isPlaying && editingItem && (_jsx("textarea", { autoFocus: true, value: editText, onChange: (e) => {
|
|
205
314
|
const next = e.target.value;
|
|
206
315
|
setEditText(next);
|
|
@@ -238,7 +347,7 @@ export function TextOverlay() {
|
|
|
238
347
|
pointerEvents: 'auto',
|
|
239
348
|
boxSizing: 'border-box',
|
|
240
349
|
} }))] }));
|
|
241
|
-
}
|
|
350
|
+
});
|
|
242
351
|
const CORNERS = [
|
|
243
352
|
{ key: 'nw', pos: { left: -5, top: -5 }, cursor: 'nwse-resize' },
|
|
244
353
|
{ key: 'ne', pos: { right: -5, top: -5 }, cursor: 'nesw-resize' },
|
|
@@ -346,7 +346,7 @@ export function SourcePanel({ style, className, defaultLane = 'media', className
|
|
|
346
346
|
color: toast.tone === 'warn' ? 'var(--elah-danger-text, #f5d0a9)' : 'var(--elah-info-text, #c8d8f0)',
|
|
347
347
|
background: toast.tone === 'warn' ? 'var(--elah-danger-bg, #3a2418)' : 'var(--elah-info-bg, #1a2433)',
|
|
348
348
|
border: `1px solid ${toast.tone === 'warn' ? 'var(--elah-danger-border, #7a4a2a)' : 'var(--elah-info-border, #355070)'}`,
|
|
349
|
-
boxShadow: '0 4px 12px rgba(0,0,0,0.35)',
|
|
349
|
+
boxShadow: 'var(--elah-toast-shadow, 0 4px 12px rgba(0, 0, 0, 0.35))',
|
|
350
350
|
}, children: toast.message })), assets.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center min-h-[120px] p-4 text-center text-ed-text-muted text-[11px] border border-dashed border-ed-border rounded-md", children: [_jsx("span", { className: "mb-2 text-2xl opacity-50", children: "\u2193" }), "Drop files here", _jsx("br", {}), "or click Add"] })) : visibleAssets.length === 0 ? (_jsx("div", { className: "text-center text-ed-text-muted text-[11px] pt-8", children: "No matches" })) : (_jsx("div", { className: "flex flex-col gap-[6px]", children: visibleAssets.map((asset) => (_jsx(ClipCard, { asset: asset, viewMode: viewMode, onDelete: handleDeleteAsset, onActivate: activationEnabled ? handleAssetActivate : undefined, slots: cardSlots }, asset.id))) }))] })] })), lane === 'elements' && (_jsxs("div", { className: "flex flex-col flex-1 min-h-0 overflow-auto", children: [_jsx("div", { className: cn('px-[10px] py-[6px] border-b border-ed-border shrink-0', classNames?.toolbar), children: _jsx("input", { type: "search", placeholder: "Search elements\u2026", className: classNames?.searchInput, style: {
|
|
351
351
|
width: '100%',
|
|
352
352
|
padding: '4px 8px',
|
package/dist/editor/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { EditorProvider } from '
|
|
2
|
-
export type { EditorProviderProps } from '
|
|
1
|
+
export { EditorProvider } from '@elah/react';
|
|
2
|
+
export type { EditorProviderProps } from '@elah/react';
|
|
3
3
|
export { AssetPanel } from './AssetPanel';
|
|
4
4
|
export type { AssetPanelProps } from './AssetPanel';
|
|
5
5
|
export { ElementsPanel } from './ElementsPanel';
|
package/dist/editor/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
export type { Clip, Track, Project, Transform, TextAnimation, TextAnimationKind, ClipType, TrackKind, FrameCount, TimelineConfig, InitialTrackConfig, EngineEvent, Transition, TransitionKind, TransitionEasing, TransitionDirection, } from '@elah/core';
|
|
1
|
+
export type { Clip, Track, Project, Transform, TextAnimation, TextAnimationKind, TextAnimationKindOption, TextAnimationEasing, MotionSpec, TextStylePreset, ClipType, TrackKind, FrameCount, TimelineConfig, InitialTrackConfig, EngineEvent, Transition, TransitionKind, TransitionEasing, TransitionDirection, LoadProjectHistory, LoadProjectTransport, ProjectLoadedEvent, } from '@elah/core';
|
|
2
2
|
export { TimelineEngine } from '@elah/core';
|
|
3
|
+
export { BUILT_IN_TEXT_STYLE_PRESETS, BUILT_IN_TEXT_TEMPLATES, applyTextTemplate, findTextTemplate, resolveTemplateRamp, LINE_HEIGHT, } from '@elah/core';
|
|
4
|
+
export { MAX_TEMPLATE_RAMP_FRAMES, MIN_TEMPLATE_RAMP_FRAMES, } from '@elah/core';
|
|
5
|
+
export type { TextTemplate, TextTemplateStyle, TextTemplateAnimation, TextTemplateStagger, } from '@elah/core';
|
|
6
|
+
export { sampleTextAnimation, resolveRampFrames, TEXT_ANIMATION_EASINGS, motionForKind, DEFAULT_SHAPE_TRANSFORM, SLIDE_TRAVEL_NORMALIZED, SPIN_TRAVEL_RADIANS, } from '@elah/core';
|
|
7
|
+
export type { TextAnimationSample, SampleTextAnimationArgs } from '@elah/core';
|
|
8
|
+
export { PROJECT_VERSION, ProjectDocumentError, readProjectDocument, isReadableProjectDocument, isRecoverableMediaSrc, relinkProjectMedia, missingMediaSummary, } from '@elah/core';
|
|
9
|
+
export type { ProjectDocumentErrorCode, ReadProjectDocumentOptions, MediaClipType, MissingMedia, RelinkMediaResult, } from '@elah/core';
|
|
10
|
+
export { serializeProject, deserializeProject } from '@elah/core';
|
|
3
11
|
export { PlaybackEngine } from '@elah/core';
|
|
4
12
|
export type { PlaybackSnapshot, PlaybackEngineConfig } from '@elah/core';
|
|
5
13
|
export { resolveTimeline } from '@elah/core';
|
|
@@ -12,6 +20,8 @@ export type { CounterSnapshot } from '@elah/core';
|
|
|
12
20
|
export { createDefaultDemuxerFactory, createMediabunnyBackend, isMediabunnyCompatible } from '@elah/core';
|
|
13
21
|
export type { MediabunnyModule, CreateMediabunnyBackendOpts } from '@elah/core';
|
|
14
22
|
export type { DemuxerBackend, DemuxerFactory, MediabunnyDemuxer } from '@elah/core';
|
|
23
|
+
export { sourceBlobCache, createSourceBlobCache, warmVideoSrc } from '@elah/core';
|
|
24
|
+
export type { SourceBlobCache, CreateSourceBlobCacheOpts, BlobFetcher } from '@elah/core';
|
|
15
25
|
export type { VideoFrameProvider, VideoFrameProviderDeps } from '@elah/core';
|
|
16
26
|
export { createVideoFrameProvider, MockVideoFrameProvider, SyntheticVideoFrameProvider } from '@elah/core';
|
|
17
27
|
export { AudioPlaybackController } from '@elah/core';
|
|
@@ -20,17 +30,16 @@ export { defaultAudioResolver } from '@elah/core';
|
|
|
20
30
|
export type { AudioResolver } from '@elah/core';
|
|
21
31
|
export { warmImageSrc, preloadProjectImages } from '@elah/core';
|
|
22
32
|
export type { ImageLoader, LoadedImage } from '@elah/core';
|
|
23
|
-
export { MEDIA_DRAG_MIME, mediaDragKindMime, importFiles, importUrl, importBlob } from '@elah/core';
|
|
33
|
+
export { MEDIA_DRAG_MIME, mediaDragKindMime, importFiles, importUrl, importBlob, beginImportUrl, determineAssetHasAudio, probeHasAudio, } from '@elah/core';
|
|
24
34
|
export { useMediaLibrary, useAssets, useMediaLibraryStore } from '@elah/react';
|
|
25
|
-
export type { MediaAsset, MediaKind, DragMediaPayload, ImportFilesOptions, ImportFilesResult, ImportUrlOptions, ImportBlobOptions, SkippedImport, MediaLibraryState, MediaLibraryActions, } from '@elah/core';
|
|
35
|
+
export type { MediaAsset, MediaKind, DragMediaPayload, MediaAssetAnalysis, MediaAssetTopObject, ImportFilesOptions, ImportFilesResult, ImportUrlOptions, ImportBlobOptions, SkippedImport, MediaLibraryState, MediaLibraryActions, } from '@elah/core';
|
|
26
36
|
export type { UseMediaLibraryApi } from '@elah/react';
|
|
27
|
-
export {
|
|
28
|
-
export {
|
|
29
|
-
export { useSelectionStore } from '@elah/react';
|
|
30
|
-
export { useTransitionsStore } from '@elah/react';
|
|
37
|
+
export { scheduleThumbnailById, snapshotMediaLibrary, hydrateMediaLibrary, refreshMissingThumbnails, } from '@elah/core';
|
|
38
|
+
export type { MediaLibrarySnapshotEntry, HydrateMediaLibraryOptions, HydrateMediaLibraryResult, } from '@elah/core';
|
|
39
|
+
export { useTracksStore, usePlaybackStore, useSelectionStore, useTransitionsStore, useTextStylePresetsStore, useClipLoadStore, } from '@elah/react';
|
|
31
40
|
export type { BoundStoreHook } from '@elah/react';
|
|
32
|
-
export { tracksStore, playbackStore, selectionStore, transitionsStore, mediaLibraryStore } from '@elah/core';
|
|
33
|
-
export type { TracksState, TracksActions, PlaybackState, PlaybackActions, SelectionState, SelectionActions, TransitionsState, TransitionsActions, } from '@elah/core';
|
|
41
|
+
export { tracksStore, playbackStore, selectionStore, transitionsStore, mediaLibraryStore, clipLoadStore, textStylePresetsStore, } from '@elah/core';
|
|
42
|
+
export type { TracksState, TracksActions, PlaybackState, PlaybackActions, SelectionState, SelectionActions, TransitionsState, TransitionsActions, ClipLoadState, ClipLoadStoreState, ClipLoadStoreActions, TextStylePresetsState, TextStylePresetsActions, } from '@elah/core';
|
|
34
43
|
export { createVideoClip } from '@elah/core';
|
|
35
44
|
export { createAudioClip } from '@elah/core';
|
|
36
45
|
export { createTextClip } from '@elah/core';
|
|
@@ -38,14 +47,20 @@ export { createImageClip } from '@elah/core';
|
|
|
38
47
|
export { createShapeClip } from '@elah/core';
|
|
39
48
|
export { createFreehandClip } from '@elah/core';
|
|
40
49
|
export type { CreateClipOptions, CreateShapeClipOptions, CreateFreehandClipOptions, ShapeClipMetadata, FreehandClipMetadata, } from '@elah/core';
|
|
50
|
+
export { TEXT_ANIMATION_KINDS, DEFAULT_TEXT_TRANSFORM, } from '@elah/core';
|
|
41
51
|
export { splitClipAtPlayhead } from '@elah/core';
|
|
42
52
|
export type { SplitAtPlayheadData } from '@elah/core';
|
|
43
53
|
export type { ActionResult, ActionFailureReason } from '@elah/core';
|
|
44
54
|
export { framesToTimecode, secondsToFrames, framesToSeconds, getTotalFrames } from '@elah/core';
|
|
45
55
|
export { generateId } from '@elah/core';
|
|
46
|
-
export { transformFromCoverRect } from '@elah/core';
|
|
56
|
+
export { transformFromContainRect, transformFromCoverRect, normalizeCrop, FULL_CROP, } from '@elah/core';
|
|
57
|
+
export type { CropRect } from '@elah/core';
|
|
47
58
|
export { snapFrame, buildSnapPoints, resolveOverlapEdgeSnap, clipsOverlap, DEFAULT_OVERLAP_TOLERANCE } from '@elah/core';
|
|
48
|
-
export {
|
|
59
|
+
export { TIMELINE_MIN_CONTENT_WIDTH, timelineContentWidth, pxToFrames, xToFrame, seekFrameAtX, snapThresholdFrames, ZOOM_MIN, ZOOM_MAX, clampZoom, computeAnchoredScrollLeft, resolveZoomAnchorX, wheelZoomStep, pinchZoom, RULER_TICK_INTERVALS_SEC, formatRulerLabel, computeRulerTicks, isCompatibleTrackKind, isClipAllowedOnTrack, TRIM_HANDLE_WIDTH_PX, isUnlimitedClipType, maxTrimDuration, minTrimDuration, minLeftTrimStart, neighbourBounds, clampLeftTrim, clampRightTrim, } from '@elah/core';
|
|
60
|
+
export type { RulerTick, TrimResult, TrimLimits } from '@elah/core';
|
|
61
|
+
export { createFrameSequence, frameAt, frameCount, normalizeFrameIndex, FrameSequenceController, createFramePreloader, pickFrameSource, supportsImageType, frameSequenceToProject, } from '@elah/core';
|
|
62
|
+
export type { Frame, FrameSource, FrameSequence, FrameLoopMode, CreateFrameSequenceOptions, FrameSequenceSnapshot, FrameSequenceControllerOptions, FramePreloader, FramePreloaderOptions, FramePreloaderStatus, FrameSourceSizeHint, FrameSequenceToProjectOptions, } from '@elah/core';
|
|
63
|
+
export { PerfSummary } from '@elah/core';
|
|
49
64
|
export { exportVideo } from '@elah/core';
|
|
50
65
|
export { lazyExportVideo } from '@elah/core';
|
|
51
66
|
export type { ExportOptions, ExportProgress, ExportVideoCodec, ExportAudioCodec } from '@elah/core';
|
|
@@ -58,11 +73,12 @@ export { usePlayback } from '@elah/timeline';
|
|
|
58
73
|
export { useSelection } from '@elah/timeline';
|
|
59
74
|
export { useTimelineDrop } from '@elah/timeline';
|
|
60
75
|
export type { TimelineDropState } from '@elah/timeline';
|
|
61
|
-
export { insertMediaAsset, insertElement } from '@elah/timeline';
|
|
76
|
+
export { insertMediaAsset, insertElement, growClipToAssetDuration } from '@elah/timeline';
|
|
62
77
|
export { ELEMENT_DRAG_MIME } from '@elah/timeline';
|
|
78
|
+
export { useKeySelectionStore } from '@elah/timeline';
|
|
63
79
|
export type { DragElementPayload, ElementKind, ShapeVariant, InsertAssetOptions, InsertAssetResult, InsertAssetFailureReason, InsertedKind, } from '@elah/timeline';
|
|
64
|
-
export { EditorProvider } from '
|
|
65
|
-
export type { EditorProviderProps } from '
|
|
80
|
+
export { EditorProvider } from '@elah/react';
|
|
81
|
+
export type { EditorProviderProps } from '@elah/react';
|
|
66
82
|
export { EditorContext, useEditor, useTimelineEngine, usePlaybackEngine } from '@elah/react';
|
|
67
83
|
export type { EditorContextValue } from '@elah/react';
|
|
68
84
|
export { useAudioMixer, useMasterVolume, useTrackLevels } from '@elah/react';
|