diff --git a/README.md b/README.md index b35b4d5..d8dccb4 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Each SVG icon **must** conform to the following: - Use a square `viewBox` attribute, preferably `0 0 24 24` - Only use a single color (e.g. black) - For best results, only use `` elements -- Do *not* use transforms +- Initial (simple) support for the `translate` transformation is included. Others are not supported yet. Pixo includes experimental support for ``, ``, and `` elements. diff --git a/lib/index.js b/lib/index.js index 73f11a2..244dc4e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -51,7 +51,8 @@ const circleToPath = (el, i) => { return { type: 'path', properties: { - d + d, + transform: el.properties.transform } } } @@ -75,7 +76,7 @@ const polygonToPath = el => { if (i === 0) return [ a, x, y ].join(' ') return [ a, 'L', x, y ].join(' ') }, 'M') + ' z' - return { type: 'path', properties: { d } } + return { type: 'path', properties: { d, transform: el.properties.transform } } } const rectToPath = el => { @@ -91,7 +92,92 @@ const rectToPath = el => { 'H', x, 'z' ].join(' ') - return { type: 'path', properties: { d } } + return { type: 'path', properties: { d, transform: el.properties.transform } } +} + +const toFloat = (val = 0) => parseFloat(val) || 0 + +const grammarSegmentToTransform = (grammar, xTransform, yTransform) => { + const split = grammar.split(' ') + return { + length: split.length, + conversion: arr => split.map((value, i, config) => { + if(value === 'x') { + return i + 1 < config.length && config[i+1] === 'y' ? + xTransform(arr[ i ], arr[ i + 1 ]): + xTransform(arr[ i ]) + } + if(value === 'y') { + return i > 0 && config[i-1] === 'x' ? + yTransform(arr[ i ], arr[ i - 1 ]): + yTransform(arr[ i ]) + } + return arr[i] + }) + } +} + +const grammarToTransform = (grammar, xTransform, yTransform) => + Object.keys(grammar).reduce((acc, key) => Object.assign(acc, { [key]: grammarSegmentToTransform(grammar[key], xTransform, yTransform)}), {}) + +const applyTransformToPath = (path, xTransform, yTransform) => { + const pathGrammar = grammarToTransform({ + z: '', + h: 'x', + v: 'y', + m: 'x y', + l: 'x y', + t: 'x y', + s: 'x y x y', + q: 'x y x y', + c: 'x y x y x y', + a: 'rx ry ra arc sweep x y' + }, xTransform, yTransform) + let currentCommand = 'm' + let commandIndex = 0 + const result = [] + for(let i =0; i < path.length; i++){ + if(pathGrammar[path.charAt(i).toLowerCase()]) { + if(i - commandIndex > 1){ + const args = path.substring(commandIndex + 1, i).trim().split(' ').map(toFloat) + for (c=0; c applyTransformToPath(path, x => x + ax, y => y + ay) +const scalePath = (path, a, b) => applyTransformToPath(path, x => x * a, y => y * (b || a)) + +const transformPath = ({ d, transform }) => { + const regex = /(translate)\(((?:[-.0-9]+[' '|',']*){2})\)|(rotate)\(((?:[-.0-9]+[' '|',']*){1,3})\)|(scale)\(((?:[-.0-9]+[' '|',']*){1,2})\)|(skewX)\(([-.0-9]+)\)|(skewY)\(([-.0-9]+)\)|(matrix)\(((?:[-.0-9]+[' '|',']*){6})\)/g + if(!transform) { + return d + } + const transformFuncs = { + translate: translatePath, + scale: scalePath + } + const transforms = [] + let match + while(match = regex.exec(transform)) { + const [ all, transform, args ] = match + if(transformFuncs[transform]) { + transforms.push([transformFuncs[transform], ...args.split(' ').map(toFloat)]) + } + else { + console.log(`${transform} is not currently a supported transformation`) + } + } + return transforms.reduce((acc, [ transform, ...args ]) => + transform.apply(null, [ acc, ...args ]), + d) } const getPath = data => data.children @@ -102,7 +188,8 @@ const getPath = data => data.children .map(rectToPath) .filter(child => child.type === 'path') .filter(child => !child.properties.fill || child.properties.fill !== 'none') - .map(child => child.properties.d) + .map(child => ({ d: child.properties.d, transform: child.properties.transform })) + .map(transformPath) .join(' ') const parse = ({ diff --git a/test.js b/test.js index 0c559b7..a9aaa13 100644 --- a/test.js +++ b/test.js @@ -65,6 +65,19 @@ const rect = { ` } +const translatedRect = { + name: 'Rect', + content: ` + + ` +} + const svgs = [ basic, multipath, @@ -129,6 +142,11 @@ test('handles rect elements', t => { t.snapshot(components) }) +test('handles translations', t => { + const components = pixo([ translatedRect ]) + t.snapshot(components) +}) + test('polygonToPath converts polygon elements to path', t => { const path = pixo.polygonToPath({ type: 'polygon', @@ -178,6 +196,20 @@ test('warns when an unsupported element is used', t => { console.log.restore() }) +test('warns when an unsupported transform is used', t => { + sinon.spy(console, 'log') + pixo([ + { + name: 'Unsupported', + content: ` + + ` + } + ]) + t.is(console.log.calledOnce, true) + console.log.restore() +}) + test('ignores elements in defs and clipPath elements', t => { const svg = pixo.parse({ name: 'Defs', diff --git a/test.js.md b/test.js.md index 7df9b3c..3070db4 100644 --- a/test.js.md +++ b/test.js.md @@ -910,4 +910,40 @@ Generated by [AVA](https://ava.li). export default RectIcon`, name: 'Rect', }, - ] + + +## handles translations + +> Snapshot 1 + + [ + { + content: `import React from 'react'␊ + ␊ + const RectIcon = ({␊ + size,␊ + color,␊ + ...props␊ + }) => (␊ + ␊ + ␊ + ␊ + )␊ + ␊ + RectIcon.displayName = 'RectIcon'␊ + ␊ + RectIcon.defaultProps = {␊ + size: 24,␊ + color: 'currentcolor'␊ + }␊ + ␊ + export default RectIcon`, + name: 'Rect', + }, + ] \ No newline at end of file diff --git a/test.js.snap b/test.js.snap index 50e4637..306b941 100644 Binary files a/test.js.snap and b/test.js.snap differ