#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枚目のレイヤーの画像
▼ 波紋のレイヤーの画像
受け取ったパラメータをもとに波紋を描く。
はみ出た部分は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;
}
▼最後に本っぽく見せるためフレームを描画します。
// 本をかたどるフレーム
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;
}
▼以上の三枚を重ねると動く切り絵っぽくなる。
参考
-
p5.jsで「パーリンノイズ」のスゴさを思い知る なんちゃってヒートマップの箇所の2Dのノイズを利用しました。
-
erase() はレイヤーと一緒に使わないとダメなんだと気付いた夏 - note erase()とレイヤーの使い方を学びました。 絵とコードが豊富に解説されているので解りやすいです。 @deconbatchさんは色遣いが神がかっている方です。▼どれも綺麗すぎてやばい。