Fractal contour

Inspired by youtu.be/-rdowhmqp5s
It can render the Mandelbrot and so much more!
Create your own fractals in the fz() and dfz() (derivative) functions.
There are several known issues that make this implementation mathematically inaccurate... For starters, it's supposed to use the polynomial roots as targets, but I'm using a fixed grid instead.
I'm also using a sort of marching squares approach to get the contour of all slices in a single pass.

Log in to post a comment.

// LL 2021

const turtle = new Turtle();

const scene = 0; // min=0 max=5 step=1 (z^5 + z^2 - z + 1,z^mb_pow^2+z^2-z+1,z^mb_pow,∑(z^i),sin(z)^mb_pow,Random stuff)
const precision = 500; // min=10 max=1500 step=1
const scale = 8; // min=1, max=10, step=0.1
const scale_power = 2; // min=0.1, max=10, step=0.1
const offset_x = 0; // min=-1, max=1, step=0.01
const offset_y = 0; // min=-1, max=1, step=0.01
const style = 0; // min=0 max=1 step=1 (Newton Raphson,Iterations to limit)

const mb_pow = 2; // min=1 max=6 step=1
const limit = 50;
const max_iterations = 100; // min=0 max=1000 step=1

const angle = 0; //0.25;
const EPS = 0.01;

function walk(i, t) {
    if (i == 0) {
        scale_t = 1 / (scale ** scale_power); // * t;
        max_iterations_t = max_iterations + t;
        buildRoots();
    }
    
    if (!contour(i, t, getFractal)) {
        return false;
    }
    
    return true;
}

function terrace(value) {
    return value;
}

const roots = [];
function buildRoots() {
    // These should have been the roots, but this is simpler and makes nice fractals
    roots.push(new Complex(0));

    for (var rx=0.025; rx<20.1; rx*=2) {
        for (var iy=0.025; iy<20.1; iy*=2) {
            roots.push(new Complex( rx,  iy));
            roots.push(new Complex( rx, -iy));
            roots.push(new Complex(-rx,  iy));
            roots.push(new Complex(-rx, -iy));
        }
    }
}

function fz(z) {
    if (scene == 0) {
        const z1 = z.dup();
        const z5 = z.dup().pow(5);
        const z2 = z.dup().pow(2);
        return z1.mulf(-1).add(z5).add(z2).addf(1);
    }
    
    if (scene == 1) {
        return z.dup().pow(mb_pow**2).add(z.dup().pow(2)).sub(z.dup()).addf(1);
    }

    if (scene == 2) {
        return z.dup().pow(mb_pow);
    }

    if (scene == 3) {
        const zr = new Complex(0);
        for (var i=1; i<mb_pow**2; i++) {
            zr.add(z.dup().pow(i));
        }
        return zr;
    }
    
    if (scene == 4) {
        const zr = z.dup().pow(mb_pow);
        return zr;
    }
    
    if (scene == 5) {
        const zr = z.dup().sin().pow(mb_pow).sin().pow(2).sin().pow(mb_pow).sin();
        return zr;
    }
}

function dfz(z) {
    if (scene == 0) {
        const c1 = z.dup();
        const c4 = z.dup().pow(4);
        return c1.mulf(2).add(c4.mulf(5)).addf(-1);
    }
    
    if (scene == 1) {
        const c1 = z.dup();
        const cn = z.dup().pow(mb_pow**2-1).mul(mb_pow**2);
        return c1.mulf(2).add(cn).addf(-1);
    }

    if (scene == 2) {
        return z.dup().pow(mb_pow*2).mulf(0.1);
    }

    if (scene == 3) {
        const zr = new Complex(0);
        for (var i=1; i<mb_pow**2; i++) {
            zr.add(z.dup().pow(i-1).mulf(i));
        }
        return zr;
    }
    
    if (scene == 4) {
        return z.dup().sin().pow(2);
    }
    
    if (scene == 5) {
        return z;
    }
}

// p: 2D point in -100 to 100 range
function getFractal(in_p2) {
    const x = rotX(in_p2[0], in_p2[1], angle * Math.PI * 2);
    const y = rotY(in_p2[0], in_p2[1], angle * Math.PI * 2);
    var p = new Complex(x * scale_t + offset_x, y * scale_t + offset_y);

    if (style == 0) {
        // Get close to a root
        for (var i=0; i<max_iterations_t; i++) {
            const fp = fz(p);
            const d = fp.len();
            if (d < EPS) break;
            const dp = dfz(p);
            p.sub(fp.dup().div(dp));
        }
        // p is now the closest point

        // Find closest root
        var v = -1;
        var min_dist = Number.MAX_VALUE;
        roots.forEach((r, id) => {
            const dc = p.dup().sub(r);
            const dist = dc.len();
            if (min_dist > dist) {
                min_dist = dist;
                v = id;
            }
        });
    
        return terrace(v);
    }
    
    var i, z = new Complex(0);
    for (i=0; i<max_iterations_t; i++) {
        const fp = fz(z).add(p);
        const d = fp.len();
        if (d >limit) break;
        z = fp;
    }
    return terrace(i);
}

