图片裁剪

CanvasRenderingContext2D.drawimage(el, …)

canvas的APIcontext.drawimage(el, ...),接口详情可以参考MDN,这个接口的作用就是在canvas中画出el元素,el可以是video,image,canvas元素,后面的参数就是一些位置宽高信息,从el元素的什么位置截取多少宽高,得到一个图像,在把这个绘制到当前canvas的什么位置宽高。

实现思路

核心就是context.drawimage(el, ...),构造Image元素,然后再取得位置信息即可获取截取图片中一小块绘制到canvas上,然后canvas可以设置大小为截取图大小,然后再使用canvas.toDataURL()导出canvas图形即可。

图片截取原理

截取的位置和宽高

这里的难点在于,如何获取截取的位置和宽高?

我们思考把原图Image设想为div矩形parent,截取框设想为div矩形child;如果child的相对parent定位,那么child的top就是所求Top,child的left就是所求的Left,child的宽度width就是所求的Width,child的高度height就是所求的Height。

思路

所以我们构造父子元素,父元素设置大小为原图大小,子元素为可移动,缩放拉伸的元素;截取的图片就是当前子元素的top,left,width,height;

截取框的移动和伸缩

上一节了解,截取框其实就是“child元素“而已。那么现在剩下的问题就是这个子元素的移动伸缩了,因为截取框肯定是大小可变,位置可移的。

不同位置大小截取
移动

可以利用mousedown、mousemove、mouseup事件来进行实现。

  1. 截取框元素中mousedown时,记录状态isMouseDown为true和位置X/Y(由event.pageX/Y取得)

  2. 截取元素中出啊发mouseup时,记录状态为isMouseDown为false

  3. 在截取框元素中触发mousemove时,检测状态isMouseDown,若为true说明鼠标点击后移动没放起,即正在拖动元素,那么就进行4;若为false则跳过

  4. 当前位置(event.pageX/Y)和上一次位置X/Y可以算出位置移动的left和top

    left = event.pageX - X

    top = event.pageY - Y

    有这个移动信息 可以直接赋值给截取框css的left和top实现移动

伸缩

同理,也是三个鼠标事件的监听,得到鼠标移动的位置信息。移动的时赋值给css的left,top实现移动,那伸缩的就是赋值这些位置信息给css的width和height实现元素的伸缩。

不过按照常规的操作习惯,伸缩最好是在一个顶点拉伸,所以截取框中还要做一个小元素作为拉伸点,如何再实现三个事件即可

拉伸点

具体实现

工具函数

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
// 读取文件,返回Promise
function readFile(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function (data) {
console.log('FileReader', 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) {
console.log(e)
resolve(img)
}
img.onerror = (err) => reject(err)
})
}

// 创建tag标签,子元素是children,children若为字符串,则标签内容为这字符串
function creatEL(tag, children) {
let el = document.createElement(tag);
if (children && Array.isArray(children)) {
children.forEach((v) => el.appendChild(v));
} else if (children && typeof children == "object") {
el.appendChild(children);
} else if (children) {
el.innerHTML = children;
}
return el;
}

function throttle(delay, fn) {
let cando = true
return function (...args) {
if (cando) {
fn.apply(this, args)
cando = false
setTimeout(() => {
cando = true
}, delay);
}
}

}

构造”父元素“,原图

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
class CropComponent {
constructor(file, options) {
if (!options.el) {
throw new Error('there must have a el as mounted point')
}
this.file = file
this.el = options.el // 用作“父元素”
}

// 创建工作台
async build() {
// 读取源图片数据
const sourceFileData = await readFile(this.file)

// 生成对应的Image元素(获取源图片宽高和作为工作台)
const imgEl = await createImgEl(sourceFileData)

const naturalWidth = imgEl.naturalWidth
const naturalHeight = imgEl.naturalHeight

this.el.style.position = `relative`
this.el.style.width = `${naturalWidth}px`
this.el.style.height = `${naturalHeight}px`
this.el.style.background = `url(${sourceFileData}) no-repeat`
this.el.style.backgroundSize = '100% 100%'
}

}

