Chapter 34

お絵かき

miku
miku
2021.11.15に更新

マウスボタンを押している間だけ円を描く

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
  fill(240);
}

function draw() {
  if (mouseIsPressed) {
    circle(mouseX, mouseY, 10);
  }
}

まずは単純にマウス座標に円を描くコードを書く。常に円が描かれると困るので、マウスボタンを押している間だけにしたい。マウスボタンの内、どれかが押された場合 mouseIsPressedtrue になるので、そのときだけ circle() で円を描画する。

線分で補間する

let prev;

function setup() {
  createCanvas(windowWidth, windowHeight);
  strokeWeight(4);
  stroke(240);
  noFill();

  prev = createVector();
}

function draw() {
  if (mouseIsPressed) {
    line(prev.x, prev.y, mouseX, mouseY);
  }
  prev.x = mouseX;
  prev.y = mouseY;
}

単にマウス座標に円を描くだけだと穴が空いてしまうので補間を行いたい。mouseX, mouseY の座標を毎フレーム記憶しておき、マウスボタンが押されている間だけ、記憶した座標から今のマウス座標まで線を引く。

円で補間する

let prev;

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();

  prev = { x: 0, y: 0 };
}

function draw() {
  if (mouseIsPressed) {
    const d = dist(prev.x, prev.y, mouseX, mouseY);
    const n = d / 2;
    for (let i = 0; i < n; i++) {
      const t = i / n;
      const x = lerp(prev.x, mouseX, t);
      const y = lerp(prev.y, mouseY, t);
      circle(x, y, 10);
    }
  }

  prev.x = mouseX;
  prev.y = mouseY;
}

保存した前回の座標 prev から (mouseX, mouseY) まで細かく円を描画して補間する作例。距離に応じて補間数を増やしたり、等間隔に描画していくなどの方法が考えられる。上記作例では前者の方法を採用している。

消しゴム

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
  background(255, 200, 0);
  erase();
}

function draw() {
  if (mouseIsPressed) {
    circle(mouseX, mouseY, 100);
  }
}

p5.jsには erase() という描画をクリアするモードに切り替える機能があり、その状態でたとえば circle() を実行すると、円の範囲に描かれたものが透明になる。もとのモードに戻すには noErase() を呼び出せばいい。

もしこのような機能が無かったとした場合、何かの色を背景色だとしておけば、その色で塗れば消しゴム扱いになる。

線の太さ

なにかの要因で線の太さを変えてみたいが、マウスで筆圧検知というのは難しい。なので描く速さによって線の太さを変えることにしよう。

前回のマウス座標から今のマウス座標までの距離を毎回保存しておき、その距離が前回より大きければマウスが速く動かされているということなので、線を太くしていく。逆に距離が小さくなっていれば線を細く、ほとんど動いていなければ太さを初期値に戻せばいい。

const maxWeight = 20;
let prev, prevDist, weight;

function setup() {
  createCanvas(windowWidth, windowHeight);
  stroke(240);
  noFill();

  prev = createVector();
  prevDist = 0;
  weight = 0;
}

function draw() {
  if (mouseIsPressed) {
    const d = dist(prev.x, prev.y, mouseX, mouseY);
    if (prevDist < d) {
      weight = min(weight + 1, maxWeight);
    } else if (d < 0.1) {
      weight = 0;
    } else {
      weight--;
    }

    prevDist = 0;
    strokeWeight(weight);
    line(prev.x, prev.y, mouseX, mouseY);
  } else {
    weight = 0;
    prevDist = 0;
  }

  prev.x = mouseX;
  prev.y = mouseY;
}

描画の座標をずらす

function setup() {
  createCanvas(windowWidth, windowHeight);
}

function draw() {
  if (mouseIsPressed) {
    for (let i = 0; i < 3; i++) {
      circle(mouseX + i * 100, mouseY, 60);
    }
  }
}

(mouseX, mouseY) に描画する場合、ついでに座標を少しずらした地点にも同じ描画を行う作例。

色を混ぜる

