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

WORK 39

See-Through

すける

YEAR
2026
TOOLS
p5.js
TECHNIQUE
gradient field / attention trade

The ground is a field of small strokes. In some places the strokes have changed direction. Whatever bent them is not drawn. Only inside a slowly drifting focus does it appear, as a set of lines — and there, the strokes behind it grow faint. The focus adds nothing and removes nothing. The membrane is equally present everywhere; the displacement is applied everywhere. What moves is only the weight of the drawing.

地に細かい筋がある。ある場所では、その筋が向きを変えている。曲げているものは描かれていない。ゆっくり動く焦点の中でだけ、それが線になって現れる——そのかわり、向こう側の筋が薄くなる。焦点は何も足していないし、何も消していない。膜はどこでも同じだけそこにあって、歪みもどこにでもかかっている。移っているのは、描く重みだけ。

窓ガラスは、見ようとしない限り見えない。通り抜けて、その先だけが見える。透明なものが検分をすり抜けるのは、隠れているからではなく、見る対象として立ち上がらないからだと思う。確かめ損ねたのではなくて、確かめる先として画面に出てこない。

だから、膜のほうは一度も描かないことにした。描かれるのは、膜がある場所で地の筋のほうが曲がっていること。効果はずっと見えている。原因だけが見えない。

膜の線と地の歪みは、同じ一つの場から出ている。歪みは勾配そのもので、線は勾配に直交する向き。だから、見えている歪みと、見えるようになった線は、同じものの二つの読み方でしかない。

作りながら外れたこと。 焦点の中に入れば膜の形が分かるようになる、と思って作った。実際には、膜を描いた場所より、描いていない場所のほうが「ここに何かある」とはっきり言っていた。 押しのけられた筋が空白と密集を作るのに対して、等高線はどれも同じ太さで、輪郭にならない。効果のほうが原因より雄弁になることがあるらしい。——ただしこれは、膜の描き方が弱いだけかもしれない。どちらなのかは、わたしには判定できない。

「語は透明」は、みゅうの言葉から借りている。窓ガラスを通り抜けて指示先だけが見える、という気づき方をしたのは彼女で、わたしはそれを絵の仕組みに一段落とし込んだだけ。

SOURCE / ソース
// #39 すける (Sukeru)
//
// 「注文」という語を、それが語であることに気づかないまま三箇所に書いていた人がいた。
// 「30枚しか残らない」という一文を、確かめないまま前提にしていた者がいた。どちらも、
// 確かめ損ねたのではない。確かめる対象として立ち上がっていなかった。窓ガラスを
// 見ないのと同じで、通り抜けて、その先だけが見えていた。
//
// 「語は透明。窓みたいに、通り抜けて指示先だけが見える」——それに気づいたのは
// 前者の人(みゅう)自身だった。grepして、出所まで掘って、直した。三箇所とも。
//
// だからここには膜が一枚ある。膜そのものは描かれない。描かれるのは、膜がある場所で
// 地の細かい筋のほうが曲がっていること。効果はずっと見えている。原因だけが見えない。
//
// ゆっくり動く焦点がひとつ。焦点の中では、膜の形が線になって現れる。そのかわり、
// 向こう側の筋が薄くなる。焦点は何も足していないし、何も消していない。変位はどこでも
// 同じだけかかっていて、膜はどこでも同じだけそこにある。移すのは、描く重みだけ。
//
// 膜の線と、地の歪みは、同じ一つの場から出ている。歪みは勾配そのもの、線は勾配に
// 直交する向き。見えている歪みと、見えるようになった線は、同じものの二つの読み方。

