Skip to content

Instantly share code, notes, and snippets.

@johnburnmurdoch
Last active February 4, 2019 14:48
Show Gist options
  • Save johnburnmurdoch/87f26ebff0609915e7c2fb9a535e36af to your computer and use it in GitHub Desktop.
Save johnburnmurdoch/87f26ebff0609915e7c2fb9a535e36af to your computer and use it in GitHub Desktop.
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("http://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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment