匀速运动任何人都非常明白是怎么回事,而缓动则比较少提到,其实说的简单一点,匀速运动通常指的是“匀速直线运动”,缓动指“变速直线运动”。速度,包含大小和方向两个维度,大小变化或方向变化都叫变速。作为初中课本里面两种常见的现象,在我们前端如何去实现它,却成了一件复杂的事。
问题的分解
运动的本质
运动的本质就是形态或者位置的改变,在我们的页面上,一个对象位置发生变化,或者形状、颜色发生变化,都是运动。但是我们现在要研究的,主要是指位置发生变化。而如果是对象大小变化引起的位置变化,我们需要把对象进行拆解,去看它的某个细节部位,实际上就是这个部位的位置变化。因此,我们在研究这类问题时,其实是在研究页面上某一个元素的位置发生变化的规律。
位置的变化包含两个方面,一是位移,一是路径。因此,我们在构建页面上元素的位置变化时,要从这两个方面去构建。而运动,意味着时间,因此,我们通常的做法是给运动过程规定一个特定的时间周期,比如要求整个运动过程在0.5s内完成,当然,有的时候我们允许它重复该周期,甚至间隔周期反向运动,形成周而复始的运动过程。
所以,最终,我们的问题被分解为:位移、路径、周期时间。
起始位置和结束位置
位移只关注你的开始位置和结束位置。这是匀速运动的时候,而如果当我们处理到缓动时,往往就需要在增加一些位置节点,例如当运动一开始后加速运动,而达到整个路程的60%位置时,开始减速运动,直至终点。当然,无论加速还是减速,整个路程的运动时间被我们确定为0.5s,不会多也不会少。
但是在不同的技术里面,位移的表达方式并不一样,比如在css里面,我们有的时候通过margin-left来确定某个对象的位置,通过改变margin-left的值来实现运动。当然,我们更加推荐使用position的left、top值,因为这更好理解,计算时也可以套用数学模型。
变速运动的路径
我们前面说到,缓动是一个变速直线运动,直线就是运动的路径。从空间的角度讲这是没有错的,但是从时间的角度讲,则不是这样。我们将它用曲线表达出来:
如果我们仅看空间上的运动轨迹,那么就是第一个图,而如果加上时间维度,就会看到第二个图和第三个图,它们的轨迹是不同的。这好像有点抽象,但是我提一个问题:在javascript里面,如何实现第三幅图这种缓动呢?很好玩儿,jQuery给我们提供了animate方法来实现,但实际上,它也依赖于css,也就是说你必须把要改变的css属性值作为参数传入。如何通过程序的方式实现这一特性呢?还需要我们另外再做思考。
而如何来确定这个路径呢?起始我们只需要构建数学模型即可。例如第一个图,我们只需要确定在单位时间里,x和y各自匀速增加的大小,当它们在周期时间内完成运动之后,得到的路径自然就是图中所显示的。而加上时间维度的时候,无非增加一个时间变量。只不过在一般的编程中,我们很难实现类似如下的编码:
{x1,y1,t1},{x2,y2,t2}, {x3,y3,t3}
但是我们要实现下面的编码,就简单多了:
{ t1: {x1,y1}, t2: {x2,y2}, t3: {x3,y3} }
所以,我们在编码时,往往会先考量时间维度,在时间维度上选取特定的刻度后,再在该刻度上进行空间规定。这也是css @keyframes的定义方法。因此,我们在自己写方案的时候,最好也这样去考虑。
匀加速运动
在上面定义的这种编码形式中,即使我们找到了用时间节点作为刻度进行速度的调整,但是仍然会有一个问题,如果从t1到t2的过程中,速度是匀速的,上面的第三个图就不会是曲线,而是折线。对于界面上的运动效果而已,也会变得很奇怪,就像在运动过程中,突然进入另外一个空间一样,速度突然变化。想要平滑过渡,我们还需要从加速度的角度考虑,使速度在变化过程中,可以很好的契合,实现平滑过渡。
从编码的角度讲,我们不得不重新计算从t1到t2这个过程中,速度到变化规律。比如类似得到:v = at 这样一个公式,找到一个常量a来确定每一个时刻所对应到速度。
匀速直线运动的实现
假定我们现在要实现这样一个场景,当用户在看视频的时候,弹幕从屏幕上匀速飘过。
css实现方法
在css3中有一个基本的transition属性实现动画,当然,它是一个结合体,就像font, background一样,它有一个transition-timing-function属性,当它当值为line的时候,表示匀速(直线)运动,当然,我们在看到这里的匀速运动时,应该把时间维度也考虑进来,也就是说在空间和时间上,动画效果都是line(直线),也就是我们上面看到的第二个图。例如:
img { width:45px; transition-property: width; transition-duration: 1s; transition-timing-function: line; } img:hover { width: 90px; }
这样可以实现图片在被鼠标访问时,宽度放大一倍。我们可以把要改变的元素的position值重新进行设定,从而改变整体的位置。所以,在弹幕这个事情上,就是这样:
div.words { position: absolute; top: 100px; left: 120%; transition: left 2s line; } div.words.fly { left: -1000%; }
这需要配合javascript来实现。当一条新的弹幕出现时,javascript给它加上.fly这个class,于是它就开始往左边飞。但是这还没有结束,你还要根据这一弹的字数,计算它的宽度,通过一个随机种子,让它的top值改变,以及结尾left值得到一个适当的改变,甚至改变它飞翔的速度,当然,这些都是比较简洁的。
但是transition仅能在被触发时执行,无法自己执行。所谓触发,就是从div.words变为div.words.fly,或者反过来,.fly被去掉,它无法自执行。这个时候,需要用到另外一个属性animate,而animate大部分情况下,需要配合@keyframes来实现。
同样是上面这个效果,我们用animate来实现:
@keyframes fly{ 100% { left: -1000%; } } div.words { position: absolute; top: 100px; left: 120%; animate: 2s fly; }
当一个div.words出现之后,会自己执行fly这个关键帧动作。当然,animate还有更多漂亮的用法,需要你自己去了解。而且,用javascript无法修改fly的100%的属性信息,所以也有缺点。
jQuery实现方法
和css不同,css是靠浏览器渲染引擎来实现动画效果,而jQuery则通过一点点的改变css样式,来改变当前的状态。jquery的animate方法有一个easing参数,该参数定义了运动路径效果,包括linear和swing等。
$('div.words').animate({left:-1000%}, 2000, 'linear');
javascript的实现方法
javascript目前能够处理这类问题的主要思路,就是做一个定时器,按每0.1s或更多移动一个像素从而实现运动效果。例如:
var words = document.getElementById('words'); var interval = 2000; var timer = setInterval(function() { var left = words.style.left; left = left - 1% * interval / 100; // 这里其实计算有问题,仅作演示 words.style.left = left; interval -= 100; if(interval == 0) clearInterval(timer); },100);
但是我们知道弹幕的数量有的时候会瞬间爆发,这种情况下,如何解决卡机问题是一个非常重要的问题。卡机就是内存不够用,性能受到局限,在这种情况下,假设浏览器允许同一时间interval的执行动作只有1000个,而你的interval竟然达到了10000个,那么就会有9000个在排队,虽然你规定interval时间结束后就要执行setInterval的回调函数,但是由于前面已经卡机了,所以这个动作得等会儿再执行,而效果是什么呢?就是弹幕突然停在原地不动了,过了可能1s,它有继续往前跑,甚至一下子冲出去老远,出现了卡顿现象。
解决这种情况,就要涉及到javascript的性能优化问题。我的思路是,把100个合并为一个去实现匀速,我们把所有的弹幕文字进行分组,每100个一组,屏幕上出现,然后将它们全部放在同一个容器里面,通过改变容器来改变弹幕文字的位置,这样上面那10000个interval瞬间就变成了100个,那也就在我们的极限1000范围内了。具体怎么操作呢?
<style> div.container { position: relative; margin-left: -100%; width: 200%; } div.words { position: absolute; } </style> <script> var container = document.createElement('div'); container.className = 'container'; var words = document.getElementsByClassName('words'); var lenth = words.length,style; for(var i = length - 1;i --;) { words[i].style.cssText += '; left: ' + rand('left') + '%; top: ' + rand('top') + '%;'; container.insertBefore(words[i]); } var interval = 2000; var timer = setInterval(function() { container.style.width = (interval / 10.00) + '%'; interval -= 1; if(interval == 0) clearInterval(timer); },1); function rand(type) {} // 用于计算一个随机值的函数,略</script>
当然,这个写法只是一个思路,并不可以直接用于项目中。你可以看到,在整个interval中,一直在改变container的宽度,而对弹幕文字没有任何操作,而通过百分百定位,可以让container宽度减小过程中,改变弹幕文字的位置,直至最后消失在屏幕中。由于是实用百分比定位,这也导致实际过程中,移动的速度和位置都不同。
当然,这真的只是一种思路,而且里面还要考虑很多因素。
2016-04-28 5763