Active DevelopmentSystemsProduct

SceneForge

Turns a JSON manifest of HTML/CSS/JS scenes into a single MP4, scene by scene. Headless Chrome renders frames, FFmpeg composes them, and a React/Monaco editor drives it. Runs entirely locally — no cloud APIs, no uploads.

JavaScriptReactFFmpegHeadlessNode.js
Private repository

The problem

Producing short motion graphics from code normally means either a heavyweight editor or a cloud rendering service. Neither lets you write a scene as ordinary HTML, CSS and JavaScript and get a video file back on your own machine.

Context and constraints

A local tool for turning a JSON manifest of HTML, CSS and JavaScript scenes into a single MP4, scene by scene. No cloud APIs, no keys and no uploads — the whole pipeline runs on the machine that owns the files.

  • Scene code is arbitrary HTML, CSS and JavaScript supplied by the user, so the editor preview and the renderer need different trust models.
  • Every scene shares one output document, so CSS from one scene must not leak into another.
  • Rendering drives a real browser and a real encoder, which makes it CPU-heavy and single-job rather than something to expose to concurrent users.
  • The stage is fixed at 1920x1080 and 30 frames per second, so timeline maths has to line up scene boundaries with frame boundaries.
  • One upstream dependency could not be installed from its published package at all, which forced a vendoring decision.

What I built

An Express backend that compiles a scene manifest into per-scene compositions, drives headless Chrome to capture frames and hands them to FFmpeg, plus a React and Monaco editor with one editor per scene, a live preview and a render button. Timeline arithmetic comes from a dedicated package rather than being reimplemented.

Architecture

A manifest becomes a video in stages: each scene is compiled to its own composition document, the timeline places those documents in time, headless Chrome renders frames, and FFmpeg encodes them into one MP4.

authorsubmitcompileplacerenderframesencodeEditorReact, Vite, MonacoScene manifestJSON, validatedExpress APILocal onlyCompositionsOne document per sceneTimelineStart and durationHeadless ChromeFrame captureFFmpegEncodeMP41920x1080 at 30fps
Illustrative architecture drawn from the project documentation. It is not a screenshot of the editor and not a frame from a render — no render artifact is published with this project, so none is shown.
Editor
React, Vite, Monaco
Scene manifest
JSON, validated
Express API
Local only
Compositions
One document per scene
Timeline
Start and duration
Headless Chrome
Frame capture
FFmpeg
Encode
MP4
1920x1080 at 30fps

My contribution

Sole engineer. Backend render pipeline, manifest model and validation, editor frontend, and the composition layer between them.

Technical decisions

What was chosen, why, and what it cost.

Compile each scene into its own document

Decision
Every scene becomes a separate composition document, embedded into the output with its own start time and duration, rather than all scenes sharing one page.
Why
Separate documents give each scene its own CSS scope, so a scene can style the page body normally and cannot leak styles into the scene after it. Sharing a document would have made every selector a potential collision.
Trade-off
More documents to coordinate, and anything intended to persist across scenes has to be arranged deliberately rather than simply existing on the page.

The preview sanitises and never executes scene JavaScript

Decision
The in-browser preview renders sanitised markup only. Scene JavaScript runs during the render and nowhere else.
Why
The preview lives in the author's own browser session. Executing arbitrary scene JavaScript there would put the editor at the mercy of the content it is editing.
Trade-off
The preview cannot show anything the scene's JavaScript does, so animation and dynamic behaviour are only visible after a render.

Strip script tags and reject navigating scene code

Decision
Script tags inside a scene's markup are stripped, JavaScript belongs in a dedicated field, and scene code that tries to navigate the page is rejected outright with a client error.
Why
A scene that navigates the rendering browser breaks the render for every scene after it, and inline scripts blur the boundary between markup and behaviour that the rest of the pipeline depends on.
Trade-off
A legitimate pattern — an inline bootstrap script — has to be rewritten to fit the manifest shape.

Vendor the character engine rather than depend on it

Decision
The mascot engine is committed into the repository as a pinned build, with refresh instructions recorded next to the code that uses it.
Why
Its published package cannot be installed — the manifest fails version parsing — so a normal dependency was not available.
Trade-off
Updates are manual and the vendored copy can drift from upstream, which is why the refresh procedure is documented rather than assumed.

Security and reliability

The failure modes that shaped the implementation.

Two trust models for the same code

The preview sanitises markup and never runs scene JavaScript; the renderer executes it in a browser it controls. The split is deliberate — the risky operation happens where the blast radius is a render job rather than the author's session.

Manifest limits are enforced, not assumed

Scene identifiers must match a restricted character set, and the manifest is capped at twenty scenes of sixty seconds each. Validation rules are covered by the test suite.

Breakout escaping is tested

Scene content is embedded into generated documents, so the tests specifically cover escaping that would otherwise let style or script content break out of its container.

Rendering is single-job by nature

A render drives a browser and an encoder and is CPU-heavy, so the documentation is explicit that it needs a queue in front of it before more than a handful of users touch it.

Verification

How the implementation was checked, and how much of that can be shown publicly.

  • Backend test suiteevidenced

    Covers the timeline arithmetic, the shape of composition output, breakout escaping for style and script content, and every manifest-validation rule. No test count is quoted here because the repository documentation does not state one.

  • Environment check endpointevidenced

    A diagnostic endpoint reports whether Chrome, FFmpeg, disk and GPU are actually available, so a render failure can be told apart from a missing dependency.

  • No published render outputnot publicly evidenced

    The repository's output directory contains no committed video, so there is no render artifact to show and none is claimed. The pipeline is described rather than demonstrated.

  • Determinism is not a documented guaranteenot publicly evidenced

    Frames are captured from a real browser against a fixed stage and frame rate, but the project documentation makes no claim that two renders of the same manifest are byte-identical, so none is made here.

Results

  • A manifest of HTML, CSS and JavaScript scenes compiles into per-scene composition documents and encodes to a single MP4 at a fixed 1920x1080, 30 frames per second stage.
  • Scenes are CSS-isolated from each other by construction rather than by naming convention.
  • Two upstream rendering quirks were diagnosed and worked around: a package that cannot be installed from its published manifest, and a build variant that throws inside its render loop under headless Chrome and paints nothing.

Limitations and disclosure

What this project does not do, and what cannot be shown publicly.

  • Rendering is CPU-heavy and single-job; the documentation is explicit that it needs a queue in front of it before more than a handful of users use it.
  • The editor preview never executes scene JavaScript, so animation and dynamic behaviour are only visible after a full render.
  • The character engine is vendored as a pinned build because its published package fails to install, so updates are manual and can drift from upstream.
  • No render artifact is published with the project, and the documentation states no test counts, rendering benchmarks or determinism guarantee.

The repository is private. This page describes the rendering architecture and the engineering decisions behind it. No editor screenshot, rendered frame or output video is shown, because none is published with the project — and no test counts or rendering benchmarks are quoted, because the repository documentation does not state any.

Other projects in the same engineering domains.