SVG Essentials/Paths
From WikiContent
(Initial conversion from Docbook) |
m |
||
(7 intermediate revisions not shown.) | |||
Line 273: | Line 273: | ||
#!/usr/bin/perl | #!/usr/bin/perl | ||
+ | use Math::Trig; | ||
# | # | ||
Line 301: | Line 302: | ||
{ | { | ||
my ($cx, $cy, $rx, $ry, $theta1, $delta, $phi) = @_; | my ($cx, $cy, $rx, $ry, $theta1, $delta, $phi) = @_; | ||
- | my ($theta2, $pi); | ||
- | my ($x0, $y0, $x1, $y1, $large_arc, $sweep); | ||
- | + | my $theta2 = $delta + $theta1; | |
- | + | $theta1 = deg2rad($theta1); | |
- | + | $theta2 = deg2rad($theta2); | |
- | + | my $phi_r = deg2rad($phi); | |
- | + | ||
- | + | ||
- | $theta1 = $theta1 | + | |
- | $theta2 = $theta2 | + | |
- | $phi_r = $phi | + | |
# | # | ||
Line 318: | Line 312: | ||
# ending points | # ending points | ||
# | # | ||
- | $x0 = $cx + cos($phi_r) * $rx * cos($theta1) + | + | my $x0 = $cx + cos($phi_r) * $rx * cos($theta1) + |
sin(-$phi_r) * $ry * sin($theta1); | sin(-$phi_r) * $ry * sin($theta1); | ||
- | $y0 = $cy + sin($phi_r) * $rx * cos($theta1) + | + | my $y0 = $cy + sin($phi_r) * $rx * cos($theta1) + |
cos($phi_r) * $ry * sin($theta1); | cos($phi_r) * $ry * sin($theta1); | ||
- | $x1 = $cx + cos($phi_r) * $rx * cos($theta2) + | + | my $x1 = $cx + cos($phi_r) * $rx * cos($theta2) + |
sin(-$phi_r) * $ry * sin($theta2); | sin(-$phi_r) * $ry * sin($theta2); | ||
- | $y1 = $cy + sin($phi_r) * $rx * cos($theta2) + | + | my $y1 = $cy + sin($phi_r) * $rx * cos($theta2) + |
cos($phi_r) * $ry * sin($theta2); | cos($phi_r) * $ry * sin($theta2); | ||
- | $large_arc = ($delta > 180) ? 1 : 0; | + | my $large_arc = ($delta > 180) ? 1 : 0; |
- | $sweep = ($delta > 0) ? 1 : 0; | + | my $sweep = ($delta > 0) ? 1 : 0; |
return ($x0, $y0, $rx, $ry, $phi, $large_arc, $sweep, $x1, $y1); | return ($x0, $y0, $rx, $ry, $phi, $large_arc, $sweep, $x1, $y1); | ||
Line 382: | Line 376: | ||
#!/usr/bin/perl | #!/usr/bin/perl | ||
- | + | use Math::Trig; | |
- | + | ||
- | + | ||
- | + | ||
- | + | ||
# | # | ||
Line 416: | Line 406: | ||
{ | { | ||
my ($x0, $y0, $rx, $ry, $phi, $large_arc, $sweep, $x, $y) = @_; | my ($x0, $y0, $rx, $ry, $phi, $large_arc, $sweep, $x, $y) = @_; | ||
- | my ($cx, $cy, $theta, $delta, $phi); | ||
- | # a plethora of temporary variables | ||
- | my ( | ||
- | $dx2, $dy2, $phi_r, $x1, $y1, | ||
- | $rx_sq, $ry_sq, | ||
- | $x1_sq, $y1_sq, | ||
- | $sign, $sq, $coef, | ||
- | $cx1, $cy1, $sx2, $sy2, | ||
- | $p, $n, | ||
- | $ux, $uy, $vx, $vy | ||
- | ); | ||
- | |||
# Compute 1/2 distance between current and final point | # Compute 1/2 distance between current and final point | ||
- | $dx2 = ($x0 - $x) / 2.0; | + | my $dx2 = ($x0 - $x) / 2.0; |
- | $dy2 = ($y0 - $y) / 2.0; | + | my $dy2 = ($y0 - $y) / 2.0; |
# Convert from degrees to radians | # Convert from degrees to radians | ||
- | $pi = atan2(1, 1) * 4.0; | ||
$phi %= 360; | $phi %= 360; | ||
- | $phi_r = $phi | + | my $phi_r = deg2rad($phi); |
# Compute (x1, y1) | # Compute (x1, y1) | ||
- | $x1 = cos($phi_r) * $dx2 + sin($phi_r) * $dy2; | + | my $x1 = cos($phi_r) * $dx2 + sin($phi_r) * $dy2; |
- | $y1 = -sin($phi_r) * $dx2 + cos($phi_r) * $dy2; | + | my $y1 = -sin($phi_r) * $dx2 + cos($phi_r) * $dy2; |
# Make sure radii are large enough | # Make sure radii are large enough | ||
$rx = abs($rx); $ry = abs($ry); | $rx = abs($rx); $ry = abs($ry); | ||
- | $rx_sq = $rx * $rx; | + | my $rx_sq = $rx * $rx; |
- | $ry_sq = $ry * $ry; | + | my $ry_sq = $ry * $ry; |
- | $x1_sq = $x1 * $x1; | + | my $x1_sq = $x1 * $x1; |
- | $y1_sq = $y1 * $y1; | + | my $y1_sq = $y1 * $y1; |
$radius_check = ($x1_sq / $rx_sq) + ($y1_sq / $ry_sq); | $radius_check = ($x1_sq / $rx_sq) + ($y1_sq / $ry_sq); | ||
Line 460: | Line 437: | ||
# Step 2: Compute (cx1, cy1) | # Step 2: Compute (cx1, cy1) | ||
- | $sign = ($large_arc == $sweep) ? -1 : 1; | + | my $sign = ($large_arc == $sweep) ? -1 : 1; |
- | $sq = (($rx_sq * $ry_sq) - ($rx_sq * $y1_sq) - ($ry_sq * $x1_sq)) / | + | my $sq = (($rx_sq * $ry_sq) - ($rx_sq * $y1_sq) - ($ry_sq * $x1_sq)) / |
(($rx_sq * $y1_sq) + ($ry_sq * $x1_sq)); | (($rx_sq * $y1_sq) + ($ry_sq * $x1_sq)); | ||
$sq = ($sq < 0) ? 0 : $sq; | $sq = ($sq < 0) ? 0 : $sq; | ||
- | $coef = | + | my $coef = $sign * sqrt($sq); |
- | $cx1 = $coef * (($rx * $y1) / $ry); | + | my $cx1 = $coef * (($rx * $y1) / $ry); |
- | $cy1 = $coef * -(($ry * $x1) / $rx); | + | my $cy1 = $coef * -(($ry * $x1) / $rx); |
# Step 3: Compute (cx, cy) from (cx1, cy1) | # Step 3: Compute (cx, cy) from (cx1, cy1) | ||
- | $sx2 = ($x0 + $x) / 2.0; | + | my $sx2 = ($x0 + $x) / 2.0; |
- | $sy2 = ($y0 + $y) / 2.0; | + | my $sy2 = ($y0 + $y) / 2.0; |
- | $cx = $sx2 + (cos($phi_r) * $cx1 - sin($phi_r) * $cy1); | + | my $cx = $sx2 + (cos($phi_r) * $cx1 - sin($phi_r) * $cy1); |
- | $cy = $sy2 + (sin($phi_r) * $cx1 + cos($phi_r) * $cy1); | + | my $cy = $sy2 + (sin($phi_r) * $cx1 + cos($phi_r) * $cy1); |
# Step 4: Compute angle start and angle extent | # Step 4: Compute angle start and angle extent | ||
- | $ux = ($x1 - $cx1) / $rx; | + | my $ux = ($x1 - $cx1) / $rx; |
- | $uy = ($y1 - $cy1) / $ry; | + | my $uy = ($y1 - $cy1) / $ry; |
- | $vx = (-$x1 - $cx1) / $rx; | + | my $vx = (-$x1 - $cx1) / $rx; |
- | $vy = (-$y1 - $cy1) / $ry; | + | my $vy = (-$y1 - $cy1) / $ry; |
- | $n = sqrt( ($ux * $ux) + ($uy * $uy) ); | + | my $n = sqrt( ($ux * $ux) + ($uy * $uy) ); |
- | $p = $ux; # 1 * ux + 0 * uy | + | my $p = $ux; # 1 * ux + 0 * uy |
$sign = ($uy < 0) ? -1 : 1; | $sign = ($uy < 0) ? -1 : 1; | ||
- | $theta = $sign * acos( $p / $n ); | + | my $theta = $sign * acos( $p / $n ); |
- | $theta = $theta | + | $theta = rad2deg($theta); |
$n = sqrt(($ux * $ux + $uy * $uy) * ($vx * $vx + $vy * $vy)); | $n = sqrt(($ux * $ux + $uy * $uy) * ($vx * $vx + $vy * $vy)); | ||
$p = $ux * $vx + $uy * $vy; | $p = $ux * $vx + $uy * $vy; | ||
$sign = (($ux * $vy - $uy * $vx) < 0) ? -1 : 1; | $sign = (($ux * $vy - $uy * $vx) < 0) ? -1 : 1; | ||
- | $delta = $sign * acos( $p / $n ); | + | my $delta = $sign * acos( $p / $n ); |
- | $delta = $delta | + | $delta = rad2deg($delta); |
if ($sweep == 0 && $delta > 0) | if ($sweep == 0 && $delta > 0) |
Current revision
All of the basic shapes described in Chapter 3 are really shorthand forms for the more general <path> element. You are well advised to use these shortcuts; they help make your SVG more readable and more structured. The <path> element is more general; it draws the outline of any arbitrary shape by specifying a series of connected lines, arcs, and curves. This outline can be filled and drawn with a stroke, just as the basic shapes are. Additionally, these paths (as well as the shorthand basic shapes) may be used to define the outline of a clipping area or a transparency mask, as you will see in Chapter 9.
All of the data describing an outline is in the <path> element's d attribute (the d stands for data). The path data consists of one-letter commands, such as M for moveto or L for lineto, followed by the coordinate information for that particular command.
Contents |
moveto, lineto, and closepath
Every path must begin with a moveto command. The command letter is a capital M followed by an x- and y-coordinate, separated by commas or whitespace. This command sets the current location of the "pen" that's drawing the outline.
This is followed by one or more lineto commands, denoted by a capital L, also followed by x- and y-coordinates, and separated by commas or whitespace. Example 6-1 has three paths. The first draws a single line, the second draws a right angle, and the third draws two thirty-degree angles. When you "pick up" the pen with another moveto, you are starting a new subpath. Notice that the use of commas and whitespace as separators is different, but perfectly legal, in all three paths. The result is Figure 6-1.
Example 6-1. Using moveto and lineto
<g style="stroke: black; fill: none;"> <!-- single line --> <path d="M 10 10 L 100 10"/> <!-- a right angle --> <path d="M 10, 20 L 100, 20 L 100,50"/> <!-- two thirty-degree angles --> <path d="M 40 60, L 10 60, L 40 42.68, M 60 60, L 90 60, L 60 42.68"/> </g>
Examining the last path more closely:
Value | Action |
---|---|
M 40 60 | Move pen to (40, 60) |
L 10 60 | Draw a line to (10, 60) |
L 40 42.68 | Draw a line to (40, 42.68) |
M 60 60 | Start a new subpath; move pen to (60, 60) — no line is drawn |
L 90 60 | Draw a line to (90, 60) |
L 60 42.68 | Draw a line to (60, 42.68) |
Note
You may have noticed that the path data doesn't look very much like the typical values for XML attributes. Because the entire path data is contained in one attribute rather than an individual element for each point or line segment, a path takes up less memory when read into a Document Object Model structure by an XML parser. Additionally, a path's compact notation allows a complex graphic to be transmitted without requiring a great deal of bandwidth.
If you want to use a <path> to draw a rectangle, you can draw all four lines, or you can draw the first three lines and then use the closepath command, denoted by a capital Z, to draw a straight line back to the beginning point of the current subpath. Example 6-2 is the SVG for Figure 6-2, which shows a rectangle drawn the hard way, a rectangle drawn with closepath, and a path that draws two triangles by opening and closing two subpaths.
Example 6-2. Using closepath
<g style="stroke: black; fill: none;"> <!-- rectangle; all four lines --> <path d="M 10 10, L 40 10, L 40 30, L 10 30, L 10 10"/> <!-- rectangle with closepath --> <path d="M 60 10, L 90 10, L 90 30, L 60 30, Z"/> <!-- two thirty-degree triangles --> <path d="M 40 60, L 10 60, L 40 42.68, Z M 60 60, L 90 60, L 60 42.68, Z"/> </g>
Examining the last path more closely:
Value | Action |
---|---|
M 40 60 | Move pen to (40, 50) |
L 10 60 | Draw a line to (10, 60) |
L 40 42.68 | Draw a line to (40, 42.68) |
Z | Close path by drawing a straight line to (40, 60), where this subpath began |
M 60 60 | Start a new subpath; move pen to (60, 60) — no line is drawn |
L 90 60 | Draw a line to (90, 60) |
L 60 42.68 | Draw a line to (60, 42.68) |
Z | Close path by drawing a straight line to (60, 60), where this subpath began |
Relative moveto and lineto
The preceding commands are all represented by uppercase letters, and the coordinates are presumed to be absolute coordinates. If you use a lowercase command letter, the coordinates are interpreted as being relative to the current pen position. Thus, the following two paths are equivalent:
<path d="M 10 10 L 20 10 L 20 30 M 40 40 L 55 35" style="stroke: black;"/> <path d="M 10 10 l 10 0 l 0 20 m 20 10 l 15 -5" style="stroke: black;"/>
If you start a path with a lowercase m (moveto), its coordinates will be interpreted as an absolute position since there's no previous pen position from which to calculate a relative position. All the other commands in this chapter also have the same upper- and lowercase distinction. An uppercase command's coordinates are absolute and a lowercase command's coordinates are relative. The closepath command, which has no coordinates, has the same effect in both upper- and lowercase.
Path Shortcuts
If content is king and design is queen, then bandwidth efficiency is the royal courtier who keeps the palace running smoothly. Since any non-trivial drawing will have paths with many tens of coordinate pairs, the <path> element has shortcuts that allow you to represent a path in as few bytes as possible.
The Horizontal lineto and Vertical lineto Commands
Since horizontal and vertical lines are so common, a path may specify a horizontal line with an H command followed by an absolute x-coordinate or an h command followed by a relative x-coordinate. Similarly, a vertical line is specified with a V command followed by an absolute y-coordinate or a v command followed by a relative y-coordinate.
Shortcut | Equivalent to | Effect |
---|---|---|
H 20 | L 20 current_y | Draws a line to absolute location (20, current_y) |
h 20 | l 20 0 | Draws a line to (current_x+20, current_y) |
V 20 | L current_x 20 | Draws a line to absolute location (current_x, 20) |
v 20 | l current_x 20 | Draws a line to location (current_x, current_y+20) |
Thus, the following path draws a rectangle 15 units in width and 25 units in height, with the upper left corner at coordinates (12, 24).
<path d="M 12 24 h 15 v 25 h -15 z"/>
Notational Shortcuts for a Path
Paths can also be made shorter by applying the following two rules:
- You may place multiple sets of coordinates after an L or l, just as you do in the <polyline> element. The following six paths all draw the same diamond that is shown in Figure 6-3; the first three are in absolute coordinates and the last three in relative coordinates. The third and sixth paths have an interesting twist — if you place multiple pairs of coordinates after a moveto, all the pairs after the first are presumed to be preceded by a lineto.^{[1]}
<path d="M 30 30 L 55 5 L 80 30 L 55 55 Z"/> <path d="M 30 30 L 55 5 80 30 55 55 Z"/> <path d="M 30 30 55 5 80 30 55 55 Z"/> <path d="m 30 30 l 25 -25 l 25 25 l -25 25 z"/> <path d="m 30 30 l 25 -25 25 25 -25 25 z"/> <path d="m 30 30 25 -25 25 25 -25 25 z"/>
- Any whitespace that is not necessary may be eliminated. You don't need a blank after a command letter since all commands are one letter only. You don't need a blank between a number and a command since the command letter can't be part of the number. You don't need a blank between a positive and a negative number since the leading minus sign of the negative number can't be a part of the positive number. This lets you reduce the third and sixth paths in the preceding listing even further:
<path d="M30 30 55 5 80 30 55 55Z"/> <path d="m30 30 25-25 25 25-25 25z"/>
Another example of the whitespace elimination rule in action is shown by the example that drew a rectangle 15 units in width and 25 units in height, with the upper left corner at coordinates (12, 24):
<path d="M 12 24 h 15 v 25 h -15 z"/> <!-- original --> <path d="M12 24h15v25h-15z"/> <!-- shorter -->
Elliptical Arc
Lines are simple; two points on a path uniquely determine the line segment between them. Since an infinite number of curves can be drawn between two points, you must give additional information to draw a curved path between them. The simplest of the curves we will examine is the elliptical arc — that is, drawing a section of an ellipse that connects two points.
Although arcs are visually the simplest curves, specifying a unique arc requires the most information. The first pieces of information you need to specify are the x- and y-radii of the ellipse on which the points lie. This narrows it down to two possible ellipses, as you can see in section (a) of Figure 6-4. The two points divide the two ellipses into four arcs. Two of them, (b) and (c), are arcs that measure less than 180 degrees. The other two, (d) and (e) are greater than 180 degrees. If you look at (b) and (c), you will notice that they are differentiated by their direction; (b) is drawn in the direction of increasing negative angle, and (c) in the direction of increasing positive angle. The same relationship holds true between (d) and (e).
But wait — we still haven't uniquely specified the potential arcs! There's no law that says that the ellipse has to have its x-radius parallel to the x-axis. Part (f) of Figure 6-4 shows the two points with their candidate ellipses rotated thirty degrees with respect to the x-axis.
(Figure 6-4 is adapted from the one found in section 8.3.8 of the World Wide Web Consortium's SVG specification.)
Thus, an arc command begins with the A abbreviation for absolute coordinates or a for relative coordinates, and is followed by seven parameters:
- The x- and y-radius of the ellipse on which the points lie.
- The x-axis-rotation of the ellipse.
- The large-arc-flag, which is zero if the arc's measure is less than 180 degrees, or one if the arc's measure is greater than or equal to 180 degrees.
- The sweep-flag, which is zero if the arc is to be drawn in the negative angle direction, or one if the arc is to be drawn in the positive angle direction.
- The ending x- and y- coordinates of the ending point. (The starting point is determined by the last point drawn or the last moveto command.)
Here are the paths used to draw the elliptical arcs in sections (b) through (e) of Figure 6-4:
<path d="M 125,75 A100,50 0 0,0 225,125"/> <!-- b --> <path d="M 125,75 A100,50 0 0,1 225,125"/> <!-- c --> <path d="M 125,75 A100,50 0 1,0 225,125"/> <!-- d --> <path d="M 125,75 A100,50 0 1,1 225,125"/> <!-- e -->
As a further example, let's enhance the background that we started in Example 4-8 to complete the yin/yang symbol that is part of the Korean flag. Example 6-3 keeps the full ellipses as <ellipse> elements, but creates the semicircles that it needs with paths. The result is shown in Figure 6-5.
Example 6-3. Using elliptical arc
<!-- gray drop shadow --> <ellipse cx="154" cy="154" rx="150" ry="120" style="fill: #999999;"/> <!-- light blue ellipse --> <ellipse cx="152" cy="152" rx="150" ry="120" style="fill: #cceeff;"/> <!-- large light red semicircle fills upper half, followed by small light red semicircle that dips into lower left half of symbol --> <path d="M 302 152 A 150 120, 0, 1, 0, 2 152 A 75 60, 0, 1, 0, 152 152" style="fill: #ffcccc;"/> <!-- light blue semicircle rises into upper right half of symbol --> <path d="M 152 152 A 75 60, 0, 1, 1, 302 152" style="fill: #cceeff;"/>
Technique: Converting from Other Arc Formats
Some other vector graphics systems let you specify an arc by defining a center point for the ellipse, its x- and y-radius, the starting angle, and the extent of the angle's arc. This is a straightforward method of specification, and is excellent for drawing arcs as single objects. This, paradoxically, is exactly why SVG instead chooses such a seemingly eccentric method to specify arcs. In SVG, an arc is not presumed to be living in lonely splendor; it is intended to be part of a connected path of lines and curves. (For example, a rounded rectangle is precisely that — a series of lines and elliptical arcs.) Thus, it makes sense to specify an arc by its endpoints.
Sometimes, though, you do want an isolated semicircle (or, more accurately, semi-ellipse). Presume that you have an ellipse specified as:
<ellipse cx="cx" cy="cy" rx="rx" ry="ry"/>
Here are the paths to draw the four possible semi-ellipses:
<!-- northern hemisphere --> <path d="M cx-rx cy A rx ry 0 1 1 cx+rx cy"/> <!-- southern hemipshere --> <path d="M cx-rx cy A rx ry 0 1 0 cx+rx cy"/> <!-- eastern hemisphere --> <path d="M cx cy-ry A rx ry 0 1 1 cx cy+ry"/> <!-- western hemisphere --> <path d="M cx cy-ry A rx ry 0 1 0 cx cy+ry"/>
For the more general case, when you wish to draw an arbitrary arc that has been specified in "center-and-angles" notation and wish to convert it to SVG's "endpoint-and-sweep" format, use the following Perl script. It prompts you for center coordinates, radii, starting angle, and angle extent. The output is a <path> tag that you can insert into your SVG files.
#!/usr/bin/perl use Math::Trig; # # Convert an elliptical arc based around a central point # to an elliptical arc parameterized for SVG. # # Input is a list containing: # center x coordinate # center y coordinate # x-radius of ellipse # y-radius of ellipse # beginning angle of arc in degrees # arc extent in degrees # x-axis rotation angle in degrees # # Output is a list containing: # # x-coordinate of beginning of arc # y-coordinate of beginning of arc # x-radius of ellipse # y-radius of ellipse # large-arc-flag as defined in SVG specification # sweep-flag as defined in SVG specification # x-coordinate of endpoint of arc # y-coordinate of endpoint of arc # sub convert_to_svg { my ($cx, $cy, $rx, $ry, $theta1, $delta, $phi) = @_; my $theta2 = $delta + $theta1; $theta1 = deg2rad($theta1); $theta2 = deg2rad($theta2); my $phi_r = deg2rad($phi); # # Figure out the coordinates of the beginning and # ending points # my $x0 = $cx + cos($phi_r) * $rx * cos($theta1) + sin(-$phi_r) * $ry * sin($theta1); my $y0 = $cy + sin($phi_r) * $rx * cos($theta1) + cos($phi_r) * $ry * sin($theta1); my $x1 = $cx + cos($phi_r) * $rx * cos($theta2) + sin(-$phi_r) * $ry * sin($theta2); my $y1 = $cy + sin($phi_r) * $rx * cos($theta2) + cos($phi_r) * $ry * sin($theta2); my $large_arc = ($delta > 180) ? 1 : 0; my $sweep = ($delta > 0) ? 1 : 0; return ($x0, $y0, $rx, $ry, $phi, $large_arc, $sweep, $x1, $y1); } # # Request input # print "Enter center x,y coordinates > "; $data = <>; $data =~ s/,/ /g; ($cx, $cy) = split /\s+/, $data; print "Enter x and y radii > "; $data = <>; $data =~ s/,/ /g; ($rx, $ry) = split/\s+/, $data; print "Enter starting angle in degrees > "; $theta = <>; chomp $theta; print "Enter angle extent in degrees > "; $delta = <>; chomp $delta; print "Enter angle of rotation in degrees > "; $phi = <>; chomp $phi; # # Echo original data # print "(cx,cy)=($cx,$cy) rx=$rx ry=$ry ", "start angle=$theta extent=$delta rotate=$phi\n"; ($x0, $y0, $rx, $ry, $phi, $large_arc_flag, $sweep_flag, $x1, $y1) = convert_to_svg( $cx, $cy, $rx, $ry, $theta, $delta, $phi); # # Produce a <path> element that fits the # specifications # print "<path d=\"M $x0 $y0 ", # Moveto initial point "A $rx $ry ", # Arc command and radii, "$phi ", # angle of rotation, "$large_arc_flag ", # the "large-arc" flag, "$sweep_flag ", # the "sweep" flag, "$x1 $y1\"/>\n"; # and the endpoint
If you wish to convert from the SVG format to a "center-and-angles" format, the mathematics is considerably more complex. You can see the formulas in detail in the SVG specification. This Perl script was adapted from the code in the Apache XML Batik project.
#!/usr/bin/perl use Math::Trig; # # Convert an elliptical arc based around a central point # to an elliptical arc parameterized for SVG. # # Input is a list containing: # # x-coordinate of beginning of arc # y-coordinate of beginning of arc # x-radius of ellipse # y-radius of ellipse # large-arc-flag as defined in SVG specification # sweep-flag as defined in SVG specification # x-coordinate of endpoint of arc # y-coordinate of endpoint of arc # # Output is a list containing: # center x coordinate # center y coordinate # x-radius of ellipse # y-radius of ellipse # beginning angle of arc in degrees # arc extent in degrees # x-axis rotation angle in degrees # sub convert_from_svg { my ($x0, $y0, $rx, $ry, $phi, $large_arc, $sweep, $x, $y) = @_; # Compute 1/2 distance between current and final point my $dx2 = ($x0 - $x) / 2.0; my $dy2 = ($y0 - $y) / 2.0; # Convert from degrees to radians $phi %= 360; my $phi_r = deg2rad($phi); # Compute (x1, y1) my $x1 = cos($phi_r) * $dx2 + sin($phi_r) * $dy2; my $y1 = -sin($phi_r) * $dx2 + cos($phi_r) * $dy2; # Make sure radii are large enough $rx = abs($rx); $ry = abs($ry); my $rx_sq = $rx * $rx; my $ry_sq = $ry * $ry; my $x1_sq = $x1 * $x1; my $y1_sq = $y1 * $y1; $radius_check = ($x1_sq / $rx_sq) + ($y1_sq / $ry_sq); if ($radius_check > 1) { $rx *= sqrt($radius_check); $ry *= sqrt($adius_check); $rx_sq = $rx * $rx; $ry_sq = $ry * $ry; } # Step 2: Compute (cx1, cy1) my $sign = ($large_arc == $sweep) ? -1 : 1; my $sq = (($rx_sq * $ry_sq) - ($rx_sq * $y1_sq) - ($ry_sq * $x1_sq)) / (($rx_sq * $y1_sq) + ($ry_sq * $x1_sq)); $sq = ($sq < 0) ? 0 : $sq; my $coef = $sign * sqrt($sq); my $cx1 = $coef * (($rx * $y1) / $ry); my $cy1 = $coef * -(($ry * $x1) / $rx); # Step 3: Compute (cx, cy) from (cx1, cy1) my $sx2 = ($x0 + $x) / 2.0; my $sy2 = ($y0 + $y) / 2.0; my $cx = $sx2 + (cos($phi_r) * $cx1 - sin($phi_r) * $cy1); my $cy = $sy2 + (sin($phi_r) * $cx1 + cos($phi_r) * $cy1); # Step 4: Compute angle start and angle extent my $ux = ($x1 - $cx1) / $rx; my $uy = ($y1 - $cy1) / $ry; my $vx = (-$x1 - $cx1) / $rx; my $vy = (-$y1 - $cy1) / $ry; my $n = sqrt( ($ux * $ux) + ($uy * $uy) ); my $p = $ux; # 1 * ux + 0 * uy $sign = ($uy < 0) ? -1 : 1; my $theta = $sign * acos( $p / $n ); $theta = rad2deg($theta); $n = sqrt(($ux * $ux + $uy * $uy) * ($vx * $vx + $vy * $vy)); $p = $ux * $vx + $uy * $vy; $sign = (($ux * $vy - $uy * $vx) < 0) ? -1 : 1; my $delta = $sign * acos( $p / $n ); $delta = rad2deg($delta); if ($sweep == 0 && $delta > 0) { $delta -= 360; } elsif ($sweep == 1 && $delta < 0) { $delta += 360; } $delta %= 360; $theta %= 360; return ($cx, $cy, $rx, $ry, $theta, $delta, $phi); } # # Request input # print "Enter starting x,y coordinates > "; $data = <>; $data =~ s/,/ /g; ($x0, $y0) = split /\s+/, $data; print "Enter ending x,y coordinates > "; $data = <>; $data =~ s/,/ /g; ($x, $y) = split /\s+/, $data; print "Enter x and y radii > "; $data = <>; $data =~ s/,/ /g; ($rx, $ry) = split/\s+/, $data; print "Enter rotation angle in degrees "; $phi = <>; chomp $phi; print "Large arc flag (0=no, 1=yes) > "; $large_arc = <>; chomp $large_arc; print "Sweep flag (0=negative, 1=positive) > "; $sweep = <>; chomp $sweep; print "From ($x0,$y0) to ($x,$y) rotate $phi", " large arc=$large_arc sweep=$sweep\n"; ($cx, $cy, $rx, $ry, $theta, $delta, $phi) = convert_from_svg( $x0, $y0, $rx, $ry, $phi, $large_arc, $sweep, $x, $y ); print "Ellipse center = ($cx, $cy)\n"; print "Start angle = $theta\n"; print "Angle extent = $delta\n";
Bézier Curves
Arcs can be characterized as clean and functional, but one would rarely use the word "graceful" to describe them. If you want graceful, you need to use curves which are produced by graphing quadratic and cubic equations. Mathematicians have known about these curves for literally hundreds of years, but drawing them was always a computationally demanding task. This changed when Pierre Bézier, working for French car manufacturer Rénault and Paul de Casteljau, an engineer for Citroën, independently discovered a computationally convenient way to generate these curves.
If you have used graphics programs like Adobe Illustrator, you draw these Bézier curves by specifying two points and then moving a "handle" as shown in the following diagram. The end of this handle is called the control point, because it controls the shape of the curve. As you move the handle, the curve changes in a way that, to the uninitiated, is completely mystifying. Mike Woodburn, a graphic designer at Key Point Software, suggests Figure 6-6 as a way to visualize how the control point and the curve interact: imagine that the line is made of flexible metal. Inside the control point is a magnet; the closer a point is to the control point, the more strongly it is attracted.
Another way to visualize the role of the control point is based on the de Casteljau method of constructing the curves. We will use this approach in the following sections. For further details on the underlying mathematics, presented in a remarkably lucid fashion, see this web site: http://graphics.cs.ucdavis.edu/GraphicsNotes/Bezier-Curves/Bezier-Curves.html.
Quadratic Bézier Curves
The simplest of the Bézier curves is the quadratic curve. You specify a beginning point, an ending point, and a control point. Imagine two tent poles placed at the endpoints of the line. These tent poles meet at the control point. Stretched between the centers of the tent poles is a rubber band. The place where the curve bends is tied to the exact center of that rubber band. This situation is shown in Figure 6-7.
Programs like Adobe Illustrator show you only one of the "tent poles." The next time you're using such a program, mentally add in the second pole and the resulting curves will be far less mysterious.
That's the concept; now for the practical matter of actually producing such a curve in SVG. You specify a quadratic curve in a <path> data with the Q or q command. The command is followed by two sets of coordinates that specify a control point and an endpoint. The uppercase command implies absolute coordinates; lowercase implies relative coordinates. The curve in Figure 6-7 was drawn from (30, 75) to (300, 120) with the control point at (240, 30), and was specified in SVG as follows:
<path d="M30 75 Q 240 30, 300 120" style="stroke: black; fill: none;"/>
You may specify several sets of coordinates after a quadratic curve command. This will generate a polybézier curve. Presume you want a <path> that draws a curve from (30, 100) to (100, 100) with a control point at (80, 30) and then continues with a curve to (200, 80) with a control point at (130, 65). Here is the SVG for this path, with control point coordinates in bold. The result is shown in the left half of Figure 6-8; the control points and lines are shown in the right half of the figure.
<path d="M30 100 Q 80 30, 100 100, 130 65, 200 80"/>
You are probably wondering, "What happened to `graceful?' That curve is just lumpy." This is an accurate assessment. Just because curves are connected doesn't mean that they will look good together. That's why SVG provides the smooth quadratic curve command, which is denoted by the letter T (or t if you want to use relative coordinates). The command is followed by the next endpoint of the curve; the control point is calculated automatically, as the specification says, by "reflection of the control point on the previous command relative to the current point."
Note
For the mathematically inclined, the new control point x2, y2 is calculated from the curve's starting point x, y and the previous control point x1, y1 with these formulas:
x2 = 2 * x - x1 y2 = 2 * y - y1
Here is a quadratic Bézier curve drawn from (30, 100) to (100, 100) with a control point at (80, 30) and then smoothly continued to (200, 80). The left half of Figure 6-9 shows the curve; the right half shows the control points. The reflected control point is shown with a dashed line. Gracefulness has returned!
<path d="M30 100 Q 80 30, 100 100 T 200 80"/>
Cubic Bézier Curves
A single quadratic Bézier curve has exactly one inflection point (the point where the curve changes direction). While these curves are more versatile than simple arcs, we can do even better by using cubic Bézier curves, which can have one or two inflection points.
The difference between the quadratic and cubic curves is that the cubic curve has two control points, one for each endpoint. The technique for generating the cubic curve is similar to that for generating the quadratic curve. You draw three lines that connect the endpoints and control points (a), and connect their midpoints. That produces two lines (b). You connect their midpoints, and that produces one line (c), whose midpoint determines one of the points on the final curve.^{[2]} See Figure 6-10.
To specify such a cubic curve, use the C or c command. The command is followed by three sets of coordinates that specify the control point for the start point, the control point for the end point, and the end point. As with all the other path commands, an uppercase command implies absolute coordinates; lowercase implies relative coordinates. The curve in the preceding diagram was drawn from (20, 80) to (200, 120) with control points at (50, 20) and (150, 60). The SVG for the path was as follows:
<path d="M20 80 C 50 20, 150 60, 200 120" style="stroke: black; fill: none;"/>
There are many interesting curves you can draw, depending upon the relationship of the control points (see Figure 6-11). To make the graphic cleaner, we show only the lines from each endpoint to its control point.
As with quadratic curves, you can construct a cubic polybézier by specifying several sets of coordinates after a cubic curve command. The last point of the first curve becomes the first point of the next curve, and so on. Here is a <path> that draws a cubic curve from (30, 100) to (100, 100) with control points at (50, 50) and (70, 20); it is immediately followed by a curve that doubles back to (65, 100) with control points at (110, 130) and (45, 150). Here is the SVG for this path, with control point coordinates in bold. The result is shown in the left half of Figure 6-12; the control points and lines are shown in the right half of the diagram.
<path d="M30 100 C 50 50, 70 20, 100 100, 110, 130, 45, 150, 65, 100"/>
If you want to guarantee a smooth join between curves, you can use the S command (or s if you want to use relative coordinates). In a manner analogous to that of the T command for quadratic curves, the new curve will take the previous curve's endpoint as its starting point, and the first control point will be the reflection of the previous ending control point. All you need to supply will be the control point for the next endpoint on the curve, followed by the next endpoint itself.
Here is a cubic Bézier curve drawn from (30, 100) to (100, 100) with control points at (50, 30) and (70, 50). It continues smoothly to (200, 80), using (150, 40) as its ending control point. The upper half shows the curve; the lower half shows the control points. The reflected control point is shown with a dashed line in Figure 6-13.
<path d="M30 100 C 50 30, 70 50, 100 100 S 150 40, 200 80"/>
Path Reference Summary
In Table 6-1, uppercase commands use absolute coordinates and lowercase commands use relative coordinates.
Table 6-1. Path commands
Command | Arguments | Effect |
---|---|---|
M m | x y | Move to given coordinates. |
L l | x y | Draw a line to the given coordinates. You may supply multiple sets of coordinates to draw a polyline. |
H h | x | Draw a horizontal line to the given x-coordinate. |
V v | y | Draw a vertical line to the given x-coordinate. |
A a | rx ry x-axis-rotation large-arc sweep x y | Draw an elliptical arc from the current point to (x, y). The points are on an ellipse with x-radius rx and y-radius ry. The ellipse is rotated x-axis-rotation degrees. If the arc is less than 180 degrees, large-arc is zero; if greater than 180 degrees, large-arc is one. If the arc is to be drawn in the positive direction, sweep is one; otherwise it is zero. |
Q q | x1 y1 x y | Draw a quadratic Bézier curve from the current point to (x, y) using control point (x1, y1). |
T t | x y | Draw a quadratic Bézier curve from the current point to (x, y). The control point will be the reflection of the previous Q command's control point. If there is no previous curve, the current point will be used as the control point. |
C c | x1 y1 x2 y2 x y | Draw a cubic Bézier curve from the current point to (x, y) using control point (x1, y1) as the control point for the beginning of the curve and (x2, y2) as the control point for the endpoint of the curve. |
S s | x2 y2 x y | Draw a cubic Bézier curve from the current point to (x, y), using (x2, y2) as the control point for this new endpoint. The first control point will be the reflection of the previous C command's ending control point. If there is no previous curve, the current point will be used as the first control point. |
Paths and Filling
The information described in Chapter 3 in Section 3.5.1 is also applicable to paths, which can not only have intersecting lines, but can also have "holes" in them. Consider the paths in Example 6-4, both of which draw nested squares. In the first path, both squares are drawn clockwise; in the second path, the outer square is drawn clockwise and the inner square is drawn counterclockwise.
Example 6-4. Using different fill-rule values on paths
<!-- both paths clockwise --> <path d="M 0 0, 60 0, 60 60, 0 60 Z M 15 15, 45 15, 45 45, 15 45Z"/> <!-- outer path clockwise; inner path counterclockwise --> <path d="M 0 0, 60 0, 60 60, 0 60 Z M 15 15, 15 45, 45 45, 45 15Z"/>
Figure 6-14 shows that there is a difference when you use a fill-rule of nonzero, which takes into account the direction of the lines when determining whether a point is inside or outside a path. Using a fill-rule of evenodd produces the same result for both paths; it uses total number of lines crossed and ignores their direction.
The marker element
Let us presume the following path, which uses a line, an elliptical arc, and another line to draw the rounded corner in Figure 6-15:
<path d="M 10 20 100 20 A 20 30 0 0 1 120 50 L 120 110" style="fill: none; stroke: black;"/>
We'd like to mark the direction of the path by putting a circle at the beginning, a solid triangle at the end, and arrowheads at the other vertices, as shown in Figure 6-16. To achieve this effect, we will construct three <marker> elements and tell the <path> to reference them.
Let's start with Example 6-5, which adds the circular marker. A marker is a "self-contained" graphic with its own private set of coordinates, so you have to specify its markerWidth and markerHeight height in the starting <marker> tag. That is followed by the SVG elements required to draw the marker, and ends with a closing </marker>. A <marker> element does not display by itself, but we are putting it in a <defs> element because that's where reusable elements belong.
Since we want the circle to be at the beginning of the path, we add a marker-start to the style in the <path>.^{[3]} The value of this property is a URL reference to the <marker> element we've just created.
Example 6-5. First attempt at circular marker
<defs> <marker id="mCircle" markerWidth="10" markerHeight="10"> <circle cx="5" cy="5" r="4" style="fill: none; stroke: black;"/> </marker> </defs> <path d="M 10 20 100 20 A 20 30 0 0 1 120 50 L 120 110" style="marker-start: url(#mCircle); fill: none; stroke: black;"/>
The result in Figure 6-17 is not quite what we planned...
The reason the circle appears in the wrong place is that, by default, the start marker's (0, 0) point is aligned with the beginning coordinate of the path. Example 6-6 adds refX and refY attributes that tell which coordinates (in the marker's system) are to align with the beginning coordinate. Once these are added, the circular marker appears exactly where it is desired in Figure 6-18.
Example 6-6. Correctly placed circular marker
<marker id="mCircle" markerWidth="10" markerHeight="10" refX="5" refY="5"> <circle cx="5" cy="5" r="4" style="fill: none; stroke: black;"/> </marker>
Given this information, we can now write Example 6-7, which adds the triangular marker and references it as the marker-end for the path. Then we can add the arrowhead marker and reference it as the marker-mid. The marker-mid will be attached to every vertex except the beginning and end of the path. Notice that the refX and refY attributes have been set so the wide end of the arrowhead aligns with the intermediate vertices while the tip of the solid triangle aligns with the ending vertex. Figure 6-19 shows the result, which draws the first marker correctly but not the others.
Example 6-7. Attempt to use three markers
<defs> <marker id="mCircle" markerWidth="10" markerHeight="10" refX="5" refY="5"> <circle cx="5" cy="5" r="4" style="fill: none; stroke: black;"/> </marker> <marker id="mArrow" markerWidth="6" markerHeight="10" refX="0" refY="4"> <path d="M 0 0 4 4 0 8" style="fill: none; stroke: black;"/> </marker> <marker id="mTriangle" markerWidth="5" markerHeight="10" refX="5" refY="5"> <path d="M 0 0 5 5 0 10 Z" style="fill: black;"/> </marker> </defs> <path d="M 10 20 100 20 A 20 30 0 0 1 120 50 L 120 110" style="marker-start: url(#mCircle); marker-mid: url(#mArrow); marker-end: url(#mTriangle); fill: none; stroke: black;"/>
To get the effect you want, you must explicitly set a marker's orient attribute to auto. This makes it automatically rotate to match the direction of the path.^{[4]} (You may also specify a number of degrees, in which case the marker will always be rotated by that amount.) Here in Example 6-8 are the markers, set to produce the effect shown in Figure 6-16. We don't need to orient the circle; it looks the same no matter how it's rotated.
Example 6-8. Correctly oriented markers
<defs> <marker id="mCircle" markerWidth="10" markerHeight="10" refX="5" refY="5"> <circle cx="5" cy="5" r="4" style="fill: none; stroke: black;"/> </marker> <marker id="mArrow" markerWidth="6" markerHeight="10" refX="0" refY="4" orient="auto"> <path d="M 0 0 4 4 0 8" style="fill: none; stroke: black;"/> </marker> <marker id="mTriangle" markerWidth="5" markerHeight="10" refX="5" refY="5" orient="auto"> <path d="M 0 0 5 5 0 10 Z" style="fill: black;"/> </marker> </defs> <path d="M 10 20 100 20 A 20 30 0 0 1 120 50 L 120 110" style="marker-start: url(#mCircle); marker-mid: url(#mArrow); marker-end: url(#mTriangle); fill: none; stroke: black;"/>
Another useful attribute is the markerUnits attribute. If set to strokeWidth, the marker's coordinate system is set so that one unit equals the stroke width. This makes your marker grow in proportion to the stroke width; it's the default behavior and it's usually what you want. If you set the attribute to userSpaceOnUse, the marker's coordinates are presumed to be the same as the coordinate system of the object that references the marker. The marker will remain the same size irrespective of the stroke width.
Marker Miscellanea
If you want the same marker at the beginning, middle, and end of a path, you don't need to specify all of the marker-start, marker-mid, and marker-end properties. Just use the marker property and have it reference the marker you want. Thus, if we wanted all the vertices to have a circular marker, as shown in Figure 6-20, we'd write the SVG in Example 6-9.
Example 6-9. Using a single marker for all vertices
<defs> <marker id="mCircle" markerWidth="10" markerHeight="10" refX="5" refY="5"> <circle cx="5" cy="5" r="4" style="fill: none; stroke: black;"/> </marker> </defs> <path d="M 10 20 100 20 A 20 30 0 0 1 120 50 L 120 110" style="marker: url(#mCircle); fill: none; stroke: black;"/>
It is also possible to set the viewBox and preserveAspectRatio attributes on a <marker> element to gain even more control over its display. These work exactly as described in Chapter 2, in Section 2.3 and in Section 2.4.
You may reference a <marker> in a <polygon>, <polyline>, or <line> element as well as in a <path>.
The following thought may have occurred to you: "If a marker can have a path in it, can that path have a marker attached to it as well?" The answer is yes, it can, but the second marker must fit into the rectangle established by the first marker's markerWidth and markerHeight. Please remember that just because a thing can be done does not mean that it should be done. If you need such an effect, you are probably better off to draw the secondary marker as a part of the primary marker rather than attempting to nest markers.
Notes
- ↑ You can also put multiple single coordinates after an horizontal lineto or vertical lineto, although it's rather pointless to do so. H 25 35 45 is the same as H 45, and v 11 13 15 is the same as v 39.
- ↑ We're dispensing with the tent analogy; it gets too unwieldy. Curves based on yurts and geodesic domes are left as exercises for the reader.
- ↑ Yes, markers are considered to be part of presentation rather than structure. This is one of those gray areas where you could argue either case.
- ↑ To be exact, the rotation is the average of the angle of the direction of the line going into the vertex and the direction of the line going out of the vertex.