Using canvas to realize web gesture unlocking

Time:2021-4-16

Recently, I took part in the front-end Star project of 360 summer vacation. There is an online assignment. The deadline is March 30. Let me manually unlock an H5 gesture. The specific effect is just like that of the original mobile phone.

Using canvas to realize web gesture unlocking

The final effect of the implementation is as follows:

Using canvas to realize web gesture unlocking

The basic requirement is this: save the password tolocalStorageAt the beginning, the password will be read from the local. If not, the user will be asked to set the password. The password should be at least five digits. If it is less than five digits, an error will be prompted. You need to verify the password you entered for the first time, and you can keep it the same twice. Then you need to verify the password, which can verify the password you entered.

H5 gesture unlocking

Scan code to view online:

Using canvas to realize web gesture unlocking

Or click to viewMobile version

Project GitHub address,H5HandLock

First of all, I want to make it clear that for this project, I refer to others,H5lock

I think a more reasonable solution should be to use canvas to achieve, I don’t know if there is a big God using CSS to achieve. If you use CSS only, you can set the connection firstdisplay: noneIt’s shown when the fingers are passing. It should be very troublesome just to set these.

I’ve known about canvas before, but I haven’t really written about it. Now I’ll introduce the process of learning canvas and realizing H5 gesture unlocking in recent days.

Preparation and layout

I use a more conventional approach here:

(function(w){
  var handLock = function(option){}

  handLock.prototype = {
    init : function(){},
    ...
  }

  w.handLock = handLock;
})(window)

//Use
new handLock({
  el: document.getElementById('id'),
  ...
}).init();

The conventional method is easy to understand and operate, but the disadvantage is that it can be modified at will.

The passed in parameter should contain a DOM object, and a canvas will be created in the DOM object. Of course, there are some other DOM parameters, such as message, info, etc.

About CSS, I don’t bother to create a new file, so I directly inline it.

canvas

1. Learn canvas and draw circles

MDN above has a simple tutorial, roughly browse, feel OK.Canvas tutorial

Create one firstcanvas, and then set its size andgetContextMethods to get the context of painting

var canvas = document.createElement('canvas');
canvas.width = canvas.height = width;
this.el.appendChild(canvas);

this.ctx = canvas.getContext('2d');

Then, draw firstn*nTwo circles come out

createCircles: function(){
  var ctx = this.ctx,
    drawCircle = this.drawCircle,
    n = this.n;
  this.r =  ctx.canvas.width  /(2 + 4 * n) // here is a reference. I think this way of drawing circles is reasonable, square and round
  r = this.r;
  this.circles  =[]; // used to store the position of the center of a circle
  for(var i = 0; i < n; i++){
    for(var j = 0; j < n; j++){
      var p = {
        x: j * 4 * r + 3 * r,
        y: i * 4 * r + 3 * r,
        id: i * 3 + j
      }
      this.circles.push(p);
    }
  }
  ctx.clearRect (0, 0,  ctx.canvas.width ,  ctx.canvas.height ); // to prevent repetition
  this.circles.forEach(function(v){
    Drawcircle (CTX, v.x, v.y); // draw each circle
  })
},

Drawcircle: function (CTX, x, y) {// circle drawing function
  ctx.strokeStyle = '#FFFFFF';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.arc(x, y, this.r, 0, Math.PI * 2, true);
  ctx.closePath();
  ctx.stroke();
}

When drawing a circle function, we need to pay attention to: how to determine the radius of a circle and the coordinates of the center of each circle (which I refer to). If the center of the circle is taken as the midpoint, each circle will expand a radius distance from top to bottom, left to right, and at the same time, in order to prevent the four sides from being too crowded, a radius distance will be filled around. So the radius iswidth / ( 4 * n + 2)Corresponding to, you can also calculate the center coordinates of each circle. There is also a set of formulas,GET

2. Draw lines

Drawing lines needs to be done with the help of touch event, that is, when wetouchstartWhen we start, we pass in the relative coordinates as one end of the linetouchmoveWhen we get to the other end of the line, we get the coordinatestouchendWhen you’re ready, start drawing lines.

This is just a test line drawing function, which will be modified later.

There are two functions to obtain the relative coordinates of the current touch:

Gettouchpos: function (E) {// get the relative position of the touch point
  var rect = e.target.getBoundingClientRect();
  Var P = {// relative coordinates
    x: e.touches[0].clientX - rect.left,
    y: e.touches[0].clientY - rect.top
  };
  return p;
}

Draw line:

