h5手机移动端实现图片截取逻辑

代码记录如下

主要是结合vuejs开发的小活动里面,需要用到截图的功能,开始自己以为实现起来很麻烦,经过同事的帮忙,居然有人实现过,就直接拿过来使用了,目前测试结果,没有发现什么兼容性的问题,不过需要按照自己的需求更改下具体的实现逻辑,代码如下

clip库的js代码段(这里是我做了部分改动的,因为最终的图片结果我认为还是直接返回比较好)

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
class Clip{
//因为要保存到vue实例中,所以直接传一个$vm当闭包使用了
constructor(wpId,$vm){
this.regional = document.getElementById(wpId);
this.getImage = document.createElement('canvas');
this.getImage.id = 'image-box';
this.editBox = document.createElement('canvas');
this.editBox.id = 'cover-box';

this.regional.appendChild(this.getImage);
this.regional.appendChild(this.editBox);
this.$vm = $vm;
}

init(file){
this.sx = 0; //裁剪框的初始x
this.sy = 0; //裁剪框的初始y
this.sWidth = 233; //裁剪框的宽
this.sHeight = 233; //裁剪框的高
this.chooseBoxScale = 233/233;

this.handleFiles(file);
}

handleFiles(file){
let t = this;
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function() {
t.imgUrl = this.result;
t.paintImg(this.result);
}
}

paintImg(picUrl){
let t = this;
let cxt = t.getImage.getContext('2d');

//先清空画布
cxt.clearRect(0, 0, this.getImage.width, this.getImage.height);

let img = new Image();
img.src = picUrl;

img.onload = function() {

let imgScale = img.width / img.height;
let boxScale = t.regional.offsetWidth / t.regional.offsetHeight;

//判断盒子与图片的比列
if (imgScale < boxScale) {
//设置图片的像素
t.imgWidth = t.regional.offsetHeight * imgScale;
t.imgHeight = t.regional.offsetHeight;
} else {
//设置图片的像素
t.imgWidth = t.regional.offsetWidth;
t.imgHeight = t.regional.offsetWidth / imgScale;
}

//判断图片与选择框的比例大小,作出裁剪
if (imgScale < t.chooseBoxScale) {
//设置选择框的像素
t.sWidth = t.imgWidth;
t.sHeight = t.imgWidth / t.chooseBoxScale;

//设置初始框的位置
t.sx = 0;
t.sy = (t.imgHeight - t.sHeight) / 2;
} else {
//设置选择框的像素
t.sWidth = t.imgHeight * t.chooseBoxScale;
t.sHeight = t.imgHeight;

t.sx = (t.imgWidth - t.sWidth) / 2;
t.sy = 0;
}

//高分屏下图片模糊,需要2倍处理
t.getImage.height = 2 * t.imgHeight;
t.getImage.width = 2 * t.imgWidth;
t.getImage.style.width = t.imgWidth + 'px';
t.getImage.style.height = t.imgHeight + 'px';

let vertSquashRatio = t.detectVerticalSquash(img);

cxt.drawImage(img, 0, 0,2 * t.imgWidth * vertSquashRatio, 2 * t.imgHeight * vertSquashRatio)

t.cutImage();
t.drag();
}
}

cutImage(){
let t = this;

//绘制遮罩层:
t.editBox.height = 2 * t.imgHeight;
t.editBox.width = 2 * t.imgWidth;

t.editBox.style.display = 'block';
t.editBox.style.width = t.imgWidth + 'px';
t.editBox.style.height = t.imgHeight + 'px';

let cover = t.editBox.getContext("2d");
cover.fillStyle = "rgba(0, 0, 0, 0.7)";

cover.fillRect(0, 0, 2 * t.imgWidth, 2 * t.imgHeight);
cover.clearRect(2 *t.sx, 2 * t.sy, 2 * t.sWidth, 2 * t.sHeight);
}

drag(){
let t = this;
let draging = false;

//记录初始点击的pageX,pageY。用于记录位移
let pageX = 0;
let pageY = 0;

//初始位移
let startX = 0;
let startY = 0;

t.editBox.addEventListener('touchmove', function(ev) {
let e = ev.touches[0];

let offsetX = e.pageX - pageX;
let offsetY = e.pageY - pageY;
if (draging) {
if (t.imgHeight == t.sHeight) {
t.sx = startX + offsetX;
if (t.sx <= 0) {
t.sx = 0;
} else if (t.sx >= t.imgWidth - t.sWidth) {
t.sx = t.imgWidth - t.sWidth;
}
} else {
t.sy = startY + offsetY;
if (t.sy <= 0) {
t.sy = 0;
} else if (t.sy >= t.imgHeight - t.sHeight) {
t.sy = t.imgHeight - t.sHeight;
}
}
t.cutImage();
}
});

t.editBox.addEventListener('touchstart', function(ev) {
let e = ev.touches[0];
draging = true;

pageX = e.pageX;
pageY = e.pageY;

startX = t.sx;
startY = t.sy;

})

t.editBox.addEventListener('touchend', function() {
draging = false;
})
}

save(callback) {
let t = this;
let saveCanvas = document.createElement('canvas');
let ctx = saveCanvas.getContext('2d');

//图片裁剪后的尺寸
saveCanvas.width = 466;
saveCanvas.height = 466;

let images = new Image();
images.src = t.imgUrl;

images.onload = function(){
//计算裁剪尺寸比例,用于裁剪图片
let cropWidthScale = images.width/t.imgWidth;
let cropHeightScale = images.height/t.imgHeight;

t.drawImageIOSFix(
ctx,
images,
cropWidthScale * t.sx ,
cropHeightScale* t.sy,
t.sWidth * cropWidthScale,
t.sHeight * cropHeightScale,
0,
0,
466,
466);

// t.$vm.clipUrl = saveCanvas.toDataURL();
t.regional.removeChild(t.getImage);
t.regional.removeChild(t.editBox);
callback(saveCanvas.toDataURL());
}
}

remove() {
let t = this;
t.regional.removeChild(t.getImage);
t.regional.removeChild(t.editBox);
}

getImageUrl() {
let t = this;
return t.$vm.clipUrl;
}

//用于修复ios下的canvas截图问题
//详情可以看这里http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios
detectVerticalSquash(img) {
if(/png$/i.test(img.src)) {
return 1;
}
let iw = img.naturalWidth, ih = img.naturalHeight;
let canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = ih;
let ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
let data = ctx.getImageData(0, 0, 1, ih).data;

let sy = 0;
let ey = ih;
let py = ih;
while (py > sy) {
const alpha = data[(py - 1) * 4 + 3];
if (alpha === 0) {
ey = py;
} else {
sy = py;
}
py = (ey + sy) >> 1;
}
const ratio = (py / ih);
return (ratio===0)?1:ratio;
}
drawImageIOSFix(ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) {
const vertSquashRatio = this.detectVerticalSquash(img);
ctx.drawImage(
img,
sx * vertSquashRatio,
sy * vertSquashRatio,
sw * vertSquashRatio,
sh * vertSquashRatio,
dx,
dy,
dw,
dh);
}
}

