这是我今年为新人设计的一门课程的文字精简版,完整的PPT可参考:http://matrix.h5jun.com/slide/show?id=117
在浏览器里,动画实现的基本原理非常简单明了,其实就是采用定时器改变显示元素的一些属性的过程。不管是 JavaScript 操作 DOM 的动画,还是 CSS3 动画,还是 Canvas 动画,或者 SVG 动画,区别只是使用的 API、何种定时器,影响什么环境(DOM/Canvas/SVG/WebGL)。
基本动画
var deg = 0;
block.addEventListener("click", function(){
var self = this;
requestAnimationFrame(function change(){
self.style.transform = "rotate(" + (deg++) +"deg)";
requestAnimationFrame(change);
});
});
上面的例子里,我们使用了定时器 requestAnimationFrame,requestAnimationFrame 是浏览器专为渲染刷新设计的定时器接口,在早期版本的浏览器里,我们可以用 setTimeout 或者 setInterval 来代替它。定时器改变了方块元素的角度,每一次定时器触发我们就刷新并增加一次它的角度值,这样就产生了方块不断旋转的动态效果。
这就是我们需要的动画,几行原生JS代码就够了,是不是很简单呢?
事实上,上面的动画不是最佳的实现方法。它存在着几个明显的改进点。
首先,requestAnimationFrame(或者 setTimeout、setInterval 等其他定时器)并不能保证严格在某个时间点被触发。还记得 JavaScript 的单线程非阻塞模型吧?如果 requestAnimationFrame 被其他任务给阻塞了,那么动画就会变慢:
"变慢"的动画
var deg = 0;
block.addEventListener("click", function(){
setInterval(function(){
var i = 0;
var t = Date.now();
while(++i < 200000000); //模拟耗时操作
console.log(Date.now() - t);
}, 100);
var self = this;
requestAnimationFrame(function change(){
self.style.transform = "rotate(" + (deg++) +"deg)";
requestAnimationFrame(change);
});
});
上面的动画,因为有其他的定时器耗时的操作,导致动画变慢。
其次,一个更加麻烦的问题是,上面的动画我们通过定时器给旋转角度增量的方式,或者说得更泛一点(暂时忽略前面那个定时器触发时间不确定的问题),我们通过定义速度的方式来改变动画,这会导致我们很难精确控制动画时间和动画的幅度。像前面这种匀速运动其实还好,如果做一些复杂的变速运动,按照我们的定义方式,我们本该设置的元素属性值将会类似于求积分,然而时间又不连贯。
正弦曲线运动
var x = 0, y = 0;
block.addEventListener("click", function(){
var self = this;
requestAnimationFrame(function change(){
self.style.transform = "translate(" +
(x++) + "px," + 100 * Math.cos(Math.PI * (y++/180)) + "px)";
requestAnimationFrame(change);
});
});
上面的动画由于时间不连贯绘制出来的曲线只能近似等于正弦曲线。
动画,是位移关于时间的函数:\(s = f(t)\)
所以,我们不该采用增量的方式来执行动画,为了更精确地控制动画,更合适的方式是将动画与时间联系起来:
动画与时间关联
function startAnimation(){
var startTime = Date.now();
requestAnimationFrame(function change(){
var current = Date.now() - startTime;
console.log("动画已执行时间: %fms", current);
requestAnimationFrame(change);
});
}
动画通常情况下有终止时间,如果是循环动画,我们也可以看做特殊的----当动画达到终止时间之后,重新开始动画。因此,我们可以将动画时间归一(Normalize)表示:
动画时间归一化表示
function startAnimation(duration, isLoop){
var startTime = Date.now();
requestAnimationFrame(function change(){
var p = (Date.now() - startTime) / duration;
if(p >= 1.0){
if(isLoop){
startTime += duration;
p -= 1.0;
}else{
p = 1.0;
}
}
console.log("动画已执行进度: %f", p);
if(p < 1.0){
requestAnimationFrame(change);
}
});
}
我们可以用时间来控制动画:
用时间来控制动画周期精确在1秒
block.addEventListener("click", function(){
var self = this, startTime = Date.now(),
duration = 1000;
setInterval(function(){
var p = (Date.now() - startTime) / duration;
self.style.transform = "rotate(" + (360 * p) +"deg)";
}, 1000/60);
});
让滑块在2秒内向右匀速移动200px
block.addEventListener("click", function(){
var self = this, startTime = Date.now(),
distance = 200, duration = 2000;
requestAnimationFrame(function step(){
var p = Math.min(1.0, (Date.now() - startTime) / duration);
self.style.transform = "translateX(" + (distance * p) +"px)";
if(p < 1.0) requestAnimationFrame(step);
});
});
我们可以将通过时间控制动画与前面的简单增量的办法做一个对比:
时间 V.S. 增量
时间增量
幅度控制 √ √
时间控制 √ X
幅度控制 √ √
不延迟 √ X
不掉帧 X √
变速运动可以模拟一些物理效果、曲线运动,以及其他的一些非均匀变化的特效。
加速度恒定,速度从0开始随时间增加而均匀增加。
\(t = T \cdot p\)
\(s_t = S \cdot p ^ {2} = (\frac{S}{T^2}) t^2 \)
\(v = \frac{2S}{T^2} \cdot t = \frac{2Sp}{T}\)
\(a = \frac{2S}{T^2} \)
通过推导可以得到匀减速运动的位移时间公式:\(s_t = Sp^2\)
滑块在2秒内向右匀加速移动200px,速度从0开始
block.addEventListener("click", function(){
var self = this, startTime = Date.now(),
distance = 200, duration = 2000;
requestAnimationFrame(function step(){
var p = Math.min(1.0, (Date.now() - startTime) / duration);
self.style.transform = "translateX(" + (distance * p * p) +"px)";
if(p < 1.0) requestAnimationFrame(step);
});
});
匀速、匀加速运动对比
实现"刹车"效果,速度随时间均匀减小直到0,让物体停止运动。
\(t = T \cdot p\)
\(s_t = \frac{2S}{T} \cdot t - (\frac{S}{T^2}) t^2 = Sp(2-p)\)
\(v = \frac{2S(1-p)}{T} = \frac{2S}{T} - \frac{2S}{T^2} \cdot t\)
\(a = - \frac{2S}{T^2} \)
通过推导可以得到匀减速运动的位移时间公式:\(s_t = Sp(2-p)\)
让滑块在2秒内向右匀减速移动200px,速度从最大减为0
block.addEventListener("click", function(){
var self = this, startTime = Date.now(),
distance = 200, duration = 2000;
requestAnimationFrame(function step(){
var p = Math.min(1.0, (Date.now() - startTime) / duration);
self.style.transform = "translateX("
+ (distance * p * (2-p)) +"px)";
if(p < 1.0) requestAnimationFrame(step);
});
});
让x、y轴同时分别运动,可以让物体沿平面轨迹运动。
抛物线运动
block.addEventListener("click", function(){
var self = this, startTime = Date.now(),
disX = 200, disY = 200,
duration = 1000 * Math.sqrt(2 * disY / 98);
//假设10px是1米,disY = 20米
requestAnimationFrame(function step(){
var p = Math.min(1.0, (Date.now() - startTime) / duration);
var tx = disX * p;
var ty = disY * p * p;
self.style.transform = "translate("
+ tx + "px" + "," + ty +"px)";
if(p < 1.0) requestAnimationFrame(step);
});
});
抛物线运动 x 轴做匀速直线运动,y 轴做匀加速直线运动
正弦线运动
block.addEventListener("click", function(){
var self = this, startTime = Date.now(),
distance = 100,
duration = 2000;
requestAnimationFrame(function step(){
var p = Math.min(1.0, (Date.now() - startTime) / duration);
var ty = distance * Math.sin(2 * Math.PI * p);
var tx = 2 * distance * p;
self.style.transform = "translate("
+ tx + "px," + ty + "px)";
if(p < 1.0) requestAnimationFrame(step);
});
});
正弦线运动 x 轴做匀速直线运动,y 轴的运动是时间 t 的正弦函数。
圆周运动
圆的代数方程涉及到开根号后的正负号问题,因此一般不使用
圆周运动 - 参数方程
block.addEventListener("click", function(){
var self = this, startTime = Date.now(),
r = 100, duration = 2000;
requestAnimationFrame(function step(){
var p = Math.min(1.0, (Date.now() - startTime) / duration);
var tx = r * Math.sin(2 * Math.PI * p),
ty = -r * Math.cos(2 * Math.PI * p);
self.style.transform = "translate("
+ tx + "px," + ty + "px)";
if(p < 1.0) requestAnimationFrame(step);
});
});
根据参数方程,圆周运动 x 轴是时间 t 的余弦函数, y 轴是时间 t 的正弦函数。
圆周运动 - 极坐标方程
block.addEventListener("click", function(){
var self = this, startTime = Date.now(),
r = 100, duration = 2000;
requestAnimationFrame(function step(){
var p = Math.min(1.0, (Date.now() - startTime) / duration);
var rotation = -360 * p;
self.style.transformOrigin = "0 " + r + "px";
self.style.transform = "rotate("
+ rotation + "deg)";
if(p < 1.0) requestAnimationFrame(step);
});
});
根据极坐标方程,圆周运动的旋转角度是时间 t 的线性函数。
我们总结一下上面的各类动画,发现它们是非常相似的,匀速运动、匀加速运动、匀减速运动、圆周运动唯一的区别仅仅在于位移方程:
我们把共同的部分 S 去掉,得到一个关于 p 的方程 \(e_p = E(p)\),这个方程我们称为动画的算子(easing),它决定了动画的性质。
为了实现更加复杂的动画,我们可以将动画进行简易的封装,要进行封装,我们先要抽象出动画相关的要素:
动画的简易封装
function Animator(duration, progress, easing){
this.duration = duration;
this.progress = progress;
this.easing = easing || function(p){return p};
}
Animator.prototype = {
start: function(finished){
var startTime = Date.now();
var duration = this.duration,
self = this;
requestAnimationFrame(function step(){
var p = (Date.now() - startTime) / duration;
var next = true;
if(p < 1.0){
self.progress(self.easing(p), p);
}else{
if(typeof finished === "function"){
next = finished() === false;
}else{
next = finished === false;
}
if(!next){
self.progress(self.easing(1.0), 1.0);
}else{
startTime += duration;
self.progress(self.easing(p), p);
}
}
if(next) requestAnimationFrame(step);
});
}
};
在上面的代码里,我们封装出一个简易的动画类 Animator, 这个类的构造器接收三个参数,分别是 duration
, process
和 easing
。它产生一个对象,包含一个start
方法,这个方法用指定 duration
、process
和 easing
执行动画。
有趣的是,start
方法包含一个参数,这个参数是一个布尔类型或者回调函数,当动画结束的时候,如果这个参数是回调函数,将执行这个函数,它的返回值如果不是 false
那么结束动画,否则循环播放动画。如果这个参数是布尔值 flase,那么也循环播放动画。
后续的例子里我们会看到这个类的用法。
我们尝试使用上面设计的动画类来构造连续播放的动画:
让滑块先向右然后再向下运动
var a1 = new Animator(1000, function(p){
var tx = 100 * p;
block.style.transform = "translateX("
+ tx + "px)";
});
var a2 = new Animator(1000, function(p){
var ty = 100 * p;
block.style.transform = "translate(100px,"
+ ty + "px)";
});
block.addEventListener("click", function(){
a1.start(function(){
a2.start();
});
});
在构造更复杂的动画的时候,为了更方便使用,避免回调嵌套,我们可以再实现一个动画队列类:
function AnimationQueue(animators){
this.animators = animators || [];
}
AnimationQueue.prototype = {
append: function(){
var args = [].slice.call(arguments);
this.animators.push.apply(this.animators, args);
},
flush: function(){
if(this.animators.length){
var self = this;
function play(){
var animator = self.animators.shift();
if(animator instanceof Animator){
animator.start(function(){
if(self.animators.length){
play();
}
});
}else{
animator.apply(self);
if(self.animators.length){
play();
}
}
}
play();
}
}
};
有了动画队列,我们就可以轻松做更复杂一点的动画,比如:
让滑块沿一个矩形边界运动
var a1 = new Animator(1000, function(p){
var tx = 100 * p;
block.style.transform = "translateX("
+ tx + "px)";
});
var a2 = new Animator(1000, function(p){
var ty = 100 * p;
block.style.transform = "translate(100px,"
+ ty + "px)";
});
var a3 = new Animator(1000, function(p){
var tx = 100 * (1-p);
block.style.transform = "translate("
+ tx + "px, 100px)";
});
var a4 = new Animator(1000, function(p){
var ty = 100 * (1-p);
block.style.transform = "translateY("
+ ty + "px)";
});
block.addEventListener("click", function(){
var animators = new AnimationQueue();
animators.append(a1, a2, a3, a4);
animators.flush();
});
注意到我们的动画队列除了支持Animator对象外,还支持普通的函数,因此我们可以组合起来做一些复杂的运动:
弹跳的小球
var a1 = new Animator(1414, function(p){
var ty = 200 * p * p;
block.style.transform = "translateY("
+ ty + "px)";
});
var a2 = new Animator(1414, function(p){
var ty = 200 - 200 * p * (2-p);
block.style.transform = "translateY("
+ ty + "px)";
});
block.addEventListener("click", function(){
var animators = new AnimationQueue();
animators.append(a1,a2, function(){
this.append(a1, a2, arguments.callee);
});
animators.flush();
});
还可以再加入更复杂的效果:
弹跳的小球 - 带阻尼效果
block.addEventListener("click", function(){
var T = 1414;
var a1 = new Animator(T, function(p){
var s = this.duration * 200 / T;
var ty = s * (p * p - 1);
block.style.transform = "translateY("
+ ty + "px)";
});
var a2 = new Animator(T, function(p){
var s = this.duration * 200 / T;
var ty = - s * p * (2-p);
block.style.transform = "translateY("
+ ty + "px)";
});
var animators = new AnimationQueue();
function foo(){
a2.duration *= 0.7;
if(a2.duration <= 0.0001){
console.log("done");
animators.animators.length = 0;
}
}
animators.append(a1 ,foo, a2,
function b(){
a1.duration *= 0.7;
this.append(a1, foo, a2, b);
});
animators.flush();
});
有时候我们也需要一些高级的数学技巧:
模拟从圆周甩出小球
var a1 = new Animator(2800, function(p){
var x = -100 * Math.sin(2.8 * Math.PI * p);
var y = 100 - 100 * Math.cos(2.8 * Math.PI * p);
block.style.transform = "translate(" + x + "px,"
+ y + "px)";
});
var a2 = new Animator(5000, function(p){
var x = -100 * Math.sin(2.8 * Math.PI)
-100 * Math.cos(2.8 * Math.PI) * Math.PI * 5 * p;
var y = 100 - 100 * Math.cos(2.8 * Math.PI)
+ 100 * Math.sin(2.8 * Math.PI) * Math.PI * 5 * p;
block.style.transform = "translate(" + x + "px,"
+ y + "px)";
});
block.addEventListener("click", function(){
a1.start(function(){
a2.start();
});
});
小球被甩出的一刻,x、y 轴速度不再变化,小球被甩出前正在做匀速圆周运动,可以求出 St,然后再对 St 求导求出 Vt
贝塞尔曲线可以用来构造平滑动画。
我们可以引入 bezier-easing 库了来支持贝塞尔曲线的JS动画:
贝塞尔动画 - easeInOutQuint
var easing = BezierEasing(0.86, 0, 0.07, 1);
//easeInOutQuint
var a1 = new Animator(2000, function(ep,p){
var x = 200 * ep;
block.style.transform = "translateX(" + x + "px)";
}, easing);
block.addEventListener("click", function(){
a1.start();
});
我们可以通过 cubic-bezier.com 和 easings.net 来定制我们想要的动画效果。
有时候,我们不但要支持元素的运动,还需要改变元素的外观,比如飞翔的小鸟需要扇动翅膀,这类动画我们可以用逐帧动画来实现:
小鸟扇翅膀逐帧动画
<style type="text/css">
.sprite {display:inline-block; overflow:hidden; background-repeat: no-repeat;background-image:url(http://res.h5jun.com/matrix/8PQEganHkhynPxk-CUyDcJEk.png);}
.bird0 {width:86px; height:60px; background-position: -178px -2px}
.bird1 {width:86px; height:60px; background-position: -90px -2px}
.bird2 {width:86px; height:60px; background-position: -2px -2px}
#bird{
position: absolute;
left: 100px;
top: 100px;
zoom: 0.5;
}
</style>
<div id="bird" class="sprite bird1"></div>
var i = 0;
setInterval(function(){
bird.className = "sprite " + "bird" + ((i++) % 3);
}, 1000/10);
看上面的代码,其实逐帧动画比之前的动画还要简单,直接用 setInterval 修改元素样式即可,需要注意的是,如果用图片的话,最好是将图片提前预加载了,这样不会出现因为图片还在加载中而显示不出动画的情况。
CSS3 支持两种动画,一种是 Transition,一种是 Animation。
Transition 是过渡动画,它只定义在样式的 class 切换的时候发生的动画,因此 Transition 动画相对比较简单,没有循环,也没有事件,它触发的时机只在元素的 className 发生变化的时候。
CSS3 动画支持的浏览器包括:
Transition 和 Animation 共同支持的属性:
Transition 和 Animation 支持同样的 Timing functions:
这其实和我们前面的JS动画里的算子概念是一致的,贝塞尔曲线也是一致的:
Transition 圆周运动
<style>
#block{
position:absolute;
left: 200px;
top: 100px;
width: 20px;
height: 20px;
background: #0c8;
text-align: center;
border-radius: 50%;
transform-origin: 0 100px;
transform: rotate(0deg);
}
#block.play {
transform: rotate(360deg);
transition: transform 2.0s linear;
}
</style>
<div id="block"></div>
block.addEventListener("click", function(){
block.className = "play";
});
Transition 使用贝塞尔曲线
#block.play {
transform: translateX(200px);
transition: transform 2.0s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
Transition 没有优先级,后面的样式会覆盖掉前面的样式中的某些 Transition 属性,因此当两个 class 都有 Transition 的时候,相互覆盖会导致奇怪的行为:
Transition 样式覆盖
#block.play {
border-radius: 0;
transform: scale(2.0);
background: #c80;
transition: all 2.0s cubic-bezier(0.68, -0.55, 0.265, 1.55) 3s;
}
#block.play2 {
/* transition 覆盖*/
background: #c8f;
transition: all 2.0s linear 0.5s;
transform: scale(2.0) rotate(360deg);
}
block.addEventListener("click", function(){
block.className = "play play2";
});
Animation 动画支持一些更高级的特性:
Animation - 往复圆周运动
#block{
position:absolute;
left: 200px;
top: 100px;
width: 20px;
height: 20px;
background: #0c8;
text-align: center;
border-radius: 50%;
animation: roll 2.0s linear 0s infinite alternate;
transform-origin: 0 100px;
}
@keyframes roll{
0%{transform:rotate(0deg)}
100%{transform:rotate(360deg)}
}
复杂的动画效果可以将 JS 和 CSS3 动画组合使用:
动画组合
#block{
position:absolute;
left: 150px;
top: 200px;
width: 20px;
height: 20px;
background: #0c8;
text-align: center;
border-radius: 50%;
animation: anim 2.0s linear 0s forwards;
}
@keyframes anim{
0%{border-radius: 50%}
50%{border-radius: 0; background: #c80;}
100%{border-radius: 20%; transform:scale(2.0); background: #08c;}
}
var easing = BezierEasing(0.68, -0.55, 0.265, 1.55);
var a1 = new Animator(2000, function(ep,p){
var x = 150 + 200 * ep;
block.style.left = x + "px";
}, easing);
block.addEventListener("webkitAnimationEnd", function(){
a1.start();
});
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。