@amandaghassaei/flat-svg
Version:
A TypeScript library for converting nested SVGs into a flat list of elements, paths, or segments and applying style-based filters.
349 lines (323 loc) • 10.7 kB
text/typescript
import { POLYGON, POLYLINE } from './constants';
import { applyTransform } from './transforms';
import {
CircleProperties,
EllipseProperties,
LineProperties,
PathParser,
PathProperties,
PolygonProperties,
PolylineProperties,
RectProperties,
Transform,
} from './types';
import { removeWhitespacePadding } from './utils';
import svgpath from 'svgpath';
import { isNonNegativeNumber, isNumber, isString } from '@amandaghassaei/type-checks';
/*
Export any geometry object as path in Abs coordinates with only L, H, V, B, and C types.
*/
const temp = [0, 0] as [number, number];
export function convertLineToPath(
properties: LineProperties,
parsingErrors: string[],
transform?: Transform
) {
let { x1, x2, y1, y2 } = properties;
// x1, x2, y1, y2 default to 0.
/* c8 ignore next if */
if (x1 === undefined) x1 = 0;
if (x2 === undefined) x2 = 0;
if (y1 === undefined) y1 = 0;
if (y2 === undefined) y2 = 0;
if (!isNumber(x1) || !isNumber(x2) || !isNumber(y1) || !isNumber(y2)) {
parsingErrors.push(`Invalid <line> properties: ${JSON.stringify({ x1, y1, x2, y2 })}.`);
return;
}
if (transform) {
temp[0] = x1;
temp[1] = y1;
[x1, y1] = applyTransform(temp, transform);
temp[0] = x2;
temp[1] = y2;
[x2, y2] = applyTransform(temp, transform);
}
return `M${x1},${y1} L${x2},${y2}`;
}
export function convertRectToPath(
properties: RectProperties,
parsingErrors: string[],
transform?: Transform
) {
let { x, y } = properties;
// x and y default to 0.
if (x === undefined) x = 0;
if (y === undefined) y = 0;
const { width, height } = properties;
if (
!isNumber(x) ||
!isNumber(y) ||
!isNonNegativeNumber(width) ||
!isNonNegativeNumber(height)
) {
parsingErrors.push(
`Invalid <rect> properties: ${JSON.stringify({ x, y, width, height })}.`
);
return;
}
let x1 = x;
let y1 = y;
let x2 = x + width;
let y2 = y;
let x3 = x + width;
let y3 = y + height;
let x4 = x;
let y4 = y + height;
if (transform) {
temp[0] = x1;
temp[1] = y1;
[x1, y1] = applyTransform(temp, transform);
temp[0] = x2;
temp[1] = y2;
[x2, y2] = applyTransform(temp, transform);
temp[0] = x3;
temp[1] = y3;
[x3, y3] = applyTransform(temp, transform);
temp[0] = x4;
temp[1] = y4;
[x4, y4] = applyTransform(temp, transform);
}
return `M${x1},${y1} L${x2},${y2} L${x3},${y3} L${x4},${y4} z`;
}
export function convertCircleToPath(
properties: CircleProperties,
parsingErrors: string[],
_preserveArcs: boolean,
transform?: Transform
) {
let { cx, cy, r } = properties;
// cx, cy, r default to 0.
if (cx === undefined) cx = 0;
/* c8 ignore next if */
if (cy === undefined) cy = 0;
if (r === undefined) r = 0;
if (!isNumber(cx) || !isNumber(cy) || !isNonNegativeNumber(r)) {
parsingErrors.push(`Invalid <circle> properties: ${JSON.stringify({ cx, cy, r })}.`);
return;
}
const pathParser = _convertEllipseToPath(cx, cy, r, r, _preserveArcs, transform);
/* c8 ignore next 7 */
if (pathParser.err) {
// Should not hit this.
parsingErrors.push(
`Problem parsing <circle> ${JSON.stringify({ cx, cy, r })} with ${pathParser.err}.`
);
return;
}
return pathParser;
}
export function convertEllipseToPath(
properties: EllipseProperties,
parsingErrors: string[],
_preserveArcs: boolean,
transform?: Transform
) {
let { cx, cy, rx, ry } = properties;
// cx, cy, rx, ry default to 0.
/* c8 ignore next if */
if (cx === undefined) cx = 0;
if (cy === undefined) cy = 0;
if (rx === undefined) rx = 0;
if (ry === undefined) ry = 0;
if (!isNumber(cx) || !isNumber(cy) || !isNonNegativeNumber(rx) || !isNonNegativeNumber(ry)) {
parsingErrors.push(`Invalid <ellipse> properties: ${JSON.stringify({ cx, cy, rx, ry })}.`);
return;
}
const pathParser = _convertEllipseToPath(cx, cy, rx, ry, _preserveArcs, transform);
/* c8 ignore next 9 */
if (pathParser.err) {
// Should not hit this.
parsingErrors.push(
`Problem parsing <ellipse> ${JSON.stringify({ cx, cy, rx, ry })} with ${
pathParser.err
}.`
);
return;
}
return pathParser;
}
// https://stackoverflow.com/questions/59011294/ellipse-to-path-convertion-using-javascript
// const ellipsePoints = new Array(24).fill(0);
function _convertEllipseToPath(
cx: number,
cy: number,
rx: number,
ry: number,
_preserveArcs: boolean,
transform?: Transform
) {
// Convert ellipse to 2 arcs.
const d = `M${cx - rx},${cy} a${rx},${ry} 0 1,0 ${rx * 2},0 a ${rx},${ry} 0 1,0 -${rx * 2},0`;
let pathParser = svgpath(d).abs() as any as PathParser;
// Convert arcs to bezier is _preserveArcs == false.
if (!_preserveArcs) pathParser = pathParser.unarc();
// Apply transform.
if (transform)
pathParser = pathParser.matrix([
transform.a,
transform.b,
transform.c,
transform.d,
transform.e,
transform.f,
]);
return pathParser;
// const kappa = 0.5522847498;
// const ox = rx * kappa; // x offset for the control point
// const oy = ry * kappa; // y offset for the control point
// ellipsePoints[0] = cx - rx;
// ellipsePoints[1] = cy;
// ellipsePoints[2] = cx - rx;
// ellipsePoints[3] = cy - oy;
// ellipsePoints[4] = cx - ox;
// ellipsePoints[5] = cy - ry;
// ellipsePoints[6] = cx;
// ellipsePoints[7] = cy - ry;
// ellipsePoints[8] = cx + ox;
// ellipsePoints[9] = cy - ry;
// ellipsePoints[10] = cx + rx;
// ellipsePoints[11] = cy - oy;
// ellipsePoints[12] = cx + rx;
// ellipsePoints[13] = cy;
// ellipsePoints[14] = cx + rx;
// ellipsePoints[15] = cy + oy;
// ellipsePoints[16] = cx + ox;
// ellipsePoints[17] = cy + ry;
// ellipsePoints[18] = cx;
// ellipsePoints[19] = cy + ry;
// ellipsePoints[20] = cx - ox;
// ellipsePoints[21] = cy + ry;
// ellipsePoints[22] = cx - rx;
// ellipsePoints[23] = cy + oy;
// if (transform) {
// for (let i = 0, length = ellipsePoints.length / 2; i < length; i++) {
// temp[0] = ellipsePoints[2 * i];
// temp[1] = ellipsePoints[2 * i + 1];
// applyTransform(temp, transform);
// ellipsePoints[2 * i] = temp[0];
// ellipsePoints[2 * i + 1] = temp[1];
// }
// }
// return `M${ellipsePoints[0]},${ellipsePoints[1]} \
// C${ellipsePoints[2]},${ellipsePoints[3]} ${ellipsePoints[4]},${ellipsePoints[5]} ${ellipsePoints[6]},${ellipsePoints[7]} \
// C${ellipsePoints[8]},${ellipsePoints[9]} ${ellipsePoints[10]},${ellipsePoints[11]} ${ellipsePoints[12]},${ellipsePoints[13]} \
// C${ellipsePoints[14]},${ellipsePoints[15]} ${ellipsePoints[16]},${ellipsePoints[17]} ${ellipsePoints[18]},${ellipsePoints[19]} \
// C${ellipsePoints[20]},${ellipsePoints[21]} ${ellipsePoints[22]},${ellipsePoints[23]} ${ellipsePoints[0]},${ellipsePoints[1]} \
// z`;
}
export function convertPolygonToPath(
properties: PolygonProperties,
parsingErrors: string[],
transform?: Transform
) {
const { points } = properties;
if (!isString(points)) {
parsingErrors.push(`Invalid <polygon> properties: ${JSON.stringify({ points })}.`);
return;
}
const path = _convertPointsToPath(points, parsingErrors, POLYGON, transform);
if (!path) return path;
return path + ' z';
}
export function convertPolylineToPath(
properties: PolylineProperties,
parsingErrors: string[],
transform?: Transform
) {
const { points } = properties;
if (!isString(points)) {
parsingErrors.push(`Invalid <polyline> properties: ${JSON.stringify({ points })}.`);
return;
}
return _convertPointsToPath(points, parsingErrors, POLYLINE, transform);
}
function _convertPointsToPath(
pointsString: string,
parsingErrors: string[],
elementType: typeof POLYGON | typeof POLYLINE,
transform?: Transform
) {
const points = removeWhitespacePadding(pointsString).split(' ');
let d = '';
while (points.length) {
const point = points.shift()!.split(',');
if (point.length === 1) {
// Sometimes polyline is not separated by commas, only by whitespace.
if (points.length && points.length % 2 === 1) {
point.push(points.shift()!); // Get next element in points array.
}
}
if (point.length !== 2) {
parsingErrors.push(
`Unable to parse points string: "${pointsString}" in <${elementType}>.`
);
return;
}
let x = parseFloat(point[0]);
let y = parseFloat(point[1]);
if (isNaN(x) || isNaN(y)) {
parsingErrors.push(
`Unable to parse points string: "${pointsString}" in <${elementType}>.`
);
return;
}
if (transform) {
temp[0] = x;
temp[1] = y;
[x, y] = applyTransform(temp, transform);
}
if (d === '') {
d += `M${x},${y}`;
} else {
d += ` L${x},${y}`;
}
}
return d;
}
export function convertPathToPath(
properties: PathProperties,
parsingErrors: string[],
_preserveArcs: boolean,
transform?: Transform
) {
const { d } = properties;
if (!isString(d)) {
parsingErrors.push(`Invalid <path> properties: ${JSON.stringify({ d })}.`);
return;
}
// Convert to absolute coordinates,
// Convert smooth curves (T/S) to regular Bezier (Q/C).
let pathParser = svgpath(d).abs().unshort() as any as PathParser;
if (_preserveArcs) {
// Convert arcs to bezier.
pathParser = pathParser.unarc();
}
// Apply transform.
if (transform) {
pathParser = pathParser.matrix([
transform.a,
transform.b,
transform.c,
transform.d,
transform.e,
transform.f,
]);
}
if (pathParser.err) {
parsingErrors.push(
`Problem parsing <path> ${JSON.stringify({ d })} with ${pathParser.err}.`
);
return;
}
return pathParser;
}