JavaScriptで回転する「対数らせん」と目玉を描きます。
// 目玉の画像
let eye_img = new Image();
eye_img.src = "./eye.png";
...
/**
* 指定矩形領域内に対数らせんを複製回転してポリゴン風に塗りつぶします。
*
* - r = a * exp(b * theta) の対数らせんを theta を delta_theta で刻んで点列化し、
* その点列を回転コピーして順方向・逆方向のポリラインを組み合わせることで
* 塗りつぶしポリゴンを作成します。
* - 描画範囲は矩形でクリップされ、背景で塗りつぶされます。
*
* @param {CanvasRenderingContext2D} ctx - 描画先の 2D コンテキスト
* @param {number} x - 描画領域の左上 x 座標
* @param {number} y - 描画領域の左上 y 座標
* @param {number} width - 描画領域の幅
* @param {number} height - 描画領域の高さ
* @param {number} [a=10] - 対数らせんのスケール係数(r = a * exp(b * theta))
* @param {number} [b=0.5] - 対数らせんの成長率(正なららせんが外側へ急速に広がる)
* @param {number} [delta_theta=0.1] - 角度刻み(ラジアン)。小さくすると滑らかになるが計算量が増える
* @param {number} [count=8] - 回転コピーの数(複製して配置する分割数、>=1)
* @param {number} [rotation=0] - らせん全体に加える回転量(ラジアン)
* @param {string|CanvasGradient|CanvasPattern} [foreColor="black"] - 塗りつぶし色
* @param {string} [bgColor="white"] - 背景色(描画領域を塗りつぶす)
*
* 備考:
* - この関数は drawPolyline(ctx, points, reverse) に依存します(drawPolyline はパス構築のみを行う想定)。
* - 打ち切り条件やサンプリング密度、count 等を外部から調整することで表現やパフォーマンスを制御できます。
*/
const drawLogarithmicSpiral = (ctx, x, y, width, height, a = 10, b = 0.5, delta_theta = 0.1, count = 8, rotation = 0, foreColor = "black", bgColor = "white") => {
ctx.save();
// クリッピングする
ctx.beginPath();
ctx.rect(x, y, width, height);
ctx.clip();
// 背景を塗りつぶす
ctx.beginPath();
ctx.fillStyle = bgColor;
ctx.fillRect(x, y, width, height);
// 中心点に座標変換する
let cx = x + width / 2, cy = y + height / 2;
ctx.translate(cx, cy);
// 画面の大きさを考慮する。
let minxy = Math.min(width, height), maxxy = Math.max(width, height);
// 対数らせんの曲線を線分で近似し頂点を配列に格納する。
let points = []
for (let theta = 0; ; theta += delta_theta) {
// 公式通り計算する
let r = a * Math.exp(b * theta);
if (r >= maxxy * Math.sqrt(2)) {
break; // 半径が大きすぎる場合は打ち切る
}
// 頂点を計算する
let x0 = r * Math.cos(theta + rotation), y0 = r * Math.sin(theta + rotation);
points.push({x: x0, y: y0});
}
// 実際にポリゴンを描く
ctx.fillStyle = foreColor;
ctx.beginPath();
const delta_angle = (2 * Math.PI) / count / 2;
for (let i = 0; i < count; ++i) {
let angle = (i / count) * (2 * Math.PI);
ctx.save();
ctx.beginPath();
ctx.rotate(angle);
drawPolyline(ctx, points); // 正順
ctx.rotate(delta_angle);
drawPolyline(ctx, points, true); // 逆順
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
}
ctx.restore();
};
const draw_0 = (ctx, x, y, width, height) => {
ctx.save();
// 中心点
let cx = x + width / 2, cy = y + height / 2;
// 画面の大きさを考慮する。
let minxy = Math.min(width, height), maxxy = Math.max(width, height);
let avgxy = (minxy + maxxy) / 2;
// 対数らせんを描く。数値は微調整する必要がある。
let a = 10, b = 0.5;
let delta_theta = 0.1, count = 8, rotation = -time / 200;
ctx.strokeStyle = "gray";
ctx.lineWidth = avgxy / 50;
drawLogarithmicSpiral(ctx, 0, 0, width, height, a, b, delta_theta, count, rotation, "black", "white");
// 座標変換により回転運動をさせる
let radius = minxy / 10;
let rotation2 = time / 500;
ctx.translate(radius * Math.cos(rotation2), radius * Math.sin(rotation2));
// 放射状のグラデーションを作る
let rInner = 0, rOuter = minxy / 2;
const g = ctx.createRadialGradient(cx, cy, rInner, cx, cy, rOuter);
g.addColorStop(0, `rgba(0, 0, 0, 100%)`); // 黒
g.addColorStop(1, `rgba(0, 0, 0, 0%)`); // 透明の黒
// 半透明のグラデーション(影)を付ける
ctx.beginPath();
ctx.arc(cx, cy, rOuter, 0, 2 * Math.PI, false);
ctx.fillStyle = g;
ctx.fill();
// 画像を使って目玉を描く
if (eye_img.complete) { // 画像読み込み完了?
let ex = minxy / 3;
let ey = ex / eye_img.width * eye_img.height; // アスペクト比を考慮
if (time % 1000 < 100)
ey /= 3; // またたき
ctx.drawImage(eye_img, 0, 0, eye_img.width, eye_img.height, cx - ex / 2, cy - ey / 2, ex, ey);
}
ctx.restore();
};![[スクリーンショット]](/katahiromz/logarithmic_spiral/raw/main/img/screenshot.png)