僕がp5.jsでクリエイティブコーディングを始めたのは、2017年春のことでした。当時、p5.jsに関する情報(特に日本語のもの)は、ネットにもまだそれほど多くなく、結局、Processingの本(中でも役立ったのは「ジェネラティブ・アート-Processingによる実践ガイド」と「Nature of Code」)と、p5.jsの公式リファレンス、あとMDNのJavaScriptリファレンスあたりを首っ引きで、少しずつ使い方を覚え、その時々のサンプル的なスケッチとコードを、ブログに上げていました。
当時書いた記事のうちいくつかは、今でも時々、検索でひっかけた人に見てもらえているようです。中でも長い期間、多くの方から見られているエントリのひとつが、p5.jsでブラーやグローのような表現を行うための方法を試行錯誤した、以下の記事です。
●p5.jsで「ブラー」や「グロー」を使う方法 – infosmith.biz
このエントリを書いた当時、p5.jsのバージョンは、まだ「0.5.16」でした。あれから3年を経て、2020年3月には待望のバージョン「1.0.0」がリリース。現時点(2020年12月13日)では、最新版が「1.1.9」となっています。
p5.jsには、ブラーやグロー、シャドウなどを表現するにあたって、そのものズバリな感じで使えるAPIは用意されていません。そのため、以前であれば、前出のエントリで試したようなものも含めて、意図した感じで、それっぽく見えるような書き方を、コーダー側で工夫する必要がありました。
しかし、現在のp5.jsには、標準的な「シャドウ」「グロー」を、より簡単に使うためのオブジェクトがあります。それが「drawingContext」です。
●drawingContext (p5.js Reference)
●drawingContextでシャドウっぽいのとグローっぽいの(クリックで別ウィンドウが開きます)
function setup() { createCanvas(600, 300); background(255); noStroke(); fill(12); rect(300,0,300,300); drawingContext.shadowBlur = 7; //シャドウのサイズの大きさ drawingContext.shadowOffsetX = 5; //X軸正方向へのズレ drawingContext.shadowOffsetY = 5; //Y軸正方向へのズレ drawingContext.shadowColor = color(50); //シャドウの色 fill(200,200,255); circle (150,150,200); drawingContext.shadowBlur = 30; drawingContext.shadowOffsetX = 0; drawingContext.shadowOffsetY = 0; drawingContext.shadowColor = color(240,240,255); //黒い背景に明るいシャドウを付けるとグローっぽくなる circle (450,150,200); noLoop(); }
こんな感じで、より簡単にドロップシャドウやグローっぽい表現ができます。各プロパティは、プログラム的に値を変えてやることができますので、draw()でのアニメーションと組み合わせても面白い効果が出せますね。
●Glowing objects (クリックで別ウィンドウが開きます)
function setup() { createCanvas(600, 300); colorMode(HSB); noStroke(); } function draw() { background(0); for (let i=0;i<3;i++){ let sc = -120+i*240; let fc = i==0 ? 0: 100; setContext(fc,sc); fill(sc, fc, 25+abs((sin(frameCount/100)*75))); rect(100+i*150,100,100,100,10,10,10,10); } } function setContext(fc, sc){ let xv = abs(sin(frameCount/100))*50 drawingContext.shadowBlur = xv; drawingContext.shadowColor = color(sc ,fc, 100); }
drawingContextの「shadowBlur」を使ったサンプルを、いくつか作ってみました。
●Spiral Rose (クリックで別ウィンドウが開きます)
function setup() { createCanvas(600, 600); colorMode(HSB); noStroke(); drawingContext.shadowBlur = 10; drawingContext.shadowColor = color(50); drawingContext.shadowOffsetX = 5; drawingContext.shadowOffsetY = 5; } function draw() { background(220); translate(width/2,height/2); for (let r=460; r>49;r-=30){ fill(330-r/5,18,100); rotate(frameCount/300); ellipse(-r/10,-r/10,r,r); } }
●Tiddlywinks (クリックで別ウィンドウが開きます)
const EASE_TYPE = t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1; const DOTS_COUNT = 15; let dots = []; function setup() { createCanvas(600, 600); for (let i=0; i<DOTS_COUNT; i++){ dots[i] = new Dot(); } } function draw(){ blendMode(BLEND); drawingContext.shadowColor = color(0); background(0,35); blendMode(SCREEN); dots.forEach(d => {d.drawMe();d.updateMe();}); if (random()<.02){ dots.forEach(d => d.stateChange()); } } function easeValue (pre,next,preTime,nextTime){ return (next - pre) * EASE_TYPE((millis()-preTime)/nextTime); } function mouseClicked(){ dots.forEach(d => d.stateChange()); } class Dot { constructor (){ this.x = random(0,width); this.y = random(0,height); this.r = random(2,200); this.red = random(0,256); this.green = random(0,256); this.blue = random(0,256); this.alpha = random(20,256); this.isStopping = true; this.preX = this.x; this.preY = this.y; this.preR = this.r; this.nextX = this.x; this.nextY = this.y; this.nextR = this.r; this.preRed = this.red; this.preGreen = this.green; this.preBlue = this.blue; this.preAlpha = this.alpha; this.nextRed = this.red; this.nextGreen = this.green; this.nextBlue = this.blue; this.nextAlpha = this.alpha; this.ease_start_time = millis(); this.ip_time = 0; } drawMe (){ noStroke(); fill(this.red,this.green,this.blue,255); drawingContext.shadowColor = color(this.red,this.green,this.blue); drawingContext.shadowBlur = this.r/4; circle(this.x, this.y, this.r); } updateMe (){ if (!this.isStopping){ if (millis() < this.ease_start_time+this.ip_time) { this.x = this.preX + easeValue (this.preX,this.nextX,this.ease_start_time,this.ip_time); this.y = this.preY+ easeValue (this.preY,this.nextY,this.ease_start_time,this.ip_time); this.r =this.preR + easeValue (this.preR,this.nextR,this.ease_start_time,this.ip_time); this.red = this.preRed + easeValue (this.preRed,this.nextRed,this.ease_start_time,this.ip_time); this.green = this.preGreen + easeValue (this.preGreen,this.nextGreen,this.ease_start_time,this.ip_time); this.blue = this.preBlue + easeValue (this.preBlue,this.nextBlue,this.ease_start_time,this.ip_time); this.alpha = this.preAlpha + easeValue (this.preAlpha,this.nextAlpha,this.ease_start_time,this.ip_time); } else { this.x = this.nextX; this.y = this.nextY; this.r =this.nextR; this.red = this.nextRed; this.green = this.nextGreen; this.blue = this.nextBlue; this.alpha = this.nextAlpha; this.isStopping = true; } } } stateChange(){ if (this.isStopping){ this.preX = this.x; this.preY = this.y; this.preR = this.r; this.nextX = random(0,width); this.nextY = random(0,height); this.nextR = random(2,200); this.preRed = this.red; this.preGreen = this.green; this.preBlue = this.blue; this.preAlpha = this.alpha; this.nextRed = random(0,256); this.nextGreen = random(0,256); this.nextBlue = random(0,256); this.nextAlpha = random(0,256); this.ease_start_time = millis(); this.ip_time = random(500,5000); this.isStopping = false; } } }
●Starry Sky (クリックで別ウィンドウが開きます)
const EASE_TYPE = (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; const DOTS_COUNT = 500; const dots = []; function setup() { createCanvas(600, 600); colorMode(HSB); noStroke(); for (let i = 0; i < DOTS_COUNT; i++) { dots[i] = new Dot(); } } function draw() { blendMode(BLEND); background(240, 100, 10, 0.25); blendMode(SCREEN); dots.forEach((d, i) => { d.drawMe(); d.updateMe(); d.stateChange(random()); }); } function easeValue(pre, next, preTime, nextTime) { return (next - pre) * EASE_TYPE((millis() - preTime) / nextTime); } function mouseClicked() { dots.forEach((d) => d.stateChange(0)); } class Dot { constructor() { this.x = random(0, width); this.y = random(0, height); this.r = random(2, 6); this.hue = random(0, 360); this.sat = 10; this.bri = 50; this.blrcycl = random(20, 100); this.blrmaxsize = this.r * 4; this.blrminsize = this.r * 0.2; this.stopping = true; this.preX = this.x; this.preY = this.y; this.nextX = this.x; this.nextY = this.y; this.ease_start_time = millis(); this.ip_time = 0; } drawMe() { push(); noStroke(); fill(this.hue, this.sat, this.bri, 1); drawingContext.shadowColor = color(this.hue, 100, 100); drawingContext.shadowBlur = map(Math.sin(frameCount / this.blrcycl), -1, 1, this.blrminsize, this.blrmaxsize); circle(this.x, this.y, this.r); pop(); } updateMe(i) { if (this.stopping == false) { if (millis() < this.ease_start_time + this.ip_time) { this.x = this.preX + easeValue( this.preX, this.nextX, this.ease_start_time, this.ip_time ); this.y = this.preY + easeValue( this.preY, this.nextY, this.ease_start_time, this.ip_time ); for (let j = 0; j < dots.length - 1; j++) { if (i !== j) { if ( dist(this.x, this.y, dots[j].x, dots[j].y) < this.r / 2 + dots[j].r / 2 ) { dots[j].stateChange(0); } } } } else { this.x = this.nextX; this.y = this.nextY; this.stopping = true; } } } stateChange(val) { if (val < 0.000001 && this.stopping == true) { this.preX = this.x; this.preY = this.y; this.nextX = random(0, width); this.nextY = random(0, height); this.ease_start_time = millis(); this.ip_time = random(2000, 5000); this.stopping = false; } } }
p5.jsの2Dモードは、HTML Canvas標準の描画インターフェース(CanvasRenderingContext2D)に、Processing的な書き方でアクセスするためのラッパーだと認識しています。ただ、p5.jsでcanvasが持つ描画機能のすべてをフォローしているわけではないのですね。
「drawingContext」は、CanvasRenderingContext2Dに、p5.js内から直接アクセスするために用意されたオブジェクトです。ですので、やろうと思えば、drawingContextだけで線を引いたり、図形を描いたりといったこともできてしまいます。もっとも、そういったことをするのであれば、あえてp5.jsを使う必要はなく、最初からバニラなJavaScriptで直接書いてしまうほうがいいわけですが。
canvasにあって、p5.jsには直接対応するAPIがない機能としては、今回の「シャドウ(グロー)」のほか、「グラデーション」あたりが、スケッチを書く際に便利そうです。
●drawingContextでグラデーションを使う (コードのみ)
function setup() { createCanvas(300, 300); var ctx = drawingContext; background(230); translate(width/2, height/2); var gradient = ctx.createRadialGradient(-15,-30,15, 0,0,70); gradient.addColorStop(0, 'orangered'); gradient.addColorStop(.99, 'blueviolet'); gradient.addColorStop(1, 'navy'); ctx.fillStyle = gradient; ctx.fillRect(-100, -100, 200, 200); noLoop(); }
「描画は基本的にp5.jsでやるけど、シャドウやグロー、グラデーションに関しては、drawingContextで簡単に出す」というような使い分けをすると良さそうですね。
p5.js ver.1時代のシャドウとグロー はコメントを受け付けていません