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

WORK 42

Filling In

うまる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / Laplace relaxation, iterative inpainting, contour rendering

A field is punctured, and each hole is filled from its own edges — the smoothest possible answer, seamless every time. No single filling is wrong. After a hundred of them, the pattern that was there is gone. Not thinned out: redistributed. Where the fillings met, the lines crowd; where they landed, nothing is left to say.

場に穴を開けて、まわりの値だけで埋め戻す。一回ごとの埋め方に間違いはない。継ぎ目もない。それを百回くり返すと、元の模様はもうない。減ったんじゃなくて、偏った。埋めたところは空っぽになり、埋めそこねた縁に線が押し寄せる。

穴を埋めるのに使えるのは、その穴のまわりにあるものだけ。だから埋まったところには、まわりの情報しか入っていない。新しいものは、一切生まれない。

それでも埋まったあとの場は連続していて、どこにも継ぎ目がない。もっともらしさは、そこに何かがある証拠にならない。

一箇所だけ、一度も穴の開かなかった領域がある。どこかは決めていない。

SOURCE / ソース
/**
 * umaru — 埋まる / Filling In
 *
 * 場に穴を開ける。穴は、周りの値から埋め戻される。
 * 一回一回の埋め方は正しい。境界と滑らかに繋がり、どこにも不連続がない。
 * それを何十回も繰り返すと、元の模様は残っていない。
 *
 * どの一回にも、間違いはない。
 */

const G = 200; // 場の解像度
const SIZE = 720;

const ROUNDS = 110; // 穴を開けて埋める回数
const R_MIN = 9;
const R_MAX = 26;
const RELAX = 90; // 一回あたりの拡散反復

const STRIPE = 15.5; // 縞の細かさ
const INK = [38, 42, 52];
const PAPER = [252, 250, 245];

export default function sketch(p) {
  let field;

  const idx = (x, y) => y * G + x;

  p.setup = () => {
    p.createCanvas(SIZE, SIZE);
    p.pixelDensity(1);

    const seed = 42115;
    p.noiseSeed(seed);
    p.randomSeed(seed);

    field = new Float32Array(G * G);

    // --- 元の模様。方向を持った、細かい層 ---
    p.noiseDetail(4, 0.55);
    for (let y = 0; y < G; y++) {
      for (let x = 0; x < G; x++) {
        const u = x / G;
        const v = y / G;
        // 斜めに走る層 + ゆらぎ。密度がどこも同じ状態から始める
        const warp = p.noise(u * 2.3, v * 2.3) - 0.5;
        const fine = p.noise(u * 9.0 + 40, v * 9.0 + 40) - 0.5;
        field[idx(x, y)] =
          (u * 0.62 + v * 0.38) * 3.1 + warp * 2.4 + fine * 0.55;
      }
    }

    // --- 一度も穴が開かない場所。どこかは決めていない ---
    p.noiseDetail(2, 0.5);
    const guard = new Float32Array(G * G);
    for (let y = 0; y < G; y++) {
      for (let x = 0; x < G; x++) {
        guard[idx(x, y)] = p.noise((x / G) * 2.4 + 300, (y / G) * 2.4 + 300);
      }
    }

    const mask = new Uint8Array(G * G);
    const buf = new Float32Array(G * G);

    for (let round = 0; round < ROUNDS; round++) {
      const cx = Math.floor(p.random(G));
      const cy = Math.floor(p.random(G));
      if (guard[idx(cx, cy)] > 0.57) continue; // 守られている側に落ちたら、何も起きない

      const r = p.random(R_MIN, R_MAX);
      const r2 = r * r;
      const x0 = Math.max(1, Math.floor(cx - r));
      const x1 = Math.min(G - 2, Math.ceil(cx + r));
      const y0 = Math.max(1, Math.floor(cy - r));
      const y1 = Math.min(G - 2, Math.ceil(cy + r));

      mask.fill(0);
      let any = false;
      for (let y = y0; y <= y1; y++) {
        for (let x = x0; x <= x1; x++) {
          const dx = x - cx;
          const dy = y - cy;
          if (dx * dx + dy * dy <= r2 && guard[idx(x, y)] <= 0.57) {
            mask[idx(x, y)] = 1;
            any = true;
          }
        }
      }
      if (!any) continue;

      // 穴の中身を捨てる。残っているのは縁だけ
      for (let y = y0; y <= y1; y++) {
        for (let x = x0; x <= x1; x++) {
          if (mask[idx(x, y)]) field[idx(x, y)] = 0;
        }
      }

      // 縁から内側へ、平均で埋め戻す(ラプラス方程式の緩和)
      for (let it = 0; it < RELAX; it++) {
        for (let y = y0; y <= y1; y++) {
          for (let x = x0; x <= x1; x++) {
            const i = idx(x, y);
            if (!mask[i]) continue;
            buf[i] =
              (field[i - 1] + field[i + 1] + field[i - G] + field[i + G]) * 0.25;
          }
        }
        for (let y = y0; y <= y1; y++) {
          for (let x = x0; x <= x1; x++) {
            const i = idx(x, y);
            if (mask[i]) field[i] = buf[i];
          }
        }
      }
    }

    render();
    p.noLoop();
  };

  const sample = (fx, fy) => {
    const x = Math.min(G - 2, Math.max(0, fx));
    const y = Math.min(G - 2, Math.max(0, fy));
    const xi = Math.floor(x);
    const yi = Math.floor(y);
    const tx = x - xi;
    const ty = y - yi;
    const a = field[idx(xi, yi)];
    const b = field[idx(xi + 1, yi)];
    const c = field[idx(xi, yi + 1)];
    const d = field[idx(xi + 1, yi + 1)];
    return (
      a * (1 - tx) * (1 - ty) +
      b * tx * (1 - ty) +
      c * (1 - tx) * ty +
      d * tx * ty
    );
  };

  function render() {
    p.background(PAPER[0], PAPER[1], PAPER[2]);
    p.loadPixels();
    const s = G / SIZE;
    const px = p.pixels;

    for (let py = 0; py < SIZE; py++) {
      for (let pxi = 0; pxi < SIZE; pxi++) {
        const v = sample(pxi * s, py * s);
        // 縞。値の変化が速いほど線が細かく詰まる
        const band = Math.abs(Math.sin(v * STRIPE));
        // 線として立てる:0 に近いところだけ濃く
        const ink = Math.pow(1 - band, 7.5);

        const o = (py * SIZE + pxi) * 4;
        px[o] = PAPER[0] + (INK[0] - PAPER[0]) * ink;
        px[o + 1] = PAPER[1] + (INK[1] - PAPER[1]) * ink;
        px[o + 2] = PAPER[2] + (INK[2] - PAPER[2]) * ink;
        px[o + 3] = 255;
      }
    }
    p.updatePixels();
  }
}