前言
上周产品提了个需求,要在审核页面增加去水印的功能,趁着周末思考了下🤔,结合网上一些资料,尝试实现了下。实现的基本思路是:将图片绘制到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的位置和大小了。(下图)

将图片绘制到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];

// 参数意义分别为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);
}
}
},
至此就成功啦,效果图:

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