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

WORK 112

Layered Into Shape

ぬりかさねる

YEAR
2026
TOOLS
p5.js
TECHNIQUE
p5.js / crayon strokes, no outline, each placed near one already there, colour changing by layer

No outline is drawn. Only short crayon strokes. The first one sits in the middle. After that, each new stroke is placed near a stroke that is already on the paper — any of them, with equal chance — so the places that are already full are chosen most often. Strokes lie along the grain of the paper. The colour changes with time, one layer after another: ochre, brown, rust, gold, black, and last a pale colour close to the paper. After 2400 frames the hand stops. Before looking, I predicted four things. The shape would not stay round but stretch along the grain: it did, a little, sideways. The edge would keep the first colour, the underpainting, and the middle would hold the last: wrong. Among the outermost strokes, the first colour is almost absent; the edge is where the newest strokes are. With no outline, would the eye still find a line at the edge: it didn't. There is no line at the outside. And the last colour would gather in the middle: it did, completely — and that is what I hadn't seen coming. The last layer covered every layer under it, and being close to the paper, it made the middle look empty. The only place a line appears is inside, where the pale meets the colours. The layers can be seen only at the edge.

輪郭は一本も引かない。クレヨンの短いストロークだけ。最初の一本はまんなか。そのあとは、もう紙の上にあるストロークのどれか一本を選んで——どれも同じ確率で——その近くに置く。だから、もう埋まっているところほど選ばれる。向きは紙の目に沿う。色は時間で替わる。黄土、茶、錆、金、黒、そして最後に、紙に近い白っぽい色。2400フレームで手を止める。見る前に四つ予想した。形は丸いままでなく、紙の目に沿って伸びる——少しだけ、横に伸びた。縁には最初の色、下塗りが残って、まんなかに最後の色が来る——外れ。いちばん外側のストロークに、最初の色はほとんどいない。縁は、いちばん新しいストロークのいる場所だった。輪郭がなくても、目は縁に線を見つけるか——見つけなかった。外側に線はない。そして最後の色はまんなかに集まる——当たった、完全に。それが、見ていなかったことだった。最後の層は下の層をぜんぶ覆って、紙に近い色だったから、まんなかが空いて見えた。線が見えるのは内側、白っぽい色と色たちが出会うところだけ。重ねた層が見えるのは、縁だけだった。

SOURCE / ソース
// #112 ぬりかさねる / Layered Into Shape
// No outline is ever drawn. Only short crayon strokes.
// The first stroke sits in the middle. After that, each new stroke is placed
// near a stroke that is already there — any of them, with equal chance —
// so the places that are already full are the places most often chosen.
// Strokes lie along the grain of the paper. The colour changes with time,
// one layer after another. At frame 2400 the hand stops.

const W = 720;
const H = 720;
const PER_FRAME = 4;
const STOP = 2400;
const LAYER = 400;
const SIGMA = 15;
const DRIFT = 5;

export const PALETTE = [
  [196, 150, 82], // ochre (first layer)
  [122, 74, 42], // brown
  [168, 64, 40], // rust
  [214, 172, 52], // gold
  [36, 30, 32], // black
  [238, 230, 212], // pale (last layer)
];

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 function grain(x, y) {
  // the direction feathers lie: a slow field, leaning down-right
  return 0.55 + 0.7 * Math.sin(x / 95) * Math.cos(y / 120) + 0.4 * Math.sin((x + y) / 210);
}

// Pure growth, shared by the sketch and by measurement.
export function makeGrower(seed = 1024) {
  const rnd = mulberry32(seed);
  const gauss = () => {
    let u = 0;
    let v = 0;
    while (u === 0) u = rnd();
    while (v === 0) v = rnd();
    return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
  };
  const strokes = [{ x: W / 2, y: H / 2 - 20, a: grain(W / 2, H / 2), layer: 0, born: 0 }];
  function step(frame) {
    const out = [];
    if (frame >= STOP) return out;
    const layer = Math.min(PALETTE.length - 1, Math.floor(frame / LAYER));
    for (let k = 0; k < PER_FRAME; k++) {
      const par = strokes[Math.floor(rnd() * strokes.length)];
      const ga = grain(par.x, par.y);
      let x = par.x + gauss() * SIGMA + Math.cos(ga) * DRIFT;
      let y = par.y + gauss() * SIGMA + Math.sin(ga) * DRIFT;
      x = Math.max(20, Math.min(W - 20, x));
      y = Math.max(20, Math.min(H - 20, y));
      const s = {
        x,
        y,
        a: grain(x, y) + gauss() * 0.25,
        len: 10 + rnd() * 14,
        layer,
        born: frame,
        jit: rnd() * 1000,
      };
      strokes.push(s);
      out.push(s);
    }
    return out;
  }
  return { strokes, step, rnd };
}

export default function sketch(p) {
  let g;
  let frame = 0;

  function crayon(s) {
    const c = PALETTE[s.layer];
    const dx = Math.cos(s.a);
    const dy = Math.sin(s.a);
    const nx = -dy;
    const ny = dx;
    p.noStroke();
    const n = Math.floor(s.len * 2.2);
    for (let i = 0; i < n; i++) {
      const t = i / n - 0.5;
      // waxy texture: the crayon skips on the paper's tooth
      const tooth = p.noise(s.x * 0.08 + i * 0.3, s.y * 0.08, s.jit);
      if (tooth < 0.38) continue;
      const w = (p.random() - 0.5) * 3.2;
      p.fill(c[0], c[1], c[2], 70 + 90 * tooth);
      p.circle(s.x + dx * t * s.len + nx * w, s.y + dy * t * s.len + ny * w, 1.6 + tooth * 1.4);
    }
  }

  p.setup = () => {
    p.createCanvas(W, H);
    p.pixelDensity(1);
    p.randomSeed(7);
    p.noiseSeed(7);
    p.background(246, 241, 230);
    // paper tooth
    p.noStroke();
    for (let i = 0; i < 9000; i++) {
      p.fill(225, 218, 202, 60);
      p.circle(p.random(W), p.random(H), p.random(0.6, 1.6));
    }
    g = makeGrower();
    crayon(g.strokes[0]);
  };

  p.draw = () => {
    if (frame < STOP) {
      for (const s of g.step(frame)) crayon(s);
    }
    frame++;
  };
}