Tangram shapes
When drawing vector graphics I often struggle to describe measurements. Take the following <path> element:
<path d="M 0 0 l 2 2 l 2 -2" /> I would argue in terms of commands:
M 0 0: move to (0, 0)l 2 2: line to (2, 2)l 2 -2: line to (4, 0)
d=""
Conveniently, you don’t need to repeat the l character after the first instance. The browser assumes the same instruction until told otherwise.
-<path d="M 0 0 l 2 2 l 2 -2" />
+<path d="M 0 0 l 2 2 2 -2" /> Eventually, you draw a triangle. How much space will that triangle occupy on the page? How many pixels? Well, there’s not a single answer, and that taps into the scalability of SVG.
Set the width to 4 pixels and the element occupies an area 4 pixels wide. Change the value to 40 pixels and the triangle will grow to the new measure.
With this in mind, when you move to the point (2, 2), you can’t really say “move 2 pixels right, 2 pixels down”. Just, “move 2 units right, 2 units down”. Relative to what? Enters the SVG coordinate system, explored in the context of seven puzzle pieces.
SVG shapes
With the viewBox attribute set up an <svg> element with a 4 by 4 canvas. The default is 300 by 100, but we don’t need such outrageous values.
<svg viewBox="0 0 4 4">
<!-- -->
</svg> The triangle introduced earlier is one of the largest pieces of the set, and we only need as much space as to fit the same <path> element.
As a matter of fact, there’s another triangle as big. Let’s start from the origin, but end in the bottom left corner.
<path d="M 0 0 l 2 2 2 -2" />
<path d="M 0 0 l 2 2 -2 2" /> Moving on to smaller pieces, there are two miniscule triangles we can paint in increments.
<path d="M 3 1 l 1 -1 0 2" />
<path d="M 1 3 l 1 -1 1 1" /> One beefier triangle ties up the bottom right corner.
<path d="M 2 4 l 2 -2 0 2" /> A solitary square tipped on its side glues the section together.
<path d="M 2 2 l 1 -1 1 1 -1 1" /> One oblong trapezoid completes the set.
<path d="M 0 4 l 1 -1 2 0 -1 1" /> Turns out, 4 by 4 was more than enough.
We can color the pieces through the fill attribute to make the shapes more appealing, but the puzzle is set.
SVG transform
The most immediate way to shuffle the shapes around comes from the transform attribute, translating and rotating elements through dedicated functions.
I like to delegate the transformation to group elements, so to have the <path> focused on the drawing logic.
<g transform="rotate(45)">
<path d="M 0 0 l 2 2 2 -2" />
</g> Little hiccup, however. Rotate the triangle in the snippet and the operation takes place as expected.
Rotate one of the other shapes, one which is not drawn from the origin like the square, and well, the result is certainly not the same.
<g transform="rotate(45)">
<path d="M 2 2 l 1 -1 1 1 -1 1" />
</g> Your eyes do not deceive you. The square is drawn in its entirety, but parts of it are just outside of the visible area. Increase the angle and you’ll see the square slowly moving out of position.
In the mesmerizing realm of the SVG coordinate system, transformations are applied from an origin point, beginning with the value described by the viewBox attribute — in our example (0, 0).
To move this hinge, this frame of reference, you need to apply a translation ahead of time.
<g transform="translate(2 2)">
<!-- rotated shape -->
</g> Since you moved the origin, draw the path from (0, 0).
-<path d="M 2 2 l 1 -1 1 1 -1 1" />
+<path d="M 0 0 l 1 -1 1 1 -1 1" /> The moment you then apply the rotation, this one works to rotate the element from the starting coordinate.
<g transform="translate(2 2)">
<g transform="rotate(45)">
<path d="M 0 0 l 1 -1 1 1 -1 1" />
</g>
</g> There’s nothing inherently wrong with the first transformation. There might come a day you’ll want to move elements from a different advantage point. In this tangram puzzle, however, we want to scatter the pieces from their initial (x, y) value.
If you are a fan of brevity, you can even apply the transformations in sequence, with a single group.
<g transform="translate(2 2) rotate(45)">
<path d="M 0 0 l 1 -1 1 1 -1 1" />
</g> The order matters, but we’ll see how later with a more practical example.
Transform practice
Draw every shape from (0, 0).
The pieces stack one above the other, with later elements drawn on top of the previous shapes.
In order to return to the compact configuration, wrap the elements in a group, and translate them by the now-unnecessary offset.
d | transform |
|---|---|
M 0 0 l 2 2 2 -2 | translate(0 0) |
M 0 0 l 2 2 -2 2 | translate(0 0) |
M 0 0 l 1 -1 0 2 | translate(3 1) |
M 0 0 l 1 -1 1 1 | translate(1 3) |
M 0 0 l 2 -2 0 2 | translate(2 4) |
M 0 0 l 1 -1 1 1 -1 1 | translate(2 2) |
M 0 0 l 1 -1 2 0 -1 1 | translate(0 4) |
If you feel cheated you’ve already seen the set, do not fret. We are going to move the pieces around in a brand new visual. This one is going to exceed the visible area, so we need to first adjust the viewBox.
-<svg viewBox="0 0 4 4">
+<svg viewBox="-2 -2 8 8"> The update is fairly technical, but in essence, you inset the canvas by 2 units and expand its size by twice the amount, 4. In this manner you have some space around the centered <path>s.
On to the sliding practice. The shape of a heart makes for a perfect milestone. Simple, yet striking.
Slide the first piece slightly to the right, slightly to the bottom.
<g transform="translate(1 1)">
<path d="M 0 0 l 2 2 2 -2" />
</g> The second, large triangle should sit right above, and we can achieve this in two steps:
translate the group to the same point
rotate the shape around the offset coordinate
<g transform="translate(1 1) rotate(-90)">
<path d="M 0 0 l 2 2 -2 2" />
</g> As mentioned in the previous section, the order of the operations matters. Consider the steps in increments.
transform=""
If you were to reverse the instructions, if you were to rotate the group first, you would then move the triangle in a completely different spot.
transform=""
When you apply the rotation, you rotate the coordinate system. Every instruction which follows will be tilted accordingly. Once more, you may want to, just not today.
The first small triangle fits nicely at the top of the left side.
<g transform="translate(1 -1) rotate(90)">
<path d="M 0 0 l 1 -1 0 2" />
</g> The second smaller unit could go pretty much anywhere, but the oblong piece surely fits nicely in the empty space.
<g transform="translate(-1 1)">
<path d="M 0 0 l 1 -1 2 0 -1 1" />
</g> The remaining triangles mirror the image on the left side.
<g transform="translate(1 1) rotate(90)">
<path d="M 0 0 l 1 -1 1 1" />
</g>
<g transform="translate(1 3) rotate(-90)">
<path d="M 0 0 l 2 -2 0 2" />
</g> Finally, the square tucks the visual at the very bottom. No rotation required.
<g transform="translate(1 3)">
<path d="M 0 0 l 1 -1 1 1 -1 1" />
</g> CSS transform
SVG has a transform attribute. On its side, CSS has a transform property. The two work rather similarly, but there are few notable differences.
Once again, let’s take the square as an example.
<g transform="translate(1 3)">
<path d="M 0 0 l 1 -1 1 1 -1 1" />
</g> Target the group element with the nth-of-type selector. This is the penultimate shape.
svg > g:nth-of-type(6) {
/* */
} To move the group around, you need to specify a unit of measure, pixels to be precise.
svg > g:nth-of-type(6) {
transform: translate(1px, 1px);
} Do not forget the compulsory comma between the two offsets as well.
Ultimately, the element is translated in the context of the viewBox attribute — the piece slides 1 unit to the right, 1 unit to the bottom. That being said, the px suffix requires a small mental adjustment compared to the SVG, unitless version.
I did not mention anything about the transform attribute on the group. The SVG version is still there, but the transform property takes precedence — CSS specificity at work. This means we can recreate the compact set with the original translations in the stylesheet.
svg > g:nth-of-type(1) {
transform: translate(0px, 0px);
}
svg > g:nth-of-type(2) {
transform: translate(0px, 0px);
}
svg > g:nth-of-type(3) {
transform: translate(3px, 1px);
}
svg > g:nth-of-type(4) {
transform: translate(1px, 3px);
}
svg > g:nth-of-type(5) {
transform: translate(2px, 4px);
}
svg > g:nth-of-type(6) {
transform: translate(2px, 2px);
}
svg > g:nth-of-type(7) {
transform: translate(0px, 4px);
} CSS transition
There is a point in introducing CSS and the overriding syntax. In the world of cascading stylesheets you have access to the transition property, and the enticing possibility of moving the shapes smoothly over time.
Condition the CSS properties to a class, right on the <svg> element.
/* :nth-of-type 1 to 7 */
svg.tangram > g:nth-of-type(7) {
transform: translate(0px, 4px);
} Toggle the class and immediately you go from the compact set to the heart-warming visual.
Apply a transition on the direct children.
svg > g {
transition: transform 0.75s ease-in-out;
} And enjoy the smooth update.
Job done? Not really. I tested the code on FireFox and, at the time of writing, the browser doesn’t seem to process the change between SVG attributes and CSS properties that well.
The discovery doesn’t spoil the entire exercise, however, and is a perfect excuse to discuss CSS specificity one last time.
The solution is to rely entirely only on CSS, for both configurations.
Update the markup to replace the attributes with inline styles. From SVG to CSS.
-<g transform="translate(1 3)">
+<g style="transform: translate(1px, 3px)"> Thankfully, the change requires a minor tweak to the syntax.
transform | style="transform" |
|---|---|
translate(0 0) | translate(0px, 0px) |
translate(0 0) | translate(0px, 0px) |
translate(3 1) | translate(3px, 1px) |
translate(1 3) | translate(1px, 3px) |
translate(2 4) | translate(2px, 4px) |
translate(2 2) | translate(2px, 2px) |
translate(0 4) | translate(0px, 4px) |
In the stylesheet override the values with the !important flag. The only option to triumph over the highly specific inline styles.
/* :nth-of-type 1 to 7 */
svg.tangram > g:nth-of-type(7) {
transform: translate(0px, 4px) !important;
} And then you are really, finally done. Free to experiment with different timing functions and enjoy the end result.