The SVG Plugin for KUTE.js extends the core engine and enables animation for various SVG specific CSS properties, as well as SVG morphing of path shapes. We'll dig into this in great detail as well as provide valuable tips on how to configure your animation for best performance and visual aesthetics. The SVG Plugin is very light, maybe one of the lightest out there, still, you will find it to be very powerful and flexible.
Keep in mind that older browsers like Internet Explorer 8 and below as well as stock browser from Android 4.3 and below do not support inline SVG so make sure to fiter out your SVG tweens.
One of the most important parts of the plugin is the SVG morphing capability. It only applies to inline <path>
and <glyph>
SVG elements, with closed shapes (their d
attribute ends with z
). On initialization or animation start, depending on the chosen KUTE.js method, it will sample a number of points along the two paths based on a default / given sample size and will create two arrays with these points, the arrays that we need for interpolation. Further more, with a set of options we can then rearrange / reverse these arrays to optimize and / or maximize the visual effect of the morph:
true
the script will log valuable information about the morph such as default/current sample size, number of points based on sample size, the recommended index for points rotation, or if one of the shapes require to be reversed. By default this option is false
for performance reasons. This option will also show you the first point for both shapes so you can visualize how the settings affect the morph.true
this option allows you to reverse the draw direction of the FIRST shape. By default this option is false
.true
this option allows you to reverse the draw direction of the SECOND shape. By default this option is also false
.In the first morph example we are going to go through some basic steps on how to setup and how to improve the morph animation. Our demo is a morph from a rectangle into a star, so first let's create an SVG element with two paths, first is going to be visible, filled with color, while second is going to be hidden. The first path is the start shape and the second is the end shape, you guessed it, and we can also add some ID to the paths so we can easily target them with our code.
<svg id="morph-example" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">
<path id="rectangle" class="bg-lime" d="M38.01,5.653h526.531c17.905,0,32.422,14.516,32.422,32.422v526.531 c0,17.905-14.517,32.422-32.422,32.422H38.01c-17.906,0-32.422-14.517-32.422-32.422V38.075C5.588,20.169,20.104,5.653,38.01,5.653z"/>
<path id="star" style="visibility:hidden" d="M301.113,12.011l99.25,179.996l201.864,38.778L461.706,380.808 l25.508,203.958l-186.101-87.287L115.01,584.766l25.507-203.958L0,230.785l201.86-38.778L301.113,12.011"/>
</svg>
Now we can apply both .to()
and fromTo()
methods:
// the fromTo() method
var tween = KUTE.fromTo('#rectangle', {path: '#rectangle' }, { path: '#star' }).start();
// OR
// the to() method will take the path's d attribute value and use it as start value
var tween = KUTE.to('#rectangle', { path: '#star' }).start();
// OR
// simply pass in a valid path string without the need to have two paths in your SVG
var tween = KUTE.to('#rectangle', { path: 'M301.113,12.011l99.25,179.996l201.864,38.778L461.706,380.808l25.508,203.958l-186.101-87.287L115.01,584.766l25.507-203.958L0,230.785l201.86-38.778L301.113,12.011' }).start();
For all the above tween objects the animation should look like this:
As you can see, the animation could need some fine tunning. Let's open the console, and this time we'll pass in the showMorphInfo: true
tween option that will help us find the best possible morph as performance and visual. Have a look:
// let's check the morph info again
var tween = KUTE.to('#rectangle', { path: '#star' }, {showMorphInfo: true}).start();
// The console log should show you this
/* ------------------------------------
KUTE.js Path Morph Log
The morph used 92 points to draw both paths based on 25 morphPrecision value.
You may also consider a morphIndex for the second path. Currently the best index seems to be 79.
If the current animation is not satisfactory, consider reversing one of the paths. Maybe the paths do not intersect or they really have different draw directions.
*/
Next, we're going to set the morphIndex: 79
option and we will get an improved morph. I also made a pen for you to play with.
Much better! You can play with the morphIndex
value, maybe you can get an even better or more interesting morph. Also notice the above shapes have some points indicating the start for each shape, making it even easier for you to improve the morph, a nice addition to the showMorphInfo
option.
When your paths are only lineto
, vertical-lineto
and horizontal-lineto
based shapes (the d
attribute consists of L
, V
and H
path commands), the SVG Plugin will work differently: it will use their points instead of sampling new ones. As a result, we boost the visual and maximize the performance. The morphPrecision
option will not apply since the paths are already polygons, still you will have access to all the other options.
// let's morph a triangle into a star
var tween1 = KUTE.to('#triangle', { path: '#star' }).start();
// or same path into a square
var tween2 = KUTE.to('#triangle', { path: '#square' }).start();
In the example below the triangle shape will morph into a square, then the square will morph into a star, so 2 tweens chained with a third that will morph back to the original triangle shape. For each tween the morph will use the number of points from the shape with most points as a sample size for the other shape. Let's have a look at the demo.
The morph for polygon paths is the best morph in terms of performance so it's worth keeping that in mind. Also using paths with only L
path command will make sure to prevent value processing and allow the animation to start as fast as possible.
In other cases, you may want to morph paths that have subpaths. Let's have a look at the following paths:
<svg id="multi-morph-example" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">
<path d="M206.115,255.957c-23.854-12.259-47.043-18.479-68.94-18.479c-2.978,0-5.976,0.09-8.974,0.354 c-27.94,2.312-53.461,9.684-69.875,15.414c-4.354,1.599-8.817,3.288-13.415,5.152L0,414.096 c30.851-11.416,58.146-16.969,83.135-16.969c40.423,0,69.764,15.104,93.996,30.652c11.481-38.959,39.022-133.045,47.241-161.162 C218.397,262.975,212.334,259.332,206.115,255.957z
M264.174,295.535l-45.223,157.074c13.416,7.686,58.549,32.024,93.105,32.024 c27.896,0,59.127-7.147,95.417-21.896l43.179-150.988c-29.316,9.461-57.438,14.26-83.732,14.26 C318.945,326.01,285.363,310.461,264.174,295.535z
M146.411,184.395c38.559,0.399,67.076,15.104,90.708,30.251l46.376-158.672c-9.772-5.598-35.403-19.547-53.929-24.3c-12.193-2.842-25.01-4.308-38.602-4.308c-25.898,0.488-54.194,6.973-86.444,19.9 L60.3,202.564c32.404-12.218,60.322-18.17,86.043-18.17C146.366,184.395,146.411,184.395,146.411,184.395L146.411,184.395z
M512,99.062c-29.407,11.416-58.104,17.233-85.514,17.233c-45.844,0-79.646-15.901-101.547-31.183L278.964,244.23 c30.873,19.854,64.146,29.939,99.062,29.939c28.474,0,57.97-6.84,87.73-20.344l-0.091-1.111l1.867-0.443L512,99.062z"/>
<path d="M0.175 256l-0.175-156.037 192-26.072v182.109z
M224 69.241l255.936-37.241v224h-255.936z
M479.999 288l-0.063 224-255.936-36.008v-187.992z
M192 471.918l-191.844-26.297-0.010-157.621h191.854z"/>
</svg>
As you can see, both these paths have subpaths, and KUTE.js will only animate the first of both in this case. To animate them all, we need to break them into multiple paths, so we can handle each path morph properly.
<svg id="multi-morph-example" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">
<path id="w11" d="M206.115,255.957c-23.854-12.259-47.043-18.479-68.94-18.479c-2.978,0-5.976,0.09-8.974,0.354 c-27.94,2.312-53.461,9.684-69.875,15.414c-4.354,1.599-8.817,3.288-13.415,5.152L0,414.096 c30.851-11.416,58.146-16.969,83.135-16.969c40.423,0,69.764,15.104,93.996,30.652c11.481-38.959,39.022-133.045,47.241-161.162 C218.397,262.975,212.334,259.332,206.115,255.957z"/>
<path id="w12" d="M264.174,295.535l-45.223,157.074c13.416,7.686,58.549,32.024,93.105,32.024 c27.896,0,59.127-7.147,95.417-21.896l43.179-150.988c-29.316,9.461-57.438,14.26-83.732,14.26 C318.945,326.01,285.363,310.461,264.174,295.535z"/>
<path id="w13" d="M146.411,184.395c38.559,0.399,67.076,15.104,90.708,30.251l46.376-158.672c-9.772-5.598-35.403-19.547-53.929-24.3c-12.193-2.842-25.01-4.308-38.602-4.308c-25.898,0.488-54.194,6.973-86.444,19.9 L60.3,202.564c32.404-12.218,60.322-18.17,86.043-18.17C146.366,184.395,146.411,184.395,146.411,184.395L146.411,184.395z"/>
<path id="w14" d="M512,99.062c-29.407,11.416-58.104,17.233-85.514,17.233c-45.844,0-79.646-15.901-101.547-31.183L278.964,244.23 c30.873,19.854,64.146,29.939,99.062,29.939c28.474,0,57.97-6.84,87.73-20.344l-0.091-1.111l1.867-0.443L512,99.062z"/>
<path id="w21" style="visibility:hidden" d="M0.175 256l-0.175-156.037 192-26.072v182.109z"/>
<path id="w22" style="visibility:hidden" d="M224 69.241l255.936-37.241v224h-255.936z"/>
<path id="w23" style="visibility:hidden" d="M479.999 288l-0.063 224-255.936-36.008v-187.992z"/>
<path id="w24" style="visibility:hidden" d="M192 471.918l-191.844-26.297-0.010-157.621h191.854z"/>
</svg>
After a close inspection we determined that paths are not ordered the same so it seems we need to tween the paths in a way that their points travel the least possible distance, as follows: #w11
to #w24
, #w13
to #w21
, #w14
to #w22
and #w12
to #w23
.
Now we can write the tween objects and get to working:
var multiMorph1 = KUTE.to('#w11', { path: '#w24' }).start();
var multiMorph2 = KUTE.to('#w13', { path: '#w21' }).start();
var multiMorph3 = KUTE.to('#w14', { path: '#w22' }).start();
var multiMorph3 = KUTE.to('#w12', { path: '#w23' }).start();
As you can imagine, it's quite hard if not impossible to code something that would do all this work automatically, so after a minute or two tweaking the options, here's what we should see:
Note that this final touch required using reverseSecondPath:true
option for all tweens because each shape have a slightly different position from its corresponding shape, so make sure to check the svg.js for a full code review.
The last morph example is a bit more complex as the paths have subpaths with different positions and other important differences such as having different amounts of subpaths as well as significant differences of their positions. In this case you have to manually clone one or more paths in a way that the number of starting shapes is equal to the number of ending shapes, as well as making sure the starting shapes are close to their corresponding end shapes; at this point you should be just like in the previous example.
An important aspect of multi path morph is syncronization: since the .to()
method will prepare the paths for interpolation at animation start, and this usually takes a bit of time, the problem can be easily solved as always using the .fromTo()
method. So, let's get into it:
// complex multi morph, the paths should be self explanatory
var morph1 = KUTE.fromTo('#start-container', { path: '#start-container' }, { path: '#end-container' });
var morph2 = KUTE.fromTo('#startpath1', { path: '#startpath1' }, { path: '#endpath1' });
var morph3 = KUTE.fromTo('#startpath1-clone', { path: '#startpath1-clone' }, { path: '#endpath1' });
var morph4 = KUTE.fromTo('#startpath2', { path: '#startpath2' }, { path: '#endpath2' });
As with the previous example, you should change which path will morph to which path so that their points travel the least possible distance and the morph animation looks visually appealing.
While there are other tools such as SVGMorpheus to enable this kind of multi-path morph, they lack in options to improve the visual and performance. The demos look acceptable in most cases, but the SVGs were manually prepared/optimized which makes it pretty much unusable on a broader scope. The SVG Plugin for KUTE.js uses approximatelly the same algorithm as D3.js for determining the coordinates for tween, it's super light, it's a better solution.
morphPrecision
value (1-10) would be required, assuming performant hardware are powering the displays. For small displays you can get quite comfortable with almost any value, including the default value.lineto
path commands are best for performance.showMorphInfo:true
tween option to check how the values required for the morph change with every new option value, but never forget to disable it after you have optimized the morph to your liking, this option enables a function that detects the best index for points rotation that is very expensive and delays the animation for quite some time..to()
and .fromTo()
methods, but the ones that use the second method will start faster, because the values have been prepared already and for the first method the processing of the two paths happens on tween start delaying the animation, so keep that in mind when working with syncing multiple tweens, the .to()
based morph will always start later. Of course this assumes the you cache the tween objects first and start the animation later, if not (you start the animation on object creation), both methods will be delayed.Next, we're going to animate the stroke of a <path>
element, as this type of animation only works with this kind of SVG elements because it's the only one that supports the .getTotalLength()
method. Here some code examples:
// draw the stroke from 0-10% to 90-100%
var tween1 = KUTE.fromTo('selector1',{draw:'0% 10%'}, {draw:'90% 100%'});
// draw the stroke from zero to full path length
var tween2 = KUTE.fromTo('selector1',{draw:'0% 0%'}, {draw:'0% 100%'});
// draw the stroke from full length to 50%
var tween3 = KUTE.fromTo('selector1',{draw:'0% 100%'}, {draw:'50% 50%'});
We're gonna chain these tweens and start the animation real quick.
Remember: the draw
property also accepts absolute values, eg. draw: '0 150'
; the .to()
method takes 0% 100%
as start value for your tweens when stroke-dasharray
and stroke-dashoffset
are not set.
As you probably noticed in the above examples we've animated the background color for some of the shapes, that is fill
, one of the properties supported by the SVG Plugin, so let's create some tweens real quick:
// fill HEX/RGBa
var tween1 = KUTE.to('selector', {fill: '#069'});
// fillOpacity Number 0-1
var tween2 = KUTE.to('selector',{fillOpacity: 0.2});
// stroke HEX/RGBa
var tween3 = KUTE.to('selector',{stroke: 'rgba(00,66,99,0.8)'});
// strokeOpacity Number 0-1
var tween4 = KUTE.to('selector',{strokeOpacity: 0.6});
// strokeWidth Number
var tween5 = KUTE.to('selector',{strokeWidth: 10});
A quick demo with the above:
Now let's have a look at gradients, here we can animate the stopColor
defined within the SVG's <linearGradient>
element.
<linearGradient id="gradient1" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color: #ffd626; stop-opacity:1"></stop>
<!-- our tween object targets the element below -->
<stop id="myStopColor" offset="100%" style="stop-color: #FF5722; stop-opacity:1"></stop>
</linearGradient>
// stopColor HEX/RGBa
var tween6 = KUTE.to('#myStopColor',{stopColor: 'rgb(00,66,99)'});
Same as above, for stopOpacity
we also target the right element defined within the SVG's <linearGradient>
element.
<linearGradient id="gradient2" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color: #2196F3; stop-opacity:1"></stop>
<!-- our tween object targets the element below -->
<stop id="myStopOpacity" offset="100%" style="stop-color: #e91b1f; stop-opacity:1"></stop>
</linearGradient>
// stopOpacity Number 0-1
var tween7 = KUTE.to('#myStopOpacity',{stopOpacity: 0.2});
The SVG Plugin can be combined with the Attributes Plugin to enable even more advanced/complex animations for SVG elements.
The future versions of the SVG Plugin might also feature improved cross browser transform animations, as currently the core engine only works and was tested with HTML elements of most types except SVG.
Since most of this plugin scripting works with path
or glyph
elements, I'm also considering a very light convertToPath
feature, but there are some already out there.