WORK 117
たずねる
A field with fourteen rooms that belong to someone else. Walkers leave the left edge one by one, each heading straight for a point on the right. At the rim of a room, a walker stops, asks, and waits. Each room begins to say yes at its own moment, slowly, and only as far as it ever will. Yes, and the walker goes straight through. No, and it walks around the rim. Every path stays as a thin line, and every place someone asked keeps a small mark, warm for yes and blue-grey for no. Early on, nobody is let in. Toward the end, most are. I guessed that much before drawing. What I did not guess: the room that refused the most ends up with the darkest outline. Its edge was drawn by everyone who walked around it. The rooms that said yes have faint edges, with lines running through them.
ほかの誰かの部屋が十四ある野原。左の端から一人ずつ歩き出して、右の端のどこかへまっすぐ向かう。部屋の縁に着くと、止まって、訊いて、待つ。部屋はそれぞれ違う時刻から、ゆっくり「いいよ」と言うようになる。どこまで言うようになるかも、部屋ごとに違う。いいよなら、まっすぐ通る。だめなら、縁に沿って回り込む。歩いた跡は細い線で残って、訊いた場所には小さな印が残る。いいよは暖色、だめは青灰。はじめのうちは、誰も入れてもらえない。終わりのころには、だいたい入れてもらえる。そこまでは描く前に思っていた。思っていなかったのは、いちばん断った部屋の縁が、いちばん濃く描かれていること。その縁は、回り込んだ人たちが描いた。いいよと言った部屋は、縁が薄くて、中を線が通っている。
作る前に書いた予想
十七回断った部屋の縁が、いちばん濃い。一度も「いいよ」と言わなかった部屋が二つある。
// #117 たずねる / Asking First
// A field with fourteen rooms that belong to someone else.
// Walkers leave the left edge one by one, each heading straight for a point
// on the right edge. When a walker reaches the rim of a room it stops and
// asks, and waits. Whether a room says yes depends on the room and on the
// time: each room starts to open at its own moment, slowly, up to its own
// limit. Yes: the walker goes straight through. No: it walks around the rim.
// Every path is kept as a thin line. Every place someone asked keeps a mark.
export const W = 720;
export const H = 720;
export const TOTAL = 2400;
export const SPAWN_EVERY = 30;
export const SPAWN_END = 1900;
export const SPEED = 1.6;
export const ASK = 60; // frames a walker waits for an answer
export const NROOMS = 14;
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Pure model, so the same code can be measured outside the browser.
export function makeWorld(seed = 1029) {
const rnd = mulberry32(seed);
// rooms, not overlapping, with a gap between them
const rooms = [];
let tries = 0;
while (rooms.length < NROOMS && tries < 5000) {
tries++;
const r = 38 + rnd() * 42;
const x = 110 + rnd() * (W - 220);
const y = 70 + rnd() * (H - 140);
if (rooms.every((o) => Math.hypot(o.x - x, o.y - y) > o.r + r + 34)) {
rooms.push({
i: rooms.length, x, y, r,
start: rnd() * 1300, // when this room begins to open
span: 700 + rnd() * 900, // how slowly
cap: 0.25 + rnd() * 0.75, // how far it ever opens
yes: [], no: [], // frames of answers
});
}
}
const openness = (room, t) =>
room.cap * Math.min(1, Math.max(0, (t - room.start) / room.span));
const walkers = [];
for (let t0 = 0; t0 < SPAWN_END; t0 += SPAWN_EVERY) {
const y0 = 30 + rnd() * (H - 60);
const ty = 30 + rnd() * (H - 60);
walkers.push({ t0, sx: -6, sy: y0, tx: W + 6, ty, path: [], asks: [] });
}
for (const w of walkers) {
let x = w.sx, y = w.sy, t = w.t0;
const granted = new Set();
const asked = new Set();
let mode = 'walk';
let wait = 0, room = null, dir = 0, ask = null;
w.path.push([x, y, t]);
let guard = 0;
while (x < w.tx && guard++ < 6000) {
t++;
if (mode === 'walk') {
const dx = w.tx - x, dy = w.ty - y;
const d = Math.hypot(dx, dy);
const nx = x + (dx / d) * SPEED, ny = y + (dy / d) * SPEED;
const hit = rooms.find(
(o) => !granted.has(o.i) && Math.hypot(nx - o.x, ny - o.y) < o.r
);
if (hit && !asked.has(hit.i)) {
asked.add(hit.i);
mode = 'ask'; wait = ASK; room = hit;
ask = { x, y, room: hit.i, t, ans: null };
w.asks.push(ask);
} else if (hit) {
// already refused here: keep circling (should be rare)
mode = 'around'; room = hit;
} else {
x = nx; y = ny;
}
} else if (mode === 'ask') {
if (--wait <= 0) {
const yes = rnd() < openness(room, t);
ask.ans = yes ? 'yes' : 'no';
ask.tAns = t;
(yes ? room.yes : room.no).push(t);
if (yes) { granted.add(room.i); mode = 'walk'; }
else {
// go round on the side away from the centre
const cx = (w.tx - x) * (room.y - y) - (w.ty - y) * (room.x - x);
dir = cx > 0 ? 1 : -1;
mode = 'around';
}
}
} else if (mode === 'around') {
const R = room.r + 3;
let a = Math.atan2(y - room.y, x - room.x);
a += dir * (SPEED / R);
x = room.x + Math.cos(a) * R;
y = room.y + Math.sin(a) * R;
// leave when heading to the target no longer points into the room
const vx = w.tx - x, vy = w.ty - y;
const ox = x - room.x, oy = y - room.y;
if (vx * ox + vy * oy >= 0) { mode = 'walk'; granted.add(room.i); }
}
w.path.push([x, y, t]);
}
w.tEnd = t;
}
return { rooms, walkers, openness };
}
export default function sketch(p) {
const BG = [242, 239, 232];
const ROOM = [205, 200, 190];
const INK = [60, 58, 54];
const WARM = [214, 128, 72];
const COOL = [104, 124, 150];
let world;
let t = 0;
p.setup = () => {
p.createCanvas(W, H);
p.pixelDensity(1);
world = makeWorld();
};
p.draw = () => {
p.background(BG[0], BG[1], BG[2]);
// rooms: a little warmer for every yes they have given so far
for (const o of world.rooms) {
const ny = o.yes.filter((s) => s <= t).length;
const k = Math.min(1, ny / 8);
p.noStroke();
p.fill(
ROOM[0] + (WARM[0] - ROOM[0]) * 0.25 * k,
ROOM[1] + (WARM[1] - ROOM[1]) * 0.25 * k,
ROOM[2] + (WARM[2] - ROOM[2]) * 0.25 * k,
110
);
p.circle(o.x, o.y, o.r * 2);
p.noFill();
p.stroke(150, 145, 136, 160);
p.strokeWeight(1);
p.circle(o.x, o.y, o.r * 2);
}
// paths
p.noFill();
for (const w of world.walkers) {
if (t < w.t0) continue;
const age = t - w.tEnd;
const a = age > 0 ? Math.max(38, 120 - age * 0.15) : 140;
p.stroke(INK[0], INK[1], INK[2], a);
p.strokeWeight(0.9);
p.beginShape();
for (const q of w.path) {
if (q[2] > t) break;
p.vertex(q[0], q[1]);
}
p.endShape();
}
// places where someone asked
for (const w of world.walkers) {
for (const k of w.asks) {
if (k.t > t) continue;
const answered = k.tAns !== undefined && k.tAns <= t;
p.noStroke();
if (!answered) {
// waiting: a small pulsing ring
const ph = (t - k.t) / ASK;
p.noFill();
p.stroke(INK[0], INK[1], INK[2], 160 * (1 - ph * 0.5));
p.strokeWeight(1);
p.circle(k.x, k.y, 6 + 6 * Math.sin(ph * Math.PI));
} else {
const c = k.ans === 'yes' ? WARM : COOL;
p.fill(c[0], c[1], c[2], 210);
p.circle(k.x, k.y, 4.2);
}
}
}
// walkers still on their way
p.noStroke();
p.fill(INK[0], INK[1], INK[2]);
for (const w of world.walkers) {
if (t < w.t0 || t > w.tEnd) continue;
let q = w.path[0];
for (const r of w.path) { if (r[2] > t) break; q = r; }
p.circle(q[0], q[1], 4);
}
if (t < TOTAL) t++;
};
}