This article will introduce the implementation of Web Worker’s off-screen Canvas like and simple Canvas like effect.

Canvas implements likes

Create a Canvas

Create a canvas tag on the page element to initialize the canvas.

You can set the width and height properties on the canvas, and you can set the width and height properties in the style property.

  • The width and height of the style on the canvas are the height and width of the canvas rendered in the browser, i.e. the actual width and height in the page.
  • The width and height of the Canvas tag are the actual width and height of the canvas.
  <canvas ref={canvasNode} width="180" height="400"></canvas>
Copy the code

Image to load

Initialize the loading of the picture of sending flowers to obtain information about the picture

 /** * load image *@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',]// Continue all loaded image information
    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)
    })
    // Get all image information
    Promise.all(promiseAll)
        .then(lists= > {
            this.listImage = lists.filter((img: HTMLImageElement) = > img && img.width > 0)
        })
        .catch(err= > {
            console.error('Image load failed... ', err)
        })
}
Copy the code

Smooth moving position

If you want to smooth a curve, you can actually use the familiar math.sin function to achieve a uniform curve.

 /** * draw each like; Here we use a closure to initialize *@private
 * @returns {(Loop<number, boolean | void>)}
 * @memberof SendLove* /
private createRender(): Loop<number, boolean | void> {
    if (!this.listImage.length) return null
    // The following are the default values initialized at creation time
    const context = this.ctx
    // Randomly fetch the scale value
    const basicScale = [0.6.0.9.1.2] [this.getRandom(0.2)]
    // Select a random image
    const img = this.listImage[this.getRandom(0.this.listImage.length - 1)]
    const offset = 20
    // The position of the random animation X-axis is that the animations do not overlap
    const basicX = this.width / 2 + this.getRandom(-offset, offset)
    const angle = this.getRandom(2.12)
    // The X-axis offset is 10-30
    let ratio = this.getRandom(10.30) * (this.getRandom(0.1)?1 : -1)
    // Get the X-axis value
    const getTranslateX = (diffTime: number): number= > {
        if (diffTime < this.scaleTime) {
            return basicX
        } else {
            return basicX + ratio * Math.sin(angle * (diffTime - this.scaleTime))
        }
    }
    // Get the Y value
    const getTranslateY = (diffTime: number): number= > {
        return Number(img.height) / 2 + (this.height - Number(img.height) / 2) * (1 - diffTime)
    }
    // Scale method multiple creates a scale value for a flower
    const getScale = (diffTime: number): number= > {
        if (diffTime < this.scaleTime) {
            return Number((diffTime / this.scaleTime).toFixed(2)) * basicScale
        } else {
            return basicScale
        }
    }
    // Random start fade time,
    const fadeOutStage = this.getRandom(16.20) / 100
    / / transparency
    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()
    }
}
Copy the code

Real-time rendering

Turn on the real-time drawing scanner and put the created render object into the renderList array. The array is not empty, indicating that there are still animations on the canvas, so you need to keep performing scan until there are no animations on the canvas.

/** * scan *@private
 * @memberof SendLove* /
private scan() {
    // Clear the screen (clear the last drawing)
    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 after rescan; Recapture length
        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)) {
            // Animation finished, delete draw
            this.renderList.splice(index, 1)
            len--
        } else {
            index++
        }
    }
}
Copy the code

Provides external interface trigger animation

/** * provides an external like interface *@returns
 * @memberof SendLove* /
public likeStart() {
    // Initialize the gift data, callback function
    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
}
Copy the code

Web Worker implements off-screen Canvas likes

What is a Web Worker

Web workers allow Javascript to create a multithreaded environment, allowing the main thread to create Worker threads and assign tasks to run in the background. In this way, high-latency, intensive tasks can be carried by the Worker thread, and the main thread can handle UI interaction smoothly without blocking or slowing down

How to use Web Workers in projects

  • Introduce the worker-plugin in the Webpack
   const WorkerPlugin = require('worker-plugin')
   // Add to plugins
   new WorkerPlugin()
Copy the code
  • The main thread creates a Worker thread by calling the Worker() constructor with the new command
   const worker = new Worker('./like.worker', { type: 'module' })
Copy the code
  • The main thread and the Worker thread have communication restrictions and are not in the same context, so it can only be done through messages
worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas as OffscreenCanvas])
worker.onmessage = function (event) {console.log(event.data)}

Copy the code
  • Worker usage precautions:

    1. Can’t manipulate DOM, can’t get window, document, parent, etc
    2. Following the origin restriction, Worker thread scripts must have the same source as the main thread. And loading the script file is blocked
    3. Improper operation or negligence can cause performance problems
    4. PostMessage cannot pass functions

Initialize the

The first step is to determine whether the browser supports off-screen Canvas

const init = async() = > {// Many browsers are not compatible with offscreenCanvas. OffscreenCanvas can be used under Windows or Web workers, while Canvas can only be used under Windows
   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)
   }
}
Copy the code

About Worker image loading problem

There is no DOM manipulation in Worker, so new Image() will report an error; Used to load images

fetch(img)
.then(response= > response.blob())
.then(blob= > resolve(createImageBitmap(blob)))
Copy the code
// Initialize the image
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) = > {
            // It is used to process image data, and it is used to draw pictures off-screen
            fetch(img)
                .then(response= > response.blob())
                .then(blob= > resolve(createImageBitmap(blob)))
        })
        promiseAll.push(p)
    })
    // It's a bit slow here
    Promise.all(promiseAll)
        .then(lists= > {
            this.listImage = lists.filter((img: ImageData) = > img && img.width > 0)
        })
        .catch(err= > {
            console.error('Image load failed... ', err)
        })
}

Copy the code

The logic of liking effect is the same as Canvas processing

/ / to draw private createRender () : Loop (Boolean | void > {the if (! This.listimage.length) return null Const basicScale = [0.6, 0.9, 1.2][this.getrandom (0, Const img = this.listimage [this.getrandom (0, this.listimage.length-1)] const offset = 20 Const basicX = this.width / 2 + this.getrandom (-offset, offset) const Angle = this.getrandom (2, Let ratio = this.getrandom (10, 30) * (this.getrandom (0, 1))? Const getTranslateX = (diffTime: number): number => { if (diffTime < this.scaleTime) { return basicX } else { return basicX + ratio * Math.sin(angle * (diffTime - This.scaletime))}} const getTranslateY = (diffTime: number): Number => {return number (img.height) / 2 + (this.height - number (img.height) / 2) * (1-difftime)} // scale method multiple Create a scale value for a flower 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, 0) this.width, this.height) this.ctx.fillStyle = '#fff' this.ctx.fillRect(0, 0, 180, Let len = this.renderlist. length if (len > 0) { RequestFrame (this.scans.bind (this)) this.scanning = true} else {this.scanning = false} while (index < len) {requestFrame(this.scans.bind (this)) this.scanning = true} else {this.scanning = false} const curRender = this.renderList[index] if (! curRender || ! CurRender. Render | | curRender. Render. Call (null, (Date. Now () - curRender. Timestamp)/curRender duration)) {/ / animation has ended, Render this.renderList.splice(index, 1) len--} else {index++}}} public likeStart() {// initialize gift data, callback function 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 }Copy the code

The last

Both ways of rendering thumbs-up animations have been completed. The complete code

That’s the end of this article. I hope it helps.

Xiaobian for the first time to write an article writing style is limited, untalented and shallow, the article if there is wrong, hope to inform.