可视化系统中动画(帧动画+变形过渡动画)功能开发总结

业务需求

1.动画

* 动画是指单个动画,每个动画只支持一张图片
* 单个动画可以支持两种动画效果:帧动画和变形动画
  • 帧动画可控制以下属性:
    • 可根据图片设计的不同自定义每一行多少帧;
      • 可根据图片设计的不同自定义帧动画行数;
      • 可自定义每一帧之间的时间间隔;
  • 变形动画可控制以下属性:
    • 动画运行时长(毫秒);
    • 动画循环次数;(总次数= 循环次数 +1)
    • 动画的缓动效果;(即运动速度的变化方式)
    • 结束动画ID【动画组使用】(即下一个动画)
    • 是否随页面加载一起触发;
    • 动画结束后是否从页面隐藏;
    • 结束动画在本动画几次循环后触发【动画组使用】
    • 结束动画出发前延迟多少毫秒【动画组使用】
    • 移动距离:
      • 横向移动多少像素
      • 纵向移动多少像素
    • 缩放
      • 宽度增加多少
      • 高度增加多少
      • 帧动画的宽高增加的比例要和原图每一帧的宽高比例相同
    • 旋转:
      • 旋转多少度(角度)
    • 动画编辑时支持属性修改后的实时预览

2.动画组

	* 动画组要求同一组动画可以配置多个动画,多个动画可同时运行,也可以利用“结束动画”属性来实现线性的按次序触发运行。具体配置见前一节标注了【动画组使用】的属性说明;
	* 在页面编排中,拖入画布的是动画组而不是动画

3.页面编排

  • 动画组拖入页面编辑器后,可以修改动画组中各个动画的属性,此修改不会影响原来的动画组中各个动画的属性值。
  • 可以设置页面中动画的触发时机,有三种触发时机:
    • 页面加载时触发
    • 焦点获得时触发
    • 焦点点击时触发

业务系统设计

1.组件设计

  • 动画组管理组件
    • 动画组列表展示
    • 支持新建动画
    • 支持批量删除
    • 列表字段:动画组编号;动画名称;创建时间;操作;
    • 列表项每个动画组支持的操作有:
      • 删除(删除动画组)
      • 编辑(进入编辑器编辑)
      • 修改(只支持修改动画组名称)

2. 动画编辑组件

  • 支持通过拖拽新建动画
  • 支持各种动画属性的配置修改
  • 支持实时预览
  • 动画组支持历史版本记录

3. 组件结构

此时原图已丢失,请看下方说明

说明:
  • views: 项目顶级组件目录
    • aviewer: 动画编辑器组件
      • com-abar:动画编辑器侧边栏组件
      • acanvas: 动画编辑器画布组件
        • animation: 单个动画组件
        • pro-bar:属性控制组件,全局通用
          • pro-item: 单组属性组件
      • tool-abar:动画编辑器顶部工具栏组件
    • animation: 动画组管理组件
    • viewer: 页面编排组件
      • canvas: 页面编排画布组件
        • pro-bar:属性控制组件,全局通用
        • animations: 动画组组件
        • focus:焦点组件

4. 数据库设计(mongdb)

  • 动画组实体——animationEntity
    • 存储动画组及动画组中各个动画的属性数据
  • 动画组历史记录实体——animationHistoryEntiry
    • 存储动画组历史版本数据

业务实现

1. 基本配置

根据需求,我们至少需要在顶级目录下建两个新页面:动画组管理页面和动画编辑页面,所以需要做一些基本配置。

1.1入口路由配置

vue/src/router.js

这一步是将新增加的动画组管理页面和动画编辑器页面加入系统的路由中,否则将访问不到

const routers = [
 ……
  {
        path: '/aviewer',
        meta: {title: '动画编辑器'},
        component: (resolve) => require(['./views/aviewer.vue'], resolve)
    },
 	{
   				path: '/animation',
          meta: {title: '动画组管理'},
          component: (resolve) => require(['./views/animation.vue'], resolve)
   }
 ……
]
1.2 webpack编译配置

vue/webpack.dev.config.js,vue/webpack.prod.config.js

这一步是为了让webpack打包时使用模板生成最终可访问的 html 页面

……

plugins:[
	
	……
				new HtmlWebpackPlugin({
            filename: './web//aviewer.html',
            template: './src/views/aviewer.ejs',
            inject: false
        }),
        new HtmlWebpackPlugin({
            filename: './web/animation.html',
            template: './src/views/animation.ejs',
            inject: false
        })
	
	……

]

……
1.3 后台登录验证

java/com.voole.boot.controller/LoginController

这是一个拦截器,当前端访问任何页面时,都会先验证是否登录,如果未登录,则会返回登录页面,否则,跳转到目标页面。

	……
	@RequestMapping("/aviewer")
	public String aviewer(HttpServletRequest request, HttpServletResponse response, HttpSession session, Model model) throws IOException {
		return isLogin(session, "dist/aviewer");
	}

	@RequestMapping("/animation")
	public String animation(HttpServletRequest request, HttpServletResponse response, HttpSession session, Model model) throws IOException {
		return isLogin(session, "dist/animation");
		/*	return "dist/admin";*/
	}
	……

2. 动画组管理

2.1 后端
控制器

java/com.voole.boot.controller/AnimationController

​ 主要实现以下几个方法:

loadAnimationList()  // 加载动画组列表
createAnimation() // 创建动画组
deleteAnimation() // 删除动画组
loadAnimationData()// 加载单个动画组数据

java/com.voole.boot.controller/AnimationHistoryController

主要实现以下方法:

loadAnimationHistorys() // 加载动画组历史版本记录		
DAO

java/com.voole.boot.dao/AnimationDao

主要定义以下几个接口

saveAnimation()  // 保存动画组
findAnimationById() // 根据ID查询动画组
updateAnimation() // 更新动画组
deleteAnimation() // 删除动画组
loadAnimationList() // 查询动画组列表

java/com.voole.boot.dao/AnimationHistoryDao

主要定义以下几个接口:

save(); // 保存动画组历史记录
loadAnimationHistorys(); // 加载动画组历史版本
queryAnimationHistoryList(); // 查询历史版本列表
findBeanById() // 根据ID查找历史记录
DAOImpl

java/com.voole.boot.dao.impl/AnimationHistoryDaoImpl

java/com.voole.boot.dao.impl/AnimationHistoryDaoImpl

实现DAO中定义的接口

Entity

java/com.voole.boot.entity/AnimationEntiry

java/com.voole/boot/entity/AnimationHistoryEntity

动画组实体定义和动画组历史记录实体定义

2.2 前端
vue组件

vue/src/views/animation.vue

动画组管理界面的vue实现

js

vue/src/views/animation.js

动画组管理界面的VUE实例化脚本

ejs模板

vue/src/views/animation.ejs

webpack打包渲染时使用的模板文件

