├── app.wxss
├── README.md
├── pages
├── logs
│ ├── logs.json
│ ├── logs.wxss
│ ├── logs.wxml
│ └── logs.js
└── index
│ ├── index.json
│ ├── index.wxml
│ ├── index.wxss
│ └── index.js
├── component
└── cropper
│ ├── cropper.json
│ ├── cropper.wxss
│ ├── cropper.wxml
│ └── cropper.js
├── app.js
├── app.json
├── utils
└── util.js
└── project.config.json
/app.wxss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 小程序图片剪裁组件
2 |
--------------------------------------------------------------------------------
/pages/logs/logs.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "查看启动日志"
3 | }
--------------------------------------------------------------------------------
/component/cropper/cropper.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true,
3 | "usingComponents": {}
4 | }
--------------------------------------------------------------------------------
/pages/index/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {
3 | "cropper": "../../component/cropper/cropper"
4 | }
5 | }
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | //app.js
2 | App({
3 | onLaunch: function () {
4 |
5 | },
6 | globalData: {
7 | userInfo: null
8 | }
9 | })
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
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 |
--------------------------------------------------------------------------------
/pages/index/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | choose Img
7 |
--------------------------------------------------------------------------------
/pages/index/index.wxss:
--------------------------------------------------------------------------------
1 | /**index.wxss**/
2 | page {
3 | width: 100%;
4 | height: 100%;
5 | }
6 | .container {
7 | width: 100%;
8 | height: 100%;
9 | background: #eee;
10 | overflow: hidden;
11 | }
12 | .cropper {
13 | width: 100%;
14 | height: 100%;
15 | }
16 | .img {
17 | margin: 20rpx auto;
18 | display: block;
19 | background: #fff;
20 | }
21 | .choose-img {
22 | width: 40%;
23 | text-align: center;
24 | padding: 30rpx;
25 | border: 1px solid #fff;
26 | margin: 20rpx auto;
27 | background: #000;
28 | color: #fff;
29 | }
--------------------------------------------------------------------------------
/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 | "packOptions": {
4 | "ignore": []
5 | },
6 | "setting": {
7 | "urlCheck": true,
8 | "es6": true,
9 | "postcss": true,
10 | "minified": true,
11 | "newFeature": true
12 | },
13 | "compileType": "miniprogram",
14 | "libVersion": "2.1.1",
15 | "appid": "wxb6123ae0700d47e3",
16 | "projectname": "qwer",
17 | "condition": {
18 | "search": {
19 | "current": -1,
20 | "list": []
21 | },
22 | "conversation": {
23 | "current": -1,
24 | "list": []
25 | },
26 | "game": {
27 | "currentL": -1,
28 | "list": []
29 | },
30 | "miniprogram": {
31 | "current": -1,
32 | "list": []
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/pages/index/index.js:
--------------------------------------------------------------------------------
1 | //index.js
2 | //获取应用实例
3 | const app = getApp()
4 |
5 | Page({
6 | data: {
7 | ratio: 102/152,
8 | originUrl: '',
9 | cropperResult: ''
10 | },
11 | uploadTap() {
12 | let _this = this
13 | wx.chooseImage({
14 | count: 1, // 默认9
15 | sizeType: ['original'], // 可以指定是原图还是压缩图,默认二者都有
16 | sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
17 | success(res) {
18 | _this.setData({
19 | originUrl: res.tempFilePaths[0],
20 | cropperResult: ''
21 | })
22 | }
23 | })
24 | },
25 | getCropperImg(e) {
26 | this.setData({
27 | originUrl: '',
28 | cropperResult: e.detail.url
29 | })
30 | }
31 | })
32 |
--------------------------------------------------------------------------------
/component/cropper/cropper.wxss:
--------------------------------------------------------------------------------
1 | .container {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | background: #000;
6 | }
7 | .img {
8 | position: absolute;
9 | top: 5%;
10 | left: 50%;
11 | transform: translateX(-50%);
12 | overflow: hidden;
13 | background: #eee;
14 | }
15 | .img image {
16 | height:400px;
17 | }
18 | .imgcrop {
19 | position: absolute;
20 | left: -50000rpx;
21 | top: -500000rpx;
22 | }
23 | .footer {
24 | position: absolute;
25 | width: 100%;
26 | height: 110rpx;
27 | color: #fff;
28 | background: #000;
29 | bottom: 0;
30 | display: flex;
31 | align-items: center;
32 | justify-content: space-around;
33 | }
34 | .footer view {
35 | width: 30%;
36 | text-align: center;
37 | }
38 | .background {
39 | width: 100%;
40 | height: 100%;
41 | position: absolute;
42 | top: 0;
43 | z-index: -1;
44 | }
--------------------------------------------------------------------------------
/component/cropper/cropper.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/component/cropper/cropper.js:
--------------------------------------------------------------------------------
1 | // component/cropper/cropper.js
2 | const device = wx.getSystemInfoSync();
3 | var twoPoint = {
4 | x1: 0,
5 | y1: 0,
6 | x2: 0,
7 | y2: 0
8 | }
9 |
10 | Component({
11 | /**
12 | * 组件的属性列表
13 | */
14 | properties: {
15 | ratio: {
16 | type: Number,
17 | observer: function (newVal, oldVal) {
18 | this.setData({
19 | width: device.windowWidth * 0.8,
20 | height: device.windowWidth * 0.8 / newVal
21 | })
22 | }
23 | },
24 | url: {
25 | type: String,
26 | observer ( newVal, oldVal ) {
27 | this.initImg( newVal )
28 | }
29 | }
30 | },
31 |
32 | /**
33 | * 组件的初始数据
34 | */
35 | data: {
36 | width: device.windowWidth * 0.8, //剪裁框的宽度
37 | height: device.windowWidth * 0.8 / (102 / 152), //剪裁框的长度
38 | originImg: null, //存放原图信息
39 | stv: {
40 | offsetX: 0, //剪裁图片左上角坐标x
41 | offsetY: 0, //剪裁图片左上角坐标y
42 | zoom: false, //是否缩放状态
43 | distance: 0, //两指距离
44 | scale: 1, //缩放倍数
45 | rotate: 0 //旋转角度
46 | },
47 | },
48 |
49 | /**
50 | * 组件的方法列表
51 | */
52 | methods: {
53 | uploadTap() {
54 | let _this = this
55 | wx.chooseImage({
56 | count: 1, // 默认9
57 | sizeType: ['original'], // 可以指定是原图还是压缩图,默认二者都有
58 | sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
59 | success(res) {
60 | _this.initImg( res.tempFilePaths[0]);
61 | }
62 | })
63 | },
64 | rotate() {
65 | let _this = this;
66 | _this.setData({
67 | 'stv.rotate': _this.data.stv.rotate % 90 == 0 ? _this.data.stv.rotate = _this.data.stv.rotate + 90 : _this.data.stv.rotate = 0
68 | })
69 | },
70 | // canvas剪裁图片
71 | cropperImg() {
72 | wx.showLoading({
73 | title: 'loading',
74 | mask: true
75 | })
76 | let _this = this;
77 | let ctx = wx.createCanvasContext('imgcrop',this);
78 | let cropData = _this.data.stv;
79 | ctx.save();
80 | // 缩放偏移值
81 | let x = (_this.data.originImg.width - _this.data.originImg.width * cropData.scale) / 2;
82 | let y = (_this.data.originImg.height - _this.data.originImg.height * cropData.scale) / 2;
83 |
84 | //画布中点坐标转移到图片中心
85 | let movex = (cropData.offsetX + x) * 2 + _this.data.originImg.width * cropData.scale;
86 | let movey = (cropData.offsetY + y) * 2 + _this.data.originImg.height * cropData.scale;
87 | ctx.translate(movex, movey);
88 | ctx.rotate(cropData.rotate * Math.PI / 180);
89 | ctx.translate(-movex, -movey);
90 |
91 | ctx.drawImage(_this.data.originImg.url, (cropData.offsetX + x) * 2, (cropData.offsetY + y) * 2, _this.data.originImg.width * 2 * cropData.scale, _this.data.originImg.height * 2 * cropData.scale);
92 | ctx.restore();
93 | ctx.draw(false, ()=> {
94 | wx.canvasToTempFilePath({
95 | canvasId: 'imgcrop',
96 | success(response) {
97 | console.log(response.tempFilePath);
98 | _this.triggerEvent("getCropperImg", { url: response.tempFilePath })
99 | wx.hideLoading();
100 | },
101 | fail( e ) {
102 | console.log( e );
103 | wx.hideLoading();
104 | wx.showToast({
105 | title: '生成图片失败',
106 | icon: 'none'
107 | })
108 | }
109 | }, this)
110 | });
111 | },
112 |
113 | initImg(url) {
114 | let _this = this;
115 | wx.getImageInfo({
116 | src: url,
117 | success(resopne) {
118 | console.log(resopne);
119 | let innerAspectRadio = resopne.width / resopne.height;
120 |
121 | if (innerAspectRadio < _this.data.width / _this.data.height) {
122 | _this.setData({
123 | originImg: {
124 | url: url,
125 | width: _this.data.width,
126 | height: _this.data.width / innerAspectRadio
127 | },
128 | stv: {
129 | offsetX: 0,
130 | offsetY: 0 - Math.abs((_this.data.height - _this.data.width / innerAspectRadio) / 2),
131 | zoom: false, //是否缩放状态
132 | distance: 0, //两指距离
133 | scale: 1, //缩放倍数
134 | rotate: 0
135 | },
136 | })
137 | } else {
138 | _this.setData({
139 | originImg: {
140 | url: url,
141 | height: _this.data.height,
142 | width: _this.data.height * innerAspectRadio
143 | },
144 | stv: {
145 | offsetX: 0 - Math.abs((_this.data.width - _this.data.height * innerAspectRadio) / 2),
146 | offsetY: 0,
147 | zoom: false, //是否缩放状态
148 | distance: 0, //两指距离
149 | scale: 1, //缩放倍数
150 | rotate: 0
151 | }
152 | })
153 | }
154 | }
155 | })
156 | },
157 | //事件处理函数
158 | touchstartCallback: function (e) {
159 | if (e.touches.length === 1) {
160 | let { clientX, clientY } = e.touches[0];
161 | this.startX = clientX;
162 | this.startY = clientY;
163 | this.touchStartEvent = e.touches;
164 | } else {
165 | let xMove = e.touches[1].clientX - e.touches[0].clientX;
166 | let yMove = e.touches[1].clientY - e.touches[0].clientY;
167 | let distance = Math.sqrt(xMove * xMove + yMove * yMove);
168 | twoPoint.x1 = e.touches[0].pageX * 2
169 | twoPoint.y1 = e.touches[0].pageY * 2
170 | twoPoint.x2 = e.touches[1].pageX * 2
171 | twoPoint.y2 = e.touches[1].pageY * 2
172 | this.setData({
173 | 'stv.distance': distance,
174 | 'stv.zoom': true, //缩放状态
175 | })
176 | }
177 | },
178 | //图片手势动态缩放
179 | touchmoveCallback: function (e) {
180 | let _this = this
181 | fn(_this, e)
182 | },
183 | touchendCallback: function (e) {
184 | //触摸结束
185 | if (e.touches.length === 0) {
186 | this.setData({
187 | 'stv.zoom': false, //重置缩放状态
188 | })
189 | }
190 | }
191 | }
192 | })
193 |
194 | /**
195 | * fn:延时调用函数
196 | * delay:延迟多长时间
197 | * mustRun:至少多长时间触发一次
198 | */
199 | var throttle = function (fn, delay, mustRun) {
200 | var timer = null,
201 | previous = null;
202 |
203 | return function () {
204 | var now = +new Date(),
205 | context = this,
206 | args = arguments;
207 | if (!previous) previous = now;
208 | var remaining = now - previous;
209 | if (mustRun && remaining >= mustRun) {
210 | fn.apply(context, args);
211 | previous = now;
212 | } else {
213 | clearTimeout(timer);
214 | timer = setTimeout(function () {
215 | fn.apply(context, args);
216 | }, delay);
217 |
218 | }
219 | }
220 | }
221 |
222 | var touchMove = function (_this, e) {
223 | //触摸移动中
224 | if (e.touches.length === 1) {
225 | //单指移动
226 | if (_this.data.stv.zoom) {
227 | //缩放状态,不处理单指
228 | return;
229 | }
230 | let { clientX, clientY } = e.touches[0];
231 | let offsetX = clientX - _this.startX;
232 | let offsetY = clientY - _this.startY;
233 | _this.startX = clientX;
234 | _this.startY = clientY;
235 | let { stv } = _this.data;
236 | stv.offsetX += offsetX;
237 | stv.offsetY += offsetY;
238 | stv.offsetLeftX = -stv.offsetX;
239 | stv.offsetLeftY = -stv.offsetLeftY;
240 | _this.setData({
241 | stv: stv
242 | });
243 |
244 | } else if (e.touches.length === 2) {
245 | //计算旋转
246 | let preTwoPoint = JSON.parse(JSON.stringify(twoPoint))
247 | twoPoint.x1 = e.touches[0].pageX * 2
248 | twoPoint.y1 = e.touches[0].pageY * 2
249 | twoPoint.x2 = e.touches[1].pageX * 2
250 |
251 | function vector(x1, y1, x2, y2) {
252 | this.x = x2 - x1;
253 | this.y = y2 - y1;
254 | };
255 |
256 | //计算点乘
257 | function calculateVM(vector1, vector2) {
258 | return (vector1.x * vector2.x + vector1.y * vector2.y) / (Math.sqrt(vector1.x * vector1.x + vector1.y * vector1.y) * Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y));
259 |
260 | }
261 | //计算叉乘
262 | function calculateVC(vector1, vector2) {
263 | return (vector1.x * vector2.y - vector2.x * vector1.y) > 0 ? 1 : -1;
264 | }
265 |
266 | let vector1 = new vector(preTwoPoint.x1, preTwoPoint.y1, preTwoPoint.x2, preTwoPoint.y2);
267 | let vector2 = new vector(twoPoint.x1, twoPoint.y1, twoPoint.x2, twoPoint.y2);
268 | let cos = calculateVM(vector1, vector2);
269 | let angle = Math.acos(cos) * 180 / Math.PI;
270 |
271 | let direction = calculateVC(vector1, vector2);
272 | let _allDeg = direction * angle;
273 |
274 |
275 | // 双指缩放
276 | let xMove = e.touches[1].clientX - e.touches[0].clientX;
277 | let yMove = e.touches[1].clientY - e.touches[0].clientY;
278 | let distance = Math.sqrt(xMove * xMove + yMove * yMove);
279 |
280 | let distanceDiff = distance - _this.data.stv.distance;
281 | let newScale = _this.data.stv.scale + 0.005 * distanceDiff;
282 |
283 | if (Math.abs(_allDeg) > 1) {
284 | _this.setData({
285 | 'stv.rotate': _this.data.stv.rotate + _allDeg
286 | })
287 | } else {
288 | //双指缩放
289 | let xMove = e.touches[1].clientX - e.touches[0].clientX;
290 | let yMove = e.touches[1].clientY - e.touches[0].clientY;
291 | let distance = Math.sqrt(xMove * xMove + yMove * yMove);
292 |
293 | let distanceDiff = distance - _this.data.stv.distance;
294 | let newScale = _this.data.stv.scale + 0.005 * distanceDiff;
295 | if (newScale < 0.2 || newScale > 2.5) {
296 | return;
297 | }
298 | _this.setData({
299 | 'stv.distance': distance,
300 | 'stv.scale': newScale,
301 | })
302 | }
303 | } else {
304 | return;
305 | }
306 | }
307 |
308 | //为touchMove函数节流
309 | const fn = throttle(touchMove, 10, 10);
--------------------------------------------------------------------------------