抠图

做了之前几篇文章,感觉自己可以了,突然想做一个浏览器的ps可以吗?然后上网搜了一下,发现有公司已经做出产品了。。。all right,伟大构思慢了一步,出生太晚了。

  • 图片拼接?

    drawImage两次,记一下图片的高度即可

  • 图片水印?

    也是两次drawImage。。。一次原图,一次水印

  • 然后发现一个比较有趣的功能——抠图。这个功能应该是特别常见,从原图中获取一部分图片。细想了一下啊,不就是更细粒度的图片裁剪吗,然后尝试了下实现基本功能,然后有了本文。

思路

还是常见的API,canvas的使用。基本上和马赛克思路一样,马赛克的鼠标滑动选中后修改一定区域的颜色,抠图做的是保存选中区域然后绘制到另一个目标canvas上。绑定事件的画笔逻辑、获取位置信息与马赛克逻辑一样。主要替换的逻辑是具体画马赛克的部分,画马赛克变成获取ImageData然后绘制到另外一个canvas【resCtx.putImageData(imgData, left, top)】

实现

创建canvas

首先应该实现创建canvas,从原图上取ImageData出来

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
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.targetEl = options.targetEl // **相对马赛克新增

this.canvas = null
this.resultCanvas = null // **相对马赛克新增
this.init()
}

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

// 存放 抠图结果 的canvas // **相对马赛克新增
this.resultCanvas = document.createElement('canvas')
this.resultCanvas.width = this.canvas.width
this.resultCanvas.height = this.canvas.height

this.el.innerHTML = ''
this.el.appendChild(this.canvas)
this.targetEl.appendChild(this.resultCanvas) // **相对马赛克新增
}

// 创建画布并且画好图片
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
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() {
// some codes

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)

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

// 创建画布并且画好图片
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
59
60
61
62
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.cutter()
}
// 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)
})
}

cutter(){
// TODO
}


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
class MasaicComponent {
constructor(options) {
//some codes
}

async init() {
// some codes
}

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

bindEvent() {
// some codes
}


cutter() {
const ctx = this.canvas.getContext('2d')
const resCtx = this.resultCanvas.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
)
resCtx.putImageData(imgData, left, top) //this.pan.x, this.pan.y)
}

setPan(options) {
// some codes
}

_drawPan() {
// some codes
}
}

详细代码 查看demo

系列推荐:

图片压缩

图片裁剪

图片马赛克

参考

ImageData——MDN