The background,


According to product requirements, the following effects need to be achieved. (Because the GIF screenshot is not very clear, so can not record the complete mouse track)

On mobile devices, when the phone presses down on the screen and moves, it erases blurring to reveal a clear picture underneath

Second, the train of thought


As for image processing, the front end basically relies on Canvas. Let’s get right to it:

Analyzing the requirements, there are 3 steps in order to implement this feature

  • Set original figure A
  • Place a blurred original picture B on the basis of the original picture
  • Add logic to expose A after erasing B

You can even consider more extensive use of components, such as scratch-offs. So the component function can be abstracted to “erase the existing image on the Canvas”

Three, follow erase


First implement the step “A can be exposed after erasing B”

3.1 erase

Start with a simple erasing effect

<canvas style="border: 1px solid;" width="400" height="400"></canvas>
Copy the code
const canvasElm = document.getElementsByTagName('canvas') [0]
const ctx = canvasElm.getContext('2d')
// Canvas fill is full of blue
ctx.rect(0.0, canvasElm.clientWidth / 2, canvasElm.clientHeight / 2)
ctx.fillStyle = 'blue'
ctx.fill()

// Set a round brush
ctx.lineCap = ctx.lineJoin = 'round'
ctx.lineWidth = 50

// Center the canvas
const x = canvasElm.clientWidth / 2
const y = canvasElm.clientHeight / 2

// Draw a stroke and set the color to red
ctx.strokeStyle = 'red'
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(x, y)
ctx.stroke()
Copy the code

The effect is to draw a red circle on the graph. The desired effect is that the paintbrush is erased

Canvas has a magic property globalCompositeOperation

Every time a new stroke is drawn on the canvas, there are actually at most three areas of collision. Examples of the above pictures:

  • Pending region: The white part of the figure that is not filled with any images. Is equivalent todivThe background of other elements is transparent by default
  • Souce region: The blue part of the image, the part of the image that already exists before drawing
  • Destination region: the red part of the figure, the newly drawn part

The default attribute for globalCompositeOperation is source-over, which means that the Destination region overwrites the Souce region

There are actually more than 20 values, and the 12 commonly used effects are as follows:

Full value: developer.mozilla.org/zh-CN/docs/…

The erasing effect we want happens to be destination-out, so using the same code above, we can get the result we want by adding the following code before drawing a stroke

ctx.globalCompositeOperation = 'destination-out'
Copy the code

3.2 follow

Next we implement the follow effect. One wipe at a time can be broken down into three actions

  • Touchstart – Finger starts touching canvas
  • Touchmove – Finger moving
  • Touchend – Finger off canvas

3.2.1 reproduction

Set up the base map first

const canvasElm = document.getElementsByTagName('canvas') [0]
const ctx = canvasElm.getContext('2d')
// Canvas fill is full of blue
ctx.rect(0.0, canvasElm.clientWidth, canvasElm.clientHeight)
ctx.fillStyle = 'blue'
ctx.fill()

// Set a round brush
ctx.lineCap = ctx.lineJoin = 'round'
ctx.lineWidth = 100

ctx.globalCompositeOperation = 'destination-out'
Copy the code

3.2.2 tools

Several tool methods

// Get the canvas coordinate of the current touch
getClipArea(e) {
  let x = e.targetTouches[0].pageX;
  let y = e.targetTouches[0].pageY;
  let ndom = this.canvas;
  while(ndom && ndom.tagName ! = ='BODY') {
    x -= ndom.offsetLeft;
    y -= ndom.offsetTop;
    ndom = ndom.offsetParent;
  }
  return {
    x,
    y
  };
}
Copy the code
// Draw a line between two points
drawLine(pos,ctx) {
  const { x, y, xEnd, yEnd } = pos
  ctx.beginPath()
  ctx.moveTo(x, y)
  ctx.lineTo(xEnd || x, yEnd || y)
  ctx.stroke()
}
Copy the code

3.2.3 event

Since this is a Touch event, you need to switch the browser to mobile mode

/ / move the begin
canvasElm.ontouchstart = e= > {
  e.preventDefault()
  constpos = { ... getClipArea(e) } drawLine(pos, ctx)/ / move ing
  canvasElm.ontouchmove = e= > {
    e.preventDefault()
    const { x: xEnd, y: yEnd } = getClipArea(e)
    Object.assign(pos, { xEnd, yEnd })
    drawLine(pos, ctx)
    Object.assign(pos, { x: xEnd, y: yEnd })
  }

  / / move the end
  canvasElm.ontouchend = () = > {
    console.log('ontouched')}}Copy the code

4. Set the original image


Next we set the blue base to the image we want. After setting background-image in CSS, we usually set position, size and other properties. For ease of understanding, our default background image properties are as follows

canvas {
  background-size: cover;
  background-position: center;
}
Copy the code

Therefore, in order to completely coincide with background-image, the image filling on canvas needs to achieve the same effect

