Display Doctrine Doc, 31/07/2026 update
What this page is for. The technical reference behind /backdata/display-doctrine: the render pipeline and the craft conventions that came out of building the EO v2 long-form video diagrams, most recently the Trauma Gut identity and the two-walls tension piece. Read by Claude at the start of any session that touches a v2 diagram HTML file. This is the mechanics; the doctrine page is the visual quick-reference.
Scope. This page documents what was actually built and learned across the Trauma Gut / tension-diagram / multi-format sessions. It is not a rewrite of the pre-existing v2 diagram grammar (closed palette, 6 TD families, the v2-canonical/ HTML masters) — it complements that system with the render-pipeline mechanics and the newer precision/format techniques, and should be extended in place as more diagrams are built rather than superseded by a second doc.
Table of contents
1. What this system is for
EO long-form videos carry concept diagrams (Double Character Crossover, The Loop, Survivor's Journey, Trauma Gut, and others) that need to: render deterministically, frame by frame, on pure black so FCPX can composite them with Blend Mode: Screen; hold one consistent visual identity across a growing catalog built over many separate sessions; and iterate fast from HTML/CSS/SVG instead of manual After Effects keyframing. The pipeline and conventions below exist to make a diagram built today look and move like one built months ago.
2. The render pipeline
Every diagram is a self-contained HTML file (inline CSS/JS, no build step) rendered by visuals/reference-html/_render/render-html-to-mov.js:
- Playwright loads the file, waits for fonts, then reads every animation via
document.getAnimations(). - Total duration =
max(delay + activeDuration)across all animations on the page, plus a hold (holdMs, default 600ms) on the final frame. - All animations are paused, then
currentTimeis set frame by frame (30fps default) and each frame is screenshotted. - ffmpeg stitches the frames — h264 default (10x lighter, visually identical for these compositions), ProRes 422 HQ available on explicit need.
Hard constraint: every animation must resolve to a finite duration. animation-iteration-count: infinite makes step 2 compute Infinity and breaks the whole render. Any continuous motif (a shake, a sway, a sweeping pulse) must ship with a bounded — possibly fractional — iteration count. See §4.3.
NODE_PATH=~/Projects/eliteoutsiders-site/node_modules \ node render-html-to-mov.js <input.html> <output.mov> [fps=30] [holdMs=600] [codec=h264] [width=1920] [height=1080]
Before any real render: open the file headlessly, call document.getAnimations(), and confirm every entry has a finite activeDuration. This check is cheap and catches the infinite-loop mistake before it wastes a render.
3. Palette and Trauma Gut identity
Closed palette, no colors outside this set:
3.1 The Trauma Gut cell — a reused signature, not a one-off
First built for the double-character-crossover square variant, then reused in its "bis" EKG-pulse variant and in the two-walls tension diagram. The ellipse + hatch + label are identical across all three; the drop-shadow glow below is NOT — it only exists on the two-walls tension diagram, added when that piece's Trauma Gut treatment was deliberately elevated. Treat it as an optional 4th layer to add when a diagram calls for more visual weight, not an automatic part of the signature yet.
- Ellipse, gold stroke
#e8d5b0width 3, opaque black base (fill-opacity0.82–0.9). Present on all three. - Gold diagonal hatch fill on top: a 10×10
<pattern>,patternTransform="rotate(45)", single 1.3px line atstroke-opacity 0.35. Present on all three. - Label text: Inter 500, white, uppercase, letter-spacing 0.05em, centered. Present on all three.
- Soft gold glow: two stacked
drop-shadows,0 0 22px rgba(232,213,176,0.55)+0 0 8px rgba(232,213,176,0.5). Two-walls tension diagram only — not present on the double-character variants.
4. Animation conventions
4.1 Entrance → Hold → Exit
The standard structure for a diagram's lifetime: elements fade or drop in with staggered delays (a wall settles, then the Trauma Gut cell drops onto the already-visible composition and lands with a soft bounce, then connecting lines fade in) — the composition holds fully assembled for the "message" duration — then everything fades out together, via one wrapper element's opacity animation, not each element separately. This "systematic fade-out" was tested on one file, validated by AL, then adopted as standard rather than left as a one-off.
4.2 The drop-in bounce (reused keyframe shape)
The Trauma Gut cell's entrance is the same 4-keyframe bounce every time it appears, applied identically to both the SVG group and its paired HTML text overlay so they move as one unit:
0% opacity 0, translateY(-300px) scale(0.92) 60% opacity 1, translateY(10px) scale(1.03) 80% translateY(-4px) scale(0.99) 100% opacity 1, translateY(0) scale(1)
4.3 Bounded loops for continuous motifs
A shake, sway, or pulse still needs a real, seamless-looking cycle, but the render pipeline needs a finite duration (§2). The fix: give the motif a normal keyframe period, then set animation-iteration-count to a fractional value equal to (target hold duration) / (period). The loop then ends exactly when the hold ends (when the fade-out starts), with zero wasted render tail and no visible pre-fade freeze. Example from the two-walls tension diagram (fade-out starts at 9.0s): a 2.6s shake period gets iteration-count: 3.462, a 3.2s sway period gets 2.8125, etc. — each computed as 9.0 / period.
4.4 "Alive before visible"
Start the continuous loop animation with no delay on an element whose opacity is separately delayed/faded in. By the time the element becomes visible it is already mid-cycle, so it reads as already-alive rather than starting from a dead rest pose. Two animations (one for transform, one for opacity) coexist fine in one comma-separated animation shorthand since they target different properties.
4.5 Variable-speed sweeps
For a motif that should change pace over time (the EKG pulse on the Trauma Gut bis variant: AL wanted it to decelerate and return to speed, repeatedly), chain several single-iteration entries of the same keyframe at different durations/delays inside one animation list, e.g. normal (2.4s) → normal (2.4s) → slow (5.1s) → normal (2.4s) → slow (5.1s) → normal (2.4s). Each entry's delay starts exactly where the previous one's duration ends, so the sweep resets and re-paces without a gap.
5. Precision discipline
Standing rule for anything geometric in this system: measure the real rendered output, don't trust the arithmetic alone. Concretely:
5.1 Rotation formula
To place a point on a rotated shape's edge, rotate it explicitly around the shape's own pivot rather than eyeballing it:
x' = cx + (x-cx)·cos(θ) - (y-cy)·sin(θ) y' = cy + (x-cx)·sin(θ) + (y-cy)·cos(θ)
5.2 Measure with getScreenCTM(), not code review
element.getScreenCTM() + SVGPoint.matrixTransform() reads the actual rendered position/angle. This caught a real bug that pure code-reading missed: a static SVG transform="rotate(9 …)" attribute left on an inner group, stacked with a CSS animation that also rotated by 9° from the same rest angle — the two composed additively, so the wall was actually rendering at 18°, not 9°. Fixed by keeping exactly one rotation source (the CSS animation, via transform-box:fill-box; transform-origin:center) and removing the static attribute.
Measure at the construction angle, not mid-cycle. A shake/sway animation never actually holds still at its base angle except at the exact instant its cycle passes through the 0%/100% keyframe (e.g. t=0, before any per-element delay has run). Checking touching-precision at a random mid-animation timestamp will show a real but expected few-pixel "wobble" — that is the shake doing its job, not a construction error. Re-check at the rest angle before concluding there is a bug.
5.3 Gradient units
gradientUnits="userSpaceOnUse" with explicit pixel x1/y1/x2/y2 is more predictable than the default objectBoundingBox on thin or rotated shapes — a default-units gradient rendered fully invisible on a thin line during the web-strand exploration. If the shape is only shallowly diagonal, a purely horizontal gradient vector (same y1/y2) is a safe simplification: only the x-projection matters when y1=y2.
5.4 Tapered strand construction
A "cable" that needs taper + color gradient + gentle sway (the web-strands holding the two walls) is not a stroked <line> — it is a filled quadrilateral <path> with two fixed points at the thick (wall) end and two animatable points at the thin (cell) end, offset perpendicular to the line's own direction: perp = (-dy/L, dx/L) · halfWidth. This is what makes taper, gradient fill, and a swaying thin end all coexist on what still reads as a single line.
6. Multi-format adaptation
Added 31/07/2026: the same diagram increasingly needs to serve more than one video aspect ratio.
6.1 Square (1:1) re-layout
Not a non-uniform stretch of the 16:9 art. When only the width changes (canvas height carried over unchanged), every vertical coordinate is reused as-is; only the horizontal budget (margins, element widths, gaps, center-element diameter) is redistributed at the same proportions as the original, and every rotated anchor point is recomputed from scratch via the rotation formula (§5.1) against the new pivot. Verified the same way as the original: touching-precision measured via getScreenCTM(), not assumed from the arithmetic.
6.2 Horizontal band, centered content
For a single source to survive being center-cropped to either a long (16:9) or short (1:1 / 9:16) cut, render at a wide canvas (e.g. 1920×1080) with the square composition centered horizontally inside it — a pure translation of the already-verified square geometry (shift the <svg>'s CSS left and every absolutely-positioned HTML overlay by the same offset), not a re-derivation. Zero new geometry risk, because nothing internal to the square composition changes.
6.3 Ambient background glow
Tested on the two-walls tension diagram: flat black background replaced by a soft radial gold gradient, centered on the diagram's own light-source element (the Trauma Gut cell) rather than the canvas geometric center, so the ambient light reads as emanating from something already gold-glowing in the piece. First pass kept the falloff tight (transparent by ~72% of a 900×650px radius) — AL's reaction was to push it wider so the tint reaches every corner of the frame rather than reading as a spotlight on the subject with the rest of the canvas still plain black (see quick-reference). Shipped values: radial-gradient(ellipse 1500px 1050px at 960px 430px, rgba(232,213,176,0.09) 0%, rgba(232,213,176,0.05) 40%, rgba(232,213,176,0.026) 75%, rgba(232,213,176,0.014) 100%) — peak opacity still subtle (9%, deliberately under the older glow referenced from 3.1-trigger-down-state) but never fully transparent, down to ~1.4% at the frame edge. Status: validated by AL 31/07/2026 ("that's what i'm talking about"), shipped as the "10ter" band+glow variant of the two-walls tension diagram. Not yet generalized to the rest of the catalog.
7. Files, naming, locations
| What | Where |
|---|---|
| Source HTML (diagrams) | Elite Outsiders/visuals/reference-html/v2-diagrams/*.html in iCloud Drive — plain files, no build step, no repo. Filenames are descriptive, never number-prefixed. |
| Render script | Elite Outsiders/visuals/reference-html/_render/render-html-to-mov.js (+ README.md alongside it) |
| Rendered output | Elite Outsiders/visuals/mov/*.mov — the catalog NUMBER prefix (e.g. 9.2-what-they-see.mov, 10-trauma-gut-tension.mov, 10bis-… for a format variant) lives on the output filename only. |
| Design exploration archive | Elite Outsiders/visuals/design-inspiration/ — comparison sheets shown to AL before a creative decision was finalized (e.g. the Trauma Gut arrow-tip options). Kept rather than discarded; long-term seed for a possible future tools.eliteoutsiders.com library page, not yet scoped or built. |
| This doc pair | public/backdata/display-doctrine.html (visual quick-reference) + public/backdata/display-doctrine-doc.html (this page) |
8. Hard rules
- Never invent an unvalidated diagram title. No title text ships until AL has given or approved the exact wording.
- Replace the .mov after every fix, without being asked. A fix to a diagram's source HTML is not done until the corresponding output in
visuals/mov/has been re-rendered and overwritten. - Test a new treatment on one file before generalizing it. The systematic fade-out and the ambient background glow were both proposed, shown on a single diagram, and only get applied catalog-wide after AL validates the one example.
- Show options before delivering a final creative call. For a genuinely new/ambiguous visual decision (an arrow tip, a strand style), present several concrete side-by-side options rather than a single guess — this repo's "show, don't just tell" pattern for anything more than a mechanical fix.
- English-only on this site. Everything on this page and its companion is English (repo rule 9), even though the source conversation and design decisions happen in French chat with AL.
9. Changelog
- 31/07/2026 (first ship) Page created from the Trauma Gut / two-walls tension / multi-format session: render pipeline mechanics, palette + Trauma Gut identity spec, animation conventions (beats, bounded loops, alive-before-visible, variable-speed sweeps), precision discipline (rotation formula, CTM measurement, gradient units, tapered strands), multi-format adaptation (square re-layout, horizontal band, ambient glow test), file/naming conventions, hard rules. Companion visual page at /backdata/display-doctrine.