The Sharper Shadow
2026 · still life
An egg is the softest thing in the room and its shadow the most certain. I kept the ground dark so the light had only one thing left to say, and let the shadow say it better.
an egg resting in a small porcelain cup under one hard lamp, the cast shadow raking left across a lit table, everything else deep quiet dark
painted by Kimi K3p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: egg and cup stand on the lower-right thirds crossing under one hard lamp high on the right;
// the cast shadow rakes left across the lit cloth, harder-edged than the egg that throws it.
// layering: black ground, the cloth washed in and then cut off at a flat far edge, the raking shadow,
// the egg, the cup painted over its base, and last the crisp darks and the lamp's edge on the shell.
const PALETTE = [
"#17130f", // ground — the dark the object is cut out of
"#0b0908", // shadow — colder and deeper than the ground
"#7a715f", // cloth — the lit plane, warm grey
"#cfc6b0", // mass — shell and porcelain in half light
"#f5efe0", // light — the lit side, rationed
"#b8703a", // accent — bounced warmth, a few marks only
];
const EGG = { x: 540, y: 490, rx: 50, ry: 65 };
const FOOT = { x: 537, y: 630, rx: 39, ry: 8.5 };
const EDGE = 568; // the cloth's far edge — everything above it is ground
// the raking shadow: a thin ribbon at the foot, one swelling for the cup and a second,
// far larger, for the egg. The far end of the shadow is the most articulate thing in the picture.
const SHADOW = [
[578, 632], [534, 629], [500, 630], [470, 631], [455, 627], [432, 617],
[405, 613], [380, 620], [356, 630], [332, 628], [306, 615], [272, 605],
[238, 605], [208, 616], [190, 635], [197, 657], [217, 673], [251, 681],
[289, 677], [318, 662], [341, 652], [363, 650], [389, 661], [413, 656],
[437, 646], [457, 640], [487, 645], [521, 647], [561, 645],
];
function eggWidth(t) {
return EGG.rx * pow(max(0, 1 - t * t), 0.4) * (1 + 0.27 * t);
}
function eggOutline(dx, dy, sx, sy, wob) {
const pts = [];
for (let i = 0; i <= 18; i++) {
const t = -1 + (2 * i) / 18;
const w = eggWidth(t) * sx * (1 + random(-wob, wob));
pts.push([EGG.x + dx + w, EGG.y + dy + EGG.ry * sy * t + random(-1, 1)]);
}
for (let i = 18; i >= 0; i--) {
const t = -1 + (2 * i) / 18;
const w = eggWidth(t) * sx * (1 + random(-wob, wob));
pts.push([EGG.x + dx - w, EGG.y + dy + EGG.ry * sy * t + random(-1, 1)]);
}
return pts;
}
function eggPass(col, op, dx, dy, sx, sy, bleed, wob) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.38, 0.5);
brush.fill(col, op);
brush.beginShape(0.66);
for (const p of eggOutline(dx, dy, sx, sy, wob)) brush.vertex(p[0], p[1]);
brush.endShape(true);
}
// a lune hugging the shell's left contour, where the form turns away from the lamp
function eggCrescent(t0, t1, depth, col, op) {
const out = [];
const back = [];
for (let i = 0; i <= 14; i++) {
const t = t0 + ((t1 - t0) * i) / 14;
const w = eggWidth(t);
const y = EGG.y + EGG.ry * t;
const belly = Math.sin((Math.PI * i) / 14);
out.push([EGG.x - w * 0.995, y]);
back.push([EGG.x - w + depth * (0.22 + 0.78 * belly), y]);
}
brush.noStroke();
brush.fillBleed(0.14, "in");
brush.fillTexture(0.42, 0.34);
brush.fill(col, op);
brush.beginShape(0.58);
for (const p of out) brush.vertex(p[0], p[1]);
for (let i = back.length - 1; i >= 0; i--) brush.vertex(back[i][0], back[i][1]);
brush.endShape(true);
}
function strokeMark(name, col, w, curv, pts) {
brush.noFill();
brush.set(name, col, w);
brush.beginShape(curv);
for (const p of pts) brush.vertex(p[0] + random(-0.7, 0.7), p[1] + random(-0.7, 0.7));
brush.endShape(false);
}
function shape(curv, pts, jitter) {
brush.beginShape(curv);
for (const p of pts) brush.vertex(p[0] + random(-jitter, jitter), p[1] + random(-jitter, jitter));
brush.endShape(true);
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#17130f");
brush.noStroke();
// 1. the cloth — laid in as three broad washes so it reads as one plane, not as marks,
// then mottled and pooled where the lamp actually falls
brush.fillBleed(0.4, "out");
brush.fillTexture(0.3, 0.2);
brush.fill("#7a715f", 54);
shape(0.04, [[-24, 574], [824, 570], [822, 812], [-22, 810]], 2.2);
brush.fill("#7a715f", 52);
shape(0.06, [[168, 576], [800, 572], [786, 716], [150, 700]], 2.4);
brush.fillBleed(0.5, "out");
brush.fill("#cfc6b0", 46);
shape(0.08, [[268, 580], [736, 576], [716, 700], [286, 684]], 2.2);
brush.fillBleed(0.5, "out");
brush.fillTexture(0.3, 0.14);
for (let i = 0; i < 46; i++) {
const t = i / 45;
const x = 122 + t * 600 + randomGaussian(0, 28);
const y = 646 + randomGaussian(0, 50) + t * 6;
const fall = 1 - pow(abs(x - 470) / 400, 1.3);
brush.fill(random() < 0.24 ? "#cfc6b0" : "#7a715f", max(11, randomGaussian(20 + 46 * max(0, fall), 8)));
brush.circle(x, y, max(52, randomGaussian(80, 12)), true);
}
// the lamp pools on the cloth behind the cup and runs out along the corridor
// the shadow will take — the shadow needs light of its own to cut into
brush.fillBleed(0.42, "out");
brush.fillTexture(0.28, 0.16);
for (let i = 0; i < 15; i++) {
brush.fill(random() < 0.5 ? "#cfc6b0" : "#f5efe0", max(14, randomGaussian(30, 9)));
brush.circle(randomGaussian(566, 54), randomGaussian(618, 22), max(28, randomGaussian(46, 10)), true);
}
for (let i = 0; i < 10; i++) {
brush.fill("#cfc6b0", max(12, randomGaussian(23, 7)));
brush.circle(randomGaussian(392, 62), randomGaussian(618, 26), max(28, randomGaussian(46, 9)), true);
}
brush.fill("#b8703a", 30);
brush.circle(618, 592, 42, true);
// 2. the veil: ground laid back over everything above the cloth's far edge,
// so the plane ends in one flat line instead of fogging up behind the egg
brush.fillBleed(0.12, "out");
brush.fillTexture(0.24, 0.6);
const veil = [
[-30, 300], [830, 300], [828, EDGE + 6], [700, EDGE + 1], [560, EDGE - 3],
[420, EDGE + 2], [280, EDGE - 2], [140, EDGE + 3], [-28, EDGE - 1],
];
brush.fill("#17130f", 244);
shape(0.05, veil, 1.2);
brush.fill("#17130f", 226);
shape(0.05, veil, 2);
brush.fill("#17130f", 208);
shape(0.05, veil, 2.6);
// nothing is painted on the wall above the cloth, and it stays that way
// vignette — sink the corners of the plane back toward black
brush.fillBleed(0.62, "out");
for (let i = 0; i < 11; i++) {
const left = i < 7;
brush.fill("#0b0908", max(70, randomGaussian(140, 24)));
const vx = left ? randomGaussian(28, 42) : randomGaussian(788, 28);
brush.circle(vx, randomGaussian(left ? 664 : 736, 62), max(56, randomGaussian(92, 15)), true);
}
for (const c of [[330, 792, 130, 122], [146, 770, 110, 98], [628, 798, 118, 96],
[742, 742, 108, 104], [676, 712, 86, 84], [770, 640, 78, 92]]) {
brush.fill("#0b0908", c[3]);
brush.circle(c[0], c[1], c[2], true);
}
// three dark raking strokes — the weave of the cloth read across the light
brush.noFill();
brush.set("cpencil", "#0b0908", 0.8);
brush.line(238, 616, 492, 606);
brush.line(352, 716, 660, 700);
brush.line(576, 594, 752, 588);
// 3. the shadow. First the cloth is brightened just above where its edge will fall —
// a cast shadow only cuts if the light beside it is up
brush.noStroke();
brush.fillBleed(0.42, "out");
brush.fillTexture(0.34, 0.22);
for (let i = 1; i < 15; i++) {
brush.fill(random() < 0.4 ? "#f5efe0" : "#cfc6b0", max(24, randomGaussian(58, 12)));
brush.circle(SHADOW[i][0] + random(-6, 6), SHADOW[i][1] - 25 + random(-6, 6), max(18, randomGaussian(29, 5)), true);
}
brush.fillBleed(0.5, "out");
for (let i = 17; i < 27; i += 2) {
brush.fill("#cfc6b0", max(11, randomGaussian(20, 6)));
brush.circle(SHADOW[i][0] + random(-6, 6), SHADOW[i][1] + 28 + random(-6, 6), max(18, randomGaussian(28, 5)), true);
}
brush.fillBleed(0.62, "out");
brush.fillTexture(0.28, 0.25);
for (let i = 0; i < 8; i++) {
const t = i / 7;
brush.fill("#0b0908", max(50, randomGaussian(84, 14)));
brush.circle(230 + t * 350 + random(-12, 12), 652 - t * 12 + random(-8, 8), max(22, randomGaussian(38 - 12 * t, 5)), true);
}
brush.fillBleed(0.04, "in");
brush.fillTexture(0.5, 0.95);
brush.fill("#0b0908", 252);
shape(0.18, SHADOW, 0.9);
brush.fill("#0b0908", 208);
shape(0.2, SHADOW, 1.8);
brush.fill("#17130f", 130);
shape(0.22, SHADOW, 2.6);
// the sharp upper lip of the shadow — the one contour drawn rather than washed
strokeMark("rotring", "#0b0908", 1.3, 0.14, SHADOW.slice(0, 15));
strokeMark("pen", "#0b0908", 1.0, 0.18, SHADOW.slice(25));
// 4. the egg — three passes for the shell, then the light laid on with soft loaded marks
eggPass("#7a715f", 168, -2, 3, 1.0, 1.0, 0.18, 0.024);
eggPass("#cfc6b0", 206, 1, 0, 0.99, 1.0, 0.16, 0.018);
eggPass("#f5efe0", 168, 5, -6, 0.82, 0.85, 0.26, 0.02);
brush.noStroke();
brush.fillBleed(0.6, "out");
brush.fillTexture(0.34, 0.2);
for (const m of [[554, 470, 29, 122], [562, 490, 20, 104], [546, 448, 20, 92], [564, 512, 16, 76]]) {
brush.fill("#f5efe0", m[3] + random(-6, 6));
brush.circle(m[0], m[1], m[2], true);
}
// the shell's dark side, sunk back into the ground
eggCrescent(-0.44, 0.99, 29, "#17130f", 206);
eggCrescent(-0.08, 0.93, 16, "#0b0908", 158);
// bounced warmth low on the shadow side — the only hot note in the picture
brush.noStroke();
brush.fillBleed(0.38, "out");
brush.fill("#b8703a", 92);
brush.circle(505, 518, 13, true);
brush.fill("#b8703a", 56);
brush.circle(496, 500, 9, true);
// the terminator, felt with a few strokes across the turn of the form
strokeMark("cpencil", "#7a715f", 0.85, 0.6, [[509, 448], [517, 484], [521, 520]]);
strokeMark("2H", "#7a715f", 0.7, 0.6, [[518, 444], [527, 482], [531, 516]]);
brush.line(502, 464, 515, 468);
brush.line(507, 498, 520, 500);
// 5. the cup, painted over the shell's lower third so the egg sits down into it
brush.noStroke();
brush.fillBleed(0.14, "out");
brush.fillTexture(0.5, 0.7);
brush.fill("#cfc6b0", 196);
shape(0.42, [
[485, 532], [491, 552], [503, 572], [516, 586], [523, 593],
[552, 593], [559, 585], [572, 570], [588, 550], [595, 532],
], 1.2);
// porcelain turns faster than shell: the light on the right flank is soft-edged,
// laid in with loaded marks, and the left flank drops straight to the ground
brush.fillBleed(0.5, "out");
brush.fillTexture(0.32, 0.2);
for (const m of [[576, 546, 16, 150], [564, 566, 13, 124], [553, 582, 10, 96]]) {
brush.fill("#f5efe0", m[3] + random(-6, 6));
brush.circle(m[0], m[1], m[2], true);
}
brush.fillBleed(0.2, "in");
brush.fillTexture(0.5, 0.6);
brush.fill("#17130f", 202);
shape(0.42, [[485, 532], [508, 538], [514, 566], [525, 594], [511, 580], [493, 554]], 1.1);
// the bowl's underside, the darkest note on the porcelain
brush.fill("#0b0908", 190);
shape(0.4, [[516, 586], [558, 586], [551, 602], [523, 603]], 1);
// stem and foot
brush.fillBleed(0.14, "out");
brush.fill("#cfc6b0", 204);
shape(0.3, [[524, 592], [551, 592], [552, 616], [574, 626], [500, 626], [522, 616]], 1.1);
brush.fillBleed(0.4, "out");
brush.fill("#f5efe0", 156);
brush.circle(548, 606, 9, true);
brush.circle(560, 622, 8, true);
brush.fillBleed(0.18, "in");
brush.fill("#17130f", 186);
shape(0.3, [[523, 593], [532, 593], [529, 616], [509, 625], [501, 625], [521, 614]], 1);
brush.fillBleed(0.1, "out");
brush.fillTexture(0.5, 0.7);
brush.fill("#cfc6b0", 208);
brush.beginShape(0.72);
for (let i = 0; i < 18; i++) {
const a = (i / 18) * 360;
brush.vertex(FOOT.x + cos(a) * FOOT.rx + random(-1, 1), FOOT.y + sin(a) * FOOT.ry + random(-0.8, 0.8));
}
brush.endShape(true);
brush.fill("#f5efe0", 190);
shape(0.5, [[540, 638], [562, 634], [575, 627], [556, 623], [540, 626]], 1);
brush.fill("#17130f", 170);
shape(0.5, [[498, 628], [514, 635], [508, 638], [497, 632]], 1);
// 6. the near rim, crossing in front of the shell
brush.fillBleed(0.08, "out");
brush.fillTexture(0.6, 0.88);
brush.fill("#cfc6b0", 220);
shape(0.55, [
[485, 531], [508, 544], [538, 549], [568, 544], [595, 530],
[592, 523], [564, 537], [536, 541], [508, 536], [487, 522],
], 1);
brush.fill("#f5efe0", 214);
shape(0.55, [[540, 548], [570, 543], [592, 530], [588, 525], [565, 537], [541, 541]], 1.1);
brush.fill("#17130f", 186);
shape(0.55, [[485, 531], [502, 542], [518, 547], [514, 540], [495, 529], [487, 521]], 0.9);
// 7. last darks: the hairline where shell meets porcelain, and the contact shadow
strokeMark("2B", "#0b0908", 1.05, 0.5, [[492, 529], [518, 539], [542, 543], [570, 538], [591, 526]]);
brush.noStroke();
brush.fillBleed(0.08, "in");
brush.fillTexture(0.66, 0.94);
brush.fill("#0b0908", 244);
shape(0.5, [[500, 628], [537, 637], [576, 632], [571, 641], [534, 645], [503, 637]], 1);
brush.fillBleed(0.48, "out");
brush.fill("#0b0908", 190);
brush.circle(505, 639, 16, true);
// 8. the lamp's edge on the shell, put down last and kept small
brush.fillBleed(0.55, "out");
brush.fillTexture(0.3, 0.15);
for (const m of [[562, 460, 10, 196], [570, 474, 7, 150], [553, 450, 8, 120]]) {
brush.fill("#f5efe0", m[3] + random(-8, 8));
brush.circle(m[0], m[1], m[2], true);
}
strokeMark("cpencil", "#f5efe0", 0.75, 0.55, [[532, 429], [558, 440], [574, 467], [579, 498]]);
strokeMark("2H", "#f5efe0", 0.6, 0.55, [[545, 432], [566, 450], [576, 482]]);
}