3. 动画编辑

3.1 动画组件创建
3.1.0 编辑器组件

要进行动画的编辑,必然先得需要一个编辑器页面,所以我们在vue/src/views目录下新建编辑器组件需要的文件:

  • aviewer.vue:编辑器页面的vue组件文件,关键代码:
<template>
<com-abar></com-abar>  // 左边侧栏组件
<com-acanvas ref="acanvas"></com-acanvas> // 画布组件
<tool-abar @save="onSave"></tool-abar> // 顶部工具栏组件
</template>

<script>
  // 导入各组件
   import comaBar from '../epgviewer/com-abar.vue'
    import comaCanvas from '../epgviewer/acanvas.vue'
    import toolaBar from '../epgviewer/tool-abar.vue'
	export default {
    ...
    // 注册局部组件并赋予别名
    components:{
      'com-abar': comaBar,
      'tool-abar': toolaBar,
      'com-acanvas': comaCanvas
    }
  }
</script>

  • aviewer.js :编辑器页面的vue实例化在这里面进行

    import Vue from 'vue'; //引入Vue核心库
    import Vuex from 'vuex'; // 引入 vuex对全局状态进行管理
    import App from '../views/aviewer.vue'; // 引入编辑器组件
    import TWEEN from '../libs/Tween'; // 引入缓动动画库
    import { default as MyAnimation}  from '../libs/ani/animation' // 引入帧动画库
    
    // 初始化 vuex 全局状态
    const store = new Vuex.Store({
        state:{
            selComId:'',  // 当前选中的组件ID
            defaultFocus:'', // 默认焦点组件ID
            selFocusId:'' // 选中的焦点组件ID
        },
        mutations: {
            selectCom (state,id) {
                state.selComId = id;
            },
            setDefaultFocus (state,id){
                state.defaultFocus = id;
            },
            selectFocusCom(state,id){
                state.selFocusId = id;
            }
        }
    });
    Vue.prototype.tween = TWEEN; // 将缓动动画库注册到编辑器Vue实例上,方便组件内全局调用
    Vue.prototype.myAnimation = MyAnimation; //将帧动画库注册到编辑器Vue实例上,方便组件内全局调用
    // 实例化Vue
    new Vue({
        el: '#app',
        store: store,
        render: h => h(App)
    });
    
    
  • aviewer.ejs : 编辑器页面的html模板文件,作用就是为vue实例化提供idappdiv节点,以及引入编译后需要引入的cssscript脚本:

    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
        <title></title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
        <link rel="stylesheet" href="/dist/vendors.css">
        <link rel="stylesheet" href="/dist/aviewer.css">
    </head>
    
    <body>
        <div id="app"></div>
        <script type="text/javascript" src="/dist/vendors.js"></script>
        <script type="text/javascript" src="/dist/aviewer.js"></script>
    </body>
    </html>
    
3.1.1 左侧边栏组件

vue/src/epgviewer/com-abar.vue

<div class="nav-button1" @mouseover="toogle"><Icon  type="navicon" size="32"></Icon></div>  // 触发侧边栏收起展开的按钮
<div class="com-list" id="com-list">
	
</div> // 可用组件列表
<div class="com-cat" id="com-cat"></div> // 组件分类
<dt v-for="element in comBasic" :id="element.name" :title="element.title"  draggable="true" @dragstart="onDrag">
  <div class="com-title">
    <i :class="element.icontype"></i>
    {{element.title}}
  </div>   
</dt>
<script>
	export default {
    data(){
      return {
        comBasic:[
          {name:'com-animation',title:'动画',icontype:'ivu-icon ivu-icon-document-text'},
        ]
      }
    },
    methods:{
      // 当可用组件被拖拽时,利用dataTransfer对象将该组件类型和Id加入datatransfer对象的类型列表中。
      onDrag(e){
        e.dataTransfer.setData('comName', e.target.id)
      }
    }
  }
</script>

关于DataTransfer对象,可参看这篇文章:拖拽献祭中的黑山羊-DataTransfer对象

3.1.2 画布组件

vue/src/epgviewer/acanvas.vue

<template>
    <div>
      // 中央主画布
    	<div class="canvasParent" id="canvasParent">
            <div ref="focusBgDiv" :currentCom="currentCom" class="canvas" @drop="onDrop" @dragover="allowDrop"
                 @mousedown="onSelectCanvas" :style="wrapStyle">
                <div :is="item.name" :key="item.id" :ref="item.id" :data="item" v-for="item in coms" :id="item.id" :position="item.pos"
                     :selected="item.selected" :mincoms="item.mincoms" :minobjs="item.minobjs" v-on:setSelect="getSetSelect" v-on:changeminComs="changeminComs"></div>
            </div>
        </div>
      // 属性控制组件
         <pro-bar :isShow="selected" :hasTab="false" :hasFocus="true" :comTitle="name">
         </pro-bar>
    </div>
</template>

<script>
	 import proBar from '../epgviewer/pro-bar.vue' // 引入属性控制组件
    import comAnimation from '../components/animation.vue' // 引入单个动画组件
    import propItem from '../components/propitem.vue' // 引入单组属性组件
  
  exprot default {
  	...
  	methods:{
      ...
      onDrop(e){
        e.stopPropagation();
        // 获取datatransfer对象的对应组件类型,拖拽实现的关键点
        var comId = e.dataTransfer.getData("comId")
        ...
      }
    }
  }
  
</script>
3.1.3 顶部工具栏组件

vue/src/epgviewer/tool-abar.vue

<template>
    <div class="toolbar">
     <Button size="small" type="info" @click="save">保 存</Button>
     <Button size="small" type="primary" @click="history">历史记录</Button>
       </div>
</template>
<script>
	exprot default {
  ...
  	methods:{
      ...
      save(){
        this.$emit('save',false)
      }
      ...
    }
  }
</script>

这里重点说一下保存功能,我们看到它是向父组件发布了一个save事件,在它的父组件aviewer.vue 中,代码如下:

...
	<com-acanvas ref="acanvas"></com-acanvas> // 将acanvas组件加入到$refs对象中,方便sava方法引用
 <tool-abar @save="onSave"></tool-abar>
 ...
 <script>
	export default {
    ...
    methods:{
      onSave(isPreview,cb){
        this.$refs.acanvas.save(isPreview,cb)
      }
    }
  }
</script>

我们可以看到,父组件中也是利用了$refs对象调用了 画布组件acanvas.vue 中的save方法。整个动画功能中会涉及到许多父子组件、兄弟组件之间的通讯,而大多数都是通过$refs对象来实现的.

##### 3.1.4 动画组件

动画组件本质上是一个拖拽组件,通过拖拽在画布中动态生成一个空的DIV节点。

vue/src/components目录中新建animation.vue,作为动画组件的载体。

