├── 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 | 7 | 选择图片 8 | 旋转 9 | 打印 10 | 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); --------------------------------------------------------------------------------