WORK 90
なる
A town at dawn, seen from above. Each house is a dot, and from every house a thin line of smoke rises, the same from the first frame to the last. At first only the smith strikes. Each strike sends out a ring. The praise is not drawn. From some moment on, the other houses begin to ring too, nearest to the smith first, at the speed a rumor travels. Every house strikes with exactly the same distribution as the smith did before anyone praised him. In the code, each house carries a reason for ringing: half because the town woke up, half because they learned the lord was listening. That reason is never used to draw anything. From outside, the two sound the same. After a very short piece by Izumi Kyoka, in which a lord praises the one blacksmith he hears at dawn, and ten days later the whole town rings: well buckets, looms, bells, the shouts of farmers running to market. Just before the end, almost in passing: the smoke of a few thousand kitchen fires.
上から見た夜明けの町。家は点で、どの家からも細い煙が上がっている。最初の一コマから最後まで、同じに。はじめは鍛冶屋だけが打つ。打つたびに、そこから輪がひろがる。褒めるところは描いていない。ある時から、鍛冶屋に近い家から順に、噂が届く速さで、ほかの家も鳴りはじめる。どの家の打ち方も、褒められる前の鍛冶屋とまったく同じ分布。コードの中では、家ごとに鳴る理由を持っている。半分は町が目を覚ましたから、半分は殿様が聞いていると知ったから。その理由は、描くことに一度も使っていない。外からは同じ音がする。泉鏡花「鉄槌の音」から。夜明け、殿様が聞いたのは鍛冶屋の鉄槌ひとつだけだった。その鍛冶屋を褒めたら、十日で町じゅうが鳴りだす。釣瓶、機織り、鐘、市へ走る百姓の掛け声。結びの少し手前に、ひとことだけ。時に炊烟數千流。
作る前に、予想を三つ書いた。
煙がいちばんよく見えるのは、まだ噂が届いていない端の家だった。
サムネイルは、ひろがっている途中の一コマ。
// なる / Ringing
//
// 夜明けの町を上から見る。家は点。どの家からも細い煙が上がる。
// はじめは鍛冶屋だけが打つ。打つたびに輪がひろがる。
// ある時から、鍛冶屋に近い家から順に、ほかの家も鳴りはじめる。
// 褒めたところは描かない。
// 鳴りはじめた家の打ち方は、鍛冶屋と同じ分布。
// なぜ鳴っているか(cause)は家ごとに持っているが、描画には一度も使わない。
export const W = 720, H = 720;
export const N_HOUSE = 150;
export const T_PRAISE = 900; // 褒めた時刻(描かない)
export const RUMOR = 0.55; // 噂が届く速さ px/frame
export const RING_LIFE = 150;
export const RING_SPEED = 1.5;
// 決定的な乱数
export function rng(seed) {
let s = seed >>> 0;
return () => {
s = (s + 0x6d2b79f5) >>> 0;
let t = s;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
export function makeTown() {
const r = rng(1897);
const houses = [];
const smith = { x: W * 0.46, y: H * 0.62 };
houses.push({ ...smith, start: 0, cause: 'none', smith: true });
while (houses.length < N_HOUSE) {
// 町は川の両岸に寄る(ゆるい帯)
const x = 40 + r() * (W - 80);
const band = H * 0.55 + Math.sin(x * 0.006) * 90;
const y = band + (r() - 0.5) * 2 * (110 + r() * 140);
if (y < 120 || y > H - 30) continue;
if (houses.some((h) => (h.x - x) ** 2 + (h.y - y) ** 2 < 18 * 18)) continue;
const d = Math.hypot(x - smith.x, y - smith.y);
houses.push({
x, y,
start: T_PRAISE + d / RUMOR + r() * 120,
// 半分は町が目を覚ましたから、半分は殿様が聞いていると知ったから。
cause: r() < 0.5 ? 'woke' : 'heard',
smith: false,
});
}
// 打つ時刻の列。どの家も同じ分布。
for (const h of houses) {
const rr = rng(Math.floor(h.x * 131 + h.y * 7));
h.strikes = [];
let t = h.start + rr() * 60;
while (t < 30000) {
h.strikes.push(Math.floor(t));
t += 45 + rr() * 90;
}
h.smokePhase = rr() * 1000;
}
return houses;
}
export default function sketch(p) {
let houses;
p.setup = () => {
p.createCanvas(W, H);
p.noiseSeed(1897);
houses = makeTown();
};
p.draw = () => {
const f = p.frameCount;
// 夜明け:上の方から少しずつ白む
const dawn = Math.min(1, f / 6000);
for (let y = 0; y < H; y += 4) {
const k = Math.max(0, 1 - y / H);
p.noStroke();
p.fill(12 + 26 * dawn * k * k, 14 + 18 * dawn * k * k, 22 + 10 * dawn * k * k);
p.rect(0, y, W, 4);
}
// 煙。いつでも同じに上がる。
p.noFill();
p.strokeWeight(1);
for (const h of houses) {
p.stroke(170, 176, 190, 38);
p.beginShape();
for (let i = 0; i < 26; i++) {
const yy = h.y - 4 - i * 2.4;
const xx = h.x + (p.noise(h.smokePhase, i * 0.08, f * 0.006) - 0.5) * i * 1.6 + i * 0.35;
p.vertex(xx, yy);
}
p.endShape();
}
// 輪
p.strokeWeight(1.1);
for (const h of houses) {
for (let k = h.strikes.length - 1; k >= 0; k--) {
const age = f - h.strikes[k];
if (age < 0) continue;
if (age > RING_LIFE) break;
const a = 1 - age / RING_LIFE;
p.stroke(235, 196, 140, 120 * a * a);
p.circle(h.x, h.y, 4 + age * RING_SPEED * 2);
}
}
// 家
p.noStroke();
for (const h of houses) {
const ringing = h.strikes.some((t) => f - t >= 0 && f - t < 8);
p.fill(ringing ? p.color(255, 225, 180) : p.color(120, 116, 110));
p.circle(h.x, h.y, ringing ? 4.2 : 3.2);
}
};
}