SVG Effect - Growing Flowers

Here’s another animated SVG idea: flowers that “grow” up to some text.

If you missed the animation, or if you want to re-run it with new random values, just refresh the page.

Here’s how I created the effect.

Libraries

I’m using svg.js and svg.filters.js to generate SVG on-the-fly from a little bit of Javascript. Svg.js provides an API for creating and manipulating SVG, and adding it into the DOM. Svg.filters.js adds in support for SVG filters, which I’m using to generate the drop shadows.

Markup

The markup for this effect is very simple, since the bulk of the display is handled in the Javascript.

<link href="https://fonts.googleapis.com/css?family=Bonbon" rel="stylesheet">

<style>

    #svg-container {
        width: 640px;
        height: 340px;
        margin-bottom: 50px;
    }

    @keyframes dash {
        to {
            stroke-dashoffset: 0;
        }
    }

</style>

<div id="svg-container"></div>

I thought the “Bonbon” font nicely complemented the flowers. All the SVG will be added to the single container. The dash keyframes definition will be explained later.

Incidentally, since this is just a demo, there are going to be lots of hard-coded constants everywhere.

Woody and Buzz meme: ‘Constants everywhere’

You have been warned…

Generic functions

The majority of the script is contained within a jQuery ‘ready’ block:

$(function() {

    ...

});

Let’s start with some very generic helper functions.

getPointFromLine returns the position of a point on a line segment, with the position being specified parametrically (passing zero gives the start point, one gives the end point, and intermediate values give intermediate points). All coordinates in this example will be objects with x and y components.

    function pointFromLine(start, end, t) {
        return {
            x: start.x + (end.x - start.x) * t,
            y: start.y + (end.y - start.y) * t
        };
    }

Next, vecAdd adds two (x,y) vectors together:

    function vecAdd(p, q) {
        return {
            x: p.x + q.x,
            y: p.y + q.y
        }
    }

getPerpendicular picks a point on a line, imagines a line perpendicular from the line at that point, and returns the point at the end of the perpendicular line. The point on the line is defined parametrically by t; the perpendicular distance from the line is given by mul - this is a multiple of the length of the original line.

    function getPerpendicular(start, end, t, mul) {
        const lineDx = end.x - start.x;
        const lineDy = end.y - start.y;
        const mag = Math.sqrt(lineDx * lineDx + lineDy * lineDy);
        const lineVectorNormalized = { x: lineDx / mag, y: lineDy / mag};
        const perpendicularOffset = {
            x: -lineVectorNormalized.y * mul * mag,
            y:  lineVectorNormalized.x * mul * mag
        };
        const perpendicularPoint = vecAdd(pointFromLine(start, end, t), perpendicularOffset);

        return perpendicularPoint;
    }

generatePathCurves generates an SVG path specification for a curving path, starting at start, ending at end, with numCurves curves, and spreading outwards specified by spread: values near zero will give little to no curving, and increasing values will give increasingly wide and sharp curves. Basically: this draws a wiggly line.

    function generatePathCurves(start, end, numCurves, spread) {
        const controlPoint = getPerpendicular(start, end, 0.5 / numCurves, spread);

        let path = `M ${start.x} ${start.y} Q ${controlPoint.x} ${controlPoint.y}`;

        for(let i = 1; i <= numCurves; ++i) {
            const linePoint = pointFromLine(start, end, i / numCurves);

            if(i == 1) path += ' , '; else path += ' T ';

            path += `${linePoint.x} ${linePoint.y}`;
        }

        return path;
    }

In the SVG path specification above:

  • M is an absolute move command;
  • Q is an absolute quadratic Bézier curve (specified as control point , end point);
  • T is an absolute quadratic Bézier curve continuation – it looks at the previous curve points and continues a smooth curve.

“Botanical” functions

Next come some routines for drawing bits of flowers.

