Two Waters Refusing
2026 · flow
Two bodies of water touch for miles and agree on nothing; the eddy is the argument, drawn out until the current forgets what it was about.
a river's silt load entering clear water from the left edge and fanning off the lower right, the boundary scrolling into eddies, the upper right left as open water
painted by GPT-5.6p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: the silt load enters narrow at the left edge, swells along a diagonal and runs off the lower-right corner;
// the upper-right quarter and the lower-left corner stay bare paper — clear water, refusing the pigment.
// layering: cold washes on the clear side, silt filling the wedge, a churned pool stacked dark in the lower right,
// a broken rope of pigment along the boundary, scrolls curling off it, then clear water hooked back into the load.
const PALETTE = [
"#ece3d1", // ground — paper, and the clear water itself
"#c69a4c", // mass — silt, sunlit surface
"#9c6b2c", // mass — silt, mid load
"#4d3013", // shadow — silt, deepest churn
"#2e534f", // shadow — cold water on the clear side
"#1d1b16", // accent — the darkest bites of the meeting, rationed
];
// the two banks of the plume: the upper one meets clear water, the lower fans off the bottom
const UPPER = [
[-40, 330], [110, 338], [250, 356], [386, 390], [506, 442], [624, 508], [748, 586], [852, 648],
];
const LOWER = [
[-40, 396], [122, 420], [266, 466], [406, 540], [528, 626], [648, 714], [770, 800], [852, 844],
];
function along(pts, t) {
const s = constrain(t, 0, 0.9999) * (pts.length - 1);
const i = floor(s);
const f = s - i;
return [lerp(pts[i][0], pts[i + 1][0], f), lerp(pts[i][1], pts[i + 1][1], f)];
}
// the boundary wobbles: scalloped where the current has bitten into it
function rimY(t) {
const p = along(UPPER, t);
return p[1] + (noise(t * 5.2, 8.3) - 0.5) * 40;
}
function siltTone(depth) {
const r = random();
if (depth > 0.66) return r < 0.56 ? "#4d3013" : r < 0.88 ? "#9c6b2c" : "#c69a4c";
if (depth > 0.34) return r < 0.5 ? "#9c6b2c" : r < 0.86 ? "#c69a4c" : "#4d3013";
return r < 0.76 ? "#c69a4c" : "#9c6b2c";
}
function blob(x, y, r, tone, alpha, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.55, 0.5);
brush.fill(tone, alpha);
brush.circle(x, y, r, true);
}
// pigment dropped four times in one place until it holds
function heavy(x, y, r, tone, alpha) {
blob(x, y, r, tone, alpha * 0.7, 0.32);
blob(x + random(-9, 9), y + random(-8, 8), r * 0.8, tone, alpha, 0.16);
blob(x + random(-12, 12), y + random(-10, 10), r * 0.58, tone, alpha, 0.08);
blob(x + random(-14, 14), y + random(-12, 12), r * 0.36, tone, alpha, 0.05);
}
// a lick of silt reaching up across the boundary into the clear side
function tongue(x, y, ang, len, tone, alpha) {
brush.noStroke();
brush.fillBleed(0.28, "out");
brush.fillTexture(0.55, 0.62);
brush.fill(tone, alpha);
brush.beginShape(0.7);
brush.vertex(x, y);
brush.vertex(x + Math.cos(ang - 0.5) * len * 0.46, y + Math.sin(ang - 0.5) * len * 0.46);
brush.vertex(x + Math.cos(ang) * len * (1 + random(-0.08, 0.14)), y + Math.sin(ang) * len);
brush.vertex(x + Math.cos(ang + 0.58) * len * 0.54, y + Math.sin(ang + 0.58) * len * 0.54);
brush.endShape(true);
}
// a scroll of the boundary, laid as a rope of pigment winding inward,
// with clear water hooked into its eye the other way
function curl(cx, cy, r, dir, tilt, aspect, deep) {
const n = 10;
const turns = 1.34 + random(-0.12, 0.2);
for (let i = 0; i <= n; i++) {
const t = i / n;
const a = tilt + dir * t * turns * Math.PI * 2;
const rad = r * (1 - 0.6 * t);
const w = r * (0.4 - 0.24 * t) * (1 + randomGaussian(0, 0.08));
blob(
cx + Math.cos(a) * rad * aspect + random(-3, 3),
cy + Math.sin(a) * rad * 0.78 + random(-3, 3),
Math.max(10, w),
i > n - 5 || random() < 0.55 ? (deep ? "#4d3013" : "#9c6b2c") : "#c69a4c",
Math.max(130, randomGaussian(deep ? 204 : 172, 16)),
0.1 + 0.1 * t,
);
}
// clear water hooked into the eye of the scroll
for (let i = 0; i < 3; i++) {
const a = tilt + 2.2 + dir * (i / 3) * 1.5;
const rad = r * (0.48 - 0.12 * i);
brush.noStroke();
brush.fillBleed(0.14, "out");
brush.fill("#ece3d1", Math.max(190, randomGaussian(232, 12)));
brush.circle(
cx + Math.cos(a) * rad * aspect,
cy + Math.sin(a) * rad * 0.78,
r * (0.3 - 0.05 * i) + random(-1.5, 2),
true,
);
}
// a dry pencil arc riding the outside of the scroll
brush.noFill();
brush.set("cpencil", deep ? "#4d3013" : "#9c6b2c", 2.0 + random(-0.3, 0.6));
brush.beginShape(0.5);
for (let i = 0; i <= 6; i++) {
const t = i / 6;
const a = tilt - 0.36 + dir * t * 0.92 * Math.PI * 2;
const rad = r * (1.2 - 0.26 * t) * (1 + randomGaussian(0, 0.035));
brush.vertex(cx + Math.cos(a) * rad * aspect, cy + Math.sin(a) * rad * 0.78);
}
brush.endShape(false);
}
// a drawn-out lens of cold water, lying with the current
function coldStreak(x, y, len, ang, thick, alpha) {
brush.noStroke();
brush.fillBleed(0.34, "out");
brush.fillTexture(0.42, 0.5);
brush.fill("#2e534f", alpha);
const cx = Math.cos(ang);
const cy = Math.sin(ang);
brush.beginShape(0.6);
brush.vertex(x, y);
brush.vertex(x + cx * len * 0.4 - cy * thick, y + cy * len * 0.4 + cx * thick);
brush.vertex(x + cx * len, y + cy * len);
brush.vertex(x + cx * len * 0.44 + cy * thick * 0.9, y + cy * len * 0.44 - cx * thick * 0.9);
brush.endShape(true);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#ece3d1");
// --- cold water on the clear side, weighted to the upper left -------------
blob(40, 158, 162, "#2e534f", 54, 0.66);
blob(196, 84, 124, "#2e534f", 40, 0.68);
blob(80, 272, 116, "#2e534f", 62, 0.5);
blob(276, 178, 90, "#2e534f", 26, 0.7);
blob(88, 708, 138, "#2e534f", 40, 0.68);
blob(20, 574, 102, "#2e534f", 34, 0.68);
coldStreak(24, 264, 216, 0.13, 22, 74);
coldStreak(126, 220, 180, 0.2, 17, 60);
coldStreak(214, 296, 158, 0.26, 13, 66);
coldStreak(48, 634, 172, 0.34, 16, 52);
coldStreak(160, 668, 132, 0.4, 12, 44);
// --- silt base: washes filling the wedge, never rising over the boundary --
for (let i = 0; i < 12; i++) {
const t = i / 11;
const up = along(UPPER, t);
const lo = along(LOWER, t);
for (let k = 0; k < 2; k++) {
const d = constrain(randomGaussian(0.54, 0.26), 0.06, 0.96);
const x = lerp(up[0], lo[0], d) + randomGaussian(0, 20);
const r = Math.max(42, randomGaussian(58 + 54 * t, 12));
const y = Math.max(lerp(up[1], lo[1], d) + randomGaussian(0, 14), rimY(t) + r * 0.42);
blob(x, y, r, siltTone(0.24 + 0.44 * t), Math.max(88, randomGaussian(124 + 30 * t, 16)), 0.4);
}
}
// --- mid churn, patchier, following the current down the diagonal ---------
for (let i = 0; i < 14; i++) {
const t = constrain(randomGaussian(0.64, 0.22), 0.04, 1);
const up = along(UPPER, t);
const lo = along(LOWER, t);
const d = constrain(0.16 + noise(t * 3.1, 1.7) * 0.72 + randomGaussian(0, 0.12), 0.08, 0.94);
const x = lerp(up[0], lo[0], d) + randomGaussian(0, 18);
const r = Math.max(28, randomGaussian(44 + 32 * t, 9));
const y = Math.max(lerp(up[1], lo[1], d) + randomGaussian(0, 15), rimY(t) + r * 0.38);
blob(x, y, r, siltTone(0.48 + 0.4 * t), Math.max(140, randomGaussian(182 + 20 * t, 18)), 0.18);
}
// --- the churned pool: darkness stacked where the load turns over ---------
for (let i = 0; i < 22; i++) {
const t = constrain(randomGaussian(0.7, 0.13), 0.44, 1);
const up = along(UPPER, t);
const lo = along(LOWER, t);
const d = constrain(randomGaussian(0.42, 0.2), 0.06, 0.86);
const x = lerp(up[0], lo[0], d) + randomGaussian(0, 22);
const y = lerp(up[1], lo[1], d) + randomGaussian(0, 20);
const r = Math.max(24, randomGaussian(40, 8));
blob(x, y, r, random() < 0.72 ? "#4d3013" : "#9c6b2c", Math.max(120, randomGaussian(168, 20)), 0.14);
}
// --- four heavy loads, hand-placed, holding the focal turn ----------------
heavy(556, 574, 72, "#4d3013", 182);
heavy(624, 656, 84, "#4d3013", 170);
heavy(468, 508, 52, "#9c6b2c", 158);
heavy(716, 728, 66, "#4d3013", 176);
// --- the boundary: a broken rope of pigment gathered along the edge -------
for (let i = 0; i < 28; i++) {
const t = i / 27;
if (random() < 0.16) continue;
const x = along(UPPER, t)[0] + randomGaussian(0, 8);
const off = Math.max(5, randomGaussian(11 + 14 * t, 5));
const r = Math.max(11, randomGaussian(15 + 13 * t, 4));
blob(
x,
rimY(t) + off,
r,
t < 0.32 ? (random() < 0.55 ? "#9c6b2c" : "#c69a4c") : random() < 0.66 ? "#4d3013" : "#9c6b2c",
Math.max(160, randomGaussian(214, 18)),
0.06,
);
if (t > 0.4 && random() < 0.55) {
blob(x + random(-9, 9), rimY(t) + off + random(6, 20), r * 0.8, "#4d3013", 196, 0.05);
}
}
// --- tongues of silt licking up into the clear water ---------------------
tongue(120, 334, -1.42, 34, "#c69a4c", 160);
tongue(232, 350, -1.08, 50, "#9c6b2c", 176);
tongue(410, 396, -1.22, 62, "#9c6b2c", 184);
tongue(482, 430, -1.78, 50, "#c69a4c", 154);
tongue(592, 494, -1.28, 78, "#9c6b2c", 176);
tongue(690, 560, -1.64, 54, "#4d3013", 150);
// --- the scrolls, the largest just past the third of the picture ----------
curl(526, 486, 88, 1, -0.66, 1.16, true);
curl(288, 350, 33, -1, 2.34, 1.02, false);
curl(686, 578, 50, 1, 1.14, 1.22, true);
// --- filaments streaming inside the load ---------------------------------
brush.noFill();
brush.set("2H", "#9c6b2c", 1.4);
brush.beginShape(0.7);
brush.vertex(10, 370);
brush.vertex(94, 386);
brush.vertex(176, 414);
brush.vertex(250, 452);
brush.endShape(false);
brush.beginShape(0.7);
brush.vertex(192, 424);
brush.vertex(268, 450);
brush.vertex(344, 488);
brush.vertex(428, 540);
brush.endShape(false);
brush.set("2H", "#4d3013", 1.5);
brush.beginShape(0.7);
brush.vertex(366, 508);
brush.vertex(442, 550);
brush.vertex(504, 600);
brush.vertex(582, 666);
brush.endShape(false);
brush.beginShape(0.7);
brush.vertex(542, 652);
brush.vertex(614, 696);
brush.vertex(678, 746);
brush.vertex(744, 800);
brush.endShape(false);
// --- clear water pulled into the load: reserved paper streaks -------------
brush.noStroke();
brush.fillBleed(0.18, "out");
brush.fillTexture(0.45, 0.6);
brush.fill("#ece3d1", 214);
brush.beginShape(0.62);
brush.vertex(348, 452);
brush.vertex(440, 492);
brush.vertex(504, 556);
brush.vertex(460, 556);
brush.vertex(394, 500);
brush.endShape(true);
brush.fill("#ece3d1", 194);
brush.beginShape(0.62);
brush.vertex(600, 598);
brush.vertex(672, 650);
brush.vertex(724, 724);
brush.vertex(678, 708);
brush.vertex(620, 628);
brush.endShape(true);
brush.fill("#ece3d1", 178);
brush.beginShape(0.62);
brush.vertex(206, 402);
brush.vertex(266, 426);
brush.vertex(294, 468);
brush.vertex(248, 448);
brush.endShape(true);
// --- last darks: the bites where the two waters refuse each other --------
brush.noStroke();
brush.fillBleed(0.05, "out");
brush.fill("#1d1b16", 186);
brush.circle(514, 506, 8.5, true);
brush.fill("#1d1b16", 160);
brush.circle(568, 592, 6.6, true);
brush.fill("#1d1b16", 140);
brush.circle(662, 690, 5.6, true);
brush.fill("#1d1b16", 124);
brush.circle(412, 428, 4.4, true);
brush.fill("#1d1b16", 106);
brush.circle(264, 370, 3.6, true);
brush.noFill();
brush.set("pen", "#1d1b16", 1.2);
brush.beginShape(0.7);
for (let i = 0; i <= 5; i++) {
const t = lerp(0.06, 0.2, i / 5);
brush.vertex(along(UPPER, t)[0], rimY(t) + random(4, 12));
}
brush.endShape(false);
brush.beginShape(0.7);
for (let i = 0; i <= 5; i++) {
const t = lerp(0.42, 0.54, i / 5);
brush.vertex(along(UPPER, t)[0], rimY(t) + random(6, 16));
}
brush.endShape(false);
brush.beginShape(0.7);
for (let i = 0; i <= 5; i++) {
const t = lerp(0.86, 0.97, i / 5);
brush.vertex(along(UPPER, t)[0], rimY(t) + random(8, 22));
}
brush.endShape(false);
}