<!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>Mandelbrot Party</title>
  <style>
    html,
    body,
    canvas {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
    }
  </style>
</head>
<body>
  <canvas id="canvas"></canvas>
  
  <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; // t in seconds
    out vec4 fragColor;
  
    const float QUALITY = 2.; // anti-aliasing amount
    const float SPEED = 0.1;
  
    vec2 rotate(vec2 p, float angle) {
      return vec2(
        p.x*cos(angle) - p.y*sin(angle),
        p.x*sin(angle) + p.y*cos(angle)
      );
    }
  
    vec2 complexMultiply(vec2 a, vec2 b) {
      return vec2(a.x*b.x - a.y*b.y, a.x*b.y + a.y*b.x);
    }
  
    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               return vec3(c,0,x) + m;
    }
  
    vec3 draw(vec2 c, float iterations, float hueShift) {
      vec2 z = vec2(0,0);
      float i;
      for (i=0.; i < iterations && dot(z,z) < 16.0; i++) {
        z=complexMultiply(z,z) + c;
      }
  
      if (i>= iterations) {
        return vec3(0,0,0);
      } else {
        float hue = (i - log2(log(dot(z,z)))) / iterations;
        hue = mod(hue + hueShift, 1.);
        return hsl2rgb(hue, 0.7, 0.45);
      }
    }
  
    void main() {
      float t = SPEED*time;
      float zoom = 150.*pow(1.4 - cos(t), 5.);
      float angle = 10.*sin(t/11.07);
      float hueShift = t/23.;
      float iterations = 180. + 100.*sin(2.03*t);
      vec2 offset = vec2(
        -0.4822 - sin(t/3.11)/4000.,
        0.6141 + cos(t/1.43)/5500.
      );
  
      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 = rotate(coord/zoom, angle) + offset;
          color += draw(c, iterations, hueShift);
          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>
<!--
  © Adam Murray 2021
  https://adammurray.blog/

  Creative Commons License
  Attribution-NonCommercial-ShareAlike 4.0 International
  https://creativecommons.org/licenses/by-nc-sa/4.0/
-->