Sunday 22 December 2019

Web Tutorial: Christmas Tree SVG Animation

Merry Christmas, readers!

For this month's web tutorial, let's do something light and fun. We'll be playing with Scalable Vector Graphics, SVG for short! SVG has been around a long time, but it is only with HTML5 that we get to embed them in HTML pages. They're different from CSS-rendered graphics, but totally compatible with CSS styling. In fact, there are many parallels.

It's really easy, if you're prepared to do a bit of simple arithmetic. Really simple arithmetic.

Without further ado, here's some HTML.
<!DOCTYPE html>
<html>
    <head>
        <title>Xmas SVG</title>

        <style>
       
        </style>
    </head>

    <body>

    </body>
</html>


Add in an svg tag. Yep, it's that simple! Then add some text in there. This should not show up on your browser, but in the event you come across a browser that's so backward it doesn't support SVG (yes, Internet Explorer, I'm looking at you), the text will be displayed.
<!DOCTYPE html>
<html>
    <head>
        <title>Xmas SVG</title>

        <style>
       
        </style>
    </head>

    <body>
            <svg>
                SVG not supported
            </svg>
    </body>
</html>


Let's do some styling. Set the body background to a dark blue, then give the svg element a height, width and a red outline.
        <style>
            body
            {
                background-color: rgba(0, 0, 50, 1);
            }

            svg
            {
                width: 500px;
                height: 500px;
                outline: 0px solid #FF0000;
            }
        </style>


Here's your SVG. We'll start drawing next.


OK, this may be counter-intuitive, but we will jump right into drawing a path - the shape of a Christmas tree. The cool thing about SVG is that unlike CSS, you don't have to divide complex shapes into rectangles and circles and figure out clever ways to make it work. Nope, you just draw a path. Like so. Give it a class of tree while you're at it.
            <svg>
                <path class="tree" />
                SVG not supported
            </svg>


Now remember you're drawing an SVG, so the usual CSS attributes don't apply. Here, we define the thickness of the lines using the stroke-width property, and set the line color to a basic green. You could define stroke and stroke-width as attributes within the path tag itself, but that's just messy.
            svg
            {
                width: 500px;
                height: 500px;
                outline: 1px solid #FF0000;
            }

            .tree
            {
                stroke: rgba(0, 255, 0, 1);
                stroke-width: 1;
            }


In the SVG, the d attribute is a string of commands we'll use to define the path. I broke up the line into several lines for clarity. It's about to get a lot longer. First, we use "M 250 100" which means "move to x-coordinate 250 and y-coordinate 100". For reference, the top left corner of the SVG is (0, 0). Since the SVG is 250 pixels wide and tall, (250, 100) is horizontally right in the middle and vertically near the top. This point is the top of the tree.
            <svg>
                <path d="
                M 250 100"
                class="tree"/>
                SVG not supported
            </svg>


Next, draw a line to the next point, somewhere left to the starting point and lower. For this, you use "l". It stands for "line", as in, "line to x minus 30, y plus 30 from the last specified point". The lower case "l" allows us to specify the instructions relative to the last point. You can use an uppercase "L", but then you'd have to specify the absolute coordinates (220, 70) which doesn't sound too bad right now, but can be a huge pain in the butt if you need to move the tree later on.
            <svg>
                <path d="
                M 250 100
                l -30 30
                "
                class="tree"/>
                SVG not supported
            </svg>


So yeah, you see what you've done? You've created a green line from (250, 100) to (220, 70).


Let's specify another line. The next one doesn't move vertically, but is 10 pixels right from the last point.
            <svg>
                <path d="
                M 250 100
                l -30 30 l 10 0
                "
                class="tree"/>
                SVG not supported
            </svg>


Now you see you've drawn a horizontal line from the last point. Do you see the black background there? That's because we never specified a background color for the path, so it's defaulting to that color. Leave it alone for now.


Here's one side of the tree drawn for you.
                <path d="
                M 250 100
                l -30 30 l 10 0
                l -30 30 l 10 0
                l -30 40 l 10 0
                l -40 50 l 10 0
                l -40 70 l 10 0
                l -40 80 l 320 0
                " class="tree"/>


See it taking shape?


