WORK 88
いつく
Houses go up anywhere, at random. Nobody knows the land. Every so often a storm crosses the picture, and as the front passes, it knocks down houses — more often where the ground is exposed to the wind. Where the wind runs is written only in the code; it is never drawn. Houses also get left for ordinary reasons, anywhere, without a storm. Nothing chooses where to stand. After a while, the older houses, darker and warmer, have gathered along lines nobody drew, while the new ones keep appearing everywhere. After Terada Torahiko's essay on typhoons, written the winter after the 1934 Muroto typhoon: the old villages came through almost untouched, and the settlements that had grown up around new railway stations did not. The old villages, he wrote, had come to rest, over a long process of selection, where the wind does the least harm.
家は、どこにでも、でたらめに建つ。誰も地形を知らない。ときどき嵐が来て、前線が画面を横切り、通った場所の家を倒す。吹きさらしの場所ほど、よく倒れる。どこを風が通るかはコードの中にだけ書いてあって、一度も描かない。嵐がなくても、家はふつうの理由で、どこででも住まれなくなる。どの家も、建つ場所を選んでいない。しばらくすると、年を取った濃くてあたたかい家は、誰も引いていない線に沿って集まり、新しい家は相変わらずどこにでも建つ。寺田寅彦「颱風雑俎」から。室戸台風の翌年の冬に書かれた随筆で、古い村はほとんど無事で、停車場ができて育った新しい集落がやられたという。古い村は「永い間の自然淘汰によって、颱風の害の最小なような地の利のある地域に定着している」。
// いつく / Settling
//
// 家は、どこにでも、でたらめに建つ。誰も地形を知らない。
// ときどき嵐が来て、前線が画面を横切り、通った場所の家を倒す。
// 倒れやすさは、その場所がどれだけ吹きさらしかで決まる。
// その吹きさらし具合(風の通り道)はコードの中にだけあって、一度も描かない。
//
// 残った家の並びだけが、見えない地形を覚えている。
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;
};
}
export const W = 720, H = 720;
const G = 90; // 吹きさらし具合の格子
const BIRTH = 0.5; // 1フレームに建つ家(平均)
const LEAVE = 1 / 7000; // 場所に関係なく、ふつうに住まなくなる確率(毎フレーム)
const STORM_EVERY = 260; // 嵐の間隔(フレーム)
const FRONT_SPEED = 9; // 前線の速さ(px/フレーム)
// なめらかな値ノイズ(p5 に依存しないので、モデル単体でも回せる)
function makeNoise(rnd) {
const N = 12, v = [];
for (let i = 0; i < (N + 1) * (N + 1); i++) v.push(rnd());
const at = (i, j) => v[j * (N + 1) + i];
const s = t => t * t * (3 - 2 * t);
return (x, y) => {
x *= N; y *= N;
const i = Math.min(N - 1, Math.floor(x)), j = Math.min(N - 1, Math.floor(y));
const fx = s(x - i), fy = s(y - j);
const a = at(i, j) + (at(i + 1, j) - at(i, j)) * fx;
const b = at(i, j + 1) + (at(i + 1, j + 1) - at(i, j + 1)) * fx;
return a + (b - a) * fy;
};
}
export function makeModel(seed = 930) {
const rnd = mulberry32(seed);
const n1 = makeNoise(rnd), n2 = makeNoise(rnd);
// 吹きさらし具合 E(0..1)。谷筋のような細い通り道が何本か走る。
const E = new Float32Array(G * G);
for (let j = 0; j < G; j++) for (let i = 0; i < G; i++) {
const u = i / (G - 1), v = j / (G - 1);
const ridge = 1 - Math.abs(n1(u, v) * 2 - 1); // 通り道=ノイズの尾根
const base = n2(u, v);
E[j * G + i] = Math.min(1, Math.pow(ridge, 3) * 0.85 + base * 0.35);
}
const exposure = (x, y) => {
const i = Math.max(0, Math.min(G - 1, Math.round(x / W * (G - 1))));
const j = Math.max(0, Math.min(G - 1, Math.round(y / H * (G - 1))));
return E[j * G + i];
};
const houses = []; // {x,y,born,fell}
let storm = null; // {dx,dy,pos,end,strength}
let frame = 0;
let felled = 0;
function step() {
frame++;
// 建つ
if (rnd() < BIRTH) houses.push({ x: rnd() * W, y: rnd() * H, born: frame, fell: -1, left: false });
for (const h of houses) {
if (h.fell < 0 && rnd() < LEAVE) { h.fell = frame; h.left = true; }
}
// 嵐が来る
if (!storm && frame % STORM_EVERY === 0) {
const a = rnd() * Math.PI * 2;
const dx = Math.cos(a), dy = Math.sin(a);
// 前線は、進む向きに沿った座標 s = x*dx + y*dy が pos の直線
const corners = [[0, 0], [W, 0], [0, H], [W, H]].map(([x, y]) => x * dx + y * dy);
storm = {
dx, dy,
pos: Math.min(...corners) - 20,
end: Math.max(...corners) + 20,
strength: 0.55 + rnd() * 0.45,
};
}
// 前線が通る
if (storm) {
const prev = storm.pos;
storm.pos += FRONT_SPEED;
for (const h of houses) {
if (h.fell >= 0) continue;
const s = h.x * storm.dx + h.y * storm.dy;
if (s > prev && s <= storm.pos) {
const e = exposure(h.x, h.y);
const t = Math.max(0, Math.min(1, (e - 0.28) / 0.42));
const p = storm.strength * t * t * (3 - 2 * t) * 0.92;
if (rnd() < p) { h.fell = frame; h.left = false; felled++; }
}
}
if (storm.pos > storm.end) storm = null;
}
// 倒れて消え終わった家を片づける
if (frame % 60 === 0) {
for (let i = houses.length - 1; i >= 0; i--) {
if (houses[i].fell >= 0 && frame - houses[i].fell > 40) houses.splice(i, 1);
}
}
}
return {
step, exposure,
get frame() { return frame; },
get houses() { return houses; },
get storm() { return storm; },
get felled() { return felled; },
};
}
export default function sketch(p) {
let m;
const rain = mulberry32(77);
p.setup = () => {
p.createCanvas(W, H);
p.pixelDensity(1);
m = makeModel();
};
p.draw = () => {
m.step(); m.step(); // 一描画に二年ぶん進める
const f = m.frame;
p.background(16, 19, 26);
p.noStroke();
// 家
for (const h of m.houses) {
if (h.fell >= 0) {
const k = (f - h.fell) / 40;
if (k > 1) continue;
if (h.left) { // 住まなくなった家は、輪を出さずに薄れる
const age = Math.min(1, (h.fell - h.born) / 2400);
p.fill(150 + 95 * age, 165 + 45 * age, 190 - 60 * age, (120 + 110 * age) * (1 - k));
p.circle(h.x, h.y, 2 + 2.4 * age);
continue;
}
p.noFill();
p.stroke(200, 205, 215, 140 * (1 - k));
p.strokeWeight(1);
p.circle(h.x, h.y, 3 + k * 10);
p.noStroke();
continue;
}
const age = Math.min(1, (f - h.born) / 2400); // 年を取るほど濃く、あたたかく
const r = 150 + 95 * age, g = 165 + 45 * age, b = 190 - 60 * age;
p.fill(r, g, b, 120 + 110 * age);
p.circle(h.x, h.y, 2 + 2.4 * age);
}
// 嵐の前線:細い雨だけ。前線そのものの線は引かない。
const s = m.storm;
if (s) {
p.stroke(170, 185, 205, 55);
p.strokeWeight(1);
const px = -s.dy, py = s.dx; // 前線に沿う向き
for (let k = 0; k < 90; k++) {
const along = (rain() - 0.5) * 1100;
const back = -rain() * 60;
const cx = W / 2 + px * along, cy = H / 2 + py * along;
const d = s.pos - (W / 2 * s.dx + H / 2 * s.dy) + back;
const x = cx + s.dx * d, y = cy + s.dy * d;
if (x < -10 || x > W + 10 || y < -10 || y > H + 10) continue;
p.line(x, y, x - s.dx * 7 - px * 3, y - s.dy * 7 - py * 3);
}
p.noStroke();
}
};
}