<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>"Biomorph" Fractal</title>
<style>
html,
body,
canvas {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<!--
© Adam Murray 2024
https://adammurray.link/
Creative Commons License
Attribution-NonCommercial-ShareAlike 4.0 International
https://creativecommons.org/licenses/by-nc-sa/4.0/
-->
<script id="vertexShader" type="x-shader/x-vertex">
#version 300 es
in vec4 vertexPosition;
void main() { // no-op vertex shader
gl_Position = vertexPosition;
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
#version 300 es
precision highp float;
uniform vec2 canvasSize;
uniform float time; // in seconds
out vec4 fragColor;
const float MAX_ITERATIONS = 12.;
const float QUALITY = 3.; // anti-aliasing amount
const float E = 2.718281828459045;
vec2 cartesianToPolar(vec2 z) {
float radius = length(z);
// atan is undefined on some platforms when the args are 0:
float angle = (z.x == 0. && z.y == 0.) ? 0. : atan(z.y, z.x);
return vec2(radius, angle);
}
vec2 polarToCartesian(float radius, float angle) {
return vec2(radius * cos(angle), radius * sin(angle));
}
vec2 complexPow(vec2 z, float exp) { // real exponent
// If z == (0,0), then in the math below, pow(0,-negativeNumber) will be a
// divide-by-zero error. Thus we special case (0,0)^anything is (0,0)
// This gives us support for negative exponents.
if (z.x == 0. && z.y == 0.) return z;
vec2 polar = cartesianToPolar(z);
return polarToCartesian(pow(polar.x, exp), polar.y * exp);
}
vec2 complexPow(vec2 z, vec2 exp) { // complex exponent
// If z == (0,0), then in the math below, log(r) will be -Infinity, which
// results in an invalid return value. Thus we special case (0,0)^anything is (0,0)
if (z.x == 0. && z.y == 0.) return z;
// https://en.wikipedia.org/wiki/Exponentiation#Computation
vec2 polar = cartesianToPolar(z);
float r = polar.x;
float theta = polar.y;
float c = exp.x;
float d = exp.y;
return polarToCartesian(
pow(r, c) * pow(E, -d*theta),
d * log(r) + c * theta
);
}
vec3 hsl2rgb(float h, float s, float l) {
float hp = 6. * mod(h,1.);
float c = s - s * abs(2.*l - 1.);
float x = c - c * abs(mod(hp,2.) - 1.);
float m = l - c/2.;
if (hp <= 1.) return vec3(c,x,0) + m;
else if (hp <= 2.) return vec3(x,c,0) + m;
else if (hp <= 3.) return vec3(0,c,x) + m;
else if (hp <= 4.) return vec3(0,x,c) + m;
else if (hp <= 5.) return vec3(x,0,c) + m;
else if (hp <= 6.) return vec3(c,0,x) + m;
else return vec3(0,0,0);
}
vec3 draw(vec2 c) {
float t = time;
vec2 z = vec2(0,0);
float zExponent = 70. - 65. * sqrt((cos(.02*t+.7) + 1.)/2.);
float len = 0.;
float i;
for(i=0.; i<MAX_ITERATIONS; i++) {
z = complexPow(z, z) + complexPow(z, zExponent) + c;
len = length(z);
if (len > 16.) break;
}
float hue = (i - log2(log(log(len)))) / MAX_ITERATIONS;
vec3 color1 = hsl2rgb(hue * 0.5, 1., 0.4);
vec3 color2 = hsl2rgb((hue * 0.5) + 0.3 * cos(t), 0.8, 0.7);
vec3 fillColor = vec3(.06, .03, 0.);
/* // simpler, non-smoothed version
if (abs(z.x) < 100.) {
return color1;
} else if(abs(z.y) < 100.) {
return color2;
} else {
return fillColor;
}
*/ // smoothed and animated version:
float xDistance = smoothstep( 5.*(cos(t*.92)+1.), 40. + 25.*sin(t*.78), abs(z.x));
float yDistance = smoothstep(-5.*(cos(t*.83)+1.), 40. - 25.*sin(t*.87), abs(z.y));
vec3 color = mix(color1, color2, clamp(0., 1., .5*abs(xDistance - yDistance)));
return mix(
color,
fillColor,
min(xDistance, yDistance)
);
}
void main() {
vec3 offset = vec3(-0.1, -0.85, 0.18);
vec3 color = vec3(0,0,0);
float samples = 0.;
float subpixel = 1./float(QUALITY);
for (float x=0.; x<1.; x+=subpixel) {
for (float y=0.; y<1.; y+=subpixel) {
vec2 fragCoord = gl_FragCoord.xy + vec2(x,y);
vec2 coord = (2.*fragCoord - canvasSize)/min(canvasSize.x, canvasSize.y);
vec2 c = coord * offset.z - offset.xy;
color += draw(c);
samples++;
}
}
fragColor = vec4(color/samples, 1);
}
</script>
<script>
const gl = canvas.getContext("webgl2");
if (!gl) {
document.body.innerHTML = '<h2>Error: WebGL2 is <a href="https://get.webgl.org/webgl2/">not supported by your browser</a></h2>';
throw "WebGL2 not supported";
}
function createShader(shaderType, sourceCode) {
const shader = gl.createShader(shaderType);
gl.shaderSource(shader, sourceCode);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw gl.getShaderInfoLog(shader);
return shader;
}
const program = gl.createProgram();
gl.attachShader(program, createShader(gl.VERTEX_SHADER, vertexShader.textContent.trim()));
gl.attachShader(program, createShader(gl.FRAGMENT_SHADER, fragmentShader.textContent.trim()));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw gl.getProgramInfoLog(program);
gl.useProgram(program);
const vertices = [[-1, -1], [1, -1], [-1, 1], [1, 1]];
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices.flat()), gl.STATIC_DRAW);
const vertexPosition = gl.getAttribLocation(program, "vertexPosition");
gl.enableVertexAttribArray(vertexPosition);
gl.vertexAttribPointer(vertexPosition, 2, gl.FLOAT, false, 0, 0);
const canvasSizeUniform = gl.getUniformLocation(program, 'canvasSize');
const timeUniform = gl.getUniformLocation(program, 'time');
function draw() {
const width = canvas.clientWidth;
const height = canvas.clientHeight;
canvas.width = width;
canvas.height = height;
gl.viewport(0, 0, width, height);
gl.uniform2f(canvasSizeUniform, width, height);
gl.uniform1f(timeUniform, performance.now() / 1000);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length);
requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>