canvasengine 2.0.0-beta.5 → 2.0.0-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/components/Button.d.ts +185 -0
  2. package/dist/components/Button.d.ts.map +1 -0
  3. package/dist/components/Canvas.d.ts +17 -0
  4. package/dist/components/Canvas.d.ts.map +1 -0
  5. package/dist/components/Container.d.ts +86 -0
  6. package/dist/components/Container.d.ts.map +1 -0
  7. package/dist/components/DOMContainer.d.ts +98 -0
  8. package/dist/components/DOMContainer.d.ts.map +1 -0
  9. package/dist/components/DOMElement.d.ts +54 -0
  10. package/dist/components/DOMElement.d.ts.map +1 -0
  11. package/dist/components/DOMSprite.d.ts +127 -0
  12. package/dist/components/DOMSprite.d.ts.map +1 -0
  13. package/dist/components/DisplayObject.d.ts +94 -0
  14. package/dist/components/DisplayObject.d.ts.map +1 -0
  15. package/dist/components/FocusContainer.d.ts +129 -0
  16. package/dist/components/FocusContainer.d.ts.map +1 -0
  17. package/dist/components/Graphic.d.ts +64 -0
  18. package/dist/components/Graphic.d.ts.map +1 -0
  19. package/dist/components/Joystick.d.ts +36 -0
  20. package/dist/components/Joystick.d.ts.map +1 -0
  21. package/dist/components/Mesh.d.ts +208 -0
  22. package/dist/components/Mesh.d.ts.map +1 -0
  23. package/dist/components/NineSliceSprite.d.ts +16 -0
  24. package/dist/components/NineSliceSprite.d.ts.map +1 -0
  25. package/dist/components/ParticleEmitter.d.ts +4 -0
  26. package/dist/components/ParticleEmitter.d.ts.map +1 -0
  27. package/dist/components/Scene.d.ts +2 -0
  28. package/dist/components/Scene.d.ts.map +1 -0
  29. package/dist/components/Sprite.d.ts +242 -0
  30. package/dist/components/Sprite.d.ts.map +1 -0
  31. package/dist/components/Text.d.ts +25 -0
  32. package/dist/components/Text.d.ts.map +1 -0
  33. package/dist/components/TilingSprite.d.ts +17 -0
  34. package/dist/components/TilingSprite.d.ts.map +1 -0
  35. package/dist/components/Video.d.ts +14 -0
  36. package/dist/components/Video.d.ts.map +1 -0
  37. package/dist/components/Viewport.d.ts +121 -0
  38. package/dist/components/Viewport.d.ts.map +1 -0
  39. package/dist/components/index.d.ts +20 -0
  40. package/dist/components/index.d.ts.map +1 -0
  41. package/dist/components/types/DisplayObject.d.ts +106 -0
  42. package/dist/components/types/DisplayObject.d.ts.map +1 -0
  43. package/dist/components/types/MouseEvent.d.ts +4 -0
  44. package/dist/components/types/MouseEvent.d.ts.map +1 -0
  45. package/dist/components/types/Spritesheet.d.ts +248 -0
  46. package/dist/components/types/Spritesheet.d.ts.map +1 -0
  47. package/dist/components/types/index.d.ts +4 -0
  48. package/dist/components/types/index.d.ts.map +1 -0
  49. package/dist/directives/Controls.d.ts +112 -0
  50. package/dist/directives/Controls.d.ts.map +1 -0
  51. package/dist/directives/ControlsBase.d.ts +199 -0
  52. package/dist/directives/ControlsBase.d.ts.map +1 -0
  53. package/dist/directives/Drag.d.ts +69 -0
  54. package/dist/directives/Drag.d.ts.map +1 -0
  55. package/dist/directives/Flash.d.ts +116 -0
  56. package/dist/directives/Flash.d.ts.map +1 -0
  57. package/dist/directives/FocusNavigation.d.ts +52 -0
  58. package/dist/directives/FocusNavigation.d.ts.map +1 -0
  59. package/dist/directives/GamepadControls.d.ts +224 -0
  60. package/dist/directives/GamepadControls.d.ts.map +1 -0
  61. package/dist/directives/JoystickControls.d.ts +171 -0
  62. package/dist/directives/JoystickControls.d.ts.map +1 -0
  63. package/dist/directives/KeyboardControls.d.ts +219 -0
  64. package/dist/directives/KeyboardControls.d.ts.map +1 -0
  65. package/dist/directives/Scheduler.d.ts +35 -0
  66. package/dist/directives/Scheduler.d.ts.map +1 -0
  67. package/dist/directives/Shake.d.ts +98 -0
  68. package/dist/directives/Shake.d.ts.map +1 -0
  69. package/dist/directives/Sound.d.ts +25 -0
  70. package/dist/directives/Sound.d.ts.map +1 -0
  71. package/dist/directives/Transition.d.ts +10 -0
  72. package/dist/directives/Transition.d.ts.map +1 -0
  73. package/dist/directives/ViewportCull.d.ts +11 -0
  74. package/dist/directives/ViewportCull.d.ts.map +1 -0
  75. package/dist/directives/ViewportFollow.d.ts +18 -0
  76. package/dist/directives/ViewportFollow.d.ts.map +1 -0
  77. package/dist/directives/index.d.ts +13 -0
  78. package/dist/directives/index.d.ts.map +1 -0
  79. package/dist/engine/FocusManager.d.ts +174 -0
  80. package/dist/engine/FocusManager.d.ts.map +1 -0
  81. package/dist/engine/animation.d.ts +72 -0
  82. package/dist/engine/animation.d.ts.map +1 -0
  83. package/dist/engine/bootstrap.d.ts +48 -0
  84. package/dist/engine/bootstrap.d.ts.map +1 -0
  85. package/dist/engine/directive.d.ts +13 -0
  86. package/dist/engine/directive.d.ts.map +1 -0
  87. package/dist/engine/reactive.d.ts +134 -0
  88. package/dist/engine/reactive.d.ts.map +1 -0
  89. package/dist/engine/signal.d.ts +71 -0
  90. package/dist/engine/signal.d.ts.map +1 -0
  91. package/dist/engine/trigger.d.ts +54 -0
  92. package/dist/engine/trigger.d.ts.map +1 -0
  93. package/dist/engine/utils.d.ts +89 -0
  94. package/dist/engine/utils.d.ts.map +1 -0
  95. package/dist/hooks/addContext.d.ts +2 -0
  96. package/dist/hooks/addContext.d.ts.map +1 -0
  97. package/dist/hooks/useFocus.d.ts +60 -0
  98. package/dist/hooks/useFocus.d.ts.map +1 -0
  99. package/dist/hooks/useProps.d.ts +42 -0
  100. package/dist/hooks/useProps.d.ts.map +1 -0
  101. package/dist/hooks/useRef.d.ts +4 -0
  102. package/dist/hooks/useRef.d.ts.map +1 -0
  103. package/dist/index-DaGekQUW.js +2218 -0
  104. package/dist/index-DaGekQUW.js.map +1 -0
  105. package/dist/index.d.ts +19 -1099
  106. package/dist/index.d.ts.map +1 -0
  107. package/dist/index.global.js +5 -0
  108. package/dist/index.global.js.map +1 -0
  109. package/dist/index.js +11749 -2901
  110. package/dist/index.js.map +1 -1
  111. package/dist/utils/Ease.d.ts +17 -0
  112. package/dist/utils/Ease.d.ts.map +1 -0
  113. package/dist/utils/GlobalAssetLoader.d.ts +141 -0
  114. package/dist/utils/GlobalAssetLoader.d.ts.map +1 -0
  115. package/dist/utils/RadialGradient.d.ts +57 -0
  116. package/dist/utils/RadialGradient.d.ts.map +1 -0
  117. package/dist/utils/functions.d.ts +2 -0
  118. package/dist/utils/functions.d.ts.map +1 -0
  119. package/dist/utils/tabindex.d.ts +16 -0
  120. package/dist/utils/tabindex.d.ts.map +1 -0
  121. package/package.json +13 -7
  122. package/src/components/Button.ts +399 -0
  123. package/src/components/Canvas.ts +62 -46
  124. package/src/components/Container.ts +21 -2
  125. package/src/components/DOMContainer.ts +379 -0
  126. package/src/components/DOMElement.ts +556 -0
  127. package/src/components/DOMSprite.ts +1040 -0
  128. package/src/components/DisplayObject.ts +392 -201
  129. package/src/components/FocusContainer.ts +368 -0
  130. package/src/components/Graphic.ts +227 -66
  131. package/src/components/Joystick.ts +363 -0
  132. package/src/components/Mesh.ts +222 -0
  133. package/src/components/NineSliceSprite.ts +4 -1
  134. package/src/components/ParticleEmitter.ts +12 -8
  135. package/src/components/Sprite.ts +297 -31
  136. package/src/components/Text.ts +125 -18
  137. package/src/components/Video.ts +2 -2
  138. package/src/components/Viewport.ts +118 -63
  139. package/src/components/index.ts +9 -2
  140. package/src/components/types/DisplayObject.ts +41 -4
  141. package/src/components/types/Spritesheet.ts +0 -118
  142. package/src/directives/Controls.ts +254 -0
  143. package/src/directives/ControlsBase.ts +267 -0
  144. package/src/directives/Drag.ts +357 -52
  145. package/src/directives/Flash.ts +419 -0
  146. package/src/directives/FocusNavigation.ts +113 -0
  147. package/src/directives/GamepadControls.ts +537 -0
  148. package/src/directives/JoystickControls.ts +396 -0
  149. package/src/directives/KeyboardControls.ts +85 -430
  150. package/src/directives/Scheduler.ts +12 -4
  151. package/src/directives/Shake.ts +298 -0
  152. package/src/directives/Sound.ts +94 -31
  153. package/src/directives/ViewportFollow.ts +40 -9
  154. package/src/directives/index.ts +12 -6
  155. package/src/engine/FocusManager.ts +510 -0
  156. package/src/engine/animation.ts +175 -21
  157. package/src/engine/bootstrap.ts +93 -3
  158. package/src/engine/directive.ts +4 -4
  159. package/src/engine/reactive.ts +901 -161
  160. package/src/engine/signal.ts +113 -25
  161. package/src/engine/trigger.ts +34 -7
  162. package/src/engine/utils.ts +19 -3
  163. package/src/hooks/useFocus.ts +91 -0
  164. package/src/hooks/useProps.ts +1 -1
  165. package/src/index.ts +8 -2
  166. package/src/types/pixi-cull.d.ts +7 -0
  167. package/src/utils/GlobalAssetLoader.ts +257 -0
  168. package/src/utils/functions.ts +7 -0
  169. package/src/utils/tabindex.ts +70 -0
  170. package/testing/index.ts +35 -4
  171. package/tsconfig.json +18 -0
  172. package/vite.config.ts +39 -0
