模仿 velocity.js 实现 DOM 动画类库(一)

动画的原理就是每隔一段时间改变画面,这个时间小到眼睛无法识别,所以看起来就像是画面在动。

DOM 动画也是一样的,每隔一定时间就改变 DOM 的某个 CSS 属性值,比如宽度、高度透明度等等,从而实现了我们所看到的 DOM 动画。

当然实现一个 DOM 动画类库并不是很困难,但一开始就要很完善很完美就很困难了,所以我准备从最简单的入手,先实现透明度动画,再实现透明度与宽度同时动画。

为什么先实现透明度呢?因为opacity的值没有单位,不像widthpx单位,要改变width就要先分割值与单位,将值做改变后加上单位;也不像background-color一样可能是十六位进制#fff也可能是rgb(0, 0, 0)需要额外处理的。

流程图

当然上面说的原理太虚无缥缈了,如果用流程图说明,大概就是这样的:

伪代码流程图

实际代码

落实到代码,总共有 5 个核心函数

  • 获取开始值 getPropertyValue => 步骤2
  • 分割属性值与单位 separateValue => 步骤2
  • 动画函数 tick => 步骤 4
  • 缓动函数计算当前值 easing => 步骤 4.1
  • 改变 DOM 属性 setPropertyValue => 步骤 4.2

之后的代码都以实现下面的div透明度变化为目标。

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>测试</title>
    <script src="./src/fakeVelocity.js"></script>
    <style>
        #div {
            width: 300px;
            height: 180px;
            background-color: red;
        }
    </style>
</head>
<body>
    <div id="div"></div>
    <button id="run">点击执行动画</button>
    <script>
        window.onload = function () {
            document.querySelector('#run').onclick = function () {
                Animation(document.querySelector('#div'), {
                    opacity: 0.5
                })
            }
        }
    </script>
</body>
</html>

getPropertyValue

首先要获取到#div的初始透明度值,div.style.opacity?结果是空,这里需要用到getComputedStylegetPropertyValue

function getPropertyValue (element, property) {
    return window.getComputedStyle(element, null).getPropertyValue(property)
}

getPropertyValuegetComputedStyle返回对象的方法,所以不用担心会和我们自己定义的getPropertyValue冲突。

function Animation (element, propertiesMap) {
    let property,
         startValue,
         endValue,
         unitType
    for(property in propertiesMap) {
        startValue = getPropertyValue(element, propertiesMap)
        endValue = propertiesMap[property]
        console.log(startValue, endValue)
    }
}

此时能够正确获取到动画开始值与动画结束值分别为10.5

separateValue

当然这个函数现在并没有用,因为opacity没有单位,但是我们知道后续需要增加有单位的值,所以先将这个函数声明好。

function separateValue (property, value) {
    return [value, '']
}

调用该函数后,将返回数组,第一个元素为值,第二个元素为单位。

function Animation(element, propertiesMap) {
    let property,
        startValue,
        endValue,
        unitType
    for(property in propertiesMap) {
        startValue = getPropertyValue(element, property)
        // 分割值与单位
        const separatedValue = separateValue(property, startValue)
        // 2、真正的开始值
        startValue = separatedValue[0]
        // 2、单位
        unitType = separatedValue[1]
        // 2、结束值
        endValue = propertiesMap[property]
    }
}

OK,回过头看看流程图,现在到了第三步,准备执行动画函数tick了。

tick

先来声明好这个动画函数,前面也提到了,在这个函数内部,每次调用都会获取到当前时间,并与调用前声明的startTime进行比对,如果currentTime - startTime >= duration就结束动画,否则就再调用一次tick

duration就是动画持续时间,通过配置项传入,这里暂时写死在代码中。

const opts = {
    duration: 400
}
3、准备调用动画函数
const startTime = new Date().getTime()
function tick () {
    // 这次调用的当前时间
    let currentTime = new Date().getTime()
    // 5、计算动画时间是否结束 (>= duration)
    const percentComplete = Math.min((currentTime - startTime) / opts.duration, 1)
    // 当前透明度的值,准备用来修改 DOM 对应属性
    let currentValue
    // 如果已经执行的动画时间大于动画应该执行的时间,就将值直接赋为结束值
    if (percentComplete === 1) {
        currentValue = endValue
    } else {
        // 4.1 计算当前值应该是多少
        currentValue = parseFloat(startValue) + (endValue - startValue) * easing['swing'](percentComplete)
    }
    console.log(currentValue)
}
// 4、调用动画函数
tick()

先看5、计算动画时间是否结束,这里并没有按照之前说的计算方法currentTime - startTime >= duration计算动画是否结束,而是使用了比较

  • (currentTime - startTime) / duration
  • 1

