### Fractal 3D contour πΎπ―π’

Quaternion Julia Fractals:
paulbourke.net/fractals/quatjulia/

```// LL 2021

const turtle = new Turtle();

const precision = 750; // min=10 max=1500 step=1
const iterations = 16; // min=1 max=100 step=1
const scene = 1; // min=0 max=5 step=1
const scene0_radius = 1; // min=0 max=1 step=0.01
const scene0_angle = 0; // min=0 max=1 step=0.01
const fractal = 0; // min=0 max=1 step=1 (Square,Cube)
const cut = 0; // min=0 max=1 step=1 (No,Yes)
const trap = 0; // min=0 max=2 step=1 (No,Yes, Hide fractal)

function walk(i, t) {
if (i==0) {
turtle.jump(0, -95); turtle.circle(95);
turtle.jump(0, -97); turtle.circle(97);
setScene(t);
}
return contour(i, t, getFractal);
}

function setScene(t) {
const scenes = [
[0, 0, 0, 0],
[-1, 0.2, 0, 0],
[-0.213, -0.0410, -0.563, -0.560],
[-0.291,-0.399,0.339,0.437],
[-0.09, 0.27, 0.68, -0.27],
[-0.125, -0.256, 0.847, 0.0895]
];

scenes[0][0] = scene0_radius * 1 * Math.cos(scene0_angle);
scenes[0][1] = scene0_radius * 2 * Math.sin(scene0_angle);
scenes[0][2] = scene0_radius * 2 * Math.cos(scene0_angle * 2) * Math.sin(scene0_angle * 2);
scenes[0][3] = scene0_radius * 3 * Math.cos(scene0_angle * 4) * Math.sin(scene0_angle * 4);

t = (t * scenes.length + scene) % scenes.length;
const a = Math.floor(t);
const b = (a + 1) % scenes.length;
const f = t - a;

C = mix4(scenes[a], scenes[b], f);
}

function GetRayDir(uv2, p3, l3, z) {
var f3 = normalize3(sub3(l3, p3));
var r3 = normalize3(cross3([0,1,0], f3));
var u3 = cross3(f3, r3);
var c3 = mul3(f3, z);
var d3 = normalize3(i3);
return d3;
}

function getRay(p2) {
// Convert to -1 to 1
var uv2 = [ p2[0] / 100, p2[1] / 100 ];

// Look at
var l3 = [ 2, 0, 0 ];

// Ray origin
var ro3 = [ 4, 2, -7 ];

// Ray direction
var rd3 = GetRayDir(uv2, ro3, l3, 1.);

return [ro3, rd3];
}

function sum4(a, b)  {
return [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]];
}

function mul4(a, f)  {
return [a[0] * f, a[1] * f, a[2] * f, a[3] * f];
}

function qLength2(q) {
return q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3];
}

function qSquare(q) {
const x = q[0] * q[0] - q[1] * q[1] - q[2] * q[2] - q[3] * q[3];
const y = 2.0 * q[0] * q[1];
const z = 2.0 * q[0] * q[2];
const w = 2.0 * q[0] * q[3];
return [x, y, z, w];
}

function qCube(q) {
const qx2 = q[0] * q[0];
const qy2 = q[1] * q[1];
const qz2 = q[2] * q[2];
const qw2 = q[3] * q[3];

const x = q[0] * (qx2 - 3.0 * qy2 - 3.0 * qz2 - 3.0 * qw2);
const y = q[1] * (3.0 * qx2 - qy2 - qz2 - qw2);
const z = q[2] * (3.0 * qx2 - qy2 - qz2 - qw2);
const w = q[3] * (3.0 * qx2 - qy2 - qz2 - qw2);

return [x, y, z, w];
}

function sdFractal(p3)
{
const q3 = mul3(p3, 0.2);

var z4 = [q3[0], -q3[1], q3[2], 0];
var dz2 = 1.0;
var m2  = 0;
var i   = 0;
var o   = 1000;

for (i=0; i<iterations; i++) {
if (fractal) {
// z' = 3zΒ² -> |z'|Β² = 9|zΒ²|Β²
dz2 *= 9 * qLength2(qSquare(z4));
// z = zΒ³ + c
z4 = sum4(qCube(z4), C);
} else {
// z' = 2z
dz2 *= 4 * qLength2(z4);
// z = zΒ² + c
z4 = sum4(qSquare(z4), C);
}

m2 = qLength2(z4);

// Orbit trap
o = Math.min(o, length3([z4[0] - 0.45, z4[2] - 0.55, 0]) - .05);

if (m2 > 256) break;
}

// sdf(z) = log|z|Β·|z|/|dz| : https://iquilezles.org/www/articles/distancefractals/distancefractals.htm
var d = 0.25 * Math.log(m2) * Math.sqrt(m2 / dz2);

// Cut
if (cut) d = Math.max(d, -q3[2]);

// Trap
if (trap == 1) d = Math.min(d, o);
if (trap == 2) d = o;

return [d, i];
}

const MAX_STEPS = 10000;
const MAX_DIST = 100;
const SURF_DIST = .0001;

function RayMarch(ro3, rd3)
{
var dO = 0;
var mat = -1;

for (var i = 0; i < MAX_STEPS; i++) {
var p3 = add3(ro3, mul3(rd3, dO));
var dS;
[dS, mat] = sdFractal(p3);
dO += dS;
if (dO > MAX_DIST) return [MAX_DIST, -1];
if (Math.abs(dS)<SURF_DIST) break;
}

return [dO, mat];
}

const round_clip = 1;

// p: 2D point in -100 to 100 range
function getFractal(in_p2) {
if (Math.hypot(in_p2[0], in_p2[1]) > 95) return -1;

// Ray direction
var ro3, rd3;
[ro3, rd3] = getRay(in_p2);

// Get distance to intersection
var dist, mat;
[dist, mat] = RayMarch(ro3, rd3);

if (dist >= MAX_DIST) return -1;

return mat;
}

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; }

function length2(v2) { return Math.sqrt(v2[0]*v2[0] + v2[1]*v2[1]); }
function length3(v3) { return Math.sqrt(v3[0]*v3[0] + v3[1]*v3[1] + v3[2]*v3[2]); }
function normalize3(v3) { var l = length3(v3); if (l<0.00001) l=1; return [v3[0]/l, v3[1]/l, v3[2]/l]; }
function mul2(a2, f) { return [a2[0]*f, a2[1]*f]; }
function mul3(a3, f) { return [a3[0]*f, a3[1]*f, a3[2]*f]; }
function add3(a3, b3) { return [a3[0]+b3[0], a3[1]+b3[1], a3[2]+b3[2]]; }
function sub3(a3, b3) { return [a3[0]-b3[0], a3[1]-b3[1], a3[2]-b3[2]]; }
function cross3(a3, b3) { return [ a3[1] * b3[2] - a3[2] * b3[1], a3[2] * b3[0] - a3[0] * b3[2], a3[0] * b3[1] - a3[1] * b3[0] ]; }
function fract3(v3) { return [ v3[0] - Math.floor(v3[0]), v3[1] - Math.floor(v3[1]), v3[2] - Math.floor(v3[2]) ]; }
function abs3(v3) { return [ Math.abs(v3[0]), Math.abs(v3[1]), Math.abs(v3[2]) ]; }
function dot3(a3, b3) { return a3[0] * b3[0] + a3[1] * b3[1] + a3[2] * b3[2]; }
function floor3(v3) { return [ Math.floor(v3[0]), Math.floor(v3[1]), Math.floor(v3[2]) ]; }
function mul_mat2(v2, m22) { return [ v2[0] * m22[0] + v2[1] * m22[1], v2[0] * m22[2] + v2[1] * m22[3] ]; }
function rotation_mat22(a) { var s = Math.sin(a); var c = Math.cos(a); return [c, -s, s, c]; }
function clamp(x, min, max) { return Math.min(max, Math.max(min, x)); }
function smoothstep(edge0, edge1, x) { x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); return x * x * (3 - 2 * x); }
function tri3(x3) { return abs3(sub3(fract3(x3), .5)); }
function mix(x, y, a) { return x * (1-a) + y * a; }
function mix4(x, y, a) { return [ x[0] * (1-a) + y[0] * a, x[1] * (1-a) + y[1] * a, x[2] * (1-a) + y[2] * a, x[3] * (1-a) + y[3] * a ]; }
function smin(a, b , s) { var h = clamp( 0.5 + 0.5*(b-a)/s, 0. , 1.); return mix(b, a, h) - h*(1.0-h)*s; } // IQ's smooth minium function.

//////////////////////////////////////////
// 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;
}

```