<!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>