```pathSpecForLeafOrPetal` generates the SVG path specification for either a leaf or a petal. The difference (apart from the color)? Leaves have a curved line down the middle representing the spine of the leaf; petals do not. Both have the same shape: two lines curving outwards from the start to the end points. Leaves have the extra curved line down the middle, just not as curved as the outsides.

    function pathSpecForLeafOrPetal(draw, start, end, drawSpine, fill, stroke) {
        const cp1 = getPerpendicular(start, end, 0.5, 0.5);
        const cp2 = getPerpendicular(start, end, 0.5, -0.5);
        const cp3 = getPerpendicular(start, end, 0.5, 0.1);
        let pathSpec =
            `M ${start.x} ${start.y} Q ${cp1.x} ${cp1.y} , ${end.x} ${end.y} ` +
            `Q ${cp2.x} ${cp2.y} , ${start.x} ${start.y} `;
        
        if(drawSpine) pathSpec += `Q ${cp3.x} ${cp3.y} , ${end.x} ${end.y}`;

        const path = draw.path(pathSpec);

        path.attr({fill: fill, stroke: stroke});

        return path;
    }

startGrowingFlower is a biggie so let’s take it bit by bit:

    function startGrowingFlower(draw, addAnimation, start, end) {

        ...

    }

The first thing to note is that this function doesn’t draw a flower: instead it starts drawing a flower, and for that we need animation facilities. I’ll describe it in more detail later but for now I’ll just say that the addAnimation parameter is a function taking two parameters: a time offset from now (in milliseconds), and another function. The idea is that if you add an animation function specifying N milliseconds, then in N milliseconds’ time the function will be called.

(There are tons of animation libraries for Javascript that could probably have cleaned up the code, but my needs were so simple for this demo they seemed like overkill).

So: onwards through startGrowingFlower. The first item to be added is the stem:

        const growTime = 1 + Math.random() * 3;
        const numCurves = 1 + Math.ceil(Math.random() * 4);
        let spread = (Math.random() - 0.5) * 0.1;
        spread += Math.sign(spread) * 0.1;
        const stemPath = generatePathCurves(start, end, numCurves, spread);
        const stem = draw.path(stemPath);
        const stemLength = stem.length();

We pick stem attributes such as the time it takes to grow, and the curve parameters, randomly. The stem itself is a curvy path generated using the functions above.

The stem itself is animated using a little SVG trick involving drawing it with a very long dash pattern, which is then animated:

        stem.attr({
            stroke: '#66a914',
            fill: 'none',
            'stroke-width': 5,
            'stroke-dasharray': stemLength,
            'stroke-dashoffset': stemLength,
            'stroke-linecap': 'round'
        }).css({
            animation: `dash ${growTime}s linear forwards`
        });

For more details there’s a nice write-up here.

Next come the two bigger leaves at the bottom, they just pop into existence at fixed time-points:

        addAnimation(800, function() {
            pathSpecForLeafOrPetal(draw, start, vecAdd(start, {x:30, y:-30}), true, '#5dc507', '#3c7e07');
        });
        addAnimation(1800, function() {
            pathSpecForLeafOrPetal(draw, start, vecAdd(start, {x:-30, y:-30}), true, '#5dc507', '#3c7e07');
        });

After those leaves come the smaller leaves that appear as the stem “grows”:

        const numStemLeaves = stemLength / 40;
        let side = 1;

        for(let i = 0; i < numStemLeaves; ++i) {
            const stemT = (i + 1) / numStemLeaves * 0.8;
            const stemPoint = stem.pointAt(stemT * stemLength);
            const sideCopy = side;
            const leafEnd = vecAdd(stemPoint, {x: sideCopy * 30, y: -10});

            addAnimation(stemT * growTime * 1000, function () {
                pathSpecForLeafOrPetal(draw, stemPoint, leafEnd, true, '#5dc507', '#3c7e07');
            });

            side = -side;
        }

Basically, the leaves appear at fixed points along the stem, apart from the last 20% (since the flower itself will be there). Some points to note:

  • Since the side variable is outside the for loop I have to capture a copy of it inside. Mutable variables are a pain…
  • SVG.js gives us methods on paths such as length (that we’ve already used) and pointAt. These are great for querying where the path actually goes, once you have created it.

