element.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import tinycolor from 'tinycolor2';
  2. import { nanoid } from 'nanoid';
  3. import type { PPTElement, PPTLineElement, Slide } from '@/lib/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. } else if ('rotate' in element && element.rotate) {
  79. const { left, top, width, height, rotate } = element;
  80. const { xRange, yRange } = getRectRotatedRange({
  81. left,
  82. top,
  83. width,
  84. height,
  85. rotate,
  86. });
  87. minX = xRange[0];
  88. maxX = xRange[1];
  89. minY = yRange[0];
  90. maxY = yRange[1];
  91. } else {
  92. minX = element.left;
  93. maxX = element.left + element.width;
  94. minY = element.top;
  95. maxY = element.top + element.height;
  96. }
  97. return { minX, maxX, minY, maxY };
  98. };
  99. /**
  100. * 计算一组元素在画布中的位置范围
  101. * @param elementList 一组元素信息
  102. */
  103. export const getElementListRange = (elementList: PPTElement[]) => {
  104. const leftValues: number[] = [];
  105. const topValues: number[] = [];
  106. const rightValues: number[] = [];
  107. const bottomValues: number[] = [];
  108. elementList.forEach((element) => {
  109. const { minX, maxX, minY, maxY } = getElementRange(element);
  110. leftValues.push(minX);
  111. topValues.push(minY);
  112. rightValues.push(maxX);
  113. bottomValues.push(maxY);
  114. });
  115. const minX = Math.min(...leftValues);
  116. const maxX = Math.max(...rightValues);
  117. const minY = Math.min(...topValues);
  118. const maxY = Math.max(...bottomValues);
  119. return { minX, maxX, minY, maxY };
  120. };
  121. /**
  122. * 计算线条元素的长度
  123. * @param element 线条元素
  124. */
  125. export const getLineElementLength = (element: PPTLineElement) => {
  126. const deltaX = element.end[0] - element.start[0];
  127. const deltaY = element.end[1] - element.start[1];
  128. const len = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
  129. return len;
  130. };
  131. export interface AlignLine {
  132. value: number;
  133. range: [number, number];
  134. }
  135. /**
  136. * 将一组对齐吸附线进行去重:同位置的的多条对齐吸附线仅留下一条,取该位置所有对齐吸附线的最大值和最小值为新的范围
  137. * @param lines 一组对齐吸附线信息
  138. */
  139. export const uniqAlignLines = (lines: AlignLine[]) => {
  140. const uniqLines: AlignLine[] = [];
  141. lines.forEach((line) => {
  142. const index = uniqLines.findIndex((_line) => _line.value === line.value);
  143. if (index === -1) uniqLines.push(line);
  144. else {
  145. const uniqLine = uniqLines[index];
  146. const rangeMin = Math.min(uniqLine.range[0], line.range[0]);
  147. const rangeMax = Math.max(uniqLine.range[1], line.range[1]);
  148. const range: [number, number] = [rangeMin, rangeMax];
  149. const _line = { value: line.value, range };
  150. uniqLines[index] = _line;
  151. }
  152. });
  153. return uniqLines;
  154. };
  155. /**
  156. * 以页面列表为基础,为每一个页面生成新的ID,并关联到旧ID形成一个字典
  157. * 主要用于页面元素时,维持数据中各处页面ID原有的关系
  158. * @param slides 页面列表
  159. */
  160. export const createSlideIdMap = (slides: Slide[]) => {
  161. const slideIdMap: IdMap = {};
  162. for (const slide of slides) {
  163. slideIdMap[slide.id] = nanoid(10);
  164. }
  165. return slideIdMap;
  166. };
  167. /**
  168. * 以元素列表为基础,为每一个元素生成新的ID,并关联到旧ID形成一个字典
  169. * 主要用于复制元素时,维持数据中各处元素ID原有的关系
  170. * 例如:原本两个组合的元素拥有相同的groupId,复制后依然会拥有另一个相同的groupId
  171. * @param elements 元素列表数据
  172. */
  173. export const createElementIdMap = (elements: PPTElement[]) => {
  174. const groupIdMap: IdMap = {};
  175. const elIdMap: IdMap = {};
  176. for (const element of elements) {
  177. const groupId = element.groupId;
  178. if (groupId && !groupIdMap[groupId]) {
  179. groupIdMap[groupId] = nanoid(10);
  180. }
  181. elIdMap[element.id] = nanoid(10);
  182. }
  183. return {
  184. groupIdMap,
  185. elIdMap,
  186. };
  187. };
  188. /**
  189. * 根据表格的主题色,获取对应用于配色的子颜色
  190. * @param themeColor 主题色
  191. */
  192. export const getTableSubThemeColor = (themeColor: string) => {
  193. const rgba = tinycolor(themeColor);
  194. return [rgba.setAlpha(0.3).toRgbString(), rgba.setAlpha(0.1).toRgbString()];
  195. };
  196. /**
  197. * 获取线条元素路径字符串
  198. * @param element 线条元素
  199. */
  200. export const getLineElementPath = (element: PPTLineElement) => {
  201. // Defensive: ensure start and end are arrays
  202. const startArr = Array.isArray(element.start) ? element.start : [0, 0];
  203. const endArr = Array.isArray(element.end) ? element.end : [100, 100];
  204. const start = startArr.join(',');
  205. const end = endArr.join(',');
  206. if (element.broken) {
  207. const mid = element.broken.join(',');
  208. return `M${start} L${mid} L${end}`;
  209. } else if (element.broken2) {
  210. const { minX, maxX, minY, maxY } = getElementRange(element);
  211. if (maxX - minX >= maxY - minY)
  212. return `M${start} L${element.broken2[0]},${startArr[1]} L${element.broken2[0]},${endArr[1]} ${end}`;
  213. return `M${start} L${startArr[0]},${element.broken2[1]} L${endArr[0]},${element.broken2[1]} ${end}`;
  214. } else if (element.curve) {
  215. const mid = element.curve.join(',');
  216. return `M${start} Q${mid} ${end}`;
  217. } else if (element.cubic) {
  218. const [c1, c2] = element.cubic;
  219. const p1 = c1.join(',');
  220. const p2 = c2.join(',');
  221. return `M${start} C${p1} ${p2} ${end}`;
  222. }
  223. return `M${start} L${end}`;
  224. };
  225. /**
  226. * 判断一个元素是否在可视范围内
  227. * @param element 元素
  228. * @param parent 父元素
  229. */
  230. export const isElementInViewport = (element: HTMLElement, parent: HTMLElement): boolean => {
  231. const elementRect = element.getBoundingClientRect();
  232. const parentRect = parent.getBoundingClientRect();
  233. return elementRect.top >= parentRect.top && elementRect.bottom <= parentRect.bottom;
  234. };