👉 Read this article in English.
概略
Processing を使ったクリエイティブ・コーディングによるデジタルアートです。
再帰による回転運動の計算により中央の紋様を生成、その周辺を埋める円と四角形はサークル・パッキングの手法を用いて描いています。
どのように作っていったか?
最初のアイディアはひさだん(@hisadan)さんのツイートでした。
#つぶやきProcessing
— ひさだん (@hisadan) May 28, 2021
float r;void setup(){size(600,600);}void draw(){background(0);fill(255,99);a(300,300,150,r);r+=PI/99;}void a(float x,float y,float d,float r){float s,c;if(d>9){push();translate(x,y);s=d*sin(r);c=d*cos(r);circle(0,0,d*4);a(s,c,d/2,r*2);a(-s,c,d/2,r*2);pop();}} pic.twitter.com/zdQds8VIZi
再帰関数により円の入れ子を実現しています。
これがとても面白く、いろいろと描画を変えて遊んでいました。
入れ子の各円の中心の軌道上に点をプロットしたもの。
軌跡に点を打ってみる。🙂#processing #creativecoding pic.twitter.com/Ki959OpPDp
— deconbatch (@deconbatch) May 29, 2021
同様に円を描画した静止画。
軌道を曲線で描いたもの。
遊んでいるうちに、再帰関数の計算式を変えると面白い形が描画されることに気が付きました。
この面白い形を活かすことにし、このままではちょっと寂しいので、周辺や隙間をサークル・パッキングの手法で埋めることにしました。
サークル・パッキングでは新しく円を描く際に、既に円が存在する場所を避けるようにロジックが組まれています。そこで、面白い形を描画するための頂点を避けるべき場所としてあらかじめ設定することで、周辺と隙間だけをサークル・パッキングで埋められるようにしました。
配色はいろいろ迷った挙げ句、いまでもこれでいいのかよくわからないままです。
"Processing" のサンプルコード
GPL で公開します。GPL の条項に基づいて、どうぞご自由にお使いください。このコードを利用して何か作品を作ってもらえるととても嬉しいです。
本コードはスクリーン上には何も描画しません。描画の結果はファイルとして保存されます。
/**
* Under the Waterfall.
* draw shapes with the calculation of rotary movement recursively.
*
* @author @deconbatch
* @version 0.1
* @license GPL Version 3 http://www.gnu.org/licenses/
* Processing 3.5.3
* 2021.06.11
*/
void setup() {
size(980, 980);
colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
rectMode(CENTER);
smooth();
noLoop();
}
void draw(){
int ptnMax = 3;
float baseHue = random(360.0);
for (int ptnCnt = 0; ptnCnt < ptnMax; ptnCnt++) {
baseHue += 90.0;
// background image
noiseSeed(floor(baseHue + ptnCnt));
image(noiseField(baseHue), 0.0, 0.0);
// foreground image
ArrayList<ArrayList<PVector>> shapes = getShapes();
PGraphics fg = createGraphics(width, height);
fg.beginDraw();
fg.colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
fg.rectMode(CENTER);
fg.translate(width * 0.5, height * 0.5);
fg.background(0.0, 0.0, 60.0, 100.0);
scabrous(fg, baseHue);
lattice(fg, baseHue);
drawShape(fg, baseHue, shapes);
drawBubbles(fg, baseHue, shapes);
fg.endDraw();
image(fg, 0.0, 0.0);
// fancy casing
casing();
saveFrame("frames/" + String.format("%04d", ptnCnt + 1) + ".png");
}
exit();
}
/**
* noiseField : draw noise field
*/
PGraphics noiseField(float _hue) {
int cellSize = 3;
float noiseDiv = 0.01;
PGraphics p = createGraphics(width, height);
p.beginDraw();
p.colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
p.noStroke();
for (int bx = 0; bx < width; bx += cellSize) {
float nx = bx * noiseDiv;
for (int by = 0; by < height; by += cellSize) {
float ny = by * noiseDiv;
float nHue = noise(nx, ny, noise(10.0, nx, ny) * 5.0);
float nSat = noise(nx, ny, noise(20.0, nx, ny) * 8.0);
float nBri = noise(nx, ny, noise(30.0, nx, ny) * 10.0);
p.fill(
(_hue + nHue * 120.0) % 360.0,
30.0 + nSat * 60.0,
30.0 + nBri * 60.0,
100.0
);
p.rect(bx, by, cellSize, cellSize);
}
}
p.endDraw();
return p;
}
/**
* getShapes : get shapes locations
*/
ArrayList<ArrayList<PVector>> getShapes() {
int rotaryMax = 72;
int cycleMax = floor(random(1.0, 5.0));
float cycleRadius = min(width, height) * random(4.0);
float phase = floor(random(8.0)) * 0.5 * HALF_PI;
ArrayList<ArrayList<PVector>> shapes = new ArrayList<ArrayList<PVector>>();
for (int rotaryCnt = 0; rotaryCnt < rotaryMax; rotaryCnt++) {
float rotation = map(rotaryCnt, 0, rotaryMax, 0.0, TWO_PI);
ArrayList<PVector> p = new ArrayList<PVector>();
shapes.add(calcRotary(
0,
p,
cycleRadius * cos(rotation * cycleMax),
cycleRadius * sin(rotation * cycleMax),
width,
rotation + phase
));
}
// size adjust
float minX = width * 10.0;
float minY = height * 10.0;
float maxX = -minX;
float maxY = -minY;
for (ArrayList<PVector> shape : shapes) {
for (PVector v : shape) {
minX = min(minX, v.x);
minY = min(minY, v.y);
maxX = max(maxX, v.x);
maxY = max(maxY, v.y);
}
}
float adjust = 0.7 * min(width / (maxX - minX), height / (maxY - minY));
for (ArrayList<PVector> shape : shapes) {
for (PVector v : shape) {
v.mult(adjust);
}
}
return shapes;
}
/**
* calcRotary : calculate rotary movement recursively
*/
ArrayList<PVector> calcRotary(int _cnt, ArrayList<PVector> _p, float x, float y, float d, float r){
if (_cnt > 10) {
return _p;
}
float nx = d * cos(r) + x;
float ny = d * sin(r) + y;
_p.add(new PVector(nx, ny));
_p = calcRotary(_cnt + 1, _p, nx, ny, d * 0.75, -r * 5.0);
return _p;
}
/**
* scabrous : draw scabrous surface
*/
void scabrous(PGraphics _p, float _hue) {
for (int x = 0; x < width * 0.5; x += 3) {
for (int y = 0; y < height * 0.5; y += 3) {
float pSiz = random(0.5, 1.0);
float pDiv = random(-2.0, 2.0);
float pSat = 0.0;
if ((x + y) % 3 == 0) {
pSat = 80.0;
}
_p.strokeWeight(pSiz);
_p.stroke((_hue + 240.0) % 360.0, pSat, 30.0, 100.0);
_p.point(x + pDiv, y + pDiv);
_p.point(-x + pDiv, y + pDiv);
_p.point(x + pDiv, -y + pDiv);
_p.point(-x + pDiv, -y + pDiv);
}
}
}
/**
* lattice : draw lattice
*/
void lattice(PGraphics _p, float _hue) {
for (int x = -width; x < width * 0.5; x += 50) {
for (int y = -height; y < height * 0.5; y += 80) {
_p.strokeWeight(random(10.0));
_p.stroke((_hue + 210.0) % 360.0, random(5.0), 60.0, 100.0);
_p.line(x - 50.0, y, x + 50.0, y);
_p.line(x, y - 80.0, x, y + 80.0);
}
}
}
/**
* drawShape : draw foreground shape
*/
void drawShape(PGraphics _p, float _hue, ArrayList<ArrayList<PVector>> _shapes) {
_p.noFill();
for (ArrayList<PVector> shape : _shapes) {
// outer rim
_p.blendMode(BLEND);
_p.stroke(0.0, 0.0, 0.0, 100.0);
_p.beginShape();
for (PVector v : shape) {
float d = dist(0.0, 0.0, v.x, v.y);
_p.strokeWeight(3.0 + d * 40.0 / width);
_p.curveVertex(v.x, v.y);
}
_p.endShape();
_p.blendMode(BLEND);
_p.stroke((_hue + 120.0) % 360.0, 20.0, 30.0, 100.0);
_p.beginShape();
for (PVector v : shape) {
float d = dist(0.0, 0.0, v.x, v.y);
_p.strokeWeight(1.0 + d * 40.0 / width);
_p.curveVertex(v.x, v.y);
}
_p.endShape();
// inner rim
_p.blendMode(BLEND);
_p.beginShape();
for (PVector v : shape) {
float d = dist(0.0, 0.0, v.x, v.y);
if (d > width * 0.2) {
_p.stroke(0.0, 0.0, 0.0, 100.0);
_p.strokeWeight(2.0 + d * 10.0 / width);
} else {
_p.noStroke();
}
_p.curveVertex(v.x, v.y);
}
_p.endShape();
// center hole to see the background
_p.blendMode(REPLACE);
_p.stroke(0.0, 0.0, 0.0, 0.0);
_p.beginShape();
for (PVector v : shape) {
float d = dist(0.0, 0.0, v.x, v.y);
_p.strokeWeight(1.0 + d * 10.0 / width);
_p.curveVertex(v.x, v.y);
}
_p.endShape();
}
}
/**
* drawBubbles : draw circle packing around the shape
*/
void drawBubbles(PGraphics _p, float _hue, ArrayList<ArrayList<PVector>> _shapes) {
// circle packing around the shape
ArrayList<Circle> avoids = new ArrayList<Circle>();
for (ArrayList<PVector> points : _shapes) {
for (PVector v : points) {
float d = dist(0.0, 0.0, v.x, v.y);
avoids.add(new Circle(v.x, v.y, 20.0 + 0.2 * d));
}
}
ArrayList<Circle> packed = circlePacking(avoids, 5.0);
// circle packing background
_p.blendMode(BLEND);
_p.noStroke();
for (Circle c : packed) {
float dRatio = dist(0.0, 0.0, c.x, c.y) / max(width, height);
float eR = c.r * (60.0 / (10.0 + c.r)) * (1.0 + dRatio);
float eBri = 20.0 + 80.0 * constrain(dRatio, 0.3, 1.0);
_p.fill((_hue + 150.0) % 360.0, 20.0, eBri, 100.0);
_p.ellipse(c.x, c.y, eR, eR);
}
// draw packed circles hole
_p.blendMode(REPLACE);
_p.stroke(0.0, 0.0, 0.0, 100.0);
for (Circle c : packed) {
_p.fill(0.0, 0.0, 80.0, random(30.0, 100.0));
_p.strokeWeight(c.r * 0.1);
if (random(1.0) < 0.3) {
_p.rect(c.x, c.y, c.r, c.r);
} else {
_p.ellipse(c.x, c.y, c.r, c.r);
}
}
}
/**
* casing : draw fancy casing
*/
private void casing() {
pushMatrix();
translate(width * 0.5, height * 0.5);
fill(0.0, 0.0, 0.0, 0.0);
strokeWeight(44.0);
stroke(0.0, 0.0, 0.0, 100.0);
rect(0.0, 0.0, width, height);
strokeWeight(40.0);
stroke(0.0, 0.0, 100.0, 100.0);
rect(0.0, 0.0, width, height);
popMatrix();
}
/**
* circlePacking : bloom circles with the Circle Packing method.
* @param _av : circles to avoid.
* @param _gap : gap between circles.
*/
private ArrayList<Circle> circlePacking(ArrayList<Circle> _av, float _gap) {
int tryMax = 100; // a trying count to add and grow circles.
float growMax = 30.0;
ArrayList<Circle> circles = new ArrayList<Circle>();
for (int tryCnt = 0; tryCnt < tryMax; tryCnt++) {
// add new circles on the grid
for (int i = 0; i < 1000; i++) {
float addX = floor(random(-50.0, 50.0)) * 0.01 * width;
float addY = floor(random(-50.0, 50.0)) * 0.01 * height;
boolean inner = false;
for (Circle c : circles) {
if (dist(addX, addY, c.x, c.y) < c.r + _gap) {
inner = true;
break;
}
}
for (Circle c : _av) {
if (dist(addX, addY, c.x, c.y) < c.r + _gap * 2.0) {
inner = true;
break;
}
}
if (!inner) {
circles.add(new Circle(addX, addY, 0.0));
}
}
// grow circles
for (Circle cThis : circles) {
if (cThis.r < growMax) {
int collision = 0;
for (Circle cThat : circles) {
if (cThis != cThat) {
if (dist(cThis.x, cThis.y, cThat.x, cThat.y) < (cThis.r + cThat.r) * 0.5 + _gap) {
collision++;
}
}
}
for (Circle cAvoid : _av) {
if (dist(cThis.x, cThis.y, cAvoid.x, cAvoid.y) < (cThis.r + cAvoid.r) * 0.5 + _gap) {
collision++;
}
}
if (collision == 0) {
cThis.grow();
}
}
}
}
return circles;
}
/**
* Circle : draw and hold location, size and color.
*/
public class Circle {
private float x, y; // coordinate
private float r; // radius
Circle(float _x, float _y, float _r) {
x = _x;
y = _y;
r = _r;
}
public void grow() {
r++;
}
}
/*
Copyright (C) 2021- deconbatch
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
おまけの再帰関数による円の入れ子のアニメーション
円の回転運動ですよ。🙂🙃🙂
— deconbatch (@deconbatch) June 18, 2021
ref. https://t.co/i7UiX2UYYo#p5js #creativecoding pic.twitter.com/1Uq6nFuwF9
元のアイディアとなった再帰関数による円の入れ子のアニメーションのコードです。
p5.js のコードです。
// ref. https://twitter.com/hisadan/status/1398307061000146948
const w = 600;
const h = 600;
const frmRate = 24;
let r;
function setup() {
createCanvas(w, h);
frameRate(frmRate);
r = 0;
}
function draw() {
background(0);
fill(255, 100);
a(w * 0.5, h * 0.5, min(w, h) * 0.25, r);
r += PI / (frmRate * 6);
}
function a(x, y, d, r) {
if (d > 9) {
push();
translate(x, y);
let s = d * sin(r);
let c = d * cos(r);
circle(0, 0, d * 4);
a(s, c, d / 2, r * 2);
a(-s, -c, d / 2, -r * 2);
pop();
}
}
ついでに、円の中心部分だけを点で描画するとこんな感じに。
その円の中心部分だけを描画しましたよ。🙄#p5js #creativecoding pic.twitter.com/4zmQoZf1D0
— deconbatch (@deconbatch) June 18, 2021