@ilovevideoeditor/renderer-dom — API reference
Live, scrubbable player that paints VideoJSON into a DOM host.
Classes
DomRenderer
Properties
currentFrame:number— Current frame rendered.layerById:Map<string, RuntimeBaseLayer>— Fast id → runtime layer lookup, kept in sync withlayers.layers:RuntimeBaseLayer[]— Runtime layer instances.onFrame:(frame: number) => void | null— Optional callback fired whenever a new frame is rendered. Set this externally to keep a UI (seek bar, time label, …) in sync with playback.playing:boolean— Whether playback is active.currentTimedurationfpstotalFrames
Methods
constructor(host: HTMLElement): DomRendereraddLayer(layerJSON: LayerJSON, index?: number): Promise<void>— Insert a new layer into the video at the given index (defaults to end).
The new layer is constructed, initialized, and mounted into the existing $canvas — other layers' DOM elements are left untouched.
compositeLayerInto(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, layer: RuntimeBaseLayer, frame?: number): Promise<void>— Rasterize a single layer (cached when possible), pipe it through the WebGL effect compositor if it declares effects, anddrawImagethe result ontoctx. Used byRuntimeGroupLayer.renderFrameto flatten each child onto the group's canvas.destroy(clearShadow: boolean): void— Tear down the renderer: stop playback (also exits smooth-playback mode on every video layer), destroy every runtime layer (releases media elements, decoders, andrequestVideoFrameCallbacksubscriptions), clear the per-layer effect canvases, dispose the rasterizer's per-layer surfaces and the WebGL effect compositor's GL context, and finally empty the shadow DOM. Pending queued mutations registered before destroy resolve to no-ops via thedestroyedguard.getPropertyDefinition(layerType: string): Record<string, PropertyDefinition> | undefined— Return the full propertiesDefinition for a layer type.getVirtualLayerHost(): Node— Where group layers should park their hidden child host. We use the shadow root so renderer CSS (which is shadow-scoped) still applies to the group's children — without this, descendants would lose their--vw/--project-*context and render at the wrong scale.loadFont(fontName: string): Promise<void>— Load a Google Font and make it available to the document (and shadow DOM).loadVideo(videoJSON: VideoJSON): Promise<void>— Load a compiled VideoJSON into the renderer.
Sets up the shadow DOM, creates runtime layers, initialises media, and renders frame 0.
play(options: { fpsCallback?: (fps: number) => void }): Promise<void>— Start real-time playback from the current frame with audio sync.
Renders audio to WAV, creates an Audio element, and advances frames inside a requestAnimationFrame loop while nudging the audio playbackRate to keep video and audio in sync.
Each layer is switched into its smooth-playback path via RuntimeBaseLayer.enterSmoothPlayback at the start of the loop — for video layers this trades the per-frame currentTime seek for a native <video>.play() plus drift correction, eliminating the seek cost that otherwise dominates live-preview frame budget. The path is reverted on stop() / seek() so scrubbing stays frame-accurate.
removeLayer(id: string): Promise<void>— Remove a layer from the video.
Destroys the layer (releasing its media ref) and detaches its DOM element. Other layers are untouched.
renderAudio(): Promise<AudioBuffer | null>— Render the full audio track as an AudioBuffer.renderFrame(frame: number, force: boolean): Promise<void>— Render a specific frame to the DOM.
Skips if already at that frame (unless forced), queues if a render is already in progress.
reorderLayers(orderedIds: string[]): Promise<void>— Reorder layers to match the given id sequence.
The runtime layers and the backing videoJSON.layers array are reordered, and the layers' $elements are re-appended to $canvas in the new order. For layers without a track, DOM order drives paint order. Layers with a track carry an explicit z-index (track + 1) written in applyProperties, so reordering them in the array without changing their track only changes DOM order, not visual stacking. No media is touched.
seek(frame: number): Promise<void>— Seek to a frame.
If playback is active, stops it (which exits smooth mode), renders the target frame deterministically through the seek path, then restarts playback (re-entering smooth mode). When playback is not active, the frame is decoded by the seek path directly — pixel-deterministic, the same path used by export.
stop(): void— Stop playback.
Bumps the play-token (so any loop suspended in await renderAudio() / requestAnimationFrame exits without running its cleanup), tears down the Audio element, and reverts every layer's smooth-playback hook so the next renderFrame (typically from seek() or currentTime =) decodes the exact requested timestamp instead of whatever the smooth decoder happened to be presenting.
updateLayer(id: string, patch: { animations?: Animation[]; effects?: any[]; properties?: Record<string, any>; settings?: Partial<LayerSettingsJSON>; transitionIn?: any; transitionOut?: any }): Promise<void>— Apply a property / settings / animations patch to a single layer.settingsis shallow-merged intolayer.json.settings.propertiesreplaceslayer.json.propertieswholesale so keys removed by the editor (e.g. via a reset-to-default) are actually cleared. Callers should pass the full post-mutation properties object.animationsreplaces the array wholesale (same rationale — callers hold the diffing logic because per-keyframe reconciliation is cheap to do in editor state).
If settings.source changed, the layer's media is re-initialized via layer.initialize() — callers should debounce rapid source swaps.
If a text layer's fontFamily is among the patched properties, the font is loaded into the shadow DOM before the frame is re-rendered.
updateVideo(patch: { backgroundColor?: string; duration?: number; height?: number; name?: string; width?: number }): Promise<void>— Patch top-level video properties (width, height, backgroundColor, name, duration).
fps changes are not supported here — they invalidate frame numbers across the pipeline and require a full loadVideo(). duration is safe to update incrementally: it's only used as the loop bound in play() and the divisor in the totalFrames getter; layer frame numbers depend on fps, not duration. An in-flight play() call captures durationSec at start, so the new bound takes effect on the next play() invocation.
registerEffect(name: string, glsl: string, params: Record<string, EffectParamDefinition>): void— Register a GLSL effect. Shares its registry withBrowserRenderer.registerTransition(name: string, fn: TransitionFn): void— Register a transition preset. The registry is shared withBrowserRendererso a transition registered on either renderer is available in both live preview and export.
Type aliases
DomRendererCallback
DomRendererCallback: (event: string, data: any) => void