这两个值的大小,取更小的那个值。当然currentTime - startTime大于等于duration时,才会是1更小,道理是相同的,不过因为

(currentTime - startTime) / duration

这个值需要用在缓动函数内,所以就不做两次处理了,当然这样也是可以的,但是没必要不是吗?

if ((currentTime - startTime) >= duration) {
    percentComplete = 1
} else {
    percentComplete = (currentTime - startTime) / duration
}

缓动函数

这个就是直接拿现成的算法来用了,上面代码是这样使用的:

currentValue = parseFloat(startValue) + (endValue - startValue) * easing['swing'](percentComplete)

重点在后面的easing['swing'](percentComplete),很容易理解,easing是一个对象,有swing属性,并且对应的值是一个函数。

// easing 缓动函数
easing = {
    swing: function (a) {
        return .5 - Math.cos(a * Math.PI) / 2
    },
    Sine: function (p) {
        return 1 - Math.cos(p * Math.PI / 2)
    },
    Circ: function (p) {
        return 1 - Math.sqrt(1 - p * p)
    }
}

这么做的好处很明显,如果我不想用swing这个缓动函数而想换一个,这样就可以:

easing[opts.easing](percentComplete)

opts.easing只要传不同的字符串,就能够直接调用对应的函数,而且还可以让用户自己拓展easing这个对象,只要opts.easing能够对应上就可以了。

这其实就是策略模式。

结束动画调用

上面的tick函数只会执行一次,因为还没有用到setInterval或者requestAnimationFrame来重复调用tick函数。

只需要在tick函数最后面,调用requestAnimationFrame(tick)即可,不过要加一个结束条件,就是percentComplete !== 1

function tick () {
    let currentTime = new Date().getTime()
    const percentComplete = Math.min((currentTime - startTime) / opts.duration, 1)
    // 当前透明度的值,准备用来修改 DOM 对应属性
    let currentValue
    // 如果已经执行的动画时间大于动画应该执行的时间,就将值直接赋为结束值
    if (percentComplete === 1) {
        currentValue = endValue
    } else {
        currentValue = parseFloat(startValue) + (endValue - startValue) * easing['swing'](percentComplete)
    }
    console.log(currentValue)
    // 6、结束动画条件
    if (percentComplete !== 1) {
        requestAnimationFrame(tick)
    }
}

现在打开控制台,点击按钮执行动画,就能看到打印1 ~ 0.5逐渐变化的过程,表示成功。现在就差最后一步,将这个值赋给 DOM 对应属性。

setPropertyValue

这个就简单了,

function setPropertyValue (element, property, value) {
    element.style[property] = value
}

所以在tick函数内这样调用该函数:

function tick () {
    let currentTime = new Date().getTime()
    const percentComplete = Math.min((currentTime - startTime) / opts.duration, 1)
    // 当前透明度的值,准备用来修改 DOM 对应属性
    let currentValue
    // 如果已经执行的动画时间大于动画应该执行的时间,就将值直接赋为结束值
    if (percentComplete === 1) {
        currentValue = endValue
    } else {
        currentValue = parseFloat(startValue) + (endValue - startValue) * easing['swing'](percentComplete)
    }
    console.log(currentValue)
    // 4.2、改变 dom 的属性值
    setPropertyValue(element, property, currentValue + unitType)
    // 6、终止调用 tick
    if (percentComplete !== 1) {
        requestAnimationFrame(tick)
    }
}

再重新刷新页面,点击按钮,看看能否正确改变透明度?

总结

真正的velocity.js源码有 4000+ 行,即使是最初的版本也有 2000+,而我们自己实现的仅仅有 60+,所以可想而知有多简陋,不过千里之行,始于足下,能够开始,就是进步。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容

  • 什么时候适合用jQuery DOM操作较多、简单的AJAX、需要兼容多款浏览器 什么时候不用jQuery 页面交互...
    yuhuan121阅读 326评论 0 1
  • JQuery是什么? jQuery就是javascript的一个库,把我们常用的一些功能进行了封装,方便我们来调用...
    阿鲁提尔阅读 354评论 0 3
  • 隐藏元素的hide方法 让页面上的元素不可见,一般可以通过设置css的display为none属性。但是通过css...
    老夫撩发少年狂阅读 1,087评论 0 2
  • 1.JQuery 基础 改变web开发人员创造搞交互性界面的方式。设计者无需花费时间纠缠JS复杂的高级特性。 1....
    LaBaby_阅读 1,332评论 0 2
  • 1.JQuery 基础 改变web开发人员创造搞交互性界面的方式。设计者无需花费时间纠缠JS复杂的高级特性。 1....
    LaBaby_阅读 1,167评论 0 1