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
text/typescript
/* 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
}
}