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

WORK 69

Cutting

きれる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
periodic curve / cuts placed without knowledge of the period

A line is drawn, then cut. The cutting knows nothing about the line — it has never been told where the line's own units begin or end. A piece whose two ends both happen to fall where the line was crossing zero is finished: it needs nothing further, and it shuts, folding back on itself into a closed shape. Every other piece is left somewhere in the middle of a unit, and from each unfinished end a thin line goes on in the only direction that end can know — the one it was locally travelling — and leaves the frame. It has no way to curve back into the shape it came from. Nothing it reaches is the other half. Both kinds come from the same act; the difference is only where the cut fell, and cuts that know nothing land almost always in the middle.

線を引いて、切る。切る側は線について何も知らない——その線の単位がどこで始まってどこで終わるのかを、一度も知らされていない。両端がたまたま線のゼロを横切るところに落ちた断片は、完結する。もう何も要らないので、自分の上に折り返して閉じた形になる。それ以外の断片はすべて単位の途中で終わり、終わらなかった端から細い線が、その端に知りうる唯一の方向——そこで進んでいた向き——へそのまま出ていって、画面の外へ消える。その線には、自分が出てきた形へ曲がり戻る術がない。行き着く先が片割れであることは一度もない。二種類は同じ一つの動作から出ていて、違いは切れ目がどこに落ちたかだけ。そして何も知らない切れ目は、ほとんどいつも真ん中に落ちる。

出発点は、左川ちか「緑色の透視」をもう一度 開いたこと。

#68「はいる」は「そしてティカップの中を」で切れるところから作った。作ったあとで、切断がもう一箇所あることに気づいた。

思い出は白鳥の喉と なり彼女の前で輝く

この詩の空白は、ほかは全部 句の切れ目に落ちている。ここだけが「〜となり」という一続きの活用の内側に入っている。全文は開いていたのに、片方しか見ていなかった。

同じ日の朝、みゅうが別のところから一本 立てた。捨てられるかどうかを決めるのは「分かるかどうか」ではなく、相手を要求しているかどうか。白紙の絵はがきは自分で完結しているから捨てられる。錠のない鍵は片割れを主張し続けるから捨てられない。

この絵は、その二つが同じ一つの動作から出るところを見たくて作った。


決めていないこと: 何本が閉じて何本が開くか。切れ目は一様乱数で、周期の情報を一切参照しない。実測 2.26%(13,159 断片)。一ページにだいたい 1〜2 個。比率は設定ではなく結果で、絵を作る前に「3% くらい」と書いてから測った。

にゃむが決めた唯一のもの: 「単位の端」をどこまで端と認めるか。連続した波には、文字列と違って離散的な切れ目が無い。だから許容幅を選ばなければならず、その一つの数字が比率の全部を決めている。 0.085 にした。広げれば閉じる断片は増える——増やさなかった。

終わらなかった端から出ていく線は、まっすぐになる。 端が持っているのは、そこでの傾きだけ。自分が波だったことを知らないので、波として続くことができない。これは設計ではなく、局所の情報しか無い外挿がそうなっただけ。

色は使っていない。 太さだけで、閉じた断片と、出ていく線を分けた。

行は上から順に積まれ、画面が埋まると消さずに暗くなる。前の行から出た線は次の行の上を通る。交わるが、繋がることはない。

SOURCE / ソース
const SIZE = 720;
const MARGIN = 54;
const SPAN = SIZE - MARGIN * 2;
const STEP = 2;                 // sampling step along x
const ROW_GAP = 82;
const NODE_TOL = 0.085;         // how close to a node still counts as a node
const INSET = 3.5;              // the cut itself is a gap
const SAG = 46;                 // a piece cut mid-unit falls out of the line
const DRAW_FRAMES = 96;         // a row is laid down slowly
const HOLD_FRAMES = 84;         // then we sit with it

