├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── build ├── flubber.js └── flubber.min.js ├── demos ├── all-distinct.html ├── all.html ├── basic-array.html ├── basic-svg.html ├── ca.geo.json ├── circles.html ├── hi.geo.json ├── lower48.topo.json ├── medley.html ├── multiple-distinct.html ├── multiple.html ├── rects.html └── vanilla-canvas.html ├── index.js ├── package.json ├── rollup.config.js ├── src ├── add.js ├── errors.js ├── interpolate.js ├── math.js ├── multiple.js ├── normalize.js ├── order.js ├── rotate.js ├── shape.js ├── svg.js ├── topology.js └── triangulate.js └── test ├── add-test.js ├── all-test.js ├── array-test.js ├── interpolate-test.js ├── multiple-test.js ├── normalize-test.js ├── order-test.js ├── rotate-test.js ├── shapes.js ├── svg-test.js ├── triangulate-test.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | junk/* 3 | .DS_Store 4 | *.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | junk/ 2 | test/ 3 | demos/ 4 | build/flubber.js 5 | rollup.config.js 6 | .gitignore 7 | .travis.yml 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | - "6" 5 | - "5" 6 | - "4" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Noah Veltman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/veltman/flubber.svg?branch=master)](https://travis-ci.org/veltman/flubber) 2 | 3 | # flubber 4 | 5 | Some best-guess methods for smoothly interpolating between 2-D shapes. 6 | 7 | ![Flubber in action](https://user-images.githubusercontent.com/2120446/27014160-e0ce7c04-4ea7-11e7-8da4-5dde839290eb.gif) 8 | 9 | ### Why? 10 | 11 | Let's say you want to animate between two SVG paths or canvas shapes in a visualization. If you plug in their coordinates or their path strings to something like `d3.transition()`, it might work if the shapes *correspond* to each other really well - for example, turning a triangle into a different triangle. But once your shapes don't really correspond, you'll get unpredictable results with weird inversions and sudden jumps. 12 | 13 | The goal of this library is to provide a best-guess interpolation for any two arbitrary shapes (or collections of shapes) that results in a reasonably smooth animation, without overthinking it. 14 | 15 | ### Installation 16 | 17 | In a browser (exposes the `flubber` global): 18 | 19 | ```html 20 | 21 | ``` 22 | 23 | With NPM: 24 | 25 | ```sh 26 | npm install flubber 27 | ``` 28 | 29 | And then import/require it: 30 | 31 | ```js 32 | var flubber = require("flubber"); // Node classic 33 | import { interpolate } from "flubber" // ES6 34 | ``` 35 | 36 | ### How to use 37 | 38 | Flubber expects a shape input to be either an SVG path string or an array of `[x, y]` points (a "ring"): 39 | 40 | ```js 41 | "M100,100 L200,100 L150,200Z" // A triangle as a path string 42 | [[100, 100], [200, 100], [150, 200]] // A triangle as a ring 43 | ``` 44 | 45 | Flubber methods return **interpolators**, functions that you can call later with a value from 0 to 1 to get back the corresponding shape, where 0 is the beginning of the animation and 1 is the end. 46 | 47 | Using D3, usage could look something like: 48 | 49 | ```js 50 | var triangle = [[1, 0], [2, 2], [0, 2]], 51 | pentagon = [[0, 0], [2, 0], [2, 1], [1, 2], [0, 1]]; 52 | 53 | var interpolator = flubber.interpolate(triangle, pentagon); 54 | 55 | d3.select("path") 56 | .transition() 57 | .attrTween("d", function(){ return interpolator; }); 58 | ``` 59 | 60 | Without D3, usage might look something like this: 61 | ```js 62 | // Mixing and matching input types is OK 63 | var triangle = "M1,0 L2,2 L0,2 Z", 64 | pentagon = [[0, 0], [2, 0], [2, 1], [1, 2], [0, 1]]; 65 | 66 | var interpolator = flubber.interpolate(triangle, pentagon); 67 | 68 | requestAnimationFrame(draw); 69 | 70 | function draw(time) { 71 | var t = howFarAlongTheAnimationIsOnAScaleOfZeroToOne(time); 72 | myPathElement.setAttribute("d", interpolator(t)); 73 | if (t < 1) { 74 | requestAnimationFrame(draw); 75 | } 76 | } 77 | ``` 78 | 79 | Note: it doesn't matter whether your ring has a closing point identical to the first point. 80 | 81 | ### API 82 | 83 | #### flubber.interpolate(fromShape, toShape [, options]) 84 | 85 | `fromShape` and `toShape` should each be a ring or an SVG path string. If your path string includes holes or multiple shapes in a single string, everything but the first outer shape will be ignored. 86 | 87 | This returns a function that takes a value `t` from 0 to 1 and returns the in-between shape: 88 | 89 | ```js 90 | var interpolator = flubber.interpolate(triangle, octagon); 91 | 92 | interpolator(0); // returns an SVG triangle path string 93 | interpolator(0.5); // returns something halfway between the triangle and the octagon 94 | interpolator(1); // returns an SVG octagon path string 95 | ``` 96 | 97 | `options` can include the following keys: 98 | 99 | `string`: whether to output results as an SVG path string or an array of points. (default: `true`) 100 | `maxSegmentLength`: the lower this number is, the smoother the resulting animation will be, at the expense of performance. Represents a number in pixels (if no transforms are involved). Set it to `false` or `Infinity` for no smoothing. (default: `10`) 101 | 102 | [.interpolate() in action with SVG paths as input](https://veltman.github.io/flubber/demos/basic-svg.html) 103 | 104 | [.interpolate() in action with GeoJSON coordinates as input](https://veltman.github.io/flubber/demos/basic-array.html) 105 | 106 | #### flubber.toCircle(fromShape, x, y, r[, options]) 107 | 108 | Like `interpolate()`, but for the specific case of transforming the shape to a circle centered at `[x, y]` with radius `r`. 109 | 110 | ```js 111 | var interpolator = flubber.toCircle(triangle, 100, 100, 10); 112 | 113 | interpolator(0); // returns an SVG triangle path string 114 | interpolator(0.5); // returns something halfway between the triangle and the circle 115 | interpolator(1); // returns a circle path string centered at 100, 100 with a radius of 10 116 | ``` 117 | 118 | [.toCircle() in action](https://veltman.github.io/flubber/demos/circles.html) 119 | 120 | #### flubber.toRect(fromShape, x, y, width, height[, options]) 121 | 122 | Like `interpolate()`, but for the specific case of transforming the shape to a rectangle with the upper-left corner `[x, y]` and the dimensions `width` x `height`. 123 | 124 | ```js 125 | var interpolator = flubber.toRect(triangle, 10, 50, 100, 200); 126 | 127 | interpolator(0); // returns an SVG triangle path string 128 | interpolator(0.5); // returns something halfway between the triangle and the rectangle 129 | interpolator(1); // returns a rectangle path string from [10, 50] in the upper left to [110, 250] in the lower right 130 | ``` 131 | 132 | [.toRect() in action](https://veltman.github.io/flubber/demos/rects.html) 133 | 134 | #### flubber.fromCircle(x, y, r, toShape[, options]) 135 | 136 | Like `toCircle()` but reversed. 137 | 138 | #### flubber.fromRect(x, y, width, height, toShape[, options]) 139 | 140 | Like `toRect()` but reversed. 141 | 142 | #### flubber.separate(fromShape, toShapeList[, options]) 143 | 144 | If you're trying to interpolate between a single shape and multiple shapes (for example, a group of three circles turning into a single big circle), this method will break your shapes into pieces so you can animate between the two sets. This isn't terribly performant and has some quirks but it tends to get the job done. 145 | 146 | `fromShape` should be a ring or SVG path string, and `toShapeList` should be an array of them. 147 | 148 | The options are the same as for `interpolate()`, with the additional option of `single`, which defaults to `false`. 149 | 150 | If `single` is false, this returns an array of `n` interpolator functions, where `n` is the length of `toShapeList`. If `single` is set to true this returns one interpolator that combines things into one giant path string or one big array of rings. 151 | 152 | ```js 153 | // returns an array of two interpolator functions 154 | var interpolators = flubber.separate(triangle, [square, otherSquare]); 155 | 156 | d3.selectAll("path") 157 | .data(interpolators) 158 | .transition() 159 | .attrTween("d", function(interpolator) { return interpolator; }); 160 | ``` 161 | 162 | [.separate() in action](https://veltman.github.io/flubber/demos/multiple-distinct.html) 163 | 164 | ```js 165 | // returns a single interpolator function 166 | var combinedInterpolator = flubber.separate(triangle, [square, otherSquare], { single: true }); 167 | 168 | // This one path element will be two squares at the end 169 | d3.select("path") 170 | .transition() 171 | .attrTween("d", function() { return combinedInterpolator; }); 172 | ``` 173 | 174 | [.separate({ single: true }) in action](https://veltman.github.io/flubber/demos/multiple.html) 175 | 176 | #### flubber.combine(fromShapeList, toShape[, options]) 177 | 178 | Like `separate()` but reversed. 179 | 180 | #### flubber.interpolateAll(fromShapeList, toShapeList[, options]) 181 | 182 | Like `separate()` or `combine()` but instead expects two arrays of shapes the same length (e.g. an array of three triangles turning into an array of three squares). The shapes will be matched up in the order of the arrays (the first `fromShapeList` item will turn into the first `toShapeList` item, and so on). 183 | 184 | [.interpolateAll() in action](https://veltman.github.io/flubber/demos/all-distinct.html) 185 | 186 | [.interpolateAll({ single: true }) in action](https://veltman.github.io/flubber/demos/all.html) 187 | 188 | #### flubber.toPathString(ring) 189 | 190 | A helper function for converting an array of points to an SVG path string. 191 | 192 | ```js 193 | flubber.toPathString([[1, 1], [2, 1], [1.5, 2]]); 194 | // Returns "M1,1L2,1L1.5,2Z" 195 | ``` 196 | 197 | #### flubber.splitPathString(pathString) 198 | 199 | A helper function for splitting an SVG path string that might contain multiple shapes into an array of one-shape path strings. 200 | 201 | ```js 202 | flubber.splitPathString("M1,1 L2,1 L1.5,2Z M3,3 L4,3 L3.5,4 Z"); 203 | // Returns ["M1,1 L2,1 L1.5,2Z", "M3,3 L4,3 L3.5,4 Z"] 204 | ``` 205 | 206 | ### Examples 207 | 208 | *Note: most of these demos use D3 to keep the code concise, but this can be used with any library, or with no library at all.* 209 | 210 | [Morphing SVG paths](https://veltman.github.io/flubber/demos/basic-svg.html) 211 | 212 | [Morphing GeoJSON coordinates](https://veltman.github.io/flubber/demos/basic-array.html) 213 | 214 | [Morphing to and from circles](https://veltman.github.io/flubber/demos/circles.html) 215 | 216 | [Morphing to and from rectangles](https://veltman.github.io/flubber/demos/rects.html) 217 | 218 | [Morphing between one shape and multiple shapes](https://veltman.github.io/flubber/demos/multiple.html) (one element) 219 | 220 | [Morphing between one shape and multiple shapes](https://veltman.github.io/flubber/demos/multiple-distinct.html) (multiple elements) 221 | 222 | [Morphing between two sets of multiple shapes](https://veltman.github.io/flubber/demos/all.html) 223 | 224 | [Vanilla JS + Canvas](https://veltman.github.io/flubber/demos/vanilla-canvas.html) 225 | 226 | [Medley of different methods](https://veltman.github.io/flubber/demos/medley.html) 227 | 228 | ### To do 229 | 230 | * Maintain original vertices when polygonizing a path string with curves 231 | * Add `force: true` option to collapse small additional polygons onto the perimeter of the largest 232 | * Support unclosed lines 233 | * Use curves between points for `fromCircle()` and `toCircle()` 234 | * Deal with holes? 235 | * Accept SVG elements as arguments instead of just path strings? 236 | * Add pre-simplification as an option 237 | * Simulated annealing or random swapping for multishape matching? 238 | 239 | ### Video 240 | 241 | [OpenVisConf 2017 talk about shape interpolation](https://www.youtube.com/watch?v=PLc1y-gim_0) 242 | 243 | ### Alternatives 244 | 245 | [react-svg-morph](https://github.com/gorangajic/react-svg-morph) - utility for morphing between two SVGs in React 246 | 247 | [GreenSock MorphSVG plugin](https://greensock.com/morphSVG) - GSAP shape morphing utility (costs money, not open source) 248 | 249 | [d3.geo2rect](https://github.com/sebastian-meier/d3.geo2rect) - a plugin for morphing between GeoJSON and a rectangular SVG grid 250 | 251 | [d3-interpolate-path](https://github.com/pbeshai/d3-interpolate-path) - a D3 interpolator to interpolate between two unclosed lines, for things like line chart transitions with mismatched data 252 | 253 | [Wilderness](https://github.com/colinmeinke/wilderness) - an SVG manipulation and animation library 254 | 255 | [Cirque](https://github.com/two-n/cirque) - JS utility for morphing between circles and polygons 256 | 257 | ### Credits 258 | 259 | Many thanks to: 260 | 261 | * Mike Bostock for [D3](https://d3js.org/) and [TopoJSON](https://github.com/topojson/topojson) 262 | * Vladimir Agafonkin and Mapbox for [earcut](https://github.com/mapbox/earcut) 263 | * Roger Veciana Rovira for [svg-path-properties](https://github.com/rveciana/svg-path-properties) 264 | * Fontello for [svgpath](https://github.com/fontello/svgpath) 265 | * Rich Harris for [Rollup](https://github.com/rollup/rollup) and [Bublé](http://buble.surge.sh/) 266 | 267 | ### License 268 | 269 | MIT License 270 | 271 | Copyright (c) 2017 Noah Veltman 272 | 273 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 274 | 275 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 276 | 277 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 278 | -------------------------------------------------------------------------------- /build/flubber.min.js: -------------------------------------------------------------------------------- 1 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.flubber=t.flubber||{})}(this,function(t){"use strict";function n(t){return 10===t||13===t||8232===t||8233===t||32===t||9===t||11===t||12===t||160===t||t>=5760&&hn.indexOf(t)>=0}function e(t){switch(32|t){case 109:case 122:case 108:case 104:case 118:case 99:case 115:case 113:case 116:case 97:case 114:return!0}return!1}function r(t){return t>=48&&t<=57}function i(t){return t>=48&&t<=57||43===t||45===t||46===t}function a(t){this.index=0,this.path=t,this.max=t.length,this.result=[],this.param=0,this.err="",this.segmentStart=0,this.data=[]}function o(t){for(;t.index=a)return void(t.err="SvgPath: missed param (at pos "+i+")");if(n=t.path.charCodeAt(i),43!==n&&45!==n||(i++,n=i2&&(t.result.push([n,r[0],r[1]]),r=r.slice(2),e="l",n="m"===n?"l":"L"),"r"===e)t.result.push([n].concat(r));else for(;r.length>=sn[e]&&(t.result.push([n].concat(r.splice(0,sn[e]))),sn[e]););}function u(t){var n,r,a,u,c=t.max;if(t.segmentStart=t.index,n=t.path.charCodeAt(t.index),!e(n))return void(t.err="SvgPath: bad command "+t.path[t.index]+" (at pos "+t.index+")");if(a=sn[t.path[t.index].toLowerCase()],t.index++,o(t),t.data=[],!a)return void h(t);for(r=!1;;){for(u=a;u>0;u--){if(s(t),t.err.length)return;t.data.push(t.param),o(t),r=!1,t.index=t.max)break;if(!i(t.path.charCodeAt(t.index)))break}}h(t)}function c(t,n){return[t[0]*n[0]+t[2]*n[1],t[1]*n[0]+t[3]*n[1],t[0]*n[2]+t[2]*n[3],t[1]*n[2]+t[3]*n[3],t[0]*n[4]+t[2]*n[5]+t[4],t[1]*n[4]+t[3]*n[5]+t[5]]}function f(){if(!(this instanceof f))return new f;this.queue=[],this.cache=null}function l(t,n,e,r){var i=t*r-n*e<0?-1:1,a=t*e+n*r;return a>1&&(a=1),a<-1&&(a=-1),i*Math.acos(a)}function p(t,n,e,r,i,a,o,s,h,u){var c=u*(t-e)/2+h*(n-r)/2,f=-h*(t-e)/2+u*(n-r)/2,p=o*o,v=s*s,g=c*c,x=f*f,y=p*v-p*x-v*g;y<0&&(y=0),y/=p*x+v*g,y=Math.sqrt(y)*(i===a?-1:1);var d=y*o/s*f,m=y*-s/o*c,M=u*d-h*m+(t+e)/2,w=h*d+u*m+(n+r)/2,b=(c-d)/o,L=(f-m)/s,A=(-c-d)/o,q=(-f-m)/s,k=l(1,0,b,L),P=l(b,L,A,q);return 0===a&&P>0&&(P-=gn),1===a&&P<0&&(P+=gn),[M,w,k,P]}function v(t,n){var e=4/3*Math.tan(n/4),r=Math.cos(t),i=Math.sin(t),a=Math.cos(t+n),o=Math.sin(t+n);return[r,i,r-i*e,i+r*e,a+o*e,o-a*e,a,o]}function g(t,n,e){if(!(this instanceof g))return new g(t,n,e);this.rx=t,this.ry=n,this.ax=e}function x(t){if(!(this instanceof x))return new x(t);var n=un(t);this.segments=n.segments,this.err=n.err,this.__stack=[]}function y(t){var n=t.match(An);return n?n.map(Number):[]}function d(t,n,e,r,i,a,o,s){this.a={x:t,y:n},this.b={x:e,y:r},this.c={x:i,y:a},this.d={x:o,y:s},null!==o&&void 0!==o&&null!==s&&void 0!==s?(this.getArcLength=_,this.getPoint=L,this.getDerivative=M):(this.getArcLength=A,this.getPoint=b,this.getDerivative=m),this.init()}function m(t,n,e){return{x:2*(1-e)*(t[1]-t[0])+2*e*(t[2]-t[1]),y:2*(1-e)*(n[1]-n[0])+2*e*(n[2]-n[1])}}function M(t,n,e){return b([3*(t[1]-t[0]),3*(t[2]-t[1]),3*(t[3]-t[2])],[3*(n[1]-n[0]),3*(n[2]-n[1]),3*(n[3]-n[2])],e)}function w(t,n,e,r,i){for(var a=1,o=t/n,s=(t-e(r,i,o))/n;a>.001;){var h=e(r,i,o+s),u=e(r,i,o-s),c=Math.abs(t-h)/n,f=Math.abs(t-u)/n;c1&&(a=1),a<-1&&(a=-1),i*Math.acos(a)}function S(t,n,e,r,i,a,o,s,h,u){var c=u*(t-e)/2+h*(n-r)/2,f=-h*(t-e)/2+u*(n-r)/2,l=o*o,p=s*s,v=c*c,g=f*f,x=l*p-l*g-p*v;x<0&&(x=0),x/=l*g+p*v,x=Math.sqrt(x)*(i===a?-1:1);var y=x*o/s*f,d=x*-s/o*c,m=u*y-h*d+(t+e)/2,M=h*y+u*d+(n+r)/2,w=(c-y)/o,b=(f-d)/s,L=(-c-y)/o,A=(-f-d)/s,q=E(1,0,w,b),k=E(w,b,L,A);return 0===a&&k>0&&(k-=En),1===a&&k<0&&(k+=En),[m,M,q,k]}function C(t,n){var e=4/3*Math.tan(n/4),r=Math.cos(t),i=Math.sin(t),a=Math.cos(t+n),o=Math.sin(t+n);return[r,i,r-i*e,i+r*e,a+o*e,o-a*e,a,o]}function Z(t,n,e,r,i,a,o,s,h){var u=0,c=[],f=[];Sn(t,n,e,r,i,a,o,s,h).forEach(function(t){var n=new qn(t[0],t[1],t[2],t[3],t[4],t[5],t[6],t[7]),e=n.getTotalLength();u+=e,c.push(e),f.push(n)}),this.length=u,this.partialLengths=c,this.curves=f}function T(t,n,e,r){this.x0=t,this.x1=n,this.y0=e,this.y1=r}function F(t,n){return Math.sqrt((t[0]-n[0])*(t[0]-n[0])+(t[1]-n[1])*(t[1]-n[1]))}function z(t,n,e){return[t[0]+(n[0]-t[0])*e,t[1]+(n[1]-t[1])*e]}function j(t,n){return F(t,n)<1e-9}function I(t,n,e){var r=t.map(function(t,e){return V(t,n[e])});return function(t){var n=r.map(function(n){return n(t)});return e?H(n):n}}function V(t,n){return function(e){return t.map(function(t,r){return t+e*(n[r]-t)})}}function X(t){return"number"==typeof t&&isFinite(t)}function Y(t){return G(t)?an(t):[(t[0][0]+t[t.length-1][0])/2,(t[0][1]+t[t.length-1][1])/2]}function G(t){for(var n=0;n0&&(o=Math.max(o,Math.ceil(e/n)));for(var s=0;sn;)i=z(r,i,.5),t.splice(e+1,0,i)}function J(t,n){var e,r,i;if("string"==typeof t){var a=Q(t,n);t=a.ring,i=a.skipBisect}else if(!Array.isArray(t))throw new TypeError(Fn);if(e=t.slice(0),!K(e))throw new TypeError(Fn);return e.length>1&&j(e[0],e[e.length-1])&&e.pop(),r=rn(e),r>0&&e.reverse(),!i&&n&&X(n)&&n>0&&$(e,n),e}function K(t){return t.every(function(t){return Array.isArray(t)&&t.length>=2&&X(t[0])&&X(t[1])})}function tt(t,n,e){var r;return r=t.length-n.length,W(t,r<0?-1*r:0),W(n,r>0?r:0),jn(t,n),I(t,n,e)}function nt(t,n,e){e=e||2;var r=n&&n.length,i=r?n[0]*e:t.length,a=et(t,0,i,e,!0),o=[];if(!a||a.next===a.prev)return o;var s,h,u,c,f,l,p;if(r&&(a=ut(t,n,a,e)),t.length>80*e){s=u=t[0],h=c=t[1];for(var v=e;vu&&(u=f),l>c&&(c=l);p=Math.max(u-s,c-h),p=0!==p?1/p:0}return it(a,o,e,s,h,p),o}function et(t,n,e,r,i){var a,o;if(i===Zt(t,n,e,r)>0)for(a=n;a=n;a-=r)o=Et(a,t[a],t[a+1],o);return o&&wt(o,o.next)&&(St(o),o=o.next),o}function rt(t,n){if(!t)return t;n||(n=t);var e,r=t;do{if(e=!1,r.steiner||!wt(r,r.next)&&0!==Mt(r.prev,r,r.next))r=r.next;else{if(St(r),(r=n=r.prev)===r.next)break;e=!0}}while(e||r!==n);return n}function it(t,n,e,r,i,a,o){if(t){!o&&a&&vt(t,r,i,a);for(var s,h,u=t;t.prev!==t.next;)if(s=t.prev,h=t.next,a?ot(t,r,i,a):at(t))n.push(s.i/e),n.push(t.i/e),n.push(h.i/e),St(t),t=h.next,u=h.next;else if((t=h)===u){o?1===o?(t=st(rt(t),n,e),it(t,n,e,r,i,a,2)):2===o&&ht(t,n,e,r,i,a):it(rt(t),n,e,r,i,a,1);break}}}function at(t){var n=t.prev,e=t,r=t.next;if(Mt(n,e,r)>=0)return!1;for(var i=t.next.next;i!==t.prev;){if(dt(n.x,n.y,e.x,e.y,r.x,r.y,i.x,i.y)&&Mt(i.prev,i,i.next)>=0)return!1;i=i.next}return!0}function ot(t,n,e,r){var i=t.prev,a=t,o=t.next;if(Mt(i,a,o)>=0)return!1;for(var s=i.xa.x?i.x>o.x?i.x:o.x:a.x>o.x?a.x:o.x,c=i.y>a.y?i.y>o.y?i.y:o.y:a.y>o.y?a.y:o.y,f=xt(s,h,n,e,r),l=xt(u,c,n,e,r),p=t.prevZ,v=t.nextZ;p&&p.z>=f&&v&&v.z<=l;){if(p!==t.prev&&p!==t.next&&dt(i.x,i.y,a.x,a.y,o.x,o.y,p.x,p.y)&&Mt(p.prev,p,p.next)>=0)return!1;if(p=p.prevZ,v!==t.prev&&v!==t.next&&dt(i.x,i.y,a.x,a.y,o.x,o.y,v.x,v.y)&&Mt(v.prev,v,v.next)>=0)return!1;v=v.nextZ}for(;p&&p.z>=f;){if(p!==t.prev&&p!==t.next&&dt(i.x,i.y,a.x,a.y,o.x,o.y,p.x,p.y)&&Mt(p.prev,p,p.next)>=0)return!1;p=p.prevZ}for(;v&&v.z<=l;){if(v!==t.prev&&v!==t.next&&dt(i.x,i.y,a.x,a.y,o.x,o.y,v.x,v.y)&&Mt(v.prev,v,v.next)>=0)return!1;v=v.nextZ}return!0}function st(t,n,e){var r=t;do{var i=r.prev,a=r.next.next;!wt(i,a)&&bt(i,r,r.next,a)&&kt(i,a)&&kt(a,i)&&(n.push(i.i/e),n.push(r.i/e),n.push(a.i/e),St(r),St(r.next),r=t=a),r=r.next}while(r!==t);return rt(r)}function ht(t,n,e,r,i,a){var o=t;do{for(var s=o.next.next;s!==o.prev;){if(o.i!==s.i&&mt(o,s)){var h=_t(o,s);return o=rt(o,o.next),h=rt(h,h.next),it(o,n,e,r,i,a),void it(h,n,e,r,i,a)}s=s.next}o=o.next}while(o!==t)}function ut(t,n,e,r){var i,a,o,s,h,u=[];for(i=0,a=n.length;i=r.next.y&&r.next.y!==r.y){var s=r.x+(a-r.y)*(r.next.x-r.x)/(r.next.y-r.y);if(s<=i&&s>o){if(o=s,s===i){if(a===r.y)return r;if(a===r.next.y)return r.next}e=r.x=r.x&&r.x>=c&&i!==r.x&&dt(ae.x||r.x===e.x&&pt(e,r)))&&(e=r,l=h)),r=r.next}while(r!==u);return e}function pt(t,n){return Mt(t.prev,t,n.prev)<0&&Mt(n.next,t,t.next)<0}function vt(t,n,e,r){var i=t;do{null===i.z&&(i.z=xt(i.x,i.y,n,e,r)),i.prevZ=i.prev,i.nextZ=i.next,i=i.next}while(i!==t);i.prevZ.nextZ=null,i.prevZ=null,gt(i)}function gt(t){var n,e,r,i,a,o,s,h,u=1;do{for(e=t,t=null,a=null,o=0;e;){for(o++,r=e,s=0,n=0;n0||h>0&&r;)0!==s&&(0===h||!r||e.z<=r.z)?(i=e,e=e.nextZ,s--):(i=r,r=r.nextZ,h--),a?a.nextZ=i:t=i,i.prevZ=a,a=i;e=r}a.nextZ=null,u*=2}while(o>1);return t}function xt(t,n,e,r,i){return t=32767*(t-e)*i,n=32767*(n-r)*i,t=16711935&(t|t<<8),t=252645135&(t|t<<4),t=858993459&(t|t<<2),t=1431655765&(t|t<<1),n=16711935&(n|n<<8),n=252645135&(n|n<<4),n=858993459&(n|n<<2),n=1431655765&(n|n<<1),t|n<<1}function yt(t){var n=t,e=t;do{(n.x=0&&(t-o)*(r-s)-(e-o)*(n-s)>=0&&(e-o)*(a-s)-(i-o)*(r-s)>=0}function mt(t,n){return t.next.i!==n.i&&t.prev.i!==n.i&&!qt(t,n)&&(kt(t,n)&&kt(n,t)&&Pt(t,n)&&(Mt(t.prev,t,n.prev)||Mt(t,n.prev,n))||wt(t,n)&&Mt(t.prev,t,t.next)>0&&Mt(n.prev,n,n.next)>0)}function Mt(t,n,e){return(n.y-t.y)*(e.x-n.x)-(n.x-t.x)*(e.y-n.y)}function wt(t,n){return t.x===n.x&&t.y===n.y}function bt(t,n,e,r){var i=At(Mt(t,n,e)),a=At(Mt(t,n,r)),o=At(Mt(e,r,t)),s=At(Mt(e,r,n));return i!==a&&o!==s||(!(0!==i||!Lt(t,e,n))||(!(0!==a||!Lt(t,r,n))||(!(0!==o||!Lt(e,t,r))||!(0!==s||!Lt(e,n,r)))))}function Lt(t,n,e){return n.x<=Math.max(t.x,e.x)&&n.x>=Math.min(t.x,e.x)&&n.y<=Math.max(t.y,e.y)&&n.y>=Math.min(t.y,e.y)}function At(t){return t>0?1:t<0?-1:0}function qt(t,n){var e=t;do{if(e.i!==t.i&&e.next.i!==t.i&&e.i!==n.i&&e.next.i!==n.i&&bt(e,e.next,t,n))return!0;e=e.next}while(e!==t);return!1}function kt(t,n){return Mt(t.prev,t,t.next)<0?Mt(t,n,t.next)>=0&&Mt(t,t.prev,n)>=0:Mt(t,n,t.prev)<0||Mt(t,t.next,n)<0}function Pt(t,n){var e=t,r=!1,i=(t.x+n.x)/2,a=(t.y+n.y)/2;do{e.y>a!=e.next.y>a&&e.next.y!==e.y&&i<(e.next.x-e.x)*(a-e.y)/(e.next.y-e.y)+e.x&&(r=!r),e=e.next}while(e!==t);return r}function _t(t,n){var e=new Ct(t.i,t.x,t.y),r=new Ct(n.i,n.x,n.y),i=t.next,a=n.prev;return t.next=n,n.prev=t,e.next=i,i.prev=e,r.next=e,e.prev=r,a.next=r,r.prev=a,r}function Et(t,n,e,r){var i=new Ct(t,n,e);return r?(i.next=r.next,i.prev=r,r.next.prev=i,r.next=i):(i.prev=i,i.next=i),i}function St(t){t.next.prev=t.prev,t.prev.next=t.next,t.prevZ&&(t.prevZ.nextZ=t.nextZ),t.nextZ&&(t.nextZ.prevZ=t.prevZ)}function Ct(t,n,e){this.i=t,this.x=n,this.y=e,this.prev=null,this.next=null,this.z=null,this.prevZ=null,this.nextZ=null,this.steiner=!1}function Zt(t,n,e,r){for(var i=0,a=n,o=e-r;a1)for(var o,s,h=1,u=i(r[0]);hu&&(s=r[0],r[0]=r[h],r[h]=s,u=o);return r}).filter(function(t){return t.length>0})}}function It(t){return function(n,e){return Un(t(n),e)}}function Vt(t,n){var e={},r={type:"Topology",objects:{triangles:{type:"GeometryCollection",geometries:[]}},arcs:[]};return t.forEach(function(t){var i=[];t.forEach(function(t,a){var o=t[0]n;)!function(){var n=e[0],i=Qn(e)[0][0],a=e[i],o=jt(t,[n,a]);o.area=n.area+a.area,o.type="Polygon",o.arcs=o.arcs[0],e.splice(i,1),e.shift(),e.splice(r(e,o.area),0,o)}();if(n>e.length)throw new RangeError("Can't collapse topology into "+n+" pieces.");return Dn(t,t.objects.triangles).features.map(function(t){return t.geometry.coordinates[0].pop(),t.geometry.coordinates[0]})}function Yt(t){for(var n=Vn(t.reduce(function(t,n){return t.concat([n[0]],[n[1]])},[])),e=[],r=0,i=n.length;r1&&(o*=Math.sqrt(g),s*=Math.sqrt(g));var x=p(t,n,e,r,i,a,o,s,u,c),y=[],d=x[2],m=x[3],M=Math.max(Math.ceil(Math.abs(m)/(gn/4)),1);m/=M;for(var w=0;wMath.abs(u-a)?(u-i)/h:h/(u-a))/Math.PI,this.ax>=0?(this.rx=Math.sqrt(u),this.ry=Math.sqrt(c)):(this.ax+=90,this.rx=Math.sqrt(c),this.ry=Math.sqrt(u)),this},g.prototype.isDegenerate=function(){return this.rx<1e-10*this.ry||this.ry<1e-10*this.rx};var dn=g;x.prototype.__matrix=function(t){var n,e=this;t.queue.length&&this.iterate(function(r,i,a,o){var s,h,u,c;switch(r[0]){case"v":s=t.calc(0,r[1],!0),h=0===s[0]?["v",s[1]]:["l",s[0],s[1]];break;case"V":s=t.calc(a,r[1],!1),h=s[0]===t.calc(a,o,!1)[0]?["V",s[1]]:["L",s[0],s[1]];break;case"h":s=t.calc(r[1],0,!0),h=0===s[1]?["h",s[0]]:["l",s[0],s[1]];break;case"H":s=t.calc(r[1],o,!1),h=s[1]===t.calc(a,o,!1)[1]?["H",s[0]]:["L",s[0],s[1]];break;case"a":case"A":var f=t.toArray(),l=dn(r[1],r[2],r[3]).transform(f);if(f[0]*f[3]-f[1]*f[2]<0&&(r[5]=r[5]?"0":"1"),s=t.calc(r[6],r[7],"a"===r[0]),"A"===r[0]&&r[6]===a&&r[7]===o||"a"===r[0]&&0===r[6]&&0===r[7]){h=["a"===r[0]?"l":"L",s[0],s[1]];break}h=l.isDegenerate()?["a"===r[0]?"l":"L",s[0],s[1]]:[r[0],l.rx,l.ry,l.ax,r[4],r[5],s[0],s[1]];break;case"m":c=i>0,s=t.calc(r[1],r[2],c),h=["m",s[0],s[1]];break;default:for(u=r[0],h=[u],c=u.toLowerCase()===u,n=1;n=0;)t.matrix(e.__stack[n].toArray());this.__matrix(t),this.__stack=[]}},x.prototype.toString=function(){var t,n,e=this,r=[];this.__evaluateStack();for(var i=0;i0&&"m"!==n&&"M"!==n&&n===e.segments[i-1][0],r=r.concat(t?e.segments[i].slice(1):e.segments[i]);return r.join(" ").replace(/ ?([achlmqrstvz]) ?/gi,"$1").replace(/ \-/g,"-").replace(/zm/g,"z m")},x.prototype.translate=function(t,n){return this.__stack.push(cn().translate(t,n||0)),this},x.prototype.scale=function(t,n){return this.__stack.push(cn().scale(t,n||0===n?n:t)),this},x.prototype.rotate=function(t,n,e){return this.__stack.push(cn().rotate(t,n||0,e||0)),this},x.prototype.skewX=function(t){return this.__stack.push(cn().skewX(t)),this},x.prototype.skewY=function(t){return this.__stack.push(cn().skewY(t)),this},x.prototype.matrix=function(t){return this.__stack.push(cn().matrix(t)),this},x.prototype.transform=function(t){return t.trim()?(this.__stack.push(vn(t)),this):this},x.prototype.round=function(t){var n,e=0,r=0,i=0,a=0;return t=t||0,this.__evaluateStack(),this.segments.forEach(function(o){var s=o[0].toLowerCase()===o[0];switch(o[0]){case"H":case"h":return s&&(o[1]+=i),i=o[1]-o[1].toFixed(t),void(o[1]=+o[1].toFixed(t));case"V":case"v":return s&&(o[1]+=a),a=o[1]-o[1].toFixed(t),void(o[1]=+o[1].toFixed(t));case"Z":case"z":return i=e,void(a=r);case"M":case"m":return s&&(o[1]+=i,o[2]+=a),i=o[1]-o[1].toFixed(t),a=o[2]-o[2].toFixed(t),e=i,r=a,o[1]=+o[1].toFixed(t),void(o[2]=+o[2].toFixed(t));case"A":case"a":return s&&(o[6]+=i,o[7]+=a),i=o[6]-o[6].toFixed(t),a=o[7]-o[7].toFixed(t),o[1]=+o[1].toFixed(t),o[2]=+o[2].toFixed(t),o[3]=+o[3].toFixed(t+2),o[6]=+o[6].toFixed(t),void(o[7]=+o[7].toFixed(t));default:return n=o.length,s&&(o[n-2]+=i,o[n-1]+=a),i=o[n-2]-o[n-2].toFixed(t),a=o[n-1]-o[n-1].toFixed(t),void o.forEach(function(n,e){e&&(o[e]=+o[e].toFixed(t))})}}),this},x.prototype.iterate=function(t,n){var e,r,i,a=this.segments,o={},s=!1,h=0,u=0,c=0,f=0;if(n||this.__evaluateStack(),a.forEach(function(n,e){var r=t(n,e,h,u);Array.isArray(r)&&(o[e]=r,s=!0);var i=n[0]===n[0].toLowerCase();switch(n[0]){case"m":case"M":return h=n[1]+(i?h:0),u=n[2]+(i?u:0),c=h,void(f=u);case"h":case"H":return void(h=n[1]+(i?h:0));case"v":case"V":return void(u=n[1]+(i?u:0));case"z":case"Z":return h=c,void(u=f);default:h=n[n.length-2]+(i?h:0),u=n[n.length-1]+(i?u:0)}}),!s)return this;for(i=[],e=0;e2&&(n.push([e].concat(r.splice(0,2))),i="l",e="m"===e?"l":"L");r.length>=0;){if(r.length===wn[i])return r.unshift(e),n.push(r);if(r.length0?{x:e.x/r,y:e.y/r}:{x:0,y:0}},getPropertiesAtLength:function(t){var n,e=w(t,this.length,this.getArcLength,[this.a.x,this.b.x,this.c.x,this.d.x],[this.a.y,this.b.y,this.c.y,this.d.y]),r=this.getDerivative([this.a.x,this.b.x,this.c.x,this.d.x],[this.a.y,this.b.y,this.c.y,this.d.y],e),i=Math.sqrt(r.x*r.x+r.y*r.y);n=i>0?{x:r.x/i,y:r.y/i}:{x:0,y:0};var a=this.getPoint([this.a.x,this.b.x,this.c.x,this.d.x],[this.a.y,this.b.y,this.c.y,this.d.y],e);return{x:a.x,y:a.y,tangentX:n.x,tangentY:n.y}}};var kn=[[],[],[-.5773502691896257,.5773502691896257],[0,-.7745966692414834,.7745966692414834],[-.33998104358485626,.33998104358485626,-.8611363115940526,.8611363115940526],[0,-.5384693101056831,.5384693101056831,-.906179845938664,.906179845938664],[.6612093864662645,-.6612093864662645,-.2386191860831969,.2386191860831969,-.932469514203152,.932469514203152],[0,.4058451513773972,-.4058451513773972,-.7415311855993945,.7415311855993945,-.9491079123427585,.9491079123427585],[-.1834346424956498,.1834346424956498,-.525532409916329,.525532409916329,-.7966664774136267,.7966664774136267,-.9602898564975363,.9602898564975363],[0,-.8360311073266358,.8360311073266358,-.9681602395076261,.9681602395076261,-.3242534234038089,.3242534234038089,-.6133714327005904,.6133714327005904],[-.14887433898163122,.14887433898163122,-.4333953941292472,.4333953941292472,-.6794095682990244,.6794095682990244,-.8650633666889845,.8650633666889845,-.9739065285171717,.9739065285171717],[0,-.26954315595234496,.26954315595234496,-.5190961292068118,.5190961292068118,-.7301520055740494,.7301520055740494,-.8870625997680953,.8870625997680953,-.978228658146057,.978228658146057],[-.1252334085114689,.1252334085114689,-.3678314989981802,.3678314989981802,-.5873179542866175,.5873179542866175,-.7699026741943047,.7699026741943047,-.9041172563704749,.9041172563704749,-.9815606342467192,.9815606342467192],[0,-.2304583159551348,.2304583159551348,-.44849275103644687,.44849275103644687,-.6423493394403402,.6423493394403402,-.8015780907333099,.8015780907333099,-.9175983992229779,.9175983992229779,-.9841830547185881,.9841830547185881],[-.10805494870734367,.10805494870734367,-.31911236892788974,.31911236892788974,-.5152486363581541,.5152486363581541,-.6872929048116855,.6872929048116855,-.827201315069765,.827201315069765,-.9284348836635735,.9284348836635735,-.9862838086968123,.9862838086968123],[0,-.20119409399743451,.20119409399743451,-.3941513470775634,.3941513470775634,-.5709721726085388,.5709721726085388,-.7244177313601701,.7244177313601701,-.8482065834104272,.8482065834104272,-.937273392400706,.937273392400706,-.9879925180204854,.9879925180204854],[-.09501250983763744,.09501250983763744,-.2816035507792589,.2816035507792589,-.45801677765722737,.45801677765722737,-.6178762444026438,.6178762444026438,-.755404408355003,.755404408355003,-.8656312023878318,.8656312023878318,-.9445750230732326,.9445750230732326,-.9894009349916499,.9894009349916499],[0,-.17848418149584785,.17848418149584785,-.3512317634538763,.3512317634538763,-.5126905370864769,.5126905370864769,-.6576711592166907,.6576711592166907,-.7815140038968014,.7815140038968014,-.8802391537269859,.8802391537269859,-.9506755217687678,.9506755217687678,-.9905754753144174,.9905754753144174],[-.0847750130417353,.0847750130417353,-.2518862256915055,.2518862256915055,-.41175116146284263,.41175116146284263,-.5597708310739475,.5597708310739475,-.6916870430603532,.6916870430603532,-.8037049589725231,.8037049589725231,-.8926024664975557,.8926024664975557,-.9558239495713977,.9558239495713977,-.9915651684209309,.9915651684209309],[0,-.16035864564022537,.16035864564022537,-.31656409996362983,.31656409996362983,-.46457074137596094,.46457074137596094,-.600545304661681,.600545304661681,-.7209661773352294,.7209661773352294,-.8227146565371428,.8227146565371428,-.9031559036148179,.9031559036148179,-.96020815213483,.96020815213483,-.9924068438435844,.9924068438435844],[-.07652652113349734,.07652652113349734,-.22778585114164507,.22778585114164507,-.37370608871541955,.37370608871541955,-.5108670019508271,.5108670019508271,-.636053680726515,.636053680726515,-.7463319064601508,.7463319064601508,-.8391169718222188,.8391169718222188,-.912234428251326,.912234428251326,-.9639719272779138,.9639719272779138,-.9931285991850949,.9931285991850949],[0,-.1455618541608951,.1455618541608951,-.2880213168024011,.2880213168024011,-.4243421202074388,.4243421202074388,-.5516188358872198,.5516188358872198,-.6671388041974123,.6671388041974123,-.7684399634756779,.7684399634756779,-.8533633645833173,.8533633645833173,-.9200993341504008,.9200993341504008,-.9672268385663063,.9672268385663063,-.9937521706203895,.9937521706203895],[-.06973927331972223,.06973927331972223,-.20786042668822127,.20786042668822127,-.34193582089208424,.34193582089208424,-.469355837986757,.469355837986757,-.5876404035069116,.5876404035069116,-.6944872631866827,.6944872631866827,-.7878168059792081,.7878168059792081,-.8658125777203002,.8658125777203002,-.926956772187174,.926956772187174,-.9700604978354287,.9700604978354287,-.9942945854823992,.9942945854823992],[0,-.1332568242984661,.1332568242984661,-.26413568097034495,.26413568097034495,-.3903010380302908,.3903010380302908,-.5095014778460075,.5095014778460075,-.6196098757636461,.6196098757636461,-.7186613631319502,.7186613631319502,-.8048884016188399,.8048884016188399,-.8767523582704416,.8767523582704416,-.9329710868260161,.9329710868260161,-.9725424712181152,.9725424712181152,-.9947693349975522,.9947693349975522],[-.06405689286260563,.06405689286260563,-.1911188674736163,.1911188674736163,-.3150426796961634,.3150426796961634,-.4337935076260451,.4337935076260451,-.5454214713888396,.5454214713888396,-.6480936519369755,.6480936519369755,-.7401241915785544,.7401241915785544,-.820001985973903,.820001985973903,-.8864155270044011,.8864155270044011,-.9382745520027328,.9382745520027328,-.9747285559713095,.9747285559713095,-.9951872199970213,.9951872199970213]],Pn=[[],[],[1,1],[.8888888888888888,.5555555555555556,.5555555555555556],[.6521451548625461,.6521451548625461,.34785484513745385,.34785484513745385],[.5688888888888889,.47862867049936647,.47862867049936647,.23692688505618908,.23692688505618908],[.3607615730481386,.3607615730481386,.46791393457269104,.46791393457269104,.17132449237917036,.17132449237917036],[.4179591836734694,.3818300505051189,.3818300505051189,.27970539148927664,.27970539148927664,.1294849661688697,.1294849661688697],[.362683783378362,.362683783378362,.31370664587788727,.31370664587788727,.22238103445337448,.22238103445337448,.10122853629037626,.10122853629037626],[.3302393550012598,.1806481606948574,.1806481606948574,.08127438836157441,.08127438836157441,.31234707704000286,.31234707704000286,.26061069640293544,.26061069640293544],[.29552422471475287,.29552422471475287,.26926671930999635,.26926671930999635,.21908636251598204,.21908636251598204,.1494513491505806,.1494513491505806,.06667134430868814,.06667134430868814],[.2729250867779006,.26280454451024665,.26280454451024665,.23319376459199048,.23319376459199048,.18629021092773426,.18629021092773426,.1255803694649046,.1255803694649046,.05566856711617366,.05566856711617366],[.24914704581340277,.24914704581340277,.2334925365383548,.2334925365383548,.20316742672306592,.20316742672306592,.16007832854334622,.16007832854334622,.10693932599531843,.10693932599531843,.04717533638651183,.04717533638651183],[.2325515532308739,.22628318026289723,.22628318026289723,.2078160475368885,.2078160475368885,.17814598076194574,.17814598076194574,.13887351021978725,.13887351021978725,.09212149983772845,.09212149983772845,.04048400476531588,.04048400476531588],[.2152638534631578,.2152638534631578,.2051984637212956,.2051984637212956,.18553839747793782,.18553839747793782,.15720316715819355,.15720316715819355,.12151857068790319,.12151857068790319,.08015808715976021,.08015808715976021,.03511946033175186,.03511946033175186],[.2025782419255613,.19843148532711158,.19843148532711158,.1861610000155622,.1861610000155622,.16626920581699392,.16626920581699392,.13957067792615432,.13957067792615432,.10715922046717194,.10715922046717194,.07036604748810812,.07036604748810812,.03075324199611727,.03075324199611727],[.1894506104550685,.1894506104550685,.18260341504492358,.18260341504492358,.16915651939500254,.16915651939500254,.14959598881657674,.14959598881657674,.12462897125553388,.12462897125553388,.09515851168249279,.09515851168249279,.062253523938647894,.062253523938647894,.027152459411754096,.027152459411754096],[.17944647035620653,.17656270536699264,.17656270536699264,.16800410215645004,.16800410215645004,.15404576107681028,.15404576107681028,.13513636846852548,.13513636846852548,.11188384719340397,.11188384719340397,.08503614831717918,.08503614831717918,.0554595293739872,.0554595293739872,.02414830286854793,.02414830286854793],[.1691423829631436,.1691423829631436,.16427648374583273,.16427648374583273,.15468467512626524,.15468467512626524,.14064291467065065,.14064291467065065,.12255520671147846,.12255520671147846,.10094204410628717,.10094204410628717,.07642573025488905,.07642573025488905,.0497145488949698,.0497145488949698,.02161601352648331,.02161601352648331],[.1610544498487837,.15896884339395434,.15896884339395434,.15276604206585967,.15276604206585967,.1426067021736066,.1426067021736066,.12875396253933621,.12875396253933621,.11156664554733399,.11156664554733399,.09149002162245,.09149002162245,.06904454273764123,.06904454273764123,.0448142267656996,.0448142267656996,.019461788229726478,.019461788229726478],[.15275338713072584,.15275338713072584,.14917298647260374,.14917298647260374,.14209610931838204,.14209610931838204,.13168863844917664,.13168863844917664,.11819453196151841,.11819453196151841,.10193011981724044,.10193011981724044,.08327674157670475,.08327674157670475,.06267204833410907,.06267204833410907,.04060142980038694,.04060142980038694,.017614007139152118,.017614007139152118],[.14608113364969041,.14452440398997005,.14452440398997005,.13988739479107315,.13988739479107315,.13226893863333747,.13226893863333747,.12183141605372853,.12183141605372853,.10879729916714838,.10879729916714838,.09344442345603386,.09344442345603386,.0761001136283793,.0761001136283793,.057134425426857205,.057134425426857205,.036953789770852494,.036953789770852494,.016017228257774335,.016017228257774335],[.13925187285563198,.13925187285563198,.13654149834601517,.13654149834601517,.13117350478706238,.13117350478706238,.12325237681051242,.12325237681051242,.11293229608053922,.11293229608053922,.10041414444288096,.10041414444288096,.08594160621706773,.08594160621706773,.06979646842452049,.06979646842452049,.052293335152683286,.052293335152683286,.03377490158481415,.03377490158481415,.0146279952982722,.0146279952982722],[.13365457218610619,.1324620394046966,.1324620394046966,.12890572218808216,.12890572218808216,.12304908430672953,.12304908430672953,.11499664022241136,.11499664022241136,.10489209146454141,.10489209146454141,.09291576606003515,.09291576606003515,.07928141177671895,.07928141177671895,.06423242140852585,.06423242140852585,.04803767173108467,.04803767173108467,.030988005856979445,.030988005856979445,.013411859487141771,.013411859487141771],[.12793819534675216,.12793819534675216,.1258374563468283,.1258374563468283,.12167047292780339,.12167047292780339,.1155056680537256,.1155056680537256,.10744427011596563,.10744427011596563,.09761865210411388,.09761865210411388,.08619016153195327,.08619016153195327,.0733464814110803,.0733464814110803,.05929858491543678,.05929858491543678,.04427743881741981,.04427743881741981,.028531388628933663,.028531388628933663,.0123412297999872,.0123412297999872]],_n=[[1],[1,1],[1,2,1],[1,3,3,1]],En=2*Math.PI,Sn=function(t,n,e,r,i,a,o,s,h){var u=Math.sin(i*En/360),c=Math.cos(i*En/360),f=c*(t-s)/2+u*(n-h)/2,l=-u*(t-s)/2+c*(n-h)/2;if(0===f&&0===l)return[];if(0===e||0===r)return[];e=Math.abs(e),r=Math.abs(r);var p=f*f/(e*e)+l*l/(r*r);p>1&&(e*=Math.sqrt(p),r*=Math.sqrt(p));var v=S(t,n,s,h,a,o,e,r,u,c),g=[],x=v[2],y=v[3],d=Math.max(Math.ceil(Math.abs(y)/(En/4)),1);y/=d;for(var m=0;mthis.length&&(t=this.length);for(var e=this.partialLengths.length-1;this.partialLengths[e]>=t&&this.partialLengths[e]>0;)e--;ethis.length&&(t=this.length);for(var e=this.partialLengths.length-1;this.partialLengths[e]>=t&&this.partialLengths[e]>0;)e--;e0&&["C","c","S","s"].indexOf(s[c-1][0])>-1?new qn(h[0],h[1],2*h[0]-s[c-1][s[c-1].length-4],2*h[1]-s[c-1][s[c-1].length-3],s[c][1],s[c][2],s[c][3],s[c][4]):new qn(h[0],h[1],h[0],h[1],s[c][1],s[c][2],s[c][3],s[c][4]),e+=a.getTotalLength(),h=[s[c][3],s[c][4]],i.push(a)):"s"===s[c][0]?(a=c>0&&["C","c","S","s"].indexOf(s[c-1][0])>-1?new qn(h[0],h[1],h[0]+a.d.x-a.c.x,h[1]+a.d.y-a.c.y,h[0]+s[c][1],h[1]+s[c][2],h[0]+s[c][3],h[1]+s[c][4]):new qn(h[0],h[1],h[0],h[1],h[0]+s[c][1],h[1]+s[c][2],h[0]+s[c][3],h[1]+s[c][4]),e+=a.getTotalLength(),h=[s[c][3]+h[0],s[c][4]+h[1]],i.push(a)):"Q"===s[c][0]?(a=h[0]!=s[c][1]&&h[1]!=s[c][2]?new qn(h[0],h[1],s[c][1],s[c][2],s[c][3],s[c][4]):new Zn(s[c][1],s[c][3],s[c][2],s[c][4]),e+=a.getTotalLength(),i.push(a),h=[s[c][3],s[c][4]],u=[s[c][1],s[c][2]]):"q"===s[c][0]?(a=0!=s[c][1]||0!=s[c][2]?new qn(h[0],h[1],h[0]+s[c][1],h[1]+s[c][2],h[0]+s[c][3],h[1]+s[c][4]):new Zn(h[0]+s[c][1],h[0]+s[c][3],h[1]+s[c][2],h[1]+s[c][4]),e+=a.getTotalLength(),u=[h[0]+s[c][1],h[1]+s[c][2]],h=[s[c][3]+h[0],s[c][4]+h[1]],i.push(a)):"T"===s[c][0]?(a=c>0&&["Q","q","T","t"].indexOf(s[c-1][0])>-1?new qn(h[0],h[1],2*h[0]-u[0],2*h[1]-u[1],s[c][1],s[c][2]):new Zn(h[0],s[c][1],h[1],s[c][2]),i.push(a),e+=a.getTotalLength(),u=[2*h[0]-u[0],2*h[1]-u[1]],h=[s[c][1],s[c][2]]):"t"===s[c][0]?(a=c>0&&["Q","q","T","t"].indexOf(s[c-1][0])>-1?new qn(h[0],h[1],2*h[0]-u[0],2*h[1]-u[1],h[0]+s[c][1],h[1]+s[c][2]):new Zn(h[0],h[0]+s[c][1],h[1],h[1]+s[c][2]),e+=a.getTotalLength(),u=[2*h[0]-u[0],2*h[1]-u[1]],h=[s[c][1]+h[0],s[c][2]+h[0]],i.push(a)):"A"===s[c][0]?(a=new Cn(h[0],h[1],s[c][1],s[c][2],s[c][3],s[c][4],s[c][5],s[c][6],s[c][7]),e+=a.getTotalLength(),h=[s[c][6],s[c][7]],i.push(a)):"a"===s[c][0]&&(a=new Cn(h[0],h[1],s[c][1],s[c][2],s[c][3],s[c][4],s[c][5],h[0]+s[c][6],h[1]+s[c][7]),e+=a.getTotalLength(),h=[h[0]+s[c][6],h[1]+s[c][7]],i.push(a)),r.push(e);return n}var e=0,r=[],i=[];n.getTotalLength=function(){return e},n.getPointAtLength=function(t){var n=a(t);return i[n.i].getPointAtLength(n.fraction)},n.getTangentAtLength=function(t){var n=a(t);return i[n.i].getTangentAtLength(n.fraction)},n.getPropertiesAtLength=function(t){var n=a(t);return i[n.i].getPropertiesAtLength(n.fraction)};var a=function(t){t<0?t=0:t>e&&(t=e);for(var n=r.length-1;r[n]>=t&&r[n]>0;)n--;return n++,{fraction:t-r[n-1],i:n}};return n(t)},Fn='All shapes must be supplied as arrays of [x, y] points or an SVG path string (https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d).\nExample valid ways of supplying a shape would be:\n[[0, 0], [10, 0], [10, 10]]\n"M0,0 L10,0 L10,10Z"\n',zn="flubber.all() expects two arrays of equal length as arguments. Each element in both arrays should be an array of [x, y] points or an SVG path string (https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d).",jn=function(t,n){for(var e,r,i,a=t.length,o=1/0,s=0;s0&&(r+=t[i-1].length,e.holes.push(r))}return e},Vn.default=Xn;var Yn=function(t){return t},Gn=function(t){if(null==t)return Yn;var n,e,r=t.scale[0],i=t.scale[1],a=t.translate[0],o=t.translate[1];return function(t,s){s||(n=e=0);var h=2,u=t.length,c=new Array(u);for(c[0]=(n+=t[0])*r+a,c[1]=(e+=t[1])*i+o;h>>1;t[i]n?1:t>=n?0:NaN},Rn=function(t){return 1===t.length&&(t=It(t)),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[a],e)<0?r=a+1:i=a}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[a],e)>0?i=a:r=a+1}return r}}},Bn=Rn(Un),Wn=(Bn.right,Math.sqrt(50),Math.sqrt(10),Math.sqrt(2),function(t,n){return Xt(Vt(Yt(t),t),n)}),$n=function(t,n){if(t.length>8)return t.map(function(t,n){return n});var e=t.map(function(t){return n.map(function(n){return Ot(t,n)})});return Gt(t,n,e)};t.interpolate=In,t.separate=Dt,t.combine=Ht,t.interpolateAll=Nt,t.splitPathString=N,t.toPathString=H,t.fromCircle=Ut,t.toCircle=Rt,t.fromRect=Bt,t.toRect=Wt,Object.defineProperty(t,"__esModule",{value:!0})}); 3 | -------------------------------------------------------------------------------- /demos/all-distinct.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 72 | -------------------------------------------------------------------------------- /demos/all.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 75 | -------------------------------------------------------------------------------- /demos/basic-array.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 51 | -------------------------------------------------------------------------------- /demos/basic-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 55 | -------------------------------------------------------------------------------- /demos/ca.geo.json: -------------------------------------------------------------------------------- 1 | {"type":"Polygon","coordinates":[[[290.413,20],[298.092,20.386],[310.971,20.667],[320.606,20.971],[327.646,20.699],[331.19,21.04],[342.433,21.187],[354.408,21.063],[369.683,21.375],[390.43,21.587],[396.577,21.454],[401.794,21.536],[425.943,20.948],[437.336,20.428],[438.238,43.695],[439.16,67.325],[440.1,87.713],[440.476,96.991],[441.143,115.842],[441.754,133.842],[442.177,149.951],[442.91,163.167],[457.621,175.095],[478.696,191.95],[494.197,204.255],[508.213,215.382],[528.883,231.483],[535.774,236.957],[550.175,248.075],[566.202,260.334],[577.087,268.669],[599.149,285.358],[614.02,296.527],[626.362,305.704],[644.082,318.791],[656.377,327.821],[673.335,340.205],[674.604,347.118],[676.258,348.02],[678.467,351.502],[682.237,353.697],[683.985,356.976],[684.662,359.465],[687.444,362.34],[687.773,366.002],[689.785,366.02],[693.385,368.09],[695.199,369.585],[697.39,370.091],[699.373,372.157],[699.871,374.429],[698.706,374.94],[695.594,379.484],[694.25,379.862],[692.934,381.835],[689.343,383.914],[688.732,385.064],[689.08,388.376],[685.809,393.956],[687.557,396.781],[686.608,397.852],[688.196,401.785],[688.243,403.538],[689.211,405.332],[688.13,406.077],[688.036,408.046],[688.807,409.872],[688.168,411.009],[689.174,412.858],[687.895,414.127],[685.724,418.442],[685.668,419.942],[681.955,421.57],[683.327,425.08],[682.302,427.049],[684.878,428.351],[685.621,433.898],[684.897,437.427],[686.965,440.242],[689.296,440.453],[690.979,439.759],[694.222,440.2],[695.98,444.207],[696.985,445.541],[697.23,448.821],[694.955,452.027],[695.434,453.757],[691.797,455.836],[688.516,455.827],[687.369,457.027],[664.508,462.313],[640.707,467.681],[610.505,474.268],[583.254,480],[582.314,475.791],[581.59,474.255],[580.744,473.118],[579.7,472.53],[578.131,472.58],[577.971,471.936],[578.779,471.002],[580.509,471.6],[580.763,472.364],[582.483,474.972],[582.934,476.205],[583.818,475.391],[582.868,473.795],[582.812,472.852],[582.088,471.913],[580.02,470.795],[580.076,470.174],[578.544,470.404],[577.407,471.467],[576.57,471.84],[576.41,468.601],[576.147,467.538],[574.728,464.87],[575.687,463.914],[575.799,462.271],[574.324,456.728],[573.675,455.197],[573.092,454.626],[571.673,450.96],[569.661,448.149],[562.461,439.984],[558.419,437.477],[555.674,435.039],[551.989,433.139],[550.006,430.991],[547.796,429.501],[545.258,428.098],[543.049,427.504],[540.85,426.23],[535.642,421.722],[532.869,420.503],[531.459,420.682],[531.252,422.035],[530.482,421.547],[529.87,420.112],[528.47,420.508],[527.558,421.961],[527.915,423.148],[526.703,423.525],[524.324,422.854],[523.967,422.389],[521.815,422.242],[521.025,420.636],[522.266,418.893],[522.341,417.315],[519.634,411.643],[517.82,409.214],[515.056,407.351],[513.919,407.319],[512.292,407.715],[509.453,407.784],[509.294,408.198],[506.38,408.313],[504.923,408.929],[503.992,410.148],[501.933,408.515],[498.314,408.253],[496.528,407.522],[495.203,407.269],[493.868,406.445],[492.9,406.408],[491.622,405.709],[490.447,405.475],[489.967,405.806],[487.072,403.851],[486.066,403.717],[484.026,400.143],[483.537,398.225],[482.607,397.264],[481.996,397.415],[480.492,396.564],[479.044,395.143],[478.179,395.253],[476.45,393.542],[475.435,392.829],[474.382,392.668],[473.63,392.121],[471.825,391.619],[470.659,390.69],[470.133,390.865],[468.366,390.511],[465.405,391.146],[464.587,391.978],[463.525,391.928],[461.165,391.021],[459.295,391.155],[457.349,391.734],[455.469,390.442],[453.965,390.377],[451.681,389.181],[450.553,389.121],[448.24,389.434],[446.445,388.79],[444.828,388.993],[439.705,389.255],[434.159,390.322],[432.946,390.796],[432.157,390.442],[430.465,386.844],[428.415,385.892],[427.55,385.285],[425.708,385.386],[425.059,384.876],[424.561,383.726],[426.178,378.886],[426.272,377.585],[424.627,374.884],[425.426,371.761],[425.52,369.778],[424.458,368.725],[424.251,367.92],[423.057,367.322],[424.195,362.533],[424.514,360.781],[424.58,358.807],[423.894,355.491],[422.465,354.837],[421.299,353.793],[419.495,353.678],[418.855,354.566],[417.784,353.637],[415.312,352.487],[414.165,351.452],[413.272,350.155],[413.328,348.945],[414.409,346.037],[415.913,345.711],[415.02,344.161],[414.409,343.977],[414.193,342.242],[413.488,340.862],[412.614,340.103],[411.044,340.237],[410.32,339.726],[408.59,339.561],[404.53,335.135],[403.289,332.578],[402.781,332.366],[401.681,330.775],[400.629,331.028],[399.736,330.264],[397.564,329.463],[395.665,326.993],[395.28,324.431],[394.688,323.332],[392.629,321.602],[391.031,319.757],[389.48,318.35],[388.343,313.552],[387.647,312.503],[385.203,311.68],[384.131,310.157],[384.047,309.472],[383.079,308.464],[382.59,307.034],[380.635,304.554],[378.548,302.788],[375.718,300.98],[375.248,301.086],[374.233,300.249],[373.829,299.085],[372.692,298.206],[371.799,297.01],[371.441,293.477],[370.99,291.545],[369.74,288.114],[369.9,287.111],[370.492,286.513],[370.21,284.645],[369.43,284.733],[368.471,283.974],[369.571,282.216],[370.078,280.754],[371.507,281.674],[372.099,282.607],[372.795,282.345],[374.073,280.924],[375.013,278.546],[375.436,275.197],[375.963,272.418],[375.38,270.937],[373.152,266.397],[371.329,264.331],[370.078,263.876],[368.612,265.026],[366.741,264.653],[366.581,265.233],[365.171,265.339],[363.376,264.994],[361.487,263.927],[359.343,261.824],[357.529,259.837],[356.467,257.974],[355.386,256.971],[354.164,256.87],[354.23,256.061],[353.374,255.283],[352.998,254.11],[351.946,253.715],[351.617,253.084],[351.1,250.219],[351.429,249.404],[351.852,244.91],[351.419,244.156],[350.15,241.051],[349.981,239.114],[349.445,238.125],[347.96,237.877],[347.217,236.386],[347.452,234.933],[347.302,233.511],[347.922,232.936],[348.223,231.662],[348.289,229.799],[347.603,225.507],[347.556,224.086],[349.577,222.913],[351.316,222.641],[352.5,223.644],[352.688,224.983],[353.779,226.652],[353.468,227.393],[352.293,227.614],[352.463,229.247],[352.961,230.333],[352.604,231.501],[353.384,231.713],[352.97,232.849],[353.572,233.415],[354.672,233.589],[355.894,234.215],[357.501,234.486],[358.215,235.705],[359.785,236.303],[360.415,237.656],[362.333,238.143],[363.658,240.259],[364.41,240.572],[366.064,240.443],[365.538,239.454],[365.143,237.959],[363.931,237.863],[363.32,237.15],[363.028,235.935],[361.863,234.137],[361.233,229.923],[360.105,228.152],[359.231,228.207],[357.783,227.034],[357.369,226.036],[357.896,225.778],[359.315,226.022],[359.127,225.282],[358.272,225.433],[356.552,225.033],[354.69,224.081],[354.822,222.995],[356.11,221.684],[355.79,220.13],[354.841,217.903],[353.44,218.11],[351.692,216.712],[350.949,215.116],[352.19,215.525],[352.293,214.83],[353.675,213.989],[353.421,212.774],[354.596,213.202],[356.025,212.848],[356.881,212.181],[356.937,211.486],[357.943,210.695],[358.817,210.548],[360.396,211.068],[361.073,212.047],[361.863,212.264],[364.419,211.081],[367.427,210.681],[369.937,211.247],[371.911,211.413],[373.876,212.291],[376.123,212.655],[377.204,212.457],[378.294,212.857],[379.563,212.949],[379.901,212.54],[378.99,211.67],[379.732,211.201],[380.277,209.278],[382.073,208.997],[382.759,208.501],[383.708,208.855],[383.605,210.29],[382.233,210.345],[381.791,211.21],[382.853,212.494],[383.652,211.955],[383.878,210.304],[384.207,210.129],[387.666,212.218],[387.412,211.137],[385.006,210.203],[384.122,208.597],[381.96,208.202],[381.302,208.846],[379.92,208.846],[379.582,210.373],[379.196,210.902],[377.551,211.965],[376.574,211.923],[375.878,211.284],[376.085,210.405],[377.758,209.54],[377.345,209.108],[375.183,210.332],[373.528,209.867],[372.118,210.635],[370.802,210.847],[371.122,209.563],[368.696,209.756],[367.004,208.602],[368.123,208.004],[367.568,206.702],[365.463,206.872],[364.927,207.461],[364.673,208.501],[362.906,210.488],[362.549,211.357],[361.458,211.293],[360.471,210.24],[358.958,210.189],[357.106,209.752],[355.988,208.091],[352.312,205.86],[351.758,206.803],[348.787,208.202],[348.966,209.572],[348.026,212.489],[350.019,213.46],[348.524,214.821],[348.984,216.201],[348.082,217.025],[349.125,217.471],[349.201,218],[350.564,219.15],[349.37,219.877],[349.125,218.906],[348.232,218.561],[348.214,219.596],[349.031,220.235],[349.313,221.606],[349.031,221.937],[347.057,222.347],[345.318,220.383],[344.256,219.518],[343.504,219.297],[342.574,218.239],[341.521,217.843],[340.581,218.193],[339.378,217.986],[338.588,216.523],[337.375,215.741],[336.783,214.389],[335.815,213.285],[332.544,211.551],[331.999,211.394],[330.354,211.831],[329.009,213.349],[328.304,213],[329.319,210.663],[331.002,206.026],[330.993,205.018],[330.448,204.121],[330.466,203.339],[329.404,201.808],[329.714,201.407],[331.952,204.517],[333.23,206.711],[334.085,207.732],[334.79,207.723],[333.371,205.547],[331.604,202.534],[330.354,201.453],[330.156,200.027],[329.122,198.601],[328.088,197.709],[327.439,197.727],[326.81,198.505],[326.321,197.194],[326.64,196.035],[325.973,193.827],[325.014,192.368],[324.441,190.956],[322.862,189.461],[319.778,187.851],[319.045,186.94],[316.629,185.008],[316.479,184.061],[314.298,181.259],[313.838,180.086],[313.123,179.088],[310.961,176.604],[309.805,176.07],[309.297,174.81],[308.244,173.669],[307.21,173.117],[305.227,170.928],[304.71,169.796],[302.933,167.703],[301.843,165.656],[302.501,165.449],[303.356,164.037],[303.977,162.224],[303.798,160.702],[302.924,157.091],[302.332,156.589],[302.445,155.816],[301.298,154.082],[300.574,150.388],[300.113,149.928],[300.367,149.031],[300.095,147.743],[299.427,146.598],[299.907,141.943],[300.649,139.725],[301.815,137.154],[301.862,136.239],[301.27,134.624],[301.382,131.813],[301.1,130.038],[300.668,129.062],[300.113,128.754],[299.681,126.629],[299.728,125.603],[299.202,123.151],[298.252,122.599],[297.284,121.528],[296.513,119.471],[295.63,118.648],[294.84,116.771],[294.05,116.012],[292.932,114.333],[291.259,113.142],[291.08,111.122],[290.102,109.664],[287.602,108.482],[286.408,106.725],[285.12,105.745],[282.911,103.721],[281.379,101.885],[282.065,99.167],[281.52,97.911],[281.548,96.342],[280.467,94.461],[280.129,93.109],[280.665,92.101],[281.125,90.017],[282.573,86.829],[283.673,84.989],[284.114,83.637],[285.59,81.139],[286.944,78.407],[287.245,78.83],[286.605,79.86],[286.098,81.24],[286.991,81.59],[287.752,81.171],[287.592,79.456],[288.241,79.106],[288.833,76.976],[289.341,76.378],[291.277,76.277],[292.556,75.486],[292.706,74.386],[292.105,73.885],[291.475,74.166],[290.826,73.572],[290.196,73.65],[289.576,75.283],[288.692,76.406],[288.429,77.229],[287.527,78.508],[287.207,78.043],[289.397,74.281],[291.108,70.141],[292.105,66.437],[292.114,65.186],[290.883,64.257],[290.478,62.38],[290.478,60.499],[291.174,60.246],[291.935,58.535],[293.327,53.498],[293.74,51.037],[294.633,46.63],[294.097,42.563],[294.614,41.749],[293.439,40.41],[293.524,38.713],[292.34,35.755],[292.415,35.033],[291.794,32.324],[290.817,31.799],[290.337,32.14],[289.284,31.008],[288.25,30.3],[289.331,28.616],[289.942,26.914],[290.742,21.757],[290.413,20]]]} 2 | -------------------------------------------------------------------------------- /demos/circles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 58 | -------------------------------------------------------------------------------- /demos/hi.geo.json: -------------------------------------------------------------------------------- 1 | {"type":"MultiPolygon","coordinates":[[[[743.5872675202813,307.48241759969096],[746.2895624940131,309.5353697793862],[749.3538912939046,308.71812747376134],[754.1084086839987,308.9335629871571],[758.9184126382213,310.70067034739304],[768.2836930616327,313.72677683554866],[780.0899518446363,319.5897565007285],[802.0901043198803,333.1490943733638],[809.132406316452,340.02119814048865],[813.0315924369006,345.0309868144195],[813.2355947525655,350.5313721333737],[813.1561942009623,357.8610079629034],[813.623487160469,363.6211954122182],[817.3140962014431,363.3931853851468],[822.4273447389664,360.87275995231175],[826.271507111516,363.4738819122663],[828.0848276529155,366.36958199153196],[827.7559892922247,372.38599512903875],[829.5617346669835,376.0664474302148],[831.8919998404308,379.4920034646593],[838.6814500212622,385.57531158374377],[845.7701731535783,388.5233423946006],[848.6454580281047,390.9077760236478],[850.1961491778911,393.803085865678],[849.8551592639888,397.5695242013662],[848.215764834717,399.79575954581924],[842.4131200776642,407.07671916578965],[836.5932511929359,409.3731803521723],[828.3455488683464,417.6666013803847],[822.6982599166813,419.4099493090662],[822.5212572455539,421.01129802566993],[817.7726430169873,422.27274233666185],[813.793490440185,425.37561222121394],[799.2828810316028,429.9405338219649],[793.6678197591202,428.6506547912345],[790.4507421455969,428.620362281461],[787.5679016860088,429.6060110359972],[784.508089166545,431.60386243990615],[780.4135478520991,434.59937556179466],[778.0179386727041,437.6163022069859],[775.300611281431,438.0918307531342],[773.2609070155443,439.08723212595487],[768.4485607728259,442.5096546985915],[761.7506780406247,446.4586163667859],[759.7684023626489,447.17180771686617],[754.1089416977909,452.7998030513518],[753.1382283140233,455.09330108610266],[753.4230392010472,460.1796742409997],[751.0268756839978,462.8619193562199],[746.9222329629035,467.05077934780365],[744.8348840674855,472.9485907653748],[742.9513855591936,475.1304207776002],[737.477475203197,479.99999999999955],[736.2971641337781,479.3137197885876],[736.3270617727671,476.26820824140304],[733.4661725094245,474.0465933500868],[728.0689957828079,471.29825766953127],[721.6577800670457,467.53259925570774],[718.2870731277751,465.31044000049224],[710.1613358189707,463.2229684995573],[708.3623043545131,458.12687069326057],[706.6454933559796,457.6214289291361],[705.4925625574178,453.89540118642844],[704.3619447075655,447.9724998565921],[705.7824514060828,440.8925994908809],[708.4904092247605,420.519594200307],[708.1885230172402,416.96650451485175],[706.3430215963074,413.7505365073357],[704.6708982789849,409.3582297790076],[701.3882724673499,400.0320926054155],[700.051844473111,398.3315233417861],[698.6878338263548,395.46625018729947],[698.9602955595983,392.4048387646294],[697.3678098253613,389.5535358849829],[696.5572538658726,386.33802170383615],[695.0596251121024,382.4464323465245],[692.7089680134368,379.05421791664867],[690.6618707904813,377.52232528965897],[687.7626210677718,374.128038286834],[684.4574679699449,365.32481014316863],[685.7218231871327,361.3480072748839],[687.1746260291493,358.01839044593],[689.9654218544181,355.46490418538133],[692.6742439705904,353.5557141348563],[693.8812718273962,352.1480466076223],[694.5617700051218,349.4851648665067],[696.4022826793735,347.9309404441483],[701.9255274981351,345.8059731923245],[704.5236964861899,341.11509249303253],[705.8179890945237,339.49530463000883],[708.7064689184069,334.15683162445566],[711.6307321308686,330.7582642527159],[713.6739198072687,330.6668366384142],[715.0781216578105,324.43732909124174],[714.1097386524832,321.75760668819976],[711.8732247017604,319.67632702566607],[707.1488375931453,312.7036163878315],[705.1421030445447,306.5990076447674],[704.7480496847343,301.6823802490426],[704.6871405874415,297.1241579476946],[707.0919878827792,292.14813926653414],[708.8174526736734,290.56757133122665],[713.5911657522756,290.44855262963847],[722.2264777071068,292.2989113770009],[725.9510519865635,292.9876068650037],[727.1421738850399,294.70775639634894],[727.32090343404,296.06384628528485],[729.5421999833559,299.58711161093606],[734.6439553483397,301.5248232067729],[738.0116678917223,303.37739551553796],[740.370399639648,306.1039583054144],[743.5872675202813,307.48241759969096]]],[[[620.1397311619189,254.3564545041986],[618.7977719240698,256.9679904839122],[615.0520574776315,257.0509580771827],[611.5316625485775,256.4807926447752],[605.3428535785985,257.86396948814763],[601.0216711424965,258.3911297468485],[598.1709388978411,255.53539463911602],[599.7429632607473,253.46540700980813],[602.5236795035455,250.96986180200065],[606.1904288429187,248.48897447853005],[610.1664934713037,246.2156646411604],[613.6082451969401,244.9256950113845],[616.3643550914451,245.16088560070148],[619.7432349962821,248.89434962954783],[618.0560910070028,253.03213051239345],[620.1397311619189,254.3564545041986]]],[[[569.6639150943172,226.6714655873534],[563.2622249019471,227.0742299891931],[560.5645338550455,223.33414848277152],[559.9068004175949,220.06791078755487],[559.6374678264589,214.2011320618717],[559.1353745241868,211.2205204287834],[556.8659516538708,209.9160324620093],[551.3561736719936,207.81378615878612],[549.7000997647606,205.1846016506356],[550.7111449479709,202.24535074994583],[553.8129684241813,200.62649215367992],[559.2292097040836,199.89481392934067],[572.1331431885194,201.6053109432637],[578.0441748259898,206.74033553612117],[582.3305425518233,211.58655090118464],[583.7498297459056,213.88468666591598],[584.0695854062997,215.63106418441248],[583.2824874548476,218.02508150030917],[580.4974992405075,222.47986194707073],[574.5251627848488,225.28075166264534],[569.6639150943172,226.6714655873534]]],[[[611.890733941601,185.25265737964992],[612.9911548632855,186.5753691597729],[614.308711513751,186.79396172577117],[618.1616108358633,188.5645806151383],[618.8240039777513,190.33234862541985],[620.3641505883284,190.77596056871198],[620.786342848707,194.27473563147169],[622.642279081883,195.90972147634693],[623.8467410802396,197.6666905501129],[624.8257239888329,199.8554787981452],[626.3518722429119,202.26221891741625],[628.6703718647435,202.05912584888347],[630.1167937586763,200.32388183031526],[632.6508983971328,200.31749734692448],[638.4538364821783,199.7024339550385],[639.9190420846594,199.34110711168478],[644.1030348340487,196.22758831052033],[646.1683510572618,195.44872881503352],[650.4300833634827,195.18294512805778],[652.5968686976144,195.4920834700365],[655.3729803145568,196.37117528974886],[656.0987663680185,197.2006763669583],[657.7591726847122,196.63136986706377],[659.8925848579593,197.939622015052],[660.5800584541048,199.5498971625534],[663.8748051008705,203.03525917840398],[666.2807232796878,204.60704879056493],[668.0337996187524,206.2554596246373],[668.2271492820757,207.465741117991],[670.4482741638519,207.5834816798838],[670.9001404361652,206.82114403282776],[672.6552609648817,208.14319609323275],[674.2816234895414,211.10221768886777],[678.814466034909,211.67778862545993],[683.3300464525819,213.78359461618538],[689.8680621038834,215.83410669021714],[692.8838825331454,221.79752151654202],[692.2925206043767,227.35972682045985],[689.9286296465266,231.8208869479181],[686.6952220020912,232.78306250537025],[684.0269509726415,236.80009394025183],[680.4660117805277,236.88515050271963],[673.3802304546996,240.65082878067642],[668.6349854587379,240.61579215151096],[665.9925837036387,240.3782760937488],[663.5650982304235,240.578792016237],[657.026686830281,243.69386900036852],[650.6176121785079,247.03252832284943],[644.0040037753276,245.5568329571165],[642.3008135039763,246.09165091484192],[636.7340074398726,246.3888784988594],[633.010588972677,242.98263382410732],[632.5822618688975,241.56145027791354],[630.8289648826958,239.6954756228215],[631.9405601512351,238.7217993602021],[631.9575258075415,236.2131627242402],[630.8094739821713,227.04250633339234],[629.7241158908091,224.52613078427294],[629.7440856752289,221.6900297696061],[628.8773040356657,219.39257430606267],[626.0237184692203,217.29972274038118],[623.9265293260196,217.39407466748344],[622.0583746811508,218.27144147165018],[621.3694990933715,220.21316826527254],[619.7114844665224,220.52909383843325],[615.87497222707,218.11092251600576],[612.9980335382816,217.64498190367385],[610.1386471095434,215.87937031889714],[608.2601069411648,216.08415803699017],[606.5117331930846,213.7838037808324],[603.3291336712095,210.4873544574939],[599.5934465768364,205.760325875523],[599.3920418474916,202.70268167991208],[598.0681565491733,201.04844917210494],[599.3162895423585,194.27560922248585],[600.4651207977918,190.94990345215228],[601.8038708296251,188.77505841183165],[603.464822069751,188.5696468986216],[604.5660594010767,187.35321742701672],[604.6214822252286,186.26283653592373],[605.804135900542,185.4190115744218],[608.4582986916156,185.65934142893138],[610.7838093893333,184.58197857358982],[611.890733941601,185.25265737964992]]],[[[524.7130123402292,159.25673206943884],[526.8130519250132,159.59561866774038],[529.9078194391489,159.39385575965662],[531.5610522683248,159.83693291932923],[532.6609416468607,161.4799684740883],[534.6510889013939,161.45750689446595],[553.8461931602353,164.10212486464934],[558.8994842079131,164.28927849736647],[559.9251807668315,162.71586337874578],[560.7127276309653,160.32248617217329],[562.0057737426357,160.00384013756366],[563.7926818753558,162.29914661780413],[564.4368603634491,164.92035419110107],[567.4147562137057,165.80550245353515],[571.1378559585926,166.60455909849907],[577.7695199521394,166.30900091651392],[580.6653024399842,165.8801767264731],[583.8579993531142,164.70709910184132],[587.3878731244124,164.72843346920445],[590.6267898966687,165.55725102458155],[591.9416780485485,165.29023036249282],[593.2611506731666,166.93200852792052],[595.3643209268041,166.83480415080203],[596.3545045613116,167.49480750705288],[593.8947787317295,172.10229009035902],[589.8919915089455,176.08533838532276],[584.9456950429609,178.88840016739277],[580.0169526152804,181.00969275435955],[574.9468790959415,182.15033597894035],[569.8036364514367,181.7022000974407],[564.6718757439077,180.71670361673887],[554.4263352844982,178.1549138935602],[548.1481233049967,176.0516744814695],[539.2036440529067,176.87677598955906],[533.0210330337117,177.28126442946223],[528.3770724380392,178.0211891945687],[524.6332111432266,177.83754518377964],[521.7234989489539,178.02190616077496],[519.1390450923539,177.02237353182454],[517.621360371995,174.96745110508664],[517.551015948913,172.3640033184547],[518.5571196495862,170.1634618652547],[519.9971918556214,168.45760149045236],[522.3585261039386,167.53061120459824],[524.4695468015411,165.03443827866113],[525.0369599972918,162.31363109952144],[523.3809323819048,160.2302840517168],[524.7130123402292,159.25673206943884]]],[[[439.3476058510271,107.5878433988778],[440.19098540231585,110.01777933819221],[441.6142577093245,110.76838085793679],[442.3639539875006,113.19811082544038],[444.69106077801575,113.39818859688557],[446.95982645174615,116.48351756239481],[446.99414642553546,119.65592562433358],[445.27355427850193,120.4902837932209],[446.2987708355376,126.65741812608167],[449.1507667872309,127.6965609501417],[451.4173982723584,131.06851876889095],[453.41637922998655,131.73196460186136],[453.6940511884438,132.76022146069636],[455.0268116562123,133.2339679305437],[457.13132772462336,130.4391217618795],[455.13711653933865,129.12216311265183],[455.33289536979305,127.5342008311909],[456.7622660035818,126.8857509231043],[459.9976227275444,127.27284255739596],[461.61822705422776,126.81213877278242],[461.9940602057617,127.84185677777077],[460.0772423008637,129.79250650790345],[459.7140498902679,132.31835956169516],[459.97423967565504,134.28489798198189],[462.16096716992797,135.60449256744278],[463.87046946913074,137.2964730269432],[464.34251441867974,138.5020779873862],[463.7638249082023,140.10185430069396],[464.236304057044,141.69529707210359],[465.6614408297942,143.85602126042568],[468.5300264314306,145.7048493457055],[471.2925736475649,147.1258837902551],[472.1470929570242,147.9729706480498],[470.6206681868386,149.7427370097439],[466.14031035357,154.11679371687342],[464.71376749777636,154.5772701203432],[464.3407111132702,152.42371988801187],[463.2020694562881,151.48153880219843],[459.68719082197293,152.4003259422252],[456.83119034350733,152.75521725264707],[454.46232545087224,153.96493016880095],[452.7517208536056,155.36095040470673],[450.6648334316559,155.44486812768992],[449.7177362370475,154.9728072123371],[448.01769849738093,152.25230230563966],[441.4864446256469,149.42148290588466],[439.5060057530822,146.04848653091813],[438.5618789997779,145.2052047577954],[436.85175730875034,146.88078656583184],[438.172864799674,148.38000710055576],[432.67768601442026,148.82711745288634],[433.53349299314056,147.7089344990145],[432.49462227908947,146.86493042638767],[430.4108995435904,146.95107921786803],[429.85527748706716,142.55857601028356],[434.0300690200314,140.4255620690401],[434.5956128493302,139.20763470811607],[430.74897208590045,136.84777370346183],[429.67745982054794,139.19699873956824],[426.83907240953164,138.44043778649348],[427.6849527689425,140.59119395910193],[427.0206036666598,141.05591980636746],[422.9576793901623,137.67639008644437],[423.8996941782568,139.9256379184444],[426.0697825444775,142.17259752916016],[428.90311244033126,144.51707188583896],[428.2336813224728,146.66365347713418],[425.8633948351357,147.6833635246794],[417.905166033862,149.34053164527995],[415.44278763431134,149.61360405041023],[413.2939100693295,150.2505551448644],[410.52265461637376,148.66623607851488],[409.307593912893,143.990294850189],[407.9003689072873,140.24998545605695],[404.88125432927836,136.23144442339026],[402.4224323391571,135.01233055943612],[401.77070437001385,131.27770318586545],[400.92545825579555,129.31538566691688],[397.1475363453794,126.41320306347643],[395.56196456080613,123.98306539039822],[395.28763966185664,117.0822432549844],[394.40710443856676,115.83954391111638],[388.72238858550656,111.1859420648534],[389.5759056629797,110.72142080353706],[394.3145506915053,109.80037288778294],[399.90393806113906,109.44159137505403],[405.7766872078133,109.5497046360033],[407.4275580808476,109.10018633003028],[409.4710453555782,109.55951641039974],[410.52110961276014,107.04368766020934],[412.8943254955243,105.65060040585149],[416.68891158200773,102.58799336963239],[416.4955477260156,101.77486033373089],[418.64643448104545,97.60023590399169],[423.36746719054815,93.35853501647716],[427.53788347351,92.1632242178539],[430.1998947389669,92.81560734778759],[431.8257376521317,94.80634606685726],[433.97892818354626,96.56011431950856],[434.067993139079,98.05366040935996],[435.2085236656652,98.61434278687011],[435.29644745514344,99.82928598997387],[436.43146027093263,101.04529995148414],[435.8409989950065,103.56873656941161],[437.5504024192514,105.90243783915639],[439.3476058510271,107.5878433988778]]],[[[136.34083774324534,82.89448801717754],[134.62642001098044,83.40449195336578],[132.90959787225052,82.14581032932028],[131.4506916125006,81.64646175464031],[129.80385082210887,76.74514380474466],[130.14500207612204,73.57146214321665],[131.59809444597076,70.28672504122505],[136.72992436362588,65.06115908881202],[140.8499176595349,61.94456184724777],[146.1572277117772,58.91105132922394],[147.58743859663932,55.37274121110113],[147.45467965877356,52.79482393899616],[149.80446666834254,51.75459116210004],[152.03218112111801,51.83599080153499],[154.51679793033745,52.75659961598649],[155.63574459370858,55.19272854543442],[152.9048388312652,58.474509515848695],[151.94434371467972,61.47338560082744],[152.90503375345878,65.23809369245691],[151.19281993240298,67.64426977712628],[146.95757916228936,69.74879455425298],[144.48075843527175,70.51007085978745],[141.54772375007562,72.36648668800308],[139.83714396241632,73.88420909919478],[138.81320748223047,75.98927607222868],[136.34083774324534,82.89448801717754]]],[[[242.6301239188358,20.928456517277937],[244.524835317996,21.855376138727024],[246.6086163941788,21.761013035977157],[249.12129206432147,23.10146816821816],[250.1117660430242,25.197631494994766],[251.76251507809565,26.88421878666668],[252.6631608942832,27.058905701170715],[254.04950445149436,31.70327047207047],[255.17233874299583,32.345092794198536],[254.9367696203331,37.7407614473218],[253.70304905956502,39.4145429917744],[251.80739785296336,44.7114156766952],[249.90968751849374,45.82769180800642],[249.72427274907935,49.724662359280046],[250.2664460742688,54.83944813011158],[249.8178817833537,58.55608165339845],[248.01038403767052,58.553854051773214],[246.5378587259051,59.28185538963862],[248.02208626423464,61.53030015680088],[247.26537556838704,62.45965023516237],[245.71993136577203,62.35374358450417],[243.4812267369355,64.225426133984],[242.53627454611478,65.89838053896256],[239.22662411901285,68.31475394301197],[239.0386087280138,69.33785945171076],[237.61846349405133,69.52209458596099],[236.29666729084443,71.01072123255062],[230.4334568523121,68.9669997892297],[226.84143015964509,68.59763892718593],[224.16856566813613,68.68631465158842],[219.1841982378101,67.66846130606882],[217.29344286226942,67.48768712871652],[216.15887134867586,66.46513384670334],[215.40333526267494,67.5815648175726],[214.26936087294837,67.86109792992875],[213.60692101484594,66.74672578934496],[211.9039802911199,66.00662760499381],[210.39006798669138,64.79974545667892],[208.3090049147986,61.17722459511424],[206.79516022169557,60.8078714318267],[206.2280101316071,59.69303088661218],[204.9031059053762,59.04454692812806],[202.288768478747,58.63241937203611],[198.65762098343723,57.008601142287716],[193.92516050737657,55.80687686920237],[192.40686891096516,52.09245834452577],[191.26996373694993,51.53637191748385],[190.32084187578704,50.14401487701298],[190.59796672407452,44.8479552497879],[190.40677524552552,43.64038902698394],[191.71286750020155,43.15376365445945],[195.22776508085818,39.17612603608313],[197.07131412657003,36.7911618224075],[197.46987715866888,33.241415436488296],[198.3503185037968,32.84447979842298],[199.57713709010568,30.911782057693472],[205.9174884290591,29.24087621122908],[209.69428929240772,27.00022291789446],[212.05580804562027,24.948888383579742],[215.0853986407193,23.474541488667],[216.21799341865486,22.070327972682662],[217.92045829808038,21.41320162463444],[219.3403319945726,21.412824825597],[220.2860813534474,20.389834261116903],[221.61142627081324,20.667674773922045],[223.0320967666261,22.433048166366916],[225.7773168900162,23.080634815925805],[226.9132041728082,23.823685939339157],[228.0484313536286,21.685001963548984],[230.41453602769184,20.75443225590334],[232.8736446959423,21.129521063308403],[235.81139665471585,21.12068953329026],[237.3249895720922,22.05105270953436],[238.5567023697979,21.862789488171074],[241.1150433373256,19.999999999999545],[242.6301239188358,20.928456517277937]]]]} -------------------------------------------------------------------------------- /demos/medley.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 89 | -------------------------------------------------------------------------------- /demos/multiple-distinct.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 62 | -------------------------------------------------------------------------------- /demos/multiple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 63 | -------------------------------------------------------------------------------- /demos/rects.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 57 | -------------------------------------------------------------------------------- /demos/vanilla-canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 79 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { default as interpolate } from "./src/interpolate.js"; 2 | export { separate, combine, interpolateAll } from "./src/multiple.js"; 3 | export { splitPathString, toPathString } from "./src/svg.js"; 4 | export { fromCircle, toCircle, fromRect, toRect } from "./src/shape.js"; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flubber", 3 | "version": "0.4.2", 4 | "description": "Best-guess methods for smoothly interpolating and animating between shapes.", 5 | "main": "build/flubber.min.js", 6 | "browser": "build/flubber.min.js", 7 | "scripts": { 8 | "test": "babel-tape-runner 'test/**-test.js'", 9 | "pretest": "rm -rf build && mkdir build && rollup --config rollup.config.js", 10 | "build": "npm run pretest && uglifyjs -m -c -- build/flubber.js > build/flubber.min.js" 11 | }, 12 | "module": "index", 13 | "jsnext:main": "index", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/veltman/flubber.git" 17 | }, 18 | "keywords": [ 19 | "shape", 20 | "morph", 21 | "tween", 22 | "svg", 23 | "animation", 24 | "triangulation" 25 | ], 26 | "author": "Noah Veltman (@veltman)", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/veltman/flubber/issues" 30 | }, 31 | "homepage": "https://github.com/veltman/flubber#readme", 32 | "devDependencies": { 33 | "babel-plugin-add-module-exports": "^0.2.1", 34 | "babel-preset-env": "^1.4.0", 35 | "babel-tape-runner": "^2.0.1", 36 | "rollup": "^0.41.6", 37 | "rollup-plugin-buble": "^0.15.0", 38 | "rollup-plugin-commonjs": "^8.0.2", 39 | "rollup-plugin-node-resolve": "^2.1.1", 40 | "tape": "^4.6.3", 41 | "uglify-js": "^2.8.20" 42 | }, 43 | "dependencies": { 44 | "d3-array": "^1.2.0", 45 | "d3-polygon": "^1.0.3", 46 | "earcut": "^2.1.1", 47 | "svg-path-properties": "^0.2.1", 48 | "svgpath": "^2.2.1", 49 | "topojson-client": "^3.0.0" 50 | }, 51 | "babel": { 52 | "presets": [ 53 | "env" 54 | ], 55 | "plugins": [ 56 | "add-module-exports" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "rollup-plugin-commonjs"; 2 | import resolve from "rollup-plugin-node-resolve"; 3 | import buble from "rollup-plugin-buble"; 4 | 5 | export default { 6 | entry: "index.js", 7 | dest: "build/flubber.js", 8 | format: "umd", 9 | moduleName: "flubber", 10 | plugins: [ 11 | resolve({ 12 | jsnext: true, 13 | main: true 14 | }), 15 | commonjs({ 16 | sourceMap: false 17 | }), 18 | buble() 19 | ] 20 | }; 21 | -------------------------------------------------------------------------------- /src/add.js: -------------------------------------------------------------------------------- 1 | import { polygonLength } from "d3-polygon"; 2 | import { distance, pointAlong } from "./math.js"; 3 | 4 | export function addPoints(ring, numPoints) { 5 | const desiredLength = ring.length + numPoints, 6 | step = polygonLength(ring) / numPoints; 7 | 8 | let i = 0, 9 | cursor = 0, 10 | insertAt = step / 2; 11 | 12 | while (ring.length < desiredLength) { 13 | let a = ring[i], 14 | b = ring[(i + 1) % ring.length], 15 | segment = distance(a, b); 16 | 17 | if (insertAt <= cursor + segment) { 18 | ring.splice( 19 | i + 1, 20 | 0, 21 | segment ? pointAlong(a, b, (insertAt - cursor) / segment) : a.slice(0) 22 | ); 23 | insertAt += step; 24 | continue; 25 | } 26 | 27 | cursor += segment; 28 | i++; 29 | } 30 | } 31 | 32 | export function bisect(ring, maxSegmentLength = Infinity) { 33 | for (let i = 0; i < ring.length; i++) { 34 | let a = ring[i], 35 | b = i === ring.length - 1 ? ring[0] : ring[i + 1]; 36 | 37 | // Could splice the whole set for a segment instead, but a bit messy 38 | while (distance(a, b) > maxSegmentLength) { 39 | b = pointAlong(a, b, 0.5); 40 | ring.splice(i + 1, 0, b); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | export const INVALID_INPUT = `All shapes must be supplied as arrays of [x, y] points or an SVG path string (https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d). 2 | Example valid ways of supplying a shape would be: 3 | [[0, 0], [10, 0], [10, 10]] 4 | "M0,0 L10,0 L10,10Z" 5 | `; 6 | 7 | export const INVALID_INPUT_ALL = `flubber.all() expects two arrays of equal length as arguments. Each element in both arrays should be an array of [x, y] points or an SVG path string (https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d).`; 8 | 9 | export const INVALID_PATH_STRING = `Invalid SVG path string supplied. 10 | Path string reference: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d 11 | `; 12 | -------------------------------------------------------------------------------- /src/interpolate.js: -------------------------------------------------------------------------------- 1 | import normalizeRing from "./normalize.js"; 2 | import { addPoints } from "./add.js"; 3 | import rotate from "./rotate.js"; 4 | import { interpolatePoints } from "./math.js"; 5 | 6 | export default function( 7 | fromShape, 8 | toShape, 9 | { maxSegmentLength = 10, string = true } = {} 10 | ) { 11 | let fromRing = normalizeRing(fromShape, maxSegmentLength), 12 | toRing = normalizeRing(toShape, maxSegmentLength), 13 | interpolator = interpolateRing(fromRing, toRing, string); 14 | 15 | // Extra optimization for near either end with path strings 16 | if ( 17 | !string || 18 | (typeof fromShape !== "string" && typeof toShape !== "string") 19 | ) { 20 | return interpolator; 21 | } 22 | 23 | return t => { 24 | if (t < 1e-4 && typeof fromShape === "string") { 25 | return fromShape; 26 | } 27 | if (1 - t < 1e-4 && typeof toShape === "string") { 28 | return toShape; 29 | } 30 | return interpolator(t); 31 | }; 32 | } 33 | 34 | export function interpolateRing(fromRing, toRing, string) { 35 | let diff; 36 | 37 | diff = fromRing.length - toRing.length; 38 | 39 | // TODO bisect and add points in one step? 40 | addPoints(fromRing, diff < 0 ? diff * -1 : 0); 41 | addPoints(toRing, diff > 0 ? diff : 0); 42 | 43 | rotate(fromRing, toRing); 44 | 45 | return interpolatePoints(fromRing, toRing, string); 46 | } 47 | -------------------------------------------------------------------------------- /src/math.js: -------------------------------------------------------------------------------- 1 | import { toPathString } from "./svg.js"; 2 | import { polygonCentroid as d3Centroid } from "d3-polygon"; 3 | 4 | export function distance(a, b) { 5 | return Math.sqrt( 6 | (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]) 7 | ); 8 | } 9 | 10 | export function pointAlong(a, b, pct) { 11 | return [a[0] + (b[0] - a[0]) * pct, a[1] + (b[1] - a[1]) * pct]; 12 | } 13 | 14 | export function samePoint(a, b) { 15 | return distance(a, b) < 1e-9; 16 | } 17 | 18 | export function interpolatePoints(a, b, string) { 19 | let interpolators = a.map((d, i) => interpolatePoint(d, b[i])); 20 | 21 | return function(t) { 22 | let values = interpolators.map(fn => fn(t)); 23 | return string ? toPathString(values) : values; 24 | }; 25 | } 26 | 27 | export function interpolatePoint(a, b) { 28 | return function(t) { 29 | return a.map((d, i) => d + t * (b[i] - d)); 30 | }; 31 | } 32 | 33 | export function isFiniteNumber(number) { 34 | return typeof number === "number" && isFinite(number); 35 | } 36 | 37 | // Use plain mean if it's a degenerate polygon (colinear points or single point) 38 | export function polygonCentroid(polygon) { 39 | return nonZeroArea(polygon) 40 | ? d3Centroid(polygon) 41 | : [ 42 | (polygon[0][0] + polygon[polygon.length - 1][0]) / 2, 43 | (polygon[0][1] + polygon[polygon.length - 1][1]) / 2 44 | ]; 45 | } 46 | 47 | function nonZeroArea(polygon) { 48 | for (let i = 0; i < polygon.length - 2; i++) { 49 | let a = polygon[i], 50 | b = polygon[i + 1], 51 | c = polygon[i + 2]; 52 | 53 | if (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1])) { 54 | return true; 55 | } 56 | } 57 | 58 | return false; 59 | } 60 | -------------------------------------------------------------------------------- /src/multiple.js: -------------------------------------------------------------------------------- 1 | import { interpolateRing } from "./interpolate.js"; 2 | import { addPoints } from "./add.js"; 3 | import normalizeRing from "./normalize.js"; 4 | import triangulate from "./triangulate.js"; 5 | import pieceOrder from "./order.js"; 6 | import { INVALID_INPUT_ALL } from "./errors.js"; 7 | 8 | export function separate( 9 | fromShape, 10 | toShapes, 11 | { maxSegmentLength = 10, string = true, single = false } = {} 12 | ) { 13 | let fromRing = normalizeRing(fromShape, maxSegmentLength); 14 | 15 | if (fromRing.length < toShapes.length + 2) { 16 | addPoints(fromRing, toShapes.length + 2 - fromRing.length); 17 | } 18 | 19 | let fromRings = triangulate(fromRing, toShapes.length), 20 | toRings = toShapes.map(d => normalizeRing(d, maxSegmentLength)), 21 | t0 = typeof fromShape === "string" && fromShape, 22 | t1; 23 | 24 | if (!single || toShapes.every(s => typeof s === "string")) { 25 | t1 = toShapes.slice(0); 26 | } 27 | 28 | return interpolateSets(fromRings, toRings, { 29 | match: true, 30 | string, 31 | single, 32 | t0, 33 | t1 34 | }); 35 | } 36 | 37 | export function combine( 38 | fromShapes, 39 | toShape, 40 | { maxSegmentLength = 10, string = true, single = false } = {} 41 | ) { 42 | let interpolators = separate(toShape, fromShapes, { 43 | maxSegmentLength, 44 | string, 45 | single 46 | }); 47 | return single 48 | ? t => interpolators(1 - t) 49 | : interpolators.map(fn => t => fn(1 - t)); 50 | } 51 | 52 | export function interpolateAll( 53 | fromShapes, 54 | toShapes, 55 | { maxSegmentLength = 10, string = true, single = false } = {} 56 | ) { 57 | if ( 58 | !Array.isArray(fromShapes) || 59 | !Array.isArray(toShapes) || 60 | fromShapes.length !== toShapes.length || 61 | !fromShapes.length 62 | ) { 63 | throw new TypeError(INVALID_INPUT_ALL); 64 | } 65 | 66 | let normalize = s => normalizeRing(s, maxSegmentLength), 67 | fromRings = fromShapes.map(normalize), 68 | toRings = toShapes.map(normalize), 69 | t0, 70 | t1; 71 | 72 | if (single) { 73 | if (fromShapes.every(s => typeof s === "string")) { 74 | t0 = fromShapes.slice(0); 75 | } 76 | if (toShapes.every(s => typeof s === "string")) { 77 | t1 = toShapes.slice(0); 78 | } 79 | } else { 80 | t0 = fromShapes.slice(0); 81 | t1 = toShapes.slice(0); 82 | } 83 | 84 | return interpolateSets(fromRings, toRings, { 85 | string, 86 | single, 87 | t0, 88 | t1, 89 | match: false 90 | }); 91 | } 92 | 93 | function interpolateSets( 94 | fromRings, 95 | toRings, 96 | { string, single, t0, t1, match } = {} 97 | ) { 98 | let order = match 99 | ? pieceOrder(fromRings, toRings) 100 | : fromRings.map((d, i) => i), 101 | interpolators = order.map((d, i) => 102 | interpolateRing(fromRings[d], toRings[i], string) 103 | ); 104 | 105 | if (match && Array.isArray(t0)) { 106 | t0 = order.map(d => t0[d]); 107 | } 108 | 109 | if (single && string) { 110 | if (Array.isArray(t0)) { 111 | t0 = t0.join(" "); 112 | } 113 | if (Array.isArray(t1)) { 114 | t1 = t1.join(" "); 115 | } 116 | } 117 | 118 | if (single) { 119 | let multiInterpolator = string 120 | ? t => interpolators.map(fn => fn(t)).join(" ") 121 | : t => interpolators.map(fn => fn(t)); 122 | 123 | if (string && (t0 || t1)) { 124 | return t => 125 | (t < 1e-4 && t0) || (1 - t < 1e-4 && t1) || multiInterpolator(t); 126 | } 127 | return multiInterpolator; 128 | } else if (string) { 129 | t0 = Array.isArray(t0) ? t0.map(d => typeof d === "string" && d) : []; 130 | t1 = Array.isArray(t1) ? t1.map(d => typeof d === "string" && d) : []; 131 | 132 | return interpolators.map((fn, i) => { 133 | if (t0[i] || t1[i]) { 134 | return t => (t < 1e-4 && t0[i]) || (1 - t < 1e-4 && t1[i]) || fn(t); 135 | } 136 | return fn; 137 | }); 138 | } 139 | 140 | return interpolators; 141 | } 142 | -------------------------------------------------------------------------------- /src/normalize.js: -------------------------------------------------------------------------------- 1 | import { polygonArea } from "d3-polygon"; 2 | import { pathStringToRing } from "./svg.js"; 3 | import { samePoint, isFiniteNumber } from "./math.js"; 4 | import { bisect } from "./add.js"; 5 | import { INVALID_INPUT } from "./errors.js"; 6 | 7 | export default function normalizeRing(ring, maxSegmentLength) { 8 | let points, area, skipBisect; 9 | 10 | if (typeof ring === "string") { 11 | let converted = pathStringToRing(ring, maxSegmentLength); 12 | ring = converted.ring; 13 | skipBisect = converted.skipBisect; 14 | } else if (!Array.isArray(ring)) { 15 | throw new TypeError(INVALID_INPUT); 16 | } 17 | 18 | points = ring.slice(0); 19 | 20 | if (!validRing(points)) { 21 | throw new TypeError(INVALID_INPUT); 22 | } 23 | 24 | // TODO skip this test to avoid scale issues? 25 | // Chosen epsilon (1e-6) is problematic for small coordinate range 26 | if (points.length > 1 && samePoint(points[0], points[points.length - 1])) { 27 | points.pop(); 28 | } 29 | 30 | area = polygonArea(points); 31 | 32 | // Make all rings clockwise 33 | if (area > 0) { 34 | points.reverse(); 35 | } 36 | 37 | if ( 38 | !skipBisect && 39 | maxSegmentLength && 40 | isFiniteNumber(maxSegmentLength) && 41 | maxSegmentLength > 0 42 | ) { 43 | bisect(points, maxSegmentLength); 44 | } 45 | 46 | return points; 47 | } 48 | 49 | function validRing(ring) { 50 | return ring.every(function(point) { 51 | return ( 52 | Array.isArray(point) && 53 | point.length >= 2 && 54 | isFiniteNumber(point[0]) && 55 | isFiniteNumber(point[1]) 56 | ); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/order.js: -------------------------------------------------------------------------------- 1 | import { polygonCentroid, distance } from "./math.js"; 2 | 3 | // With 8 or fewer shapes, find the best permutation 4 | // Skip if array is huge (9+ shapes) 5 | export default function(start, end) { 6 | if (start.length > 8) { 7 | return start.map((d, i) => i); 8 | } 9 | let distances = start.map(p1 => end.map(p2 => squaredDistance(p1, p2))); 10 | return bestOrder(start, end, distances); 11 | } 12 | 13 | export function bestOrder(start, end, distances) { 14 | let min = Infinity, 15 | best = start.map((d, i) => i); 16 | 17 | function permute(arr, order = [], sum = 0) { 18 | for (let i = 0; i < arr.length; i++) { 19 | let cur = arr.splice(i, 1), 20 | dist = distances[cur[0]][order.length]; 21 | if (sum + dist < min) { 22 | if (arr.length) { 23 | permute(arr.slice(), order.concat(cur), sum + dist); 24 | } else { 25 | min = sum + dist; 26 | best = order.concat(cur); 27 | } 28 | } 29 | if (arr.length) { 30 | arr.splice(i, 0, cur[0]); 31 | } 32 | } 33 | } 34 | 35 | permute(best); 36 | return best; 37 | } 38 | 39 | function squaredDistance(p1, p2) { 40 | let d = distance(polygonCentroid(p1), polygonCentroid(p2)); 41 | return d * d; 42 | } 43 | -------------------------------------------------------------------------------- /src/rotate.js: -------------------------------------------------------------------------------- 1 | import { distance } from "./math.js"; 2 | 3 | export default function(ring, vs) { 4 | let len = ring.length, 5 | min = Infinity, 6 | bestOffset, 7 | sumOfSquares, 8 | spliced; 9 | 10 | for (let offset = 0; offset < len; offset++) { 11 | sumOfSquares = 0; 12 | 13 | vs.forEach(function(p, i) { 14 | let d = distance(ring[(offset + i) % len], p); 15 | sumOfSquares += d * d; 16 | }); 17 | 18 | if (sumOfSquares < min) { 19 | min = sumOfSquares; 20 | bestOffset = offset; 21 | } 22 | } 23 | 24 | if (bestOffset) { 25 | spliced = ring.splice(0, bestOffset); 26 | ring.splice(ring.length, 0, ...spliced); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/shape.js: -------------------------------------------------------------------------------- 1 | import { polygonLength } from "d3-polygon"; 2 | import { 3 | polygonCentroid, 4 | interpolatePoints, 5 | distance, 6 | isFiniteNumber 7 | } from "./math.js"; 8 | import normalizeRing from "./normalize.js"; 9 | import { addPoints } from "./add.js"; 10 | 11 | export function fromCircle(x, y, radius, toShape, options) { 12 | return fromShape( 13 | circlePoints(x, y, radius), 14 | toShape, 15 | circlePath(x, y, radius), 16 | 2 * Math.PI * radius, 17 | options 18 | ); 19 | } 20 | 21 | export function toCircle(fromShape, x, y, radius, options) { 22 | let interpolator = fromCircle(x, y, radius, fromShape, options); 23 | return t => interpolator(1 - t); 24 | } 25 | 26 | export function fromRect(x, y, width, height, toShape, options) { 27 | return fromShape( 28 | rectPoints(x, y, width, height), 29 | toShape, 30 | rectPath(x, y, width, height), 31 | 2 * width + 2 * height, 32 | options 33 | ); 34 | } 35 | 36 | export function toRect(fromShape, x, y, width, height, options) { 37 | let interpolator = fromRect(x, y, width, height, fromShape, options); 38 | return t => interpolator(1 - t); 39 | } 40 | 41 | function fromShape( 42 | fromFn, 43 | toShape, 44 | original, 45 | perimeter, 46 | { maxSegmentLength = 10, string = true } = {} 47 | ) { 48 | let toRing = normalizeRing(toShape, maxSegmentLength), 49 | fromRing, 50 | interpolator; 51 | 52 | // Enforce maxSegmentLength on circle/rect perimeter too 53 | if ( 54 | isFiniteNumber(perimeter) && 55 | toRing.length < perimeter / maxSegmentLength 56 | ) { 57 | addPoints(toRing, Math.ceil(perimeter / maxSegmentLength - toRing.length)); 58 | } 59 | 60 | fromRing = fromFn(toRing); 61 | interpolator = interpolatePoints(fromRing, toRing, string); 62 | 63 | if (string) { 64 | return t => (t < 1e-4 ? original : interpolator(t)); 65 | } 66 | 67 | return interpolator; 68 | } 69 | 70 | export function circlePoints(x, y, radius) { 71 | return function(ring) { 72 | let centroid = polygonCentroid(ring), 73 | perimeter = polygonLength([...ring, ring[0]]), 74 | startingAngle = Math.atan2( 75 | ring[0][1] - centroid[1], 76 | ring[0][0] - centroid[0] 77 | ), 78 | along = 0; 79 | 80 | return ring.map((point, i) => { 81 | let angle; 82 | if (i) { 83 | along += distance(point, ring[i - 1]); 84 | } 85 | angle = 86 | startingAngle + 87 | 2 * Math.PI * (perimeter ? along / perimeter : i / ring.length); 88 | return [Math.cos(angle) * radius + x, Math.sin(angle) * radius + y]; 89 | }); 90 | }; 91 | } 92 | 93 | // TODO splice in exact corners? 94 | export function rectPoints(x, y, width, height) { 95 | return function(ring) { 96 | let centroid = polygonCentroid(ring), 97 | perimeter = polygonLength([...ring, ring[0]]), 98 | startingAngle = Math.atan2( 99 | ring[0][1] - centroid[1], 100 | ring[0][0] - centroid[0] 101 | ), 102 | along = 0; 103 | 104 | if (startingAngle < 0) { 105 | startingAngle = 2 * Math.PI + startingAngle; 106 | } 107 | 108 | let startingProgress = startingAngle / (2 * Math.PI); 109 | 110 | return ring.map((point, i) => { 111 | if (i) { 112 | along += distance(point, ring[i - 1]); 113 | } 114 | let relative = rectPoint( 115 | (startingProgress + (perimeter ? along / perimeter : i / ring.length)) % 116 | 1 117 | ); 118 | return [x + relative[0] * width, y + relative[1] * height]; 119 | }); 120 | }; 121 | } 122 | 123 | // TODO don't do this 124 | function rectPoint(progress) { 125 | if (progress <= 1 / 8) { 126 | return [1, 0.5 + progress * 4]; 127 | } 128 | if (progress <= 3 / 8) { 129 | return [1.5 - 4 * progress, 1]; 130 | } 131 | if (progress <= 5 / 8) { 132 | return [0, 2.5 - 4 * progress]; 133 | } 134 | if (progress <= 7 / 8) { 135 | return [4 * progress - 2.5, 0]; 136 | } 137 | return [1, 4 * progress - 3.5]; 138 | } 139 | 140 | export function circlePath(x, y, radius) { 141 | let l = x - radius + "," + y, 142 | r = x + radius + "," + y, 143 | pre = "A" + radius + "," + radius + ",0,1,1,"; 144 | 145 | return "M" + l + pre + r + pre + l + "Z"; 146 | } 147 | 148 | export function rectPath(x, y, width, height) { 149 | let r = x + width, 150 | b = y + height; 151 | return ( 152 | "M" + 153 | x + 154 | "," + 155 | y + 156 | "L" + 157 | r + 158 | "," + 159 | y + 160 | "L" + 161 | r + 162 | "," + 163 | b + 164 | "L" + 165 | x + 166 | "," + 167 | b + 168 | "Z" 169 | ); 170 | } 171 | -------------------------------------------------------------------------------- /src/svg.js: -------------------------------------------------------------------------------- 1 | import Path from "svgpath"; 2 | import { svgPathProperties } from "svg-path-properties"; 3 | import { isFiniteNumber } from "./math.js"; 4 | import { INVALID_INPUT } from "./errors.js"; 5 | 6 | function parse(str) { 7 | return new Path(str).abs(); 8 | } 9 | 10 | function split(parsed) { 11 | return parsed 12 | .toString() 13 | .split("M") 14 | .map((d, i) => { 15 | d = d.trim(); 16 | return i && d ? "M" + d : d; 17 | }) 18 | .filter(d => d); 19 | } 20 | 21 | export function toPathString(ring) { 22 | return "M" + ring.join("L") + "Z"; 23 | } 24 | 25 | export function splitPathString(str) { 26 | return split(parse(str)); 27 | } 28 | 29 | export function pathStringToRing(str, maxSegmentLength) { 30 | let parsed = parse(str); 31 | 32 | return exactRing(parsed) || approximateRing(parsed, maxSegmentLength); 33 | } 34 | 35 | function exactRing(parsed) { 36 | let segments = parsed.segments || [], 37 | ring = []; 38 | 39 | if (!segments.length || segments[0][0] !== "M") { 40 | return false; 41 | } 42 | 43 | for (let i = 0; i < segments.length; i++) { 44 | let [command, x, y] = segments[i]; 45 | if ((command === "M" && i) || command === "Z") { 46 | break; 47 | } else if (command === "M" || command === "L") { 48 | ring.push([x, y]); 49 | } else if (command === "H") { 50 | ring.push([x, ring[ring.length - 1][1]]); 51 | } else if (command === "V") { 52 | ring.push([ring[ring.length - 1][0], x]); 53 | } else { 54 | return false; 55 | } 56 | } 57 | 58 | return ring.length ? { ring } : false; 59 | } 60 | 61 | function approximateRing(parsed, maxSegmentLength) { 62 | let ringPath = split(parsed)[0], 63 | ring = [], 64 | len, 65 | m, 66 | numPoints = 3; 67 | 68 | if (!ringPath) { 69 | throw new TypeError(INVALID_INPUT); 70 | } 71 | 72 | m = measure(ringPath); 73 | len = m.getTotalLength(); 74 | 75 | if ( 76 | maxSegmentLength && 77 | isFiniteNumber(maxSegmentLength) && 78 | maxSegmentLength > 0 79 | ) { 80 | numPoints = Math.max(numPoints, Math.ceil(len / maxSegmentLength)); 81 | } 82 | 83 | for (let i = 0; i < numPoints; i++) { 84 | let p = m.getPointAtLength((len * i) / numPoints); 85 | ring.push([p.x, p.y]); 86 | } 87 | 88 | return { 89 | ring, 90 | skipBisect: true 91 | }; 92 | } 93 | 94 | function measure(d) { 95 | // Use native browser measurement if running in browser 96 | if (typeof window !== "undefined" && window && window.document) { 97 | try { 98 | let path = window.document.createElementNS( 99 | "http://www.w3.org/2000/svg", 100 | "path" 101 | ); 102 | path.setAttributeNS(null, "d", d); 103 | return path; 104 | } catch (e) {} 105 | } 106 | // Fall back to svg-path-properties 107 | return svgPathProperties(d); 108 | } 109 | -------------------------------------------------------------------------------- /src/topology.js: -------------------------------------------------------------------------------- 1 | import { neighbors, mergeArcs, feature } from "topojson-client"; 2 | import { polygonArea } from "d3-polygon"; 3 | import { bisector } from "d3-array"; 4 | 5 | // TODO use TopoJSON native instead? 6 | export function createTopology(triangles, ring) { 7 | const arcIndices = {}, 8 | topology = { 9 | type: "Topology", 10 | objects: { 11 | triangles: { 12 | type: "GeometryCollection", 13 | geometries: [] 14 | } 15 | }, 16 | arcs: [] 17 | }; 18 | 19 | triangles.forEach(function(triangle) { 20 | const geometry = []; 21 | 22 | triangle.forEach(function(arc, i) { 23 | const slug = arc[0] < arc[1] ? arc.join(",") : arc[1] + "," + arc[0], 24 | coordinates = arc.map(function(pointIndex) { 25 | return ring[pointIndex]; 26 | }); 27 | 28 | if (slug in arcIndices) { 29 | geometry.push(~arcIndices[slug]); 30 | } else { 31 | geometry.push((arcIndices[slug] = topology.arcs.length)); 32 | topology.arcs.push(coordinates); 33 | } 34 | }); 35 | 36 | topology.objects.triangles.geometries.push({ 37 | type: "Polygon", 38 | area: Math.abs( 39 | polygonArea( 40 | triangle.map(function(d) { 41 | return ring[d[0]]; 42 | }) 43 | ) 44 | ), 45 | arcs: [geometry] 46 | }); 47 | }); 48 | 49 | // Sort smallest first 50 | // TODO sorted insertion? 51 | topology.objects.triangles.geometries.sort((a, b) => a.area - b.area); 52 | 53 | return topology; 54 | } 55 | 56 | export function collapseTopology(topology, numPieces) { 57 | const geometries = topology.objects.triangles.geometries, 58 | bisect = bisector(d => d.area).left; 59 | 60 | while (geometries.length > numPieces) { 61 | mergeSmallestFeature(); 62 | } 63 | 64 | if (numPieces > geometries.length) { 65 | throw new RangeError( 66 | "Can't collapse topology into " + numPieces + " pieces." 67 | ); 68 | } 69 | 70 | return feature(topology, topology.objects.triangles).features.map(f => { 71 | f.geometry.coordinates[0].pop(); 72 | return f.geometry.coordinates[0]; 73 | }); 74 | 75 | function mergeSmallestFeature() { 76 | const smallest = geometries[0], 77 | neighborIndex = neighbors(geometries)[0][0], 78 | neighbor = geometries[neighborIndex], 79 | merged = mergeArcs(topology, [smallest, neighbor]); 80 | 81 | // MultiPolygon -> Polygon 82 | merged.area = smallest.area + neighbor.area; 83 | merged.type = "Polygon"; 84 | merged.arcs = merged.arcs[0]; 85 | 86 | // Delete smallest and its chosen neighbor 87 | geometries.splice(neighborIndex, 1); 88 | geometries.shift(); 89 | 90 | // Add new merged shape in sorted order 91 | geometries.splice(bisect(geometries, merged.area), 0, merged); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/triangulate.js: -------------------------------------------------------------------------------- 1 | import earcut from "earcut"; 2 | import { createTopology, collapseTopology } from "./topology.js"; 3 | 4 | export default function(ring, numPieces) { 5 | return collapseTopology(createTopology(cut(ring), ring), numPieces); 6 | } 7 | 8 | export function cut(ring) { 9 | let cuts = earcut( 10 | ring.reduce((arr, point) => [...arr, point[0], point[1]], []) 11 | ), 12 | triangles = []; 13 | 14 | for (let i = 0, l = cuts.length; i < l; i += 3) { 15 | // Save each triangle as segments [a, b], [b, c], [c, a] 16 | triangles.push([ 17 | [cuts[i], cuts[i + 1]], 18 | [cuts[i + 1], cuts[i + 2]], 19 | [cuts[i + 2], cuts[i]] 20 | ]); 21 | } 22 | 23 | return triangles; 24 | } 25 | -------------------------------------------------------------------------------- /test/add-test.js: -------------------------------------------------------------------------------- 1 | import { supertape } from "./utils.js"; 2 | import * as shapes from "./shapes.js"; 3 | import { addPoints, bisect } from "../src/add.js"; 4 | 5 | let tape = supertape(); 6 | 7 | tape("Add points", function(test) { 8 | let added = shapes.square1(); 9 | 10 | addPoints(added, 4); 11 | 12 | test.inDelta(added, [ 13 | [0, 0], 14 | [50, 0], 15 | [100, 0], 16 | [100, 50], 17 | [100, 100], 18 | [50, 100], 19 | [0, 100], 20 | [0, 50] 21 | ]); 22 | 23 | added = shapes.square1(); 24 | addPoints(added, 3); 25 | 26 | test.inDelta(added, [ 27 | [0, 0], 28 | [100 * 2 / 3, 0], 29 | [100, 0], 30 | [100, 100], 31 | [100, 100], 32 | [0, 100], 33 | [0, 100 * 2 / 3] 34 | ]); 35 | 36 | test.end(); 37 | }); 38 | 39 | tape("Bisect segments", function(test) { 40 | let added = shapes.square1(), 41 | rect = shapes.rect(), 42 | once = [[0, 0], [50, 0], [100, 0], [100, 50], [100, 100], [50, 100], [0, 100], [0, 50]], 43 | twice = [ 44 | [0, 0], 45 | [25, 0], 46 | [50, 0], 47 | [75, 0], 48 | [100, 0], 49 | [100, 25], 50 | [100, 50], 51 | [100, 75], 52 | [100, 100], 53 | [75, 100], 54 | [50, 100], 55 | [25, 100], 56 | [0, 100], 57 | [0, 75], 58 | [0, 50], 59 | [0, 25] 60 | ]; 61 | 62 | bisect(added, 150); 63 | 64 | test.deepEqual(added, shapes.square1()); 65 | 66 | bisect(added, 75); 67 | 68 | test.deepEqual(added, once); 69 | 70 | bisect(added, 50); 71 | 72 | test.deepEqual(added, once); 73 | 74 | bisect(added, 49); 75 | 76 | test.deepEqual(added, twice); 77 | 78 | bisect(added, 25); 79 | 80 | test.deepEqual(added, twice); 81 | 82 | bisect(rect, 30); 83 | 84 | test.deepEqual(rect, [ 85 | [0, 0], 86 | [1, 0], 87 | [1, 25], 88 | [1, 50], 89 | [1, 75], 90 | [1, 100], 91 | [0, 100], 92 | [0, 75], 93 | [0, 50], 94 | [0, 25] 95 | ]); 96 | 97 | test.end(); 98 | }); 99 | 100 | tape("Zero-length handling", function(test) { 101 | let original = { 102 | all: [[0, 0], [0, 0], [0, 0]], 103 | partial: [[0, 0], [1, 1], [1, 1], [1, 1]], 104 | single: [[0, 0]] 105 | }; 106 | 107 | let added = {}; 108 | 109 | for (let key in original) { 110 | added[key] = original[key].slice(0); 111 | addPoints(added[key], 2); 112 | } 113 | 114 | test.inDelta(added.all, [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]); 115 | 116 | test.inDelta(added.single, [[0, 0], [0, 0], [0, 0]]); 117 | 118 | test.inDelta(added.partial, [[0, 0], [0.5, 0.5], [1, 1], [1, 1], [1, 1], [0.5, 0.5]]); 119 | 120 | test.end(); 121 | }); 122 | 123 | tape("Line segment", function(test) { 124 | let original = [[0, 0], [100, 0]], 125 | added; 126 | 127 | added = original.slice(0); 128 | addPoints(added, 1); 129 | 130 | test.inDelta(added, [[0, 0], [100, 0], [100, 0]]); 131 | 132 | added = original.slice(0); 133 | addPoints(added, 2); 134 | 135 | test.inDelta(added, [[0, 0], [50, 0], [100, 0], [50, 0]]); 136 | 137 | test.end(); 138 | }); 139 | -------------------------------------------------------------------------------- /test/all-test.js: -------------------------------------------------------------------------------- 1 | import { supertape } from "./utils.js"; 2 | import { interpolateAll } from "../src/multiple.js"; 3 | import * as shapes from "./shapes.js"; 4 | import { INVALID_INPUT_ALL } from "../src/errors.js"; 5 | 6 | let tape = supertape(); 7 | 8 | let isPathStr = /^M.+Z$/; 9 | 10 | let square = "M0,0L100,0L100,100L0,100Z", 11 | halfcircle = "M0,0 A50,50,0,0,0,100,0Z", 12 | bezier = 13 | "M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"; 14 | 15 | tape("all with array inputs", function(test) { 16 | let interpolate = interpolateAll( 17 | [shapes.square1(), shapes.square2()], 18 | [shapes.triangle1(), shapes.triangle2()], 19 | { maxSegmentLength: Infinity } 20 | ), 21 | interpolateArray = interpolateAll( 22 | [shapes.square1(), shapes.square2()], 23 | [shapes.triangle1(), shapes.triangle2()], 24 | { maxSegmentLength: Infinity, string: false } 25 | ); 26 | 27 | test.equal(interpolate.length, 2); 28 | test.assert(interpolate.every(fn => typeof fn === "function")); 29 | test.assert(interpolate.every(fn => fn(0).match(isPathStr))); 30 | test.assert(interpolate.every(fn => fn(0.5).match(isPathStr))); 31 | test.assert(interpolate.every(fn => fn(1).match(isPathStr))); 32 | 33 | test.equal(interpolateArray.length, 2); 34 | test.assert(interpolateArray.every(fn => typeof fn === "function")); 35 | test.assert(interpolateArray.every(fn => fn(0).length === 4)); 36 | test.assert(interpolateArray.every(fn => fn(0.5).length === 4)); 37 | test.assert(interpolateArray.every(fn => fn(1).length === 4)); 38 | 39 | test.end(); 40 | }); 41 | 42 | tape("all with string inputs", function(test) { 43 | let interpolate = interpolateAll([square, halfcircle], [bezier, square], { 44 | maxSegmentLength: 10 45 | }), 46 | interpolateArray = interpolateAll([square, halfcircle], [bezier, square], { 47 | maxSegmentLength: Infinity, 48 | string: false 49 | }); 50 | 51 | test.equal(interpolate.length, 2); 52 | test.assert(interpolate.every(fn => typeof fn === "function")); 53 | test.assert(interpolate.every(fn => fn(0.5).match(isPathStr))); 54 | test.assert(interpolate[0](0) === square); 55 | test.assert(interpolate[1](0) === halfcircle); 56 | test.assert(interpolate[0](1) === bezier); 57 | test.assert(interpolate[1](1) === square); 58 | 59 | test.equal(interpolateArray.length, 2); 60 | test.assert(interpolateArray.every(fn => typeof fn === "function")); 61 | test.assert(interpolateArray.every(fn => fn(0).length === 4)); 62 | test.assert(interpolateArray.every(fn => fn(0.5).length === 4)); 63 | test.assert(interpolateArray.every(fn => fn(1).length === 4)); 64 | 65 | test.end(); 66 | }); 67 | 68 | tape("all with array inputs single", function(test) { 69 | let interpolate = interpolateAll( 70 | [shapes.square1(), shapes.square2()], 71 | [shapes.triangle1(), shapes.triangle2()], 72 | { maxSegmentLength: Infinity, single: true } 73 | ), 74 | interpolateArray = interpolateAll( 75 | [shapes.square1(), shapes.square2()], 76 | [shapes.triangle1(), shapes.triangle2()], 77 | { maxSegmentLength: Infinity, string: false, single: true } 78 | ); 79 | 80 | test.equal(typeof interpolate, "function"); 81 | test.assert(interpolate(0).match(isPathStr)); 82 | test.assert(interpolate(0.5).match(isPathStr)); 83 | test.assert(interpolate(1).match(isPathStr)); 84 | test.equal(interpolate(0).split(" ").length, 2); 85 | test.equal(interpolate(0.5).split(" ").length, 2); 86 | test.equal(interpolate(1).split(" ").length, 2); 87 | 88 | test.equal(typeof interpolateArray, "function"); 89 | test.equal(interpolateArray(0).length, 2); 90 | test.equal(interpolateArray(0.5).length, 2); 91 | interpolateArray(0.5).forEach(function(ring) { 92 | test.equal(ring.length, 4); 93 | ring.forEach(function(point) { 94 | test.equal(point.length, 2); 95 | test.equal(typeof point[0], "number"); 96 | test.equal(typeof point[1], "number"); 97 | }); 98 | }); 99 | test.equal(interpolateArray(1).length, 2); 100 | test.end(); 101 | }); 102 | 103 | tape("all with string inputs single", function(test) { 104 | let interpolate = interpolateAll([square, halfcircle], [bezier, square], { 105 | single: true 106 | }), 107 | interpolateArray = interpolateAll([square, halfcircle], [bezier, square], { 108 | maxSegmentLength: Infinity, 109 | string: false, 110 | single: true 111 | }); 112 | 113 | test.equal(typeof interpolate, "function"); 114 | test.assert(interpolate(0).match(isPathStr)); 115 | test.assert(interpolate(0.5).match(isPathStr)); 116 | test.assert(interpolate(1).match(isPathStr)); 117 | test.equal(interpolate(0), [square, halfcircle].join(" ")); 118 | test.equal(interpolate(0.5).split(" ").length, 2); 119 | test.equal(interpolate(1), [bezier, square].join(" ")); 120 | 121 | test.equal(typeof interpolateArray, "function"); 122 | test.equal(interpolateArray(0).length, 2); 123 | test.equal(interpolateArray(0.5).length, 2); 124 | interpolateArray(0.5).forEach(function(ring) { 125 | ring.forEach(function(point) { 126 | test.equal(point.length, 2); 127 | test.equal(typeof point[0], "number"); 128 | test.equal(typeof point[1], "number"); 129 | }); 130 | }); 131 | test.equal(interpolateArray(1).length, 2); 132 | test.end(); 133 | }); 134 | 135 | tape("mix-and-match", function(test) { 136 | let interpolate = interpolateAll([halfcircle, shapes.square1()], [shapes.square2(), bezier]), 137 | interpolateSingle = interpolateAll([halfcircle, shapes.square1()], [shapes.square2(), bezier], { 138 | single: true 139 | }); 140 | 141 | test.equal(interpolate[0](0), halfcircle); 142 | test.notDeepEqual(interpolate[1](0), shapes.square1()); 143 | test.notDeepEqual(interpolate[0](1), shapes.square2()); 144 | test.equal(interpolate[1](1), bezier); 145 | 146 | test.end(); 147 | }); 148 | 149 | tape("errors", function(test) { 150 | let err = new RegExp(INVALID_INPUT_ALL.slice(25, 50)); 151 | 152 | let ring = [[0, 0], [1, 1], [2, 2]]; 153 | 154 | test.throws(() => interpolateAll(1), err); 155 | test.throws(() => interpolateAll([square, square], [square]), err); 156 | test.throws(() => interpolateAll([], []), err); 157 | test.throws(() => interpolateAll([square, square], [square, square, square]), err); 158 | 159 | test.doesNotThrow(() => interpolateAll([square, square], [square, square]), err); 160 | test.doesNotThrow(() => interpolateAll([square, ring, square], [ring, square, ring]), err); 161 | 162 | test.end(); 163 | }); 164 | -------------------------------------------------------------------------------- /test/array-test.js: -------------------------------------------------------------------------------- 1 | import { supertape } from "./utils.js"; 2 | import interpolate from "../src/interpolate.js"; 3 | import { separate, combine } from "../src/multiple.js"; 4 | import { toPathString } from "../src/svg.js"; 5 | import * as shapes from "./shapes.js"; 6 | 7 | let tape = supertape(); 8 | 9 | let halfcircle = "M0,0 A50,50,0,0,0,100,0Z", 10 | bezier = 11 | "M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"; 12 | 13 | tape("interpolate single array", function(test) { 14 | var interpolator = interpolate(shapes.square1(), shapes.square2(), { 15 | string: false, 16 | maxSegmentLength: Infinity 17 | }); 18 | 19 | test.deepEqual(interpolator(0), shapes.square1()); 20 | test.deepEqual(interpolator(1), shapes.square2()); 21 | test.deepEqual(interpolator(0.5), [[50, 0], [150, 0], [150, 100], [50, 100]]); 22 | test.end(); 23 | }); 24 | 25 | tape("interpolate single array as string", function(test) { 26 | var interpolator = interpolate(shapes.square1(), shapes.square2(), { 27 | maxSegmentLength: Infinity 28 | }); 29 | 30 | test.deepEqual(interpolator(0), toPathString(shapes.square1())); 31 | test.deepEqual(interpolator(1), toPathString(shapes.square2())); 32 | test.deepEqual(interpolator(0.5), toPathString([[50, 0], [150, 0], [150, 100], [50, 100]])); 33 | test.end(); 34 | }); 35 | 36 | tape("separate/combine arrays", function(test) { 37 | var separator = separate(shapes.square1(), [shapes.triangle1(), shapes.square2()], { 38 | maxSegmentLength: Infinity, 39 | string: false 40 | }); 41 | 42 | var combiner = combine([shapes.triangle1(), shapes.square2()], shapes.square1(), { 43 | maxSegmentLength: Infinity, 44 | string: false 45 | }); 46 | 47 | test.inDelta(separator[0](0), [[0, 0], [100, 100], [0, 100]]); 48 | test.inDelta(separator[1](0), [[0, 0], [100, 0], [100, 70.7], [100, 100]], 0.1); 49 | test.inDelta(separator[0](0), combiner[0](1)); 50 | test.inDelta(separator[1](0), combiner[1](1)); 51 | 52 | test.end(); 53 | }); 54 | 55 | tape("separate/combine arrays single", function(test) { 56 | var separator = separate(shapes.square1(), [shapes.triangle1(), shapes.square2()], { 57 | maxSegmentLength: Infinity, 58 | single: true, 59 | string: false 60 | }); 61 | 62 | test.inDelta( 63 | separator(0), 64 | [[[0, 0], [100, 100], [0, 100]], [[0, 0], [100, 0], [100, 70.7], [100, 100]]], 65 | 0.1 66 | ); 67 | 68 | test.end(); 69 | }); 70 | 71 | tape("mix and match string/ring", function(test) { 72 | var separator = separate(shapes.square1(), [shapes.triangle1(), toPathString(shapes.square2())], { 73 | maxSegmentLength: Infinity, 74 | single: true, 75 | string: false 76 | }); 77 | 78 | var combiner = combine([shapes.triangle1(), shapes.square2()], toPathString(shapes.square1()), { 79 | maxSegmentLength: Infinity, 80 | single: true, 81 | string: false 82 | }); 83 | 84 | test.inDelta( 85 | separator(0), 86 | [[[0, 0], [100, 100], [0, 100]], [[0, 0], [100, 0], [100, 70.7], [100, 100]]], 87 | 0.1 88 | ); 89 | test.deepEqual(separator(0), combiner(1)); 90 | 91 | test.end(); 92 | }); 93 | 94 | tape("separate/combine arrays with too few points", function(test) { 95 | var separator = separate(shapes.triangle1(), [shapes.square1(), shapes.square2()], { 96 | maxSegmentLength: Infinity, 97 | string: false 98 | }); 99 | 100 | test.assert(separator.length === 2); 101 | 102 | separator.forEach(function(fn) { 103 | test.assert(Array.isArray(fn(0.5)) && fn(0.5).length === 4); 104 | }); 105 | 106 | test.end(); 107 | }); 108 | -------------------------------------------------------------------------------- /test/interpolate-test.js: -------------------------------------------------------------------------------- 1 | import { supertape } from "./utils.js"; 2 | import * as shapes from "./shapes.js"; 3 | import { toPathString } from "../src/svg.js"; 4 | import { interpolateRing } from "../src/interpolate.js"; 5 | import { interpolatePoints } from "../src/math.js"; 6 | 7 | let tape = supertape(); 8 | 9 | tape("interpolatePoints", function(test) { 10 | let square1 = shapes.square1(), 11 | square2 = shapes.square2(), 12 | interpolateArray = interpolatePoints(square1, square2), 13 | interpolateString = interpolatePoints(square1, square2, true); 14 | 15 | test.deepEqual(interpolateArray(0), square1); 16 | test.deepEqual(interpolateArray(0.5), [[50, 0], [150, 0], [150, 100], [50, 100]]); 17 | test.deepEqual(interpolateArray(1), square2); 18 | test.deepEqual(interpolateString(0), toPathString(square1)); 19 | test.deepEqual(interpolateString(0.5), toPathString([[50, 0], [150, 0], [150, 100], [50, 100]])); 20 | test.deepEqual(interpolateString(1), toPathString(square2)); 21 | 22 | test.end(); 23 | }); 24 | 25 | tape("interpolateRing", function(test) { 26 | let square = shapes.square1(), 27 | triangle = shapes.triangle1(), 28 | interpolate = interpolateRing(square, triangle); 29 | 30 | test.deepEqual(interpolate(0), [[100, 0], [100, 100], [0, 100], [0, 0]]); 31 | test.deepEqual(interpolate(0.5), [[52.5, 0], [55, 100], [2.5, 100], [0, 50]]); 32 | test.deepEqual(interpolate(1), [[5, 0], [10, 100], [5, 100], [0, 100]]); 33 | 34 | test.end(); 35 | }); 36 | -------------------------------------------------------------------------------- /test/multiple-test.js: -------------------------------------------------------------------------------- 1 | import { supertape } from "./utils.js"; 2 | import { separate, combine } from "../src/multiple.js"; 3 | 4 | let tape = supertape(); 5 | 6 | let square = "M0,0L100,0L100,100L0,100Z", 7 | halfcircle = "M0,0 A50,50,0,0,0,100,0Z", 8 | bezier = 9 | "M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"; 10 | 11 | tape("separate/combine single preserve string at t=0, t=1", function(test) { 12 | let separator = separate(square, [halfcircle, bezier], { single: true }), 13 | combiner = combine([halfcircle, bezier], square, { single: true }); 14 | 15 | test.equal(separator(0), square); 16 | test.equal(separator(1), [halfcircle, bezier].join(" ")); 17 | test.equal(separator(0), combiner(1)); 18 | test.equal(separator(1), combiner(0)); 19 | 20 | test.end(); 21 | }); 22 | 23 | tape("separate/combine preserve string at t=1", function(test) { 24 | let separator = separate(square, [halfcircle, bezier]), 25 | combiner = combine([halfcircle, bezier], square); 26 | 27 | test.equal(separator[0](1), halfcircle); 28 | test.equal(separator[1](1), bezier); 29 | test.equal(separator[0](1), combiner[0](0)); 30 | test.equal(separator[1](1), combiner[1](0)); 31 | test.equal(separator.length, combiner.length); 32 | test.assert( 33 | separator.every(function(fn) { 34 | let val = fn(0.5); 35 | return typeof val === "string"; 36 | }) 37 | ); 38 | 39 | test.end(); 40 | }); 41 | -------------------------------------------------------------------------------- /test/normalize-test.js: -------------------------------------------------------------------------------- 1 | import { supertape } from "./utils.js"; 2 | import * as shapes from "./shapes.js"; 3 | import normalizeRing from "../src/normalize.js"; 4 | import { INVALID_INPUT } from "../src/errors.js"; 5 | 6 | let tape = supertape(); 7 | 8 | tape("No closing point", function(test) { 9 | let square = shapes.square1(), 10 | closed = [...square, square[0]], 11 | fuzzy = [...square, [0, 1e-12]], 12 | normalized = normalizeRing(square); 13 | 14 | // Confirm copy 15 | test.notEqual(square, normalized); 16 | test.deepEqual(square, normalized); 17 | 18 | // Confirm remove endpoint 19 | test.notDeepEqual(closed, normalized); 20 | closed.pop(); 21 | test.deepEqual(closed, normalized); 22 | 23 | // Confirm remove fuzzy endpoint 24 | test.notDeepEqual(fuzzy, normalized); 25 | fuzzy.pop(); 26 | test.deepEqual(fuzzy, normalized); 27 | 28 | test.end(); 29 | }); 30 | 31 | tape("Matching order", function(test) { 32 | let square = shapes.square1(), 33 | reversed = shapes.square1().reverse(), 34 | reversedClosed = [...reversed, reversed[0]]; 35 | 36 | // Noop 37 | test.deepEqual(square, normalizeRing(square)); 38 | 39 | // Confirm matching order 40 | test.deepEqual(square, normalizeRing(reversed)); 41 | test.deepEqual(square, normalizeRing(reversedClosed)); 42 | 43 | test.end(); 44 | }); 45 | 46 | tape("Expects valid ring or string", function(test) { 47 | let err = new RegExp(INVALID_INPUT.slice(0, 25)); 48 | 49 | // Invalid 50 | test.throws(() => normalizeRing(1), err); 51 | test.throws(() => normalizeRing({}), err); 52 | test.throws(() => normalizeRing([1, 2, 3]), err); 53 | 54 | // Partially valid 55 | test.throws(() => normalizeRing([[0, 0], [1, 1], [2, 2], "x"]), err); 56 | 57 | test.end(); 58 | }); 59 | 60 | tape("Bisect", function(test) { 61 | 62 | // No bisecting zero-length segments 63 | test.deepEqual(normalizeRing([[0, 0]], 1e-6), [[0, 0]]); 64 | test.deepEqual(normalizeRing([[0, 0], [0, 0], [0, 0]], 1e-6), [[0, 0], [0, 0]]); 65 | test.deepEqual(normalizeRing([[0, 0], [0, 0], [1, 0]], 0.6), [[0, 0], [0, 0], [0.5, 0], [1, 0], [0.5, 0]]); 66 | 67 | test.deepEqual(normalizeRing([[0, 0], [1, 0]], 0.6), [[0, 0], [0.5, 0], [1, 0], [0.5, 0]]); 68 | test.end(); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /test/order-test.js: -------------------------------------------------------------------------------- 1 | import { shuffle, range } from "d3-array"; 2 | import tape from "tape"; 3 | import { bestOrder } from "../src/order.js"; 4 | 5 | let start = range(6); 6 | 7 | tape("Best order", function(test) { 8 | for (let i = 0; i < 3; i++) { 9 | let end = start.slice(0); 10 | shuffle(end); 11 | let distances = start.map(a => end.map(b => Math.abs(a - b))), 12 | order = bestOrder(start, end, distances), 13 | ordered = order.map(d => start[d]); 14 | test.deepEqual(ordered, end); 15 | } 16 | 17 | test.end(); 18 | }); 19 | -------------------------------------------------------------------------------- /test/rotate-test.js: -------------------------------------------------------------------------------- 1 | import { supertape } from "./utils.js"; 2 | import * as shapes from "./shapes.js"; 3 | import rotate from "../src/rotate.js"; 4 | 5 | let tape = supertape(); 6 | 7 | tape("Rotate squares successfully", function(test) { 8 | let square = shapes.square1(); 9 | 10 | for (let i = 0; i < 4; i++) { 11 | let off = offsetRing(shapes.square1(), i); 12 | 13 | // Rings don't match 14 | test[i ? "notDeepEqual" : "deepEqual"](square, off); 15 | 16 | rotate(off, square); 17 | 18 | // Equal post-rotation 19 | test.deepEqual(square, off); 20 | 21 | // Square hasn't changed 22 | test.deepEqual(square, shapes.square1()); 23 | } 24 | 25 | test.end(); 26 | }); 27 | 28 | tape("Min Distance", function(test) { 29 | let triangle = shapes.triangle1(), alt = shapes.triangle2(); 30 | 31 | for (let i = 0; i < 3; i++) { 32 | let off = offsetRing(shapes.triangle2(), i); 33 | 34 | // Rings don't match 35 | test[i ? "notDeepEqual" : "deepEqual"](alt, off); 36 | 37 | rotate(off, triangle); 38 | 39 | // Expected post-rotation 40 | test.deepEqual(alt, off); 41 | 42 | // Triangle hasn't changed 43 | test.deepEqual(triangle, shapes.triangle1()); 44 | } 45 | 46 | test.end(); 47 | }); 48 | 49 | function offsetRing(arr, n) { 50 | for (let i = 0; i < n; i++) { 51 | arr.push(arr.shift()); 52 | } 53 | 54 | return arr; 55 | } 56 | -------------------------------------------------------------------------------- /test/shapes.js: -------------------------------------------------------------------------------- 1 | export function triangle1() { 2 | return [[5, 0], [10, 100], [0, 100]]; 3 | } 4 | 5 | export function triangle2() { 6 | return [[10, 100], [6, 1000], [0, 100]]; 7 | } 8 | 9 | export function square1() { 10 | return [[0, 0], [100, 0], [100, 100], [0, 100]]; 11 | } 12 | 13 | export function square2() { 14 | return [[100, 0], [200, 0], [200, 100], [100, 100]]; 15 | } 16 | 17 | export function rect() { 18 | return [[0, 0], [1, 0], [1, 100], [0, 100]]; 19 | } 20 | -------------------------------------------------------------------------------- /test/svg-test.js: -------------------------------------------------------------------------------- 1 | import { supertape } from "./utils.js"; 2 | import normalizeRing from "../src/normalize.js"; 3 | import interpolate from "../src/interpolate.js"; 4 | import { separate, combine } from "../src/multiple.js"; 5 | import { toPathString, splitPathString, pathStringToRing } from "../src/svg.js"; 6 | 7 | let tape = supertape(); 8 | 9 | let square = "M0,0L100,0L100,100L0,100Z", 10 | halfcircle = "M0,0 A50,50,0,0,0,100,0Z", 11 | bezier = 12 | "M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"; 13 | 14 | tape("toPathString", function(test) { 15 | test.equal(toPathString([[0, 0], [1, 1], [2, 2]]), "M0,0L1,1L2,2Z"); 16 | test.end(); 17 | }); 18 | 19 | tape("splitPathString", function(test) { 20 | // Single 21 | test.deepEqual(splitPathString("M1,2L3,4Z"), ["M1 2L3 4Z"]); 22 | 23 | // Multiple 24 | test.deepEqual(splitPathString("M1,2L3,4ZM5,6L7,8Z"), ["M1 2L3 4Z", "M5 6L7 8Z"]); 25 | 26 | // Whitespace 27 | test.deepEqual(splitPathString("M1,2L3,4Z M5,6L7,8Z"), ["M1 2L3 4Z", "M5 6L7 8Z"]); 28 | test.deepEqual(splitPathString(" M1,2L3,4Z M5,6L7,8Z"), ["M1 2L3 4Z", "M5 6L7 8Z"]); 29 | test.deepEqual(splitPathString(" M1,2L3,4Z M5,6L7,8Z "), ["M1 2L3 4Z", "M5 6L7 8Z"]); 30 | 31 | // Relative 32 | test.deepEqual(splitPathString("M1,2l2,2Z m4,4l2,2Z"), ["M1 2L3 4Z", "M5 6L7 8Z"]); 33 | 34 | // Implicit lineto 35 | test.deepEqual(splitPathString("M1,2l2,2Z m4,4 2,2Z"), ["M1 2L3 4Z", "M5 6L7 8Z"]); 36 | 37 | test.end(); 38 | }); 39 | 40 | tape("pathStringToRing general", function(test) { 41 | let halfcircle = "M0,0 A50,50,0,0,0,100,0Z", 42 | bezier = 43 | "M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"; 44 | 45 | test.inDelta( 46 | pathStringToRing(halfcircle, 50).ring, 47 | [ 48 | [0, 0], 49 | [17.25457763671875, 37.80250930786133], 50 | [57.125911712646484, 49.49604034423828], 51 | [92.07535552978516, 27.02345085144043], 52 | [85.70057678222656, -0.000012940427041030489], 53 | [42.85029602050781, -0.000006470214884757297] 54 | ], 55 | 0.5 56 | ); 57 | 58 | test.inDelta( 59 | pathStringToRing(bezier, 600).ring, 60 | [ 61 | [636.5, 315], 62 | [287.3954162597656, 363.1767578125], 63 | [271.6760559082031, 178.45318603515625], 64 | [457.7559814453125, 34.372894287109375], 65 | [866.4840698242188, 227.6297607421875] 66 | ], 67 | 0.5 68 | ); 69 | 70 | test.end(); 71 | }); 72 | 73 | tape("interpolate preserve string at t=0, t=1", function(test) { 74 | let interpolator = interpolate(halfcircle, bezier); 75 | 76 | test.deepEqual(interpolator(0), halfcircle); 77 | test.deepEqual(interpolator(1), bezier); 78 | 79 | let ringInterpolator = interpolate(halfcircle, bezier, { string: false }), 80 | first = ringInterpolator(0), 81 | last = ringInterpolator(1); 82 | 83 | test.deepEqual(first.length, last.length); 84 | 85 | test.end(); 86 | }); 87 | 88 | tape("pathStringToRing preserve", function(test) { 89 | let square1 = "M0,0L100,0L100,100L0,100Z", 90 | square2 = "M0,0H100V100H0Z", 91 | square3 = "M0,0h100v100h-100Z", 92 | original = [[0, 0], [100, 0], [100, 100], [0, 100]], 93 | bisected = [[0, 0], [50, 0], [100, 0], [100, 50], [100, 100], [50, 100], [0, 100], [0, 50]]; 94 | 95 | test.deepEqual(pathStringToRing(square1, 125).ring, original); 96 | test.deepEqual(pathStringToRing(square2, 125).ring, original); 97 | test.deepEqual(pathStringToRing(square3, 125).ring, original); 98 | 99 | // Not bisected 100 | test.deepEqual(pathStringToRing(square1, 90).ring, original); 101 | test.deepEqual(pathStringToRing(square2, 90).ring, original); 102 | test.deepEqual(pathStringToRing(square3, 90).ring, original); 103 | 104 | test.deepEqual(normalizeRing(square1, 90), bisected); 105 | test.deepEqual(normalizeRing(square2, 90), bisected); 106 | test.deepEqual(normalizeRing(square3, 90), bisected); 107 | 108 | test.end(); 109 | }); 110 | 111 | tape("pathStringToRing produces at least 3 points", function(test) { 112 | let infinite = pathStringToRing(halfcircle, Infinity).ring, 113 | tooBig = pathStringToRing(halfcircle, 100000).ring; 114 | 115 | test.deepEqual(infinite, tooBig); 116 | test.assert(Array.isArray(infinite) && infinite.length === 3); 117 | test.assert(infinite.every(d => Array.isArray(d) && d.length === 2)); 118 | 119 | test.end(); 120 | }); 121 | -------------------------------------------------------------------------------- /test/triangulate-test.js: -------------------------------------------------------------------------------- 1 | import { supertape } from "./utils.js"; 2 | import { default as triangulate, cut } from "../src/triangulate.js"; 3 | import { createTopology, collapseTopology } from "../src/topology.js"; 4 | import * as shapes from "./shapes.js"; 5 | 6 | let tape = supertape(); 7 | 8 | tape("Triangulate a square", function(test) { 9 | let square = shapes.square1(); 10 | 11 | test.deepEqual(cut(square), [[[2, 3], [3, 0], [0, 2]], [[0, 1], [1, 2], [2, 0]]]); 12 | test.end(); 13 | }); 14 | 15 | tape("Create/collapse a triangulation", function(test) { 16 | let square = shapes.square1(), 17 | cuts = cut(square); 18 | 19 | let topology = createTopology(cuts, square); 20 | 21 | test.deepEqual(topology, { 22 | type: "Topology", 23 | objects: { 24 | triangles: { 25 | type: "GeometryCollection", 26 | geometries: [ 27 | { type: "Polygon", area: 5000, arcs: [[0, 1, 2]] }, 28 | { type: "Polygon", area: 5000, arcs: [[3, 4, -3]] } 29 | ] 30 | } 31 | }, 32 | arcs: [ 33 | [[100, 100], [0, 100]], 34 | [[0, 100], [0, 0]], 35 | [[0, 0], [100, 100]], 36 | [[0, 0], [100, 0]], 37 | [[100, 0], [100, 100]] 38 | ] 39 | }); 40 | 41 | // Don't change anything 42 | let collapsed = collapseTopology(topology, 2); 43 | 44 | test.deepEqual(collapsed, [[[100, 100], [0, 100], [0, 0]], [[0, 0], [100, 0], [100, 100]]]); 45 | test.equal(topology.objects.triangles.geometries.length, 2); 46 | 47 | collapsed = collapseTopology(topology, 1); 48 | test.deepEqual(collapsed, [[[100, 100], [0, 100], [0, 0], [100, 0]]]); 49 | test.equal(topology.objects.triangles.geometries.length, 1); 50 | 51 | test.throws(() => collapseTopology(topology, 2)); 52 | 53 | test.end(); 54 | }); 55 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import tape from "tape"; 2 | 3 | // From https://github.com/d3/d3-geo/ 4 | function inDelta(actual, expected, delta) { 5 | return (Array.isArray(expected) ? inDeltaArray : inDeltaNumber)(actual, expected, delta); 6 | } 7 | 8 | function inDeltaArray(actual, expected, delta) { 9 | var n = expected.length, i = -1; 10 | if (actual.length !== n) return false; 11 | while (++i < n) 12 | if (!inDelta(actual[i], expected[i], delta)) return false; 13 | return true; 14 | } 15 | 16 | function inDeltaNumber(actual, expected, delta) { 17 | return actual >= expected - delta && actual <= expected + delta; 18 | } 19 | 20 | export function supertape() { 21 | tape.Test.prototype.inDelta = function(actual, expected, delta) { 22 | delta = delta || 1e-6; 23 | this._assert(inDelta(actual, expected, delta), { 24 | message: "should be in delta " + delta, 25 | operator: "inDelta", 26 | actual: actual, 27 | expected: expected 28 | }); 29 | }; 30 | 31 | return tape; 32 | } 33 | --------------------------------------------------------------------------------