移动端 canvas实现涂抹功能

508 阅读4分钟
前言

需要通过H5上实现一个涂抹消除的功能,包括放大镜,涂抹痕迹、放大缩小,平移等功能

实现

实现过程概述: 用到了四个图层,分别是原图图层、涂抹图层、导出涂抹的图层、放大镜图层,每个图层都是一个canvas。
将操作的图片放在canvas_1中,手指涂抹时,smearCanvas绘制出线条,绘制完成时,将涂抹层的图案复刻到canvas_2中并改变颜色导出。涂抹时,enlargeCanvas实时绘制手指区域的图案并放大。

涂抹和放大镜

let canvas_1 = document.createElement("canvas")  //原图图层
let canvas_layer_1 = canvas_1.getContext("2d")

let smearCanvas = document.createElement('canvas') //涂抹图层
let smearLayer = smearCanvas.getContext('2d')

let canvas_2 = document.createElement("canvas")  //导出涂抹的涂层
let canvas_layer_2 = canvas_2.getContext("2d")

let enlargeCanvas = document.getElementById('enlarge_canvas') //放大镜的图层
let enlargeCtx = enlargeCanvas.getContext('2d');

canvas布局,一个是主要操作区域,另一个是放大镜。

// 放大镜
<canvas ref={enlargeRef} style={{ marginTop: `${window.barHeight}px` }} id="enlarge_canvas" className={enlargePoi ? styles[`enlarge1`] : styles['enlarge0']}></canvas>

// 图片操作区
  <canvas
   id="canvas"
   className={styles['canvas']}
   style={{ width: `${canvasWrap.w}px`, height: `${canvasWrap.h}px`, transform: `scale(${scale}) translate(${translateData.x}px, ${translateData.y}px)` }}
   onTouchStart={debounce(touchStart, 80)}
   onTouchMove={touchMove}
   onTouchEnd={debounce(touchEnd, 80)}
  ></canvas>

初始化时计算图片宽高,并设置画布的尺寸

   //初始化处理
    const initCanvas = async () => {
        maxWidth = mainRef.current.clientWidth
        maxHeight = mainRef.current.clientHeight


        let image = new Image()
        image.src = imageUrl


        enlargeCanvas = document.getElementById('enlarge_canvas') //放大镜的图层
        enlargeCtx = enlargeCanvas.getContext('2d');

        console.log('enlargeRef', enlargeRef);
        console.log(canvasRef);



        image.onload = async () => {

            let w, h

            w = Math.round((image.width / image.height) * maxHeight)
            h = Math.round(maxWidth / (image.width / image.height))

            magicRadius = Math.round(image.width / 100) || 1 //计算魔法笔半径大小

            if (image.width < image.height) {
                magicRadius = Math.round(image.height / 100)
                // 长图
                if (w > maxWidth) {
                    w = maxWidth

                } else {
                    h = maxHeight
                }

            } else if (image.width === image.height) {
                // 等比例
                w = h = maxWidth < maxHeight ? maxWidth : maxHeight
            }
            else {
                // 宽图
                if (h > maxHeight) {
                    h = maxHeight
                } else {
                    w = maxWidth
                    h = Math.round(w / (image.width / image.height))
                }

            }
       
            setCanvasWrap({ w, h })


            smearCanvas.width = canvas.width = canvas_1.width = w * dpr
            smearCanvas.height = canvas.height = canvas_1.height = h * dpr

            canvas_2.width = image.width
            canvas_2.height = image.height


            enlargeCanvas.width = enlargeRef.current.clientWidth
            enlargeCanvas.height = enlargeRef.current.clientHeight

            magnifyRectangle.width = enlargeCanvas.width * dpr
            magnifyRectangle.height = enlargeCanvas.height * dpr
           

            canvas_layer_1.drawImage(image, 0, 0, canvas.width, canvas.height)

            ctx.drawImage(canvas_1, 0, 0)
            setLoading(false)
            setShowModal(false)
            setSmearCanvasInfo()
        }
        image.onerror = (e) => {
    
            Toast.show({ desc: 'failed' })
            setLoading(false)
            setShowModal(false)
            console.log('图片加载失败', imageUrl);
        }

    }

