实现简单的图片马赛克功能

2,412 阅读5分钟

前言

上周产品提了个需求,要在审核页面增加去水印的功能,趁着周末思考了下🤔,结合网上一些资料,尝试实现了下。实现的基本思路是:将图片绘制到canvas画布上,然后通过鼠标移动形成一个矩形区域,然后在矩形区域中填充马赛克。

实现

鼠标移动生成矩形

这里我的想法是将图片放在一个容器中(editor),在容器中设置一个绝对定位的子容器(mask),通过拖拽控制矩形大小。

// template
<div>
    <h3>图片马赛克</h3>
    <div class="editor" ref="ed" @mousemove="handleMove" @mousedown="handleDown" @mouseup="handleUp">
        <img src="../assets/2.png" @dragstart.prevent ref="originImg" />
        <div ref="mask" class="mask" :style="maskStyle"></div>
    </div>
</div>

// script
data(){
    return {
        conY:0, // 图片容器的距页面顶部的距离
        conX:0, // 图片容器的距页面左侧的距离
        startX: 0, // 当鼠标按下并且移动时由sx赋值而来
        startY: 0, // 当鼠标按下并且移动时由sy赋值而来
        sx:0, // 鼠标按下时的pageX
        sy:0, // 鼠标按下时的pageY
        endX: 0, // 鼠标移动过程中的pageX
        endY: 0, // 鼠标移动过程中的pageY
        flag: false // 开关,当鼠标按下移动时才会触发mask的宽高及定位的变化
    }
},
computed:{
    maskStyle(){
        // 根据鼠标当前的x和y确定mask的宽高和定位
        // mask的top值取起始时y值和鼠标移动中y值中的小值,left同理
        let top = this.startY > this.endY ? this.endY : this.startY;
        let left = this.startX > this.endX ? this.endX : this.startX;
        return {
            top: top + 'px',
            left: left + 'px',
            width: Math.abs(this.startX - this.endX) + 'px',
            height: Math.abs(this.startY - this.endY) + 'px',
        }
    }
},
mounted(){
    // 挂载完成获取容器的距顶和距左数值
    this.conY = this.$refs.ed.offsetTop;
    this.conX = this.$refs.ed.offsetLeft;
},
methods:{
    handleDown(e){ // 记录下鼠标按下时的x,y值并开启开关,允许在之后的拖动中更改mask的样式
        this.sx = e.offsetX;
        this.sy = e.offsetY;
        this.flag = true;
    },
    resetPos(){
        // 将mask框还原为初始状态
        this.sx = 0;
        this.sy = 0;
        this.startX = 0;
        this.startY = 0;
        this.endX = 0;
        this.endY = 0;
    },
    handleMove(e){
        // 为防止鼠标在容器上移动触发mask变化,加了开关
        if(!this.flag) return ;
        // 防止点击触发mask变化,只有在鼠标按下并且移动时才将初始值用来更改mask
        this.startX = this.sx;
        this.startY = this.sy;
        
        // 这里开始用的是offsetX和offsetY,但是鼠标拖动过程中发现mask位置会出现晃动的情况,改用pageX - conX的写法
        this.endX = e.pageX - this.conX;
        this.endY = e.pageY - this.conY;
    },
    handleUp(){
        // 将开关置为false
        this.flag = false;
    },
}

这时就可以通过鼠标移动控制mask的位置和大小了。(下图)

(这里由于我本地调试使用的是本地图片,如果是真正做项目时使用网络链接的图片地址需要给img添加crossOrigin = 'anonymous',如果图片和项目不在同一台服务器下还需要服务器配置下允许跨域。)

将图片绘制到canvas画布上

为了开发方便,先将canvas放在一个别的容器里,再加个按钮,点击触发绘制

// template中添加下面代码
...
<el-button type="primary" @click="addMosaic">添加马赛克</el-button>
<div class="canvas-con" ref="cvsCon"></div>

