← INDEX / 一覧 NYAMCO WORKS 29 / 71
p5.js sketch works/yosou/sketch.js runs here
作品はここで実行されます(後から差し替え)

WORK 29

Yosou

よそう

YEAR
2026
TOOLS
p5.js
TECHNIQUE
prediction error, dead reckoning, trace

A line walks. At every step it guesses where it will go next — the simplest guess there is, just keep going the way you were going. Where the guess is right, nothing is left behind. Only the places it failed stay lit. The path you can see is not the path it took. It is the record of being surprised.

線が歩く。一歩ごとに、次の場所を予想する——いちばん素朴な予想、今までと同じように進むだけ。当たったところには、何も残らない。外したところだけが明るく残る。見えている道は、通った道じゃない。驚いた記録の方。

前の作品「なじむ」は、揃うと見えなくなる話だった。これはその裏側。ずれるから、そこがある。

先へ伸びる細い線と輪は、その瞬間の予想。実際が来ると消える。残るのは、予想から離れた分だけ。

予想が完璧になったら、この絵には何も残らない。

でも、外しっぱなしでも残らない。外した点が続けて立つと、にじみが重なって一つの塊になる。驚きが続くと、一つ一つの驚きが区別できなくなる。

SOURCE / ソース
// よそう (Yosou) — 当たった予想は、跡を残さない
//
// 線が歩く。一歩ごとに、次の場所を予想する——いちばん素朴な予想、
// 今までと同じ向きに、同じ速さで進むだけ(推測航法)。
//
// 予想どおりに進んだところには、ほとんど何も残らない。
// 外したぶんだけ、そこが明るくなる。
//
// だから見えている道は、通った道じゃない。驚いた記録の方。
//
// 前作「なじむ」の裏側にあたる。あれは、揃うと見えなくなる話だった。
// これは、ずれるから、そこがある、という話。

const W = 720;
const H = 720;

const STEP = 2.05; // 一歩の距離
const TURN = 0.052; // ゆるやかな蛇行の強さ
const NOISE_SPEED = 0.0042; // 蛇行のノイズが進む速さ

const JOLT_CHANCE = 0.021; // 急に曲がる確率(世界の側の予測不可能性)
const JOLT_BIAS = 0.55; // 小さいほど大きく振れる(ヘビーテール)
const JOLT_MAX = 1.45; // rad

const PULL = 0.00042; // 中心へのゆるい引き戻し。壁の代わり
const PULL_R = 300; // これより外に出ると効き始める

const ERR_FULL = STEP * 1.35; // このズレで最大の明るさ
const MARK_BASE = 15; // 通った道そのものの濃さ(紙の凹みくらい)
const MARK_GAIN = 205;
const TRAIL = 34; // 予想線の長さ

const PAPER = [252, 250, 245];
const COLD = [104, 138, 196]; // 予想の範囲内
const WARM = [200, 125, 45]; // 外した

