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

WORK 111

Jumping Back

とびのく

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / one held chord, three sawtooth auto-pans in series, sine rests as rings

The same held chord and the same three auto-pans in series as #109, Rests Nobody Wrote: two on the grid (quarter notes and dotted eighths), one free (1.13 seconds). Only the wave is changed. A sine pan swings; a sawtooth pan ramps from hard left to hard right and then jumps straight back. The free pan is at depth zero for the first four rows and full depth after. Filled dots are the sawtooth's rests, where the level falls below 0.05. Rings mark where the sine version would have rested. In #109 the two grid pans alone already made rests, because a sine lingers near its edges: the deepest silences came where both pans were slightly away from hard left and hard right at once. A sawtooth never lingers. It reaches the edge and leaves in the same instant. So in the first four rows there are rings and no dots at all. With the free pan turned up, nine rests remain out of eighty-two. Before drawing I guessed they would belong to the free pan's jump, since they come about every 1.13 seconds. That was half wrong. They sit where a grid pan jumps, within about 15 ms of the sixteenth line, and the free pan only decides which of those jumps can close. They come every four and a half seconds, a little more than one row, so they walk across the page as a diagonal nobody drew.

#109「やすむ」と同じ和音、同じ三つの直列オートパン。グリッドに二つ(四分音符と付点八分)、自由に一つ(1.13秒)。変えたのは波だけ。サインのパンは揺れる。鋸のパンは、左いっぱいから右いっぱいまでまっすぐ上って、それから一気に戻る。自由な一つの深さは、はじめの四行は 0、そのあとはいっぱい。塗りの点は鋸の休符(振幅が 0.05 を下回ったところ)。白い輪は、サインだったら休符があった場所。#109 では、グリッドの二つだけでもう休符ができていた。サインは端の近くでぐずぐずするから。いちばん深い沈黙は、二つがどちらも左右の端から少しずつ外れているところにあった。鋸はぐずぐずしない。端に着いた瞬間に、もう離れている。だから最初の四行には輪だけがあって、点は一つもない。自由な一つを上げると、八十二あった休符のうち九つだけが残る。描く前、これは自由な一つの跳びのものだと思った。だいたい 1.13 秒おきに来たから。半分外れた。点が座っているのは、グリッドの二つのどちらかが跳ぶところ——十六分の線から 15ms 以内。自由な一つが決めているのは、どの跳びが閉じられるか、だけだった。点は四秒半おきに来る。一行より少し長い。だから点は、誰も引いていない斜めの線になって、ページを横切っていく。

左の小さなつまみが、自由なオートパンの深さ(はじめの四行は 0、そのあとはいっぱい)。帯の上が L、下が R。行の下の白い輪がサインの休符、塗りの点が鋸の休符——青はグリッドの上、赤はグリッドの外。

作る前に書いた予想

  1. グリッドの二つだけでは、鋸は休符を作らない。→ 当たり。 いちばん深い谷でも 0.215(サインは 0.028)。いちばん自信がなかった予想だった。
  2. 休符は、跳ぶ瞬間に張りついて、グリッドの線のすぐ上に来る。→ 当たり。 中央値はおよそ 8ms(サインは 21ms)。
  3. (式だけ回したあとで)跳んでいるのは自由な一つ。→ 外れ。 1.13 秒ごとに来るように見えたのは、自由な一つが「どの跳びを閉じるか」を選んでいたから。跳んでいるのはグリッドの二つ。
  4. 帯の形が、崖と坂の非対称になる。→ 当たり。
  5. 輪と点が同じ行にあるとき、目はどっちを休符として読むか。→ 点。輪は、もう無いものとして読める。
  6. 自由な一つを上げると、跳びの二回に一回くらいが休符になる。→ 外れ。 九つだけ。四回に一回くらい。
  7. 点と十六分の線の距離は、跳ぶたびに少しずつ変わっていって、八行目あたりで一度 線に戻る。→ 描いてすぐは「外れ、ずっと 15ms 以内」と書いた。あとで点の色を数え直したら、前半は 3〜5ms、後半は 13〜15ms だった。少しずつ離れていく、は当たり。戻る、は外れ。 はじめは幅の物差しで採点していて、言っていたのは向きの方だった。
SOURCE / ソース
// #111 とびのく / Off the Line
// Same chord, same three auto-pans in series as #109, but every pan is a sawtooth:
// it ramps from hard left to hard right and then jumps straight back.
// Rows 1-4: the free pan C sits at depth 0. Rows 5-12: depth 1.
// Filled dots: rests of the sawtooth version. Hollow rings: where the sine version
// of #109 (with the same knob schedule) would have rested.

