Drawings · For your coding agent
Drawing skill
The kit draws 85 objects; your project will want ones it doesn’t have. The drawing skill teaches your coding agent to draw them in the same pen, from a seed, in your own code.
- Watering can
- Carrying a parcel
Install
Next to the kit, from the same registry. It lands in .claude/skills and .agents/skills, for Claude Code, Codex and any agent that reads skills.
npx shadcn add @fineliner/drawing-skillOr copy it from the repository. Then ask your agent for the drawing you need.
Ask in your own words
“Our booking app has no bicycle drawing. Add one in the same style for the empty state, above ‘No bookings yet’.”
An object
<Drawing object="bicycle" />“One of those Fineliner people, but walking a dog on a lead. It has to look like the kit’s other people, not clip art.”
A person in a new pose
<Drawing object="dog-walker" />“A header drawing of our little roastery: a drum roaster, a few sacks of beans, a scale, somebody working there.”
A scene
<Drawing scene="roastery" />
What your agent does with it
- Draws in your module, not the kit’s
- New drawings go in src/drawings, registered with define() and defineScene(), so updating the kit never loses them.
- Builds from the kit’s parts
- Boxes, rounds and cylinders; people from the kit’s clothes and hair; scenes on the kit’s stage, walls and drawn border.
- Follows the style
- One line, calm 3/4 views, corners that meet, a few pockets of ink, and every choice taken from the seed.
- Looks before it reports
- It renders a contact sheet at several seeds and both hands, and checks it against the list the kit’s own drawings were reviewed with.
SKILL.md
The skill, in full
Fineliner’s drawings are not image files. Each one is a small TypeScript function that tells a seeded pen where to go: the same seed draws the same picture on the server and in the browser, and a different seed draws a sibling with its own small differences. A project grows the library by writing more of these functions in its own files and registering them, the same way the kit registers its own objects and scenes.
Your job is to draw what the project needs so that it sits on the page next to the kit’s own drawings as if the same person had drawn it: one thin wobbly line, calm 3/4 views, corners that meet, a few pockets of solid ink, nothing more.
Before you draw
- Find the kit. It lives in
src/fineliner/(installed byshadcn add); the engine issrc/fineliner/pen/. Look atsrc/fineliner/pen/objects/*.ts: every kit object is there (on an older kit, before the collections split), and the closest one to what you’re asked for is your best guide (a bicycle starts from how the chair and desk lamp are built; a bakery counter from the desk and the bistro table). Readreferences/style.mdonce, before the first drawing of a session. - Check it doesn’t exist. The ids in
objects/*.ts, orlistObjects()once the collections are imported. Sometimes the answer is an existing object with an option (windowtakesview,platetakesdish,persontakesposeandhair). - Decide what it is. An object (one thing on its own tile, placed in scenes too), a person in a new pose, or a scene (a room composed from objects inside the drawn border). Know where it will appear and how big: a 48 px empty-state spot needs fewer, larger parts than a hero scene.
Where the code goes
Write the drawings in the project’s own module, never inside src/fineliner/. The kit’s files are replaced when the
project runs shadcn add again to update, and anything added to them is lost.
src/drawings/index.ts ← your objects and scenes, registered on import
Import that module (import "@/drawings") at the top of every file that renders <Drawing> with one of your ids, or
calls drawing() with one. Registration happens when the module runs, into one library per JavaScript realm, so a Next.js server component
that imports it covers the server render of a client <Drawing> too. The browser needs it only to draw there (a reroll,
line boil’s later takes): import it in a client component as well for those. A wrong or missing
import shows up as the error No drawing object "your-id".
The kit’s own drawings register the same way, one collection at a time: @/fineliner/pen is the engine only. Import
the collections your module places or your pages show (import "@/fineliner/pen/objects/indie", or every one with
@/fineliner/pen/objects, and the kit’s scenes with @/fineliner/pen/objects/rooms), and install them if the project
lacks them (npx shadcn add @fineliner/drawings-indie). A kit without src/fineliner/pen/objects/indie.ts predates the
split: there the engine registers everything itself.
assets/example-drawings.ts is a complete, working module (a watering can, a person carrying a parcel, and a shop
counter scene). Copy its shape.
Drawing an object
Read references/engine.md for the API. The essentials:
import {
define,
objectsIn,
depth,
box,
type Draw,
type Point,
} from "@/fineliner/pen"
/** A wooden step stool. `at` is where it stands (bottom-left); k is its scale. */
export const stool: Draw = (s, p, r, at, k = 1) => {
const o = depth(r) // where "back" is, from the seed
const top = box(s, p, o, [at[0], at[1] - 40 * k], 60 * k, 6 * k, 30 * k)
// …legs first (behind), then the top box drawn over their ends
}
const T = objectsIn("workshop") // your own collection name
define(T("stool", "Step stool", [120, 100], [20, 90], 1, stool, [
"furniture",
"home",
]))
sis the sheet you draw on,pthe pen,rthe shape stream for every choice (sizes, angles, how many).atandksay where and how big; build every coordinate from them so scenes can place and scale the object.- Draw back to front.
s.blank(p.shape(outline))hides everything drawn earlier inside that outline; then stroke the outline. That’s how a front leg hides the back one and a cup hides its handle’s roots. - Closed outlines are one stroke (
p.shape), so corners meet. Inner lines should end on an outline or under a part drawn after them, so the pen’s small run-on lands on a line instead of poking out.box,pad,drum,bandedandcylinderalready do this. - Every random choice comes from
r, neverMath.random()orDate.now(): the drawing must be the same for the same seed everywhere. Take sizes and angles fromronly where variety helps; keep the object recognisable at every seed. - The tile (
size,at,kindefine) is the object’s own frame for<Drawing object>: leave about 8 px of air round the drawing and check that no seed runs outside it.
People and scenes
- People:
references/people.md. Build new poses from the kit’sclothes(),head()andhairBehind(), so they wear the same loose tops, wide trousers and trainers and the same inked hair. People in scenes are usually only their clothes (no head), as in every kit scene. - Scenes:
references/scenes.md.defineScene(name, { name, label, size, compose })with the kit’sstage()for the walls, rug, border andput(), then<Drawing scene="name" seed="…" />.
Look at it large, then fix it
A drawing that typechecks can still look wrong, and you only see that at size. Render a contact sheet of the new ids across several seeds and both hands:
node <skill-dir>/scripts/render.mjs --module src/drawings/index.ts --ids stool,shop-counter --seeds a,b,c,d --out drawings-check.png
It needs jiti in the project (most Vite and Next apps have it already; if not, the script says so). For the PNG it uses
sharp if the project has it, or a Chrome or Edge on the machine, headless; with neither it writes an HTML page. Don’t
add dependencies to the user’s project just for this. It also warns when an object comes within 4 px of its tile’s
edge or runs past it, and when a seed gives broken coordinates. Then look at the image and ask:
- Would it sit unnoticed next to the kit’s chair, mug and turntable? Same line, same calm, same amount of ink?
- Do corners meet? Does anything poke through a part in front of it, or float without touching what holds it?
- Is the perspective one perspective (one
depth(r)per object), tipping up and to the right? - Does every seed still read as the thing, and does the calm hand (
--hands calm) look as clean as the lively one? - At the size it will be used, is any detail smaller than the pen’s wobble (a scribble)? Drop it or make it bigger.
Fix and render again until the answers are yes. Two or three rounds is normal.
Finish
- Run the project’s typecheck, and render the page that uses the drawing.
- Tell the user what you drew, the id(s) and how to use them (
<Drawing object="stool" seed="any-string" />), where the code lives, and what you weren’t able to check.
references/style.mdThe look: the rules, and the mistakes that make a new drawing stand out
What a Fineliner drawing is, and the mistakes that make a new one stand out from the kit’s. Read this once before you draw; come back to the checklist at the end when you review a contact sheet.
The look in one breath
One thin navy fine-liner on unbleached paper, drawn by a calm, slightly naive hand. It comes from natural-wine labels and indie record sleeves: everyday things (records, a chair, a plant, a mug), people who are only their clothes or have a dot for each eye, and rooms with plenty of empty wall. Deadpan, never cute.
Rules
- One pen, one weight. Every line is the same monoline; the CSS sets its width, so never draw a line twice to make it bolder, and never draw sketchy double strokes. Thin parts are one line: a bicycle’s tubes and spokes, a lamp’s stem, a cord, a lead, a chair’s rail at small sizes. Draw both edges only of parts with real thickness at the size of use (a table top, a leg of a sofa, a pot’s rim); doubling every tube makes a drawing heavy and technical.
- Contours, not tone. No hatching, shading, gradients or drop shadows. A thing is its outline plus the few inner lines that explain it (a drawer’s handle, a book’s pages, a pot’s rim).
- Ink is rare.
s.fillmakes solid ink; use it for at most two or three small places per object, where the eye should land: a record on a platter, the soil in a pot, a lampshade’s inside, buttons, hair. A large solid shape (a black animal, a dark coat, a heap of beans) needs a reason the drawing can’t do without; a precedent in the kit (its black cat) is not one. A drawing with more fill than outline has left the style. - The wash is for art.
s.washis a thinned second tone for things like wine in a glass, a sticky note or a pot’s rim band. One washed area per object at most; many objects have none. - Flattened 3/4 view. Objects are seen a little from above and from the front-left, receding up and to the right at
the angle
depth(r)gives. Round things are ellipses seen from above (drum,cylinder). One object has one perspective; neighbours may disagree a little. Things that read best from the side (a bicycle, a dog, a car) are drawn in profile. - A calm hand. The pen wobbles on its own. Your geometry should be clean and deliberate: straight where the thing is straight, symmetric where it is symmetric. The naivety is in simple shapes and slight wonkiness, not in errors.
- Air. Leave space inside and around a drawing. Fewer, larger parts read better than many small ones.
- Deadpan subjects. No faces on objects, no mascots, no speech, no lettering inside drawings beyond scribbled lines
(
scribble) where a label or a screen would have text.
How the kit’s drawings get clean (learned the hard way)
These are the fixes the kit’s own library needed. A new drawing that skips them looks rough next to it.
- Closed outlines in one stroke.
p.shape(points)draws a closed outline as one run whose ends meet mid-edge, so corners join.p.edges(points)draws every edge separately, and each overshoots its corners: rectangles turn into crossed sticks. Useedgesonly for a deliberately loose, sketchy part, which is rare. - Lines end on something. An open line’s ends run on a little past where you put them (up to about 4.5 px with the lively hand). End inner lines on an outline (the overshoot lands on that line), or under a part drawn later that blanks them, or pull the end back a couple of pixels. A seam across a box ends just inside its edges.
- Back to front with blanks. Draw what is behind first, then
s.blank(p.shape(outline))and stroke the outline of what is in front. Legs run up into a table top drawn after them; stems go into a pot whose front is drawn after them. Joins then look made, not drawn. - Small details are scribbles. Anything shorter than about 6 px at the size of use becomes a knot of wobble. Use a
dot (
p.discfilled) instead of a tiny line, or leave it out. - Rings look like rings. A small closed circle reads cleaner as
p.disc(...)stroked (s.stroke(p.disc(...))) than asp.ring, which deliberately runs past its start like a quick hand. - People don’t read old. Hair is a filled shape (outlined hair reads as grey); clothes are loose sweaters, hoodies,
open overshirts and tees, wide trousers with rolled hems or a short skirt, and trainers. The kit’s
clothes()andhead()already do this.
Review checklist
Look at the contact sheet at several seeds, both hands, and ask:
- Would it pass unnoticed between the kit’s chair, mug and turntable?
- Do all corners meet? Does any line poke through a part in front of it, past a rim, or out of the silhouette?
- Does every part touch what holds it (a handle its cup, a shade its arm, a person their chair)?
- One perspective? Round tops are ellipses, boxes recede up and to the right.
- Is ink rare and purposeful, with no large solid shape it could do without? Is there at most one washed area?
- Are thin parts single lines, and only thick parts drawn with both edges?
- Does every seed still read as the thing? Is anything outside the tile?
- At the size it will be used, are there scribbles where details were meant to be?
references/engine.mdThe pen engine: coordinates, the sheet, the pen, shapes and seeds
Everything here is exported from @/fineliner/pen (the kit’s src/fineliner/pen/index.ts). The engine is
framework-free TypeScript: no React, no DOM, so a drawing runs on the server, at build time and in the browser.
Contents
- Coordinates
- A draw function
- The sheet: what a mark is
- The pen: how a line goes
- Geometry helpers
- Shapes
- Seeds and the shape stream
- Registering an object
- Placing objects inside other drawings
Coordinates
SVG coordinates: x to the right, y down. A drawing’s at is usually its bottom-left or bottom-centre (say which in
the function’s comment) and everything is built upward from it, scaled by k:
const P = (u: number, v: number): Point => [at[0] + u * k, at[1] - v * k] // u right, v up, in the object's own units
Write the object in its own units (a mug 40 wide, 44 tall) and let P place it. Angles in arc are degrees, 0 to the
right and 90 down.
A draw function
type Draw = (
s: Sheet,
p: Pen,
r: Stream,
at: Point,
k: number,
options: DrawOptions,
) => unknown
What it returns is for scenes that stack things: desk returns its top as a Box so a scene can put a lamp on
top.top(0.9, 0.6). Return nothing if nothing sits on it.
options are choices a caller can make instead of the seed: kind, pose, view, dish, hair, glasses, beard,
black, wine, top, w, h, d, legs. Reuse these names; destructure with a seeded default
({ kind = r.pick(["round", "square"]) } = {}) so the object works alone and on command.
The sheet: what a mark is
| Call | Draws |
|---|---|
s.stroke(d) |
a pen line |
s.fill(d) |
solid ink (rare) |
s.wash(d) |
the thinned second tone, for art (wine, a sticky note) |
s.paper(d) |
flat paper over ink, for small holes (a record’s label, a bulb inside a dark shade) |
s.blank(d) |
hides everything drawn before it inside the shape; later marks are unaffected |
d is SVG path data from the pen. Layering is by order: draw back to front and blank before each front part.
The pen: how a line goes
| Call | Use for |
|---|---|
p.shape(points) |
a closed outline in one stroke, corners meeting; also the shape to blank or fill |
p.path(points) |
an open run through points (a sleeve’s edge, a stem); its two ends run on a little |
p.line(a, b) |
one straight stroke; both ends run on a little |
p.edges(points, closed?) |
each edge its own stroke, overshooting every corner (rarely what you want) |
p.disc(c, u, v?) |
a closed ellipse outline, for fills, blanks and clean small rings |
p.ring(c, u, v?) |
a drawn ellipse that goes a little past its start, like a quick hand |
p.round(c, u, v, from, turn) |
points round an ellipse (radians), to feed into path or shape |
u and v are the ellipse’s axis vectors: dot(c, r) gives [c, [r, 0], [0, r]] for a circle, so
s.fill(p.disc(...dot(c, 3 * k))). HANDS[p.hand].over is how far this hand runs past an end, if you need to pull an end
back by it.
Geometry helpers
add(a, b, f?), lerp(a, b, t), times(v, f), length(v), dist(a, b), turn(pt, centre, degrees),
arc(c, rx, ry, a0, a1, n?) (points on an ellipse), rect(x, y, w, h) (corners from the top-left),
roundRect(x, y, w, h, rad) (a rounded rectangle’s loop, for p.shape), dot(c, r), deg, snap, last.
Use Math.sqrt, never **, Math.pow or Math.hypot: they round differently between engines and a last-bit
difference can change how many points a stroke gets, so server and browser would draw different pictures.
Shapes
| Helper | Draws |
|---|---|
depth(r) |
the object’s “back” direction, up and to the right at a seeded angle; use once per object |
box(s, p, o, at, w, h, d) |
an oblique box (front face w × h, bottom-left at at, receding d along o); returns Box |
pad(s, p, o, at, w, h, d, rad) |
a padded box with rolled top corners (cushions, arms of a sofa); returns Box |
banded(s, p, x, y, w, h, rad, cut) |
a rounded rectangle with a band across its top (a browser’s tab bar, a camera’s top) |
cylinder(s, p, c, rx, ry, h) |
an upright cylinder; c is the centre of its top |
drum(s, p, c, [rt, rb, h, e], inside?, wash?) |
a round seen from above (pot, lampshade, base); inside() draws what stands in it |
handle(s, p, x, y0, y1, reach) |
a cup’s handle on its side |
scribble(s, p, r, x, y, w, lines, gap?) |
wavy lines of text for menus, notes and screens |
sprig(s, p, a, b, size) |
a twig with small solid leaves |
A Box gives you points on it: front(u, v) on the front face (u left to right, v bottom to top, 0 to 1) and
top(u, v) on the top face (v front to back). Put things on a box with top(u, v).
Seeds and the shape stream
r is the drawing’s shape stream: r.next() (0 to 1), r.between(a, b), r.signed() (−1 to 1), r.chance(p),
r.pick(list). Every composition choice comes from r. Take the draws in a fixed order: a draw inside an if shifts
every draw after it for the seeds that take the other branch, which is fine, but don’t make the number of draws depend
on anything but r and options.
The pen has its own stream for its wobble. Line boil redraws the same composition with another pen, so a drawing’s
shape must never depend on the pen (don’t call p.next() for composition).
Registering an object
const T = objectsIn("bakery") // any collection name
define(T("croissant", "Croissant", [140, 90], [20, 78], 1, croissant, [
"food",
"breakfast",
]))
T(id, name, size, at, k, draw, tags): the id is what <Drawing object="croissant"> takes; the name is its accessible
name; size is the tile [width, height] in px; at and k are where and how large the object draws on its tile.
Defining an id again replaces it (handy with hot reload). Ids are global: prefix yours if they could clash with a kit id
(listObjects() lists every registered one: the kit collections you imported and your own).
Placing objects inside other drawings
place(s, id, seed, hand, at, k?, options?, take?) draws a registered object into a sheet you are drawing, with its own
fork of the seed. You can also call one of your draw functions directly from another (sack(s, p, r, at, k, {})): it
shares the caller’s pen and stream, and options is required, so pass {} when there are none. Scenes use it through stage().put (see scenes.md); an object can use it too, to hold another (a
person carrying a record).
references/people.mdPeople: clothes, hair, poses and what they hold
The kit’s people are young and dressed for everyday: loose sweaters, hoodies, open overshirts and tees, wide trousers with rolled hems or a short skirt, and trainers. Hair is a pocket of ink. Faces are two dots, a line for a nose and a short mouth. In scenes, people are usually only their clothes, with a collar ring where a head would be (the “visitor”), as on the label the style comes from.
Reuse the kit’s pieces rather than drawing bodies from scratch, so a new figure matches the others.
The pieces
All exported from @/fineliner/pen/objects/people, which also registers the kit’s people (on a kit from before the
collections split, from @/fineliner/pen).
clothes(s, p, r, at, k, { pose, top, bottom, stride, nobody }) // → { hands: [left, right], neck }
hairBehind(s, p, headCentre, k, hair) // long hair, a ponytail: draw before clothes()
head(s, p, r, headCentre, k, { hair, glasses, beard }) // face, ears, hair in front: draw after clothes()
HAIR // "bob", "crop", "fringe", "bun", "curls", "long", "ponytail", "waves"
TOPS // "sweater", "hoodie", "overshirt", "tee" (a tee shows bare arms: not for a headless visitor)
atis between the feet on the floor; the figure is about 193 units tall atk = 1, and the head’s centre is[at[0], at[1] - 178 * k].- Inside
clothes()the units run up as negative v: a point is[at[0] + u * k, at[1] + v * k], the neck atv = -163, the waist at-90. Use the same convention for anything you add on the body. pose:"stand","wave"(right arm up),"mug"(right forearm forward, hand in front of the chest) or"walk"(arms swinging, feet apart).hands[1]is the right hand: put what they hold there.stridespreads the feet;nobody: trueleaves out hands and feet (the visitor).
A person holding something
export const parcelCarrier: Draw = (
s,
p,
r,
at,
k = 1,
{ hair = r.pick(HAIR) } = {},
) => {
const c: Point = [at[0], at[1] - 178 * k]
hairBehind(s, p, c, k, hair)
const { hands } = clothes(s, p, r, at, k, { pose: "mug", top: r.pick(TOPS) })
head(s, p, r, c, k, { hair })
// The parcel goes last, in front of the arm that holds it.
const o = depth(r)
box(s, p, o, add(hands[1] as Point, [-8 * k, 8 * k]), 34 * k, 22 * k, 14 * k)
}
For a visitor (only clothes), skip hairBehind and head, pass nobody: true, pick the top from the first three
TOPS, and draw the collar ring: s.stroke(p.ring([at[0], at[1] - 162.5 * k], [8 * k, 0], [0, 2.6 * k])).
A pose clothes() doesn’t have
Sitting, kneeling, seen from behind, bending: draw the figure yourself in the same proportions and the same clothes.
The kit’s src/fineliner/pen/objects/seated.ts is the worked example (someone at a desk, from behind, on an office
chair): the torso as one closed outline, sleeves as closed shapes swung from the shoulder, hair from behind as a filled
cap, then the chair drawn over them. Read it before drawing a new pose. Keep:
- the proportions: head about a sixth of standing height, shoulders about 46 units wide, sleeves about 10 wide;
- the clothes’ details: a rib band at the hem and cuffs of a sweater or hoodie, a hood lying on the back, an overshirt’s open fronts and collar;
- hair as a filled shape, never an outline;
- hands as small rings (
p.ring, radius about 3.6 units), only on people with heads.
Avoid
- Outlined hair, bald heads with glasses, buttoned-up collars, tucked-in waistbands, long skirts and black shoes: together they make everyone look retired.
- Detailed faces (eyebrows, noses with nostrils, teeth), fingers, muscles. People are as simple as the objects.
- Mascot energy: big smiles, thumbs up, speech bubbles.
references/scenes.mdScenes: the stage, walls and placing objects in a room
A scene is a room drawn inside the hand-drawn border, composed back to front from objects. The kit’s four (room,
studio, bistro, work) are in src/fineliner/pen/objects/rooms.ts (pen/scenes.ts on an older kit); read one before
writing your own. A scene places objects by id, so import the collections it uses at the top of your module
(import "@/fineliner/pen/objects/indie" for the window, say).
Adding a scene
import { defineScene, stage, type Box, type HandName } from "@/fineliner/pen"
function shop(seed: string, hand: HandName, take: number) {
const { s, p, r, put, w, h, m, floor, walls, rug, border } = stage(
seed,
hand,
take,
"shop",
)
walls()
put("window", "window", [r.between(0.24, 0.3) * w, floor - 124], 0.85)
// …furniture back to front, then what stands on it, then people…
border()
return s
}
defineScene("shop", {
name: "Shop",
label: "A shop counter with a till, bread and someone buying a parcel", // the accessible name
size: [640, 440],
compose: shop,
})
Then <Drawing scene="shop" seed="anything" /> or drawing({ scene: "shop", seed }).
The stage
stage(seed, hand, take, name) returns:
| Field | What it is |
|---|---|
s, p |
the scene’s sheet and pen, for anything you draw directly (a counter, a doorway, a floor curve) |
r |
the room’s stream: every layout choice comes from it, so every take has the same composition |
put(id, key, at, k?, options?) |
draws a registered object (yours or the kit’s); key names its seed fork, so two speakers differ |
w, h, m |
the sheet’s size (640 × 440) and the border’s margin (14) |
corner, floor |
x where the side wall meets the back wall, y where the back wall meets the floor |
walls(), rug(), border() |
the room’s lines, a sweeping floor curve, and the frame (draw it last) |
name goes into the sheet’s mask ids, so two scenes from one seed on a page don’t collide: pass the scene’s own name.
Composing
- Order is depth. Walls, then what hangs on the back wall (window, picture, shelf), then furniture from back to front,
then what stands on it, then people and pets in front, then
border(). Each object blanks what was drawn before it, so something drawn later always sits in front. - Nearer is lower and larger. Things standing on the floor at the back wall stand at
y ≈ floor + 40…80; things near the viewer stand nearh - m - 14, scaled up a little. A standing visitor near the front isk ≈ 1.25; a desk at the back wallk = 1. - Stack with boxes. Furniture that returns a
Box(desk,shelf) gives you its top, sotop.top(0.9, 0.6)is the right back of the desk: put a lamp there. Things further back (higherv) go first. - Leave air. Most of the wall stays empty. Two or three things on the wall, one piece of furniture with a few things on it, one or two people: that’s a full scene.
- Seeded variety, same story. Take positions from
r.between(…)within a small range, and user.chance(…)for swaps (a cat or a plant, a monitor or a laptop), never for the scene’s main subject. - Check the seams. In the contact sheet, look at every place where two objects meet: legs on the floor, a person in a chair, a lamp on a desk. Nothing should float above what holds it or sink into it.
Also in the skill: scripts/render.mjs, which draws the contact sheet; the worked example above; and three test prompts.