Progress bars that trace a path, a pen plotter drawing a shape stroke by stroke, an SVG line animating in as if it were being drawn by hand — all of these boil down to the same question: given a polyline through points \(P_1, P_2, \dots, P_n\), how do you draw only the first \(p\) percent of it, measured by arc length rather than by number of segments?
Setting Up the Total and Target Length
Write the vector of the \(k\)-th segment as \(\mathbf{v}_k = P_{k+1} - P_k\). The total length of the polyline is just the sum of the segment lengths,
\[L=\sum\limits_{k=1}^{n-1}\|P_{k+1}-P_k\|=\sum\limits_{k=1}^{n-1}\|\mathbf{v}_k\|,\]
and the arc length we need to travel to reach the \(p\)-th percentile of the path is simply \(w = L\cdot p\) for \(p\in[0, 1]\).
Finding the Point at Distance w
Walking along the polyline for a distance \(w\) lands somewhere on some segment \(m\) — the smallest index for which the cumulative length up to and including segment \(m\) first reaches or exceeds \(w\). On that segment, the vector no longer runs all the way from \(P_m\) to \(P_{m+1}\), but only from \(P_m\) up to some point \(Q\) on the segment. That point is \(P_m + \hat{\mathbf{v}}_m d\), where \(\hat{\mathbf{v}}_m\) is the unit direction of segment \(m\) and \(d\) is however much of \(w\) is left once every full segment before \(m\) has been accounted for:
\[d = w - \sum\limits_{k=1}^{m-1}\|\mathbf{v}_k\|.\]
Substituting \(w\) and \(d\) back in gives \(Q\) purely in terms of the path's points:
\[Q = P_m + \hat{\mathbf{v}}_m\left(p\cdot\sum\limits_{k=1}^{n-1}\|\mathbf{v}_k\| - \sum\limits_{k=1}^{m-1}\|\mathbf{v}_k\|\right).\]
Moving the normalization of \(\mathbf{v}_m\) into the parenthesized term turns it into a plain fraction of that one segment, which is a more convenient form to compute with:
\[Q = P_m + {\mathbf{v}}_m\left(\frac{p\cdot\sum\limits_{k=1}^{n-1}\|\mathbf{v}_k\| - \sum\limits_{k=1}^{m-1}\|\mathbf{v}_k\|}{\|\mathbf{v}_m\|}\right).\]
From Formula to Algorithm
Turning this into code just means walking the path once to get \(L\), then walking it again while keeping a running total, drawing every full segment until the running total would overshoot \(p\cdot L\), and drawing only a partial last segment at that point:
let length = 0;
for (let k = 1; k < path.length; k++) {
length += norm(path[k] - path[k - 1]);
}
let partial = 0;
for (let k = 1; k < path.length; k++) {
const A = path[k - 1];
const B = path[k];
const d = norm(B - A);
if (partial + d <= p * length) {
drawLine(A, B);
} else {
drawLine(A, A + (B - A) * (p * length - partial) / d);
break;
}
partial += d;
} The expression partial + d is computed twice here — once in the condition, once again (as partial, after the update) on the next iteration. Updating partial before the check instead removes that duplication; the inner formula then needs partial - d subtracted back out, which is just A + (B - A) * (p * length - (partial - d)) / d, and simplifies to B + (B - A) * (p * length - partial) / d:
let length = 0;
for (let k = 1; k < path.length; k++) {
length += norm(path[k] - path[k - 1]);
}
let partial = 0;
for (let k = 1; k < path.length; k++) {
const A = path[k - 1];
const B = path[k];
const d = norm(B - A);
partial += d;
if (partial <= p * length) {
drawLine(A, B);
} else {
drawLine(A, B + (B - A) * (p * length - partial) / d);
break;
}
} Removing the Branch
Call \(r = (p\cdot L - \text{partial})/d\) the local coordinate along the current segment, measured backwards from \(B\). Once partial has been updated for the current segment, \(r\) is exactly \(0\) for a segment that's fully inside the drawn portion (the target is still further down the path, at or beyond \(B\)), drops to somewhere in \([-1, 0]\) for the one segment that actually contains the target point, and keeps falling below \(-1\) for every segment after that (the target was already reached earlier). Clamping \(r\) to \([-1, 0]\) therefore reproduces the exact same three cases as the explicit branch above, without ever branching:
let length = 0;
for (let k = 1; k < path.length; k++) {
length += norm(path[k] - path[k - 1]);
}
let partial = 0;
for (let k = 1; k < path.length; k++) {
const A = path[k - 1];
const B = path[k];
const d = norm(B - A);
partial += d;
drawLine(A, B + (B - A) * clamp((p * length - partial) / d, -1, 0));
} It is easy to get this the wrong way round — clamping to \([0, 1]\) instead looks plausible at first glance, since \(r\) reads like "a fraction of the segment", but it isn't one measured from \(A\): the formula is anchored at \(B\), so the fraction runs backwards, and \([-1, 0]\) is the range that actually keeps \(Q\) on the segment. The one downside compared to the branching versions is that the early exit is gone: every segment past the target still runs through a (now zero-length) drawLine call, which is harmless but no longer saves the remaining iterations.