Calculates a path's length or points as well as tangent angles at length based on raw pathdata strings.
This library aims to work as a performant workaround to emulate natively supported browser methods getTotalLength() and getPointAtLength() in a non-rendered environment such as node or virtual DOM applications or canvas.
Features: This library provides methods to get:
- path length from raw SVG path data strings
- point coordinates at specified length
- tangent angles (handy for SVG motion path emulations)
- segments at length
- split segments at length into separate path data chunks
- shape support: length/point-at-length from elements
- get bounding box
- area calculations
The provided methods calculate points at lengths by measuring all segments lengths and saving them in a reusable lookup object.
This way you can efficiently calculate hundreds of points on a path without sacrificing too much performance – unlike the quite expensive native getPointAtlength() method.
- Usage
- Path data input
- Canvas helper: Path2D_svg()
- Methods and options
- Get bounding box
- Get Area
- Get polygon
- Split paths at length
- Updates and Versions
- Accuracy
- Performance
- Addons
- Report bugs
- Demos
- Alternative libraries
- Credits
- Related Repositories/projects
Load JS locally or via cdn
// ESM
<script src="https://cdn.jsdelivr.net/npm/svg-getpointatlength@latest/+esm"></script>
// IIFE
<script src="https://cdn.jsdelivr.net/npm/svg-getpointatlength@latest/dist/svg-getpointatlength.min.js"></script>
or (unpkg.com version)
<script src="https://www.unpkg.com/svg-getpointatlength@latest/dist/svg-getpointatlength.js"></script>
In case you can live without the fancy features introduced in version 2, you may also load the smaller version from the dist directory.
<!--ESM -->
<script type="module" src="https://cdn.jsdelivr.net/npm/svg-getpointatlength@latest/dist/svg-getpointatlength_lite.esm.js"></script>
<!--IIFE -->
<script src="https://cdn.jsdelivr.net/npm/svg-getpointatlength@latest/dist/svg-getpointatlength_lite.js"></script>
This version doesn't include helpers for:
Otherwise it supports all features from version 1.3.x and is ~35–40% smaller.
Example: calculate path length from pathData
let d = `M3,7
L13,7
m-20,10
l10,0
V27
H23
v10
h10
C 33,43 38,47 43,47
c 0,5 5,10 10,10
S 63,67 63,67
s -10,10 10,10
Q 50,50 73,57
q 20,-5 0,-10
T 70,40
t 0,-15
A 5, 10 45 1040,20
a5,5 20 01 -10,-10
Z `
// measure path and save metrics in lookup object
let pathLengthLookup = getPathLengthLookup(d)
let totalLength = pathLengthLookup.totalLength
console.log(totalLength)
// point at length
let pt = pathLengthLookup.getPointAtLength(totalLength/2)
console.log(pt)
If you only need to retrieve the total length of a path you can use the simplified helper getPathLength()
// only length – slightly faster as we don't calculate intermediate lengths
let length = getPathLength(d)
console.log(length)
<script type="module">
// init
import { getPathLookup } from 'svg-getpointatlength.esm.min.js';
let lookup = getPathLookup(d)
let pt = lookup.getPointAtLength(10)
</script>
npm install svg-getpointatlength
var pathDataLength = require("svg-getpointatlength");
var { getPathLengthLookup, getPathLengthFromD, getPathDataLength, getLength, parsePathDataNormalized } = pathDataLength;
let d = `M3,7
L13,7
m-20,10
l10,0
V27
H23
v10
h10
C 33,43 38,47 43,47
c 0,5 5,10 10,10
S 63,67 63,67
s -10,10 10,10
Q 50,50 73,57
q 20,-5 0,-10
T 70,40
t 0,-15
A 5, 10 45 1040,20
a5,5 20 01 -10,-10
Z `
// measure path and save metrics in lookup object
let pathLengthLookup = getPathLengthLookup(d)
let totalLength = pathLengthLookup.totalLength
console.log(totalLength)
// point at length
let pt = pathLengthLookup.getPointAtLength(totalLength/2)
console.log(pt)
getPathLengthLookup(d) accepts:
- stringified path data (as used in
dattributes) - an already parsed path data array – also native pathData object retrieved by
path.getPathData()(natively supported in Firefox or via polyfill) - SVGGeometry element (including shapes like
<rect>,<ellipse>) - SVG markup: all geometry elements are converted to path data
- polygon data:
- stringified as in
<polygon>pointsattribute - vertex array like
[{x:1, y:2}, {x:3, y:4}]
- stringified as in
- cached lookup JSON: you can store the complete stringified lookup object as a JSON and pass it to the lookup function (handy to skip the complex measure process)
Version 2 adds a custom class Path2D_svg() to retrieve lookup data from a Path2D object.
It's a similar approach as in Kaiido's Path2D Inspection library
const path = new Path2D_svg();
path.moveTo(350, 50);
path.bezierCurveTo(370, 0, 430, 100, 450, 50);
// get lookup
let lookup = path.getPathLookup();
let pt = lookup.getPointAtLength(10)
// get path data
let pathData = path.getPathData();
// stringified path data
let d = path.getPathDataString();
getPathLengthLookup(d) returns a lookup objects including reusable data about ech path segment as well as the total length.
{
"totalLength": path total length,
"segments": [
{
//lengths calculated between t=0 to t=1 in 36 steps
"lengths": [ length array ],
"points": [ control point array ],
"index": segment index,
"total": segment length,
"type": segment command type (c, q, l, a etc.),
},
//... subsequent segment info
]
}
lookup.pathLengthLookup.getPointAtLength(length, getTangent = false, getSegment = false) returns an object like this
{x: 10, y:20, index:segmentIndex, t:tValue}
Optionally, you can also include tangent angles and segment indices (as well as self contained path data) from the current point-at-length:
| method | options/agruments | description | default/values |
|---|---|---|---|
getPathLengthLookup(d, precision, onlyLength, getTangent, conversions ) |
d |
A path data string or a already parsed path data array | none |
precision |
Specify accuracy for Bézier length calculations. This parameter sets the amount of length intermediate calculations. Default should work even for highly accurate calcuations. Legendre-Gauss approximation is already adaptive | medium, high, low |
|
onlyLength |
skips the lookup creation and returns only the length of a path | false |
|
getTangent |
include tangent angles in lookup object (can improve performance) | true | |
conversions |
convert path commands e.g arcs to cubics | { arcToCubic:false, quadraticToCubic:false} | |
getPointAtLength() |
length |
gets point at specified length | none |
getTangent |
include tangent angles in point object (can improve performance) | false | |
getSegment |
include segment info in object | false | |
getSegmentAtLength() |
length |
gets segment at specified length | none |
getBBox |
include bounding box data (total and segment) | true | |
getArea |
include area data (total and segment) | true |
Version 2 add a shorter getPathLookup() method that works the same way as getPathLengthLookup()
// select path
let path = document.querySelector('path')
// get path data attribute
let d = path.getAttribute('d')
// measure path, create lookup
let pathLengthLookup = getPathLengthLookup(d)
// alternative - get lookup from element
let pathLengthLookup_from_element = path.getPathLookup()
let pathLengthLookup_element_input = getPathLengthLookup(path)
// get point, tangent and segment
let length = 100;
let getTangent = true;
let getSegment = true;
let pt = pathLengthLookup.getPointAtLength(length, getTangent, getSegment);
let tangentAngle = pt.angle;
let segmentIndex = pt.index;
let segmentCommand = pt.com;
The returned data object will look like this:
{
// tangent angle in radians
angle: 1.123,
// original command
com: {type: 'A', values: Array(7), p0: {…}},
// original command/segment index
index: 1,
// t value for target length
t: 0.25,
// point coordinates
x: 10,
y: 15,
}
See pointAtLength.html example in demos folder.
So you also have info about the current segment the length is in as well as the t value used to interpolate the point.
lookup.getSegmentAtLength(len) returns the current segment's index as well as the path data, area and bounding box (both optional enabled by default).
Usage:
// get segment
let segment = pathLookup.getSegmentAtLength(length);
let {index, pathData, d, angle, x, y } = segment;
// render current path segment
path.setAttribute('d', d)
// returns
{
x: 264.85,
y: 53.947,
angle: 1.36,
d: "M 200 50 A 50 25 20 1 1 275 100",
pathData: [{…}, {…}],
bbox: {x:0, y:0, width:100, height:200},
t: 0.45,
index: 3,
};
All in all, you may consider lookup.getSegmentAtLength(len) as the »big sister« of pointAtLength() as it return pretty much anything you could want to know from a certain point.
However, it also adds more calculation at first run. If you only need point data and maybe angles – stick with pointAtLength()
Version 2 introduces a bounding box method which returns a bbox object similar to native SVG getBBox().
Once you call it the lookup is complemented with bounding box data for the entire path as well as dimensions for each segment.
let {x, y, width, height} = lookup.getBBox()
Version 2 adds an area calculation.
This is also handy to get the drawing direction of a path or segment:
- area < 0 = counter clockwise
- area > 0 = clockwise
let lookup = path.getPathLookup();
/**
* adds area data to lookup
* for each segment
* total path area
*/
let area = lookup.getArea()
Alternatively, you can get all area data calling getSegmentAtLength() (big sister =)
let segmentData = lookup.getSegmentAtLength(10)
By default getArea parameter is enabled:
getSegmentAtLength(length = 0, getBBox = true, getArea=true, decimals=-1)
While the area calculations should be quite accurate – based on proper calculations for each curve/segment type – we can't accurately calculate areas for self-intersecting paths.
You can also retrieve a polygon from a lookup via lookup.getPolygon(options)
let options = {
keepCorners: true,
keepLines:true,
vertices: 24,
decimals: 3,
}
let polyData = lookup.getPolygon(options)
let {points, poly, d} = polyData;
getPolygon() returns an object containing these properties:
poly: point object arraypoints: point string - as used for SVG<polygon>and<polyline>d: SVG path data string - as used for SVG<path>
| parameter | type | default | effect |
|---|---|---|---|
| keepCorners | Boolean | true | retains segment start and end point – unlike brute force vertex calculation based on equal length intervals |
| keepLines | Boolean | true | doesn't add unnecessary points for line to segments – reduces number of points |
| vertices | number | 16 | target max number of vertices. Will be adjusted if keepCorners is active to distribute points across all segments |
| decimals | number | 3 | rounds coordinates for more compact data output. Set -1 for no rounding |
lookup.splitPathAtLength(len) splits a path at the specified length and returns an object containing:
- an array of path data chunks (before and after split position)
- an array of stringified pathdata - to be applied to a
<path>dattribute directly. See example "split.html". - index of original segment
{
"pathDataArr": [
[
{"type": "M","values": [0, 100 ]},
{"type": "Q","values": [50, 0, 100, 50]}
],
[
{"type": "M","values": [0, 100 ]},
{"type": "Q","values": [50, 0, 100, 50]}
]
],
"dArr": ["M 0 100 ...", "M 0 100 ..."],
"index": 3
}
Usage:
let splitPathData = lookup.splitPathAtLength(len)
let [d1, d2] = splitPathData.dArr;
getPathLengthLookup() now also supports elements these SVGGeometryElements:
<path><rect><circle><ellipse><polygon><polyline><line>
relative units like % are also supported as long as a viewBox is provided. Physical units like in, mm, pt, em are converted to user units pixel based on a 96 dpi resolution.
let path = document.querySelector('path')
// measure path, create lookup
let pathLengthLookup = getPathLengthLookup(path);
// total length
let {totalLength} = pathLengthLookup;
// get point
let pt = pathLengthLookup.getPointAtLength(totalLength/2);
For usage in node.js you need a DOM parser like JSDOM.
To retrieve the path data from an element use getPathDataFromEl(el, stringify)
let ellipse = document.querySelector('ellipse');
let pathData = getPathDataFromEl(ellipse);
// measure path, create lookup
let pathLengthLookup = getPathLengthLookup(pathData);
// total length
let {totalLength} = pathLengthLookup;
// get point
let pt = pathLengthLookup.getPointAtLength(totalLength/2);
As of version 2 you can stringify the path data (for usage as a d path attribute) via new parameter:
let stringify = true;
let pathDataString = getPathDataFromEl(el, stringify)
- Version 2:
- improved input normalization: accepts SVG DOM elements, native pathData objects (retrieved from
getPathData()) and point arrays - split paths at length: creates 2 separate selfcontained path data according to split position
- improved input normalization: accepts SVG DOM elements, native pathData objects (retrieved from
- Version 1.3.1 fixes a rare parsing issue where 'M' commands were omitted (e.g
zfollowed by another drawing command thanM– unfortunately valid). See updated demo with "path-from-hell3" (... a pretty good stress test for any path data parser=). Thanks to vboye-foreflight's PR we now get the point at last length whenever the input length exceeds the total length - compliant with native methods' behavior. - Version 1.3.0 support for shapes (ellipse, circle, rect etc.)
- Version 1.2.4 fixed arc angle errors
- Version 1.2.0 calculates elliptic arcs directly – removing arc to cubic conversion
- Version 1.1.0 improved performance for recurring point-at-length calculations, fixed tangent calculation bugs and added flat bezier edge cases
- Version 1.0.15 improved performance for recurring point-at-length calculations
- Version 1.0.13 added support for tangent angles at a specified length/point
In case you encounter any problems with the latest versions you can just load a previous one like so:
<script src="https://www.unpkg.com/[email protected]/getPointAtLengthLookup.js"></script>
See npm repo for all existing versions
Save path/segment metrics as a reusable lookup for further calculations
- path data is parsed from a
dstring to get computable absolute values - the lookup stores
2.1 segement total lenghts
2.2 partial lengths at certaintintervals - point at lengths are calculated by finding the closest length in the segment array
Then we find the closest length in the length interval array. We interpolate a newtvalue based on the length difference to get a close length approximation
This library also includes a quite versatile parsing function that could be used separately.
parsePathDataNormalized(d, options)
As length calculations are based on normalized path data values.
All values are converted to absolute and longhand commands.
let options= {
toAbsolute: true, //necessary for most calculations
toLonghands: true, //dito
arcToCubic: false, //sometimes necessary
arcAccuracy: 4, //arc to cubic precision
}
| parameter | default | effect |
|---|---|---|
| toAbsolute | true | convert all to absolute |
| toLonghands | true | convert all shorthands to longhands |
| arcToCubic | false | convert arcs A commands to cubic béziers |
| arcToCubic | 4 | arc to cubic precision – adds more cubic segments to improve length accuracy |
// get original path data: including relative and shorthand commands
let pathData_notNormalized = parsePathDataNormalized(d, {toAbsolute:false, toLonghands:false})
In fact the native browser methods getTotalLength() and getPointAtlength() return different results in Firefox, chromium/blink and webkit.
Compared against reproducible/calculable objects/shapes like circles the methods provided by this library actually provide a more accurate result.
Cubic bezier length are approximated using Legendre-Gauss quadrature integral approximation Weights and Abscissae values are adjusted for long path segments.
Elliptical Arc A commands are approximated also via Legendre-Gauss quadrature (new in version 1.2). Circular arcs are retained which improves speed and accuracy.
Native getPointAtLength() browser implementations aren't well optimized for recurring point calculations as they start from scratch on each call (parsing, measuring, calculating point at length). To be fair: there is no trivial length calculation algorithm.
Since this library stores all important length data segment by segment – subsequent point (or tangent angle) calculations are way faster than the native methods.
| points | native | lookup |
|---|---|---|
| 10 | 2.1 ms | 2.1 ms |
| 100 | 21.1 ms | 2.2 ms |
| 1000 | 210 ms | 3.9 ms |
| 10000 | 2093.6 ms | 6.7 ms |
The lookup creation will usuall take up ~ 1-2ms (depending on the path).
As you can see the lookup's setup overhead is already compensated at 10 iteration.
When we're entering a range of 100 or 1000 points the lookup method clearly wins whereas native getPointAtLength() severely impacts rendering performance.
getPointAtLengthLookup_getPolygon.js includes a helper to generate polygons from path data retaining segemnt final on-path points
<script src="https://cdn.jsdelivr.net/gh/herrstrietzel/svg-getpointatlength@main/getPointAtLengthLookup_getPolygon.js"></script>
let options = {
// target vertice number
vertices: 16,
// round coordinates
decimals: 3,
// retain segment final points: retains shape
adaptive: true,
// return polygon if path has only linetos
retainPoly: true,
// find an adaptive close approximation based on a length difference threshold
tolerance: 0
}
let vertices = polygonFromPathData(pathData, options)
If you found a bug - feel free to file an issue. For debugging you may also test your path with this codepen testbed
You can easily test paths using the web application:
- get point, tangent and segment: demo codepen
- Canvas helper
- getPointAtlength: native vs. lookup
- get point at length – performance/accuracy
- get point and area
- path to polygon
- get length at point
- point ant tangent angle on motion-path
- scroll path
(See also demos folder)
- Kaiido's "path2D-inspection" – interesting if yo're foremost working with canvas
- rveciana's "svg-path-properties"
- Mike 'Pomax' Kamermans for explaining the theory. See Stackoverflow post "Finding points on curves in HTML 5 2d Canvas context"
- Vitaly Puzrin for svgpath library providing for instance a great and customizable arc-to-cubic approximation – the base for the more accurate arc-to-cubic approximations
- Jarek Foksa for developping the great getPathData() polyfill – probably the most productive contributor to the "new" W3C SVGPathData interface draft
- obviously, Dmitry Baranovskiy – a lot of these helper functions originate either from Raphaël or snap.svg – or are at least heavily inspired by some helpers from these libraries
- svg-parse-path-normalized – Parse path data from string including fine-grained normalizing options
- fix-path-directions – Correct sub path directions in compound path for apps that don't support fill-rules or just reverse path directions (e.g for path animations)
- svg-pathdata-getbbox – Calculates a path bounding box based on its raw pathdata
- svg-transform – A library to transform or de-transform/flatten svg paths