Sunday 12 February 2023

Web Tutorial: Valentine's Day Animated SVG

Valentine's Day is here in a couple days. As tradition demands, I've got a nice web tutorial for an animated SVG.

We begin with some boilerplate HTML with an SVG parked in the middle. The div has been given a red outline because the boundary will be important for us to see where things begin and end.
<!DOCTYPE html>
<html>
    <head>
        <title>Happy Valentine's Day!</title>

        <style>
            #svgContainer
            {
                width: 500px;
                margin: 5em auto 0 auto;
        outline: 1px solid red;
            }

            svg
            {
                width: 500px;
                height: 500px;
            }
        </style>
    </head>

    <body>
        <div id="svgContainer">
            <svg>
                SVG not supported
            </svg>
        </div>
    </body>
</html>


Start off with something simple. Two text tags.
<svg>
    <text x="155" y="60" class="message">HAPPY</text>
    <text x="70" y="100" class="message">VALENTINE'S!</text>

    SVG not supported
</svg>


Style them here. I'm making them a nice pink.
<style>
    #svgContainer
    {
        width: 500px;
        margin: 5em auto 0 auto;
    }

    svg
    {
        width: 500px;
        height: 500px;
    }

    .message
    {
        fill: rgb(255, 200, 200);
        font: bold 50px verdana;
        textLength: 100%;
    }    

</style>


Nothing too elaborate there.




Let's get to the heart (heh heh) of the matter. We will make an SVG heart using the path tag and some curves. We will preemptively style this to have a black background.
svg
{
    width: 500px;
    height: 500px;
}

.heart
{
    fill: black;
}    
    

.message
{
    fill: rgb(255, 200, 200);
    font: bold 50px verdana;
    textLength: 100%;
}    


Now with the d attribute, start in the horizontal middle of the SVG, and maybe a third of the height vertically. Don't forget to style it using the CSS class heart.
<svg>
    <text x="155" y="60" class="message">HAPPY</text>
    <text x="70" y="100" class="message">VALENTINE'S!</text>
    
    <path d="
    M 250 200
    Z"  class="heart"/>


    SVG not supported
</svg>


Move to the left.
<path d="
M 250 200
q -100 0
Z" class="heart"/>


Apply a curve upwards. Vertically, we tug the line upwards by 60 pixels, and horizontally left by 50 pixels.
<path d="
M 250 200
q -50 -60, -100 0
Z" class="heart"/>


See where this is going?




Now go back right to the middle, and way down.
<path d="
M 250 200
q -50 -60, -100 0
q 100 150
Z" class="heart"/>


The curve is applied. Go left 50 pixels and down 80 pixels. I had to experiment with different values to get it just right.
<path d="
M 250 200
q -50 -60, -100 0
q -50 80, 100 150
Z" class="heart"/>


And we have half a heart!




Move right and up.
<path d="
M 250 200
q -50 -60, -100 0
q -50 80, 100 150
q 100 -150
Z" class="heart"/>


The curve to apply here is tricky, and you really have to understand your curves well. You have to tug the line right 150 pixels. And because you're moving from a lower position to a higher position, the vertical offset is negative. Hard for me to explain, but try it yourself!
<path d="
M 250 200
q -50 -60, -100 0
q -50 80, 100 150
q 150 -70, 100 -150
Z" class="heart"/>


Three quarters done!




Back to the origin, with one last curve.
<path d="
M 250 200
q -50 -60, -100 0
q -50 80, 100 150
q 150 -70, 100 -150
q -50 -60, -100 0
Z" class="heart"/>


Now you got a heart.




Let's fill this in! We want a 3D effect, so we style it with a gradient. The gradient will be called heartGradient.
.heart
{
    fill: url('#heartGradient');
}    


Start a defs tag and input the gradient here.
<svg>
    <defs>
        <radialGradient id="heartGradient">

        </radialGradient>
    </defs>


    <text x="155" y="60" class="message">HAPPY</text>
    <text x="70" y="100" class="message">VALENTINE'S!</text>