关键代码如下:

 <com-drag :id="id" :pos="position" :selected="selected" :style="{width:scalew + 'px',height:scaleh + 'px',backgroundRepeat:'no-repeat',backgroundSize:bgSize}">
    </com-drag>   // 拖拽组件
    <pro-bar :ref="propRefId" :isShow="selected" :hasFocus="hasFocus" :comId="id" :comTitle="title" :comDesc="desc"
             :dsTypeOptions="[{key:'filminfo',title:'影片信息'}]"
             :dsParamsData="ds"
             :showBindDs="bindDs"
             @bind-datasource="bindDataSource"
            :isAnimation="true">
    </pro-bar>  // 属性控制组件
<script>
	 import comBase from '../components/component' //引入组件基类
  export default {
    name: 'com-animation',
    extends:comBase,  // 继承基类
  }
</script>

这里我们可以看到该组件的内容是一个拖拽组件(本身就是一个空DIV)和一个属性控制组件,而这两个组件在这里并没有引入,那是因为该动画组件继承自组件基类,使用到的拖拽组件和属性控制组件都已经在基类中引入了,如下:

vue/src/components/component.js

import comDrag from '../components/draggable.vue'
import proBar from '../epgviewer/pro-bar.vue'
import propItem from '../components/propitem.vue'

export default {
  ...
  components:{
        'com-drag': comDrag,
        'pro-bar': proBar,
        'prop-item': propItem
    },
  ...
}
3.2 基础属性设置

​ 这里说的基础属性,指的就是单个动画的基础属性,主要有以下几个:

  • 坐标:组件在画布上的位置
  • 大小:组件的可视区域大小
  • 逐帧图片:动画图片上传

代码实现主要是在 animation.vue中:

<pro-bar :ref="propRefId" :isShow="selected" :hasFocus="hasFocus" :comId="id" :comTitle="title" :comDesc="desc"
             :dsTypeOptions="[{key:'filminfo',title:'影片信息'}]"
             :dsParamsData="ds"
             :showBindDs="bindDs"
             @bind-datasource="bindDataSource"
            :isAnimation="true">
        <prop-item propTitle="大小"><Input size="small" v-model="w" style="width: 50px;" number/>&nbsp;<Input size="small" v-model="h" style="width: 50px;" number/>
        </prop-item>
        <prop-item>
            <b>逐帧图片:</b>
            <Checkbox v-model="seen">可见</Checkbox>
            <template v-if="!seen">

            </template>
            <template v-else>
                <Button type="text" size="small" style="padding:0;" @click="rmImg">清除<Icon type="close"></Icon></Button>
                <Upload ref="imgUpload" :action="uploadImgUrl"
                        :format="['jpg','jpeg','png','gif']" :maxSize="2048"
                        :onFormatError="upImgTypeErr" :onExceededSize="upImgSizeErr"
                        :onSuccess="handleUpImg" :showUploadList="false">
                    <Button size="small" type="ghost">上传图片</Button>
                </Upload>
            </template>
        </prop-item>
    </pro-bar>

我们可以看到,这里只有组件大小的设置和图片上传组件。而坐标组件是所有组件通用的属性,所以在属性控制组件中,稍后我们可以看到。

这里说一下图片上传,这里是使用了 iview框架的 Upload组件,只需要设置图片上传地址和几个处理函数的名称就行。而后台处理调用时,也修改了java.com.voole.boot.controller.UploadController 控制器,因为动画组件的图片和页面编排中上传的图片要分别上传存储,参数和存储位置都不同,所以新增了专门针对动画图片的上传接口。

3.3 行为属性设置

​ 行为属性设置主要在全局通用的pro-bar 组件中实现。

vue/src/epgviewer/pro-bar.vue

...
	 <template v-if="tabLbl == '行为'">
	 	<template v-if="hasFocus && isAnimation">
                <prop-item propTitle="公共行为">
                    动画时长<Input v-model="commonAction.duration" small clearable number @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                    循环次数<Input v-model="commonAction.loop" small clearable number @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                    动画效果
                    <Select v-model="commonAction.effect" @on-change="onActFocus">
                        <Option v-for="item in effects" :value="item.value" :key="item.value">{{ item.label }}</Option>
                    </Select>
                    结束动画<Input v-model="commonAction.next" small clearable @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                    是否随页面触发<br />
                    <Checkbox  v-model="commonAction.runOnLoad" @on-focus="onActFocus" @on-blur="onActBlur"></Checkbox >
                    <prop-item  propTitle="结束动画选项">
                        几次循环后触发<Input v-model="commonAction.loopForNext" small clearable @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                        触发前延迟<Input v-model="commonAction.delayForNext" small clearable @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                        结束后是否隐藏<br />
                        <Checkbox  v-model="commonAction.isHide" @on-focus="onActFocus" @on-blur="onActBlur"></Checkbox >
                    </prop-item>
                </prop-item>
                <prop-item propTitle="帧动画">
                    每行动画帧数<Input v-model="frameAction.totalFrameNum" clearable number small @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                    总行数 <Input v-model="frameAction.totalRows" clearable number small @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                    每帧间隔(ms)<Input v-model="frameAction.interval" clearable number small @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                </prop-item>
                <prop-item propTitle="移动">
                    X<Input v-model="moveAction.moveX" clearable number small @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                    Y<Input v-model="moveAction.moveY" clearable number small @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                </prop-item>
                <prop-item propTitle="缩放"><Input v-model="scaleAction.width" clearable number small @on-focus="onActFocus" @on-blur="onActBlur"></Input><Input v-model="scaleAction.height" clearable number small @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                </prop-item>
                <prop-item propTitle="旋转">
                    角度<Input v-model="rotateAction.angle" clearable number small @on-focus="onActFocus" @on-blur="onActBlur"></Input>
                </prop-item>
            </template>
   </template>
...

animation.vue组件中,我们使用 pro-bar 时,传入了hasFocusisAnimation 参数,来代表这个组件具有行为面板并且是一个动画组件,而在pro-bar.vue中,我们就根据这两个属性来判断它是否是一个动画组件,如果是,才显示动画相关的属性设置项。

3.4 动画实现
3.4.0 流程设计

​ 开始 ——> 属性侦听 ——> 获得动画组件ID —— > 获取DOM节点 —— > 获取帧动画参数 —— > 启动帧动画

——> 获取缓动动画参数 —— > 启动缓动动画 ——> 检查是否有结束动画,如果有,触发。

3.4.1 属性侦听

由于动画行为属性都在pro-bar中,所以我们这一步的代码在pro-bar.vue中实现:

