效果圖
話不多說,先看下效果
實現思路
考慮了一下這個功能,肯定得用 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=“”
/>
>
)
}
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 實現截圖功能——擷取圖片的一部分
加入我們
財經前端團隊是一支基礎技術與業務支援並重的團隊,既有紮實過硬的技術底盤,又服務於公司多個核心業務:抖音支付,金融,保險,證券等。目前財經前端團隊大部隊集中在北京,深圳、杭州也建立有研發中心,各地業務都在蓬勃發展中,團隊氛圍開放活潑,熱切期待志同道合的朋友加入我們!