前端图像处理之马赛克

726 阅读3分钟

开门见山

接了个需求,图片处理新增马赛克,可以撤销上一步与还原撤销

项目地址可拉下来直接用

技术需求

JScanvas

设计思路

  • canvas画布绘制图片,保证图片比例不变,并且画布不能有留白
  • 鼠标在画布上移动时,获取鼠标位置
  • 获取鼠标定位范围size(每个马赛克大小)的像素平均值
  • 将该区域所有像素点的rgba设为刚刚所求的平均值
  • 循环2-4步,绘制前需要判断当前绘制点与上一次绘制点是否相离太近
  • 导出原尺寸大小的马赛克图片url
  • 撤销:保存每一次打马赛克后的图片数据
  • 恢复:缓存撤销数据

代码实现

定义一个函数,接受容器ID和canvasID

//html
<div id="content"><canvas id="myCanvas" width="1200" height="0"></canvas></div>

//js
function CreateMosaic(contentId, canvasId){
    //所有代码均在此函数内部
}

定义初始化函数,返回所需变量

// 初始化参数
    const init = () =>{
        const content = document.getElementById(contentId)
        const canvas = document.getElementById(canvasId);
        const painting = canvas.getContext('2d');
        const size = 20 // 每次马赛克范围,像素
        let lastP = [0, 0] // 最后一次绘制中心点
        let isPrint = false // 绘制启动, true表示可绘制,false表示不可绘制
        let brushFollow = false // 画笔启动
        let brush = null // 画笔
        let handleData = [] // 每次操作步骤
        let cacheDeleteData = [] // 缓存撤销数据用于恢复
        let imgWH = [] // 图片原始宽高
        return { content, canvas, painting, size, lastP, isPrint, brushFollow, brush, handleData, cacheDeleteData, imgWH }
    }
let { content, canvas, painting, size, lastP, isPrint, brushFollow, brush, handleData, cacheDeleteData, imgWH } = init()

图片按canvas宽度等比缩放,并设置canvas最后的高与图片缩放后的高度一致

// 计算出图片等比缩放的宽高
const calculate = (cw, pw, ph) => {
    ph = ph * ( cw / pw )
    pw = cw
    return {pw, ph}
}

const img = new Image();
    img.src = '图片地址'
    img.crossOrigin = '';//跨域
    img.onload = () => {
        const {pw, ph} = calculate(canvas.width, img.width, img.height)
        imgWH = [img.width, img.height]
        painting.drawImage(img, 0, 0, pw, ph);
        canvas.height = ph
        handleData.push(painting.getImageData(0, 0, canvas.width, canvas.height))
    }

定义求鼠标定位范围size(每个马赛克大小)的像素平均值

// 获取范围内像素均值
    const getPxAVG = (imgDate) => {
        const data = imgDate.data
        let r = 0, g = 0, b = 0, a = 0
        const sumPx = data.length / 4
        for (let i = 0; i < sumPx; i ++){
            r = r + data[i * 4]
            g = g + data[i * 4 + 1]
            b = b + data[i * 4 + 2]
            a = a + data[i * 4 + 3]
        }
        return [r / sumPx, g / sumPx, b / sumPx, a / sumPx]
    }

设置某个区域所有像素点的rgba值

// 设置范围内像素值
const setPxColor = (imgDate, color) => {
    for (let i = 0; i < imgDate.data.length; i ++){
        imgDate.data[i] = color[i % 4]
    }
    return imgDate
}

定义绘制马赛克函数

// 绘制马赛克
    const printMosaic = (x, y) => {
        // 获取鼠标定位范围size的像素数据
        const imgData = painting.getImageData(x, y, size, size)
        // 获取范围像素数据平均值
        const pxAVG = getPxAVG(imgData)
        // 创建该区域马赛克图像数据
        let mosaic = setPxColor(painting.createImageData(size, size), pxAVG)
        // 重新绘制
        painting.putImageData(mosaic, x, y)
    }

定义onmousedown,onmousemove,onmouseup函数

// 判断是否超过上一次马赛克范围
    const judgeD = (currentP, lastP, r) => {
        const [x1, y1] = currentP, [x2, y2] = lastP
        return Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + Math.pow(Math.abs(y1 - y2), 2)) <= r
    }
    canvas.onmousedown = e => {
        if (!brushFollow) return false
        isPrint = true
    }
    canvas.onmousemove = e => {
        const { offsetX, offsetY } = e
        if (!brushFollow) return false
        brush.style.transform = `translate(${offsetX - size / 2}px, ${offsetY - size / 2}px)`
        if (!isPrint) return false
        if (!judgeD([offsetX, offsetY], lastP, size / 2)){
            lastP = [offsetX, offsetY]
            printMosaic(offsetX - (size / 2), offsetY - (size / 2))
        }
    }
    canvas.onmouseup = e => {
        isPrint = false
        handleData.push(painting.getImageData(0, 0, canvas.width, canvas.height))
        cacheDeleteData = []
    }

定义画笔创建与启动函数

    // 创建画笔
    const createBrush = () => {
        const brush = document.createElement('div')
        brush.style.position = 'absolute'
        brush.style.zIndex = '99'
        brush.style.width = size + 'px'
        brush.style.height = size + 'px'
        brush.style.background = 'rgba(233,233,233,0.5)'
        brush.style.pointerEvents = 'none'
        brush.style.left = '0'
        brush.style.right = '0'
        brush.style.transform = `translate(-${size}px, -${size}px)`
        content.appendChild(brush)
        return brush
    }
    // 启动
    const start = () => {
        !brush && (brush = createBrush())
        brushFollow = true
    }
    // 停止
    const stop = () => {
        brush && brush.remove()
        brushFollow = false
        brush = null
    }
    canvas.onmouseenter = start
    canvas.onmouseleave = stop

撤销与恢复

    // 撤销
    const revocation = () => {
        if (handleData.length <= 1) return false
        cacheDeleteData.push(handleData.pop())
        painting.putImageData(handleData[handleData.length - 1], 0, 0)
    }
    // 恢复
    const recover = () => {
        if (cacheDeleteData.length === 0) return false
        handleData.push(cacheDeleteData.pop())
        painting.putImageData(handleData[handleData.length - 1], 0, 0)
    }
    //CreateMosaic暴露这两个函数即可
    return {
        revocation,
        recover,
    }

返回原始尺寸的马赛克图片

//未实际写代码,说一下思路
// 创建一个canvas,宽高为imgWH缓存下的值。display:none,放入dom中。
//将canvas.toDataURL('image/png')结果新赋值给一个新的new Image()
//然后在 新canvas.getContext('2d').drawImage(0, 0, ...[imgWH])绘制
//最后 新canvas.toDataURL('image/png') 就是我们要的结果

效果

image.png