canvas.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. import { create } from 'zustand';
  2. import { createSelectors } from '@/lib/utils/create-selectors';
  3. import type { TextAttrs } from '@/lib/prosemirror/utils';
  4. import { defaultRichTextAttrs } from '@/lib/prosemirror/utils';
  5. import type { TextFormatPainter, ShapeFormatPainter, CreatingElement } from '@/lib/types/edit';
  6. import type { PercentageGeometry } from '@/lib/types/action';
  7. /**
  8. * Spotlight options
  9. */
  10. export interface SpotlightOptions {
  11. radius?: number; // Spotlight radius (pixels)
  12. dimness?: number; // Background dimming level (0-1)
  13. transition?: number; // Transition animation duration (milliseconds)
  14. }
  15. /**
  16. * Highlight overlay options
  17. */
  18. export interface HighlightOverlayOptions {
  19. color?: string; // Highlight color
  20. opacity?: number; // Highlight opacity (0-1)
  21. borderWidth?: number; // Border width
  22. animated?: boolean; // Whether to animate
  23. }
  24. /**
  25. * Laser pointer options
  26. */
  27. export interface LaserOptions {
  28. color?: string; // Laser pointer color, default red
  29. duration?: number; // Duration (milliseconds)
  30. }
  31. /**
  32. * Canvas Store - Manages all UI state of the Canvas editor
  33. *
  34. * Responsibilities:
  35. * - Element selection state (selected, handling, editing)
  36. * - Canvas viewport state (zoom, drag, ruler, grid)
  37. * - Toolbar and panel state
  38. * - Element being created
  39. * - Rich text editing state
  40. * - Format painter state
  41. *
  42. * Note: Does not manage slide data (elements, background, etc.), which is managed by Scene Context
  43. */
  44. // ==================== Store Interface ====================
  45. interface CanvasState {
  46. // ===== Element selection state =====
  47. activeElementIdList: string[]; // Currently selected element IDs
  48. handleElementId: string; // Element being operated (drag, resize, etc.)
  49. activeGroupElementId: string; // Selected child element within a group
  50. editingElementId: string; // Element being edited (e.g., text editing)
  51. hiddenElementIdList: string[]; // Hidden element IDs
  52. // ===== Teaching feature state =====
  53. spotlightElementId: string; // Element focused by spotlight
  54. spotlightOptions: SpotlightOptions | null; // Spotlight configuration
  55. spotlightMode: 'pixel' | 'percentage'; // Spotlight mode: pixel or percentage
  56. spotlightPercentageGeometry: PercentageGeometry | null; // Percentage mode geometry info
  57. highlightedElementIds: string[]; // Highlighted element IDs
  58. highlightOptions: HighlightOverlayOptions | null; // Highlight configuration
  59. laserElementId: string; // Element focused by laser pointer
  60. laserOptions: LaserOptions | null; // Laser pointer configuration
  61. zoomTarget: { elementId: string; scale: number } | null; // Zoom target
  62. // ===== Canvas viewport state =====
  63. canvasScale: number; // Canvas actual zoom scale
  64. canvasPercentage: number; // Canvas percentage (used to calculate canvasScale)
  65. viewportSize: number; // Viewport width base (default 1000px)
  66. viewportRatio: number; // Viewport aspect ratio (default 0.5625, i.e. 16:9)
  67. canvasDragged: boolean; // Whether canvas is being dragged
  68. // ===== Display aids =====
  69. showRuler: boolean; // Show ruler
  70. gridLineSize: number; // Grid line size (0 means hidden)
  71. // ===== Toolbar and panels =====
  72. toolbarState: 'design' | 'ai' | 'elAnimation'; // Right toolbar state
  73. showSelectPanel: boolean; // Selection panel
  74. showSearchPanel: boolean; // Find and replace panel
  75. // ===== Element creation =====
  76. creatingElement: CreatingElement | null; // Element being created (needs draw-to-insert)
  77. creatingCustomShape: boolean; // Drawing custom shape (arbitrary polygon)
  78. // ===== Editing state =====
  79. isScaling: boolean; // Element scaling in progress
  80. clipingImageElementId: string; // Image being cropped
  81. richTextAttrs: TextAttrs; // Rich text editing state
  82. // ===== Format painter =====
  83. textFormatPainter: TextFormatPainter | null; // Text format painter
  84. shapeFormatPainter: ShapeFormatPainter | null; // Shape format painter
  85. // ===== Video playback =====
  86. playingVideoElementId: string; // Video element currently playing
  87. // ===== Whiteboard =====
  88. whiteboardOpen: boolean; // Whether whiteboard is open
  89. whiteboardClearing: boolean; // Whiteboard clear animation in progress
  90. // ===== Other =====
  91. thumbnailsFocus: boolean; // Whether left thumbnail area is focused
  92. editorAreaFocus: boolean; // Whether editor area is focused
  93. disableHotkeys: boolean; // Whether hotkeys are disabled
  94. selectedTableCells: string[]; // Selected table cells
  95. // ===== Actions =====
  96. // ----- Element selection -----
  97. setActiveElementIdList: (ids: string[]) => void;
  98. setHandleElementId: (id: string) => void;
  99. setActiveGroupElementId: (id: string) => void;
  100. setEditingElementId: (id: string) => void;
  101. setHiddenElementIdList: (ids: string[]) => void;
  102. clearSelection: () => void; // Clear all selections
  103. // ----- Canvas viewport -----
  104. setCanvasScale: (scale: number) => void;
  105. setCanvasPercentage: (percentage: number) => void;
  106. setViewportSize: (size: number) => void;
  107. setViewportRatio: (ratio: number) => void;
  108. setCanvasDragged: (dragged: boolean) => void;
  109. // ----- Display aids -----
  110. setRulerState: (show: boolean) => void;
  111. setGridLineSize: (size: number) => void;
  112. // ----- Toolbar and panels -----
  113. setToolbarState: (state: 'design' | 'ai') => void;
  114. setSelectPanelState: (show: boolean) => void;
  115. setSearchPanelState: (show: boolean) => void;
  116. // ----- Element creation -----
  117. setCreatingElement: (element: CreatingElement | null) => void;
  118. setCreatingCustomShapeState: (creating: boolean) => void;
  119. // ----- Editing state -----
  120. setScalingState: (isScaling: boolean) => void;
  121. setClipingImageElementId: (id: string) => void;
  122. setRichtextAttrs: (attrs: TextAttrs) => void;
  123. // ----- Format painter -----
  124. setTextFormatPainter: (painter: TextFormatPainter | null) => void;
  125. setShapeFormatPainter: (painter: ShapeFormatPainter | null) => void;
  126. // ----- Video playback -----
  127. playVideo: (elementId: string) => void;
  128. pauseVideo: () => void;
  129. // ----- Whiteboard -----
  130. setWhiteboardOpen: (open: boolean) => void;
  131. setWhiteboardClearing: (clearing: boolean) => void;
  132. // ----- Other -----
  133. setThumbnailsFocus: (focus: boolean) => void;
  134. setEditorAreaFocus: (focus: boolean) => void;
  135. setDisableHotkeysState: (disable: boolean) => void;
  136. setSelectedTableCells: (cells: string[]) => void;
  137. // ----- Teaching features -----
  138. setSpotlight: (elementId: string, options?: SpotlightOptions) => void;
  139. clearSpotlight: () => void;
  140. setSpotlightPercentage: (
  141. elementId: string,
  142. geometry: PercentageGeometry,
  143. options?: SpotlightOptions,
  144. ) => void;
  145. setHighlight: (elementIds: string[], options?: HighlightOverlayOptions) => void;
  146. clearHighlight: () => void;
  147. setLaser: (elementId: string, options?: LaserOptions) => void;
  148. clearLaser: () => void;
  149. setZoom: (elementId: string, scale: number) => void;
  150. clearZoom: () => void;
  151. clearAllEffects: () => void;
  152. // ----- Batch operations -----
  153. resetCanvasState: () => void; // Reset Canvas state (used when switching scenes)
  154. }
  155. // ==================== Initial State ====================
  156. const initialState = {
  157. // Element selection
  158. activeElementIdList: [],
  159. handleElementId: '',
  160. activeGroupElementId: '',
  161. editingElementId: '',
  162. hiddenElementIdList: [],
  163. // Canvas viewport
  164. canvasScale: 1,
  165. canvasPercentage: 90,
  166. viewportSize: 1000,
  167. viewportRatio: 0.5625, // 16:9
  168. canvasDragged: false,
  169. // Display aids
  170. showRuler: false,
  171. gridLineSize: 0,
  172. // Toolbar and panels
  173. toolbarState: 'ai' as const,
  174. showSelectPanel: false,
  175. showSearchPanel: false,
  176. // Element creation
  177. creatingElement: null,
  178. creatingCustomShape: false,
  179. // Editing state
  180. isScaling: false,
  181. clipingImageElementId: '',
  182. richTextAttrs: defaultRichTextAttrs,
  183. // Format painter
  184. textFormatPainter: null,
  185. shapeFormatPainter: null,
  186. // Video playback
  187. playingVideoElementId: '',
  188. // Whiteboard
  189. whiteboardOpen: false,
  190. whiteboardClearing: false,
  191. // Other: false,
  192. editorAreaFocus: false,
  193. thumbnailsFocus: false,
  194. disableHotkeys: false,
  195. selectedTableCells: [],
  196. // Teaching features
  197. spotlightElementId: '',
  198. spotlightOptions: null,
  199. spotlightMode: 'pixel' as const,
  200. spotlightPercentageGeometry: null,
  201. highlightedElementIds: [],
  202. highlightOptions: null,
  203. laserElementId: '',
  204. laserOptions: null,
  205. zoomTarget: null,
  206. };
  207. // ==================== Store Implementation ====================
  208. const useCanvasStoreBase = create<CanvasState>((set, get) => ({
  209. ...initialState,
  210. // ===== Element Selection Actions =====
  211. setActiveElementIdList: (ids) => {
  212. set({ activeElementIdList: ids });
  213. // Auto-set handleElementId: set to that element for single select, empty for multi-select or none
  214. if (ids.length === 1) {
  215. set({ handleElementId: ids[0] });
  216. } else if (ids.length === 0) {
  217. set({ handleElementId: '' });
  218. }
  219. // Auto-switch to design panel when elements are selected
  220. if (ids.length > 0) {
  221. set({ toolbarState: 'design' });
  222. }
  223. },
  224. setHandleElementId: (id) => set({ handleElementId: id }),
  225. setActiveGroupElementId: (id) => set({ activeGroupElementId: id }),
  226. setEditingElementId: (id) => set({ editingElementId: id }),
  227. setHiddenElementIdList: (ids) => set({ hiddenElementIdList: ids }),
  228. clearSelection: () => {
  229. set({
  230. activeElementIdList: [],
  231. handleElementId: '',
  232. activeGroupElementId: '',
  233. editingElementId: '',
  234. });
  235. },
  236. // ===== Canvas Viewport Actions =====
  237. setCanvasScale: (scale) => set({ canvasScale: scale }),
  238. setCanvasPercentage: (percentage) => set({ canvasPercentage: percentage }),
  239. setViewportSize: (size) => set({ viewportSize: size }),
  240. setViewportRatio: (ratio) => set({ viewportRatio: ratio }),
  241. setCanvasDragged: (dragged) => set({ canvasDragged: dragged }),
  242. // ===== Display Aids Actions =====
  243. setRulerState: (show) => set({ showRuler: show }),
  244. setGridLineSize: (size) => set({ gridLineSize: size }),
  245. // ===== Toolbar and Panel Actions =====
  246. setToolbarState: (toolbarState) => set({ toolbarState }),
  247. setSelectPanelState: (show) => set({ showSelectPanel: show }),
  248. setSearchPanelState: (show) => set({ showSearchPanel: show }),
  249. // ===== Element Creation Actions =====
  250. setCreatingElement: (element) => set({ creatingElement: element }),
  251. setCreatingCustomShapeState: (creating) => set({ creatingCustomShape: creating }),
  252. // ===== Editing State Actions =====
  253. setScalingState: (isScaling) => set({ isScaling }),
  254. setClipingImageElementId: (id) => set({ clipingImageElementId: id }),
  255. setRichtextAttrs: (attrs) => set({ richTextAttrs: attrs }),
  256. // ===== Format Painter Actions =====
  257. setTextFormatPainter: (painter) => set({ textFormatPainter: painter }),
  258. setShapeFormatPainter: (painter) => set({ shapeFormatPainter: painter }),
  259. // ===== Video Playback Actions =====
  260. playVideo: (elementId) => set({ playingVideoElementId: elementId }),
  261. pauseVideo: () => set({ playingVideoElementId: '' }),
  262. // ===== Whiteboard Actions =====
  263. setWhiteboardOpen: (open) => set({ whiteboardOpen: open }),
  264. setWhiteboardClearing: (clearing) => set({ whiteboardClearing: clearing }),
  265. // ===== Other Actions =====
  266. setThumbnailsFocus: (focus) => set({ thumbnailsFocus: focus }),
  267. setEditorAreaFocus: (focus) => set({ editorAreaFocus: focus }),
  268. setDisableHotkeysState: (disable) => set({ disableHotkeys: disable }),
  269. setSelectedTableCells: (cells) => set({ selectedTableCells: cells }),
  270. // ===== Teaching Feature Actions =====
  271. setSpotlight: (elementId, options = {}) => {
  272. set({
  273. spotlightElementId: elementId,
  274. spotlightMode: 'pixel',
  275. spotlightOptions: {
  276. radius: 200,
  277. dimness: 0.7,
  278. transition: 300,
  279. ...options,
  280. },
  281. spotlightPercentageGeometry: null,
  282. });
  283. },
  284. setSpotlightPercentage: (elementId, geometry, options = {}) => {
  285. set({
  286. spotlightElementId: elementId,
  287. spotlightMode: 'percentage',
  288. spotlightPercentageGeometry: geometry,
  289. spotlightOptions: {
  290. dimness: 0.7,
  291. transition: 300,
  292. ...options,
  293. },
  294. });
  295. },
  296. clearSpotlight: () => {
  297. set({
  298. spotlightElementId: '',
  299. spotlightOptions: null,
  300. spotlightMode: 'pixel',
  301. spotlightPercentageGeometry: null,
  302. });
  303. },
  304. setHighlight: (elementIds, options = {}) => {
  305. set({
  306. highlightedElementIds: elementIds,
  307. highlightOptions: {
  308. color: '#ff6b6b',
  309. opacity: 0.3,
  310. borderWidth: 3,
  311. animated: true,
  312. ...options,
  313. },
  314. });
  315. },
  316. clearHighlight: () => {
  317. set({
  318. highlightedElementIds: [],
  319. highlightOptions: null,
  320. });
  321. },
  322. setLaser: (elementId, options = {}) => {
  323. set({
  324. laserElementId: elementId,
  325. laserOptions: {
  326. color: '#ff0000',
  327. duration: 3000,
  328. ...options,
  329. },
  330. });
  331. },
  332. clearLaser: () => {
  333. set({
  334. laserElementId: '',
  335. laserOptions: null,
  336. });
  337. },
  338. setZoom: (elementId, scale) => {
  339. set({
  340. zoomTarget: { elementId, scale },
  341. });
  342. },
  343. clearZoom: () => {
  344. set({
  345. zoomTarget: null,
  346. });
  347. },
  348. clearAllEffects: () => {
  349. set({
  350. spotlightElementId: '',
  351. spotlightOptions: null,
  352. spotlightMode: 'pixel',
  353. spotlightPercentageGeometry: null,
  354. highlightedElementIds: [],
  355. highlightOptions: null,
  356. laserElementId: '',
  357. laserOptions: null,
  358. zoomTarget: null,
  359. // Note: playingVideoElementId intentionally NOT cleared here.
  360. // Video playback has its own lifecycle (playVideo/pauseVideo/onEnded)
  361. // and must not be interrupted by visual effect auto-clear timers.
  362. });
  363. },
  364. // ===== Batch Operations =====
  365. resetCanvasState: () => {
  366. set({
  367. ...initialState,
  368. // Preserve viewport settings
  369. viewportSize: get().viewportSize,
  370. viewportRatio: get().viewportRatio,
  371. });
  372. },
  373. }));
  374. // Enhance store with selectors, supporting store.use.xxx() syntax
  375. export const useCanvasStore = createSelectors(useCanvasStoreBase);