Paris
2026 · city
The tower is not a mass, so I refused to print it as one. Every chord, cross and belt is a drawn line and the paper stays open between them. I stood downstream of the Trocadero axis, where two of the four first-floor arches show and the bridge turns broadside.
the eiffel tower drawn as open ironwork across the seine, over the five arches of the pont d'iena
painted by Claude Opus 5p5 2.3.2 · brush 2.2.2 · 2026-08-30
// composition: a letterpress vignette of the Eiffel Tower from the right bank a little downstream of
// the Trocadero axis — far enough off the axis that the Pont d'Iena crosses the sheet broadside and
// all five of its arches read, and that the tower's square base turns ~25 deg, so the nearest corner
// comes forward and exactly TWO of the four first-floor arches are visible: the wide near face and,
// to its right, one narrow foreshortened side face. The tower stands on the axis x=372; the bridge
// and the ruled Seine carry the width below it; two runs of Haussmann facade flank its feet.
//
// four facts held:
// 1 PROPORTION — 330 m on a 125 m square. Turned 25 deg the square projects 165 m wide, so the
// print's half-width at the ground is 0.24 of its height, not a needle. First platform at 57 m
// (17%), second at 115 m (35%), top platform at 276 m (84%), antenna finishing at 330 m.
// 2 MATERIAL — the tower is OPEN PUDDLED IRON, not a mass. Every member is a drawn line over bare
// paper: rotring chords for the four corner uprights, pen crosses for the bracing, rotring belts.
// A very pale half-plate sits behind it and nothing else; paper shows between every member.
// 3 ORDERING — river and bridge in front and lowest; the far quai and its blocks behind them; the
// tower behind that. The four feet project to x offsets -1.00, -0.364, +0.364, +1.00 of the base
// half-width and their depths differ, so their feet sit at four different heights on the sheet.
// 4 SITE — the feet stand on level ground above the river; the Haussmann blocks on the quai are six
// storeys, under a fifth of the tower, and they stop well short of the sheet's edges.
//
// counts drawn: 4 legs (the near pair heavier, the far pair paler); 2 visible first-floor arches —
// the near one full at 94 px, the side one narrow and foreshortened at 35 px; 5 lattice bays per
// leg, the lowest two sub-braced; 5 bays from the first platform to the second; 16 belts from the
// second platform to the top; 3 platforms, one cupola, one antenna; 5 arches and 4 river piers on
// the Pont d'Iena; 2 Haussmann blocks of 2 x 5 windows.
//
// layering: paper flecks, Seine plate, the far quay, the pale half-plates behind the ironwork, the
// far legs, the narrow arch, the near legs, the near arch girder, the first platform, the pylon
// bands, the upper platforms and cupola, the antenna, the Haussmann blocks and quai, the bridge in
// front of everything, water dashes and a bateau-mouche, the deepest darks, one gilt mark last.
const PALETTE = [
"#efe6d2", // paper — warm cream sheet
"#4e4038", // iron — the tower's warm brown-slate paint
"#2f3a44", // ink — slate-navy line and shadow plate
"#5d7f88", // verdigris — the Seine and the zinc roofs
"#d9cfb6", // stone — the bridge's masonry and Haussmann limestone
"#c9a24a", // gilt — the summit lamp, the one warm accent
];
const BASE = 650; // the tower's nearest foot on the Champ de Mars
const APEX = 212; // the tip of the antenna
const SPAN = BASE - APEX;
const AXIS = 372; // the tower's vertical axis
const HALF = 101; // outer half-width at the ground
// [height fraction, half-width as a fraction of the ground half-width]
const PROF = [
[0.000, 1.000], [0.030, 0.898], [0.070, 0.780], [0.115, 0.664],
[0.173, 0.560], [0.240, 0.452], [0.290, 0.384], [0.348, 0.322],
[0.450, 0.240], [0.560, 0.186], [0.680, 0.152], [0.790, 0.118],
[0.836, 0.100], [0.860, 0.112], [0.890, 0.062], [0.915, 0.026],
[1.000, 0.008],
];
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[2], w || 1.0);
brush.line(x1, y1, x2, y2);
};
// a dark window / arch slot
const slot = (x, y, w, h) => plate(box(x, y, w, h), PALETTE[2], 1.1, 1.5, 90, 0.2);
// a drawn chord — a polyline of ruled segments, the way an iron upright is drafted
const strand = (pts, w, name, col) => {
for (let i = 0; i < pts.length - 1; i++) {
ink(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], w, name, col);
}
};
// --- tower geometry -------------------------------------------------------
const wAt = (t) => {
const tt = constrain(t, 0, 1);
let i = 0;
for (; i < PROF.length - 2; i++) if (PROF[i + 1][0] >= tt) break;
const u = (tt - PROF[i][0]) / max(0.0001, PROF[i + 1][0] - PROF[i][0]);
return lerp(PROF[i][1], PROF[i + 1][1], constrain(u, 0, 1));
};
const yAt = (t) => BASE - t * SPAN;
const xAt = (ox, t) => AXIS + ox * HALF * wAt(t);
const T1 = 0.173; // first platform
const T2 = 0.348; // second platform
const T3 = 0.836; // top platform
const TSP = 0.075; // the springing of the first-floor arches
// the four corners of the square base, turned 25 deg: the corners project to x offsets
// cos(70), cos(160), cos(250), cos(340) of the half-diagonal, i.e. -1.00, -0.364, +0.364, +1.00.
// Depth differs per corner, so each foot lands at its own height: the nearest corner (+0.364)
// lowest, the hidden far corner (-0.364) highest.
const NR = 0.364;
const LEGS = [
{ ox: -NR, lift: -22, wid: 18, near: false },
{ ox: 1.00, lift: -16, wid: 23, near: false },
{ ox: -1.00, lift: -8, wid: 27, near: true },
{ ox: NR, lift: 0, wid: 30, near: true },
];
const legHalf = (leg, t) => lerp(leg.wid, 7, constrain(t / T1, 0, 1)) / 2;
const legY = (leg, t) => yAt(t) + leg.lift * (1 - constrain(t / T1, 0, 1));
const legPoly = (leg, t0, t1, steps) => {
const l = [], r = [];
for (let i = 0; i <= steps; i++) {
const t = lerp(t0, t1, i / steps);
const cx = xAt(leg.ox, t), hw = legHalf(leg, t), y = legY(leg, t);
l.push([cx - hw, y]);
r.push([cx + hw, y]);
}
return l.concat(r.reverse());
};
const legEdge = (leg, sgn, steps) => {
const pts = [];
for (let i = 0; i <= steps; i++) {
const t = (i / steps) * T1;
pts.push([xAt(leg.ox, t) + sgn * legHalf(leg, t), legY(leg, t)]);
}
return pts;
};
// the bay rhythm inside a leg: large open panels at the foot, tightening as it rises
const LEGBAYS = [0, 0.30, 0.555, 0.75, 0.89, 1.0];
// a leg drawn as iron: two rotring chords, pen crosses in the real bay rhythm, rotring belts
const legIron = (leg) => {
const wt = leg.near ? 0.82 : 0.62;
const bw = leg.near ? 1.05 : 0.85;
for (let i = 0; i < 5; i++) {
const t0 = LEGBAYS[i] * T1, t1 = LEGBAYS[i + 1] * T1;
const a = { x: xAt(leg.ox, t0), h: legHalf(leg, t0), y: legY(leg, t0) };
const b = { x: xAt(leg.ox, t1), h: legHalf(leg, t1), y: legY(leg, t1) };
ink(a.x - a.h, a.y, b.x + b.h, b.y, g(wt, 0.08, 0.35, 1.2), "pen", PALETTE[1]);
ink(a.x + a.h, a.y, b.x - b.h, b.y, g(wt, 0.08, 0.35, 1.2), "pen", PALETTE[1]);
ink(b.x - b.h - 1, b.y, b.x + b.h + 1, b.y + g(0, 0.2, -0.4, 0.4), g(bw, 0.1, 0.6, 1.3),
"rotring", leg.near ? PALETTE[2] : PALETTE[1]);
if (i < 2) {
const m = { x: (a.x + b.x) / 2, h: (a.h + b.h) / 2, y: (a.y + b.y) / 2 };
ink(a.x - a.h, a.y, m.x + m.h, m.y, wt * 0.68, "pen", PALETTE[1]);
ink(a.x + a.h, a.y, m.x - m.h, m.y, wt * 0.68, "pen", PALETTE[1]);
ink(m.x - m.h, m.y, b.x + b.h, b.y, wt * 0.68, "pen", PALETTE[1]);
ink(m.x + m.h, m.y, b.x - b.h, b.y, wt * 0.68, "pen", PALETTE[1]);
ink(m.x - m.h - 1, m.y, m.x + m.h + 1, m.y, wt * 0.9, "pen", PALETTE[1]);
}
}
strand(legEdge(leg, -1, 10), leg.near ? 1.35 : 0.95, "rotring", leg.near ? PALETTE[2] : PALETTE[1]);
strand(legEdge(leg, 1, 10), leg.near ? 1.2 : 0.8, "rotring", leg.near ? PALETTE[2] : PALETTE[1]);
};
// one first-floor arch: a curved girder springing off the INNER face of each leg, so its ends meet
// the iron instead of hooking down past it
const archOf = (legA, legB, rise, lift) => ({
xA: xAt(legA.ox, TSP) + legHalf(legA, TSP) * 0.6,
xB: xAt(legB.ox, TSP) - legHalf(legB, TSP) * 0.6,
yS: yAt(TSP) + lift,
rise: rise,
});
// the parabola the girder follows, sampled for its radial ribs
const archAt = (a, u) => [lerp(a.xA, a.xB, u), a.yS - a.rise * (1 - Math.pow(2 * u - 1, 2))];
// the girder is drawn, not plated: a 3-vertex charcoal curve carries the warm iron core, and two
// ruled rotring curves bound it above and below so it ends hard instead of feathering into a smear.
// Paper stays open under the arch.
const archIron = (a, weight, depth, ribs) => {
brush.noFill();
brush.set("charcoal", PALETTE[1], weight);
brush.beginShape(0.55);
brush.vertex(a.xA, a.yS - depth * 0.5);
brush.vertex((a.xA + a.xB) / 2, a.yS - a.rise - depth * 0.5);
brush.vertex(a.xB, a.yS - depth * 0.5);
brush.endShape();
const ext = [], intr = [];
for (let i = 0; i <= 20; i++) {
const p = archAt(a, i / 20);
ext.push([p[0], p[1] - depth]);
intr.push([p[0], p[1] + depth * 0.4]);
}
strand(ext, 1.05, "rotring", PALETTE[2]);
strand(intr, 0.85, "rotring", PALETTE[2]);
for (let i = 1; i < ribs; i++) {
const p = archAt(a, i / ribs);
ink(p[0], p[1] - depth, p[0] + g(0, 0.3, -0.7, 0.7), p[1] + depth * 0.4, 0.65, "pen", PALETTE[2]);
}
};
// a band of the pylon above the first platform, drawn as iron: crosses on the two visible faces
// and one ruled belt per bay. The chords are drawn once, over the whole run.
const ironBand = (t0, t1, bays, wt, sub) => {
for (let i = 0; i < bays; i++) {
const a = lerp(t0, t1, i / bays), b = lerp(t0, t1, (i + 1) / bays);
const ya = yAt(a), yb = yAt(b);
// the wide near face: the nearest corner (+0.364) to the left corner (-1.00)
ink(xAt(-1, a), ya, xAt(NR, b), yb, g(wt, 0.07, 0.35, 1.3), "pen", PALETTE[1]);
ink(xAt(NR, a), ya, xAt(-1, b), yb, g(wt, 0.07, 0.35, 1.3), "pen", PALETTE[1]);
// the narrow side face, strongly foreshortened
ink(xAt(NR, a), ya, xAt(1, b), yb, g(wt * 0.85, 0.07, 0.3, 1.2), "pen", PALETTE[1]);
ink(xAt(1, a), ya, xAt(NR, b), yb, g(wt * 0.85, 0.07, 0.3, 1.2), "pen", PALETTE[1]);
if (sub) {
const m = (a + b) / 2, ym = yAt(m);
ink(xAt(-1, a), ya, xAt(-0.318, m), ym, wt * 0.55, "pen", PALETTE[1]);
ink(xAt(NR, a), ya, xAt(-0.318, m), ym, wt * 0.55, "pen", PALETTE[1]);
ink(xAt(-0.318, m), ym, xAt(-1, b), yb, wt * 0.55, "pen", PALETTE[1]);
ink(xAt(-0.318, m), ym, xAt(NR, b), yb, wt * 0.55, "pen", PALETTE[1]);
}
ink(xAt(-1, b) - 1.6, yb, xAt(1, b) + 1.6, yb + g(0, 0.25, -0.5, 0.5),
g(1.0, 0.1, 0.7, 1.25), "rotring", PALETTE[2]);
}
};
// the four corner uprights, followed as curves the whole way up
const ironChords = (t0, t1, steps) => {
const chordAt = (ox) => {
const pts = [];
for (let i = 0; i <= steps; i++) {
const t = lerp(t0, t1, i / steps);
pts.push([xAt(ox, t), yAt(t)]);
}
return pts;
};
strand(chordAt(-1), 1.05, "rotring", PALETTE[2]);
strand(chordAt(1), 0.95, "rotring", PALETTE[1]);
strand(chordAt(NR), 0.9, "rotring", PALETTE[2]);
strand(chordAt(-NR), 0.7, "pen", PALETTE[1]);
};
// a platform: a thin slab plate, misregistered in ink, with pen-tick railings above it
const deckOf = (t, lip, thick, col) => {
const hw = HALF * wAt(t) + lip, y = yAt(t);
const pts = box(AXIS - hw, y - thick * 0.4, hw * 2, thick);
plate(pts, col, 1.4, 1.7, 4, 0.25);
plate(shift(pts, 1.2, 1), PALETTE[2], 1.05, 3.0, 0, 0.3);
ink(AXIS - hw - 3, y - thick * 0.4, AXIS + hw + 3, y - thick * 0.4 - 0.6, 1.35);
ink(AXIS - hw - 2, y + thick * 0.6, AXIS + hw + 2, y + thick * 0.6, 1.0);
return { hw: hw, y: y };
};
// a Haussmann block on the quai: limestone face, zinc mansard, two storeys of five windows
const quaiBlock = (x, w, top, sill) => {
halfPlate(box(x, top + 11, w, sill - top - 11), PALETTE[4], 1.15, 2.7, 66);
const roof = [[x + 2, top], [x + w - 2, top], [x + w, top + 11], [x, top + 11]];
plate(roof, PALETTE[3], 1.05, 3.2, 14, 0.3);
fadePlate(box(x, top + 11, w, 5), PALETTE[2], 0.85, 4.0, 0, 0.7);
ink(x - 2, top + 11, x + w + 2, top + 11, 1.2);
ink(x + 1, top, x + w - 1, top, 0.85, "pen");
for (let r = 0; r < 2; r++) {
for (let i = 0; i < 5; i++) {
slot(x + 5 + i * ((w - 10) / 5) + g(0, 0.3, -0.8, 0.8), top + 15 + r * 7.6, 3.2, 4.8);
}
}
ink(x, top + 11, x, sill, 0.9);
ink(x + w, top + 11, x + w, sill, 0.8);
for (let i = 0; i < 3; i++) {
const cx = x + 9 + i * (w / 3.2);
ink(cx, top + 1, cx + g(0, 0.5, -1, 1), top - g(6, 1.4, 3.5, 9), 1.7, "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[4]);
}
// 1 — the Seine: one plate thinning downstream, no wider than the print
fadePlate([[196, 652], [614, 648], [626, 716], [186, 720]], PALETTE[3], 0.95, 5.4, 0, 0.82);
brush.noStroke();
brush.noFill();
brush.hatchStyle("cpencil", PALETTE[3], 1.05);
brush.hatch(6.0, 0, { rand: 0.12, continuous: false, gradient: 0.6 });
brush.polygon([[210, 708], [594, 704], [602, 722], [202, 725]]);
brush.noHatch();
// 2 — the far quay the tower stands on: one thin stone lip, visible through the open arches
plate(box(196, 646, 440, 3.5), PALETTE[4], 1.1, 1.6, 0, 0.2);
ink(194, 648, 636, 648, 0.9);
// 3 — the pale ground the ironwork is drawn over: one very open half-plate per leg, nothing more
for (let i = 0; i < LEGS.length; i++) {
const leg = LEGS[i];
halfPlate(legPoly(leg, 0, T1, 6), PALETTE[1], leg.near ? 0.9 : 0.66, leg.near ? 3.4 : 4.4,
leg.near ? 64 : 58);
}
// 4 — the far pair of legs, drawn light: the hidden far corner and the right side corner
legIron(LEGS[0]);
legIron(LEGS[1]);
// 5 — the narrow side arch, foreshortened, springing from the nearest corner to the right one
archIron(archOf(LEGS[3], LEGS[1], 22, -12), 1.5, 3.4, 5);
// 6 — the near pair of legs, the heavier iron
legIron(LEGS[2]);
legIron(LEGS[3]);
// 7 — the near arch: the full first-floor girder across the wide face, paper open beneath it
archIron(archOf(LEGS[2], LEGS[3], 26, 0), 2.0, 4.2, 9);
// 8 — the web of iron between the arch crown and the first platform, drawn as crosses
const webTop = yAt(T1) + 2;
const webL = xAt(-1, 0.150) + 4, webR = xAt(NR, 0.150) - 3;
for (let i = 0; i < 7; i++) {
const x0 = lerp(webL, webR, i / 7), x1 = lerp(webL, webR, (i + 1) / 7);
ink(x0, 586, x1, webTop, 0.55, "pen", PALETTE[1]);
ink(x1, 586, x0, webTop, 0.55, "pen", PALETTE[1]);
}
ink(webL - 2, 586, webR + 2, 586, 0.85, "rotring", PALETTE[1]);
// 9 — the first platform: a thin slab with a ticked railing and a run of glazing
const d1 = deckOf(T1, 8, 10, PALETTE[1]);
for (let i = 0; i < 9; i++) slot(AXIS - d1.hw + 6 + i * ((d1.hw * 2 - 14) / 9), d1.y - 10, 4.4, 5.0);
ink(AXIS - d1.hw - 1, d1.y - 10.5, AXIS + d1.hw + 1, d1.y - 11, 0.85, "rotring", PALETTE[1]);
for (let i = 0; i < 14; i++) {
const x = AXIS - d1.hw + 4 + i * ((d1.hw * 2 - 8) / 13);
ink(x, d1.y - 5, x, d1.y - 10.5, 0.65, "pen", PALETTE[1]);
}
// 10 — the pylon above the first platform: pale half-plates behind, then the iron drawn over them
const bandPoly = (t0, t1) => {
const l = [], r = [];
for (let i = 0; i <= 5; i++) {
const t = lerp(t0, t1, i / 5);
l.push([xAt(-1, t), yAt(t)]);
r.push([xAt(1, t), yAt(t)]);
}
return l.concat(r.reverse());
};
halfPlate(bandPoly(T1, T2), PALETTE[1], 0.9, 3.5, 66);
halfPlate(bandPoly(T2, 0.66), PALETTE[1], 0.85, 3.7, 70);
halfPlate(bandPoly(0.66, T3), PALETTE[1], 0.8, 3.8, 74);
ironBand(T1, T2, 5, 0.8, true);
ironBand(T2, 0.52, 4, 0.74, false);
ironBand(0.52, 0.66, 4, 0.68, false);
ironBand(0.66, 0.78, 4, 0.6, false);
ironBand(0.78, T3, 4, 0.52, false);
ironChords(T1, T2, 6);
ironChords(T2, T3, 12);
// 11 — the second platform, the small 196 m ledge, the top platform and its cupola
const d2 = deckOf(T2, 8, 9.5, PALETTE[1]);
for (let i = 0; i < 14; i++) {
const x = AXIS - d2.hw + 2 + i * ((d2.hw * 2 - 4) / 13);
ink(x, d2.y - 4, x, d2.y - 9, 0.7, "pen");
}
for (let i = 0; i < 6; i++) slot(AXIS - d2.hw + 5 + i * ((d2.hw * 2 - 12) / 6), d2.y - 8, 3.6, 4.4);
deckOf(0.594, 3.5, 4.5, PALETTE[1]);
deckOf(T3, 4, 6.5, PALETTE[1]);
// the cupola: one small solid plate, the last mass on the sheet
plate(box(AXIS - 7.5, yAt(T3) - 15, 15, 12), PALETTE[1], 1.35, 1.9, 68, 0.25);
plate(shift(box(AXIS + 1, yAt(T3) - 15, 6.5, 12), 1, 1), PALETTE[2], 1.05, 3.0, 90, 0.3);
ink(AXIS - 9, yAt(T3) - 15, AXIS + 9, yAt(T3) - 15.5, 1.1);
for (let i = 0; i < 4; i++) slot(AXIS - 5.5 + i * 3.4, yAt(T3) - 12.5, 2.2, 5.2);
// the antenna: two tapering irons and four rings
ink(AXIS - 2.2, yAt(T3) - 15, AXIS - 0.6, APEX + 4, 1.3, "rotring", PALETTE[2]);
ink(AXIS + 2.4, yAt(T3) - 15, AXIS + 0.8, APEX + 4, 1.05, "rotring", PALETTE[1]);
const rings = [254, 240, 228, 220];
for (let i = 0; i < rings.length; i++) {
const hw = 3.4 - i * 0.7;
ink(AXIS - hw, rings[i], AXIS + hw, rings[i] + g(0, 0.3, -0.6, 0.6), g(0.85, 0.1, 0.6, 1.1));
}
ink(AXIS - 1.4, APEX + 6, AXIS + 1.4, APEX + 6, 1.4, "charcoal", PALETTE[1]);
// 12 — the Haussmann blocks on the quai, flanking the feet and overlapping them a little
quaiBlock(190, 76, 620, 647);
quaiBlock(522, 112, 614, 647);
// 13 — the Pont d'Iena in front of everything: a stone body with five openings punched through it,
// standing on four river piers, the roadway ruled along the top and ticked with balusters
const SPR = 692, WAT = 708, RISE = 20;
const tiltAt = (x) => -(x - 192) * 0.011;
const bodyPts = [[192, 662], [606, 657], [606, 708], [192, 713]];
halfPlate(bodyPts, PALETTE[4], 1.22, 2.55, 6);
fadePlate([[192, 663], [606, 658], [606, 672], [192, 677]], PALETTE[2], 0.9, 3.6, 26, 0.7);
const spans = [[208, 270], [288, 350], [368, 430], [448, 510], [528, 590]];
for (let i = 0; i < spans.length; i++) {
const x0 = spans[i][0], x1 = spans[i][1], tilt = tiltAt(x0);
const ring = [];
for (let k = 0; k <= 12; k++) {
const u = k / 12;
const h = RISE * Math.sqrt(max(0, 1 - Math.pow(2 * u - 1, 2)));
ring.push([lerp(x0, x1, u), SPR + tilt - h]);
}
// the voids are the darkest slots in the print
plate(ring.concat([[x1, WAT + tilt], [x0, WAT + tilt]]), PALETTE[2], 1.5, 1.4, 84, 0.25);
for (let k = 0; k < 12; k++) {
ink(ring[k][0], ring[k][1] - 1.8, ring[k + 1][0], ring[k + 1][1] - 1.8, 1.1);
}
}
for (let i = 0; i < 4; i++) {
const px = spans[i][1], tilt = tiltAt(px);
ink(px, SPR + tilt - 5, px, WAT + tilt + 1, 0.85, "pen");
ink(px + 18, SPR + tilt - 5, px + 18, WAT + tilt + 1, 0.85, "pen");
}
// the parapet, ruled in rotring: the cap, the roadway line and the waterline
ink(188, 656, 610, 651, 1.0);
ink(188, 662, 610, 657, 1.0);
ink(192, 673, 606, 668, 1.0);
ink(194, 710, 604, 705, 1.0);
for (let i = 0; i < 42; i++) {
const x = 194 + i * 10;
ink(x, 662 - (x - 192) * 0.0121, x, 656.5 - (x - 192) * 0.0121, 0.7, "pen");
}
// people on the parapet — the scale marks, and nothing more
ink(262, 652, 262, 644, 1.6, "charcoal");
ink(266, 652, 266.5, 645, 1.4, "charcoal");
ink(486, 648, 486, 640, 1.6, "charcoal");
// 14 — the water: broken dashes that stop short of the sheet, the reflection, the bateau-mouche
for (let i = 0; i < 40; i++) {
const y = 710 + i * 0.6 + g(0, 0.5, -1, 1);
const x0 = 166 + random(0, 330);
const len = constrain(randomGaussian(84, 54), 14, 250);
if (x0 + len > 644) continue;
if (y > 712 && y < 722 && x0 < 430 && x0 + len > 372) continue; // leave the boat its water
ink(x0, y, x0 + len, y, g(1.15, 0.3, 0.7, 1.8), random() < 0.65 ? "2B" : "cpencil",
random() < 0.8 ? PALETTE[3] : PALETTE[2]);
}
for (let i = 0; i < 9; i++) {
const y = 726 + i * 0.7;
ink(AXIS - 22 + g(0, 3, -5, 6), y, AXIS + 22 - g(0, 3, -5, 6), y, g(1.0, 0.25, 0.6, 1.5), "2B", PALETTE[3]);
}
// a bateau-mouche under the middle arch: one charcoal dash and a nick for the bow
ink(380, 717, 421, 716.6, 2.2, "charcoal", PALETTE[2]);
ink(421, 716.6, 428, 713.4, 1.2, "charcoal", PALETTE[2]);
ink(378, 719.4, 424, 719, 0.8, "rotring", PALETTE[2]);
// 15 — last darks: the outer irons restated at the feet, the shadow pooling under the near legs
for (let i = 0; i < LEGS.length; i++) {
const leg = LEGS[i];
const y0 = legY(leg, 0), hw = legHalf(leg, 0);
// each foot lands on its own line: the four corners stand at four depths on the same ground
ink(xAt(leg.ox, 0) - hw - 2, y0, xAt(leg.ox, 0) + hw + 2, y0, leg.near ? 1.5 : 1.0);
if (leg.near) fadePlate(legPoly(leg, 0, 0.022, 2), PALETTE[2], 0.9, 4.2, 90, 0.6);
else ink(xAt(leg.ox, 0) - hw, y0 + 2, xAt(leg.ox, 0) + hw, y0 + 2, 0.7, "pen");
}
// 16 — the summit lamp: one gilt mark, and nothing else warm on the sheet
ink(AXIS, APEX + 8, AXIS, APEX, 2.2, "charcoal", PALETTE[5]);
}