Troubleshooters.Com®, Code Corner, and SVG Clickmaps Present:

Animated SVG Clickmap Landmines

Contents:

Introduction

Before reading this document, you should read the SVG Clickmap Landmines document. As difficult and obscure as that doc is, things get much murkier when your SVG moves SVG elements. And even more murky when those elements are moved in relation to other elements.

All examples in this document store all javascript inside the SVG element. And things go best (and most browser independent) when the SVG element is entirely included within the HTML document. An easy way to do that ,while still doing your graphical design in Inkscape, is explained here.

Watch out for Length Objects

Watch out for SVG length objects, such as mycircle.cx, mycircle.cy, and mycircle.r. These are not integer variables you can read and assign. They look like integer variables, and in some contexts they might even work like integer variables, but they're not, and forgetting that sets you up for a long walk down the Boulevard of Blunder.

When you have problems reading or writing element positions or dimensions, your first step is to determine the type. There are all sorts of "usually work on some browsers" type ways to get this information, such as mycircle.constructor.name or the even less reliable mycircle.prototype.toString (the lack of parens on toString() is not a typo, it's a low level string representation. I don't use these. Instead, I use the alert() function, as follows:

alert('diag1 ' + mycircle.cx);

On Firefox, Chromium, Palemoon, Qupzilla, surf, and probably many other browsers, the preceding alert() function puts up a dialog box, saying the following:

diag1 [object SVGAnimatedLength]

If you had used something nonexisting instead of mycircle.cx, for instance, mycircle.junkcx, the alert() would have thrown up the following message:

diag1 undefined

The preceding doesn't mean that the string 'diag1' is undefined. It means that something undefined was appended to the string 'diag1'.

Anyway, mycircle.cx is type SVGAnimatedLength. That's interesting because an object of type SVGAnimatedLength has two component objects called baseVal and animVal. These represent, respectively, the initial value before any Javascript was executed, and the current value. The alert() command shows each of these to be an SVGLength object, not a number. To turn them into a number, you need to further append .value. In other words, the following alert statement throws up 'diag1' followed by the numeric value of mycircle.cx:

alert(mycircle.cx.animVal.value)

The following code moves mycircle 10 units to the right:

mycircle.setAttribute('cx', mycircle.cx.animVal.value + 10)

Notice that to set cx, you need to use setAttribute(), not any kind of assignment. This is because cx is read-only. You must use setAttribute() to change any dimension of position of any shape, or the d attribute of a path.

Landmine Alert: animVal is read-only

Any animVal is read-only, meaning you cannot change it by assignment or function. If you need to change an attribute (cx for example), change cx.baseVal, not cx.animVal. Note that when you change cx.baseVal, it changes cx.animVal to the same amount. This is also true for baseVal and animVal related to matrices also.

Use setAttribute() to change positions and dimensions

Always use setAttribute() to change an element's positions and dimensions, such as radius, length, width, and starting coordinates. This is because such positions and dimensions, are read-only, meaning you can't assign a value to them. So the proper way to make circle my circle's radius 10% larger is the following:

	mycircle.setAttribute('r', mycircle.r.animVal.value * 1.1);

Do not try to assign a value to mycircle.r.animVal.value or mycircle.r.animVal or mycircle.r because doing so won't change the attribute. Use only setAttribute().

Always use setAttribute() to change the d attribute of a path element. Although there are cases where direct assignment to the d attribute appear to work, such direct assignment usually fails, whereas setAttribute() always succeeds.

Always use setAttribute() to change the value of an element's attribute: Never try to assign to it, because that won't work. If you have theoretical questions about anything in this section, please reread the section on Watch out for Length Objects.

The first argument to setAttribute() is always a string

Be careful. Forgetting this can run you around in a swamp of unproductive troubleshooting.

The first argument to setAttribute is always a string, with that string representing the name of the argument to set. For instance, consider the following code to make circle my circle's radius 10% larger, by increasing mycircle.r, is the following:

mycircle.setAttribute('r', mycircle.r.animVal.value * 1.1);

Observe that its first argument is 'r', a string. Removing the quotes would cause the code to fail.

Always get numeric value using animVal.value or BaseVal.value

As explained in the section on Watch out for Length Objects, a numeric value cannot be read directly out of mycircle.cx, mycircle.cy, mycircle.r. Instead, append .animVal.value or .baseVal.value. To retrieve mypath.d as a pure string, use , or mypath.d.animVal.value.

Translations can ruin your whole day

SVG translations are so complicated and so underdocumented that they absolutely can ruin your whole day. Or more than a day. Multi-hour, trial and error troubleshooting almost always results when somebody doesn't know the exact, intricate choreography for getting and putting translations. Translation related problems often masquerade as other types of problems. This section is long, but necessary. This section on translations explains the problems, and shows the exact, intricate choreography necessary to get the location of a group having a translation.

Definition of a transform

A translation is one form of a transform. In SVG1, translations include the following transformations:

The following graphic might help clarify the preceding list:

Graphic of the four transforms

Life gets insanely complex if more than one of these transforms is applied to an object, because each transform affects each other transform's calculations. Operating with more than one transform on an object is way beyond the scope of this document. Do whatever you need to do to make sure only to apply one type of transformation to an object. For instance you can remove all translate and scale transforms from a group by ungrouping and regrouping it. Ungrouping causes the group's transforms to be applied to its contents' dimensions and positions. Regrouping incorporates the group's contents at their current dimensions and positions, without transforms.

For simplicity's sake, the rest of this section assumes you're dealing only with translation type transforms, only translations applied to groups, and only one transform or translation per group. It's easy to construct groups this way within Inkscape.

A group's position is defined by its transform and contents

SVG groups are delineated by the <g> and </g> tags. The <g> element can take a lot of different attributes, but x and y are not attritutes it can take. The group's position in its layer, drawing or containing group is determined by the group's translation. Basically, if you have a group containing exactly one transform, that transform being a translate, that contains geometric elements not having their own transforms, then to get the "true" position of one of the group's elements, you add that element's x coord to the group's x translation, and same for y. If you have a group containing a circle, having class grpcircle, having no transforms of its own, the following function returns the true position of the circle as an array with element 0 as the X coordinate, and element 1 as the Y coordinate:

function grp_coords(grp){
	matrix = grp.transform.animVal.getItem(0).matrix;
	var coords = [];
	ghead = grp.querySelector('.grpcircle');
	coords[0] = ghead.cx.animVal.value + matrix.e;
	coords[1] = ghead.cy.animVal.value + matrix.f;
	return(coords);
}

If you have such a group, with id=mygrp4, and a circle without any transforms, id=mycircle8, the following code moves center point of the circle to the center point of the circle element of the group:

var grp_elm = querySelector('#mygrp4');
var coords = grp_coords(grp_elm);
var circle_elm = querySelector('mycircle8');
circle_elm.setAttribute('cx', coords[0]);
circle_elm.setAttribute('cy', coords[1]);

Move a group by adjusting its translate

The preceding section moved an element over a group. Suppose you want to do the opposite: Move a group over an element? This is done by adjusting the group's translate, once again assuming it has exactly one transform and that's a translate. This sounds easy, but in fact it's complex enough to be disturbing, and underdocumented enough to make a stable person curse.

Imagine a situation where you want to move a group of class "screw" so its center is directly over an anchor circle. This is even more complicated than moving a circle over an anchored screw. First, you find the coordinates of the circle, assuming the circle has no translates. Next, you get the real coordinates of the screw to be moved, meaning those coordinates must take into account the translation on the screw group, requiring the grp_coords() function, which is repeated below to refresh your memory:

function grp_coords(grp){
	matrix = grp.transform.animVal.getItem(0).matrix;
	var coords = [];
	ghead = grp.querySelector('.grpcircle');
	coords[0] = ghead.cx.animVal.value + matrix.e;
	coords[1] = ghead.cy.animVal.value + matrix.f;
	return(coords);
}

Now you find what must be added to the screw's real coordinates so they are the same as the circles. Do this by subtracting the screw's real x from the circle's x, and the screw's real y from the circle's y. Now you simply add that calculated x and y movement to the screw's current translation. The following is code in the anchor circle's click to move the screw to the anchor circle's location:

function anchorcircleclick(circ){
	/* Get coords of circle you'll be moving TO */
	var circcx = circ.cx.animVal.value;
	var circcy = circ.cy.animVal.value;

	/* Get coords of screw you'll be moving */
	var screw = document.querySelector('.screw');
	screw_coords = grp_coords(screw);
	var screwcx = screw_coords[0];
	var screwcy = screw_coords[1];

	/* Get the travel distance */
	/* from screw to circle, x and y */
	var dcx = circcx - screwcx;
	var dcy = circcy - screwcy;

	/* Modify the screw's translation */
	var matrix = screw.transform.animVal.getItem(0).matrix;
	var mx = matrix.e;
	var my = matrix.f;
	screw.transform.baseVal.getItem(0).setTranslate(mx + dcx, my+dcy);
}

The preceding code is for a special set of circumstances:

The general case would be way too complicated to discuss, but this section gives you an idea of how to move a group by adjusting its translation.

About Matrices

The preceding two sections contained code like the following:

matrix = grp.transform.animVal.getItem(0).matrix;

In the preceding, matrix is an attribute of type SVGMatrix, which consists of six values: matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, and matrix.f. All six work together to define the object's transform, which sets the object's coordinate system.

If the transform contains only a single translation, then matrix.e represents the translation's x value, and matrix.f represents the translation's y value. However, if there's any rotation, scaling, or skewing involved, all bets are off: The translation part of the transform must be evaluated using matrix arithmetic, requiring some moderately challenging learning and definitely beyond the scope of this document.

Location troubleshooting tip

Sooner or later, an attempted movement of an element will make the element appear to disappear. Now, did a javascript error cause a malfunction, did the element somehow become transparent, or is the element really there but just in a location outside of the SVG image's viewport? It's usually the latter, and there's a way to pin it down, especially if the element is a circle or contains a circle within itself.

Temporarily make the circle's radius huge, like 10,000 or 100,000 or even 1,000,000. If the problem was that its location was fallout outside the viewport, the now gigantic circle completely covers the viewport and more, meaning the entire image becomes the color of the circle. Reduce the radius by orders of magnitude until it disappears, then by halves or quarters until one or two sides are curved, at which time you can deduce approximately where the center is, and try to diagnose the problem with the center location.

Other Documentation

This document warns you of SVG animation landmines and gives you correct ways to perform simple animation tasks. At some point you'll need more documentation. Here are a few documents that might help:

Wrapup

Animation in SVG is a challenge. It's underdocumented. The right ways to do things are inobvious and unexpected. Use this document to alert you to SVG animation landmines you'll encounter, and the right ways to get around those landmines. Read this document early and often. Every minute you spend reading this document will save you a half hour of trial and error, contradictory web lookup, and frustration moving SVG elements.


[ Training | Troubleshooters.Com | Email Steve Litt ]