
<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      canvas {
        position: absolute;
        margin: 0;
        top: 50%;
        left: 50%;
        -ms-transform: translate(-50%, -50%);
        transform: translate(-50%, -50%);
        display: block;
      }
      html, body {
        margin: auto auto;
        padding: 0;
      }
    </style>
    <meta charset="utf-8" />
  </head>
  <body>
    <canvas id = "canvas" width = "400" height = "400"> No HTML5 support. </canvas>
    <script id = "volume renderer">       
      const vWidth = $INSERT_VOLUME_WIDTH_HERE;
      const vDepth = $INSERT_VOLUME_DEPTH_HERE;
      const vHeight  = $INSERT_VOLUME_HEIGHT_HERE;
      const frames = $INSERT_FRAMES_HERE;
      const totalFrames = $INSERT_FRAME_COUNT_HERE;
      let framesPerSecond = $INSERT_FPS_HERE;
      let slice, nextSlice;
      let slice1DUInt8, nextSlice1DUInt8;
      
      function InitData(){
        volumeLength = vWidth * vDepth * vHeight;
        textureLength = sqrFloor(volumeLength);
        
        slice1DUInt8 = new Uint8Array(4*textureLength*textureLength);
        nextSlice1DUInt8 = new Uint8Array(4*textureLength*textureLength);

        InitGL();
        newSlice();
      };
      
      let gl;
      let animationTime, animationSpeed, lastTime, timeDelta;
      let theta, phi, radius, origin;
      let brightnessScale, threshold;
      let sliceTex, nextSliceTex, _lastFrameVal, textureLength, volumeLength;
      
      let vertexShaderTxt = 
      ["precision highp float;",
      "",
      "attribute vec2 vertPosition, vertUV;",
      "",
      "varying vec2 fragPos, fragUV;",
      "",
      "void main()",
      "{",
      "  fragPos = vertPosition;",
      "  fragUV = vertUV;",
      "  ",
      "  gl_Position = vec4(vertPosition, 0.0, 1.0);",
      "}"].join(" ");
      
      let fragmentShaderTxt =
      ["precision highp float;",
      "",
      "varying vec2 fragPos, fragUV;",
      "",
      "uniform sampler2D uVolumeA, uVolumeB;",
      "uniform float vWidth, vHeight, vDepth, tSize;",
      "uniform float uRadius, uTheta, uPhi, uEpsilon, fov;",
      "uniform float uThreshold, uScale, uTimeFrameDiff;",
      "uniform vec3 uOrigin;",
      "",
      "vec2 Index2UV(int index)",
      "{",
      "  int j = index / int(tSize);",
      "  int i = index - j*int(tSize);",
      "  ",
      "  return vec2(i,j)/tSize;",
      "}",
      "",
      "int World2Index(vec3 ray)",
      "{",    
      "  return int(floor(ray.x)) + int(floor(ray.y))*int(floor(vWidth)) + int(floor(ray.z))*int(floor(vWidth*vDepth));",
      "}",
      "",
      "bool InBounds(vec3 ray)",
      "{",
      "  if(ray.x < 0.0 || ray.x >= vWidth) return false;",
      "  if(ray.y < 0.0 || ray.y >= vDepth) return false;",
      "  if(ray.z < 0.0 || ray.z >= vHeight) return false;",
      "",
      "  return true;",
      "}",
      "",
      "vec3 DistanceFrom(vec3 p, vec3 d)",
      "{",
      "  vec3 DF;",
      "  float smallNum = 0.0001;",
      "  ",
      "  DF.x = (d.x>0.0 ? floor(p.x+1.0)-p.x+smallNum : (d.x<0.0 ? p.x-ceil(p.x-1.0)+smallNum : 2.0));",
      "  DF.y = (d.y>0.0 ? floor(p.y+1.0)-p.y+smallNum : (d.y<0.0 ? p.y-ceil(p.y-1.0)+smallNum : 2.0));",
      "  DF.z = (d.z>0.0 ? floor(p.z+1.0)-p.z+smallNum : (d.z<0.0 ? p.z-ceil(p.z-1.0)+smallNum : 2.0));",
      "  ",
      "  return DF;",
      "}",
      "",
      "vec3 TimeFrom(vec3 DF, vec3 d)",
      "{",
      "  vec3 t;",
      "  ",
      "  t.x = d.x != 0.0 ? abs(DF.x/d.x) : 1000000.0;",
      "  t.y = d.y != 0.0 ? abs(DF.y/d.y) : 1000000.0;",
      "  t.z = d.z != 0.0 ? abs(DF.z/d.z) : 1000000.0;",
      "  ",
      "  return t;",
      "}",
      "vec3 RayDelta(vec3 d, vec3 t, vec3 DF)",
      "{",
      "  vec3 dr;",
      "",
      "  dr.x = t.x<t.y&&t.x<t.z ? DF.x*d.x/abs(d.x) : (t.y<t.z&&t.y<t.x ? DF.y*d.x/abs(d.y) : DF.z*d.x/abs(d.z));",
      "  dr.y = t.x<t.y&&t.x<t.z ? DF.x*d.y/abs(d.x) : (t.y<t.z&&t.y<t.x ? DF.y*d.y/abs(d.y) : DF.z*d.y/abs(d.z));",
      "  dr.z = t.x<t.y&&t.x<t.z ? DF.x*d.z/abs(d.x) : (t.y<t.z&&t.y<t.x ? DF.y*d.z/abs(d.y) : DF.z*d.z/abs(d.z));",
      "",
      "  return dr;",
      "}",
      "",
      "vec3 RotateTowardsOrigin(vec3 dir)",
      "{",
      "  float x = dir.x*cos(uTheta)*cos(uPhi) - dir.y*sin(uTheta) + dir.z*cos(uTheta)*sin(uPhi);",
      "  float y = dir.x*sin(uTheta)*cos(uPhi) + dir.y*cos(uTheta) + dir.z*sin(uTheta)*sin(uPhi);",
      "  float z = -dir.x*sin(uPhi) + dir.z*cos(uPhi);",
      "",
      "  return vec3(x,y,z);",
      "}",
      "",
      "vec3 RotateAroundVolumeOrigin(vec3 p)",
      "{",
      "  return uOrigin + RotateTowardsOrigin(p);",
      "}",
      "",
      "vec4 SampleTimeFrame(vec3 rayPos, vec3 dP)",
      "{",
      "    if(InBounds(rayPos))",
      "    {",
      "      return length(dP)*( ",
      "        (1.0-uTimeFrameDiff)* texture2D(uVolumeA,Index2UV(World2Index(rayPos))) + ",
      "        uTimeFrameDiff*       texture2D(uVolumeB,Index2UV(World2Index(rayPos))) );",
      "    }",
      "    ",
      "    return vec4(0,0,0,0);",
      "}",
      "",
      "vec4 RayTrace(vec3 origin, vec3 rayDir)",
      "{",
      "  vec4 color = vec4(0,0,0,0);",
      "  vec3 rayPos = origin, dP, DF, t;",
      "  ",
      "  for(int i = 0; i < 150; i++)",
      "  {",
      "    DF = DistanceFrom(rayPos, rayDir);",
      "    t = TimeFrom(DF, rayDir);",
      "    dP = RayDelta(rayDir, t, DF);",
      "    color += SampleTimeFrame(rayPos, dP);",
      "    ",
      "    ",
      "    rayPos += RayDelta(rayDir, t, DF);",
      "    if(length(origin-rayPos) >= 100.0) break;",
      "  }",
      "",
      "  color = min(vec4(1.0,1.0,1.0,1.0),uScale*100.0*vec4(color.xyz,0.0)/(length(rayPos-origin)*uThreshold));",
      "  color.w = 1.0;",
      "  ",
      "  return color;",
      "}",
      "",
      "void main()",
      "{",
      "  vec3 pos = RotateAroundVolumeOrigin(vec3(0,0,uRadius));",
      "  vec3 rayDir = RotateTowardsOrigin(normalize(vec3(fragPos.x,fragPos.y,-fov)));",
      "  ",
      "  gl_FragColor = RayTrace(pos, rayDir);",
      "}"].join(" ");

      function sqrFloor(x){
        let y = 2;
        while(Math.ceil(x/(y*y) > 1)) y *= 2;

        return y;
      };

      function newSlice(){
        let v;
        let cFIndex = Math.floor(animationTime) * volumeLength;
        let nextCFIndex = (Math.floor(animationTime + 1) * volumeLength) % frames.length;
        
        slice = frames.slice(cFIndex,cFIndex+volumeLength);
        nextSlice = frames.slice(nextCFIndex,nextCFIndex+volumeLength);

        slice.forEach((element,index) => { 
          v = frames[cFIndex + index];
          
          slice1DUInt8[4*index+0] = v;
          slice1DUInt8[4*index+1] = v;
          slice1DUInt8[4*index+2] = v;
          slice1DUInt8[4*index+3] = v;
        });
        
        nextSlice.forEach((element,index) => { 
          v = frames[nextCFIndex + index];
          
          nextSlice1DUInt8[4*index+0] = v;
          nextSlice1DUInt8[4*index+1] = v;
          nextSlice1DUInt8[4*index+2] = v;
          nextSlice1DUInt8[4*index+3] = v;
        });

        sliceTex = gl.createTexture();
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, sliceTex);

        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, textureLength, textureLength, 0, gl.RGBA, gl.UNSIGNED_BYTE, slice1DUInt8);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        
        nextSliceTex = gl.createTexture();
        gl.activeTexture(gl.TEXTURE1);
        gl.bindTexture(gl.TEXTURE_2D, nextSliceTex);

        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, textureLength, textureLength, 0, gl.RGBA, gl.UNSIGNED_BYTE, nextSlice1DUInt8);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
      };

      function updateFrame(){
        timeDelta = (Date.now() - lastTime)/1000;
        lastTime = Date.now();
        animationTime = (animationTime + animationSpeed*timeDelta + totalFrames) % totalFrames;
        if(Math.floor(animationTime) != _lastFrameVal) newSlice();
        _lastFrameVal = Math.floor(animationTime);
      };

      function InitGL(){  
        let canvas = document.getElementById("canvas");
        gl = canvas.getContext("webgl");

        console.log("starting...");

        if(!gl) { 
          gl = canvas.getContext("expiremental-webgl"); 
          console.log("using expirimental-webgl");
        }
        if(!gl) { console.log("webgl is not supported :("); return; }      

        gl.clearColor(0.75,0.85,0.8,1.0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        let vertexShader = gl.createShader(gl.VERTEX_SHADER);
        let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

        gl.shaderSource(vertexShader,vertexShaderTxt);
        gl.shaderSource(fragmentShader,fragmentShaderTxt);

        gl.compileShader(vertexShader);
        if(!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)){
          console.error("ERROR compiling vertex shader!", gl.getShaderInfoLog(vertexShader));
          return;
        }

        gl.compileShader(fragmentShader);
        if(!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)){
          console.error("ERROR compiling fragment shader!", gl.getShaderInfoLog(fragmentShader));
          return;
        }

        let program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        if(!gl.getProgramParameter(program, gl.LINK_STATUS)){
          console.error("ERROR linking program!",gl.getProgramInfoLog(program));
          return;
        }

        //debug validate program
        gl.validateProgram(program);
        if(!gl.getProgramParameter(program, gl.VALIDATE_STATUS)){
          console.error("ERROR validating program!",gl.getProgramInfoLog(program));
          return;
        }

        gl.useProgram(program);
        
        let triangleVertices = 
        [//X   ,  Y     U   ,   V
          -1,   1,     0.0,  1.0,
          -1,  -1,     0.0,  0.0,
           1,  -1,     1.0,  0.0,
           1,   1,     1.0,  1.0];

        let triangleIndices = [0,1,2, 0,2,3];

        let triangleVertexBufferObject = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER,triangleVertexBufferObject);
        gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(triangleVertices), gl.STATIC_DRAW);

        let triangleIndexBufferObject = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, triangleIndexBufferObject);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(triangleIndices), gl.STATIC_DRAW);

        //set shader attributes
        let positionAttribLocation = gl.getAttribLocation(program, "vertPosition");
        gl.vertexAttribPointer(
          positionAttribLocation,
          2, //number of elements per attribute
          gl.FLOAT,
          gl.FALSE, //is normalized
          4*Float32Array.BYTES_PER_ELEMENT, //size of vertex
          0 );//offset

        let uvAttribLocation = gl.getAttribLocation(program, "vertUV");
        gl.vertexAttribPointer(
          uvAttribLocation,
          2, //number of elements per attribute
          gl.FLOAT,
          gl.FALSE, //is normalized
          4*Float32Array.BYTES_PER_ELEMENT,  //size of vertex
          2*Float32Array.BYTES_PER_ELEMENT );//offset

        gl.enableVertexAttribArray(positionAttribLocation);
        gl.enableVertexAttribArray(uvAttribLocation);

        //shader uniforms
        let volumeAUniformLocation = gl.getUniformLocation(program,"uVolumeA");
        let volumeBUniformLocation = gl.getUniformLocation(program,"uVolumeB");

        let uOriginUniformLocation = gl.getUniformLocation(program,"uOrigin");

        let tSizeUniformLocation = gl.getUniformLocation(program,"tSize");

        let vWidthUniformLocation = gl.getUniformLocation(program,"vWidth");
        let vHeightUniformLocation = gl.getUniformLocation(program,"vHeight");
        let vDepthUniformLocation = gl.getUniformLocation(program,"vDepth");
        
        let fovUniformLocation = gl.getUniformLocation(program,"fov");
        
        let radiusUniformLocation = gl.getUniformLocation(program,"uRadius");
        let thetaUniformLocation = gl.getUniformLocation(program,"uTheta");
        let phiUniformLocation = gl.getUniformLocation(program,"uPhi");
        let epsilonUniformLocation = gl.getUniformLocation(program,"uEpsilon");
        
        let scaleUniformLocation = gl.getUniformLocation(program,"uScale");
        let thresholdUniformLocation = gl.getUniformLocation(program,"uThreshold");
        let timeFrameDiffLocation = gl.getUniformLocation(program,"uTimeFrameDiff");
        
        animationTime = 0;
        animationSpeed = framesPerSecond; 
        lastTime = Date.now();
        
        brightnessScale = $INSERT_BRIGHTNESS_HERE;
        threshold = $INSERT_THRESHOLD_HERE;
        radius = $INSERT_RADIUS_HERE;
        theta = $INSERT_THETA_HERE;
        phi = $INSERT_PHI_HERE;
        origin = $INSERT_ORIGIN_HERE;
        
        let shiftKey = false;
        let ctrlKey = false;
        canvas.onwheel = function(event) {
          if(shiftKey){
            threshold = Math.max(Math.min(threshold + 0.01*event.deltaY, 5),0.0001);
          }
          else if(ctrlKey){
            brightnessScale = Math.max(Math.min(brightnessScale + 0.01*event.deltaY, 5),0.0001);
          }
          else{
            radius = Math.max(Math.min(radius + event.deltaY, 100),0);
          }
        };
        
        let mouseDown = false;
        let lastScreenX, lastScreenY;
        window.onmouseup = function(event) {
          mouseDown = false;
        }
        canvas.onmousedown = function(event) {
          lastScreenX = event.screenX;
          lastScreenY = event.screenY;
          mouseDown = true;
        }
        window.onmousemove = function(event) {
          if(!mouseDown) return;
          if(event.shiftKey){
            origin[0] -= 0.1*(event.screenX - lastScreenX);
            origin[1] += + 0.1*(event.screenY - lastScreenY);
          }
          else{
            theta -= 0.01*(event.screenY - lastScreenY);
            phi -= 0.01*(event.screenX - lastScreenX);
          }
          
          lastScreenX = event.screenX;
          lastScreenY = event.screenY;
        }
        window.onkeydown = function(event){
          if(event.shiftKey) shiftKey = true;
          else if(event.ctrlKey ) ctrlKey = true;
          else if(event.code == "Space"){
            if(animationSpeed == 0.0) animationSpeed = framesPerSecond;
            else if(animationSpeed == framesPerSecond) animationSpeed = 0.0;
          }
          else if(event.code == "KeyR"){
            radius = $INSERT_RADIUS_HERE;
            theta = $INSERT_THETA_HERE;
            phi = $INSERT_PHI_HERE;
          }
        }
        window.onkeyup = function(event){
          if(event.code == "ShiftLeft" || event.code == "ShiftRight") shiftKey = false;
          else if(event.code == "ControlLeft" || event.code == "ControlRight") ctrlKey = false;
        }

        //main loop
        let mainLoop = function(){        
          updateFrame();

          gl.uniform1i(volumeAUniformLocation, 0);
          gl.uniform1i(volumeBUniformLocation, 1);
          gl.uniform1f(tSizeUniformLocation, textureLength);

          gl.uniform1f(vWidthUniformLocation, vWidth);
          gl.uniform1f(vDepthUniformLocation, vDepth);
          gl.uniform1f(vHeightUniformLocation, vHeight);
          
          gl.uniform1f(radiusUniformLocation, radius);
          gl.uniform1f(thetaUniformLocation, theta);
          gl.uniform1f(phiUniformLocation, phi);
          gl.uniform1f(epsilonUniformLocation, 0);
          gl.uniform3fv(uOriginUniformLocation,new Float32Array(origin));
          
          gl.uniform1f(thresholdUniformLocation, threshold);
          gl.uniform1f(scaleUniformLocation, brightnessScale);
          gl.uniform1f(fovUniformLocation, 1);
          
          gl.uniform1f(timeFrameDiffLocation, animationTime - Math.floor(animationTime));
          
          gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
          gl.drawElements(gl.TRIANGLES, triangleIndices.length, gl.UNSIGNED_SHORT, 0); //draw a canvas!

          requestAnimationFrame(mainLoop);
        }
        requestAnimationFrame(mainLoop);

        console.log("webgl successfully initiated!");
      };
    
      InitData(); //run the program
    </script>
  </body>
</html>
