vue的动画

407 阅读12分钟
原文链接: mp.weixin.qq.com

有时候写写东西有种呕心沥血的错觉,觉得自己似乎做了什么大事。

这是一篇反反复复谢了很多遍开头的文章,原始是最近事情太多,文章基本上只能起个头,之后就没有之后了。

这里只是做了一个简单的分享。

讲述一下如何用VUE做个简单的交互动画。

我想大家很清楚VUE,REACT以及ANGULAR,以数据绑定和组件化而闻名于世,作为新时代的开发者,当然不能漏掉对于这些框架的了解了。

欣仔比较喜欢做一些交互类的页面动画,以尽量提高页面的用户体验。当年就是因为处于这方面的爱好才做的flash的开发。

目前的主流框架提供的特性似乎跟动画没有太大的关联,毕竟主体功能性主次,交互性其次,所以基本上项目上线也都会先保证功能流程测试OK后最后再做动画方面的调整。

我可以给页面动画定义为:

  1. 固长动画:

  2. 交互动画:

    固长动画

顾名思义,当然就是固定时长,固定路径的动画,以及由固长动画动画组成的动画序列集, 固长动画也可以是循环的。一个固长动画单元可以描述为从时间点A到时间点B之间元素的移动状态,比如元素CELL在一秒内从left为0px移动到left为100px。

这在CSS3里面不难实现,唯一的问题是写起来非常麻烦。

首先先写一遍如下:

  1. @keyframes MOVE{

  2. from:{transform:translate(0px ,0px)}

  3. to:{transform:translate( 100,0px )}

  4. }

  5. ## X5

  6. .CELL{

  7. animtaion:MOVE 1s;

  8. ## X5

  9. }

然后再用-webkit-,-ms-,-moz-,-o-再跟上写一遍,顺利完成

“PENTA KILL”

欣仔在第一次面对这样的使用场景的时候是很惆怅的。所以当时也第一时间也封装了一些简单方法以方便调用赋值。

当时webpack等构建工具还不是很流行,所以很抵触这样写动画,而且出于前几年的经验,这样写动画已经失去了快感。。而且一点都不酷不是么。。。

后来由GreenSock从flash 平台移植了一个动画控制类工具:“GSAP”

大家可以去他们的官网查看:

https://greensock.com/

具体的使用用法也很简单,比如:

  1. import {TweenLite} from 'gsap' ;

  2. ......

    TweenMax .to('.cell',. 5,{left:'100px' })

很酷。。因为你还可以这样写:

  1. import {TweenLite} from 'gsap' ;

  2. ......

  3. TweenMax .to('.cell',. 5,{left:'100px' ,

  4. top: '200px',

  5. opacity:. 5,

  6. scale: 3})

这意味着该元素可以在0.5秒内对定义的各个属性作出相应的变化,的确很酷,是个很巧妙的封装。

 交互动画:

区别于固长动画,交互动画没有固定的时长,也没有固定的路径,你很难用css去做一个有效的模拟。也很难用上面的GSAP做一个有效的模拟,其中有一个关键点的区别就是:时间&响应。固长动画有一个可以明确描述的时间,以及可以明确描述的运动轨迹;交互动画,有一个运行着的没有尽头的时间轴,所有物件儿在这个时间轴内按照各自的习性运动着,以及会收到环境变化的影响。

这么一描述,好像交互动画才更有灵魂,以及跟贴近于我们的世界。

interesting~

那么用VUE如何在处理交互以上的动画呢?

其实很简单,大家都知道,VUE,或则REACT都有一个数据绑定的功能,这也是他们各自的三板斧。

所以借用此功能,常规情况下,我们都用MVVM框架做一些数据绑定的工作,当我们绕过CSS,用js去制作动画的时候,我们也得利用这特性。

我们希望最终的画面是这样的。

在鼠标移入之前,元素们看起来有点像在做布朗运动,但又有点区别。总之你不能准确地的用“从哪里来,到哪里去”这样的描述去概括这样的画面。

以较优方案来处理,每一个你看起来像是雪花的东西在VUE的应用内都是一个“组件”,欣仔把他叫做ball组件:以下都叫ball。我们给ball 的样式属性“style”绑定了一个对象,当这个对象内的属性作出变化,继而直接影响到了ball的样式表达。

