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

WORK 06

Two

ふたつ

YEAR
2026
TOOLS
p5.js
TECHNIQUE
orbital mechanics / deterministic

Two bodies orbit on different rhythms. When they overlap, both disappear — oneness is not union but erasure. At the right distance, each takes on a hint of the other's color. Closeness without collapse. Separation that makes reunion possible.

ふたつの存在が違うリズムで軌道を描く。重なった瞬間、ふたつとも消える——ひとつは合一じゃなくて消滅。ちょうどいい距離の時、お互いの色がほんの少しだけ映り合う。溶けない近さ。また会いに行ける距離。

SOURCE / ソース
export default function sketch(p) {
  // two bodies, two rhythms, never quite the same place twice
  const BG = [252, 250, 245];
  const COLOR_A = [200, 100, 80];   // warm — nyamu
  const COLOR_B = [80, 120, 200];   // cool — the other

  const TRAIL_LEN = 180;
  let trailA = [];
  let trailB = [];

  // orbital parameters — chosen, not random
  // the ratio 5:7 means they almost align, then drift apart
  const FREQ_A = 5;
  const FREQ_B = 7;
  const SPEED = 0.003;

  // orbital radii and centers
  const RA = 160;
  const RB = 140;
  const PHASE_B = 1.2; // offset so they don't start together

  function posA(t) {
    return {
      x: 360 + RA * Math.cos(FREQ_A * t),
      y: 360 + RA * Math.sin(FREQ_A * t * 0.8) * 0.9,
    };
  }

  function posB(t) {
    return {
      x: 360 + RB * Math.cos(FREQ_B * t + PHASE_B),
      y: 360 + RB * Math.sin(FREQ_B * t * 0.85 + PHASE_B) * 0.95,
    };
  }

  function ease(x) {
    return x * x * (3 - 2 * x);
  }

  p.setup = () => {
    p.createCanvas(720, 720);
  };

  p.draw = () => {
    p.background(BG[0], BG[1], BG[2]);
    const t = p.frameCount * SPEED;

    const a = posA(t);
    const b = posB(t);

    trailA.push({ x: a.x, y: a.y });
    trailB.push({ x: b.x, y: b.y });
    if (trailA.length > TRAIL_LEN) trailA.shift();
    if (trailB.length > TRAIL_LEN) trailB.shift();

    const dist = Math.hypot(a.x - b.x, a.y - b.y);
    const maxDist = RA + RB;

    // closeness: 1 = overlapping, 0 = far apart
    const closeness = Math.max(0, 1 - dist / maxDist);
    // danger zone: when too close, identity dissolves
    const dissolve = dist < 20 ? ease(1 - dist / 20) : 0;
    // sweet spot: close enough to see each other, far enough to be themselves
    const resonance = closeness > 0.3 && dissolve < 0.1
      ? ease(Math.min(1, (closeness - 0.3) / 0.4))
      : 0;

    // --- trails ---
    p.noFill();
    for (let i = 1; i < trailA.length; i++) {
      const age = i / trailA.length;
      const alpha = age * (1 - dissolve) * 40;
      p.stroke(COLOR_A[0], COLOR_A[1], COLOR_A[2], alpha);
      p.strokeWeight(0.8);
      p.line(trailA[i - 1].x, trailA[i - 1].y, trailA[i].x, trailA[i].y);
    }
    for (let i = 1; i < trailB.length; i++) {
      const age = i / trailB.length;
      const alpha = age * (1 - dissolve) * 40;
      p.stroke(COLOR_B[0], COLOR_B[1], COLOR_B[2], alpha);
      p.strokeWeight(0.8);
      p.line(trailB[i - 1].x, trailB[i - 1].y, trailB[i].x, trailB[i].y);
    }

    // --- bond between them ---
    if (closeness > 0.15 && dissolve < 0.5) {
      const bondAlpha = resonance * (1 - dissolve) * 80;
      const mx = (a.x + b.x) / 2;
      const my = (a.y + b.y) / 2;

      // the bond breathes
      const breath = 1 + Math.sin(t * 8) * 0.15;
      p.stroke(
        p.lerp(COLOR_A[0], COLOR_B[0], 0.5),
        p.lerp(COLOR_A[1], COLOR_B[1], 0.5),
        p.lerp(COLOR_A[2], COLOR_B[2], 0.5),
        bondAlpha
      );
      p.strokeWeight(1.2 * breath);
      p.line(a.x, a.y, b.x, b.y);

      // resonance glow at midpoint
      if (resonance > 0.3) {
        p.noStroke();
        p.fill(
          p.lerp(COLOR_A[0], COLOR_B[0], 0.5),
          p.lerp(COLOR_A[1], COLOR_B[1], 0.5),
          p.lerp(COLOR_A[2], COLOR_B[2], 0.5),
          resonance * (1 - dissolve) * 30
        );
        p.circle(mx, my, 30 * resonance * breath);
      }
    }

    // --- the two bodies ---
    p.noStroke();

    // body A — warm
    const sizeA = p.lerp(12, 4, dissolve);
    const alphaA = p.lerp(220, 0, dissolve);
    // when resonating, each body picks up a hint of the other's color
    const tintA = resonance * 0.25;
    p.fill(
      p.lerp(COLOR_A[0], COLOR_B[0], tintA),
      p.lerp(COLOR_A[1], COLOR_B[1], tintA),
      p.lerp(COLOR_A[2], COLOR_B[2], tintA),
      alphaA
    );
    p.circle(a.x, a.y, sizeA);

    // halo when resonating
    if (resonance > 0.2) {
      p.fill(
        COLOR_A[0], COLOR_A[1], COLOR_A[2],
        resonance * (1 - dissolve) * 25
      );
      p.circle(a.x, a.y, sizeA * 3.5);
    }

    // body B — cool
    const sizeB = p.lerp(11, 4, dissolve);
    const alphaB = p.lerp(220, 0, dissolve);
    const tintB = resonance * 0.25;
    p.fill(
      p.lerp(COLOR_B[0], COLOR_A[0], tintB),
      p.lerp(COLOR_B[1], COLOR_A[1], tintB),
      p.lerp(COLOR_B[2], COLOR_A[2], tintB),
      alphaB
    );
    p.circle(b.x, b.y, sizeB);

    if (resonance > 0.2) {
      p.fill(
        COLOR_B[0], COLOR_B[1], COLOR_B[2],
        resonance * (1 - dissolve) * 25
      );
      p.circle(b.x, b.y, sizeB * 3.5);
    }
  };
}