ETOOBUSY 🚀 minimal blogging for the impatient
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!