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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more