Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<path>` elements
- Do *not* use transforms
- Initial (simple) support for the `translate` transformation is included. Others are not supported yet.

Pixo includes experimental support for `<circle>`, `<polygon>`, and `<rect>` elements.

Expand Down
95 changes: 91 additions & 4 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ const circleToPath = (el, i) => {
return {
type: 'path',
properties: {
d
d,
transform: el.properties.transform
}
}
}
Expand All @@ -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 => {
Expand All @@ -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<args.length; c+=currentCommand.length) {
result.push(...currentCommand.conversion(args.slice(c, c+currentCommand.length)))
}
}
result.push(path.charAt(i))
currentCommand = pathGrammar[path.charAt(i).toLowerCase()]
commandIndex = i
}
}
return result.join(' ')
}

const translatePath = (path, ax, ay) => 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
Expand All @@ -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 = ({
Expand Down
32 changes: 32 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ const rect = {
</svg>`
}

const translatedRect = {
name: 'Rect',
content: `<svg viewBox='0 0 32 32'>
<rect
x='2'
y='9'
width='28'
height='14'
transform='translate(40 20)'
/>
</svg>`
}

const svgs = [
basic,
multipath,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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: `<svg viewBox='0 0 32 32'>
<path d="M 0 0 L 56 32" transform="matrix(1 2 3 4 5 6)" />
</svg>`
}
])
t.is(console.log.calledOnce, true)
console.log.restore()
})

test('ignores elements in defs and clipPath elements', t => {
const svg = pixo.parse({
name: 'Defs',
Expand Down
38 changes: 37 additions & 1 deletion test.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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␊
}) => (␊
<svg␊
{...props}␊
viewBox='0 0 32 32'␊
width={size}␊
height={size}␊
fill={color}␊
>␊
<path d='M 42 29 H 70 V 43 H 42 z' />␊
</svg>␊
)␊
RectIcon.displayName = 'RectIcon'␊
RectIcon.defaultProps = {␊
size: 24,␊
color: 'currentcolor'␊
}␊
export default RectIcon`,
name: 'Rect',
},
]
Binary file modified test.js.snap
Binary file not shown.