tui-image-cropper.vue 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091
  1. <template>
  2. <view class="tui-container" @touchmove.stop.prevent="stop">
  3. <view class="tui-image-cropper" @touchend="cutTouchEnd" @touchstart="cutTouchStart" @touchmove="cutTouchMove">
  4. <view class="tui-content">
  5. <view class="tui-content-top tui-bg-transparent"
  6. :style="{ height: cutY + 'px', transitionProperty: cutAnimation ? '' : 'background' }"></view>
  7. <view class="tui-content-middle" :style="{ height: canvasHeight + 'px' }">
  8. <view class="tui-bg-transparent"
  9. :style="{ width: cutX + 'px', transitionProperty: cutAnimation ? '' : 'background' }"></view>
  10. <view class="tui-cropper-box"
  11. :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', borderColor: borderColor, transitionProperty: cutAnimation ? '' : 'background' }">
  12. <view v-for="(item, index) in 4" :key="index" class="tui-edge"
  13. :class="[`tui-${index < 2 ? 'top' : 'bottom'}-${index === 0 || index === 2 ? 'left' : 'right'}`]"
  14. :style="{
  15. width: edgeWidth,
  16. height: edgeWidth,
  17. borderColor: edgeColor,
  18. borderWidth: edgeBorderWidth,
  19. left: index === 0 || index === 2 ? `-${edgeOffsets}` : 'auto',
  20. right: index === 1 || index === 3 ? `-${edgeOffsets}` : 'auto',
  21. top: index < 2 ? `-${edgeOffsets}` : 'auto',
  22. bottom: index > 1 ? `-${edgeOffsets}` : 'auto'
  23. }"></view>
  24. </view>
  25. <view class="tui-flex-auto tui-bg-transparent"
  26. :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
  27. </view>
  28. <view class="tui-flex-auto tui-bg-transparent"
  29. :style="{ transitionProperty: cutAnimation ? '' : 'background' }"></view>
  30. </view>
  31. <image @load="imageLoad" @error="imageLoad" @touchstart="start" @touchmove="move" @touchend="end" :style="{
  32. width: imgWidth ? imgWidth + 'px' : 'auto',
  33. height: imgHeight ? imgHeight + 'px' : 'auto',
  34. transform: imgTransform,
  35. transitionDuration: (cutAnimation ? 0.35 : 0) + 's'
  36. }" class="tui-cropper-image" :class="{'tui-cropper__image-hidden':!imageUrl}" :src="imageUrl" mode="widthFix">
  37. </image>
  38. </view>
  39. <canvas canvas-id="tui-image-cropper" id="tui-image-cropper" :disable-scroll="true"
  40. :style="{ width: CROPPER_WIDTH * scaleRatio + 'px', height: CROPPER_HEIGHT * scaleRatio + 'px' }"
  41. class="tui-cropper-canvas"></canvas>
  42. <view class="tui-cropper-tabbar" v-if="!custom">
  43. <view class="tui-op-btn" @tap.stop="back">取消</view>
  44. <image :src="rotateImg" class="tui-rotate-img" @tap="setAngle"></image>
  45. <view class="tui-op-btn" @tap.stop="getImage">完成</view>
  46. </view>
  47. </view>
  48. </template>
  49. <script>
  50. /**
  51. * 注意:组件中使用的图片地址,将文件复制到自己项目中
  52. * 如果图片位置与组件同级,编译成小程序时图片会丢失
  53. * 拷贝static下整个components文件夹
  54. *也可直接转成base64(不建议)
  55. * */
  56. export default {
  57. name: 'tuiImageCropper',
  58. emits: ['ready', 'cropper', 'imageLoad'],
  59. props: {
  60. //图片路径
  61. imageUrl: {
  62. type: String,
  63. default: ''
  64. },
  65. /*
  66. 默认正方形,可修改大小控制比例
  67. 裁剪框高度 px
  68. */
  69. height: {
  70. type: Number,
  71. default: 280
  72. },
  73. //裁剪框宽度 px
  74. width: {
  75. type: Number,
  76. default: 280
  77. },
  78. //裁剪框最小宽度 px
  79. minWidth: {
  80. type: Number,
  81. default: 100
  82. },
  83. //裁剪框最小高度 px
  84. minHeight: {
  85. type: Number,
  86. default: 100
  87. },
  88. //裁剪框最大宽度 px
  89. maxWidth: {
  90. type: Number,
  91. default: 360
  92. },
  93. //裁剪框最大高度 px
  94. maxHeight: {
  95. type: Number,
  96. default: 360
  97. },
  98. //裁剪框border颜色
  99. borderColor: {
  100. type: String,
  101. default: 'rgba(255,255,255,0.1)'
  102. },
  103. //裁剪框边缘线颜色
  104. edgeColor: {
  105. type: String,
  106. default: '#FFFFFF'
  107. },
  108. //裁剪框边缘线宽度 w=h
  109. edgeWidth: {
  110. type: String,
  111. default: '34rpx'
  112. },
  113. //裁剪框边缘线border宽度
  114. edgeBorderWidth: {
  115. type: String,
  116. default: '6rpx'
  117. },
  118. //偏移距离,根据edgeBorderWidth进行调整
  119. edgeOffsets: {
  120. type: String,
  121. default: '6rpx'
  122. },
  123. /**
  124. * 如果宽度和高度都为true则裁剪框禁止拖动
  125. * 裁剪框宽度锁定
  126. */
  127. lockWidth: {
  128. type: Boolean,
  129. default: false
  130. },
  131. //裁剪框高度锁定
  132. lockHeight: {
  133. type: Boolean,
  134. default: false
  135. },
  136. //锁定裁剪框比例(放大或缩小)
  137. lockRatio: {
  138. type: Boolean,
  139. default: false
  140. },
  141. //生成的图片尺寸相对剪裁框的比例
  142. // #ifndef MP-QQ
  143. scaleRatio: {
  144. type: [Number, String],
  145. default: 2
  146. },
  147. // #endif
  148. // #ifdef MP-QQ
  149. scaleRatio: {
  150. type: [Number, String],
  151. default: 3
  152. },
  153. // #endif
  154. //图片的质量,取值范围为 (0, 1],不在范围内时当作1.0处理
  155. quality: {
  156. type: Number,
  157. default: 0.8
  158. },
  159. //图片旋转角度
  160. rotateAngle: {
  161. type: Number,
  162. default: 0
  163. },
  164. //图片最小缩放比
  165. minScale: {
  166. type: Number,
  167. default: 0.5
  168. },
  169. //图片最大缩放比
  170. maxScale: {
  171. type: Number,
  172. default: 2
  173. },
  174. //是否禁用触摸旋转(为false则可以触摸转动图片,limitMove为false生效)
  175. disableRotate: {
  176. type: Boolean,
  177. default: true
  178. },
  179. //是否限制移动范围(剪裁框只能在图片内,为true不可触摸转动图片)
  180. limitMove: {
  181. type: Boolean,
  182. default: true
  183. },
  184. //自定义操作栏(为true时隐藏底部操作栏)
  185. custom: {
  186. type: Boolean,
  187. default: false
  188. },
  189. //值发生改变开始裁剪(custom为true时生效)
  190. startCutting: {
  191. type: [Number, Boolean],
  192. default: 0
  193. },
  194. /**
  195. * 是否返回base64(H5端默认base64)
  196. * 支持平台:App,微信小程序,支付宝小程序,H5(默认url就是base64)
  197. **/
  198. isBase64: {
  199. type: Boolean,
  200. default: false
  201. },
  202. //裁剪时是否显示loadding
  203. loadding: {
  204. type: Boolean,
  205. default: true
  206. },
  207. //旋转icon
  208. rotateImg: {
  209. type: String,
  210. default: '/static/components/cropper/img_rotate.png'
  211. },
  212. //裁剪后图片类型:jpg/png
  213. fileType: {
  214. type: String,
  215. default: 'png'
  216. }
  217. },
  218. data() {
  219. return {
  220. MOVE_THROTTLE: null, //触摸移动节流setTimeout
  221. MOVE_THROTTLE_FLAG: true, //节流标识
  222. TIME_CUT_CENTER: null,
  223. CROPPER_WIDTH: 200, //裁剪框宽
  224. CROPPER_HEIGHT: 200, //裁剪框高
  225. CUT_START: null,
  226. cutX: 0, //画布x轴起点
  227. cutY: 0, //画布y轴起点0
  228. touchRelative: [{
  229. x: 0,
  230. y: 0
  231. }], //手指或鼠标和图片中心的相对位置
  232. flagCutTouch: false, //是否是拖动裁剪框
  233. hypotenuseLength: 0, //双指触摸时斜边长度
  234. flagEndTouch: false, //是否结束触摸
  235. canvasWidth: 0,
  236. canvasHeight: 0,
  237. imgWidth: 0, //图片宽度
  238. imgHeight: 0, //图片高度
  239. scale: 1, //图片缩放比
  240. angle: 0, //图片旋转角度
  241. cutAnimation: false, //是否开启图片和裁剪框过渡
  242. cutAnimationTime: null,
  243. imgTop: 0, //图片上边距
  244. imgLeft: 0, //图片左边距
  245. ctx: null,
  246. sysInfo: null
  247. };
  248. },
  249. computed: {
  250. imgTransform: function() {
  251. return `translate3d(${this.imgLeft - this.imgWidth / 2}px,${this.imgTop - this.imgHeight / 2}px,0) scale(${this.scale}) rotate(${this.angle}deg)`;
  252. }
  253. },
  254. watch: {
  255. imageUrl(val, oldVal) {
  256. this.imageReset();
  257. this.showLoading();
  258. uni.getImageInfo({
  259. src: val,
  260. success: res => {
  261. //计算图片尺寸
  262. this.imgComputeSize(res.width, res.height);
  263. if (this.limitMove) {
  264. //限制移动,不留空白处理
  265. this.imgMarginDetectionScale();
  266. }
  267. },
  268. fail: err => {
  269. this.imgComputeSize();
  270. if (this.limitMove) {
  271. this.imgMarginDetectionScale();
  272. }
  273. }
  274. });
  275. },
  276. //监听截取框宽高变化
  277. canvasWidth(val) {
  278. if (val < this.minWidth) {
  279. this.canvasWidth = this.minWidth;
  280. }
  281. this.computeCutSize();
  282. },
  283. canvasHeight(val) {
  284. if (val < this.minHeight) {
  285. this.canvasHeight = this.minHeight;
  286. }
  287. this.computeCutSize();
  288. },
  289. rotateAngle(val) {
  290. this.cutAnimation = true;
  291. this.angle = val;
  292. },
  293. angle(val) {
  294. this.moveStop();
  295. if (this.limitMove && val % 90) {
  296. this.angle = Math.round(val / 90) * 90;
  297. }
  298. this.imgMarginDetectionScale();
  299. },
  300. cutAnimation(val) {
  301. //开启过渡260毫秒之后自动关闭
  302. clearTimeout(this.cutAnimationTime);
  303. if (val) {
  304. this.cutAnimationTime = setTimeout(() => {
  305. this.cutAnimation = false;
  306. }, 260);
  307. }
  308. },
  309. limitMove(val) {
  310. if (val) {
  311. if (this.angle % 90) {
  312. this.angle = Math.round(this.angle / 90) * 90;
  313. }
  314. this.imgMarginDetectionScale();
  315. }
  316. },
  317. cutY(value) {
  318. this.cutDetectionPosition();
  319. },
  320. cutX(value) {
  321. this.cutDetectionPosition();
  322. },
  323. startCutting(val) {
  324. if (this.custom && val) {
  325. this.getImage();
  326. }
  327. }
  328. },
  329. mounted() {
  330. this.sysInfo = uni.getSystemInfoSync();
  331. this.imgTop = this.sysInfo.windowHeight / 2;
  332. this.imgLeft = this.sysInfo.windowWidth / 2;
  333. this.CROPPER_WIDTH = this.width;
  334. this.CROPPER_HEIGHT = this.height;
  335. this.canvasHeight = this.height;
  336. this.canvasWidth = this.width;
  337. this.ctx = uni.createCanvasContext('tui-image-cropper', this);
  338. this.setCutCenter();
  339. //设置裁剪框大小>设置图片尺寸>绘制canvas
  340. this.computeCutSize();
  341. //检查裁剪框是否在范围内
  342. this.cutDetectionPosition();
  343. setTimeout(() => {
  344. this.$emit('ready', {});
  345. }, 200);
  346. },
  347. methods: {
  348. //网络图片转成本地文件[同步执行]
  349. async getLocalImage(url) {
  350. return await new Promise((resolve, reject) => {
  351. uni.downloadFile({
  352. url: url,
  353. success: res => {
  354. resolve(res.tempFilePath);
  355. },
  356. fail: res => {
  357. reject(false)
  358. }
  359. })
  360. })
  361. },
  362. //返回裁剪后图片信息
  363. getImage() {
  364. if (!this.imageUrl) {
  365. uni.showToast({
  366. title: '请选择图片',
  367. icon: 'none'
  368. });
  369. return;
  370. }
  371. this.loadding && this.showLoading();
  372. let draw = async () => {
  373. //图片实际大小
  374. let imgWidth = this.imgWidth * this.scale * this.scaleRatio;
  375. let imgHeight = this.imgHeight * this.scale * this.scaleRatio;
  376. //canvas和图片的相对距离
  377. let xpos = this.imgLeft - this.cutX;
  378. let ypos = this.imgTop - this.cutY;
  379. //旋转画布
  380. this.ctx.translate(xpos * this.scaleRatio, ypos * this.scaleRatio);
  381. this.ctx.rotate((this.angle * Math.PI) / 180);
  382. let imgUrl = this.imageUrl;
  383. // #ifdef APP-PLUS || MP-WEIXIN
  384. if (~this.imageUrl.indexOf('https:')) {
  385. imgUrl = await this.getLocalImage(this.imageUrl)
  386. }
  387. // #endif
  388. this.ctx.drawImage(imgUrl, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
  389. this.ctx.draw(false, () => {
  390. let params = {
  391. width: this.canvasWidth * this.scaleRatio,
  392. height: Math.round(this.canvasHeight * this.scaleRatio),
  393. // #ifdef MP-QQ
  394. destWidth: this.canvasWidth * this.scaleRatio * 2,
  395. destHeight: Math.round(this.canvasHeight) * this.scaleRatio * 2,
  396. // #endif
  397. // #ifndef MP-QQ
  398. destWidth: this.canvasWidth * this.scaleRatio,
  399. destHeight: Math.round(this.canvasHeight) * this.scaleRatio,
  400. // #endif
  401. fileType: this.fileType || 'png',
  402. quality: this.quality
  403. };
  404. let data = {
  405. url: '',
  406. base64: '',
  407. width: this.canvasWidth * this.scaleRatio,
  408. height: this.canvasHeight * this.scaleRatio
  409. };
  410. // #ifdef MP-ALIPAY
  411. if (this.isBase64) {
  412. this.ctx.toDataURL(params).then(dataURL => {
  413. data.base64 = dataURL;
  414. this.loadding && uni.hideLoading();
  415. this.ctx.rotate(((360 - this.angle % 360) * Math
  416. .PI) / 180);
  417. this.ctx.translate(-xpos * this.scaleRatio, -
  418. ypos * this.scaleRatio);
  419. this.ctx.clearRect(0, 0, this.canvasWidth * this
  420. .scaleRatio, this.canvasHeight * this.scaleRatio);
  421. this.ctx.draw();
  422. this.$emit('cropper', data);
  423. });
  424. } else {
  425. this.ctx.toTempFilePath({
  426. ...params,
  427. success: res => {
  428. data.url = res.apFilePath;
  429. this.loadding && uni.hideLoading();
  430. this.ctx.rotate(((360 - this.angle % 360) * Math
  431. .PI) / 180);
  432. this.ctx.translate(-xpos * this.scaleRatio, -
  433. ypos * this.scaleRatio);
  434. this.ctx.clearRect(0, 0, this.canvasWidth * this
  435. .scaleRatio, this.canvasHeight * this.scaleRatio);
  436. this.ctx.draw();
  437. this.$emit('cropper', data);
  438. }
  439. });
  440. }
  441. // #endif
  442. // #ifndef MP-ALIPAY
  443. let isBase64=this.isBase64
  444. // #ifdef MP-BAIDU || MP-TOUTIAO || H5
  445. isBase64 = false;
  446. // #endif
  447. if (isBase64) {
  448. uni.canvasGetImageData({
  449. canvasId: 'tui-image-cropper',
  450. x: 0,
  451. y: 0,
  452. width: this.canvasWidth * this.scaleRatio,
  453. height: Math.round(this.canvasHeight * this.scaleRatio),
  454. success: res => {
  455. const arrayBuffer = new Uint8Array(res.data);
  456. const base64 = uni.arrayBufferToBase64(arrayBuffer);
  457. data.base64 = base64;
  458. this.loadding && uni.hideLoading();
  459. this.$emit('cropper', data);
  460. }
  461. }, this);
  462. } else {
  463. uni.canvasToTempFilePath({
  464. ...params,
  465. canvasId: 'tui-image-cropper',
  466. success: res => {
  467. data.url = res.tempFilePath;
  468. // #ifdef H5
  469. data.base64 = res.tempFilePath;
  470. // #endif
  471. this.loadding && uni.hideLoading();
  472. this.$emit('cropper', data);
  473. },
  474. fail(res) {
  475. console.log(res);
  476. }
  477. },
  478. this
  479. );
  480. }
  481. // #endif
  482. });
  483. };
  484. if (this.CROPPER_WIDTH != this.canvasWidth || this.CROPPER_HEIGHT != this.canvasHeight) {
  485. this.CROPPER_WIDTH = this.canvasWidth;
  486. this.CROPPER_HEIGHT = this.canvasHeight;
  487. this.ctx.draw();
  488. this.$nextTick(() => {
  489. setTimeout(() => {
  490. draw();
  491. }, 100);
  492. });
  493. } else {
  494. draw();
  495. }
  496. },
  497. /**
  498. * 设置剪裁框和图片居中
  499. */
  500. setCutCenter() {
  501. let sys = this.sysInfo || uni.getSystemInfoSync();
  502. let cutY = (sys.windowHeight - this.canvasHeight) * 0.5;
  503. let cutX = (sys.windowWidth - this.canvasWidth) * 0.5;
  504. //顺序不能变
  505. this.imgTop = this.imgTop - this.cutY + cutY;
  506. this.cutY = cutY; //截取的框上边距
  507. this.imgLeft = this.imgLeft - this.cutX + cutX;
  508. this.cutX = cutX; //截取的框左边距
  509. },
  510. imageReset() {
  511. // this.cutAnimation = true;
  512. this.scale = 1;
  513. this.angle = 0;
  514. let sys = this.sysInfo || uni.getSystemInfoSync();
  515. this.imgTop = sys.windowHeight / 2;
  516. this.imgLeft = sys.windowWidth / 2;
  517. },
  518. imageLoad(e) {
  519. this.imageReset();
  520. uni.hideLoading();
  521. this.$emit('imageLoad', {});
  522. },
  523. //检测剪裁框位置是否在允许的范围内(屏幕内)
  524. cutDetectionPosition() {
  525. let cutDetectionPositionTop = () => {
  526. //检测上边距是否在范围内
  527. if (this.cutY < 0) {
  528. this.cutY = 0;
  529. }
  530. if (this.cutY > this.sysInfo.windowHeight - this.canvasHeight) {
  531. this.cutY = this.sysInfo.windowHeight - this.canvasHeight;
  532. }
  533. },
  534. cutDetectionPositionLeft = () => {
  535. //检测左边距是否在范围内
  536. if (this.cutX < 0) {
  537. this.cutX = 0;
  538. }
  539. if (this.cutX > this.sysInfo.windowWidth - this.canvasWidth) {
  540. this.cutX = this.sysInfo.windowWidth - this.canvasWidth;
  541. }
  542. };
  543. //裁剪框坐标处理(如果只写一个参数则另一个默认为0,都不写默认居中)
  544. if (this.cutY == null && this.cutX == null) {
  545. let cutY = (this.sysInfo.windowHeight - this.canvasHeight) * 0.5;
  546. let cutX = (this.sysInfo.windowWidth - this.canvasWidth) * 0.5;
  547. this.cutY = cutY; //截取的框上边距
  548. this.cutX = cutX; //截取的框左边距
  549. } else if (this.cutY != null && this.cutX != null) {
  550. cutDetectionPositionTop();
  551. cutDetectionPositionLeft();
  552. } else if (this.cutY != null && this.cutX == null) {
  553. cutDetectionPositionTop();
  554. this.cutX = (this.sysInfo.windowWidth - this.canvasWidth) / 2;
  555. } else if (this.cutY == null && this.cutX != null) {
  556. cutDetectionPositionLeft();
  557. this.cutY = (this.sysInfo.windowHeight - this.canvasHeight) / 2;
  558. }
  559. },
  560. /**
  561. * 图片边缘检测-位置
  562. */
  563. imgMarginDetectionPosition(scale) {
  564. if (!this.limitMove) return;
  565. let left = this.imgLeft;
  566. let top = this.imgTop;
  567. scale = scale || this.scale;
  568. let imgWidth = this.imgWidth;
  569. let imgHeight = this.imgHeight;
  570. if ((this.angle / 90) % 2) {
  571. imgWidth = this.imgHeight;
  572. imgHeight = this.imgWidth;
  573. }
  574. left = this.cutX + (imgWidth * scale) / 2 >= left ? left : this.cutX + (imgWidth * scale) / 2;
  575. left = this.cutX + this.canvasWidth - (imgWidth * scale) / 2 <= left ? left : this.cutX + this
  576. .canvasWidth - (
  577. imgWidth * scale) / 2;
  578. top = this.cutY + (imgHeight * scale) / 2 >= top ? top : this.cutY + (imgHeight * scale) / 2;
  579. top = this.cutY + this.canvasHeight - (imgHeight * scale) / 2 <= top ? top : this.cutY + this
  580. .canvasHeight - (
  581. imgHeight * scale) / 2;
  582. this.imgLeft = left;
  583. this.imgTop = top;
  584. this.scale = scale;
  585. },
  586. /**
  587. * 图片边缘检测-缩放
  588. */
  589. imgMarginDetectionScale(scale) {
  590. if (!this.limitMove) return;
  591. scale = scale || this.scale;
  592. let imgWidth = this.imgWidth;
  593. let imgHeight = this.imgHeight;
  594. if ((this.angle / 90) % 2) {
  595. imgWidth = this.imgHeight;
  596. imgHeight = this.imgWidth;
  597. }
  598. if (imgWidth * scale < this.canvasWidth) {
  599. scale = this.canvasWidth / imgWidth;
  600. }
  601. if (imgHeight * scale < this.canvasHeight) {
  602. scale = Math.max(scale, this.canvasHeight / imgHeight);
  603. }
  604. this.imgMarginDetectionPosition(scale);
  605. },
  606. /**
  607. * 计算图片尺寸
  608. */
  609. imgComputeSize(width, height) {
  610. //默认按图片最小边 = 对应裁剪框尺寸
  611. let imgWidth = width,
  612. imgHeight = height;
  613. if (imgWidth && imgHeight) {
  614. if (imgWidth / imgHeight > (this.canvasWidth || this.width) / (this.canvasHeight || this.height)) {
  615. imgHeight = this.canvasHeight || this.height;
  616. imgWidth = (width / height) * imgHeight;
  617. } else {
  618. imgWidth = this.canvasWidth || this.width;
  619. imgHeight = (height / width) * imgWidth;
  620. }
  621. } else {
  622. let sys = this.sysInfo || uni.getSystemInfoSync();
  623. imgWidth = sys.windowWidth;
  624. imgHeight = 0;
  625. }
  626. this.imgWidth = imgWidth;
  627. this.imgHeight = imgHeight;
  628. },
  629. //改变截取框大小
  630. computeCutSize() {
  631. if (this.canvasWidth > this.sysInfo.windowWidth) {
  632. this.canvasWidth = this.sysInfo.windowWidth;
  633. } else if (this.canvasWidth + this.cutX > this.sysInfo.windowWidth) {
  634. this.cutX = this.sysInfo.windowWidth - this.cutX;
  635. }
  636. if (this.canvasHeight > this.sysInfo.windowHeight) {
  637. this.canvasHeight = this.sysInfo.windowHeight;
  638. } else if (this.canvasHeight + this.cutY > this.sysInfo.windowHeight) {
  639. this.cutY = this.sysInfo.windowHeight - this.cutY;
  640. }
  641. },
  642. //开始触摸
  643. start(e) {
  644. this.flagEndTouch = false;
  645. if (e.touches.length == 1) {
  646. //单指拖动
  647. this.touchRelative[0] = {
  648. x: e.touches[0].clientX - this.imgLeft,
  649. y: e.touches[0].clientY - this.imgTop
  650. };
  651. } else {
  652. //双指放大
  653. let width = Math.abs(e.touches[0].clientX - e.touches[1].clientX);
  654. let height = Math.abs(e.touches[0].clientY - e.touches[1].clientY);
  655. this.touchRelative = [{
  656. x: e.touches[0].clientX - this.imgLeft,
  657. y: e.touches[0].clientY - this.imgTop
  658. },
  659. {
  660. x: e.touches[1].clientX - this.imgLeft,
  661. y: e.touches[1].clientY - this.imgTop
  662. }
  663. ];
  664. this.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
  665. }
  666. },
  667. moveThrottle() {
  668. if (this.sysInfo.platform == 'android') {
  669. clearTimeout(this.MOVE_THROTTLE);
  670. this.MOVE_THROTTLE = setTimeout(() => {
  671. this.MOVE_THROTTLE_FLAG = true;
  672. }, 800 / 40);
  673. return this.MOVE_THROTTLE_FLAG;
  674. } else {
  675. this.MOVE_THROTTLE_FLAG = true;
  676. }
  677. },
  678. move(e) {
  679. if (this.flagEndTouch || !this.MOVE_THROTTLE_FLAG) return;
  680. this.MOVE_THROTTLE_FLAG = false;
  681. this.moveThrottle();
  682. this.moveDuring();
  683. if (e.touches.length == 1) {
  684. //单指拖动
  685. let left = e.touches[0].clientX - this.touchRelative[0].x,
  686. top = e.touches[0].clientY - this.touchRelative[0].y;
  687. //图像边缘检测,防止截取到空白
  688. this.imgLeft = left;
  689. this.imgTop = top;
  690. this.imgMarginDetectionPosition();
  691. } else {
  692. //双指放大
  693. let width = Math.abs(e.touches[0].clientX - e.touches[1].clientX),
  694. height = Math.abs(e.touches[0].clientY - e.touches[1].clientY),
  695. hypotenuse = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)),
  696. scale = this.scale * (hypotenuse / this.hypotenuseLength),
  697. current_deg = 0;
  698. scale = scale <= this.minScale ? this.minScale : scale;
  699. scale = scale >= this.maxScale ? this.maxScale : scale;
  700. //图像边缘检测,防止截取到空白
  701. // this.scale = scale;
  702. this.imgMarginDetectionScale(scale);
  703. //双指旋转(如果没禁用旋转)
  704. let touchRelative = [{
  705. x: e.touches[0].clientX - this.imgLeft,
  706. y: e.touches[0].clientY - this.imgTop
  707. },
  708. {
  709. x: e.touches[1].clientX - this.imgLeft,
  710. y: e.touches[1].clientY - this.imgTop
  711. }
  712. ];
  713. if (!this.disableRotate) {
  714. let first_atan = (180 / Math.PI) * Math.atan2(touchRelative[0].y, touchRelative[0].x);
  715. let first_atan_old = (180 / Math.PI) * Math.atan2(this.touchRelative[0].y, this.touchRelative[0]
  716. .x);
  717. let second_atan = (180 / Math.PI) * Math.atan2(touchRelative[1].y, touchRelative[1].x);
  718. let second_atan_old = (180 / Math.PI) * Math.atan2(this.touchRelative[1].y, this.touchRelative[1]
  719. .x);
  720. //当前旋转的角度
  721. let first_deg = first_atan - first_atan_old,
  722. second_deg = second_atan - second_atan_old;
  723. if (first_deg != 0) {
  724. current_deg = first_deg;
  725. } else if (second_deg != 0) {
  726. current_deg = second_deg;
  727. }
  728. }
  729. this.touchRelative = touchRelative;
  730. this.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
  731. //更新视图
  732. this.angle = this.angle + current_deg;
  733. this.scale = this.scale;
  734. }
  735. },
  736. //结束操作
  737. end(e) {
  738. this.flagEndTouch = true;
  739. this.moveStop();
  740. },
  741. //裁剪框处理
  742. cutTouchMove(e) {
  743. if (this.flagCutTouch && this.MOVE_THROTTLE_FLAG) {
  744. if (this.lockRatio && (this.lockWidth || this.lockHeight)) return;
  745. //节流
  746. this.MOVE_THROTTLE_FLAG = false;
  747. this.moveThrottle();
  748. let width = this.canvasWidth,
  749. height = this.canvasHeight,
  750. cutY = this.cutY,
  751. cutX = this.cutX,
  752. size_correct = () => {
  753. width = width <= this.maxWidth ? (width >= this.minWidth ? width : this.minWidth) : this
  754. .maxWidth;
  755. height = height <= this.maxHeight ? (height >= this.minHeight ? height : this.minHeight) : this
  756. .maxHeight;
  757. },
  758. size_inspect = () => {
  759. if ((width > this.maxWidth || width < this.minWidth || height > this.maxHeight || height < this
  760. .minHeight) &&
  761. this.lockRatio) {
  762. size_correct();
  763. return false;
  764. } else {
  765. size_correct();
  766. return true;
  767. }
  768. };
  769. height = this.CUT_START.height + (this.CUT_START.corner > 1 && this.CUT_START.corner < 4 ? 1 : -1) * (
  770. this.CUT_START
  771. .y - e.touches[0].clientY);
  772. switch (this.CUT_START.corner) {
  773. case 1:
  774. width = this.CUT_START.width - this.CUT_START.x + e.touches[0].clientX;
  775. if (this.lockRatio) {
  776. height = width / (this.canvasWidth / this.canvasHeight);
  777. }
  778. if (!size_inspect()) return;
  779. break;
  780. case 2:
  781. width = this.CUT_START.width - this.CUT_START.x + e.touches[0].clientX;
  782. if (this.lockRatio) {
  783. height = width / (this.canvasWidth / this.canvasHeight);
  784. }
  785. if (!size_inspect()) return;
  786. cutY = this.CUT_START.cutY - (height - this.CUT_START.height);
  787. break;
  788. case 3:
  789. width = this.CUT_START.width + this.CUT_START.x - e.touches[0].clientX;
  790. if (this.lockRatio) {
  791. height = width / (this.canvasWidth / this.canvasHeight);
  792. }
  793. if (!size_inspect()) return;
  794. cutY = this.CUT_START.cutY - (height - this.CUT_START.height);
  795. cutX = this.CUT_START.cutX - (width - this.CUT_START.width);
  796. break;
  797. case 4:
  798. width = this.CUT_START.width + this.CUT_START.x - e.touches[0].clientX;
  799. if (this.lockRatio) {
  800. height = width / (this.canvasWidth / this.canvasHeight);
  801. }
  802. if (!size_inspect()) return;
  803. cutX = this.CUT_START.cutX - (width - this.CUT_START.width);
  804. break;
  805. default:
  806. break;
  807. }
  808. if (!this.lockWidth && !this.lockHeight) {
  809. this.canvasWidth = width;
  810. this.cutX = cutX;
  811. this.canvasHeight = height;
  812. this.cutY = cutY;
  813. } else if (!this.lockWidth) {
  814. this.canvasWidth = width;
  815. this.cutX = cutX;
  816. } else if (!this.lockHeight) {
  817. this.canvasHeight = height;
  818. this.cutY = cutY;
  819. }
  820. this.imgMarginDetectionScale();
  821. }
  822. },
  823. cutTouchStart(e) {
  824. let currentX = e.touches[0].clientX;
  825. let currentY = e.touches[0].clientY;
  826. /*
  827. * (右下-1 右上-2 左上-3 左下-4)
  828. * left_x [3,4]
  829. * top_y [2,3]
  830. * right_x [1,2]
  831. * bottom_y [1,4]
  832. */
  833. let left_x1 = this.cutX - 24;
  834. let left_x2 = this.cutX + 24;
  835. let top_y1 = this.cutY - 24;
  836. let top_y2 = this.cutY + 24;
  837. let right_x1 = this.cutX + this.canvasWidth - 24;
  838. let right_x2 = this.cutX + this.canvasWidth + 24;
  839. let bottom_y1 = this.cutY + this.canvasHeight - 24;
  840. let bottom_y2 = this.cutY + this.canvasHeight + 24;
  841. if (currentX > right_x1 && currentX < right_x2 && currentY > bottom_y1 && currentY < bottom_y2) {
  842. this.moveDuring();
  843. this.flagCutTouch = true;
  844. this.flagEndTouch = true;
  845. this.CUT_START = {
  846. width: this.canvasWidth,
  847. height: this.canvasHeight,
  848. x: currentX,
  849. y: currentY,
  850. corner: 1
  851. };
  852. } else if (currentX > right_x1 && currentX < right_x2 && currentY > top_y1 && currentY < top_y2) {
  853. this.moveDuring();
  854. this.flagCutTouch = true;
  855. this.flagEndTouch = true;
  856. this.CUT_START = {
  857. width: this.canvasWidth,
  858. height: this.canvasHeight,
  859. x: currentX,
  860. y: currentY,
  861. cutY: this.cutY,
  862. cutX: this.cutX,
  863. corner: 2
  864. };
  865. } else if (currentX > left_x1 && currentX < left_x2 && currentY > top_y1 && currentY < top_y2) {
  866. this.moveDuring();
  867. this.flagCutTouch = true;
  868. this.flagEndTouch = true;
  869. this.CUT_START = {
  870. width: this.canvasWidth,
  871. height: this.canvasHeight,
  872. cutY: this.cutY,
  873. cutX: this.cutX,
  874. x: currentX,
  875. y: currentY,
  876. corner: 3
  877. };
  878. } else if (currentX > left_x1 && currentX < left_x2 && currentY > bottom_y1 && currentY < bottom_y2) {
  879. this.moveDuring();
  880. this.flagCutTouch = true;
  881. this.flagEndTouch = true;
  882. this.CUT_START = {
  883. width: this.canvasWidth,
  884. height: this.canvasHeight,
  885. cutY: this.cutY,
  886. cutX: this.cutX,
  887. x: currentX,
  888. y: currentY,
  889. corner: 4
  890. };
  891. }
  892. },
  893. cutTouchEnd(e) {
  894. this.moveStop();
  895. this.flagCutTouch = false;
  896. },
  897. //停止移动时需要做的操作
  898. moveStop() {
  899. //清空之前的自动居中延迟函数并添加最新的
  900. clearTimeout(this.TIME_CUT_CENTER);
  901. this.TIME_CUT_CENTER = setTimeout(() => {
  902. //动画启动
  903. if (!this.cutAnimation) {
  904. this.cutAnimation = true;
  905. }
  906. this.setCutCenter();
  907. }, 800);
  908. },
  909. //移动中
  910. moveDuring() {
  911. //清空之前的自动居中延迟函数
  912. clearTimeout(this.TIME_CUT_CENTER);
  913. },
  914. showLoading() {
  915. uni.showLoading({
  916. // #ifndef MP-ALIPAY
  917. mask: true,
  918. // #endif
  919. title: '请稍候...'
  920. });
  921. },
  922. stop() {},
  923. back() {
  924. uni.navigateBack();
  925. },
  926. setAngle() {
  927. this.cutAnimation = true;
  928. this.angle = this.angle + 90;
  929. }
  930. }
  931. };
  932. </script>
  933. <style scoped>
  934. .tui-container {
  935. width: 100vw;
  936. height: 100vh;
  937. padding: 0;
  938. background-color: rgba(0, 0, 0, 0.6);
  939. position: fixed;
  940. top: 0;
  941. left: 0;
  942. z-index: 1;
  943. }
  944. .tui-image-cropper {
  945. width: 100vw;
  946. height: 100vh;
  947. position: absolute;
  948. }
  949. .tui-content {
  950. width: 100vw;
  951. height: 100vh;
  952. padding: 0;
  953. position: absolute;
  954. z-index: 9;
  955. display: flex;
  956. flex-direction: column;
  957. pointer-events: none;
  958. }
  959. .tui-bg-transparent {
  960. background-color: rgba(0, 0, 0, 0.6);
  961. transition-duration: 0.35s;
  962. }
  963. .tui-content-top {
  964. pointer-events: none;
  965. }
  966. .tui-content-middle {
  967. width: 100%;
  968. height: 200px;
  969. display: flex;
  970. box-sizing: border-box;
  971. }
  972. .tui-cropper-box {
  973. position: relative;
  974. /* transition-duration: 0.3s; */
  975. border-style: solid;
  976. border-width: 1rpx;
  977. box-sizing: border-box;
  978. }
  979. .tui-flex-auto {
  980. flex: auto;
  981. }
  982. .tui-cropper-image {
  983. width: 100%;
  984. border-style: none;
  985. position: absolute;
  986. top: 0;
  987. left: 0;
  988. z-index: 2;
  989. -webkit-backface-visibility: hidden;
  990. backface-visibility: hidden;
  991. transform-origin: center;
  992. }
  993. .tui-cropper__image-hidden {
  994. opacity: 0;
  995. visibility: hidden;
  996. }
  997. .tui-cropper-canvas {
  998. position: fixed;
  999. z-index: 10;
  1000. left: -2000px;
  1001. top: -2000px;
  1002. pointer-events: none;
  1003. }
  1004. .tui-edge {
  1005. border-style: solid;
  1006. pointer-events: auto;
  1007. position: absolute;
  1008. box-sizing: border-box;
  1009. }
  1010. .tui-top-left {
  1011. border-bottom-width: 0 !important;
  1012. border-right-width: 0 !important;
  1013. }
  1014. .tui-top-right {
  1015. border-bottom-width: 0 !important;
  1016. border-left-width: 0 !important;
  1017. }
  1018. .tui-bottom-left {
  1019. border-top-width: 0 !important;
  1020. border-right-width: 0 !important;
  1021. }
  1022. .tui-bottom-right {
  1023. border-top-width: 0 !important;
  1024. border-left-width: 0 !important;
  1025. }
  1026. .tui-cropper-tabbar {
  1027. width: 100%;
  1028. height: 120rpx;
  1029. padding: 0 40rpx;
  1030. box-sizing: border-box;
  1031. position: fixed;
  1032. left: 0;
  1033. bottom: 0;
  1034. z-index: 99;
  1035. display: flex;
  1036. align-items: center;
  1037. justify-content: space-between;
  1038. color: #ffffff;
  1039. font-size: 32rpx;
  1040. }
  1041. .tui-cropper-tabbar::after {
  1042. content: ' ';
  1043. position: absolute;
  1044. top: 0;
  1045. right: 0;
  1046. left: 0;
  1047. border-top: 1rpx solid rgba(255, 255, 255, 0.2);
  1048. -webkit-transform: scaleY(0.5) translateZ(0);
  1049. transform: scaleY(0.5) translateZ(0);
  1050. transform-origin: 0 100%;
  1051. }
  1052. .tui-op-btn {
  1053. height: 80rpx;
  1054. display: flex;
  1055. align-items: center;
  1056. }
  1057. .tui-rotate-img {
  1058. width: 44rpx;
  1059. height: 44rpx;
  1060. }
  1061. </style>