@elah/editor 0.6.0 → 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 +4 -3
- package/dist/editor/Preview/Preview.js +1 -7
- package/dist/editor/index.d.ts +2 -2
- package/dist/editor/index.js +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/package.json +4 -4
- package/dist/editor/EditorProvider.d.ts +0 -14
- package/dist/editor/EditorProvider.js +0 -95
package/README.md
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
The full Elah video editor SDK for React. Combines the core engine, timeline UI, WebGL2 renderer, media library, and export pipeline into a single package.
|
|
4
4
|
|
|
5
|
-
Ships `EditorProvider`, `Preview` (WebGL2 canvas + interactive transform overlays), `Timeline`, `AssetPanel`, `SourcePanel`, and `ElementsPanel`, and re-exports the public `@elah/core`, `@elah/react`, and `@elah/timeline` API — so most apps only ever import from `@elah/editor`. (Renderer and debug internals are the exception; import those from `@elah/core` directly.) Supports video, image, text, **shape**, and **freehand** clips, **multiple video tracks**, **multi-track audio**, and MP4 export.
|
|
5
|
+
Ships `EditorProvider`, `Preview` (WebGL2 canvas + interactive transform overlays), `Timeline`, `AssetPanel`, `SourcePanel`, and `ElementsPanel`, and re-exports the public `@elah/core`, `@elah/react`, and `@elah/timeline` API — so most apps only ever import from `@elah/editor`. (Renderer and debug internals, and the 1.0.0 programmatic layer, are the exception; import those from `@elah/core` directly.) Supports video, image, text, **shape**, and **freehand** clips, **multiple video tracks**, **multi-track audio**, and export to MP4, WebM, GIF or a PNG sequence. Projects that use the newer **adjustment**, **generator**, **compound** and **sequence** clips, and the nine transitions, open, play, save and export; `@elah/editor` has no editing UI for those clip types yet. A selected clip does show its **keyframes** as diamonds on its bottom edge: click to seek and select, drag to retime with snapping as one undo step, Shift+click for multi-select (`<KeyframeLane>` from `@elah/timeline`, drawn automatically; `useKeySelectionStore` is re-exported here).
|
|
6
|
+
|
|
7
|
+
**1.0.0:** the exports of this package follow semver. The 1.0.0 programmatic layer (effects, generators, motion helpers, VideoSpec, HTML scenes, editor ops, `packageFontsBaseUrl`) is **not** re-exported here: import it from `@elah/core`, which this package already depends on (list it in your own `package.json`). VideoSpec, HTML scenes and the op vocabulary are live in 1.0 and sit on their own entries, not the `@elah/core` root: `@elah/core/spec`, `@elah/core/html`, `@elah/core/ops` and `@elah/core/icons`; see [the changelog](../../CHANGELOG.md).
|
|
6
8
|
|
|
7
9
|
[](https://www.npmjs.com/package/@elah/editor)
|
|
8
|
-
[](../../BUNDLE_STRATEGY.md)
|
|
9
10
|
[](https://github.com/elahlabs/elah/blob/main/LICENSE)
|
|
10
11
|
|
|
11
12
|
---
|
|
@@ -20,7 +21,7 @@ Peer dependencies: `react`, `react-dom` >= 18, `lucide-react` >= 0.400.0 (used b
|
|
|
20
21
|
bundled `@elah/timeline` UI for clip icons — install it alongside `react`/`react-dom`
|
|
21
22
|
even though nothing in your own code imports it).
|
|
22
23
|
|
|
23
|
-
**Bundle size:**
|
|
24
|
+
**Bundle size:** an editor app (`EditorProvider`, `Preview`, `Timeline`) is about **112 KiB gz** — esbuild, minified, gzipped, with `react`, `lucide-react` and `mediabunny` external — up from 68 KiB in 0.6.0, because effects, generators, sequence clips, roto masks and audio Enhance became chunks that load on first use (`prepareFeatures(project)` loads a project's features up front). [BUNDLE_STRATEGY.md](../../BUNDLE_STRATEGY.md) explains how the figures are measured (`node scripts/measure-bundle.mjs`). `@elah/core` grew substantially in 1.0.0 (it now carries the motion layer, effects and the model-facing layers); import `@elah/core/engine` directly if you need only the engine. `createDefaultDemuxerFactory()` imports `mediabunny` statically, so it pulls the codec into the startup graph; every other path loads it lazily.
|
|
24
25
|
|
|
25
26
|
---
|
|
26
27
|
|
|
@@ -10,12 +10,10 @@ import { TextOverlay } from './TextOverlay';
|
|
|
10
10
|
import { ShapeOverlay } from './ShapeOverlay';
|
|
11
11
|
import { MediaTransformOverlay } from './MediaTransformOverlay';
|
|
12
12
|
import { PreviewLoadingOverlay } from './PreviewLoadingOverlay';
|
|
13
|
-
import { TransitionOverlay } from './TransitionOverlay';
|
|
14
13
|
import { StageBorder } from './StageBorder';
|
|
15
14
|
export const Preview = forwardRef(function Preview({ demuxerFactory, debug = false, probeLayer = false, clearColor, preserveDrawingBuffer, enableAudio = true, audioResolver, style, className, }, ref) {
|
|
16
15
|
const containerRef = useRef(null);
|
|
17
16
|
const rendererRef = useRef(null);
|
|
18
|
-
const transitionOverlayRef = useRef(null);
|
|
19
17
|
const engine = useTimelineEngine();
|
|
20
18
|
const playback = usePlaybackEngine();
|
|
21
19
|
const [glState, setGlState] = useState('ok');
|
|
@@ -136,11 +134,7 @@ export const Preview = forwardRef(function Preview({ demuxerFactory, debug = fal
|
|
|
136
134
|
const frame = Math.floor(playback.getFrameAt());
|
|
137
135
|
const project = engine.getProject();
|
|
138
136
|
const scene = perf.measure('resolve', () => resolveTimeline(frame, project));
|
|
139
|
-
const canvas = renderer.getCanvas();
|
|
140
|
-
if (canvas)
|
|
141
|
-
transitionOverlayRef.current?.captureIfNewTransition(scene, canvas);
|
|
142
137
|
perf.measure('render', () => renderer.render(scene));
|
|
143
|
-
transitionOverlayRef.current?.update(scene);
|
|
144
138
|
lastPrewarmAt = now;
|
|
145
139
|
perf.measure('prewarm', () => {
|
|
146
140
|
const prewarmScene = resolveTimeline(frame + PREWARM_HORIZON_FRAMES, project);
|
|
@@ -172,7 +166,7 @@ export const Preview = forwardRef(function Preview({ demuxerFactory, debug = fal
|
|
|
172
166
|
clipLoadStore.getState().clear();
|
|
173
167
|
};
|
|
174
168
|
}, [engine, playback, demuxerFactory, debug, probeLayer, preserveDrawingBuffer, enableAudio, audioResolver, reloadKey]);
|
|
175
|
-
return (_jsxs("div", { ref: containerRef, className: cn('relative w-full h-full bg-[var(--elah-preview-bg,#06070A)]', className), style: style, children: [_jsx(StageBorder, {}), _jsx(
|
|
169
|
+
return (_jsxs("div", { ref: containerRef, className: cn('relative w-full h-full bg-[var(--elah-preview-bg,#06070A)]', className), style: style, children: [_jsx(StageBorder, {}), _jsx(MediaTransformOverlay, {}), _jsx(ShapeOverlay, {}), _jsx(TextOverlay, {}), _jsx(PreviewLoadingOverlay, {}), glState !== 'ok' && (_jsx("div", { className: "absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-[var(--elah-overlay-scrim,rgba(0,0,0,0.7))] text-[color:var(--elah-overlay-text,#ffffff)] text-sm", "data-testid": "preview-gl-recovery", children: glState === 'lost' ? (_jsx("span", { children: "Recovering preview\u2026" })) : (_jsxs(_Fragment, { children: [_jsx("span", { children: "The preview stopped rendering (graphics context lost)." }), _jsx("button", { type: "button", className: "rounded-md border border-[color:var(--elah-overlay-border,rgba(255,255,255,0.3))] bg-[var(--elah-overlay-bg,rgba(255,255,255,0.1))] px-3 py-1.5 hover:bg-[var(--elah-overlay-bg-hover,rgba(255,255,255,0.2))]", onClick: () => {
|
|
176
170
|
setGlState('ok');
|
|
177
171
|
setReloadKey((k) => k + 1);
|
|
178
172
|
}, children: "Reload preview" })] })) }))] }));
|
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
|
@@ -56,6 +56,8 @@ export { generateId } from '@elah/core';
|
|
|
56
56
|
export { transformFromContainRect, transformFromCoverRect, normalizeCrop, FULL_CROP, } from '@elah/core';
|
|
57
57
|
export type { CropRect } from '@elah/core';
|
|
58
58
|
export { snapFrame, buildSnapPoints, resolveOverlapEdgeSnap, clipsOverlap, DEFAULT_OVERLAP_TOLERANCE } from '@elah/core';
|
|
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';
|
|
59
61
|
export { createFrameSequence, frameAt, frameCount, normalizeFrameIndex, FrameSequenceController, createFramePreloader, pickFrameSource, supportsImageType, frameSequenceToProject, } from '@elah/core';
|
|
60
62
|
export type { Frame, FrameSource, FrameSequence, FrameLoopMode, CreateFrameSequenceOptions, FrameSequenceSnapshot, FrameSequenceControllerOptions, FramePreloader, FramePreloaderOptions, FramePreloaderStatus, FrameSourceSizeHint, FrameSequenceToProjectOptions, } from '@elah/core';
|
|
61
63
|
export { PerfSummary } from '@elah/core';
|
|
@@ -73,9 +75,10 @@ export { useTimelineDrop } from '@elah/timeline';
|
|
|
73
75
|
export type { TimelineDropState } from '@elah/timeline';
|
|
74
76
|
export { insertMediaAsset, insertElement, growClipToAssetDuration } from '@elah/timeline';
|
|
75
77
|
export { ELEMENT_DRAG_MIME } from '@elah/timeline';
|
|
78
|
+
export { useKeySelectionStore } from '@elah/timeline';
|
|
76
79
|
export type { DragElementPayload, ElementKind, ShapeVariant, InsertAssetOptions, InsertAssetResult, InsertAssetFailureReason, InsertedKind, } from '@elah/timeline';
|
|
77
|
-
export { EditorProvider } from '
|
|
78
|
-
export type { EditorProviderProps } from '
|
|
80
|
+
export { EditorProvider } from '@elah/react';
|
|
81
|
+
export type { EditorProviderProps } from '@elah/react';
|
|
79
82
|
export { EditorContext, useEditor, useTimelineEngine, usePlaybackEngine } from '@elah/react';
|
|
80
83
|
export type { EditorContextValue } from '@elah/react';
|
|
81
84
|
export { useAudioMixer, useMasterVolume, useTrackLevels } from '@elah/react';
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ export { framesToTimecode, secondsToFrames, framesToSeconds, getTotalFrames } fr
|
|
|
31
31
|
export { generateId } from '@elah/core';
|
|
32
32
|
export { transformFromContainRect, transformFromCoverRect, normalizeCrop, FULL_CROP, } from '@elah/core';
|
|
33
33
|
export { snapFrame, buildSnapPoints, resolveOverlapEdgeSnap, clipsOverlap, DEFAULT_OVERLAP_TOLERANCE } from '@elah/core';
|
|
34
|
+
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';
|
|
34
35
|
export { createFrameSequence, frameAt, frameCount, normalizeFrameIndex, FrameSequenceController, createFramePreloader, pickFrameSource, supportsImageType, frameSequenceToProject, } from '@elah/core';
|
|
35
36
|
export { PerfSummary } from '@elah/core';
|
|
36
37
|
export { exportVideo } from '@elah/core';
|
|
@@ -44,7 +45,8 @@ export { useSelection } from '@elah/timeline';
|
|
|
44
45
|
export { useTimelineDrop } from '@elah/timeline';
|
|
45
46
|
export { insertMediaAsset, insertElement, growClipToAssetDuration } from '@elah/timeline';
|
|
46
47
|
export { ELEMENT_DRAG_MIME } from '@elah/timeline';
|
|
47
|
-
export {
|
|
48
|
+
export { useKeySelectionStore } from '@elah/timeline';
|
|
49
|
+
export { EditorProvider } from '@elah/react';
|
|
48
50
|
export { EditorContext, useEditor, useTimelineEngine, usePlaybackEngine } from '@elah/react';
|
|
49
51
|
export { useAudioMixer, useMasterVolume, useTrackLevels } from '@elah/react';
|
|
50
52
|
export { useResolvedScene } from './editor/useResolvedScene';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elah/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Full React video editor SDK — timeline UI, WebGL2 preview, media library, drag and drop, and MP4 export",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"lucide-react": ">=0.400.0"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@elah/core": "^0.
|
|
63
|
-
"@elah/react": "^0.
|
|
64
|
-
"@elah/timeline": "^0.
|
|
62
|
+
"@elah/core": "^1.0.0",
|
|
63
|
+
"@elah/react": "^1.0.0",
|
|
64
|
+
"@elah/timeline": "^1.0.0",
|
|
65
65
|
"immer": "^10.1.1",
|
|
66
66
|
"zustand": "^5.0.3"
|
|
67
67
|
},
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
import type { InitialTrackConfig } from '@elah/core';
|
|
3
|
-
export interface EditorProviderProps {
|
|
4
|
-
fps: number;
|
|
5
|
-
stage?: {
|
|
6
|
-
width: number;
|
|
7
|
-
height: number;
|
|
8
|
-
};
|
|
9
|
-
defaultTrackHeight?: number;
|
|
10
|
-
maxHistorySize?: number;
|
|
11
|
-
initialTracks?: InitialTrackConfig[];
|
|
12
|
-
children: ReactNode;
|
|
13
|
-
}
|
|
14
|
-
export declare function EditorProvider({ fps, stage, defaultTrackHeight, maxHistorySize, initialTracks, children, }: EditorProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo } from 'react';
|
|
3
|
-
import { TimelineEngine, PlaybackEngine, installTraceGlobal, trace, } from '@elah/core';
|
|
4
|
-
import { useTracksStore, usePlaybackStore, useTransitionsStore, EditorContext, } from '@elah/react';
|
|
5
|
-
export function EditorProvider({ fps, stage, defaultTrackHeight, maxHistorySize, initialTracks, children, }) {
|
|
6
|
-
const { engine, playback } = useMemo(() => {
|
|
7
|
-
const engine = new TimelineEngine({
|
|
8
|
-
fps,
|
|
9
|
-
stage,
|
|
10
|
-
defaultTrackHeight,
|
|
11
|
-
maxHistorySize,
|
|
12
|
-
initialTracks,
|
|
13
|
-
});
|
|
14
|
-
const playback = new PlaybackEngine({
|
|
15
|
-
fps,
|
|
16
|
-
getTotalFrames: () => useTracksStore.getState().totalFrames,
|
|
17
|
-
});
|
|
18
|
-
engine.on('project:loaded', ({ transport }) => {
|
|
19
|
-
if (transport !== 'rewind')
|
|
20
|
-
return;
|
|
21
|
-
playback.pause();
|
|
22
|
-
playback.seek(0);
|
|
23
|
-
const pb = usePlaybackStore.getState();
|
|
24
|
-
if (pb.isPlaying)
|
|
25
|
-
pb.pause();
|
|
26
|
-
if (pb.currentFrame !== 0)
|
|
27
|
-
pb.setCurrentFrame(0);
|
|
28
|
-
});
|
|
29
|
-
return { engine, playback };
|
|
30
|
-
}, []);
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
const syncAll = () => {
|
|
33
|
-
const project = engine.getProject();
|
|
34
|
-
useTracksStore.getState().sync(project, {
|
|
35
|
-
canUndo: engine.canUndo(),
|
|
36
|
-
canRedo: engine.canRedo(),
|
|
37
|
-
});
|
|
38
|
-
useTransitionsStore.getState().sync(project);
|
|
39
|
-
};
|
|
40
|
-
engine.on('change', syncAll);
|
|
41
|
-
engine.on('history:change', syncAll);
|
|
42
|
-
syncAll();
|
|
43
|
-
return () => {
|
|
44
|
-
engine.off('change', syncAll);
|
|
45
|
-
engine.off('history:change', syncAll);
|
|
46
|
-
};
|
|
47
|
-
}, [engine]);
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
return playback.subscribe((snapshot) => {
|
|
50
|
-
const pb = usePlaybackStore.getState();
|
|
51
|
-
if (snapshot.currentFrame !== pb.currentFrame) {
|
|
52
|
-
pb.setCurrentFrame(snapshot.currentFrame);
|
|
53
|
-
}
|
|
54
|
-
if (snapshot.isPlaying && !pb.isPlaying)
|
|
55
|
-
pb.play();
|
|
56
|
-
else if (!snapshot.isPlaying && pb.isPlaying)
|
|
57
|
-
pb.pause();
|
|
58
|
-
});
|
|
59
|
-
}, [playback]);
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
installTraceGlobal();
|
|
62
|
-
const s0 = usePlaybackStore.getState();
|
|
63
|
-
playback.setPlaybackRate(s0.playbackRate);
|
|
64
|
-
playback.setLoop(s0.loop);
|
|
65
|
-
if (s0.isPlaying)
|
|
66
|
-
playback.play();
|
|
67
|
-
return usePlaybackStore.subscribe((state, prev) => {
|
|
68
|
-
if (state.isPlaying !== prev.isPlaying) {
|
|
69
|
-
if (state.isPlaying)
|
|
70
|
-
playback.play();
|
|
71
|
-
else
|
|
72
|
-
playback.pause();
|
|
73
|
-
}
|
|
74
|
-
if (state.currentFrameEpoch !== prev.currentFrameEpoch) {
|
|
75
|
-
const willSeek = state.currentFrame !== playback.currentFrame;
|
|
76
|
-
trace('SEEK_GATE', {
|
|
77
|
-
storeFrame: state.currentFrame,
|
|
78
|
-
engineFrame: playback.currentFrame,
|
|
79
|
-
willSeek,
|
|
80
|
-
});
|
|
81
|
-
if (willSeek)
|
|
82
|
-
playback.seek(state.currentFrame);
|
|
83
|
-
}
|
|
84
|
-
if (state.playbackRate !== prev.playbackRate) {
|
|
85
|
-
playback.setPlaybackRate(state.playbackRate);
|
|
86
|
-
}
|
|
87
|
-
if (state.loop !== prev.loop) {
|
|
88
|
-
playback.setLoop(state.loop);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
}, [playback]);
|
|
92
|
-
useEffect(() => () => playback.destroy(), [playback]);
|
|
93
|
-
const value = useMemo(() => ({ engine, playback }), [engine, playback]);
|
|
94
|
-
return (_jsx(EditorContext.Provider, { value: value, children: children }));
|
|
95
|
-
}
|