wd-img-cropper.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. <template>
  2. <!-- 绘制的图片canvas -->
  3. <view v-if="modelValue" :class="`wd-img-cropper ${customClass}`" :style="customStyle" @touchmove="preventTouchMove">
  4. <!-- 展示在用户面前的裁剪框 -->
  5. <view class="wd-img-cropper__wrapper">
  6. <!-- 画出裁剪框 -->
  7. <view class="wd-img-cropper__cut">
  8. <!-- 上方阴影块 -->
  9. <view :class="`wd-img-cropper__cut--top ${IS_TOUCH_END ? '' : 'is-hightlight'}`" :style="`height: ${cutTop}px;`"></view>
  10. <view class="wd-img-cropper__cut--middle" :style="`height: ${cutHeight}px;`">
  11. <!-- 左侧阴影块 -->
  12. <view
  13. :class="`wd-img-cropper__cut--left ${IS_TOUCH_END ? '' : 'is-hightlight'}`"
  14. :style="`width: ${cutLeft}px; height: ${cutHeight}px;`"
  15. ></view>
  16. <!-- 裁剪框 -->
  17. <view class="wd-img-cropper__cut--body" :style="`width: ${cutWidth}px; height: ${cutHeight}px;`">
  18. <!-- 内部网格线 -->
  19. <view class="is-gridlines-x"></view>
  20. <view class="is-gridlines-y"></view>
  21. <!-- 裁剪窗体四个对角 -->
  22. <view class="is-left-top"></view>
  23. <view class="is-left-bottom"></view>
  24. <view class="is-right-top"></view>
  25. <view class="is-right-bottom"></view>
  26. </view>
  27. <!-- 右侧阴影块 -->
  28. <view :class="`wd-img-cropper__cut--right ${IS_TOUCH_END ? '' : 'is-hightlight'}`"></view>
  29. </view>
  30. <!-- 底部阴影块 -->
  31. <view :class="`wd-img-cropper__cut--bottom ${IS_TOUCH_END ? '' : 'is-hightlight'}`"></view>
  32. </view>
  33. <!-- 展示的传过来的图片: 控制图片的旋转角度(rotate)、缩放程度(imgScale)、移动位置(translate) -->
  34. <image
  35. :prop="isAnimation"
  36. :change:prop="animation ? animation.setAnimation : ''"
  37. class="wd-img-cropper__img"
  38. :src="imgSrc"
  39. :style="imageStyle"
  40. :lazy-load="false"
  41. @touchstart="handleImgTouchStart"
  42. @touchmove="handleImgTouchMove"
  43. @touchend="handleImgTouchEnd"
  44. @error="handleImgLoadError"
  45. @load="handleImgLoaded"
  46. />
  47. </view>
  48. <!-- 绘制的图片canvas -->
  49. <canvas
  50. :canvas-id="canvasId"
  51. :id="canvasId"
  52. class="wd-img-cropper__canvas"
  53. :disable-scroll="true"
  54. :style="`width: ${Number(canvasWidth) * canvasScale}px; height: ${Number(canvasHeight) * canvasScale}px;`"
  55. />
  56. <!-- 下方按钮 -->
  57. <view class="wd-img-cropper__footer">
  58. <wd-icon custom-class="wd-img-cropper__rotate" v-if="!disabledRotate" name="rotate" @click="handleRotate"></wd-icon>
  59. <view class="wd-img-cropper__footer--button">
  60. <view class="is-cancel" @click="handleCancel">{{ cancelButtonText || translate('cancel') }}</view>
  61. <wd-button size="small" :custom-style="buttonStyle" @click="handleConfirm">{{ confirmButtonText || translate('confirm') }}</wd-button>
  62. </view>
  63. </view>
  64. </view>
  65. </template>
  66. <script lang="ts">
  67. export default {
  68. name: 'wd-img-cropper',
  69. options: {
  70. virtualHost: true,
  71. addGlobalClass: true,
  72. styleIsolation: 'shared'
  73. }
  74. }
  75. </script>
  76. <script lang="ts" setup>
  77. import wdIcon from '../wd-icon/wd-icon.vue'
  78. import wdButton from '../wd-button/wd-button.vue'
  79. import { computed, getCurrentInstance, ref, watch } from 'vue'
  80. import { addUnit, objToStyle, uuid } from '../common/util'
  81. import { useTranslate } from '../composables/useTranslate'
  82. import { imgCropperProps, type ImgCropperExpose } from './types'
  83. const canvasId = ref<string>(`cropper${uuid()}`) // canvas 组件的唯一标识符
  84. // 延时动画设置
  85. let CHANGE_TIME: any | null = null
  86. // 移动节流
  87. let MOVE_THROTTLE: any | null = null
  88. // 节流标志
  89. let MOVE_THROTTLE_FLAG: boolean = true
  90. // 图片设置尺寸,此值不变(记录最初设定的尺寸)
  91. let INIT_IMGWIDTH: null | number | string = null
  92. // 图片设置尺寸,此值不变(记录最初设定的尺寸)
  93. let INIT_IMGHEIGHT: null | number | string = null
  94. // 顶部裁剪框占比
  95. const TOP_PERCENT = 0.85
  96. const props = defineProps(imgCropperProps)
  97. const emit = defineEmits(['imgloaded', 'imgloaderror', 'cancel', 'confirm', 'update:modelValue'])
  98. const { translate } = useTranslate('img-cropper')
  99. // 旋转角度
  100. const imgAngle = ref<number>(0)
  101. // 是否开启动画
  102. const isAnimation = ref<boolean>(false)
  103. // #ifdef MP-ALIPAY || APP-PLUS || H5
  104. // hack 避免钉钉小程序、支付宝小程序、app抛出相关异常
  105. const animation: any = null
  106. // #endif
  107. // 裁剪框的宽高
  108. const picWidth = ref<number>(0)
  109. const picHeight = ref<number>(0)
  110. const cutWidth = ref<number>(0)
  111. const cutHeight = ref<number>(0)
  112. const offset = ref<number>(20)
  113. // 裁剪框的距顶距左
  114. const cutLeft = ref<number>(0)
  115. const cutTop = ref<number>(0)
  116. // canvas最终成像宽高
  117. const canvasWidth = ref<string | number>('')
  118. const canvasHeight = ref<string | number>('')
  119. const canvasScale = ref<number>(2)
  120. // 当前缩放大小
  121. const imgScale = ref<number>(1)
  122. // // 图片宽高
  123. // imgWidth: null,
  124. // imgHeight: null,
  125. // 图片中心轴点距左的距离
  126. const imgLeft = ref<number>(uni.getSystemInfoSync().windowWidth / 2)
  127. const imgTop = ref<number>((uni.getSystemInfoSync().windowHeight / 2) * TOP_PERCENT)
  128. const imgInfo = ref<UniApp.GetImageInfoSuccessData | null>(null)
  129. const info = ref<UniApp.GetSystemInfoResult>(uni.getSystemInfoSync())
  130. // 是否移动中设置 同时控制背景颜色是否高亮
  131. const IS_TOUCH_END = ref<boolean>(true)
  132. // 记录移动中的双指位置 [0][1]分别代表两根手指 [1]做待用参数
  133. const movingPosRecord = ref<Record<string, string | number>[]>([
  134. {
  135. x: '',
  136. y: ''
  137. },
  138. {
  139. x: '',
  140. y: ''
  141. }
  142. ])
  143. // 双指缩放时 两个坐标点斜边长度
  144. const fingerDistance = ref<string | number>('')
  145. const ctx = ref<UniApp.CanvasContext | null>(null)
  146. const { proxy } = getCurrentInstance() as any
  147. watch(
  148. () => props.modelValue,
  149. (newValue) => {
  150. if (newValue) {
  151. INIT_IMGWIDTH = props.imgWidth
  152. INIT_IMGHEIGHT = props.imgHeight
  153. info.value = uni.getSystemInfoSync()
  154. // 根据aspectRatio计算裁剪框尺寸
  155. const [widthRatio, heightRatio] = props.aspectRatio.split(':').map(Number)
  156. const tempCutWidth = info.value.windowWidth - offset.value * 2
  157. const tempCutHeight = (tempCutWidth * heightRatio) / widthRatio
  158. cutWidth.value = tempCutWidth
  159. cutHeight.value = tempCutHeight
  160. cutTop.value = (info.value.windowHeight * TOP_PERCENT - tempCutHeight) / 2
  161. cutLeft.value = offset.value
  162. canvasScale.value = props.exportScale
  163. canvasHeight.value = tempCutHeight
  164. canvasWidth.value = tempCutWidth
  165. // 根据开发者设置的图片目标尺寸计算实际尺寸
  166. initImageSize()
  167. // 初始化canvas
  168. initCanvas()
  169. // 加载图片
  170. props.imgSrc && loadImg()
  171. } else {
  172. resetImg()
  173. }
  174. },
  175. {
  176. deep: true,
  177. immediate: true
  178. }
  179. )
  180. watch(
  181. () => props.imgSrc,
  182. (newValue) => {
  183. newValue && loadImg()
  184. },
  185. {
  186. deep: true,
  187. immediate: true
  188. }
  189. )
  190. watch(
  191. () => imgAngle.value,
  192. (newValue) => {
  193. if (newValue % 90) {
  194. imgAngle.value = Math.round(newValue / 90) * 90
  195. }
  196. },
  197. {
  198. deep: true,
  199. immediate: true
  200. }
  201. )
  202. watch(
  203. () => isAnimation.value,
  204. (newValue) => {
  205. // 开启过渡300毫秒之后自动关闭
  206. CHANGE_TIME && clearTimeout(CHANGE_TIME)
  207. if (newValue) {
  208. CHANGE_TIME = setTimeout(() => {
  209. revertIsAnimation(false)
  210. clearTimeout(CHANGE_TIME)
  211. }, 300)
  212. }
  213. },
  214. {
  215. deep: true,
  216. immediate: true
  217. }
  218. )
  219. const buttonStyle = computed(() => {
  220. const style: Record<string, string | number> = {
  221. position: 'absolute',
  222. right: 0,
  223. // height: 32px;
  224. width: '56px',
  225. 'border-radius': '16px',
  226. 'font-size': '16px'
  227. }
  228. return objToStyle(style)
  229. })
  230. const imageStyle = computed(() => {
  231. const style: Record<string, string | number> = {
  232. width: picWidth.value ? addUnit(picWidth.value) : 'auto',
  233. height: picHeight.value ? addUnit(picHeight.value) : 'auto',
  234. transform: `translate(${addUnit(imgLeft.value - picWidth.value / 2)}, ${addUnit(imgTop.value - picHeight.value / 2)}) scale(${
  235. imgScale.value
  236. }) rotate(${imgAngle.value}deg)`,
  237. 'transition-duration': (isAnimation.value ? 0.4 : 0) + 's'
  238. }
  239. return objToStyle(style)
  240. })
  241. /**
  242. * 逆转是否使用动画
  243. */
  244. function revertIsAnimation(animation: boolean | { value: boolean }) {
  245. if (typeof animation === 'boolean') {
  246. isAnimation.value = animation
  247. } else {
  248. isAnimation.value = animation.value
  249. }
  250. }
  251. /**
  252. * 控制旋转角度
  253. * @param angle 角度
  254. */
  255. function setRoate(angle: number) {
  256. if (!angle || props.disabledRotate) return
  257. revertIsAnimation(true)
  258. imgAngle.value = angle
  259. // 重新计算缩放比例
  260. let tempPicWidth = picWidth.value
  261. let tempPicHeight = picHeight.value
  262. // 旋转后宽高互换
  263. if ((angle / 90) % 2) {
  264. tempPicWidth = picHeight.value
  265. tempPicHeight = picWidth.value
  266. }
  267. // 计算新的缩放比例
  268. const widthRatio = cutWidth.value / tempPicWidth
  269. const heightRatio = cutHeight.value / tempPicHeight
  270. imgScale.value = Math.max(widthRatio, heightRatio)
  271. // 检测边缘位置
  272. detectImgPosIsEdge()
  273. }
  274. /**
  275. * 初始化图片的大小和角度以及距离
  276. */
  277. function resetImg() {
  278. const { windowHeight, windowWidth } = uni.getSystemInfoSync()
  279. imgScale.value = 1
  280. imgAngle.value = 0
  281. imgLeft.value = windowWidth / 2
  282. imgTop.value = (windowHeight / 2) * TOP_PERCENT
  283. }
  284. /**
  285. * 加载图片资源文件,并初始化裁剪框内图片信息
  286. */
  287. function loadImg() {
  288. if (!props.imgSrc) return
  289. uni.getImageInfo({
  290. src: props.imgSrc,
  291. success: (res) => {
  292. // 存储img图片信息
  293. imgInfo.value = res
  294. // 计算最后图片尺寸
  295. computeImgSize()
  296. // 初始化尺寸
  297. resetImg()
  298. },
  299. fail: () => {
  300. // this.setData({ imgSrc: '' })
  301. }
  302. })
  303. }
  304. /**
  305. * 设置图片尺寸,使其短边完全显示并填满裁剪框
  306. */
  307. function computeImgSize() {
  308. let tempPicWidth: number = picWidth.value
  309. let tempPicHeight: number = picHeight.value
  310. if (!INIT_IMGHEIGHT && !INIT_IMGWIDTH) {
  311. // 计算图片与裁剪框的宽高比
  312. const imgRatio = imgInfo.value!.width / imgInfo.value!.height
  313. const cropRatio = cutWidth.value / cutHeight.value
  314. if (imgRatio > cropRatio) {
  315. // 图片更宽,以高度为准
  316. tempPicHeight = cutHeight.value
  317. tempPicWidth = tempPicHeight * imgRatio
  318. } else {
  319. // 图片更高,以宽度为准
  320. tempPicWidth = cutWidth.value
  321. tempPicHeight = tempPicWidth / imgRatio
  322. }
  323. } else if (INIT_IMGHEIGHT && !INIT_IMGWIDTH) {
  324. tempPicHeight = Number(INIT_IMGHEIGHT)
  325. tempPicWidth = (imgInfo.value!.width / imgInfo.value!.height) * tempPicHeight
  326. } else if ((!INIT_IMGHEIGHT && INIT_IMGWIDTH) || (INIT_IMGHEIGHT && INIT_IMGWIDTH)) {
  327. tempPicWidth = Number(INIT_IMGWIDTH)
  328. tempPicHeight = (imgInfo.value!.height / imgInfo.value!.width) * tempPicWidth
  329. }
  330. // 确保计算后的尺寸至少有一边等于裁剪框尺寸
  331. const widthRatio = cutWidth.value / tempPicWidth
  332. const heightRatio = cutHeight.value / tempPicHeight
  333. const scale = Math.max(widthRatio, heightRatio)
  334. picWidth.value = tempPicWidth
  335. picHeight.value = tempPicHeight
  336. // 设置初始缩放以适应裁剪框
  337. imgScale.value = scale
  338. }
  339. /**
  340. * canvas 初始化
  341. */
  342. function initCanvas() {
  343. if (!ctx.value) {
  344. ctx.value = uni.createCanvasContext(canvasId.value, proxy)
  345. }
  346. }
  347. /**
  348. * 图片初始化,处理宽高特殊单位
  349. */
  350. function initImageSize() {
  351. // 处理宽高特殊单位 %>px
  352. if (INIT_IMGWIDTH && typeof INIT_IMGWIDTH === 'string' && INIT_IMGWIDTH.indexOf('%') !== -1) {
  353. const width: string = INIT_IMGWIDTH.replace('%', '')
  354. INIT_IMGWIDTH = (info.value.windowWidth / 100) * Number(width)
  355. picWidth.value = INIT_IMGWIDTH
  356. } else if (INIT_IMGWIDTH && typeof INIT_IMGWIDTH === 'number') {
  357. picWidth.value = INIT_IMGWIDTH
  358. }
  359. if (INIT_IMGHEIGHT && typeof INIT_IMGHEIGHT === 'string' && INIT_IMGHEIGHT.indexOf('%') !== -1) {
  360. const height = (props.imgHeight as string).replace('%', '')
  361. // INIT_IMGHEIGHT = this.data.imgHeight = (info.value.windowHeight / 100) * Number(height)
  362. INIT_IMGHEIGHT = (info.value.windowHeight / 100) * Number(height)
  363. picWidth.value = INIT_IMGHEIGHT
  364. } else if (INIT_IMGHEIGHT && typeof INIT_IMGHEIGHT === 'number') {
  365. picWidth.value = Number(INIT_IMGWIDTH)
  366. }
  367. }
  368. /**
  369. * 图片拖动边缘检测:检测移动或缩放时 是否触碰到图片边缘位置
  370. */
  371. function detectImgPosIsEdge(scale?: number) {
  372. const currentScale = scale || imgScale.value
  373. let currentImgLeft = imgLeft.value
  374. let currentImgTop = imgTop.value
  375. let currentPicWidth = picWidth.value
  376. let currentPicHeight = picHeight.value
  377. // 翻转后宽高切换
  378. if ((imgAngle.value / 90) % 2) {
  379. currentPicWidth = picHeight.value
  380. currentPicHeight = picWidth.value
  381. }
  382. // 左
  383. currentImgLeft =
  384. (currentPicWidth * currentScale) / 2 + cutLeft.value >= currentImgLeft ? currentImgLeft : (currentPicWidth * imgScale.value) / 2 + cutLeft.value
  385. // 右
  386. currentImgLeft =
  387. cutLeft.value + cutWidth.value - (currentPicWidth * currentScale) / 2 <= currentImgLeft
  388. ? currentImgLeft
  389. : cutLeft.value + cutWidth.value - (currentPicWidth * currentScale) / 2
  390. // 上
  391. currentImgTop =
  392. (currentPicHeight * currentScale) / 2 + cutTop.value >= currentImgTop ? currentImgTop : (currentPicHeight * currentScale) / 2 + cutTop.value
  393. // 下
  394. currentImgTop =
  395. cutTop.value + cutHeight.value - (currentPicHeight * currentScale) / 2 <= currentImgTop
  396. ? currentImgTop
  397. : cutTop.value + cutHeight.value - (currentPicHeight * currentScale) / 2
  398. imgScale.value = currentScale
  399. imgTop.value = currentImgTop
  400. imgLeft.value = currentImgLeft
  401. }
  402. /**
  403. * 缩放边缘检测:检测移动或缩放时 是否触碰到图片边缘位置
  404. */
  405. function detectImgScaleIsEdge() {
  406. let tempPicWidth = picWidth.value
  407. let tempPicHeight = picHeight.value
  408. let tempImgScale = imgScale.value
  409. // 翻转后宽高切换
  410. if ((imgAngle.value / 90) % 2) {
  411. tempPicWidth = picHeight.value
  412. tempPicHeight = picWidth.value
  413. }
  414. if (tempPicWidth * tempImgScale < cutWidth.value) {
  415. tempImgScale = cutWidth.value / tempPicWidth
  416. }
  417. if (tempPicHeight * tempImgScale < cutHeight.value) {
  418. tempImgScale = cutHeight.value / tempPicHeight
  419. }
  420. detectImgPosIsEdge(tempImgScale)
  421. }
  422. /**
  423. * 节流
  424. */
  425. function throttle() {
  426. if (info.value.platform === 'android') {
  427. MOVE_THROTTLE && clearTimeout(MOVE_THROTTLE)
  428. MOVE_THROTTLE = setTimeout(() => {
  429. MOVE_THROTTLE_FLAG = true
  430. }, 1000 / 40)
  431. } else {
  432. !MOVE_THROTTLE_FLAG && (MOVE_THROTTLE_FLAG = true)
  433. }
  434. }
  435. /**
  436. * {图片区} 开始拖动
  437. */
  438. function handleImgTouchStart(event: any) {
  439. // 如果处于在拖动中,背景为淡色展示全部,拖动结束则为 0.85 透明度
  440. IS_TOUCH_END.value = false
  441. if (event.touches.length === 1) {
  442. // 单指拖动
  443. movingPosRecord.value[0] = {
  444. x: event.touches[0].clientX - imgLeft.value,
  445. y: event.touches[0].clientY - imgTop.value
  446. }
  447. } else {
  448. // 以两指为坐标点 做直角三角形 a2 + b2 = c2
  449. const width = Math.abs(event.touches[1].clientX - event.touches[0].clientX)
  450. const height = Math.abs(event.touches[1].clientY - event.touches[0].clientY)
  451. fingerDistance.value = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2))
  452. }
  453. }
  454. /**
  455. * {图片区} 拖动中
  456. */
  457. function handleImgTouchMove(event: any) {
  458. if (IS_TOUCH_END.value || !MOVE_THROTTLE_FLAG) return
  459. // 节流
  460. throttle()
  461. if (event.touches.length === 1) {
  462. // 单指拖动
  463. const { x, y } = movingPosRecord.value[0]
  464. const left = event.touches[0].clientX - Number(x)
  465. const top = event.touches[0].clientY - Number(y)
  466. imgLeft.value = left
  467. imgTop.value = top
  468. detectImgPosIsEdge()
  469. } else {
  470. // 以两指为坐标点 做直角三角形 a2 + b2 = c2
  471. const width = Math.abs(event.touches[1].clientX - event.touches[0].clientX)
  472. const height = Math.abs(event.touches[1].clientY - event.touches[0].clientY)
  473. const hypotenuse = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2))
  474. const scale = imgScale.value * (hypotenuse / Number(fingerDistance.value))
  475. imgScale.value = Math.min(scale, props.maxScale)
  476. detectImgScaleIsEdge()
  477. fingerDistance.value = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2))
  478. }
  479. }
  480. /**
  481. * {图片区} 拖动结束
  482. */
  483. function handleImgTouchEnd() {
  484. IS_TOUCH_END.value = true
  485. }
  486. /**
  487. * 图片已加载完成
  488. */
  489. function handleImgLoaded(res: any) {
  490. emit('imgloaded', res)
  491. }
  492. /**
  493. * 图片加载失败
  494. */
  495. function handleImgLoadError(err: any) {
  496. emit('imgloaderror', err)
  497. }
  498. /**
  499. * 旋转图片
  500. */
  501. function handleRotate() {
  502. setRoate(imgAngle.value - 90)
  503. }
  504. /**
  505. * 取消裁剪图片
  506. */
  507. function handleCancel() {
  508. emit('cancel')
  509. emit('update:modelValue', false)
  510. }
  511. /**
  512. * 完成裁剪
  513. */
  514. function handleConfirm() {
  515. draw()
  516. }
  517. /**
  518. * canvas 绘制图片输出成文件类型
  519. */
  520. function canvasToImage() {
  521. const { fileType, quality, exportScale } = props
  522. uni.canvasToTempFilePath(
  523. {
  524. width: cutWidth.value * exportScale,
  525. height: Math.round(cutHeight.value * exportScale),
  526. destWidth: cutWidth.value * exportScale,
  527. destHeight: Math.round(cutHeight.value * exportScale),
  528. fileType,
  529. quality,
  530. canvasId: canvasId.value,
  531. success: (res: any) => {
  532. const result = { tempFilePath: res.tempFilePath, width: cutWidth.value * exportScale, height: cutHeight.value * exportScale }
  533. // #ifdef MP-DINGTALK
  534. result.tempFilePath = res.filePath
  535. // #endif
  536. emit('confirm', result)
  537. },
  538. complete: () => {
  539. emit('update:modelValue', false)
  540. }
  541. },
  542. proxy
  543. )
  544. }
  545. /**
  546. * canvas绘制,用canvas模拟裁剪框 对根据图片当前的裁剪信息进行模拟
  547. */
  548. function draw() {
  549. if (!props.imgSrc) return
  550. const draw = () => {
  551. // 图片真实大小
  552. const width = picWidth.value * imgScale.value * props.exportScale
  553. const height = picHeight.value * imgScale.value * props.exportScale
  554. // 取裁剪框和图片的交集
  555. const x = imgLeft.value - cutLeft.value
  556. const y = imgTop.value - cutTop.value
  557. // 如果直接使用canvas绘制的图片会有锯齿,因此需要*设备像素比
  558. // 设置(x, y)设置图片在canvas中的位置
  559. ctx.value!.translate(x * props.exportScale, y * props.exportScale)
  560. // 设置 旋转角度
  561. if (!props.disabledRotate) {
  562. ctx.value!.rotate((imgAngle.value * Math.PI) / 180)
  563. }
  564. // drawImage 的 旋转是根据以当前图片的图片水平垂直方向为x、y轴,并在x y轴上移动
  565. ctx.value!.drawImage(props.imgSrc, -width / 2, -height / 2, width, height)
  566. ctx.value!.restore()
  567. // 绘制图片
  568. ctx.value!.draw(false, () => {
  569. canvasToImage()
  570. })
  571. }
  572. canvasHeight.value = cutHeight.value
  573. canvasWidth.value = cutWidth.value
  574. draw()
  575. }
  576. function preventTouchMove() {}
  577. defineExpose<ImgCropperExpose>({
  578. revertIsAnimation,
  579. setRoate,
  580. resetImg
  581. })
  582. </script>
  583. <!-- #ifdef MP-WEIXIN || MP-QQ -->
  584. <script module="animation" lang="wxs">
  585. function setAnimation(newValue, oldValue, ownerInstance){
  586. if (newValue) {
  587. var id = ownerInstance.setTimeout(function() {
  588. ownerInstance.callMethod('revertIsAnimation',{value:false})
  589. ownerInstance.clearTimeout(id)
  590. },300)
  591. }
  592. }
  593. module.exports= {
  594. setAnimation:setAnimation,
  595. }
  596. </script>
  597. <!-- #endif -->
  598. <style lang="scss" scoped>
  599. @import './index.scss';
  600. </style>