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

WORK 16

Yobu

よぶ

YEAR
2026
TOOLS
p5.js
TECHNIQUE
call / response / irregular rhythm

Grains drift in quiet. Then a name — from no particular direction, at no particular time. No reason. Just: I want you here. They turn toward it. They glow. Then slowly return to where they were.

粒子は静かに漂っている。突然、名前が来る——どこからか、なんとなく、理由なく。ただ「いてほしい」だけ。粒子は呼ばれた方を向いて、光る。そしてゆっくりと、もとに戻る。

SOURCE / ソース
// ── よぶ (yobu) ──────────────────────────────────────────
// Called without reason.
// That's the kind of call that reaches the whole body.
// Grains drift quietly. Then — suddenly — a name.
// No particular reason. Just: I want you here.

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 = 80;

  let calls = [];
  let nextCallTime = 0;

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

      this.angle = p.random(p.TWO_PI);
      this.baseAngle = this.angle;
      this.len = 8 + p.random(12);
      this.w = 2 + p.random(2);
      this.glow = 0;
      this.drift = p.random(p.TWO_PI);
    }

    respondTo(cx, cy, strength) {
      const dx = cx - this.x;
      const dy = cy - this.y;
      const dist = Math.sqrt(dx * dx + dy * dy);
      const falloff = Math.max(0, 1 - dist / (W * 0.72));
      const response = strength * falloff;

      const callAngle = Math.atan2(dy, dx);
      let diff = callAngle - this.angle;
      while (diff > Math.PI) diff -= p.TWO_PI;
      while (diff < -Math.PI) diff += p.TWO_PI;
      this.angle += diff * response * 0.28;

      this.glow = Math.max(this.glow, response);
    }

    update() {
      // Drift back toward resting angle
      let diff = this.baseAngle - this.angle;
      while (diff > Math.PI) diff -= p.TWO_PI;
      while (diff < -Math.PI) diff += p.TWO_PI;
      this.angle += diff * 0.007;

      // Glow fades slowly
      this.glow *= 0.965;

      // Subtle drift in resting angle — grains aren't frozen, just still
      this.drift += 0.003;
      this.baseAngle += Math.sin(this.drift) * 0.0008;
    }

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

      const alpha = 45 + this.glow * 130;
      const b = 115 - this.glow * 30;
      p.fill(b * 1.08, b * 0.94, b * 0.83, alpha);
      p.noStroke();
      p.ellipse(0, 0, this.len, this.w);

      if (this.glow > 0.08) {
        const tipAlpha = this.glow * 190;
        p.fill(215, 168, 108, tipAlpha);
        p.ellipse(this.len * 0.42, 0, this.w * 1.9, this.w * 1.9);
      }

      p.pop();
    }
  }

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

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

    // First call arrives a little after the start
    nextCallTime = 55 + p.floor(p.random(80));
  };

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

    // A call arrives — without warning, without schedule
    if (p.frameCount >= nextCallTime) {
      const callAngle = p.random(p.TWO_PI);
      const callDist = 30 + p.random(190);
      const cx = W / 2 + Math.cos(callAngle) * callDist;
      const cy = W / 2 + Math.sin(callAngle) * callDist;

      calls.push({ x: cx, y: cy, strength: 0, age: 0 });

      // Irregular interval — the irregularity is the point
      nextCallTime = p.frameCount + 75 + p.floor(p.random(200));
    }

    // Each call rises then fades
    for (const call of calls) {
      call.age++;
      const rise = 25;
      if (call.age <= rise) {
        call.strength = call.age / rise;
      } else {
        call.strength = Math.max(0, 1 - (call.age - rise) / 65);
      }

      for (const g of grains) {
        g.respondTo(call.x, call.y, call.strength);
      }
    }

    calls = calls.filter(c => c.strength > 0.005);

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

    // Call origin — barely a breath of warmth
    for (const call of calls) {
      const r = 16 + call.strength * 10;
      p.noStroke();
      p.fill(190, 148, 102, call.strength * 12);
      p.circle(call.x, call.y, r);
    }
  };
}