mirror of
https://github.com/iconify/iconify.git
synced 2025-02-06 14:08:40 +00:00
Fix incorrect transformations for negative rotation, for merging icon and customisation rotations, better code for merging customisations
This commit is contained in:
parent
8da393c816
commit
46ae993b95
@ -1,7 +1,7 @@
|
||||
import { replaceIDs } from './ids';
|
||||
import { calculateSize } from './calc-size';
|
||||
import { fullIcon, IconifyIcon } from '../icon';
|
||||
import { fullCustomisations } from '../customisations';
|
||||
import { defaults, mergeCustomisations } from '../customisations';
|
||||
import type { IconifyIconCustomisations } from '../customisations';
|
||||
import { iconToSVG } from '.';
|
||||
import type { IconifyIconBuildResult } from '.';
|
||||
@ -29,6 +29,9 @@ export const builderFunctions: IconifyBuilderFunctions = {
|
||||
replaceIDs,
|
||||
calculateSize,
|
||||
buildIcon: (icon, customisations) => {
|
||||
return iconToSVG(fullIcon(icon), fullCustomisations(customisations));
|
||||
return iconToSVG(
|
||||
fullIcon(icon),
|
||||
mergeCustomisations(defaults, customisations)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@ -66,7 +66,7 @@ interface ViewBox {
|
||||
* Get SVG attributes and content from icon + customisations
|
||||
*
|
||||
* Does not generate style to make it compatible with frameworks that use objects for style, such as React.
|
||||
* Instead, it generates verticalAlign value that should be added to style.
|
||||
* Instead, it generates 'inline' value. If true, rendering engine should add verticalAlign: -0.125em to icon.
|
||||
*
|
||||
* Customisations should be normalised by platform specific parser.
|
||||
* Result should be converted to <svg> by platform specific parser.
|
||||
@ -84,80 +84,102 @@ export function iconToSVG(
|
||||
height: icon.height,
|
||||
};
|
||||
|
||||
// Apply transformations
|
||||
const transformations: string[] = [];
|
||||
const hFlip = customisations.hFlip !== icon.hFlip;
|
||||
const vFlip = customisations.vFlip !== icon.vFlip;
|
||||
let rotation = customisations.rotate + icon.rotate;
|
||||
// Body
|
||||
let body = icon.body;
|
||||
|
||||
if (hFlip) {
|
||||
if (vFlip) {
|
||||
rotation += 2;
|
||||
} else {
|
||||
// Horizontal flip
|
||||
// Apply transformations
|
||||
[icon, customisations].forEach((props) => {
|
||||
const transformations: string[] = [];
|
||||
const hFlip = props.hFlip;
|
||||
const vFlip = props.vFlip;
|
||||
let rotation = props.rotate;
|
||||
|
||||
// Icon is flipped first, then rotated
|
||||
if (hFlip) {
|
||||
if (vFlip) {
|
||||
rotation += 2;
|
||||
} else {
|
||||
// Horizontal flip
|
||||
transformations.push(
|
||||
'translate(' +
|
||||
(box.width + box.left) +
|
||||
' ' +
|
||||
(0 - box.top) +
|
||||
')'
|
||||
);
|
||||
transformations.push('scale(-1 1)');
|
||||
box.top = box.left = 0;
|
||||
}
|
||||
} else if (vFlip) {
|
||||
// Vertical flip
|
||||
transformations.push(
|
||||
'translate(' +
|
||||
(box.width + box.left) +
|
||||
(0 - box.left) +
|
||||
' ' +
|
||||
(0 - box.top) +
|
||||
(box.height + box.top) +
|
||||
')'
|
||||
);
|
||||
transformations.push('scale(-1 1)');
|
||||
transformations.push('scale(1 -1)');
|
||||
box.top = box.left = 0;
|
||||
}
|
||||
} else if (vFlip) {
|
||||
// Vertical flip
|
||||
transformations.push(
|
||||
'translate(' + (0 - box.left) + ' ' + (box.height + box.top) + ')'
|
||||
);
|
||||
transformations.push('scale(1 -1)');
|
||||
box.top = box.left = 0;
|
||||
}
|
||||
|
||||
let tempValue: number;
|
||||
rotation = rotation % 4;
|
||||
switch (rotation) {
|
||||
case 1:
|
||||
// 90deg
|
||||
tempValue = box.height / 2 + box.top;
|
||||
transformations.unshift(
|
||||
'rotate(90 ' + tempValue + ' ' + tempValue + ')'
|
||||
);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// 180deg
|
||||
transformations.unshift(
|
||||
'rotate(180 ' +
|
||||
(box.width / 2 + box.left) +
|
||||
' ' +
|
||||
(box.height / 2 + box.top) +
|
||||
')'
|
||||
);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// 270deg
|
||||
tempValue = box.width / 2 + box.left;
|
||||
transformations.unshift(
|
||||
'rotate(-90 ' + tempValue + ' ' + tempValue + ')'
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (rotation % 2 === 1) {
|
||||
// Swap width/height and x/y for 90deg or 270deg rotation
|
||||
if (box.left !== 0 || box.top !== 0) {
|
||||
tempValue = box.left;
|
||||
box.left = box.top;
|
||||
box.top = tempValue;
|
||||
let tempValue: number;
|
||||
if (rotation < 0) {
|
||||
rotation -= Math.floor(rotation / 4) * 4;
|
||||
}
|
||||
if (box.width !== box.height) {
|
||||
tempValue = box.width;
|
||||
box.width = box.height;
|
||||
box.height = tempValue;
|
||||
rotation = rotation % 4;
|
||||
switch (rotation) {
|
||||
case 1:
|
||||
// 90deg
|
||||
tempValue = box.height / 2 + box.top;
|
||||
transformations.unshift(
|
||||
'rotate(90 ' + tempValue + ' ' + tempValue + ')'
|
||||
);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// 180deg
|
||||
transformations.unshift(
|
||||
'rotate(180 ' +
|
||||
(box.width / 2 + box.left) +
|
||||
' ' +
|
||||
(box.height / 2 + box.top) +
|
||||
')'
|
||||
);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// 270deg
|
||||
tempValue = box.width / 2 + box.left;
|
||||
transformations.unshift(
|
||||
'rotate(-90 ' + tempValue + ' ' + tempValue + ')'
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (rotation % 2 === 1) {
|
||||
// Swap width/height and x/y for 90deg or 270deg rotation
|
||||
if (box.left !== 0 || box.top !== 0) {
|
||||
tempValue = box.left;
|
||||
box.left = box.top;
|
||||
box.top = tempValue;
|
||||
}
|
||||
if (box.width !== box.height) {
|
||||
tempValue = box.width;
|
||||
box.width = box.height;
|
||||
box.height = tempValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (transformations.length) {
|
||||
body =
|
||||
'<g transform="' +
|
||||
transformations.join(' ') +
|
||||
'">' +
|
||||
body +
|
||||
'</g>';
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate dimensions
|
||||
let width, height;
|
||||
@ -195,13 +217,6 @@ export function iconToSVG(
|
||||
width = typeof width === 'string' ? width : width + '';
|
||||
height = typeof height === 'string' ? height : height + '';
|
||||
|
||||
// Generate body
|
||||
let body = icon.body;
|
||||
if (transformations.length) {
|
||||
body =
|
||||
'<g transform="' + transformations.join(' ') + '">' + body + '</g>';
|
||||
}
|
||||
|
||||
// Result
|
||||
const result: IconifyIconBuildResult = {
|
||||
attributes: {
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { merge } from '../misc/merge';
|
||||
|
||||
/**
|
||||
* Icon alignment
|
||||
*/
|
||||
@ -57,11 +55,81 @@ export const defaults: FullIconCustomisations = Object.freeze({
|
||||
rotate: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* TypeScript
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental, @typescript-eslint/no-unused-vars
|
||||
function assertNever(v: never) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert IconifyIconCustomisations to FullIconCustomisations
|
||||
*/
|
||||
export function fullCustomisations(
|
||||
export function mergeCustomisations(
|
||||
defaults: FullIconCustomisations,
|
||||
item: IconifyIconCustomisations
|
||||
): FullIconCustomisations {
|
||||
return merge(defaults, item) as FullIconCustomisations;
|
||||
const result: FullIconCustomisations = {} as FullIconCustomisations;
|
||||
for (const key in defaults) {
|
||||
const attr = key as keyof FullIconCustomisations;
|
||||
|
||||
// Copy old value
|
||||
(result as Record<string, unknown>)[attr] = defaults[attr];
|
||||
|
||||
if (item[attr] === void 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate new value
|
||||
const value = item[attr];
|
||||
switch (attr) {
|
||||
// Boolean attributes that override old value
|
||||
case 'inline':
|
||||
case 'slice':
|
||||
if (typeof value === 'boolean') {
|
||||
result[attr] = value;
|
||||
}
|
||||
break;
|
||||
|
||||
// Boolean attributes that are merged
|
||||
case 'hFlip':
|
||||
case 'vFlip':
|
||||
if (value === true) {
|
||||
result[attr] = !result[attr];
|
||||
}
|
||||
break;
|
||||
|
||||
// Non-empty string
|
||||
case 'hAlign':
|
||||
case 'vAlign':
|
||||
if (typeof value === 'string' && value !== '') {
|
||||
(result as Record<string, unknown>)[attr] = value;
|
||||
}
|
||||
break;
|
||||
|
||||
// Non-empty string / non-zero number / null
|
||||
case 'width':
|
||||
case 'height':
|
||||
if (
|
||||
(typeof value === 'string' && value !== '') ||
|
||||
(typeof value === 'number' && value) ||
|
||||
value === null
|
||||
) {
|
||||
result[attr] = value;
|
||||
}
|
||||
break;
|
||||
|
||||
// Rotation
|
||||
case 'rotate':
|
||||
if (typeof value === 'number') {
|
||||
result[attr] += value;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
assertNever(attr);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { iconToSVG } from '../../lib/builder';
|
||||
import type { FullIconifyIcon } from '../../lib/icon';
|
||||
import { iconDefaults, fullIcon } from '../../lib/icon';
|
||||
import type { FullIconCustomisations } from '../../lib/customisations';
|
||||
import { defaults, fullCustomisations } from '../../lib/customisations';
|
||||
import { defaults, mergeCustomisations } from '../../lib/customisations';
|
||||
|
||||
describe('Testing iconToSVG', () => {
|
||||
it('Empty icon', () => {
|
||||
@ -26,7 +26,7 @@ describe('Testing iconToSVG', () => {
|
||||
});
|
||||
|
||||
it('Auto size, inline, body', () => {
|
||||
const custom: FullIconCustomisations = fullCustomisations({
|
||||
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
|
||||
inline: true,
|
||||
height: 'auto',
|
||||
});
|
||||
@ -49,7 +49,7 @@ describe('Testing iconToSVG', () => {
|
||||
});
|
||||
|
||||
it('Auto size, inline, body', () => {
|
||||
const custom: FullIconCustomisations = fullCustomisations({
|
||||
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
|
||||
inline: true,
|
||||
height: 'auto',
|
||||
});
|
||||
@ -72,7 +72,7 @@ describe('Testing iconToSVG', () => {
|
||||
});
|
||||
|
||||
it('Custom size, alignment', () => {
|
||||
const custom: FullIconCustomisations = fullCustomisations({
|
||||
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
|
||||
height: 'auto',
|
||||
hAlign: 'left',
|
||||
slice: true,
|
||||
@ -97,7 +97,7 @@ describe('Testing iconToSVG', () => {
|
||||
});
|
||||
|
||||
it('Rotation, alignment', () => {
|
||||
const custom: FullIconCustomisations = fullCustomisations({
|
||||
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
|
||||
height: '40px',
|
||||
vAlign: 'bottom',
|
||||
rotate: 1,
|
||||
@ -121,8 +121,32 @@ describe('Testing iconToSVG', () => {
|
||||
expect(result).to.be.eql(expected);
|
||||
});
|
||||
|
||||
it('Negative rotation', () => {
|
||||
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
|
||||
height: '40px',
|
||||
rotate: -1,
|
||||
});
|
||||
const icon: FullIconifyIcon = fullIcon({
|
||||
width: 20,
|
||||
height: 16,
|
||||
body: '<path d="..." />',
|
||||
});
|
||||
const expected: IconifyIconBuildResult = {
|
||||
attributes: {
|
||||
width: '32px',
|
||||
height: '40px',
|
||||
preserveAspectRatio: 'xMidYMid meet',
|
||||
viewBox: '0 0 16 20',
|
||||
},
|
||||
body: '<g transform="rotate(-90 10 10)"><path d="..." /></g>',
|
||||
};
|
||||
|
||||
const result = iconToSVG(icon, custom);
|
||||
expect(result).to.be.eql(expected);
|
||||
});
|
||||
|
||||
it('Flip, alignment', () => {
|
||||
const custom: FullIconCustomisations = fullCustomisations({
|
||||
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
|
||||
height: '32',
|
||||
vAlign: 'top',
|
||||
hAlign: 'right',
|
||||
@ -140,8 +164,7 @@ describe('Testing iconToSVG', () => {
|
||||
preserveAspectRatio: 'xMaxYMin meet',
|
||||
viewBox: '0 0 20 16',
|
||||
},
|
||||
body:
|
||||
'<g transform="translate(20 0) scale(-1 1)"><path d="..." /></g>',
|
||||
body: '<g transform="translate(20 0) scale(-1 1)"><path d="..." /></g>',
|
||||
};
|
||||
|
||||
const result = iconToSVG(icon, custom);
|
||||
@ -149,8 +172,8 @@ describe('Testing iconToSVG', () => {
|
||||
});
|
||||
|
||||
it('Flip, rotation', () => {
|
||||
const custom: FullIconCustomisations = fullCustomisations({
|
||||
vFlip: true,
|
||||
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
|
||||
hFlip: true,
|
||||
rotate: 1,
|
||||
});
|
||||
const icon: FullIconifyIcon = fullIcon({
|
||||
@ -165,8 +188,34 @@ describe('Testing iconToSVG', () => {
|
||||
preserveAspectRatio: 'xMidYMid meet',
|
||||
viewBox: '0 0 16 20',
|
||||
},
|
||||
body:
|
||||
'<g transform="rotate(90 8 8) translate(0 16) scale(1 -1)"><path d="..." /></g>',
|
||||
body: '<g transform="rotate(90 8 8) translate(20 0) scale(-1 1)"><path d="..." /></g>',
|
||||
};
|
||||
|
||||
const result = iconToSVG(icon, custom);
|
||||
expect(result).to.be.eql(expected);
|
||||
});
|
||||
|
||||
it('Flip icon that is rotated by default', () => {
|
||||
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
|
||||
hFlip: true,
|
||||
});
|
||||
const icon: FullIconifyIcon = fullIcon({
|
||||
width: 20,
|
||||
height: 16,
|
||||
body: '<path d="..." />',
|
||||
rotate: 1,
|
||||
});
|
||||
|
||||
// Horizontally flipping icon that has 90 or 270 degrees rotation will result in vertical flip.
|
||||
// Therefore result should be rotation + vertical flip to visually match horizontal flip on normal icon.
|
||||
const expected: IconifyIconBuildResult = {
|
||||
attributes: {
|
||||
width: '0.8em',
|
||||
height: '1em',
|
||||
preserveAspectRatio: 'xMidYMid meet',
|
||||
viewBox: '0 0 16 20',
|
||||
},
|
||||
body: '<g transform="translate(16 0) scale(-1 1)"><g transform="rotate(90 8 8)"><path d="..." /></g></g>',
|
||||
};
|
||||
|
||||
const result = iconToSVG(icon, custom);
|
||||
@ -174,7 +223,7 @@ describe('Testing iconToSVG', () => {
|
||||
});
|
||||
|
||||
it('Flip and rotation canceling eachother', () => {
|
||||
const custom: FullIconCustomisations = fullCustomisations({
|
||||
const custom: FullIconCustomisations = mergeCustomisations(defaults, {
|
||||
width: '1em',
|
||||
height: 'auto',
|
||||
hFlip: true,
|
||||
@ -204,7 +253,10 @@ describe('Testing iconToSVG', () => {
|
||||
const iconBody =
|
||||
'<g stroke="currentColor" stroke-width="16" stroke-linecap="round" stroke-linejoin="round" fill="none" fill-rule="evenodd"><path d="M40 64l48-48" class="animation-delay-0 animation-duration-10 animate-stroke stroke-length-102"/><path d="M40 64l48 48" class="animation-delay-0 animation-duration-10 animate-stroke stroke-length-102"/></g>';
|
||||
|
||||
const custom: FullIconCustomisations = fullCustomisations({});
|
||||
const custom: FullIconCustomisations = mergeCustomisations(
|
||||
defaults,
|
||||
{}
|
||||
);
|
||||
const icon: FullIconifyIcon = fullIcon({
|
||||
body: iconBody,
|
||||
width: 128,
|
||||
|
Loading…
x
Reference in New Issue
Block a user