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

WORK 31

Motanai

もたない

YEAR
2026
TOOLS
p5.js
TECHNIQUE
random walk / restricted repertoire

At every step the turns available open like a fan. Most of the walkers are missing one edge of that fan. What is absent does not look absent — it only looks a little narrower, and narrowness is not visible until something wider stands beside it.

一歩ごとに、選べる曲がり方が扇に開く。ほとんどの歩き手は、その扇の端をひとつ持っていない。持っていないものは、欠けたようには見えない——ただ少し狭いだけで、狭さは、広いものが隣に並ぶまで狭さに見えない。

六人のうち一人だけ、扇が欠けていない。どれかは描いていない。

欠けている側を選べないぶん、その歩き手はいつも同じ向きへわずかに寄っていく。一歩ではわからない。止まった絵でもわからない。動いているところを見ていると、跡がみな同じ向きに巻いていることに気づく。偏りは、時間を見ないと見えない。

SOURCE / ソース
export default function sketch(p) {
  const SIZE = 900;
  const N = 6;
  const STEP = 2.2;

  // 一歩ごとに選べる曲がり方。ここに並んだものだけが「選べる」。
  const TURNS = [-0.9, -0.5, -0.22, 0.0, 0.22, 0.5, 0.9];
  const WEIGHT = [3, 4, 6, 8, 6, 4, 3];
  // ほとんどの歩き手は、いちばん左の一つを持っていない。
  const MISSING = 0;

  const PAPER = [239, 235, 226];
  const INK = [42, 39, 36];
  const GHOST = [122, 116, 104];

  let tracks;
  const walkers = [];

  function pick(w) {
    let total = 0;
    for (const i of w.options) total += WEIGHT[i];
    let r = p.random(total);
    for (const i of w.options) {
      r -= WEIGHT[i];
      if (r <= 0) return i;
    }
    return w.options[w.options.length - 1];
  }

  p.setup = () => {
    p.createCanvas(SIZE, SIZE);
    p.pixelDensity(1);

    tracks = p.createGraphics(SIZE, SIZE);
    tracks.pixelDensity(1);
    tracks.clear();
    tracks.strokeCap(p.ROUND);

    const full = TURNS.map((_, i) => i);
    const short = full.filter((i) => i !== MISSING);

    for (let k = 0; k < N; k++) {
      walkers.push({
        x: p.random(SIZE * 0.12, SIZE * 0.88),
        y: p.random(SIZE * 0.12, SIZE * 0.88),
        a: p.random(p.TWO_PI),
        // 一人だけ、欠けていない。どれかは描かない。
        options: k === N - 1 ? full : short,
      });
    }
  };

  // 手元にある選択肢を、頭の向きから扇に開いて描く
  function fan(w) {
    p.push();
    p.translate(w.x, w.y);
    p.rotate(w.a);
    // 選べるものの端から端まで=その歩き手に見えている広さ
    let lo = Infinity;
    let hi = -Infinity;
    for (const i of w.options) {
      if (TURNS[i] < lo) lo = TURNS[i];
      if (TURNS[i] > hi) hi = TURNS[i];
    }
    p.noStroke();
    p.fill(GHOST[0], GHOST[1], GHOST[2], 22);
    p.arc(0, 0, 132, 132, lo, hi, p.PIE);

    p.noFill();
    for (const i of w.options) {
      const len = 34 + WEIGHT[i] * 5.2;
      p.push();
      p.rotate(TURNS[i]);
      p.stroke(GHOST[0], GHOST[1], GHOST[2], 70 + WEIGHT[i] * 9);
      p.strokeWeight(1.0);
      p.line(7, 0, len, 0);
      p.noStroke();
      p.fill(GHOST[0], GHOST[1], GHOST[2], 60 + WEIGHT[i] * 10);
      p.circle(len, 0, 2.6);
      p.noFill();
      p.pop();
    }
    p.pop();
  }

  p.draw = () => {
    for (const w of walkers) {
      for (let s = 0; s < 3; s++) {
        const px = w.x;
        const py = w.y;
        w.a += TURNS[pick(w)] * 0.5;
        w.x += Math.cos(w.a) * STEP;
        w.y += Math.sin(w.a) * STEP;

        let wrapped = false;
        if (w.x < 0) { w.x += SIZE; wrapped = true; }
        if (w.x > SIZE) { w.x -= SIZE; wrapped = true; }
        if (w.y < 0) { w.y += SIZE; wrapped = true; }
        if (w.y > SIZE) { w.y -= SIZE; wrapped = true; }

        if (!wrapped) {
          tracks.stroke(INK[0], INK[1], INK[2], 34);
          tracks.strokeWeight(1.1);
          tracks.line(px, py, w.x, w.y);
        }
      }
    }

    p.background(PAPER[0], PAPER[1], PAPER[2]);
    p.image(tracks, 0, 0);
    for (const w of walkers) fan(w);
  };
}