Ellipses (for SVG): transformation implementation

TL;DR

Let’s use the maths for converting from the SVG Path syntax for ellipses to the parametric representation, which will allow us to eventually calculate the bounding box.

In our previous posts:

we looked at the math for finding the equations that allow us to express an arc of ellipse in parametric representation (see Ellipses (for SVG): parameter and angles) starting from what is available in the SVG path attribute d (see Ellipses (for SVG): mapping to SVG representation).

Implementation time! This function provides us the parametric representation parameters:

 1 sub ellipse_p2c ($P0, $P1, $R, $xdegs, $fA, $fS) {
 2    my $phi = $xdegs * PI / 180; # turn into radians
 3    my ($sinp, $cosp) = (sin($phi), cos($phi));
 4    my ($x0, $y0, $x1, $y1, $xr, $yr) = map {$_->@{qw< x y >}} ($P0, $P1, $R);
 5    my ($x0t, $y0t) = (($x0 - $x1) / 2, ($y0 - $y1) / 2);
 6    my ($x_0, $y_0) = ($cosp * $x0t + $sinp * $y0t, -$sinp * $x0t + $cosp * $y0t);
 7    my ($x2r, $y2r) = ($xr **2, $yr ** 2);
 8    my $lambda = (($x_0 / $xr)**2 + ($y_0 / $yr)**2);
 9    if ($lambda > 1) { # make it a 1 by expanding $rx and $ry with a factor
10       my $factor = sqrt($lambda);
11       ($xr, $yr) = ($xr * $factor, $yr * $factor);
12       $lambda = 1;
13    }
14    my $cf = sqrt(1 / $lambda - 1);
15    $cf = -$cf if ($fA xor $fS);
16    my ($x_c, $y_c) = (-$cf * $xr * $y_0 / $yr, $cf * $yr * $x_0 / $xr);
17    my ($xc, $yc) = ( # the center, yay!
18       ($x0 + $x1) / 2 + $cosp * $x_c - $sinp * $y_c,
19       ($y0 + $y1) / 2 + $sinp * $x_c + $cosp * $y_c,
20    );
21    my $pi2 = 2 * PI;
22    my ($t_begin, $t_end) = map {
23       atan2($xr * ($_ * $y_0 - $y_c), $yr * ($_ * $x_0 - $x_c));
24    } (1, -1);
25    $t_begin -= $pi2 if $t_begin > $t_end; # now $t_begin < $t_end
26    my $delta = $t_end - $t_begin;
27    $t_begin = $t_end             # swap if...
28       if ($delta <= PI && $fA)   # arc is short but long is required
29       || ($delta > PI && ! $fA); # arc is long but short is required
30    # adjust $t_begin in [0, 2*PI[
31    $t_begin -= floor($t_begin / $pi2) * $pi2;
32    $t_end = $t_begin + $delta;
33    return (
34       {x => $xc, y => $yc}, # center
35       {x => $xr, y => $yr}, # radii, possibly scaled up
36       $phi,                 # ellipse rotation angle
37       $t_begin,             # begining value for parameter t
38       $t_end,               # ending value for parameter t
39    );
40 }

Line 2 converts the rotation angle in radians (because it’s originally in degrees), so no big deal. Lines 3 and 4 calculate the sine and cosine of this angle, which will be used extensively during the function.

Line 4 simply unwraps our input points $P0$ and $P1$ (corresponding to $\mathbf{P}_1$ and $\mathbf{P}_2$ respectively, just to confuse things a bit 🤓) and the two radii. In this function we stick to a convention where the variable name always starts with the coordinate axis, followed by what it is for (hence, $r_x$ becomes $xr).

Variables $x_0 and $y_0 represent the first point in the translated-then-rotated system (line 6, with a little help from line 5).

Lines 8 through 20 deal with finding the center (see Ellipses (for SVG): finding the center). In particular, $\Lambda$ is calculated in line 8 and radii are adjusted if needed (lines 9 through 13).

After line 13, we know that $lambda is not greater than $1$ so we can take the square root in line 14 calculating the center factor $cf, i.e. the factor that both $C’_x$ and $C’_y$ have in common. Its sign is then adjusted according to the values of the flags (line 15).

Last, line 16 calculates $\mathbf{C’}$ and lines 17 through 20 bring it back to the original coordinate system and give us $\mathbf{C}$.

Lines 21 through 32 deal with calculating the endpoints of the contiguous interval $[t_{begin}, t_{end}]$ for the parameter (see Ellipses (for SVG): parameter values). It’s basically a straightforward implementation of the formulas we already discussed, with the note that $t_{begin}$ (i.e. $t_begin) is shifted to live in the interval $[0, 2\pi[$ (line 31, although this is not strictly necessary).

This is it… we had to swat a bit for these 40 lines of code!


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