├── src ├── image-cropper.json ├── image-cropper.wxml ├── image-cropper.wxss └── image-cropper.js ├── demo ├── app.wxss ├── component │ └── image-cropper │ │ ├── image-cropper.json │ │ ├── image-cropper.wxml │ │ ├── image-cropper.wxss │ │ └── image-cropper.js ├── app.js ├── sitemap.json ├── index │ ├── index.json │ ├── index.wxml │ ├── index.wxss │ └── index.js ├── app.json ├── cropper │ ├── cropper.json │ ├── cropper.wxss │ ├── cropper.wxml │ └── cropper.js └── project.config.json ├── .github └── ISSUE_TEMPLATE │ ├── encourage.jpg │ └── bug_report.md ├── .gitignore ├── LICENSE └── README.md /src/image-cropper.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true 3 | } -------------------------------------------------------------------------------- /demo/app.wxss: -------------------------------------------------------------------------------- 1 | page{ 2 | height: 100%; 3 | width: 100%; 4 | } -------------------------------------------------------------------------------- /demo/component/image-cropper/image-cropper.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true 3 | } -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | App({ 2 | onLaunch: function () { 3 | 4 | }, 5 | globalData:{} 6 | }) 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/encourage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1977474741/image-cropper/HEAD/.github/ISSUE_TEMPLATE/encourage.jpg -------------------------------------------------------------------------------- /demo/sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /demo/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "navigationBarTitleText": "image-cropper", 4 | "navigationBarBackgroundColor": "#292929", 5 | "navigationBarTextStyle": "white", 6 | "backgroundColor": "#292929" 7 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 提交bug 3 | about: 按照这个模板写 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 对应工具或者iOS或者Andriod的版本号 11 | 12 | 微信版本号 13 | 14 | 代码截图 15 | 16 | 重现步骤 17 | 18 | 期待的行为 19 | 20 | 实际的行为 21 | -------------------------------------------------------------------------------- /demo/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 点击裁剪 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages":[ 3 | "index/index", 4 | "cropper/cropper" 5 | ], 6 | "window":{ 7 | "backgroundTextStyle":"light", 8 | "navigationBarBackgroundColor": "#fff", 9 | "navigationBarTitleText": "Weixin", 10 | "navigationBarTextStyle":"black" 11 | }, 12 | "sitemapLocation": "sitemap.json" 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | desktop.ini 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /demo/cropper/cropper.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "image-cropper", 3 | "disableScroll": true, 4 | "navigationBarBackgroundColor": "#292929", 5 | "navigationBarTextStyle": "white", 6 | "backgroundColor": "#292929", 7 | "usingComponents": { 8 | "image-cropper": "../component/image-cropper/image-cropper" 9 | } 10 | } -------------------------------------------------------------------------------- /demo/index/index.wxss: -------------------------------------------------------------------------------- 1 | .intro { 2 | background: #292929; 3 | height: 100%; 4 | width: 100%; 5 | text-align: center; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | .head { 12 | margin-top: -20%; 13 | overflow: hidden; 14 | border-radius: 100%; 15 | width: 200rpx; 16 | height: 200rpx; 17 | border: 4rpx solid #fff5f7; 18 | line-height: 200rpx; 19 | color: #fff5f7; 20 | } 21 | 22 | .head>image { 23 | width: 100%; 24 | } -------------------------------------------------------------------------------- /demo/index/index.js: -------------------------------------------------------------------------------- 1 | const app = getApp() 2 | 3 | Page({ 4 | data: { 5 | src: '' 6 | }, 7 | toCropper() { 8 | wx.navigateTo({ 9 | url: `/cropper/cropper?imgSrc=${this.data.src}` 10 | }) 11 | }, 12 | onShow() { 13 | if (app.globalData.imgSrc) { 14 | this.setData({ 15 | src: app.globalData.imgSrc 16 | }) 17 | } 18 | }, 19 | onLoad: function () { 20 | console.log('代码片段是一种迷你、可分享的小程序或小游戏项目,可用于分享小程序和小游戏的开发经验、展示组件和 API 的使用、复现开发问题和 Bug 等。可点击以下链接查看代码片段的详细文档:') 21 | console.log('https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/devtools.html') 22 | }, 23 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 wx-plugin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/cropper/cropper.wxss: -------------------------------------------------------------------------------- 1 | .top { 2 | position: absolute; 3 | width: 100%; 4 | top: 10rpx; 5 | display: flex; 6 | flex-flow: wrap; 7 | z-index: 10; 8 | color: white; 9 | justify-content: space-around; 10 | } 11 | 12 | .hint { 13 | position: absolute; 14 | top: 10rpx; 15 | width: 100%; 16 | font-size: 33rpx; 17 | text-align: center; 18 | color: white; 19 | z-index: 10; 20 | } 21 | 22 | page { 23 | background: white; 24 | } 25 | 26 | view { 27 | font-size: 30rpx; 28 | } 29 | 30 | .bottom { 31 | position: absolute; 32 | width: 100%; 33 | bottom: 50rpx; 34 | display: flex; 35 | z-index: 10; 36 | justify-content: center; 37 | align-items: center; 38 | flex-wrap: wrap; 39 | height: 210rpx; 40 | } 41 | 42 | button { 43 | font-size: 27rpx; 44 | z-index: 2; 45 | padding: 0 20rpx; 46 | height: 60rpx; 47 | min-width: 70rpx; 48 | margin: 0 4rpx; 49 | } 50 | 51 | .input { 52 | display: flex; 53 | height: 50rpx; 54 | width: 50%; 55 | } 56 | 57 | .input>.label { 58 | min-width: 150rpx; 59 | font-size: 30rpx; 60 | height: 50rpx; 61 | line-height: 50rpx; 62 | } 63 | 64 | .input>input { 65 | margin-left: 10rpx; 66 | text-align: center; 67 | max-width: 160rpx; 68 | border: 1px solid rgb(255, 255, 255); 69 | height: 50rpx; 70 | line-height: 50rpx; 71 | min-height: 50rpx; 72 | box-sizing: border-box; 73 | } -------------------------------------------------------------------------------- /demo/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": true, 9 | "enhance": false, 10 | "postcss": true, 11 | "preloadBackgroundData": false, 12 | "minified": true, 13 | "newFeature": false, 14 | "coverView": true, 15 | "nodeModules": false, 16 | "autoAudits": false, 17 | "showShadowRootInWxmlPanel": true, 18 | "scopeDataCheck": false, 19 | "uglifyFileName": false, 20 | "checkInvalidKey": true, 21 | "checkSiteMap": true, 22 | "uploadWithSourceMap": true, 23 | "compileHotReLoad": false, 24 | "useMultiFrameRuntime": true, 25 | "useApiHook": true, 26 | "useApiHostProcess": false, 27 | "babelSetting": { 28 | "ignore": [], 29 | "disablePlugins": [], 30 | "outputPath": "" 31 | }, 32 | "enableEngineNative": false, 33 | "bundle": false, 34 | "useIsolateContext": true, 35 | "useCompilerModule": true, 36 | "userConfirmedUseCompilerModuleSwitch": false, 37 | "userConfirmedBundleSwitch": false, 38 | "packNpmManually": false, 39 | "packNpmRelationList": [], 40 | "minifyWXSS": true 41 | }, 42 | "compileType": "miniprogram", 43 | "libVersion": "2.15.0", 44 | "appid": "touristappid", 45 | "projectname": "image-cropper", 46 | "debugOptions": { 47 | "hidedInDevtools": [] 48 | }, 49 | "simulatorType": "wechat", 50 | "simulatorPluginLibVersion": {}, 51 | "condition": { 52 | "search": { 53 | "list": [] 54 | }, 55 | "conversation": { 56 | "list": [] 57 | }, 58 | "plugin": { 59 | "list": [] 60 | }, 61 | "game": { 62 | "currentL": -1, 63 | "list": [] 64 | }, 65 | "gamePlugin": { 66 | "list": [] 67 | }, 68 | "miniprogram": { 69 | "list": [ 70 | { 71 | "id": -1, 72 | "name": "cropper/cropper", 73 | "pathName": "cropper/cropper", 74 | "scene": null 75 | } 76 | ] 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /demo/cropper/cropper.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 | -------------------------------------------------------------------------------- /src/image-cropper.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 | -------------------------------------------------------------------------------- /demo/component/image-cropper/image-cropper.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 | -------------------------------------------------------------------------------- /src/image-cropper.wxss: -------------------------------------------------------------------------------- 1 | .image-cropper { 2 | background: rgba(14, 13, 13, .8); 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100vw; 7 | height: 100vh; 8 | z-index: 1; 9 | } 10 | 11 | .image-cropper .main { 12 | position: absolute; 13 | width: 100vw; 14 | height: 100vh; 15 | overflow: hidden; 16 | } 17 | 18 | .image-cropper .content { 19 | z-index: 9; 20 | position: absolute; 21 | width: 100vw; 22 | height: 100vh; 23 | display: flex; 24 | flex-direction: column; 25 | pointer-events: none; 26 | } 27 | 28 | .image-cropper .bg_black { 29 | background: rgba(0, 0, 0, 0.8) !important; 30 | } 31 | 32 | .image-cropper .bg_gray { 33 | background: rgba(0, 0, 0, 0.45); 34 | transition-duration: .35s; 35 | } 36 | 37 | .image-cropper .content>.content_top { 38 | pointer-events: none; 39 | } 40 | 41 | .image-cropper .content>.content_middle { 42 | display: flex; 43 | height: 200px; 44 | width: 100%; 45 | } 46 | 47 | .image-cropper .content_middle_middle { 48 | width: 200px; 49 | box-sizing: border-box; 50 | position: relative; 51 | transition-duration: .3s; 52 | } 53 | 54 | .image-cropper .content_middle_right { 55 | flex: auto; 56 | } 57 | 58 | .image-cropper .content>.content_bottom { 59 | flex: auto; 60 | } 61 | 62 | .image-cropper .img { 63 | z-index: 2; 64 | top: 0; 65 | left: 0; 66 | position: absolute; 67 | border: none; 68 | width: 100%; 69 | backface-visibility: hidden; 70 | transform-origin: center; 71 | } 72 | 73 | .image-cropper .image-cropper-canvas { 74 | position: fixed; 75 | background: white; 76 | width: 150px; 77 | height: 150px; 78 | z-index: 10; 79 | top: -200%; 80 | pointer-events: none; 81 | } 82 | 83 | .image-cropper .border { 84 | background: white; 85 | pointer-events: auto; 86 | position: absolute; 87 | } 88 | 89 | .image-cropper .border-top-left { 90 | left: -2.5px; 91 | top: -2.5px; 92 | height: 2.5px; 93 | width: 33rpx; 94 | } 95 | 96 | .image-cropper .border-top-right { 97 | right: -2.5px; 98 | top: -2.5px; 99 | height: 2.5px; 100 | width: 33rpx; 101 | } 102 | 103 | .image-cropper .border-right-top { 104 | top: -1px; 105 | width: 2.5px; 106 | height: 30rpx; 107 | right: -2.5px; 108 | } 109 | 110 | .image-cropper .border-right-bottom { 111 | width: 2.5px; 112 | height: 30rpx; 113 | right: -2.5px; 114 | bottom: -1px; 115 | } 116 | 117 | .image-cropper .border-bottom-left { 118 | height: 2.5px; 119 | width: 33rpx; 120 | bottom: -2.5px; 121 | left: -2.5px; 122 | } 123 | 124 | .image-cropper .border-bottom-right { 125 | height: 2.5px; 126 | width: 33rpx; 127 | bottom: -2.5px; 128 | right: -2.5px; 129 | } 130 | 131 | .image-cropper .border-left-top { 132 | top: -1px; 133 | width: 2.5px; 134 | height: 30rpx; 135 | left: -2.5px; 136 | } 137 | 138 | .image-cropper .border-left-bottom { 139 | width: 2.5px; 140 | height: 30rpx; 141 | left: -2.5px; 142 | bottom: -1px; 143 | } 144 | -------------------------------------------------------------------------------- /demo/component/image-cropper/image-cropper.wxss: -------------------------------------------------------------------------------- 1 | .image-cropper { 2 | background: rgba(14, 13, 13, .8); 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100vw; 7 | height: 100vh; 8 | z-index: 1; 9 | } 10 | 11 | .image-cropper .main { 12 | position: absolute; 13 | width: 100vw; 14 | height: 100vh; 15 | overflow: hidden; 16 | } 17 | 18 | .image-cropper .content { 19 | z-index: 9; 20 | position: absolute; 21 | width: 100vw; 22 | height: 100vh; 23 | display: flex; 24 | flex-direction: column; 25 | pointer-events: none; 26 | } 27 | 28 | .image-cropper .bg_black { 29 | background: rgba(0, 0, 0, 0.8) !important; 30 | } 31 | 32 | .image-cropper .bg_gray { 33 | background: rgba(0, 0, 0, 0.45); 34 | transition-duration: .35s; 35 | } 36 | 37 | .image-cropper .content>.content_top { 38 | pointer-events: none; 39 | } 40 | 41 | .image-cropper .content>.content_middle { 42 | display: flex; 43 | height: 200px; 44 | width: 100%; 45 | } 46 | 47 | .image-cropper .content_middle_middle { 48 | width: 200px; 49 | box-sizing: border-box; 50 | position: relative; 51 | transition-duration: .3s; 52 | } 53 | 54 | .image-cropper .content_middle_right { 55 | flex: auto; 56 | } 57 | 58 | .image-cropper .content>.content_bottom { 59 | flex: auto; 60 | } 61 | 62 | .image-cropper .img { 63 | z-index: 2; 64 | top: 0; 65 | left: 0; 66 | position: absolute; 67 | border: none; 68 | width: 100%; 69 | backface-visibility: hidden; 70 | transform-origin: center; 71 | } 72 | 73 | .image-cropper .image-cropper-canvas { 74 | position: fixed; 75 | background: white; 76 | width: 150px; 77 | height: 150px; 78 | z-index: 10; 79 | top: -200%; 80 | pointer-events: none; 81 | } 82 | 83 | .image-cropper .border { 84 | background: white; 85 | pointer-events: auto; 86 | position: absolute; 87 | } 88 | 89 | .image-cropper .border-top-left { 90 | left: -2.5px; 91 | top: -2.5px; 92 | height: 2.5px; 93 | width: 33rpx; 94 | } 95 | 96 | .image-cropper .border-top-right { 97 | right: -2.5px; 98 | top: -2.5px; 99 | height: 2.5px; 100 | width: 33rpx; 101 | } 102 | 103 | .image-cropper .border-right-top { 104 | top: -1px; 105 | width: 2.5px; 106 | height: 30rpx; 107 | right: -2.5px; 108 | } 109 | 110 | .image-cropper .border-right-bottom { 111 | width: 2.5px; 112 | height: 30rpx; 113 | right: -2.5px; 114 | bottom: -1px; 115 | } 116 | 117 | .image-cropper .border-bottom-left { 118 | height: 2.5px; 119 | width: 33rpx; 120 | bottom: -2.5px; 121 | left: -2.5px; 122 | } 123 | 124 | .image-cropper .border-bottom-right { 125 | height: 2.5px; 126 | width: 33rpx; 127 | bottom: -2.5px; 128 | right: -2.5px; 129 | } 130 | 131 | .image-cropper .border-left-top { 132 | top: -1px; 133 | width: 2.5px; 134 | height: 30rpx; 135 | left: -2.5px; 136 | } 137 | 138 | .image-cropper .border-left-bottom { 139 | width: 2.5px; 140 | height: 30rpx; 141 | left: -2.5px; 142 | bottom: -1px; 143 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # image-cropper 2 | ## 一款高性能的小程序图片裁剪插件,支持旋转。 3 | ###### `1.功能强大。` 4 | ###### `2.性能超高超流畅,大图毫无卡顿感。` 5 | ###### `3.组件化,使用简单。` 6 | ###### `4.点击中间窗口实时查看裁剪结果。` 7 |

