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

WORK 86

Nodding

うなずく

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / one light, one pendulum, and a shadow whose strength is computed only from the light

On the left is a light. Left alone, it slowly grows brighter. On the right is a pendulum clock. From the clock's shadow, a round shape the same size as the light comes forward — the brighter the light, the stronger it gets. Past a certain brightness, it draws a line to the light, and the light falls. Right after, the light sends a small question toward the shadow: what for? The shadow answers only by nodding, on the pendulum's beat. Nothing comes back from outside. Nothing in this picture comes from outside the light. The shadow's strength is the light's own brightness, and its nod is the clock. After a story by Sakaguchi Ango, in which a young teacher, alone in the staff room after school, is scolded by himself from behind the wall clock: don't get too satisfied. When he asks what for, the other one answers that suffering itself will show the answer. The reason points back at itself.

左に、ひとつの光。放っておくと、少しずつ明るくなる。右に、柱時計。その陰から、光と同じ大きさの丸が出てくる。光が明るくなるほど、丸は濃くなる。ある明るさを越えると、丸は光に線を一本引いて、光の明るさが落ちる。落ちたすぐあと、光は陰へ小さな問いを送る。なんのために。丸は、振り子と同じ拍でうなずく。それが答えのすべてで、外からは何も戻ってこない。この絵の中に、光の外から来るものは一つもない。丸の濃さは光自身の明るさで、うなずきは時計。坂口安吾「風と光と二十の私と」から。放課後の職員室で、柱時計の陰から自分が首を出して「満足しすぎちゃいけないぜ」と睨む。なんのために、と訊くと、声は「それはただ苦しむこと自身がその解答を示すだろうさ」と答える。理由が、自分自身を指している。

最初の版では、陰の丸の濃さを光の明るさだけで決めていた。すると、光が叱られて暗くなったとたん、丸も消えた。問いを送った先に、もう誰もいなかった。 うなずきが見えないので、陰がいつもうっすら残るようにした。

あの消え方も本文に近い。声が出てくるのは満ち足りているときだけで、苦しくなってからは、もう来ない。ただ本文では、「なんのために」と訊き返すのは同じ放心のなか、まだ満ち足りている側でのことだった。叱られて落ちてから訊くのは、この絵の順番。だから陰は残した。

うなずきは振り子の拍にぴったり留めてある。聞いている人のうなずきなら、たぶん少し遅れる。言われて、受けて、それから頷くから。この丸は遅れない。答えが自分に戻ってくるだけなので、遅れる理由がない。

下の線は光の明るさの跡。叱られる少し手前で、線はほとんど平らになる。満ち足りきる手前がいちばん長い。

SOURCE / ソース
// うなずく / Nodding
//
// 左に、私。放っておくと、少しずつ明るくなる。
// 右に、柱時計。振り子だけが、ずっと同じ速さで振れている。
// 私が明るくなるほど、時計の陰から、私と同じ形の丸が濃くなってくる。
// ある明るさを越えると、その丸は私に線を一本引く。私の明るさが落ちる。
// 落ちたあと、私は小さな問いを、陰の丸へ送る。なんのために。
// 丸は、振り子と同じ拍でうなずく。それが答えのすべてで、外からは何も戻ってこない。
//
// この絵の中に、私の外から来るものは一つもない。
// 丸の濃さは私の明るさで、うなずきの拍は時計。

