@elah/editor 0.2.1 → 0.3.1
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/LICENSE +201 -0
- package/README.md +160 -127
- package/dist/editor/AssetPanel/AssetPanel.d.ts +4 -1
- package/dist/editor/AssetPanel/AssetPanel.js +109 -138
- package/dist/editor/ElementsPanel/ElementsPanel.d.ts +4 -1
- package/dist/editor/ElementsPanel/ElementsPanel.js +87 -46
- package/dist/editor/Preview/MediaTransformOverlay.js +4 -4
- package/dist/editor/Preview/Preview.d.ts +2 -0
- package/dist/editor/Preview/Preview.js +38 -12
- package/dist/editor/Preview/ShapeOverlay.d.ts +1 -0
- package/dist/editor/Preview/ShapeOverlay.js +178 -0
- package/dist/editor/Preview/StageBorder.js +3 -3
- package/dist/editor/Preview/TextOverlay.js +5 -5
- package/dist/editor/Preview/TransitionOverlay.js +1 -5
- package/dist/editor/SourcePanel/SourcePanel.d.ts +37 -0
- package/dist/editor/SourcePanel/SourcePanel.js +381 -0
- package/dist/editor/SourcePanel/index.d.ts +2 -0
- package/dist/editor/SourcePanel/index.js +1 -0
- package/dist/editor/activation.d.ts +26 -0
- package/dist/editor/activation.js +53 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +5 -1
- package/dist/styles.css +1 -0
- package/package.json +75 -69
- package/src/styles/index.css +3 -0
- package/src/styles/tokens.css +204 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { useTimelineEngine, useSelectionStore, usePlaybackStore, computeContainViewport, } from '@elah/core';
|
|
4
|
+
import { useResolvedScene } from '../useResolvedScene';
|
|
5
|
+
const DEFAULT_SCALE = 0.5;
|
|
6
|
+
const DEFAULT_CENTER = 0.5;
|
|
7
|
+
const MIN_BOX_PX = 28;
|
|
8
|
+
const MIN_SCALE = 0.05;
|
|
9
|
+
const MAX_SCALE = 4;
|
|
10
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
11
|
+
const clamp01 = (v) => clamp(v, 0, 1);
|
|
12
|
+
function baseTransform(t) {
|
|
13
|
+
return {
|
|
14
|
+
x: t?.x ?? DEFAULT_CENTER,
|
|
15
|
+
y: t?.y ?? DEFAULT_CENTER,
|
|
16
|
+
scale: t?.scale ?? DEFAULT_SCALE,
|
|
17
|
+
rotation: t?.rotation ?? 0,
|
|
18
|
+
anchor: t?.anchor ?? { x: 0.5, y: 0.5 },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function ShapeOverlay() {
|
|
22
|
+
const engine = useTimelineEngine();
|
|
23
|
+
const scene = useResolvedScene();
|
|
24
|
+
const isPlaying = usePlaybackStore((s) => s.isPlaying);
|
|
25
|
+
const selectedClipIds = useSelectionStore((s) => s.selectedClipIds);
|
|
26
|
+
const selectClip = useSelectionStore((s) => s.selectClip);
|
|
27
|
+
const clearSelection = useSelectionStore((s) => s.clearSelection);
|
|
28
|
+
const rootRef = useRef(null);
|
|
29
|
+
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
30
|
+
const gestureRef = useRef(null);
|
|
31
|
+
useLayoutEffect(() => {
|
|
32
|
+
const el = rootRef.current;
|
|
33
|
+
if (!el)
|
|
34
|
+
return;
|
|
35
|
+
const apply = () => setSize({ width: el.clientWidth, height: el.clientHeight });
|
|
36
|
+
apply();
|
|
37
|
+
const obs = new ResizeObserver(apply);
|
|
38
|
+
obs.observe(el);
|
|
39
|
+
return () => obs.disconnect();
|
|
40
|
+
}, []);
|
|
41
|
+
const stage = scene.stage;
|
|
42
|
+
const fit = useMemo(() => computeContainViewport(size.width, size.height, stage.width, stage.height), [size.width, size.height, stage.width, stage.height]);
|
|
43
|
+
const scale = stage.width > 0 ? fit.width / stage.width : 1;
|
|
44
|
+
const items = useMemo(() => {
|
|
45
|
+
if (fit.width <= 0)
|
|
46
|
+
return [];
|
|
47
|
+
const shortSide = Math.min(stage.width, stage.height);
|
|
48
|
+
return scene.shapes.map((clip) => {
|
|
49
|
+
const t = baseTransform(clip.transform);
|
|
50
|
+
const sideStage = t.scale * shortSide;
|
|
51
|
+
const cxStage = t.x * stage.width;
|
|
52
|
+
const cyStage = t.y * stage.height;
|
|
53
|
+
const centerScreenX = fit.x + cxStage * scale;
|
|
54
|
+
const centerScreenY = fit.y + cyStage * scale;
|
|
55
|
+
const rawSide = sideStage * scale;
|
|
56
|
+
const w = Math.max(rawSide, MIN_BOX_PX);
|
|
57
|
+
const h = Math.max(rawSide, MIN_BOX_PX);
|
|
58
|
+
const left = centerScreenX - w / 2;
|
|
59
|
+
const top = centerScreenY - h / 2;
|
|
60
|
+
return {
|
|
61
|
+
clip,
|
|
62
|
+
rect: { left, top, width: w, height: h },
|
|
63
|
+
centerScreenX,
|
|
64
|
+
centerScreenY,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}, [scene.shapes, stage.width, stage.height, fit, scale]);
|
|
68
|
+
const ownsSelection = useMemo(() => items.some((it) => selectedClipIds.has(it.clip.id)), [items, selectedClipIds]);
|
|
69
|
+
const handlePointerMove = useCallback((e) => {
|
|
70
|
+
const g = gestureRef.current;
|
|
71
|
+
if (!g)
|
|
72
|
+
return;
|
|
73
|
+
if (g.type === 'move') {
|
|
74
|
+
const dx = (e.clientX - g.startClientX) / scale;
|
|
75
|
+
const dy = (e.clientY - g.startClientY) / scale;
|
|
76
|
+
const x = clamp01(g.base.x + dx / g.stageW);
|
|
77
|
+
const y = clamp01(g.base.y + dy / g.stageH);
|
|
78
|
+
engine.previewClip(g.id, g.trackId, { transform: { ...g.base, x, y } });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const dist = Math.hypot(e.clientX - g.centerClientX, e.clientY - g.centerClientY);
|
|
82
|
+
const ratio = g.startDist > 0 ? dist / g.startDist : 1;
|
|
83
|
+
const next = clamp(g.base.scale * ratio, MIN_SCALE, MAX_SCALE);
|
|
84
|
+
engine.previewClip(g.id, g.trackId, { transform: { ...g.base, scale: next } });
|
|
85
|
+
}
|
|
86
|
+
}, [engine, scale]);
|
|
87
|
+
const endGesture = useCallback((e) => {
|
|
88
|
+
const g = gestureRef.current;
|
|
89
|
+
if (!g)
|
|
90
|
+
return;
|
|
91
|
+
gestureRef.current = null;
|
|
92
|
+
const target = e.currentTarget;
|
|
93
|
+
if (target.hasPointerCapture?.(e.pointerId)) {
|
|
94
|
+
target.releasePointerCapture(e.pointerId);
|
|
95
|
+
}
|
|
96
|
+
engine.commitInteraction(g.type === 'move' ? 'Move shape' : 'Resize shape');
|
|
97
|
+
}, [engine]);
|
|
98
|
+
const beginMove = useCallback((e, item) => {
|
|
99
|
+
e.stopPropagation();
|
|
100
|
+
if (!selectedClipIds.has(item.clip.id))
|
|
101
|
+
selectClip(item.clip.id);
|
|
102
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
103
|
+
gestureRef.current = {
|
|
104
|
+
type: 'move',
|
|
105
|
+
id: item.clip.id,
|
|
106
|
+
trackId: item.clip.trackId,
|
|
107
|
+
base: baseTransform(item.clip.transform),
|
|
108
|
+
stageW: stage.width,
|
|
109
|
+
stageH: stage.height,
|
|
110
|
+
startClientX: e.clientX,
|
|
111
|
+
startClientY: e.clientY,
|
|
112
|
+
centerClientX: 0,
|
|
113
|
+
centerClientY: 0,
|
|
114
|
+
startDist: 0,
|
|
115
|
+
};
|
|
116
|
+
}, [selectedClipIds, selectClip, stage.width, stage.height]);
|
|
117
|
+
const beginResize = useCallback((e, item) => {
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
if (!selectedClipIds.has(item.clip.id))
|
|
120
|
+
selectClip(item.clip.id);
|
|
121
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
122
|
+
const rootRect = rootRef.current?.getBoundingClientRect();
|
|
123
|
+
const centerClientX = (rootRect?.left ?? 0) + item.centerScreenX;
|
|
124
|
+
const centerClientY = (rootRect?.top ?? 0) + item.centerScreenY;
|
|
125
|
+
gestureRef.current = {
|
|
126
|
+
type: 'resize',
|
|
127
|
+
id: item.clip.id,
|
|
128
|
+
trackId: item.clip.trackId,
|
|
129
|
+
base: baseTransform(item.clip.transform),
|
|
130
|
+
stageW: stage.width,
|
|
131
|
+
stageH: stage.height,
|
|
132
|
+
startClientX: e.clientX,
|
|
133
|
+
startClientY: e.clientY,
|
|
134
|
+
centerClientX,
|
|
135
|
+
centerClientY,
|
|
136
|
+
startDist: Math.hypot(e.clientX - centerClientX, e.clientY - centerClientY),
|
|
137
|
+
};
|
|
138
|
+
}, [selectedClipIds, selectClip, stage.width, stage.height]);
|
|
139
|
+
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 &&
|
|
140
|
+
items.map((item) => {
|
|
141
|
+
const { clip, rect } = item;
|
|
142
|
+
const selected = selectedClipIds.has(clip.id);
|
|
143
|
+
const boxStyle = {
|
|
144
|
+
position: 'absolute',
|
|
145
|
+
left: rect.left,
|
|
146
|
+
top: rect.top,
|
|
147
|
+
width: rect.width,
|
|
148
|
+
height: rect.height,
|
|
149
|
+
boxSizing: 'border-box',
|
|
150
|
+
border: selected
|
|
151
|
+
? '1px solid var(--elah-selection-color, #4c9aff)'
|
|
152
|
+
: '1px solid transparent',
|
|
153
|
+
borderRadius: 2,
|
|
154
|
+
cursor: 'move',
|
|
155
|
+
pointerEvents: 'auto',
|
|
156
|
+
touchAction: 'none',
|
|
157
|
+
};
|
|
158
|
+
return (_jsx("div", { style: boxStyle, onPointerDown: (e) => beginMove(e, item), onPointerMove: handlePointerMove, onPointerUp: endGesture, onPointerCancel: endGesture, children: selected &&
|
|
159
|
+
CORNERS.map((corner) => (_jsx("div", { onPointerDown: (e) => beginResize(e, item), onPointerMove: handlePointerMove, onPointerUp: endGesture, onPointerCancel: endGesture, style: {
|
|
160
|
+
position: 'absolute',
|
|
161
|
+
...corner.pos,
|
|
162
|
+
width: 10,
|
|
163
|
+
height: 10,
|
|
164
|
+
background: 'var(--elah-selection-handle, #fff)',
|
|
165
|
+
border: '1px solid var(--elah-selection-color, #4c9aff)',
|
|
166
|
+
borderRadius: 2,
|
|
167
|
+
cursor: corner.cursor,
|
|
168
|
+
pointerEvents: 'auto',
|
|
169
|
+
touchAction: 'none',
|
|
170
|
+
} }, corner.key))) }, clip.id));
|
|
171
|
+
})] }));
|
|
172
|
+
}
|
|
173
|
+
const CORNERS = [
|
|
174
|
+
{ key: 'nw', pos: { left: -5, top: -5 }, cursor: 'nwse-resize' },
|
|
175
|
+
{ key: 'ne', pos: { right: -5, top: -5 }, cursor: 'nesw-resize' },
|
|
176
|
+
{ key: 'sw', pos: { left: -5, bottom: -5 }, cursor: 'nesw-resize' },
|
|
177
|
+
{ key: 'se', pos: { right: -5, bottom: -5 }, cursor: 'nwse-resize' },
|
|
178
|
+
];
|
|
@@ -16,14 +16,14 @@ export function StageBorder() {
|
|
|
16
16
|
return () => obs.disconnect();
|
|
17
17
|
}, []);
|
|
18
18
|
const fit = useMemo(() => computeContainViewport(size.width, size.height, stage.width, stage.height), [size.width, size.height, stage.width, stage.height]);
|
|
19
|
-
return (_jsx("div", { ref: rootRef,
|
|
19
|
+
return (_jsx("div", { ref: rootRef, className: "absolute inset-0 z-[1] pointer-events-none overflow-hidden", children: fit.width > 0 && fit.height > 0 && (_jsx("div", { style: {
|
|
20
20
|
position: 'absolute',
|
|
21
21
|
left: fit.x,
|
|
22
22
|
top: fit.y,
|
|
23
23
|
width: fit.width,
|
|
24
24
|
height: fit.height,
|
|
25
|
-
border: '1px solid rgba(225, 29, 72, 0.45)',
|
|
26
|
-
boxShadow: '0 0 20px rgba(225, 29, 72, 0.08)',
|
|
25
|
+
border: '1px solid var(--elah-stage-border, rgba(225, 29, 72, 0.45))',
|
|
26
|
+
boxShadow: 'var(--elah-stage-glow, 0 0 20px rgba(225, 29, 72, 0.08))',
|
|
27
27
|
boxSizing: 'border-box',
|
|
28
28
|
} })) }));
|
|
29
29
|
}
|
|
@@ -163,7 +163,7 @@ export function TextOverlay() {
|
|
|
163
163
|
commitEditing();
|
|
164
164
|
}, [isPlaying]);
|
|
165
165
|
const editingItem = editingId ? items.find((it) => it.clip.id === editingId) : undefined;
|
|
166
|
-
return (_jsxs("div", { ref: rootRef,
|
|
166
|
+
return (_jsxs("div", { ref: rootRef, className: "absolute inset-0 z-[4] pointer-events-none overflow-hidden", children: [!isPlaying && (ownsSelection || editingId) && (_jsx("div", { className: "absolute inset-0 pointer-events-auto", onPointerDown: () => {
|
|
167
167
|
if (editingId)
|
|
168
168
|
commitEditing();
|
|
169
169
|
clearSelection();
|
|
@@ -179,7 +179,7 @@ export function TextOverlay() {
|
|
|
179
179
|
width: rect.width + BOX_PAD * 2,
|
|
180
180
|
height: rect.height + BOX_PAD * 2,
|
|
181
181
|
boxSizing: 'border-box',
|
|
182
|
-
border: selected ? '1px solid #4c9aff' : '1px solid transparent',
|
|
182
|
+
border: selected ? '1px solid var(--elah-selection-color, #4c9aff)' : '1px solid transparent',
|
|
183
183
|
borderRadius: 2,
|
|
184
184
|
cursor: isEditing ? 'text' : 'move',
|
|
185
185
|
pointerEvents: isEditing ? 'none' : 'auto',
|
|
@@ -193,8 +193,8 @@ export function TextOverlay() {
|
|
|
193
193
|
...corner.pos,
|
|
194
194
|
width: 10,
|
|
195
195
|
height: 10,
|
|
196
|
-
background: '#fff',
|
|
197
|
-
border: '1px solid #4c9aff',
|
|
196
|
+
background: 'var(--elah-selection-handle, #fff)',
|
|
197
|
+
border: '1px solid var(--elah-selection-color, #4c9aff)',
|
|
198
198
|
borderRadius: 2,
|
|
199
199
|
cursor: corner.cursor,
|
|
200
200
|
pointerEvents: 'auto',
|
|
@@ -227,7 +227,7 @@ export function TextOverlay() {
|
|
|
227
227
|
font: `${editingItem.layout.style.fontWeight} ${editingItem.layout.style.fontSize * scale}px ${editingItem.layout.style.fontFamily}`,
|
|
228
228
|
lineHeight: `${editingItem.layout.lineAdvance * scale}px`,
|
|
229
229
|
textAlign: editingItem.layout.style.textAlign,
|
|
230
|
-
border: '1px solid #4c9aff',
|
|
230
|
+
border: '1px solid var(--elah-selection-color, #4c9aff)',
|
|
231
231
|
outline: 'none',
|
|
232
232
|
resize: 'none',
|
|
233
233
|
padding: 0,
|
|
@@ -57,11 +57,7 @@ export const TransitionOverlay = forwardRef(function TransitionOverlay(_, ref) {
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
}), []);
|
|
60
|
-
return (_jsx("div", { ref: rootRef, style: {
|
|
61
|
-
position: 'absolute',
|
|
62
|
-
inset: 0,
|
|
60
|
+
return (_jsx("div", { ref: rootRef, className: "absolute inset-0 pointer-events-none overflow-hidden", style: {
|
|
63
61
|
zIndex: 1,
|
|
64
|
-
pointerEvents: 'none',
|
|
65
|
-
overflow: 'hidden',
|
|
66
62
|
} }));
|
|
67
63
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
import { type AssetActivationHandler } from '../activation';
|
|
3
|
+
export interface SourcePanelClassNames {
|
|
4
|
+
root?: string;
|
|
5
|
+
tabBar?: string;
|
|
6
|
+
tab?: string;
|
|
7
|
+
tabActive?: string;
|
|
8
|
+
toolbar?: string;
|
|
9
|
+
ingestButton?: string;
|
|
10
|
+
searchInput?: string;
|
|
11
|
+
sortChip?: string;
|
|
12
|
+
sortChipActive?: string;
|
|
13
|
+
card?: string;
|
|
14
|
+
cardTitle?: string;
|
|
15
|
+
cardMeta?: string;
|
|
16
|
+
thumb?: string;
|
|
17
|
+
badge?: string;
|
|
18
|
+
badgeVideo?: string;
|
|
19
|
+
badgeAudio?: string;
|
|
20
|
+
badgeImage?: string;
|
|
21
|
+
tile?: string;
|
|
22
|
+
tileLabel?: string;
|
|
23
|
+
dropzone?: string;
|
|
24
|
+
toast?: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface SourcePanelProps {
|
|
28
|
+
style?: CSSProperties;
|
|
29
|
+
className?: string;
|
|
30
|
+
defaultLane?: Lane;
|
|
31
|
+
classNames?: SourcePanelClassNames;
|
|
32
|
+
activateOnTap?: boolean;
|
|
33
|
+
onAssetActivate?: AssetActivationHandler;
|
|
34
|
+
}
|
|
35
|
+
type Lane = 'media' | 'elements';
|
|
36
|
+
export declare function SourcePanel({ style, className, defaultLane, classNames, activateOnTap, onAssetActivate, }: SourcePanelProps): import("react/jsx-runtime").JSX.Element;
|
|
37
|
+
export {};
|