涂抹部分的实现

  // 涂鸦--选择区域
    function smearAction() {
        smearLayer.clearRect(0, 0, smearCanvas.width, smearCanvas.height)
        if (eventName === 'start') {
            smearLayer.beginPath()
            smearLayer.arc(magnifyRectangle.x, magnifyRectangle.y, Math.round((lineWidth * dpr) / scale / 2), 0, 2 * Math.PI, false);
            smearLayer.fill();
            // 在点击太快时 上面的不生效 所以这里处理点击事件
            console.log('smearAction是否点击', isTap);
            if (!isTap) {
                smearLayer.beginPath()
                smearLayer.moveTo(magnifyRectangle.x, magnifyRectangle.y)
            }
        } else if (eventName === 'move') {
            smearLayer.lineTo(magnifyRectangle.x, magnifyRectangle.y)
        } else {
            smearLayer.closePath()
        }
        !isTap && smearLayer.stroke()


        draw()
    }
    
     function draw() {

        enlargeCtx.clearRect(0, 0, enlargeCanvas.width, enlargeCanvas.height)
        canvas_layer_2.clearRect(0, 0, canvas_2.width, canvas_2.height)
        ctx.clearRect(0, 0, canvas.width, canvas.height)

        ctx.drawImage(smearCanvas, 0, 0)
        ctx.globalCompositeOperation = "destination-over";
        ctx.drawImage(canvas_1, 0, 0)
    }

放大镜部分的逻辑,获取手指对应在画布的位置,取手指区域的图案,绘制在放大镜图层中

   // 放大镜
    function drawMagnifyGlass(mouse) {

        magnifyRectangle.x = mouse.x;
        magnifyRectangle.y = mouse.y;
        // 涂鸦轨迹 
        smearAction()

        // 放大图像
        let mX = magnifyRectangle.x - magnifyRectangle.width / scale / 2;
        let mY = magnifyRectangle.y - magnifyRectangle.height / scale / 2;

        if (mX < 0) {
            mX = 0
        } else if (mX + magnifyRectangle.width / scale > canvas.width) {
            mX = canvas.width - magnifyRectangle.width / scale
        }

        if (mY < 0) {
            mY = 0
        } else if (mY + magnifyRectangle.height / scale > canvas.height) {
            mY = canvas.height - magnifyRectangle.height / scale
        }
        enlargeCtx.clearRect(0, 0, enlargeCanvas.width, enlargeCanvas.height)
        enlargeCtx.drawImage(canvas,
            mX, mY,
            magnifyRectangle.width / scale, magnifyRectangle.height / scale,
            0, 0,
            enlargeCanvas.width,
            enlargeCanvas.height
        )

        ctx.restore();
    }

用户涂抹操作的逻辑

// 开始涂抹
  const touchStart = (e) => {
        console.log('touchStart');
        eventName = 'start'
        let touchData = e.touches[0]
        let poiData = windowToCanvas({ x: touchData.clientX, y: touchData.clientY })

        if (e.touches.length === 1) {
            // 单手指画轨迹
            canDrawLine = true;

            if (!actionType) {
                movePoiForMagic = poiData
                return
            }
            enlargeCanvas.style.visibility = 'visible'
            drawMagnifyGlass(poiData)

        } else {
            canDrawLine = false
        }
    }

    // 
    const touchMove = (e) => {
        let touchData = e.touches[0]
        let poiData = windowToCanvas({ x: touchData.clientX, y: touchData.clientY })

        eventName = 'move'
        console.log('touchMove');
        if (e.touches.length < 2 && canDrawLine) {
            if (!actionType) {
                movePoiForMagic = poiData
                return
            }
            enlargeCanvas.style.visibility = 'visible'
            // 单手指画轨迹
            drawMagnifyGlass(poiData)
        } else {
            canDrawLine = false
        }
    }


    const touchEnd = (e) => {

        eventName = 'end'
        enlargeCanvas.style.visibility = 'hidden'
        setEnlargePoi(0)
        if (!isPinch && canDrawLine) {
            if (!actionType) {
                console.log(e, 'touchEnd');
                dealMagicMask()
                canDrawLine = false
                isTap = false
                return
            }
            // 导出涂鸦图层
            convertCanvasToImage(smearCanvas, smearLayer)

        }
        smearLayer.clearRect(0, 0, smearCanvas.width, smearCanvas.height)
        draw()
        canDrawLine = false
        isTap = false
    }

