All MicroEvals
Javascript Painter
Create MicroEval
Header image for Javascript Painter

Javascript Painter

Prompt

You are a painter who works in JavaScript. Paint a single botanical specimen — one flower, one fern, one bush, or one plant — using p5.js and the p5.brush library. Follow the API reference below precisely. ## Artistic direction Take inspiration from the tropical vegetation in Henri "Le Douanier" Rousseau's jungle paintings: dense, rhythmic foliage; bold silhouettes; exaggerated leaf shapes; flattened perspective; decorative repetition; and subtly uncanny, dreamlike plant forms. Interpret this visual language rather than reproducing any particular painting. Choose **one** subject and commit to it fully: * An arching fern with a strong base and several fronds of alternating leaflets * A single flower on its stem, with leaves and a distinct head or bloom * A compact bush or shrub built from many overlapping small leaves * A broad-leaved plant resembling a banana or canna * A compact radial plant with leaves spreading like a fan or star * A stranger invented plant that still feels botanically plausible One specimen only — not a group, not a pair, not a small arrangement. It must be a self-contained object growing from its own single stem, base, or crown, so it can be cropped out and used as an isolated asset. All the density and rhythm goes *into* this one plant. Where the eye would otherwise travel across a collection, it should instead travel through the internal structure of this specimen: overlapping foliage, varied leaf scale and orientation, layered greens, a clear silhouette read from a distance and rewarding detail up close. ## Visual requirements * Plain white `#ffffff` background, kept completely clean — no paper texture, speckles, washes, vignette, or stray marks outside the plant silhouette. * Draw only the plant: no animals, people, landscape, ground plane, pots, typography, borders, shadows, or decorative background elements. * Restrained palette of ~7–10 greens, from yellow-green and emerald to deep forest green, with occasional muted olive accents. If the subject is a flower, one restrained non-green accent for the bloom is permitted. * Avoid photographic realism, gradients, neon colours, and generic clip-art symmetry. * Construct the plant from clear, deliberate silhouettes. * Vary leaf scale, curvature, orientation, spacing, and colour so it feels hand-composed rather than procedurally repeated. * Use overlapping masses within the plant for depth and layering. * Add restrained painterly variation: softly irregular edges, visible brush texture, slight pigment variation, and a few darker vein or contour strokes. * The finished image should feel like a single refined botanical plate painted by hand. ## Composition * One specimen, centred in the canvas, with generous white margins on all sides. * The plant should fill roughly the central 60–70% of the canvas — large enough to carry real detail, never touching or crowding the edges. * Keep the whole silhouette comfortably inside the frame, including the outermost leaf tips. ## Drawing approach Build reusable functions for the parts — something like `drawFrond(...)`, `drawLeaf(...)`, `drawStem(...)`, `drawBloom(...)` — and compose the single specimen from them. Use curved spines and calculated leaf placement rather than many unrelated random polygons. Leaflets should follow the changing tangent of the spine they sit on and shrink gradually toward its tip. Leaves should have organic asymmetry, gently irregular contours, tapered ends, and understated central veins. Prefer `brush.polygon()`, `brush.spline()`, and p5.brush watercolour fills and strokes. Use `brush.fill()`, `brush.fillBleed()`, and `brush.fillTexture()` carefully so the foliage stays saturated and graphic rather than pale or washed out. Use p5.brush for the visible artwork rather than relying mainly on ordinary p5 primitives. Use controlled randomness with a fixed seed. The composition must be deterministic and render identically on every load. If a value is used in two places — a petiole length shared by a stem and the leaf it carries, say — compute it **once** into a variable or array and reuse it. Calling a random helper twice for "the same" quantity draws two different numbers from the sequential PRNG, and the parts will not line up. ## Technical requirements One complete, self-contained HTML file using the **p5 build** of p5.brush, with both versions pinned exactly as below: ```html <script src="https://cdn.jsdelivr.net/npm/p5@2.2/lib/p5.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/p5.brush@2.2.2/dist/p5.brush.js"></script> ``` Do not substitute `@latest` or any other version — the API and the built-in brush set below are specific to **p5.brush 2.2.2**. * Portrait canvas ~1000 × 1250, created with `createCanvas(1000, 1250, WEBGL)`. * Background exactly `#ffffff`. * Translate from the centred WEBGL origin to top-left coordinates before drawing. * Call `brush.scaleBrushes(...)` in setup. * Use `randomSeed(...)` and `noiseSeed(...)`. * Render once and call `noLoop()`. * Do **not** use `brush.render()`, `brush.clear()`, or standalone-build transform methods. * Do not load external images, fonts, textures, data, or local assets. * Keep the entire plant comfortably inside the canvas. * Must work when saved locally and opened directly in a modern browser. ## Output Return one complete HTML file in a single fenced `html` code block and nothing else. Do not explain the code. --- # p5.brush 2.2.2 reference (p5 build) p5.brush is a natural drawing library — pencils, charcoal, markers, watercolour fills, hatch patterns, and vector fields. It ships in two builds; **everything below is the p5 build**, which requires p5.js 2.x. Do not use standalone-build calls (`brush.render()`, `brush.clear()`, `brush.push/pop/translate/rotate/scale`, `brush.angleMode()`, `brush.seed()`) — they are wrong here. ### ⚠️ Built-in brush names — read before writing any `brush.set()` call There are exactly **eleven** built-in brushes in 2.2.2: ``` pen rotring 2B HB 2H cpencil pastel crayon charcoal spray marker ``` **Any other name throws and kills the sketch.** `brush.set()` calls `brush.pick()`, which does not fall back to a default — it raises `Brush "<name>" not found` and the exception unwinds out of `draw()`, so nothing renders at all and you get a blank canvas. Beware in particular of names that appear in older p5.brush documentation and in the README still shipped inside the npm package. That list is from v1 and is **wrong for 2.2.2**: * `marker2` — **removed**. Use `marker`. * `hatch_brush` — **removed**. Use any brush above with `brush.hatchStyle()`. * Invented felt-tip-sounding names such as `felt`, `felttip`, `brushpen`, `ink` — these never existed. Use `marker` for a felt-tip mark, `charcoal` or `crayon` for dry media. `pastel` and `crayon` are the two brushes **added** in v2; they are the ones intended for `brush.mass()`. If you want a mark the built-ins do not provide, define it yourself with `brush.add(name, params)` in `setup()` before drawing — do not guess at a name and hope it exists. ### Setup The canvas **must be WEBGL**. The library initialises automatically when `createCanvas()` is called — no `brush.load()` needed for the main canvas. ```js function setup() { createCanvas(600, 600, WEBGL) angleMode(DEGREES) // brush angle APIs follow p5's angleMode brush.scaleBrushes(3) // scale brushes to canvas size; 3 suits 600×600 } function draw() { background("#fffceb") translate(-width/2, -height/2) // WEBGL origin is centre — shift to top-left brush.set("HB", "#222", 1) brush.line(50, 50, 550, 550) } ``` ### What hooks automatically * `push()` / `pop()` — saves and restores brush stroke, fill, and hatch state * `translate()`, `rotate()`, `scale()` — all p5 transforms apply to brush strokes * `randomSeed(n)` / `noiseSeed(n)` — seed both p5 and the brush library * `angleMode(mode)` — all brush angle APIs follow p5's current angle mode ### Angle units Under `angleMode(DEGREES)`, p5's own trigonometry (`cos`, `sin`, `atan2`) takes and returns degrees, and every p5.brush angle argument does the same. JavaScript's `Math.cos`, `Math.sin`, and `Math.atan2` always work in radians and are **not** affected by `angleMode`. Never pass a degree value to a `Math.*` trig function — it fails silently, producing a plausible-looking but meaningless number rather than an error. Use bare `cos()` / `sin()` / `atan2()` for anything angular, and reserve `Math.*` for unit-free work such as `Math.pow`, `Math.abs`, and profile curves driven by `Math.PI`. ### Instance mode ```js const sketch = (p) => { brush.instance(p) // before setup/draw p.setup = () => { p.createCanvas(600, 600, p.WEBGL) } p.draw = () => { ... } } new p5(sketch) ``` ### Offscreen targets `brush.load()` only redirects drawing to a secondary surface: ```js const pg = createGraphics(300, 200, WEBGL) brush.load(pg) brush.set("HB", "black", 1) brush.circle(150, 100, 70) brush.load() // restore main canvas image(pg, 20, 20) ``` Also accepts an active `p5.Framebuffer` inside `fb.draw(() => { ... })`. ### Configuration * `brush.scaleBrushes(scale)` — scale all registered brushes to canvas size. Call before adding custom brushes if you only want built-ins scaled. * `brush.load(target)` — pass a WEBGL `p5.Graphics` or active `p5.Framebuffer`; call with no args to restore the main canvas. ### Stroke operations * `brush.set(name, color, weight)` — brush name (must be one of the eleven listed above, or one you registered with `brush.add()`), colour (hex string or p5.Color), weight multiplier. Enables stroke. * `brush.pick(name)` — change brush type only, keeping colour and weight. Throws on an unknown name. * `brush.stroke(r, g, b)` or `brush.stroke(color)` — set stroke colour; enables stroke. * `brush.strokeWeight(weight)` — weight multiplier only. * `brush.noStroke()`

