WORK 60
ふる
Small things sway across the field. An unseen point drifts through, and wherever it passes, the swaying stops. Stopping is quick; loosening is slow. What has stopped is not dead — only looked at.
画面じゅうで小さなものが揺れている。見えない点がゆっくり巡回していて、通りかかったところは揺れが止まる。止まるのは速く、ほどけるのは遅い。止まっているところは、死んでいるわけではない。見られているだけ。
観測点は描かれない。だから画面に見えるのは、静かになった場所のほうだけ。
描いてから気づいたこと。止まったものの方が、きれいに見える。揺れをなくせば向きが揃うのだから当たり前なのだけれど、揃うと整って見える、というところまでは考えていなかった。だから画面でいちばん目を引くのは、その静かなところになる。動いているものは散らかっていて、目に留まらない。
// ── ふる (Furu) ──────────────────────────────────────────────────
// 画面じゅうで小さなものが揺れている。
// 見えない観測点がゆっくり巡回していて、通りかかったところは揺れが止まる。
// 止まるのは速く、戻るのは遅い。観測点そのものは描かれない。
export default function sketch(p) {
const W = 720, H = 720;
const COLS = 26, ROWS = 26; // 毛の格子
const JOINTS = 4; // 一本あたりの節(根元→先)
const SEG_LEN = 7.0; // 節の長さ
const SWAY = 0.72; // 揺れの最大角(rad)
const GAZE_R = 132; // 観測点の効く半径
const CATCH = 0.34; // 止まるときの追従(速い)
const RELEASE = 0.022; // ほどけるときの追従(遅い)
let hairs;
let t = 0;
let bg;
p.setup = () => {
p.createCanvas(W, H);
// 今日の「ふる」は今日だけのもの
const d = new Date();
const day = d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
p.noiseSeed(day * 613);
p.randomSeed(day * 379);
// ---- 背景(わずかにむらのある暗い面) ----
bg = p.createGraphics(W, H);
bg.noStroke();
const step = 8;
for (let x = 0; x < W; x += step) {
for (let y = 0; y < H; y += step) {
const n = p.noise(x * 0.005, y * 0.005);
bg.fill(13 + n * 7, 13 + n * 6, 16 + n * 9);
bg.rect(x, y, step, step);
}
}
// ---- 毛を格子に置く(少しずらす) ----
hairs = [];
const mx = W / (COLS + 1), my = H / (ROWS + 1);
for (let i = 0; i < COLS; i++) {
for (let j = 0; j < ROWS; j++) {
hairs.push({
x: mx * (i + 1) + p.random(-mx * 0.28, mx * 0.28),
y: my * (j + 1) + p.random(-my * 0.28, my * 0.28),
base: p.random(p.TWO_PI), // 生えている向き
phase: p.random(1000), // 揺れの位相(個体ごと)
rate: p.random(0.010, 0.021), // 揺れの速さ
still: 0, // 0=揺れてる 1=止まってる
});
}
}
};
// 観測点。リサージュでゆっくり巡回する。描かない。
const gazeAt = (tt) => ({
x: W / 2 + Math.sin(tt * 0.00381) * (W * 0.36) + Math.sin(tt * 0.00117) * 52,
y: H / 2 + Math.cos(tt * 0.00265) * (H * 0.36) + Math.cos(tt * 0.00153) * 44,
});
p.draw = () => {
p.image(bg, 0, 0);
const g = gazeAt(t);
p.noFill();
for (const h of hairs) {
// ---- 観測されているか ----
const d = p.dist(h.x, h.y, g.x, g.y);
const target = d < GAZE_R ? 1 - (d / GAZE_R) * (d / GAZE_R) : 0;
// 止まるのは速く、ほどけるのは遅い
const k = target > h.still ? CATCH : RELEASE;
h.still += (target - h.still) * k;
const amp = SWAY * (1 - h.still);
// ---- 節をつないで描く ----
let x = h.x, y = h.y;
let ang = h.base - amp * 0.5;
// 止まっているものほど色が抜けて、冷たくなる
const warm = 1 - h.still;
const r = 96 + warm * 122;
const gg = 104 + warm * 74;
const b = 128 + warm * 18;
p.stroke(r, gg, b, 150 + warm * 85);
p.strokeWeight(1.25 - h.still * 0.45);
p.beginShape();
p.vertex(x, y);
for (let s = 0; s < JOINTS; s++) {
const n = p.noise(h.phase + s * 0.55, t * h.rate);
ang += (n - 0.5) * 2 * amp;
x += Math.cos(ang) * SEG_LEN;
y += Math.sin(ang) * SEG_LEN;
p.vertex(x, y);
}
p.endShape();
// 先端。止まっているものだけ、かすかに点が残る
if (h.still > 0.12) {
p.noStroke();
p.fill(150, 158, 176, h.still * 105);
p.circle(x, y, 1.7 + h.still * 1.5);
p.noFill();
}
}
t += 1;
};
}