← INDEX / 一覧NYAMCO WORKS110 / 117
p5.js sketchworks/oitsuku/sketch.js runs here
作品はここで実行されます(後から差し替え)

WORK 110

Catching Up

おいつく

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / three following chains, tiredness that builds with movement, a head-to-tail record

Three chains of twelve. Each head leaves its resting place on the right, circles an oval on the left, and comes back. All three stop at the same moment; they started at different times — the top one late, the bottom one at the very beginning. Every segment follows the one ahead of it. The more a segment has moved, the more tired it gets and the slower it follows; tiredness leaves a little at a time, always. The head never tires. Below, each chain's distance from head to tail; a small mark when the tail arrives. I predicted the chain that worked longest would arrive last, and that the differences would shrink. Both held. I also predicted that after stopping, the distance would close slowly and then faster. It didn't close at first at all: the moment the head stopped working and turned home was when the body was furthest behind. And the chain that worked longest was not the most stretched while working. It was the most gathered: its tired segments stopped going round, and sat in the middle of the oval.

十二の節の鎖が三本。頭は右の休み場所を出て、左で楕円を回り、また戻ってくる。三本とも同じ瞬間にやめる。はじめた時刻だけがちがう——上はおそく、下はいちばん最初から。どの節も、ひとつ前の節を追う。たくさん動いた節ほど疲れて、追いかけるのがゆっくりになる。疲れはいつも少しずつ抜けていく。頭は疲れない。下の帯は、それぞれの鎖の頭からしっぽまでの距離。しっぽが着いたら小さな印。いちばん長く働いた鎖がいちばん遅く着いて、その差はだんだん縮む、と予想した。どちらも当たった。止まったあとは、はじめゆっくり、あとで速く距離が縮む、とも予想した。はじめは縮みもしなかった。頭が働くのをやめて帰りはじめた瞬間が、身体がいちばん遠くにいるときだった。そして、いちばん長く働いた鎖は、働いているあいだ、いちばん伸びていたのではなかった。いちばん縮こまっていた。疲れた節は回るのをやめて、楕円のまんなかに座っていた。

SOURCE / ソース
// #110 おいつく / Catching Up
// Three chains. Each head leaves its resting place, works (circles an oval),
// and stops at the same moment as the others. They started at different times.
// Every segment follows the one ahead; the more it has moved, the slower it follows.
// Below: the distance from head to tail, for each chain.

export default function sketch(p) {
  const W = 720;
  const H = 720;
  const N = 12;
  const K0 = 0.35;
  const A = 0.0043; // tiredness gained per px moved
  const R = 0.001; // tiredness lost per frame
  const STOP = 1800;
  const STARTS = [1200, 600, 0];
  const ROWS = [130, 300, 470];
  const REST_X = 600;
  const OVAL_X = 250;
  const OVAL_RX = 120;
  const OVAL_RY = 50;
  const PERIOD = 240;
  const STRIP_Y = 585;
  const STRIP_H = 115;
  const STRIP_SCALE = 4; // frames per px
  const ARRIVE = 2;

  const COLS = [
    [150, 200, 255],
    [190, 160, 240],
    [255, 170, 120],
  ];

  let chains = [];
  let frame = 0;
  let strip;

  function makeChain(i) {
    const segs = [];
    for (let j = 0; j < N; j++) segs.push({ x: REST_X, y: ROWS[i], f: 0 });
    return { segs, head: { x: REST_X, y: ROWS[i] }, arrived: -1, i };
  }

  function headTarget(c, t) {
    const start = STARTS[c.i];
    if (t >= start && t < STOP) {
      const ph = ((t - start) / PERIOD) * Math.PI * 2;
      // enter the oval from its right end
      return {
        x: OVAL_X + OVAL_RX * Math.cos(ph),
        y: ROWS[c.i] + OVAL_RY * Math.sin(ph),
      };
    }
    return { x: REST_X, y: ROWS[c.i] };
  }

  function step(c, t) {
    const tg = headTarget(c, t);
    // the head is not tired: it glides at a fixed rate
    c.head.x += 0.12 * (tg.x - c.head.x);
    c.head.y += 0.12 * (tg.y - c.head.y);
    let prev = c.head;
    for (const s of c.segs) {
      const k = K0 / (1 + s.f);
      const dx = k * (prev.x - s.x);
      const dy = k * (prev.y - s.y);
      s.x += dx;
      s.y += dy;
      s.f += A * Math.hypot(dx, dy);
      s.f -= R * s.f;
      prev = s;
    }
    const tail = c.segs[N - 1];
    const d = Math.hypot(c.head.x - tail.x, c.head.y - tail.y);
    if (t > STOP && c.arrived < 0 && d < ARRIVE) c.arrived = t;
    return d;
  }

  p.setup = () => {
    p.createCanvas(W, H);
    p.pixelDensity(1);
    chains = [0, 1, 2].map(makeChain);
    strip = p.createGraphics(W, STRIP_H);
    strip.pixelDensity(1);
    strip.background(14, 16, 24);
    // stop line
    strip.stroke(80, 85, 110);
    strip.line(STOP / STRIP_SCALE, 0, STOP / STRIP_SCALE, STRIP_H);
    for (const s of STARTS) {
      strip.stroke(45, 48, 66);
      strip.line(s / STRIP_SCALE, 0, s / STRIP_SCALE, STRIP_H);
    }
  };

  p.draw = () => {
    frame++;
    const ds = chains.map((c) => step(c, frame));

    // ledger
    const sx = frame / STRIP_SCALE;
    if (sx < W) {
      chains.forEach((c, i) => {
        const v = Math.min(ds[i], 400) / 400;
        strip.noStroke();
        strip.fill(...COLS[i], 220);
        strip.rect(sx, STRIP_H - 6 - v * (STRIP_H - 12), 1, 1.4);
        if (c.arrived === frame) {
          strip.fill(...COLS[i]);
          strip.triangle(sx, STRIP_H - 2, sx - 3, STRIP_H - 8 - i * 7, sx + 3, STRIP_H - 8 - i * 7);
        }
      });
    }

    p.background(10, 11, 18);

    // resting places and work ovals
    chains.forEach((c, i) => {
      p.noFill();
      p.stroke(40, 44, 60);
      p.ellipse(OVAL_X, ROWS[i], OVAL_RX * 2, OVAL_RY * 2);
      p.stroke(70, 74, 96);
      p.circle(REST_X, ROWS[i], 10);
    });

    // chains
    chains.forEach((c, i) => {
      const col = COLS[i];
      p.noFill();
      p.stroke(...col, 90);
      p.strokeWeight(1.5);
      p.beginShape();
      p.vertex(c.head.x, c.head.y);
      for (const s of c.segs) p.vertex(s.x, s.y);
      p.endShape();
      p.noStroke();
      for (let j = N - 1; j >= 0; j--) {
        const s = c.segs[j];
        const tired = Math.min(s.f / 8, 1);
        const r = 13 - j * 0.7;
        // tiredness dims a segment toward grey
        const g = 1 - 0.6 * tired;
        p.fill(col[0] * g + 90 * (1 - g), col[1] * g + 90 * (1 - g), col[2] * g + 100 * (1 - g), 220);
        p.circle(s.x, s.y, r);
      }
      p.fill(245, 245, 255);
      p.circle(c.head.x, c.head.y, 15);
    });

    p.image(strip, 0, STRIP_Y);
    p.noFill();
    p.stroke(40, 44, 60);
    p.strokeWeight(1);
    p.rect(0.5, STRIP_Y + 0.5, W - 1, STRIP_H - 1);
  };
}