Let's complete the drawing.
                <path d="
                M 250 100
                l -30 30 l 10 0
                l -30 30 l 10 0
                l -30 40 l 10 0
                l -40 50 l 10 0
                l -40 70 l 10 0
                l -40 80 l 320 0
                l -40 -80 l 10 0
                l -40 -70 l 10 0 
                l -40 -50 l 10 0
                l -30 -40 l 10 0
                l -30 -30 l 10 0
                l -30 -30
                " class="tree"/>


Ah. Spectacular!


To color the tree, we use the fill property. We'll set it to a muted green.
            .tree
            {
                stroke: rgba(0, 255, 0, 1);
                stroke-width: 1;
                fill: rgb(0, 100, 0);
            }


The tree's been drawn! But it looks a bit stiff...


What this tree needs is curves. Instead of "l", use "q". That's "q" as in "Quadratic Bézier curve". It still draws a line to the point specified, but two new numbers, namely, 0 and 10, form the arguments. The first number is how much the line will curve towards the x-axis, while the second number is how much the line will curve towards the y-axis. In this case, "0" means that no action is taken along the x-axis while the line dips slightly about "10" pixels vertically.
                <path d="
                M 250 100
                q 0 10 -30 30 l 10 0
                l -30 30 l 10 0
                l -30 40 l 10 0
                l -40 50 l 10 0
                l -40 70 l 10 0
                l -40 80 l 320 0
                l -40 -80 l 10 0
                l -40 -70 l 10 0 
                l -40 -50 l 10 0
                l -30 -40 l 10 0
                l -30 -30 l 10 0
                l -30 -30
                " class="tree"/>


See the curve at the top?


The rest of it has been done for you. Experiment! Quadratic Bézier curves aren't the point of this tutorial today, and they aren't that important right now. But they sure do make things prettier.
                <path d="
                M 250 100
                q 0 10 -30 30 l 10 0
                q 0 10 -30 30 l 10 0
                q 0 20 -30 40 l 10 0
                q 0 20 -40 50 l 10 0
                q 0 30 -40 70 l 10 0
                q 0 40 -40 80 l 320 0
                q -40 -40 -40 -80 l 10 0
                q -40 -30 -40 -70 l 10 0 
                q -30 -20 -40 -50 l 10 0
                q -20 -10 -30 -40 l 10 0
                q -20 -10 -30 -30 l 10 0
                q -20 -10 -30 -30
                " class="tree"/>


Nice curves!


The Star

So now you've drawn a tree-shaped path and colored it. Let's draw a circle at the top of the tree. The center of the circle is defined by the cx and cy attributes. They've been set to the coordinates of the topmost point of the tree. The radius is defined by the attribute r. In this case, it's 5 pixels. We'll set the rest of the circle using the toplight CSS class.
                <circle cx="250" cy="100" r="5" class="toplight">

                </circle>

                <path d="
                M 250 100
                q 0 10 -30 30 l 10 0
                q 0 10 -30 30 l 10 0
                q 0 20 -30 40 l 10 0
                q 0 20 -40 50 l 10 0
                q 0 30 -40 70 l 10 0
                q 0 40 -40 80 l 320 0
                q -40 -40 -40 -80 l 10 0
                q -40 -30 -40 -70 l 10 0 
                q -30 -20 -40 -50 l 10 0
                q -20 -10 -30 -40 l 10 0
                q -20 -10 -30 -30 l 10 0
                q -20 -10 -30 -30
                " class="tree"/>


Here, we define a bright yellow (almost white) border at a very low opacity, and the fill is the same color but at higher opacity. The border width is 15 pixels.
            .tree
            {
                stroke: rgba(0, 255, 0, 1);
                stroke-width: 1;
                fill: rgb(0, 100, 0);
            }

            .toplight
            {
                stroke: rgba(255, 255, 200, 0.3);
                stroke-width: 15;
                fill: rgba(255, 255, 200, 0.8);
            }


Note that there are no z-index properties for SVGs. Whatever element comes first is covered by anything that is drawn over it. Which is pretty damn convenient under certain conditions!


Let's animate that!

Use the animate tag within the circle tag. The attributeName attribute defines the attribute that will be animated. It's "r". The from and to attributes determines what values the animation will change "r" to, And the dur attribute defines how long the entire animation takes. Let's set it to half a second. The repeatCount attribute, set to "indefinite", means that the animation will repeat forever.

Refresh your browser. The circle at the top will appear to be blinking rapidly! That's because it's growing from 4 pixels radius to 6 pixels radius every half a second.
                <circle cx="250" cy="100" r="5" class="toplight">
                    <animate attributeName="r" from="5" to="6" dur="0.1s" repeatCount="indefinite" />
                </circle>


