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
P1 and P2 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, rx 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, Λ 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 C′ and lines 17 through 20 bring it back to the original coordinate system and give us C.
Lines 21 through 32 deal with calculating the endpoints of the
contiguous interval [tbegin,tend] 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 tbegin (i.e. $t_begin
) is shifted to live in
the interval [0,2Ď€[ (line 31, although this is not strictly
necessary).
This is it… we had to swat a bit for these 40 lines of code!