Mid-Span
2026 · architecture
The cables know exactly where the far shore is; the fog sees no reason to confirm it.
a suspension bridge dissolving into morning fog, one beacon still burning
painted by GPT-6 Astrap5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: the bridge enters from the left edge — backstay, tower, then the catenary reaching
// right and dissolving into fog before it can arrive; water is exactly three dashes, low left.
// layering: far cable and hangers first (already half fog), then the near structure, every stroke's
// ink mixed toward mist as it travels; pale washes drawn last to veil the dissolve.
const PALETTE = [
"#e8ecee", // ground: pale blue-grey mist
"#2b2f36", // ink
"#9fb0ba", // half-fog ink
"#dde5e9", // veil wash
"#c8402f", // accent: signal beacon
];
const INK = "#2b2f36";
const MIST = "#e8ecee";
function mixHex(a, b, t) {
let out = "#";
for (let i = 1; i < 6; i += 2) {
const v = round(lerp(parseInt(a.slice(i, i + 2), 16), parseInt(b.slice(i, i + 2), 16), constrain(t, 0, 1)));
out += ("0" + v.toString(16)).slice(-2);
}
return out;
}
function cabY(x) {
return 430 - 0.002566 * (x - 540) * (x - 540);
}
function fadeT(x) {
return pow(constrain(map(x, 205, 660, 0, 1), 0, 1), 1.35);
}
function mistBlob(cx, cy, rx, ry, op) {
brush.noStroke();
brush.fillBleed(0.6, "out");
brush.fillTexture(0.45, 0.4);
brush.fill("#dde5e9", op);
brush.beginShape(0.85);
for (let i = 0; i < 9; i++) {
const a = i * 40;
const w = 1 + randomGaussian(0, 0.13);
brush.vertex(cx + cos(a) * rx * w, cy + sin(a) * ry * w);
}
brush.endShape(true);
}
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.3);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(MIST);
// far cable and hangers — the other side of the roadway, deeper in fog
for (let x = 221; x < 640; x += 26) {
const t = min(1, fadeT(x) * 0.75 + 0.38);
brush.set("rotring", mixHex(INK, MIST, t), 0.7);
brush.line(x, cabY(x - 16) + 12, x + 26, cabY(x + 10) + 12);
}
for (let x = 245; x < 600; x += 34) {
const t = min(1, fadeT(x) * 0.8 + 0.42);
brush.set("rotring", mixHex(INK, MIST, t), 0.55);
brush.line(x, cabY(x - 16) + 13, x, 516);
}
brush.set("rotring", mixHex(INK, MIST, 0.5), 1.2);
brush.line(224, 152, 224, 548);
// backstays entering from the left edge
brush.set("rotring", INK, 1.3);
brush.line(-4, 334, 202, 144);
brush.set("rotring", mixHex(INK, MIST, 0.45), 0.8);
brush.line(-4, 352, 218, 165);
// main cable, losing conviction as it goes
for (let x = 205; x < 660; x += 22) {
const t = fadeT(x);
if (t > 0.96) break;
brush.set("rotring", mixHex(INK, MIST, t), max(0.4, 1.4 * (1 - t * 0.55)));
brush.line(x, cabY(x), x + 22, cabY(x + 22));
}
// hangers, thinning into the fog
for (let x = 233; x < 648; x += 22) {
const t = min(1, fadeT(x) * 1.08);
if (t > 0.94) continue;
brush.set("rotring", mixHex(INK, MIST, t), max(0.35, 0.85 * (1 - t * 0.5)));
brush.line(x + random(-1.5, 1.5), cabY(x) + 2, x + random(-1.5, 1.5), 521);
}
// the deck, twice, also dissolving
for (let x = -4; x < 652; x += 30) {
const t = pow(constrain(map(x, 0, 655, 0, 1), 0, 1), 1.7);
brush.set("rotring", mixHex(INK, MIST, t), max(0.4, 1.5 * (1 - t * 0.5)));
brush.line(x, 521 + random(-1, 1), x + 30, 521);
brush.set("rotring", mixHex(INK, MIST, min(1, t + 0.2)), max(0.35, 1.0 * (1 - t * 0.5)));
brush.line(x, 530, x + 30, 530 + random(-1, 1));
}
// The load-bearing faces: deck web and tower steel emerge before their outlines.
brush.noFill();
brush.noStroke();
brush.hatchStyle("cpencil", mixHex(INK, MIST, 0.2), 1.45);
brush.hatch(2.5, 88, { rand: 0.16, continuous: false, gradient: 0.15 });
brush.polygon([[201, 138], [207, 138], [207, 560], [201, 560]]);
brush.noHatch();
brush.hatchStyle("cpencil", mixHex(INK, MIST, 0.43), 1.15);
brush.hatch(3, 0, { rand: 0.24, continuous: false, gradient: 0.6 });
brush.polygon([[0, 522], [420, 522], [536, 527], [420, 533], [0, 537]]);
brush.noHatch();
// Cross-bracing describes construction; its contrast diminishes with the span.
for (let x = 14; x < 474; x += 28) {
brush.set("pen", mixHex(INK, MIST, 0.25 + fadeT(x) * 0.6), 0.65);
brush.line(x, 523, x + 14, 534);
brush.line(x + 14, 534, x + 28, 523);
}
for (let y = 170; y < 492; y += 47) {
brush.set("pen", mixHex(INK, MIST, 0.14), 0.7);
brush.line(207, y, 213, y + 30);
brush.line(213, y + 30, 207, y + 47);
}
brush.set("charcoal", mixHex(INK, MIST, 0.34), 2.2);
brush.line(194, 561, 220, 561);
// the near tower, the one thing the fog concedes
brush.set("rotring", INK, 1.8);
brush.line(201, 138, 201, 560);
brush.line(213, 138, 213, 558);
brush.set("rotring", INK, 1.2);
brush.line(196, 316, 219, 316);
brush.line(196, 452, 219, 452);
brush.line(198, 136, 217, 136);
// A broken vertical reflection locates the tower in the water.
for (let i = 0; i < 16; i++) {
const y = 568 + i * 5;
const reach = 7 + i * 0.8;
const x = 207 + randomGaussian(0, 3);
brush.set("cpencil", mixHex(INK, MIST, 0.5 + i * 0.026), 1.1);
brush.line(x - reach, y, x + reach * 0.7, y + randomGaussian(0, 0.6));
}
// quiet horizontal water marks
brush.set("2B", mixHex(INK, MIST, 0.45), 1.1);
brush.line(74, 596, 128, 594);
brush.line(160, 611, 204, 610);
brush.line(96, 623, 132, 622);
// fog, drawn as fog
mistBlob(560, 320, 170, 150, 130);
mistBlob(660, 440, 190, 160, 140);
mistBlob(620, 180, 150, 110, 110);
mistBlob(720, 570, 140, 120, 120);
mistBlob(480, 470, 130, 90, 90);
mistBlob(180, 690, 160, 80, 60);
// the beacon, last, so the fog cannot have it
brush.noStroke();
brush.fillBleed(0.5, "out");
brush.fillTexture(0.5, 0.5);
brush.fill("#c8402f", 55);
brush.circle(207, 127, 9, true);
brush.fill("#c8402f", 215);
brush.circle(207, 127, 3.5, true);
}