// script中添加创建画布的函数
// data里添加canvas和ctx两个属性,这里就不展示在这了
addMosaic(){
    let editor = this.$refs.ed; // 放置图片的容器
    let img = this.$refs.originImg; // 原始的图片
    let w = editor.offsetWidth; // 获取图片容器宽高
    let h = editor.offsetHeight;

    let imgW = img.offsetWidth; // 获取图片宽高
    let imgH = img.offsetHeight;

    // 避免每次点击按钮都创建新的画布,这样可以保留每一次的马赛克,去除这个判断就只能保留最新一次的马赛克
    if(!this.canvas){
        // 创建canvas并将其设为和图片容器一样大小
        let c = document.createElement('canvas')
        c.width = w;
        c.height = h;
        this.$refs.cvsCon.innerHTML = '';
        this.$refs.cvsCon.appendChild(c)
        this.canvas = c;
        this.ctx = this.canvas.getContext('2d');
        this.ctx.drawImage(img,0,0,imgW,imgH); // 将图片绘制到画布上
    }

    // 获取mask的位置和大小,用于之后绘制马赛克(也可以在之前设置mask时将各项参数保存下来,这里就不用再次获取了)
    let y = +(this.$refs.mask.style.top.split('px')[0]);
    let x = +(this.$refs.mask.style.left.split('px')[0]);
    let conW = +(this.$refs.mask.style.width.split('px')[0]);
    let conH = +(this.$refs.mask.style.height.split('px')[0]);

    // 为了避免影响下一次mask的生成,将mask的位置、大小还原为初始状态
    this.resetPos();

    // 当没有mask时,不绘制马赛克
    if(!conW || !conH) return ;
    // 获取mask对应区域的图片数据
    let imgData = this.ctx.getImageData(x,y,conW,conH);
    // 绘制马赛克,下面会详细说明
    this.createMosaic(this.ctx,x,y,conW,conH,Math.ceil(conW/10),imgData);
},

这里是通过canvas.drawImage将图片绘制到画布上,然后获取mask位置,为之后绘制马赛克做准备

绘制马赛克

将上一步通过getImageData得到的图片数据imgData打印出来,可以看到其中的data项是一个大数组,数组里是一个个介于0 ~ 255的数字(这里没有截取到),这里的data根据w3c的解释是 拷贝了画布指定矩形的像素数据

下面是一些个人理解(根据公式反推的理解),可能有不对的地方:
一个像素数据即rgba,包括了四项内容,每一项内容都是data数组内的一项,像素的排列是自上而下一行一行排列的,也就是先是从左到右,再从上到下,因此(x,y)位置点的像素数据就是(y * width + x) * 4, 即

// (x,y)位置的点数据
let ind = (y * width + x) * 4
pixel.r = data[ind];
pixel.g = data[ind + 1];
pixel.b = data[ind + 2];
pixel.a = data[ind + 3];

这里设置马赛克的思路是遍历这个data数组,步长自己设置,我设置的是mask宽度/10,也就是马赛克在横向上固定10个。然后将步长区域内颜色变为统一的一种颜色,我设置的这个颜色是步长内的随机一个颜色,然后使用ctx.fillStyle填充画笔,ctx.fillRect绘制这个步长大小的方块区域,代码如下:

// 参数意义分别为canvas画笔, 起始位置, 马赛克区域总的宽高, 步长, 马赛克区域对应的原始图片数据
createMosaic(context, sx, sy, width, height, size, {data}){
    // 以步长size为间距循环遍历马赛克区域的像素
    for (let y = sy; y < sy + height; y += size) {
        for (let x = sx; x < sx + width; x += size) {
            // 获取0 ~ size的随机数
            let rand = Math.floor(Math.random() * size);
            // size步长内随机一个元素的颜色
            let ind = ((y - sy + rand) * width + x - sx + rand) * 4
            let cR = data[ind],
                cG = data[ind + 1],
                cB = data[ind + 2];
            // 填充画笔
            context.fillStyle = `rgb(${cR},${cG},${cB})`;
            // 绘制size步长内的马赛克小方块
            context.fillRect(x, y, size, size);
        }
    }
},

至此就成功啦,效果图:

(第一次写文章,不知道怎么设置图片大小,希望大家见谅)