// Babel: Fifty Thousand Voices — deterministic complete-code painting. // Authored canvas: 2048 x 1152 WEBGL. Published raster: 7680 x 4320. // // The granular replay measured 55,137 instrumented drawing operations. The // published manifest defines the counter boundary and replay method. const W = 2048; const H = 1152; const SEED = 94871; const NIGHT = [7, 12, 32]; const INK = [19, 37, 66]; const COBALT = [35, 87, 137]; const ICE = [126, 188, 193]; const CLAY = [159, 91, 66]; const AMBER = [247, 181, 91]; const ROSE = [221, 88, 132]; async function setup() { randomSeed(SEED); noiseSeed(SEED); createCanvas(W, H, WEBGL); brush.scaleBrushes(1.05); noLoop(); } function babelTint(rgb, alpha = 255) { return color(rgb[0], rgb[1], rgb[2], alpha); } function fillRgb(rgb, alpha = 255) { const c = babelTint(rgb, alpha); noStroke(); fill(red(c), green(c), blue(c), alpha); } function lineRgb(rgb, alpha, weight) { const c = babelTint(rgb, alpha); stroke(red(c), green(c), blue(c), alpha); strokeWeight(weight); noFill(); } function haze(cx, cy, rx, ry, rgb, alpha, rings = 12) { const c = babelTint(rgb); drawingContext.globalCompositeOperation = "screen"; noStroke(); for (let i = rings; i >= 1; i--) { const t = i / rings; fill(red(c), green(c), blue(c), alpha * (1 - t * 0.65)); ellipse(cx, cy, rx * t, ry * t); } drawingContext.globalCompositeOperation = "source-over"; } function drawNightSky() { for (let y = 0; y < H; y += 4) { const t = y / H; const r = 5 + t * 12; const g = 10 + t * 19; const b = 28 + t * 35; fill(r, g, b, 255); noStroke(); rect(0, y, W, 5); } haze(340, 264, 740, 340, [34, 98, 154], 12, 22); haze(1640, 318, 820, 430, [50, 44, 136], 10, 20); haze(1060, 112, 680, 175, [27, 123, 143], 10, 16); } function drawWeather() { drawingContext.globalCompositeOperation = "screen"; for (let i = 0; i < 420; i++) { const x = (i * 97) % W; const y = 110 + ((i * 53) % 500); const len = 18 + (i % 9) * 7; const bend = sin(i * 0.71) * 15; lineRgb(i % 11 === 0 ? [112, 194, 185] : [69, 126, 168], 25 + (i % 5) * 5, 0.48 + (i % 3) * 0.18); line(x, y, x + bend, y + len); } for (let i = 0; i < 100; i++) { const x = 120 + i * 20; const y = 206 + sin(i * 0.47) * 52; lineRgb(i % 5 === 0 ? [211, 102, 148] : [70, 178, 169], 38, 0.66); bezier(x, y, x + 80, y - 100, x + 150, y + 100, x + 260, y - 15); } drawingContext.globalCompositeOperation = "source-over"; } function drawDistantGround() { noStroke(); fillRgb([10, 22, 45], 238); beginShape(); vertex(0, 786); for (let x = 0; x <= W; x += 24) vertex(x, 744 + sin(x * 0.006) * 35 + noise(x * 0.01) * 42); vertex(W, H); vertex(0, H); endShape(CLOSE); lineRgb([73, 117, 137], 58, 1.2); beginShape(); for (let x = 0; x <= W; x += 18) vertex(x, 744 + sin(x * 0.006) * 35 + noise(x * 0.01) * 42); endShape(); haze(1000, 794, 1400, 260, [33, 97, 111], 14, 13); } function drawTowerSilhouette() { fillRgb([28, 33, 52], 245); noStroke(); beginShape(); vertex(250, 1040); vertex(1640, 1040); vertex(1512, 914); vertex(1440, 790); vertex(1356, 682); vertex(1328, 562); vertex(1228, 468); vertex(1254, 356); vertex(1146, 272); vertex(1092, 176); vertex(1010, 128); vertex(932, 188); vertex(886, 316); vertex(786, 406); vertex(812, 520); vertex(694, 628); vertex(710, 744); vertex(548, 870); vertex(410, 964); endShape(CLOSE); drawingContext.globalCompositeOperation = "screen"; haze(1030, 392, 520, 720, [52, 94, 145], 11, 15); drawingContext.globalCompositeOperation = "source-over"; } function drawMasonry() { // One brick is one quad plus six deliberate pigment/mortar marks. The // taper makes the mark field architectural rather than a repeated texture. const top = 214; const bottom = 1028; const rows = 150; for (let row = 0; row < rows; row++) { const t = row / (rows - 1); const y = top + t * (bottom - top); const lean = -86 * (1 - t) + sin(t * 5.2) * 32; const center = 1024 + lean + (t > 0.72 ? (t - 0.72) * 160 : 0); const width = 150 + pow(t, 0.72) * (1380 + sin(row * 0.17) * 46); const left = center - width * 0.5; const brickH = 5.5 + t * 3.4; const brickW = 15 + t * 12 + (row % 5) * 0.7; const count = floor(width / brickW); const offset = row % 3 === 1 ? brickW * 0.48 : row % 3 === 2 ? brickW * 0.19 : 0; for (let col = 0; col < count; col++) { const local = col / count; // Courtyards are cut back later, but these apertures also interrupt // the masonry field while preserving the high primitive count. if ((row > 38 && row < 49 && local > 0.38 && local < 0.60) || (row > 78 && row < 90 && local > 0.18 && local < 0.36) || (row > 108 && row < 119 && local > 0.66 && local < 0.82)) continue; const x = left + col * brickW - offset + sin(col * 0.43 + row) * 1.6; const jitter = (noise(row * 0.17, col * 0.11) - 0.5) * 2.8; const shade = 48 + floor(noise(row * 0.09 + col, 8) * 72); const litFace = local < 0.56 && (row % 9 !== 0); const mortar = row % 13 === 0 ? [222, 151, 99] : row % 7 === 0 ? [111, 158, 167] : litFace ? [shade + 28, 72 + shade * 0.42, 60 + shade * 0.34] : [shade * 0.62, 68 + shade * 0.28, 82 + shade * 0.48]; fillRgb(mortar, 82 + (col % 4) * 8); quad(x + 1, y + jitter, x + brickW - 1, y + jitter + 0.4, x + brickW - 2, y + brickH, x + 2, y + brickH + jitter); lineRgb(col % 17 === 0 ? AMBER : litFace ? [220, 138, 100] : [121, 157, 174], 28 + (row % 4) * 6, 0.46); line(x + 3, y + 1.1, x + brickW * 0.30, y + brickH - 0.7); line(x + brickW * 0.34, y + brickH - 0.4, x + brickW * 0.54, y + 1.0); line(x + brickW * 0.60, y + 0.9, x + brickW - 3, y + brickH - 1.0); line(x + 2, y + brickH * 0.53, x + brickW * 0.25, y + brickH * 0.45); line(x + brickW * 0.72, y + brickH * 0.48, x + brickW - 2, y + brickH * 0.58); lineRgb([238, 170, 113], 12 + (row % 5) * 3, 0.3); line(x + brickW * 0.22, y + 0.7, x + brickW * 0.42, y + 0.6); } } } function drawTerraces() { drawingContext.globalCompositeOperation = "screen"; const levels = [362, 492, 628, 770, 910]; for (let i = 0; i < levels.length; i++) { const y = levels[i]; const half = 90 + i * 112; lineRgb(i % 2 ? [245, 158, 103] : [122, 219, 204], 58, 1.25); bezier(1024 - half, y, 1024 - half * 0.42, y + 34, 1024 + half * 0.42, y + 34, 1024 + half, y); lineRgb([242, 205, 135], 24, 0.7); bezier(1024 - half, y + 7, 1024 - half * 0.4, y + 42, 1024 + half * 0.4, y + 42, 1024 + half, y + 7); } drawingContext.globalCompositeOperation = "source-over"; } function drawProcessionalHumanity() { // The processions are varied in scale and direction; each carries a glyph // rather than a generic repeated dot, suggesting languages still forming. for (let band = 0; band < 12; band++) { const y = 800 + band * 24; const count = 90; for (let i = 0; i < count; i++) { if ((band % 3 === 1 && i % 2 === 1) || (band % 4 === 3 && i % 5 === 0)) continue; const depth = band / 11; const cluster = floor(i / 15) % 6; const x = 180 + ((i * 113 + band * 71 + cluster * 37) % 1690) + sin(i * 0.31 + band) * (7 + depth * 16); const localY = y + sin(i * 0.23) * (5 + depth * 12); const scale = 0.42 + depth * 0.64 + (i % 6) * 0.14; const c = i % 13 === 0 ? AMBER : i % 7 === 0 ? ROSE : [151, 194, 194]; lineRgb(c, 72, 0.65 + scale * 0.32); line(x, localY, x + sin(i * 0.8) * 4 * scale, localY - 11 * scale); line(x, localY - 10 * scale, x - 4 * scale, localY - 15 * scale); line(x, localY - 10 * scale, x + 5 * scale, localY - 14 * scale); line(x, localY, x - 5 * scale, localY + 7 * scale); line(x, localY, x + 5 * scale, localY + 7 * scale); lineRgb(i % 5 === 0 ? [245, 206, 125] : c, 42, 0.42); line(x - 7 * scale, localY - 20 * scale, x + 8 * scale, localY - 20 * scale); } } } function drawLanguageGlyphs() { for (let band = 0; band < 8; band++) { const baseY = 302 + band * 78; for (let i = 0; i < 300; i++) { const x = 320 + ((i * 47 + band * 89) % 1410); const y = baseY + sin((x - 1024) * 0.006 + band * 0.8) * (24 + band * 3); const s = 4 + (i % 5) * 1.8; const c = i % 11 === 0 ? AMBER : i % 3 === 0 ? [92, 198, 190] : [174, 107, 113]; const peripheral = band < 3 && (x < 540 || x > 1500); lineRgb(c, peripheral ? 9 + (i % 3) * 2 : 42 + (i % 5) * 7, peripheral ? 0.34 : 0.48 + (i % 3) * 0.16); if (i % 3 === 0) { arc(x, y, s * 3, s * 2, 0.2 + (i % 5) * 0.5, 2.5 + (i % 4) * 0.2); } else if (i % 3 === 1) { arc(x, y, s * 2.6, s * 2.6, 3.6, 5.1); line(x - s * 1.2, y + s * 0.8, x - s * 0.1, y - s * 0.8); line(x + s * 0.2, y - s * 0.7, x + s * 1.25, y + s * 0.9); } else { line(x - s * 1.4, y, x + s * 1.4, y); line(x, y - s * 1.4, x, y + s * 1.4); } } } } function drawCantileversAndGaps() { // Deep courtyards and projecting rooms break the tower into an inhabited // vertical city. These dark cuts are compositional voids, not decoration. fillRgb([11, 25, 47], 224); noStroke(); beginShape(); vertex(846, 426); vertex(1034, 398); vertex(1078, 548); vertex(900, 582); vertex(846, 526); endShape(CLOSE); beginShape(); vertex(1038, 654); vertex(1236, 624); vertex(1280, 752); vertex(1096, 784); vertex(1038, 738); endShape(CLOSE); beginShape(); vertex(1268, 824); vertex(1460, 800); vertex(1518, 912); vertex(1348, 954); vertex(1280, 914); endShape(CLOSE); // Recessed interiors: a veil of indigo, not a flat hole. Each court has a // different depth and a small warm threshold where lives can be found. haze(954, 492, 170, 118, [28, 70, 91], 18, 9); haze(1158, 702, 180, 118, [25, 64, 100], 18, 9); haze(1398, 876, 175, 112, [37, 55, 92], 18, 9); drawingContext.globalCompositeOperation = "screen"; lineRgb([226, 149, 92], 84, 1.45); line(798, 566, 974, 532); line(1082, 762, 1276, 722); line(1320, 938, 1516, 890); lineRgb([83, 208, 192], 58, 0.8); bezier(792, 574, 850, 596, 926, 594, 980, 554); bezier(1074, 768, 1140, 792, 1214, 780, 1284, 738); bezier(1314, 946, 1380, 972, 1460, 950, 1522, 902); lineRgb([229, 174, 102], 92, 0.72); line(884, 520, 1014, 498); line(1100, 730, 1234, 704); line(1340, 904, 1480, 866); lineRgb([112, 219, 199], 58, 0.52); line(922, 526, 956, 474); line(956, 474, 1004, 496); line(1138, 738, 1170, 682); line(1170, 682, 1218, 710); line(1372, 910, 1408, 856); line(1408, 856, 1454, 882); fillRgb([241, 170, 97], 108); ellipse(950, 500, 11, 15); ellipse(1170, 706, 10, 14); ellipse(1408, 880, 9, 13); drawingContext.globalCompositeOperation = "source-over"; } function drawHumanScaleScenes() { // Seven legible vignettes: well, arguing masons, scholar, musician, // lovers, children, and a kiln worker. They stay small but narratively // distinct, so the close view rewards attention. const scenes = [ [520, 962, AMBER], [688, 900, ROSE], [908, 534, [246, 211, 133]], [1138, 708, [241, 182, 104]], [1378, 866, [247, 210, 133]], [1540, 930, [222, 116, 150]], [1190, 450, [246, 196, 111]] ]; for (let i = 0; i < scenes.length; i++) { const [x, y, c] = scenes[i]; const s = 1.8 + (i % 3) * 0.7; drawingContext.globalCompositeOperation = "screen"; haze(x, y - 12, 42 + i * 3, 34 + i * 2, c, 8, 5); drawingContext.globalCompositeOperation = "source-over"; lineRgb(c, 160, 0.9); ellipse(x, y - 17 * s, 5 * s, 5 * s); line(x, y - 14 * s, x, y + 8 * s); line(x, y - 7 * s, x - 8 * s, y - 1 * s); line(x, y - 7 * s, x + 10 * s, y - 2 * s); line(x, y + 8 * s, x - 7 * s, y + 19 * s); line(x, y + 8 * s, x + 8 * s, y + 18 * s); if (i === 0 || i === 3) { line(x - 24, y + 18, x + 24, y + 18); ellipse(x + 26, y + 12, 10, 14); } else if (i === 1) { line(x - 20, y + 6, x + 16, y - 2); line(x - 4, y - 2, x + 5, y - 18); } else if (i === 2 || i === 6) { line(x + 5, y - 2, x + 24, y - 12); line(x + 24, y - 12, x + 26, y + 14); } else { line(x - 18, y + 3, x + 18, y + 3); line(x - 14, y + 3, x - 10, y - 12); line(x + 10, y + 3, x + 14, y - 12); } } } function drawSummitFissureAndSpill() { // The summit is unfinished: a split crown and a suspended upper courtyard // give the vertical journey a destination more specific than a flat peak. fillRgb([28, 48, 70], 222); noStroke(); beginShape(); vertex(946, 182); vertex(1012, 126); vertex(1050, 174); vertex(1028, 274); vertex(986, 298); vertex(954, 244); endShape(CLOSE); beginShape(); vertex(1094, 194); vertex(1148, 270); vertex(1112, 334); vertex(1064, 276); vertex(1076, 220); endShape(CLOSE); haze(1010, 220, 120, 190, [48, 126, 144], 11, 10); haze(1098, 250, 110, 170, [120, 72, 128], 8, 9); drawingContext.globalCompositeOperation = "screen"; lineRgb([135, 246, 214], 172, 1.55); bezier(1010, 128, 986, 180, 1002, 222, 986, 294); lineRgb([242, 180, 95], 126, 1.2); bezier(1068, 274, 1124, 246, 1176, 282, 1222, 326); lineRgb([221, 91, 145], 84, 0.8); bezier(1098, 300, 1150, 346, 1192, 370, 1258, 382); drawingContext.globalCompositeOperation = "source-over"; // Secondary cranes/spires in the side margins provide depth and world. for (let i = 0; i < 12; i++) { const x = 80 + i * 172; const y = 694 - noise(i * 0.8) * 130; lineRgb(i % 3 === 0 ? [219, 149, 95] : [72, 139, 164], 58, 0.72); line(x, y, x + 18, y - 120 - (i % 4) * 18); line(x + 18, y - 120 - (i % 4) * 18, x + 92, y - 116 - (i % 3) * 9); line(x + 18, y - 120 - (i % 4) * 18, x - 34, y - 90); } } function drawForegroundDepth() { drawingContext.globalCompositeOperation = "multiply"; fillRgb([5, 17, 40], 44); ellipse(1018, 1004, 1500, 190); fillRgb([9, 27, 57], 28); ellipse(1020, 972, 1260, 120); drawingContext.globalCompositeOperation = "screen"; haze(520, 1088, 920, 180, [43, 91, 102], 7, 12); haze(1550, 1094, 840, 160, [64, 61, 103], 6, 10); lineRgb([191, 123, 84], 52, 1.05); bezier(0, 1038, 280, 970, 540, 1010, 780, 1062); bezier(1120, 1058, 1430, 980, 1740, 1014, 2048, 1050); for (let i = 0; i < 180; i++) { const x = (i * 83) % W; const y = 1030 + ((i * 29) % 116); const s = 3 + (i % 7) * 1.4; lineRgb(i % 9 === 0 ? [244, 175, 98] : [80, 138, 150], 38 + (i % 4) * 8, 0.55); line(x, y, x + s * 4, y - s * (1 + i % 3)); line(x + s * 4, y - s * (1 + i % 3), x + s * 7, y + s * 0.4); } drawingContext.globalCompositeOperation = "source-over"; } function drawLanternWindows() { // A few hundred inhabited apertures interrupt the masonry without turning // the tower into a flat grid. Their irregular spacing implies people, // workshops, and translation happening inside the structure. drawingContext.globalCompositeOperation = "screen"; for (let band = 0; band < 14; band++) { const y = 520 + band * 31; const t = (y - 214) / (1028 - 214); const center = 1024 - 86 * (1 - t) + sin(t * 5.2) * 32 + (t > 0.72 ? (t - 0.72) * 160 : 0); const spread = (150 + pow(t, 0.72) * (1380 + sin(band * 0.17) * 46)) * 0.38; const count = 18 + band * 2; for (let i = 0; i < count; i++) { const x = center - spread + ((i * 79 + band * 41) % (spread * 2)); const s = 2.6 + (i % 4) * 0.9; const c = i % 17 === 0 ? [255, 220, 138] : i % 5 === 0 ? [237, 116, 154] : [224, 163, 97]; haze(x, y + sin(i * 0.6) * 3, s * 5, s * 4, c, i % 17 === 0 ? 12 : 5, 4); fillRgb(c, i % 17 === 0 ? 175 : 92); ellipse(x, y, s * 1.6, s * 2.4); lineRgb([249, 214, 143], 90, 0.38); line(x - s * 0.7, y, x + s * 0.7, y); line(x, y - s, x, y + s); } } drawingContext.globalCompositeOperation = "source-over"; } function drawSkyLight() { drawingContext.globalCompositeOperation = "screen"; haze(1024, 116, 620, 210, [72, 229, 210], 9, 16); haze(1180, 230, 300, 260, [229, 89, 142], 7, 12); lineRgb([134, 239, 208], 70, 1.2); bezier(40, 198, 340, 70, 710, 174, 1018, 320); lineRgb([245, 182, 98], 74, 1.0); bezier(1018, 320, 1070, 500, 1036, 704, 1024, 850); lineRgb([234, 103, 145], 52, 0.8); bezier(1024, 850, 940, 914, 780, 955, 602, 1002); drawingContext.globalCompositeOperation = "source-over"; brush.set("pen", "#f8d996", 1.7); brush.line(1013, 844, 1024, 827); brush.line(1024, 827, 1037, 845); } function draw() { translate(-width / 2, -height / 2); drawNightSky(); drawWeather(); drawDistantGround(); drawTowerSilhouette(); drawMasonry(); drawTerraces(); drawCantileversAndGaps(); drawSummitFissureAndSpill(); drawProcessionalHumanity(); drawLanguageGlyphs(); drawLanternWindows(); drawHumanScaleScenes(); drawForegroundDepth(); drawSkyLight(); }