シンプルなコードで美しい図が描ける、クリエイティブ・コーディングの一手法「ノード・ガーデン」の作例を紹介します。工夫次第でいろいろ楽しめるサンプルコードも載せていますので、いじって作って遊んでみてください。
文中の解説は p5.js を使って行い、最後に作例の完全なコードを p5.js と Processing 両方で掲載しました。
👉 Read this article in English.
今回作成するクリエイティブ・コーディング作例
今回の作例は「円周上に配置した点(ノード)の間に線を引く」というものです。
元ネタは、クリエイティブコーダー @takawo さんの YouTube 動画「Daily Coding Live Sessions #15 [ARTSCLOUD]」です。
これは、 @takawo さんが自身のクリエイティブ・コーディングの様子をライブ配信するもので、作品を作る過程の考え方や、アイデアを形にしていく手順を知るのが面白くて、よく見ています。毎回生配信で、その場で考えながらコーディングしていくんですが、大丈夫かいな?というこちらの心配をよそに、毎回ちゃんと結果を出すところがすごいんです。
@takawo さんの例でランダム配置されていた円を、規則的に配置してみたらどうなるか?という作例になります。
まずはノードガーデンの基本のコード
「ノードガーデン」がどういうものか、どういうアルゴリズムで作られているかを知りたい場合は、やさしく解説した別記事がありますので、こちらをご参照ください。
今回の作例で使う基本形のコードはこちらになります。
/*
* ノードガーデンの基本のコード
* p5.js
* @author @deconbatch
* @version 0.1
* created 2022.05.04
* license CC0 https://creativecommons.org/publicdomain/zero/1.0/
*/
function setup() {
createCanvas(640, 640);
const baseDist = min(width, height);
const nodes = getNodes();
drawNodeGarden(nodes, baseDist * 0.1, baseDist * 0.2);
for (let n of nodes) {
circle(n.x, n.y, 10);
}
}
/*
* getNodes : ノードを配置して返す
*/
function getNodes() {
const nodes = new Array();
for (let i = 0; i < 50; i++) {
nodes.push(
createVector(
random(width),
random(height)
)
);
}
return nodes;
}
/*
* drawNodeGarden : 与えられたノードと距離の条件を基に線を引く
*/
function drawNodeGarden(_nodes, _minDist, _maxDist) {
for (let n of _nodes) {
for (let m of _nodes) {
let d = dist(n.x, n.y, m.x, m.y);
if (d > _minDist && d < _maxDist) {
line(n.x, n.y, m.x, m.y);
}
}
}
}
後から改造が容易なように、ノードの配置(getNodes)と、描画(drawNodeGarden)で関数を分けておきました。
関数 getNodes() の中身は、ランダムにノードを配置するコードになっています。この関数の中身を変えれば、他に影響を与えずにノードの配置を変えられるわけです。
関数 drawNodeGarden() は、下記三つのパラメータを受取り、ノードのお互いの距離が最短距離と最長距離の間にある場合に、そのノード間に線を引くコードです。
- ノード
- 最短距離
- 最長距離
ノードを二重ループで回してるんで、同じノード間で線を 2回引いちゃってますが、コードをなるべくシンプルにしたかったの。許してね。😉✨
下記のコードは、ノードの配置が思ったようになっているかを確認するために入れています。確認不要なら取ってしまっても構いません。
for (let n of nodes) {
circle(n.x, n.y, 10);
}
関数 getNodes() を改造して作例を作る
関数 getNodes() の中身を書き換えて、円周上にノードを 36個均等に置いてみます。
function getNodes() {
const nodeNum = 36; // 円周上のノード数
const ringR = min(width, height) * 0.4; // 円周の半径
const nodes = new Array();
for (let nodeCnt = 0; nodeCnt < nodeNum; nodeCnt++) {
let t = TWO_PI * nodeCnt / nodeNum;
let x = ringR * cos(t);
let y = ringR * sin(t);
nodes.push(createVector(x, y));
}
return nodes;
}
ノードの配置は上手くいっているようですね。では、この円を複数個、半径を変えながら同心円状に並べてみます。
円の数は 3個、外側にいくほどノードの数を多くしてみましょう。
function getNodes() {
const ringNum = 3; // 円の数
const nodeNumMin = 18; // 最も内側の円の円周上のノード数
const ringMaxR = min(width, height) * 0.4;
const nodes = new Array();
for (let ring = 0; ring < ringNum; ring++) {
let ringR = ringMaxR * (ring + 1) / ringNum;
let nodeNum = (ring + 1) * nodeNumMin;
for (let nodeCnt = 0; nodeCnt < nodeNum; nodeCnt++) {
let t = TWO_PI * nodeCnt / nodeNum;
let x = ringR * cos(t);
let y = ringR * sin(t);
nodes.push(createVector(x, y));
}
}
return nodes;
}
スポークの多すぎる車輪って感じですね。円周上のノードの数を減らすと幾何学模様のように、円を増やすと球状のように見えたりします。
const ringNum = 3;
const nodeNumMin = 6;
const ringNum = 6;
const nodeNumMin = 6;
クリエイティブ・コーディング的に色気を出す
円の数や、円周上のノードの数を変えることで、様々な形を作ることができました。ここではさらに進めて、クリエイティブ・コーディング的な面白みを加えていきましょう。
まずは、同心円状に配置する円の中心位置をずらしてみます。今回はランダムにずらしてみました。円が多い場合は渦巻状にずらすのも面白いです。
ずらす方向を phaseT、ずらし幅を phaseD としてコードを書いてみます。
let phaseT = random(TWO_PI);
let phaseD = ringMaxR * 0.25 / ringNum;
for (let nodeCnt = 0; nodeCnt < nodeNum; nodeCnt++) {
let t = TWO_PI * nodeCnt / nodeNum;
let x = ringR * cos(t) + cos(phaseT) * phaseD;
let y = ringR * sin(t) + sin(phaseT) * phaseD;
nodes.push(createVector(x, y));
}
ノード間に線を引く条件を変えてみても形に変化が表れます。同心円状の配置のままで、drawNodeGarden() 関数に渡す最短距離と最長距離を変えてみた例です。
drawNodeGarden(nodes, baseDist * 0.51, baseDist * 0.52);
drawNodeGarden(nodes, baseDist * 0.14, baseDist * 0.15);
最後に色を付けてみます。カラーモードを HSB にして、 drawNodeGarden() の中で、角度によって色相を変えるコードを書いてみます。
function setup() {
createCanvas(640, 640);
colorMode(HSB, 360, 100, 100, 100); // カラーモードを HSB に
// 角度によって色相を変える
let lHue = map(atan2(m.y - n.y, m.x - n.x), -PI, PI, 0, 360);
stroke(lHue, 60, 80, 100);
line(n.x, n.y, m.x, m.y);
極彩色!😳 色相は 360 全色使うよりも、120 ぐらいに抑えた方がバランス良く落ち着くでしょう。
// 例:220(空色)から 340(紫)まで
let lHue = (220 + map(atan2(m.y - n.y, m.x - n.x), -PI, PI, 0, 120)) % 360;
自由に使える p5.js/Processing の作例コード
説明に使用したコードを応用して作った作例のコードを、 p5.js/Processing それぞれで掲載します。CC0 で公開しますので、ご自由にお使いください。
p5.js コード
/*
* 円周上に配置した点の間に線を引く、クリエイティブ・コーディングの作例
*
* p5.js
* @author @deconbatch
* @version 0.1
* created 2022.05.04
* license CC0 https://creativecommons.org/publicdomain/zero/1.0/
*/
function setup() {
createCanvas(980, 980);
colorMode(HSB, 360, 100, 100, 100);
smooth();
noLoop();
const ringNum = 26; // ノードを置く円の個数
const nodeMin = 4; // 最小の円の円周上に置くノードの数
const minDist = 1.2; // 線を引くノード間の最短距離
const maxDist = 1.8; // 同、最長距離
const baseSiz = min(width, height) * 0.45;
const baseHue = random(360);
background((baseHue + 240) % 360, 90, 30, 100);
noFill();
translate(width * 0.5, height * 0.5);
rotate(random(PI));
drawNodeGarden(
getNodes(baseSiz, ringNum, nodeMin),
baseSiz * minDist / ringNum,
baseSiz * maxDist / ringNum,
baseHue
);
}
/*
* getNodes : ノードを配置して配列で返す
*
* _ringMaxR : ノードを置く円の最大半径
* _ringNum : ノードを置く円の個数
* _nodeNumMin : 最小の円の円周上に置くノードの数
*/
function getNodes(_ringMaxR, _ringNum, _nodeNumMin) {
const nodes = new Array();
for (let ring = 0; ring < _ringNum; ring++) {
let ringR = _ringMaxR * (ring + 1) / _ringNum;
let nodeNum = (ring + 1) * _nodeNumMin;
let phaseT = random(TWO_PI);
let phaseD = _ringMaxR * 0.5 / _ringNum;
for (let nodeCnt = 0; nodeCnt < nodeNum; nodeCnt++) {
let t = TWO_PI * nodeCnt / nodeNum;
let x = ringR * cos(t) + cos(phaseT) * phaseD;
let y = ringR * sin(t) + sin(phaseT) * phaseD;
nodes.push(createVector(x, y));
}
}
return nodes;
}
/*
* drawNodeGarden : 与えられたノードと距離の条件を基に線を引く
*
* _nodes : ノードの配列
* _min, _max : _min < ノード間の距離 < _max のとき線を引く
* _hue : ベース色
*/
function drawNodeGarden(_nodes, _min, _max, _hue) {
strokeWeight(3);
for (let i = 0; i < _nodes.length - 1; i++) {
let n = _nodes[i];
for (let j = i + 1; j < _nodes.length; j++) {
let m = _nodes[j];
let d = dist(n.x, n.y, m.x, m.y);
if (d > _min && d < _max) {
let lHue = _hue + map(d, _min, _max, 0, 120);
stroke(lHue % 360, 60, 80, 100);
line(n.x, n.y, m.x, m.y);
}
}
}
}
Processing コード
/*
* 円周上に配置した点の間に線を引く、クリエイティブ・コーディングの作例
*
* Processing 3.5.3
* @author @deconbatch
* @version 0.1
* created 2022.05.04
* license CC0 https://creativecommons.org/publicdomain/zero/1.0/
*/
void setup() {
size(980, 980);
colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
smooth();
noLoop();
int ringNum = 26; // ノードを置く円の個数
int nodeMin = 4; // 最小の円の円周上に置くノードの数
float minDist = 1.2; // 線を引くノード間の最短距離
float maxDist = 1.8; // 同、最長距離
float baseSiz = min(width, height) * 0.45;
float baseHue = random(360);
background((baseHue + 240.0) % 360.0, 90.0, 30.0, 100.0);
noFill();
translate(width * 0.5, height * 0.5);
rotate(random(PI));
drawNodeGarden(
getNodes(baseSiz, ringNum, nodeMin),
baseSiz * minDist / ringNum,
baseSiz * maxDist / ringNum,
baseHue
);
}
/*
* getNodes : ノードを配置して配列で返す
*
* _ringMaxR : ノードを置く円の最大半径
* _ringNum : ノードを置く円の個数
* _nodeNumMin : 最小の円の円周上に置くノードの数
*/
ArrayList<PVector> getNodes(float _ringMaxR, int _ringNum, int _nodeNumMin) {
ArrayList<PVector> nodes = new ArrayList();
for (int ring = 0; ring < _ringNum; ring++) {
float ringR = _ringMaxR * (ring + 1) / _ringNum;
int nodeNum = (ring + 1) * _nodeNumMin;
float phaseT = random(TWO_PI);
float phaseD = _ringMaxR * 0.5 / _ringNum;
for (int nodeCnt = 0; nodeCnt < nodeNum; nodeCnt++) {
float t = TWO_PI * nodeCnt / nodeNum;
float x = ringR * cos(t) + cos(phaseT) * phaseD;
float y = ringR * sin(t) + sin(phaseT) * phaseD;
nodes.add(new PVector(x, y));
}
}
return nodes;
}
/*
* drawNodeGarden : 与えられたノードと距離の条件を基に線を引く
*
* _nodes : ノードの配列
* _min, _max : _min < ノード間の距離 < _max のとき線を引く
* _hue : ベース色
*/
void drawNodeGarden(ArrayList<PVector> _nodes, float _min, float _max, float _hue) {
strokeWeight(3.0);
for (int i = 0; i < _nodes.size() - 1; i++) {
PVector n = _nodes.get(i);
for (int j = i + 1; j < _nodes.size(); j++) {
PVector m = _nodes.get(j);
float d = dist(n.x, n.y, m.x, m.y);
if (d > _min && d < _max) {
float lHue = _hue + map(d, _min, _max, 0.0, 120.0);
stroke(lHue % 360.0, 60.0, 80.0, 100.0);
line(n.x, n.y, m.x, m.y);
}
}
}
}