@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 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
  [![npm](https://img.shields.io/npm/v/@elah/editor)](https://www.npmjs.com/package/@elah/editor)
8
- [![gzip size](https://img.shields.io/badge/gzip-~64%20KiB%20SDK-brightgreen)](../../BUNDLE_STRATEGY.md)
9
10
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](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:** ~19 KiB gzipped for the editor layer; an app using the full SDK (`core` + `react` + `timeline` + `editor`) starts at ~64 KiB gzipped with the demuxer injected or omitted, and ~227 KiB if it calls `createDefaultDemuxerFactory()`, which pulls `mediabunny` (159 KiB) into the startup graph. Measured with code splitting — see [BUNDLE_STRATEGY.md](../../BUNDLE_STRATEGY.md).
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(TransitionOverlay, { ref: transitionOverlayRef }), _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: () => {
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" })] })) }))] }));
@@ -1,5 +1,5 @@
1
- export { EditorProvider } from './EditorProvider';
2
- export type { EditorProviderProps } from './EditorProvider';
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';
@@ -1,4 +1,4 @@
1
- export { EditorProvider } from './EditorProvider';
1
+ export { EditorProvider } from '@elah/react';
2
2
  export { AssetPanel } from './AssetPanel';
3
3
  export { ElementsPanel } from './ElementsPanel';
4
4
  export { Preview } from './Preview';
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 './editor/EditorProvider';
78
- export type { EditorProviderProps } from './editor/EditorProvider';
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 { EditorProvider } from './editor/EditorProvider';
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.6.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.6.0",
63
- "@elah/react": "^0.6.0",
64
- "@elah/timeline": "^0.6.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
- }