Skip to content

ADR 0030: Animated conceptual diagrams are frame-stepped captures of committed brand-tokened sources, shipped through the clip pipeline

Status: Accepted Date: 2026-08-03 Issue: #1161

Context

The Explanation section explains models: the ISA-88 procedural hierarchy, recipe-to-control-recipe instantiation, the 13-state machine, the GitOps flow, deployment topologies. Motion carries these better than stills, and the Explanation & Standards visuals epic needs one deliberate answer on treatment before it can storyboard anything.

An inventory of both sections (appendix below) sharpened the problem. The section's 38 image embeds resolve to 22 unique PNGs, and every one of them is a product-UI screenshot generated from a .shot.yaml spec. The repo contains zero drawn diagrams. The conceptual load is carried by three mermaid blocks and by prose alone: the three most conceptual pages (procedural-sfc-dialect.md, gitops-for-automation-engineers.md, ui-deployment-topologies.md, 5,700 words together) have no visuals at all. The animated-diagram question is therefore a greenfield authoring question. There is no migration backlog.

The doctrine from #1161 stands: film the product's own diagram surfaces first. The gateway already renders live SFC charts, FB networks, state-machine-driven faceplates, and the recipe hierarchy viewer, and the recorder pipeline (#655) films them with zero new tooling. The SVG leg this ADR designs exists only for the truly abstract diagrams no product surface renders.