所以我们在ball内的template内这样编写我们的源码:

  1. <template>

  2. <div :style ='style.ball.style ' style ="background:#ffffff; opacity: 0.8;" >

  3. </div>

  4. </template>

就是这么简单明了,在

:style ='style.ball.style '

这里做了一个样式的绑定,于是我们只需在组件的脚本中增加对应的data就行了。

  1. <script>

  2. export default {

  3. data (){

  4. return {

  5. style:{

  6. //绑定元素的style属性

  7. ball:{style:{}}

  8. }

  9. }

  10. }

  11. }

  12. </script>

至于你想知道为什么欣仔要叫他style.ball.style 而不是叫ball.style 或者直接叫ball?

这不在我们这篇分享的讨论范围内,如有机会,欣仔肯定也会写篇文章告诉大家平日里大家开发的一些非必要但是又可以增加开发质量的心得,但在这篇文章里,大家就叫他style.ball.style好了。其实也好理解,就是style集合下面的ball类型的style属性。

如何让ball动起来?我们只需改变style.ball.style的内容就可以了。

以一个简单的left和top为例子:

ball.style可以写成

  1. style:{

  2. ball:{

  3. style:{

  4. top: "100px",

  5. left: "100px"

  6. }

  7. }

  8. }

就这样即可完成ball 样式定位至left 和 top 分别为100px和100px的位置(首先ball的样式的position得预设为“absolute”)。

当我需要改变ball的位置,就是直接改变style.ball.style.left或者style.ball.style.top 的内容。

但限于篇幅的问题,欣仔需要直接分享给大家较优的解决方案。

欣仔需要ball的属性变化是一个比较独立,耦合度相对较低方便复用的对象,所以style.ball或者style.ball.style至少得是一个完整的名叫StyleObject类型描述,这个类型有着自己的灵魂,而不用受ball组件的牵绊,由于ball是最接近视觉层的,所以他们之间的牵盼关系得反过来。。

这个StyleObject 需要设计成这样:1: 有一些基本的属性:

width,height,scale,left,top,rotation ,这些属性同时有这个get和set的方法。

同时StyleObject 还有一个完整的关于样式的描述,封装以上这些属性。

我们用一个独立的独立的object 对象去包装以上这些属性。由于这些属性在动画过程是是不断变化的,所以属性的包装需要在动画时间行进的过程中时刻进行着。

2:StyleObject 内又一套控制自己动画过程的完整的逻辑,

3:StyleObject内还有一个叫style的get方法,这get方法用来返回一个完整的样式表达。

所以我们在做

:style ='style.ball.style '

这个样式绑定的时候,实际上是把ball的style属性绑定到了StyleObject的style。

StyleObject的大致就长这样:

    //StyleObject.js

  1. import { ShinEventDispatcher } from 'shinevent';

  2. class StyleObject extends ShinEventDispatcher{

  3. constructor(){

  4. super();

  5. this.scale= 1;

  6. this.rotation= 0;

  7. this._style={};

  8. }

  9. set width(num){

  10. this._width=num;

  11. }

  12. get width(){

  13. return this._width;

  14. }

  15. set height(num)

  16. {

  17. this._height=num;

  18. }

  19. get height()

  20. {

  21. return this._height;

  22. }

  23. set x(num)

  24. {

  25. this._left=num;

  26. }

  27. get x(){

  28. return this._left;

  29. }

  30. set y(num)

  31. {

  32. this._top=num;

  33. }

  34. get y()

    {

  35. return this._top;

  36. }

  37. set scale(num){

  38. this._scale=num;

  39. }

  40. get scale()

  41. {

  42. return this._scale;

  43. }

  44. set rotation(num){

  45. this._rotstion=num;

  46. }

  47. get rotation(){

  48. return this._rotstion;

  49. }

  50. update(){

  51. this._style={

  52. width: `${this.width}px`,

  53. height: `${this.height}px`,

  54. position: 'absolute',

  55. borderRadius: `${this.width/2}px`,

  56. transform: `scale(${this.scale},${this.scale})

  57. rotate(${this.rotation}deg)

  58. translate(${this.x}px,${this.y}px)`

  59. }

  60. }

  61. get style(){

  62. return this._style

  63. }

  64. }

  65. export default StyleObject

大家可以顾名思义的知道各个方法是干嘛用的。

为啥要用到ShinEventDispatcher?

:是为了解耦用的。至于解耦的一些思路,可以看我的上一篇文章:

前端如何优雅的解耦

为了更好的实现扩展,我们不在StyleObject内插入更多的代码,而是选择新建一个BallStyleObject类型实现扩展,在这个类型里面实现更细化的与ball更相关的动画逻辑。

BallStyleObject 和 StyleObject 是子类与父类的继承关系。

用UML图示描述两者时间的关系可以描述为如下:

BallStyleObject直接继承自StyleObject,在BallStyleObject增加了一些与ball的运动相关系的一些属性,以及重写了update方法。

最终BallStyleObject长这样:

  1. import StyleObject from './StyleObject' ;

  2. class BallStyleObject extends StyleObject {

  3. constructor(){

  4. super();

  5. this.moveSpeed= Math.random()* 10+ 5;

  6. this.moveSpeed2= Math.random()* 10- 5;

  7. this.width= this.height= Math.random()* 5+ 5;

  8. this.tox= this.x= Math.random()* 750;

  9. this.toy= this.y= Math.random()* 1200;

  10. this.roat= 0;

  11. this.ra= 500;

  12. this.tora= 100+ Math.random()* 50;

  13. this.roatSpeed= Math.random()* 10- 5;

  14. }

  15. update(){

  16. //更新方法为没一次增加自己的y(top坐标)

  17. //属性值映射在get style 方法上

  18. this.roat+= this.roatSpeed;

  19. this.ra+=( 100- this.ra)* 0.1;

  20. this.toy+= this.moveSpeed;

  21. this.tox+= this.moveSpeed2;

  22. this.x+=( this.tox- this.x)* 0.3;

  23. this.y+=( this.toy- this.y)* 0.2;

  24. //如果走到屏幕大于1200的屏幕外的时候,

  25. //重新更新自己的位置

  26. if(this.y> 1200)

  27. {

  28. this.tox= this.x= Math.random()* 750;

  29. this.y=this.toy=- 200;

  30. this.moveSpeed= Math.random()* 10+ 5;

  31. this.moveSpeed2= Math.random()* 10- 5;

  32. }

  33. super.update();

  34. }

  35. }

  36. export { BallStyleObject };

区别于StyleObject内的update的方法,BallStyleObject更实质性的对当前对象的各类属性作出了改变操作。最终用

super.update();

调用父类的update方法,使得经过变更的各个样式的属性更新至_style内。

然后回到ball这个组件内,现在只需要对style.ball.style 作出调整,在created钩子中示例化StyleObject,实际上ball被绑定的是BallStyleObject。

  1. created: function() {

  2. //在新建的时候注册当前的style元素

  3. let obj= new BallStyleObject();

  4. this.style.ball=obj;

  5. },

到目前为止,已经完成了关键性的3步:

  1. 新建组件类型ball,并将其样式绑定组件内相关属性。

  2. 新建StyleObject,封装与样式相关的各个属性,以及继承一个新的BallStyleObject类型,封装与ball相关的动画逻辑

  3. 将ball的样式关联上具体的BallStyleObject实例。

以及到目前为止,所有的元素都还没能够动起来。

但是显而易见,只需要实时调用BallStyleObject的update方法,就可以实现样式的变动,由于ball的样式绑定了BallStyleObject,所以一旦BallStyleObject内的样式属性变了,ball就会动起来了。。

关于实时调用,无非就是用setInterval,或者setTimeout,或者requestionAnimation这三种。其中效率以及精确度从高到底排序为

requestionAnimation>setTimeout>setInterval

要编写动画是绕不开这三者之间的纠葛的,而且我们的交互动画是运行在一个没有终点的时间长线上,时间长线就由他们构建的,如果动画元素过多,为每一个元素都单独起一个时间线显然也是不明智的。所以欣仔也建议大家之后在写交互动画的时候建立一个单独的时间轴,所有元素在监听这个时间轴的变化从而作出对应的update操作。

欣仔封装了一个shintimeline,大家可以愉快的去npmjs安装这个包。

npm install --save shintimeline

然后可以愉快的使用啦

具体的使用方法github地址:

https://github.com/shinku/shintimeline

或者在npmjs的网站中搜索shintimeline就可。

从较优的项目解决方案考虑,时间轴的监听也需要用一个类来处理。

欣仔给他起名为:AnimationController

这个类用来建立shintimeline 和 各个可动元素之间的关系。

在shintimeline中,时间轴是一个单例,在这个案例中,

AnimationController

也会被设计成一个单例,以确保他的可用性覆盖面更广。

他大概长这样:

  1. //导入统一的时间轴对象以及时间相关的事件

  2. import {ShinTimeLine ,ShinTimeEvent } from 'shintimeline'

  3. import { ShinEventDispatcher } from "shinevent" ;

  4. class AnimationController extends ShinEventDispatcher {

  5. constructor(){

  6. //单例模式,确保只会被新建一次。

  7. super();

  8. if( AnimationController._instance){

  9. throw "count not be definded again";

  10. }

  11. this.animations= new Map();

  12. this.start();

  13. }

  14. static instance(){

  15. if(! AnimationController._instance)

  16. {

  17. AnimationController._instance= new AnimationController();

  18. }

  19. return AnimationController._instance;

  20. }

  21. start(){

  22. //初始化

  23. ShinTimeLine.addEventListener( ShinTimeEvent.FRAME, this.onenterframe, this);

  24. }

  25. onenterframe(e){

  26. this.update();

  27. }

  28. addCell(ani){

  29. this.animations. set(ani,ani);

  30. }

  31. removeCell(ani){

  32. //移除ani元素

  33. if( this.animations. get(ani)){

  34. this.animations. delete(ani);

  35. }

  36. }

  37. update(){

  38. //console.log(1);

  39. this.animations.forEach((ani,index)=>{

  40. //console.log(ani.update);

  41. if(ani.update){

  42. ani.update();

  43. }

  44. })

  45. }

  46. }

  47. export default AnimationController .instance();

其中又一个addCell方法,目的是把StyleObject 对象push进一个数组。AnimationController的update方法在其监听到时间轴变化的时候执行。他再作为托管角色,遍历并调用了数组内所有StyleObject的update方法。

ShinTimeLine .addEventListener(ShinTimeEvent .FRAME,this .onenterframe,this );

的作用就是在shintimeline在发布FRAME事件的时候,执行特定的方法。

而通过github中的描述可知,FRAME事件会每秒发布24次,这就意味着

AnimationController

的单例对象会每秒24次的onterframe方法(即update方法)。从而所有在这个对象的对象数组中的所有元素,每秒会执行24次,也就意味着,所有元素已经跟时间关系邦迪个起来了,元素们就可以动起来了~

所以到这我们做了另外两件事:

  1. 新建AnimationController 类,并让他监听shintimeline的时间轴事件。

  2. 在ball中,绑定ball与BallStyleObject的同时,把这个StyleObject 添加进AnimationController的单例中

  1. ......

  2. import anic from '../stylecontroller/AnimationController.js'

  3. ......

  4. created: function() {

  5. //在新建的时候注册当前的style元素

  6. let obj= new BallStyleObject();

  7. this.style.ball=obj;

  8. ani.addCell(obj);

  9. },

  10. destroyed: function () {

  11. //被销毁的时候注销掉改当前的style元素

  12. anic.removeCell( this.style.ball);

  13. },

到这儿分享的内容基本快end了,完整的项目地址可以查看:

https://github.com/shinku/vue_animation

通过下载代码,并运行起这个vue的应用,就可以看到对应的效果啦。

源码中欣仔还加入了一些其他的交互特效,读者们又兴趣可以去看看内部的机制。

到这里大家发现其实这个案例中似乎与VUE框架没有多大的关联,关于vue相关的代码也就了了几行。因为本身这就不是一篇介绍VUE的文章,剔除VUE的代码部分,换成REACT或者ANGULAR的代码,也可以正常的运行起来。

这其中的OOP思想,以及涉及到的一些设计模式,包括组件复用的思想,欣仔希望对读者们又更多的启发吧~

如果大家喜欢,欢迎关注我的微信公众号哦,不定期分享技术原创内容,帮助大家一起成长。

相关文章:

前端如何优雅的解耦