この記事では書籍「世界一美しい錯視アート」に掲載されている「触手」という作品をProcessingを使って再現してみます。
Contents
触手
今回作成した錯視アート「触手」です。
書籍では「触手が動いて見える。」と解説されています。自分には中心から外側に向けて波が広がっていくような印象があります。
描き方のポイント
背景はキャンバスより少し小さい
この作品をみると、触手の先が背景のグラデーションから少しはみ出していることが分かります。つまり、背景は描画キャンパスより少し小さめに描く必要があります。
これを実施するために、今回は以下の手順で行っています。
- 別記事「円形のグラデーションを描く」で紹介した方法で、まず描画キャンパス一杯に背景を描きます。
- 描画キャンパス一杯に描いた背景を一旦PImageクラスの変数に画像データとして取り込みます。
- 背景を白色で塗りつぶします。
- PImageクラスの変数に取り込んだ画像データを少しリサイズして描画キャンパスの中央に描画します。
楕円を並べていくときの注意点
背景を描くことができたら、後は白色と黒色の楕円を交互に並べていくことで触手を描いていきます。そんなに難しいことではないですが、注意点としては
- 楕円を並べた結果、その楕円の列は背景となっているグラデーションと同じサイズになること
- 触手の先は矩形で終わり、背景領域から少しはみ出ること
でしょうか。
中央の黒色の部分は楕円の残り、arc関数を使って表示する
中央の黒色の部分は触手の両端から楕円を並べていったとき、中央でぶつかった楕円の残りになります。これはarc関数を用いて楕円に対する円弧を準備し、それを対称に並べることで実現することができます。ただ、楕円に対する円弧を描くときは、注意が必要です。詳細は別記事「円弧を描く」の「楕円形の円弧を描く際の注意点」を見てください。
プログラムコード
今回作成した錯視アート「触手」のプログラムコードを載せておきます。
void setup() {
size(1000, 1000);
int ellipse_num = 27; // 中心から外側に向けて並べる楕円の数
float x0 = width/2.0 - width/100.0; // 背景グラデーションのサイズ(半分の値)。ここでは仮の設定。
float dist = x0 / ellipse_num; // 楕円の中心間の距離
float radius_x = dist * 5.0 / 4.0; // 楕円のx軸方向の半径(中心間距離より少し大きめにとる)
float radius_y = width/100.0; // 楕円のy軸方向の半径
float background_size = x0 + radius_x - dist; // 楕円の半径分調整。
PImage img = createImage(width, height, RGB); // 背景画像用に準備
noStroke();
translate(width/2, height/2);
// 背景となるグラデーションをキャンバス全体に描画
color col_out = color(#F52CDE); // 背景グラデーションの一番外側の色
color col_in = color(#A6E9FC); // 背景グラデーションの一番内側の色
grdCircle(0, 0, width*sqrt(2.0), col_out, col_in);
// 描画した背景グラデーションを一旦PImageに保存
loadPixels();
for(int j=0; j<img.height; j++){
for(int i=0; i<img.width; i++){
img.set(i, j, pixels[j*width + i]);
}
}
// 背景を一旦白色で上書き
background(255,255,255);
// 保存していた背景グラデーションを縮小して描画する
image(img, -background_size, -background_size, 2.0*background_size, 2.0*background_size);
// 触手となる図形を描いていく
int line_num = 6;
for(int i=0; i<line_num; i++){
drawEllipses(x0, radius_x, radius_y, dist, ellipse_num);
rotate(-radians(180.0/line_num));
}
}
// 円形のグラデーションを描く関数
void grdCircle(
float x, // 円の中心位置のx座標
float y, // 円の中心位置のy座標
float d, // 一番外側の円の直径
color col_out, // 円形のグラデーションの一番外側の色
color col_in // 円形のグラデーションの一番内側の色
) {
float c = 100;
for (int i=0; i<c; i++) {
color col = lerpColor(col_out, col_in, i/c); // 円の色
float dd = lerp(d, 0.0, i/c); // 円の直径
noStroke();
fill(col);
ellipse(x, y, dd, dd); // 円を描く
}
}
// 触手となる図形を描く関数
void drawEllipses(
float xs, // 背景のサイズ(半分の値)
float radius_x, // 楕円のx軸方向の半径(中心間距離より少し大きめにとる)
float radius_y, // 楕円のy軸方向の半径
float dist, // 楕円の中心間の距離
int num // 並べる楕円の数
){
int i = 0;
float x;
x = xs - dist;
// 一番外側は黒色の矩形を描く
fill(0,0,0);
rect(x, -radius_y, width/2.0-x, 2.0*radius_y);
rect(-width/2.0, -radius_y, width/2.0-x, 2.0*radius_y);
// 白色と黒色の楕円を交互に並べていく
while(i<num){
noStroke();
if (i % 2 == 0){
fill(255,255,255);
} else {
fill(0,0,0);
}
ellipse(x, 0.0, 2.0*radius_x, 2.0*radius_y);
ellipse(-x, 0.0, 2.0*radius_x, 2.0*radius_y);
x = x - dist;
i++;
}
// 中心位置には楕円の一部を描く
fill(0,0,0);
arc(x, 0.0, 2.0*radius_x, 2.0*radius_y, -atan(sqrt(pow(radius_x,2.0)-pow(dist,2.0))/dist), atan(sqrt(pow(radius_x,2.0)-pow(dist,2.0))/dist), OPEN);
arc(-x, 0.0, 2.0*radius_x, 2.0*radius_y, PI-atan(sqrt(pow(radius_x,2.0)-pow(dist,2.0))/dist), PI+atan(sqrt(pow(radius_x,2.0)-pow(dist,2.0))/dist), OPEN);
}