The existing clip pipeline, which the SVG leg should ride:

  • Spec: committed docs/videos/*.clip.yaml, schema-validated (scripts/docs-shots/schema/clip.schema.json).
  • Record: record.mjs drives Playwright, captures wall-clock WebM, transcodes via ffmpeg to <stem>.mp4 + <stem>.gif + <stem>.jpg poster.
  • Governance: lint-docs-clips (schema validity, strict doc-orphan gate, covers: freshness, retired-site scan).
  • Publish: publish.mjs uploads to R2 keyed by tier, rewrites docs/videos/manifest.json with URL + sha256; idempotent by content hash.
  • Embed: the canonical <figure class="doc-video"> block (STYLE-GUIDE § Video Clips) with required poster, plus the play-once autoplay contract (doc-video-autoplay.js).

Requirements from #1161 for the SVG leg: deterministic output (same input, same frames), brand-tokened styling only, committed spec files as source of truth, the same publish pipeline and strict orphan gate, and motion restraint (staged reveals and traveling highlights; no ambient looping; the play-once autoplay contract applies).

Decision

We build the SVG leg as a thin extension of the clip pipeline. Four decisions:

1. An animated diagram IS a clip. A diagram declares itself with a diagram: block in an ordinary *.clip.yaml under docs/videos/:

video: isa88-four-layer-stack
summary: The four-layer stack, revealed layer by layer
diagram:
  src: diagrams/isa88-four-layer-stack.html   # relative to docs/videos/
tags: [diagram, explanation]
covers:
  - docs/videos/diagrams/isa88-four-layer-stack.html

The animation source is a committed, self-contained HTML/SVG/CSS file under docs/videos/diagrams/. Because the spec is a clip spec, every downstream stage applies unchanged: lint-docs-clips validates it and enforces the strict orphan gate, covers: freshness now watches the diagram source itself, publish.mjs uploads the artifacts and rows the manifest, and the doc embeds the standard figure.doc-video block. A diagram: spec rejects the filming-only keys (steps, stage, url, scenario, identity, auth, storageState, skipIf) at load time.

2. Rendering is deterministic frame-stepping, with no wall clock. record.mjs routes diagram: specs to a renderer (lib/render-diagram.mjs) that:

  1. serves the repo root over a loopback HTTP server and opens the diagram page in the pipeline's Chromium at the clip viewport (1280×720 default);
  2. waits for document.fonts.ready, collects document.getAnimations({subtree: true}), and pauses every animation;
  3. refuses to render if any animation is infinite (getComputedTiming().endTime unbounded), enforcing the no-ambient- looping rule mechanically;
  4. derives the timeline (latest animation end plus a short hold on the completed diagram) and seeks every animation's currentTime to frame / fps, screenshotting each frame;
  5. assembles the frames with ffmpeg into the same artifact set as a filmed clip: <stem>.mp4 (libx264, same encode parameters), <stem>.gif, and <stem>.jpg poster, which for diagrams defaults to the final frame (the completed diagram is the correct thumbnail).

The renderer never contacts the gateway, needs no cluster or docs-shots stack, and holds no load-governor heartbeat. A diagram re-render is a seconds-long, laptop-local operation. Same source and spec produce the same frames on the same host toolchain; the manifest's sha256 is the drift detector. (Bitwise stability across different Chromium/ffmpeg/font versions is out of scope, exactly as it is for screenshots.)

3. The animation vocabulary is CSS animations/transitions and WAAPI, declared in the committed source. These are the technologies document.getAnimations() can pause and seek. SMIL and rAF-driven JS animation are excluded: neither is externally seekable, so neither can render deterministically. Motion restraint follows from the vocabulary plus the infinite-animation refusal: staged reveals (opacity/transform, animation-delay sequencing) and traveling highlights (stroke-dashoffset, offset-path) express everything the Explanation diagrams need.

4. Styling consumes the gateway's generated brand tokens directly. The diagram page links internal/gateway/static/css/base.css (served by the render server from the repo root) and a shared docs/videos/diagrams/diagram-base.css; diagram-specific CSS uses var(--token) exclusively, with zero raw color literals. This is the lint-docs-brand-hex philosophy applied to a new surface, and it reuses the existing generated block, so no third palette block exists to drift when the brand manifest changes. Diagrams render on the gateway's dark --bg-primary and read as product-family artifacts next to filmed clips.

The GIF fallback keeps the clip pipeline's existing loop behavior; the play-once contract is enforced where it always was, on the <video> element by doc-video-autoplay.js. The motion-restraint rule governs the authored animation itself, which ends on a stable completed frame.

Alternatives Considered

  • Embed the live SVG/CSS animation in the docs page (no video at all — the reader's browser runs the animation). The source stays vector-crisp at any zoom and weighs a few kilobytes. Rejected on four grounds. First, it forfeits the settled doc-video contract: the play-once/click-to-replay behavior, start-on-scroll, poster-before-load and reduced-motion handling all fall out of <video> for free, while a CSS animation has no ended event to hang any of it on, so the docs theme would grow a parallel animation controller. Second, style isolation: the diagram consumes the gateway's brand tokens, which do not exist in the docs page's CSS scope (mkdocs-material has its own --md-* palette), so inlining means bridging two token systems or letting the theme's cascade, and every Material upgrade, reach into the diagram. Third, reviewability: the program's quality gate approves a fixed artifact frame by frame, and the manifest sha256 detects drift; a live animation renders differently per browser, font stack, and viewport, leaving nothing stable to approve or hash. Fourth, consistency: diagrams sit beside filmed clips on the same pages and should carry the same player chrome, publish machinery, and tier system. The conceded costs of rendering are raster output at the clip viewport and ~10x the bytes. (Recorded after the accepted decision prompted exactly this question in review; the alternative was considered and the decision is unchanged.)

  • Film the product only; no SVG leg. The default, and it stays the default wherever a product surface carries the concept (see appendix verdicts). Insufficient alone: the model-mapping ladder, the instantiation cascade crossing CRD boundaries, and deployment topologies have no product surface, and mermaid cannot animate them.

  • Wall-clock recording of the diagram page through recordOne verbatim. Maximum reuse, but it inherits everything designed for live capture: encode-time jitter, boot-trim measurement, the DEGHOST keyframe repair, the luma-inversion guard. Determinism ("same input, same frames") is unachievable by construction, and the workarounds cost more than the frame-stepper they replace.

  • Motion Canvas. MIT-licensed, genuinely deterministic, built for exactly this. Rejected for dependency surface: it brings a TypeScript scene format, its own renderer and editor UI, and a second animation idiom the epic's authors would learn alongside the clip DSL. Brand tokens would need bridging from CSS into its TS scene graph, working against the generated-block model. Our motion needs (staged reveal, traveling highlight) sit far below its power floor.

  • Remotion. React-based programmatic video. Rejected on licensing (company license required at our size) before dependency-surface arguments even start.

  • Lottie/Bodymovin. Authoring lives in After Effects, a proprietary tool outside the repo; the committed artifact would be exported JSON that no reviewer can diff meaningfully. Fails the committed-source-of-truth requirement.

  • SMIL animation inside the SVG. Self-contained and dependency-free, but deprecated in spirit, uneven in tooling, and invisible to getAnimations(), so the renderer cannot seek it. CSS on SVG reaches the same effects seekably.

Consequences

  • Zero new dependencies: the leg is Playwright + ffmpeg, both already load-bearing in the pipeline, plus roughly two hundred lines of renderer.
  • Full governance reuse. Orphan gate, freshness (now watching the diagram source file), retired-site scan, publish idempotence, manifest hashing, and the embed/autoplay contract all apply to diagrams from day one, with no parallel machinery to keep in sync.
  • Diagram renders are cluster-free and fast, so re-rendering after a brand-token change is cheap, and a future CI check could re-render and hash-compare without standing up a stack.
  • Authoring is hand-written HTML/SVG/CSS within a constrained vocabulary. That is a real skill demand on the epic, and the storyboards must design within staged-reveal/traveling-highlight motion. The PoC (isa88-four-layer-stack) is the reference implementation.
  • The clip schema now describes two kinds of clip. Load-time validation keeps them disjoint; the cost is a slightly wider schema.
  • Follow-ups deferred to the Explanation epic: a lint extending the raw-hex ban to docs/videos/diagrams/ sources, and per-page embed placement decisions (whether a mermaid block stays as the reduced-motion/print fallback beside its animated figure).

Appendix: per-diagram inventory and verdicts

Inventory of the Explanation and Standards sections, 2026-08-03. Verdicts follow the #1161 doctrine: product-surface footage first, SVG animation only for the truly abstract, static where motion adds nothing.

Existing visuals

Asset Page Verdict
22 unique UI screenshots (38 embeds, all shot-spec PNGs) adaptation-guide, parameter-binding, alarms-and-interlocks, debugging-running-batches Static stays. How-to UI references; not a migration backlog.
Mermaid: four-layer stack (flowchart TD) library/mental-models.md SVG animation (the PoC, isa88-four-layer-stack). Staged reveal down the ladder, traveling highlight along the binding edges. Epic decides whether the mermaid stays as fallback.
Mermaid: five-layer parameter binding chain library/parameter-binding.md SVG animation. The master-to-control instantiation cascade crossing CRD boundaries; no product surface shows it end to end. Reuses the PoC's ladder grammar.
Mermaid: alarm-to-operator flow library/alarms-and-interlocks.md Static stays (this wave). The page already carries the interlock-bypass clip; the operator-facing half is better served by more product footage if anything.
Video: interlock-bypass clip library/alarms-and-interlocks.md Stays. Already filmed, published, embedded.

Un-illustrated concepts (net-new visuals)

Concept Page Verdict
ISA-88 procedural hierarchy / model-mapping ladder library/mental-models.md SVG animation (the PoC).
Recipe instantiation cascade (master → control) library/parameter-binding.md, terminology.md SVG animation.
13-state machine library/mental-models.md, equipment-operation.md Both, product first. Footage of the command bar and faceplate walking Hold/Restart/Stop transitions carries the lived behavior; an SVG state-graph animation carries the full topology (transient vs quiescent states) that no surface renders whole.
Procedural SFC dialect (charts, steps, transitions) explanation/procedural-sfc-dialect.md Product-surface footage. The SFC monitor walking a live chart IS the diagram; the existing SFC clip bank (st-hold, sfc-monitor-api, three-views) proves the format.
GitOps promotion flow explanation/gitops-for-automation-engineers.md Product-surface footage. recipe-promotion-gitops and gitops-provisioning already film the real flow end to end; the epic re-embeds or recuts.
UI deployment topologies (direct / Git PR / review queue), site-to-enterprise explanation/ui-deployment-topologies.md SVG animation. Pure topology; no product surface.
Standards section (MTP, IEC 63280, NE 148, NE 175/NOA; 5 pages, zero visuals today) docs/standards/* Static stays this wave. Prose + conformance tables. An MTP/NOA architecture panel is a storyboard-time stretch candidate, SVG leg if taken.

Compliance-section visuals belong to the Compliance epic and are out of scope here.