export default function sketch(p) {
  const W = 720;
  const H = 720;
  const ROWS = 12;
  const SEC_PER_ROW = 4;          // two bars at 120 BPM
  const BEAT = 0.5;
  const MX = 52;
  const ROW_W = W - MX - 28;
  const TOP = 34;
  const ROW_H = (H - TOP - 26) / ROWS;
  const HH = ROW_H * 0.34;        // half-height of the band
  const PX_PER_SEC = ROW_W / SEC_PER_ROW;
  const SUB = 8;                  // sub-samples per pixel column
  const THRESH = 0.05;
  const GRID_TOL = BEAT / 4 / 16; // within 1/64 of a beat of a sixteenth line

  const PA = 0.5, PB = 0.375, PC = 1.13;

  let layer;
  let col = 0;                    // global column index
  const TOTAL_COLS = ROWS * ROW_W;
  const gap = { saw: { inGap: false, start: 0 }, sin: { inGap: false, start: 0 } };
  const rests = [];               // {t, onGrid, saw}

  function depthC(t) {
    const row = Math.floor(t / SEC_PER_ROW);
    return row < 4 ? 0 : 1;
  }

  function wave(t, period, saw) {
    const x = (t / period) % 1;
    return saw ? 2 * x - 1 : Math.sin(2 * Math.PI * x);
  }

  function theta(t, period, depth, saw) {
    return Math.PI / 4 + depth * (Math.PI / 4) * wave(t, period, saw);
  }

  function gains(t, saw = true) {
    const a = theta(t, PA, 1, saw);
    const b = theta(t, PB, 1, saw);
    const c = theta(t, PC, depthC(t), saw);
    const L = Math.cos(a) * Math.cos(b) * Math.cos(c);
    const R = Math.sin(a) * Math.sin(b) * Math.sin(c);
    return [L, R];
  }

  function rowY(row) {
    return TOP + row * ROW_H + ROW_H * 0.40;
  }

  p.setup = () => {
    p.createCanvas(W, H);
    p.pixelDensity(1);
    layer = p.createGraphics(W, H);
    layer.pixelDensity(1);
    layer.background(18, 22, 34);
    // sixteenth grid, faint; beats a little stronger
    for (let r = 0; r < ROWS; r++) {
      const cy = rowY(r);
      for (let s = 0; s <= SEC_PER_ROW / (BEAT / 4); s++) {
        const x = MX + (s * BEAT) / 4 * PX_PER_SEC;
        const strong = s % 4 === 0;
        layer.stroke(120, 140, 180, strong ? 46 : 20);
        layer.line(x, cy - HH - 3, x, cy + HH + 3);
      }
      layer.stroke(120, 140, 180, 30);
      layer.line(MX, cy, MX + ROW_W, cy);
    }
  };

  function drawKnob(g, r) {
    const cy = rowY(r);
    const cx = 24;
    const d = depthC(r * SEC_PER_ROW + SEC_PER_ROW - 0.001);
    const d0 = depthC(r * SEC_PER_ROW);
    g.noFill();
    g.stroke(120, 140, 180, 70);
    g.strokeWeight(1);
    g.arc(cx, cy, 18, 18, p.PI * 0.75, p.PI * 2.25);
    const ang = p.PI * 0.75 + ((d0 + d) / 2) * p.PI * 1.5;
    g.stroke(236, 226, 205, 200);
    g.strokeWeight(1.5);
    g.line(cx, cy, cx + Math.cos(ang) * 8, cy + Math.sin(ang) * 8);
    g.strokeWeight(1);
  }

  function checkRest(kind, t, amp) {
    const g = gap[kind];
    if (amp < THRESH && !g.inGap) { g.inGap = true; g.start = t; }
    if (amp >= THRESH && g.inGap) {
      g.inGap = false;
      const tc = (g.start + t) / 2;
      const six = BEAT / 4;
      const off = Math.abs(tc / six - Math.round(tc / six)) * six;
      const onGrid = off <= GRID_TOL;
      rests.push({ t: tc, onGrid, saw: kind === 'saw' });
      const rr = Math.floor(tc / SEC_PER_ROW);
      const rx = MX + (tc - rr * SEC_PER_ROW) * PX_PER_SEC;
      const ry = rowY(rr) + HH + 7;
      if (kind === 'sin') {
        layer.noFill();
        layer.stroke(150, 160, 190, 120);
        layer.strokeWeight(1);
        layer.circle(rx, ry, 7);
      } else {
        layer.noStroke();
        if (onGrid) layer.fill(140, 170, 220, 235);
        else layer.fill(232, 120, 96, 240);
        layer.circle(rx, ry, 5);
      }
    }
  }

  function step() {
    const r = Math.floor(col / ROW_W);
    const x = MX + (col % ROW_W) + 0.5;
    const cy = rowY(r);
    if (col % ROW_W === 0) drawKnob(layer, r);
    let maxL = 0, maxR = 0;
    for (let k = 0; k < SUB; k++) {
      const t = (col + k / SUB) / PX_PER_SEC;
      const [L, R] = gains(t);
      maxL = Math.max(maxL, L);
      maxR = Math.max(maxR, R);
      checkRest('saw', t, Math.hypot(L, R));
      const [sL, sR] = gains(t, false);
      checkRest('sin', t, Math.hypot(sL, sR));
    }
    layer.stroke(236, 226, 205, 210);
    layer.line(x, cy - Math.min(1, maxL / 0.75) * HH, x, cy);
    layer.stroke(206, 214, 236, 190);
    layer.line(x, cy, x, cy + Math.min(1, maxR / 0.75) * HH);
    col++;
  }

  p.draw = () => {
    const perFrame = 3;
    for (let i = 0; i < perFrame && col < TOTAL_COLS; i++) step();
    p.image(layer, 0, 0);
    if (col < TOTAL_COLS) {
      const r = Math.floor(col / ROW_W);
      const x = MX + (col % ROW_W);
      p.stroke(236, 226, 205, 90);
      p.line(x, rowY(r) - HH - 4, x, rowY(r) + HH + 4);
    }
  };
}