@@ -0,0 +1,510 @@
1
+ import { isSignal, signal, Signal, WritableSignal, WritableObjectSignal } from "@signe/reactive";
2
+ import { Element, isElementFrozen } from "./reactive";
3
+ import { CanvasViewport } from "../components/Viewport";
4
+ import { SignalOrPrimitive } from "../components/types";
5
+
6
+ /**
7
+ * Options for scroll behavior when navigating to focused elements
8
+ *
9
+ * @property padding - Padding around the element in pixels (default: 0)
10
+ * @property smooth - Enable smooth scrolling animation (default: false)
11
+ * @property center - Center the element in the viewport (default: true)
12
+ * @property duration - Animation duration in ms if smooth=true (default: 300)
13
+ */
14
+ export interface ScrollOptions {
15
+ padding?: number;
16
+ smooth?: boolean;
17
+ center?: boolean;
18
+ duration?: number;
19
+ }
20
+
21
+ /**
22
+ * Data structure for a focus container
23
+ */
24
+ type WritableElementSignal = WritableSignal<Element | null> | WritableObjectSignal<Element | null>;
25
+
26
+ interface FocusContainerData {
27
+ id: string;
28
+ element?: Element<any>;
29
+ focusables: Map<number, Element<any>>;
30
+ currentIndex: WritableSignal<number | null>;
31
+ focusedElement: WritableElementSignal;
32
+ onFocusChange?: (index: number, element: Element | null) => void;
33
+ autoScroll?: boolean | ScrollOptions;
34
+ viewport?: CanvasViewport;
35
+ tabindex?: SignalOrPrimitive<number> | null;
36
+ tabindexSubscription?: any;
37
+ pendingIndex?: number;
38
+ }
39
+
40
+ /**
41
+ * Central manager for focus navigation system
42
+ *
43
+ * Manages focusable elements within containers, handles navigation,
44
+ * and provides scroll integration with Viewport.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * const manager = FocusManager.getInstance();
49
+ * manager.registerContainer('menu', containerData);
50
+ * manager.navigate('menu', 'next');
51
+ * ```
52
+ */
53
+ export class FocusManager {
54
+ private static instance: FocusManager | null = null;
55
+ private containers: Map<string, FocusContainerData> = new Map();
56
+ private scrollAnimations: Map<string, { startTime: number; startX: number; startY: number; targetX: number; targetY: number; duration: number }> = new Map();
57
+
58
+ /**
59
+ * Get the singleton instance of FocusManager
60
+ *
61
+ * @returns The FocusManager instance
62
+ */
63
+ static getInstance(): FocusManager {
64
+ if (!FocusManager.instance) {
65
+ FocusManager.instance = new FocusManager();
66
+ }
67
+ return FocusManager.instance;
68
+ }
69
+
70
+ /**
71
+ * Register a focus container
72
+ *
73
+ * @param id - Unique identifier for the container
74
+ * @param data - Container data including signals and callbacks
75
+ */
76
+ registerContainer(id: string, data: Omit<FocusContainerData, 'id'>): void {
77
+ this.containers.set(id, { ...data, id });
78
+ }
79
+
80
+ /**
81
+ * Update a focus container's data
82
+ *
83
+ * @param id - Container identifier
84
+ * @param data - Partial container data to update
85
+ */
86
+ updateContainer(id: string, data: Partial<Omit<FocusContainerData, 'id'>>): void {
87
+ const container = this.containers.get(id);
88
+ if (container) {
89
+ this.containers.set(id, { ...container, ...data });
90
+ }
91
+ }
92
+
93
+ setTabindex(id: string, tabindex?: SignalOrPrimitive<number> | null): void {
94
+ const container = this.containers.get(id);
95
+ if (!container) return;
96
+
97
+ // Cleanup previous subscription
98
+ if (container.tabindexSubscription) {
99
+ container.tabindexSubscription.unsubscribe();
100
+ }
101
+
102
+ if (tabindex === undefined || tabindex === null) {
103
+ container.tabindex = undefined;
104
+ return;
105
+ }
106
+
107
+ container.tabindex = tabindex;
108
+
109
+ const currentTabindex = isSignal(tabindex) ? (tabindex as Signal<number>)() : tabindex;
110
+ if (typeof currentTabindex === "number") {
111
+ this.setIndex(id, currentTabindex);
112
+ }
113
+
114
+ if (isSignal(tabindex)) {
115
+ container.tabindexSubscription = ((tabindex as Signal<number>).observable as any).subscribe((value: any) => {
116
+ if (value !== null && value !== container.currentIndex()) {
117
+ this.setIndex(id, value);
118
+ }
119
+ });
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Unregister a focus container
125
+ *
126
+ * @param id - Container identifier to remove
127
+ */
128
+ unregisterContainer(id: string): void {
129
+ this.containers.delete(id);
130
+ this.scrollAnimations.delete(id);
131
+ }
132
+
133
+ /**
134
+ * Register a focusable element in a container
135
+ *
136
+ * @param containerId - Container identifier
137
+ * @param element - Element to register
138
+ * @param index - Focus index for this element
139
+ */
140
+ registerFocusable(containerId: string, element: Element, index: number): void {
141
+ const container = this.containers.get(containerId);
142
+ if (!container) {
143
+ console.warn(`FocusContainer with id "${containerId}" not found`);
144
+ return;
145
+ }
146
+ container.focusables.set(index, element);
147
+
148
+ // If this is the index we are supposed to be at, set it now
149
+ const currentTabindex = isSignal(container.tabindex) ? (container.tabindex as Signal<number>)() : container.tabindex;
150
+ if (container.pendingIndex === index || (currentTabindex === index && container.currentIndex() === null)) {
151
+ container.pendingIndex = undefined;
152
+ this.applyFocus(container, containerId, index, element);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Unregister a focusable element from a container
158
+ *
159
+ * @param containerId - Container identifier
160
+ * @param index - Focus index to remove
161
+ */
162
+ unregisterFocusable(containerId: string, index: number): void {
163
+ const container = this.containers.get(containerId);
164
+ if (!container) return;
165
+ container.focusables.delete(index);
166
+ }
167
+
168
+ /**
169
+ * Navigate to next or previous focusable element
170
+ *
171
+ * @param containerId - Container identifier
172
+ * @param direction - Navigation direction ('next' or 'previous')
173
+ */
174
+ navigate(containerId: string, direction: 'next' | 'previous'): void {
175
+ const container = this.containers.get(containerId);
176
+ if (!container) {
177
+ return;
178
+ }
179
+
180
+ // Check if container is frozen (including parent containers)
181
+ if (container.element && isElementFrozen(container.element)) {
182
+ return;
183
+ }
184
+
185
+ const currentIndex = container.currentIndex();
186
+ const focusableIndices = Array.from(container.focusables.keys()).sort((a, b) => a - b);
187
+
188
+ if (focusableIndices.length === 0) return;
189
+
190
+ let newIndex: number | null = null;
191
+
192
+ if (currentIndex === null) {
193
+ // No current focus, go to first or last
194
+ newIndex = direction === 'next' ? focusableIndices[0] : focusableIndices[focusableIndices.length - 1];
195
+ } else {
196
+ const currentIndexPos = focusableIndices.indexOf(currentIndex);
197
+ if (direction === 'next') {
198
+ if (currentIndexPos < focusableIndices.length - 1) {
199
+ newIndex = focusableIndices[currentIndexPos + 1];
200
+ } else {
201
+ // Wrap around to first
202
+ newIndex = focusableIndices[0];
203
+ }
204
+ } else {
205
+ if (currentIndexPos > 0) {
206
+ newIndex = focusableIndices[currentIndexPos - 1];
207
+ } else {
208
+ // Wrap around to last
209
+ newIndex = focusableIndices[focusableIndices.length - 1];
210
+ }
211
+ }
212
+ }
213
+
214
+ if (newIndex !== null) {
215
+ const tabindex = container.tabindex;
216
+ if (isSignal(tabindex) && typeof (tabindex as any).set === "function") {
217
+ (tabindex as any).set(newIndex);
218
+ } else {
219
+ this.setIndex(containerId, newIndex);
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Set the focus index for a container
226
+ *
227
+ * @param containerId - Container identifier
228
+ * @param index - Focus index to set
229
+ */
230
+ setIndex(containerId: string, index: number): void {
231
+ const container = this.containers.get(containerId);
232
+ if (!container) return;
233
+
234
+ const element = container.focusables.get(index);
235
+ if (!element) {
236
+ container.pendingIndex = index;
237
+ return;
238
+ }
239
+ this.applyFocus(container, containerId, index, element);
240
+ }
241
+
242
+ private applyFocus(
243
+ container: FocusContainerData,
244
+ containerId: string,
245
+ index: number,
246
+ element: Element
247
+ ): void {
248
+ container.currentIndex.set(index);
249
+ container.focusedElement.set(element);
250
+
251
+ // Sync back to tabindex signal if it exists
252
+ const tabindex = container.tabindex;
253
+ if (isSignal(tabindex) && (tabindex as any)() !== index && typeof (tabindex as any).set === "function") {
254
+ (tabindex as any).set(index);
255
+ }
256
+
257
+ // Trigger callback
258
+ if (container.onFocusChange) {
259
+ container.onFocusChange(index, element);
260
+ }
261
+ // Handle DOM focus and scrolling
262
+ const instance = element.componentInstance as any;
263
+ if (instance && instance.element && typeof instance.element.focus === 'function') {
264
+ const domElement = instance.element as HTMLElement;
265
+ // Focus the native DOM element so :focus styles apply
266
+ domElement.focus();
267
+
268
+ // Scroll the element into view, centering it in the scrollable parent
269
+ if (typeof domElement.scrollIntoView === 'function') {
270
+ domElement.scrollIntoView({
271
+ block: 'center',
272
+ behavior: 'smooth'
273
+ });
274
+ }
275
+ }
276
+
277
+ // Handle auto-scroll if enabled
278
+ if (container.autoScroll) {
279
+ const viewport = container.viewport;
280
+ if (viewport) {
281
+ const options: ScrollOptions = typeof container.autoScroll === 'boolean'
282
+ ? { center: true }
283
+ : container.autoScroll;
284
+ this.scrollToElement(containerId, index, viewport, options);
285
+ }
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Get the element at a specific index
291
+ *
292
+ * @param containerId - Container identifier
293
+ * @param index - Focus index
294
+ * @returns Element at index or null
295
+ */
296
+ getElement(containerId: string, index: number): Element | null {
297
+ const container = this.containers.get(containerId);
298
+ if (!container) return null;
299
+ return container.focusables.get(index) || null;
300
+ }
301
+
302
+ /**
303
+ * Get current focus index for a container
304
+ *
305
+ * @param containerId - Container identifier
306
+ * @returns Current index signal
307
+ */
308
+ getCurrentIndexSignal(containerId: string): Signal<number | null> | null {
309
+ const container = this.containers.get(containerId);
310
+ return container ? container.currentIndex : null;
311
+ }
312
+
313
+ /**
314
+ * Get current focused element signal for a container
315
+ *
316
+ * @param containerId - Container identifier
317
+ * @returns Current element signal
318
+ */
319
+ getFocusedElementSignal(containerId: string): Signal<Element | null> | null {
320
+ const container = this.containers.get(containerId);
321
+ return container ? (container.focusedElement as unknown as Signal<Element | null>) : null;
322
+ }
323
+
324
+ /**
325
+ * Check if an element is visible in the viewport
326
+ *
327
+ * @param element - Element to check
328
+ * @param viewport - Viewport to check against (optional)
329
+ * @returns True if element is visible
330
+ */
331
+ isElementVisible(element: Element, viewport?: CanvasViewport): boolean {
332
+ if (!viewport) return true;
333
+
334
+ const bounds = this.getElementBounds(element);
335
+ const visibleBounds = viewport.getVisibleBounds();
336
+
337
+ return (
338
+ bounds.x < visibleBounds.right &&
339
+ bounds.x + bounds.width > visibleBounds.left &&
340
+ bounds.y < visibleBounds.bottom &&
341
+ bounds.y + bounds.height > visibleBounds.top
342
+ );
343
+ }
344
+
345
+ /**
346
+ * Get global bounds of an element
347
+ *
348
+ * @param element - Element to get bounds for
349
+ * @returns Bounds object with x, y, width, height
350
+ */
351
+ getElementBounds(element: Element): { x: number; y: number; width: number; height: number } {
352
+ const instance = element.componentInstance;
353
+ if (!instance) {
354
+ return { x: 0, y: 0, width: 0, height: 0 };
355
+ }
356
+
357
+ // Get local bounds
358
+ const localBounds = instance.getLocalBounds?.();
359
+ if (!localBounds) {
360
+ return { x: 0, y: 0, width: 0, height: 0 };
361
+ }
362
+
363
+ // Get global position
364
+ const globalPos = instance.getGlobalPosition?.();
365
+ if (!globalPos) {
366
+ return { x: 0, y: 0, width: 0, height: 0 };
367
+ }
368
+
369
+ return {
370
+ x: globalPos.x,
371
+ y: globalPos.y,
372
+ width: localBounds.width,
373
+ height: localBounds.height
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Scroll viewport to show an element
379
+ *
380
+ * @param containerId - Container identifier
381
+ * @param index - Focus index of element to scroll to
382
+ * @param viewport - Viewport instance (optional, uses container's viewport if not provided)
383
+ * @param options - Scroll options
384
+ */
385
+ scrollToElement(
386
+ containerId: string,
387
+ index: number,
388
+ viewport?: CanvasViewport,
389
+ options: ScrollOptions = {}
390
+ ): void {
391
+ const container = this.containers.get(containerId);
392
+ if (!container) return;
393
+
394
+ const element = container.focusables.get(index);
395
+ if (!element) return;
396
+
397
+ const targetViewport = viewport || container.viewport;
398
+ if (!targetViewport) return;
399
+
400
+ const bounds = this.getElementBounds(element);
401
+ const visibleBounds = targetViewport.getVisibleBounds();
402
+ const padding = options.padding || 0;
403
+ const center = options.center !== false; // Default to true
404
+ const smooth = options.smooth || false;
405
+ const duration = options.duration || 300;
406
+
407
+ // Check if element is already visible
408
+ if (this.isElementVisible(element, targetViewport)) {
409
+ // Element is visible, but check if we need to center it
410
+ if (center) {
411
+ const centerX = bounds.x + bounds.width / 2;
412
+ const centerY = bounds.y + bounds.height / 2;
413
+
414
+ if (smooth) {
415
+ this.animateScroll(containerId, targetViewport, centerX, centerY, duration);
416
+ } else {
417
+ targetViewport.moveCenter(centerX, centerY);
418
+ }
419
+ }
420
+ return;
421
+ }
422
+
423
+ // Element is not visible, scroll to it
424
+ if (center) {
425
+ const centerX = bounds.x + bounds.width / 2;
426
+ const centerY = bounds.y + bounds.height / 2;
427
+
428
+ if (smooth) {
429
+ this.animateScroll(containerId, targetViewport, centerX, centerY, duration);
430
+ } else {
431
+ targetViewport.moveCenter(centerX, centerY);
432
+ }
433
+ } else {
434
+ // Scroll to make element visible with padding
435
+ const targetX = bounds.x - padding;
436
+ const targetY = bounds.y - padding;
437
+ const targetWidth = bounds.width + padding * 2;
438
+ const targetHeight = bounds.height + padding * 2;
439
+
440
+ if (smooth) {
441
+ // For smooth fit, we'll animate to center
442
+ const centerX = bounds.x + bounds.width / 2;
443
+ const centerY = bounds.y + bounds.height / 2;
444
+ this.animateScroll(containerId, targetViewport, centerX, centerY, duration);
445
+ } else {
446
+ targetViewport.fit(targetX, targetY, targetWidth, targetHeight, padding);
447
+ }
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Animate smooth scrolling
453
+ *
454
+ * @param containerId - Container identifier
455
+ * @param viewport - Viewport instance
456
+ * @param targetX - Target X position
457
+ * @param targetY - Target Y position
458
+ * @param duration - Animation duration in ms
459
+ */
460
+ private animateScroll(
461
+ containerId: string,
462
+ viewport: CanvasViewport,
463
+ targetX: number,
464
+ targetY: number,
465
+ duration: number
466
+ ): void {
467
+ const currentCenter = viewport.center;
468
+ const startX = currentCenter.x;
469
+ const startY = currentCenter.y;
470
+
471
+ const animation = {
472
+ startTime: Date.now(),
473
+ startX,
474
+ startY,
475
+ targetX,
476
+ targetY,
477
+ duration
478
+ };
479
+
480
+ this.scrollAnimations.set(containerId, animation);
481
+
482
+ // Use requestAnimationFrame for smooth animation
483
+ const animate = () => {
484
+ const anim = this.scrollAnimations.get(containerId);
485
+ if (!anim) return;
486
+
487
+ const elapsed = Date.now() - anim.startTime;
488
+ const progress = Math.min(elapsed / anim.duration, 1);
489
+
490
+ // Easing function (ease-out)
491
+ const eased = 1 - Math.pow(1 - progress, 3);
492
+
493
+ const currentX = anim.startX + (anim.targetX - anim.startX) * eased;
494
+ const currentY = anim.startY + (anim.targetY - anim.startY) * eased;
495
+
496
+ viewport.moveCenter(currentX, currentY);
497
+
498
+ if (progress < 1) {
499
+ requestAnimationFrame(animate);
500
+ } else {
501
+ this.scrollAnimations.delete(containerId);
502
+ }
503
+ };
504
+
505
+ requestAnimationFrame(animate);
506
+ }
507
+ }
508
+
509
+ // Export singleton instance
510
+ export const focusManager = FocusManager.getInstance();