d3 插值-实现一个简单的 animation

本篇介绍插值的一种典型应用 动画: (搜索关键词: 补间动画)

要实现一个动画, 一般会在动画过程中定义关键帧, 然后在帧之间创建"补间": 计算出在每一个时间点下物体的形状、位置、颜色等信息, 然后绘制物体即可;

 

一个简单的支持插入关键帧的动画方法, 以下是完整示例代码; 

 

<!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, width: 400}, 5000, easeInOut).addTween(
                {
                    k: 0.4,
                    value: {
                        left: 300,
                        top: 500,
                        text: 300,
                        width: 140
                    }
                },
                {
                    k: 0.6,
                    value: {
                        left: 1000,
                        top: 300,
                        text: 400,
                        width: 300
                    }
                },
                {
                    k: 0.8,
                    value: {
                        left: 800,
                        top: 500,
                        text: 200,
                        width: 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>

 

posted @ 2020-11-25 01:45  芋头圆  阅读(116)  评论(0编辑  收藏  举报