1 | 'use strict';
|
---|
2 |
|
---|
3 | const { collectStylesheet, computeStyle } = require('../lib/style.js');
|
---|
4 | const { pathElems } = require('./_collections.js');
|
---|
5 | const { path2js, js2path } = require('./_path.js');
|
---|
6 | const { applyTransforms } = require('./_applyTransforms.js');
|
---|
7 | const { cleanupOutData } = require('../lib/svgo/tools');
|
---|
8 |
|
---|
9 | exports.name = 'convertPathData';
|
---|
10 | exports.type = 'visitor';
|
---|
11 | exports.active = true;
|
---|
12 | exports.description =
|
---|
13 | 'optimizes path data: writes in shorter form, applies transformations';
|
---|
14 |
|
---|
15 | exports.params = {
|
---|
16 | applyTransforms: true,
|
---|
17 | applyTransformsStroked: true,
|
---|
18 | makeArcs: {
|
---|
19 | threshold: 2.5, // coefficient of rounding error
|
---|
20 | tolerance: 0.5, // percentage of radius
|
---|
21 | },
|
---|
22 | straightCurves: true,
|
---|
23 | lineShorthands: true,
|
---|
24 | curveSmoothShorthands: true,
|
---|
25 | floatPrecision: 3,
|
---|
26 | transformPrecision: 5,
|
---|
27 | removeUseless: true,
|
---|
28 | collapseRepeated: true,
|
---|
29 | utilizeAbsolute: true,
|
---|
30 | leadingZero: true,
|
---|
31 | negativeExtraSpace: true,
|
---|
32 | noSpaceAfterFlags: false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20
|
---|
33 | forceAbsolutePath: false,
|
---|
34 | };
|
---|
35 |
|
---|
36 | let roundData;
|
---|
37 | let precision;
|
---|
38 | let error;
|
---|
39 | let arcThreshold;
|
---|
40 | let arcTolerance;
|
---|
41 |
|
---|
42 | /**
|
---|
43 | * Convert absolute Path to relative,
|
---|
44 | * collapse repeated instructions,
|
---|
45 | * detect and convert Lineto shorthands,
|
---|
46 | * remove useless instructions like "l0,0",
|
---|
47 | * trim useless delimiters and leading zeros,
|
---|
48 | * decrease accuracy of floating-point numbers.
|
---|
49 | *
|
---|
50 | * @see https://www.w3.org/TR/SVG11/paths.html#PathData
|
---|
51 | *
|
---|
52 | * @param {Object} item current iteration item
|
---|
53 | * @param {Object} params plugin params
|
---|
54 | * @return {Boolean} if false, item will be filtered out
|
---|
55 | *
|
---|
56 | * @author Kir Belevich
|
---|
57 | */
|
---|
58 | exports.fn = (root, params) => {
|
---|
59 | const stylesheet = collectStylesheet(root);
|
---|
60 | return {
|
---|
61 | element: {
|
---|
62 | enter: (node) => {
|
---|
63 | if (pathElems.includes(node.name) && node.attributes.d != null) {
|
---|
64 | const computedStyle = computeStyle(stylesheet, node);
|
---|
65 | precision = params.floatPrecision;
|
---|
66 | error =
|
---|
67 | precision !== false
|
---|
68 | ? +Math.pow(0.1, precision).toFixed(precision)
|
---|
69 | : 1e-2;
|
---|
70 | roundData = precision > 0 && precision < 20 ? strongRound : round;
|
---|
71 | if (params.makeArcs) {
|
---|
72 | arcThreshold = params.makeArcs.threshold;
|
---|
73 | arcTolerance = params.makeArcs.tolerance;
|
---|
74 | }
|
---|
75 | const hasMarkerMid = computedStyle['marker-mid'] != null;
|
---|
76 |
|
---|
77 | const maybeHasStroke =
|
---|
78 | computedStyle.stroke &&
|
---|
79 | (computedStyle.stroke.type === 'dynamic' ||
|
---|
80 | computedStyle.stroke.value !== 'none');
|
---|
81 | const maybeHasLinecap =
|
---|
82 | computedStyle['stroke-linecap'] &&
|
---|
83 | (computedStyle['stroke-linecap'].type === 'dynamic' ||
|
---|
84 | computedStyle['stroke-linecap'].value !== 'butt');
|
---|
85 | const maybeHasStrokeAndLinecap = maybeHasStroke && maybeHasLinecap;
|
---|
86 |
|
---|
87 | var data = path2js(node);
|
---|
88 |
|
---|
89 | // TODO: get rid of functions returns
|
---|
90 | if (data.length) {
|
---|
91 | if (params.applyTransforms) {
|
---|
92 | applyTransforms(node, data, params);
|
---|
93 | }
|
---|
94 |
|
---|
95 | convertToRelative(data);
|
---|
96 |
|
---|
97 | data = filters(data, params, {
|
---|
98 | maybeHasStrokeAndLinecap,
|
---|
99 | hasMarkerMid,
|
---|
100 | });
|
---|
101 |
|
---|
102 | if (params.utilizeAbsolute) {
|
---|
103 | data = convertToMixed(data, params);
|
---|
104 | }
|
---|
105 |
|
---|
106 | js2path(node, data, params);
|
---|
107 | }
|
---|
108 | }
|
---|
109 | },
|
---|
110 | },
|
---|
111 | };
|
---|
112 | };
|
---|
113 |
|
---|
114 | /**
|
---|
115 | * Convert absolute path data coordinates to relative.
|
---|
116 | *
|
---|
117 | * @param {Array} path input path data
|
---|
118 | * @param {Object} params plugin params
|
---|
119 | * @return {Array} output path data
|
---|
120 | */
|
---|
121 | const convertToRelative = (pathData) => {
|
---|
122 | let start = [0, 0];
|
---|
123 | let cursor = [0, 0];
|
---|
124 | let prevCoords = [0, 0];
|
---|
125 |
|
---|
126 | for (let i = 0; i < pathData.length; i += 1) {
|
---|
127 | const pathItem = pathData[i];
|
---|
128 | let { command, args } = pathItem;
|
---|
129 |
|
---|
130 | // moveto (x y)
|
---|
131 | if (command === 'm') {
|
---|
132 | // update start and cursor
|
---|
133 | cursor[0] += args[0];
|
---|
134 | cursor[1] += args[1];
|
---|
135 | start[0] = cursor[0];
|
---|
136 | start[1] = cursor[1];
|
---|
137 | }
|
---|
138 | if (command === 'M') {
|
---|
139 | // M → m
|
---|
140 | // skip first moveto
|
---|
141 | if (i !== 0) {
|
---|
142 | command = 'm';
|
---|
143 | }
|
---|
144 | args[0] -= cursor[0];
|
---|
145 | args[1] -= cursor[1];
|
---|
146 | // update start and cursor
|
---|
147 | cursor[0] += args[0];
|
---|
148 | cursor[1] += args[1];
|
---|
149 | start[0] = cursor[0];
|
---|
150 | start[1] = cursor[1];
|
---|
151 | }
|
---|
152 |
|
---|
153 | // lineto (x y)
|
---|
154 | if (command === 'l') {
|
---|
155 | cursor[0] += args[0];
|
---|
156 | cursor[1] += args[1];
|
---|
157 | }
|
---|
158 | if (command === 'L') {
|
---|
159 | // L → l
|
---|
160 | command = 'l';
|
---|
161 | args[0] -= cursor[0];
|
---|
162 | args[1] -= cursor[1];
|
---|
163 | cursor[0] += args[0];
|
---|
164 | cursor[1] += args[1];
|
---|
165 | }
|
---|
166 |
|
---|
167 | // horizontal lineto (x)
|
---|
168 | if (command === 'h') {
|
---|
169 | cursor[0] += args[0];
|
---|
170 | }
|
---|
171 | if (command === 'H') {
|
---|
172 | // H → h
|
---|
173 | command = 'h';
|
---|
174 | args[0] -= cursor[0];
|
---|
175 | cursor[0] += args[0];
|
---|
176 | }
|
---|
177 |
|
---|
178 | // vertical lineto (y)
|
---|
179 | if (command === 'v') {
|
---|
180 | cursor[1] += args[0];
|
---|
181 | }
|
---|
182 | if (command === 'V') {
|
---|
183 | // V → v
|
---|
184 | command = 'v';
|
---|
185 | args[0] -= cursor[1];
|
---|
186 | cursor[1] += args[0];
|
---|
187 | }
|
---|
188 |
|
---|
189 | // curveto (x1 y1 x2 y2 x y)
|
---|
190 | if (command === 'c') {
|
---|
191 | cursor[0] += args[4];
|
---|
192 | cursor[1] += args[5];
|
---|
193 | }
|
---|
194 | if (command === 'C') {
|
---|
195 | // C → c
|
---|
196 | command = 'c';
|
---|
197 | args[0] -= cursor[0];
|
---|
198 | args[1] -= cursor[1];
|
---|
199 | args[2] -= cursor[0];
|
---|
200 | args[3] -= cursor[1];
|
---|
201 | args[4] -= cursor[0];
|
---|
202 | args[5] -= cursor[1];
|
---|
203 | cursor[0] += args[4];
|
---|
204 | cursor[1] += args[5];
|
---|
205 | }
|
---|
206 |
|
---|
207 | // smooth curveto (x2 y2 x y)
|
---|
208 | if (command === 's') {
|
---|
209 | cursor[0] += args[2];
|
---|
210 | cursor[1] += args[3];
|
---|
211 | }
|
---|
212 | if (command === 'S') {
|
---|
213 | // S → s
|
---|
214 | command = 's';
|
---|
215 | args[0] -= cursor[0];
|
---|
216 | args[1] -= cursor[1];
|
---|
217 | args[2] -= cursor[0];
|
---|
218 | args[3] -= cursor[1];
|
---|
219 | cursor[0] += args[2];
|
---|
220 | cursor[1] += args[3];
|
---|
221 | }
|
---|
222 |
|
---|
223 | // quadratic Bézier curveto (x1 y1 x y)
|
---|
224 | if (command === 'q') {
|
---|
225 | cursor[0] += args[2];
|
---|
226 | cursor[1] += args[3];
|
---|
227 | }
|
---|
228 | if (command === 'Q') {
|
---|
229 | // Q → q
|
---|
230 | command = 'q';
|
---|
231 | args[0] -= cursor[0];
|
---|
232 | args[1] -= cursor[1];
|
---|
233 | args[2] -= cursor[0];
|
---|
234 | args[3] -= cursor[1];
|
---|
235 | cursor[0] += args[2];
|
---|
236 | cursor[1] += args[3];
|
---|
237 | }
|
---|
238 |
|
---|
239 | // smooth quadratic Bézier curveto (x y)
|
---|
240 | if (command === 't') {
|
---|
241 | cursor[0] += args[0];
|
---|
242 | cursor[1] += args[1];
|
---|
243 | }
|
---|
244 | if (command === 'T') {
|
---|
245 | // T → t
|
---|
246 | command = 't';
|
---|
247 | args[0] -= cursor[0];
|
---|
248 | args[1] -= cursor[1];
|
---|
249 | cursor[0] += args[0];
|
---|
250 | cursor[1] += args[1];
|
---|
251 | }
|
---|
252 |
|
---|
253 | // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
|
---|
254 | if (command === 'a') {
|
---|
255 | cursor[0] += args[5];
|
---|
256 | cursor[1] += args[6];
|
---|
257 | }
|
---|
258 | if (command === 'A') {
|
---|
259 | // A → a
|
---|
260 | command = 'a';
|
---|
261 | args[5] -= cursor[0];
|
---|
262 | args[6] -= cursor[1];
|
---|
263 | cursor[0] += args[5];
|
---|
264 | cursor[1] += args[6];
|
---|
265 | }
|
---|
266 |
|
---|
267 | // closepath
|
---|
268 | if (command === 'Z' || command === 'z') {
|
---|
269 | // reset cursor
|
---|
270 | cursor[0] = start[0];
|
---|
271 | cursor[1] = start[1];
|
---|
272 | }
|
---|
273 |
|
---|
274 | pathItem.command = command;
|
---|
275 | pathItem.args = args;
|
---|
276 | // store absolute coordinates for later use
|
---|
277 | // base should preserve reference from other element
|
---|
278 | pathItem.base = prevCoords;
|
---|
279 | pathItem.coords = [cursor[0], cursor[1]];
|
---|
280 | prevCoords = pathItem.coords;
|
---|
281 | }
|
---|
282 |
|
---|
283 | return pathData;
|
---|
284 | };
|
---|
285 |
|
---|
286 | /**
|
---|
287 | * Main filters loop.
|
---|
288 | *
|
---|
289 | * @param {Array} path input path data
|
---|
290 | * @param {Object} params plugin params
|
---|
291 | * @return {Array} output path data
|
---|
292 | */
|
---|
293 | function filters(path, params, { maybeHasStrokeAndLinecap, hasMarkerMid }) {
|
---|
294 | var stringify = data2Path.bind(null, params),
|
---|
295 | relSubpoint = [0, 0],
|
---|
296 | pathBase = [0, 0],
|
---|
297 | prev = {};
|
---|
298 |
|
---|
299 | path = path.filter(function (item, index, path) {
|
---|
300 | let command = item.command;
|
---|
301 | let data = item.args;
|
---|
302 | let next = path[index + 1];
|
---|
303 |
|
---|
304 | if (command !== 'Z' && command !== 'z') {
|
---|
305 | var sdata = data,
|
---|
306 | circle;
|
---|
307 |
|
---|
308 | if (command === 's') {
|
---|
309 | sdata = [0, 0].concat(data);
|
---|
310 |
|
---|
311 | if (command === 'c' || command === 's') {
|
---|
312 | var pdata = prev.args,
|
---|
313 | n = pdata.length;
|
---|
314 |
|
---|
315 | // (-x, -y) of the prev tangent point relative to the current point
|
---|
316 | sdata[0] = pdata[n - 2] - pdata[n - 4];
|
---|
317 | sdata[1] = pdata[n - 1] - pdata[n - 3];
|
---|
318 | }
|
---|
319 | }
|
---|
320 |
|
---|
321 | // convert curves to arcs if possible
|
---|
322 | if (
|
---|
323 | params.makeArcs &&
|
---|
324 | (command == 'c' || command == 's') &&
|
---|
325 | isConvex(sdata) &&
|
---|
326 | (circle = findCircle(sdata))
|
---|
327 | ) {
|
---|
328 | var r = roundData([circle.radius])[0],
|
---|
329 | angle = findArcAngle(sdata, circle),
|
---|
330 | sweep = sdata[5] * sdata[0] - sdata[4] * sdata[1] > 0 ? 1 : 0,
|
---|
331 | arc = {
|
---|
332 | command: 'a',
|
---|
333 | args: [r, r, 0, 0, sweep, sdata[4], sdata[5]],
|
---|
334 | coords: item.coords.slice(),
|
---|
335 | base: item.base,
|
---|
336 | },
|
---|
337 | output = [arc],
|
---|
338 | // relative coordinates to adjust the found circle
|
---|
339 | relCenter = [
|
---|
340 | circle.center[0] - sdata[4],
|
---|
341 | circle.center[1] - sdata[5],
|
---|
342 | ],
|
---|
343 | relCircle = { center: relCenter, radius: circle.radius },
|
---|
344 | arcCurves = [item],
|
---|
345 | hasPrev = 0,
|
---|
346 | suffix = '',
|
---|
347 | nextLonghand;
|
---|
348 |
|
---|
349 | if (
|
---|
350 | (prev.command == 'c' &&
|
---|
351 | isConvex(prev.args) &&
|
---|
352 | isArcPrev(prev.args, circle)) ||
|
---|
353 | (prev.command == 'a' && prev.sdata && isArcPrev(prev.sdata, circle))
|
---|
354 | ) {
|
---|
355 | arcCurves.unshift(prev);
|
---|
356 | arc.base = prev.base;
|
---|
357 | arc.args[5] = arc.coords[0] - arc.base[0];
|
---|
358 | arc.args[6] = arc.coords[1] - arc.base[1];
|
---|
359 | var prevData = prev.command == 'a' ? prev.sdata : prev.args;
|
---|
360 | var prevAngle = findArcAngle(prevData, {
|
---|
361 | center: [
|
---|
362 | prevData[4] + circle.center[0],
|
---|
363 | prevData[5] + circle.center[1],
|
---|
364 | ],
|
---|
365 | radius: circle.radius,
|
---|
366 | });
|
---|
367 | angle += prevAngle;
|
---|
368 | if (angle > Math.PI) arc.args[3] = 1;
|
---|
369 | hasPrev = 1;
|
---|
370 | }
|
---|
371 |
|
---|
372 | // check if next curves are fitting the arc
|
---|
373 | for (
|
---|
374 | var j = index;
|
---|
375 | (next = path[++j]) && ~'cs'.indexOf(next.command);
|
---|
376 |
|
---|
377 | ) {
|
---|
378 | var nextData = next.args;
|
---|
379 | if (next.command == 's') {
|
---|
380 | nextLonghand = makeLonghand(
|
---|
381 | { command: 's', args: next.args.slice() },
|
---|
382 | path[j - 1].args
|
---|
383 | );
|
---|
384 | nextData = nextLonghand.args;
|
---|
385 | nextLonghand.args = nextData.slice(0, 2);
|
---|
386 | suffix = stringify([nextLonghand]);
|
---|
387 | }
|
---|
388 | if (isConvex(nextData) && isArc(nextData, relCircle)) {
|
---|
389 | angle += findArcAngle(nextData, relCircle);
|
---|
390 | if (angle - 2 * Math.PI > 1e-3) break; // more than 360°
|
---|
391 | if (angle > Math.PI) arc.args[3] = 1;
|
---|
392 | arcCurves.push(next);
|
---|
393 | if (2 * Math.PI - angle > 1e-3) {
|
---|
394 | // less than 360°
|
---|
395 | arc.coords = next.coords;
|
---|
396 | arc.args[5] = arc.coords[0] - arc.base[0];
|
---|
397 | arc.args[6] = arc.coords[1] - arc.base[1];
|
---|
398 | } else {
|
---|
399 | // full circle, make a half-circle arc and add a second one
|
---|
400 | arc.args[5] = 2 * (relCircle.center[0] - nextData[4]);
|
---|
401 | arc.args[6] = 2 * (relCircle.center[1] - nextData[5]);
|
---|
402 | arc.coords = [
|
---|
403 | arc.base[0] + arc.args[5],
|
---|
404 | arc.base[1] + arc.args[6],
|
---|
405 | ];
|
---|
406 | arc = {
|
---|
407 | command: 'a',
|
---|
408 | args: [
|
---|
409 | r,
|
---|
410 | r,
|
---|
411 | 0,
|
---|
412 | 0,
|
---|
413 | sweep,
|
---|
414 | next.coords[0] - arc.coords[0],
|
---|
415 | next.coords[1] - arc.coords[1],
|
---|
416 | ],
|
---|
417 | coords: next.coords,
|
---|
418 | base: arc.coords,
|
---|
419 | };
|
---|
420 | output.push(arc);
|
---|
421 | j++;
|
---|
422 | break;
|
---|
423 | }
|
---|
424 | relCenter[0] -= nextData[4];
|
---|
425 | relCenter[1] -= nextData[5];
|
---|
426 | } else break;
|
---|
427 | }
|
---|
428 |
|
---|
429 | if ((stringify(output) + suffix).length < stringify(arcCurves).length) {
|
---|
430 | if (path[j] && path[j].command == 's') {
|
---|
431 | makeLonghand(path[j], path[j - 1].args);
|
---|
432 | }
|
---|
433 | if (hasPrev) {
|
---|
434 | var prevArc = output.shift();
|
---|
435 | roundData(prevArc.args);
|
---|
436 | relSubpoint[0] += prevArc.args[5] - prev.args[prev.args.length - 2];
|
---|
437 | relSubpoint[1] += prevArc.args[6] - prev.args[prev.args.length - 1];
|
---|
438 | prev.command = 'a';
|
---|
439 | prev.args = prevArc.args;
|
---|
440 | item.base = prev.coords = prevArc.coords;
|
---|
441 | }
|
---|
442 | arc = output.shift();
|
---|
443 | if (arcCurves.length == 1) {
|
---|
444 | item.sdata = sdata.slice(); // preserve curve data for future checks
|
---|
445 | } else if (arcCurves.length - 1 - hasPrev > 0) {
|
---|
446 | // filter out consumed next items
|
---|
447 | path.splice.apply(
|
---|
448 | path,
|
---|
449 | [index + 1, arcCurves.length - 1 - hasPrev].concat(output)
|
---|
450 | );
|
---|
451 | }
|
---|
452 | if (!arc) return false;
|
---|
453 | command = 'a';
|
---|
454 | data = arc.args;
|
---|
455 | item.coords = arc.coords;
|
---|
456 | }
|
---|
457 | }
|
---|
458 |
|
---|
459 | // Rounding relative coordinates, taking in account accummulating error
|
---|
460 | // to get closer to absolute coordinates. Sum of rounded value remains same:
|
---|
461 | // l .25 3 .25 2 .25 3 .25 2 -> l .3 3 .2 2 .3 3 .2 2
|
---|
462 | if (precision !== false) {
|
---|
463 | if (
|
---|
464 | command === 'm' ||
|
---|
465 | command === 'l' ||
|
---|
466 | command === 't' ||
|
---|
467 | command === 'q' ||
|
---|
468 | command === 's' ||
|
---|
469 | command === 'c'
|
---|
470 | ) {
|
---|
471 | for (var i = data.length; i--; ) {
|
---|
472 | data[i] += item.base[i % 2] - relSubpoint[i % 2];
|
---|
473 | }
|
---|
474 | } else if (command == 'h') {
|
---|
475 | data[0] += item.base[0] - relSubpoint[0];
|
---|
476 | } else if (command == 'v') {
|
---|
477 | data[0] += item.base[1] - relSubpoint[1];
|
---|
478 | } else if (command == 'a') {
|
---|
479 | data[5] += item.base[0] - relSubpoint[0];
|
---|
480 | data[6] += item.base[1] - relSubpoint[1];
|
---|
481 | }
|
---|
482 | roundData(data);
|
---|
483 |
|
---|
484 | if (command == 'h') relSubpoint[0] += data[0];
|
---|
485 | else if (command == 'v') relSubpoint[1] += data[0];
|
---|
486 | else {
|
---|
487 | relSubpoint[0] += data[data.length - 2];
|
---|
488 | relSubpoint[1] += data[data.length - 1];
|
---|
489 | }
|
---|
490 | roundData(relSubpoint);
|
---|
491 |
|
---|
492 | if (command === 'M' || command === 'm') {
|
---|
493 | pathBase[0] = relSubpoint[0];
|
---|
494 | pathBase[1] = relSubpoint[1];
|
---|
495 | }
|
---|
496 | }
|
---|
497 |
|
---|
498 | // convert straight curves into lines segments
|
---|
499 | if (params.straightCurves) {
|
---|
500 | if (
|
---|
501 | (command === 'c' && isCurveStraightLine(data)) ||
|
---|
502 | (command === 's' && isCurveStraightLine(sdata))
|
---|
503 | ) {
|
---|
504 | if (next && next.command == 's') makeLonghand(next, data); // fix up next curve
|
---|
505 | command = 'l';
|
---|
506 | data = data.slice(-2);
|
---|
507 | } else if (command === 'q' && isCurveStraightLine(data)) {
|
---|
508 | if (next && next.command == 't') makeLonghand(next, data); // fix up next curve
|
---|
509 | command = 'l';
|
---|
510 | data = data.slice(-2);
|
---|
511 | } else if (
|
---|
512 | command === 't' &&
|
---|
513 | prev.command !== 'q' &&
|
---|
514 | prev.command !== 't'
|
---|
515 | ) {
|
---|
516 | command = 'l';
|
---|
517 | data = data.slice(-2);
|
---|
518 | } else if (command === 'a' && (data[0] === 0 || data[1] === 0)) {
|
---|
519 | command = 'l';
|
---|
520 | data = data.slice(-2);
|
---|
521 | }
|
---|
522 | }
|
---|
523 |
|
---|
524 | // horizontal and vertical line shorthands
|
---|
525 | // l 50 0 → h 50
|
---|
526 | // l 0 50 → v 50
|
---|
527 | if (params.lineShorthands && command === 'l') {
|
---|
528 | if (data[1] === 0) {
|
---|
529 | command = 'h';
|
---|
530 | data.pop();
|
---|
531 | } else if (data[0] === 0) {
|
---|
532 | command = 'v';
|
---|
533 | data.shift();
|
---|
534 | }
|
---|
535 | }
|
---|
536 |
|
---|
537 | // collapse repeated commands
|
---|
538 | // h 20 h 30 -> h 50
|
---|
539 | if (
|
---|
540 | params.collapseRepeated &&
|
---|
541 | hasMarkerMid === false &&
|
---|
542 | (command === 'm' || command === 'h' || command === 'v') &&
|
---|
543 | prev.command &&
|
---|
544 | command == prev.command.toLowerCase() &&
|
---|
545 | ((command != 'h' && command != 'v') ||
|
---|
546 | prev.args[0] >= 0 == data[0] >= 0)
|
---|
547 | ) {
|
---|
548 | prev.args[0] += data[0];
|
---|
549 | if (command != 'h' && command != 'v') {
|
---|
550 | prev.args[1] += data[1];
|
---|
551 | }
|
---|
552 | prev.coords = item.coords;
|
---|
553 | path[index] = prev;
|
---|
554 | return false;
|
---|
555 | }
|
---|
556 |
|
---|
557 | // convert curves into smooth shorthands
|
---|
558 | if (params.curveSmoothShorthands && prev.command) {
|
---|
559 | // curveto
|
---|
560 | if (command === 'c') {
|
---|
561 | // c + c → c + s
|
---|
562 | if (
|
---|
563 | prev.command === 'c' &&
|
---|
564 | data[0] === -(prev.args[2] - prev.args[4]) &&
|
---|
565 | data[1] === -(prev.args[3] - prev.args[5])
|
---|
566 | ) {
|
---|
567 | command = 's';
|
---|
568 | data = data.slice(2);
|
---|
569 | }
|
---|
570 |
|
---|
571 | // s + c → s + s
|
---|
572 | else if (
|
---|
573 | prev.command === 's' &&
|
---|
574 | data[0] === -(prev.args[0] - prev.args[2]) &&
|
---|
575 | data[1] === -(prev.args[1] - prev.args[3])
|
---|
576 | ) {
|
---|
577 | command = 's';
|
---|
578 | data = data.slice(2);
|
---|
579 | }
|
---|
580 |
|
---|
581 | // [^cs] + c → [^cs] + s
|
---|
582 | else if (
|
---|
583 | prev.command !== 'c' &&
|
---|
584 | prev.command !== 's' &&
|
---|
585 | data[0] === 0 &&
|
---|
586 | data[1] === 0
|
---|
587 | ) {
|
---|
588 | command = 's';
|
---|
589 | data = data.slice(2);
|
---|
590 | }
|
---|
591 | }
|
---|
592 |
|
---|
593 | // quadratic Bézier curveto
|
---|
594 | else if (command === 'q') {
|
---|
595 | // q + q → q + t
|
---|
596 | if (
|
---|
597 | prev.command === 'q' &&
|
---|
598 | data[0] === prev.args[2] - prev.args[0] &&
|
---|
599 | data[1] === prev.args[3] - prev.args[1]
|
---|
600 | ) {
|
---|
601 | command = 't';
|
---|
602 | data = data.slice(2);
|
---|
603 | }
|
---|
604 |
|
---|
605 | // t + q → t + t
|
---|
606 | else if (
|
---|
607 | prev.command === 't' &&
|
---|
608 | data[2] === prev.args[0] &&
|
---|
609 | data[3] === prev.args[1]
|
---|
610 | ) {
|
---|
611 | command = 't';
|
---|
612 | data = data.slice(2);
|
---|
613 | }
|
---|
614 | }
|
---|
615 | }
|
---|
616 |
|
---|
617 | // remove useless non-first path segments
|
---|
618 | if (params.removeUseless && !maybeHasStrokeAndLinecap) {
|
---|
619 | // l 0,0 / h 0 / v 0 / q 0,0 0,0 / t 0,0 / c 0,0 0,0 0,0 / s 0,0 0,0
|
---|
620 | if (
|
---|
621 | (command === 'l' ||
|
---|
622 | command === 'h' ||
|
---|
623 | command === 'v' ||
|
---|
624 | command === 'q' ||
|
---|
625 | command === 't' ||
|
---|
626 | command === 'c' ||
|
---|
627 | command === 's') &&
|
---|
628 | data.every(function (i) {
|
---|
629 | return i === 0;
|
---|
630 | })
|
---|
631 | ) {
|
---|
632 | path[index] = prev;
|
---|
633 | return false;
|
---|
634 | }
|
---|
635 |
|
---|
636 | // a 25,25 -30 0,1 0,0
|
---|
637 | if (command === 'a' && data[5] === 0 && data[6] === 0) {
|
---|
638 | path[index] = prev;
|
---|
639 | return false;
|
---|
640 | }
|
---|
641 | }
|
---|
642 |
|
---|
643 | item.command = command;
|
---|
644 | item.args = data;
|
---|
645 |
|
---|
646 | prev = item;
|
---|
647 | } else {
|
---|
648 | // z resets coordinates
|
---|
649 | relSubpoint[0] = pathBase[0];
|
---|
650 | relSubpoint[1] = pathBase[1];
|
---|
651 | if (prev.command === 'Z' || prev.command === 'z') return false;
|
---|
652 | prev = item;
|
---|
653 | }
|
---|
654 |
|
---|
655 | return true;
|
---|
656 | });
|
---|
657 |
|
---|
658 | return path;
|
---|
659 | }
|
---|
660 |
|
---|
661 | /**
|
---|
662 | * Writes data in shortest form using absolute or relative coordinates.
|
---|
663 | *
|
---|
664 | * @param {Array} data input path data
|
---|
665 | * @return {Boolean} output
|
---|
666 | */
|
---|
667 | function convertToMixed(path, params) {
|
---|
668 | var prev = path[0];
|
---|
669 |
|
---|
670 | path = path.filter(function (item, index) {
|
---|
671 | if (index == 0) return true;
|
---|
672 | if (item.command === 'Z' || item.command === 'z') {
|
---|
673 | prev = item;
|
---|
674 | return true;
|
---|
675 | }
|
---|
676 |
|
---|
677 | var command = item.command,
|
---|
678 | data = item.args,
|
---|
679 | adata = data.slice();
|
---|
680 |
|
---|
681 | if (
|
---|
682 | command === 'm' ||
|
---|
683 | command === 'l' ||
|
---|
684 | command === 't' ||
|
---|
685 | command === 'q' ||
|
---|
686 | command === 's' ||
|
---|
687 | command === 'c'
|
---|
688 | ) {
|
---|
689 | for (var i = adata.length; i--; ) {
|
---|
690 | adata[i] += item.base[i % 2];
|
---|
691 | }
|
---|
692 | } else if (command == 'h') {
|
---|
693 | adata[0] += item.base[0];
|
---|
694 | } else if (command == 'v') {
|
---|
695 | adata[0] += item.base[1];
|
---|
696 | } else if (command == 'a') {
|
---|
697 | adata[5] += item.base[0];
|
---|
698 | adata[6] += item.base[1];
|
---|
699 | }
|
---|
700 |
|
---|
701 | roundData(adata);
|
---|
702 |
|
---|
703 | var absoluteDataStr = cleanupOutData(adata, params),
|
---|
704 | relativeDataStr = cleanupOutData(data, params);
|
---|
705 |
|
---|
706 | // Convert to absolute coordinates if it's shorter or forceAbsolutePath is true.
|
---|
707 | // v-20 -> V0
|
---|
708 | // Don't convert if it fits following previous command.
|
---|
709 | // l20 30-10-50 instead of l20 30L20 30
|
---|
710 | if (
|
---|
711 | params.forceAbsolutePath ||
|
---|
712 | (absoluteDataStr.length < relativeDataStr.length &&
|
---|
713 | !(
|
---|
714 | params.negativeExtraSpace &&
|
---|
715 | command == prev.command &&
|
---|
716 | prev.command.charCodeAt(0) > 96 &&
|
---|
717 | absoluteDataStr.length == relativeDataStr.length - 1 &&
|
---|
718 | (data[0] < 0 ||
|
---|
719 | (/^0\./.test(data[0]) && prev.args[prev.args.length - 1] % 1))
|
---|
720 | ))
|
---|
721 | ) {
|
---|
722 | item.command = command.toUpperCase();
|
---|
723 | item.args = adata;
|
---|
724 | }
|
---|
725 |
|
---|
726 | prev = item;
|
---|
727 |
|
---|
728 | return true;
|
---|
729 | });
|
---|
730 |
|
---|
731 | return path;
|
---|
732 | }
|
---|
733 |
|
---|
734 | /**
|
---|
735 | * Checks if curve is convex. Control points of such a curve must form
|
---|
736 | * a convex quadrilateral with diagonals crosspoint inside of it.
|
---|
737 | *
|
---|
738 | * @param {Array} data input path data
|
---|
739 | * @return {Boolean} output
|
---|
740 | */
|
---|
741 | function isConvex(data) {
|
---|
742 | var center = getIntersection([
|
---|
743 | 0,
|
---|
744 | 0,
|
---|
745 | data[2],
|
---|
746 | data[3],
|
---|
747 | data[0],
|
---|
748 | data[1],
|
---|
749 | data[4],
|
---|
750 | data[5],
|
---|
751 | ]);
|
---|
752 |
|
---|
753 | return (
|
---|
754 | center &&
|
---|
755 | data[2] < center[0] == center[0] < 0 &&
|
---|
756 | data[3] < center[1] == center[1] < 0 &&
|
---|
757 | data[4] < center[0] == center[0] < data[0] &&
|
---|
758 | data[5] < center[1] == center[1] < data[1]
|
---|
759 | );
|
---|
760 | }
|
---|
761 |
|
---|
762 | /**
|
---|
763 | * Computes lines equations by two points and returns their intersection point.
|
---|
764 | *
|
---|
765 | * @param {Array} coords 8 numbers for 4 pairs of coordinates (x,y)
|
---|
766 | * @return {Array|undefined} output coordinate of lines' crosspoint
|
---|
767 | */
|
---|
768 | function getIntersection(coords) {
|
---|
769 | // Prev line equation parameters.
|
---|
770 | var a1 = coords[1] - coords[3], // y1 - y2
|
---|
771 | b1 = coords[2] - coords[0], // x2 - x1
|
---|
772 | c1 = coords[0] * coords[3] - coords[2] * coords[1], // x1 * y2 - x2 * y1
|
---|
773 | // Next line equation parameters
|
---|
774 | a2 = coords[5] - coords[7], // y1 - y2
|
---|
775 | b2 = coords[6] - coords[4], // x2 - x1
|
---|
776 | c2 = coords[4] * coords[7] - coords[5] * coords[6], // x1 * y2 - x2 * y1
|
---|
777 | denom = a1 * b2 - a2 * b1;
|
---|
778 |
|
---|
779 | if (!denom) return; // parallel lines havn't an intersection
|
---|
780 |
|
---|
781 | var cross = [(b1 * c2 - b2 * c1) / denom, (a1 * c2 - a2 * c1) / -denom];
|
---|
782 | if (
|
---|
783 | !isNaN(cross[0]) &&
|
---|
784 | !isNaN(cross[1]) &&
|
---|
785 | isFinite(cross[0]) &&
|
---|
786 | isFinite(cross[1])
|
---|
787 | ) {
|
---|
788 | return cross;
|
---|
789 | }
|
---|
790 | }
|
---|
791 |
|
---|
792 | /**
|
---|
793 | * Decrease accuracy of floating-point numbers
|
---|
794 | * in path data keeping a specified number of decimals.
|
---|
795 | * Smart rounds values like 2.3491 to 2.35 instead of 2.349.
|
---|
796 | * Doesn't apply "smartness" if the number precision fits already.
|
---|
797 | *
|
---|
798 | * @param {Array} data input data array
|
---|
799 | * @return {Array} output data array
|
---|
800 | */
|
---|
801 | function strongRound(data) {
|
---|
802 | for (var i = data.length; i-- > 0; ) {
|
---|
803 | if (data[i].toFixed(precision) != data[i]) {
|
---|
804 | var rounded = +data[i].toFixed(precision - 1);
|
---|
805 | data[i] =
|
---|
806 | +Math.abs(rounded - data[i]).toFixed(precision + 1) >= error
|
---|
807 | ? +data[i].toFixed(precision)
|
---|
808 | : rounded;
|
---|
809 | }
|
---|
810 | }
|
---|
811 | return data;
|
---|
812 | }
|
---|
813 |
|
---|
814 | /**
|
---|
815 | * Simple rounding function if precision is 0.
|
---|
816 | *
|
---|
817 | * @param {Array} data input data array
|
---|
818 | * @return {Array} output data array
|
---|
819 | */
|
---|
820 | function round(data) {
|
---|
821 | for (var i = data.length; i-- > 0; ) {
|
---|
822 | data[i] = Math.round(data[i]);
|
---|
823 | }
|
---|
824 | return data;
|
---|
825 | }
|
---|
826 |
|
---|
827 | /**
|
---|
828 | * Checks if a curve is a straight line by measuring distance
|
---|
829 | * from middle points to the line formed by end points.
|
---|
830 | *
|
---|
831 | * @param {Array} xs array of curve points x-coordinates
|
---|
832 | * @param {Array} ys array of curve points y-coordinates
|
---|
833 | * @return {Boolean}
|
---|
834 | */
|
---|
835 |
|
---|
836 | function isCurveStraightLine(data) {
|
---|
837 | // Get line equation a·x + b·y + c = 0 coefficients a, b (c = 0) by start and end points.
|
---|
838 | var i = data.length - 2,
|
---|
839 | a = -data[i + 1], // y1 − y2 (y1 = 0)
|
---|
840 | b = data[i], // x2 − x1 (x1 = 0)
|
---|
841 | d = 1 / (a * a + b * b); // same part for all points
|
---|
842 |
|
---|
843 | if (i <= 1 || !isFinite(d)) return false; // curve that ends at start point isn't the case
|
---|
844 |
|
---|
845 | // Distance from point (x0, y0) to the line is sqrt((c − a·x0 − b·y0)² / (a² + b²))
|
---|
846 | while ((i -= 2) >= 0) {
|
---|
847 | if (Math.sqrt(Math.pow(a * data[i] + b * data[i + 1], 2) * d) > error)
|
---|
848 | return false;
|
---|
849 | }
|
---|
850 |
|
---|
851 | return true;
|
---|
852 | }
|
---|
853 |
|
---|
854 | /**
|
---|
855 | * Converts next curve from shorthand to full form using the current curve data.
|
---|
856 | *
|
---|
857 | * @param {Object} item curve to convert
|
---|
858 | * @param {Array} data current curve data
|
---|
859 | */
|
---|
860 |
|
---|
861 | function makeLonghand(item, data) {
|
---|
862 | switch (item.command) {
|
---|
863 | case 's':
|
---|
864 | item.command = 'c';
|
---|
865 | break;
|
---|
866 | case 't':
|
---|
867 | item.command = 'q';
|
---|
868 | break;
|
---|
869 | }
|
---|
870 | item.args.unshift(
|
---|
871 | data[data.length - 2] - data[data.length - 4],
|
---|
872 | data[data.length - 1] - data[data.length - 3]
|
---|
873 | );
|
---|
874 | return item;
|
---|
875 | }
|
---|
876 |
|
---|
877 | /**
|
---|
878 | * Returns distance between two points
|
---|
879 | *
|
---|
880 | * @param {Array} point1 first point coordinates
|
---|
881 | * @param {Array} point2 second point coordinates
|
---|
882 | * @return {Number} distance
|
---|
883 | */
|
---|
884 |
|
---|
885 | function getDistance(point1, point2) {
|
---|
886 | return Math.hypot(point1[0] - point2[0], point1[1] - point2[1]);
|
---|
887 | }
|
---|
888 |
|
---|
889 | /**
|
---|
890 | * Returns coordinates of the curve point corresponding to the certain t
|
---|
891 | * a·(1 - t)³·p1 + b·(1 - t)²·t·p2 + c·(1 - t)·t²·p3 + d·t³·p4,
|
---|
892 | * where pN are control points and p1 is zero due to relative coordinates.
|
---|
893 | *
|
---|
894 | * @param {Array} curve array of curve points coordinates
|
---|
895 | * @param {Number} t parametric position from 0 to 1
|
---|
896 | * @return {Array} Point coordinates
|
---|
897 | */
|
---|
898 |
|
---|
899 | function getCubicBezierPoint(curve, t) {
|
---|
900 | var sqrT = t * t,
|
---|
901 | cubT = sqrT * t,
|
---|
902 | mt = 1 - t,
|
---|
903 | sqrMt = mt * mt;
|
---|
904 |
|
---|
905 | return [
|
---|
906 | 3 * sqrMt * t * curve[0] + 3 * mt * sqrT * curve[2] + cubT * curve[4],
|
---|
907 | 3 * sqrMt * t * curve[1] + 3 * mt * sqrT * curve[3] + cubT * curve[5],
|
---|
908 | ];
|
---|
909 | }
|
---|
910 |
|
---|
911 | /**
|
---|
912 | * Finds circle by 3 points of the curve and checks if the curve fits the found circle.
|
---|
913 | *
|
---|
914 | * @param {Array} curve
|
---|
915 | * @return {Object|undefined} circle
|
---|
916 | */
|
---|
917 |
|
---|
918 | function findCircle(curve) {
|
---|
919 | var midPoint = getCubicBezierPoint(curve, 1 / 2),
|
---|
920 | m1 = [midPoint[0] / 2, midPoint[1] / 2],
|
---|
921 | m2 = [(midPoint[0] + curve[4]) / 2, (midPoint[1] + curve[5]) / 2],
|
---|
922 | center = getIntersection([
|
---|
923 | m1[0],
|
---|
924 | m1[1],
|
---|
925 | m1[0] + m1[1],
|
---|
926 | m1[1] - m1[0],
|
---|
927 | m2[0],
|
---|
928 | m2[1],
|
---|
929 | m2[0] + (m2[1] - midPoint[1]),
|
---|
930 | m2[1] - (m2[0] - midPoint[0]),
|
---|
931 | ]),
|
---|
932 | radius = center && getDistance([0, 0], center),
|
---|
933 | tolerance = Math.min(arcThreshold * error, (arcTolerance * radius) / 100);
|
---|
934 |
|
---|
935 | if (
|
---|
936 | center &&
|
---|
937 | radius < 1e15 &&
|
---|
938 | [1 / 4, 3 / 4].every(function (point) {
|
---|
939 | return (
|
---|
940 | Math.abs(
|
---|
941 | getDistance(getCubicBezierPoint(curve, point), center) - radius
|
---|
942 | ) <= tolerance
|
---|
943 | );
|
---|
944 | })
|
---|
945 | )
|
---|
946 | return { center: center, radius: radius };
|
---|
947 | }
|
---|
948 |
|
---|
949 | /**
|
---|
950 | * Checks if a curve fits the given circle.
|
---|
951 | *
|
---|
952 | * @param {Object} circle
|
---|
953 | * @param {Array} curve
|
---|
954 | * @return {Boolean}
|
---|
955 | */
|
---|
956 |
|
---|
957 | function isArc(curve, circle) {
|
---|
958 | var tolerance = Math.min(
|
---|
959 | arcThreshold * error,
|
---|
960 | (arcTolerance * circle.radius) / 100
|
---|
961 | );
|
---|
962 |
|
---|
963 | return [0, 1 / 4, 1 / 2, 3 / 4, 1].every(function (point) {
|
---|
964 | return (
|
---|
965 | Math.abs(
|
---|
966 | getDistance(getCubicBezierPoint(curve, point), circle.center) -
|
---|
967 | circle.radius
|
---|
968 | ) <= tolerance
|
---|
969 | );
|
---|
970 | });
|
---|
971 | }
|
---|
972 |
|
---|
973 | /**
|
---|
974 | * Checks if a previous curve fits the given circle.
|
---|
975 | *
|
---|
976 | * @param {Object} circle
|
---|
977 | * @param {Array} curve
|
---|
978 | * @return {Boolean}
|
---|
979 | */
|
---|
980 |
|
---|
981 | function isArcPrev(curve, circle) {
|
---|
982 | return isArc(curve, {
|
---|
983 | center: [circle.center[0] + curve[4], circle.center[1] + curve[5]],
|
---|
984 | radius: circle.radius,
|
---|
985 | });
|
---|
986 | }
|
---|
987 |
|
---|
988 | /**
|
---|
989 | * Finds angle of a curve fitting the given arc.
|
---|
990 |
|
---|
991 | * @param {Array} curve
|
---|
992 | * @param {Object} relCircle
|
---|
993 | * @return {Number} angle
|
---|
994 | */
|
---|
995 |
|
---|
996 | function findArcAngle(curve, relCircle) {
|
---|
997 | var x1 = -relCircle.center[0],
|
---|
998 | y1 = -relCircle.center[1],
|
---|
999 | x2 = curve[4] - relCircle.center[0],
|
---|
1000 | y2 = curve[5] - relCircle.center[1];
|
---|
1001 |
|
---|
1002 | return Math.acos(
|
---|
1003 | (x1 * x2 + y1 * y2) / Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2))
|
---|
1004 | );
|
---|
1005 | }
|
---|
1006 |
|
---|
1007 | /**
|
---|
1008 | * Converts given path data to string.
|
---|
1009 | *
|
---|
1010 | * @param {Object} params
|
---|
1011 | * @param {Array} pathData
|
---|
1012 | * @return {String}
|
---|
1013 | */
|
---|
1014 |
|
---|
1015 | function data2Path(params, pathData) {
|
---|
1016 | return pathData.reduce(function (pathString, item) {
|
---|
1017 | var strData = '';
|
---|
1018 | if (item.args) {
|
---|
1019 | strData = cleanupOutData(roundData(item.args.slice()), params);
|
---|
1020 | }
|
---|
1021 | return pathString + item.command + strData;
|
---|
1022 | }, '');
|
---|
1023 | }
|
---|