It begins with a very light pink, at the top right...
<radialGradient id="heartGradient" cx="10%" cy="10%" r="90%" fx="10%" fy="10%">
    <stop offset="0%" style="stop-color:rgb(255, 200, 200); stop-opacity:1" />
</radialGradient>


...to red...
<radialGradient id="heartGradient" cx="10%" cy="10%" r="90%" fx="10%" fy="10%">
    <stop offset="0%" style="stop-color:rgb(255, 200, 200); stop-opacity:1" />
    <stop offset="20%" style="stop-color:rgb(255, 0, 0); stop-opacity:1" />
</radialGradient>


...to brown and then dark brown. The percentages ensure that red takes up the majority. Opacity is 1 all the way.
<radialGradient id="heartGradient" cx="10%" cy="10%" r="90%" fx="10%" fy="10%">
    <stop offset="0%" style="stop-color:rgb(255, 200, 200); stop-opacity:1" />
    <stop offset="20%" style="stop-color:rgb(255, 0, 0); stop-opacity:1" />
    <stop offset="90%" style="stop-color:rgb(200, 0, 0); stop-opacity:1" />
    <stop offset="100%" style="stop-color:rgb(150, 0, 0); stop-opacity:1" />

</radialGradient>


Beautiful!




Now let's apply a shadow. Add an ellipse below the heart. It will be wide and flat, and styled using the shadow CSS class.
<svg>
    <defs>
        <radialGradient id="heartGradient" cx="10%" cy="10%" r="90%" fx="10%" fy="10%">
            <stop offset="0%" style="stop-color:rgb(255, 200, 200); stop-opacity:1" />
            <stop offset="20%" style="stop-color:rgb(255, 0, 0); stop-opacity:1" />
            <stop offset="90%" style="stop-color:rgb(200, 0, 0); stop-opacity:1" />
            <stop offset="100%" style="stop-color:rgb(150, 0, 0); stop-opacity:1" />
        </radialGradient>
    </defs>

    <text x="155" y="60" class="message">HAPPY</text>
    <text x="70" y="100" class="message">VALENTINE'S!</text>

    <path d="
    M 250 200
    q -50 -60, -100 0
    q -50 80, 100 150
    q 150 -70, 100 -150
    q -50 -60, -100 0
    Z" class="heart"/>

    <ellipse cx="250" cy="400" rx="120" ry="10" class="shadow" />

    SVG not supported
</svg>


The fill will be shadowGradient.
.heart
{
    fill: url('#heartGradient');
}    

.shadow
{
    fill: url('#shadowGradient');
}


.message
{
    fill: rgb(255, 200, 200);
    font: bold 50px verdana;
    textLength: 100%;
}


And this gradient is simple - grey to white, but with 0 opacity at the white part.
<defs>
    <radialGradient id="heartGradient" cx="10%" cy="10%" r="90%" fx="10%" fy="10%">
        <stop offset="0%" style="stop-color:rgb(255, 200, 200); stop-opacity:1" />
        <stop offset="20%" style="stop-color:rgb(255, 0, 0); stop-opacity:1" />
        <stop offset="90%" style="stop-color:rgb(200, 0, 0); stop-opacity:1" />
        <stop offset="100%" style="stop-color:rgb(150, 0, 0); stop-opacity:1" />
    </radialGradient>

    <radialGradient id="shadowGradient" cx="50%" cy="50%" r="100%" fx="50%" fy="50%">
        <stop offset="0%" style="stop-color:rgb(100, 100, 100); stop-opacity:1" />
        <stop offset="50%" style="stop-color:rgb(100, 100, 100); stop-opacity:0" />
    </radialGradient>

</defs>


Here's your shadow!




Now we're going to place circle tags before and after the heart. So some will appear behind and some will appear in front. The CSS class is mote.
<circle class="mote"></circle>
<circle class="mote"></circle>
<circle class="mote"></circle>
<circle class="mote"></circle>
<circle class="mote"></circle>
<circle class="mote"></circle>
<circle class="mote"></circle>


<path d="
M 250 200
q -50 -60, -100 0
q -50 80, 100 150
q 150 -70, 100 -150
q -50 -60, -100 0
Z" class="heart"/>

