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

WORK 62

Needle

さす

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / divergent walk, convergent readout

Each dot carries a needle that measures its own speed. The needle never lies. Speeds drift toward a common value, so in time every needle points the same way — perfect agreement, arriving. Meanwhile the dots themselves scatter and do not come back. The needle points at a quantity, not at a destination. Reading the same, and getting closer, turn out to be two different things.

点はそれぞれ針を持っていて、針は自分の速さを測っている。嘘は一つもない。速さは少しずつ共通の値へ寄っていくので、やがてすべての針がおなじ方を指す。読みは、そろう。そのあいだ点そのものは散っていって、戻ってこない。針が指しているのは量であって、行き先ではない。読みが合うことと、近づくことは、別のことだった。

針の角度は、速さだけで決まる。位置も、向きも、一文字も入っていない。

画面の端は巻き戻していない。出ていったものは、出ていったままにしてある。

SOURCE / ソース
// ── さす (Needle) ────────────────────────────────────────────────
// 針は正直だ。速さをちゃんと測って、そのぶんだけ傾く。嘘は一つもない。
// 速さは少しずつ揃っていくので、針もやがて全部おなじ方を指す。
// でも針が指しているのは値であって、その点がどこへ行くかではない。
// 読みが揃っていくあいだ、点そのものは散っていく。同時に、両方。
export default function sketch(p) {
  const W = 720, H = 720;

  const N = 52;
  const SPEED_TARGET = 1.30;    // 速さが寄っていく先
  const CONVERGE = 0.0042;      // 速さだけにかかる引力(向きには一切かからない)
  const TURN = 0.020;           // 向きのランダムウォーク幅
  const NEEDLE = 23;            // 針の長さ
  const TRAIL = 140;            // 足跡の保持数

  const S_MIN = 0.30, S_MAX = 2.55;   // 針の目盛りの両端

  let pts = [];
  let ink, needleCol;

  p.setup = () => {
    p.createCanvas(W, H);

    const d = new Date();
    const day = d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
    p.randomSeed(day * 419);
    p.noiseSeed(day * 977);

    ink       = p.color(30, 32, 38);
    needleCol = p.color(198, 62, 74);

    for (let i = 0; i < N; i++) {
      pts.push({
        x: p.width / 2 + p.random(-46, 46),
        y: p.height / 2 + p.random(-46, 46),
        a: p.random(p.TWO_PI),
        s: p.random(S_MIN + 0.04, S_MAX - 0.04),   // 最初はばらばら
        seed: p.random(1000),
        trail: [],
      });
    }

    p.background(250, 250, 247);
  };

  const wrap = (q) => {
    let jumped = false;
    if (q.x < -8)      { q.x += W + 16; jumped = true; }
    else if (q.x > W + 8) { q.x -= W + 16; jumped = true; }
    if (q.y < -8)      { q.y += H + 16; jumped = true; }
    else if (q.y > H + 8) { q.y -= H + 16; jumped = true; }
    return jumped;
  };

  p.draw = () => {
    p.background(250, 250, 247, 74);

    for (const q of pts) {
      // 向き: 誰とも相談しない。ただ勝手に曲がる
      q.a += (p.noise(q.seed, p.frameCount * 0.0038) - 0.5) * TURN * 2;

      // 速さ: ゆっくり共通の値へ寄る
      q.s += (SPEED_TARGET - q.s) * CONVERGE;

      q.x += Math.cos(q.a) * q.s;
      q.y += Math.sin(q.a) * q.s;

      if (wrap(q)) q.trail.length = 0;   // 縁をまたいだ足跡は繋がない
      q.trail.push([q.x, q.y]);
      if (q.trail.length > TRAIL) q.trail.shift();
    }

    // 足跡
    p.noFill();
    p.stroke(30, 32, 38, 27);
    p.strokeWeight(1);
    for (const q of pts) {
      if (q.trail.length < 2) continue;
      p.beginShape();
      for (const [tx, ty] of q.trail) p.vertex(tx, ty);
      p.endShape();
    }

    // 点と針
    for (const q of pts) {
      // 針の角度は速さだけで決まる。位置も向きも一切入らない
      const ang = p.map(q.s, S_MIN, S_MAX, -p.PI * 0.46, p.PI * 0.46, true);

      p.push();
      p.translate(q.x, q.y);

      p.stroke(30, 32, 38, 120);
      p.strokeWeight(1);
      p.line(-NEEDLE * 0.3, 0, NEEDLE * 0.3, 0);   // 目盛りの台座

      p.stroke(needleCol);
      p.strokeWeight(1.8);
      p.line(0, 0, Math.sin(ang) * NEEDLE, -Math.cos(ang) * NEEDLE);

      p.noStroke();
      p.fill(ink);
      p.circle(0, 0, 3.2);
      p.pop();
    }
  };
}