npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@videoflow/renderer-dom

v1.2.0

Published

DOM-based live video renderer for VideoFlow — renders video frames directly to a DOM element using Shadow DOM

Readme

@videoflow/renderer-dom

npm license

Live, scrubbable preview of VideoFlow videos directly in the DOM. Mount a DomRenderer into any element, hand it a compiled VideoJSON, and you get a player with frame-accurate seek, audio sync, and incremental editing primitives — perfect for previews and as the rendering core of a video editor.

Live demo: videoflow.dev/playground · Renderers docs: videoflow.dev/renderers

If you're building a visual timeline editor, see also VideoFlow React Video Editor — a drop-in React UI built on top of this renderer.


Why use this package?

  • Real-time DOM playback. Layers render as native DOM elements (text via <textual-layer>, media via <video> / <canvas>, shapes via SVG, …) inside a Shadow DOM for full style isolation.
  • Frame-accurate seek + audio sync. seek(frame) jumps anywhere instantly. play() runs a requestAnimationFrame loop with audio mixed via OfflineAudioContext and played back through an <audio> element synced to the visual frame index.
  • Incremental editing primitives. addLayer, removeLayer, updateLayer, reorderLayers, updateVideo mutate a single layer and re-render only the current frame — no loadVideo() round-trip, no flicker.
  • Same transition + effect engine as export. Built-in transitions, mix-blend-mode, GLSL effects, and groups all work identically here and in @videoflow/renderer-browser. Live preview matches the exported MP4 pixel-for-pixel for every common case.
  • Editor-friendly hit testing. Effect layers split into a [data-effect-layer] source (kept invisible but pointer-targetable for selection) and a [data-effect-overlay] canvas (visible but pointer-events: none) — clicks always land on the layer's actual bounding box, not the full-screen overlay.

Installation

npm install @videoflow/core @videoflow/renderer-dom

Quick Start

import VideoFlow from '@videoflow/core';
import DomRenderer from '@videoflow/renderer-dom';

// 1. Build a video
const $ = new VideoFlow({ width: 1920, height: 1080, fps: 30 });
const title = $.addText({ text: 'Hello!', fontSize: 6, color: '#fff' });
title.fadeIn('1s');
$.wait('3s');
title.fadeOut('1s');

// 2. Compile and play it back
const json = await $.compile();
const player = new DomRenderer(document.getElementById('player'));
await player.loadVideo(json);
await player.play();

The <div id="player"> is the host element — DomRenderer attaches a Shadow DOM inside it and scales the rendered video to fit using container queries.


Player API

Construction & lifecycle

const player = new DomRenderer(hostElement);

await player.loadVideo(videoJSON);   // load (or hot-swap) a project
player.destroy();                    // clean up Shadow DOM, GL contexts, audio

Playback

await player.play({
  fpsCallback: (fps) => console.log(fps.toFixed(1)),  // optional render-fps HUD
});
player.stop();

Seek / scrub

await player.seek(150);                  // jump to frame 150
player.currentTime = 4.2;                // setter — same as seek(round(t * fps))
console.log(player.currentTime);         // getter — current time in seconds
await player.renderFrame(150);           // render one frame without starting playback

Public properties

player.playing        // boolean — is playback active?
player.currentFrame   // number  — current frame index
player.currentTime    // number  — current time in seconds (get/set)
player.totalFrames    // number  — duration * fps
player.duration       // number  — duration in seconds
player.fps            // number  — frames per second

onFrame callback

Assign a function to be notified every time a new frame paints — during play() or after a seek / renderFrame call. Ideal for keeping a seek bar / time label in sync.

player.onFrame = (frame) => {
  timeline.value = String((frame / player.totalFrames) * 100);
  timeLabel.textContent = (frame / player.fps).toFixed(2) + 's';
};
// Clear it:
player.onFrame = null;

Editing API

These methods mutate the loaded project and re-render only the current frame — much cheaper than calling loadVideo() again. All are asynchronous and serialised through an internal mutation queue, so you can safely fire them in quick succession from a UI.

updateLayer(id, patch)

Patch a layer's settings, properties, animations, transitions, or effects.

await player.updateLayer('title', {
  properties: { color: '#ff5a1f', fontSize: 8 },
});

await player.updateLayer('title', {
  settings: { startTime: 1, sourceDuration: 5 },
  transitionIn: { transition: 'slideUp', duration: '500ms' },
});

addLayer(layerJSON, index?)

await player.addLayer({
  id: 'caption',
  type: 'text',
  properties: { text: 'New caption', fontSize: 3, position: [0.5, 0.85] },
  settings: { startTime: 2, sourceDuration: 3 },
  animations: [],
});

removeLayer(id) / reorderLayers(orderedIds)

await player.removeLayer('caption');
await player.reorderLayers(['bg', 'title', 'caption']);

updateVideo(patch)

Top-level project properties that can be patched without a full reload (width, height, backgroundColor, name, duration). Changing fps requires loadVideo().

await player.updateVideo({ width: 1080, height: 1080, backgroundColor: '#0a0d18' });

Transitions

