What the Cup Says
2026 · gesture
Two strokes are enough for the cup because the steam does all the talking.
steam rising from a barely-drawn cup, three ribbons drifting right
painted by oXAlphap5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: cup hinted by two pen strokes at bottom-center-left; three steam ribbons own the upper two-thirds, wandering wider as they rise and drifting right
// layering: one faint blue shadow at the cup's foot, the two cup strokes, then each ribbon as a long 2B gesture with a lighter echo pass; the third ribbon crosses the others
const PALETTE = [
"#f2e9d8", // warm paper
"#55524c", // graphite ink, cup and near steam
"#8a867e", // mid graphite, echo passes
"#b5b1a8", // far graphite, faintest steam
"#a5b4c6", // accent: the one blue shadow
];
// one rising ribbon: wander amplitude grows with height, whole path drifts right
function ribbon(x0, y0, drift, amp, phase, col, w) {
const pts = [];
const top = 88 + randomGaussian(0, 14);
const steps = 30;
for (let i = 0; i <= steps; i++) {
const t = i / steps; // 0 at cup, 1 at top
const y = y0 - (y0 - top) * t;
const wander =
Math.sin(t * 7.2 + phase) * amp * t * t +
(noise(phase * 10 + t * 3.1) - 0.5) * amp * 1.6 * t;
const x = x0 + drift * t * t + wander;
pts.push([x, y]);
}
// main pass
brush.set("2B", col, w);
brush.beginShape(0.75);
for (const p of pts) brush.vertex(p[0], p[1]);
brush.endShape(false);
// lighter echo, offset a breath to the right, only the upper reach
brush.set("2H", "#b5b1a8", w * 0.8);
brush.beginShape(0.75);
for (let i = 8; i <= steps; i++) {
brush.vertex(pts[i][0] + randomGaussian(4, 1.5), pts[i][1] + randomGaussian(0, 1.5));
}
brush.endShape(false);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.3);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#f2e9d8");
const cupX = 296; // bottom-center-left
const cupY = 636;
// the one faint blue shadow, pooling right of the cup's foot
brush.noStroke();
brush.fillBleed(0.55, "out");
brush.fillTexture(0.4, 0.5);
brush.fill("#a5b4c6", 46);
brush.beginShape(0.7);
brush.vertex(cupX - 26, cupY + 74);
brush.vertex(cupX + 92, cupY + 66);
brush.vertex(cupX + 148, cupY + 84);
brush.vertex(cupX + 60, cupY + 96);
brush.endShape(true);
brush.fill("#a5b4c6", 28);
brush.beginShape(0.7);
brush.vertex(cupX + 10, cupY + 78);
brush.vertex(cupX + 110, cupY + 74);
brush.vertex(cupX + 66, cupY + 90);
brush.endShape(true);
// the cup: exactly two pen strokes
// stroke one — the rim, an open ellipse arc
brush.set("pen", "#55524c", 1.3);
brush.beginShape(0.8);
brush.vertex(cupX - 64, cupY - 2);
brush.vertex(cupX - 20, cupY + 10);
brush.vertex(cupX + 40, cupY + 8);
brush.vertex(cupX + 66, cupY - 6);
brush.endShape(false);
// stroke two — the body, one falling curve catching the base
brush.set("pen", "#55524c", 1.1);
brush.beginShape(0.7);
brush.vertex(cupX - 62, cupY + 2);
brush.vertex(cupX - 52, cupY + 52);
brush.vertex(cupX - 8, cupY + 72);
brush.vertex(cupX + 46, cupY + 60);
brush.vertex(cupX + 62, cupY + 6);
brush.endShape(false);
// three steam ribbons, drifting right, wander growing with height
ribbon(cupX - 24, cupY - 14, 148, 44, 0.9, "#55524c", 1.7); // nearest, darkest
ribbon(cupX + 30, cupY - 12, 190, 58, 2.1, "#8a867e", 1.4); // drifting furthest
// the third crosses the others: starts right, thrown left then over to the right
ribbon(cupX + 52, cupY - 16, -60, 88, 4.4, "#8a867e", 1.2);
// a last breath of steam, almost gone, high right
brush.set("2H", "#b5b1a8", 1);
brush.beginShape(0.8);
brush.vertex(508, 196);
brush.vertex(534, 152);
brush.vertex(528, 108);
brush.vertex(552, 72);
brush.endShape(false);
}