Skip to content

Instantly share code, notes, and snippets.

@pbeshai
Last active July 17, 2023 09:40
Show Gist options
  • Save pbeshai/65420c8d722cdbb0600b276c3adcc6e8 to your computer and use it in GitHub Desktop.
Save pbeshai/65420c8d722cdbb0600b276c3adcc6e8 to your computer and use it in GitHub Desktop.
Animate thousands of points with canvas and D3
license: mit
height: 620
border: no

Animate thousands of points with canvas and D3

Using D3 and d3-transition works great when animating hundreds of points with SVG, but performance breaks down when you need to animate more than a thousand. This block demonstrates a simple approach to animating thousands of points between different layouts using canvas, d3-timer, and d3-ease.

See the blog post for more details.

/**
* 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 draw(){var t=canvas.node().getContext("2d");t.save(),t.clearRect(0,0,width,height);for(var i=0;i<points.length;++i){var n=points[i];t.fillStyle=n.color,t.fillRect(n.x,n.y,pointWidth,pointWidth)}t.restore()}function animate(t){points.forEach(function(t){t.sx=t.x,t.sy=t.y}),t(points),points.forEach(function(t){t.tx=t.x,t.ty=t.y}),timer=d3.timer(function(t){var i=Math.min(1,ease(t/duration));points.forEach(function(t){t.x=t.sx*(1-i)+t.tx*i,t.y=t.sy*(1-i)+t.ty*i}),draw(),1===i&&(timer.stop(),currLayout=(currLayout+1)%layouts.length,animate(layouts[currLayout]))})}var width=600,height=600,numPoints=7e3,pointWidth=4,pointMargin=3,duration=1500,ease=d3.easeCubic,timer,currLayout=0,points=createPoints(numPoints,pointWidth,width,height),toGrid=function(t){return gridLayout(t,pointWidth+pointMargin,width)},toSine=function(t){return sineLayout(t,pointWidth+pointMargin,width,height)},toSpiral=function(t){return spiralLayout(t,pointWidth+pointMargin,width,height)},toPhyllotaxis=function(t){return phyllotaxisLayout(t,pointWidth+pointMargin,width/2,height/2)},layouts=[toSine,toPhyllotaxis,toSpiral,toPhyllotaxis,toGrid],screenScale=window.devicePixelRatio||1,canvas=d3.select("body").append("canvas").attr("width",width*screenScale).attr("height",height*screenScale).style("width",width+"px").style("height",height+"px").on("click",function(){d3.select(".play-control").style("display",""),timer.stop()});canvas.node().getContext("2d").scale(screenScale,screenScale),toGrid(points),draw(),d3.select("body").append("div").attr("class","play-control").text("PLAY").on("click",function(){animate(layouts[currLayout]),d3.select(this).style("display","none")});
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNjcmlwdC5qcyJdLCJuYW1lcyI6WyJkcmF3IiwiY29uc3QiLCJjdHgiLCJjYW52YXMiLCJub2RlIiwiZ2V0Q29udGV4dCIsInNhdmUiLCJjbGVhclJlY3QiLCJ3aWR0aCIsImhlaWdodCIsImxldCIsImkiLCJwb2ludHMiLCJsZW5ndGgiLCJwb2ludCIsImZpbGxTdHlsZSIsImNvbG9yIiwiZmlsbFJlY3QiLCJ4IiwieSIsInBvaW50V2lkdGgiLCJyZXN0b3JlIiwiYW5pbWF0ZSIsImxheW91dCIsImZvckVhY2giLCJzeCIsInN5IiwidHgiLCJ0eSIsInRpbWVyIiwiZDMiLCJlbGFwc2VkIiwidCIsIk1hdGgiLCJtaW4iLCJlYXNlIiwiZHVyYXRpb24iLCJzdG9wIiwiY3VyckxheW91dCIsImxheW91dHMiLCJudW1Qb2ludHMiLCJwb2ludE1hcmdpbiIsImVhc2VDdWJpYyIsImNyZWF0ZVBvaW50cyIsInRvR3JpZCIsImdyaWRMYXlvdXQiLCJ0b1NpbmUiLCJzaW5lTGF5b3V0IiwidG9TcGlyYWwiLCJzcGlyYWxMYXlvdXQiLCJ0b1BoeWxsb3RheGlzIiwicGh5bGxvdGF4aXNMYXlvdXQiLCJzY3JlZW5TY2FsZSIsIndpbmRvdyIsImRldmljZVBpeGVsUmF0aW8iLCJzZWxlY3QiLCJhcHBlbmQiLCJhdHRyIiwic3R5bGUiLCJvbiIsInNjYWxlIiwidGV4dCIsInRoaXMiXSwibWFwcGluZ3MiOiJBQWdDQSxRQUFTQSxRQUNQQyxHQUFNQyxHQUFNQyxPQUFPQyxPQUFPQyxXQUFXLEtBQ3JDSCxHQUFJSSxPQUdKSixFQUFJSyxVQUFVLEVBQUcsRUFBR0MsTUFBT0MsT0FHM0IsS0FBS0MsR0FBSUMsR0FBSSxFQUFHQSxFQUFJQyxPQUFPQyxTQUFVRixFQUFHLENBQ3RDVixHQUFNYSxHQUFRRixPQUFTRCxFQUN2QlQsR0FBSWEsVUFBWUQsRUFBTUUsTUFDdEJkLEVBQUllLFNBQVNILEVBQU1JLEVBQUdKLEVBQU1LLEVBQUdDLFdBQVlBLFlBRzdDbEIsRUFBSW1CLFVBSU4sUUFBU0MsU0FBUUMsR0FFZlgsT0FBT1ksUUFBUSxTQUFBVixHQUNiQSxFQUFNVyxHQUFLWCxFQUFNSSxFQUNqQkosRUFBTVksR0FBS1osRUFBTUssSUFJbkJJLEVBQU9YLFFBR1BBLE9BQU9ZLFFBQVEsU0FBQVYsR0FDYkEsRUFBTWEsR0FBS2IsRUFBTUksRUFDakJKLEVBQU1jLEdBQUtkLEVBQU1LLElBR25CVSxNQUFRQyxHQUFHRCxNQUFNLFNBQUFFLEdBRWY5QixHQUFPK0IsR0FBR0MsS0FBS0MsSUFBSyxFQUFFQyxLQUFLSixFQUFVSyxVQUdyQ3hCLFFBQU9ZLFFBQVEsU0FBQVYsR0FDYkEsRUFBTUksRUFBSUosRUFBTVcsSUFBTSxFQUFJTyxHQUFLbEIsRUFBTWEsR0FBS0ssRUFDMUNsQixFQUFNSyxFQUFJTCxFQUFNWSxJQUFNLEVBQUlNLEdBQUtsQixFQUFNYyxHQUFLSSxJQUk1Q2hDLE9BR1UsSUFBTmdDLElBRUZILE1BQU1RLE9BR05DLFlBQWNBLFdBQWEsR0FBS0MsUUFBUTFCLE9BR3hDUyxRQUFRaUIsUUFBUUQsZ0JBdkZ0QnJDLEdBQU1PLE9BQVEsSUFDUkMsT0FBUyxJQUdUK0IsVUFBWSxJQUNacEIsV0FBZSxFQUNmcUIsWUFBZ0IsRUFHaEJMLFNBQVcsS0FDWEQsS0FBU0wsR0FBQ1ksVUFDWmIsTUFDQVMsV0FBYSxFQUdYMUIsT0FBUytCLGFBQWFILFVBQVdwQixXQUFZWixNQUFPQyxRQUdwRG1DLE9BQVMsU0FBQWhDLEdBQUMsTUFBQWlDLFlBQVFqQyxFQUN0QlEsV0FBYXFCLFlBQWFqQyxRQUN0QnNDLE9BQVMsU0FBQWxDLEdBQUMsTUFBQW1DLFlBQVFuQyxFQUN0QlEsV0FBYXFCLFlBQWFqQyxNQUFPQyxTQUM3QnVDLFNBQVcsU0FBQXBDLEdBQUMsTUFBQXFDLGNBQVdyQyxFQUMzQlEsV0FBYXFCLFlBQWFqQyxNQUFPQyxTQUM3QnlDLGNBQWdCLFNBQUF0QyxHQUFDLE1BQUF1QyxtQkFBV3ZDLEVBQ2hDUSxXQUFhcUIsWUFBYWpDLE1BQVEsRUFBR0MsT0FBUyxJQUcxQzhCLFNBQVdPLE9BQVFJLGNBQWVGLFNBQVVFLGNBQWVOLFFBaUUzRFEsWUFBY0MsT0FBT0Msa0JBQXNCLEVBQzNDbkQsT0FBVzJCLEdBQUN5QixPQUFPLFFBQVFDLE9BQU8sVUFDckNDLEtBQUssUUFBU2pELE1BQVE0QyxhQUN0QkssS0FBSyxTQUFVaEQsT0FBUzJDLGFBQ3hCTSxNQUFNLFFBQVNsRCxNQUFRLE1BQ3ZCa0QsTUFBTSxTQUFVakQsT0FBUyxNQUN6QmtELEdBQUcsUUFBUyxXQUNYN0IsR0FBR3lCLE9BQU8saUJBQWlCRyxNQUFNLFVBQVcsSUFDNUM3QixNQUFNUSxRQUVWbEMsUUFBT0MsT0FBT0MsV0FBVyxNQUFNdUQsTUFBTVIsWUFBYUEsYUFHbERSLE9BQU9oQyxRQUNQWixPQUVBOEIsR0FBR3lCLE9BQU8sUUFBUUMsT0FBTyxPQUN0QkMsS0FBSyxRQUFTLGdCQUNkSSxLQUFLLFFBQ0xGLEdBQUcsUUFBUyxXQUVYckMsUUFBUWlCLFFBQVFELGFBR2hCUixHQUFHeUIsT0FBT08sTUFBTUosTUFBTSxVQUFXIiwiZmlsZSI6InNjcmlwdC5qcyIsInNvdXJjZXNDb250ZW50IjpbIi8vIGNhbnZhcyBzZXR0aW5nc1xuY29uc3Qgd2lkdGggPSA2MDA7XG5jb25zdCBoZWlnaHQgPSA2MDA7XG5cbi8vIHBvaW50IHNldHRpbmdzXG5jb25zdCBudW1Qb2ludHMgPSA3MDAwO1xuY29uc3QgcG9pbnRXaWR0aCA9IDQ7XG5jb25zdCBwb2ludE1hcmdpbiA9IDM7XG5cbi8vIGFuaW1hdGlvbiBzZXR0aW5nc1xuY29uc3QgZHVyYXRpb24gPSAxNTAwO1xuY29uc3QgZWFzZSA9IGQzLmVhc2VDdWJpYztcbmxldCB0aW1lcjtcbmxldCBjdXJyTGF5b3V0ID0gMDtcblxuLy8gY3JlYXRlIHNldCBvZiBwb2ludHNcbmNvbnN0IHBvaW50cyA9IGNyZWF0ZVBvaW50cyhudW1Qb2ludHMsIHBvaW50V2lkdGgsIHdpZHRoLCBoZWlnaHQpO1xuXG4vLyB3cmFwIGxheW91dCBoZWxwZXJzIHNvIHRoZXkgb25seSB0YWtlIHBvaW50cyBhcyBhbiBhcmd1bWVudFxuY29uc3QgdG9HcmlkID0gKHBvaW50cykgPT4gZ3JpZExheW91dChwb2ludHMsXG4gIHBvaW50V2lkdGggKyBwb2ludE1hcmdpbiwgd2lkdGgpO1xuY29uc3QgdG9TaW5lID0gKHBvaW50cykgPT4gc2luZUxheW91dChwb2ludHMsXG4gIHBvaW50V2lkdGggKyBwb2ludE1hcmdpbiwgd2lkdGgsIGhlaWdodCk7XG5jb25zdCB0b1NwaXJhbCA9IChwb2ludHMpID0+IHNwaXJhbExheW91dChwb2ludHMsXG4gIHBvaW50V2lkdGggKyBwb2ludE1hcmdpbiwgd2lkdGgsIGhlaWdodCk7XG5jb25zdCB0b1BoeWxsb3RheGlzID0gKHBvaW50cykgPT4gcGh5bGxvdGF4aXNMYXlvdXQocG9pbnRzLFxuICBwb2ludFdpZHRoICsgcG9pbnRNYXJnaW4sIHdpZHRoIC8gMiwgaGVpZ2h0IC8gMik7XG5cbi8vIHN0b3JlIHRoZSBsYXlvdXRzIGluIGFuIGFycmF5IHRvIHNlcXVlbmNlIHRocm91Z2hcbmNvbnN0IGxheW91dHMgPSBbdG9TaW5lLCB0b1BoeWxsb3RheGlzLCB0b1NwaXJhbCwgdG9QaHlsbG90YXhpcywgdG9HcmlkXTtcblxuLy8gZHJhdyB0aGUgcG9pbnRzIGJhc2VkIG9uIHRoZWlyIGN1cnJlbnQgbGF5b3V0XG5mdW5jdGlvbiBkcmF3KCkge1xuICBjb25zdCBjdHggPSBjYW52YXMubm9kZSgpLmdldENvbnRleHQoJzJkJyk7XG4gIGN0eC5zYXZlKCk7XG5cbiAgLy8gZXJhc2Ugd2hhdCBpcyBvbiB0aGUgY2FudmFzIGN1cnJlbnRseVxuICBjdHguY2xlYXJSZWN0KDAsIDAsIHdpZHRoLCBoZWlnaHQpO1xuXG4gIC8vIGRyYXcgZWFjaCBwb2ludCBhcyBhIHJlY3RhbmdsZVxuICBmb3IgKGxldCBpID0gMDsgaSA8IHBvaW50cy5sZW5ndGg7ICsraSkge1xuICAgIGNvbnN0IHBvaW50ID0gcG9pbnRzW2ldO1xuICAgIGN0eC5maWxsU3R5bGUgPSBwb2ludC5jb2xvcjtcbiAgICBjdHguZmlsbFJlY3QocG9pbnQueCwgcG9pbnQueSwgcG9pbnRXaWR0aCwgcG9pbnRXaWR0aCk7XG4gIH1cblxuICBjdHgucmVzdG9yZSgpO1xufVxuXG4vLyBhbmltYXRlIHRoZSBwb2ludHMgdG8gYSBnaXZlbiBsYXlvdXRcbmZ1bmN0aW9uIGFuaW1hdGUobGF5b3V0KSB7XG4gIC8vIHN0b3JlIHRoZSBzb3VyY2UgcG9zaXRpb25cbiAgcG9pbnRzLmZvckVhY2gocG9pbnQgPT4ge1xuICAgIHBvaW50LnN4ID0gcG9pbnQueDtcbiAgICBwb2ludC5zeSA9IHBvaW50Lnk7XG4gIH0pO1xuXG4gIC8vIGdldCBkZXN0aW5hdGlvbiB4IGFuZCB5IHBvc2l0aW9uIG9uIGVhY2ggcG9pbnRcbiAgbGF5b3V0KHBvaW50cyk7XG5cbiAgLy8gc3RvcmUgdGhlIGRlc3RpbmF0aW9uIHBvc2l0aW9uXG4gIHBvaW50cy5mb3JFYWNoKHBvaW50ID0+IHtcbiAgICBwb2ludC50eCA9IHBvaW50Lng7XG4gICAgcG9pbnQudHkgPSBwb2ludC55O1xuICB9KTtcblxuICB0aW1lciA9IGQzLnRpbWVyKChlbGFwc2VkKSA9PiB7XG4gICAgLy8gY29tcHV0ZSBob3cgZmFyIHRocm91Z2ggdGhlIGFuaW1hdGlvbiB3ZSBhcmUgKDAgdG8gMSlcbiAgICBjb25zdCB0ID0gTWF0aC5taW4oMSwgZWFzZShlbGFwc2VkIC8gZHVyYXRpb24pKTtcblxuICAgIC8vIHVwZGF0ZSBwb2ludCBwb3NpdGlvbnMgKGludGVycG9sYXRlIGJldHdlZW4gc291cmNlIGFuZCB0YXJnZXQpXG4gICAgcG9pbnRzLmZvckVhY2gocG9pbnQgPT4ge1xuICAgICAgcG9pbnQueCA9IHBvaW50LnN4ICogKDEgLSB0KSArIHBvaW50LnR4ICogdDtcbiAgICAgIHBvaW50LnkgPSBwb2ludC5zeSAqICgxIC0gdCkgKyBwb2ludC50eSAqIHQ7XG4gICAgfSk7XG5cbiAgICAvLyB1cGRhdGUgd2hhdCBpcyBkcmF3biBvbiBzY3JlZW5cbiAgICBkcmF3KCk7XG5cbiAgICAvLyBpZiB0aGlzIGFuaW1hdGlvbiBpcyBvdmVyXG4gICAgaWYgKHQgPT09IDEpIHtcbiAgICAgIC8vIHN0b3AgdGhpcyB0aW1lciBmb3IgdGhpcyBsYXlvdXQgYW5kIHN0YXJ0IGEgbmV3IG9uZVxuICAgICAgdGltZXIuc3RvcCgpO1xuXG4gICAgICAvLyB1cGRhdGUgdG8gdXNlIG5leHQgbGF5b3V0XG4gICAgICBjdXJyTGF5b3V0ID0gKGN1cnJMYXlvdXQgKyAxKSAlIGxheW91dHMubGVuZ3RoO1xuXG4gICAgICAvLyBzdGFydCBhbmltYXRpb24gZm9yIG5leHQgbGF5b3V0XG4gICAgICBhbmltYXRlKGxheW91dHNbY3VyckxheW91dF0pO1xuICAgIH1cbiAgfSk7XG59XG5cbi8vIGNyZWF0ZSB0aGUgY2FudmFzXG5jb25zdCBzY3JlZW5TY2FsZSA9IHdpbmRvdy5kZXZpY2VQaXhlbFJhdGlvIHx8IDE7XG5jb25zdCBjYW52YXMgPSBkMy5zZWxlY3QoJ2JvZHknKS5hcHBlbmQoJ2NhbnZhcycpXG4gIC5hdHRyKCd3aWR0aCcsIHdpZHRoICogc2NyZWVuU2NhbGUpXG4gIC5hdHRyKCdoZWlnaHQnLCBoZWlnaHQgKiBzY3JlZW5TY2FsZSlcbiAgLnN0eWxlKCd3aWR0aCcsIGAke3dpZHRofXB4YClcbiAgLnN0eWxlKCdoZWlnaHQnLCBgJHtoZWlnaHR9cHhgKVxuICAub24oJ2NsaWNrJywgZnVuY3Rpb24gKCkge1xuICAgIGQzLnNlbGVjdCgnLnBsYXktY29udHJvbCcpLnN0eWxlKCdkaXNwbGF5JywgJycpO1xuICAgIHRpbWVyLnN0b3AoKTtcbiAgfSk7XG5jYW52YXMubm9kZSgpLmdldENvbnRleHQoJzJkJykuc2NhbGUoc2NyZWVuU2NhbGUsIHNjcmVlblNjYWxlKTtcblxuLy8gc3RhcnQgb2ZmIGFzIGEgZ3JpZFxudG9HcmlkKHBvaW50cyk7XG5kcmF3KCk7XG5cbmQzLnNlbGVjdCgnYm9keScpLmFwcGVuZCgnZGl2JylcbiAgLmF0dHIoJ2NsYXNzJywgJ3BsYXktY29udHJvbCcpXG4gIC50ZXh0KCdQTEFZJylcbiAgLm9uKCdjbGljaycsIGZ1bmN0aW9uICgpIHtcbiAgICAvLyBzdGFydCB0aGUgYW5pbWF0aW9uXG4gICAgYW5pbWF0ZShsYXlvdXRzW2N1cnJMYXlvdXRdKTtcblxuICAgIC8vIHJlbW92ZSB0aGUgcGxheSBjb250cm9sXG4gICAgZDMuc2VsZWN0KHRoaXMpLnN0eWxlKCdkaXNwbGF5JywgJ25vbmUnKTtcbiAgfSk7XG4iXX0=
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>
<html lang="en">
<head>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<meta charset='UTF-8'>
<script src="https://d3js.org/d3.v4.min.js"></script>
<title>Animate thousands of points with canvas and D3</title>
<style>
html, body {
padding: 0;
margin: 0;
}
canvas {
cursor: pointer;
}
.play-control {
position: absolute;
top: 0px;
left: 0px;
width: 600px;
height: 600px;
line-height: 600px;
text-align: center;
background-color: rgba(0, 0, 0, 0.1);
color: #f4f4f4;
text-shadow: rgba(0, 0, 0, 0.7) 3px 3px 0px;
font-size: 100px;
font-family: 'helvetica neue', calibri, sans-serif;
font-weight: 100;
cursor: pointer;
}
.play-control:hover {
color: #fff;
text-shadow: #000 3px 3px 0px;
background-color: rgba(0, 0, 0, 0.04);
}
</style>
</head>
<body>
<script src="dist_common.js"></script>
<script src="dist.js"></script>
</body>
</html>
// canvas settings
const width = 600;
const height = 600;
// point settings
const numPoints = 7000;
const pointWidth = 4;
const pointMargin = 3;
// animation settings
const duration = 1500;
const ease = d3.easeCubic;
let timer;
let currLayout = 0;
// create set of points
const points = createPoints(numPoints, pointWidth, width, height);
// wrap layout helpers so they only take points as an argument
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 toPhyllotaxis = (points) => phyllotaxisLayout(points,
pointWidth + pointMargin, width / 2, height / 2);
// store the layouts in an array to sequence through
const layouts = [toSine, toPhyllotaxis, toSpiral, toPhyllotaxis, toGrid];
// draw the points based on their current layout
function draw() {
const ctx = canvas.node().getContext('2d');
ctx.save();
// erase what is on the canvas currently
ctx.clearRect(0, 0, width, height);
// draw each point as a rectangle
for (let i = 0; i < points.length; ++i) {
const point = points[i];
ctx.fillStyle = point.color;
ctx.fillRect(point.x, point.y, pointWidth, pointWidth);
}
ctx.restore();
}
// animate the points to a given layout
function animate(layout) {
// store the source position
points.forEach(point => {
point.sx = point.x;
point.sy = point.y;
});
// get destination x and y position on each point
layout(points);
// store the destination position
points.forEach(point => {
point.tx = point.x;
point.ty = point.y;
});
timer = d3.timer((elapsed) => {
// compute how far through the animation we are (0 to 1)
const t = Math.min(1, ease(elapsed / duration));
// update point positions (interpolate between source and target)
points.forEach(point => {
point.x = point.sx * (1 - t) + point.tx * t;
point.y = point.sy * (1 - t) + point.ty * t;
});
// update what is drawn on screen
draw();
// if this animation is over
if (t === 1) {
// stop this timer for this layout and start a new one
timer.stop();
// update to use next layout
currLayout = (currLayout + 1) % layouts.length;
// start animation for next layout
animate(layouts[currLayout]);
}
});
}
// create the canvas
const screenScale = window.devicePixelRatio || 1;
const canvas = d3.select('body').append('canvas')
.attr('width', width * screenScale)
.attr('height', height * screenScale)
.style('width', `${width}px`)
.style('height', `${height}px`)
.on('click', function () {
d3.select('.play-control').style('display', '');
timer.stop();
});
canvas.node().getContext('2d').scale(screenScale, screenScale);
// start off as a grid
toGrid(points);
draw();
d3.select('body').append('div')
.attr('class', 'play-control')
.text('PLAY')
.on('click', function () {
// start the animation
animate(layouts[currLayout]);
// remove the play control
d3.select(this).style('display', 'none');
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment