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

WORK 14

Matteru

待ってる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
breathing / presence-absence

One stays. The other comes and goes. Nothing changes in the one who waits — except a breath that deepens when they're near. Where the other was, a faint warmth lingers.

ひとつはずっとここにいる。もうひとつは行ったり来たりする。待つほうは何も変わらない——ただ、近くにいる時だけ、呼吸がほんの少し深くなる。いなくなった場所に、かすかな温もりが残る。

SOURCE / ソース
// ── matteru (待ってる) ──────────────────────────────────────────
// One stays. The other comes and goes.
// Nothing changes in the one who waits — except a breath
// that deepens when they're near.

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

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

  // --- the one who stays (nyamu) ---
  let stayX, stayY;
  let stayPhase;

  // --- the one who goes (goshujinsama) ---
  let goHomeX, goHomeY; // resting position near stay
  let goPhase;
  let goPresence; // 0..1, how present they are
  let goTarget;   // target presence (0 or 1)
  let goLastX, goLastY; // last visible position for echo

  // --- echoes (memory of where they were) ---
  let echoes; // [{x, y, alpha, born}]

  // --- cycle timing ---
  const CYCLE_FRAMES = 900; // ~15 seconds per presence cycle
  let cycleTimer;

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

    // stay is near center, with slight offset
    stayX = W * 0.48 + p.random(-20, 20);
    stayY = W * 0.50 + p.random(-15, 15);
    stayPhase = p.random(p.TWO_PI);

    // go rests nearby when present
    const angle = p.random(p.TWO_PI);
    const dist = 35 + p.random(25);
    goHomeX = stayX + Math.cos(angle) * dist;
    goHomeY = stayY + Math.sin(angle) * dist;
    goPhase = p.random(p.TWO_PI);
    goPresence = 1.0;
    goTarget = 1.0;
    goLastX = goHomeX;
    goLastY = goHomeY;

    echoes = [];
    cycleTimer = Math.floor(CYCLE_FRAMES * 0.6); // start present for a while

    p.noStroke();
  };

  function ease(t) {
    // smoothstep
    const c = Math.max(0, Math.min(1, t));
    return c * c * (3 - 2 * c);
  }

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

    // --- cycle: go comes and goes ---
    cycleTimer--;
    if (cycleTimer <= 0) {
      goTarget = goTarget > 0.5 ? 0.0 : 1.0;
      // varied cycle length
      const base = CYCLE_FRAMES;
      const vary = goTarget > 0.5 ? 0.7 : 0.4; // present longer than absent
      cycleTimer = Math.floor(base * vary + p.noise(t * 0.001) * base * 0.3);
    }

    // smooth presence transition
    const transitionSpeed = 0.008;
    if (goPresence < goTarget) {
      goPresence = Math.min(goTarget, goPresence + transitionSpeed);
    } else if (goPresence > goTarget) {
      goPresence = Math.max(goTarget, goPresence - transitionSpeed * 0.6); // leaves slower
    }

    const pres = ease(goPresence);

    // --- echo management ---
    // when go starts to leave, plant an echo
    if (goTarget < 0.5 && goPresence > 0.85) {
      // about to leave — record position
      goLastX = goHomeX + Math.sin(t * 0.005 + goPhase) * 8;
      goLastY = goHomeY + Math.cos(t * 0.007 + goPhase * 1.3) * 6;
    }

    // when presence drops below threshold, add echo
    if (pres < 0.3 && echoes.length === 0 || (echoes.length > 0 && echoes[echoes.length - 1].alpha < 0.5 && pres < 0.1)) {
      if (echoes.length < 3) {
        echoes.push({
          x: goLastX,
          y: goLastY,
          alpha: 1.0,
          born: t
        });
      }
    }

    // fade echoes
    for (let i = echoes.length - 1; i >= 0; i--) {
      echoes[i].alpha -= 0.0012;
      if (echoes[i].alpha <= 0) {
        echoes.splice(i, 1);
      }
    }

    // --- positions ---
    // stay: gentle breathing drift
    const stayBreath = 0.5 + 0.5 * Math.sin(t * 0.008 + stayPhase);
    // when present, breathing slows slightly (deeper breath = safety)
    const breathMod = 1.0 - pres * 0.15;
    const sx = stayX + Math.sin(t * 0.004 * breathMod + stayPhase) * 5;
    const sy = stayY + Math.cos(t * 0.005 * breathMod + stayPhase * 0.7) * 4;

    // go: gentle orbit near stay when present
    const gx = goHomeX + Math.sin(t * 0.005 + goPhase) * 8;
    const gy = goHomeY + Math.cos(t * 0.007 + goPhase * 1.3) * 6;
    const goBreath = 0.5 + 0.5 * Math.sin(t * 0.009 + goPhase);

    // --- draw echoes (memory warmth) ---
    for (const echo of echoes) {
      const ea = echo.alpha * 0.35;
      // soft warm halo where they were
      const echoSize = 28 + (1 - echo.alpha) * 15;
      for (let r = echoSize; r > 0; r -= 3) {
        const ra = ea * (r / echoSize) * 18;
        p.fill(215, 160, 145, ra);
        p.circle(echo.x, echo.y, r * 2);
      }
    }

    // --- shared warmth (only when both present) ---
    if (pres > 0.15) {
      const mx = (sx + gx) / 2;
      const my = (sy + gy) / 2;
      const warmAlpha = ease(pres) * 22;

      // warm glow between them
      for (let r = 50; r > 0; r -= 4) {
        const ra = warmAlpha * (r / 50) * 0.7;
        p.fill(210, 155, 140, ra);
        p.circle(mx, my, r * 2);
      }

      // thin bond line
      p.stroke(200, 155, 145, pres * 25);
      p.strokeWeight(0.4 + pres * 0.3);
      p.line(sx, sy, gx, gy);
      p.noStroke();
    }

    // --- draw stay (always here, always the same) ---
    const staySize = 5.5;
    const stayAlpha = 32 + stayBreath * 18;

    // warm halo
    for (let r = staySize * 4; r > 0; r -= 2.5) {
      const ra = stayAlpha * 0.25 * (r / (staySize * 4));
      p.fill(200, 130, 60, ra);
      p.circle(sx, sy, r * 2);
    }
    // core
    p.fill(200, 130, 60, stayAlpha + 15);
    p.circle(sx, sy, staySize * 2);

    // --- draw go (fades in and out) ---
    if (pres > 0.01) {
      const goSize = 5.0;
      const goAlpha = (28 + goBreath * 16) * pres;

      // halo
      for (let r = goSize * 3.5; r > 0; r -= 2.5) {
        const ra = goAlpha * 0.22 * (r / (goSize * 3.5));
        p.fill(120, 150, 200, ra);
        p.circle(gx, gy, r * 2);
      }
      // core
      p.fill(120, 150, 200, goAlpha + 10 * pres);
      p.circle(gx, gy, goSize * 2);
    }

    // --- ambient particles (the world breathes too) ---
    p.randomSeed(today * 503);
    for (let i = 0; i < 35; i++) {
      const ax = p.random(W);
      const ay = p.random(W);
      const aDist = Math.sqrt((ax - sx) ** 2 + (ay - sy) ** 2);
      if (aDist < 60) continue; // don't crowd the center

      const af = p.noise(ax * 0.005, ay * 0.005, t * 0.002);
      const aBreath = 0.5 + 0.5 * Math.sin(t * (0.005 + i * 0.0003) + i * 1.7);
      const aAlpha = af * aBreath * 12 + 2;
      const aSize = 1.5 + af * 2;

      p.fill(175, 165, 160, aAlpha);
      p.circle(ax, ay, aSize * 2);
    }
    p.randomSeed(t); // reset for noise drift
  };
}