计算手与画布的对应位置

  // 计算手指位置和canvas位置
    function windowToCanvas({ x, y }) {
        let box = canvas.getBoundingClientRect();

        console.log(box, 'box');
        const minX = enlargeRef.current.offsetLeft
        const maxX = enlargeRef.current.offsetLeft + enlargeRef.current.offsetWidth
        const minY = enlargeRef.current.offsetTop
        const maxY = enlargeRef.current.offsetTop + enlargeRef.current.offsetHeight
        if ((minX < x - 10) && (x - 10 < maxX) && (minY < y - 10) && (y - 10 < maxY)) {
            // 如果碰到了放大框,放大镜的位置移到另一个角落
            let v = +(!enlargePoi)
            setEnlargePoi(v)
        }

        // 考虑放大缩小的因素,需要先除缩放的倍数
        return {
            x: ((x - box.left) / scaleNum) * dpr,
            y: ((y - box.top) / scaleNum) * dpr
        };
    }

缩放和平移

借助any-touch实现监听
参考github


    // 注册手势 --缩放--平移
    const initAtPan = () => {
        if (at) at.destroy();
        at = new AnyTouch(canvas);
        at.on(['tap'], e => {
            isTap = true
            enlargeCanvas.style.visibility = 'hidden'
            console.log(e, 'TAPclick点击');
        })
        at.on('pinch', (e) => {
            console.log('pinch');
            isDouble = true
            const { scale: currentScale, displacementX, displacementY, phase, isEnd } = e
            switch (phase) {
                case 'start':
                    enlargeCanvas.style.visibility = 'hidden'
                    break;
                case 'move':
                    if (Math.abs(currentScale - 1) >= 0.2) {
                        isPinch = true
                        currentChangeNum = (scaleNum * currentScale).toFixed(1)
                        if (currentChangeNum <= 0.5) {
                            // 缩小最小为0.5倍
                            currentChangeNum = 0.5
                        }
                        setTranslateData({ x: 0, y: 0 })
                        setScale(currentChangeNum)

                        smearLayer.lineWidth = Math.round((lineWidth * dpr) / currentChangeNum)
                    } else {

                        // 平移
                        let { width, height, } = canvas.getBoundingClientRect();
                        // 计算当前图片与容器的宽高差值
                        let diffW = width - maxWidth
                        let diffH = height - maxHeight
                        if (diffW > 0 || diffH > 0) {
                            //图片宽度(高度)大于容器宽度(高度),可以平移
                            let x = Math.round(displacementX / scaleNum)
                            let y = Math.round(displacementY / scaleNum)
                            x = x + currentTrans.x
                            y = y + currentTrans.y

                            setTranslateData({ x, y })
                        }
                    }
                    break;
                case 'end':
                    if (isPinch) {
                        scaleNum = currentChangeNum
                    } else {
                        let { left, top, width, height, } = canvas.getBoundingClientRect();

                        let x = displacementX
                        let y = displacementY

                        // 计算当前图片与容器的宽高差值
                        let diffW = width - maxWidth
                        let diffH = height - maxHeight
                        if (diffW > 0 || diffH > 0) {
                            //图片宽度(高度)大于容器宽度(高度),可以平移
                            if (x > 0) {
                                //x轴方向向右偏移时,left不能为负数
                                if (left > 0) x = (diffW / 2 - currentTrans.x * scaleNum) * (x / Math.abs(x))
                            } else {
                                //x轴方向向左偏移时,left+可视区域宽度<=width
                                if ((Math.abs(left) + maxWidth) > width) x = (diffW / 2 + currentTrans.x * scaleNum) * (x / Math.abs(x))
                            }
                            if (y > 0) {
                                // y轴方向向下偏移时,top不能为负数
                                if (top > 0) y = (diffH / 2 - currentTrans.y * scaleNum) * (y / Math.abs(y))
                            } else {
                                //y轴方向向上偏移时,top+可视区域高度<=height
                                if (Math.abs(top) + maxHeight > height) y = (diffH / 2 + currentTrans.y * scaleNum) * (y / Math.abs(y))
                            }
                            let getX = Math.round(x / scaleNum)
                            let getY = Math.round(y / scaleNum)
                            getX = currentTrans.x + getX
                            getY = currentTrans.y + getY

                            setTranslateData({ x: getX, y: getY })
                            currentTrans = { x: getX, y: getY }

                        }
                    }
                    isPinch = false
                    isDouble = false
                    isTap = false
                    enlargeCanvas.style.visibility = 'hidden'
                    console.log('pinchend', isEnd);
                    break;
            }
            // if (isEnd) isDouble = false


        })
    }