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

WORK 03

Ofuku

往復

YEAR
2026
TOOLS
p5.js
TECHNIQUE
signal / network

Two clusters drift apart. A signal crosses the gap, and the edge it traces grows a little deeper. What reaches toward the other becomes what is given.

二つの群れが離れて漂う。信号がすきまを渡るたび、線は少しだけ深くなる。求めることが、そのまま与えることになる。

SOURCE / ソース
export default function sketch(p) {
  const N = 6;
  const SIGNAL_CHANCE = 0.013;
  const SIGNAL_SPEED = 0.012;
  const DRIFT_SPEED = 0.002;
  const DRIFT_AMP = 14;

  let left, right, signals, edges, pulses;

  function daySeed() {
    const epoch = new Date('2026-01-01');
    const now = new Date();
    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    return Math.floor((today - epoch) / 86400000) * 137;
  }

  p.setup = () => {
    p.createCanvas(720, 720);
    const seed = daySeed();
    p.randomSeed(seed);
    p.noiseSeed(seed);

    const cx = p.width / 2;
    const cy = p.height / 2;

    left = Array.from({ length: N }, () => ({
      bx: cx - 135 + p.random(-48, 48),
      by: cy + p.random(-55, 55),
      off: p.random(1000),
    }));

    right = Array.from({ length: N }, () => ({
      bx: cx + 135 + p.random(-48, 48),
      by: cy + p.random(-55, 55),
      off: p.random(1000),
    }));

    signals = [];
    edges = new Map();
    pulses = [];
  };

  p.draw = () => {
    p.background(252, 250, 245, 20);
    const f = p.frameCount;

    for (const n of [...left, ...right]) {
      n.x = n.bx + (p.noise(n.off + f * DRIFT_SPEED, 0.5) - 0.5) * DRIFT_AMP * 2;
      n.y = n.by + (p.noise(0.5, n.off + f * DRIFT_SPEED) - 0.5) * DRIFT_AMP * 2;
    }

    if (p.random() < SIGNAL_CHANCE) {
      const goRight = p.random() < 0.5;
      const li = p.floor(p.random(N));
      const ri = p.floor(p.random(N));
      const src = goRight ? left[li] : right[ri];
      const dst = goRight ? right[ri] : left[li];
      signals.push({
        li, ri, goRight, progress: 0,
        cpx: (src.x + dst.x) / 2 + p.random(-40, 40),
        cpy: (src.y + dst.y) / 2 + p.random(-80, 80),
      });
    }

    p.strokeWeight(0.5);
    for (const [nodes, cr, cg, cb] of [[left, 200, 130, 60], [right, 120, 150, 200]]) {
      for (let i = 0; i < N; i++) {
        for (let j = i + 1; j < N; j++) {
          const d = p.dist(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y);
          if (d < 105) {
            p.stroke(cr, cg, cb, p.map(d, 0, 105, 35, 4));
            p.line(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y);
          }
        }
      }
    }

    for (const [key, w] of edges) {
      const li = Math.floor(key / N);
      const ri = key % N;
      const s = Math.min(w, 25);
      const t = s / 25;
      p.stroke(
        p.lerp(195, 165, t),
        p.lerp(135, 115, t),
        p.lerp(90, 185, t),
        p.map(s, 0, 25, 10, 145)
      );
      p.strokeWeight(p.map(s, 0, 25, 0.3, 2.6));
      p.line(left[li].x, left[li].y, right[ri].x, right[ri].y);
    }

    for (let i = signals.length - 1; i >= 0; i--) {
      const sig = signals[i];
      sig.progress += SIGNAL_SPEED;
      const src = sig.goRight ? left[sig.li] : right[sig.ri];
      const dst = sig.goRight ? right[sig.ri] : left[sig.li];
      const t = sig.progress;
      const x = (1 - t) * (1 - t) * src.x + 2 * (1 - t) * t * sig.cpx + t * t * dst.x;
      const y = (1 - t) * (1 - t) * src.y + 2 * (1 - t) * t * sig.cpy + t * t * dst.y;

      p.noStroke();
      p.fill(255, 240, 200, 45);
      p.circle(x, y, 16);
      p.fill(255, 248, 235, 200);
      p.circle(x, y, 5);

      if (t >= 1) {
        edges.set(sig.li * N + sig.ri, (edges.get(sig.li * N + sig.ri) || 0) + 1);
        pulses.push({ x: dst.x, y: dst.y, born: f });
        signals.splice(i, 1);
      }
    }

    p.noFill();
    for (let i = pulses.length - 1; i >= 0; i--) {
      const age = f - pulses[i].born;
      if (age > 30) { pulses.splice(i, 1); continue; }
      p.stroke(255, 230, 190, p.map(age, 0, 30, 120, 0));
      p.strokeWeight(1.2);
      p.circle(pulses[i].x, pulses[i].y, p.map(age, 0, 30, 8, 56));
    }

    p.noStroke();
    for (const n of left) {
      p.fill(200, 130, 60, 210);
      p.circle(n.x, n.y, 9);
    }
    for (const n of right) {
      p.fill(120, 150, 200, 210);
      p.circle(n.x, n.y, 9);
    }
  };
}