本文会介绍Web Worker实现离屏Canvas点赞和单纯Canvas实现点赞效果。
Canvas实现点赞
创建Canvas
页面元素上新建 canvas 标签,初始化 canvas。
canvas 上可以设置 width 和 height 属性,也可以在 style 属性里面设置 width 和 height。
- canvas 上 style 的 width 和 height 是 canvas 在浏览器中被渲染的高度和宽度,即在页面中的实际宽高。
- canvas 标签的 width 和 height 是画布实际宽度和高度。
<canvas ref={canvasNode} width="180" height="400"></canvas>
图片加载
初始化加载送花图片,获取图片相关信息
/** * 加载图片 * @private * @memberof SendLove */
private loadImage() {
const imgs = [
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
]
// 承接所有加载后的图片信息
const promiseAll: Array<Promise<HTMLImageElement>> = []
imgs.forEach((img: string) => {
const p = new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image()
image.src = img
image.crossOrigin = 'Anonymous'
image.onerror = image.onload = resolve.bind(null, image)
})
promiseAll.push(p)
})
// 获取所有图片信息
Promise.all(promiseAll)
.then(lists => {
this.listImage = lists.filter((img: HTMLImageElement) => img && img.width > 0)
})
.catch(err => {
console.error('图片加载失败...', err)
})
}
平滑移动位置
如果要做到平滑曲线,其实可以使用我们再熟悉不过的正弦( Math.sin )函数来实现均匀曲线。
/** * 绘制每一个点赞;这里使用了闭包,初始化 * @private * @returns {(Loop<number, boolean | void>)} * @memberof SendLove */
private createRender(): Loop<number, boolean | void> {
if (!this.listImage.length) return null
// 以下是在创建时,初始化默认值
const context = this.ctx
// 随机取出scale值
const basicScale = [0.6, 0.9, 1.2][this.getRandom(0, 2)]
//随机取一张图片
const img = this.listImage[this.getRandom(0, this.listImage.length - 1)]
const offset = 20
// 随机动画X轴的位置,是动画不重叠在一起
const basicX = this.width / 2 + this.getRandom(-offset, offset)
const angle = this.getRandom(2, 12)
// x轴偏移量10 - 30
let ratio = this.getRandom(10, 30) * (this.getRandom(0, 1) ? 1 : -1)
// 获取X轴值
const getTranslateX = (diffTime: number): number => {
if (diffTime < this.scaleTime) {
return basicX
} else {
return basicX + ratio * Math.sin(angle * (diffTime - this.scaleTime))
}
}
// 获取Y轴值
const getTranslateY = (diffTime: number): number => {
return Number(img.height) / 2 + (this.height - Number(img.height) / 2) * (1 - diffTime)
}
// scale方法倍数 针对一个鲜花创建一个scale值
const getScale = (diffTime: number): number => {
if (diffTime < this.scaleTime) {
return Number((diffTime / this.scaleTime).toFixed(2)) * basicScale
} else {
return basicScale
}
}
// 随机开始淡出时间,
const fadeOutStage = this.getRandom(16, 20) / 100
// 透明度
const getAlpha = (diffTime: number): number => {
const left = 1 - diffTime
if (left > fadeOutStage) {
return 1
} else {
return 1 - Number(((fadeOutStage - left) / fadeOutStage).toFixed(2))
}
}
return diffTime => {
if (diffTime >= 1) return true
const scale = getScale(diffTime)
context.save()
context.beginPath()
context.translate(getTranslateX(diffTime), getTranslateY(diffTime))
context.scale(scale, scale)
context.globalAlpha = getAlpha(diffTime)
context.drawImage(img, -img.width / 2, -img.height / 2, Number(img.width), Number(img.height))
context.restore()
}
}
实时绘制
开启实时绘制扫描器,将创建的渲染对象放入 renderList 数组,数组不为空,说明 canvas 上还有动画,就需要不停的去执行 scan,直到 canvas 上没有动画结束为止。
/** * 扫描 * @private * @memberof SendLove */
private scan() {
// 清屏(清除上一次绘制内容)
this.ctx.clearRect(0, 0, this.width, this.height)
this.ctx.fillStyle = '#fff'
this.ctx.fillRect(0, 0, 180, 400)
let index = 0
let len = this.renderList.length
if (len > 0) {
// 重新扫描后index= 0;重新获取长度
requestFrame(this.scan.bind(this))
this.scanning = true
} else {
this.scanning = false
}
while (index < len) {
const curRender = this.renderList[index]
if (!curRender || !curRender.render || curRender.render.call(null, (Date.now() - curRender.timestamp) / curRender.duration)) {
// 动画已结束,删除绘制
this.renderList.splice(index, 1)
len--
} else {
index++
}
}
}
提供对外的接口触发动画
/** * 提供对外的点赞的接口 * @returns * @memberof SendLove */
public likeStart() {
// 初始化礼物数据、回调函数
const render = this.createRender()
const duration = this.getRandom(1500, 3000)
this.renderList.push({
render,
duration,
timestamp: Date.now()
})
if (!this.scanning) {
this.scanning = true
requestFrame(this.scan.bind(this))
}
return this
}
Web Worker实现离屏Canvas点赞
什么是Web Worker
Web Worker允许Javascript创造多线程环境,允许主线程创建Worker线程,将任务分配在后台运行。这样高延迟,密集型的任务可以由Worker线程负担,主线程负责UI交互就会很流畅,不会会阻塞或拖慢
怎么在项目中使用Web Worker
- 在webpack 中引入worker-plugin
const WorkerPlugin = require('worker-plugin')
// 在plugins添加
new WorkerPlugin()
- 主线程使用new命令调用Worker()构造函数创建一个Worker线程
const worker = new Worker('./like.worker', { type: 'module' })
- 主线程与Worker线程存在通信限制,不再同一个上下文中,所以只能通过消息完成
worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas as OffscreenCanvas])
worker.onmessage = function (event) {console.log(event.data)}
-
Worker使用注意事项:
- 无法操作DOM,无法获取window, document, parent等对象
- 遵守同源限制, Worker线程的脚本文件,必须于主线程同源。并且加载脚本文件是阻塞的
- 不当的操作或者疏忽容易引起性能问题
- postMessage不能传递函数
初始化
首先要判断浏览器是否支持离屏Canvas
const init = async () => {
// offscreenCanvas离屏画卡很多浏览器不兼容, offscreenCanvas可以在window下可以使用也可以在web worker下使用, canvas只能在window下使用
if ('OffscreenCanvas' in window) {
const worker = new Worker('./like.worker', { type: 'module' })
const offscreenCanvas = canvasNode.current.transferControlToOffscreen()
worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas as OffscreenCanvas])
worker.addEventListener('error', error => {
console.log(error)
})
setNewWorker(worker)
} else {
const thumbsUpAni = new SendLove(canvasNode.current)
setCavasAni(thumbsUpAni)
}
}
关于Worker 图片加载问题
Worker中没办法操作DOM, 所以new Image()会报错;使用来加载图片
fetch(img)
.then(response => response.blob())
.then(blob => resolve(createImageBitmap(blob)))
// 初始化图片
private loadImage() {
const imgs = [
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
'https://img.qlchat.com/qlLive/activity/image/LCP31WOW-4IMP-NLAE-1620807553972-OYWXNLLNFNJI.png',
]
const promiseAll: Array<Promise<any>> = []
imgs.forEach((img: string) => {
const p = new Promise((resolve, reject) => {
// 用于处理图片数据,用于离屏画图
fetch(img)
.then(response => response.blob())
.then(blob => resolve(createImageBitmap(blob)))
})
promiseAll.push(p)
})
// 这里处理有点慢
Promise.all(promiseAll)
.then(lists => {
this.listImage = lists.filter((img: ImageData) => img && img.width > 0)
})
.catch(err => {
console.error('图片加载失败...', err)
})
}
点赞效果逻辑还是和Canvas处理一致
// 绘制
private createRender(): Loop<boolean | void> {
if (!this.listImage.length) return null
// 一下是在创建时,初始化默认值
const context = this.ctx
// 随机取出scale值
const basicScale = [0.6, 0.9, 1.2][this.getRandom(0, 2)]
//随机取一张图片
const img = this.listImage[this.getRandom(0, this.listImage.length - 1)]
const offset = 20
// 随机动画X轴的位置,是动画不重叠在一起
const basicX = this.width / 2 + this.getRandom(-offset, offset)
const angle = this.getRandom(2, 12)
// x轴偏移量10 - 30
let ratio = this.getRandom(10, 30) * (this.getRandom(0, 1) ? 1 : -1)
// 获取X轴值
const getTranslateX = (diffTime: number): number => {
if (diffTime < this.scaleTime) {
return basicX
} else {
return basicX + ratio * Math.sin(angle * (diffTime - this.scaleTime))
}
}
// 获取Y轴值
const getTranslateY = (diffTime: number): number => {
return Number(img.height) / 2 + (this.height - Number(img.height) / 2) * (1 - diffTime)
}
// scale方法倍数 针对一个鲜花创建一个scale值
const getScale = (diffTime: number): number => {
if (diffTime < this.scaleTime) {
return Number((diffTime / this.scaleTime).toFixed(2)) * basicScale
} else {
return basicScale
}
}
// 随机开始淡出时间,
const fadeOutStage = this.getRandom(16, 20) / 100
// 透明度
const getAlpha = (diffTime: number): number => {
const left = 1 - diffTime
if (left > fadeOutStage) {
return 1
} else {
return 1 - Number(((fadeOutStage - left) / fadeOutStage).toFixed(2))
}
}
return diffTime => {
if (diffTime >= 1) return true
const scale = getScale(diffTime)
context.save()
context.beginPath()
context.translate(getTranslateX(diffTime), getTranslateY(diffTime))
context.scale(scale, scale)
context.globalAlpha = getAlpha(diffTime)
context.drawImage(img, -img.width / 2, -img.height / 2, Number(img.width), Number(img.height))
context.restore()
}
}
// 扫描渲染列表
private scan() {
// 清屏(清除上一次绘制内容)
this.ctx.clearRect(0, 0, this.width, this.height)
this.ctx.fillStyle = '#fff'
this.ctx.fillRect(0, 0, 180, 400)
let index = 0
let len = this.renderList.length
if (len > 0) {
// 重新扫描后index= 0;重新获取长度
requestFrame(this.scan.bind(this))
this.scanning = true
} else {
this.scanning = false
}
while (index < len) {
const curRender = this.renderList[index]
if (!curRender || !curRender.render || curRender.render.call(null, (Date.now() - curRender.timestamp) / curRender.duration)) {
// 动画已结束,删除绘制
this.renderList.splice(index, 1)
len--
} else {
index++
}
}
}
// 点赞开始
public likeStart() {
// 初始化礼物数据、回调函数
const render = this.createRender()
const duration = this.getRandom(1500, 3000)
this.renderList.push({
render,
duration,
timestamp: Date.now()
})
if (!this.scanning) {
this.scanning = true
requestFrame(this.scan.bind(this))
}
return this
}
最后
两种方式渲染点赞动画都已经完成。完整代码
本文到此结束。希望对你有帮助。
小编第一次写文章文笔有限、才疏学浅,文中如有不正之处,万望告知。
今天的文章Canvas实现点赞效果以及使用离屏画卡实现分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/17959.html