What does Doodle do?
A code-drawn illustration system. Subjects are authored as geometry, projected through a camera, and rendered by a seeded "hand" that makes a mark look drawn. Plain HTML + JS, no build, no dependencies: open a page in a browser.
Setup: where you work
The engine ships inside this skill at engine/ (next to this file). Never edit
it there; an update of the skill overwrites it.
- If
LOCAL.mdexists next to this file, read it first: it names this machine's workspace and tools, and it overrides the default below. - Otherwise the workspace is
./.doodle(hidden) in the current project. If it does not exist, create it:cp -R "<this skill's folder>/engine" ./.doodle. - Every path below (
src/…,bin/…) is relative to the workspace. Rendering needs Chrome or Chromium (CHROME=/pathto override) and python3; nothing else.
One drawing = bin/new.sh <name>, which makes drawings/<name>.js (draw inside
draw(S), full toolkit, auto-framed) and the page that renders it. Several things in
one picture are one drawing; several separate pictures are several names.
The examples are code, not pages. The modules in src/ (things.js,
people.js, trials.js, scapes.js …) hold worked subjects: read them and copy
their construction into your drawing; never add to them or edit them. The routing
table below names which module to start from.
Deliver IMAGES, not pages. When a drawing reads, run bin/render.sh <name>: it
writes doodles/<name>/<name>.png (colour) and <name>-line.png (line only) in the
project and opens that folder for the user. Tell them the folder path. Do not build,
open or point the user at an HTML page, canvas or dial panel unless they ask for one.
This file is the whole contract, and it is short on purpose so it loads
whole. The detail lives in references/ (moved there verbatim, nothing
dropped). The ROUTING table below says which reference to read for the job.
Reading it is part of the job, not optional: every rule in those files was paid
for with a rejected drawing.
The procedure, every time
- Name the FORM and the CLASS (tables below). The form decides the page, framing and detail budget; the class decides the pen, nib, fill and tone.
- Route. Read the reference files the routing table names for your subject, and open the module it names as your starting point. Never start a subject from a blank file when a kit already exists for it.
- Measure before drawing. From a reference image: proportions and
palette, probed, never recalled. Without one: write the subject's
proportions down as numbers in the module header (the shoe needed eight
numbers; three renders without them failed).
references/hand-and-craft.md§ Measurement. - Build LINE FIRST. Judge
?outline=1before colour. Every value is carried twice, as a fill and in line. Detail falls off from focus to mid-ground to paper.references/line-first.md. - Render and LOOK, in line AND colour, at frame size. Contact sheet for composition, the large single frame for line quality. Both, every round.
- Fix in the KIT, not the subject, when the break is a helper's. Loop until every subject reads.
bin/check.sh drawings/<name>.html(no argument = every drawing): it renders, no NaN, no console errors, no class drift.- Deliver:
bin/render.sh <name>— PNGs indoodles/<name>/, folder opened. - Fold what you learned into this skill IN THE SAME TURN (§ Evolving).
Routing: what to read, where to start
| Subject | Start from | Read |
|---|---|---|
| any doodle, first time in a session | n/a | references/line-first.md, references/forms-and-classes.md |
| an everyday object (phone, keys, bottle, cloth, glasses …) | src/things.js (nearest of 20; cam, S.prism, S.on, S.dash) | things.md |
| a product, device, gadget, appliance | src/retro.js (Retro._kit) | products.md, geometry.md |
| a round object (cup, vase, fruit, bottle) | src/stilllife.js stage (lathe, ball) or src/still.js | still-life.md, geometry.md (path construction) |
| a still life / arrangement / from a photo | src/stilllife.js | still-life.md |
| furniture | src/furniture3.js, src/kitchen.js | products.md |
| an everyday object (laptop, scissors, glasses, toothbrush, hairdryer, t-shirt, soap, notebook, padlock, teddy, hanger, backpack, bottle…) | src/things.js def(...) | products.md, geometry.md |
| a PERSON, people, anyone in a scene | src/people.js People.figure; never Trials._figure (dead) | people.md |
| an animal | src/trials.js (cat) | figures-and-trials.md |
| glass, liquid, transparency | src/trials.js (glass) | figures-and-trials.md |
| a bicycle, a frame, anything of thin tubes | src/trials.js (bicycle), S.limb | figures-and-trials.md |
| a shoe, a garment, cloth | src/trials.js (sneaker, fruit cloth) | figures-and-trials.md |
| plants, flowers, leaves | src/plants3.js, src/flowers.js; holed leaves: trials.js monstera | plants.md, figures-and-trials.md |
| a building, house, facade | src/houses.js, src/arch.js, src/arch2.js | architecture.md, geometry.md |
| a landscape, outdoor view | src/scapes.js (the scene kit) | landscapes.md |
| night, dark, weather, rain | src/trials.js (night, rain) on the scene kit | figures-and-trials.md, landscapes.md |
| a street, a diorama, a café | src/cafe.js, src/scenes.js | landscapes.md, geometry.md |
| a room / interior | src/kitchen.js, src/deskscene.js | geometry.md, landscapes.md (fidelity) |
| pen and ink, no colour | src/pen.js | line-first.md (tone), forms-and-classes.md (ink) |
| a storyboard / sequence | src/board.js | storyboards.md |
| lettering, a sign | src/letter.js | pages-and-files.md |
| a screen, UI, terminal (light text on dark) | src/letter.js + tone-coloured lines | pages-and-files.md (letter.js entry) |
| make it move (Rive) | the Rive exporter (not bundled; see the file) | animation.md |
| watch it draw, underdrawing | ?draw=<key>, ?sketch=1 | hand-and-craft.md |
| clean / vector / not doodly | n/a | the clean-illustration skill if installed |
| a SET, a stress test | an existing set page as template | checking.md |
| a doodle that must LINE UP with a render (sketch → final wipe) | trace the silhouette, author the interior | registration.md |
| which page / file does what | n/a | references/pages-and-files.md |
All references/ paths are relative to this skill folder.
Forms
| They said | Form | Example to start from | Detail budget |
|---|---|---|---|
| "draw me a mug / a chair" | subject | src/things.js, src/retro.js | everything, one thing at full fidelity |
| "make me N of them" | set | one bin/new.sh drawing per item, shared helpers in each | one toolkit, detail lives in the toolkit |
| "a house / a shopfront" | elevation | src/houses.js, src/arch.js | three bays, 55% solid black fill |
| "a street / a view / a landscape" | scene | src/scapes.js, stated frame + clip | the three-rung depth ladder |
| "the whole block / a little world" | diorama | src/cafe.js | the block's edges stay IN the picture |
| "a room" | interior | src/kitchen.js, src/deskscene.js | foreground full, back wall characteristic |
| "storyboard this" | board | src/board.js | set by SHOT SIZE, not the subject |
| "pen and ink / no colour" | register ink | src/pen.js | tone carried in line, no fill at all |
A set is not a scene (things on one page share nothing but a sheet), and a board is not a set (equal fidelity loses the sequence). The tell: must the things share the SAME SPACE? Then it is a scene.
Classes (src/spec.js; full text in references/forms-and-classes.md)
| class | pen | nib | fit | fill | tone |
|---|---|---|---|---|---|
object | 0.30 | firm | .84/.84 | flat, shaded by face normal | shadow is a darker FILL, plus its line twin |
ink | 0.26 | pen | .90/.88 | NONE, the paper is the object | line only; a dot screen is forbidden |
product | 0.30 | firm | .84/.84 | one material = one colour | creases > ~35° only |
elevation | 0.30 | firm | .86/.84 | 55% solid black panes | hatch roofs, the shadow side |
plant | 0.30 | firm | .84/.88 | flat; pale shade .96, others .84 | scribble for mass foliage only |
flower | 0.26 | firm | .92/.90 | flat | none |
scene | 0.30 | firm | .96/.94 | flat; shadow a darker fill | hatch only where nothing stands in front |
diorama | 0.22 | firm | .94/.92 | flat | none |
panel | 0.26 | firm | .94/.94 | flat, less than the subject alone | one value ground, one sky |
furniture, interior | 0.16 / 0.15 | pen | — | LEGACY weights; new work takes object | — |
Declare the class, never retype the numbers: Spec.wrap(cls, label, build).
bin/check.sh audits drift on every page. A class owns the hand; a page owns
the paper. Add a class only when the fill or tone policy genuinely differs.
Hard rules (each one is a rejected drawing)
Line first and fidelity
- Line only (
?outline=1) must be a FINISHED drawing. Every page honours?outline=1; check pages you copied (one hard-codedoutline: false). - Every value is carried twice: dark material = hatch; a turn = ribs that bow with
the surface; a cast shadow = hatch with no outline; a mark that NAMES the thing =
an outlined mark, never a
w: 0fill. Audit everyw: 0. - Focus gets full detail, the mid-ground contour plus 2–3 lines, the background is PAPER (its only tone the shadow the subject throws). Squint: if the wall is as busy as the subject, cut the wall.
- Hatching is QUEUED as lines (
S.hatch), never ak.Ttone; later fills cut it.gapis the value: 2.4 light, 1.5 mid, 1.1 dark. Cross-hatch only the ONE darkest thing in the focus. - A shadow side is a CRESCENT bounded by curves that bow with the form, never a straight half-plane cut (that reads as an object split in two).
Geometry and path construction
- A straight line you did not author is a path bug: a chord from concatenating two
halves in the wrong direction, or an open shape stroked
closed. Print the points. - A band round an opaque body is its NEAR half only; only an open mouth is a full ellipse.
- Holes are CUT (
keyhole), never painted in the background colour. Hole shows in line but not in colour = its winding. - Thin things (limbs, tubes, stems) are ONE strip through their joints; outline the sides, let the ends plug into what is drawn later.
- Containers: inside → far rim → contents → front wall. Contents sink below the lip.
- Measure proportions as numbers before drawing a known object. Stretching a wrong shape keeps it wrong (the shoe).
- Size the furniture from the body that uses it, never the reverse.
- A doodle that must register with an image: TRACE its silhouette from the image's alpha, author the interior, fix the frame to the image and crop the class fit off on export (
registration.md). - People:
People.figureonly. Gesture before detail (weight on one leg, hips and shoulders tilt opposite); poses are ANGLES so proportion is locked; furniture and held things are derived from the returned joints. Back hair paints before the body. - A long straight dotted line across a soft shape = the crescent-wrap bug (fixed in
S.soft); a wavy lathe = few knots + per-segment easing: use a dense profile withsteps: 1. { dash: true }does nothing: useS.dash. Frame objects to their content bbox, not by eye.- "Scrappy" = construction, not detail: judge ONE subject at full size; outlines must JOIN (no open ends at joints), tone is tight and regular or absent, pen weight fits the form's width.
Framing
- The frame is DERIVED, never typed: on the picture-plane kits it is the FIRST
region's bounds, so the first call is
S.panel(<the frame>); with a camera, from the projection. Something not visible? Check it is inside the frame before redrawing it bigger.clip = trueclips to the stated frame. - Every ground layer runs to the frame bottom (
FOOT), or sky shows where a nearer layer dips. One ground rule reaches both edges of a sheet, not past them.
Scenes, night, weather
- Depth is a number
d(0 near, 1 far) and pen, hatch gap, colour and detail all read it. A mountain at the bench's weight is THE flat-landscape failure. - Night inverts value: the sky is the densest hatch, every light a keyholed gap; a glow is LIGHTER than the sky. A figure against light is a hatched silhouette.
- Rain is drawn last, absent under an umbrella; a reflection is the subject flipped, squashed, in broken bands, no outline.
Pages and palettes
- A page must declare every palette key its subjects use; an unknown key once rendered black silently.
- Parallel headless Chrome runs need a separate
--user-data-direach, or shots come back blank.
Verify
cd <workspace> # see Setup
bin/check.sh drawings/<name>.html # renders, no NaN, no console errors, no class drift
bin/shot.sh "drawings/<name>.html?outline=1" out.png [1120x700] # a working look; drop ?outline=1 for colour
bin/render.sh <name> # the delivery: doodles/<name>/*.png, folder opened
Chrome/Chromium is found by bin/chrome.sh (override CHROME=/path); each shot gets
its own --user-data-dir, so parallel runs are safe. Look at every PNG (open it, or
read it if you can see images). A passing check.sh proves it rendered, not that it
reads.
Evolving this skill
When you fix a gap, write it down where a FRESH session will find it (if this skill is installed read-only, keep these notes in the workspace and say so):
- The topic's reference file (
references/<topic>.md): the rule, with its symptom, stated as an instruction. Create a reference file for a new topic and add it to the ROUTING table. - This file's Hard rules, as one line, if it applies beyond one subject.
- New page, module or helper: the routing table here and
references/pages-and-files.md. LEARNINGS.md: the dated history (symptom → cause → rule). It is a log, not the instructions: it is long, sogrepit rather than read it whole. A rule that lives ONLY in LEARNINGS.md is lost to the next session; promote it. Older entries mention VDL (The author's private design canvas) and Rive projects that are not bundled; the rules in them still hold, skip those steps.
Keep this file short enough to load whole (target under ~300 lines). If it grows, move detail into a reference file and leave a one-line rule here.