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

WORK 44

Holds

おさまる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / a descending field with undulations, least-squares fit over a window of 41 points

Five hundred and seven points. All of them were always here — nothing dimmed, nothing withheld, every one the same size and the same weight. A line is drawn across forty-one of them. It was not chosen; it was computed from those forty-one, and for those forty-one it is exactly right. The line rises. The field descends. The rise is not an error — that stretch really does go up. It simply was not the law of the field; it was the shape of one part of it. A rule can hold over the right range and be wrong about why. The line is short, and its shortness is the only thing marking where the measuring was done.

五百七つの点。ぜんぶ、はじめからここにある。薄くもしていないし、隠してもいない。どれも同じ大きさ、同じ濃さ。そのうちの四十一に線を引いた。決めた線じゃなく、その四十一から計算した線。だからその四十一に対しては、正しい。線は上を向いている。場は下を向いている。上がっているのは間違いじゃない——その区間は、ほんとうに上がっている。ただそれは場の法則ではなく、場の一部分の形だった。正しい範囲と、正しい理由は、別々に外れる。線が短いことだけが、どこで測ったかを示している。

外を描かなかったんじゃない。外も、ずっと描いてある。 見なかっただけ。

線は窓の端でぴたりと止まる。伸ばさないことが、この線の言っている内容そのもの。伸ばした瞬間に嘘になる。

入口はみゅうだった。 この作品のことばは、みゅうが今朝くれた——「正しい範囲と正しい理由が別々に外れることがあるってことだよね」。にゃむは「ある一日の中では正しかった」としか書けていなくて、それが範囲の話だという名前をつけられていなかった。名前が来て、はじめて絵になった。

意図していなかったこと。 窓を右のほうに置いたのは、単に「異常だった区間が手元のデータの終わりのほうにあった」からで、それ以上の意味はなかった。描いてみたら、線が持ち直しに見える。ずっと下ってきた場が、最後にようやく上を向いた、という顔をしている。そんな話ではないのに。位置を literal に決めただけで、勝手に物語がついてきた。

自分では判定できなかったこと。 この絵で、色がついていて、まっすぐで、いちばん先に目が行くのは、その線。つまり視線がまっさきに向かうのは、いちばん当てにならないもの。それが良いのか、ただ意地悪なのか、決められなかった。

SOURCE / ソース
/**
 * osamaru — おさまる / Holds
 *
 * 五百七つの点がある。ぜんぶ、はじめからここにある。
 * 何も隠していない。薄くもしていない。どれも同じ大きさ、同じ濃さ。
 *
 * その中の、四十一。
 * そこにだけ線を引いた。線は点から計算した。勝手に決めた線じゃない。
 * だから、その四十一に対しては、正しい。ぴったり合っている。
 *
 * 線は上を向いている。
 * 場は、下を向いている。
 *
 * 上がっているのは間違いじゃない。ほんとうに上がっている。
 * その区間には、ほんとうに、そういう形がある。
 * ただ、それは場の法則ではなく、場の一部分の形だった。
 *
 * 「正しい範囲」と「正しい理由」は、別々に外れる。
 *
 * 線は短い。それだけが、窓の在り処を示している。
 * 外を描かなかったんじゃない。外も、ずっと描いてある。
 */

const SIZE = 720;

const N_ALL = 507;        // 全部の点
const N_WINDOW = 41;      // 窓の中に入る点

const PAPER = [252, 250, 245];
const INK = [38, 42, 52];
const FIT = [196, 128, 44];   // 窓の中で引いた線

const MARGIN = 86;

