├── .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 | [](https://badge.fury.io/js/polymorph-js)
6 | [](https://travis-ci.org/notoriousb1t/polymorph)
7 | [](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 | Flubber (53kb) |
21 | Polymorph (6kb) |
22 |
23 |
24 |
25 |
27 | |
28 |
29 |
31 | |
32 |
33 |
34 |
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 | }
--------------------------------------------------------------------------------