schedule2021-02-20

【p5js】パーリンノイズとeraseを使ったブックカバー#PCD2021

#PCD2021の7日目のお題「ブックカバー」のアニメーションを作りました。

p5jsで切り絵と波紋をイメージして描いています。 技術的な要素ではパーリンノイズと図形の差分erase()とlayerを利用しました。

PCD JAPAN 2021.02.20-21 Processing Community Day Japan 2021

Code

動くコードは@OpenProcessingで公開しています。
※ メモリを消費するらしく一定時間でリロードが発生します。

ここではレイヤーに分けてコメントします。

▼初期設定。 本の背景のグラデーションは1枚目のレイヤーで描画してます。 波紋の中心座標などのパラメータはここで決めてしまいます。

let width = 600;
let height = 600;

let count = 0;

// 背景のノイズのパラメータ
var noiseVal = 40;     // ノイズ
var zseed = [0, 7000]; // ノイズのシード値を固定
let layer01 = null;

// 波紋のパラメータ
let ripple_num = 4;
let ripple_params = [];

function setup() {
  createCanvas(width, height);

  // 背景のノイズのレイヤー
  let layer = createGraphics(width, height);
  layer.background("#2f4f4f");
  layer = umi(layer);
  layer01 = layer;

  // ランダムな波紋のパラメータ
  for (let i = 0; i < ripple_num; i++) {
    ripple_params[i] = {
      x: random(0, width),
      y: random(0, height),
      offset: random(0, width),
      a: random(40, 90),
      breadth: random(10, 20)
    }
  }
}

▼ レイヤーを重ねて描画しているところ。 波紋のレイヤーが可変になっている。

function draw() {
  count++;

  // 背景のノイズのレイヤー
  let layers = [layer01];

  // 波紋のレイヤー
  for (let i = 0; i < ripple_num; i++) {
    let layer = ripples(
      ripple_params[i].x,       // 中心x座標
      ripple_params[i].y,       // 中心y座標
      ripple_params[i].offset,  // 波紋の初期の半径
      ripple_params[i].a,       // 波紋の間隔
      ripple_params[i].breadth, // 波紋の幅
      count);                   // 時間
    layers.push(layer);
  }

  // 白い枠のレイヤー
  layers.push(frame());

  // レイヤーを重ねて描画する
  for (let layer of layers) {
    image(layer, 0, 0);
  }
}

▼ 背景のノイズのレイヤー。 2Dのノイズはパーリンノイズになっている。シードを固定してx, y座標を少しずつずらすと滲んだようなグラデーションになる。

// 背景のノイズのレイヤー
function umi(layer) {
  let m = width / 8;
  for (var y = m; y < height - m; y++) {
    for (var x = m * 2; x < width - m * 2; x++) {
      // noiseValで割ってノイズの分布を引き延ばしている
      let nx = x / noiseVal;
      let ny = y / noiseVal;
      let g = noise(nx, ny, zseed[0]) * 50;
      let b = noise(nx, ny, zseed[1]) * 70;
      layer.stroke(color(0, 200 + g, 200 + b));
      layer.point(x, y);
    }
  }
  return layer;
}

▼ 1枚目のレイヤーの画像
layer1

▼ 波紋のレイヤーの画像
受け取ったパラメータをもとに波紋を描く。 はみ出た部分はerase_out()で切り取っている。 一つのレイヤーで一つの波紋にしないとerase()が他のレイヤーに影響していしまう。

// 波紋の描画
function ripples(x, y, offset, a, breadth, count) {
  let layer = createGraphics(width, height);
  layer.noStroke();
  layer.fill("#fffafa");

  let num = 5;

  // 外側の円から順に描画
  for (let n = num; 0 < n; n--) {
    let r = (count + offset) % 1200 - 300 + n * a;
    if (r < 0) continue;
    layer.ellipse(x, y, r + breadth);

    layer.erase();
    layer.ellipse(x, y, r);
    layer.noErase();
  }
  layer = erase_out(layer);

  return layer;
}

// フレーム外にでた図形を切り取る
function erase_out(layer) {
  let m = width / 8;
  let m2 = m * 2;
  let b = 5;

  layer.erase()
  layer.rect(0, 0, width, m + b); // top
  layer.rect(0, height - m - b, width, height); // bottom
  layer.rect(0, 0, m2 + b, height); // left
  layer.rect(width - m2 - b, 0, width, height); // right
  layer.noErase();
  return layer;
}

layer2

▼最後に本っぽく見せるためフレームを描画します。

// 本をかたどるフレーム
function frame() {
  let m = width / 8;
  let m2 = m * 2;
  let b = 10;

  let layer = createGraphics(width, height);
  layer.fill("#fffafa");
  layer.rect(m2 - b, m - b, width - m2 * 2 + b * 2, height - m * 2 + b * 2, 10);

  layer.erase()
  layer.rect(m2 + b, m + b, width - m2 * 2 - b * 2, height - m * 2 - b * 2, 10);
  layer.noErase();

  return layer;
}

layer

▼以上の三枚を重ねると動く切り絵っぽくなる。 layers

bookcover

参考