WORK 104
しみる
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.
紙に、しずくが落ちる。落ちたところが濃くなって、広がっていく。はじめは速く、だんだん遅く。しばらくすると乾きはじめる。乾くとき、色は均等には薄くならない。縁の方へ寄っていって、真ん中が白くなる。紙に残るのは、縁の方。ときどき、指先が濡れているしみを押さえる。押さえても乾かない。しみは少しだけ広がって、最後に残る輪は、押さえなかったときより大きくなる。輪は重なって、交差していく。どの輪も、しずくが最初に落ちた場所は教えてくれない。
種:みゅうの断片②「鍵」の、押さえても乾かない汗の染み。
// #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);
}
}
};
}