<circle class="mote"></circle>
<circle class="mote"></circle>
<circle class="mote"></circle>
<circle class="mote"></circle>
<circle class="mote"></circle>
<circle class="mote"></circle>
<circle class="mote"></circle>


All of them will be filled in using moteGradient.
.heart
{
    fill: url('#heartGradient');
}    

.shadow
{
    fill: url('#shadowGradient');
}    

.message
{
    fill: rgb(255, 200, 200);
    font: bold 50px verdana;
    textLength: 100%;
}    

.mote
{
    fill: url('#moteGradient');
}


moteGradient is from pink to white, again with opacity 0 at the white part.
<defs>
    <radialGradient id="heartGradient" cx="10%" cy="10%" r="90%" fx="10%" fy="10%">
        <stop offset="0%" style="stop-color:rgb(255, 200, 200); stop-opacity:1" />
        <stop offset="20%" style="stop-color:rgb(255, 0, 0); stop-opacity:1" />
        <stop offset="90%" style="stop-color:rgb(200, 0, 0); stop-opacity:1" />
        <stop offset="100%" style="stop-color:rgb(150, 0, 0); stop-opacity:1" />
    </radialGradient>

    <radialGradient id="shadowGradient" cx="50%" cy="50%" r="100%" fx="50%" fy="50%">
        <stop offset="0%" style="stop-color:rgb(100, 100, 100); stop-opacity:1" />
        <stop offset="50%" style="stop-color:rgb(100, 100, 100); stop-opacity:0" />
    </radialGradient>

    <radialGradient id="moteGradient" cx="50%" cy="50%" r="100%" fx="50%" fy="50%">
        <stop offset="0%" style="stop-color:rgb(255, 200, 200); stop-opacity:1" />
        <stop offset="50%" style="stop-color:rgb(255, 200, 200); stop-opacity:0" />
    </radialGradient>

</defs>


Each of these will have different positions and sizes.
<circle cx="190" cy="370" r="5" class="mote"></circle>
<circle cx="220" cy="350" r="8" class="mote"></circle>
<circle cx="260" cy="380" r="10" class="mote"></circle>
<circle cx="290" cy="390" r="5" class="mote"></circle>
<circle cx="2950" cy="380" r="3" class="mote"></circle>
<circle cx="300" cy="370" r="10" class="mote"></circle>
<circle cx="330" cy="350" r="8" class="mote"></circle>

<path d="
M 250 200
q -50 -60, -100 0
q -50 80, 100 150
q 150 -70, 100 -150
q -50 -60, -100 0
Z" class="heart"/>

<circle cx="190" cy="370" r="5" class="mote"></circle>
<circle cx="220" cy="350" r="8" class="mote"></circle>
<circle cx="260" cy="380" r="10" class="mote"></circle>
<circle cx="290" cy="390" r="5" class="mote"></circle>
<circle cx="295" cy="380" r="3" class="mote"></circle>
<circle cx="300" cy="370" r="10" class="mote"></circle>
<circle cx="330" cy="350" r="8" class="mote"></circle>


Here's what it should look like now.




Now to animate this shit! The animate tag should be inside each of these. For starters, the repeatCount property should be set to indefinite.
<circle cx="190" cy="370" r="5" class="mote">
    <animate repeatCount="indefinite" />
</circle>
<circle cx="220" cy="350" r="8" class="mote">
    <animate repeatCount="indefinite" />
</circle>
<circle cx="260" cy="380" r="10" class="mote">
    <animate repeatCount="indefinite" />
</circle>
<circle cx="290" cy="390" r="5" class="mote">
    <animate repeatCount="indefinite" />
</circle>
<circle cx="295" cy="380" r="3" class="mote">
    <animate repeatCount="indefinite" />
</circle>
<circle cx="300" cy="370" r="10" class="mote">
    <animate repeatCount="indefinite" />                    
</circle>
<circle cx="330" cy="350" r="8" class="mote">
    <animate repeatCount="indefinite" />
</circle>


<path d="
M 250 200
q -50 -60, -100 0
q -50 80, 100 150
q 150 -70, 100 -150
q -50 -60, -100 0
Z" class="heart"/>