css代码段(这里我只用到了截图图片一小部分的UI,就是弹窗后截取图片的UI部分,这一部分如果自己写的话,就要看源码了。其实截图后的展示部分的功能,完全可以根据自己的需求来实现,而且原作者是用的less)

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
.file{
height: 40px;
display: block;
margin: 40px auto 0;
}

.clip-img{
width: 300px;
height: 225px;
margin: 20px auto 0;
border: 1px solid #999;
overflow: hidden;
}

.clip-img img{
width: 100%;
}

.upload-wp {
text-align: center;
width: 300px;
margin: 20px auto 0;
}

.upload-wp button {
padding: 5px 10px;
}

.upload-wp p {
word-wrap: break-word;
font-size: 12px;
}

.clip-wp {
position: fixed;
width: 100%;
top: 0;
bottom: 0;
z-index: 11;
background-color: #000;
text-align: center;
}

.clip-wp #container{
background-color: #000;
text-align: center;
width: 100%;
left: 0;
right: 0;
top: 20px;
bottom: 80px;
margin: 0 auto;
position: absolute;
}

.clip-wp #save-img{
position: absolute;
bottom: 20px;
width: 40%;
left: 5%;
height: 42px;
line-height: 42px;
color: #fff;
background-color: #32c47c;
border-radius: 20px;
}

