SVG path bounding box: arcs of ellipses

TL;DR

Let’s take a look at the bounding box for arcs of ellipses, at the very last!

In the previous post Ellipses (for SVG): transformation implementation we saw the implementation of a function to turn the representation of an arc of ellipse inside a SVG path (based on the two endpoints and other parameters) to a more tractable representation (the parametric representation, in particular) for finding the bounding box.

So, we have a representation (from Ellipses (for SVG): parameter and angles):

\[x(t) = C_x + cos(\phi) \cdot r_x \cdot cos(t) - sin(\phi) \cdot r_y \cdot sin(t) \\ y(t) = C_y + sin(\phi) \cdot r_x \cdot cos(t) + cos(\phi) \cdot r_y \cdot sin(t)\]

and we also have all the needed parameters, including a contiguous range for the parameter $t$… we can move on towards the bounding box!

Maxima and minima

As before with BĂ©zier curves (see SVG path bounding box: quadratic BĂ©zier curves and SVG path bounding box: cubic BĂ©zier curves) we can treat the two axes independently and use the derivatives to find the candidate values of $t$ that might yield us maxima or minima in the required interval.

The two derivatives are:

\[\dot{x}(t) = - cos(\phi) \cdot r_x \cdot sin(t) - sin(\phi) \cdot r_y \cdot cos(t) \\ \dot{y}(t) = - sin(\phi) \cdot r_x \cdot sin(t) + cos(\phi) \cdot r_y \cdot cos(t)\]

Finding the zero for the $X$ coordinate means finding $t$ such that:

\[cos(\phi) \cdot r_x \cdot sin(t) = - sin(\phi) \cdot r_y \cdot cos(t)\]

We can find this with the help of atan2:

\[t_{x,1} = atan2(-sin(\phi) \cdot r_y, cos(\phi) \cdot r_x)\]

Note that there is also another candidate that is spaced $\pi$ apart:

\[t_{x,2} = t_{x,1} + \pi\]

Both these two candidates also have equivalents that are spaced by multiples of $2\pi$, of course. We should find the two equivalents that are closer to $t_{begin}$ and greater than it, then check if the actually fall in the $[t_{begin}, t_{end}]$ interval; if any of them does, then it must be considered for the bounding box evaluation.

Similar equations and reasoning can be done for the $Y$ axis, of course.

Implementation

On to the implementation:

 1 sub ellipse_bb ($C, $R, $phi, $t_start, $t_end) {
 2    my %retval = (min => {}, max => {});
 3    my $pi2 = 2 * PI;
 4    $_ -= floor($_ / $pi2) * $pi2 for ($t_start, $t_end);
 5    $t_end += $pi2 if $t_end < $t_start;
 6 
 7    # align to a sane range, might be improved...
 8    my ($Rx, $Ry, $cosp, $sinp) = ($R->{x}, $R->{y}, cos($phi), sin($phi));
 9    for my $axis (qw< x y >) {
10       my @candidates = ($t_start, $t_end);
11       my $theta = atan2(-$Ry * $sinp, $Rx * $cosp);
12       for (1 .. 2) {
13          $theta -= floor($theta / $pi2) * $pi2; # normale
14          $theta += $pi2 if $theta < $t_start;  # try to fit in range
15          push @candidates, $theta if $theta < $t_end;
16          $theta += PI; # prepare for 2nd iteration
17       }
18 
19       for my $theta (@candidates) {
20          my $v = $C->{$axis} +
21             $Rx * $cosp * cos($theta) +
22             $Ry * $sinp * sin($theta);
23          $retval{min}{$axis} //= $retval{max}{$axis} //= $v;
24          if    ($v < $retval{min}{$axis}) { $retval{min}{$axis} = $v }
25          elsif ($v > $retval{max}{$axis}) { $retval{max}{$axis} = $v }
26       }
27 
28       ($cosp, $sinp) = (-$sinp, $cosp); # prepare for y-iteration
29    }
30    return \%retval;
31 }

Lines 3 through 5 re-normalize the interval putting $t_{begin}$ inside $[0, 2\pi[$ and $t_{end}$ immediately after it.

The two axes are treated separately (line 9), just like we did for BĂ©zier curves. Our candidates for finding the minima and maxima start with our endpoints (line 10), then we calculate $t_1$ using atan2 (line 11) and establish whether it or its $\pi$-spaced sibling fall in the interval (lines 12 through 17).

Lines 19 through 26 take care to evaluate the parametric function in the candidate values for $t$ and update the bounding box accordingly.

Line 28 is… peculiar. To reuse the same exact formulas of the $X$ axis also for the $Y$ axis, it’s sufficient to do the indicating swapping… cool, this saves us a lot of cut-and-paste!

Conclusions

We now have the last missing piece in our quest for the bounding box of a SVG path… it’s been funny, hasn’t it?!? 🤓


Comments? Octodon, , GitHub, Reddit, or drop me a line!