This is neither an introductory text book, nor a reference manual. Instead, it is aimed so that any of these people:
or
might be able to pick it up and then do any or all of the following:
Over the past 35 years of my involvement with computing, I have had the occasion to use, as both learner and teacher, a wide variety of books on computing and computing languages. I have gained much from many sources, but at the same time my preferences have, no doubt, congealed somewhat. Perhaps some of my preferences will coincide with those of the reader.
While this book is not intended for the beginning computer user, I would hope it is approachable by any of these sorts of people:
That is, it aims to provide some of the purpose of an introduction to the topic, and some of the purposes of a reference. At the same time, though, it is not a comprehensive guide to SVG. In fact, in the time following completion of the first draft, new topics that really should be included have arisen, new browsers have come onto the scene, the SVG specification itself has started to grow. In the Afterword I offer suggestions for directions I would hope to see this document grow, over time. The SVG Interest Group, I am hoping, will provide help in bringing these efforts forward.
The book attempts to discuss SVG in broader terms, but at the same time to illustrate how one can write JavaScript programs that use and manipulate SVG. It is not as broad in its coverage of stand-alone SVG as some existing books, though I believe it goes deeper into scripting than many.
Several goals helped to guide the development of this book.
In short, I'd like it to be the book that did not seem to exist when I started learning SVG.
SVG or Scalable Vector Graphics is a relatively new World Wide Web Consortium (W3C) standard, used by a host of companies and organizations, for the creation and display of vector graphic material. SVG is an XML language that allows dynamic creation of content using JavaScript within or outside the context of the World Wide Web.
If you ever close your eyes and see pictures that have never been drawn or movies that have not yet been made, then SVG might be for you. Just as typing or drawing or playing a musical instrument, developing hypertexts or carving stone can help you to express a part of what is inside you, so might SVG expand your expressive ability. Think of SVG as an expressive medium. With it you can let your readers' browser build your vector graphics, animate them, and let your readers interact with and change the evolution of those graphics dynamically. Users can draw over them, append to them, or use them to plot user-selected sources of data. And you can do it in an open-standards environment that is rapidly growing in popularity and cross-browser acceptance. It is good for less fanciful endeavors, like business and science, too. It is sort of like HTML, only graphical.
The first public draft of SVG was released by the World Wide Web consortium in February of 19991. During the preceding years, interest in the use of vector graphics had grown. The PostScript page description language developed by Adobe Systems Inc. during the 1980's had given the print-based community a way of describing images in ways which could be rescaled to adapt to the resolution of the display device, usually a printer. It was natural to seek a similar vector-based approach to web-based presentation.
In 1998 an XML-based language, Vector Markup Language (VML) was introduced by Microsoft. It contains many of the same sorts of features, though few programmers adopted VML as a medium of expression and Microsoft seems to have abandoned development of VML.
By the end of 1999, development of SVG had begun in earnest. Within two years, six subsequent working drafts appeared. IBM and Corel each released software that exported SVG. IBM released an SVG viewer and several software initiatives released SVG drawing packages for a variety of operating systems. Since that time support and endorsement has grown. By 2005, A Google search (at www.google.com) for "SVG" returned over 3.7 million links on the WWW. Table 1 compares these results with other technologies. By February 2009, all these numbers had increased considerably (HTML itself rose almost eightfold), but SVG had risen to 11.9 million web documents moving well ahead of Fortran which had risen to 8.6 million.
| Query | Number of documents found by search at www.google.com2 |
|---|---|
| “HTML” | 1,610,000,000 |
| “PHP” | 454,000,000 |
| “Java” (includes island) | 150,000,000 |
| “Linux” | 86,400,000 |
| “Perl” | 51,600,000 |
| “JavaScript” | 49,900,000 |
| “Unix” | 35,200,000 |
| “C++” | 28,900,000 |
| “SQL” | 21,200,000 |
| “MySQL” | 20,300,000 |
| “Pascal” (includes Blaise) | 14,500,000 |
| “Visual Basic” | 8,330,000 |
| “Fortran” | 5,350,000 |
| “SVG” | 3,750,000 |
| “COBOL” | 2,630,000 |
| “Lisp” (includes stuttering) | 2,300,000 |
| SMIL | 1,600,000 |
| “awk” | 912,000 |
| “VML” | 497,000 |
| “ALGOL” | 489,000 |
| “SNOBOL” | 40,900 |
SVG has some advantages over conventional bitmapped graphics, such as JPEG, GIF, and PNG, used in the browser environment, because of several reasons:
SVG is an XML language. This is important for at least three reasons. First, the code tends to adhere to agreed upon standards of how SVG should be written and how client software should respond. Second, like all XML, it is written in text, and can generally be read not only by machines but also by humans. Third, and perhaps most importantly, JavaScript can be used to manipulate both the objects and the Document Object Model, in ways quite similar to how JavaScript is used in conjunction with HTML. If you already know how to use JavaScript and HTML for web-programming, the learning curve will be pretty gentle, particularly in view of the benefits to be gained.
Examples are illustrated briefly, just to give an idea of what SVG looks like. In subsequent chapters, we will explain in detail what is actually going on. If you wish to see actual "live" examples on the web of the following, they can be viewed at this location which is a part of the author's web site where many hundreds of examples (sometimes in varying states of disrepair) can be seen.
The object primitives defined by the W3C's current recommendation 1.13are the line, rect(angle), circle, ellipse, polyline, polygon, text, and the path. Each is described with an XML tag such as the following example:
| SVG code | illustration |
|---|---|
<line x1="0" y1="100" x2="100" y2="0"
|
|
|
simple line |
|
The above draws a black line (typically anti-aliased when drawn in the browser) of thickness 2 from the point (100, 200) to the point (200, 100). If one uses right-click in the browser, a zoom-in option appears. The visitor will notice that, unlike bitmapped graphics, the line does not become grainy as one zooms in.
| SVG code | illustration |
|---|---|
<rect x="0" y="0" width="200" height="150" fill="#FFFF00" stroke="blue" stroke-width="5" />
|
|
|
rectangle |
|
This example draws a rectangle with its upper left corner at (200, 200) its lower right corner at (500, 400) with a blue boundary that is 5 units thick and which is filled with yellow (the familiar RGB hexadecimal is used here).
| SVG code | illustration |
|---|---|
<text x="15" y="45" font-size="40" fill="red">some text</text>
|
|
|
text |
|
The above draws the string "some text" in large red letters and positions the string on the screen.
Other objects are similarly defined and can be appended, one after another into the display window, with the most recently defined element appearing in front of or on top of earlier-defined shapes.
| SVG code | illustration |
|---|---|
<rect x="0" y="0" width="200" height="100"
|
|
|
rect with ellipse |
|
The above code specifies a red oval inscribed in a yellow rectangle.
One of the most flexible of SVG's primitive objects is the path. <path> uses a series of lines, splines (either cubic or quadratic), and elliptical arcs to define arbitrarily complex curves that combine smooth or jagged transitions. The following code
<path d="M 100 100 L 200 200" stroke="black" stroke-width="12"/>
defines a simple line equivalent to the line defined by
<line x1="200" y1="200" x2="100" y2="100" stroke-width="12" stroke="black" />.
It proceeds by placing the pen down at (100, 100) and then drawing a line to (200, 200).
| SVG code | illustration |
|---|---|
<path d="M 100 200 L 200 300, 300 20,0 400 300, 500 200"
|
|
|
path |
|
Similarly, the code shown above draws a zig-zag in the plane resembling a "W", moving from (100, 200) to (200, 300) and eventually to (500, 200).
| SVG code | illustration |
|---|---|
<path d="M 0 0 L 100 100" stroke="black"
|
|
|
path with line |
|
Likewise, the above defines two crossing lines: one thicker than the other. We use, in one case, a line, in the other a path to accomplish much the same thing.
| SVG code | illustration |
|---|---|
<path d="M 20 40 C 100 -30 180 90 20 160 L 120 160" stroke="black" fill="none" stroke-width="5" />
|
|
|
more complex path |
|
A more complex path, above, resembles the numeral 2. The "C" portion of the path describes a cubic spline — the path begins at (20, 40) and heads toward (100, -30), based on the tangent at the start point. The curve then heads down to the right toward (180, 90) but with a final destination of (20, 160). To adjoin multiple splines together into a single complex curve in VML, a predecessor to SVG, required a good deal more effort than in SVG.
There are several different ways of putting SVG content in a web page. Let's get started without a lot of tedium about why this or why not that. Later in the book, we will look into some of the advantages and disadvantages of various approaches, but for now let's just talk about two major approaches: standalone SVG documents, and HTML documents with SVG in them. Both of these approaches require a common set of preparatory steps.
There are many ways of seeing and generating SVG content that do not involve web (HTML) browsers. Just as there are HTML browsers that do not recognize SVG, there are SVG browsers that do not comprehend HTML. But among current web browsers that support both HTML and SVG, we are, as of this writing, talking about one of the following five browsers (hereafter referred to as "the five browsers":
Mention should be made of additional contexts in which SVG can be viewed and or created:
Opera, Firefox, Safari, and Chrome users will enjoy SVG support that is native to the browser, while many of the others, including Internet Explorer require a plug-in. Many web browsers and SVG viewers with some HTML capability are able to interpret differing degrees of SVG, JavaScript and HTML, so it is best to check your local supermarket for availability and freshness.
Once you have downloaded, plugged in, or otherwise installed a likely candidate for SVG viewing, it is good to test your browser to make sure it is able to actually interpret SVG. For this you might either
In writing a book (which even though it is electronic, I hope does not grow stale too quickly), I am reluctant to point the reader to many of the 12 million web pages that either include or discuss SVG, since the average lifespan of a web page (44 days according to the best estimate4I can find) is considerably less than the time it takes a project of this size to appear in print. But I suspect strongly that wiki.svg, wikipedia and I will all still be living by the time your eyes reach this book. That's why I will bank on the above URLs as being worth mentioning.
Once you have web software installed that is able to see SVG, then it is time to write a bit of your own. For this there are a number of editors that allow SVG markup to be written. The developer can use a simple text editor, although numerous good, and sometimes free, graphical editing packages are available as well. Oxygen and XMLSpy are two commercial SVG text editors. Among the open source or shareware alternatives, Batik seems to have accumulated a fair-sized user community. I myself use an HTML editor/viewer that does not understand SVG (in terms of tag completion or highlighting) but is at least able to view it. Many folks recommend the Firebug plug-in, associated with Firefox, and Opera's tabbed browsing allows one to go back and forth from source code to view quite easily. Ultimately all you need is a text editor that can save files in plain ASCII or Unicode. Save your file with a .svg extension in the filename.
As a sample file, try the following simple example5:
Save it as "simplest.svg." Point your web browser at it to make sure you can see part of a black circle. If so, you are ready to start creating SVG content
If you already have a place on the web, then put your file in that place. If you can see your file when you point your web browser at it, then congratulations; others can most likely see it too. If you can't see it, it is most likely a server configuration problem. The web server should send an HTTP header for the svg file type that looks like:
Content-Type: image/svg+xml
If you are your own systems administrator, then it is likely you know where to edit your server's configuration so as to effect such a change. If not, talk to your systems administrator and he or she probably will. If not, please encourage them, ever so respectfully, to have a look at what the SVG Wiki says about the topic at this link from the SVG Wiki.
The default coordinate system in SVG is much the same as in HTML. It works as a two-dimensional x-y plane. The origin (where x=0 and y=0) is the upper left-hand corner. As we move right from there, x increases. As we move downward, y increases. Generally, units are measured in pixels. That is, if our browser window has a rectangle of 343 pixels high by 501 pixels wide then the lower right corner of that window will be the point (501,343).
Now, to be sure, things are not always this simple. Sometimes, we have scaling and zoom effects in place which can be affected by a number of considerations, foremost among which might be the viewBox, a rectangle which resets the scale of the units associated with the viewing rectangle. Also, the dimensions of the HTML window may interact with the SVG object if it is embedded in HTML. These considerations will be discussed in more detail later in the book.
Other than that, we can generally assume that when we refer to a point with coordinates (100,100), it will be a point diagonally downward (100√2 pixels) from the upper left corner of the browser's viewable window.
According to the World Wide Web Consortium's recommendations, the SVG graphics elements are "the element types that can cause graphics to be drawn onto the target canvas. Those are: 'path', 'text', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'image' and 'use'."6
These are the primitives, so to speak and form an appropriate starting point for our discussion. The <polyline> and <polygon> objects don't add anything that the more flexible path cannot do, so those will not be considered in this treatment. It makes sense to discuss <use> along with grouping and transformations (once we have something worth <use>-ing), so I will present the others starting with the simpler objects first.
I offer three recommendations on how one might learn all of this:
Before discussing the basic drawing objects, let's first consider the use of color values in SVG and the order in which drawn objects appear on the page.
Colors may be specified in much the same way that they are in HTML/CSS:
Accordingly, the color "red" may be defined alternately as "red", "#f00", "#ff0000", "rgb(255,0,0)", or as "rgb(100%,0%,0%)".
Objects appear from back to front in the order they are defined, with objects defined later appearing in front of or above (and occluding if they overlap) those defined earlier. More concerning overlaying objects will be found in the next section "operations: grouping, reusing, scaling, translation and rotation."
The <line> object draws a line between two specified points: (x1,y1) and (x2,y2). In order to see the line, it must have a stroke (i.e., a color).
The code <line x1="10" y1="10" x2="100" y2="100"> draws an invisible line in most browsers, while in Internet Explorer, a faint hint of a grey line might be seen (which, curiously, does not expand in size when we zoom in on it).
Hence, a sort of minimal line consists of code such as the following:
<line x1="5" y1="5" stroke="red" x2="90 y2="90>
Another attribute known as "stroke-width" controls the thickness of the line and, by default, is assigned a value of 1.
The stroke and stroke-width attributes as well as the starting and ending points are varied in the following illustration:
| SVG code |
|---|
<line x1="5" y1="10" x2="99" y2="30" stroke-width=".5" stroke="red"/>
|
| illustration |
|
A number of other attributes exist for lines, two of which: the stroke-dasharray and the stroke-linecap are worth mentioning in this treatment.
| SVG code |
|
<line x1="15" y1="15" x2="140" y2="135" stroke-width="25"
<line x1="15" y1="15" x2="140" y2="135" stroke-width="25"
<line x1="15" y1="155" x2="160" y2="60" stroke-width="25" |
| illustration |
|
The stroke-dasharray gives a flexible way of making dashed lines, shape borders, and paths. In the above illustration, we have made two pairs each consisting of two identical lines (except for the stroke and its dasharray) one on top of the other. The top line of each pair has had its stroke-dasharray applied which takes a sequence of numeric values S=(v1,v2,v3,...,vn) and turns the stroke on and off: on for the first value v1 pixels along the length of the line; off for the next v2 pixels and so forth. If the sum of the values vi in S is less than the length of the line, then the values are repeated again as needed. In the case of the first line, the value of stroke-dasharray="8,3,2,18" has an even number of values so the blue and aqua colored bands repeat aqua 8 pixels, clear 3 pixels, aqua 2 pixels and clear 18 pixels, starting over again with 8 more pixels of aqua. Since the underlying but identically shaped line is blue, the blue of the underlying line is what shows. In the case of the second line, the value of stroke-dasharray="8,3,2" has an odd number of values so the repeating sequence goes like this:
(8 orange, 3 clear, 2 orange, 8 clear, 3 orange, 2 clear, ...).
The first of the two pairs of lines has two lines; both use stroke-linecap, having stroke-linecap="round". This makes the end of the line rounded instead of flat, as in the second example which uses the default or flat value of stroke-linecap.
Another useful aspect of lines involves the <marker> tag which can be used to define arrow or other shapes appropriate for attaching to the beginning or ends of lines. The W3C gives a clear example7 for those so interested, though it is a bit verbose for our treatment here. Another example can be seen here
The <line>, <rect>-angle, <circle> and <ellipse> elements can all be seen as special cases of what could instead be done with the <path> object. But these are such familiar geometric objects that it is natural to define them separately.
A rectangle is drawn using the <rect> tag, which, by default, produces a rectangle with sides parallel to the edges of the browser window. We will see how to rotate rectangles later on so that they might be parallel to something other than the ground, without having to lift and tilt our monitors. We may also skew them so that they cease to be rectangles at all, but rather become parallelograms.
A <rect> receives a starting point (x,y) a width and a height attribute. If no fill color or pattern is specified, by default, the rectangle will be filled with black.
<rect x="60" y="95" height="30" width="50" />
Common attributes that are used in conjunction with the rectangle include the fill, which specifies its color (or pattern), its stroke and stroke-width (which determine aspects of its border or edge). Here are some rectangles that exemplify these attributes as well as the use of various color reference schemes and the partial overlay and occlusion of objects.
| SVG code | illustration |
|---|---|
<rect x="62" y="25" height="110" width="16"
|
|
Note in the above example that the first rectangle defined, the tall thin one, appears under all subsequent rectangles. Note also, that the colors "#f88" #ff8888" are equivalent and that "rgb(100%,50%,50%)" while visibly similar, is actually a bit darker since half of "ff" is actually "7f" rather than "88".
The fill of a <rect> can also be a more complex. Gradients, masks, patterns, and various filters are all available to alter the way a rectangle appears in SVG. These are more advanced topics and are dealt with later in this book. For something analogous to the stroke-dasharray seen above for the <line> element, consider the <gradient> as discussed in the next chapter.
A circle is indeed a special case of an ellipse, so if you prefer parsimony in the amount of syntax you have to learn, please feel free to skip right ahead to the ellipse. The <circle> does have a slightly simpler syntax, so if you prefer keeping your keystrokes few, or if the ellipse's eccentricity troubles you in some fundamental way, then <circle> may be worth your while to learn.
The simplest circle requires only a center point (cx,cy) and a radius, r:
<circle cx="80" cy="50" r="40"/>
This produces a circle of radius 40 pixels filled (by default) with black.
Just as with rectangles, we might play with the stroke, the stroke-width and the stroke-dasharray to create various interesting effects. Note that if we wish a circle to appear to have an empty center, we define some stroke color and then set fill="none" to make it hollow. The illustration below shows the effects of adjusting several of these attributes.
| Circles with varying fill, stroke, stroke-width and stroke-dasharray | |
| SVG code | illustration |
|---|---|
|
<circle cx="80" cy="50" r="40"/> <circle cx="80" cy="110" r="40" fill="red"/>
<circle cx="80" cy="170" r="40" <circle cx="80" cy="160" r="20" fill="red" stroke="black" stroke-width="10"/>
<circle cx="140" cy="110" r="60" fill="none" stroke="#579" stroke-width="30" |
|
| A similar example can be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/circles3.svg | |
The ellipse is just like the circle but has two radii instead of one. rx represents half the distance from the leftmost to the rightmost sides, while ry is the distance from top to center of the ellipse. The ellipse is always aligned with its horizontal axis parallel to the bottom of the window, unless one applies a rotation transform (as discussed later in this chapter). The ellipse can be a considerably more evocative shape than a circle, and given that it is a circle when rx=ry, it is more flexible as well.
| Identical clusters of ellipses except for stroke-dasharray | |
| SVG code | illustration |
|---|---|
|
<ellipse cx="80" cy="110" rx="75" ry="105" fill="#538"/> <ellipse cx="80" cy="110" rx="60" ry="40" fill="black" stroke="red" stroke-width="25"/> <ellipse cx="80" cy="110" rx="35" ry="20" fill="#538" stroke="yellow" stroke-width="25"/> <ellipse cx="80" cy="50" rx="40" ry="30" fill="red" stroke="black" stroke-width="25"/> <ellipse cx="80" cy="50" rx="30" ry="20" fill="orange" stroke="red" stroke-width="10"/> <ellipse cx="80" cy="170" rx="40" ry="30" fill="yellow" stroke="orange" stroke-width="25" /> <ellipse cx="80" cy="170" rx="30" ry="20" fill="red" stroke="black" stroke-width="10"/> |
with dasharray without dasharray |
| This example may be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/ellipses2.svg | |
The code of the two illustrations is identical except that the figure on the left has had the attribute-value pair stroke-dasharray="3,6"added to four of its seven ellipses.
If one wanted to learn only one drawing primitive, then the <path> would probably be it. It can be used to replace <rect>, <ellipse>, and <circle>, though it would not be advised unless your mental arithmetic skills are quite good (e.g. simultaneous differential equations). <path> is a very flexible drawing option. It renders the movement of a stylus through two dimensions, with both pen-up and pen-down options, including straight and curved segments joined together at vertices which are either smooth or sharp.
There are many aspects of the <path> that we will not discuss here. Fortunately, the W3C's chapter on paths is thorough and has plenty of illustrations of most of its numerous facets. Here, we cover only absolute rather than relative coordinates, and only the raw path elements rather than their simplified forms (such as "S" as a special case of "C"). We will deal with pen-down, linear, quadratic and cubic forms, and arcs.
Like <rect>, <line> and the other elements, we've seen, <path> has attributes like stroke, stroke-width, stroke-dasharray, and fill. But while the other elements we've looked at have special meanings given to particular coordinates (like "rx" or "x2"), the path has a sequence of such coordinates held in an attribute named "d". This string of coordinates can be of arbitrary length.
6a. Paths: M and L
We begin by specifying where the drawing will begin by inserting as the first element of "d" a notation such as "M x y" for numbers x and y. We might think of "M x y" as meaning "move pen to the coordinate x y." From there, we have options of moving (with pen still down on the canvas) linearly (L), quadratically (Q), cubically (C) or through an elliptic arc (A). For example, d="M 100 100 L 200 200" would succeed in drawing a diagonal line from the point (100,100) to the point (200,200), as shown.
| SVG code | illustration |
|---|---|
<path stroke="black"
|
|
The pen-down and line modes stay in effect until turned off, so we might concatenate yet other pairs of coordinates into the path.
| SVG code | illustration |
|---|---|
<path d="M 100 100
|
|
A couple of things should be noted. First, in the above example, we did not specify a stroke since, by default, the figure is filled with black. Second, if we specify that a path has no fill (using fill="none") then the path will not appear to loop back to the beginning. Third, we might, for sake of legibility, be tempted to add commas, between pairs of coordinates. This is just fine, in the general case, though a few cases have been reported in which certain browsers seem to be troubled by large numbers of commas as coordinate delimiters. Fourth, we may assume that L (or line) is the default way of moving to the next point, and it need not be specifed. That is d="M 100 100 L 200 200 100 150" should be equivalent to d="M 100,100 200,200 100,150" . These observations are illustrated as follows. Note that once we specify fill="none" the figure will be invisible, unless we specify a stroke.
| SVG code | illustration |
|---|---|
|
<path d="M 50,100 150,200 50,150"/> <path d="M 100 50 L 200 150 100 100" fill="none" stroke="black"/> |
|
| This example may be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/path2.svg | |
The path will also be unclosed — that is, the two endpoints will not be connected unless we specify that they should be. If we wish a path to be closed, we modify it with the z flag at the end of the path as follows:
open:
<path d="M 100,50 200,150 100,100" fill="none" stroke="black"/>
closed:
<path d="M 100,50 200,150 100,100 z" fill="none" stroke="black"/>
Since paths are, by default, filled with black, it is natural to wonder what happens when the path crosses itself. By default, the union of the regions traversed by the path is filled, unless we specify otherwise.
| SVG code | illustration |
|---|---|
<path d="M 70,140 L 150,0 200,100 40,100 100,0 170,140"/>
|
|
|
Here we show the default fill technique as well as the "even-odd" fill rule on a shape which intersects itself on more than one occasion. The points are labeled just to make it easier to read what might seem a long list of six coordinate pairs.
Another interesting aspect of <path> is that we might combine multiple path segments into a common path definition. That is, a path may have multiple components by having more than one pen-down operation. Note in the figure below that the two path segments are indeed treated as one since the orange fill is applied to the entire figure than to the two separate triangular components. The interior of the figure is also transparent, as illustrated by the rotated and reduced version of the image appearing partly inside and partly outside the foreground figure.
| SVG code | illustration |
|---|---|
<path fill="orange"
A <path> with fill="green" et cetera... is also included in the drawing. |
|
6b. Paths: Q -- Quadratic Bézier curves.
I became aware of Bézier curves in the mid 1980's when I discovered that Adobe Illustrator had the ability to draw amazing curves quickly. I did not know what sort of crazy-fast mathematics would be able to solve all those equations so quickly. A good treatment of the subject may be found at Wikipedia.8
Here's basically how a quadratic Bézier works in SVG. We define an initial point (say 100,200) with a pen-down. From there, we set a course heading toward the next point. Instead of going to the next point, we just aim that direction. So, for example, while "M 100 200 L 200 400" actually arrives at the point "200,400", "M 100 200 Q 200 400 ... " merely heads that way. Ultimately, in addition to a "heading" we also have a final "destination" and that is the final coordinate pair required of the quadratic Bézier. In the illustration we see that
"M 100,200 L 200,400 300,200"
draws a red path between (and reaching each of) the three points indicated. Simply replacing the "L" with a "Q" to draw
"M 100,200 Q 200,400 300,200"
produces a curve passing through both endpoints, and becoming tangent to the associated lines of the allied line-path at the endpoints to the segments
| illustration |
|
| SVG code |
<path d="M 100 200 Q 200,400 300,200" fill="none" stroke="blue" />
|
While there is an infinite family of curves tangent both to the line "M 100 200 L 200 300" at (100, 200) and to "M 200 400 L 300 200" at (300,200), there is only one quadratic that shares these properties, even if we allow for rotations (in the sense of parametric equations) of the quadratic. That is, the curve is uniquely defined by those three points in the plane. Likewise, any three non-collinear points in the plane determine one quadratic Bézier curve.
Revisiting the earlier example in which the fill-rule was modified to produce an empty space in the middle of the curve, we may draw the same curve with quadratic splines instead of lines to see the effect.
|
|
<path fill-rule="evenodd
<path fill="red" fill-rule="evenodd" |
6c. Paths: C -- Cubic Bézier curves.
We can imagine raising the degree of the polynomial to allow the satisfaction of increasingly more constraints on a curve. With a cubic Bézier, we are able to change the skewness and kurtosis of a curve tangent to the inscribing polygon at the specified endpoint, because instead of a single "control point" affecting the direction of the curve, we now have two control points.
In the above figure, we see the effect of allowing the two control points to move symmetrically along the edges of the triangle in the direction of the vertex at (200,400). All four cubic Béziers are like the quadratic Bézier (in blue) in that they have the same starting and end points and are all tangent to the same lines at those points. Each curve as we move down from the red curve to the sharp red angle has control points which are along the lines, but progressively closer to the vertex.
A sort of limiting case can be seen in the following diagram in which the two control points converge to either the end points of the curve or to the vertex. The lower of the two green curves never gets any lower than what is shown, though the higher green curve will be equivalent to the line when d="M 0,0 C 0,0 400,0 400,0". Effectively then, the kurtosis or peakedness of the curve can be adjusted anywhere between the ranges shown.
While the above examples adjust the two control points symmetrically, we may adjust the skewness or asymmetry of the curve by adjusting the two control points asymmetrically.
Ultimately, the power of cubic Béziers can be seen in this ability to bend flexibly in 2D. Additionally, they may be stitched together piecewise and smoothly so as to make cubic splines that can approximate any 2D curve with what is usually acceptable accuracy.
The following illustrates a collection of curves each tangent to the same pair of lines at the same pair of endpoints:
The following demonstrates how Bézier curves may be stitched together smoothly. For this to happen, it is necessary that the slopes of the lines at either side of a segment's endpoint be the same.
Observe that the two paths "brown" and "blue" share beginning and end points, initial and final control points, as well as midpoints (150,200). They differ only in terms of the control points surrounding the midpoint. The blue path aims toward (100,100) and then changes direction toward (200,300) passing through the midpoint on its way and there tangent to the line as shown. Because the three relevant points (100,100), (150,200) and (200,300) are collinear, the slopes of both segments are the same at the point where they meet, implying that the curve is smooth (continuously differentiable) at that point. The principle is applied repeatedly in the following illustration in which each labeled endpoint of a cubic Bézier is surrounded by two points collinear with it.
|
|
6d. Paths: A — Elliptical arc.
One other aspect of the <path> deserves mention. That is the elliptical arc. It might seem that an arc would be a very simple topic, but when we realize that given any two points in the plane and two elliptical radii, there often are two ellipses that traverse those points with specified radii and those points specify two different arcs for each ellipse. The arc subcommand of the <path> has the following syntax: A rx ry XAR large-arc-flag sweep-flag x y. The arc begins at the current point (determined by the last coordinate specified, e.g. by the M subcommand), and ends at (x,y). The ellipse will have radii of rx and ry, with the x-axis of the ellipse being rotated by XAR degrees. The particular ellipse (of the two possible) is specified by the large-arc-flag (0 or 1) and the particular segment of the ellipse is specified by the sweep-flag. (0 or 1). The following illustration shows two different ellipses passing through (100,100) and (200,150) each with different choices for its sweep-flag. The yellow arc is identical to the red one, and the blue to the green, except for the sweep-flag. Both ellipses have had zero rotation applied.
|
|
This example may be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/arcs.svg |
Ordinarily all of our drawn objects are completely opaque. That is, opacity is, by default, 100%. If we wish to make things partly transparent, it is very easy: we simply add opacity=p for some number 0<p<1 as an (attribute, value) pair into the tag we wish to modify. A simple example is the preceding illustration of arc segments in which each of the four arc segments is given an opacity of 0.5, allowing any underlying objects to shine through:
|
| See illustration in previous figure. |
The <image> tag in SVG is much like the <img> tag in HTML: a way of putting the contents of an image file (PNG, JPEG, or SVG formats) into a rectangle on a page. I am not quite sure why a vector graphics language came to have methods for inserting bitmaps. It makes sense, though, since most vector drawing packages give ready access to bitmaps. It certainly expands our graphics repertoire. Additionally, numerous interesting filters exist within SVG which give us considerable power at manipulating bitmapped as well as vector graphics.
Generally we include a tag much like a <rect>. We specify the upper left corner of the rectangle (x,y) we specify its width and height, and we specify the file or URL from which the material will be loaded.
<image xlink:href="filename" x="100" y="100" width="150" height="200" />
| several uses of the image tag |
|
|
We observe that:
It is also important to note that as of this writing, Firefox does not appear to support .svg file types in the <image> tag and both Chrome and Safari seem to have some oddities associated with aspect ratios in this context. (Similar issues can be observed vis a vis browser support for the <img> tag in HTML.) SMIL animation (discussed later) does not seem to be supported by content in the <image> tag — that is, a .svg file containing SMIL will not currently be animated when imported via <image>. It is also worth noting that if and when the other browsers do offer support for .svg file types, syntax of the following sort may be preferred since it is namespace aware:
Alternatively, we will frequently include an attribute assignment which reads
xmlns:xlink=http://www.w3.org/1999/xlink
in the opening <svg> tag. This allows the XML definition of all such compound attributes beginning with "xlink" as in xlink:href="url(#r)" to be interpreted properly throughout the document.
Putting text on a page is a natural thing to do. Future versions of SVG are likely to offer more possibilities than we have at the moment and browser support for text seems to be poised for improvement. Right now one should be aware that there are some problems associated with the appearance of text across browsers.
Nevertheless a few simpler things may be done reliably, simply and consistently. Here's a sort of simplest case:
| illustration |
|---|
|
| SVG code |
<text x="0" y="100" font-size="80" fill="red">
|
| See this example at http://srufaculty.sru.edu/david.dailey/svg/newstuff/text2.svg |
The dimensions of the text (obtained by using the method getBBox(), discussed in later chapters) varies a bit between browsers as shown below. Interestingly, similar difference remain in effect even when font-family="monospace" is specified (which is unsupported in FF1.5, though apparently will be in the next edition).
| Browser | Left | Top | Bottom | Right |
|---|---|---|---|---|
| IE/ASV | 6.15 | 42.72 | 115.79 | 359.48 |
| FF1.5 | 6 | 42 | 117 | 358 |
| Opera 9 | -0.14 | 28.47 | 118.53 | 337.37 |
Similar results would be observed for HTML since a fundamental premise of the web has been that font support and layout is a choice left to the browser software.
The 3WC specification reveals that SVG fonts should be equivalent to those of CSS-2, but it may be important to specify generic font families (specifically serif, sans-serif, cursive, fantasy or monospace) to increase the probability that your visitors' browsers can see them. Even so, as the following illustrates, current browser support for font-families is lagging behind the specifications.
|
||
| IE/ASV | FF1.5 | Opera |
The specification also provides dozens of other ways of controlling the appearance of text, some of which have been implemented in existing browsers. Below is a sampling of some effects that are possible in at least some browsers already:
As of Spring 2009 all five of the primary browsers now support text effects such as shown below.
| SVG code |
|---|
|
| appearance |
|
The path above is defined inside a <defs> tag which serves to define the path but without rendering it. Various flags exist which adjust the positioning of the text along the path, many of which seem not yet to be supported by browsers. One exception is the startOffset attribute of the <textPath> which provides a distance in pixels from the beginning of the curve, where the text will actually begin. When animated with SMIL (see Chapter 4), this attribute makes the text appear to crawl along the curve with speed determined by the SMIL.
The rate at which browser improvement is bringing new features forward would render quite out-of-date any attempt to state a list of currently supported features, but suffice it to say, there are major browser differences here at the current time.
Thus far we have had the opportunity to see much similarity between SVG and HTML: two markup languages with tags and attributes that modify the way those tags look. Where SVG starts to look less like a markup language and more like a programming environment is in its ability to reuse and modify its own content (within its own system). That is, elements can be contained in other elements in such a way that containers modify the appearance of the elements inside them. Specifically we can group and reuse elements in ways that simplify maintenance of code and which shorten the overall length of our documents. The <use> (reuse) and <g> (or group) tags bear similarity to the variables and objects encountered in programming languages. And while those tags can be exemplified with examples drawing just on the "simple objects" discussed earlier in this chapter, their utility becomes, perhaps more pronounced once we have the abilities to transform objects using the isometric planar primitive operations of translation, rotation (including reflection), and scaling.
The three easiet ways to move things around in SVG are rotation, scaling and translation. All are considered to be special cases of the transform attribute of a tag. Suppose we have an object, like a complex path, which we have drawn (either by typing coordinates, or with a graphical editor) and once we bring it into our SVG document, we discover, that while we like the shape, it needs to be moved around a bit. That's what transform=translate is for. The syntax looks like this:
transform=translate(dx,dy)
where dx and dy represent the change in the current position on the x and y axes. Using transform=translate(0,0) would leave an object at its current position. Here's a simple example in which a complex path is drawn near a simple ellipse, before and after the application of a translation 100 pixels leftward and 100 pixels up:
| transform=translate(dx,dy) | |
| Before translation | After translation |
|---|---|
|
|
|
|
| This example may be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/translate.svg | |
transform="rotate(120,219.5,241)"
The point (219.5, 241) is chosen as the center of rotation, since it represents the midpoint of the bounding rectangle enclosing the un-rotated shape. (Again this point is determined through a JavaScript calculation involving getBBox(), a method that will be discussed later.)
The illustration below shows an ellipse centered at (100,50) (as well as an accompanying text label), before and after a rescale by a factor of 2.5. Note that the ellipse's center (like all the points on the ellipse) has each of its coordinates rescaled by the same factor.
|
|
<ellipse
|
<ellipse transform="scale(2.5)" cx="100" cy="50" rx="40" ry="20" fill="grey" stroke="black" stroke-width="12" stroke-dasharray="3,5,2"/>
|
Note that the scale command resizes not only the object, but also its border or stroke.
If we wish to expand a figure differently in one direction than the other, we simply add a second parameter to the transform as shown in the following in which we rescale by a factor of three horizontally, but only x 1.2 vertically.
|
|
|
|
Note here that the hash marks associated with the dasharray remain no longer perpendicular to the path.
Below is an example in which we scale differentially in the x and y directions, preserving the height by multiplying it by 1.0, but flipping and shrinking horizontally by multiplying by -0.5.
| Differential, negative and fractional scaling. |
|
To see how we might scale something while keeping it centered about the same point, we use multiple transformations: a scale and a translation, as demonstrated in the next section.
We may combine transformations by simply concatenating as follows:
transform="translate(-100,-50),scale(1.5)"
The operations are performed in the order right to left, so in the above case, the scale is applied first, moving all points 1.5 times further from the origin. Then the figure is moved upward to the left.
In the following, we see how rescaling followed by a translation produces the desired effect of expanding an ellipse but keeping it centered about the same location.
The original ellipse is centered at (200,100). When we simply rescale it, by a factor of 1.5, the center moves accordingly to (300,150), namely to 1.5 x (200,100). To move the ellipse back to its original center, we then apply a translation: translate (-100,-50), since (300,150) + (-100,-50) = (200,100). This sort of arithmetic is easily automated, if need be, through the use of JavaScript.
There are two other things about transformations that your author would like you to be aware of.
a. We may skew objects (deform from their rectangle to an arbitrary parallelogram having two sides parallel to the original) in SVG using SkewX and SkewY tranformations9.
b. We may perform combinations of skew, rotate, translate, and scale using something called the CTM or current transformation matrix. It comes in handy should a whole collection of transforms be applied to an object and we wish to figure out, where at last, it has ended up. This topic is discussed a bit more when we talk about scripting in a later chapter.
Once we start taking the things we have built and moving them around on the screen, it is natural to want some of them to move together as a unit. The group tag, or <g>, is a tag that merely serves to put elements together, so that they might share a common set of transformations or other attributes.
Consider the simple figure drawn below with its code as shown:
|
<rect x="100" y="100" width="100" height="20" fill="#888" />
|
If we wanted to make three copies of it all side by side as in the following illustration, then we could perform two editing replacements: first change all the x="100" statements to x="-20" and the cx="150" to cx="30", then, in the next copy, change the x="100" to x="220"and cx="150" to cx="270". The four statements turn into 12 statements, 8 of which have simple editing applied to effect the change.
| Manually editing lots of coordinates. | ||
|
||
|
|
|
If the object being replicated were a complex path, the amount of arithmetic we would have to do might become annoying. Fortunately, the <g> tag saves us some work, since instead we might just duplicate the code twice, placing each copy inside groups: <g>copy1</g>, <g>copy2</g> and then apply a separate transform to each as shown:
| Using <g> groups to replicate code with transforms. | ||
<g transform = translate(-120,0)>[place a copy of the same code here] </g>
|
original code |
<g transform = translate(120,0)>[place another copy of the same code here] </g>
|
|---|---|---|
|
||
We end up with a few more characters, but considerably less cognitive effort and time will be expended.
The group tag may also be used to define other attributes of elements within the group, such as the color used to fill some or all objects. If an object has an attribute defined as
someNamedAttribute="inherit"
then it will take whatever value of that attribute its containing group has been assigned.
In the following illustration, code is reused more effectively than manually editing each of the six rectangles, by letting the rectangles inherit their fill color from their groups.
| Inheriting attributes from the group. | |
|
|
|
|
In the next section we accomplish the same result but with considerably less code, using the <use> tag.
| Reusing code (with modifications) — the <use> tag | |
|
|
|
#1 |
|
|
#2 |
<use xlink:href="G" transform="translate(120,0)" fill="#bbb"> |
Above we have built the three rectangles and the oval, with the fill color of the rectangles left undefined: that is, to be inherited from their group. We then put all four objects inside a group with id="G". That group can then be referred to within a <use> tag, by simply typing:
xlink:href="G"
This (a hypertext link to the object in this document known as "G")10 takes all the code within the object "G" and, as a part of the <use>, builds another instance. In this case we have applied a transform to the new instance to slide it to the left, but we have also defined fill="#bbb" so that all objects having the fill="inherit" property (in this case, just the three rectangles) are colored light grey. In the meantime, we must still assign a color to the rectangles of the first instance, so I've wrapped the group "G" in yet another container and given that container its own fill color (black).
Another example may help illustrate the compactness and utility that <use> can bring to our code.
Step 1: We begin with an ellipse and two copies of it, rotated either 30 or 60 degrees.
| Re-using an ellipse — Step 1 | |
| SVG code | illustration |
|---|---|
<g stroke="black" stroke-width="2" fill="none" >
|
|
Step 2: We then take the three ellipses, put them in a group, with id="g2"; and then reuse that group, with a new rotation of 90 degrees applied to the whole group. This means we will now have a whole flower consisting of six ellipses (each with different rotations: 0,30,60,90,120 and 150 degrees). Since we intend to also reuse this flower, we'll wrap it together in its own group with id="g3" and let the stroke and fill properties go up to the outermost container, since all things inside share those attribute values.
| Re-using an ellipse — Step 2 | |
| SVG code | illustration |
|---|---|
|
|
Step 3. Having grouped the six ellipses together into the object "g3", we will now reuse that object three more times, each with a different color and position. To do this we let the stroke property move up to a new top level that contains the first flower, allowing the inner object "g3" to have its own stroke undefined. Each of the <use> tags which reuse "g3" can then impart its own stroke color.
| re-using an ellipse — Step 3 |
|
|
| An animated example of this can be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/use4.svg |
Chapter 6 discusses the issue of various different HTML containers that can be used to display SVG content in an HTML web page. We might use <iframe>, <embed> <object> or even <img> and each will work to some extent in modern versions of the five browsers: Firefox, Internet Explorer (with the ASV plugin), Opera, Chrome, and Safari. While <object> would be the preferred approach from the perspective of compliance with W3C standards, some problems exist with both it and the <iframe> that make "<embed>" a persistent practical recommendation from some experts11. Others, point out that this is only for consistency with IE and the ASV plugin, and that <object> is preferable.
Later, in Chapter 6, concerning SVG and HTML, ways of using the more "standards-compliant" <object> tag for SVG content in HTML, as well as "in-line" SVG will be discussed. My own experiments (see, for example, here and here) together with certain other factors, lead me to use <embed> as the vehicle of choice, though this issue will be discussed in more detail later.
Given an SVG document, saved with a .svg extension, it may be placed in a web page using an <embed> as follows:
| somefileA.svg | webpage.html | |
|---|---|---|
<svg xmlns="http://www.w3.org/2000/svg">
|
<html><body><b>
|
|
| Appearance in the browser: | ||
| somefileA.svg | webpage.html | |
|
|
|
This is much as it should appear in any of Firefox, Internet Explorer, Safari, Opera or Chrome. It is a sort of simplest case, in that fewer keystrokes in the SVG file will probably not do anything in at least one of the browsers.
Notes:
xmlns:xlink="http://www.w3.org/1999/xlink"A natural question emerges at this point: how might we adjust the <embed> so that the SVG content fits properly? There are several issues associated with this question.
If we as programmers know how big the content in the SVG file is (a sort of smallest rectangle starting at the origin which contains all the drawn objects), then we simply find that amount of real estate in our web page and allocate it to the SVG object through setting attributes on the <embed>. In the example below, we fail to allocate enough space for the <embed> and it appears truncated on the page.
| somefileB.svg | webpage.html |
|---|---|
|
|
| Appearance in the browser: | |
|
|
|
We change the <embed> so it's bigger: <embed src="/somefile1.svg" height="150" width="150"
And now, the graphic fits: |
|
|
|
Increasing the size of the <embed> allows the graphic to fit in the viewing area. But in this case we had to take what we knew of the radius of the circle and add that to its center to calculate an appropriate size for the <embed>. We might prefer a technique which adjusts to the graphic more automatically. The following accomplishes this by establishing a "viewBox": a relativized coordinate system within the SVG. By centering the circle relative to the SVG space through the viewBox (which is, in this case, a 200 x 200 pixel rectangle) and then letting the height and width attributes of the SVG expand to 100% of the available space, then, the SVG will expand or contract as needed to fit the <embed>. More on the viewBox attribute will be discussed in the section on zooming and panning in a later chapter.
| somefileC.svg | webpage.html |
|---|---|
|
|
| Appearance in the browser: | |
|
|
The above solution scales nicely, in the sense that if we define the size of the embed as a percentage of the browser window then the SVG will expand or contract in a more customized way.
| webpage.html | Appearance in the browser |
|---|---|
|
|
If we wish to use JavaScript to interact with the SVG document (though these topics are the subject of much to come in later chapters), then we may wish to know the size of the SVG object (if it is specified in absolute terms). To determine its width and height we might use
document.embeds[0].clientWidth
document.embeds[0].clientHeight.
Since users of Internet Explorer and some older browsers will need the Adobe SVG Viewer plugin, it makes good sense to include the following attribute in one's embed tag:
pluginspage="http://www.adobe.com/svg/viewer/install/"
This allows the user to find out what they need in order to actually see the SVG should the browser not be SVG capable.
If one is interested in compliance even with very old browsers (e.g. Netscape Navigator 4 or older) the SVG Wiki12 suggests wrapping an <object> around an <embed> as follows:
<object data="sample.svgz" type="image/svg+xml" width="400" height="300">
<embed src="/sample.svgz" type="image/svg+xml" width="400" height="300" />
</object>
What does seem to work for both Internet Explorer and other browsers is the following:
<object id="E" type="image/svg+xml" data="ovals.svg" width="320" height="240">
<param name="src" value="ovals.svg">
</object>
Again, while it seems that the browser developers are beginning to converge on workable solutions to these things, the future may see the use of <embed> decline as support for <object> becomes stronger, consistent with published standards. However, with the HTML standard under current revision (<embed> is in the current HTML5 draft) and with alternatives to the Adobe plugin likely to emerge it is difficult to see how this particular microfuture may develop.
Using events within either HTML or SVG to send messages to the other's scripts and DOM is covered in detail here.
As a graphics language, SVG is not limited to just a set of graphic primitives (albeit ones with a rich set of attributes). There are other ways of filling, cropping and distorting objects that greatly enhance our arsenal of tools.
The term "gradient" refers to a gradual change of colors, blending from one into the next, generally with the small local changes in color values being imperceptible. It is fairly easy to define a gradient in SVG. First we build a gradient object, then we use it as the fill (or stroke) of another object or set of objects. The gradient object consists of a series of colors (called stop-colors) and the ways those colors will be faded into one another. There are two primary types of gradient: radial, in which the colors surround some central point in concentric bands, and linear in which the transitions all take place perpendicular to some basic line or direction.
| Gradients applied to a path | |
<path d="M 100 200 200 200 150 100 z"
|
|
| linear | radial |
|---|---|
|
|
|
|
| White is applied from left to right | White is applied from center to outside |
The object to which the gradient will be applied uses a local url (similar to the xlink:href we saw earlier with the <use> object) as the attribute value of the "fill" attribute, hence demonstrating that an object may have a color or a gradient as its fill, but not both.
Note that in the linear gradient above, two "stops" have been built. This means the gradient has two colors applied to it, one for each stop. Those colors are determined by the stop-color attribute. The offset attribute determines where between 0% and 1=100% of the way from left to right, the associated color (in this case black or white) should be applied. That is, white is applied at the leftmost part of the triangle, while black is applied to the rightmost part. Shades of grey gradually darken as we move to the right, with a grayscale value of 128/256 or 50% occurring halfway across the image or along the line where x=150. For the radial gradient, the midpoint of the bounding rectangle around the path is chosen as the center. From there we apply our first stop-color (zero percent of the way out toward the corners of the bounding box). Black will be applied to the four corners of the bounding rectangle, with shades of grey gradually lightening as we move toward the center.
The number of stops in a gradient need not be limited to two. The rectangles below are 200 pixels wide. That means the linear gradient is white at 0 pixels and 150 pixels from the left, and black at 50 and 200 pixels.
| Four stops apiece for linear and radial gradients applied to <rect> |
<stop offset="0" stop-color="white"/>
|
|
Next, we observe that we can change the angle that a linear gradient traverses its fill, or the center point from which the waves of the radial gradient ripple outward.
| The linear gradient in the underlying layer has several stops in black, white and grey. Ordinarily the color-bands would run vertically. We have rotated their angle 30 degrees though with a gradientTransform, rotated about the center (50%,50%) of the <rect>. |
|
The radial gradient in the foreground has had its fx (the x position of its focus) changed to 95% meaning that instead of concentric rings being centered about the middle of the <rect>, they are now offset to its extreme right side. | |
<linearGradient id="l" gradientTransform="rotate(30 .5 .5)">
|
<radialGradient id="r" fx=".95">
|
||
In addition to specifying the color of a <stop> within a gradient, we may also specify its opacity through an attribute known as stop-opacity. We may thus make gradients act like differential masks, gradually allowing an image underneath to fade in to view.
<stop offset=".8" stop-color="black" stop-opacity="0.5"/>
Stop-opacity (like regular opacity of drawn objects) takes on values between 0 (transparent) and 1.0 (opaque).
Here are some examples in which stop-opacity has been used with gradients to allow differing amounts of what is underneath to be visible along a partly transparent gradient.
| Various applications of stop-opacity within gradients | ||
|
|
|
| Superimposition of three copied but rotated linear gradients. | Changing tonalities with a radial gradient over an <image> | Two radial gradients superimposed |
|---|---|---|
|
||
| Two radial gradients with spreadMethod="repeat" (see below) | ||
|
These examples may be seen at |
||
The spreadMethod determines how the gradient will fill a shape if it happens to "run out" before the image is filled. Suppose, as in the example below left, we have a radial gradient which fills an ellipse but the stops of which are so close to the center that its effect is constrained to a small portion of the ellipse. We might choose to replicate that fill pattern replicating the color transitions multiplicative outward as shown in the example on the right. The two examples are the same except that the latter one has an attribute of spreadMethod="repeat" defined. In order for this method to work, the attribute gradientUnits="userSpaceOnUse" must also be assigned.
| spreadMethod is undefined | spreadMethod="reflect" |
|---|---|
|
|
<radialGradient id="gradient1" cx="30%" cy="60%" r="31" fx="26%" fy="34%">
|
<radialGradient id="gradient1" cx="30%" cy="60%" r="31" fx="26%" fy="34%"
|
Like a gradient, a pattern defines a fill method that may be applied to a given shape. In the case of a pattern though, we may specify some graphics that fill a given rectangle within a pattern, and then allow the pattern to replicate across the region being filled. An example should make it fairly clear.
We define three identical ellipses in close proximity to one another:
Note that these ellipses all fit inside the rectangle (0,0) to (22,15) without any of the ellipses extending past the edges. Now, we will build a pattern-space: a rectangle of size 22 by 15, in which the three ovals are placed. (We use the patternUnits attribute to make sure the coordinates of the pattern conform to the absolute viewing window rather than to fractions of the object being filled.)
| The definition and use of a <pattern> | |
| The pattern itself (scale x 6): |
|
|---|---|
<g id="ovals3" fill="#835" stroke-width=".7" stroke="#006">
|
|
| Applied to a region | |
<pattern id="Oval" patternUnits="userSpaceOnUse" width="22" height="15" >
|
|
|
|
| This example may be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/pattern1d.svg | |
In the above example, note that another ellipse (the blue one) has been placed under the pattern just to help see how the pattern, when not completely filled, is actually transparent.
The objects we place inside a pattern can be numerous and complex, as shown in the following example, where the objects are ellipses filled with reflected gradients.
| A pattern space filled with reflected gradients in ovals. |
|
While using the stop-opacity of a gradient can allow us to appear to clip or crop an underlying image down to a smaller region in the shape of either a rectangle (in the case of linear gradients) or ellipse (in the case of radial gradients), this technique gives us no easy way to clip down to an arbitrary polygon13.
Masks and clip-paths are a more realistic approach to cutting a shape out of an underlying picture.
The <mask> and <clipPath> tags provide similar sets of capabilities. We may think of a <clipPath> as a special case of a <mask> which is slightly simpler to use, but not quite so powerful. As such, we will introduce it first.
the <clipPath>
We use a <clipPath> to carve a shape into another graphic element.
That is, a <clipPath> is a container for a set of graphic elements (any combination of 'path', 'text', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'image' and 'use'), which when applied to another graphic element, through its clip-path attribute, results in the restriction of the visible part of that graphic element to the defined clipPath.
In the example below, an <image> tag is defined with a clip-path attribute referring to to a simple <clipPath> containing an ellipse. The rendered portion of the image is limited to those pixels that are within the ellipse. As with gradients and other SVG items containing references to things defined elsewhere in the document, the clipPath is given an id and then the thing to be clipped by it refers to that id within its clip-path attribute.
| A simple <clipPath> applied to an <image> |
|
|
We may insert more than one graphic element inside a clipPath, and the graphic element may itself be complex (in the sense that a fill-method="evenodd" assignment would render the region with more than one contiguous sub-region). If an element is complex in this way, then it must have its clip-method (rather than its fill-method) set to "evenodd." The example below shows a clipPath containing three shapes inside it: two simple ellipses and a complex path with two distinct subregions. A single rectangle has been placed "behind" the image, so that we may observe that the regions cropped away from the rendered image are indeed invisible.
|
<clipPath> containing three graphic elements and applied to an <image> |
|
|
| This example may be see at http://srufaculty.sru.edu/david.dailey/svg/newstuff/clipPath1.svg |
Two <clipPath>s may be intersected. The following demonstrates a picture being clipped first to a star-shaped region "ST". The result, "I", is then reused (being reflected and translated) with a new clip-path applied — one that happens to coincide with a rectangle, "R", that passes beneath it and is reused to form the second clipping path, "C2" . The example is an interesting one since it illustrates some of the complex ways in which SVG objects can be combined with one another.
| Repeated clippings of an <image>: first to a star, then to a rectangular subregion of the star. |
|
|
| A similar example can be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/clipPath4.svg |
Above, we have taken the result "I" of a clipping operation and applied another clip to that. We might accomplish a similar result, following from the above code by applying the "ST" clipPath as a clipping path to another clipPath containing the rectangle "R" as shown in the following code.
| Applying a clipPath to a clipPath |
<clipPath id="C3" clip-path="url(#ST)">
|
It should also be noted that the major SVG browsers show some inconsistent behavior regarding clipPaths at the current point in time. While all browsers seem to agree on the handling of the earlier example involving two <image> tags above, the addition of additional complexity in the URL cited as viewed in different browsers is markedly different, with some, but not all of the differences being attributable to the presence of SMIL. Neither Firefox, Safari, nor Chrome seems to appreciate the application of a clip-path directly to a clipPath, though Opera and IE/ASV behave as one might expect on the basis of intuition alone.
Because of the expressive power of SVG, there are often multiple ways to accomplish the same end. As demonstrated below, we might clip an image to a shape using the clipPath, as we have investigated in this section, but we might also use the <mask>, a composite filter (covered in the next chapter), or simply overlay a rectangle with a hole in it (the least elegant of the approaches). All but the last approach actually remove unwanted parts of the picture as is illustrated by the rectangle which appears behind the first three images, but is interrupted by the overlaid region in the fourth.
| Clipping to a shape using clipPath, mask, composite, and overlay. | |
|
|
|
|
|
|
|
|
The <mask>
As can be seen from the above illustration, the mask and the clipPath have much in common. The fundamental difference is that while the clipPath provides an all-or-none clipping function, the mask can provide partial occlusion of the underlying object based on color values provided within the mask.
In a sort of simplest case (see the figure "Clipping to a shape using clipPath, mask, composite, and overlay" above), a mask, just like a clipPath, provides a region that divides the object to be clipped into two parts: the visible (black or opaque) part and the invisible (white or transparent) part. A mask also allows, however, for this division to provide an alpha channel to an object based on color or transparency values of the mask. That is, suppose instead of merely clipping a bitmap or other graphic we wish to make parts of it invisible or partly visible, while we might see a gradual transition from invisible to visible in other parts.
To illustrate the difference between a clipPath and a Mask, consider the following example.
| A gradient <mask> applied to a bit of <text> |
|
|
| This example may be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/mask6.svg |
A simple linear gradient is defined ranging from black to white as we move from left to right. The gradient is applied to a rectangle starting at (x,y)=(300,300) and ending at (x+w,y+h)=(700,400). The rectangle is not actually visible since it is part of a mask that has id="Ma". The mask is then applied to a text object, the bounds of which extend well beyond the rectangle of the mask, in both horizontal directions. Since black, at the left side of the mask, is equivalent (at least in the case of RGB images) to "opaque," the text is hidden at that side. As we move toward the right (which in this case coincides with white and hence "transparent") the text becomes more visible (since its mask is more transparent).
In the next example, we look a bit closer at the mask, this time using four different transparency levels (also known as alpha values) within the mask, applied through four separate rectangles.
| Discrete levels of masking opacity |
|
|
| This example can be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/mask4.svg |
In this case we have, within the mask, a group of four rectangles with transparency ranging from 25% to 100%, arranged from left to right. The words "unmasked values" appear without any mask applied, while the words "Masked values" have had the mask applied. The bottom two thirds of the image, in fact, all have had the same mask applied (as members of a group to which the mask is actually applied), so that we may see exactly where the 25% mask (and the other values) actually kick in.
The above example serves to demonstrate that masks may have discretely defined regions with discrete transparency levels. The next example is a closer look at the continuous case of gradient, or continuous, levels of change, this time, applied radially, rather than linearly.
| Application of a radial gradient <mask> to an <image> |
|
|
|
An animated version of this example can be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/mask2.svg |
The three dashed lines are all significant here: the outermost coincides with the outer edge of the ellipse. Note that moving from this edge outward, the underlying rectangular image ceases to be visible since it is effectively clipped by the mask. At the very centermost point, (50%,39%), the image again becomes invisible, since at offset=0, the center of the ellipse, the value is black which corresponds to opacity in the mask. The second ellipse, corresponding roughly to the contours of the face, represents an ellipse with rx="25%" and ry="19.5%". That is it is an ellipse that coincides with the middle stop of the gradient. It is at that contour level, where the opacity of the mask is zero, meaning that it is there where the image is most visible. The reader may benefit, also, from observing how the grid lines (to which the mask has also been applied), disappear and then reappear as we move outward from the center. The mask is hiding them in exactly the same way as it hides the face.
Through the application of richer masks, the effects can become more striking. The following represents two applications to underlying bitmapped images of masks that contain reflected gradients. In the first case the focal point of the gradient has been set to be determined by mouse movement, in the second, a series of underlying colored stripes interact visually with the image at the points of its transparency, as determined by the mask.
| Reflected radial gradients as masks applied to <image> tags | |
|
|
SVG has a wealthy set of options for manipulating pictures (either drawn or bitmapped). These are options attached to the <filter> object: a bag of tricks both diverse and, in some cases, complex. Unfortunately, the filters are apparently difficult to implement by the browser developers, and so as of this writing all of the features seem to have been implemented in Opera, some of the features are not implemented in Internet Explorer, several more are not yet implemented in Firefox. None, I think have yet been implemented in Safari or Chrome. The development of SVG has split into two tracks — those trying to implement an acceptable subset of SVG (called "SVG Tiny") on small-display mobile devices, and those working toward compliance with the superset, sometimes called "SVG Full". Fortunately SVG Full and SVG Tiny are consistent with one another, so it is just a matter of making sure that the features you desire reach the audience you seek to reach, which has been the same fundamental problem with cross-platform computing since it got started in the 1950's.
Another note that one of my reviewers recommends, is that I warn you, the reader, that you may want to skip ahead to other sections. As he puts it: "people will likely get bogged down" in this chapter. The good news is that you don't need to know it for what comes later!
SVG's filtering options are called filter primitives.14As primitives they are probably not semantically complete in the sense of allowing us to form all possible expressions (whatever that might mean in the language of imagery). They also lack the irreducibility that one often associates with semantic primitives: many equivalent results can be expressed in several different ways15.
Filters can be computationally quite time consuming. The larger the region they are applied to, the slower they may take to render. This is particularly relevant when one considers animating any of the attributes of these filter effects.
This treatment of SVG's filters will not be exhaustive. Let us examine a few of the filter primitives to give a basic sense of how they work and what they do.
The basic <filter>
A <filter> is applied to another object much as a clipPath or gradient — namely through a filter="url(#filtername)" attribute defined within the object to which the filter will be applied. The <filter> tag itself must have one or more filter primitives inside it; those primitive operations will be conducted in the order they are defined, from top to bottom.
Example syntax:
<filter id="F">
<anyParticularPrimitive1>
<anyParticularPrimitive2>
...
<anyParticularPrimitiveN>
</filter>
<anyParticularSVGObjectOrGroup filter="url(#F)"/>
Simpler filter primitives
Some filters are composite filters in the sense that they require the prior definition of other filters. Others are a bit simpler, in that they may be applied directly to graphic objects without advance buildup. We'll begin the study of filters with the simpler ones: feGaussianBlur, feColorMatrix, and feSpecularLighting. Later we'll cover the more complex ones. Now, let's get right on to some real examples.
This filter blurs an image. The parameter associated with this filter is the standard deviation (stdDeviation) which controls the distance from which neighboring pixels will be allowed to influence a pixel and hence, the amount of blurring.
First, a filter is set up with an <feGaussianBlur> inside:
<filter id="A">
<feGaussianBlur stdDeviation="1" />
</filter>
Then the <filter> is applied to an image to be blurred.
<rect x="42%" y="10%" width="16%" height="25%"
fill="white" filter="url(#A)"/>
The following shows the effect of increasing the value of stdDeviation on two different images on a black background.
| Effect of s=stdDeviation on feGaussianBlur | ||
<filter id="A"><feGaussianBlur stdDeviation=S/></filter>
|
||
<rect x="42%" y="10%" width="16%" height="25%" filter="url(#A)" fill="white"/>
|
||
|
|
|
| S=2 | S=10 | S=25 |
<image x="42%" y="10%" width="16%" height="25%" filter="url(#A)" xlink:href="p0.jpg"/>
|
||
|
|
|
| S=2 | S=10 | S=25 |
Observe that the blurred object expands beyond its original bounds and that values outside its boundary are considered to be transparent so that any background present (in this case, monochromatic black) will be visible inside the edges of the image itself. To restrict the image so it does not bleed beyond its boundaries, one can either set the x, y, height and width attributes of the filter itself (the easiest way), or use another filter primitive, the feOffset, discussed later in this chapter.
| Restricting the extent of a filter to the size of the source image. | |
|
|
| Restricted to size of source image | Unrestricted to size of source image |
|---|---|
|
|
It is also worth noting that if <feGaussianBlur> takes two parameters, rather than one, for its stdDeviationattribute, then the first will represent horizontal blurring, while the second represents vertical blurring. The statement
<feGaussianBlur id="fGB" stdDeviation="25, 0" />
will blur the object only horizontally, in ways that, for a monochromatic rectangle might resemble a linear gradient with three equidistant stops.
The feColorMatrix primitive allows the redefinition of colors within an image, based on the ability to multiply each pixel's RGB and alpha levels by numeric coefficients. In the more complex situation, users may specify an entire matrix of twenty coefficients (4 by 5) to be multiplied by the one-by-four vector representing the color value of a given pixel. In simpler situations, predefined matrices have been associated with special flags (such as "saturate", "hueRotate", or "luminanceToAlpha") meaning one may simply specify one of the flags to perform the indicated operation.
| feColorMatrix type="saturate" | |
|
|
| Original Image | Filtered image |
|---|---|
<image x="25%" y="0" width="25%" height="35%" xlink:href="p2.jpg"/>
|
|
| An animated example of this may be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/filterColorMatrixSaturate.svg | |
Below are the results of several experiments with type="matrix" in which we may specify our own matrix to be multiplied by the pixel values of the image. We begin by observing that multiplying the identity matrix (in which values [i,i]=1):
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 1 0
by an image would result in no change whatever to it.
The rows of the matrix represent respectively R,G,B, and alpha, so for example, in the second image we see that the alpha channel is being positively influenced by red, blue and green, while each of those colors negatively influences itself. The result is much like a black and white negative with transparency being maximized where the original image is brightest. The range of effects presented should allow the reader, with some experimentation of her own, to get a feel for how these matrix transformations work.
| feColorMatrix type="saturate" | |
|
Original image |
|
|
|
|
|
|
|
|
|
| This example can be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/filterColorMatrixMat.svg | |
The values of the matrix above do not need to be typeset as they are. They could be specified simply as a space delimited string. The above format helps with legibility for both author and reader.
feConvolveMatrix
This filter allows what in image processing is known as a convolution filter. It allows us to define a square matrix (typically n by n for some odd number n) in which the center cell of the matrix refers to the pixel itself, and the cells above, left, below and right of it within the matrix, refer to the pixels above, left, below, and to the right of that pixel in the source image. The numeric coefficients in the matrix define the weight that each neighboring pixel will have in the calculation of the new color value of that pixel. In the simplest case, the matrix
0 0 0
0 1 0
0 0 0
leaves any image unaffected, since the new value of a pixel will be equal to 1 times its current value plus the sum of zero times the values of its eight nearest neighbors (those immediately N, NE, E, SE, S, SW, W, and NW of it).
A convolution matrix is defined as the value of the attributes kernelMatrix within an <feConvolveMatrix> as follows:
<filter id="edge">
<feConvolveMatrix order="3"
kernelMatrix="
-1 -1 -1
-1 7 -1
-1 -1 -1
" />
</filter>
<image id="M4" x="465" xlink:href="p17.jpg"
width="150" height="175"
filter="url(#edge)" />
We might expect the above to exaggerate those pixels that are very different from their neighbors, since each pixels neighboring pixels are weighted negatively.
The convolution matrix specified by
kernelMatrix=" -1 -1 -1 -1 -1 -1 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 2 3 2 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 -1 -1 -1 -1 -1 -1 "
will have the effect of striping an image horizontally (akin to applying a horizontal blur). That is because a pixel is averaged with all the pixels, within radius two, that are at the same height. The pixel itself has only a bit more weight than its horizontal neighbors. Likewise the fact that we have chosen to degrade pixels based on similarity to those some vertical distance away, means that we will tend to sharpen our horizontal edges, a bit, since those are where differences between regions are most pronounced and where pixel values will tend to be exaggerated relative to neighbors. To see the effect of these striping convolutions, let us apply both a predominantly horizontal and a predominantly vertical striping effect to the small grained fill pattern, url(#Oval), developed in the last section.
| Matrices for vertical and horizontal striping | |
<ellipse fill="url(#Oval)" cx="50%" cy="50%" rx="10%" ry="10%"/>
|
The original image |
|
Vertical striping |
|
Horizontal striping |
Lastly, are a series of other matrices performing a variety of image manipulations, to show not only some of the sorts of manipulations possible, but also to give some insights into how these convolutions work.
| Different kernel matrices for the feConvolveMatrix primitive | ||||
|
||||
<image id="M4" x="155" xlink:href="p17.jpg" width="150" height="175"/>
|
|
|
|
|
The National Institute of Health has for many years provided a freeware package known as NIH Image which does a variety of interesting image analytic operations including convolutions. The National Institute of Standards provides some informative reading on convolution filters and image processing in general.17
Additionally, a goodly collection of actual convolution filters that the reader may find useful can be found at OpenGL.org.
The <feComponentTransfer> primitive allows the independent redefinition of each of the four color channels: R,G, B, and A (alpha). It allows the adjustment of brightness and contrast through application of any of a variety of different functions to any or all channels of an image. The types of adjustment allowed include identity, table, discrete, linear, and gamma. discrete can be used to posterize an image (that is to reduce it to fewer color values). linear is used for simple brightening and darkening (or contrast adjustment) while table can be used to remap the function (like discrete) only continuously.
| Some uses of <feComponentTransfer> | |
|
The unfiltered image.
<image x="5%" y="2%" height="25%" width="20%" xlink:href="p84.jpg"/>
|
|
|
|
|
Here, with type='table' we invert the chromatic range. We take the normal range from 0 to 1 and map to a new distribution: 0→1 and 1→0, and all in between, for each of the three channels of the image. |
|
|
|
|
This option maps the red values in the interval [0,1] to one of the three values as follows: (0 to .25) →0; (.25 to .50) →.5; (.50 to .75 and.75 to 1.0) →1. The green channel is mapped to either 0%, 50% or 100% green with the threshold between these levels being chosen halfway between the endpoints. The blue channel (relatively insignificant in this particular image) is dampened to black (removing its effect altogether). The source image has no alpha channel (i.e., it is everywhere opaque), hence there is no need to modify that channel. |
|
An example of type="linear" is displayed in the section on <feTurbulence> a bit later in this chapter.
Among these filter primitives that take a simple input from a drawn object and produce a visible result effect is <feMorphology>. It is a rather simple effect, having just two parameters controlling the type and magnitude of the effect. The W3C has this to say about <feMorphology>: "This filter primitive performs 'fattening' or 'thinning' of artwork. It is particularly useful for fattening or thinning an alpha channel."18
A few interesting filters exist which do not necessarily receive input, per se, but rather can create imagery by themselves. They are most commonly used in conjunction with other filters but can be most handy when it comes to building imagery or in processing of other images. The most important of these are feFlood, feTile, feTurbulence, feDiffuseLighting and feSpecularLighting.
<feFlood> gives a new way of drawing a rectangle on the screen. The difference between it and other rectangles is that it can easily be combined on-the-fly with a variety of other filters as will be demonstrated shortly. In the following example, an <feFlood> primitive is applied to each of three objects: two rectangles and an ellipse. The geometry here is worth describing in some detail so that we might be able to make some sense of the relative versus absolute coordinates so often used within SVG.
| <feFlood> applied to three shapes |
|
|
Observe, in the figure above, that the <feFlood> consists of a small grey rectangle. The "x" attribute is set as 50% while the other attributes are all in absolute coordinates. When the filter is applied to each of three shapes, the rectangle begins halfway from the left edge of each. Note that the filter's rectangle is not clipped to the shape of the ellipse it is applied to. In fact, like other stand-alone operators, <feFlood> is applied to a rectangle that coincides with the filter space — either inherited from the object (as in this case), or as applied through the attributes, x, y, width, and height in the filter tag itself.
The feFlood becomes considerably more interesting when combined with feTile — a process by which patterns (much like the <pattern> object but without actually being rendered) may be created and stored away for subsequent use by other filters.
The feFlood filter primitive was one of the later ones implemented in browsers. Currently though (spring 2009) it is available in Opera, FF and IE/ASV.
Just as feFlood allows the introduction of a colored rectangle into a filter, feImage allows the introduction of a rectangular bitmap into a filter. If for example, we wished to let each of several rectangles overlay the same bitmapped graphic, then we might filter each of those rectangles with a filter that contains the feImage primitive. <feImage> involves no parameters; it simply inserts an external image file into a filter processing stream.
Because it is difficult to use the <feImage> construct without the use of multiple inputs to a filter, an example will be given under our discussion of <feMerge> later in this chapter.
<feTurbulence> is used to create textures. It creates patterns of smooth visual noise that fill a rectangle with rather pleasant swirls of pastel coloration. From the W3C's SVG 1.1 specification, we find that it
"creates an image using the Perlin turbulence function. It allows the synthesis of artificial textures like clouds or marble." 19
Like <feFlood>, <feTurbulence> fills a rectangle with new content. It has one required parameter baseFrequency and a variety of optional parameters as well. In the simplest case the primitive is used as follows:
<filter id="T1">
<feTurbulence baseFrequency=".04"/>
</filter>
<rect x="30" y="10" height="100" width="100" filter="url(#T1)"/>
A more fully populated example of the syntax of the primitive may be seen here:
<filter id="T10">
<feTurbulence baseFrequency=".01" type="fractalNoise"
numOctaves="3" seed="23" stitchTiles="stitch" />
</filter>
By varying the values of the parameters baseFrequency, numOctaves (which by default is 1.0), type (which by default is "turbulence"), stitchTiles ("noStitch" by default) and seed("0" by default), we can produce numerous interesting types of pattern as shown in the following diagram.
| Various stand-alone uses of <feTurbulence> | ||||
|
Effects of seed and numOctaves |
|||
|---|---|---|---|---|
|
Effects of numOctaves and baseFrequency |
||||
|
Effects of type and baseFrequency |
||||
|
A. <feTurbulence baseFrequency = ".04"/> |
B. <feTurbulence baseFrequency = ".04" numOctaves="2"/> |
C. <feTurbulence baseFrequency = ".04" numOctaves="2" seed="201"/> |
D. <feTurbulence baseFrequency = ".04" numOctaves="5" seed="201"/> |
|
|
E. <feTurbulence baseFrequency = ".04"/> |
F. <feTurbulence baseFrequency = ".01"/> |
G. <feTurbulence baseFrequency = ".1" numOctaves="1" |
H. <feTurbulence baseFrequency = ".1" numOctaves="3" /> |
|
|
I. <feTurbulence baseFrequency = ".04" type = "fractalNoise"/> |
J. <feTurbulence baseFrequency = ".01" type = "fractalNoise" numOctaves = "3"/> |
K. <feTurbulence baseFrequency = ".04,.1" /> |
L. <feTurbulence baseFrequency = ".1,.01" /> |
|
In the above examples, note that as we move across the first row from A through D, we vary the numOctaves and also the seed. As numOctaves grows from the default value of 1.0 to 2, and finally to 5, the grain of the pattern becomes tighter and its fractal complexity appears to increase.
The purpose of seedis to provide a different start position for the random number generator underlying the function. Note that as we move from cell A to either cell B or cell E, the transition is gradual across the cell boundary. That is, the function is continuous across these areas of the table seeded with the same random number. As we move across the boundary from B to C, where the seed changes, the function no longer appears to be continuous, though continuity is preserved (even despite the octave change) across the boundary between C and D.
The second row investigates changes in baseFrequency, which is sort of like a scaling variable affecting the size of the associated patterns. If we were to change baseFrequency through a SMIL animation (discussed later) we would see the overall pattern remain intact as it expands and moves away from the origin. Cell F has the largest grained pattern of these shown, with a baseFrequency value less than the others. baseFrequency controls, primarily, the size of the grain of the distortion map. Notice that cells G and H which share seedand baseFrequency values, but differ in numOctaves still appear to be continuous across the G/H boundary.
In cells K and L, we observe that we may specify different values of baseFrequency for the horizontal and vertical directions,
baseFrequency= ".1,.01"
imparting a directional grain to the pattern. This can come in quite handy in uses of feTurbulence in conjunction with other effects, in creating striation as part of our textures.
There are two values of type in the SVG 1.1 specification: type="turbulence" (the default) and type="fractalNoise". In cells I and J we look at the effect of type="fractalNoise". The other ten cells all use the default value.
<feTurbulence> used in conjunction with other filters can yield a broad range of quite interesting effects. We will discuss the chaining together and composition of multiple filters shortly, but here are the combined effects of turbulence with saturation (using feColorMatrix) and sharpening (using feConvolveMatrix).
| Effects of sharpening with or without color adjustment | |||
|
Not sharpened |
||
|
Sharpened with
|
|||
| Not color-adjusted | Super-saturated | Unsaturated | |
|---|---|---|---|
Enhancing the contrast of an <feTurbulence> plot:
|
|
|
Here we start with turbulence and then use a linear function to exaggerate the slope of the color values for all three channels, other than alpha, which we dampen out by letting opacity become 1.0 everywhere. |
|
Herewith two more samplings of various combined effects that involve feTurbulence.
| Various effects involving feTurbulence | |
| Text and oval distorted using feDisplacement applied to feTurbulence. |
|
| Several copies of an image distorted using feDisplacement applied to feTurbulence. |
|
| Application of feTurbulence to an image, through a mask. |
|
| Various textural effects | ||
|
||
|
A — baseFrequency = ".23" with radialGradient overlay |
B — baseFrequency = ".007,.25" with feFlood (to add light brown) and feColorMatrix (to add red tones |
C — Base rectangle with black stroke is heavily perturbed. Decorative overlays of other rectangles are also used. |
|
D — baseFrequency = ".2" streaked with <feConvolveMatrix> and with radialGradient overlay |
E. — Same as B, but with slightly different colors and secondary feTurbulence for finer-grained distortion |
F — baseFrequency=".2,.4" with feFlood and feColorMatrix and secondary turbulence |
Numerous techniques exist within SVG for designing and controlling the placement of light sources. Many of these effects can be simulated through the overlay of partly transparent gradients, but the effects are powerful and quite useful for those who already know something of the landscape of lighting effects. Usually one will want to combine these effects using the various methods for combining multiple filter effects discussed later, but here is one example of the use of feSpecularLighting to create an image.
| Placing an <fePointLight> on a black background |
|
|
Two filter primitives which neither operate on raw objects, nor produce stand-alone imagery are discussed a bit separately since they can sometime provide useful results.
<feFlood> is to <rect> as <feImage> is to <image>. Likewise, <feTile> is directly akin to <pattern>. It allows us to bring repeating patterns of imagery into the filter apparatus, so that we might then use it to create effects. Herewith is a simple use of the <feTile> primitive:
| Building tiled layers within a filter using <feTile> |
|
|
As can be seen from the above, <feTile> merely takes the imagery that exists within the filter and fills the filter space with it.
This is used to move a chunk of imagery, typically the SourceGraphic or the BackgroundImage, around a bit within a filter for purposes of slight realignment. The following drop shadow result is accomplished with <feOffset>. We proceed by taking in an image and applying a blur filter. That blurred image is then offset (20 pixels to the right and 15 pixels down) and stored as result "B". Result B is then merged under the original SourceGraphic to create the effect.
| <feOffset> to create a blurred drop shadow. |
|
|
Combining Filter Primitives
There are a variety of ways of combining filter primitives. One way is to apply one filter to an object, then nest the object within a <g> which has its own filter applied.
<filter id="F1">
<someFilterPrimitive1/>
</filter>
<filter id="F2">
<someFilterPrimitive2/>
</filter>
<g filter="url(#F2)">
<rect height="20%" width="15%" filter="url(#F1)"/>
</g>
An equivalent result can be obtained by chaining filter primitives together within a single <filter> element as follows:
<filter id="Fs">
<someFilterPrimitive1/>
<someFilterPrimitive2/>
</filter>
<rect height="20%" width="15%" filter="url(#Fs)"/>
In the above, we assume that filterPrimitive1 was the essence of #F1, while filterPrimitive2 was the essence of #F2. The latter approach is likely to be more efficient time-wise because it withholds any rendering while processing is still taking place. The run-time behavior of these filters can be a serious consideration, since some of the filters we have discussed in this section take on the order of seconds, rather than milliseconds to perform, at least on contemporary machines.
In addition to being able to sequentially chain together the results of different filter primitives, where each successive filter takes the output of the preceding filter (known as its "result") as its input (known as its "in") it is also possible to combine filters in more complex orders.
First, consider the default way in which filters handle multiple effects. Ordinarily, the first primitive within a <filter> receives, as input, the "SourceGraphic" — the element to which the filter has been applied. For example, if we define
<rect filter="url(#Fs)" ... />
then it is that rectangle that is considered to be the SourceGraphic of the filter "Fs." Each primitive in succession (FP1, FP2, ...FPk), takes the output or "result" from the previous filter as if it were its input. We show two equivalent approaches the first which just uses default values of the in and result of successive filters, while the second makes all those default values explicit. There would be no reason to specify the values of inor resultin the following example, but the example may help make it clear what is meant by the inand the result of a filter. In both cases, it is the final filter, from which the output is rendered into the affected graphical objects.
| Two equivalent approaches to sequential multi-filter processing | |
|
|
| In the above, FPx refers to any filter primitive (such as feGaussianBlur, etc.) | |
|---|---|
Once we know where the SourceGraphic enters into the computations and how results are named and reused, then we are in a position to start varying the order and using those more complex filter primitives that combine results of two or more primitives, hence chaining filter primitives together in more complex and interesting ways.
SVG also gives access to the graphical content underneath a given image. That is, the state of the rendered imagery in the layer below the filtered object may itself be used as a part of the filter. This allows combinations of an image with its background using techniques for combining two images: feMerge, feBlend, feComposite, and feDisplacementMap. The use of BackgroundImage to do this will be revisited shortly.
The following are filters which operate on two or more images, or which utilize as input, the output of other filters. We'll start with the simpler ones and move on from there.
feMerge
The feMerge filter allows the combination of filters concurrently, rather than serially (as in the earlier examples). Rather than each filter being applied to the output of the preceding filter, feMerge gives us a way to temporarily store the output of each filter. Once several layers have been created and stored as the results of different primitives, then they may be placed on the canvas in order from bottom to top. Topmost layers should have some transparency (or incompleteness) in the fill area, so as to allow those layers underneath to be visible.20
In the following example, we are interested in converting an image from standard RGB to partial transparency, in this case using the darkest parts of the image, so that an underlying color shines through. In this case, 'yellow' created as a part of the filter, is used.
| Using <feMerge> to combine <feFlood> with <feColorMatrix> applied to <image> |
|
|
What we have done above, is to create a yellow rectangle with the <feFlood>. We then store it temporarily in the variable "A". Next we use the <feColorMatrix> to operate on the SourceGraphic (the JPEG image). In the top three rows of the color matrix, we preserve the RG and B channels but in the last, or alpha, row, we let positive values in any of the three channels contribute positively to the alpha channel, hence creating transparency in the darker parts of the image. This interim result is labeled "B." We then rebuild the image by laying down the interim results: first A, then atop that B. Since B is now partially transparent we may see the yellow, underneath.
<feMerge> allows for any number of <feMergeNode>s to be inserted into the filter, so that we may build rather complex objects with it.
The reader may realize that there are at least two other ways of accomplishing the above effect. One is to simply build a yellow <rect>, under the <image>.
|
Build <rect> underneath <image> |
|
The advantage of the <feMerge> approach shown earlier, is that it is portable. We may apply this filter to any image we wish to without having to build a <rect> underneath it. If we wish to add or withdraw such an effect dynamically, it will be much cleaner since the effect (including its color) is entirely self-contained.
Another approach is to build a <rect> on top of the image and then using the BackgroundImage (as discussed momentarily) to "swap" the order of the two images within the filter.
Here's another example of the use of <feMerge>, this time in conjunction with <feImage>.
| Bringing a bitmap into a filter with <feImage> |
|
|
In the above, a particular image is brought in through an <feImage> and laid down at the beginning of the <feMerge>.While effects such as seen above could probably be accomplished through other means (such as <mask> with transparency), the ability to bring an image directly into the processing stream is certainly convenient.
By increasing the transparency of all or part of an SVG graphic, we allow whatever is underneath it (its background) to become at least partly visible. However the opacity of the top layer does not really allow what is underneath it to interact with it in any substantial way. Though background content may be visible, it is not available for modification or use within the current filter when it is viewed only through the transparency of what is atop it. Just as we may refer to the SourceGraphic as the object to which the filter has been applied, we may also refer to the BackgroundImage as whatever happens to be behind it.
In order to allow the content of BackgroundImage to be made available to a filter, a container (a <g> or a <use>) that contains both the SourceGraphic and any underlying elements desired to be filtered with it must be instructed to make that background content available to the filter through setting its enable-background attribute to "new":
<g enable-background="new">
An illustration using <feMerge> may demonstrate how this can prove useful. We will apply a Gaussian blur to a source image, and a slightly different blur to its background to see how this allows filters to simultaneous manipulate two images with a single filter.
| Accessing and using BackgroundImage in a filter | |
|
|
| Left: making use of BackgroundImage within a filter with feMerge | Right: the same filter without merging of BackgroundImage |
|---|---|
|
|
|
The two SourceGraphics and the three underlying horizontal stripes that constitute the BackgroundImage |
|
|
|
In the above illustration, the three vertical stripes, though lying below the SourceGraphic (the blurred white rectangle) have not been included in the <g> that has enable-background turned on, hence they are not affected by the filter #BI. Note how the three horizontal stripes in the left image (constituting BackgroundImage) have been horizontally blurred and have been layered atop the SourceGraphic. At right, though the BackgroundImage has been made accessible to the filter and a temporary image of it after horizontal blurring has been made, that result has not been shared with the feMerge and hence is discarded prior to rendering.
<feBlend> is used to blend, or mix two images together with a variety of simple methods (i.e., values of the mode attribute): "normal","screen","multiply","lighten", and "darken."
| FeBlend applied to BackgroundImage using, from left to right, modes "normal","screen","multiply","lighten", and "darken." |
|
|
<feBlend> receives input from two sources rather than just one, allowing it to take results from other filters as well as SourceGraphic (the default21) or BackgroundImage. As such it can combine quite sophisticated processes.
The values of its mode attribute are as they would be in a photo editing program like Adobe Photoshop�. Stated informally, these modes work as follows:
example:
white screen black = white
and
red (#FF0000) screen grey (#808080) = #ff8080 ("rose" or a rose-like color)
example:
white mult black = black
and
red (#FF0000) mult grey (#808080) = #800000 (a shade of red darker than "darkred")
example:
white lighten black = white
and
red (#FF0000) lighten grey (#808080) = #ff8080 ("rose")
example:
white darken black = black
and
red (#FF0000) darken grey (#808080) = #800000 ("darker red")
Neither <feMerge> nor <feBlend> presents us with a way to either average or intersect two images.<feComposite> can be used for that work. It allows the superimposition of the footprints of images as well as the relative blending of their pixel values. Like <feMerge> it takes two inputs in and in2. By default, in is the SourceGraphic.
A typical use would look like
<filter id="in">
<feComposite in2="BackgroundImage" operator="in" />
</filter>
The operator attribute takes values of "in", "over","out","atop","xor", and "arithmetic". All of these except "arithmetic" are simple attributes, but when "arithmetic" is specified, four other parameters are invoked: k1, k2, k3, and k4. These assign weights respectively to: a component representing the multiple of the two images, the linear effect of the first image, the linear effect of the second image, and an intercept or brightness adjustment. In the following illustration, when operator is arithmetic, then k1="0" k2="1" k3="-1" and k4="1", meaning that the SourceGraphic(in) contributes positively, the BackgroundImage(in2) contributes negatively and brightness has been boosted.
| Illustration of four values of operator in<feComposite> |
|
Of the various operator values, "arithmetic" and "in", probably are most useful. "Arithmetic" is useful since it allows percentage-based blending of two images (much like opacity) as well as more complex effects as shown above. "In" is useful since it constrains the presence of one image to the footprint of another, much like a clipPath, but done as a part of a filter stream.
This effect is a bit different from others in the sense that it converts pixel color values in one image into geometric distortions of another image.
<feDisplacmentMap> takes in(SourceGraphic by default) and in2, and uses a specified channel (R,G,B, or A) of in2to serve as displacement values which determine the direction and distance each pixel of in will be moved in either the x or y (or both) direction.
For example, if we chose to use the Red channel of in2to horizontally distort in, and if the underlying image represented by in2is say, a red and black checkerboard (high on Red on the red squares and low on Red on the black squares) then those pixels of in which lie above red squares will be moved to the right, while those above black squares will be moved to the left.
| Using a checkerboard to displace parts of an image. |
|
|
| An example using feDisplacementMap with SMIL can be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/filterDisplacementMap9.svg |
In the illustration above, we signify that we wish to use the Red channel and that we wish it to be used for horizontal distortion, when we specify
xChannelSelector="R" .
The scale attribute (100 in this case) determines the magnitude of the distortion (100 pixels). A value of zero would mean that no displacement of pixels will occur.
In the above example, slight discoloration of the warped image occurs in IE/ASV3, though the Opera browser keeps the image as we would expect it. Firefox, as of this writing, does not manage <feDisplacementMap>. One suggestion as to how to circumvent the discoloration would be to adjust the colorspace-interpolation-filters to "linearRGB." Another way would be to bring the image into the filter through an <feImage> as shown in the following:
| <feDisplacementMap> as a part of a more complex filter |
|
|
The fact that we used both red squares and the red channel is really unimportant. We are restricting to just the red channel, so that "red" "white" "yellow" or "magenta" squares, all of which are 100% on their red channel would all have had the same result. That is, had we used white squares instead of red, the result would have been the same, since white, restricted to the Red channel is "#FF" which translates to 100%.
Clearly this filter primitive has lots of potential for creating interesting effects with various warping gradients. The following example uses an underlying reflected gradient.
| A reflected radial gradient (left) and its use in <feDisplacementMap> (right) | |
|
|
We may use <feDisplacementMap> to define distortions that go beyond those allowed by the isometric spatial transformations: translate, rotate, scale, and skew. Based on continuous radial, and linear gradients, particularly as enhanced by <feTurbulence> it should be possible to build warps of almost any kind desired.
| A few distortions (random and customized) |
|
| Some text and an ellipse warped through <feDisplacement> by <feTurbulence> |
|---|
|
| Some text and an ellipse warped through <feDisplacement> by a rotated linear gradient. |
| This example can be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/filterDisplacementMap5a.svg |
Synchronized Multimedia Integration Language, or SMIL, is a language separate from, but closely integrated with SVG. It allows for various animation effects to be used in conjunction with SVG.
SMIL has been a W3C recommendation since 1998 when version 1.0 was adopted22. The SMIL working group has remained active since then with version 2.1 becoming a recommendation in December 2005.
SMIL's applications are not limited to the SVG environment, being appropriate for multimedia developments in a variety of other contexts, including HTML. The W3C has this to say about SMIL23:
"The Synchronized Multimedia Activity designed the Synchronized Multimedia Integration Language (SMIL, pronounced "smile") for choreographing multimedia presentations where audio, video, text and graphics are combined in real time. SMIL is a W3C Recommendation that enables authors to specify and control the precise time a sentence is spoken and make it coincide with the display of a given image."
This discussion of SMIL will be limited to its application within SVG, and even then, will cover only a fraction of this rather vast landscape.24For a broader consideration, there are numerous references available on the web, and several books including one by two members of the SMIL Working Group25.
SMIL is an example of what is known as declarative animation (related to declarative programming). Rather than specifying the details of how to do something (as in an imperative language), a declarative approach specifies what the end result is supposed to be and leaves the details of implementation up to the client software. Other examples of declarative approaches include HTML, PostScript, and SVG. With each, the programmer/developer describes something like a <circle> and lets the device implement it to the best of its ability. The developer does not need to worry about kerning of characters, placement of pixels, anti-aliasing, dithering and fill-region algorithms. In fact the entire field of vector graphics is sort of a case in point: one describes the thing to be drawn and then different devices (like a screen or a printer) will render the underlying concept in, perhaps, very different ways. 26
An example:
| Progress of a simple SMIL animation | ||
|
|
|
| Prior to clicking "GO" | 1 (or 3) seconds elapsed | 2 seconds elapsed |
|---|---|---|
|
||
| This example may be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/SMIL1.svg | ||
Above, we have two rectangles each inside <g>s named either "G" (for "GO") or "S" (for "STOP"). Unlike in HTML, neither "button" has an event handler directly associated with its code. Instead, the "arming" of the button is done by any of the things that will respond to it. The ellipse has had a special tag, <animate>, inserted into it. That's where the SMIL takes place.
Let's take a closer look:
| The animate tag in SVG/SMIL | |
| The code | The meaning |
|---|---|
<animate
|
The tag is embedded in the middle of the SVG object to be animated, in this case an ellipse. |
attributeName="rx"
|
This identifies which attribute of the parent object (ellipse) will be modified by the animation. |
begin="G.click"
|
This says that the animation will begin when the object with id="G" is clicked upon. |
end="S.click
|
This says that the animation will finish when the object with id="S" is clicked upon. |
dur="4s"
|
This says the animation will last 4 seconds. |
|
|
This establishes the values of the animated attribute (rx), which will begin at 10 (pixels) increase incrementally to 110 (pixels) and finish back at 10 again. By default, 110 will be reached halfway between the beginning and end of the animation: namely after 2.0 seconds have elapsed. |
repeatCount="indefinite"
|
This means the animation will keep going indefinitely. That it will continue repeating until "S" is clicked. |
/>
|
This terminates the <animate> tag. |
An important observation from the above, is that we do not tell this infinite loop how often it is to refresh the screen; we trust the browser to figure that out for us based on the description of what we want to have happen. Again the emphasis is on what is done rather than how to do it.
Another thing to note is that in this case we wish to have the ellipse grow and then shrink again (gradually in both directions). To do this, we specify the maximum value (110), place it in the middle of the two minima (10) at either end of the values list, and let the browser figure out that 110 will occur halfway through the 4 second animation.
Before plunging into SMIL's capabilities and exploring the syntax of the <animate> and allied tags, we'll make a brief comparison to another prevalent approach to web-based animation. If you are not familiar with JavaScript-based animation, then you may wish to skip this section. For those with prior knowledge of JavaScript animation, it may help to put SMIL animation in a helpful cognitive framework. This section is not required for an understanding of SMIL; rather it is a justification for why you should learn SMIL! It is easier.
While this book discusses the IE/ASV, Firefox, Opera, Safari and Chrome browsers, only IE/ASV and Opera support most of SMIL at the current time, while Safari's support is somewhat fledgling, having just been introduced in Safari 4 beta in early 2009. JavaScript is supported in all five environments. Some might view this as reason to dismiss SMIL right here and now, but by the time this book reaches your hands, it is quite possible that Firefox and other browsers will be quite close to having SMIL implemented within their SVG suite.
In JavaScript, animation is usually created using the methods setTimeout() or setInterval() (associated with the window object). These methods allow repeated updates of the screen after changing certain attributes of the objects on the screen.
Suppose, for example, we wish to move an object around on the screen by changing the values of its x and y coordinates of a <div> that contains it. (Typically, the object will be a child of an absolutely positioned <div> tag.) With every refresh of the screen (happening every dt units of time) we move the <div> dx pixels horizontally and dy pixels vertically. The author of such an animation must guess the screen refresh rate of a typical visitor's client software and then adjust dt, dx, and dy accordingly so that dx and dy are kept as small as possible subject to the constraint that the browser must do all that it needs to in dt units of time. That is, the author must engage in guesswork and experimentation to determine what will produce a smooth animation. If we make dt too small then the CPU and screen of the user's machine may not be able to perform all the calculations that we require of it. If dt is too large, the object will appear to move very slowly, implying that we may wish to increase dx and dy to increase the speed. This however, is fraught with problems since values of dx and dy that are too large will result in apparent large jerking leaps across the screen.
Another complexity is involved in setting up multiple independent animations running in parallel. This has been rather notorious for the difficulty of managing the timing. Typically, one sets up a large central timing loop in which all animated objects are updated every dt units of time — the problem is that not all animations will look quite right if they are all running in integer multiples of the same dt, which is what happens with the central timing loop. Alternatively, one may try to set up multiple setTimeout loops, but the problem here (as oft reported) is that any synchrony associated with the separate loops tends to dissipate over time, particularly when the JavaScript application is taxing the CPU to begin with.
In contrast, the declarative animation of SMIL lets the browser software handle all these decisions since the locus of the animation is kept directly affiliated with the animated object.
Herewith, a side-by-side comparison of the two different approaches to setting up an oscillating ellipse as in the above example.
| Oscillating ellipse — two approaches with similar results | |
| SMIL animation | JavaScript animation |
|---|---|
|
|
Note that there is considerably less code to maintain, and considerably less complexity in the code with SMIL for this sort of animation27. It should also be stated that the behavior of these two approaches is not completely equivalent. On the Windows machine I am using today, the JavaScript approach runs slightly faster in Internet Explorer. An older machine will run it slower. This can be changed by adjusting the timing — currently it changes the radius of the ellipse one pixel every 10 milliseconds. In Opera, the JavaScript animation is not smooth (appearing to jerk just a bit), though the SMIL animation is quite smooth.
One pleasant feature about the SMIL approach is the close integration of the animation with the object itself. Note that the <animate> tag is nested directly within the <ellipse> rather than in a function located elsewhere and relying on at least some global variables.
Not all animated effects are best in SMIL. Many types of animation (like setting a variety of objects bouncing off of one another) are simply not feasible in SMIL. However, both SMIL and JavaScript-based animations are possible in SVG, so that the developer has both sets of capabilities at his or her disposal.
Most simple SMIL animations appear at least as smooth if not smoother than their JavaScript counterparts. At first glance, some SMIL animations may seem more sluggish than their JavaScript counterparts. In general, it seems that so long as the attributes being animated and the objects to which they belong remain relatively simple, SMIL animations will be just as robust and often smoother than JavaScript animations for the same purpose. There are examples28, however where the JavaScript animation will appear smoother than its SMIL counterpart. Many of these differences are likely to be browser-dependent.
In some tests of the relative priorities given to SMIL and JavaScript animation, when both are running and when the calculations or screen I/O overwhelm the processor, it was found that both the IE/ASV and Opera browsers give first priority to the SMIL animation, attempting to accomplish those tasks first before any JavaScript is attempted.29This means, on occasion, that applications which rely on both may end up with the JavaScript completely stalled.
Let's start with a sort of simplest case: a rectangle that moves across the screen from left to right.
<rect x="10%" y="20" height="100" width="50" fill="blue">
<animate attributeName="x" dur="4" values="10%;90%" />
</rect>
In this case we've specified exactly three attributes: the attribute being changed (attributeName), the duration of the effect (dur) and the starting and ending values of the animated attribute. This results in a blue rectangle that starts moving when the page is loaded, moves across the window (until it is 90% of the way across), stops for a moment's hesitation30, and then resets itself to its original, pre-animated, position. If we had begun with the assignment x="50%" instead of x="10%" we would see no difference in the appearance of the animation from beginning throughout its duration (since by the time the screen is rendered the animation has already begun), but at the end of the animation, the rectangle will reset to x="50%": hence a difference in how it terminates.
We may also make all these measures not in terms of relative screen coordinates like 95%, but in absolute pixel amounts like 120, if we so prefer.
If we wished to have the animation stop at its last position, instead of reverting to its first, we could add the attribute
fill="freeze"
If we wished for the animation to move smoothly rightward (until 90% of the way across the screen) and then return smoothly to the beginning, we simply add another value to the values list.
<rect x="10%" y="20" height="100" width="50" fill="blue">
<animate attributeName="x" dur="4" values="10%;90%;10%" />
</rect>
This succeeds in moving the rectangle all the way across the screen and back in the same four seconds used to move it just one direction in the earlier example. If we wished the last two animations to move at the same speed, then we would double the duration, since the distance to be traveled is twice what it was.
We note also that the animation will reach its maximum value of x (90%) exactly halfway through the four seconds, since 90% is at the midpoint (the median position) of the three values contained in the values list. That is to say:
values="10%;90%;10%"values="10%;50%;90%;50%;10%"
are equivalent (since 50% is halfway between 10% and 90%), while
values="10%;90%;10%"values="10%;30%;90%;30%;10%"
are not. The latter values list will cause the rectangle to move faster during its middle half (the second and third of the four intervals determined by the five endpoints) than during its beginning or end.
If we wish the animation not to stop but to run itself three times (at four seconds apiece), then we add an attribute:
repeatCount="3"
If we wish it not to stop but to keep going, then
repeatCount="indefinite"
will do the trick. It will keep going either until the page is closed, or the animation is stopped through some event, as discussed shortly.
Multiple animations and timing
It is important to realize that we may animate more than one element at a time:
<rect x="50%" y="20" height="5%" width="5%" fill="blue">
<animate attributeName="x" dur="2" values="10%;90%;10%" />
<animate attributeName="y" dur="2" values="10%;90%;10%" />
</rect>
This code succeeds in moving the rectangle back and forth diagonally across the screen, from upper left to lower right.
In this example, the timing of both the x and y attributes are the same, meaning that both values reach their maxima and minima at the same time (as in a typical single loop JavaScript animation). This need not be the case however. The following code creates an animation in which the circle bounces about the screen like a billiard ball:
<circle cx="50%" cy="20" r="5%" fill="blue">
<animate attributeName="cx" dur="2.7" values="5%;95%;5%"
repeatCount="indefinite" />
<animate attributeName="cy" dur="3" values="5%;95%;5%"
repeatCount="indefinite" />
</circle>
This particular animation will repeat itself every 27 seconds, since 27 is the smallest number that is evenly divisible by both 3 and 2.7 — a necessary condition for the periodicities of cx and cy to synchronize. Observe, also that the angles involved in the bouncing of this billiard ball are all in the neighborhood of 45 degrees (assuming a square screen) since the values 2.7 and 3.0 are close in magnitude. If we wanted the bouncing to be more vertical than horizontal, then we could merely decrease the duration of the cy variable to something like:
<animate attributeName="cy" dur="0.5" values="5%;95%;5%"
repeatCount="indefinite" />
while keeping the duration of cx unchanged. Alternatively, to the same end, we could also increase the number of key values for cy as follows:
<animate attributeName="cy" dur="3" repeatCount="indefinite" values="5%;95%;5%;95%;5%;95%;5%" />
It is well worth pointing out that the types of the values should match. For example, if we were to try to interpolate between the value "0" and the value "50%", the browsers can be expected to use discrete animation in such a case. There are good reasons for this. I will leave finding those reasons as an exercise for the reader. ☺
In such animations, though we have succeeded in varying the horizontal and vertical components of the velocity independently, the apparent overall speed of the movement remains constant. We may vary the apparent speed through the use of the keyTimes attribute.
What keyTimes does for us is to allow the values to provide an uneven distribution over the time interval. Ordinarily, when we specify something like values="5%; 95%; 5%", those values for the animated variable correspond to the times 0%, 50% and 100% (as a percent of the way through the animation). The statement values="5%; 10%; 95%; 10%; 5%" would, by default, correspond to the times (0%; 25%; 50%; 75%; 100%) — that is, five "key times" associated with the four intervals between the beginning and end of the animation. Accordingly, the animation given by
<animate attributeName="cy" dur="0.5" values="5%;95%;5%" repeatCount="indefinite" />
is equivalent to the one specified by:
<animate attributeName="cy" dur="0.5" values="5%;95%;5%" keyTimes="0; .5; 1" repeatCount="indefinite" />
The keyTimes attribute, by default, breaks the animation duration into N equal intervals, where N is the number of values in the values attribute. keyTimes, like values, is a semi-colon delimited list that serves to determine when, in the course of the animation each of the values should be attained by the animated object. We may change the time at which the animation will reach intermediate values, by making unequal the intervals specified within keyTimes. The animation
<animate attributeName="cy" dur="10" values="5%;95%;5%"
keyTimes="0; .1; 1" repeatCount="indefinite" />
will make the attribute cy take on its value of 95% after one second (the duration, 10 seconds, multiplied by the second key time: 0.1). The ellipse will, however, take nine seconds to get back to the starting position, meaning that it moves considerably faster in the first second than in the remaining time.
Thus far each of the animations we have considered (changing x and y coordinates) will result in animations for which both a) the paths traversed by the objects will be piecewise linear and b) the derivative of the velocity curve will be either constant or discontinuous. That is if there is to be any change of direction, speeding up or slowing down, then these changes will be sudden or discrete rather than gradual and continuous. There is, however, another timing control mechanism, known as keySplines which allows us to change the curve governing the rate of change over the keyTimes interval from 0 to 1 and thus produce gradual rates of change to an object's attributes.
If we specify that
calcMode="spline"
then we are in a position to be able to use keySplines to make transitions among the keyTimes follow cubic splines rather than simple component-wise linear chunks.
This topic is a bit complex, and since it is not crucial to one's understanding of SMIL animation, one might wish to skip ahead. Nevertheless, herewith is some discussion and examples of the use of keySplines.
| A series of points traversed by a moving object using keySplines. |
|
|
Here, the path followed during the first half of the animation is the path on which points 1,2,3,4,5, and 6 lie. After the midpoint of the animation, the blue circle returns to its initial position via the line 6, 7, 8, 9, 10. If calcMode="spline" were not invoked, the animation would follow the path (10,9,8,7,6,7,8,9,10) repeatedly. The keySplines attribute contains a series of two control points in the (timein, timeout) plane, or four integers for every inter-keyTimes interval: in other words, we will have 4(n-1) integers for n keyTimes. Each pair of control points defines an approach gradient between the associated pair of key values. In this example, we will begin at (t=0, y=5%) and follow the spline attracted first by (t=1, y=5%) (all the while x is increasing linearly); then attracted by (t=0, y=95%) and finally ending up (at the end of the first transition) at (t=1, y=95%) namely halfway through the animation at position #6. (since x with half the duration has already been reset to its endpoint, 5%). The animation will speed up considerably between positions #3 and #4 since that is when the time deformation is greatest. The animation continues back along the two lines, since the keySplines path "0 0 1 1" does not alter the time sequence in the second half of the animation.
One more example may help to clarify a bit.
|
In the above the blue circle will follow the path given, more simply, by the ellipse
<ellipse cx="50%" cy="50%" rx="45%" ry="45%" />
Were it not for the keySplines, the animation would follow a diamond connecting the four midpoints of the bounding rectangle of the screen. However since time runs linearly in the x direction while warped in the y direction and vice versa, the path ends up being bowed. The other thing about the above animation is that it speeds up considerably at the position of the key values; since that is where the time gradient changes most rapidly.
In case this seems a bit complex, it is. The W3C has this31to say on the subject:
When a keySplines attribute is used to adjust the pacing between values in an animation, the semantics can be thought of as changing the pace of time in the given interval. An equivalent model is that keySplines simply changes the pace at which interpolation progresses through the given interval. The two interpretations are equivalent mathematically, and the significant point is that the notion of "time" as defined for the animation function f(t) should not be construed as real world clock time. For the purposes of animation, "time" can behave quite differently from real world clock time.
The reader will perhaps be relieved to discover that there are simpler ways to get an object to travel along a particular path during animation than by warping the time-space continuum. We will turn to other varieties of animation very shortly.
Before moving to other aspects of SMIL, a couple of observations should be made. The author of an SVG document is not limited to one animated object, nor just a few per page. I have tested the embedding of several thousand independent objects each with independent SMIL animations, and generally (depending on the complexity of the features being animated), the browsers keep up fairly well.
We might also encourage ourselves to remember that almost anything can be animated. In reading the W3C recommendation, finding something that cannot be animated is rather rare32. Animation is a most interesting aspect of SVG. It also can be quite instructive to the learner: by animating an attribute, one can gain a very concrete sense of what exactly it is that that attribute controls. Here is a brief listing of some of the types of effects one can create -- some with rather stunning results:
| Various animatable attributes of objects |
|
The cx, cy, rx and ry of an ellipse |
|
The height of an image |
|
The opacity of an image |
|
The scale of an feDisplacementMap |
|
The focal points and radius of a reflected radial gradient |
|
The offset and stop-opacity of a stop in a radial gradient |
|
The baseFrequency or seed of an feTurbulence |
|
The height of a rectangle contained in a pattern |
|
The values of an feColorMatrix |
We also have the capability to animate features which are multi-valued, path-based, transformational, non-numeric, or chromatic. We will discuss examples of each, in turn.
Thus far, all the animated effects we have investigated for SMIL have involved single-valued (or scalar) attributes: such things as the height of a rectangle, the radius of a circle. However in some cases, the value of an attribute may be a list (or vector) with multiple values in it. An obvious example is the d attribute of a path: a space delimited list of x and y coordinates. Well, it so turns out, that we can animate these sorts of things as well, provided the number of items in the lists associated with the animated attributes matches up. If a path has 17 points at the beginning of the animation it should have 17 points throughout the animation.
Then, (provided that the lists match up in quantity and are numeric), SMIL interpolates by generating intermediate frames. It's great for animating Bézier curves to give character to a contour or allow a complex shape to mutate gradually. Here's a simple example:
| Gradually changing one control point in the "d" attribute of a cubic spline curve | ||
|
|
|
|
Beginning (0 seconds) C2=(100,400) |
Middle of first half (about 0.5 sec) C2 ≈(100,250) |
End of first half (1 sec) C2=(100,100) |
|
||
| This example may be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/path10.svg | ||
In this example, only one point has been changed, to make the example simple enough to read. But that is not at all a requirement. I've created animations of Béziers that contain several hundred points, and the animations appear to transition quite smoothly.
One of the most convenient and pleasant aspects of SVG is the fact that we may build a path, and let an object follow it over time. Given the complexity of Bézier curves for example, being able to instruct an object "follow this path" without having to calculate, in your code, where the path actually is in (x,y) coordinates, is remarkably handy and user-friendly. It is not too tricky, so here's an example.
| Using <animateMotion> to follow a <path>. Five stages of a repeating animation. |
|
|
| This example may be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/SMIL7g.svg |
In the above, the path (with id="curve") describes a smooth curve using two cubic transitions. It begins and ends at the same (x,y) coordinate, (110,200). The <ellipse> is instructed to follow "curve" through the use of an <animateMotion> containing an embedded reference to the path by its id. We specified rotate="auto" so that the ellipse will actually rotate as it moves around the path so that its orientation parallels that of the path. Another matter worth noting is the initial position of the ellipse. Observe that ellipse #2 is just slightly above the curve. This is because its initial position cy=-5 becomes translated into an offset relative to its ultimate position on the curve.33
Timing of the motion is uniform relative to the length of the curve. If we wish to vary the timing, and make it differential over the path, then we can apply keySplines as discussed above in the case of the <animate> tag. The following can be inserted into the above example, replacing the earlier <animateMotion> to change the motion so that it appears to hesitate twice in each traversal of its path
<animateMotion dur="2s" keyTimes="0;.6;1"
calcMode="spline" keySplines="0 0 1 0;0 0 1 0"
rotate="auto" repeatCount="indefinite" >
<mpath xlink:href="#curve"/>
</animateMotion>
Animation of transformations
Not all SVG objects share the same collection of attributes. A rectangle and an image share the attributes x, y, height and width, but an ellipse has cx, cy, rx, and ry to control its placement on the screen. A common way of moving all these objects about on the screen is a natural thing for a developer to want. Likewise we might wish to be able to change the size and rotation of objects without having to calculate the new coordinates and replot a new curve. We have seen (in Chapter 2) how to use the transform tag to rotate, translate, and scale objects, so it might appear obvious to try something like:
<someSVGtag transform="rotate(90)">
<animate attributeName="transform" dur="2s"
values="rotate(90);rotate(180)" />
</someSVGtag>
It doesn't work quite that way, but that's actually close to how it does work. You may recall then when we wanted to rotate a linearGradient we used something called <gradientTransform>. Likewise, to rotate an animation we use <animateTransform>. An example:
<ellipse cx="280" cy="175" rx="100" ry="50" fill="blue">
<animateTransform attributeName="transform" type="rotate" dur="2.5"
from="360,280,175" to="0,280,175" repeatCount="indefinite"/>
</ellipse>
This succeeds in drawing a blue ellipse and then rotating it 360 degrees counterclockwise every 2.5 seconds.
We discussed the impact that one transform will have on the next in Chapter 2 (section 4: "Multiple transformations and more"). Likewise during animation, we may be interested in, for example, keeping an object in the same relative screen location while changing its scale and rotating it, hence in applying more than one <animateTransform> to a given object.
<ellipse cx="216" cy="242" rx="160" ry="219" fill="#964">
<animateTransform attributeName="transform" type="translate" dur="4s"
values="0,0;-110,-140;0,0" repeatCount="indefinite"/>
<animateTransform attributeName="transform" additive="sum" type="scale"
dur="4s" values="1;1.5;1" repeatCount="indefinite"/>
<animateTransform attributeName="transform" additive="sum" type="rotate"
dur="7s" values="0,216 242;360 216 242" repeatCount="indefinite"/>
</ellipse>
Just as we had to adjust for the effect that one transform had on the object when applying another with static images, we likewise must do so with moving images. The above example will both rotate and resize an ellipse, but preserve its center.
In order to do this, we require that the attribute
additive="sum"
be set to prevent the previous animations from being ignored.
While SMIL provides for smooth transitions between attributes with numeric values, these are not the only sorts of attributes that can be animated. A couple of examples are provided to encourage the reader to think broadly about SMIL's utility.
| Various non-numeric but useful animations. | |
| The xlink:href of an <image>. It creates an image "rollover" transitioning between 3 images, and showing each for 2 seconds apiece |
|
| The fill of a <rect>. Color values are interpolated smoothly. |
|
| The flood-color of an <feFlood>. Color values are interpolated smoothly. |
|
| The mode of an <feBlend>. Useful for seeing how <feBlend> modes actually work.. |
|
If values of an attribute cannot be interpolated between, that is not a problem for SMIL. If intermediate values (as in the modes of an <feBlend> or the files associated with an <image> are the values chosen, then each value is simply kept for an appropriate fraction of the duration of the animation.
On the other hand, since the values of colors associated with the "fill" attribute can be interpolated, the browser will actually do so (at least IE/ASV and Opera where these effects can be observed).
SVG actually contains an <animateColor> tag, though it appears to offer little, if anything, that cannot be done with the animation of the fill, stroke, or flood-color attributes as in the above examples.
A given SVG page may combine textual information with interesting graphics. That is clearly part of the appeal of SVG and the web in general (else we'd still all be using gopher34). Suppose that we would like our visitors to be able to read our information for a few seconds before certain animations begin — perhaps we fear that our graphics might be so compelling that they might overwhelm the message. We could tell our marketing division that if they are really worried then they should tone down the graphics a bit, or we could simply delay their onset.
Another scenario that makes more sense to this academic fellow is this: suppose I wish to have several objects following one another around a path — spaced out at equal intervals around the path. I that case, I can animate each but delay when it is that they begin. Each earlier one can be given a head start along the path. We may do that by specifying a begin attribute of, say begin="1.0s" to delay the onset of the animation of one of the things we wish to hold back.
| Two ellipses following the same path; one is halfway behind the other |
|
In the above, the two ellipses are identical except for the "begin='1'" assignment in the second. Since "1" = "1.0s" is half of dur="2s", the second animation will begin halfway through the other's traversal of the path.
The only problem with this approach is that during that first second while it is waiting to start, the second ellipse is rendered, but at its origin (0,0) (since neither cx or cy is specified, their default values are each zero). That may be undesirable, or, at best, odd. How might we keep the image invisible while it is waiting to start?
|
Before the second animation gets started. (Note the ellipse waiting in the corner.) |
|---|
|
Well, like many things in SVG, there is more than one way, and each is instructive in its own way.
First, we might proceed as follows: instead of positioning the ellipse-in-waiting at (0,0) why don't we just place it on the curve? We know, for example, that the point (450,100) lies on the curve (it is the last of a cubic triplet in the d attribute) and it would seem to be about halfway along the curve. So, let's try the following:
<ellipse id="secondEllipse" cx="450" cy="100" ... >
There are three problems with this. The first, possibly minor issue, is that the second ellipse will appear in its initial and unrotated orientation. It will not be pointing in the same direction as the curve. The second is that the point (450,100) is probably not the exact midpoint of the curve. This is perhaps not a major issue either. The biggest problem, though, is that the positioning of the thing will be done relative not just to the curve but to its own offset relative to (0,0). Ultimately the underlying transformations (rotation, scale, and translation) will be amplified by its now large distance from the origin (and hence the curve itself). The second ellipse will not in fact follow the curve but some strangely distorted and amplified shadow of it (most of which exists off-screen in this case).
Alternatively, we could animate the opacity of the second ellipse, having it wait until after it has moved from its initial position (0,0) onto the curve, thereupon to become visible.
<ellipse cx="0" cy="0" rx="20" ry="12" fill="#aaa"
stroke="#666" stroke-width="2" opacity="0">
<animate attributeName="opacity" values="0;.8" dur="1" begin="1"
fill="freeze"/>
<animateMotion dur="2s" begin="1" rotate="auto"
repeatCount="indefinite" >
<mpath xlink:href="#curve"/>
</animateMotion>
</ellipse>
We had to remember to set the initial value of its opacity to zero, since the onset of the animation is delayed by a second, meaning that its initial value of zero under the animation will not be encountered until the animation stops. We also have to set fill="freeze" so that the opacity retains its final value and does not reset to the initial value.
The above succeeds in bringing the second ellipse onto the curve at 1.0 seconds, but at zero opacity. It then begins moving and gradually fading into view over the next second. This works just fine and is a somewhat pleasant effect.
This approach with fading an object in by adjusting its opacity is, however, a bit fancier than we may have intended for this relatively simple problem. The goal was to prevent the object from becoming visible until its animation started.
It turns out there is a simpler way to control such a thing in SVG — the <set>. In the above example, instead of the <animate attributeName="opacity" ...> tag, use this:
<set attributeName="opacity" to=".8" begin="1" />
It keeps the second ellipse invisible until one second after the animations begin (which is just at the time it is introduced onto the path).
A more modular solution which lets the onset of the <set> coincide directly with the beginning of the <animateMotion> (rather than having both rely on the same unit of measured time: 1.0s) is given by the following:
<ellipse cx="0" cy="0" rx="20" ry="12" fill="#aaa"
stroke="#666" stroke-width="2" opacity="0">
<set attributeName="opacity" to=".8" begin="M.begin" />
<animateMotion id="M" dur="2s" begin="1" rotate="auto"
repeatCount="indefinite" >
<mpath xlink:href="#curve"/>
</animateMotion>
</ellipse>
We give the <animateMotion> an id ("M") and then let the "begin" of the <set> be triggered by the "begin" of "M." My wife would call this a "puckish solution."
begin="M.begin"
The above is an example of an animation (in this case a set — a sort of a degenerate case of an animation) being triggered by an event. We'll discuss this in just a bit more detail now, since it is popular and natural to use a mouse-click to start an animation.
| Animation triggered by mouse-click | ||
|
|
|
| Before begin | At begin | After dur="6s" |
|---|---|---|
|
||
When we click on the text object (the parent of the <animate> and hence, the target of the click), the animation begins, looping from a size 8 font to a size 50 font every 6 seconds.
What is noteworthy about this (and may seem different from what the JavaScript programmer may be familiar with) is that the event handler is assigned to the object receiving the click by some other object, in this case the <animate>. This is interesting since we may have multiple objects whose animations are all triggered by a click on the <text> though their identities are more or less unknown to the developer who looks only at the source code associated with the <text> itself.
Let's extrapolate a bit and make the example above have a bit more behavior. It is common in the world of the web and in user-interface practice in general, to have things which act like buttons (by triggering events) also feel like buttons. That is, in the real world, a button tends to have a bit of give to it: brushing your hand against the toaster accidentally does not turn it on (either the toaster or the hand). We often make our virtual buttons responsive, in somewhat the same way, to let the user know that it is a button, and hence, that it may do something when clicked. A common trick for this is to give it a rollover effect. This can be achieved quite easily in SVG.
| Button with rollover effect | ||
|
|
|
| Before begin | When mouse enters | When mouse exits |
|---|---|---|
|
||
The above text will still expand and shrink as before but we note that we have also affiliated with the text the ability to respond to two new events: the event of when the mouse moves over it and then another when the mouse departs.
As we saw in an earlier example in this section (where we transitioned the opacity of ellipses to follow a curve), objects need not be triggered by events on nearby objects (in the sense of proximity in the document). We saw how
<set attributeName="opacity" to=".8" begin="M.begin" />
relied on an event associated with an object named M. We can also delay the onset of the second to follow the onset of the first by one second:
begin="M.begin+1"
Suddenly SMIL's timing starts to take on a richer character altogether! Likewise we may start an animation of object A with a mouse-click on object B.
begin="B.click"
And we might also use something of the following type:
begin="0s;B.click"
This means begin (the <animation> or <set>) either at zero seconds (when the page loads) or when a button is clicked. Using either of two ways to start an animation doesn't make sense, though, until we first can figure out how to stop an animation.
There are several fairly straightforward ways to stop an animation.35
We might:
For now, approaches 1 and 2 are relevant to SMIL, while the others await our discussion of scripting.
While approaches 1 and 2 above, should follow fairly directly from the above section (on starting an animation), an interesting problem emerges if we try to create an on/off button which both starts and stops a given animation. The problem and its solution are just instructive enough, that it may prove useful to examine it in some detail.
To have a single button on a page which both starts and stops an animation seems like a natural thing. To have two buttons, one that starts something and another that stops it is fairly straightforward:
First we build a couple of buttons. We'll put text ("go" or "stop") on top of rectangles and put the text and its rectangle in a group with an id (so that clicks on either the text or the rectangle behind it are responded to by the group):
| A start and a stop button |
|
|
Clearly we could make much fancier looking buttons, but here we're interested more in making the code legible than in making the buttons fancy.
Now, we want to make an animation which responds appropriately to clicks on the other button.
Let's make an animation which stops when "stop" is clicked.
| An animation (an ellipse following a curve) that stops when the "stop" button is clicked |
|
|
The reason we include
fill="freeze"
above is that without it, the ellipse would revert to its initial position at cx=0; cy=0. This keeps it at its last position when the animation stops.
This works just fine. Now how do we get the "go" button to work as well? It would seem natural to use
begin="go.click"
inserted into the <animateMotion> tag. The problem is that then the animation never starts, until we push the "go" button. We wanted the animation to start when the page loads or when the go button is clicked. A solution:
begin="0s;go.click"
This means start the animation either at zero seconds (i.e., when the page loads), or when the "go" button is clicked. This does the trick.
As long as we're concentrating on what is becoming a rather lengthy piece of code, let's work a bit further in this direction. Let's get two ovals following one another around the curve, and have them both start and stop when the buttons are clicked. But let us still have the second one start a second later than the first. Here's how to get the second animation to start a second after the first.
<ellipse cx="0" cy="0" rx="20" ry="12" fill="#aaa" stroke="#666" stroke-
width="2" opacity="0">
<set attributeName="opacity" to=".8" begin="One.begin+1" />
<animateMotion dur="2s" begin="One.begin+1" id="M"
end="stop.click" fill="freeze" rotate="auto"
repeatCount="indefinite" >
<mpath xlink:href="#curve"/>
</animateMotion>
</ellipse>
Just like the first, animation, we stop this one based on a click on "stop." But instead of trying to have the animation begin at 1s (which indeed delays its beginning to one second after the loading of the page) we set its onset to
begin="One.begin+1"
The onset is, hence, relative to the beginning of the first animation, but delayed one second. That way, whether the first animation begins through loading of the page, or through a mouse click, the second animation follows a second behind.
But how might we make it so that one button does both starting and stopping. It is quite easy in JavaScript, but that topic awaits a later chapter. How to use SMIL to do this is the question.
The first way we'll consider is to actually put two buttons in the same location (mimicking one button) and letting a click on either hide itself and reveal the other.
<text id="start" x="35%" y="35%" font-size="30" fill="Green">
<set attributeName="display" to="none" begin="start.click" />
<set attributeName="display" to="inline" begin="stop.click " />
Start
</text>
<text id="stop" x="35%" y="35%" font-size="30" fill="red" display="none">
<set attributeName="display" to="inline" begin="start.click " />
<set attributeName="display" to="none" begin="stop.click" />
Stop
</text>
Then it is simply a matter of affiliating the cessation of the animation with the stop button (when it is visible) and the animation's begin with the start button.
<animateMotion dur="3s" begin="start.click" end="stop.click" ... " >
Clearly, the key here is to make it look as though the state of the button is changing, when in fact the entire button is being replaced by another.
To actually have one button that serves as both a start and stop button, here is a solution that does the trick (though the activity is triggered by mousedown and mouseup events rather than successive clicks):
| One ring to bind them | |
|
|
| Before button is pushed | While button is depressed. |
|---|---|
|
|
| This example can be seen at http://srufaculty.sru.edu/david.dailey/svg/newstuff/SMIL7h.svg | |
The mouse events change not only the fill attributes of the <text> and the <rect>, begin and stop the animation of the <ellipse> but also change the text displayed on the button. This last result is accomplished through the use of a <tref>, a child of a <text> node that allows the text itself to be animated.
In the following chapter (under SMIL to JavaScript event passing), we will find that SMIL events can be used to trigger JavaScript functions, extending its capabilities even further.
In conclusion, SMIL is a very compact and efficient way of directing complex events with minimal script. It is accessible to script, and hence, compatible with it. It is a type of choreography that seems to be commanding the attention of influential people. It is quite likely that we will see more of it and allied technologies popping up here and there in web development during the foreseeable future.
Why scripting?
Some of the readers of this book may have programmed extensively in many languages. Others may never have written serious programs. Others still may have dabbled, through a perhaps inspired process of trial and error, with minor modifications to a JavaScript program that they found attached to an HTML document developed by someone else. Still others may be unaware of the difference between a programming language and a markup language.
To those who know programming beyond a cursory introduction, the scope of what programming might add to SVG is immediately obvious. To the others, it is recommended that you read carefully through Appendix II in which JavaScript is introduced in a rather abbreviated way, focusing on those aspects most likely to help with one's SVG development.
Regardless of the reader's vantage point, an explanation of "why scripting?" is probably in order. Given how flexible SVG is, with its compound filters and its SMIL animation that enables message passing between objects, what can scripting do for us that we cannot do already?
Traditionally, we distinguish between programming languages and simple sets of commands (like batch files) by virtue of three important constructs: the ability to define variables which store information for later use, conditional logic (if some condition is met, then perform some specific action, otherwise don't), and looping (keep doing something again and again until some condition is met). Once one has those three constructs in a language then one can do pretty much whatever can be specifically defined.36Let's look at SVG in these terms.
We can store information in objects (though it isn't clear how we might later use that information without programming):
<rect id="specialMessageForWhomsoeverKnowsHowToReadIt" x="100" ...>
We can also loop until some condition is met or let it begin conditionally (when, for example, a mouse clicks) using a SMIL animation.
<animate dur="2s" end="someAct.end" fill="freeze" begin="Button.click" repeatCount="indefinite" >
So, what is programming going to do for us that SVG with SMIL can't? The concept is probably a hard one to characterize completely. A course in Theory of Computing would probably give the reader a good idea, but absent that I will attempt to illustrate the concept with a few examples:
Without programming in SVG we cannot
From my experience as a college teacher, I have discovered that not all people would share all of the above as personal interests. However, one can take some comfort, perhaps, in realizing that, if one wanted to, one could do any of the above with the right program. I would venture to suggest that not all of these programs, though, have yet been written. You might have to do it yourself.
Another note that I should mention in this context: there have been a variety of good "libraries" developed to help with the deployment of scripting in the context of SVG. Several have large and useful open-source components. I would love it if one of the readers would volunteer a chapter on this topic, as I haven't frankly followed these developments at all closely. The Dojo Toolkit (see http://www.dojotoolkit.org/) and Gemi (http://www.dotuscomus.com/svg/) are two of those that I know enough about to recommend exploring further.
Let us start rather modestly, with figuring out how to activate simple scripts based on actions done by our users37or visitors. I'll try to follow the goal of keeping examples simple, brief and crisp, in hopes that the material will make sense to programmers and non-programmers alike. But toward the end of this chapter and to some extent in the next chapter (on SVG and HTML), a few longer examples will be included. This is, in part, since the nature of this book has changed from its original conception as a print document to a web-based version. Longer examples can be comprehended more fully by interacting with them on the web, and at least one of the book's reviewers recommended that a few more complex examples be included. The authorial intent, though of keeping the examples brief and self-contained, is nonetheless, reiterated. If you are one of those readers who is a non-programmer, then please be comforted to know that a sincere attempt has been made to keep things simple when possible!
Let's see if we can do a very simple thing: activate an "alert box"38as soon as our SVG page loads. If we are familiar with launching scripts from HTML we might be tempted to try something like the following:
| A first (but partially unsuccessful) attempt to launch a JavaScript function when a page loads | |
|
|
| When the page loads in Opera 9.0 | When the page loads in IE/ASV3.03 |
|---|---|
|
|
| When the page loads in Firefox1.5 | |
|
|
When we do this, we'll discover that when the page is viewed in either Opera 9.0 or Internet Explorer (with the Adobe plugin), then an "alert()" actually appears as desired. But, it is worth noting, IE does not render the image until after the alert is dispatched while Opera 9.0 does. And to our dismay, Firefox, or for that matter, Opera 9.6, Safari 3.2.2 and Chrome 1.0, don't render the page at all, instead revealing an error message.
It turns out that this is not these other browsers' fault. Their behavior is consistent with the W3C standards on being "namespace aware." In Chapter 2 we discovered that the <svg> tag needed a bit more to be proper. Specifically, W3C recommends (and most browsers require) that for an SVG document to be properly formed the <svg> tag must contain a namespace declaration.
<svg xmlns="http://www.w3.org/2000/svg" onload="alert('message')">
would suffice for this purpose since, Firefox reading the document as XML now knows what XML namespace to use to interpret the file. But so long as we're at it we might as well add an additional namespace declarations:
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
This allows us to use xlink (as in <use xlink:href="OBJ" />) if we like. Most authors recommend that both namespace declarations be used in all SVG documents if for no other reason than to avoid potential problems later on. There are times at which we might like to add in additional xml namespaces, particularly in dealing with compound documents which intermingle different XML languages. We might for example wish to add such things as
xmlns:ev="http://www.w3.org/2001/xml-events"
or
xmlns:math="http://www.w3.org/1998/Math/MathML"
or even
xmlns:html="http://www.w3.org/TR/xhtml1/strict"
So, throughout this treatment, we will assume that our opening <svg> tag looks like this:
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
We have seen how to fire a simple JavaScript command ("alert()") from the event caused by the loading of an SVG Document.
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
onload="alert('message')">
<ellipse cx="75" cy="75" rx="40" ry="30" fill="blue"/>
</svg>
Unfortunately, we also saw that Internet Explorer differs from the other browsers in its definition of "onload," since in the above code, IE fires the alert before the ellipse is drawn, while in FF and Opera the ellipse is visible while the alert box is displayed. In Internet Explorer, and at times in the other browsers, the load is viewed as having taken place before any actual rendering has taken place; other times it is assumed to take place afterwards. More on this topic will be seen later, but let us turn now to launching small programs from other kinds of events, like mouse clicks.
In the following example, a SMIL animation is triggered by a mouse click, but in turn, the beginning or end of the animation triggers a JavaScript function:
<ellipse cx="150" cy="75" rx="10" ry="40" fill="blue">
<animate id="A" attributeName="rx" begin="click"
end="A.begin+4"
onbegin="alert('started')" onend="alert('stopped')"
dur="4s" values="10;110;10" repeatCount="indefinite"/>
</ellipse>
We might be tempted to use the beginning of a small SMIL animation (of infinitesimal duration) as an alternative to an onload statement, were it not for the fact that certain browsers (most notably Chrome and Firefox) have not yet implemented SMIL.
At any rate, the above example shows that we are indeed able to launch a small JavaScript program based on a mouseclick.
It is worth pointing out that "onactivate" is considered slightly more general than "onclick" since the former can refer to activation caused by viewers that do not have a mouse connected, but instead have other input devices.
The simpler code:
<ellipse cx="150" cy="75" rx="10" ry="40" fill="blue" onactivate="alert('ellipse') />
also results in launching of an "alert()" based on a mouseclick, without the intervening animation. The event onactivate, however, appears not to be widely deployed, yet, across browsers, so the reader is encouraged to test it in the contexts where it is expected to be used before deploying.
Those readers familiar with JavaScript in the context of HTML are probably aware that it is common to put longer programs (than a simple "alert()") inside a <script> tag. That is where we turn our attention next.
We will typically place our programs (scripts) inside a <script> tag:
<script>alert('hello')</script>
For this to work, in the general case, however, a CDATA section is needed to keep our scripts from being parsed as XML. Although the CDATA looks like a bit of an abomination, to the eye that is familiar with reading HTML, its ancestry can be traced back to a common progenitor: Standard Generalized Markup Language or SGML.
In parsing an XML document, the beginnings of tags are signaled by the occurrence of the less than sign "<". Programming languages, such as those in the C family (like JavaScript, Java, C++, and Perl) tend to use less-than signs frequently, either in conditional statements or as characters in strings. This, however, can wreak havoc with a naïve parser that attempts to read an XML document with scripting in it. Hence, we use a CDATA section to hide the <script> from the interpreter.39We do this by placing
<![CDATA[
after our script tag, but before any JavaScript statements, and placing the string
]]>
after all JavaScript but before the </script> tag. Following is a simple example:
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<script>
<![CDATA[
function Here(){
alert("hello")
}
]]>
</script>
<text id="Text" x="170" y="150" font-size="24"
fill="black">Click</text>
<rect id="Rect" onclick="Here()" x="155" y="125" height="30" width="80"
stroke="black" stroke-width="2" fill="green" opacity=".5"/>
</svg>
In the above example, a click on the rectangle results in activation of the function Here() and thence an alert() box is displayed.
It so turns out that (at least in FF, IE and Opera), the use of CDATA in this particular example is not strictly required. That is, a shortened version of the <script>, without CDATA will work just fine in IE, FF and Opera.
<script> |
However, in the following example, browsers will not respond the way we might hope, since the less than sign in the code disrupts the XML parsing of the SVG.
<script> |
Internet Explorer responds with an "unterminated string constant" message; Firefox explains that there is an "XML Parsing Error: mismatched tag. Expected: </html>"; while Opera reveals a "mismatched end tag" affiliated with the <HTML> tag. Placing the CDATA section around the contents of the text, succeeds in hiding the JavaScript from the parser and does so successfully in all five browsers. The alert() properly displays the string "<html>".
<script><![CDATA[ |
Hereafter, we will simply assume that we always place a CDATA section around the material inside a <script> tag.
Having figured out how to run JavaScript programs, it now makes sense to see if we can use JavaScript to somehow change our SVG document. In particular, we might be interested in changing a document in ways beyond what SMIL allows. First, let's use JavaScript to find known objects within an SVG document and then answer questions about the properties of those objects.40
If the reader is familiar with JavaScript in the HTML environment, she is probably aware that one often uses the object named document to refer to the entire HTML document, namely a container of the elements associated with a web page. Likewise in SVG, we may refer to document as the entity containing all the nodes of the SVG (including the root node itself).
Therefore, the following page results in an alert displaying the string "R" in all five of the major browsers:
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<script><![CDATA[
function Here(){
alert(document.getElementById("R").id)
}
]]>
</script>
<text x="170" y="150" font-size="24" fill="black">Click
<rect id="R" onclick="Here()" x="155" y="125" height="30" width="80"
stroke="black" stroke-width="2" fill="green" opacity=".5"/>
</svg>
What we have done in the above code is to first use document to find the entire SVG document, then, within that, we look for any element that has an id of "R" by using
document.getElementById("R")
We then display the id property of that node:
document.getElementById("R").id
which, not surprisingly, ends up being "R".
Of all the properties associated with a node, we may note that most attributes cannot be referenced in this manner. For example, the expression
document.getElementById("R").height
is meaningless. Instead, we use
document.getElementById("R").getAttribute("height")41
as the way to retrieve ordinary attributes associated with an SVG object. Specifically, the command
alert(document.getElementById("R").getAttribute("height"))
produces, as output, the string "30".
An alternative approach is worth mentioning:
document.getElementById("R").height.baseVal.value
As discussed at SVG Document Object Model, the SVG DOM methods allow alternative ways, beyond what is typically available in generic XML for interrogating DOM. Given the nature of differences between the rendered page and mark-up, in part, due to animation, these methods are frequently recommended. As Erik Dahlstrom (a co-chair of the SVG Working Group of the W3C) has explained the difference:
The difference is that getAttribute returns a DOMString, and height.baseVal.value returns a float. Now, the conversion between string and float may not be such an issue in most cases, but if you do it often then those extra float <-> string conversions can become costly.
Before we look further into attributes of nodes, let us look a bit further into some of the other methods and properties associated directly with a node.
| Methods and properties associated with an SVG node | |
| The SVG Document | |
|---|---|
|
| Fill_in_the_Blank | value |
|---|---|
| document.getElementById("R").id | R |
| document.getElementById("R").nodeName | rect |
| document.getElementById("R").parentNode.nodeName | svg |
| document.nodeName | #document |
| document.getElementById("R").previousSibling.nodeName | #text |
| document.getElementById("R").attributes.length | 10 |
| document.getElementById("R").attributes.item(0).nodeNam e | id |
| document.getElementById("R").attributes.item(0).node Value | R |
|
document.getElementById("R").attributes.item( 1).nodeName |
onclick |
|
document.getElementById("R").attributes.item( 1).nodeValue |
Here() |
Most of these are discussed later in this chapter. Most are included here just to illustrate how once we locate an SVG object (a node), we can not only find out about it, but we can also use it as a starting point for exploring the SVG hierarchy of nodes, both nearby and far away.
For now, we should point out that
document.getElementById("R").nodeName
(which in this case is "rect" since that's what "R" is) provides a useful way of confirming that a node we have found indeed is a node of the variety or type that we suspect it is. This can prove to be an important capability as documents get large and as nodes and node naming become complex.
Once we have found a node by its "id" as in the above examples, we may also interrogate any particular attribute of that node using the "getAttribute" method associated with SVG nodes. This can be useful if we have objects whose attributes change over time, since we may not have independently stored that information in a data structure (though for various reasons, it is often better to, as discussed subsequently.) Also if we inherit SVG objects as parts of other documents, then we may have need to parse those documents, in which case getAttribute() may prove most handy.
All five major browsers, as of this writing, are each successful in interpreting, without problem, the DOM1 command:
node.getAttribute(attributeName)
However, the reader should be aware that as web documents start to, in coming years, to contain potentially multiple XML markup languages, the notation:
node.getAttributeNS(null, attributeName)
may become necessary. It works currently in most browsers, so in fact, it is generally recommended that we use this bulkier format. Some have suggested the standards could specify that in an SVG document, for example, namespace declarations could default to contextually appropriate settings, but this suggestion seems not to have met with widespread support in the standards community. (As of Spring 2009, discussions within the HTML5 working group of the W3C, have been discussing possibilities of making the SVG grammar, possibly a bit less formal.)
For example, if we have a rectangle defined by
<rect id="R" onclick="Here()" x="155" y="125"
height="30" width="80"
stroke="black" stroke-width="2" fill="green"
opacity=".5"/>
then document.getElementById("R").getAttributeNS(null, "fill") would return the value "green."
| Attributes of an SVG node | |
| The SVG Document | |
|---|---|
|
| Fill_in_the_Blank | value |
|---|---|
| T.nodeName | text |
| T.getAttributeNS(null,'x') | 40 |
| T.getAttributeNS(null,'font-size') | 24 |
| R.nodeName | rect |
| R.getAttributeNS(null,'x') | 25 |
| R.getAttributeNS(null,'height') | 30 |
| R.getAttributeNS(null,'fill') | green |
In short, O.getAttributeNS() is used to find the value of any particular named attribute associated with the object O. If one is interested in exploring all attributes of a node, in circumstances that we may not know what to expect a priori, see the section on "XML and the SVG DOM" later in this chapter. O.getAttributeNS (as well as O.getAttribute) assumes that we already know what attribute(s) we are looking for.
In order to make SVG documents dynamic, we must provide some ways for the documents to change in response to events (either user-generated events like mouse clicks, browser events like images finishing the download process or AJAX-like HTTP events, or time-based events, like the passage of 20 seconds). Some of these events can be captured using SMIL and many attributes of objects (including whether or not they are visible) can be manipulated with SMIL. But certain kinds of activities, conditions, and calculations require a full-fledged programming environment under the hood, and hence, JavaScript has the ability to change attributes of any object drawn in the SVG canvas.
If O is a variable referring to an SVG object (and to ensure that this method will be XML namespace compliant) we will typically use
O.setAttributeNS(null, attributename, value)
to accomplish the same thing that, although deprecated42, O.setAttribute(attribute, value) also accomplishes in all three browsers.
We may use setAttribute to reset any attribute (that is an attributeName=attributeValue pair) appearing within an SVG tag. Consider the following examples:
In the following example, clicking on an ellipse changes its color from grey to black.
| resetting the "fill" of an <ellipse> | |
|
|
| After page loads | After ellipse is clicked |
|---|---|
|
|
The ellipse, having id="C" has an "onclick" event handler. Clicking the ellipse activates the function changeFill(). First that function finds the ellipse by referencing its id, then it modifies its "fill" attribute to the new value "black."
In the example below, the entire <svg> tag is made to call a function whenever the mouse moves. The function then repositions one of the two ellipses by resetting its "cx" and "cy" attributes.
| Moving an ellipse by resetting its center point. | |
|
|
| After page loads | As mouse moves |
|---|---|
|
|
When the page loads, the function startup() is run, assigning the variable C to the grey ellipse. The <svg> element itself (also known as the documentElement) is armed to listen for mouse movement. Specifically, whenever the mouse moves, the function moveIt() is invoked. The moveIt() function receives as a parameter, the event itself (namely the mousemove event), including the coordinates of the mouse event. Those coordinates, clientX and clientY, are then used to set the appropriate attributes of the ellipse being "dragged".
In this example we use three functions:
startMove() prepares the object to be moved;
moveIt() actually relocates the ellipse;
drop() releases the object.
| Dragging and dropping an ellipse. |
|
The ellipse (having id="C") has an onmousedown to initiate movement by activating startMove(). The onmouseup event calls drop() which terminates movement. The function startMove() arms the entire SVG document space (the documentElement or document.firstChild) to listen to mouse movement. It does this by assigning the onmousemove attribute to the function moveIt(evt) :
document.documentElement.setAttribute("onmousemove","moveIt(evt)")
moveIt() in turn, resets the center coordinates of the ellipse, cx and cy, based on the mouse coordinates of evt, the triggering event.
Thus upon clicking the ellipse, the entire canvas will listen to mouse movement. The listening remains in effect until withdrawn by a mouseup event occurring on the ellipse.
document.documentElement.setAttributeNS(null, "onmousemove",null)
A few things should be noted here. First, we assign the onmousemove listener to the entire document space43. We could assign it, instead, only to the ellipse and it would still drag and drop. The problem though would be that if the mouse movement would happen to, as it often will, get ahead of the screen updating, then the ellipse will no longer receive mouse events and will be dropped at its present location. By arming the entire space to listen to mouse movement, we avoid that possibility, even if the mouse gets ahead of the ellipse. Second, the document itself contains a white rectangle which spans the entire browser window. This is useful since it allows the mouse activity to be captured even when the mouse moves outside the space defined when the document loads. Without this, we might again, "lose" the ellipse if the mouse is moved too rapidly. Third, the withdrawal of the onmousemove listening is done by a mouseup on the ellipse. It might be better to have this listener withdrawn by a mouseup anywhere in the documentElement. The code would be a bit more complex to beginning eyes, and it is relatively unlikely that someone will release the mouse while it is still moving rapidly (the circumstance under which the mouse gets ahead of the thing it is dragging).
As one last example of the use of setAttribute to change things, following is an example in which clicking a button (a group with a <rect> and a <text>) modifies the stop-color of each of three <stop> tags in a <linearGradient>.
| Resetting the stop-colors within the <stop> of a gradient. | |
|
|
|
|
| When the page loads | After clicking on "Random Gradient" |
|---|---|
The loop inside the function modify() finds all three <stop> tags, and modifies their "stop-color" attribute, replacing the value with a color randomly constructed by the function color(). The color() function merely constructs a string of the form "#xxx" where the three values of "x" represent hexadecimal digits in the range 0 to F. This slightly more complex example is given to illustrate how broadly setAttributeNS() might be applied to our SVG content.
If we attempt to change the "xlink:href" attribute of an <image> tag, we may be a bit surprised. If "I" refers to an <image> tag then the expression
I.setAttributeNS(null,"xlink:href","newone.jpg")
does not work. However, the simpler syntax,
I.setAttribute("xlink:href","newone.jpg")
does not work in Opera or Firefox, though it does in IE/ASV.
To make it work properly across browsers, we must declare the namespace associated with the xlink module of SVG, namely "http://www.w3.org/1999/xlink". That is, the statement should look like:
I.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href","newone.jpg")
To handle this in the general case, we frequently will establish a global JavaScript variable such as
var xlinkns = "http://www.w3.org/1999/xlink";
at the beginning of our <script> tag to avoid the typing of this lengthy string.
While on the topic, another approach should be mentioned: using the SVGURIReference interface. That allows us to access the xlink:href through SVG 1.1 DOM by imageElement.href.baseVal
In summary:
| Modifying the xlink:href of an <image> tag | |
| wrong | I.setAttributeNS(null,"xlink:href","newone.jpg") |
| wrong | I.setAttribute("xlink:href","newone.jpg") |
| right |
var xlinkns = "http://www.w3.org/1999/xlink"; |
| alternative |
I.href.baseVal="newone.jpg" |
In the preceding examples, we used the event object (evt) to determine the current mouse coordinates. It may also be used to determine the target of an event: for example, to find which object has been clicked upon.
In the examples below, we contrast two methods of changing attributes of an object that has been activated. In the first approach, we pass a string identifying the object to the functions. That identifier is then used to find the object within the DOM (using getElementById) and thence to modify certain of its attributes. The alternative approach is to examine the event object (in this case, either a mouseover or a mouseout) and to determine what object generated the event — namely to find the target of the event. This approach is a bit tidier, code-wise (having slightly fewer keystrokes). It is likely also a bit more efficient in terms of implementation since no need to re-enter the DOM is presented; the object has already been referenced by the event. In applications where speed of processing is a consideration, the latter is likely to be preferable.
| Using mouseover and mouseout effects to signal that an object has received focus. | |
| Identifying active object by passing its id | Using evt.target to get active element |
|---|---|
|
|
By passing the event object as a parameter to the receiving function we might also gain a tiny bit more parsimony (and modularity as well) by examining the type of event which triggered the function. That is, we might replace the two functions:
|
with one function that is equivalent in the context of this rollover effect:
|
When the object being activated is simple (like an ellipse or rectangle), evt.target works quite well to prepare it for subsequent manipulation. Now, let us turn to the more complicated situation of the group, <g> containing two or more simple objects inside it.
In the following page, a <rect> and a <text> are both nestled inside a <g>. A click on either of the two simple elements activates a function which identifies the nodeName (either "rect" or "text") of the object clicked upon.
|
Displaying the nodeName of the element clicked upon. |
|
One thing about evt.target that is perhaps worth mentioning is that if we establish an event handler for onload in the <svg> tag itself and ask for evt.target.nodeName, then the target will, in fact, be seen to be the <svg> tag. This is as we would expect, but some early versions of SVG players seem to have used this as a method for finding the object known as document. That is, document and evt.target.ownerDocument refer to the same thing, when the event is fired from the <svg>.
In our discussion of the <g> tag in Chapter 1, we saw that frequently we use a group as a place to consolidate attributes common to several of its member elements. We saw the use of <g transform="translate(100,0)">, for example, to reposition an entire group. Likewise, we may assign an event handler to the entire group as in <g onclick="doIt()">. This generally works well, unless we try to identify the group itself that was clicked on. That is because even though an event handler is assigned to the group, the target of the event ends up being the specific element within the group that actually received the event. Specifically, in the following example, it is either the <rect> or the <text> which is identified as evt.target, despite the event handler belonging to the <g> element.
| evt.target refers to a child of a group |
|
|
Depending on where in the group we click, we find either the <rect> or the <text> but not the <g>. |
This can create problems, if, for example, we wished to allow the dragging and dropping of various buttons labeled with text around the screen. One way to solve this problem would be to use evt.target.parentNode to find the parent (namely the <g> tag) of whichever node receives the event. This does not generalize well, however, to more complex groups (for example where groups might be found inside groups).
The answer to the above issue lies with evt.currentTarget. Its purpose, as illustrated below, is to find the superordinate object containing evt.target, specifically the group or other container to which the event listener has been assigned.
|
This succeeds in displaying "MyGroup" whenever either the "rect" or the "text" within the group is clicked upon. That this also works for <use> can be seen in the following example:
|
|
In the world of dynamic web pages, we frequently use text messages as a way of communicating with our visitors. In the HTML environment, scripts as these:
| Some common HTML approaches to changing text | ||
|
|
|
are commonly used to customize messages in HTML documents so as to respond to the activities of our web visitors.
Text tags in SVG are a bit like <div> tags in HTML. The material between the <text> and the </text> is a string of characters. Unlike <div> tags, however the string inside a <text> usually does not contain additional markup, but typically consists of plain ASCII (or Unicode) characters. That content may not be addressed in SVG by using innerHTML as is frequently done in HTML, since in SVG there is no innerHTML content associated with anything.44
Instead, we must use DOM techniques to find and modify material within a <text> tag.
Consider a somewhat generic SVG text object:
<text id="T" x="50" y="63" font-size="18pt" fill="black">
Here is a simple message
</text>
Here the object T=document.getElementById("T") is the text tag. That is, T.nodeName is "text" and T.id is "T".
The string "Here is a simple message" is, in fact, not an attribute of T, but a child of T. That is: T.firstChild.nodeName is "#text"
More to the point, T.firstChild.nodeValue is "Here is a simple message"
Hence, to change the nodeValue appearing within a <text> we use an approach such as illustrated below.
| Changing the message inside a <text> |
|
It should be noted that in addition to nodeValue as a way of setting the value of text node (T.firstChild.nodeValue=string), we also have data and textContent (T.firstChild.data=string or T.textContent.data=string). The latter of these destroys any sub-elements that may be under the element T.
In the preceding sections, we considered several aspects of interactivity including techniques for interrogating and modifying SVG elements. To make an SVG document, truly "application-like," we will wish to allow not just the modification of existing elements, but the creation of new ones.
This involves essentially three steps:
Fortunately, the most tedious of these steps, the second step, has already been discussed, so in this section we may concentrate on creation of new elements and inserting those into the DOM.
Creation of new elements is typically done with either a createElementNS statement (or the short lived, and now largely obsolete, createElement). We will also consider briefly the createTextNode as well as the cloneNode method.
After an element has been created it is then (in the typical case) added to our SVG document using the appendChild method.
Let us begin with the canonical simple case. You may recall from the first chapter, we found a candidate simplest case for an SVG document as shown here:
|
Here is the apparatus allowing a function to create just such a circle in response to the loading of the SVG document.
|
The onload event activates the function "create()." That function in turn, does three things. First it creates a new <circle> using the createElementNS method. Note that we must use an XML namespace definition in this method, so the variable xmlns has been declared globally. We use the temporary variable "C" so that we may refer to this object in subsequent statements. Secondly, we define the only truly essential attribute of the <circle>: its radius, using setAttributeNS. Finally, we append the new element, with its one attribute to the document space. For this we use the appendChild method associated with any XML node. Actually we wish to append it to the root of the <svg> itself, which in the DOM is equivalent to document.documentElement. In future examples, I will typically use a global variable "Root" to refer to document.documentElement.
The above example is rather contrived since we would clearly not use a script to create what could so much more easily be created with markup. But it sets the stage for more complex things. Typically, consistent with our concept of "dynamic content," we might like our new content to arrive at new locations and to have new properties.
We now demonstrate the use of a function to create a new rectangle, assign it a random color, position it just below the preceding <rect> and then to embed it in the document.
| New <rect>'s with new positions and colors | |
| Code | Annotations |
|---|---|
|
--An SVG document with namespace |
|
--This function (called by a |
|
--This function returns a string of the form "#XXX" where each X is a hexadecimal value. |
|
--We terminate the <script> and |
| Illustration | |
|
|
One might be tempted to observe that the use of six calls to setAttributeNS() to populate the element R with properties seems a bit cumbersome, particularly in relation to the relatively concise markup associated with a <rect> tag. We will shortly demonstrate the use of the cloneNode method which in many cases may simplify our coding considerably, by allowing new objects to inherit properties of objects that already exist. One could also use CSS (Cascading Style Sheet) styles to define classes of objects that will inherit certain properties from a style. This is discussed briefly in this book, but is more useful for setting properties like opacity and stroke that are really styling attributes, rather than things like x and y. Cloning turns out to be a bit more robust.
Text is slightly different from other things in SVG. A <text> tag has a child consisting of a string which appears without a tag of its own: <text>string</text>. A <script> tag is similar, but the vast majority of SVG appears through attributes of tags, rather than through relatively unstructured material which dangles inside them. So while we may use the createElementNS method to create a <text> node, we must turn to a different method to put a string inside the node. For this, we use the createTextNodeNS method, in conjunction with createElementNS to create the text node, create a node containing our string and append our string (in this case the string "4321" of numeric digits) as a child of the text node:
T=document.createElementNS(xmlns,"text") |
We illustrate with a short example, in which new text nodes are created and populated with single integers (uninteresting, as strings go, but illustrative). I believe the reader should be able to unpack the following code without detailed annotation based on the annotation of the preceding example "New <rect>'s with new positions and colors" in the section on createElementNS.
| New <text>'s with messages in each | illustration |
|
|
Here each new <text> node is given a child ("Msg", a string) consisting of an integer representing the vertical positioning of the node.
The cloneNode method allows us to duplicate or clone existing nodes such that the newly created object contains all of the properties (except id) associated with the original (including or not, as we wish, the children inside it).
The use of cloneNode is typically as follows:
var R=document.getElementById("R")
|
That is, an existing object within the DOM is located. Once found it is then cloned. The resultant object is then appended to the DOM. Note that the cloneNode method receives a single parameter: a Boolean value representing whether or not we wish to duplicate all descendants of the node to be cloned. If the object has no children or if we do not wish to invite the children to the party, then we simply specify "false" as the value of that parameter.
Here is an example of its use to create new rectangles. It accomplishes the same as the more lengthy code presented " in the section on createElementNS in the example "New <rect>'s with new positions and colors."
| Using cloneNode to make new <rect>'s with new positions and colors |
|
In the above example, we see that each of the attributes, "x", "y", "width", "height", "fill" and "stroke" are replicated in the new object NewR. Then instead of issuing statements to set all six of these attributes, we have changed just two of them, the "y" and the "fill". It tidies up the code a good deal, though we would might suspect some minimal degradation of runtime performance, due to retrieving a node from the document before creating it.
In the case of cloning a composite object (with children) like a group, we use cloneNode(true) to duplicate the subordinate node structure inside the object. In the following example, we are interested in duplicating a <g> containing both a <rect> and a <text>, but in putting each new object below the preceding on the screen. We have seen, earlier in working with groups, that it is sometimes easier to move groups around using a transform attribute, rather than manipulating attributes of all the children separately. Accordingly, this is the approach we use here:
| Using cloneNode to make new <g>'s at new locations |
|
The value of the transform attribute is actually a string looking like "translate(0,90)." In order to incorporate new values of the vertical displacement, y, into the string, the literal and variable parts are broken out separately and concatenated together.
If one actually runs the above code, not only is the group created, with its subordinate elements, but each new group also contains an onclick event-handler and hence is itself enabled to activate the clone() function.
In addition to these techniques which can do most of what one might need to do with an SVG document, two which are part of the SVG 1.1 specification (and hence standard in most browsers) are worth mentioning: replaceChild() and insertBefore(). Both can be useful in changing the stacking order of elements within the SVG DOM.
A simple use of the replaceChild() method may be seen here:
F=document.createElementNS(xmlns,"circle") |
In this example, an object named "C" is replaced by a newly created circle named "F".
Here is an example of the usage of insertBefore(). Instead of appending new elements to the DOM we clone an existing object and then insert new content before it in the DOM. The result will be that the first object (CL) will remain at the top of the DOM tree as the last child.
var CL=document.getElementById("C")
|
Such proves useful when we would like some element like a menu to remain above all dynamically added content.
In our previous discussion of dragging and dropping45, we assigned and withdrew event handlers from the Root document to move a single element around. Armed, now, with evt.target we may easily generalize to the dragging and dropping of multiple independent objects.
| Dragging and dropping any of three objects |
|
Note from the above, that we simply use the event object to determine the source of the event (namely which of three circles was clicked on) to allow it to be the one we reposition as a result of mousemove events anywhere in the <svg>,
We might additionally, wish to be able to create new objects which themselves are draggable and droppable. Herewith is a demonstration of one way46to do that. It should be remarked that some attempt to streamline the code by minimizing keystrokes has been made. For example, instead of typing the lengthy line
document.documentElement.setAttributeNS(null,"onmousemove","move(evt)")
we use, here, instead, the equivalent47
var R=document.documentElement |
This allows for a side-by-side presentation of both code and annotation,
| Using mouse clicks to create circles than can be dragged and dropped. | |
| Code | Annotation |
|---|---|
|
The usual beginnings.
|
|
|
|
|
|
|
|
|
|
|
|
|
Perhaps the two trickiest parts of the above are in the startMove() function. In startMove() we cancel the event bubbling associated with the mousedown event. When we click on a <circle> both it and the SVG Root would ordinarily receive the mousedown event, unless we cancel its continued propagation which would ultimately reach Root. This could done with a statement equivalent to
SVGRoot.setAttributeNS(null,"onmousedown",null)
This would however, withdraw the listener from the Root, and after our dragging of the object is done (onmouseup) we would then have to reassign the listener for the mousedown event to the Root. Both of these would entail modifications of the DOM.
Another way of handling this is as we have done it here: by stopping event propagation with the .stopPropagation() method of the event.
e.stopPropagation()
simply prevents a given event, "e", from bubbling or trickling (depending on the browser's event model) beyond the first target (with objects further from the root being those which are activate first). This may be a bit advanced for the casual reader, but if one tries the startMove() function without the call to stopPropagation(), he or she will be able to see exactly what we are talking about.
The function startMove() also re-appends the selected element to the Root. This succeeds in making the selected object the element last created, hence making it appear on top of the other elements. It is not strictly needed for functionality, but may add to one's sense of aesthetics in user-interface, since it may be a bit disconcerting to see the object you are dragging disappear behind other content.
Similar effects with changing the stacking order of objects can be accomplished using the replaceChild() and insertBefore() methods as mentioned earlier.
The removeChild method is what is used to delete content from an SVG document. It is applied as a method of a given node and accepts a node as its single parameter.
For example, let G=document.getElementById("G") be a node representing a group (<g>)somewhere in an SVG tree.
<g id="G"> |
Then to delete the rectangle "R" from the document, we may issue the statements:
G=document.getElementById("G")
|
If we wish to remove an object, but are not certain what node it is a child of, then we may use the parentNode method discussed in the next section and then apply the removeChild method to the parentNode of the node in question.
| Removing children of an unknown parent |
|
To remove all child elements from a particular element, it is important to realize that the obvious approach of cycling through all children of that element may not work in quite the way that we might expect. Examples will be included in the forthcoming section on parentNode.
We have already, in passing, made reference to the SVG DOM in many places. SVG content is placed into a tree-like structure, the root node of which is the <svg> tag. While we may retrieve elements based on their id, or append them as children to the root element, we may also retrieve all elements of the tree (or any subtree) or the attributes of these elements by various DOM methods that SVG shares with the broader XML specification. Though the XML specifications mention numerous methods applicable in certain circumstances48, and which may save steps in our programming, the following six methods provide sufficient flexibility that one can accomplish most of what one seeks to by an understanding of what they are and when to use them.
First we should point out that a node refers to any object (i.e., anything that has a tag, and hence a nodeName) in the SVG DOM. For example, the <svg> tag itself is generally known as the node serving as the root of the tree. It is the container in which all other tags are placed.
As we have pointed out document.documentElement is generally49, the same as <svg> the root SVG node. We may explore the DOM tree starting with it.
If, when the SVG document loads, we call a script which displays
document.documentElement.nodeName
we will see that it is "svg". In contrast,
document.nodeName
is simply "#document". Items which are merely strings, which is what the entire document ultimately is, are assigned these pseudo-nodeNames, beginning with "#" to indicate that they are simply strings.
We'll begin our discussion of DOM methods relative to this root node:
Root=document.documentElement.
The firstChild method receives one parameter, a node, and returns an object as output. It represents the first item (tagged node or otherwise) found inside a node.
document.firstChild finds the first tag in the document itself: namely the <svg> tag. That is, document.documentElement and document.firstChild refer to exactly the same thing. Again, document.firstChild.nodeName is "svg". We may then repeat this method on the retrieved object:
document.firstChild.firstChild retrieves the first object inside the <svg>
For sake of understanding, let us work with the following rather simple document. Note that all the tags abut, with no space or text between them, making matters a bit simpler and more convenient.
| A document with two children of Root |
|
If the function startup() contains the following code:
C1=document.firstChild |
then the resulting string displayed is "svgscript#cdata-section". The reasons are fairly straightforward: the first tag in the document, as we have noted, is the <SVG> tag. Its first child is the <script> tag. The first (and only) child of the <script> tag is the <CDATA> section (handled somewhat specially within XML languages, which explains its somewhat mutant nodeName). We could not, in this case, take the analysis any further since the <CDATA> rather like a <text> has no children, per se, but rather has an untagged string inside it.50While the cdata-section could be examined through its nodeValue, it is important to realize that this only works because its child is a string rather than a node. For example,
document.getElementById("G").nodeValue
produces nothing useful (its value is null).
Suppose we wished to explore, from a little deeper in the tree, say, starting with the <g> tag:
| <g id="G"><rect x="0" y="0" width="50" height="50" fill="green"/><rect x="9" y="9" width="50" height="50" fill="red"/></g> |
Not surprisingly, we find that a <rect> is the first child of the group. Since that <rect> is, itself, childless, an attempt to retrieve G.firstChild.firstChild produces an error.
The above discussion perhaps motivates the question: how to find the second child of a node? We might imagine having a large but countable set of methods, each associated with an ordinal number: secondChild, thirdChild, etc. Alas, we never run out of ordinal numbers, and a language with a countable but infinite set of commands would be unwieldy to implement on computers built with molecules, so we take a hint from set theory and borrow a variant of the successor function: the nextSibling method. Before turning to nextSibling, however, the reader should be aware of one more aspect of the firstChild method which can, at times, be perplexing.
An important thing to note about firstChild, is that it may pick up different object than what we might expect. Consider the difference between these two document fragments:
| Two almost equivalent (except for white space) SVG groups |
|
1.
|
|
2.
|
Let
G=document.getElementById("G")
H=document.getElementById("H")
then in Internet Explorer, G.firstChild is the green rectangle, but H.firstChild is the carriage return/tab sequence used to make the code presentable for human eyes.
In HTML, different browsers resolve the issue of white space differently, but my experiments with different browsers in SVG suggest that all three of the major browsers all view the text between tags as a separate child.
We might use nodeName in conjunction with nextSibling (as discussed next) to be certain that we are pointing to a real tag, rather than white space51. Alternatively, we might use the getElementsByTagName (discussed a bit later) method to retrieve only those elements of a particular type (if we know what we are looking for).
Like the firstChild method, nextSibling accepts a node as a parameter and returns a node as output. It returns the next child of the parent of the current node. For example, if the first child of a parent is passed to nextSibling, then what is returned will be the second child of the parent. Consider our example of a simple group:
<g id="G"><rect x="0" y="0" width="50" height="50" fill="green"/><rect
x="9" y="9" width="50" height="50" fill="red"/></g>
In this case, if:
G=document.getElementById("G")
then G.firstChild will be the green rectangle and G.firstChild.nextSibling will be the red rectangle.
As with firstChild, nextSibling sees the white space (like carriage returns and tabs) between tags as #text nodes, so we may end up with different results than we expect based on our typesetting preferences.
Often we may be uncertain what sort of a DOM an SVG document contains. This may be true either when we allow the user to create and remove content repeatedly, or when we embed or otherwise import external SVG content into a document or its script. If, for example, we hope to recursively expand all nodes of a given DOM, then being able to deal with all children of a given branch rather than one at a time can be most convenient. This is done with the childNodes method of a given node.
.childNodes receives an SVG node as its single parameter and returns a "node list," a collection (much like an array) of objects.
O.childNodes.length represents the number of children of the object O.
O.childNodes.item(0) represents the 1st child of the object O.
O.childNodes.item(i) represents the i-1st child of the object O.
Consider the following SVG document containing a small amount of markup.
| A document with five children of Root |
|
We may observe that if
Root=document.documentElement
(i.e.,Root=document.firstChild)
then Root.childNodes.length is 5 (Namely, <script>,<rect>,<text>,#text,and <g>).
The #text node is simply the white space between the <text> and the <g>.
Root.childNodes.item[0] is the same as the <script>,
Root.childNodes.item[1] is the same as the first <rect>,
Root.childNodes.item[2] is the same as the <text>,
Root.childNodes.item[3] is the same as the carriage return, and
Root.childNodes.item[4] is the same as the <g>.
Likewise, if
H=document.getElementById("H")
then
H.childNodes.item[0] is the same as the green <rect> and
H.childNodes.item[1] is the same as the red <rect>
If one were forced to get by without the use of childNodes, one could, using just firstChild and nextSibling, and continuing until O.nextSibling becomes null. The use of childNodes saves us from the need to interrogate nodes to see if they are null or not, and provides a handy way of referencing them without multiple entries in the DOM.
Under the section concerning parentNode we will see how to remove all childNodes of a given node.
Another method of objects which, though not strictly essential for our survival, is nevertheless helpful. Like childNodes, if O is a node, xmlns is our XML namespace, and tagname is a type of tag (like "rect" or "g") then
O.getElementsByTagNameNS(xmlns, tagname)
returns a nodelist, with the elements of this nodelist consisting of all descendents of the node O (not just its immediate children, but all nodes in its subtree) of a given nodeName. In the above example A document with five children of Root:
Root.getElementsByTagNameNS(xmlns, "rect")
returns a nodelist of three items representing the three <rect> tags in the entire document, even though only one of them is a direct child of Root. As with childNodes, the nodelist returned by getElementsByTagNameNS is manipulated not as an array but through its item list:
Root.getElementsByTagNameNS(xmlns, "rect").length is 3.
Root.getElementsByTagNameNS(xmlns, "rect").item(0) is the white <rect>,
Root.getElementsByTagNameNS(xmlns, "rect").item(1) is the green <rect>,
Root.getElementsByTagNameNS(xmlns, "rect").item(2) is the red <rect>.
For a more extensive demonstration of the use of getElementsByTagName, the following succeeds in allowing each tagged node in the document to, when clicked upon, to find and change the colors of all other nodes of the same type.
| Using getElementsByTag name to modify all elements of a kind. | |
| Markup and code | Explanation |
|---|---|
|
The standard beginnings. |
|
An array of colors a counter, and a status field. To each tagged node of the document we assign an onclick handler and send it the nodeName. |
|
Display the type of the node clicked. Find all nodes of the same kind and change their colors. |
|
A collection of nodes: <text>, <rect>,<ellipse>, and <path> |
As a special note to the more advanced programmer: the list returned by getElementsByTagName represents an active list of nodes present in the SVG document. As such, it may be better, from the perspective of the performance of the program, to maintain a separate data structure containing a virtual list, so that large-scale manipulations of a DOM that contains a very large number of nodes may be done more effectively. Also, in this case of the above example, we could have used CSS to change attributes of classes so that a collection of nodes could be changed in one place instead of changing attributes of every element.
On occasion we might need to retrieve the SVG element directly superordinate to a given node. In the following example
<g id="G" transform="translate(100,100) "> |
the command
document.getElementById("T").parentNode
will retrieve the <g> with id="G".
If, for example, we wished to find all text nodes whose first child contain a certain substring and then to translate the groups containing those, so that those nodes are all collected together into a particular part of the screen, then parentNode can prove most convenient.
It is common in scripting SVG documents to wish to develop a script which removes all children of a given node. However, it is important to realize that the following, seemingly, natural approach will not work:
| Removing child nodes | |
| The SVG code common to both approaches | |
|---|---|
|
|
| How not to do it | A successful approach |
|
|
The key here is that the list childNodes is live, meaning that as soon as we've deleted a node from the list and attempt to re-enter the looping statement, the node list has changed, meaning that the number i will now have skipped beyond the next node. If the reader actually implements the illustrated code, he will find that the value of GChildren.length begins at 9 and through a succession of mouseclicks works its way down to 4, then to 2, and finally to 1 before removing all children of the group. Instead, the second process which works backward from the end of the list will properly succeed in finding
We have seen how to retrieve a particular attribute value of a given node when we know which attribute, Q, (e.g., Q="x" or Q="fill") we are interested in examining:
document.getElementById("T").getAttributeNS(Q)
But suppose we encounter a node for which (for whatever reason) we do not know what attributes may have been defined. attributes gives us the way to explore the attributes of a given node. If the variable N points to the node in question, then
N.attributes
returns a nodelist of attribute name and attribute value pairs. Those pairs may be interrogated through nodeName and nodeValue, respectively, as follows:
| Determining attributes of an SVG node | |
| The SVG Document | |
|---|---|
|
| Fill_in_the_Blank | value |
|---|---|
| R.attributes.length | 10 |
| R.attributes.item(0).nodeName | id |
| R.attributes.item(0).nodeValue | R |
| R.attributes.item(1).nodeName | onclick |
| R.attributes.item(1).nodeValue | Here() |
| R.attributes.item(2).nodeName | x |
| R.attributes.item(2).nodeValue | 155 |
In the above examples, we may instead examine all of the attribute (name, value) pairs similarly through the use of a simple loop.
As a part of code that serializes (or converts an SVG DOM back into a textual representation), I have frequently used code of the following sort to find and display all the attributes of a given node n:
function listAttributes(n){
|
Available to us in SVG, are a few delightful functions that allow us to interrogate properties of object that have been drawn in our canvas. While one might argue that if we have drawn them then we most certainly should know where they are52, not all programmers are as fastidious as others, and not all those who think they are fastidious are always as omniscient as they might wish themselves to believe.
These special functions allow us to determine the bounding box (or minimal enclosing rectangle) of a shape, determine the length of a path or the position of a point p% of the way along that path. We may determine the size of a string as rendered, and we may determine what transformation has finally been applied to an object after a series of transformations have been applied. All useful things these can be, even to the programmer who has never misplaced a sock.
The getBBox method of object drawn in SVG allows us to determine the coordinates of the smallest rectangle (with sides parallel to the screen) in which the object fits. Specifically, getBBox accepts, as input, any SVG graphics element or group and returns a rectangle containing four attributes (the x and y of its upper leftmost corner, its height and its width).
For certain elements like <rect> or <ellipse> the properties of the bounding rectangle are trivial and obvious. For more complex entities like Bézier curves and text objects (where knowing how wide all the characters and the inter-character kerning would be rather complex to calculate), the getBBox method can save the programmer extensive effort.
As a simple example consider the following tag:
<text id="T" x="20" y="20" font-family="garamond" font-size="18"> |
The getBBox method is used as follows:
var Box=document.getElementById("T").getBBox()
After the above has been defined then the properties of the object variable "Box" are as follows in three different browsers:
| Applying getBBox to a <text> in different browsers | |||
|---|---|---|---|
|
var Box=document.getElementById("T").getBBox() |
|||
|
<text id="T" x="20px" y="20px" font-family="garamond" font-size="18pt">Here is some text</text> |
|||
| Firefox 1.5 | IE/ASV | Opera 9 | |
| Box.x | 20 | 20 | 20 |
| Box.y | 4 | 4.64 | -1 |
| Box.width | 164 | 162.6 | 164 |
| Box.height | 17 | 15.76 | 27 |
The fact that getBBox returns different values for the same object in the different browsers is one very compelling reason for using it.
Here is another example, in which we may see the results of drawing a bounding box around a <text> object.
| Applying getBBox to a <text> |
<text x="0" y="100" font-size="60" fill="red">Doing text - Click</text>
|
| illustration |
|---|
|
|
It is worth noting that getBBox responds to original untransformed values of a drawn object. If an object has transformations applied (scale, rotate, or translate) then one must take those transforms into effect and apply them to the bounding box as well. See the example under getCTM later in this section.
A related function which should be used when viewports differ (e.g., when viewBox is used) isgetScreenBBox. The reader is referred to the example at Getting an SVG Element's Full Bounding Box as illustration.
Given a path (consisting of linear, elliptical, and/or Bézier segments) we might sometimes wish to be able to place markers of some kind at intervals along that path. The special methods of path elements, getTotalLength and getPointAtLength, allow us to do exactly this.
To understand how they work, let us start with a simple example, and follow it through algebraically.
<path id="P" d="M 200,100 350,50 400,100 z" fill="#edf"/>
The midpoint of the path will be the point halfway around the circuit starting at (200,100) passing through (350,50) and (400,100) and returning to the beginning. The three line segments have length, respectively of sqrt(1502+502), sqrt(502+502), and 200, for a total distance around the triangle of about 428.8 (=70.7+158.1+200). The halfway point, then will be at a distance, along the path, of 214.4 from the starting node, or about (214.4-70.7)/158.1 = 90.88% of the distance from (350,50) to (400,100). That is, the midpoint will be at about (389.8,89.8).
We may retrieve similar results (with far less conceptual work) by invoking the getTotalLength and getPointAtLength as in the following.
| Finding a point halfway along a path. |
<path id="P" d="M 200,100 350,50 400,100 z" fill="#edf"/>
|
|
|
getTotalLength is a method applied to a <path> (in this case the path named by the variable Path). It returns a floating point number equal to (or at least a good estimate of) the length of the path from beginning to end. In the case of the above triangle it is about 428.8.
.getPointAtLength(r) is, likewise, a method applied to a path, but it accepts a parameter between 0 and 1 which specifies the proportion of the distance from the beginning to the end of the path, at which we would like to find the coordinates of a point. What getPointAtLength returns is actually an SVG virtual point object. Mid, in the above code, has the properties Mid.x=389.8 and Mid.y=89.8. Because SVG Point objects also include a transformMatrix method in their class definition, the manipulation of a path by transform commands will not confuse the getPointAtLength method of a path.
If we consider the more general case of an arbitrary path (instead of the simpler case where all components are linear), these two methods can prove even handier. In the following case the path is composed of linear segments except for one portion (which happens to contain the midpoint) which is an elliptic arc.
| A path consisting of linear and nonlinear parts. |
<path d="M 200 100 350 50 400 100 A 100 30 0 0 1 400 150 z" fill="#fd9"/>
|
|
midpoint is at approx. (439, 115) perimeter of curve is approx. 543.1. |
|
The problem here is that computing the length of an elliptic arc (or even the "simpler" problem of the circumference of an ellipse) cannot be done in closed algebraic form since the integral resulting from putting the ellipse in parametric form can only be approximated through infinite series. Both functions getTotalLength and getPointAtLength use fast and reasonably accurate estimates for calculations involving both elliptic arcs and Bézier curves.
We conclude with the following example gives a general method for interrogating any path in an SVG document, and at the same time gives us some insight into the accuracy of the calculations involved.
| Finding midpoints of a selected path. |
|
| Illustration in which second path has been clicked. |
|---|
|
| This example can be seen at http://srufaculty.sru.edu/david.dailey/svg/getPointAtLength.svg |
We observe that the green curve should, in fact, be centered at (150,125) instead of the coordinates shown above that are off by a tiny fraction of a percent.
A variety of specialized methods exist for <text> objects. Rather than presenting examples of the use of each, we merely excerpt from the W3C's SVG 1.1 recommendation about these methods, and afterward show an example using a few of these to do some interesting work.
Returns the total number of characters to be rendered within the current element. Includes characters which are included via a 'tref' reference.
The total sum of all of the advance values from rendering all of the characters within this element, including the advance value on the glyphs (horizontal or vertical), the effect of properties 'kerning', 'letter-spacing' and 'word-spacing' and adjustments due to attributes dx and dy on 'tspan' elements. For non-rendering environments, the user agent shall make reasonable assumptions about glyph metrics.
The total sum of all of the advance values from rendering the specified substring of the characters, including the advance value on the glyphs (horizontal or vertical), the effect of properties 'kerning', 'letter-spacing' and 'word-spacing' and adjustments due to attributes dx and dy on 'tspan' elements. For non-rendering environments, the user agent shall make reasonable assumptions about glyph metrics.
Returns the current text position before rendering the character in the user coordinate system for rendering the glyph(s) that correspond to the specified character. The current text position has already taken into account the effects of any inter-character adjustments due to properties 'kerning', 'letter-spacing' and 'word-spacing' and adjustments due to attributes x, y, dx and dy. If multiple consecutive characters are rendered inseparably (e.g., as a single glyph or a sequence of glyphs), then each of the inseparable characters will return the start position for the first glyph.
Returns the current text position after rendering the character in the user coordinate system for rendering the glyph(s) that correspond to the specified character. This current text position does not take into account the effects of any inter-character adjustments to prepare for the next character, such as properties 'kerning', 'letter-spacing' and 'word-spacing' and adjustments due to attributes x, y, dx and dy. If multiple consecutive characters are rendered inseparably (e.g., as a single glyph or a sequence of glyphs), then each of the inseparable characters will return the end position for the last glyph.
Returns a tightest rectangle which defines the minimum and maximum X and Y values in the user coordinate system for rendering the glyph(s) that correspond to the specified character. The calculations assume that all glyphs occupy the full standard glyph cell for the font. If multiple consecutive characters are rendered inseparably (e.g., as a single glyph or a sequence of glyphs), then each of the inseparable characters will return the same extent.
Returns the rotation value relative to the current user coordinate system used to render the glyph(s) corresponding to the specified character. If multiple glyph(s) are used to render the given character and the glyphs each have different rotations (e.g., due to text-on-a-path), the user agent shall return an average value (e.g., the rotation angle at the midpoint along the path for all glyphs used to render this character). The rotation value represents the rotation that is supplemental to any rotation due to properties 'glyph-orientation-horizontal' and 'glyph-orientation-vertical'; thus, any glyph rotations due to these properties are not included into the returned rotation value. If multiple consecutive characters are rendered inseparably (e.g., as a single glyph or a sequence of glyphs), then each of the inseparable characters will return the same rotation value.
Returns the index of the character whose corresponding glyph cell bounding box contains the specified point. The calculations assume that all glyphs occupy the full standard glyph cell for the font. If no such character exists, a value of -1 is returned. If multiple such characters exist, the character within the element whose glyphs were rendered last (i.e., take into account any reordering such as for bidirectional text) is used. If multiple consecutive characters are rendered inseparably (e.g., as a single glyph or a sequence of glyphs), then the user agent shall allocate an equal percentage of the text advance amount to each of the contributing characters in determining which of the characters is chosen.
Causes the specified substring to be selected just as if the user selected the substring interactively.53
Many of these would appear to be useful in quite specialized circumstances, For example if we wished to do some graphical markup of text which was typeset on a curve, then getRotationOfChar, might come in handy. Following is an example of something which might prove useful in circumstances where we wished to interrogate user-events relative to text typeset on a page.
| Some text handling methods for interrogating user-selected substring. | |
| The code | explanation |
|---|---|
|
The standard beginnings. |
|
This function (activated by mousemove |
|
Three strings with onmousemove event handlers and one, "m", for displaying results. |
| Illustration | |
|
|
| This example can be seen at http://srufaculty.sru.edu/david.dailey/svg/textCursor.svg | |
It should be noted that the above example works fine in IE/ASV, Chrome, Safari, and Opera, but in Firefox 3.0.8, the characters are properly retrieved at the mouse position, but the text does not appear highlighted.
We have observed earlier in this book that transformations (scale, rotate, translate, skew) can be applied repeatedly to a given object. Once an object has been transformed by a collection of multiple effects, it can be difficult to determine quite where on (or off) the screen a drawn object has been moved. getCTM is a method of an object which returns the Current Transformation Matrix, a composite matrix representing the cumulative result of each of the simpler transformations. The subject is treated quite thoroughly within the SVG 1.1 specifications, and the reader may wish to find more on it there. Let it suffice for our purposes to say that each of the transform primitives (scale, rotate, translate, skew) can be viewed as the multiplication of a matrix by a collection of vectors, each representing the points making up an object. By combining several transformations, we are in effect concatenating matrix multiplications. The resultant matrix (representing these affine and rotational transformations) ends up being a three-by-three matrix with the last row being the identity row (0 0 1). Effectively, then each such composite transformation can be specified by the six remaining unconstrained cells of this three-by-three matrix. That is what is meant by the Current Transformation Matrix returned by the getCTM method of any drawn object.54
We observed earlier in this chapter, that the getBBox method of an object is not sensitive to, and hence will be misled by the transform attribute being applied to that object. We show how to overcome that problem, with the use of getCTM().
| Use of getCTM to reattach a bounding box to the element that built it. | |
|
A standard beginning. |
|
We add event handlers to all <text> tags. |
|
When the mouse passes over a <text>, this function draws the bounding box over the location where the object appears to be (prior to examining its transforms). |
|
When the <text> is actually clicked on, this function takes the six free parameters of the Current Transformation Matrix, builds a new string out of them, and applies the resulting string as a new matrix-transform to the bounding box. |
|
Three <text> objects (each assigned event handlers onload of the document) and a <rect> to serve as a bounding box. |
|
Illustration: bounding box before application of getCTM (above) bounding box after application of getCTM (below) |
|
|---|---|
|
|
|
|
| This example can be seen at http://srufaculty.sru.edu/david.dailey/svg/getCTM.svg | |
We can observe from the preceding example that by applying a given CTM to any object, it inherits the cumulative effects of whatever transforms had been applied to the original object.
It should probably be mentioned that some authors have reported that the .getCTM method can be unreliable in more complex situations. Some authors have addressed this with the development of custom methods that extend the utility of this particular function. (See http://svg.jibbering.com/svg/2006-12-08.html by way of discussion.)
In a number of instances we may wish to have scripting and SMIL animation combined in the same document. Sometimes, it may even prove fruitful to have both JavaScript and SMIL based animation in the same document. For example, if certain objects have relatively simple oscillating behavior while others respond dynamically to distances from edges or mouse actions, then we may want to intermingle our varieties of animation.
Certain methods exist for instigating JavaScript functions from SMIL events and from activating SMIL animations from JavaScript functions. We conclude this chapter with some explanation and illustration of two such methods that can handle most of our needs in this area. We may use onend (or onbegin) associated with the end of SMIL animation to trigger a JavaScript function. From JavaScript we may use Animation.beginElement() (or endElement()) to start a SMIL animation.
Within a SMIL animation the attribute onend="doIt()" will cause the JavaScript function doIt() to be performed as soon as the animation ends. A simple usage:
|
|
Here, the ellipse moves back and forth across the screen once horizontally. After completing its mission, the function is activated. The function determines what triggered it (the animation object), finds the parent of that object (the ellipse) and resets the fill pattern of the parent. This particular example, though illustrative of using SMIL to activate JavaScript, could, as seen in the chapter on SMIL, be performed without JavaScript using the <set> element of SMIL.
A small note about the use of onend is in order. If we attempt to build an animation dynamically using document.createElementNS(xmlns,"animate"), then attempts to create the onend attribute for that object seem to fail in IE/ASV. It appears to be a bug in the browser.
Two other things worth mentioning in this context: onrepeat can be used to with a counter to allow a function to be triggered after an animation has been repeated a certain number of times. This could be useful in allowing the animation to keep going, but to nevertheless spawn a new activity before it has actually ended.
Also, one may use script to synthesize events such as clicks on SVG elements, hence triggering associated animations. I'm hoping that a reader may provide a useful example of such a use.
The beginElement() function in SVG/JavaScript is a method applied to an animation object (<animate>, <animateTransform>, <animateMotion>, etc.). It can be used by a script to activate a SMIL animation. A simple usage is shown here.
|
|
Note that the begin attribute of the animate tag has a value of "indefinite." This is necessary to allow the script to be able to determine the time at which the animation begins.
Likewise if an animation tag A contains the attribute end="indefinite" then A.endElement() will succeed in terminating that animation.
Combining beginElement() together in a script with the use of the onend attribute of a SMIL animation can yield some worthwhile results. In the following, we allow objects (<g> tags) to move across the screen following SMIL animation. When the animation is complete (onend) we trigger a JavaScript function which rewrites the text inside the animated group (adding one to a counter inside each object). The JavaScript then restarts the animation, apparently seamlessly.
In the example below, we allow the moving objects to follow an oscillating Bézier curve, which is itself animated. The process of doing this animation with JavaScript rather than SMIL would be quite painful, hence strongly motivating this use of SMIL.
| Allowing uniquely numbered ovals to march across a Bézier curve. | |
|
The standard beginning with a shortcut name for document and a counter. |
|
We start the animation 'A' and wait 2 seconds to start the animation 'B'. |
|
Whenever either animation ends, we figure out which one it is. We then find the <text> tag inside its group and rewrite the text with the current value of the counter. We then restart that particular animation. |
|
A Bézier curve is defined as a path for the animation to follow with its <mpath>. The Bézier curve is itself animated so as to oscillate. |
|
Two groups are defined, each containing an ellipse, a text, and an animateMotion allowing the group to follow our oscillating path #P (as defined above). Whenever either animation reaches its end state, it triggers the rebuild function, which ultimately restarts the animation. The second group has its opacity assigned and removed to avoid its phantom appearance in the upper left of the screen at those moments following the end of its animation but preceding its restart. |
| Illustration | |
|---|---|
|
|
| This example can be seen at http://srufaculty.sru.edu/david.dailey/svg/bezierovals.svg | |
A JavaScript call to document.documentElement.pauseAnimations() will stop all SMIL animations within an SVG document, freezing objects at their current position. Likewise document.documentElement.unpauseAnimations() resumes those SMIL animations from whatever position they were paused. One technical note is that the Adobe SVG viewer in Internet Explorer sometimes seems to throw an uncaught exception causing the browser to crash occasionally when using these methods.
With graphics, text, hyperlinks, JavaScript, SMIL and DOM available to SVG, and with SVG accessible to the most important browsers, one could ask "who needs HTML?" Indeed the majority of SVG documents on the web at the current time are probably implemented without any HTML.
From the viewpoint of the SVG developer, what does HTML offer that SVG can benefit from? Two categories of explanation come to mind: inertia and functionality.
HTML is big and ubiquitous. It has mammoth inertia55. With its large presence, HTML brings several important things:
A good browser these days pretty much has to support JavaScript, CSS, and DOM56. This all works well to SVG's advantage. It means that most people use fairly mature browsers that handle almost all of what a coder can throw at it. Browser inconsistencies have become the exception rather than the rule in recent years. Perhaps the main contribution of HTML to the future of SVG is that nearly everyone with a computer has a browser, and to the extent that those browsers can see SVG so can all those people.
The de facto user interface "standards" that people have come to expect when they traverse the web will continue to influence web interface in the worlds beyond HTML for many years. The use of single clicks instead of double clicks, the notion that the left side of a page might contain navigational assistance of some form, the way that <select>s and other objects respond to onmouseover and onmousedown — these are all examples of the sorts of interface that audiences expect to interact with. Like the QWERTY keyboard, the consumer has come to expect keys to be in certain places, and even if, by sophisticated perceptual-motor engineering work, we may design a better configuration of the keys of a keyboard, this does not mean that better will happen. The expected behavior of certain HTML form elements (widgets) has been stumbled into through historical accident rather than through careful human factors research. Sometimes, it seems to have lacked even the consistent architecture which we might expect of our highways or schools. The SVG designer may be tempted to reinvent some of this baggage with inspired awareness of the shortcomings of business as usual, but does so at some peril of being washed ashore by the substantial momentum of the status quo.
Like the behavioral and cognitive habits of the audience, the habits of the developer will shape any table at which both SVG and HTML hope to sit. HTML/JavaScript coders tend to like (despite its absence from W3C standards) the innerHTML property of screen objects. There is a momentum in place (from document.write to O.getElementById(id).innerHTML) for developers to view content as a string of characters. The DOM2 methods have been available for some years in HTML through ECMAScript, though a random perusal of typical web pages will likely reveal few authors that are actually using them. Google reports that over half of the billion web pages they have surveyed use JavaScript57but their study does not go beyond the <script> tag itself and its attributes. The typical JavaScript is relatively short (under a hundred lines in most cases) and is often used less to customize the Graphical User Interface (GUI) or to rebuild DOM than to process form information and other user data. The purposes and nature, thus, of coding in HTML may be substantially different than in SVG.
While the coding habits of HTML web developers may be slightly askew from the job description for SVG developers, the applicant pool is enormous. As argued in Appendix II, because of the web, there have probably been more programs written in JavaScript than in all other languages combined. Adopting JavaScript/ECMAScript as a scripting companion to SVG instantly certifies (with some degree of dubiousness to the "diploma") programmers numbered in the millions. Compare this to the thousands of programmers with expertise in .Net/XAML or in Lingo/ActionScript or in OpenGL or in VML/JavaScript or any of the other approaches to dynamic "content-rich web browsing" and suddenly SVG/JavaScript looks not only sensible, but canonical.
Let us recall, once more, how large and useful the web is. Two anecdotes are worthy of note, each taking place at different institutions of higher learning. At one, about twenty years ago and before that institution had Arpanet/Internet connectivity, I gave a talk hosted by its library about "Libraries of the Future." In it I talked about the vast "web" of information and presented diagrams of interconnected, graph-theoretic, multi-dimensional repositories of huge amounts of information available at the desktop, and being "surfed" by bleary-eyed adolescents with tongues hanging out. In 1986 these concepts were viewed with skepticism by most in the library community, and as heresy by others. In 1996 at another institution, working on a particularly odious task, I had occasion to need to know the name of a local mountain where prominent authors had sometimes met to discuss their work and gain inspiration. I had visited the place but could not remember the names of the authors or the mountain. The college was on holiday so I thought I would call the library's reference desk with the question. The reference librarian on duty said it had been a slow day and she would work on it and get back with me. An hour went by and I reached a pause in my other work, so I thought "let me see what I can find." In fifteen minutes of poking around on the web in the pre-Google world, I found that it was Hawthorne and Melville (along with Oliver Wendell Holmes) who met on Monument Mountain. Two hours later the library called back having finally found the same information in a reference book. No wonder those 20thcentury librarians thought those ideas were heretical.
Nowadays much of people's astonishment about how big the web is, and some of their skepticism about how authoritative it can be, has started to dwindle. Most folks realize it is an enormous resource. The search engines have realized this for over a decade now. If we think of the web's content as a backdrop for our interface designs then the natural landscape in which we craft our shelters and way stations would inspire not only Melville but Frank Lloyd Wright as well. With AJAX providing a conduit between client and server, SVG with JavaScript has access to the entire watermelon, seeds and rind included. The SVG developer may need to continue parsing HTML that is not well-formed (according to XML rules) in order to digest the sometimes healthy content inside.
In terms of HTML's functionality, it comes with a historical mindset that is missing in SVG — the notion that the browser will decide how to manage content on the screen. SVG elements must be positioned (albeit relative to the visible area) by the author. Historically one could argue that the development of styles and DOM scripting in HTML have both allowed, and somewhat through zeitgeist, necessitated, authors to take more control of layout of their elements. And while CSS and its advocates have pushed against the use of the HTML table as a way of laying out content, there is a generation of HTML authors who learned their fledgling HTML who author by hand and who use the <table> as a way to position elements on the web page.
SVG has not had the sophistication of control of layout that the HTML table brings (with its cols, colspan, and rowspan features) nor of the apparently more popular, but less powerful58 use of CSS for the same thing.
The ways that SVG typically interleaves with HTML offers little opportunity for us to leverage whatever power tables could offer, since multiple <embed>, <object>, and <frame> elements containing SVG will prove a bit bulky and slow to be scattering numerously throughout our web pages.
Apparently, in recognition of this shortcoming, in mid 2008, the SVG Working Group began working on a set of modules to extend the specification, including one module specifically charged with addressing layout issues. It has recently (spring 2009) released notes for public comment on the subject. It appears to offer a dramatic extension of SVG's layout capabilities through customized extensions of CSS. Cameron McCormack, one of the co-chairs of the working group, has written that it is comprised of a set of rectilinear constructs (like CSS and HTML tables). The topic is revisited briefly in Chapter Seven, Directions for Development.
So it may seem that SVG is not well-poised for borrowing from HTML's layout flexibility. As such, what other functions does HTML perform well? Much of its utility, at least for SVG, stems from its form elements: <input type="text">, <input type="radio">, <input type="checkbox">, <input type="password">, <input type="file">, <select> and <textarea>. Some of these like <radio> buttons are not profoundly difficult to make in SVG, nor are they crucial in most of our web development. But in HTML, all these widgets are there already. We don't have to build them. Some like the <select> object usually prove non-trivial for my students when I given the task to them of building them in SVG. Others like <textarea> and <input type=file> end up being major programming projects, with icebergs hidden here and there only partially exposed. The final chapter of this book mentions another approach to the desired interactivity that is on the horizon: Xforms. This approach may give us similar functionality without having to rely on HTML form elements with their default appearance and behavior.
For now, we will discuss the sorts of code involved in building some of the common HTML widgets in SVG. This will also all serve to expand the reader's familiarity with the relationship between scripting and SVG to solve commonly encountered types of problems.
Code to make text and graphics perform like radio buttons will be quite similar to that for checkboxes: click events change the graphic of the clicked object, and either the others or not.
To make it so the user sees something that looks and acts like a collection of radio buttons is not too difficult: a series of carefully positioned ellipses next to some text. When an ellipse is clicked, its fill pattern changes. The script involved is relatively simple, and in truth, radio buttons are of little use in HTML without some scripting. However the amount of markup we ask the author to do in creating radio buttons in SVG could be rather lengthy — an implementation I have shown to my students involving little scripting but a lot of markup looks something like this for one radio button:
<g id="radio"> |
Such an approach also requires an author to manage a lot of screen coordinates. We might instead prefer our authors to be able to simply declare something considerably more terse like
<g id="radio"> |
and then have that converted by script into an appearance that is as we wish it to be. The scripts in the following page accomplish exactly that role.
| A script creating radio buttons from simple text tags. | |
|
The standard beginning. A style to simplify the markup of the text and radio circles. |
|
An array for the innermost circles, the visibility of which will be changed. We establish the position of the radio group and the vertical offset between the items. We s |