imageCropper.vue 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. <template>
  2. <div>
  3. <div
  4. class="vue-crop-layout"
  5. ref="layoutRef"
  6. :style="{ width: sWidth + 'px', height: sHeight + 'px' }"
  7. >
  8. <VueCropper
  9. class="cropper-cls"
  10. ref="cropperRef"
  11. :img="url"
  12. :outputSize="1"
  13. canScale
  14. autoCrop
  15. centerBox
  16. :fixed="!!fixed"
  17. :fixedNumber="fixed"
  18. />
  19. </div>
  20. <div class="control">
  21. <el-button type="primary" @click="cropperRef.rotateRight()"> 旋转 </el-button>
  22. </div>
  23. </div>
  24. </template>
  25. <script setup lang="ts">
  26. import "vue-cropper/dist/index.css";
  27. import { ref, computed } from "vue";
  28. import { VueCropper } from "vue-cropper";
  29. import { QuiskExpose } from "@/helper/mount";
  30. import { getDomMatrix } from "@/util";
  31. import { inverse, multiply, positionTransform, rotateZ, translate } from "@/util/mt4";
  32. type CropperProps = {
  33. img: Blob | string;
  34. fixed: [number, number];
  35. };
  36. const props = defineProps<CropperProps>();
  37. // 样式控制
  38. const sWidth = 500;
  39. const sHeight = (props.fixed[1] / props.fixed[0]) * sWidth;
  40. const realImage = new Image();
  41. const url = computed(() => {
  42. const url = typeof props.img === "string" ? props.img : URL.createObjectURL(props.img);
  43. realImage.src = url;
  44. return url;
  45. });
  46. const layoutRef = ref<HTMLDivElement>();
  47. const cropperRef = ref<any>();
  48. const getDrawInfo = () => {
  49. const imgDom = layoutRef.value?.querySelector(".cropper-box-canvas") as HTMLElement;
  50. const cropDom = layoutRef.value?.querySelector(".cropper-crop-box") as HTMLElement;
  51. const imgMatrix = getDomMatrix(imgDom);
  52. const cropMatrix = getDomMatrix(cropDom);
  53. const cropSize = [cropperRef.value.cropW, cropperRef.value.cropH];
  54. // 屏幕位置
  55. const cropBox = [
  56. positionTransform([-cropSize[0] / 2, -cropSize[1] / 2, 0], cropMatrix),
  57. positionTransform([cropSize[0] / 2, cropSize[1] / 2, 0], cropMatrix),
  58. ];
  59. const scale = [
  60. realImage.width / imgDom.offsetWidth,
  61. realImage.height / imgDom.offsetHeight,
  62. ];
  63. const invImageMatrix = inverse(imgMatrix);
  64. const lt = positionTransform(cropBox[0], invImageMatrix);
  65. const rb = positionTransform(cropBox[1], invImageMatrix);
  66. const imgBound = [
  67. lt[0] * scale[0] + realImage.width / 2,
  68. lt[1] * scale[1] + realImage.height / 2,
  69. rb[0] * scale[0] + realImage.width / 2,
  70. rb[1] * scale[1] + realImage.height / 2,
  71. ];
  72. const realBound = {
  73. left: Math.round(imgBound[0]),
  74. top: Math.round(imgBound[1]),
  75. right: Math.round(imgBound[2]),
  76. bottom: Math.round(imgBound[3]),
  77. };
  78. // 旋转过
  79. if (realBound.left > realBound.right) {
  80. [realBound.left, realBound.right] = [realBound.right, realBound.left];
  81. }
  82. if (realBound.top > realBound.bottom) {
  83. [realBound.top, realBound.bottom] = [realBound.bottom, realBound.top];
  84. }
  85. return {
  86. ...realBound,
  87. rotate: (cropperRef.value.rotate * Math.PI) / 2,
  88. };
  89. };
  90. const clipImage = () => {
  91. const data = getDrawInfo();
  92. const canvas = document.createElement("canvas");
  93. const ctx = canvas.getContext("2d")!;
  94. const w = data.right - data.left;
  95. const h = data.bottom - data.top;
  96. const boxMatrix = multiply(
  97. translate(-w / 2, -h / 2, 0),
  98. rotateZ(data.rotate),
  99. translate(w / 2, h / 2, 0)
  100. );
  101. const start = positionTransform([0, 0, 0], boxMatrix);
  102. const end = positionTransform([w, h, 0], boxMatrix);
  103. const cw = (canvas.width = Math.abs(end[0] - start[0]));
  104. const ch = (canvas.height = Math.abs(end[1] - start[1]));
  105. ctx.translate(cw / 2, ch / 2);
  106. ctx.rotate(data.rotate);
  107. ctx.drawImage(realImage, data.left, data.top, w, h, -w / 2, -h / 2, w, h);
  108. return new Promise<Blob | null>((resolve) => canvas.toBlob(resolve));
  109. };
  110. defineExpose<QuiskExpose>({
  111. submit: clipImage,
  112. });
  113. </script>
  114. <style lang="scss" scoped>
  115. .vue-crop-layout {
  116. width: 100%;
  117. }
  118. .control {
  119. margin-top: 20px;
  120. text-align: center;
  121. }
  122. </style>
  123. <style lang="scss">
  124. .vue-crop-layout {
  125. .cropper-view-box {
  126. outline-color: var(--el-color-primary);
  127. }
  128. }
  129. </style>