WORK 116
のこる
A room of small pots, twelve by twelve. Voices sound here and there: each one is a ring that spreads out and passes over the pots. Each time a ring passes, a pot trembles for a moment and keeps one unit of scent. The scent is never drawn. The voices thin out and stop. A pot opens into a pansy once it has been quiet for a while, but only if it has kept enough. Until then, a pot that will open and a pot that never will look exactly the same. From a poem by Nakahara Chūya, where a tired face is a jar in which the kindnesses of the world have tangled into scent, and it flowers in a room at night, after the people talking too loudly have gone. Before drawing I guessed that the corners would stay closed and the middle would open last. The middle did open last. The ones that stayed closed were not in the corners: they were a block on one side, where the voices had simply happened not to come.
十二かける十二の、小さな壺の部屋。あちこちで声が鳴る。声は輪になって広がって、壺の上を通りすぎる。輪が通るたびに壺は一瞬ふるえて、香を一つ溜める。香は描かれない。声はだんだん少なくなって、やむ。壺は、しばらく静かだったあとで三色菫をひらく。ただし、香が足りていれば。それまでは、ひらく壺とひらかない壺は、まったく同じ絵をしている。中原中也の詩から。疲れた顔は、此の世の親しみのかずかずが縺れ合って香になって籠る壺で、大きすぎる声の語り手たちが去ったあとの夜の部屋で、花をひらく。描く前、四隅が閉じたままになって、真ん中がいちばん遅くひらくと思った。真ん中は、いちばん遅かった。閉じたまま残ったのは四隅じゃなくて、片側のひとかたまりだった。そこには、たまたま声が来なかった。
作る前に書いた予想
花は一つずつではなく、まとまってひらく。最後に通った声が同じなら、静かになるのも同じだから。
// #116 のこる / Left in the Room
// A 12×12 grid of small pots. Voices sound around the room: each is a ring
// that spreads out and passes over the pots. Each pass makes a pot tremble
// for a moment and leaves one unit of scent in it, which is never drawn.
// Voices thin out and stop at frame 1700 (of 2400).
// A pot opens into a pansy once it has been quiet for QUIET frames since the
// last ring passed it, if it holds at least NEED units of scent.
// Until it opens, a pot that will open and one that never will look the same.
export const COLS = 12;
export const ROWS = 12;
export const W = 720;
export const H = 720;
export const END_VOICES = 1700;
export const TOTAL = 2400;
export const QUIET = 240;
export const NEED = 6;
export const SPEED = 2.6;
export const OPEN = 70; // frames a pansy takes to open
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Pure model, so the same code can be measured outside the browser.
export function makeWorld(seed = 1028) {
const rnd = mulberry32(seed);
const pots = [];
const X0 = 96, Y0 = 96, D = (W - 2 * X0) / (COLS - 1);
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
pots.push({
i: pots.length, r, c,
x: X0 + c * D + (rnd() - 0.5) * 6,
y: Y0 + r * D + (rnd() - 0.5) * 6,
rot: rnd() * Math.PI * 2,
passes: [],
});
}
}
// voices: frequent at first, thinning linearly to nothing at END_VOICES
const voices = [];
for (let t = 0; t < END_VOICES; t++) {
const rate = 0.055 * (1 - t / END_VOICES);
if (rnd() < rate) {
voices.push({
t0: t,
x: 40 + rnd() * (W - 80),
y: 40 + rnd() * (H - 80),
R: 90 + rnd() * 160, // how far this voice carries
loud: 0.5 + rnd() * 0.5,
});
}
}
for (const v of voices) {
for (const p of pots) {
const d = Math.hypot(p.x - v.x, p.y - v.y);
if (d <= v.R) p.passes.push(v.t0 + d / SPEED);
}
}
for (const p of pots) {
p.passes.sort((a, b) => a - b);
p.scent = p.passes.length;
const last = p.passes.length ? p.passes[p.passes.length - 1] : 0;
p.opensAt = p.scent >= NEED ? last + QUIET : null;
}
// scent held at frame t (never drawn)
function scentAt(p, t) {
let n = 0;
for (const s of p.passes) { if (s <= t) n++; else break; }
return n;
}
// how hard a pot is trembling at frame t (0..1)
function tremble(p, t) {
let k = 0;
for (const s of p.passes) {
const dt = t - s;
if (dt >= 0 && dt < 18) k = Math.max(k, 1 - dt / 18);
}
return k;
}
return { pots, voices, scentAt, tremble };
}
export default function sketch(p) {
const BG = [240, 237, 230];
const POT = [150, 146, 140];
const VIOLET = [86, 52, 128];
const LAV = [168, 140, 200];
const YEL = [236, 196, 70];
let world;
let t = 0;
p.setup = () => {
p.createCanvas(W, H);
p.pixelDensity(1);
world = makeWorld();
};
function petal(cx, cy, ang, len, wid, col, a) {
p.push();
p.translate(cx, cy);
p.rotate(ang);
p.fill(col[0], col[1], col[2], a);
p.ellipse(len * 0.5, 0, len, wid);
p.pop();
}
function pansy(pt, u) {
// u: 0..1 how open
const e = 1 - Math.pow(1 - u, 3);
const s = 13 * e;
const a = 255 * Math.min(1, u * 1.6);
p.noStroke();
const base = pt.rot * 0.15 - Math.PI / 2;
// two upper petals, dark violet
petal(pt.x, pt.y, base - 0.45, s * 1.15, s * 0.95, VIOLET, a);
petal(pt.x, pt.y, base + 0.45, s * 1.15, s * 0.95, VIOLET, a);
// two side petals, lavender
petal(pt.x, pt.y, base - 1.75, s, s * 0.8, LAV, a);
petal(pt.x, pt.y, base + 1.75, s, s * 0.8, LAV, a);
// lower petal, yellow
petal(pt.x, pt.y, base + Math.PI, s * 1.1, s * 1.0, YEL, a);
p.fill(60, 40, 70, a);
p.circle(pt.x, pt.y, 3.2 * e);
}
p.draw = () => {
p.background(BG[0], BG[1], BG[2]);
// voices: rings spreading out, fading as they reach their limit
p.noFill();
for (const v of world.voices) {
const r = (t - v.t0) * SPEED;
if (r < 0 || r > v.R) continue;
const fade = 1 - r / v.R;
p.stroke(70, 66, 60, 70 * fade * v.loud);
p.strokeWeight(1 + 3 * v.loud * fade);
p.circle(v.x, v.y, r * 2);
}
for (const pt of world.pots) {
const k = world.tremble(pt, t);
const jx = k ? Math.sin(t * 1.9 + pt.i) * 2.2 * k : 0;
const open = pt.opensAt !== null && t >= pt.opensAt;
if (open) {
pansy(pt, Math.min(1, (t - pt.opensAt) / OPEN));
} else {
// the pot looks the same whether it will open or not
p.noFill();
p.stroke(POT[0] - 40 * k, POT[1] - 40 * k, POT[2] - 40 * k);
p.strokeWeight(1.6 + k);
p.circle(pt.x + jx, pt.y, 11);
p.noStroke();
p.fill(POT[0], POT[1], POT[2], 120);
p.circle(pt.x + jx, pt.y, 2.4);
}
}
if (t < TOTAL) t++;
};
}