Skip to content

Instantly share code, notes, and snippets.

@pbeshai
Last active January 27, 2019 03:40
  • Star 0 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save pbeshai/51d05995c5410a52116f89738144c622 to your computer and use it in GitHub Desktop.
Animate 100,000 points with regl - II
license: mit
height: 720
border: no
/**
* 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(t){function n(l){if(g[l])return g[l].exports;var i=g[l]={i:l,l:!1,exports:{}};return t[l].call(i.exports,i,i.exports,n),i.l=!0,i.exports}var g={};n.m=t,n.c=g,n.i=function(t){return t},n.d=function(t,g,l){n.o(t,g)||Object.defineProperty(t,g,{configurable:!1,enumerable:!0,get:l})},n.n=function(t){var g=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(g,"a",g),g},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=0)}([function(module,exports,__webpack_require__){"use strict";eval("\n\nfunction main(err, regl) {\n var numPoints = 100000;\n var pointWidth = 4;\n var pointMargin = 1;\n var width = window.innerWidth;\n var height = window.innerHeight;\n\n // duration of the animation ignoring delays\n var duration = 1500;\n\n // multiply this value by the index of a point to get its delay\n var delayByIndex = 500 / numPoints;\n\n // include max delay in here\n var maxDuration = duration + delayByIndex * numPoints;\n\n // create helpers that will layout the points in different ways (see common.js)\n var toPhyllotaxis = function toPhyllotaxis(points) {\n return phyllotaxisLayout(points, pointWidth + pointMargin, width / 2, height / 2);\n };\n var toGrid = function toGrid(points) {\n return gridLayout(points, pointWidth + pointMargin, width);\n };\n var toSine = function toSine(points) {\n return sineLayout(points, pointWidth + pointMargin, width, height);\n };\n var toSpiral = function toSpiral(points) {\n return spiralLayout(points, pointWidth + pointMargin, width, height);\n };\n\n // set the order of the layouts and some initial animation state\n var layouts = [toPhyllotaxis, toSpiral, toGrid, toSine];\n var currentLayout = 0;\n var startTime = null; // in seconds\n\n // wrap d3 color scales so they produce vec3s with values 0-1\n function wrapColorScale(scale) {\n return function (t) {\n var rgb = d3.rgb(scale(1 - t));\n return [rgb.r / 255, rgb.g / 255, rgb.b / 255];\n };\n }\n\n // the order of color scales to loop through\n var colorScales = [d3.scaleSequential(d3.interpolateViridis), d3.scaleSequential(d3.interpolateInferno), d3.scaleSequential(d3.interpolateCool)].map(wrapColorScale);\n var currentColorScale = 0;\n\n // function to compile a draw points regl func\n function createDrawPoints(points) {\n var drawPoints = regl({\n frag: '\\n\\t\\t\\t// set the precision of floating point numbers\\n\\t\\t precision highp float;\\n\\n\\t\\t // this value is populated by the vertex shader\\n\\t\\t\\tvarying vec3 fragColor;\\n\\n\\t\\t\\tvoid main() {\\n\\t\\t\\t\\t// gl_FragColor is a special variable that holds the color of a pixel\\n\\t\\t\\t\\tgl_FragColor = vec4(fragColor, 1);\\n\\t\\t\\t}\\n\\t\\t\\t',\n\n vert: '\\n\\t\\t\\t// per vertex attributes\\n\\t\\t\\tattribute vec2 positionStart;\\n\\t\\t\\tattribute vec2 positionEnd;\\n\\t\\t\\tattribute float index;\\n\\t\\t\\tattribute vec3 colorStart;\\n\\t\\t\\tattribute vec3 colorEnd;\\n\\n\\t\\t\\t// variables to send to the fragment shader\\n\\t\\t\\tvarying vec3 fragColor;\\n\\n\\t\\t\\t// values that are the same for all vertices\\n\\t\\t\\tuniform float pointWidth;\\n\\t\\t\\tuniform float stageWidth;\\n\\t\\t\\tuniform float stageHeight;\\n\\t\\t\\tuniform float elapsed;\\n\\t\\t\\tuniform float duration;\\n\\t\\t\\tuniform float delayByIndex;\\n\\n\\t\\t\\t// helper function to transform from pixel space to normalized device coordinates (NDC)\\n\\t\\t\\t// in NDC (0,0) is the middle, (-1, 1) is the top left and (1, -1) is the bottom right.\\n\\t\\t\\tvec2 normalizeCoords(vec2 position) {\\n\\t\\t\\t\\t// read in the positions into x and y vars\\n\\t float x = position[0];\\n\\t float y = position[1];\\n\\n\\t\\t\\t\\treturn vec2(\\n\\t\\t 2.0 * ((x / stageWidth) - 0.5),\\n\\t\\t // invert y since we think [0,0] is bottom left in pixel space\\n\\t\\t -(2.0 * ((y / stageHeight) - 0.5)));\\n\\t\\t\\t}\\n\\n\\t\\t\\t// helper function to handle cubic easing (copied from d3 for consistency)\\n\\t\\t\\t// note there are pre-made easing functions available via glslify.\\n\\t\\t\\tfloat easeCubicInOut(float t) {\\n\\t\\t\\t\\tt *= 2.0;\\n t = (t <= 1.0 ? t * t * t : (t -= 2.0) * t * t + 2.0) / 2.0;\\n\\n if (t > 1.0) {\\n t = 1.0;\\n }\\n\\n return t;\\n\\t\\t\\t}\\n\\n\\t\\t\\tvoid main() {\\n\\t\\t\\t\\t// update the size of a point based on the prop pointWidth\\n\\t\\t\\t\\tgl_PointSize = pointWidth;\\n\\n\\t\\t\\t\\tfloat delay = delayByIndex * index;\\n\\n\\t\\t\\t\\t// number between 0 and 1 indicating how far through the animation this\\n\\t\\t\\t\\t// vertex is.\\n\\t 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\\n\\t // otherwise we are animating, so use cubic easing\\n\\t } else {\\n\\t t = easeCubicInOut((elapsed - delay) / duration);\\n\\t }\\n\\n\\t\\t\\t\\t// interpolate position\\n\\t vec2 position = mix(positionStart, positionEnd, t);\\n\\n\\t // interpolate and send color to the fragment shader\\n\\t fragColor = mix(colorStart, colorEnd, t);\\n\\n\\t\\t\\t\\t// scale to normalized device coordinates\\n\\t\\t\\t\\t// gl_Position is a special variable that holds the position of a vertex\\n\\t gl_Position = vec4(normalizeCoords(position), 0.0, 1.0);\\n\\t\\t\\t}\\n\\t\\t\\t',\n\n attributes: {\n positionStart: points.map(function (d) {\n return [d.sx, d.sy];\n }),\n positionEnd: points.map(function (d) {\n return [d.tx, d.ty];\n }),\n colorStart: points.map(function (d) {\n return d.colorStart;\n }),\n colorEnd: points.map(function (d) {\n return d.colorEnd;\n }),\n index: d3.range(points.length)\n },\n\n uniforms: {\n pointWidth: regl.prop('pointWidth'),\n stageWidth: regl.prop('stageWidth'),\n stageHeight: regl.prop('stageHeight'),\n delayByIndex: regl.prop('delayByIndex'),\n duration: regl.prop('duration'),\n\n // time in milliseconds since the prop startTime (i.e. time elapsed)\n elapsed: function elapsed(_ref, _ref2) {\n var time = _ref.time;\n var _ref2$startTime = _ref2.startTime,\n startTime = _ref2$startTime === undefined ? 0 : _ref2$startTime;\n return (time - startTime) * 1000;\n }\n },\n\n count: points.length,\n primitive: 'points'\n });\n\n return drawPoints;\n }\n\n // start animation loop (note: time is in seconds)\n function animate(layout, points) {\n console.log('animating with new layout');\n // make previous end the new beginning\n points.forEach(function (d) {\n d.sx = d.tx;\n d.sy = d.ty;\n d.colorStart = d.colorEnd;\n });\n\n // layout points\n layout(points);\n\n // copy layout x y to end positions\n var colorScale = colorScales[currentColorScale];\n points.forEach(function (d, i) {\n d.tx = d.x;\n d.ty = d.y;\n d.colorEnd = colorScale(i / points.length);\n });\n\n // create the regl function with the new start and end points\n var drawPoints = createDrawPoints(points);\n\n var frameLoop = regl.frame(function (_ref3) {\n var time = _ref3.time;\n\n if (startTime === null) {\n startTime = time;\n }\n\n // clear the buffer\n regl.clear({\n // background color (black)\n color: [0, 0, 0, 1],\n depth: 1\n });\n\n // draw the points using our created regl func\n drawPoints({\n pointWidth: pointWidth,\n stageWidth: width,\n stageHeight: height,\n duration: duration,\n delayByIndex: delayByIndex,\n startTime: startTime\n });\n\n // if we have exceeded the maximum duration, move on to the next animation\n if (time - startTime > maxDuration / 1000) {\n console.log('done animating, moving to next layout');\n frameLoop.cancel();\n\n currentLayout = (currentLayout + 1) % layouts.length;\n startTime = null;\n currentColorScale = (currentColorScale + 1) % colorScales.length;\n animate(layouts[currentLayout], points);\n }\n });\n }\n\n // create initial set of points\n var points = createPoints(numPoints, pointWidth, width, height);\n\n // initialize with all the points in the middle of the screen\n points.forEach(function (d, i) {\n d.tx = width / 2;\n d.ty = height / 2;\n d.colorEnd = [0, 0, 0];\n });\n\n animate(layouts[currentLayout], points);\n}\n\n// initialize regl\ncreateREGL({\n // callback when regl is initialized\n onDone: main\n});//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9zY3JpcHQuanM/OWE5NSJdLCJzb3VyY2VzQ29udGVudCI6WyJmdW5jdGlvbiBtYWluKGVyciwgcmVnbCkge1xuICBjb25zdCBudW1Qb2ludHMgPSAxMDAwMDA7XG4gIGNvbnN0IHBvaW50V2lkdGggPSA0O1xuICBjb25zdCBwb2ludE1hcmdpbiA9IDE7XG4gIGNvbnN0IHdpZHRoID0gd2luZG93LmlubmVyV2lkdGg7XG4gIGNvbnN0IGhlaWdodCA9IHdpbmRvdy5pbm5lckhlaWdodDtcblxuICAvLyBkdXJhdGlvbiBvZiB0aGUgYW5pbWF0aW9uIGlnbm9yaW5nIGRlbGF5c1xuICBjb25zdCBkdXJhdGlvbiA9IDE1MDA7XG5cbiAgLy8gbXVsdGlwbHkgdGhpcyB2YWx1ZSBieSB0aGUgaW5kZXggb2YgYSBwb2ludCB0byBnZXQgaXRzIGRlbGF5XG4gIGNvbnN0IGRlbGF5QnlJbmRleCA9IDUwMCAvIG51bVBvaW50cztcblxuICAvLyBpbmNsdWRlIG1heCBkZWxheSBpbiBoZXJlXG4gIGNvbnN0IG1heER1cmF0aW9uID0gZHVyYXRpb24gKyBkZWxheUJ5SW5kZXggKiBudW1Qb2ludHM7XG5cbiAgLy8gY3JlYXRlIGhlbHBlcnMgdGhhdCB3aWxsIGxheW91dCB0aGUgcG9pbnRzIGluIGRpZmZlcmVudCB3YXlzIChzZWUgY29tbW9uLmpzKVxuICBjb25zdCB0b1BoeWxsb3RheGlzID0gcG9pbnRzID0+XG4gICAgcGh5bGxvdGF4aXNMYXlvdXQocG9pbnRzLCBwb2ludFdpZHRoICsgcG9pbnRNYXJnaW4sIHdpZHRoIC8gMiwgaGVpZ2h0IC8gMik7XG4gIGNvbnN0IHRvR3JpZCA9IHBvaW50cyA9PiBncmlkTGF5b3V0KHBvaW50cywgcG9pbnRXaWR0aCArIHBvaW50TWFyZ2luLCB3aWR0aCk7XG4gIGNvbnN0IHRvU2luZSA9IHBvaW50cyA9PlxuICAgIHNpbmVMYXlvdXQocG9pbnRzLCBwb2ludFdpZHRoICsgcG9pbnRNYXJnaW4sIHdpZHRoLCBoZWlnaHQpO1xuICBjb25zdCB0b1NwaXJhbCA9IHBvaW50cyA9PlxuICAgIHNwaXJhbExheW91dChwb2ludHMsIHBvaW50V2lkdGggKyBwb2ludE1hcmdpbiwgd2lkdGgsIGhlaWdodCk7XG5cbiAgLy8gc2V0IHRoZSBvcmRlciBvZiB0aGUgbGF5b3V0cyBhbmQgc29tZSBpbml0aWFsIGFuaW1hdGlvbiBzdGF0ZVxuICBjb25zdCBsYXlvdXRzID0gW3RvUGh5bGxvdGF4aXMsIHRvU3BpcmFsLCB0b0dyaWQsIHRvU2luZV07XG4gIGxldCBjdXJyZW50TGF5b3V0ID0gMDtcbiAgbGV0IHN0YXJ0VGltZSA9IG51bGw7IC8vIGluIHNlY29uZHNcblxuICAvLyB3cmFwIGQzIGNvbG9yIHNjYWxlcyBzbyB0aGV5IHByb2R1Y2UgdmVjM3Mgd2l0aCB2YWx1ZXMgMC0xXG4gIGZ1bmN0aW9uIHdyYXBDb2xvclNjYWxlKHNjYWxlKSB7XG4gICAgcmV0dXJuIHQgPT4ge1xuICAgICAgY29uc3QgcmdiID0gZDMucmdiKHNjYWxlKDEgLSB0KSk7XG4gICAgICByZXR1cm4gW3JnYi5yIC8gMjU1LCByZ2IuZyAvIDI1NSwgcmdiLmIgLyAyNTVdO1xuICAgIH07XG4gIH1cblxuICAvLyB0aGUgb3JkZXIgb2YgY29sb3Igc2NhbGVzIHRvIGxvb3AgdGhyb3VnaFxuICBjb25zdCBjb2xvclNjYWxlcyA9IFtcbiAgICBkMy5zY2FsZVNlcXVlbnRpYWwoZDMuaW50ZXJwb2xhdGVWaXJpZGlzKSxcbiAgICBkMy5zY2FsZVNlcXVlbnRpYWwoZDMuaW50ZXJwb2xhdGVJbmZlcm5vKSxcbiAgICBkMy5zY2FsZVNlcXVlbnRpYWwoZDMuaW50ZXJwb2xhdGVDb29sKSxcbiAgXS5tYXAod3JhcENvbG9yU2NhbGUpO1xuICBsZXQgY3VycmVudENvbG9yU2NhbGUgPSAwO1xuXG4gIC8vIGZ1bmN0aW9uIHRvIGNvbXBpbGUgYSBkcmF3IHBvaW50cyByZWdsIGZ1bmNcbiAgZnVuY3Rpb24gY3JlYXRlRHJhd1BvaW50cyhwb2ludHMpIHtcbiAgICBjb25zdCBkcmF3UG9pbnRzID0gcmVnbCh7XG4gICAgICBmcmFnOiBgXG5cdFx0XHQvLyBzZXQgdGhlIHByZWNpc2lvbiBvZiBmbG9hdGluZyBwb2ludCBudW1iZXJzXG5cdFx0ICBwcmVjaXNpb24gaGlnaHAgZmxvYXQ7XG5cblx0XHQgIC8vIHRoaXMgdmFsdWUgaXMgcG9wdWxhdGVkIGJ5IHRoZSB2ZXJ0ZXggc2hhZGVyXG5cdFx0XHR2YXJ5aW5nIHZlYzMgZnJhZ0NvbG9yO1xuXG5cdFx0XHR2b2lkIG1haW4oKSB7XG5cdFx0XHRcdC8vIGdsX0ZyYWdDb2xvciBpcyBhIHNwZWNpYWwgdmFyaWFibGUgdGhhdCBob2xkcyB0aGUgY29sb3Igb2YgYSBwaXhlbFxuXHRcdFx0XHRnbF9GcmFnQ29sb3IgPSB2ZWM0KGZyYWdDb2xvciwgMSk7XG5cdFx0XHR9XG5cdFx0XHRgLFxuXG4gICAgICB2ZXJ0OiBgXG5cdFx0XHQvLyBwZXIgdmVydGV4IGF0dHJpYnV0ZXNcblx0XHRcdGF0dHJpYnV0ZSB2ZWMyIHBvc2l0aW9uU3RhcnQ7XG5cdFx0XHRhdHRyaWJ1dGUgdmVjMiBwb3NpdGlvbkVuZDtcblx0XHRcdGF0dHJpYnV0ZSBmbG9hdCBpbmRleDtcblx0XHRcdGF0dHJpYnV0ZSB2ZWMzIGNvbG9yU3RhcnQ7XG5cdFx0XHRhdHRyaWJ1dGUgdmVjMyBjb2xvckVuZDtcblxuXHRcdFx0Ly8gdmFyaWFibGVzIHRvIHNlbmQgdG8gdGhlIGZyYWdtZW50IHNoYWRlclxuXHRcdFx0dmFyeWluZyB2ZWMzIGZyYWdDb2xvcjtcblxuXHRcdFx0Ly8gdmFsdWVzIHRoYXQgYXJlIHRoZSBzYW1lIGZvciBhbGwgdmVydGljZXNcblx0XHRcdHVuaWZvcm0gZmxvYXQgcG9pbnRXaWR0aDtcblx0XHRcdHVuaWZvcm0gZmxvYXQgc3RhZ2VXaWR0aDtcblx0XHRcdHVuaWZvcm0gZmxvYXQgc3RhZ2VIZWlnaHQ7XG5cdFx0XHR1bmlmb3JtIGZsb2F0IGVsYXBzZWQ7XG5cdFx0XHR1bmlmb3JtIGZsb2F0IGR1cmF0aW9uO1xuXHRcdFx0dW5pZm9ybSBmbG9hdCBkZWxheUJ5SW5kZXg7XG5cblx0XHRcdC8vIGhlbHBlciBmdW5jdGlvbiB0byB0cmFuc2Zvcm0gZnJvbSBwaXhlbCBzcGFjZSB0byBub3JtYWxpemVkIGRldmljZSBjb29yZGluYXRlcyAoTkRDKVxuXHRcdFx0Ly8gaW4gTkRDICgwLDApIGlzIHRoZSBtaWRkbGUsICgtMSwgMSkgaXMgdGhlIHRvcCBsZWZ0IGFuZCAoMSwgLTEpIGlzIHRoZSBib3R0b20gcmlnaHQuXG5cdFx0XHR2ZWMyIG5vcm1hbGl6ZUNvb3Jkcyh2ZWMyIHBvc2l0aW9uKSB7XG5cdFx0XHRcdC8vIHJlYWQgaW4gdGhlIHBvc2l0aW9ucyBpbnRvIHggYW5kIHkgdmFyc1xuXHQgICAgICBmbG9hdCB4ID0gcG9zaXRpb25bMF07XG5cdCAgICAgIGZsb2F0IHkgPSBwb3NpdGlvblsxXTtcblxuXHRcdFx0XHRyZXR1cm4gdmVjMihcblx0XHQgICAgICAyLjAgKiAoKHggLyBzdGFnZVdpZHRoKSAtIDAuNSksXG5cdFx0ICAgICAgLy8gaW52ZXJ0IHkgc2luY2Ugd2UgdGhpbmsgWzAsMF0gaXMgYm90dG9tIGxlZnQgaW4gcGl4ZWwgc3BhY2Vcblx0XHQgICAgICAtKDIuMCAqICgoeSAvIHN0YWdlSGVpZ2h0KSAtIDAuNSkpKTtcblx0XHRcdH1cblxuXHRcdFx0Ly8gaGVscGVyIGZ1bmN0aW9uIHRvIGhhbmRsZSBjdWJpYyBlYXNpbmcgKGNvcGllZCBmcm9tIGQzIGZvciBjb25zaXN0ZW5jeSlcblx0XHRcdC8vIG5vdGUgdGhlcmUgYXJlIHByZS1tYWRlIGVhc2luZyBmdW5jdGlvbnMgYXZhaWxhYmxlIHZpYSBnbHNsaWZ5LlxuXHRcdFx0ZmxvYXQgZWFzZUN1YmljSW5PdXQoZmxvYXQgdCkge1xuXHRcdFx0XHR0ICo9IDIuMDtcbiAgICAgICAgdCA9ICh0IDw9IDEuMCA/IHQgKiB0ICogdCA6ICh0IC09IDIuMCkgKiB0ICogdCArIDIuMCkgLyAyLjA7XG5cbiAgICAgICAgaWYgKHQgPiAxLjApIHtcbiAgICAgICAgICB0ID0gMS4wO1xuICAgICAgICB9XG5cbiAgICAgICAgcmV0dXJuIHQ7XG5cdFx0XHR9XG5cblx0XHRcdHZvaWQgbWFpbigpIHtcblx0XHRcdFx0Ly8gdXBkYXRlIHRoZSBzaXplIG9mIGEgcG9pbnQgYmFzZWQgb24gdGhlIHByb3AgcG9pbnRXaWR0aFxuXHRcdFx0XHRnbF9Qb2ludFNpemUgPSBwb2ludFdpZHRoO1xuXG5cdFx0XHRcdGZsb2F0IGRlbGF5ID0gZGVsYXlCeUluZGV4ICogaW5kZXg7XG5cblx0XHRcdFx0Ly8gbnVtYmVyIGJldHdlZW4gMCBhbmQgMSBpbmRpY2F0aW5nIGhvdyBmYXIgdGhyb3VnaCB0aGUgYW5pbWF0aW9uIHRoaXNcblx0XHRcdFx0Ly8gdmVydGV4IGlzLlxuXHQgICAgICBmbG9hdCB0O1xuXG5cdCAgICAgIC8vIGRyYXdpbmcgd2l0aG91dCBhbmltYXRpb24sIHNvIHNob3cgZW5kIHN0YXRlIGltbWVkaWF0ZWx5XG5cdCAgICAgIGlmIChkdXJhdGlvbiA9PSAwLjApIHtcblx0ICAgICAgICB0ID0gMS4wO1xuXG5cdCAgICAgIC8vIHN0aWxsIGRlbGF5aW5nIGJlZm9yZSBhbmltYXRpbmdcblx0ICAgICAgfSBlbHNlIGlmIChlbGFwc2VkIDwgZGVsYXkpIHtcblx0ICAgICAgICB0ID0gMC4wO1xuXG5cdCAgICAgIC8vIG90aGVyd2lzZSB3ZSBhcmUgYW5pbWF0aW5nLCBzbyB1c2UgY3ViaWMgZWFzaW5nXG5cdCAgICAgIH0gZWxzZSB7XG5cdCAgICAgICAgdCA9IGVhc2VDdWJpY0luT3V0KChlbGFwc2VkIC0gZGVsYXkpIC8gZHVyYXRpb24pO1xuXHQgICAgICB9XG5cblx0XHRcdFx0Ly8gaW50ZXJwb2xhdGUgcG9zaXRpb25cblx0ICAgICAgdmVjMiBwb3NpdGlvbiA9IG1peChwb3NpdGlvblN0YXJ0LCBwb3NpdGlvbkVuZCwgdCk7XG5cblx0ICAgICAgLy8gaW50ZXJwb2xhdGUgYW5kIHNlbmQgY29sb3IgdG8gdGhlIGZyYWdtZW50IHNoYWRlclxuXHQgICAgICBmcmFnQ29sb3IgPSBtaXgoY29sb3JTdGFydCwgY29sb3JFbmQsIHQpO1xuXG5cdFx0XHRcdC8vIHNjYWxlIHRvIG5vcm1hbGl6ZWQgZGV2aWNlIGNvb3JkaW5hdGVzXG5cdFx0XHRcdC8vIGdsX1Bvc2l0aW9uIGlzIGEgc3BlY2lhbCB2YXJpYWJsZSB0aGF0IGhvbGRzIHRoZSBwb3NpdGlvbiBvZiBhIHZlcnRleFxuXHQgICAgICBnbF9Qb3NpdGlvbiA9IHZlYzQobm9ybWFsaXplQ29vcmRzKHBvc2l0aW9uKSwgMC4wLCAxLjApO1xuXHRcdFx0fVxuXHRcdFx0YCxcblxuICAgICAgYXR0cmlidXRlczoge1xuICAgICAgICBwb3NpdGlvblN0YXJ0OiBwb2ludHMubWFwKGQgPT4gW2Quc3gsIGQuc3ldKSxcbiAgICAgICAgcG9zaXRpb25FbmQ6IHBvaW50cy5tYXAoZCA9PiBbZC50eCwgZC50eV0pLFxuICAgICAgICBjb2xvclN0YXJ0OiBwb2ludHMubWFwKGQgPT4gZC5jb2xvclN0YXJ0KSxcbiAgICAgICAgY29sb3JFbmQ6IHBvaW50cy5tYXAoZCA9PiBkLmNvbG9yRW5kKSxcbiAgICAgICAgaW5kZXg6IGQzLnJhbmdlKHBvaW50cy5sZW5ndGgpLFxuICAgICAgfSxcblxuICAgICAgdW5pZm9ybXM6IHtcbiAgICAgICAgcG9pbnRXaWR0aDogcmVnbC5wcm9wKCdwb2ludFdpZHRoJyksXG4gICAgICAgIHN0YWdlV2lkdGg6IHJlZ2wucHJvcCgnc3RhZ2VXaWR0aCcpLFxuICAgICAgICBzdGFnZUhlaWdodDogcmVnbC5wcm9wKCdzdGFnZUhlaWdodCcpLFxuICAgICAgICBkZWxheUJ5SW5kZXg6IHJlZ2wucHJvcCgnZGVsYXlCeUluZGV4JyksXG4gICAgICAgIGR1cmF0aW9uOiByZWdsLnByb3AoJ2R1cmF0aW9uJyksXG5cbiAgICAgICAgLy8gdGltZSBpbiBtaWxsaXNlY29uZHMgc2luY2UgdGhlIHByb3Agc3RhcnRUaW1lIChpLmUuIHRpbWUgZWxhcHNlZClcbiAgICAgICAgZWxhcHNlZDogKHsgdGltZSB9LCB7IHN0YXJ0VGltZSA9IDAgfSkgPT4gKHRpbWUgLSBzdGFydFRpbWUpICogMTAwMCxcbiAgICAgIH0sXG5cbiAgICAgIGNvdW50OiBwb2ludHMubGVuZ3RoLFxuICAgICAgcHJpbWl0aXZlOiAncG9pbnRzJyxcbiAgICB9KTtcblxuICAgIHJldHVybiBkcmF3UG9pbnRzO1xuICB9XG5cbiAgLy8gc3RhcnQgYW5pbWF0aW9uIGxvb3AgKG5vdGU6IHRpbWUgaXMgaW4gc2Vjb25kcylcbiAgZnVuY3Rpb24gYW5pbWF0ZShsYXlvdXQsIHBvaW50cykge1xuICAgIGNvbnNvbGUubG9nKCdhbmltYXRpbmcgd2l0aCBuZXcgbGF5b3V0Jyk7XG4gICAgLy8gbWFrZSBwcmV2aW91cyBlbmQgdGhlIG5ldyBiZWdpbm5pbmdcbiAgICBwb2ludHMuZm9yRWFjaChkID0+IHtcbiAgICAgIGQuc3ggPSBkLnR4O1xuICAgICAgZC5zeSA9IGQudHk7XG4gICAgICBkLmNvbG9yU3RhcnQgPSBkLmNvbG9yRW5kO1xuICAgIH0pO1xuXG4gICAgLy8gbGF5b3V0IHBvaW50c1xuICAgIGxheW91dChwb2ludHMpO1xuXG4gICAgLy8gY29weSBsYXlvdXQgeCB5IHRvIGVuZCBwb3NpdGlvbnNcbiAgICBjb25zdCBjb2xvclNjYWxlID0gY29sb3JTY2FsZXNbY3VycmVudENvbG9yU2NhbGVdO1xuICAgIHBvaW50cy5mb3JFYWNoKChkLCBpKSA9PiB7XG4gICAgICBkLnR4ID0gZC54O1xuICAgICAgZC50eSA9IGQueTtcbiAgICAgIGQuY29sb3JFbmQgPSBjb2xvclNjYWxlKGkgLyBwb2ludHMubGVuZ3RoKTtcbiAgICB9KTtcblxuICAgIC8vIGNyZWF0ZSB0aGUgcmVnbCBmdW5jdGlvbiB3aXRoIHRoZSBuZXcgc3RhcnQgYW5kIGVuZCBwb2ludHNcbiAgICBjb25zdCBkcmF3UG9pbnRzID0gY3JlYXRlRHJhd1BvaW50cyhwb2ludHMpO1xuXG4gICAgY29uc3QgZnJhbWVMb29wID0gcmVnbC5mcmFtZSgoeyB0aW1lIH0pID0+IHtcbiAgICAgIGlmIChzdGFydFRpbWUgPT09IG51bGwpIHtcbiAgICAgICAgc3RhcnRUaW1lID0gdGltZTtcbiAgICAgIH1cblxuICAgICAgLy8gY2xlYXIgdGhlIGJ1ZmZlclxuICAgICAgcmVnbC5jbGVhcih7XG4gICAgICAgIC8vIGJhY2tncm91bmQgY29sb3IgKGJsYWNrKVxuICAgICAgICBjb2xvcjogWzAsIDAsIDAsIDFdLFxuICAgICAgICBkZXB0aDogMSxcbiAgICAgIH0pO1xuXG4gICAgICAvLyBkcmF3IHRoZSBwb2ludHMgdXNpbmcgb3VyIGNyZWF0ZWQgcmVnbCBmdW5jXG4gICAgICBkcmF3UG9pbnRzKHtcbiAgICAgICAgcG9pbnRXaWR0aCxcbiAgICAgICAgc3RhZ2VXaWR0aDogd2lkdGgsXG4gICAgICAgIHN0YWdlSGVpZ2h0OiBoZWlnaHQsXG4gICAgICAgIGR1cmF0aW9uLFxuICAgICAgICBkZWxheUJ5SW5kZXgsXG4gICAgICAgIHN0YXJ0VGltZSxcbiAgICAgIH0pO1xuXG4gICAgICAvLyBpZiB3ZSBoYXZlIGV4Y2VlZGVkIHRoZSBtYXhpbXVtIGR1cmF0aW9uLCBtb3ZlIG9uIHRvIHRoZSBuZXh0IGFuaW1hdGlvblxuICAgICAgaWYgKHRpbWUgLSBzdGFydFRpbWUgPiBtYXhEdXJhdGlvbiAvIDEwMDApIHtcbiAgICAgICAgY29uc29sZS5sb2coJ2RvbmUgYW5pbWF0aW5nLCBtb3ZpbmcgdG8gbmV4dCBsYXlvdXQnKTtcbiAgICAgICAgZnJhbWVMb29wLmNhbmNlbCgpO1xuXG4gICAgICAgIGN1cnJlbnRMYXlvdXQgPSAoY3VycmVudExheW91dCArIDEpICUgbGF5b3V0cy5sZW5ndGg7XG4gICAgICAgIHN0YXJ0VGltZSA9IG51bGw7XG4gICAgICAgIGN1cnJlbnRDb2xvclNjYWxlID0gKGN1cnJlbnRDb2xvclNjYWxlICsgMSkgJSBjb2xvclNjYWxlcy5sZW5ndGg7XG4gICAgICAgIGFuaW1hdGUobGF5b3V0c1tjdXJyZW50TGF5b3V0XSwgcG9pbnRzKTtcbiAgICAgIH1cbiAgICB9KTtcbiAgfVxuXG4gIC8vIGNyZWF0ZSBpbml0aWFsIHNldCBvZiBwb2ludHNcbiAgY29uc3QgcG9pbnRzID0gY3JlYXRlUG9pbnRzKG51bVBvaW50cywgcG9pbnRXaWR0aCwgd2lkdGgsIGhlaWdodCk7XG5cbiAgLy8gaW5pdGlhbGl6ZSB3aXRoIGFsbCB0aGUgcG9pbnRzIGluIHRoZSBtaWRkbGUgb2YgdGhlIHNjcmVlblxuICBwb2ludHMuZm9yRWFjaCgoZCwgaSkgPT4ge1xuICAgIGQudHggPSB3aWR0aCAvIDI7XG4gICAgZC50eSA9IGhlaWdodCAvIDI7XG4gICAgZC5jb2xvckVuZCA9IFswLCAwLCAwXTtcbiAgfSk7XG5cbiAgYW5pbWF0ZShsYXlvdXRzW2N1cnJlbnRMYXlvdXRdLCBwb2ludHMpO1xufVxuXG4vLyBpbml0aWFsaXplIHJlZ2xcbmNyZWF0ZVJFR0woe1xuICAvLyBjYWxsYmFjayB3aGVuIHJlZ2wgaXMgaW5pdGlhbGl6ZWRcbiAgb25Eb25lOiBtYWluLFxufSk7XG5cblxuXG4vLyBXRUJQQUNLIEZPT1RFUiAvL1xuLy8gc2NyaXB0LmpzIl0sIm1hcHBpbmdzIjoiOztBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBQUE7QUFFQTtBQUFBO0FBQUE7QUFDQTtBQUFBO0FBQUE7QUFFQTtBQUFBO0FBQUE7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUtBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBWUE7QUFDQTtBQStFQTtBQUNBO0FBQUE7QUFBQTtBQUNBO0FBQUE7QUFBQTtBQUNBO0FBQUE7QUFBQTtBQUNBO0FBQUE7QUFBQTtBQUNBO0FBTEE7QUFDQTtBQU9BO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFSQTtBQUNBO0FBVUE7QUFDQTtBQWxIQTtBQUNBO0FBb0hBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFDQTtBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUhBO0FBQ0E7QUFLQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBTkE7QUFDQTtBQVFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUZBIiwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///0\n")}]);
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://cdnjs.cloudflare.com/ajax/libs/regl/1.3.11/regl.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="dist_common.js"></script>
<script src="dist.js"></script>
</body>
function main(err, regl) {
const numPoints = 100000;
const pointWidth = 4;
const pointMargin = 1;
const width = window.innerWidth;
const height = window.innerHeight;
// duration of the animation ignoring delays
const duration = 1500;
// multiply this value by the index of a point to get its delay
const delayByIndex = 500 / numPoints;
// include max delay in here
const maxDuration = duration + delayByIndex * numPoints;
// create helpers that will layout the points in different ways (see common.js)
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);
// set the order of the layouts and some initial animation state
const layouts = [toPhyllotaxis, toSpiral, toGrid, toSine];
let currentLayout = 0;
let startTime = null; // in seconds
// wrap d3 color scales so they produce vec3s with values 0-1
function wrapColorScale(scale) {
return t => {
const rgb = d3.rgb(scale(1 - t));
return [rgb.r / 255, rgb.g / 255, rgb.b / 255];
};
}
// the order of color scales to loop through
const colorScales = [
d3.scaleSequential(d3.interpolateViridis),
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: `
// set the precision of floating point numbers
precision highp float;
// this value is populated by the vertex shader
varying vec3 fragColor;
void main() {
// gl_FragColor is a special variable that holds the color of a pixel
gl_FragColor = vec4(fragColor, 1);
}
`,
vert: `
// per vertex attributes
attribute vec2 positionStart;
attribute vec2 positionEnd;
attribute float index;
attribute vec3 colorStart;
attribute vec3 colorEnd;
// variables to send to the fragment shader
varying vec3 fragColor;
// values that are the same for all vertices
uniform float pointWidth;
uniform float stageWidth;
uniform float stageHeight;
uniform float elapsed;
uniform float duration;
uniform float delayByIndex;
// helper function to transform from pixel space to normalized device coordinates (NDC)
// in NDC (0,0) is the middle, (-1, 1) is the top left and (1, -1) is the bottom right.
vec2 normalizeCoords(vec2 position) {
// read in the positions into x and y vars
float x = position[0];
float y = position[1];
return vec2(
2.0 * ((x / stageWidth) - 0.5),
// invert y since we think [0,0] is bottom left in pixel space
-(2.0 * ((y / stageHeight) - 0.5)));
}
// helper function to handle cubic easing (copied from d3 for consistency)
// note there are pre-made easing functions available via glslify.
float easeCubicInOut(float t) {
t *= 2.0;
t = (t <= 1.0 ? t * t * t : (t -= 2.0) * t * t + 2.0) / 2.0;
if (t > 1.0) {
t = 1.0;
}
return t;
}
void main() {
// update the size of a point based on the prop pointWidth
gl_PointSize = pointWidth;
float delay = delayByIndex * index;
// number between 0 and 1 indicating how far through the animation this
// vertex is.
float t;
// drawing without animation, so show end state immediately
if (duration == 0.0) {
t = 1.0;
// still delaying before animating
} else if (elapsed < delay) {
t = 0.0;
// otherwise we are animating, so use cubic easing
} else {
t = easeCubicInOut((elapsed - delay) / duration);
}
// interpolate position
vec2 position = mix(positionStart, positionEnd, t);
// interpolate and send color to the fragment shader
fragColor = mix(colorStart, colorEnd, t);
// scale to normalized device coordinates
// gl_Position is a special variable that holds the position of a vertex
gl_Position = vec4(normalizeCoords(position), 0.0, 1.0);
}
`,
attributes: {
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;
}
// start animation loop (note: time is in seconds)
function animate(layout, points) {
console.log('animating with new layout');
// make previous end the new beginning
points.forEach(d => {
d.sx = d.tx;
d.sy = d.ty;
d.colorStart = d.colorEnd;
});
// layout points
layout(points);
// copy layout x y to end positions
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);
const frameLoop = regl.frame(({ time }) => {
if (startTime === null) {
startTime = time;
}
// clear the buffer
regl.clear({
// background color (black)
color: [0, 0, 0, 1],
depth: 1,
});
// draw the points using our created regl func
drawPoints({
pointWidth,
stageWidth: width,
stageHeight: height,
duration,
delayByIndex,
startTime,
});
// if we have exceeded the maximum duration, move on to the next animation
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);
// initialize with all the points in the middle of the screen
points.forEach((d, i) => {
d.tx = width / 2;
d.ty = height / 2;
d.colorEnd = [0, 0, 0];
});
animate(layouts[currentLayout], points);
}
// initialize regl
createREGL({
// callback when regl is initialized
onDone: main,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment