Published
Edited
Apr 29, 2022
3 forks
47 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
params
Insert cell
function getSettings() {
const _aim = mutable aim;
return {
imax: +imax,
squareRadius: Math.log(squareRadius) / Math.log(20),
colorA,
colorB,
colorC,
x: _aim.x.arr,
y: _aim.y.arr,
hx: _aim.hx.arr,
hy: _aim.hy.arr,
phi: _aim.phi
};
}
Insert cell
initialAim = ({
x: new Double(params.x || -0.75),
y: new Double(params.y || 0),
hx: new Double(params.hx || 1.25),
hy: new Double(params.hy || 1.15),
phi: params.phi || 0
})
Insert cell
mutable aim = initialAim
Insert cell
setupControls = {
let wheelAccum = 0;
let dragPoint = null;
let cancelClick = false;

function wheelZoom(pos) {
const factor = Math.pow(2, wheelAccum / 200);
pos.x = pos.x.add(mutable aim.x.sub(pos.x).mul(factor));
pos.y = pos.y.add(mutable aim.y.sub(pos.y).mul(factor));
simpleZoom(pos, factor);
wheelAccum = 0;
}

canvas.onclick = e => {
if (!cancelClick) {
simpleZoom(getPos(e), 1 / 2);
}
cancelClick = false;
};

canvas.onmousedown = e => {
dragPoint = getPos(e);
};

canvas.onmousemove = e => {
if (dragPoint) {
cancelClick = true;
const pos = getPos(e);
simpleMove({
x: mutable aim.x.sub(pos.x.sub(dragPoint.x)),
y: mutable aim.y.sub(pos.y.sub(dragPoint.y))
});
}
};

canvas.onmouseup = e => {
dragPoint = null;
};

canvas.onwheel = e => {
e.preventDefault();
if (wheelAccum == 0) {
setTimeout(
() => wheelZoom(getPos(e)),
Math.max(mutable renderTime, minMsPerFrame)
);
}
wheelAccum += e.deltaY;
};

draw();

return true;
}
Insert cell
getPos = {
let prevPhi, prevCos, prevSin;

return function getPos(e) {
const _aim = mutable aim;
const x = e.offsetX || e.layerX;
const y = e.offsetY || e.layerY;
if (prevPhi != _aim.phi) {
prevPhi = _aim.phi;
prevCos = Math.cos(_aim.phi);
prevSin = Math.sin(_aim.phi);
}
const dx = _aim.hx.mul((2 * x) / width - 1);
const dy = _aim.hy.mul((2 * y) / width - 1);
return {
x: _aim.x.add(dx.mul(prevCos).add(dy.mul(prevSin))),
y: _aim.y.add(dx.mul(prevSin).sub(dy.mul(prevCos))),
px: x,
py: y
};
};
}
Insert cell
function simpleMove(pos) {
mutable aim = { ...mutable aim, x: pos.x, y: pos.y };
draw();
}
Insert cell
function simpleZoom(pos, factor) {
const _aim = mutable aim;
mutable aim = {
..._aim,
x: pos.x,
y: pos.y,
hx: _aim.hx.mul(factor),
hy: _aim.hy.mul(factor)
};
draw();
}
Insert cell
minMsPerFrame = 16
Insert cell
mutable renderTime = 0
Insert cell
programGetter = (() => {
let savedGl = null,
savedVert = null,
savedFrag = null,
savedProgramInfo = null;
return (gl, vert, frag) => {
if (gl != savedGl || vert != savedVert || savedFrag != frag()) {
savedGl = gl;
savedVert = vert;
savedFrag = frag();
savedProgramInfo = twgl.createProgramInfo(savedGl, [vert, savedFrag]);
}
return savedProgramInfo;
};
})()
Insert cell
gl = {
const glMandel = twgl.getWebGLContext(canvas, {
antialias: false,
depth: false
});
twgl.addExtensionsToContext(glMandel);
twgl.resizeCanvasToDisplaySize(glMandel.canvas, window.devicePixelRatio || 1);
glMandel.viewport(0, 0, glMandel.canvas.width, glMandel.canvas.height);
return glMandel;
}
Insert cell
function calcOrbit(c, c0, returnIteration) {
let x = c0 ? c0.x : c.x,
y = c0 ? c0.y : c.y;
let xx = x.sqr(),
yy = y.sqr(),
xy = x.mul(y);
let dx = Double.One,
dy = Double.Zero,
temp;
let i,
orbit = [x.toNumber(), y.toNumber(), dx.toNumber(), dy.toNumber()];
for (i = 1; i < imax && xx.add(yy).lt(squareRadius); i++) {
temp = x
.mul(dx)
.sub(y.mul(dy))
.mul(2)
.add(1);
dy = x
.mul(dy)
.add(y.mul(dx))
.mul(2);
dx = temp;
x = xx.sub(yy).add(c.x);
y = xy.add(xy).add(c.y);
xx = x.sqr();
yy = y.sqr();
xy = x.mul(y);
if (!returnIteration) {
orbit.push(x.toNumber());
orbit.push(y.toNumber());
orbit.push(dx.toNumber());
orbit.push(dy.toNumber());
}
}
return returnIteration ? i : orbit;
}
Insert cell
function searchOrigin() {
let repeat = 15,
n = 12,
m = 3;
let z = {},
zbest = {},
newAim = { ...mutable aim },
f,
fbest = -Infinity;
for (let k = 0; k < repeat; k++) {
for (let i = 0; i <= n; i++) {
for (let j = 0; j <= n; j++) {
z.x = newAim.x.add(newAim.hx.mul((2 * i) / n - 1));
z.y = newAim.y.add(newAim.hy.mul((2 * j) / n - 1));
f = calcOrbit(z, null, true);
if (f == imax) {
return z;
} else if (f > fbest) {
zbest = { ...zbest, ...z };
fbest = f;
}
}
}
newAim = {
...newAim,
...zbest,
hx: newAim.hx.div(m / n),
hy: newAim.hy.div(m / n)
};
}
return zbest;
}
Insert cell
programInfo = {
const programInfo = programGetter(gl, vert, frag);
gl.useProgram(programInfo.program);
return programInfo;
}
Insert cell
function draw() {
const _aim = mutable aim;
try {
const start = performance.now();
const origin = searchOrigin(0);
const orbit = calcOrbit(origin);
const texsize = Math.ceil(Math.sqrt(orbit.length / 4));
const orbittex = twgl.createTexture(gl, {
format: gl.RGBA,
type: gl.FLOAT,
minMag: gl.NEAREST,
wrap: gl.CLAMP_TO_EDGE,
src: orbit
});

const attribs = {
a_position: { data: [1, 1, 1, -1, -1, -1, -1, 1], numComponents: 2 }
};
const bufferInfo = twgl.createBufferInfoFromArrays(gl, attribs);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);

const uniforms = {
rotator: [Math.sin(_aim.phi), Math.cos(_aim.phi)],
center: [
_aim.x.sub(origin.x).toNumber(),
_aim.y.sub(origin.y).toNumber()
],
size: [_aim.hx.toNumber(), _aim.hy.toNumber()],
pixelsize: [
_aim.hx.toNumber() / gl.canvas.width,
_aim.hy.toNumber() / gl.canvas.height
],
texsize: texsize,
orbittex: orbittex
};
twgl.setUniforms(programInfo, uniforms);

twgl.drawBufferInfo(gl, bufferInfo, gl.TRIANGLE_FAN);
mutable renderTime = Math.round(performance.now() - start);
} catch (error) {
console.error(error);
}
}
Insert cell
vert = `
precision highp float;

attribute vec2 a_position;
uniform vec2 rotator;
uniform vec2 center;
uniform vec2 size;
varying vec2 delta;

void main() {

/* window coordinates with new origin in complex space */
vec2 z = size * a_position;
delta = center + vec2(z.x * rotator.y - z.y * rotator.x, dot(z, rotator));

gl_Position = vec4(a_position, 0.0, 1.0);
}`
Insert cell
frag = () => `
precision highp float;

#define imax ${imax}
#define square_radius ${squareRadius}.
const float logLogRR = log2(log2(square_radius));
varying vec2 delta;
varying vec2 texcoord;

uniform vec2 rotator;
uniform vec2 size;
uniform vec2 pixelsize;
uniform float texsize;
uniform sampler2D orbittex;

vec4 unpackOrbit(int i) {
float fi = float(i);
vec2 texcoord = vec2(mod(fi, texsize), floor(fi / texsize)) / texsize;
return texture2D(orbittex, texcoord);
}

float interpolate(float s, float s1, float s2, float s3, float d) {
float d2 = d * d, d3 = d * d2;
return 0.5 * (s * (d3 - d2) + s1 * (d + 4.*d2 - 3.*d3) + s2 * (2. - 5.*d2 + 3.*d3) + s3 * (-d + 2.*d2 - d3));
}

struct result {
float time;
float zz;
float dzdz;
float stripe;
float argZ;
};

/* fractal calculator with perturbation theory for mandelbrot & julia set */
result calculator(vec2 AA) {
float u = delta.x + AA.x, v = delta.y + AA.y;
float zz, time, temp, du = 0., dv = 0.;
float stripe, s1, s2, s3;
vec2 z, dz, O, dO;

for (int i = 0; i < imax; i++) {
/* Recall global coordinates: Z = O + W, Z' = O' + W' */
vec4 values = unpackOrbit(i);
O = values.xy;
dO = values.zw;
z = O + vec2(u, v);
dz = dO + vec2(du, dv);
zz = dot(z, z);

/* Calc derivative: dW'(u,v) -> 2 * (O' * W + Z * W') */
temp = 2. * (dO.x * u - dO.y * v + z.x * du - z.y * dv);
dv = 2. * (dO.x * v + dO.y * u + z.x * dv + z.y * du);
du = temp;

/* Next step in the iterative process: W(u,v) -> W^2 + 2 * O * W + <delta> */
temp = u * u - v * v + 2. * (u * O.x - v * O.y);
v = u * v + u * v + 2. * (v * O.x + u * O.y);
u = temp;
u += delta.x;
v += delta.y;
/* Stripe average, a color algo based on statistcs */
stripe += z.x * z.y / zz * step(0.0, time);
s3 = s2; s2 = s1; s1 = stripe;

/* Loop in webgl1 */
time += 1.;
if (zz > square_radius) { break; }
}

time += clamp(1.0 + logLogRR - log2(log2(zz)), 0., 1.);
stripe = interpolate(stripe, s1, s2, s3, fract(time));
return result(time, zz, dot(dz,dz), stripe, atan(z.y, z.x));
}

void main() {
/* Get result */
result R = calculator(vec2(0));

/* DEM (Distance Estimation) = 2 * |Z / Z'| * ln(|Z|) */
float dem = sqrt(R.zz / R.dzdz) * log2(R.zz);
float dem_weight = 800. / min(size.x, size.y);

/* Final coloring */
vec3 color;
color += 0.7 + 2.5 * (R.stripe / clamp(R.time, 0., 200.)) * (1. - 0.6 * step(float(imax), 1. + R.time));
color = 0.5 + 0.5 * sin(color + vec3(${colorA}, ${colorB}, ${colorC}) + 50.0 * R.time / float(imax));

gl_FragColor = vec4(color, 1.);
}`
Insert cell
Double = require("double.js@0.0.8/dist/double.iife.min.js").catch(
() => window["Double"]
)
Insert cell
twgl = require("https://cdn.jsdelivr.net/npm/twgl-base.js@4.8.2/dist/4.x/twgl.min.js")
Insert cell
params = {
const searchParams = new URLSearchParams(html`<a href>`.search);
const result = {};
for (let key of searchParams.keys()) {
try {
result[key] = JSON.parse(searchParams.get(key));
} catch (error) {
// Unknown param
}
}
return result;
}
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more