DomRenderer fully supports transitionIn / transitionOut declared on layers. All built-in presets (slideUp, zoom, overshootPop, blurResolve, glitchResolve, motionBlurSlide, noiseDissolve, wipeReveal, typewriter, numberCountUp, …) animate automatically — no extra setup. See the core README → Transitions for the full table and the signed-p contract.

Custom transitions

DomRenderer.registerTransition() writes to a registry shared with BrowserRenderer, so a preset registered here also runs at export time:

import DomRenderer from '@videoflow/renderer-dom';

DomRenderer.registerTransition('spinIn', (p, properties, params, ctx) => {
  const t = 1 - Math.abs(p);            // 0 at edges, 1 at rest
  properties.rotation = (properties.rotation ?? 0) + (1 - t) * (params.angle ?? 360);
  properties.opacity  = (properties.opacity  ?? 1) * t;
  return properties;
}, {
  defaultEasing: 'easeOut',
  layerCategory: 'visual',
});

GLSL effects

Layers with an effects array (or transition presets that inject effects via injectsEffects: true) are rendered through a project-sized <canvas data-effect-overlay> per layer. Each frame, the layer is rasterized off-screen and piped through the shared WebGL compositor, then painted onto the overlay. Non-effect layers stay on the fast DOM-mutation path, so there's zero overhead for the common case.

Effects flow through groups too: an effects array on a $.group(...) runs against the group's composited surface, so a single shader pass can apply to a whole sub-tree.

Custom effects

import DomRenderer from '@videoflow/renderer-dom';

DomRenderer.registerEffect(
  'glitchShift',
  `
vec4 effect(sampler2D tex, vec2 uv, vec2 resolution) {
  vec2 shifted = uv + vec2(u_amount * sin(uv.y * 40.0), 0.0);
  return texture2D(tex, shifted);
}`,
  {
    amount: { type: 'float', default: 0.02, min: 0, max: 0.1, animatable: true },
  },
);

The same registry is used by @videoflow/renderer-browser, so effects you register live also export correctly.


How it works

  1. Shadow DOM mount. Each layer becomes a real DOM element inside the host's Shadow Root. CSS handles the entire visual pipeline — transforms, blend modes, filters, shadows, font loading, fit modes — and [data-renderer] carries isolation: isolate so blend modes stay scoped to the project.
  2. Per-frame property pass. On every seek / renderFrame / animation tick, each layer's interpolated properties at the target frame are written as inline CSS / custom properties — so the browser re-renders incrementally instead of rebuilding the DOM.
  3. Effect overlays. Layers with effects render an off-screen rasterized bitmap into a sibling overlay canvas. The overlay is pointer-events: none (clicks fall through to the source), and mix-blend-mode is mirrored from the layer so it composites correctly.
  4. Audio sync. On play(), the project audio is rendered to a single AudioBuffer via OfflineAudioContext (recursing through groups, honouring volume/pan/pitch/mute and audio-side transitions like fade). The buffer is wrapped in an <audio> element; the rAF loop nudges its playbackRate to keep audio and visual frame index in sync.
  5. Group sub-mixes. Group children live in an off-screen virtualRoot (so getComputedStyle and Web Animations resolve correctly), and the renderer's compositeLayerInto flattens them into the group's <canvas> each frame — the only group artefact in the visible DOM tree.

End-to-end example: a video player with controls

<div id="player" style="aspect-ratio: 16/9; background: #000;"></div>
<div>
  <button id="playBtn">Play</button>
  <button id="stopBtn">Stop</button>
  <input type="range" id="timeline" min="0" max="100" value="0">
  <span id="time">0:00</span>
</div>

<script type="module">
  import VideoFlow from '@videoflow/core';
  import DomRenderer from '@videoflow/renderer-dom';

  // Build the video
  const $ = new VideoFlow({ width: 1280, height: 720, fps: 30 });
  $.addText(
    { text: 'VideoFlow Preview', fontSize: 5, fontWeight: 800 },
    {
      sourceDuration: '5s',
      transitionIn:  { transition: 'slideUp', duration: '500ms' },
      transitionOut: { transition: 'fade',    duration: '400ms' },
    },
  );
  $.wait('5s');

  // Mount the player
  const json = await $.compile();
  const player = new DomRenderer(document.getElementById('player'));
  await player.loadVideo(json);

  // Wire up controls
  document.getElementById('playBtn').onclick = () => player.play();
  document.getElementById('stopBtn').onclick = () => player.stop();

  player.onFrame = (frame) => {
    timeline.value = String((frame / player.totalFrames) * 100);
    time.textContent = (frame / player.fps).toFixed(2) + 's';
  };

  document.getElementById('timeline').addEventListener('input', (e) => {
    const frame = Math.floor((e.target.value / 100) * player.totalFrames);
    player.seek(frame);
  });
</script>

Notes & requirements

  • Modern browsers only. Uses Shadow DOM, container queries, OffscreenCanvas, and (for effects) WebGL.
  • CORS. Image / video / audio sources must be CORS-readable for decode() and decodeAudioData() to succeed.
  • Hit-testing. When listening to clicks/pointer events on the host, walk event.composedPath() and pick up data-id to identify the targeted layer — effect overlays already pass clicks through to the source layer.

Related packages

Resources

License

Apache-2.0