tui-cropper.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. <template>
  2. <view class="tui-cropper__box" @touchmove.stop.prevent="stop">
  3. <image @load="imageLoad" @error="imageLoad" @touchstart="parse.touchstart" @touchmove="parse.touchmove"
  4. @touchend="parse.touchend" :data-minScale="minScale" :data-maxScale="maxScale" :style="{
  5. width: (imgWidth ? imgWidth : width) + 'px',
  6. height: imgHeight ? imgHeight + 'px' : 'auto',
  7. transitionDuration: (animation ? 0.3 : 0) + 's'
  8. }" class="tui-cropper__image" :class="{'tui-cropper__image-hidden':!imageUrl}" :src="imageUrl" mode="widthFix">
  9. </image>
  10. <view class="tui-backdrop__cropper"
  11. :style="{ width: width + 'px', height: height + 'px', borderRadius: isRound ? '50%' : '0' }">
  12. <view class="tui-cropper__border" :change:prop="parse.propsChange" :prop="props" :data-width="width"
  13. :data-height="height" :data-windowHeight="sysInfo.windowHeight || 600"
  14. :data-windowWidth="sysInfo.windowWidth || 400" :data-imgTop="imgTop" :data-imgLeft="imgLeft"
  15. :data-imgWidth="imgWidth" :data-imgHeight="imgHeight" :data-angle="angle"
  16. :style="{ borderRadius: isRound ? '50%' : '0', border: border }"></view>
  17. </view>
  18. <canvas canvas-id="tui-image__cropper" id="tui-image__cropper" :disable-scroll="true"
  19. :style="{ width: width * scaleRatio + 'px', height: height * scaleRatio + 'px' }"
  20. class="tui-cropper__canvas"></canvas>
  21. <view class="tui-cropper__tabbar" v-if="!custom">
  22. <view class="tui-op__btn" @tap.stop="back">取消</view>
  23. <image :src="rotateImg" class="tui-rotate__img" @tap="setAngle"></image>
  24. <view class="tui-op__btn" @tap.stop="getImage">完成</view>
  25. </view>
  26. </view>
  27. </template>
  28. <script src="./tui-cropper.wxs" module="parse" lang="wxs"></script>
  29. <script>
  30. export default {
  31. name: 'tuiCropper',
  32. emits: ['ready', 'cropper', 'imageLoad', 'initAngle'],
  33. props: {
  34. //图片路径
  35. imageUrl: {
  36. type: String,
  37. default: ''
  38. },
  39. //裁剪框高度 px
  40. height: {
  41. type: Number,
  42. default: 280
  43. },
  44. //裁剪框宽度 px
  45. width: {
  46. type: Number,
  47. default: 280
  48. },
  49. //是否为圆形裁剪框
  50. isRound: {
  51. type: Boolean,
  52. default: true
  53. },
  54. //裁剪框边框
  55. border: {
  56. type: String,
  57. default: '1px solid #fff'
  58. },
  59. //生成的图片尺寸相对剪裁框的比例
  60. scaleRatio: {
  61. type: Number,
  62. default: 2
  63. },
  64. //图片的质量,取值范围为 (0, 1],不在范围内时当作1.0处理
  65. quality: {
  66. type: Number,
  67. default: 0.8
  68. },
  69. //图片旋转角度
  70. rotateAngle: {
  71. type: Number,
  72. default: 0
  73. },
  74. //图片最小缩放比
  75. minScale: {
  76. type: Number,
  77. default: 0.5
  78. },
  79. //图片最大缩放比
  80. maxScale: {
  81. type: Number,
  82. default: 2
  83. },
  84. //自定义操作栏(为true时隐藏底部操作栏)
  85. custom: {
  86. type: Boolean,
  87. default: false
  88. },
  89. //值发生改变开始裁剪(custom为true时生效)
  90. startCutting: {
  91. type: [Number, Boolean],
  92. default: 0
  93. },
  94. /**
  95. * 是否返回base64(H5端默认base64)
  96. * 支持平台:App,微信小程序,支付宝小程序,H5(默认url就是base64)
  97. **/
  98. isBase64: {
  99. type: Boolean,
  100. default: false
  101. },
  102. //裁剪时是否显示loadding
  103. loadding: {
  104. type: Boolean,
  105. default: true
  106. },
  107. //旋转icon
  108. rotateImg: {
  109. type: String,
  110. default: '/static/components/cropper/img_rotate.png'
  111. }
  112. },
  113. data() {
  114. return {
  115. TIME_CUT_CENTER: null,
  116. cutX: 0, //画布x轴起点
  117. cutY: 0, //画布y轴起点0
  118. imgWidth: 0,
  119. imgHeight: 0,
  120. scale: 1, //图片缩放比
  121. angle: 0, //图片旋转角度
  122. animation: false, //是否开启图片过渡效果
  123. animationTime: null,
  124. imgTop: 0,
  125. imgLeft: 0,
  126. ctx: null,
  127. sysInfo: {},
  128. props: '',
  129. sizeChange: 0, //2
  130. angleChange: 0, //3
  131. resetChange: 0, //4
  132. centerChange: 0 //5
  133. };
  134. },
  135. watch: {
  136. //定义变量然后利用change触发
  137. imageUrl(val, oldVal) {
  138. this.imageReset();
  139. this.showLoading();
  140. uni.getImageInfo({
  141. src: val,
  142. success: res => {
  143. //计算图片尺寸
  144. this.imgComputeSize(res.width, res.height);
  145. this.angleChange++;
  146. this.props = `3,${this.angleChange}`;
  147. },
  148. fail: err => {
  149. this.imgComputeSize();
  150. this.angleChange++;
  151. this.props = `3,${this.angleChange}`;
  152. }
  153. });
  154. },
  155. rotateAngle(val) {
  156. this.animation = true;
  157. this.angle = val;
  158. this.angleChanged(val);
  159. },
  160. animation(val) {
  161. //开启过渡220毫秒之后自动关闭
  162. clearTimeout(this.animationTime);
  163. if (val) {
  164. this.animationTime = setTimeout(() => {
  165. this.animation = false;
  166. }, 220);
  167. }
  168. },
  169. startCutting(val) {
  170. if (this.custom && val) {
  171. this.getImage();
  172. }
  173. }
  174. },
  175. mounted() {
  176. this.sysInfo = uni.getSystemInfoSync();
  177. this.imgTop = this.sysInfo.windowHeight / 2;
  178. this.imgLeft = this.sysInfo.windowWidth / 2;
  179. this.ctx = uni.createCanvasContext('tui-image__cropper', this);
  180. //初始化
  181. setTimeout(() => {
  182. this.props = '1,1';
  183. }, 0);
  184. setTimeout(() => {
  185. this.$emit('ready', {});
  186. }, 200);
  187. },
  188. methods: {
  189. //网络图片转成本地文件[同步执行]
  190. async getLocalImage(url) {
  191. return await new Promise((resolve, reject) => {
  192. uni.downloadFile({
  193. url: url,
  194. success: res => {
  195. resolve(res.tempFilePath);
  196. },
  197. fail: res => {
  198. reject(false);
  199. }
  200. });
  201. });
  202. },
  203. //返回裁剪后图片信息
  204. getImage() {
  205. if (!this.imageUrl) {
  206. uni.showToast({
  207. title: '请选择图片',
  208. icon: 'none'
  209. });
  210. return;
  211. }
  212. this.loadding && this.showLoading();
  213. let draw = async () => {
  214. //图片实际大小
  215. let imgWidth = this.imgWidth * this.scale * this.scaleRatio;
  216. let imgHeight = this.imgHeight * this.scale * this.scaleRatio;
  217. //canvas和图片的相对距离
  218. let xpos = this.imgLeft - this.cutX;
  219. let ypos = this.imgTop - this.cutY;
  220. //旋转画布
  221. this.ctx.translate(xpos * this.scaleRatio, ypos * this.scaleRatio);
  222. this.ctx.rotate((this.angle * Math.PI) / 180);
  223. let imgUrl = this.imageUrl;
  224. // #ifdef APP-PLUS || MP-WEIXIN
  225. if (~this.imageUrl.indexOf('https:')) {
  226. imgUrl = await this.getLocalImage(this.imageUrl);
  227. }
  228. // #endif
  229. this.ctx.drawImage(imgUrl, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
  230. this.ctx.draw(false, () => {
  231. let params = {
  232. width: this.width * this.scaleRatio,
  233. height: Math.round(this.height * this.scaleRatio),
  234. destWidth: this.width * this.scaleRatio,
  235. destHeight: Math.round(this.height) * this.scaleRatio,
  236. fileType: 'png',
  237. quality: this.quality
  238. };
  239. let data = {
  240. url: '',
  241. base64: '',
  242. width: this.width * this.scaleRatio,
  243. height: this.height * this.scaleRatio
  244. };
  245. // #ifdef MP-ALIPAY
  246. if (this.isBase64) {
  247. this.ctx.toDataURL(params).then(dataURL => {
  248. data.base64 = dataURL;
  249. this.loadding && uni.hideLoading();
  250. this.$emit('cropper', data);
  251. });
  252. } else {
  253. this.ctx.toTempFilePath({
  254. ...params,
  255. success: res => {
  256. data.url = res.apFilePath;
  257. this.loadding && uni.hideLoading();
  258. this.$emit('cropper', data);
  259. }
  260. });
  261. }
  262. // #endif
  263. // #ifndef MP-ALIPAY
  264. let isBase64=this.isBase64
  265. // #ifdef MP-BAIDU || MP-TOUTIAO || H5
  266. isBase64 = false;
  267. // #endif
  268. if (isBase64) {
  269. uni.canvasGetImageData({
  270. canvasId: 'tui-image__cropper',
  271. x: 0,
  272. y: 0,
  273. width: this.width * this.scaleRatio,
  274. height: Math.round(this.height * this.scaleRatio),
  275. success: res => {
  276. const arrayBuffer = new Uint8Array(res.data);
  277. const base64 = uni.arrayBufferToBase64(arrayBuffer);
  278. data.base64 = base64;
  279. this.loadding && uni.hideLoading();
  280. this.$emit('cropper', data);
  281. }
  282. },
  283. this
  284. );
  285. } else {
  286. uni.canvasToTempFilePath({
  287. ...params,
  288. canvasId: 'tui-image__cropper',
  289. success: res => {
  290. data.url = res.tempFilePath;
  291. // #ifdef H5
  292. data.base64 = res.tempFilePath;
  293. // #endif
  294. this.loadding && uni.hideLoading();
  295. this.$emit('cropper', data);
  296. },
  297. fail(res) {
  298. console.log(res);
  299. }
  300. },
  301. this
  302. );
  303. }
  304. // #endif
  305. });
  306. };
  307. draw();
  308. },
  309. change(e) {
  310. this.cutX = e.cutX || 0;
  311. this.cutY = e.cutY || 0;
  312. this.imgWidth = e.imgWidth || this.imgWidth;
  313. this.imgHeight = e.imgHeight || this.imgHeight;
  314. this.scale = e.scale || 1;
  315. this.angle = e.angle || 0;
  316. this.imgTop = e.imgTop || 0;
  317. this.imgLeft = e.imgLeft || 0;
  318. },
  319. imageReset() {
  320. this.scale = 1;
  321. this.angle = 0;
  322. let sys = this.sysInfo.windowHeight ? this.sysInfo : uni.getSystemInfoSync();
  323. this.imgTop = sys.windowHeight / 2;
  324. this.imgLeft = sys.windowWidth / 2;
  325. this.resetChange++;
  326. this.props = `4,${this.resetChange}`;
  327. //初始化旋转角度 0deg
  328. this.$emit('initAngle', {});
  329. },
  330. imgComputeSize(width, height) {
  331. //默认按图片最小边 = 对应裁剪框尺寸
  332. let imgWidth = width,
  333. imgHeight = height;
  334. if (imgWidth && imgHeight) {
  335. if (imgWidth / imgHeight > this.width / this.height) {
  336. imgHeight = this.height;
  337. imgWidth = (width / height) * imgHeight;
  338. } else {
  339. imgWidth = this.width;
  340. imgHeight = (height / width) * imgWidth;
  341. }
  342. } else {
  343. let sys = this.sysInfo.windowHeight ? this.sysInfo : uni.getSystemInfoSync();
  344. imgWidth = sys.windowWidth;
  345. imgHeight = 0;
  346. }
  347. this.imgWidth = imgWidth;
  348. this.imgHeight = imgHeight;
  349. this.sizeChange++;
  350. this.props = `2,${this.sizeChange}`;
  351. },
  352. imageLoad(e) {
  353. this.imageReset();
  354. uni.hideLoading();
  355. this.$emit('imageLoad', {});
  356. },
  357. moveStop() {
  358. clearTimeout(this.TIME_CUT_CENTER);
  359. this.TIME_CUT_CENTER = setTimeout(() => {
  360. this.centerChange++;
  361. this.props = `5,${this.centerChange}`;
  362. }, 666);
  363. },
  364. moveDuring() {
  365. clearTimeout(this.TIME_CUT_CENTER);
  366. },
  367. showLoading() {
  368. uni.showLoading({
  369. title: '请稍候...',
  370. mask: true
  371. });
  372. },
  373. stop() {},
  374. back() {
  375. uni.navigateBack();
  376. },
  377. angleChanged(val) {
  378. this.moveStop();
  379. if (val % 90) {
  380. this.angle = Math.round(val / 90) * 90;
  381. }
  382. this.angleChange++;
  383. this.props = `3,${this.angleChange}`;
  384. },
  385. setAngle() {
  386. this.animation = true;
  387. this.angle = this.angle + 90;
  388. this.angleChanged(this.angle);
  389. }
  390. }
  391. };
  392. </script>
  393. <style scoped>
  394. .tui-cropper__box {
  395. width: 100vw;
  396. height: 100vh;
  397. background-color: rgba(0, 0, 0, 0.7);
  398. position: fixed;
  399. top: 0;
  400. left: 0;
  401. z-index: 1;
  402. }
  403. .tui-cropper__image {
  404. width: 100%;
  405. border-style: none;
  406. position: absolute;
  407. top: 0;
  408. left: 0;
  409. z-index: 2;
  410. -webkit-backface-visibility: hidden;
  411. backface-visibility: hidden;
  412. transform-origin: center;
  413. }
  414. .tui-cropper__image-hidden {
  415. visibility: hidden;
  416. opacity: 0;
  417. }
  418. .tui-cropper__canvas {
  419. position: fixed;
  420. z-index: 10;
  421. left: -2000px;
  422. top: -2000px;
  423. pointer-events: none;
  424. }
  425. .tui-backdrop__cropper {
  426. position: fixed;
  427. z-index: 4;
  428. left: 50%;
  429. top: 50%;
  430. transform: translate(-50%, -50%);
  431. border: 3000px solid rgba(0, 0, 0, 0.55);
  432. pointer-events: none;
  433. }
  434. .tui-cropper__border {
  435. position: absolute;
  436. left: 0;
  437. top: 0;
  438. width: 100%;
  439. height: 100%;
  440. box-sizing: border-box;
  441. pointer-events: none;
  442. }
  443. .tui-cropper__tabbar {
  444. width: 100%;
  445. height: 120rpx;
  446. padding: 0 40rpx;
  447. box-sizing: border-box;
  448. position: fixed;
  449. left: 0;
  450. bottom: 0;
  451. z-index: 99;
  452. display: flex;
  453. align-items: center;
  454. justify-content: space-between;
  455. color: #ffffff;
  456. font-size: 32rpx;
  457. }
  458. .tui-cropper__tabbar::after {
  459. content: ' ';
  460. position: absolute;
  461. top: 0;
  462. right: 0;
  463. left: 0;
  464. border-top: 1rpx solid rgba(255, 255, 255, 0.2);
  465. -webkit-transform: scaleY(0.5) translateZ(0);
  466. transform: scaleY(0.5) translateZ(0);
  467. transform-origin: 0 100%;
  468. }
  469. .tui-op__btn {
  470. height: 80rpx;
  471. display: flex;
  472. align-items: center;
  473. }
  474. .tui-rotate__img {
  475. width: 44rpx;
  476. height: 44rpx;
  477. }
  478. </style>