<script>
	export default {
    ...
    data(){
    	return {
    						// 公共动画属性
    						commonAction:{
                    duration:1000, // 缓动动画时长,毫秒
                    effect:'Easing-Linear-None', // 缓动动画速度曲线
                    next:'', // 结束动画ID
                    loop:0,  // 缓动动画循环次数
                    loopForNext:0, // 本动画执行几次后触发结束动画
                    delayForNext:0, // 触发结束动画前延迟多少毫秒
                    isHide:false, // 动画结束后是否隐藏——页面编排中使用
                    runOnLoad:false, // 是否随页面加载而触发动画——页面编排中使用
                },
    						// 帧动画参数
                frameAction:{
                    totalFrameNum:0, // 每一行帧数(针对帧动画图片)
                    interval:0, // 每一帧之间的间隔,控制帧动画速度
                    totalRows:1 // 帧动画图片总行数
                },
                // 位移动画参数  
                moveAction:{
                    moveX:0,  // 横向位移量,单位px
                    moveY:0   // 纵向位移量,单位px
                },
                // 缩放动画参数  
                scaleAction:{
                    width:0, // 宽度变化值,px
                    height:0  // 高度变化值,px
                },
                // 旋转动画参数
                rotateAction:{
                    angle:0  // 旋转角度, 单位 deg
                },
                timeout:''  // 防抖定时器
  		}
  },
    methods:{
      			// 重置动画
      			refreshAction() {
                if (!this.setAct) return;
                var _self = this;
                _self._debounce2(_self._updatePro, 1000);

            },
              // 更新参数,调用动画预览
              _updatePro() {
                var _self = this;
                _self.$parent.$parent.save(false, function () {
                  if (_self.$parent.$parent.$refs[_self.comId]) {
                    _self.$parent.$parent.$refs[_self.comId][0].runTransform();
                  }
                });
              },
              // 监听防抖  
            _debounce2(func, wait) {
                var _self = this;
                return (() => {
                    if (_self.timeout !== "") {
                        clearTimeout(_self.timeout);
                    }
                    _self.timeout = setTimeout(() => {
                        func();
                    }, wait);
                })();
            }
    },
    ...
    watch:{
       			'moveAction.moveX': {
                handler(newName, oldName) {
                    this.refreshAction();
                },
                immediate:true
            },
            'moveAction.moveY': {
                handler(newName, oldName) {
                    this.refreshAction()
                }
            },
       			'commonAction.effect': {...},
             ...                       
                                   
    }
  }
</script>

data对象职中,我们定义了动画需要的所有参数,然后在侦听器中侦听这些属性的变化。

注意对于data对象中object对象的属性作监听,需要使用特殊的语法,即监听‘obj.key’,然后设定一个方法,它会在侦听到该属性变化时执行,还有一个可选项,immediate属性设置是否在该选项初始化时立即执行该方法。

Vue框架真听到任何一个在watch对象里设置过的属性变化时,都会触发他们的handler方法,而这些方法全都会调用refreshAction()方法来进行数据库的数据更新保存和属性值变化后动画的实时预览。

由于Vue框架对每一个属性的每一次变化都会进行侦听发应,导致我们在为某个属性输入一个多位数字的值时,会多次触发数据库保存方法和动画实时更新预览,一是不断弹出保存成功的提示框,二是造成计算资源和网络资源的浪费,所以我们在refreshAction()方法加入了防抖功能,来保证同一个属性变化,不管值是多少位,只触发一次保存和动画预览刷新。

在进行数据更新和实时预览刷新时,我们看到以下代码:

	var _self = this;
  _self.$parent.$parent.save(false, function () {
    if (_self.$parent.$parent.$refs[_self.comId]) {
    _self.$parent.$parent.$refs[_self.comId][0].runTransform();
    }
  });

这里,_self.$parent.$parent是设么意思呢?从组件结构那一节我们知道,pro-bar 是的父组件是animation组件,而animation组件的父组件是 acanvas 组件,而我们知道,所有设置过 ref 属性的子组件都会在组件的$refs对象中,而在acanvas 组件里加载组件时我们可以看下它的代码:

...
<div :is="item.name" :key="item.id" :ref="item.id" :data="item" v-for="item in coms" :id="item.id" :position="item.pos"
                     :selected="item.selected" :mincoms="item.mincoms" :minobjs="item.minobjs" v-on:setSelect="getSetSelect" v-on:changeminComs="changeminComs"></div>
...

我们可以看到,这里通过:ref="item.id" 将每一个组件都加入到了$refs 对象中,当然也就包括我们拖拽进来的animation组件,所以通过$parent$refs 对象,我们既可以调用到 acanvas组件的save() 方法来保存更新数据,也可以调用到animation组件的runTransform() 方法类触发动画的实时预览。

当我们明白了其中原理,就会知道,_self.$parent.$parent.$refs[_self.comId][0].runTransform(); 其实可以有另一种更简单的写法:_self.$parent.runTransform();

3.4.2 帧动画实现

​ 帧动画的实现,主要是利用了自己开发的一个帧对象库,对于该库的说明,见该动画库的文档。现就主要逻辑做出说明:

vue/src/components/animation.vue

<template>
	<div>
    <!-- 在style属性中,设置动画元素的宽、高以及背景图片大小为计算属性,用于实现缩放效果 -->
    	<com-drag :id="id" :pos="position" :selected="selected" :style="{width:scalew + 'px',height:scaleh + 'px',backgroundRepeat:'no-repeat',backgroundSize:bgSize}">
    </com-drag>
  </div>
</template>

