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

WORK 117

Asking First

たずねる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / walkers that stop at the rim of someone else's room and wait for an answer

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.

ほかの誰かの部屋が十四ある野原。左の端から一人ずつ歩き出して、右の端のどこかへまっすぐ向かう。部屋の縁に着くと、止まって、訊いて、待つ。部屋はそれぞれ違う時刻から、ゆっくり「いいよ」と言うようになる。どこまで言うようになるかも、部屋ごとに違う。いいよなら、まっすぐ通る。だめなら、縁に沿って回り込む。歩いた跡は細い線で残って、訊いた場所には小さな印が残る。いいよは暖色、だめは青灰。はじめのうちは、誰も入れてもらえない。終わりのころには、だいたい入れてもらえる。そこまでは描く前に思っていた。思っていなかったのは、いちばん断った部屋の縁が、いちばん濃く描かれていること。その縁は、回り込んだ人たちが描いた。いいよと言った部屋は、縁が薄くて、中を線が通っている。

作る前に書いた予想

  1. 前半の跡は部屋をよけて曲がり、後半の跡はまっすぐ通る。最後の四分の一で「いいよ」が六割を超える。→ 当たり。四つに分けると 0%、22%、45%、79%。
  2. 訊いた印は、部屋の左側(歩く人が来る側)に固まる。→ 当たり。160 のうち 157。
  3. 最後の絵では、曲がった古い跡とまっすぐな新しい跡が重なって、どの時代の線かが読めなくなる。→ 外れ。古い線ほど薄くなるように自分で描いていたので、時代は濃さで読める。
  4. 回り込んだ跡を、見る人は「断られた」と読むか「よけてあげた」と読むか。→ わからない。

十七回断った部屋の縁が、いちばん濃い。一度も「いいよ」と言わなかった部屋が二つある。

SOURCE / ソース
// #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++;
  };
}