0️⃣ 背景
小程序中要实现物体飞入动画,效果如下: 复杂点:
- 曲线运动轨迹。
- 动画链。
1️⃣ 技术选型
css动画
CSS 动画(补间动画、关键帧动画)可以在不借助 Javascript 的情况下做出一些简单的动画效果。 你也可以通过 Javascript 控制 CSS 动画,使用少量的代码,就能让动画表现更加出色。
- Transition:transition: ;
1. 需要触发时机,使用css伪类选择器 or 动态设置css/style
2. 无法直接实现曲线运动轨迹,需要套娃。简单举例:父元素x轴匀速运动,子元素y轴变速运动。
3. 手动控制动画链,计算delay时间 or 使用sleep方式。
- Animation:animation:
1. 小程序下,计算动画起始点后,无法动态修改keyframe
2. 同transition第ii点。
3. 同transition第iii点。
js动画
JavaScript 动画(逐帧动画),每一帧都需要手动控制,可以处理 CSS 无法处理的事情。 例如,沿着具有与 Bezier 曲线不同的时序函数的复杂路径移动,或者实现画布上的动画。
实现js动画,我们需要关注:
- 渲染时机:setTimeout、setInterval、requestAnimationFrame(小程序❌,可以用polyfill)。
- 渲染方式: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
- 很巧妙,使用时间计算进度,
tween.start()
标记开始时间。 tween.update(time)
计算当前进度,可以传入time进行时间穿梭。- 也方便stop、pause、resume 操作。
这里填坑,tweenjs使用时间轴来计算。
使用时间计算当前进度 -> 进度缓动函数=实际进度 -> 实际进度(结束值-起始值)+起始值=实际值
动画组Group
- 动画组(简称group)由多个Tween实例(简称tween)组成
group.update()
会遍历调用tween.update()
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方法主要做了:
- 初始化状态参数_isPlaying、_isPaused、_onStartCallbackFired、_isChainStopped
- 设置内部属性
- 将new Tween(source)的source参数和Tween内部属性_valuesStart关联起来,达到动画组内的实例共享source的目的
- 设置_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方法主要做了:
- (递归)赋值,并设置起始拷贝值_valueStartRepeat
- 对数组插值进行处理
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方法主要做了:
- 触发onStart事件
- 计算更新后的属性值
- 检查进度,如果满了
- 如果没有repeat,触发onComplete事件,检查是否有动画链,有则调用,结束当前动画
- 如果有repeat,处理repeat、delay、yoyo,使用_valuesRepeatStart作为中间值swap or reassign,并更新startTime
4️⃣ 飞入动画实现
- 多个金币:小程序无法动态创建dom,v-for产生多个节点
- 起止位置:getBoudingClientRect获取金币栏位置,并用top判断是否下拉后飞入 or 直接飞入
- 补间动画: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还可以做什么?
- 配合threejs进行3d渲染
- 做数字增长动效
- ...
6️⃣ 参考
- tweenjs github
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!