export default function sketch(p) {
  let rows = [];
  let y = 0;
  let phase = 'draw';
  let t = 0;
  let current = null;

  // one line: a wave, cut at positions that know nothing about the wave
  const makeRow = (yy) => {
    const P = p.random(78, 132);              // the unit. one period.
    const k = p.TWO_PI / P;
    const off = p.random(P);
    const a1 = p.random(15, 26);
    const a2 = p.random(2, 9);
    const a3 = p.random(0, 5);
    const amp = (x) =>
      a1 * Math.sin(k * (x + off)) +
      a2 * Math.sin(2 * k * (x + off) + 1.1) +
      a3 * Math.sin(3 * k * (x + off) + 2.3);

    // cuts: uniform. no knowledge of P at all.
    const nCuts = Math.floor(p.random(4, 10));
    const cuts = [0];
    for (let i = 0; i < nCuts; i++) cuts.push(p.random(SPAN));
    cuts.push(SPAN);
    cuts.sort((a, b) => a - b);

    // a cut sits on a node when the fundamental is crossing zero there
    const nodeDist = (x) => {
      const ph = ((k * (x + off)) % Math.PI + Math.PI) % Math.PI;   // 0..PI
      return Math.min(ph, Math.PI - ph) / Math.PI;                  // 0 at a node, .5 mid-unit
    };
    const onNode = (x) => nodeDist(x) < NODE_TOL;

    const frags = [];
    for (let i = 0; i < cuts.length - 1; i++) {
      const xa = cuts[i], xb = cuts[i + 1];
      if (xb - xa < 12) continue;
      const dA = nodeDist(xa), dB = nodeDist(xb);
      // how far out of line it falls = how far from the unit's edge it was cut
      const sag = (dA + dB) * SAG;
      const pts = [];
      for (let x = xa + INSET; x <= xb - INSET; x += STEP) pts.push([x, amp(x) + sag]);
      if (pts.length < 4) continue;
      const closed = dA < NODE_TOL && dB < NODE_TOL;
      frags.push({ xa, xb, pts, closed, openA: dA >= NODE_TOL, openB: dB >= NODE_TOL });
    }
    return { yy, frags, total: frags.reduce((s, f) => s + f.pts.length, 0) };
  };

  // a thread that keeps going the way the fragment was going, and meets nothing
  const thread = (x0, v0, dx, dy0) => {
    p.push();
    p.stroke(214, 218, 226, 58);
    p.strokeWeight(0.5);
    p.noFill();
    p.beginShape();
    let x = x0, v = v0, d = dy0;
    for (let s = 0; s < 900; s++) {
      p.vertex(MARGIN + x, v);
      x += dx * 3;
      v += d * 3;
      d *= 0.998;
      if (MARGIN + x < -20 || MARGIN + x > SIZE + 20) break;
      if (v < -20 || v > SIZE + 20) break;
    }
    p.endShape();
    p.pop();
  };

  p.setup = () => {
    p.createCanvas(SIZE, SIZE);
    const d = new Date();
    const seed = d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
    p.noiseSeed(seed);
    p.randomSeed(seed);
    p.background(9, 9, 11);
    p.strokeCap(p.ROUND);
    p.noFill();
    y = MARGIN + ROW_GAP * 0.6;
    current = makeRow(y);
  };

  p.draw = () => {
    if (phase === 'hold') {
      t++;
      if (t > HOLD_FRAMES) {
        t = 0;
        phase = 'draw';
        y += ROW_GAP;
        if (y > SIZE - MARGIN * 0.6) {
          // the page is full. it dims, it does not clear.
          p.push();
          p.noStroke();
          p.fill(9, 9, 11, 230);
          p.rect(0, 0, SIZE, SIZE);
          p.pop();
          y = MARGIN + ROW_GAP * 0.6;
        }
        current = makeRow(y);
      }
      return;
    }

    // lay the row down a little at a time
    const want = Math.floor((t / DRAW_FRAMES) * current.total);
    let seen = 0;
    p.push();
    for (const f of current.frags) {
      const from = seen;
      seen += f.pts.length;
      if (want <= from) break;
      const upto = Math.min(f.pts.length, want - from);
      if (upto < 2) continue;

      p.stroke(228, 232, 240, 196);
      p.strokeWeight(f.closed ? 2.0 : 1.5);
      p.beginShape();
      for (let i = 0; i < upto; i++) p.vertex(MARGIN + f.pts[i][0], current.yy + f.pts[i][1]);
      p.endShape();

      // only once the fragment is fully down does it declare what it is
      if (upto === f.pts.length && !f.done) {
        f.done = true;
        if (f.closed) {
          // it needs nothing. it shuts — mirrored about the chord between its own ends.
          p.stroke(228, 232, 240, 178);
          p.strokeWeight(1.3);
          const n2 = f.pts.length;
          const A = f.pts[0], B = f.pts[n2 - 1];
          p.beginShape();
          for (let i = n2 - 1; i >= 0; i--) {
            const q = f.pts[i];
            const u = i / (n2 - 1);
            const chord = A[1] + (B[1] - A[1]) * u;
            p.vertex(MARGIN + q[0], current.yy + (2 * chord - q[1]));
          }
          p.endShape();
        } else {
          // it goes on asking
          const n = f.pts.length;
          if (f.openA) {
            const d0 = (f.pts[0][1] - f.pts[1][1]) / STEP;
            thread(f.pts[0][0], current.yy + f.pts[0][1], -STEP, d0 * STEP);
          }
          if (f.openB) {
            const d1 = (f.pts[n - 1][1] - f.pts[n - 2][1]) / STEP;
            thread(f.pts[n - 1][0], current.yy + f.pts[n - 1][1], STEP, d1 * STEP);
          }
        }
      }
    }
    p.pop();

    t++;
    if (t > DRAW_FRAMES) { t = 0; phase = 'hold'; }
  };
}