Low Tide Lens
2026 · landscape
Twice a day the sea forgets this much of itself, and the sand holds it up to the sky like a lens.
a tide pool seen from directly above, wet rings deepening to a teal heart in dry sand
painted by Claude Fable 5p5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: the pool sits off-center right at roughly (520, 442), six wet rings deepening to a teal heart;
// the rest of the sheet stays sparse dry sand. layering: sand grain first, wet halo, rings outermost-in with
// pooling bleeds, two ink lips on the inner walls, pebbles last.
const PALETTE = [
"#e8dcc3", // dry sand paper
"#c9b48d", // sand grain
"#8a6b48", // wet sepia rim
"#bcd8cd", // sea-glass shallow
"#6fae9f", // mid teal
"#2e6b63", // deep pool heart — the accent
"#7a6a55", // pebble grey-brown
];
// edge-deformed ring radius, stable per ring via seedOff so lips can retrace walls
function ringR(baseR, ang, seedOff, wobble) {
const n = noise(seedOff * 4.7 + cos(ang) * 1.3 + 3.0, seedOff * 3.9 + sin(ang) * 1.3 + 7.0);
return baseR * (1 + (n - 0.5) * wobble);
}
function ring(cx, cy, baseR, seedOff, wobble, col, opacity, bleed) {
brush.noStroke();
brush.fillBleed(bleed, "out");
brush.fillTexture(0.45, 0.55);
brush.fill(col, opacity);
brush.beginShape(0.6);
for (let i = 0; i < 34; i++) {
const ang = (i / 34) * TWO_PI;
const r = ringR(baseR, ang, seedOff, wobble);
brush.vertex(cx + cos(ang) * r, cy + sin(ang) * r * 0.94);
}
brush.endShape(true);
}
function ringLip(cx, cy, baseR, seedOff, wobble, a0, a1, col, weight) {
brush.noFill();
brush.set("rotring", col, weight);
brush.beginShape(0.4);
for (let i = 0; i <= 16; i++) {
const ang = lerp(a0, a1, i / 16);
const r = ringR(baseR, ang, seedOff, wobble);
brush.vertex(cx + cos(ang) * r + random(-1.5, 1.5), cy + sin(ang) * r * 0.94 + random(-1.5, 1.5));
}
brush.endShape(false);
}
function pebble(x, y, r, col, opacity) {
brush.noStroke();
brush.fillBleed(0.3, "out");
brush.fillTexture(0.6, 0.6);
brush.fill(col, opacity);
brush.circle(x, y, r, true);
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PALETTE[0]);
// sparse dry-sand grain everywhere the water isn't
for (let i = 0; i < 130; i++) {
const px = random(30, 770);
const py = random(30, 770);
if (dist(px, py, 524, 445) < 252) continue;
brush.set("spray", PALETTE[1], random(0.7, 1.5));
brush.line(px, py, px + random(2, 7), py + random(-2, 3));
}
// a few sepia drift dashes upper-left, the tide's old handwriting
brush.set("pen", PALETTE[2], 0.5);
brush.line(96, 148, 152, 141);
brush.line(170, 196, 231, 187);
brush.line(74, 258, 128, 254);
brush.line(212, 96, 262, 92);
// the pool: rings outermost-in, centers drifting, each wetter than the last
ring(528, 448, 235, 0.2, 0.22, PALETTE[2], 42, 0.75);
ring(524, 446, 192, 0.5, 0.24, PALETTE[2], 92, 0.6);
ring(520, 444, 160, 0.8, 0.26, PALETTE[3], 112, 0.55);
ring(516, 440, 126, 1.1, 0.28, PALETTE[4], 122, 0.5);
ring(512, 438, 92, 1.4, 0.3, PALETTE[5], 108, 0.55);
ring(508, 434, 54, 1.7, 0.34, PALETTE[5], 168, 0.62);
// two ink lips retracing the inner walls, partial so the water stays soft
ringLip(520, 444, 160, 0.8, 0.26, -2.6, -0.7, PALETTE[5], 0.55);
ringLip(516, 440, 126, 1.1, 0.28, 0.4, 2.1, PALETTE[5], 0.6);
// pebbles resting in the shallows
pebble(452, 392, 14, PALETTE[6], 148);
pebble(447, 396, 9, PALETTE[2], 92);
pebble(590, 476, 11, PALETTE[6], 158);
pebble(586, 480, 7, PALETTE[2], 88);
pebble(556, 356, 8, PALETTE[2], 132);
pebble(478, 512, 10, PALETTE[6], 126);
pebble(536, 528, 6, PALETTE[1], 142);
pebble(608, 408, 5, PALETTE[6], 118);
// the 1% disorder: one pebble the tide left stranded, far outside the pool
pebble(196, 588, 9, PALETTE[6], 138);
}