Use canvas to realize a small screenshot function

Byte front end 2021-09-15 08:22:05

design sketch

Don't talk much , Let's see the effect first  Insert picture description here

Realize the idea

  1. Consider this function , It must work hook, Because it's a state thing ,hook You need to return a screenshot , Cancel the function of screenshots and capture pictures
  2. Add two... Under the same parent element node where the picture is located canvas,【canvas A】 It is used to show the dynamic effect of screenshots ( such as , The background of the area not intercepted is grayed out , The intercepted area displays a border );【canvas B】 Used to show the complete picture , It is convenient for intercepting action and generating screenshot data ( remember canvas A and canvas B, I'll use it later )
  3. By means of 【canvas A】 By listening mouseup,mousemove,mousedown Three events calculate the intercepted area , Generate interception action , Generate intercepted pictures, etc
  4. When the screenshot action is completed, the screenshot data is generated immediately and returned

difficulty

1. Calculate the interception area

After the screenshot starts , stay 【canvas A】 Of mousedown Coordinates of the starting point of the event record A, adopt mousemove The event monitors the specific coordinates in real time ,document Class mouseup Event record end coordinates B( The mouse may run out of the screenshot area , So it's in document On the monitor mouseup), With A As a starting point ,B End point ,AB Two points can calculate the interception area

 // Get the starting point of the screenshot
【canvas A】.onmousedown = function (e) {
Record the starting point coordinates A
}
// Get mouse coordinates
【canvas A】.onmousemove = function ( Coordinate data ) {
1. Record mouse coordinates
2. Generate screenshot area dynamic effect
}
// Get the end point of the screenshot
document.addEventListener('mouseup', function (e) {
1. Record end coordinates
2. Generate screenshots ()
}
 Copy code 

2 Screenshot animation effect ( The unselected part is grayed out , Cut part, add border, etc )

stay mousedown Put... In the event 【canvas A】 Give ash

 // Set the gray background for screenshots
【canvas A】.fillStyle = 'rgba(0,0,0,0.6)'
【canvas A】.strokeStyle = 'rgba(0,143,255,1)'
 Copy code 

stay mouseup Draw the intercepted effect on the event

  • First step : mask :globalCompositeOperation = 'source-over' Indicates that the source image is displayed on the target image , Then proceed to fillRect(0, 0, 【canvas A】.width, 【canvas A】.height) That is to draw the gray style we set before on the upper layer of the target image , The first step of ash setting effect is realized
  • The second step : Picture frame :globalCompositeOperation = 'destination-out' Display the target image outside the source image . Only the part of the target image other than the source image will be displayed , The source image is transparent .【canvas A】.fillRect(x, y, w, h) Make the intercepted interior transparent
  • The third step : Stroke : Know everything. , No more details
 // First step : mask
【canvas A】.globalCompositeOperation = 'source-over'
【canvas A】.fillRect(0, 0, 【canvas A】.width, 【canvas A】.height)
// The second step : Picture frame
【canvas A】.globalCompositeOperation = 'destination-out'
【canvas A】.fillRect(x, y, w, h)
// The third step : Stroke
【canvas A】.globalCompositeOperation = 'source-over'
【canvas A】.moveTo(x, y)
【canvas A】.lineTo(x + w, y)
【canvas A】.lineTo(x + w, y + h)
【canvas A】.lineTo(x, y + h)
【canvas A】.lineTo(x, y)
【canvas A】.stroke()
【canvas A】.closePath()
 Copy code 

3. Generate & Get a picture of the intercepted area

After the mouse action stops, the screenshot ends , So you need to be in moveup Event generation intercepts image data , Here you can go through canvas Self contained canvas.toDataURL Turn the screenshot into base64, Because by mousedown and mousemove We have obtained the interception area of the user , And at the beginning of the screenshot , Will draw the original picture to 【canvas B】 in , So we can go straight to 【canvas B】 The region is intercepted on the, and then a picture is generated ~

const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const data = 【canvas B】.getImageData(area.x, area.y, area.w, area.h)
canvas.width = area.w
canvas.height = area.h
context.putImageData(data, 0, 0)
return canvas.toDataURL('image/png', 1)
 Copy code 

Complete code

I have encapsulated the screenshot function into a hook, I need to take it myself . It's rough , Feel free to give feedback if you have any questions .

Usage method

This hook Will return three functions init, cut, cancelCut, And screenshot data clipImgData,

  • init: stay init The function passes in the parent element of the screenshot area
  • cut: Start screenshot , You need to pass the original image as a parameter
  • cancelCut: Cancel the screenshot function
  • clipImgData:base64 Screenshot data in format

1. Screenshot function hook

const clip = () => {
const clipAreaWrap = useRef(null) // Screenshot area dom
const clipCanvas = useRef(null) // For screenshots canvas, And the screenshot starts to generate the screenshot effect ( The background is grayed out )
const drawCanvas = useRef(null) // Draw the picture to canvas It's convenient to Used to generate intercepted pictures base64 data
const [clipImgData, setClipImgData] = useState('')
const init = (wrap) => {
if (!wrap) return
clipAreaWrap.current = wrap
clipCanvas.current = document.createElement('canvas')
drawCanvas.current = document.createElement('canvas')
clipCanvas.current.style =
'width:100%;height:100%;z-index: 2;position: absolute;left: 0;top: 0;'
drawCanvas.current.style =
'width:100%;height:100%;z-index: 1;position: absolute;left: 0;top: 0;'
clipAreaWrap.current.appendChild(clipCanvas.current)
clipAreaWrap.current.appendChild(drawCanvas.current)
}
// Screenshot
const cut = (souceImg: string) => {
const drawCanvasCtx = drawCanvas.current.getContext('2d')
const clipCanvasCtx = clipCanvas.current.getContext('2d')
const wrapWidth = clipAreaWrap.current.clientWidth
const wrapHeight = clipAreaWrap.current.clientHeight
clipCanvas.current.width = wrapWidth
clipCanvas.current.height = wrapHeight
drawCanvas.current.width = wrapWidth
drawCanvas.current.height = wrapHeight
// Set the gray background for screenshots
clipCanvasCtx.fillStyle = 'rgba(0,0,0,0.6)'
clipCanvasCtx.strokeStyle = 'rgba(0,143,255,1)'
// Generate an intercepted region img Then take it as canvas The first parameter of
const clipImg = document.createElement('img')
clipImg.classList.add('img_anonymous')
clipImg.crossOrigin = 'anonymous'
clipImg.src = souceImg
// Q: Why do we need append To clipAreaWrap in
// A: Because direct clipImg.src The introduction of is not css Styling ( Mainly width and height ) If you don't append Go straight ahead drawCanvasCtx.drawImage,
// It's actually the original size clipImg
clipAreaWrap.current.appendChild(clipImg)
// Draw a screenshot area
clipImg.onload = () => {
// x,y -> Calculate from drawCanvasCtx Which one of x,y Coordinate points for drawing
const x = Math.floor((wrapWidth - clipImg.width) / 2)
const y = Math.floor((wrapHeight - clipImg.height) / 2)
// Q: Why use the width and height of the clone node here
// A: because clipImg The width and height of is in dom Has been css Modified width and height ( Long / wide ) 了 , Instead of the actual length and width of the picture
// Use this width and height in drawCanvasCtx Your drawing will only draw clipImg A small part of ( Because the false width height is smaller than the true width height ), It looks like it's magnified
const clipImgCopy = clipImg.cloneNode()
drawCanvasCtx.drawImage(
clipImg,
0,
0,
clipImgCopy.width,
clipImgCopy.height,
x,
y,
clipImg.width,
clipImg.height
)
}
let start = null
// Get the starting point of the screenshot
clipCanvas.current.onmousedown = function (e) {
start = {
x: e.offsetX,
y: e.offsetY
}
}
// Draw screenshot area effect
clipCanvas.current.onmousemove = function (e) {
if (start) {
fill(
clipCanvasCtx,
wrapWidth,
wrapHeight,
start.x,
start.y,
e.offsetX - start.x,
e.offsetY - start.y
)
}
}
// The screenshot is over , Get screenshot image data
document.addEventListener('mouseup', function (e) {
if (start) {
var url = getClipPicUrl(
{
x: start.x,
y: start.y,
w: e.offsetX - start.x,
h: e.offsetY - start.y
},
drawCanvasCtx
)
start = null
// Generate base64 The format of the picture
setClipImgData(url)
}
})
}
const cancelCut = () => {
clipCanvas.current.width = clipAreaWrap.current.clientWidth
clipCanvas.current.height = clipAreaWrap.current.clientHeight
drawCanvas.current.width = clipAreaWrap.current.clientWidth
drawCanvas.current.height = clipAreaWrap.current.clientHeight
const drawCanvasCtx = drawCanvas.current.getContext('2d')
const clipCanvasCtx = clipCanvas.current.getContext('2d')
drawCanvasCtx.clearRect(
0,
0,
drawCanvas.current.clientWidth,
drawCanvas.current.clientHeight
)
clipCanvasCtx.clearRect(
0,
0,
clipCanvas.current.clientWidth,
clipCanvas.current.clientHeight
)
// Remove mouse events
clipCanvas.current.onmousedown = null
clipCanvas.current.onmousemove = null
}
const getClipPicUrl = (area, drawCanvasCtx) => {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const data = drawCanvasCtx.getImageData(area.x, area.y, area.w, area.h)
canvas.width = area.w
canvas.height = area.h
context.putImageData(data, 0, 0)
return canvas.toDataURL('image/png', 1)
}
// Draw the effect of screenshot
const fill = (context, ctxWidth, ctxHeight, x, y, w, h) => {
context.clearRect(0, 0, ctxWidth, ctxHeight)
context.beginPath()
// mask
context.globalCompositeOperation = 'source-over'
context.fillRect(0, 0, ctxWidth, ctxHeight)
// Picture frame
context.globalCompositeOperation = 'destination-out'
context.fillRect(x, y, w, h)
// Stroke
context.globalCompositeOperation = 'source-over'
context.moveTo(x, y)
context.lineTo(x + w, y)
context.lineTo(x + w, y + h)
context.lineTo(x, y + h)
context.lineTo(x, y)
// context.stroke()
context.closePath()
}
return { init, cut, cancelCut, clipImgData }
}
 Copy code 

2. html part

import React, { ReactElement, useEffect, useRef, useState } from 'react'
import './index.less'
export default () => {
const clipAreaWrap = useRef(null) // Screenshot area dom
const { init, cut, cancelCut, clipImgData } = clip()
return (
<>
<div className="clip-area-wrap" ref={clipAreaWrap}>
<img
className="clip-area-example"
src={require('../../assets/img/pet/cat-all.png')}
alt=""
/>
</div>
<div className="clip-img-area">
<img src={clipImgData} alt="" id="img" />
</div>
<div className="operation">
<button
onClick={() => {
init(clipAreaWrap.current)
cut(
'https://cdn-tos.baohuaxia.com/obj/static-assets/433ed21f7f4a27a5bde94a8119d618c5.png'
)
}}
>
Screenshot
</button>
<button
onClick={() => {
cancelCut()
}}
>
Cancel
</button>
</div>
</>
)
}
 Copy code 

3.CSS

.clip-area-wrap {
height: 450px;
position: relative;
// Picture center
img {
width: 100%;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 100%;
max-height: 100%;
}
}
// Echo area
.clip-img-area {
width: 250px;
height: 250px;
position: relative;
margin: 0 auto;
// Picture center
img {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-width: 100%;
max-height: 100%;
}
}
 Copy code 

Follow up ideas

Later, I want to realize some functions , such as :

  • Automatically put the screenshot into the clipboard
  • Generate and return screenshots in different formats as needed
  • When the picture is too large , Do image compression (canvas.toDataURL Can achieve )
  • ……

The article will be updated continuously , Stay tuned

Reference resources

canvas Realize the screenshot function —— Capture part of the picture

Join us

The financial front-end team is a team that pays equal attention to basic technology and business support , Both solid and excellent technical chassis , It also serves many core businesses of the company : Tiktok payment , Finance , insurance , Securities, etc . At present, a large number of financial front-end teams are concentrated in Beijing , Shenzhen 、 Hangzhou also has an R & D Center , Business is booming everywhere , The team atmosphere is open and lively , We sincerely look forward to like-minded friends joining us !

Please bring the original link to reprint ,thank
Similar articles

2021-09-15