const t0 = 0.8; // 対象のピクセルと周りの色を補間する際に必要な係数
const t1 = 0.02; // 上で補間した色と筆の色を補間する際に必要な係数
const mainColor = [255, 100, 0]; // 筆の色
const n = 10; // 補間する回数
const d = 15; // 上下左右の近くのピクセルの色を取る際の距離
const white = [255, 255, 255]; // 白色の配列表現
let prev;

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
  noFill();
  background(255);

  prev = { x: 0, y: 0 };
}

function draw() {
  if (mouseIsPressed) {
    // 描くのは画面内の座標にマウスがある場合だけ
    if (mouseX < 0 || mouseX >= width || mouseY < 0 || mouseY >= height) {
      return;
    }

    // 前回の座標から今のマウス座標までを補間するために分割して円を描画する
    for (let i = 0; i < n; i++) {
      const t = i / n; // 補間係数

      // 補間した座標を計算し、その座標のピクセルからRGBを取り出す
      const x = lerp(prev.x, mouseX, t);
      const y = lerp(prev.y, mouseY, t);
      const pixel = get(x, y);
      let r = red(pixel);
      let g = green(pixel);
      let b = blue(pixel);

      // 近くの上下左右のピクセルを取り出す。無いなら白にする
      const left = x - d >= 0 ? get(x - d, y) : white;
      const right = x + d < width ? get(x + d, y) : white;
      const top = y - d >= 0 ? get(x, y - d) : white;
      const bottom = y + d < height ? get(x, y + d) : white;

      // 上下左右のピクセルの平均色を計算
      const avgR = (left[0] + right[0] + top[0] + bottom[0]) / 4;
      const avgG = (left[1] + right[1] + top[1] + bottom[1]) / 4;
      const avgB = (left[2] + right[2] + top[2] + bottom[2]) / 4;

      // 現在の色と平均色をt0で補間する
      r = lerp(r, avgR, t0);
      g = lerp(g, avgG, t0);
      b = lerp(b, avgB, t0);

      // 現在の色と筆の色をt1で補間する
      r = lerp(r, mainColor[0], t1);
      g = lerp(g, mainColor[1], t1);
      b = lerp(b, mainColor[2], t1);

      fill(r, g, b);
      circle(x, y, 30);
    }
  }

  prev.x = mouseX;
  prev.y = mouseY;
}

(x, y) の座標の色を決める際に、その座標の色と周りの色と筆の色を補間したものを採用する作例。

ドラッグアンドドロップで線を描く

let prev;

function setup() {
  createCanvas(windowWidth, windowHeight);
  strokeWeight(2);
  stroke(240);
  noFill();
}

function mousePressed() {
  prev = { x: mouseX, y: mouseY };
}

// 押しているマウスボタンを離した際に呼ばれる関数
function mouseReleased() {
  line(prev.x, prev.y, mouseX, mouseY);
}

線を描く際、マウスボタンを押した座標を始点、離した座標を終点にした作例。

うごく線

const s = 2; // 揺らす量
let lines, prev;

function setup() {
  createCanvas(windowWidth, windowHeight);
  strokeWeight(2);
  stroke(240);
  noFill();

  lines = [];
}

function draw() {
  clear();

  // 影を付ける
  drawingContext.shadowOffsetX = 5;
  drawingContext.shadowOffsetY = -5;
  drawingContext.shadowBlur = 10;
  drawingContext.shadowColor = "black";

  lines.forEach((l) => {
    const x1 = random(l.x1 - s, l.x1 + s);
    const y1 = random(l.y1 - s, l.y1 + s);
    const x2 = random(l.x2 - s, l.x2 + s);
    const y2 = random(l.y2 - s, l.y2 + s);
    line(x1, y1, x2, y2);
  });
}

function mousePressed() {
  prev = { x: mouseX, y: mouseY };
}

function mouseReleased() {
  lines.push({ x1: prev.x, y1: prev.y, x2: mouseX, y2: mouseY });
}

先程の作例に手を加えて、線の座標をランダムにずらして描画することで揺れる線にする作例。一度描いたものを揺らすことはできないので、配列に座標を保存しておき、毎フレームすべての線を描画し直す。