Place Markers Along a Path
Sometimes you have a curve and you need to put things on it: dots along a hike trail, frame markers on an animation timeline, station pins on a route, or a dashed-by-hand guide line. The trick is to ask the path how long it is, then sample points at regular intervals.
The basics
Path::parse() accepts any SVG path data string. From there, getLength() returns the total length and getPointAtLength($l) returns a Point (with public x / y) at distance $l along the curve.
<?php
use Atelier\Svg\Path\Path;
$trail = Path::parse('M20,80 C 80,20 220,20 280,80');
$length = $trail->getLength(); // 281.82…
$mid = $trail->getPointAtLength($length / 2); // ~Point(150, 35)
Length is in user units - the same units your viewBox uses. There's no DPI involved.
Drop evenly-spaced markers
Pick a count, divide the length, sample at each step. Closing the loop with <= $count ensures both endpoints are hit.
<?php
use Atelier\Svg\Path\Path;
use Atelier\Svg\Svg;
$d = 'M20,80 C 80,20 220,20 280,80';
$trail = Path::parse($d);
$length = $trail->getLength();
$svg = Svg::create(300, 100)
->path($d, ['stroke' => '#888', 'fill' => 'none', 'stroke-width' => '1']);
$count = 7;
for ($i = 0; $i <= $count; $i++) {
$point = $trail->getPointAtLength($length * $i / $count);
$svg->circle($point->x, $point->y, 3, ['fill' => '#3b82f6']);
}
$svg->save('trail.svg');
Note that the path data is passed twice: once to Path::parse() for measuring, once to Svg::path() so the curve is actually drawn. The path geometry and its rendered element are independent - Path is a measurement tool, not a DOM element.
Step every N units
If you want a dot every 20 units regardless of total length, drive the loop by distance:
<?php
$step = 20;
for ($l = 0; $l <= $length; $l += $step) {
$point = $trail->getPointAtLength($l);
$svg->circle($point->x, $point->y, 2, ['fill' => '#888']);
}
For a 282-unit trail with $step = 20, you'll get 15 markers - the last one slightly before the end. If reaching the endpoint exactly matters, add $trail->getPointAtLength($length) after the loop.
Place differently-sized markers (start, mid, end)
Mix custom rendering at known positions:
<?php
$start = $trail->getPointAtLength(0);
$end = $trail->getPointAtLength($length);
$mid = $trail->getPointAtLength($length / 2);
$svg->circle($start->x, $start->y, 5, ['fill' => '#10b981']); // origin
$svg->circle($end->x, $end->y, 5, ['fill' => '#ef4444']); // destination
$svg->circle($mid->x, $mid->y, 4, ['fill' => 'none',
'stroke' => '#f59e0b',
'stroke-width' => '1.5']); // checkpoint
Distribute by content, not distance
Sometimes you have N things to lay out (waypoints, frames, labels) and the spacing should adapt. Use the count-based loop and pull labels from a parallel array:
<?php
$labels = ['Start', 'A', 'B', 'C', 'Finish'];
$count = count($labels) - 1;
foreach ($labels as $i => $label) {
$p = $trail->getPointAtLength($length * $i / $count);
$svg->circle($p->x, $p->y, 3, ['fill' => '#3b82f6']);
$svg->text($p->x, $p->y - 8, $label, [
'text-anchor' => 'middle',
'font-size' => '10',
'fill' => '#444',
]);
}
Works for any path, not just curves
Path::parse() understands the full SVG path mini-language: M, L, C, Q, A, Z, and their relative variants. You can also build a Path from primitives:
<?php
$ring = Path::circle(150, 50, 40); // closed circle as a path
$square = Path::rectangle(40, 20, 60, 60);
$star = Path::star(150, 50, 30, 15, 5);
// Same API works on all of them:
$length = $ring->getLength(); // 226.27 - cubic-Bezier approximation of a circle
$top = $ring->getPointAtLength(0); // (110, 50) - leftmost point of the ring
So you can place petals around a circle, anchor labels on the corners of a star, or seed particles along a polygon outline using the same loop.
Quick reference
| Why | |
|---|---|
Path::parse($d) |
Parse a d attribute string into a measurable path |
Path::circle($cx, $cy, $r) and friends |
Build measurable paths from primitives without serializing |
$path->getLength() |
Total length in user units (same as viewBox) |
$path->getPointAtLength($l) |
Point on the curve at a given distance - returns null past the end |
Loop $i = 0; $i <= $count; $i++ |
Evenly-spaced markers including both endpoints |
Loop $l = 0; $l <= $length; $l += $step |
Fixed-distance markers (last one may fall short of the end) |
See also
- Path overview - full Path API
- Animate shapes - use measured points as animation frames
- Build charts - distribute data points along axes