在做微信小程序时,碰到一个需求,要求用户上传一张照片进行裁剪,选择贴纸后生成一张图片,这里来分享一下我实现的方法。
一、结构部分
- 首先是将原始图片放在
movable-area
组件内部,原始图片保持与movable-area 相同高宽,(说明:在这一步之前已经做过图片裁剪了,这一步这里的原始的图片的高宽都是一样的,即movable-area的高宽);类名sticker-box内部的就是贴纸图片以及取消贴纸的叉叉。
<movable-area class='img-box width-full' style="width:320px; height:178.133px">
<image class='original-img' mode="widthFix" src='{{imgUrl}}'></image>
<!-- 贴图开始 -->
<movable-view wx:if="{{chosedImg}}" style="transform:translate({{stv.offsetX}}px, {{stv.offsetY}}px);width:{{stv.width}}px;height:{{stv.height}}px" x="{{x}}" y="{{y}}" direction="all">
<view class='sticker-box' style=' rotate({{Img.rotate}}deg)' catchtouchstart="touchstartCallback" catchtouchmove="touchmoveCallback" catchtouchend="touchendCallback" >
<image class='sticker width-full' mode="widthFix" src="{{chosedImg}}"></image>
</view>
<image class='cancel' bindtap='cancel' src='../../images/cancel.png'></image>
</movable-view>
<!-- 贴图结束 -->
</movable-area>
- 底部贴纸列表
<view class='bottom'>
<view class="sticker-lists-body">
<scroll-view class="recommend_scroll_x_box" scroll-x="true">
<view class="sticker-list" wx:for="{{stickers}}" data-url="{{item}}" bindtap='changeImg'>
<image src='{{item}}'></image>
</view>
</scroll-view>
</view>
<view class='tab'>
<view class='tab-list clearfix'>
<image class='active' mode="widthFix" src='../../images/icon05.png'></image>
</view>
<button class='color-white' bindtap='save'>下一步 </button>
<button bindtap='toImg' class='color-red'> 上一步 </button>
</view>
</view>
- 用于绘图的canvas
<canvas style="width: 640px;height: 356.266px;" canvas-id="mycanvas"/>
二、样式部分
page {
height: 100%;
}
.width-full {
width: 100%;
}
.color-white {
color: #fff;
}
.color-red{
color: #f56259;
}
.bg-white {
background-color: #fff;
}
.bg-red {
background-color: #f56259;
}
.flex {
display: box; /* OLD - Android 4.4- */
display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */
display: -ms-flexbox; /* TWEENER - IE 10 */
display: -webkit-flex; /* NEW - Chrome */
display: flex;
}
.flex-hc {
-webkit-box-pack: center;
-webkit-justify-content: center;
-moz-justify-content: center;
-ms-justify-content: center;
-o-justify-content: center;
justify-content: center;
}
.flex-vc {
-webkit-box-align: center;
-webkit-align-items: center;
-moz-align-items: center;
-ms-align-items: center;
-o-align-items: center;
align-items: center;
}
.pull-right {
float: right;
}
.pull-left {
float: left;
}
.clearfix {
clear: both;
}
.clearfix:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.top-box {
padding: 20rpx 30rpx;
height: calc(100% - 182px);
display: flex;
align-items: center;
justify-content: center;
}
.original-img {
width: 320px;
margin: 0 auto;
}
.bg-img {
position: absolute;
top: 0;
right: 0;
z-index: -1;
}
.bottom {
position: fixed;
bottom: 50px;
left: 0;
width: 100%;
background-color: rgba(230,225,225,0.8);
}
.bottom>view.tab {
padding-right: 30rpx;
padding-left: 30rpx;
}
.bottom>view.sticker-lists-body{
padding-left: 30rpx;
}
.recommend_scroll_x_box {
height: 100rpx;
padding-top: 30rpx;
padding-bottom: 40rpx;
width: 100%;
overflow: auto;
white-space: nowrap;
display: flex;
vertical-align: top;
}
::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
.sticker-lists-body {
padding-left: 30rpx;
}
.sticker-lists-body .sticker-list {
width: 100rpx;
height: 100rpx;
margin-right: 24rpx;
display: inline-block;
vertical-align: top;
}
.sticker-lists-body .sticker-list image {
width: 100rpx;
height: 100rpx;
background-color: #ffffff;
}
.bottom image {
width: 50rpx;
height: 50rpx;
}
.bottom .tab image {
margin-right: 60rpx;
}
.bottom .tab {
padding: 25rpx 60rpx;
background-color: #f4f4f4;
height: 70rpx;
}
.bottom .tab .tab-list {
position: relative;
float: left;
display: flex;
align-items: center;
margin-top: 10rpx;
}
.bottom .tab button{
background-color: #d81e06;
float: right;
height: 70rpx;
line-height: 70rpx;
font-size: 30rpx;
}
.bottom .tab button.color-red {
background-color: #fff;
border: 1rpx solid #d81e06;
margin-right: 10rpx;
}
.bottom .tab .tab-list image.active::before {
content: '';
position: absolute;
top: -50rpx;
left: 5rpx;
border-right: 20rpx solid transparent;
border-left: 20rpx solid transparent;
border-bottom: 20rpx solid #f4f4f4;
}
movable-view {
height: 50px;
width: 50px;
}
movable-view .sticker-box {
position: relative;
width:100%;
height: 100%;
border: 1rpx dashed #ccc;
}
image.cancel {
position: absolute;
top: -15rpx;
left: -15rpx;
width:30rpx;
height: 30rpx;
z-index: 30;
}
canvas属于客户端创建的原生组件,级别很高,用z-index控制无效,设置display:none之后对绘图有影响,取巧让canvas定位在可视页面之外
.canvas-box {
opacity: 0;
position: fixed;
top: 150%;
left: 0;
z-index: -1;
}
三、js部分
- 设置data数据
data: {
imgUrl : '../../images/example.png',//实际项目中用的是上一个裁剪页面传来的图片
stickers: ['../../images/sticker/1.png',
'../../images/sticker/2.png',
'../../images/sticker/3.png',
'../../images/sticker/4.png',
'../../images/sticker/5.png',
'../../images/sticker/6.png',
'../../images/sticker/7.png',
'../../images/sticker/8.png',
'../../images/sticker/9.png',
'../../images/sticker/10.png',
'../../images/sticker/11.png'],
x: 160,
y: 50,
chosedImg: false,
stv: {
offsetX: 160,
offsetY: 50,
zoom: false, //是否缩放状态
distance: 0, //两指距离
scale: 1, //缩放倍数
width: 50,
height: 50,
},
- 贴图移动与双指缩放,通过offsetX和offsetY 来记录贴纸的位置,通过width和height来记录贴纸的高宽
// 贴图触摸开始
touchstartCallback: function (e) {
//console.log('touchstartCallback');
//console.log(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);
this.setData({
'stv.distance': distance,
'stv.zoom': true, //缩放状态
})
}
},
// 贴图触摸移动中
touchmoveCallback: function (e) {
//console.log('touchmoveCallback');
//console.log(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;
var nowWidth = this.data.stv.width;
var maxoffsetX = 320 - nowWidth;
var nowHeight = this.data.stv.height;
var maxoffsetY = 178.125 - nowHeight;
if (stv.offsetX > maxoffsetX) {
stv.offsetX = maxoffsetX;
} else if (stv.offsetX < 0) {
stv.offsetX = 0;
}
if (stv.offsetY > maxoffsetY) {
stv.offsetY = maxoffsetY;
} else if (stv.offsetY < 0) {
stv.offsetY = 0;
}
this.setData({
stv: stv
});
} 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.5) {
newScale = 0.5;
}
if (newScale > 4) {
newScale = 4;
}
let newWidth = newScale * 50;
let newHeight = newScale * 50;
this.setData({
'stv.distance': distance,
'stv.scale': newScale,
'stv.width': newWidth,
'stv.height': newWidth,
})
//console.log(this.data.stv.scale)
}
},
// 贴图触摸结束
touchendCallback: function (e) {
// console.log('touchendCallback');
//console.log(e);
if (e.touches.length === 0) {
this.setData({
'stv.zoom': false, //重置缩放状态
})
}
},
- 点击贴纸左上角的叉叉取消贴纸
//取消圣诞帽
cancel: function () {
this.setData({
chosedImg: false,
x: 150,
y: 75,
stv: {
offsetX: 75,
offsetY: 75,
zoom: false, //是否缩放状态
distance: 0, //两指距离
scale: 1, //缩放倍数
width: 50,
height: 50,
}
})
},
- 切换贴纸
changeImg: function (e) {
var $img = e.currentTarget.dataset.url;
var chosedImg = this.data.chosedImg;
var chosedImg1 = this.data.chosedImg1;
var chosedImg2 = this.data.chosedImg2;
this.setData({
chosedImg: false,
x: 160,
y: 50,
stv: {
offsetX: 160,
offsetY: 50,
zoom: false, //是否缩放状态
distance: 0, //两指距离
scale: 1, //缩放倍数
width: 50,
height: 50,
}
}),
this.setData({
chosedImg: $img,
})
},
- 接下来就是我们的canvas绘图部分
//将贴纸绘制到canvas的固定
setHat: function (context) {
var hat = this.data.chosedImg;
var newtop = this.data.stv.offsetX * 2;
var newleft = this.data.stv.offsetY * 2;
var newswidth = this.data.stv.width * 2;
var newheight = this.data.stv.height * 2;
context.drawImage(hat, newtop, newleft, newswidth, newheight)
context.save();
context.restore();
context.stroke();
},
//将canvas转换为图片保存到本地,然后将图片路径传给image图片的src
createNewImg: function (imgUrl) {
var that = this;
var chosedImg = this.data.chosedImg;
var formValue = that.data.formValue;
var path = imgUrl;
var context = wx.createCanvasContext('mycanvas');
//为了解决绘制出来的图片有锯齿,这里绘制图片时放大了一倍进行绘制
context.drawImage(path, 0, 0, 640, 356.266);
//若选择了贴纸就绘制贴纸
if(chosedImg){
this.setHat(context);
}
//绘制图片
context.draw();
//将生成好的图片保存到本地,需要延迟一会,绘制期间耗时
setTimeout(function () {
wx.canvasToTempFilePath({
canvasId: 'mycanvas',
success: function (res) {
var tempFilePath = res.tempFilePath;
console.log(tempFilePath);
formValue[0].imagePath = tempFilePath;
formValue[0].videoUrl = "";
//imagePath即生成的图片路径,正常项目中点击下一步会做图片上传,这里不做讲解,只给出了地址,可以在页面中调用地址查看图片
that.setData({
imagePath: tempFilePath,
})
},fail: function (res) {
console.log(res);
}
});
}, 200);
},
//点击下一步保存按钮
save: function () {
console.log("1111")
var that = this;
wx.showLoading({
title: '创建中...',
})
setTimeout(function () {
var imgUrl = that.data.imgUrl
//wx.hideToast()
that.createNewImg(imgUrl);
that.setData({
maskHidden: true
});
console.log("canvas")
}, 1000)
},