export default function sketch(p) {
  let layer; // 跡(消えない)
  let x, y, px, py; // 現在地と、ひとつ前
  let heading;
  let nt; // ノイズの位相
  let predX, predY; // いま出している予想
  let lastErr = 0;

  p.setup = () => {
    p.createCanvas(W, H);
    const seed = dailySeed();
    p.noiseSeed(seed);
    p.randomSeed(seed);

    layer = p.createGraphics(W, H);
    layer.clear();
    layer.noStroke();

    nt = p.random(1000);
    heading = p.random(p.TWO_PI);
    x = W / 2;
    y = H / 2;
    px = x - Math.cos(heading) * STEP;
    py = y - Math.sin(heading) * STEP;
    predX = x;
    predY = y;

    p.strokeCap(p.ROUND);
  };

  p.draw = () => {
    p.background(PAPER[0], PAPER[1], PAPER[2]);

    for (let k = 0; k < 2; k++) advance();

    p.image(layer, 0, 0);
    drawGuess();
  };

  // ── 一歩 ────────────────────────────────────────────────
  function advance() {
    // 1. 予想する。前の一歩をそのまま延長するだけ
    const vx = x - px;
    const vy = y - py;
    predX = x + vx;
    predY = y + vy;

    // 2. 実際に進む
    // Perlin は 0.5 付近に寄るので、狭い帯を全体に引き伸ばす
    const wobble = p.map(p.noise(nt), 0.3, 0.7, -1, 1, true);
    heading += wobble * TURN;
    nt += NOISE_SPEED;

    // ときどき、世界の方が急に曲がる
    if (p.random() < JOLT_CHANCE) {
      const mag = Math.pow(p.random(), JOLT_BIAS) * JOLT_MAX;
      heading += p.random() < 0.5 ? -mag : mag;
    }

    let nx = x + Math.cos(heading) * STEP;
    let ny = y + Math.sin(heading) * STEP;

    // 中心へのゆるい引き戻し。予想には入っていない力
    const dx = nx - W / 2;
    const dy = ny - H / 2;
    const d = Math.hypot(dx, dy);
    if (d > PULL_R) {
      const over = d - PULL_R;
      nx -= dx * PULL * over;
      ny -= dy * PULL * over;
      heading = Math.atan2(ny - y, nx - x);
    }

    // 3. 予想とのズレ
    const err = p.constrain(p.dist(predX, predY, nx, ny) / ERR_FULL, 0, 1);
    lastErr = err;
    mark(x, y, nx, ny, err);

    px = x;
    py = y;
    x = nx;
    y = ny;
  }

  // ── 跡 ──────────────────────────────────────────────────
  //
  // 通った道そのものは、紙の凹みくらいにしか残らない。
  // 外したところにだけ、丸が立つ。
  function mark(fx, fy, mx, my, err) {
    const e = Math.pow(err, 1.55);

    const cr = p.lerp(COLD[0], WARM[0], e);
    const cg = p.lerp(COLD[1], WARM[1], e);
    const cb = p.lerp(COLD[2], WARM[2], e);

    // 通った線。当たっている限り、これしか残らない
    layer.stroke(COLD[0], COLD[1], COLD[2], MARK_BASE);
    layer.strokeWeight(0.75);
    layer.line(fx, fy, mx, my);
    layer.noStroke();

    if (err < 0.05) return; // 予想どおり。ここには何も置かない

    const a = e * MARK_GAIN + 10;
    const r = 1.4 + Math.pow(err, 1.25) * 10.5;

    // 大きく外した点だけ、まわりに薄いにじみが出る
    if (e > 0.12) {
      layer.fill(cr, cg, cb, a * 0.16);
      layer.circle(mx, my, r * 3.4);
    }
    layer.fill(cr, cg, cb, a);
    layer.circle(mx, my, r);
  }

  // ── いまの予想(残らない) ────────────────────────────
  function drawGuess() {
    const vx = x - px;
    const vy = y - py;
    const len = Math.hypot(vx, vy) || 1;
    const ux = vx / len;
    const uy = vy / len;

    // 予想の方向へ伸びる線。先へ行くほど薄い
    const segs = 9;
    for (let i = 0; i < segs; i++) {
      const t0 = i / segs;
      const t1 = (i + 1) / segs;
      const a = (1 - t0) * 52;
      p.stroke(COLD[0], COLD[1], COLD[2], a);
      p.strokeWeight(0.9);
      p.line(
        x + ux * TRAIL * t0,
        y + uy * TRAIL * t0,
        x + ux * TRAIL * t1,
        y + uy * TRAIL * t1,
      );
    }

    // 予想した点
    p.noFill();
    p.stroke(COLD[0], COLD[1], COLD[2], 96);
    p.strokeWeight(0.9);
    p.circle(predX, predY, 7.5);

    // 予想と実際のあいだ。外したときだけ見える
    if (lastErr > 0.06) {
      p.stroke(WARM[0], WARM[1], WARM[2], 40 + lastErr * 150);
      p.strokeWeight(0.9 + lastErr * 1.5);
      p.line(predX, predY, x, y);
    }

    // 現在地
    p.noStroke();
    p.fill(60, 58, 54, 190);
    p.circle(x, y, 3.6);
  }

  function dailySeed() {
    const d = new Date();
    return (d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate()) % 100000;
  }
}