├── README.md ├── app.js ├── app.json ├── app.wxss ├── assets └── test.jpg ├── pages └── cropper │ ├── cropper-example.js │ ├── cropper-example.json │ ├── cropper-example.wxml │ ├── cropper-example.wxss │ ├── cropper.js │ ├── cropper.json │ ├── cropper.wxml │ └── cropper.wxss ├── project.config.json ├── screenshot.gif ├── sitemap.json └── utils └── util.js /README.md: -------------------------------------------------------------------------------- 1 | # wx-cropper 2 | 通用微信小程序图片裁剪组件,简单易用,帮助快速开发图片裁剪功能,支持自定义约束宽高,适用于头像或其他需要特定长宽比图片的场景。 3 | # 裁剪效果预览图 4 | ![image](https://github.com/light-wind/wx-cropper/blob/master/screenshot.gif) 5 | 6 | # 用法 7 | 下载源码,pages/cropper/目录拷贝到项目中。 8 | ## 1、在json文件中添加cropper组件 9 | ``` 10 | { 11 | "usingComponents": { 12 | "cropper":"/pages/cropper/cropper" 13 | }, 14 | "disableScroll": true 15 | } 16 | ``` 17 | 18 | ## 2、wxml页面文件中 19 | ``` 20 | 21 | 22 | 23 | 24 | 25 | 取消 26 | 确定 27 | 28 | 29 | 30 | ``` 31 | 32 | ## 3、在onLoad中初始化组件 33 | ``` 34 | cropper = this.selectComponent('#cropper'); 35 | cropper.fnInit({ 36 | imagePath:'/assets/test.jpg', //*必填 37 | debug: true, //可选。是否启用调试,默认值为false。true:打印过程日志;false:关闭过程日志 38 | outputFileType: 'jpg', //可选。目标文件的类型。默认值为jpg,jpg:输出jpg格式图片;png:输出png格式图片 39 | quality: 1, //可选。图片的质量。默认值为1,即最高质量。目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。 40 | aspectRatio: 1.25, //可选。裁剪的宽高比,默认null,即不限制剪裁宽高比。aspectRatio需大于0 41 | minBoxWidthRatio: 0.2, //可选。最小剪裁尺寸与原图尺寸的比率,默认0.15,即宽度最小剪裁到原图的0.15宽。 42 | minBoxHeightRatio: 0.2, //可选。同minBoxWidthRatio,当设置aspectRatio时,minBoxHeight值设置无效。minBoxHeight值由minBoxWidth 和 aspectRatio自动计算得到。 43 | initialBoxWidthRatio: 0.6, //可选。剪裁框初始大小比率。默认值0.6,即剪裁框默认宽度为图片宽度的0.6倍。 44 | initialBoxHeightRatio: 0.6 //可选。同initialBoxWidthRatio,当设置aspectRatio时,initialBoxHeightRatio值设置无效。initialBoxHeightRatio值由initialBoxWidthRatio 和 aspectRatio自动计算得到。 45 | }); 46 | ``` 47 | 48 | ## 4、在点击事件中裁剪图片,获取裁剪后的图片路径 49 | ``` 50 | cropper.fnCrop({ 51 | //裁剪成功的回调 52 | success:function(res){ 53 | console.log(res) 54 | //生成文件的临时路径 55 | console.log(res.tempFilePath); 56 | wx.previewImage({ 57 | urls: [res.tempFilePath], 58 | }) 59 | }, 60 | //裁剪失败的回调 61 | fail:function(res){ 62 | console.log(res); 63 | }, 64 | //裁剪结束的回调 65 | complete:function(){ 66 | //complete 67 | } 68 | }); 69 | ``` 70 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | 4 | 5 | 6 | }) -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/cropper/cropper-example" 4 | 5 | ], 6 | "window": { 7 | "backgroundTextStyle": "light", 8 | "navigationBarBackgroundColor": "#fff", 9 | "navigationBarTitleText": "WeChat", 10 | "navigationBarTextStyle": "black" 11 | }, 12 | "sitemapLocation": "sitemap.json" 13 | } -------------------------------------------------------------------------------- /app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | .container { 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-between; 8 | padding: 200rpx 0; 9 | box-sizing: border-box; 10 | } 11 | -------------------------------------------------------------------------------- /assets/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-wind/wx-cropper/757e3e9743e7dd994274d07ec45f86cd22977f7f/assets/test.jpg -------------------------------------------------------------------------------- /pages/cropper/cropper-example.js: -------------------------------------------------------------------------------- 1 | 2 | let cropper = null; 3 | 4 | Page({ 5 | 6 | 7 | data: { 8 | 9 | }, 10 | 11 | 12 | ////////////// init ///////////////////////// 13 | onLoad: function (options) { 14 | 15 | cropper = this.selectComponent('#cropper'); 16 | cropper.fnInit({ 17 | imagePath:'/assets/test.jpg', //*必填 18 | debug: true, //可选。是否启用调试,默认值为false。true:打印过程日志;false:关闭过程日志 19 | outputFileType: 'jpg', //可选。目标文件的类型。默认值为jpg,jpg:输出jpg格式图片;png:输出png格式图片 20 | quality: 1, //可选。图片的质量。默认值为1,即最高质量。目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。 21 | aspectRatio: 1.25, //可选。裁剪的宽高比,默认null,即不限制剪裁宽高比。aspectRatio需大于0 22 | minBoxWidthRatio: 0.2, //可选。最小剪裁尺寸与原图尺寸的比率,默认0.15,即宽度最小剪裁到原图的0.15宽。 23 | minBoxHeightRatio: 0.2, //可选。同minBoxWidthRatio,当设置aspectRatio时,minBoxHeight值设置无效。minBoxHeight值由minBoxWidth 和 aspectRatio自动计算得到。 24 | initialBoxWidthRatio: 0.6, //可选。剪裁框初始大小比率。默认值0.6,即剪裁框默认宽度为图片宽度的0.6倍。 25 | initialBoxHeightRatio: 0.6 //可选。同initialBoxWidthRatio,当设置aspectRatio时,initialBoxHeightRatio值设置无效。initialBoxHeightRatio值由initialBoxWidthRatio 和 aspectRatio自动计算得到。 26 | }); 27 | 28 | 29 | }, 30 | 31 | //////// cancel /////////////////// 32 | fnCancel:function(){ 33 | console.log('cancel') 34 | //todo something 35 | }, 36 | 37 | ////////// do crop //////////////////// 38 | fnSubmit:function(){ 39 | console.log('submit') 40 | //do crop 41 | cropper.fnCrop({ 42 | 43 | //剪裁成功的回调 44 | success:function(res){ 45 | console.log(res) 46 | //生成文件的临时路径 47 | console.log(res.tempFilePath); 48 | wx.previewImage({ 49 | urls: [res.tempFilePath], 50 | }) 51 | }, 52 | //剪裁失败的回调 53 | fail:function(res){ 54 | console.log(res); 55 | }, 56 | 57 | //剪裁结束的回调 58 | complete:function(){ 59 | //complete 60 | } 61 | }); 62 | } 63 | }) -------------------------------------------------------------------------------- /pages/cropper/cropper-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "cropper":"/pages/cropper/cropper" 4 | }, 5 | "disableScroll": true 6 | } 7 | -------------------------------------------------------------------------------- /pages/cropper/cropper-example.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 取消 8 | 确定 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /pages/cropper/cropper-example.wxss: -------------------------------------------------------------------------------- 1 | page{ 2 | height: 100%; 3 | box-sizing: border-box; 4 | } 5 | .body{ 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | .stage{ 12 | flex-grow: 1; 13 | } 14 | 15 | 16 | .bar{ 17 | height: 80rpx; 18 | padding: 36rpx; 19 | display: flex; 20 | flex-direction: row; 21 | background-color: black; 22 | } 23 | 24 | .btn{ 25 | position: relative; 26 | flex-grow: 1; 27 | border: 2rpx solid grey; 28 | line-height: 80rpx; 29 | text-align: center; 30 | color: white; 31 | font-size: 32rpx; 32 | } 33 | 34 | .btn-cancel{ 35 | border-top-left-radius: 40rpx; 36 | border-bottom-left-radius: 40rpx; 37 | } 38 | 39 | .btn-submit{ 40 | border-top-right-radius: 40rpx; 41 | border-bottom-right-radius: 40rpx; 42 | } -------------------------------------------------------------------------------- /pages/cropper/cropper.js: -------------------------------------------------------------------------------- 1 | let fnLog = function(msg) { 2 | if (debug) { 3 | console.log(msg) 4 | } 5 | } 6 | 7 | //config 8 | let debug = false;//是否启用调试,默认值为false。true:打印过程日志;false:关闭过程日志 9 | let outputFileType = 'jpg';//目标文件的类型。默认值为jpg,jpg:输出jpg格式图片;png:输出png格式图片 10 | let quality = 1;//图片的质量。默认值为1,即最高质量。目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。 11 | let aspectRatio = null;//目标图片的宽高比,默认null,即不限制剪裁宽高比。aspectRatio需大于0 12 | // 13 | 14 | 15 | let layoutLeft = 0; 16 | let layoutTop = 0; 17 | let layoutWidth = 0; 18 | let layoutHeight = 0; 19 | 20 | let stageLeft = 0; 21 | let stageTop = 0; 22 | let stageWidth = 0; 23 | let stageHeight = 0; 24 | 25 | let imageWidth = 0; 26 | let imageHeight = 0; 27 | 28 | 29 | let pixelRatio = 1;//todo设备像素密度//暂不使用// 30 | 31 | let imageStageRatio = 1;//图片实际尺寸与剪裁舞台大小的比值,用于尺寸换算。 32 | 33 | let minBoxWidth = 0; 34 | let minBoxHeight = 0; 35 | 36 | //initial 37 | let minBoxWidthRatio = 0.15;//最小剪裁尺寸与原图尺寸的比率,默认0.15,即宽度最小剪裁到原图的0.15宽。 38 | let minBoxHeightRatio = 0.15;//同minBoxWidthRatio,当设置aspectRatio时,minBoxHeight值设置无效。minBoxHeight值由minBoxWidth 和 aspectRatio自动计算得到。 39 | 40 | let initialBoxWidthRatio = 0.6;//剪裁框初始大小比率。默认值0.6,即剪裁框默认宽度为图片宽度的0.6倍。 41 | let initialBoxHeightRatio = 0.6; //同initialBoxWidthRatio,当设置aspectRatio时,initialBoxHeightRatio值设置无效。initialBoxHeightRatio值由initialBoxWidthRatio 和 aspectRatio自动计算得到。 42 | // 43 | 44 | 45 | let touchStartBoxLeft = 0; 46 | let touchStartBoxTop = 0; 47 | let touchStartBoxWidth = 0; 48 | let touchStartBoxHeight = 0; 49 | 50 | let touchStartX = 0; 51 | let touchStartY = 0; 52 | 53 | 54 | Component({ 55 | 56 | 57 | /** 58 | * 组件的初始数据 59 | */ 60 | data: { 61 | 62 | imagePath: '', 63 | 64 | 65 | stageLeft: 0, 66 | stageTop: 0, 67 | stageWidth: 0, 68 | stageHeight: 0, 69 | 70 | boxWidth: 0, 71 | boxHeight: 0, 72 | boxLeft: 0, 73 | boxTop: 0, 74 | 75 | canvasWidth: 0, 76 | canvasHeight: 0 77 | 78 | }, 79 | 80 | 81 | 82 | /** 83 | * 组件的方法列表 84 | */ 85 | methods: { 86 | 87 | 88 | fnInit: function(opts) { 89 | let _self = this; 90 | 91 | let imagePath = opts.imagePath; 92 | 93 | if (opts.debug) { 94 | debug = opts.debug; 95 | } 96 | 97 | if (opts.minBoxWidthRatio) { 98 | minBoxWidthRatio = opts.minBoxWidthRatio; 99 | } 100 | 101 | if (opts.minBoxHeightRatio) { 102 | minBoxHeightRatio = opts.minBoxHeightRatio; 103 | } 104 | 105 | if (opts.initialBoxWidthRatio) { 106 | initialBoxWidthRatio = opts.initialBoxWidthRatio; 107 | } 108 | 109 | if (opts.initialBoxHeightRatio) { 110 | initialBoxHeightRatio = opts.initialBoxHeightRatio; 111 | } 112 | 113 | if (opts.aspectRatio) { 114 | aspectRatio = opts.aspectRatio; 115 | } 116 | 117 | 118 | 119 | 120 | 121 | wx.createSelectorQuery().in(this).select('.layout').boundingClientRect(function(rect) { 122 | fnLog(rect); 123 | layoutLeft = rect.left; 124 | layoutTop = rect.top; 125 | layoutWidth = rect.width; 126 | layoutHeight = rect.height; 127 | 128 | wx.getImageInfo({ 129 | src: imagePath, 130 | success: function(imageInfo) { 131 | fnLog(imageInfo) 132 | imageWidth = imageInfo.width; 133 | imageHeight = imageInfo.height; 134 | 135 | let imageWH = imageWidth / imageHeight; 136 | let layoutWH = layoutWidth / layoutHeight; 137 | if (imageWH >= layoutWH) { 138 | stageWidth = layoutWidth; 139 | stageHeight = stageWidth / imageWH; 140 | imageStageRatio = imageHeight / stageHeight; 141 | } else { 142 | stageHeight = layoutHeight; 143 | stageWidth = layoutHeight * imageWH; 144 | imageStageRatio = imageWidth / stageWidth; 145 | } 146 | stageLeft = (layoutWidth - stageWidth) / 2; 147 | stageTop = (layoutHeight - stageHeight) / 2; 148 | 149 | 150 | minBoxWidth = stageWidth * minBoxWidthRatio; 151 | minBoxHeight = stageHeight * minBoxHeightRatio; 152 | 153 | let boxWidth = stageWidth * initialBoxWidthRatio; 154 | let boxHeight = stageHeight * initialBoxHeightRatio; 155 | 156 | if (aspectRatio){ 157 | boxHeight = boxWidth / aspectRatio; 158 | } 159 | if(boxHeight > stageHeight){ 160 | boxHeight = stageHeight; 161 | boxWidth =boxHeight * aspectRatio; 162 | } 163 | 164 | let boxLeft = (stageWidth - boxWidth) / 2; 165 | let boxTop = (stageHeight - boxHeight) / 2; 166 | 167 | 168 | _self.setData({ 169 | imagePath: imagePath, 170 | canvasWidth: imageWidth * pixelRatio, 171 | canvasHeight: imageHeight * pixelRatio, 172 | 173 | stageLeft: stageLeft, 174 | stageTop: stageTop, 175 | stageWidth: stageWidth, 176 | stageHeight, 177 | stageHeight, 178 | 179 | boxWidth: boxWidth, 180 | boxHeight: boxHeight, 181 | boxLeft: boxLeft, 182 | boxTop: boxTop 183 | }) 184 | } 185 | }) 186 | 187 | }).exec(); 188 | 189 | }, 190 | 191 | 192 | ////////////////////////////////////// 193 | fnTouchStart: function(e) { 194 | fnLog('start') 195 | fnLog(e) 196 | 197 | let _self = this; 198 | 199 | 200 | let touch = e.touches[0]; 201 | let pageX = touch.pageX; 202 | let pageY = touch.pageY; 203 | 204 | touchStartX = pageX; 205 | touchStartY = pageY; 206 | 207 | touchStartBoxLeft = _self.data.boxLeft; 208 | touchStartBoxTop = _self.data.boxTop; 209 | touchStartBoxWidth = _self.data.boxWidth; 210 | touchStartBoxHeight = _self.data.boxHeight; 211 | 212 | 213 | }, 214 | 215 | fnTouchMove: function(e) { 216 | fnLog('move') 217 | fnLog(e) 218 | let _self = this; 219 | 220 | let targetId = e.target.id; 221 | let touch = e.touches[0]; 222 | let pageX = touch.pageX; 223 | let pageY = touch.pageY; 224 | 225 | let offsetX = pageX - touchStartX; 226 | let offsetY = pageY - touchStartY; 227 | 228 | 229 | 230 | 231 | 232 | if (targetId == 'box') { 233 | let newBoxLeft = touchStartBoxLeft + offsetX; 234 | let newBoxTop = touchStartBoxTop + offsetY; 235 | 236 | if (newBoxLeft < 0) { 237 | newBoxLeft = 0; 238 | } 239 | if (newBoxTop < 0) { 240 | newBoxTop = 0; 241 | } 242 | if (newBoxLeft + touchStartBoxWidth > stageWidth) { 243 | newBoxLeft = stageWidth - touchStartBoxWidth; 244 | } 245 | if (newBoxTop + touchStartBoxHeight > stageHeight) { 246 | newBoxTop = stageHeight - touchStartBoxHeight; 247 | } 248 | _self.setData({ 249 | boxLeft: newBoxLeft, 250 | boxTop: newBoxTop 251 | }); 252 | } else if (targetId == 'lt') { 253 | 254 | if (aspectRatio) { 255 | offsetY = offsetX / aspectRatio 256 | } 257 | 258 | let newBoxLeft = touchStartBoxLeft + offsetX; 259 | let newBoxTop = touchStartBoxTop + offsetY; 260 | 261 | if (newBoxLeft < 0) { 262 | newBoxLeft = 0; 263 | } 264 | if (newBoxTop < 0) { 265 | newBoxTop = 0; 266 | } 267 | 268 | if ((touchStartBoxLeft + touchStartBoxWidth - newBoxLeft) < minBoxWidth) { 269 | newBoxLeft = touchStartBoxLeft + touchStartBoxWidth - minBoxWidth; 270 | } 271 | if ((touchStartBoxTop + touchStartBoxHeight - newBoxTop) < minBoxHeight) { 272 | newBoxTop = touchStartBoxTop + touchStartBoxHeight - minBoxHeight; 273 | } 274 | 275 | let newBoxWidth = touchStartBoxWidth - (newBoxLeft - touchStartBoxLeft); 276 | let newBoxHeight = touchStartBoxHeight - (newBoxTop - touchStartBoxTop); 277 | 278 | 279 | //约束比例 280 | if (newBoxTop == 0 && aspectRatio && newBoxLeft != 0) { 281 | newBoxWidth = newBoxHeight * aspectRatio; 282 | newBoxLeft = touchStartBoxWidth - newBoxWidth + touchStartBoxLeft; 283 | } 284 | if(newBoxLeft == 0 && aspectRatio){ 285 | newBoxHeight = newBoxWidth / aspectRatio; 286 | newBoxTop = touchStartBoxHeight - newBoxHeight + touchStartBoxTop; 287 | } 288 | 289 | if (newBoxWidth == minBoxWidth && aspectRatio) { 290 | newBoxHeight = newBoxWidth / aspectRatio; 291 | newBoxTop = touchStartBoxHeight - newBoxHeight + touchStartBoxTop; 292 | } 293 | 294 | _self.setData({ 295 | boxTop: newBoxTop, 296 | boxLeft: newBoxLeft, 297 | boxWidth: newBoxWidth, 298 | boxHeight: newBoxHeight 299 | }); 300 | 301 | } else if (targetId == 'rt') { 302 | 303 | if (aspectRatio) { 304 | offsetY = -offsetX / aspectRatio 305 | } 306 | 307 | 308 | 309 | let newBoxWidth = touchStartBoxWidth + offsetX; 310 | if (newBoxWidth < minBoxWidth) { 311 | newBoxWidth = minBoxWidth; 312 | } 313 | if (touchStartBoxLeft + newBoxWidth > stageWidth) { 314 | newBoxWidth = stageWidth - touchStartBoxLeft; 315 | } 316 | 317 | 318 | let newBoxTop = touchStartBoxTop + offsetY; 319 | 320 | if (newBoxTop < 0) { 321 | newBoxTop = 0; 322 | } 323 | 324 | if ((touchStartBoxTop + touchStartBoxHeight - newBoxTop) < minBoxHeight) { 325 | newBoxTop = touchStartBoxTop + touchStartBoxHeight - minBoxHeight; 326 | } 327 | let newBoxHeight = touchStartBoxHeight - (newBoxTop - touchStartBoxTop); 328 | 329 | 330 | //约束比例 331 | if (newBoxTop == 0 && aspectRatio && newBoxWidth != stageWidth - touchStartBoxLeft) { 332 | newBoxWidth = newBoxHeight * aspectRatio; 333 | } 334 | 335 | if (newBoxWidth == stageWidth - touchStartBoxLeft && aspectRatio) { 336 | newBoxHeight = newBoxWidth / aspectRatio; 337 | newBoxTop = touchStartBoxHeight - newBoxHeight + touchStartBoxTop; 338 | } 339 | 340 | if (newBoxWidth == minBoxWidth && aspectRatio) { 341 | newBoxHeight = newBoxWidth / aspectRatio; 342 | newBoxTop = touchStartBoxHeight - newBoxHeight + touchStartBoxTop; 343 | } 344 | 345 | 346 | 347 | 348 | _self.setData({ 349 | boxTop: newBoxTop, 350 | boxHeight: newBoxHeight, 351 | boxWidth: newBoxWidth 352 | }); 353 | } else if (targetId == 'lb') { 354 | 355 | if (aspectRatio) { 356 | offsetY = -offsetX / aspectRatio 357 | } 358 | let newBoxLeft = touchStartBoxLeft + offsetX; 359 | 360 | if (newBoxLeft < 0) { 361 | newBoxLeft = 0; 362 | } 363 | if ((touchStartBoxLeft + touchStartBoxWidth - newBoxLeft) < minBoxWidth) { 364 | newBoxLeft = touchStartBoxLeft + touchStartBoxWidth - minBoxWidth; 365 | } 366 | 367 | let newBoxWidth = touchStartBoxWidth - (newBoxLeft - touchStartBoxLeft); 368 | 369 | 370 | let newBoxHeight = touchStartBoxHeight + offsetY; 371 | if (newBoxHeight < minBoxHeight) { 372 | newBoxHeight = minBoxHeight; 373 | } 374 | if (touchStartBoxTop + newBoxHeight > stageHeight) { 375 | newBoxHeight = stageHeight - touchStartBoxTop; 376 | } 377 | 378 | //约束比例 379 | if (newBoxHeight == stageHeight - touchStartBoxTop && aspectRatio && newBoxLeft != 0) { 380 | newBoxWidth = newBoxHeight * aspectRatio; 381 | newBoxLeft = touchStartBoxWidth - newBoxWidth + touchStartBoxLeft; 382 | } 383 | if(newBoxLeft == 0 && aspectRatio){ 384 | newBoxHeight = newBoxWidth / aspectRatio; 385 | } 386 | 387 | if (newBoxWidth == minBoxWidth && aspectRatio) { 388 | newBoxHeight = newBoxWidth / aspectRatio; 389 | } 390 | 391 | 392 | 393 | 394 | _self.setData({ 395 | 396 | boxLeft: newBoxLeft, 397 | boxWidth: newBoxWidth, 398 | boxHeight: newBoxHeight 399 | }); 400 | } else if (targetId == 'rb') { 401 | if (aspectRatio) { 402 | offsetY = offsetX / aspectRatio 403 | } 404 | let newBoxWidth = touchStartBoxWidth + offsetX; 405 | if (newBoxWidth < minBoxWidth) { 406 | newBoxWidth = minBoxWidth; 407 | } 408 | if (touchStartBoxLeft + newBoxWidth > stageWidth) { 409 | newBoxWidth = stageWidth - touchStartBoxLeft; 410 | } 411 | 412 | let newBoxHeight = touchStartBoxHeight + offsetY; 413 | if (newBoxHeight < minBoxHeight) { 414 | newBoxHeight = minBoxHeight; 415 | } 416 | if (touchStartBoxTop + newBoxHeight > stageHeight) { 417 | newBoxHeight = stageHeight - touchStartBoxTop; 418 | } 419 | 420 | 421 | //约束比例 422 | if (newBoxHeight == stageHeight - touchStartBoxTop && aspectRatio && newBoxWidth != stageWidth - touchStartBoxLeft) { 423 | newBoxWidth = newBoxHeight * aspectRatio; 424 | } 425 | 426 | if (newBoxWidth == stageWidth - touchStartBoxLeft && aspectRatio){ 427 | newBoxHeight = newBoxWidth / aspectRatio; 428 | } 429 | 430 | if(newBoxWidth == minBoxWidth && aspectRatio){ 431 | newBoxHeight = newBoxWidth / aspectRatio; 432 | } 433 | 434 | 435 | _self.setData({ 436 | 437 | boxWidth: newBoxWidth, 438 | boxHeight: newBoxHeight 439 | }); 440 | 441 | } 442 | 443 | 444 | }, 445 | 446 | fnTouchEnd: function(e) { 447 | fnLog('end') 448 | }, 449 | 450 | fnTouchCancel: function(e) { 451 | fnLog('cancel') 452 | }, 453 | 454 | 455 | fnCrop: function(opts) { 456 | let _self = this; 457 | 458 | 459 | let _success = function() {}; 460 | let _fail = function() {}; 461 | let _complete = function() {}; 462 | 463 | if (opts.success != null) { 464 | _success = opts.success; 465 | } 466 | if (opts.fail != null) { 467 | _fail = opts.fail; 468 | } 469 | if (opts.complete != null) { 470 | _complete = opts.complete; 471 | } 472 | 473 | 474 | 475 | 476 | let imagePath = _self.data.imagePath; 477 | let canvasContext = wx.createCanvasContext('canvas', _self); 478 | 479 | 480 | 481 | let boxLeft = _self.data.boxLeft; 482 | let boxTop = _self.data.boxTop; 483 | let boxWidth = _self.data.boxWidth; 484 | let boxHeight = _self.data.boxHeight; 485 | 486 | let sx = Math.ceil(boxLeft * imageStageRatio); 487 | let sy = Math.ceil(boxTop * imageStageRatio); 488 | 489 | let sWidth = Math.ceil(boxWidth * imageStageRatio); 490 | let sHeight = Math.ceil(boxHeight * imageStageRatio); 491 | let dx = 0; 492 | let dy = 0; 493 | 494 | 495 | 496 | let dWidth = Math.ceil(sWidth * pixelRatio); 497 | let dHeight = Math.ceil(sHeight * pixelRatio); 498 | 499 | 500 | 501 | canvasContext.drawImage(imagePath, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); 502 | canvasContext.draw(false, function() { 503 | wx.canvasToTempFilePath({ 504 | x: dx, 505 | y: dy, 506 | width: dWidth, 507 | height: dHeight, 508 | destWidth: sWidth, 509 | destHeight: sHeight, 510 | canvasId: 'canvas', 511 | fileType: outputFileType, 512 | quality: quality, 513 | success: _success, 514 | fail: _fail, 515 | complete: _complete 516 | }, _self); 517 | }) 518 | 519 | 520 | } 521 | 522 | 523 | } 524 | 525 | 526 | }) -------------------------------------------------------------------------------- /pages/cropper/cropper.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /pages/cropper/cropper.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 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 | -------------------------------------------------------------------------------- /pages/cropper/cropper.wxss: -------------------------------------------------------------------------------- 1 | /* pages/cutter/cutter.wxss */ 2 | :host{ 3 | position: relative; 4 | } 5 | 6 | 7 | .layout{ 8 | position: absolute; 9 | left: 0rpx; 10 | right: 0rpx; 11 | top: 0rpx; 12 | bottom: 0rpx; 13 | background-color: black; 14 | } 15 | 16 | .stage{ 17 | position: absolute; 18 | } 19 | 20 | .image{ 21 | position: absolute; 22 | width: 100%; 23 | height: 100%; 24 | } 25 | 26 | 27 | 28 | .box{ 29 | position: absolute; 30 | border: 4rpx solid grey; 31 | box-sizing: border-box; 32 | box-shadow: 0rpx 0rpx 0rpx 2000rpx rgba(0, 0, 0, 0.5) 33 | } 34 | 35 | 36 | .lt{ 37 | position: absolute; 38 | height: 48rpx; 39 | width: 48rpx; 40 | left: -6rpx; 41 | top: -6rpx; 42 | border-left: 12rpx solid white; 43 | border-top: 12rpx solid white; 44 | } 45 | 46 | .lb{ 47 | position: absolute; 48 | height: 48rpx; 49 | width: 48rpx; 50 | left: -6rpx; 51 | bottom: -6rpx; 52 | border-left: 12rpx solid white; 53 | border-bottom: 12rpx solid white; 54 | } 55 | 56 | .rt{ 57 | position: absolute; 58 | height: 48rpx; 59 | width: 48rpx; 60 | right: -6rpx; 61 | top: -6rpx; 62 | border-right: 12rpx solid white; 63 | border-top: 12rpx solid white; 64 | } 65 | 66 | .rb{ 67 | position: absolute; 68 | height: 48rpx; 69 | width: 48rpx; 70 | right: -6rpx; 71 | bottom: -6rpx; 72 | border-right: 12rpx solid white; 73 | border-bottom: 12rpx solid white; 74 | } 75 | 76 | .line-v{ 77 | position: absolute; 78 | width: 2rpx; 79 | height: 100%; 80 | background-color: rgba(255, 255, 255, 0.5); 81 | } 82 | .line-h{ 83 | height: 2rpx; 84 | position: absolute; 85 | width: 100%; 86 | background-color: rgba(255, 255, 255, 0.5); 87 | } -------------------------------------------------------------------------------- /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.0.4", 16 | "appid": "wx8115d6d888d52e47", 17 | "projectname": "capt-mini-components", 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 | } -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-wind/wx-cropper/757e3e9743e7dd994274d07ec45f86cd22977f7f/screenshot.gif -------------------------------------------------------------------------------- /sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------