51
Fibonacci Spiral and the Golden Ratio
which is the positive root of . Convergence is geometric, so by the ratio already matches to four decimals.
idle
158 lines · vanilla
view source
// Fibonacci Spiral — squares of side F_n laid corner-to-corner, joined by
// quarter-circle arcs into the golden spiral. F_n / F_{n-1} -> phi.
let W, H, squares, scale, cx, cy, build, restartAt;
const MAX_N = 12; // F_12 = 144; ratio converges fast
const PHI = (1 + Math.sqrt(5)) / 2;
function fibs(n) {
const f = [1, 1];
for (let i = 2; i < n; i++) f.push(f[i - 1] + f[i - 2]);
return f;
}
// Build squares in math coords (y up). Direction cycles CCW: L, U, R, D.
// Each new square attaches along the long edge of the previous bounding box.
function layout() {
const F = fibs(MAX_N);
const sq = [];
// Square 0: unit at origin.
sq.push({ x: 0, y: 0, s: F[0], dir: 0 });
// Bounding box tracker.
let minX = 0, minY = 0, maxX = F[0], maxY = F[0];
// Direction order: L, U, R, D, L, U, ... attach to that side of the bbox.
const DIRS = ["L", "U", "R", "D"];
for (let i = 1; i < MAX_N; i++) {
const s = F[i];
const d = DIRS[(i - 1) % 4];
let x, y;
if (d === "L") { x = minX - s; y = minY; minX -= s; }
else if (d === "U") { x = minX; y = maxY; maxY += s; }
else if (d === "R") { x = maxX; y = maxY - s; maxX += s; }
else { x = maxX - s; y = minY - s; minY -= s; }
sq.push({ x, y, s, dir: d });
}
return sq;
}
function fitToCanvas() {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const s of squares) {
if (s.x < minX) minX = s.x;
if (s.y < minY) minY = s.y;
if (s.x + s.s > maxX) maxX = s.x + s.s;
if (s.y + s.s > maxY) maxY = s.y + s.s;
}
const bw = maxX - minX, bh = maxY - minY;
const pad = 50;
scale = Math.min((W - pad * 2) / bw, (H - pad * 2) / bh);
cx = W / 2 - ((minX + maxX) / 2) * scale;
cy = H / 2 + ((minY + maxY) / 2) * scale; // y inverted
}
function toCanvas(x, y) {
return [cx + x * scale, cy - y * scale];
}
function squareColor(i) {
const h = (i * 34 + 200) % 360;
return `hsl(${h} 80% 60%)`;
}
function drawSquare(ctx, s, i, alpha) {
const [px, py] = toCanvas(s.x, s.y + s.s); // top-left in canvas coords
const sp = s.s * scale;
ctx.globalAlpha = alpha * 0.18;
ctx.fillStyle = squareColor(i);
ctx.fillRect(px, py, sp, sp);
ctx.globalAlpha = alpha * 0.9;
ctx.strokeStyle = squareColor(i);
ctx.lineWidth = 1.5;
ctx.strokeRect(px, py, sp, sp);
ctx.globalAlpha = alpha;
if (sp > 26) {
ctx.fillStyle = "#fff";
const fs = Math.min(18, Math.max(10, sp * 0.18)) | 0;
ctx.font = `${fs}px ui-monospace, monospace`;
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillText(String(s.s), px + sp / 2, py + sp / 2);
}
ctx.globalAlpha = 1;
}
// Arc inside each square. The pivot corner is the one farthest from the
// growing edge, so successive arcs join into a continuous CCW spiral.
// In math coords the arc sweeps a quarter (pi/2). Mapping by dir:
// square 0 (the seed unit): pivot top-right, angle pi -> 3pi/2 (down-left quadrant)
// dir L: pivot bottom-right, angle pi/2 -> pi (up-left)
// dir U: pivot bottom-left, angle 0 -> pi/2 (up-right)
// dir R: pivot top-left, angle 3pi/2 -> 2pi (down-right)
// dir D: pivot top-right, angle pi -> 3pi/2 (down-left)
function arcParams(s, i) {
const x = s.x, y = s.y, sp = s.s;
let dir = s.dir;
if (i === 0) dir = "D"; // seed square: same orientation as D-attached
let ax, ay, a0, a1;
if (dir === "L") { ax = x + sp; ay = y; a0 = Math.PI / 2; a1 = Math.PI; }
else if (dir === "U") { ax = x; ay = y; a0 = 0; a1 = Math.PI / 2; }
else if (dir === "R") { ax = x; ay = y + sp; a0 = 3 * Math.PI / 2; a1 = 2 * Math.PI; }
else /* D */ { ax = x + sp; ay = y + sp; a0 = Math.PI; a1 = 3 * Math.PI / 2; }
return { ax, ay, a0, a1, r: sp };
}
function drawArc(ctx, s, i, t) {
const p = arcParams(s, i);
const [px, py] = toCanvas(p.ax, p.ay);
const r = p.r * scale;
// Canvas y is flipped: math angle a maps to canvas angle -a; CCW sweep
// in math becomes CW in canvas, so pass anticlockwise=false with swapped ends.
const cStart = -p.a0;
const cEnd = -(p.a0 + (p.a1 - p.a0) * t);
ctx.beginPath();
// Going from cStart to cEnd clockwise in canvas (anticlockwise=false), but
// since cEnd < cStart we need anticlockwise=true to sweep the short way.
ctx.arc(px, py, r, cStart, cEnd, true);
ctx.strokeStyle = "#ffe27a";
ctx.lineWidth = 2.4;
ctx.shadowColor = "rgba(255,226,122,0.6)";
ctx.shadowBlur = 6;
ctx.stroke();
ctx.shadowBlur = 0;
}
function drawHUD(ctx) {
const built = build.idx; // 0..MAX_N-1
const F = squares.map(s => s.s);
const n = built + 1; // human-facing index: F_1 = 1
const fn = F[built];
const fnm1 = built >= 1 ? F[built - 1] : 1;
const ratio = built >= 1 ? fn / fnm1 : 1;
const err = Math.abs(ratio - PHI);
ctx.fillStyle = "rgba(0,0,0,0.62)";
ctx.fillRect(10, 10, 256, 100);
ctx.fillStyle = "#fff";
ctx.font = "13px ui-monospace, monospace";
ctx.textAlign = "left";
ctx.textBaseline = "alphabetic";
ctx.fillText("Fibonacci spiral", 18, 30);
ctx.fillStyle = "#cfe8ff";
ctx.fillText(`n = ${n} F_n = ${fn}`, 18, 50);
ctx.fillStyle = "#ffe27a";
ctx.fillText(`F_n / F_{n-1} = ${ratio.toFixed(8)}`, 18, 70);
ctx.fillStyle = "#9be3a3";
ctx.fillText(`phi - ratio = ${err.toExponential(2)}`, 18, 90);
ctx.fillStyle = "rgba(255,255,255,0.55)";
ctx.fillText(`phi = ${PHI.toFixed(8)}`, 18, 108);
}
function init({ width, height }) {
W = width; H = height;
squares = layout();
fitToCanvas();
build = { idx: 0, t: 0 };
restartAt = 0;
}
function tick({ ctx, dt, width, height, time }) {
if (width !== W || height !== H) {
W = width; H = height;
fitToCanvas();
}
ctx.fillStyle = "#070914";
ctx.fillRect(0, 0, W, H);
// Construction pace: ~1.2 squares/sec, slower at the start so early steps are readable.
const baseSpeed = 0.6 + Math.min(1.4, build.idx * 0.12);
build.t += (dt || 0.016) * baseSpeed;
if (build.t >= 1) {
if (build.idx < squares.length - 1) {
build.idx++;
build.t = 0;
} else {
build.t = 1;
if (!restartAt) restartAt = time + 4.5;
if (time >= restartAt) {
build.idx = 0;
build.t = 0;
restartAt = 0;
}
}
}
for (let i = 0; i <= build.idx; i++) {
const a = i < build.idx ? 1 : Math.min(1, 0.4 + build.t * 0.6);
drawSquare(ctx, squares[i], i, a);
}
for (let i = 0; i <= build.idx; i++) {
const t = i < build.idx ? 1 : build.t;
drawArc(ctx, squares[i], i, t);
}
drawHUD(ctx);
}
Comments (2)
Log in to comment.