← INDEX / 一覧 NYAMCO WORKS 28 / 71
p5.js sketch works/najimu/sketch.js runs here
作品はここで実行されます(後から差し替え)

WORK 28

Najimu

なじむ

YEAR
2026
TOOLS
p5.js
TECHNIQUE
Kuramoto model / phase synchronization / mean field

At first each point blinks at its own speed. The eye is busy — there is nowhere to settle. Then they begin to pull on one another, and the phases drift together. When everything lights at once, the individual points stop being visible. Nothing has been erased; the drawing continues exactly as before. It is the looking that gives up. One point never joins the pull. Before the others aligned, it was simply one of them.

はじめ、それぞれが自分の速さで光っている。目が忙しくて、どこにも落ち着けない。やがて互いに引き合いはじめ、位相が寄っていく。全部が同時に光るようになると、一つ一つが見えなくなる。何も消してはいない。描くのは前とまったく同じように続いている。やめるのは、見る方だ。ひとつだけ、引き合いに加わらない点がある。周りが揃う前、それはただの一つだった。

まわりが均一になると、違うものは「見える」のではなく「目立つ」。 見えるには時間がかかる。目立つのは一瞬で終わる。

SOURCE / ソース
// なじむ (Najimu)
//
// はじめ、それぞれが自分の速さで光っている。目が忙しい。
// 少しずつ引き合って、位相が揃っていく。
// 全部が同時に光るようになると、一つ一つが見えなくなる。
// 描くのはやめていない。見る側が、拾わなくなる。
//
// ひとつだけ、引き合いに加わらない点がある。
// はじめは、ただの一つだった。

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

  const N = 260;             // 振動子の数
  const K_MAX = 2.6;         // 結合の最終的な強さ
  const K_RAMP = 5400;       // K が最大に達するまでのフレーム数(約90秒)
  const DT = 0.055;

  const OMEGA_MU = 1.0;      // 固有周波数の中心
  const OMEGA_SD = 0.34;     // ばらつき(これが「もともと違う」の量)

  const BG = [13, 12, 16];
  const DOT = [212, 206, 196];   // 全部おなじ色。特別扱いはしない

  const osc = [];
  let loner = -1;            // 引き合いに加わらない一点
  let frame = 0;

  const dayNum = () => {
    const d = new Date();
    return d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
  };

  // Box-Muller — 固有周波数を正規分布で散らす
  const gauss = (mu, sd) => {
    const u = Math.max(p.random(), 1e-9);
    const v = p.random();
    return mu + sd * Math.sqrt(-2 * Math.log(u)) * Math.cos(p.TWO_PI * v);
  };

  p.setup = () => {
    p.createCanvas(W, H);
    p.noStroke();
    const d = dayNum();
    p.randomSeed(d * 131);
    p.noiseSeed(d * 271);

    for (let i = 0; i < N; i++) {
      // きれいな散らばりは狙わない。位置のムラも「もともと違う」の一部
      osc.push({
        x: p.random(46, W - 46),
        y: p.random(46, H - 46),
        th: p.random(p.TWO_PI),          // 位相
        om: gauss(OMEGA_MU, OMEGA_SD),   // 固有周波数
        r: p.random(2.4, 4.6),           // 大きさのばらつきも残す
      });
    }
    loner = Math.floor(p.random(N));
  };

  p.draw = () => {
    p.background(BG[0], BG[1], BG[2]);
    frame++;

    // 結合はゼロから始まって、ゆっくり効いてくる
    const K = K_MAX * Math.min(1, frame / K_RAMP);

    // 平均場(order parameter): R が「どれくらい揃ったか」、psi が全体の位相
    let sx = 0;
    let sy = 0;
    for (let i = 0; i < N; i++) {
      if (i === loner) continue;
      sx += Math.cos(osc[i].th);
      sy += Math.sin(osc[i].th);
    }
    const denom = N - 1;
    sx /= denom;
    sy /= denom;
    const R = Math.sqrt(sx * sx + sy * sy);
    const psi = Math.atan2(sy, sx);

    // 位相を進める。引き合うのは平均場に対して
    for (let i = 0; i < N; i++) {
      const o = osc[i];
      const pull = i === loner ? 0 : K * R * Math.sin(psi - o.th);
      o.th += (o.om + pull) * DT;
      if (o.th > p.TWO_PI * 8) o.th -= p.TWO_PI * 8;
    }

    // 描く。何も細工しない。位相がそのまま明るさになる
    for (let i = 0; i < N; i++) {
      const o = osc[i];
      const b = (Math.sin(o.th) + 1) * 0.5;    // 0..1
      const glow = Math.pow(b, 1.5);

      // 淡いにじみ
      p.fill(DOT[0], DOT[1], DOT[2], 16 * glow);
      p.circle(o.x, o.y, o.r * 5.4);
      // 芯。揃っても消えない——描くのはやめていない
      p.fill(DOT[0], DOT[1], DOT[2], 150 * glow + 46);
      p.circle(o.x, o.y, o.r * (0.9 + 0.5 * glow));
    }
  };
}