Transformations

Before jumping into more complicated things, let's take a look at transformations. For now, think of transformations as manipulations of whole SVG shapes.
Without going into too much mathematical detail, it suffices to say that transformations, as used in SVG, are affine transformations of coordinate systems used by shapes in our drawing. The beautiful thing is they can be defined as matrix multiplications, making them very efficient to compute--it's much more performant for your graphics card to animate a shape moving from left to right using a transformation than it is to recalculate the position of each point in a shape and modify the shape directly.

However, unless your brain is made out of linear algebra, using transformations as matrices can get very tricky. SVG helps out by coming with a set of predefined transformations, namely, translate(), scale(), rotate(), skewX(), and skewY().

According to Wikipedia, an affine transformation is any transformation that preserves points, straight lines, and planes, while keeping sets of parallel lines parallel. They don't necessarily preserve distances but do preserve ratios of distances between points on a straight line. This means that if you take a rectangle, you can use affine transformations to rotate it, make it bigger, and even turn it into a parallelogram; however, no matter what you do, it will never become a trapezoid.

Computers handle transformations as matrix multiplication because any sequence of transformations can be collapsed into a single matrix. This means that they only have to apply a single transformation that encompasses your sequence of transformations when drawing the shape, which is handy.

We will apply transformations with the transform attribute. We can define multiple transformations that are applied in order. The order of operations can change the result. You'll note this in the following examples.

Let's move our eye to the edge of the rectangle:

  chart.container.selectAll('ellipse, circle') 
.attr('transform', 'translate(150, 0)');

We selected everything our eye is made of (two ellipses and a circle), and then applied the translate transformation. It moved the shape's origin along the (150, 0) vector, moving the shape 150 pixels to the right and 0 pixels down.

If you try moving it again, you'll note that new transformations are applied according to the original state of our shape. That's because there can only be one transform attribute per shape.

Our picture looks as follows:

Let's rotate the eye by 45 degrees:

chart.container.selectAll('ellipse, circle') 
.attr('transform', 'translate(150, 0) rotate(45)');

The output will look something like this:

That's not what we wanted at all!

What tricked us is that rotations happen around the origin of the image, not the shape. We have to define the rotation axis ourselves:

chart.container.selectAll('ellipse, circle') 
.attr('transform', 'translate(150, 0) rotate(-45, 350, 250)');

By adding two more arguments to rotate(), we defined the rotation axis and achieved the desired result:

Let's make the eye a little bigger with the Scale() transformation:

chart.container.selectAll('ellipse, circle') 
.attr('transform', 'translate(150, 0) rotate(-45, 350, 250)
scale(1.2)');

This will make our object 1.2 times bigger along both the axes; two arguments would have scaled by different factors along the x and y axes:

Once again, we pushed the position of the eye because scaling is anchored at the zeroth point of the whole image. We have to use another translate to move it back. However, the coordinate system we're working on is now rotated by 45 degrees and scaled. This makes things tricky. We need to translate between the two coordinate systems to move the eye correctly. To move the eye 70 pixels to the left, we have to move it along each axis by 70*sqrt(2)/2 pixels, which is the result of cosine and sine at an angle of 45.

However, that's just messy. The number looks funny, and we worked way too much for something so simple. We're also falling off the page. Let's change the order of operations instead:

chart.container.selectAll('ellipse, circle')
.attr('transform', `translate(150, 0)
scale(1.2)
translate(-250, 0)
rotate(-45, ${350 / 1.2}, ${250 / 1.2})`);

Much better! We got exactly what we wanted:

A lot has changed, let's take a look.

First, we translate to our familiar position and then scale by 1.2, pushing the eye out of position. We fix this by translating back to the left by 250 pixels, and then finally performing the 45 degree rotation, making sure to divide the pivot point by 1.2.

There's one more thing we can do to the poor eye; skew it. Two skew transformations exist: skewX and skewY. Both skew along their respective axis:

  chart.container.selectAll('ellipse, circle') 
.attr('transform', `translate(150, 0)
scale(1.2)
translate(-250, 0)
rotate(-45, ${350 / 1.2}, ${250 / 1.2})
skewY(20)`);

We just bolted skewY(20) on to the end of the transform attribute:

We have once more destroyed our careful centering. Fixing this is left as an exercise for the reader.

All said, transformations really are just matrix multiplication. In fact, you can define any transformation you want with the matrix() function, which the other methods are really just shortcuts for. To master transformations, take a look at what kind of matrix produces each of the preceding effects via the W3C specification that is available at http://www.w3.org/TR/SVG/coords.html#EstablishingANewUserSpace.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset