图片马赛克

ImageData对象

  • canvas的ctx.getImageData(left.top,width,height)可以获取canvas图像内的特定位置宽高的像素信息,这些信息存放在一个ImageData对象里面。

  • ImageData内就是数组,根据特定公式获取对应位置的像素的R/G/B/Alpha的信息:

    例如,要读取图片中位于第50行,第200列的像素的蓝色部份,你会写以下代码:

    1
    blueComponent = imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 2];

    根据行、列读取某像素点的R/G/B/A值的公式:

    1
    imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 0/1/2/3];
  • canvas的ctx.putImageData(imgData, left, top)可以在特定位置写入像素信息。

这个几个API合起来其实就是修改图像的某个地方。相对于直接画,这种方式操作的粒度更细而已。

更详细查看MDN

原理

像素其实是一个四方形的色块,图片是由不同像素(色块)组成的,在一定大小下,像素越多代表的色块越多那么数据越多,图片也就越清晰;像素少,那么数据少,图片只有寥寥几个色块组成,每个色块就会大。因为大,色块的棱角就会明显,图形也会因此出现一些“锯齿”(可以由此理解抗锯齿),失真。

马赛克可以是失真,马赛克就是某个区域内的色块更大,相对其他地方,图形更模糊。那么想要某个区域打上马赛克,只需要模糊对应区域,即减少组成的色块数量。

如何减少色块数量?屏幕的像素数量是固定,即原始色块数量是固定的,这个无法改变。那么就用多个原始色块组成一个大的色块。具体到代码其实就利用ImageData修改某一个区域的像素为同一种颜色。

整体的实现思路:

  1. 获取图片,使用canvasContext.drawImage,canvas元素图片预览

  2. canvas监听mousedown、mousemove、mouseup三个事件,mousedown表示鼠标点击下,准备开始打马赛克记录状态isDraw = true;mouseup表示鼠标松开,结束打马赛克,记录状态isDraw = false;mousemove表示图像种鼠标在移动,通过isDraw判断是否在打马赛克;若是执行3,若不是不做操作。

  3. 马赛克逻辑:通过mousemove的event参数可以获取触发事件元素的位置偏移值、

    1
    2
    left = event.offsetX
    top = event.offsetY

    这个位置即当前需要打马赛克的位置。马赛克的宽高可以事先配置

  4. canvas的ctx.getImageData(left.top,width,height)获取像素信息

  5. 获取像素数据的区域,划分比像素更大若干的色块(大的色块),每个色块设置成同一个颜色,这个颜色在色块自身区域随机选取。达成“失真模糊”效果

  6. 把这块像素数据写回canvas原来位置

具体实现

一些工具函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 读取文件,返回Promise
function readFile(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function (data) {
resolve(reader.result)
}
reader.onerror = (err) => reject(err)
})
}

// 根据fileData创建Image元素并返回,返回Promise
function createImgEl(fileData) {
return new Promise((resolve, reject) => {
const img = new Image()
img.src = fileData
img.onload = function (e) {
resolve(img)
}
img.onerror = (err) => reject(err)
})
}


// ImageData获取x行y列颜色为color,obj为ImageData数据对象
function getXY(obj, x, y) {
const w = obj.width;
// var h = obj.height;
// var d = obj.data;
let color = [];
color[0] = obj.data[4 * (y * w + x)];
color[1] = obj.data[4 * (y * w + x) + 1];
color[2] = obj.data[4 * (y * w + x) + 2];
color[3] = obj.data[4 * (y * w + x) + 3];
return color;
}

// ImageData设置x行y列颜色为color,obj为ImageData数据对象
function setXY(obj, x, y, color) {
var w = obj.width;
// var h = obj.height;
// var d = obj.data;
obj.data[4 * (y * w + x)] = color[0];
obj.data[4 * (y * w + x) + 1] = color[1];
obj.data[4 * (y * w + x) + 2] = color[2];
obj.data[4 * (y * w + x) + 3] = color[3];
}

创建canvas

首先应该实现创建canvas,因为马赛克是在canvas上绘制的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class MasaicComponent {
constructor(options) {
if (!options.file) {
throw new Error('musted have file in the options as source picture')
}
if (!options.el) {
throw new Error('musted have el in the options as place to work ')
}
this.file = options.file
this.el = options.el // 挂载点

this.canvas = null
this.init()
}

async init() {
this.canvas = await this.createCanvas()

this.el.innerHTML = ''
this.el.appendChild(this.canvas)
}

// 创建画布并且画好图片
async createCanvas() {
const fileData = await readFile(this.file)
const imgEl = await createImgEl(fileData)

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.position = 'relative'

// 图片大小适配
// const proportion = imgEl.naturalWidth / imgEl.naturalHeight
const containerStyle = window.getComputedStyle(this.el)
const maxWidth = containerStyle.width.slice(0, -2)
const maxHeight = containerStyle.height.slice(0, -2)
let width = imgEl.naturalWidth
let height = imgEl.naturalHeight
if (width > maxWidth) {
width = maxWidth
height = Math.floor((width / maxWidth) * maxHeight)
} else if (height > maxHeight) {
width = Math.floor((height * maxWidth) / maxHeight)
height = maxHeight
}

canvas.width = width
canvas.height = height

ctx.drawImage(imgEl, 0, 0, width, height)

return canvas
}
}

