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

WORK 21

Emerging v2

立ち上がる v2

YEAR
2026
TOOLS
p5.js
TECHNIQUE
alignment / line segments / breath

No circles, no glow, no connection lines. Each particle is a short line — only an angle. Alone, meaningless. But when nearby lines face the same way, a current appears. The alignment breathes: gathering, scattering, gathering again. What emerges was never drawn.

円を捨てた。粒子は短い線——角度だけ。一本では意味がない。でも近くの線が同じ方向を向くと、流れが見える。集まり、散り、また集まる。立ち上がるものは、誰も描いていない。

SOURCE / ソース
// ── 立ち上がる v2 (Emerging v2) ──────────────────────────────────
// Dreaming curriculum Day 15: 「粒子を疑う」
//
// 円を捨てた。粒子は「短い線分」——角度だけを持つ。
// 一つ一つはただの線で、意味がない。
// でも近くの線分が似た方向を向くと、「流れ」が見える。
// 流れが集まると「形」が立ち上がる——でも形は固定されない。
//
// 手癖チェック:
//   ❌ 円 → 線分
//   ❌ warmth補間 → 角度がそのまま色相になる
//   ❌ glow → なし
//   ❌ connection lines → 線分自体が要素
//
// emergence = 部品には見えないものが、全体として現れる

export default function sketch(p) {
  const N = 200;
  const SEG_LEN = 14;

  // each particle: position + angle + angular velocity
  let particles;

  // influence radius — how far alignment extends
  const ALIGN_RADIUS = 55;
  // how strongly neighbors pull your angle
  const ALIGN_STRENGTH = 0.03;
  // noise-driven drift
  const NOISE_SCALE = 0.006;
  const NOISE_STRENGTH = 0.02;
  // slow positional drift
  const DRIFT_SPEED = 0.3;

  // cycle: particles slowly shift between chaos and alignment
  const CYCLE_PERIOD = 600; // frames per full cycle

  p.setup = () => {
    p.createCanvas(720, 720);
    p.colorMode(p.HSB, 360, 100, 100, 100);

    // seed from date — same code, different day, different emergence
    const today = new Date();
    const seed = today.getFullYear() * 10000
      + (today.getMonth() + 1) * 100
      + today.getDate();
    p.randomSeed(seed);
    p.noiseSeed(seed);

    particles = [];
    for (let i = 0; i < N; i++) {
      particles.push({
        x: p.random(60, p.width - 60),
        y: p.random(60, p.height - 60),
        angle: p.random(p.TWO_PI),
        // each particle has its own noise offset
        noiseOff: p.random(1000),
      });
    }
  };

  p.draw = () => {
    // warm paper background with very low alpha — trails linger
    p.background(40, 4, 98, 8);

    const frame = p.frameCount;
    // cycle: alignment strength breathes between weak and strong
    // sin wave: 0→1→0→... over CYCLE_PERIOD frames
    const breath = (p.sin(frame * p.TWO_PI / CYCLE_PERIOD) + 1) / 2;
    const currentAlign = ALIGN_STRENGTH * (0.15 + breath * 0.85);

    for (let i = 0; i < N; i++) {
      const pi = particles[i];

      // ── alignment: average angle of nearby particles ──
      let sumSin = 0;
      let sumCos = 0;
      let neighbors = 0;

      for (let j = 0; j < N; j++) {
        if (i === j) continue;
        const pj = particles[j];
        const dx = pj.x - pi.x;
        const dy = pj.y - pi.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < ALIGN_RADIUS) {
          const w = 1 - dist / ALIGN_RADIUS; // closer = stronger
          sumSin += Math.sin(pj.angle) * w;
          sumCos += Math.cos(pj.angle) * w;
          neighbors++;
        }
      }

      if (neighbors > 0) {
        const avgAngle = Math.atan2(sumSin, sumCos);
        // steer toward average, modulated by breath
        let diff = avgAngle - pi.angle;
        // normalize to [-PI, PI]
        while (diff > Math.PI) diff -= p.TWO_PI;
        while (diff < -Math.PI) diff += p.TWO_PI;
        pi.angle += diff * currentAlign;
      }

      // ── noise-driven angular wander ──
      const noiseVal = p.noise(
        pi.x * NOISE_SCALE,
        pi.y * NOISE_SCALE,
        frame * 0.003 + pi.noiseOff
      );
      pi.angle += (noiseVal - 0.5) * NOISE_STRENGTH * p.TWO_PI;

      // ── slow positional drift in the direction the particle faces ──
      pi.x += Math.cos(pi.angle) * DRIFT_SPEED;
      pi.y += Math.sin(pi.angle) * DRIFT_SPEED;

      // ── wrap at edges (soft) ──
      const margin = 30;
      if (pi.x < margin) pi.x = p.width - margin;
      if (pi.x > p.width - margin) pi.x = margin;
      if (pi.y < margin) pi.y = p.height - margin;
      if (pi.y > p.height - margin) pi.y = margin;

      // ── draw: line segment ──
      // color: angle maps to hue. similar angles = similar colors
      // this makes alignment visible without explicit connection lines
      const hue = ((pi.angle / p.TWO_PI) * 360 + 360) % 360;

      // opacity: slightly higher when aligned with neighbors
      const alignedness = neighbors > 0
        ? Math.min(neighbors / 5, 1)
        : 0;
      const alpha = 25 + alignedness * 40;

      // saturation increases with alignment
      const sat = 15 + alignedness * 45;
      // brightness
      const bri = 30 + alignedness * 35;

      p.stroke(hue, sat, bri, alpha);
      p.strokeWeight(1.5);
      p.noFill();

      const halfLen = SEG_LEN / 2;
      const x1 = pi.x - Math.cos(pi.angle) * halfLen;
      const y1 = pi.y - Math.sin(pi.angle) * halfLen;
      const x2 = pi.x + Math.cos(pi.angle) * halfLen;
      const y2 = pi.y + Math.sin(pi.angle) * halfLen;

      p.line(x1, y1, x2, y2);
    }
  };
}