百度前端技术学院是一个为大学生创办的免费的前端技术实践、分享、交流平台。由百度校园招聘组、百度校园品牌部、百度前端技术部以及多个百度的前端团队联合创办。学院组织了一批百度在职工程师,精心编写了数十个实践编码任务,将技术知识点系统有机地串联在各个充满趣味与挑战的任务中,同学们通过实际地编码练习来掌握知识,再辅以互相评价、学习笔记等方式,加深对于学习内容的理解。在过去的三年中,百度前端技术学院累积吸引了上万名同学参加,并且有数十名同学在学习后,顺利加入了百度,成为了百度的前端工程师。

当我们讨论动画时,我们在讨论什么

作者周易课程封装DOM动画类库(一)7418次浏览32017-04-19 18:41

当我们讨论动画时,我们在讨论什么

由于任务已经截止,无法发布新笔记,原计划另开一坑的color篇现也更新到这里。


transform篇

基本完成了IFE的动画库任务(一),内心是崩溃的。JS补间动画的原理的确很简单,但在实现过程中,才意识到以前用的成熟的动画库,做了多少的工作。

JS动画实现原理

对于多数成熟的动画库,你只需要提供动画的末状态 end 与动画时长 duration,它就能帮你绘制一段缓动动画,其原理很是单纯,大体可以拆成以下几步:

  1. 根据你提供的末状态 end,获得元素对应的初状态 begin
  2. 通过初末状态,获得状态的变化量 change
  3. 定义一个变量来保存动画进行到的时间,如lastTime;一个变量startTime获得动画开始时的时间(new Date())
  4. 获得当前时间currTime(new Date()),的lastTime = currTime - startTime,将 begin,change,lastTime和duration传入缓动函数,计算出当前时间的状态,并进行设置
  5. 如果lastTime大于duration表明这段动画执行完毕,直接将位置定位到end;否则回到步骤4

但就是这简单的5步,在实现的过程中有无数的坑啊......。

PS: 缓动函数的用法,无耻的贴上如何使用Tween.js各类原生动画运动缓动算法,这次的重点不是这个。

如何从传入的属性值获得必要的数据

为了便于使用,传入的属性值是简单的CSS属性,如:

{
  width: "12px",
  height: "12px"
}

为了将其作为末状态,我们需要提取出

  1. 需要改变的属性名
  2. 对应属性值的数值,因为要计算中间值
  3. 对应属性值的单位

我们可以用正则表达式来匹配12px中的px,将匹配结果保存即可得到单位,再将匹配到的px换成'',即可得到数值的字符串形式。这里要对transform系列的属性值做特别处理,由于transform的属性值是rotate(30deg)这种形式,所以无法直接处理,于是参考其他动画库,对于transform的属性值特别传入,如:

{
  width: "12px",
  height: "12px",
  rotateZ: "30deg",
  translateX: "20px"
}

然后对其的单位提取步骤便可以统一。

如何获得初始状态

初始状态的获得主要依靠getComputedStyle(el, null).getPropertyValue(propertyName)方法,由于要传入属性名,所以肯定要依据添加的动画末状态来找到对应的属性名。这里依然是要特别处理transform系列的属性值,可以用一个正则来匹配末状态传入的属性名,如果是rotateZ这样的,就将transform作为参数传入上面的方法,来获得初始状态的属性...................?结果是返回了matrix(a,b,c,d,e,f)这样的属性值,这其中缘由可见理解CSS3 transform中的Matrix(矩阵),总之我们可以方便的从rotate值计算出matrix值,但是想要反过来(主要是获得rotate值,涉及到三角函数)却很不精确,暂时不考虑这种方法。事实上我们希望能像获取元素的style属性一样,直接得到rotate属性值,但对于设置在CSS样式表里的属性,的确无法直接得到。so目前的一个很坑的地方——对于一个在样式表里设置了transform属性的元素,在第一次进行动画时,无法获得其初状态......这个问题在当前的velocityJs中也仍然存在,会直接从0的值开始动画,这里的解决方法暂时还没有寻得。但至少,之后的动画我们还是有办法掌控的。

如何在计算完成后,设置元素的状态

