diff --git a/plugins/_path.js b/plugins/_path.js index 2ee5392c..3913ba2c 100644 --- a/plugins/_path.js +++ b/plugins/_path.js @@ -1,8 +1,8 @@ 'use strict'; var regPathInstructions = /([MmLlHhVvCcSsQqTtAaZz])\s*/, - regPathData = /[\-+]?\d*\.?\d+([eE][\-+]?\d+)?/g, - regNumericValues = /[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/, + regPathData = /[-+]?(?:\d*\.\d+|\d+\.?)([eE][-+]?\d+)?/g, + regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/, transform2js = require('./_transforms').transform2js, transformsMultiply = require('./_transforms').transformsMultiply, collections = require('./_collections.js'), @@ -18,85 +18,69 @@ var regPathInstructions = /([MmLlHhVvCcSsQqTtAaZz])\s*/, * @param {Object} params plugin params * @return {Array} output array */ -exports.path2js = function(pathString) { +exports.path2js = function(path) { + if (path.pathJS) return path.pathJS; - // JS representation of the path data - var path = [], - // current instruction context - instruction; + var paramsLength = { // Number of parameters of every path command + H: 1, V: 1, M: 2, L: 2, T: 2, Q: 4, S: 4, C: 6, A: 7, + h: 1, v: 1, m: 2, l: 2, t: 2, q: 4, s: 4, c: 6, a: 7 + }, + pathData = [], // JS representation of the path data + instruction, // current instruction context + startMoveto = false; // splitting path string into array like ['M', '10 50', 'L', '20 30'] - pathString.split(regPathInstructions).forEach(function(data) { - if (data) { - // instruction item - if (regPathInstructions.test(data)) { - instruction = data; + path.attr('d').value.split(regPathInstructions).forEach(function(data) { + if (!data) return; + if (!startMoveto) { + if (data == 'M' || data == 'm') { + startMoveto = true; + } else return; + } - // z - instruction w/o data - if ('Zz'.indexOf(instruction) > -1) { - path.push({ - instruction: 'z' - }); - } - // data item - } else { + // instruction item + if (regPathInstructions.test(data)) { + instruction = data; - data = data.trim().match(regPathData); + // z - instruction w/o data + if (instruction == 'Z' || instruction == 'z') { + pathData.push({ + instruction: 'z' + }); + } + // data item + } else { + data = data.match(regPathData); + if (!data) return; - if (data) { + data = data.map(Number); - var index = 0, - pair = 2; - - data = data.map(function(str) { - return +str; - }); - - // deal with very first 'Mm' and multiple points data - if ('Mm'.indexOf(instruction) > -1) { - - path.push({ - instruction: instruction, - data: data.slice(index, index + pair) - }); - - index += pair; - - if (data.length) { - instruction = instruction === instruction.toLowerCase() ? 'l' : 'L'; - } - - } - - if ('HhVv'.indexOf(instruction) > -1) { - pair = 1; - } else if ('LlTt'.indexOf(instruction) > -1) { - pair = 2; - } else if ('QqSs'.indexOf(instruction) > -1) { - pair = 4; - } else if ('Cc'.indexOf(instruction) > -1) { - pair = 6; - } else if ('Aa'.indexOf(instruction) > -1) { - pair = 7; - } - - while(index < data.length) { - path.push({ - instruction: instruction, - data: data.slice(index, index + pair) - }); - - index += pair; - } - - } + // Subsequent moveto pairs of coordinates are threated as implicit lineto commands + // http://www.w3.org/TR/SVG/paths.html#PathDataMovetoCommands + if (instruction == 'M' || instruction == 'm') { + pathData.push({ + instruction: pathData.length == 0 ? 'M' : instruction, + data: data.splice(0, 2) + }); + instruction = instruction == 'M' ? 'L' : 'l'; + } + for (var pair = paramsLength[instruction]; data.length;) { + pathData.push({ + instruction: instruction, + data: data.splice(0, pair) + }); } } }); - return path; + // First moveto is actually absolute. Subsequent coordinates were separated above. + if (pathData.length && pathData[0].instruction == 'm') { + pathData[0].instruction = 'M'; + } + path.pathJS = pathData; + return pathData; }; /** @@ -105,79 +89,76 @@ exports.path2js = function(pathString) { * @param {Array} data input data * @return {Array} output data */ -exports.relative2absolute = function(data) { - +var relative2absolute = exports.relative2absolute = function(data) { var currentPoint = [0, 0], subpathPoint = [0, 0], i; - data.forEach(function(item) { + data = data.map(function(item) { - if (item.instruction === 'M') { + var instruction = item.instruction, + itemData = item.data && item.data.slice(); - currentPoint = item.data.slice(-2); - subpathPoint = item.data.slice(-2); + if (instruction == 'M') { - } else if ('mlcsqta'.indexOf(item.instruction) > -1) { + set(currentPoint, itemData); + set(subpathPoint, itemData); - for (i = 0; i < item.data.length; i++) { - if (i % 2 === 0) { - item.data[i] += currentPoint[0]; - } else { - item.data[i] += currentPoint[1]; - } + } else if ('mlcsqt'.indexOf(instruction) > -1) { - if (i > 0) { - var index = i + 1; + for (i = 0; i < itemData.length; i++) { + itemData[i] += currentPoint[i % 2]; + } + set(currentPoint, itemData); - if ('mlt'.indexOf(item.instruction) > -1 && index % 2 === 0) { - currentPoint[0] = item.data[i - 1]; - currentPoint[1] = item.data[i]; - } else if ('qs'.indexOf(item.instruction) > -1 && index % 4 === 0) { - currentPoint[0] = item.data[i - 1]; - currentPoint[1] = item.data[i]; - } else if (item.instruction === 'c' && index % 6 === 0) { - currentPoint[0] = item.data[i - 1]; - currentPoint[1] = item.data[i]; - } else if (item.instruction === 'a' && index % 7 === 0) { - currentPoint[0] = item.data[i - 1]; - currentPoint[1] = item.data[i]; - } - } + if (instruction == 'm') { + set(subpathPoint, itemData); } - if (item.instruction === 'm') { - subpathPoint = item.data.slice(-2); - } + } else if (instruction == 'a') { - } else if (item.instruction === 'h') { + itemData[5] += currentPoint[0]; + itemData[6] += currentPoint[1]; + set(currentPoint, itemData); - for (i = 0; i < item.data.length; i++) { - item.data[i] += currentPoint[0]; - } + } else if (instruction == 'h') { - currentPoint[0] = item.data[item.data.length - 1]; + itemData[0] += currentPoint[0]; + currentPoint[0] = itemData[0]; - } else if (item.instruction === 'v') { + } else if (instruction == 'v') { - for (i = 0; i < item.data.length; i++) { - item.data[i] += currentPoint[1]; - } + itemData[0] += currentPoint[1]; + currentPoint[1] = itemData[0]; - currentPoint[1] = item.data[item.data.length - 1]; + } else if ('MZLCSQTA'.indexOf(instruction) > -1) { - } else { + set(currentPoint, itemData); - currentPoint = subpathPoint; + } else if (instruction == 'H') { + + currentPoint[0] = itemData[0]; + + } else if (instruction == 'V') { + + currentPoint[1] = itemData[0]; + + } else if (instruction == 'z') { + + set(currentPoint, subpathPoint); } - item.instruction = item.instruction.toUpperCase(); + return instruction == 'z' ? + { instruction: 'z' } : + { + instruction: instruction.toUpperCase(), + data: itemData + }; }); return data; - }; /** @@ -241,27 +222,26 @@ exports.applyTransforms = function(elem, path, applyTransformsStroked, floatPrec // If an 'a' command can't be transformed directly, convert path to curves. if (!splittedMatrix.isSimple && path.some(function(i) { return i.instruction == 'a' })) { var prev; - path = path.reduce(function(newPath, item){ + path.forEach(function(item, index, path){ if (item.instruction == 'a') { var curves = a2c.apply(0, [0, 0].concat(item.data)), + items = [], curveData; while ((curveData = curves.splice(0,6)).length) { - item = { + var base = prev.coords; + items.push(prev = { instruction: 'c', data: curveData, + coords: [base[0] + item.data[4], base[1] + item.data[5]], base: prev.coords - }; - item.coords = [item.base[0] + item.data[4], item.base[1] + item.data[5]]; - prev = item; - newPath.push(item); + }); } + path.splice.apply(path, [index, 1].concat(items)); } else { - newPath.push(item); if (prev) item.base = prev.coords; prev = item; } - return newPath; - }, []); + }); } path.forEach(function(pathItem) { @@ -291,8 +271,8 @@ exports.applyTransforms = function(elem, path, applyTransformsStroked, floatPrec // then apply it only to the first absoluted M newPoint = transformPoint(matrix.data, pathItem.data[0], pathItem.data[1]); - pathItem.data[0] = pathItem.coords[0] = newPoint[0]; - pathItem.data[1] = pathItem.coords[1] = newPoint[1]; + set(pathItem.data, newPoint); + set(pathItem.coords, newPoint); // clear translate() data from transform matrix matrix.data[4] = 0; @@ -553,21 +533,63 @@ function computeQuadraticFirstDerivativeRoot(a, b, c) { * @param {Object} params plugin params * @return {String} output path string */ -exports.js2path = function(path, params) { +exports.js2path = function(path, data, params) { - // output path data string - var pathString = ''; + path.pathJS = data; - path.forEach(function(item) { + if (params.collapseRepeated) { + data = collapseRepeated(data); + } - pathString += item.instruction + (item.data ? cleanupOutData(item.data, params) : ''); - - }); - - return pathString; + path.attr('d').value = data.reduce(function(pathString, item) { + return pathString += item.instruction + (item.data ? cleanupOutData(item.data, params) : ''); + }, ''); }; +/** + * Collapse repeated instructions data + * + * @param {Array} path input path data + * @return {Array} output path data + */ +function collapseRepeated(data) { + + var prev, + prevIndex; + + // copy an array and modifieds item to keep original data untouched + data = data.reduce(function(newPath, item) { + if ( + prev && item.data && + item.instruction == prev.instruction + ) { + // concat previous data with current + prev = newPath[prevIndex] = { + instruction: prev.instruction, + data: prev.data.concat(item.data), + coords: item.coords, + base: prev.base + } + } else { + newPath.push(item); + prev = item; + prevIndex = newPath.length - 1; + } + + return newPath; + }, []); + + return data; + +} + +function set(dest, source) { + dest[0] = source[source.length - 2]; + dest[1] = source[source.length - 1]; + return dest; +} + /* Based on code from Snap.svg (Apache 2 license). http://snapsvg.io/ * Thanks to Dmitry Baranovskiy for his great work! */ diff --git a/plugins/convertPathData.js b/plugins/convertPathData.js index 81d5e5df..250f4ecb 100644 --- a/plugins/convertPathData.js +++ b/plugins/convertPathData.js @@ -51,7 +51,7 @@ exports.fn = function(item, params) { error = precision !== false ? +Math.pow(.1, precision).toFixed(precision) : 1e-2; hasMarkerMid = item.hasAttr('marker-mid'); - var data = path2js(item.attr('d').value); + var data = path2js(item); // TODO: get rid of functions returns if (data.length) { @@ -63,17 +63,11 @@ exports.fn = function(item, params) { data = filters(data, params); - if (params.collapseRepeated) { - data = collapseRepeated(data, params); - } - if (params.utilizeAbsolute) { data = convertToMixed(data, params); } - item.pathJS = data; - - item.attr('d').value = js2path(data, params); + js2path(item, data, params); } } @@ -91,7 +85,6 @@ function convertToRelative(path) { var point = [0, 0], subpathPoint = [0, 0], - mM = false, baseItem; path.forEach(function(item, index) { @@ -106,18 +99,12 @@ function convertToRelative(path) { // recalculate current point if ('mcslqta'.indexOf(instruction) > -1) { - var newPoint = data.slice(-2); - - point[0] += newPoint[0]; - point[1] += newPoint[1]; + point[0] += data[data.length - 2]; + point[1] += data[data.length - 1]; if (instruction === 'm') { - if (index === 0) { - instruction = 'M'; - mM = true; - } - - subpathPoint = point.slice(-2); + subpathPoint[0] = point[0]; + subpathPoint[1] = point[1]; baseItem = item; } @@ -134,20 +121,16 @@ function convertToRelative(path) { // convert absolute path data coordinates to relative // if "M" was not transformed from "m" // M → m - if ( - instruction === 'M' && - (!mM || index > 0) - ) { + if (instruction === 'M') { if (index > 0) instruction = 'm'; data[0] -= point[0]; data[1] -= point[1]; - point[0] += data[0]; - point[1] += data[1]; + subpathPoint[0] = point[0] += data[0]; + subpathPoint[1] = point[1] += data[1]; - subpathPoint = point.slice(-2); baseItem = item; } @@ -241,11 +224,12 @@ function convertToRelative(path) { } // !data === z, reset current point - else { - if(baseItem) + else if (instruction == 'z') { + if (baseItem) { item.coords = baseItem.coords; - point = subpathPoint; - mM = false; + } + point[0] = subpathPoint[0]; + point[1] = subpathPoint[1]; } item.base = index > 0 ? path[index - 1].coords : [0, 0]; @@ -267,7 +251,7 @@ function filters(path, params) { var relSubpoint = [0, 0], pathBase = [0, 0], - prev; + prev = {}; path = path.filter(function(item, index) { @@ -279,7 +263,7 @@ function filters(path, params) { if (instruction === 's') { var sdata = [0, 0].concat(data); - if (prev && 'cs'.indexOf(prev.instruction) > -1) { + if ('cs'.indexOf(prev.instruction) > -1) { var pdata = prev.data, n = pdata.length; @@ -351,7 +335,6 @@ function filters(path, params) { // q else if ( - prev && instruction === 'q' && isCurveStraightLine( [ 0, data[0], data[2] ], @@ -372,7 +355,6 @@ function filters(path, params) { // q (original) + t if ( - prev && prev.original && prev.original.instruction === 'q' ) { @@ -389,7 +371,7 @@ function filters(path, params) { } // [^qt] + t - else if (!prev || 'qt'.indexOf(prev.instruction) === -1) { + else if ('qt'.indexOf(prev.instruction) < 0) { instruction = 'l'; data = data.slice(-2); } @@ -422,8 +404,30 @@ function filters(path, params) { } } + // 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) == (item.data[0] >= 0) + )) { + prev.data[0] += data[0]; + if (instruction != 'h' && instruction != 'v') { + prev.data[1] += data[1]; + } + prev.coords = item.coords; + if (prev.original) prev.original = null; + path[index] = prev; + return false; + } + // convert curves into smooth shorthands - if (params.curveSmoothShorthands && prev) { + if (params.curveSmoothShorthands && prev.instruction) { // curveto if (instruction === 'c') { @@ -523,6 +527,8 @@ function filters(path, params) { // z resets coordinates relSubpoint[0] = pathBase[0]; relSubpoint[1] = pathBase[1]; + if (prev.instruction == 'z') return false; + prev = item; } @@ -581,74 +587,15 @@ function convertToMixed(path, params) { if ( absoluteDataStr.length < relativeDataStr.length && !( - params.negativeExtraSpace && instruction == prev.instruction && + params.negativeExtraSpace && + instruction == prev.instruction && + prev.instruction.charCodeAt(0) > 96 && absoluteDataStr.length == relativeDataStr.length - 1 && (data[0] < 0 || 0 < data[0] && data[0] < 1 && prev.data[prev.data.length - 1] % 1) ) ) { - if (instruction.toUpperCase() != prev.instruction) { - item.instruction = instruction.toUpperCase(); - item.data = adata; - } else { - prev.data = prev.data.concat(adata); - prev.coords = item.coords; - path[index] = prev; - return false; - } - } else if (instruction == prev.instruction) { - prev.data = prev.data.concat(data); - prev.coords = item.coords; - path[index] = prev; - return false; - } - - prev = item; - - return true; - - }); - - return path; - -} - -/** - * Collapse repeated instructions data - * - * @param {Array} path input path data - * @return {Array} output path data - */ -function collapseRepeated(path, params) { - - var prev; - - path = path.filter(function(item) { - - if ( - !hasMarkerMid && - prev && - item.instruction === prev.instruction && - ( - 'Mmz'.indexOf(item.instruction) > -1 || - 'hv'.indexOf(item.instruction) > -1 && (prev.data[0] >= 0) == (item.data[0] >= 0) || - !params.utilizeAbsolute - ) - ) { - // increase previous h or v data with current - if ('hv'.indexOf(item.instruction) > -1) { - prev.data[0] += item.data[0]; - } else if (item.instruction.toLowerCase() === 'm') { - prev.data[0] += item.data[0]; - prev.data[1] += item.data[1]; - // concat previous data with current if it is not z - } else if (item.data) { - prev.data = prev.data.concat(item.data); - } - prev.coords = item.coords; - - // filter out current item - return false; - + item.instruction = instruction.toUpperCase(); + item.data = adata; } prev = item; diff --git a/plugins/mergePaths.js b/plugins/mergePaths.js index 7dabd584..7612ad06 100644 --- a/plugins/mergePaths.js +++ b/plugins/mergePaths.js @@ -4,8 +4,14 @@ exports.type = 'perItem'; exports.active = true; +exports.params = { + collapseRepeated: true, + leadingZero: true, + negativeExtraSpace: true +}; + var path2js = require('./_path.js').path2js, - relative2absolute = require('./_path.js').relative2absolute; + js2path = require('./_path.js').js2path; /** * Merge multiple Paths into one. @@ -13,13 +19,13 @@ var path2js = require('./_path.js').path2js, * @param {Object} item current iteration item * @return {Boolean} if false, item will be filtered out * - * @author Kir Belevich + * @author Kir Belevich, Lev Solntsev */ -exports.fn = function(item) { +exports.fn = function(item, params) { if (!item.isElem() || item.isEmpty()) return; - var prevContentItem, + var prevContentItem = null, prevContentItemKeys = null; item.content = item.content.filter(function(contentItem) { @@ -31,7 +37,7 @@ exports.fn = function(item) { contentItem.hasAttr('d') ) { - if (prevContentItemKeys == null) { + if (!prevContentItemKeys) { prevContentItemKeys = Object.keys(prevContentItem.attrs); } @@ -41,15 +47,12 @@ exports.fn = function(item) { return key == 'd' || prevContentItem.hasAttr(key) && prevContentItem.attr(key).value == contentItem.attr(key).value; - }); + }), + prevPathJS = path2js(prevContentItem), + curPathJS = path2js(contentItem); if (equalData) { - var prevPathJS = prevContentItem.pathJS; - if (prevContentItem.pathJS) { - prevPathJS.push.apply(prevPathJS, contentItem.pathJS); - } - - prevContentItem.attr('d').value += contentItem.attr('d').value.replace(/m/i, 'M'); + js2path(prevContentItem, prevPathJS.concat(curPathJS), params); return false; } } diff --git a/plugins/transformsWithOnePath.js b/plugins/transformsWithOnePath.js index b11ec986..f390dab3 100644 --- a/plugins/transformsWithOnePath.js +++ b/plugins/transformsWithOnePath.js @@ -32,11 +32,13 @@ exports.params = { negativeExtraSpace: true }; -var relative2absolute = require('./_path.js').relative2absolute, - computeCubicBoundingBox = require('./_path.js').computeCubicBoundingBox, - computeQuadraticBoundingBox = require('./_path.js').computeQuadraticBoundingBox, - applyTransforms = require('./_path.js').applyTransforms, - js2path = require('./_path.js').js2path, +var _path = require('./_path.js'), + relative2absolute = _path.relative2absolute, + computeCubicBoundingBox = _path.computeCubicBoundingBox, + computeQuadraticBoundingBox = _path.computeQuadraticBoundingBox, + applyTransforms = _path.applyTransforms, + js2path = _path.js2path, + path2js = _path.path2js, EXTEND = require('whet.extend'); exports.fn = function(data, params) { @@ -52,7 +54,7 @@ exports.fn = function(data, params) { var svgElem = item, pathElem = svgElem.content[0], // get absoluted Path data - path = relative2absolute(EXTEND(true, [], pathElem.pathJS)), + path = relative2absolute(EXTEND(true, [], path2js(pathElem))), xs = [], ys = [], cubicСontrolPoint = [0, 0], @@ -312,7 +314,7 @@ exports.fn = function(data, params) { }); // save new - pathElem.attr('d').value = js2path(path, params); + js2path(pathElem, path, params); } } diff --git a/test/plugins/convertPathData.03.svg b/test/plugins/convertPathData.03.svg index d7a2b184..09358251 100644 --- a/test/plugins/convertPathData.03.svg +++ b/test/plugins/convertPathData.03.svg @@ -19,7 +19,7 @@ @@@ - + diff --git a/test/plugins/convertPathData.07.svg b/test/plugins/convertPathData.07.svg index 74121dd6..9c9451ae 100644 --- a/test/plugins/convertPathData.07.svg +++ b/test/plugins/convertPathData.07.svg @@ -1,21 +1,21 @@ - - - - - - + + + + + + @@@ - - - - - - + + + + + + diff --git a/test/plugins/convertPathData.12.svg b/test/plugins/convertPathData.12.svg index 113c7940..21801dc8 100644 --- a/test/plugins/convertPathData.12.svg +++ b/test/plugins/convertPathData.12.svg @@ -9,6 +9,7 @@ + @@ -27,6 +28,7 @@ + diff --git a/test/plugins/mergePaths.01.svg b/test/plugins/mergePaths.01.svg index 454f4ffc..64bf585e 100644 --- a/test/plugins/mergePaths.01.svg +++ b/test/plugins/mergePaths.01.svg @@ -1,7 +1,7 @@ - + @@ -11,7 +11,7 @@ @@@ - + - + diff --git a/test/plugins/mergePaths.02.svg b/test/plugins/mergePaths.02.svg index 77a4e88e..56039dde 100644 --- a/test/plugins/mergePaths.02.svg +++ b/test/plugins/mergePaths.02.svg @@ -18,8 +18,8 @@ @@@ - + - - + +