From c21fef54e1aede8cf9f4b4b73ab79d91b7907d2b Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 5 Mar 2021 18:08:02 +0300 Subject: [PATCH] Remove useless path commands considering static styles --- plugins/convertPathData.js | 1425 +++++++++++++-------------- test/plugins/convertPathData.03.svg | 4 +- 2 files changed, 714 insertions(+), 715 deletions(-) diff --git a/plugins/convertPathData.js b/plugins/convertPathData.js index 80c9b8651..b899391a2 100644 --- a/plugins/convertPathData.js +++ b/plugins/convertPathData.js @@ -1,44 +1,46 @@ 'use strict'; +const { computeStyle } = require('../lib/style.js'); + exports.type = 'perItem'; exports.active = true; -exports.description = 'optimizes path data: writes in shorter form, applies transformations'; +exports.description = + 'optimizes path data: writes in shorter form, applies transformations'; exports.params = { - applyTransforms: true, - applyTransformsStroked: true, - makeArcs: { - threshold: 2.5, // coefficient of rounding error - tolerance: 0.5 // percentage of radius - }, - straightCurves: true, - lineShorthands: true, - curveSmoothShorthands: true, - floatPrecision: 3, - transformPrecision: 5, - removeUseless: true, - collapseRepeated: true, - utilizeAbsolute: true, - leadingZero: true, - negativeExtraSpace: true, - noSpaceAfterFlags: false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20 - forceAbsolutePath: false, + applyTransforms: true, + applyTransformsStroked: true, + makeArcs: { + threshold: 2.5, // coefficient of rounding error + tolerance: 0.5, // percentage of radius + }, + straightCurves: true, + lineShorthands: true, + curveSmoothShorthands: true, + floatPrecision: 3, + transformPrecision: 5, + removeUseless: true, + collapseRepeated: true, + utilizeAbsolute: true, + leadingZero: true, + negativeExtraSpace: true, + noSpaceAfterFlags: false, // a20 60 45 0 1 30 20 → a20 60 45 0130 20 + forceAbsolutePath: false, }; var pathElems = require('./_collections.js').pathElems, - path2js = require('./_path.js').path2js, - js2path = require('./_path.js').js2path, - applyTransforms = require('./_path.js').applyTransforms, - cleanupOutData = require('../lib/svgo/tools').cleanupOutData, - roundData, - precision, - error, - arcThreshold, - arcTolerance, - hasMarkerMid, - hasStrokeLinecap; + path2js = require('./_path.js').path2js, + js2path = require('./_path.js').js2path, + applyTransforms = require('./_path.js').applyTransforms, + cleanupOutData = require('../lib/svgo/tools').cleanupOutData, + roundData, + precision, + error, + arcThreshold, + arcTolerance, + hasMarkerMid; /** * Convert absolute Path to relative, @@ -56,46 +58,50 @@ var pathElems = require('./_collections.js').pathElems, * * @author Kir Belevich */ -exports.fn = function(item, params) { - - if (item.isElem(pathElems) && item.hasAttr('d')) { - - precision = params.floatPrecision; - error = precision !== false ? +Math.pow(.1, precision).toFixed(precision) : 1e-2; - roundData = precision > 0 && precision < 20 ? strongRound : round; - if (params.makeArcs) { - arcThreshold = params.makeArcs.threshold; - arcTolerance = params.makeArcs.tolerance; - } - hasMarkerMid = item.hasAttr('marker-mid'); - - const stroke = item.computedAttr('stroke'); - const strokeLinecap = item.computedAttr('stroke-linecap'); - // stroke-linecap may exist in inline styles which are not parsed for now - hasStrokeLinecap = item.hasAttr('style') || - stroke && stroke != 'none' && strokeLinecap && strokeLinecap != 'butt'; +exports.fn = function (item, params) { + if (item.isElem(pathElems) && item.hasAttr('d')) { + const computedStyle = computeStyle(item); + precision = params.floatPrecision; + error = + precision !== false ? +Math.pow(0.1, precision).toFixed(precision) : 1e-2; + roundData = precision > 0 && precision < 20 ? strongRound : round; + if (params.makeArcs) { + arcThreshold = params.makeArcs.threshold; + arcTolerance = params.makeArcs.tolerance; + } + hasMarkerMid = item.hasAttr('marker-mid'); - var data = path2js(item); + const maybeHasStroke = + computedStyle.stroke && + (computedStyle.stroke.type === 'dynamic' || + computedStyle.stroke.value !== 'none'); + const maybeHasLinecap = + computedStyle['stroke-linecap'] && + (computedStyle['stroke-linecap'].type === 'dynamic' || + computedStyle['stroke-linecap'].value !== 'butt'); + const maybeHasStrokeAndLinecap = maybeHasStroke && maybeHasLinecap; - // TODO: get rid of functions returns - if (data.length) { - convertToRelative(data); + var data = path2js(item); - if (params.applyTransforms) { - data = applyTransforms(item, data, params); - } + // TODO: get rid of functions returns + if (data.length) { + convertToRelative(data); - data = filters(data, params); + if (params.applyTransforms) { + data = applyTransforms(item, data, params); + } - if (params.utilizeAbsolute) { - data = convertToMixed(data, params); - } + data = filters(data, params, { + maybeHasStrokeAndLinecap, + }); - js2path(item, data, params); - } + if (params.utilizeAbsolute) { + data = convertToMixed(data, params); + } + js2path(item, data, params); } - + } }; /** @@ -106,162 +112,141 @@ exports.fn = function(item, params) { * @return {Array} output path data */ function convertToRelative(path) { + var point = [0, 0], + subpathPoint = [0, 0], + baseItem; + + path.forEach(function (item, index) { + var instruction = item.instruction, + data = item.data; + + // data !== !z + if (data) { + // already relative + // recalculate current point + if ('mcslqta'.indexOf(instruction) > -1) { + point[0] += data[data.length - 2]; + point[1] += data[data.length - 1]; + + if (instruction === 'm') { + subpathPoint[0] = point[0]; + subpathPoint[1] = point[1]; + baseItem = item; + } + } else if (instruction === 'h') { + point[0] += data[0]; + } else if (instruction === 'v') { + point[1] += data[0]; + } - var point = [0, 0], - subpathPoint = [0, 0], - baseItem; - - path.forEach(function(item, index) { - - var instruction = item.instruction, - data = item.data; - - // data !== !z - if (data) { - - // already relative - // recalculate current point - if ('mcslqta'.indexOf(instruction) > -1) { - - point[0] += data[data.length - 2]; - point[1] += data[data.length - 1]; - - if (instruction === 'm') { - subpathPoint[0] = point[0]; - subpathPoint[1] = point[1]; - baseItem = item; - } - - } else if (instruction === 'h') { - - point[0] += data[0]; - - } else if (instruction === 'v') { - - point[1] += data[0]; - - } - - // convert absolute path data coordinates to relative - // if "M" was not transformed from "m" - // M → m - if (instruction === 'M') { - - if (index > 0) instruction = 'm'; - - data[0] -= point[0]; - data[1] -= point[1]; - - subpathPoint[0] = point[0] += data[0]; - subpathPoint[1] = point[1] += data[1]; - - baseItem = item; - - } - - // L → l - // T → t - else if ('LT'.indexOf(instruction) > -1) { - - instruction = instruction.toLowerCase(); - - // x y - // 0 1 - data[0] -= point[0]; - data[1] -= point[1]; - - point[0] += data[0]; - point[1] += data[1]; - - // C → c - } else if (instruction === 'C') { + // convert absolute path data coordinates to relative + // if "M" was not transformed from "m" + // M → m + if (instruction === 'M') { + if (index > 0) instruction = 'm'; - instruction = 'c'; + data[0] -= point[0]; + data[1] -= point[1]; - // x1 y1 x2 y2 x y - // 0 1 2 3 4 5 - data[0] -= point[0]; - data[1] -= point[1]; - data[2] -= point[0]; - data[3] -= point[1]; - data[4] -= point[0]; - data[5] -= point[1]; + subpathPoint[0] = point[0] += data[0]; + subpathPoint[1] = point[1] += data[1]; - point[0] += data[4]; - point[1] += data[5]; + baseItem = item; + } - // S → s - // Q → q - } else if ('SQ'.indexOf(instruction) > -1) { + // L → l + // T → t + else if ('LT'.indexOf(instruction) > -1) { + instruction = instruction.toLowerCase(); - instruction = instruction.toLowerCase(); + // x y + // 0 1 + data[0] -= point[0]; + data[1] -= point[1]; - // x1 y1 x y - // 0 1 2 3 - data[0] -= point[0]; - data[1] -= point[1]; - data[2] -= point[0]; - data[3] -= point[1]; + point[0] += data[0]; + point[1] += data[1]; - point[0] += data[2]; - point[1] += data[3]; + // C → c + } else if (instruction === 'C') { + instruction = 'c'; - // A → a - } else if (instruction === 'A') { + // x1 y1 x2 y2 x y + // 0 1 2 3 4 5 + data[0] -= point[0]; + data[1] -= point[1]; + data[2] -= point[0]; + data[3] -= point[1]; + data[4] -= point[0]; + data[5] -= point[1]; - instruction = 'a'; + point[0] += data[4]; + point[1] += data[5]; - // rx ry x-axis-rotation large-arc-flag sweep-flag x y - // 0 1 2 3 4 5 6 - data[5] -= point[0]; - data[6] -= point[1]; + // S → s + // Q → q + } else if ('SQ'.indexOf(instruction) > -1) { + instruction = instruction.toLowerCase(); - point[0] += data[5]; - point[1] += data[6]; + // x1 y1 x y + // 0 1 2 3 + data[0] -= point[0]; + data[1] -= point[1]; + data[2] -= point[0]; + data[3] -= point[1]; - // H → h - } else if (instruction === 'H') { + point[0] += data[2]; + point[1] += data[3]; - instruction = 'h'; + // A → a + } else if (instruction === 'A') { + instruction = 'a'; - data[0] -= point[0]; + // rx ry x-axis-rotation large-arc-flag sweep-flag x y + // 0 1 2 3 4 5 6 + data[5] -= point[0]; + data[6] -= point[1]; - point[0] += data[0]; + point[0] += data[5]; + point[1] += data[6]; - // V → v - } else if (instruction === 'V') { + // H → h + } else if (instruction === 'H') { + instruction = 'h'; - instruction = 'v'; + data[0] -= point[0]; - data[0] -= point[1]; + point[0] += data[0]; - point[1] += data[0]; + // V → v + } else if (instruction === 'V') { + instruction = 'v'; - } + data[0] -= point[1]; - item.instruction = instruction; - item.data = data; + point[1] += data[0]; + } - // store absolute coordinates for later use - item.coords = point.slice(-2); + item.instruction = instruction; + item.data = data; - } - - // !data === z, reset current point - else if (instruction == 'z') { - if (baseItem) { - item.coords = baseItem.coords; - } - point[0] = subpathPoint[0]; - point[1] = subpathPoint[1]; - } - - item.base = index > 0 ? path[index - 1].coords : [0, 0]; + // store absolute coordinates for later use + item.coords = point.slice(-2); + } - }); + // !data === z, reset current point + else if (instruction == 'z') { + if (baseItem) { + item.coords = baseItem.coords; + } + point[0] = subpathPoint[0]; + point[1] = subpathPoint[1]; + } - return path; + item.base = index > 0 ? path[index - 1].coords : [0, 0]; + }); + return path; } /** @@ -271,374 +256,360 @@ function convertToRelative(path) { * @param {Object} params plugin params * @return {Array} output path data */ -function filters(path, params) { - - var stringify = data2Path.bind(null, params), - relSubpoint = [0, 0], - pathBase = [0, 0], - prev = {}; - - path = path.filter(function(item, index, path) { - - var instruction = item.instruction, - data = item.data, - next = path[index + 1]; - - if (data) { - - var sdata = data, - circle; - - if (instruction === 's') { - sdata = [0, 0].concat(data); - - if ('cs'.indexOf(prev.instruction) > -1) { - var pdata = prev.data, - n = pdata.length; - - // (-x, -y) of the prev tangent point relative to the current point - sdata[0] = pdata[n - 2] - pdata[n - 4]; - sdata[1] = pdata[n - 1] - pdata[n - 3]; - } - - } - - // convert curves to arcs if possible - if ( - params.makeArcs && - (instruction == 'c' || instruction == 's') && - isConvex(sdata) && - (circle = findCircle(sdata)) - ) { - var r = roundData([circle.radius])[0], - angle = findArcAngle(sdata, circle), - sweep = sdata[5] * sdata[0] - sdata[4] * sdata[1] > 0 ? 1 : 0, - arc = { - instruction: 'a', - data: [r, r, 0, 0, sweep, sdata[4], sdata[5]], - coords: item.coords.slice(), - base: item.base - }, - output = [arc], - // relative coordinates to adjust the found circle - relCenter = [circle.center[0] - sdata[4], circle.center[1] - sdata[5]], - relCircle = { center: relCenter, radius: circle.radius }, - arcCurves = [item], - hasPrev = 0, - suffix = '', - nextLonghand; - - if ( - prev.instruction == 'c' && isConvex(prev.data) && isArcPrev(prev.data, circle) || - prev.instruction == 'a' && prev.sdata && isArcPrev(prev.sdata, circle) - ) { - arcCurves.unshift(prev); - arc.base = prev.base; - arc.data[5] = arc.coords[0] - arc.base[0]; - arc.data[6] = arc.coords[1] - arc.base[1]; - var prevData = prev.instruction == 'a' ? prev.sdata : prev.data; - var prevAngle = findArcAngle(prevData, - { - center: [prevData[4] + circle.center[0], prevData[5] + circle.center[1]], - radius: circle.radius - } - ); - angle += prevAngle; - if (angle > Math.PI) arc.data[3] = 1; - hasPrev = 1; - } - - // check if next curves are fitting the arc - for (var j = index; (next = path[++j]) && ~'cs'.indexOf(next.instruction);) { - var nextData = next.data; - if (next.instruction == 's') { - nextLonghand = makeLonghand({instruction: 's', data: next.data.slice() }, - path[j - 1].data); - nextData = nextLonghand.data; - nextLonghand.data = nextData.slice(0, 2); - suffix = stringify([nextLonghand]); - } - if (isConvex(nextData) && isArc(nextData, relCircle)) { - angle += findArcAngle(nextData, relCircle); - if (angle - 2 * Math.PI > 1e-3) break; // more than 360° - if (angle > Math.PI) arc.data[3] = 1; - arcCurves.push(next); - if (2 * Math.PI - angle > 1e-3) { // less than 360° - arc.coords = next.coords; - arc.data[5] = arc.coords[0] - arc.base[0]; - arc.data[6] = arc.coords[1] - arc.base[1]; - } else { - // full circle, make a half-circle arc and add a second one - arc.data[5] = 2 * (relCircle.center[0] - nextData[4]); - arc.data[6] = 2 * (relCircle.center[1] - nextData[5]); - arc.coords = [arc.base[0] + arc.data[5], arc.base[1] + arc.data[6]]; - arc = { - instruction: 'a', - data: [r, r, 0, 0, sweep, - next.coords[0] - arc.coords[0], next.coords[1] - arc.coords[1]], - coords: next.coords, - base: arc.coords - }; - output.push(arc); - j++; - break; - } - relCenter[0] -= nextData[4]; - relCenter[1] -= nextData[5]; - } else break; - } - - if ((stringify(output) + suffix).length < stringify(arcCurves).length) { - if (path[j] && path[j].instruction == 's') { - makeLonghand(path[j], path[j - 1].data); - } - if (hasPrev) { - var prevArc = output.shift(); - roundData(prevArc.data); - relSubpoint[0] += prevArc.data[5] - prev.data[prev.data.length - 2]; - relSubpoint[1] += prevArc.data[6] - prev.data[prev.data.length - 1]; - prev.instruction = 'a'; - prev.data = prevArc.data; - item.base = prev.coords = prevArc.coords; - } - arc = output.shift(); - if (arcCurves.length == 1) { - item.sdata = sdata.slice(); // preserve curve data for future checks - } else if (arcCurves.length - 1 - hasPrev > 0) { - // filter out consumed next items - path.splice.apply(path, [index + 1, arcCurves.length - 1 - hasPrev].concat(output)); - } - if (!arc) return false; - instruction = 'a'; - data = arc.data; - item.coords = arc.coords; - } - } - - // Rounding relative coordinates, taking in account accummulating error - // to get closer to absolute coordinates. Sum of rounded value remains same: - // l .25 3 .25 2 .25 3 .25 2 -> l .3 3 .2 2 .3 3 .2 2 - if (precision !== false) { - if ('mltqsc'.indexOf(instruction) > -1) { - for (var i = data.length; i--;) { - data[i] += item.base[i % 2] - relSubpoint[i % 2]; - } - } else if (instruction == 'h') { - data[0] += item.base[0] - relSubpoint[0]; - } else if (instruction == 'v') { - data[0] += item.base[1] - relSubpoint[1]; - } else if (instruction == 'a') { - data[5] += item.base[0] - relSubpoint[0]; - data[6] += item.base[1] - relSubpoint[1]; - } - roundData(data); - - if (instruction == 'h') relSubpoint[0] += data[0]; - else if (instruction == 'v') relSubpoint[1] += data[0]; - else { - relSubpoint[0] += data[data.length - 2]; - relSubpoint[1] += data[data.length - 1]; - } - roundData(relSubpoint); - - if (instruction.toLowerCase() == 'm') { - pathBase[0] = relSubpoint[0]; - pathBase[1] = relSubpoint[1]; - } - } - - // convert straight curves into lines segments - if (params.straightCurves) { - - if ( - instruction === 'c' && - isCurveStraightLine(data) || - instruction === 's' && - isCurveStraightLine(sdata) - ) { - if (next && next.instruction == 's') - makeLonghand(next, data); // fix up next curve - instruction = 'l'; - data = data.slice(-2); - } - - else if ( - instruction === 'q' && - isCurveStraightLine(data) - ) { - if (next && next.instruction == 't') - makeLonghand(next, data); // fix up next curve - instruction = 'l'; - data = data.slice(-2); - } - - else if ( - instruction === 't' && - prev.instruction !== 'q' && - prev.instruction !== 't' - ) { - instruction = 'l'; - data = data.slice(-2); - } - - else if ( - instruction === 'a' && - (data[0] === 0 || data[1] === 0) - ) { - instruction = 'l'; - data = data.slice(-2); - } - } - - // horizontal and vertical line shorthands - // l 50 0 → h 50 - // l 0 50 → v 50 - if ( - params.lineShorthands && - instruction === 'l' - ) { - if (data[1] === 0) { - instruction = 'h'; - data.pop(); - } else if (data[0] === 0) { - instruction = 'v'; - data.shift(); - } - } +function filters(path, params, { maybeHasStrokeAndLinecap }) { + var stringify = data2Path.bind(null, params), + relSubpoint = [0, 0], + pathBase = [0, 0], + prev = {}; + + path = path.filter(function (item, index, path) { + var instruction = item.instruction, + data = item.data, + next = path[index + 1]; + + if (data) { + var sdata = data, + circle; + + if (instruction === 's') { + sdata = [0, 0].concat(data); + + if ('cs'.indexOf(prev.instruction) > -1) { + var pdata = prev.data, + n = pdata.length; + + // (-x, -y) of the prev tangent point relative to the current point + sdata[0] = pdata[n - 2] - pdata[n - 4]; + sdata[1] = pdata[n - 1] - pdata[n - 3]; + } + } + + // convert curves to arcs if possible + if ( + params.makeArcs && + (instruction == 'c' || instruction == 's') && + isConvex(sdata) && + (circle = findCircle(sdata)) + ) { + var r = roundData([circle.radius])[0], + angle = findArcAngle(sdata, circle), + sweep = sdata[5] * sdata[0] - sdata[4] * sdata[1] > 0 ? 1 : 0, + arc = { + instruction: 'a', + data: [r, r, 0, 0, sweep, sdata[4], sdata[5]], + coords: item.coords.slice(), + base: item.base, + }, + output = [arc], + // relative coordinates to adjust the found circle + relCenter = [ + circle.center[0] - sdata[4], + circle.center[1] - sdata[5], + ], + relCircle = { center: relCenter, radius: circle.radius }, + arcCurves = [item], + hasPrev = 0, + suffix = '', + nextLonghand; - // collapse repeated commands - // h 20 h 30 -> h 50 - if ( - params.collapseRepeated && - !hasMarkerMid && - ('mhv'.indexOf(instruction) > -1) && - prev.instruction && - instruction == prev.instruction.toLowerCase() && - ( - (instruction != 'h' && instruction != 'v') || - (prev.data[0] >= 0) == (data[0] >= 0) - )) { - prev.data[0] += data[0]; - if (instruction != 'h' && instruction != 'v') { - prev.data[1] += data[1]; - } - prev.coords = item.coords; - path[index] = prev; - return false; - } + if ( + (prev.instruction == 'c' && + isConvex(prev.data) && + isArcPrev(prev.data, circle)) || + (prev.instruction == 'a' && + prev.sdata && + isArcPrev(prev.sdata, circle)) + ) { + arcCurves.unshift(prev); + arc.base = prev.base; + arc.data[5] = arc.coords[0] - arc.base[0]; + arc.data[6] = arc.coords[1] - arc.base[1]; + var prevData = prev.instruction == 'a' ? prev.sdata : prev.data; + var prevAngle = findArcAngle(prevData, { + center: [ + prevData[4] + circle.center[0], + prevData[5] + circle.center[1], + ], + radius: circle.radius, + }); + angle += prevAngle; + if (angle > Math.PI) arc.data[3] = 1; + hasPrev = 1; + } - // convert curves into smooth shorthands - if (params.curveSmoothShorthands && prev.instruction) { - - // curveto - if (instruction === 'c') { - - // c + c → c + s - if ( - prev.instruction === 'c' && - data[0] === -(prev.data[2] - prev.data[4]) && - data[1] === -(prev.data[3] - prev.data[5]) - ) { - instruction = 's'; - data = data.slice(2); - } - - // s + c → s + s - else if ( - prev.instruction === 's' && - data[0] === -(prev.data[0] - prev.data[2]) && - data[1] === -(prev.data[1] - prev.data[3]) - ) { - instruction = 's'; - data = data.slice(2); - } - - // [^cs] + c → [^cs] + s - else if ( - 'cs'.indexOf(prev.instruction) === -1 && - data[0] === 0 && - data[1] === 0 - ) { - instruction = 's'; - data = data.slice(2); - } - - } - - // quadratic Bézier curveto - else if (instruction === 'q') { - - // q + q → q + t - if ( - prev.instruction === 'q' && - data[0] === (prev.data[2] - prev.data[0]) && - data[1] === (prev.data[3] - prev.data[1]) - ) { - instruction = 't'; - data = data.slice(2); - } - - // t + q → t + t - else if ( - prev.instruction === 't' && - data[2] === prev.data[0] && - data[3] === prev.data[1] - ) { - instruction = 't'; - data = data.slice(2); - } - - } + // check if next curves are fitting the arc + for ( + var j = index; + (next = path[++j]) && ~'cs'.indexOf(next.instruction); + ) { + var nextData = next.data; + if (next.instruction == 's') { + nextLonghand = makeLonghand( + { instruction: 's', data: next.data.slice() }, + path[j - 1].data + ); + nextData = nextLonghand.data; + nextLonghand.data = nextData.slice(0, 2); + suffix = stringify([nextLonghand]); + } + if (isConvex(nextData) && isArc(nextData, relCircle)) { + angle += findArcAngle(nextData, relCircle); + if (angle - 2 * Math.PI > 1e-3) break; // more than 360° + if (angle > Math.PI) arc.data[3] = 1; + arcCurves.push(next); + if (2 * Math.PI - angle > 1e-3) { + // less than 360° + arc.coords = next.coords; + arc.data[5] = arc.coords[0] - arc.base[0]; + arc.data[6] = arc.coords[1] - arc.base[1]; + } else { + // full circle, make a half-circle arc and add a second one + arc.data[5] = 2 * (relCircle.center[0] - nextData[4]); + arc.data[6] = 2 * (relCircle.center[1] - nextData[5]); + arc.coords = [ + arc.base[0] + arc.data[5], + arc.base[1] + arc.data[6], + ]; + arc = { + instruction: 'a', + data: [ + r, + r, + 0, + 0, + sweep, + next.coords[0] - arc.coords[0], + next.coords[1] - arc.coords[1], + ], + coords: next.coords, + base: arc.coords, + }; + output.push(arc); + j++; + break; } + relCenter[0] -= nextData[4]; + relCenter[1] -= nextData[5]; + } else break; + } - // remove useless non-first path segments - if (params.removeUseless && !hasStrokeLinecap) { - - // 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 - if ( - ( - 'lhvqtcs'.indexOf(instruction) > -1 - ) && - data.every(function(i) { return i === 0; }) - ) { - path[index] = prev; - return false; - } - - // a 25,25 -30 0,1 0,0 - if ( - instruction === 'a' && - data[5] === 0 && - data[6] === 0 - ) { - path[index] = prev; - return false; - } - - } + if ((stringify(output) + suffix).length < stringify(arcCurves).length) { + if (path[j] && path[j].instruction == 's') { + makeLonghand(path[j], path[j - 1].data); + } + if (hasPrev) { + var prevArc = output.shift(); + roundData(prevArc.data); + relSubpoint[0] += prevArc.data[5] - prev.data[prev.data.length - 2]; + relSubpoint[1] += prevArc.data[6] - prev.data[prev.data.length - 1]; + prev.instruction = 'a'; + prev.data = prevArc.data; + item.base = prev.coords = prevArc.coords; + } + arc = output.shift(); + if (arcCurves.length == 1) { + item.sdata = sdata.slice(); // preserve curve data for future checks + } else if (arcCurves.length - 1 - hasPrev > 0) { + // filter out consumed next items + path.splice.apply( + path, + [index + 1, arcCurves.length - 1 - hasPrev].concat(output) + ); + } + if (!arc) return false; + instruction = 'a'; + data = arc.data; + item.coords = arc.coords; + } + } - item.instruction = instruction; - item.data = data; + // Rounding relative coordinates, taking in account accummulating error + // to get closer to absolute coordinates. Sum of rounded value remains same: + // l .25 3 .25 2 .25 3 .25 2 -> l .3 3 .2 2 .3 3 .2 2 + if (precision !== false) { + if ('mltqsc'.indexOf(instruction) > -1) { + for (var i = data.length; i--; ) { + data[i] += item.base[i % 2] - relSubpoint[i % 2]; + } + } else if (instruction == 'h') { + data[0] += item.base[0] - relSubpoint[0]; + } else if (instruction == 'v') { + data[0] += item.base[1] - relSubpoint[1]; + } else if (instruction == 'a') { + data[5] += item.base[0] - relSubpoint[0]; + data[6] += item.base[1] - relSubpoint[1]; + } + roundData(data); - prev = item; + if (instruction == 'h') relSubpoint[0] += data[0]; + else if (instruction == 'v') relSubpoint[1] += data[0]; + else { + relSubpoint[0] += data[data.length - 2]; + relSubpoint[1] += data[data.length - 1]; + } + roundData(relSubpoint); - } else { + if (instruction.toLowerCase() == 'm') { + pathBase[0] = relSubpoint[0]; + pathBase[1] = relSubpoint[1]; + } + } - // z resets coordinates - relSubpoint[0] = pathBase[0]; - relSubpoint[1] = pathBase[1]; - if (prev.instruction == 'z') return false; - prev = item; + // convert straight curves into lines segments + if (params.straightCurves) { + if ( + (instruction === 'c' && isCurveStraightLine(data)) || + (instruction === 's' && isCurveStraightLine(sdata)) + ) { + if (next && next.instruction == 's') makeLonghand(next, data); // fix up next curve + instruction = 'l'; + data = data.slice(-2); + } else if (instruction === 'q' && isCurveStraightLine(data)) { + if (next && next.instruction == 't') makeLonghand(next, data); // fix up next curve + instruction = 'l'; + data = data.slice(-2); + } else if ( + instruction === 't' && + prev.instruction !== 'q' && + prev.instruction !== 't' + ) { + instruction = 'l'; + data = data.slice(-2); + } else if (instruction === 'a' && (data[0] === 0 || data[1] === 0)) { + instruction = 'l'; + data = data.slice(-2); + } + } + + // horizontal and vertical line shorthands + // l 50 0 → h 50 + // l 0 50 → v 50 + if (params.lineShorthands && instruction === 'l') { + if (data[1] === 0) { + instruction = 'h'; + data.pop(); + } else if (data[0] === 0) { + instruction = 'v'; + data.shift(); + } + } + + // collapse repeated commands + // h 20 h 30 -> h 50 + if ( + params.collapseRepeated && + !hasMarkerMid && + 'mhv'.indexOf(instruction) > -1 && + prev.instruction && + instruction == prev.instruction.toLowerCase() && + ((instruction != 'h' && instruction != 'v') || + prev.data[0] >= 0 == data[0] >= 0) + ) { + prev.data[0] += data[0]; + if (instruction != 'h' && instruction != 'v') { + prev.data[1] += data[1]; + } + prev.coords = item.coords; + path[index] = prev; + return false; + } + + // convert curves into smooth shorthands + if (params.curveSmoothShorthands && prev.instruction) { + // curveto + if (instruction === 'c') { + // c + c → c + s + if ( + prev.instruction === 'c' && + data[0] === -(prev.data[2] - prev.data[4]) && + data[1] === -(prev.data[3] - prev.data[5]) + ) { + instruction = 's'; + data = data.slice(2); + } + + // s + c → s + s + else if ( + prev.instruction === 's' && + data[0] === -(prev.data[0] - prev.data[2]) && + data[1] === -(prev.data[1] - prev.data[3]) + ) { + instruction = 's'; + data = data.slice(2); + } + + // [^cs] + c → [^cs] + s + else if ( + 'cs'.indexOf(prev.instruction) === -1 && + data[0] === 0 && + data[1] === 0 + ) { + instruction = 's'; + data = data.slice(2); + } + } + // quadratic Bézier curveto + else if (instruction === 'q') { + // q + q → q + t + if ( + prev.instruction === 'q' && + data[0] === prev.data[2] - prev.data[0] && + data[1] === prev.data[3] - prev.data[1] + ) { + instruction = 't'; + data = data.slice(2); + } + + // t + q → t + t + else if ( + prev.instruction === 't' && + data[2] === prev.data[0] && + data[3] === prev.data[1] + ) { + instruction = 't'; + data = data.slice(2); + } } + } - return true; + // remove useless non-first path segments + if (params.removeUseless && !maybeHasStrokeAndLinecap) { + // 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 + if ( + 'lhvqtcs'.indexOf(instruction) > -1 && + data.every(function (i) { + return i === 0; + }) + ) { + path[index] = prev; + return false; + } - }); + // a 25,25 -30 0,1 0,0 + if (instruction === 'a' && data[5] === 0 && data[6] === 0) { + path[index] = prev; + return false; + } + } + + item.instruction = instruction; + item.data = data; + + prev = item; + } else { + // z resets coordinates + relSubpoint[0] = pathBase[0]; + relSubpoint[1] = pathBase[1]; + if (prev.instruction == 'z') return false; + prev = item; + } - return path; + return true; + }); + return path; } /** @@ -648,66 +619,63 @@ function filters(path, params) { * @return {Boolean} output */ function convertToMixed(path, params) { + var prev = path[0]; - var prev = path[0]; - - path = path.filter(function(item, index) { - - if (index == 0) return true; - if (!item.data) { - prev = item; - return true; - } - - var instruction = item.instruction, - data = item.data, - adata = data && data.slice(0); - - if ('mltqsc'.indexOf(instruction) > -1) { - for (var i = adata.length; i--;) { - adata[i] += item.base[i % 2]; - } - } else if (instruction == 'h') { - adata[0] += item.base[0]; - } else if (instruction == 'v') { - adata[0] += item.base[1]; - } else if (instruction == 'a') { - adata[5] += item.base[0]; - adata[6] += item.base[1]; - } - - roundData(adata); + path = path.filter(function (item, index) { + if (index == 0) return true; + if (!item.data) { + prev = item; + return true; + } - var absoluteDataStr = cleanupOutData(adata, params), - relativeDataStr = cleanupOutData(data, params); + var instruction = item.instruction, + data = item.data, + adata = data && data.slice(0); + + if ('mltqsc'.indexOf(instruction) > -1) { + for (var i = adata.length; i--; ) { + adata[i] += item.base[i % 2]; + } + } else if (instruction == 'h') { + adata[0] += item.base[0]; + } else if (instruction == 'v') { + adata[0] += item.base[1]; + } else if (instruction == 'a') { + adata[5] += item.base[0]; + adata[6] += item.base[1]; + } - // Convert to absolute coordinates if it's shorter or forceAbsolutePath is true. - // v-20 -> V0 - // Don't convert if it fits following previous instruction. - // l20 30-10-50 instead of l20 30L20 30 - if ( - params.forceAbsolutePath || ( - absoluteDataStr.length < relativeDataStr.length && - !( - params.negativeExtraSpace && - instruction == prev.instruction && - prev.instruction.charCodeAt(0) > 96 && - absoluteDataStr.length == relativeDataStr.length - 1 && - (data[0] < 0 || /^0\./.test(data[0]) && prev.data[prev.data.length - 1] % 1) - )) - ) { - item.instruction = instruction.toUpperCase(); - item.data = adata; - } + roundData(adata); - prev = item; + var absoluteDataStr = cleanupOutData(adata, params), + relativeDataStr = cleanupOutData(data, params); - return true; + // Convert to absolute coordinates if it's shorter or forceAbsolutePath is true. + // v-20 -> V0 + // Don't convert if it fits following previous instruction. + // l20 30-10-50 instead of l20 30L20 30 + if ( + params.forceAbsolutePath || + (absoluteDataStr.length < relativeDataStr.length && + !( + params.negativeExtraSpace && + instruction == prev.instruction && + prev.instruction.charCodeAt(0) > 96 && + absoluteDataStr.length == relativeDataStr.length - 1 && + (data[0] < 0 || + (/^0\./.test(data[0]) && prev.data[prev.data.length - 1] % 1)) + )) + ) { + item.instruction = instruction.toUpperCase(); + item.data = adata; + } - }); + prev = item; - return path; + return true; + }); + return path; } /** @@ -718,15 +686,24 @@ function convertToMixed(path, params) { * @return {Boolean} output */ function isConvex(data) { - - var center = getIntersection([0, 0, data[2], data[3], data[0], data[1], data[4], data[5]]); - - return center && - (data[2] < center[0] == center[0] < 0) && - (data[3] < center[1] == center[1] < 0) && - (data[4] < center[0] == center[0] < data[0]) && - (data[5] < center[1] == center[1] < data[1]); - + var center = getIntersection([ + 0, + 0, + data[2], + data[3], + data[0], + data[1], + data[4], + data[5], + ]); + + return ( + center && + data[2] < center[0] == center[0] < 0 && + data[3] < center[1] == center[1] < 0 && + data[4] < center[0] == center[0] < data[0] && + data[5] < center[1] == center[1] < data[1] + ); } /** @@ -736,31 +713,27 @@ function isConvex(data) { * @return {Array|undefined} output coordinate of lines' crosspoint */ function getIntersection(coords) { - - // Prev line equation parameters. - var a1 = coords[1] - coords[3], // y1 - y2 - b1 = coords[2] - coords[0], // x2 - x1 - c1 = coords[0] * coords[3] - coords[2] * coords[1], // x1 * y2 - x2 * y1 - - // Next line equation parameters - a2 = coords[5] - coords[7], // y1 - y2 - b2 = coords[6] - coords[4], // x2 - x1 - c2 = coords[4] * coords[7] - coords[5] * coords[6], // x1 * y2 - x2 * y1 - denom = (a1 * b2 - a2 * b1); - - if (!denom) return; // parallel lines havn't an intersection - - var cross = [ - (b1 * c2 - b2 * c1) / denom, - (a1 * c2 - a2 * c1) / -denom - ]; - if ( - !isNaN(cross[0]) && !isNaN(cross[1]) && - isFinite(cross[0]) && isFinite(cross[1]) - ) { - return cross; - } - + // Prev line equation parameters. + var a1 = coords[1] - coords[3], // y1 - y2 + b1 = coords[2] - coords[0], // x2 - x1 + c1 = coords[0] * coords[3] - coords[2] * coords[1], // x1 * y2 - x2 * y1 + // Next line equation parameters + a2 = coords[5] - coords[7], // y1 - y2 + b2 = coords[6] - coords[4], // x2 - x1 + c2 = coords[4] * coords[7] - coords[5] * coords[6], // x1 * y2 - x2 * y1 + denom = a1 * b2 - a2 * b1; + + if (!denom) return; // parallel lines havn't an intersection + + var cross = [(b1 * c2 - b2 * c1) / denom, (a1 * c2 - a2 * c1) / -denom]; + if ( + !isNaN(cross[0]) && + !isNaN(cross[1]) && + isFinite(cross[0]) && + isFinite(cross[1]) + ) { + return cross; + } } /** @@ -773,15 +746,16 @@ function getIntersection(coords) { * @return {Array} output data array */ function strongRound(data) { - for (var i = data.length; i-- > 0;) { - if (data[i].toFixed(precision) != data[i]) { - var rounded = +data[i].toFixed(precision - 1); - data[i] = +Math.abs(rounded - data[i]).toFixed(precision + 1) >= error ? - +data[i].toFixed(precision) : - rounded; - } + for (var i = data.length; i-- > 0; ) { + if (data[i].toFixed(precision) != data[i]) { + var rounded = +data[i].toFixed(precision - 1); + data[i] = + +Math.abs(rounded - data[i]).toFixed(precision + 1) >= error + ? +data[i].toFixed(precision) + : rounded; } - return data; + } + return data; } /** @@ -791,10 +765,10 @@ function strongRound(data) { * @return {Array} output data array */ function round(data) { - for (var i = data.length; i-- > 0;) { - data[i] = Math.round(data[i]); - } - return data; + for (var i = data.length; i-- > 0; ) { + data[i] = Math.round(data[i]); + } + return data; } /** @@ -807,23 +781,21 @@ function round(data) { */ function isCurveStraightLine(data) { + // Get line equation a·x + b·y + c = 0 coefficients a, b (c = 0) by start and end points. + var i = data.length - 2, + a = -data[i + 1], // y1 − y2 (y1 = 0) + b = data[i], // x2 − x1 (x1 = 0) + d = 1 / (a * a + b * b); // same part for all points - // Get line equation a·x + b·y + c = 0 coefficients a, b (c = 0) by start and end points. - var i = data.length - 2, - a = -data[i + 1], // y1 − y2 (y1 = 0) - b = data[i], // x2 − x1 (x1 = 0) - d = 1 / (a * a + b * b); // same part for all points + if (i <= 1 || !isFinite(d)) return false; // curve that ends at start point isn't the case - if (i <= 1 || !isFinite(d)) return false; // curve that ends at start point isn't the case - - // Distance from point (x0, y0) to the line is sqrt((c − a·x0 − b·y0)² / (a² + b²)) - while ((i -= 2) >= 0) { - if (Math.sqrt(Math.pow(a * data[i] + b * data[i + 1], 2) * d) > error) - return false; - } - - return true; + // Distance from point (x0, y0) to the line is sqrt((c − a·x0 − b·y0)² / (a² + b²)) + while ((i -= 2) >= 0) { + if (Math.sqrt(Math.pow(a * data[i] + b * data[i + 1], 2) * d) > error) + return false; + } + return true; } /** @@ -834,12 +806,19 @@ function isCurveStraightLine(data) { */ function makeLonghand(item, data) { - switch (item.instruction) { - case 's': item.instruction = 'c'; break; - case 't': item.instruction = 'q'; break; - } - item.data.unshift(data[data.length - 2] - data[data.length - 4], data[data.length - 1] - data[data.length - 3]); - return item; + switch (item.instruction) { + case 's': + item.instruction = 'c'; + break; + case 't': + item.instruction = 'q'; + break; + } + item.data.unshift( + data[data.length - 2] - data[data.length - 4], + data[data.length - 1] - data[data.length - 3] + ); + return item; } /** @@ -851,7 +830,7 @@ function makeLonghand(item, data) { */ function getDistance(point1, point2) { - return Math.hypot(point1[0] - point2[0], point1[1] - point2[1]); + return Math.hypot(point1[0] - point2[0], point1[1] - point2[1]); } /** @@ -865,15 +844,15 @@ function getDistance(point1, point2) { */ function getCubicBezierPoint(curve, t) { - var sqrT = t * t, - cubT = sqrT * t, - mt = 1 - t, - sqrMt = mt * mt; - - return [ - 3 * sqrMt * t * curve[0] + 3 * mt * sqrT * curve[2] + cubT * curve[4], - 3 * sqrMt * t * curve[1] + 3 * mt * sqrT * curve[3] + cubT * curve[5] - ]; + var sqrT = t * t, + cubT = sqrT * t, + mt = 1 - t, + sqrMt = mt * mt; + + return [ + 3 * sqrMt * t * curve[0] + 3 * mt * sqrT * curve[2] + cubT * curve[4], + 3 * sqrMt * t * curve[1] + 3 * mt * sqrT * curve[3] + cubT * curve[5], + ]; } /** @@ -884,23 +863,34 @@ function getCubicBezierPoint(curve, t) { */ function findCircle(curve) { - var midPoint = getCubicBezierPoint(curve, 1/2), - m1 = [midPoint[0] / 2, midPoint[1] / 2], - m2 = [(midPoint[0] + curve[4]) / 2, (midPoint[1] + curve[5]) / 2], - center = getIntersection([ - m1[0], m1[1], - m1[0] + m1[1], m1[1] - m1[0], - m2[0], m2[1], - m2[0] + (m2[1] - midPoint[1]), m2[1] - (m2[0] - midPoint[0]) - ]), - radius = center && getDistance([0, 0], center), - tolerance = Math.min(arcThreshold * error, arcTolerance * radius / 100); - - if (center && radius < 1e15 && - [1/4, 3/4].every(function(point) { - return Math.abs(getDistance(getCubicBezierPoint(curve, point), center) - radius) <= tolerance; - })) - return { center: center, radius: radius}; + var midPoint = getCubicBezierPoint(curve, 1 / 2), + m1 = [midPoint[0] / 2, midPoint[1] / 2], + m2 = [(midPoint[0] + curve[4]) / 2, (midPoint[1] + curve[5]) / 2], + center = getIntersection([ + m1[0], + m1[1], + m1[0] + m1[1], + m1[1] - m1[0], + m2[0], + m2[1], + m2[0] + (m2[1] - midPoint[1]), + m2[1] - (m2[0] - midPoint[0]), + ]), + radius = center && getDistance([0, 0], center), + tolerance = Math.min(arcThreshold * error, (arcTolerance * radius) / 100); + + if ( + center && + radius < 1e15 && + [1 / 4, 3 / 4].every(function (point) { + return ( + Math.abs( + getDistance(getCubicBezierPoint(curve, point), center) - radius + ) <= tolerance + ); + }) + ) + return { center: center, radius: radius }; } /** @@ -912,11 +902,19 @@ function findCircle(curve) { */ function isArc(curve, circle) { - var tolerance = Math.min(arcThreshold * error, arcTolerance * circle.radius / 100); - - return [0, 1/4, 1/2, 3/4, 1].every(function(point) { - return Math.abs(getDistance(getCubicBezierPoint(curve, point), circle.center) - circle.radius) <= tolerance; - }); + var tolerance = Math.min( + arcThreshold * error, + (arcTolerance * circle.radius) / 100 + ); + + return [0, 1 / 4, 1 / 2, 3 / 4, 1].every(function (point) { + return ( + Math.abs( + getDistance(getCubicBezierPoint(curve, point), circle.center) - + circle.radius + ) <= tolerance + ); + }); } /** @@ -928,10 +926,10 @@ function isArc(curve, circle) { */ function isArcPrev(curve, circle) { - return isArc(curve, { - center: [circle.center[0] + curve[4], circle.center[1] + curve[5]], - radius: circle.radius - }); + return isArc(curve, { + center: [circle.center[0] + curve[4], circle.center[1] + curve[5]], + radius: circle.radius, + }); } /** @@ -943,15 +941,14 @@ function isArcPrev(curve, circle) { */ function findArcAngle(curve, relCircle) { - var x1 = -relCircle.center[0], - y1 = -relCircle.center[1], - x2 = curve[4] - relCircle.center[0], - y2 = curve[5] - relCircle.center[1]; - - return Math.acos( - (x1 * x2 + y1 * y2) / - Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2)) - ); + var x1 = -relCircle.center[0], + y1 = -relCircle.center[1], + x2 = curve[4] - relCircle.center[0], + y2 = curve[5] - relCircle.center[1]; + + return Math.acos( + (x1 * x2 + y1 * y2) / Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2)) + ); } /** @@ -963,11 +960,11 @@ function findArcAngle(curve, relCircle) { */ function data2Path(params, pathData) { - return pathData.reduce(function(pathString, item) { - var strData = ''; - if (item.data) { - strData = cleanupOutData(roundData(item.data.slice()), params); - } - return pathString + item.instruction + strData; - }, ''); + return pathData.reduce(function (pathString, item) { + var strData = ''; + if (item.data) { + strData = cleanupOutData(roundData(item.data.slice()), params); + } + return pathString + item.instruction + strData; + }, ''); } diff --git a/test/plugins/convertPathData.03.svg b/test/plugins/convertPathData.03.svg index 1a574b4a4..c3253bd30 100644 --- a/test/plugins/convertPathData.03.svg +++ b/test/plugins/convertPathData.03.svg @@ -15,8 +15,9 @@ - + + @@@ @@ -40,4 +41,5 @@ +