Next up comes the flower-head itself. Here we get into a bit of an ordering problem: I want the circle in the center to appear first (before the petals), but I want it to display on top of the petals.

        const flowerCenterScale = stemLength / 250;
        const flowerCenterRadius = 15 * flowerCenterScale;
        const flowerInnerRadius = 5 * flowerCenterScale;
        const flowerOuterRadius = 50 * flowerCenterScale;
        let flowerGroup = undefined;
        let petalsGroup = undefined;

        function addFlowerCircle() {
            if(petalsGroup) return;

            flowerGroup = draw.group();
            petalsGroup = flowerGroup.group();

            flowerGroup.filterWith(function(add) {
                const blur = add.offset(0, 1).in(add.$sourceAlpha).gaussianBlur(1);

                add.blend(add.$source, blur)
            });

            const flowerCircle = flowerGroup.circle(flowerCenterRadius * 2);
        
            flowerCircle.move(end.x - flowerCenterRadius, end.y - flowerCenterRadius);
            flowerCircle.attr({fill: '#fff', stroke: '#a00'});
        }

addFlowerCircle is written in such a way that it can be called multiple times, but it will only create the groups and the circle itself on the first invocation. It is called for the circle itself…

        addAnimation(growTime * 1000, function () {
            addFlowerCircle();
        });

… but it is also called by the petals animation, just to be sure everything is in place:

        let direction = Math.random() < 0.5 ? 1 : -1;

        for(let angle = 0; angle < Math.PI * 2.0; angle += Math.PI * 2.0 / 10.0) {
            const sa = Math.sin(angle * direction);
            const ca = Math.cos(angle * direction);

            const petalStart = { x: end.x + sa * flowerInnerRadius, y: end.y + ca * flowerInnerRadius };
            const petalEnd = { x: end.x + sa * flowerOuterRadius, y: end.y + ca * flowerOuterRadius };

            addAnimation(growTime * 1000 + angle * 200, function() {
                addFlowerCircle();
                pathSpecForLeafOrPetal(petalsGroup, petalStart, petalEnd, false, '#ff4f38', '#fff');
            });
        }

Petals are generated either clockwise or anti-clockwise, just to add a bit of variety.

That’s the end of startGrowingFlower. We’ll move on to the main body of code next.

Overall display

The display needs a background so very simple sky and earth components are added:

    const width = 640;
    const height = 340;

    const draw = SVG().addTo('#svg-container').size(width, height);

    const skyFill = draw.gradient('radial', function(add) {
        add.stop(0, '#5c92ff');
        add.stop(0.3, '#b8cfff');
        add.stop(1, '#010b6b');
    }).attr({cx: 0.5, cy: 1.0, r: 1});

    const rect = draw.rect(width, height).attr({fill: skyFill});

    const groundPathTop = generatePathCurves(
        {x: -10, y: height*0.75}, {x: width + 10, y: height*0.75}, 7 , 0.02
    );

    const groundPath = `${groundPathTop} V ${height+10} H 0 Z`;

    const groundFill = draw.gradient('linear', function(add) {
        add.stop(0, '#602e16');
        add.stop(1, '#391604');
    }).from(0, 0).to(0, 1);

    const ground = draw.path(groundPath);
    
    ground.attr({fill: groundFill, stroke: '#12910d', 'stroke-width': 8});

Nothing terribly exciting there really.

Next comes a group for the flowers to be placed in. I called this behindGroup because I was originally planning to have some flowers in front of the text as well, but I ditched that idea. I give a little drop-shadow to this group just to make the flowers stand out a bit.

    const behindGroup = draw.group();

    behindGroup.filterWith(function(add) {
        const blur = add.offset(0, 1).in(add.$sourceAlpha).gaussianBlur(1);

        add.blend(add.$source, blur)
    });

On top of the flowers is the text, again, nothing particularly thrilling here:

    const heading = draw.text('Let it grow...').font({
        family: 'Bonbon, cursive',
        size: 84,
        anchor: 'middle',
        x: width*0.5,
        y: -30
    }).attr({fill: '#fff', stroke: '#000', 'stroke-width': 0.8});

    heading.filterWith(function(add) {
        const blur = add.offset(0, 2).in(add.$sourceAlpha).gaussianBlur(5);

        add.blend(add.$source, blur)
    });

