├── pages ├── Demo │ ├── Demo.json │ ├── Demo.wxss │ ├── Demo.wxml │ └── Demo.js ├── detail │ ├── detail.json │ ├── detail.wxss │ ├── detail.wxml │ └── detail.js ├── example │ ├── example.json │ ├── example.wxml │ ├── example.wxss │ └── example.js └── gif │ ├── gif.json │ ├── gif.wxml │ ├── gif.wxss │ └── gif.js ├── images ├── cancel.png ├── focus.gif ├── example.png ├── choseImg-bg.png └── sticker │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── README.md ├── app.json ├── utils └── util.js ├── project.config.json ├── app.js └── app.wxss /pages/Demo/Demo.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pages/detail/detail.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pages/example/example.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pages/gif/gif.json: -------------------------------------------------------------------------------- 1 | { 2 | "disableScroll": true 3 | 4 | } -------------------------------------------------------------------------------- /images/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/cancel.png -------------------------------------------------------------------------------- /images/focus.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/focus.gif -------------------------------------------------------------------------------- /images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/example.png -------------------------------------------------------------------------------- /images/choseImg-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/choseImg-bg.png -------------------------------------------------------------------------------- /images/sticker/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/sticker/1.png -------------------------------------------------------------------------------- /images/sticker/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/sticker/10.png -------------------------------------------------------------------------------- /images/sticker/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/sticker/11.png -------------------------------------------------------------------------------- /images/sticker/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/sticker/2.png -------------------------------------------------------------------------------- /images/sticker/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/sticker/3.png -------------------------------------------------------------------------------- /images/sticker/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/sticker/4.png -------------------------------------------------------------------------------- /images/sticker/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/sticker/5.png -------------------------------------------------------------------------------- /images/sticker/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/sticker/6.png -------------------------------------------------------------------------------- /images/sticker/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/sticker/7.png -------------------------------------------------------------------------------- /images/sticker/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/sticker/8.png -------------------------------------------------------------------------------- /images/sticker/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlianOL/wx_sticker/HEAD/images/sticker/9.png -------------------------------------------------------------------------------- /pages/detail/detail.wxss: -------------------------------------------------------------------------------- 1 | /* pages/detail/detail.wxss */ 2 | 3 | image{ 4 | width: 320px; 5 | height: 178px; 6 | margin: 100rpx auto; 7 | } -------------------------------------------------------------------------------- /pages/detail/detail.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sticker 2 | 微信小程序实现图片贴纸效果 3 | 4 | 实现了贴纸叠加、删除、单指移动、右下角缩放旋转的功能 5 | 6 | 这个Demo并没有进行边缘判断 7 | 没有使用最新的movable-view 8 | 部分代码参考了大佬的方法https://blog.csdn.net/qq_37942845/article/details/80169907 9 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/gif/gif", 4 | "pages/Demo/Demo", 5 | "pages/example/example", 6 | 7 | 8 | "pages/detail/detail" 9 | 10 | ], 11 | "window": { 12 | 13 | "backgroundTextStyle": "light", 14 | "navigationBarBackgroundColor": "#fff", 15 | "navigationBarTitleText": "canvas实现贴图", 16 | "navigationBarTextStyle": "black" 17 | } 18 | } -------------------------------------------------------------------------------- /pages/Demo/Demo.wxss: -------------------------------------------------------------------------------- 1 | /* pages/Demo/Demo.wxss */ 2 | .back-container { 3 | display: flex; 4 | flex-direction: column; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | .area-container { 9 | width: 750rpx; 10 | height: 750rpx; 11 | } 12 | .top-container { 13 | 14 | position: absolute; 15 | width: 750rpx; 16 | height: 750rpx; 17 | background-color: #ddd; 18 | z-index:999; 19 | } 20 | .areaview { 21 | width: 200rpx; 22 | height: 200rpx; 23 | } -------------------------------------------------------------------------------- /utils/util.js: -------------------------------------------------------------------------------- 1 | const formatTime = date => { 2 | const year = date.getFullYear() 3 | const month = date.getMonth() + 1 4 | const day = date.getDate() 5 | const hour = date.getHours() 6 | const minute = date.getMinutes() 7 | const second = date.getSeconds() 8 | 9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 10 | } 11 | 12 | const formatNumber = n => { 13 | n = n.toString() 14 | return n[1] ? n : '0' + n 15 | } 16 | 17 | module.exports = { 18 | formatTime: formatTime 19 | } 20 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件。", 3 | "setting": { 4 | "urlCheck": true, 5 | "es6": true, 6 | "postcss": true, 7 | "minified": true, 8 | "newFeature": true 9 | }, 10 | "compileType": "miniprogram", 11 | "libVersion": "1.9.90", 12 | "appid": "wxb7ce775b75516de5", 13 | "projectname": "canvas%E5%AE%9E%E7%8E%B0%E8%B4%B4%E5%9B%BE", 14 | "condition": { 15 | "search": { 16 | "current": -1, 17 | "list": [] 18 | }, 19 | "conversation": { 20 | "current": -1, 21 | "list": [] 22 | }, 23 | "miniprogram": { 24 | "current": -1, 25 | "list": [] 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /pages/Demo/Demo.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /pages/Demo/Demo.js: -------------------------------------------------------------------------------- 1 | // pages/Demo/Demo.js 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | 9 | }, 10 | 11 | /** 12 | * 生命周期函数--监听页面加载 13 | */ 14 | onLoad: function (options) { 15 | 16 | }, 17 | 18 | /** 19 | * 生命周期函数--监听页面初次渲染完成 20 | */ 21 | onReady: function () { 22 | 23 | }, 24 | 25 | /** 26 | * 生命周期函数--监听页面显示 27 | */ 28 | onShow: function () { 29 | 30 | }, 31 | 32 | /** 33 | * 生命周期函数--监听页面隐藏 34 | */ 35 | onHide: function () { 36 | 37 | }, 38 | 39 | /** 40 | * 生命周期函数--监听页面卸载 41 | */ 42 | onUnload: function () { 43 | 44 | }, 45 | 46 | /** 47 | * 页面相关事件处理函数--监听用户下拉动作 48 | */ 49 | onPullDownRefresh: function () { 50 | 51 | }, 52 | 53 | /** 54 | * 页面上拉触底事件的处理函数 55 | */ 56 | onReachBottom: function () { 57 | 58 | }, 59 | 60 | /** 61 | * 用户点击右上角分享 62 | */ 63 | onShareAppMessage: function () { 64 | 65 | } 66 | }) -------------------------------------------------------------------------------- /pages/detail/detail.js: -------------------------------------------------------------------------------- 1 | // pages/detail/detail.js 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | 9 | }, 10 | 11 | /** 12 | * 生命周期函数--监听页面加载 13 | */ 14 | onLoad: function (options) { 15 | var path = options.path; 16 | this.setData({ 17 | path:path 18 | }) 19 | 20 | }, 21 | 22 | /** 23 | * 生命周期函数--监听页面初次渲染完成 24 | */ 25 | onReady: function () { 26 | 27 | }, 28 | 29 | /** 30 | * 生命周期函数--监听页面显示 31 | */ 32 | onShow: function () { 33 | 34 | }, 35 | 36 | /** 37 | * 生命周期函数--监听页面隐藏 38 | */ 39 | onHide: function () { 40 | 41 | }, 42 | 43 | /** 44 | * 生命周期函数--监听页面卸载 45 | */ 46 | onUnload: function () { 47 | 48 | }, 49 | 50 | /** 51 | * 页面相关事件处理函数--监听用户下拉动作 52 | */ 53 | onPullDownRefresh: function () { 54 | 55 | }, 56 | 57 | /** 58 | * 页面上拉触底事件的处理函数 59 | */ 60 | onReachBottom: function () { 61 | 62 | }, 63 | 64 | /** 65 | * 用户点击右上角分享 66 | */ 67 | onShareAppMessage: function () { 68 | 69 | } 70 | }) -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | onLaunch: function () { 4 | // 展示本地存储能力 5 | var logs = wx.getStorageSync('logs') || [] 6 | logs.unshift(Date.now()) 7 | wx.setStorageSync('logs', logs) 8 | 9 | // 登录 10 | wx.login({ 11 | success: res => { 12 | // 发送 res.code 到后台换取 openId, sessionKey, unionId 13 | } 14 | }) 15 | // 获取用户信息 16 | wx.getSetting({ 17 | success: res => { 18 | if (res.authSetting['scope.userInfo']) { 19 | // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 20 | wx.getUserInfo({ 21 | success: res => { 22 | // 可以将 res 发送给后台解码出 unionId 23 | this.globalData.userInfo = res.userInfo 24 | 25 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 26 | // 所以此处加入 callback 以防止这种情况 27 | if (this.userInfoReadyCallback) { 28 | this.userInfoReadyCallback(res) 29 | } 30 | } 31 | }) 32 | } 33 | } 34 | }) 35 | }, 36 | globalData: { 37 | userInfo: null 38 | } 39 | }) -------------------------------------------------------------------------------- /app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | image { 3 | width: auto; 4 | height: auto; 5 | } 6 | 7 | .width-full { 8 | width: 100%; 9 | } 10 | 11 | .color-white { 12 | color: #fff; 13 | } 14 | 15 | .flex { 16 | display: box; /* OLD - Android 4.4- */ 17 | display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ 18 | display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ 19 | display: -ms-flexbox; /* TWEENER - IE 10 */ 20 | display: -webkit-flex; /* NEW - Chrome */ 21 | display: flex; 22 | 23 | } 24 | 25 | .flex-hc { 26 | /* 09版 */ 27 | -webkit-box-pack: center; 28 | /* 12版 */ 29 | -webkit-justify-content: center; 30 | -moz-justify-content: center; 31 | -ms-justify-content: center; 32 | -o-justify-content: center; 33 | justify-content: center; 34 | } 35 | 36 | .flex-vc { 37 | /* 09版 */ 38 | -webkit-box-align: center; 39 | /* 12版 */ 40 | -webkit-align-items: center; 41 | -moz-align-items: center; 42 | -ms-align-items: center; 43 | -o-align-items: center; 44 | align-items: center; 45 | } 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /pages/example/example.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /pages/gif/gif.wxml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /pages/gif/gif.wxss: -------------------------------------------------------------------------------- 1 | /* pages/gif/gif.wxss */ 2 | 3 | page { 4 | background-color: #ddd; 5 | } 6 | 7 | .gif-constainer { 8 | width: 150rpx; 9 | height: 150rpx; 10 | } 11 | 12 | .back-container { 13 | width: 750rpx; 14 | height: 750rpx; 15 | background-color: #888; 16 | } 17 | 18 | .bottom-container { 19 | height: 500rpx; 20 | } 21 | 22 | .image-container { 23 | width: 100rpx; 24 | height: 100rpx; 25 | } 26 | 27 | .image.cancel { 28 | position: absolute; 29 | top: -15rpx; 30 | left: -15rpx; 31 | width: 30rpx; 32 | height: 30rpx; 33 | z-index: 30; 34 | } 35 | 36 | .touchWrap { 37 | transform-origin: center; 38 | position: absolute; 39 | z-index: 100; 40 | width: 200rpx; 41 | height: 200rpx; 42 | } 43 | 44 | .imgWrap { 45 | box-sizing: border-box; 46 | width: 100%; 47 | transform-origin: center; 48 | float: left; 49 | border: 5rpx transparent dashed; 50 | } 51 | 52 | .x { 53 | position: absolute; 54 | top: 0rpx; 55 | left: 0rpx; 56 | width: 30rpx; 57 | height: 30rpx; 58 | z-index: 30; 59 | } 60 | 61 | .o { 62 | position: absolute; 63 | bottom: 0rpx; 64 | right: 0rpx; 65 | width: 30rpx; 66 | height: 30rpx; 67 | z-index: 30; 68 | } 69 | 70 | .recommend_scroll_x_box { 71 | position: relative; 72 | display: flex; 73 | flex-direction: row; 74 | background-color: #987; 75 | height: 150rpx; 76 | /* padding-top: 30rpx; 77 | padding-bottom: 40rpx; */ 78 | width: 100%; 79 | overflow: auto; 80 | white-space: nowrap; 81 | /* display: flex; */ 82 | vertical-align: top; 83 | } 84 | 85 | .sticker-lists image { 86 | width: 100rpx; 87 | height: 100rpx; 88 | } 89 | 90 | .sticker-lists { 91 | margin: 20rpx 20rpx 20rpx 20rpx; 92 | width: 100rpx; 93 | height: 100rpx; 94 | display: inline-block; 95 | align-items: center; 96 | align-content: center; 97 | align-self: center; 98 | } 99 | -------------------------------------------------------------------------------- /pages/example/example.wxss: -------------------------------------------------------------------------------- 1 | /* pages/uploadImg/uploadImg.wxss */ 2 | page { 3 | height: 100%; 4 | } 5 | .top-box { 6 | padding: 20rpx 30rpx; 7 | height: calc(100% - 182px); 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | } 12 | 13 | .original-img { 14 | width: 320px; 15 | height: 178px; 16 | margin: 0 auto; 17 | } 18 | 19 | .bg-img { 20 | position: absolute; 21 | top: 0; 22 | right: 0; 23 | z-index: -1; 24 | } 25 | 26 | .bottom { 27 | position: fixed; 28 | bottom: 0; 29 | left: 0; 30 | width: 100%; 31 | background-color: rgba(230,225,225,0.8); 32 | } 33 | 34 | .bottom>view.tab { 35 | padding-right: 30rpx; 36 | padding-left: 30rpx; 37 | } 38 | 39 | .bottom>view.sticker-lists-body{ 40 | padding-left: 30rpx; 41 | } 42 | 43 | .recommend_scroll_x_box { 44 | height: 100rpx; 45 | padding-top: 30rpx; 46 | padding-bottom: 40rpx; 47 | width: 100%; 48 | overflow: auto; 49 | white-space: nowrap; 50 | display: flex; 51 | vertical-align: top; 52 | } 53 | 54 | ::-webkit-scrollbar { 55 | width: 0; 56 | height: 0; 57 | color: transparent; 58 | } 59 | 60 | .sticker-lists-body { 61 | padding-left: 30rpx; 62 | } 63 | 64 | .sticker-lists-body .sticker-list { 65 | width: 100rpx; 66 | height: 100rpx; 67 | margin-right: 24rpx; 68 | display: inline-block; 69 | vertical-align: top; 70 | } 71 | .sticker-lists-body .sticker-list image { 72 | width: 100rpx; 73 | height: 100rpx; 74 | background-color: #ffffff; 75 | } 76 | 77 | .bottom image { 78 | width: 50rpx; 79 | height: 50rpx; 80 | } 81 | 82 | .bottom .tab image { 83 | margin-right: 60rpx; 84 | } 85 | 86 | .bottom .tab { 87 | padding: 25rpx 60rpx; 88 | background-color: #f4f4f4; 89 | height: 70rpx; 90 | } 91 | 92 | .bottom .tab .tab-list { 93 | position: relative; 94 | float: left; 95 | display: flex; 96 | align-items: center; 97 | margin-top: 10rpx; 98 | } 99 | 100 | .bottom .tab button{ 101 | background-color: #d81e06; 102 | float: right; 103 | height: 70rpx; 104 | line-height: 70rpx; 105 | font-size: 30rpx; 106 | } 107 | 108 | .bottom .tab button.color-red { 109 | background-color: #fff; 110 | border: 1rpx solid #d81e06; 111 | margin-right: 10rpx; 112 | } 113 | 114 | .bottom .tab .tab-list image.active::before { 115 | content: ''; 116 | position: absolute; 117 | top: -50rpx; 118 | left: 5rpx; 119 | border-right: 20rpx solid transparent; 120 | border-left: 20rpx solid transparent; 121 | border-bottom: 20rpx solid #f4f4f4; 122 | } 123 | 124 | 125 | 126 | 127 | 128 | movable-view { 129 | height: 50px; 130 | width: 50px; 131 | } 132 | 133 | movable-view .sticker-box { 134 | position: relative; 135 | width:100%; 136 | height: 100%; 137 | border: 1rpx dashed #ccc; 138 | } 139 | 140 | image.cancel { 141 | position: absolute; 142 | top: -15rpx; 143 | left: -15rpx; 144 | width:30rpx; 145 | height: 30rpx; 146 | z-index: 30; 147 | } 148 | 149 | .canvas-box { 150 | opacity: 0; 151 | position: fixed; 152 | top: 100%; 153 | left: 0; 154 | z-index: -1; 155 | } 156 | 157 | .canvas-box canvas { 158 | opacity: 0; 159 | } -------------------------------------------------------------------------------- /pages/gif/gif.js: -------------------------------------------------------------------------------- 1 | // pages/gif/gif.js 2 | var items = new Array(); 3 | var index = null; 4 | Page({ 5 | 6 | /** 7 | * 页面的初始数据 8 | */ 9 | data: { 10 | stickers: [ 11 | '../../images/sticker/1.png', 12 | '../../images/sticker/2.png', 13 | '../../images/sticker/3.png', 14 | '../../images/sticker/4.png', 15 | '../../images/sticker/5.png', 16 | '../../images/sticker/6.png', 17 | '../../images/sticker/7.png', 18 | '../../images/sticker/8.png', 19 | '../../images/sticker/9.png', 20 | '../../images/sticker/10.png', 21 | '../../images/sticker/11.png'], 22 | // itemList: [{ 23 | // id: 1, 24 | // image: '../../images/sticker/1.png',//图片地址 25 | // top: 100,//初始图片的位置 26 | // left: 100, 27 | // x: 150, //初始圆心位置,可再downImg之后又宽高和初始的图片位置得出 28 | // y: 150, 29 | // scale: 1,//缩放比例 1为不缩放 30 | // angle: 0,//旋转角度 31 | // active: false, //判定点击状态 32 | // width: 100, 33 | // height: 100 34 | // }, { 35 | // id: 2, 36 | // image: '../../images/sticker/2.png', 37 | // top: 50, 38 | // left: 50, 39 | // x: 100, 40 | // y: 100, 41 | // scale: 1, 42 | // angle: 0, 43 | // active: false, 44 | // width: 100, 45 | // height:100 46 | // }], 47 | 48 | }, 49 | WraptouchStart: function (e) { 50 | for (let i = 0; i < items.length; i++) { //旋转数据找到点击的 51 | items[i].active = false; 52 | if (e.currentTarget.dataset.id == items[i].id) { 53 | index = i; //记录下标 54 | items[index].active = true; //开启点击属性 55 | } 56 | } 57 | 58 | items[index].lx = e.touches[0].clientX; // 记录点击时的坐标值 59 | items[index].ly = e.touches[0].clientY; 60 | this.setData({ //赋值 61 | itemList: items 62 | }) 63 | } 64 | , WraptouchMove: function (e) { 65 | //移动时的坐标值也写图片的属性里 66 | items[index]._lx = e.touches[0].clientX; 67 | items[index]._ly = e.touches[0].clientY; 68 | 69 | //追加改动值 70 | items[index].left += items[index]._lx - items[index].lx; // x方向 71 | items[index].top += items[index]._ly - items[index].ly; // y方向 72 | items[index].x += items[index]._lx - items[index].lx; 73 | items[index].y += items[index]._ly - items[index].ly; 74 | 75 | //把新的值赋给老的值 76 | items[index].lx = e.touches[0].clientX; 77 | items[index].ly = e.touches[0].clientY; 78 | this.setData({//赋值就移动了 79 | itemList: items 80 | }) 81 | }, 82 | WraptouchEnd:function(e) { 83 | 84 | }, 85 | // 触摸开始事件 items是this.data.itemList的全局变量,便于赋值 所有的值都应给到对应的对象里 86 | touchStart: function (e) { 87 | //找到点击的那个图片对象,并记录 88 | for (let i = 0; i < items.length; i++) { 89 | items[i].active = false; 90 | 91 | if (e.currentTarget.dataset.id == items[i].id) { 92 | console.log('e.currentTarget.dataset.id', e.currentTarget.dataset.id) 93 | index = i; 94 | console.log(items[index]) 95 | items[index].active = true; 96 | } 97 | } 98 | //获取作为移动前角度的坐标 99 | items[index].tx = e.touches[0].clientX; 100 | items[index].ty = e.touches[0].clientY; 101 | //移动前的角度 102 | items[index].anglePre = this.countDeg(items[index].x, items[index].y, items[index].tx, items[index].ty) 103 | console.log("移动前的角度", items[index].anglePre) 104 | //获取图片半径 105 | items[index].r = this.getDistancs(items[index].x, items[index].y, items[index].left, items[index].top)-20;//20是右下角移动图片到本图边缘的估计值,因为这个获取半径的方法跟手指的位置有关 106 | console.log("半径", items[index].r); 107 | }, 108 | 109 | // 触摸移动事件 110 | touchMove: function (e) { 111 | //记录移动后的位置 112 | items[index]._tx = e.touches[0].clientX; 113 | items[index]._ty = e.touches[0].clientY; 114 | //移动的点到圆心的距离 115 | var width = wx.getSystemInfoSync().windowWidth 116 | items[index].disPtoO = this.getDistancs(items[index].x, items[index].y, items[index]._tx - width * 0.125, items[index]._ty - 10) 117 | 118 | items[index].scale = items[index].disPtoO / items[index].r; //手指滑动的点到圆心的距离与半径的比值作为图片的放大比例 119 | items[index].oScale = 1 / items[index].scale;//图片放大响应的右下角按钮同比缩小 120 | 121 | //移动后位置的角度 122 | items[index].angleNext = this.countDeg(items[index].x, items[index].y, items[index]._tx, items[index]._ty) 123 | //角度差 124 | items[index].new_rotate = items[index].angleNext - items[index].anglePre; 125 | 126 | //叠加的角度差 127 | items[index].rotate += items[index].new_rotate; 128 | items[index].angle = items[index].rotate; //赋值 129 | 130 | //用过移动后的坐标赋值为移动前坐标 131 | items[index].tx = e.touches[0].clientX; 132 | items[index].ty = e.touches[0].clientY; 133 | items[index].anglePre = this.countDeg(items[index].x, items[index].y, items[index].tx, items[index].ty) 134 | items[index].angle = items[index].anglePre-135; 135 | 136 | //赋值setData渲染 137 | this.setData({ 138 | "itemList": items 139 | }) 140 | }, 141 | deleteItem:function(e){ 142 | console.log("删除按钮:",e) 143 | for (let i = 0; i < items.length; i++) { 144 | 145 | 146 | if (e.currentTarget.dataset.id == items[i].id) { 147 | items.splice(i, 1); 148 | this.setData({ 149 | "itemList": items 150 | }) 151 | } 152 | } 153 | }, 154 | /** 155 | * 生命周期函数--监听页面加载 156 | */ 157 | onLoad: function (options) { 158 | // items = this.data.itemList; 159 | // this.setData({ 160 | // "itemList": items 161 | // }) 162 | }, 163 | changeImg:function(e){ 164 | 165 | 166 | var temDic = { 167 | id: e.currentTarget.dataset.index+1, 168 | image: e.currentTarget.dataset.url,//图片地址 169 | top: 100,//初始图片的位置 170 | left: 100, 171 | x: 150, //初始圆心位置,可再downImg之后又宽高和初始的图片位置得出 172 | y: 150, 173 | scale: 1,//缩放比例 1为不缩放 174 | angle: 0,//旋转角度 175 | active: false, //判定点击状态 176 | width: 100, 177 | height: 100 178 | } 179 | items.push(temDic) 180 | this.setData({ 181 | "itemList": items 182 | }) 183 | }, 184 | /** 185 | * 生命周期函数--监听页面初次渲染完成 186 | */ 187 | onReady: function () { 188 | 189 | }, 190 | 191 | /** 192 | * 生命周期函数--监听页面显示 193 | */ 194 | onShow: function () { 195 | 196 | }, 197 | 198 | /** 199 | * 生命周期函数--监听页面隐藏 200 | */ 201 | onHide: function () { 202 | 203 | }, 204 | 205 | /** 206 | * 生命周期函数--监听页面卸载 207 | */ 208 | onUnload: function () { 209 | 210 | }, 211 | 212 | /** 213 | * 页面相关事件处理函数--监听用户下拉动作 214 | */ 215 | onPullDownRefresh: function () { 216 | 217 | }, 218 | 219 | /** 220 | * 页面上拉触底事件的处理函数 221 | */ 222 | onReachBottom: function () { 223 | 224 | }, 225 | 226 | /** 227 | * 用户点击右上角分享 228 | */ 229 | onShareAppMessage: function () { 230 | 231 | }, 232 | /* 233 | *参数1和2为图片圆心坐标 234 | *参数3和4为手点击的坐标 235 | *返回值为手点击的坐标到圆心的角度 236 | */ 237 | countDeg: function (cx, cy, pointer_x, pointer_y) { 238 | var ox = pointer_x - cx; 239 | var oy = pointer_y - cy; 240 | var to = Math.abs(ox / oy); 241 | var angle = Math.atan(to) / (2 * Math.PI) * 360;//鼠标相对于旋转中心的角度 242 | 243 | if (ox < 0 && oy < 0)//相对在左上角,第四象限,js中坐标系是从左上角开始的,这里的象限是正常坐标系 244 | { 245 | angle = -angle; 246 | } else if (ox <= 0 && oy >= 0)//左下角,3象限 247 | { 248 | angle = -(180 - angle) 249 | } else if (ox > 0 && oy < 0)//右上角,1象限 250 | { 251 | angle = angle; 252 | } else if (ox > 0 && oy > 0)//右下角,2象限 253 | { 254 | angle = 180 - angle; 255 | } 256 | 257 | return angle; 258 | }, 259 | //计算触摸点到圆心的距离 260 | getDistancs(cx, cy, pointer_x, pointer_y) { 261 | var ox = pointer_x - cx; 262 | var oy = pointer_y - cy; 263 | return Math.sqrt( 264 | ox * ox + oy * oy 265 | ); 266 | } 267 | }) -------------------------------------------------------------------------------- /pages/example/example.js: -------------------------------------------------------------------------------- 1 | // pages/uploadImg/uploadImg.js 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | imgUrl: '../../images/example.png', 9 | stickers: ['../../images/sticker/1.png', 10 | '../../images/sticker/2.png', 11 | '../../images/sticker/3.png', 12 | '../../images/sticker/4.png', 13 | '../../images/sticker/5.png', 14 | '../../images/sticker/6.png', 15 | '../../images/sticker/7.png', 16 | '../../images/sticker/8.png', 17 | '../../images/sticker/9.png', 18 | '../../images/sticker/10.png', 19 | '../../images/sticker/11.png'], 20 | 21 | x: 160, 22 | y: 50, 23 | chosedImg: false, 24 | stv: { 25 | offsetX: 160, 26 | offsetY: 50, 27 | zoom: false, //是否缩放状态 28 | distance: 0, //两指距离 29 | scale: 1, //缩放倍数 30 | width: 50, 31 | height: 50, 32 | }, 33 | }, 34 | // 第一张贴图触摸开始 35 | touchstartCallback: function (e) { 36 | //console.log('touchstartCallback'); 37 | //console.log(e); 38 | if (e.touches.length === 1) { 39 | let { clientX, clientY } = e.touches[0]; 40 | this.startX = clientX; 41 | this.startY = clientY; 42 | this.touchStartEvent = e.touches; 43 | } else { 44 | let xMove = e.touches[1].clientX - e.touches[0].clientX; 45 | let yMove = e.touches[1].clientY - e.touches[0].clientY; 46 | let distance = Math.sqrt(xMove * xMove + yMove * yMove); 47 | this.setData({ 48 | 'stv.distance': distance, 49 | 'stv.zoom': true, //缩放状态 50 | }) 51 | } 52 | }, 53 | 54 | // 第一张贴图触摸移动中 55 | touchmoveCallback: function (e) { 56 | //console.log('touchmoveCallback'); 57 | //console.log(e); 58 | if (e.touches.length === 1) { 59 | //单指移动 60 | if (this.data.stv.zoom) { 61 | //缩放状态,不处理单指 62 | return; 63 | } 64 | let { clientX, clientY } = e.touches[0]; 65 | let offsetX = clientX - this.startX; 66 | let offsetY = clientY - this.startY; 67 | this.startX = clientX; 68 | this.startY = clientY; 69 | let { stv } = this.data; 70 | stv.offsetX += offsetX; 71 | stv.offsetY += offsetY; 72 | stv.offsetLeftX = -stv.offsetX; 73 | stv.offsetLeftY = -stv.offsetLeftY; 74 | var nowWidth = this.data.stv.width; 75 | var maxoffsetX = 320 - nowWidth; 76 | var nowHeight = this.data.stv.height; 77 | var maxoffsetY = 178.125 - nowHeight; 78 | 79 | if (stv.offsetX > maxoffsetX) { 80 | stv.offsetX = maxoffsetX; 81 | } else if (stv.offsetX < 0) { 82 | stv.offsetX = 0; 83 | } 84 | if (stv.offsetY > maxoffsetY) { 85 | stv.offsetY = maxoffsetY; 86 | } else if (stv.offsetY < 0) { 87 | stv.offsetY = 0; 88 | } 89 | this.setData({ 90 | stv: stv 91 | }); 92 | 93 | } else { 94 | //双指缩放 95 | let xMove = e.touches[1].clientX - e.touches[0].clientX; 96 | let yMove = e.touches[1].clientY - e.touches[0].clientY; 97 | let distance = Math.sqrt(xMove * xMove + yMove * yMove); 98 | 99 | let distanceDiff = distance - this.data.stv.distance; 100 | let newScale = this.data.stv.scale + 0.005 * distanceDiff; 101 | if (newScale < 0.5) { 102 | newScale = 0.5; 103 | } 104 | if (newScale > 4) { 105 | newScale = 4; 106 | } 107 | let newWidth = newScale * 50; 108 | let newHeight = newScale * 50; 109 | 110 | this.setData({ 111 | 'stv.distance': distance, 112 | 'stv.scale': newScale, 113 | 'stv.width': newWidth, 114 | 'stv.height': newWidth, 115 | }) 116 | //console.log(this.data.stv.scale) 117 | } 118 | }, 119 | 120 | // 第一张贴图触摸结束 121 | touchendCallback: function (e) { 122 | // console.log('touchendCallback'); 123 | //console.log(e); 124 | if (e.touches.length === 0) { 125 | this.setData({ 126 | 'stv.zoom': false, //重置缩放状态 127 | }) 128 | } 129 | }, 130 | 131 | //切换贴纸 132 | changeImg: function (e) { 133 | var $img = e.currentTarget.dataset.url; 134 | var chosedImg = this.data.chosedImg; 135 | this.setData({ 136 | x: 160, 137 | y: 50, 138 | stv: { 139 | offsetX: 160, 140 | offsetY: 50, 141 | zoom: false, //是否缩放状态 142 | distance: 0, //两指距离 143 | scale: 1, //缩放倍数 144 | width: 50, 145 | height: 50, 146 | }, 147 | chosedImg: $img, 148 | }) 149 | }, 150 | 151 | //取消贴纸 152 | cancel: function () { 153 | this.setData({ 154 | chosedImg: false, 155 | x: 150, 156 | y: 75, 157 | stv: { 158 | offsetX: 75, 159 | offsetY: 75, 160 | zoom: false, //是否缩放状态 161 | distance: 0, //两指距离 162 | scale: 1, //缩放倍数 163 | width: 50, 164 | height: 50, 165 | } 166 | }) 167 | }, 168 | 169 | //将贴纸绘制到canvas的固定 170 | setSticker: function (context) { 171 | var Sticker = this.data.chosedImg; 172 | var newtop = this.data.stv.offsetX * 2; 173 | var newleft = this.data.stv.offsetY * 2; 174 | var newswidth = this.data.stv.width * 2; 175 | var newheight = this.data.stv.height * 2; 176 | context.drawImage(Sticker, newtop, newleft, newswidth, newheight) 177 | context.save(); 178 | context.restore(); 179 | context.stroke(); 180 | }, 181 | 182 | //将canvas转换为图片保存到本地,然后将图片路径传给image图片的src 183 | createNewImg: function (imgUrl) { 184 | var that = this; 185 | var chosedImg = this.data.chosedImg; 186 | var path = imgUrl; 187 | var context = wx.createCanvasContext('mycanvas'); 188 | 189 | //防止锯齿,绘的图片是所需图片的两倍 190 | context.drawImage(path, 0, 0, 640, 356.266); 191 | 192 | //如果有贴纸则绘制贴纸 193 | if (chosedImg) { 194 | this.setSticker(context); 195 | } 196 | //绘制图片 197 | context.draw(); 198 | 199 | //将生成好的图片保存到本地,需要延迟一会,绘制期间耗时 200 | setTimeout(function () { 201 | wx.canvasToTempFilePath({ 202 | canvasId: 'mycanvas', 203 | success: function (res) { 204 | var tempFilePath = res.tempFilePath; 205 | console.log(tempFilePath); 206 | that.setData({ 207 | imagePath: tempFilePath, 208 | }) 209 | wx.navigateTo({ 210 | url: '/pages/detail/detail?path=' + tempFilePath, 211 | }) 212 | }, 213 | fail: function (res) { 214 | console.log(res); 215 | }, 216 | complete:function(e){ 217 | wx.hideLoading(); 218 | } 219 | }); 220 | }, 200); 221 | 222 | }, 223 | 224 | //点击保存按钮 225 | save: function () { 226 | var that = this; 227 | wx.showLoading({ 228 | title: '创建中...', 229 | duration: 10000, 230 | }) 231 | var imgUrl = that.data.imgUrl 232 | that.createNewImg(imgUrl); 233 | }, 234 | 235 | /** 236 | * 生命周期函数--监听页面加载 237 | */ 238 | onLoad: function () { 239 | }, 240 | 241 | /** 242 | * 生命周期函数--监听页面初次渲染完成 243 | */ 244 | onReady: function () { 245 | 246 | }, 247 | 248 | /** 249 | * 生命周期函数--监听页面显示 250 | */ 251 | onShow: function () { 252 | 253 | }, 254 | 255 | /** 256 | * 生命周期函数--监听页面隐藏 257 | */ 258 | onHide: function () { 259 | 260 | }, 261 | 262 | /** 263 | * 生命周期函数--监听页面卸载 264 | */ 265 | onUnload: function () { 266 | 267 | }, 268 | 269 | /** 270 | * 页面相关事件处理函数--监听用户下拉动作 271 | */ 272 | onPullDownRefresh: function () { 273 | 274 | }, 275 | 276 | /** 277 | * 页面上拉触底事件的处理函数 278 | */ 279 | onReachBottom: function () { 280 | 281 | }, 282 | 283 | /** 284 | * 用户点击右上角分享 285 | */ 286 | onShareAppMessage: function () { 287 | 288 | } 289 | }) --------------------------------------------------------------------------------