├── 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 |
--------------------------------------------------------------------------------