在做微信小程序时,碰到一个需求,要求用户上传一张照片进行裁剪,选择贴纸后生成一张图片,这里来分享一下我实现的方法。

一、结构部分

  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>
  1. 底部贴纸列表
<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>
  1. 用于绘图的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部分

  1. 设置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,
},
  1. 贴图移动与双指缩放,通过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, //重置缩放状态
		})
	}
},
  1. 点击贴纸左上角的叉叉取消贴纸
//取消圣诞帽

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,
		}
	})
},
  1. 切换贴纸
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,
	})
},
  1. 接下来就是我们的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)
},

github地址:https://github.com/sky-Aimee/sticker.git

01-27 23:48