在做微信小程序时,碰到一个需求,要求用户上传一张照片进行裁剪,选择贴纸后生成一张图片,这里来分享一下我实现的方法。
一.结构部分
1.首先是将原始图片放在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>
2.底部贴纸列表
<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>
3.用于绘图的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) },