最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • TweenJs浅析

    正文概述 掘金(bugmaker)   2021-03-15   539

    0️⃣ 背景

    小程序中要实现物体飞入动画,效果如下: TweenJs浅析 复杂点:

    1. 曲线运动轨迹。
    2. 动画链。

    1️⃣ 技术选型

    css动画

    CSS 动画(补间动画、关键帧动画)可以在不借助 Javascript 的情况下做出一些简单的动画效果。 你也可以通过 Javascript 控制 CSS 动画,使用少量的代码,就能让动画表现更加出色。

    1. Transition:transition: ;
    1. 需要触发时机,使用css伪类选择器 or 动态设置css/style
    2. 无法直接实现曲线运动轨迹,需要套娃。简单举例:父元素x轴匀速运动,子元素y轴变速运动。
    3. 手动控制动画链,计算delay时间 or 使用sleep方式。
    
    1. Animation:animation:
    1. 小程序下,计算动画起始点后,无法动态修改keyframe
    2. 同transition第ii点。
    3. 同transition第iii点。
    

    js动画

    JavaScript 动画(逐帧动画),每一帧都需要手动控制,可以处理 CSS 无法处理的事情。 例如,沿着具有与 Bezier 曲线不同的时序函数的复杂路径移动,或者实现画布上的动画。

    实现js动画,我们需要关注:

    1. 渲染时机:setTimeout、setInterval、requestAnimationFrame(小程序❌,可以用polyfill)。
    2. 渲染方式:dom、canvas

    在我们的应用场景下,可以通过简单数学公式计算每一帧。简单拆解下: 变量:

    • 时间:t
    • 横坐标:x
    • 纵坐标:y

    常量:

    • 横轴位移:offsetX
    • 纵轴位移:offsetY
    • 动画总时长:duration

    公式:

    • 横轴time-function:x=v*t
    • 纵轴time-function:y=1/2at^2

    先用常量算出速度v和a,再反向计算每一帧的x、y位置。 这是我们直接能想到的方式,先埋一个坑

    But: 这只是计算位置,还有缩放、透明度等等,有点…(这个需求做不了?)。还好tweenjs可以解放你的时间。

    2️⃣ tweenjs

    tweenjs是一个补间动画引擎。即设定动画的起止状态,也就是关键帧,而关键帧之间的过渡状态由tweenjs计算。也就是说tweenjs只进行计算,与小程序、H5等平台无关。

    简介

    tweenjs 2011年发布r1,最近更新时2020.10,一直在维护中,且业内有不错口碑。使用方便、api丰富、可以实现包括css animation在内的多种功能。如下简单例子:

    import TWEEN from '@tweenjs/tween.js';
    export default {
        data(){
            return {
                source: {x: 0, y: 0}
            }
        },
        mounted(){
            // 创建一个tween实例,起点是(0,0)
            var tween = new TWEEN.Tween(this.source);
            // 1s 内, x从0增加到200
            tween.to({x: 200},1000)
            // 启动计算
            tween.start();
            // 监听更新事件,
            tween.onUpdate(() => {
              // 设置css样式,or 绘制canvas样式
              const transform = `translate(${this.source.x}px, ${this.source.y}px)`;
              this.styleObject = {
                transform,
              };
            });
            // 启动动画
            this.animate()
        },
        methods: {
            animate() {
              window.requestAnimationFrame(this.animate);
              // 执行tween计算,可以传入time,如果未传入time则是当前时间
              TWEEN.update();
            },
        },
    }
    

    实例方法tween.xxx

    tween实例上还有哪些方法:

    • 状态方法:start(启动)、stop(停止)、pause(暂停)、resume(恢复)
    • 控制方法:to(目标)、chain(链式)、repeat(重复)、delay(延迟)、repeatDelay(延迟重复)、yoyo(反弹)、interpolation(补间数组)
    • 事件:onStart(启动)、onStop(停止)、onUpdate(暂停)、onComplete(完成)。

    动画组Group

    TWEEN类(默认动画组)上还有哪些方法:

    • getAll 获取所有tween实例、removeAll删除所有tween实例
    • add 添加tween实例、remove 删除tween实例、update 更新tween实例

    缓动函数Easing

    tween内置多种缓动函数,它们按表示的方程式类型分组:Linear(线性),Quadratic(二次方),Cubic(三次方),Quintic(四次方),Quintic(五次方),Sinusoidal(正弦波),Exponential(指数),Circular(圆形),Elastic(弹性),Back(后退)和Bounce(反弹),然后按缓动类型:In(进),out(出)和InOut(进出)。

    进阶用法

    • interpolation(数组插值),如下示意,easing负责计算全局进度(0-1),interpolation负责计算局部进度

    3️⃣ tweenjs源码浅析

    目录结构

    • src
      • Easing.ts # 缓动函数
      • Group.ts # 动画组
      • Index.ts # 对外导出TWEEN和多个方法
      • Now.ts # 获取当前时间函数
      • Sequence.ts # id生成器
      • Tween.ts # tween.js核心逻辑
      • Version.ts # 版本
      • mainGroup: # 默认动画组

    核心概念 & 逻辑

    时间轴Timing Line

    1. 很巧妙,使用时间计算进度,tween.start()标记开始时间。
    2. tween.update(time)计算当前进度,可以传入time进行时间穿梭。
    3. 也方便stop、pause、resume 操作。

    这里填坑,tweenjs使用时间轴来计算。

    使用时间计算当前进度 -> 进度缓动函数=实际进度 -> 实际进度(结束值-起始值)+起始值=实际值

    动画组Group

    1. 动画组(简称group)由多个Tween实例(简称tween)组成
    2. group.update()会遍历调用tween.update()
    3. tween.upate()会改变初始值(简称source)动画组可以通过共用source共享变动后的数据。

    源码浅析

    start方法

      // 开始方法
      start(time?: number): this {
        if (this._isPlaying) {
          return this
        }
    
        // 添加动画组
        this._group && this._group.add(this as any)
        // 重复次数
        this._repeat = this._initialRepeat
        // yoyo方法用到的,待补充
        if (this._reversed) {
          // If we were reversed (f.e. using the yoyo feature) then we need to
          // flip the tween direction back to forward.
    
          this._reversed = false
    
          for (const property in this._valuesStartRepeat) {
            this._swapEndStartRepeatValues(property)
            this._valuesStart[property] = this._valuesStartRepeat[property]
          }
        }
        // 运行状态true
        this._isPlaying = true
        // 暂停状态false
        this._isPaused = false
        // startCallback未触发
        this._onStartCallbackFired = false
        // 链式调用未停止
        this._isChainStopped = false
        // 标记开始时间,不传time使用当前时间,time字符串是当前时间+time,其他是time
        this._startTime = time !== undefined ? (typeof time === 'string' ? now() + parseFloat(time) : time) : now()
        // 开始时间 = 开始时间 + 延迟时间
        this._startTime += this._delayTime
        // 设置属性,source对象,起始值,结束值,
        this._setupProperties(this._object, this._valuesStart, this._valuesEnd, this._valuesStartRepeat)
    
        return this
      }
    

    start方法主要做了:

    1. 初始化状态参数_isPlaying、_isPaused、_onStartCallbackFired、_isChainStopped
    2. 设置内部属性
    3. 将new Tween(source)的source参数和Tween内部属性_valuesStart关联起来,达到动画组内的实例共享source的目的
    4. 设置_startTime

    _setupProperties方法

    private _setupProperties(
        _object: UnknownProps, // 起始值
        _valuesStart: UnknownProps, // 起始值
        _valuesEnd: UnknownProps, // 结束值
        _valuesStartRepeat: UnknownProps, // 起始值的拷贝,用来实现repeat和yoyo
      ): void {
        // 循环结束值的属性,假设起始值{x: 0, y: 0},结束值{y: 100},遍历到y
        for (const property in _valuesEnd) {
          // 起始值的某一属性的值 x=0
          const startValue = _object[property]
          // 起始值的某一属性的值是否是数组 false
          const startValueIsArray = Array.isArray(startValue)
          // 起始值的某一属性的值的类型 number
          const propType = startValueIsArray ? 'array' : typeof startValue
          // 是否是数组插值
          const isInterpolationList = !startValueIsArray && Array.isArray(_valuesEnd[property])
    
          // 如果to的属性在source中不存在,不设置
          if (propType === 'undefined' || propType === 'function') {
            continue
          }
          // 数组插值的处理
          if (isInterpolationList) {
            let endValues = _valuesEnd[property] as Array<number | string>
    
            if (endValues.length === 0) {
              continue
            }
            // 处理相对值
            endValues = endValues.map(this._handleRelativeValue.bind(this, startValue as number))
    
            // 数组插值将开始值查到结束值前面,方便后续计算。
            _valuesEnd[property] = [startValue].concat(endValues)
          }
    
          // 深层对象,递归设置。
          if ((propType === 'object' || startValueIsArray) && startValue && !isInterpolationList) {
            _valuesStart[property] = startValueIsArray ? [] : {}
    
            for (const prop in startValue as object) {
              _valuesStart[property][prop] = startValue[prop]
            }
    
            _valuesStartRepeat[property] = startValueIsArray ? [] : {} // TODO? repeat nested values? And yoyo? And array values?
    
           // 递归设置
            this._setupProperties(startValue, _valuesStart[property], _valuesEnd[property], _valuesStartRepeat[property])
          } else {
            // 简单数值,直接赋值
            if (typeof _valuesStart[property] === 'undefined') {
              _valuesStart[property] = startValue
            }
    
            if (!startValueIsArray) {
              _valuesStart[property] *= 1.0 // Ensures we're using numbers, not strings
            }
            // 初始化初始值拷贝
            if (isInterpolationList) {
              _valuesStartRepeat[property] = _valuesEnd[property].slice().reverse()
            } else {
              _valuesStartRepeat[property] = _valuesStart[property] || 0
            }
          }
        }
      }
    

    _setupProperties方法主要做了:

    1. (递归)赋值,并设置起始拷贝值_valueStartRepeat
    2. 对数组插值进行处理

    update方法

    update(time = now(), autoStart = true): boolean {
        if (this._isPaused) return true
    
        let property
        let elapsed
        // 结束时间
        const endTime = this._startTime + this._duration
        // 未到达终点 并且 未在运行
        if (!this._goToEnd && !this._isPlaying) {
          // time > 结束时间,返回false
          if (time > endTime) return false
          // 如果自动开始,则开始
          if (autoStart) this.start(time)
        }
        // 标记未结束
        this._goToEnd = false
        // time 小于 开始时间,返回true,
        if (time < this._startTime) {
          return true
        }
        // 开始事件未触发,则触发。
        if (this._onStartCallbackFired === false) {
          if (this._onStartCallback) {
            this._onStartCallback(this._object)
          }
          this._onStartCallbackFired = true
        }
        // 时间进度0-1
        elapsed = (time - this._startTime) / this._duration
        elapsed = this._duration === 0 || elapsed > 1 ? 1 : elapsed
        // 经过缓动函数计算后的值
        const value = this._easingFunction(elapsed)
        // 更新属性
        this._updateProperties(this._object, this._valuesStart, this._valuesEnd, value)
        // 更新事件触发
        if (this._onUpdateCallback) {
          this._onUpdateCallback(this._object, elapsed)
        }
        // 如果进度满了
        if (elapsed === 1) {
          // 重复次数大于1,减少次数
          if (this._repeat > 0) {
            if (isFinite(this._repeat)) {
              this._repeat--
            }
            // 重新设置开始值,重新开始,startTime = now
            for (property in this._valuesStartRepeat) {
              if (!this._yoyo && typeof this._valuesEnd[property] === 'string') {
                this._valuesStartRepeat[property] =
                  this._valuesStartRepeat[property] + parseFloat(this._valuesEnd[property])
              }
              if (this._yoyo) {
                // 交换end和startRepeat的值。
                this._swapEndStartRepeatValues(property)
              }
              // repeat,将备份startRepeat赋值回start
              this._valuesStart[property] = this._valuesStartRepeat[property]
            }
            // yoyo,反转
            if (this._yoyo) {
              this._reversed = !this._reversed
            }
            // 重置startTime并加上repeatDelayTime or delayTime
            if (this._repeatDelayTime !== undefined) {
              this._startTime = time + this._repeatDelayTime
            } else {
              this._startTime = time + this._delayTime
            }
            // 触发onRepeat事件
            if (this._onRepeatCallback) {
              this._onRepeatCallback(this._object)
            }
            return true
          } else {
            // 触发结束事件
            if (this._onCompleteCallback) {
              this._onCompleteCallback(this._object)
            }
            // 开始动画链的下一个tween的动画,并重新设置开始时间。
            for (let i = 0, numChainedTweens = this._chainedTweens.length; i < numChainedTweens; i++) {
              this._chainedTweens[i].start(this._startTime + this._duration)
            }
            this._isPlaying = false
            return false
          }
        }
    
        return true
      }
    

    update方法主要做了:

    1. 触发onStart事件
    2. 计算更新后的属性值
    3. 检查进度,如果满了
    4. 如果没有repeat,触发onComplete事件,检查是否有动画链,有则调用,结束当前动画
    5. 如果有repeat,处理repeat、delay、yoyo,使用_valuesRepeatStart作为中间值swap or reassign,并更新startTime

    4️⃣ 飞入动画实现

    1. 多个金币:小程序无法动态创建dom,v-for产生多个节点
    2. 起止位置:getBoudingClientRect获取金币栏位置,并用top判断是否下拉后飞入 or 直接飞入
    3. 补间动画:tweenjs拆分横向运动和纵向运动,循环设置延时,生成动画链。
    // 简单实现,忽略起止位置计算
    <template>
      <div>
        <div class="coin-bar" :style="coinBarStyle"></div>
        <div class="coin" v-for="n in coinNum" :key="n" :style="coinStyleList[n]"/>
      </div>
    </template>
    
    <script>
    import TWEEN from "@tweenjs/tween.js";
    export default {
      data() {
        return {
          coinNum: 5,
          source: {
            x: 300,
            y: 400,
            scale: 1,
            opacity: 1,
          },
          target: {
            x: 0,
            y: 0,
            scale: 0.4,
            opacity: 0.4,
          },
          coinStyleList: [],
          coinBarStyle: {},
        };
      },
      mounted() {
        this.init();
      },
      methods: {
        coinBar(source, target) {
          const tween = new TWEEN.Tween(source).to(target);
          tween.onUpdate((res) => {
            const transform = `translate(${res.x}px, ${res.y}px)`;
            this.coinBarStyle = {
              transform,
            };
          });
          return tween;
        },
        coinFly(n = 1, delay = 0) {
          const tween1 = new TWEEN.Tween(this.source)
            .to({ x: this.target.x }, 1000)
            .delay(delay);
          const tween2 = new TWEEN.Tween(this.source)
            .to(
              {
                y: this.target.y,
                scale: this.target.scale,
                opacity: this.target.opacity,
              },
              1000
            )
            .easing(TWEEN.Easing.Cubic.Out)
            .delay(delay);
          tween2.onUpdate((res) => {
            const transform = `translate(${res.x}px, ${res.y}px) scale(${res.scale})`;
            this.coinStyleList[n] = {
              transform,
              opacity: res.opacity,
            }
             
          });
          return [tween1, tween2];
        },
        init() {
          setTimeout(() => {
            // 金币栏降下
            const tween1 = this.coinBar({ x: 0, y: -30 }, { y: 0 });
            let tween2 = [];
            // 金币飞入
            for(var i = 1;i <= this.coinNum; i++){
                const tweens = this.coinFly(i, i*200);
                tween2 = [...tween2, ...tweens];
            }
            // 金币栏上升
            const tween3 = this.coinBar({ x: 0, y: 0 }, { y: -30 });
            // 生成动画链
            tween2[tween2.length-1].chain(tween3)
            const tween = tween1.chain(...tween2);
            tween.start();
          }, 1000);
    
          this.animate();
        },
        // 执行时机
        animate() {
          requestAnimationFrame(this.animate);
          TWEEN.update();
        },
      },
    };
    </script>
    
    <style>
    .coin {
      position: absolute;
      width: 50px;
      height: 50px;
      background: yellow;
      border-radius: 50%;
      transform: translate(300px, 400px);
    }
    .coin-bar {
      position: fixed;
      top: 0px;
      height: 30px;
      width: 200px;
      background: blue;
      transform: translate(0, -30px);
    }
    </style>
    

    5️⃣ 更多

    tweenjs还可以做什么?

    1. 配合threejs进行3d渲染
    2. 做数字增长动效
    3. ...

    6️⃣ 参考

    1. tweenjs github

    起源地下载网 » TweenJs浅析

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元