.clip-wp #cancel-img{
position: absolute;
bottom: 20px;
width: 40%;
left: 55%;
height: 42px;
line-height: 42px;
color: #fff;
background-color: #32c47c;
border-radius: 20px;
}

.clip-wp #image-box {
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
top: 0px;
margin: auto;
}

.clip-wp #cover-box {
position: absolute;
z-index: 9999;
display: none;
left: 0px;
right: 0px;
bottom: 0px;
top: 0px;
margin: auto;
}

.preview-wp {
text-align: left;
position: fixed;
top: 0;
bottom: 0;
width: 100%;
background-color: #000;
overflow:auto;
}

.preview-wp .preview-img{
position: absolute;
top: 50%;
width: 100%;
transform: translate(0 , -50% );
-webkit-transform: translate(0 ,-50%);
}

html代码实现部分,这个就要根据具体的情况,我是页面上有两个图片按钮,点击后替换图片 所以我就直接用一个file类型的input,上传完之后将图片替换到指定的按钮上,然后重置input,这样就可以重复不断的更新上传图片

1
2
3
4
5
6
<!-- 上传图片的隐藏元素 -->
<div style="width:9vw;height:9vw;background:red;position:absolute;top:2vw;display:none">
<form id="fileElem1">
<input ref=fileElem type="file" class="file" accept="image/*;capture=camera" name="img" @change="clipImg($event)" style="display: none">
</form>
</div>
1
2
3
4
5
6
7
<span style="display:inline-block;position:relative;">
<span style="display:inline-block;width:9vw;height:9vw;line-height:9vw;text-align:center;border:solid .2vw #fff;background-color:#f7e9d4;box-shadow:0 1vw 1vw 0 #e0cfb3;overflow:hidden;border-radius:50%" @click='choiceMeAvatar()'>
<img v-bind:src="images.left_avatar_kuang" style="width: 100%">
</span>
<span style="display: inline-block;width: 9vw;height: 9vw;line-height:9vw;text-align:center;border: solid 0.2vw #ffffff;background-color: #f7e9d4;box-shadow: 0vw 1vw 1vw 0vw #e0cfb3;overflow: hidden;border-radius: 50%;margin-left: -2vw" @click='choiceTaAvatar()'>
<img v-bind:src="images.right_avatar_kuang" style="width: 100%">
</span>

vue js代码methods实现部分

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
clipImg: function(event){
this.clip = new Clip('container', this);
this.clip.init(event.target.files[0]);
this.isClip = true;
document.body.addEventListener('touchmove', this.noScoll, false);
// main-container 固定
document.getElementById('main-container').style.position = 'fixed';
},

saveImg: function(){
this.isClip = false;
var self = this;
var imageData = this.clip.save(function(imageData){
console.log(self.currentUploadFieldName);
console.log(imageData);

if (self.currentUploadFieldName == 'left_avatar_kuang') {
self.images.left_avatar_kuang = imageData;
} else if (self.currentUploadFieldName == 'right_avatar_kuang') {
self.images.right_avatar_kuang = imageData;
}
});

document.body.removeEventListener('touchmove', this.noScoll, false);
// form中input reset
document.getElementById("fileElem1") && document.getElementById("fileElem1").reset();
// main-container 解除固定
document.getElementById('main-container').style.position = 'relative';
},

cancelImg: function() {
this.isClip = false;
this.clip.remove();
this.clip = null;
document.body.removeEventListener('touchmove', this.noScoll, false);
// form中input reset
document.getElementById("fileElem1") && document.getElementById("fileElem1").reset();
// main-container 解除固定
document.getElementById('main-container').style.position = 'relative';
},

vue变量部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
data: {
currentUploadFieldName: '',
images: {
'left_avatar_kuang': '/xxx/avatar_default.png',
'right_avatar_kuang': '/xxx/avatar_default.png',
},
isClip: false,
clipUrl:'',
noScoll: function(evt){
this.isClip && evt.preventDefault();
},
clip:{},
}

我这里只是给出了基本上80%实现部分,还有20%具体细节的部分要具体细化了。

参考文章:链接1