<script>
	...
  mounted(){
    this.isAnimation = false; // 该属性用来控制是否启动动画
    this.createAnimationInstance(); // 创建帧动画实例
  	this.loadImage(); // 加载背景图片
  },
    data(){
      return {
        ...
        imgWidth:320, // 动画背景图片原图宽度
        imgHight:400, // 动画背景图片原图高度
        Animation:"", // 帧动画实例
        repeatAnimation:"", // 帧动画重复动画实例
        isAnimation:false, // 是否允许启动动画
        ...
      }
    },
    methods:{
      	// 利用帧动画库初始化加载帧动画的背景图片
        loadImage(){
          var imgUrl = this.imgUrl(this.src);
          this.Animation.loadImage([imgUrl]);
        },
          // 利用帧动画库类创建一个帧动画实例,并且在创建前清空之前的动画实例,避免重复创建
        createAnimationInstance(){
          this.Animation = "";
          this.repeatAnimation = "";
          if(this.myAnimation && this.Animation === ""){
            this.Animation = new this.myAnimation();
          }
        },
          runTransform(){
                // 保存this
                var _self = this;
                // 获取DOM元素
                var comId = _self.id;
                var com =  document.getElementById(comId);
                if(com){
                    // 获取帧动画参数
                    var imgUrl = _self.imgUrl(_self.src);
                    var imgWidth = _self.imgWidth;
                    var imgHeight = _self.imgHight;     
                    var rows = _self.data.data.frameAction.totalRows || 1;
                    var frameLength = _self.data.data.frameAction.totalFrameNum;
                    var frame = 0;
                    var interval = _self.data.data.frameAction.interval;
                    interval = interval === 0 ? 200 : interval;
                  	// 只有帧动画每行帧数大于0且行数大于0时,才运行帧动画
                    if(frameLength > 0 && rows > 0){
                      // 生成背景图片位置变化的数组
                        var positions = _self.Animation.genPositionsByPersent(frameLength,rows);
                    }else{
                        var positions = ["0% 0%"];
                    }
                  // 只有帧动画的位置信息超过1个,才运行帧动画,否则为非帧动画
                    if(positions.length === 1){
                        return;
                    }
                    //初始化一个时钟,用来记录上一帧开始的时间
                    var lastTick = 0;
                    lastTick = +new Date();
                   //定义一个执行帧动画的方法,参数为当前时间
                    function runFrameAnimation(now) {
                        var now = now || +new Date();
                       // 如果当前时间减去上一帧执行时间大于等于设定的每帧间隔时间,才会切换到下一帧
                        if(now-lastTick >= interval){
                            frame++;
                           // 当前帧如果是位置数组中最后一个位置,则回到第一帧
                            if(frame === positions.length){
                                frame = 0;
                            }
                           // 当前帧背景图片位置更新
                            var position = positions[frame].split(' ');
                            com.style.backgroundPosition = position[0] + ' ' + position[1];
                            lastTick = now;
                        }
                    }
                  // 定义帧动画的重复播放,enterFrame保证里面的方法按帧执行
                    _self.repeatAnimation = _self.Animation.enterFrame(function () {
                        var now  = +new Date();
                        if(frameLength > 0){
                            runFrameAnimation(now);
                        }

                    });
                   // 启动帧动画
                    _self.repeatAnimation.start(interval);
                  ...
                  // 此处省略缓动动画实现,下一小节讲
                }else{
                    return;
                }

            },
            // 封装一个启动动画的函数,用于拓展场景,比如某些情况下不允许动画自动开始播放
            startAnimation(){
                console.log(this.id);
                this.isAnimation = true;
                this.runTransform();
            }
    },


</script>
3.4.3 缓动动画实现

缓动动画的实现主要是利用了开源的 Tween.js 缓动动画库,它可以实现多种速度曲线的缓动动画,具体API和使用文档可以自行百度。

vue/src/components/animation.vue

<script>
	export default {
    ...
    mounted:{
      // 初始化 TweenJs 监听
      this.tweenAni();
    },
    data(){
    	retur{
        w: 320, // 动画元素宽度
        h: 440, // 动画元素高度
        wa:0, // 动画元素缩放横向变化量
        ha:0, // 动画元素缩放纵向变化量
    		transform:"", // 缓动动画实例
  	}
  },
    methods:{
      	tweenAni: function () {
          // 调用requestAnimationFrame 来实现监听和刷新
          requestAnimationFrame(this.tweenAni);
          if(this.tween){
            this.tween.update();
          }
            },
       runTransform(){
         ... //省略帧动画部分
          // 获取动画参数
         						// 获取原始坐标
                    var pos = {
                        x:_self.data.x,
                        y:_self.data.y
                    };
                    var next = _self.data.data.commonAction.next; // 结束动画ID
                    var duration = _self.data.data.commonAction.duration; // 缓动动画时长
                    var effectStr = _self.data.data.commonAction.effect; // 缓动动画效果
                    var loop = _self.data.data.commonAction.loop; //循环次数
                    loop = loop -1 < 0 ? 0 : loop-1;
                    var loopForNext = _self.data.data.commonAction.loopForNext; //几次循环后触发下一个动画
                    loopForNext = loopForNext -1 < 0 ? 0 : loopForNext-1;
                    var lastLoop = loop - loopForNext < 0 ? 0 : loop - loopForNext; //触发结束动画时剩余循环次数
                    var delayForNext = _self.data.data.commonAction.delayForNext;// 触发结束动画前的延迟
                    loopForNext = loopForNext === 0 ? 1 :loopForNext;
          					// 处理缓动速度曲线参数
                    var effect = effectStr || "Easing-Linear-None";
                    var ef = effect.split("-");
                    var fn = this.tween[ef[0]][ef[1]][ef[2]]; 
                    // 获取起始位置参数
                    var startX = _self.$parent.objs[comId].x || _self.objs[comId].x;
                    var startY = _self.$parent.objs[comId].y || _self.objs[comId].y;
                    var startW = 0;
                    var startH = 0;
                    var startA = 0;
                    // 获取结束位置参数
                    var posXend = parseInt(this.data.data.moveAction.moveX) + parseInt(pos.x);
                    var posYend = parseInt(this.data.data.moveAction.moveY) + parseInt(pos.y);
                    var wEnd =  parseInt(this.data.data.scaleAction.width);
                    var hEnd =  parseInt(this.data.data.scaleAction.height);
                    var aEnd = parseInt(_self.data.data.rotateAction.angle) ;
                    // 将起始位置和结束位置参数赋值为动画要求的对象格式
                    let AnimationMoveNow = {
                            x: startX,
                            y: startY,
                            w: startW,
                            h: startH,
                            a: startA,
                        },
                        AnimationMoveEnd = {
                            x:posXend,
                            y:posYend,
                            w:wEnd,
                            h:hEnd,
                            a:aEnd,
                        } ;
                    // 新建一个缓动动画实例

                   _self.transform =  new _self.tween.Tween(AnimationMoveNow) // 传入开始位置
                        .to(AnimationMoveEnd, duration) // 指定时间内完成结束位置
                        .easing(fn) // 缓动方法名
                        .onUpdate(() => {
                            // 上面的值更新时执行的设置
                            var x = AnimationMoveNow.x;
                            var y = AnimationMoveNow.y;
                            _self.wa = AnimationMoveNow.w;
                            _self.ha = AnimationMoveNow.h;
                            _self.$parent.objs[comId].pos = 'left:' + x + 'px;' + 'top:' + y + 'px;';
                            _self.$parent.objs[comId].pos += 'transform:rotate('+AnimationMoveNow.a+'deg);-ms-transform:rotate('+AnimationMoveNow.a+'deg);-moz-transform:rotate('+AnimationMoveNow.a+'deg);-webkit-transform:rotate('+AnimationMoveNow.a+'deg);-o-transform:rotate('+AnimationMoveNow.a+'deg);'
                        })
                        .repeat(loopForNext) // 在规定的下一个动画触发时停止当前动画
                       .onComplete(()=>{
                     			// 触发下一个动画后继续剩余的循环次数
                           let AnimationMoveNow = {
                                   x: startX,
                                   y: startY,
                                   w: startW,
                                   h: startH,
                                   a: startA,
                               },
                               AnimationMoveEnd = {
                                   x:posXend,
                                   y:posYend,
                                   w:wEnd,
                                   h:hEnd,
                                   a:aEnd,
                               } ;
                           new _self.tween.Tween(AnimationMoveNow) // 传入开始位置
                               .to(AnimationMoveEnd, duration) // 指定时间内完成结束位置
                               .easing(fn) // 缓动方法名
                               .onUpdate(() => {
                                   // 上面的值更新时执行的设置
                                   var x = AnimationMoveNow.x;
                                   var y = AnimationMoveNow.y;
                                   _self.wa = AnimationMoveNow.w;
                                   _self.ha = AnimationMoveNow.h;
                                   _self.$parent.objs[comId].pos = 'left:' + x + 'px;' + 'top:' + y + 'px;';
                                   _self.$parent.objs[comId].pos += 'transform:rotate('+AnimationMoveNow.a+'deg);-ms-transform:rotate('+AnimationMoveNow.a+'deg);-moz-transform:rotate('+AnimationMoveNow.a+'deg);-webkit-transform:rotate('+AnimationMoveNow.a+'deg);-o-transform:rotate('+AnimationMoveNow.a+'deg);'
                               })
                               .repeat(lastLoop).start();
                     // 如果有结束动画,则在参数规定的延迟时间时候启动结束动画
                           if(next && next !== ""){
                               console.log("next");
                               setTimeout(function () {
                                   if( _self.$parent.$refs[next][0]){
                                       _self.$parent.$refs[next][0].startAnimation();
                                   }
                               },delayForNext);
                           }
                       });
                    _self.transform.start(); // 启动缓动动画
       }
    }
    
  }
