Eight Arguments
2026 · fauna
Eight limbs that each decide for themselves; the one pointed at you has already made up its mind.
an octopus in the lower-right dark, arms unfurling to the upper left in thinning ink, one arm reaching straight out at the viewer
painted by Claude Opus 5p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: a dark mass crouches in the lower-right corner and five arms unfurl on the
// diagonal to the upper left, ink thinning as they reach; a sixth arm foreshortens straight
// at the viewer, its blunt end and swollen suckers landing on the lower-third intersection.
// layering: pale water wash, the ghost arm behind, arm bodies as dry broad strokes, the corner
// mass, contour line, suckers, then the near arm and the darkest ink. the lower-left stays paper.
const PALETTE = [
"#ece4d3", // ground — warm paper, the water's light
"#7f8f8c", // pale — the arm furthest back, and thinned tips
"#40606a", // wash — arm interiors, water shadow
"#182732", // ink — contour and the corner mass
"#a8492e", // accent — a handful of suckers on the near arm
];
const GROUND = PALETTE[0];
const PALE = PALETTE[1];
const WASH = PALETTE[2];
const INK = PALETTE[3];
const ACCENT = PALETTE[4];
// arms as bare control lines: the creature is a gesture before it is a body
const ARM_LONG = [[702, 698], [604, 646], [502, 606], [402, 562], [306, 502], [226, 420], [196, 330], [238, 268], [312, 262]];
const ARM_HIGH = [[690, 658], [612, 560], [542, 468], [472, 378], [422, 288], [402, 198], [434, 118], [504, 74]];
const ARM_LOW = [[726, 744], [642, 726], [560, 708], [480, 684], [418, 650], [376, 606]];
const ARM_CURL = [[706, 672], [620, 598], [520, 538], [430, 498], [348, 458], [278, 428], [230, 390], [228, 346], [264, 328], [296, 348]];
const ARM_GHOST = [[712, 690], [688, 600], [670, 508], [672, 428], [692, 364]];
// the near arm: how far along, which side, how wide a sucker — hand-set, none alike
const NEAR_SUCKERS = [
[0.09, -1, 0.30], [0.17, 1, 0.27], [0.26, -1, 0.35], [0.33, 1, 0.31],
[0.44, -1, 0.38], [0.51, 1, 0.34], [0.61, -1, 0.41], [0.68, 1, 0.36],
[0.78, -1, 0.43], [0.84, 1, 0.39], [0.93, -1, 0.44], [0.97, 1, 0.33],
];
function easeSmooth(pts, passes) {
let p = pts;
for (let k = 0; k < passes; k++) {
const out = [p[0]];
for (let i = 0; i < p.length - 1; i++) {
const a = p[i];
const b = p[i + 1];
out.push([lerp(a[0], b[0], 0.26), lerp(a[1], b[1], 0.26)]);
out.push([lerp(a[0], b[0], 0.74), lerp(a[1], b[1], 0.74)]);
}
out.push(p[p.length - 1]);
p = out;
}
return p;
}
function normalAt(p, i) {
const a = p[max(0, i - 1)];
const b = p[min(p.length - 1, i + 1)];
const tx = b[0] - a[0];
const ty = b[1] - a[1];
const m = Math.hypot(tx, ty) || 1;
return [-ty / m, tx / m];
}
// a stroke that runs along an arm, offset sideways, over one third of its length
function ribbon(p, HW, off, i0, i1, name, col, wt, curv) {
brush.noFill();
brush.set(name, col, wt);
brush.beginShape(curv);
for (let i = i0; i <= i1; i += 3) {
const nrm = normalAt(p, i);
brush.vertex(p[i][0] + nrm[0] * HW[i] * off, p[i][1] + nrm[1] * HW[i] * off);
}
const nrm = normalAt(p, i1);
brush.vertex(p[i1][0] + nrm[0] * HW[i1] * off, p[i1][1] + nrm[1] * HW[i1] * off);
brush.endShape(false);
}
// one arm: dry broad strokes for the flesh, a contour that thins and pales as it
// reaches, and a sucker row grading down toward the tip
function arm(ctrl, w0, w1, wt, side, nz, ghost) {
const p = easeSmooth(ctrl, 3);
const n = p.length;
const HW = [];
for (let i = 0; i < n; i++) {
const t = i / (n - 1);
HW.push(lerp(w0, w1, pow(t, 0.85)) * (0.86 + noise(i * 0.07, nz) * 0.3) * 0.5);
}
const body = ghost ? PALE : WASH;
const edge = ghost ? PALE : INK;
const thinned = lerpColor(color(edge), color(PALE), 0.55);
const faded = lerpColor(color(edge), color(GROUND), 0.6);
const cuts = [0, 0.4, 0.72, 1];
const idx = [
floor(cuts[0] * (n - 1)), floor(cuts[1] * (n - 1)),
floor(cuts[2] * (n - 1)), n - 1,
];
// flesh: three dry passes per third, the near third heaviest
for (let s = 0; s < 3; s++) {
const hw = HW[idx[s]];
const dim = ghost ? 0.55 : 1;
ribbon(p, HW, -0.34, idx[s], idx[s + 1], "cpencil", body, hw * 0.5 * dim, 0.5);
ribbon(p, HW, 0.3, idx[s], idx[s + 1], "cpencil", s === 0 ? body : PALE, hw * 0.42 * dim, 0.5);
if (s < 2) ribbon(p, HW, -0.02, idx[s], idx[s + 1], "charcoal", s === 0 ? INK : body, hw * 0.24 * dim, 0.5);
}
// contour, both edges, thinning and paling toward the tip
const cols = [edge, thinned, faded];
const wts = [wt, wt * 0.66, wt * 0.4];
for (let s = 0; s < 3; s++) {
for (let d = 0; d < 2; d++) {
ribbon(p, HW, d === 0 ? 1 : -1, idx[s], idx[s + 1], s === 0 ? "2B" : "pen", cols[s], wts[s], 0.55);
}
}
// suckers, thinning out before the tip lets go
for (let i = 5; i < n - 6; i += 4) {
const t = i / (n - 1);
if (random() < 0.1 + t * 0.4) continue;
const nrm = normalAt(p, i);
const off = HW[i] * (0.44 + random(-0.06, 0.08));
const x = p[i][0] + nrm[0] * off * side;
const y = p[i][1] + nrm[1] * off * side;
const r = max(1.4, HW[i] * (0.42 + random(-0.07, 0.07)) * (1 - t * 0.4));
brush.noFill();
brush.set("pen", t < 0.45 ? edge : lerpColor(color(edge), color(PALE), 0.5), 0.85 - t * 0.35);
brush.circle(x, y, r, true);
if (!ghost && t < 0.36 && random() < 0.45) {
brush.set("2B", INK, r * 0.5);
brush.line(x - 1, y, x + 1.4, y + 0.6);
}
}
}
// the arm coming at you: short, widening, blunt-ended, its suckers seen face-on
function nearArm() {
const bx = 706;
const by = 702;
const len = Math.hypot(540 - bx, 552 - by);
const ax = (540 - bx) / len;
const ay = (552 - by) / len;
const nx = -ay;
const ny = ax;
const half = [];
const cx = [];
const cy = [];
for (let k = 0; k <= 16; k++) {
const t = k / 16;
half.push(lerp(15, 38, pow(t, 0.72)) * (0.94 + noise(t * 3.1, 61.4) * 0.16));
cx.push(bx + ax * len * t + noise(t * 2.4, 9.1) * 7 - 3.5);
cy.push(by + ay * len * t + noise(t * 2.4, 3.7) * 7 - 3.5);
}
// one true wash here — this is the arm nearest the eye, it may be wet
brush.noStroke();
brush.fillBleed(0.4, "out");
brush.fillTexture(0.6, 0.5);
brush.fill(WASH, 104);
brush.beginShape(0.35);
for (let k = 0; k <= 16; k += 2) brush.vertex(cx[k] + nx * half[k], cy[k] + ny * half[k]);
for (let k = 16; k >= 0; k -= 2) brush.vertex(cx[k] - nx * half[k], cy[k] - ny * half[k]);
brush.endShape(true);
brush.fill(INK, 58);
brush.circle(520, 574, 19, true);
brush.noFill();
brush.set("2B", INK, 2.1);
brush.beginShape(0.5);
for (let k = 0; k <= 16; k += 2) brush.vertex(cx[k] + nx * half[k], cy[k] + ny * half[k]);
brush.endShape(false);
brush.set("2B", INK, 1.8);
brush.beginShape(0.5);
for (let k = 0; k <= 16; k += 2) brush.vertex(cx[k] - nx * half[k], cy[k] - ny * half[k]);
brush.endShape(false);
brush.set("charcoal", INK, 1.6);
brush.beginShape(0.7);
brush.vertex(cx[16] + nx * half[16], cy[16] + ny * half[16]);
brush.vertex(536, 528);
brush.vertex(510, 550);
brush.vertex(524, 580);
brush.vertex(cx[16] - nx * half[16], cy[16] - ny * half[16]);
brush.endShape(false);
for (let k = 0; k < NEAR_SUCKERS.length; k++) {
const t = NEAR_SUCKERS[k][0];
const side = NEAR_SUCKERS[k][1];
const h = lerp(15, 38, pow(t, 0.72));
const sx = bx + ax * len * t + nx * h * 0.46 * side + random(-1.8, 1.8);
const sy = by + ay * len * t + ny * h * 0.46 * side + random(-1.8, 1.8);
const r = h * NEAR_SUCKERS[k][2];
brush.noStroke();
brush.fillBleed(0.26, "out");
brush.fill(t > 0.68 && random() < 0.6 ? ACCENT : WASH, 92 + random(-18, 24));
brush.circle(sx, sy, r * 0.86, true);
brush.noFill();
brush.set("2B", INK, 1.2 + t * 0.6);
brush.circle(sx, sy, r, true);
if (r > 9) {
brush.set("pen", lerpColor(color(INK), color(PALE), 0.4), 0.8);
brush.circle(sx + random(-1.4, 1.4), sy + random(-1.4, 1.4), r * 0.5, true);
}
}
}
// the corner mass: overlapping pools, no edge you could cut out with scissors
function mass() {
brush.noStroke();
brush.fillTexture(0.45, 0.4);
const seeds = [
[690, 738, 84], [748, 694, 76], [716, 784, 72], [788, 750, 78],
[654, 706, 58], [736, 640, 54], [664, 778, 52], [782, 668, 50],
[702, 692, 64], [766, 792, 60],
];
for (let i = 0; i < seeds.length; i++) {
brush.fillBleed(0.34 + (i % 3) * 0.08, "out");
brush.fill(i % 3 === 0 ? WASH : INK, max(70, randomGaussian(118, 16)));
brush.circle(seeds[i][0] + random(-6, 6), seeds[i][1] + random(-6, 6), seeds[i][2] * (0.94 + random(-0.06, 0.1)), true);
}
// the mantle's shoulder, drawn not filled — hatch across the top of the dark
brush.hatchStyle("2B", INK, 0.75);
brush.hatch(11, 124, { rand: 0.14, continuous: false, gradient: 0.35 });
brush.noFill();
brush.set("2B", INK, 1.0);
brush.beginShape(0.5);
brush.vertex(628, 728);
brush.vertex(670, 666);
brush.vertex(738, 638);
brush.vertex(790, 666);
brush.endShape(false);
brush.noHatch();
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(GROUND);
// water: three slow breaths on the diagonal, thinning toward the upper left
brush.noStroke();
brush.fillTexture(0.35, 0.35);
brush.fillBleed(0.5, "out");
brush.fill(WASH, 18);
brush.circle(608, 618, 168, true);
brush.fill(WASH, 13);
brush.circle(402, 458, 148, true);
brush.fill(PALE, 11);
brush.circle(262, 306, 116, true);
arm(ARM_GHOST, 34, 12, 1.0, -1, 7.3, true);
arm(ARM_HIGH, 46, 8, 1.5, 1, 21.7, false);
arm(ARM_LONG, 52, 9, 1.7, -1, 44.1, false);
arm(ARM_CURL, 42, 7, 1.4, 1, 12.9, false);
arm(ARM_LOW, 38, 11, 1.3, -1, 33.5, false);
mass();
// one eye, holding the mass together
brush.noStroke();
brush.fillBleed(0.22, "out");
brush.fill(GROUND, 172);
brush.circle(660, 662, 13, true);
brush.fill(PALE, 96);
brush.circle(662, 660, 8, true);
brush.noFill();
brush.set("charcoal", INK, 2.6);
brush.line(650, 664, 672, 659);
brush.set("2B", INK, 1.1);
brush.beginShape(0.6);
brush.vertex(644, 653);
brush.vertex(660, 645);
brush.vertex(678, 651);
brush.endShape(false);
nearArm();
// last ink: the deepest corner, where the animal keeps its weight
brush.noStroke();
brush.fillBleed(0.2, "out");
brush.fill(INK, 208);
brush.circle(774, 784, 44, true);
brush.fill(INK, 172);
brush.circle(716, 764, 30, true);
// moving water, drawn only where the paper is otherwise empty
brush.field("seabed");
brush.set("2H", PALE, 0.85);
brush.flowLine(126, 616, 190, 350);
brush.flowLine(88, 470, 150, 14);
brush.flowLine(492, 168, 132, 204);
brush.set("2H", lerpColor(color(WASH), color(GROUND), 0.5), 0.9);
brush.flowLine(318, 700, 156, 332);
brush.noField();
// a single loosed sucker, adrift far from the animal
brush.noStroke();
brush.fill(ACCENT, 118);
brush.circle(214, 226, 4.6, true);
}