Skip to content

Instantly share code, notes, and snippets.

@arscan
Forked from pbeshai/.block
Last active May 7, 2017 13:34
Show Gist options
  • Save arscan/f50c9218ea88cee2b6d917f67566ea61 to your computer and use it in GitHub Desktop.
Save arscan/f50c9218ea88cee2b6d917f67566ea61 to your computer and use it in GitHub Desktop.
Animate 100,000 points with regl
license: mit
height: 720
border: no

Animate 100,000 points with regl

Blog to follow eventually... There's also a cool way to do this with textures that I will create one day, but hopefully this gives you an idea of how to play around with particles in regl. Code is similar to my canvas example.

/**
* Given a set of points, lay them out in a phyllotaxis layout.
* Mutates the `points` passed in by updating the x and y values.
*
* @param {Object[]} points The array of points to update. Will get `x` and `y` set.
* @param {Number} pointWidth The size in pixels of the point's width. Should also include margin.
* @param {Number} xOffset The x offset to apply to all points
* @param {Number} yOffset The y offset to apply to all points
*
* @return {Object[]} points with modified x and y
*/
function phyllotaxisLayout(points, pointWidth, xOffset = 0, yOffset = 0, iOffset = 0) {
// theta determines the spiral of the layout
const theta = Math.PI * (3 - Math.sqrt(5));
const pointRadius = pointWidth / 2;
points.forEach((point, i) => {
const index = (i + iOffset) % points.length;
const phylloX = pointRadius * Math.sqrt(index) * Math.cos(index * theta);
const phylloY = pointRadius * Math.sqrt(index) * Math.sin(index * theta);
point.x = xOffset + phylloX - pointRadius;
point.y = yOffset + phylloY - pointRadius;
});
return points;
}
/**
* Given a set of points, lay them out in a grid.
* Mutates the `points` passed in by updating the x and y values.
*
* @param {Object[]} points The array of points to update. Will get `x` and `y` set.
* @param {Number} pointWidth The size in pixels of the point's width. Should also include margin.
* @param {Number} gridWidth The width of the grid of points
*
* @return {Object[]} points with modified x and y
*/
function gridLayout(points, pointWidth, gridWidth) {
const pointHeight = pointWidth;
const pointsPerRow = Math.floor(gridWidth / pointWidth);
const numRows = points.length / pointsPerRow;
points.forEach((point, i) => {
point.x = pointWidth * (i % pointsPerRow);
point.y = pointHeight * Math.floor(i / pointsPerRow);
});
return points;
}
/**
* Given a set of points, lay them out randomly.
* Mutates the `points` passed in by updating the x and y values.
*
* @param {Object[]} points The array of points to update. Will get `x` and `y` set.
* @param {Number} pointWidth The size in pixels of the point's width. Should also include margin.
* @param {Number} width The width of the area to place them in
* @param {Number} height The height of the area to place them in
*
* @return {Object[]} points with modified x and y
*/
function randomLayout(points, pointWidth, width, height) {
points.forEach((point, i) => {
point.x = Math.random() * (width - pointWidth);
point.y = Math.random() * (height - pointWidth);
});
return points;
}
/**
* Given a set of points, lay them out in a sine wave.
* Mutates the `points` passed in by updating the x and y values.
*
* @param {Object[]} points The array of points to update. Will get `x` and `y` set.
* @param {Number} pointWidth The size in pixels of the point's width. Should also include margin.
* @param {Number} width The width of the area to place them in
* @param {Number} height The height of the area to place them in
*
* @return {Object[]} points with modified x and y
*/
function sineLayout(points, pointWidth, width, height) {
const amplitude = 0.3 * (height / 2);
const yOffset = height / 2;
const periods = 3;
const yScale = d3.scaleLinear()
.domain([0, points.length - 1])
.range([0, periods * 2 * Math.PI]);
points.forEach((point, i) => {
point.x = (i / points.length) * (width - pointWidth);
point.y = amplitude * Math.sin(yScale(i)) + yOffset;
});
return points;
}
/**
* Given a set of points, lay them out in a spiral.
* Mutates the `points` passed in by updating the x and y values.
*
* @param {Object[]} points The array of points to update. Will get `x` and `y` set.
* @param {Number} pointWidth The size in pixels of the point's width. Should also include margin.
* @param {Number} width The width of the area to place them in
* @param {Number} height The height of the area to place them in
*
* @return {Object[]} points with modified x and y
*/
function spiralLayout(points, pointWidth, width, height) {
const amplitude = 0.3 * (height / 2);
const xOffset = width / 2;
const yOffset = height / 2;
const periods = 20;
const rScale = d3.scaleLinear()
.domain([0, points.length -1])
.range([0, Math.min(width / 2, height / 2) - pointWidth]);
const thetaScale = d3.scaleLinear()
.domain([0, points.length - 1])
.range([0, periods * 2 * Math.PI]);
points.forEach((point, i) => {
point.x = rScale(i) * Math.cos(thetaScale(i)) + xOffset
point.y = rScale(i) * Math.sin(thetaScale(i)) + yOffset;
});
return points;
}
/**
* Generate an object array of `numPoints` length with unique IDs
* and assigned colors
*/
function createPoints(numPoints, pointWidth, width, height) {
const colorScale = d3.scaleSequential(d3.interpolateViridis)
.domain([numPoints - 1, 0]);
const points = d3.range(numPoints).map(id => ({
id,
color: colorScale(id),
}));
return randomLayout(points, pointWidth, width, height);
}
function main(t,n){function o(t){var n=d3.scaleLinear().domain([0,1]).range([.4,1]);return function(o){var i=d3.rgb(t(n(o)));return[i.r/255,i.g/255,i.b/255]}}function i(t){var o=n({frag:"\n\t\t precision highp float;\n\t\t\tvarying vec3 fragColor;\n\t\t\tvoid main() {\n\t\t\t\tgl_FragColor = vec4(fragColor, 1);\n\t\t\t}\n\t\t\t",vert:"\n\t\t\tattribute vec2 positionStart;\n\t\t\tattribute vec2 positionEnd;\n\t\t\tattribute float index;\n\t\t\tattribute vec3 colorStart;\n\t\t\tattribute vec3 colorEnd;\n\n\t\t\tvarying vec3 fragColor;\n\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\t\t\tuniform float delayByIndex;\n\t\t\tvoid main() {\n\t\t\t\tgl_PointSize = pointWidth;\n\n\t\t\t\tfloat delay = delayByIndex * index;\n\t float 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 // still delaying before animating\n\t } else if (elapsed < delay) {\n\t t = 0.0;\n\t } else {\n\t t = 2.0 * ((elapsed - delay) / duration);\n\n\t // cubic easing (cubicInOut) -- note there are glslify things for this toPhyllotaxis\n\t // this is copied from d3.\n\t t = (t <= 1.0 ? t * t * t : (t -= 2.0) * t * t + 2.0) / 2.0;\n\n\t if (t > 1.0) {\n\t t = 1.0;\n\t }\n\t }\n\n\t\t\t\t// interpolate position\n\t float x = mix(positionStart[0], positionEnd[0], t);\n\t float y = mix(positionStart[1], positionEnd[1], t);\n\n\t // interpolate color\n\t fragColor = mix(colorStart, colorEnd, t);\n\n\t\t\t\t// scale to normalized device coordinates (-1, -1) to (1, 1)\n\t gl_Position = vec4(\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 (needed for d3.zoom)\n\t\t -(2.0 * ((y / stageHeight) - 0.5)),\n\t\t 0.0,\n\t\t 1.0);\n\t\t\t}\n\t\t\t",attributes:{positionStart:t.map(function(t){return[t.sx,t.sy]}),positionEnd:t.map(function(t){return[t.tx,t.ty]}),colorStart:t.map(function(t){return t.colorStart}),colorEnd:t.map(function(t){return t.colorEnd}),index:d3.range(t.length)},uniforms:{pointWidth:n.prop("pointWidth"),stageWidth:n.prop("stageWidth"),stageHeight:n.prop("stageHeight"),delayByIndex:n.prop("delayByIndex"),duration:n.prop("duration"),elapsed:function(t,n){var o=t.time,i=n.startTime;return void 0===i&&(i=0),1e3*(o-i)}},count:t.length,primitive:"points"});return o}function e(t,o){console.log("animating with new layout"),o.forEach(function(t){t.sx=t.tx,t.sy=t.ty,t.colorStart=t.colorEnd}),t(o);var r=S[b];o.forEach(function(t,n){t.tx=t.x,t.ty=t.y,t.colorEnd=r(n/o.length)});var d=i(o);a=n.frame(function(t){var i=t.time;null===E&&(E=i),n.clear({color:[0,0,0,1],depth:1}),d({pointWidth:l,stageWidth:u,stageHeight:s,duration:c,delayByIndex:f,startTime:E}),i-E>g/1e3&&(console.log("done animating, moving to next layout"),a.cancel(),x=(x+1)%v.length,E=null,b=(b+1)%S.length,e(v[x],o))})}var a,r=1e5,l=4,d=1,u=window.innerWidth,s=window.innerHeight,c=1500,f=500/r,g=c+f*r,p=function(t){return phyllotaxisLayout(t,l+d,u/2,s/2)},h=function(t){return gridLayout(t,l+d,u)},m=function(t){return sineLayout(t,l+d,u,s)},y=function(t){return spiralLayout(t,l+d,u,s)},v=[p,h,m,y],x=0,E=null,S=[d3.scaleSequential(d3.interpolateViridis),d3.scaleSequential(d3.interpolateMagma),d3.scaleSequential(d3.interpolateInferno),d3.scaleSequential(d3.interpolateCool)].map(o),b=0,w=createPoints(r,l,u,s);window.points=w,w.forEach(function(t,n){t.tx=u/2,t.ty=s/2,t.colorEnd=S[b](n/w.length)}),e(v[x],w)}regl({extensions:["OES_texture_float"],onDone:main});
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNjcmlwdC5qcyJdLCJuYW1lcyI6WyJtYWluIiwiZXJyIiwicmVnbCIsIndyYXBDb2xvclNjYWxlIiwic2NhbGUiLCJjb25zdCIsInRTY2FsZSIsImQzIiwic2NhbGVMaW5lYXIiLCJkb21haW4iLCJyYW5nZSIsInQiLCJyZ2IiLCJyIiwiZyIsImIiLCJjcmVhdGVEcmF3UG9pbnRzIiwicG9pbnRzIiwiZHJhd1BvaW50cyIsImZyYWciLCJ2ZXJ0IiwiYXR0cmlidXRlcyIsInBvc2l0aW9uU3RhcnQiLCJtYXAiLCJkIiwic3giLCJzeSIsInBvc2l0aW9uRW5kIiwidHgiLCJ0eSIsImNvbG9yU3RhcnQiLCJjb2xvckVuZCIsImluZGV4IiwibGVuZ3RoIiwidW5pZm9ybXMiLCJwb2ludFdpZHRoIiwicHJvcCIsInN0YWdlV2lkdGgiLCJzdGFnZUhlaWdodCIsImRlbGF5QnlJbmRleCIsImR1cmF0aW9uIiwiZWxhcHNlZCIsInJlZiIsInJlZiQxIiwidGltZSIsInN0YXJ0VGltZSIsImNvdW50IiwicHJpbWl0aXZlIiwiYW5pbWF0ZSIsImxheW91dCIsImNvbnNvbGUiLCJsb2ciLCJmb3JFYWNoIiwiY29sb3JTY2FsZSIsImNvbG9yU2NhbGVzIiwiY3VycmVudENvbG9yU2NhbGUiLCJpIiwieCIsInkiLCJmcmFtZUxvb3AiLCJmcmFtZSIsImNsZWFyIiwiY29sb3IiLCJkZXB0aCIsIndpZHRoIiwiaGVpZ2h0IiwibWF4RHVyYXRpb24iLCJjYW5jZWwiLCJjdXJyZW50TGF5b3V0IiwibGF5b3V0cyIsIm51bVBvaW50cyIsInBvaW50TWFyZ2luIiwid2luZG93IiwiaW5uZXJXaWR0aCIsImlubmVySGVpZ2h0IiwidG9QaHlsbG90YXhpcyIsInBoeWxsb3RheGlzTGF5b3V0IiwidG9HcmlkIiwiZ3JpZExheW91dCIsInRvU2luZSIsInNpbmVMYXlvdXQiLCJ0b1NwaXJhbCIsInNwaXJhbExheW91dCIsInNjYWxlU2VxdWVudGlhbCIsImludGVycG9sYXRlVmlyaWRpcyIsImludGVycG9sYXRlTWFnbWEiLCJpbnRlcnBvbGF0ZUluZmVybm8iLCJpbnRlcnBvbGF0ZUNvb2wiLCJjcmVhdGVQb2ludHMiLCJleHRlbnNpb25zIiwib25Eb25lIl0sIm1hcHBpbmdzIjoiQUFBQSxRQUFTQSxNQUFLQyxFQUFLQyxHQXVCbEIsUUFBU0MsR0FBZUMsR0FDdkJDLEdBQU1DLEdBQVdDLEdBQUNDLGNBQWNDLFFBQVMsRUFBSSxJQUFFQyxPQUFPLEdBQU8sR0FDN0QsT0FBTyxVQUFBQyxHQUNOTixHQUFNTyxHQUFRTCxHQUFDSyxJQUFJUixFQUFNRSxFQUFTSyxJQUNsQyxRQUFRQyxFQUFJQyxFQUFJLElBQUtELEVBQUlFLEVBQUksSUFBS0YsRUFBSUcsRUFBSSxNQWE1QyxRQUFTQyxHQUFpQkMsR0FDekJaLEdBQU1hLEdBQWFoQixHQUNsQmlCLEtBQU0sa0pBUU5DLEtBNkRDLGlwREFKREMsWUF1REFDLGNBQVdMLEVBQUFNLElBQUEsU0FBQUMsR0FBQSxPQUFBQSxFQUFBQyxHQUFBRCxFQUFBRSxNQXJEVkMsWUFBYVYsRUFBT00sSUFBSSxTQUFBQyxHQUFFLE9BQUlBLEVBQUVJLEdBQUlKLEVBQUVLLE1BdUR0Q0MsV0FBWWIsRUFBT00sSUFBQSxTQUFBQyxHQUFBLE1BQUFBLEdBQUFNLGFBQ25CQyxTQUFRZCxFQUFBTSxJQUFBLFNBQUFDLEdBQUEsTUFBQUEsR0FBQU8sV0FDUkMsTUFBRXpCLEdBQUFHLE1BQUFPLEVBQUFnQixTQW5ESEMsVUF1RENDLFdBQVlqQyxFQUFLa0MsS0FBQSxjQUNqQkMsV0FBV25DLEVBQUVrQyxLQUFNLGNBQ25CRSxZQUFBcEMsRUFBUWtDLEtBQUEsZUFDUkcsYUFBQXJDLEVBQUFrQyxLQUFBLGdCQUNBSSxTQUFBdEMsRUFBQWtDLEtBQVMsWUFHTkssUUFBTyxTQUFBQyxFQUFhQyxNQUFiQyxHQUFTRixFQUFBRSx5Q0FBa0IsR0FBTyxLQUFBQSxFQUFBQyxLQW5EN0NDLE1BQU83QixFQUFPZ0IsT0FDZGMsVUFzREMsVUFuREYsT0FzREU3QixHQWxESCxRQUFTOEIsR0FBUUMsRUFBUWhDLEdBQ3hCaUMsUUFBUUMsSUFBSSw2QkF3RGI5QyxFQUFNK0MsUUFBUyxTQUFBNUIsR0FDZkEsRUFBQUMsR0FBT0QsRUFBQUksR0FDUEosRUFBQUUsR0FBT0YsRUFBQUssR0FDTEwsRUFBQ00sV0FBY04sRUFBQ08sV0FsRGpCa0IsRUFBT2hDLEVBR1BaLElBQU1nRCxHQUFhQyxFQUFZQyxFQUMvQnRDLEdBQU9tQyxRQUFRLFNBQUM1QixFQUFHZ0MsR0FDbEJoQyxFQUFFSSxHQUFLSixFQUFFaUMsRUF1RFpqQyxFQUFLSyxHQUFBTCxFQUFBa0MsRUFyREZsQyxFQUFFTyxTQUFXc0IsRUFBV0csRUFBSXZDLEVBQU9nQixTQUlwQzVCLElBQU1hLEdBQWFGLEVBQWlCQyxFQXdEcEMwQyxHQUFZekQsRUFBQTBELE1BQUEsU0FBQWxCLE1BQUFFLEdBQUFGLEVBQUFFLElBQ1gsUUFBQUMsSUFyRENBLEVBQVlELEdBR2IxQyxFQUFLMkQsT0FFSkMsT0FBUSxFQUFHLEVBQUcsRUFBRyxHQUNqQkMsTUFBTyxJQUdSN0MsR0FDQ2lCLFdBQUFBLEVBQ0FFLFdBQVkyQixFQUNaMUIsWUFBYTJCLEVBQ2J6QixTQUFBQSxFQUNBRCxhQUFBQSxFQUNBTSxVQUFBQSxJQUdHRCxFQUFPQyxFQUFhcUIsRUFBYyxNQUNyQ2hCLFFBQVFDLElBQUkseUNBQ1pRLEVBQVVRLFNBRVZDLEdBQWlCQSxFQUFnQixHQUFLQyxFQUFRcEMsT0FDOUNZLEVBQVksS0FDWlUsR0FBcUJBLEVBQW9CLEdBQUtELEVBQVlyQixPQUMxRGUsRUFBUXFCLEVBQVFELEdBQWdCbkQsTUF2TG5DWixHQWtCSXNELEdBbEJFVyxFQUFZLElBQ1puQyxFQUFlLEVBQ2ZvQyxFQUFnQixFQUNoQlAsRUFBUVEsT0FBT0MsV0FDZlIsRUFBU08sT0FBT0UsWUFDaEJsQyxFQUFXLEtBQ1hELEVBQWUsSUFBTStCLEVBQ3JCSixFQUFjMUIsRUFBV0QsRUFBZStCLEVBRXhDSyxFQUFnQixTQUFBMUQsR0FBQyxNQUFBMkQsbUJBQVczRCxFQUFBa0IsRUFBMEJvQyxFQUFhUCxFQUFBLEVBQVdDLEVBQU8sSUFDckZZLEVBQVMsU0FBQTVELEdBQUMsTUFBQTZELFlBQVE3RCxFQUFHa0IsRUFBbUJvQyxFQUFhUCxJQUNyRGUsRUFBUyxTQUFBOUQsR0FBQyxNQUFBK0QsWUFBUS9ELEVBQUdrQixFQUFtQm9DLEVBQWFQLEVBQUFDLElBQ3JEZ0IsRUFBVyxTQUFBaEUsR0FBQyxNQUFBaUUsY0FBV2pFLEVBQUFrQixFQUFxQm9DLEVBQWFQLEVBQUFDLElBRXpESSxHQUFXTSxFQUFlRSxFQUFRRSxFQUFRRSxHQUM1Q2IsRUFBZ0IsRUFDaEJ2QixFQUFZLEtBY1ZTLEdBQ0wvQyxHQUFHNEUsZ0JBQWdCNUUsR0FBRzZFLG9CQUN0QjdFLEdBQUc0RSxnQkFBZ0I1RSxHQUFHOEUsa0JBQ3RCOUUsR0FBRzRFLGdCQUFnQjVFLEdBQUcrRSxvQkFDdEIvRSxHQUFHNEUsZ0JBQWdCNUUsR0FBR2dGLGtCQUNyQmhFLElBQUlwQixHQUNGb0QsRUFBb0IsRUEwSmxCdEMsRUFBU3VFLGFBQWFsQixFQUFXbkMsRUFBWTZCLEVBQU9DLEVBQzFETyxRQUFPdkQsT0FBU0EsRUFDaEJBLEVBQU9tQyxRQUFRLFNBQUM1QixFQUFHZ0MsR0FDbEJoQyxFQUFFSSxHQUFLb0MsRUFBUSxFQUNmeEMsRUFBRUssR0FBS29DLEVBQVMsRUFDaEJ6QyxFQUFFTyxTQUFXdUIsRUFBWUMsR0FBbUJDLEVBQUl2QyxFQUFPZ0IsVUFHeERlLEVBQVFxQixFQUFRRCxHQUFnQm5ELEdBS2pDZixNQUVFdUYsWUFDRSxxQkFJRkMsT0FBUTFGIiwiZmlsZSI6InNjcmlwdC5qcyIsInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIG1haW4oZXJyLCByZWdsKSB7XG5cdGNvbnN0IG51bVBvaW50cyA9IDEwMDAwMDtcblx0Y29uc3QgcG9pbnRXaWR0aCA9IDQ7XG5cdGNvbnN0IHBvaW50TWFyZ2luID0gMTtcblx0Y29uc3Qgd2lkdGggPSB3aW5kb3cuaW5uZXJXaWR0aDtcblx0Y29uc3QgaGVpZ2h0ID0gd2luZG93LmlubmVySGVpZ2h0O1xuXHRjb25zdCBkdXJhdGlvbiA9IDE1MDA7XG5cdGNvbnN0IGRlbGF5QnlJbmRleCA9IDUwMCAvIG51bVBvaW50cztcblx0Y29uc3QgbWF4RHVyYXRpb24gPSBkdXJhdGlvbiArIGRlbGF5QnlJbmRleCAqIG51bVBvaW50czsgLy8gaW5jbHVkZSBtYXggZGVsYXkgaW4gaGVyZVxuXG5cdGNvbnN0IHRvUGh5bGxvdGF4aXMgPSAocG9pbnRzKSA9PiBwaHlsbG90YXhpc0xheW91dChwb2ludHMsIHBvaW50V2lkdGggKyBwb2ludE1hcmdpbiwgd2lkdGggLyAyLCBoZWlnaHQgLyAyKTtcblx0Y29uc3QgdG9HcmlkID0gKHBvaW50cykgPT4gZ3JpZExheW91dChwb2ludHMsIHBvaW50V2lkdGggKyBwb2ludE1hcmdpbiwgd2lkdGgpO1xuXHRjb25zdCB0b1NpbmUgPSAocG9pbnRzKSA9PiBzaW5lTGF5b3V0KHBvaW50cywgcG9pbnRXaWR0aCArIHBvaW50TWFyZ2luLCB3aWR0aCwgaGVpZ2h0KTtcblx0Y29uc3QgdG9TcGlyYWwgPSAocG9pbnRzKSA9PiBzcGlyYWxMYXlvdXQocG9pbnRzLCBwb2ludFdpZHRoICsgcG9pbnRNYXJnaW4sIHdpZHRoLCBoZWlnaHQpO1xuXG5cdGNvbnN0IGxheW91dHMgPSBbdG9QaHlsbG90YXhpcywgdG9HcmlkLCB0b1NpbmUsIHRvU3BpcmFsXTtcblx0bGV0IGN1cnJlbnRMYXlvdXQgPSAwO1xuXHRsZXQgc3RhcnRUaW1lID0gbnVsbDsgLy8gaW4gc2Vjb25kc1xuXHQvLyBzdGFydCBhbmltYXRpb24gbG9vcCAobm90ZTogdGltZSBpcyBpbiBzZWNvbmRzKVxuXHRsZXQgZnJhbWVMb29wO1xuXG5cdC8vIHdyYXAgZDMgY29sb3Igc2NhbGVzIHNvIHRoZXkgcHJvZHVjZSB2ZWMzcyB3aXRoIHZhbHVlcyAwLTFcblx0Ly8gYWxzbyBsaW1pdCB0aGUgdCB2YWx1ZSB0byByZW1vdmUgZGFya2VzdCBjb2xvclxuXHRmdW5jdGlvbiB3cmFwQ29sb3JTY2FsZShzY2FsZSkge1xuXHRcdGNvbnN0IHRTY2FsZSA9IGQzLnNjYWxlTGluZWFyKCkuZG9tYWluKFswLCAxXSkucmFuZ2UoWzAuNCwgMV0pO1xuXHRcdHJldHVybiB0ID0+IHtcblx0XHRcdGNvbnN0IHJnYiA9IGQzLnJnYihzY2FsZSh0U2NhbGUodCkpKTtcblx0XHRcdHJldHVybiBbcmdiLnIgLyAyNTUsIHJnYi5nIC8gMjU1LCByZ2IuYiAvIDI1NV07XG5cdFx0fTtcblx0fVxuXG5cdGNvbnN0IGNvbG9yU2NhbGVzID0gW1xuXHRcdGQzLnNjYWxlU2VxdWVudGlhbChkMy5pbnRlcnBvbGF0ZVZpcmlkaXMpLFxuXHRcdGQzLnNjYWxlU2VxdWVudGlhbChkMy5pbnRlcnBvbGF0ZU1hZ21hKSxcblx0XHRkMy5zY2FsZVNlcXVlbnRpYWwoZDMuaW50ZXJwb2xhdGVJbmZlcm5vKSxcblx0XHRkMy5zY2FsZVNlcXVlbnRpYWwoZDMuaW50ZXJwb2xhdGVDb29sKSxcblx0XS5tYXAod3JhcENvbG9yU2NhbGUpO1xuXHRsZXQgY3VycmVudENvbG9yU2NhbGUgPSAwO1xuXG5cdC8vIGZ1bmN0aW9uIHRvIGNvbXBpbGUgYSBkcmF3IHBvaW50cyByZWdsIGZ1bmNcblx0ZnVuY3Rpb24gY3JlYXRlRHJhd1BvaW50cyhwb2ludHMpIHtcblx0XHRjb25zdCBkcmF3UG9pbnRzID0gcmVnbCh7XG5cdFx0XHRmcmFnOiBgXG5cdFx0ICBwcmVjaXNpb24gaGlnaHAgZmxvYXQ7XG5cdFx0XHR2YXJ5aW5nIHZlYzMgZnJhZ0NvbG9yO1xuXHRcdFx0dm9pZCBtYWluKCkge1xuXHRcdFx0XHRnbF9GcmFnQ29sb3IgPSB2ZWM0KGZyYWdDb2xvciwgMSk7XG5cdFx0XHR9XG5cdFx0XHRgLFxuXG5cdFx0XHR2ZXJ0OiBgXG5cdFx0XHRhdHRyaWJ1dGUgdmVjMiBwb3NpdGlvblN0YXJ0O1xuXHRcdFx0YXR0cmlidXRlIHZlYzIgcG9zaXRpb25FbmQ7XG5cdFx0XHRhdHRyaWJ1dGUgZmxvYXQgaW5kZXg7XG5cdFx0XHRhdHRyaWJ1dGUgdmVjMyBjb2xvclN0YXJ0O1xuXHRcdFx0YXR0cmlidXRlIHZlYzMgY29sb3JFbmQ7XG5cblx0XHRcdHZhcnlpbmcgdmVjMyBmcmFnQ29sb3I7XG5cblx0XHRcdHVuaWZvcm0gZmxvYXQgcG9pbnRXaWR0aDtcblx0XHRcdHVuaWZvcm0gZmxvYXQgc3RhZ2VXaWR0aDtcblx0XHRcdHVuaWZvcm0gZmxvYXQgc3RhZ2VIZWlnaHQ7XG5cdFx0XHR1bmlmb3JtIGZsb2F0IGVsYXBzZWQ7XG5cdFx0XHR1bmlmb3JtIGZsb2F0IGR1cmF0aW9uO1xuXHRcdFx0dW5pZm9ybSBmbG9hdCBkZWxheUJ5SW5kZXg7XG5cdFx0XHR2b2lkIG1haW4oKSB7XG5cdFx0XHRcdGdsX1BvaW50U2l6ZSA9IHBvaW50V2lkdGg7XG5cblx0XHRcdFx0ZmxvYXQgZGVsYXkgPSBkZWxheUJ5SW5kZXggKiBpbmRleDtcblx0ICAgICAgZmxvYXQgdDtcblxuXHQgICAgICAvLyBkcmF3aW5nIHdpdGhvdXQgYW5pbWF0aW9uLCBzbyBzaG93IGVuZCBzdGF0ZSBpbW1lZGlhdGVseVxuXHQgICAgICBpZiAoZHVyYXRpb24gPT0gMC4wKSB7XG5cdCAgICAgICAgdCA9IDEuMDtcblxuXHQgICAgICAvLyBzdGlsbCBkZWxheWluZyBiZWZvcmUgYW5pbWF0aW5nXG5cdCAgICAgIH0gZWxzZSBpZiAoZWxhcHNlZCA8IGRlbGF5KSB7XG5cdCAgICAgICAgdCA9IDAuMDtcblx0ICAgICAgfSBlbHNlIHtcblx0ICAgICAgICB0ID0gMi4wICogKChlbGFwc2VkIC0gZGVsYXkpIC8gZHVyYXRpb24pO1xuXG5cdCAgICAgICAgLy8gY3ViaWMgZWFzaW5nIChjdWJpY0luT3V0KSAtLSBub3RlIHRoZXJlIGFyZSBnbHNsaWZ5IHRoaW5ncyBmb3IgdGhpcyB0b1BoeWxsb3RheGlzXG5cdCAgICAgICAgLy8gdGhpcyBpcyBjb3BpZWQgZnJvbSBkMy5cblx0ICAgICAgICB0ID0gKHQgPD0gMS4wID8gdCAqIHQgKiB0IDogKHQgLT0gMi4wKSAqIHQgKiB0ICsgMi4wKSAvIDIuMDtcblxuXHQgICAgICAgIGlmICh0ID4gMS4wKSB7XG5cdCAgICAgICAgICB0ID0gMS4wO1xuXHQgICAgICAgIH1cblx0ICAgICAgfVxuXG5cdFx0XHRcdC8vIGludGVycG9sYXRlIHBvc2l0aW9uXG5cdCAgICAgIGZsb2F0IHggPSBtaXgocG9zaXRpb25TdGFydFswXSwgcG9zaXRpb25FbmRbMF0sIHQpO1xuXHQgICAgICBmbG9hdCB5ID0gbWl4KHBvc2l0aW9uU3RhcnRbMV0sIHBvc2l0aW9uRW5kWzFdLCB0KTtcblxuXHQgICAgICAvLyBpbnRlcnBvbGF0ZSBjb2xvclxuXHQgICAgICBmcmFnQ29sb3IgPSBtaXgoY29sb3JTdGFydCwgY29sb3JFbmQsIHQpO1xuXG5cdFx0XHRcdC8vIHNjYWxlIHRvIG5vcm1hbGl6ZWQgZGV2aWNlIGNvb3JkaW5hdGVzICgtMSwgLTEpIHRvICgxLCAxKVxuXHQgICAgICBnbF9Qb3NpdGlvbiA9IHZlYzQoXG5cdFx0ICAgICAgMi4wICogKCh4IC8gc3RhZ2VXaWR0aCkgLSAwLjUpLFxuXHRcdCAgICAgIC8vIGludmVydCB5IHNpbmNlIHdlIHRoaW5rIFswLDBdIGlzIGJvdHRvbSBsZWZ0IGluIHBpeGVsIHNwYWNlIChuZWVkZWQgZm9yIGQzLnpvb20pXG5cdFx0ICAgICAgLSgyLjAgKiAoKHkgLyBzdGFnZUhlaWdodCkgLSAwLjUpKSxcblx0XHQgICAgICAwLjAsXG5cdFx0ICAgICAgMS4wKTtcblx0XHRcdH1cblx0XHRcdGAsXG5cblx0XHRcdGF0dHJpYnV0ZXM6IHtcblx0XHRcdFx0cG9zaXRpb25TdGFydDogcG9pbnRzLm1hcChkID0+IFtkLnN4LCBkLnN5XSksXG5cdFx0XHRcdHBvc2l0aW9uRW5kOiBwb2ludHMubWFwKGQgPT4gW2QudHgsIGQudHldKSxcblx0XHRcdFx0Y29sb3JTdGFydDogcG9pbnRzLm1hcChkID0+IGQuY29sb3JTdGFydCksXG5cdFx0XHRcdGNvbG9yRW5kOiBwb2ludHMubWFwKGQgPT4gZC5jb2xvckVuZCksXG5cdFx0XHRcdGluZGV4OiBkMy5yYW5nZShwb2ludHMubGVuZ3RoKSxcblx0XHRcdH0sXG5cblx0XHRcdHVuaWZvcm1zOiB7XG5cdFx0XHRcdHBvaW50V2lkdGg6IHJlZ2wucHJvcCgncG9pbnRXaWR0aCcpLFxuXHRcdFx0XHRzdGFnZVdpZHRoOiByZWdsLnByb3AoJ3N0YWdlV2lkdGgnKSxcblx0XHRcdFx0c3RhZ2VIZWlnaHQ6IHJlZ2wucHJvcCgnc3RhZ2VIZWlnaHQnKSxcblx0XHRcdFx0ZGVsYXlCeUluZGV4OiByZWdsLnByb3AoJ2RlbGF5QnlJbmRleCcpLFxuXHQgICAgICBkdXJhdGlvbjogcmVnbC5wcm9wKCdkdXJhdGlvbicpLFxuXG5cdCAgICAgIC8vIHRpbWUgaW4gbWlsbGlzZWNvbmRzIHNpbmNlIHRoZSBwcm9wIHN0YXJ0VGltZSAoaS5lLiB0aW1lIGVsYXBzZWQpXG5cdCAgICAgIGVsYXBzZWQ6ICh7IHRpbWUgfSwgeyBzdGFydFRpbWUgPSAwIH0pID0+ICh0aW1lIC0gc3RhcnRUaW1lKSAqIDEwMDAsXG5cdFx0XHR9LFxuXG5cdFx0XHRjb3VudDogcG9pbnRzLmxlbmd0aCxcblx0XHRcdHByaW1pdGl2ZTogJ3BvaW50cycsXG5cdFx0fSk7XG5cblx0XHRyZXR1cm4gZHJhd1BvaW50cztcblx0fVxuXG5cblx0ZnVuY3Rpb24gYW5pbWF0ZShsYXlvdXQsIHBvaW50cykge1xuXHRcdGNvbnNvbGUubG9nKCdhbmltYXRpbmcgd2l0aCBuZXcgbGF5b3V0Jyk7XG5cdFx0Ly8gbWFrZSBwcmV2aW91cyBlbmQgdGhlIG5ldyBiZWdpbm5pbmdcblx0XHRwb2ludHMuZm9yRWFjaChkID0+IHtcblx0XHRcdGQuc3ggPSBkLnR4O1xuXHRcdFx0ZC5zeSA9IGQudHk7XG5cdFx0XHRkLmNvbG9yU3RhcnQgPSBkLmNvbG9yRW5kO1xuXHRcdH0pO1xuXG5cdFx0Ly8gbGF5b3V0IHBvaW50c1xuXHRcdGxheW91dChwb2ludHMpO1xuXG5cdFx0Ly8gY29weSBsYXlvdXQgeCB5IHRvIGVuZCBwb3NpdGlvbnNcblx0XHRjb25zdCBjb2xvclNjYWxlID0gY29sb3JTY2FsZXNbY3VycmVudENvbG9yU2NhbGVdO1xuXHRcdHBvaW50cy5mb3JFYWNoKChkLCBpKSA9PiB7XG5cdFx0XHRkLnR4ID0gZC54O1xuXHRcdFx0ZC50eSA9IGQueTtcblx0XHRcdGQuY29sb3JFbmQgPSBjb2xvclNjYWxlKGkgLyBwb2ludHMubGVuZ3RoKVxuXHRcdH0pO1xuXG5cdFx0Ly8gY3JlYXRlIHRoZSByZWdsIGZ1bmN0aW9uIHdpdGggdGhlIG5ldyBzdGFydCBhbmQgZW5kIHBvaW50c1xuXHRcdGNvbnN0IGRyYXdQb2ludHMgPSBjcmVhdGVEcmF3UG9pbnRzKHBvaW50cyk7XG5cblx0XHRmcmFtZUxvb3AgPSByZWdsLmZyYW1lKCh7IHRpbWUgfSkgPT4ge1xuXHRcdFx0aWYgKHN0YXJ0VGltZSA9PT0gbnVsbCkge1xuXHRcdFx0XHRzdGFydFRpbWUgPSB0aW1lO1xuXHRcdFx0fVxuXG5cdFx0XHRyZWdsLmNsZWFyKHtcblx0XHRcdFx0Ly8gYmFja2dyb3VuZCBjb2xvciAoYmxhY2spXG5cdFx0XHRcdGNvbG9yOiBbMCwgMCwgMCwgMV0sXG5cdFx0XHRcdGRlcHRoOiAxLFxuXHRcdFx0fSk7XG5cblx0XHRcdGRyYXdQb2ludHMoe1xuXHRcdFx0XHRwb2ludFdpZHRoLFxuXHRcdFx0XHRzdGFnZVdpZHRoOiB3aWR0aCxcblx0XHRcdFx0c3RhZ2VIZWlnaHQ6IGhlaWdodCxcblx0XHRcdFx0ZHVyYXRpb24sXG5cdFx0XHRcdGRlbGF5QnlJbmRleCxcblx0XHRcdFx0c3RhcnRUaW1lLFxuXHRcdFx0fSk7XG5cblx0XHRcdGlmICh0aW1lIC0gc3RhcnRUaW1lID4gKG1heER1cmF0aW9uIC8gMTAwMCkpIHtcblx0XHRcdFx0Y29uc29sZS5sb2coJ2RvbmUgYW5pbWF0aW5nLCBtb3ZpbmcgdG8gbmV4dCBsYXlvdXQnKTtcblx0XHRcdFx0ZnJhbWVMb29wLmNhbmNlbCgpO1xuXG5cdFx0XHRcdGN1cnJlbnRMYXlvdXQgPSAoY3VycmVudExheW91dCArIDEpICUgbGF5b3V0cy5sZW5ndGg7XG5cdFx0XHRcdHN0YXJ0VGltZSA9IG51bGw7XG5cdFx0XHRcdGN1cnJlbnRDb2xvclNjYWxlID0gKGN1cnJlbnRDb2xvclNjYWxlICsgMSkgJSBjb2xvclNjYWxlcy5sZW5ndGg7XG5cdFx0XHRcdGFuaW1hdGUobGF5b3V0c1tjdXJyZW50TGF5b3V0XSwgcG9pbnRzKTtcblx0XHRcdH1cblx0XHR9KTtcblx0fVxuXG5cblx0Ly8gY3JlYXRlIGluaXRpYWwgc2V0IG9mIHBvaW50c1xuXHRjb25zdCBwb2ludHMgPSBjcmVhdGVQb2ludHMobnVtUG9pbnRzLCBwb2ludFdpZHRoLCB3aWR0aCwgaGVpZ2h0KTtcblx0d2luZG93LnBvaW50cyA9IHBvaW50cztcblx0cG9pbnRzLmZvckVhY2goKGQsIGkpID0+IHtcblx0XHRkLnR4ID0gd2lkdGggLyAyO1xuXHRcdGQudHkgPSBoZWlnaHQgLyAyO1xuXHRcdGQuY29sb3JFbmQgPSBjb2xvclNjYWxlc1tjdXJyZW50Q29sb3JTY2FsZV0oaSAvIHBvaW50cy5sZW5ndGgpO1xuXHR9KTtcblxuXHRhbmltYXRlKGxheW91dHNbY3VycmVudExheW91dF0sIHBvaW50cyk7XG59XG5cblxuLy8gaW5pdGlhbGl6ZSByZWdsXG5yZWdsKHtcblx0Ly8gZW5hYmxlIHRoZSB0ZXh0dXJlIGZsb2F0IGV4dGVuc2lvbiB0byBzdG9yZSBwb3NpdGlvbnMgaW4gYnVmZmVyc1xuICBleHRlbnNpb25zOiBbXG4gICAgJ09FU190ZXh0dXJlX2Zsb2F0JyxcbiAgXSxcblxuICAvLyBjYWxsYmFjayB3aGVuIHJlZ2wgaXMgaW5pdGlhbGl6ZWRcbiAgb25Eb25lOiBtYWluXG59KTtcbiJdfQ==
function phyllotaxisLayout(points,pointWidth,xOffset,yOffset,iOffset){if(xOffset===void 0)xOffset=0;if(yOffset===void 0)yOffset=0;if(iOffset===void 0)iOffset=0;var theta=Math.PI*(3-Math.sqrt(5));var pointRadius=pointWidth/2;points.forEach(function(point,i){var index=(i+iOffset)%points.length;var phylloX=pointRadius*Math.sqrt(index)*Math.cos(index*theta);var phylloY=pointRadius*Math.sqrt(index)*Math.sin(index*theta);point.x=xOffset+phylloX-pointRadius;point.y=yOffset+phylloY-pointRadius});return points}function gridLayout(points,pointWidth,gridWidth){var pointHeight=pointWidth;var pointsPerRow=Math.floor(gridWidth/pointWidth);var numRows=points.length/pointsPerRow;points.forEach(function(point,i){point.x=pointWidth*(i%pointsPerRow);point.y=pointHeight*Math.floor(i/pointsPerRow)});return points}function randomLayout(points,pointWidth,width,height){points.forEach(function(point,i){point.x=Math.random()*(width-pointWidth);point.y=Math.random()*(height-pointWidth)});return points}function sineLayout(points,pointWidth,width,height){var amplitude=.3*(height/2);var yOffset=height/2;var periods=3;var yScale=d3.scaleLinear().domain([0,points.length-1]).range([0,periods*2*Math.PI]);points.forEach(function(point,i){point.x=i/points.length*(width-pointWidth);point.y=amplitude*Math.sin(yScale(i))+yOffset});return points}function spiralLayout(points,pointWidth,width,height){var amplitude=.3*(height/2);var xOffset=width/2;var yOffset=height/2;var periods=20;var rScale=d3.scaleLinear().domain([0,points.length-1]).range([0,Math.min(width/2,height/2)-pointWidth]);var thetaScale=d3.scaleLinear().domain([0,points.length-1]).range([0,periods*2*Math.PI]);points.forEach(function(point,i){point.x=rScale(i)*Math.cos(thetaScale(i))+xOffset;point.y=rScale(i)*Math.sin(thetaScale(i))+yOffset});return points}function createPoints(numPoints,pointWidth,width,height){var colorScale=d3.scaleSequential(d3.interpolateViridis).domain([numPoints-1,0]);var points=d3.range(numPoints).map(function(id){return{id:id,color:colorScale(id)}});return randomLayout(points,pointWidth,width,height)}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImNvbW1vbi5qcyJdLCJuYW1lcyI6WyJwaHlsbG90YXhpc0xheW91dCIsInBvaW50cyIsInBvaW50V2lkdGgiLCJ4T2Zmc2V0IiwieU9mZnNldCIsImlPZmZzZXQiLCJjb25zdCIsInRoZXRhIiwiTWF0aCIsIlBJIiwic3FydCIsInBvaW50UmFkaXVzIiwiZm9yRWFjaCIsInBvaW50IiwiaSIsImluZGV4IiwibGVuZ3RoIiwicGh5bGxvWCIsImNvcyIsInBoeWxsb1kiLCJzaW4iLCJ4IiwieSIsImdyaWRMYXlvdXQiLCJncmlkV2lkdGgiLCJwb2ludEhlaWdodCIsInBvaW50c1BlclJvdyIsImZsb29yIiwibnVtUm93cyIsInJhbmRvbUxheW91dCIsIndpZHRoIiwiaGVpZ2h0IiwicmFuZG9tIiwic2luZUxheW91dCIsImFtcGxpdHVkZSIsInBlcmlvZHMiLCJ5U2NhbGUiLCJkMyIsInNjYWxlTGluZWFyIiwiZG9tYWluIiwicmFuZ2UiLCJzcGlyYWxMYXlvdXQiLCJyU2NhbGUiLCJtaW4iLCJ0aGV0YVNjYWxlIiwiY3JlYXRlUG9pbnRzIiwibnVtUG9pbnRzIiwiY29sb3JTY2FsZSIsInNjYWxlU2VxdWVudGlhbCIsImludGVycG9sYXRlVmlyaWRpcyIsIm1hcCIsImlkIiwiY29sb3IiXSwibWFwcGluZ3MiOiJBQVdBLFFBQVNBLG1CQUFrQkMsT0FBUUMsV0FBWUMsUUFBYUMsUUFBYUMscUNBQWhCLDhCQUFhLDhCQUFhLENBRWpGQyxJQUFNQyxPQUFRQyxLQUFLQyxJQUFNLEVBQUlELEtBQUtFLEtBQUssR0FFdkNKLElBQU1LLGFBQWNULFdBQWEsQ0FFakNELFFBQU9XLFFBQVEsU0FBQ0MsTUFBT0MsR0FDckJSLEdBQU1TLFFBQVNELEVBQUlULFNBQVdKLE9BQU9lLE1BQ3JDVixJQUFNVyxTQUFVTixZQUFjSCxLQUFLRSxLQUFLSyxPQUFTUCxLQUFLVSxJQUFJSCxNQUFRUixNQUNsRUQsSUFBTWEsU0FBVVIsWUFBY0gsS0FBS0UsS0FBS0ssT0FBU1AsS0FBS1ksSUFBSUwsTUFBUVIsTUFFbEVNLE9BQU1RLEVBQUlsQixRQUFVYyxRQUFVTixXQUM5QkUsT0FBTVMsRUFBSWxCLFFBQVVlLFFBQVVSLGFBR2hDLE9BQU9WLFFBYVQsUUFBU3NCLFlBQVd0QixPQUFRQyxXQUFZc0IsV0FDdENsQixHQUFNbUIsYUFBY3ZCLFVBQ3BCSSxJQUFNb0IsY0FBZWxCLEtBQUttQixNQUFNSCxVQUFZdEIsV0FDNUNJLElBQU1zQixTQUFVM0IsT0FBT2UsT0FBU1UsWUFFaEN6QixRQUFPVyxRQUFRLFNBQUNDLE1BQU9DLEdBQ3JCRCxNQUFNUSxFQUFJbkIsWUFBY1ksRUFBSVksYUFDNUJiLE9BQU1TLEVBQUlHLFlBQWNqQixLQUFLbUIsTUFBTWIsRUFBSVksZUFHekMsT0FBT3pCLFFBY1QsUUFBUzRCLGNBQWE1QixPQUFRQyxXQUFZNEIsTUFBT0MsUUFDL0M5QixPQUFPVyxRQUFRLFNBQUNDLE1BQU9DLEdBQ3JCRCxNQUFNUSxFQUFJYixLQUFLd0IsVUFBWUYsTUFBUTVCLFdBQ25DVyxPQUFNUyxFQUFJZCxLQUFLd0IsVUFBWUQsT0FBUzdCLGFBR3RDLE9BQU9ELFFBY1QsUUFBU2dDLFlBQVdoQyxPQUFRQyxXQUFZNEIsTUFBT0MsUUFDN0N6QixHQUFNNEIsV0FBWSxJQUFPSCxPQUFTLEVBQ2xDekIsSUFBTUYsU0FBVTJCLE9BQVMsQ0FDekJ6QixJQUFNNkIsU0FBVSxDQUNoQjdCLElBQU04QixRQUFTQyxHQUFHQyxjQUNmQyxRQUFRLEVBQUd0QyxPQUFPZSxPQUFTLElBQzNCd0IsT0FBTyxFQUFHTCxRQUFVLEVBQUkzQixLQUFLQyxJQUVoQ1IsUUFBT1csUUFBUSxTQUFDQyxNQUFPQyxHQUNyQkQsTUFBTVEsRUFBS1AsRUFBSWIsT0FBT2UsUUFBV2MsTUFBUTVCLFdBQ3pDVyxPQUFNUyxFQUFJWSxVQUFZMUIsS0FBS1ksSUFBSWdCLE9BQU90QixJQUFNVixTQUc5QyxPQUFPSCxRQWNULFFBQVN3QyxjQUFheEMsT0FBUUMsV0FBWTRCLE1BQU9DLFFBQy9DekIsR0FBTTRCLFdBQVksSUFBT0gsT0FBUyxFQUNsQ3pCLElBQU1ILFNBQVUyQixNQUFRLENBQ3hCeEIsSUFBTUYsU0FBVTJCLE9BQVMsQ0FDekJ6QixJQUFNNkIsU0FBVSxFQUVoQjdCLElBQU1vQyxRQUFTTCxHQUFHQyxjQUNmQyxRQUFRLEVBQUd0QyxPQUFPZSxPQUFRLElBQzFCd0IsT0FBTyxFQUFHaEMsS0FBS21DLElBQUliLE1BQVEsRUFBR0MsT0FBUyxHQUFLN0IsWUFFL0NJLElBQU1zQyxZQUFhUCxHQUFHQyxjQUNuQkMsUUFBUSxFQUFHdEMsT0FBT2UsT0FBUyxJQUMzQndCLE9BQU8sRUFBR0wsUUFBVSxFQUFJM0IsS0FBS0MsSUFFaENSLFFBQU9XLFFBQVEsU0FBQ0MsTUFBT0MsR0FDckJELE1BQU1RLEVBQUlxQixPQUFPNUIsR0FBS04sS0FBS1UsSUFBSTBCLFdBQVc5QixJQUFNWCxPQUNoRFUsT0FBTVMsRUFBSW9CLE9BQU81QixHQUFLTixLQUFLWSxJQUFJd0IsV0FBVzlCLElBQU1WLFNBR2xELE9BQU9ILFFBVVQsUUFBUzRDLGNBQWFDLFVBQVc1QyxXQUFZNEIsTUFBT0MsUUFDbER6QixHQUFNeUMsWUFBYVYsR0FBR1csZ0JBQWdCWCxHQUFHWSxvQkFDdENWLFFBQVFPLFVBQVksRUFBRyxHQUUxQnhDLElBQU1MLFFBQVNvQyxHQUFHRyxNQUFNTSxXQUFXSSxJQUFJLFNBQUFDLElBQUcsT0FDeENBLEdBQUFBLEdBQ0FDLE1BQU9MLFdBQVdJLE1BR3BCLE9BQU90QixjQUFhNUIsT0FBUUMsV0FBWTRCLE1BQU9DIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBHaXZlbiBhIHNldCBvZiBwb2ludHMsIGxheSB0aGVtIG91dCBpbiBhIHBoeWxsb3RheGlzIGxheW91dC5cbiAqIE11dGF0ZXMgdGhlIGBwb2ludHNgIHBhc3NlZCBpbiBieSB1cGRhdGluZyB0aGUgeCBhbmQgeSB2YWx1ZXMuXG4gKlxuICogQHBhcmFtIHtPYmplY3RbXX0gcG9pbnRzIFRoZSBhcnJheSBvZiBwb2ludHMgdG8gdXBkYXRlLiBXaWxsIGdldCBgeGAgYW5kIGB5YCBzZXQuXG4gKiBAcGFyYW0ge051bWJlcn0gcG9pbnRXaWR0aCBUaGUgc2l6ZSBpbiBwaXhlbHMgb2YgdGhlIHBvaW50J3Mgd2lkdGguIFNob3VsZCBhbHNvIGluY2x1ZGUgbWFyZ2luLlxuICogQHBhcmFtIHtOdW1iZXJ9IHhPZmZzZXQgVGhlIHggb2Zmc2V0IHRvIGFwcGx5IHRvIGFsbCBwb2ludHNcbiAqIEBwYXJhbSB7TnVtYmVyfSB5T2Zmc2V0IFRoZSB5IG9mZnNldCB0byBhcHBseSB0byBhbGwgcG9pbnRzXG4gKlxuICogQHJldHVybiB7T2JqZWN0W119IHBvaW50cyB3aXRoIG1vZGlmaWVkIHggYW5kIHlcbiAqL1xuZnVuY3Rpb24gcGh5bGxvdGF4aXNMYXlvdXQocG9pbnRzLCBwb2ludFdpZHRoLCB4T2Zmc2V0ID0gMCwgeU9mZnNldCA9IDAsIGlPZmZzZXQgPSAwKSB7XG4gIC8vIHRoZXRhIGRldGVybWluZXMgdGhlIHNwaXJhbCBvZiB0aGUgbGF5b3V0XG4gIGNvbnN0IHRoZXRhID0gTWF0aC5QSSAqICgzIC0gTWF0aC5zcXJ0KDUpKTtcblxuICBjb25zdCBwb2ludFJhZGl1cyA9IHBvaW50V2lkdGggLyAyO1xuXG4gIHBvaW50cy5mb3JFYWNoKChwb2ludCwgaSkgPT4ge1xuICAgIGNvbnN0IGluZGV4ID0gKGkgKyBpT2Zmc2V0KSAlIHBvaW50cy5sZW5ndGg7XG4gICAgY29uc3QgcGh5bGxvWCA9IHBvaW50UmFkaXVzICogTWF0aC5zcXJ0KGluZGV4KSAqIE1hdGguY29zKGluZGV4ICogdGhldGEpO1xuICAgIGNvbnN0IHBoeWxsb1kgPSBwb2ludFJhZGl1cyAqIE1hdGguc3FydChpbmRleCkgKiBNYXRoLnNpbihpbmRleCAqIHRoZXRhKTtcblxuICAgIHBvaW50LnggPSB4T2Zmc2V0ICsgcGh5bGxvWCAtIHBvaW50UmFkaXVzO1xuICAgIHBvaW50LnkgPSB5T2Zmc2V0ICsgcGh5bGxvWSAtIHBvaW50UmFkaXVzO1xuICB9KTtcblxuICByZXR1cm4gcG9pbnRzO1xufVxuXG4vKipcbiAqIEdpdmVuIGEgc2V0IG9mIHBvaW50cywgbGF5IHRoZW0gb3V0IGluIGEgZ3JpZC5cbiAqIE11dGF0ZXMgdGhlIGBwb2ludHNgIHBhc3NlZCBpbiBieSB1cGRhdGluZyB0aGUgeCBhbmQgeSB2YWx1ZXMuXG4gKlxuICogQHBhcmFtIHtPYmplY3RbXX0gcG9pbnRzIFRoZSBhcnJheSBvZiBwb2ludHMgdG8gdXBkYXRlLiBXaWxsIGdldCBgeGAgYW5kIGB5YCBzZXQuXG4gKiBAcGFyYW0ge051bWJlcn0gcG9pbnRXaWR0aCBUaGUgc2l6ZSBpbiBwaXhlbHMgb2YgdGhlIHBvaW50J3Mgd2lkdGguIFNob3VsZCBhbHNvIGluY2x1ZGUgbWFyZ2luLlxuICogQHBhcmFtIHtOdW1iZXJ9IGdyaWRXaWR0aCBUaGUgd2lkdGggb2YgdGhlIGdyaWQgb2YgcG9pbnRzXG4gKlxuICogQHJldHVybiB7T2JqZWN0W119IHBvaW50cyB3aXRoIG1vZGlmaWVkIHggYW5kIHlcbiAqL1xuZnVuY3Rpb24gZ3JpZExheW91dChwb2ludHMsIHBvaW50V2lkdGgsIGdyaWRXaWR0aCkge1xuICBjb25zdCBwb2ludEhlaWdodCA9IHBvaW50V2lkdGg7XG4gIGNvbnN0IHBvaW50c1BlclJvdyA9IE1hdGguZmxvb3IoZ3JpZFdpZHRoIC8gcG9pbnRXaWR0aCk7XG4gIGNvbnN0IG51bVJvd3MgPSBwb2ludHMubGVuZ3RoIC8gcG9pbnRzUGVyUm93O1xuXG4gIHBvaW50cy5mb3JFYWNoKChwb2ludCwgaSkgPT4ge1xuICAgIHBvaW50LnggPSBwb2ludFdpZHRoICogKGkgJSBwb2ludHNQZXJSb3cpO1xuICAgIHBvaW50LnkgPSBwb2ludEhlaWdodCAqIE1hdGguZmxvb3IoaSAvIHBvaW50c1BlclJvdyk7XG4gIH0pO1xuXG4gIHJldHVybiBwb2ludHM7XG59XG5cbi8qKlxuICogR2l2ZW4gYSBzZXQgb2YgcG9pbnRzLCBsYXkgdGhlbSBvdXQgcmFuZG9tbHkuXG4gKiBNdXRhdGVzIHRoZSBgcG9pbnRzYCBwYXNzZWQgaW4gYnkgdXBkYXRpbmcgdGhlIHggYW5kIHkgdmFsdWVzLlxuICpcbiAqIEBwYXJhbSB7T2JqZWN0W119IHBvaW50cyBUaGUgYXJyYXkgb2YgcG9pbnRzIHRvIHVwZGF0ZS4gV2lsbCBnZXQgYHhgIGFuZCBgeWAgc2V0LlxuICogQHBhcmFtIHtOdW1iZXJ9IHBvaW50V2lkdGggVGhlIHNpemUgaW4gcGl4ZWxzIG9mIHRoZSBwb2ludCdzIHdpZHRoLiBTaG91bGQgYWxzbyBpbmNsdWRlIG1hcmdpbi5cbiAqIEBwYXJhbSB7TnVtYmVyfSB3aWR0aCBUaGUgd2lkdGggb2YgdGhlIGFyZWEgdG8gcGxhY2UgdGhlbSBpblxuICogQHBhcmFtIHtOdW1iZXJ9IGhlaWdodCBUaGUgaGVpZ2h0IG9mIHRoZSBhcmVhIHRvIHBsYWNlIHRoZW0gaW5cbiAqXG4gKiBAcmV0dXJuIHtPYmplY3RbXX0gcG9pbnRzIHdpdGggbW9kaWZpZWQgeCBhbmQgeVxuICovXG5mdW5jdGlvbiByYW5kb21MYXlvdXQocG9pbnRzLCBwb2ludFdpZHRoLCB3aWR0aCwgaGVpZ2h0KSB7XG4gIHBvaW50cy5mb3JFYWNoKChwb2ludCwgaSkgPT4ge1xuICAgIHBvaW50LnggPSBNYXRoLnJhbmRvbSgpICogKHdpZHRoIC0gcG9pbnRXaWR0aCk7XG4gICAgcG9pbnQueSA9IE1hdGgucmFuZG9tKCkgKiAoaGVpZ2h0IC0gcG9pbnRXaWR0aCk7XG4gIH0pO1xuXG4gIHJldHVybiBwb2ludHM7XG59XG5cbi8qKlxuICogR2l2ZW4gYSBzZXQgb2YgcG9pbnRzLCBsYXkgdGhlbSBvdXQgaW4gYSBzaW5lIHdhdmUuXG4gKiBNdXRhdGVzIHRoZSBgcG9pbnRzYCBwYXNzZWQgaW4gYnkgdXBkYXRpbmcgdGhlIHggYW5kIHkgdmFsdWVzLlxuICpcbiAqIEBwYXJhbSB7T2JqZWN0W119IHBvaW50cyBUaGUgYXJyYXkgb2YgcG9pbnRzIHRvIHVwZGF0ZS4gV2lsbCBnZXQgYHhgIGFuZCBgeWAgc2V0LlxuICogQHBhcmFtIHtOdW1iZXJ9IHBvaW50V2lkdGggVGhlIHNpemUgaW4gcGl4ZWxzIG9mIHRoZSBwb2ludCdzIHdpZHRoLiBTaG91bGQgYWxzbyBpbmNsdWRlIG1hcmdpbi5cbiAqIEBwYXJhbSB7TnVtYmVyfSB3aWR0aCBUaGUgd2lkdGggb2YgdGhlIGFyZWEgdG8gcGxhY2UgdGhlbSBpblxuICogQHBhcmFtIHtOdW1iZXJ9IGhlaWdodCBUaGUgaGVpZ2h0IG9mIHRoZSBhcmVhIHRvIHBsYWNlIHRoZW0gaW5cbiAqXG4gKiBAcmV0dXJuIHtPYmplY3RbXX0gcG9pbnRzIHdpdGggbW9kaWZpZWQgeCBhbmQgeVxuICovXG5mdW5jdGlvbiBzaW5lTGF5b3V0KHBvaW50cywgcG9pbnRXaWR0aCwgd2lkdGgsIGhlaWdodCkge1xuICBjb25zdCBhbXBsaXR1ZGUgPSAwLjMgKiAoaGVpZ2h0IC8gMik7XG4gIGNvbnN0IHlPZmZzZXQgPSBoZWlnaHQgLyAyO1xuICBjb25zdCBwZXJpb2RzID0gMztcbiAgY29uc3QgeVNjYWxlID0gZDMuc2NhbGVMaW5lYXIoKVxuICAgIC5kb21haW4oWzAsIHBvaW50cy5sZW5ndGggLSAxXSlcbiAgICAucmFuZ2UoWzAsIHBlcmlvZHMgKiAyICogTWF0aC5QSV0pO1xuXG4gIHBvaW50cy5mb3JFYWNoKChwb2ludCwgaSkgPT4ge1xuICAgIHBvaW50LnggPSAoaSAvIHBvaW50cy5sZW5ndGgpICogKHdpZHRoIC0gcG9pbnRXaWR0aCk7XG4gICAgcG9pbnQueSA9IGFtcGxpdHVkZSAqIE1hdGguc2luKHlTY2FsZShpKSkgKyB5T2Zmc2V0O1xuICB9KTtcblxuICByZXR1cm4gcG9pbnRzO1xufVxuXG4vKipcbiAqIEdpdmVuIGEgc2V0IG9mIHBvaW50cywgbGF5IHRoZW0gb3V0IGluIGEgc3BpcmFsLlxuICogTXV0YXRlcyB0aGUgYHBvaW50c2AgcGFzc2VkIGluIGJ5IHVwZGF0aW5nIHRoZSB4IGFuZCB5IHZhbHVlcy5cbiAqXG4gKiBAcGFyYW0ge09iamVjdFtdfSBwb2ludHMgVGhlIGFycmF5IG9mIHBvaW50cyB0byB1cGRhdGUuIFdpbGwgZ2V0IGB4YCBhbmQgYHlgIHNldC5cbiAqIEBwYXJhbSB7TnVtYmVyfSBwb2ludFdpZHRoIFRoZSBzaXplIGluIHBpeGVscyBvZiB0aGUgcG9pbnQncyB3aWR0aC4gU2hvdWxkIGFsc28gaW5jbHVkZSBtYXJnaW4uXG4gKiBAcGFyYW0ge051bWJlcn0gd2lkdGggVGhlIHdpZHRoIG9mIHRoZSBhcmVhIHRvIHBsYWNlIHRoZW0gaW5cbiAqIEBwYXJhbSB7TnVtYmVyfSBoZWlnaHQgVGhlIGhlaWdodCBvZiB0aGUgYXJlYSB0byBwbGFjZSB0aGVtIGluXG4gKlxuICogQHJldHVybiB7T2JqZWN0W119IHBvaW50cyB3aXRoIG1vZGlmaWVkIHggYW5kIHlcbiAqL1xuZnVuY3Rpb24gc3BpcmFsTGF5b3V0KHBvaW50cywgcG9pbnRXaWR0aCwgd2lkdGgsIGhlaWdodCkge1xuICBjb25zdCBhbXBsaXR1ZGUgPSAwLjMgKiAoaGVpZ2h0IC8gMik7XG4gIGNvbnN0IHhPZmZzZXQgPSB3aWR0aCAvIDI7XG4gIGNvbnN0IHlPZmZzZXQgPSBoZWlnaHQgLyAyO1xuICBjb25zdCBwZXJpb2RzID0gMjA7XG5cbiAgY29uc3QgclNjYWxlID0gZDMuc2NhbGVMaW5lYXIoKVxuICAgIC5kb21haW4oWzAsIHBvaW50cy5sZW5ndGggLTFdKVxuICAgIC5yYW5nZShbMCwgTWF0aC5taW4od2lkdGggLyAyLCBoZWlnaHQgLyAyKSAtIHBvaW50V2lkdGhdKTtcblxuICBjb25zdCB0aGV0YVNjYWxlID0gZDMuc2NhbGVMaW5lYXIoKVxuICAgIC5kb21haW4oWzAsIHBvaW50cy5sZW5ndGggLSAxXSlcbiAgICAucmFuZ2UoWzAsIHBlcmlvZHMgKiAyICogTWF0aC5QSV0pO1xuXG4gIHBvaW50cy5mb3JFYWNoKChwb2ludCwgaSkgPT4ge1xuICAgIHBvaW50LnggPSByU2NhbGUoaSkgKiBNYXRoLmNvcyh0aGV0YVNjYWxlKGkpKSArIHhPZmZzZXRcbiAgICBwb2ludC55ID0gclNjYWxlKGkpICogTWF0aC5zaW4odGhldGFTY2FsZShpKSkgKyB5T2Zmc2V0O1xuICB9KTtcblxuICByZXR1cm4gcG9pbnRzO1xufVxuXG5cblxuXG4vKipcbiAqIEdlbmVyYXRlIGFuIG9iamVjdCBhcnJheSBvZiBgbnVtUG9pbnRzYCBsZW5ndGggd2l0aCB1bmlxdWUgSURzXG4gKiBhbmQgYXNzaWduZWQgY29sb3JzXG4gKi9cbmZ1bmN0aW9uIGNyZWF0ZVBvaW50cyhudW1Qb2ludHMsIHBvaW50V2lkdGgsIHdpZHRoLCBoZWlnaHQpIHtcbiAgY29uc3QgY29sb3JTY2FsZSA9IGQzLnNjYWxlU2VxdWVudGlhbChkMy5pbnRlcnBvbGF0ZVZpcmlkaXMpXG4gICAgLmRvbWFpbihbbnVtUG9pbnRzIC0gMSwgMF0pO1xuXG4gIGNvbnN0IHBvaW50cyA9IGQzLnJhbmdlKG51bVBvaW50cykubWFwKGlkID0+ICh7XG4gICAgaWQsXG4gICAgY29sb3I6IGNvbG9yU2NhbGUoaWQpLFxuICB9KSk7XG5cbiAgcmV0dXJuIHJhbmRvbUxheW91dChwb2ludHMsIHBvaW50V2lkdGgsIHdpZHRoLCBoZWlnaHQpO1xufVxuIl19
<!DOCTYPE html>
<title>Animate 100,000 points with regl</title>
<body>
<script src="https://wzrd.in/standalone/regl@1.3.0"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="common.js"></script>
<script src="script.js"></script>
</body>
function main(err, regl) {
const numPoints = 1000;
const pointWidth = 4;
const pointMargin = 1;
const width = window.innerWidth;
const height = window.innerHeight;
const duration = 1500;
const delayByIndex = 500 / numPoints;
const maxDuration = duration + delayByIndex * numPoints; // include max delay in here
const toPhyllotaxis = (points) => phyllotaxisLayout(points, pointWidth + pointMargin, width / 2, height / 2);
const toGrid = (points) => gridLayout(points, pointWidth + pointMargin, width);
const toSine = (points) => sineLayout(points, pointWidth + pointMargin, width, height);
const toSpiral = (points) => spiralLayout(points, pointWidth + pointMargin, width, height);
const layouts = [toPhyllotaxis, toGrid, toSine, toSpiral];
let currentLayout = 0;
let startTime = null; // in seconds
// start animation loop (note: time is in seconds)
let frameLoop;
// wrap d3 color scales so they produce vec3s with values 0-1
// also limit the t value to remove darkest color
function wrapColorScale(scale) {
const tScale = d3.scaleLinear().domain([0, 1]).range([0.4, 1]);
return t => {
const rgb = d3.rgb(scale(tScale(t)));
return [rgb.r / 255, rgb.g / 255, rgb.b / 255];
};
}
const colorScales = [
d3.scaleSequential(d3.interpolateViridis),
d3.scaleSequential(d3.interpolateMagma),
d3.scaleSequential(d3.interpolateInferno),
d3.scaleSequential(d3.interpolateCool),
].map(wrapColorScale);
let currentColorScale = 0;
// function to compile a draw points regl func
function createDrawPoints(points) {
const drawPoints = regl({
frag: `
precision highp float;
varying vec3 fragColor;
void main() {
gl_FragColor = vec4(fragColor, 1);
}
`,
vert: `
attribute vec2 positionStart;
attribute vec2 positionEnd;
attribute float index;
attribute vec3 colorStart;
attribute vec3 colorEnd;
varying vec3 fragColor;
uniform float pointWidth;
uniform float stageWidth;
uniform float stageHeight;
uniform float elapsed;
uniform float duration;
uniform float delayByIndex;
void main() {
gl_PointSize = pointWidth;
float delay = delayByIndex * index;
float t;
// drawing without animation, so show end state immediately
if (duration == 0.0) {
t = 1.0;
// still delaying before animating
} else if (elapsed < delay) {
t = 0.0;
} else {
t = 2.0 * ((elapsed - delay) / duration);
// cubic easing (cubicInOut) -- note there are glslify things for this toPhyllotaxis
// this is copied from d3.
t = (t <= 1.0 ? t * t * t : (t -= 2.0) * t * t + 2.0) / 2.0;
if (t > 1.0) {
t = 1.0;
}
}
// interpolate position
float x = mix(positionStart[0], positionEnd[0], t);
float y = mix(positionStart[1], positionEnd[1], t);
// interpolate color
fragColor = mix(colorStart, colorEnd, t);
// scale to normalized device coordinates (-1, -1) to (1, 1)
gl_Position = vec4(
2.0 * ((x / stageWidth) - 0.5),
// invert y since we think [0,0] is bottom left in pixel space (needed for d3.zoom)
-(2.0 * ((y / stageHeight) - 0.5)),
0.0,
1.0);
}
`,
attributes: {
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),
index: d3.range(points.length),
},
uniforms: {
pointWidth: regl.prop('pointWidth'),
stageWidth: regl.prop('stageWidth'),
stageHeight: regl.prop('stageHeight'),
delayByIndex: regl.prop('delayByIndex'),
duration: regl.prop('duration'),
// time in milliseconds since the prop startTime (i.e. time elapsed)
elapsed: ({ time }, { startTime = 0 }) => (time - startTime) * 1000,
},
count: points.length,
primitive: 'points',
});
return drawPoints;
}
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
const colorScale = colorScales[currentColorScale];
points.forEach((d, i) => {
d.tx = d.x;
d.ty = d.y;
d.colorEnd = colorScale(i / points.length)
});
// create the regl function with the new start and end points
const drawPoints = createDrawPoints(points);
frameLoop = regl.frame(({ time }) => {
if (startTime === null) {
startTime = time;
}
regl.clear({
// background color (black)
color: [0, 0, 0, 1],
depth: 1,
});
drawPoints({
pointWidth,
stageWidth: width,
stageHeight: height,
duration,
delayByIndex,
startTime,
});
if (time - startTime > (maxDuration / 1000)) {
console.log('done animating, moving to next layout');
frameLoop.cancel();
currentLayout = (currentLayout + 1) % layouts.length;
startTime = null;
currentColorScale = (currentColorScale + 1) % colorScales.length;
animate(layouts[currentLayout], points);
}
});
}
// create initial set of points
const points = createPoints(numPoints, pointWidth, width, height);
window.points = points;
points.forEach((d, i) => {
d.tx = width / 2;
d.ty = height / 2;
d.colorEnd = colorScales[currentColorScale](i / points.length);
});
animate(layouts[currentLayout], points);
}
// initialize regl
regl({
// enable the texture float extension to store positions in buffers
extensions: [
'OES_texture_float',
],
// 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