p5.js ver.1時代のシャドウとグロー


2020年12月13日
With
p5.js ver.1時代のシャドウとグロー はコメントを受け付けていません

僕が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で簡単に出す」というような使い分けをすると良さそうですね。