Skip to content

Instantly share code, notes, and snippets.

@pbeshai
Last active January 31, 2019 14:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pbeshai/bff9ebf7751f7f324a7efe472d01785a to your computer and use it in GitHub Desktop.
Save pbeshai/bff9ebf7751f7f324a7efe472d01785a to your computer and use it in GitHub Desktop.
Packed Pebbles
license: mit
height: 500
border: no

Example of creating more organic circles aka "pebbles" or "blobs" (created in response to Nadieh's tweet, but a bit more rounded with perlin noise).

Uses perlin noise for the variation, but you can experiment with different ways to vary the roundedness to get different effects.

Made with blockup

!function(n){function a(t){if(e[t])return e[t].exports;var g=e[t]={i:t,l:!1,exports:{}};return n[t].call(g.exports,g,g.exports,a),g.l=!0,g.exports}var e={};a.m=n,a.c=e,a.i=function(n){return n},a.d=function(n,e,t){a.o(n,e)||Object.defineProperty(n,e,{configurable:!1,enumerable:!0,get:t})},a.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return a.d(e,"a",e),e},a.o=function(n,a){return Object.prototype.hasOwnProperty.call(n,a)},a.p="",a(a.s=0)}([function(module,exports,__webpack_require__){"use strict";eval("\n\nvar margin = { top: 10, right: 30, bottom: 30, left: 30 };\nvar width = 960 - margin.left - margin.right;\nvar height = 500 - margin.top - margin.bottom;\n\nvar noise = window.noise; // from perlin.js\n\n/**\n * Given a set of nodes, packs them in a circle based on their `size` attribute\n * @param {*} nodes\n * @param {*} minRadius\n * @param {*} maxRadius\n */\nfunction layoutNodes(nodes) {\n var minRadius = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 4;\n var maxRadius = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 100;\n\n var sizeMax = d3.max(nodes, function (d) {\n return d.size;\n });\n\n // scale size to our radius range, use sqrt to account for area vs radius perception\n var sizeScale = d3.scaleSqrt().domain([1, sizeMax]).range([minRadius, maxRadius]).clamp(true);\n\n // use size scale to save radius\n nodes.forEach(function (d) {\n d.r = sizeScale(d.size);\n });\n\n // use d3 circle packing (requires the 'r')\n d3.packSiblings(nodes);\n}\n\n/**\n * Computes a blobby/pebbly circle for a more organic look than a pure circle.\n * Uses perlin noise to generate the blobbiness.\n *\n * @param {Number} radius The desired radius of the pebble\n * @param {Number} numPoints The resolution of the pebble\n * @param {Number} noiseAmplitude The amount of noise applied\n * @param {Number} radiusScaleFactor The amount to shrink the radius by so it fits within the expected circle size\n * @param {Function} curve The d3 curve function to apply\n * @returns {String} The path `d` attribute for the pebble\n */\nfunction pebblePath() {\n var radius = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 10;\n var numPoints = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 50;\n var noiseAmplitude = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : radius / 15;\n var radiusScaleFactor = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1;\n var curve = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : d3.curveCatmullRomClosed;\n\n var radialPoints = [];\n var noiseOffset = Math.random() * 100;\n\n for (var i = 0; i < numPoints; ++i) {\n var theta = (i / numPoints - 1) * 2 * Math.PI;\n var r = radiusScaleFactor * radius + // reduce radius so blob extension mostly stays inside\n noise.perlin2(noiseOffset + Math.sin(theta), noiseOffset + Math.cos(theta)) * noiseAmplitude;\n\n radialPoints.push([theta, r]);\n }\n\n var radialLine = d3.lineRadial().curve(curve)(radialPoints);\n return radialLine;\n}\n\n// generate data\nvar nodes = d3.range(40).map(function (d) {\n return { size: Math.random() * 20 + 5 };\n});\nlayoutNodes(nodes, 2, 40); // adds r (radius), x and y to each node\nvar nodeMargin = 4;\n\nvar colors = ['tomato', '#0bb', '#555'];\n\nvar svg = d3.select('body').append('svg').attr('width', width + margin.left + margin.right).attr('height', height + margin.top + margin.bottom);\n\nsvg.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');\nvar gNodes = svg.append('g').attr('transform', 'translate(' + width / 2 + ', ' + height / 2 + ')').attr('class', 'g-nodes');\n\nvar nodesBinding = gNodes.selectAll('.node').data(nodes);\n\nvar nodesEntering = nodesBinding.enter().append('path').attr('class', 'node');\n\nnodesBinding.merge(nodesEntering).attr('d', function (d) {\n return pebblePath(d.r - nodeMargin);\n}).attr('transform', function (d) {\n return 'translate(' + d.x + ' ' + d.y + ')';\n}).style('fill', function (d, i) {\n return colors[i % colors.length];\n});//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9zY3JpcHQuanM/OWE5NSJdLCJzb3VyY2VzQ29udGVudCI6WyJjb25zdCBtYXJnaW4gPSB7IHRvcDogMTAsIHJpZ2h0OiAzMCwgYm90dG9tOiAzMCwgbGVmdDogMzAgfTtcbmNvbnN0IHdpZHRoID0gOTYwIC0gbWFyZ2luLmxlZnQgLSBtYXJnaW4ucmlnaHQ7XG5jb25zdCBoZWlnaHQgPSA1MDAgLSBtYXJnaW4udG9wIC0gbWFyZ2luLmJvdHRvbTtcblxuY29uc3Qgbm9pc2UgPSB3aW5kb3cubm9pc2U7IC8vIGZyb20gcGVybGluLmpzXG5cbi8qKlxuICogR2l2ZW4gYSBzZXQgb2Ygbm9kZXMsIHBhY2tzIHRoZW0gaW4gYSBjaXJjbGUgYmFzZWQgb24gdGhlaXIgYHNpemVgIGF0dHJpYnV0ZVxuICogQHBhcmFtIHsqfSBub2Rlc1xuICogQHBhcmFtIHsqfSBtaW5SYWRpdXNcbiAqIEBwYXJhbSB7Kn0gbWF4UmFkaXVzXG4gKi9cbmZ1bmN0aW9uIGxheW91dE5vZGVzKG5vZGVzLCBtaW5SYWRpdXMgPSA0LCBtYXhSYWRpdXMgPSAxMDApIHtcbiAgY29uc3Qgc2l6ZU1heCA9IGQzLm1heChub2RlcywgZCA9PiBkLnNpemUpO1xuXG4gIC8vIHNjYWxlIHNpemUgdG8gb3VyIHJhZGl1cyByYW5nZSwgdXNlIHNxcnQgdG8gYWNjb3VudCBmb3IgYXJlYSB2cyByYWRpdXMgcGVyY2VwdGlvblxuICBjb25zdCBzaXplU2NhbGUgPSBkM1xuICAgIC5zY2FsZVNxcnQoKVxuICAgIC5kb21haW4oWzEsIHNpemVNYXhdKVxuICAgIC5yYW5nZShbbWluUmFkaXVzLCBtYXhSYWRpdXNdKVxuICAgIC5jbGFtcCh0cnVlKTtcblxuICAvLyB1c2Ugc2l6ZSBzY2FsZSB0byBzYXZlIHJhZGl1c1xuICBub2Rlcy5mb3JFYWNoKGQgPT4ge1xuICAgIGQuciA9IHNpemVTY2FsZShkLnNpemUpO1xuICB9KTtcblxuICAvLyB1c2UgZDMgY2lyY2xlIHBhY2tpbmcgKHJlcXVpcmVzIHRoZSAncicpXG4gIGQzLnBhY2tTaWJsaW5ncyhub2Rlcyk7XG59XG5cbi8qKlxuICogQ29tcHV0ZXMgYSBibG9iYnkvcGViYmx5IGNpcmNsZSBmb3IgYSBtb3JlIG9yZ2FuaWMgbG9vayB0aGFuIGEgcHVyZSBjaXJjbGUuXG4gKiBVc2VzIHBlcmxpbiBub2lzZSB0byBnZW5lcmF0ZSB0aGUgYmxvYmJpbmVzcy5cbiAqXG4gKiBAcGFyYW0ge051bWJlcn0gcmFkaXVzIFRoZSBkZXNpcmVkIHJhZGl1cyBvZiB0aGUgcGViYmxlXG4gKiBAcGFyYW0ge051bWJlcn0gbnVtUG9pbnRzIFRoZSByZXNvbHV0aW9uIG9mIHRoZSBwZWJibGVcbiAqIEBwYXJhbSB7TnVtYmVyfSBub2lzZUFtcGxpdHVkZSBUaGUgYW1vdW50IG9mIG5vaXNlIGFwcGxpZWRcbiAqIEBwYXJhbSB7TnVtYmVyfSByYWRpdXNTY2FsZUZhY3RvciBUaGUgYW1vdW50IHRvIHNocmluayB0aGUgcmFkaXVzIGJ5IHNvIGl0IGZpdHMgd2l0aGluIHRoZSBleHBlY3RlZCBjaXJjbGUgc2l6ZVxuICogQHBhcmFtIHtGdW5jdGlvbn0gY3VydmUgVGhlIGQzIGN1cnZlIGZ1bmN0aW9uIHRvIGFwcGx5XG4gKiBAcmV0dXJucyB7U3RyaW5nfSBUaGUgcGF0aCBgZGAgYXR0cmlidXRlIGZvciB0aGUgcGViYmxlXG4gKi9cbmZ1bmN0aW9uIHBlYmJsZVBhdGgoXG4gIHJhZGl1cyA9IDEwLFxuICBudW1Qb2ludHMgPSA1MCxcbiAgbm9pc2VBbXBsaXR1ZGUgPSByYWRpdXMgLyAxNSxcbiAgcmFkaXVzU2NhbGVGYWN0b3IgPSAxLFxuICBjdXJ2ZSA9IGQzLmN1cnZlQ2F0bXVsbFJvbUNsb3NlZFxuKSB7XG4gIGNvbnN0IHJhZGlhbFBvaW50cyA9IFtdO1xuICBjb25zdCBub2lzZU9mZnNldCA9IE1hdGgucmFuZG9tKCkgKiAxMDA7XG5cbiAgZm9yIChsZXQgaSA9IDA7IGkgPCBudW1Qb2ludHM7ICsraSkge1xuICAgIGNvbnN0IHRoZXRhID0gKGkgLyBudW1Qb2ludHMgLSAxKSAqIDIgKiBNYXRoLlBJO1xuICAgIGNvbnN0IHIgPVxuICAgICAgcmFkaXVzU2NhbGVGYWN0b3IgKiByYWRpdXMgKyAvLyByZWR1Y2UgcmFkaXVzIHNvIGJsb2IgZXh0ZW5zaW9uIG1vc3RseSBzdGF5cyBpbnNpZGVcbiAgICAgIG5vaXNlLnBlcmxpbjIoXG4gICAgICAgIG5vaXNlT2Zmc2V0ICsgTWF0aC5zaW4odGhldGEpLFxuICAgICAgICBub2lzZU9mZnNldCArIE1hdGguY29zKHRoZXRhKVxuICAgICAgKSAqXG4gICAgICAgIG5vaXNlQW1wbGl0dWRlO1xuXG4gICAgcmFkaWFsUG9pbnRzLnB1c2goW3RoZXRhLCByXSk7XG4gIH1cblxuICBjb25zdCByYWRpYWxMaW5lID0gZDMubGluZVJhZGlhbCgpLmN1cnZlKGN1cnZlKShyYWRpYWxQb2ludHMpO1xuICByZXR1cm4gcmFkaWFsTGluZTtcbn1cblxuLy8gZ2VuZXJhdGUgZGF0YVxuY29uc3Qgbm9kZXMgPSBkMy5yYW5nZSg0MCkubWFwKGQgPT4gKHsgc2l6ZTogTWF0aC5yYW5kb20oKSAqIDIwICsgNSB9KSk7XG5sYXlvdXROb2Rlcyhub2RlcywgMiwgNDApOyAvLyBhZGRzIHIgKHJhZGl1cyksIHggYW5kIHkgdG8gZWFjaCBub2RlXG5jb25zdCBub2RlTWFyZ2luID0gNDtcblxuY29uc3QgY29sb3JzID0gWyd0b21hdG8nLCAnIzBiYicsICcjNTU1J107XG5cbmNvbnN0IHN2ZyA9IGQzXG4gIC5zZWxlY3QoJ2JvZHknKVxuICAuYXBwZW5kKCdzdmcnKVxuICAuYXR0cignd2lkdGgnLCB3aWR0aCArIG1hcmdpbi5sZWZ0ICsgbWFyZ2luLnJpZ2h0KVxuICAuYXR0cignaGVpZ2h0JywgaGVpZ2h0ICsgbWFyZ2luLnRvcCArIG1hcmdpbi5ib3R0b20pO1xuXG5zdmcuYXBwZW5kKCdnJykuYXR0cigndHJhbnNmb3JtJywgYHRyYW5zbGF0ZSgke21hcmdpbi5sZWZ0fSwgJHttYXJnaW4udG9wfSlgKTtcbmNvbnN0IGdOb2RlcyA9IHN2Z1xuICAuYXBwZW5kKCdnJylcbiAgLmF0dHIoJ3RyYW5zZm9ybScsIGB0cmFuc2xhdGUoJHt3aWR0aCAvIDJ9LCAke2hlaWdodCAvIDJ9KWApXG4gIC5hdHRyKCdjbGFzcycsICdnLW5vZGVzJyk7XG5cbmNvbnN0IG5vZGVzQmluZGluZyA9IGdOb2Rlcy5zZWxlY3RBbGwoJy5ub2RlJykuZGF0YShub2Rlcyk7XG5cbmNvbnN0IG5vZGVzRW50ZXJpbmcgPSBub2Rlc0JpbmRpbmdcbiAgLmVudGVyKClcbiAgLmFwcGVuZCgncGF0aCcpXG4gIC5hdHRyKCdjbGFzcycsICdub2RlJyk7XG5cbm5vZGVzQmluZGluZ1xuICAubWVyZ2Uobm9kZXNFbnRlcmluZylcbiAgLmF0dHIoJ2QnLCBkID0+IHBlYmJsZVBhdGgoZC5yIC0gbm9kZU1hcmdpbikpXG4gIC5hdHRyKCd0cmFuc2Zvcm0nLCBkID0+IGB0cmFuc2xhdGUoJHtkLnh9ICR7ZC55fSlgKVxuICAuc3R5bGUoJ2ZpbGwnLCAoZCwgaSkgPT4gY29sb3JzW2kgJSBjb2xvcnMubGVuZ3RoXSk7XG5cblxuXG4vLyBXRUJQQUNLIEZPT1RFUiAvL1xuLy8gc2NyaXB0LmpzIl0sIm1hcHBpbmdzIjoiOztBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOzs7Ozs7QUFNQTtBQUFBO0FBQUE7QUFDQTtBQUFBO0FBQUE7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBS0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7Ozs7Ozs7Ozs7O0FBV0E7QUFNQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQ0E7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBS0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBS0E7QUFDQTtBQUNBO0FBSUE7QUFDQTtBQUNBO0FBQ0E7QUFJQTtBQUVBO0FBQUE7QUFDQTtBQUFBO0FBQ0E7QUFBQSIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///0\n")}]);
<!DOCTYPE html>
<title>Packed Pebbles</title>
<body>
<script src='https://d3js.org/d3.v5.min.js'></script>
<script src='perlin.js'></script>
<script src='dist.js'></script>
</body>
{
"standard": {
"globals": [
"d3"
]
}
}
// from https://github.com/josephg/noisejs
/*
* A speed-improved perlin and simplex noise algorithms for 2D.
*
* Based on example code by Stefan Gustavson (stegu@itn.liu.se).
* Optimisations by Peter Eastman (peastman@drizzle.stanford.edu).
* Better rank ordering method by Stefan Gustavson in 2012.
* Converted to Javascript by Joseph Gentle.
*
* Version 2012-03-09
*
* This code was placed in the public domain by its original author,
* Stefan Gustavson. You may use it as you see fit, but
* attribution is appreciated.
*
*/
!function(t){var o=t.noise={};function r(t,o,r){this.x=t,this.y=o,this.z=r}r.prototype.dot2=function(t,o){return this.x*t+this.y*o},r.prototype.dot3=function(t,o,r){return this.x*t+this.y*o+this.z*r};var n=[new r(1,1,0),new r(-1,1,0),new r(1,-1,0),new r(-1,-1,0),new r(1,0,1),new r(-1,0,1),new r(1,0,-1),new r(-1,0,-1),new r(0,1,1),new r(0,-1,1),new r(0,1,-1),new r(0,-1,-1)],e=[151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180],a=new Array(512),i=new Array(512);o.seed=function(t){t>0&&t<1&&(t*=65536),(t=Math.floor(t))<256&&(t|=t<<8);for(var o=0;o<256;o++){var r;r=1&o?e[o]^255&t:e[o]^t>>8&255,a[o]=a[o+256]=r,i[o]=i[o+256]=n[r%12]}},o.seed(0);var d=.5*(Math.sqrt(3)-1),f=(3-Math.sqrt(3))/6,h=1/6;function u(t){return t*t*t*(t*(6*t-15)+10)}function s(t,o,r){return(1-r)*t+r*o}o.simplex2=function(t,o){var r,n,e=(t+o)*d,h=Math.floor(t+e),u=Math.floor(o+e),s=(h+u)*f,l=t-h+s,w=o-u+s;l>w?(r=1,n=0):(r=0,n=1);var v=l-r+f,M=w-n+f,c=l-1+2*f,p=w-1+2*f,y=i[(h&=255)+a[u&=255]],x=i[h+r+a[u+n]],m=i[h+1+a[u+1]],q=.5-l*l-w*w,z=.5-v*v-M*M,A=.5-c*c-p*p;return 70*((q<0?0:(q*=q)*q*y.dot2(l,w))+(z<0?0:(z*=z)*z*x.dot2(v,M))+(A<0?0:(A*=A)*A*m.dot2(c,p)))},o.simplex3=function(t,o,r){var n,e,d,f,u,s,l=(t+o+r)*(1/3),w=Math.floor(t+l),v=Math.floor(o+l),M=Math.floor(r+l),c=(w+v+M)*h,p=t-w+c,y=o-v+c,x=r-M+c;p>=y?y>=x?(n=1,e=0,d=0,f=1,u=1,s=0):p>=x?(n=1,e=0,d=0,f=1,u=0,s=1):(n=0,e=0,d=1,f=1,u=0,s=1):y<x?(n=0,e=0,d=1,f=0,u=1,s=1):p<x?(n=0,e=1,d=0,f=0,u=1,s=1):(n=0,e=1,d=0,f=1,u=1,s=0);var m=p-n+h,q=y-e+h,z=x-d+h,A=p-f+2*h,b=y-u+2*h,g=x-s+2*h,j=p-1+.5,k=y-1+.5,B=x-1+.5,C=i[(w&=255)+a[(v&=255)+a[M&=255]]],D=i[w+n+a[v+e+a[M+d]]],E=i[w+f+a[v+u+a[M+s]]],F=i[w+1+a[v+1+a[M+1]]],G=.6-p*p-y*y-x*x,H=.6-m*m-q*q-z*z,I=.6-A*A-b*b-g*g,J=.6-j*j-k*k-B*B;return 32*((G<0?0:(G*=G)*G*C.dot3(p,y,x))+(H<0?0:(H*=H)*H*D.dot3(m,q,z))+(I<0?0:(I*=I)*I*E.dot3(A,b,g))+(J<0?0:(J*=J)*J*F.dot3(j,k,B)))},o.perlin2=function(t,o){var r=Math.floor(t),n=Math.floor(o);t-=r,o-=n;var e=i[(r&=255)+a[n&=255]].dot2(t,o),d=i[r+a[n+1]].dot2(t,o-1),f=i[r+1+a[n]].dot2(t-1,o),h=i[r+1+a[n+1]].dot2(t-1,o-1),l=u(t);return s(s(e,f,l),s(d,h,l),u(o))},o.perlin3=function(t,o,r){var n=Math.floor(t),e=Math.floor(o),d=Math.floor(r);t-=n,o-=e,r-=d;var f=i[(n&=255)+a[(e&=255)+a[d&=255]]].dot3(t,o,r),h=i[n+a[e+a[d+1]]].dot3(t,o,r-1),l=i[n+a[e+1+a[d]]].dot3(t,o-1,r),w=i[n+a[e+1+a[d+1]]].dot3(t,o-1,r-1),v=i[n+1+a[e+a[d]]].dot3(t-1,o,r),M=i[n+1+a[e+a[d+1]]].dot3(t-1,o,r-1),c=i[n+1+a[e+1+a[d]]].dot3(t-1,o-1,r),p=i[n+1+a[e+1+a[d+1]]].dot3(t-1,o-1,r-1),y=u(t),x=u(o),m=u(r);return s(s(s(f,v,y),s(h,M,y),m),s(s(l,c,y),s(w,p,y),m),x)}}(this);
const margin = { top: 10, right: 30, bottom: 30, left: 30 };
const width = 960 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const noise = window.noise; // from perlin.js
/**
* Given a set of nodes, packs them in a circle based on their `size` attribute
* @param {*} nodes
* @param {*} minRadius
* @param {*} maxRadius
*/
function layoutNodes(nodes, minRadius = 4, maxRadius = 100) {
const sizeMax = d3.max(nodes, d => d.size);
// scale size to our radius range, use sqrt to account for area vs radius perception
const sizeScale = d3
.scaleSqrt()
.domain([1, sizeMax])
.range([minRadius, maxRadius])
.clamp(true);
// use size scale to save radius
nodes.forEach(d => {
d.r = sizeScale(d.size);
});
// use d3 circle packing (requires the 'r')
d3.packSiblings(nodes);
}
/**
* Computes a blobby/pebbly circle for a more organic look than a pure circle.
* Uses perlin noise to generate the blobbiness.
*
* @param {Number} radius The desired radius of the pebble
* @param {Number} numPoints The resolution of the pebble
* @param {Number} noiseAmplitude The amount of noise applied
* @param {Number} radiusScaleFactor The amount to shrink the radius by so it fits within the expected circle size
* @param {Function} curve The d3 curve function to apply
* @returns {String} The path `d` attribute for the pebble
*/
function pebblePath(
radius = 10,
numPoints = 50,
noiseAmplitude = radius / 15,
radiusScaleFactor = 1,
curve = d3.curveCatmullRomClosed
) {
const radialPoints = [];
const noiseOffset = Math.random() * 100;
for (let i = 0; i < numPoints; ++i) {
const theta = (i / numPoints - 1) * 2 * Math.PI;
const r =
radiusScaleFactor * radius + // reduce radius so blob extension mostly stays inside
noise.perlin2(
noiseOffset + Math.sin(theta),
noiseOffset + Math.cos(theta)
) *
noiseAmplitude;
radialPoints.push([theta, r]);
}
const radialLine = d3.lineRadial().curve(curve)(radialPoints);
return radialLine;
}
// generate data
const nodes = d3.range(40).map(d => ({ size: Math.random() * 20 + 5 }));
layoutNodes(nodes, 2, 40); // adds r (radius), x and y to each node
const nodeMargin = 4;
const colors = ['tomato', '#0bb', '#555'];
const svg = d3
.select('body')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
svg.append('g').attr('transform', `translate(${margin.left}, ${margin.top})`);
const gNodes = svg
.append('g')
.attr('transform', `translate(${width / 2}, ${height / 2})`)
.attr('class', 'g-nodes');
const nodesBinding = gNodes.selectAll('.node').data(nodes);
const nodesEntering = nodesBinding
.enter()
.append('path')
.attr('class', 'node');
nodesBinding
.merge(nodesEntering)
.attr('d', d => pebblePath(d.r - nodeMargin))
.attr('transform', d => `translate(${d.x} ${d.y})`)
.style('fill', (d, i) => colors[i % colors.length]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment