Handled Without Gloves
2026 · botanical
Specimen clarity, from the hand of someone who reached for it bare. Four blades crowd one low node, every margin walked tooth by tooth and every tooth thrown forward. The last marks laid are the spine of the stalk and the sting standing off it.
stinging nettle seen close in the manner of a Victorian botanical plate: one heavy toothed leaf mass anchoring the lower left, blades hatched across their lateral veins with the shaded halves worked hard, a bristled stem running off the top edge, cool greens on warm paper, the right third left as bare paper
painted by GPT-5.6p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: a heavy nettle leaf mass anchors the lower-left thirds crossing — four blades from
// one low node, the largest reaching up-left, another falling off the bottom edge. A bristled stem
// climbs out of the node and off the top, thinning as it goes. The right third stays bare paper.
// layering: cool washes pooling under the mass, then blade bodies hatched across their own lateral
// veins with the shaded half worked hardest, then vein structure, then the ink teeth, the dark at
// the node, and last of all the sting standing off every margin and the whole length of the stem.
const PALETTE = [
"#eae3d4", // paper
"#9db08f", // light
"#557a5f", // mass
"#2c4437", // shadow
"#131b16", // ink
"#8c4b39", // accent
];
// an ovate-cordate blade: widest a fifth up from the heart-shaped base, then a long drawn point
function bladeProfile(t) {
const u = constrain(t, 0, 1);
return Math.pow(Math.sin(Math.PI * Math.pow(u, 0.42)), 1.15);
}
// walk the margin tooth by tooth — a notch, then a tip thrown forward toward the apex
function buildBlade(bx, by, ang, len, half, teeth, curl) {
const ca = Math.cos(ang);
const sa = Math.sin(ang);
const nx = -sa;
const ny = ca;
const pAt = (t, off) => {
const a = len * t;
const o = off + curl * len * t * t;
return [bx + ca * a + nx * o, by + sa * a + ny * o];
};
const halfAt = (t) => half * bladeProfile(t);
const T0 = 0.13;
const tips = [];
const rims = [[], []];
for (const side of [1, -1]) {
const rim = rims[side === 1 ? 0 : 1];
for (let i = 0; i < teeth; i++) {
const f = i / teeth;
const bite = constrain(randomGaussian(1, 0.26), 0.4, 1.45);
const tn = T0 + (1 - T0) * f;
const tt = T0 + (1 - T0) * (f + 0.72 / teeth);
rim.push(pAt(tn + randomGaussian(0, 0.004), halfAt(tn) * (1 - 0.13 * bite) * side));
const tp = pAt(tt + randomGaussian(0, 0.004), halfAt(tt) * (1 + 0.09 * bite) * side);
rim.push(tp);
tips.push({ x: tp[0], y: tp[1], t: tt, side });
}
}
const apex = pAt(1.0, randomGaussian(0, half * 0.02));
const pts = [pAt(0.04, 0), pAt(-0.012, half * 0.3), pAt(0.03, half * 0.52)];
for (const p of rims[0]) pts.push(p);
pts.push(apex);
for (let i = rims[1].length - 1; i >= 0; i--) pts.push(rims[1][i]);
pts.push(pAt(0.03, -half * 0.52), pAt(-0.012, -half * 0.3));
// a strip of the blade between two fractions of the half-width — how tone follows the form
const bandPoly = (side, u0, u1, t0, t1) => {
const poly = [];
const N = 10;
for (let i = 0; i <= N; i++) {
const t = lerp(t0, t1, i / N);
poly.push(pAt(t, halfAt(t) * u0 * side));
}
for (let i = N; i >= 0; i--) {
const t = lerp(t0, t1, i / N);
poly.push(pAt(t, halfAt(t) * u1 * side));
}
return poly;
};
return { bx, by, ca, sa, nx, ny, len, half, ang, pts, tips, apex, pAt, halfAt, bandPoly };
}
// hatching laid across the lateral veins, the way a plate is engraved
function toneBlade(L, poly, spacing, angOff, col, name, w) {
brush.noFill();
brush.noStroke();
brush.hatchStyle(name, col, w);
brush.hatch(spacing, (L.ang * 180) / Math.PI + angOff + randomGaussian(0, 4), { rand: 0.2, continuous: false });
brush.polygon(poly);
brush.noHatch();
}
function fillPoly(poly, tone, alpha, bleed, tex) {
brush.noStroke();
brush.noHatch();
brush.fillBleed(bleed, "out");
brush.fillTexture(tex, 0.56);
brush.fill(tone, constrain(alpha + randomGaussian(0, 9), 18, 242));
brush.polygon(poly);
}
// the whole passage for one blade. `side` is the shaded half, `f` how hard it is worked.
function renderBlade(L, side, force) {
const f = constrain(force, 0, 1);
fillPoly(L.pts, "#9db08f", 130 + f * 28, 0.42, 0.42);
toneBlade(L, L.pts, 5.8 - f * 1.6, 64, "#557a5f", "cpencil", 1.1 + f * 0.5);
toneBlade(L, L.bandPoly(side, -0.08, 1.08, 0.09, 0.99), 3.6 - f * 1.2, 62, "#2c4437", "cpencil", 1.2 + f * 0.5);
toneBlade(L, L.bandPoly(side, 0.46, 1.08, 0.09, 0.99), 6.8 - f * 2, 16, "#2c4437", "2B", 0.9 + f * 0.35);
if (f > 0.55) toneBlade(L, L.bandPoly(side, 0.6, 1.08, 0.12, 0.92), 8.6 - f * 2.2, 92, "#131b16", "2H", 0.85);
if (f > 0.5) toneBlade(L, L.bandPoly(-side, 0.55, 1.06, 0.14, 0.94), 7.5, 60, "#557a5f", "cpencil", 1.2);
// the light lifted off the blade beside the midrib, on the side away from the shadow
fillPoly(L.bandPoly(-side, -0.06, 0.5, 0.1, 0.97), "#9db08f", 152 + f * 44, 0.2, 0.4);
}
// pencil laid along the lateral veins, each stroke cut to the blade's own half-width
function strokeBlade(L, side, n, mid, col, name, w) {
brush.noFill();
brush.noHatch();
brush.set(name, col, w);
for (let i = 0; i < n; i++) {
const t = constrain(randomGaussian(mid, 0.19), 0.14, 0.94);
const u0 = constrain(randomGaussian(0.14, 0.12), 0.02, 0.55);
const u1 = constrain(u0 + max(0.18, randomGaussian(0.44, 0.15)), 0.22, 0.95);
const t1 = constrain(t + randomGaussian(0.05, 0.03), 0.15, 0.97);
const a = L.pAt(t, L.halfAt(t) * u0 * side);
const b = L.pAt(t1, L.halfAt(t1) * u1 * side);
brush.line(a[0], a[1], b[0], b[1]);
}
}
// midrib, then a lateral running out to each tooth it can reach — Pratt's honest structure
function veinBlade(L, col, w, name, keep) {
brush.noFill();
brush.noHatch();
brush.set(name, col, w);
const m0 = L.pAt(0.04, 0);
const m1 = L.pAt(0.42, randomGaussian(0, 3));
const m2 = L.pAt(0.74, randomGaussian(0, 3));
const m3 = L.pAt(0.98, 0);
brush.beginShape(0.35);
brush.vertex(m0[0], m0[1]);
brush.vertex(m1[0], m1[1]);
brush.vertex(m2[0], m2[1]);
brush.vertex(m3[0], m3[1]);
brush.endShape(false);
brush.set(name, col, w * 0.6);
for (const tp of L.tips) {
if (random() > keep) continue;
const root = tp.t * 0.5 + 0.04;
const r = L.pAt(root, 0);
const mx = lerp(r[0], tp.x, 0.56) + randomGaussian(0, 3.4);
const my = lerp(r[1], tp.y, 0.56) + randomGaussian(0, 3.4);
brush.beginShape(0.4);
brush.vertex(r[0], r[1]);
brush.vertex(mx, my);
brush.vertex(lerp(r[0], tp.x, 0.9), lerp(r[1], tp.y, 0.9));
brush.endShape(false);
}
}
// broken ink along the margin — only the runs a pen would bother with, plus a tick per tooth
function inkBlade(L, col, w, coverage) {
brush.noFill();
brush.noHatch();
brush.set("pen", col, w);
const P = L.pts;
let i = 1;
while (i < P.length - 2) {
const run = 3 + floor(abs(randomGaussian(3.2, 1.8)));
if (random() < coverage) {
brush.beginShape(0);
for (let k = i; k <= min(i + run, P.length - 1); k++) {
brush.vertex(P[k][0] + randomGaussian(0, 0.8), P[k][1] + randomGaussian(0, 0.8));
}
brush.endShape(false);
}
i += run + (random() < 0.45 ? 1 : 0);
}
brush.set("2B", col, w * 1.2);
for (const tp of L.tips) {
if (random() > coverage * 0.7) continue;
const ox = L.nx * tp.side * 0.72 + L.ca * 0.45;
const oy = L.ny * tp.side * 0.72 + L.sa * 0.45;
const h = max(2.4, randomGaussian(4.6, 1.2));
brush.line(tp.x - ox * h, tp.y - oy * h, tp.x, tp.y);
}
}
// the sting: stiff bristles standing off the teeth, close-set enough to read as fuzz
function hairBlade(L, chance, meanLen, col) {
brush.noFill();
brush.noHatch();
brush.set("pen", col, 0.6);
for (const tp of L.tips) {
for (let k = 0; k < 3; k++) {
if (random() > chance) continue;
const skew = randomGaussian(0, 0.42);
const dx = L.nx * tp.side * 0.88 + L.ca * 0.34;
const dy = L.ny * tp.side * 0.88 + L.sa * 0.34;
const ox = dx * Math.cos(skew) - dy * Math.sin(skew);
const oy = dy * Math.cos(skew) + dx * Math.sin(skew);
const h = max(3, randomGaussian(meanLen, meanLen * 0.3));
const jx = randomGaussian(0, 2.2);
const jy = randomGaussian(0, 2.2);
brush.line(tp.x + jx + ox * 1.2, tp.y + jy + oy * 1.2, tp.x + jx + ox * h, tp.y + jy + oy * h);
}
}
}
// a stalk built from short offset passes so it reads solid, not speckled
function drawStalk(P, wide, col, name, off) {
brush.noFill();
brush.noHatch();
for (let i = 0; i < P.length - 1; i++) {
const dx = P[i + 1][0] - P[i][0];
const dy = P[i + 1][1] - P[i][1];
const m = Math.hypot(dx, dy) || 1;
const nx = (-dy / m) * off;
const ny = (dx / m) * off;
brush.set(name, col, max(0.4, wide + randomGaussian(0, 0.16)));
brush.line(P[i][0] + nx, P[i][1] + ny, P[i + 1][0] + nx, P[i + 1][1] + ny);
}
}
function stalkHairs(P, per, meanLen, col) {
brush.noFill();
brush.noHatch();
brush.set("pen", col, 0.6);
for (let i = 0; i < P.length - 1; i++) {
const dx = P[i + 1][0] - P[i][0];
const dy = P[i + 1][1] - P[i][1];
const m = Math.hypot(dx, dy) || 1;
const nx = -dy / m;
const ny = dx / m;
for (let k = 0; k < per; k++) {
const t = constrain((k + 0.5) / per + randomGaussian(0, 0.03), 0.02, 0.98);
const side = k % 2 === 0 ? 1 : -1;
const bx = P[i][0] + dx * t;
const by = P[i][1] + dy * t;
const skew = randomGaussian(0, 0.36);
const ox = (nx * Math.cos(skew) - ny * Math.sin(skew)) * side;
const oy = (ny * Math.cos(skew) + nx * Math.sin(skew)) * side;
const h = max(3.5, randomGaussian(meanLen, meanLen * 0.3));
brush.line(bx + ox * 1.6, by + oy * 1.6, bx + ox * h, by + oy * h);
}
}
}
// axil flowers: dull beads on a drooping string
function drawTassel(x, y, ang, len, beads) {
brush.noStroke();
brush.noHatch();
brush.fillBleed(0.24, "out");
brush.fillTexture(0.5, 0.6);
let px = x;
let py = y;
for (let i = 0; i < beads; i++) {
px += Math.cos(ang) * (len / beads) + randomGaussian(0, 2.2);
py += Math.sin(ang) * (len / beads) + randomGaussian(0, 1.6);
brush.fill(random() < 0.11 ? "#8c4b39" : "#557a5f", constrain(randomGaussian(158, 22), 70, 224));
brush.circle(px, py, max(1.4, randomGaussian(4.2 - (i / beads) * 1.8, 0.6)), true);
}
}
function poolDark(x, y, r, col, alpha, bleed) {
brush.noStroke();
brush.noHatch();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.62, 0.42);
brush.fill(col, alpha);
brush.circle(x, y, r, true);
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#eae3d4");
// 1 — cool air pooling under the mass; nothing at all in the right third
const AIR = [[178, 636, 274, "#9db08f", 40], [272, 752, 214, "#557a5f", 34],
[96, 704, 176, "#557a5f", 26], [222, 792, 158, "#2c4437", 22], [288, 336, 152, "#9db08f", 18]];
for (const a of AIR) poolDark(a[0], a[1], a[2], a[3], a[4], 0.62);
// 2 — the blades: four crowd the low node, then a big pair, a bare stretch, and the young tip
const back = buildBlade(262, 674, 3.28, 190, 74, 8, 0.05);
const fall = buildBlade(286, 706, 2.36, 260, 96, 9, -0.05);
const swing = buildBlade(288, 702, 0.66, 148, 58, 7, 0.06);
const front = buildBlade(268, 688, 4.10, 310, 118, 11, 0.05);
const sail = buildBlade(332, 412, 3.90, 214, 88, 9, 0.06);
const reach = buildBlade(338, 420, 0.55, 120, 48, 7, -0.06);
const upL = buildBlade(398, 194, 3.80, 118, 46, 7, 0.05);
const upR = buildBlade(402, 200, 0.42, 74, 28, 6, -0.05);
const bud = buildBlade(438, 78, 3.98, 60, 24, 5, 0.06);
// blade, shaded side, how hard it is worked, ink weight, ink coverage, sting chance, sting length
const PLANT = [
[back, -1, 0.30, 1.05, 0.66, 0.17, 7.0],
[fall, 1, 0.64, 1.35, 0.84, 0.24, 8.0],
[swing, -1, 0.32, 1.05, 0.70, 0.15, 6.5],
[front, -1, 1.00, 1.65, 0.92, 0.34, 8.5],
[sail, -1, 0.58, 1.25, 0.80, 0.22, 7.5],
[reach, 1, 0.36, 1.05, 0.68, 0.15, 6.5],
[upL, -1, 0.32, 1.00, 0.66, 0.15, 6.0],
[upR, 1, 0.24, 0.90, 0.58, 0.12, 5.5],
[bud, -1, 0.18, 0.80, 0.50, 0.10, 5.0],
];
// the stalk, laid in mid-tone now and struck dark at the end — a diagonal, not a mast
const STEM = [[300, 736], [292, 628], [308, 520], [334, 414], [366, 306], [400, 196], [434, 86], [456, -22]];
drawStalk(STEM, 2.6, "#557a5f", "2B", 0);
drawStalk(STEM, 1.6, "#9db08f", "cpencil", -2.4);
// back to front. The light comes from the upper right, so most shadow falls down and left.
for (const b of PLANT) renderBlade(b[0], b[1], b[2]);
strokeBlade(front, -1, 20, 0.40, "#2c4437", "cpencil", 1.8);
strokeBlade(front, -1, 9, 0.26, "#131b16", "2B", 1.2);
strokeBlade(sail, -1, 10, 0.44, "#2c4437", "cpencil", 1.4);
// 3 — the darkest passage in the picture: pigment gathering where the four blades meet
toneBlade(front, front.bandPoly(-1, 0.06, 1.05, 0.09, 0.44), 2.2, 62, "#131b16", "2B", 1.15);
toneBlade(fall, fall.bandPoly(1, 0.2, 1.05, 0.09, 0.4), 2.8, 62, "#131b16", "2H", 1.0);
toneBlade(swing, swing.bandPoly(-1, 0.0, 1.05, 0.09, 0.46), 3.0, 62, "#2c4437", "2B", 1.1);
const NODE = [[272, 692, 44, "#2c4437", 168], [240, 732, 34, "#2c4437", 138],
[288, 664, 25, "#131b16", 122], [256, 706, 20, "#131b16", 102],
[318, 736, 22, "#131b16", 112], [336, 416, 19, "#2c4437", 124], [398, 198, 13, "#2c4437", 100]];
for (const n of NODE) poolDark(n[0], n[1], n[2], n[3], n[4], 0.34);
// 4 — vein structure, second layer of drawing
for (const b of PLANT) {
const f = b[2];
veinBlade(b[0], f > 0.5 ? "#131b16" : "#2c4437", 0.9 + f * 1.1, f > 0.28 ? "2B" : "2H", 0.4 + f * 0.45);
}
// 5 — petioles, and the drooping axil flowers on the mass side only
drawStalk([[268, 688], [300, 722]], 2.3, "#2c4437", "2B", 0);
drawStalk([[332, 412], [356, 436]], 1.8, "#2c4437", "2B", 0);
drawStalk([[398, 194], [410, 210]], 1.4, "#2c4437", "2B", 0);
drawTassel(306, 726, 1.28, 96, 12);
drawTassel(356, 440, 2.42, 68, 9);
drawTassel(406, 206, 1.05, 28, 6);
// 6 — the teeth, cut in ink last of the drawn marks
for (const b of PLANT) inkBlade(b[0], b[2] > 0.2 ? "#131b16" : "#2c4437", b[3], b[4]);
// 7 — the spine: the stalk struck again in near-black, heaviest at the bottom
drawStalk(STEM.slice(0, 4), 2.3, "#131b16", "charcoal", 0.9);
drawStalk(STEM.slice(0, 3), 1.7, "#131b16", "2B", -0.7);
drawStalk(STEM.slice(0, 5), 1.4, "#131b16", "2B", 1.9);
drawStalk(STEM.slice(3, 6), 1.6, "#2c4437", "charcoal", 0.7);
drawStalk(STEM.slice(5), 1.1, "#2c4437", "2B", 0.5);
// two crisp sides bracketing the charcoal, so the stalk reads as a stalk and not a chain
drawStalk(STEM.slice(0, 5), 0.85, "#131b16", "pen", -2.0);
drawStalk(STEM.slice(0, 6), 0.8, "#131b16", "pen", 2.4);
// 8 — the sting itself
for (const b of PLANT) hairBlade(b[0], b[5], b[6], b[2] > 0.4 ? "#131b16" : "#2c4437");
stalkHairs(STEM.slice(0, 4), 12, 9.5, "#131b16");
stalkHairs(STEM.slice(3), 9, 7.5, "#2c4437");
// 9 — accent, rationed: rust at the nodes, and one margin already rusted through
brush.noStroke();
brush.noHatch();
brush.fillBleed(0.3, "out");
const RUST = [[276, 696, 7.0, 178], [340, 420, 5.2, 150], [402, 202, 4.0, 118],
[254, 526, 6.4, 152], [266, 508, 4.6, 116], [244, 542, 3.8, 96]];
for (const r of RUST) {
brush.fill("#8c4b39", r[3]);
brush.circle(r[0], r[1], r[2], true);
}
}