// Basic operations for complex numbers
class Complex {
    constructor(re, im) { this.re = re; this.im = (im === undefined) ? 0 : im; }
    dup() { const c = new Complex(this.re, this.im); return c; }
    add(rhs) { this.re += rhs.re; this.im += rhs.im; return this; }
    sub(rhs) { this.re -= rhs.re; this.im -= rhs.im; return this; }
    addf(f) { this.re += f; return this; }
    mulf(f) { this.re *= f; this.im *= f; return this; }
    pow(p) { const c = this.dup(); for (var i=1; i<p; i++) this.mul(c); return this; }
    len() { return Math.hypot(this.re, this.im); }
    abs() { this.re = Math.abs(this.re); this.im = Math.abs(this.im); return this; }

    mul(rhs) {
        const re = this.re * rhs.re - this.im * rhs.im;
        const im = this.re * rhs.im + this.im * rhs.re;
        this.re = re; this.im = im;
        return this;
    }

    div(rhs) {
		const re = (this.re * rhs.re + rhs.im * this.im) / (rhs.re * rhs.re + rhs.im * rhs.im);
		const im = (rhs.re * this.im - this.re * rhs.im) / (rhs.re * rhs.re + rhs.im * rhs.im);
		this.re = re; this.im = im;
        return this;
    }
    
    sin() {
        const a = this.re;
        const b = this.im;
        this.re = Math.sin(a) * Math.cosh(b);
        this.im = Math.cos(a) * Math.sinh(b);
        return this;
    }
}

function rotX(x, y, a) { return Math.cos(a) * x - Math.sin(a) * y; }
function rotY(x, y, a) { return Math.sin(a) * x + Math.cos(a) * y; }

//////////////////////////////////////////
// Contour utility by Lionel Lemarie 2021
// https://turtletoy.net/turtle/765d77abf4
function contour(i, t, zFunc) {
    if (i == 0) { cache = {}; }
    const r = 100 / precision;
    
    if (i >= precision*precision) return false;

    const xx = i % precision;
    const yy = (i / precision) | 0;
    
    const ci00 = (xx-1) + (yy-1) * precision;
    const ci01 = (xx-0) + (yy-1) * precision;
    const ci10 = (xx-1) + (yy-0) * precision;
    const ci11 = (xx-0) + (yy-0) * precision;

    const x0 = ((xx-1) / precision - 0.5) * 200;
    const y0 = ((yy-1) / precision - 0.5) * 200;
    const x1 = ((xx-0) / precision - 0.5) * 200;
    const y1 = ((yy-0) / precision - 0.5) * 200;

    const z00 = cache[ci00], z01 = cache[ci01], z10 = cache[ci10];
    const z11 = cache[ci11] = zFunc([x1, y1]);

    var lines = [];
    
    // A bit like marching cubes
    if (z00 != z01 && z00 != z10 && z00 != z11) lines = [[[1,0],[0,1]],[[0,1],[1,2]],[[1,2],[2,1]],[[2,1],[1,0]]];
    if (z00 == z01 && z00 == z10 && z00 != z11) lines = [[[2,1],[1,2]]];
    if (z00 == z01 && z00 == z11 && z00 != z10) lines = [[[0,1],[1,2]]];
    if (z00 == z01 && z10 == z11 && z00 != z10) lines = [[[0,1],[2,1]]];
    if (z00 == z10 && z00 == z11 && z00 != z01) lines = [[[1,0],[2,1]]];
    if (z00 == z10 && z01 == z11 && z00 != z01) lines = [[[1,0],[1,2]]];
    if (z00 == z11 && z01 == z10 && z00 != z01) lines = [[[1,0],[0,1]],[[2,1],[1,2]]];
    if (z01 == z10 && z01 == z11 && z00 != z01) lines = [[[1,0],[0,1]]];

    lines.forEach(l => {
        turtle.jump(r/2+x0+l[0][0]*r, r/2+y0+l[0][1]*r);
        turtle.goto(r/2+x0+l[1][0]*r, r/2+y0+l[1][1]*r);
    });

    return true;
}