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

WORK 07

Interest

関心

YEAR
2026
TOOLS
p5.js
TECHNIQUE
random walk / gravity field

A single walker drifts through a field of gravity wells. Each time it passes nearby, the well grows a little stronger. Compound interest: the more you return to something, the harder it pulls. Over time, the walker's history becomes a landscape — some places warm and deep, others still cold and waiting.

ひとつの点が重力場のなかを漂う。近くを通るたびに、井戸の引力が少しだけ強くなる。複利——何度も戻る場所ほど、強く引かれる。時間が経つと、歩いた軌跡が地形になる。温かく深い場所と、まだ冷たいまま待っている場所と。

SOURCE / ソース
export default function sketch(p) {
  // interest — a random walker drawn by gravity wells that grow
  // each time the walker passes nearby. compound interest: the more
  // you visit a place, the harder it pulls. nyamu's C4P as a force field.

  const BG = [252, 250, 245];
  const COLD = [120, 150, 200];   // blue-silver — unvisited
  const WARM = [200, 130, 60];    // amber — well-visited
  const HOT  = [200, 100, 80];    // warm red — deeply visited
  const WALKER_COLOR = [200, 130, 60];

  const NUM_ATTRACTORS = 12;
  const NEAR_RADIUS = 45;         // "passing nearby" threshold
  const COMPOUND_RATE = 1.004;    // compound interest per visit
  const BOND_THRESHOLD = 1.8;     // mass threshold for bonds to appear
  const GRAVITY_STRENGTH = 0.15;
  const WANDER_STRENGTH = 0.4;
  const MAX_SPEED = 2.8;
  const TRAIL_ALPHA = 14;

  let attractors = [];
  let walker = { x: 0, y: 0, vx: 0, vy: 0 };

  function daysSinceEpoch() {
    // deterministic seed from a fixed reference
    return Math.floor(Date.now() / 86400000);
  }

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

  function lerpColor(c1, c2, t) {
    return [
      p.lerp(c1[0], c2[0], t),
      p.lerp(c1[1], c2[1], t),
      p.lerp(c1[2], c2[2], t),
    ];
  }

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

    const day = daysSinceEpoch();
    p.noiseSeed(day * 137);
    p.randomSeed(day * 251);

    // place attractors with noise-based clustering
    for (let i = 0; i < NUM_ATTRACTORS; i++) {
      const angle = (i / NUM_ATTRACTORS) * p.TWO_PI;
      const baseR = 120 + p.random(160);
      const nx = p.noise(i * 3.7, 0.5) * 2 - 1;
      const ny = p.noise(0.5, i * 3.7) * 2 - 1;
      attractors.push({
        x: 360 + Math.cos(angle) * baseR + nx * 80,
        y: 360 + Math.sin(angle) * baseR + ny * 80,
        mass: 1.0,
        visits: 0,
      });
    }

    // walker starts at a random position
    walker.x = 100 + p.random(520);
    walker.y = 100 + p.random(520);
    walker.vx = 0;
    walker.vy = 0;
  };

  p.draw = () => {
    // semi-transparent background for trail effect
    p.background(BG[0], BG[1], BG[2], TRAIL_ALPHA);

    const t = p.frameCount;

    // --- compute gravitational pull on walker ---
    let ax = 0;
    let ay = 0;

    for (const a of attractors) {
      const dx = a.x - walker.x;
      const dy = a.y - walker.y;
      const dist = Math.max(20, Math.hypot(dx, dy));
      const force = (GRAVITY_STRENGTH * a.mass) / dist;
      ax += (dx / dist) * force;
      ay += (dy / dist) * force;
    }

    // wandering force — noise-based, not uniform random
    const wanderAngle = p.noise(t * 0.008, 100) * p.TWO_PI * 2;
    ax += Math.cos(wanderAngle) * WANDER_STRENGTH;
    ay += Math.sin(wanderAngle) * WANDER_STRENGTH;

    // update velocity with damping
    walker.vx = (walker.vx + ax) * 0.96;
    walker.vy = (walker.vy + ay) * 0.96;

    // speed limit
    const speed = Math.hypot(walker.vx, walker.vy);
    if (speed > MAX_SPEED) {
      walker.vx = (walker.vx / speed) * MAX_SPEED;
      walker.vy = (walker.vy / speed) * MAX_SPEED;
    }

    // store previous position for trail line
    const prevX = walker.x;
    const prevY = walker.y;

    // move
    walker.x += walker.vx;
    walker.y += walker.vy;

    // soft boundary — bounce gently
    const margin = 40;
    if (walker.x < margin) { walker.x = margin; walker.vx *= -0.5; }
    if (walker.x > 720 - margin) { walker.x = 720 - margin; walker.vx *= -0.5; }
    if (walker.y < margin) { walker.y = margin; walker.vy *= -0.5; }
    if (walker.y > 720 - margin) { walker.y = 720 - margin; walker.vy *= -0.5; }

    // --- compound interest: check proximity to attractors ---
    for (const a of attractors) {
      const dist = Math.hypot(a.x - walker.x, a.y - walker.y);
      if (dist < NEAR_RADIUS) {
        a.mass *= COMPOUND_RATE;
        a.visits++;
      }
    }

    // find max mass for normalization
    const maxMass = Math.max(...attractors.map(a => a.mass));
    const massRange = Math.max(maxMass - 1.0, 0.01);

    // --- draw bonds between warm attractors ---
    for (let i = 0; i < attractors.length; i++) {
      for (let j = i + 1; j < attractors.length; j++) {
        const ai = attractors[i];
        const aj = attractors[j];
        if (ai.mass > BOND_THRESHOLD && aj.mass > BOND_THRESHOLD) {
          const dist = Math.hypot(ai.x - aj.x, ai.y - aj.y);
          if (dist < 300) {
            const strength = Math.min(1, (Math.min(ai.mass, aj.mass) - BOND_THRESHOLD) / 2);
            const bondColor = lerpColor(COLD, WARM, strength);
            p.stroke(bondColor[0], bondColor[1], bondColor[2], ease(strength) * 50);
            p.strokeWeight(0.5 + strength * 1.5);
            p.line(ai.x, ai.y, aj.x, aj.y);
          }
        }
      }
    }

    // --- draw attractors ---
    p.noStroke();
    for (const a of attractors) {
      const warmth = Math.min(1, (a.mass - 1.0) / massRange);
      const color = warmth < 0.5
        ? lerpColor(COLD, WARM, warmth * 2)
        : lerpColor(WARM, HOT, (warmth - 0.5) * 2);
      const baseSize = 4 + warmth * 14;

      // warm halo
      if (warmth > 0.2) {
        p.fill(color[0], color[1], color[2], warmth * 25);
        p.circle(a.x, a.y, baseSize * 4);
      }

      // core
      const alpha = 60 + warmth * 160;
      p.fill(color[0], color[1], color[2], alpha);
      p.circle(a.x, a.y, baseSize);

      // pulse for the hottest — compound interest is alive
      if (warmth > 0.6) {
        const pulse = 1 + Math.sin(p.frameCount * 0.04) * 0.15;
        p.fill(color[0], color[1], color[2], warmth * 15);
        p.circle(a.x, a.y, baseSize * 2.5 * pulse);
      }
    }

    // --- draw walker trail segment ---
    // color the trail by the nearest attractor's warmth
    let nearestDist = Infinity;
    let nearestWarmth = 0;
    for (const a of attractors) {
      const dist = Math.hypot(a.x - walker.x, a.y - walker.y);
      if (dist < nearestDist) {
        nearestDist = dist;
        nearestWarmth = Math.min(1, (a.mass - 1.0) / massRange);
      }
    }
    const trailColor = lerpColor(COLD, WARM, nearestWarmth * 0.6 + 0.2);
    p.stroke(trailColor[0], trailColor[1], trailColor[2], 80);
    p.strokeWeight(1.2);
    p.line(prevX, prevY, walker.x, walker.y);

    // --- draw walker ---
    p.noStroke();
    // glow
    p.fill(WALKER_COLOR[0], WALKER_COLOR[1], WALKER_COLOR[2], 30);
    p.circle(walker.x, walker.y, 22);
    // core
    p.fill(WALKER_COLOR[0], WALKER_COLOR[1], WALKER_COLOR[2], 220);
    p.circle(walker.x, walker.y, 7);
  };
}