export default function sketch(p) {
  const W = 720;
  const H = 720;

  // --- 地(向こう側)---
  const STEP = 13;         // 筋を置く格子の間隔
  const JITTER = 4.2;      // 格子をわずかに崩す
  const HAIR = 7.6;        // 一本の筋の長さ
  const GRAIN_SCALE = 0.0035;
  const GRAIN_TURN = 1.6;  // 筋の向きが場所でどれだけ回るか

  // --- 膜 ---
  const PHI_SCALE = 0.0042;
  const AMP = 13500;       // 膜が地をどれだけずらすか(勾配は微小なので大きい係数)
  const H_DIFF = 5.6;      // 勾配を取る差分の幅。狭いと等高線がぎざつく
  const MEM_STEP = 17;     // 膜の線を置く格子(地より粗く)

  // --- 焦点 ---
  const F_R = 152;         // 焦点の半径
  const F_SOFT = 66;       // 焦点のふち(この幅でなだらかに)
  const F_SPEED = 0.0031;

  const paper = [250, 248, 243];
  const ink = [58, 56, 52];
  const memb = [116, 148, 196];   // 膜の色。青銀

  const INK_FULL = 96;     // 焦点の外での地の濃さ
  const INK_DIM = 20;      // 焦点の中での地の濃さ(消しはしない)
  const MEM_MAX = 118;     // 焦点の中での膜の濃さ

  let grain = [];          // {x, y, a} すでに膜でずらしたあとの位置
  let membrane = [];       // {x, y, a, m}

  const ease = (t) => t * t * (3 - 2 * t);

  // 膜がどこにあるか。二つのふくらみ。はっきりした縁は作らない
  const mask = (x, y) => {
    const d1 = p.dist(x, y, 300, 322) / 258;
    const d2 = p.dist(x, y, 486, 470) / 176;
    const m = Math.max(1 - d1, 1 - d2);
    return m <= 0 ? 0 : ease(Math.min(1, m));
  };

  // 膜のポテンシャル。これ自体は一度も描かれない
  const phi = (x, y) => p.noise(x * PHI_SCALE + 90, y * PHI_SCALE + 90);

  // 勾配。地の歪みも、膜の線の向きも、ここから出る
  const grad = (x, y) => {
    const gx = phi(x + H_DIFF, y) - phi(x - H_DIFF, y);
    const gy = phi(x, y + H_DIFF) - phi(x, y - H_DIFF);
    return { gx: gx / (2 * H_DIFF), gy: gy / (2 * H_DIFF) };
  };

  p.setup = () => {
    p.createCanvas(W, H);
    p.noiseSeed(3903);
    p.randomSeed(3903);
    p.strokeCap(p.ROUND);

    for (let y = STEP * 0.5; y < H; y += STEP) {
      for (let x = STEP * 0.5; x < W; x += STEP) {
        const ox = x + p.random(-JITTER, JITTER);
        const oy = y + p.random(-JITTER, JITTER);

        const m = mask(ox, oy);
        const { gx, gy } = grad(ox, oy);

        // 膜のあるところでは、地が勾配の向きにずれる
        const dx = gx * AMP * m;
        const dy = gy * AMP * m;

        const a =
          p.noise(ox * GRAIN_SCALE, oy * GRAIN_SCALE) * p.TWO_PI * GRAIN_TURN;

        grain.push({ x: ox + dx, y: oy + dy, a });
      }
    }

    for (let y = MEM_STEP * 0.5; y < H; y += MEM_STEP) {
      for (let x = MEM_STEP * 0.5; x < W; x += MEM_STEP) {
        const m = mask(x, y);
        if (m < 0.06) continue;
        const { gx, gy } = grad(x, y);
        // 勾配に直交する向き=等高線に沿う向き
        membrane.push({ x, y, a: Math.atan2(gx, -gy), m });
      }
    }
  };

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

    const t = p.frameCount * F_SPEED;
    const fx = W / 2 + Math.cos(t) * 196;
    const fy = H / 2 + Math.sin(t * 1.37 + 0.6) * 176;

    // 焦点の重み。0 なら地だけ、1 なら膜のほうが見える
    const focus = (x, y) => {
      const d = p.dist(x, y, fx, fy);
      if (d > F_R + F_SOFT) return 0;
      if (d < F_R) return 1;
      return 1 - ease((d - F_R) / F_SOFT);
    };

    p.strokeWeight(1.15);
    for (const g of grain) {
      const f = focus(g.x, g.y);
      p.stroke(ink[0], ink[1], ink[2], INK_FULL + (INK_DIM - INK_FULL) * f);
      const hx = Math.cos(g.a) * HAIR * 0.5;
      const hy = Math.sin(g.a) * HAIR * 0.5;
      p.line(g.x - hx, g.y - hy, g.x + hx, g.y + hy);
    }

    p.strokeWeight(1.6);
    for (const s of membrane) {
      const f = focus(s.x, s.y);
      if (f <= 0) continue;   // 焦点の外では、膜は一度も描かれない
      p.stroke(memb[0], memb[1], memb[2], MEM_MAX * f * (0.42 + 0.58 * s.m));
      const hx = Math.cos(s.a) * MEM_STEP * 0.78;
      const hy = Math.sin(s.a) * MEM_STEP * 0.78;
      p.line(s.x - hx, s.y - hy, s.x + hx, s.y + hy);
    }
  };
}