WORK 92
あおる
A chart recorder draws one line across the paper, left to right, row after row. The height of the line is how alive a cold old engine is. Left alone it sinks, and if it reaches the dotted line it stops. Two people are keeping it running. Neither has a clock. They listen, a little late, and when it sounds too low they blip the throttle. When one of them does, the other hears it and holds back. Each blip leaves a small mark above the row; a hand that reached and then held back leaves a fainter one. As the engine warms, the height it can hold by itself rises, and the blips come further apart, until no one needs to touch it. The last rows are a line running on its own. After the warm-up of old racing engines, kept from stalling by mechanics who fan the throttle by ear, and after Hosoi Wakizo's short poem "Working Machine" (1923), whose machine never tires and so never needs anyone to listen to it.
チャート記録計が、紙の上に一本の線を、左から右へ、行を折り返しながら書いていく。線の高さは、冷えた古いエンジンの元気。放っておくと沈んで、点線まで沈んだら止まる。二人がそれを生かしている。どちらも時計を持っていない。少し遅れて聴いていて、沈みすぎたと聞こえたら、煽る。一人が煽ると、もう一人はその音を聞いて待つ。煽った印が行の上に小さく残り、手を伸ばしてやめた方は、薄い印を残す。エンジンが温まると、ひとりで立っていられる高さが上がって、煽る間隔が開いていき、やがて誰も触らなくなる。最後の行は、ひとりで回っている線。古いレーシングエンジンの暖気——止まらないように、メカニックが耳で聴きながら煽りつづけるところ——から。それと、細井和喜蔵の短い詩「作業機械」(1923)。その機械は疲れないので、誰にも聴いてもらう必要がない。
// あおる / Blipping
//
// チャート記録計みたいに、紙の上を一本の線が左から右へ、行を折り返しながら書いていく。
// 線の高さは、冷えた古いエンジンの元気。放っておくと沈む。下の点線まで沈んだら止まる。
//
// 二人の手がいる。どちらも時計を持っていない。
// 耳で聴いていて(少し遅れて)、沈みすぎたと聞こえたら、煽る。
// 先に煽った方の音が聞こえたら、もう一人は待つ。
// 煽った印が、行の上に小さく残る。二人で二色。
// 手を伸ばして、もう鳴っていたのでやめたときは、薄い印。
//
// エンジンはだんだん温まる。温まると、自分で立っていられる高さが上がる。
// そこまで来たら、もう誰も煽らない。
//
// 古いレーシングエンジンの暖気は、止まらないように、メカニックが耳で聴きながら煽りつづける。
// 細井和喜蔵「作業機械」(1923)の機械は疲れないので、誰にも聴いてもらう必要がない。
export default function sketch(p) {
const W = 720, H = 720;
const MX = 36, TOP = 54, ROW_H = 88, ROWS = 7;
const ROW_W = W - 2 * MX; // 1 フレーム 1px
const PAPER = ROWS * ROW_W; // 一枚ぶんのフレーム数
const REST = 240; // 紙を替えるまでの間
// エンジン
const K = 0.021; // 冷えているときの沈み方
const STALL = 0.16; // ここまで沈んだら止まる
const SELF_IDLE = 0.46; // 温まりきったら、ひとりで立っていられる高さ
const WARM_FRAMES = 5000; // 温まるまで
let E, warm, frame, dead, history, marks, hands, pulse, sheet;
function reset() {
sheet = (sheet ?? -1) + 1;
p.noiseSeed(20261003 + sheet * 31);
p.randomSeed(20261003 + sheet * 31);
E = 0.9; warm = 0; frame = 0; dead = false;
history = new Float32Array(PAPER + 1);
marks = [];
pulse = 0;
hands = [
{ ear: 9 + p.random(-2, 2), react: 12 + p.random(-3, 3), col: [178, 74, 52], wait: 0, base: 0.31, nz: 0 },
{ ear: 11 + p.random(-2, 2), react: 14 + p.random(-3, 3), col: [52, 96, 150], wait: 0, base: 0.30, nz: 50 },
];
}
p.setup = () => {
p.createCanvas(W, H);
p.pixelDensity(1);
reset();
};
function step() {
warm = Math.min(1, frame / WARM_FRAMES);
const w = warm * warm * (3 - 2 * warm);
const target = SELF_IDLE * w;
const k = K * (1 - 0.35 * w);
E += -k * (E - target);
if (pulse > 0) { E += pulse * 0.25; pulse *= 0.75; if (pulse < 0.005) pulse = 0; }
if (E < STALL) { dead = true; }
// 二人の手。聴こえているのは、少し前の音。
for (let i = 0; i < hands.length; i++) {
const h = hands[i];
if (h.wait > 0) { h.wait--; continue; }
const heardAt = frame - Math.round(h.ear);
if (heardAt < 0) continue;
const heard = history[heardAt];
const T = h.base + 0.05 * (p.noise(h.nz + frame * 0.004) - 0.5);
if (heard < T) {
// 聞こえた。手を伸ばす。
h.wait = Math.round(h.react);
h.pending = Math.round(h.react);
h.heardThen = heard;
}
}
for (let i = 0; i < hands.length; i++) {
const h = hands[i];
if (h.pending !== undefined) {
h.pending--;
if (h.pending <= 0) {
h.pending = undefined;
// 押す直前、もう一人の音が聞こえていたら、やめる。
const heardAt = frame - Math.round(h.ear);
const someoneElse = heardAt > 0 && history[heardAt] > h.heardThen + 0.02;
if (!someoneElse && pulse === 0) {
pulse = 0.55 * (1.05 - E);
marks.push({ f: frame, who: i, held: false });
} else {
// 手は伸びていた。でも、もう鳴っていたので、やめた。
marks.push({ f: frame, who: i, held: true });
}
// 自分で押した音が耳に届くまでは、待つ。
h.wait = Math.round(h.ear) + 4;
}
}
}
history[frame] = E;
frame++;
}
function rowXY(f) {
const r = Math.floor(f / ROW_W);
const x = MX + (f % ROW_W);
const base = TOP + r * ROW_H + ROW_H - 18;
return [x, base, r];
}
p.draw = () => {
if (frame < PAPER && !dead) step();
else if (frame < PAPER + REST) frame++;
else reset();
p.background(241, 236, 224);
// 罫線
for (let r = 0; r < ROWS; r++) {
const base = TOP + r * ROW_H + ROW_H - 18;
p.stroke(200, 192, 176); p.strokeWeight(1);
p.line(MX, base, MX + ROW_W, base);
p.stroke(214, 206, 190);
for (let x = MX; x < MX + ROW_W; x += 60) p.line(x, base - 3, x, base);
// 止まる線
p.stroke(190, 160, 150);
p.drawingContext.setLineDash([2, 4]);
const sy = base - STALL * 60;
p.line(MX, sy, MX + ROW_W, sy);
p.drawingContext.setLineDash([]);
}
const upto = Math.min(frame, PAPER);
// 線。ふるえは元気の大きさに比例。
p.noFill();
p.stroke(38, 34, 30); p.strokeWeight(1.1);
let prevRow = -1;
for (let f = 0; f < upto; f++) {
const [x, base, r] = rowXY(f);
if (r !== prevRow) { if (prevRow >= 0) p.endShape(); p.beginShape(); prevRow = r; }
const e = history[f];
const jit = (p.noise(f * 0.9, 3) - 0.5) * 6 * e;
p.vertex(x, base - e * 60 + jit);
}
if (prevRow >= 0) p.endShape();
// 手の印
p.noStroke();
for (const m of marks) {
if (m.f >= upto) continue;
const [x, base] = rowXY(m.f);
const c = hands[m.who].col;
const y = base - 66 - m.who * 6;
if (m.held) {
p.fill(c[0], c[1], c[2], 70);
p.rect(x - 0.5, y + 2, 1.2, 3);
} else {
p.fill(c[0], c[1], c[2], 210);
p.rect(x - 0.5, y, 1.6, 5);
}
}
// ペン先
if (frame < PAPER && !dead) {
const [x, base] = rowXY(upto - 1 < 0 ? 0 : upto - 1);
const e = history[Math.max(0, upto - 1)];
p.fill(38, 34, 30); p.noStroke();
p.circle(x, base - e * 60, 3);
}
};
}