function fillImageWithCoverAndCenter(src) {
  const img = new Image()
  img.src = src
  img.onload = () = > {
    const imageAspectRatio = img.width / img.height
    const canvasAspectRatio = canvasElm.width / canvasElm.height
    // Use different rules depending on the size relationship between the image scale and canvas scale
    if (imageAspectRatio > canvasAspectRatio) {
      const imageWidth = canvasElm.height * imageAspectRatio
      ctx.drawImage(
      img,
      (canvasElm.width - imageWidth) / 2.0,
      imageWidth,
      canvasElm.height
      )
    } else {
      const imageHeight = canvasElm.width / imageAspectRatio
      ctx.drawImage(
      img,
      0,
      (canvasElm.height - imageHeight) / 2,
      canvasElm.width,
      imageHeight
      )
    }
    ctx.globalCompositeOperation = 'destination-out'
  }
}

fillImageWithCoverAndCenter('./girl.jpg')
Copy the code

5. Gaussian blur


I just need one last step. Before applying Gaussian blur to the image, we need to use the getImageData method to get all the pixel information.

The captured pixels are stored rGBA style

  • R – Red (0-255)
  • G – Green (0-255)
  • B – Blue (0-255)
  • A-alpha channel (0-255; 0 is transparent, 255 is fully visible)

And the information is completely tiled in a one-dimensional array: [0,0,0,255, 255,255,255,255, 255,255, 0,255,255…

The principle of Gaussian blur is not described here, which is basically that each pixel takes the average of the surrounding pixels.

The algorithm of gaussian blur – nguyen other www.ruanyifeng.com/blog/2012/1…

// This method must be executed in image.onload
// After filling the image, before setting the globalCompositeOperation
function gaussBlur() {
  // Get pixel information
  const imgData = ctx.getImageData(
    0.0,
    canvasElm.width,
    canvasElm.height
  )
  // Blur intensity
  const sigma = 10
  // Blur the radius
  const radius = 10
  
  const pixes = imgData.data
  const width = imgData.width
  const height = imgData.height
  
  const gaussMatrix = []
  const a = 1 / (Math.sqrt(2 * Math.PI) * sigma)
  const b = -1 / (2 * sigma * sigma)
  let gaussSum = 0
  // Generate a Gaussian matrix
  for (let i = 0, x = -radius; x <= radius; x++, i++) {
    const g = a * Math.exp(b * x * x)
    gaussMatrix[i] = g
    gaussSum += g
  }
  // normalize to ensure that the value of the gaussian matrix is between [0,1]
  for (let i = 0, len = gaussMatrix.length; i < len; i++) {
    gaussMatrix[i] /= gaussSum
  }
  const B_LIST_LENGTH = 3
  // One-dimensional Gaussian in the x direction
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const bList = new Array(B_LIST_LENGTH).fill(0)
      gaussSum = 0
      for (let j = -radius; j <= radius; j++) {
        const k = x + j
        if (k >= 0 && k < width) {
          // make sure k does not exceed x
          // r,g,b,a
          const i = (y * width + k) * 4
          for (let l = 0; l < bList.length; l++) {
            bList[l] += pixes[i + l] * gaussMatrix[j + radius]
          }
          gaussSum += gaussMatrix[j + radius]
        }
      }
      const i = (y * width + x) * 4
      // Divide by gaussSum to eliminate the problem of insufficient Gaussian computation for pixels at the edge
      for (let l = 0; l < bList.length; l++) {
        pixes[i + l] = bList[l] / gaussSum
      }
    }
  }
  
  // One-dimensional Gaussian operation in y direction
  for (let x = 0; x < width; x++) {
    for (let y = 0; y < height; y++) {
      const bList = new Array(B_LIST_LENGTH).fill(0)
      gaussSum = 0
      for (let j = -radius; j <= radius; j++) {
        const k = y + j
        if (k >= 0 && k < height) {
          // make sure k does not exceed y
          const i = (k * width + x) * 4
          for (let l = 0; l < bList.length; l++) {
            bList[l] += pixes[i + l] * gaussMatrix[j + radius]
          }
          gaussSum += gaussMatrix[j + radius]
        }
      }
      const i = (y * width + x) * 4
      for (let l = 0; l < bList.length; l++) {
        pixes[i + l] = bList[l] / gaussSum
      }
    }
  }
  
  // Fill the blurred image
  ctx.putImageData(imgData, 0.0)}Copy the code

Then set the background for the canvas

 canvas {
   background-image: url(./girl.jpg);
   background-size: cover;
   background-position: center;
}
Copy the code

Perfect ~

Sixth, perfect


This is just the simplest implementation. We may also need to calculate the scale of the image after each wipe (such as performing a specific method after a specified scale pixel is erased). You also need to expose various parameters to make Eraser a complete SDK

These are all relatively simple and will not be repeated here

Reference:


  • It realizes the erasers erased effect – www.cnblogs.com/axes/p/3850…
  • Based on the gaussian blur processing H5canvas and js – www.jianshu.com/p/e3142f0ed…