画笔ui相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class MasaicComponent {
constructor(options) {
if (!options.file) {
throw new Error('musted have file in the options as source picture')
}
if (!options.el) {
throw new Error('musted have el in the options as place to work ')
}
this.file = options.file
this.el = options.el // 挂载点

this.canvas = null
this.init()
}

async init() {
this.canvas = await this.createCanvas()

this.el.innerHTML = ''
this.el.appendChild(this.canvas)

// 画笔元素
this.panEl = document.createElement('div')
this.panEl.style.width = this.pan.radius + 'px'
this.panEl.style.height = this.pan.radius + 'px'
this.panEl.style.borderRadius = '50%'
this.panEl.style.position = 'absolute'
this.panEl.style.backgroundColor = 'yellow'
this.panEl.style.transform = 'translate(-50%,-50%)'
this.panEl.style.pointerEvents = 'none'
this.panEl.style.display = 'none'
this.panEl.style.opacity = 0.5

// 画笔相对于挂载元素的起始偏移
this.panOffest = {
left: this.canvas.offsetLeft, //canvasStyle.left.slice(0, -2) - 0,
top: this.canvas.offsetTop, // canvasStyle.top.slice(0, -2) - 0,
}
this.el.appendChild(this.panEl)

// 绘制画笔
this.drawPan = this._drawPan.bind(this)

}

// 创建画布并且画好图片
async createCanvas() {
// some codes
}

// 设置画笔大小
setPan(options) {
this.pan = {
...this.pan,
...options,
}
this.panEl.style.width = this.pan.radius + 'px'
this.panEl.style.height = this.pan.radius + 'px'
}


_drawPan() {
this.panEl.style.left = `${this.panOffest.left + this.pan.x}px`
this.panEl.style.top = `${this.panOffest.top + this.pan.y}px`
this.raf = window.requestAnimationFrame(this.drawPan)
}
}

绑定事件

点击移动时获取绘制马赛克的位置数据和触发绘制马赛克

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class MasaicComponent {
constructor(options) {
//some codes
}

async init() {
// some codes

// 绑定 画笔马赛克操作(点击,移动,放起)
this.bindEvent()
}

// 创建画布并且画好图片
async createCanvas() {
// some codes
}

bindEvent() {
// 画笔操作
let isDraw = false
this.canvas.addEventListener('mousedown', (e) => {
isDraw = true
})
this.canvas.addEventListener('mousemove', (e) => {
this.pan.x = e.offsetX
this.pan.y = e.offsetY
if (isDraw) {
this.drawMasaic()
}
// ref = window.requestAnimationFrame(this.pan.draw(e))
})
this.canvas.addEventListener('mouseup', (e) => {
isDraw = false
})

// 画笔ui
this.raf = null
this.canvas.addEventListener('mouseover', (e) => {
this.panEl.style.display = 'block'
this.raf = window.requestAnimationFrame(this.drawPan)
})

this.canvas.addEventListener('mouseout', (e) => {
this.panEl.style.display = 'none'
window.cancelAnimationFrame(this.raf)
})
}


setPan(options) {
// some codes
}

_drawPan() {
// some codes
}
}

马赛克绘制的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class MasaicComponent {
constructor(options) {
//some codes
}

async init() {
// some codes
}

// 创建画布并且画好图片
async createCanvas() {
// some codes
}

bindEvent() {
// some codes
}


drawMasaic() {
const ctx = this.canvas.getContext('2d')

let left = this.pan.x - Math.floor(this.pan.radius / 2)
left = left == 0 ? 0 : left
let top = this.pan.y - Math.floor(this.pan.radius / 2)
top = top == 0 ? 0 : top

const imgData = ctx.getImageData(
// this.pan.x,
// this.pan.y,
left,
top,
this.pan.radius,
this.pan.radius
)
const w = imgData.width
const h = imgData.height

const num = 2
const sw = Math.floor(w / num)
const sh = Math.floor(h / num)

// 遍历选中图片区域,获取其中每一个小区域
for (let i = 0; i < sh; i++) {
for (let j = 0; j < sw; j++) {
let rw = Math.floor(Math.random() * num)
let rh = Math.floor(Math.random() * num)
const color = getXY(imgData, j * num + rw, i * num + rh)

// 设置当前小区域为同一种颜色
for (let ii = 0; ii < num; ii++) {
for (let jj = 0; jj < num; jj++) {
setXY(imgData, j * num + jj, i * num + ii, color)
}
}
}
}

ctx.putImageData(imgData, left, top) //this.pan.x, this.pan.y)
}

setPan(options) {
// some codes
}

_drawPan() {
// some codes
}
}

详细代码 查看demo

系列推荐:

图片压缩

图片裁剪

前端抠图?

参考

canva实践小实例 —— 马赛克效果

ImageData——MDN