Fog Takes the Shore
2026 · weather
Fog is less a thing to paint than a decision about how much to withhold. Three greys, one warm note, and the shore gives up its rocks one at a time from the right.
a fog bank swallowing a shoreline in three greys, the last rocks surviving low-left and half-erased where the fog edge crosses them
painted by Mistral Medium 3.1p5 2.3.2 · brush 2.2.2 · 2026-08-25
// composition: horizon band — fog owns the upper two-thirds and rolls in from the right;
// the surviving rocks sit low-left on the thirds line and dissolve rightwards until nothing is left.
// layering: pale fog washes first, then rocks built from stacked translucent passes, a fog scrim over
// their right flanks, and the wet darks under the nearest rock last.
const PALETTE = [
"#d9d9d3", // ground — paper, and the light inside the fog
"#a9aeaa", // mass — the body of the fog, the middle value
"#575e60", // shadow — wet rock, the dark value
"#2b3134", // ink — the last darks, rationed
"#9c8a6f", // accent — one warm note where the sand is wet
];
const PAPER = PALETTE[0];
const FOG = PALETTE[1];
const ROCK = PALETTE[2];
const INK = PALETTE[3];
const SAND = PALETTE[4];
// ---- fog ------------------------------------------------------------------
// fog keeps only enough border to hold pigment — the blobs overlap until no edge survives
function wash(x, y, r, op, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.36, 0.2);
brush.fill(FOG, op);
brush.circle(x, y, r, true);
}
function veil(x, y, r, op) {
brush.noStroke();
brush.fillBleed(0.5, "out");
brush.fillTexture(0.26, 0.28);
brush.fill(PAPER, op);
brush.circle(x, y, r, true);
}
// a broken drag stroke — the horizon and the wet sand are never one ruled line
function drag(x0, x1, y, col, w, gap) {
brush.set("cpencil", col, w);
let x = x0;
while (x < x1) {
const seg = Math.max(18, randomGaussian(62, 20));
const end = Math.min(x1, x + seg);
brush.line(x, y + randomGaussian(0, 1.8), end, y + randomGaussian(0, 1.8));
x = end + Math.max(6, randomGaussian(gap, gap * 0.5));
}
}
// ---- rock -----------------------------------------------------------------
// a boulder, not a cone: the sine profile is flattened into a plateau and skewed to one shoulder
function rockPoints(cx, baseY, w, h, skew, k) {
const pts = [];
const n = 9;
for (let i = 0; i <= n; i++) {
const t = i / n;
const x = cx - w / 2 + w * t;
const prof = Math.pow(Math.sin(Math.PI * Math.pow(t, skew)), 0.42);
const bump = noise(x * 0.016, k * 4.7) - 0.5;
const y = baseY - h * (prof * (0.88 + bump * 0.44));
pts.push([x, y + randomGaussian(0, h * 0.055)]);
}
pts.push([cx + w / 2 + randomGaussian(0, 3.5), baseY + randomGaussian(2, 2.4)]);
pts.push([cx - w / 2 + randomGaussian(0, 3.5), baseY + randomGaussian(2, 2.4)]);
return pts;
}
function pour(pts, cx, baseY, s, dx, dy, curv) {
brush.beginShape(curv);
for (let i = 0; i < pts.length; i++) {
const x = cx + (pts[i][0] - cx) * s + dx + randomGaussian(0, 2.4);
const y = baseY + (pts[i][1] - baseY) * s + dy + randomGaussian(0, 2.4);
brush.vertex(x, y, 1);
}
brush.endShape(true);
}
// a near rock: five stacked translucent passes, weight collecting at the wet base
function rock(cx, baseY, w, h, op, skew, k) {
const pts = rockPoints(cx, baseY, w, h, skew, k);
brush.noStroke();
brush.fillBleed(0.42, "out");
brush.fillTexture(0.3, 0.12);
brush.fill(FOG, op * 0.5 + random(-4, 6));
pour(pts, cx, baseY, 1.08, random(-5, 5), random(-6, 2), 0.44);
brush.fillBleed(0.16, "out");
brush.fillTexture(0.62, 0.46);
brush.fill(ROCK, op * 0.62 + random(-6, 8));
pour(pts, cx, baseY, 0.99, random(-3, 3), random(-2, 2), 0.36);
brush.fill(ROCK, op * 0.72 + random(-7, 7));
pour(pts, cx, baseY, 0.9, random(-4, 2), random(0, 4), 0.32);
brush.fill(ROCK, op + random(-8, 8));
pour(pts, cx, baseY, 0.62, -w * 0.13 + random(-3, 3), random(0, 4), 0.3);
// the shadow side: ink pooled in the lee of the rock, never one flat wedge
brush.fillBleed(0.24, "out");
brush.fillTexture(0.6, 0.3);
for (let i = 0; i < 9; i++) {
const t = random(0.06, 0.5);
const j = floor(t * 9);
const topY = pts[j][1];
const x = cx - w / 2 + w * t + randomGaussian(0, 5);
const y = lerp(topY, baseY, random(0.3, 0.94));
const r = Math.max(6, (baseY - topY) * random(0.15, 0.3));
brush.fill(INK, Math.max(50, randomGaussian(op * 0.78, 20)));
brush.circle(x, y, r, true);
}
// pooling along the waterline where the rock sits in the wet
brush.fillBleed(0.14, "out");
for (let i = 0; i < 4; i++) {
const t = random(0.12, 0.78);
brush.fill(INK, Math.max(40, randomGaussian(op * 0.7, 18)));
brush.circle(cx - w / 2 + w * t + randomGaussian(0, 4), baseY - randomGaussian(3, 3), Math.max(6, randomGaussian(h * 0.13, h * 0.03)), true);
}
}
// a far rock: two soft passes, barely more than a stain
function ghost(cx, baseY, w, h, op, skew, k) {
const pts = rockPoints(cx, baseY, w, h, skew, k);
brush.noStroke();
brush.fillBleed(0.48, "out");
brush.fillTexture(0.28, 0.12);
brush.fill(FOG, op * 0.8 + random(-5, 5));
pour(pts, cx, baseY, 1.05, random(-4, 4), random(-4, 2), 0.5);
brush.fillBleed(0.3, "out");
brush.fillTexture(0.4, 0.2);
brush.fill(ROCK, op + random(-6, 6));
pour(pts, cx, baseY, 0.78, random(-4, 2), random(0, 3), 0.4);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PAPER);
// --- 1. the bank: a diagonal mass leaning out of the top-right corner down onto
// the water. Core first, then a wide fringe so the mass never owns an outline.
for (let i = 0; i < 40; i++) {
const t = i / 39 + random(-0.012, 0.012);
const d = randomGaussian(0, 66);
const x = constrain(lerp(880, 300, t) - 0.655 * d, -70, 880);
const floor = 336 + constrain((x - 330) * 0.42, 0, 186);
const y = constrain(lerp(-24, 496, t) - 0.755 * d, -40, floor);
wash(x, y, Math.max(58, randomGaussian(92, 13)), Math.max(46, randomGaussian(70, 9)), 0.4 + random(0, 0.18));
}
for (let i = 0; i < 28; i++) {
const t = i / 27 + random(-0.02, 0.02);
const d = randomGaussian(0, 172);
const x = constrain(lerp(898, 268, t) - 0.655 * d, -90, 900);
const floor = 344 + constrain((x - 330) * 0.42, 0, 190);
const y = constrain(lerp(-40, 512, t) - 0.755 * d, -60, floor);
wash(x, y, Math.max(60, randomGaussian(106, 18)), Math.max(16, randomGaussian(26, 6)), 0.62 + random(0, 0.2));
}
// the leading edge coming down over the water, where it takes the far rocks
for (let i = 0; i < 10; i++) {
const x = constrain(randomGaussian(650, 130), 400, 870);
wash(x, constrain(randomGaussian(492, 30), 430, 542), Math.max(40, randomGaussian(64, 12)), Math.max(36, randomGaussian(62, 10)), 0.4);
}
// the fog's grain: dense sideways drift, granulation rather than shape
brush.field("waves");
for (let i = 0; i < 96; i++) {
const t = i / 95;
const d = randomGaussian(0, 128);
const x = constrain(lerp(878, 292, t) - 0.655 * d, -80, 880);
const floor = 340 + constrain((x - 330) * 0.42, 0, 188);
const y = constrain(lerp(-20, 500, t) - 0.755 * d, 4, floor);
brush.set("spray", FOG, Math.max(3, randomGaussian(13 - abs(d) * 0.03, 2.6)));
brush.stroke(FOG);
brush.flowLine(x, y, Math.max(60, randomGaussian(210, 66)), 177 + randomGaussian(0, 5));
}
for (let i = 0; i < 46; i++) {
const t = i / 45;
const d = randomGaussian(0, 58);
const x = constrain(lerp(870, 320, t) - 0.655 * d, -60, 870);
const floor = 330 + constrain((x - 330) * 0.42, 0, 186);
const y = constrain(lerp(0, 486, t) - 0.755 * d, 8, floor);
brush.set("spray", FOG, Math.max(4, randomGaussian(7, 2)));
brush.stroke(FOG);
brush.flowLine(x, y, Math.max(50, randomGaussian(140, 44)), 178 + randomGaussian(0, 4));
}
brush.noField();
// --- 2. the waterline: soft under the rocks, broken, gone before the right edge
wash(120, 486, 104, 46, 0.4);
wash(268, 490, 88, 34, 0.44);
wash(392, 486, 70, 22, 0.5);
brush.set("HB", ROCK, 1.5);
brush.line(-18, 476, 96, 477);
brush.line(84, 478, 214, 476);
brush.set("HB", FOG, 1.2);
brush.line(200, 477, 306, 479);
drag(288, 442, 478, FOG, 1.0, 24);
drag(420, 512, 477, FOG, 0.8, 38);
// the strip of light on the water, left of the bank — wiped back out of the grey
veil(120, 452, 118, 60);
veil(268, 448, 96, 44);
veil(30, 462, 104, 52);
veil(392, 444, 74, 26);
// --- 3. the rocks, low-left, losing their weight as they recede right
rock(164, 606, 262, 138, 194, 1.34, 0.31);
rock(314, 574, 174, 100, 178, 0.72, 1.13);
rock(218, 664, 102, 42, 176, 1.15, 2.07);
rock(402, 552, 118, 64, 128, 0.86, 3.41);
ghost(476, 538, 86, 44, 84, 1.22, 4.29);
ghost(546, 528, 62, 30, 56, 0.8, 5.17);
ghost(608, 519, 44, 20, 36, 1.1, 6.03);
ghost(662, 513, 32, 13, 22, 0.9, 7.11);
// --- 4. the scrim: fog laid back over the right-hand rocks, eating their edges
veil(700, 508, 76, 104);
veil(640, 512, 64, 86);
veil(588, 521, 54, 66);
veil(536, 529, 46, 48);
veil(494, 537, 40, 32);
veil(452, 545, 34, 22);
for (let i = 0; i < 14; i++) {
const x = randomGaussian(578, 108);
const y = randomGaussian(524, 26);
veil(x, y, Math.max(20, randomGaussian(34, 9)), Math.max(14, randomGaussian(20 + (x - 420) * 0.14, 8)));
}
brush.field("waves");
for (let i = 0; i < 14; i++) {
brush.set("spray", PAPER, 3.4 + random(0, 1.8));
brush.stroke(PAPER);
brush.flowLine(randomGaussian(596, 130), randomGaussian(522, 30), Math.max(50, randomGaussian(120, 40)), 177 + randomGaussian(0, 4));
}
brush.noField();
// --- 5. wet sand, dragged flat across the lower left; the rest stays paper
wash(118, 692, 122, 26, 0.5);
wash(272, 730, 104, 22, 0.54);
wash(34, 748, 96, 18, 0.56);
drag(-16, 214, 702, FOG, 1.5, 34);
drag(56, 288, 738, FOG, 1.1, 44);
drag(244, 402, 714, FOG, 0.8, 52);
// the one warm note: sand showing through where the water has pulled back
brush.noStroke();
brush.fillBleed(0.4, "out");
brush.fillTexture(0.44, 0.18);
brush.fill(SAND, 58);
brush.circle(150, 722, 62, true);
brush.fill(SAND, 40);
brush.circle(258, 754, 44, true);
brush.fill(SAND, 30);
brush.circle(72, 758, 38, true);
brush.set("cpencil", SAND, 1.2);
brush.line(108, 690, 206, 695);
brush.line(196, 744, 278, 738);
// --- 6. the darks, last, and only where the picture needs holding
brush.noStroke();
brush.fillBleed(0.14, "out");
brush.fillTexture(0.7, 0.6);
brush.fill(INK, 176);
brush.circle(58, 608, 26, true);
brush.fill(INK, 158);
brush.circle(104, 616, 20, true);
brush.fill(INK, 138);
brush.circle(152, 620, 15, true);
brush.fill(INK, 118);
brush.circle(226, 594, 12, true);
brush.fill(INK, 92);
brush.circle(302, 582, 9, true);
brush.fill(INK, 146);
brush.circle(198, 676, 12, true);
brush.fill(INK, 108);
brush.circle(240, 672, 8, true);
// the wet shelf under the rocks: short broken dashes on the water, never one bar
brush.set("2B", INK, 1.1);
for (let i = 0; i < 11; i++) {
const x = randomGaussian(120, 82);
const len = Math.max(9, randomGaussian(26, 10));
brush.line(x, 623 + randomGaussian(0, 2.6), x + len, 624 + randomGaussian(0, 2.6));
}
brush.set("2B", INK, 0.85);
for (let i = 0; i < 7; i++) {
const x = randomGaussian(288, 46);
const len = Math.max(7, randomGaussian(17, 6));
brush.line(x, 590 + randomGaussian(0, 2.2), x + len, 591 + randomGaussian(0, 2.2));
}
brush.line(208, 689, 232, 690);
brush.line(240, 686, 256, 688);
brush.set("2H", ROCK, 0.8);
brush.line(322, 580, 356, 582);
brush.line(374, 574, 398, 575);
// a last shred of fog crossing the nearest crest, so nothing is fully solid
veil(178, 528, 42, 42);
veil(258, 520, 34, 52);
veil(336, 536, 30, 60);
}