本篇文章将使用动效工厂从0-1去搭建并生成各种各样的粒子动画, 利用可视化编辑的能力,而你通过简易的操作就可以实现, 下面我就带你一步一步如何去实现一个粒子Action ,准确的来说去根据一个业务实现一个自定义的action。
读完本篇文章你可以了解到下面这几点:
- 如何实现一个自定义action
- 如何使用动效工厂
动效工厂
在实现自定义action 之前, 我先简单的介绍下我们这个项目的背景。什么是动效工厂?
我们团队用的动效场景比较多,所以沉淀了一些动效,同时为了下次在遇到可以更好的复用。于是我们能支持多少动效,或者有哪些动效可以被复用,就可以在动效工厂的动效库里去找。
这时候就会有同学说这些东西都不满足需求、不通用,于是就有了接下来要讲的的自定义action,也就是实现一个动效库没有的动画,然后再提交到动效库里,后面有类似的动效,别人也就可以复用了。
粒子动画
这里的话我带大家实现一个粒子动画, 首先还是解释什么是粒子动画????
粒子动画是由在一定范围内随机生成的大量粒子产生运动而组成的动画,被广泛运用于模拟天气系统、烟雾光效等方面。在电商平台的微型游戏化场景中,粒子动画主要 用于呈现在能量收集、金币收集时的特效。
这里我给大家准备了两段我们业务场景中使用的例子:
上面的粒子还是不够多,并且给粒子 贴上对应的贴图,再以某种动画的去展示,从而达到视觉上感觉非常爽的状态,就是有一种好多的感觉!
创建粒子类
其实无论什么样的粒子效果,你光看效果你都不知道如何分析这一个动画。其实分析动画,我们可以先看一个粒子是怎么变化的。我们看下面这张图:
粒子A在画布上,从A位置经过一定的动画 可能是线性变化, 也有可能是抛物线,或者是我们的三阶贝塞尔曲线变化。 但是万变不离其宗, 本质上都x 和 y 的变化, 但是x 从 x0 运行到 x1 , y方向 从y0 运动 y1, 那么其实对应当前这个一个粒子来说 也就是需要有vx 和vy 这两个属性。这样一个粒子最基本的属性 其实对于基本的动画已经满足了。 我们写下面这段代码:
class Particle {
constructor(x = 0, y = 0) {
this.x = x
this.y = x
this.radius = 1
this.vx = 0
this.vy = 0
this.color = 'hsla(197,100%,50%,80%)'
}
draw(ctx) {
ctx.save()
ctx.fillStyle = this.color
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
ctx.restore()
}
move() {
this.x += this.vx
this.y += this.vy
}
}
这样其实我们就能构造出当前粒子 所需要的特性。但是这时候我们还是需要一个粒子manager去管理所有的粒子,主要是用来
- 控制粒子的数量
- 生成多个类型的粒子
- 执行渲染操作
- 控制粒子的 x 方向 速度 和 y方向的速度 在哪一个区间
其实这些就是所谓的统一管理,代码就不展示了。其实上面所谓的这些操作 就是构成了粒子action 的一些配置
ACTION编写
我们打开到这个目录
这里面有我们动效工厂 所有的action,比如我这里其实就是新建一个 particle-action.ts
这里先看下 一个action最基本的结构是如何实现的。
就是actionConfig 这里的话 其实 就是对应到动效工厂 可视化视图的哪些属性,我们会有一些默认属性, 也就是每个action 固定的一些属性,但是同时也会有保持特殊的情况。然后你在导出一个Action 函数
我们看下这里是如何写的,这里其实都是固定写法。但是我还是想讲的明白点:
首先action肯定是作用在视图上的, 在动效工厂里其实就是 对应的View
然后就是一些缓动 动画, 的一些参数,我们看下代码
export const particleAction: Action = (
view: View,
{ timing, color, radius, duration, ...config },
) => {
let ctx: CanvasRenderingContext2D | null
return (time: number) => {
const progress = useProgress(time, timing, duration, config)
useOnce(() => {
ctx = view.makeCanvas().getContext('2d')
})
if (ctx) {
view.addLayer(ctx.canvas)
}
}
}
这里用到了 2个 独立的钩子
useOnce 和 useProgress
这里要不得不提了,为啥要用钩子, 最主要的目的是方便action在调用的时候和frame联动(这里借鉴了React hooks 的思想)
设置或者获取当前frame的duration, 我简单给大家介绍几个
useCache
缓存变量,保持action的执行过程
useState
保存和修改action的状态
useOnce
在Frame的整个生命周期中只会被执行一次。
useFinish
在Frame被移除的时候执行
useContext
获取frame的当前的wind。
useProgress
获取当前的进度是多少
useOnce
其实有点类似于单例,我们只创建一次,因为上面的函数是每一帧都进行了调用, 所以为了避免重复创建所存在的问题
我这里简单的说一下,这行代码
ctx = view.makeCanvas().getContext('2d')
其实view 只是我们抽象出来的东西,无论你做什么操作,也就是渲染画面, 都是依赖canvas。 所以view 上都makeCanvas 实例的方法,后面所有的动作,都在这个canvas 上去渲染, 从而形成动画。
2d 其实是一种最简单 也是最常用的方法, 但是canvas 不仅仅只有 2d 还有webgl 和 webgl2。
其实也就是说白了 我们这个东西后面扩展 3d 能力 是OK的 ,其实我也做了一些尝试, 但是由于业务场景 不是特别多,也是自己展示。
我这里可以给大家分享一下我用我们动效工厂 3D 做出的动画。
好了言归正传我们继续写我们的粒子action
我们看下这张图:
一个粒子在固定的view 移动,如果移动边界,其实也就是这时候速度该怎么变化呢,其实也就是所谓的碰撞检测
我这里其实就是有一个弹力系数, 当粒子运动到边界 也就是粒子的vx 方向 和 vy 方向 变成相反方向,如果没有开启碰撞检测的话, 其实就是去改变 粒子的位置, 设置粒子的 x 和 y 值 在对应的位置。看下面这张图
我这只是一个方向的。 代码如下:
// 碰撞检测
checkWall(isWallCollisionTest = true, bounce = 1, W = 1000, H = 1000) {
if (isWallCollisionTest) {
if (this.x + this.radius > W) {
this.x = W - this.radius;
this.vx *= -bounce;
} else if (this.x - this.radius < 0) {
this.x = this.radius;
this.vx *= -bounce;
}
if (this.y + this.radius > H) {
this.y = H - this.radius;
this.vy *= -bounce;
} else if (this.y - this.radius < 0) {
this.y = this.radius;
this.vy *= -bounce;
}
} else {
if (this.x - this.radius > W) {
this.x = 0;
} else if (this.x + this.radius < 0) {
this.x = W;
}
if (this.y - this.radius > H) {
this.y = 0;
} else if (this.y + this.radius < 0) {
this.y = H;
}
}
}
我们我设置100 的粒子 我们看下 碰撞检测的效果:
这里的卡顿是录制视频的时候卡顿和我们其他没关系的。canvas 粒子数量不够多的时候是不会卡顿的。
文字粒子
然后我们在当前的粒子上进行了扩充,扩充下文字粒子。这里其实简单说下,canvas整个画布其实就是像素点,我们可以实现原理其实很简单,Canvas中有个getImageData的方法,可以得到一个矩形范围所有像素点数据。那么我们就试试来获取一个文字的形状吧。
data属性返回一个 Uint8ClampedArray,它可以被使用作为查看初始像素数据。每个像素用4个1bytes值(按照红,绿,蓝和透明值的顺序; 这就是"RGBA"格式) 来代表。每个颜色值部份用0至255来代表。每个部份被分配到一个在数组内连续的索引,左上角像素的红色部份在数组的索引0位置。像素从左到右被处理,然后往下,遍历整个数组
我这里使用的画布大小是 1000 * 1000, 用坐标系来表示就是x轴1000,y轴1000
其实就是RGBA(255,255,255,0) 这四个类似的数字表示一个像素,那10001000这个画布用Uint8ClampedArray数组表示,总共由多少个元素呢? 就是1000 1000 * 4 个元素
第一步,用 measureText 的方法来计算出文字适当的尺寸和位置。
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d');
canvas.setAttribute('width', 1000);
canvas.setAttribute('height', 1000);
ctx.clearRect(0, 0, 1000, 1000)
ctx.font = 'bold 10p Arial'
const measure = ctx.measureText(this.text)
const size = 0.15
const fSize = Math.min(1000 * size * 10 / 7, 1000 * size * 10 / measure.width);
// 10像素字体行高 lineHeight=7 magic
ctx.font = `${fSize}px Arial`;
let left = (1000 - measure.width) / 2 - measure.width / 2;
const bottom = (1000 + fSize / 10 * 7) / 2;
ctx.fillText(this.text, left, bottom)
const imageData = ctx.getImageData(0, 0, 1000, 1000).data
for (let x = 0; x < 1000; x += 4) {
for (let y = 0; y < 1000; y += 4) {
const fontIndex = (x + y * 1000) * 4 + 3;
if (imageData[fontIndex] > 0) {
this.points.push({
x, y
})
}
}
}
其实这里 我们就获得像素, 获得文字所组成的像素点。然后我们就可以渲染了, 这里我们看下下面这个视频:
为什么用动效工厂??
这些你都不需要去重新写,你只要会用我们的动效工厂、简单配置就可以生成上面的粒子动画。
你可以在这个页面进行配置, 配置当前action对应的属性,然后这东西 和我们的dsl 有关系, 这个属性的命名一定要注意。 我会在后面文章进行一步一步剖析。
分析动画
上图文字粒子动画从动画的角度就是4个关键帧, 然后到一步就进行切换。 那其实对应到我们动效工厂其实就是的时间轴是这个样子的
我第一个关键帧对应的动画参数,以及持续的时间;然后第一帧结束完,播放第二幁。
那么这样对应的视图其实也就是 4个view ,因为视图和 action一一对应的, 或者是一对多的关系。
也就是根节点 我们的root 下面有4个view如图:
然后我们选择任意视图, 点击+ 号 选择 粒子弹框如下:
选择粒子,然后底下进度条就会出现、点击,就可以在动画属性编辑。
我们设置文字的参数,文字粒子的大小。
如图:
其实后面几个都是同样的操作,唯一不同的就是展示的文字不同,还有一个就是延迟action执行,第二个延迟1000秒第三个延迟2000以此类推。
当你进度条看下这个样子你就会明白了,一目了然
然后点击播放按钮就可以预览动画了:
HOW TO USE
这个其实非常简单,点击左下角的下载按钮,会生成一个ts文件
这个ts文件就是包括当前视图以及资源view的各种信息,不过你不需要考虑。
直接安装我们的sdk
yarn install @wind/core
然后调用
import { play } from '@wind/core'
play(animation(), {container: canvas 节点})
这样就可以使用了!
最后
大家有问题的可以在评论区交流哦!
文/Fly
关注得物技术,做最潮技术人!