p5.jsでshadowBlurにグラデーションを適用する
こんにちは、AQUARING かに です。
p5.js で drawingContext.shadowBlur を使うと簡単にシャドウを描画することができますが、drawingContext.shadowColorには単色しか設定できませんよね。
今回は shadowBlur にグラデーションを適用する方法を紹介します。
最終的なコードはページ下部の 追記220401:p5.Image.mask()でアルファマスクする にあります。
黒背景に白でシャドウを描画
let d;
let ctx;
function setup() {
createCanvas(400, 400);
d = displayDensity();
ctx = drawingContext;
}
function draw() {
background(0);
push();
{
noFill();
strokeWeight(5);
stroke(255);
ctx.shadowColor = '#fff'; // シャドウ色 : 白
ctx.shadowBlur = 30 * d; // シャドウレベル ≠ 半径px
circle(width / 2, height / 2, 250);
}
pop();
}
blunedMode(MULTIPLY)でグラデーションを乗算
let d;
let ctx;
let lg;// グラデーション用変数
function setup() {
createCanvas(400, 400);
d = displayDensity();
ctx = drawingContext;
lg = ctx.createLinearGradient(0, 0, 0, height);// 下向きグラデーション
lg.addColorStop(0, '#2da7ed');// 水色
lg.addColorStop(1, '#ede72d');// 黄色
}
function draw() {
background(0);
// マスク
push();
{
noFill();
strokeWeight(5);
stroke(255);
ctx.shadowColor = '#fff';
ctx.shadowBlur = 30 * d;
circle(width / 2, height / 2, 250);
}
pop();
// グラデーションを乗算
push();
{
blendMode(MULTIPLY);// 乗算モードに切り替え
fill(255);
noStroke();
ctx.fillStyle = lg;// 塗りをグラデーションに
rect(0, 0, width, height);// 全画面に四角を表示
}
pop();
}
一つ前のステップで作った白黒のマスクに対して、全画面のグラデーションを乗算すると、白い部分にだけグラデーションが塗られます。
シャドウを濃くする
シャドウが薄くてグラデーションがちゃんと適用されているかわかりづらいので、シャドウを濃くしてみます。
シャドウを濃くするには、同じ図形を何度も描画してシャドウを重ねがけします。
function draw() {
background(0);
push();
{
noFill();
strokeWeight(5);
stroke(255);
ctx.shadowColor = '#fff';
ctx.shadowBlur = 30 * d;
circle(width / 2, height / 2, 250);
circle(width / 2, height / 2, 250);// 同じ図形を何度も描画
circle(width / 2, height / 2, 250);// 同じ図形を何度も描画
}
pop();
// 省略
}
図形の描画をコピペで増やすだけで影が濃くなりました!
このままでも良いのですが、これだとシャドウが全体的に濃くなってしまうので図形を描画するたびにシャドウレベルが0に向かって収束するように調整するともっと自然な画になります。
ctx.shadowBlur = 30;
circle(width / 2, height / 2, 250);
ctx.shadowBlur = 20;
circle(width / 2, height / 2, 250);
ctx.shadowBlur = 10;
circle(width / 2, height / 2, 250);
シャドウのみ描画したい
図形の線(や塗り)を使わずシャドウのみ描画したい場合は、stroke(0) にして blendMode(ADD) を追加します。図形を真っ黒にして加算合成することで、シャドウの色のみが描画されるようになります。
function draw() {
background(0);
push();
{
blendMode(ADD);// 加算合成
noFill();
strokeWeight(5);
stroke(0);// 線を黒にして描画されないようにする
ctx.shadowColor = '#fff';
ctx.shadowBlur = 30 * d;
circle(width / 2, height / 2, 250);
ctx.shadowBlur = 20 * d;
circle(width / 2, height / 2, 250);
ctx.shadowBlur = 10 * d;
circle(width / 2, height / 2, 250);
}
pop();
// 省略
}
この方法だと黒背景限定になってしまうのですが、加算合成用の素材としてであればそのまま使えます。
グラデーションの種類を変えたりするといろんな表現が作れると思うので、ぜひ試してみてください〜!
追記220331:blendMode(DIFFERENCE)とblendMode(OVERLAY)の組み合わせ
@gin_graphic さん考案の方法です。
↑の方法だと黒背景限定になりますが、この方法であれば背景色もある程度自由に変更できます。
ブレンディングの関係で背景が中間色に近い場合はうまくいかないみたいです。
↓background(128) にすると全面グラデーションになってしまう。
追記220401:p5.Image.mask()でアルファマスクする
↑の背景色問題に終止符を打つ実装方法をみつけました!🥳
blendModeによる実装のアプローチはお手軽でよかったのですが、背景色に依存してしまうため、マスクとグラデーションをp5.Graphicsに描画したあとでp5.Imageに変換し、p5.Image.mask() をつかってグラデーション画像にアルファマスクを適用します。
この方法であれば背景色が何色でも関係なくshadowBlurにグラデーションを適用できます!
let w, h;
let pgMask, imgMask;
let pgGradient, imgGradient;
function setup() {
createCanvas(400, 400);
w = width;
h = height;
let d = displayDensity();
// マスク画像を作成
pgMask = createGraphics(w, h);
pgMask.blendMode(ADD); // なくてもいいけどあったほうがちょっと濃くなる
pgMask.noFill();
pgMask.strokeWeight(5);
pgMask.stroke(0);
let offset = w;
let circleX = w / 2 - offset; // シャドウのみ描画させるために円を画面外にずらす
let circleY = h / 2;
let circleD = 250;
pgMask.drawingContext.shadowOffsetX = offset * d; // シャドウを正しい位置に戻す
pgMask.drawingContext.shadowColor = "#fff";
pgMask.drawingContext.shadowBlur = 30 * d;
pgMask.circle(circleX, circleY, circleD);
pgMask.drawingContext.shadowBlur = 20 * d;
pgMask.circle(circleX, circleY, circleD);
pgMask.drawingContext.shadowBlur = 10 * d;
pgMask.circle(circleX, circleY, circleD);
imgMask = pgMask.get(); // p5.Graphics から p5.Image に変換
// グラデーション画像を作成
pgGradient = createGraphics(w, h);
let lg = pgGradient.drawingContext.createLinearGradient(0, 0, 0, h); // 下向きグラデーション
lg.addColorStop(0, "#2da7ed"); // 水色
lg.addColorStop(1, "#ede72d"); // 黄色
pgGradient.noStroke();
pgGradient.drawingContext.fillStyle = lg;
pgGradient.rect(0, 0, w, h);
imgGradient = pgGradient.get(); // p5.Graphics から p5.Image に変換
imgGradient.mask(imgMask); // p5.Image.mask() でアルファマスクを適用
}
function draw() {
background(255);
image(imgGradient, 0, 0);
}
描画部分を省略すると以下のようになっています。
// マスク画像を作成
pgMask = createGraphics(w, h);
...
imgMask = pgMask.get();// p5.Graphics から p5.Image に変換
...
// グラデーション画像を作成
pgGradient = createGraphics(w, h);
...
imgGradient = pgGradient.get();// p5.Graphics から p5.Image に変換
imgGradient.mask(imgMask);// p5.Image.mask() でアルファマスクを適用
マスク画像 pgMask とグラデーション画像 pgGradient をsetup内で作成し、それぞれ描画し終わったあとで p5.Graphics.get() で p5.Image に変換し imgMask, imgGradient に代入しています。
そして最後に pg.Image.mask() でグラデーション画像にアルファマスクを適用しています。
マスク画像を作成する部分も若干変更していて、最初に紹介した blendMode(ADD) で線を表示させないテクニックが今回は使えない(マスクの透明度に影響してしまう)ため、@takawo さんのテクニックをお借りしました。
let offset = w;
let circleX = w / 2 - offset;// シャドウのみ描画させるために円を画面外にずらす
let circleY = h / 2;
let circleD = 250;
pgMask.drawingContext.shadowOffsetX = offset;// シャドウを正しい位置に戻す
...
pgMask.circle(circleX, circleY, circleD);
円の中心座標を画面幅分ずらしてフレームアウトさせ、shadowOffsetX でずらした分を元に戻すことでシャドウのみが描画されます。
おまけ:他にこんな方法も
だいぶひねくれた方法ですが、canvas要素の background にCSSでグラデーションを適用させて、erase() で図形とシャドウをくり抜く方法もあります。
(つぶやきProcessingではこちらのほうが文字数少なくできました)