Skip to content

Instantly share code, notes, and snippets.

@pbeshai
Last active January 27, 2019 03:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pbeshai/5309144c8a5faa3dfec5401cc850c7b5 to your computer and use it in GitHub Desktop.
Save pbeshai/5309144c8a5faa3dfec5401cc850c7b5 to your computer and use it in GitHub Desktop.
Animate 100,000 points with regl - I
license: mit
height: 720
border: no
!function(t){function n(I){if(g[I])return g[I].exports;var c=g[I]={i:I,l:!1,exports:{}};return t[I].call(c.exports,c,c.exports,n),c.l=!0,c.exports}var g={};n.m=t,n.c=g,n.i=function(t){return t},n.d=function(t,g,I){n.o(t,g)||Object.defineProperty(t,g,{configurable:!1,enumerable:!0,get:I})},n.n=function(t){var g=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(g,"a",g),g},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=0)}([function(module,exports,__webpack_require__){"use strict";eval("\n\nfunction main(err, regl) {\n var numPoints = 100000;\n var pointWidth = 4;\n var width = window.innerWidth;\n var height = window.innerHeight;\n\n // duration of the animation ignoring delays\n var duration = 1500; // 1500ms = 1.5s\n\n // helper to layout points in a green fuzzy circle\n function greenCircleLayout(points) {\n var rng = d3.randomNormal(0, 0.05);\n points.forEach(function (d, i) {\n d.x = (rng() + Math.cos(i)) * (width / 2.5) + width / 2;\n d.y = (rng() + Math.sin(i)) * (height / 2.5) + height / 2;\n d.color = [0, Math.random(), 0]; // random amount of green\n });\n }\n\n // helper to layout points in a normally distributed area, colored blue\n function blueNormalLayout(points) {\n var rng = d3.randomNormal(0, 0.15);\n points.forEach(function (d) {\n d.x = rng() * width + width / 2;\n d.y = rng() * height + height / 2;\n d.color = [0, d.color[1] * 0.5, 0.9]; // some previous green and 0.9 blue\n });\n }\n\n // set the order of the layouts and some initial animation state\n var layouts = [greenCircleLayout, blueNormalLayout];\n var currentLayout = 0; // start with green circle layout\n\n // function to compile a draw points regl func\n function createDrawPoints(points) {\n var drawPoints = regl({\n frag: '\\n\\t\\t\\t// set the precision of floating point numbers\\n\\t\\t precision highp float;\\n\\n\\t\\t // this value is populated by the vertex shader\\n\\t\\t\\tvarying vec3 fragColor;\\n\\n\\t\\t\\tvoid main() {\\n\\t\\t\\t\\t// gl_FragColor is a special variable that holds the color of a pixel\\n\\t\\t\\t\\tgl_FragColor = vec4(fragColor, 1);\\n\\t\\t\\t}\\n\\t\\t\\t',\n\n vert: '\\n\\t\\t\\t// per vertex attributes\\n\\t\\t\\tattribute vec2 positionStart;\\n\\t\\t\\tattribute vec2 positionEnd;\\n\\t\\t\\tattribute vec3 colorStart;\\n\\t\\t\\tattribute vec3 colorEnd;\\n\\n\\t\\t\\t// variables to send to the fragment shader\\n\\t\\t\\tvarying vec3 fragColor;\\n\\n\\t\\t\\t// values that are the same for all vertices\\n\\t\\t\\tuniform float pointWidth;\\n\\t\\t\\tuniform float stageWidth;\\n\\t\\t\\tuniform float stageHeight;\\n\\t\\t\\tuniform float elapsed;\\n\\t\\t\\tuniform float duration;\\n\\n\\t\\t\\t// helper function to transform from pixel space to normalized device coordinates (NDC)\\n\\t\\t\\t// in NDC (0,0) is the middle, (-1, 1) is the top left and (1, -1) is the bottom right.\\n\\t\\t\\tvec2 normalizeCoords(vec2 position) {\\n\\t\\t\\t\\t// read in the positions into x and y vars\\n\\t float x = position[0];\\n\\t float y = position[1];\\n\\n\\t\\t\\t\\treturn vec2(\\n\\t\\t 2.0 * ((x / stageWidth) - 0.5),\\n\\t\\t // invert y since we think [0,0] is bottom left in pixel space\\n\\t\\t -(2.0 * ((y / stageHeight) - 0.5)));\\n\\t\\t\\t}\\n\\n\\t\\t\\t// helper function to handle cubic easing (copied from d3 for consistency)\\n\\t\\t\\t// note there are pre-made easing functions available via glslify.\\n\\t\\t\\tfloat easeCubicInOut(float t) {\\n\\t\\t\\t\\tt *= 2.0;\\n t = (t <= 1.0 ? t * t * t : (t -= 2.0) * t * t + 2.0) / 2.0;\\n\\n if (t > 1.0) {\\n t = 1.0;\\n }\\n\\n return t;\\n\\t\\t\\t}\\n\\n\\t\\t\\tvoid main() {\\n\\t\\t\\t\\t// update the size of a point based on the prop pointWidth\\n\\t\\t\\t\\tgl_PointSize = pointWidth;\\n\\n\\t\\t\\t\\t// number between 0 and 1 indicating how far through the animation this\\n\\t\\t\\t\\t// vertex is.\\n\\t\\t\\t\\tfloat t;\\n\\n\\t // drawing without animation, so show end state immediately\\n\\t if (duration == 0.0) {\\n\\t t = 1.0;\\n\\n\\t // otherwise we are animating, so use cubic easing\\n\\t } else {\\n\\t t = easeCubicInOut(elapsed / duration);\\n\\t }\\n\\n\\t // interpolate position\\n\\t vec2 position = mix(positionStart, positionEnd, t);\\n\\n\\t // interpolate and send color to the fragment shader\\n\\t fragColor = mix(colorStart, colorEnd, t);\\n\\n\\t\\t\\t\\t// scale to normalized device coordinates\\n\\t\\t\\t\\t// gl_Position is a special variable that holds the position of a vertex\\n\\t gl_Position = vec4(normalizeCoords(position), 0.0, 1.0);\\n\\t\\t\\t}\\n\\t\\t\\t',\n\n attributes: {\n // each of these gets mapped to a single entry for each of the points.\n // this means the vertex shader will receive just the relevant value for a given point.\n positionStart: points.map(function (d) {\n return [d.sx, d.sy];\n }),\n positionEnd: points.map(function (d) {\n return [d.tx, d.ty];\n }),\n colorStart: points.map(function (d) {\n return d.colorStart;\n }),\n colorEnd: points.map(function (d) {\n return d.colorEnd;\n })\n },\n\n uniforms: {\n // by using `regl.prop` to pass these in, we can specify them as arguments\n // to our drawPoints function\n pointWidth: regl.prop('pointWidth'),\n\n // regl actually provides these as viewportWidth and viewportHeight but I\n // am using these outside and I want to ensure they are the same numbers,\n // so I am explicitly passing them in.\n stageWidth: regl.prop('stageWidth'),\n stageHeight: regl.prop('stageHeight'),\n\n duration: regl.prop('duration'),\n // time in milliseconds since the prop startTime (i.e. time elapsed)\n // note that `time` is passed by regl whereas `startTime` is a prop passed\n // to the drawPoints function.\n elapsed: function elapsed(_ref, _ref2) {\n var time = _ref.time;\n var _ref2$startTime = _ref2.startTime,\n startTime = _ref2$startTime === undefined ? 0 : _ref2$startTime;\n return (time - startTime) * 1000;\n }\n },\n\n // specify the number of points to draw\n count: points.length,\n\n // specify that each vertex is a point (not part of a mesh)\n primitive: 'points'\n });\n\n return drawPoints;\n }\n\n // function to start the animation loop (note: time is in seconds)\n function animate(layout, points) {\n console.log('animating with new layout');\n // make previous end the new beginning\n points.forEach(function (d) {\n d.sx = d.tx;\n d.sy = d.ty;\n d.colorStart = d.colorEnd;\n });\n\n // layout points\n layout(points);\n\n // copy layout x y to end positions\n points.forEach(function (d, i) {\n d.tx = d.x;\n d.ty = d.y;\n d.colorEnd = d.color;\n });\n\n // create the regl function with the new start and end points\n var drawPoints = createDrawPoints(points);\n\n // start an animation loop\n var startTime = null; // in seconds\n var frameLoop = regl.frame(function (_ref3) {\n var time = _ref3.time;\n\n // keep track of start time so we can get time elapsed\n // this is important since time doesn't reset when starting new animations\n if (startTime === null) {\n startTime = time;\n }\n\n // clear the buffer\n regl.clear({\n // background color (black)\n color: [0, 0, 0, 1],\n depth: 1\n });\n\n // draw the points using our created regl func\n // note that the arguments are available via `regl.prop`.\n drawPoints({\n pointWidth: pointWidth,\n stageWidth: width,\n stageHeight: height,\n duration: duration,\n startTime: startTime\n });\n\n // if we have exceeded the maximum duration, move on to the next animation\n if (time - startTime > duration / 1000) {\n console.log('done animating, moving to next layout');\n\n // cancel this loop, we are going to start another\n frameLoop.cancel();\n\n // increment to use next layout function\n currentLayout = (currentLayout + 1) % layouts.length;\n\n // start a new animation loop with next layout function\n animate(layouts[currentLayout], points);\n }\n });\n }\n\n // create initial set of points\n // initialize with all the points in the middle of the screen and black\n var points = d3.range(numPoints).map(function (i) {\n return {\n tx: width / 2,\n ty: height / 2,\n colorEnd: [0, 0, 0]\n };\n });\n\n // start the initial animation\n animate(layouts[currentLayout], points);\n}\n\n// initialize regl\ncreateREGL({\n // callback when regl is initialized\n onDone: main\n});//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9zY3JpcHQuanM/OWE5NSJdLCJzb3VyY2VzQ29udGVudCI6WyJmdW5jdGlvbiBtYWluKGVyciwgcmVnbCkge1xuICBjb25zdCBudW1Qb2ludHMgPSAxMDAwMDA7XG4gIGNvbnN0IHBvaW50V2lkdGggPSA0O1xuICBjb25zdCB3aWR0aCA9IHdpbmRvdy5pbm5lcldpZHRoO1xuICBjb25zdCBoZWlnaHQgPSB3aW5kb3cuaW5uZXJIZWlnaHQ7XG5cbiAgLy8gZHVyYXRpb24gb2YgdGhlIGFuaW1hdGlvbiBpZ25vcmluZyBkZWxheXNcbiAgY29uc3QgZHVyYXRpb24gPSAxNTAwOyAvLyAxNTAwbXMgPSAxLjVzXG5cbiAgLy8gaGVscGVyIHRvIGxheW91dCBwb2ludHMgaW4gYSBncmVlbiBmdXp6eSBjaXJjbGVcbiAgZnVuY3Rpb24gZ3JlZW5DaXJjbGVMYXlvdXQocG9pbnRzKSB7XG4gICAgY29uc3Qgcm5nID0gZDMucmFuZG9tTm9ybWFsKDAsIDAuMDUpO1xuICAgIHBvaW50cy5mb3JFYWNoKChkLCBpKSA9PiB7XG4gICAgICBkLnggPSAocm5nKCkgKyBNYXRoLmNvcyhpKSkgKiAod2lkdGggLyAyLjUpICsgd2lkdGggLyAyO1xuICAgICAgZC55ID0gKHJuZygpICsgTWF0aC5zaW4oaSkpICogKGhlaWdodCAvIDIuNSkgKyBoZWlnaHQgLyAyO1xuICAgICAgZC5jb2xvciA9IFswLCBNYXRoLnJhbmRvbSgpLCAwXTsgLy8gcmFuZG9tIGFtb3VudCBvZiBncmVlblxuICAgIH0pO1xuICB9XG5cbiAgLy8gaGVscGVyIHRvIGxheW91dCBwb2ludHMgaW4gYSBub3JtYWxseSBkaXN0cmlidXRlZCBhcmVhLCBjb2xvcmVkIGJsdWVcbiAgZnVuY3Rpb24gYmx1ZU5vcm1hbExheW91dChwb2ludHMpIHtcbiAgICBjb25zdCBybmcgPSBkMy5yYW5kb21Ob3JtYWwoMCwgMC4xNSk7XG4gICAgcG9pbnRzLmZvckVhY2goZCA9PiB7XG4gICAgICBkLnggPSBybmcoKSAqIHdpZHRoICsgd2lkdGggLyAyO1xuICAgICAgZC55ID0gcm5nKCkgKiBoZWlnaHQgKyBoZWlnaHQgLyAyO1xuICAgICAgZC5jb2xvciA9IFswLCBkLmNvbG9yWzFdICogMC41LCAwLjldOyAvLyBzb21lIHByZXZpb3VzIGdyZWVuIGFuZCAwLjkgYmx1ZVxuICAgIH0pO1xuICB9XG5cbiAgLy8gc2V0IHRoZSBvcmRlciBvZiB0aGUgbGF5b3V0cyBhbmQgc29tZSBpbml0aWFsIGFuaW1hdGlvbiBzdGF0ZVxuICBjb25zdCBsYXlvdXRzID0gW2dyZWVuQ2lyY2xlTGF5b3V0LCBibHVlTm9ybWFsTGF5b3V0XTtcbiAgbGV0IGN1cnJlbnRMYXlvdXQgPSAwOyAvLyBzdGFydCB3aXRoIGdyZWVuIGNpcmNsZSBsYXlvdXRcblxuICAvLyBmdW5jdGlvbiB0byBjb21waWxlIGEgZHJhdyBwb2ludHMgcmVnbCBmdW5jXG4gIGZ1bmN0aW9uIGNyZWF0ZURyYXdQb2ludHMocG9pbnRzKSB7XG4gICAgY29uc3QgZHJhd1BvaW50cyA9IHJlZ2woe1xuICAgICAgZnJhZzogYFxuXHRcdFx0Ly8gc2V0IHRoZSBwcmVjaXNpb24gb2YgZmxvYXRpbmcgcG9pbnQgbnVtYmVyc1xuXHRcdCAgcHJlY2lzaW9uIGhpZ2hwIGZsb2F0O1xuXG5cdFx0ICAvLyB0aGlzIHZhbHVlIGlzIHBvcHVsYXRlZCBieSB0aGUgdmVydGV4IHNoYWRlclxuXHRcdFx0dmFyeWluZyB2ZWMzIGZyYWdDb2xvcjtcblxuXHRcdFx0dm9pZCBtYWluKCkge1xuXHRcdFx0XHQvLyBnbF9GcmFnQ29sb3IgaXMgYSBzcGVjaWFsIHZhcmlhYmxlIHRoYXQgaG9sZHMgdGhlIGNvbG9yIG9mIGEgcGl4ZWxcblx0XHRcdFx0Z2xfRnJhZ0NvbG9yID0gdmVjNChmcmFnQ29sb3IsIDEpO1xuXHRcdFx0fVxuXHRcdFx0YCxcblxuICAgICAgdmVydDogYFxuXHRcdFx0Ly8gcGVyIHZlcnRleCBhdHRyaWJ1dGVzXG5cdFx0XHRhdHRyaWJ1dGUgdmVjMiBwb3NpdGlvblN0YXJ0O1xuXHRcdFx0YXR0cmlidXRlIHZlYzIgcG9zaXRpb25FbmQ7XG5cdFx0XHRhdHRyaWJ1dGUgdmVjMyBjb2xvclN0YXJ0O1xuXHRcdFx0YXR0cmlidXRlIHZlYzMgY29sb3JFbmQ7XG5cblx0XHRcdC8vIHZhcmlhYmxlcyB0byBzZW5kIHRvIHRoZSBmcmFnbWVudCBzaGFkZXJcblx0XHRcdHZhcnlpbmcgdmVjMyBmcmFnQ29sb3I7XG5cblx0XHRcdC8vIHZhbHVlcyB0aGF0IGFyZSB0aGUgc2FtZSBmb3IgYWxsIHZlcnRpY2VzXG5cdFx0XHR1bmlmb3JtIGZsb2F0IHBvaW50V2lkdGg7XG5cdFx0XHR1bmlmb3JtIGZsb2F0IHN0YWdlV2lkdGg7XG5cdFx0XHR1bmlmb3JtIGZsb2F0IHN0YWdlSGVpZ2h0O1xuXHRcdFx0dW5pZm9ybSBmbG9hdCBlbGFwc2VkO1xuXHRcdFx0dW5pZm9ybSBmbG9hdCBkdXJhdGlvbjtcblxuXHRcdFx0Ly8gaGVscGVyIGZ1bmN0aW9uIHRvIHRyYW5zZm9ybSBmcm9tIHBpeGVsIHNwYWNlIHRvIG5vcm1hbGl6ZWQgZGV2aWNlIGNvb3JkaW5hdGVzIChOREMpXG5cdFx0XHQvLyBpbiBOREMgKDAsMCkgaXMgdGhlIG1pZGRsZSwgKC0xLCAxKSBpcyB0aGUgdG9wIGxlZnQgYW5kICgxLCAtMSkgaXMgdGhlIGJvdHRvbSByaWdodC5cblx0XHRcdHZlYzIgbm9ybWFsaXplQ29vcmRzKHZlYzIgcG9zaXRpb24pIHtcblx0XHRcdFx0Ly8gcmVhZCBpbiB0aGUgcG9zaXRpb25zIGludG8geCBhbmQgeSB2YXJzXG5cdCAgICAgIGZsb2F0IHggPSBwb3NpdGlvblswXTtcblx0ICAgICAgZmxvYXQgeSA9IHBvc2l0aW9uWzFdO1xuXG5cdFx0XHRcdHJldHVybiB2ZWMyKFxuXHRcdCAgICAgIDIuMCAqICgoeCAvIHN0YWdlV2lkdGgpIC0gMC41KSxcblx0XHQgICAgICAvLyBpbnZlcnQgeSBzaW5jZSB3ZSB0aGluayBbMCwwXSBpcyBib3R0b20gbGVmdCBpbiBwaXhlbCBzcGFjZVxuXHRcdCAgICAgIC0oMi4wICogKCh5IC8gc3RhZ2VIZWlnaHQpIC0gMC41KSkpO1xuXHRcdFx0fVxuXG5cdFx0XHQvLyBoZWxwZXIgZnVuY3Rpb24gdG8gaGFuZGxlIGN1YmljIGVhc2luZyAoY29waWVkIGZyb20gZDMgZm9yIGNvbnNpc3RlbmN5KVxuXHRcdFx0Ly8gbm90ZSB0aGVyZSBhcmUgcHJlLW1hZGUgZWFzaW5nIGZ1bmN0aW9ucyBhdmFpbGFibGUgdmlhIGdsc2xpZnkuXG5cdFx0XHRmbG9hdCBlYXNlQ3ViaWNJbk91dChmbG9hdCB0KSB7XG5cdFx0XHRcdHQgKj0gMi4wO1xuICAgICAgICB0ID0gKHQgPD0gMS4wID8gdCAqIHQgKiB0IDogKHQgLT0gMi4wKSAqIHQgKiB0ICsgMi4wKSAvIDIuMDtcblxuICAgICAgICBpZiAodCA+IDEuMCkge1xuICAgICAgICAgIHQgPSAxLjA7XG4gICAgICAgIH1cblxuICAgICAgICByZXR1cm4gdDtcblx0XHRcdH1cblxuXHRcdFx0dm9pZCBtYWluKCkge1xuXHRcdFx0XHQvLyB1cGRhdGUgdGhlIHNpemUgb2YgYSBwb2ludCBiYXNlZCBvbiB0aGUgcHJvcCBwb2ludFdpZHRoXG5cdFx0XHRcdGdsX1BvaW50U2l6ZSA9IHBvaW50V2lkdGg7XG5cblx0XHRcdFx0Ly8gbnVtYmVyIGJldHdlZW4gMCBhbmQgMSBpbmRpY2F0aW5nIGhvdyBmYXIgdGhyb3VnaCB0aGUgYW5pbWF0aW9uIHRoaXNcblx0XHRcdFx0Ly8gdmVydGV4IGlzLlxuXHRcdFx0XHRmbG9hdCB0O1xuXG5cdCAgICAgIC8vIGRyYXdpbmcgd2l0aG91dCBhbmltYXRpb24sIHNvIHNob3cgZW5kIHN0YXRlIGltbWVkaWF0ZWx5XG5cdCAgICAgIGlmIChkdXJhdGlvbiA9PSAwLjApIHtcblx0ICAgICAgICB0ID0gMS4wO1xuXG5cdCAgICAgIC8vIG90aGVyd2lzZSB3ZSBhcmUgYW5pbWF0aW5nLCBzbyB1c2UgY3ViaWMgZWFzaW5nXG5cdCAgICAgIH0gZWxzZSB7XG5cdCAgICAgICAgdCA9IGVhc2VDdWJpY0luT3V0KGVsYXBzZWQgLyBkdXJhdGlvbik7XG5cdCAgICAgIH1cblxuXHQgICAgICAvLyBpbnRlcnBvbGF0ZSBwb3NpdGlvblxuXHQgICAgICB2ZWMyIHBvc2l0aW9uID0gbWl4KHBvc2l0aW9uU3RhcnQsIHBvc2l0aW9uRW5kLCB0KTtcblxuXHQgICAgICAvLyBpbnRlcnBvbGF0ZSBhbmQgc2VuZCBjb2xvciB0byB0aGUgZnJhZ21lbnQgc2hhZGVyXG5cdCAgICAgIGZyYWdDb2xvciA9IG1peChjb2xvclN0YXJ0LCBjb2xvckVuZCwgdCk7XG5cblx0XHRcdFx0Ly8gc2NhbGUgdG8gbm9ybWFsaXplZCBkZXZpY2UgY29vcmRpbmF0ZXNcblx0XHRcdFx0Ly8gZ2xfUG9zaXRpb24gaXMgYSBzcGVjaWFsIHZhcmlhYmxlIHRoYXQgaG9sZHMgdGhlIHBvc2l0aW9uIG9mIGEgdmVydGV4XG5cdCAgICAgIGdsX1Bvc2l0aW9uID0gdmVjNChub3JtYWxpemVDb29yZHMocG9zaXRpb24pLCAwLjAsIDEuMCk7XG5cdFx0XHR9XG5cdFx0XHRgLFxuXG4gICAgICBhdHRyaWJ1dGVzOiB7XG4gICAgICAgIC8vIGVhY2ggb2YgdGhlc2UgZ2V0cyBtYXBwZWQgdG8gYSBzaW5nbGUgZW50cnkgZm9yIGVhY2ggb2YgdGhlIHBvaW50cy5cbiAgICAgICAgLy8gdGhpcyBtZWFucyB0aGUgdmVydGV4IHNoYWRlciB3aWxsIHJlY2VpdmUganVzdCB0aGUgcmVsZXZhbnQgdmFsdWUgZm9yIGEgZ2l2ZW4gcG9pbnQuXG4gICAgICAgIHBvc2l0aW9uU3RhcnQ6IHBvaW50cy5tYXAoZCA9PiBbZC5zeCwgZC5zeV0pLFxuICAgICAgICBwb3NpdGlvbkVuZDogcG9pbnRzLm1hcChkID0+IFtkLnR4LCBkLnR5XSksXG4gICAgICAgIGNvbG9yU3RhcnQ6IHBvaW50cy5tYXAoZCA9PiBkLmNvbG9yU3RhcnQpLFxuICAgICAgICBjb2xvckVuZDogcG9pbnRzLm1hcChkID0+IGQuY29sb3JFbmQpLFxuICAgICAgfSxcblxuICAgICAgdW5pZm9ybXM6IHtcbiAgICAgICAgLy8gYnkgdXNpbmcgYHJlZ2wucHJvcGAgdG8gcGFzcyB0aGVzZSBpbiwgd2UgY2FuIHNwZWNpZnkgdGhlbSBhcyBhcmd1bWVudHNcbiAgICAgICAgLy8gdG8gb3VyIGRyYXdQb2ludHMgZnVuY3Rpb25cbiAgICAgICAgcG9pbnRXaWR0aDogcmVnbC5wcm9wKCdwb2ludFdpZHRoJyksXG5cbiAgICAgICAgLy8gcmVnbCBhY3R1YWxseSBwcm92aWRlcyB0aGVzZSBhcyB2aWV3cG9ydFdpZHRoIGFuZCB2aWV3cG9ydEhlaWdodCBidXQgSVxuICAgICAgICAvLyBhbSB1c2luZyB0aGVzZSBvdXRzaWRlIGFuZCBJIHdhbnQgdG8gZW5zdXJlIHRoZXkgYXJlIHRoZSBzYW1lIG51bWJlcnMsXG4gICAgICAgIC8vIHNvIEkgYW0gZXhwbGljaXRseSBwYXNzaW5nIHRoZW0gaW4uXG4gICAgICAgIHN0YWdlV2lkdGg6IHJlZ2wucHJvcCgnc3RhZ2VXaWR0aCcpLFxuICAgICAgICBzdGFnZUhlaWdodDogcmVnbC5wcm9wKCdzdGFnZUhlaWdodCcpLFxuXG4gICAgICAgIGR1cmF0aW9uOiByZWdsLnByb3AoJ2R1cmF0aW9uJyksXG4gICAgICAgIC8vIHRpbWUgaW4gbWlsbGlzZWNvbmRzIHNpbmNlIHRoZSBwcm9wIHN0YXJ0VGltZSAoaS5lLiB0aW1lIGVsYXBzZWQpXG4gICAgICAgIC8vIG5vdGUgdGhhdCBgdGltZWAgaXMgcGFzc2VkIGJ5IHJlZ2wgd2hlcmVhcyBgc3RhcnRUaW1lYCBpcyBhIHByb3AgcGFzc2VkXG4gICAgICAgIC8vIHRvIHRoZSBkcmF3UG9pbnRzIGZ1bmN0aW9uLlxuICAgICAgICBlbGFwc2VkOiAoeyB0aW1lIH0sIHsgc3RhcnRUaW1lID0gMCB9KSA9PiAodGltZSAtIHN0YXJ0VGltZSkgKiAxMDAwLFxuICAgICAgfSxcblxuICAgICAgLy8gc3BlY2lmeSB0aGUgbnVtYmVyIG9mIHBvaW50cyB0byBkcmF3XG4gICAgICBjb3VudDogcG9pbnRzLmxlbmd0aCxcblxuICAgICAgLy8gc3BlY2lmeSB0aGF0IGVhY2ggdmVydGV4IGlzIGEgcG9pbnQgKG5vdCBwYXJ0IG9mIGEgbWVzaClcbiAgICAgIHByaW1pdGl2ZTogJ3BvaW50cycsXG4gICAgfSk7XG5cbiAgICByZXR1cm4gZHJhd1BvaW50cztcbiAgfVxuXG4gIC8vIGZ1bmN0aW9uIHRvIHN0YXJ0IHRoZSBhbmltYXRpb24gbG9vcCAobm90ZTogdGltZSBpcyBpbiBzZWNvbmRzKVxuICBmdW5jdGlvbiBhbmltYXRlKGxheW91dCwgcG9pbnRzKSB7XG4gICAgY29uc29sZS5sb2coJ2FuaW1hdGluZyB3aXRoIG5ldyBsYXlvdXQnKTtcbiAgICAvLyBtYWtlIHByZXZpb3VzIGVuZCB0aGUgbmV3IGJlZ2lubmluZ1xuICAgIHBvaW50cy5mb3JFYWNoKGQgPT4ge1xuICAgICAgZC5zeCA9IGQudHg7XG4gICAgICBkLnN5ID0gZC50eTtcbiAgICAgIGQuY29sb3JTdGFydCA9IGQuY29sb3JFbmQ7XG4gICAgfSk7XG5cbiAgICAvLyBsYXlvdXQgcG9pbnRzXG4gICAgbGF5b3V0KHBvaW50cyk7XG5cbiAgICAvLyBjb3B5IGxheW91dCB4IHkgdG8gZW5kIHBvc2l0aW9uc1xuICAgIHBvaW50cy5mb3JFYWNoKChkLCBpKSA9PiB7XG4gICAgICBkLnR4ID0gZC54O1xuICAgICAgZC50eSA9IGQueTtcbiAgICAgIGQuY29sb3JFbmQgPSBkLmNvbG9yO1xuICAgIH0pO1xuXG4gICAgLy8gY3JlYXRlIHRoZSByZWdsIGZ1bmN0aW9uIHdpdGggdGhlIG5ldyBzdGFydCBhbmQgZW5kIHBvaW50c1xuICAgIGNvbnN0IGRyYXdQb2ludHMgPSBjcmVhdGVEcmF3UG9pbnRzKHBvaW50cyk7XG5cbiAgICAvLyBzdGFydCBhbiBhbmltYXRpb24gbG9vcFxuICAgIGxldCBzdGFydFRpbWUgPSBudWxsOyAvLyBpbiBzZWNvbmRzXG4gICAgY29uc3QgZnJhbWVMb29wID0gcmVnbC5mcmFtZSgoeyB0aW1lIH0pID0+IHtcbiAgICAgIC8vIGtlZXAgdHJhY2sgb2Ygc3RhcnQgdGltZSBzbyB3ZSBjYW4gZ2V0IHRpbWUgZWxhcHNlZFxuICAgICAgLy8gdGhpcyBpcyBpbXBvcnRhbnQgc2luY2UgdGltZSBkb2Vzbid0IHJlc2V0IHdoZW4gc3RhcnRpbmcgbmV3IGFuaW1hdGlvbnNcbiAgICAgIGlmIChzdGFydFRpbWUgPT09IG51bGwpIHtcbiAgICAgICAgc3RhcnRUaW1lID0gdGltZTtcbiAgICAgIH1cblxuICAgICAgLy8gY2xlYXIgdGhlIGJ1ZmZlclxuICAgICAgcmVnbC5jbGVhcih7XG4gICAgICAgIC8vIGJhY2tncm91bmQgY29sb3IgKGJsYWNrKVxuICAgICAgICBjb2xvcjogWzAsIDAsIDAsIDFdLFxuICAgICAgICBkZXB0aDogMSxcbiAgICAgIH0pO1xuXG4gICAgICAvLyBkcmF3IHRoZSBwb2ludHMgdXNpbmcgb3VyIGNyZWF0ZWQgcmVnbCBmdW5jXG4gICAgICAvLyBub3RlIHRoYXQgdGhlIGFyZ3VtZW50cyBhcmUgYXZhaWxhYmxlIHZpYSBgcmVnbC5wcm9wYC5cbiAgICAgIGRyYXdQb2ludHMoe1xuICAgICAgICBwb2ludFdpZHRoLFxuICAgICAgICBzdGFnZVdpZHRoOiB3aWR0aCxcbiAgICAgICAgc3RhZ2VIZWlnaHQ6IGhlaWdodCxcbiAgICAgICAgZHVyYXRpb24sXG4gICAgICAgIHN0YXJ0VGltZSxcbiAgICAgIH0pO1xuXG4gICAgICAvLyBpZiB3ZSBoYXZlIGV4Y2VlZGVkIHRoZSBtYXhpbXVtIGR1cmF0aW9uLCBtb3ZlIG9uIHRvIHRoZSBuZXh0IGFuaW1hdGlvblxuICAgICAgaWYgKHRpbWUgLSBzdGFydFRpbWUgPiBkdXJhdGlvbiAvIDEwMDApIHtcbiAgICAgICAgY29uc29sZS5sb2coJ2RvbmUgYW5pbWF0aW5nLCBtb3ZpbmcgdG8gbmV4dCBsYXlvdXQnKTtcblxuICAgICAgICAvLyBjYW5jZWwgdGhpcyBsb29wLCB3ZSBhcmUgZ29pbmcgdG8gc3RhcnQgYW5vdGhlclxuICAgICAgICBmcmFtZUxvb3AuY2FuY2VsKCk7XG5cbiAgICAgICAgLy8gaW5jcmVtZW50IHRvIHVzZSBuZXh0IGxheW91dCBmdW5jdGlvblxuICAgICAgICBjdXJyZW50TGF5b3V0ID0gKGN1cnJlbnRMYXlvdXQgKyAxKSAlIGxheW91dHMubGVuZ3RoO1xuXG4gICAgICAgIC8vIHN0YXJ0IGEgbmV3IGFuaW1hdGlvbiBsb29wIHdpdGggbmV4dCBsYXlvdXQgZnVuY3Rpb25cbiAgICAgICAgYW5pbWF0ZShsYXlvdXRzW2N1cnJlbnRMYXlvdXRdLCBwb2ludHMpO1xuICAgICAgfVxuICAgIH0pO1xuICB9XG5cbiAgLy8gY3JlYXRlIGluaXRpYWwgc2V0IG9mIHBvaW50c1xuICAvLyBpbml0aWFsaXplIHdpdGggYWxsIHRoZSBwb2ludHMgaW4gdGhlIG1pZGRsZSBvZiB0aGUgc2NyZWVuIGFuZCBibGFja1xuICBjb25zdCBwb2ludHMgPSBkMy5yYW5nZShudW1Qb2ludHMpLm1hcChpID0+ICh7XG4gICAgdHg6IHdpZHRoIC8gMixcbiAgICB0eTogaGVpZ2h0IC8gMixcbiAgICBjb2xvckVuZDogWzAsIDAsIDBdLFxuICB9KSk7XG5cbiAgLy8gc3RhcnQgdGhlIGluaXRpYWwgYW5pbWF0aW9uXG4gIGFuaW1hdGUobGF5b3V0c1tjdXJyZW50TGF5b3V0XSwgcG9pbnRzKTtcbn1cblxuLy8gaW5pdGlhbGl6ZSByZWdsXG5jcmVhdGVSRUdMKHtcbiAgLy8gY2FsbGJhY2sgd2hlbiByZWdsIGlzIGluaXRpYWxpemVkXG4gIG9uRG9uZTogbWFpbixcbn0pO1xuXG5cblxuLy8gV0VCUEFDSyBGT09URVIgLy9cbi8vIHNjcmlwdC5qcyJdLCJtYXBwaW5ncyI6Ijs7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBWUE7QUFDQTtBQXVFQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBQUE7QUFDQTtBQUFBO0FBQUE7QUFDQTtBQUFBO0FBQUE7QUFDQTtBQUFBO0FBQUE7QUFOQTtBQUNBO0FBUUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBZkE7QUFDQTtBQWlCQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBckhBO0FBQ0E7QUF1SEE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBQ0E7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBSEE7QUFDQTtBQUtBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFMQTtBQUNBO0FBT0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUhBO0FBQUE7QUFDQTtBQUtBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFGQSIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///0\n")}]);
<!DOCTYPE html>
<title>Animate 100,000 points with regl - I</title>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/regl/1.3.11/regl.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="dist.js"></script>
</body>
function main(err, regl) {
const numPoints = 100000;
const pointWidth = 4;
const width = window.innerWidth;
const height = window.innerHeight;
// duration of the animation ignoring delays
const duration = 1500; // 1500ms = 1.5s
// helper to layout points in a green fuzzy circle
function greenCircleLayout(points) {
const rng = d3.randomNormal(0, 0.05);
points.forEach((d, i) => {
d.x = (rng() + Math.cos(i)) * (width / 2.5) + width / 2;
d.y = (rng() + Math.sin(i)) * (height / 2.5) + height / 2;
d.color = [0, Math.random(), 0]; // random amount of green
});
}
// helper to layout points in a normally distributed area, colored blue
function blueNormalLayout(points) {
const rng = d3.randomNormal(0, 0.15);
points.forEach(d => {
d.x = rng() * width + width / 2;
d.y = rng() * height + height / 2;
d.color = [0, d.color[1] * 0.5, 0.9]; // some previous green and 0.9 blue
});
}
// set the order of the layouts and some initial animation state
const layouts = [greenCircleLayout, blueNormalLayout];
let currentLayout = 0; // start with green circle layout
// function to compile a draw points regl func
function createDrawPoints(points) {
const drawPoints = regl({
frag: `
// set the precision of floating point numbers
precision highp float;
// this value is populated by the vertex shader
varying vec3 fragColor;
void main() {
// gl_FragColor is a special variable that holds the color of a pixel
gl_FragColor = vec4(fragColor, 1);
}
`,
vert: `
// per vertex attributes
attribute vec2 positionStart;
attribute vec2 positionEnd;
attribute vec3 colorStart;
attribute vec3 colorEnd;
// variables to send to the fragment shader
varying vec3 fragColor;
// values that are the same for all vertices
uniform float pointWidth;
uniform float stageWidth;
uniform float stageHeight;
uniform float elapsed;
uniform float duration;
// helper function to transform from pixel space to normalized device coordinates (NDC)
// in NDC (0,0) is the middle, (-1, 1) is the top left and (1, -1) is the bottom right.
vec2 normalizeCoords(vec2 position) {
// read in the positions into x and y vars
float x = position[0];
float y = position[1];
return vec2(
2.0 * ((x / stageWidth) - 0.5),
// invert y since we think [0,0] is bottom left in pixel space
-(2.0 * ((y / stageHeight) - 0.5)));
}
// helper function to handle cubic easing (copied from d3 for consistency)
// note there are pre-made easing functions available via glslify.
float easeCubicInOut(float t) {
t *= 2.0;
t = (t <= 1.0 ? t * t * t : (t -= 2.0) * t * t + 2.0) / 2.0;
if (t > 1.0) {
t = 1.0;
}
return t;
}
void main() {
// update the size of a point based on the prop pointWidth
gl_PointSize = pointWidth;
// number between 0 and 1 indicating how far through the animation this
// vertex is.
float t;
// drawing without animation, so show end state immediately
if (duration == 0.0) {
t = 1.0;
// otherwise we are animating, so use cubic easing
} else {
t = easeCubicInOut(elapsed / duration);
}
// interpolate position
vec2 position = mix(positionStart, positionEnd, t);
// interpolate and send color to the fragment shader
fragColor = mix(colorStart, colorEnd, t);
// scale to normalized device coordinates
// gl_Position is a special variable that holds the position of a vertex
gl_Position = vec4(normalizeCoords(position), 0.0, 1.0);
}
`,
attributes: {
// each of these gets mapped to a single entry for each of the points.
// this means the vertex shader will receive just the relevant value for a given point.
positionStart: points.map(d => [d.sx, d.sy]),
positionEnd: points.map(d => [d.tx, d.ty]),
colorStart: points.map(d => d.colorStart),
colorEnd: points.map(d => d.colorEnd),
},
uniforms: {
// by using `regl.prop` to pass these in, we can specify them as arguments
// to our drawPoints function
pointWidth: regl.prop('pointWidth'),
// regl actually provides these as viewportWidth and viewportHeight but I
// am using these outside and I want to ensure they are the same numbers,
// so I am explicitly passing them in.
stageWidth: regl.prop('stageWidth'),
stageHeight: regl.prop('stageHeight'),
duration: regl.prop('duration'),
// time in milliseconds since the prop startTime (i.e. time elapsed)
// note that `time` is passed by regl whereas `startTime` is a prop passed
// to the drawPoints function.
elapsed: ({ time }, { startTime = 0 }) => (time - startTime) * 1000,
},
// specify the number of points to draw
count: points.length,
// specify that each vertex is a point (not part of a mesh)
primitive: 'points',
});
return drawPoints;
}
// function to start the animation loop (note: time is in seconds)
function animate(layout, points) {
console.log('animating with new layout');
// make previous end the new beginning
points.forEach(d => {
d.sx = d.tx;
d.sy = d.ty;
d.colorStart = d.colorEnd;
});
// layout points
layout(points);
// copy layout x y to end positions
points.forEach((d, i) => {
d.tx = d.x;
d.ty = d.y;
d.colorEnd = d.color;
});
// create the regl function with the new start and end points
const drawPoints = createDrawPoints(points);
// start an animation loop
let startTime = null; // in seconds
const frameLoop = regl.frame(({ time }) => {
// keep track of start time so we can get time elapsed
// this is important since time doesn't reset when starting new animations
if (startTime === null) {
startTime = time;
}
// clear the buffer
regl.clear({
// background color (black)
color: [0, 0, 0, 1],
depth: 1,
});
// draw the points using our created regl func
// note that the arguments are available via `regl.prop`.
drawPoints({
pointWidth,
stageWidth: width,
stageHeight: height,
duration,
startTime,
});
// if we have exceeded the maximum duration, move on to the next animation
if (time - startTime > duration / 1000) {
console.log('done animating, moving to next layout');
// cancel this loop, we are going to start another
frameLoop.cancel();
// increment to use next layout function
currentLayout = (currentLayout + 1) % layouts.length;
// start a new animation loop with next layout function
animate(layouts[currentLayout], points);
}
});
}
// create initial set of points
// initialize with all the points in the middle of the screen and black
const points = d3.range(numPoints).map(i => ({
tx: width / 2,
ty: height / 2,
colorEnd: [0, 0, 0],
}));
// start the initial animation
animate(layouts[currentLayout], points);
}
// initialize regl
createREGL({
// callback when regl is initialized
onDone: main,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment