CSS动画与交互:为什么动画要用贝塞尔曲线这么奇怪的东西?
下载APP
关闭
渠道合作
推荐作者
CSS动画与交互:为什么动画要用贝塞尔曲线这么奇怪的东西?
2019-04-20 winter 来自北京
《重学前端》
课程介绍
讲述:winter
时长07:34大小6.92M
你好,我是 winter,今天我们来学习一下 CSS 的动画和交互。
在 CSS 属性中,有这么一类属性,它负责的不是静态的展现,而是根据用户行为产生交互。这就是今天我们要讲的属性。
首先我们先从属性来讲起。CSS 中跟动画相关的属性有两个:animation 和 transition。
animation 属性和 transition 属性
我们先来看下 animation 的示例,通过示例来了解一下 animation 属性的基本用法:
这里展示了 animation 的基本用法,实际上 animation 分成六个部分:
animation-name 动画的名称,这是一个 keyframes 类型的值(我们在第 9 讲“CSS 语法:除了属性和选择器,你还需要知道这些带 @的规则”讲到过,keyframes 产生一种数据,用于定义动画关键帧);
animation-duration 动画的时长;
animation-timing-function 动画的时间曲线;
animation-delay 动画开始前的延迟;
animation-iteration-count 动画的播放次数;
animation-direction 动画的方向。
我们先来看 animation-name,这个是一个 keyframes 类型,需要配合 @规则来使用。
比如,我们前面的示例中,就必须配合定义 mymove 这个 keyframes。keyframes 的主体结构是一个名称和花括号中的定义,它按照百分比来规定数值,例如:
这里我们可以规定在开始时把 top 值设为 0,在 50% 是设为 30px,在 75% 时设为 10px,到 100% 时重新设为 0,这样,动画执行时就会按照我们指定的关键帧来变换数值。
这里,0% 和 100% 可以写成 from 和 to,不过一般不会混用,画风会变得很奇怪,比如:
这里关键帧之间,是使用 animation-timing-function 作为时间曲线的,稍后我会详细介绍时间曲线。
接下来我们来介绍一下 transition。transition 与 animation 相比来说,是简单得多的一个属性。
它有四个部分:
transition-property 要变换的属性;
transition-duration 变换的时长;
transition-timing-function 时间曲线;
transition-delay 延迟。
这里的四个部分,可以重复多次,指定多个属性的变换规则。
实际上,有时候我们会把 transition 和 animation 组合,抛弃 animation 的 timing-function,以编排不同段用不同的曲线。
在这个例子中,在 keyframes 中定义了 transition 属性,以达到各段曲线都不同的效果。
接下来,我们就来详细讲讲刚才提到的 timing-function,动画的时间曲线。
三次贝塞尔曲线
我想,你能从很多 CSS 的资料中都找到了贝塞尔曲线,但是为什么 CSS 的时间曲线要选用(三次)贝塞尔曲线呢?
我们在这里首先要了解一下贝塞尔曲线,贝塞尔曲线是一种插值曲线,它描述了两个点之间差值来形成连续的曲线形状的规则。
一个量(可以是任何矢量或者标量)从一个值到变化到另一个值,如果我们希望它按照一定时间平滑地过渡,就必须要对它进行插值。
最基本的情况,我们认为这个变化是按照时间均匀进行的,这个时候,我们称其为线性插值。而实际上,线性插值不大能满足我们的需要,因此数学上出现了很多其它的插值算法,其中贝塞尔插值法是非常典型的一种。它根据一些变换中的控制点来决定值与时间的关系。
贝塞尔曲线是一种被工业生产验证了很多年的曲线,它最大的特点就是“平滑”。时间曲线平滑,意味着较少突兀的变化,这是一般动画设计所追求的。
贝塞尔曲线用于建筑设计和工业设计都有很多年历史了,它最初的应用是汽车工业用贝塞尔曲线来设计车型。
K 次贝塞尔插值算法需要 k+1 个控制点,最简单的一次贝塞尔插值就是线性插值,将时间表示为 0 到 1 的区间,一次贝塞尔插值公式是:
“二次贝塞尔插值”有 3 个控制点,相当于对 P0 和 P1,P1 和 P2 分别做贝塞尔插值,再对结果做一次贝塞尔插值计算
“三次贝塞尔插值”则是“两次‘二次贝塞尔插值’的结果,再做一次贝塞尔插值”:
贝塞尔曲线的定义中带有一个参数 t,但是这个 t 并非真正的时间,实际上贝塞尔曲线的一个点 (x, y),这里的 x 轴才代表时间。
这就造成了一个问题,如果我们使用贝塞尔曲线的直接定义,是没办法直接根据时间来计算出数值的,因此,浏览器中一般都采用了数值算法,其中公认做有效的是牛顿积分,我们可以看下 JavaScript 版本的代码:
这段代码其实完全翻译自 WebKit 的 C++ 代码,牛顿积分的具体原理请参考相关数学著作,注释中也有相关的链接。
这个 JavaScript 版本的三次贝塞尔曲线可以用于实现跟 CSS 一模一样的动画。
贝塞尔曲线拟合
理论上,贝塞尔曲线可以通过分段的方式拟合任意曲线,但是有一些特殊的曲线,是可以用贝塞尔曲线完美拟合的,比如抛物线。
这里我做了一个示例,用于模拟抛物线:
这段代码中,我实现了抛物线运动的小球,其中核心代码就是 generateCubicBezier 函数。
这个公式完全来自于一篇论文,推理过程我也不清楚,但是不论如何,它确实能够用于模拟抛物线。
实际上,我们日常工作中,如果需要用贝塞尔曲线拟合任何曲线,都可以找到相应的论文,我们只要取它的结论即可。
总结
我们今天的课程,重点介绍了动画和它背后的一些机制。
CSS 用 transition 和 animation 两个属性来实现动画,这两个属性的基本用法很简单,我们今天还介绍了它们背后的原理:贝塞尔曲线。
我们中介绍了贝塞尔曲线的实现原理和贝塞尔曲线的拟合技巧。
最后,留给你一个小问题,请纯粹用 JavaScript 来实现一个 transition 函数,用它来跟 CSS 的 transition 来做一下对比,看看有哪些区别。
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 11
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
CSS Flex排版:为什么垂直居中这么难?
下一篇
HTML语言:DTD到底是什么?
精选留言(7)
- 阿成2019-04-20const tweenFns = { linear: (from, to, t, d) => from + (to - from) * (t / d) } /** * only support "linear" timing-function * duration unit is "ms" * @param {HTMLElement} el * @param {({prop: String, value: String, duration: Number})[]} list */ function transitionTo(el, list) { let startTime let oldStyle = new Map() let newStyle = new Map() for (let prop of list) { oldStyle.set(prop.name, window.getComputedStyle(el)[prop.name]) } for (let prop of list) { el.style[prop.name] = prop.value } for (let prop of list) { newStyle.set(prop.name, window.getComputedStyle(el)[prop.name]) } for (let prop of list) { el.style[prop.name] = oldStyle.get(prop.name) } requestAnimationFrame(run) function run(time) { if (startTime == null) startTime = time let t = time - startTime let done = true for (let prop of list) { if (t >= prop.duration) { el.style[prop.name] = newStyle.get(prop.name) continue } done = false let oldPropValue = oldStyle.get(prop.name) let newPropValue = newStyle.get(prop.name) if (prop.name === 'transform') { if (oldPropValue === 'none') oldPropValue = 'matrix(1, 0, 0, 1, 0, 0)' if (newPropValue === 'none') newPropValue = 'matrix(1, 0, 0, 1, 0, 0)' } el.style[prop.name] = generateNewStyle(oldPropValue, newPropValue, t, prop.duration, tweenFns.linear) } if (!done) requestAnimationFrame(run) } } function generateNewStyle(from, to, t, duration, tweenFn) { let fromExp = /[\d.-]+/g let toExp = /[\d.-]+/g let fromMatch let toMatch let result = '' let lastIndex = 0 while (fromMatch = fromExp.exec(from)) { result += from.slice(lastIndex, fromMatch.index) toMatch = toExp.exec(to) result += tweenFn(+fromMatch[0], +toMatch[0], t, duration) lastIndex = fromExp.lastIndex } result += from.slice(lastIndex) return result }展开共 1 条评论26
- 阿成2019-04-20跟CSS的transition比,JS更加偏向指令式,而CSS更加偏向声明式,当然,这本身也是两门语言自身的特点,CSS用法简单直观,JS则在控制方面有更大的灵活性。 上面我只实现了 linear timing function(其他的函数实现网上大把大把的...),具体用法如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style> #ball { width: 100px; height: 100px; background: blue; } </style> </head> <body> <div id="ball"></div> <script src="transition.js"></script> <script> transitionTo(document.getElementById('ball'), [ {name: 'transform', duration: 1000, value: 'translate(400px, 200px) rotate(40deg)'}, {name: 'backgroundColor', duration: 1000, value: 'red'}, {name: 'width', duration: 1000, value: '200px'}, {name: 'height', duration: 1000, value: '200px'} ]) </script> </body> </html>展开共 3 条评论21
- 许童童2019-04-20这个课后练习有点难啊。希望老师可以带着大家过一遍。7
- Marvin2020-08-03// 利用老师提供的贝塞尔曲线函数 function timing_function(easing) { let resolve; if (easing === 'linear') resolve = generate(0, 0, 1, 1); else if (easing === 'ease') resolve = generate(0.25, 0.1, 0.25, 1); else if (easing === 'ease-in') resolve = generate(0.42, 0, 1, 1); else if (easing === 'ease-out') resolve = generate(0, 0, 0.58, 1); else if (easing === 'ease-in-out') resolve = generate(0.42, 0, 0.58, 1); else if (easing.indexOf('cubic-bezier') === 0) { let arr = easing.match(/(?<=\()(.*)(?=\))/)[0].split(","); arr.map(item => { return Number(item); }) resolve = generate(...arr); } else { resolve = generate(0, 0, 1, 1); } return resolve; } function transition(el, target_value, transition_property, transition_duration, transition_timing_function, transition_delay) { let start = 0; let bezier = timing_function(transition_timing_function); let scale = 1 / transition_duration; let targetArr = target_value.match(/(\d*)(.*)/); console.log(targetArr); function step(timestamp) { if (!start) start = timestamp; let progress = timestamp - start; let y = bezier(scale * progress); // y轴的比例 el.style[transition_property] = (Number(targetArr[1]) * y) + targetArr[2]; if (progress <= transition_duration)requestAnimationFrame(step); } setTimeout(() => { requestAnimationFrame(step) }, transition_delay); } let ball = document.getElementsByClassName("ball")[0]; transition(ball, "50px", "font-size", 2000, "liner", 1000);展开2
- 剑客不能说2019-09-27一脸懵逼状态看完的~共 1 条评论2
- Marvin2020-08-03不是非常严谨的实现,但是差不多了。可以设置各种属性和时间曲线。 只支持数值+单位的形式例如:left: 200px 或者 font-size: 20px; https://github.com/OleileiA/TransitionJs/blob/master/transition.html
- zlxag2020-06-09交互没有?