Singapore
2026 · city
Each tower is two solid slabs, not legs: one stands straight, one leans in from a wider footing and meets it at the 23rd of 55 floors. I counted every floor in ruled glass, kept the SkyPark as thin as it is, and left the sky as paper.
marina bay sands and the artscience lotus seen across the bay from the esplanade
painted by Claude Opus 5p5 2.3.2 · brush 2.2.2 · 2026-08-30
// viewpoint: Marina Bay Sands across Marina Bay from the Esplanade / Merlion promenade, the three
// towers side by side under one SkyPark, the ArtScience lotus small and pale to the west.
// facts — (1) proportion: each of the three towers is 55 storeys, ~230px tall and ~54px wide at the
// top, a 4.3:1 shaft — dense and glassy, not skeletal; (2) ordering/overlap: a tower is TWO tall
// SOLID slabs, the east slab vertical and the west slab leaning in from a wider footing to meet it
// at the 23rd of 55 floors, so the lower two-fifths is a narrow tapering V of bare paper between
// two solid masses and everything above is one merged shaft; the middle tower stands nearest and
// overlaps its neighbours' feet; (3) material: blue-green glass curtain wall — a verdigris
// halfPlate carrying 55 ruled pen floor lines, ink over-plates on the V's inner faces and on the
// shaded east flanks, concrete edge columns as stone pen rulings the full height; (4) site: the
// SkyPark is a THIN 7px deck, not a slab, spanning all three towers and cantilevering 66px past
// the north tower over open air like a ship's bow; the bay is ruled water; the sky is bare paper.
// counts drawn: 3 towers x 2 solid slabs = 6, 55 floor lines per tower, 6 stone edge columns per
// tower, 10 lotus petals, 1 deck with 1 cantilever, 30 railing ticks, 6 tree ticks, 1 double-helix
// bridge, 3 broken reflections, 1 bumboat, 1 gilt nick.
// layering: paper flecks, the shore lip, the lotus and the helix bridge, the south and north towers,
// then the nearer middle tower over them, the thin deck, the bay and its reflections, last darks,
// and the single gilt nick on the deck.
const PALETTE = [
"#efe6d2", // paper — warm cream sheet
"#2f3a44", // ink — slate-navy line and shade plate
"#5d7f88", // verdigris — the glass curtain wall, the bay
"#d9cfb6", // stone — concrete edge columns, the deck, the museum shell
"#c9a24a", // gilt — the one warm accent, a glint on the deck
];
const SHORE = 578; // the far shore's lip; the bay begins below it
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);
};
// rule a polygon's perimeter — a plate becomes a drawn thing when its edge is ruled
const outlineOf = (pts, w, name, col) => {
for (let i = 0; i < pts.length; i++) {
const a = pts[i], b = pts[(i + 1) % pts.length];
ink(a[0], a[1], b[0], b[1], w, name, col);
}
};
// rule an open path segment by segment, so a ruled edge follows a sagging plate instead of cutting it
const alongPath = (path, w, name, col) => {
for (let i = 0; i < path.length - 1; i++) {
ink(path[i][0], path[i][1], path[i + 1][0], path[i + 1][1], w, name, col);
}
};
const yOnPath = (path, x) => {
for (let i = 0; i < path.length - 1; i++) {
const a = path[i], b = path[i + 1];
if (x >= a[0] && x <= b[0]) return lerp(a[1], b[1], (x - a[0]) / (b[0] - a[0]));
}
return path[path.length - 1][1];
};
// one lotus petal of the ArtScience Museum — a tapering finger, not a triangle
const petalAt = (cx, cy, aDeg, len, wid) => {
const a = (aDeg * Math.PI) / 180;
const dx = Math.sin(a), dy = -Math.cos(a);
const px = Math.cos(a), py = Math.sin(a);
const p = (t, s) => [cx + dx * len * t + px * s, cy + dy * len * t + py * s];
return [p(0, -2.6), p(0.38, -wid * 0.5), p(0.76, -wid * 0.27), p(1, 0), p(0.76, wid * 0.27), p(0.38, wid * 0.5), p(0, 2.6)];
};
// one tower: two SOLID slabs — the east one vertical, the west one leaning in from a wider
// footing to merge with it at the 23rd of 55 floors. Between them, low down, a taper of bare paper.
const slabTower = (L, W, yB, yT, splay, near) => {
const h = yB - yT;
const yM = yB - h * 0.42; // the 23rd floor of 55 — where the two slabs become one shaft
const wW = Math.round(W * 0.46); // the west slab's width inside the merged shaft
const oB = L - splay; // the west slab's outer foot — the wider footing
const iB = L + wW - 15; // its inner foot; the 15px gap to the east slab is the foot of the V
const west = [[oB, yB], [iB, yB], [L + wW, yM], [L + wW, yT], [L, yT], [L, yM]];
const east = box(L + wW, yT, W - wW, h);
const d = near ? 2.1 : 2.2;
// the glass: a verdigris half-plate with a thin second pass so the mass reads heavy, not open
halfPlate(west, PALETTE[2], 1.3, d, 74);
halfPlate(east, PALETTE[2], 1.35, d, 88);
// ink over-plates: the two inner faces of the V, and the shaded east flank the whole height
plate([[iB - 5, yB], [iB, yB], [L + wW, yM], [L + wW - 5, yM]], PALETTE[1], 1.15, 3.4, 84, 0.3);
plate(box(L + wW, yM, 5, yB - yM), PALETTE[1], 1.1, 3.6, 90, 0.3);
plate(shift(box(L + W - 11, yT, 11, h), 1.5, 1), PALETTE[1], 1.2, 3.0, 92, 0.3);
// 55 storeys: a ruled pen floor line every ~4px, heavier at the sky-lobby bands
for (let f = 1; f < 55; f++) {
const y = yB - (h * f) / 55 + g(0, 0.25, -0.6, 0.6);
const sky = f % 11 === 0; // the sky-lobby bands, every eleventh floor
const wt = sky ? 1.45 : 1.0, pen = sky ? "rotring" : "pen";
ink(L + wW, y, L + W, y, wt, pen);
if (y <= yM) {
ink(L, y, L + wW, y, wt, pen);
} else {
const k = (yB - y) / (yB - yM);
ink(lerp(oB, L, k), y, lerp(iB, L + wW, k), y, wt, pen);
}
}
// the concrete edge columns — stone rulings running the full height, kinked where the slab leans
ink(oB, yB, L, yM, 0.8, "pen", PALETTE[3]); ink(L, yM, L, yT, 0.8, "pen", PALETTE[3]);
ink(iB, yB, L + wW, yM, 0.7, "pen", PALETTE[3]);
ink(L + wW, yB, L + wW, yT, 0.7, "pen", PALETTE[3]);
ink(L + W, yT, L + W, yB, 0.8, "pen", PALETTE[3]);
ink(L + wW * 0.5, yT, L + wW * 0.5, yM, 0.6, "pen", PALETTE[3]);
ink(L + W - 9, yT, L + W - 9, yB, 0.6, "pen", PALETTE[3]);
// last darks on this tower: the crisp arrises that keep the slabs solid
ink(oB, yB, L, yM, 1.0); ink(L, yM, L, yT, 1.0);
ink(iB, yB, L + wW, yM, 0.9);
ink(L + wW, yM, L + wW, yB, 0.9);
ink(L + W, yT, L + W, yB, 1.3);
// the shadow the tower casts on its own foot
fadePlate(box(oB, yB - 16, 16, 16), PALETTE[1], 1.0, 3.2, 90, 0.7);
};
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, sparse, 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]);
}
// 1 — the far shore: one thin stone lip the whole print stands on
plate(box(146, SHORE, 500, 4), PALETTE[3], 1.1, 1.6, 0, 0.2);
ink(142, SHORE + 4, 652, SHORE + 4, 1.2);
// 2 — the ArtScience Museum: a bowl with ten petals of unequal length opening from it
const MX = 194, MY = 566;
const ANG = [-78, -61, -45, -29, -13, 3, 19, 35, 52, 70];
const LEN = [36, 43, 49, 55, 58, 52, 46, 40, 35, 30];
for (let i = 0; i < 10; i++) {
const a = ANG[i] + g(0, 1.3, -3, 3), ln = LEN[i] + g(0, 1.2, -3, 3);
const pts = petalAt(MX, MY, a, ln, 10 + ln * 0.19);
halfPlate(pts, PALETTE[3], 1.2, 2.4, 38 + i * 11);
// ruled, so each petal is a counted finger of the flower and not one pale shrub
outlineOf(pts, 0.6, "pen");
}
// the shallow bowl the petals open from — a saucer on the shore, not a pot
halfPlate([[MX - 20, MY - 1], [MX + 20, MY - 1], [MX + 13, SHORE - 2], [MX - 14, SHORE - 2]], PALETTE[3], 1.2, 2.2, 66);
plate([[MX + 7, MY - 1], [MX + 20, MY - 1], [MX + 13, SHORE - 2], [MX + 5, SHORE - 2]], PALETTE[1], 0.95, 4.0, 84, 0.3);
ink(MX - 20, MY - 1, MX + 20, MY - 1, 0.9, "pen");
ink(MX - 15, SHORE - 2, MX + 14, SHORE - 2, 1.0);
// 3 — the Helix Bridge: a fine pen double spiral running out to the towers
for (let i = 0; i < 44; i++) {
const t = i / 44, u = (i + 1) / 44;
const xa = lerp(264, 322, t), xb = lerp(264, 322, u);
const ya = 564 - 3 * t - 4.4 * Math.sin(t * Math.PI * 5);
const yb = 564 - 3 * u - 4.4 * Math.sin(u * Math.PI * 5);
const ya2 = 564 - 3 * t + 4.4 * Math.sin(t * Math.PI * 5);
const yb2 = 564 - 3 * u + 4.4 * Math.sin(u * Math.PI * 5);
ink(xa, ya, xb, yb, 0.6, "pen");
ink(xa, ya2, xb, yb2, 0.6, "pen");
if (i % 6 === 0) ink(xa, ya, xa, ya2, 0.5, "pen");
}
ink(262, 568, 324, 565, 0.8, "pen");
// 4 — the three towers. South and north first; the middle one is nearer, so it goes over them.
slabTower(306, 54, 572, 345, 14, false);
slabTower(462, 54, 573, 345.5, 14, false);
slabTower(382, 58, 577, 347, 14, true);
// 5 — the SkyPark: a THIN deck, 7px, spanning all three towers and flying 66px past the north one
const DTOP = [[294, 338.5], [336, 338], [382, 339.5], [411, 340], [440, 339.5], [486, 338.5], [516, 338], [550, 336.5], [582, 334.6]];
const DBOT = [[579, 340], [550, 343.5], [516, 345], [486, 345.5], [440, 346.5], [411, 347], [382, 346.5], [336, 345], [293, 345.5]];
// the ink shadow beneath the deck, laid first so the stone plate prints over its top edge
fadePlate(DBOT.concat(shift(DBOT, -1, 5).reverse()), PALETTE[1], 1.1, 3.0, 0, 0.7);
halfPlate(DTOP.concat(DBOT), PALETTE[3], 1.35, 2.0, 6);
alongPath(DTOP, 1.3);
alongPath(DBOT.slice().reverse(), 1.0, "pen");
// the railing along the deck, thirty ticks, and six tiny trees
for (let i = 0; i < 30; i++) {
const x = 298 + i * 9.5, y = yOnPath(DTOP, x);
ink(x + g(0, 0.3, -0.7, 0.7), y, x, y - g(3.6, 0.6, 2.4, 4.6), 0.65, "pen");
}
const TREE = [326, 366, 418, 464, 504, 542];
for (let i = 0; i < TREE.length; i++) {
const x = TREE[i], y = yOnPath(DTOP, x);
ink(x, y, x, y - 4.4, 1.4, "charcoal");
ink(x - 2.2, y - 4.4, x + 2.2, y - 4.8, 1.6, "charcoal");
}
// 6 — the bay: a verdigris plate thinning downward, broken dashes, three reflections, one bumboat
fadePlate([[128, SHORE + 6], [652, SHORE + 6], [664, SHORE + 46], [116, SHORE + 46]], PALETTE[2], 1.05, 4.6, 0, 0.82);
brush.hatchStyle("cpencil", PALETTE[2], 1.15);
brush.noStroke(); brush.noFill();
brush.hatch(5.0, 0, { rand: 0.12, continuous: false, gradient: 0.62 });
brush.polygon([[150, SHORE + 6], [636, SHORE + 6], [642, SHORE + 24], [144, SHORE + 24]]);
brush.noHatch();
for (let i = 0; i < 52; i++) {
const y = SHORE + 8 + i * 1.4 + g(0, 0.5, -1, 1);
const x0 = 110 + random(0, 400);
const len = constrain(randomGaussian(126, 66), 18, 380);
if (x0 + len > 700) continue;
ink(x0, y, x0 + len, y, g(1.25, 0.3, 0.7, 1.9), random() < 0.65 ? "2B" : "cpencil", random() < 0.82 ? PALETTE[2] : PALETTE[1]);
}
const REFL = [[292, 360], [368, 440], [448, 516]];
for (let s = 0; s < REFL.length; s++) {
for (let i = 0; i < 9; i++) {
const y = SHORE + 7 + i * 3.0;
const x0 = REFL[s][0] + g(0, 2.5, -5, 5), x1 = REFL[s][1] + g(0, 2.5, -5, 5);
const a = random(0.05, 0.3), b = random(0.7, 0.95); // broken, never the whole width
ink(lerp(x0, x1, a), y, lerp(x0, x1, b), y, g(1.5, 0.3, 1.0, 2.1), "2B", i === 2 ? PALETTE[1] : PALETTE[2]);
}
}
// one bumboat low on the bay
ink(242, SHORE + 33, 272, SHORE + 33, 2.4, "charcoal");
ink(272, SHORE + 33, 276, SHORE + 29, 1.3, "charcoal");
ink(240, SHORE + 33, 237, SHORE + 30, 1.1, "charcoal");
// 7 — the accent: one gilt nick on the deck, and nothing else warm on the sheet
ink(458, 339.6, 462, 340.1, 1.5, "charcoal", PALETTE[4]);
}