tm-avatarCrop.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. <template>
  2. <view class="tm-avatarCrop fixed t-0 l-0 black" :style="{ width: `${width}px`, height: `${height}px` }">
  3. <tm-sticky model="bottom">
  4. <view class="fulled flex-center mb-32">
  5. <tm-button size="m" text @click="$emit('cancel')">取消</tm-button>
  6. <tm-button size="m" @click="selectedImage">选择图片</tm-button>
  7. <tm-button size="m" @click="saveImage">确定</tm-button>
  8. </view>
  9. </tm-sticky>
  10. <view class="flex-center"><canvas id="AvatarCrop" canvas-id="AvatarCrop" :style="{ width: `${area_width}px`, height: `${area_height}px` }"></canvas></view>
  11. <movable-area class="absolute t-0 l-0 zIndex-n1" :style="{ width: `${width}px`, height: `${height}px` }">
  12. <movable-view
  13. :out-of-bounds="false"
  14. @scale="movaScaleChange"
  15. @change="movaChange"
  16. :x="areview_x"
  17. :y="areview_y"
  18. direction="all"
  19. :scale="true"
  20. :style="{ width: `${scale_w}px`, height: `${scale_h}px` }"
  21. >
  22. <image v-show="image_src" @load="loadImage" :src="image_src" :style="{ width: `${scale_w}px`, height: `${scale_h}px` }"></image>
  23. </movable-view>
  24. </movable-area>
  25. <view :style="{ width: `${width}px`, height: `${height}px` }" class="absolute tm-avatarCrop-bodywk t-0 l-0 zIndex-n16">
  26. <view
  27. class="tm-avatarCrop-area relative"
  28. :class="[isArc ? 'rounded' : '']"
  29. :style="{
  30. width: `${area_width}px`,
  31. height: `${area_height}px`,
  32. top: `${posArray[0].y}px`,
  33. left: `${posArray[0].x}px`
  34. }"
  35. >
  36. <view class="flex-center text-size-s" :style="{ height: pos_size + 'px' }">宽:{{ Math.floor(area_width) }},高:{{ Math.floor(area_height) }}</view>
  37. <block v-for="(item, index) in 4" :key="index">
  38. <view
  39. v-if="(isRatio == true && index !== 3) || index == 3"
  40. :key="index"
  41. :style="{ width: `${pos_size}px`, height: `${pos_size}px` }"
  42. @touchstart.stop.prevent="m_start($event, index)"
  43. @touchmove.stop.prevent="m_move($event, index)"
  44. @touchend.stop.prevent="m_end($event, index)"
  45. @mousedown.stop.prevent="m_start($event, index)"
  46. @mousemove.stop.prevent="m_move($event, index)"
  47. @mouseup.stop.prevent="m_end($event, index)"
  48. @mouseleave="m_end($event, index)"
  49. class="tm-avatarCrop-pos absolute black opacity-5"
  50. :class="[
  51. 'tm-avatarCrop-pos-'+index,
  52. index == 0?'tm-avatarCrop-area-top-left':'',
  53. index == 1?'tm-avatarCrop-area-top-right': '',
  54. index == 2?'tm-avatarCrop-area-bottom-left': '',
  55. index == 3?'tm-avatarCrop-area-bottom-right': ''
  56. ]"
  57. :id="`${index}`"
  58. >
  59. <tm-icons style="line-height: 0;" color="white" v-if="index !== 3" :size="22" dense name="icon-expand-alt"></tm-icons>
  60. <tm-icons style="line-height: 0;" color="white" v-else :size="22" dense name="icon-arrows-alt"></tm-icons>
  61. </view>
  62. </block>
  63. </view>
  64. </view>
  65. </view>
  66. </template>
  67. <script>
  68. /**
  69. * 图片裁剪
  70. * @property {Number} area-width = [] 默认300,裁剪框的宽度
  71. * @property {Number} area-height = [] 默认300,裁剪框的高度
  72. * @property {Number} quality = [] 默认1,图片压缩质量0-1
  73. * @property {String} fileType = [jpg|png] 默认 png
  74. * @property {Boolean} is-ratio = [] 默认false,是否允许用户调整裁剪框的大小
  75. * @property {Boolean} is-arc = [] 默认false,是否圆形
  76. */
  77. import tmIcons from '@/tm-vuetify/components/tm-icons/tm-icons.vue';
  78. import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
  79. import tmSticky from '@/tm-vuetify/components/tm-sticky/tm-sticky.vue';
  80. import tmImages from '@/tm-vuetify/components/tm-images/tm-images.vue';
  81. export default {
  82. name: 'tm-avatarCrop',
  83. props: {
  84. areaWidth: {
  85. type: Number,
  86. default: 300
  87. },
  88. areaHeight: {
  89. type: Number,
  90. default: 300
  91. },
  92. //是否允许用户调整距形区域的大小 。
  93. isRatio: {
  94. type: Boolean,
  95. default: false
  96. },
  97. //是否圆形。
  98. isArc: {
  99. type: Boolean,
  100. default: false
  101. },
  102. quality: {
  103. type: Number,
  104. default: 1
  105. },
  106. fileType:{
  107. type:String,
  108. default:'png'
  109. },
  110. confirm: {
  111. type: Function,
  112. default: function(data) {
  113. return function(data) {};
  114. }
  115. }
  116. },
  117. computed: {},
  118. components: {
  119. tmIcons,
  120. tmButton,
  121. tmSticky,
  122. tmImages
  123. },
  124. data() {
  125. return {
  126. width: 0,
  127. height: 0,
  128. canvanid: 'AvatarCrop',
  129. showCanva: false,
  130. area_width: 0,
  131. area_height: 0,
  132. prevent_left: 0,
  133. prevent_top: 0,
  134. old_x: 0,
  135. old_y: 0,
  136. posArray: [],
  137. endDrage: true,
  138. pos_size: 24,
  139. image_src: '',
  140. scale_w: 0, //图片缩放的宽,
  141. scale_h: 0, //图片缩放的高。
  142. scale: 1,
  143. real_w: 0,
  144. real_h: 0,
  145. scale_areview_x: 0,
  146. scale_areview_y: 0,
  147. areview_x: 0,
  148. areview_y: 0,
  149. areview_new_x: 0,
  150. areview_new_y: 0,
  151. isAddImage: false
  152. };
  153. },
  154. destroyed() {},
  155. created() {
  156. let sys = uni.getSystemInfoSync();
  157. this.width = sys.windowWidth;
  158. this.height = sys.screenHeight;
  159. this.area_width = uni.upx2px(this.areaWidth);
  160. this.area_height = uni.upx2px(this.areaHeight);
  161. let dr = [];
  162. for (let i = 0; i < 4; i++) {
  163. dr.push({
  164. x: 0,
  165. y: 0
  166. });
  167. }
  168. dr[0].x = (this.width - this.area_width) / 2;
  169. dr[0].y = (this.height - this.area_height) / 2;
  170. this.posArray = [...dr];
  171. },
  172. async mounted() {
  173. let t = this;
  174. await this.jishunTopData();
  175. },
  176. methods: {
  177. async jishunTopData() {
  178. let t =this;
  179. this.$nextTick(async function() {
  180. this.listData = [];
  181. uni.$tm.sleep(100).then(s=>{
  182. let psd = uni.createSelectorQuery().in(t);
  183. psd.select('.tm-avatarCrop-pos-0').boundingClientRect()
  184. .select('.tm-avatarCrop-pos-1').boundingClientRect()
  185. .select('.tm-avatarCrop-pos-2').boundingClientRect()
  186. .select('.tm-avatarCrop-pos-3').boundingClientRect()
  187. .exec(function(p){
  188. let list = p;
  189. let dr = [...t.posArray];
  190. for (let i = 0; i < list.length; i++) {
  191. if(list[i]){
  192. dr.splice(parseInt(list[i].id), 1, {
  193. x: list[i].left,
  194. y: list[i].top
  195. });
  196. }
  197. }
  198. t.posArray = [...dr];
  199. })
  200. })
  201. });
  202. },
  203. async loadImage(e) {
  204. this.isAddImage = true;
  205. this.posArray.splice(0, 1, {
  206. x: (this.width - this.area_width) / 2,
  207. y: (this.height - this.area_height) / 2
  208. });
  209. await this.jishunTopData();
  210. this.$nextTick(async function() {
  211. let img_w = e.detail.width;
  212. let img_h = e.detail.height;
  213. this.real_w = img_w;
  214. this.real_h = img_h;
  215. let ratio_w = img_w >= this.width ? this.width : img_w;
  216. let ratio_h = ratio_w / (img_w / img_h);
  217. this.scale_w = ratio_w;
  218. this.scale_h = ratio_h;
  219. //图片宽大于高度时,
  220. this.areview_y = (this.height - this.scale_h) / 2;
  221. this.areview_x = (this.width - this.scale_w) / 2;
  222. this.areview_new_x = this.areview_x;
  223. this.areview_new_y = this.areview_y;
  224. });
  225. },
  226. selectedImage() {
  227. let t = this;
  228. uni.chooseImage({
  229. count: 1,
  230. sizeType: ['compressed'],
  231. success: function(res) {
  232. t.image_src = res.tempFilePaths[0];
  233. }
  234. });
  235. },
  236. saveImage() {
  237. if (!this.image_src) {
  238. uni.$tm.toast('未选择图片');
  239. return;
  240. }
  241. let t = this;
  242. this.$nextTick(async function() {
  243. let scale_x = this.posArray[0].x - this.areview_new_x;
  244. let scale_y = this.posArray[0].y - this.areview_new_y;
  245. //计算真实的xy时,需要通过原有的缩放的大小通过比例来放大或者缩小到真实在源图片上的坐标。
  246. let real_x = (this.real_w / (this.scale_w * this.scale)) * scale_x;
  247. let real_y = (this.real_h / (this.scale_h * this.scale)) * scale_y;
  248. let real_w = (this.real_w / (this.scale_w * this.scale)) * this.area_width;
  249. let real_h = real_w;
  250. if (this.isRatio) {
  251. real_h = (this.real_h / (this.scale_h * this.scale)) * this.area_height;
  252. }
  253. const ctx = uni.createCanvasContext('AvatarCrop', this);
  254. if (!ctx) return;
  255. //如果把框移动到图片外,则不要截取。
  256. if (real_x < 0 || real_y < 0) {
  257. uni.$tm.toast('请把框移入图片中');
  258. return;
  259. }
  260. if (this.isArc) {
  261. ctx.beginPath();
  262. ctx.arc(this.area_width / 2, this.area_width / 2, this.area_width / 2, 0, 2 * Math.PI);
  263. ctx.clip();
  264. }
  265. ctx.drawImage(this.image_src, real_x, real_y, real_w, real_h, 0, 0, this.area_width, this.area_height);
  266. uni.showLoading({ title: '...' });
  267. function getimage() {
  268. let rtx = uni.getSystemInfoSync().pixelRatio;
  269. let a_w = t.area_width * rtx;
  270. let a_h = t.area_height * rtx;
  271. console.log(a_w,a_h);
  272. uni.canvasToTempFilePath(
  273. {
  274. x: 0,
  275. y: 0,
  276. width: a_w,
  277. height: a_h,
  278. canvasId: 'AvatarCrop',
  279. quality:t.quality,
  280. fileType:t.fileType,
  281. success: function(res) {
  282. // 在H5平台下,tempFilePath 为 base64
  283. uni.hideLoading();
  284. t.$nextTick(function() {
  285. t.$emit('confirm', { width:a_w, height: a_h, src: res.tempFilePath });
  286. t.confirm({ width: a_w, height: a_h, src: res.tempFilePath });
  287. });
  288. },
  289. fail: function(res) {
  290. uni.$tm.toast('请重试');
  291. }
  292. },
  293. t
  294. );
  295. }
  296. ctx.draw(true, function() {
  297. getimage();
  298. });
  299. });
  300. },
  301. movaChange(e) {
  302. //当添加新图片时,这里的执行要比添加时的慢。因此会覆盖前面设置的xy
  303. if (!this.isAddImage) {
  304. //移动后,真实的x,y已经得到,不需要再计算缩放的xy
  305. //(因为uniapp的bug缩放后返回 的xy始终是原有的xy而不是真实的)
  306. this.scale_areview_x = 0;
  307. this.scale_areview_y = 0;
  308. this.areview_new_x = e.detail.x;
  309. this.areview_new_y = e.detail.y;
  310. } else {
  311. this.isAddImage = false;
  312. }
  313. },
  314. movaScaleChange(e) {
  315. //通过缩放,计算出缩放后的x,y的和原有的x,y之间的差值得到真实的x,y。
  316. //(因为uniapp的bug缩放后返回 的xy始终是原有的xy而不是真实的)
  317. let scale_x = -(this.scale_w - this.scale_w * e.detail.scale) / 2;
  318. let scale_y = (this.scale_h - this.scale_h * e.detail.scale) / 2;
  319. this.areview_new_x = -scale_x;
  320. this.areview_new_y = this.posArray[0].y - Math.abs(scale_y);
  321. // 保存缩放的比例。
  322. this.scale = e.detail.scale;
  323. },
  324. m_start(event, index) {
  325. event.preventDefault();
  326. event.stopPropagation();
  327. const ctx = uni.createCanvasContext('AvatarCrop', this);
  328. if (ctx) {
  329. ctx.clearRect(0, 0, this.area_width, this.area_height);
  330. ctx.draw();
  331. }
  332. var touch;
  333. if (event.type.indexOf('mouse') == -1 && event.changedTouches.length == 1) {
  334. touch = event.changedTouches[0];
  335. } else {
  336. touch = {
  337. pageX:event.pageX,
  338. pageY:event.pageY
  339. }
  340. }
  341. // #ifdef APP-VUE
  342. if (index == 0 || index == 2) {
  343. this.old_x = touch.pageX;
  344. } else if (index == 1 || index == 3) {
  345. this.old_x = touch.pageX + this.area_width;
  346. }
  347. if (index == 0 || index == 1) {
  348. this.old_y = touch.pageY;
  349. } else if (index == 2 || index == 3) {
  350. this.old_y = touch.pageY+ this.area_height;
  351. }
  352. // #endif
  353. // #ifndef APP-VUE
  354. if (index == 0 || index == 2) {
  355. this.old_x = touch.pageX - event.currentTarget.offsetLeft - this.posArray[index].x;
  356. } else if (index == 1 || index == 3) {
  357. this.old_x = touch.pageX - event.currentTarget.offsetLeft - this.posArray[index].x + this.area_width - this.pos_size;
  358. }
  359. if (index == 0 || index == 1) {
  360. this.old_y = touch.pageY - event.currentTarget.offsetTop - this.posArray[index].y;
  361. } else if (index == 2 || index == 3) {
  362. this.old_y = touch.pageY - event.currentTarget.offsetTop - this.posArray[index].y + this.area_height - this.pos_size;
  363. }
  364. // #endif
  365. this.endDrage = false;
  366. },
  367. m_move(event, index) {
  368. if (this.endDrage) return;
  369. let t = this;
  370. event.preventDefault();
  371. event.stopPropagation();
  372. var touch;
  373. if (event.type.indexOf('mouse') == -1 && event.changedTouches.length == 1) {
  374. var touch = event.changedTouches[0];
  375. } else {
  376. touch = {
  377. pageX:event.pageX,
  378. pageY:event.pageY
  379. }
  380. }
  381. // #ifdef APP-VUE
  382. let ch = touch.pageY - this.pos_size/2;
  383. let chx = touch.pageX - this.pos_size/2;
  384. // #endif
  385. // #ifndef APP-VUE
  386. let ch = touch.pageY - t.old_y;
  387. let chx = touch.pageX - t.old_x;
  388. // #endif
  389. let pos_size = this.pos_size;
  390. let x_cha_len = chx - t.posArray[index].x;
  391. let y_cha_len = ch - t.posArray[index].y;
  392. t.posArray.splice(index, 1, {
  393. x: chx,
  394. y: ch
  395. });
  396. let w = 0;
  397. let h = 0;
  398. if (index == 0) {
  399. t.posArray.splice(1, 1, {
  400. x: t.posArray[1].x,
  401. y: ch
  402. });
  403. t.posArray.splice(2, 1, {
  404. x: chx,
  405. y: t.posArray[2].y
  406. });
  407. w = t.posArray[1].x + pos_size - t.posArray[0].x;
  408. h = t.posArray[2].y + pos_size - t.posArray[0].y;
  409. } else if (index == 1) {
  410. t.posArray.splice(0, 1, {
  411. x: t.posArray[0].x,
  412. y: ch
  413. });
  414. t.posArray.splice(3, 1, {
  415. x: chx,
  416. y: t.posArray[3].y
  417. });
  418. w = t.posArray[1].x + pos_size - t.posArray[0].x;
  419. h = t.posArray[2].y + pos_size - t.posArray[1].y;
  420. } else if (index == 2) {
  421. t.posArray.splice(0, 1, {
  422. x: chx,
  423. y: t.posArray[0].y
  424. });
  425. t.posArray.splice(3, 1, {
  426. x: t.posArray[3].x,
  427. y: ch
  428. });
  429. w = t.posArray[3].x + pos_size - t.posArray[2].x;
  430. h = t.posArray[2].y + pos_size - t.posArray[1].y;
  431. }
  432. if (index !== 3) {
  433. this.area_width = w < 30 ? 30 : w;
  434. this.area_height = h < 30 ? 30 : h;
  435. } else {
  436. let top_x = chx - this.area_width + pos_size;
  437. let top_y = ch - this.area_height + pos_size;
  438. t.posArray.splice(0, 1, {
  439. x: top_x,
  440. y: top_y
  441. });
  442. t.posArray.splice(1, 1, {
  443. x: top_x + this.area_width - pos_size,
  444. y: top_y
  445. });
  446. t.posArray.splice(2, 1, {
  447. x: top_x,
  448. y: top_y + this.area_height - pos_size
  449. });
  450. }
  451. },
  452. m_end(event, index) {
  453. if (this.disabled) return;
  454. let t = this;
  455. event.preventDefault();
  456. event.stopPropagation();
  457. this.endDrage = true;
  458. }
  459. }
  460. };
  461. </script>
  462. <style lang="scss" scoped>
  463. .tm-avatarCrop {
  464. .tm-avatarCrop-bodywk {
  465. pointer-events: none;
  466. }
  467. .tm-avatarCrop-area {
  468. background-color: rgba(0, 0, 0, 0.3);
  469. border: 1px dotted rgba(255, 255, 255, 0.7);
  470. pointer-events: none;
  471. .tm-avatarCrop-pos {
  472. display: flex;
  473. justify-content: center;
  474. align-items: center;
  475. background-color: grey;
  476. pointer-events: auto;
  477. &.tm-avatarCrop-area-top-left {
  478. left: 0;
  479. top: 0;
  480. transform: rotate(90deg);
  481. }
  482. &.tm-avatarCrop-area-top-right {
  483. right: 0;
  484. top: 0;
  485. }
  486. &.tm-avatarCrop-area-bottom-right {
  487. right: 0;
  488. bottom: 0;
  489. }
  490. &.tm-avatarCrop-area-bottom-left {
  491. left: 0;
  492. bottom: 0;
  493. }
  494. }
  495. }
  496. }
  497. </style>