这是一款基于SVG和Segment.js制作的Loading加载按钮特效。该特效在点击按钮之后会出现基于SVG路径的无限圆形加载效果,加载成功或失败时会转换为不同的图形,显示不同的状态。
使用方法
SVG可以绘制很多基本图形,如圆形、椭圆、矩形等,但是只有SVG路径可以制作线条动画。因此,我们要制作一个圆形的线条动画必须使用路径。关于如何绘制一条SVG路径,可以参考这里。
在这个特效中,为了制作线条动画,使用了Segment.js插件。Segment.js是一个可以制作SVG路径片段动画的js库。
绘制路径
要制作出这个特效,首先是要了解线条如何动画。所有的线条必须手动画出来。下面是第一个loading效果的SVG线条。
<svg width="120px" height="120px"> <path class="outer-path" stroke="#fff" d="M 60 60 m 0 -50 a 50 50 0 1 1 0 100 a 50 50 0 1 1 0 -100"></path> <path class="inner-path" stroke="rgba(255, 255, 255, 0.5)" d="M 60 60 m 0 -30 a 30 30 0 1 1 0 60 a 30 30 0 1 1 0 -60"></path> <path class="success-path" stroke="#fff" d="M 60 10 A 50 50 0 0 1 91 21 L 75 45 L 55 75 L 45 65"></path> <path class="error-path" stroke="#fff" d="M 60 10 A 50 50 0 0 1 95 25 L 45 75"></path> <path class="error-path2" stroke="#fff" d="M 60 30 A 30 30 0 0 1 81 81 L 45 45"></path> </svg>
为它添加一些CSS样式:
body{ background: #354458; } svg path{ stroke-linecap: round; stroke-linejoin: round; stroke-width: 4; fill: none; }
上面的代码通过直线和弧线一步步的创建了整个loading所需的线条。创建后会得到如下图左边所示的效果。右边的图是失败状态的效果。
通过Segment.js进行动画
下面是创建这个loading动画的关键js代码。
var outer = document.querySelector('.outer-path'), inner = document.querySelector('.inner-path'), outerSegment = new Segment(outer, 0, 0.1), innerSegment = new Segment(inner, 0, 0.1); function outerAnimation(){ outerSegment.draw('15%', '25%', 0.2, {callback: function(){ outerSegment.draw('75%', '150%', 0.3, {circular:true, callback: function(){ outerSegment.draw('70%', '75%', 0.3, {circular:true, callback: function(){ outerSegment.draw('100%', '100% + 0.1', 0.4, {circular:true, callback: function(){ outerAnimation(); innerAnimation(); }}); }}); }}); }}); } function innerAnimation(){ innerSegment.draw('20%', '80%', 0.6, {callback: function(){ innerSegment.draw('100%', '100% + 0.1', 0.6, {circular:true}); }}); } outerAnimation(); innerAnimation();
创建一个通用js库
通过上面的代码我们已经可以进行loading加载线条动画了。但是我们还没有处理loading成功和失败时的状态。另外如果想添加新的loader该如何做呢?最好的方法是创建一个通用的js库或插件来处理这些问题。下面就是这个通用库的js代码:
function LoadingButton(el, options){ this.el = el; this.options = options; this.init(); } LoadingButton.prototype = { // Initialize everything init: function(){ this.infinite = true; this.succeed = false; this.initDOM(); this.initSegments(); this.initEvents(); }, // Create an span element with inner text of the button and insert the corresponding SVG beside it initDOM: function(){ this.el.innerHTML = '' + this.el.innerHTML + ''; this.span = this.el.querySelector('span'); var div = document.createElement('div'); div.innerHTML = document.querySelector(this.options.svg).innerHTML; this.svg = div.querySelector('svg'); this.el.appendChild(this.svg); }, // Initialize the segments for all the paths of the loader itself, and for the success and error animations initSegments: function(){ for(var i = 0, paths = this.options.paths, len = paths.length; i < len; i++){ paths[i].el = this.svg.querySelector(paths[i].selector); paths[i].begin = paths[i].begin ? paths[i].begin : 0; paths[i].end = paths[i].end ? paths[i].end : 0.1; paths[i].segment = new Segment(paths[i].el, paths[i].begin, paths[i].end); } this.success = this.el.querySelector('.success-path'); this.error = this.el.querySelector('.error-path'); this.error2 = this.el.querySelector('.error-path2'); this.successSegment = new Segment(this.success, 0, 0.1); this.errorSegment = new Segment(this.error, 0, 0.1); this.errorSegment2 = new Segment(this.error2, 0, 0.1); }, // Initialize the click event in loading buttons, that trigger the animation initEvents: function(){ var self = this; self.el.addEventListener('click', function(){ self.el.disabled = 'disabled'; classie.add(self.el, 'open-loading'); self.span.innerHTML = 'Sending'; for(var i = 0, paths = self.options.paths, len = paths.length; i < len; i++){ paths[i].animation.call(self, paths[i].segment); } }, false); }, // Make it fail triggerFail: function(){ this.infinite = false; this.succeed = false; }, // Make it succeed triggerSuccess: function(){ this.infinite = false; this.succeed = true; }, // When each animation cycle is completed, check whether any feedback has triggered and call the feedback // handler, otherwise it restarts again completed: function(reset){ if(this.infinite){ for(var i = 0, paths = this.options.paths, len = paths.length; i < len; i++){ if(reset){ paths[i].segment.draw(0, 0.1); } paths[i].animation.call(this, paths[i].segment); } }else{ this.handleResponse(); } }, // Handle the feedback request, and perform the success or error animation handleResponse: function(){ for(var i = 0, paths = this.options.paths, len = paths.length; i < len; i++){ paths[i].el.style.visibility = 'hidden'; } if(this.succeed){ this.success.style.visibility = 'visible'; this.successAnimation(); }else{ this.error.style.visibility = 'visible'; this.error2.style.visibility = 'visible'; this.errorAnimation(); } }, // Success animation successAnimation: function(){ var self = this; self.successSegment.draw('100% - 50', '100%', 0.4, {callback: function(){ self.span.innerHTML = 'Succeed'; classie.add(self.el, 'succeed'); setTimeout(function(){ self.reset(); }, 2000); }}); }, // Error animation errorAnimation: function(){ var self = this; self.errorSegment.draw('100% - 42.5', '100%', 0.4); self.errorSegment2.draw('100% - 42.5', '100%', 0.4, {callback: function(){ self.span.innerHTML = 'Failed'; classie.add(self.el, 'failed'); setTimeout(function(){ self.reset(); }, 2000); }}); }, // Reset the entire loading button to the initial state reset: function(){ this.el.removeAttribute('disabled'); classie.remove(this.el, 'open-loading'); this.span.innerHTML = 'Send'; classie.remove(this.el, 'succeed'); classie.remove(this.el, 'failed'); this.resetSegments(); this.infinite = true; for(var i = 0, paths = this.options.paths, len = paths.length; i < len; i++){ paths[i].el.style.visibility = 'visible'; } this.success.style.visibility = 'hidden'; this.error.style.visibility = 'hidden'; this.error2.style.visibility = 'hidden'; }, // Reset the segments to the initial state resetSegments: function(){ for(var i = 0, paths = this.options.paths, len = paths.length; i < len; i++){ paths[i].segment.draw(paths[i].begin, paths[i].end); } this.successSegment.draw(0, 0.1); this.errorSegment.draw(0, 0.1); this.errorSegment2.draw(0, 0.1); } };
当然还应该为动画添加一些必要的CSS样式,下面使用的是SCSS代码:
// Loading button .loading-button{ // When loading button is open &.open-loading{ color: rgba(255, 255, 255, 0.8); &.infinity{ padding-top: 80px; } svg{ display: inline-block; visibility: visible; opacity: 1; transition: 1s opacity; transform: translateX(-50%); } } // Loading failed &.failed{ background-color: #EB7260; } // Loading succeed &.succeed{ background-color: #29ABA4; } // Remove transition when changing demo position &.no-transition{ transition: 0s; *{ transition: 0s; } } // SVG element, centered and hidden initially svg{ visibility: hidden; position: absolute; left: 50%; transform: translateX(-50%); opacity: 0; transition: 0s; path{ stroke-linecap: round; stroke-linejoin: round; stroke-width: 4; fill: none; // To hide success and error paths &.success-path, &.error-path, &.error-path2{ visibility: hidden; } } } } // Handle positions .loading-button { &.top { svg{ top: 10px; } } &.bottom { svg{ bottom: 10px; } } &.left { svg { top: 50%; transform: scale(0.25) translateY(-50%); transform-origin: 0 0 0; left: 20px; } } &.right { svg { top: 50%; transform: scale(0.25) translateY(-50%); transform-origin: 100% 0 0; left: auto; right: 20px; } } &.open-loading { &.left { padding-left: 60px; } &.right { padding-right: 60px; } &.top, &.bottom { svg{ transition-delay: 0.2s; } } &.circular-loading, &.circle-loading { &.top { padding-top: 140px; } &.bottom { padding-bottom: 140px; } } &.infinity-loading { &.top { padding-top: 80px; } &.bottom { padding-bottom: 80px; } } } }
使用新的SVG loading加载效果
如果要使用新的SVG loading加载效果,可以像下面这样将SVG代码放置在一个模板中:
<script type="text/template" id="circular-loading"> <svg width="120px" height="120px"> <path class="outer-path" stroke="#fff" d="M 60 60 m 0 -50 a 50 50 0 1 1 0 100 a 50 50 0 1 1 0 -100"></path> <path class="inner-path" stroke="rgba(255, 255, 255, 0.5)" d="M 60 60 m 0 -30 a 30 30 0 1 1 0 60 a 30 30 0 1 1 0 -60"></path> <path class="success-path" stroke="#fff" d="M 60 10 A 50 50 0 0 1 91 21 L 75 45 L 55 75 L 45 65"></path> <path class="error-path" stroke="#fff" d="M 60 10 A 50 50 0 0 1 95 25 L 45 75"></path> <path class="error-path2" stroke="#fff" d="M 60 30 A 30 30 0 0 1 81 81 L 45 45"></path> </svg> </script>
然后使用下面的代码来驱动新的loading动画。
function circularLoading(){ var button = document.querySelector('.loading-button'), options = { svg: '#circular-loading', paths: [ {selector: '.outer-path', animation: outerAnimation}, {selector: '.inner-path', animation: innerAnimation} ] }, loading = new LoadingButton(button, options); function outerAnimation(segment){ var self = this; segment.draw('15%', '25%', 0.2, {callback: function(){ segment.draw('75%', '150%', 0.3, {circular:true, callback: function(){ segment.draw('70%', '75%', 0.3, {circular:true, callback: function(){ segment.draw('100%', '100% + 0.1', 0.4, {circular:true, callback: function(){ self.completed(true); }}); }}); }}); }}); } function innerAnimation(segment){ segment.draw('20%', '80%', 0.6, {callback: function(){ segment.draw('100%', '100% + 0.1', 0.6, {circular:true}); }}); } return loading; }