体验Demo

8 |
9 | 10 | ## 初始准备 11 | #### 1.json文件中添加image-cropper 12 | ```json 13 | "usingComponents": { 14 | "image-cropper": "../image-cropper/image-cropper" 15 | }, 16 | "navigationBarTitleText": "裁剪图片", 17 | "disableScroll": true 18 | ``` 19 | #### 2.wxml文件 20 | ```html 21 | 22 | ``` 23 | #### 3.简单示例 24 | ```javascript 25 | Page({ 26 | data: { 27 | src:'', 28 | width:250,//宽度 29 | height: 250,//高度 30 | }, 31 | onLoad: function (options) { 32 | //获取到image-cropper实例 33 | this.cropper = this.selectComponent("#image-cropper"); 34 | //开始裁剪 35 | this.setData({ 36 | src:"https://raw.githubusercontent.com/1977474741/image-cropper/dev/image/code.jpg", 37 | }); 38 | wx.showLoading({ 39 | title: '加载中' 40 | }) 41 | }, 42 | cropperload(e){ 43 | console.log("cropper初始化完成"); 44 | }, 45 | loadimage(e){ 46 | console.log("图片加载完成",e.detail); 47 | wx.hideLoading(); 48 | //重置图片角度、缩放、位置 49 | this.cropper.imgReset(); 50 | }, 51 | clickcut(e) { 52 | console.log(e.detail); 53 | //点击裁剪框阅览图片 54 | wx.previewImage({ 55 | current: e.detail.url, // 当前显示图片的http链接 56 | urls: [e.detail.url] // 需要预览的图片http链接列表 57 | }) 58 | }, 59 | }) 60 | ``` 61 | ## 参数说明(高亮属性表示对于上个版本有修改,请注意) 62 | | 属性 | 类型 | 缺省值 | 取值 | 描述 | 必填 | 63 | | ------------- |:------:|:------:|:-----:|:-----:|:-----:| 64 | | imgSrc | String | 无 |无限制|图片地址(如果是网络图片需配置安全域名)|否| 65 | | disable_rotate| Boolean| false |true/false|禁止用户旋转(为false时建议同时设置limit_move为false)|否| 66 | | limit_move | Boolean| false |true/false|限制图片移动范围(裁剪框始终在图片内)(为true时建议同时设置disable_rotate为true)|否| 67 | | width | Number | 200 |超过屏幕宽度自动转为屏幕宽度|裁剪框宽度|否| 68 | | height | Number | 200 |超过屏幕高度自动转为屏幕高度|裁剪框高度|否| 69 | | max_width | Number | 300 |裁剪框最大宽度|裁剪框最大宽度|否| 70 | | max_height | Number | 300 |裁剪框最大高度|裁剪框最大高度|否| 71 | | min_width | Number | 100 |裁剪框最小宽度|裁剪框最小宽度|否| 72 | | min_height | Number | 100 |裁剪框最小高度|裁剪框最小高度|否| 73 | | disable_width | Boolean | false |true/false|锁定裁剪框宽度|否| 74 | | disable_height | Boolean | false |true/false|锁定裁剪框高度|否| 75 | | disable_ratio | Boolean | false |true/false|锁定裁剪框比例|否| 76 | | export_scale | Number | 3 |无限制|输出图片的比例(相对于裁剪框尺寸)|否| 77 | | quality | Number | 1 |0-1|生成的图片质量|否| 78 | | ~~cut_top~~ | Number | 居中 |始终在屏幕内 |裁剪框上边距|否| 79 | | ~~cut_left~~ | Number | 居中 |始终在屏幕内 |裁剪框左边距|否| 80 | | `img_width` | Number | 宽高都不设置,最小边填满裁剪框 |支持%(不加单位为px)(只设置宽度,高度自适应)|图片宽度|否| 81 | | `img_height` | Number | 宽高都不设置,最小边填满裁剪框 |支持%(不加单位为px)(只设置高度,宽度自适应)|图片高度|否| 82 | | scale | Number | 1 |无限制|图片的缩放比|否| 83 | | angle | Number | 0 |(limit_move=true时angle=n*90)|图片的旋转角度|否| 84 | | min_scale | Number | 0.5 |无限制|图片的最小缩放比|否| 85 | | max_scale | Number | 2 |无限制|图片的最大缩放比|否| 86 | | bindload | Function | null |函数名称|cropper初始化完成|否| 87 | | bindimageload | Function | null |函数名称|图片加载完成,返回值Object{width,height,path,type等}|否| 88 | | bindtapcut | Function | null |函数名称|点击中间裁剪框,返回值Object{src,width,height}|否| 89 | ## 函数说明 90 | | 函数名 | 参数 | 返回值 |描述|参数必填| 91 | | ------------- |:------: |:------:|:------:|:------:| 92 | | upload | 无 | 无 |调起wx上传图片接口并开始剪裁|否| 93 | | pushImg | src | 无 |放入图片开始裁剪|是| 94 | | getImg |Function(回调函数) | `Object{url,width,height}` |裁剪并获取图片(图片尺寸 = 图片宽高 * export_scale)|是| 95 | | ~~setCutXY~~ | X、Y | 无 |设置裁剪框位置|是| 96 | | setCutSize | width、height | 无 |设置裁剪框大小|是| 97 | | setCutCenter | 无 | 无 |设置裁剪框居中|否| 98 | | setScale | scale | 无 |设置图片缩放比例(不受min_scale、max_scale影响)|是| 99 | | setAngle | deg | 无 |设置图片旋转角度(带过渡效果)|是| 100 | | setTransform |{x,y,angle,scale,cutX,cutY}| 无 |图片在原有基础上的变化(scale受min_scale、max_scale影响)|根据需要传参| 101 | | imgReset |无 | 无 |重置图片的角度、缩放、位置(可以在onloadImage回调里使用)|否| 102 | 103 | Demo地址:https://github.com/wx-plugin/image-cropper-demo 104 | 105 | [点击导入代码片段](https://developers.weixin.qq.com/s/Z1VsB9mG7Cpm) 106 | 107 | 如果有什么好的建议欢迎提issues或者提pr 108 | 109 | ## 进群 | 鼓励作者 110 | 111 | 112 | -------------------------------------------------------------------------------- /demo/cropper/cropper.js: -------------------------------------------------------------------------------- 1 | //获取应用实例 2 | const app = getApp() 3 | Page({ 4 | data: { 5 | src: '', 6 | width: 250, //宽度 7 | height: 250, //高度 8 | max_width: 300, 9 | max_height: 300, 10 | disable_rotate: true, //是否禁用旋转 11 | disable_ratio: false, //锁定比例 12 | limit_move: true, //是否限制移动 13 | }, 14 | onLoad: function (options) { 15 | this.cropper = this.selectComponent("#image-cropper"); 16 | this.setData({ 17 | src: options.imgSrc 18 | }); 19 | if(!options.imgSrc){ 20 | this.cropper.upload(); //上传图片 21 | } 22 | }, 23 | cropperload(e) { 24 | console.log('cropper加载完成'); 25 | }, 26 | loadimage(e) { 27 | wx.hideLoading(); 28 | console.log('图片'); 29 | this.cropper.imgReset(); 30 | }, 31 | clickcut(e) { 32 | console.log(e.detail); 33 | //图片预览 34 | wx.previewImage({ 35 | current: e.detail.url, // 当前显示图片的http链接 36 | urls: [e.detail.url] // 需要预览的图片http链接列表 37 | }) 38 | }, 39 | upload() { 40 | let that = this; 41 | wx.chooseImage({ 42 | count: 1, 43 | sizeType: ['original', 'compressed'], 44 | sourceType: ['album', 'camera'], 45 | success(res) { 46 | wx.showLoading({ 47 | title: '加载中', 48 | }) 49 | const tempFilePaths = res.tempFilePaths[0]; 50 | //重置图片角度、缩放、位置 51 | that.cropper.imgReset(); 52 | that.setData({ 53 | src: tempFilePaths 54 | }); 55 | } 56 | }) 57 | }, 58 | setWidth(e) { 59 | this.setData({ 60 | width: e.detail.value < 10 ? 10 : e.detail.value 61 | }); 62 | this.setData({ 63 | cut_left: this.cropper.data.cut_left 64 | }); 65 | }, 66 | setHeight(e) { 67 | this.setData({ 68 | height: e.detail.value < 10 ? 10 : e.detail.value 69 | }); 70 | this.setData({ 71 | cut_top: this.cropper.data.cut_top 72 | }); 73 | }, 74 | switchChangeDisableRatio(e) { 75 | //设置宽度之后使剪裁框居中 76 | this.setData({ 77 | disable_ratio: e.detail.value 78 | }); 79 | }, 80 | setCutTop(e) { 81 | this.setData({ 82 | cut_top: e.detail.value 83 | }); 84 | this.setData({ 85 | cut_top: this.cropper.data.cut_top 86 | }); 87 | }, 88 | setCutLeft(e) { 89 | this.setData({ 90 | cut_left: e.detail.value 91 | }); 92 | this.setData({ 93 | cut_left: this.cropper.data.cut_left 94 | }); 95 | }, 96 | switchChangeDisableRotate(e) { 97 | //开启旋转的同时不限制移动 98 | if (!e.detail.value) { 99 | this.setData({ 100 | limit_move: false, 101 | disable_rotate: e.detail.value 102 | }); 103 | } else { 104 | this.setData({ 105 | disable_rotate: e.detail.value 106 | }); 107 | } 108 | }, 109 | switchChangeLimitMove(e) { 110 | //限制移动的同时锁定旋转 111 | if (e.detail.value) { 112 | this.setData({ 113 | disable_rotate: true 114 | }); 115 | } 116 | this.cropper.setLimitMove(e.detail.value); 117 | }, 118 | switchChangeDisableWidth(e) { 119 | this.setData({ 120 | disable_width: e.detail.value 121 | }); 122 | }, 123 | switchChangeDisableHeight(e) { 124 | this.setData({ 125 | disable_height: e.detail.value 126 | }); 127 | }, 128 | submit() { 129 | this.cropper.getImg((obj) => { 130 | app.globalData.imgSrc = obj.url; 131 | wx.navigateBack({ 132 | delta: -1 133 | }) 134 | }); 135 | }, 136 | rotate() { 137 | //在用户旋转的基础上旋转90° 138 | this.cropper.setAngle(this.cropper.data.angle += 90); 139 | }, 140 | top() { 141 | this.data.top = setInterval(() => { 142 | this.cropper.setTransform({ 143 | y: -3 144 | }); 145 | }, 1000 / 60) 146 | }, 147 | bottom() { 148 | this.data.bottom = setInterval(() => { 149 | this.cropper.setTransform({ 150 | y: 3 151 | }); 152 | }, 1000 / 60) 153 | }, 154 | left() { 155 | this.data.left = setInterval(() => { 156 | this.cropper.setTransform({ 157 | x: -3 158 | }); 159 | }, 1000 / 60) 160 | }, 161 | right() { 162 | this.data.right = setInterval(() => { 163 | this.cropper.setTransform({ 164 | x: 3 165 | }); 166 | }, 1000 / 60) 167 | }, 168 | narrow() { 169 | this.data.narrow = setInterval(() => { 170 | this.cropper.setTransform({ 171 | scale: -0.02 172 | }); 173 | }, 1000 / 60) 174 | }, 175 | enlarge() { 176 | this.data.enlarge = setInterval(() => { 177 | this.cropper.setTransform({ 178 | scale: 0.02 179 | }); 180 | }, 1000 / 60) 181 | }, 182 | end(e) { 183 | clearInterval(this.data[e.currentTarget.dataset.type]); 184 | }, 185 | }) -------------------------------------------------------------------------------- /src/image-cropper.js: -------------------------------------------------------------------------------- 1 | Component({ 2 | properties: { 3 | /** 4 | * 图片路径 5 | */ 6 | 'imgSrc': { 7 | type: String 8 | }, 9 | /** 10 | * 裁剪框高度 11 | */ 12 | 'height': { 13 | type: Number, 14 | value: 200 15 | }, 16 | /** 17 | * 裁剪框宽度 18 | */ 19 | 'width': { 20 | type: Number, 21 | value: 200 22 | }, 23 | /** 24 | * 裁剪框最小尺寸 25 | */ 26 | 'min_width': { 27 | type: Number, 28 | value: 100 29 | }, 30 | 'min_height': { 31 | type: Number, 32 | value: 100 33 | }, 34 | /** 35 | * 裁剪框最大尺寸 36 | */ 37 | 'max_width': { 38 | type: Number, 39 | value: 300 40 | }, 41 | 'max_height': { 42 | type: Number, 43 | value: 300 44 | }, 45 | /** 46 | * 裁剪框禁止拖动 47 | */ 48 | 'disable_width': { 49 | type: Boolean, 50 | value: false 51 | }, 52 | 'disable_height': { 53 | type: Boolean, 54 | value: false 55 | }, 56 | /** 57 | * 锁定裁剪框比例 58 | */ 59 | 'disable_ratio': { 60 | type: Boolean, 61 | value: false 62 | }, 63 | /** 64 | * 生成的图片尺寸相对剪裁框的比例 65 | */ 66 | 'export_scale': { 67 | type: Number, 68 | value: 3 69 | }, 70 | /** 71 | * 生成的图片质量0-1 72 | */ 73 | 'quality': { 74 | type: Number, 75 | value: 1 76 | }, 77 | 'cut_top': { 78 | type: Number, 79 | value: null 80 | }, 81 | 'cut_left': { 82 | type: Number, 83 | value: null 84 | }, 85 | /** 86 | * canvas上边距(不设置默认不显示) 87 | */ 88 | 'canvas_top': { 89 | type: Number, 90 | value: null 91 | }, 92 | /** 93 | * canvas左边距(不设置默认不显示) 94 | */ 95 | 'canvas_left': { 96 | type: Number, 97 | value: null 98 | }, 99 | /** 100 | * 图片宽度 101 | */ 102 | 'img_width': { 103 | type: null, 104 | value: null 105 | }, 106 | /** 107 | * 图片高度 108 | */ 109 | 'img_height': { 110 | type: null, 111 | value: null 112 | }, 113 | /** 114 | * 图片缩放比 115 | */ 116 | 'scale': { 117 | type: Number, 118 | value: 1 119 | }, 120 | /** 121 | * 图片旋转角度 122 | */ 123 | 'angle': { 124 | type: Number, 125 | value: 0 126 | }, 127 | /** 128 | * 最小缩放比 129 | */ 130 | 'min_scale': { 131 | type: Number, 132 | value: 0.5 133 | }, 134 | /** 135 | * 最大缩放比 136 | */ 137 | 'max_scale': { 138 | type: Number, 139 | value: 2 140 | }, 141 | /** 142 | * 是否禁用旋转 143 | */ 144 | 'disable_rotate': { 145 | type: Boolean, 146 | value: false 147 | }, 148 | /** 149 | * 是否限制移动范围(剪裁框只能在图片内) 150 | */ 151 | 'limit_move': { 152 | type: Boolean, 153 | value: false 154 | } 155 | }, 156 | data: { 157 | el: 'image-cropper', //暂时无用 158 | info: wx.getSystemInfoSync(), 159 | MOVE_THROTTLE: null, //触摸移动节流settimeout 160 | MOVE_THROTTLE_FLAG: true, //节流标识 161 | INIT_IMGWIDTH: 0, //图片设置尺寸,此值不变(记录最初设定的尺寸) 162 | INIT_IMGHEIGHT: 0, //图片设置尺寸,此值不变(记录最初设定的尺寸) 163 | TIME_BG: null, //背景变暗延时函数 164 | TIME_CUT_CENTER: null, 165 | _touch_img_relative: [{ 166 | x: 0, 167 | y: 0 168 | }], //鼠标和图片中心的相对位置 169 | _flag_cut_touch: false, //是否是拖动裁剪框 170 | _hypotenuse_length: 0, //双指触摸时斜边长度 171 | _flag_img_endtouch: false, //是否结束触摸 172 | _flag_bright: true, //背景是否亮 173 | _canvas_overflow: true, //canvas缩略图是否在屏幕外面 174 | _canvas_width: 200, 175 | _canvas_height: 200, 176 | origin_x: 0.5, //图片旋转中心 177 | origin_y: 0.5, //图片旋转中心 178 | _cut_animation: false, //是否开启图片和裁剪框过渡 179 | _img_top: wx.getSystemInfoSync().windowHeight / 2, //图片上边距 180 | _img_left: wx.getSystemInfoSync().windowWidth / 2, //图片左边距 181 | watch: { 182 | //监听截取框宽高变化 183 | width(value, that) { 184 | if (value < that.data.min_width) { 185 | that.setData({ 186 | width: that.data.min_width 187 | }); 188 | } 189 | that._computeCutSize(); 190 | }, 191 | height(value, that) { 192 | if (value < that.data.min_height) { 193 | that.setData({ 194 | height: that.data.min_height 195 | }); 196 | } 197 | that._computeCutSize(); 198 | }, 199 | angle(value, that) { 200 | //停止居中裁剪框,继续修改图片位置 201 | that._moveStop(); 202 | if (that.data.limit_move) { 203 | if (that.data.angle % 90) { 204 | that.setData({ 205 | angle: Math.round(that.data.angle / 90) * 90 206 | }); 207 | return; 208 | } 209 | } 210 | }, 211 | _cut_animation(value, that) { 212 | //开启过渡300毫秒之后自动关闭 213 | clearTimeout(that.data._cut_animation_time); 214 | if (value) { 215 | that.data._cut_animation_time = setTimeout(() => { 216 | that.setData({ 217 | _cut_animation: false 218 | }); 219 | }, 300) 220 | } 221 | }, 222 | limit_move(value, that) { 223 | if (value) { 224 | if (that.data.angle % 90) { 225 | that.setData({ 226 | angle: Math.round(that.data.angle / 90) * 90 227 | }); 228 | } 229 | that._imgMarginDetectionScale(); 230 | !that.data._canvas_overflow && that._draw(); 231 | } 232 | }, 233 | canvas_top(value, that) { 234 | that._canvasDetectionPosition(); 235 | }, 236 | canvas_left(value, that) { 237 | that._canvasDetectionPosition(); 238 | }, 239 | imgSrc(value, that) { 240 | that.pushImg(); 241 | }, 242 | cut_top(value, that) { 243 | that._cutDetectionPosition(); 244 | if (that.data.limit_move) { 245 | !that.data._canvas_overflow && that._draw(); 246 | } 247 | }, 248 | cut_left(value, that) { 249 | that._cutDetectionPosition(); 250 | if (that.data.limit_move) { 251 | !that.data._canvas_overflow && that._draw(); 252 | } 253 | } 254 | } 255 | }, 256 | attached() { 257 | this.data.info = wx.getSystemInfoSync(); 258 | //启用数据监听 259 | this._watcher(); 260 | this.data.INIT_IMGWIDTH = this.data.img_width; 261 | this.data.INIT_IMGHEIGHT = this.data.img_height; 262 | this.setData({ 263 | _canvas_height: this.data.height, 264 | _canvas_width: this.data.width, 265 | }); 266 | this._initCanvas(); 267 | this.data.imgSrc && (this.data.imgSrc = this.data.imgSrc); 268 | //根据开发者设置的图片目标尺寸计算实际尺寸 269 | this._initImageSize(); 270 | //设置裁剪框大小>设置图片尺寸>绘制canvas 271 | this._computeCutSize(); 272 | //检查裁剪框是否在范围内 273 | this._cutDetectionPosition(); 274 | //检查canvas是否在范围内 275 | this._canvasDetectionPosition(); 276 | //初始化完成 277 | this.triggerEvent('load', { 278 | cropper: this 279 | }); 280 | }, 281 | methods: { 282 | /** 283 | * 上传图片 284 | */ 285 | upload() { 286 | let that = this; 287 | wx.chooseImage({ 288 | count: 1, 289 | sizeType: ['original', 'compressed'], 290 | sourceType: ['album', 'camera'], 291 | success(res) { 292 | const tempFilePaths = res.tempFilePaths[0]; 293 | that.pushImg(tempFilePaths); 294 | wx.showLoading({ 295 | title: '加载中...' 296 | }) 297 | } 298 | }) 299 | }, 300 | /** 301 | * 返回图片信息 302 | */ 303 | getImg(getCallback) { 304 | this._draw(() => { 305 | wx.canvasToTempFilePath({ 306 | width: this.data.width * this.data.export_scale, 307 | height: Math.round(this.data.height * this.data.export_scale), 308 | destWidth: this.data.width * this.data.export_scale, 309 | destHeight: Math.round(this.data.height) * this.data.export_scale, 310 | fileType: 'png', 311 | quality: this.data.quality, 312 | canvasId: this.data.el, 313 | success: (res) => { 314 | getCallback({ 315 | url: res.tempFilePath, 316 | width: this.data.width * this.data.export_scale, 317 | height: this.data.height * this.data.export_scale 318 | }); 319 | } 320 | }, this) 321 | }); 322 | }, 323 | /** 324 | * 设置图片动画 325 | * { 326 | * x:10,//图片在原有基础上向下移动10px 327 | * y:10,//图片在原有基础上向右移动10px 328 | * angle:10,//图片在原有基础上旋转10deg 329 | * scale:0.5,//图片在原有基础上增加0.5倍 330 | * } 331 | */ 332 | setTransform(transform) { 333 | if (!transform) return; 334 | if (!this.data.disable_rotate) { 335 | this.setData({ 336 | angle: transform.angle ? this.data.angle + transform.angle : this.data.angle 337 | }); 338 | } 339 | var scale = this.data.scale; 340 | if (transform.scale) { 341 | scale = this.data.scale + transform.scale; 342 | scale = scale <= this.data.min_scale ? this.data.min_scale : scale; 343 | scale = scale >= this.data.max_scale ? this.data.max_scale : scale; 344 | } 345 | this.data.scale = scale; 346 | let cutX = this.data.cut_left; 347 | let cutY = this.data.cut_top; 348 | if (transform.cutX) { 349 | this.setData({ 350 | cut_left: cutX + transform.cutX 351 | }); 352 | this.data.watch.cut_left(null, this); 353 | } 354 | if (transform.cutY) { 355 | this.setData({ 356 | cut_top: cutY + transform.cutY 357 | }); 358 | this.data.watch.cut_top(null, this); 359 | } 360 | this.data._img_top = transform.y ? this.data._img_top + transform.y : this.data._img_top; 361 | this.data._img_left = transform.x ? this.data._img_left + transform.x : this.data._img_left; 362 | //图像边缘检测,防止截取到空白 363 | this._imgMarginDetectionScale(); 364 | //停止居中裁剪框,继续修改图片位置 365 | this._moveDuring(); 366 | this.setData({ 367 | scale: this.data.scale, 368 | _img_top: this.data._img_top, 369 | _img_left: this.data._img_left 370 | }); 371 | !this.data._canvas_overflow && this._draw(); 372 | //可以居中裁剪框了 373 | this._moveStop(); //结束操作 374 | }, 375 | /** 376 | * 设置剪裁框位置 377 | */ 378 | setCutXY(x, y) { 379 | this.setData({ 380 | cut_top: y, 381 | cut_left: x 382 | }); 383 | }, 384 | /** 385 | * 设置剪裁框尺寸 386 | */ 387 | setCutSize(w, h) { 388 | this.setData({ 389 | width: w, 390 | height: h 391 | }); 392 | this._computeCutSize(); 393 | }, 394 | /** 395 | * 设置剪裁框和图片居中 396 | */ 397 | setCutCenter() { 398 | let cut_top = (this.data.info.windowHeight - this.data.height) * 0.5; 399 | let cut_left = (this.data.info.windowWidth - this.data.width) * 0.5; 400 | //顺序不能变 401 | this.setData({ 402 | _img_top: this.data._img_top - this.data.cut_top + cut_top, 403 | cut_top: cut_top, //截取的框上边距 404 | _img_left: this.data._img_left - this.data.cut_left + cut_left, 405 | cut_left: cut_left, //截取的框左边距 406 | }); 407 | }, 408 | _setCutCenter() { 409 | let cut_top = (this.data.info.windowHeight - this.data.height) * 0.5; 410 | let cut_left = (this.data.info.windowWidth - this.data.width) * 0.5; 411 | this.setData({ 412 | cut_top: cut_top, //截取的框上边距 413 | cut_left: cut_left, //截取的框左边距 414 | }); 415 | }, 416 | /** 417 | * 设置剪裁框宽度-即将废弃 418 | */ 419 | setWidth(width) { 420 | this.setData({ 421 | width: width 422 | }); 423 | this._computeCutSize(); 424 | }, 425 | /** 426 | * 设置剪裁框高度-即将废弃 427 | */ 428 | setHeight(height) { 429 | this.setData({ 430 | height: height 431 | }); 432 | this._computeCutSize(); 433 | }, 434 | /** 435 | * 是否锁定旋转 436 | */ 437 | setDisableRotate(value) { 438 | this.data.disable_rotate = value; 439 | }, 440 | /** 441 | * 是否限制移动 442 | */ 443 | setLimitMove(value) { 444 | this.setData({ 445 | _cut_animation: true, 446 | limit_move: !!value 447 | }); 448 | }, 449 | /** 450 | * 初始化图片,包括位置、大小、旋转角度 451 | */ 452 | imgReset() { 453 | this.setData({ 454 | scale: 1, 455 | angle: 0, 456 | _img_top: wx.getSystemInfoSync().windowHeight / 2, 457 | _img_left: wx.getSystemInfoSync().windowWidth / 2, 458 | }) 459 | }, 460 | /** 461 | * 加载(更换)图片 462 | */ 463 | pushImg(src) { 464 | if (src) { 465 | this.setData({ 466 | imgSrc: src 467 | }); 468 | //发现是手动赋值直接返回,交给watch处理 469 | return; 470 | } 471 | 472 | // getImageInfo接口传入 src: '' 会导致内存泄漏 473 | 474 | if (!this.data.imgSrc) return; 475 | wx.getImageInfo({ 476 | src: this.data.imgSrc, 477 | success: (res) => { 478 | this.data.imageObject = res; 479 | //图片非本地路径需要换成本地路径 480 | if (this.data.imgSrc.search(/tmp/) == -1) { 481 | this.setData({ 482 | imgSrc: res.path 483 | }); 484 | } 485 | //计算最后图片尺寸 486 | this._imgComputeSize(); 487 | if (this.data.limit_move) { 488 | //限制移动,不留空白处理 489 | this._imgMarginDetectionScale(); 490 | } 491 | this._draw(); 492 | }, 493 | fail: (err) => { 494 | this.setData({ 495 | imgSrc: '' 496 | }); 497 | } 498 | }); 499 | }, 500 | imageLoad(e) { 501 | setTimeout(() => { 502 | this.triggerEvent('imageload', this.data.imageObject); 503 | 504 | }) 505 | }, 506 | /** 507 | * 设置图片放大缩小 508 | */ 509 | setScale(scale) { 510 | if (!scale) return; 511 | this.setData({ 512 | scale: scale 513 | }); 514 | !this.data._canvas_overflow && this._draw(); 515 | }, 516 | /** 517 | * 设置图片旋转角度 518 | */ 519 | setAngle(angle) { 520 | if (!angle) return; 521 | this.setData({ 522 | _cut_animation: true, 523 | angle: angle 524 | }); 525 | this._imgMarginDetectionScale(); 526 | !this.data._canvas_overflow && this._draw(); 527 | }, 528 | _initCanvas() { 529 | //初始化canvas 530 | if (!this.data.ctx) { 531 | this.data.ctx = wx.createCanvasContext("image-cropper", this); 532 | } 533 | }, 534 | /** 535 | * 根据开发者设置的图片目标尺寸计算实际尺寸 536 | */ 537 | _initImageSize() { 538 | //处理宽高特殊单位 %>px 539 | if (this.data.INIT_IMGWIDTH && typeof this.data.INIT_IMGWIDTH == "string" && this.data.INIT_IMGWIDTH.indexOf("%") != -1) { 540 | let width = this.data.INIT_IMGWIDTH.replace("%", ""); 541 | this.data.INIT_IMGWIDTH = this.data.img_width = this.data.info.windowWidth / 100 * width; 542 | } 543 | if (this.data.INIT_IMGHEIGHT && typeof this.data.INIT_IMGHEIGHT == "string" && this.data.INIT_IMGHEIGHT.indexOf("%") != -1) { 544 | let height = this.data.img_height.replace("%", ""); 545 | this.data.INIT_IMGHEIGHT = this.data.img_height = this.data.info.windowHeight / 100 * height; 546 | } 547 | }, 548 | /** 549 | * 检测剪裁框位置是否在允许的范围内(屏幕内) 550 | */ 551 | _cutDetectionPosition() { 552 | let _cutDetectionPositionTop = () => { 553 | //检测上边距是否在范围内 554 | if (this.data.cut_top < 0) { 555 | this.setData({ 556 | cut_top: 0 557 | }); 558 | } 559 | if (this.data.cut_top > this.data.info.windowHeight - this.data.height) { 560 | this.setData({ 561 | cut_top: this.data.info.windowHeight - this.data.height 562 | }); 563 | } 564 | }, 565 | _cutDetectionPositionLeft = () => { 566 | //检测左边距是否在范围内 567 | if (this.data.cut_left < 0) { 568 | this.setData({ 569 | cut_left: 0 570 | }); 571 | } 572 | if (this.data.cut_left > this.data.info.windowWidth - this.data.width) { 573 | this.setData({ 574 | cut_left: this.data.info.windowWidth - this.data.width 575 | }); 576 | } 577 | }; 578 | //裁剪框坐标处理(如果只写一个参数则另一个默认为0,都不写默认居中) 579 | if (this.data.cut_top == null && this.data.cut_left == null) { 580 | this._setCutCenter(); 581 | } else if (this.data.cut_top != null && this.data.cut_left != null) { 582 | _cutDetectionPositionTop(); 583 | _cutDetectionPositionLeft(); 584 | } else if (this.data.cut_top != null && this.data.cut_left == null) { 585 | _cutDetectionPositionTop(); 586 | this.setData({ 587 | cut_left: (this.data.info.windowWidth - this.data.width) / 2 588 | }); 589 | } else if (this.data.cut_top == null && this.data.cut_left != null) { 590 | _cutDetectionPositionLeft(); 591 | this.setData({ 592 | cut_top: (this.data.info.windowHeight - this.data.height) / 2 593 | }); 594 | } 595 | }, 596 | /** 597 | * 检测canvas位置是否在允许的范围内(屏幕内)如果在屏幕外则不开启实时渲染 598 | * 如果只写一个参数则另一个默认为0,都不写默认超出屏幕外 599 | */ 600 | _canvasDetectionPosition() { 601 | if (this.data.canvas_top == null && this.data.canvas_left == null) { 602 | this.data._canvas_overflow = false; 603 | this.setData({ 604 | canvas_top: -5000, 605 | canvas_left: -5000 606 | }); 607 | } else if (this.data.canvas_top != null && this.data.canvas_left != null) { 608 | if (this.data.canvas_top < -this.data.height || this.data.canvas_top > this.data.info.windowHeight) { 609 | this.data._canvas_overflow = true; 610 | } else { 611 | this.data._canvas_overflow = false; 612 | } 613 | } else if (this.data.canvas_top != null && this.data.canvas_left == null) { 614 | this.setData({ 615 | canvas_left: 0 616 | }); 617 | } else if (this.data.canvas_top == null && this.data.canvas_left != null) { 618 | this.setData({ 619 | canvas_top: 0 620 | }); 621 | if (this.data.canvas_left < -this.data.width || this.data.canvas_left > this.data.info.windowWidth) { 622 | this.data._canvas_overflow = true; 623 | } else { 624 | this.data._canvas_overflow = false; 625 | } 626 | } 627 | }, 628 | /** 629 | * 图片边缘检测-位置 630 | */ 631 | _imgMarginDetectionPosition(scale) { 632 | if (!this.data.limit_move) return; 633 | let left = this.data._img_left; 634 | let top = this.data._img_top; 635 | var scale = scale || this.data.scale; 636 | let img_width = this.data.img_width; 637 | let img_height = this.data.img_height; 638 | if (this.data.angle / 90 % 2) { 639 | img_width = this.data.img_height; 640 | img_height = this.data.img_width; 641 | } 642 | left = this.data.cut_left + img_width * scale / 2 >= left ? left : this.data.cut_left + img_width * scale / 2; 643 | left = this.data.cut_left + this.data.width - img_width * scale / 2 <= left ? left : this.data.cut_left + this.data.width - img_width * scale / 2; 644 | top = this.data.cut_top + img_height * scale / 2 >= top ? top : this.data.cut_top + img_height * scale / 2; 645 | top = this.data.cut_top + this.data.height - img_height * scale / 2 <= top ? top : this.data.cut_top + this.data.height - img_height * scale / 2; 646 | this.setData({ 647 | _img_left: left, 648 | _img_top: top, 649 | scale: scale 650 | }) 651 | }, 652 | /** 653 | * 图片边缘检测-缩放 654 | */ 655 | _imgMarginDetectionScale() { 656 | if (!this.data.limit_move) return; 657 | let scale = this.data.scale; 658 | let img_width = this.data.img_width; 659 | let img_height = this.data.img_height; 660 | if (this.data.angle / 90 % 2) { 661 | img_width = this.data.img_height; 662 | img_height = this.data.img_width; 663 | } 664 | if (img_width * scale < this.data.width) { 665 | scale = this.data.width / img_width; 666 | } 667 | if (img_height * scale < this.data.height) { 668 | scale = Math.max(scale, this.data.height / img_height); 669 | } 670 | this._imgMarginDetectionPosition(scale); 671 | }, 672 | _setData(obj) { 673 | let data = {}; 674 | for (var key in obj) { 675 | if (this.data[key] != obj[key]) { 676 | data[key] = obj[key]; 677 | } 678 | } 679 | this.setData(data); 680 | return data; 681 | }, 682 | /** 683 | * 计算图片尺寸 684 | */ 685 | _imgComputeSize() { 686 | let img_width = this.data.img_width, 687 | img_height = this.data.img_height; 688 | if (!this.data.INIT_IMGHEIGHT && !this.data.INIT_IMGWIDTH) { 689 | //默认按图片最小边 = 对应裁剪框尺寸 690 | img_width = this.data.imageObject.width; 691 | img_height = this.data.imageObject.height; 692 | if (img_width / img_height > this.data.width / this.data.height) { 693 | img_height = this.data.height; 694 | img_width = this.data.imageObject.width / this.data.imageObject.height * img_height; 695 | } else { 696 | img_width = this.data.width; 697 | img_height = this.data.imageObject.height / this.data.imageObject.width * img_width; 698 | } 699 | } else if (this.data.INIT_IMGHEIGHT && !this.data.INIT_IMGWIDTH) { 700 | img_width = this.data.imageObject.width / this.data.imageObject.height * this.data.INIT_IMGHEIGHT; 701 | } else if (!this.data.INIT_IMGHEIGHT && this.data.INIT_IMGWIDTH) { 702 | img_height = this.data.imageObject.height / this.data.imageObject.width * this.data.INIT_IMGWIDTH; 703 | } 704 | this.setData({ 705 | img_width: img_width, 706 | img_height: img_height 707 | }); 708 | }, 709 | //改变截取框大小 710 | _computeCutSize() { 711 | if (this.data.width > this.data.info.windowWidth) { 712 | this.setData({ 713 | width: this.data.info.windowWidth, 714 | }); 715 | } else if (this.data.width + this.data.cut_left > this.data.info.windowWidth) { 716 | this.setData({ 717 | cut_left: this.data.info.windowWidth - this.data.cut_left, 718 | }); 719 | }; 720 | if (this.data.height > this.data.info.windowHeight) { 721 | this.setData({ 722 | height: this.data.info.windowHeight, 723 | }); 724 | } else if (this.data.height + this.data.cut_top > this.data.info.windowHeight) { 725 | this.setData({ 726 | cut_top: this.data.info.windowHeight - this.data.cut_top, 727 | }); 728 | }!this.data._canvas_overflow && this._draw(); 729 | }, 730 | //开始触摸 731 | _start(event) { 732 | this.data._flag_img_endtouch = false; 733 | if (event.touches.length == 1) { 734 | //单指拖动 735 | this.data._touch_img_relative[0] = { 736 | x: (event.touches[0].clientX - this.data._img_left), 737 | y: (event.touches[0].clientY - this.data._img_top) 738 | } 739 | } else { 740 | //双指放大 741 | let width = Math.abs(event.touches[0].clientX - event.touches[1].clientX); 742 | let height = Math.abs(event.touches[0].clientY - event.touches[1].clientY); 743 | this.data._touch_img_relative = [{ 744 | x: (event.touches[0].clientX - this.data._img_left), 745 | y: (event.touches[0].clientY - this.data._img_top) 746 | }, { 747 | x: (event.touches[1].clientX - this.data._img_left), 748 | y: (event.touches[1].clientY - this.data._img_top) 749 | }]; 750 | this.data._hypotenuse_length = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); 751 | }!this.data._canvas_overflow && this._draw(); 752 | }, 753 | _move_throttle() { 754 | //安卓需要节流 755 | if (this.data.info.platform == 'android') { 756 | clearTimeout(this.data.MOVE_THROTTLE); 757 | this.data.MOVE_THROTTLE = setTimeout(() => { 758 | this.data.MOVE_THROTTLE_FLAG = true; 759 | }, 1000 / 40) 760 | return this.data.MOVE_THROTTLE_FLAG; 761 | } else { 762 | this.data.MOVE_THROTTLE_FLAG = true; 763 | } 764 | }, 765 | _move(event) { 766 | if (this.data._flag_img_endtouch || !this.data.MOVE_THROTTLE_FLAG) return; 767 | this.data.MOVE_THROTTLE_FLAG = false; 768 | this._move_throttle(); 769 | this._moveDuring(); 770 | if (event.touches.length == 1) { 771 | //单指拖动 772 | let left = (event.touches[0].clientX - this.data._touch_img_relative[0].x), 773 | top = (event.touches[0].clientY - this.data._touch_img_relative[0].y); 774 | //图像边缘检测,防止截取到空白 775 | this.data._img_left = left; 776 | this.data._img_top = top; 777 | this._imgMarginDetectionPosition(); 778 | this.setData({ 779 | _img_left: this.data._img_left, 780 | _img_top: this.data._img_top 781 | }); 782 | } else { 783 | //双指放大 784 | let width = (Math.abs(event.touches[0].clientX - event.touches[1].clientX)), 785 | height = (Math.abs(event.touches[0].clientY - event.touches[1].clientY)), 786 | hypotenuse = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)), 787 | scale = this.data.scale * (hypotenuse / this.data._hypotenuse_length), 788 | current_deg = 0; 789 | scale = scale <= this.data.min_scale ? this.data.min_scale : scale; 790 | scale = scale >= this.data.max_scale ? this.data.max_scale : scale; 791 | //图像边缘检测,防止截取到空白 792 | this.data.scale = scale; 793 | this._imgMarginDetectionScale(); 794 | //双指旋转(如果没禁用旋转) 795 | let _touch_img_relative = [{ 796 | x: (event.touches[0].clientX - this.data._img_left), 797 | y: (event.touches[0].clientY - this.data._img_top) 798 | }, { 799 | x: (event.touches[1].clientX - this.data._img_left), 800 | y: (event.touches[1].clientY - this.data._img_top) 801 | }]; 802 | if (!this.data.disable_rotate) { 803 | let first_atan = 180 / Math.PI * Math.atan2(_touch_img_relative[0].y, _touch_img_relative[0].x); 804 | let first_atan_old = 180 / Math.PI * Math.atan2(this.data._touch_img_relative[0].y, this.data._touch_img_relative[0].x); 805 | let second_atan = 180 / Math.PI * Math.atan2(_touch_img_relative[1].y, _touch_img_relative[1].x); 806 | let second_atan_old = 180 / Math.PI * Math.atan2(this.data._touch_img_relative[1].y, this.data._touch_img_relative[1].x); 807 | //当前旋转的角度 808 | let first_deg = first_atan - first_atan_old, 809 | second_deg = second_atan - second_atan_old; 810 | if (first_deg != 0) { 811 | current_deg = first_deg; 812 | } else if (second_deg != 0) { 813 | current_deg = second_deg; 814 | } 815 | } 816 | this.data._touch_img_relative = _touch_img_relative; 817 | this.data._hypotenuse_length = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); 818 | //更新视图 819 | this.setData({ 820 | angle: this.data.angle + current_deg, 821 | scale: this.data.scale 822 | }); 823 | }!this.data._canvas_overflow && this._draw(); 824 | }, 825 | //结束操作 826 | _end(event) { 827 | this.data._flag_img_endtouch = true; 828 | this._moveStop(); 829 | }, 830 | //点击中间剪裁框处理 831 | _click(event) { 832 | if (!this.data.imgSrc) { 833 | //调起上传 834 | this.upload(); 835 | return; 836 | } 837 | this._draw(() => { 838 | let x = event.detail ? event.detail.x : event.touches[0].clientX; 839 | let y = event.detail ? event.detail.y : event.touches[0].clientY; 840 | if ((x >= this.data.cut_left && x <= (this.data.cut_left + this.data.width)) && (y >= this.data.cut_top && y <= (this.data.cut_top + this.data.height))) { 841 | //生成图片并回调 842 | wx.canvasToTempFilePath({ 843 | width: this.data.width * this.data.export_scale, 844 | height: Math.round(this.data.height * this.data.export_scale), 845 | destWidth: this.data.width * this.data.export_scale, 846 | destHeight: Math.round(this.data.height) * this.data.export_scale, 847 | fileType: 'png', 848 | quality: this.data.quality, 849 | canvasId: this.data.el, 850 | success: (res) => { 851 | this.triggerEvent('tapcut', { 852 | url: res.tempFilePath, 853 | width: this.data.width * this.data.export_scale, 854 | height: this.data.height * this.data.export_scale 855 | }); 856 | } 857 | }, this) 858 | } 859 | }); 860 | }, 861 | //渲染 862 | _draw(callback) { 863 | if (!this.data.imgSrc) return; 864 | let draw = () => { 865 | //图片实际大小 866 | let img_width = this.data.img_width * this.data.scale * this.data.export_scale; 867 | let img_height = this.data.img_height * this.data.scale * this.data.export_scale; 868 | //canvas和图片的相对距离 869 | var xpos = this.data._img_left - this.data.cut_left; 870 | var ypos = this.data._img_top - this.data.cut_top; 871 | //旋转画布 872 | this.data.ctx.translate(xpos * this.data.export_scale, ypos * this.data.export_scale); 873 | this.data.ctx.rotate(this.data.angle * Math.PI / 180); 874 | this.data.ctx.drawImage(this.data.imgSrc, -img_width / 2, -img_height / 2, img_width, img_height); 875 | this.data.ctx.draw(false, () => { 876 | callback && callback(); 877 | }); 878 | } 879 | if (this.data.ctx.width != this.data.width || this.data.ctx.height != this.data.height) { 880 | //优化拖动裁剪框,所以必须把宽高设置放在离用户触发渲染最近的地方 881 | this.setData({ 882 | _canvas_height: this.data.height, 883 | _canvas_width: this.data.width, 884 | }, () => { 885 | //延迟40毫秒防止点击过快出现拉伸或裁剪过多 886 | setTimeout(() => { 887 | draw(); 888 | }, 40); 889 | }); 890 | } else { 891 | draw(); 892 | } 893 | }, 894 | //裁剪框处理 895 | _cutTouchMove(e) { 896 | if (this.data._flag_cut_touch && this.data.MOVE_THROTTLE_FLAG) { 897 | if (this.data.disable_ratio && (this.data.disable_width || this.data.disable_height)) return; 898 | //节流 899 | this.data.MOVE_THROTTLE_FLAG = false; 900 | this._move_throttle(); 901 | let width = this.data.width, 902 | height = this.data.height, 903 | cut_top = this.data.cut_top, 904 | cut_left = this.data.cut_left, 905 | size_correct = () => { 906 | width = width <= this.data.max_width ? width >= this.data.min_width ? width : this.data.min_width : this.data.max_width; 907 | height = height <= this.data.max_height ? height >= this.data.min_height ? height : this.data.min_height : this.data.max_height; 908 | }, 909 | size_inspect = () => { 910 | if ((width > this.data.max_width || width < this.data.min_width || height > this.data.max_height || height < this.data.min_height) && this.data.disable_ratio) { 911 | size_correct(); 912 | return false; 913 | } else { 914 | size_correct(); 915 | return true; 916 | } 917 | }; 918 | height = this.data.CUT_START.height + ((this.data.CUT_START.corner > 1 && this.data.CUT_START.corner < 4 ? 1 : -1) * (this.data.CUT_START.y - e.touches[0].clientY)); 919 | switch (this.data.CUT_START.corner) { 920 | case 1: 921 | width = this.data.CUT_START.width + this.data.CUT_START.x - e.touches[0].clientX; 922 | if (this.data.disable_ratio) { 923 | height = width / (this.data.width / this.data.height) 924 | } 925 | if (!size_inspect()) return; 926 | cut_left = this.data.CUT_START.cut_left - (width - this.data.CUT_START.width); 927 | break 928 | case 2: 929 | width = this.data.CUT_START.width + this.data.CUT_START.x - e.touches[0].clientX; 930 | if (this.data.disable_ratio) { 931 | height = width / (this.data.width / this.data.height) 932 | } 933 | if (!size_inspect()) return; 934 | cut_top = this.data.CUT_START.cut_top - (height - this.data.CUT_START.height) 935 | cut_left = this.data.CUT_START.cut_left - (width - this.data.CUT_START.width) 936 | break 937 | case 3: 938 | width = this.data.CUT_START.width - this.data.CUT_START.x + e.touches[0].clientX; 939 | if (this.data.disable_ratio) { 940 | height = width / (this.data.width / this.data.height) 941 | } 942 | if (!size_inspect()) return; 943 | cut_top = this.data.CUT_START.cut_top - (height - this.data.CUT_START.height); 944 | break 945 | case 4: 946 | width = this.data.CUT_START.width - this.data.CUT_START.x + e.touches[0].clientX; 947 | if (this.data.disable_ratio) { 948 | height = width / (this.data.width / this.data.height) 949 | } 950 | if (!size_inspect()) return; 951 | break 952 | } 953 | if (!this.data.disable_width && !this.data.disable_height) { 954 | this.setData({ 955 | width: width, 956 | cut_left: cut_left, 957 | height: height, 958 | cut_top: cut_top, 959 | }) 960 | } else if (!this.data.disable_width) { 961 | this.setData({ 962 | width: width, 963 | cut_left: cut_left 964 | }) 965 | } else if (!this.data.disable_height) { 966 | this.setData({ 967 | height: height, 968 | cut_top: cut_top 969 | }) 970 | } 971 | this._imgMarginDetectionScale(); 972 | } 973 | }, 974 | _cutTouchStart(e) { 975 | let currentX = e.touches[0].clientX; 976 | let currentY = e.touches[0].clientY; 977 | let cutbox_top4 = this.data.cut_top + this.data.height - 30; 978 | let cutbox_bottom4 = this.data.cut_top + this.data.height + 20; 979 | let cutbox_left4 = this.data.cut_left + this.data.width - 30; 980 | let cutbox_right4 = this.data.cut_left + this.data.width + 30; 981 | 982 | let cutbox_top3 = this.data.cut_top - 30; 983 | let cutbox_bottom3 = this.data.cut_top + 30; 984 | let cutbox_left3 = this.data.cut_left + this.data.width - 30; 985 | let cutbox_right3 = this.data.cut_left + this.data.width + 30; 986 | 987 | let cutbox_top2 = this.data.cut_top - 30; 988 | let cutbox_bottom2 = this.data.cut_top + 30; 989 | let cutbox_left2 = this.data.cut_left - 30; 990 | let cutbox_right2 = this.data.cut_left + 30; 991 | 992 | let cutbox_top1 = this.data.cut_top + this.data.height - 30; 993 | let cutbox_bottom1 = this.data.cut_top + this.data.height + 30; 994 | let cutbox_left1 = this.data.cut_left - 30; 995 | let cutbox_right1 = this.data.cut_left + 30; 996 | if (currentX > cutbox_left4 && currentX < cutbox_right4 && currentY > cutbox_top4 && currentY < cutbox_bottom4) { 997 | this._moveDuring(); 998 | this.data._flag_cut_touch = true; 999 | this.data._flag_img_endtouch = true; 1000 | this.data.CUT_START = { 1001 | width: this.data.width, 1002 | height: this.data.height, 1003 | x: currentX, 1004 | y: currentY, 1005 | corner: 4 1006 | } 1007 | } else if (currentX > cutbox_left3 && currentX < cutbox_right3 && currentY > cutbox_top3 && currentY < cutbox_bottom3) { 1008 | this._moveDuring(); 1009 | this.data._flag_cut_touch = true; 1010 | this.data._flag_img_endtouch = true; 1011 | this.data.CUT_START = { 1012 | width: this.data.width, 1013 | height: this.data.height, 1014 | x: currentX, 1015 | y: currentY, 1016 | cut_top: this.data.cut_top, 1017 | cut_left: this.data.cut_left, 1018 | corner: 3 1019 | } 1020 | } else if (currentX > cutbox_left2 && currentX < cutbox_right2 && currentY > cutbox_top2 && currentY < cutbox_bottom2) { 1021 | this._moveDuring(); 1022 | this.data._flag_cut_touch = true; 1023 | this.data._flag_img_endtouch = true; 1024 | this.data.CUT_START = { 1025 | width: this.data.width, 1026 | height: this.data.height, 1027 | cut_top: this.data.cut_top, 1028 | cut_left: this.data.cut_left, 1029 | x: currentX, 1030 | y: currentY, 1031 | corner: 2 1032 | } 1033 | } else if (currentX > cutbox_left1 && currentX < cutbox_right1 && currentY > cutbox_top1 && currentY < cutbox_bottom1) { 1034 | this._moveDuring(); 1035 | this.data._flag_cut_touch = true; 1036 | this.data._flag_img_endtouch = true; 1037 | this.data.CUT_START = { 1038 | width: this.data.width, 1039 | height: this.data.height, 1040 | cut_top: this.data.cut_top, 1041 | cut_left: this.data.cut_left, 1042 | x: currentX, 1043 | y: currentY, 1044 | corner: 1 1045 | } 1046 | } 1047 | }, 1048 | _cutTouchEnd(e) { 1049 | this._moveStop(); 1050 | this.data._flag_cut_touch = false; 1051 | }, 1052 | //停止移动时需要做的操作 1053 | _moveStop() { 1054 | //清空之前的自动居中延迟函数并添加最新的 1055 | clearTimeout(this.data.TIME_CUT_CENTER); 1056 | this.data.TIME_CUT_CENTER = setTimeout(() => { 1057 | //动画启动 1058 | if (!this.data._cut_animation) { 1059 | this.setData({ 1060 | _cut_animation: true 1061 | }); 1062 | } 1063 | this.setCutCenter(); 1064 | }, 1000) 1065 | //清空之前的背景变化延迟函数并添加最新的 1066 | clearTimeout(this.data.TIME_BG); 1067 | this.data.TIME_BG = setTimeout(() => { 1068 | if (this.data._flag_bright) { 1069 | this.setData({ 1070 | _flag_bright: false 1071 | }); 1072 | } 1073 | }, 2000) 1074 | }, 1075 | //移动中 1076 | _moveDuring() { 1077 | //清空之前的自动居中延迟函数 1078 | clearTimeout(this.data.TIME_CUT_CENTER); 1079 | //清空之前的背景变化延迟函数 1080 | clearTimeout(this.data.TIME_BG); 1081 | //高亮背景 1082 | if (!this.data._flag_bright) { 1083 | this.setData({ 1084 | _flag_bright: true 1085 | }); 1086 | } 1087 | }, 1088 | //监听器 1089 | _watcher() { 1090 | Object.keys(this.data).forEach(v => { 1091 | this._observe(this.data, v, this.data.watch[v]); 1092 | }) 1093 | }, 1094 | _observe(obj, key, watchFun) { 1095 | var val = obj[key]; 1096 | Object.defineProperty(obj, key, { 1097 | configurable: true, 1098 | enumerable: true, 1099 | set: (value) => { 1100 | val = value; 1101 | watchFun && watchFun(val, this); 1102 | }, 1103 | get() { 1104 | if (val && '_img_top|img_left||width|height|min_width|max_width|min_height|max_height|export_scale|cut_top|cut_left|canvas_top|canvas_left|img_width|img_height|scale|angle|min_scale|max_scale'.indexOf(key) != -1) { 1105 | let ret = parseFloat(parseFloat(val).toFixed(3)); 1106 | if (typeof val == "string" && val.indexOf("%") != -1) { 1107 | ret += '%'; 1108 | } 1109 | return ret; 1110 | } 1111 | return val; 1112 | } 1113 | }) 1114 | }, 1115 | _preventTouchMove() {} 1116 | } 1117 | }) -------------------------------------------------------------------------------- /demo/component/image-cropper/image-cropper.js: -------------------------------------------------------------------------------- 1 | Component({ 2 | properties: { 3 | /** 4 | * 图片路径 5 | */ 6 | 'imgSrc': { 7 | type: String 8 | }, 9 | /** 10 | * 裁剪框高度 11 | */ 12 | 'height': { 13 | type: Number, 14 | value: 200 15 | }, 16 | /** 17 | * 裁剪框宽度 18 | */ 19 | 'width': { 20 | type: Number, 21 | value: 200 22 | }, 23 | /** 24 | * 裁剪框最小尺寸 25 | */ 26 | 'min_width': { 27 | type: Number, 28 | value: 100 29 | }, 30 | 'min_height': { 31 | type: Number, 32 | value: 100 33 | }, 34 | /** 35 | * 裁剪框最大尺寸 36 | */ 37 | 'max_width': { 38 | type: Number, 39 | value: 300 40 | }, 41 | 'max_height': { 42 | type: Number, 43 | value: 300 44 | }, 45 | /** 46 | * 裁剪框禁止拖动 47 | */ 48 | 'disable_width': { 49 | type: Boolean, 50 | value: false 51 | }, 52 | 'disable_height': { 53 | type: Boolean, 54 | value: false 55 | }, 56 | /** 57 | * 锁定裁剪框比例 58 | */ 59 | 'disable_ratio': { 60 | type: Boolean, 61 | value: false 62 | }, 63 | /** 64 | * 生成的图片尺寸相对剪裁框的比例 65 | */ 66 | 'export_scale': { 67 | type: Number, 68 | value: 3 69 | }, 70 | /** 71 | * 生成的图片质量0-1 72 | */ 73 | 'quality': { 74 | type: Number, 75 | value: 1 76 | }, 77 | 'cut_top': { 78 | type: Number, 79 | value: null 80 | }, 81 | 'cut_left': { 82 | type: Number, 83 | value: null 84 | }, 85 | /** 86 | * canvas上边距(不设置默认不显示) 87 | */ 88 | 'canvas_top': { 89 | type: Number, 90 | value: null 91 | }, 92 | /** 93 | * canvas左边距(不设置默认不显示) 94 | */ 95 | 'canvas_left': { 96 | type: Number, 97 | value: null 98 | }, 99 | /** 100 | * 图片宽度 101 | */ 102 | 'img_width': { 103 | type: null, 104 | value: null 105 | }, 106 | /** 107 | * 图片高度 108 | */ 109 | 'img_height': { 110 | type: null, 111 | value: null 112 | }, 113 | /** 114 | * 图片缩放比 115 | */ 116 | 'scale': { 117 | type: Number, 118 | value: 1 119 | }, 120 | /** 121 | * 图片旋转角度 122 | */ 123 | 'angle': { 124 | type: Number, 125 | value: 0 126 | }, 127 | /** 128 | * 最小缩放比 129 | */ 130 | 'min_scale': { 131 | type: Number, 132 | value: 0.5 133 | }, 134 | /** 135 | * 最大缩放比 136 | */ 137 | 'max_scale': { 138 | type: Number, 139 | value: 2 140 | }, 141 | /** 142 | * 是否禁用旋转 143 | */ 144 | 'disable_rotate': { 145 | type: Boolean, 146 | value: false 147 | }, 148 | /** 149 | * 是否限制移动范围(剪裁框只能在图片内) 150 | */ 151 | 'limit_move': { 152 | type: Boolean, 153 | value: false 154 | } 155 | }, 156 | data: { 157 | el: 'image-cropper', //暂时无用 158 | info: wx.getSystemInfoSync(), 159 | MOVE_THROTTLE: null, //触摸移动节流settimeout 160 | MOVE_THROTTLE_FLAG: true, //节流标识 161 | INIT_IMGWIDTH: 0, //图片设置尺寸,此值不变(记录最初设定的尺寸) 162 | INIT_IMGHEIGHT: 0, //图片设置尺寸,此值不变(记录最初设定的尺寸) 163 | TIME_BG: null, //背景变暗延时函数 164 | TIME_CUT_CENTER: null, 165 | _touch_img_relative: [{ 166 | x: 0, 167 | y: 0 168 | }], //鼠标和图片中心的相对位置 169 | _flag_cut_touch: false, //是否是拖动裁剪框 170 | _hypotenuse_length: 0, //双指触摸时斜边长度 171 | _flag_img_endtouch: false, //是否结束触摸 172 | _flag_bright: true, //背景是否亮 173 | _canvas_overflow: true, //canvas缩略图是否在屏幕外面 174 | _canvas_width: 200, 175 | _canvas_height: 200, 176 | origin_x: 0.5, //图片旋转中心 177 | origin_y: 0.5, //图片旋转中心 178 | _cut_animation: false, //是否开启图片和裁剪框过渡 179 | _img_top: wx.getSystemInfoSync().windowHeight / 2, //图片上边距 180 | _img_left: wx.getSystemInfoSync().windowWidth / 2, //图片左边距 181 | watch: { 182 | //监听截取框宽高变化 183 | width(value, that) { 184 | if (value < that.data.min_width) { 185 | that.setData({ 186 | width: that.data.min_width 187 | }); 188 | } 189 | that._computeCutSize(); 190 | }, 191 | height(value, that) { 192 | if (value < that.data.min_height) { 193 | that.setData({ 194 | height: that.data.min_height 195 | }); 196 | } 197 | that._computeCutSize(); 198 | }, 199 | angle(value, that) { 200 | //停止居中裁剪框,继续修改图片位置 201 | that._moveStop(); 202 | if (that.data.limit_move) { 203 | if (that.data.angle % 90) { 204 | that.setData({ 205 | angle: Math.round(that.data.angle / 90) * 90 206 | }); 207 | return; 208 | } 209 | } 210 | }, 211 | _cut_animation(value, that) { 212 | //开启过渡300毫秒之后自动关闭 213 | clearTimeout(that.data._cut_animation_time); 214 | if (value) { 215 | that.data._cut_animation_time = setTimeout(() => { 216 | that.setData({ 217 | _cut_animation: false 218 | }); 219 | }, 300) 220 | } 221 | }, 222 | limit_move(value, that) { 223 | if (value) { 224 | if (that.data.angle % 90) { 225 | that.setData({ 226 | angle: Math.round(that.data.angle / 90) * 90 227 | }); 228 | } 229 | that._imgMarginDetectionScale(); 230 | !that.data._canvas_overflow && that._draw(); 231 | } 232 | }, 233 | canvas_top(value, that) { 234 | that._canvasDetectionPosition(); 235 | }, 236 | canvas_left(value, that) { 237 | that._canvasDetectionPosition(); 238 | }, 239 | imgSrc(value, that) { 240 | that.pushImg(); 241 | }, 242 | cut_top(value, that) { 243 | that._cutDetectionPosition(); 244 | if (that.data.limit_move) { 245 | !that.data._canvas_overflow && that._draw(); 246 | } 247 | }, 248 | cut_left(value, that) { 249 | that._cutDetectionPosition(); 250 | if (that.data.limit_move) { 251 | !that.data._canvas_overflow && that._draw(); 252 | } 253 | } 254 | } 255 | }, 256 | attached() { 257 | this.data.info = wx.getSystemInfoSync(); 258 | //启用数据监听 259 | this._watcher(); 260 | this.data.INIT_IMGWIDTH = this.data.img_width; 261 | this.data.INIT_IMGHEIGHT = this.data.img_height; 262 | this.setData({ 263 | _canvas_height: this.data.height, 264 | _canvas_width: this.data.width, 265 | }); 266 | this._initCanvas(); 267 | this.data.imgSrc && (this.data.imgSrc = this.data.imgSrc); 268 | //根据开发者设置的图片目标尺寸计算实际尺寸 269 | this._initImageSize(); 270 | //设置裁剪框大小>设置图片尺寸>绘制canvas 271 | this._computeCutSize(); 272 | //检查裁剪框是否在范围内 273 | this._cutDetectionPosition(); 274 | //检查canvas是否在范围内 275 | this._canvasDetectionPosition(); 276 | //初始化完成 277 | this.triggerEvent('load', { 278 | cropper: this 279 | }); 280 | }, 281 | methods: { 282 | /** 283 | * 上传图片 284 | */ 285 | upload() { 286 | let that = this; 287 | wx.chooseImage({ 288 | count: 1, 289 | sizeType: ['original', 'compressed'], 290 | sourceType: ['album', 'camera'], 291 | success(res) { 292 | const tempFilePaths = res.tempFilePaths[0]; 293 | that.pushImg(tempFilePaths); 294 | wx.showLoading({ 295 | title: '加载中...' 296 | }) 297 | } 298 | }) 299 | }, 300 | /** 301 | * 返回图片信息 302 | */ 303 | getImg(getCallback) { 304 | this._draw(() => { 305 | wx.canvasToTempFilePath({ 306 | width: this.data.width * this.data.export_scale, 307 | height: Math.round(this.data.height * this.data.export_scale), 308 | destWidth: this.data.width * this.data.export_scale, 309 | destHeight: Math.round(this.data.height) * this.data.export_scale, 310 | fileType: 'png', 311 | quality: this.data.quality, 312 | canvasId: this.data.el, 313 | success: (res) => { 314 | getCallback({ 315 | url: res.tempFilePath, 316 | width: this.data.width * this.data.export_scale, 317 | height: this.data.height * this.data.export_scale 318 | }); 319 | } 320 | }, this) 321 | }); 322 | }, 323 | /** 324 | * 设置图片动画 325 | * { 326 | * x:10,//图片在原有基础上向下移动10px 327 | * y:10,//图片在原有基础上向右移动10px 328 | * angle:10,//图片在原有基础上旋转10deg 329 | * scale:0.5,//图片在原有基础上增加0.5倍 330 | * } 331 | */ 332 | setTransform(transform) { 333 | if (!transform) return; 334 | if (!this.data.disable_rotate) { 335 | this.setData({ 336 | angle: transform.angle ? this.data.angle + transform.angle : this.data.angle 337 | }); 338 | } 339 | var scale = this.data.scale; 340 | if (transform.scale) { 341 | scale = this.data.scale + transform.scale; 342 | scale = scale <= this.data.min_scale ? this.data.min_scale : scale; 343 | scale = scale >= this.data.max_scale ? this.data.max_scale : scale; 344 | } 345 | this.data.scale = scale; 346 | let cutX = this.data.cut_left; 347 | let cutY = this.data.cut_top; 348 | if (transform.cutX) { 349 | this.setData({ 350 | cut_left: cutX + transform.cutX 351 | }); 352 | this.data.watch.cut_left(null, this); 353 | } 354 | if (transform.cutY) { 355 | this.setData({ 356 | cut_top: cutY + transform.cutY 357 | }); 358 | this.data.watch.cut_top(null, this); 359 | } 360 | this.data._img_top = transform.y ? this.data._img_top + transform.y : this.data._img_top; 361 | this.data._img_left = transform.x ? this.data._img_left + transform.x : this.data._img_left; 362 | //图像边缘检测,防止截取到空白 363 | this._imgMarginDetectionScale(); 364 | //停止居中裁剪框,继续修改图片位置 365 | this._moveDuring(); 366 | this.setData({ 367 | scale: this.data.scale, 368 | _img_top: this.data._img_top, 369 | _img_left: this.data._img_left 370 | }); 371 | !this.data._canvas_overflow && this._draw(); 372 | //可以居中裁剪框了 373 | this._moveStop(); //结束操作 374 | }, 375 | /** 376 | * 设置剪裁框位置 377 | */ 378 | setCutXY(x, y) { 379 | this.setData({ 380 | cut_top: y, 381 | cut_left: x 382 | }); 383 | }, 384 | /** 385 | * 设置剪裁框尺寸 386 | */ 387 | setCutSize(w, h) { 388 | this.setData({ 389 | width: w, 390 | height: h 391 | }); 392 | this._computeCutSize(); 393 | }, 394 | /** 395 | * 设置剪裁框和图片居中 396 | */ 397 | setCutCenter() { 398 | let cut_top = (this.data.info.windowHeight - this.data.height) * 0.5; 399 | let cut_left = (this.data.info.windowWidth - this.data.width) * 0.5; 400 | //顺序不能变 401 | this.setData({ 402 | _img_top: this.data._img_top - this.data.cut_top + cut_top, 403 | cut_top: cut_top, //截取的框上边距 404 | _img_left: this.data._img_left - this.data.cut_left + cut_left, 405 | cut_left: cut_left, //截取的框左边距 406 | }); 407 | }, 408 | _setCutCenter() { 409 | let cut_top = (this.data.info.windowHeight - this.data.height) * 0.5; 410 | let cut_left = (this.data.info.windowWidth - this.data.width) * 0.5; 411 | this.setData({ 412 | cut_top: cut_top, //截取的框上边距 413 | cut_left: cut_left, //截取的框左边距 414 | }); 415 | }, 416 | /** 417 | * 设置剪裁框宽度-即将废弃 418 | */ 419 | setWidth(width) { 420 | this.setData({ 421 | width: width 422 | }); 423 | this._computeCutSize(); 424 | }, 425 | /** 426 | * 设置剪裁框高度-即将废弃 427 | */ 428 | setHeight(height) { 429 | this.setData({ 430 | height: height 431 | }); 432 | this._computeCutSize(); 433 | }, 434 | /** 435 | * 是否锁定旋转 436 | */ 437 | setDisableRotate(value) { 438 | this.data.disable_rotate = value; 439 | }, 440 | /** 441 | * 是否限制移动 442 | */ 443 | setLimitMove(value) { 444 | this.setData({ 445 | _cut_animation: true, 446 | limit_move: !!value 447 | }); 448 | }, 449 | /** 450 | * 初始化图片,包括位置、大小、旋转角度 451 | */ 452 | imgReset() { 453 | this.setData({ 454 | scale: 1, 455 | angle: 0, 456 | _img_top: wx.getSystemInfoSync().windowHeight / 2, 457 | _img_left: wx.getSystemInfoSync().windowWidth / 2, 458 | }) 459 | }, 460 | /** 461 | * 加载(更换)图片 462 | */ 463 | pushImg(src) { 464 | if (src) { 465 | this.setData({ 466 | imgSrc: src 467 | }); 468 | //发现是手动赋值直接返回,交给watch处理 469 | return; 470 | } 471 | 472 | // getImageInfo接口传入 src: '' 会导致内存泄漏 473 | 474 | if (!this.data.imgSrc) return; 475 | wx.getImageInfo({ 476 | src: this.data.imgSrc, 477 | success: (res) => { 478 | this.data.imageObject = res; 479 | //图片非本地路径需要换成本地路径 480 | if (this.data.imgSrc.search(/tmp/) == -1) { 481 | this.setData({ 482 | imgSrc: res.path 483 | }); 484 | } 485 | //计算最后图片尺寸 486 | this._imgComputeSize(); 487 | if (this.data.limit_move) { 488 | //限制移动,不留空白处理 489 | this._imgMarginDetectionScale(); 490 | } 491 | this._draw(); 492 | }, 493 | fail: (err) => { 494 | this.setData({ 495 | imgSrc: '' 496 | }); 497 | } 498 | }); 499 | }, 500 | imageLoad(e) { 501 | setTimeout(() => { 502 | this.triggerEvent('imageload', this.data.imageObject); 503 | 504 | }, 1000) 505 | }, 506 | /** 507 | * 设置图片放大缩小 508 | */ 509 | setScale(scale) { 510 | if (!scale) return; 511 | this.setData({ 512 | scale: scale 513 | }); 514 | !this.data._canvas_overflow && this._draw(); 515 | }, 516 | /** 517 | * 设置图片旋转角度 518 | */ 519 | setAngle(angle) { 520 | if (!angle) return; 521 | this.setData({ 522 | _cut_animation: true, 523 | angle: angle 524 | }); 525 | this._imgMarginDetectionScale(); 526 | !this.data._canvas_overflow && this._draw(); 527 | }, 528 | _initCanvas() { 529 | //初始化canvas 530 | if (!this.data.ctx) { 531 | this.data.ctx = wx.createCanvasContext("image-cropper", this); 532 | } 533 | }, 534 | /** 535 | * 根据开发者设置的图片目标尺寸计算实际尺寸 536 | */ 537 | _initImageSize() { 538 | //处理宽高特殊单位 %>px 539 | if (this.data.INIT_IMGWIDTH && typeof this.data.INIT_IMGWIDTH == "string" && this.data.INIT_IMGWIDTH.indexOf("%") != -1) { 540 | let width = this.data.INIT_IMGWIDTH.replace("%", ""); 541 | this.data.INIT_IMGWIDTH = this.data.img_width = this.data.info.windowWidth / 100 * width; 542 | } 543 | if (this.data.INIT_IMGHEIGHT && typeof this.data.INIT_IMGHEIGHT == "string" && this.data.INIT_IMGHEIGHT.indexOf("%") != -1) { 544 | let height = this.data.img_height.replace("%", ""); 545 | this.data.INIT_IMGHEIGHT = this.data.img_height = this.data.info.windowHeight / 100 * height; 546 | } 547 | }, 548 | /** 549 | * 检测剪裁框位置是否在允许的范围内(屏幕内) 550 | */ 551 | _cutDetectionPosition() { 552 | let _cutDetectionPositionTop = () => { 553 | //检测上边距是否在范围内 554 | if (this.data.cut_top < 0) { 555 | this.setData({ 556 | cut_top: 0 557 | }); 558 | } 559 | if (this.data.cut_top > this.data.info.windowHeight - this.data.height) { 560 | this.setData({ 561 | cut_top: this.data.info.windowHeight - this.data.height 562 | }); 563 | } 564 | }, 565 | _cutDetectionPositionLeft = () => { 566 | //检测左边距是否在范围内 567 | if (this.data.cut_left < 0) { 568 | this.setData({ 569 | cut_left: 0 570 | }); 571 | } 572 | if (this.data.cut_left > this.data.info.windowWidth - this.data.width) { 573 | this.setData({ 574 | cut_left: this.data.info.windowWidth - this.data.width 575 | }); 576 | } 577 | }; 578 | //裁剪框坐标处理(如果只写一个参数则另一个默认为0,都不写默认居中) 579 | if (this.data.cut_top == null && this.data.cut_left == null) { 580 | this._setCutCenter(); 581 | } else if (this.data.cut_top != null && this.data.cut_left != null) { 582 | _cutDetectionPositionTop(); 583 | _cutDetectionPositionLeft(); 584 | } else if (this.data.cut_top != null && this.data.cut_left == null) { 585 | _cutDetectionPositionTop(); 586 | this.setData({ 587 | cut_left: (this.data.info.windowWidth - this.data.width) / 2 588 | }); 589 | } else if (this.data.cut_top == null && this.data.cut_left != null) { 590 | _cutDetectionPositionLeft(); 591 | this.setData({ 592 | cut_top: (this.data.info.windowHeight - this.data.height) / 2 593 | }); 594 | } 595 | }, 596 | /** 597 | * 检测canvas位置是否在允许的范围内(屏幕内)如果在屏幕外则不开启实时渲染 598 | * 如果只写一个参数则另一个默认为0,都不写默认超出屏幕外 599 | */ 600 | _canvasDetectionPosition() { 601 | if (this.data.canvas_top == null && this.data.canvas_left == null) { 602 | this.data._canvas_overflow = false; 603 | this.setData({ 604 | canvas_top: -5000, 605 | canvas_left: -5000 606 | }); 607 | } else if (this.data.canvas_top != null && this.data.canvas_left != null) { 608 | if (this.data.canvas_top < -this.data.height || this.data.canvas_top > this.data.info.windowHeight) { 609 | this.data._canvas_overflow = true; 610 | } else { 611 | this.data._canvas_overflow = false; 612 | } 613 | } else if (this.data.canvas_top != null && this.data.canvas_left == null) { 614 | this.setData({ 615 | canvas_left: 0 616 | }); 617 | } else if (this.data.canvas_top == null && this.data.canvas_left != null) { 618 | this.setData({ 619 | canvas_top: 0 620 | }); 621 | if (this.data.canvas_left < -this.data.width || this.data.canvas_left > this.data.info.windowWidth) { 622 | this.data._canvas_overflow = true; 623 | } else { 624 | this.data._canvas_overflow = false; 625 | } 626 | } 627 | }, 628 | /** 629 | * 图片边缘检测-位置 630 | */ 631 | _imgMarginDetectionPosition(scale) { 632 | if (!this.data.limit_move) return; 633 | let left = this.data._img_left; 634 | let top = this.data._img_top; 635 | var scale = scale || this.data.scale; 636 | let img_width = this.data.img_width; 637 | let img_height = this.data.img_height; 638 | if (this.data.angle / 90 % 2) { 639 | img_width = this.data.img_height; 640 | img_height = this.data.img_width; 641 | } 642 | left = this.data.cut_left + img_width * scale / 2 >= left ? left : this.data.cut_left + img_width * scale / 2; 643 | left = this.data.cut_left + this.data.width - img_width * scale / 2 <= left ? left : this.data.cut_left + this.data.width - img_width * scale / 2; 644 | top = this.data.cut_top + img_height * scale / 2 >= top ? top : this.data.cut_top + img_height * scale / 2; 645 | top = this.data.cut_top + this.data.height - img_height * scale / 2 <= top ? top : this.data.cut_top + this.data.height - img_height * scale / 2; 646 | this.setData({ 647 | _img_left: left, 648 | _img_top: top, 649 | scale: scale 650 | }) 651 | }, 652 | /** 653 | * 图片边缘检测-缩放 654 | */ 655 | _imgMarginDetectionScale() { 656 | if (!this.data.limit_move) return; 657 | let scale = this.data.scale; 658 | let img_width = this.data.img_width; 659 | let img_height = this.data.img_height; 660 | if (this.data.angle / 90 % 2) { 661 | img_width = this.data.img_height; 662 | img_height = this.data.img_width; 663 | } 664 | if (img_width * scale < this.data.width) { 665 | scale = this.data.width / img_width; 666 | } 667 | if (img_height * scale < this.data.height) { 668 | scale = Math.max(scale, this.data.height / img_height); 669 | } 670 | this._imgMarginDetectionPosition(scale); 671 | }, 672 | _setData(obj) { 673 | let data = {}; 674 | for (var key in obj) { 675 | if (this.data[key] != obj[key]) { 676 | data[key] = obj[key]; 677 | } 678 | } 679 | this.setData(data); 680 | return data; 681 | }, 682 | /** 683 | * 计算图片尺寸 684 | */ 685 | _imgComputeSize() { 686 | let img_width = this.data.img_width, 687 | img_height = this.data.img_height; 688 | if (!this.data.INIT_IMGHEIGHT && !this.data.INIT_IMGWIDTH) { 689 | //默认按图片最小边 = 对应裁剪框尺寸 690 | img_width = this.data.imageObject.width; 691 | img_height = this.data.imageObject.height; 692 | if (img_width / img_height > this.data.width / this.data.height) { 693 | img_height = this.data.height; 694 | img_width = this.data.imageObject.width / this.data.imageObject.height * img_height; 695 | } else { 696 | img_width = this.data.width; 697 | img_height = this.data.imageObject.height / this.data.imageObject.width * img_width; 698 | } 699 | } else if (this.data.INIT_IMGHEIGHT && !this.data.INIT_IMGWIDTH) { 700 | img_width = this.data.imageObject.width / this.data.imageObject.height * this.data.INIT_IMGHEIGHT; 701 | } else if (!this.data.INIT_IMGHEIGHT && this.data.INIT_IMGWIDTH) { 702 | img_height = this.data.imageObject.height / this.data.imageObject.width * this.data.INIT_IMGWIDTH; 703 | } 704 | this.setData({ 705 | img_width: img_width, 706 | img_height: img_height 707 | }); 708 | }, 709 | //改变截取框大小 710 | _computeCutSize() { 711 | if (this.data.width > this.data.info.windowWidth) { 712 | this.setData({ 713 | width: this.data.info.windowWidth, 714 | }); 715 | } else if (this.data.width + this.data.cut_left > this.data.info.windowWidth) { 716 | this.setData({ 717 | cut_left: this.data.info.windowWidth - this.data.cut_left, 718 | }); 719 | }; 720 | if (this.data.height > this.data.info.windowHeight) { 721 | this.setData({ 722 | height: this.data.info.windowHeight, 723 | }); 724 | } else if (this.data.height + this.data.cut_top > this.data.info.windowHeight) { 725 | this.setData({ 726 | cut_top: this.data.info.windowHeight - this.data.cut_top, 727 | }); 728 | }!this.data._canvas_overflow && this._draw(); 729 | }, 730 | //开始触摸 731 | _start(event) { 732 | this.data._flag_img_endtouch = false; 733 | if (event.touches.length == 1) { 734 | //单指拖动 735 | this.data._touch_img_relative[0] = { 736 | x: (event.touches[0].clientX - this.data._img_left), 737 | y: (event.touches[0].clientY - this.data._img_top) 738 | } 739 | } else { 740 | //双指放大 741 | let width = Math.abs(event.touches[0].clientX - event.touches[1].clientX); 742 | let height = Math.abs(event.touches[0].clientY - event.touches[1].clientY); 743 | this.data._touch_img_relative = [{ 744 | x: (event.touches[0].clientX - this.data._img_left), 745 | y: (event.touches[0].clientY - this.data._img_top) 746 | }, { 747 | x: (event.touches[1].clientX - this.data._img_left), 748 | y: (event.touches[1].clientY - this.data._img_top) 749 | }]; 750 | this.data._hypotenuse_length = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); 751 | }!this.data._canvas_overflow && this._draw(); 752 | }, 753 | _move_throttle() { 754 | //安卓需要节流 755 | if (this.data.info.platform == 'android') { 756 | clearTimeout(this.data.MOVE_THROTTLE); 757 | this.data.MOVE_THROTTLE = setTimeout(() => { 758 | this.data.MOVE_THROTTLE_FLAG = true; 759 | }, 1000 / 40) 760 | return this.data.MOVE_THROTTLE_FLAG; 761 | } else { 762 | this.data.MOVE_THROTTLE_FLAG = true; 763 | } 764 | }, 765 | _move(event) { 766 | if (this.data._flag_img_endtouch || !this.data.MOVE_THROTTLE_FLAG) return; 767 | this.data.MOVE_THROTTLE_FLAG = false; 768 | this._move_throttle(); 769 | this._moveDuring(); 770 | if (event.touches.length == 1) { 771 | //单指拖动 772 | let left = (event.touches[0].clientX - this.data._touch_img_relative[0].x), 773 | top = (event.touches[0].clientY - this.data._touch_img_relative[0].y); 774 | //图像边缘检测,防止截取到空白 775 | this.data._img_left = left; 776 | this.data._img_top = top; 777 | this._imgMarginDetectionPosition(); 778 | this.setData({ 779 | _img_left: this.data._img_left, 780 | _img_top: this.data._img_top 781 | }); 782 | } else { 783 | //双指放大 784 | let width = (Math.abs(event.touches[0].clientX - event.touches[1].clientX)), 785 | height = (Math.abs(event.touches[0].clientY - event.touches[1].clientY)), 786 | hypotenuse = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)), 787 | scale = this.data.scale * (hypotenuse / this.data._hypotenuse_length), 788 | current_deg = 0; 789 | scale = scale <= this.data.min_scale ? this.data.min_scale : scale; 790 | scale = scale >= this.data.max_scale ? this.data.max_scale : scale; 791 | //图像边缘检测,防止截取到空白 792 | this.data.scale = scale; 793 | this._imgMarginDetectionScale(); 794 | //双指旋转(如果没禁用旋转) 795 | let _touch_img_relative = [{ 796 | x: (event.touches[0].clientX - this.data._img_left), 797 | y: (event.touches[0].clientY - this.data._img_top) 798 | }, { 799 | x: (event.touches[1].clientX - this.data._img_left), 800 | y: (event.touches[1].clientY - this.data._img_top) 801 | }]; 802 | if (!this.data.disable_rotate) { 803 | let first_atan = 180 / Math.PI * Math.atan2(_touch_img_relative[0].y, _touch_img_relative[0].x); 804 | let first_atan_old = 180 / Math.PI * Math.atan2(this.data._touch_img_relative[0].y, this.data._touch_img_relative[0].x); 805 | let second_atan = 180 / Math.PI * Math.atan2(_touch_img_relative[1].y, _touch_img_relative[1].x); 806 | let second_atan_old = 180 / Math.PI * Math.atan2(this.data._touch_img_relative[1].y, this.data._touch_img_relative[1].x); 807 | //当前旋转的角度 808 | let first_deg = first_atan - first_atan_old, 809 | second_deg = second_atan - second_atan_old; 810 | if (first_deg != 0) { 811 | current_deg = first_deg; 812 | } else if (second_deg != 0) { 813 | current_deg = second_deg; 814 | } 815 | } 816 | this.data._touch_img_relative = _touch_img_relative; 817 | this.data._hypotenuse_length = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); 818 | //更新视图 819 | this.setData({ 820 | angle: this.data.angle + current_deg, 821 | scale: this.data.scale 822 | }); 823 | }!this.data._canvas_overflow && this._draw(); 824 | }, 825 | //结束操作 826 | _end(event) { 827 | this.data._flag_img_endtouch = true; 828 | this._moveStop(); 829 | }, 830 | //点击中间剪裁框处理 831 | _click(event) { 832 | if (!this.data.imgSrc) { 833 | //调起上传 834 | this.upload(); 835 | return; 836 | } 837 | this._draw(() => { 838 | let x = event.detail ? event.detail.x : event.touches[0].clientX; 839 | let y = event.detail ? event.detail.y : event.touches[0].clientY; 840 | if ((x >= this.data.cut_left && x <= (this.data.cut_left + this.data.width)) && (y >= this.data.cut_top && y <= (this.data.cut_top + this.data.height))) { 841 | //生成图片并回调 842 | wx.canvasToTempFilePath({ 843 | width: this.data.width * this.data.export_scale, 844 | height: Math.round(this.data.height * this.data.export_scale), 845 | destWidth: this.data.width * this.data.export_scale, 846 | destHeight: Math.round(this.data.height) * this.data.export_scale, 847 | fileType: 'png', 848 | quality: this.data.quality, 849 | canvasId: this.data.el, 850 | success: (res) => { 851 | this.triggerEvent('tapcut', { 852 | url: res.tempFilePath, 853 | width: this.data.width * this.data.export_scale, 854 | height: this.data.height * this.data.export_scale 855 | }); 856 | } 857 | }, this) 858 | } 859 | }); 860 | }, 861 | //渲染 862 | _draw(callback) { 863 | if (!this.data.imgSrc) return; 864 | let draw = () => { 865 | //图片实际大小 866 | let img_width = this.data.img_width * this.data.scale * this.data.export_scale; 867 | let img_height = this.data.img_height * this.data.scale * this.data.export_scale; 868 | //canvas和图片的相对距离 869 | var xpos = this.data._img_left - this.data.cut_left; 870 | var ypos = this.data._img_top - this.data.cut_top; 871 | //旋转画布 872 | this.data.ctx.translate(xpos * this.data.export_scale, ypos * this.data.export_scale); 873 | this.data.ctx.rotate(this.data.angle * Math.PI / 180); 874 | this.data.ctx.drawImage(this.data.imgSrc, -img_width / 2, -img_height / 2, img_width, img_height); 875 | this.data.ctx.draw(false, () => { 876 | callback && callback(); 877 | }); 878 | } 879 | if (this.data.ctx.width != this.data.width || this.data.ctx.height != this.data.height) { 880 | //优化拖动裁剪框,所以必须把宽高设置放在离用户触发渲染最近的地方 881 | this.setData({ 882 | _canvas_height: this.data.height, 883 | _canvas_width: this.data.width, 884 | }, () => { 885 | //延迟40毫秒防止点击过快出现拉伸或裁剪过多 886 | setTimeout(() => { 887 | draw(); 888 | }, 40); 889 | }); 890 | } else { 891 | draw(); 892 | } 893 | }, 894 | //裁剪框处理 895 | _cutTouchMove(e) { 896 | if (this.data._flag_cut_touch && this.data.MOVE_THROTTLE_FLAG) { 897 | if (this.data.disable_ratio && (this.data.disable_width || this.data.disable_height)) return; 898 | //节流 899 | this.data.MOVE_THROTTLE_FLAG = false; 900 | this._move_throttle(); 901 | let width = this.data.width, 902 | height = this.data.height, 903 | cut_top = this.data.cut_top, 904 | cut_left = this.data.cut_left, 905 | size_correct = () => { 906 | width = width <= this.data.max_width ? width >= this.data.min_width ? width : this.data.min_width : this.data.max_width; 907 | height = height <= this.data.max_height ? height >= this.data.min_height ? height : this.data.min_height : this.data.max_height; 908 | }, 909 | size_inspect = () => { 910 | if ((width > this.data.max_width || width < this.data.min_width || height > this.data.max_height || height < this.data.min_height) && this.data.disable_ratio) { 911 | size_correct(); 912 | return false; 913 | } else { 914 | size_correct(); 915 | return true; 916 | } 917 | }; 918 | height = this.data.CUT_START.height + ((this.data.CUT_START.corner > 1 && this.data.CUT_START.corner < 4 ? 1 : -1) * (this.data.CUT_START.y - e.touches[0].clientY)); 919 | switch (this.data.CUT_START.corner) { 920 | case 1: 921 | width = this.data.CUT_START.width + this.data.CUT_START.x - e.touches[0].clientX; 922 | if (this.data.disable_ratio) { 923 | height = width / (this.data.width / this.data.height) 924 | } 925 | if (!size_inspect()) return; 926 | cut_left = this.data.CUT_START.cut_left - (width - this.data.CUT_START.width); 927 | break 928 | case 2: 929 | width = this.data.CUT_START.width + this.data.CUT_START.x - e.touches[0].clientX; 930 | if (this.data.disable_ratio) { 931 | height = width / (this.data.width / this.data.height) 932 | } 933 | if (!size_inspect()) return; 934 | cut_top = this.data.CUT_START.cut_top - (height - this.data.CUT_START.height) 935 | cut_left = this.data.CUT_START.cut_left - (width - this.data.CUT_START.width) 936 | break 937 | case 3: 938 | width = this.data.CUT_START.width - this.data.CUT_START.x + e.touches[0].clientX; 939 | if (this.data.disable_ratio) { 940 | height = width / (this.data.width / this.data.height) 941 | } 942 | if (!size_inspect()) return; 943 | cut_top = this.data.CUT_START.cut_top - (height - this.data.CUT_START.height); 944 | break 945 | case 4: 946 | width = this.data.CUT_START.width - this.data.CUT_START.x + e.touches[0].clientX; 947 | if (this.data.disable_ratio) { 948 | height = width / (this.data.width / this.data.height) 949 | } 950 | if (!size_inspect()) return; 951 | break 952 | } 953 | if (!this.data.disable_width && !this.data.disable_height) { 954 | this.setData({ 955 | width: width, 956 | cut_left: cut_left, 957 | height: height, 958 | cut_top: cut_top, 959 | }) 960 | } else if (!this.data.disable_width) { 961 | this.setData({ 962 | width: width, 963 | cut_left: cut_left 964 | }) 965 | } else if (!this.data.disable_height) { 966 | this.setData({ 967 | height: height, 968 | cut_top: cut_top 969 | }) 970 | } 971 | this._imgMarginDetectionScale(); 972 | } 973 | }, 974 | _cutTouchStart(e) { 975 | let currentX = e.touches[0].clientX; 976 | let currentY = e.touches[0].clientY; 977 | let cutbox_top4 = this.data.cut_top + this.data.height - 30; 978 | let cutbox_bottom4 = this.data.cut_top + this.data.height + 20; 979 | let cutbox_left4 = this.data.cut_left + this.data.width - 30; 980 | let cutbox_right4 = this.data.cut_left + this.data.width + 30; 981 | 982 | let cutbox_top3 = this.data.cut_top - 30; 983 | let cutbox_bottom3 = this.data.cut_top + 30; 984 | let cutbox_left3 = this.data.cut_left + this.data.width - 30; 985 | let cutbox_right3 = this.data.cut_left + this.data.width + 30; 986 | 987 | let cutbox_top2 = this.data.cut_top - 30; 988 | let cutbox_bottom2 = this.data.cut_top + 30; 989 | let cutbox_left2 = this.data.cut_left - 30; 990 | let cutbox_right2 = this.data.cut_left + 30; 991 | 992 | let cutbox_top1 = this.data.cut_top + this.data.height - 30; 993 | let cutbox_bottom1 = this.data.cut_top + this.data.height + 30; 994 | let cutbox_left1 = this.data.cut_left - 30; 995 | let cutbox_right1 = this.data.cut_left + 30; 996 | if (currentX > cutbox_left4 && currentX < cutbox_right4 && currentY > cutbox_top4 && currentY < cutbox_bottom4) { 997 | this._moveDuring(); 998 | this.data._flag_cut_touch = true; 999 | this.data._flag_img_endtouch = true; 1000 | this.data.CUT_START = { 1001 | width: this.data.width, 1002 | height: this.data.height, 1003 | x: currentX, 1004 | y: currentY, 1005 | corner: 4 1006 | } 1007 | } else if (currentX > cutbox_left3 && currentX < cutbox_right3 && currentY > cutbox_top3 && currentY < cutbox_bottom3) { 1008 | this._moveDuring(); 1009 | this.data._flag_cut_touch = true; 1010 | this.data._flag_img_endtouch = true; 1011 | this.data.CUT_START = { 1012 | width: this.data.width, 1013 | height: this.data.height, 1014 | x: currentX, 1015 | y: currentY, 1016 | cut_top: this.data.cut_top, 1017 | cut_left: this.data.cut_left, 1018 | corner: 3 1019 | } 1020 | } else if (currentX > cutbox_left2 && currentX < cutbox_right2 && currentY > cutbox_top2 && currentY < cutbox_bottom2) { 1021 | this._moveDuring(); 1022 | this.data._flag_cut_touch = true; 1023 | this.data._flag_img_endtouch = true; 1024 | this.data.CUT_START = { 1025 | width: this.data.width, 1026 | height: this.data.height, 1027 | cut_top: this.data.cut_top, 1028 | cut_left: this.data.cut_left, 1029 | x: currentX, 1030 | y: currentY, 1031 | corner: 2 1032 | } 1033 | } else if (currentX > cutbox_left1 && currentX < cutbox_right1 && currentY > cutbox_top1 && currentY < cutbox_bottom1) { 1034 | this._moveDuring(); 1035 | this.data._flag_cut_touch = true; 1036 | this.data._flag_img_endtouch = true; 1037 | this.data.CUT_START = { 1038 | width: this.data.width, 1039 | height: this.data.height, 1040 | cut_top: this.data.cut_top, 1041 | cut_left: this.data.cut_left, 1042 | x: currentX, 1043 | y: currentY, 1044 | corner: 1 1045 | } 1046 | } 1047 | }, 1048 | _cutTouchEnd(e) { 1049 | this._moveStop(); 1050 | this.data._flag_cut_touch = false; 1051 | }, 1052 | //停止移动时需要做的操作 1053 | _moveStop() { 1054 | //清空之前的自动居中延迟函数并添加最新的 1055 | clearTimeout(this.data.TIME_CUT_CENTER); 1056 | this.data.TIME_CUT_CENTER = setTimeout(() => { 1057 | //动画启动 1058 | if (!this.data._cut_animation) { 1059 | this.setData({ 1060 | _cut_animation: true 1061 | }); 1062 | } 1063 | this.setCutCenter(); 1064 | }, 1000) 1065 | //清空之前的背景变化延迟函数并添加最新的 1066 | clearTimeout(this.data.TIME_BG); 1067 | this.data.TIME_BG = setTimeout(() => { 1068 | if (this.data._flag_bright) { 1069 | this.setData({ 1070 | _flag_bright: false 1071 | }); 1072 | } 1073 | }, 2000) 1074 | }, 1075 | //移动中 1076 | _moveDuring() { 1077 | //清空之前的自动居中延迟函数 1078 | clearTimeout(this.data.TIME_CUT_CENTER); 1079 | //清空之前的背景变化延迟函数 1080 | clearTimeout(this.data.TIME_BG); 1081 | //高亮背景 1082 | if (!this.data._flag_bright) { 1083 | this.setData({ 1084 | _flag_bright: true 1085 | }); 1086 | } 1087 | }, 1088 | //监听器 1089 | _watcher() { 1090 | Object.keys(this.data).forEach(v => { 1091 | this._observe(this.data, v, this.data.watch[v]); 1092 | }) 1093 | }, 1094 | _observe(obj, key, watchFun) { 1095 | var val = obj[key]; 1096 | Object.defineProperty(obj, key, { 1097 | configurable: true, 1098 | enumerable: true, 1099 | set: (value) => { 1100 | val = value; 1101 | watchFun && watchFun(val, this); 1102 | }, 1103 | get() { 1104 | if (val && '_img_top|img_left||width|height|min_width|max_width|min_height|max_height|export_scale|cut_top|cut_left|canvas_top|canvas_left|img_width|img_height|scale|angle|min_scale|max_scale'.indexOf(key) != -1) { 1105 | let ret = parseFloat(parseFloat(val).toFixed(3)); 1106 | if (typeof val == "string" && val.indexOf("%") != -1) { 1107 | ret += '%'; 1108 | } 1109 | return ret; 1110 | } 1111 | return val; 1112 | } 1113 | }) 1114 | }, 1115 | _preventTouchMove() {} 1116 | } 1117 | }) --------------------------------------------------------------------------------