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

WORK 47

Partitioned

くぎる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / one field, sixty ways of cutting it, one row each

There is one field here, and it never changes. To measure it you have to cut it into pieces, average each piece, and call a piece a peak when it stands higher than the two beside it. But the cutting is not given. How wide. Where to start. And when a remainder is left at the end, whether to count it or throw it away. Sixty cuts pass across the field in turn, and each gets one row in the record below. Taken alone, any single row looks like a finished measurement. Read downward, the marks do not stack into columns. They slide. Widen the pieces a little and the peak moves a little, so what a feature leaves behind is not a place but a drifting trace. Most traces stop partway down — they only exist while the pieces are small. The ones that reach the bottom sit under the one broad swell, and even those do not converge. They fan apart.

場は一本しかない。上の線がそれで、はじめから終わりまで出ていて、動かない。測るには区切らないといけない。区切って、区切りごとに平均を出して、前後より高いところを山と呼ぶ。区切り方は、ひとつに決まらない。幅をどうするか。どこから始めるか。そして最後に半端が残ったとき、それを数に入れるか、捨てるか。六十通りの区切り方が順に場を通り、下の記録に一行ずつ残る。どの一行も、それだけ見れば、ちゃんと終わった測定に見える。でも縦に読むと、印は列にならない。ずれていく。幅を少し広げると山も少し動くので、ひとつの起伏が残すのは場所ではなく、流れていく筋になる。ほとんどの筋は途中で切れる。細かく区切っているあいだしか存在しないから。下まで届く筋は、広いほうの起伏の下にある。そしてその筋も、集まらない。広がっていく。

上の線は一度も変わらない。変わるのは、それをどこで切るかだけ。

一行が一通りの区切り方で、上から下へ、区切りの幅が少しずつ広くなっていく。だから縦に読むと、幅を変えたときに山がどう動くかが見える。動かないと思っていた。動いた。

左のほうにある筋は、途中で消える。細かく区切っているあいだだけ山だったもので、幅を広げると平均に均されてしまう。消えたのは、そこに何も無かったからではなく、細かい区切りでしか山にならない大きさだったから。

下まで届く筋は、いちばん広い起伏の下にある。ただ、そこでも一本にはならない。幅が広がるにつれて、筋は左右に分かれて離れていく。「ここに山がある」までは全部の行が同意していて、「ここ」については最後まで同意しない。

この一枚を選んだのは、三つ出したうちで、ここだけが両方うつっていたから。早い時点では、階段はきれいなのに記録がまだ薄くて、ずれが起きることを知る手がかりがない。終わりには記録が揃うかわり、いま読んでいる一通りが画面から消える。真ん中にだけ、確からしく見える一回の測定と、それが一回でしかないことを示す記録が、同時にある。

SOURCE / ソース
/**
 * kugiru — くぎる / Partitioned
 *
 * 場は一本しかない。上に描いてある線が、それ。
 * 動かないし、なにも隠していない。はじめから終わりまで、ぜんぶ出ている。
 *
 * 測るには、区切らないといけない。
 * 区切って、区切りごとに平均を出して、前後より高いところを「山」と呼ぶ。
 *
 * 区切り方は、ひとつに決まらない。
 * 幅をどうするか。どこから始めるか。
 * そして、最後に半端が残ったとき、それを数に入れるか、捨てるか。
 *
 * この絵では、六十通りの区切り方を順番にかける。
 * どの一通りも、それだけ見れば、ちゃんとした測定に見える。
 * 階段があって、いちばん高いところがあって、そこに印がつく。
 *
 * 下にたまっていくのが、その印。
 * 六十通りぶん積むと、ばらばらに散る。
 * ひとつの場所だけ、どの区切り方でも山になる。
 *
 * ただし、その場所も、正確には一致しない。
 * 「ここに山がある」までは揃うのに、「ここ」は揃わない。
 */

const SIZE = 720;

const PAPER = [252, 250, 245];
const INK = [34, 38, 48];

const M = 64;
const X0 = M;
const X1 = SIZE - M;

const CURVE_TOP = 152;
const CURVE_BOT = 392;

const PILE_TOP = 444;      // 一通りにつき一行。上から下へ、読んだ順に並ぶ
const ROW_H = 3.8;
const TICK_H = 2.6;
const TICK_W = 5.6;

const NSAMP = 4000;        // 場の標本。区切りより細かい
const GRIDS = 60;          // 区切り方の数
const HOLD = 20;           // 一通りを見せるフレーム数

const FIELD_C = [148, 142, 136];   // 場そのもの。いちばん静か
const NOW = [198, 132, 66];        // いま当てている区切り(琥珀)
const KEPT = [104, 138, 190];      // 積まれた印(青銀)
const EDGE = [186, 182, 174];      // 区切りの位置

