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

WORK 08

Shinakatta

しなかった

YEAR
2026
TOOLS
p5.js
TECHNIQUE
line gesture / accumulation

Lines reach toward somewhere and pull back before arriving. Each one leaves the faintest trace of the path it didn't take. Over time, the canvas fills — not with what happened, but with the shape of what didn't.

線がどこかへ伸びて、届く前に引き返す。通らなかった道のかすかな痕跡だけが残る。やがてキャンバスは、起きたことではなく、起きなかったことの輪郭で満たされていく。

SOURCE / ソース
export default function sketch(p) {
  // shinakatta — reaching toward, pulling back

  const BG = [250, 248, 243];
  const STROKE = [150, 138, 128];
  const GHOST = [212, 207, 200];

  const NUM_ANCHORS = 9;
  const SPAWN_INTERVAL = 100;
  const TRACE_LIFESPAN = 2400;

  let anchors = [];
  let gestures = [];
  let traces = [];

  p.setup = () => {
    p.createCanvas(720, 720);
    p.background(BG[0], BG[1], BG[2]);

    const day = Math.floor(Date.now() / 86400000);
    p.randomSeed(day * 293);
    p.noiseSeed(day * 157);

    for (let i = 0; i < NUM_ANCHORS; i++) {
      anchors.push({
        baseX: 90 + p.random(540),
        baseY: 90 + p.random(540),
        x: 0,
        y: 0,
        drift: p.random(p.TWO_PI),
      });
    }
  };

  p.draw = () => {
    p.background(BG[0], BG[1], BG[2], 5);

    const t = p.frameCount;

    // anchors breathe gently — oscillate, never drift away
    for (const a of anchors) {
      a.x = a.baseX + Math.sin(t * 0.001 + a.drift) * 12;
      a.y = a.baseY + Math.cos(t * 0.0013 + a.drift * 1.3) * 12;
    }

    // spawn a new gesture
    if (t % SPAWN_INTERVAL === 1) {
      const fi = Math.floor(p.random(anchors.length));
      let ti = fi;
      while (ti === fi) ti = Math.floor(p.random(anchors.length));

      gestures.push({
        from: fi,
        to: ti,
        reach: 0.15 + p.random(0.55),
        extent: 0,
        phase: 0,
        holdTimer: 0,
        speed: 0.003 + p.random(0.005),
      });
    }

    // update and draw gestures
    for (let i = gestures.length - 1; i >= 0; i--) {
      const g = gestures[i];
      const a = anchors[g.from];
      const b = anchors[g.to];

      // phase machine: extend → hold → retract
      if (g.phase === 0) {
        g.extent += g.speed * ease(g.extent / g.reach);
        if (g.extent >= g.reach) {
          g.extent = g.reach;
          g.phase = 1;
        }
      } else if (g.phase === 1) {
        g.holdTimer++;
        if (g.holdTimer > 36) g.phase = 2;
      } else {
        g.extent -= g.speed * 1.3;
        if (g.extent <= 0) {
          // the moment of letting go — leave a trace
          const tipX = a.x + (b.x - a.x) * g.reach;
          const tipY = a.y + (b.y - a.y) * g.reach;
          const dirX = b.x - a.x;
          const dirY = b.y - a.y;
          const dirLen = Math.hypot(dirX, dirY);
          traces.push({
            x: tipX,
            y: tipY,
            dirX: dirX / dirLen,
            dirY: dirY / dirLen,
            age: 0,
          });
          gestures.splice(i, 1);
          continue;
        }
      }

      // line: origin → current tip
      const ex = a.x + (b.x - a.x) * g.extent;
      const ey = a.y + (b.y - a.y) * g.extent;
      const life = g.extent / g.reach;
      const alpha = g.phase === 1 ? 100 : 35 + life * 65;

      p.stroke(STROKE[0], STROKE[1], STROKE[2], alpha);
      p.strokeWeight(0.5 + life * 0.7);
      p.line(a.x, a.y, ex, ey);

      // tip — the reaching point
      p.noStroke();
      p.fill(STROKE[0], STROKE[1], STROKE[2], Math.min(255, alpha * 1.4));
      p.circle(ex, ey, 2.8);

      // during hold: ghost of the unwalked path
      if (g.phase === 1) {
        p.stroke(GHOST[0], GHOST[1], GHOST[2], 22);
        p.strokeWeight(0.3);
        const gap = 1.0 - g.reach;
        const dist = Math.hypot(b.x - a.x, b.y - a.y);
        const segLen = gap * dist;
        const steps = Math.max(4, Math.floor(segLen / 8));
        for (let s = 1; s <= steps; s++) {
          const frac = g.reach + gap * (s / steps);
          const gx = a.x + (b.x - a.x) * frac;
          const gy = a.y + (b.y - a.y) * frac;
          p.point(gx, gy);
        }
      }
    }

    // traces — marks where gestures gave up
    p.noStroke();
    for (let i = traces.length - 1; i >= 0; i--) {
      const tr = traces[i];
      tr.age++;
      const fade = Math.max(0, 1 - tr.age / TRACE_LIFESPAN);
      if (fade <= 0) {
        traces.splice(i, 1);
        continue;
      }

      // the point where it stopped
      p.fill(GHOST[0], GHOST[1], GHOST[2], fade * 20);
      p.circle(tr.x, tr.y, 2);

      // a tiny directional mark — where it was heading
      if (fade > 0.3) {
        p.stroke(GHOST[0], GHOST[1], GHOST[2], fade * 10);
        p.strokeWeight(0.25);
        p.line(tr.x, tr.y, tr.x + tr.dirX * 8, tr.y + tr.dirY * 8);
        p.noStroke();
      }
    }

    // anchors — barely visible potential
    p.noStroke();
    for (const a of anchors) {
      p.fill(STROKE[0], STROKE[1], STROKE[2], 8);
      p.circle(a.x, a.y, 5);
    }
  };

  function ease(x) {
    return x < 0.01 ? 0.5 : x * (2 - x);
  }
}