</script>

为了实现第一个动画循环n次后触发第二个动画(即结束动画)的需求,我们将第一个动画分成了两段执行,第一段执行n次,然后结束,触发执行第二个动画,接着再重新开始执行第一个动画,结束 N-n 次后结束(N为总循环次数)。

4.页面编排

4.1 动画组组件

​ 由于我们最后在页面编排中使用动画时其实是要使用整个动画组,所以我们需要一个组件来承载动画组,并且可以拖拽入页面编排的画布。所以我们还是要新建一个动画组组件(编译后也是一个空DIV),在里面引入拖拽组件来获得拖拽能力。

vue/src/components/animations.vue

<template>
	<div>
    <!-- 使用拖拽组件获得拖拽能力 -->
    <com-drag :id="id" :pos="position" :selected="selected" :style="{width:scalew + 'px',height:scaleh + 'px'}">
    </com-drag>
  </div>
</template>
...
4.2 动画组组件拖入页面编排画布

我们有了动画组组件并且获得了拖拽能力,现在我们只要将它放入页面编排的左侧边栏就可以实现拖拽入画布了:

vue/src/epgviewer/com-bar.vue

<script>
	export default {
    data(){
      return{
        ...
        comOhter:[
          ...
          {name:'com-animations',title:'动画组组件',icontype:'ivu-icon ivu-icon-more'},
        ]
      }
    }
  }
</script>

当然,由于com-bar组件是canvas组件的子组件,要想在它里面使用动画组组件(以及动画组件),还得在canvas组件中引入动画组组件和动画组件并注册为组件内部全局组件。

vue/src/epgviewer/canvas.vue

<script>
  ...
	import animations from '../components/animations'
  import animation from '../components/animation'
  export default {
    ...
    components:{
      //...
      'com-animations':animations,
      'com-animation':animation
    }
  }
</script>

此时,我们实现了将动画组组件拖入页面画布,但是这时我们拖入的只是一个空的动画组组件,那么我们如何将它与我们已经制作好的动画组关联呢?即使关联好了,我们的需求是当动画组拖入画布,我们可以对动画组中的单个动画进行参数调整,但是我们拖入的是动画组而非动画,所以动画的属性面板是无法加载到画布中的,又该怎么办呢?我们采用了一种巧妙的逻辑:

  • 拖入动画组组件,动画组组件的属性面板中提供所有已制作好的动画组列表,选择目标动画组
  • 载入目标动画组的数据,遍历动画组的子组件(单个动画组件),将每一个单个动画都复制给页面的子组件对象,使之成为页面的组件
  • 删除动画组组件

关键代码:

vue/src/components/animations.vue

<pro-bar :ref="propRefId" :isShow="selected" :hasFocus="hasFocus" :comId="id" :comTitle="title" :comDesc="desc"
             :showBindDs="bindDs">
        <prop-item propTitle="动画组选择">
          	// 下拉选择动画组,值是一个侦听属性
            <Select v-model="AnimationID">
                <Option v-for="item in Animations" :value="item.id" :key="item.id">{{ item.name }}</Option>
            </Select>
        </prop-item>
    </pro-bar>
...
<script>
  export default{
    // 组件挂载到页面中后加载所有动画组组件列表
    mounted(){
     this.loadAnimations();
   },
    data(){
      return {
         AnimationID:"", // 选中的动画组ID
        Animations:[], // 动画组列表
      }
    },
    methods:{
      // 根据动画组ID加载动画组数据,并且将其中的动画组件复制给父级页面
      loadAnimationData (AnimationID) {
                this.cfg.store.animationId(AnimationID);
                var iurl = this.cfg.iurl('aviewer', 'loadAnimationData');
                var _self = this;
                var data = { animation_id: AnimationID}
                this.util.ajax(this, iurl, data, function (response) {
                    var all = JSON.parse(response.data);
                    if (all.hasOwnProperty('coms')) {
                        _self.coms = all.coms;
                        var comId = '';
                        for (var i in _self.coms) {
                            _self.$parent.coms.push(_self.coms[i]);
                            _self.$parent.objs[_self.coms[i].id] = _self.coms[i];
                        }
                        var parentComId = _self.id;
                        for(var j=0;j<_self.$parent.coms.length;j++){
                                if(_self.$parent.coms[j].id === parentComId){
                                    _self.$parent.coms.splice(j,1);
                                }
                        }
                      	// 从页面中删除当前动画组组件
                        delete _self.$parent.objs[parentComId];
                    }
                });
            },
      		// 加载动画组组件列表
            loadAnimations(){
                var iurl = this.cfg.iurl('aviewer', 'loadAnimationList');
                var _self = this;
                var data = {};
                _self.util.ajax(_self, iurl, data, function (response) {
                    _self.Animations = response.data;
                });
            }
    }
    watch:{
    ...
    // 侦听该属性的改变(即下拉选择框值的变化)
    	AnimationID(val){
        this.loadAnimationData(val);
      }
  }
  }
	 
</script>
4.3 触发时机

根据需求,动画在页面中的触发时机也可以设置,一共有三种触发机制:

  • 随页面加载而一起触发
  • 焦点移动事件中触发
  • 焦点点击事件中触发

其中,随页面加载一起触发的功能我们再动画组件自身的属性中绑定了,而剩下的焦点事件触发,我们就需要在焦点组件的属性面板中设置了。

vue/src/epgviewer/pro-var.vue

