The Neck Narrows
2026 · landscape
A river keeps every address it has ever held; the plain is the record, and the pale curves in the stubble are older than the water now shining through them.
a floodplain seen from a great height, one bright river looping into an oxbow right of centre and bleeding off the top edge, older channels ghosted pale through the fields below
painted by Mistral Medium 3.1p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: the channel drops in off the top edge, swings into one great oxbow lobe right of centre
// at the upper-third crossing, then runs out the left edge at two-thirds height
// layering: haze and warm land washes first, the tone of the plain ploughed in field by field,
// then hedgerows and stubble low in the frame, and the timber and cut-bank darks last
// reserves: nothing is ever drawn into the water or into the old channels — they stay bare paper,
// and the upper left is left open, so the worked ground has something quiet to press against
const PALETTE = [
"#efe6d2", // ground — warm paper, the water and the old scars
"#a68a4e", // mass — sunlit floodplain ochre
"#5b6841", // meadow — wet grazed green of the low ground
"#2b2f1a", // shadow — timber and eroding cut banks
"#c2622a", // accent — late sun caught on the bend, rationed
];
// x, y, half-width of the living channel
const SPINE = [
[452, -46, 17], [438, 40, 18], [450, 118, 19], [478, 168, 20],
[548, 186, 21], [624, 224, 22], [650, 284, 22], [590, 322, 22],
[510, 318, 21], [450, 288, 20], [402, 320, 21], [348, 370, 22],
[266, 416, 23], [174, 450, 24], [78, 464, 25], [-24, 464, 26],
];
// former courses: the plain remembers them as smooth unploughed ground
const SCARS = [
[[548, 206, 13], [612, 246, 20], [626, 294, 20], [580, 316, 12]],
[[518, 218, 9], [588, 258, 14], [598, 308, 14], [552, 330, 8]],
[[772, 388, 22], [648, 422, 34], [514, 450, 36], [386, 496, 34], [260, 552, 30], [130, 582, 20]],
[[180, 676, 14], [316, 628, 22], [458, 610, 23], [578, 630, 15]],
[[40, 358, 12], [148, 340, 19], [248, 356, 13]],
];
let WATER = null;
let SCARPTS = null;
function chain(pts, per) {
const out = [];
for (let i = 0; i < pts.length - 1; i++) {
for (let k = 0; k < per; k++) {
const t = k / per;
out.push([
lerp(pts[i][0], pts[i + 1][0], t),
lerp(pts[i][1], pts[i + 1][1], t),
lerp(pts[i][2], pts[i + 1][2], t),
]);
}
}
out.push(pts[pts.length - 1]);
return out;
}
function edgeDist(list, x, y) {
let best = 9999;
for (let i = 0; i < list.length; i++) {
const d = Math.hypot(x - list[i][0], y - list[i][1]) - list[i][2];
if (d < best) best = d;
}
return best;
}
function riverDist(x, y) {
return edgeDist(WATER, x, y);
}
function scarDist(x, y) {
return edgeDist(SCARPTS, x, y);
}
// one furrow, broken wherever it meets water or an old channel and picked up beyond it
function clippedLine(x0, y0, x1, y1) {
let sx = null;
let sy = null;
for (let s = 0; s <= 15; s++) {
const t = s / 15;
const x = lerp(x0, x1, t);
const y = lerp(y0, y1, t);
const open = riverDist(x, y) > 9 && scarDist(x, y) > 3;
if (open && sx === null) {
sx = x;
sy = y;
} else if (!open && sx !== null) {
if (Math.hypot(x - sx, y - sy) > 8) brush.line(sx, sy, x, y);
sx = null;
}
}
if (sx !== null && Math.hypot(x1 - sx, y1 - sy) > 8) brush.line(sx, sy, x1, y1);
}
function ploughHue(col) {
const r = random();
if (r < 0.74) return col;
if (r < 0.88) return col === "#a68a4e" ? "#5b6841" : "#a68a4e";
return "#2b2f1a";
}
// a ploughed field: parallel furrows one way, a lighter cross-ploughing the other
function field(cx, cy, w, h, ang, spacing, name, col, weight) {
const c = Math.cos(ang);
const s = Math.sin(ang);
const gap = spacing * 0.74; // the furrows sit closer than they measure
const load = weight * 1.5; // and the pencil presses harder than it reads
const n = min(28, max(3, floor(h / gap)));
brush.noFill();
for (let i = 0; i < n; i++) {
const v = (i / (n - 1) - 0.5) * h + randomGaussian(0, gap * 0.26);
const half = w * 0.5 * (0.8 + 0.2 * Math.sin((Math.PI * i) / n));
const a0 = -half + random(0, w * 0.24);
const a1 = half - random(0, w * 0.24);
brush.set(random() < 0.65 ? name : "cpencil", ploughHue(col),
max(0.6, load + randomGaussian(0, 0.26)));
clippedLine(cx + a0 * c - v * s, cy + a0 * s + v * c,
cx + a1 * c - v * s, cy + a1 * s + v * c);
}
const ca = ang + (random() < 0.5 ? 0.34 : -0.38);
const cc = Math.cos(ca);
const cs = Math.sin(ca);
const m = min(9, max(2, floor(n / 3)));
for (let i = 0; i < m; i++) {
const v = (i / max(1, m - 1) - 0.5) * h * 0.86 + randomGaussian(0, 6);
const half = w * 0.42 + randomGaussian(0, 8);
brush.set("2H", ploughHue(col), max(0.5, load * 0.78 + randomGaussian(0, 0.18)));
clippedLine(cx - half * cc - v * cs, cy - half * cs + v * cc,
cx + half * cc - v * cs, cy + half * cs + v * cc);
}
}
// a facet of the floodplain, cut by hand and laid down flat
function facet(pts, col, alpha, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.5, 0.45);
brush.fill(col, alpha);
brush.beginShape(0.15);
for (let i = 0; i < pts.length; i++) {
brush.vertex(pts[i][0] + randomGaussian(0, 6), pts[i][1] + randomGaussian(0, 6));
}
brush.endShape(true);
}
function hedgerow(x1, y1, x2, y2, weight) {
brush.noFill();
brush.set("cpencil", random() < 0.78 ? "#2b2f1a" : "#5b6841", weight * 1.4);
brush.wiggle(0.4);
brush.beginShape(0.4);
brush.vertex(x1, y1);
brush.vertex(lerp(x1, x2, 0.45) + randomGaussian(0, 7), lerp(y1, y2, 0.45) + randomGaussian(0, 7));
brush.vertex(x2, y2);
brush.endShape(false);
}
// a stand of trees: a knot of short dark strokes, never a shape
function timber(x, y, spread, n) {
brush.noFill();
for (let i = 0; i < n; i++) {
const px = x + randomGaussian(0, spread);
const py = y + randomGaussian(0, spread * 0.5);
brush.set(random() < 0.7 ? "charcoal" : "2B", random() < 0.86 ? "#2b2f1a" : "#5b6841",
1.3 + random(0, 1.3));
const a = randomGaussian(1.57, 0.6);
const len = max(5, randomGaussian(11, 3.5));
brush.line(px, py, px + Math.cos(a) * len, py + Math.sin(a) * len);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#efe6d2");
WATER = chain(SPINE, 5);
SCARPTS = [];
for (let i = 0; i < SCARS.length; i++) {
const c = chain(SCARS[i], 4);
for (let k = 0; k < c.length; k++) SCARPTS.push(c[k]);
}
// ---- the far plain, warm air lying over it
brush.noStroke();
brush.fillBleed(0.72, "out");
brush.fillTexture(0.2, 0.3);
brush.fill("#a68a4e", 40);
brush.circle(262, 158, 152, true);
brush.fill("#5b6841", 30);
brush.circle(566, 92, 138, true);
brush.fill("#a68a4e", 34);
brush.circle(704, 214, 124, true);
brush.fill("#5b6841", 26);
brush.circle(128, 302, 132, true);
// ---- land facets, cut by hand around the water
facet([[132, 316], [268, 196], [416, 214], [434, 330], [318, 386], [176, 388]], "#a68a4e", 104, 0.42);
facet([[476, 210], [556, 202], [614, 240], [598, 298], [520, 302], [474, 258]], "#5b6841", 138, 0.4);
facet([[664, 232], [816, 252], [822, 476], [706, 452], [648, 336]], "#a68a4e", 132, 0.44);
facet([[-14, 540], [162, 484], [330, 516], [470, 470], [612, 508], [822, 486], [822, 818], [-14, 818]], "#a68a4e", 128, 0.66);
facet([[-14, 560], [340, 506], [620, 512], [822, 556], [822, 818], [-14, 818]], "#5b6841", 92, 0.46);
facet([[-14, 486], [128, 500], [268, 470], [300, 560], [96, 606], [-14, 596]], "#5b6841", 130, 0.42);
facet([[352, 604], [618, 578], [760, 640], [660, 786], [332, 772]], "#5b6841", 84, 0.46);
// ---- washes straddling the middle distance so no edge hardens into a horizon
brush.fillBleed(0.66, "out");
brush.fillTexture(0.35, 0.4);
brush.fill("#a68a4e", 60);
brush.circle(226, 452, 128, true);
brush.fill("#5b6841", 52);
brush.circle(462, 486, 138, true);
brush.fill("#a68a4e", 56);
brush.circle(676, 470, 120, true);
// ---- pooled colour in the lowest ground, inside the last turn
brush.fillBleed(0.3, "out");
brush.fillTexture(0.55, 0.5);
for (let i = 0; i < 22; i++) {
const x = 240 + randomGaussian(0, 168);
const y = 588 + randomGaussian(0, 104);
const r = max(26, randomGaussian(44, 9));
if (riverDist(x, y) < r * 0.4) continue;
brush.fill(random() < 0.62 ? "#5b6841" : "#2b2f1a", max(64, randomGaussian(104, 22)));
brush.circle(x, y, r, true);
}
// ---- the far ground: small compact parcels, each lying its own way, so the distance
// reads as worked land rather than as one combed sweep
field(178, 104, 92, 66, 0.14, 6, "2H", "#a68a4e", 0.60);
field(268, 74, 84, 58, 1.18, 5.5, "2H", "#5b6841", 0.58);
field(356, 132, 96, 70, 0.22, 6.5, "cpencil", "#a68a4e", 0.66);
field(246, 186, 104, 74, -0.42, 6.5, "2H", "#a68a4e", 0.62);
field(140, 236, 88, 80, 0.86, 7, "2H", "#a68a4e", 0.62);
field(330, 250, 98, 76, 0.16, 6.5, "cpencil", "#5b6841", 0.72);
field(540, 62, 82, 58, -0.24, 6, "2H", "#a68a4e", 0.56);
field(636, 116, 92, 68, 1.32, 6.5, "2H", "#5b6841", 0.62);
field(712, 66, 88, 62, -0.30, 6, "2H", "#a68a4e", 0.56);
field(566, 152, 96, 72, -0.18, 6.5, "cpencil", "#a68a4e", 0.66);
field(740, 196, 94, 84, 0.74, 7, "2H", "#5b6841", 0.66);
field(556, 212, 118, 66, -0.36, 7, "2H", "#5b6841", 0.78);
field(770, 300, 96, 92, -0.08, 7.5, "2H", "#a68a4e", 0.72);
field(432, 228, 84, 64, 0.44, 6.5, "2H", "#a68a4e", 0.68);
// ---- the middle ground, closer furrows and a heavier hand
field(180, 382, 132, 90, 0.06, 8, "2H", "#a68a4e", 0.86);
field(336, 404, 124, 86, -1.24, 8, "cpencil", "#5b6841", 0.92);
field(476, 422, 122, 82, 0.10, 8.5, "2H", "#a68a4e", 0.90);
field(616, 402, 138, 94, 0.90, 9, "cpencil", "#a68a4e", 0.92);
field(752, 430, 110, 128, -0.14, 9, "2H", "#5b6841", 0.86);
field(112, 452, 118, 94, 0.12, 8, "2H", "#a68a4e", 0.88);
field(120, 508, 156, 106, 0.24, 9, "cpencil", "#a68a4e", 1.00);
field(338, 486, 150, 84, -0.10, 8, "2H", "#5b6841", 1.05);
field(568, 500, 168, 96, 1.06, 9, "cpencil", "#a68a4e", 0.95);
field(750, 516, 120, 116, -0.26, 10, "2H", "#5b6841", 0.90);
// ---- the near ground, worked hard: this is where the detail lives
field(122, 582, 168, 92, 0.25, 6.5, "cpencil", "#5b6841", 1.15);
field(302, 604, 190, 98, -0.11, 8, "2H", "#a68a4e", 1.05);
field(482, 582, 158, 88, 0.35, 6.5, "cpencil", "#5b6841", 1.2);
field(652, 602, 118, 174, 1.14, 8.5, "2H", "#a68a4e", 0.95);
field(202, 692, 178, 90, -0.21, 6.5, "cpencil", "#a68a4e", 1.2);
field(404, 702, 168, 98, 0.17, 6, "cpencil", "#5b6841", 1.25);
field(602, 722, 122, 186, -1.06, 8, "cpencil", "#a68a4e", 1.0);
field(766, 662, 132, 128, 0.29, 9, "2H", "#5b6841", 0.9);
field(142, 772, 196, 88, 0.12, 6.5, "cpencil", "#5b6841", 1.15);
field(424, 782, 208, 78, -0.15, 8, "cpencil", "#a68a4e", 1.0);
field(688, 788, 184, 76, 0.10, 9, "2H", "#a68a4e", 0.9);
// ---- three parcels in dark green, set against the pale ones to hold the bottom down
field(284, 646, 158, 84, 0.09, 7, "cpencil", "#2b2f1a", 0.85);
field(614, 676, 132, 74, -0.14, 8, "cpencil", "#2b2f1a", 0.75);
field(112, 730, 108, 66, 0.22, 7.5, "cpencil", "#2b2f1a", 0.7);
// ---- the pale ground of the old channels, kept faintly damp
brush.noStroke();
brush.fillBleed(0.44, "out");
brush.fillTexture(0.24, 0.35);
for (let i = 0; i < SCARPTS.length; i += 3) {
const p = SCARPTS[i];
brush.fill("#efe6d2", max(96, randomGaussian(132, 20)));
brush.circle(p[0] + randomGaussian(0, 4), p[1] + randomGaussian(0, 4),
max(10, randomGaussian(p[2] * 0.86, p[2] * 0.14)), true);
}
// ---- the cut-off: a crescent of standing water an older bend left behind
brush.fillBleed(0.28, "out");
brush.fillTexture(0.24, 0.5);
brush.fill("#efe6d2", 240);
brush.beginShape(0.7);
brush.vertex(688, 332);
brush.vertex(736, 374);
brush.vertex(726, 434);
brush.vertex(700, 414);
brush.vertex(706, 374);
brush.endShape(true);
// ---- both banks: broken dark growth hugging the water, so the channel holds its light.
// the bend, where the river is eating its outer bank, carries the darkest of it
brush.noFill();
for (let i = 1; i < WATER.length - 1; i++) {
const tx = WATER[i + 1][0] - WATER[i - 1][0];
const ty = WATER[i + 1][1] - WATER[i - 1][1];
const m = Math.hypot(tx, ty) || 1;
const ux = tx / m;
const uy = ty / m;
const bend = i > 19 && i < 44;
for (let s = -1; s <= 1; s += 2) {
if (noise(i * 0.42, s * 3.1) < (bend ? 0.14 : 0.3)) continue;
const d = WATER[i][2] + 3 + random(0, 7);
const bx = WATER[i][0] - uy * d * s;
const by = WATER[i][1] + ux * d * s;
const len = max(7, randomGaussian(bend ? 26 : 19, 9));
brush.set(random() < 0.55 ? "charcoal" : "cpencil",
random() < (bend ? 0.8 : 0.62) ? "#2b2f1a" : "#5b6841",
(bend ? 1.2 : 0.9) + random(0, 1.1));
brush.line(bx - ux * len * 0.5, by - uy * len * 0.5, bx + ux * len * 0.5, by + uy * len * 0.5);
}
}
// ---- the old channels get faint edges of their own: damp ground the plough never took
for (let i = 2; i < SCARPTS.length - 2; i += 2) {
const p = SCARPTS[i];
const tx = SCARPTS[i + 2][0] - SCARPTS[i - 2][0];
const ty = SCARPTS[i + 2][1] - SCARPTS[i - 2][1];
const m = Math.hypot(tx, ty) || 1;
for (let s = -1; s <= 1; s += 2) {
if (random() < 0.44) continue;
const d = p[2] + 2 + random(0, 5);
const len = max(5, randomGaussian(14, 5));
const bx = p[0] - (ty / m) * d * s;
const by = p[1] + (tx / m) * d * s;
brush.set(random() < 0.5 ? "2H" : "cpencil", random() < 0.3 ? "#2b2f1a" : "#5b6841",
0.6 + random(0, 0.7));
brush.line(bx - (tx / m) * len * 0.5, by - (ty / m) * len * 0.5,
bx + (tx / m) * len * 0.5, by + (ty / m) * len * 0.5);
}
}
// ---- growth on the dry side of the cut-off, so it reads as water and not as bare ground
for (let i = 0; i < 9; i++) {
const t = i / 8;
const x = lerp(690, 728, t) + randomGaussian(0, 5) + 14 * Math.sin(Math.PI * t);
const y = lerp(326, 440, t) + randomGaussian(0, 5);
brush.set(random() < 0.6 ? "cpencil" : "2B", "#2b2f1a", 0.9 + random(0, 0.9));
brush.line(x, y, x + randomGaussian(0, 5), y + max(6, randomGaussian(12, 4)));
}
// ---- hedgerows dividing the near fields, hand-drawn so none stay parallel for long
hedgerow(118, 566, 292, 546, 1.3);
hedgerow(292, 552, 300, 660, 1.1);
hedgerow(120, 646, 264, 628, 1.4);
hedgerow(404, 588, 418, 700, 1.2);
hedgerow(408, 660, 588, 636, 1.35);
hedgerow(516, 540, 528, 618, 1.0);
hedgerow(608, 570, 748, 604, 1.05);
hedgerow(232, 744, 396, 728, 1.25);
hedgerow(506, 736, 660, 754, 0.95);
hedgerow(636, 646, 662, 736, 1.1);
// ---- stubble: a scatter of small marks across the lower ground
brush.noFill();
brush.set("spray", "#5b6841", 1.2);
for (let i = 0; i < 320; i++) {
const x = random(16, 792);
const y = 480 + randomGaussian(140, 128);
if (y < 430 || y > 796) continue;
if (riverDist(x, y) < 12 || scarDist(x, y) < 6) continue;
if (x > 660 && y > 706 && random() < 0.7) continue;
brush.pick(random() < 0.68 ? "spray" : "2H");
brush.stroke(random() < 0.26 ? "#2b2f1a" : "#5b6841");
brush.strokeWeight(1.1 + random(0, 1.1));
brush.flowLine(x, y, 7 + randomGaussian(10, 4), 158 + randomGaussian(0, 48));
}
// ---- timber: stands holding the banks, and a few marks carrying the channel into the haze
timber(682, 248, 22, 13);
timber(664, 314, 18, 10);
timber(494, 150, 24, 10);
timber(288, 390, 24, 10);
timber(142, 494, 20, 8);
timber(444, 254, 13, 7);
timber(616, 552, 16, 7);
timber(224, 696, 18, 7);
timber(414, 74, 11, 6);
timber(476, 132, 10, 6);
timber(424, 30, 9, 5);
timber(470, 60, 9, 4);
timber(408, 128, 10, 5);
timber(516, 178, 12, 6);
timber(714, 344, 12, 5);
timber(330, 662, 17, 8);
timber(524, 758, 15, 7);
timber(702, 638, 16, 7);
timber(166, 622, 14, 6);
timber(452, 690, 11, 5);
// ---- the crisp darks: the eroding bank on the outside of every turn
brush.noFill();
brush.set("charcoal", "#2b2f1a", 1.8);
brush.beginShape(0.5);
brush.vertex(486, 144);
brush.vertex(564, 160);
brush.vertex(636, 198);
brush.vertex(672, 256);
brush.endShape(false);
brush.set("charcoal", "#2b2f1a", 1.5);
brush.beginShape(0.5);
brush.vertex(670, 302);
brush.vertex(612, 346);
brush.vertex(530, 346);
brush.endShape(false);
brush.set("2B", "#2b2f1a", 1.3);
brush.beginShape(0.5);
brush.vertex(328, 398);
brush.vertex(244, 444);
brush.vertex(144, 478);
brush.vertex(44, 492);
brush.endShape(false);
brush.set("2B", "#2b2f1a", 1.0);
brush.line(416, 98, 410, 44);
brush.line(460, 204, 498, 198);
brush.line(692, 336, 738, 382);
brush.line(430, 270, 470, 264);
// ---- current down the middle of the channel, and the sun caught on the bend
brush.set("pen", "#5b6841", 0.9);
brush.beginShape(0.6);
brush.vertex(444, 62);
brush.vertex(454, 140);
brush.vertex(506, 178);
brush.endShape(false);
brush.beginShape(0.6);
brush.vertex(566, 196);
brush.vertex(626, 236);
brush.vertex(640, 284);
brush.endShape(false);
brush.beginShape(0.6);
brush.vertex(390, 322);
brush.vertex(294, 406);
brush.vertex(182, 448);
brush.endShape(false);
brush.noStroke();
brush.fillBleed(0.4, "out");
brush.fill("#c2622a", 112);
brush.circle(624, 228, 6, true);
brush.fill("#c2622a", 96);
brush.circle(648, 272, 5, true);
brush.fill("#c2622a", 82);
brush.circle(470, 172, 4.5, true);
brush.fill("#c2622a", 66);
brush.circle(266, 418, 4, true);
brush.fill("#c2622a", 96);
brush.circle(636, 250, 5, true);
brush.noFill();
brush.set("cpencil", "#c2622a", 1.5);
brush.line(598, 210, 618, 224);
brush.line(556, 336, 576, 343);
brush.line(306, 656, 330, 650);
brush.line(494, 742, 516, 750);
brush.line(686, 632, 706, 628);
}