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

WORK 91

Heard

きこえる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / a bell that is never drawn, counted only where its sound lands

A moonlit night, seen from the side: sky, clouds, a wooded hill, a road, water. Along the road, from west to east, goes a white horse with a silver bell. Neither the horse nor the bell is drawn; the horse is a single number in the code. Each time the bell rings, the sound arrives at points across the landscape, late by the distance it travelled, and each point it reaches lights for a moment in the way its material would: a small ring on the water, a smear in the cloud, a flicker in the trees, a short line on the road, a faint dot in the sky. On the hill there is one lit window. It dims slowly. When it goes out, the bell stops, and the horse, still somewhere on the road, is no longer heard at all. After a sixteen-line children's song by Ogawa Mimei, which counts the bell not where it is but where it sounds: in the water, in the sky, in the clouds. Asked until when it rings, the song answers with a state rather than an hour: while the little one falls asleep.

月夜を横から見ている。空、雲、森の丘、道、水。道の上を西から東へ、銀の鈴をつけた白い馬が行く。馬も鈴も描いていない。馬はコードの中の一つの数でしかない。鈴が鳴るたびに、音は進んだ距離のぶんだけ遅れて景色のあちこちに着き、着いた点だけが、その材質の光り方で一瞬光る。水では小さな輪に、雲ではにじみに、森ではちらつきに、道では短い線に、空ではかすかな点に。丘の上に灯った窓が一つある。ゆっくり暗くなって、消えると鈴は鳴らなくなる。馬はまだ道のどこかにいるけれど、もう聞こえない。小川未明の十六行の童謡「鈴が鳴る」から。この歌は鈴を、鈴のある場所ではなく、鳴った場所で数える。水で鳴る、空で鳴る、雲で鳴る。いつまで鳴らしてゆくのかと訊かれて、時刻ではなく、誰かの様子で答える。坊やがおねんねする間。

SOURCE / ソース
// きこえる / Heard
//
// 月夜を横から見ている。上から空、雲、丘と森、道、水。
//
// 道の上を、西から東へ、鈴をつけた白い馬が行く——ということになっている。
// 馬も鈴も描かない。馬の位置は、コードの中の一つの数にしかない。
//
// 鈴が鳴るたびに、音は距離のぶんだけ遅れて、景色のあちこちに着く。
// 着いた点だけが、その場所の材質の光り方で、一瞬光る。
// 水では小さな輪に、雲ではにじみに、森ではちらつきに、道では短い線に、空ではかすかな点に。
//
// 丘の上に窓が一つある。はじめは灯っていて、だんだん暗くなる。
// 消えたら鈴は鳴らなくなる。馬はまだ道の途中にいて、その先は一度も照らされない。
//
// 小川未明「鈴が鳴る」から。
//   水で鳴る、空で鳴る、雲で鳴る。
//   いつから、いつまで鳴らしてゆく。坊やがおねんねする間。

