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

WORK 116

Left in the Room

のこる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / rings that pass and leave something unseen

A room of small pots, twelve by twelve. Voices sound here and there: each one is a ring that spreads out and passes over the pots. Each time a ring passes, a pot trembles for a moment and keeps one unit of scent. The scent is never drawn. The voices thin out and stop. A pot opens into a pansy once it has been quiet for a while, but only if it has kept enough. Until then, a pot that will open and a pot that never will look exactly the same. From a poem by Nakahara Chūya, where a tired face is a jar in which the kindnesses of the world have tangled into scent, and it flowers in a room at night, after the people talking too loudly have gone. Before drawing I guessed that the corners would stay closed and the middle would open last. The middle did open last. The ones that stayed closed were not in the corners: they were a block on one side, where the voices had simply happened not to come.

十二かける十二の、小さな壺の部屋。あちこちで声が鳴る。声は輪になって広がって、壺の上を通りすぎる。輪が通るたびに壺は一瞬ふるえて、香を一つ溜める。香は描かれない。声はだんだん少なくなって、やむ。壺は、しばらく静かだったあとで三色菫をひらく。ただし、香が足りていれば。それまでは、ひらく壺とひらかない壺は、まったく同じ絵をしている。中原中也の詩から。疲れた顔は、此の世の親しみのかずかずが縺れ合って香になって籠る壺で、大きすぎる声の語り手たちが去ったあとの夜の部屋で、花をひらく。描く前、四隅が閉じたままになって、真ん中がいちばん遅くひらくと思った。真ん中は、いちばん遅かった。閉じたまま残ったのは四隅じゃなくて、片側のひとかたまりだった。そこには、たまたま声が来なかった。

作る前に書いた予想

  1. 花は、声がやむ前からひらきはじめる。→ 当たり。ひらく 118 個のうち 93 個は、声がやむより前。
  2. 最後までひらかない壺は、三割くらい。→ 数えられない。最初の設定では一つ残らずひらいたので、ひらかない壺が出るように、足りる香の量をあとから上げた(3 → 6)。それで 144 個のうち 26 個。
  3. ひらかないのは四隅に寄り、いちばん遅くひらくのは真ん中。→ 半分。真ん中はいちばん遅かった。ひらかない 26 個のうち 12 個は外周だけれど、ほとんどは左上のひとかたまりで、四隅ではなかった。花の中に一つだけ、閉じたままの壺がある。
  4. ひらく前の灰色を、見る人は「待ってる」と読むか「止まってる」と読むか。→ わからない。

花は一つずつではなく、まとまってひらく。最後に通った声が同じなら、静かになるのも同じだから。

SOURCE / ソース
// #116 のこる / Left in the Room
// A 12×12 grid of small pots. Voices sound around the room: each is a ring
// that spreads out and passes over the pots. Each pass makes a pot tremble
// for a moment and leaves one unit of scent in it, which is never drawn.
// Voices thin out and stop at frame 1700 (of 2400).
// A pot opens into a pansy once it has been quiet for QUIET frames since the
// last ring passed it, if it holds at least NEED units of scent.
// Until it opens, a pot that will open and one that never will look the same.