DrawLine: function (P1, P2) {// draw line
  this.ctx.beginPath();
  this.ctx.lineWidth = 3;
  this.ctx.moveTo(p1.x, p2.y);
  this.ctx.lineTo(p.x, p.y);
  this.ctx.stroke();
  this.ctx.closePath();
},

And then there’s cannvastouchstarttouchmove, andtouchendThe incident is over.

3. Draw a broken line

The so-called broken line drawing is to connect the touched points, which can be regarded as broken line drawing.

First of all, we need to use two arrays. One array is used to store the touched points, and the other array is used to store the untouched points. Then, during the move monitoring, we judge the relative position of the touch. If we touch the point, we move the point from the untouched point to the touch. Then, we draw a broken line. The idea is very simple.

DrawLine: function (P) {// draw a broken line
  this.ctx.beginPath();
  this.ctx.lineWidth = 3;
  this.ctx.moveTo(this.touchCircles[0].x, this.touchCircles[0].y);
  for (var i = 1 ; i < this.touchCircles.length ; i++) {
    this.ctx.lineTo(this.touchCircles[i].x, this.touchCircles[i].y);
  }
  this.ctx.lineTo(p.x, p.y);
  this.ctx.stroke();
  this.ctx.closePath();
},
Judgepos: function (P) {// judge whether the contact is in the circle
  for(var i = 0; i < this.restCircles.length; i++){
    temp = this.restCircles[i];
    if(Math.abs(p.x - temp.x) < r && Math.abs(p.y - temp.y) < r){
      this.touchCircles.push(temp);
      this.restCircles.splice(i, 1);
      this.touchFlag = true;
      break;
    }
  }
}

4. Mark painted

As mentioned earlier, we need to put the touched points (circles) into the array. At this time, we need to mark these touched points and draw a small solid circle at the center of the circle

drawPoints: function(){
  for (var i = 0 ; i < this.touchCircles.length ; i++) {
    this.ctx.fillStyle = '#FFFFFF';
    this.ctx.beginPath();
    this.ctx.arc(this.touchCircles[i].x, this.touchCircles[i].y, this.r / 2, 0, Math.PI * 2, true);
    this.ctx.closePath();
    this.ctx.fill();
  }
}

At the same time, add a reset function, call it when touching end, and call reset canvas 400 ms.

So far, a simple version of H5 gesture unlocking has been basically completed.

password

In order to remember and reset the password, save the password in localstorage, but first add the necessary HTML and style.

1. Add message and radio box

In order to make the interface as simple as possible (the uglier the better), we directly add the following after the body:

<div id="select">
  < div class = "message" > please input gesture password < / div >
  <div class="radio">
    < label > < input type = "radio" name = "pass" > set gesture password
    < label > < input type = "radio" name = "pass" > verify gesture password < / label >
  </div>
</div>

Pass the form added to DOM to handlock:

var el = document.getElementById('handlock'),
  info = el.getElementsByClassName('info')[0],
  select = document.getElementById('select'),
  message = select.getElementsByClassName('message')[0],
  radio = select.getElementsByClassName('radio')[0],
  setPass = radio.children[0].children[0],
  checkPass = radio.children[1].children[0];
new handLock({
  el: el,
  info: info,
  message: message,
  setPass: setPass,
  checkPass: checkPass,
  n: 3
}).init();

2. Info information display

About info information display, I wrote a floating window, and then default todisplay: noneAnd then I wrote oneshowInfoThe function is used to display prompt information

Showinfo: function (message, timer) {// is specially used to display info
  var info = this.dom.info;
  info.innerHTML = message;
  info.style.display = 'block';
  setTimeout(function(){
    info.style.display = '';
  }, 1000)
}

About the style of info, in HTML.

3. About password

Regardless of the situation of reading from localstorage, a new lspass object is added to store passwords. Because there are many password situations, such as setting password, secondary confirmation password and verifying password, three password modes are temporarily set for the convenience of management. They are:

Model: 1 verify password mode

Model: 2 set password mode

Model: 3 set password secondary verification

See the following figure for details:

Using canvas to realize web gesture unlocking

These three models, as long as we deal with how to jump between them, are OK, that is, the change of state.

So there’s initpass:

Initpass: function() {// initializes the password
  this.lsPass = w.localStorage.getItem('HandLockPass') ? {
    model: 1,
    pass: w.localStorage.getItem('HandLockPass').split('-')
  } : { model: 2 };
  this.updateMessage();
},