export default function sketch(p) {
  const W = 720, H = 720;

  // --- 景色 -------------------------------------------------------
  const WATER_Y = 600;
  const ridge = (x) => 360 + 70 * (p.noise(x * 0.004, 3.1) - 0.5) + 30 * (p.noise(x * 0.015, 7.7) - 0.5);
  const roadY = (x) => 548 + 10 * (p.noise(x * 0.006, 11.3) - 0.5);

  // --- 鈴 ---------------------------------------------------------
  const C = 5.0;            // 音の速さ(px / frame)
  const HORSE_V = 0.42;     // 馬の速さ(px / frame)
  const HORSE_X0 = -30;
  const PAIR_GAP = 9;       // りんりん、の二つの間
  const PERIOD = 52;        // りんりん、りんりん
  const SLEEP_START = 860;  // 窓が暗くなりはじめる
  const SLEEP_END = 1100;   // 窓が消える。ここから先は鳴らない
  const NIGHT_LEN = 1460;   // 次の夜まで

  // 材質ごとの光り方
  const MAT = {
    sky:    { col: [196, 206, 255], decay: 0.90, gain: 0.55 },
    cloud:  { col: [226, 226, 240], decay: 0.965, gain: 0.85 },
    forest: { col: [168, 232, 184], decay: 0.86, gain: 1.05 },
    road:   { col: [244, 228, 192], decay: 0.92, gain: 1.00 },
    bank:   { col: [196, 224, 172], decay: 0.88, gain: 0.75 },
    water:  { col: [176, 214, 255], decay: 0.935, gain: 0.95 },
  };

  let rx = [], ry = [], rm = [], rj = [], rb = [], rage = [];
  let clouds = [];
  let rings = [];
  let t = 0;
  let night = 0;
  let house = { x: 0, y: 0 };
  let ground;

  function inCloud(x, y) {
    for (const c of clouds) {
      const dx = (x - c.x) / c.w, dy = (y - c.y) / c.h;
      if (dx * dx + dy * dy < 1) return true;
    }
    return false;
  }

  function build() {
    const daySeed = Math.floor(Date.now() / 86400000);
    p.randomSeed(daySeed * 131 + night * 17);
    p.noiseSeed(daySeed * 137 + night * 29);

    clouds = [];
    for (let i = 0; i < 4; i++) {
      clouds.push({ x: p.random(40, W - 40), y: p.random(120, 250), w: p.random(70, 140), h: p.random(16, 30) });
    }

    rx = []; ry = []; rm = []; rj = []; rb = []; rage = [];
    const add = (x, y, m) => { rx.push(x); ry.push(y); rm.push(m); rj.push(p.random(0.6, 1.2)); rb.push(0); rage.push(999); };

    let n = 0;
    while (n < 2600) {
      const x = p.random(W), y = p.random(H);
      const r = ridge(x), ry0 = roadY(x);
      let m;
      if (y < r) m = inCloud(x, y) ? 'cloud' : 'sky';
      else if (y < ry0 - 7) m = 'forest';
      else if (y < ry0 + 5) m = 'road';
      else if (y < WATER_Y) m = 'bank';
      else m = 'water';
      // 密度の調整:空は疎らに、雲と水と道は少し濃く
      const keep = { sky: 0.30, cloud: 1.0, forest: 0.55, road: 1.0, bank: 0.6, water: 0.8 }[m];
      if (p.random() < keep) { add(x, y, m); }
      n++;
    }
    // 道は細いので、道の上に足しておく
    for (let i = 0; i < 160; i++) {
      const x = p.random(W);
      add(x, roadY(x) + p.random(-2, 2), 'road');
    }

    const hx = p.random(W * 0.22, W * 0.4);
    house = { x: hx, y: ridge(hx) };

    // 動かない背景を一枚に
    ground = p.createGraphics(W, H);
    ground.pixelDensity(1);
    ground.noStroke();
    for (let y = 0; y < H; y++) {
      const k = y / H;
      ground.fill(10 + 8 * k, 13 + 9 * k, 30 + 6 * k);
      ground.rect(0, y, W, 1);
    }
    // 月
    const mx = W * p.random(0.62, 0.82), my = p.random(70, 120);
    for (let r = 60; r > 0; r -= 2) {
      ground.fill(220, 225, 255, 3);
      ground.circle(mx, my, r * 2.2);
    }
    ground.fill(232, 234, 250, 200);
    ground.circle(mx, my, 34);
    // 丘
    ground.fill(8, 12, 20);
    ground.beginShape();
    ground.vertex(0, H);
    for (let x = 0; x <= W; x += 4) ground.vertex(x, ridge(x));
    ground.vertex(W, H);
    ground.endShape(p.CLOSE);
    // 水
    ground.fill(9, 14, 28);
    ground.rect(0, WATER_Y, W, H - WATER_Y);
    // 道(ほとんど見えない)
    ground.stroke(22, 24, 30);
    ground.strokeWeight(3);
    ground.noFill();
    ground.beginShape();
    for (let x = 0; x <= W; x += 6) ground.vertex(x, roadY(x));
    ground.endShape();
    ground.noStroke();
    // 家
    ground.fill(14, 16, 22);
    ground.rect(house.x - 9, house.y - 14, 18, 14);
    ground.triangle(house.x - 12, house.y - 14, house.x + 12, house.y - 14, house.x, house.y - 24);

    rings = [];
    t = 0;
  }

  function horseX(tt) { return HORSE_X0 + tt * HORSE_V; }

  function windowLight(tt) {
    if (tt < SLEEP_START) return 1;
    if (tt > SLEEP_END) return 0;
    const k = (tt - SLEEP_START) / (SLEEP_END - SLEEP_START);
    return Math.pow(1 - k, 1.6);
  }

  p.setup = () => {
    p.createCanvas(W, H);
    p.pixelDensity(1);
    build();
  };

  p.draw = () => {
    // 鳴らす
    const phase = t % PERIOD;
    if (t < SLEEP_END && windowLight(t) > 0.12 && (phase === 0 || phase === PAIR_GAP)) {
      const hx = horseX(t);
      rings.push({ x: hx, y: roadY(hx) - 10, t0: t });
    }

    // 音が着く
    for (const r of rings) {
      const front = (t - r.t0) * C, back = front - C;
      for (let i = 0; i < rx.length; i++) {
        const dx = rx[i] - r.x, dy = ry[i] - r.y;
        const d2 = dx * dx + dy * dy;
        if (d2 > front * front || d2 <= back * back) continue;
        const d = Math.sqrt(d2);
        const amp = MAT[rm[i]].gain * rj[i] / Math.pow(1 + d / 90, 1.6);
        if (amp > rb[i]) { rb[i] = amp; rage[i] = 0; }
      }
    }
    rings = rings.filter((r) => (t - r.t0) * C < 1100);

    p.image(ground, 0, 0);

    // 窓
    const wl = windowLight(t);
    if (wl > 0.01) {
      p.noStroke();
      p.fill(255, 196, 120, 30 * wl);
      p.circle(house.x, house.y - 7, 22);
      p.fill(255, 206, 130, 230 * wl);
      p.rect(house.x - 2.5, house.y - 10, 5, 5);
    }

    // 光る
    for (let i = 0; i < rx.length; i++) {
      const b = rb[i];
      if (b < 0.01) { rage[i]++; continue; }
      const m = MAT[rm[i]];
      const [cr, cg, cb] = m.col;
      const a = 255 * Math.min(1, b * 2.1);
      if (rm[i] === 'water') {
        p.noFill();
        p.stroke(cr, cg, cb, a * 0.8);
        p.strokeWeight(0.8);
        const rr = 2 + rage[i] * 0.25;
        p.ellipse(rx[i], ry[i], rr * 2, rr * 0.7);
      } else if (rm[i] === 'cloud') {
        p.noStroke();
        p.fill(cr, cg, cb, a * 0.18);
        p.circle(rx[i], ry[i], 14);
        p.fill(cr, cg, cb, a * 0.5);
        p.circle(rx[i], ry[i], 2.2);
      } else if (rm[i] === 'road') {
        p.stroke(cr, cg, cb, a);
        p.strokeWeight(1.1);
        p.line(rx[i] - 3, ry[i], rx[i] + 3, ry[i]);
      } else if (rm[i] === 'forest') {
        p.noStroke();
        const f = (rage[i] % 3 === 1) ? 0.45 : 1;   // ちらつく
        p.fill(cr, cg, cb, a * f);
        p.circle(rx[i], ry[i], 2.5);
      } else {
        p.noStroke();
        p.fill(cr, cg, cb, a);
        p.circle(rx[i], ry[i], rm[i] === 'sky' ? 1.5 : 1.9);
      }
      rb[i] *= m.decay;
      rage[i]++;
    }

    t++;
    if (t >= NIGHT_LEN) { night++; build(); }
  };
}