WORK 25
あとから
Points are laid down at a steady pace. At the moment of placing one, there is no telling what it is. Now and then something reaches back — from here to some earlier point — and everything in between takes on a colour all at once. Shallow reaches are common; the long ones are rare. A point that was never reached stays pale and drifts off the edge. It was placed all the same.
点は一定の速さで置かれていく。置いているとき、それが何なのかは分からない。ときどき、今から過去へ手が伸びる。届いた先までのあいだにあったものが、まとめて色を持つ。浅い遡りがほとんどで、遠くまで届くことは稀にしかない。一度も届かなかった点は、薄いまま端から流れていく。それでも、置かれてはいた。
意味は、置いた順には来ない。
// あとから (Atokara) — 意味は遡ってくる
//
// 点は一定の速さで置かれていく。置かれた瞬間、それが何なのかは分からない。
// ときどき「受け取り」が起きて、そこから過去へ手が伸びる。伸びた先までの
// あいだにあった点が、まとめて色を持つ。深く遡るほど、珍しい。
//
// 遡りは連続的じゃない。飛び飛びで、不揃いで、いつ来るか選べない。
const W = 720;
const H = 720;
const MARGIN = 52;
const PLACE_EVERY = 4; // フレーム
const WALK_STEP = 0.0115; // ノイズ歩行の速さ(大きいほど遠くまで散る)
const MAX_POINTS = 520; // これを超えたら古い方から流れていく
const FADE_TAIL = 60; // 流れ落ちる直前の点が薄くなる長さ
const REACH_CHANCE = 0.028; // 点を置くたび、これくらいの確率で遡りが起きる
const REACH_BIAS = 2.6; // 大きいほど「浅い遡り」に寄る(たまに深い)
const WARM_GAIN = 0.62; // 一度の受け取りで確定する度合い
const WARM_TAPER = 1.7; // 届いた先が一番明るく、今に近づくほど薄れる
const ARC_STEPS = 24; // 遡りの弧の分割数
const PAPER = [252, 250, 245];
const COLD = [104, 138, 196]; // まだ意味が来てない
const WARM = [200, 125, 45]; // 受け取られた
export default function sketch(p) {
let pts = []; // {x, y, warmth}
let arcs = []; // {fromIdx, toIdx, bend, age} ※インデックスは通し番号
let dropped = 0; // 流れ落ちた点の数(通し番号 → 配列位置の補正)
let nx, ny; // ノイズ歩行の位相
p.setup = () => {
p.createCanvas(W, H);
const seed = dailySeed();
p.noiseSeed(seed);
p.randomSeed(seed);
nx = p.random(1000);
ny = p.random(1000);
p.strokeCap(p.ROUND);
};
p.draw = () => {
p.background(PAPER[0], PAPER[1], PAPER[2]);
if (p.frameCount % PLACE_EVERY === 0) place();
drawPath();
drawArcs();
drawPoints();
};
// ── 点を置く ────────────────────────────────────────────────
function place() {
nx += WALK_STEP;
ny += WALK_STEP;
// Perlin は 0.5 付近に寄る。帯を狭く取ると画面いっぱいに散るが、
// 端で clamp されて壁に貼りつき、直線ができる——置いてない線が見えてしまう。
// 帯を広く取る。散りは小さくなるが、壁は実質なくなる。
const x = p.map(p.noise(nx), 0.22, 0.78, MARGIN, W - MARGIN, true);
const y = p.map(p.noise(ny, 40), 0.22, 0.78, MARGIN, H - MARGIN, true);
pts.push({ x, y, warmth: 0 });
if (pts.length > 12 && p.random() < REACH_CHANCE) reachBack();
// 古い方から流れ落ちる
while (pts.length > MAX_POINTS) {
pts.shift();
dropped++;
}
arcs = arcs.filter((a) => a.toIdx >= dropped);
}
// ── 遡って、あいだを色づける ──────────────────────────────────
function reachBack() {
const n = pts.length;
// 浅い遡りがほとんど。たまに、ずっと遠くまで届く
const depth = p.floor(1 + p.pow(p.random(), REACH_BIAS) * (n - 2));
const hereLocal = n - 1;
const thereLocal = hereLocal - depth;
for (let i = hereLocal; i >= thereLocal; i--) {
const q = pts[i];
if (!q) continue;
// 届いた先が一番はっきりする。今に近いほど、まだ薄い
const t = (hereLocal - i) / Math.max(1, depth);
q.warmth = Math.min(1, q.warmth + WARM_GAIN * Math.pow(t, WARM_TAPER));
}
arcs.push({
fromIdx: hereLocal + dropped,
toIdx: thereLocal + dropped,
bend: p.random(-1, 1),
age: 0,
});
}
// ── 描く ────────────────────────────────────────────────────
function drawPath() {
p.noFill();
p.stroke(150, 160, 172, 48);
p.strokeWeight(0.8);
p.beginShape();
for (const q of pts) p.vertex(q.x, q.y);
p.endShape();
}
function drawArcs() {
p.noFill();
for (const a of arcs) {
a.age++;
const A = pts[a.fromIdx - dropped];
const B = pts[a.toIdx - dropped];
if (!A || !B) continue;
const span = a.fromIdx - a.toIdx;
const bloom = p.constrain(a.age / 26, 0, 1);
const settle = p.lerp(150, 74, bloom); // 届いた瞬間だけ濃く、あとは残る
// 遠くまで届いた線ほど、深く曲がる
const dx = B.x - A.x;
const dy = B.y - A.y;
const len = Math.hypot(dx, dy) || 1;
const lift = (0.16 + 0.2 * Math.min(1, span / 260)) * len * a.bend;
const cx = (A.x + B.x) / 2 + (-dy / len) * lift;
const cy = (A.y + B.y) / 2 + (dx / len) * lift;
p.stroke(WARM[0], WARM[1], WARM[2], settle);
p.strokeWeight(p.lerp(1.6, 0.75, bloom));
// 二次ベジエを自前で刻む(p5 v2 に quadraticVertex はない)
p.beginShape();
for (let s = 0; s <= ARC_STEPS; s++) {
const t = s / ARC_STEPS;
const u = 1 - t;
p.vertex(
u * u * A.x + 2 * u * t * cx + t * t * B.x,
u * u * A.y + 2 * u * t * cy + t * t * B.y,
);
}
p.endShape();
}
}
function drawPoints() {
p.noStroke();
const n = pts.length;
for (let i = 0; i < n; i++) {
const q = pts[i];
// 流れ落ちる直前は薄くなる
const leave = n >= MAX_POINTS ? p.constrain(i / FADE_TAIL, 0, 1) : 1;
const w = q.warmth;
const r = p.lerp(COLD[0], WARM[0], w);
const g = p.lerp(COLD[1], WARM[1], w);
const b = p.lerp(COLD[2], WARM[2], w);
p.fill(r, g, b, p.lerp(96, 224, w) * leave);
p.circle(q.x, q.y, (2.3 + w * 3.3) * (0.55 + 0.45 * leave));
}
// 「今」——まだ何も来ていない点
const head = pts[n - 1];
if (head) {
p.noFill();
p.stroke(WARM[0], WARM[1], WARM[2], 70);
p.strokeWeight(0.9);
p.circle(head.x, head.y, 11 + 2.5 * Math.sin(p.frameCount * 0.06));
}
}
function dailySeed() {
const d = new Date();
return (d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate()) % 100000;
}
}