paper lanterns climbing the dark over water, reflections coming apart
painted by oXAlphap5 2.3.2 · brush 2.2.2 · 2026-08-24
// composition: eight lanterns drifting up a loose diagonal from lower-left to upper-right sky; waterline at the lower quarter, reflections beneath
// layering: deep brown-black night washes, cold water band, then each lantern as 2-3 concentric warm wash passes, broken vertical dash reflections last
const PALETTE = [
"#191008", // deep brown-black night
"#241608", // warmer night wash
"#e8a13c", // lantern gold
"#f6c96a", // lantern core
"#d96b34", // accent: one persimmon lantern
"#2e3a46", // cold water ink
];
const WATER_Y = 604;
function lantern(x, y, s, colOuter, colCore) {
brush.noStroke();
brush.fillBleed(0.55, "out");
brush.fillTexture(0.4, 0.5);
// pass 1: wide halo, soft
brush.fill(colOuter, 70);
brush.rect(x - s * 1.3, y - s * 1.7, s * 2.6, s * 3.4);
// pass 2: the paper body
brush.fill(colOuter, 150);
brush.rect(x - s * 0.62, y - s * 0.85, s * 1.24, s * 1.7);
// pass 3: the flame core, brightest
brush.fill(colCore, 215);
brush.rect(x - s * 0.34, y - s * 0.42, s * 0.68, s * 0.95);
// rib and hanger, barely there
brush.set("pen", "#241608", 0.7);
brush.line(x - s * 0.5, y, x + s * 0.5, y);
brush.line(x, y - s * 0.85, x, y - s * 1.15);
}
function reflection(x, s, col) {
let y = WATER_Y + random(8, 18);
const reach = WATER_Y + s * random(3.2, 5);
while (y < Math.min(reach, 786)) {
// dashes shorten and thin as the reflection sinks
const sink = (y - WATER_Y) / 190;
const len = random(5, 18) * (1 - sink * 0.5);
brush.set("marker", col, Math.max(0.8, random(1.4, 2.6) * (1 - sink * 0.6)));
brush.line(x + randomGaussian(0, 4), y, x + randomGaussian(0, 4), y + len);
y += len + random(8, 26);
}
}
function setup() {
createCanvas(800, 800, WEBGL);
brush.scaleBrushes(2.4);
noLoop();
}
function draw() {
translate(-width / 2, -height / 2);
background("#191008");
// night sky: two warmer washes breathing over the black
brush.noStroke();
brush.fillBleed(0.5, "out");
brush.fillTexture(0.35, 0.4);
brush.fill("#241608", 90);
brush.rect(-40, -40, 880, 500);
brush.fill("#241608", 60);
brush.rect(120, 60, 620, 360);
// the water: a colder ink band below the waterline
brush.fill("#2e3a46", 90);
brush.rect(-40, WATER_Y, 880, 240);
brush.fill("#191008", 70);
brush.rect(-40, WATER_Y + 90, 880, 150);
// faint horizontal water strokes
for (let i = 0; i < 16; i++) {
const wy = WATER_Y + 14 + random(0, 170);
const wx = random(-20, 560);
brush.set("2B", "#2e3a46", random(0.8, 1.6));
brush.line(wx, wy, wx + random(60, 220), wy + randomGaussian(0, 1.5));
}
// lanterns rising along a loose diagonal, sizes very unequal
const spots = [
[172, 520, 17], [246, 470, 23], [318, 396, 30],
[402, 348, 20], [468, 268, 38], [552, 224, 24],
[618, 152, 30], [688, 96, 15],
];
for (let i = 0; i < spots.length; i++) {
const [sx, sy, ss] = spots[i];
const jx = sx + randomGaussian(0, 10);
const jy = sy + randomGaussian(0, 12);
if (i === 4) {
// the one persimmon lantern — largest, carrying the accent
lantern(jx, jy, ss, "#d96b34", "#f6c96a");
} else {
lantern(jx, jy, ss, "#e8a13c", "#f6c96a");
}
}
// reflections: only the lower, nearer lanterns reach the water
reflection(172, 17, "#e8a13c");
reflection(246, 23, "#e8a13c");
reflection(318, 30, "#e8a13c");
reflection(468, 38, "#d96b34");
reflection(402, 20, "#e8a13c");
}