可以做区位分析的网站/山东疫情最新情况
简介
帧动画就是在连续的关键帧中分解动画动作,也就是在时间轴上的每帧上逐帧绘制不同的内容,使其连续播放成动画。常见的帧动画方式有GIF,CSS3 animation和js。使用GIF和CSS3 animation的不足是不能灵活地控制动画的暂停和播放(比如点击暂停),也不能灵活地捕捉到动画完成的事件,另外在动画扩展性上js更好。
原理
js实现帧动画的有几种方法。一是用一个img标签去承载图片,定时改变img的src属性,这样显然不好。第二种是把所有动画关键帧绘制在一张图片里,把图片作为元素的background-image,定时改变元素的background-position(雪碧图)。对于只是位移的动画,可以通过js直接改变元素的left或者top值
实践
下面说一下第二种
var imgUrl = 'xx.png';var positions = ['0 -854', '-174 852', 'x y'...];var ele = document.getElementById('ele');animation(ele, positions, imgUrl);function animation(ele, positions, imgUrl){ele.style.backgroundImage = 'url(' + imgUrl + ')';ele.style.backgroundRepeat = 'no-repeat';var index = 0;function run(){var position = positions[index].split(' ');ele.style.backgroundPosition = position[0] + 'px ' + position[1] + 'px';index++;if(index >= positions.length){index = 0;}setTimeout(run, 80);}run();}
封装
封装一个动画对象我们一般会经过这四步,需求分析,设计编程接口,设计调用方式,最后是代码设计。
首先是需求分析,首先我们要支持图片预加载(图片加载是异步过程,我们要保证动画开始的时候图片已经加载完)。第二个是要支持单组动画控制循环次数(可以不断循环),第三个是可以切换到另一个动画。第四个是动画可以暂停和继续播放,第五个是支持动画完成后的回调函数。
根据以上需求我们设计了这样一个接口
loadImage(imglist) //预加载图片changePosition(ele, position, imageUrl) //通过改变元素的background-position实现动画enterFrame(callback) //每一帧动画执行的函数,可以自定义每一帧动画的callbackrepeat(times) //动画重复执行的次数,times为空时表示无限次repeatForever() //相当于上面不传参数的情况,这个接口比较友好wait(time) //每个动画执行之后等待的时间then(callback) //动画执行完成后的回调函数start(interval) //动画开始执行,interval表示动画执行的间隔pause() //动画暂停restart() //动画从上一次暂停处重新执行unmount() //释放资源
接下来我们设计一下调用方式,首先我们希望支持链式调用,如下面的例子
var animation = require('animation');var demo = animation().loadImage(images).changePosition(ele, position, imgUrl).repeat(2).then(function(){//所有动画执行后的回调})demo.start(80);
最后是代码设计,我们把图片预加载,动画执行,动画结束等看成一个任务队列(js的数组)
任务队列中有两种类型的任务,一种是同步执行完成(从预加载到动画执行),另一种是异步定时执行的。
那么我们需要记录当前任务链的索引,在每个任务执行完毕后,通过调用next方法,执行下一个任务,同时更新任务链索引值
代码编写
我们先定义一个基本框架
function Animation(){ }Animation.prototype = {loadImage: function(imglist){},//添加一个异步定时任务changePosition: function(ele, positions, imageUrl){},//每一帧动画执行的回调函数enterFrame: function(taskFn){},//在一个任务结束时的回调函数then: function(callback){},start: function(interval){},//添加一个同步任务,该任务是回退到上一个任务中,实现重复上一个任务的效果repeat: function(times){},//无限循环上一次任务repeatForever: function(){},//设置当前任务结束后到下个任务前的等待时间timewait: function(time){},pause: function(){},restart: function(){},unmount: function(){}}
接下来我们开始填充具体代码
//类似常量的写法,在ES6中使用const关键字var STATE_INITIAL = 0;var STATE_START = 0;var STATE_STOP = 0;//同步任务var TASK_SYNC = 0;var TASK_ASYNC = 1;//CMD写法,引入imageloader函数(后面会详细讲)var loadImage = require('./imageloader');var Timeline = require('./timeline');//简单的函数封装,执行callbackfunction next(callback){//如果callback没传进来,undefined就退出callback && callback();}function Animation(){ this.taskQueue = [];this.index = 0;this.state = STATE_INITIAL;this.timeline = new Timeline();}Animation.prototype = {//类内部使用//添加一个任务到任务队列_add: function(taskFn, type){this.taskQueue.push({taskFn: taskFn,type: type})return this;},_runTask: function(){if(!this.taskQueue || this.state !== STATE_START){return;}if(this.index === this.taskQueue.length){this.unmount();return;}var task = this.taskQueue[index];if(task.type === TASK_SYNC){this._syncTask(task);} else {this._asyncTask(task);}},_syncTask: function(task){var self = this;var next = function(){//切换到下一个任务self._next(task);}var taskFn = task.taskFn;taskFn(next);},_asyncTask: function(task){var self = this;//定义每一帧执行的回调函数var enterFrame = function(time){var taskFn = task.taskFn;var next = function(){//停止当前任务self.timeline.stop();self._next(task);}taskFn(next, time);}this.timeline.onenterframe = enterFrame;this.timeline.start(this.interval);},_next: function(task){this.index++;//如果有wait属性,设置等待时间var self = this;task.wait ? setTimeout(function(){self._runTask();}, task.wait) : this._runTask();},loadImage: function(imglist){var taskFn = function(next){//把next当作callback传入loadImage(imglist.slice(), next);}var type = TASK_SYNC;//把this实例传出去,那么在_add方法中也要return thisreturn this._add(taskFn, type);},//添加一个异步定时任务changePosition: function(ele, positions, imageUrl){var len = positions.length;var taskFn;var type;if(len){var self = this;taskFn = function(next, time){if(imgUrl){ele.style.backgroundImage = 'url(' + imageUrl + ')';}//获得当前背景图片位置索引// | 0 相当于Math.floorvar index = Math.min(time/self.interval | 0, len - 1);var position = positions[index].split(' ');//改变dom对象的背景图片位置ele.style.backgroundPosition = position[0] + 'px ' + position[1] + 'px';if(index === len - 1){next();}}type = TASK_ASYNC;} else {taskFn = next;type = TASK_SYNC;}return this._add(taskFn, type);},//每一帧动画执行的回调函数enterFrame: function(taskFn){return this._add(taskFn, TASK_ASYNC);},//在一个任务结束时的回调函数then: function(callback){var taskFn = function(next){callback();next();}var type = TASK_SYNC;return this._add(taskFn, type);},start: function(interval){if(this.state === STATE_START){return this;}if(!this.taskQueue.length){return this;}this.state = STATE_START;this._runTask();return this;},//添加一个同步任务,该任务是回退到上一个任务中,实现重复上一个任务的效果repeat: function(times){var self = this;var taskFn = function(){if(typeof times === 'undefined'){//无限回退到上一个任务self.index--;self._runTask();return;}if(times){times--;self.index--;self._runTask(); } else {//达到重复次数var task = self.taskQueue[self.index];self._next(task);}}var type = TASK_SYNC;return this._add(taskFn, type);},//无限循环上一次任务repeatForever: function(){return this.repeat();},//设置当前任务结束后到下个任务前的等待时间wait: function(){if(this.taskQueue && this.taskQueue.length > 0){this.taskQueue[this.taskQueue.length - 1].wait = time;}return this;},pause: function(){if(this.state === STATE_START){this.state = STATE_STOP;this.timeline.stop();return this;}return this;},restart: function(){if(this.state === STATE_STOP){this.state = STATE_START;this.timeline.restart();return this;}return this;},unmount: function(){if(this.state !== STATE_INITIAL){this.state = STATE_INITIAL;this.taskQueue = null;this.timeline.stop();this.timeline = null;return this;}}}module.exports = function(){//类似工厂模式return new Animation();}
我们在imgeloader.js中定义一个预加载模块
//images为加载图片的数组或者对象//callback 全部图片加载完毕后的回调函数//timeout 加载超时的时长function loadImage(images, callback, timeout){//图片路径记数器var count = 0;//全部图片加载成功的tagvar success = true;var timeoutId = 0;var isTimeout = false;for(var key in images){//过滤掉原型上的属性if(!images.hasOwnProperty(key)){continue;}var item = images[key];//如果item是一个string的时候,当作src处理if(typeof item === 'string'){//连等表示前两个等于第三个item = image[key] = {src: item}}if(!item || !item.src){continue;}count++;//设置图片的iditem.id = '_img_'+ key + getId();item.img = window[item.id] = new Image();doload(item);}//如果数组的数据验证失败,count为0,直接调用回调//这里是对count任务的同步验证,count还没开始--if(!count){callback(false);} else if(timeout){timeoutId = setTimeout(onTimeout, timeout);}function doload(item){item.status = 'loading';var img = item.img;//加载成功img.onload = function(){success = success && true;item.status = 'loaded';done();}img.onerror = function(){item.status = 'error';success = false;done();}//发起一个http/https请求img.src = item.src;//图片加载完成function done(){img.onload = img.onerror = null;try {delete window[item.id];} catch(e){}//如果count为0的时候//所有图片加载且没有超时情况,清除计时器且执行回调if(!--count && !isTimeout){clearTimeout(timeoutId);callback(success);}}//超时处理函数function onTimeout(timeout){isTimeout = true;callback(false);} }}//_id变量不会污染全局,只是在这个模块闭包中var _id = 0;function getId(){return ++_id;}module.exports = loadImage;
然后使用一个模块timeline.js来取代定时器,因为在浏览器运行环境中定时器是不准的
//setTimeout的定时器值推荐最小使用16.7ms的原因(16.7 = 1000 / 60, 即每秒60帧)var DEFAULT_INTERVAL = 1000 / 60;var STATE_INITIAL = 0;var STATE_START = 1;var STATE_STOP = 2;var requestAnimaitionFrame = (function(){return window.requestAnimationFrame ||//chromewindow.webkitRequestAnimationFrame ||//firefoxwindow.mozRequestAnimationFrame ||//operawindow.oRequestAnimationFrame || function(callback){return window.setTimeout(callback, callback.interval || DEFAULT_INTERVAL);}})()var cancelAnimationFrame = (function(){//类似上面的兼容return window.cancelAnimationFrame ...})()//时间轴类function TimeLine(){this.animationHandler = 0;this.state = STATE_INITIAL;}TimeLine.prototype = {//时间轴上每一次回调执行的函数//time是从动画开始到当前执行的时间onenterframe: function(time){//在主对象中定义},//interval 每一次回调的间隔时间start: function(interval){if(this.state === STATE_START){return ;}this.state = STATE_START;this.interval = interval || DEFAULT_INTERVAL;//+new Date相当于new Date().getTime()startTimeline(this, +new Date());},stop: function(){if(this.state !== STATE_START){return;}this.state = STATE_STOP;//记录动画从开始到现在经历的时间if(this.startTime){this.dur = +new Date() - this.startTime;}cancelAnimationFrame(this.animationHandler);},restart: funtion(){if(this.state === STATE_START){return;}if(!this.dur || this.interval){return;}this.state = STATE_START;//无缝连接动画startTimeline(this, +new Date() - this.dur);}}//时间轴动画启动函数function startTimeline(timeline, startTime){timeline.startTime = startTime;nextTick.interval = timeline.interval;//记录上一次回调的时间戳,tick的意思是时钟的滴答var lastTick = +new Date();nextTick();//每一帧执行的函数function nextTick(){var now = +new Date();//每17毫秒刷新一次,更精确的定时器timeline.animationHandler = requestAnimationFrame(nextTick);//如果当前时间与上一次回调的时间戳大于设置的时间间隔,表示这次可以执行回调函数if(now - lastTick >= timeline.interval){timeline.onenterframe(now - startTime);lastTick = now;}}}module.exports = Timeline;