本篇介绍插值的一种典型应用 动画: (搜索关键词: 补间动画)
要实现一个动画, 一般会在动画过程中定义关键帧, 然后在帧之间创建"补间": 计算出在每一个时间点下物体的形状、位置、颜色等信息, 然后绘制物体即可;
一个简单的支持插入关键帧的动画方法, 以下是完整示例代码;
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .box{ width: 200px; height: 200px; border: 5px solid #333; background-color: green; font-size: 30px; color: #fff; font-weight: bold; } </style> </head> <body> <div class="box">这是一个盒子</div> <script> window.onload = function() { var elem = document.querySelector('.box'); var animate = new Animate({left: 0, top: 0, text: 1}, {left: 1200, top: 300, text: 500, 400}, 5000, easeInOut).addTween( { k: 0.4, value: { left: 300, top: 500, text: 300, 140 } }, { k: 0.6, value: { left: 1000, top: 300, text: 400, 300 } }, { k: 0.8, value: { left: 800, top: 500, text: 200, 200 } } ); var time1 = Date.now() animate.begin() animate.on('update', function(attrs) { elem.innerHTML = parseInt(attrs.text); elem.style.width = (attrs.width || 100) + 'px' elem.style.transform = `translate(${attrs.left}px, ${attrs.top}px)`; }) animate.on('finish', function(attrs) { var time2 = Date.now() console.log('动画结束!', attrs) console.log('累计耗时::', time2 - time1); }) elem.addEventListener('click', function(e) { if (e.target.dataset.lock == '1') { animate.continue() e.target.dataset.lock = '0' } else { animate.pause() e.target.dataset.lock = '1' } }) } function linear(k) { return k } function easeIn(k) { return k * k; } function easeInOut(k) { if ((k *= 2) < 1) { return 0.5 * k * k; } return - 0.5 * (--k * (k - 2) - 1); } function type(value) { return Object.prototype.toString.call(value).match(/[A-Z][a-z]+/)[0].toLowerCase() } function Animate(start, end, duration, delay, easeing){ this.start = start; this.now = start this.end = end; this.duration = duration || 1000; this.easeing = easeing || linear; this.initTimestamp = Date.now(); this.stackTimestamp = 0; this.requestAnimation = null this.eventHandlers = {} // this.delay = delay || 0 this.dis = [Animate.sub(start, end)]; this.frames = [{value: this.start, k: 0}, {value: this.end, k: 1}] } // Animate.prototype.resetFrameDis = function() { var i = 0, len = this.frames.length, res = [], next, cur for(;i < len-1;i++ ) { next = this.frames[i+1] cur = this.frames[i] res.push(Animate.sub(cur.value, next.value)) } this.dis = res } // Animate.prototype.emit = function(key) { var list = this.eventHandlers[key]; var _this = this; if (list && list.length) { list.forEach(function(fn) { typeof fn === 'function' && fn(_this.now) }) } } // Animate.prototype.on = function(key, callback) { this.eventHandlers[key] = this.eventHandlers[key] || []; this.eventHandlers[key].push(callback) } // Animate.prototype.off = function(key, callback) { if (callback === undefined && typeof key === 'string') { delete this.eventHandlers[key] } else if (callback === undefined && key === undefined) { this.eventHandlers = {} } else if (typeof callback === 'function' && typeof key === 'string') { var index = this.eventHandlers[key].indexOf(callback); if (index === -1) { console.error('Error: function ', callback.name + ' is not in quene') } else { this.eventHandlers[key].splice(index, 1) } } } // Animate.prototype.pause = function() { this.stackTimestamp += (Date.now() - this.initTimestamp) window.cancelAnimationFrame(this.requestAnimation) } // Animate.prototype.continue = function() { this.reset(Date.now()) this.begin(); } // Animate.prototype.reset = function(timestamp) { this.initTimestamp = timestamp; } Animate.prototype.update = function() { var curTimestamp = Date.now(); var now var k = Animate.climp((curTimestamp - this.initTimestamp + this.stackTimestamp) / this.duration, 0, 1) var currentFrameIndex, currentFrameEaseing, dis; if (this.frames.length === 2) { currentFrameEaseing = this.easeing(k) dis = Animate.multi(this.dis[0], currentFrameEaseing) this.now = Animate.add(this.frames[0].value, dis); } else { currentFrameIndex = this.getFrameIndex(k) currentFrameEaseing = this.easeing(this.getFrameEasing(k)); dis = Animate.multi(this.dis[currentFrameIndex], currentFrameEaseing) this.now = Animate.add(this.frames[currentFrameIndex].value, dis); } } Animate.prototype.begin = function() { var _this = this; var tick = function() { if (_this.isFinish()) { _this.emit('finish') window.cancelAnimationFrame(_this.requestAnimation) return } else { _this.update() _this.emit('update') _this.requestAnimation = window.requestAnimationFrame(tick) } } tick() } Animate.prototype.isFinish = function() { return Animate.compareRightIsBigger(this.end, this.now) } // Animate.prototype.getFrameIndex = function(k) { var next = 0, i = 0, len = this.frames.length, ck; for (; i<len; i++) { ck = this.frames[i].k; if (k > ck) { next = i } } return next } // Animate.prototype.getFrameEasing = function(k) { var i = 0, len = this.frames.length, cur, next, pre; for (; i<len; i++) { cur = this.frames[i]; if (k > cur.k) { pre = cur.k next = this.frames[i+1] ? this.frames[i+1].k : 1 } } return (k - pre) / (next - pre) } // 添加关键帧 Animate.prototype.addTween = function() { var args = arguments, len = args.length, frame = [], _k; for(var i = 0; i< args.length; i++) { _k = args[i].k if (args[i].hasOwnProperty('k') && _k > 0 && _k < 1) { frame.push(args[i]) } else { _k = (1 / (len + 1)) * i; frame.push({k: _k, value: args[i]}) } } this.frames = this.frames.concat(frame) this.frames.sort(function(f1, f2) { return f1.k < f2.k ? -1 : 1 }); this.resetFrameDis() return this } // 比较值大小 Animate.compareRightIsBigger = function(min, max) { var tv1 = type(min); var tv2 = type(max); if (tv1 !== tv2) { return Number(min) <= Number(max) ? true : undefined } else { if (tv1 === 'number') { return min <= max } else if (tv1 === 'date') { return min <= max } else if (tv1 === 'string') { return Number(min) <= Number(max) } else if (tv1 === 'array') { return min.every(function(v, i) { return min[i] == undefined || max[i] == undefined ? trye : min[i] <= max[i] }) } else if (tv1 === 'object') { var isBigger = true for (var key in min) { if (!max.hasOwnProperty(key) || max[key] == undefined || min[key] == undefined) { continue } isBigger = min[key] <= max[key] if (isBigger === false) { break } } return isBigger } } } // 相加 Animate.add = function(value1, value2) { var tv1 = type(value1); var tv2 = type(value2); var output; if (tv1 !== tv2) { output = Number(value1) + Number(value2) return Animate.isNaN(output) ? undefined : output } if (tv1 === 'number') { return value1 + value2 } else if (tv1 === 'string') { return Number(value1) + Number(value2) } else if (tv1 === 'date') { return value1 + value2 } else if (tv1 === 'array') { output = [] value1.forEach(function(v, i) { if (value1[i] != undefined || value2[i] != undefined) { output[i] = Number(value1[i]) + Number(value2[i]) } }) return output } else if (tv1 === 'object') { output = {} for (var key in value1) { if (!value2.hasOwnProperty(key) || value2[key] == undefined || value1[key] == undefined) { continue } output[key] = Number(value1[key]) + Number(value2[key]) } return output } } // 相乘 Animate.multi = function(value, k) { var tv = type(value), output; if (tv === 'string' && !Animate.isNaN(Number(value)) ) { return +value * k } else if (tv === 'number') { return value * k } else if (tv === 'date') { return value * k } else if (tv === 'array') { output = [] value.forEach(function(v, i) { if (v != undefined) { output[i] = +v * k } }) return output } else if (tv === 'object') { output = {} for (var key in value) { if (value != undefined) { output[key] = +value[key] * k } } return output } else { return output } } // 相减 Animate.sub = function(start, end) { var ts = type(start); var te = type(end); var i = 0; var lenS, lenE, minLen, key, res if (ts !== te) { res = Number(end) - Number(start) return Animate.isNaN(res) ? undefined : res } if (ts === 'number') { res = end - start } else if (ts === 'date') { res = end - start } else if (ts === 'string') { res = +end - start res = Animate.isNaN(res) ? undefined : res } else if (ts === 'array') { lenS = start.length; lenE = end.length; minLen = Math.min(lenE, lenS); res = [] for (; i < minLen; i++) { if (end[i] == undefined || start[i] == undefined) { res[i] = null } else { res[i] = +end[i] - start[i] } } } else if (ts === 'object') { res = {} for(key in start) { if (!end.hasOwnProperty(key) || end[key] == undefined || start[key] == undefined) { continue } res[key] = +end[key] - start[key] } } return res } // Animate.type = function(v) { return Object.prototype.toString.call(value).match(/[A-Z][a-z]+/)[0].toLowerCase() } Animate.isNaN = function(v) { return v !== v } Animate.getPrecision = function(v) { return v - parseInt(v) === 0 ? 1 : v - parseInt(v) } // Animate.climp = function(v, min, max) { if (v <= min) { return min } else if (v >= max) { return max } else { return v } } </script> </body> </html>