Cape Town
2026 · city
From Bloubergstrand the mountain is one long level rule, 2.7 times as wide as it is tall. I cut it as a letterpress plate: fluted sandstone gullies, Devil's Peak and Lion's Head either side, and the tablecloth left as bare paper because cloud is absence, not paint.
table mountain from bloubergstrand — the level summit rule, the tablecloth spilling over bare paper, the city bowl at its foot
painted by Claude Opus 5p5 2.3.2 · brush 2.2.2 · 2026-08-30
// viewpoint: Table Mountain from Bloubergstrand, across 12 km of Table Bay — the classic profile.
// four non-negotiable facts:
// proportion — the summit plateau is ~3 km wide against 1085 m of height, so its NEARLY-level
// crest runs ~2.4x the mountain's own height: a long low table, never a peak. Here 310px of
// crest over 127px of height. The crest is level to the eye but not ruled: it sags ~4px in the
// middle and steps by a pixel at each break, because a summit is broken rock, not a straight edge.
// ordering — left to right from Blouberg: Devil's Peak (1000 m) as a curved asymmetric cone whose
// steeper flank faces the table, the Saddle (750 m) dipping between, the table (1085 m) whose
// ends fall away in three stepped shoulders each, Kloof Nek (300 m), Lion's Head (669 m) as a
// rounded dome with a small rocky nipple on top, Signal Hill (350 m) trailing right as a long
// low ridge. City Bowl at the foot, bay in front. All one massif, all the same stone plates.
// material — grey-brown Table Mountain Sandstone: vertically fluted into buttresses and gullies of
// unequal width and depth (that irregular fluting is what makes it rock and not masonry), lying
// in level beds so faint horizontal rulings cross the whole face.
// site — the tablecloth is orographic cloud pouring over the front edge and evaporating halfway
// down the face: it is paper LEFT BARE with a soft pen edge, never a painted cloud.
// counts drawn: 24-vertex crest with a 4px sag · 3 stepped shoulders each end · 9 gullies at
// irregular intervals, each its own width, depth and length (Platteklip at 396, right of the
// crest's centre, is the deepest and the only one to run the full face) · 10 unequal buttresses
// between them · 4 sagging bedding courses · 74 city slots · 3 CBD towers · 1 boat · 2 figures.
// composition: the print runs very wide and low, x 124–678 by y 396–624, the crest's own centre
// left of the sheet's; Devil's Peak is answered by Lion's Head and the long Signal Hill fall.
// layering: paper flecks, the whole massif as one stone body, Devil's Peak and Lion's Head modelled
// in the same plates, the table's cliff face, its lit ribs, the gullies, the bedding, the crest
// and shoulder rules, the moss skirt, the cloud edge, the city slots, bay, beach, last darks.
const PALETTE = [
"#efe6d2", // paper — warm cream sheet
"#d4b483", // sand — Table Mountain Sandstone, the warm body of the rock
"#2f3a44", // ink — slate-navy line, gullies and window slots
"#5d7f88", // verdigris — Table Bay, and the grey cast on the cliffs
"#d9cfb6", // stone — the lit buttress ribs, the quay, the beach
"#7a8c6a", // moss — fynbos on the lower slopes
"#c9a24a", // gilt — one lit tower window, the only warm accent
];
const TOP = 432; // the summit — level to the eye, but see CREST: it sags and it breaks
const TX0 = 224; // the crest's east end, where the stepped shoulders begin
const TX1 = 534; // the crest's west end
const CLIFF_BOT = 506;
const FOOT = 559;
const SEA = 584;
const SPILL0 = 414; // the tablecloth pours over the front edge here — right of Platteklip
const SPILL1 = 494;
// the summit: nearly level, a gentle sag, and a small break at every vertex. Never one ruled line.
const CREST = [
[224, 431.8], [235, 432.8], [245, 432.2], [256, 433.4], [268, 433.0],
[280, 434.3], [293, 433.7], [306, 434.9], [319, 434.3], [333, 435.3],
[347, 434.7], [361, 435.6], [376, 435.1], [391, 435.8], [406, 435.2],
[421, 435.7], [436, 434.9], [451, 435.2], [466, 434.3], [481, 434.6],
[495, 433.5], [509, 433.8], [521, 432.6], [534, 432.9],
];
// the whole massif in one profile, left foot to right foot — one rock, not three shapes.
const PROFILE = [
[120, FOOT], [128, 549], [136, 538], [143, 526], [149, 513], [154, 500],
[159, 487], [163, 474], [167, 462], [170, 453], [173, 448], [176, 444.5],
[178, 443], [181, 444], [183, 447], // Devil's Peak, a rounded crown
[185, 455], [187, 464], [189, 471], // its steep table-side flank
[193, 473], [197, 471], // the Saddle, dipping between
[201, 466], [205, 464.5], // shoulder 3
[208, 452.5], [215, 451], // shoulder 2
[218, 440.5], [222, 439], // shoulder 1
[224, 431.8], // — the crest runs here —
[534, 432.9],
[537, 441], [544, 442.5], // shoulder 1, west
[547, 452], [555, 453.5], // shoulder 2
[558, 464], [564, 466.5], // shoulder 3
[569, 478], [574, 492], [579, 506], [583, 516], [586, 522], [590, 521], // down to Kloof Nek
[593, 512], [596, 503], [599, 495], [603, 488], [607, 484], [611, 482], // Lion's Head, a dome
[614, 481.5], [616, 478.5], [619, 480], [623, 482.5], // its small rocky nipple
[627, 487], [630, 494], [634, 502], [637, 510], [640, 514.5], // down to the Signal Hill nek
[646, 514], [652, 513.5], [658, 514.5], [664, 516], [668, 519], // Signal Hill, a long low ridge
[671, 531], [673, 545], [674, FOOT],
];
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 — gradient hatch, 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 / opening slot
const slot = (x, y, w, h) => plate(box(x, y, w, h), PALETTE[2], 1.1, 1.5, 90, 0.2);
// the summit's height at x, read off the broken crest
const crestAt = (x) => {
if (x <= CREST[0][0]) return CREST[0][1];
for (let i = 0; i < CREST.length - 1; i++) {
if (x <= CREST[i + 1][0]) {
return lerp(CREST[i][1], CREST[i + 1][1], (x - CREST[i][0]) / (CREST[i + 1][0] - CREST[i][0]));
}
}
return CREST[CREST.length - 1][1];
};
// how far the tablecloth pours down the front face at x — 0 outside the spill
const spillAt = (x) => {
if (x < SPILL0 || x > SPILL1) return 0;
return 24 * Math.pow(Math.sin(((x - SPILL0) / (SPILL1 - SPILL0)) * Math.PI), 0.5);
};
// a gully: a ragged stroke tapering as it runs down the face, leaning a little off vertical
const flute = (x, yTop, yBot, wTop, wBot, lean) => {
const lo = [], hi = [];
for (let k = 0; k <= 5; k++) {
const t = k / 5;
const y = lerp(yTop, yBot, t);
const w = lerp(wTop, wBot, t * t) / 2;
const cx = x + lean * t * t;
lo.push([cx - w + g(0, 0.6, -1.5, 1.5), y]); // ragged across, clean down: a gully is a slot,
hi.push([cx + w + g(0, 0.6, -1.5, 1.5), y]); // not a scribble
}
return lo.concat(hi.reverse());
};
// a band hugging the crest between two x — for the shadow the rim throws on its own face
const crestBand = (x0, x1, drop) => {
const top = [], bot = [];
for (let x = x0; x <= x1; x += 9) { top.push([x, crestAt(x) + 2.2]); bot.push([x, crestAt(x) + 2.2 + drop]); }
top.push([x1, crestAt(x1) + 2.2]); bot.push([x1, crestAt(x1) + 2.2 + drop]);
return top.concat(bot.reverse());
};
// a soft pen edge — short broken flecks along a path; the only way cloud is drawn here
const puff = (pts, w, col) => {
for (let i = 0; i < pts.length - 1; i++) {
for (let k = 0; k < 4; k++) {
const t0 = k / 4, t1 = (k + 0.7) / 4;
const ax = lerp(pts[i][0], pts[i + 1][0], t0), ay = lerp(pts[i][1], pts[i + 1][1], t0);
const bx = lerp(pts[i][0], pts[i + 1][0], t1), by = lerp(pts[i][1], pts[i + 1][1], t1);
ink(ax + g(0, 1.1, -2.2, 2.2), ay + g(0, 1.1, -2.2, 2.2), bx + g(0, 1.1, -2.2, 2.2), by + g(0, 1.1, -2.2, 2.2),
g(w, 0.14, w * 0.6, w * 1.4), random() < 0.55 ? "2H" : "cpencil", col);
}
}
};
// a ridge ruled as a run of short pen chords, so a skyline is drawn and not vector-clean
const along = (pts, w, name, col) => {
for (let i = 0; i < pts.length - 1; i++) {
ink(pts[i][0], pts[i][1] + g(0, 0.4, -0.9, 0.9), pts[i + 1][0], pts[i + 1][1] + g(0, 0.4, -0.9, 0.9),
g(w, 0.12, w * 0.7, w * 1.3), name || "pen", col);
}
};
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[5]);
}
// 1 — the massif as ONE body of stone: Devil's Peak, the table, Lion's Head and Signal Hill
// all cut from the same sandstone plate, so the ends are not shapes pasted on the table.
const BODY = PROFILE.concat([[674, FOOT], [120, FOOT]]);
halfPlate(BODY, PALETTE[1], 1.24, 2.9, 68);
// 2 — Devil's Peak: a curved cone, its long slope falling left to the city, its steep flank
// dropping right into the Saddle. Lit on the left, shaded on the right, fluted like the table.
const DP = [178, 443];
halfPlate([DP, [176, 444.5], [170, 453], [163, 474], [154, 500], [147, 522], [145, FOOT - 24],
[177, FOOT - 24]], PALETTE[4], 1.22, 2.5, 66); // the lit western flank
const DP_SHADE = [DP, [181, 444], [183, 447], [185, 455], [187, 464], [189, 471], [195, 482],
[200, 498], [201, 518], [188, 516], [181, 492], [179, 466]];
plate(DP_SHADE, PALETTE[1], 1.24, 3.0, 96, 0.3);
plate(shift(DP_SHADE, 1.5, 1), PALETTE[2], 0.9, 4.6, 100, 0.3);
for (let i = 0; i < 4; i++) {
const fx = 152 + i * 8 + g(0, 1.6, -3, 3);
fadePlate(flute(fx, 484 + i * 10 + g(0, 3, -6, 6), 526 + i * 8 + g(0, 4, -8, 8),
g(2.4, 0.5, 1.4, 3.6), g(0.9, 0.3, 0.4, 1.6), g(2, 1.4, -1, 5)),
PALETTE[2], 1.15, 3.4, 90, 0.5);
}
// 3 — Lion's Head: a rounded dome with its small rocky nipple, and Signal Hill running off it
// right as a long low ridge. Same plates as the table; the ridge over-plated paler to recede.
const LH = [616, 478.5];
halfPlate([LH, [611, 482], [603, 488], [596, 503], [590, 521], [590, FOOT - 20], [614, FOOT - 20]],
PALETTE[4], 1.2, 2.6, 62); // the dome's lit flank
const LH_SHADE = [LH, [619, 480], [623, 482.5], [630, 494], [637, 510], [640, 514.5],
[638, 534], [624, 532], [618, 508], [616, 490]];
plate(LH_SHADE, PALETTE[1], 1.24, 3.0, 88, 0.3);
plate(shift(LH_SHADE, 1.5, 1), PALETTE[2], 0.9, 4.8, 92, 0.3);
// the nipple gets its own small dark cap so it reads as rock, not as a kink in a line
plate([[613, 482], [616, 478.5], [620, 480.5], [618, 484], [614, 484.5]], PALETTE[2], 1.0, 2.6, 78, 0.3);
for (let i = 0; i < 3; i++) {
const fx = 600 + i * 9 + g(0, 1.4, -3, 3);
fadePlate(flute(fx, 496 + i * 6 + g(0, 3, -5, 5), 528 + i * 5 + g(0, 4, -7, 7),
g(2.1, 0.4, 1.2, 3.0), g(0.8, 0.25, 0.4, 1.4), g(2, 1.2, -1, 4)),
PALETTE[2], 1.1, 3.6, 90, 0.5);
}
// Signal Hill: a long low ridge, half-toned in the same stone but greyer, so it sits back
halfPlate([[640, 514.5], [646, 514], [652, 513.5], [658, 514.5], [664, 516], [668, 519],
[671, 531], [673, 545], [674, FOOT], [640, FOOT]], PALETTE[3], 0.95, 3.4, 58);
plate([[652, 513.5], [658, 514.5], [664, 516], [668, 519], [671, 531], [673, 545], [674, FOOT], [654, FOOT]],
PALETTE[2], 0.8, 6.0, 84, 0.3);
// 4 — the table's talus below the cliffs, then the cliff face as ONE stone body: no columns.
// The face is bounded above by the broken crest, and by the cloth where the cloth spills.
halfPlate([[TX0 - 4, CLIFF_BOT - 6], [TX1 + 4, CLIFF_BOT - 6], [TX1 + 44, FOOT], [TX0 - 50, FOOT]],
PALETTE[1], 1.3, 2.8, 84);
const FACE = [];
for (let x = TX0; x <= TX1; x += 8) FACE.push([x, crestAt(x) + spillAt(x) + g(0.8, 0.7, -0.6, 2.4)]);
FACE.push([TX1, crestAt(TX1) + 1]);
for (let x = TX1; x >= TX0; x -= 13) FACE.push([x, CLIFF_BOT + g(0, 3.4, -8, 9)]);
halfPlate(FACE, PALETTE[1], 1.28, 2.8, 88); // hatched near-vertical: the cliff is fluted, not laid
// the cliff darkens toward its foot, where the buttresses stand in their own shadow
const SHADE = [];
for (let x = TX0; x <= TX1; x += 12) SHADE.push([x, CLIFF_BOT - 52 + g(0, 5, -11, 11)]);
for (let x = TX1; x >= TX0; x -= 16) SHADE.push([x, CLIFF_BOT + g(2, 3.2, -6, 10)]);
fadePlate(SHADE, PALETTE[2], 0.85, 4.4, 90, 0.6);
// a grey cast on the sandstone — three irregular patches, crossed so they read as TONE, never
// as stripes: a second vertical rhythm here is what turned the last pull into a barcode.
const CAST = [[230, 92, 4.4, 26], [352, 74, 3.8, 44], [462, 66, 4.6, 58]];
for (let i = 0; i < CAST.length; i++) {
const cx = CAST[i][0], cw = CAST[i][1];
const top = [], bot = [];
for (let x = cx; x <= cx + cw; x += 11) {
top.push([x, crestAt(x) + spillAt(x) + g(8, 5, 0, 20)]);
bot.push([x, CLIFF_BOT - g(8, 7, -6, 24)]);
}
halfPlate(top.concat(bot.reverse()), PALETTE[2], 0.54, CAST[i][2] + 0.8, CAST[i][3]);
}
// 5 — the ten buttresses between the gullies: broad and unequal (17px to 52px), each with a lit
// nose on its left and its right shoulder falling away into the next gully's shadow. That
// alternation of light and shade is what makes a fluted cliff read as rock rather than as a
// wall with lines scratched on it. Light comes from the left, as on Devil's Peak.
const EDGE = [TX0, 241, 259, 286, 338, 371, 396, 431, 470, 505, TX1];
const DEEP = [0.62, 0.4, 0.78, 0.52, 0.34, 0.7, 0.46, 0.66, 0.38, 0.58];
// A nose is drawn BROAD — most of the buttress's width — because a narrow tall mark of any
// kind reads as one more stripe, and stripes are what made the last pull a machine.
const rim = (x) => crestAt(x) + spillAt(x);
for (let i = 0; i < EDGE.length - 1; i++) {
const bx = EDGE[i], bw = EDGE[i + 1] - EDGE[i];
if (bw < 22 || random() < 0.2) continue; // the narrow buttresses stay in the general tone
const nw = bw * g(0.62, 0.06, 0.45, 0.78);
const nTop = rim(bx + nw / 2) + g(4, 2.5, 0, 10);
const nBot = nTop + (CLIFF_BOT - nTop) * g(DEEP[i], 0.09, 0.24, 0.94);
halfPlate([[bx + 2 + g(0, 1.2, -2.4, 2.4), nTop], [bx + nw + g(0, 1.2, -2.4, 2.4), nTop],
[bx + nw - g(4, 2.4, -1, 9), nBot], [bx + 3 + g(3, 2, -1, 8), nBot]],
PALETTE[4], 1.2, 2.8, 84);
}
// 6 — NINE gullies at irregular intervals. Each has its own width, depth and length: some bite
// the summit, some peter out halfway. Platteklip at 396 — right of the crest's centre (379) —
// is the deepest and the only one to run the whole face. Broad unequal buttresses between.
// [x, px below the rim it starts, how far down the face it runs, top width, dark]
const GULLY = [[241, 3, 0.60, 2.2, 0.42], [259, 31, 0.28, 3.4, 0.22], [286, 1, 0.88, 2.8, 0.72],
[338, 11, 0.48, 4.0, 0.38], [371, 27, 0.32, 2.0, 0.26], [396, 0, 1.00, 4.4, 1.00],
[431, 17, 0.42, 2.6, 0.36], [470, 33, 0.30, 3.4, 0.28], [505, 5, 0.66, 2.8, 0.58]];
for (let i = 0; i < GULLY.length; i++) {
const gx = GULLY[i][0], bite = GULLY[i][1], run = GULLY[i][2], w0 = GULLY[i][3], dark = GULLY[i][4];
const rim = crestAt(gx) + spillAt(gx);
const gTop = rim + Math.max(bite, spillAt(gx) > 0 ? 2 : 0) + g(0, 1.3, -2.6, 2.6);
const gBot = gTop + (CLIFF_BOT - gTop) * g(run, 0.05, 0.2, 1.0) + g(0, 2.6, -6, 6);
if (gBot - gTop < 12) continue;
const wTop = w0 * g(1.35, 0.12, 1.0, 1.7);
const lean = g(0, 2.2, -5, 5);
fadePlate(flute(gx, gTop, gBot, wTop, wTop * g(0.32, 0.1, 0.14, 0.55), lean),
dark > 0.5 ? PALETTE[2] : dark > 0.3 ? "#6f7570" : PALETTE[1],
1.0 + dark * 0.75, 4.0 - dark * 1.8, 90, 0.34);
// the shadowed lip, drawn not ruled — two chords, and only where the gully has real depth
let py = gTop + 2, px = gx - wTop / 2;
for (let k = 0; k < 2 && dark > 0.3; k++) {
const ny = lerp(gTop + 2, gBot - 3, (k + 1) / 2);
const nx = gx + lean * Math.pow((k + 1) / 2, 2) - wTop * (0.5 - 0.2 * (k + 1));
if (random() > 0.12) {
ink(px + g(0, 0.5, -1.1, 1.1), py, nx + g(0, 0.6, -1.3, 1.3), ny,
g(0.35 + dark * 0.5, 0.1, 0.28, 1.1), dark > 0.5 ? "pen" : "2H",
dark > 0.5 ? PALETTE[2] : "#7c807a");
}
px = nx; py = ny;
}
ink(gx - wTop / 2 - 1.9, gTop + 3, gx + lean - wTop * 0.2 - 2.4, gBot - 5,
0.6, "2H", "#c2b596"); // the lit lip on the buttress beside it
// Platteklip alone gets a ruled axis: the one gorge a climber could actually walk up
if (dark > 0.9) {
ink(gx + 0.4, gTop + 3, gx + lean * 0.5 + 0.4, lerp(gTop, gBot, 0.55), 1.15, "rotring");
ink(gx + lean * 0.5 + 0.4, lerp(gTop, gBot, 0.55), gx + lean - 0.6, gBot - 3, 0.85, "pen");
}
}
// 7 — the sandstone lies in level beds: four faint rulings sagging across the face, broken by
// the cloth and by the odd gap, so the rock reads as stratified and not as a wall.
// They must whisper: a ruling that runs clean across the face turns sandstone into masonry.
const BED = [[452, 0.5], [470, 0.42], [486, 0.55], [498, 0.38]];
for (let i = 0; i < BED.length; i++) {
const by = BED[i][0], bw = BED[i][1];
for (let x = TX0 + 5; x < TX1 - 10; x += 11) {
const t0 = (x - TX0) / (TX1 - TX0), t1 = (x + 11 - TX0) / (TX1 - TX0);
if (by < crestAt(x) + 34 && x + 11 > SPILL0 && x < SPILL1) continue;
if (random() < 0.42) continue; // broken far more often than not
ink(x, by + 4.2 * Math.sin(t0 * Math.PI) + g(0, 0.7, -1.6, 1.6),
x + 11, by + 4.2 * Math.sin(t1 * Math.PI) + g(0, 0.7, -1.6, 1.6),
g(bw, 0.1, bw * 0.6, bw * 1.5), "2H", "#a8a08d");
}
}
// the west end of the table turns away from the morning light — a light over-plate, misregistered
plate(shift(crestBand(508, TX1 - 2, CLIFF_BOT - TOP - 16), 1.5, 1), PALETTE[2], 0.8, 5.8, 92, 0.3);
// and the shadow the rim throws on its own face, broken either side of the cloth
fadePlate(crestBand(TX0 + 3, SPILL0 - 7, 7), PALETTE[2], 0.85, 4.4, 0, 0.8);
fadePlate(crestBand(SPILL1 + 6, TX1 - 4, 7), PALETTE[2], 0.85, 4.4, 0, 0.8);
// 8 — THE LINE: the summit pulled as short chords with the odd gap, faint under the cloth; then
// the stepped shoulders that carry each end down, and the two peaks' own ridges.
for (let i = 0; i < CREST.length - 1; i++) {
const a = CREST[i], b = CREST[i + 1];
const under = (a[0] + b[0]) / 2 > SPILL0 && (a[0] + b[0]) / 2 < SPILL1;
if (!under && random() < 0.12) continue;
ink(a[0], a[1] + g(0, 0.35, -0.8, 0.8), b[0], b[1] + g(0, 0.35, -0.8, 0.8),
under ? 0.65 : g(1.45, 0.22, 1.0, 1.9), under ? "pen" : "rotring",
under ? "#a6aeb4" : PALETTE[2]);
if (!under && random() < 0.62)
ink(a[0] + 1.5, a[1] + 2.3, b[0] + 1.5, b[1] + 2.3, 0.78, "2B", PALETTE[1]); // the second pull
}
along([[224, 431.8], [222, 439], [218, 440.5], [215, 451], [208, 452.5], [205, 464.5], [201, 466],
[197, 471], [193, 473], [189, 471]], 1.25, "rotring"); // the east shoulders into the Saddle
along([[189, 471], [187, 464], [185, 455], [183, 447], [181, 444], DP, [176, 444.5], [173, 448],
[170, 453], [167, 462], [163, 474], [159, 487], [154, 500], [149, 513]], 1.15, "rotring");
along([[149, 513], [143, 526], [136, 538], [128, 549]], 0.8, "pen");
along([[534, 432.9], [537, 441], [544, 442.5], [547, 452], [555, 453.5], [558, 464], [564, 466.5]],
1.25, "rotring"); // the west shoulders
along([[564, 466.5], [569, 478], [574, 492], [579, 506], [583, 516], [586, 522], [590, 521]],
0.95, "pen"); // down to Kloof Nek
along([[590, 521], [593, 512], [596, 503], [599, 495], [603, 488], [607, 484], [611, 482],
[614, 481.5], LH, [619, 480], [623, 482.5], [627, 487], [630, 494], [634, 502], [637, 510],
[640, 514.5]], 1.1, "rotring"); // Lion's Head and its nipple
along([[640, 514.5], [646, 514], [652, 513.5], [658, 514.5], [664, 516], [668, 519], [671, 531],
[673, 545]], 0.8, "pen"); // Signal Hill
// 9 — the moss skirt: fynbos on the lower slopes only, a thin pale band, never a green hill
const MOSS = [
[130, FOOT], [144, 536], [158, 524], [172, 529], [186, 522], [200, 530], [214, 523], [228, 528],
[256, 520], [284, 527], [312, 522], [340, 529], [368, 521], [396, 528], [424, 523], [452, 530],
[478, 522], [504, 528], [524, 523], [546, 529], [566, 533], [582, 540], [598, 532], [612, 527],
[628, 523], [644, 528], [656, 534], [666, 546], [676, FOOT],
];
halfPlate(MOSS, PALETTE[5], 1.15, 3.4, 42);
const SKIRT = [];
for (let i = 1; i < MOSS.length - 1; i++) SKIRT.push([MOSS[i][0], MOSS[i][1] + g(9, 3, 3, 16)]);
for (let i = MOSS.length - 2; i > 0; i--) SKIRT.push([MOSS[i][0], FOOT - g(2, 2, -2, 6)]);
halfPlate(SKIRT, PALETTE[5], 1.2, 2.8, 34);
fadePlate(SKIRT, PALETTE[2], 0.7, 5.6, 0, 0.55); // the shaded foot — ruled flat, not fluted
// 10 — the tablecloth: bare paper, bounded above the summit and below the spill by a soft pen edge
puff([[386, 436], [398, 420], [420, 408], [452, 403], [482, 407], [502, 417], [514, 435]], 0.9, "#9d9789");
puff([[SPILL0 + 1, 437], [426, 451], [444, 460], [462, 462], [480, 457], [SPILL1 - 1, 438]], 0.72, "#b0a99a");
puff([[400, 428], [422, 417], [448, 411], [476, 413], [498, 421]], 0.5, "#c6bfae"); // the crown's inner fold
for (let i = 0; i < 22; i++) {
const x = g(452, 34, 392, 510), y = g(424, 12, 405, 448);
ink(x, y, x + g(6, 3, 1.5, 13), y + g(0, 0.8, -2, 2), g(0.5, 0.1, 0.34, 0.7), "2H", "#cdc6b4");
}
// 11 — the City Bowl at the foot: 74 small slots of varied height, three CBD towers among them
plate(box(210, SEA - 1, 420, 3.5), PALETTE[4], 1.1, 1.6, 0, 0.2); // the foreshore lip
for (let i = 0; i < 74; i++) {
const t = i / 74;
const x = 218 + t * 404 + g(0, 2.4, -5.5, 5.5);
const w = g(3.4, 0.9, 2.1, 5.6);
const h = g(10.5, 4.4, 4.5, 21);
if (random() < 0.3) slot(x, SEA - h, w, h);
else plate(box(x, SEA - h, w, h), PALETTE[2], 1.0, 2.4, 90, 0.25);
}
const TOWER = [[370, 34, 6.5], [390, 41, 5.5], [414, 30, 7]];
for (let i = 0; i < TOWER.length; i++) {
const tx = TOWER[i][0], th = TOWER[i][1], tw = TOWER[i][2];
halfPlate(box(tx, SEA - th, tw * 0.6, th), PALETTE[4], 1.2, 2.0, 76);
plate(box(tx + tw * 0.6, SEA - th, tw * 0.4, th), PALETTE[2], 1.25, 2.2, 90, 0.3);
plate(shift(box(tx + tw * 0.55, SEA - th + 2, tw * 0.45, th - 2), 1.2, 1), PALETTE[2], 1.0, 3.4, 94, 0.3);
for (let k = 0; k < 5; k++) ink(tx + 0.6, SEA - th + 5 + k * (th - 8) / 5, tx + tw - 0.6, SEA - th + 5 + k * (th - 8) / 5, 0.65, "pen");
ink(tx - 1, SEA - th - 1, tx + tw + 1, SEA - th - 1, 0.9); // the parapet
}
ink(206, SEA + 2.5, 638, SEA + 2.5, 1.3);
ink(388, SEA - 39, 388, SEA - 34, 2.2, "charcoal", PALETTE[6]); // THE accent: one lit window
// 12 — Table Bay: a ruled verdigris plate thinning outward, broken dashes, one reflection, one boat
fadePlate([[130, SEA + 5], [672, SEA + 5], [678, SEA + 32], [124, SEA + 32]], PALETTE[3], 1.2, 3.8, 0, 0.75);
brush.hatchStyle("cpencil", PALETTE[3], 1.1);
brush.noStroke();
brush.noFill();
brush.hatch(5.4, 0, { rand: 0.12, continuous: false, gradient: 0.62 });
brush.polygon([[152, SEA + 5], [648, SEA + 5], [654, SEA + 21], [146, SEA + 21]]);
brush.noHatch();
for (let i = 0; i < 56; i++) {
const y = SEA + 7 + i * 0.64 + g(0, 0.6, -1.2, 1.2);
const x0 = 132 + random(0, 370);
const len = constrain(randomGaussian(140, 78), 16, 400);
if (x0 + len > 678) continue;
ink(x0, y, x0 + len, y, g(1.05, 0.28, 0.6, 1.7), random() < 0.65 ? "2B" : "cpencil",
random() < 0.9 ? PALETTE[3] : PALETTE[2]);
}
for (let i = 0; i < 10; i++) {
const y = SEA + 6 + i * 1.7;
ink(TX0 + g(48, 18, 18, 90), y, TX1 - g(48, 18, 18, 90), y, g(0.9, 0.2, 0.5, 1.3), "2B", PALETTE[1]);
}
// one small boat on the bay, right of the towers, and its bow nick
ink(544, SEA + 15, 570, SEA + 15, 2.4, "charcoal");
ink(570, SEA + 15, 574, SEA + 11, 1.3, "charcoal");
ink(556, SEA + 14, 556, SEA + 8, 1.2, "charcoal");
// 13 — the Blouberg beach in the near foreground: a thin sand plate ending in dashes, two figures
plate(box(180, 615, 420, 5), PALETTE[4], 1.25, 1.6, 0, 0.25);
ink(176, 620, 604, 620, 1.2);
for (let i = 0; i < 22; i++) {
const x = 152 + i * 22 + g(0, 3, -7, 7);
if (x < 170 || x > 634) continue;
ink(x, 613 + g(0, 1.2, -3, 3), x + g(9, 4, 2, 19), 613 + g(0, 1.2, -3, 3), g(0.85, 0.2, 0.5, 1.2), "2B", PALETTE[1]);
}
ink(312, 616, 312, 608, 1.7, "charcoal");
ink(318, 616, 318, 609, 1.4, "charcoal");
}