Fractal Spiral

A recursive, fractal spiral

Log in to post a comment.

Canvas.setpenopacity(0.54);

const arms = 18; // min=4 max=18 step=1 Primary spiral arms
const depth = 1; // min=1 max=7 step=1 Recursive lace layers
const revolutions = 12; // min=2 max=12 step=0.5 Turns per primary spiral
const spread = 0.2; // min=0.2 max=0.7 step=0.01 Radial growth per step
const organic = 0.01; // min=0 max=0.4 step=0.01 Turbulence in the flow
const lace = 1.00; // min=0 max=1 step=0.01 Density of lace loops
const bloom = 0.17; // min=0.05 max=0.6 step=0.01 Size of lace blooms
const weave = 1.0; // min=0 max=1 step=0.01 Filigree weaving between arms

// Slowpoke prevents overdrawing the same segment, keeping lines crisp.
function Slowpoke(x, y) {
    const drawn = {};
    class S extends Turtle {
        goto(x, y) {
            const p = Array.isArray(x) ? x : [x, y];
            if (this.isdown()) {
                const a = [this.x(), this.y()];
                const h1 = a[0].toFixed(6) + '_' + p[0].toFixed(6) + a[1].toFixed(6) + '_' + p[1].toFixed(6);
                const h2 = p[0].toFixed(6) + '_' + a[0].toFixed(6) + p[1].toFixed(6) + '_' + a[1].toFixed(6);
                if (drawn[h1] || drawn[h2]) {
                    super.up(); super.goto(p); super.down();
                    return;
                }
                drawn[h1] = drawn[h2] = true;
            }
            super.goto(p);
        }
    }
    return new S(x, y);
}

const turtle = Slowpoke();
turtle.pendown();

const steps = [];
const fullTurn = Math.PI * 2;
const golden = (1 + Math.sqrt(5)) / 2;

const baseSteps = Math.floor(revolutions * 190);
const seed = 271.713;

function fract(x) { return x - Math.floor(x); }
function hash(n) { return fract(Math.sin(n * 127.1 + seed) * 43758.5453); }
function signedNoise(n) { return hash(n) * 2 - 1; }
function smoothNoise(a, b, t) { return signedNoise(a) * (1 - t) + signedNoise(b) * t; }

function pushPoint(kind, x, y) { steps.push({ kind, x, y }); }

function addLace(cx, cy, dir, scale, jitter) {
const petals = 7 + Math.floor(6 * scale);
const radius = bloom * (0.6 + scale * 0.9) * (0.85 + 0.35 * signedNoise(jitter));
    const rot = dir + signedNoise(jitter * 1.3) * 0.6;
    const points = [];
    for (let i = 0; i <= petals; i++) {
        const a = rot + fullTurn * (i / petals);
        const micro = 1 + 0.25 * Math.sin(i * 3.1 + jitter * 0.9);
        const r = radius * micro;
        points.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]);
    }
    if (points.length < 2) return;
    pushPoint('jump', points[0][0], points[0][1]);
    for (let i = 1; i < points.length; i++) {
        pushPoint('draw', points[i][0], points[i][1]);
    }
}

// Recursive logarithmic spiral with lace and secondary curls.
function spawnSpiral(cx, cy, startTheta, scale, level, jitter) {
    const pointCount = Math.max(240, Math.floor(baseSteps * (0.45 + scale * 0.6)));
    const thetaStep = (fullTurn * revolutions) / baseSteps;
    const radialStep = spread * (0.7 + scale * 0.4);
    const curveBias = 0.4 + 0.35 * scale;
    const wobble = organic * (0.5 + 0.4 * scale);
    const laceEvery = Math.max(6, Math.floor((16 - lace * 12) + level * 1.8));
    let first = true;

    for (let i = 0; i < pointCount; i++) {
        const t = startTheta + i * thetaStep;
        const growth = Math.pow(i / pointCount, 1.18);
        const baseR = (1.2 + 0.45 * scale) + radialStep * i;
        const microCurl = 1 + 0.07 * Math.sin(t * 5 + jitter);
        const r = baseR * (1 + curveBias * growth) * microCurl *
            (1 + wobble * smoothNoise(jitter + i * 0.17, jitter + i * 0.31, 0.5));
        const x = cx + Math.cos(t) * r;
        const y = cy + Math.sin(t) * r;

        if (first) {
            pushPoint('jump', x, y);
            first = false;
        } else {
            pushPoint('draw', x, y);
        }

        if (lace > 0 && i % laceEvery === 0) {
            addLace(x, y, t + Math.PI / 2, scale, jitter + i);
        }

        if (weave > 0 && level === depth - 1 && i % (laceEvery + 3) === 0) {
            // Weave subtle bridges toward a neighbor arm to increase complexity.
            const phase = t + fullTurn / (arms * golden);
            const wx = x + Math.cos(phase) * r * 0.05 * weave;
            const wy = y + Math.sin(phase) * r * 0.05 * weave;
            pushPoint('jump', x, y);
            pushPoint('draw', wx, wy);
        }

        if (level > 0) {
            const branchEvery = Math.max(26, Math.floor(pointCount / (2 + level)));
            if (i > branchEvery * 0.5 && i % branchEvery === 0) {
                const tilt = (0.33 + 0.37 * hash(i + jitter)) * (signedNoise(i) >= 0 ? 1 : -1);
                const spin = t + tilt + 0.18 * signedNoise(jitter + i);
                const subScale = scale * (0.53 + 0.2 * hash(i + seed));
                spawnSpiral(x, y, spin, subScale, level - 1, jitter + i * 1.4);
            }
        }
    }
}

for (let i = 0; i < arms; i++) {
    const startTheta = i * (fullTurn / arms) + fullTurn / (golden * arms);
    spawnSpiral(0, 0, startTheta, 1, depth - 1, i * 11.7);
    // Secondary interleave for extra texture.
    spawnSpiral(0, 0, startTheta + fullTurn / (arms * 2), 0.52, Math.max(0, depth - 3), i * 13.9 + 5.1);
}

function walk(i) {
    const s = steps[i];
    if (!s) return false;
    if (s.kind === 'jump') turtle.jump(s.x, s.y);
    else turtle.goto(s.x, s.y);
    return true;
}