效果圖

話不多說,先看下效果

使用canvas實現一個小小的截圖功能

實現思路

考慮了一下這個功能,肯定得用 hook,因為是一個有狀態的東西,hook 需要返回截圖,取消截圖的功能函式以及擷取圖片

在圖片所在的同一父元素節點下新增兩個 canvas,【canvas A】用於展示截圖動效(比如,未被擷取區域背景置灰,擷取區域顯示邊框);【canvas B】用於展示完整圖片,便於擷取動作進行以及生成截圖資料(記住 canvas A 和 canvas B,後面講解還會用到)

透過在【canvas A】上透過監聽

mouseup,mousemove,mousedown

三個事件計算擷取的區域,生成擷取動效,生成擷取圖片等

截圖動作完畢的時候即時生成擷取圖片資料返回

難點

1. 計算擷取區域

在截圖開始後,在【canvas A】的

mousedown

事件記錄起點座標 A, 透過

mousemove

事件實時監聽具體的座標,document 級的

mouseup

事件記錄結束座標 B(滑鼠有可能會跑出截圖區域外,所以是在 document 上監聽 mouseup),以 A 為起點,B 為終點,AB 兩點就能夠計算出擷取區域

// 獲取截圖開始的點

【canvas A】。onmousedown = function (e) {

記錄起點座標A

}

// 獲取滑鼠座標

【canvas A】。onmousemove = function (座標資料) {

1。 記錄滑鼠座標

2。 生成截圖區域動效

}

// 獲取截圖結束的點

document。addEventListener(‘mouseup’, function (e) {

1。記錄終點座標

2。 生成截圖()

}

2 截圖動畫效果(未被選取部分置灰,擷取部分新增邊框等)

mousedown

事件上把【canvas A】給置灰

// 設定截圖時灰色背景

【canvas A】。fillStyle = ‘rgba(0,0,0,0。6)’

【canvas A】。strokeStyle = ‘rgba(0,143,255,1)’

mouseup

事件上繪製被擷取效果

第一步:遮罩層:globalCompositeOperation = ‘source-over’ 表示在目標影象上顯示源影象,那麼此時進行

fillRect(0, 0, 【canvas A】。width, 【canvas A】。height)

那就是把我們之前設定好置灰樣式在目標圖片上層進行繪製,實現了第一步置灰效果

第二步:畫框:globalCompositeOperation = ‘destination-out’ 在源影象之外顯示目標影象。只有源影象之外的目標影象部分會被顯示,源影象是透明的。

【canvas A】。fillRect(x, y, w, h)

使得擷取內部透明

第三步:描邊:懂的都懂,不細講了

//第一步:遮罩層

【canvas A】。globalCompositeOperation = ‘source-over’

【canvas A】。fillRect(0, 0, 【canvas A】。width, 【canvas A】。height)

//第二步:畫框

【canvas A】。globalCompositeOperation = ‘destination-out’

【canvas A】。fillRect(x, y, w, h)

//第三步:描邊

【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()

3. 生成&獲得擷取區域圖片

滑鼠動作停止後就是截圖結束,所以需要在

moveup

事件生成擷取圖片資料,在這裡可以透過 canvas 自帶的 canvas。toDataURL 把截圖轉化為 base64,因為透過

mousedown

mousemove

我們已經獲取使用者的擷取區域了,並且我們在截圖開始的時候,會把原圖片繪製到【canvas B】中,所以我們可以直接在【canvas B】上對該區域進行擷取然後生成圖片~

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)

完整程式碼

我已經把截圖功能封裝成了一個 hook,有需要自取。還比較糙,有問題隨時反饋。

使用方法

這個 hook 會返回三個函式 init, cut, cancelCut,以及截圖資料 clipImgData,

init:在 init 函式把需要截圖區域的父元素傳進去

cut:開始截圖,需要把原圖片作為引數傳入

cancelCut:取消截圖功能

clipImgData:base64 格式的截圖資料

1. 截圖功能 hook

const clip = () => {

const clipAreaWrap = useRef(null) // 截圖區域dom

const clipCanvas = useRef(null) // 用於截圖的的canvas,以及截圖開始生成截圖效果(背景置灰)

const drawCanvas = useRef(null) // 把圖片繪製到canvas上方便 用於生成擷取圖片的base64資料

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)

}

// 截圖

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

// 設定截圖時灰色背景

clipCanvasCtx。fillStyle = ‘rgba(0,0,0,0。6)’

clipCanvasCtx。strokeStyle = ‘rgba(0,143,255,1)’

// 生成一個擷取區域的img 然後把它作為canvas的第一個引數