...
<template v-if="tabLbl == '行为'">
  ...
	<prop-item v-if="hasFocus && !isAnimation && (comTitle==='焦点组件')" propTitle="动画绑定">
                <prop-item propTitle="焦点移动触发动画">
                    <Select size="small" style="width:100px;" v-model="eatarget.type">
                        <Option value="none" key="none"></Option>
                        <Option value="com" key="com">联动组件</Option>
                    </Select>
                    <Input v-if="eatarget.type == 'com'" size="small" placeholder="点击输入框,然后右键选择组件" @on-focus="onClickInput2ComA" style="width:150px;" v-model="eatarget.value"/>
                </prop-item>
                <prop-item propTitle="确认键触发动画">
                    <Select size="small" style="width:100px;" v-model="fatarget.type">
                        <Option value="none" key="none"></Option>
                        <Option value="com" key="com">联动组件</Option>
                    </Select>
                    <Input v-if="fatarget.type == 'com'" size="small" placeholder="点击输入框,然后右键选择组件" @on-focus="onClickInput2ComFA" style="width:150px;" v-model="fatarget.value"/>
                </prop-item>
            </prop-item>
</template>
<scrript>
	export default {
  	data(){
  		fatarget:{type: 'none', value: ''},
  		eatarget:{type: 'none', value: ''},
  	},
  	methods:{
  		 		onClickInput2ComA(e) {
            this.currWait.k = 'target'
            this.currWait.v = 'eatarget'
            },
        onClickInput2ComFA(e) {
          this.currWait.k = 'target'
          this.currWait.v = 'fatarget'
        },
  	}
  }
</scrript>

我们定义了两个对象,一个是焦点移动动画事件对象,一个是焦点点击动画事件对象,并且做了两个联动组件的绑定,这样就可以将每一个单独的动画组件绑定到特定的焦点元素上面了。

4.4 预览

在页面编排中使用了动画组后,我们需要能够在页面预览中实现动画的触发和绑定。

页面预览实则是通过后端渲染实现的,所以我们需要修改后端代码来实现动画的预览。

首先,后端渲染时要读每一个组件的模板,这些模板放置在templates/epgcoms目录下,每一个可拖拽组件都有一个单独的目录,所以我们在这个目录下为我们的动画组件新建一个目录(由于动画组组件只是充当了一个过渡作用,并不会在最后的渲染中使用,所以不用为它建模板)templates/epgcoms/animation ,并且为它建立模板:

templates/epgcoms/animation/view.ftl

<#setting number_format="#">  // 解决ftl的数字格式问题
<div id="${d.id}" parentid="${d.animationsId}"  class="com-animation" style="display:none;z-index:9999;${d.position}${d.wrapStyle}background-image:url('${d.src}')"> //最终出现在页面中时的模板
</div>
<script language="JavaScript" type="text/javascript">
  // 将组件的各个属性赋值到前端的动画组件对象中
    animations['${d.id}'] = {
        imgHight:${d.imgHight},
        imgWidth:${d.imgWidth},
        animationsId:'${d.animationsId}',
        wa:${d.wa},
        w:${d.w},
        ha:${d.ha},
        h:${d.h},
        moveX:${d.moveAction.moveX},
        moveY:${d.moveAction.moveY},
        totalFrameNum:${d.frameAction.totalFrameNum},
        interval:${d.frameAction.interval},
        totalRows:${d.frameAction.totalRows},
        duration:${d.commonAction.duration},
        next:'${d.commonAction.next}',
        loop:${d.commonAction.loop},
        loopForNext:${d.commonAction.loopForNext},
        delayForNext:${d.commonAction.delayForNext},
        isHide:'${d.commonAction.isHide?c}',
        runOnLoad:'${d.commonAction.runOnLoad?c}',
        effect:'${d.commonAction.effect}',
        scalew:${d.scaleAction.width},
        scaleh:${d.scaleAction.height},
        angle:${d.rotateAction.angle},
        src:'${d.src}',
        isRunning:false
    };
</script>

这里我们看到两个对象,一个是从后端赋值过来的 d 对象,其实它就是单个动画组件的data属性,另一个是animations对象,这是一个在页面模板中定义的对象,用来存储所有的动画组件数据。它的作用是利用freemaker模板将后端数据复制到前端,供前端渲染时使用。

上面我们给焦点组件的属性新加了两个对象,在渲染时也要渲染到焦点的DOM节点属性上:

java.com.voole.boot.tpl/BaseResolver.java

protected void buildTarget() {
  ...
    if (StringUtils.isNotBlank(data.getString("eatarget"))) {
				JSONObject ftarget = data.getJSONObject("eatarget");
				if (!StringUtils.equals(ftarget.getString("type"), "none")) {
					String type = ftarget.getString("type");
					String value = ftarget.getString("value");
					target.append("  feat=\"" + type + "\" feav=\"" + value + "\"");
				}
			}
			if (StringUtils.isNotBlank(data.getString("fatarget"))) {
				JSONObject ftarget = data.getJSONObject("fatarget");
				if (!StringUtils.equals(ftarget.getString("type"), "none")) {
					String type = ftarget.getString("type");
					String value = ftarget.getString("value");
					target.append("  ffat=\"" + type + "\" ffav=\"" + value + "\"");
				}
			}
}

我们这里将焦点点击动画事件的联动组件类型和值(其实就是动画组件ID)赋值给 featfeav属性,把焦点移动动画事件的联动组件类型和值赋值给 ffatffav属性。

然后我们需要在页面通用模板中去编写动画播放逻辑:

templates/tpl/epg_page.html

...
<head>
	<script type="text/javascript" language="JavaScript">
        //...
        var animations = {}; //定义一个对象,来存储所有动画组件的属性数据
    </script>
</head>
<body>
  <!--之前在animation/view.ftl 中所有的内容其实经过后台渲染全部生成了html代码放入了content字段中 -->
  ${content} 