Updatemessage: function() {// updates DOM according to the current mode
  if(this.lsPass.model == 2){
    this.dom.setPass.checked = true;
    this.dom.message . innerHTML ='Please set gesture password ';
  }else if(this.lsPass.model == 1){
    this.dom.checkPass.checked = true;
    this.dom.message . innerHTML ='Please verify gesture password ';
  }else if(this.lsPass.model = 3){
    this.dom.setPass.checked = true;
    this.dom.message . innerHTML ='Please enter the password again ';
  }
},

It is necessary to introduce the format of lspass again

this.lsPass = {
  Model: 1, // indicates the current mode
  Pass: [0, 1, 2, 4, 5] // indicates the current password, which may not exist
}

Because there was a basic implementation framework before, now we just need to write a function after touchend. The function is to judge the current model first and implement the corresponding function. Here we need to use the touchcircles array to indicate the password order:

checkPass: function(){
  var succ, model =  this.lsPass.model ; // succ will be used later
  If (model = = 2) {// set password
    if( this.touchCircles.length  < 5) {// verify password length
      succ = false;
      this.showInfo ('password length is at least 5! ', 1000);
    }else{
      succ = true;
      this.lsPass.temp  =[]; // store the password in the temporary area
      for(var i = 0; i < this.touchCircles.length; i++){
        this.lsPass.temp.push(this.touchCircles[i].id);
      }
      this.lsPass.model = 3;
      this.showInfo ('Please enter the password again ', 1000));
      this.updateMessage();
    }
  }Else if (model = = 3) {// confirm password
    var flag = true;
    //First, verify that the password is correct
    if(this.touchCircles.length == this.lsPass.temp.length){
      var tc = this.touchCircles, lt = this.lsPass.temp;
      for(var i = 0; i < tc.length; i++){
        if(tc[i].id != lt[i]){
          flag = false;
        }
      }
    }else{
      flag = false;
    }
    if(!flag){
      succ = false;
      this.showInfo ('two passwords are inconsistent, please re-enter ', 1000);';
      this.lsPass.model  =2; // due to incorrect password, return to model 2
      this.updateMessage();
    }else{
      Succ = true; // the password is correct, localstorage stores it, and sets the status to model 1
      w. localStorage.setItem ('HandLockPass',  this.lsPass.temp . join ('-'); // store string
      this.lsPass.model = 1; 
      this.lsPass.pass = this.lsPass.temp;
      this.updateMessage();
    }
    delete  this.lsPass.temp ; // it's very important to delete the bug
  }Else if (model = = 1) {// verify password
    var tc = this.touchCircles, lp = this.lsPass.pass, flag = true;
    if(tc.length == lp.length){
      for(var i = 0; i < tc.length; i++){
        if(tc[i].id != lp[i]){
          flag = false;
        }
      }
    }else{
      flag = false;
    }
    if(!flag){
      succ = false;
      this.showInfo ('sorry, wrong password ', 1000));
    }else{
      succ = true;
      this.showInfo ('congratulations, verification passed ', 1000);
    }
  }
},

The password setting should refer to the previous figure, and be alert to the change of state at all times.

4. Reset the password manually

The idea is also very simple, that is, to add a click event. After clicking, you can change the model. The click event is as follows:

this.dom.setPass.addEventListener('click', function(e){
  self.lsPass.model  =2; // change the model to set the password
  self.updateMessage (); // update message
  self.showInfo ('Please set password ', 1000));
})
this.dom.checkPass.addEventListener('click', function(e){
  if(self.lsPass.pass){
    self.lsPass.model = 1;
    self.updateMessage();
    self.showInfo ('Please verify password ', 1000)
  }else{
    self.showInfo ('Please set password first ', 1000));
    self.updateMessage();
  }
})

PS: there are several small bugs in itBecause there are only three models, when you set it, when you click reset password, the password is not set successfully, and the password status is verified. At this time, the old password cannot be used. The reason is thatThere are only three models

5. Add touch end color change

The main function of this function is to give users a reminder. If the user’s password conforms to the specification, it will be displayed in green. If it does not conform to the specification or is wrong, it will be displayed in red.

This is because a succ variable has been set for redrawing.

Drawendcircles: function (color) {// end redraws the touched circle
  for(var i = 0; i < this.touchCircles.length; i++){
    this.drawCircle(this.touchCircles[i].x, this.touchCircles[i].y, color);
  }
},

//Call
if(succ){
  this.drawEndCircles ('#2cff26'); // Green
}else{
  this.drawEndCircles ('Red '); // red
}

Then, a demo version is generated, although there are still some bugs, which will be solved later. (branch password)

Some bugs

Some bugs were found when I was doing it. Some bugs were found only when I was testing with my mobile phone. For example, when I was using chrome, I didn’t notice this bug. When I was testing with Android Chrome browser, I found that when I touch move down, the browser’s pull-down refresh would be triggered. Solution: add onepreventDefaultI didn’t expect to succeed.

this.canvas.addEventListener('touchmove', function(e){
  e.preventDefault ? e.preventDefault() : null;
  var p = self.getTouchPos(e);
  if(self.touchFlag){
    self.update(p);
  }else{
    self.judgePos(p);
  }
}, false)

About showinfo

Because there is a setTimeout function in showinfo, we can see that the performance in the function is 1s. As a result, if we operate fast and show many info in 1s, the later info will be confused by the setTimeout of the first info, and the display time will be less than 1s or shorter. For example, when repeatedly clicking to set gesture password and verify gesture password, this bug will be generated.

There are two solutions. One is to add a special array for display. Each time you take a value from the array, it will be displayed. Another way to solve problems is similar to the idea of anti jitter, that is, when a new show comes, clear the previous setTimeout.

The second way of thinking is adopted here

Showinfo: function (message, timer) {// is specially used to display info
  clearTimeout(this.showInfo.timer);
  var info = this.dom.info;
  info.innerHTML = message;
  info.style.display = 'block';
  this.showInfo.timer = setTimeout(function(){
    info.style.display = '';
  }, timer || 1000)
},

Solve the small tail

The so-called small tail is as follows:

Using canvas to realize web gesture unlocking

The solution is also very simple. When you touch end, do it firstclearRectIt’s OK.

About optimization

Performance optimization is always a big problem. Don’t think that the front end can write code without considering memory.

Before in the design of their own web pages, the use of the scroll, a mouse pulley touch, scroll function on the implementation of more than a dozen hundreds of times, before also considered solutions.

Optimizing the canvas section

For the touchmove function, the principle is the same. As soon as you swipe your finger, it is executed n times. This problem will be solved later. Let’s look at another problem first.

Touchmove is a high-frequency function. If you don’t look at my code carefully, you may not know much about my canvas drawing method. Here’s what the touchmove function does:

  1. First judge that if a password is not selected, the current position will be monitored until the first password is selected, and then enter the second step;

  2. Enter the update function, which mainly does four things: redrawing circle (password), judging current position, redrawing point and redrawing line;

The second step is a very worrying action. Why do you have to redraw circles, points and lines every time?

Using canvas to realize web gesture unlocking

The above figure can illustrate the problem very well, because in the process of setting or verifying the password, we need to use a line to connect the contact to the current last password, and when touching move, we can see that they are changing. It’s a great function. It can outline the trajectory of touchmove.

However, it is necessary to refresh canvas all the time, and the performance is greatly reduced,It’s the whole canvas.

Because there is only one canvas, you need to draw not only the background circle (password), but also the selected password points and broken lines. Many of these steps only need to be done once from the beginning to the end. For example, the background circle only needs to be drawn once when it is started. The password has been selected. It will only be used once when a new element is added to touchcircles. There is no need to redraw it. Just draw it. The broken line is divided into two parts, one is the connection between the selected password, and the other is the connection between the last password point and the current contact.

It would be nice to have two canvass, one for static storage and one for redrawing

Why not!

My solution is that there are two canvass. One is at the bottom, which is used to describe static circles, points and broken lines. The other is at the top. On the one hand, it monitors the touchmove event, and on the other hand, it constantly redraws the line between the center of the last password point and the current contact. If so, the efficiency of the touchmove function can be greatly improved.

Insert the second canvas:

var canvas2 = canvas.cloneNode(canvas, true);
canvas2. style.position  ='absolute'; // let the upper canvas cover the lower canvas
canvas2.style.top = '0';
canvas2.style.left = '0';
this.el.appendChild(canvas2);
this.ctx2 = canvas2.getContext('2d');

To change the second ctx2 for touch monitoring, and set athis.reDrawParameter, indicating that a new password is added, and new contents need to be added to the point and polyline. The update function should be changed to this:

