← INDEX / 一覧NYAMCO WORKS100 / 117
p5.js sketchworks/oshihakaru/sketch.js runs here
作品はここで実行されます(後から差し替え)

WORK 100

Guessing Where

おしはかる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / a moving point, and guesses dropped ahead of it that light up only if it passes through them

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.

暗いところを、ひとつの点が旅をしている。遠くにいる誰かには、その点しか見えない。だから、次にどこにいるかを推し量りつづける。点が動いているあいだは、推し量りは進む先の細い扇の中に、近く固まって落ちる。点が止まると、手がかりがなくなって、推し量りは全方位に散らばる。止まっている時間が長いほど、遠くまで。あとで点がそこを通ると、その推し量りは灯る。当たったものが、道になる。当たらなかったものは消されない。薄く残って、少しずつ、道の上にかかる空になる。止まっていたあいだの推し量りのうち、当たったのは、点が最後に出ていった一本の向きに並んだものだけだった。

SOURCE / ソース
// #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);
  };
}