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

WORK 97

Neighbors

りんじん

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / rotating gapped rings, light that only crosses the outer walls

Two fortresses of rings, side by side. Each is a stack of concentric walls turning slowly, in alternating directions. The outer walls have gaps; the innermost three are closed. When a gap in one outer wall turns to face the neighbor, a little light slips out and drifts across. If it arrives at a gap, it goes one wall deeper; if not, it stops there and leaves a trace of its color in that wall. So the outer walls slowly take on each other's color, and fade back if nothing comes for a while. The inside of each is never drawn. Nothing switches either of them off. From the name of the automata in the manga 宙に参る (Sora ni Mairu): Rinjin, written 輪陣, a ring-shaped formation of walls that keeps a secret humans cannot read — and also 隣人, the one who lives next door.

ふたつの、輪の陣が隣りあっている。どちらも同心の壁が何重にも重なって、ゆっくり、交互に逆の向きへ回っている。外側の壁には切れ目があって、内側の三枚には無い。片方の外側の切れ目が隣の方を向いたとき、すこし光がこぼれて、隣へ渡っていく。着いた先がちょうど切れ目なら、もう一枚奥へ進む。切れ目でなければそこで止まって、その壁に自分の色をすこし残す。だから外側の壁どうしは、だんだん相手の色を帯びていく。しばらく何も来なければ、また戻る。内側の中は、どちらも描いていない。どちらかを止めるスイッチも無い。漫画『宙に参る』のオートマトンの名前から——リンジン。人間が読めない秘密を守るために、輪のように壁を重ねた陣で「輪陣」。そして「隣人」、隣に住んでいる人。

SOURCE / ソース
// #97 りんじん / Neighbors
// Two ring-fortresses side by side. Only the outer walls trade light.
// The inside of each is never drawn.
export default function sketch(p) {
  const W = 720, H = 720;
  const NR = 9;                 // rings per neighbor
  const R0 = 46, DR = 13;       // innermost radius, spacing
  const CLOSED = 3;             // innermost rings have no gaps
  let hoods = [];
  let motes = [];

  const OWN = [[236, 170, 92], [104, 160, 236]];

  p.setup = () => {
    p.createCanvas(W, H);
    p.pixelDensity(1);
    const day = Math.floor(Date.now() / 86400000);
    p.randomSeed(day * 97);
    const gapC = 2 * (R0 + DR * (NR - 1)) + 18;
    const cx = [W / 2 - gapC / 2, W / 2 + gapC / 2];
    for (let h = 0; h < 2; h++) {
      const rings = [];
      for (let k = 0; k < NR; k++) {
        const r = R0 + DR * k;
        const dir = (k + h) % 2 === 0 ? 1 : -1;
        const w = dir * p.random(0.0015, 0.0045) * (1.4 - k / NR);
        const gaps = [];
        if (k >= CLOSED) {
          const ng = 1 + Math.floor(p.random(3));
          for (let g = 0; g < ng; g++) {
            gaps.push({ a: p.random(p.TWO_PI), w: p.random(0.18, 0.42) });
          }
        }
        rings.push({ r, w, phase: p.random(p.TWO_PI), gaps, m: 0 });
      }
      hoods.push({ x: cx[h], y: H / 2, rings, own: OWN[h], other: OWN[1 - h] });
    }
    p.background(10, 11, 18);
  };

  const wrap = (a) => ((a % p.TWO_PI) + p.TWO_PI) % p.TWO_PI;
  function inGap(ring, absA) {
    const rel = wrap(absA - ring.phase);
    for (const g of ring.gaps) {
      let d = Math.abs(wrap(rel - g.a));
      d = Math.min(d, p.TWO_PI - d);
      if (d < g.w / 2) return true;
    }
    return false;
  }

  function spawn() {
    for (let h = 0; h < 2; h++) {
      const me = hoods[h], you = hoods[1 - h];
      const outer = me.rings[NR - 1];
      const toward = Math.atan2(you.y - me.y, you.x - me.x);
      for (let s = -2; s <= 2; s++) {
        const a = toward + s * 0.12;
        if (inGap(outer, a) && p.random() < 0.18) {
          motes.push({
            from: h, x: me.x + Math.cos(a) * outer.r, y: me.y + Math.sin(a) * outer.r,
            tx: you.x, ty: you.y, depth: NR - 1, life: 0,
            wob: p.random(p.TWO_PI),
          });
        }
      }
    }
  }

  p.draw = () => {
    p.noStroke();
    p.fill(10, 11, 18, 46);
    p.rect(0, 0, W, H);

    for (const hd of hoods) {
      for (const ring of hd.rings) {
        ring.phase += ring.w;
        ring.m = Math.max(0, ring.m - 0.00012);
      }
    }
    spawn();

    // motes travel toward the neighbor and stop at the first wall that is closed there
    for (const m of motes) {
      const you = hoods[1 - m.from];
      const dx = m.tx - m.x, dy = m.ty - m.y;
      const d = Math.hypot(dx, dy);
      const ux = dx / d, uy = dy / d;
      m.life++;
      const sp = 1.6;
      m.x += ux * sp - uy * Math.sin(m.wob + m.life * 0.09) * 0.5;
      m.y += uy * sp + ux * Math.sin(m.wob + m.life * 0.09) * 0.5;
      const ring = you.rings[m.depth];
      if (d <= ring.r) {
        const a = Math.atan2(m.y - you.y, m.x - you.x);
        if (inGap(ring, a) && m.depth > 0) {
          m.depth--;
        } else {
          ring.m += 0.05 * (1 - ring.m);
          m.dead = true;
        }
      }
      const c = hoods[m.from].own;
      p.fill(c[0], c[1], c[2], 220);
      p.circle(m.x, m.y, 3.2);
      p.fill(c[0], c[1], c[2], 40);
      p.circle(m.x, m.y, 9);
    }
    motes = motes.filter((m) => !m.dead);

    p.noFill();
    p.strokeCap(p.SQUARE);
    for (const hd of hoods) {
      for (let k = 0; k < NR; k++) {
        const ring = hd.rings[k];
        const t = Math.min(1, ring.m);
        const col = [0, 1, 2].map((i) => hd.own[i] * (1 - t) + hd.other[i] * t);
        p.stroke(col[0], col[1], col[2], 120 + 100 * (k / NR));
        p.strokeWeight(k < CLOSED ? 2.4 : 1.6);
        if (ring.gaps.length === 0) {
          p.circle(hd.x, hd.y, ring.r * 2);
          continue;
        }
        // draw the ring between its gaps
        const g = ring.gaps.map((q) => [wrap(q.a - q.w / 2), q.w]).sort((a, b) => a[0] - b[0]);
        for (let i = 0; i < g.length; i++) {
          const start = g[i][0] + g[i][1];
          const end = i + 1 < g.length ? g[i + 1][0] : g[0][0] + p.TWO_PI;
          if (end > start) p.arc(hd.x, hd.y, ring.r * 2, ring.r * 2, start + ring.phase, end + ring.phase);
        }
      }
    }
  };
}