Two Pears, One Bruised
2026 · still life
A pear has no edge, only a place where it stops turning toward the light; the bruise is where the form remembers being handled.
two pears on a plain cloth in the lower right, built from facets of ochre and cool grey with no outline, the nearer one bruised on its lit shoulder, the rest of the paper left empty
painted by Mistral Medium 3.1p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: two pears sit on the lower-right thirds crossing, the near one bruised on its lit shoulder; the upper-left two-thirds is left as bare paper
// light comes from the upper right, so both bodies dissolve leftward into a cool shadow that pools along the table
// layering: pale table and air washes, cast shadow, each pear built from facets of colour back-to-front, then bruise, hatched shadow flanks, contact darks and broken contour last
const PALETTE = [
"#eae4d6", // ground — warm paper
"#e0c88f", // light — the sunlit turn of the fruit
"#c8a044", // mass — the ochre body
"#87904f", // mid — unripe green facets
"#5b6872", // shadow — cool blue-grey
"#8a4536", // accent — the bruise, rationed
"#332f29", // ink — the last darks
];
// The half-width of the fruit at sixteen stations, t = 0 at the stem to 1 at the
// base: a knob at the stem, a near-straight neck, a fast swell, a belly that
// holds, then a base that flattens where the fruit takes the table. Sampled
// rather than solved — a closed form gave a cone, and past t = 1 a negative sine
// under a fractional power gave NaN, which silently emptied every fill polygon
// downstream and left p5.brush reading x off an absent vertex.
const PEAR_PROFILE = [0.16, 0.2, 0.24, 0.27, 0.3, 0.34, 0.42, 0.56, 0.72, 0.86, 0.95, 1.0, 1.0, 0.95, 0.82, 0.44];
function pearRadius(t) {
const u = constrain(t, 0, 1) * (PEAR_PROFILE.length - 1);
const i = Math.min(PEAR_PROFILE.length - 2, Math.floor(u));
return lerp(PEAR_PROFILE[i], PEAR_PROFILE[i + 1], u - i);
}
function pearPoint(p, t, u) {
const tc = constrain(t, 0, 1);
const r = pearRadius(tc) * p.w;
const ax = p.bx + p.lean * (1 - tc) * p.h;
return [ax + constrain(u, -1.3, 1.3) * r, p.by - (1 - tc) * p.h];
}
// positive shade is turned toward the light, negative is turned away
function pearShade(t, u) {
return constrain(0.74 * u - 0.9 * (t - 0.38), -1, 1);
}
// Cool grey is held back for the flank that is genuinely turned away; letting it
// into the mid tones punched grey windows through the lit side of the fruit.
function facetColour(shade) {
const r = random();
if (shade > 0.5) return r < 0.7 ? "#e0c88f" : "#c8a044";
if (shade > 0.06) return r < 0.58 ? "#c8a044" : r < 0.84 ? "#e0c88f" : "#87904f";
if (shade > -0.3) return r < 0.46 ? "#87904f" : r < 0.8 ? "#c8a044" : "#e0c88f";
return r < 0.52 ? "#5b6872" : r < 0.82 ? "#87904f" : "#332f29";
}
// A closed band of the body's surface, for hatching one flank without touching
// the lit side. Built by pushing so the point list can never carry a hole.
function bandShape(p, tA, tB, uOut, uIn) {
const pts = [];
const steps = 7;
for (let k = 0; k <= steps; k++) pts.push(pearPoint(p, lerp(tA, tB, k / steps), uOut));
for (let k = steps; k >= 0; k--) pts.push(pearPoint(p, lerp(tA, tB, k / steps), uIn));
return pts;
}
// The body laid in whole first, so the facets have a mass to sit on rather than
// paper to float over.
function pearSilhouette(p, col, op, bleed, swell) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.46, 0.44);
brush.fill(col, op);
brush.beginShape(0.34);
for (let k = 0; k <= 18; k++) {
const t = k / 18;
const q = pearPoint(p, t, -swell + random(-0.04, 0.03));
brush.vertex(q[0], q[1]);
}
for (let k = 18; k >= 0; k--) {
const t = k / 18;
const q = pearPoint(p, t, swell + random(-0.03, 0.04));
brush.vertex(q[0], q[1]);
}
brush.endShape(true);
}
// Cezanne's method as code: no outline, only planes of colour set edge to edge,
// each one a little larger or smaller than its neighbour so the form turns. The
// column count shifts row to row so the planes never stack into a plaid.
function facetPass(p, rows, cols, tLo, tHi, uLo, uHi, spill, opBase) {
brush.noStroke();
brush.fillBleed(0.15, "out");
brush.fillTexture(0.33, 0.34);
for (let i = 0; i < rows; i++) {
const t0 = lerp(tLo, tHi, i / rows);
const t1 = lerp(tLo, tHi, (i + 1) / rows);
const cn = Math.max(2, cols - (i % 3 === 1 ? 1 : 0));
for (let j = 0; j < cn; j++) {
const u0 = lerp(uLo, uHi, j / cn);
const u1 = lerp(uLo, uHi, (j + 1) / cn);
const tm = (t0 + t1) / 2;
const um = (u0 + u1) / 2;
const shade = pearShade(tm, um);
const edge = j === 0 || j === cn - 1 ? spill : 0;
const jit = 0.07;
const a = pearPoint(p, t0 + random(-0.012, 0.012), u0 - edge + random(-jit, jit));
const b = pearPoint(p, t0 + random(-0.012, 0.012), u1 + edge + random(-jit, jit));
const c = pearPoint(p, t1 + random(-0.012, 0.012), u1 + edge + random(-jit, jit));
const d = pearPoint(p, t1 + random(-0.012, 0.012), u0 - edge + random(-jit, jit));
// a plane thinner than the brush is a smudge, not a facet
if (Math.abs(b[0] - a[0]) + Math.abs(c[0] - d[0]) < 5) continue;
const op = constrain(randomGaussian(opBase + 34 * Math.abs(shade), 18), 74, 214);
brush.fill(facetColour(shade), op);
brush.polygon([a, b, c, d]);
}
}
}
function flankHatch(p, tA, tB, uOut, uIn, tool, col, weight, gap, ang) {
brush.hatchStyle(tool, col, weight);
brush.hatch(gap, ang, { rand: 0.2, continuous: false });
brush.noStroke();
brush.noFill();
brush.polygon(bandShape(p, tA, tB, uOut, uIn));
brush.noHatch();
}
function contourFragment(p, tA, tB, side, col, w, curl) {
brush.noFill();
brush.set(curl, col, w);
brush.beginShape(0.35);
for (let k = 0; k <= 5; k++) {
const t = lerp(tA, tB, k / 5);
const q = pearPoint(p, t, side * (1 + random(-0.02, 0.05)));
brush.vertex(q[0], q[1]);
}
brush.endShape(false);
}
const NEAR = { bx: 508, by: 610, h: 256, w: 80, lean: -0.045 };
const FAR = { bx: 622, by: 586, h: 160, w: 62, lean: 0.09 };
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#eae4d6");
// --- the table: pale cool air laid in first, running off the right edge
brush.noStroke();
brush.fillBleed(0.62, "out");
brush.fillTexture(0.3, 0.4);
brush.fill("#5b6872", 34);
brush.circle(586, 660, 112, true);
brush.fill("#5b6872", 26);
brush.circle(740, 640, 88, true);
brush.fill("#5b6872", 16);
brush.circle(440, 688, 60, true);
brush.fill("#e0c88f", 36);
brush.circle(612, 500, 96, true);
brush.fill("#e0c88f", 22);
brush.circle(726, 446, 66, true);
brush.fill("#e0c88f", 14);
brush.circle(492, 436, 50, true);
// --- cast shadow: low, long and pressed close, so it lies on the cloth
brush.fillBleed(0.3, "out");
brush.fillTexture(0.5, 0.52);
brush.fill("#5b6872", 170);
brush.circle(474, 618, 28, true);
brush.fill("#5b6872", 146);
brush.circle(438, 624, 23, true);
brush.fill("#5b6872", 112);
brush.circle(404, 630, 18, true);
brush.fill("#5b6872", 78);
brush.circle(374, 635, 13, true);
brush.fill("#5b6872", 46);
brush.circle(348, 640, 10, true);
brush.fill("#5b6872", 150);
brush.circle(584, 592, 22, true);
brush.fill("#5b6872", 104);
brush.circle(552, 602, 17, true);
brush.fill("#87904f", 66);
brush.circle(500, 626, 22, true);
// a fine tooth inside the shadow only — enough to read as dark cloth, not cloth
brush.hatchStyle("2H", "#5b6872", 0.7);
brush.hatch(5, 168, { rand: 0.16, continuous: false });
brush.noStroke();
brush.noFill();
brush.polygon([
[386, 632],
[482, 615],
[488, 638],
[380, 651],
]);
brush.noHatch();
// --- far pear, painted back-to-front and kept a shade cooler than the near one
pearSilhouette(FAR, "#c8a044", 132, 0.3, 0.98);
pearSilhouette(FAR, "#87904f", 82, 0.18, 0.86);
facetPass(FAR, 8, 3, 0.02, 0.99, -1, 1, 0.08, 110);
facetPass(FAR, 4, 2, 0.34, 0.94, -1, -0.1, 0.05, 124);
facetPass(FAR, 3, 2, 0.5, 0.94, 0.1, 1, 0.03, 96);
flankHatch(FAR, 0.24, 0.94, -1.02, -0.34, "2H", "#5b6872", 0.85, 6, 84);
// --- near pear, the larger and warmer mass, overlapping its neighbour
pearSilhouette(NEAR, "#c8a044", 152, 0.3, 0.99);
pearSilhouette(NEAR, "#e0c88f", 96, 0.2, 0.88);
facetPass(NEAR, 11, 4, 0.01, 0.995, -1, 1, 0.12, 120);
facetPass(NEAR, 5, 2, 0.28, 0.96, -1, -0.14, 0.06, 134);
facetPass(NEAR, 4, 2, 0.44, 0.97, 0.12, 1, 0.03, 104);
facetPass(NEAR, 3, 2, 0.1, 0.46, -0.5, 0.7, 0.02, 98);
// the shadow side, hatched twice: cool grey over the whole flank, ink in the well
flankHatch(NEAR, 0.2, 0.95, -1.02, -0.3, "2H", "#5b6872", 0.9, 5, 76);
flankHatch(NEAR, 0.62, 0.96, -1.0, -0.46, "2H", "#332f29", 0.75, 8, 116);
// --- the bruise: a soft russet stain on the lit shoulder, then its darker core
brush.noStroke();
brush.fillBleed(0.5, "out");
brush.fillTexture(0.52, 0.48);
const b1 = pearPoint(NEAR, 0.35, 0.4);
brush.fill("#8a4536", 120);
brush.circle(b1[0], b1[1], 21, true);
const b2 = pearPoint(NEAR, 0.44, 0.58);
brush.fill("#8a4536", 150);
brush.circle(b2[0], b2[1], 14, true);
const b3 = pearPoint(NEAR, 0.26, 0.22);
brush.fill("#8a4536", 84);
brush.circle(b3[0], b3[1], 11, true);
brush.fillBleed(0.14, "out");
const b4 = pearPoint(NEAR, 0.39, 0.5);
brush.fill("#8a4536", 208);
brush.circle(b4[0], b4[1], 8.5, true);
brush.fill("#8a4536", 176);
brush.circle(b4[0] - 7, b4[1] + 6, 6, true);
brush.fill("#8a4536", 158);
brush.circle(b2[0] + 4, b2[1] + 8, 5, true);
brush.fill("#332f29", 150);
brush.circle(b4[0] + 2, b4[1] + 1.5, 3.4, true);
// --- darks last: where the fruit meets the cloth, and where the two bodies touch
brush.noFill();
brush.set("2B", "#332f29", 2.1);
brush.line(480, 608, 522, 611);
brush.set("2B", "#332f29", 1.5);
brush.line(524, 611, 546, 608);
brush.set("2B", "#332f29", 1.7);
brush.line(602, 585, 640, 587);
brush.set("2B", "#5b6872", 1.4);
brush.beginShape(0.4);
brush.vertex(568, 584);
brush.vertex(574, 552);
brush.vertex(578, 524);
brush.endShape(false);
brush.set("2H", "#5b6872", 1.0);
brush.line(580, 512, 578, 486);
// --- broken contour: only the shadow side gets a line, and never a whole one
contourFragment(NEAR, 0.52, 0.8, -1, "#332f29", 1.4, "2B");
contourFragment(NEAR, 0.84, 0.96, -1, "#332f29", 1.0, "2B");
contourFragment(NEAR, 0.16, 0.34, -1, "#5b6872", 0.9, "2B");
contourFragment(FAR, 0.46, 0.72, -1, "#332f29", 1.1, "2B");
contourFragment(FAR, 0.84, 0.96, 1, "#5b6872", 0.95, "2B");
contourFragment(NEAR, 0.88, 0.97, 1, "#332f29", 1.0, "2B");
// --- stems, short and seated right on the shoulder of the fruit
brush.noFill();
brush.set("2B", "#332f29", 1.7);
const s1 = pearPoint(NEAR, 0.01, -0.3);
brush.beginShape(0.4);
brush.vertex(s1[0], s1[1] + 6);
brush.vertex(s1[0] - 4, s1[1] - 5);
brush.vertex(s1[0] - 1, s1[1] - 16);
brush.endShape(false);
brush.set("2B", "#332f29", 1.3);
const s2 = pearPoint(FAR, 0.01, 0.3);
brush.beginShape(0);
brush.vertex(s2[0], s2[1] + 5);
brush.vertex(s2[0] + 4, s2[1] - 9);
brush.endShape(false);
// two slow strokes for the cloth, out at the edges of the shadow, nothing above
brush.set("cpencil", "#5b6872", 1.2);
brush.field("hand");
brush.flowLine(326, 654, 62, 8);
brush.flowLine(704, 646, 66, 178);
brush.noField();
// one seed of the accent adrift on the cloth, well away from the fruit
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fill("#8a4536", 176);
brush.circle(400, 694, 4.4, true);
}