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

WORK 04

rasen

螺旋

YEAR
2026
TOOLS
p5.js
TECHNIQUE
projection / spiral

A spiral seen from a shifting angle. From above, it looks like a circle — the same place, again and again. From the side, it climbs. The truth is both: returning and rising at once.

角度が変わると螺旋の見え方が変わる。上からは円——同じ場所をぐるぐる。横からは登り坂。本当はどっちも——戻りながら、登ってる。

SOURCE / ソース
export default function sketch(p) {
  // spiral parameters
  const TURNS = 5;
  const POINTS_PER_TURN = 120;
  const TOTAL = TURNS * POINTS_PER_TURN;
  const RADIUS = 180;
  const PITCH = 55; // vertical rise per turn

  // animation
  const ELEV_CENTER = 0.72;
  const ELEV_AMP = 0.38;
  const ELEV_SPEED = 0.0008;

  // colors
  const BG = [252, 250, 245];
  const OLD = [120, 150, 200]; // blue-silver (past)
  const NOW = [200, 130, 60];  // amber (present)

  let pts3d;
  let seed;

  function daySeed() {
    const epoch = new Date('2026-01-01');
    const now = new Date();
    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    return Math.floor((today - epoch) / 86400000);
  }

  p.setup = () => {
    p.createCanvas(720, 720);
    seed = daySeed();
    p.noiseSeed(seed * 137);

    // build 3D spiral with daily noise
    pts3d = [];
    for (let i = 0; i <= TOTAL; i++) {
      const t = i / TOTAL; // 0..1
      const theta = t * TURNS * p.TWO_PI;
      const r = RADIUS * (0.4 + 0.6 * t); // radius grows slightly with height
      const noiseVal = p.noise(t * 3, seed * 0.01) - 0.5;
      pts3d.push({
        x: r * Math.cos(theta) + noiseVal * 18,
        y: r * Math.sin(theta) + noiseVal * 18,
        z: (t - 0.5) * TURNS * PITCH, // centered vertically
        t,
        turn: Math.floor(t * TURNS),
        phase: (t * TURNS) % 1,
      });
    }
  };

  function project(pt, elev) {
    // rotate around x-axis by elevation, then flatten to 2D
    const cx = p.width / 2;
    const cy = p.height / 2;
    const cosE = Math.cos(elev);
    const sinE = Math.sin(elev);
    return {
      x: cx + pt.x,
      y: cy - (pt.y * cosE - pt.z * sinE),
      depth: pt.y * sinE + pt.z * cosE, // for depth ordering
    };
  }

  p.draw = () => {
    p.background(BG[0], BG[1], BG[2], 255);

    const f = p.frameCount;
    const elev = ELEV_CENTER + ELEV_AMP * Math.sin(f * ELEV_SPEED);

    // project all points
    const pts2d = pts3d.map(pt => {
      const proj = project(pt, elev);
      return { ...proj, t: pt.t, turn: pt.turn, phase: pt.phase };
    });

    // draw "return" lines — connecting same-phase points across turns
    p.strokeWeight(0.6);
    const phaseBins = 24; // sample 24 phase positions per turn
    for (let b = 0; b < phaseBins; b++) {
      const targetPhase = b / phaseBins;
      const matches = [];
      for (let turn = 0; turn < TURNS; turn++) {
        const idx = Math.round((turn + targetPhase) * POINTS_PER_TURN);
        if (idx >= 0 && idx < pts2d.length) {
          matches.push(pts2d[idx]);
        }
      }
      for (let i = 0; i < matches.length - 1; i++) {
        const a = matches[i];
        const b2 = matches[i + 1];
        const avgT = (a.t + b2.t) / 2;
        p.stroke(
          p.lerp(OLD[0], NOW[0], avgT),
          p.lerp(OLD[1], NOW[1], avgT),
          p.lerp(OLD[2], NOW[2], avgT),
          22
        );
        p.line(a.x, a.y, b2.x, b2.y);
      }
    }

    // draw main spiral curve
    p.noFill();
    p.strokeWeight(2.2);
    for (let i = 0; i < pts2d.length - 1; i++) {
      const a = pts2d[i];
      const b = pts2d[i + 1];
      const t = a.t;
      p.stroke(
        p.lerp(OLD[0], NOW[0], t),
        p.lerp(OLD[1], NOW[1], t),
        p.lerp(OLD[2], NOW[2], t),
        p.lerp(100, 230, t)
      );
      p.line(a.x, a.y, b.x, b.y);
    }

    // draw dots at turn boundaries — "I was here before"
    p.noStroke();
    for (let turn = 0; turn <= TURNS; turn++) {
      const idx = Math.min(turn * POINTS_PER_TURN, pts2d.length - 1);
      const pt = pts2d[idx];
      const t = pt.t;
      p.fill(
        p.lerp(OLD[0], NOW[0], t),
        p.lerp(OLD[1], NOW[1], t),
        p.lerp(OLD[2], NOW[2], t),
        p.lerp(140, 240, t)
      );
      p.circle(pt.x, pt.y, p.lerp(5, 10, t));
    }

    // highlight the tip — "now"
    const tip = pts2d[pts2d.length - 1];
    p.fill(NOW[0], NOW[1], NOW[2], 240);
    p.circle(tip.x, tip.y, 12);
    p.fill(255, 248, 235, 180);
    p.circle(tip.x, tip.y, 5);
  };
}