block by johnburnmurdoch 87f26ebff0609915e7c2fb9a535e36af

Animating 50,000 points with WebGL

Full Screen

index.html

<!doctype html>
<html class="no-js" lang="">
    <head>
        <meta charset="utf-8">
        <title>Animating 50,000 points with WebGL</title>
        <meta name="description" content="Animating 50,000 points with WebGL">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <script src=https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.0.0/pixi.js></script>
    </head>
    <body>
        <div id=graphic></div>
        <script type-'text/javascript'>
          let nDots = 50000,
            dots = [],
            stopAnimation = false,
            WebGL = true,
            PR = devicePixelRatio || 1,
            width = document.body.clientWidth,
            height = window.innerHeight-16,
            scaledWidth = width*PR,
            scaledHeight = height*PR,
            caontainer,
            renderer,
            circleTexture,
            canvas,
            context;

          function phyllotaxis(radius) {
            var theta = Math.PI * (3 - Math.sqrt(5));
            return function(i) {
              var r = radius * Math.sqrt(i), a = theta * i;
              return {
                x: width / 2 + r * Math.cos(a),
                y: height / 2 + r * Math.sin(a)
              };
            };
          }

          function gridLayout(points, gridWidth) {
            const pointHeight = pointWidth = ((33*0.16));
            const pointsPerRow = Math.floor(width / pointWidth);
            const numRows = points.length / pointsPerRow;

            return function(i){
              return {
                x: (pointWidth * (i % pointsPerRow)) + pointWidth,
                y: (pointHeight * Math.floor(i / pointsPerRow)) + pointHeight
              };
            };
          }

          if(WebGL == true){
            // Create the canvas
            renderer = new PIXI.autoDetectRenderer(width, height,
              {
                backgroundColor: 0xFFFFFF,
                // transparent: true,
                antialias: true,
                resolution: 1
              }
            );
            
            // Add to the document
            document.getElementById('graphic').appendChild(renderer.view);

            // Create the root of the scene graph
            container = new PIXI.Container(0xFFFFFF);

            renderer.render(container);

            // Create a texture to be used as a Sprite from a white circle png image
            circleTexture = new PIXI.Texture.fromImage("//johnburnmurdoch.github.io/images/circle.png");
          }

          if(WebGL == false){
            canvas = document.createElement('canvas');
            document.getElementById('graphic').appendChild(canvas);
            canvas.setAttribute('width', scaledWidth);
            canvas.setAttribute('height', scaledHeight);
            canvas.style.width = `${width}px`;
            canvas.style.height = `${height}px`;
            context = canvas.getContext("2d");
            context.scale(PR, PR);
            context.clearRect(0,0,scaledWidth,scaledHeight);
            // context.globalCompositeOperation = 'multiply';
          }

          // Get the visible size back to 1000 px
          // renderer.view.style.width = (width*0.5) + 'px';
          // renderer.view.style.height = (height*0.5) + 'px';

          const data = Array.from({length: nDots}, (d,i) => i).map(phyllotaxis(2.5)).map((d,i) => {
            let opacity = 1-(i/nDots);
            // let opacity = Math.random();
            return{
              x: d.x,
              y: d.y,
              opacity: opacity,
              color: WebGL ? ((240 << 16) + (0 << 8) + 0) : `hsla(0, 100%, 50%, ${opacity})`,
              // size: Math.random() * 0.15
              size: 0.11
            }
          });

          const data1 = Array.from({length: nDots}, (d,i) => i).map(gridLayout(width)).map(d => {
            let opacity = Math.random();
            return{
              x: d.x,
              y: d.y
            }
          });

          const data2 = Array.from({length: nDots}, d => {
            return{
              x: Math.random()*width,
              y: Math.random()*height
            }
          });

          function render() {

            data.forEach((d,i) => {
              // Set all characterstics of the circle
              let dot = new PIXI.Sprite(circleTexture);
              dot.tint = d.color;
              dot.blendMode = PIXI.BLEND_MODES.MULTIPLY;
              dot.anchor.x = 0.5;
              dot.anchor.y = 0.5;
              dot.position.x = d.x;
              dot.position.y = d.y;
              dot.scale.x = dot.scale.y = d.size;
              dot.alpha = d.opacity;

              // Save the circle
              dots[i] = dot;

              if(WebGL == false){
                context.fillStyle = d.color;
                context.beginPath();
                context.arc(d.x, d.y, (33*0.12)/PR, 0, Math.PI * 2);
                context.fill();
              }else{
                // Add to the container
                container.addChild(dot);
              }

            });

            if(WebGL == true){
                renderer.render(container);
            }

            // setTimeout(_ => {
            //   animate();
            // }, 1000);

          }

          setTimeout(_ => {
            render();
          }, 100);

          const fps = 10;
          const tweenTime = 5;
          const tweenFrames = fps * tweenTime;
          let animate,
            frame = 0,
            progress = 0;

          animate = function() {

            // Track progress as proportion of frames completed
            frame++;
            // frame = ++frame % tweenFrames;
            progress = (frame / tweenFrames) || 0;
            // console.log(frame, progress);

            if(WebGL == false){
              context.clearRect(0,0,scaledWidth,scaledHeight);
            }
            
            for (var i = 0; i < dots.length; i++) {

              // Trial and testing has taught me that it's best to 
              // do all of these values separately
              x0 = data[i].x;
              x1 = data1[i].x;

              y0 = data[i].y;
              y1 = data1[i].y;

              // Interpolate between them
              xInt = x0 + ((x1 - x0) * progress);
              yInt = y0 + ((y1 - y0) * progress);

              if(WebGL == false){
                context.fillStyle = data[i].color;
                context.beginPath();
                context.arc(xInt, yInt, (33*0.12)/PR, 0, Math.PI * 2);
                context.fill();
              }else{
                dots[i].position.x = xInt;
                dots[i].position.y = yInt;
              }

            }

            // Cue up next frame then render the updates
            if(WebGL == true){
              renderer.render(container);
            }

            if(frame >= (tweenFrames)) stopAnimation = true;

            if(!stopAnimation) requestAnimationFrame(animate);

          };

          document.getElementsByTagName('body')[0].addEventListener('touchstart', animate);
          document.getElementsByTagName('body')[0].addEventListener('dblclick', animate);

        </script>
    </body>
</html>