Day6で、フィボナッチ螺旋を追加しました。中身を見ていきます。
リポジトリ:
GitHub Page(最新):
ソースの確認
Day6終了時点のソース:
draw関数
p5.jsでメイン描画をやるdraw関数をまず確認します。
p.draw = () => {
p.background(230, 20, 95); // 背景色(薄い紫系)
p.translate(p.width / 2, p.height / 2); // 座標系の原点を画面中央に移動
// マウスの位置で螺旋の密度とサイズを制御
let spiralDensity = p.map(p.mouseX, 0, p.width, 1, 5); // 螺旋の密度
let maxRadius = p.map(p.mouseY, 0, p.height, 100, 250); // 最大半径
// フィボナッチ螺旋を描画
drawFibonacciSpiral(maxRadius, spiralDensity);
// 黄金角(約137.5度)で配置された点を描画
drawGoldenAnglePoints(maxRadius);
angle += 0.5; // アニメーション用の角度を更新
};
大きく2つやっていることがわかります。
- 螺旋の密度と最大半径を指定してフィボナッチ螺旋を描画
- 黄金角(約137.5度)で配置された点を描画
なるほど。ところで螺旋の密度と最大半径とは何でしょうか。
フィボナッチ螺旋を描画する関数
ソースを見てみると、ポイントはコメントを書いてくれていますが、もう少し詳細に説明が欲しいところです。
ポイントだけコメントが書いてあるソース
// フィボナッチ螺旋を描画する関数
function drawFibonacciSpiral(maxRadius, density) {
p.noFill();
p.strokeWeight(2);
// 螺旋を複数描画してパターンを作成
for (let offset = 0; offset < 360; offset += 360 / 6) {
p.beginShape();
for (let theta = 0; theta < 720; theta += density) {
// 対数螺旋: r = a * e^(b*θ)
// フィボナッチ螺旋では b = log(φ) / 90° を使用
let rad = theta * p.PI / 180;
let r = (maxRadius / 10) * p.pow(goldenRatio, rad / (p.PI / 2));
if (r > maxRadius) break;
let x = r * p.cos(theta + offset + angle);
let y = r * p.sin(theta + offset + angle);
// 色相を半径に基づいて変化
let hue = (r / maxRadius) * 180 + 180;
p.stroke(hue, 80, 90, 70);
p.vertex(x, y);
}
p.endShape();
}
}
さっそくコメントを書くようにお願いしてみます。
チャットプロンプト (Agentモード、Claude Sonnet4.5):
最初のリクエスト
drawFibonacciSpiral関数について、数学的な説明とp5.jsの説明を初心者向けに、丁寧めにつけてほしい。
追加でお願い
p5.jsの説明は、使っているところの行末にコメントでつけて
修正後のソース:
/**
* フィボナッチ螺旋を描画する関数
*
* 【数学的な説明】
* フィボナッチ螺旋は、自然界でよく見られる美しいパターンです。
* 貝殻の渦巻きや植物の葉の配置などに現れます。
*
* この螺旋は「対数螺旋」という数式で表現できます:
* r = a × φ^(θ / 90°)
* ここで:
* r = 中心からの距離(半径)
* θ = 角度(degrees)
* φ = 黄金比(約1.618)
* a = 初期サイズの係数
*
* 角度が増えると、半径が黄金比の累乗で大きくなっていくため、
* 美しい螺旋状のカーブが描かれます。
*
* @param {number} maxRadius - 螺旋の最大半径(マウスのY座標で変化)
* @param {number} density - 螺旋の密度(マウスのX座標で変化、値が小さいほど密)
*/
function drawFibonacciSpiral(maxRadius, density) {
p.noFill(); // 図形の塗りつぶしをなしに設定
p.strokeWeight(2); // 線の太さを2pxに設定
// 螺旋を6本描画して、花びらのようなパターンを作成
// offsetで60度ずつ(360/6)回転させた位置に配置
for (let offset = 0; offset < 360; offset += 360 / 6) {
p.beginShape(); // 連続した点を線で結ぶ図形の描画を開始
// 角度0度から720度まで螺旋を描画(2回転分)
// densityの値で点の間隔が変わる(小さいほど滑らかな螺旋になる)
for (let theta = 0; theta < 720; theta += density) {
// 数学的計算: 対数螺旋の公式 r = a × φ^(θ / 90°)
// thetaをラジアンに変換(p.PIを使用)
let rad = theta * p.PI / 180;
// 黄金比の累乗で半径を計算
// maxRadius/10 は初期サイズ、goldenRatioを累乗して成長させる
let r = (maxRadius / 10) * p.pow(goldenRatio, rad / (p.PI / 2));
// 半径が最大値を超えたら、この螺旋の描画を終了
if (r > maxRadius) break;
// 極座標(r, θ)をデカルト座標(x, y)に変換
// offset: 螺旋の回転位置、angle: アニメーション用の回転
let x = r * p.cos(theta + offset + angle);
let y = r * p.sin(theta + offset + angle);
// 色相を半径に基づいて変化(外側ほど色が変わる)
// 180〜360の範囲で色相が変化(青紫〜赤紫のグラデーション)
let hue = (r / maxRadius) * 180 + 180;
p.stroke(hue, 80, 90, 70); // 線の色を設定(色相, 彩度, 明度, 透明度)- HSBカラーモード
p.vertex(x, y); // 計算した座標に頂点を追加(これらの点が線で結ばれる)
}
p.endShape(); // 図形の描画を終了
}
}
対数螺旋(ベルヌーイ螺旋)
コメントを見る限り「対数螺旋(ベルヌーイ螺旋)」を書いているプログラムであることがわかります。しかし、なんということでしょう。ベルヌーイ螺旋という響きにこそほのかに記憶があるものの、定義については、まったく思い出せません。数学的な定義と実装を確認していきます。
数学的な定義
今こそ書籍の活躍するときです。対数螺旋(ベルヌーイ螺旋)は、極座標系で$r = a e^{b\theta}$ という式で表されることがわかりました。
ぐるぐる回して$\theta$が大きくなると同じ角度(30度と390度とか)で半径が大きくなるから螺旋になるのだと理解しました。
実装の確認
まず関数のコメントに以下が書いてあります。微妙に違います。$e$がいないし、$\theta/90°$ってなんでしょう...。この違いが「フィボナッチ」なんでしょうか。
* この螺旋は「対数螺旋」という数式で表現できます:
* r = a × φ^(θ / 90°)
* ここで:
* r = 中心からの距離(半径)
* θ = 角度(degrees)
* φ = 黄金比(約1.618)
* a = 初期サイズの係数
半径を計算する実装コードはここでしょうか。
// 数学的計算: 対数螺旋の公式 r = a × φ^(θ / 90°)
// thetaをラジアンに変換(p.PIを使用)
let rad = theta * p.PI / 180;
// 黄金比の累乗で半径を計算
// maxRadius/10 は初期サイズ、goldenRatioを累乗して成長させる
let r = (maxRadius / 10) * p.pow(goldenRatio, rad / (p.PI / 2));
degreeの世界とradianの世界が混ざっていて、理解が困難です…。
わからないことは質問しましょう。
チャットプロンプト (Askモード、Claude Sonnet4.5):
degreeとradianが混ざっていて、わかりにくいんだけど、なんで混ざってるの?
このコードでdegreeとradianが混ざっている理由を説明します:
混在の理由
- p5.jsの設定: p.angleMode(p.DEGREES) で度数法に設定されているため、p.cos() や p.sin() などのp5.js関数は度数法で動作します。
- 数学的計算: しかし、対数螺旋の公式 r = a × φ^(θ / 90°) の指数部分では、ラジアン単位での計算が必要なため、明示的に変換しています。
問題のコード
let rad = theta * p.PI / 180; // degreeをradianに変換
let r = (maxRadius / 10) * p.pow(goldenRatio, rad / (p.PI / 2)); // radianで計算
その後:
let x = r * p.cos(theta + offset + angle); // thetaはdegreeのまま使用
let y = r * p.sin(theta + offset + angle); // p.angleMode(DEGREES)のため
改善案
統一するには、以下のいずれかの方法があります:
方法1: すべてradianに統一
- p.angleMode(p.RADIANS) に変更
- すべての角度計算をradianで行う
方法2: degreeのまま一貫
- p.angleMode(p.DEGREES) は維持
- 指数計算部分も度数法ベースの式に調整
混在すると可読性が下がり、バグの原因になりやすいため、統一することをお勧めします。
では、統一することにします。
どちらでもいいなら、その辺に転がっている数学的な解説と同じラジアンにしたいと思います。
次やること
対数螺旋を実装するところをもう少しがんばります。