Now comes the animation management. Earlier I made use of an addAnimation function that was passed in to the other flower growing functions. Well here it is:

    let animationList = [];

    let addAnimation = function (futureMilliseconds, fn) {
        animationList.push({ when: performance.now() + futureMilliseconds, fn: fn });

        if(animationList.length >= 2) {
            if(animationList[animationList.length-1].when < animationList[animationList.length-2].when) {
                animationList.sort(function(a,b){
                    if(a.when === b.when) return 0;
                    else if(a.when < b.when) return -1;
                    else return 1;
                });
            }
        }
    }

The animations are just a list of { when, fn } entries. The entries are kept ordered from the ones to be applied first, to the ones to be applied last. The ordering is maintained with a simple check: if the one being pushed onto the end of the list is later than the previous end of list then everything is good; but if it is earlier then the list needs re-sorting.

The flowers themselves are started growing by adding their functions into the animation list:

    for(let x = 70; x <= width - 60; x += 100) {
        addAnimation(Math.random() * 5000, function() {
            const xDirectionOffset = (Math.random() - 0.5) * 100;
            const yStart = height - Math.random() * 50;
            const yEnd = 90 + Math.random() * 100;

            startGrowingFlower(behindGroup, addAnimation, {x: x, y: yStart}, {x: x + xDirectionOffset, y: yEnd});
        });
    }

Once I had everything coming together I thought the final result looked a bit like a postcard, so I decided to go with that theme by making the edges appear shaped using lots of white circles:

    const edgeCircleSize = 15;

    for(let x = 0; x < width; x += edgeCircleSize) {
        draw.circle(edgeCircleSize).move(x, -edgeCircleSize / 2).attr({fill: '#fff'});
        draw.circle(edgeCircleSize).move(x, height - edgeCircleSize / 2).attr({fill: '#fff'});
    }
    for(let y = 0; y < height; y += edgeCircleSize) {
        draw.circle(edgeCircleSize).move(-edgeCircleSize / 2, y).attr({fill: '#fff'});
        draw.circle(edgeCircleSize).move(width - edgeCircleSize / 2, y).attr({fill: '#fff'});
    }

Finally, a timer is started to run the animations:

    let timerId = setInterval(function() {
        const now = performance.now();

        if(animationList.length === 0) {
            clearInterval(timerId);
        }

        while(animationList.length > 0) {
            const first = animationList[0];

            if (now > first.when) {
                animationList.shift();

                first.fn(draw, addAnimation);
            } else {
                break;
            }
        }
    }, 20);

On each tick, it does the following:

  • If the animations list is empty, then the timer is cancelled – all animations have completed and we no longer need the timer.
  • If not empty, then the animation to be run next is peeked at:
    • If we’ve gone past the time the animation should be run, then pop it off, run the animation, and keep looking at the remainder of the list;
    • Otherwise, exit the timer and wait for the next interval.

Ideas for improvements

I’m sure there are plenty of ways this effect could be improved. Here’s a few ideas:

  • As items are added (for example leaves), there could be a brief animation showing them growing or popping into place;
  • The flowers could be ordered according to “distance”. Flowers that were closer would be bigger, on top of those further away, and would be positioned further down the screen.
  • You could have different kinds of flowers – different styles, colors, or growing patterns.
  • You could tidy up all the constants and make this size-responsive and themeable.

Final thoughts

You’re welcome to use the code presented on this page in your own projects, with no need for attribution. (I’d recommend cleaning it up a bit first however…)

If you do make use of this code and improve it, why not post a comment below? I’d love to see this effect being used for real, and see how you improved it.

Published: Friday, August 21, 2020

Pinterest
Reddit
Hacker News

You may be interested in...

Hackification.io is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to amazon.com. I may earn a small commission for my endorsement, recommendation, testimonial, and/or link to any products or services from this website.

Comments? Questions?