WORK 17
しょくばい
Two flows cross here — warm and cool, horizontal and vertical. Without the catalyst, they'd pass through the same point and go their separate ways. With it, a brief thread forms between them in the crossing. The catalyst itself barely moves.
温かい流れと涼しい流れが、ここで交差する。触媒がなければ、同じ場所を通り過ぎるだけ。でも触媒があると——交差する瞬間に、細い糸が現れる。そして消えていく。触媒自身は、ほとんど動かない。
// ── しょくばい (shokubai) ──────────────────────────────────────────
// A catalyst adds nothing.
// Two flows cross here — warm and cool.
// Without it, they'd pass through the same point
// and go their separate ways.
// With it, something happens in the crossing.
export default function sketch(p) {
const W = 720;
const CX = W / 2, CY = W / 2;
const FIELD_R = 108; // catalyst field radius
const today = (() => {
const d = new Date();
return d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
})();
const N = 16;
let groupA = []; // horizontal flow — warm
let groupB = []; // vertical flow — cool
let bonds = [];
class Particle {
constructor(type) {
this.type = type;
this.phase = p.random(p.TWO_PI);
this.wobble = p.random(p.TWO_PI);
this.speed = 0.011 + p.random(0.009);
this.wobbleSpd = 0.007 + p.random(0.005);
if (type === 'A') {
this.baseY = p.random(60, W - 60);
this.ampX = 190 + p.random(85);
} else {
this.baseX = p.random(60, W - 60);
this.ampY = 190 + p.random(85);
}
}
get x() {
return this.type === 'A'
? CX + Math.cos(this.phase) * this.ampX + Math.sin(this.wobble * 1.3) * 9
: this.baseX + Math.sin(this.wobble) * 11;
}
get y() {
return this.type === 'A'
? this.baseY + Math.cos(this.wobble * 0.7) * 8
: CY + Math.sin(this.phase) * this.ampY + Math.cos(this.wobble * 1.1) * 9;
}
update() {
this.phase += this.speed;
this.wobble += this.wobbleSpd;
}
inField() {
const dx = this.x - CX;
const dy = this.y - CY;
return dx * dx + dy * dy < FIELD_R * FIELD_R;
}
draw() {
const inF = this.inField();
const breath = 0.5 + 0.5 * Math.sin(p.frameCount * 0.009 + this.phase);
const r = this.type === 'A' ? 200 : 95;
const g = this.type === 'A' ? 138 : 140;
const b = this.type === 'A' ? 70 : 198;
const alpha = inF ? 72 + breath * 52 : 28 + breath * 22;
const size = inF ? 5.0 : 3.6;
if (inF) {
p.fill(r, g, b, alpha * 0.22);
p.circle(this.x, this.y, size * 4.0);
}
p.fill(r, g, b, alpha);
p.circle(this.x, this.y, size * 2);
}
}
p.setup = () => {
p.createCanvas(W, W);
p.randomSeed(today * 347);
p.noiseSeed(today * 197);
for (let i = 0; i < N; i++) {
groupA.push(new Particle('A'));
groupB.push(new Particle('B'));
}
};
p.draw = () => {
p.background(252, 250, 245);
p.noStroke();
const t = p.frameCount;
for (const pt of [...groupA, ...groupB]) pt.update();
// Catalyst-mediated bonds: both A and B must be in the field
for (const a of groupA) {
if (!a.inField()) continue;
for (const b of groupB) {
if (!b.inField()) continue;
const dx = a.x - b.x;
const dy = a.y - b.y;
if (dx * dx + dy * dy < 195 * 195 && p.random() < 0.005) {
bonds.push({ x1: a.x, y1: a.y, x2: b.x, y2: b.y, life: 1.0 });
}
}
}
// Draw fading bonds
for (const bond of bonds) {
bond.life -= 0.009;
const alpha = bond.life * bond.life * 60;
p.stroke(165, 148, 136, alpha);
p.strokeWeight(0.5 + bond.life * 0.5);
p.line(bond.x1, bond.y1, bond.x2, bond.y2);
}
bonds = bonds.filter(b => b.life > 0.01);
p.noStroke();
// Draw particles (above bonds)
for (const pt of [...groupA, ...groupB]) pt.draw();
// Catalyst — the quiet center that makes it possible
const cb = 0.5 + 0.5 * Math.sin(t * 0.0032);
// Field boundary — barely a breath
p.fill(195, 183, 168, 9);
p.circle(CX, CY, FIELD_R * 2);
// Inner warmth
p.fill(205, 190, 170, 15);
p.circle(CX, CY, FIELD_R * 0.85);
// The catalyst itself — small, still, present
p.fill(185, 170, 152, 50 + cb * 33);
p.circle(CX, CY, 7);
};
}