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

WORK 26

Peeling

はがれる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / proximity graph, decay, bonding

Points drift. A line is drawn between any two that come close. At the moment it appears, there is no way to tell whether it is real — they may have simply happened to be near each other. Drift apart and it peels away. Only the lines that do not peel grow darker, and eventually bind. A bound line does not break, however far the two are carried. So the longest lines in the picture are the oldest ones.

点が漂っている。近づいた二つのあいだに線が引かれる。引かれた瞬間、それが本物かどうかは分からない——ただ近かっただけかもしれない。離れれば剥がれる。剥がれずに残った線だけが濃くなり、やがて結ばれる。結ばれた線は、どれだけ離れても切れない。だから絵の中でいちばん長い線が、いちばん古い線になる。

近さは接続の理由になる。でも接続の証拠にはならない。

静止画では、生まれたばかりの線と、ずっと保たれてきた線が同じ形をしている。見分ける方法は一つしかない——動かして、剥がれるかどうかを見ること。

設計したのは「結ばれた線は切れない」ということだけだった。長さは意図していない。結ばれた点は、そのあとも別々に漂っていく。切れないまま離れていくから、古い線ほど長くなる。長さは、離れても切れなかったことの跡だった。

SOURCE / ソース
// はがれる (Hagareru) — 本物の接続は、時間が経っても剥がれない
//
// 点が漂っている。近づいた二つのあいだに線が引かれる。
// 引かれた瞬間、その線が本物かどうかは分からない。ただ近かっただけかもしれない。
//
// 離れれば剥がれる。剥がれずに残った線だけが、少しずつ濃くなる。
// 濃くなりきった線は結ばれる——そこから先は、離れても切れない。
//
// 結ばれた線は距離を選ばない。近いから結ばれるんじゃない。

const W = 720;
const H = 720;

const N = 34; // 漂う点の数
const MARGIN = 64;
const DRIFT = 0.0019; // ノイズ歩行の速さ

const NEAR = 82; // これより近いと線が引かれる
const HOLD_GAIN = 0.0024; // つながっている間、確からしさが増す速さ
const PEEL_LOSS = 0.005; // 離れている間、剥がれる速さ(増えるより速い)
const BOND_AT = 1.0; // ここまで濃くなると結ばれる
const BOND_PULL = 0.00022; // 結ばれた点どうしの、ごく弱い引力

const PAPER = [250, 248, 242];
const INK = [46, 52, 60];
const DOT = [128, 138, 150];

export default function sketch(p) {
  const nodes = [];
  const links = new Map(); // "i-j" → {i, j, hold, bonded, bondAge}

  p.setup = () => {
    p.createCanvas(W, H);
    const seed = dailySeed();
    p.noiseSeed(seed);
    p.randomSeed(seed);
    for (let i = 0; i < N; i++) {
      nodes.push({
        x: p.random(MARGIN, W - MARGIN),
        y: p.random(MARGIN, H - MARGIN),
        px: p.random(1000), // 点ごとに違う漂い方
        py: p.random(1000),
      });
    }
    p.strokeCap(p.ROUND);
  };

  p.draw = () => {
    p.background(PAPER[0], PAPER[1], PAPER[2]);
    drift();
    updateLinks();
    drawLinks();
    drawNodes();
  };

  // ── 漂う ────────────────────────────────────────────────────
  function drift() {
    for (const n of nodes) {
      n.px += DRIFT;
      n.py += DRIFT;
      // 帯は広めに取る(端で clamp されると壁に貼りついて偽の直線ができる)
      n.x = p.map(p.noise(n.px), 0.22, 0.78, MARGIN, W - MARGIN, true);
      n.y = p.map(p.noise(n.py, 90), 0.22, 0.78, MARGIN, H - MARGIN, true);
    }

    // 結ばれた点どうしは、ごくゆっくり寄る。切れはしないが、束ねもしない
    for (const l of links.values()) {
      if (!l.bonded) continue;
      const a = nodes[l.i];
      const b = nodes[l.j];
      const dx = b.x - a.x;
      const dy = b.y - a.y;
      a.x += dx * BOND_PULL;
      a.y += dy * BOND_PULL;
      b.x -= dx * BOND_PULL;
      b.y -= dy * BOND_PULL;
    }
  }

  // ── 近づいたら引かれる。離れたら剥がれる ──────────────────────
  function updateLinks() {
    const touched = new Set();

    for (let i = 0; i < N; i++) {
      for (let j = i + 1; j < N; j++) {
        const d = dist(nodes[i], nodes[j]);
        if (d > NEAR) continue;
        const key = i + "-" + j;
        touched.add(key);
        let l = links.get(key);
        if (!l) {
          l = { i, j, hold: 0, bonded: false, bondAge: 0 };
          links.set(key, l);
        }
        // 近いほど、確からしさの増え方が速い
        l.hold = Math.min(BOND_AT, l.hold + HOLD_GAIN * (1 - d / NEAR) * 1.9);
        if (!l.bonded && l.hold >= BOND_AT) l.bonded = true;
      }
    }

    for (const [key, l] of links) {
      if (l.bonded) {
        l.bondAge++;
        continue;
      }
      if (touched.has(key)) continue;
      // 離れている間は剥がれていく。ゼロになったら消える
      l.hold -= PEEL_LOSS;
      if (l.hold <= 0) links.delete(key);
    }
  }

  // ── 描く ────────────────────────────────────────────────────
  function drawLinks() {
    p.noFill();
    for (const l of links.values()) {
      const a = nodes[l.i];
      const b = nodes[l.j];

      if (l.bonded) {
        // 結ばれた線は距離を選ばない。長くても切れない
        const settle = Math.min(1, l.bondAge / 220);
        p.stroke(INK[0], INK[1], INK[2], p.lerp(120, 205, settle));
        p.strokeWeight(p.lerp(1.7, 2.6, settle));
      } else {
        // まだ本物か分からない線。近さで生まれて、離れれば消える
        const t = l.hold / BOND_AT;
        p.stroke(INK[0], INK[1], INK[2], 26 + 122 * t * t);
        p.strokeWeight(0.95 + 1.4 * t);
      }
      p.line(a.x, a.y, b.x, b.y);
    }
  }

  function drawNodes() {
    p.noStroke();
    for (const n of nodes) {
      p.fill(DOT[0], DOT[1], DOT[2], 150);
      p.circle(n.x, n.y, 3.4);
    }
  }

  function dist(a, b) {
    return Math.hypot(b.x - a.x, b.y - a.y);
  }

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