useScaleElement.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. import { computed, Ref } from 'vue'
  2. import { useStore } from 'vuex'
  3. import { State, MutationTypes } from '@/store'
  4. import { ElementTypes, PPTElement, PPTImageElement, PPTLineElement, PPTShapeElement } from '@/types/slides'
  5. import { OperatePoints, ElementScaleHandler, AlignmentLineProps, MultiSelectRange } from '@/types/edit'
  6. import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
  7. import { AlignLine, uniqAlignLines } from '@/utils/element'
  8. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  9. // 计算元素被旋转一定角度后,八个操作点的新坐标
  10. interface RotateElementData {
  11. left: number;
  12. top: number;
  13. width: number;
  14. height: number;
  15. }
  16. const getRotateElementPoints = (element: RotateElementData, angle: number) => {
  17. const { left, top, width, height } = element
  18. const radius = Math.sqrt( Math.pow(width, 2) + Math.pow(height, 2) ) / 2
  19. const auxiliaryAngle = Math.atan(height / width) * 180 / Math.PI
  20. const tlbraRadian = (180 - angle - auxiliaryAngle) * Math.PI / 180
  21. const trblaRadian = (auxiliaryAngle - angle) * Math.PI / 180
  22. const taRadian = (90 - angle) * Math.PI / 180
  23. const raRadian = angle * Math.PI / 180
  24. const halfWidth = width / 2
  25. const halfHeight = height / 2
  26. const middleLeft = left + halfWidth
  27. const middleTop = top + halfHeight
  28. const leftTopPoint = {
  29. left: middleLeft + radius * Math.cos(tlbraRadian),
  30. top: middleTop - radius * Math.sin(tlbraRadian),
  31. }
  32. const topPoint = {
  33. left: middleLeft + halfHeight * Math.cos(taRadian),
  34. top: middleTop - halfHeight * Math.sin(taRadian),
  35. }
  36. const rightTopPoint = {
  37. left: middleLeft + radius * Math.cos(trblaRadian),
  38. top: middleTop - radius * Math.sin(trblaRadian),
  39. }
  40. const rightPoint = {
  41. left: middleLeft + halfWidth * Math.cos(raRadian),
  42. top: middleTop + halfWidth * Math.sin(raRadian),
  43. }
  44. const rightBottomPoint = {
  45. left: middleLeft - radius * Math.cos(tlbraRadian),
  46. top: middleTop + radius * Math.sin(tlbraRadian),
  47. }
  48. const bottomPoint = {
  49. left: middleLeft - halfHeight * Math.sin(raRadian),
  50. top: middleTop + halfHeight * Math.cos(raRadian),
  51. }
  52. const leftBottomPoint = {
  53. left: middleLeft - radius * Math.cos(trblaRadian),
  54. top: middleTop + radius * Math.sin(trblaRadian),
  55. }
  56. const leftPoint = {
  57. left: middleLeft - halfWidth * Math.cos(raRadian),
  58. top: middleTop - halfWidth * Math.sin(raRadian),
  59. }
  60. return { leftTopPoint, topPoint, rightTopPoint, rightPoint, rightBottomPoint, bottomPoint, leftBottomPoint, leftPoint }
  61. }
  62. // 获取元素某个操作点对角线上另一端的操作点坐标(例如:左上 <-> 右下)
  63. const getOppositePoint = (direction: number, points: ReturnType<typeof getRotateElementPoints>): { left: number; top: number } => {
  64. const oppositeMap = {
  65. [OperatePoints.RIGHT_BOTTOM]: points.leftTopPoint,
  66. [OperatePoints.LEFT_BOTTOM]: points.rightTopPoint,
  67. [OperatePoints.LEFT_TOP]: points.rightBottomPoint,
  68. [OperatePoints.RIGHT_TOP]: points.leftBottomPoint,
  69. [OperatePoints.TOP]: points.bottomPoint,
  70. [OperatePoints.BOTTOM]: points.topPoint,
  71. [OperatePoints.LEFT]: points.rightPoint,
  72. [OperatePoints.RIGHT]: points.leftPoint,
  73. }
  74. return oppositeMap[direction]
  75. }
  76. export default (
  77. elementList: Ref<PPTElement[]>,
  78. activeGroupElementId: Ref<string>,
  79. alignmentLines: Ref<AlignmentLineProps[]>,
  80. ) => {
  81. const store = useStore<State>()
  82. const activeElementIdList = computed(() => store.state.activeElementIdList)
  83. const ctrlOrShiftKeyActive: Ref<boolean> = computed(() => store.getters.ctrlOrShiftKeyActive)
  84. const canvasScale = computed(() => store.state.canvasScale)
  85. const { addHistorySnapshot } = useHistorySnapshot()
  86. const scaleElement = (e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: ElementScaleHandler) => {
  87. let isMouseDown = true
  88. const elOriginLeft = element.left
  89. const elOriginTop = element.top
  90. const elOriginWidth = element.width
  91. const elOriginHeight = element.height
  92. const fixedRatio = ctrlOrShiftKeyActive.value || ('fixedRatio' in element && element.fixedRatio)
  93. const aspectRatio = elOriginWidth / elOriginHeight
  94. const elRotate = ('rotate' in element && element.rotate) ? element.rotate : 0
  95. const rotateRadian = Math.PI * elRotate / 180
  96. const startPageX = e.pageX
  97. const startPageY = e.pageY
  98. const minSize = 15
  99. const getSizeWithinRange = (size: number) => size < minSize ? minSize : size
  100. let points: ReturnType<typeof getRotateElementPoints>
  101. let baseLeft = 0
  102. let baseTop = 0
  103. let horizontalLines: AlignLine[] = []
  104. let verticalLines: AlignLine[] = []
  105. if('rotate' in element && element.rotate) {
  106. // 元素旋转后的各点坐标以及对角坐标
  107. const { left, top, width, height } = element
  108. points = getRotateElementPoints({ left, top, width, height }, elRotate)
  109. const oppositePoint = getOppositePoint(command, points)
  110. // 基点坐标(以操作点相对的点为基点,例如拖动右下角,实际上是保持左上角不变的前提下修改其他信息)
  111. baseLeft = oppositePoint.left
  112. baseTop = oppositePoint.top
  113. }
  114. else {
  115. const edgeWidth = VIEWPORT_SIZE
  116. const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
  117. const isActiveGroupElement = element.id === activeGroupElementId.value
  118. for(const el of elementList.value) {
  119. if('rotate' in el && el.rotate) continue
  120. if(el.type === ElementTypes.LINE) continue
  121. if(isActiveGroupElement && el.id === element.id) continue
  122. if(!isActiveGroupElement && activeElementIdList.value.includes(el.id)) continue
  123. const left = el.left
  124. const top = el.top
  125. const width = el.width
  126. const height = el.height
  127. const right = left + width
  128. const bottom = top + height
  129. const topLine: AlignLine = { value: top, range: [left, right] }
  130. const bottomLine: AlignLine = { value: bottom, range: [left, right] }
  131. const leftLine: AlignLine = { value: left, range: [top, bottom] }
  132. const rightLine: AlignLine = { value: right, range: [top, bottom] }
  133. horizontalLines.push(topLine, bottomLine)
  134. verticalLines.push(leftLine, rightLine)
  135. }
  136. // 页面边界、水平和垂直的中心位置
  137. const edgeTopLine: AlignLine = { value: 0, range: [0, edgeWidth] }
  138. const edgeBottomLine: AlignLine = { value: edgeHeight, range: [0, edgeWidth] }
  139. const edgeHorizontalCenterLine: AlignLine = { value: edgeHeight / 2, range: [0, edgeWidth] }
  140. const edgeLeftLine: AlignLine = { value: 0, range: [0, edgeHeight] }
  141. const edgeRightLine: AlignLine = { value: edgeWidth, range: [0, edgeHeight] }
  142. const edgeVerticalCenterLine: AlignLine = { value: edgeWidth / 2, range: [0, edgeHeight] }
  143. horizontalLines.push(edgeTopLine, edgeBottomLine, edgeHorizontalCenterLine)
  144. verticalLines.push(edgeLeftLine, edgeRightLine, edgeVerticalCenterLine)
  145. horizontalLines = uniqAlignLines(horizontalLines)
  146. verticalLines = uniqAlignLines(verticalLines)
  147. }
  148. // 对齐吸附方法
  149. const alignedAdsorption = (currentX: number | null, currentY: number | null) => {
  150. const sorptionRange = 3
  151. const _alignmentLines: AlignmentLineProps[] = []
  152. let isVerticalAdsorbed = false
  153. let isHorizontalAdsorbed = false
  154. const correctionVal = { offsetX: 0, offsetY: 0 }
  155. if(currentY || currentY === 0) {
  156. for(let i = 0; i < horizontalLines.length; i++) {
  157. const { value, range } = horizontalLines[i]
  158. const min = Math.min(...range, currentX || 0)
  159. const max = Math.max(...range, currentX || 0)
  160. if(Math.abs(currentY - value) < sorptionRange) {
  161. if(!isHorizontalAdsorbed) {
  162. correctionVal.offsetY = currentY - value
  163. isHorizontalAdsorbed = true
  164. }
  165. _alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
  166. }
  167. }
  168. }
  169. if(currentX || currentX === 0) {
  170. for(let i = 0; i < verticalLines.length; i++) {
  171. const { value, range } = verticalLines[i]
  172. const min = Math.min(...range, (currentY || 0))
  173. const max = Math.max(...range, (currentY || 0))
  174. if(Math.abs(currentX - value) < sorptionRange) {
  175. if(!isVerticalAdsorbed) {
  176. correctionVal.offsetX = currentX - value
  177. isVerticalAdsorbed = true
  178. }
  179. _alignmentLines.push({ type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40 })
  180. }
  181. }
  182. }
  183. alignmentLines.value = _alignmentLines
  184. return correctionVal
  185. }
  186. document.onmousemove = e => {
  187. if(!isMouseDown) return
  188. const currentPageX = e.pageX
  189. const currentPageY = e.pageY
  190. const x = currentPageX - startPageX
  191. const y = currentPageY - startPageY
  192. let width = elOriginWidth
  193. let height = elOriginHeight
  194. let left = elOriginLeft
  195. let top = elOriginTop
  196. // 元素被旋转的情况下
  197. if(elRotate) {
  198. // 根据元素旋转的角度,修正鼠标按下后移动的距离(因为拖动的方向发生了改变)
  199. const revisedX = (Math.cos(rotateRadian) * x + Math.sin(rotateRadian) * y) / canvasScale.value
  200. let revisedY = (Math.cos(rotateRadian) * y - Math.sin(rotateRadian) * x) / canvasScale.value
  201. // 锁定宽高比例
  202. if(fixedRatio) {
  203. if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) revisedY = revisedX / aspectRatio
  204. if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) revisedY = -revisedX / aspectRatio
  205. }
  206. // 根据不同的操作点分别计算元素缩放后的大小和位置
  207. // 这里计算的位置是错误的,因为旋转后缩放实际上也改变了元素的位置,需要在后面进行矫正
  208. // 这里计算的大小是正确的,因为上面修正鼠标按下后移动的距离时其实已经进行过了矫正
  209. if(command === OperatePoints.RIGHT_BOTTOM) {
  210. width = getSizeWithinRange(elOriginWidth + revisedX)
  211. height = getSizeWithinRange(elOriginHeight + revisedY)
  212. }
  213. else if(command === OperatePoints.LEFT_BOTTOM) {
  214. width = getSizeWithinRange(elOriginWidth - revisedX)
  215. height = getSizeWithinRange(elOriginHeight + revisedY)
  216. left = elOriginLeft - (width - elOriginWidth)
  217. }
  218. else if(command === OperatePoints.LEFT_TOP) {
  219. width = getSizeWithinRange(elOriginWidth - revisedX)
  220. height = getSizeWithinRange(elOriginHeight - revisedY)
  221. left = elOriginLeft - (width - elOriginWidth)
  222. top = elOriginTop - (height - elOriginHeight)
  223. }
  224. else if(command === OperatePoints.RIGHT_TOP) {
  225. width = getSizeWithinRange(elOriginWidth + revisedX)
  226. height = getSizeWithinRange(elOriginHeight - revisedY)
  227. top = elOriginTop - (height - elOriginHeight)
  228. }
  229. else if(command === OperatePoints.TOP) {
  230. height = getSizeWithinRange(elOriginHeight - revisedY)
  231. top = elOriginTop - (height - elOriginHeight)
  232. }
  233. else if(command === OperatePoints.BOTTOM) {
  234. height = getSizeWithinRange(elOriginHeight + revisedY)
  235. }
  236. else if(command === OperatePoints.LEFT) {
  237. width = getSizeWithinRange(elOriginWidth - revisedX)
  238. left = elOriginLeft - (width - elOriginWidth)
  239. }
  240. else if(command === OperatePoints.RIGHT) {
  241. width = getSizeWithinRange(elOriginWidth + revisedX)
  242. }
  243. // 获取当前元素基点坐标,与初始状态的基点坐标进行对比并矫正差值
  244. const currentPoints = getRotateElementPoints({ width, height, left, top }, elRotate)
  245. const currentOppositePoint = getOppositePoint(command, currentPoints)
  246. const currentBaseLeft = currentOppositePoint.left
  247. const currentBaseTop = currentOppositePoint.top
  248. const offsetX = currentBaseLeft - baseLeft
  249. const offsetY = currentBaseTop - baseTop
  250. left = left - offsetX
  251. top = top - offsetY
  252. }
  253. // 元素未被旋转的情况下,根据所操纵点的位置添加对齐吸附
  254. else {
  255. let moveX = x / canvasScale.value
  256. let moveY = y / canvasScale.value
  257. if(fixedRatio) {
  258. if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) moveY = moveX / aspectRatio
  259. if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) moveY = -moveX / aspectRatio
  260. }
  261. if(command === OperatePoints.RIGHT_BOTTOM) {
  262. const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, elOriginTop + elOriginHeight + moveY)
  263. moveX = moveX - offsetX
  264. moveY = moveY - offsetY
  265. if(fixedRatio) {
  266. if(offsetY) moveX = moveY * aspectRatio
  267. else moveY = moveX / aspectRatio
  268. }
  269. width = getSizeWithinRange(elOriginWidth + moveX)
  270. height = getSizeWithinRange(elOriginHeight + moveY)
  271. }
  272. else if(command === OperatePoints.LEFT_BOTTOM) {
  273. const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + moveX, elOriginTop + elOriginHeight + moveY)
  274. moveX = moveX - offsetX
  275. moveY = moveY - offsetY
  276. if(fixedRatio) {
  277. if(offsetY) moveX = -moveY * aspectRatio
  278. else moveY = -moveX / aspectRatio
  279. }
  280. width = getSizeWithinRange(elOriginWidth - moveX)
  281. height = getSizeWithinRange(elOriginHeight + moveY)
  282. left = elOriginLeft - (width - elOriginWidth)
  283. }
  284. else if(command === OperatePoints.LEFT_TOP) {
  285. const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + moveX, elOriginTop + moveY)
  286. moveX = moveX - offsetX
  287. moveY = moveY - offsetY
  288. if(fixedRatio) {
  289. if(offsetY) moveX = moveY * aspectRatio
  290. else moveY = moveX / aspectRatio
  291. }
  292. width = getSizeWithinRange(elOriginWidth - moveX)
  293. height = getSizeWithinRange(elOriginHeight - moveY)
  294. left = elOriginLeft - (width - elOriginWidth)
  295. top = elOriginTop - (height - elOriginHeight)
  296. }
  297. else if(command === OperatePoints.RIGHT_TOP) {
  298. const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, elOriginTop + moveY)
  299. moveX = moveX - offsetX
  300. moveY = moveY - offsetY
  301. if(fixedRatio) {
  302. if(offsetY) moveX = -moveY * aspectRatio
  303. else moveY = -moveX / aspectRatio
  304. }
  305. width = getSizeWithinRange(elOriginWidth + moveX)
  306. height = getSizeWithinRange(elOriginHeight - moveY)
  307. top = elOriginTop - (height - elOriginHeight)
  308. }
  309. else if(command === OperatePoints.LEFT) {
  310. const { offsetX } = alignedAdsorption(elOriginLeft + moveX, null)
  311. moveX = moveX - offsetX
  312. width = getSizeWithinRange(elOriginWidth - moveX)
  313. left = elOriginLeft - (width - elOriginWidth)
  314. }
  315. else if(command === OperatePoints.RIGHT) {
  316. const { offsetX } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, null)
  317. moveX = moveX - offsetX
  318. width = getSizeWithinRange(elOriginWidth + moveX)
  319. }
  320. else if(command === OperatePoints.TOP) {
  321. const { offsetY } = alignedAdsorption(null, elOriginTop + moveY)
  322. moveY = moveY - offsetY
  323. height = getSizeWithinRange(elOriginHeight - moveY)
  324. top = elOriginTop - (height - elOriginHeight)
  325. }
  326. else if(command === OperatePoints.BOTTOM) {
  327. const { offsetY } = alignedAdsorption(null, elOriginTop + elOriginHeight + moveY)
  328. moveY = moveY - offsetY
  329. height = getSizeWithinRange(elOriginHeight + moveY)
  330. }
  331. }
  332. elementList.value = elementList.value.map(el => element.id === el.id ? { ...el, left, top, width, height } : el)
  333. }
  334. document.onmouseup = e => {
  335. isMouseDown = false
  336. document.onmousemove = null
  337. document.onmouseup = null
  338. alignmentLines.value = []
  339. if(startPageX === e.pageX && startPageY === e.pageY) return
  340. store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
  341. addHistorySnapshot()
  342. }
  343. }
  344. const scaleMultiElement = (e: MouseEvent, range: MultiSelectRange, command: ElementScaleHandler) => {
  345. let isMouseDown = true
  346. const { minX, maxX, minY, maxY } = range
  347. const operateWidth = maxX - minX
  348. const operateHeight = maxY - minY
  349. const aspectRatio = operateWidth / operateHeight
  350. const startPageX = e.pageX
  351. const startPageY = e.pageY
  352. const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
  353. document.onmousemove = e => {
  354. if(!isMouseDown) return
  355. const currentPageX = e.pageX
  356. const currentPageY = e.pageY
  357. // 鼠标按下后移动的距离
  358. const x = (currentPageX - startPageX) / canvasScale.value
  359. let y = (currentPageY - startPageY) / canvasScale.value
  360. // 锁定宽高比例
  361. if(ctrlOrShiftKeyActive.value) {
  362. if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) y = x / aspectRatio
  363. if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) y = -x / aspectRatio
  364. }
  365. // 获取鼠标缩放时当前所有激活元素的范围
  366. let currentMinX = minX
  367. let currentMaxX = maxX
  368. let currentMinY = minY
  369. let currentMaxY = maxY
  370. if(command === OperatePoints.RIGHT_BOTTOM) {
  371. currentMaxX = maxX + x
  372. currentMaxY = maxY + y
  373. }
  374. else if(command === OperatePoints.LEFT_BOTTOM) {
  375. currentMinX = minX + x
  376. currentMaxY = maxY + y
  377. }
  378. else if(command === OperatePoints.LEFT_TOP) {
  379. currentMinX = minX + x
  380. currentMinY = minY + y
  381. }
  382. else if(command === OperatePoints.RIGHT_TOP) {
  383. currentMaxX = maxX + x
  384. currentMinY = minY + y
  385. }
  386. else if(command === OperatePoints.TOP) {
  387. currentMinY = minY + y
  388. }
  389. else if(command === OperatePoints.BOTTOM) {
  390. currentMaxY = maxY + y
  391. }
  392. else if(command === OperatePoints.LEFT) {
  393. currentMinX = minX + x
  394. }
  395. else if(command === OperatePoints.RIGHT) {
  396. currentMaxX = maxX + x
  397. }
  398. // 多选下所有元素整体宽高
  399. const currentOppositeWidth = currentMaxX - currentMinX
  400. const currentOppositeHeight = currentMaxY - currentMinY
  401. // 所有元素的整体宽高与被操作元素宽高的比例
  402. let widthScale = currentOppositeWidth / operateWidth
  403. let heightScale = currentOppositeHeight / operateHeight
  404. if(widthScale <= 0) widthScale = 0
  405. if(heightScale <= 0) heightScale = 0
  406. // 根据上面计算的比例,修改所有被激活元素的位置大小
  407. // 宽高通过乘以对应的比例得到,位置通过将被操作元素在所有元素整体中的相对位置乘以对应比例获得
  408. elementList.value = elementList.value.map(el => {
  409. if((el.type === ElementTypes.IMAGE || el.type === ElementTypes.SHAPE) && activeElementIdList.value.includes(el.id)) {
  410. const originElement = originElementList.find(originEl => originEl.id === el.id) as PPTImageElement | PPTShapeElement
  411. return {
  412. ...el,
  413. width: originElement.width * widthScale,
  414. height: originElement.height * heightScale,
  415. left: currentMinX + (originElement.left - minX) * widthScale,
  416. top: currentMinY + (originElement.top - minY) * heightScale,
  417. }
  418. }
  419. return el
  420. })
  421. }
  422. document.onmouseup = e => {
  423. isMouseDown = false
  424. document.onmousemove = null
  425. document.onmouseup = null
  426. if(startPageX === e.pageX && startPageY === e.pageY) return
  427. store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
  428. addHistorySnapshot()
  429. }
  430. }
  431. return {
  432. scaleElement,
  433. scaleMultiElement,
  434. }
  435. }