In web development I sometimes get an itch to learn something new. Just a tick that encourages me to push my knowledge a bit more. The possibilities are endless and sometimes I just feel like exploring them for the sake of learning something. And this week I got the itch to teach myself some SVG animations.
SVG’s are Scalable Vector Graphics printed through a browser through a SVG code generated by programs like Adobe Illustrator or Inkscape paths. Being extremely familiar with illustrator and having no experience with Inkscape naturally I downloaded the new program, stared at it for 5 minutes and decided to return to my adobe safety zone. Nestled securely in my wheelhouse I created my illustration, making sure that all my polyfills and circles were converted to compound paths.
My company had a release of it’s new battery case for the iPhone 6, obviously a pretty big product to launch. Early in my research I’d stumbled upon this link, I was inspired by it’s application to tech: how well it described the concept of product design and engineering as process. Naturally I downloaded the git and played around with the code they’d provided. After a half-hour of finagling I realized I’d never be able to use a plugin for this technology, I’d be letting myself and my craft down and decided it was back to the grind and the hand-code method to make this happen.
So I started with some basic code to animate the SVG. I’ve always found Chris Coyier over at CSS Tricks to be a reliable source of information and his article How SVG Line Animation Works was incredibly helpful just to wrap my head around the concept. Once I understood the process behind it I was able to incorporate a simple SVG animation using jquery/javascript to manipulate the css on my path and run the animation.
function drawSVGPaths(_parentElement, _timeMin, _timeMax, _timeDelay) {
var paths = $(_parentElement).find('path');
$.each( paths, function(i) {
//get the total length
var totalLength = this.getTotalLength();
//set PATHs to invisible
$(this).css({
'stroke-dashoffset': totalLength,
'stroke-dasharray': totalLength + ' ' + totalLength
});
//animate
$(this).delay(_timeDelay*i).animate({
'stroke-dashoffset': 0
}, {
duration: Math.floor(Math.random() * _timeMax) + _timeMin
,easing: 'easeInOutQuad'
});
});
}
function startSVGAnimation(parentElement) {
drawSVGPaths(parentElement, 2000, 3000, 50);
}
As you can see I’m selecting all ‘path’ elements in my _parentElement, assigning their length to a variable, then animating them over a random time interval with some clever math (I pulled the clever math from another source that I can’t seem to find, if found please let me know and I’ll link it!). This of course just ran the animation. I needed the animation to start only once the element was scrolled in to the viewport.
After a few more hours of research I found myself with the following code.
function isScrolledIntoView(elem) {
var docViewTop = $(window).scrollTop();
var docViewBottom = docViewTop + $(window).height();
var elemTop = $(elem).offset().top;
var elemBottom = elemTop + $(elem).height();
return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
}
$(window).scroll(function() {
if (isScrolledIntoView('#svg-container')) {
startSVGAnimation($('#route'));
}
});
This allowed me to trigger my animation once the element was scrolled into view. Solid! I started my CSS styling and jovially patted myself on the back for solving the puzzle all by my google! Until I loaded the page and realized the animation was repeating itself each time the page was scrolled, and as I scroll just a little very frequently the animation looked like it was stuttering. Obviously not my intention. Back to the code board!
I’m gonna admit this took me a lot longer to figure out than I’d like to say. I finally had to ask a developer friend to help me and the code he wrote is so annoyingly simple I wanted to punch myself for not figuring it out.
var hasExecuted = false;
$(window).scroll(function() {
if (!hasExecuted && isScrolledIntoView('#svg-container')) {
startSVGAnimation($('#route'));
hasExecuted = true;
}
});
Excuse me while I rage kick myself in the face, even looking at how simple the solution was, just sitting out of my reach still gets my blood boiling. But once I had this part refined I was able to move forward and work on fading in the actual product image on top of my SVG illustration once it was completed.
function drawSVGPaths(_parentElement, _timeMin, _timeMax, _timeDelay) {
var paths = $(_parentElement).find('path');
$.each( paths, function(i) {
//get the total length
var totalLength = this.getTotalLength();
//set PATHs to invisible
$(this).css({
'stroke-dashoffset': totalLength,
'stroke-dasharray': totalLength + ' ' + totalLength
});
//animate
$(this).delay(_timeDelay*i).animate({
'stroke-dashoffset': 0
}, {
duration: Math.floor(Math.random() * _timeMax) + _timeMin
,easing: 'easeInOutQuad'
});
});
var finalImg = $(_parentElement).next('img');
var imgTime = _timeMin + _timeMax + _timeDelay;
setTimeout(function() {
finalImg.animate({opacity: 1.0})
}, imgTime);
}
My final drawSVGPaths function selected the image directly after the parent element and animated it’s opacity from 0 to 1 over the time interval defined for the animation itself. I wanted to make sure I could reuse this function on later projects, blood, sweat, and tears were going into this project I better be able to use it afterwards.
With a document.ready function to hide the paths on page load I was ready to deploy! I put this effect into use on the product page for the iPhone 6 Spare and have to say I’m pretty proud of it. I stuck through the difficulties and persevered through the frustrations. With google at my side, I can achieve anything! But mostly cool SVG animations.
Result
Final Code
$(document).ready(function() {
function hideSVGPaths(_parentElement) {
var paths = $(_parentElement).find('path');
$.each( paths, function(i) {
var totalLength = this.getTotalLength();
$(this).css({
'stroke-dashoffset': totalLength,
'stroke-dasharray': totalLength + ' ' + totalLength
});
});
}
hideSVGPaths('#route');
hideSVGPaths('#dimensions');
});
var hasExecuted1 = false;
var hasExecuted2 = false;
$(window).scroll(function() {
if (!hasExecuted1 && isScrolledIntoView('#svg-container')) {
startSVGAnimation($('#route'));
hasExecuted1 = true;
}
if (!hasExecuted2 && isScrolledIntoView('#iphone6-thin')) {
startSVGAnimation($('#dimensions'));
hasExecuted2 = true;
}
});
function isScrolledIntoView(elem) {
var docViewTop = $(window).scrollTop();
var docViewBottom = docViewTop + $(window).height();
var elemTop = $(elem).offset().top;
var elemBottom = elemTop + $(elem).height();
return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
}
function drawSVGPaths(_parentElement, _timeMin, _timeMax, _timeDelay) {
var paths = $(_parentElement).find('path');
$.each( paths, function(i) {
//get the total length
var totalLength = this.getTotalLength();
//set PATHs to invisible
$(this).css({
'stroke-dashoffset': totalLength,
'stroke-dasharray': totalLength + ' ' + totalLength
});
//animate
$(this).delay(_timeDelay*i).animate({
'stroke-dashoffset': 0
}, {
duration: Math.floor(Math.random() * _timeMax) + _timeMin
,easing: 'easeInOutQuad'
});
});
if ($(_parentElement).next().is('img')) {
var finalImg = $(_parentElement).next('img');
var imgTime = _timeMin + _timeMax + _timeDelay;
setTimeout(function() {
finalImg.animate({opacity: 1.0})
}, imgTime);
}
}
function startSVGAnimation(parentElement) {
drawSVGPaths(parentElement, 2000, 3000, 50);
}