useRotateElement.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import { useCallback, type RefObject } from 'react';
  2. import type {
  3. PPTElement,
  4. PPTLineElement,
  5. PPTVideoElement,
  6. PPTAudioElement,
  7. PPTChartElement,
  8. } from '@/lib/types/slides';
  9. import { useHistorySnapshot } from '@/lib/hooks/use-history-snapshot';
  10. import { useCanvasOperations } from '@/lib/hooks/use-canvas-operations';
  11. /**
  12. * Calculate the angle (in radians) of the line from the origin to the given coordinates
  13. * @param x Coordinate x
  14. * @param y Coordinate y
  15. */
  16. const getAngleFromCoordinate = (x: number, y: number) => {
  17. const radian = Math.atan2(x, y);
  18. const angle = (180 / Math.PI) * radian;
  19. return angle;
  20. };
  21. /**
  22. * Rotate element Hook
  23. *
  24. * @param elementListRef - Element list ref (stores the latest value)
  25. * @param setElementList - Element list setter (used to trigger re-render)
  26. * @param viewportRef - Viewport reference
  27. * @param canvasScale - Canvas scale ratio
  28. */
  29. export function useRotateElement(
  30. elementListRef: React.RefObject<PPTElement[]>,
  31. setElementList: React.Dispatch<React.SetStateAction<PPTElement[]>>,
  32. viewportRef: RefObject<HTMLElement | null>,
  33. canvasScale: number,
  34. ) {
  35. const updateSlide = useCanvasOperations().updateSlide;
  36. const { addHistorySnapshot } = useHistorySnapshot();
  37. // Rotate element
  38. const rotateElement = useCallback(
  39. (
  40. e: React.MouseEvent | React.TouchEvent,
  41. element: Exclude<
  42. PPTElement,
  43. PPTChartElement | PPTLineElement | PPTVideoElement | PPTAudioElement
  44. >,
  45. ) => {
  46. const native = e.nativeEvent;
  47. const isTouchEvent = native instanceof TouchEvent;
  48. if (isTouchEvent && !native.changedTouches?.length) return;
  49. let isMouseDown = true;
  50. let angle = 0;
  51. const elOriginRotate = element.rotate || 0;
  52. const elLeft = element.left;
  53. const elTop = element.top;
  54. const elWidth = element.width;
  55. const elHeight = element.height;
  56. // Element center point (rotation center)
  57. const centerX = elLeft + elWidth / 2;
  58. const centerY = elTop + elHeight / 2;
  59. if (!viewportRef.current) return;
  60. const viewportRect = viewportRef.current.getBoundingClientRect();
  61. const handleMouseMove = (e: MouseEvent | TouchEvent) => {
  62. if (!isMouseDown) return;
  63. const currentPageX = e instanceof MouseEvent ? e.pageX : e.changedTouches[0].pageX;
  64. const currentPageY = e instanceof MouseEvent ? e.pageY : e.changedTouches[0].pageY;
  65. // Calculate the angle of the line from the current mouse position to the element center
  66. const mouseX = (currentPageX - viewportRect.left) / canvasScale;
  67. const mouseY = (currentPageY - viewportRect.top) / canvasScale;
  68. const x = mouseX - centerX;
  69. const y = centerY - mouseY;
  70. angle = getAngleFromCoordinate(x, y);
  71. // Snap to multiples of 45 degrees when close
  72. const sorptionRange = 5;
  73. if (Math.abs(angle) <= sorptionRange) angle = 0;
  74. else if (angle > 0 && Math.abs(angle - 45) <= sorptionRange) angle -= angle - 45;
  75. else if (angle < 0 && Math.abs(angle + 45) <= sorptionRange) angle -= angle + 45;
  76. else if (angle > 0 && Math.abs(angle - 90) <= sorptionRange) angle -= angle - 90;
  77. else if (angle < 0 && Math.abs(angle + 90) <= sorptionRange) angle -= angle + 90;
  78. else if (angle > 0 && Math.abs(angle - 135) <= sorptionRange) angle -= angle - 135;
  79. else if (angle < 0 && Math.abs(angle + 135) <= sorptionRange) angle -= angle + 135;
  80. else if (angle > 0 && Math.abs(angle - 180) <= sorptionRange) angle -= angle - 180;
  81. else if (angle < 0 && Math.abs(angle + 180) <= sorptionRange) angle -= angle + 180;
  82. const newElements = elementListRef.current.map((el) => {
  83. if (el.id === element.id && 'rotate' in el) {
  84. return { ...el, rotate: angle };
  85. }
  86. return el;
  87. });
  88. // Update both ref and state
  89. elementListRef.current = newElements;
  90. setElementList(newElements);
  91. };
  92. const handleMouseUp = () => {
  93. isMouseDown = false;
  94. document.onmousemove = null;
  95. document.onmouseup = null;
  96. document.ontouchmove = null;
  97. document.ontouchend = null;
  98. if (elOriginRotate === angle) return;
  99. updateSlide({ elements: elementListRef.current });
  100. addHistorySnapshot();
  101. };
  102. if (isTouchEvent) {
  103. document.ontouchmove = handleMouseMove;
  104. document.ontouchend = handleMouseUp;
  105. } else {
  106. document.onmousemove = handleMouseMove;
  107. document.onmouseup = handleMouseUp;
  108. }
  109. },
  110. [elementListRef, setElementList, viewportRef, canvasScale, updateSlide, addHistorySnapshot],
  111. );
  112. return {
  113. rotateElement,
  114. };
  115. }