The race is on
Here’s a perfectly relatable premise. The person next to you is watching a motorsport competition, passionately voicing every twist and turn. Suddenly, you focus on a corner of the telly, where a graphic updates the drivers as they loop around the circuit and a thought pops into your head: wouldn’t that make for an exciting SMIL demo?
Motion
The concept is closely related to a certain buzzing bee, at least at first.
Draw a believable car and update its position with the <animateMotion> element.
<g>
<animateMotion
dur="5s"
rotate="auto"
path="..."
/>
<!-- ... -->
</g> The core of the animation is the path attribute, where you add the string you usually see in the d attribute of <path> elements. And for once, we are interested in drawing the actual track.
<path d="M 40 5 h 20 a 15 15 0 0 1 0 30 h -40 a 15 15 0 0 1 0 -30 h 20" /> Conveniently, it is possible to avoid repeating the instruction.
add an
idto the element<path id="track" d="M 40 5 ..." />instead of closing
<animateMotion>immediately, add the<mpath>element pointing to the path by reference<animateMotion dur="5s" rotate="auto"> <mpath href="#track" /> </animateMotion>
In this manner you can draw the track and use the same element to animate the driver. And over one lap, the race is set.
For more laps, we can repeat the instruction with the repeatCount attribute, but after a while, the motion is bound to get tedious.
We still want to draw the track, but it would be intriguing to have the car follow its guidance less than faithfully.
Paths
Remember a cherished celestial body. To draw the cutesy mascot we traced the outline of a circle, converting between polar and cartesian coordinates. The idea here is similar.
Switch the track layout to a <circle> with a given radius.
<circle r="25" /> For the path guiding the car, replicate the surface with a number of points.
const points = Array(12)
.fill()
.map((_, i, { length }) => {
const offset = 25;
// ...
}); I’ll refer you to the inspiring article for the in-depth math, but past the consideration of the radians, cosine and sine functions, points describes an array of [x, y]coordinates.
In this moment, you have a series of points mimicking the round shape.
The challenge then is to connect the dots.
The most immediate way is to add the coordinates one after the other with straight lines. Start with the first point.
const [x0, y0] = points[0];
let d = `M ${x0} ${y0}`; Continue the d attribute with the L character and the remaining coordinates.
for (let i = 1; i < points.length; i++) {
const [x, y] = points[i];
d += `L ${x} ${y}`;
} Close the path with the first point.
d += `L ${x0} ${y0}`; This would work to recreate the circle, but in a rather unimpressive manner.
What is more, the animation along the path becomes rather janky.
Add the string in the <animateMotion> element — this time, without the assistance of the convenient <mpath>.
<animateMotion
dur="5s"
path={d}
/> And with each point the car turns with an immediate, evident jump. Always in the same direction as the vehicle updates the trajectory.
A better solution falls back to the humble quadratic bezier curve and a brand new character. Consider a basic instruction, formulating a curve with the letter Q and a single control point.
<path
d="
M 0 0
Q 10 -10 20 0
"
/> If you were to follow the syntax with the letter T you would complete the path with a smooth curve.
<path
d="
M 0 0
Q 10 -10 20 0
T 40 0
"
/> You only need the values for where the curve ends, and the browser mirrors the control point for the second part. The effect is rather intriguing, and much clearer if you toy with the syntax.
d="M 0 0 Q 10 -10 20 0 T 40 0"
Back to the array of coordinates, let’s try to include the new character in the path. Once more, start with the first point.
const [x0, y0] = points[0];
let d = `M ${x0} ${y0}`; At this juncture, we want to connect with the second point with the Q character.
const [x1, y1] = points[1]; What about the control point, though? The T character doesn’t require one, but the first quadratic curve surely needs it.
// ...cx, cy
d += `Q ${cx} ${cy} ${x1} ${y1}`; The point halfway between the two dots gives a first, if simplistic, value.
const cx = (x0 + x1) / 2;
const cy = (y0 + y1) / 2; Small hiccup: the point sits right in the center of the line connecting the two ends and you end up with a straight line.
The good news is that you do not need to ponder the complex math for perpendicular and bisecting lines. At least you don’t have to. Since the remaining points are offset in both dimensions, they are connected with nice, gentle curves.
If you are looking for a quick fix, however, you can take advantage of the current situation. The Q character connects the top-most point with the one right next to it. This means the second point is always down and to the right.
Introduce a random offset, pushing the control point downwards.
const noise = Math.random();
const cy = (y0 + y1) / 2 + noise; And as the point gets closer to the end coordinate, move the same back, in the opposite direction.
const cx = (x0 + x1) / 2 - noise; Of course you can experiment with a negative offset, in which case the logic is mirrored. Vertically, the point gets closer to the origin, and you compensate for the change horizontally, pushing the point further away.
Enough workarounds, however. Thanks to the T character needing only one set of values, the remaining instructions are much simpler. Continue with the points, making sure to skip the two points you already considered.
for (let i = 2; i < points.length; i++) {
const [x, y] = points[i];
d += `T ${x} ${y}`;
} And once again, complete the path with the origin.
d += `T ${x0} ${y0}`; If not realistic, the animation becomes incredibly more entertaining than the previous, regular version.
With a hint of randomness you are bound to be drawn into the race. You can even repeat the exercise for more laps. For this you need to create as many paths as necessary and chain the animations one after the other.
<animateMotion
id="lap"
dur="5s"
path={d0}
/>
<animateMotion
begin="lap.end"
dur="5s"
path={d1}
/> And why not, you could even extend the concept to more drivers. Wouldn’t that be fun? You could experiment with the duration as well and wonder, who’s going to win?
Of course you can still wonder whether cars would ever spin around a circle, following the design of a pretty dumb architect. But instead of considering more elaborate designs, take a moment to reflect on the visual. You are not restricted to solid ground. And that cherished celestial body might inspire more than a layout. Even a scenario where the circular motion just makes sense.