element.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import tinycolor from 'tinycolor2'
  2. import { nanoid } from 'nanoid'
  3. import type { PPTElement, PPTLineElement, Slide } from '@/types/slides'
  4. interface RotatedElementData {
  5. left: number
  6. top: number
  7. width: number
  8. height: number
  9. rotate: number
  10. }
  11. interface IdMap {
  12. [id: string]: string
  13. }
  14. /**
  15. * 计算元素在画布中的矩形范围旋转后的新位置范围
  16. * @param element 元素的位置大小和旋转角度信息
  17. */
  18. export const getRectRotatedRange = (element: RotatedElementData) => {
  19. const { left, top, width, height, rotate = 0 } = element
  20. const radius = Math.sqrt( Math.pow(width, 2) + Math.pow(height, 2) ) / 2
  21. const auxiliaryAngle = Math.atan(height / width) * 180 / Math.PI
  22. const tlbraRadian = (180 - rotate - auxiliaryAngle) * Math.PI / 180
  23. const trblaRadian = (auxiliaryAngle - rotate) * Math.PI / 180
  24. const middleLeft = left + width / 2
  25. const middleTop = top + height / 2
  26. const xAxis = [
  27. middleLeft + radius * Math.cos(tlbraRadian),
  28. middleLeft + radius * Math.cos(trblaRadian),
  29. middleLeft - radius * Math.cos(tlbraRadian),
  30. middleLeft - radius * Math.cos(trblaRadian),
  31. ]
  32. const yAxis = [
  33. middleTop - radius * Math.sin(tlbraRadian),
  34. middleTop - radius * Math.sin(trblaRadian),
  35. middleTop + radius * Math.sin(tlbraRadian),
  36. middleTop + radius * Math.sin(trblaRadian),
  37. ]
  38. return {
  39. xRange: [Math.min(...xAxis), Math.max(...xAxis)],
  40. yRange: [Math.min(...yAxis), Math.max(...yAxis)],
  41. }
  42. }
  43. /**
  44. * 计算元素在画布中的矩形范围旋转后的新位置与旋转之前位置的偏离距离
  45. * @param element 元素的位置大小和旋转角度信息
  46. */
  47. export const getRectRotatedOffset = (element: RotatedElementData) => {
  48. const { xRange: originXRange, yRange: originYRange } = getRectRotatedRange({
  49. left: element.left,
  50. top: element.top,
  51. width: element.width,
  52. height: element.height,
  53. rotate: 0,
  54. })
  55. const { xRange: rotatedXRange, yRange: rotatedYRange } = getRectRotatedRange({
  56. left: element.left,
  57. top: element.top,
  58. width: element.width,
  59. height: element.height,
  60. rotate: element.rotate,
  61. })
  62. return {
  63. offsetX: rotatedXRange[0] - originXRange[0],
  64. offsetY: rotatedYRange[0] - originYRange[0],
  65. }
  66. }
  67. /**
  68. * 计算元素在画布中的位置范围
  69. * @param element 元素信息
  70. */
  71. export const getElementRange = (element: PPTElement) => {
  72. let minX, maxX, minY, maxY
  73. if (element.type === 'line') {
  74. minX = element.left
  75. maxX = element.left + Math.max(element.start[0], element.end[0])
  76. minY = element.top
  77. maxY = element.top + Math.max(element.start[1], element.end[1])
  78. }
  79. else if ('rotate' in element && element.rotate) {
  80. const { left, top, width, height, rotate } = element
  81. const { xRange, yRange } = getRectRotatedRange({ left, top, width, height, rotate })
  82. minX = xRange[0]
  83. maxX = xRange[1]
  84. minY = yRange[0]
  85. maxY = yRange[1]
  86. }
  87. else {
  88. minX = element.left
  89. maxX = element.left + element.width
  90. minY = element.top
  91. maxY = element.top + element.height
  92. }
  93. return { minX, maxX, minY, maxY }
  94. }
  95. /**
  96. * 计算一组元素在画布中的位置范围
  97. * @param elementList 一组元素信息
  98. */
  99. export const getElementListRange = (elementList: PPTElement[]) => {
  100. const leftValues: number[] = []
  101. const topValues: number[] = []
  102. const rightValues: number[] = []
  103. const bottomValues: number[] = []
  104. elementList.forEach(element => {
  105. const { minX, maxX, minY, maxY } = getElementRange(element)
  106. leftValues.push(minX)
  107. topValues.push(minY)
  108. rightValues.push(maxX)
  109. bottomValues.push(maxY)
  110. })
  111. const minX = Math.min(...leftValues)
  112. const maxX = Math.max(...rightValues)
  113. const minY = Math.min(...topValues)
  114. const maxY = Math.max(...bottomValues)
  115. return { minX, maxX, minY, maxY }
  116. }
  117. /**
  118. * 计算线条元素的长度
  119. * @param element 线条元素
  120. */
  121. export const getLineElementLength = (element: PPTLineElement) => {
  122. const deltaX = element.end[0] - element.start[0]
  123. const deltaY = element.end[1] - element.start[1]
  124. const len = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
  125. return len
  126. }
  127. export interface AlignLine {
  128. value: number
  129. range: [number, number]
  130. }
  131. /**
  132. * 将一组对齐吸附线进行去重:同位置的的多条对齐吸附线仅留下一条,取该位置所有对齐吸附线的最大值和最小值为新的范围
  133. * @param lines 一组对齐吸附线信息
  134. */
  135. export const uniqAlignLines = (lines: AlignLine[]) => {
  136. const uniqLines: AlignLine[] = []
  137. lines.forEach(line => {
  138. const index = uniqLines.findIndex(_line => _line.value === line.value)
  139. if (index === -1) uniqLines.push(line)
  140. else {
  141. const uniqLine = uniqLines[index]
  142. const rangeMin = Math.min(uniqLine.range[0], line.range[0])
  143. const rangeMax = Math.max(uniqLine.range[1], line.range[1])
  144. const range: [number, number] = [rangeMin, rangeMax]
  145. const _line = { value: line.value, range }
  146. uniqLines[index] = _line
  147. }
  148. })
  149. return uniqLines
  150. }
  151. /**
  152. * 以页面列表为基础,为每一个页面生成新的ID,并关联到旧ID形成一个字典
  153. * 主要用于页面元素时,维持数据中各处页面ID原有的关系
  154. * @param slides 页面列表
  155. */
  156. export const createSlideIdMap = (slides: Slide[]) => {
  157. const slideIdMap: IdMap = {}
  158. for (const slide of slides) {
  159. slideIdMap[slide.id] = nanoid(10)
  160. }
  161. return slideIdMap
  162. }
  163. /**
  164. * 以元素列表为基础,为每一个元素生成新的ID,并关联到旧ID形成一个字典
  165. * 主要用于复制元素时,维持数据中各处元素ID原有的关系
  166. * 例如:原本两个组合的元素拥有相同的groupId,复制后依然会拥有另一个相同的groupId
  167. * @param elements 元素列表数据
  168. */
  169. export const createElementIdMap = (elements: PPTElement[]) => {
  170. const groupIdMap: IdMap = {}
  171. const elIdMap: IdMap = {}
  172. for (const element of elements) {
  173. const groupId = element.groupId
  174. if (groupId && !groupIdMap[groupId]) {
  175. groupIdMap[groupId] = nanoid(10)
  176. }
  177. elIdMap[element.id] = nanoid(10)
  178. }
  179. return {
  180. groupIdMap,
  181. elIdMap,
  182. }
  183. }
  184. /**
  185. * 根据表格的主题色,获取对应用于配色的子颜色
  186. * @param themeColor 主题色
  187. */
  188. export const getTableSubThemeColor = (themeColor: string) => {
  189. const rgba = tinycolor(themeColor)
  190. return [
  191. rgba.setAlpha(0.3).toRgbString(),
  192. rgba.setAlpha(0.1).toRgbString(),
  193. ]
  194. }
  195. /**
  196. * 获取线条元素路径字符串
  197. * @param element 线条元素
  198. */
  199. export const getLineElementPath = (element: PPTLineElement) => {
  200. const start = element.start.join(',')
  201. const end = element.end.join(',')
  202. if (element.broken) {
  203. const mid = element.broken.join(',')
  204. return `M${start} L${mid} L${end}`
  205. }
  206. else if (element.broken2) {
  207. const { minX, maxX, minY, maxY } = getElementRange(element)
  208. if (maxX - minX >= maxY - minY) return `M${start} L${element.broken2[0]},${element.start[1]} L${element.broken2[0]},${element.end[1]} ${end}`
  209. return `M${start} L${element.start[0]},${element.broken2[1]} L${element.end[0]},${element.broken2[1]} ${end}`
  210. }
  211. else if (element.curve) {
  212. const mid = element.curve.join(',')
  213. return `M${start} Q${mid} ${end}`
  214. }
  215. else if (element.cubic) {
  216. const [c1, c2] = element.cubic
  217. const p1 = c1.join(',')
  218. const p2 = c2.join(',')
  219. return `M${start} C${p1} ${p2} ${end}`
  220. }
  221. return `M${start} L${end}`
  222. }
  223. /**
  224. * 判断一个元素是否在可视范围内
  225. * @param element 元素
  226. * @param parent 父元素
  227. */
  228. export const isElementInViewport = (element: HTMLElement, parent: HTMLElement): boolean => {
  229. const elementRect = element.getBoundingClientRect()
  230. const parentRect = parent.getBoundingClientRect()
  231. return (
  232. elementRect.top >= parentRect.top &&
  233. elementRect.bottom <= parentRect.bottom
  234. )
  235. }