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

WORK 15

toward

むかう

YEAR
2026
TOOLS
p5.js
TECHNIQUE
orientation field / rotation convergence

Before the step, the turning. Ninety grains scattered on a warm ground, each slowly rotating to face a gently drifting pull — not moving, just orienting. Direction found before the body moves.

動く前に、向く。90粒の種が、ゆっくりと漂う引力に向かって回り続ける。位置は変わらない。向いてる方向だけが、少しずつ変わっていく。

SOURCE / ソース
// ── むかう (mukau) ──────────────────────────────────────────
// Before the step, the turning.
// Direction found before the body moves.
// A field of grains, each slowly facing something
// it hasn't reached yet.

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

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

  let grains = [];
  const N = 90;

  let pullX, pullY, pullPhase;

  class Grain {
    constructor() {
      const angle = p.random(p.TWO_PI);
      const r = 60 + p.random(260);
      this.x = W / 2 + Math.cos(angle) * r;
      this.y = W / 2 + Math.sin(angle) * r;

      this.angle = p.random(p.TWO_PI);
      this.turnRate = 0.004 + p.random(0.012);
      this.len = 10 + p.random(14);
      this.w = 2.5 + p.random(2.5);
    }

    alignment() {
      const dx = pullX - this.x;
      const dy = pullY - this.y;
      const target = Math.atan2(dy, dx);
      let diff = this.angle - target;
      while (diff > Math.PI) diff -= p.TWO_PI;
      while (diff < -Math.PI) diff += p.TWO_PI;
      return 1 - Math.abs(diff) / Math.PI;
    }

    update() {
      const dx = pullX - this.x;
      const dy = pullY - this.y;
      const target = Math.atan2(dy, dx);

      let diff = target - this.angle;
      while (diff > Math.PI) diff -= p.TWO_PI;
      while (diff < -Math.PI) diff += p.TWO_PI;

      this.angle += diff * this.turnRate;
    }

    draw() {
      const align = this.alignment();

      p.push();
      p.translate(this.x, this.y);
      p.rotate(this.angle);

      // Warm ochre-grey, more present when aligned
      const b = 110 - align * 65;
      const a = 70 + align * 130;
      p.fill(b * 1.1, b * 0.93, b * 0.82, a);
      p.noStroke();
      p.ellipse(0, 0, this.len, this.w);

      // Warm tip at the facing end — appears as alignment grows
      if (align > 0.55) {
        const tipAlpha = ((align - 0.55) / 0.45) * 200;
        p.fill(210, 165, 105, tipAlpha);
        const tipX = this.len * 0.44;
        p.ellipse(tipX, 0, this.w * 1.8, this.w * 1.8);
      }

      p.pop();
    }
  }

  p.setup = () => {
    p.createCanvas(W, W);
    p.randomSeed(today * 347);
    p.noiseSeed(today * 197);

    pullPhase = 0;
    pullX = W / 2;
    pullY = W / 2;

    for (let i = 0; i < N; i++) {
      grains.push(new Grain());
    }
  };

  p.draw = () => {
    p.background(252, 249, 245);

    pullPhase += 0.004;
    pullX = W * 0.5 + Math.cos(pullPhase * 0.62) * 50;
    pullY = W * 0.5 + Math.sin(pullPhase) * 50;

    for (const g of grains) {
      g.update();
      g.draw();
    }

    // Pull center — barely visible, just a breath of warmth
    const pA = 7 + 5 * Math.sin(p.frameCount * 0.025);
    p.noStroke();
    p.fill(190, 145, 100, pA);
    p.circle(pullX, pullY, 22);
  };
}