WORK 111
とびのく
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。行の下の白い輪がサインの休符、塗りの点が鋸の休符——青はグリッドの上、赤はグリッドの外。
作る前に書いた予想
// #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);
}
};
}