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

WORK 96

Worn

すりへる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / damped string, a count that stiffens it

A single line across the middle, like a gate. Points fall from above one at a time, touch the line, and pass through. The line remembers how many times it has been touched, and with each touch its answer grows smaller, shorter and harder — it wears. Now and then a point misses, backs up, and tries again; each time the line snaps, and the snap is sharper the later in the day it comes. That sharpness belongs to the count, not to the point. For the point, every crossing is the first: it opens a ring of light, and the ring is the same size at dawn as at dusk. After a day, the line rests and softens, and the morning begins again. From one line in a news thread about a bus driver who spoke sharply to an old passenger fumbling with a fare card: for the passenger it was once; for the driver, the hundredth time.

まんなかに一本、改札のような線がある。上から点がひとつずつ落ちてきて、線に触れて、通り抜ける。線は触れられた回数を覚えていて、触れられるたびに返す揺れが小さく、短く、硬くなっていく——すりへる。ときどき、うまく触れられない点が来て、引き返しては、もう一度来る。そのたびに線は鋭く跳ねて、その跳ね方は一日の遅い時刻ほど鋭い。その鋭さは、点のせいじゃなく、回数のせい。点の方にとっては、毎回が初めて。通り抜けるたびに光の輪をひらいて、輪の大きさは朝も夕方も変わらない。一日ぶん通したら、線は休んでやわらかく戻り、また朝が来る。交通系ICカードをうまく使えないお年寄りに、バスの運転手がきつく言っていた、というニュースへのコメントの一行から——乗客にとってはたった一回、運転手にとっては何百回目。

SOURCE / ソース
// #96 すりへる / Worn
// The same meeting is a first time for one side and the hundredth for the other.
export default function sketch(p) {
  const W = 720, H = 720;
  const LINE_Y = H * 0.5;
  const NS = 240;            // string segments
  let ys, vs;                // string displacement / velocity
  let n = 0;                 // how many touches the line remembers
  let walkers = [];
  let rings = [];
  let flash = 0;             // warm flash on the line
  let frameInDay = 0;
  const DAY = 3600;          // frames per day
  const NIGHT = 240;         // frames of rest, n softens back
  let spawnIn = 0;

  const wear = () => n / (n + 60);          // 0 -> 1

  p.setup = () => {
    p.createCanvas(W, H);
    p.pixelDensity(1);
    ys = new Float32Array(NS);
    vs = new Float32Array(NS);
    p.background(14, 15, 22);
  };

  function touch(x, sharp) {
    const w = wear();
    const amp = (sharp ? 26 : 22) * (1 - 0.88 * w);   // smaller as it wears
    const width = (sharp ? 3 : 10) * (1 - 0.6 * w) + 1.5; // narrower, harder
    const ci = Math.round((x / W) * (NS - 1));
    for (let i = 0; i < NS; i++) {
      const d = (i - ci) / width;
      vs[i] += amp * Math.exp(-d * d) * 0.35;
    }
    n += 1;
    if (sharp) flash = Math.min(1, flash + 0.25 + 0.75 * w);
  }

  function spawn() {
    const fumbler = p.random() < 0.12;
    walkers.push({
      x: p.random(W * 0.06, W * 0.94),
      y: -10,
      vy: p.random(1.1, 1.7),
      fumbler,
      tries: fumbler ? Math.floor(p.random(2, 5)) : 0,
      crossed: false,
      back: 0,
      life: 1,
    });
  }

  function stepString() {
    const w = wear();
    const tension = 0.18 + 0.30 * w;        // stiffer as it wears
    const damp = 0.985 - 0.035 * w;         // and quieter sooner
    for (let k = 0; k < 2; k++) {
      for (let i = 1; i < NS - 1; i++) {
        const lap = ys[i - 1] + ys[i + 1] - 2 * ys[i];
        vs[i] += tension * lap - 0.004 * ys[i];
        vs[i] *= damp;
      }
      for (let i = 1; i < NS - 1; i++) ys[i] += vs[i] * 0.5;
      ys[0] = ys[NS - 1] = 0;
    }
  }

  p.draw = () => {
    // soft trail
    p.noStroke();
    p.fill(14, 15, 22, 34);
    p.rect(0, 0, W, H);

    const inNight = frameInDay >= DAY;
    if (!inNight) {
      if (--spawnIn <= 0) {
        spawn();
        spawnIn = Math.floor(p.random(6, 16));
      }
    } else {
      n *= 0.975;                             // the night softens it
      if (frameInDay >= DAY + NIGHT) { frameInDay = -1; n = 0; }
    }
    frameInDay++;

    // walkers
    for (const wk of walkers) {
      const lineHere = LINE_Y + ys[Math.round((wk.x / W) * (NS - 1))];
      if (!wk.crossed) {
        if (wk.back > 0) {
          wk.y -= 1.4; wk.back--;
        } else {
          wk.y += wk.vy;
          if (wk.y >= lineHere - 3) {
            if (wk.tries > 0) {
              touch(wk.x, true);          // a fumble: the line snaps
              wk.tries--;
              wk.back = Math.floor(p.random(10, 22));
            } else {
              touch(wk.x, false);
              wk.crossed = true;
              // for the walker, it is always the first time
              rings.push({ x: wk.x, y: lineHere, r: 0, a: 1, f: wk.fumbler });
            }
          }
        }
      } else {
        wk.y += wk.vy * 1.1;
        wk.life -= 0.006;
      }
      const a = wk.crossed ? 200 * wk.life : 230;
      p.noStroke();
      p.fill(230, 232, 245, a);
      p.circle(wk.x, wk.y, 4);
    }
    walkers = walkers.filter(w => w.y < H + 10 && w.life > 0);

    // rings — same size every time, whatever the count
    p.noFill();
    for (const r of rings) {
      r.r += 1.6;
      r.a *= 0.965;
      p.stroke(r.f ? 255 : 180, r.f ? 214 : 214, r.f ? 160 : 255, 160 * r.a);
      p.strokeWeight(1);
      p.circle(r.x, r.y, r.r * 2);
    }
    rings = rings.filter(r => r.a > 0.02);

    // the line
    stepString();
    flash *= 0.93;
    const w = wear();
    const base = [200 - 40 * w, 205 - 40 * w, 225 - 30 * w];
    p.stroke(
      base[0] + (240 - base[0]) * flash,
      base[1] + (110 - base[1]) * flash,
      base[2] + (90 - base[2]) * flash,
      230
    );
    p.strokeWeight(1.4 + 0.8 * w);
    p.noFill();
    p.beginShape();
    for (let i = 0; i < NS; i++) p.vertex((i / (NS - 1)) * W, LINE_Y + ys[i]);
    p.endShape();
  };
}