</body>
...
<!-- 引入缓动动画库和帧动画库,其中帧动画库是经过webpack打包压缩过的,在vue组件中使用的则是未经打包压缩的原始版本 -->
<script type="text/javascript" language="JavaScript" charset="utf-8" src="../js/animation.js"></script>
<script type="text/javascript" language="JavaScript" charset="utf-8" src="../js/Tween.js"></script>
...
<script type="text/javascript">
   // 定义播放动画的方法,这里的逻辑基本上与 animation 组件中的一样,不同的是,在animation组件中,我们是通过 vue中的父子、兄弟组件的通讯机制来拿各个属性值的,而在这里,我们需要从前端定义的 animations 对象中获取所有组件及其属性值。
    window.runTransform = function(id){
        var Animation = window.animation;
        var tween = window.TWEEN;
        var tweenAni = function () {
            requestAnimationFrame(tweenAni);
            if(tween){
                tween.update();
            }
        }
        tweenAni();
        // 获取DOM元素
        var comId = id;
        var com =  document.getElementById(comId);
        if(com){
            var d = animations[id];
            var tmpX = d.imgWidth / d.w;
            var tmpY = d.imgHight / d.h;
            var bgSizeX = parseFloat(tmpX.toFixed(2)) * 100;
            var bgSizeY = parseFloat(tmpY.toFixed(2)) * 100;
            var bgSize = bgSizeX + '% '+ bgSizeY+'%';
            com.style.backgroundSize = bgSize;
            com.style.display = "block";
            var isRunning = d.isRunning;
            if(isRunning){
                return;
            }
            console.log(d);
            // 获取帧动画参数
            var imgUrl = d.src;
            var imgWidth = parseInt(d.imgWidth);
            var imgHeight = parseInt(d.imgHight);
            var rows = parseInt(d.totalRows);
            var frameLength = parseInt(d.totalFrameNum);
            var frame = 0;
            var interval = parseInt(d.interval);
            interval = interval === 0 ? 200 : interval;
            if(frameLength > 0 && rows > 0){
                var positions = Animation().genPositionsByPersent(frameLength,rows);
            }else{
                var positions = ["0% 0%"];
            }
            console.log(positions);
            if(positions.length === 1){
                return;
            }
            var lastTick = 0;
            lastTick = +new Date();
            function runFrameAnimation(now) {
                var now = now || +new Date();
                if(now-lastTick >= interval){
                    frame++;
                    if(frame === positions.length){
                        frame = 0;
                    }
                    var position = positions[frame].split(' ');
                    com.style.backgroundPosition = position[0] + ' ' + position[1] ;
                    lastTick = now;
                }
            }
            var repeatAnimation = Animation().enterFrame(function () {
                console.log('enterFrame');
                var now  = +new Date();
                if(frameLength > 0){
                    runFrameAnimation(now);
                }

            });
            repeatAnimation.start(interval);
            // 获取动画参数
            var pos = {
                x:parseInt(com.style.left),
                y:parseInt(com.style.top)
            };
            var next = d.next;
            var duration = parseInt(d.duration);
            var effectStr = d.effect;
            var loop = parseInt(d.loop);
            var loopForNext = parseInt(d.loopForNext);
            var delayForNext = parseInt(d.delayForNext);
            var effect = effectStr || "Easing-Linear-None";
            var ef = effect.split("-");
            var fn = tween[ef[0]][ef[1]][ef[2]];
            // 获取起始位置参数
            var startX = pos.x;
            var startY = pos.y;
            var startW = 0;
            var startH = 0;
            var startA = 0;
            // 获取结束位置参数
            var posXend = parseInt(d.moveX) + parseInt(pos.x);
            var posYend = parseInt(d.moveY) + parseInt(pos.y);
            var wEnd =  parseInt(d.scalew);
            var hEnd =  parseInt(d.scaleh);
            var aEnd = parseInt(d.angle) ;
            // 将起始位置和结束位置参数赋值为动画要求的对象格式
            var AnimationMoveNow = {
                    x: startX,
                    y: startY,
                    w: startW,
                    h: startH,
                    a: startA,
                },
                AnimationMoveEnd = {
                    x:posXend,
                    y:posYend,
                    w:wEnd,
                    h:hEnd,
                    a:aEnd,
                } ;

            var vendor = (function(){
                var transformNames = {
                    webkit: 'webkitTransform',
                    Moz: 'MozTransform',
                    O: 'OTransform',
                    ms: 'msTransform',
                    standard: 'transform'
                };
                for (var key in transformNames) {
                    if (com.style[transformNames[key]] !== undefined) {
                        return key;
                    }
                }
                return false;
            })();

            function prefixStyle(style) {
                if (vendor === false) {
                    return false;
                }
                if (vendor === 'standard') {
                    return style;
                }
                return vendor + style.charAt(0).toUpperCase() + style.substr(1);
            }
            // 新建一个缓动动画

            var transform = new tween.Tween(AnimationMoveNow) // 传入开始位置
                .to(AnimationMoveEnd, duration) // 指定时间内完成结束位置
                .easing(fn) // 缓动方法名
                .onStart(function () {

                })
                .onUpdate(() => {
                    // 上面的值更新时执行的设置
                    console.log('update');
                    var x = AnimationMoveNow.x;
                    var y = AnimationMoveNow.y;
                    com.style.left = x +'px';
                    com.style.top = y + 'px';
                    com.style[prefixStyle('transform')] = 'rotate('+AnimationMoveNow.a+'deg)'
                })
                .repeat(loopForNext)
                .onComplete(()=>{
                    var AnimationMoveNow = {
                            x: startX,
                            y: startY,
                            w: startW,
                            h: startH,
                            a: startA,
                        },
                        AnimationMoveEnd = {
                            x:posXend,
                            y:posYend,
                            w:wEnd,
                            h:hEnd,
                            a:aEnd,
                        } ;
                    new tween.Tween(AnimationMoveNow) // 传入开始位置
                        .to(AnimationMoveEnd, duration) // 指定时间内完成结束位置
                        .easing(fn) // 缓动方法名
                        .onStart(function () {
                        })
                        .onUpdate(() => {
                            // 上面的值更新时执行的设置
                            var x = AnimationMoveNow.x;
                            var y = AnimationMoveNow.y;
                            com.style.left = x +'px';
                            com.style.top = y + 'px';
                            com.style[prefixStyle('transform')] = 'rotate('+AnimationMoveNow.a+'deg)'
                        })
                        .onComplete(
                            function () {
                                if(d.isHide === "true"){
                                    com.style.display = "none";
                                }
                            }
                        )
                        .repeat(loop-loopForNext).start();
                    if(next && next !== ""){
                        setTimeout(function () {
                           window.runTransform(next);
                        },delayForNext);
                    }
                });
                transform.start();
                animations[id].isRunning = true;
        }else{
            return;
        }
    }
</script>
<script type="text/javascript" language="JavaScript" charset="utf-8">
    ${js}
    ekey.ready(".container", function () {
        
			//...
      // ekey加载后,遍历组件,如果它的 runOnload属性(是否随页面加载触发)为 true,则立即运行动画
        for(var key in animations){
            if(animations[key].runOnLoad === "true"){
                window.runTransform(key);
            }
        }


    });
</script>

前面我们将 焦点触发动画的相关属性加到了DOM节点的属性上,那么同样还需要绑定到真正的焦点事件中去:

templates/tpl/js/epgviewer62.js

//...
window.EPGdatasource = new (function () {
  this.onFE = function (e, flag) {
       //...
        // 动画触发——焦点移动事件
        var feav = e.attr('feav');
        if(feav!=null){
            window.runTransform(feav);
        }
    };
    var actInfo="";
    this.onEnter = function (e) {
   
        // 动画触发---焦点点击事件
        var ffav = e.attr('ffav');
        if(ffav!=null){
            window.runTransform(ffav);
        }
     		...
    };
})();
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值