「Processing Advent Calendar 2022」二十日の記事で、ギン / Gin(@gin_graphic)さんが L-system の解説をされていました。
【p5.js】L-systemで木を描く - ギンの備忘録
大変わかりやすく書かれていて、記事中のコードを使って簡単に L-system での描画を試すことができました。
👉 Read this article in English.
いろいろ試してみる中で私が惹かれたのは、特定の回転角度のときに描かれる幾何学模様でした。
L-system を使った描画は、変換式や回転角度などの組み合わせで結果が様々に変化します。しかも、どういう組み合わせで結果がどうなるかの予測がとても難しいです。
そんな無限の組み合わせがある中で、ハートにビビッとくる描画結果を見つけるための「L-system パラメータお試しキット」を p5.js で作成しました。
角度を変えるだけで別の顔
前述の ギン / Gin さんの記事中にあるコード、そのコードの角度の値を変えただけで、描画される画像の雰囲気が全然違ってくることに興味を持ちました。
let distance = 20;
let angle = radians(90);
let distance = 20;
let angle = radians(120);
樹木を模した図形が描かれるのも面白いけれど、私はこのような幾何学的な模様の方により面白みを感じます。変換式を変えてみるとまた違う幾何学模様が描かれました。変換式、変換回数、角度の組み合わせで様々な結果が得られる。これは無限に遊べてしまいそうです。
L-system パラメータお試しキット
より効率的に遊ぶため、自分が面白いと感じる「変換式、変換回数、角度」を探すのに使えるコードを p5.js で書いてみました。
実行すると、結果画像とパラメータ値が表示されます。上下キーで変換回数の増減、左右キーで角度を変更します。変換文字列は誤って消したりすると取り返しがつかない重要なものなので、画面入力とはせず、メモの意味も込めてコード中に書く方式にしました。
/**
* L-system tester.
* ref. https://gin-graphic.hatenablog.com/entry/2022/12/20/000000
*
* @author @deconbatch
* @version 0.1
* @license GPL3
* p5.js 1.5.0
* created 2022.12.31
*/
const initProduction = '+F[+F]F';
const maxRepeat = 10;
const maxAngle = 180;
const w = 800;
const h = w;
let repeatNum = 2;
let rotateAngle = 15;
/**
* keyPressed : change parameters then re-draw.
*/
function keyPressed() {
if (keyCode == UP_ARROW) {
repeatNum++;
} else if (keyCode == DOWN_ARROW) {
repeatNum--;
} else if (keyCode == RIGHT_ARROW) {
rotateAngle += 15;
} else if (keyCode == LEFT_ARROW) {
rotateAngle -= 15;
}
repeatNum = constrain(repeatNum, 1, maxRepeat);
rotateAngle = constrain(rotateAngle, -maxAngle, maxAngle);
redraw();
}
/**
* setup : world famous setup function.
*/
function setup() {
createCanvas(w, h)
angleMode(DEGREES);
noLoop();
noFill();
stroke(0);
}
/**
* draw : generate the production then draw it.
*/
function draw() {
// generate the production
const production = genProduction(initProduction, repeatNum);
// fit size and position with canvas
const fit = fitting(production, rotateAngle);
// draw
background(240);
push();
translate(fit.x, fit.y);
strokeWeight(3 / repeatNum);
drawShape(production, rotateAngle, fit.z);
pop();
drawParams(initProduction, repeatNum, rotateAngle, fit.z);
}
/**
* genProduction : generate the production
*/
function genProduction(_baseP, _num) {
let word = 'F';
for (let i = 0; i < _num; i++) {
let replaced = word.replace(/F/g, _baseP);
word = replaced;
}
return word;
}
/**
* fitting : calculate the size and position to fit to the canvas
*/
function fitting(_prd, _angle) {
let len = 10; // size
let cX = 0; // center x
let cY = 0; // center y
let minX = width;
let minY = height;
let maxX = 0;
let maxY = 0;
let x = 0;
let y = 0;
let a = 0;
const saveX = [];
const saveY = [];
const saveA = [];
for (let c of _prd) {
switch (c) {
case "F":
x += len * sin(a);
y += len * cos(a);
break;
case "+":
a += _angle;
break;
case "-":
a -= _angle;
break;
case "[":
saveX.push(x);
saveY.push(y);
saveA.push(a);
break;
case "]":
x = saveX.pop();
y = saveY.pop();
a = saveA.pop();
break;
default:
break;
}
minX = min(minX, x);
minY = min(minY, y);
maxX = max(maxX, x);
maxY = max(maxY, y);
}
const rate = max((maxX - minX) / width, (maxY - minY) / height) * 1.5;
if (rate != 0) {
len /= rate;
const rminX = minX / rate;
const rminY = minY / rate;
const rmaxX = maxX / rate;
const rmaxY = maxY / rate;
cX = width * 0.5 - (rminX + (rmaxX - rminX) * 0.5);
cY = height * 0.5 - (rminY + (rmaxY - rminY) * 0.5);
}
return createVector(cX, cY, len);
}
/**
* drawShape : draw the shape
*/
function drawShape(_prd, _angle, _len) {
for (let c of _prd) {
switch (c) {
case "F":
line(0, 0, 0, _len);
translate(0, _len);
break;
case "+":
rotate(-_angle);
break;
case "-":
rotate(+_angle);
break;
case "[":
push();
break;
case "]":
pop();
break;
default:
break;
}
}
}
/**
* drawParams : draw parameters
*/
function drawParams(_f, _n, _a, _l) {
const siz = 12;
push();
noStroke();
fill(0);
textSize(siz);
textAlign(LEFT, CENTER);
textFont('monospace');
text('PRODUCTION:' + _f, 10, siz);
text(' REPEAT:' + _n, 10, siz * 2);
text(' ANGLE:' + _a, 10, siz * 3);
text(' LENGTH:' + _l, 10, siz * 4);
pop();
}
/*
Copyright (C) 2022- 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/>
*/
コードは GPL3 で公開します。ライセンス条項に則ってご自由にお使いください。
私の愛したパラメータ
お試しキットで見つけたお気に入りの画像をいくつかご紹介します。
オバケちゃん
blendMode(SCREEN) で描画してみました。
- 変換式:-F[+F+F]-F
- 変換回数:8
- 角度:150度
秋の収穫
角度を変えて作った2つの形を組み合わせたものです。ellipse() と line() で描画しました。
- 変換式:F[[[+FF]-F[-F]]F]-F
- 変換回数:4
- 角度:13度と -9度
line() だけだとこうなります。
抽象画
こちらも角度の違う2つの形を組み合わせたものです。角度をジャストからちょっとずらしたらボカシ的効果が出ました。
- 変換式:F[+F]-F[+F[-F]+F]
- 変換回数:5
- 角度:60.1度と 120.1度
いろいろ組み合わせると、こんな絵も
L-systemで無限の時を…
L-system はちょっとした違いが思わぬ結果につながるので、やり始めると本当にキリがありません。
皆さんも、今回紹介した「パラメータお試しキット」を使って、無限に時間を溶かしてみませんか?楽しいですよ。