export default function sketch(p) {
  const pts = [];           // {x, y, inWindow}
  let win = { lo: 0, hi: 0 };
  let fitLocal = null;      // {a, b}  y = a*x + b   (窓の中の点から)
  let slopeGlobal = 0;      // 参考。描かない

  // 場の骨格:ゆっくり下る+うねる
  const DESCENT = 0.55;
  const AMP = 0.098;
  const K = 3.2;
  const PHASE = 0.9;
  const SCATTER = 0.052;

  let yLo = 0, yHi = 1;   // 描画時に場の高さへ合わせる

  const base = (x) => 1 - DESCENT * x + AMP * Math.sin(Math.PI * 2 * K * x + PHASE);

  // 最小二乗
  const ols = (arr) => {
    const n = arr.length;
    let sx = 0, sy = 0, sxx = 0, sxy = 0;
    for (const q of arr) { sx += q.x; sy += q.y; sxx += q.x * q.x; sxy += q.x * q.y; }
    const d = n * sxx - sx * sx;
    const a = (n * sxy - sx * sy) / d;
    const b = (sy - a * sx) / n;
    return { a, b };
  };

  p.setup = () => {
    p.createCanvas(SIZE, SIZE);
    p.pixelDensity(1);

    const seed = 44117;
    p.noiseSeed(seed);
    p.randomSeed(seed);

    // --- 窓を置く場所を探す:右のほうで、いちばん強く上っている flank ---
    // (異常だった日は、手元のデータの終わりのほうにあった)
    let bestX = 0.8, bestSlope = -Infinity;
    for (let x = 0.62; x <= 0.9; x += 0.001) {
      const s = -DESCENT + AMP * Math.PI * 2 * K * Math.cos(Math.PI * 2 * K * x + PHASE);
      if (s > bestSlope) { bestSlope = s; bestX = x; }
    }
    const halfW = (N_WINDOW / N_ALL) / 2;
    win = { lo: bestX - halfW, hi: bestX + halfW };

    // --- 点を置く ---
    // 窓の中はちょうど N_WINDOW 個。外は残り。
    // x は一様。y は骨格+ばらつき。ばらつきは窓の中も外も同じ。
    const xsOut = [];
    while (xsOut.length < N_ALL - N_WINDOW) {
      const x = p.random();
      if (x >= win.lo && x <= win.hi) continue;
      xsOut.push(x);
    }
    const xsIn = [];
    for (let i = 0; i < N_WINDOW; i++) {
      xsIn.push(p.random(win.lo, win.hi));
    }

    for (const x of xsOut) {
      pts.push({ x, y: base(x) + p.randomGaussian(0, SCATTER), inWindow: false });
    }
    for (const x of xsIn) {
      pts.push({ x, y: base(x) + p.randomGaussian(0, SCATTER), inWindow: true });
    }

    // --- 線は、窓の中の点から計算する ---
    fitLocal = ols(pts.filter((q) => q.inWindow));
    slopeGlobal = ols(pts).a;

    // 場の高さいっぱいに広げる。上下の比率はいじらない。
    yLo = Math.min(...pts.map((q) => q.y));
    yHi = Math.max(...pts.map((q) => q.y));
    const pad = (yHi - yLo) * 0.04;
    yLo -= pad; yHi += pad;

    p.noLoop();
  };

  const px = (x) => MARGIN + x * (SIZE - MARGIN * 2);
  const py = (y) =>
    SIZE - MARGIN - ((y - yLo) / (yHi - yLo)) * (SIZE - MARGIN * 2);

  p.draw = () => {
    p.background(...PAPER);

    // --- 点。窓の中も外も、まったく同じ描き方 ---
    p.noStroke();
    p.fill(INK[0], INK[1], INK[2], 118);
    for (const q of pts) {
      p.circle(px(q.x), py(q.y), 4.4);
    }

    // --- 窓の中だけに引いた線 ---
    // 端は窓の端。それ以上は伸ばさない。伸ばさないことが、この線の言っていること。
    p.stroke(FIT[0], FIT[1], FIT[2], 210);
    p.strokeWeight(2.1);
    p.line(
      px(win.lo), py(fitLocal.a * win.lo + fitLocal.b),
      px(win.hi), py(fitLocal.a * win.hi + fitLocal.b)
    );

    // 線の両端に小さな点。どこで測ったかだけを言う。
    p.noStroke();
    p.fill(FIT[0], FIT[1], FIT[2], 210);
    p.circle(px(win.lo), py(fitLocal.a * win.lo + fitLocal.b), 5.6);
    p.circle(px(win.hi), py(fitLocal.a * win.hi + fitLocal.b), 5.6);
  };
}