transform系的没什么好讲的,直接style = 数值 + 单位即可。但是由于transform的属性值很有可能不止一个,如果直接给style赋新值,将会覆盖所有transform属性。解决方法:定义一个transformCache对象,如果有transform属性需要设置,就在transformCache查找是否存在该属性:不存在便定义该属性与对应属性值,如果存在就更新属性值;然后遍历所有的属性,拼接出完整的transform属性。


由于transform有关的很多地方都要特殊处理,所以设计模式的运用得当在该情形下尤为重要,我自己写的似乎就有些混乱了......而且初始状态的transform获取还没有解决,待更新......


color篇

设计模式的运用

除了上篇的transform动画需要特殊处理,还有一个大头就是属性值为颜色的处理。不同于transform,属性值为颜色的属性可以有很多种,而颜色属性值又有rgb、hsl、rgba甚至十六进制等多种表示方式,意味着两者对属性名、属性值分离的逻辑也是不同的,再加上对普通属性处理的逻辑,至少有三种不同的分离逻辑。因此在加入颜色的处理方法之前,我先用策略模式重构了这部分逻辑,最后结果如下:

/**
 * 对porps属性值处理,获得渲染时所需的数据
 */
Rush.prototype._handleProps = (function() {
    // transform 的属性需要特别处理
    const transformProperties = ["translateX", "translateY", "translateZ", "scale", "scaleX", "scaleY", "scaleZ", "skewX", "skewY", "rotateX", "rotateY", "rotateZ"];

    // color 的属性需要特别处理
    const colorProperties = ["color", "background-color", "border-color", "outline-color"];

    var propertyHandler = {};

    // 普通属性的处理方法
    propertyHandler['default'] = function(task, key) {
        var el = this.el;

        var begin; // 初始属性值和单位
        var end = propertyValueHandler(key, task.props[key]); // 末属性数值和单位

        var realPropertyName = key; // 真正的属性名
        var styleLogic = 'default';

        var beginValue = getComputedStyle(el, null).getPropertyValue(realPropertyName); // 获得初始属性值(带单位)

        begin = propertyValueHandler(key, beginValue); // 获得属性数值和单位

        realPropertyName = transferStyleName(realPropertyName); // 将连字符格式转换为驼峰式

        // 为task新增属性
        task.newProps[key] = {
            begin,
            end,
            realPropertyName,
            styleLogic
        }
    }

    // transform属性的处理方法
    for (var propertyName of transformProperties) {
        propertyHandler[propertyName] = function(task, key) {
            var el = this.el;

            var begin; // 初始属性值和单位
            var end = propertyValueHandler(key, task.props[key]); // 末属性数值和单位

            var realPropertyName = 'transform';
            var styleLogic = 'transform';

            var beginValue; // 初始属性值(带单位)

            // 如果已经缓存了transform属性
            if (el.transformCache) {
                if (el.transformCache[key]) {
                    beginValue = el.transformCache[key].value;
                } else {
                    beginValue = 0;
                    el.transformCache[key] = {
                        value: beginValue,
                        unitType: end.unitType
                    };
                }
            } else {
                // 只有在元素没有在style中定义任何transform属性时才会调用
                beginValue = 0;

                // 给这个元素添加transfromCache属性,用于保存transfrom的各个属性
                // 因为如果style中的transform被设置了多个值,读取到的将是"rotate(30deg) translateX(10px)"这样的值,将无法处理
                el.transformCache = {};
                el.transformCache[key] = {
                    value: beginValue,
                    unitType: end.unitType
                };
            }

            begin = propertyValueHandler(key, beginValue); // 获得属性数值和单位

            // 为task新增属性
            task.newProps[key] = {
                begin,
                end,
                realPropertyName,
                styleLogic
            }
        }
    }

    // color属性的处理方法,统一转换为rgba来处理
    for (var propertyName of colorProperties) {
        propertyHandler[propertyName] = function(task, key) {
            var el = this.el;

            var begin;
            var end = normalize2rgba(task.props[key]);

            var realPropertyName = key;

            var beginValue = getComputedStyle(el, null).getPropertyValue(realPropertyName); // e.g. rgba(255, 255, 255, 1);
            begin = normalize2rgba(beginValue); // 返回的是转换后的rgba对象

            var styleLogic = 'rgba';

            realPropertyName = transferStyleName(realPropertyName); // 将连字符格式转换为驼峰式


            task.newProps[key] = {
                begin,
                end,
                realPropertyName,
                styleLogic
            }
        }
    }

    return function(task) {
        var el = this.el;

        task.newProps = {}; // 保存渲染动画时所需的数据

        for (var key in task.props) {
            if (propertyHandler[key]) { // 特殊属性
                propertyHandler[key].call(this, task, key);
            } else { // 普通属性
                propertyHandler['default'].call(this, task, key);
            }
        }
    }
})();

