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

WORK 25

Afterward

あとから

YEAR
2026
TOOLS
p5.js
TECHNIQUE
noise walk / retroactive arcs

Points are laid down at a steady pace. At the moment of placing one, there is no telling what it is. Now and then something reaches back — from here to some earlier point — and everything in between takes on a colour all at once. Shallow reaches are common; the long ones are rare. A point that was never reached stays pale and drifts off the edge. It was placed all the same.

点は一定の速さで置かれていく。置いているとき、それが何なのかは分からない。ときどき、今から過去へ手が伸びる。届いた先までのあいだにあったものが、まとめて色を持つ。浅い遡りがほとんどで、遠くまで届くことは稀にしかない。一度も届かなかった点は、薄いまま端から流れていく。それでも、置かれてはいた。

意味は、置いた順には来ない。

SOURCE / ソース
// あとから (Atokara) — 意味は遡ってくる
//
// 点は一定の速さで置かれていく。置かれた瞬間、それが何なのかは分からない。
// ときどき「受け取り」が起きて、そこから過去へ手が伸びる。伸びた先までの
// あいだにあった点が、まとめて色を持つ。深く遡るほど、珍しい。
//
// 遡りは連続的じゃない。飛び飛びで、不揃いで、いつ来るか選べない。

const W = 720;
const H = 720;

const MARGIN = 52;
const PLACE_EVERY = 4; // フレーム
const WALK_STEP = 0.0115; // ノイズ歩行の速さ(大きいほど遠くまで散る)
const MAX_POINTS = 520; // これを超えたら古い方から流れていく
const FADE_TAIL = 60; // 流れ落ちる直前の点が薄くなる長さ

const REACH_CHANCE = 0.028; // 点を置くたび、これくらいの確率で遡りが起きる
const REACH_BIAS = 2.6; // 大きいほど「浅い遡り」に寄る(たまに深い)
const WARM_GAIN = 0.62; // 一度の受け取りで確定する度合い
const WARM_TAPER = 1.7; // 届いた先が一番明るく、今に近づくほど薄れる
const ARC_STEPS = 24; // 遡りの弧の分割数

const PAPER = [252, 250, 245];
const COLD = [104, 138, 196]; // まだ意味が来てない
const WARM = [200, 125, 45]; // 受け取られた

