preface

“You draw me guess” this game must be familiar to everyone, whether online or offline have played, just received a demand, need to use H5 to achieve a. Downloaded some apps, played, found that the experience is not very good, some synchronization is slow, some brushes are not smooth.

Spent some time, these problems have been optimized one by one, not to say, and share with you the technical solutions and implementation ideas.

The body of the

The article introduction

  • Basic function realization

  • Jagged edge of brush

  • Segmented transmission

  • Prevent brush loss

  • Brush fluency

  • Logical carding diagram

Basic function realization

Those of you who have the basis of the front end should know that the basic principle of H5 to achieve the drawing function is to listen to touch events: Touchstart, touchmove, touchend, get the xy coordinates of each touch point, use canvas API, connect these points to become our brushes. It should be noted that the XY obtained by listening for the event is based on the coordinate point at the upper left corner of the screen. To make the drawing point fall below your finger, subtract the XY distance from the canvas element itself, as follows:

// HTML <canvas ref="drawCanvas"></canvas> // js mounted(){ touchend this.$refs.drawCanvas.addEventListener('touchstart', this.canvasTouchstart) this.$refs.drawCanvas.addEventListener('touchmove', Enclosing canvasTouchmove) enclosing $refs. DrawCanvas. AddEventListener (' touchend, enclosing canvasTouchend) / / canvas high const wide {left, Top} = this. $refs. DrawCanvas. GetClientRects () [0] this. Cl = left this. Ct = top / / initializes the canvas this. CTX = This $refs. DrawCanvas. GetContext (' 2 d ')} the methods: {/ / canvasTouchstart (e) {/ / compatible with PC event attributes const touch = e. argetTouches ? E.t argetTouches[0] : e // touch.clientx, touch.clienty, touch point is xy at the upper left corner of the screen. // this.cl, this.ct, canvas element distance screen left, top const [x, y] = [touch.clientX - this.cl, touch.clientY - this.ct] this.ctx.beginPath() this.ctx.moveTo(x, Y)} // canvasTouchmove(e){const touch = e.argettouches? e.targetTouches[0] : e const [x, y] = [touch.clientX - this.cl, touch.clientY - this.ct] this.ctx.lineTo(x, Y) this.ctx.stroke() // canvasTouchend(){this.ctx.closepath ()}}Copy the code

Yes, the core code is so much, if you want to adapt to the PC, it’s not touch events, it’s mouse events.

this.$refs.drawCanvas.addEventListener('mousedown', this.canvasTouchstart)
document.addEventListener('mousemove', this.canvasTouchmove)
document.addEventListener('mouseup', this.canvasTouchend)
Copy the code

Note: when listening for mouse events, there is no property, e. gettouches, and you can directly read e, mousemove, mouseup events should listen to the document, not the canvas element, otherwise, the event listener will occur, the specific mechanism here will not be described.

Jagged edge of brush

The basic function is in place, but the strokes have serrations and burrs, how to optimize?

  1. Smooth transition between lineJoin and lineCap properties
The lineJoin property sets or returns the type of the corner created when two lines meet. The ctx.lineCap property sets or returns the style of the line end cap. LineJoin = 'round' this.cxx.linecap = 'round'Copy the code
  1. Increased canvas resolution

On the large screen of shopping mall, we can see pixel blocks one by one, but not on mobile phone, because the resolution of mobile phone is very high, canvas can also be imagined as a screen. When the resolution is too low, what you see is pixel blocks, which is one of the reasons for the sawtooth. You just need to increase the actual pixels of canvas.

<canvas :width="drawSize.w * 4" :height="drawSize.h * 4" style="`width:${drawSize.w}; height:${drawSize.h}; `" ref="drawCanvas"> </canvas>Copy the code

In other words, the width and height of Cancas we actually see are W, h. His actual resolution: W4, H4

Segmented transmission

  • The data transfer

How is the painting data transmitted. We need to record all xy points in the three listener events, package them into different drawing frames, and send them.

