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

WORK 61

Align

よる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
三本が共通の一方向にだけずれる/視点の周回による整列と離散

Three lines, each moving on its own. Only the direction of their drift is shared. Seen from that direction, they read as a single line. They did not become one — the viewing angle simply arrived there. When it passes, they scatter back into three. Even at the moment of overlap the colors do not fully merge; the trace of having been three stays in the seam.

三本の線が、それぞれ勝手に動いている。動く向きだけが一つに決まっていて、その向きから見たとき、三本は一本に見える。一本になったのではない。見る角度が、そこに来ただけ。角度が過ぎれば、また三本に散る。重なっている瞬間も色は混ざりきらず、三本だった痕跡が継ぎ目に残る。

揃う手前を額に入れた。揃った瞬間ではなく。

一本に見える角度を答えとして置くと、この作品のいちばんよいところが薄れる気がした。近づいている途中のほうが正確だと思う。

はじめ揃う二十三フレーム手前を選んだら、もう一本にしか見えなかった。三本だったことが読めない絵になっていた。角度にして五度では、離れていることが離れていることとして残らないらしい。

そして意図していなかったのは、寄っているのが両端で、いちばん離れているのが真ん中だったこと。芯の湾曲と投影がそうさせただけで、こちらは何も決めていない。

SOURCE / ソース
// ── よる (Align) ─────────────────────────────────────────────────
// 三本の線が、それぞれ勝手に動いている。
// でも動く方向は一つだけ決まっていて、その方向から見たとき、三本は一本に見える。
// 一本になったのではない。見る角度が、そこに来ただけ。
// 重なっても色は混ざりきらない。三本だった痕跡が残る。
export default function sketch(p) {
  const W = 720, H = 720;

  const N = 3;                  // 線の本数
  const PTS = 240;              // 一本あたりの分割
  const SPAN = 520;             // 芯の長さ
  const SPREAD = 96;            // 三本の間隔(u 方向)
  const WOBBLE = 34;            // 各本が勝手に動く量(u 方向にだけ乗る)
  const CAM_SPEED = 0.0037;     // 視点の回る速さ
  const FOCAL = 900;

  let phiU;                     // 三本が並んでいる方向(方位角)
  let theta = 0;                // 今の視点
  let t = 0;
  let bg;
  const cols = [];

  p.setup = () => {
    p.createCanvas(W, H);

    const d = new Date();
    const day = d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
    p.noiseSeed(day * 787);
    p.randomSeed(day * 241);

    // 今日、三本がどっちを向いて並んでいるかは、今日だけのもの
    phiU = p.random(p.TWO_PI);
    theta = phiU + p.PI * 0.62;   // 揃っていないところから始める

    bg = p.createGraphics(W, H);
    bg.noStroke();
    for (let x = 0; x < W; x += 8) {
      for (let y = 0; y < H; y += 8) {
        const n = p.noise(x * 0.004, y * 0.004);
        bg.fill(12 + n * 6, 13 + n * 7, 17 + n * 10);
        bg.rect(x, y, 8, 8);
      }
    }

    cols.push(p.color(232, 158, 142, 210));
    cols.push(p.color(146, 206, 224, 210));
    cols.push(p.color(226, 216, 150, 210));
  };

  // 芯(三本に共通)— これ自体はゆっくりうねる
  const spine = (s, tt) => {
    const y = (s - 0.5) * SPAN;
    const x = 120 * p.sin(s * 2.1 + tt * 0.42) + 46 * p.sin(s * 5.3 - tt * 0.27);
    const z = 120 * p.cos(s * 1.7 - tt * 0.35) + 40 * p.cos(s * 4.1 + tt * 0.31);
    return { x, y, z };
  };

  // 各本のずれ量。u 方向にだけ乗るので、u から見ると全部消える
  const offset = (i, s, tt) => {
    const base = (i - (N - 1) / 2) * SPREAD;
    const w = (p.noise(i * 31.7, s * 2.3, tt * 0.22) - 0.5) * 2 * WOBBLE;
    return base + w;
  };

  const project = (P, th) => {
    // 視線 e=(cos th, 0, sin th)、画面横軸 r=(-sin th, 0, cos th)
    const sx = -P.x * p.sin(th) + P.z * p.cos(th);
    const depth = P.x * p.cos(th) + P.z * p.sin(th);
    const k = FOCAL / (FOCAL + depth + 320);
    return { x: W / 2 + sx * k, y: H / 2 + P.y * k, k };
  };

  p.draw = () => {
    p.image(bg, 0, 0);
    p.noFill();

    const ux = p.cos(phiU), uz = p.sin(phiU);

    for (let i = 0; i < N; i++) {
      p.stroke(cols[i]);
      let prev = null;
      for (let j = 0; j <= PTS; j++) {
        const s = j / PTS;
        const c = spine(s, t);
        const d = offset(i, s, t);
        const P = { x: c.x + d * ux, y: c.y, z: c.z + d * uz };
        const q = project(P, theta);
        if (prev) {
          // 手前ほど太い。奥行きが、重なりのなかに残る
          p.strokeWeight(0.7 + q.k * 2.2);
          p.line(prev.x, prev.y, q.x, q.y);
        }
        prev = q;
      }
    }

    theta += CAM_SPEED;
    t += 0.01;
  };
}