color的痛点

  • color的多种属性值

    属性值可能是rgb色,hsl色,rgba色甚至十六进制色,为他们各做一种处理逻辑显然太过复杂且没有必要,将其全部转换成同一单位再进行处理更加明智。考虑到透明度,我觉得全部转换为rgba最为合适。颜色转换的逻辑到处都能找到就不细说了。

  • rgba属性值的分离

    不同于之前所有的属性值,rgba值有4个数值,考虑到我们的缓动函数每次只处理一个数值,需要用数组保存再进行分别处理

    for (var key in task.newProps) {
                if (typeof task.newProps[key].begin.num !== 'number') {
                    var beginArr = task.newProps[key].begin.num;
                    var endArr = task.newProps[key].end.num;
    
                    var newArr = [];
                    for (var i = 0; i < beginArr.length; i++) {
                        var beginValue = beginArr[i],
                            changeValue = endArr[i] - beginValue,
                            newValue = easing(currTime, beginValue, changeValue, duration); // 根据缓动函数计算新的位置
                            newArr.push(newValue);
                    };
    
                    self.styleHandler(task, key, newArr);
                } else {
                    var beginValue = task.newProps[key].begin.num, // 初始位置
                        changeValue = task.newProps[key].end.num - beginValue; // 位置改变量
    
                    var newValue = easing(currTime, beginValue, changeValue, duration); // 根据缓动函数计算新的位置
    
                    // 更新style
                    self.styleHandler(task, key, newValue);
                }
            }
    
  • 将处理后得到的新属性重新拼接为style样式

    同样是采取策略模式,为不同的拼接逻辑定义了对应的拼接方法

    Rush.prototype.styleHandler = (function() {
        var t = {
            'transform': function(task, key, newValue) {
                this.el.transformCache[key].value = newValue; // 更新缓存值
    
                var propertyValue = '',
                    propertyName = task.newProps[key].realPropertyName;
    
                // e.g transform: rotateZ(100deg) translateX(50px)
                for (var key in this.el.transformCache) {
                    var name = key, // e.g rotateZ
                        val = this.el.transformCache[key].value, // e.g 100
                        unitType = this.el.transformCache[key].unitType; // e.g deg
    
                    propertyValue += `${name}(${val}${unitType})`; // e.g rotate(100deg)
                }
    
                this.el.style[propertyName] = propertyValue;
            },
    
            'rgba': function(task, key, newArr) {
                var text = 'rgba(';
    
                for (var i = 0; i < newArr.length - 1; i++) {
                    text += (newArr[i]).toFixed() + ', ';
                }
                text += newArr[newArr.length - 1].toFixed(2) + ')';
    
                this.el.style[task.newProps[key].realPropertyName] = text;
            },
    
            'default': function(task, key, newValue) {
                this.el.style[task.newProps[key].realPropertyName] = `${newValue}${task.newProps[key].end.unitType}`;
            }
        };
    
        return function(task, key, newValue) {
            var styleLogic = task.newProps[key].styleLogic;
    
            t[styleLogic].call(this, task, key, newValue);
        };
    })();
    

至此,通用js动画库的两大难点基本得到解决,虽说是针对transform和color的处理方法,但在编码过程中,如何运用设计模式优化代码结构或许花费了更多的时间。这个任务还有很多需要细化的地方,俺完成后可能会再更新,如有意见欢迎提出。

完成了演示以及文档:rushB.js中文文档

1条评论