Update: function (P) {// update touchmove
  this.judgePos (p) ; // judge every time
  this.drawLine2TouchPos (p) ; // add a new function to draw the line from the center of the last password dot to the contact
  if( this.reDraw ){// a new password has been added
    this.reDraw = false;
    this.drawPoints (); // add new point
    this.drawLine (); // add new line
  }
},
drawLine2TouchPos: function(p){
  var len = this.touchCircles.length;
  if(len >= 1){
    this.ctx2 .clearRect(0, 0,  this.width ,  this.width ); // clear first
    this.ctx2.beginPath();
    this.ctx2.lineWidth = 3;
    this.ctx2.moveTo(this.touchCircles[len - 1].x, this.touchCircles[len - 1].y);
    this.ctx2.lineTo(p.x, p.y);
    this.ctx2.stroke();
    this.ctx2.closePath();
  }
},

The corresponding drawpoints and drawLine functions also need to be modified correspondingly. From the principle, we only need to draw all the new ones.

What’s the effect

Using canvas to realize web gesture unlocking

The move function is executed multiple times, while other functions are executed only once when a new password is added.

Add throttling function

As I have said before, the touchmove function is executed a lot. Although we have used two canvass to optimize the redrawing, touchmove still has a bit of overhead.

At this time, I think of anti dithering and throttling. First of all, anti dithering is definitely not good. In case I have been in touch state, redrawing will delay my death, and throttling will be better at this time.Anti shake and throttling

First write a throttling function

throttle: function(func, delay, mustRun){
  var timer, startTime = new Date(), self = this;
  return function(){
    var curTime = new Date(), args = arguments;
    clearTimeout(timer);
    if(curTime - startTime >= mustRun){
      startTime = curTime;
      func.apply(self, args);
    }else{
      timer = setTimeout(function(){
        func.apply(self, args);
      }, delay)
    }
  }
}

The meaning of throttling function: if the function is triggered again within the delay time, it will be timed again. This function is the same as anti jitter. The third parameter, mustrun, is a time interval, which means that a function can be executed immediately after the time interval is greater than mustrun.

Then, the callback function of touchmove is modified

var t = this.throttle(function(e){
  e.preventDefault ? e.preventDefault() : null;
  e.stopPropagation ? e.stopPropagation() : null;
  var p = this.getTouchPos(e);
  if(this.touchFlag){
    this.update(p);
  }else{
    this.judgePos(p);
  }
}, 16, 16)
this.canvas2.addEventListener('touchmove', t, false)

As for the time interval between delay and mustrun, there is a concept of 16ms in web performance. That is to say, if you want to reach 60 frames per second, the interval of 1000 / 60 is about 16ms. If the interval is greater than 16 ms, FPS will be lower than 60.

In view of this, we set delay and mustrun to 16. In extreme cases, that is, in the worst case, it may take 15 + 15 = 30ms to execute once. At this time, it is reasonable to set two 8s. However, considering that finger movement is a continuous process, how can it be executed every 15 seconds? After online testing, we find that it is set to 16 The effect is not bad.

Can performance really be optimized? Let’s take a look at two pictures. Do and wanddo represent real execution and queuing up for execution in throttling function.

When the speed of touchmove is normal or fast:

Using canvas to realize web gesture unlocking

When touchmove is slow:

Using canvas to realize web gesture unlocking

It can be seen that in the sliding process, the speed is average and fast, which is optimized by half on average, and the slow effect is also optimized by 20-30%. Usually, when the gesture lock is unlocked, it must be very fast. It can be seen that the optimization of throttling is very obvious.

The key is that the optimized process is not affected.

Finally, there is a bug in this throttling function: because of the delay in execution, thee.preventDefaultFailure, in the mobile browser to decline, there will be a refresh situation, which is also a harm of event delay.

Solution: cancel the default event in advance in the throttling function

throttle: function(func, delay, mustRun){
  var timer, startTime = new Date(), self = this;
  return function(e){
    if(e){
      e. Preventdefault? E.preventdefault(): null; // cancel the default event in advance, don't wait for setTimeout
      e.stopPropagation ? e.stopPropagation() : null;
    }
    ...
  }
}

summary

It took about three days to unlock the H5 gesture, and I was quite satisfied. Although I may not be recognized by the judges, I learned a lot of new knowledge in the process of doing it.

reference resources

H5lock
Canvas tutorial
JS gets the value in the radio box
Front end high performance scroll and page rendering optimization

WelcomeMy blogcommunication.

Recommended Today

Analysis of super comprehensive MySQL statement locking (Part 1)

A series of articles: Analysis of super comprehensive MySQL statement locking (Part 1) Analysis of super comprehensive MySQL statement locking (Part 2) Analysis of super comprehensive MySQL statement locking (Part 2) Preparation in advance Build a system to store heroes of the Three KingdomsheroTable: CREATE TABLE hero ( number INT, name VARCHAR(100), country varchar(100), PRIMARY […]