Skip to content

Instantly share code, notes, and snippets.

@tafsiri
Forked from pbeshai/.block
Last active May 8, 2017 19:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tafsiri/dba04b04ae949760f96f97a2fba23ba6 to your computer and use it in GitHub Desktop.
Save tafsiri/dba04b04ae949760f96f97a2fba23ba6 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\n\t\t\tvoid main() {\n\t\t\t\tfloat x = gl_PointCoord.x;\n\t\t\t\tfloat y = gl_PointCoord.y;\n\n\t\t\t\t// Measure distance from center\n\t\t\t\tvec2 uv = vec2(x, y) * 2. - 1.;\n\t\t\t\tfloat point_dist = length(uv);\n\n\t\t\t\tif (point_dist > 1.000) {\n\t\t\t\t\tdiscard;\n\t\t\t\t}\n\t\t\t\tgl_FragColor = vec4(fragColor, 1.0);\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:s,stageHeight:c,duration:u,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=1e4,l=20,d=1,s=window.innerWidth,c=window.innerHeight,u=1500,f=500/r,g=u+f*r,p=function(t){return phyllotaxisLayout(t,l+d,s/2,c/2)},h=function(t){return gridLayout(t,l+d,s)},m=function(t){return sineLayout(t,l+d,s,c)},y=function(t){return spiralLayout(t,l+d,s,c)},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,s,c);window.points=w,w.forEach(function(t,n){t.tx=s/2,t.ty=c/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,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNjcmlwdC5qcyJdLCJuYW1lcyI6WyJtYWluIiwiZXJyIiwicmVnbCIsIndyYXBDb2xvclNjYWxlIiwic2NhbGUiLCJjb25zdCIsInRTY2FsZSIsImQzIiwic2NhbGVMaW5lYXIiLCJkb21haW4iLCJyYW5nZSIsInQiLCJyZ2IiLCJyIiwiZyIsImIiLCJjcmVhdGVEcmF3UG9pbnRzIiwicG9pbnRzIiwiZHJhd1BvaW50cyIsImZyYWciLCJ2ZXJ0IiwiYXR0cmlidXRlcyIsInBvc2l0aW9uU3RhcnQiLCJtYXAiLCJkIiwic3giLCJzeSIsInBvc2l0aW9uRW5kIiwidHgiLCJ0eSIsImNvbG9yU3RhcnQiLCJjb2xvckVuZCIsImluZGV4IiwibGVuZ3RoIiwidW5pZm9ybXMiLCJwb2ludFdpZHRoIiwicHJvcCIsInN0YWdlV2lkdGgiLCJzdGFnZUhlaWdodCIsImRlbGF5QnlJbmRleCIsImR1cmF0aW9uIiwiZWxhcHNlZCIsInJlZiIsInJlZiQxIiwidGltZSIsInN0YXJ0VGltZSIsInByaW1pdGl2ZSIsImFuaW1hdGUiLCJsYXlvdXQiLCJjb25zb2xlIiwibG9nIiwiZm9yRWFjaCIsImNvbG9yU2NhbGUiLCJjb2xvclNjYWxlcyIsImN1cnJlbnRDb2xvclNjYWxlIiwiaSIsIngiLCJ5IiwiZnJhbWVMb29wIiwiZnJhbWUiLCJjbGVhciIsImNvbG9yIiwiZGVwdGgiLCJ3aWR0aCIsImhlaWdodCIsIm1heER1cmF0aW9uIiwiY2FuY2VsIiwiY3VycmVudExheW91dCIsImxheW91dHMiLCJudW1Qb2ludHMiLCJwb2ludE1hcmdpbiIsIndpbmRvdyIsImlubmVyV2lkdGgiLCJpbm5lckhlaWdodCIsInRvUGh5bGxvdGF4aXMiLCJwaHlsbG90YXhpc0xheW91dCIsInRvR3JpZCIsImdyaWRMYXlvdXQiLCJ0b1NpbmUiLCJzaW5lTGF5b3V0IiwidG9TcGlyYWwiLCJzcGlyYWxMYXlvdXQiLCJzY2FsZVNlcXVlbnRpYWwiLCJpbnRlcnBvbGF0ZVZpcmlkaXMiLCJpbnRlcnBvbGF0ZU1hZ21hIiwiaW50ZXJwb2xhdGVJbmZlcm5vIiwiaW50ZXJwb2xhdGVDb29sIiwiY3JlYXRlUG9pbnRzIiwiZXh0ZW5zaW9ucyIsIm9uRG9uZSJdLCJtYXBwaW5ncyI6IkFBQUEsUUFBU0EsTUFBS0MsRUFBS0MsR0F1QmxCLFFBQVNDLEdBQWVDLEdBQ3ZCQyxHQUFNQyxHQUFXQyxHQUFDQyxjQUFjQyxRQUFTLEVBQUksSUFBRUMsT0FBTyxHQUFPLEdBQzdELE9BQU8sVUFBQUMsR0FDTk4sR0FBTU8sR0FBUUwsR0FBQ0ssSUFBSVIsRUFBTUUsRUFBU0ssSUFDbEMsUUFBUUMsRUFBSUMsRUFBSSxJQUFLRCxFQUFJRSxFQUFJLElBQUtGLEVBQUlHLEVBQUksTUFhNUMsUUFBU0MsR0FBaUJDLEdBQ3pCWixHQUFNYSxHQUFhaEIsR0FDbEJpQixLQUFNLDhaQW1CTkMsS0FBTSxpcERBeUROQyxZQWtFQ0MsY0FBQUwsRUFBQU0sSUFBWSxTQUFBQyxHQUFBLE9BQUFBLEVBQUFDLEdBQUFELEVBQUFFLE1BQ1pDLFlBQUFWLEVBQVNNLElBQUEsU0FBQUMsR0FBQSxPQUFBQSxFQUFBSSxHQUFBSixFQUFBSyxNQUNUQyxXQUFFYixFQUFBTSxJQUFBLFNBQUFDLEdBQUEsTUFBQUEsR0FBQU0sYUFoRUZDLFNBQVVkLEVBQU9NLElBQUksU0FBQUMsR0FBRSxNQUFHQSxHQUFFTyxXQWtFN0JDLE1BQVF6QixHQUFHRyxNQUFBTyxFQUFhZ0IsU0E5RHhCQyxVQWtFQ0MsV0FBQWpDLEVBQWlCa0MsS0FBQSxjQUNqQkMsV0FBWW5DLEVBQUtrQyxLQUFBLGNBQ2pCRSxZQUFBcEMsRUFBaUJrQyxLQUFJLGVBQ3JCRyxhQUFRckMsRUFBUWtDLEtBQUEsZ0JBQ2hCSSxTQUFBdEMsRUFBQWtDLEtBQUEsWUE5REdLLFFBQVMsU0FBQ0MsRUFBVUMsTUFBUkMsR0FBSUYsRUFBQUUseUNBQWtCLEdBQTZCLEtBQXBCQSxFQUFPQyxLQW9FeER4QyxNQUFNWSxFQUFTZ0IsT0FDZmEsVUFBYSxVQUdaLE9BQU81QixHQUlSLFFBQVE2QixHQUFRQyxFQUFBL0IsR0FDaEJnQyxRQUFBQyxJQUFBLDZCQS9EQ2pDLEVBQU9rQyxRQUFRLFNBQUEzQixHQUNkQSxFQUFFQyxHQUFLRCxFQUFFSSxHQWtFWkosRUFBS0UsR0FBQUYsRUFBQUssR0FoRUZMLEVBQUVNLFdBQWFOLEVBQUVPLFdBSWxCaUIsRUFBTy9CLEVBb0VQWixJQUFDK0MsR0FBQUMsRUFBQUMsRUFoRURyQyxHQUFPa0MsUUFBUSxTQUFDM0IsRUFBRytCLEdBQ2xCL0IsRUFBRUksR0FBS0osRUFBRWdDLEVBQ1RoQyxFQUFFSyxHQUFLTCxFQUFFaUMsRUFDVGpDLEVBQUVPLFNBQVdxQixFQUFXRyxFQUFJdEMsRUFBT2dCLFNBSXBDNUIsSUFBTWEsR0FBYUYsRUFBaUJDLEVBRXBDeUMsR0FBWXhELEVBQUt5RCxNQUFNLFNBQUNqQixNQUFFRSxHQUFJRixFQUFBRSxJQUNYLFFBQWRDLElBQ0hBLEVBQVlELEdBR2IxQyxFQUFLMEQsT0FFSkMsT0FBUSxFQUFHLEVBQUcsRUFBRyxHQUNqQkMsTUFBTyxJQUdSNUMsR0FDQ2lCLFdBQUFBLEVBQ0FFLFdBQVkwQixFQUNaekIsWUFBYTBCLEVBQ2J4QixTQUFBQSxFQUNBRCxhQUFBQSxFQUNBTSxVQUFBQSxJQUdHRCxFQUFPQyxFQUFhb0IsRUFBYyxNQUNyQ2hCLFFBQVFDLElBQUkseUNBQ1pRLEVBQVVRLFNBRVZDLEdBQWlCQSxFQUFnQixHQUFLQyxFQUFRbkMsT0FDOUNZLEVBQVksS0FDWlMsR0FBcUJBLEVBQW9CLEdBQUtELEVBQVlwQixPQUMxRGMsRUFBUXFCLEVBQVFELEdBQWdCbEQsTUFsTW5DWixHQWtCSXFELEdBbEJFVyxFQUFZLElBQ1psQyxFQUFlLEdBQ2ZtQyxFQUFnQixFQUNoQlAsRUFBUVEsT0FBT0MsV0FDZlIsRUFBU08sT0FBT0UsWUFDaEJqQyxFQUFXLEtBQ1hELEVBQWUsSUFBTThCLEVBQ3JCSixFQUFjekIsRUFBV0QsRUFBZThCLEVBRXhDSyxFQUFnQixTQUFBekQsR0FBQyxNQUFBMEQsbUJBQVcxRCxFQUFBa0IsRUFBMEJtQyxFQUFhUCxFQUFBLEVBQVdDLEVBQU8sSUFDckZZLEVBQVMsU0FBQTNELEdBQUMsTUFBQTRELFlBQVE1RCxFQUFHa0IsRUFBbUJtQyxFQUFhUCxJQUNyRGUsRUFBUyxTQUFBN0QsR0FBQyxNQUFBOEQsWUFBUTlELEVBQUdrQixFQUFtQm1DLEVBQWFQLEVBQUFDLElBQ3JEZ0IsRUFBVyxTQUFBL0QsR0FBQyxNQUFBZ0UsY0FBV2hFLEVBQUFrQixFQUFxQm1DLEVBQWFQLEVBQUFDLElBRXpESSxHQUFXTSxFQUFlRSxFQUFRRSxFQUFRRSxHQUM1Q2IsRUFBZ0IsRUFDaEJ0QixFQUFZLEtBY1ZRLEdBQ0w5QyxHQUFHMkUsZ0JBQWdCM0UsR0FBRzRFLG9CQUN0QjVFLEdBQUcyRSxnQkFBZ0IzRSxHQUFHNkUsa0JBQ3RCN0UsR0FBRzJFLGdCQUFnQjNFLEdBQUc4RSxvQkFDdEI5RSxHQUFHMkUsZ0JBQWdCM0UsR0FBRytFLGtCQUNyQi9ELElBQUlwQixHQUNGbUQsRUFBb0IsRUFxS2xCckMsRUFBU3NFLGFBQWFsQixFQUFXbEMsRUFBWTRCLEVBQU9DLEVBQzFETyxRQUFPdEQsT0FBU0EsRUFDaEJBLEVBQU9rQyxRQUFRLFNBQUMzQixFQUFHK0IsR0FDbEIvQixFQUFFSSxHQUFLbUMsRUFBUSxFQUNmdkMsRUFBRUssR0FBS21DLEVBQVMsRUFDaEJ4QyxFQUFFTyxTQUFXc0IsRUFBWUMsR0FBbUJDLEVBQUl0QyxFQUFPZ0IsVUFHeERjLEVBQVFxQixFQUFRRCxHQUFnQmxELEdBS2pDZixNQUVFc0YsWUFDRSxxQkFJRkMsT0FBUXpGIiwiZmlsZSI6InNjcmlwdC5qcyIsInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIG1haW4oZXJyLCByZWdsKSB7XG5cdGNvbnN0IG51bVBvaW50cyA9IDEwMDAwO1xuXHRjb25zdCBwb2ludFdpZHRoID0gMjA7XG5cdGNvbnN0IHBvaW50TWFyZ2luID0gMTtcblx0Y29uc3Qgd2lkdGggPSB3aW5kb3cuaW5uZXJXaWR0aDtcblx0Y29uc3QgaGVpZ2h0ID0gd2luZG93LmlubmVySGVpZ2h0O1xuXHRjb25zdCBkdXJhdGlvbiA9IDE1MDA7XG5cdGNvbnN0IGRlbGF5QnlJbmRleCA9IDUwMCAvIG51bVBvaW50cztcblx0Y29uc3QgbWF4RHVyYXRpb24gPSBkdXJhdGlvbiArIGRlbGF5QnlJbmRleCAqIG51bVBvaW50czsgLy8gaW5jbHVkZSBtYXggZGVsYXkgaW4gaGVyZVxuXG5cdGNvbnN0IHRvUGh5bGxvdGF4aXMgPSAocG9pbnRzKSA9PiBwaHlsbG90YXhpc0xheW91dChwb2ludHMsIHBvaW50V2lkdGggKyBwb2ludE1hcmdpbiwgd2lkdGggLyAyLCBoZWlnaHQgLyAyKTtcblx0Y29uc3QgdG9HcmlkID0gKHBvaW50cykgPT4gZ3JpZExheW91dChwb2ludHMsIHBvaW50V2lkdGggKyBwb2ludE1hcmdpbiwgd2lkdGgpO1xuXHRjb25zdCB0b1NpbmUgPSAocG9pbnRzKSA9PiBzaW5lTGF5b3V0KHBvaW50cywgcG9pbnRXaWR0aCArIHBvaW50TWFyZ2luLCB3aWR0aCwgaGVpZ2h0KTtcblx0Y29uc3QgdG9TcGlyYWwgPSAocG9pbnRzKSA9PiBzcGlyYWxMYXlvdXQocG9pbnRzLCBwb2ludFdpZHRoICsgcG9pbnRNYXJnaW4sIHdpZHRoLCBoZWlnaHQpO1xuXG5cdGNvbnN0IGxheW91dHMgPSBbdG9QaHlsbG90YXhpcywgdG9HcmlkLCB0b1NpbmUsIHRvU3BpcmFsXTtcblx0bGV0IGN1cnJlbnRMYXlvdXQgPSAwO1xuXHRsZXQgc3RhcnRUaW1lID0gbnVsbDsgLy8gaW4gc2Vjb25kc1xuXHQvLyBzdGFydCBhbmltYXRpb24gbG9vcCAobm90ZTogdGltZSBpcyBpbiBzZWNvbmRzKVxuXHRsZXQgZnJhbWVMb29wO1xuXG5cdC8vIHdyYXAgZDMgY29sb3Igc2NhbGVzIHNvIHRoZXkgcHJvZHVjZSB2ZWMzcyB3aXRoIHZhbHVlcyAwLTFcblx0Ly8gYWxzbyBsaW1pdCB0aGUgdCB2YWx1ZSB0byByZW1vdmUgZGFya2VzdCBjb2xvclxuXHRmdW5jdGlvbiB3cmFwQ29sb3JTY2FsZShzY2FsZSkge1xuXHRcdGNvbnN0IHRTY2FsZSA9IGQzLnNjYWxlTGluZWFyKCkuZG9tYWluKFswLCAxXSkucmFuZ2UoWzAuNCwgMV0pO1xuXHRcdHJldHVybiB0ID0+IHtcblx0XHRcdGNvbnN0IHJnYiA9IGQzLnJnYihzY2FsZSh0U2NhbGUodCkpKTtcblx0XHRcdHJldHVybiBbcmdiLnIgLyAyNTUsIHJnYi5nIC8gMjU1LCByZ2IuYiAvIDI1NV07XG5cdFx0fTtcblx0fVxuXG5cdGNvbnN0IGNvbG9yU2NhbGVzID0gW1xuXHRcdGQzLnNjYWxlU2VxdWVudGlhbChkMy5pbnRlcnBvbGF0ZVZpcmlkaXMpLFxuXHRcdGQzLnNjYWxlU2VxdWVudGlhbChkMy5pbnRlcnBvbGF0ZU1hZ21hKSxcblx0XHRkMy5zY2FsZVNlcXVlbnRpYWwoZDMuaW50ZXJwb2xhdGVJbmZlcm5vKSxcblx0XHRkMy5zY2FsZVNlcXVlbnRpYWwoZDMuaW50ZXJwb2xhdGVDb29sKSxcblx0XS5tYXAod3JhcENvbG9yU2NhbGUpO1xuXHRsZXQgY3VycmVudENvbG9yU2NhbGUgPSAwO1xuXG5cdC8vIGZ1bmN0aW9uIHRvIGNvbXBpbGUgYSBkcmF3IHBvaW50cyByZWdsIGZ1bmNcblx0ZnVuY3Rpb24gY3JlYXRlRHJhd1BvaW50cyhwb2ludHMpIHtcblx0XHRjb25zdCBkcmF3UG9pbnRzID0gcmVnbCh7XG5cdFx0XHRmcmFnOiBgXG5cdFx0ICBwcmVjaXNpb24gaGlnaHAgZmxvYXQ7XG5cdFx0XHR2YXJ5aW5nIHZlYzMgZnJhZ0NvbG9yO1xuXG5cdFx0XHR2b2lkIG1haW4oKSB7XG5cdFx0XHRcdGZsb2F0IHggPSBnbF9Qb2ludENvb3JkLng7XG5cdFx0XHRcdGZsb2F0IHkgPSBnbF9Qb2ludENvb3JkLnk7XG5cblx0XHRcdFx0Ly8gTWVhc3VyZSBkaXN0YW5jZSBmcm9tIGNlbnRlclxuXHRcdFx0XHR2ZWMyIHV2ID0gdmVjMih4LCB5KSAqIDIuIC0gMS47XG5cdFx0XHRcdGZsb2F0IHBvaW50X2Rpc3QgPSBsZW5ndGgodXYpO1xuXG5cdFx0XHRcdGlmIChwb2ludF9kaXN0ID4gMS4wMDApIHtcblx0XHRcdFx0XHRkaXNjYXJkO1xuXHRcdFx0XHR9XG5cdFx0XHRcdGdsX0ZyYWdDb2xvciA9IHZlYzQoZnJhZ0NvbG9yLCAxLjApO1xuXHRcdFx0fVxuXHRcdFx0YCxcblxuXHRcdFx0dmVydDogYFxuXHRcdFx0YXR0cmlidXRlIHZlYzIgcG9zaXRpb25TdGFydDtcblx0XHRcdGF0dHJpYnV0ZSB2ZWMyIHBvc2l0aW9uRW5kO1xuXHRcdFx0YXR0cmlidXRlIGZsb2F0IGluZGV4O1xuXHRcdFx0YXR0cmlidXRlIHZlYzMgY29sb3JTdGFydDtcblx0XHRcdGF0dHJpYnV0ZSB2ZWMzIGNvbG9yRW5kO1xuXG5cdFx0XHR2YXJ5aW5nIHZlYzMgZnJhZ0NvbG9yO1xuXG5cdFx0XHR1bmlmb3JtIGZsb2F0IHBvaW50V2lkdGg7XG5cdFx0XHR1bmlmb3JtIGZsb2F0IHN0YWdlV2lkdGg7XG5cdFx0XHR1bmlmb3JtIGZsb2F0IHN0YWdlSGVpZ2h0O1xuXHRcdFx0dW5pZm9ybSBmbG9hdCBlbGFwc2VkO1xuXHRcdFx0dW5pZm9ybSBmbG9hdCBkdXJhdGlvbjtcblx0XHRcdHVuaWZvcm0gZmxvYXQgZGVsYXlCeUluZGV4O1xuXHRcdFx0dm9pZCBtYWluKCkge1xuXHRcdFx0XHRnbF9Qb2ludFNpemUgPSBwb2ludFdpZHRoO1xuXG5cdFx0XHRcdGZsb2F0IGRlbGF5ID0gZGVsYXlCeUluZGV4ICogaW5kZXg7XG5cdCAgICAgIGZsb2F0IHQ7XG5cblx0ICAgICAgLy8gZHJhd2luZyB3aXRob3V0IGFuaW1hdGlvbiwgc28gc2hvdyBlbmQgc3RhdGUgaW1tZWRpYXRlbHlcblx0ICAgICAgaWYgKGR1cmF0aW9uID09IDAuMCkge1xuXHQgICAgICAgIHQgPSAxLjA7XG5cblx0ICAgICAgLy8gc3RpbGwgZGVsYXlpbmcgYmVmb3JlIGFuaW1hdGluZ1xuXHQgICAgICB9IGVsc2UgaWYgKGVsYXBzZWQgPCBkZWxheSkge1xuXHQgICAgICAgIHQgPSAwLjA7XG5cdCAgICAgIH0gZWxzZSB7XG5cdCAgICAgICAgdCA9IDIuMCAqICgoZWxhcHNlZCAtIGRlbGF5KSAvIGR1cmF0aW9uKTtcblxuXHQgICAgICAgIC8vIGN1YmljIGVhc2luZyAoY3ViaWNJbk91dCkgLS0gbm90ZSB0aGVyZSBhcmUgZ2xzbGlmeSB0aGluZ3MgZm9yIHRoaXMgdG9QaHlsbG90YXhpc1xuXHQgICAgICAgIC8vIHRoaXMgaXMgY29waWVkIGZyb20gZDMuXG5cdCAgICAgICAgdCA9ICh0IDw9IDEuMCA/IHQgKiB0ICogdCA6ICh0IC09IDIuMCkgKiB0ICogdCArIDIuMCkgLyAyLjA7XG5cblx0ICAgICAgICBpZiAodCA+IDEuMCkge1xuXHQgICAgICAgICAgdCA9IDEuMDtcblx0ICAgICAgICB9XG5cdCAgICAgIH1cblxuXHRcdFx0XHQvLyBpbnRlcnBvbGF0ZSBwb3NpdGlvblxuXHQgICAgICBmbG9hdCB4ID0gbWl4KHBvc2l0aW9uU3RhcnRbMF0sIHBvc2l0aW9uRW5kWzBdLCB0KTtcblx0ICAgICAgZmxvYXQgeSA9IG1peChwb3NpdGlvblN0YXJ0WzFdLCBwb3NpdGlvbkVuZFsxXSwgdCk7XG5cblx0ICAgICAgLy8gaW50ZXJwb2xhdGUgY29sb3Jcblx0ICAgICAgZnJhZ0NvbG9yID0gbWl4KGNvbG9yU3RhcnQsIGNvbG9yRW5kLCB0KTtcblxuXHRcdFx0XHQvLyBzY2FsZSB0byBub3JtYWxpemVkIGRldmljZSBjb29yZGluYXRlcyAoLTEsIC0xKSB0byAoMSwgMSlcblx0ICAgICAgZ2xfUG9zaXRpb24gPSB2ZWM0KFxuXHRcdCAgICAgIDIuMCAqICgoeCAvIHN0YWdlV2lkdGgpIC0gMC41KSxcblx0XHQgICAgICAvLyBpbnZlcnQgeSBzaW5jZSB3ZSB0aGluayBbMCwwXSBpcyBib3R0b20gbGVmdCBpbiBwaXhlbCBzcGFjZSAobmVlZGVkIGZvciBkMy56b29tKVxuXHRcdCAgICAgIC0oMi4wICogKCh5IC8gc3RhZ2VIZWlnaHQpIC0gMC41KSksXG5cdFx0ICAgICAgMC4wLFxuXHRcdCAgICAgIDEuMCk7XG5cdFx0XHR9XG5cdFx0XHRgLFxuXG5cdFx0XHRhdHRyaWJ1dGVzOiB7XG5cdFx0XHRcdHBvc2l0aW9uU3RhcnQ6IHBvaW50cy5tYXAoZCA9PiBbZC5zeCwgZC5zeV0pLFxuXHRcdFx0XHRwb3NpdGlvbkVuZDogcG9pbnRzLm1hcChkID0+IFtkLnR4LCBkLnR5XSksXG5cdFx0XHRcdGNvbG9yU3RhcnQ6IHBvaW50cy5tYXAoZCA9PiBkLmNvbG9yU3RhcnQpLFxuXHRcdFx0XHRjb2xvckVuZDogcG9pbnRzLm1hcChkID0+IGQuY29sb3JFbmQpLFxuXHRcdFx0XHRpbmRleDogZDMucmFuZ2UocG9pbnRzLmxlbmd0aCksXG5cdFx0XHR9LFxuXG5cdFx0XHR1bmlmb3Jtczoge1xuXHRcdFx0XHRwb2ludFdpZHRoOiByZWdsLnByb3AoJ3BvaW50V2lkdGgnKSxcblx0XHRcdFx0c3RhZ2VXaWR0aDogcmVnbC5wcm9wKCdzdGFnZVdpZHRoJyksXG5cdFx0XHRcdHN0YWdlSGVpZ2h0OiByZWdsLnByb3AoJ3N0YWdlSGVpZ2h0JyksXG5cdFx0XHRcdGRlbGF5QnlJbmRleDogcmVnbC5wcm9wKCdkZWxheUJ5SW5kZXgnKSxcblx0ICAgICAgZHVyYXRpb246IHJlZ2wucHJvcCgnZHVyYXRpb24nKSxcblxuXHQgICAgICAvLyB0aW1lIGluIG1pbGxpc2Vjb25kcyBzaW5jZSB0aGUgcHJvcCBzdGFydFRpbWUgKGkuZS4gdGltZSBlbGFwc2VkKVxuXHQgICAgICBlbGFwc2VkOiAoeyB0aW1lIH0sIHsgc3RhcnRUaW1lID0gMCB9KSA9PiAodGltZSAtIHN0YXJ0VGltZSkgKiAxMDAwLFxuXHRcdFx0fSxcblxuXHRcdFx0Y291bnQ6IHBvaW50cy5sZW5ndGgsXG5cdFx0XHRwcmltaXRpdmU6ICdwb2ludHMnLFxuXHRcdH0pO1xuXG5cdFx0cmV0dXJuIGRyYXdQb2ludHM7XG5cdH1cblxuXG5cdGZ1bmN0aW9uIGFuaW1hdGUobGF5b3V0LCBwb2ludHMpIHtcblx0XHRjb25zb2xlLmxvZygnYW5pbWF0aW5nIHdpdGggbmV3IGxheW91dCcpO1xuXHRcdC8vIG1ha2UgcHJldmlvdXMgZW5kIHRoZSBuZXcgYmVnaW5uaW5nXG5cdFx0cG9pbnRzLmZvckVhY2goZCA9PiB7XG5cdFx0XHRkLnN4ID0gZC50eDtcblx0XHRcdGQuc3kgPSBkLnR5O1xuXHRcdFx0ZC5jb2xvclN0YXJ0ID0gZC5jb2xvckVuZDtcblx0XHR9KTtcblxuXHRcdC8vIGxheW91dCBwb2ludHNcblx0XHRsYXlvdXQocG9pbnRzKTtcblxuXHRcdC8vIGNvcHkgbGF5b3V0IHggeSB0byBlbmQgcG9zaXRpb25zXG5cdFx0Y29uc3QgY29sb3JTY2FsZSA9IGNvbG9yU2NhbGVzW2N1cnJlbnRDb2xvclNjYWxlXTtcblx0XHRwb2ludHMuZm9yRWFjaCgoZCwgaSkgPT4ge1xuXHRcdFx0ZC50eCA9IGQueDtcblx0XHRcdGQudHkgPSBkLnk7XG5cdFx0XHRkLmNvbG9yRW5kID0gY29sb3JTY2FsZShpIC8gcG9pbnRzLmxlbmd0aClcblx0XHR9KTtcblxuXHRcdC8vIGNyZWF0ZSB0aGUgcmVnbCBmdW5jdGlvbiB3aXRoIHRoZSBuZXcgc3RhcnQgYW5kIGVuZCBwb2ludHNcblx0XHRjb25zdCBkcmF3UG9pbnRzID0gY3JlYXRlRHJhd1BvaW50cyhwb2ludHMpO1xuXG5cdFx0ZnJhbWVMb29wID0gcmVnbC5mcmFtZSgoeyB0aW1lIH0pID0+IHtcblx0XHRcdGlmIChzdGFydFRpbWUgPT09IG51bGwpIHtcblx0XHRcdFx0c3RhcnRUaW1lID0gdGltZTtcblx0XHRcdH1cblxuXHRcdFx0cmVnbC5jbGVhcih7XG5cdFx0XHRcdC8vIGJhY2tncm91bmQgY29sb3IgKGJsYWNrKVxuXHRcdFx0XHRjb2xvcjogWzAsIDAsIDAsIDFdLFxuXHRcdFx0XHRkZXB0aDogMSxcblx0XHRcdH0pO1xuXG5cdFx0XHRkcmF3UG9pbnRzKHtcblx0XHRcdFx0cG9pbnRXaWR0aCxcblx0XHRcdFx0c3RhZ2VXaWR0aDogd2lkdGgsXG5cdFx0XHRcdHN0YWdlSGVpZ2h0OiBoZWlnaHQsXG5cdFx0XHRcdGR1cmF0aW9uLFxuXHRcdFx0XHRkZWxheUJ5SW5kZXgsXG5cdFx0XHRcdHN0YXJ0VGltZSxcblx0XHRcdH0pO1xuXG5cdFx0XHRpZiAodGltZSAtIHN0YXJ0VGltZSA+IChtYXhEdXJhdGlvbiAvIDEwMDApKSB7XG5cdFx0XHRcdGNvbnNvbGUubG9nKCdkb25lIGFuaW1hdGluZywgbW92aW5nIHRvIG5leHQgbGF5b3V0Jyk7XG5cdFx0XHRcdGZyYW1lTG9vcC5jYW5jZWwoKTtcblxuXHRcdFx0XHRjdXJyZW50TGF5b3V0ID0gKGN1cnJlbnRMYXlvdXQgKyAxKSAlIGxheW91dHMubGVuZ3RoO1xuXHRcdFx0XHRzdGFydFRpbWUgPSBudWxsO1xuXHRcdFx0XHRjdXJyZW50Q29sb3JTY2FsZSA9IChjdXJyZW50Q29sb3JTY2FsZSArIDEpICUgY29sb3JTY2FsZXMubGVuZ3RoO1xuXHRcdFx0XHRhbmltYXRlKGxheW91dHNbY3VycmVudExheW91dF0sIHBvaW50cyk7XG5cdFx0XHR9XG5cdFx0fSk7XG5cdH1cblxuXG5cdC8vIGNyZWF0ZSBpbml0aWFsIHNldCBvZiBwb2ludHNcblx0Y29uc3QgcG9pbnRzID0gY3JlYXRlUG9pbnRzKG51bVBvaW50cywgcG9pbnRXaWR0aCwgd2lkdGgsIGhlaWdodCk7XG5cdHdpbmRvdy5wb2ludHMgPSBwb2ludHM7XG5cdHBvaW50cy5mb3JFYWNoKChkLCBpKSA9PiB7XG5cdFx0ZC50eCA9IHdpZHRoIC8gMjtcblx0XHRkLnR5ID0gaGVpZ2h0IC8gMjtcblx0XHRkLmNvbG9yRW5kID0gY29sb3JTY2FsZXNbY3VycmVudENvbG9yU2NhbGVdKGkgLyBwb2ludHMubGVuZ3RoKTtcblx0fSk7XG5cblx0YW5pbWF0ZShsYXlvdXRzW2N1cnJlbnRMYXlvdXRdLCBwb2ludHMpO1xufVxuXG5cbi8vIGluaXRpYWxpemUgcmVnbFxucmVnbCh7XG5cdC8vIGVuYWJsZSB0aGUgdGV4dHVyZSBmbG9hdCBleHRlbnNpb24gdG8gc3RvcmUgcG9zaXRpb25zIGluIGJ1ZmZlcnNcbiAgZXh0ZW5zaW9uczogW1xuICAgICdPRVNfdGV4dHVyZV9mbG9hdCcsXG4gIF0sXG5cbiAgLy8gY2FsbGJhY2sgd2hlbiByZWdsIGlzIGluaXRpYWxpemVkXG4gIG9uRG9uZTogbWFpblxufSk7XG4iXX0=
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="dist_common.js"></script>
<script src="dist.js"></script>
</body>
function main(err, regl) {
const numPoints = 10000;
const pointWidth = 20;
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() {
float x = gl_PointCoord.x;
float y = gl_PointCoord.y;
// Measure distance from center
vec2 uv = vec2(x, y) * 2. - 1.;
float point_dist = length(uv);
if (point_dist > 1.000) {
discard;
}
gl_FragColor = vec4(fragColor, 1.0);
}
`,
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