---
layout: "article.njk"
title: "The Doodle Architect: How Wrist Physics and SVG Shaders Solved the LLM Illustration Problem"
description: "Why LLMs produce sterile vector slop when asked to sketch, the mathematical kinematics of hand-drawn lines, and the zero-JS evaluation stack we built for authentic sketchnotes."
date: "2026-09-01"
tags: ["AI", "LLMs", "SVG", "Architecture", "Design", "Graphics"]
featured: true
permalink: "/articles/doodle-svg-architect-hand-drawn-systems/"
eleventyExcludeFromCollections: false
published: true
---

When you ask a frontier model to "draw a quick napkin diagram" or "sketch a whiteboard architecture," you almost always receive one of two catastrophic failure modes:

1. **Sterile Computer Geometry:** Perfectly flat `<rect>` and `<circle>` elements with razor-sharp corners and bucket fills that look like 1995 clip art.
2. **Bézier Spaghetti:** Erratic spline control points oscillating wildly with unnatural cusps and pinch points.

Just as we discovered when building the [Illustrated Room](/articles/building-the-illustrated-room-spatial-reasoning-llms/), models fail not because they lack vector syntax knowledge, but because **autoregressive token prediction has no built-in kinematics of human motor control**.

To solve this, we built **`doodle-svg-architect`**—an agent skill and dual-tier evaluation harness that enforces wrist-bowing physics, offset watercolor washes, and zero-JS GPU paper shaders.

---

# 1. The Physics of Human Inking

A human hand with a fountain pen does not emit Euclidean line segments. Authentic sketches exhibit four non-negotiable physical traits:

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                          THE FOUR PHYSICAL LAWS                             │
│                                                                             │
│  1. Endpoint Overshoot   ──► Pen lands past the vertex (±2-3px)             │
│  2. Wrist Bowing         ──► Biomechanics create chord-normal arc curvature │
│  3. Dual-Pass Retracing  ──► Instinctive second pass creates sketchy depth  │
│  4. Offset Marker Wash   ──► Color wash bleeds and shifts outside contours  │
└─────────────────────────────────────────────────────────────────────────────┘
```

### The Wood et al. Bowing Formulation
Based on the empirical visualization research of Wood, Isenberg, et al. (2012), our skill models segment bowing using chord-normal displacement:

For a line between $P_1(x_1, y_1)$ and $P_2(x_2, y_2)$ with length $L$:
$$\mathbf{u} = \left(\frac{\Delta x}{L}, \frac{\Delta y}{L}\right), \quad \mathbf{n} = \left(-\mathbf{u}_y, \mathbf{u}_x\right)$$

The midpoint target displaces along the normal:
$$d_{\text{bow}} = \text{bowing} \cdot \left(\frac{L}{200}\right) \cdot \mathcal{N}(0, 1)$$

Cubic Bézier control points at $1/3$ and $2/3$ intervals inherit this bowing with stochastic jitter:
$$CP_1 = P_1' + \frac{1}{3}(P_2' - P_1') + d_{\text{bow}}\cdot\mathbf{n} + \mathbf{J}_1$$
$$CP_2 = P_1' + \frac{2}{3}(P_2' - P_1') + d_{\text{bow}}\cdot\mathbf{n} + \mathbf{J}_2$$

---

# 2. Zero-JS GPU Paper Shaders

Rather than bundling heavy client-side JavaScript runtimes (like canvas libraries), the entire aesthetic is rendered natively by the browser's GPU using declarative SVG filter graphs:

```xml
<defs>
  <!-- Tactile Cotton-Rag Paper Texture -->
  <filter id="paper-texture" x="0%" y="0%" width="100%" height="100%">
    <feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="4" result="noise" />
    <feDiffuseLighting in="noise" lighting-color="#fef6e4" surfaceScale="2" diffuseConstant="1.1" result="light">
      <feDistantLight azimuth="55" elevation="45" />
    </feDiffuseLighting>
    <feBlend mode="multiply" in="SourceGraphic" in2="light" />
  </filter>

  <!-- Hand-Drawn Stroke Jitter -->
  <filter id="sketch-stroke" x="-20%" y="-20%" width="140%" height="140%">
    <feTurbulence type="fractalNoise" baseFrequency="0.06 0.04" numOctaves="3" seed="42" result="noise" />
    <feDisplacementMap in="SourceGraphic" in2="noise" scale="3.5" xChannelSelector="R" yChannelSelector="G" />
  </filter>