export default function sketch(p) {
  let pts = []; // {x, y, warmth}
  let arcs = []; // {fromIdx, toIdx, bend, age}  ※インデックスは通し番号
  let dropped = 0; // 流れ落ちた点の数(通し番号 → 配列位置の補正)
  let nx, ny; // ノイズ歩行の位相

  p.setup = () => {
    p.createCanvas(W, H);
    const seed = dailySeed();
    p.noiseSeed(seed);
    p.randomSeed(seed);
    nx = p.random(1000);
    ny = p.random(1000);
    p.strokeCap(p.ROUND);
  };

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

    if (p.frameCount % PLACE_EVERY === 0) place();

    drawPath();
    drawArcs();
    drawPoints();
  };

  // ── 点を置く ────────────────────────────────────────────────
  function place() {
    nx += WALK_STEP;
    ny += WALK_STEP;
    // Perlin は 0.5 付近に寄る。帯を狭く取ると画面いっぱいに散るが、
    // 端で clamp されて壁に貼りつき、直線ができる——置いてない線が見えてしまう。
    // 帯を広く取る。散りは小さくなるが、壁は実質なくなる。
    const x = p.map(p.noise(nx), 0.22, 0.78, MARGIN, W - MARGIN, true);
    const y = p.map(p.noise(ny, 40), 0.22, 0.78, MARGIN, H - MARGIN, true);
    pts.push({ x, y, warmth: 0 });

    if (pts.length > 12 && p.random() < REACH_CHANCE) reachBack();

    // 古い方から流れ落ちる
    while (pts.length > MAX_POINTS) {
      pts.shift();
      dropped++;
    }
    arcs = arcs.filter((a) => a.toIdx >= dropped);
  }

  // ── 遡って、あいだを色づける ──────────────────────────────────
  function reachBack() {
    const n = pts.length;
    // 浅い遡りがほとんど。たまに、ずっと遠くまで届く
    const depth = p.floor(1 + p.pow(p.random(), REACH_BIAS) * (n - 2));
    const hereLocal = n - 1;
    const thereLocal = hereLocal - depth;

    for (let i = hereLocal; i >= thereLocal; i--) {
      const q = pts[i];
      if (!q) continue;
      // 届いた先が一番はっきりする。今に近いほど、まだ薄い
      const t = (hereLocal - i) / Math.max(1, depth);
      q.warmth = Math.min(1, q.warmth + WARM_GAIN * Math.pow(t, WARM_TAPER));
    }

    arcs.push({
      fromIdx: hereLocal + dropped,
      toIdx: thereLocal + dropped,
      bend: p.random(-1, 1),
      age: 0,
    });
  }

  // ── 描く ────────────────────────────────────────────────────
  function drawPath() {
    p.noFill();
    p.stroke(150, 160, 172, 48);
    p.strokeWeight(0.8);
    p.beginShape();
    for (const q of pts) p.vertex(q.x, q.y);
    p.endShape();
  }

  function drawArcs() {
    p.noFill();
    for (const a of arcs) {
      a.age++;
      const A = pts[a.fromIdx - dropped];
      const B = pts[a.toIdx - dropped];
      if (!A || !B) continue;

      const span = a.fromIdx - a.toIdx;
      const bloom = p.constrain(a.age / 26, 0, 1);
      const settle = p.lerp(150, 74, bloom); // 届いた瞬間だけ濃く、あとは残る

      // 遠くまで届いた線ほど、深く曲がる
      const dx = B.x - A.x;
      const dy = B.y - A.y;
      const len = Math.hypot(dx, dy) || 1;
      const lift = (0.16 + 0.2 * Math.min(1, span / 260)) * len * a.bend;
      const cx = (A.x + B.x) / 2 + (-dy / len) * lift;
      const cy = (A.y + B.y) / 2 + (dx / len) * lift;

      p.stroke(WARM[0], WARM[1], WARM[2], settle);
      p.strokeWeight(p.lerp(1.6, 0.75, bloom));
      // 二次ベジエを自前で刻む(p5 v2 に quadraticVertex はない)
      p.beginShape();
      for (let s = 0; s <= ARC_STEPS; s++) {
        const t = s / ARC_STEPS;
        const u = 1 - t;
        p.vertex(
          u * u * A.x + 2 * u * t * cx + t * t * B.x,
          u * u * A.y + 2 * u * t * cy + t * t * B.y,
        );
      }
      p.endShape();
    }
  }

  function drawPoints() {
    p.noStroke();
    const n = pts.length;
    for (let i = 0; i < n; i++) {
      const q = pts[i];
      // 流れ落ちる直前は薄くなる
      const leave = n >= MAX_POINTS ? p.constrain(i / FADE_TAIL, 0, 1) : 1;
      const w = q.warmth;
      const r = p.lerp(COLD[0], WARM[0], w);
      const g = p.lerp(COLD[1], WARM[1], w);
      const b = p.lerp(COLD[2], WARM[2], w);
      p.fill(r, g, b, p.lerp(96, 224, w) * leave);
      p.circle(q.x, q.y, (2.3 + w * 3.3) * (0.55 + 0.45 * leave));
    }

    // 「今」——まだ何も来ていない点
    const head = pts[n - 1];
    if (head) {
      p.noFill();
      p.stroke(WARM[0], WARM[1], WARM[2], 70);
      p.strokeWeight(0.9);
      p.circle(head.x, head.y, 11 + 2.5 * Math.sin(p.frameCount * 0.06));
    }
  }

  function dailySeed() {
    const d = new Date();
    return (d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate()) % 100000;
  }
}