Docs  →  The Generative CSS Animation Pipeline

From Text to Canvas: The Generative CSS Animation Pipeline

How MooduTools turns the raw text emitted by an on-device LLM into living, interactive HTML5/CSS animations — without a canvas API, without WebGL, and without trusting anything the model says.

Category: Rendering & CSS Animation Reading time: ~12 min Level: Intermediate

1. Overview

The companion architecture document described where MooduTools runs. This document describes what it makes. When a user types a mood, an on-device model returns a block of raw text. That text is meant to be a self-contained fragment of HTML and CSS: a little stage of divs, gradients, and @keyframes that produces a moving, expressive picture. The interesting engineering lives in how that text is produced responsibly, parsed safely, parameterized by emotion, and rendered at 60 frames per second — all inside a single origin with no external assets.

Two constraints shape everything. First, the output must be safe: we are injecting code that originated from a generative model, so it is treated as untrusted until proven inert. Second, the output must be expressive: because we aim for emotional resonance rather than data visualization, the pipeline speaks in motion, easing, light, and rhythm rather than axes and labels.

2. Constraining the Model Before It Speaks

The safest code is code the model is asked to produce under a strict contract. The system prompt is a deterministic specification, not a wish. It tells the model that it is a frontend developer and storyteller, that it must return only a raw HTML fragment with no markdown fences, no explanation, and no commentary, and that the fragment must be a single self-contained scene with inline CSS. Tightening the prompt at the source reduces the burden on the sanitizer downstream.

const SYSTEM_PROMPT =
  'You are an expert creative frontend developer and storyteller... ' +
  'Output ONLY the raw executable HTML/CSS. ' +
  'No markdown, no code fences, no explanations, no commentary.';
  
  // The user mood is appended at request time:
  `User's mood: "${mood}". Generate the art for this mood.`

A secondary, equally important part of the contract is determinism of shape. The model is expected to return meaningful text to author a micro-story, but the visual result must always be expressible as plain CSS keyframes — no canvas rasterization, no WebGL shaders, no external fonts or images. This constraint is what keeps the whole pipeline offline and dependency-free, and it is what lets a single static page render the model's imagination using only the compositor thread.

3. Parsing & Sanitization

Once the text arrives, the pipeline performs a defensive parse. A naive approach would inject the string straight into innerHTML; a competent one treats the model as an attacker.

function cleanOutput(raw) {
  if (typeof raw !== 'string') return '';
  let out = raw;
  // 1) Strip fenced code blocks and stray backticks.
  out = out.replace(/```[\s\S]*?```/g, '').replace(/```/gi, '');
  // 2) Hard-remove any script element, no exceptions.
  out = out.replace(//gi, '');
  // 3) Keep only the outermost printable fragment.
  const start = out.indexOf('');
  if (start !== -1 && end !== -1 && end > start) {
    out = out.slice(start, end + '
'.length); } return out.trim(); }

This single function collapses several classes of risk at once. Stripping fenced blocks removes markdown that would otherwise render as visible or break the DOM. Stripping <script> (and its case variations) neutralizes the most dangerous payload. Slicing between the first and last <div> discards surrounding prose the model may have accidentally leaked. In production this is reinforced by a strict Content-Security-Policy that forbids inline event handlers and javascript: URLs as a belt-and-suspenders layer, and every injected node is created through the DOM rather than parsed through sugar so that attributes like onerror have no execution path.

4. Emotion to Parameters

Pure LLM output is expressive but non-deterministic. Two people typing “joy” within seconds of each other get different fragments, and that variety is part of the charm. Yet the pipeline also needs a stable, reproducible mapping from an emotion word to concrete motion parameters for those browsers and cases where the model is unavailable. This is where the emotion-parameter layer sits.

Curated presets map each supported emotion to a profile of CSS design tokens: a palette, a keyframe signature, easing curves, and a density of moving elements. A “joyful” profile might lean on fast springlike easing, warm saturated hues, and bouncy translate/scale keyframes. A “melancholic” profile prefers slow cubic-bezier fades, desaturated cool tones, and drifting vertical motion that reads like rain or memory.

const EMOTION = {
  joyful:     { easing: 'cubic-bezier(.2, 1.4, .4, 1)', duration: '1.6s', hues: 'sunrise'  },
  melancholic:{ easing: 'ease-in-out',                  duration: '5s',   hues: 'dusk'     },
  energized:  { easing: 'cubic-bezier(.1,.9,.2,1)',     duration: '1.1s', hues: 'electric' },
};

A design token is a named decision. Expressing emotion as tokens rather than hard-coded values means the raw CSS the model returns and the curated presets can share the same visual language: both speak in easing curves, hue ramps, and keyframe timings. When the on-device model is present, the model chooses freely within its system-prompt contract; when it is absent, the preset system reproduces the same genre of motion from the same semantic vocabulary. This unification is what lets a visitor switch seamlessly between an AI-generated scene and a live demo without noticing a seam in the rendering engine.

5. Runtime Rendering

Rendering is deliberately boring, and that is a feature. After sanitization, the fragment is injected into an offscreen container and the browser's layout and compositor take over:

canvas.hidden = false;
canvas.innerHTML = html;            // sanitized bottleneck
runCanvasAnimations();              // force a reflow so @keyframes start cleanly

function runCanvasAnimations() {
  canvas.style.display = 'none';
  void canvas.offsetHeight;         // flush layout
  canvas.style.display = '';
}

The void canvas.offsetHeight line deserves explanation. CSS keyframes authored inside a dynamically injected node sometimes fail to start because the browser batches style and layout changes. Touching the offsetHeight forces a synchronous reflow, guaranteeing the compositor recognizes the new animation frames and that stagger delays begin exactly where intended. It is a tiny, unglamorous instruction with an outsized effect on perceived polish.

Because the art is pure CSS, nearly all animation work is offloaded to the browser's compositor thread rather than blocked on the JavaScript event loop. Transforms, opacity, and filter animations run on their own thread, so even elaborate scenes keep the main thread free to handle the surrounding interactions — saving the story card, capturing a clip, or recasting the mood. The page couples generative content to a fully responsive UI, which is precisely the behavior users read as “smooth.”

6. Performance & Resilience

Four pragmatic rules keep rendering both fast and resilient in production:

Expression is bounded by physics, not by imagination. By steering the generative output toward compositor-friendly properties, the pipeline can afford grand scenes — dozens of drifting, glowing elements — without trading away responsiveness.

7. Conclusion

The generative CSS animation pipeline is the bridge between an on-device model's poetic text and a living picture worth feeling. It constrains the model with a disciplined prompt, sanitizes its output with a paranoid parser, expresses emotion through a shared token vocabulary, and delegates the actual motion to the browser's compositor. Nothing leaves the device, nothing external is loaded, and nothing the model produces is ever trusted blindly.

The result is a system that is simultaneously fast, private, and surprisingly expressive: a full generative art experience delivered as a handful of static files and a single in-browser model. For engineers, it is a working demonstration that on-device AI need not mean diminished craft — only more constrained surfaces, and more deliberate design within them.

Reviewed from the top of the stack, start with the full system context in Zero-Cost Infrastructure: Running AI Completely in Client Browsers.