Three Spoons at Rest
2026 · still life
Nested things borrow each other's shadow; put three of them down and the light has to decide which one it loves.
three worn spoons laid together on a warm grey table, bowls nested and handles fanning apart, low across the lower-left of an otherwise empty sheet
painted by Kimi K2.6p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: three spoons lie in a shallow diagonal across the lower-left third, bowls nested near the lower-left thirds mark, handles fanning up to the right
// layering: warm grey air and table shade first, one pooled shadow under the nested bowls, then each spoon back-to-front (silhouette, shaded flank, hollow, lit rim), ink contacts and one ochre reflection last
// breathing room: the upper half and the right third stay bare paper apart from one faint breath of grey
const PALETTE = [
"#ece4d5", // ground — warm paper, the light itself
"#9d9583", // mass — pewter, the bodies of the spoons
"#6b6455", // shadow — umber grey, hollows and pooled shade
"#332f26", // ink — near-black, contact darks and broken contours
"#bd8b4a", // accent — ochre, one reflected warmth, rationed
];
// the highlight is paper cooled by a breath of pewter — never chalk-white
let LIT = null;
function ovalPoint(cx, cy, ang, rw, rh, t) {
const ex = Math.cos(t) * rw;
const ey = Math.sin(t) * rh;
return [
cx + ex * Math.cos(ang) - ey * Math.sin(ang),
cy + ex * Math.sin(ang) + ey * Math.cos(ang),
];
}
// a bowl outline: never a true ellipse — each vertex wanders a little
function ovalPts(cx, cy, ang, rw, rh, n, wobble) {
const pts = [];
for (let i = 0; i < n; i++) {
const t = (i / n) * Math.PI * 2;
pts.push(
ovalPoint(cx, cy, ang, rw * (1 + randomGaussian(0, wobble)), rh * (1 + randomGaussian(0, wobble)), t)
);
}
return pts;
}
function ovalArc(cx, cy, ang, rw, rh, t0, t1, n) {
const pts = [];
for (let i = 0; i <= n; i++) {
const t = t0 + (t1 - t0) * (i / n);
pts.push(ovalPoint(cx, cy, ang, rw + randomGaussian(0, 0.7), rh + randomGaussian(0, 0.7), t));
}
return pts;
}
function shape(pts, curvature, close) {
brush.beginShape(curvature);
for (let i = 0; i < pts.length; i++) brush.vertex(pts[i][0], pts[i][1]);
brush.endShape(close);
}
// the axis of a handle: pinched at the neck, bellied, swelling again at the tip
function handleAxis(bx, by, ang, len, bend) {
const ca = Math.cos(ang);
const sa = Math.sin(ang);
const ts = [0.0, 0.14, 0.34, 0.56, 0.76, 0.9, 1.0];
const pts = [];
for (let i = 0; i < ts.length; i++) {
const t = ts[i];
const off = bend * Math.sin(t * Math.PI) + randomGaussian(0, 0.6);
pts.push([bx + ca * len * t - sa * off, by + sa * len * t + ca * off]);
}
return pts;
}
function handlePts(bx, by, ang, len, bend, wNeck, wWaist, wTip) {
const axis = handleAxis(bx, by, ang, len, bend);
const ws = [wNeck, wNeck * 0.78, wWaist, wWaist * 1.16, wTip * 0.86, wTip, wTip * 0.36];
const nx = -Math.sin(ang);
const ny = Math.cos(ang);
const out = [];
for (let i = 0; i < axis.length; i++) {
const w = ws[i] * (1 + randomGaussian(0, 0.06));
out.push([axis[i][0] + nx * w, axis[i][1] + ny * w]);
}
for (let i = axis.length - 1; i >= 0; i--) {
const w = ws[i] * (1 + randomGaussian(0, 0.06));
out.push([axis[i][0] - nx * w, axis[i][1] - ny * w]);
}
return out;
}
// a small dark laid twice so the pigment actually pools
function pooledDark(cx, cy, ang, rw, rh, op) {
brush.noStroke();
brush.fillBleed(0.16, "out");
brush.fillTexture(0.2, 0.85);
brush.fill("#332f26", op);
shape(ovalPts(cx, cy, ang, rw, rh, 11, 0.08), 0.92, true);
brush.fill("#332f26", op * 0.78);
shape(ovalPts(cx + randomGaussian(0, 1.6), cy + randomGaussian(0, 1.3), ang, rw * 0.8, rh * 0.76, 10, 0.09), 0.92, true);
}
// one spoon: stacked passes, a dark hollow, a lit rim, and only the darks it needs
function spoon(bx, by, ang, len, rw, rh, bend, tone, tipW, hatchBowl) {
const hx = bx + Math.cos(ang) * rw * 0.74;
const hy = by + Math.sin(ang) * rw * 0.74;
const nx = -Math.sin(ang);
const ny = Math.cos(ang);
const wNeck = rh * 0.2;
const wWaist = rh * 0.15;
brush.noStroke();
brush.noHatch();
// silhouette — umber under, pewter over, a third pass pulled toward the light
brush.fillBleed(0.09, "out");
brush.fillTexture(0.22, 0.92);
brush.fill("#6b6455", tone);
shape(ovalPts(bx, by, ang, rw, rh, 16, 0.03), 0.9, true);
brush.fill("#9d9583", tone * 0.95);
shape(ovalPts(bx - 2, by - 3, ang, rw * 0.92, rh * 0.89, 15, 0.03), 0.9, true);
brush.fill("#9d9583", tone * 0.6);
shape(ovalPts(bx - 9, by - 10, ang, rw * 0.56, rh * 0.5, 13, 0.04), 0.9, true);
brush.fill("#6b6455", tone * 0.94);
shape(handlePts(hx, hy, ang, len, bend, wNeck, wWaist, tipW), 0.55, true);
brush.fill("#9d9583", tone * 0.84);
shape(handlePts(hx - 2, hy - 3, ang, len * 0.99, bend, wNeck * 0.78, wWaist * 0.76, tipW * 0.8), 0.55, true);
// the hollow: the far wall of the bowl in deep shade, the near rim left light
brush.fillBleed(0.12, "out");
brush.fillTexture(0.3, 0.7);
brush.fill("#6b6455", 214);
shape(ovalPts(bx + 8, by + 9, ang, rw * 0.58, rh * 0.46, 12, 0.045), 0.95, true);
brush.fill("#332f26", 134);
shape(ovalPts(bx + 12, by + 12, ang, rw * 0.34, rh * 0.23, 10, 0.06), 0.95, true);
// concavity, felt as fine hatching rather than drawn — only on the front bowls
if (hatchBowl) {
brush.noFill();
brush.hatchStyle("2H", "#332f26", 0.75);
brush.hatch(4.4 + randomGaussian(0, 0.3), 112 + randomGaussian(0, 6), { rand: 0.14, continuous: false });
shape(ovalPts(bx + 3, by + 3, ang, rw * 0.7, rh * 0.56, 12, 0.04), 0.92, true);
brush.noHatch();
}
// the turn of the bowl, drawn thin and only where the form goes away from us
brush.noFill();
brush.set("rotring", "#332f26", 1.15);
shape(ovalArc(bx, by, ang, rw * 0.98, rh * 0.96, Math.PI * 0.1, Math.PI * 0.56, 6), 0.7, false);
// lit rim on the upper-left arc, and the gleam running down the handle
brush.set("2H", LIT, 2.3);
shape(ovalArc(bx, by, ang, rw * 0.86, rh * 0.82, Math.PI * 0.7, Math.PI * 1.34, 7), 0.7, false);
brush.set("cpencil", LIT, 2.1);
const gl = handleAxis(hx, hy, ang, len * 0.88, bend * 0.8);
brush.beginShape(0.5);
for (let i = 1; i < gl.length - 1; i++) brush.vertex(gl[i][0] - nx * 2.4, gl[i][1] - ny * 2.4);
brush.endShape(false);
// one dark at the neck, where the handle leaves the bowl and the light does not reach
brush.set("pen", "#332f26", 1.25);
const nk = handleAxis(hx, hy, ang, len * 0.3, bend * 0.3);
brush.beginShape(0.5);
for (let i = 0; i < 4; i++) {
const w = wNeck * (1.05 - i * 0.06) + randomGaussian(0, 0.35);
brush.vertex(nk[i][0] + nx * w, nk[i][1] + ny * w);
}
brush.endShape(false);
brush.noFill();
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#ece4d5");
LIT = lerpColor(color("#ece4d5"), color("#9d9583"), 0.28);
// 1. air — a wide breath of warm grey holding the lower-left, running off two edges
brush.noStroke();
brush.fillBleed(0.72, "out");
brush.fillTexture(0.3, 0.3);
brush.fill("#9d9583", 46);
brush.circle(162, 616, 278, true);
brush.fill("#9d9583", 32);
brush.circle(330, 684, 198, true);
brush.fill("#9d9583", 24);
brush.circle(38, 448, 188, true);
brush.fill("#9d9583", 15);
brush.circle(396, 518, 150, true);
// the single faint note in the open right — enough that the paper is not dead
brush.fill("#9d9583", 10);
brush.circle(660, 234, 182, true);
// 2. the table plane, a shallow band of shade that thins away to the right
brush.fillBleed(0.5, "out");
brush.fill("#6b6455", 40);
shape(
[
[-40, 668],
[144, 652],
[330, 664],
[482, 692],
[504, 812],
[-40, 812],
],
0.4,
true
);
// 3. one long pool of shade along the group, deepest where the bowls sit
brush.fillBleed(0.52, "out");
brush.fillTexture(0.34, 0.45);
brush.fill("#6b6455", 148);
shape(ovalPts(264, 674, 0.05, 64, 19, 13, 0.06), 0.92, true);
brush.fill("#6b6455", 72);
shape(ovalPts(206, 654, -0.16, 34, 14, 12, 0.08), 0.92, true);
brush.fill("#6b6455", 54);
shape(ovalPts(352, 664, -0.02, 76, 17, 12, 0.09), 0.92, true);
brush.fill("#6b6455", 24);
shape(ovalPts(438, 646, -0.08, 60, 13, 11, 0.1), 0.92, true);
// 4. the three spoons, back to front — the middle one takes the light
spoon(204, 546, -0.37, 268, 49, 33, -13, 150, 10.4, false);
spoon(238, 632, 0.09, 246, 42, 27, 9, 184, 8.0, true);
spoon(228, 594, -0.15, 276, 47, 31, -6, 216, 9.4, true);
// 5. ochre pooling in the two front bowls, the only warm note in the picture
brush.noStroke();
brush.fillBleed(0.18, "out");
brush.fillTexture(0.4, 0.6);
brush.fill("#bd8b4a", 112);
shape(ovalPts(232, 600, -0.15, 20, 7, 11, 0.06), 0.92, true);
brush.fill("#bd8b4a", 76);
shape(ovalPts(243, 636, 0.09, 12, 6, 9, 0.07), 0.92, true);
// the front bowl keeps the deepest hollow, so the eye settles there
brush.fillBleed(0.13, "out");
brush.fillTexture(0.28, 0.7);
brush.fill("#6b6455", 196);
shape(ovalPts(243, 606, -0.15, 24, 11, 11, 0.05), 0.95, true);
brush.fill("#332f26", 118);
shape(ovalPts(248, 610, -0.15, 14, 6, 10, 0.06), 0.95, true);
// 6. the darks, last and few — contact where the nested bowls meet the table
pooledDark(234, 660, 0.05, 35, 7, 212);
pooledDark(278, 664, 0.11, 21, 5, 154);
pooledDark(180, 620, -0.25, 17, 6, 140);
pooledDark(184, 572, -0.35, 13, 5, 148);
pooledDark(266, 628, 0.02, 12, 4, 114);
// three handle tips, each told with one quiet dark touch
brush.noFill();
brush.set("rotring", "#332f26", 0.95);
shape(ovalArc(456, 449, -0.37, 15, 5, Math.PI * 0.05, Math.PI * 0.9, 4), 0.6, false);
shape(ovalArc(500, 552, -0.15, 14, 5, Math.PI * 0.02, Math.PI * 0.82, 4), 0.6, false);
shape(ovalArc(478, 664, 0.09, 12, 4, Math.PI * 0.08, Math.PI * 0.86, 4), 0.6, false);
// one crumb of ochre far out in the open, keeping the right side alive
brush.noStroke();
brush.fillBleed(0.4, "out");
brush.fill("#bd8b4a", 62);
brush.circle(584, 352, 4.2, true);
}