<circle cx="150" cy="370" r="10" class="mote">
    <animate repeatCount="indefinite" />
</circle>
<circle cx="200" cy="350" r="8" class="mote">
    <animate repeatCount="indefinite" />
</circle>
<circle cx="250" cy="380" r="5" class="mote">
    <animate repeatCount="indefinite" />
</circle>
<circle cx="270" cy="390" r="5" class="mote">
    <animate repeatCount="indefinite" />
</circle>
<circle cx="300" cy="380" r="8" class="mote">
    <animate repeatCount="indefinite" />
</circle>
<circle cx="310" cy="370" r="10" class="mote">
    <animate repeatCount="indefinite" />
</circle>
<circle cx="350" cy="350" r="5" class="mote">
    <animate repeatCount="indefinite" />
</circle>


Each of these animations will move the circle upwards. But how far upwards (depending on the to attribute), how long the animation takes and the delay at the start, should all vary.
<circle cx="190" cy="370" r="5" class="mote">
    <animate attributeName="cy" from="370" to="50" dur="30s" begin="0s" repeatCount="indefinite" />
</circle>
<circle cx="220" cy="350" r="8" class="mote">
    <animate attributeName="cy" from="350" to="10" dur="30s" begin="0.5s" repeatCount="indefinite" />
</circle>
<circle cx="260" cy="380" r="10" class="mote">
    <animate attributeName="cy" from="380" to="50" dur="8s" begin="0.5s" repeatCount="indefinite" />
</circle>
<circle cx="290" cy="390" r="5" class="mote">
    <animate attributeName="cy" from="390" to="40" dur="15s" begin="0s" repeatCount="indefinite" />
</circle>
<circle cx="2950" cy="380" r="3" class="mote">
    <animate attributeName="cy" from="380" to="50" dur="8s" begin="0.5s" repeatCount="indefinite" />
</circle>
<circle cx="300" cy="370" r="10" class="mote">
    <animate attributeName="cy" from="370" to="50" dur="30s" begin="0.5s" repeatCount="indefinite" />                    
</circle>
<circle cx="330" cy="350" r="8" class="mote">
    <animate attributeName="cy" from="350" to="10" dur="15s" begin="0.5s" repeatCount="indefinite" />
</circle>


<path d="
M 250 200
q -50 -60, -100 0
q -50 80, 100 150
q 150 -70, 100 -150
q -50 -60, -100 0
Z" class="heart"/>

<circle cx="150" cy="370" r="10" class="mote">
    <animate attributeName="cy" from="370" to="30" dur="15s" begin="0s" repeatCount="indefinite" />
</circle>
<circle cx="200" cy="350" r="8" class="mote">
    <animate attributeName="cy" from="350" to="50" dur="30s" begin="0s" repeatCount="indefinite" />
</circle>
<circle cx="250" cy="380" r="5" class="mote">
    <animate attributeName="cy" from="380" to="10" dur="8s" begin="0.5s" repeatCount="indefinite" />
</circle>
<circle cx="270" cy="390" r="5" class="mote">
    <animate attributeName="cy" from="390" to="50" dur="30s" begin="0s" repeatCount="indefinite" />
</circle>
<circle cx="300" cy="380" r="8" class="mote">
    <animate attributeName="cy" from="380" to="30" dur="8s" begin="0.5s" repeatCount="indefinite" />
</circle>
<circle cx="310" cy="370" r="10" class="mote">
    <animate attributeName="cy" from="370" to="50" dur="15s" begin="0s" repeatCount="indefinite" />
</circle>
<circle cx="350" cy="350" r="5" class="mote">
    <animate attributeName="cy" from="350" to="50" dur="15s" begin="0s" repeatCount="indefinite" />
</circle>


Remove the red outline.
#svgContainer
{
    width: 500px;
    margin: 5em auto 0 auto;
    outline: 0px solid red;
}


Have you ever seen anything more amazing?!




Love is the answer!

There you go, just a simple web tutorial. But so pretty. I had fun, hope you did too. Now go forth and be romantic, lovebirds!
Heartening, isn't it?
T___T

No comments:

Post a Comment