Web Dev
Animating a Scalable Vector Graphic (SVG)
• 7 min read
As I was designing my profile website, I had the feeling the "hero" section the user sees when first navigating to the site needed a little something extra. It needed something to indicate to the user that there was more information to be found down below. So I brainstormed ideas. Should I just use a simple downward pointing arrow? Or something more dynamic - a flashing downward pointing arrow? Then something came to mind that I thought was clever...
A pull-down resistor circuit! It's a simple enough design that sort of looks like an arrow (GND symbol at the bottom) and also symbolically indicates the need to pull down the web page (scroll down). In the electrical engineering world, a pull-down resistor is a way to make sure the input signal to a logic circuit is logic '0' when inactive instead of floating around indeterminately.
So the plan was to implement a pull-down resistor as an SVG and put it at the bottom of the hero section. It would function as a button that when pressed would scroll the user down to the first section of the website. I wanted a little dynamic flair so I decided on including a switch component opening/closing. When the switch is in the open state, the input voltage is pulled down by the resistor to GND (represented by gray wire). When the switch closes, the wire is powered by supply voltage (represented by orange wire). It proved to be a little trickier than I expected to animate these behaviors.
I implemented the SVG in Inkscape by tracing over a schematic I quickly put together in KiCAD. I made sure to break the SVG down in to individual paths that I could later animate in react. The 4 main parts are:
- top (supply voltage)
- switch group
- resistor
- GND
The top never changes -- the supply voltage is always applied to it so it will be a nice orange color at all times. The switch group is arguably the most complex -- it needs to rotate from an open position to closed position, then back to open. The resistor and gnd paths aren't too complicated but they need to change color to represent the sudden flow of current through them when the switch is closed.
To begin the process, I exported the SVG from Inkscape and opened it up with Visual Studio Code. Tip: save the file as Plain SVG in Inkscape otherwise you'll end up with a bunch of Inkscape namespaces that have to be removed else you'll get an error 'JSX Namespace is disabled by default because react does not support it yet'. I created a react function component called CircuitSVG
that returns the SVG XML data. At this point I ran into my first hurdle - the XML data formats the style in a way that is incompatible with react:
<path id="gnd-bottom" d="m 119.07376,232.75528 v 2" style="display:inline;fill:none;stroke:#000000;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
In order to render with react, this needs to change to:
<path id="gnd-bottom" d="m 119.07376,232.75528 v 2" style={{ display: 'inline', fill: 'none', stroke: '#000000', strokeWidth: 6, strokeLinecap: 'butt', strokeLinejoin: 'miter', strokeMiterlimit: 4, strokeDasharray: 'none', strokeOpacity: 1, }} />
Fair enough, but I am using SCSS in my project so I added a className='svg-circuit-item'
attribute to all my SVG path elements and moved the styles into that class:
.svg-circuit-item{ display: inline; opacity: 1; fill: none; fill-opacity: 1; fill-rule: nonzero; stroke: gray; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-opacity: 1; stroke-width: 2; }
I kept any attributes that need to be customized (e.g. stroke width) in the react code. This allows the customized element style to override the svg-circuit-item
class style. With the basic styles configured and the paths in place, it was time to animate the SVG.
To animate the paths, I chained a total of 6 animations together using each path's onAnimationEnd
attribute and assigning a handleAnimationEnd
function to it. Each animation has a state determined by the useState()
react hook that indicates whether the animation is running or not. When the animation ends the handleAnimationEnd
function kicks off the next animation by setting the animation state of the next animation to true
. For every path that needs to be animated, I assign an animation name in its style attribute. The animation name is conditionally set by a ternary operator to be undefined
when the animation state is set to false
:
// State variables to determine animation run state const [animation1, setAnimation1] = useState(false); const [animation2, setAnimation2] = useState(false); // Handles launching animation 2 when animation 1 ends const handleAnimation1End = () => { setAnimation2(true); }; // The animation of the SVG element is determined // by the animation name and onAnimationEnd event <circle r="2.999" cy="146.12494" cx="119.14196" id="svg-switch-bottom" className='svg-circuit-item' style={{ animationName: animation1 ? 'powerUp' : undefined, }} onAnimationEnd={handleAnimation1End} />
The powerUp
animation handles setting the stroke color of the path to simulate voltage flowing in the path. Overall there are 4 total animations:
- powerUp
- draw
- rotator
- reverse-rotator
@keyframes rotator { 0% { transform: rotate(0); } 100% { transform: rotate(-17deg);} } @keyframes reverse-rotator { 0% { transform: rotate(-17deg); } 100% { transform: rotate(0deg);} } @keyframes draw{ 0%{ stroke-dashoffset: 130.01345825195312;} 100%{stroke-dashoffset: 0;} } @keyframes powerUp{ 0%{ stroke: gray} 100%{ stroke: $primary-color;} }
The draw
animation follows the path of the resistor filling the color with orange to simulate current flow. I duplicated the resistor path so that the powered up path simply draws on top of the static gray resistor path. The difficult part there is determining the path length which we'll need to input for the animation stroke-dashoffset
value. I found the length by finding the path element in the DOM and calling the getTotalLength()
function on it:
const path = document.getElementById('svg-resistor'); const length = path.getTotalLength();
Perhaps the trickiest animation was the switch gate animation. It needs to rotate from open to closed position and back to open. To do this I defined two animations rotator
and reverse-rotator
. The rotator
animation plays 2 seconds after page load via the useEffect()
React hook. Once the entire chain of animations finishes, the switch's animation name changes to the reverse-rotator
which launches causes the switch gate to reopen. To make the animation more dynamic, I configured it animation to loop by resetting the animation name to rotator
1 second after the reverse-rotator
animation ends.
Last but not least, I set the entire SVG button to fade in 0.5 seconds after the page loads and applying an opacity transition for the button. Of course the button needs an onClick
method which handles the scrolling into view of the desired section of the web site. I passed this handleSvgCircuitClick
function in as a prop from the parent component.
const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { const showButton = setTimeout(() => { setIsLoaded(true); }, 500); return () => { clearTimeout(showButton); }; }, []);
<button className={`svg-button ${isLoaded ? "svg-button-show" : ""}`} onClick={handleSvgCircuitClick}> <CircuitSVG isLoaded={isLoaded}/> </button>
.svg-button{ opacity: 0; transition: opacity 1s ease-in-out; } .svg-button-show{ opacity: 1; }
So that's about it, you can check out the final code here. Let me know down below in the comments if you know of any fancy React/JS libraries that handle SVG animation.
Comments