├── README.md ├── index.html ├── index.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # Smoothly animating points with regl 2 | 3 | > http://rreusser.github.io/smoothly-animating-points-with-regl/ 4 | 5 | Built with [regl](https://github.com/regl-project/regl/) and inspired by the statement on Peter Beshai's lovely blog post [Smoothly animate thousands of points with HTML5 Canvas and D3](https://bocoup.com/blog/smoothly-animate-thousands-of-points-with-html5-canvas-and-d3): 6 | 7 | > Using this approach with canvas can only get us so far. As you exceed 5,000 points and approach closer to 10,000 it is common to see degradation in performance. If you really need to animate that many points flying around, your best bet is to turn to WebGL and have shaders do the work for you. The regl library provides a nice interface to working with shaders and can be used effectively for this purpose, but that’s a topic for another day! 8 | 9 | ## To run 10 | 11 | ```bash 12 | $ npm install 13 | $ npm start 14 | ``` 15 | 16 | ## License 17 | 18 | © 2017 Ricky Reusser. WTFPL. 19 | 20 | As per [the MIT License](https://bl.ocks.org/pbeshai/65420c8d722cdbb0600b276c3adcc6e8#license) on [Peter Beshai's original block](https://bl.ocks.org/pbeshai/65420c8d722cdbb0600b276c3adcc6e8), this repo uses (with gratitude!) the skeleton of the layout functions and is otherwise original code. 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const glsl = require('glslify') 2 | const linspace = require('ndarray-linspace') 3 | const vectorFill = require('ndarray-vector-fill') 4 | const ndarray = require('ndarray') 5 | const ease = require('eases/cubic-in-out') 6 | require('regl')({onDone: require('fail-nicely')(run)}) 7 | 8 | function phyllotaxis (n) { 9 | const theta = Math.PI * (3 - Math.sqrt(5)) 10 | return function (i) { 11 | let r = Math.sqrt(i / n) 12 | let th = i * theta 13 | return [ 14 | r * Math.cos(th), 15 | r * Math.sin(th) 16 | ] 17 | } 18 | } 19 | 20 | function grid (n) { 21 | const rowlen = Math.round(Math.sqrt(n)) 22 | return (i) => [ 23 | -0.8 + 1.6 / rowlen * (i % rowlen), 24 | -0.8 + 1.6 / rowlen * Math.floor(i / rowlen) 25 | ] 26 | } 27 | 28 | function sine (n) { 29 | let xscale = 2 / (n - 1) 30 | return function (i) { 31 | let x = -1 + i * xscale 32 | return [x, Math.sin(x * Math.PI * 3) * 0.3] 33 | } 34 | } 35 | 36 | function spiral (n) { 37 | return function (i) { 38 | let t = Math.sqrt(i / (n - 1)) 39 | return [ 40 | t * Math.cos(t * Math.PI * 40), 41 | t * Math.sin(t * Math.PI * 40) 42 | ] 43 | } 44 | } 45 | 46 | function run (regl) { 47 | let n = 100000 48 | let datasets = [] 49 | let colorBasis 50 | let datasetPtr = 0 51 | 52 | let pointRadius = 3 53 | 54 | let lastSwitchTime = 0 55 | let switchInterval = 2 56 | let switchDuration = 1 57 | 58 | const createDatasets = () => { 59 | // This is a cute little pattern that *either* creates a buffer or updates 60 | // the existing buffer since both the constructor and the current instance 61 | // can be called as a function. 62 | datasets = [phyllotaxis, grid, sine, spiral].map((func, i) => 63 | (datasets[i] || regl.buffer)(vectorFill(ndarray([], [n, 2]), func(n))) 64 | ) 65 | // This is just a list from 1 to 0 for coloring: 66 | colorBasis = (colorBasis || regl.buffer)(linspace(ndarray([], [n]), 1, 0)) 67 | } 68 | 69 | // Initialize: 70 | createDatasets() 71 | 72 | // Create nice controls: 73 | require('control-panel')([ 74 | {type: 'range', min: 1, max: 10, label: 'radius', initial: pointRadius, step: 0.25}, 75 | {type: 'range', min: 1000, max: 200000, label: 'n', initial: n, step: 1000} 76 | ], {width: 400}).on('input', (data) => { 77 | pointRadius = data.radius 78 | if (data.n !== n) { 79 | n = Math.round(data.n) 80 | createDatasets() 81 | } 82 | }) 83 | 84 | const drawPoints = regl({ 85 | vert: ` 86 | precision mediump float; 87 | attribute vec2 xy0, xy1; 88 | attribute float basis; 89 | varying float t; 90 | uniform float aspect, interp, radius; 91 | void main () { 92 | t = basis; 93 | // Interpolate between the two positions: 94 | vec2 pos = mix(xy0, xy1, interp); 95 | gl_Position = vec4(pos.x, pos.y * aspect, 0, 1); 96 | gl_PointSize = radius; 97 | } 98 | `, 99 | frag: glsl(` 100 | precision mediump float; 101 | #pragma glslify: colormap = require(glsl-colormap/viridis) 102 | varying float t; 103 | void main () { 104 | gl_FragColor = colormap(t); 105 | } 106 | `), 107 | depth: {enable: false}, 108 | attributes: { 109 | // Pass two buffers between which we ease in the vertex shader: 110 | xy0: () => datasets[datasetPtr % datasets.length], 111 | xy1: () => datasets[(datasetPtr + 1) % datasets.length], 112 | basis: () => colorBasis 113 | }, 114 | uniforms: { 115 | radius: () => pointRadius, 116 | aspect: ctx => ctx.viewportWidth / ctx.viewportHeight, 117 | // The current interpolation position, from 0 to 1: 118 | interp: (ctx, props) => Math.max(0, Math.min(1, props.interp)) 119 | }, 120 | primitive: 'point', 121 | count: () => n 122 | }) 123 | 124 | regl.frame(({time}) => { 125 | // Check how long it's been since the last switch, and cycle the buffers 126 | // and reset the timer if it's time for a switch: 127 | if ((time - lastSwitchTime) > switchInterval) { 128 | lastSwitchTime = time 129 | datasetPtr++ 130 | } 131 | 132 | drawPoints({interp: ease((time - lastSwitchTime) / switchDuration)}) 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "points", 3 | "version": "1.0.0", 4 | "description": "Smoothly animating points with regl", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo --open --live --force-default-index --host localhost index.js -- -g es2040 -t glslify", 8 | "build": "browserify index.js -g es2040 -t glslify | uglifyjs -cm| indexhtmlify | metadataify | github-cornerify > index.html", 9 | "lint": "standard", 10 | "lint-fix": "standard --fix" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/rreusser/smoothly-animating-points-with-regl" 15 | }, 16 | "keywords": [], 17 | "author": "Ricky Reusser", 18 | "license": "WTFPL", 19 | "dependencies": { 20 | "browserify": "^14.1.0", 21 | "budo": "^9.4.7", 22 | "control-panel": "^1.2.0", 23 | "eases": "^1.0.8", 24 | "es2040": "^1.2.5", 25 | "fail-nicely": "^2.0.0", 26 | "github-cornerify": "^1.0.7", 27 | "glsl-colormap": "^1.0.1", 28 | "glslify": "^6.0.1", 29 | "indexhtmlify": "^1.3.1", 30 | "metadataify": "^1.0.3", 31 | "ndarray": "^1.0.18", 32 | "ndarray-linspace": "^2.0.3", 33 | "ndarray-vector-fill": "^1.0.0", 34 | "regl": "^1.3.0", 35 | "standard": "^9.0.1", 36 | "uglify-js": "^2.8.13" 37 | } 38 | } 39 | --------------------------------------------------------------------------------