canvasTouchstart(){ // ... Other this.point=[] sendPoint({type:1, point:[{x,y}]})} canvasTouchmove(){//... This.point. Push ({x,y})} canvasTouchend(){//... Other // Send sendPoint({type:2, point:this.point}) // Send sendPoint({type:3})}Copy the code

Guess the word side only need to get the data after rendering, it seems to be no problem, but if the painting side does not let go of a stroke, even the stroke, then this stroke will never be sent out, synchronization to others, this is also one of the reasons for a lot of app experience is not good, how to solve it? Segment transmission.

  • Optimized piecewise transmission

Specific idea: In the process of moving, no matter whether the painting is finished or not, transmission should be made every once in a while. Here, 800ms is temporarily set. (The specific reason is 800ms, I want to facilitate the fluency of subsequent processing, depending on the situation) And the rest will be transmitted at the end. The code is as follows:

This.currenttime =new Date() // canvasTouchmove(){//... other this.point.push({x,y}) if(new Date() - this.currentTime >= 800){ sendPoint({ type:2, point:this.point }) this.point=[] } }Copy the code

This way, no matter how long it takes you to draw, others can receive the finished data as soon as possible.

Prevent brush loss

The network is unstable, you can’t guarantee that your every transaction will reach the server, and you can’t guarantee that the server will notify everyone, so how do you insure them?

Specific ideas:

Each stroke carries an increasing unique ID.

Drawing party: keep all sent frames locally first, send local data regularly and circularly, wait for the “frame reply” sent by the server, and delete the local frame.

The local ID is incremented. Each time a frame is received, it detects whether it is the next frame. If yes, then draw, if the ID is less than the next frame, then no processing; If it is larger than the next frame, it will temporarily save the frame and actively request the server to lose the frame (for example, when receiving the data with ID =2 and ID =5 and processing the data with ID =5, it will save the frame to the local first and then actively request the server for ID =3 and ID =4). Each time a frame is received, loop through the local array to see if there is another frame to draw.

The code is as follows:

SetInterval (()=>{this.drawdata.foreach (e=>{sendPoint(e)})},2000) // Ws.on(-321,e=>{this.drawData = this.drawdata.filter (v => V.ID! == e.id) }) } methods:{ canvasTouchstart(){ // ... SendPoint (startData) this.drawdata.push (startdata) this.id++} canvasTouchmove(){//... SendPoint (moveData) this.drawdata.push (movedata) this.id++} canvasTouchend(){//... Other // Send and save sendPoint(moveData) this. Drawdata. push(moveData) This.drawdata.push (enddata) this.id++}} this.id=1 // localid this.localdrawdata.push (enddata) this.drawData.push(enddata) this.id++}} Ws. On (- 320, e = > {e.f orEach (v = > {the if (this. Id + 1 = = = v.i d) {/ / draw the draw () this. Id = v.i d} the if (this. Id + 1 < v.i d) {/ / save the local, Localdrawdata.push (v) // Request the next ID, RequestLostDraw ({lostId:this.id+1, lostLen:v.id-(this.id+1)})}}) requestLostDraw({lostId:this.id+1, lostLen:v.i. Look to whether can draw this. LocalDrawData. ForEach (v = > {the if (this. Id + 1 = = = v.i d) {/ / draw the draw () this. Id = v.i d}})})}Copy the code

Brush fluency (emphasis)

So far, one of the improvements to the experience is that the guessers see the strokes shown segment by segment. As shown in the figure:

The question is, why?

Our drawing data is sent every 800ms, and no matter how many dots there are in this section, the guessing party will draw directly, and what the eye sees is that a line is directly generated.

So how do you make it silky?

As we all know, most of our phones refresh at 60hz (though there are also higher rates of 90hz and 120hz), with screens refreshing 60 images per second. 60hz is enough for us to see that the picture is smooth, as long as the stroke refresh rate at 60hz, we see that it is smooth. 1000ms/60=16.6ms, that is, you only need to render the stroke 16ms once.

Specific ideas:

  1. The data is divided into small segments

In order to arrive in time, the above section has been processed, 800ms once, shall we just change the time to 16ms to solve the problem? Yes and no. Problems can arise, with too much data being transferred to the server. Another method is to split the data after receiving it, and break the 800ms data into 16ms data, 50 pieces of data. The code is as follows:

// Scatter the returned data, For the small collection handlePartData (res) {if (res.point) {// process the brush moving point data if (res.cmdType === = 2) {const pointNum = res.poin.length / Const attr = {cmdType: res.cmdType, size: res.size, color: Res.color,} // const num = math.ceil (pointNum > 1); const pointArr = [] for (let i = 0, len = res.point.length; i < len; i++) { const v = res.point[i] if (! (i % num) && i ! == 0) { this.miniDrawList.push({ ... attr, point: [...pointArr], }) pointArr.length = 0 } pointArr.push({ x: v.x, y: v.y }) } if (pointArr.length) { this.miniDrawList.push({ ... Res.point. ForEach (v => {this.minidrawList.push ({... Attr, point: [{x: v.x, y: v.y}],})}} else {this.minidRawList.push (res)}}}, attr, point: [{x: v.x, y: v.y}],})}} else {Copy the code
  1. Render at 16ms

The data is already split, so I’m going to render it at 16ms, and I’m just going to render all the data in order. SetInterval? RequestAnimationFrame tells the browser that you want to perform an animation and requires the browser to call the specified callback function to update the animation before the next redraw. The code is as follows:

mounted(){ this.animationFrameFun() } methods:{ animationFrameFun () { this.drawLocalData() this.drawTimer = requestAnimationFrame(this.animationFrameFun) }, drawLocalData () { let drawStatus = false while (! drawStatus) { if (! This.minidrawlist.length) return const data = this.minidrawList [0] If (data.cmdtype === 2) {drawStatus = true} else {this.draw(this.minidrawList [0]) this.minidrawList.shift () if (data.cmdtype === 2) {drawStatus = true} else {this.draw(this.minidrawList [0]) this.minidrawList.shift () } } this.draw(this.miniDrawList[0]) this.miniDrawList.shift() }, }Copy the code

All right, that’s it, that’s it, that’s it, that’s it, that’s it, that’s it, that’s it, that’s it.

Logical carding diagram

In addition, the logical diagram of stroke processing is attached to help you understand.

  • Painting is a

  • Guess the word,

Note: Code snippets are pseudocode, intended to express the idea. If you want to use them directly, you have to consider reuse, encapsulation, and memory leaks

At the end

Well, this is the end of sharing, thank you for here, your watch is to my recognition.

Has ️ than heart has ️