UNPKG

svg-path-properties

Version:

Calculate the length for an SVG path, to use it with node or a Canvas element

389 lines (371 loc) 13 kB
/* eslint-disable security/detect-object-injection */ import parse from './parse.ts' import { PointArray, Properties, PartProperties, Point } from './types.ts' import { LinearPosition } from './linear.ts' import { Arc } from './arc.ts' import { Bezier } from './bezier.ts' export default class SVGPathProperties implements Properties { private length: number = 0 private partial_lengths: number[] = [] private functions: (null | Properties)[] = [] private initial_point: null | Point = null constructor (source: string | [string, ...Array<number>][]) { const parsed = Array.isArray(source) ? source : parse(source) let cur: PointArray = [0, 0] let prevPoint: PointArray = [0, 0] let curve: Bezier | undefined let ringStart: PointArray = [0, 0] for (let i = 0; i < parsed.length; i++) { // moveTo if (parsed[i][0] === 'M') { cur = [parsed[i][1], parsed[i][2]] ringStart = [cur[0], cur[1]] this.functions.push(null) if (i === 0) { this.initial_point = { x: parsed[i][1], y: parsed[i][2] } } } else if (parsed[i][0] === 'm') { cur = [parsed[i][1] + cur[0], parsed[i][2] + cur[1]] ringStart = [cur[0], cur[1]] this.functions.push(null) // lineTo } else if (parsed[i][0] === 'L') { this.length += Math.sqrt( Math.pow(cur[0] - parsed[i][1], 2) + Math.pow(cur[1] - parsed[i][2], 2) ) this.functions.push(new LinearPosition(cur[0], parsed[i][1], cur[1], parsed[i][2])) cur = [parsed[i][1], parsed[i][2]] } else if (parsed[i][0] === 'l') { this.length += Math.sqrt(Math.pow(parsed[i][1], 2) + Math.pow(parsed[i][2], 2)) this.functions.push( new LinearPosition(cur[0], parsed[i][1] + cur[0], cur[1], parsed[i][2] + cur[1]) ) cur = [parsed[i][1] + cur[0], parsed[i][2] + cur[1]] } else if (parsed[i][0] === 'H') { this.length += Math.abs(cur[0] - parsed[i][1]) this.functions.push(new LinearPosition(cur[0], parsed[i][1], cur[1], cur[1])) cur[0] = parsed[i][1] } else if (parsed[i][0] === 'h') { this.length += Math.abs(parsed[i][1]) this.functions.push(new LinearPosition(cur[0], cur[0] + parsed[i][1], cur[1], cur[1])) cur[0] = parsed[i][1] + cur[0] } else if (parsed[i][0] === 'V') { this.length += Math.abs(cur[1] - parsed[i][1]) this.functions.push(new LinearPosition(cur[0], cur[0], cur[1], parsed[i][1])) cur[1] = parsed[i][1] } else if (parsed[i][0] === 'v') { this.length += Math.abs(parsed[i][1]) this.functions.push(new LinearPosition(cur[0], cur[0], cur[1], cur[1] + parsed[i][1])) cur[1] = parsed[i][1] + cur[1] // Close path } else if (parsed[i][0] === 'z' || parsed[i][0] === 'Z') { this.length += Math.sqrt( Math.pow(ringStart[0] - cur[0], 2) + Math.pow(ringStart[1] - cur[1], 2) ) this.functions.push(new LinearPosition(cur[0], ringStart[0], cur[1], ringStart[1])) cur = [ringStart[0], ringStart[1]] // Cubic Bezier curves } else if (parsed[i][0] === 'C') { curve = new Bezier( cur[0], cur[1], parsed[i][1], parsed[i][2], parsed[i][3], parsed[i][4], parsed[i][5], parsed[i][6] ) this.length += curve.getTotalLength() cur = [parsed[i][5], parsed[i][6]] this.functions.push(curve) } else if (parsed[i][0] === 'c') { curve = new Bezier( cur[0], cur[1], cur[0] + parsed[i][1], cur[1] + parsed[i][2], cur[0] + parsed[i][3], cur[1] + parsed[i][4], cur[0] + parsed[i][5], cur[1] + parsed[i][6] ) if (curve.getTotalLength() > 0) { this.length += curve.getTotalLength() this.functions.push(curve) cur = [parsed[i][5] + cur[0], parsed[i][6] + cur[1]] } else { this.functions.push(new LinearPosition(cur[0], cur[0], cur[1], cur[1])) } } else if (parsed[i][0] === 'S') { if (i > 0 && ['C', 'c', 'S', 's'].indexOf(parsed[i - 1][0]) > -1) { if (curve) { const c = curve.getC() curve = new Bezier( cur[0], cur[1], 2 * cur[0] - c.x, 2 * cur[1] - c.y, parsed[i][1], parsed[i][2], parsed[i][3], parsed[i][4] ) } } else { curve = new Bezier( cur[0], cur[1], cur[0], cur[1], parsed[i][1], parsed[i][2], parsed[i][3], parsed[i][4] ) } if (curve) { this.length += curve.getTotalLength() cur = [parsed[i][3], parsed[i][4]] this.functions.push(curve) } } else if (parsed[i][0] === 's') { // 240 225 if (i > 0 && ['C', 'c', 'S', 's'].indexOf(parsed[i - 1][0]) > -1) { if (curve) { const c = curve.getC() const d = curve.getD() curve = new Bezier( cur[0], cur[1], cur[0] + d.x - c.x, cur[1] + d.y - c.y, cur[0] + parsed[i][1], cur[1] + parsed[i][2], cur[0] + parsed[i][3], cur[1] + parsed[i][4] ) } } else { curve = new Bezier( cur[0], cur[1], cur[0], cur[1], cur[0] + parsed[i][1], cur[1] + parsed[i][2], cur[0] + parsed[i][3], cur[1] + parsed[i][4] ) } if (curve) { this.length += curve.getTotalLength() cur = [parsed[i][3] + cur[0], parsed[i][4] + cur[1]] this.functions.push(curve) } } else if (parsed[i][0] === 'Q') { // Quadratic Bezier curves if (cur[0] === parsed[i][1] && cur[1] === parsed[i][2]) { const linearCurve = new LinearPosition( parsed[i][1], parsed[i][3], parsed[i][2], parsed[i][4] ) this.length += linearCurve.getTotalLength() this.functions.push(linearCurve) } else { curve = new Bezier( cur[0], cur[1], parsed[i][1], parsed[i][2], parsed[i][3], parsed[i][4], undefined, undefined ) this.length += curve.getTotalLength() this.functions.push(curve) } cur = [parsed[i][3], parsed[i][4]] prevPoint = [parsed[i][1], parsed[i][2]] } else if (parsed[i][0] === 'q') { if (!(parsed[i][1] === 0 && parsed[i][2] === 0)) { curve = new Bezier( cur[0], cur[1], cur[0] + parsed[i][1], cur[1] + parsed[i][2], cur[0] + parsed[i][3], cur[1] + parsed[i][4], undefined, undefined ) this.length += curve.getTotalLength() this.functions.push(curve) } else { const linearCurve = new LinearPosition( cur[0] + parsed[i][1], cur[0] + parsed[i][3], cur[1] + parsed[i][2], cur[1] + parsed[i][4] ) this.length += linearCurve.getTotalLength() this.functions.push(linearCurve) } prevPoint = [cur[0] + parsed[i][1], cur[1] + parsed[i][2]] cur = [parsed[i][3] + cur[0], parsed[i][4] + cur[1]] } else if (parsed[i][0] === 'T') { if (i > 0 && ['Q', 'q', 'T', 't'].indexOf(parsed[i - 1][0]) > -1) { curve = new Bezier( cur[0], cur[1], 2 * cur[0] - prevPoint[0], 2 * cur[1] - prevPoint[1], parsed[i][1], parsed[i][2], undefined, undefined ) this.functions.push(curve) this.length += curve.getTotalLength() } else { const linearCurve = new LinearPosition(cur[0], parsed[i][1], cur[1], parsed[i][2]) this.functions.push(linearCurve) this.length += linearCurve.getTotalLength() } prevPoint = [2 * cur[0] - prevPoint[0], 2 * cur[1] - prevPoint[1]] cur = [parsed[i][1], parsed[i][2]] } else if (parsed[i][0] === 't') { if (i > 0 && ['Q', 'q', 'T', 't'].indexOf(parsed[i - 1][0]) > -1) { curve = new Bezier( cur[0], cur[1], 2 * cur[0] - prevPoint[0], 2 * cur[1] - prevPoint[1], cur[0] + parsed[i][1], cur[1] + parsed[i][2], undefined, undefined ) this.length += curve.getTotalLength() this.functions.push(curve) } else { const linearCurve = new LinearPosition( cur[0], cur[0] + parsed[i][1], cur[1], cur[1] + parsed[i][2] ) this.length += linearCurve.getTotalLength() this.functions.push(linearCurve) } prevPoint = [2 * cur[0] - prevPoint[0], 2 * cur[1] - prevPoint[1]] cur = [parsed[i][1] + cur[0], parsed[i][2] + cur[1]] } else if (parsed[i][0] === 'A') { const arcCurve = new Arc( cur[0], cur[1], parsed[i][1], parsed[i][2], parsed[i][3], parsed[i][4] === 1, parsed[i][5] === 1, parsed[i][6], parsed[i][7] ) this.length += arcCurve.getTotalLength() cur = [parsed[i][6], parsed[i][7]] this.functions.push(arcCurve) } else if (parsed[i][0] === 'a') { const arcCurve = new Arc( cur[0], cur[1], parsed[i][1], parsed[i][2], parsed[i][3], parsed[i][4] === 1, parsed[i][5] === 1, cur[0] + parsed[i][6], cur[1] + parsed[i][7] ) this.length += arcCurve.getTotalLength() cur = [cur[0] + parsed[i][6], cur[1] + parsed[i][7]] this.functions.push(arcCurve) } this.partial_lengths.push(this.length) } } private getPartAtLength = (fractionLength: number) => { if (fractionLength < 0) { fractionLength = 0 } else if (fractionLength > this.length) { fractionLength = this.length } let i = this.partial_lengths.length - 1 while (this.partial_lengths[i] >= fractionLength && i > 0) { i-- } i++ return { fraction: fractionLength - this.partial_lengths[i - 1], i } } public getTotalLength = () => { return this.length } public getPointAtLength = (fractionLength: number) => { const fractionPart = this.getPartAtLength(fractionLength) const functionAtPart = this.functions[fractionPart.i] if (functionAtPart) { return functionAtPart.getPointAtLength(fractionPart.fraction) } else if (this.initial_point) { return this.initial_point } throw new Error('Wrong function at this part.') } public getTangentAtLength = (fractionLength: number) => { const fractionPart = this.getPartAtLength(fractionLength) const functionAtPart = this.functions[fractionPart.i] if (functionAtPart) { return functionAtPart.getTangentAtLength(fractionPart.fraction) } else if (this.initial_point) { return { x: 0, y: 0 } } throw new Error('Wrong function at this part.') } public getPropertiesAtLength = (fractionLength: number) => { const fractionPart = this.getPartAtLength(fractionLength) const functionAtPart = this.functions[fractionPart.i] if (functionAtPart) { return functionAtPart.getPropertiesAtLength(fractionPart.fraction) } else if (this.initial_point) { return { x: this.initial_point.x, y: this.initial_point.y, tangentX: 0, tangentY: 0 } } throw new Error('Wrong function at this part.') } public getParts = () => { const parts: PartProperties[] = [] for (let i = 0; i < this.functions.length; i++) { if (this.functions[i] !== null) { this.functions[i] = this.functions[i] as Properties const properties: PartProperties = { start: this.functions[i]!.getPointAtLength(0), end: this.functions[i]!.getPointAtLength( this.partial_lengths[i] - this.partial_lengths[i - 1] ), length: this.partial_lengths[i] - this.partial_lengths[i - 1], getPointAtLength: this.functions[i]!.getPointAtLength, getTangentAtLength: this.functions[i]!.getTangentAtLength, getPropertiesAtLength: this.functions[i]!.getPropertiesAtLength } parts.push(properties) } } return parts } }