用canvas实现截图下载

193 阅读2分钟

msedge_sW7LJTZ3pM.gif

HTML

<div>
    <input type="file">
    <button class="confirm">放入选中截图</button>
    <button class="download">下载选中截图</button>
</div>
<canvas id="cvs"></canvas>

<div class="shot-img-area">
</div>

具体实现

首先初始化参数

const inp = document.querySelector('input'),
    shotImgArea = document.querySelector('.shot-img-area'), // 放入截图区域
    confirm = document.querySelector('.confirm'),
    download = document.querySelector('.download'),
    cvs = document.getElementById('cvs'),
    imgShot = new imgShotCvs(cvs)

// 配置
const MASK_OPACITY = .7

接下来实现这个ImgShowCvs

首先初始化参数

class imgShotCvs {
    constructor(cvs) {
        this.cvs = cvs
        this.ctx = cvs.getContext('2d', { willReadFrequently: true })

        this.initState()
        this.bindEvent()
    }

    initState() {
        this.stPos = []
        this.endPos = []

        // 拖动截图区域大小
        this.shotWidth = 0
        this.shotHeight = 0

        // 填充图片高度 也是`canvas`高度
        this.width = 0
        this.height = 0
        // 填充的图片
        this.img = null
        // 截图区域的图片数据
        this.shotImgData = []
        // 偏移值
        const { left, top } = this.cvs.getBoundingClientRect()
        this.left = left
        this.top = top
    }
}

接下来绑定点击事件

注意,在类里绑定事件,this指向是DOM元素,所以用箭头函数定义,做法同React的类组件

bindEvent() {
    this.cvs.addEventListener('mousedown', this.onMouseDown)
}

onMouseDown = (e) => {
    this.stPos = [e.pageX - this.left, e.pageY - this.top]

    this.cvs.addEventListener('mousemove', this.onMouseMove)
    this.cvs.addEventListener('mouseup', this.onMouseUp)
}

在鼠标点击时,记录起点,一定要减去偏移值

然后绑定移动和抬起事件

setImg(img) {
    this.img = img
    this.ctx.drawImage(img, 0, 0)
}

onMouseMove = (e) => {
    this.endPos = [e.pageX - this.left, e.pageY - this.top]
    const [stX, stY] = this.stPos,
        [endX, endY] = this.endPos

    // 记录 `终点 - 起点` 得到宽高
    this.shotWidth = endX - stX
    this.shotHeight = endY - stY

    this.clear()
    this.drawMask()
    this.drawSreenShot()
}

clear() {
    this.ctx.clearRect(0, 0, this.width, this.height)
}

drawMask() {
    this.ctx.fillStyle = `rgba(0, 0, 0, ${MASK_OPACITY})`
    this.ctx.fillRect(0, 0, this.width, this.height)
}

drawSreenShot() {
    // 擦除区域模式
    this.ctx.globalCompositeOperation = 'destination-out'
    // 从鼠标点击起点开始画 
    this.ctx.fillRect(...this.stPos, this.shotWidth, this.shotHeight)

    // 往擦除区域填充
    this.ctx.globalCompositeOperation = 'destination-over'
    this.setImg(this.img)
}

每次作画都要清除重绘,canvas是有GPU加速的,这点数据频繁清除无伤大雅

然后画个遮罩

清除画板后,使用destination-out模式,填充你选中的区域

再用destination-over模式,放入数据,这样就会把你选中的区域,变成图片,没有选中的区域,则是遮罩的黑色

onMouseUp = () => {
    this.setShotImgData()
    this.cvs.removeEventListener('mousemove', this.onMouseMove)
    this.cvs.removeEventListener('mouseup', this.onMouseUp)
}

setShotImgData() {
    if (this.shotWidth === 0 || this.shotHeight === 0) {
        return
    }
    this.shotImgData = this.ctx.getImageData(...this.stPos, this.shotWidth, this.shotHeight)
}

鼠标抬起时,解绑事件

然后把截图区域的图片,保存他的ImgData,里面是ArrayBuffer,每四位代表rgba

然后提供一个方法,用来获取新区域的canvas


getShotImg() {
    const cvs = document.createElement('canvas'),
        ctx = cvs.getContext('2d')

    cvs.width = this.shotWidth
    cvs.height = this.shotHeight
    ctx.putImageData(this.shotImgData, 0, 0)
    return cvs
}

这个类就实现了,接下来就是简单的处理input选择文件了

下载的话,只需要获取canvasBlob数据,转成url即可

 function bindEvent() {
    inp.onchange = async function () {
        const src = await getBase64(this.files[0]),
            img = await getImg(src),
            { width, height } = img

        imgShot.setSize(width, height)
        imgShot.setImg(img)
        imgShot.drawMask()
    }

    confirm.onclick = function () {
        shotImgArea.querySelector('canvas')?.remove()

        const cvs = imgShot.getShotImg()
        shotImgArea.appendChild(cvs)
    }

    download.onclick = function () {
        const cvs = imgShot.getShotImg()
        cvs.toBlob(blob => {
            const a = document.createElement('a'),
                url = URL.createObjectURL(blob)

            a.href = url
            a.download = Date.now()
            a.click()
        })
    }
}

function getBase64(blob) {
    const fr = new FileReader()
    fr.readAsDataURL(blob)

    return new Promise((resolve) => {
        fr.onload = function () {
            resolve(this.result)
        }
    })
}

function getImg(src) {
    const img = new Image()
    img.src = src
    return new Promise((resolve) => {
        img.onload = function () {
            resolve(img)
        }
    })
}

源码 gitee.com/cjl2385/dig…