<!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>Black hole</title>
  <style>
    html,
    body,
    canvas {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
    }
  </style>
</head>
<body>
  <canvas id="canvas"></canvas>
  
  <script id="vertex" type="x-shader/x-vertex">
    #version 300 es
  
    uniform float numVertices;
    uniform float time;
    uniform vec2 canvasSize;
  
    const float TWO_PI_OVER_PHI = 6.2831853072 / 1.61803398875;
  
    void main() {
      // use larger point sizes for larger canvases:
      float minDim = min(canvasSize.x, canvasSize.y);
      gl_PointSize = floor(minDim/200.) + 1.;
  
      float offset = (cos(time/10.) - 1.) * numVertices/2.;
      float i = float(gl_VertexID) + offset; // gl_VertexID == vertex index
  
      if (i < 0.) {
        gl_Position = vec4(2, 2, 2, 1); // draw outside clip space
        return;
      }
  
      vec2 p = vec2(sqrt(i/(numVertices+offset)), i * TWO_PI_OVER_PHI);
      p.y -= offset * 3.88325; // compensate for rotation
      p = vec2(p.x * cos(p.y), p.x * sin(p.y)); // polar (radius, angle) to cartesian
  
      // compensate for aspect ratio:
      if (canvasSize.x > canvasSize.y) {
        p.x *= canvasSize.y/canvasSize.x;
      } else {
        p.y *= canvasSize.x/canvasSize.y;
      }
  
      gl_Position = vec4(p, 0, 1);
    }
  </script>
  
  <script id="fragment" type="x-shader/x-fragment">
    #version 300 es
    precision highp float;
  
    uniform vec2 canvasSize;
    out vec4 fragColor;
  
    void main() {
      vec2 coord = (2.*gl_FragCoord.xy - canvasSize)/min(canvasSize.x, canvasSize.y);
      float c = sqrt(length(coord));
      fragColor = vec4(c, c, c, 1);
    }
  </script>
  
  <script>
    const canvas = document.querySelector('canvas');
    const vertexShader = document.querySelector('script[type="x-shader/x-vertex"]');
    const fragmentShader = document.querySelector('script[type="x-shader/x-fragment"]');
  
    const gl = canvas.getContext("webgl2");
    if (!gl) {
      document.body.innerHTML ='<p>Error: WebGL2 is <a href="https://get.webgl.org/webgl2/">not supported by your browser</a></p>';
      throw "WebGL2 not supported";
    }
  
    function createShader(shaderType, sourceCode) {
      const shader = gl.createShader(shaderType);
      gl.shaderSource(shader, sourceCode.trim());
      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 canvasSizeUniform = gl.getUniformLocation(program, 'canvasSize');
    const timeUniform = gl.getUniformLocation(program, 'time');
  
    const numVertices = 1000;
    const numVerticesUniform = gl.getUniformLocation(program, 'numVertices');
    gl.uniform1f(numVerticesUniform, numVertices);
  
    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.);
  
      // Since we are determining all vertex positions inside the vertex
      // shader, we don't actually need to pass in a list of vertices.
      // Instead we just tell it how many vertices to draw and rely on the
      // vertex index (gl_VertexID) to calculate position.
      gl.drawArrays(gl.POINTS, 0, numVertices);
  
      requestAnimationFrame(draw);
    }
    draw();
  </script>
  
  <style>
    canvas {
      background-color: black;
    }
  </style>

  
</body>
<!--
  © Adam Murray 2022
  https://adammurray.blog/

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