imageCropper.vue 4.6 KB

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