ksp-image-cutter.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757
  1. <template>
  2. <view v-show="url" class="ksp-image-cutter">
  3. <canvas
  4. :style="{ width: target.width + 'px', height: target.height + 'px' }"
  5. canvas-id="target"
  6. ></canvas>
  7. <view class="body">
  8. <image
  9. v-if="url"
  10. class="image"
  11. @load="imageLoad"
  12. :style="{
  13. left: image.left + 'px',
  14. top: image.top + 'px',
  15. width: image.width + 'px',
  16. height: image.height + 'px',
  17. }"
  18. :src="url"
  19. ></image>
  20. <view v-if="mask.show" class="mask"></view>
  21. <view
  22. @touchstart="touchStart($event, 'plank')"
  23. @touchmove="touchMove"
  24. @touchend="touchEnd"
  25. @touchcancel="touchCancel"
  26. class="plank"
  27. >
  28. <view
  29. class="frame"
  30. @touchstart="touchStart($event, 'frame')"
  31. @touchstart.stop.prevent="touchHandle"
  32. :style="{
  33. left: frame.left + 'px',
  34. top: frame.top + 'px',
  35. width: frame.width + 'px',
  36. height: frame.height + 'px',
  37. }"
  38. >
  39. <canvas
  40. v-if="mask.show"
  41. class="canvas"
  42. :style="{ width: frame.width + 'px', height: frame.height + 'px' }"
  43. canvas-id="canvas"
  44. ></canvas>
  45. <view class="rect"></view>
  46. <view class="line-one"></view>
  47. <view class="line-two"></view>
  48. <view class="line-three"></view>
  49. <view class="line-four"></view>
  50. <view
  51. @touchstart="touchStart($event, 'left')"
  52. @touchstart.stop.prevent="touchHandle"
  53. class="frame-left"
  54. ></view>
  55. <view
  56. @touchstart="touchStart($event, 'right')"
  57. @touchstart.stop.prevent="touchHandle"
  58. class="frame-right"
  59. ></view>
  60. <view
  61. @touchstart="touchStart($event, 'top')"
  62. @touchstart.stop.prevent="touchHandle"
  63. class="frame-top"
  64. ></view>
  65. <view
  66. @touchstart="touchStart($event, 'bottom')"
  67. @touchstart.stop.prevent="touchHandle"
  68. class="frame-bottom"
  69. ></view>
  70. <view
  71. @touchstart="touchStart($event, 'left-top')"
  72. @touchstart.stop.prevent="touchHandle"
  73. class="frame-left-top"
  74. ></view>
  75. <view
  76. @touchstart="touchStart($event, 'left-bottom')"
  77. @touchstart.stop.prevent="touchHandle"
  78. class="frame-left-bottom"
  79. ></view>
  80. <view
  81. @touchstart="touchStart($event, 'right-top')"
  82. @touchstart.stop.prevent="touchHandle"
  83. class="frame-right-top"
  84. ></view>
  85. <view
  86. @touchstart="touchStart($event, 'right-bottom')"
  87. @touchstart.stop.prevent="touchHandle"
  88. class="frame-right-bottom"
  89. ></view>
  90. </view>
  91. </view>
  92. </view>
  93. <view class="toolbar">
  94. <button @tap="oncancle" class="btn-cancel">返回</button>
  95. <button @tap="onok" class="btn-ok">确定</button>
  96. </view>
  97. </view>
  98. </template>
  99. <script>
  100. export default {
  101. props: {
  102. url: {
  103. type: String,
  104. default: '',
  105. },
  106. fixed: {
  107. type: Boolean,
  108. default: false,
  109. },
  110. width: {
  111. type: Number,
  112. default: 200,
  113. },
  114. height: {
  115. type: Number,
  116. default: 200,
  117. },
  118. maxWidth: {
  119. type: Number,
  120. default: 1024,
  121. },
  122. maxHeight: {
  123. type: Number,
  124. default: 1024,
  125. },
  126. blob: {
  127. type: Boolean,
  128. default: true,
  129. },
  130. },
  131. data() {
  132. return {
  133. mask: {
  134. show: false,
  135. },
  136. frame: {
  137. left: 50,
  138. top: 50,
  139. width: this.width,
  140. height: this.height,
  141. },
  142. image: {
  143. left: 20,
  144. top: 20,
  145. width: 300,
  146. height: 400,
  147. },
  148. real: {
  149. width: 100,
  150. height: 100,
  151. },
  152. target: {
  153. width: this.width,
  154. height: this.height,
  155. },
  156. touches: [],
  157. type: '',
  158. start: {
  159. frame: {
  160. left: 0,
  161. top: 0,
  162. width: 0,
  163. height: 0,
  164. },
  165. image: {
  166. left: 0,
  167. top: 0,
  168. width: 0,
  169. height: 0,
  170. },
  171. },
  172. timeoutId: -1,
  173. context: null,
  174. };
  175. },
  176. mounted() {
  177. //#ifdef H5
  178. this.$el.addEventListener('touchmove', (ev) => {
  179. ev.preventDefault();
  180. });
  181. // #endif
  182. this.context = uni.createCanvasContext('canvas', this);
  183. this.targetContext = uni.createCanvasContext('target', this);
  184. },
  185. methods: {
  186. imageLoad(ev) {
  187. this.mask.show = true;
  188. this.real.width = ev.detail.width;
  189. this.real.height = ev.detail.height;
  190. this.image.width = ev.detail.width;
  191. this.image.height = ev.detail.height;
  192. this.frame.width = this.width;
  193. this.frame.height = this.height;
  194. if (!this.fixed) {
  195. this.frame.width = this.image.width;
  196. this.frame.height = this.image.height;
  197. }
  198. var query = uni.createSelectorQuery().in(this);
  199. query
  200. .select('.body')
  201. .boundingClientRect((data) => {
  202. var bw = data.width;
  203. var bh = data.height;
  204. var fw = this.frame.width;
  205. var fh = this.frame.height;
  206. var tw = bw * 0.8;
  207. var th = bh * 0.8;
  208. var sx = tw / fw;
  209. var sy = th / fh;
  210. var scale = sx;
  211. if (sx < sy) {
  212. scale = sy;
  213. }
  214. tw = fw * scale;
  215. th = fh * scale;
  216. var tx = (bw - tw) / 2;
  217. var ty = (bh - th) / 2;
  218. this.frame.width = tw;
  219. this.frame.height = th;
  220. this.frame.left = tx;
  221. this.frame.top = ty;
  222. var iw = this.image.width;
  223. var ih = this.image.height;
  224. sx = tw / iw;
  225. sy = th / ih;
  226. scale = sx;
  227. if (sx < sy) {
  228. scale = sy;
  229. }
  230. this.image.width = iw * scale;
  231. this.image.height = ih * scale;
  232. this.image.left = (bw - this.image.width) / 2;
  233. this.image.top = (bh - this.image.height) / 2;
  234. setTimeout(() => {
  235. this.trimImage();
  236. }, 100);
  237. })
  238. .exec();
  239. },
  240. touchHandle() {},
  241. touchStart(ev, type) {
  242. this.stopTime();
  243. this.mask.show = false;
  244. if (this.touches.length == 0) {
  245. this.type = type;
  246. this.start.frame.left = this.frame.left;
  247. this.start.frame.top = this.frame.top;
  248. this.start.frame.width = this.frame.width;
  249. this.start.frame.height = this.frame.height;
  250. this.start.image.left = this.image.left;
  251. this.start.image.top = this.image.top;
  252. this.start.image.width = this.image.width;
  253. this.start.image.height = this.image.height;
  254. }
  255. var touches = ev.changedTouches;
  256. for (var i = 0; i < touches.length; i++) {
  257. var touch = touches[i];
  258. // this.touches[touch.identifier] = touch;
  259. this.touches.push(touch);
  260. }
  261. },
  262. touchMove(ev) {
  263. this.stopTime();
  264. ev.preventDefault();
  265. var touches = ev.touches;
  266. if (this.touches.length == 1) {
  267. if (this.type == 'plank' || this.type == 'frame' || this.fixed) {
  268. this.moveImage(this.touches[0], touches[0]);
  269. } else {
  270. this.scaleFrame(this.touches[0], touches[0], this.type);
  271. }
  272. } else if (this.touches.length == 2 && touches.length == 2) {
  273. var ta = this.touches[0];
  274. var tb = this.touches[1];
  275. var tc = touches[0];
  276. var td = touches[1];
  277. if (ta.identifier != tc.identifier) {
  278. var temp = tc;
  279. tc = td;
  280. td = temp;
  281. }
  282. this.scaleImage(ta, tb, tc, td);
  283. }
  284. },
  285. touchEnd(ev) {
  286. this.type = '';
  287. this.touches = [];
  288. this.startTime();
  289. },
  290. touchCancel(ev) {
  291. this.type = '';
  292. this.touches = [];
  293. this.startTime();
  294. },
  295. startTime() {
  296. this.stopTime();
  297. this.timeoutId = setTimeout(() => {
  298. this.trimImage();
  299. }, 800);
  300. },
  301. stopTime() {
  302. if (this.timeoutId >= 0) {
  303. clearTimeout(this.timeoutId);
  304. this.timeoutId = -1;
  305. }
  306. },
  307. trimImage() {
  308. this.mask.show = true;
  309. var query = uni.createSelectorQuery().in(this);
  310. query
  311. .select('.body')
  312. .boundingClientRect((data) => {
  313. var bw = data.width;
  314. var bh = data.height;
  315. var fw = this.frame.width;
  316. var fh = this.frame.height;
  317. var tw = bw * 0.8;
  318. var th = bh * 0.8;
  319. var sx = tw / fw;
  320. var sy = th / fh;
  321. var scale = sx;
  322. if (sx > sy) {
  323. scale = sy;
  324. }
  325. tw = fw * scale;
  326. th = fh * scale;
  327. var tx = (bw - tw) / 2;
  328. var ty = (bh - th) / 2;
  329. var ax =
  330. tx -
  331. this.frame.left +
  332. (this.frame.left - this.image.left) * (1 - scale);
  333. var ay =
  334. ty -
  335. this.frame.top +
  336. (this.frame.top - this.image.top) * (1 - scale);
  337. this.frame.width = tw;
  338. this.frame.height = th;
  339. this.frame.left = tx;
  340. this.frame.top = ty;
  341. this.image.width *= scale;
  342. this.image.height *= scale;
  343. this.image.left += ax;
  344. this.image.top += ay;
  345. })
  346. .exec();
  347. setTimeout(() => {
  348. var scale = this.image.width / this.real.width;
  349. var x = (this.frame.left - this.image.left) / scale;
  350. var y = (this.frame.top - this.image.top) / scale;
  351. var width = this.frame.width / scale;
  352. var height = this.frame.height / scale;
  353. this.context.drawImage(
  354. this.url,
  355. x,
  356. y,
  357. width,
  358. height,
  359. 0,
  360. 0,
  361. this.frame.width,
  362. this.frame.height
  363. );
  364. this.context.draw(false);
  365. }, 100);
  366. },
  367. moveImage(ta, tb) {
  368. var ax = tb.clientX - ta.clientX;
  369. var ay = tb.clientY - ta.clientY;
  370. this.image.left = this.start.image.left + ax;
  371. this.image.top = this.start.image.top + ay;
  372. if (this.image.left > this.frame.left) {
  373. this.image.left = this.frame.left;
  374. }
  375. if (this.image.top > this.frame.top) {
  376. this.image.top = this.frame.top;
  377. }
  378. if (
  379. this.image.left + this.image.width <
  380. this.frame.left + this.frame.width
  381. ) {
  382. this.image.left = this.frame.left + this.frame.width - this.image.width;
  383. }
  384. if (
  385. this.image.top + this.image.height <
  386. this.frame.top + this.frame.height
  387. ) {
  388. this.image.top = this.frame.top + this.frame.height - this.image.height;
  389. }
  390. },
  391. scaleImage(ta, tb, tc, td) {
  392. var x1 = ta.clientX;
  393. var y1 = ta.clientY;
  394. var x2 = tb.clientX;
  395. var y2 = tb.clientY;
  396. var x3 = tc.clientX;
  397. var y3 = tc.clientY;
  398. var x4 = td.clientX;
  399. var y4 = td.clientY;
  400. var ol = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
  401. var el = Math.sqrt((x3 - x4) * (x3 - x4) + (y3 - y4) * (y3 - y4));
  402. var ocx = (x1 + x2) / 2;
  403. var ocy = (y1 + y2) / 2;
  404. var ecx = (x3 + x4) / 2;
  405. var ecy = (y3 + y4) / 2;
  406. var ax = ecx - ocx;
  407. var ay = ecy - ocy;
  408. var scale = el / ol;
  409. if (this.start.image.width * scale < this.frame.width) {
  410. scale = this.frame.width / this.start.image.width;
  411. }
  412. if (this.start.image.height * scale < this.frame.height) {
  413. scale = this.frame.height / this.start.image.height;
  414. }
  415. if (this.start.image.width * scale < this.frame.width) {
  416. scale = this.frame.width / this.start.image.width;
  417. }
  418. this.image.left =
  419. this.start.image.left +
  420. ax -
  421. (ocx - this.start.image.left) * (scale - 1);
  422. this.image.top =
  423. this.start.image.top + ay - (ocy - this.start.image.top) * (scale - 1);
  424. this.image.width = this.start.image.width * scale;
  425. this.image.height = this.start.image.height * scale;
  426. if (this.image.left > this.frame.left) {
  427. this.image.left = this.frame.left;
  428. }
  429. if (this.image.top > this.frame.top) {
  430. this.image.top = this.frame.top;
  431. }
  432. if (
  433. this.image.left + this.image.width <
  434. this.frame.left + this.frame.width
  435. ) {
  436. this.image.left = this.frame.left + this.frame.width - this.image.width;
  437. }
  438. if (
  439. this.image.top + this.image.height <
  440. this.frame.top + this.frame.height
  441. ) {
  442. this.image.top = this.frame.top + this.frame.height - this.image.height;
  443. }
  444. },
  445. scaleFrame(ta, tb, type) {
  446. var ax = tb.clientX - ta.clientX;
  447. var ay = tb.clientY - ta.clientY;
  448. var x1 = this.start.frame.left;
  449. var y1 = this.start.frame.top;
  450. var x2 = this.start.frame.left + this.start.frame.width;
  451. var y2 = this.start.frame.top + this.start.frame.height;
  452. if (type == 'left') {
  453. x1 += ax;
  454. } else if (type == 'right') {
  455. x2 += ax;
  456. } else if (type == 'top') {
  457. y1 += ay;
  458. } else if (type == 'bottom') {
  459. y2 += ay;
  460. } else if (type == 'left-top') {
  461. x1 += ax;
  462. y1 += ay;
  463. } else if (type == 'left-bottom') {
  464. x1 += ax;
  465. y2 += ay;
  466. } else if (type == 'right-top') {
  467. x2 += ax;
  468. y1 += ay;
  469. } else if (type == 'right-bottom') {
  470. x2 += ax;
  471. y2 += ay;
  472. }
  473. if (x1 < this.image.left) {
  474. x1 = this.image.left;
  475. }
  476. if (y1 < this.image.top) {
  477. y1 = this.image.top;
  478. }
  479. if (x2 > this.image.left + this.image.width) {
  480. x2 = this.image.left + this.image.width;
  481. }
  482. if (y2 > this.image.top + this.image.height) {
  483. y2 = this.image.top + this.image.height;
  484. }
  485. this.frame.left = x1;
  486. this.frame.top = y1;
  487. this.frame.width = x2 - x1;
  488. this.frame.height = y2 - y1;
  489. },
  490. parseBlob(base64) {
  491. var arr = base64.split(',');
  492. var mime = arr[0].match(/:(.*?);/)[1];
  493. var bstr = atob(arr[1]);
  494. var n = bstr.length;
  495. var u8arr = new Uint8Array(n);
  496. for (var i = 0; i < n; i++) {
  497. u8arr[i] = bstr.charCodeAt(i);
  498. }
  499. var url = URL || webkitURL;
  500. return url.createObjectURL(new Blob([u8arr], { type: mime }));
  501. },
  502. onok() {
  503. var scale = this.image.width / this.real.width;
  504. var x = (this.frame.left - this.image.left) / scale;
  505. var y = (this.frame.top - this.image.top) / scale;
  506. var width = this.frame.width / scale;
  507. var height = this.frame.height / scale;
  508. var tw = width;
  509. var th = height;
  510. if (this.fixed) {
  511. tw = this.width / 2;
  512. th = this.height / 2;
  513. } else {
  514. if (tw > this.maxWidth / 2) {
  515. var sc = this.maxWidth / 2 / tw;
  516. tw = tw * sc;
  517. th = th * sc;
  518. }
  519. if (th > this.maxHeight / 2) {
  520. var sc = this.maxHeight / 2 / th;
  521. th = th * sc;
  522. tw = tw * sc;
  523. }
  524. }
  525. this.target.width = tw;
  526. this.target.height = th;
  527. uni.showLoading({
  528. title: '正在裁剪',
  529. });
  530. setTimeout(() => {
  531. this.targetContext.drawImage(
  532. this.url,
  533. x,
  534. y,
  535. width,
  536. height,
  537. 0,
  538. 0,
  539. tw,
  540. th
  541. );
  542. this.targetContext.draw(false, () => {
  543. uni.canvasToTempFilePath(
  544. {
  545. canvasId: 'target',
  546. success: (res) => {
  547. var path = res.tempFilePath;
  548. // #ifdef H5
  549. if (this.blob) {
  550. path = this.parseBlob(path);
  551. }
  552. // #endif
  553. this.$emit('ok', {
  554. path: path,
  555. });
  556. },
  557. fail: (ev) => {
  558. console.log(ev);
  559. },
  560. complete: () => {
  561. uni.hideLoading();
  562. },
  563. },
  564. this
  565. );
  566. });
  567. }, 100);
  568. },
  569. oncancle() {
  570. this.$emit('cancel');
  571. },
  572. },
  573. };
  574. </script>
  575. <style scoped>
  576. .ksp-image-cutter {
  577. position: absolute;
  578. width: 100%;
  579. height: 100%;
  580. top: 0;
  581. bottom: 0;
  582. z-index: 1000;
  583. }
  584. .toolbar {
  585. position: absolute;
  586. width: 100%;
  587. height: 100upx;
  588. left: 0upx;
  589. bottom: 0upx;
  590. box-sizing: border-box;
  591. border-bottom: 1px solid #c0c0c0;
  592. background: #f8f8f8;
  593. }
  594. .btn-cancel {
  595. position: absolute;
  596. left: 100upx;
  597. top: 12upx;
  598. font-size: 30upx;
  599. line-height: 30upx;
  600. padding: 20upx;
  601. color: #333333;
  602. }
  603. .btn-ok {
  604. position: absolute;
  605. right: 100upx;
  606. top: 12upx;
  607. font-size: 30upx;
  608. line-height: 30upx;
  609. padding: 20upx;
  610. color: #333333;
  611. }
  612. .body {
  613. position: absolute;
  614. left: 0upx;
  615. right: 0upx;
  616. top: 0upx;
  617. bottom: 100upx;
  618. background: black;
  619. overflow: hidden;
  620. }
  621. .mask {
  622. position: absolute;
  623. left: 0upx;
  624. right: 0upx;
  625. top: 0upx;
  626. bottom: 0upx;
  627. background: black;
  628. opacity: 0.4;
  629. }
  630. .plank {
  631. position: absolute;
  632. left: 0upx;
  633. right: 0upx;
  634. top: 0upx;
  635. bottom: 0upx;
  636. }
  637. .image {
  638. position: absolute;
  639. }
  640. .frame {
  641. position: absolute;
  642. }
  643. .canvas {
  644. position: absolute;
  645. display: block;
  646. left: 0px;
  647. top: 0px;
  648. }
  649. .rect {
  650. position: absolute;
  651. left: -2px;
  652. top: -2px;
  653. width: 100%;
  654. height: 100%;
  655. border: 2px solid white;
  656. }
  657. .line-one {
  658. position: absolute;
  659. width: 100%;
  660. height: 1px;
  661. background: white;
  662. left: 0;
  663. top: 33.3%;
  664. }
  665. .line-two {
  666. position: absolute;
  667. width: 100%;
  668. height: 1px;
  669. background: white;
  670. left: 0;
  671. top: 66.7%;
  672. }
  673. .line-three {
  674. position: absolute;
  675. width: 1px;
  676. height: 100%;
  677. background: white;
  678. top: 0;
  679. left: 33.3%;
  680. }
  681. .line-four {
  682. position: absolute;
  683. width: 1px;
  684. height: 100%;
  685. background: white;
  686. top: 0;
  687. left: 66.7%;
  688. }
  689. .frame-left {
  690. position: absolute;
  691. height: 100%;
  692. width: 8px;
  693. left: -4px;
  694. top: 0;
  695. }
  696. .frame-right {
  697. position: absolute;
  698. height: 100%;
  699. width: 8px;
  700. right: -4px;
  701. top: 0;
  702. }
  703. .frame-top {
  704. position: absolute;
  705. width: 100%;
  706. height: 8px;
  707. top: -4px;
  708. left: 0;
  709. }
  710. .frame-bottom {
  711. position: absolute;
  712. width: 100%;
  713. height: 8px;
  714. bottom: -4px;
  715. left: 0;
  716. }
  717. .frame-left-top {
  718. position: absolute;
  719. width: 20px;
  720. height: 20px;
  721. left: -6px;
  722. top: -6px;
  723. border-left: 4px solid red;
  724. border-top: 4px solid red;
  725. }
  726. .frame-left-bottom {
  727. position: absolute;
  728. width: 20px;
  729. height: 20px;
  730. left: -6px;
  731. bottom: -6px;
  732. border-left: 4px solid red;
  733. border-bottom: 4px solid red;
  734. }
  735. .frame-right-top {
  736. position: absolute;
  737. width: 20px;
  738. height: 20px;
  739. right: -6px;
  740. top: -6px;
  741. border-right: 4px solid red;
  742. border-top: 4px solid red;
  743. }
  744. .frame-right-bottom {
  745. position: absolute;
  746. width: 20px;
  747. height: 20px;
  748. right: -6px;
  749. bottom: -6px;
  750. border-right: 4px solid red;
  751. border-bottom: 4px solid red;
  752. }
  753. </style>