Kolkata
2026 · city
Howrah is a cantilever, so I drew no cable and no curve: the top chord climbs each tower and runs dead level across the suspended span. Steel is line over bare paper, not a mass. The market and the ferry only prove there is a river under it.
howrah bridge from the hooghly downstream — the balanced cantilever's straight sloping top chord and level suspended span, two riveted steel towers, mullick ghat's flower market under the near anchor arm
painted by Claude Opus 5p5 2.3.2 · brush 2.2.2 · 2026-08-30
// composition: a letterpress vignette of HOWRAH BRIDGE seen from the Hooghly a little downstream of
// the Kolkata bank, at a shallow angle so all 705 m read end to end — the near Kolkata tower stands
// right of the print's centre and large, the Howrah tower small and far to the left. The print runs
// very wide and low across the lower middle of the sheet (~x 106–700, y 440–648), ~74% of the width;
// everything above it is bare paper. No sky, no ground band.
// four non-negotiable facts: (1) STRUCTURE — this is a BALANCED CANTILEVER steel truss, not a
// suspension bridge: there is no cable, no catenary, no hanger anywhere on it. The top chord is a
// shallow TRAPEZOID of straight lines — up the anchor arm to the tower head, straight down the
// sloping cantilever arm, dead level across the 172 m suspended span, and back up. (2) PROPORTION —
// 705 m long, 457 m main span, towers only 82 m: the thing is long and low, and the near tower reads
// bigger only because it is nearer. (3) MATERIAL — riveted steel drawn as open LINE over bare paper:
// rotring chords and tower legs, pen diagonals, the paper showing between every member; the towers
// alone carry a pale ink halfPlate behind them. (4) SITE — not one pier stands in the river; both
// towers sit on caissons on the banks, and Mullick Ghat's flower market crowds the Kolkata foot.
// counts drawn: 40 truss panels — 7 anchor + 8 cantilever + 10 suspended + 8 cantilever + 7 anchor;
// 41 posts; 8 cross-struts per tower; 4 market sheds; 1 ferry.
// layering: paper flecks, the Howrah shore, the river plate and its ruled dashes, the two pale tower
// halfPlates, the whole lattice in ink line, the tower line-work, the deck, the ghat, the ferry, darks.
const PALETTE = [
"#efe6d2", // paper — warm cream sheet
"#2f3a44", // ink — slate-navy: every chord, post, diagonal and gusset
"#5d7f88", // verdigris — the Hooghly, and the far Howrah shore
"#d9cfb6", // stone — caissons, anchorages, the ghat wall and shed walls
"#a8452c", // brick — the flower market's tiled roofs
"#c9a24a", // gilt — one nick on the ferry, the only warm accent
];
const g = (m, sd, lo, hi) => constrain(randomGaussian(m, sd), lo, hi);
const shift = (pts, dx, dy) => pts.map((p) => [p[0] + dx, p[1] + dy]);
const box = (x, y, w, h) => [[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
// a solid grainy plate — the letterpress solid. noStroke: a plate never carries the last pen's outline.
const plate = (pts, col, w, dist, ang, rnd) => {
brush.noStroke();
brush.noFill();
brush.hatchStyle("charcoal", col, w);
brush.hatch(dist, ang, { rand: rnd, continuous: false, gradient: 0 });
brush.polygon(pts);
brush.noHatch();
};
// a half-tone plate — two crossed cpencil passes, paper breathing through
const halfPlate = (pts, col, w, dist, ang) => {
brush.noStroke();
brush.noFill();
brush.hatchStyle("cpencil", col, w);
brush.hatch(dist, ang, { rand: 0.25, continuous: false, gradient: 0 });
brush.polygon(pts);
brush.hatch(dist * 1.12, ang - 84, { rand: 0.25, continuous: false, gradient: 0 });
brush.polygon(pts);
brush.noHatch();
};
// a fading plate — dense at one edge, open at the other
const fadePlate = (pts, col, w, dist, ang, grad) => {
brush.noStroke();
brush.noFill();
brush.hatchStyle("charcoal", col, w);
brush.hatch(dist, ang, { rand: 0.3, continuous: false, gradient: grad });
brush.polygon(pts);
brush.noHatch();
};
const ink = (x1, y1, x2, y2, w, name, col) => {
brush.noFill();
brush.set(name || "rotring", col || PALETTE[1], w || 1.0);
brush.line(x1, y1, x2, y2);
};
const slot = (x, y, w, h) => plate(box(x, y, w, h), PALETTE[1], 1.1, 1.5, 90, 0.2);
// ---- the balanced cantilever, laid out at its real proportions and foreshortened ----
const PANELS = 40;
const T_FAR = 7, CT_A = 15, CT_B = 25, T_NEAR = 33; // tower, tip, tip, tower
const X0 = 120, X1 = 684; // the bridge end to end, anchorage to anchorage
const DY0 = 512, DY1 = 550; // the deck: ONE straight line, rising a little as it recedes
const TOWER_H = 172; // tower height above the deck at unit (nearest) scale
const Q = 1.15; // foreshortening: the far end is ~2.15x further off
const scAt = (i) => 1 / (1 + Q * (1 - i / PANELS));
// top chord height above the deck, in tower heights, AT THE SIX KEY NODES ONLY.
// Everything between them is a straight line in the picture — that is the whole point.
const KEY = [0, T_FAR, CT_A, CT_B, T_NEAR, PANELS];
const KEY_H = [0.22, 1.0, 0.3, 0.3, 1.0, 0.22];
let NX = [], NT = [], ND = [];
const deckAtX = (x) => DY0 + ((x - X0) / (X1 - X0)) * (DY1 - DY0);
const buildTruss = () => {
let s = 0;
for (let i = 0; i < PANELS; i++) s += scAt(i);
const unit = (X1 - X0) / s;
NX = [X0]; NT = []; ND = [];
for (let i = 0; i < PANELS; i++) NX.push(NX[i] + unit * scAt(i));
for (let i = 0; i <= PANELS; i++) { ND.push(deckAtX(NX[i])); NT.push(0); }
// the six key top-chord points, then straight-line interpolation between them in SCREEN space
const ky = [];
for (let k = 0; k < KEY.length; k++) {
const n = KEY[k];
ky.push(ND[n] - TOWER_H * scAt(n) * KEY_H[k]);
}
for (let k = 0; k < KEY.length - 1; k++) {
const a = KEY[k], b = KEY[k + 1];
for (let i = a; i <= b; i++) {
const t = (NX[i] - NX[a]) / (NX[b] - NX[a]);
NT[i] = lerp(ky[k], ky[k + 1], t) + g(0, 0.22, -0.5, 0.5);
}
}
};
// one tower: a riveted steel portal — two battered legs, six cross-struts, X-braced, on its caisson
const towerAt = (i, halfFoot, tw, drop, struts) => {
const xc = NX[i], yT = NT[i] - 3, yD = ND[i], yP = yD + drop;
const halfTop = halfFoot * 0.6;
const legX = (s, t) => xc + s * lerp(halfFoot, halfTop, t);
const tAt = (y) => (yP - y) / (yP - yT);
// the caisson the tower stands on
plate(box(xc - halfFoot - 5, yP - 3, halfFoot * 2 + 10, 9), PALETTE[3], 1.3, 1.6, 8, 0.22);
fadePlate(box(xc - halfFoot - 5, yP - 3, halfFoot * 2 + 10, 9), PALETTE[1], 0.95, 3.2, 0, 0.6);
ink(xc - halfFoot - 6, yP + 6, xc + halfFoot + 6, yP + 6, 1.35);
// the two battered legs, each a pair of rotring lines — open steel, never a solid mass
for (let k = 0; k < 2; k++) {
const s = k === 0 ? -1 : 1;
const xf = legX(s, 0), xu = legX(s, 1);
ink(xf - tw, yP, xu - tw * 0.8, yT, 1.35);
ink(xf + tw, yP, xu + tw * 0.8, yT, 1.15);
for (let m = 0; m < 7; m++) { // the leg's own lacing
const t1 = m / 7, t2 = (m + 1) / 7;
const y1 = lerp(yP, yT, t1), y2 = lerp(yP, yT, t2);
ink(legX(s, t1) - tw, y1, legX(s, t2) + tw, y2, 0.82, "pen");
ink(legX(s, t1) + tw, y1, legX(s, t2) - tw, y2, 0.82, "pen");
}
}
// the portal bracing between the legs, above the deck only
for (let k = 0; k <= struts; k++) {
const t = lerp(tAt(yD - 1), 1, k / struts);
const y = lerp(yP, yT, t);
ink(legX(-1, t), y, legX(1, t), y, 1.12);
if (k < struts) {
const t2 = lerp(tAt(yD - 1), 1, (k + 1) / struts), y2 = lerp(yP, yT, t2);
ink(legX(-1, t), y, legX(1, t2), y2, 0.88, "pen");
ink(legX(1, t), y, legX(-1, t2), y2, 0.88, "pen");
}
}
// the cross-girder over the tower head, and the two finial nicks
plate(box(xc - halfTop - tw * 1.15, yT - 7, (halfTop + tw * 1.15) * 2, 4.5), PALETTE[1], 1.3, 1.6, 0, 0.2);
ink(xc - halfTop - tw * 1.5, yT - 8, xc + halfTop + tw * 1.5, yT - 8, 1.25);
ink(xc - halfTop, yT - 8.5, xc - halfTop, yT - 13, 1.5, "charcoal");
ink(xc + halfTop, yT - 8.5, xc + halfTop, yT - 13, 1.5, "charcoal");
};
function setup() {
createCanvas(800, 800, WEBGL);
angleMode(DEGREES);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background(PALETTE[0]);
// 0 — paper flecks: faint warm specks so the sheet is not dead, never dark
for (let i = 0; i < 220; i++) {
const x = random(10, 790), y = random(10, 790);
ink(x, y, x + g(1.4, 0.5, 0.5, 2.6), y + g(0, 0.4, -1, 1), g(0.45, 0.1, 0.3, 0.6), "2H", random() < 0.7 ? "#bfb39a" : PALETTE[3]);
}
buildTruss();
// 1 — the Howrah bank under the far tower: a pale foreshore, a low run of godowns, dashes
halfPlate([[110, 530], [232, 540], [238, 556], [108, 548]], PALETTE[2], 1.2, 2.3, 62);
halfPlate([[116, 520], [214, 527], [214, 535], [114, 529]], PALETTE[2], 1.15, 2.2, 74);
for (let i = 0; i < 7; i++) slot(122 + i * 13 + g(0, 0.5, -1.2, 1.2), 527, 3.6, 5.2);
ink(114, 529, 216, 536, 1.0, "pen");
ink(108, 547, 196, 552, 1.0, "pen");
for (let i = 0; i < 5; i++) { // the bank dissolving downstream into dashes
const x = 200 + i * 13 + g(0, 1.4, -3, 3);
ink(x, 552 + i * 1.1, x + g(7, 2, 3, 11), 552 + i * 1.1, g(1.0, 0.2, 0.7, 1.4), "2B", PALETTE[2]);
}
// 2 — the Hooghly: one thin verdigris plate, then broken dashes thinning downstream until the
// river dissolves into bare paper. No hard edge anywhere — the dashes overrun the plate's ends.
fadePlate([[214, 560], [640, 584], [664, 616], [148, 594]], PALETTE[2], 1.2, 3.8, 0, 0.75);
for (let i = 0; i < 54; i++) {
const t = i / 54;
const y = 560 + i * 1.62 + g(0, 0.5, -1, 1);
if (random() < t * 0.5) continue; // the surface opens up as it comes on
const near = random() < 0.42; // half the rows are short and set left
const x0 = near ? 116 + random(0, 150) : 190 + random(0, 330);
const len = constrain(randomGaussian((near ? 110 : 176) - t * 90, 66), 12, 368);
if (x0 + len > 688) continue;
ink(x0, y, x0 + len, y, g(1.3 - t * 0.55, 0.28, 0.55, 1.9), random() < 0.65 ? "2B" : "cpencil", random() < 0.78 ? PALETTE[2] : PALETTE[1]);
}
// 3 — the two pale tower halfPlates, BEHIND the line, so the towers read as the heaviest masses
const backPlate = (i, hf, drop) => {
const xc = NX[i], yT = NT[i] - 8, yP = ND[i] + drop, ht = hf * 0.6;
halfPlate([[xc - hf - 2, yP], [xc + hf + 2, yP], [xc + ht + 2, yT], [xc - ht - 2, yT]], PALETTE[1], 1.25, 2.2, 84);
};
backPlate(T_FAR, 16, 14);
backPlate(T_NEAR, 26, 26);
// 4 — the lattice: 40 panels of straight chord, post and K-braced web over open paper
for (let i = 0; i < PANELS; i++) {
const xa = NX[i], xb = NX[i + 1];
const ta = NT[i], tb = NT[i + 1], da = ND[i], db = ND[i + 1];
const dep = (da - ta + db - tb) / 2;
const ma = (ta + da) / 2, mb = (tb + db) / 2;
const left = i < 20;
const wv = 0.72 + scAt(i) * 0.12;
// the K: both diagonals off the mid-point of one post, mirrored about mid-river
if (left) { ink(xa, ma, xb, tb, wv, "pen"); ink(xa, ma, xb, db, wv, "pen"); }
else { ink(xb, mb, xa, ta, wv, "pen"); ink(xb, mb, xa, da, wv, "pen"); }
if (dep > 44) {
// deep panels beside the towers carry an intermediate chord and a second tier of web
ink(xa, ma, xb, mb, wv * 0.9, "pen");
const qa = (ta + ma) / 2, qb = (tb + mb) / 2;
if (left) { ink(xa, qa, xb, tb, wv * 0.85, "pen"); ink(xa, qa, xb, mb, wv * 0.85, "pen"); }
else { ink(xb, qb, xa, ta, wv * 0.85, "pen"); ink(xb, qb, xa, ma, wv * 0.85, "pen"); }
}
ink(xa, ta, xb, tb, 1.12 + scAt(i) * 0.32); // TOP CHORD — straight, sloping, never a curve
ink(xa, da, xb, db, 1.06 + scAt(i) * 0.28); // bottom chord, at the deck
}
for (let i = 0; i <= PANELS; i++) {
ink(NX[i], NT[i] - 1, NX[i], ND[i] + 1, 0.74 + scAt(i) * 0.14, "pen");
}
// 5 — the masonry anchorages the two anchor arms sit down onto
plate(box(108, 513, 25, 22), PALETTE[3], 1.25, 1.7, 12, 0.25);
fadePlate(box(108, 513, 25, 22), PALETTE[1], 0.95, 3.4, 0, 0.6);
ink(106, 535, 135, 536, 1.25);
ink(133, 513, 133, 535, 1.0);
plate(box(662, 550, 34, 28), PALETTE[3], 1.35, 1.6, 8, 0.25);
fadePlate(box(662, 550, 34, 28), PALETTE[1], 1.0, 3.2, 0, 0.62);
ink(660, 578, 698, 579, 1.35);
ink(662, 550, 662, 577, 1.05);
// 6 — the two towers in line: the Howrah one small and far, Kolkata's near and right of centre
towerAt(T_FAR, 16, 3.4, 14, 8);
towerAt(T_NEAR, 26, 5.4, 26, 8);
// 7 — the deck: one straight horizontal line low down, its kerb ruled, a little traffic on it
ink(NX[0] - 4, ND[0] + 2, NX[PANELS] + 4, ND[PANELS] + 2, 1.0); // the deck edge, ruled crisp
ink(NX[0] - 2, ND[0] + 5, NX[PANELS] + 2, ND[PANELS] + 5, 0.7, "pen"); // its shadow, one hair under
for (let i = 0; i < 6; i++) {
const t = 0.22 + i * 0.13, x = lerp(NX[0], NX[PANELS], t), y = deckAtX(x);
ink(x, y - 1, x + g(8, 1.6, 5, 12), y - 1, 2.1, "charcoal");
}
// 8 — Mullick Ghat: the stone bank, the flower market's five low sheds, figures on the steps
plate([[500, 562], [666, 578], [660, 598], [496, 588]], PALETTE[3], 1.3, 1.7, 6, 0.25);
fadePlate([[500, 562], [546, 567], [546, 573], [496, 570]], PALETTE[1], 0.95, 3.4, 0, 0.6);
ink(494, 570, 574, 578, 1.2);
ink(586, 580, 646, 585, 1.1);
for (let i = 0; i < 4; i++) { // the ghat steps dissolving into the river
const x = 500 + i * 24 + g(0, 2, -4, 4), y = 589 + i * 1.4;
ink(x, y, x + g(15, 4, 7, 24), y, g(1.2, 0.25, 0.8, 1.7), "2B", PALETTE[3]);
}
for (let i = 0; i < 5; i++) { // five separate low sheds, paper in the gaps and down to the water
const x = 544 + i * 23, h = 15 - i * 0.6, base = 556 + i * 2.4, top = base - h;
plate(box(x, top + 4, 16, h - 4), PALETTE[3], 1.2, 2.0, 66, 0.25);
plate([[x - 2.5, top + 4], [x + 18.5, top + 4], [x + 15, top], [x + 1, top]],
i === 1 || i === 3 ? PALETTE[4] : PALETTE[3], 1.3, 2.0, 0, 0.25);
ink(x - 3, top + 4.4, x + 19, top + 4.4, 1.0); // the eave, ruled so each roof reads apart
ink(x - 0.5, top + 4.5, x - 0.5, base, 0.8, "pen"); // and its two ends, so it is a box not a mass
ink(x + 16.5, top + 4.5, x + 16.5, base, 0.8, "pen");
ink(x, base, x + 16, base, 0.75, "pen");
slot(x + 5 + g(0, 0.4, -1, 1), top + 8, 4.2, h - 9);
}
for (let i = 0; i < 5; i++) {
const x = 508 + i * 17 + g(0, 1.6, -3.5, 3.5), y = 583 + i * 1.4;
ink(x, y, x + g(0, 0.6, -1.3, 1.3), y - g(7.4, 0.7, 6.2, 8.8), 1.55, "charcoal");
}
// 9 — one ferry crossing under the suspended span, low and left to answer the near tower
plate(box(286, 596, 58, 7), PALETTE[1], 1.4, 1.5, 0, 0.2);
ink(284, 603.5, 348, 603.5, 2.5, "charcoal");
ink(346, 596, 352, 600, 1.5, "charcoal");
ink(286, 596, 282, 600, 1.3, "charcoal");
plate(box(302, 587, 25, 9), PALETTE[1], 1.2, 1.8, 0, 0.25);
ink(314, 588, 314, 579, 2.6, "charcoal");
ink(314, 581, 314, 577, 2.2, "charcoal", PALETTE[5]); // the one gilt nick
// 10 — last darks: the bridge broken on the water, four gussets, crisp ends on the anchor arms
for (let i = 0; i < 16; i++) {
const x = 164 + i * 29 + g(0, 2.4, -6, 6), y = 572 + i * 1.1;
ink(x, y, x + g(13, 3.4, 5, 20), y + g(0, 0.5, -1, 1), g(1.3, 0.3, 0.8, 1.9), "2B", random() < 0.6 ? PALETTE[1] : PALETTE[2]);
}
for (let i = 0; i < 9; i++) {
const x = NX[T_NEAR] + g(0, 7, -16, 16), y = 580 + i * 2.6;
ink(x - 6, y, x + 6, y, g(1.4, 0.3, 0.8, 2.0), "2B", PALETTE[1]);
}
for (let i = 0; i < 6; i++) {
const x = NX[T_FAR] + g(0, 5, -11, 11), y = 562 + i * 2.2;
ink(x - 3, y, x + 3, y, g(1.2, 0.3, 0.7, 1.7), "2B", PALETTE[1]);
}
const gus = [CT_A, CT_B];
for (let i = 0; i < gus.length; i++) {
const n = gus[i], r = 1.8 + scAt(n) * 1.0;
plate(box(NX[n] - r, NT[n] - r + 0.4, r * 2, r * 2), PALETTE[1], 1.1, 1.6, 45, 0.2);
}
ink(NX[0], ND[0] - 12, NX[0], ND[0] + 2, 1.15);
ink(NX[PANELS], ND[PANELS] - 26, NX[PANELS], ND[PANELS] + 2, 1.3);
}