React Canvas 实现电子签名

74 阅读1分钟

提供撤销,清除,导出功能 图片格式为 png, 可自行调整

注意: canvas 宽高要设置 width,height。 设置在style里面是没有用的,会导致画图的的设备的移动倍率不正确,导致鼠标移动,实际上画出来的线偏移。偏离左上角越远,偏移越。

import React, { useImperativeHandle } from 'react'
import { memo, useEffect, useState } from 'react'

type TCoordinate = {
 x: number
 y: number
}
export default memo(
 React.forwardRef((props, ref) => {
   devicePixelRatio =  1
   const [id] = useState(`signature-${Math.random().toString(36).slice(-8)}`)
   const [thisCanvas, setThisCanvas] = useState<HTMLCanvasElement>()
   const [isDown, setIsDown] = useState(false)
   const [ctx, setCtx] = useState<CanvasRenderingContext2D>()

   const [ponits, setPonits] = useState<TCoordinate[][]>([[]] as TCoordinate[][])
   const [currentDrawIndex, setCurrentDrawIndex] = useState(0)
   const [lastPoint, setLastPoint] = useState<TCoordinate>({ x: 0, y: 0 })

   const draw = (
     ctx: CanvasRenderingContext2D,
     coordinate: { startX: number; startY: number; endX: number; endY: number },
   ) => {
     ctx.moveTo(coordinate.startX, coordinate.startY)
     ctx.lineTo(coordinate.endX, coordinate.endY)
     ctx.stroke()
     !ponits[currentDrawIndex] && (ponits[currentDrawIndex] = [])
     ponits[currentDrawIndex].push({ x: coordinate.endX, y: coordinate.endY })
   }

   const getCurrentCoordinate = (e: React.MouseEvent<HTMLCanvasElement, MouseEvent>) => {
     if (thisCanvas) {
       const canvasRect = thisCanvas.getBoundingClientRect()
       const x = e.clientX - canvasRect.left
       const y = e.clientY - canvasRect.top
       // 记录)*起点 x,y
       return { x, y }
     } else {
       return { x: 0, y: 0 }
     }
   }

   const mouseLeave = (e: React.MouseEvent<HTMLCanvasElement, MouseEvent>) => {
     if (isDown) {
       setCurrentDrawIndex(currentDrawIndex + 1)
       ctx?.closePath()
       setIsDown(false)
       setLastPoint({} as TCoordinate)
     }
   }

   const mouseDown = (e: React.MouseEvent<HTMLCanvasElement, MouseEvent>) => {
     setIsDown(true)
     ctx?.beginPath()
   }

   const mouseMove = (e: React.MouseEvent<HTMLCanvasElement, MouseEvent>) => {
     const coordinate = getCurrentCoordinate(e)

     if (ctx && isDown) {
       lastPoint.x &&
         draw(ctx, {
           endX: coordinate.x * devicePixelRatio,
           endY: coordinate.y * devicePixelRatio,
           startX: lastPoint.x * devicePixelRatio,
           startY: lastPoint.y * devicePixelRatio,
         })
       !ponits[currentDrawIndex] && (ponits[currentDrawIndex] = [])
       ponits[currentDrawIndex].push({ x: coordinate.x, y: coordinate.y })
       setPonits([...ponits])

       setLastPoint({ x: coordinate.x, y: coordinate.y })
     }
   }
   const mouseUp = (e: React.MouseEvent<HTMLCanvasElement, MouseEvent>) => {
     setIsDown(false)
     setLastPoint({} as TCoordinate)
     setCurrentDrawIndex(currentDrawIndex + 1)
     ctx?.closePath()
   }

   const undo = () => {
     if (ctx && thisCanvas) {
       ctx.clearRect(0, 0, thisCanvas.width, thisCanvas.height)
     }
     if (ponits.length) {
       currentDrawIndex !== 0 && setCurrentDrawIndex(currentDrawIndex - 1)

       const resultArr = ponits.slice(0, ponits.length - 1)

       resultArr.forEach((line) => {
         if (line?.length > 1 && ctx) {
           line.forEach((xy, index) => {
             ctx?.beginPath()
             if (line[index + 1]?.x) {
               draw(ctx, {
                 startX: xy.x,
                 startY: xy.y,
                 endX: line[index + 1].x,
                 endY: line[index + 1].y,
               })
             }
             if (index === line.length - 1) {
               ctx?.closePath()
             }
           })
         }
       })

       setLastPoint({} as TCoordinate)

       setPonits([...resultArr])
     }
   }

   const reset = () => {
     if (ctx && thisCanvas) {
       ctx.clearRect(0, 0, thisCanvas.width, thisCanvas.height)
     }
     setPonits([[]])
     setCurrentDrawIndex(0)
     setLastPoint({} as TCoordinate)
   }
   useEffect(() => {
     const c = document.getElementById(id) as HTMLCanvasElement
     c && setThisCanvas(c)
     if (c.getContext) {
       const ctx = c.getContext('2d') as CanvasRenderingContext2D
       setCtx(ctx)
       ctx.lineWidth = 2
       ctx.strokeStyle = '#000'
     }
   }, [])

   const toImage = () => {
     if (thisCanvas) {
       const image = thisCanvas.toDataURL('image/png')
       return image
       // let link = document.createElement('a')
       // document.body.appendChild(link)
       // link.download = 'picture.png'
       // link.href = image
       // // 触发点击
       // link.click()
       // // 移除元素
       // document.body.removeChild(link)
     }
   }

   useImperativeHandle(ref, () => ({
     undo: undo,
     reset: reset,
     getImage: toImage,
   }))
   return (
     <div>
       <div onClick={undo}>撤销</div>
       <div onClick={reset}>重写</div>
       <div onClick={toImage}>图片</div>
       <canvas
         style={{ border: '1px solid' }}
         id={id}
         width={`${500 * devicePixelRatio} px`}
         height={`${300 * devicePixelRatio} px`}
         onMouseLeave={mouseLeave}
         onMouseDown={mouseDown}
         onMouseUp={mouseUp}
         onMouseMove={mouseMove}
       >
         Your browser doesn't suppot this function, please use the last edition of browsers like:
         Chorme / Egde / Firefox
       </canvas>
     </div>
   )
 }),
)

修改记录: devicePixelRatio 不要乘个这个倍率,用了反而会有偏移,默认1 的就好