<div class="main-view-container">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" class="svg-def">
<filter id="glow1" y="-50%" height="180%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"></feGaussianBlur>
<feMerge>
<feMergeNode in="coloredBlur"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<filter id="glow2" y="-50%" height="180%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"></feGaussianBlur>
<feMerge>
<feMergeNode in="coloredBlur"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</svg>
<div class="content">
<div class="loader">
<svg id="loaderSVG" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 292.6">
<g id="checkPathLayer" filter="url(#glow2)">
<path class="path path--filtered" id="checkPath" opacity="0" fill="none" stroke="#EDEDED" stroke-width="7" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M162.8 112.8l-46.7 66.7-.2-.2-28.8-26.9"></path>
</g>
<g id="watchThumbPieces" stroke="none" opacity="0">
<path class="path path--filled" id="watchThumbTopHead" fill="#333333" d="M134.4,51.2h-19c-1.7,0-3-1.3-3-3v-11c0-1.7,1.3-3,3-3h19c1.7,0,3,1.3,3,3v11
C137.4,49.9,136.1,51.2,134.4,51.2z"></path>
<path class="path path--filled" id="watchThumbSideHead" fill="#333333" d="M63.3,72.9L49.9,86.3c-1.2,1.2-3.1,1.2-4.2,0l-7.8-7.8c-1.2-1.2-1.2-3.1,0-4.2
l13.4-13.4c1.2-1.2,3.1-1.2,4.2,0l7.8,7.8C64.5,69.8,64.5,71.7,63.3,72.9z"></path>
<rect class="path path--filled" id="watchThumbTopBase" x="117.9" y="51.2" fill="#333333" width="14" height="15"></rect>
<rect class="path path--filled" id="watchThumbSideBase" x="54.9" y="77.4" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -41.9085 68.6215)" fill="#333333" width="14" height="15"></rect>
</g>
<g id="start">
<!-- The face will act as our initial click hood to activate the animation -->
<circle class="path path--stroked" id="watchRimBg" fill="none" stroke="#333" stroke-width="11" stroke-miterlimit="10" cx="125" cy="146.3" r="75">
</circle>
<g class="click-listener">
<g id="watchRimGlowLayer" filter="url(#glow1)">
<circle class="path path--filtered" id="watchRimGlow" opacity="0" fill="transparent" stroke="#EDEDED" stroke-width="11" stroke-miterlimit="10" cx="125" cy="146.3" r="75">
</circle>
</g>
<g id="downloadIconContainer">
<path class="path path--filled" id="downloadIconBase" fill="#333" d="M110.9 166h28.2c1.1 0 2-.9 2-2v-62c0-1.1-.9-2-2-2h-28.2c-1.1 0-2 .9-2 2v62c0 1.1.9 2 2 2z"></path>
<path class="path path--filled" id="downloadIconArrow" fill="#333" d="M155 162.3c1.1 0 1.3.6.5 1.4l-29.3 27c-.8.7-2.1.7-2.9 0l-29.3-27c-.8-.7-.6-1.4.5-1.4H155z"></path>
</g>
</g>
</g>
</svg>
</div>
</div>
</div>
body {
background-color: hsla(210, 10%, 8%, 1);
}
$watch-color__primary: hsla(191, 15%, 22%, 1);
$watch-color__glow--loading: hsla(205, 72%, 50%, 1);
$watch-color__glow--complete: hsla(133, 100%, 66%, 1);
.svg-def {
position: absolute;
width: 0;
height: 0;
}
.click-listener {
cursor: pointer;
&.animating {
cursor: default;
}
}
#watchRimGlow {
z-index: 4;
transform-origin: 50% 50%;
transform: rotateZ(-90deg) rotateX(180deg); // Rotate so that SVG path starts at top of the watch and the length runs clockwise (the default, (i.e. trigonometric) start point is x-most point of the circle, with length running counterclockwise)
}
.path--filled {
fill: $watch-color__primary;
}
.path--stroked {
stroke: $watch-color__primary;
}
.path--filtered {
stroke: $watch-color__glow--loading;
}
#checkPath {
&.path--filtered {
stroke: $watch-color__glow--complete;
}
}
#loaderSVG {
width: 20em;
opacity: 0; /* animate into view onload after we've positioned */
}
View Compiled
(function(window) {
// Wire up our references
var loaderSVG = document.querySelector('#loaderSVG'),
checkMarkPath = document.querySelector('#checkPath'),
watchRimBackgroundLayer = document.querySelector('#watchRimBg'),
watchRimGlow = document.querySelector('#watchRimGlow'),
clickable = document.querySelector('.click-listener'),
watchThumbPiecesLayer = document.querySelector('#watchThumbPieces'),
watchThumbTopBase = document.querySelector('#watchThumbTopBase'),
watchThumbTopHead = document.querySelector('#watchThumbTopHead'),
watchThumbSideBase = document.querySelector('#watchThumbSideBase'),
watchThumbSideHead = document.querySelector('#watchThumbSideHead'),
watchThumbPieces = [
watchThumbTopBase,
watchThumbTopHead,
watchThumbSideBase,
watchThumbSideHead
],
// Animate arrow and base into timer center and hand, respectively,
// when we being the load animation
centerArrow = document.querySelector('#downloadIconArrow'),
centerBase = document.querySelector('#downloadIconBase'),
isAnimating = false,
ANIMATION_DURATION_MULTIPLIER = 1,
DURATIONS = {
makeWatchHand: ANIMATION_DURATION_MULTIPLIER * 0.4,
makeWatchThumbs: ANIMATION_DURATION_MULTIPLIER * 0.45,
addRimGlow: ANIMATION_DURATION_MULTIPLIER * 1.1,
pressThumb: ANIMATION_DURATION_MULTIPLIER * 0.12,
runWatch: ANIMATION_DURATION_MULTIPLIER * 3.1,
hideWatchThumbs: ANIMATION_DURATION_MULTIPLIER * 0.2,
morphHandsToCheck: ANIMATION_DURATION_MULTIPLIER * 1.1
},
toggledClasses = {
animating: 'animating',
pressThumb: 'press-thumb',
loadComplete: 'load-complete'
},
LABELS = {
makeWatchHand: 'makeWatchHand',
makeWatchThumbs: 'makeWatchThumbs',
addRimGlow: 'addRimGlow',
pressThumb: 'pressThumb',
glowComplete: 'glowComplete',
loadComplete: 'loadComplete'
},
tlConfig = {
repeat: 0, // TODO: Handle resetting the icon at some point?
},
masterTl = new TimelineMax(tlConfig);
TweenMax.set(
loaderSVG, {
position: 'absolute',
top: '50%',
left: '50%',
yPercent: -50,
xPercent: -50,
opacity: 1
}
);
/**
* Arrow head rotates 180 degrees, scales down, then translates
* up to form the center node of the watch hand.
*
* At the same time, the arrow base will
* scale down its X-axis to the width of a watch hand.
*/
function makeHand(duration) {
var tl = new TimelineMax();
TweenMax.set([centerBase, centerArrow], {
transformOrigin: '50%, 50%'
});
tl.add([
TweenMax.to(
centerArrow,
duration, {
rotation: 180,
scale: 0.5,
y: '-=100%',
ease: Power3.easeOut
}
),
TweenMax.to(
centerBase,
duration, {
scaleX: 0.2,
y: '-=10%',
ease: Power3.easeOut
}
)
]);
return tl;
}
function setupAnimation() {
masterTl.set(checkMarkPath, {
drawSVG: '58%, 58%'
});
}
function makeThumbs(duration) {
var tl = new TimelineMax();
tl.set(watchThumbPieces, {
drawSVG: '0%'
});
tl.set(watchThumbPiecesLayer, {
opacity: 1
});
tl.to(
[watchThumbTopBase, watchThumbSideBase],
duration / 2, {
drawSVG: '100%',
ease: Linear.easeNone
}
);
tl.to(
[watchThumbTopHead, watchThumbSideHead],
duration / 2, {
drawSVG: '100%',
ease: Back.easeOut.config(1.7)
}
);
return tl;
}
/**
* Press the top thumb
*/
function pressThumb(duration) {
var thumbPressTl = new TimelineMax(),
boundingRect = watchThumbTopBase.getBoundingClientRect(),
yDist = boundingRect.top - boundingRect.bottom;
// simultaneously undraw top thumb base and
// follow through by down-shifting the thumb head
thumbPressTl.add([
TweenMax.to(
watchThumbTopBase,
duration * (1 / 8), {
drawSVG: '0%'
}),
TweenMax.to(watchThumbTopHead, duration, {
y: '-=' + yDist
})
]);
// restore the thumb
thumbPressTl.add([
TweenMax.to(watchThumbTopBase, duration, {
drawSVG: '100%'
}),
TweenMax.to(watchThumbTopHead, duration, {
y: '+=' + yDist
})
]);
return thumbPressTl;
}
/**
* Propagate the glow filter around the watch face.
*/
function addRimGlow(duration) {
var rimGlowTl = new TimelineMax();
rimGlowTl.add(TweenMax.set(watchRimGlow, {
drawSVG: '0%'
}));
rimGlowTl.add(TweenMax.set(watchRimGlow, {
opacity: 1
}));
rimGlowTl.add(TweenMax.to(watchRimGlow, duration, {
drawSVG: '100%'
}));
return rimGlowTl;
}
function startWatch(duration, rimGlowTl) {
var tl = new TimelineMax();
tl.set(centerBase, {
transformOrigin: '50% 90%'
});
tl.add([
TweenMax.to(centerBase, duration, {
rotation: 360,
ease: Linear.easeNone
}),
// undraw the glow around the rim
//TweenMax.to(watchRimGlowLayer, 0.1, {drawSVG: '100% 0%'}),
//function () { rimGlowTl.reverse(0); }
TweenMax.to(watchRimGlow, duration, {
drawSVG: '0%',
ease: Linear.easeNone
})
]);
return tl;
}
function hideThumbs(duration) {
var tl = new TimelineMax();
tl.to([watchThumbSideHead, watchThumbTopHead], duration / 2, {
drawSVG: '0%',
ease: Power3.easeOut
});
tl.to([watchThumbSideBase, watchThumbTopHead], duration / 2, {
drawSVG: '0%',
ease: Power3.easeOut
});
tl.set(watchThumbPieces, {
opacity: 0,
zIndex: -1
});
return tl;
}
/**
* -- Center node becomes undrawn, hand shrinks to smaller dot and pops up
* -- Translate dot back down to base of the checkmark and, alas, draw the checkmark
*/
function morphHandsToCheck(duration) {
var tl = new TimelineMax();
tl.add([
TweenMax.to(
centerArrow,
duration * 0.2, {
opacity: 0,
zIndex: -1,
ease: Power3.easeOut
}
),
TweenMax.set(centerBase, {
transformOrigin: '50% 90%'
}),
TweenMax.to(
centerBase,
duration * 0.2, {
scaleY: 0.2,
ease: Power3.easeOut
}
)
]);
tl.add(
TweenMax.to(centerBase, duration * 0.2, {
y: '-=200%',
ease: Power3.easeOut
})
);
tl.add(
TweenMax.to(
centerBase,
duration * 0.2, {
y: '+=200%',
ease: Power3.easeOut,
onComplete: function() {
TweenMax.set(centerBase, {
opacity: 0,
zIndex: -1
});
}
}
)
);
// Prepare the checkmark path and then draw it
tl.set(checkMarkPath, {
opacity: 0,
drawSVG: '0%',
zIndex: 5
});
tl.add(
TweenMax.fromTo(
checkMarkPath,
duration * 0.4, {
opacity: 1,
drawSVG: '50% 50%'
}, // meet in middle
{
drawSVG: '100%', // draw out from middle
onComplete: resetTl
}
)
);
return tl;
}
function animateLoader() {
var rimGlowTl = addRimGlow(DURATIONS.addRimGlow);
setupAnimation();
masterTl.add(makeHand(DURATIONS.makeWatchHand), LABELS.makeWatchHand);
masterTl.add(makeThumbs(DURATIONS.makeWatchThumbs), LABELS.makeWatchThumbs);
masterTl.add(rimGlowTl, LABELS.makeWatchThumbs + '+=0.35');
masterTl.addLabel(LABELS.addRimGlow);
masterTl.add(pressThumb(DURATIONS.pressThumb), LABELS.addRimGlow + '+=0.35');
masterTl.addLabel(LABELS.pressThumb);
masterTl.add(startWatch(DURATIONS.runWatch, rimGlowTl), LABELS.pressThumb + '+=0.1');
masterTl.addLabel(LABELS.loadComplete);
masterTl.add(
[
hideThumbs(DURATIONS.hideWatchThumbs),
morphHandsToCheck(DURATIONS.morphHandsToCheck)
],
LABELS.loadComplete + '+=0.2'
);
}
function resetTl() {
clickable.classList.remove(toggledClasses.animating);
isAnimating = false;
// TODO: Return the button to its original state here
}
function init() {
clickable.addEventListener('mouseup', function() {
if (!isAnimating) {
isAnimating = true;
clickable.classList.add(toggledClasses.animating);
animateLoader();
}
}, false);
}
window.addEventListener('load', function() {
init();
}, false);
}(window));
This Pen doesn't use any external CSS resources.