tt-cropper.vue 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107
  1. <template>
  2. <view class="t-cropper" v-show="imageUrl" disable-scroll>
  3. <!-- #ifdef MP-WEIXIN || MP-ALIPAY-->
  4. <canvas
  5. type="2d"
  6. canvas-id="canvas-cropper"
  7. id="canvas-cropper"
  8. class="canvas"
  9. ></canvas>
  10. <!-- #endif -->
  11. <!-- #ifdef APP-PLUS || H5 -->
  12. <canvas
  13. :canvas-id="canvasId"
  14. :style="{
  15. width: origin.canvasWidth + 'px',
  16. height: origin.canvasHeight + 'px',
  17. }"
  18. ></canvas>
  19. <!-- #endif -->
  20. <!-- 裁剪区域 -->
  21. <view class="t-preview-container" :class="{ showPage: inInit }">
  22. <view
  23. class="preview-body"
  24. @touchstart="(e) => touchStart(e, 'body')"
  25. @touchmove="touchMove"
  26. @touchend="touchEnd"
  27. @touchcancel="touchCancel"
  28. >
  29. <!-- 完整背景图片 -->
  30. <view
  31. class="image-wrap"
  32. :class="{ transit: origin.transit }"
  33. :style="[comImageStyle('image-wrap')]"
  34. >
  35. <image
  36. class="image"
  37. :class="{ transit: origin.transit }"
  38. :src="origin.imageUrl"
  39. @load="imageLoad"
  40. :style="[comImageStyle('image')]"
  41. />
  42. </view>
  43. <view class="mask-model"></view>
  44. <view
  45. class="frame-box"
  46. :class="{ transit: origin.transit }"
  47. :style="[comImageStyle('frame-box')]"
  48. >
  49. <view class="rect" :style="cirStyle">
  50. <!-- 裁剪框图片 -->
  51. <view
  52. class="image-rect"
  53. :class="{ transit: origin.transit }"
  54. :style="[comImageStyle('image-rect')]"
  55. >
  56. <image
  57. class="rect-img"
  58. :class="{ transit: origin.transit }"
  59. :src="origin.imageUrl"
  60. :style="[comImageStyle('image')]"
  61. />
  62. </view>
  63. </view>
  64. <!-- 矩阵框线条 -->
  65. <view v-if="radius < 50" class="line-box">
  66. <view class="line-one"></view>
  67. <view class="line-two"></view>
  68. <view class="line-three"></view>
  69. <view class="line-four"></view>
  70. </view>
  71. <view
  72. class="frame-left-top"
  73. @touchstart.stop="(e) => touchStart(e, 'left-top')"
  74. ></view>
  75. <view
  76. class="frame-left-bottom"
  77. @touchstart.stop="(e) => touchStart(e, 'left-bottom')"
  78. ></view>
  79. <view
  80. class="frame-right-top"
  81. @touchstart.stop="(e) => touchStart(e, 'right-top')"
  82. ></view>
  83. <view
  84. class="frame-right-bottom"
  85. @touchstart.stop="(e) => touchStart(e, 'right-bottom')"
  86. ></view>
  87. </view>
  88. </view>
  89. <!-- 底部工具栏 -->
  90. <view class="toolbar">
  91. <view @tap.stop="onCancle" class="btn-cancel">取消</view>
  92. <view
  93. v-if="isRotateBtn && mode !== 'free'"
  94. @tap.stop="onAngle"
  95. class="btn-rotate"
  96. >
  97. <image src="../../static/svg/rotate.svg" data-type="inverse" />
  98. </view>
  99. <view @tap.stop="onConfirm" class="btn-confirm">确定</view>
  100. </view>
  101. </view>
  102. </view>
  103. </template>
  104. <script>
  105. /**
  106. * @name:防抖
  107. * @param {function} fn
  108. * @param {wait} wait
  109. * @return {function}
  110. */
  111. const debounce = (fn, wait = 1000) => {
  112. let time = null;
  113. return function (...args) {
  114. if (time) clearTimeout(time);
  115. time = setTimeout(() => {
  116. fn.apply(this, args);
  117. }, wait);
  118. };
  119. };
  120. export default {
  121. /**
  122. * @property {String} mode 模式
  123. * @value fixed 固定模式,裁剪出固定大小
  124. * @value ratio 等比模式,宽高等比缩放
  125. * @value free 自由模式,不限制宽高比
  126. * @property {String} imageUrl 图片路径
  127. * @property {Number} width 宽度
  128. * @property {Number} height 高度
  129. * @property {Number} maxWidth 最大宽度
  130. * @property {Number} minHeight 最大高度
  131. * @property {Number} scaleRatio 裁剪比列缩放
  132. * @property {Number} minRatio 最小缩放
  133. * @property {Number} maxRatio 最大缩放
  134. * @property {Boolean} isRotateBtn 是否显示旋转
  135. * @property {Boolean} isCutSize 是否导出高清裁剪原图
  136. * @property {Number} radius 裁剪图片圆角半径,单位px
  137. * @property {Number} delay 快速重复点击时间
  138. */
  139. props: {
  140. mode: {
  141. type: String,
  142. default: "ratio",
  143. },
  144. imageUrl: {
  145. type: String,
  146. default: "",
  147. },
  148. width: {
  149. type: Number,
  150. default: 200,
  151. },
  152. height: {
  153. type: Number,
  154. default: 200,
  155. },
  156. maxWidth: {
  157. type: Number,
  158. default: 1024,
  159. },
  160. maxHeight: {
  161. type: Number,
  162. default: 1024,
  163. },
  164. scaleRatio: {
  165. type: Number,
  166. default: 0.7,
  167. },
  168. minRatio: {
  169. type: Number,
  170. default: 1,
  171. },
  172. maxRatio: {
  173. type: Number,
  174. default: 3,
  175. },
  176. isRotateBtn: {
  177. type: Boolean,
  178. default: true,
  179. },
  180. isCutSize: {
  181. type: Boolean,
  182. default: true,
  183. },
  184. radius: {
  185. type: Number,
  186. default: 0,
  187. },
  188. delay: {
  189. type: Number,
  190. default: 250,
  191. },
  192. },
  193. mounted() {
  194. this.onConfirm = this.createConfirm(); //确定按钮防抖
  195. },
  196. data() {
  197. return {
  198. inInit: false, //是否初始化
  199. canvasId: Math.random().toString(36).slice(-6), //获取组件实例
  200. // 获取原始图片的大小
  201. origin: {
  202. imageUrl: "", //设置图片url
  203. real: {
  204. //原始图片宽高
  205. width: 100,
  206. height: 100,
  207. },
  208. body: {
  209. //页面宽高
  210. width: 100,
  211. height: 100,
  212. },
  213. frame: {
  214. //矩形框
  215. left: 50,
  216. top: 50,
  217. width: 200,
  218. height: 300,
  219. },
  220. bgImage: {
  221. //背景框
  222. left: 20,
  223. top: 20,
  224. width: 300,
  225. height: 400,
  226. },
  227. rotate: 0,
  228. transit: false,
  229. canvasWidth: 100,
  230. canvasHeight: 100,
  231. },
  232. //绘制手指触摸
  233. move: {
  234. touchType: "body", //移动的类型
  235. start: {
  236. //开始的位置
  237. frame: {
  238. left: 0,
  239. top: 0,
  240. width: 0,
  241. height: 0,
  242. },
  243. bgImage: {
  244. left: 0,
  245. top: 0,
  246. width: 0,
  247. height: 0,
  248. },
  249. },
  250. },
  251. touch: {
  252. touchStart: [], //记录开始触摸
  253. },
  254. frameSize: 150, //裁剪框最小尺寸
  255. };
  256. },
  257. watch: {
  258. // 监听原始图片的变动
  259. imageUrl(val) {
  260. if (val) {
  261. uni.showLoading({
  262. title: "请稍候...",
  263. mask: true,
  264. });
  265. this.origin.imageUrl = val; //设置图片url
  266. }
  267. },
  268. },
  269. computed: {
  270. //计算圆角比列
  271. cirStyle() {
  272. const { width } = this.origin.frame;
  273. let scale = this.width / width;
  274. let radius = this.radius / scale;
  275. return `border-radius: ${radius}px`;
  276. },
  277. // 公共图片背景位置
  278. comImageStyle() {
  279. return (source) => {
  280. const { left, top, width, height } = this.origin.bgImage;
  281. if (source == "image-wrap") {
  282. //计算背景盒子样式
  283. const style = {};
  284. style.left = left + "px";
  285. style.top = top + "px";
  286. style.width = width + "px";
  287. style.height = height + "px";
  288. return style;
  289. } else if (source == "image") {
  290. // 计算背景图片样式
  291. let left = 0;
  292. let top = 0;
  293. let width = this.origin.bgImage.width;
  294. let height = this.origin.bgImage.height;
  295. if (this.origin.rotate % 180 != 0) {
  296. width = this.origin.bgImage.height;
  297. height = this.origin.bgImage.width;
  298. top = width / 2 - height / 2;
  299. left = height / 2 - width / 2;
  300. }
  301. const style = {};
  302. style.left = left + "px";
  303. style.top = top + "px";
  304. style.width = width + "px";
  305. style.height = height + "px";
  306. style.transform = `rotate(${this.origin.rotate}deg)`;
  307. return style;
  308. } else if (source == "image-rect") {
  309. // 计算裁剪框上显示的图片位置
  310. const style = {};
  311. style.left = left - this.origin.frame.left + "px";
  312. style.top = top - this.origin.frame.top + "px";
  313. style.width = width + "px";
  314. style.height = height + "px";
  315. return style;
  316. } else if (source == "frame-box") {
  317. //裁剪矩形框
  318. const style = {};
  319. style.left = this.origin.frame.left + "px";
  320. style.top = this.origin.frame.top + "px";
  321. style.width = this.origin.frame.width + "px";
  322. style.height = this.origin.frame.height + "px";
  323. return style;
  324. }
  325. };
  326. },
  327. },
  328. methods: {
  329. // 检测图片加载完成
  330. imageLoad(e) {
  331. const { width, height } = e.detail;
  332. this.origin.real.width = width;
  333. this.origin.real.height = height;
  334. var query = uni.createSelectorQuery().in(this);
  335. query
  336. .select(".preview-body")
  337. .boundingClientRect((data) => {
  338. this.origin.body.width = data.width;
  339. this.origin.body.height = data.height;
  340. this.inInit = true;
  341. this.imageReset(); //重置图片
  342. })
  343. .exec();
  344. },
  345. //重置图片
  346. imageReset() {
  347. this.origin.rotate = 0;
  348. let frameRate = this.width / this.height; //裁剪比列
  349. let frameHeight = this.origin.body.height * this.scaleRatio; //裁剪框图片高度 * 缩小0.7倍
  350. let frameWidth = this.origin.body.width * this.scaleRatio; //裁剪框图片宽度 * 缩小0.7倍
  351. // 缩放后的宽度/高度 > 组件裁剪的比例,就要对宽度重写
  352. if (frameWidth / frameHeight > frameRate) {
  353. frameWidth = frameHeight * frameRate;
  354. } else {
  355. frameHeight = frameWidth / frameRate;
  356. }
  357. // 裁剪框左边距=页面宽度-裁剪框宽度 / 2
  358. let frameleft = (this.origin.body.width - frameWidth) / 2;
  359. let frameTop = (this.origin.body.height - frameHeight) / 2;
  360. this.origin.frame = {
  361. left: frameleft,
  362. top: frameTop,
  363. width: frameWidth,
  364. height: frameHeight,
  365. };
  366. // 背景图片位置:
  367. let bgRate = this.origin.real.width / this.origin.real.height; //背景比列
  368. let bgWidth = frameWidth;
  369. let bgHeight = frameHeight;
  370. // 裁剪框图片宽度/裁剪框图片高度>实际的图片比例
  371. if (bgWidth / bgHeight > bgRate) {
  372. bgHeight = bgWidth / bgRate;
  373. } else {
  374. bgWidth = bgHeight * bgRate;
  375. }
  376. // 背景左边距
  377. let bgLeft = (frameWidth - bgWidth) / 2 + this.origin.frame.left;
  378. // 背景右边距
  379. let bgTop = (frameHeight - bgHeight) / 2 + this.origin.frame.top;
  380. this.origin.bgImage = {
  381. left: bgLeft,
  382. top: bgTop,
  383. width: bgWidth,
  384. height: bgHeight,
  385. };
  386. uni.hideLoading();
  387. },
  388. // 取消按钮
  389. onCancle() {
  390. this.$emit("cancel");
  391. },
  392. onAngle() {
  393. this.origin.rotate -= 90; // 旋转的角度
  394. let width = this.origin.bgImage.height; // 背景框宽度
  395. let height = this.origin.bgImage.width; // 背景框高度
  396. let left = this.origin.bgImage.left; // 背景框左边距
  397. let top = this.origin.bgImage.top; // 背景框顶边距
  398. let fWidth = this.origin.frame.width; // 裁剪框宽度
  399. let fLeft = this.origin.frame.left; // 裁剪框左边距
  400. let fTop = this.origin.frame.top; // 裁剪框顶边距
  401. // 左边距 = 矩形框左边距 + (背景框顶部 - 裁剪框顶部)
  402. left = fLeft + (top - fTop);
  403. // 顶部边距 = 矩形框顶边距 -(背景框高度 - 矩形框宽度 -(矩形框左边距 - 背景框左边距))
  404. top = fTop - (height - fWidth - (fLeft - this.origin.bgImage.left));
  405. this.origin.bgImage = {
  406. left: left,
  407. top: top,
  408. width: width,
  409. height: height,
  410. };
  411. this.origin.transit = true;
  412. setTimeout(() => {
  413. this.origin.transit = false;
  414. }, 300);
  415. },
  416. // 确定按钮
  417. onConfirm() {},
  418. //确定按钮防抖
  419. createConfirm() {
  420. return debounce(() => {
  421. // #ifdef H5 || APP-PLUS
  422. this.confirmH5();
  423. // #endif
  424. // #ifdef MP-WEIXIN || MP-ALIPAY
  425. this.confirmWx();
  426. // #endif
  427. }, this.delay);
  428. },
  429. async confirmH5() {
  430. let that = this;
  431. let mx = this.computeMatrix(); //获取画布的信息
  432. // 设备画布的宽高
  433. this.origin.canvasWidth = mx.tw;
  434. this.origin.canvasHeight = mx.th;
  435. uni.showLoading({
  436. title: "处理中",
  437. });
  438. // 设置宽高后等待更新完成在获取Canvas节点
  439. await new Promise((resolve) => {
  440. setTimeout(() => {
  441. resolve();
  442. }, 200);
  443. });
  444. // 创建一个画布
  445. let ctx = uni.createCanvasContext(this.canvasId, this);
  446. // 矩形内清除指定的像素
  447. await ctx.clearRect(0, 0, mx.tw, mx.th);
  448. //剪切圆角
  449. await this.drawClipImage(ctx, mx);
  450. // 旋转
  451. ctx.rotate((this.origin.rotate * Math.PI) / 180);
  452. //绘制图片
  453. ctx.drawImage(
  454. that.imageUrl,
  455. mx.sx,
  456. mx.sy,
  457. mx.sw,
  458. mx.sh,
  459. mx.dx,
  460. mx.dy,
  461. mx.dw,
  462. mx.dh
  463. );
  464. ctx.restore();
  465. ctx.draw(false, () => {
  466. uni.canvasToTempFilePath(
  467. {
  468. canvasId: that.canvasId,
  469. // #ifdef H5
  470. destWidth: that.isCutSize ? mx.tw : that.origin.frame.width,
  471. destHeight: that.isCutSize ? mx.th : that.origin.frame.height,
  472. // #endif
  473. success: (res) => {
  474. var path = res.tempFilePath;
  475. // #ifdef H5
  476. path = that.parseBlob(path);
  477. that.$emit("confirm", {
  478. errMsg: "parseBlob:ok",
  479. tempFilePath: path,
  480. });
  481. // #endif
  482. // #ifdef APP-PLUS
  483. that.$emit("confirm", res);
  484. // #endif
  485. },
  486. fail: (err) => {
  487. console.log(err);
  488. },
  489. complete: () => {
  490. uni.hideLoading(); //关闭loading
  491. },
  492. },
  493. that
  494. );
  495. });
  496. },
  497. confirmWx() {
  498. uni.showLoading({
  499. title: "处理中",
  500. });
  501. let mx = this.computeMatrix(); //获取画布的信息
  502. uni
  503. .createSelectorQuery()
  504. // #ifndef MP-ALIPAY
  505. .in(this)
  506. // #endif
  507. .select("#canvas-cropper")
  508. .fields({
  509. node: true,
  510. size: true,
  511. })
  512. .exec(async (res) => {
  513. const textCanvas = res[0].node;
  514. const ctx = textCanvas.getContext("2d");
  515. // 初始化画布大小
  516. textCanvas.width = mx.tw;
  517. textCanvas.height = mx.th;
  518. // //绘制canvas
  519. await this.drawClipImage(ctx, mx); //剪切圆角
  520. await ctx.rotate((this.origin.rotate * Math.PI) / 180);
  521. await this.createImage(this.origin.imageUrl, mx, ctx, textCanvas);
  522. // #ifdef MP-WEIXIN
  523. wx.canvasToTempFilePath({
  524. canvas: textCanvas,
  525. canvasId: "canvasID",
  526. success: (res) => {
  527. this.$emit("confirm", res);
  528. },
  529. fail: (err) => {
  530. console.log(err);
  531. },
  532. });
  533. // #endif
  534. // #ifdef MP-ALIPAY
  535. textCanvas.toTempFilePath({
  536. success: (res) => {
  537. this.$emit("confirm", res);
  538. },
  539. fail: (err) => {
  540. console.log(err);
  541. },
  542. });
  543. // #endif
  544. uni.hideLoading(); //关闭loading
  545. });
  546. },
  547. //剪切图片圆角
  548. drawClipImage(ctx, mx) {
  549. let { radius } = this;
  550. if (radius > 0) {
  551. const w = Math.round(mx.tw); //画布宽度
  552. const h = Math.round(mx.th); //画布高度
  553. // 被缩放后的尺寸且裁剪的宽高相同
  554. if (this.width === this.height && w == h && this.width < w) {
  555. radius = (w / this.width) * radius;
  556. }
  557. // 被缩放后的尺寸且裁剪的宽高不相同
  558. if (this.width != this.height && w != h && this.width < w) {
  559. if (w > h) {
  560. radius = (w / h) * radius;
  561. } else {
  562. radius = (h / w) * radius;
  563. }
  564. }
  565. // 如果裁剪是圆形
  566. if (w === h && radius >= w / 2) {
  567. // 圆形
  568. ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
  569. } else {
  570. // 圆角矩形
  571. if (w !== h) {
  572. // 限制圆角半径不能超过短边的一半
  573. radius = Math.min(w / 2, h / 2, radius);
  574. }
  575. // 其他形状
  576. ctx.moveTo(radius, 0);
  577. ctx.arcTo(w, 0, w, h, radius);
  578. ctx.arcTo(w, h, 0, h, radius);
  579. ctx.arcTo(0, h, 0, 0, radius);
  580. ctx.arcTo(0, 0, w, 0, radius);
  581. ctx.closePath();
  582. }
  583. ctx.clip(); //从原始画布剪切任意形状和尺寸的区域
  584. ctx.restore(); //返回之前保存过的路径状态和属性
  585. }
  586. },
  587. // 创建一个图片
  588. createImage(img, mx, ctx, textCanvas) {
  589. return new Promise((resolve) => {
  590. const headerImg = textCanvas.createImage();
  591. headerImg.src = img;
  592. headerImg.onload = () => {
  593. let result = ctx.drawImage(
  594. headerImg,
  595. mx.sx,
  596. mx.sy,
  597. mx.sw,
  598. mx.sh,
  599. mx.dx,
  600. mx.dy,
  601. mx.dw,
  602. mx.dh
  603. );
  604. resolve(result);
  605. };
  606. });
  607. },
  608. // 计算矩阵
  609. computeMatrix() {
  610. let width = this.width;
  611. let height = this.height;
  612. let mul = this.origin.bgImage.width / this.origin.real.width;
  613. if (this.origin.rotate % 180 != 0) {
  614. mul = this.origin.bgImage.height / this.origin.real.width;
  615. }
  616. if (this.mode != "fixed") {
  617. width = this.origin.frame.width / mul;
  618. height = this.origin.frame.height / mul;
  619. }
  620. var rate = width / height;
  621. if (width > this.maxWidth) {
  622. width = this.maxWidth;
  623. height = width / rate;
  624. }
  625. if (height > this.maxHeight) {
  626. height = this.maxHeight;
  627. width = height * rate;
  628. }
  629. var sx = (this.origin.frame.left - this.origin.bgImage.left) / mul;
  630. var sy = (this.origin.frame.top - this.origin.bgImage.top) / mul;
  631. var sw = this.origin.frame.width / mul;
  632. var sh = this.origin.frame.height / mul;
  633. var ox = sx + sw / 2;
  634. var oy = sy + sh / 2;
  635. if (this.origin.rotate % 180 != 0) {
  636. var temp = sw;
  637. sw = sh;
  638. sh = temp;
  639. }
  640. var angle = this.origin.rotate % 360;
  641. if (angle < 0) {
  642. angle += 360;
  643. }
  644. if (angle == 270) {
  645. var x = this.origin.real.width - oy;
  646. var y = ox;
  647. ox = x;
  648. oy = y;
  649. }
  650. if (angle == 180) {
  651. var x = this.origin.real.width - ox;
  652. var y = this.origin.real.height - oy;
  653. ox = x;
  654. oy = y;
  655. }
  656. if (angle == 90) {
  657. var x = oy;
  658. var y = this.origin.real.height - ox;
  659. ox = x;
  660. oy = y;
  661. }
  662. sx = ox - sw / 2;
  663. sy = oy - sh / 2;
  664. let dr = { x: 0, y: 0, w: width, h: height };
  665. dr = this.parseRect(dr, -this.origin.rotate);
  666. return {
  667. tw: width,
  668. th: height,
  669. sx: sx,
  670. sy: sy,
  671. sw: sw,
  672. sh: sh,
  673. dx: dr.x,
  674. dy: dr.y,
  675. dw: dr.w,
  676. dh: dr.h,
  677. };
  678. },
  679. //计算矩阵
  680. parseRect(rect, angle) {
  681. let x1 = rect.x;
  682. let y1 = rect.y;
  683. let x2 = rect.x + rect.w;
  684. let y2 = rect.y + rect.h;
  685. let p1 = this.parsePoint({ x: x1, y: y1 }, angle);
  686. let p2 = this.parsePoint({ x: x2, y: y2 }, angle);
  687. let result = {};
  688. result.x = Math.min(p1.x, p2.x);
  689. result.y = Math.min(p1.y, p2.y);
  690. result.w = Math.abs(p2.x - p1.x);
  691. result.h = Math.abs(p2.y - p1.y);
  692. return result;
  693. },
  694. //计算x、y
  695. parsePoint(point, angle) {
  696. var result = {};
  697. result.x =
  698. point.x * Math.cos((angle * Math.PI) / 180) -
  699. point.y * Math.sin((angle * Math.PI) / 180);
  700. result.y =
  701. point.y * Math.cos((angle * Math.PI) / 180) +
  702. point.x * Math.sin((angle * Math.PI) / 180);
  703. return result;
  704. },
  705. //base64转blob
  706. parseBlob(base64) {
  707. var arr = base64.split(",");
  708. var mime = arr[0].match(/:(.*?);/)[1];
  709. var bstr = atob(arr[1]);
  710. var n = bstr.length;
  711. var u8arr = new Uint8Array(n);
  712. for (var i = 0; i < n; i++) {
  713. u8arr[i] = bstr.charCodeAt(i);
  714. }
  715. var url = URL || webkitURL;
  716. return url.createObjectURL(new Blob([u8arr], { type: mime }));
  717. },
  718. touchStart(e, touchType = "") {
  719. // #ifdef APP-PLUS || H5
  720. if (e.preventDefault) {
  721. e.preventDefault();
  722. }
  723. if (e.stopPropagation) {
  724. e.stopPropagation();
  725. }
  726. // #endif
  727. switch (touchType) {
  728. case "body":
  729. this.move.touchType = "body";
  730. break;
  731. case "left-top":
  732. this.move.touchType = "left-top";
  733. break;
  734. case "left-bottom":
  735. this.move.touchType = "left-bottom";
  736. break;
  737. case "right-top":
  738. this.move.touchType = "right-top";
  739. break;
  740. case "right-bottom":
  741. this.move.touchType = "right-bottom";
  742. break;
  743. default:
  744. this.move.touchType = "body";
  745. break;
  746. }
  747. this.touch.touchStart = e.touches;
  748. // 裁剪框的开始位置
  749. this.move.start.frame.left = this.origin.frame.left;
  750. this.move.start.frame.top = this.origin.frame.top;
  751. this.move.start.frame.width = this.origin.frame.width;
  752. this.move.start.frame.height = this.origin.frame.height;
  753. // 背景图片的开始位置
  754. this.move.start.bgImage.left = this.origin.bgImage.left;
  755. this.move.start.bgImage.top = this.origin.bgImage.top;
  756. this.move.start.bgImage.width = this.origin.bgImage.width;
  757. this.move.start.bgImage.height = this.origin.bgImage.height;
  758. return false;
  759. },
  760. touchMove(e) {
  761. // #ifdef APP-PLUS || H5
  762. if (e.preventDefault) {
  763. e.preventDefault();
  764. }
  765. if (e.stopPropagation) {
  766. e.stopPropagation();
  767. }
  768. // #endif
  769. // 单手操作
  770. if (this.touch.touchStart.length == 1) {
  771. if (this.move.touchType == "body") {
  772. // 移动图片
  773. this.moveImage(this.touch.touchStart[0], e.touches[0]);
  774. } else {
  775. if (this.mode != "fixed") {
  776. // 放大缩小图片
  777. this.scaleFrame(this.touch.touchStart[0], e.touches[0]);
  778. }
  779. }
  780. } else if (this.touch.touchStart.length == 2 && e.touches.length == 2) {
  781. var ta = this.touch.touchStart[0];
  782. var tb = this.touch.touchStart[1];
  783. var tc = e.touches[0];
  784. var td = e.touches[1];
  785. if (ta.identifier != tc.identifier) {
  786. var temp = tc;
  787. tc = td;
  788. td = temp;
  789. }
  790. this.scaleImage(ta, tb, tc, td);
  791. }
  792. },
  793. // 移动图片
  794. moveImage(ta, tb) {
  795. let mx = tb.clientX - ta.clientX;
  796. let my = tb.clientY - ta.clientY;
  797. // 开始移动图片背景 + 移动的左边距离
  798. this.origin.bgImage.left = this.move.start.bgImage.left + mx;
  799. this.origin.bgImage.top = this.move.start.bgImage.top + my;
  800. let frameLeft = this.origin.frame.left;
  801. let frameTop = this.origin.frame.top;
  802. let frameWidth = this.origin.frame.width;
  803. let frameHeight = this.origin.frame.height;
  804. // 左边触边
  805. if (this.origin.bgImage.left > frameLeft) {
  806. this.origin.bgImage.left = frameLeft;
  807. }
  808. // 头部触边
  809. if (this.origin.bgImage.top > frameTop) {
  810. this.origin.bgImage.top = frameTop;
  811. }
  812. // 右边触边
  813. if (this.origin.bgImage.left + this.origin.bgImage.width < frameLeft + frameWidth) {
  814. this.origin.bgImage.left = frameLeft + frameWidth - this.origin.bgImage.width;
  815. }
  816. // 底部触边:背景距离头部+背景的高度 < 背景高度 + 矩形框顶部
  817. if (this.origin.bgImage.top + this.origin.bgImage.height < frameTop + frameHeight) {
  818. this.origin.bgImage.top = frameTop + frameHeight - this.origin.bgImage.height;
  819. }
  820. },
  821. // 缩放裁剪框
  822. scaleFrame(ta, tb) {
  823. let mx = tb.clientX - ta.clientX;
  824. let my = tb.clientY - ta.clientY;
  825. let x1 = this.move.start.frame.left;
  826. let y1 = this.move.start.frame.top;
  827. let x2 = this.move.start.frame.left + this.move.start.frame.width;
  828. let y2 = this.move.start.frame.top + this.move.start.frame.height;
  829. let cx1 = false;
  830. let cy1 = false;
  831. let cx2 = false;
  832. let cy2 = false;
  833. let mix = this.frameSize;
  834. let rate = this.origin.frame.width / this.origin.frame.height;
  835. const { width: fWidth, height: fHeight } = this.origin.frame;
  836. const { width: bgWidth, height: bgHeight } = this.origin.bgImage;
  837. const { width: realWidth, height: realHeight } = this.origin.real;
  838. //左上角
  839. if (this.move.touchType == "left-top") {
  840. x1 += mx;
  841. y1 += my;
  842. cx1 = true;
  843. cy1 = true;
  844. // 左下角
  845. } else if (this.move.touchType == "left-bottom") {
  846. x1 += mx;
  847. y2 += my;
  848. cx1 = true;
  849. cy2 = true;
  850. // 右上角
  851. } else if (this.move.touchType == "right-top") {
  852. x2 += mx;
  853. y1 += my;
  854. cx2 = true;
  855. cy1 = true;
  856. // 右下角
  857. } else if (this.move.touchType == "right-bottom") {
  858. x2 += mx;
  859. y2 += my;
  860. cx2 = true;
  861. cy2 = true;
  862. }
  863. if (x1 < this.origin.bgImage.left) {
  864. x1 = this.origin.bgImage.left;
  865. }
  866. if (y1 < this.origin.bgImage.top) {
  867. y1 = this.origin.bgImage.top;
  868. }
  869. if (x2 > this.origin.bgImage.left + this.origin.bgImage.width) {
  870. x2 = this.origin.bgImage.left + this.origin.bgImage.width;
  871. }
  872. if (y2 > this.origin.bgImage.top + this.origin.bgImage.height) {
  873. y2 = this.origin.bgImage.top + this.origin.bgImage.height;
  874. }
  875. if (cx1) {
  876. if (x1 > x2 - mix) {
  877. x1 = x2 - mix;
  878. }
  879. }
  880. if (cy1) {
  881. if (y1 > y2 - mix) {
  882. y1 = y2 - mix;
  883. }
  884. }
  885. if (cx2) {
  886. if (x2 < x1 + mix) {
  887. x2 = x1 + mix;
  888. }
  889. }
  890. if (cy2) {
  891. if (y2 < y1 + mix) {
  892. y2 = y1 + mix;
  893. }
  894. }
  895. if (cx1) {
  896. if (this.mode != "free") {
  897. var val = x2 - rate * (y2 - y1);
  898. if (x1 < val) {
  899. x1 = val;
  900. }
  901. }
  902. }
  903. if (cy1) {
  904. if (this.mode != "free") {
  905. var val = y2 - (x2 - x1) / rate;
  906. if (y1 < val) {
  907. y1 = val;
  908. }
  909. }
  910. }
  911. if (cx2) {
  912. if (this.mode != "free") {
  913. var val = rate * (y2 - y1) + x1;
  914. if (x2 > val) {
  915. x2 = val;
  916. }
  917. }
  918. }
  919. if (cy2) {
  920. if (this.mode != "free") {
  921. var val = (x2 - x1) / rate + y1;
  922. if (y2 > val) {
  923. y2 = val;
  924. }
  925. }
  926. }
  927. // 裁剪框缩放限制
  928. const owFrame = x2 - x1;
  929. const ohFrame = y2 - y1;
  930. if (owFrame < fWidth || ohFrame < fHeight) {
  931. const currentRatio = this.scaleRatio * this.maxRatio;
  932. const isScale =
  933. bgWidth * currentRatio >= realWidth / this.scaleRatio &&
  934. bgHeight * currentRatio >= realHeight / this.scaleRatio;
  935. if (isScale) {
  936. return false;
  937. }
  938. }
  939. this.origin.frame.left = x1;
  940. this.origin.frame.top = y1;
  941. this.origin.frame.width = x2 - x1;
  942. this.origin.frame.height = y2 - y1;
  943. },
  944. scaleImage(ta, tb, tc, td) {
  945. let x1 = ta.clientX;
  946. let y1 = ta.clientY;
  947. let x2 = tb.clientX;
  948. let y2 = tb.clientY;
  949. let x3 = tc.clientX;
  950. let y3 = tc.clientY;
  951. let x4 = td.clientX;
  952. let y4 = td.clientY;
  953. let ol = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
  954. let el = Math.sqrt((x3 - x4) * (x3 - x4) + (y3 - y4) * (y3 - y4));
  955. let ocx = (x1 + x2) / 2;
  956. let ocy = (y1 + y2) / 2;
  957. let ecx = (x3 + x4) / 2;
  958. let ecy = (y3 + y4) / 2;
  959. let ax = ecx - ocx;
  960. let ay = ecy - ocy;
  961. let scale = el / ol;
  962. // 检测放大/缩小倍率
  963. if (this.move.start.bgImage.width * scale < this.origin.frame.width) {
  964. scale = this.origin.frame.width / this.move.start.bgImage.width;
  965. }
  966. if (this.move.start.bgImage.height * scale < this.origin.frame.height) {
  967. scale = this.origin.frame.height / this.move.start.bgImage.height;
  968. }
  969. // 最大缩放
  970. if (
  971. this.move.start.bgImage.width * scale >
  972. this.origin.frame.width * this.maxRatio
  973. ) {
  974. return;
  975. }
  976. this.origin.bgImage.left =
  977. this.move.start.bgImage.left +
  978. ax -
  979. (ocx - this.move.start.bgImage.left) * (scale - 1);
  980. this.origin.bgImage.top =
  981. this.move.start.bgImage.top +
  982. ay -
  983. (ocy - this.move.start.bgImage.top) * (scale - 1);
  984. this.origin.bgImage.width = this.move.start.bgImage.width * scale;
  985. this.origin.bgImage.height = this.move.start.bgImage.height * scale;
  986. if (this.origin.bgImage.left > this.origin.frame.left) {
  987. this.origin.bgImage.left = this.origin.frame.left;
  988. }
  989. if (this.origin.bgImage.top > this.origin.frame.top) {
  990. this.origin.bgImage.top = this.origin.frame.top;
  991. }
  992. if (
  993. this.origin.bgImage.left + this.origin.bgImage.width <
  994. this.origin.frame.left + this.origin.frame.width
  995. ) {
  996. this.origin.bgImage.left =
  997. this.origin.frame.left + this.origin.frame.width - this.origin.bgImage.width;
  998. }
  999. if (
  1000. this.origin.bgImage.top + this.origin.bgImage.height <
  1001. this.origin.frame.top + this.origin.frame.height
  1002. ) {
  1003. this.origin.bgImage.top =
  1004. this.origin.frame.top + this.origin.frame.height - this.origin.bgImage.height;
  1005. }
  1006. },
  1007. // 手指触摸动作结束
  1008. touchEnd() {
  1009. this.touch.touchStart = [];
  1010. this.resizeImage(); //调整图片的大小
  1011. },
  1012. // 手指触摸动作被打断,如来电提醒,弹窗
  1013. touchCancel() {
  1014. this.touch.touchStart = [];
  1015. },
  1016. // 调整图片的大小
  1017. resizeImage() {
  1018. var rate = this.origin.frame.width / this.origin.frame.height; //比列
  1019. var width = this.origin.body.width * this.scaleRatio; //裁剪框图片高度 * 缩小0.7倍
  1020. var height = this.origin.body.height * this.scaleRatio; //裁剪框图片宽度 * 缩小0.7倍
  1021. // 图片的位置:
  1022. // 缩放后的宽度/高度 > 组件裁剪的比例,就要对宽度重写
  1023. if (width / height > rate) {
  1024. width = height * rate;
  1025. } else {
  1026. height = width / rate;
  1027. }
  1028. var left = (this.origin.body.width - width) / 2;
  1029. var top = (this.origin.body.height - height) / 2;
  1030. var mul = width / this.origin.frame.width;
  1031. var ox = this.origin.frame.left - this.origin.bgImage.left;
  1032. var oy = this.origin.frame.top - this.origin.bgImage.top;
  1033. this.origin.frame = {
  1034. left: left,
  1035. top: top,
  1036. width: width,
  1037. height: height,
  1038. };
  1039. width = this.origin.bgImage.width * mul;
  1040. height = this.origin.bgImage.height * mul;
  1041. left = this.origin.frame.left - ox * mul;
  1042. top = this.origin.frame.top - oy * mul;
  1043. this.origin.bgImage = {
  1044. left: left,
  1045. top: top,
  1046. width: width,
  1047. height: height,
  1048. };
  1049. if (mul != 1) {
  1050. this.origin.transit = true;
  1051. setTimeout(() => {
  1052. this.origin.transit = false;
  1053. }, 300);
  1054. }
  1055. },
  1056. },
  1057. };
  1058. </script>
  1059. <style>
  1060. /* page {
  1061. overflow: hidden;
  1062. overscroll-behavior: none;
  1063. } */
  1064. </style>
  1065. <style lang="scss" scoped>
  1066. @import "./tt-cropper";
  1067. </style>