Subchapter 14.2
references/anti-patterns.mdMarkdown12 KBView on GitHub
You default to these. Stop.
Each entry: the bad habit, what it produces, what to do instead. Written in the voice of someone who has watched an agent do this 10 times — because we have.
Also bundled
Gitignore.crown-plane { left: 0; right: 0; text-align: center } is the template default, and you’ll leave it on every video because it worked once. On a subject that sits right-of-center (Jobs in a 16:9 frame), the body eats the middle 60% of the word and “THE BEATLES” becomes “THE ___ S”.
Before using the default crown, run the three conditions in layout-heuristics.md § Crown placement:
If any one fails, move the crown to the larger clean zone with a narrower container and smaller font. Don’t keep centering a word that’s about to be 60% occluded.
That’s correct for left-aligned. Wrong for the templates you’re using, which are right-aligned on the main column and center-aligned on crowns. The real leftmost depends on the word width.
| Alignment | leftmost_x |
|---|---|
| Left | plane_left + padding_left |
| Right | plane_right − padding_right − longest_line_width |
| Center | plane_center − longest_line_width / 2 |
Compute it against the longest wrapped line, not the whole phrase. “four very talented guys” wraps to 3 lines — the widest is “talented” (~8 chars), not 23.
It isn’t. With left: 180px and a right-aligned 468px word, the text starts 104px before plane_left relative to the plane, but actually lands somewhere inside the plane box. The plane box just sets the coordinate space — the text positions inside it based on alignment. Check the compiled output, not the plane attribute.
Look at the subject’s body center, not the frame center. Jobs sits at x=1100 in a 1920 frame. The scene’s center of gravity is 1100, not 960. Center-aligning text to 960 is center-aligning to the empty left third, not to anything meaningful.
top: 40, right: 30, width: 720, rotateY: -13 worked for a 1280×720 frame with an acoustic foam wall on the right. It will not work on a 1920×1080 frame with a bookshelf backdrop. Run the 6-item checklist in scene-types.md for the new video before reusing any numbers.
Template defaults (66/78/92/140) are tuned for a ~560px column. When you bump the plane to 700px+ and don’t touch the fonts, the captions feel underweight — small text swimming in negative space.
Use the Font-size × column-width matrix. For a 700px column, that’s 78/108/128/220 — a 30-40% bump across the board. Re-check pillarbox safety after bumping (bigger right-aligned text extends further left).
Intro = italic, smaller, contemplative. Fine for filler discourse markers (“You know,”, “So,”, “Well,”). Not for the first line when it’s actually the thesis (“I’ve had this kind of upbringing”). Read the words semantically, not positionally.
A story without an emph or crown is typographically flat — viewers can’t feel the crescendo. Reserve at least ONE emph per video for the line that lands. Skip it only for truly monotone content (policy statements, warnings).
Style is supposed to signal hierarchy. Using intro/phrase/emph/dream/crown all in one 15-second video means none of them signal anything. Pick 2-3 styles max for a short clip.
intro / phrase / emph / dream / crown are scaffolding, not a closed set. The canonical memory-wall.html uses cap-1 / cap-2 / cap-3 / cap-4 — position-indexed with bespoke typography per position. That’s how it achieves the 3-line right-aligned cascade on the climax. You can’t express “this cap has a hanging indent at position 2” with "style": "dream".
The "style" field in plan.json accepts any string — it becomes class="cap-<string>". Define the class in custom_css:
"custom_css": ".cap-1 { font-size: 78px; ... } .cap-2 { padding-right: 44px; }",
"groups": [{"id": "cg-0", "style": "1", ...}]When the scene needs per-position bespoke typography, do this. Don’t force-fit into intro / phrase / emph.
When scene framing matches references/example-renders/memory-wall.html or champion.html, clone that HTML into <project>/index.html and only swap the GROUPS array. Don’t re-derive the design from presets — you’ll lose the specific per-position typography that took many iterations to validate. See bespoke-vs-presets.md § The clone-and-tweak workflow.
Overlay works on mid-tone backgrounds. On dark bookshelves (<60 luminance), overlay makes white text render as black. On sunny backgrounds (>180 luminance), overlay blows out. Check the actual luminance of the caption region in a sampled frame. Pick:
overlayscreennormal with opaque textYou saw a “typewriter breath” effect somewhere and added letterSpacing: "0.04em" to the fromTo “from” state. Result: inline-block reflow every frame → the whole .cap line box recomputes → captions visibly jump between rows. See the “Some → line 2” bug from memory-wall.
Animate only opacity + transform. If you want a breath effect, use scale or y, not letter-spacing.
Default feels safer: fade the container in, then fade each word in. But container.opacity × word.opacity produces a non-linear curve — at progress 40% the combined opacity is 16%, not 40%. Visible result: captions seem to “pop” into view around halfway.
Set container opacity to 1 at entrance via tl.set, then fade only the words. Single-layer opacity = linear perceived fade.
Flex-direction: column with multiple .cap children looks tidy in source. At runtime, hidden captions (visibility: hidden) still reserve flex space. The newly-entering caption shows up at the bottom of the column instead of the designed position — landing in the gesture zone, getting clipped.
Use position: absolute for each .cap inside the plane so hidden ones don’t occupy layout. Both shipped templates do this.
Caption at exactly t=0 feels like it was there before the video started. Offset 0.1-0.3s (hyperframes motion-principles.md agrees). Same for the very last caption — let it exit before the video fades.
TV archive clips cut to B-roll mid-sentence. Interview clips insert cutaways to the interviewer. You didn’t check frames beyond the first one, so you rendered captions across a shot transition. Result: captions designed for Subject A are placed relative to Subject B’s position (or empty frame) for half the render.
Sample frames at 20%, 50%, 80% BEFORE planning. If the scene changes, trim to the largest single-subject segment.
You saw black bars on the sides and didn’t computing the safe-zone. Your captions cross the pillarbox. Or: the source already has burned-in subtitles and you added more — two caption systems fighting for attention.
Run the letterbox probe first. If the source has existing captions, refuse with: “source already captioned, adding more would conflict.”
Transcription is Whisper (via transcribe.cjs, no API key) — good word timings, but not infallible: a word can land with a near-zero duration or a timestamp a beat off. This skill is verbatim + on-beat, so check-timing.cjs --strict (80ms tolerance) is the gate, not a suggestion — fix drift in plan.json before rendering, and never pack two transcript words into one timed entry (the second inherits the first’s timestamp and fires early).
It’s the Apple way, obviously faster. No. CoreML partitions the ONNX graph across providers. The mixed-precision boundary produced (observed with the previous RVM engine) alpha=30 inside the subject’s face while background correctly reads 0. Captions shine through face. Pin the CPU execution provider only (onnxruntime-node). Our matte.cjs already does this; don’t “optimize” it by re-adding CoreML.
DECISION FLIPPED 2026-06-12 after a 5-model × 6-scene A/B with caption renders: the matte’s job here is CAPTION LAYERING, not prop fidelity. u2net_human_seg (via hyperframes remove-background) usually excludes thin offset furniture (mic boom arms) from the matte — words stop being sliced by booms, which beat PP-MattingV2’s prop-preserving behavior on real caption videos. It is NOT surgical: large salient objects near the subject (telescope rigs) can still leak in — always sample frames_fg/. Known cost: HELD products can drop out intermittently (captions pass in front) — route product-demo climaxes away from held objects. isnet-general-use lost outright (backlit-hair collapse). birefnet-portrait (MIT) beat everything semantically (keeps held items AND drops furniture) but is 928 MB / ~7 s-per-frame CPU — a future quality tier, not the default.
Transcripts are verbose — “you know, um, I, I mean, you know” is 5 words from a single beat. Captioning all of them clutters the screen and breaks reading rhythm.
Editorially drop filler. Use the rules in caption-grouping.md: merge short fragments, cut repeated discourse markers, keep the meaning, trim the noise. You are writing typography to support speech, not a court transcript.
“3 words per caption, always” makes every caption look the same and fights the natural cadence of speech. Break on sentence boundaries, 250ms+ pauses, and semantic units instead. Caption sizes will naturally vary from 2 words to 5 — that’s correct.
You’ll read this file alone and feel covered. These anti-patterns reference concepts defined in layout-heuristics.md, typography-presets.md, scene-types.md. If you haven’t read those, the fix advice here won’t make sense.
Order of reading for a new video:
If you’re pressed for time, still read this one — it flags the failures you’re about to make.
This file