In this tutorial our goal is to create a Braun BN0171 Watch SVG animation starting from a Sketch file resource. We will look at how to extract and optimise the SVG for web, how to add the necessary CSS, and finally how to bring this to life with a sprinkling of JavaScript. Itβs just for fun, but often I find making things fun to be the best way to learn things.
OK let's begin
To start with we are going to download this resource:-
https://www.behance.net/gallery/64303683/Braun-Watch-Free-Vector-Resource
Extracting the SVG and optimising it for web
When we export an SVG from Sketch it outputs a lot of extra guff we don't really need.
Open up your SVG in a code editor and you will see something like this at the beginning (plus a lot of unnecessary ID's):-
<?xml version="1.0" encoding="UTF-8"?>
<svg width="403px" height="776px" viewBox="0 0 403 776" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 62 (91390) - https://sketch.com -->
<title>Group</title>
<desc>Created with Sketch.</desc>
We don't need all that extra stuff so we're going to use a tool to clean this up. I recommend optimising your SVG code using something like https://jakearchibald.github.io/svgomg/
My method was to extract certain things separately like the time indicators. The end result html was:-
<div class="container">
<div class="watch">
<svg width="403" height="776" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="b">
<stop stop-color="#383838" offset="0%"/>
<stop stop-color="#2D2D2D" offset="100%"/>
</linearGradient>
<linearGradient x1="50%" y1="114.4%" x2="50%" y2="0%" id="c">
<stop stop-color="#383838" offset="0%"/>
<stop stop-color="#2D2D2D" offset="100%"/>
</linearGradient>
<linearGradient x1="129.7%" y1="50%" x2="37%" y2="50%" id="e">
<stop stop-color="#141414" offset="0%"/>
<stop stop-color="#5A5A5A" offset="100%"/>
</linearGradient>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="g">
<stop stop-color="#010101" offset="0%"/>
<stop stop-color="#6A6A6A" offset="23.4%"/>
<stop stop-color="#4A4848" offset="35.4%"/>
<stop stop-color="#383838" offset="55%"/>
<stop offset="100%"/>
</linearGradient>
<linearGradient x1="166.8%" y1="50%" x2="-40.1%" y2="50%" id="f">
<stop stop-color="#262626" offset="0%"/>
<stop offset="46.6%"/>
<stop stop-color="#1F1F1F" offset="100%"/>
</linearGradient>
<linearGradient x1="-4.7%" y1="18.2%" x2="72.6%" y2="94.2%" id="j">
<stop stop-color="#47474A" offset="0%"/>
<stop stop-color="#050505" offset="100%"/>
</linearGradient>
<filter x="-72.7%" y="-44.4%" width="245.5%" height="188.9%" filterUnits="objectBoundingBox" id="d">
<feOffset dy="4" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.177139946 0" in="shadowBlurOuter1" result="shadowMatrixOuter1"/>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter x="-3.1%" y="-3.1%" width="107%" height="112.3%" filterUnits="objectBoundingBox" id="h">
<feMorphology radius="10" in="SourceAlpha" result="shadowSpreadOuter1"/>
<feOffset dx="9" dy="28" in="shadowSpreadOuter1" result="shadowOffsetOuter1"/>
<feGaussianBlur stdDeviation="7" in="shadowOffsetOuter1" result="shadowBlurOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.109091938 0" in="shadowBlurOuter1"/>
</filter>
<filter x="-8.1%" y="-6%" width="132.3%" height="113.1%" filterUnits="objectBoundingBox" id="l">
<feOffset dx="20" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feGaussianBlur stdDeviation="4.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" in="shadowBlurOuter1"/>
</filter>
<filter x="-7.2%" y="-7.2%" width="114.5%" height="114.5%" filterUnits="objectBoundingBox" id="n">
<feMorphology radius="5" in="SourceAlpha" result="shadowSpreadOuter1"/>
<feOffset in="shadowSpreadOuter1" result="shadowOffsetOuter1"/>
<feMorphology radius="15" in="SourceAlpha" result="shadowInner"/>
<feOffset in="shadowInner" result="shadowInner"/>
<feComposite in="shadowOffsetOuter1" in2="shadowInner" operator="out" result="shadowOffsetOuter1"/>
<feGaussianBlur stdDeviation="12" in="shadowOffsetOuter1" result="shadowBlurOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" in="shadowBlurOuter1"/>
</filter>
<circle id="i" cx="179.5" cy="179.5" r="179.5"/>
<circle id="a" cx="180" cy="181" r="165"/>
<circle id="o" cx="179.5" cy="179.5" r="179.5"/>
<radialGradient cx="50%" cy="50%" fx="50%" fy="50%" r="53.1%" gradientTransform="rotate(88 1 1) scale(1 1.18918)" id="p">
<stop stop-color="#303033" offset="0%"/>
<stop stop-color="#979797" offset="6.4%"/>
<stop stop-color="#3D3D40" offset="100%"/>
</radialGradient>
<radialGradient cx="50%" cy="50%" fx="50%" fy="50%" r="48.1%" id="q">
<stop stop-color="#303033" offset="0%"/>
<stop stop-color="#ADADAD" offset="90%"/>
<stop stop-color="#3D3D40" offset="100%"/>
</radialGradient>
<path d="M181 345v16a179 179 0 110-359v16a163 163 0 100 327zm0 0v16a179 179 0 110-359v16a163 163 0 100 327z" id="m"/>
<mask id="k" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="330" height="330" fill="#fff">
<use xlink:href="#a"/>
</mask>
</defs>
<g fill="none" fill-rule="evenodd">
<g transform="translate(109)">
<path fill="#000" d="M1 0h173v264H1z"/>
<rect fill="url(#b)" y="198" width="176" height="64" rx="3"/>
<rect fill="url(#b)" y="132" width="176" height="64" rx="3"/>
<rect fill="url(#b)" y="66" width="176" height="64" rx="3"/>
<rect fill="url(#b)" width="176" height="64" rx="3"/>
</g>
<g transform="matrix(1 0 0 -1 111 776)">
<path fill="#000" d="M1 0h173v264H1z"/>
<rect fill="url(#c)" y="198" width="176" height="64" rx="3"/>
<rect fill="url(#c)" y="132" width="176" height="64" rx="3"/>
<rect fill="url(#c)" y="66" width="176" height="64" rx="3"/>
<rect fill="url(#c)" width="176" height="64" rx="3"/>
</g>
<g transform="translate(19 207)">
<g filter="url(#d)" transform="translate(358 162)">
<path fill="#000" d="M0 2h8v32H0z"/>
<ellipse fill="url(#e)" cx="13" cy="18" rx="9" ry="17"/>
<path d="M17 29a6 6 0 01-7 6H4V3 1h6a6 6 0 017 6v22z" stroke="url(#f)" stroke-width="2" fill="url(#g)"/>
</g>
<use fill="#000" filter="url(#h)" xlink:href="#i"/>
<use fill="url(#j)" xlink:href="#i"/>
<use stroke="#6A6A6C" mask="url(#k)" stroke-width="82" stroke-dasharray="1,85.40000152587891" xlink:href="#a"/>
<use filter="url(#l)" xlink:href="#m" fill-rule="nonzero" fill="#000"/>
<g opacity=".8">
<use fill="#000" filter="url(#n)" xlink:href="#o"/>
<circle stroke="url(#p)" stroke-width="20" stroke-linejoin="square" cx="179.5" cy="179.5" r="169.5"/>
</g>
<circle stroke="url(#q)" stroke-width="16" cx="179.5" cy="179.5" r="171.5"/>
</g>
</g>
<img class="watch-logo" width="50" height="21" src="{{ base.url }}/assets/css-tricks/img/braun-logo.png" alt="Braun">
</svg>
<div class="watch-face">
<svg class="hours" width="6" height="87" xmlns="http://www.w3.org/2000/svg">
<path d="M5 1H0v85h5z" fill="#BBB" fill-rule="evenodd"/>
</svg>
<svg class="mins" width="6" height="130" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1h5v128H1z" fill="#BBB" fill-rule="evenodd"/>
</svg>
<svg class="secs" width="20px" height="155px" viewBox="0 0 20 155" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-745.000000, -310.000000)" fill="#E9BB00">
<g transform="translate(642.000000, 207.000000)">
<g transform="translate(43.000000, 103.000000)">
<path d="M106.472762,11.6036371 C108.625079,12.8462778 109.362517,15.5984346 108.119876,17.7507514 L102.119876,28.1430562 C102.004219,28.3433801 101.875486,28.5314473 101.735376,28.7068021 C102.653234,30.1676886 103.183369,31.8961942 103.183369,33.7485195 C103.183369,38.9952246 98.9300743,43.2485195 93.6833692,43.2485195 C92.6719919,43.2485195 91.6975278,43.0904753 90.7833266,42.7977366 L31.7992886,144.960273 C31.385075,145.677712 30.4676894,145.923525 29.7502505,145.509311 C29.0328115,145.095098 28.7869988,144.177712 29.2012124,143.460273 L88.103434,41.4379377 C85.7278274,39.7110658 84.1833692,36.9101564 84.1833692,33.7485195 C84.1833692,28.5018144 88.4366641,24.2485195 93.6833692,24.2485195 C93.8000598,24.2485195 93.916259,24.2506234 94.0319309,24.2547953 C94.1123135,24.047175 94.210476,23.8425393 94.3256475,23.6430562 L100.325648,13.2507514 C101.568288,11.0984346 104.320445,10.3609964 106.472762,11.6036371 Z M93.5,31.1643518 C92.1192881,31.1643518 91,32.2836399 91,33.6643518 C91,35.0450637 92.1192881,36.1643518 93.5,36.1643518 C94.8807119,36.1643518 96,35.0450637 96,33.6643518 C96,32.2836399 94.8807119,31.1643518 93.5,31.1643518 Z" transform="translate(68.861757, 78.355262) rotate(150.000000) translate(-68.861757, -78.355262) "></path>
</g>
</g>
</g>
</g>
</svg>
</div>
</div>
</div>
All we need to do then is identify the hour, minute and second hands and add the correct classes. I recommend inspecting the html in Chrome dev tools for this part.
Adding styles
Now we can style the SVG with CSS. The trickiest part was getting the transform-origin
right which took a bit of trial and error. Also notice a nice trick with filter: drop-shadow
which works much better than box-shadow
in this case.
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background-color: #FFCC00;
}
.container {
display: flex;
align-items: center;
justify-content: center;
height: 776px;
margin: 0 auto 100px;
}
.watch {
width: 403px;
position: relative;
&-logo {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-28px, -70px);
}
&-face {
position: absolute;
width: 359px;
height: 359px;
top: 208px;
left: 19px;
}
.hours {
position: absolute;
left: 50%;
top: 88px;
transform-origin: 48% 105%;
filter: drop-shadow(1px 1px 0 #999999) drop-shadow(0.11rem 0.11rem 0.13rem rgba(0, 0, 0, 0.95));
}
.mins {
position: absolute;
left: 49%;
margin-left: 2px;
top: 45px;
transform-origin: 48% 103%;
filter: drop-shadow(1px 1px 0 #999999) drop-shadow(0.11rem 0.11rem 0.13rem rgba(0, 0, 0, 0.95));
}
.secs {
position: absolute;
left: 48%;
top: 50px;
transform-origin: 49% 84%;
filter: drop-shadow(1px 1px 0 #f3dc7c) drop-shadow(0.11rem 0.11rem 0.13rem rgba(0, 0, 0, 0.95));
}
}
Bringing it all to life
The final step was to bring this to life keeping the JS to a minimum we come up with:-
var inc = 1000;
clock();
function clock() {
const date = new Date();
const hours = ((date.getHours() + 11) % 12 + 1);
const minutes = date.getMinutes();
const seconds = date.getSeconds();
const hour = hours * 30;
const minute = minutes * 6;
const second = seconds * 6;
document.querySelector('.hours').style.transform = `rotate(${hour}deg)`
document.querySelector('.mins').style.transform = `rotate(${minute}deg)`
document.querySelector('.secs').style.transform = `rotate(${second}deg)`
}
setInterval(clock, inc);
And that's it! Hope you enjoyed this little write-up.
Final Result
ps, the designer seemed to think there were 72 seconds in a minute so I had to omit the secondary indicators on the watch. :(
If you found this post useful please share it π€
About the Author
Simon is a freelance web designer.
Top comments (0)