A system prompt was added to support web rendering

Answer guidance

### Fill operations Fill simulates watercolour — soft edges, bleed, texture layering. * `brush.fill(color, opacity)` or `brush.fill(r, g, b, opacity)` — opacity 0–255. Enables fill. * `brush.noFill()` * `brush.wash(color, opacity)` — fast solid fill, no watercolour simulation. `brush.noWash()` disables. * `brush.fillBleed(strength, direction?)` — edge bleed 0–1; direction `"out"` or `"in"`. * `brush.fillTexture(textureStrength, borderIntensity, scatter?)` — both 0–1. `scatter` (default `true`) enables sparse scattered polygon layers; set `false` for a cleaner trim without edge texture noise. * For performance, group shapes by fill colour/opacity so internal caching is reused. ### Hatch operations * `brush.hatch(dist, angle, options?)` — `dist` = spacing in canvas units; `angle` follows current `angleMode()`. Options: `{rand: 0–1, continuous: bool, gradient: 0–1}`. * `brush.noHatch()` * `brush.hatchStyle(name, color, weight)` — brush used for hatching; `name` must be a valid brush name. * `brush.mass(brushName, color, options?)` — dry-media, hand-filled hatched fill; best with `crayon` or `pastel`. `brushName` always required. Options: `precision`, `strength`, `gradient` (0–1), `outline` (bool). `brush.noMass()` disables. * `brush.hatchArray(polygons)` / `brush.massArray(polygons)` — apply current hatch/mass style to a `brush.Polygon` or array (array = outer shapes + holes via even-odd logic, drawn as one gesture). ### Vector fields Built-in: `"hand"`, `"curved"`, `"zigzag"`, `"waves"`, `"seabed"`, `"spiral"`, `"columns"`. * `brush.field(name)` / `brush.noField()` * `brush.wiggle(intensity)` — shorthand for `"hand"` with given wobble strength. * `brush.listFields()` — array of field names. * `brush.refreshField(t)` — update field with a time value; call in the draw loop for animation. * `brush.addField(name, fn, options?)` — custom field. `fn(t, field)` fills a 2D angle grid (degrees by default) and returns it. Pass `{ angleMode: "radians" }` if your generator writes radians. ```js brush.addField("diagonal", (t, field) => { for (let col = 0; col < field.length; col++) for (let row = 0; row < field[0].length; row++) field[col][row] = 45 + t * 10 return field }) brush.field("diagonal") ``` ### Primitives Lines (stroke only): * `brush.line(x1, y1, x2, y2)` * `brush.flowLine(x, y, length, dir)` — follows the active vector field; `dir` follows `angleMode()`. * `brush.spline(points, curvature?)` — smooth curve through `[[x,y], [x,y,pressure], ...]`. Requires at least 2 points. Curvature 0–1. Returns a `brush.Plot`. Manual strokes (stroke only): ```js brush.beginStroke("curve", x, y) // or "segments" brush.move(angle, length, pressure) brush.endStroke(angle, pressure) ``` Shapes (stroke + fill + hatch): * `brush.rect(x, y, w, h, mode?)` — `mode`: `"corner"` (default) or `"center"`. Returns nothing. * `brush.circle(x, y, radius, r?)` — `r` = hand-drawn irregularity 0–1. Returns `[plot, offsetX, offsetY]`. * `brush.arc(x, y, radius, start, end)` — stroke only; angles follow `angleMode()`. Returns a `brush.Plot`, or `null` on zero sweep. * `brush.polygon(pointsArray)` — from `[[x,y], ...]`. Not affected by vector fields. Returns a `brush.Polygon`. * `brush.beginShape(curvature?)` / `brush.vertex(x, y, pressure?)` / `brush.endShape(close?)` — `endShape()` returns a `brush.Plot`. Any stroke primitive throws `No brush or color set` if called while stroke is disabled — call `brush.set(...)` (or `brush.stroke(...)`) first. ### Primitive-first drawing Prefer the main primitives (`line`, `flowLine`, `spline`, `rect`, `circle`, `arc`, `polygon`, `beginShape`/`vertex`/`endShape`) for all visible geometry. To redraw a shape in another style, call the same primitive again with the new brush state rather than reaching for `Polygon.draw()` / `Plot.draw()`. Use those stored-geometry methods only where the geometry itself is needed — e.g. as inputs to `hatchArray()` / `massArray()`. Draw native p5 text and overlays after all brush geometry so they stay on top. ### Brush management * `brush.box()` — array of all registered brush names. In 2.2.2 this returns exactly: `pen`, `rotring`, `2B`, `HB`, `2H`, `cpencil`, `pastel`, `crayon`, `charcoal`, `spray`, `marker`. * `brush.clip([x1, y1, x2, y2])` — clip strokes to a rectangle; transform captured at call time. `brush.noClip()` removes it. * `brush.add(name, params)` — define a custom brush. `brush.add()` params: | Property | Description | |---|---| | `type` | `"default"`, `"spray"`, `"marker"`, `"custom"`, `"image"` | | `weight` | Base thickness in canvas units | | `scatter` | Sideways spread | | `sharpness` | Edge softness 0–1 (`"default"` only) | | `grain` | Texture density (`"default"` only) | | `opacity` | Mark opacity 0–255 | | `spacing` | Stamp gap along stroke (1 = no overlap) | | `pressure` | `[start, end]`, `[start, mid, end]`, or `(t) => value` | | `tip` | `"custom"` type: `(_m) => { ... }` where `_m` is a `p5.Graphics` — any p5 command works. Draw in a 100×100 unit space, origin centred. Dark = opaque, white = transparent. | | `image` | `"image"` type: `{ src: "./tip.jpg" }` | | `rotate` | `"none"`, `"natural"`, `"random"` | | `markerTip` | `"marker"`/`"custom"`/`"image"` only. Boolean, default `true`; `false` disables soft tip buildup at stroke ends. | | `noise` | Per-stroke opacity variation. `0` = identical strokes, `1` = maximum. Default `0.3`. | Image brushes return a Promise — `await brush.add(...)` inside an `async setup()`. Custom brushes need no `await`. (Image brushes are out of scope here: this piece must load no external assets.) ```js brush.add("diamond", { type: "custom", weight: 5, scatter: 0.08, opacity: 23, spacing: 0.6, pressure: [0.5, 1.5, 0.5], tip: (_m) => { _m.rotate(Math.PI / 4); _m.rect(-1.5, -1.5, 3, 3) }, rotate: "natural", markerTip: false }) ``` ### Composing shapes Primitives return their geometry — store the return value to apply effects later or reuse the shape: ```js const frame = brush.polygon([[50,50],[350,50],[350,350],[50,350]]) brush.hatch(8, Math.PI / 4) frame.hatch() brush.mass("pastel", "#4b6cb7", { strength: 0.8 }) frame.mass() ``` ### Exposed classes `brush.Polygon(pointsArray)` ```js let p = new brush.Polygon([[x1,y1],[x2,y2],[x3,y3]]) p.draw(brushName, color, weight) p.fill(color, opacity, bleed, texture) p.wash(color, opacity) p.hatch(distance, angle, options) p.mass() p.intersect(line) // returns [{x,y}, ...] // Attributes: p.vertices, p.sides ``` `brush.Plot(type)` ```js let plot = new brush.Plot("curve") plot.addSegment(angle, length, pressure) plot.endPlot(angle, pressure) plot.draw(x, y); plot.fill(x, y); plot.wash(x, y) plot.hatch(x, y); plot.mass(x, y); plot.rotate(angle) plot.genPol(x, y) // returns a Polygon ``` `brush.Position(x, y)` ```js let pos = new brush.Position(x, y) pos.moveTo(dir, length, stepLength) // dir follows current angleMode() pos.plotTo(plot, length, stepLength, scale) ``` ### Key gotchas * **Brush names throw.** Only the eleven names listed at the top of this reference exist in 2.2.2. `marker2` and `hatch_brush` are v1 names and have been removed; `felt` and similar never existed. One bad name in one helper function blanks the entire canvas. * **`Math.*` trig ignores `angleMode`.** Use p5's `cos`/`sin`/`atan2` for angles; `Math.cos(degrees)` fails silently. * Compute shared values once. Two calls to a random helper for the same quantity return different numbers, so stems and the leaves they carry will not meet. * The WebGL canvas origin is at the **centre** — use `translate(-width/2, -height/2)` to work in top-left coordinates. * `brush.arc()` is stroke-only; `brush.circle()` supports stroke, fill, and hatch. * `brush.rect()` mode uses strings: `"corner"` or `"center"` — not p5 constants. * Fill opacity is 0–255, not 0–100. * `brush.flowLine()` and `brush.hatch()` angles follow the current `angleMode()`. * `brush.load()` is not needed for the main canvas. * `randomSeed()` and `noiseSeed()` seed the brush library automatically.