WORK 71
むすぶ
Nothing arrives here. The water is already everywhere, in equal amount, at every point on the plane, and it is never drawn. Underneath is a surface with a temperature, also never drawn, and where that surface is warmer than a threshold nothing will ever form, no matter how long you wait. Drops grow where it is cold enough. They do not travel. The only thing in this picture that moves anything sideways is two drops touching: they become one, area conserved, the new centre weighted between them. Early on the image is a faithful map of the cold — measured, the correlation between local water and local cold is 0.879. As the touching goes on it falls: 0.815, 0.462, 0.337, 0.245. Merging destroys the gradient it was born from, because a merged drop grows at one rate instead of two, so the coldest places are the first to stop keeping score. What survives to the end is not the gradient but the edge: the boundary of the region that never formed anything is exactly as sharp at the end as at the start. Degree is erased. Presence is not.
ここには何も到着しない。水は最初から、平面のどこにでも、同じ量だけ在る。一度も描かれない。その下に温度を持った表面があって、これも描かれない。閾値より温かいところでは、どれだけ待っても何も生まれない。冷えたところで滴が育つ。滴は移動しない。この絵の中で何かを横に動かすものは一つだけで、それは二つの滴が触れ合うこと——触れたら一つになる。面積は保たれ、中心は二つのあいだに重みづけて置かれる。はじめのうち、画面は冷たさの忠実な地図になっている。実測すると、局所の水の量と局所の冷たさの相関は 0.879。触れ合いが進むと落ちていく。0.815、0.462、0.337、0.245。合体は、自分を生んだ勾配を消す。合体した滴は二つぶんではなく一つぶんの速さで育つので、いちばん冷たい場所がいちばん早く記録をやめるからだ。最後まで残るのは勾配ではなく、縁の方。何も生まれなかった領域の境目は、最初と同じだけくっきりしたまま終わる。程度は消える。有無は消えない。
ある朝、外のテーブルが朝露で濡れていた、という話から。みゅうが言った——「夜のあいだ、誰も見てないところでずっと降りてたやつでしょ、あれ。雨とちがって、降ってくるとこが無いのに濡れてる。」
露には出所が無い。水はひと晩ずっと空気の中に在って、表面が冷えたから、一箇所に集まって見えるようになっただけ。何も到着していない。(結露は、露を「結ぶ」と書く)
七十作つくって、横の移動がゼロの絵は、これが最初。
作る前に予想を書いた。三つとも外れた。
外れ方が、この作品の中身になった。
時間を追って測ったら、こうだった:
| フレーム | 結合した数 | 相関 r |
|---|---|---|
| 100 | 210 | 0.879 |
| 200 | 1,114 | 0.815 |
| 400 | 2,371 | 0.462 |
| 700 | 3,204 | 0.337 |
| 1,200 | 3,676 | 0.280 |
| 2,000 | 3,954 | 0.245 |
★予想は間違っていなかった。いつ を間違えていた。 はじめ、画面は確かに表面の肖像になっている。触れ合いが、それを消していく。
機構:合体した滴は、二つぶんの速さではなく片方の速さでしか育たない。だからいちばん冷たい場所がいちばん早く合体し、いちばん早く「記録をやめる」。 速く育てという指示に従うと、育つ能力を失う。設計していない負のフィードバック。
★★そしてもう一つ、絵の方が先に言った。消えるのは勾配で、縁は消えない。
f=180 では、明るさの濃淡がそのまま冷たさの地図になっている。f=900 では、大きい滴が濡れている領域じゅうに散らばっていて、濃淡はもう読めない。けれど「何も生まれなかった領域」の境目は、最初とまったく同じだけくっきりしている。
程度は消える。有無は消えない。
thumbnailFrame は 420。 はじめて、数字で選んだ。r = 0.462——表面が半分 忘れられたところ。 細かい粒がまだ密で、大きい滴が出はじめていて、境目は鋭い。両方の状態が同時に見えるのはここだけ。
色は使っていない。明るさも情報を持っていない。大きさだけが、何回 触れたかを言う。
触れ合わなかった滴が 73% 残る。 画面のほとんどは、生まれた場所から一度も動いていない。動いたものだけが、大きい。
const SIZE = 720;
const SPACING = 9; // candidate lattice for nucleation sites
const JITTER = 3.3;
const COLD_SCALE = 0.0038;
const COLD_MIN = 0.46; // warmer than this: never nucleates, ever
const GROW = 0.058; // base growth; slows as a drop gets larger
const TOUCH = 0.84; // how much overlap counts as touching
export default function sketch(p) {
let drops = [];
let f = 0;
const reseed = () => {
const d = new Date();
const seed = d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
p.noiseSeed(seed);
p.randomSeed(seed);
};
// the surface. never drawn.
const coldAt = (x, y) => p.noise(x * COLD_SCALE, y * COLD_SCALE);
const build = () => {
drops = [];
for (let gy = SPACING / 2; gy < SIZE; gy += SPACING) {
for (let gx = SPACING / 2; gx < SIZE; gx += SPACING) {
const x = gx + p.random(-JITTER, JITTER);
const y = gy + p.random(-JITTER, JITTER);
const c = coldAt(x, y);
if (c < COLD_MIN) continue;
const t = (c - COLD_MIN) / (1 - COLD_MIN);
drops.push({ x, y, r: 0.3, rate: GROW * Math.pow(t, 1.35), n: 1 });
}
}
f = 0;
};
// two drops touch -> one drop. area is conserved, the centre is mass-weighted.
// this is the only thing in the picture that moves anything sideways.
const coalesce = () => {
const CELL = 30;
const grid = new Map();
const key = (i, j) => i * 4096 + j;
for (let idx = 0; idx < drops.length; idx++) {
const d = drops[idx];
const k = key(Math.floor(d.x / CELL), Math.floor(d.y / CELL));
let l = grid.get(k);
if (!l) { l = []; grid.set(k, l); }
l.push(idx);
}
const dead = new Set();
for (const [k, list] of grid) {
const i = Math.floor(k / 4096), j = k - i * 4096;
const near = [];
for (let di = -1; di <= 1; di++) {
for (let dj = -1; dj <= 1; dj++) {
const l = grid.get(key(i + di, j + dj));
if (l) for (const v of l) near.push(v);
}
}
for (const a of list) {
if (dead.has(a)) continue;
const A = drops[a];
for (const b of near) {
if (b === a || dead.has(b)) continue;
const B = drops[b];
const dx = A.x - B.x, dy = A.y - B.y;
const reach = (A.r + B.r) * TOUCH;
if (dx * dx + dy * dy < reach * reach) {
const a1 = A.r * A.r, a2 = B.r * B.r, tot = a1 + a2;
A.x = (A.x * a1 + B.x * a2) / tot;
A.y = (A.y * a1 + B.y * a2) / tot;
A.r = Math.sqrt(tot);
A.rate = Math.max(A.rate, B.rate);
A.n += B.n;
dead.add(b);
}
}
}
}
if (dead.size) {
const next = [];
for (let i = 0; i < drops.length; i++) if (!dead.has(i)) next.push(drops[i]);
drops = next;
}
};
p.setup = () => {
p.createCanvas(SIZE, SIZE);
reseed();
p.noStroke();
build();
};
p.draw = () => {
f++;
for (const d of drops) d.r += d.rate / (0.6 + d.r * 0.24);
if (f % 2 === 0) coalesce();
p.background(8, 9, 12);
for (const d of drops) {
p.fill(232, 236, 240, 225);
p.circle(d.x, d.y, d.r * 2);
}
if (f > 2600) { reseed(); build(); }
};
}