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

😺 效果圖

話不多說,先看下效果

📒 實現思路

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

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

  3. 通過在【canvas A】上通過監聽 mouseup,mousemove,mousedown 三個事件計算截取的區域,生成截取動效,生成截取圖片等

  4. 截圖動作完畢的時候即時生成截取圖片數據返回

🤔 難點

  1. 計算截取區域

 // 獲取截圖開始的點
【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 事件上繪製被截取效果

  //第一步:遮罩層
  【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()
  1. 生成 & 獲得截取區域圖片

鼠標動作停止後就是截圖結束,所以需要在 moveup 事件生成截取圖片數據,在這裏可以通過 canvas 自帶的 canvas.toDataURL 把截圖轉化爲 base64,因爲通過 mousedownmousemove 我們已經獲取用戶的截取區域了,並且我們在截圖開始的時候,會把原圖片繪製到【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,

  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 }
}
  1. 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 (
    <>
      <div class ref={clipAreaWrap}>
        <img
          class
          src={require('../../assets/img/pet/cat-all.png')}
          alt=""
        />
      </div>
      <div class>
        <img src={clipImgData} alt="" id="img" />
      </div>
      <div class>
        <button
          onClick={() ={
            init(clipAreaWrap.current)
            cut(
              'https://cdn-tos.baohuaxia.com/obj/static-assets/433ed21f7f4a27a5bde94a8119d618c5.png'
            )
          }}
        >
          截圖
        </button>
        <button
          onClick={() ={
            cancelCut()
          }}
        >
          取消
        </button>
      </div>
    </>
  )
}

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 實現截圖功能——截取圖片的一部分 [1]

參考資料

[1]

canvas 實現截圖功能——截取圖片的一部分: https://blog.csdn.net/HuangsTing/article/details/106141263

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/oz2p0QoUQP0y2x3ys8oBJw