D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
johnburnmurdoch
Full window
Github gist
Animating 50,000 points with WebGL
<!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("https://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>