h5手机移动端实现图片截取逻辑
代码记录如下
主要是结合vuejs开发的小活动里面,需要用到截图的功能,开始自己以为实现起来很麻烦,经过同事的帮忙,居然有人实现过,就直接拿过来使用了,目前测试结果,没有发现什么兼容性的问题,不过需要按照自己的需求更改下具体的实现逻辑,代码如下
clip库的js代码段(这里是我做了部分改动的,因为最终的图片结果我认为还是直接返回比较好)
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)
.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,这样就可以重复不断的更新上传图片
<!-- 上传图片的隐藏元素 -->
<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>
<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实现部分
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变量部分代码如下
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
版权声明
由 durban创作并维护的 Gowhich博客采用创作共用保留署名-非商业-禁止演绎4.0国际许可证。
本文首发于 博客( https://www.gowhich.com ),版权所有,侵权必究。
版权声明
由 durban创作并维护的 Gowhich博客采用创作共用保留署名-非商业-禁止演绎4.0国际许可证。
本文首发于 Gowhich博客( https://www.gowhich.com ),版权所有,侵权必究。