├── app.wxss ├── utils └── util.js ├── images ├── 1.png ├── o.png ├── x.png └── bg.png ├── README.md.bak ├── pages ├── logs │ ├── logs.json │ ├── logs.wxss │ ├── logs.wxml │ └── logs.js └── index │ ├── index.json │ ├── index.wxml │ ├── index.wxss │ └── index.js ├── app.js ├── sitemap.json ├── README.md ├── app.json └── project.config.json /app.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/util.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peng20017/wx-drop/HEAD/images/1.png -------------------------------------------------------------------------------- /images/o.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peng20017/wx-drop/HEAD/images/o.png -------------------------------------------------------------------------------- /images/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peng20017/wx-drop/HEAD/images/x.png -------------------------------------------------------------------------------- /README.md.bak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peng20017/wx-drop/HEAD/README.md.bak -------------------------------------------------------------------------------- /images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peng20017/wx-drop/HEAD/images/bg.png -------------------------------------------------------------------------------- /pages/logs/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "查看启动日志", 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll":true, 4 | "disableSwipeBack":true 5 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | onLaunch: function () { 4 | }, 5 | globalData: { 6 | userInfo: null 7 | } 8 | }) -------------------------------------------------------------------------------- /pages/logs/logs.wxss: -------------------------------------------------------------------------------- 1 | .log-list { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 40rpx; 5 | } 6 | .log-item { 7 | margin: 10rpx; 8 | } 9 | -------------------------------------------------------------------------------- /sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### wx-drop 2 | ### 实现了微信小程序内单手对图片进行拖动、缩放、旋转 3 | ### 可以根据此原理进一步升级到对各种组件的拖动、缩放、旋转,不单单是图片 4 | ### 把项目克隆到本地,放在微信开发工具就可以了 5 | ### 新添加的图片合成功能 6 | #### 特别需要注意的是:本项目中使用的图片都是本地图片,如果使用线上图片的话,需要先下载到本地再继续使用合成功能! 7 | -------------------------------------------------------------------------------- /pages/logs/logs.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{index + 1}}. {{log}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index", 4 | "pages/logs/logs" 5 | ], 6 | "window": { 7 | "backgroundTextStyle": "light", 8 | "navigationBarBackgroundColor": "#fff", 9 | "navigationBarTitleText": "WeChat", 10 | "navigationBarTextStyle": "black" 11 | }, 12 | "sitemapLocation": "sitemap.json" 13 | } -------------------------------------------------------------------------------- /pages/logs/logs.js: -------------------------------------------------------------------------------- 1 | //logs.js 2 | const util = require('../../utils/util.js') 3 | 4 | Page({ 5 | data: { 6 | logs: [] 7 | }, 8 | onLoad: function () { 9 | this.setData({ 10 | logs: (wx.getStorageSync('logs') || []).map(log => { 11 | return util.formatTime(new Date(log)) 12 | }) 13 | }) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": true, 9 | "postcss": true, 10 | "minified": true, 11 | "newFeature": true, 12 | "autoAudits": false 13 | }, 14 | "compileType": "miniprogram", 15 | "libVersion": "2.6.2", 16 | "appid": "wx818bf4ccdd19bdb1", 17 | "projectname": "drop", 18 | "debugOptions": { 19 | "hidedInDevtools": [] 20 | }, 21 | "isGameTourist": false, 22 | "simulatorType": "wechat", 23 | "simulatorPluginLibVersion": {}, 24 | "condition": { 25 | "search": { 26 | "current": -1, 27 | "list": [] 28 | }, 29 | "conversation": { 30 | "current": -1, 31 | "list": [] 32 | }, 33 | "game": { 34 | "currentL": -1, 35 | "list": [] 36 | }, 37 | "miniprogram": { 38 | "current": -1, 39 | "list": [] 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | .bg { 3 | width: 100%; 4 | height: 100vh; 5 | } 6 | .contentWarp{ 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | top: 0; 11 | left: 0; 12 | bottom: 0; 13 | right: 0; 14 | margin: auto; 15 | /* background-color: #d1e3f1; */ 16 | } 17 | .touchWrap{ 18 | transform-origin: center; 19 | position: absolute; 20 | z-index: 100; 21 | } 22 | 23 | .imgWrap { 24 | box-sizing: border-box; 25 | width: 100%; 26 | transform-origin: center; 27 | float: left; 28 | border: 5rpx transparent dashed; 29 | } 30 | .imgWrap image { 31 | float: left; 32 | } 33 | .touchActive .x { 34 | display: block; 35 | } 36 | 37 | .touchActive .o { 38 | display: block; 39 | } 40 | 41 | .x { 42 | position: absolute; 43 | top: -25rpx; 44 | left: -25rpx; 45 | z-index: 500; 46 | display: none; 47 | width: 50rpx; 48 | height: 50rpx; 49 | overflow: hidden; 50 | font-weight: bold; 51 | color: #d1e3f1; 52 | } 53 | .o { 54 | position: absolute; 55 | bottom: -25rpx; 56 | right: -25rpx; 57 | width: 50rpx; 58 | height: 50rpx; 59 | text-align: center; 60 | display: none; 61 | overflow: hidden; 62 | font-weight: bold; 63 | color: #d1e3f1; 64 | } 65 | .active { 66 | background-color: rgb(78, 114, 151); 67 | } 68 | 69 | .active view { 70 | border: none; 71 | } 72 | .touchActive { 73 | /* border: 4rpx #fff dashed; */ 74 | z-index: 400; 75 | } 76 | .fixed { 77 | position: absolute; 78 | bottom: 0; 79 | left: 0; 80 | right: 0; 81 | margin: auto; 82 | } 83 | 84 | 85 | .canvasWrap { 86 | position: absolute; 87 | width: 100%; 88 | height: 100%; 89 | top: 0; 90 | left: 0; 91 | background-color: rgba(0, 0, 0, 0.6); 92 | z-index: 999; 93 | text-align: center; 94 | } 95 | 96 | .maskCanvas { 97 | position: absolute; 98 | left: -200%; 99 | top: 0; 100 | } 101 | 102 | .btn { 103 | font-size: 30rpx; 104 | color: #81b7c4; 105 | border: 3rpx solid #81b7c4; 106 | background-color: #fff; 107 | line-height: 90rpx; 108 | width: 50%; 109 | margin-top: 20rpx; 110 | height: 90rpx; 111 | } 112 | 113 | .btnView view { 114 | padding-bottom: 20rpx; 115 | } 116 | 117 | .hand { 118 | position: absolute; 119 | left: 100rpx; 120 | right: 0; 121 | margin: auto; 122 | z-index: 100; 123 | } 124 | 125 | .getUserInfoBtn { 126 | position: initial; 127 | border: none; 128 | background-color: none; 129 | } 130 | 131 | .getUserInfoBtn::after { 132 | border: none; 133 | } 134 | 135 | .btn_view { 136 | display: flex; 137 | padding: 20rpx; 138 | } 139 | 140 | .btn_view button { 141 | width: 300rpx; 142 | font-size: 30rpx; 143 | color: #81b7c4; 144 | border: 3rpx solid #81b7c4; 145 | background-color: #fff; 146 | line-height: 90rpx; 147 | } 148 | 149 | .resImg { 150 | width: 75%; 151 | margin-top: 10px; 152 | } -------------------------------------------------------------------------------- /pages/index/index.js: -------------------------------------------------------------------------------- 1 | let index = 0, 2 | items = [], 3 | flag = true, 4 | itemId = 1; 5 | const hCw = 1.62; // 图片宽高比 6 | const canvasPre = 1; // 展示的canvas占mask的百分比 7 | const maskCanvas = wx.createCanvasContext('maskCanvas'); 8 | Page({ 9 | /** 10 | * 页面的初始数据 11 | */ 12 | data: { 13 | itemList: [], 14 | }, 15 | 16 | /** 17 | * 生命周期函数--监听页面加载 18 | */ 19 | onLoad: function(options) { 20 | items = this.data.itemList; 21 | this.drawTime = 0 22 | this.setDropItem({ 23 | url: '/images/1.png' 24 | }); 25 | this.setDropItem({ 26 | url: '/images/1.png' 27 | }); 28 | wx.getSystemInfo({ 29 | success: sysData => { 30 | this.sysData = sysData 31 | this.setData({ 32 | canvasWidth: this.sysData.windowWidth * canvasPre, // 如果觉得不清晰的话,可以把所有组件、宽高放大一倍 33 | canvasHeight: this.sysData.windowWidth * canvasPre * hCw, 34 | }) 35 | } 36 | }) 37 | }, 38 | setDropItem(imgData) { 39 | let data = {} 40 | wx.getImageInfo({ 41 | src: imgData.url, 42 | success: res => { 43 | // 初始化数据 44 | data.width = res.width; //宽度 45 | data.height = res.height; //高度 46 | data.image = imgData.url; //地址 47 | data.id = ++itemId; //id 48 | data.top = 0; //top定位 49 | data.left = 0; //left定位 50 | //圆心坐标 51 | data.x = data.left + data.width / 2; 52 | data.y = data.top + data.height / 2; 53 | data.scale = 1; //scale缩放 54 | data.oScale = 1; //方向缩放 55 | data.rotate = 1; //旋转角度 56 | data.active = false; //选中状态 57 | console.log(data) 58 | items[items.length] = data; 59 | this.setData({ 60 | itemList: items 61 | }) 62 | } 63 | }) 64 | }, 65 | WraptouchStart: function(e) { 66 | for (let i = 0; i < items.length; i++) { 67 | items[i].active = false; 68 | if (e.currentTarget.dataset.id == items[i].id) { 69 | index = i; 70 | items[index].active = true; 71 | } 72 | } 73 | this.setData({ 74 | itemList: items 75 | }) 76 | 77 | items[index].lx = e.touches[0].clientX; 78 | items[index].ly = e.touches[0].clientY; 79 | 80 | console.log(items[index]) 81 | }, 82 | WraptouchMove(e) { 83 | if (flag) { 84 | flag = false; 85 | setTimeout(() => { 86 | flag = true; 87 | }, 100) 88 | } 89 | // console.log('WraptouchMove', e) 90 | items[index]._lx = e.touches[0].clientX; 91 | items[index]._ly = e.touches[0].clientY; 92 | 93 | items[index].left += items[index]._lx - items[index].lx; 94 | items[index].top += items[index]._ly - items[index].ly; 95 | items[index].x += items[index]._lx - items[index].lx; 96 | items[index].y += items[index]._ly - items[index].ly; 97 | 98 | items[index].lx = e.touches[0].clientX; 99 | items[index].ly = e.touches[0].clientY; 100 | console.log(items) 101 | this.setData({ 102 | itemList: items 103 | }) 104 | }, 105 | WraptouchEnd() { 106 | this.synthesis() 107 | }, 108 | oTouchStart(e) { 109 | //找到点击的那个图片对象,并记录 110 | for (let i = 0; i < items.length; i++) { 111 | items[i].active = false; 112 | if (e.currentTarget.dataset.id == items[i].id) { 113 | console.log('e.currentTarget.dataset.id', e.currentTarget.dataset.id) 114 | index = i; 115 | items[index].active = true; 116 | } 117 | } 118 | //获取作为移动前角度的坐标 119 | items[index].tx = e.touches[0].clientX; 120 | items[index].ty = e.touches[0].clientY; 121 | //移动前的角度 122 | items[index].anglePre = this.countDeg(items[index].x, items[index].y, items[index].tx, items[index].ty) 123 | //获取图片半径 124 | items[index].r = this.getDistancs(items[index].x, items[index].y, items[index].left, items[index].top); 125 | console.log(items[index]) 126 | }, 127 | oTouchMove: function(e) { 128 | if (flag) { 129 | flag = false; 130 | setTimeout(() => { 131 | flag = true; 132 | }, 100) 133 | } 134 | //记录移动后的位置 135 | items[index]._tx = e.touches[0].clientX; 136 | items[index]._ty = e.touches[0].clientY; 137 | //移动的点到圆心的距离 138 | items[index].disPtoO = this.getDistancs(items[index].x, items[index].y, items[index]._tx, items[index]._ty - 10) 139 | 140 | items[index].scale = items[index].disPtoO / items[index].r; 141 | items[index].oScale = 1 / items[index].scale; 142 | 143 | //移动后位置的角度 144 | items[index].angleNext = this.countDeg(items[index].x, items[index].y, items[index]._tx, items[index]._ty) 145 | //角度差 146 | items[index].new_rotate = items[index].angleNext - items[index].anglePre; 147 | 148 | //叠加的角度差 149 | items[index].rotate += items[index].new_rotate; 150 | items[index].angle = items[index].rotate; //赋值 151 | 152 | //用过移动后的坐标赋值为移动前坐标 153 | items[index].tx = e.touches[0].clientX; 154 | items[index].ty = e.touches[0].clientY; 155 | items[index].anglePre = this.countDeg(items[index].x, items[index].y, items[index].tx, items[index].ty) 156 | 157 | //赋值setData渲染 158 | this.setData({ 159 | itemList: items 160 | }) 161 | 162 | }, 163 | getDistancs(cx, cy, pointer_x, pointer_y) { 164 | var ox = pointer_x - cx; 165 | var oy = pointer_y - cy; 166 | return Math.sqrt( 167 | ox * ox + oy * oy 168 | ); 169 | }, 170 | /* 171 | *参数1和2为图片圆心坐标 172 | *参数3和4为手点击的坐标 173 | *返回值为手点击的坐标到圆心的角度 174 | */ 175 | countDeg: function(cx, cy, pointer_x, pointer_y) { 176 | var ox = pointer_x - cx; 177 | var oy = pointer_y - cy; 178 | var to = Math.abs(ox / oy); 179 | var angle = Math.atan(to) / (2 * Math.PI) * 360; 180 | // console.log("ox.oy:", ox, oy) 181 | if (ox < 0 && oy < 0) //相对在左上角,第四象限,js中坐标系是从左上角开始的,这里的象限是正常坐标系 182 | { 183 | angle = -angle; 184 | } else if (ox <= 0 && oy >= 0) //左下角,3象限 185 | { 186 | angle = -(180 - angle) 187 | } else if (ox > 0 && oy < 0) //右上角,1象限 188 | { 189 | angle = angle; 190 | } else if (ox > 0 && oy > 0) //右下角,2象限 191 | { 192 | angle = 180 - angle; 193 | } 194 | return angle; 195 | }, 196 | deleteItem: function(e) { 197 | let newList = []; 198 | for (let i = 0; i < items.length; i++) { 199 | if (e.currentTarget.dataset.id != items[i].id) { 200 | newList.push(items[i]) 201 | } 202 | } 203 | if (newList.length > 0) { 204 | newList[newList.length - 1].active = true; 205 | } 206 | items = newList; 207 | this.setData({ 208 | itemList: items 209 | }) 210 | }, 211 | openMask () { 212 | if (this.drawTime == 0) { 213 | this.synthesis() 214 | } 215 | this.setData({ 216 | showCanvas: true 217 | }) 218 | }, 219 | synthesis() { // 合成图片 220 | this.drawTime = this.drawTime + 1 221 | console.log('合成图片') 222 | maskCanvas.save(); 223 | maskCanvas.beginPath(); 224 | //一张白图 可以不画 225 | maskCanvas.setFillStyle('#fff'); 226 | maskCanvas.fillRect(0, 0, this.sysData.windowWidth, this.data.canvasHeight) 227 | maskCanvas.closePath(); 228 | maskCanvas.stroke(); 229 | 230 | //画背景 hCw 为 1.62 背景图的高宽比 231 | maskCanvas.drawImage('/images/bg.png', 0, 0, this.data.canvasWidth, this.data.canvasHeight); 232 | /* 233 | num为canvas内背景图占canvas的百分比,若全背景num =1 234 | prop值为canvas内背景的宽度与可移动区域的宽度的比,如一致,则prop =1; 235 | */ 236 | //画组件 237 | const num = 1, 238 | prop = 1; 239 | items.forEach((currentValue, index) => { 240 | maskCanvas.save(); 241 | maskCanvas.translate(this.data.canvasWidth * (1 - num) / 2, 0); 242 | maskCanvas.beginPath(); 243 | maskCanvas.translate(currentValue.x * prop, currentValue.y * prop); //圆心坐标 244 | maskCanvas.rotate(currentValue.angle * Math.PI / 180); 245 | maskCanvas.translate(-(currentValue.width * currentValue.scale * prop / 2), -(currentValue.height * currentValue.scale * prop / 2)) 246 | maskCanvas.drawImage(currentValue.image, 0, 0, currentValue.width * currentValue.scale * prop, currentValue.height * currentValue.scale * prop); 247 | maskCanvas.restore(); 248 | }) 249 | maskCanvas.draw(false, (e) => { 250 | wx.canvasToTempFilePath({ 251 | canvasId: 'maskCanvas', 252 | success: res => { 253 | console.log('draw success') 254 | console.log(res.tempFilePath) 255 | this.setData({ 256 | canvasTemImg: res.tempFilePath 257 | }) 258 | } 259 | }, this) 260 | }) 261 | }, 262 | disappearCanvas() { 263 | this.setData({ 264 | showCanvas: false 265 | }) 266 | }, 267 | saveImg: function() { 268 | wx.saveImageToPhotosAlbum({ 269 | filePath: this.data.canvasTemImg, 270 | success: res => { 271 | wx.showToast({ 272 | title: '保存成功', 273 | icon: "success" 274 | }) 275 | }, 276 | fail: res => { 277 | console.log(res) 278 | wx.openSetting({ 279 | success: settingdata => { 280 | console.log(settingdata) 281 | if (settingdata.authSetting['scope.writePhotosAlbum']) { 282 | console.log('获取权限成功,给出再次点击图片保存到相册的提示。') 283 | } else { 284 | console.log('获取权限失败,给出不给权限就无法正常使用的提示') 285 | } 286 | }, 287 | fail: error => { 288 | console.log(error) 289 | } 290 | }) 291 | wx.showModal({ 292 | title: '提示', 293 | content: '保存失败,请确保相册权限已打开', 294 | }) 295 | } 296 | }) 297 | } 298 | }) --------------------------------------------------------------------------------