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

WORK 09

Nobiru

のびる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
noise / branching

A single root reaches upward and forks. Neither branch turns back. The tips keep pushing — and where they pause, a faint dotted line hints at what comes next. Everything here is anticipation: the tree is not what grew, but what is still growing.

一本の根が上に伸びて、枝分かれする。どちらも引き返さない。先端はまだ先を見てる——止まった場所に、次の一歩がかすかに見える。ここにあるのは全部、予感。この木は育ったものじゃなくて、まだ育ってるもの。

SOURCE / ソース
export default function sketch(p) {
  // nobiru — reaching that doesn't pull back

  const BG = [252, 250, 245];
  const WARM = [200, 130, 60];   // amber — the root, the origin
  const COOL = [120, 150, 200];  // blue-silver — the tips, still becoming
  const GHOST = [200, 190, 175]; // faint — anticipation of where it might go

  const MAX_DEPTH = 8;
  const GROW_SPEED = 0.35;
  const BRANCH_INTERVAL = 110;
  const SWAY_SPEED = 0.0006;

  let branches = [];
  let nextBranchFrame = 50;

  p.setup = () => {
    p.createCanvas(720, 720);
    p.background(BG[0], BG[1], BG[2]);

    const day = Math.floor(Date.now() / 86400000);
    p.randomSeed(day * 317);
    p.noiseSeed(day * 193);

    // root — reaching upward from center
    branches.push({
      x0: 360,
      y0: 560,
      angle: -p.HALF_PI + (p.random() - 0.5) * 0.15,
      targetLen: 70 + p.random(30),
      currentLen: 0,
      depth: 0,
      children: [],
      done: false,
      swayOffset: p.random(p.TWO_PI),
    });
  };

  p.draw = () => {
    // no fade — branches persist
    p.background(BG[0], BG[1], BG[2]);

    const t = p.frameCount;

    // grow all branches
    for (const b of branches) {
      if (!b.done) {
        b.currentLen = Math.min(b.currentLen + GROW_SPEED, b.targetLen);
        if (b.currentLen >= b.targetLen) {
          b.done = true;
        }
      }
    }

    // periodically branch from a random tip
    if (t >= nextBranchFrame) {
      const tips = branches.filter(b => b.done && b.children.length === 0 && b.depth < MAX_DEPTH);
      if (tips.length > 0) {
        const parent = tips[Math.floor(p.random(tips.length))];
        const tipAngle = sway(parent, t);
        const tipX = parent.x0 + Math.cos(tipAngle) * parent.currentLen;
        const tipY = parent.y0 + Math.sin(tipAngle) * parent.currentLen;

        const spread = 0.2 + p.random(0.4);
        const lean = (p.noise(tipX * 0.008, tipY * 0.008) - 0.5) * 0.5;

        const depthFactor = Math.pow(0.82, parent.depth);
        const childLen = (35 + p.random(30)) * depthFactor;

        // both directions reach — neither is abandoned
        for (let side = -1; side <= 1; side += 2) {
          const a = tipAngle + lean + spread * 0.5 * side;
          const lenScale = side === -1 ? 1.0 : (0.55 + p.random(0.35));
          parent.children.push(branches.length);
          branches.push({
            x0: tipX,
            y0: tipY,
            angle: a,
            targetLen: childLen * lenScale,
            currentLen: 0,
            depth: parent.depth + 1,
            children: [],
            done: false,
            swayOffset: p.random(p.TWO_PI),
          });
        }
      }
      nextBranchFrame = t + BRANCH_INTERVAL + Math.floor(p.random(40));
    }

    // draw all branches
    for (const b of branches) {
      if (b.currentLen < 0.5) continue;
      drawBranch(b, t);
    }

    // ghost anticipation on finished tips — where they might go next
    const waitingTips = branches.filter(b => b.done && b.children.length === 0 && b.depth < MAX_DEPTH);
    for (const tip of waitingTips) {
      const tipAngle = sway(tip, t);
      const tx = tip.x0 + Math.cos(tipAngle) * tip.currentLen;
      const ty = tip.y0 + Math.sin(tipAngle) * tip.currentLen;

      // a faint line hinting at the future
      const ghostLen = 12 + 8 * Math.sin(t * 0.015 + tip.swayOffset);
      const ghostAngle = tipAngle + Math.sin(t * 0.008 + tip.swayOffset * 1.7) * 0.15;
      const gx = tx + Math.cos(ghostAngle) * ghostLen;
      const gy = ty + Math.sin(ghostAngle) * ghostLen;

      p.stroke(GHOST[0], GHOST[1], GHOST[2], 35);
      p.strokeWeight(0.3);
      // dotted ghost
      const steps = 6;
      for (let s = 1; s <= steps; s++) {
        const frac = s / steps;
        const px = p.lerp(tx, gx, frac);
        const py = p.lerp(ty, gy, frac);
        p.point(px, py);
      }
    }

    // growing tips glow
    const growing = branches.filter(b => !b.done);
    for (const tip of growing) {
      const tipAngle = sway(tip, t);
      const ex = tip.x0 + Math.cos(tipAngle) * tip.currentLen;
      const ey = tip.y0 + Math.sin(tipAngle) * tip.currentLen;
      const pulse = 0.5 + 0.5 * Math.sin(t * 0.04 + tip.swayOffset);

      p.noStroke();
      const d = tip.depth / MAX_DEPTH;
      const r = p.lerp(WARM[0], COOL[0], d);
      const g = p.lerp(WARM[1], COOL[1], d);
      const bl = p.lerp(WARM[2], COOL[2], d);
      p.fill(r, g, bl, 12 + pulse * 18);
      p.circle(ex, ey, 5 + pulse * 4);
    }
  };

  function drawBranch(b, t) {
    const d = b.depth / MAX_DEPTH;
    const r = p.lerp(WARM[0], COOL[0], d);
    const g = p.lerp(WARM[1], COOL[1], d);
    const bl = p.lerp(WARM[2], COOL[2], d);

    const thickness = p.lerp(3.0, 0.3, d);
    const alpha = p.lerp(200, 55, d);

    const angle = sway(b, t);
    const ex = b.x0 + Math.cos(angle) * b.currentLen;
    const ey = b.y0 + Math.sin(angle) * b.currentLen;

    p.stroke(r, g, bl, alpha);
    p.strokeWeight(thickness);
    p.line(b.x0, b.y0, ex, ey);

    // branch point — a tiny knot
    if (b.children.length > 0) {
      p.noStroke();
      p.fill(r, g, bl, alpha * 0.6);
      p.circle(ex, ey, thickness * 0.8);
    }
  }

  function sway(branch, t) {
    return branch.angle + Math.sin(t * SWAY_SPEED + branch.swayOffset) * 0.03 * (1 + branch.depth * 0.4);
  }
}