这是我参与更文挑战的第8天,活动详情查看: 更文挑战
微信小程序开发中上传头像时需要裁剪的,不然头像太大,记录自己使用的图片裁剪组件封装。组件基于we-cropper。
图片旋转和大小裁剪
主要是主页背景图的上传操作
组件结构
wxml
<view class="container">
<!-- 剪裁框与初始图片,剪裁框监听用户手势,获取移动缩放旋转值,images通过css样式显示变化 -->
<view class="img" style="width:{{ width }}px; height:{{height}}px;top:{{topHeight+120}}px" catchtouchstart="touchstartCallback" catchtouchmove="touchmoveCallback" catchtouchend="touchendCallback" >
<image style="transform: translate({{stv.offsetX}}px, {{stv.offsetY}}px) scale({{stv.scale}}) rotate({{ stv.rotate }}deg);width:{{originImg.width}}px; height: {{originImg.height}}px" src="{{ originImg.url }}"></image>
</view>
<view class='footer'>
<view bindtap='uploadTap'>重新选择</view>
<view bindtap='rotate'>旋转</view>
<view bindtap='cropperImg'>修改好了</view>
</view>
<!-- canvas长宽设为初始图片设置的长款的两倍,使剪裁得到的图片更清晰,也不至于过大 -->
<canvas class='imgcrop' style="width:{{ width * 2 }}px;height:{{ height * 2}}px;" canvas-id='imgcrop'></canvas>
</view>
wxss
.container {
width: 100%;
height: 100%;
background: #000;
position: fixed;
top: 0;
left: 0;
z-index: 99;
}
.img {
position: absolute;
top: 5%;
left: 50%;
transform: translateX(-50%);
overflow: hidden;
background: #eee;
}
.img image {
height:400px;
}
.imgcrop {
position: absolute;
left: -50000rpx;
top: -500000rpx;
}
.footer {
position: absolute;
width: 100%;
height: 110rpx;
color: #fff;
background: #000;
bottom: 0;
display: flex;
align-items: center;
justify-content: space-around;
}
.footer view {
width: 30%;
text-align: center;
}
.background {
width: 100%;
height: 100%;
position: absolute;
top: 0;
z-index: -1;
}
js
// 上传自己的头像以及个人页面的背景图裁剪组件
const app = getApp();
let topHeight = app.globalData.sysInfo.statusBarHeight + app.globalData.sysInfo.navHeight;
const device = wx.getSystemInfoSync();
var twoPoint = {
x1: 0,
y1: 0,
x2: 0,
y2: 0
};
Component({
properties: {
ratio: {
type: Number,
observer: function(newVal, oldVal) {
this.setData({
width: device.windowWidth,
height: device.windowWidth * 0.47
});
}
},
url: {
type: String,
observer(newVal, oldVal) {
this.initImg(newVal);
}
}
},
data: {
topHeight: topHeight,
width: device.windowWidth, //剪裁框的宽度
height: device.windowWidth * 0.47, //剪裁框的长度
originImg: null, //存放原图信息
stv: {
offsetX: 0, //剪裁图片左上角坐标x
offsetY: 0, //剪裁图片左上角坐标y
zoom: false, //是否缩放状态
distance: 0, //两指距离
scale: 1, //缩放倍数
rotate: 0 //旋转角度
}
},
methods: {
uploadTap() {
let _this = this;
wx.chooseImage({
count: 1, // 默认9
sourceType: ["album", "camera"], // 可以指定来源是相册还是相机,默认二者都有
success(res) {
_this.initImg(res.tempFilePaths[0]);
}
});
},
rotate() {
let _this = this;
_this.setData({
"stv.rotate":
_this.data.stv.rotate % 90 == 0
? (_this.data.stv.rotate = _this.data.stv.rotate + 90)
: (_this.data.stv.rotate = 0)
});
},
// canvas剪裁图片
cropperImg() {
wx.showLoading({
title: "loading",
mask: true
});
let _this = this;
let ctx = wx.createCanvasContext("imgcrop", this);
let cropData = _this.data.stv;
ctx.save();
// 缩放偏移值
let x =
(_this.data.originImg.width -
_this.data.originImg.width * cropData.scale) /
2;
let y =
(_this.data.originImg.height -
_this.data.originImg.height * cropData.scale) /
2;
//画布中点坐标转移到图片中心
let movex =
(cropData.offsetX + x) * 2 +
_this.data.originImg.width * cropData.scale;
let movey =
(cropData.offsetY + y) * 2 +
_this.data.originImg.height * cropData.scale;
ctx.translate(movex, movey);
ctx.rotate((cropData.rotate * Math.PI) / 180);
ctx.translate(-movex, -movey);
ctx.drawImage(
_this.data.originImg.url,
(cropData.offsetX + x) * 2,
(cropData.offsetY + y) * 2,
_this.data.originImg.width * 2 * cropData.scale,
_this.data.originImg.height * 2 * cropData.scale
);
ctx.restore();
ctx.draw(false, () => {
wx.canvasToTempFilePath(
{
canvasId: "imgcrop",
success(response) {
_this.triggerEvent("getCropperImg", {
url: response.tempFilePath
});
wx.hideLoading();
},
fail(e) {
wx.hideLoading();
wx.showToast({
title: "生成图片失败",
icon: "none"
});
}
},
this
);
});
},
initImg(url) {
let _this = this;
wx.getImageInfo({
src: url,
success(resopne) {
let innerAspectRadio = resopne.width / resopne.height;
if (innerAspectRadio < _this.data.width / _this.data.height) {
_this.setData({
originImg: {
url: url,
width: _this.data.width,
height: _this.data.width / innerAspectRadio
},
stv: {
offsetX: 0,
offsetY:
0 -
Math.abs(
(_this.data.height - _this.data.width / innerAspectRadio) /
2
),
zoom: false, //是否缩放状态
distance: 0, //两指距离
scale: 1, //缩放倍数
rotate: 0
}
});
} else {
_this.setData({
originImg: {
url: url,
height: _this.data.height,
width: _this.data.height * innerAspectRadio
},
stv: {
offsetX:
0 -
Math.abs(
(_this.data.width - _this.data.height * innerAspectRadio) /
2
),
offsetY: 0,
zoom: false, //是否缩放状态
distance: 0, //两指距离
scale: 1, //缩放倍数
rotate: 0
}
});
}
}
});
},
//事件处理函数
touchstartCallback: function(e) {
if (e.touches.length === 1) {
let { clientX, clientY } = e.touches[0];
this.startX = clientX;
this.startY = clientY;
this.touchStartEvent = e.touches;
} else {
let xMove = e.touches[1].clientX - e.touches[0].clientX;
let yMove = e.touches[1].clientY - e.touches[0].clientY;
let distance = Math.sqrt(xMove * xMove + yMove * yMove);
twoPoint.x1 = e.touches[0].pageX * 2;
twoPoint.y1 = e.touches[0].pageY * 2;
twoPoint.x2 = e.touches[1].pageX * 2;
twoPoint.y2 = e.touches[1].pageY * 2;
this.setData({
"stv.distance": distance,
"stv.zoom": true //缩放状态
});
}
},
//图片手势动态缩放
touchmoveCallback: function(e) {
let _this = this;
fn(_this, e);
},
touchendCallback: function(e) {
//触摸结束
if (e.touches.length === 0) {
this.setData({
"stv.zoom": false //重置缩放状态
});
}
}
}
});
/**
* fn:延时调用函数
* delay:延迟多长时间
* mustRun:至少多长时间触发一次
*/
var throttle = function(fn, delay, mustRun) {
var timer = null,
previous = null;
return function() {
var now = +new Date(),
context = this,
args = arguments;
if (!previous) previous = now;
var remaining = now - previous;
if (mustRun && remaining >= mustRun) {
fn.apply(context, args);
previous = now;
} else {
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
}
};
};
var touchMove = function(_this, e) {
//触摸移动中
if (e.touches.length === 1) {
//单指移动
if (_this.data.stv.zoom) {
//缩放状态,不处理单指
return;
}
let { clientX, clientY } = e.touches[0];
let offsetX = clientX - _this.startX;
let offsetY = clientY - _this.startY;
_this.startX = clientX;
_this.startY = clientY;
let { stv } = _this.data;
stv.offsetX += offsetX;
stv.offsetY += offsetY;
stv.offsetLeftX = -stv.offsetX;
stv.offsetLeftY = -stv.offsetLeftY;
_this.setData({
stv: stv
});
} else if (e.touches.length === 2) {
//计算旋转
let preTwoPoint = JSON.parse(JSON.stringify(twoPoint));
twoPoint.x1 = e.touches[0].pageX * 2;
twoPoint.y1 = e.touches[0].pageY * 2;
twoPoint.x2 = e.touches[1].pageX * 2;
function vector(x1, y1, x2, y2) {
this.x = x2 - x1;
this.y = y2 - y1;
}
//计算点乘
function calculateVM(vector1, vector2) {
return (
(vector1.x * vector2.x + vector1.y * vector2.y) /
(Math.sqrt(vector1.x * vector1.x + vector1.y * vector1.y) *
Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y))
);
}
//计算叉乘
function calculateVC(vector1, vector2) {
return vector1.x * vector2.y - vector2.x * vector1.y > 0 ? 1 : -1;
}
let vector1 = new vector(
preTwoPoint.x1,
preTwoPoint.y1,
preTwoPoint.x2,
preTwoPoint.y2
);
let vector2 = new vector(
twoPoint.x1,
twoPoint.y1,
twoPoint.x2,
twoPoint.y2
);
let cos = calculateVM(vector1, vector2);
let angle = (Math.acos(cos) * 180) / Math.PI;
let direction = calculateVC(vector1, vector2);
let _allDeg = direction * angle;
// 双指缩放
let xMove = e.touches[1].clientX - e.touches[0].clientX;
let yMove = e.touches[1].clientY - e.touches[0].clientY;
let distance = Math.sqrt(xMove * xMove + yMove * yMove);
let distanceDiff = distance - _this.data.stv.distance;
let newScale = _this.data.stv.scale + 0.005 * distanceDiff;
if (Math.abs(_allDeg) > 1) {
_this.setData({
"stv.rotate": _this.data.stv.rotate + _allDeg
});
} else {
//双指缩放
let xMove = e.touches[1].clientX - e.touches[0].clientX;
let yMove = e.touches[1].clientY - e.touches[0].clientY;
let distance = Math.sqrt(xMove * xMove + yMove * yMove);
let distanceDiff = distance - _this.data.stv.distance;
let newScale = _this.data.stv.scale + 0.005 * distanceDiff;
if (newScale < 0.2 || newScale > 2.5) {
return;
}
_this.setData({
"stv.distance": distance,
"stv.scale": newScale
});
}
} else {
return;
}
};
//为touchMove函数节流
const fn = throttle(touchMove, 10, 10);
组件使用
<cropper bind:getCropperImg="_getCropperImg" url="{{ originUrl }}" ratio="{{ratio}}"></cropper>
上传裁好图片
_getCropperImg(e) {
var coverPath = e.detail.url;
// 上传函数
}
用户头像裁剪
wxml
<view class="cropper-wrapper" catchtap='clickReturn'>
<canvas
class="cropper"
disable-scroll="true"
bindtouchstart="touchStart"
bindtouchmove="touchMove"
bindtouchend="touchEnd"
style="width:{{cropperOpt.width}}px;height:{{cropperOpt.height}}px;background-color: rgba(0, 0, 0, 0.8)"
canvas-id="{{cropperOpt.id}}">
</canvas>
<view class="cropper-buttons">
<view class="left" catchtap="upload">重新选择</view>
<view class="right" catchtap="getCropperImage">确定</view>
</view>
</view>
wxss
.cropper-buttons view {
line-height: 50px;
display: inline-block;
width: 50%;
background-color: #000;
color: #fff;
font-size: 18px;
}
.left {
text-align: left;
padding-left: 40rpx;
}
.right {
text-align: right;
padding-right: 40rpx;
}
.cropper-buttons{
display: flex;
}
js
import WeCropper from '../../utils/we-cropper.js';
const device = wx.getSystemInfoSync();
const width = device.windowWidth;
const height = device.windowHeight - 50;
var app = getApp();
var cropper = null;
Page({
data: {
cropperOpt: {
id: 'cropper',
width,
height,
scale: 1,
zoom: 5,
cut: {
x: 15,
y: (height - 300) / 2,
width: (width - 30),
height: (width - 30)
},
option: null
},
w: 0,
h: 0,
showCanvas: false
},
touchStart(e) {
this.wecropper.touchStart(e)
},
touchMove(e) {
this.wecropper.touchMove(e)
},
touchEnd(e) {
this.wecropper.touchEnd(e)
},
cancel: function() {
wx.navigateBack({
delta: 1
})
},
getCropperImage() {
var option = this.data.option;
this.wecropper.getCropperImage((src) => {
var pages = getCurrentPages();
var prePage = pages[pages.length - 2];
if (option.avatar) {
prePage.setAvatar(src);
} else if (option.cover) {
prePage.setCover(src);
}
setTimeout(() => {
wx.navigateBack({
delta: 1
})
}, 200)
})
},
onLoad: function(options) {
var self = this;
self.setData({
showCanvas: true
})
self.setData({
option: options
});
const {
cropperOpt
} = this.data;
if (options.cover) {
cropperOpt.cut.height = cropperOpt.cut.width
} else if (options.avatar || options.isSetRole) {
cropperOpt.cut.height = cropperOpt.cut.width;
} else if (options.bgImg) {
cropperOpt.cut.y = height * 1 / 10;
cropperOpt.cut.x = 15;
cropperOpt.cut.width = 345;
cropperOpt.cut.height = 614;
} else {
cropperOpt.cut.y = 50;
}
var src = options.src;
cropper = new WeCropper(cropperOpt)
.on('ready', (ctx) => {})
.on('beforeImageLoad', (ctx) => {
wx.showToast({
title: '上传中',
icon: 'loading',
duration: 20000
})
})
.on('imageLoad', (ctx) => {
wx.hideToast()
})
.on('beforeDraw', (ctx, instance) => {})
.updateCanvas();
self.wecropper.pushOrign(src);
},
upload: function() {
var self = this;
wx.chooseImage({
count: 1,
sizeType: ['original'],
sourceType: ['album', 'camera'],
success: function(res) {
var tempFilePath = self.checkPhoto(res.tempFilePaths[0]);
var option = self.data.option;
option.src = tempFilePath;
self.onLoad(option);
}
})
},
checkPhoto(tempFilePath) {
var type = "";
if (tempFilePath.length) {
type = tempFilePath.match(/^(.*)(\.)(.{1,8})$/)[3];
type = type.toUpperCase();
}
if (type != "JPEG" && type != "PNG" && type != "JPG" && type != "GIF") {
wx.showToast({
title: '已过滤特殊图片格式',
icon: "none",
duration: 2000
})
return false;
} else {
return tempFilePath;
}
},
})