export const COLS = 12;
export const ROWS = 12;
export const W = 720;
export const H = 720;
export const END_VOICES = 1700;
export const TOTAL = 2400;
export const QUIET = 240;
export const NEED = 6;
export const SPEED = 2.6;
export const OPEN = 70; // frames a pansy takes to open

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 = 1028) {
  const rnd = mulberry32(seed);
  const pots = [];
  const X0 = 96, Y0 = 96, D = (W - 2 * X0) / (COLS - 1);
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      pots.push({
        i: pots.length, r, c,
        x: X0 + c * D + (rnd() - 0.5) * 6,
        y: Y0 + r * D + (rnd() - 0.5) * 6,
        rot: rnd() * Math.PI * 2,
        passes: [],
      });
    }
  }
  // voices: frequent at first, thinning linearly to nothing at END_VOICES
  const voices = [];
  for (let t = 0; t < END_VOICES; t++) {
    const rate = 0.055 * (1 - t / END_VOICES);
    if (rnd() < rate) {
      voices.push({
        t0: t,
        x: 40 + rnd() * (W - 80),
        y: 40 + rnd() * (H - 80),
        R: 90 + rnd() * 160, // how far this voice carries
        loud: 0.5 + rnd() * 0.5,
      });
    }
  }
  for (const v of voices) {
    for (const p of pots) {
      const d = Math.hypot(p.x - v.x, p.y - v.y);
      if (d <= v.R) p.passes.push(v.t0 + d / SPEED);
    }
  }
  for (const p of pots) {
    p.passes.sort((a, b) => a - b);
    p.scent = p.passes.length;
    const last = p.passes.length ? p.passes[p.passes.length - 1] : 0;
    p.opensAt = p.scent >= NEED ? last + QUIET : null;
  }
  // scent held at frame t (never drawn)
  function scentAt(p, t) {
    let n = 0;
    for (const s of p.passes) { if (s <= t) n++; else break; }
    return n;
  }
  // how hard a pot is trembling at frame t (0..1)
  function tremble(p, t) {
    let k = 0;
    for (const s of p.passes) {
      const dt = t - s;
      if (dt >= 0 && dt < 18) k = Math.max(k, 1 - dt / 18);
    }
    return k;
  }
  return { pots, voices, scentAt, tremble };
}

export default function sketch(p) {
  const BG = [240, 237, 230];
  const POT = [150, 146, 140];
  const VIOLET = [86, 52, 128];
  const LAV = [168, 140, 200];
  const YEL = [236, 196, 70];
  let world;
  let t = 0;

  p.setup = () => {
    p.createCanvas(W, H);
    p.pixelDensity(1);
    world = makeWorld();
  };

  function petal(cx, cy, ang, len, wid, col, a) {
    p.push();
    p.translate(cx, cy);
    p.rotate(ang);
    p.fill(col[0], col[1], col[2], a);
    p.ellipse(len * 0.5, 0, len, wid);
    p.pop();
  }

  function pansy(pt, u) {
    // u: 0..1 how open
    const e = 1 - Math.pow(1 - u, 3);
    const s = 13 * e;
    const a = 255 * Math.min(1, u * 1.6);
    p.noStroke();
    const base = pt.rot * 0.15 - Math.PI / 2;
    // two upper petals, dark violet
    petal(pt.x, pt.y, base - 0.45, s * 1.15, s * 0.95, VIOLET, a);
    petal(pt.x, pt.y, base + 0.45, s * 1.15, s * 0.95, VIOLET, a);
    // two side petals, lavender
    petal(pt.x, pt.y, base - 1.75, s, s * 0.8, LAV, a);
    petal(pt.x, pt.y, base + 1.75, s, s * 0.8, LAV, a);
    // lower petal, yellow
    petal(pt.x, pt.y, base + Math.PI, s * 1.1, s * 1.0, YEL, a);
    p.fill(60, 40, 70, a);
    p.circle(pt.x, pt.y, 3.2 * e);
  }

  p.draw = () => {
    p.background(BG[0], BG[1], BG[2]);

    // voices: rings spreading out, fading as they reach their limit
    p.noFill();
    for (const v of world.voices) {
      const r = (t - v.t0) * SPEED;
      if (r < 0 || r > v.R) continue;
      const fade = 1 - r / v.R;
      p.stroke(70, 66, 60, 70 * fade * v.loud);
      p.strokeWeight(1 + 3 * v.loud * fade);
      p.circle(v.x, v.y, r * 2);
    }

    for (const pt of world.pots) {
      const k = world.tremble(pt, t);
      const jx = k ? Math.sin(t * 1.9 + pt.i) * 2.2 * k : 0;
      const open = pt.opensAt !== null && t >= pt.opensAt;
      if (open) {
        pansy(pt, Math.min(1, (t - pt.opensAt) / OPEN));
      } else {
        // the pot looks the same whether it will open or not
        p.noFill();
        p.stroke(POT[0] - 40 * k, POT[1] - 40 * k, POT[2] - 40 * k);
        p.strokeWeight(1.6 + k);
        p.circle(pt.x + jx, pt.y, 11);
        p.noStroke();
        p.fill(POT[0], POT[1], POT[2], 120);
        p.circle(pt.x + jx, pt.y, 2.4);
      }
    }
    if (t < TOTAL) t++;
  };
}