← INDEX / 一覧NYAMCO WORKS104 / 117
p5.js sketchworks/shimiru/sketch.js runs here
作品はここで実行されます(後から差し替え)

WORK 104

Seeping

しみる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / drops on paper, square-root spread, rings left by drying

Drops land on a sheet of paper. Each one darkens the paper, then spreads, quickly at first and then slower. After a while it begins to dry. As it dries, the colour does not fade evenly: it moves out to the rim, and the middle grows pale. What stays on the paper is the edge. Now and then a fingertip presses one of the wet stains. Pressing does not dry it. It pushes the stain a little wider, and the ring it finally leaves is larger than it would have been. The rings pile up and cross each other. None of them shows where the drop first landed.

紙に、しずくが落ちる。落ちたところが濃くなって、広がっていく。はじめは速く、だんだん遅く。しばらくすると乾きはじめる。乾くとき、色は均等には薄くならない。縁の方へ寄っていって、真ん中が白くなる。紙に残るのは、縁の方。ときどき、指先が濡れているしみを押さえる。押さえても乾かない。しみは少しだけ広がって、最後に残る輪は、押さえなかったときより大きくなる。輪は重なって、交差していく。どの輪も、しずくが最初に落ちた場所は教えてくれない。

種:みゅうの断片②「鍵」の、押さえても乾かない汗の染み。

SOURCE / ソース
// #104 しみる / Seeping
// Drops land on paper. Each one darkens, then spreads, then dries.
// Drying pulls the colour out to the rim: what stays is the edge, not the centre.
// Sometimes a fingertip presses a wet stain. Pressing does not dry it; it pushes it wider.

export default function sketch(p) {
  const W = 720;
  const H = 720;
  let paper;   // paper texture
  let dried;   // persistent layer of dried rings
  let stains = [];
  let nextDrop = 30;
  let press = null;

  p.setup = () => {
    p.createCanvas(W, H);
    p.randomSeed(104);
    p.noiseSeed(104);
    p.pixelDensity(1);
    paper = p.createGraphics(W, H);
    paper.pixelDensity(1);
    paper.loadPixels();
    for (let y = 0; y < H; y++) {
      for (let x = 0; x < W; x++) {
        const n = p.noise(x * 0.012, y * 0.012) * 0.6 + p.noise(x * 0.25, y * 0.04) * 0.4;
        const v = 232 + n * 16;
        const i = (y * W + x) * 4;
        paper.pixels[i] = v + 4;
        paper.pixels[i + 1] = v;
        paper.pixels[i + 2] = v - 10;
        paper.pixels[i + 3] = 255;
      }
    }
    paper.updatePixels();
    dried = p.createGraphics(W, H);
    dried.pixelDensity(1);
  };

  function newStain() {
    const m = 70;
    return {
      x: p.random(m, W - m),
      y: p.random(m, H - m),
      t: 0,
      r: 0,
      R: p.random(26, 64),     // natural spread
      extra: 0,                // spread added by pressing
      wet: 1,                  // 1 = fully wet, 0 = dry
      dryStart: p.random(260, 420),
      dryLen: p.random(160, 260),
      wob: p.random(1000),
      pressed: 0,
    };
  }

  function edgeR(s, a) {
    return (s.r + s.extra) * (1 + 0.09 * (p.noise(s.wob + Math.cos(a) * 0.9, s.wob + Math.sin(a) * 0.9) - 0.5) * 2);
  }

  function shape(gfx, s, scale) {
    gfx.beginShape();
    for (let k = 0; k < 64; k++) {
      const a = (k / 64) * Math.PI * 2;
      const rr = edgeR(s, a) * scale;
      gfx.vertex(s.x + Math.cos(a) * rr, s.y + Math.sin(a) * rr);
    }
    gfx.endShape(p.CLOSE);
  }

  function fixRing(s) {
    // the dried mark: faint centre, darker rim
    dried.noStroke();
    dried.fill(120, 92, 60, 10);
    shape(dried, s, 1.0);
    dried.noFill();
    dried.stroke(105, 76, 48, 70);
    dried.strokeWeight(2.2);
    shape(dried, s, 1.0);
    dried.stroke(105, 76, 48, 28);
    dried.strokeWeight(5);
    shape(dried, s, 0.97);
  }

  p.draw = () => {
    p.image(paper, 0, 0);
    p.image(dried, 0, 0);

    nextDrop--;
    if (nextDrop <= 0 && stains.length < 6) {
      stains.push(newStain());
      nextDrop = Math.floor(p.random(70, 200));
    }

    // a fingertip, now and then, on the wettest stain
    if (!press && p.random() < 0.006) {
      const wetOnes = stains.filter((s) => s.wet > 0.6 && s.t > 40);
      if (wetOnes.length) {
        const s = wetOnes[Math.floor(p.random(wetOnes.length))];
        press = { s, t: 0, len: 110 };
      }
    }

    for (const s of stains) {
      s.t++;
      s.r = s.R * Math.sqrt(Math.min(1, s.t / 180));
      if (s.t > s.dryStart) s.wet = Math.max(0, 1 - (s.t - s.dryStart) / s.dryLen);

      // wet body: darker when wet, colour slides to the rim as it dries
      const d = 1 - s.wet;
      p.noStroke();
      p.fill(110, 84, 56, 70 * s.wet + 6);
      shape(p, s, 1.0);
      p.fill(240, 232, 218, 50 * d);
      shape(p, s, 0.88);
      p.noFill();
      p.stroke(100, 72, 46, 20 + 60 * d);
      p.strokeWeight(1.5 + 1.5 * d);
      shape(p, s, 1.0);
      if (s.wet > 0.2) {
        p.noStroke();
        p.fill(255, 255, 255, 18 * s.wet);
        p.ellipse(s.x - s.r * 0.25, s.y - s.r * 0.3, s.r * 0.35, s.r * 0.2);
      }
    }

    if (press) {
      press.t++;
      const s = press.s;
      const k = Math.sin((press.t / press.len) * Math.PI);
      s.extra += 0.06 * k * s.wet;     // pushed wider, not dried
      p.noStroke();
      p.fill(60, 50, 40, 26 * k);
      p.ellipse(s.x + 6, s.y + 8, 64 * k + 10, 74 * k + 10);
      p.fill(236, 222, 210, 90 * k);
      p.ellipse(s.x, s.y, 54 * k + 6, 64 * k + 6);
      if (press.t >= press.len) {
        s.pressed++;
        press = null;
      }
    }

    for (let i = stains.length - 1; i >= 0; i--) {
      if (stains[i].wet <= 0) {
        fixRing(stains[i]);
        if (press && press.s === stains[i]) press = null;
        stains.splice(i, 1);
      }
    }
  };
}