├── .editorconfig ├── .gitignore ├── .npmignore ├── README.md ├── assets ├── flubber-evolve.gif └── polymorph-evolve.gif ├── config ├── compress.json └── rollup.cdn.js ├── dist ├── polymorph.js └── polymorph.min.js ├── index.html ├── package-lock.json ├── package.json ├── src ├── constants.ts ├── getPath.ts ├── index.ts ├── interpolate.ts ├── operators │ ├── arcToCurve.ts │ ├── computeAbsoluteOrigin.ts │ ├── fillPoints.ts │ ├── fillSegments.ts │ ├── getSortedSegments.ts │ ├── interpolatePath.ts │ ├── normalizePaths.ts │ ├── normalizePoints.ts │ ├── parsePoints.ts │ ├── perimeterPoints.ts │ ├── renderPath.ts │ └── rotatePoints.ts ├── path.ts ├── types.ts └── utilities │ ├── browser.ts │ ├── coalesce.ts │ ├── createNumberArray.ts │ ├── distance.ts │ ├── errors.ts │ ├── inspect.ts │ ├── math.ts │ └── objects.ts ├── tests └── operators │ ├── fillPoints.ts │ ├── fillSegments.ts │ ├── interpolatePath.ts │ ├── parsePath.ts │ ├── perimeterPoints.ts │ └── renderPath.ts ├── tsconfig.json ├── tsconfig.node.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | 7 | [*.md] 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .alm 2 | .vscode 3 | .idea 4 | .vs 5 | npm-debug.log* 6 | /node_modules 7 | /lib 8 | /types 9 | /src/**/*.js 10 | /src/**/*.map 11 | /test/**/*.js 12 | /test/**/*.map 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | docs 4 | docs-source 5 | tests 6 | assets 7 | .alm 8 | .vscode 9 | .editorconfig 10 | .gitignore 11 | rollup.common.js 12 | tsconfig.json 13 | tslint.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polymorph 2 | 3 | *Morph SVG Paths* 4 | 5 | [![npm version](https://badge.fury.io/js/polymorph-js.svg)](https://badge.fury.io/js/polymorph-js) 6 | [![Build Status](https://travis-ci.org/notoriousb1t/polymorph.svg?branch=master)](https://travis-ci.org/notoriousb1t/polymorph) 7 | [![Downloads](https://img.shields.io/npm/dm/polymorph-js.svg)](https://www.npmjs.com/package/polymorph-js) 8 | 9 | ## Features 10 | 11 | - morphs between two or more svg paths using a simple function 12 | - handles variable length paths and holes in paths 13 | - compatible with [Just Animate](https://github.com/just-animate/just-animate), [Popmotion](https://github.com/popmotion/popmotion), [nm8](https://github.com/davidkpiano/nm8), [TweenRex](https://github.com/tweenrex/tweenrex), and other animation libraries 14 | - Super tiny, about 6k minified 15 | - Free for commercial and non-commercial use under the MIT license 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 32 | 33 | 34 |

Flubber (53kb)

Polymorph (6kb)

25 | 27 | 29 | 31 |
35 | 36 | ## Get Started 37 | 38 | Read the full [documentation](https://notoriousb1t.github.io/polymorph-docs) to get started or check out the demos below. 39 | 40 | ## Demos 41 | - [Morph Leonardo da Vinci to a Skull](https://codepen.io/notoriousb1t/pen/KyPoYm) 42 | - [Charmander Evolves with Just Animate 2 + Polymorph](https://codepen.io/notoriousb1t/pen/gXpYEG?editors=1010) 43 | - [Morphin' Icons with Just Animate 2 + Polymorph](https://codepen.io/notoriousb1t/pen/veMyxw?editors=1010) 44 | 45 | 46 | ## License 47 | This library is licensed under MIT. 48 | 49 | ## Have a question or something to contribute? 50 | Please create an [issue](https://github.com/notoriousb1t/polymorph/issues) for questions or to discuss new features. 51 | 52 | ## Special Thanks 53 | 54 | Special thanks to [@shshaw](https://twitter.com/shshaw) for the amazing logo and artwork! 55 | -------------------------------------------------------------------------------- /assets/flubber-evolve.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notoriousb1t/polymorph/a19a16cfa0e5654cbe4709edeccad7dfe5db7040/assets/flubber-evolve.gif -------------------------------------------------------------------------------- /assets/polymorph-evolve.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notoriousb1t/polymorph/a19a16cfa0e5654cbe4709edeccad7dfe5db7040/assets/polymorph-evolve.gif -------------------------------------------------------------------------------- /config/compress.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": "verbose", 3 | "compress": { 4 | "collapse_vars": true, 5 | "comparisons": true, 6 | "conditionals": true, 7 | "dead_code": true, 8 | "drop_console": true, 9 | "evaluate": false, 10 | "if_return": true, 11 | "inline": true, 12 | "reduce_vars": true, 13 | "loops": true, 14 | "passes": 1, 15 | "unsafe_comps": true, 16 | "typeofs": false 17 | }, 18 | "output": { 19 | "semicolons": false 20 | }, 21 | "mangle": { 22 | "reserved": ["_"] 23 | }, 24 | "toplevel": false, 25 | "ie8": false 26 | } 27 | -------------------------------------------------------------------------------- /config/rollup.cdn.js: -------------------------------------------------------------------------------- 1 | 2 | import typescript from 'rollup-plugin-typescript'; 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | 5 | module.exports = { 6 | input: 'src/index.ts', 7 | output: { 8 | file: 'dist/polymorph.js', 9 | format: 'umd', 10 | name: 'polymorph' 11 | }, 12 | plugins: [ 13 | typescript({ 14 | tsconfig: false, 15 | target: 'es5', 16 | rootDir: 'src', 17 | module: 'es2015', 18 | removeComments: true, 19 | declaration: false, 20 | typescript: require('typescript'), 21 | noImplicitAny: true 22 | }), 23 | nodeResolve({ 24 | mainFields: ["main", "module"], 25 | browser: true, 26 | extensions: [ '.js', '.json' ], 27 | preferBuiltins: false 28 | }) 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /dist/polymorph.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = global || self, factory(global.polymorph = {})); 5 | }(this, function (exports) { 'use strict'; 6 | 7 | var _ = undefined; 8 | var SPACE = ' '; 9 | var FILL = 'fill'; 10 | var DRAW_LINE_VERTICAL = 'V'; 11 | var DRAW_LINE_HORIZONTAL = 'H'; 12 | var DRAW_LINE = 'L'; 13 | var CLOSE_PATH = 'Z'; 14 | var MOVE_CURSOR = 'M'; 15 | var DRAW_CURVE_CUBIC_BEZIER = 'C'; 16 | var DRAW_CURVE_SMOOTH = 'S'; 17 | var DRAW_CURVE_QUADRATIC = 'Q'; 18 | var DRAW_CURVE_QUADRATIC_CONTINUATION = 'T'; 19 | var DRAW_ARC = 'A'; 20 | 21 | function isString(obj) { 22 | return typeof obj === 'string'; 23 | } 24 | 25 | var math = Math; 26 | var abs = math.abs; 27 | var min = math.min; 28 | var max = math.max; 29 | var floor = math.floor; 30 | var round = math.round; 31 | var sqrt = math.sqrt; 32 | var pow = math.pow; 33 | var cos = math.cos; 34 | var asin = math.asin; 35 | var sin = math.sin; 36 | var tan = math.tan; 37 | var PI = math.PI; 38 | var QUADRATIC_RATIO = 2.0 / 3; 39 | var EPSILON = pow(2, -52); 40 | 41 | function renderPath(ns, formatter) { 42 | if (formatter === void 0) { formatter = round; } 43 | if (isString(ns)) { 44 | return ns; 45 | } 46 | var result = []; 47 | for (var i = 0; i < ns.length; i++) { 48 | var n = ns[i]; 49 | result.push(MOVE_CURSOR, formatter(n[0]), formatter(n[1]), DRAW_CURVE_CUBIC_BEZIER); 50 | var lastResult = void 0; 51 | for (var f = 2; f < n.length; f += 6) { 52 | var p0 = formatter(n[f]); 53 | var p1 = formatter(n[f + 1]); 54 | var p2 = formatter(n[f + 2]); 55 | var p3 = formatter(n[f + 3]); 56 | var dx = formatter(n[f + 4]); 57 | var dy = formatter(n[f + 5]); 58 | var isPoint = p0 == dx && p2 == dx && p1 == dy && p3 == dy; 59 | if (!isPoint || lastResult != 60 | (lastResult = ('' + p0 + p1 + p2 + p3 + dx + dy))) { 61 | result.push(p0, p1, p2, p3, dx, dy); 62 | } 63 | } 64 | } 65 | return result.join(SPACE); 66 | } 67 | 68 | function raiseError() { 69 | throw new Error(Array.prototype.join.call(arguments, SPACE)); 70 | } 71 | 72 | var userAgent = typeof window !== 'undefined' && window.navigator.userAgent; 73 | var isEdge = /(MSIE |Trident\/|Edge\/)/i.test(userAgent); 74 | 75 | var arrayConstructor = isEdge ? Array : Float32Array; 76 | function createNumberArray(n) { 77 | return new arrayConstructor(n); 78 | } 79 | 80 | function computeDimensions(points) { 81 | var xmin = points[0]; 82 | var ymin = points[1]; 83 | var ymax = ymin; 84 | var xmax = xmin; 85 | for (var i = 2; i < points.length; i += 6) { 86 | var x = points[i + 4]; 87 | var y = points[i + 5]; 88 | xmin = min(xmin, x); 89 | xmax = max(xmax, x); 90 | ymin = min(ymin, y); 91 | ymax = max(ymax, y); 92 | } 93 | return { 94 | x: xmin, 95 | w: (xmax - xmin), 96 | y: ymin, 97 | h: (ymax - ymin) 98 | }; 99 | } 100 | function computeAbsoluteOrigin(relativeX, relativeY, points) { 101 | var dimensions = computeDimensions(points); 102 | return { 103 | x: dimensions.x + dimensions.w * relativeX, 104 | y: dimensions.y + dimensions.h * relativeY 105 | }; 106 | } 107 | 108 | function fillSegments(larger, smaller, origin) { 109 | var largeLen = larger.length; 110 | var smallLen = smaller.length; 111 | if (largeLen < smallLen) { 112 | return fillSegments(smaller, larger, origin); 113 | } 114 | smaller.length = largeLen; 115 | for (var i = smallLen; i < largeLen; i++) { 116 | var l = larger[i]; 117 | var originX = origin.x; 118 | var originY = origin.y; 119 | if (!origin.absolute) { 120 | var absoluteOrigin = computeAbsoluteOrigin(originX, originY, l); 121 | originX = absoluteOrigin.x; 122 | originY = absoluteOrigin.y; 123 | } 124 | var d = createNumberArray(l.length); 125 | for (var k = 0; k < l.length; k += 2) { 126 | d[k] = originX; 127 | d[k + 1] = originY; 128 | } 129 | smaller[i] = d; 130 | } 131 | } 132 | 133 | function rotatePoints(ns, count) { 134 | var len = ns.length; 135 | var rightLen = len - count; 136 | var buffer = createNumberArray(count); 137 | var i; 138 | for (i = 0; i < count; i++) { 139 | buffer[i] = ns[i]; 140 | } 141 | for (i = count; i < len; i++) { 142 | ns[i - count] = ns[i]; 143 | } 144 | for (i = 0; i < count; i++) { 145 | ns[rightLen + i] = buffer[i]; 146 | } 147 | } 148 | 149 | function distance(x1, y1, x2, y2) { 150 | return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); 151 | } 152 | 153 | function normalizePoints(absolute, originX, originY, ns) { 154 | var len = ns.length; 155 | if (ns[len - 2] !== ns[0] || ns[len - 1] !== ns[1]) { 156 | return; 157 | } 158 | if (!absolute) { 159 | var relativeOrigin = computeAbsoluteOrigin(originX, originY, ns); 160 | originX = relativeOrigin.x; 161 | originY = relativeOrigin.y; 162 | } 163 | var buffer = ns.slice(2); 164 | len = buffer.length; 165 | var index, minAmount; 166 | for (var i = 0; i < len; i += 6) { 167 | var next = distance(originX, originX, buffer[i], buffer[i + 1]); 168 | if (minAmount === _ || next < minAmount) { 169 | minAmount = next; 170 | index = i; 171 | } 172 | } 173 | rotatePoints(buffer, index); 174 | ns[0] = buffer[len - 2]; 175 | ns[1] = buffer[len - 1]; 176 | for (var i = 0; i < buffer.length; i++) { 177 | ns[i + 2] = buffer[i]; 178 | } 179 | } 180 | 181 | function fillPoints(matrix, addPoints) { 182 | var ilen = matrix[0].length; 183 | for (var i = 0; i < ilen; i++) { 184 | var left = matrix[0][i]; 185 | var right = matrix[1][i]; 186 | var totalLength = max(left.length + addPoints, right.length + addPoints); 187 | matrix[0][i] = fillSubpath(left, totalLength); 188 | matrix[1][i] = fillSubpath(right, totalLength); 189 | } 190 | } 191 | function fillSubpath(ns, totalLength) { 192 | var totalNeeded = totalLength - ns.length; 193 | var ratio = Math.ceil(totalLength / ns.length); 194 | var result = createNumberArray(totalLength); 195 | result[0] = ns[0]; 196 | result[1] = ns[1]; 197 | var k = 1, j = 1; 198 | while (j < totalLength - 1) { 199 | result[++j] = ns[++k]; 200 | result[++j] = ns[++k]; 201 | result[++j] = ns[++k]; 202 | result[++j] = ns[++k]; 203 | var dx = result[++j] = ns[++k]; 204 | var dy = result[++j] = ns[++k]; 205 | if (totalNeeded) { 206 | for (var f = 0; f < ratio && totalNeeded; f++) { 207 | result[j + 5] = result[j + 3] = result[j + 1] = dx; 208 | result[j + 6] = result[j + 4] = result[j + 2] = dy; 209 | j += 6; 210 | totalNeeded -= 6; 211 | } 212 | } 213 | } 214 | return result; 215 | } 216 | 217 | function perimeterPoints(pts) { 218 | var n = pts.length; 219 | var x2 = pts[n - 2]; 220 | var y2 = pts[n - 1]; 221 | var p = 0; 222 | for (var i = 0; i < n; i += 6) { 223 | p += distance(pts[i], pts[i + 1], x2, y2); 224 | x2 = pts[i]; 225 | y2 = pts[i + 1]; 226 | } 227 | return floor(p); 228 | } 229 | 230 | function getSortedSegments(pathSegments) { 231 | return pathSegments 232 | .map(function (points) { return ({ 233 | points: points, 234 | perimeter: perimeterPoints(points) 235 | }); }) 236 | .sort(function (a, b) { return b.perimeter - a.perimeter; }) 237 | .map(function (a) { return a.points; }); 238 | } 239 | 240 | function normalizePaths(left, right, options) { 241 | if (options.optimize === FILL) { 242 | left = getSortedSegments(left); 243 | right = getSortedSegments(right); 244 | } 245 | if (left.length !== right.length) { 246 | if (options.optimize === FILL) { 247 | fillSegments(left, right, options.origin); 248 | } 249 | else { 250 | raiseError('optimize:none requires equal lengths'); 251 | } 252 | } 253 | var matrix = Array(2); 254 | matrix[0] = left; 255 | matrix[1] = right; 256 | if (options.optimize === FILL) { 257 | var _a = options.origin, x = _a.x, y = _a.y, absolute = _a.absolute; 258 | for (var i = 0; i < left.length; i++) { 259 | normalizePoints(absolute, x, y, matrix[0][i]); 260 | normalizePoints(absolute, x, y, matrix[1][i]); 261 | } 262 | fillPoints(matrix, options.addPoints * 6); 263 | } 264 | return matrix; 265 | } 266 | 267 | function fillObject(dest, src) { 268 | for (var key in src) { 269 | if (!dest.hasOwnProperty(key)) { 270 | dest[key] = src[key]; 271 | } 272 | } 273 | return dest; 274 | } 275 | 276 | var defaultOptions = { 277 | addPoints: 0, 278 | optimize: FILL, 279 | origin: { x: 0, y: 0 }, 280 | precision: 0 281 | }; 282 | function interpolatePath(paths, options) { 283 | options = fillObject(options, defaultOptions); 284 | if (!paths || paths.length < 2) { 285 | raiseError('invalid arguments'); 286 | } 287 | var hlen = paths.length - 1; 288 | var items = Array(hlen); 289 | for (var h = 0; h < hlen; h++) { 290 | items[h] = getPathInterpolator(paths[h], paths[h + 1], options); 291 | } 292 | var formatter = !options.precision ? _ : function (n) { return n.toFixed(options.precision); }; 293 | return function (offset) { 294 | var d = hlen * offset; 295 | var flr = min(floor(d), hlen - 1); 296 | return renderPath(items[flr]((d - flr) / (flr + 1)), formatter); 297 | }; 298 | } 299 | function getPathInterpolator(left, right, options) { 300 | var matrix = normalizePaths(left.getData(), right.getData(), options); 301 | var n = matrix[0].length; 302 | return function (offset) { 303 | if (abs(offset - 0) < EPSILON) { 304 | return left.getStringData(); 305 | } 306 | if (abs(offset - 1) < EPSILON) { 307 | return right.getStringData(); 308 | } 309 | var results = Array(n); 310 | for (var h = 0; h < n; h++) { 311 | results[h] = mixPoints(matrix[0][h], matrix[1][h], offset); 312 | } 313 | return results; 314 | }; 315 | } 316 | function mixPoints(a, b, o) { 317 | var alen = a.length; 318 | var results = createNumberArray(alen); 319 | for (var i = 0; i < alen; i++) { 320 | results[i] = a[i] + (b[i] - a[i]) * o; 321 | } 322 | return results; 323 | } 324 | 325 | function coalesce(current, fallback) { 326 | return current === _ ? fallback : current; 327 | } 328 | 329 | var _120 = PI * 120 / 180; 330 | var PI2 = PI * 2; 331 | function arcToCurve(x1, y1, rx, ry, angle, large, sweep, dx, dy, f1, f2, cx, cy) { 332 | if (rx <= 0 || ry <= 0) { 333 | return [x1, y1, dx, dy, dx, dy]; 334 | } 335 | var rad = PI / 180 * (+angle || 0); 336 | var cosrad = cos(rad); 337 | var sinrad = sin(rad); 338 | var recursive = !!f1; 339 | if (!recursive) { 340 | var x1old = x1; 341 | var dxold = dx; 342 | x1 = x1old * cosrad - y1 * -sinrad; 343 | y1 = x1old * -sinrad + y1 * cosrad; 344 | dx = dxold * cosrad - dy * -sinrad; 345 | dy = dxold * -sinrad + dy * cosrad; 346 | var x = (x1 - dx) / 2; 347 | var y = (y1 - dy) / 2; 348 | var h = x * x / (rx * rx) + y * y / (ry * ry); 349 | if (h > 1) { 350 | h = sqrt(h); 351 | rx = h * rx; 352 | ry = h * ry; 353 | } 354 | var k = (large === sweep ? -1 : 1) * 355 | sqrt(abs((rx * rx * ry * ry - rx * rx * y * y - ry * ry * x * x) / (rx * rx * y * y + ry * ry * x * x))); 356 | cx = k * rx * y / ry + (x1 + dx) / 2; 357 | cy = k * -ry * x / rx + (y1 + dy) / 2; 358 | f1 = asin((y1 - cy) / ry); 359 | f2 = asin((dy - cy) / ry); 360 | if (x1 < cx) { 361 | f1 = PI - f1; 362 | } 363 | if (dx < cx) { 364 | f2 = PI - f2; 365 | } 366 | if (f1 < 0) { 367 | f1 += PI2; 368 | } 369 | if (f2 < 0) { 370 | f2 += PI2; 371 | } 372 | if (sweep && f1 > f2) { 373 | f1 -= PI2; 374 | } 375 | if (!sweep && f2 > f1) { 376 | f2 -= PI2; 377 | } 378 | } 379 | var res; 380 | if (abs(f2 - f1) > _120) { 381 | var f2old = f2; 382 | var x2old = dx; 383 | var y2old = dy; 384 | f2 = f1 + _120 * (sweep && f2 > f1 ? 1 : -1); 385 | dx = cx + rx * cos(f2); 386 | dy = cy + ry * sin(f2); 387 | res = arcToCurve(dx, dy, rx, ry, angle, 0, sweep, x2old, y2old, f2, f2old, cx, cy); 388 | } 389 | else { 390 | res = []; 391 | } 392 | var t = 4 / 3 * tan((f2 - f1) / 4); 393 | res.splice(0, 0, 2 * x1 - (x1 + t * rx * sin(f1)), 2 * y1 - (y1 - t * ry * cos(f1)), dx + t * rx * sin(f2), dy - t * ry * cos(f2), dx, dy); 394 | if (!recursive) { 395 | for (var i = 0, ilen = res.length; i < ilen; i += 2) { 396 | var xt = res[i], yt = res[i + 1]; 397 | res[i] = xt * cosrad - yt * sinrad; 398 | res[i + 1] = xt * sinrad + yt * cosrad; 399 | } 400 | } 401 | return res; 402 | } 403 | 404 | var argLengths = { M: 2, H: 1, V: 1, L: 2, Z: 0, C: 6, S: 4, Q: 4, T: 2, A: 7 }; 405 | function addCurve(ctx, x1, y1, x2, y2, dx, dy) { 406 | var x = ctx.x; 407 | var y = ctx.y; 408 | ctx.x = coalesce(dx, x); 409 | ctx.y = coalesce(dy, y); 410 | ctx.current.push(coalesce(x1, x), (y1 = coalesce(y1, y)), (x2 = coalesce(x2, x)), (y2 = coalesce(y2, y)), ctx.x, ctx.y); 411 | ctx.lc = ctx.c; 412 | } 413 | function convertToAbsolute(ctx) { 414 | var c = ctx.c; 415 | var t = ctx.t; 416 | var x = ctx.x; 417 | var y = ctx.y; 418 | if (c === DRAW_LINE_VERTICAL) { 419 | t[0] += y; 420 | } 421 | else if (c === DRAW_LINE_HORIZONTAL) { 422 | t[0] += x; 423 | } 424 | else if (c === DRAW_ARC) { 425 | t[5] += x; 426 | t[6] += y; 427 | } 428 | else { 429 | for (var j = 0; j < t.length; j += 2) { 430 | t[j] += x; 431 | t[j + 1] += y; 432 | } 433 | } 434 | } 435 | function parseSegments(d) { 436 | return d 437 | .replace(/[\^\s]*([mhvlzcsqta]|\-?\d*\.?\d+)[,\$\s]*/gi, ' $1') 438 | .replace(/([mhvlzcsqta])/gi, ' $1') 439 | .trim() 440 | .split(' ') 441 | .map(parseSegment); 442 | } 443 | function parseSegment(s2) { 444 | return s2.split(SPACE).map(parseCommand); 445 | } 446 | function parseCommand(str, i) { 447 | return i === 0 ? str : +str; 448 | } 449 | function parsePoints(d) { 450 | var ctx = { 451 | x: 0, 452 | y: 0, 453 | segments: [] 454 | }; 455 | var segments = parseSegments(d); 456 | for (var i = 0; i < segments.length; i++) { 457 | var terms = segments[i]; 458 | var commandLetter = terms[0]; 459 | var command = commandLetter.toUpperCase(); 460 | var isRelative = command !== CLOSE_PATH && command !== commandLetter; 461 | ctx.c = command; 462 | var maxLength = argLengths[command]; 463 | var t2 = terms; 464 | var k = 1; 465 | do { 466 | ctx.t = t2.length === 1 ? t2 : t2.slice(k, k + maxLength); 467 | if (isRelative) { 468 | convertToAbsolute(ctx); 469 | } 470 | var n = ctx.t; 471 | var x = ctx.x; 472 | var y = ctx.y; 473 | var x1 = void 0, y1 = void 0, dx = void 0, dy = void 0, x2 = void 0, y2 = void 0; 474 | if (command === MOVE_CURSOR) { 475 | ctx.segments.push((ctx.current = [(ctx.x = n[0]), (ctx.y = n[1])])); 476 | } 477 | else if (command === DRAW_LINE_HORIZONTAL) { 478 | addCurve(ctx, _, _, _, _, n[0], _); 479 | } 480 | else if (command === DRAW_LINE_VERTICAL) { 481 | addCurve(ctx, _, _, _, _, _, n[0]); 482 | } 483 | else if (command === DRAW_LINE) { 484 | addCurve(ctx, _, _, _, _, n[0], n[1]); 485 | } 486 | else if (command === CLOSE_PATH) { 487 | addCurve(ctx, _, _, _, _, ctx.current[0], ctx.current[1]); 488 | } 489 | else if (command === DRAW_CURVE_CUBIC_BEZIER) { 490 | addCurve(ctx, n[0], n[1], n[2], n[3], n[4], n[5]); 491 | ctx.cx = n[2]; 492 | ctx.cy = n[3]; 493 | } 494 | else if (command === DRAW_CURVE_SMOOTH) { 495 | var isInitialCurve = ctx.lc !== DRAW_CURVE_SMOOTH && ctx.lc !== DRAW_CURVE_CUBIC_BEZIER; 496 | x1 = isInitialCurve ? _ : x * 2 - ctx.cx; 497 | y1 = isInitialCurve ? _ : y * 2 - ctx.cy; 498 | addCurve(ctx, x1, y1, n[0], n[1], n[2], n[3]); 499 | ctx.cx = n[0]; 500 | ctx.cy = n[1]; 501 | } 502 | else if (command === DRAW_CURVE_QUADRATIC) { 503 | var cx1 = n[0]; 504 | var cy1 = n[1]; 505 | dx = n[2]; 506 | dy = n[3]; 507 | addCurve(ctx, x + (cx1 - x) * QUADRATIC_RATIO, y + (cy1 - y) * QUADRATIC_RATIO, dx + (cx1 - dx) * QUADRATIC_RATIO, dy + (cy1 - dy) * QUADRATIC_RATIO, dx, dy); 508 | ctx.cx = cx1; 509 | ctx.cy = cy1; 510 | } 511 | else if (command === DRAW_CURVE_QUADRATIC_CONTINUATION) { 512 | dx = n[0]; 513 | dy = n[1]; 514 | if (ctx.lc === DRAW_CURVE_QUADRATIC || ctx.lc === DRAW_CURVE_QUADRATIC_CONTINUATION) { 515 | x1 = x + (x * 2 - ctx.cx - x) * QUADRATIC_RATIO; 516 | y1 = y + (y * 2 - ctx.cy - y) * QUADRATIC_RATIO; 517 | x2 = dx + (x * 2 - ctx.cx - dx) * QUADRATIC_RATIO; 518 | y2 = dy + (y * 2 - ctx.cy - dy) * QUADRATIC_RATIO; 519 | } 520 | else { 521 | x1 = x2 = x; 522 | y1 = y2 = y; 523 | } 524 | addCurve(ctx, x1, y1, x2, y2, dx, dy); 525 | ctx.cx = x2; 526 | ctx.cy = y2; 527 | } 528 | else if (command === DRAW_ARC) { 529 | var beziers = arcToCurve(x, y, n[0], n[1], n[2], n[3], n[4], n[5], n[6]); 530 | for (var j = 0; j < beziers.length; j += 6) { 531 | addCurve(ctx, beziers[j], beziers[j + 1], beziers[j + 2], beziers[j + 3], beziers[j + 4], beziers[j + 5]); 532 | } 533 | } 534 | else { 535 | raiseError(ctx.c, ' is not supported'); 536 | } 537 | k += maxLength; 538 | } while (k < t2.length); 539 | } 540 | return ctx.segments; 541 | } 542 | 543 | var selectorRegex = /^([#|\.]|path)/i; 544 | function convertToPathData(pathSource) { 545 | if (Array.isArray(pathSource)) { 546 | return { data: pathSource, stringData: _ }; 547 | } 548 | var stringData; 549 | if (typeof pathSource === 'string' && selectorRegex.test(pathSource)) { 550 | pathSource = document.querySelector(pathSource); 551 | } 552 | else { 553 | stringData = pathSource; 554 | } 555 | if (typeof pathSource === 'string') { 556 | return { data: parsePoints(pathSource), stringData: stringData }; 557 | } 558 | var pathElement = pathSource; 559 | if (pathElement.tagName.toUpperCase() === 'PATH') { 560 | stringData = pathElement.getAttribute('d'); 561 | return { data: parsePoints(stringData), stringData: stringData }; 562 | } 563 | return raiseError('Unsupported element ', pathElement.tagName); 564 | } 565 | 566 | var Path = (function () { 567 | function Path(pathSelectorOrElement) { 568 | var _a = convertToPathData(pathSelectorOrElement), data = _a.data, stringData = _a.stringData; 569 | this.data = data; 570 | this.stringData = stringData; 571 | } 572 | Path.prototype.getData = function () { 573 | return this.data; 574 | }; 575 | Path.prototype.getStringData = function () { 576 | return this.stringData || (this.stringData = this.render()); 577 | }; 578 | Path.prototype.render = function (formatter) { 579 | if (formatter === void 0) { formatter = round; } 580 | var pathData = this.data; 581 | var result = []; 582 | for (var i = 0; i < pathData.length; i++) { 583 | var n = pathData[i]; 584 | result.push(MOVE_CURSOR, formatter(n[0]), formatter(n[1]), DRAW_CURVE_CUBIC_BEZIER); 585 | var lastResult = void 0; 586 | for (var f = 2; f < n.length; f += 6) { 587 | var p0 = formatter(n[f]); 588 | var p1 = formatter(n[f + 1]); 589 | var p2 = formatter(n[f + 2]); 590 | var p3 = formatter(n[f + 3]); 591 | var dx = formatter(n[f + 4]); 592 | var dy = formatter(n[f + 5]); 593 | var isPoint = p0 == dx && p2 == dx && p1 == dy && p3 == dy; 594 | if (!isPoint || lastResult != 595 | (lastResult = ('' + p0 + p1 + p2 + p3 + dx + dy))) { 596 | result.push(p0, p1, p2, p3, dx, dy); 597 | } 598 | } 599 | } 600 | return result.join(SPACE); 601 | }; 602 | return Path; 603 | }()); 604 | 605 | function interpolate(paths, options) { 606 | return interpolatePath(paths.map(function (path) { return new Path(path); }), options || {}); 607 | } 608 | 609 | exports.Path = Path; 610 | exports.interpolate = interpolate; 611 | 612 | Object.defineProperty(exports, '__esModule', { value: true }); 613 | 614 | })); 615 | -------------------------------------------------------------------------------- /dist/polymorph.min.js: -------------------------------------------------------------------------------- 1 | !function(r,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((r=r||self).polymorph={})}(this,function(r){"use strict" 2 | var _=void 0,g=" ",c="fill",S="V",b="H",q="L",M="Z",j="M",C="C",T="S",E="Q",H="T",U="A" 3 | var t=Math,$=t.abs,u=t.min,s=t.max,o=t.floor,p=t.round,F=t.sqrt,n=t.pow,I=t.cos,L=t.asin,N=t.sin,O=t.tan,Q=t.PI,V=2/3,f=n(2,-52) 4 | function Z(){throw new Error(Array.prototype.join.call(arguments,g))}var e="undefined"!=typeof window&&window.navigator.userAgent,i=/(MSIE |Trident\/|Edge\/)/i.test(e)?Array:Float32Array 5 | function h(r){return new i(r)}function d(r,t,n){var e=function(r){for(var t=r[0],n=r[1],e=n,i=t,a=2;ak){var P=s,S=f,b=u 53 | g=G(f=l+n*I(s=c+k*(o&&c 2 | 3 | Redirecting to https://notoriousb1t.github.io/polymorph-docs/ 4 | 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Christopher Wallis (http://notoriousb1t.com)", 3 | "version": "1.0.2", 4 | "bugs": { 5 | "url": "https://github.com/notoriousb1t/polymorph/issues" 6 | }, 7 | "jest": { 8 | "testURL": "http://localhost/", 9 | "roots": [ 10 | "/tests" 11 | ], 12 | "transform": { 13 | "^.+\\.tsx?$": "ts-jest" 14 | }, 15 | "testRegex": ".*\\.ts$", 16 | "moduleFileExtensions": [ 17 | "ts", 18 | "tsx", 19 | "js", 20 | "jsx", 21 | "json", 22 | "node" 23 | ] 24 | }, 25 | "description": "Morph SVG shapes", 26 | "devDependencies": { 27 | "@types/jest": "^24.0.15", 28 | "@types/node": "^11.9.0", 29 | "del-cli": "^1.1.0", 30 | "jest": "^24.8.0", 31 | "rollup": "^1.16.4", 32 | "rollup-plugin-node-resolve": "^5.2.0", 33 | "rollup-plugin-typescript": "^1.0.0", 34 | "ts-jest": "^24.0.2", 35 | "ts-node": "^8.3.0", 36 | "tslint": "^6.1.3", 37 | "typescript": "^5.4.5", 38 | "uglify-js": "^3.6.0" 39 | }, 40 | "homepage": "https://notoriousb1t.github.io/polymorph-docs", 41 | "license": "MIT", 42 | "main": "./lib/index.js", 43 | "name": "polymorph-js", 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/notoriousb1t/polymorph" 47 | }, 48 | "types": "./types/index", 49 | "typings": "./types/index", 50 | "typeRoots": [ 51 | "node_modules/@types" 52 | ], 53 | "scripts": { 54 | "build": "npm run build:cdn && npm run compress:cdn && npm run build:node", 55 | "build:cdn": "rollup -c ./config/rollup.cdn.js", 56 | "build:node": "tsc -p tsconfig.node.json", 57 | "compress:cdn": "uglifyjs --config-file ./config/compress.json -o dist/polymorph.min.js dist/polymorph.js", 58 | "clean": "node_modules/.bin/del-cli -f dist lib types", 59 | "postversion": "git push --follow-tags && npm publish", 60 | "preversion": "npm run rebuild", 61 | "rebuild": "npm run clean && npm run build", 62 | "test": "jest" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const _ = undefined as undefined 2 | export const SPACE = ' ' 3 | export const FILL = 'fill' as 'fill' 4 | export const NONE = 'none' as 'none' 5 | 6 | 7 | export const DRAW_LINE_VERTICAL = 'V' 8 | export const DRAW_LINE_HORIZONTAL = 'H' 9 | export const DRAW_LINE = 'L' 10 | export const CLOSE_PATH = 'Z' 11 | export const MOVE_CURSOR = 'M' 12 | export const DRAW_CURVE_CUBIC_BEZIER = 'C' 13 | export const DRAW_CURVE_SMOOTH = 'S' 14 | export const DRAW_CURVE_QUADRATIC = 'Q' 15 | export const DRAW_CURVE_QUADRATIC_CONTINUATION = 'T' 16 | export const DRAW_ARC = 'A' 17 | -------------------------------------------------------------------------------- /src/getPath.ts: -------------------------------------------------------------------------------- 1 | import { raiseError } from './utilities/errors'; 2 | import { FloatArray } from './types'; 3 | import { parsePoints } from './operators/parsePoints'; 4 | import { _ } from './constants'; 5 | 6 | const selectorRegex = /^([#|\.]|path)/i; 7 | 8 | export type IPathSource = string | SupportedElement; 9 | type SupportedElement = SVGPathElement; 10 | 11 | /** 12 | * This function figures out what kind of source the path is coming from and converts 13 | * it to the appropriate array representation. 14 | * @param pathSource The source of the path. This can be string path data, a path 15 | * element, or a string containing an HTML selector to a path element. 16 | */ 17 | export function convertToPathData(pathSource: FloatArray[] | IPathSource): IPathData { 18 | if (Array.isArray(pathSource)) { 19 | return { data: pathSource, stringData: _ } 20 | } 21 | let stringData: string | undefined; 22 | if (typeof pathSource === 'string' && selectorRegex.test(pathSource)) { 23 | pathSource = document.querySelector(pathSource) as SupportedElement; 24 | } else { 25 | stringData = pathSource as string; 26 | } 27 | if (typeof pathSource === 'string') { 28 | // at this point, it should be path data. 29 | return { data: parsePoints(pathSource), stringData }; 30 | } 31 | 32 | const pathElement = pathSource as SupportedElement; 33 | if (pathElement.tagName.toUpperCase() === 'PATH') { 34 | // path's can be converted to path data by reading the d property. 35 | stringData = pathElement.getAttribute('d'); 36 | return { data: parsePoints(stringData), stringData }; 37 | } 38 | // in case a non-supported element is passed, throw an error. 39 | return raiseError('Unsupported element ', (pathElement as Element).tagName); 40 | } 41 | 42 | export interface IPathData { 43 | data: FloatArray[]; 44 | stringData: string | undefined; 45 | } 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export { interpolate } from './interpolate' 3 | export { Path } from './path'; 4 | -------------------------------------------------------------------------------- /src/interpolate.ts: -------------------------------------------------------------------------------- 1 | import { interpolatePath } from './operators/interpolatePath' 2 | import { IMorphable, InterpolateOptions } from './types' 3 | import { Path } from './path'; 4 | 5 | /** 6 | * Returns a function to interpolate between the two path shapes. 7 | * @param left path data, CSS selector, or path element 8 | * @param right path data, CSS selector, or path element 9 | */ 10 | export function interpolate(paths: IMorphable[], options?: InterpolateOptions): (offset: number) => string { 11 | return interpolatePath( 12 | paths.map((path: IMorphable) => new Path(path)), options || {}) 13 | } 14 | -------------------------------------------------------------------------------- /src/operators/arcToCurve.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PI, 3 | abs, 4 | cos, 5 | sin, 6 | tan, 7 | sqrt, 8 | asin 9 | // cos, sin, sqrt, asin, tan 10 | } from '../utilities/math' 11 | 12 | const _120 = PI * 120 / 180 13 | const PI2 = PI * 2 14 | 15 | export function arcToCurve( 16 | x1: number, 17 | y1: number, 18 | rx: number, 19 | ry: number, 20 | angle: number, 21 | large: number, 22 | sweep: number, 23 | dx: number, 24 | dy: number, 25 | f1?: number, 26 | f2?: number, 27 | cx?: number, 28 | cy?: number 29 | ): any { 30 | if (rx <= 0 || ry <= 0) { 31 | return [x1, y1, dx, dy, dx, dy] 32 | } 33 | 34 | const rad = PI / 180 * (+angle || 0) 35 | const cosrad = cos(rad) 36 | const sinrad = sin(rad) 37 | const recursive = !!f1 38 | 39 | if (!recursive) { 40 | const x1old = x1 41 | const dxold = dx 42 | x1 = x1old * cosrad - y1 * -sinrad 43 | y1 = x1old * -sinrad + y1 * cosrad 44 | dx = dxold * cosrad - dy * -sinrad 45 | dy = dxold * -sinrad + dy * cosrad 46 | 47 | const x = (x1 - dx) / 2 48 | const y = (y1 - dy) / 2 49 | 50 | let h = x * x / (rx * rx) + y * y / (ry * ry) 51 | if (h > 1) { 52 | h = sqrt(h) 53 | rx = h * rx 54 | ry = h * ry 55 | } 56 | 57 | const k = 58 | (large === sweep ? -1 : 1) * 59 | sqrt(abs((rx * rx * ry * ry - rx * rx * y * y - ry * ry * x * x) / (rx * rx * y * y + ry * ry * x * x))) 60 | 61 | cx = k * rx * y / ry + (x1 + dx) / 2 62 | cy = k * -ry * x / rx + (y1 + dy) / 2 63 | 64 | f1 = asin((y1 - cy) / ry) 65 | f2 = asin((dy - cy) / ry) 66 | 67 | if (x1 < cx) { 68 | f1 = PI - f1 69 | } 70 | if (dx < cx) { 71 | f2 = PI - f2 72 | } 73 | if (f1 < 0) { 74 | f1 += PI2 75 | } 76 | if (f2 < 0) { 77 | f2 += PI2 78 | } 79 | if (sweep && f1 > f2) { 80 | f1 -= PI2 81 | } 82 | if (!sweep && f2 > f1) { 83 | f2 -= PI2 84 | } 85 | } 86 | 87 | let res: number[] 88 | if (abs(f2 - f1) > _120) { 89 | const f2old = f2 90 | const x2old = dx 91 | const y2old = dy 92 | 93 | f2 = f1 + _120 * (sweep && f2 > f1 ? 1 : -1) 94 | dx = cx + rx * cos(f2) 95 | dy = cy + ry * sin(f2) 96 | res = arcToCurve(dx, dy, rx, ry, angle, 0, sweep, x2old, y2old, f2, f2old, cx, cy) 97 | } else { 98 | res = [] 99 | } 100 | 101 | const t = 4 / 3 * tan((f2 - f1) / 4) 102 | 103 | // insert this curve into the beginning of the array 104 | res.splice( 105 | 0, 106 | 0, 107 | 2 * x1 - (x1 + t * rx * sin(f1)), 108 | 2 * y1 - (y1 - t * ry * cos(f1)), 109 | dx + t * rx * sin(f2), 110 | dy - t * ry * cos(f2), 111 | dx, 112 | dy 113 | ) 114 | 115 | if (!recursive) { 116 | // if this is a top-level arc, rotate into position 117 | for (let i = 0, ilen = res.length; i < ilen; i += 2) { 118 | const xt = res[i], yt = res[i + 1] 119 | res[i] = xt * cosrad - yt * sinrad 120 | res[i + 1] = xt * sinrad + yt * cosrad 121 | } 122 | } 123 | 124 | return res 125 | } 126 | -------------------------------------------------------------------------------- /src/operators/computeAbsoluteOrigin.ts: -------------------------------------------------------------------------------- 1 | import { FloatArray } from '../types'; 2 | import { min, max } from '../utilities/math'; 3 | 4 | function computeDimensions(points: FloatArray): { 5 | x: number; 6 | y: number; 7 | w: number; 8 | h: number; 9 | } { 10 | let xmin = points[0]; 11 | let ymin = points[1]; 12 | let ymax = ymin; 13 | let xmax = xmin; 14 | for (let i = 2; i < points.length; i += 6) { 15 | let x = points[i + 4]; 16 | let y = points[i + 5]; 17 | xmin = min(xmin, x); 18 | xmax = max(xmax, x); 19 | ymin = min(ymin, y); 20 | ymax = max(ymax, y); 21 | } 22 | return { 23 | x: xmin, 24 | w: (xmax - xmin), 25 | y: ymin, 26 | h: (ymax - ymin) 27 | }; 28 | } 29 | 30 | export function computeAbsoluteOrigin(relativeX: number, relativeY: number, points: FloatArray): {x: number, y: number} { 31 | const dimensions = computeDimensions(points); 32 | return { 33 | x: dimensions.x + dimensions.w * relativeX, 34 | y: dimensions.y + dimensions.h * relativeY 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/operators/fillPoints.ts: -------------------------------------------------------------------------------- 1 | import { max } from '../utilities/math'; 2 | import { FloatArray, Matrix } from '../types'; 3 | import { createNumberArray } from '../utilities/createNumberArray'; 4 | 5 | export function fillPoints(matrix: Matrix, addPoints: number): void { 6 | const ilen = matrix[0].length 7 | for (let i = 0; i < ilen; i++) { 8 | const left = matrix[0][i] 9 | const right = matrix[1][i] 10 | 11 | // find the target length 12 | const totalLength = max(left.length + addPoints, right.length + addPoints); 13 | 14 | matrix[0][i] = fillSubpath(left, totalLength); 15 | matrix[1][i] = fillSubpath(right, totalLength); 16 | } 17 | } 18 | 19 | export function fillSubpath(ns: FloatArray, totalLength: number): FloatArray { 20 | let totalNeeded = totalLength - ns.length; 21 | const ratio = Math.ceil(totalLength / ns.length); 22 | const result = createNumberArray(totalLength); 23 | 24 | result[0] = ns[0]; 25 | result[1] = ns[1]; 26 | 27 | let k = 1, j = 1; 28 | while (j < totalLength - 1) { 29 | result[++j] = ns[++k]; 30 | result[++j] = ns[++k]; 31 | result[++j] = ns[++k]; 32 | result[++j] = ns[++k]; 33 | const dx = result[++j] = ns[++k]; 34 | const dy = result[++j] = ns[++k]; 35 | 36 | if (totalNeeded) { 37 | for (let f = 0; f < ratio && totalNeeded; f++) { 38 | result[j + 5] = result[j + 3] = result[j + 1] = dx; 39 | result[j + 6] = result[j + 4] = result[j + 2] = dy; 40 | j += 6; 41 | totalNeeded -= 6; 42 | } 43 | } 44 | } 45 | return result; 46 | } 47 | -------------------------------------------------------------------------------- /src/operators/fillSegments.ts: -------------------------------------------------------------------------------- 1 | import { IOrigin, FloatArray } from '../types'; 2 | import { createNumberArray } from '../utilities/createNumberArray'; 3 | import { computeAbsoluteOrigin } from './computeAbsoluteOrigin'; 4 | 5 | 6 | export function fillSegments(larger: FloatArray[], smaller: FloatArray[], origin: IOrigin): void { 7 | const largeLen = larger.length 8 | const smallLen = smaller.length 9 | if (largeLen < smallLen) { 10 | // swap sides so larger is larger (or equal) 11 | return fillSegments(smaller, larger, origin) 12 | } 13 | 14 | // resize the array 15 | smaller.length = largeLen; 16 | for (let i = smallLen; i < largeLen; i++) { 17 | const l = larger[i] 18 | let originX = origin.x; 19 | let originY = origin.y; 20 | if (!origin.absolute) { 21 | const absoluteOrigin = computeAbsoluteOrigin(originX, originY, l); 22 | originX = absoluteOrigin.x; 23 | originY = absoluteOrigin.y; 24 | } 25 | 26 | const d = createNumberArray(l.length) 27 | for (let k = 0; k < l.length; k += 2) { 28 | d[k] = originX; 29 | d[k + 1] = originY; 30 | } 31 | 32 | smaller[i] = d; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/operators/getSortedSegments.ts: -------------------------------------------------------------------------------- 1 | import { FloatArray } from '../types'; 2 | import { perimeterPoints } from './perimeterPoints'; 3 | 4 | interface ISortContext { 5 | points: FloatArray; 6 | perimeter: number; 7 | } 8 | 9 | export function getSortedSegments(pathSegments: FloatArray[]): FloatArray[] { 10 | return pathSegments 11 | .map((points: FloatArray) => ({ 12 | points, 13 | perimeter: perimeterPoints(points) 14 | })) 15 | .sort((a: ISortContext, b: ISortContext) => b.perimeter - a.perimeter) 16 | .map((a: ISortContext) => a.points); 17 | } 18 | -------------------------------------------------------------------------------- /src/operators/interpolatePath.ts: -------------------------------------------------------------------------------- 1 | import { IRenderer, InterpolateOptions, FloatArray } from '../types' 2 | import { renderPath } from './renderPath' 3 | import { EPSILON, abs, floor, min } from '../utilities/math' 4 | import { raiseError } from '../utilities/errors' 5 | import { normalizePaths } from './normalizePaths' 6 | import { fillObject } from '../utilities/objects' 7 | import { createNumberArray } from '../utilities/createNumberArray' 8 | import { FILL, _ } from '../constants' 9 | import { Path } from '../path'; 10 | 11 | const defaultOptions: InterpolateOptions = { 12 | addPoints: 0, 13 | optimize: FILL, 14 | origin: { x: 0, y: 0 }, 15 | precision: 0 16 | } 17 | 18 | /** 19 | * Returns a function to interpolate between the two path shapes. polymorph.parse() must be called 20 | * before invoking this function. In most cases, it is more appropriate to use polymorph.morph() instead of this. 21 | * @param leftPath path to interpolate 22 | * @param rightPath path to interpolate 23 | */ 24 | export function interpolatePath(paths: Path[], options: InterpolateOptions): (offset: number) => string { 25 | options = fillObject(options, defaultOptions) 26 | 27 | if (!paths || paths.length < 2) { 28 | raiseError('invalid arguments') 29 | } 30 | 31 | const hlen = paths.length - 1 32 | const items: IRenderer[] = Array(hlen) 33 | for (let h = 0; h < hlen; h++) { 34 | items[h] = getPathInterpolator(paths[h], paths[h + 1], options) 35 | } 36 | 37 | // create formatter for the precision 38 | const formatter = !options.precision ? _ : (n: number) => n.toFixed(options.precision) 39 | 40 | return (offset: number): string => { 41 | const d = hlen * offset 42 | const flr = min(floor(d), hlen - 1) 43 | return renderPath(items[flr]((d - flr) / (flr + 1)), formatter) 44 | } 45 | } 46 | 47 | function getPathInterpolator(left: Path, right: Path, options: InterpolateOptions): IRenderer { 48 | const matrix = normalizePaths(left.getData(), right.getData(), options) 49 | const n = matrix[0].length 50 | return (offset: number) => { 51 | if (abs(offset - 0) < EPSILON) { 52 | return left.getStringData() 53 | } 54 | if (abs(offset - 1) < EPSILON) { 55 | return right.getStringData() 56 | } 57 | 58 | const results: FloatArray[] = Array(n) 59 | for (let h = 0; h < n; h++) { 60 | results[h] = mixPoints(matrix[0][h], matrix[1][h], offset) 61 | } 62 | return results 63 | } 64 | } 65 | 66 | export function mixPoints(a: FloatArray, b: FloatArray, o: number): FloatArray { 67 | const alen = a.length 68 | const results = createNumberArray(alen) 69 | for (let i = 0; i < alen; i++) { 70 | results[i] = a[i] + (b[i] - a[i]) * o 71 | } 72 | return results 73 | } 74 | -------------------------------------------------------------------------------- /src/operators/normalizePaths.ts: -------------------------------------------------------------------------------- 1 | import { InterpolateOptions, FloatArray, Matrix } from '../types' 2 | import { fillSegments } from './fillSegments' 3 | import { normalizePoints } from './normalizePoints' 4 | import { fillPoints } from './fillPoints' 5 | import { FILL } from '../constants' 6 | import { raiseError } from '../utilities/errors' 7 | import { getSortedSegments } from './getSortedSegments'; 8 | 9 | export function normalizePaths(left: FloatArray[], right: FloatArray[], options: InterpolateOptions): FloatArray[][] { 10 | // sort segments by perimeter size (more or less area) 11 | if (options.optimize === FILL) { 12 | left = getSortedSegments(left); 13 | right = getSortedSegments(right); 14 | } 15 | 16 | if (left.length !== right.length) { 17 | if (options.optimize === FILL) { 18 | // ensure there are an equal amount of segments 19 | fillSegments(left, right, options.origin) 20 | } else { 21 | raiseError('optimize:none requires equal lengths') 22 | } 23 | } 24 | 25 | const matrix = Array(2) as Matrix 26 | matrix[0] = left 27 | matrix[1] = right 28 | 29 | if (options.optimize === FILL) { 30 | const {x, y, absolute} = options.origin; 31 | // shift so both svg's are being drawn from relatively the same place 32 | for (let i = 0; i < left.length; i++) { 33 | normalizePoints(absolute, x, y, matrix[0][i]) 34 | normalizePoints(absolute, x, y, matrix[1][i]) 35 | } 36 | fillPoints(matrix, options.addPoints * 6) 37 | } 38 | return matrix 39 | } 40 | -------------------------------------------------------------------------------- /src/operators/normalizePoints.ts: -------------------------------------------------------------------------------- 1 | import { rotatePoints } from './rotatePoints' 2 | import { _ } from '../constants' 3 | import { distance } from '../utilities/distance' 4 | import { FloatArray } from '../types' 5 | import { computeAbsoluteOrigin } from './computeAbsoluteOrigin'; 6 | 7 | export function normalizePoints(absolute: boolean, originX: number, originY: number, ns: FloatArray): void { 8 | let len = ns.length 9 | if (ns[len - 2] !== ns[0] || ns[len - 1] !== ns[1]) { 10 | // skip redraw if this is not a closed shape 11 | return 12 | } 13 | 14 | if (!absolute) { 15 | const relativeOrigin = computeAbsoluteOrigin(originX, originY, ns); 16 | originX = relativeOrigin.x; 17 | originY = relativeOrigin.y; 18 | } 19 | 20 | // create buffer to hold rotating data 21 | const buffer = ns.slice(2) 22 | len = buffer.length 23 | 24 | // find the index of the shortest distance from the upper left corner 25 | let index: number, minAmount: number 26 | for (let i = 0; i < len; i += 6) { 27 | // find the distance to the upper left corner 28 | const next = distance(originX, originX, buffer[i], buffer[i + 1]) 29 | 30 | if (minAmount === _ || next < minAmount) { 31 | // capture the amount and set the index 32 | minAmount = next 33 | index = i 34 | } 35 | } 36 | 37 | // rotate the points so that index is drawn first 38 | rotatePoints(buffer, index) 39 | 40 | // copy starting position from ending rotated position 41 | ns[0] = buffer[len - 2] 42 | ns[1] = buffer[len - 1] 43 | 44 | // copy rotated/aligned back onto original 45 | for (let i = 0; i < buffer.length; i++) { 46 | ns[i + 2] = buffer[i] 47 | } 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/operators/parsePoints.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:max-line-length 2 | import { _, SPACE, DRAW_LINE_VERTICAL, DRAW_LINE_HORIZONTAL, DRAW_ARC, CLOSE_PATH, MOVE_CURSOR, DRAW_LINE, DRAW_CURVE_CUBIC_BEZIER, DRAW_CURVE_SMOOTH, DRAW_CURVE_QUADRATIC, DRAW_CURVE_QUADRATIC_CONTINUATION } from '../constants'; 3 | import { coalesce } from '../utilities/coalesce'; 4 | import { IParseContext, FloatArray } from '../types'; 5 | import { raiseError } from '../utilities/errors'; 6 | import { QUADRATIC_RATIO } from '../utilities/math'; 7 | import { arcToCurve } from './arcToCurve'; 8 | 9 | // describes the number of arguments each command has 10 | const argLengths = { M: 2, H: 1, V: 1, L: 2, Z: 0, C: 6, S: 4, Q: 4, T: 2, A: 7 }; 11 | 12 | /** 13 | * Adds a curve to the segment 14 | * @param ctx Parser context 15 | * @param x1 start control point x 16 | * @param y1 start control point y 17 | * @param x2 end control point x 18 | * @param y2 end control point y 19 | * @param dx destination x 20 | * @param dy destination y 21 | */ 22 | function addCurve( 23 | ctx: IParseContext, 24 | x1: number | undefined, 25 | y1: number | undefined, 26 | x2: number | undefined, 27 | y2: number | undefined, 28 | dx: number | undefined, 29 | dy: number | undefined 30 | ): void { 31 | // store x and y 32 | const x = ctx.x; 33 | const y = ctx.y; 34 | 35 | // move cursor 36 | ctx.x = coalesce(dx, x); 37 | ctx.y = coalesce(dy, y); 38 | 39 | // add numbers to points 40 | ctx.current.push(coalesce(x1, x), (y1 = coalesce(y1, y)), (x2 = coalesce(x2, x)), (y2 = coalesce(y2, y)), ctx.x, ctx.y); 41 | 42 | // set last command type 43 | ctx.lc = ctx.c; 44 | } 45 | 46 | /** 47 | * Converts the current terms in the context to absolute position based on the 48 | * current cursor position 49 | * @param ctx Parser context 50 | */ 51 | function convertToAbsolute(ctx: IParseContext): void { 52 | const c = ctx.c; 53 | const t = ctx.t; 54 | const x = ctx.x; 55 | const y = ctx.y; 56 | 57 | if (c === DRAW_LINE_VERTICAL) { 58 | t[0] += y; 59 | } else if (c === DRAW_LINE_HORIZONTAL) { 60 | t[0] += x; 61 | } else if (c === DRAW_ARC) { 62 | t[5] += x; 63 | t[6] += y; 64 | } else { 65 | for (let j = 0; j < t.length; j += 2) { 66 | t[j] += x; 67 | t[j + 1] += y; 68 | } 69 | } 70 | } 71 | 72 | function parseSegments(d: string): (string | number)[][] { 73 | // replace all terms with space + term to remove garbage 74 | // replace command letters with an additional space 75 | // remove spaces around 76 | // split on double-space (splits on command segment) 77 | // parse each segment into an of list of command + args 78 | return d 79 | .replace(/[\^\s]*([mhvlzcsqta]|\-?\d*\.?\d+)[,\$\s]*/gi, ' $1') 80 | .replace(/([mhvlzcsqta])/gi, ' $1') 81 | .trim() 82 | .split(' ') 83 | .map(parseSegment); 84 | } 85 | 86 | function parseSegment(s2: string): (string | number)[] { 87 | // split command segment into command + args 88 | return s2.split(SPACE).map(parseCommand); 89 | } 90 | 91 | function parseCommand(str: string, i: number): string | number { 92 | // convert all terms except command into a number 93 | return i === 0 ? str : +str; 94 | } 95 | 96 | /** 97 | * Returns an [] with cursor position + polybezier [mx, my, ...[x1, y1, x2, y2, dx, dy] ] 98 | * @param d string to parse 99 | */ 100 | export function parsePoints(d: string): FloatArray[] { 101 | // create parser context 102 | const ctx: IParseContext = { 103 | x: 0, 104 | y: 0, 105 | segments: [] 106 | }; 107 | 108 | // split into segments 109 | const segments = parseSegments(d); 110 | 111 | // start building 112 | for (let i = 0; i < segments.length; i++) { 113 | const terms = segments[i]; 114 | const commandLetter = terms[0] as string; 115 | 116 | // setup context 117 | const command = commandLetter.toUpperCase() as keyof typeof argLengths; 118 | const isRelative = command !== CLOSE_PATH && command !== commandLetter; 119 | 120 | // set command on context 121 | ctx.c = command; 122 | 123 | const maxLength = argLengths[command]; 124 | 125 | // process each part of this command. Use do-while to accomodate Z 126 | const t2 = terms as number[]; 127 | let k = 1; 128 | do { 129 | // split this segment into a sub-segment 130 | ctx.t = t2.length === 1 ? t2 : t2.slice(k, k + maxLength); 131 | 132 | // convert to absolute if necessary 133 | if (isRelative) { 134 | convertToAbsolute(ctx); 135 | } 136 | // parse 137 | 138 | const n = ctx.t; 139 | const x = ctx.x; 140 | const y = ctx.y; 141 | 142 | let x1: number, y1: number, dx: number, dy: number, x2: number, y2: number; 143 | 144 | if (command === MOVE_CURSOR) { 145 | ctx.segments.push((ctx.current = [(ctx.x = n[0]), (ctx.y = n[1])])); 146 | } else if (command === DRAW_LINE_HORIZONTAL) { 147 | addCurve(ctx, _, _, _, _, n[0], _); 148 | } else if (command === DRAW_LINE_VERTICAL) { 149 | addCurve(ctx, _, _, _, _, _, n[0]); 150 | } else if (command === DRAW_LINE) { 151 | addCurve(ctx, _, _, _, _, n[0], n[1]); 152 | } else if (command === CLOSE_PATH) { 153 | addCurve(ctx, _, _, _, _, ctx.current[0], ctx.current[1]); 154 | } else if (command === DRAW_CURVE_CUBIC_BEZIER) { 155 | addCurve(ctx, n[0], n[1], n[2], n[3], n[4], n[5]); 156 | 157 | // set last control point for sub-sequence C/S 158 | ctx.cx = n[2]; 159 | ctx.cy = n[3]; 160 | } else if (command === DRAW_CURVE_SMOOTH) { 161 | const isInitialCurve = ctx.lc !== DRAW_CURVE_SMOOTH && ctx.lc !== DRAW_CURVE_CUBIC_BEZIER; 162 | x1 = isInitialCurve ? _ : x * 2 - ctx.cx; 163 | y1 = isInitialCurve ? _ : y * 2 - ctx.cy; 164 | 165 | addCurve(ctx, x1, y1, n[0], n[1], n[2], n[3]); 166 | 167 | // set last control point for sub-sequence C/S 168 | ctx.cx = n[0]; 169 | ctx.cy = n[1]; 170 | } else if (command === DRAW_CURVE_QUADRATIC) { 171 | const cx1 = n[0]; 172 | const cy1 = n[1]; 173 | dx = n[2]; 174 | dy = n[3]; 175 | 176 | addCurve( 177 | ctx, 178 | x + (cx1 - x) * QUADRATIC_RATIO, 179 | y + (cy1 - y) * QUADRATIC_RATIO, 180 | dx + (cx1 - dx) * QUADRATIC_RATIO, 181 | dy + (cy1 - dy) * QUADRATIC_RATIO, 182 | dx, 183 | dy 184 | ); 185 | 186 | ctx.cx = cx1; 187 | ctx.cy = cy1; 188 | } else if (command === DRAW_CURVE_QUADRATIC_CONTINUATION) { 189 | dx = n[0]; 190 | dy = n[1]; 191 | 192 | if (ctx.lc === DRAW_CURVE_QUADRATIC || ctx.lc === DRAW_CURVE_QUADRATIC_CONTINUATION) { 193 | x1 = x + (x * 2 - ctx.cx - x) * QUADRATIC_RATIO; 194 | y1 = y + (y * 2 - ctx.cy - y) * QUADRATIC_RATIO; 195 | x2 = dx + (x * 2 - ctx.cx - dx) * QUADRATIC_RATIO; 196 | y2 = dy + (y * 2 - ctx.cy - dy) * QUADRATIC_RATIO; 197 | } else { 198 | x1 = x2 = x; 199 | y1 = y2 = y; 200 | } 201 | 202 | addCurve(ctx, x1, y1, x2, y2, dx, dy); 203 | 204 | ctx.cx = x2; 205 | ctx.cy = y2; 206 | } else if (command === DRAW_ARC) { 207 | const beziers = arcToCurve(x, y, n[0], n[1], n[2], n[3], n[4], n[5], n[6]); 208 | 209 | for (let j = 0; j < beziers.length; j += 6) { 210 | addCurve(ctx, beziers[j], beziers[j + 1], beziers[j + 2], beziers[j + 3], beziers[j + 4], beziers[j + 5]); 211 | } 212 | } else { 213 | raiseError(ctx.c, ' is not supported'); 214 | } 215 | 216 | k += maxLength; 217 | } while (k < t2.length); 218 | } 219 | 220 | // return segments with the largest sub-paths first 221 | // this makes it more likely that holes will be filled 222 | return ctx.segments; 223 | } 224 | -------------------------------------------------------------------------------- /src/operators/perimeterPoints.ts: -------------------------------------------------------------------------------- 1 | import { floor } from '../utilities/math'; 2 | import { distance } from '../utilities/distance'; 3 | import { FloatArray } from '../types'; 4 | 5 | /** 6 | * Approximates the perimeter around a shape 7 | * @param pts 8 | */ 9 | export function perimeterPoints(pts: FloatArray): number { 10 | const n = pts.length 11 | let x2 = pts[n - 2] 12 | let y2 = pts[n - 1] 13 | let p = 0 14 | 15 | for (let i = 0; i < n; i += 6) { 16 | p += distance(pts[i], pts[i + 1], x2, y2) 17 | x2 = pts[i] 18 | y2 = pts[i + 1] 19 | } 20 | 21 | return floor(p) 22 | } 23 | -------------------------------------------------------------------------------- /src/operators/renderPath.ts: -------------------------------------------------------------------------------- 1 | import {SPACE, MOVE_CURSOR, DRAW_CURVE_CUBIC_BEZIER } from '../constants' 2 | import { isString } from '../utilities/inspect' 3 | import { FloatArray, Func } from '../types' 4 | import { round } from '../utilities/math'; 5 | 6 | /** 7 | * Converts poly-bezier data back to SVG Path data. 8 | * @param ns poly-bezier data 9 | */ 10 | export function renderPath(ns: FloatArray[] | string, formatter: Func = round): string { 11 | if (isString(ns)) { 12 | return ns as string 13 | } 14 | 15 | let result = [] 16 | for (let i = 0; i < ns.length; i++) { 17 | const n = ns[i] as FloatArray 18 | result.push(MOVE_CURSOR, formatter(n[0]), formatter(n[1]), DRAW_CURVE_CUBIC_BEZIER); 19 | let lastResult; 20 | for (let f = 2; f < n.length; f += 6) { 21 | const p0 = formatter(n[f]) 22 | const p1 = formatter(n[f + 1]) 23 | const p2 = formatter(n[f + 2]) 24 | const p3 = formatter(n[f + 3]) 25 | const dx = formatter(n[f + 4]) 26 | const dy = formatter(n[f + 5]) 27 | 28 | // this comparision purposefully needs to coerce numbers and string interchangably 29 | // tslint:disable-next-line:triple-equals 30 | const isPoint = p0 == dx && p2 == dx && p1 == dy && p3 == dy; 31 | 32 | // prevent duplicate points from rendering 33 | // tslint:disable-next-line:triple-equals 34 | if (!isPoint || lastResult != 35 | // tslint:disable-next-line:no-conditional-assignment 36 | (lastResult = ('' + p0 + p1 + p2 + p3 + dx + dy))) { 37 | result.push(p0, p1, p2, p3, dx, dy) 38 | } 39 | } 40 | } 41 | return result.join(SPACE) 42 | } 43 | -------------------------------------------------------------------------------- /src/operators/rotatePoints.ts: -------------------------------------------------------------------------------- 1 | import { FloatArray } from '../types' 2 | import { createNumberArray } from '../utilities/createNumberArray'; 3 | 4 | export function rotatePoints(ns: FloatArray, count: number): void { 5 | const len = ns.length 6 | const rightLen = len - count 7 | const buffer = createNumberArray(count) 8 | let i: number 9 | 10 | // write to a temporary buffer 11 | for (i = 0; i < count; i++) { 12 | buffer[i] = ns[i] 13 | } 14 | 15 | // overwrite from the starting point 16 | for (i = count; i < len; i++) { 17 | ns[i - count] = ns[i] 18 | } 19 | 20 | // write temporary buffer back in 21 | for (i = 0; i < count; i++) { 22 | ns[rightLen + i] = buffer[i] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/path.ts: -------------------------------------------------------------------------------- 1 | import { FloatArray } from './types'; 2 | import { convertToPathData, IPathSource } from './getPath'; 3 | import { round } from './utilities/math'; 4 | import { DRAW_CURVE_CUBIC_BEZIER, MOVE_CURSOR, SPACE } from './constants'; 5 | 6 | export class Path { 7 | private data: FloatArray[]; 8 | private stringData: string | undefined; 9 | 10 | constructor(pathSelectorOrElement: IPathSource | FloatArray[]) { 11 | const { data, stringData } = convertToPathData(pathSelectorOrElement); 12 | this.data = data; 13 | this.stringData = stringData; 14 | } 15 | 16 | public getData(): FloatArray[] { 17 | return this.data; 18 | } 19 | 20 | public getStringData(): string { 21 | return this.stringData || (this.stringData = this.render()); 22 | } 23 | 24 | public render(formatter: (n: number) => (number | string) = round): string { 25 | const pathData = this.data; 26 | 27 | let result = [] 28 | for (let i = 0; i < pathData.length; i++) { 29 | const n = pathData[i]; 30 | result.push(MOVE_CURSOR, formatter(n[0]), formatter(n[1]), DRAW_CURVE_CUBIC_BEZIER); 31 | let lastResult; 32 | for (let f = 2; f < n.length; f += 6) { 33 | const p0 = formatter(n[f]) 34 | const p1 = formatter(n[f + 1]) 35 | const p2 = formatter(n[f + 2]) 36 | const p3 = formatter(n[f + 3]) 37 | const dx = formatter(n[f + 4]) 38 | const dy = formatter(n[f + 5]) 39 | 40 | // this comparision purposefully needs to coerce numbers and string interchangably 41 | // tslint:disable-next-line:triple-equals 42 | const isPoint = p0 == dx && p2 == dx && p1 == dy && p3 == dy; 43 | 44 | // prevent duplicate points from rendering 45 | // tslint:disable-next-line:triple-equals 46 | if (!isPoint || lastResult != 47 | // tslint:disable-next-line:no-conditional-assignment 48 | (lastResult = ('' + p0 + p1 + p2 + p3 + dx + dy))) { 49 | result.push(p0, p1, p2, p3, dx, dy) 50 | } 51 | } 52 | } 53 | return result.join(SPACE) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Matrix = [FloatArray[], FloatArray[]] 2 | export type Func = (input: T1) => T2; 3 | 4 | export interface IRenderer { 5 | (offset: number): T 6 | } 7 | 8 | export interface IPath { 9 | path: string 10 | data: FloatArray[] 11 | } 12 | 13 | export type IMorphable = string | SVGPathElement; 14 | 15 | /** 16 | * Used to keep track of the current state of the path/point parser 17 | */ 18 | export interface IParseContext { 19 | /** 20 | * Cursor X position 21 | */ 22 | x: number 23 | /** 24 | * Cursor Y position 25 | */ 26 | y: number 27 | /** 28 | * Last Control X 29 | */ 30 | cx?: number 31 | /** 32 | * Last Control Y 33 | */ 34 | cy?: number 35 | /** 36 | * Last command that was seen 37 | */ 38 | lc?: string 39 | /** 40 | * Current command being parsed 41 | */ 42 | c?: string 43 | /** 44 | * Terms being parsed 45 | */ 46 | t?: FloatArray 47 | /** 48 | * All segments 49 | */ 50 | segments: FloatArray[] 51 | /** 52 | * Current poly-bezier. (The one being bult) 53 | */ 54 | current?: number[] 55 | } 56 | 57 | // tslint:disable-next-line:interface-name 58 | export interface InterpolateOptions { 59 | /** 60 | * Origin of the shape 61 | */ 62 | origin?: IOrigin 63 | /** 64 | * Determines the strategy to optimize two paths for each other. 65 | * 66 | * - none: use when both shapes have an equal number of subpaths and points 67 | * - fill: (default) creates subpaths and adds points to align both paths 68 | */ 69 | optimize?: 'none' | 'fill' 70 | 71 | /** 72 | * Number of points to add when using optimize: fill. The default is 0. 73 | */ 74 | addPoints?: number 75 | 76 | /** 77 | * Number of decimal places to use when rendering 'd' strings. 78 | * For most animations, 0 is recommended. If very small shapes are being used, this can be increased to 79 | * improve smoothness at the cost of (browser) rendering speed 80 | * The default is 0 (no decimal places) and also the recommended value. 81 | */ 82 | precision?: number 83 | } 84 | 85 | // tslint:disable-next-line:interface-name 86 | export interface FloatArray { 87 | length: number 88 | [index: number]: number 89 | slice(startIndex: number): FloatArray; 90 | } 91 | 92 | export interface IFloatArrayConstructor { 93 | new (count: number): FloatArray 94 | } 95 | 96 | export interface IOrigin { 97 | /** 98 | * The y position 99 | */ 100 | x: number 101 | /** 102 | * The x position 103 | */ 104 | y: number 105 | /** 106 | * If true, x and y are absolute coordinates in the SVG. 107 | * If false, x and y are a number between 0 and 1 representing 0% to 100% of the matched subpath 108 | */ 109 | absolute?: boolean 110 | } 111 | -------------------------------------------------------------------------------- /src/utilities/browser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file does browser sniffing (oh no!) to figure out if this is Internet Explorer or Edge 3 | */ 4 | 5 | // tslint:disable-next-line:no-var-keyword 6 | var userAgent = typeof window !== 'undefined' && window.navigator.userAgent 7 | export const isEdge = /(MSIE |Trident\/|Edge\/)/i.test(userAgent) 8 | -------------------------------------------------------------------------------- /src/utilities/coalesce.ts: -------------------------------------------------------------------------------- 1 | import { _ } from '../constants' 2 | 3 | /** 4 | * Evaluates the value for undefined, the fallback value is used if true 5 | * @param current value to evaluate 6 | * @param fallback value to set if equal to undefined 7 | */ 8 | export function coalesce(current: T, fallback: T): T { 9 | return current === _ ? fallback : current 10 | } 11 | -------------------------------------------------------------------------------- /src/utilities/createNumberArray.ts: -------------------------------------------------------------------------------- 1 | import { FloatArray } from '../types' 2 | import { isEdge } from './browser'; 3 | 4 | const arrayConstructor = isEdge ? Array : Float32Array 5 | 6 | export function createNumberArray(n: number): FloatArray { 7 | return new arrayConstructor(n) 8 | } 9 | -------------------------------------------------------------------------------- /src/utilities/distance.ts: -------------------------------------------------------------------------------- 1 | import { sqrt } from './math'; 2 | 3 | export function distance(x1: number, y1: number, x2: number, y2: number): number { 4 | return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)) 5 | } 6 | -------------------------------------------------------------------------------- /src/utilities/errors.ts: -------------------------------------------------------------------------------- 1 | import { SPACE } from '../constants'; 2 | 3 | export function raiseError(...messages: string[]): never 4 | export function raiseError(): never { 5 | throw new Error(Array.prototype.join.call(arguments, SPACE)) 6 | } 7 | -------------------------------------------------------------------------------- /src/utilities/inspect.ts: -------------------------------------------------------------------------------- 1 | export function isString(obj: any): boolean { 2 | return typeof obj === 'string' 3 | } 4 | -------------------------------------------------------------------------------- /src/utilities/math.ts: -------------------------------------------------------------------------------- 1 | const math = Math 2 | export const abs = math.abs 3 | export const min = math.min 4 | export const max = math.max 5 | export const floor = math.floor 6 | export const round = math.round 7 | export const sqrt = math.sqrt 8 | export const pow = math.pow 9 | export const cos = math.cos 10 | export const asin = math.asin 11 | export const sin = math.sin 12 | export const tan = math.tan 13 | export const PI = math.PI 14 | 15 | /** Ratio from the control point in a quad to a cubic bezier control points */ 16 | export const QUADRATIC_RATIO = 2.0 / 3 17 | 18 | /** Equivalent to Number.EPSILON */ 19 | export const EPSILON = pow(2, -52) 20 | -------------------------------------------------------------------------------- /src/utilities/objects.ts: -------------------------------------------------------------------------------- 1 | export function fillObject(dest: Record, src: T): T { 2 | for (let key in src) { 3 | if (!dest.hasOwnProperty(key)) { 4 | dest[key] = src[key] 5 | } 6 | } 7 | return dest as T; 8 | } 9 | -------------------------------------------------------------------------------- /tests/operators/fillPoints.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { fillSubpath } from '../../src/operators/fillPoints' 3 | 4 | test('it returns the same path if the same length is set', () => { 5 | // prettier-ignore 6 | const start = [ 7 | 10, 10, 8 | 0, 0, 0, 0, 0, 0, 9 | 1, 1, 1, 1, 1, 1, 10 | 2, 2, 2, 2, 2, 2 11 | ] 12 | const actual = fillSubpath(start, 20) 13 | 14 | assert.equal(20, actual.length) 15 | 16 | // prettier-ignore 17 | assert.deepEqual(actual, Float32Array.from([ 18 | 10, 10, 19 | 0, 0, 0, 0, 0, 0, 20 | 1, 1, 1, 1, 1, 1, 21 | 2, 2, 2, 2, 2, 2 22 | ])) 23 | }) 24 | 25 | test('it fills the path as possible when a new set is available', () => { 26 | // prettier-ignore 27 | const start = [ 28 | 10, 10, 29 | 0, 0, 0, 0, 0, 0, 30 | 1, 1, 1, 1, 1, 1, 31 | 2, 2, 2, 2, 2, 2 32 | ] 33 | const actual = fillSubpath(start, 26) 34 | 35 | assert.equal(26, actual.length) 36 | 37 | // prettier-ignore 38 | assert.deepEqual(actual, Float32Array.from([ 39 | 10, 10, 40 | 0, 0, 0, 0, 0, 0, 41 | 0, 0, 0, 0, 0, 0, 42 | 1, 1, 1, 1, 1, 1, 43 | 2, 2, 2, 2, 2, 2 44 | ])) 45 | }) 46 | 47 | test('it fills the path evenly if there are twice as many elements', () => { 48 | // prettier-ignore 49 | const start = [ 50 | 10, 10, 51 | 0, 0, 0, 0, 0, 0, 52 | 1, 1, 1, 1, 1, 1, 53 | 2, 2, 2, 2, 2, 2 54 | ] 55 | const actual = fillSubpath(start, 38) 56 | // prettier-ignore 57 | const expected = Float32Array.from([ 58 | 10, 10, 59 | 0, 0, 0, 0, 0, 0, 60 | 0, 0, 0, 0, 0, 0, 61 | 0, 0, 0, 0, 0, 0, 62 | 1, 1, 1, 1, 1, 1, 63 | 1, 1, 1, 1, 1, 1, 64 | 2, 2, 2, 2, 2, 2 65 | ]) 66 | 67 | assert.equal(38, actual.length) 68 | assert.deepEqual(actual, expected) 69 | }) 70 | -------------------------------------------------------------------------------- /tests/operators/fillSegments.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { Path } from '../../src/path'; 3 | import { fillSegments } from '../../src/operators/fillSegments'; 4 | 5 | 6 | test('fills segments from the right', () => { 7 | const left = new Path('M0,0 V12 H12 V0z M16,16 V20 H20 V16z'); 8 | const right = new Path('M0,0 V12 H12 V0z'); 9 | 10 | fillSegments(left.getData(), right.getData(), { x: 0, y: 0 }); 11 | 12 | assert.equal(left.getData().length, right.getData().length); 13 | }) 14 | 15 | test('fills segments from the left', () => { 16 | const right = new Path('M0,0 V12 H12 V0z M16,16 V20 H20 V16z'); 17 | const left = new Path('M0,0 V12 H12 V0z'); 18 | 19 | fillSegments(left.getData(), right.getData(), { x: 0, y: 0 }); 20 | 21 | assert.equal(left.getData().length, right.getData().length); 22 | }) 23 | -------------------------------------------------------------------------------- /tests/operators/interpolatePath.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { mixPoints } from '../../src/operators/interpolatePath' 3 | 4 | 5 | test('finds the midpoint when .25 is provided', () => { 6 | assert.deepEqual(mixPoints([0, 0, 0], [10, 100, 1000], 0.25), Float32Array.from([2.5, 25, 250])) 7 | }) 8 | test('finds the midpoint when .5 is provided', () => { 9 | assert.deepEqual(mixPoints([0, 0, 0], [10, 100, 1000], 0.5), Float32Array.from([5, 50, 500])) 10 | }) 11 | test('finds the midpoint when .75 is provided', () => { 12 | assert.deepEqual(mixPoints([0, 0, 0], [10, 100, 1000], 0.75), Float32Array.from([7.5, 75, 750])) 13 | }) -------------------------------------------------------------------------------- /tests/operators/parsePath.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { Path } from '../../src/path' 3 | import { parsePoints } from '../../src/operators/parsePoints' 4 | import { renderPath } from '../../src/operators/renderPath' 5 | 6 | test('parses terms properly with spaces', () => { 7 | assert.deepEqual(new Path('M 10 42 v 0').getData()[0], [10, 42, 10, 42, 10, 42, 10, 42]) 8 | }) 9 | test('ignores spaces, tabs, and new lines', () => { 10 | assert.deepEqual(new Path('M10,42\n \tv0').getData()[0], [10, 42, 10, 42, 10, 42, 10, 42]) 11 | }) 12 | test('parses terms properly with commas', () => { 13 | assert.deepEqual(new Path('M10,42v0').getData()[0], [10, 42, 10, 42, 10, 42, 10, 42]) 14 | }) 15 | test('parses move (M | m)', () => { 16 | assert.deepEqual(new Path('M 10 42v0').getData()[0], [10, 42, 10, 42, 10, 42, 10, 42]) 17 | }) 18 | test('parses move (Z | z)', () => { 19 | assert.deepEqual(new Path('M 10 42z').getData()[0], [10, 42, 10, 42, 10, 42, 10, 42]) 20 | }) 21 | test('parses h', () => { 22 | assert.deepEqual(new Path('M 10 50 h 50').getData()[0], [10, 50, 10, 50, 10, 50, 60, 50]) 23 | }) 24 | test('parses H', () => { 25 | assert.deepEqual(new Path('M 10 50 H 60').getData()[0], [10, 50, 10, 50, 10, 50, 60, 50]) 26 | }) 27 | test('parses v', () => { 28 | assert.deepEqual(new Path('M 50 10 v 50').getData()[0], [50, 10, 50, 10, 50, 10, 50, 60]) 29 | }) 30 | test('parses V', () => { 31 | assert.deepEqual(new Path('M 50 10 V 60').getData()[0], [50, 10, 50, 10, 50, 10, 50, 60]) 32 | }) 33 | test('parses l', () => { 34 | assert.deepEqual(new Path('M 10 10 l 10 10').getData()[0], [10, 10, 10, 10, 10, 10, 20, 20]) 35 | }) 36 | test('parses L', () => { 37 | assert.deepEqual(new Path('M 10 10 L 20 20').getData()[0], [10, 10, 10, 10, 10, 10, 20, 20]) 38 | }) 39 | test('parses c', () => { 40 | assert.deepEqual(new Path('M 10 10 c 10 5 5 10 25 25').getData()[0], [10, 10, 20, 15, 15, 20, 35, 35]) 41 | }) 42 | test('parses C', () => { 43 | assert.deepEqual(new Path('M 10 10 C 20 15 15 20 35 35').getData()[0], [10, 10, 20, 15, 15, 20, 35, 35]) 44 | }) 45 | test('parses s', () => { 46 | assert.deepEqual(new Path('M 10 10 s 50 35 55 85').getData()[0], [10, 10, 10, 10, 60, 45, 65, 95]) 47 | }) 48 | test('parses s + s', () => { 49 | const actual = new Path('M 10 10 s 10 40 25 25 s 10 40 25 25').getData()[0] 50 | assert.deepEqual(actual, [10, 10, 10, 10, 20, 50, 35, 35, 50, 20, 45, 75, 60, 60]) 51 | }) 52 | test('parses s with multiple argument sets', () => { 53 | assert.deepEqual( 54 | renderPath(parsePoints('M 10 10 s 10 40 25 25 10 40 25 25'), Math.round), 55 | 'M 10 10 C 10 10 20 50 35 35 50 20 45 75 60 60' 56 | ) 57 | }) 58 | test('parses S', () => { 59 | assert.deepEqual(new Path('M 10 10 S 20 15 35 35').getData()[0], [10, 10, 10, 10, 20, 15, 35, 35]) 60 | }) 61 | test('parses S + S', () => { 62 | const actual = new Path('M 10 10 S 20 50 35 35 S 45 75 60 60').getData()[0] 63 | assert.deepEqual(actual, [10, 10, 10, 10, 20, 50, 35, 35, 50, 20, 45, 75, 60, 60]) 64 | }) 65 | test('parses S with multiple argument sets', () => { 66 | assert.deepEqual( 67 | renderPath(parsePoints('M 10 10 S 20 50 35 35 45 75 60 60'), Math.round), 68 | 'M 10 10 C 10 10 20 50 35 35 50 20 45 75 60 60' 69 | ) 70 | }) 71 | test('parses c followed by s', () => { 72 | assert.deepEqual( 73 | renderPath(parsePoints('M 10 10 c10 10 10 40 25 25 s10 40 25 25'), Math.round), 74 | 'M 10 10 C 20 20 20 50 35 35 50 20 45 75 60 60' 75 | ) 76 | }) 77 | test('parses q', () => { 78 | assert.deepEqual(renderPath(parsePoints('M 10 10 q 10 5 15 25'), Math.round), 'M 10 10 C 17 13 22 22 25 35') 79 | }) 80 | test('parses Q', () => { 81 | assert.deepEqual(renderPath(parsePoints('M 10 10 Q 20 15 25 35'), Math.round), 'M 10 10 C 17 13 22 22 25 35') 82 | }) 83 | test('parses t', () => { 84 | assert.deepEqual(renderPath(parsePoints('M 10 10 t 15 25'), Math.round), 'M 10 10 C 10 10 10 10 25 35') 85 | }) 86 | test('parses t + t', () => { 87 | assert.deepEqual( 88 | renderPath(parsePoints('M 10 10 t 15 25 t 25 15'), Math.round), 89 | 'M 10 10 C 10 10 10 10 25 35 35 52 43 57 50 50' 90 | ) 91 | }) 92 | test('parses t with multiple argument sets', () => { 93 | assert.deepEqual( 94 | renderPath(parsePoints('M 10 10 t 15 25 25 15'), Math.round), 95 | 'M 10 10 C 10 10 10 10 25 35 35 52 43 57 50 50' 96 | ) 97 | }) 98 | test('parses T', () => { 99 | assert.deepEqual(renderPath(parsePoints('M 10 10 T 25 35'), Math.round), 'M 10 10 C 10 10 10 10 25 35') 100 | }) 101 | test('parses T + T', () => { 102 | assert.deepEqual( 103 | renderPath(parsePoints('M 10 10 T 25 35 T 70 50'), Math.round), 104 | 'M 10 10 C 10 10 10 10 25 35 35 52 50 57 70 50' 105 | ) 106 | }) 107 | test('parses T with multiple argument sets', () => { 108 | assert.deepEqual( 109 | renderPath(parsePoints('M 10 10 T 25 35 70 50'), Math.round), 110 | 'M 10 10 C 10 10 10 10 25 35 35 52 50 57 70 50' 111 | ) 112 | }) 113 | 114 | test('parsePaths multi-segment paths', () => { 115 | const original = 'M0,0 V12 H12 V0z M16,16 V20 H20 V16z' 116 | const actual = new Path(original) 117 | // prettier-ignore 118 | const expected = [ 119 | [0, 0, 0, 0, 0, 0, 0, 12, 0, 12, 0, 12, 12, 12, 12, 12, 12, 12, 12, 0, 12, 0, 12, 0, 0, 0], 120 | [16, 16, 16, 16, 16, 16, 16, 20, 16, 20, 16, 20, 20, 20, 20, 20, 20, 20, 20, 16, 20, 16, 20, 16, 16, 16]]; 121 | assert.deepEqual(actual.getData(), expected) 122 | }) 123 | 124 | test('parses a', () => { 125 | const actual = renderPath(parsePoints('M25 25 a20 20 30 0 0 50 50'), Math.round) 126 | assert.deepEqual(actual, 'M 25 25 C 6 44 15 77 41 84 53 87 66 84 75 75') 127 | }) 128 | test('parses a with sweep flag', () => { 129 | const actual = renderPath(parsePoints('M25 25 a20 20 30 0 1 50 50'), Math.round) 130 | assert.deepEqual(actual, 'M 25 25 C 44 6 77 15 84 41 87 53 84 66 75 75') 131 | }) 132 | test('parses a with large flag', () => { 133 | const actual = renderPath(parsePoints('M0,0 a20 5 90 1 0 100 0'), Math.round) 134 | assert.deepEqual(actual, 'M 0 0 C 0 154 42 250 75 173 90 137 100 71 100 0') 135 | }) 136 | test('parses a with multiple arcs', () => { 137 | const actual = renderPath( 138 | parsePoints('M20,20 a10 10 45 1 0 0 25 10 10 45 1 0 0 25 10 10 45 1 0 0 25'), 139 | Math.round 140 | ) 141 | assert.deepEqual( 142 | actual, 143 | 'M 20 20 C 10 20 4 30 9 39 11 43 16 45 20 45 10 45 4 55 9 64 11 68 16 70 20 70 10 70 4 80 9 89 11 93 16 95 20 95' 144 | ) 145 | }) 146 | test('parses A', () => { 147 | const actual = renderPath(parsePoints('M25 25 A20 20 30 0 0 50 50'), Math.round) 148 | assert.deepEqual(actual, 'M 25 25 C 20 40 34 54 49 50 49 50 50 50 50 50') 149 | }) 150 | test('parses A with sweep flag', () => { 151 | const actual = renderPath(parsePoints('M25 25 A20 20 30 0 1 50 50'), Math.round) 152 | assert.deepEqual(actual, 'M 25 25 C 40 20 54 34 50 49 50 49 50 50 50 50') 153 | }) 154 | test('parses A with large flag', () => { 155 | const actual = renderPath(parsePoints('M25 25 A20 5 90 1 0 50 50'), Math.round) 156 | assert.deepEqual(actual, 'M 25 25 C 23 63 32 98 41 87 45 82 49 68 50 50') 157 | }) 158 | -------------------------------------------------------------------------------- /tests/operators/perimeterPoints.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { perimeterPoints } from '../../src/operators/perimeterPoints' 3 | import { parsePoints } from '../../src/operators/parsePoints' 4 | 5 | test('sums up the perimeter when going clockwise', () => { 6 | const points = parsePoints('M0,0 H20V20H0z') 7 | const clockwise = perimeterPoints(points[0]) 8 | 9 | assert.equal(clockwise, 80) 10 | }) -------------------------------------------------------------------------------- /tests/operators/renderPath.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { renderPath } from '../../src/operators/renderPath'; 3 | 4 | test('renders a segment properly', () => { 5 | // prettier-ignore 6 | const start = [[ 7 | 0, 10, 8 | 20, 20, 40, 40, 50, 50 9 | ]] 10 | assert.equal(renderPath(start, Math.round), 'M 0 10 C 20 20 40 40 50 50') 11 | }) 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": false, 4 | "allowUnusedLabels": false, 5 | "declaration": false, 6 | "forceConsistentCasingInFileNames": true, 7 | "inlineSourceMap": false, 8 | "module": "commonjs", 9 | "newLine": "LF", 10 | "noEmit": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitAny": true, 13 | "noImplicitReturns": true, 14 | "noUnusedLocals": true, 15 | "preserveConstEnums": false, 16 | "removeComments": false, 17 | "sourceMap": false, 18 | "target": "es5", 19 | "lib": [ 20 | "es2015", 21 | "dom" 22 | ] 23 | }, 24 | "exclude": [ 25 | "dist", 26 | "lib", 27 | "node_modules", 28 | "types" 29 | ] 30 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": false, 4 | "allowUnusedLabels": false, 5 | "declaration": true, 6 | "declarationDir": "types", 7 | "forceConsistentCasingInFileNames": true, 8 | "inlineSourceMap": false, 9 | "newLine": "LF", 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": false, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": true, 16 | "preserveConstEnums": false, 17 | "removeComments": true, 18 | "rootDir": "src", 19 | "sourceMap": false, 20 | "strictNullChecks": false, 21 | "target": "es5", 22 | "outDir": "lib", 23 | "lib": [ 24 | "es2015", 25 | "dom" 26 | ] 27 | }, 28 | "exclude": [ 29 | "dist", "lib", 30 | "tests", 31 | "node_modules", "types"] 32 | } 33 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | true, 5 | "parameters", 6 | "arguments", 7 | "statements" 8 | ], 9 | "ban": false, 10 | "class-name": false, 11 | "comment-format": [ 12 | true, 13 | "check-space", 14 | "check-lowercase" 15 | ], 16 | "curly": true, 17 | "eofline": true, 18 | "forin": false, 19 | "indent": [ 20 | true, 21 | "spaces" 22 | ], 23 | "interface-name": [ 24 | true, 25 | "always-prefix" 26 | ], 27 | "jsdoc-format": true, 28 | "label-position": true, 29 | "label-undefined": true, 30 | "max-line-length": [ 31 | true, 32 | 200 33 | ], 34 | "member-access": true, 35 | "member-ordering": [ 36 | true, 37 | "public-before-private", 38 | "static-before-instance", 39 | "variables-before-functions" 40 | ], 41 | "no-any": false, 42 | "no-arg": true, 43 | "no-bitwise": false, 44 | "no-conditional-assignment": true, 45 | "no-consecutive-blank-lines": false, 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-construct": true, 55 | "no-constructor-vars": true, 56 | "no-debugger": true, 57 | "no-duplicate-key": true, 58 | "no-duplicate-variable": true, 59 | "no-empty": true, 60 | "no-eval": true, 61 | "no-inferrable-types": false, 62 | "no-internal-module": false, 63 | "no-null-keyword": true, 64 | "no-require-imports": false, 65 | "no-shadowed-variable": true, 66 | "no-string-literal": false, 67 | "no-switch-case-fall-through": true, 68 | "no-trailing-whitespace": false, 69 | "no-unreachable": true, 70 | "no-unused-expression": true, 71 | "no-unused-variable": true, 72 | "no-use-before-declare": true, 73 | "no-var-keyword": true, 74 | "no-var-requires": false, 75 | "object-literal-sort-keys": false, 76 | "one-line": [ 77 | true, 78 | "check-open-brace", 79 | "check-catch", 80 | "check-else", 81 | "check-finally", 82 | "check-whitespace" 83 | ], 84 | "quotemark": [ 85 | true, 86 | "single", 87 | "avoid-escape" 88 | ], 89 | "radix": true, 90 | "semicolon": [ 91 | false, 92 | "always" 93 | ], 94 | "switch-default": true, 95 | "trailing-comma": [ 96 | true, 97 | { 98 | "multiline": "never", 99 | "singleline": "never" 100 | } 101 | ], 102 | "triple-equals": [ 103 | true, 104 | "allow-null-check" 105 | ], 106 | "typedef": [ 107 | true, 108 | "call-signature", 109 | "parameter", 110 | "arrow-parameter", 111 | "property-declaration", 112 | "member-variable-declaration" 113 | ], 114 | "typedef-whitespace": [ 115 | true, 116 | { 117 | "call-signature": "nospace", 118 | "index-signature": "nospace", 119 | "parameter": "nospace", 120 | "property-declaration": "nospace", 121 | "variable-declaration": "nospace" 122 | }, 123 | { 124 | "call-signature": "space", 125 | "index-signature": "space", 126 | "parameter": "space", 127 | "property-declaration": "space", 128 | "variable-declaration": "space" 129 | } 130 | ], 131 | "use-strict": [ 132 | true, 133 | "check-module" 134 | ], 135 | "variable-name": [ 136 | true, 137 | "check-format", 138 | "allow-leading-underscore", 139 | "ban-keywords" 140 | ], 141 | "whitespace": [ 142 | true, 143 | "check-branch", 144 | "check-decl", 145 | "check-operator", 146 | "check-separator", 147 | "check-type" 148 | ] 149 | } 150 | } --------------------------------------------------------------------------------