├── 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 |
22 |
23 |
24 |
25 |
26 |
27 |
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 | })
--------------------------------------------------------------------------------