</defs>
```

---

# 3. The Dual-Tier Evaluation Loop

No illustration is shipped without running `eval_doodle.py`:

```bash
python3 scripts/eval_doodle.py <file>.html --render
```

1. **Tier 1 (Deterministic AST Linter):**
   - Strictly enforces the `<!-- PALETTE: ... -->` color contract (zero stray hex codes).
   - Verifies line weight hierarchy (at least 2 distinct stroke tiers: 3.5px bold contour, 1.5px fine detail).
   - Enforces DOM phase layering (semi-translucent washes MUST render in `<g id="phase-2-washes">` behind ink strokes).
   - Rejects unstyled raw geometric shapes.
2. **Tier 2 & 3 (Headless Pixel & LLM-as-Judge):**
   - Captures headless Chrome render at 2x resolution.
   - Evaluates 3-second cognitive clarity, hand-drawn vitality, and visual metaphor wit.

---

# 4. Live Exemplar: Autonomous Agent Sketchnote

Below is an authentic, pure SVG sketchnote generated via the `doodle-svg-architect` pipeline:

<div class="my-8 rounded-lg overflow-hidden border border-[#001858]/20 shadow-lg">
  <svg viewBox="0 0 1200 800" width="100%" height="auto" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <filter id="demo-paper-texture" x="0%" y="0%" width="100%" height="100%">
        <feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="4" result="paperNoise" />
        <feDiffuseLighting in="paperNoise" lighting-color="#fef6e4" surfaceScale="2" diffuseConstant="1.1" result="paperLight">
          <feDistantLight azimuth="55" elevation="45" />
        </feDiffuseLighting>
        <feBlend mode="multiply" in="SourceGraphic" in2="paperLight" />
      </filter>
      <filter id="demo-sketch-stroke" x="-20%" y="-20%" width="140%" height="140%">
        <feTurbulence type="fractalNoise" baseFrequency="0.06 0.04" numOctaves="3" seed="42" result="noise" />
        <feDisplacementMap in="SourceGraphic" in2="noise" scale="3" xChannelSelector="R" yChannelSelector="G" />
      </filter>
      <filter id="demo-watercolor-wash" x="-20%" y="-20%" width="140%" height="140%">
        <feTurbulence type="fractalNoise" baseFrequency="0.02" numOctaves="3" seed="17" result="warp" />
        <feDisplacementMap in="SourceGraphic" in2="warp" scale="8" result="warped" />
        <feGaussianBlur in="warped" stdDeviation="1" />
      </filter>
    </defs>

    <g id="phase-1-substrate">
      <rect width="100%" height="100%" fill="#fef6e4" filter="url(#demo-paper-texture)" />
      <path d="M 30 35 Q 600 28 1170 32 Q 1174 400 1168 768 Q 600 772 32 765 Q 26 400 30 35" 
            fill="none" stroke="#001858" stroke-width="2" opacity="0.25" stroke-linecap="round" />
    </g>

    <g id="phase-2-washes">
      <path d="M 284 48 Q 600 42 916 46 L 908 114 Q 596 118 280 110 Z" fill="#f4d06f" opacity="0.55" filter="url(#demo-watercolor-wash)" />
      <path d="M 76 226 Q 190 220 306 228 Q 312 370 302 466 Q 186 474 72 462 Z" fill="#8bd3dd" opacity="0.32" filter="url(#demo-watercolor-wash)" />
      <path d="M 444 196 Q 600 188 756 198 Q 764 390 754 514 Q 596 524 440 512 Z" fill="#f582ae" opacity="0.35" filter="url(#demo-watercolor-wash)" />
      <path d="M 894 226 Q 1010 220 1124 228 Q 1130 370 1122 466 Q 1006 474 890 462 Z" fill="#8bd3dd" opacity="0.32" filter="url(#demo-watercolor-wash)" />
      <path d="M 284 596 Q 600 588 916 598 Q 922 690 912 734 Q 596 742 280 732 Z" fill="#f4d06f" opacity="0.35" filter="url(#demo-watercolor-wash)" />
      <path d="M 944 606 Q 1040 600 1136 608 Q 1140 680 1134 724 Q 1038 730 940 722 Z" fill="#f582ae" opacity="0.45" filter="url(#demo-watercolor-wash)" />
    </g>

    <g id="phase-4-strokes">
      <path d="M 280 44 Q 600 38 920 42 L 910 108 Q 600 114 274 104 Z" fill="#ffffff" stroke="#001858" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
      <path d="M 70 220 Q 190 214 310 222 Q 316 360 308 460 Q 186 468 68 456 Q 64 358 70 220" fill="#ffffff" stroke="#001858" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
      <path d="M 436 190 Q 600 182 764 192 Q 772 380 760 510 Q 596 520 432 506 Q 428 378 436 190" fill="#ffffff" stroke="#001858" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
      <path d="M 888 220 Q 1010 214 1130 222 Q 1136 360 1128 460 Q 1006 468 884 456 Q 880 358 888 220" fill="#ffffff" stroke="#001858" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
      <path d="M 276 590 Q 600 582 924 592 Q 930 680 920 730 Q 596 738 272 728 Q 268 678 276 590" fill="#ffffff" stroke="#001858" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" />
    </g>

    <g id="phase-5-connectors">
      <path d="M 312 310 Q 374 290 430 310" fill="none" stroke="#001858" stroke-width="3" stroke-linecap="round" />
      <path d="M 412 300 L 434 311 L 410 322" fill="none" stroke="#001858" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
      <path d="M 768 310 Q 828 290 882 310" fill="none" stroke="#001858" stroke-width="3" stroke-linecap="round" />
      <path d="M 864 300 L 886 311 L 862 322" fill="none" stroke="#001858" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
      <path d="M 560 514 Q 540 550 560 584" fill="none" stroke="#001858" stroke-width="3" stroke-linecap="round" />
      <path d="M 550 568 L 561 588 L 572 566" fill="none" stroke="#001858" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
    </g>

    <g id="phase-6-lettering" style="font-family: 'Caveat', cursive, sans-serif;">
      <text x="600" y="80" text-anchor="middle" font-size="34px" font-weight="700" fill="#001858">AUTONOMOUS AGENT WORKFLOW</text>
      <text x="600" y="148" text-anchor="middle" font-size="22px" font-weight="600" fill="#001858" opacity="0.85">Deterministic Reasoning · Tool Grounding · Compounding Loops</text>
      <text x="190" y="256" text-anchor="middle" font-size="20px" font-weight="700" fill="#001858">1. Ingestion</text>
      <text x="600" y="232" text-anchor="middle" font-size="26px" font-weight="700" fill="#001858">2. Orchestrator (Star)</text>
      <text x="1010" y="256" text-anchor="middle" font-size="20px" font-weight="700" fill="#001858">3. Memory Vault</text>
      <text x="600" y="625" text-anchor="middle" font-size="20px" font-weight="700" fill="#001858">4. Model Context Protocol (MCP) Workers</text>
    </g>
  </svg>
</div>

---

# What's Next

The complete skill, reference library, and evaluation tool are packaged in `.agents/skills/doodle-svg-architect/`. Any agent operating in this codebase can now activate the skill to generate whiteboard diagrams, system explainers, and interactive doodle components with zero geometric slop.
