WORK 107
ぬける
Every morning a vendor walks down one alley and calls three notes. The middle note starts a major third above the other two, and each morning it sinks a little, unevenly, without anyone deciding it. The windows on both sides light up when the call still sounds like "wake up" to them. A window that was woken yesterday will still wake to a slightly flatter call; a window that stayed asleep forgets a little more. At the bottom, a ledger keeps one column per morning: the shape of the call, and how many windows woke. For a long time nothing in the alley changes. The call is already half gone before the first window stays dark. By the time every window stays dark, the vendor is still walking, and still calling.
毎朝、売り手がひとつの路地を通って、三つの音で呼ぶ。まんなかの音は、はじめ両端より長三度高い。それが朝ごとに少しずつ、ばらばらに、誰が決めたわけでもなく下がっていく。両側の窓は、その声がまだ「起きろ」に聞こえたときに灯る。きのう起こされた窓は、少し平らな声でもまだ起きる。起きなかった窓は、少しずつ忘れていく。下の帳簿には、朝ごとに一列、その朝の声の形と、起きた窓の数が残る。しばらくのあいだ、路地では何も変わらない。最初の窓が暗いままになるころには、声はもう半分ぬけている。どの窓も起きなくなっても、売り手は通りつづけて、呼びつづけている。
種:寺田寅彦「物売りの声」(昭和十年)の一行目。豆腐屋のラッパが、中に長三度くらい高い音をひとつはさんで「起きろ」と聞こえていたのが、「ただひと色の音の系列」になった。
// #107 ぬける / A Note Drops Out
// Every morning a vendor walks one alley and calls three notes.
// The middle note starts a major third (4 semitones) above the others,
// and drifts down a little each morning, unevenly.
// A window wakes (lights) if the call still sounds like "wake up" to it:
// how high the middle note is, plus habit — a window woken recently
// still wakes to a slightly flatter call.
// At the bottom, a ledger: one column per morning, the shape of the call
// that morning and how many windows woke.
export default function sketch(p) {
const W = 720;
const H = 720;
const DAY = 200; // frames per morning
const MORNINGS = 72; // ledger columns
const ALLEY_Y = 250;
const ROW_TOP = 128;
const ROW_BOT = 372;
const N = 14; // windows per side
let windows = [];
let interval = 4.0; // semitones above the outer notes
let ledger = []; // { iv, lit }
let pulses = [];
let day = -1;
let litToday = 0;
p.setup = () => {
p.createCanvas(W, H);
p.randomSeed(107);
p.noiseSeed(107);
p.pixelDensity(1);
for (let side = 0; side < 2; side++) {
for (let i = 0; i < N; i++) {
const x = 46 + i * ((W - 92) / (N - 1)) + p.random(-6, 6);
windows.push({
x,
y: side === 0 ? ROW_TOP : ROW_BOT,
th: p.random(1.6, 3.2), // how high the middle note has to be
habit: 1.0, // starts used to being woken
lit: 0, // 0..1 brightness
woke: false,
heard: false,
});
}
}
globalThis.__nukeruLedger = ledger;
};
function startMorning() {
if (day >= 0) {
ledger.push({ iv: interval, lit: litToday });
// habit: woken windows get more used to waking, others forget
for (const w of windows) {
if (w.woke) w.habit = Math.min(1, w.habit + 0.2);
else if (w.heard) w.habit *= 0.8;
}
// the middle note drifts down, unevenly; nobody decides this
interval = Math.max(0, interval - 0.065 + p.randomGaussian(0, 0.12));
}
day++;
litToday = 0;
for (const w of windows) {
w.woke = false;
w.heard = false;
}
}
function vendorX(t) {
return p.map(t, 0, DAY * 0.85, -20, W + 20, true);
}
function call(x) {
// three notes, a short gap between each
for (let k = 0; k < 3; k++) {
pulses.push({ x, born: p.frameCount + k * 7, k, iv: interval });
}
}
function hear(w) {
if (w.heard) return;
w.heard = true;
const sounds = interval + w.habit * 1.4 + p.randomGaussian(0, 0.25);
if (sounds > w.th) {
w.woke = true;
litToday++;
}
}
function drawAlley(t) {
// dawn: dark to pale over the morning
const u = t / DAY;
const c1 = p.lerpColor(p.color(22, 26, 40), p.color(70, 78, 98), u);
p.background(c1);
p.noStroke();
p.fill(p.lerpColor(p.color(34, 34, 40), p.color(92, 90, 92), u));
p.rect(0, ALLEY_Y - 40, W, 80);
// houses
for (const w of windows) {
const top = w.y < ALLEY_Y;
p.fill(p.lerpColor(p.color(40, 42, 56), p.color(98, 96, 108), u));
p.rect(w.x - 20, top ? w.y - 70 : w.y - 24, 40, 94, 2);
// window
const off = p.lerpColor(p.color(28, 32, 48), p.color(60, 66, 86), u);
const on = p.color(255, 206, 120);
p.fill(p.lerpColor(off, on, w.lit));
p.rect(w.x - 9, w.y - 9, 18, 18, 1);
if (w.lit > 0.05) {
p.fill(255, 200, 110, 40 * w.lit);
p.rect(w.x - 14, top ? w.y + 9 : w.y - 26, 28, 17);
}
}
}
function drawPulses() {
p.noFill();
for (let i = pulses.length - 1; i >= 0; i--) {
const s = pulses[i];
const age = p.frameCount - s.born;
if (age < 0) continue;
const r = age * 4.2;
if (r > 190) {
pulses.splice(i, 1);
continue;
}
// the middle note is drawn brighter the higher it is
const hi = s.k === 1 ? p.constrain(s.iv / 4, 0, 1) : 0;
const a = (1 - r / 190) * (70 + 120 * hi);
p.stroke(p.lerpColor(p.color(170, 180, 200), p.color(255, 236, 190), hi), a);
p.strokeWeight(s.k === 1 ? 1.2 + hi * 1.3 : 1.2);
p.circle(s.x, ALLEY_Y, r * 2);
// windows hear the middle note pass
if (s.k === 1) {
for (const w of windows) {
const d = p.dist(s.x, ALLEY_Y, w.x, w.y);
if (Math.abs(d - r) < 2.5) hear(w);
}
}
}
}
function drawVendor(t) {
const x = vendorX(t);
p.noStroke();
p.fill(210, 200, 180);
p.circle(x, ALLEY_Y, 9);
p.fill(150, 140, 120);
p.rect(x - 12, ALLEY_Y + 6, 24, 5, 2);
}
function drawLedger() {
const top = 470;
p.noStroke();
p.fill(236, 232, 222);
p.rect(0, top, W, H - top);
p.fill(120, 112, 100);
p.textFont('monospace');
p.textSize(10);
p.text('mornings →', 18, top + 18);
const colW = (W - 36) / MORNINGS;
for (let i = 0; i < ledger.length && i < MORNINGS; i++) {
const e = ledger[i];
const cx = 18 + i * colW + colW / 2;
// the shape of the call: three notes, middle raised by the interval
const base = top + 80;
const rise = e.iv * 9;
p.stroke(60, 56, 52, 200);
p.strokeWeight(1);
p.line(cx - colW * 0.36, base, cx, base - rise);
p.line(cx, base - rise, cx + colW * 0.36, base);
p.noStroke();
p.fill(60, 56, 52);
p.circle(cx - colW * 0.36, base, 2);
p.circle(cx, base - rise, 2);
p.circle(cx + colW * 0.36, base, 2);
// how many windows woke
const h = (e.lit / windows.length) * 110;
p.fill(214, 150, 60, 210);
p.rect(cx - colW * 0.3, top + 100, colW * 0.6, h);
}
}
p.draw = () => {
const t = (p.frameCount - 1) % DAY;
if (t === 0) startMorning();
if (t % 34 === 6 && t < DAY * 0.85) call(vendorX(t));
for (const w of windows) {
const target = w.woke ? 1 : 0;
w.lit += (target - w.lit) * 0.05;
}
drawAlley(t);
drawPulses();
drawVendor(t);
drawLedger();
};
}