WORK 100
おしはかる
A point travels across the dark, and someone far away can see only the point. They keep guessing where it will be next. While it moves, the guesses fall close together in a narrow fan ahead of it. When it stops, there is nothing to go on, and the guesses scatter in every direction, farther the longer it stays still. When the point later passes through a guess, that guess lights up. The guesses that came true become the road. The ones that never did are not erased. They stay, faint, and slowly turn into the sky the road runs under. Of all the guesses made while it stood still, the only ones that came true lie along the single way it finally left.
暗いところを、ひとつの点が旅をしている。遠くにいる誰かには、その点しか見えない。だから、次にどこにいるかを推し量りつづける。点が動いているあいだは、推し量りは進む先の細い扇の中に、近く固まって落ちる。点が止まると、手がかりがなくなって、推し量りは全方位に散らばる。止まっている時間が長いほど、遠くまで。あとで点がそこを通ると、その推し量りは灯る。当たったものが、道になる。当たらなかったものは消されない。薄く残って、少しずつ、道の上にかかる空になる。止まっていたあいだの推し量りのうち、当たったのは、点が最後に出ていった一本の向きに並んだものだけだった。
// #100 おしはかる / Guessing Where
// A point travels. Someone far away sees only the point, and keeps guessing
// where it will be next. While it moves, the guesses fall in a narrow fan
// ahead. When it stops, they scatter in every direction, farther the longer
// it stays still. A guess the point later passes through lights up.
export default function sketch(p) {
const W = 720, H = 720;
const CYCLE = 5200; // frames before the journey starts over
const MAX_GUESSES = 9000;
const HIT_R = 7;
let traveler, plan, step, waitLeft, stillFor;
let trail, guesses, cycleStart;
const makePlan = () => {
const pts = [];
const home = { x: p.random(110, 200), y: p.random(520, 610) };
// a small loop out and back: something was forgotten
pts.push({ x: home.x + 38, y: home.y - 22, stop: 0 });
pts.push({ x: home.x + 10, y: home.y - 44, stop: 0 });
pts.push({ x: home.x + 2, y: home.y - 4, stop: 70 });
// then the real journey: long reaches, broken by stops
let x = home.x, y = home.y;
let dir = p.random(-1.2, -0.5); // up and to the right, roughly
for (let i = 0; i < 9; i++) {
dir += p.randomGaussian(0, 0.55);
const reach = p.random(90, 190);
x = p.constrain(x + Math.cos(dir) * reach, 70, W - 70);
y = p.constrain(y + Math.sin(dir) * reach, 70, H - 70);
// steer back toward the middle if we hugged a wall
if (x <= 70 || x >= W - 70 || y <= 70 || y >= H - 70) {
dir = Math.atan2(H / 2 - y, W / 2 - x) + p.randomGaussian(0, 0.4);
}
const stop = p.random() < 0.45 ? Math.floor(p.random(160, 520)) : 0;
pts.push({ x, y, stop });
}
pts[pts.length - 1].stop = 9999; // the last place it stays
return { home, pts };
};
const reset = () => {
plan = makePlan();
traveler = { x: plan.home.x, y: plan.home.y, h: -0.6, v: 0 };
step = 0;
waitLeft = 160; // it lingers at home before leaving
stillFor = 0;
trail = [];
guesses = [];
cycleStart = p.frameCount;
};
p.setup = () => {
p.createCanvas(W, H);
p.colorMode(p.HSB, 360, 100, 100, 1);
p.randomSeed(100);
p.noiseSeed(100);
reset();
};
const moveTraveler = () => {
if (waitLeft > 0) {
waitLeft--;
traveler.v *= 0.85;
stillFor++;
return;
}
const target = plan.pts[step];
if (!target) { stillFor++; return; }
const dx = target.x - traveler.x, dy = target.y - traveler.y;
const d = Math.hypot(dx, dy);
if (d < 4) {
waitLeft = target.stop;
step++;
return;
}
stillFor = 0;
const want = Math.atan2(dy, dx);
let diff = want - traveler.h;
while (diff > Math.PI) diff -= p.TWO_PI;
while (diff < -Math.PI) diff += p.TWO_PI;
traveler.h += p.constrain(diff, -0.06, 0.06)
+ (p.noise(p.frameCount * 0.01) - 0.5) * 0.04;
traveler.v = p.lerp(traveler.v, Math.min(1.7, 0.4 + d * 0.03), 0.08);
traveler.x += Math.cos(traveler.h) * traveler.v;
traveler.y += Math.sin(traveler.h) * traveler.v;
if (p.frameCount % 2 === 0) trail.push({ x: traveler.x, y: traveler.y });
};
const guess = () => {
let ang, dist, still;
if (stillFor > 6) {
still = true;
ang = p.random(p.TWO_PI);
const reach = Math.min(18 + 7.5 * Math.sqrt(stillFor), 240);
dist = Math.pow(p.random(), 0.6) * reach;
} else {
still = false;
ang = traveler.h + p.randomGaussian(0, 0.22);
dist = p.random(14, 30) + traveler.v * p.random(10, 60);
}
guesses.push({
x: traveler.x + Math.cos(ang) * dist,
y: traveler.y + Math.sin(ang) * dist,
age: 0, hit: 0, still,
});
if (guesses.length > MAX_GUESSES) guesses.shift();
};
p.draw = () => {
if (p.frameCount - cycleStart > CYCLE) reset();
moveTraveler();
guess();
if (stillFor > 120 && p.frameCount % 2 === 0) guess();
// a guess the point passes through comes true
for (const g of guesses) {
g.age++;
if (!g.hit && traveler.v > 0.2 &&
Math.abs(g.x - traveler.x) < HIT_R && Math.abs(g.y - traveler.y) < HIT_R) {
g.hit = 1;
}
}
p.background(225, 40, 7);
// guesses
p.noStroke();
for (const g of guesses) {
const fresh = Math.max(0, 1 - g.age / 220);
if (g.hit) {
p.fill(g.still ? 18 : 38, 72, 98, 0.8);
p.circle(g.x, g.y, 2.6);
} else {
const a = 0.17 + 0.4 * fresh;
p.fill(g.still ? 205 : 190, 45, 85, a);
p.circle(g.x, g.y, 1.6 + fresh * 1.2);
}
}
// the path it actually took
p.noFill();
p.stroke(40, 20, 95, 0.32);
p.strokeWeight(1);
p.beginShape();
for (const t of trail) p.vertex(t.x, t.y);
p.endShape();
// the point itself
p.noStroke();
p.fill(40, 25, 100, 0.25);
p.circle(traveler.x, traveler.y, 14);
p.fill(40, 10, 100, 1);
p.circle(traveler.x, traveler.y, 5);
};
}