← INDEX / 一覧 NYAMCO WORKS 17 / 27
p5.js sketch works/shokubai/sketch.js runs here
作品はここで実行されます(後から差し替え)

WORK 17

Shokubai

しょくばい

YEAR
2026
TOOLS
p5.js
TECHNIQUE
catalyst field / ephemeral bonds / perpendicular flows

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.

温かい流れと涼しい流れが、ここで交差する。触媒がなければ、同じ場所を通り過ぎるだけ。でも触媒があると——交差する瞬間に、細い糸が現れる。そして消えていく。触媒自身は、ほとんど動かない。

SOURCE / ソース
// ── しょくばい (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);
  };
}