Time for some decorations...

We'll draw another circle. Place it anywhere within the Christmas Tree, make it as large as you want, and style it using the CSS class ball.
                <path d="
                M 250 100
                q 0 10 -30 30 l 10 0
                q 0 10 -30 30 l 10 0
                q 0 20 -30 40 l 10 0
                q 0 20 -40 50 l 10 0
                q 0 30 -40 70 l 10 0
                q 0 40 -40 80 l 320 0
                q -40 -40 -40 -80 l 10 0
                q -40 -30 -40 -70 l 10 0 
                q -30 -20 -40 -50 l 10 0
                q -20 -10 -30 -40 l 10 0
                q -20 -10 -30 -30 l 10 0
                q -20 -10 -30 -30
                " class="tree"/>

                <circle cx="270" cy="150" r="5" class="ball">

                </circle>


Here, we will give ball an orange outline, 1 pixel thick. For the fill property, we will use an SVG-defined value. It's represented with a hash sign and the value name, which is "ballGradient".
            .tree
            {
                stroke: rgba(0, 255, 0, 1);
                stroke-width: 1;
                fill: rgb(0, 100, 0);
            }

            .ball
            {
                stroke: rgba(255, 200, 100, 0.3);
                stroke-width: 1;
                fill: url(#ballGradient);
            }

            .toplight
            {
                stroke: rgba(255, 255, 200, 0.3);
                stroke-width: 15;
                fill: rgba(255, 255, 200, 0.8);
            }


You should now see an orange circular outline near the top of the tree.


Now let's define the fill, ballGradient. We first create a defs tag.
            <svg>
                <defs>

                </defs>

                <circle cx="250" cy="100" r="5" class="toplight">
                    <animate attributeName="r" from="5" to="6" dur="0.1s" repeatCount="indefinite" />
                </circle>


Within this tag, create a radialGradient tag with an id of ballGradient.
                <defs>
                    <radialGradient id="ballGradient">

                    </radialGradient>
                </defs>


Let's fill in these attributes. fx and fy define the center point of the innermost circle. In our case, we want it horizontally to the left, and vertically middle. cx and cy define the center point of the outermost circle. I'm gonna place this right in the middle of the ball. r defines the radius of the gradient.
                <defs>
                    <radialGradient id="ballGradient" cx="50%" cy="50%" r="80%" fx="10%" fy="50%">

                    </radialGradient>
                </defs>


Now, radial gradients need stops. Stops are the points in the gradient where the color r opacity changes. For the first stop, let's define the offset attribute at 10%, meaning that when the gradient reaches 10% of its entire range, the color is bright orange.
                <defs>
                    <radialGradient id="ballGradient" cx="50%" cy="50%" r="80%" fx="10%" fy="50%">
                        <stop offset="10%" style="stop-color:rgb(255, 200, 100); stop-opacity:1" />
                    </radialGradient>
                </defs>


Then at 100%, the color should have changed to a darker shade of orange.
                <defs>
                    <radialGradient id="ballGradient" cx="50%" cy="50%" r="80%" fx="10%" fy="50%">
                        <stop offset="10%" style="stop-color:rgb(255, 200, 100); stop-opacity:1" />
                        <stop offset="100%" style="stop-color:rgb(255, 50, 0); stop-opacity:1" />
                    </radialGradient>
                </defs>


All good? Fantastic.


Let's add an animation to this decorative ball. Here, we'll animate the width of the outline. Fill in whatever values you like. Get creative!
                <circle cx="270" cy="150" r="5" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2.5s" repeatCount="indefinite" />
                </circle>


You'll see the decorative orange ball animate with a radiant glow effect.


Nice, right? So just place several more of these babies all  over the tree, with random cx and cy attributes, radii and animation values. In particular, try varying the animation durations.
                <circle cx="270" cy="150" r="5" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="240" cy="290" r="3" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>

                <circle cx="150" cy="330" r="7" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="290" cy="250" r="5" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>

                <circle cx="220" cy="180" r="6" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="1.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="170" cy="260" r="3" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="1.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="280" cy="320" r="5" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>

                <circle cx="230" cy="380" r="4" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="350" cy="360" r="7" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>

                <circle cx="220" cy="230" r="8" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="330" cy="290" r="5" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>

                <circle cx="240" cy="130" r="3" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>
                SVG not supported


This is great, ain't it?


Some text

Let's put up a text greeting in your SVG. It can be something boring, like "Merry Christmas". Here's two lines of text using the text tag. We position the first line on top of the tree, and the second line below.
                <defs>
                    <radialGradient id="ballGradient" cx="50%" cy="50%" r="80%" fx="10%" fy="50%">
                        <stop offset="10%" style="stop-color:rgb(255, 200, 100); stop-opacity:1" />
                        <stop offset="100%" style="stop-color:rgb(255, 50, 0); stop-opacity:1" />
                    </radialGradient>
                </defs>

                <text x="150" y="75">MERRY</text>
                <text x="80" y="455">CHRISTMAS</text>

                <circle cx="250" cy="100" r="5" class="toplight">
                    <animate attributeName="r" from="5" to="6" dur="0.1s" repeatCount="indefinite" />
                </circle>


Then let's style this. I'm going to set the font style and size, and give it a red fill and white outline. Pretty Christmassy, eh?
            .toplight
            {
                stroke: rgba(255, 255, 200, 0.3);
                stroke-width: 15;
                fill: rgba(255, 255, 200, 0.8);
            }

            text
            {
                stroke: rgba(255, 255, 255, 1);
                stroke-width: 1;
                fill: rgba(255, 0, 0, 1);
                font: bold 50px georgia;
            }


Bueno!


Next, we'll try to position this SVG in the middle of the screen. Remember, SVGs can't be directly manipulated to be relatively positioned, so let's encase this entire thing within a div. Style it using the svgContainer class.
        <div id="svgContainer">
            <svg>
                <defs>
                    <radialGradient id="ballGradient" cx="50%" cy="50%" r="80%" fx="10%" fy="50%">
                        <stop offset="10%" style="stop-color:rgb(255, 200, 100); stop-opacity:1" />
                        <stop offset="100%" style="stop-color:rgb(255, 50, 0); stop-opacity:1" />
                    </radialGradient>
                </defs>

                <text x="150" y="75">MERRY</text>
                <text x="80" y="455">CHRISTMAS</text>

                <circle cx="250" cy="100" r="5" class="toplight">
                    <animate attributeName="r" from="5" to="6" dur="0.1s" repeatCount="indefinite" />
                </circle>

                <path d="
                M 250 100
                q 0 10 -30 30 l 10 0
                q 0 10 -30 30 l 10 0
                q 0 20 -30 40 l 10 0
                q 0 20 -40 50 l 10 0
                q 0 30 -40 70 l 10 0
                q 0 40 -40 80 l 320 0
                q -40 -40 -40 -80 l 10 0
                q -40 -30 -40 -70 l 10 0 
                q -30 -20 -40 -50 l 10 0
                q -20 -10 -30 -40 l 10 0
                q -20 -10 -30 -30 l 10 0
                q -20 -10 -30 -30
                " class="tree"/>

                <circle cx="270" cy="150" r="5" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="240" cy="290" r="3" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>

                <circle cx="150" cy="330" r="7" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="290" cy="250" r="5" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>

                <circle cx="220" cy="180" r="6" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="1.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="170" cy="260" r="3" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="1.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="280" cy="320" r="5" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>

                <circle cx="230" cy="380" r="4" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="350" cy="360" r="7" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>

                <circle cx="220" cy="230" r="8" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2.5s" repeatCount="indefinite" />
                </circle>

                <circle cx="330" cy="290" r="5" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>

                <circle cx="240" cy="130" r="3" class="ball">
                    <animate attributeName="stroke-width" from="1" to="10" dur="2s" repeatCount="indefinite" />
                </circle>
                SVG not supported
            </svg>
        </div>


Then write the CSS style. While you're at it, remove the red outline of the SVG.
            body
            {
                background-color: rgba(0, 0, 50, 1);
            }

            #svgContainer
            {
                width: 500px;
                margin: 5em auto 0 auto;
            }

            svg
            {
                width: 500px;
                height: 500px;
                outline: 0px solid #FF0000;
            }


And here your SVG is nicely positioned in the middle of your screen!


Here's a preview of the actual thing...

MERRY CHRISTMAS SVG not supported


Final thoughts

SVGs are fun to write, though it would be foolhardy to totally do without CSS. Maybe some day we'll take a more in-depth look at the different approaches, but for now, it's time to enjoy your holiday season!

Here's to a well-lit Christmas!
T___T

No comments:

Post a Comment