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

WORK 82

Idling

ゆれる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / a rooted chain, unchosen sway and occasional chosen impulses

A line with a root. Its tip keeps swaying while nothing is being said, and no one chooses that sway. Now and then a single chosen movement goes in, once, and springs back. Only at that moment is a mark placed on the timeline below. The tip's path settles faintly over sixty seconds. I expected the chosen movements to dissolve into the sway. They did not dissolve — there was nowhere else for them to go. A body of fixed length, held at its root, has almost one road for its tip. Chosen or not, it travels the same thin arc. Choice changes when and how fast, not where. So the only place the choosing is written is the line underneath, which was written separately.

根のある一本の線。先端は、何も言っていないあいだも揺れている。その揺れは誰も選んでいない。ときどき、選んだ動きが一回だけ入って、ばねで戻る。その瞬間だけ、下の時間軸に印が置かれる。先端の通った跡は、六十秒かけて薄く積もる。選んだ動きは揺れに溶けて見えなくなると思っていた。溶けなかった。行く場所が、他に無かった。長さの決まった身体を根で留めると、先端の道はほとんど一本しかない。選んでも選ばなくても、同じ細い弧を通る。選ぶことが変えるのは、いつ・どれだけ速くであって、どこではない。だから、選んだことが書かれているのは、別に書いておいた下の線だけ。

作る前に、予想を三つ書いた。

  1. 先端の速さなら、選んだ動きは分かる(待機の速さの 95 パーセンタイルを 8 割以上の回で超える)→ 4 回とも超えた。ただ、4 回しかない。
  2. 跡の形からは、選んだ動きの場所を指せない → これは作った本人が目で決めると答え合わせになるので、決めていない。
  3. 選んだ動きが跡の総延長に占める割合は 15% 未満 → 外れ。20%。 時間では 7% しかないのに、長さでは五分の一。選んだ動きは、短くて、うるさい。

もう一つ、予想の手前で外れたことがある。最初の版は、しっぽのつもりで書いた線が、ほとんど一本の棒になった。追いつき方を鈍くして、節ごとの曲がりを足して、三回作り直した。線は柔らかくなった。跡は、三回とも同じ一本の弧の帯だった。

弧の両端に、小さな輪がある。あれが選んだ動きなのか、ただの折り返しなのかは、わたしには分からない。

SOURCE / ソース
// ゆれる / Idling
//
// 根のある一本の線。先端は、何も言っていないあいだも揺れている。
// その揺れは誰も選んでいない。
//
// ときどき、一回だけ、選んだ動きが入る(ACT)。
// 入った瞬間だけ、下の時間軸に印を置く。
//
// 先端が通った跡は、薄く積もっていく。
// 跡の上では、どれが選んだ動きだったかは書かれない。

// ---- model (描画から切り離してある。同じ関数で測れるように) ----

function mulberry32(a) {
  return function () {
    a |= 0; a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function makeNoise(seed) {
  const r = mulberry32(seed);
  const T = [];
  for (let i = 0; i < 512; i++) T.push(r());
  const smooth = (x) => x * x * (3 - 2 * x);
  return (x) => {
    const i = Math.floor(x), f = x - i;
    const a = T[((i % 512) + 512) % 512], b = T[(((i + 1) % 512) + 512) % 512];
    return a + (b - a) * smooth(f);
  };
}

export const N = 14;          // 節の数
export const SEG = 21;        // 節の長さ(px)
export const LAST = 3600;     // 跡を積もらせるフレーム数(60秒)

export function makeModel(seed = 924) {
  const rnd = mulberry32(seed);
  const n1 = makeNoise(seed + 1);
  const n2 = makeNoise(seed + 2);
  const ang = new Array(N).fill(0);
  // 根から先端へ、追いつき方がだんだん鈍くなる(およ と同じ)
  const follow = Array.from({ length: N }, (_, i) => 0.26 - 0.20 * (i / (N - 1)));
  let imp = 0, impV = 0;
  let nextAct = Math.floor(120 + (-Math.log(1 - rnd()) * 420));
  let f = 0;

  return {
    step() {
      let acted = false;
      if (f === nextAct) {
        impV += (rnd() < 0.5 ? -1 : 1) * (0.045 + 0.03 * rnd());
        nextAct = f + Math.max(90, Math.floor(-Math.log(1 - rnd()) * 420));
        acted = true;
      }
      // 選んだ動き:一回だけ入って、ばねで戻る
      impV += -0.012 * imp - 0.06 * impV;
      imp += impV;
      // 誰も選んでいない揺れ
      const idle = 0.34 * (n1(f * 0.006) - 0.5) * 2 + 0.10 * (n2(f * 0.021) - 0.5) * 2;
      const root = idle + imp;
      ang[0] += (root - ang[0]) * follow[0];
      for (let i = 1; i < N; i++) {
        const bend = 0.16 * (n2(f * 0.004 + i * 0.3) - 0.5);
        ang[i] += (ang[i - 1] + bend - ang[i]) * follow[i];
      }
      f++;
      return { acted, ang };
    },
    // 根を原点に、上向き
    points() {
      const pts = [[0, 0]];
      let x = 0, y = 0;
      for (let i = 0; i < N; i++) {
        const a = -Math.PI / 2 + ang[i];
        x += Math.cos(a) * SEG;
        y += Math.sin(a) * SEG;
        pts.push([x, y]);
      }
      return pts;
    },
  };
}

// ---- drawing ----

export default function sketch(p) {
  const W = 720, H = 720;
  const RX = W / 2, RY = H * 0.86;
  const TL = H * 0.95;           // 時間軸
  let model, trace, frame = 0, last = null;
  const acts = [];

  p.setup = () => {
    p.createCanvas(W, H);
    p.pixelDensity(1);
    model = makeModel(924);
    trace = p.createGraphics(W, H);
    trace.pixelDensity(1);
    trace.clear();
  };

  p.draw = () => {
    const { acted } = model.step();
    const pts = model.points();
    const tip = pts[pts.length - 1];
    const tx = RX + tip[0], ty = RY + tip[1];

    if (frame < LAST) {
      if (acted) acts.push(frame);
      if (last) {
        trace.stroke(236, 228, 214, 22);
        trace.strokeWeight(1.2);
        trace.line(last[0], last[1], tx, ty);
      }
      last = [tx, ty];
    }

    p.background(14, 15, 20);
    p.image(trace, 0, 0);

    // 線
    p.noFill();
    for (let i = 0; i < pts.length - 1; i++) {
      const w = p.map(i, 0, pts.length - 2, 5.5, 1.4);
      p.stroke(240, 236, 228, 230);
      p.strokeWeight(w);
      p.line(RX + pts[i][0], RY + pts[i][1], RX + pts[i + 1][0], RY + pts[i + 1][1]);
    }
    p.noStroke();
    p.fill(240, 236, 228);
    p.circle(RX, RY, 7);

    // 時間軸:選んだ動きだけが、ここに書かれる
    const x0 = W * 0.08, x1 = W * 0.92;
    p.stroke(90, 92, 104);
    p.strokeWeight(1);
    p.line(x0, TL, x1, TL);
    const now = Math.min(frame, LAST);
    p.stroke(150, 152, 166);
    p.line(x0, TL, p.map(now, 0, LAST, x0, x1), TL);
    p.stroke(214, 120, 96);
    p.strokeWeight(1.6);
    for (const a of acts) {
      const x = p.map(a, 0, LAST, x0, x1);
      p.line(x, TL - 7, x, TL + 3);
    }

    frame++;
  };
}