Canvas实现点赞效果以及使用离屏画卡实现

Canvas实现点赞效果以及使用离屏画卡实现主要介绍Canvas运动路径、透明度的实现。主线程和Worker线程之间的通信和回调,以及注意事项。

image.png

本文会介绍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使用注意事项:

    1. 无法操作DOM,无法获取window, document, parent等对象
    2. 遵守同源限制, Worker线程的脚本文件,必须于主线程同源。并且加载脚本文件是阻塞的
    3. 不当的操作或者疏忽容易引起性能问题
    4. 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

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注