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