创建“子元素”,裁剪框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 创建裁剪框(带伸缩点)
createCropBox() {
//裁剪框
this.cutFieldEl = document.createElement('div')
this.cutFieldEl.classList.add('cropbox')

// 伸缩点
this.moveFielEl = document.createElement('div')
this.moveFielEl.classList.add('moveField')

this.el.appendChild(this.cutFieldEl)
this.cutFieldEl.appendChild(this.moveFielEl)
}

裁剪框移动实现

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
class CropComponent {
constructor(file, options) {
if (!options.el) {
throw new Error('there must have a el as mounted point')
}
this.file = file
this.el = options.el
this.containerEl = options.containerEl
this.cutFieldEl = null
this.moveFielEl = null
this.mouseCtx = null

this.move = this._move.bind(this)
}

// 创建工作台
async build() {
// some code

// 多加一行
this.createCropBox()
}

// 创建裁剪框(带伸缩点)
createCropBox() {
this.cutFieldEl = document.createElement('div')
this.cutFieldEl.classList.add('cropbox')
this.moveFielEl = document.createElement('div')
this.moveFielEl.classList.add('moveField')

// 裁剪框的移动事件监听
this.mouseCtx = {
isMouseDown: false,
x: 0,
y: 0,
}
this.cutFieldEl.addEventListener(
'mousedown',
(e) => {
// console.log('mousedown')
this.mouseCtx = {
isMouseDown: true,
x: e.pageX,
y: e.pageY,
}
},
false
)
this.cutFieldEl.addEventListener(
'mousemove',
throttle(5, (e) => {
// 裁剪框的移动
if (this.mouseCtx.isMouseDown) {
this.move(e)
}
// 裁剪框的缩放
if (this.moveMouseCtx.isMouseDown) {
this.expand(e)
}
}),
false
)
this.cutFieldEl.addEventListener(
'mouseup',
(e) => {
// console.log('mouseup', e)
this.mouseCtx.isMouseDown = false
},
false
)

this.el.appendChild(this.cutFieldEl)
this.cutFieldEl.appendChild(this.moveFielEl)
}

// 截取框移动逻辑
_move(e) {
const offsetX = e.pageX - this.mouseCtx.x
const offsetY = e.pageY - this.mouseCtx.y

// 校验不能移出图片工作区
const cutFieldStyle = window.getComputedStyle(this.cutFieldEl)
const workspaceStyle = window.getComputedStyle(this.el)
let left = cutFieldStyle.left.slice(0, -2) - 0 + offsetX
let top = cutFieldStyle.top.slice(0, -2) - 0 + offsetY
const maxLeft =
workspaceStyle.width.slice(0, -2) - cutFieldStyle.width.slice(0, -2)
const maxTop =
workspaceStyle.height.slice(0, -2) -
cutFieldStyle.height.slice(0, -2)
left = left < 0 ? 0 : left
left = left > maxLeft ? maxLeft : left
top = top < 0 ? 0 : top
top = top > maxTop ? maxTop : top

this.cutFieldEl.style.left = left + 'px'
this.cutFieldEl.style.top = top + 'px'
this.mouseCtx.x += offsetX
this.mouseCtx.y += offsetY
}
}

裁剪框拉伸实现

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

