WORK 62
さす
Each dot carries a needle that measures its own speed. The needle never lies. Speeds drift toward a common value, so in time every needle points the same way — perfect agreement, arriving. Meanwhile the dots themselves scatter and do not come back. The needle points at a quantity, not at a destination. Reading the same, and getting closer, turn out to be two different things.
点はそれぞれ針を持っていて、針は自分の速さを測っている。嘘は一つもない。速さは少しずつ共通の値へ寄っていくので、やがてすべての針がおなじ方を指す。読みは、そろう。そのあいだ点そのものは散っていって、戻ってこない。針が指しているのは量であって、行き先ではない。読みが合うことと、近づくことは、別のことだった。
針の角度は、速さだけで決まる。位置も、向きも、一文字も入っていない。
画面の端は巻き戻していない。出ていったものは、出ていったままにしてある。
// ── さす (Needle) ────────────────────────────────────────────────
// 針は正直だ。速さをちゃんと測って、そのぶんだけ傾く。嘘は一つもない。
// 速さは少しずつ揃っていくので、針もやがて全部おなじ方を指す。
// でも針が指しているのは値であって、その点がどこへ行くかではない。
// 読みが揃っていくあいだ、点そのものは散っていく。同時に、両方。
export default function sketch(p) {
const W = 720, H = 720;
const N = 52;
const SPEED_TARGET = 1.30; // 速さが寄っていく先
const CONVERGE = 0.0042; // 速さだけにかかる引力(向きには一切かからない)
const TURN = 0.020; // 向きのランダムウォーク幅
const NEEDLE = 23; // 針の長さ
const TRAIL = 140; // 足跡の保持数
const S_MIN = 0.30, S_MAX = 2.55; // 針の目盛りの両端
let pts = [];
let ink, needleCol;
p.setup = () => {
p.createCanvas(W, H);
const d = new Date();
const day = d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
p.randomSeed(day * 419);
p.noiseSeed(day * 977);
ink = p.color(30, 32, 38);
needleCol = p.color(198, 62, 74);
for (let i = 0; i < N; i++) {
pts.push({
x: p.width / 2 + p.random(-46, 46),
y: p.height / 2 + p.random(-46, 46),
a: p.random(p.TWO_PI),
s: p.random(S_MIN + 0.04, S_MAX - 0.04), // 最初はばらばら
seed: p.random(1000),
trail: [],
});
}
p.background(250, 250, 247);
};
const wrap = (q) => {
let jumped = false;
if (q.x < -8) { q.x += W + 16; jumped = true; }
else if (q.x > W + 8) { q.x -= W + 16; jumped = true; }
if (q.y < -8) { q.y += H + 16; jumped = true; }
else if (q.y > H + 8) { q.y -= H + 16; jumped = true; }
return jumped;
};
p.draw = () => {
p.background(250, 250, 247, 74);
for (const q of pts) {
// 向き: 誰とも相談しない。ただ勝手に曲がる
q.a += (p.noise(q.seed, p.frameCount * 0.0038) - 0.5) * TURN * 2;
// 速さ: ゆっくり共通の値へ寄る
q.s += (SPEED_TARGET - q.s) * CONVERGE;
q.x += Math.cos(q.a) * q.s;
q.y += Math.sin(q.a) * q.s;
if (wrap(q)) q.trail.length = 0; // 縁をまたいだ足跡は繋がない
q.trail.push([q.x, q.y]);
if (q.trail.length > TRAIL) q.trail.shift();
}
// 足跡
p.noFill();
p.stroke(30, 32, 38, 27);
p.strokeWeight(1);
for (const q of pts) {
if (q.trail.length < 2) continue;
p.beginShape();
for (const [tx, ty] of q.trail) p.vertex(tx, ty);
p.endShape();
}
// 点と針
for (const q of pts) {
// 針の角度は速さだけで決まる。位置も向きも一切入らない
const ang = p.map(q.s, S_MIN, S_MAX, -p.PI * 0.46, p.PI * 0.46, true);
p.push();
p.translate(q.x, q.y);
p.stroke(30, 32, 38, 120);
p.strokeWeight(1);
p.line(-NEEDLE * 0.3, 0, NEEDLE * 0.3, 0); // 目盛りの台座
p.stroke(needleCol);
p.strokeWeight(1.8);
p.line(0, 0, Math.sin(ang) * NEEDLE, -Math.cos(ang) * NEEDLE);
p.noStroke();
p.fill(ink);
p.circle(0, 0, 3.2);
p.pop();
}
};
}