cl-cropper.vue 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <template>
  2. <view class="cl-cropper__wrap" v-if="visible" @touchmove.stop.prevent catchtouchmove="false">
  3. <view class="cl-cropper">
  4. <view class="cl-cropper__container">
  5. <!-- 原图片 -->
  6. <view
  7. class="cl-cropper__image"
  8. :class="[filterBlur ? 'is-filter-blur' : '']"
  9. :style="[imageSize, imageTransform]"
  10. @touchstart.stop.prevent="onTouchStart"
  11. @touchmove.stop.prevent="onTouchMove"
  12. @touchend.stop.prevent="onTouchEnd"
  13. >
  14. <image :src="image.url" />
  15. </view>
  16. <!-- 选择框 -->
  17. <view
  18. class="cl-cropper__view"
  19. :class="[round ? 'is-round' : '']"
  20. :style="[cropStyle]"
  21. >
  22. <image :src="image.url" :style="[imageSize, cropTransform]" />
  23. </view>
  24. <!-- 工具 -->
  25. <view class="cl-cropper__tools">
  26. <!-- 选择图片 -->
  27. <view class="cl-cropper__tools-item" @tap="chooseImage">
  28. <cl-icon name="image" :size="30"></cl-icon>
  29. <text>选择图片</text>
  30. </view>
  31. <!-- 旋转 -->
  32. <view class="cl-cropper__tools-item" @tap="rotateImage">
  33. <cl-icon name="refresh" :size="30"></cl-icon>
  34. <text>旋转</text>
  35. </view>
  36. </view>
  37. <!-- 遮罩层 -->
  38. <view class="cl-cropper__mask"></view>
  39. </view>
  40. <view class="cl-cropper__footer">
  41. <button @tap="cancel">取消</button>
  42. <button @tap="confirm">确认</button>
  43. </view>
  44. </view>
  45. <!-- Canvas -->
  46. <canvas
  47. id="canvas"
  48. canvas-id="canvas"
  49. class="cl-cropper__canvas"
  50. :style="[cropStyle]"
  51. ></canvas>
  52. <!-- Loading -->
  53. <cl-loading-mask fullscreen :loading="loading"></cl-loading-mask>
  54. <!-- Toast -->
  55. <cl-toast ref="toast"></cl-toast>
  56. </view>
  57. </template>
  58. <script>
  59. const { windowWidth, windowHeight, pixelRatio } = uni.getSystemInfoSync();
  60. /**
  61. * cropper 图片裁剪
  62. * @description 支持圆形,方形,背景高斯模糊
  63. * @tutorial https://docs.cool-js.com/uni/components/advanced/cropper.html
  64. * @property {String} url 图片地址
  65. * @property {Number} imageWidth 图片宽度,默认320
  66. * @property {Number} cropHeight 裁剪高度,默认200
  67. * @property {Number} cropWidth 裁剪宽度,默认200
  68. * @property {String} backgroundColor 底色
  69. * @property {Boolean} round 是否圆形
  70. * @property {Boolean} filterBlur 高斯模糊
  71. * @event {Function} success 裁剪成功时触发
  72. * @event {Function} error 裁剪失败时触发
  73. * @example 见教程
  74. */
  75. export default {
  76. name: "cl-cropper",
  77. props: {
  78. // 图片地址
  79. url: String,
  80. // 图片宽度
  81. imageWidth: {
  82. type: Number,
  83. default: 320
  84. },
  85. // 裁剪高度
  86. cropHeight: {
  87. type: Number,
  88. default: 200
  89. },
  90. // 裁剪宽度
  91. cropWidth: {
  92. type: Number,
  93. default: 200
  94. },
  95. // 底色
  96. backgroundColor: String,
  97. // 是否圆形
  98. round: Boolean,
  99. // 高斯模糊
  100. filterBlur: Boolean
  101. },
  102. data() {
  103. return {
  104. visible: false,
  105. loading: false,
  106. lock: false,
  107. image: {
  108. height: "",
  109. width: "",
  110. left: 0,
  111. top: 0,
  112. scale: 1,
  113. rotate: 0,
  114. url: ""
  115. },
  116. crop: {
  117. height: this.cropHeight,
  118. width: this.cropWidth,
  119. left: 0,
  120. top: 0
  121. },
  122. start: {
  123. x: 0,
  124. y: 0,
  125. hyp: 0,
  126. scale: 1
  127. }
  128. };
  129. },
  130. computed: {
  131. imageSize() {
  132. return {
  133. height: `${this.image.height}px`,
  134. width: `${this.image.width}px`
  135. };
  136. },
  137. cropStyle() {
  138. return {
  139. height: `${this.crop.height}px`,
  140. width: `${this.crop.width}px`,
  141. backgroundColor: this.backgroundColor
  142. };
  143. },
  144. imageTransform() {
  145. let { scale, rotate, left, top } = this.image;
  146. return {
  147. transform: `scale(${scale}, ${scale}) translate3d(${left / scale}px, ${top /
  148. scale}px, 0) rotateZ(${rotate}deg)`
  149. };
  150. },
  151. cropTransform() {
  152. let { scale, rotate, left, top } = this.image;
  153. return {
  154. transform: `scale(${scale}, ${scale}) translate(${(left - this.crop.left) /
  155. scale}px, ${(top - this.crop.top) / scale}px) rotateZ(${rotate}deg)`
  156. };
  157. }
  158. },
  159. methods: {
  160. open(options) {
  161. const { url } = options || {};
  162. this.visible = true;
  163. this.$nextTick(() => {
  164. if (url || this.url) {
  165. this.setImage(url || this.url);
  166. }
  167. });
  168. },
  169. close() {
  170. this.visible = false;
  171. this.loading = false;
  172. this.image.rotate = 0;
  173. this.image.scale = 1;
  174. this.image.url = "";
  175. },
  176. setImage(url) {
  177. this.image.url = url;
  178. uni.getImageInfo({
  179. src: this.image.url,
  180. success: res => {
  181. // 图片大小位置
  182. this.image.width = this.imageWidth * this.image.scale;
  183. this.image.height = (this.image.width / res.width) * res.height;
  184. this.image.left = (windowWidth - this.image.width) / 2;
  185. this.image.top = (windowHeight - this.image.height) / 2;
  186. // 裁剪框的位置
  187. this.crop.left = (windowWidth - this.crop.width) / 2;
  188. this.crop.top = (windowHeight - this.crop.height) / 2;
  189. }
  190. });
  191. },
  192. chooseImage() {
  193. uni.chooseImage({
  194. success: res => {
  195. this.image.rotate = 0;
  196. this.image.scale = 1;
  197. this.setImage(res.tempFilePaths[0]);
  198. }
  199. });
  200. },
  201. rotateImage() {
  202. this.image.rotate += 90;
  203. if (this.image.rotate >= 360) {
  204. this.image.rotate = 0;
  205. }
  206. },
  207. confirm() {
  208. const ctx = uni.createCanvasContext("canvas", this);
  209. // 图片信息
  210. let { url, height, width, scale, rotate, left, top } = this.image;
  211. if (!url) {
  212. return this.$refs["toast"].open("请先选择图片");
  213. }
  214. // 加载动画
  215. this.loading = true;
  216. // 画布大小
  217. let h = height * scale;
  218. let w = width * scale;
  219. // 图片位置
  220. let x = this.crop.left - left - (width - w) / 2;
  221. let y = this.crop.top - top - (height - h) / 2;
  222. // 填充底色
  223. if (this.backgroundColor) {
  224. ctx.setFillStyle(this.backgroundColor);
  225. }
  226. // 旋转
  227. ctx.rotate((rotate * Math.PI) / 180);
  228. // 设置大小
  229. ctx.fillRect(0, 0, w, h);
  230. ctx.save();
  231. // 画圆
  232. if (this.round) {
  233. switch (rotate) {
  234. case 90:
  235. ctx.arc(100, -100, 100, 0, 2 * Math.PI);
  236. break;
  237. case 180:
  238. ctx.arc(-100, -100, 100, 0, 2 * Math.PI);
  239. break;
  240. case 270:
  241. ctx.arc(-100, 100, 100, 0, 2 * Math.PI);
  242. break;
  243. default:
  244. ctx.arc(100, 100, 100, 0, 2 * Math.PI);
  245. break;
  246. }
  247. ctx.arc(100, 100, 100, 0, 2 * Math.PI);
  248. ctx.fill();
  249. ctx.clip();
  250. }
  251. // 绘图
  252. switch (rotate) {
  253. case 90:
  254. x += (h - w) / 2;
  255. y -= (h - w) / 2;
  256. ctx.drawImage(url, -y, x, w, -h);
  257. break;
  258. case 180:
  259. ctx.drawImage(url, x, y, -w, -h);
  260. break;
  261. case 270:
  262. x += (h - w) / 2;
  263. y -= (h - w) / 2;
  264. ctx.drawImage(url, y, -x, -w, h);
  265. break;
  266. default:
  267. ctx.drawImage(url, -x, -y, w, h);
  268. break;
  269. }
  270. ctx.restore();
  271. ctx.draw(false, () => {
  272. // 导出base64
  273. uni.canvasToTempFilePath(
  274. {
  275. canvasId: "canvas",
  276. destWidth: this.crop.width * pixelRatio,
  277. destHeight: this.crop.height * pixelRatio,
  278. success: res => {
  279. this.$emit("success", res.tempFilePath);
  280. this.close();
  281. },
  282. fail: e => {
  283. this.loading = false;
  284. this.$refs.toast.open(e.errMsg);
  285. this.$emit("fail", e.errMsg);
  286. }
  287. },
  288. this
  289. );
  290. });
  291. },
  292. cancel() {
  293. this.close();
  294. },
  295. // 缩放偏移
  296. getScaleOffset() {
  297. let { rotate, scale, width, height } = this.image;
  298. let d = null;
  299. let x = 0;
  300. let y = 0;
  301. if (rotate == 90 || rotate == 270) {
  302. let r = ((height - width) / 2) * scale + (scale - 1) * 60;
  303. x = r;
  304. y = -r;
  305. } else {
  306. x = ((scale - 1) * width) / 2;
  307. y = ((scale - 1) * height) / 2;
  308. }
  309. return {
  310. x,
  311. y
  312. };
  313. },
  314. onTouchStart(e) {
  315. // 加锁
  316. this.lock = true;
  317. // 双指放大缩小
  318. if (e.touches.length >= 2) {
  319. let x = e.touches[0].pageX - e.touches[1].pageX;
  320. let y = e.touches[0].pageY - e.touches[1].pageY;
  321. // 比例
  322. this.start.scale = this.image.scale;
  323. // 斜边
  324. this.start.hyp = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
  325. }
  326. // 单指拖动
  327. else {
  328. this.start.x = e.touches[0].pageX - this.image.left;
  329. this.start.y = e.touches[0].pageY - this.image.top;
  330. }
  331. },
  332. onTouchMove(e) {
  333. if (this.lock) {
  334. // 双指放大缩小
  335. if (e.touches.length >= 2) {
  336. // 计算双指位置
  337. let x = e.touches[0].pageX - e.touches[1].pageX;
  338. let y = e.touches[0].pageY - e.touches[1].pageY;
  339. // 斜边
  340. let hyp = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
  341. // 比例
  342. let scale = this.start.scale + (hyp - this.start.hyp) / 100;
  343. // 最小
  344. if (scale < 0.3) {
  345. scale = 0.3;
  346. }
  347. // 最大
  348. if (scale > 4) {
  349. scale = 4;
  350. }
  351. // 设置比例
  352. this.image.scale = scale;
  353. }
  354. // 单指拖动
  355. else {
  356. // 移动位置
  357. let x = e.touches[0].pageX - this.start.x;
  358. let y = e.touches[0].pageY - this.start.y;
  359. // 设置位置
  360. this.image.left = x;
  361. this.image.top = y;
  362. }
  363. }
  364. },
  365. onTouchEnd(e) {
  366. this.lock = false;
  367. }
  368. }
  369. };
  370. </script>