class CropComponent {
constructor(file, options) {
// some code
this.expand = this._expand.bind(this)
}

// 创建工作台
async build() {
// some code
}

// 创建裁剪框(带伸缩点)
createCropBox() {
// some code

// 裁剪框的缩放事件监听
this.moveMouseCtx = {
isMouseDown: false,
x: 0,
y: 0,
}
this.moveFielEl.addEventListener('mousedown', (e) => {
e.stopPropagation()
// console.log('move mousedown')
this.moveMouseCtx = {
isMouseDown: true,
x: e.pageX,
y: e.pageY,
}
})
this.moveFielEl.addEventListener(
'mousemove',
throttle(5, (e) => {
e.stopPropagation()
if (this.moveMouseCtx.isMouseDown) {
this.expand(e)
}
})
)
this.moveFielEl.addEventListener('mouseup', (e) => {
e.stopPropagation()
// console.log('move mouseup', e)
this.moveMouseCtx.isMouseDown = false
})

// 如果需要,也绑定控制事件到容器元素上
if (this.containerEl) {
this.handleContainer()
}

this.el.appendChild(this.cutFieldEl)
this.cutFieldEl.appendChild(this.moveFielEl)
}

// 截取框移动逻辑
_move(e) {
// some code
}

// 截取框拉伸扩展逻辑
_expand(e) {
const offsetX = e.pageX - this.moveMouseCtx.x
const offsetY = e.pageY - this.moveMouseCtx.y

// 裁剪区域扩大
const cutFieldStyle = window.getComputedStyle(this.cutFieldEl)
const workspaceStyle = window.getComputedStyle(this.el)
let width = cutFieldStyle.width.slice(0, -2) - 0
let height = cutFieldStyle.height.slice(0, -2) - 0
width = width + offsetX
height = height + offsetY
const maxWidth =
workspaceStyle.width.slice(0, -2) - cutFieldStyle.left.slice(0, -2)
const maxHeight =
workspaceStyle.height.slice(0, -2) - cutFieldStyle.top.slice(0, -2)
width = width < 0 ? 0 : width
width = width > maxWidth ? maxWidth : width
height = height < 0 ? 0 : height
height = height > maxHeight ? maxHeight : height

this.cutFieldEl.style.width = width + 'px'
this.cutFieldEl.style.height = height + 'px'

this.moveMouseCtx.x += offsetX
this.moveMouseCtx.y += offsetY
}

}

获取位置宽高信息

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
// 截图工作台ui组件
// const cropComponent = new CropComponent(file,{el,containerEl})
// cropComponent.build() // 构建截取框
// const positionMsg = cropComponent.getPositionMsg() // 获取截取位置信息(可直接传递给crop的第二参数)
class CropComponent {
constructor(file, options) {
// some code
}

// 创建工作台
async build() {
// some code
}

// 创建裁剪框(带伸缩点)
createCropBox() {
// some code
}

// 截取框移动逻辑
_move(e) {
// some code
}
// 截取框拉伸扩展逻辑
_expand(e) {
// some code
}


// 获取当前的截取框位置宽高信息
getPositionMsg() {
const cutFieldStyle = window.getComputedStyle(this.cutFieldEl)
return {
top: cutFieldStyle.top.slice(0, -2) - 0,
left: cutFieldStyle.left.slice(0, -2) - 0,
width: cutFieldStyle.width.slice(0, -2) - 0,
height: cutFieldStyle.height.slice(0, -2) - 0,
}
}
}

裁剪逻辑

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
// 截取sourceFile中options内位置信息的图片,返回base64
async function crop(sourceFile, options) {
// 读取源图片数据
const sourceFileData = await readFile(sourceFile)

// 生成对应的Image元素(获取源图片宽高和绘制sourceCanvas)
const imgEl = await createImgEl(sourceFileData)

// 处理截图的位置,宽高
// targetTop, targetLeft, targetWidth, targetHeight
const defaultOptions = {
top: 0,
left: 0,
width: imgEl.naturalWidth,
height: imgEl.naturalHeight,
type: 'image/jpeg',
}
const realOptions = {
...defaultOptions,
...options,
}

// 截图
// 创建resultCanvas
const resultCanvas = document.createElement('canvas')
resultCanvas.width = realOptions.width
resultCanvas.height = realOptions.height
const resultCtx = resultCanvas.getContext('2d')
// 把截取画面绘制经resultCanvas
resultCtx.drawImage(
imgEl,
realOptions.left,
realOptions.top,
realOptions.width,
realOptions.height,
0,
0,
realOptions.width,
realOptions.height
)

// 导出
const code = resultCanvas.toDataURL(realOptions.type, 1)

return code
}

代码参考demo

系列推荐:

图片压缩

图片马赛克

前端抠图?

参考

https://juejin.cn/post/6860024132730519560#heading-5

最后更新: 2021年07月30日 20:15

原始链接: https://idkhts.github.io/2021/07/26/%E5%9B%BE%E7%89%87%E8%A3%81%E5%89%AA/