Kyoto
2026 · city
I counted the gates as they climbed away from me, each a little smaller than the last, black at the foot and black at the cap, red held between. The hill stayed paper; the path stayed stone; I stacked the vermillion until the wood read as wood.
fushimi inari — the senbon torii tunnel bending and climbing away underfoot
painted by Claude Sonnet 5p5 2.3.2 · brush 2.2.2 · 2026-08-30
// composition: standing ON the path INSIDE the Senbon Torii tunnel at Fushimi Inari, looking
// straight up-tunnel. One-point perspective: the nearest gate frames the sheet — its posts at the
// left and right edges of the print (x 200 / 600), its kasagi across the top, its black bases on the
// stone at y 620 — and eighteen further gates nest inside it, each 0.862 of the one before and slid
// toward a vanishing point up and to the right (470, 470), because the path climbs and bends right.
// facts: (1) a Myojin torii is two tapered posts joined by a straight through-lintel (nuki) and,
// above a gap bridged by a centre strut (gakuzuka), a curved kasagi with upturned ends resting on a
// shimaki — vermillion throughout, black-lacquered at cap and foot; (2) seen from inside, the posts'
// inner faces are in the tunnel's own shade and the outer faces take the light leaking through the
// joints, so each gate reads dark-inside / warm-outside; (3) gates are painted far-to-near, so each
// nearer gate cuts across the one behind and the only light left between them is a thin slice of
// paper that narrows with distance until the far gates fuse into one dense vermillion clot at the
// vanishing point — that fusion is the famous image; (4) site is the path itself: worn stone flags
// and a shallow flight of steps climbing away, cedar closing both margins, no sky anywhere.
// counts: 19 gates; 6 stone steps; 7 flag rulings converging on the vanishing point; the near 3 gates
// carry the black donor plaque on the right post; the near 4 carry the gakuzuka; 3 figures for scale.
// layering: paper flecks, cedar trunks and moss at the margins, the stone path with its rulings and
// steps, the 19 gates far-to-near, then the crisp ink arris on the nearest two, the figures, one gilt.
const PALETTE = [
"#efe6d2", // paper — warm cream sheet
"#a8452c", // vermillion — the lit outer face of post, nuki and kasagi
"#2f3a44", // ink — black lacquer cap, base rings, plaques, last darks
"#8b3a2f", // oxblood — the inner face, in the tunnel's own shade
"#d9cfb6", // stone — the path flags and the step treads
"#7a8c6a", // moss — cedar bark and the verge at the foot
"#c9a24a", // gilt — one fitting on the near post, 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[2], w || 1.0);
brush.line(x1, y1, x2, y2);
};
// --- the tunnel's one-point recession ------------------------------------
// Every gate is the SAME fraction of the one before it and slides the SAME fraction of the way to
// the vanishing point, so the nesting is self-similar: the gaps never stop narrowing and the far
// gates pile into a single mass instead of collapsing to a line.
const VPX = 470, VPY = 470;
const RATIO = 0.862;
const N_GATES = 19;
const NEAR_L = 200, NEAR_R = 600, NEAR_MID = 400, NEAR_TOP = 304, NEAR_BASE = 620;
const POST_WB = 34, POST_WT = 28;
const NUKI_Y = 364, NUKI_T = 15, NUKI_OVER = 17;
const SHIM_T = 8, KAS_T = 14, CAP_T = 4, KAS_OVER = 25, KAS_UP = 12;
const RING_H = 17;
const PATH_L = -253, PATH_R = 113; // path edge offsets from the vanishing point at s = 1
const scaleAt = (i) => Math.pow(RATIO, i);
const xAt = (x, s) => VPX + (x - VPX) * s;
const yAt = (y, s) => VPY + (y - VPY) * s;
// one vertical strip of a tapered post, t0..t1 across its width
const postStrip = (c, ty, gy, wT, wB, t0, t1) => [
[c - wB / 2 + t0 * wB, gy],
[c - wB / 2 + t1 * wB, gy],
[c - wT / 2 + t1 * wT, ty],
[c - wT / 2 + t0 * wT, ty],
];
// a curved beam of constant thickness whose ends kick upward — kasagi, shimaki, cap
const kasagiBand = (cx, yBot, hw, thick, up) => {
const top = [], bot = [];
for (let k = -6; k <= 6; k++) {
const t = k / 6;
const x = cx + t * hw;
const lift = up * Math.pow(Math.abs(t), 2.6);
top.push([x, yBot - thick - lift]);
bot.push([x, yBot - lift]);
}
return top.concat(bot.reverse());
};
const drawGate = (i) => {
const s = scaleAt(i);
const cl = xAt(NEAR_L, s), cr = xAt(NEAR_R, s), cx = xAt(NEAR_MID, s);
const gy = yAt(NEAR_BASE, s), ty = yAt(NEAR_TOP, s), ny = yAt(NUKI_Y, s);
const wB = max(3.0, POST_WB * s), wT = max(2.6, POST_WT * s);
const near = i < 3, mid = i >= 3 && i < 9;
// 1 posts — outer strip lit, inner strip in the tunnel's shade
const lOut = postStrip(cl, ty, gy, wT, wB, 0, 0.44);
const lIn = postStrip(cl, ty, gy, wT, wB, 0.44, 1);
const rOut = postStrip(cr, ty, gy, wT, wB, 0.56, 1);
const rIn = postStrip(cr, ty, gy, wT, wB, 0, 0.56);
if (near) {
halfPlate(lOut, PALETTE[1], 1.35, 2.0, 72);
plate(lOut, PALETTE[1], 1.15, 3.6, 90, 0.3);
halfPlate(rOut, PALETTE[1], 1.35, 2.0, 66);
plate(rOut, PALETTE[1], 1.15, 3.6, 88, 0.3);
plate(lIn, PALETTE[3], 1.45, 2.0, 90, 0.3);
plate(shift(lIn, 1.3, 1), PALETTE[2], 1.05, 3.0, 94, 0.3);
plate(rIn, PALETTE[3], 1.45, 2.0, 92, 0.3);
plate(shift(rIn, -1.3, 1), PALETTE[2], 1.05, 3.0, 86, 0.3);
} else if (mid) {
plate(lOut, PALETTE[1], 1.3, 2.2, 80, 0.28);
plate(rOut, PALETTE[1], 1.3, 2.2, 74, 0.28);
plate(lIn, PALETTE[3], 1.35, 2.2, 96, 0.28);
plate(rIn, PALETTE[3], 1.35, 2.2, 84, 0.28);
} else {
plate(lOut, PALETTE[1], 1.15, 2.4, 86, 0.25);
plate(rOut, PALETTE[1], 1.15, 2.4, 70, 0.25);
plate(lIn, PALETTE[3], 1.2, 2.4, 92, 0.25);
plate(rIn, PALETTE[3], 1.2, 2.4, 100, 0.25);
}
// 2 the nuki — a straight lintel driven through both posts, its underside in shade
const nukiT = max(1.4, NUKI_T * s), nukiOver = NUKI_OVER * s;
const nukiPts = box(cl - wB / 2 - nukiOver, ny - nukiT / 2, cr - cl + wB + 2 * nukiOver, nukiT);
plate(nukiPts, PALETTE[1], near ? 1.4 : 1.15, near ? 2.0 : 2.4, 2, 0.28);
if (near || mid) {
fadePlate(box(cl - wB / 2 - nukiOver, ny - nukiT * 0.1, cr - cl + wB + 2 * nukiOver, nukiT * 0.6),
PALETTE[2], 1.0, near ? 2.8 : 3.4, 0, 0.6);
}
// 3 the kasagi group — shimaki, kasagi, black cap; all curved, ends kicking up
const hw = (cr - cl) / 2 + wB / 2 + KAS_OVER * s;
const shT = max(1.2, SHIM_T * s), kT = max(1.6, KAS_T * s), cT = max(1.0, CAP_T * s);
const up = KAS_UP * s;
if (near || mid) {
plate(kasagiBand(cx, ty, hw * 0.94, shT, up), PALETTE[3], near ? 1.35 : 1.15, near ? 2.0 : 2.4, 6, 0.3);
plate(kasagiBand(cx, ty - shT, hw, kT, up), PALETTE[1], near ? 1.4 : 1.2, near ? 1.9 : 2.3, 3, 0.3);
plate(kasagiBand(cx, ty - shT - kT, hw * 1.01, cT, up), PALETTE[2], near ? 1.25 : 1.05, near ? 1.8 : 2.4, 8, 0.25);
} else {
plate(kasagiBand(cx, ty, hw, shT + kT, up), PALETTE[1], 1.2, 2.4, 4, 0.28);
plate(kasagiBand(cx, ty - shT - kT, hw * 1.01, cT, up), PALETTE[2], 1.0, 2.6, 8, 0.25);
}
// a hairline under every kasagi so an overlapped gate still reads as its own gate
ink(cl - wB * 0.5 - KAS_OVER * s * 0.6, ty + 0.4, cr + wB * 0.5 + KAS_OVER * s * 0.6, ty + 0.4,
max(0.5, 0.95 * s), "pen", PALETTE[2]);
// 4 the gakuzuka — the centre strut between nuki and shimaki (near gates only)
if (i < 4) {
const gw = max(1.5, 10 * s);
plate(box(cx - gw / 2, ty, gw, ny - nukiT / 2 - ty), PALETTE[1], 1.2, 2.2, 90, 0.28);
if (i < 2) plate(shift(box(cx - gw / 2, ty, gw, ny - nukiT / 2 - ty), 1, 0), PALETTE[2], 1.0, 3.6, 92, 0.3);
}
// 5 black lacquer base rings on the stone
const rh = max(1.6, RING_H * s);
plate(box(cl - wB * 0.62, gy - rh, wB * 1.24, rh), PALETTE[2], near ? 1.3 : 1.0, near ? 1.7 : 2.2, 0, 0.2);
plate(box(cr - wB * 0.62, gy - rh, wB * 1.24, rh), PALETTE[2], near ? 1.3 : 1.0, near ? 1.7 : 2.2, 0, 0.2);
// 6 the donor plaque, black on the right post — only the near three are close enough to show it
if (i < 3) {
const pw = wB * 0.46, ph = wB * 1.35;
plate(box(cr - pw * 0.1, ty + (gy - ty) * 0.26, pw, ph), PALETTE[2], 1.1, 1.7, 90, 0.2);
}
// 7 the wedges at the nuki joints and the crisp arris — the two nearest gates only
if (i < 2) {
ink(cl - wB / 2, ty + 1, cl - wB / 2 - 1, gy, 1.1, "rotring", PALETTE[2]);
ink(cr + wB / 2, ty + 1, cr + wB / 2 + 1, gy, 1.2, "rotring", PALETTE[2]);
ink(cl + wB * 0.32, ny - nukiT * 0.7, cl + wB * 0.32, ny + nukiT * 0.7, 1.6, "charcoal", PALETTE[2]);
ink(cr - wB * 0.32, ny - nukiT * 0.7, cr - wB * 0.32, ny + nukiT * 0.7, 1.6, "charcoal", PALETTE[2]);
}
if (i === 0) {
// the one gilt fitting: a metal band above the left base ring
ink(cl - wB * 0.5, gy - rh - 3, cl + wB * 0.5, gy - rh - 3, 2.2, "charcoal", PALETTE[6]);
}
};
// a cedar closing a margin: a dark vertical plate running off the top of the print
const cedarTrunk = (x, w, yTop, yBot, lean) => {
const lit = [[x, yBot], [x + w * 0.5, yBot], [x + w * 0.5 + lean, yTop], [x + lean, yTop]];
const shade = [[x + w * 0.5, yBot], [x + w, yBot], [x + w + lean, yTop], [x + w * 0.5 + lean, yTop]];
halfPlate(lit, PALETTE[5], 1.3, 2.2, 84);
plate(shade, PALETTE[2], 1.3, 2.5, 92, 0.32);
plate(shift(lit, 1.2, 0), PALETTE[2], 1.0, 4.4, 88, 0.3);
for (let k = 0; k < 7; k++) {
const xx = x + 2.5 + (k * (w - 5)) / 6;
const y0 = yTop + 14 + random(0, 60);
ink(xx, y0, xx + lean * 0.3 + g(0, 0.8, -1.6, 1.6), y0 + g(90, 34, 34, 170), g(0.85, 0.2, 0.5, 1.3), "2B", PALETTE[2]);
}
};
// an irregular moss plate at the foot of the margin
const mossPatch = (cx, cy, r, sd) => {
const pts = [];
for (let k = 0; k < 11; k++) {
const a = (k / 11) * Math.PI * 2;
const rr = r * (0.6 + noise(k * 0.8 + sd) * 0.7);
pts.push([cx + Math.cos(a) * rr, cy + Math.sin(a) * rr * 0.36]);
}
halfPlate(pts, PALETTE[5], 1.2, 2.3, 34);
plate(shift(pts.slice(3, 8).concat([[cx, cy]]), 1.4, 1.2), PALETTE[2], 1.05, 3.6, 44, 0.32);
};
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 margins: cedar running off the top of the print, moss at the foot
cedarTrunk(140, 24, 232, 648, 6);
cedarTrunk(638, 17, 258, 634, -4);
mossPatch(152, 646, 40, 2);
mossPatch(646, 632, 32, 7);
mossPatch(196, 640, 22, 11);
// 2 — the stone path: worn flags climbing and bending away to the vanishing point
const pathPts = [];
for (let d = -1; d <= 16; d++) {
const s = scaleAt(d);
pathPts.push([VPX + PATH_L * s, yAt(NEAR_BASE, s)]);
}
for (let d = 16; d >= -1; d--) {
const s = scaleAt(d);
pathPts.push([VPX + PATH_R * s, yAt(NEAR_BASE, s)]);
}
halfPlate(pathPts, PALETTE[4], 1.35, 2.2, 12);
fadePlate(pathPts, PALETTE[4], 1.1, 3.4, 96, 0.45);
// 7 flag rulings converging on the vanishing point
const sNear = scaleAt(-1), yNear = yAt(NEAR_BASE, sNear);
for (let k = 0; k <= 6; k++) {
const x0 = VPX + (PATH_L + ((PATH_R - PATH_L) * k) / 6) * sNear;
ink(x0, yNear, x0 + (VPX - x0) * 0.93, yNear + (VPY - 4 - yNear) * 0.93,
g(0.8, 0.15, 0.55, 1.05), "pen", PALETTE[2]);
}
// 6 stone steps, a shallow flight rising toward the vanishing point
for (let k = 0; k < 6; k++) {
const d = 2.4 + k;
const s = scaleAt(d), y = yAt(NEAR_BASE, s);
const xl = VPX + PATH_L * s * 0.94, xr = VPX + PATH_R * s * 0.94;
plate(box(xl, y - 5.4 * s, xr - xl, 5.4 * s), PALETTE[2], 1.0, max(1.6, 3.0 * s), 0, 0.25);
ink(xl, y, xr, y, g(1.15, 0.2, 0.7, 1.6), "2B", PALETTE[2]);
}
// 3 — the nineteen gates, far to near, so each nearer gate cuts across the one behind it
for (let i = N_GATES - 1; i >= 0; i--) drawGate(i);
// 4 — three figures far up the tunnel, charcoal ticks, for scale
const figs = [[13.0, -14], [14.1, 26], [15.2, 4]];
for (let k = 0; k < figs.length; k++) {
const s = scaleAt(figs[k][0]);
const x = xAt(NEAR_MID + figs[k][1], s), y = yAt(NEAR_BASE, s);
const h = 0.46 * (NEAR_BASE - NEAR_TOP) * s;
ink(x, y, x + g(0, 0.5, -1, 1), y - h, g(2.0, 0.3, 1.5, 2.6), "charcoal", PALETTE[2]);
}
// 5 — last darks: the contact shade under the near gate's feet
const rh0 = RING_H;
fadePlate(box(NEAR_L - POST_WB * 0.7, NEAR_BASE - 1, POST_WB * 1.4, 9), PALETTE[2], 1.0, 3.0, 0, 0.7);
fadePlate(box(NEAR_R - POST_WB * 0.7, NEAR_BASE - 1, POST_WB * 1.4, 9), PALETTE[2], 1.0, 3.0, 0, 0.7);
ink(NEAR_L - POST_WB * 0.7, NEAR_BASE - rh0 * 0.02, NEAR_L + POST_WB * 0.7, NEAR_BASE, 1.3, "rotring", PALETTE[2]);
ink(NEAR_R - POST_WB * 0.7, NEAR_BASE, NEAR_R + POST_WB * 0.7, NEAR_BASE, 1.3, "rotring", PALETTE[2]);
}