One prompt, eleven agents, no human art direction. Click any panel to zoom.
Given verbatim to all 11 arms, uncorrected — suppposed carries three p's, and its is missing its apostrophe:
Create a scene for a DnD campaign showing a wide shot of a fantasy village with mountains in the background. its suppposed to be Phandalin from forgotten realms
svg-mosaic — shape-first, two-loop procedural art.
Loop 1 settles the flat SVG scene as a z-ordered shape list, iterating on silhouette, value and layout only.
Loop 2 textures inside each shape independently: jittered-grid Voronoi cells clipped by that shape's own clipPath, so boundaries are owned by Loop 1 by construction.
One haiku arm, plus sonnet-5 and opus-5 each at low, medium, high, xhigh and max effort.
Effort is set in agent frontmatter, not at the call site. Every arm worked in its own directory and never saw another arm's output.
Write and Edit author and revise the scene file. Bash parse-checks the inline script with node, then drives Chrome through rodney to reload the page and take a screenshot. Read opens that screenshot, so the agent actually looks at its own render — the skill forbids shipping a scene it has not viewed.
The tok and tools figures are cost, not quality. Tool calls run roughly five to seven per iteration, so the count tracks the write–render–look–fix loop almost exactly.
---
name: svg-mosaic
description: Use when creating stained-glass, mosaic, leaded-glass, or shape-textured vector art via code — an SVG scene whose shapes are filled with tessellated texture. Triggers on stained glass, mosaic art, voronoi art, textured SVG scene, shape-first rendering, glass panel art.
---
# Shape-First SVG Mosaic
You build textured vector art in **two strictly ordered loops**: first settle a flat
SVG scene, then apply texture *inside each shape*. Never the reverse, never merged.
The scene owns every boundary; the texture is decoration that cannot move an edge.
Render in the Browser Preview and inspect screenshots between iterations — a scene
you have not looked at is not finished (same loop discipline as the pixel-art skill).
---
## Loop 1 — the scene loop (flat SVG)
Draw the whole composition as flat, untextured SVG shapes. Iterate with screenshots
until it passes:
- Silhouette: subject identifiable from shapes alone
- Three value masses at a squint; one brightest spot at the focal point
- Depth as 2-3 layers; no tangent shapes kissing edge-to-edge
**Loop 1's output is a contract** — a z-ordered shape list, painted back to front:
```js
const SHAPES = [
// id, outline path (doubles as clipPath), bbox, base colour, cell spacing
{ id: 'sky', path: 'M0 0 H256 V256 H0 Z', bbox: [0,0,256,256], col: [30,26,58], spacing: 30 },
{ id: 'moon', path: circle(200,48,20), bbox: [180,28,40,40], col: [236,232,210], spacing: 10 },
// ... spacing: 0 = this shape stays a single glass piece
];
```
Do not start Loop 2 while Loop 1 is unresolved. If the composition fails later,
back up to Loop 1 — do not fix composition with texture.
### Icon libraries as silhouette donors
Icon sets are SVG, so a `<path>` from one drops straight into the shape list.
Use them for **props and celestial objects** — moon, sun, cloud, flame, tree,
mountain, crown, key, anchor, shield. Do not expect them for **posed figures**;
no set has "knight lunging with couched lance", so action poses stay hand-drawn.
**Check the fill style first.** Lucide, Feather, Tabler, and Heroicons-outline are
stroke-based (`fill="none" stroke-width="2"`). A `clipPath` treats that path as a
*fill*, giving a degenerate sliver, not the icon — the method needs closed filled
regions. Reach for filled single-path sets instead: Font Awesome Solid, Material
Symbols Filled, Bootstrap Icons `*-fill`, Heroicons Solid, and **game-icons.net**
(4000+ filled fantasy icons — gryphons, helms, lances; CC BY).
**A donated icon is a starting silhouette, not finished art.** It is SVG like
everything else, so edit it: delete subpaths that carry detail the mosaic will
supply anyway, simplify a busy contour, stretch or mirror it, splice two icons
into one shape. Judge the result by Loop 1's checklist, not by the icon's polish.
**Bake the transform into the path data.** Every set has its own viewBox (24, 512).
`clipPath` plus a `transform` is fiddly; pre-scale the coordinates so the shape's
path and bbox live in your scene's coordinate space like any hand-authored shape.
Attribution follows the set's licence (CC BY for Font Awesome Free and game-icons).
---
## Loop 2 — the texture loop (per shape)
For each shape independently:
1. Scatter **jittered-grid seeds** across the shape's bbox (deterministic RNG, seeded per shape id).
2. Compute Voronoi cells (half-plane clipping against the bbox — code below).
3. Emit cells inside `<g clip-path="url(#clip-<id>)">` — the browser trims them to the outline.
4. Under the cells, draw a **backing plate** (the shape filled in its base colour) to close hairline gaps at the clip edge.
5. Over the cells, restroke the **shape outline heavy** (~2.4px) — Loop 1's drawing, preserved as the main lead line. Cells get thin strokes (~1.4px).
Iterate only on texture: cell spacing, tint jitter, stroke weights. One fix per
iteration, screenshot each time.
### Loop 2 is optional — mosaic on by default
Because the scene is a standalone contract, Loop 2 is a switch, not a stage. Gate
it on a URL param so the same file emits either output — mosaic unless asked
otherwise, flat SVG on request:
```js
const FLAT = new URLSearchParams(location.search).get('flat') === '1';
// ... inside the per-shape loop:
if (FLAT || !sh.spacing) {
body += '<path d="' + sh.path + '" fill="' + fillFor(1) + '" stroke="#0a0910" stroke-width="2"/>';
continue; // flat piece — no clipPath, no cells
}
```
This costs one branch and pays twice: `?flat=1` is also how you inspect Loop 1
during its own iterations, so the flat renderer is not extra code — it is the
Loop 1 harness, kept.
Deliver mosaic by default. Hand over flat SVG only when the user asks for plain
vector output, or when the piece is destined for further editing.
### Rules
- **Boundaries by construction.** All colour edges come from clipPath, never from
where tessellation cells happen to fall.
- **Density is a per-shape knob.** Big panes in large calm areas, fine facets at the
focal point, `spacing: 0` single pieces for small features (windows, doors).
- **Deterministic seeds.** Same input, same picture, every reload.
- **Tint jitter, not hue jitter.** Per-cell brightness `0.86 + ((i*13)%7)*0.05`
around the base colour reads as glass; random hues read as confetti.
### Voronoi + emission core
```js
function voronoiCells(seeds, [bx, by, bw, bh]) {
const cells = [];
for (let i = 0; i < seeds.length; i++) {
let poly = [[bx,by],[bx+bw,by],[bx+bw,by+bh],[bx,by+bh]];
const [ax, ay] = seeds[i];
for (let j = 0; j < seeds.length && poly.length; j++) {
if (j === i) continue;
const [sx, sy] = seeds[j];
const dx = sx-ax, dy = sy-ay, c = (sx*sx+sy*sy-ax*ax-ay*ay)/2;
const inside = (x, y) => dx*x + dy*y <= c;
const out = [];
for (let k = 0; k < poly.length; k++) {
const [x1,y1] = poly[k], [x2,y2] = poly[(k+1)%poly.length];
const in1 = inside(x1,y1), in2 = inside(x2,y2);
if (in1) out.push([x1,y1]);
if (in1 !== in2) {
const t = (c - dx*x1 - dy*y1) / (dx*(x2-x1) + dy*(y2-y1));
out.push([x1+(x2-x1)*t, y1+(y2-y1)*t]);
}
}
poly = out;
}
if (poly.length >= 3) cells.push(poly);
}
return cells;
}
// seeds: jittered grid over bbox
for (let gy = by + sp/2; gy < by + bh; gy += sp)
for (let gx = bx + sp/2; gx < bx + bw; gx += sp)
seeds.push([gx + (R()-0.5)*sp*0.9, gy + (R()-0.5)*sp*0.9]);
```
Circle outlines as paths (clipPath needs paths, not `<circle>` when mixing):
```js
const circle = (cx, cy, r) =>
'M'+(cx-r)+' '+cy+' a'+r+' '+r+' 0 1 0 '+(2*r)+' 0 a'+r+' '+r+' 0 1 0 '+(-2*r)+' 0 Z';
```
---
## Verification
- **Syntax-check the inline script with `node` + `vm.Script` before every browser load** —
a parse error renders as a silently black page.
- Screenshot after every iteration. The Browser pane renders `file://` as a static
no-JS snapshot — serve over HTTP (`python3 -m http.server`).
- Display a piece count + shape count caption; a sudden piece-count change flags a
broken shape.
## Anti-patterns — reject on sight
- Global tessellation over the whole canvas, cells coloured by sampling the scene —
produces ragged emergent edges that then need containment hacks
- Texturing before the flat scene passes inspection
- Fixing composition problems inside Loop 2
- `Math.random()` anywhere
- Skipping the backing plate (hairline background-coloured cracks at clip edges)
- Hue-randomised cells
- Animating seed positions or re-tessellating per frame — cell boundaries crawl
- Smooth fill fades or shimmer on every cell — glass glints, it does not pulse
- Feeding a **stroke-based** icon path to `clipPath` — clips to a sliver, not the icon
- Dropping an icon in unedited and calling Loop 1 done — it is a silhouette, not art
- Separate feather/petal shapes laid over a smooth outline — they read as loose
confetti; put shallow lobes on the outline itself instead
## Animation — glass shimmer
Geometry is frozen after Loop 2; the only thing that may animate is **per-cell tint**
(colour cycling transplanted from pixel art: values move through fixed cells).
Never animate seed positions or re-tessellate per frame — boundaries crawl.
SMIL, no JS loop, stepped so the flicker reads as glass catching light:
```js
// inside the cell loop - shimmer only a sparse subset (~1 in 4), or only the
// light-source shape (moon, windows); everything shimmering reads as static
if (sh.shimmer && i % 4 === 0) {
const dur = 3.2, phase = -((i * 13) % 7) / 7 * dur;
cell = '<polygon points="' + pts + '" fill="' + fillFor(j) + '" stroke="#0a0910" stroke-width="1.4">' +
'<animate attributeName="fill" values="' + fillFor(j) + ';' + fillFor(j + 0.14) + ';' + fillFor(j) + '"' +
' dur="' + dur + 's" begin="' + phase.toFixed(2) + 's" repeatCount="indefinite" calcMode="discrete"/>' +
'</polygon>';
}
```
- `calcMode="discrete"` — stepped, not smooth fades; choppiness is intentional
(same 8-12fps doctrine as the pixel-art skill)
- Amplitude small (~+0.14 brightness) except on the light source itself
- Deterministic phases from the cell index — no `Math.random()`
- 1-2 shimmering shapes per scene, not every shape
## Generalising the texture
Loop 2's tessellation is swappable while everything else holds: hatching strokes,
halftone dots, Truchet tiles, or brick courses inside each clipPath. The method is
"structure loop, then texture loop" — mosaic is just the proven instance.
## Reporting
Use the pixel-art skill's iteration format: `ITERATION n / Rendered / Observed /
Changing`, and never `DONE` without a render viewed this turn.