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

@fr0/renderer-browser

v0.1.0

Published

Browser DOM renderer for fr0

Readme

@fr0/renderer-browser

Browser-side React DOM renderer for fr0 + canvas-direct MP4 export pipeline.

Status

✅ Phase α-2 (preview, React DOM) — done ✅ Phase β-1 (MP4 export) — superseded by δ-5 ✅ Phase δ-5 (canvas-direct encoder, html2canvas撤廃) — done 🛑 Embed-only henceforth — the React DOM components are kept as a second renderer for IR validation; new visual features land in @fr0/renderer-canvas first.

Purpose (post-δ-5)

This package now plays two roles:

  1. React DOM preview / embed. <TimelineRenderer> and the layer components (<TextLayer>, <ShapeLayer>, …) render a Timeline as positioned React DOM. Useful for live preview UIs and for embedding fr0 output inside a React app where DOM (a11y, SEO, click handlers) matters.
  2. Browser-side MP4 export entry point. renderTimelineToVideo drives an offscreen <canvas> via @fr0/renderer-canvas and pipes the bitmaps into WebCodecs VideoEncoder + mp4-muxer. The encoder no longer touches the React DOM tree.

The two paths are intentionally independent: a feature added to the IR must be implemented in both renderers (canvas and DOM) before it ships, and any visual divergence between them is treated as an IR-ambiguity bug. This is how the project earns the right to call its IR "renderer-agnostic".

For server-side rendering see @fr0/renderer-node (which runs the same canvas pipeline inside a Playwright Chromium). For playback UI see @fr0/player.

Quick start — preview (React DOM)

import { TimelineRenderer } from '@fr0/renderer-browser';
import type { Timeline } from '@fr0/core';

function Preview({ timeline, frame }: { timeline: Timeline; frame: number }) {
  return <TimelineRenderer timeline={timeline} frame={frame} />;
}

Stateless — frame is driven by the caller. Playback loops, scrubbing, and controls belong to @fr0/player.

Quick start — MP4 export

import { renderTimelineToVideo } from '@fr0/renderer-browser';

const { blob, frames, width, height, fps } = await renderTimelineToVideo(timeline, {
  onProgress: (frame, total) => console.log(`${frame}/${total}`),
});

const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'timeline.mp4';
a.click();
URL.revokeObjectURL(url);

Browser-only. Requires WebCodecs VideoEncoder (Chromium-based browsers, Safari 16.4+, Firefox 130+).

For canvas-side custom components (type: 'custom' layers), pass a CanvasComponentRegistry from @fr0/renderer-canvas via the registry option. React COMPONENTS from @fr0/components no longer apply to the export path — they only power the DOM preview.

Public API

Preview (React DOM)

| Export | Purpose | |---|---| | <TimelineRenderer> | Top-level preview component. Calls resolve() once per (timeline, frame) via useMemo and renders every layer in z-order inside a canvas-sized root <div>. | | <LayerRouter> | Dispatches a single ResolvedLayer to the correct concrete component based on layer.type. Returns null for invisible layers. | | <TextLayer> / <ShapeLayer> / <ImageLayer> / <VideoLayer> / <GroupLayer> / <CustomLayer> | Concrete layer components. Importable directly for custom hosts. | | applyTransform(resolvedTransform) | Pure helper mapping a ResolvedTransform to the CSSProperties used on every layer element. | | CustomComponentRegistry / CustomComponentProps | Types for the React-side custom layer registry. |

Encoder (canvas-direct)

| Export | Purpose | |---|---| | renderTimelineToVideo(timeline, options) | High-level one-shot: offscreen <canvas> + image / video preload + font wait + frame loop (drawTimelineVideoFrame → encoder.encode) + mux. Returns { blob, frames, width, height, fps }. | | preloadImages(timeline) | Walks image layers (including nested group children) and decodes every unique src as HTMLImageElement before the render loop. | | preloadVideos(timeline) | Walks video layers and creates an HTMLVideoElement per unique src, awaiting loadedmetadata. | | createWebCodecsEncoder(options) | Low-level WebCodecs VideoEncoder wrapper (H.264, hardware-preferred, 60-frame keyframe cadence). | | createMp4Muxer(options) | Low-level mp4-muxer wrapper using ArrayBufferTarget with fastStart: 'in-memory'. |

Design notes — preview (React DOM)

  • Frame distribution is pure props, not React Context. The renderer is a one-shot data flow from timeline + frame → DOM.
  • Visibility is conditional rendering, not CSS. Layers outside their [from, from + duration) window return null.
  • Transform-origin carries the anchor. left / top are the raw x/y from core; rotation and scale pivot via transform-origin: {anchorX*100}% {anchorY*100}%.
  • The layer router is a closed switch, not a registry. The one exception is the custom layer type, which uses an explicit Map registry passed as a prop.
  • Fill kind: 'solid' only. The DOM renderer flattens FillValue to a CSS color string and ignores gradient kinds; gradients render correctly through the canvas renderer instead.

Design notes — encoder (Phase δ-5)

  • Canvas-direct, no DOM intermediary. The encoder allocates a single offscreen HTMLCanvasElement of timeline.width × timeline.height and calls drawTimeline(ctx, timeline, frame) from @fr0/renderer-canvas. There is no React tree, no html2canvas, no DOM ↔ bitmap step.
  • WebCodecs VideoEncoder with codec 'avc1.42001e' (H.264 Baseline 3.0). Widest MP4 playback compatibility. Hardware acceleration is preferred. Keyframes every 60 frames.
  • mp4-muxer ArrayBufferTarget + fastStart: 'in-memory' — everything in RAM until finalization, the resulting Blob is playable directly.
  • Frame-by-frame imperative driving. renderTimelineToVideo advances the counter as fast as the canvas + encoder pipeline can absorb — not real-time playback.
  • Image / video preloading. preloadImages and preloadVideos run before the first frame so fonts and decoded bitmaps are stable. The video preload also mounts the temporary <video> elements in an offscreen host because some browsers refuse to seek detached video nodes.
  • Per-frame video seek. Inside the loop, seekVideosForFrame walks the resolved state, sets currentTime = startTime + localFrame / fps on each visible video element, and waitForVideoSeeks blocks until every touched element fires seeked (with a 2 s fallback).
  • Single source of truth. Preview, browser export, and Node export (via renderer-node) all run the same drawTimeline function. Visual divergence between preview and export is impossible by construction — the preview path has its own DOM renderer, but the export path is unified across browser and Node.

Testing

pnpm --filter @fr0/renderer-browser test

48 tests across 10 files, using vitest + jsdom + @testing-library/react. The DOM components have direct rendering tests; the encoder is exercised end-to-end via the smoke test in @fr0/renderer-node (which actually runs Playwright Chromium and produces a real MP4).

The Vite demo in packages/examples/react-player is the manual end-to-end check — pick a sample, click Export, verify the downloaded MP4 plays.

What this package is NOT