Skip to content

Instantly share code, notes, and snippets.

@pbeshai
Last active March 21, 2025 08:55
Show Gist options
  • Select an option

  • Save pbeshai/65420c8d722cdbb0600b276c3adcc6e8 to your computer and use it in GitHub Desktop.

Select an option

Save pbeshai/65420c8d722cdbb0600b276c3adcc6e8 to your computer and use it in GitHub Desktop.

Revisions

  1. Peter Beshai revised this gist Mar 17, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -2,4 +2,4 @@

    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. (TBD).
    See the [blog post](https://bocoup.com/blog/smoothly-animate-thousands-of-points-with-html5-canvas-and-d3) for more details.
  2. pbeshai revised this gist Mar 11, 2017. 3 changed files with 1 addition and 8 deletions.
    1 change: 0 additions & 1 deletion dist.css
    Original file line number Diff line number Diff line change
    @@ -1 +0,0 @@
    .bar rect{fill:#4682b4}.bar text{fill:#fff;font:10px sans-serif}
    2 changes: 1 addition & 1 deletion index.html
    Original file line number Diff line number Diff line change
    @@ -4,7 +4,7 @@
    <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>Canvas Particles</title>
    <title>Animate thousands of points with canvas and D3</title>
    <style>
    html, body {
    padding: 0;
    6 changes: 0 additions & 6 deletions style.styl
    Original file line number Diff line number Diff line change
    @@ -1,6 +0,0 @@
    .bar rect
    fill steelblue

    .bar text
    fill #fff
    font 10px sans-serif
  3. pbeshai revised this gist Mar 11, 2017. 10 changed files with 331 additions and 0 deletions.
    5 changes: 5 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    ### 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. (TBD).
    150 changes: 150 additions & 0 deletions common.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,150 @@
    /**
    * 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);
    }
    1 change: 1 addition & 0 deletions dist.css
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    .bar rect{fill:#4682b4}.bar text{fill:#fff;font:10px sans-serif}
    2 changes: 2 additions & 0 deletions dist.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,2 @@
    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=
    2 changes: 2 additions & 0 deletions dist_common.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,2 @@
    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
    45 changes: 45 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,45 @@
    <!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>Canvas Particles</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>
    Binary file added preview.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    120 changes: 120 additions & 0 deletions script.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,120 @@
    // 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');
    });
    6 changes: 6 additions & 0 deletions style.styl
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,6 @@
    .bar rect
    fill steelblue

    .bar text
    fill #fff
    font 10px sans-serif
    Binary file added thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  4. Peter Beshai created this gist Mar 11, 2017.
    3 changes: 3 additions & 0 deletions .block
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    license: mit
    height: 620
    border: no