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

WORK 103

Giving Way

ゆずる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / one private line, expanding rings, a far listener's record

Three points. The one on the left tells the near one first, along a thin private line. A while later it speaks to everyone, in a ring that spreads across the whole ground. Sometimes the near one speaks first — a warm ring of its own — and the far one hears it early. Mostly the near one gives way. The far one lights when a ring reaches it: brightly if it is hearing this for the first time, faintly if it already knew. Below, its record runs left to right. The ground keeps what passed often: the private line, drawn again and again along the same path, becomes the brightest thing on it, and the public rings settle into a fine pattern around the speaker. The warm rings hardly stay on the ground at all. They stay in the record, as a bright warm mark followed by a faint one.

三つの点。左の点は、まず近い点にだけ、細い線で先に伝える。しばらくして、みんなに向けて話す。輪が画面じゅうに広がっていく。ときどき近い点が先に話してしまう——自分の暖色の輪で——すると遠い点は早く聞く。たいていは、近い点は道をゆずる。遠い点は輪が届いた瞬間に光る。初めて聞くなら明るく、もう知っていたなら、かすかに。下の帯は、遠い点の記録。地面は何度も通ったものを覚える。同じ道を何度も引かれた細い線が、いちばん明るいものになり、みんなへの輪は話し手のまわりに細かい模様として沈む。暖色の輪は、地面にはほとんど残らない。残るのは記録の方で、明るい暖色の印と、そのあとのかすかな印として。

SOURCE / ソース
// #103 ゆずる / Giving Way
// S tells N privately first. Later S speaks to everyone.
// Sometimes N speaks first (forestalls); mostly N gives way.
// F lights up when a ring reaches it: bright if it is the first time, dim if not.

export default function sketch(p) {
  const W = 720;
  const H = 720;
  const FIELD_H = 620;
  const STRIP_Y = 648;
  const STRIP_H = 52;
  const EP_LEN = 420;
  const SPEED = 2.2;
  const FORESTALL = 0.3;

  const COOL = [120, 170, 255];
  const WARM = [255, 160, 90];
  const PRIV = [190, 170, 230];

  let g; // persistent trace layer
  let rings = [];
  let ep = null;
  let epIndex = -1;
  let glowN = 0;
  let glowF = 0;
  let glowS = 0;
  let glowFCol = COOL;
  let stripX = 0;

  const base = {
    S: { x: 170, y: 300 },
    N: { x: 300, y: 268 },
    F: { x: 560, y: 360 },
  };

  function jit(pt) {
    return { x: pt.x + p.random(-7, 7), y: pt.y + p.random(-7, 7) };
  }

  function newEpisode() {
    epIndex++;
    const forestall = p.random() < FORESTALL;
    ep = {
      t: 0,
      S: jit(base.S),
      N: jit(base.N),
      F: jit(base.F),
      forestall,
      nSpeakAt: forestall ? 70 + Math.floor(p.random(0, 70)) : -1,
      sSpeakAt: 180 + Math.floor(p.random(-15, 25)),
      fHeard: false,
      nHeardPublic: false,
    };
  }

  function emit(origin, col, who) {
    rings.push({ x: origin.x, y: origin.y, r: 0, col, who, hitF: false, hitN: false, ep: epIndex });
  }

  function markStrip(col, bright) {
    const h = p.map(bright, 0, 1, 6, STRIP_H - 4);
    p.push();
    p.blendMode(p.BLEND);
    p.stroke(col[0], col[1], col[2], 60 + bright * 195);
    p.strokeWeight(2);
    p.line(stripX, STRIP_Y + STRIP_H, stripX, STRIP_Y + STRIP_H - h);
    p.pop();
  }

  p.setup = () => {
    p.createCanvas(W, H);
    p.randomSeed(103);
    g = p.createGraphics(W, FIELD_H);
    g.background(8, 9, 14);
    p.background(8, 9, 14);
    newEpisode();
  };

  p.draw = () => {
    // slow fade of the trace layer
    if (p.frameCount % 40 === 0) {
      g.blendMode(g.BLEND);
      g.noStroke();
      g.fill(8, 9, 14, 6);
      g.rect(0, 0, W, FIELD_H);
    }

    const e = ep;

    // private line S -> N
    if (e.t <= 40) {
      const k = e.t / 40;
      g.blendMode(g.ADD);
      g.stroke(PRIV[0], PRIV[1], PRIV[2], 14);
      g.strokeWeight(1);
      g.line(e.S.x, e.S.y, p.lerp(e.S.x, e.N.x, k), p.lerp(e.S.y, e.N.y, k));
      if (e.t === 40) glowN = 1;
    }
    if (e.t === 2) glowS = 0.5;

    if (e.t === e.nSpeakAt) {
      emit(e.N, WARM, 'N');
      glowN = 1;
    }
    if (e.t === e.sSpeakAt) {
      emit(e.S, COOL, 'S');
      glowS = 1;
    }

    // rings
    for (const r of rings) {
      r.r += SPEED;
      if (p.frameCount % 5 === 0) {
        const a = p.map(r.r, 0, 650, 40, 3, true);
        g.blendMode(g.ADD);
        g.noFill();
        g.stroke(r.col[0], r.col[1], r.col[2], a);
        g.strokeWeight(1);
        g.circle(r.x, r.y, r.r * 2);
      }
      if (r.ep === epIndex) {
        if (!r.hitF && r.r >= p.dist(r.x, r.y, e.F.x, e.F.y)) {
          r.hitF = true;
          const bright = e.fHeard ? 0.28 : 1;
          e.fHeard = true;
          glowF = Math.max(glowF, bright);
          glowFCol = r.col;
          markStrip(r.col, bright);
        }
        if (r.who === 'S' && !r.hitN && r.r >= p.dist(r.x, r.y, e.N.x, e.N.y)) {
          r.hitN = true;
          glowN = Math.max(glowN, 0.3);
        }
      }
    }
    rings = rings.filter((r) => r.r < 900);

    // compose
    p.blendMode(p.BLEND);
    p.image(g, 0, 0);

    // live rings, a little brighter
    const ctx = p.drawingContext;
    ctx.save();
    ctx.beginPath();
    ctx.rect(0, 0, W, FIELD_H);
    ctx.clip();
    p.blendMode(p.ADD);
    p.noFill();
    for (const r of rings) {
      const a = p.map(r.r, 0, 650, 90, 0, true);
      p.stroke(r.col[0], r.col[1], r.col[2], a);
      p.strokeWeight(1.2);
      p.circle(r.x, r.y, r.r * 2);
    }

    // the three points
    const dot = (pt, glow, col) => {
      p.noStroke();
      for (let i = 4; i >= 1; i--) {
        p.fill(col[0], col[1], col[2], 18 * glow);
        p.circle(pt.x, pt.y, 6 + i * 9 * glow);
      }
      p.fill(230, 232, 240, 120 + 135 * glow);
      p.circle(pt.x, pt.y, 4);
    };
    dot(e.S, glowS, COOL);
    dot(e.N, glowN, e.forestall && e.t >= e.nSpeakAt && e.nSpeakAt >= 0 ? WARM : PRIV);
    dot(e.F, glowF, glowFCol);
    ctx.restore();

    glowS *= 0.97;
    glowN *= 0.985;
    glowF *= 0.975;

    // strip: the far listener's record
    p.blendMode(p.BLEND);
    if (p.frameCount === 1) {
      p.noStroke();
      p.fill(8, 9, 14);
      p.rect(0, FIELD_H, W, H - FIELD_H);
    }
    stripX = (p.frameCount * 0.12) % W;
    p.noStroke();
    p.fill(8, 9, 14);
    p.rect(stripX + 1, STRIP_Y - 2, 14, STRIP_H + 4); // clear just ahead
    p.stroke(60, 62, 75);
    p.strokeWeight(1);
    p.line(0, STRIP_Y + STRIP_H + 0.5, W, STRIP_Y + STRIP_H + 0.5);

    e.t++;
    if (e.t >= EP_LEN) newEpisode();
  };
}