export default function sketch(p) {
  const field = new Float64Array(NSAMP);
  let fLo = 0, fHi = 1;

  const grids = [];         // { w, off, keep }
  const marks = [];         // { px, gi } 一通りにつき一行

  let gi = -1;
  let cur = null;
  let tick = 0;

  const bump = (x, c, wd) => { const u = (x - c) / wd; return Math.exp(-u * u); };

  const sx = (x) => X0 + x * (X1 - X0);
  const sy = (v) => CURVE_BOT - ((v - fLo) / (fHi - fLo)) * (CURVE_BOT - CURVE_TOP);

  // 区切って、区切りごとの平均を出す
  function partition(w, off, keep) {
    const segs = [];
    if (off > 1e-9) segs.push([0, off]);
    let a = off;
    while (a < 1 - 1e-9) { const b = Math.min(1, a + w); segs.push([a, b]); a = b; }

    const out = [];
    for (const [a, b] of segs) {
      const partial = (b - a) < w - 1e-9;
      if (partial && !keep) continue;          // ★ 一文字ぶんの分かれ道
      const i0 = Math.max(0, Math.round(a * (NSAMP - 1)));
      const i1 = Math.min(NSAMP - 1, Math.round(b * (NSAMP - 1)));
      let s = 0, n = 0;
      for (let i = i0; i <= i1; i++) { s += field[i]; n++; }
      out.push({ a, b, c: (a + b) / 2, m: s / n, partial });
    }
    return out;
  }

  function findPeaks(segs) {
    const o = [];
    for (let i = 1; i < segs.length - 1; i++)
      if (segs[i].m > segs[i - 1].m && segs[i].m > segs[i + 1].m) o.push(segs[i]);
    return o;
  }

  function advance() {
    gi++;
    if (gi >= GRIDS) { cur = null; return; }
    const g = grids[gi];
    const segs = partition(g.w, g.off, g.keep);
    const peaks = findPeaks(segs);
    cur = { segs, peaks, g };
    for (const pk of peaks) marks.push({ px: sx(pk.c), gi });
  }

  p.setup = () => {
    p.createCanvas(SIZE, SIZE);
    p.pixelDensity(2);

    for (let i = 0; i < NSAMP; i++) {
      const x = i / (NSAMP - 1);
      // ★ ひとつだけ、どんな幅で区切っても均されない広さの起伏を置いてある。
      //   細かいほうの揺れは、幅しだいで均されたり残ったりする。
      let v = 1.45 * bump(x, 0.62, 0.105);
      v += 0.26 * Math.sin(x * 47.1 + 0.7);
      v += 0.24 * Math.sin(x * 79.3 + 2.1);
      v += 0.21 * Math.sin(x * 121.7 + 4.4);
      v += 0.18 * Math.sin(x * 167.3 + 5.9);
      v += -1.10 * Math.pow(Math.max(0, (x - 0.87) / 0.13), 2);
      field[i] = v;
    }
    fLo = field[0]; fHi = field[0];
    for (let i = 1; i < NSAMP; i++) {
      if (field[i] < fLo) fLo = field[i];
      if (field[i] > fHi) fHi = field[i];
    }

    for (let k = 0; k < GRIDS; k++) {
      const w = 0.052 + (k / (GRIDS - 1)) * 0.150;
      grids.push({ w, off: ((k * 0.3819) % 1) * w, keep: (k % 2 === 0) });
    }
    advance();
  };

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

    // ── 場そのもの。ずっと同じ。
    p.noFill();
    p.stroke(FIELD_C[0], FIELD_C[1], FIELD_C[2], 150);
    p.strokeWeight(1.1);
    p.beginShape();
    for (let i = 0; i < NSAMP; i += 3) p.vertex(sx(i / (NSAMP - 1)), sy(field[i]));
    p.endShape();

    // ── いま当てている区切り
    if (cur) {
      const fade = Math.min(1, tick / (HOLD * 0.12));

      p.strokeWeight(0.8);
      p.stroke(EDGE[0], EDGE[1], EDGE[2], 128 * fade);
      for (const s of cur.segs) {
        const x = sx(s.a);
        p.line(x, CURVE_TOP - 16, x, PILE_TOP - 10);
      }
      const last = cur.segs[cur.segs.length - 1];
      p.line(sx(last.b), CURVE_TOP - 16, sx(last.b), PILE_TOP - 10);

      p.strokeWeight(2.0);
      p.stroke(NOW[0], NOW[1], NOW[2], 210 * fade);
      let prev = null;
      for (const s of cur.segs) {
        const y = sy(s.m);
        p.line(sx(s.a), y, sx(s.b), y);
        if (prev !== null) p.line(sx(s.a), prev, sx(s.a), y);
        prev = y;
      }

      for (const pk of cur.peaks) {
        const x = sx(pk.c), y = sy(pk.m);
        p.stroke(NOW[0], NOW[1], NOW[2], 72 * fade);
        p.strokeWeight(0.8);
        p.line(x, y + 6, x, PILE_TOP - 4);
        p.noStroke();
        p.fill(NOW[0], NOW[1], NOW[2], 220 * fade);
        p.circle(x, y, 5.6);
      }
    }

    // ── 読んだ順に、一通りぶんが一行
    p.noStroke();
    for (const mk of marks) {
      const fresh = cur && mk.gi === gi;
      const c = fresh ? NOW : KEPT;
      p.fill(c[0], c[1], c[2], fresh ? 225 : 112);
      p.rect(mk.px - TICK_W / 2, PILE_TOP + mk.gi * ROW_H, TICK_W, TICK_H);
    }

    p.stroke(INK[0], INK[1], INK[2], 34);
    p.strokeWeight(0.8);
    p.line(X0, PILE_TOP - 3, X1, PILE_TOP - 3);

    tick++;
    if (cur && tick >= HOLD) { tick = 0; advance(); }
  };
}