const clipImg = document。createElement(‘img’)

clipImg。classList。add(‘img_anonymous’)

clipImg。crossOrigin = ‘anonymous’

clipImg。src = souceImg

// Q: 這裡為什麼需要append到clipAreaWrap裡

// A: 因為直接clipImg。src的引入是沒有css樣式的(主要是寬高)如果不append直接進行drawCanvasCtx。drawImage,

// 那其實畫的是原始大小的clipImg

clipAreaWrap。current。appendChild(clipImg)

// 繪製截圖區域

clipImg。onload = () => {

// x,y -> 計算從drawCanvasCtx的的哪一x,y座標點進行繪製

const x = Math。floor((wrapWidth - clipImg。width) / 2)

const y = Math。floor((wrapHeight - clipImg。height) / 2)

// Q: 為什麼這裡要用克隆節點的寬高

// A: 因為clipImg的寬高是在dom中已經被css修改過的寬高(長/寬)了,而非該圖片的真實長和寬

// 用這個寬高在drawCanvasCtx的繪圖只會繪製clipImg的小部分內容(因為假寬高比真寬高小),看起來就像是被放大了

const clipImgCopy = clipImg。cloneNode()

drawCanvasCtx。drawImage(

clipImg,

0,

0,

clipImgCopy。width,

clipImgCopy。height,

x,

y,

clipImg。width,

clipImg。height

}

let start = null

// 獲取截圖開始的點

clipCanvas。current。onmousedown = function (e) {

start = {

x: e。offsetX,

y: e。offsetY

}

}

// 繪製截圖區域效果

clipCanvas。current。onmousemove = function (e) {

if (start) {

fill(

clipCanvasCtx,

wrapWidth,

wrapHeight,

start。x,

start。y,

e。offsetX - start。x,

e。offsetY - start。y

}

}

// 截圖完畢,獲取截圖圖片資料

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

//生成base64格式的圖

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

//移除滑鼠事件

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)

}

// 繪製出截圖的效果

const fill = (context, ctxWidth, ctxHeight, x, y, w, h) => {

context。clearRect(0, 0, ctxWidth, ctxHeight)

context。beginPath()

//遮罩層

context。globalCompositeOperation = ‘source-over’

context。fillRect(0, 0, ctxWidth, ctxHeight)

//畫框

context。globalCompositeOperation = ‘destination-out’

context。fillRect(x, y, w, h)

//描邊

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 }

}

2. html 部分

import React, { ReactElement, useEffect, useRef, useState } from ‘react’

import ‘。/index。less’

export default () => {

const clipAreaWrap = useRef(null) // 截圖區域dom

const { init, cut, cancelCut, clipImgData } = clip()

return (

<>

className=“clip-area-example”

alt="使用canvas實現一個小小的截圖功能" data-isLoading="0" src="/static/img/blank.gif" data-src={require(‘。。/。。/assets/img/pet/cat-all。png’)}

alt=“”

/>

使用canvas實現一個小小的截圖功能

onClick={() => {

init(clipAreaWrap。current)

cut(

‘https://cdn-tos。baohuaxia。com/obj/static-assets/433ed21f7f4a27a5bde94a8119d618c5。png’

}}

>

截圖

onClick={() => {

cancelCut()

}}

>

取消

}

3.CSS

。clip-area-wrap {

height: 450px;

position: relative;

//圖片居中顯示

img {

width: 100%;

display: block;

position: absolute;

top: 50%;

left: 50%;

transform: translate(-50%, -50%);

max-width: 100%;

max-height: 100%;

}

}

//回顯區域

。clip-img-area {

width: 250px;

height: 250px;

position: relative;

margin: 0 auto;

//圖片居中顯示

img {

position: absolute;

left: 50%;

top: 50%;

transform: translate(-50%, -50%);

max-width: 100%;

max-height: 100%;

}

}

後續想法

後續還想實現一些功能,比如:

自動把截圖放入剪下板

根據需要生成並返回不同格式的截圖

圖片過大時,進行圖片壓縮(canvas。toDataURL 可以實現)

……

文章會持續更新,敬請關注

參考

canvas 實現截圖功能——擷取圖片的一部分

加入我們

財經前端團隊是一支基礎技術與業務支援並重的團隊,既有紮實過硬的技術底盤,又服務於公司多個核心業務:抖音支付,金融,保險,證券等。目前財經前端團隊大部隊集中在北京,深圳、杭州也建立有研發中心,各地業務都在蓬勃發展中,團隊氛圍開放活潑,熱切期待志同道合的朋友加入我們!