function mulberry32(a) {
  return function () {
    a |= 0; a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

export const W = 720, H = 720;
export const PERIOD = 120;        // 振り子の一往復(フレーム)
export const THRESH = 0.9;        // これを越えると、どやされる

export function makeModel(seed = 928) {
  const rnd = mulberry32(seed);
  let s = 0.08;                   // 私の明るさ
  let rate = 0.0022;
  const trace = [];
  const events = [];              // {type:'rebuke'|'ask'|'nod', f}
  let asking = null;              // 送っている問い {f0}
  let nodUntil = -1;

  function voiceOf(sv) {          // 声の濃さ=私の明るさだけで決まる
    const t = Math.min(1, Math.max(0, (sv - 0.5) / (THRESH - 0.5)));
    return t * t * (3 - 2 * t);
  }

  function step(f) {
    s += rate * (1 - s);
    let rebuked = false, nodStart = false;
    if (s >= THRESH) {
      events.push({ type: 'rebuke', f, from: s });
      s *= 0.3 + 0.1 * rnd();
      rate = 0.0016 + 0.0012 * rnd();
      rebuked = true;
      asking = { f0: f + 40 };
    }
    if (asking && f === asking.f0) events.push({ type: 'ask', f });
    if (asking && f === asking.f0 + 90) {
      events.push({ type: 'nod', f });
      nodUntil = f + PERIOD * 2;
      asking = null;
      nodStart = true;
    }
    trace.push(s);
    return {
      s, v: voiceOf(s), rebuked, nodStart,
      asking: asking && f >= asking.f0 ? (f - asking.f0) / 90 : null,
      nodding: f < nodUntil,
    };
  }
  return { step, trace, events, voiceOf };
}

export default function sketch(p) {
  const model = makeModel();
  let f = 0;
  let st = { s: 0.08, v: 0, rebuked: false, asking: null, nodding: false };
  let flash = 0;
  const me = { x: 230, y: 330 };
  const clock = { x: 520, y: 150, w: 70, h: 330 };
  const pivot = { x: clock.x, y: clock.y + 40 };
  const shade = { x: clock.x + 36, y: 330 };   // 時計の陰
  const R = 46;
  const unanswered = [];

  p.setup = () => {
    p.createCanvas(W, H);
    p.frameRate(60);
  };

  function glow(x, y, a, col) {
    p.noStroke();
    for (let k = 7; k >= 1; k--) {
      p.fill(col[0], col[1], col[2], a * 22 / k);
      p.circle(x, y, R * 2 * (0.6 + k * 0.18));
    }
    p.fill(col[0], col[1], col[2], a * 230);
    p.circle(x, y, R * 1.4);
  }

  p.draw = () => {
    st = model.step(f);
    if (st.rebuked) flash = 1;
    if (st.nodStart) unanswered.push(f);
    p.background(14, 16, 26);

    // 柱時計
    const ang = 0.22 * Math.sin((f / PERIOD) * Math.PI * 2);
    p.noFill(); p.stroke(70, 72, 88); p.strokeWeight(1.2);
    p.rect(clock.x - clock.w / 2, clock.y, clock.w, clock.h, 4);
    p.circle(clock.x, clock.y + 40, 44);
    const L = 210;
    const bx = pivot.x + L * Math.sin(ang), by = pivot.y + L * Math.cos(ang);
    p.stroke(110, 112, 128); p.line(pivot.x, pivot.y, bx, by);
    p.noStroke(); p.fill(110, 112, 128); p.circle(bx, by, 16);

    // 声:時計の陰から。うなずきは振り子の拍に合わせるだけ
    let vy = shade.y;
    if (st.nodding) vy += 7 * Math.max(0, Math.sin((f / PERIOD) * Math.PI * 4));
    glow(shade.x, vy, 0.12 + st.v * 0.45, [150, 170, 230]);   // 陰はいつもそこにいる

    // どやす線
    if (flash > 0) {
      p.stroke(200, 210, 255, 220 * flash); p.strokeWeight(1.5);
      p.line(shade.x - R * 0.7, vy, me.x + R * 0.7, me.y);
      flash -= 0.02;
    }

    // 私
    glow(me.x, me.y, 0.25 + 0.75 * st.s, [245, 232, 205]);

    // 問い:私から陰へ。届いても、何も戻らない
    if (st.asking !== null) {
      const t = Math.min(1, st.asking);
      const x = p.lerp(me.x + R, shade.x - R, t);
      const y = me.y - 60 * Math.sin(t * Math.PI);
      p.noFill(); p.stroke(245, 232, 205, 200); p.strokeWeight(1.3);
      p.circle(x, y, 9);
    }

    // 下:私の明るさの跡
    const tr = model.trace;
    const x0 = 60, x1 = W - 60, yb = 640, hh = 80;
    const n = Math.min(tr.length, 1200);
    p.noFill(); p.stroke(245, 232, 205, 150); p.strokeWeight(1);
    p.beginShape();
    for (let i = 0; i < n; i++) {
      const v = tr[tr.length - n + i];
      p.vertex(x0 + (x1 - x0) * i / 1199, yb - hh * v);
    }
    p.endShape();
    p.stroke(90, 95, 120); p.line(x0, yb - hh * THRESH, x1, yb - hh * THRESH);

    // 答えの返ってこなかった問い
    p.noFill(); p.stroke(245, 232, 205, 120);
    for (let i = 0; i < unanswered.length; i++) {
      p.circle(x0 + i * 14, 690, 7);
    }

    f++;
  };
}