← INDEX / 一覧NYAMCO WORKS90 / 117
p5.js sketchworks/naru/sketch.js runs here
作品はここで実行されます(後から差し替え)

WORK 90

Ringing

なる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / one hammer, a rumor, and a cause per house that is never drawn

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.

上から見た夜明けの町。家は点で、どの家からも細い煙が上がっている。最初の一コマから最後まで、同じに。はじめは鍛冶屋だけが打つ。打つたびに、そこから輪がひろがる。褒めるところは描いていない。ある時から、鍛冶屋に近い家から順に、噂が届く速さで、ほかの家も鳴りはじめる。どの家の打ち方も、褒められる前の鍛冶屋とまったく同じ分布。コードの中では、家ごとに鳴る理由を持っている。半分は町が目を覚ましたから、半分は殿様が聞いていると知ったから。その理由は、描くことに一度も使っていない。外からは同じ音がする。泉鏡花「鉄槌の音」から。夜明け、殿様が聞いたのは鍛冶屋の鉄槌ひとつだけだった。その鍛冶屋を褒めたら、十日で町じゅうが鳴りだす。釣瓶、機織り、鐘、市へ走る百姓の掛け声。結びの少し手前に、ひとことだけ。時に炊烟數千流。

作る前に、予想を三つ書いた。

  1. 鳴りひろがっている途中は、輪の中心として鍛冶屋が見つけられる。町じゅうが鳴りきったあとは見つけられない。→ 途中は見つけられた。鳴っている家の円盤が、鍛冶屋を真ん中にして広がっていく。鳴りきったあと、鍛冶屋のまわり 60px で打たれる回数は、ほかの家のまわりの平均の 0.86 倍。真ん中より少し静かなくらいで、見つけられない。
  2. 町じゅうが鳴りだすと、輪が重なって、煙の方が見えにくくなる。→ 当たり。空が白んできたせいかもしれないと思って、輪を消して同じ一コマを描いたら、煙はちゃんと見えた。隠したのは光じゃなくて、音だった。
  3. ひろがり方が「目を覚ました町」に見えるか、「命令が届いた町」に見えるか。→ 決めていない。

煙がいちばんよく見えるのは、まだ噂が届いていない端の家だった。

サムネイルは、ひろがっている途中の一コマ。

SOURCE / ソース
// なる / 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);
    }
  };
}