直播重要的功能之一就是送礼,礼物动画能增加直播间互动的趣味性,刺激互动与消费。此文探究一下直播网站中送礼动画的常用实现。
根据动画资源的不同,可以分为video、svga、apng、gif,也可以使用一些业界最新的动画资源例如pag等,不同的资源就有不同的实现方案。
VIDEO动画
动画可以看作是一种视频,用户送礼,播放一段动画视频,在网页对应的就能到<video>标签,但是如果直接使用video标签,就一定会有进度条、是否静音等的控制条出现,直接影响送礼的效果。因此web端是通过canvas, canvas的drawImage,可以绘制一个video对象,同时让video隐藏播放,播放的过程中逐帧drawImage到画布中。 逐帧绘制可以通过requestAnimationFrame来实现
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d')
let videoWidth, videoHeight;
function cb() {
ctx.clearRect(0,0, videoWidth / 2, videoHeight / 2) // 清空上一次的画布
ctx.drawImage(video, 0, 0, videoWidth, videoHeight, 0, 0, videoWidth / 2, videoHeight / 2); // 绘制当前的video元素
window.requestAnimationFrame(cb); // 调用下一次
}
video.addEventListener('play', function(e) {
videoWidth = e.target.videoWidth;
videoHeight = e.target.videoHeight;
canvas.width = videoWidth / 2; // 控制画布大小
canvas.height = videoHeight / 2; // 控制画布大小
window.requestAnimationFrame(cb)
});
提前下载video资源
video播放是边播放边下载的,如果播放视频卡顿,此时绘制到画布上的video也是卡顿的表现,因此需要提前下载好资源,可以通过blob的下载方式,将动画资源请求完整再播放。
let videoBlob = ''
fetch(url, {
responseType
}).then(res => {
if(responseType === 'blob') {
videoBlob = res.blob();
return res.blob(); // 将资源blob对象化
} else if(responseType === 'arraybuffer') {
return res.arrayBuffer();
}
return res;
});
// 设置video对象资源
this.videoElement.src = URL.createObjectURL(videoBlob);
this.videoElement.crossOrigin = "Anonymous";
透明视频动画
video是不带alpha通道的,具体表现就是一个视频如果有需要表现是透明的地方,渲染出来却是黑色的,而对于动画来说通常边缘都是透明的,或者是带有一层渐变,逐渐过渡到透明,因此还需要一个视频透明化的处理。
具体做法就是UI在产出动画资源的时候,会带有Alpha帧,对应说明哪些地方是有像素,哪些地方没有像素。
右边的图层,对应左边示意是否有像素的地方,有像素的地方是非黑色,无像素的地方就是黑色。 通过canvas的getImageData获取左边和右边两个图层的像素数据,将左边图层的数据塞入右边alpha值,再通过putImageData重新设置左边图层的数据,就可以实现透明背景。
// 左边像素数据
const image = canvas.getImageData(
0,
0,
width
height
);
const imageData = image.data;
const alphaImage = canvas.getImageData(
width,
0,
width
height
);
for (let i = 3, len = imageData.length; i < len; i = i + 4) {
imageData[i] = alphaData[i - 1];
}
canvas.clearRect(0, 0, width, height);
canvas.putImageData(image, 0, 0);
使用webgl
使用canvas会导致CPU占用高,webgl可以开启GPU渲染,CPU占用低,因此可以通过webgl来绘制动画视频。
在这里贴上一个大佬的实现,关键代码在于设置rgb值和alpha值混合 webgl 实现透明视频 动画
gl_FragColor = vec4(texture2D(u_sampler, v_texCoord).rgb, texture2D(u_sampler, v_texCoord+vec2(-0.5, 0)).r);
const textureVertice = new Float32Array([
0.5, 1.0,
1.0, 1.0,
0.5, 0.0,
1.0, 0.0
]); // 这里将纹理左半部分映射到整个画布上
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
SVGA动画
video动画是一种比较静态的动画,mp4是什么样子动画结果就是什么样子,而SVGA动画可以动态设置某些元素,比方用户头像之类的,可以做一些定制化处理。因此如果动画有需要动态设置的部分,就可以考虑使用SVGA格式。
SVGA动画可以通过svga.lite库来解析
import { Parser, Player } from 'svga.lite';
const canvas = document.createElement('canvas');
const parser = new Parser({ disableWorker }); // 创建解析器
const player = new Player(canvas); // 创建播放器
// 加载动画资源
const fileData = await this.loadData(url, {
responseType: 'arraybuffer',
});
// 解析器解析动画资源
const svgaData = await parser.do(fileData);
// 播放器装载动画资源
await player.mount(svgaData);
// 设置动态图像 一定要在开始播放前进行
player.setImage('http://yourserver.com/xxx.png', 'ImageKey');
// 设置动态文本 一定要在开始播放前进行
player.setText('Hello, World!', 'ImageKey');
// 开始播放动画
player.start();
PAG动画
PAG是腾讯推出的一套完整的动画工作流, 基于WebAssembly + WebGL。应用过程也非常简单,实例化PAG、加载素材为PAGFile对象,实例化PAGView对象,播放。
<canvas class="canvas" id="pag"></canvas>
<script src="https://cdn.jsdelivr.net/npm/libpag@latest/lib/libpag.min.js"></script>
<script>
window.onload = async () => {
// 实例化 PAG
const PAG = await window.libpag.PAGInit();
// 获取 PAG 素材数据
const buffer = await fetch('https://pag.art/file/like.pag').then((response) => response.arrayBuffer());
// 加载 PAG 素材为 PAGFile 对象
const pagFile = await PAG.PAGFile.load(buffer);
// 将画布尺寸设置为 PAGFile的尺寸
const canvas = document.getElementById('pag');
canvas.width = pagFile.width();
canvas.height = pagFile.height();
// 实例化 PAGView 对象
const pagView = await PAG.PAGView.init(pagFile, canvas);
// 播放 PAGView
await pagView.play();
};
</script>
APNG动画
APNG是基于PNG格式扩展的一种动画格式,与PNG相比它可以展示动态图像。与GIF相比它支持Alpha透明通道,而GIF不支持透明通道,边缘带有杂边。APNG在web中的使用,可以img直接设置apng格式的文件,此时不可以动态控制播放的时机。如果需要控制播放的时机,可以调用react-apng库。
import ApngComponent from 'react-apng';
const spinBoxRef = useRef();
function play() {
spinBoxRef.current?.one();
}
<ApngComponent
ref={spinBoxRef}
src={APNG_SOURCE.SPIN}
/>
图形动画
直播礼物动画还有一种特殊的动画就是图形动画,比方说送100朵玫瑰就能组成一个心形之类的,简述一下实现过程。每种图形都有一个表示坐标的一个json数据,每个坐标组合起来就是一个心形。画心的过程就是canvas画布drawImage的过程。结合requestAnimationFrame就可以实现一个动态画心的过程。
离屏渲染
离屏渲染是canvas动画的一种优化方式,具体描述就是不直接在主canvas上渲染,而是有一个副的canvas,在副的canvas上绘制,然后再在主canvas上画整个副canvas。尽可能减少在主canvas调用渲染相关 API 的次数。 相当于在屏幕渲染的时候开辟一个缓冲区,将当前需要加载的动画事先在缓冲区渲染完成之后,再显示到屏幕上. 当渲染的数量非常大时,可以使用这种方法来优化,如果数量少但是又要额外开辟渲染区,这就得不偿失。
在下面的例子中,通过离屏渲染,避开每一帧频繁的调用渲染相关 API 的次数
class OffScreen {
constructor() {
this.canvas = document.createElement('canvas');
this.r = 2.5;
this.width = this.canvas.width = 5;
this.height = this.canvas.height = 5;
this.ctx = this.canvas.getContext('2d');
this.x = this.width * Math.random();
this.y = this.height * Math.random();
this.create();
}
// 创建粒子
create() {
this.ctx.save();
this.ctx.fillStyle = 'rgba(255,255,255)';
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, 2.5, 0, 2 * Math.PI, false);
this.ctx.closePath();
this.ctx.fill();
this.ctx.restore();
}
// 绘制粒子
move(ctx, x, y) {
ctx.drawImage(this.canvas, x, y);
}
}
class DrawSnow {
constructor(count,useOffCanvas) {
......
this.useOffCanvas = useOffCanvas; // 是否使用离屏渲染
this.init();
}
init() {
let offScreen = '';
if (this.useOffCanvas) {
offScreen = new OffScreen();
}
for (let i = 0; i < this.count; i++) {
let x = this.width * Math.random();
let y = this.height * Math.random();
if (this.useOffCanvas) {
this.snowArray.push({
instance: offScreen,
x: x,
y: y
});
} else {
this.snowArray.push({
x: x,
y: y
});
this.draw(x, y);
}
}
this.animate();
}
animate() {
this.content.clearRect(0, 0, this.width, this.height);
for (let i in this.snowArray) {
let snow = this.snowArray[i];
snow.y += 2;
if (snow.y >= this.height + 10) {
snow.y = Math.random() * 50;
}
if (this.useOffCanvas) {
snow.instance.move(this.content, snow.x, snow.y);
} else {
this.draw(snow.x, snow.y);
}
}
this.timer = requestAnimationFrame(() => {
this.animate();
});
}
draw() {
this.content.save();
this.content.fillStyle = 'rgba(255,255,255)';
this.content.beginPath();
this.content.arc(this.x, this.y, 2.5, 0, 2 * Math.PI, false);
this.content.closePath();
this.content.fill();
this.content.restore();
}
}
3D动画
未完待续……