canvasframework 0.3.6

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 (85) hide show
  1. package/README.md +554 -0
  2. package/components/Accordion.js +252 -0
  3. package/components/AndroidDatePickerDialog.js +398 -0
  4. package/components/AppBar.js +225 -0
  5. package/components/Avatar.js +202 -0
  6. package/components/BottomNavigationBar.js +205 -0
  7. package/components/BottomSheet.js +374 -0
  8. package/components/Button.js +225 -0
  9. package/components/Card.js +193 -0
  10. package/components/Checkbox.js +180 -0
  11. package/components/Chip.js +212 -0
  12. package/components/CircularProgress.js +143 -0
  13. package/components/ContextMenu.js +116 -0
  14. package/components/DatePicker.js +257 -0
  15. package/components/Dialog.js +367 -0
  16. package/components/Divider.js +125 -0
  17. package/components/Drawer.js +261 -0
  18. package/components/FAB.js +270 -0
  19. package/components/FileUpload.js +315 -0
  20. package/components/IOSDatePickerWheel.js +268 -0
  21. package/components/ImageCarousel.js +193 -0
  22. package/components/ImageComponent.js +223 -0
  23. package/components/Input.js +309 -0
  24. package/components/List.js +94 -0
  25. package/components/ListItem.js +223 -0
  26. package/components/Modal.js +364 -0
  27. package/components/MultiSelectDialog.js +206 -0
  28. package/components/NumberInput.js +271 -0
  29. package/components/ProgressBar.js +88 -0
  30. package/components/RadioButton.js +142 -0
  31. package/components/SearchInput.js +315 -0
  32. package/components/SegmentedControl.js +202 -0
  33. package/components/Select.js +199 -0
  34. package/components/SelectDialog.js +255 -0
  35. package/components/Slider.js +113 -0
  36. package/components/Snackbar.js +243 -0
  37. package/components/Stepper.js +281 -0
  38. package/components/SwipeableListItem.js +179 -0
  39. package/components/Switch.js +147 -0
  40. package/components/Table.js +492 -0
  41. package/components/Tabs.js +125 -0
  42. package/components/Text.js +141 -0
  43. package/components/TextField.js +331 -0
  44. package/components/Toast.js +236 -0
  45. package/components/TreeView.js +420 -0
  46. package/components/Video.js +397 -0
  47. package/components/View.js +140 -0
  48. package/components/VirtualList.js +120 -0
  49. package/core/CanvasFramework.js +1271 -0
  50. package/core/CanvasWork.js +32 -0
  51. package/core/Component.js +153 -0
  52. package/core/LogicWorker.js +25 -0
  53. package/core/WebGLCanvasAdapter.js +1369 -0
  54. package/features/Column.js +43 -0
  55. package/features/Grid.js +47 -0
  56. package/features/LayoutComponent.js +43 -0
  57. package/features/OpenStreetMap.js +310 -0
  58. package/features/Positioned.js +33 -0
  59. package/features/PullToRefresh.js +328 -0
  60. package/features/Row.js +40 -0
  61. package/features/SignaturePad.js +257 -0
  62. package/features/Skeleton.js +84 -0
  63. package/features/Stack.js +21 -0
  64. package/index.js +101 -0
  65. package/manager/AccessibilityManager.js +107 -0
  66. package/manager/ErrorHandler.js +59 -0
  67. package/manager/FeatureFlags.js +60 -0
  68. package/manager/MemoryManager.js +107 -0
  69. package/manager/PerformanceMonitor.js +84 -0
  70. package/manager/SecurityManager.js +54 -0
  71. package/package.json +28 -0
  72. package/utils/AnimationEngine.js +428 -0
  73. package/utils/DataStore.js +403 -0
  74. package/utils/EventBus.js +407 -0
  75. package/utils/FetchClient.js +74 -0
  76. package/utils/FormValidator.js +355 -0
  77. package/utils/GeoLocationService.js +62 -0
  78. package/utils/I18n.js +207 -0
  79. package/utils/IndexedDBManager.js +273 -0
  80. package/utils/OfflineSyncManager.js +342 -0
  81. package/utils/QueryBuilder.js +478 -0
  82. package/utils/SafeArea.js +64 -0
  83. package/utils/SecureStorage.js +289 -0
  84. package/utils/StateManager.js +207 -0
  85. package/utils/WebSocketClient.js +66 -0
@@ -0,0 +1,1271 @@
1
+ import Button from '../components/Button.js';
2
+ import SegmentedControl from '../components/SegmentedControl.js';
3
+ import Input from '../components/Input.js';
4
+ import Slider from '../components/Slider.js';
5
+ import Text from '../components/Text.js';
6
+ import View from '../components/View.js';
7
+ import Card from '../components/Card.js';
8
+ import FAB from '../components/FAB.js';
9
+ import CircularProgress from '../components/CircularProgress.js';
10
+ import ImageComponent from '../components/ImageComponent.js';
11
+ import DatePicker from '../components/DatePicker.js';
12
+ import IOSDatePickerWheel from '../components/IOSDatePickerWheel.js';
13
+ import AndroidDatePickerDialog from '../components/AndroidDatePickerDialog.js';
14
+ import Avatar from '../components/Avatar.js';
15
+ import Snackbar from '../components/Snackbar.js';
16
+ import BottomNavigationBar from '../components/BottomNavigationBar.js';
17
+ import Video from '../components/Video.js';
18
+ import Modal from '../components/Modal.js';
19
+ import Drawer from '../components/Drawer.js';
20
+ import AppBar from '../components/AppBar.js';
21
+ import Chip from '../components/Chip.js';
22
+ import Stepper from '../components/Stepper.js';
23
+ import Accordion from '../components/Accordion.js';
24
+ import Tabs from '../components/Tabs.js';
25
+ import Switch from '../components/Switch.js';
26
+ import SwipeableListItem from '../components/SwipeableListItem.js';
27
+ import ListItem from '../components/ListItem.js';
28
+ import List from '../components/List.js';
29
+ import VirtualList from '../components/VirtualList.js';
30
+ import BottomSheet from '../components/BottomSheet.js';
31
+ import ProgressBar from '../components/ProgressBar.js';
32
+ import RadioButton from '../components/RadioButton.js';
33
+ import Dialog from '../components/Dialog.js';
34
+ import ContextMenu from '../components/ContextMenu.js';
35
+ import Checkbox from '../components/Checkbox.js';
36
+ import Toast from '../components/Toast.js';
37
+ import NumberInput from '../components/NumberInput.js';
38
+ import TextField from '../components/TextField.js';
39
+ import SelectDialog from '../components/SelectDialog.js';
40
+ import Select from '../components/Select.js';
41
+ import MultiSelectDialog from '../components/MultiSelectDialog.js';
42
+ import Divider from '../components/Divider.js';
43
+ import FileUpload from '../components/FileUpload.js';
44
+ import Table from '../components/Table.js';
45
+ import TreeView from '../components/TreeView.js';
46
+ import SearchInput from '../components/SearchInput.js';
47
+ import ImageCarousel from '../components/ImageCarousel.js';
48
+
49
+ // Utils
50
+ import SafeArea from '../utils/SafeArea.js';
51
+ import StateManager from '../utils/StateManager.js';
52
+ import I18n from '../utils/I18n.js';
53
+ import SecureStorage from '../utils/SecureStorage.js';
54
+ import FormValidator from '../utils/FormValidator.js';
55
+ import DataStore from '../utils/DataStore.js';
56
+ import EventBus from '../utils/EventBus.js';
57
+ import IndexedDBManager from '../utils/IndexedDBManager.js';
58
+ import QueryBuilder from '../utils/QueryBuilder.js';
59
+ import OfflineSyncManager from '../utils/OfflineSyncManager.js';
60
+ import FetchClient from '../utils/FetchClient.js';
61
+ import GeoLocationService from '../utils/GeoLocationService.js';
62
+ import WebSocketClient from '../utils/WebSocketClient.js';
63
+ import AnimationEngine from '../utils/AnimationEngine.js';
64
+
65
+ // Features
66
+ import PullToRefresh from '../features/PullToRefresh.js';
67
+ import Skeleton from '../features/Skeleton.js';
68
+ import SignaturePad from '../features/SignaturePad.js';
69
+ import OpenStreetMap from '../features/OpenStreetMap.js';
70
+ import LayoutComponent from '../features/LayoutComponent.js';
71
+ import Grid from '../features/Grid.js';
72
+ import Row from '../features/Row.js';
73
+ import Column from '../features/Column.js';
74
+ import Positioned from '../features/Positioned.js';
75
+ import Stack from '../features/Stack.js';
76
+
77
+ // Manager
78
+ import ErrorHandler from '../manager/ErrorHandler.js';
79
+ import PerformanceMonitor from '../manager/PerformanceMonitor.js';
80
+ import AccessibilityManager from '../manager/AccessibilityManager.js';
81
+ import MemoryManager from '../manager/MemoryManager.js';
82
+ import SecurityManager from '../manager/SecurityManager.js';
83
+ import FeatureFlags from '../manager/FeatureFlags.js';
84
+
85
+ // WebGL Adapter
86
+ import WebGLCanvasAdapter from './WebGLCanvasAdapter.js';
87
+
88
+ // theme
89
+ export const lightTheme = {
90
+ background: '#FFFFFF',
91
+ text: '#000000',
92
+ primary: '#6200EE',
93
+ secondary: '#03DAC6',
94
+ buttonText: '#FFFFFF',
95
+ buttonBackground: '#6200EE',
96
+ border: '#E0E0E0'
97
+ };
98
+
99
+ export const darkTheme = {
100
+ background: '#121212',
101
+ text: '#FFFFFF',
102
+ primary: '#BB86FC',
103
+ secondary: '#03DAC6',
104
+ buttonText: '#000000',
105
+ buttonBackground: '#BB86FC',
106
+ border: '#333333'
107
+ };
108
+
109
+ /**
110
+ * Framework principal pour créer des interfaces utilisateur basées sur Canvas
111
+ * @class
112
+ * @property {HTMLCanvasElement} canvas - Élément canvas HTML
113
+ * @property {CanvasRenderingContext2D} ctx - Contexte 2D du canvas
114
+ * @property {number} width - Largeur du canvas
115
+ * @property {number} height - Hauteur du canvas
116
+ * @property {number} dpr - Device Pixel Ratio
117
+ * @property {string} platform - Plateforme détectée ('material' ou 'cupertino')
118
+ * @property {Component[]} components - Liste des composants
119
+ * @property {Map} routes - Routes de navigation
120
+ * @property {string} currentRoute - Route actuelle
121
+ * @property {Object} state - État global
122
+ * @property {boolean} isDragging - Indique si un drag est en cours
123
+ * @property {number} lastTouchY - Dernière position Y du touch
124
+ * @property {number} scrollOffset - Offset de défilement
125
+ * @property {number} scrollVelocity - Vitesse de défilement
126
+ * @property {number} scrollFriction - Friction du défilement
127
+ */
128
+ class CanvasFramework {
129
+ /**
130
+ * Crée une instance de CanvasFramework
131
+ * @param {string} canvasId - ID de l'élément canvas
132
+ */
133
+ constructor(canvasId, options = {}) {
134
+ this.canvas = document.getElementById(canvasId);
135
+ this.ctx = this.canvas.getContext('2d');
136
+ this.width = window.innerWidth;
137
+ this.height = window.innerHeight;
138
+ this.dpr = window.devicePixelRatio || 1;
139
+
140
+ this.platform = this.detectPlatform();
141
+
142
+ // Thèmes
143
+ this.lightTheme = lightTheme;
144
+ this.darkTheme = darkTheme;
145
+ this.theme = lightTheme; // thème par défaut
146
+
147
+ this.components = [];
148
+ this.state = {};
149
+ // NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
150
+ this.useWebGL = options.useWebGL !== false; // true par défaut
151
+ // Initialiser le contexte approprié
152
+ if (this.useWebGL) {
153
+ try {
154
+ this.ctx = new WebGLCanvasAdapter(this.canvas);
155
+ } catch (e) {
156
+ this.ctx = this.canvas.getContext('2d');
157
+ this.useWebGL = false;
158
+ }
159
+ } else {
160
+ this.ctx = this.canvas.getContext('2d');
161
+ }
162
+ // Calcule FPS
163
+ this.fps = 0;
164
+ this._frames = 0;
165
+ this._lastFpsTime = performance.now();
166
+ this.showFps = options.showFps || false; // false par défaut
167
+ this.debbug = options.debug || false; // false par défaut (et correction de la faute de frappe)
168
+ // Worker pour multithreading
169
+ this.worker = new Worker('./CanvasWorker.js', { type: 'module' });
170
+ this.worker.onmessage = this.handleWorkerMessage.bind(this);
171
+ this.worker.postMessage({ type: 'INIT', payload: { components: [] } });
172
+
173
+ // Worker logique pour calculs séparés
174
+ this.logicWorker = new Worker('./LogicWorker.js', { type: 'module' });
175
+ this.logicWorker.onmessage = this.handleLogicWorkerMessage.bind(this);
176
+ this.logicWorkerState = {};
177
+
178
+ // Envoyer l'état initial au worker
179
+ this.logicWorker.postMessage({ type: 'SET_STATE', payload: this.state });
180
+
181
+ // Gestion des événements
182
+ this.isDragging = false;
183
+ this.lastTouchY = 0;
184
+ this.scrollOffset = 0;
185
+ this.scrollVelocity = 0;
186
+ this.scrollFriction = 0.95;
187
+
188
+ // Optimisation
189
+ this.dirtyComponents = new Set();
190
+ this.optimizationEnabled = false;
191
+
192
+ // AJOUTER CETTE LIGNE
193
+ this.animator = new AnimationEngine();
194
+
195
+ // ===== NOUVEAU SYSTÈME DE ROUTING =====
196
+ this.routes = new Map();
197
+ this.currentRoute = '/';
198
+ this.currentParams = {};
199
+ this.currentQuery = {};
200
+ this.history = [];
201
+ this.historyIndex = -1;
202
+
203
+ // Animation de transition
204
+ this.transitionState = {
205
+ isTransitioning: false,
206
+ progress: 0,
207
+ duration: 300,
208
+ type: 'slide', // 'slide', 'fade', 'none'
209
+ direction: 'forward', // 'forward', 'back'
210
+ oldComponents: [],
211
+ newComponents: []
212
+ };
213
+
214
+ this.setupCanvas();
215
+ this.setupEventListeners();
216
+ this.setupHistoryListener();
217
+ this.startRenderLoop();
218
+ }
219
+
220
+ wrapContext(ctx, theme) {
221
+ const originalFillStyle = Object.getOwnPropertyDescriptor(CanvasRenderingContext2D.prototype, 'fillStyle');
222
+ Object.defineProperty(ctx, 'fillStyle', {
223
+ set: (value) => {
224
+ // Si value est blanc/noir ou une couleur “neutre”, tu remplaces par theme
225
+ if (value === '#FFFFFF' || value === '#000000') {
226
+ originalFillStyle.set.call(ctx, theme.text);
227
+ } else {
228
+ originalFillStyle.set.call(ctx, value);
229
+ }
230
+ },
231
+ get: () => originalFillStyle.get.call(ctx)
232
+ });
233
+ }
234
+
235
+ // Set Theme dynamique
236
+ setTheme(theme) {
237
+ this.theme = theme;
238
+
239
+ // Intercepter le context pour remplacer les couleurs globalement
240
+ if (!this.useWebGL) {
241
+ this.wrapContext(this.ctx, theme);
242
+ }
243
+
244
+ // marque tous les composants dirty pour redraw
245
+ for (let comp of this.components) comp.markDirty();
246
+ }
247
+
248
+ // Switch Theme
249
+ toggleDarkMode() {
250
+ if (this.theme === lightTheme) {
251
+ this.setTheme(darkTheme);
252
+ } else {
253
+ this.setTheme(lightTheme);
254
+ }
255
+ }
256
+
257
+ enableFpsDisplay(enable = true) {
258
+ this.showFps = enable;
259
+ }
260
+
261
+ // AJOUTER CETTE MÉTHODE (optionnel - pour faciliter l'accès)
262
+ animate(component, options) {
263
+ return this.animator.animate(component, options);
264
+ }
265
+
266
+ // ----- Worker UI -----
267
+ handleWorkerMessage(e) {
268
+ const { type, payload } = e.data;
269
+ switch(type) {
270
+ case 'LAYOUT_DONE':
271
+ for (let update of payload) {
272
+ const comp = this.components.find(c => c.id === update.id);
273
+ if (comp) comp.height = update.height;
274
+ }
275
+ break;
276
+ case 'SCROLL_UPDATED':
277
+ this.scrollOffset = payload.offset;
278
+ this.scrollVelocity = payload.velocity;
279
+ break;
280
+ }
281
+ }
282
+
283
+ updateLayoutAsync() {
284
+ this.worker.postMessage({ type: 'UPDATE_LAYOUT' });
285
+ }
286
+
287
+ updateScrollInertia() {
288
+ const maxScroll = this.getMaxScroll();
289
+ this.worker.postMessage({
290
+ type: 'SCROLL_INERTIA',
291
+ payload: {
292
+ offset: this.scrollOffset,
293
+ velocity: this.scrollVelocity,
294
+ friction: this.scrollFriction,
295
+ maxScroll
296
+ }
297
+ });
298
+ }
299
+
300
+ // ------ Logic Worker --------
301
+ handleLogicWorkerMessage(e) {
302
+ const { type, payload } = e.data;
303
+ switch(type) {
304
+ case 'STATE_UPDATED':
305
+ // Le worker a renvoyé le nouvel état global
306
+ this.logicWorkerState = payload;
307
+ break;
308
+
309
+ case 'EXECUTION_RESULT':
310
+ // Résultat d'une tâche spécifique envoyée au worker
311
+ if (this.onWorkerResult) this.onWorkerResult(payload);
312
+ break;
313
+
314
+ case 'EXECUTION_ERROR':
315
+ console.error('Logic Worker Error:', payload);
316
+ break;
317
+ }
318
+ }
319
+
320
+ runLogicTask(taskName, taskData) {
321
+ this.logicWorker.postMessage({
322
+ type: 'EXECUTE_TASK',
323
+ payload: { taskName, taskData }
324
+ });
325
+ }
326
+
327
+ updateLogicWorkerState(newState) {
328
+ this.logicWorkerState = { ...this.logicWorkerState, ...newState };
329
+ this.logicWorker.postMessage({ type: 'SET_STATE', payload: this.logicWorkerState });
330
+ }
331
+
332
+ detectPlatform() {
333
+ const ua = navigator.userAgent.toLowerCase();
334
+ if (/android/.test(ua)) return 'material';
335
+ if (/iphone|ipad|ipod/.test(ua)) return 'cupertino';
336
+ return 'material';
337
+ }
338
+
339
+ setupCanvas() {
340
+ this.canvas.width = this.width * this.dpr;
341
+ this.canvas.height = this.height * this.dpr;
342
+ this.canvas.style.width = this.width + 'px';
343
+ this.canvas.style.height = this.height + 'px';
344
+
345
+ // Échelle uniquement pour Canvas 2D
346
+ if (!this.useWebGL) {
347
+ this.ctx.scale(this.dpr, this.dpr);
348
+ } else {
349
+ // WebGL gère le DPR automatiquement via la matrice de projection
350
+ this.ctx.updateProjectionMatrix();
351
+ }
352
+ }
353
+
354
+ setupEventListeners() {
355
+ this.canvas.addEventListener('touchstart', this.handleTouchStart.bind(this));
356
+ this.canvas.addEventListener('touchmove', this.handleTouchMove.bind(this));
357
+ this.canvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
358
+ this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
359
+ this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
360
+ this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
361
+ window.addEventListener('resize', this.handleResize.bind(this));
362
+ }
363
+
364
+ /**
365
+ * Configure l'écoute de l'historique du navigateur
366
+ * @private
367
+ */
368
+ setupHistoryListener() {
369
+ window.addEventListener('popstate', (e) => {
370
+ if (e.state && e.state.route) {
371
+ this.navigateTo(e.state.route, {
372
+ replace: true,
373
+ animate: true,
374
+ direction: 'back'
375
+ });
376
+ }
377
+ });
378
+ }
379
+
380
+ // ===== MÉTHODES DE ROUTING =====
381
+
382
+ /**
383
+ * Définit une route avec pattern de paramètres
384
+ * @param {string} pattern - Pattern de la route (ex: '/user/:id', '/posts/:category/:id')
385
+ * @param {Function} component - Fonction qui crée les composants
386
+ * @param {Object} options - Options de la route
387
+ * @returns {CanvasFramework}
388
+ */
389
+ route(pattern, component, options = {}) {
390
+ const route = {
391
+ pattern,
392
+ component,
393
+ regex: this.patternToRegex(pattern),
394
+ paramNames: this.extractParamNames(pattern),
395
+ beforeEnter: options.beforeEnter,
396
+ afterEnter: options.afterEnter,
397
+ beforeLeave: options.beforeLeave,
398
+ transition: options.transition || 'slide'
399
+ };
400
+
401
+ this.routes.set(pattern, route);
402
+ return this;
403
+ }
404
+
405
+ /**
406
+ * Convertit un pattern de route en regex
407
+ * @private
408
+ */
409
+ patternToRegex(pattern) {
410
+ const regexPattern = pattern
411
+ .replace(/\//g, '\\/')
412
+ .replace(/:([^\/]+)/g, '([^\\/]+)');
413
+ return new RegExp(`^${regexPattern}$`);
414
+ }
415
+
416
+ /**
417
+ * Extrait les noms des paramètres d'un pattern
418
+ * @private
419
+ */
420
+ extractParamNames(pattern) {
421
+ const matches = pattern.match(/:([^\/]+)/g);
422
+ return matches ? matches.map(m => m.slice(1)) : [];
423
+ }
424
+
425
+ /**
426
+ * Trouve la route correspondant à un path
427
+ * @private
428
+ */
429
+ matchRoute(path) {
430
+ // Séparer le path et la query string
431
+ const [pathname, queryString] = path.split('?');
432
+
433
+ for (let [pattern, route] of this.routes) {
434
+ const match = pathname.match(route.regex);
435
+ if (match) {
436
+ const params = {};
437
+ route.paramNames.forEach((name, index) => {
438
+ params[name] = match[index + 1];
439
+ });
440
+
441
+ const query = this.parseQueryString(queryString);
442
+
443
+ return { route, params, query, pathname };
444
+ }
445
+ }
446
+ return null;
447
+ }
448
+
449
+ /**
450
+ * Parse une query string
451
+ * @private
452
+ */
453
+ parseQueryString(queryString) {
454
+ if (!queryString) return {};
455
+
456
+ const params = {};
457
+ queryString.split('&').forEach(param => {
458
+ const [key, value] = param.split('=');
459
+ params[decodeURIComponent(key)] = decodeURIComponent(value || '');
460
+ });
461
+ return params;
462
+ }
463
+
464
+ /**
465
+ * Navigue vers une route
466
+ * @param {string} path - Chemin de destination (ex: '/user/123', '/posts/tech/456?sort=date')
467
+ * @param {Object} options - Options de navigation
468
+ */
469
+ navigate(path, options = {}) {
470
+ this.navigateTo(path, options);
471
+ }
472
+
473
+ /**
474
+ * Méthode interne de navigation
475
+ * @private
476
+ */
477
+ async navigateTo(path, options = {}) {
478
+ const {
479
+ replace = false,
480
+ animate = true,
481
+ direction = 'forward',
482
+ transition = null,
483
+ state = {}
484
+ } = options;
485
+
486
+ const match = this.matchRoute(path);
487
+ if (!match) {
488
+ console.warn(`Route not found: ${path}`);
489
+ return;
490
+ }
491
+
492
+ const { route, params, query, pathname } = match;
493
+
494
+ // Hook beforeLeave de la route actuelle
495
+ const currentRouteData = this.routes.get(this.currentRoute);
496
+ if (currentRouteData?.beforeLeave) {
497
+ const canLeave = await currentRouteData.beforeLeave(this.currentParams, this.currentQuery);
498
+ if (canLeave === false) return;
499
+ }
500
+
501
+ // Hook beforeEnter de la nouvelle route
502
+ if (route.beforeEnter) {
503
+ const canEnter = await route.beforeEnter(params, query);
504
+ if (canEnter === false) return;
505
+ }
506
+
507
+ // Sauvegarder l'ancienne route pour l'animation
508
+ const oldComponents = [...this.components];
509
+
510
+ // Mettre à jour l'état
511
+ this.currentRoute = pathname;
512
+ this.currentParams = params;
513
+ this.currentQuery = query;
514
+
515
+ // Gérer l'historique
516
+ if (!replace) {
517
+ this.historyIndex++;
518
+ this.history = this.history.slice(0, this.historyIndex);
519
+ this.history.push({ path, params, query, state });
520
+
521
+ // Mettre à jour l'historique du navigateur
522
+ window.history.pushState(
523
+ { route: path, params, query, state },
524
+ '',
525
+ path
526
+ );
527
+ } else {
528
+ this.history[this.historyIndex] = { path, params, query, state };
529
+ window.history.replaceState(
530
+ { route: path, params, query, state },
531
+ '',
532
+ path
533
+ );
534
+ }
535
+
536
+ // Créer les nouveaux composants
537
+ this.components = [];
538
+ if (typeof route.component === 'function') {
539
+ route.component(this, params, query);
540
+ }
541
+
542
+ // Lancer l'animation de transition
543
+ if (animate && !this.transitionState.isTransitioning) {
544
+ const transitionType = transition || route.transition || 'slide';
545
+ this.startTransition(oldComponents, this.components, transitionType, direction);
546
+ }
547
+
548
+ // Hook afterEnter
549
+ if (route.afterEnter) {
550
+ route.afterEnter(params, query);
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Démarre une animation de transition
556
+ * @private
557
+ */
558
+ startTransition(oldComponents, newComponents, type, direction) {
559
+ this.transitionState = {
560
+ isTransitioning: true,
561
+ progress: 0,
562
+ duration: 300,
563
+ type,
564
+ direction,
565
+ oldComponents: [...oldComponents],
566
+ newComponents: [...newComponents],
567
+ startTime: Date.now()
568
+ };
569
+ }
570
+
571
+ /**
572
+ * Met à jour l'animation de transition
573
+ * @private
574
+ */
575
+ updateTransition() {
576
+ if (!this.transitionState.isTransitioning) return;
577
+
578
+ const elapsed = Date.now() - this.transitionState.startTime;
579
+ this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
580
+
581
+ // Fonction d'easing (ease-in-out)
582
+ const eased = this.easeInOutCubic(this.transitionState.progress);
583
+
584
+ // Appliquer la transformation selon le type
585
+ this.ctx.save();
586
+ this.applyTransitionTransform(eased);
587
+ this.ctx.restore();
588
+
589
+ // Terminer la transition
590
+ if (this.transitionState.progress >= 1) {
591
+ this.transitionState.isTransitioning = false;
592
+ this.transitionState.oldComponents = [];
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Applique la transformation de transition
598
+ * @private
599
+ */
600
+ applyTransitionTransform(progress) {
601
+ const { type, direction, oldComponents, newComponents } = this.transitionState;
602
+ const directionMultiplier = direction === 'forward' ? 1 : -1;
603
+
604
+ switch (type) {
605
+ case 'slide':
606
+ // Dessiner l'ancienne vue qui sort
607
+ this.ctx.save();
608
+ this.ctx.translate(-this.width * progress * directionMultiplier, 0);
609
+ this.ctx.globalAlpha = 1 - progress * 0.3;
610
+ for (let comp of oldComponents) {
611
+ if (comp.visible) comp.draw(this.ctx);
612
+ }
613
+ this.ctx.restore();
614
+
615
+ // Dessiner la nouvelle vue qui entre
616
+ this.ctx.save();
617
+ this.ctx.translate(this.width * (1 - progress) * directionMultiplier, 0);
618
+ for (let comp of newComponents) {
619
+ if (comp.visible) comp.draw(this.ctx);
620
+ }
621
+ this.ctx.restore();
622
+ break;
623
+
624
+ case 'fade':
625
+ // Dessiner l'ancienne vue qui fade out
626
+ this.ctx.save();
627
+ this.ctx.globalAlpha = 1 - progress;
628
+ for (let comp of oldComponents) {
629
+ if (comp.visible) comp.draw(this.ctx);
630
+ }
631
+ this.ctx.restore();
632
+
633
+ // Dessiner la nouvelle vue qui fade in
634
+ this.ctx.save();
635
+ this.ctx.globalAlpha = progress;
636
+ for (let comp of newComponents) {
637
+ if (comp.visible) comp.draw(this.ctx);
638
+ }
639
+ this.ctx.restore();
640
+ break;
641
+
642
+ case 'none':
643
+ // Pas d'animation, juste afficher la nouvelle vue
644
+ for (let comp of newComponents) {
645
+ if (comp.visible) comp.draw(this.ctx);
646
+ }
647
+ break;
648
+ }
649
+ }
650
+
651
+ /**
652
+ * Fonction d'easing
653
+ * @private
654
+ */
655
+ easeInOutCubic(t) {
656
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
657
+ }
658
+
659
+ /**
660
+ * Retour en arrière dans l'historique
661
+ */
662
+ goBack() {
663
+ if (this.historyIndex > 0) {
664
+ this.historyIndex--;
665
+ const historyEntry = this.history[this.historyIndex];
666
+ this.navigateTo(historyEntry.path, {
667
+ replace: true,
668
+ animate: true,
669
+ direction: 'back'
670
+ });
671
+ window.history.back();
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Avancer dans l'historique
677
+ */
678
+ goForward() {
679
+ if (this.historyIndex < this.history.length - 1) {
680
+ this.historyIndex++;
681
+ const historyEntry = this.history[this.historyIndex];
682
+ this.navigateTo(historyEntry.path, {
683
+ replace: true,
684
+ animate: true,
685
+ direction: 'forward'
686
+ });
687
+ window.history.forward();
688
+ }
689
+ }
690
+
691
+ /**
692
+ * Obtient les paramètres de la route actuelle
693
+ * @returns {Object}
694
+ */
695
+ getParams() {
696
+ return { ...this.currentParams };
697
+ }
698
+
699
+ /**
700
+ * Obtient la query string de la route actuelle
701
+ * @returns {Object}
702
+ */
703
+ getQuery() {
704
+ return { ...this.currentQuery };
705
+ }
706
+
707
+ /**
708
+ * Obtient un paramètre spécifique
709
+ * @param {string} name
710
+ * @returns {string|undefined}
711
+ */
712
+ getParam(name) {
713
+ return this.currentParams[name];
714
+ }
715
+
716
+ /**
717
+ * Obtient un paramètre de query spécifique
718
+ * @param {string} name
719
+ * @returns {string|undefined}
720
+ */
721
+ getQueryParam(name) {
722
+ return this.currentQuery[name];
723
+ }
724
+
725
+ // ===== FIN DES MÉTHODES DE ROUTING =====
726
+
727
+ handleTouchStart(e) {
728
+ e.preventDefault();
729
+ this.isDragging = false;
730
+ const touch = e.touches[0];
731
+ const pos = this.getTouchPos(touch);
732
+ this.lastTouchY = pos.y;
733
+ this.checkComponentsAtPosition(pos.x, pos.y, 'start');
734
+ }
735
+
736
+ handleTouchMove(e) {
737
+ e.preventDefault();
738
+ const touch = e.touches[0];
739
+ const pos = this.getTouchPos(touch);
740
+
741
+ if (!this.isDragging) {
742
+ const deltaY = Math.abs(pos.y - this.lastTouchY);
743
+ if (deltaY > 5) {
744
+ this.isDragging = true;
745
+ }
746
+ }
747
+
748
+ if (this.isDragging) {
749
+ const deltaY = pos.y - this.lastTouchY;
750
+ this.scrollOffset += deltaY;
751
+ const maxScroll = this.getMaxScroll();
752
+ this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -maxScroll);
753
+ this.scrollVelocity = deltaY;
754
+ this.lastTouchY = pos.y;
755
+ } else {
756
+ this.checkComponentsAtPosition(pos.x, pos.y, 'move');
757
+ }
758
+ }
759
+
760
+ handleTouchEnd(e) {
761
+ e.preventDefault();
762
+ const touch = e.changedTouches[0];
763
+ const pos = this.getTouchPos(touch);
764
+
765
+ if (!this.isDragging) {
766
+ this.checkComponentsAtPosition(pos.x, pos.y, 'end');
767
+ } else {
768
+ this.isDragging = false;
769
+ }
770
+ }
771
+
772
+ handleMouseDown(e) {
773
+ this.isDragging = false;
774
+ this.lastTouchY = e.clientY;
775
+ this.checkComponentsAtPosition(e.clientX, e.clientY, 'start');
776
+ }
777
+
778
+ handleMouseMove(e) {
779
+ if (!this.isDragging) {
780
+ const deltaY = Math.abs(e.clientY - this.lastTouchY);
781
+ if (deltaY > 5) {
782
+ this.isDragging = true;
783
+ }
784
+ }
785
+
786
+ if (this.isDragging) {
787
+ const deltaY = e.clientY - this.lastTouchY;
788
+ this.scrollOffset += deltaY;
789
+ const maxScroll = this.getMaxScroll();
790
+ this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -maxScroll);
791
+ this.scrollVelocity = deltaY;
792
+ this.lastTouchY = e.clientY;
793
+ } else {
794
+ this.checkComponentsAtPosition(e.clientX, e.clientY, 'move');
795
+ }
796
+ }
797
+
798
+ handleMouseUp(e) {
799
+ if (!this.isDragging) {
800
+ this.checkComponentsAtPosition(e.clientX, e.clientY, 'end');
801
+ } else {
802
+ this.isDragging = false;
803
+ }
804
+ }
805
+
806
+ getTouchPos(touch) {
807
+ const rect = this.canvas.getBoundingClientRect();
808
+ return {
809
+ x: touch.clientX - rect.left,
810
+ y: touch.clientY - rect.top
811
+ };
812
+ }
813
+
814
+ checkComponentsAtPosition(x, y, eventType) {
815
+ const isFixedComponent = (comp) => {
816
+ return comp instanceof AppBar ||
817
+ comp instanceof BottomNavigationBar ||
818
+ comp instanceof Drawer ||
819
+ comp instanceof Dialog ||
820
+ comp instanceof Modal ||
821
+ comp instanceof FAB ||
822
+ comp instanceof BottomSheet ||
823
+ comp instanceof ContextMenu ||
824
+ comp instanceof OpenStreetMap ||
825
+ comp instanceof SelectDialog;
826
+ };
827
+
828
+ for (let i = this.components.length - 1; i >= 0; i--) {
829
+ const comp = this.components[i];
830
+
831
+ if (comp.visible) {
832
+ const adjustedY = isFixedComponent(comp) ? y : y - this.scrollOffset;
833
+
834
+ if (comp instanceof Card && comp.clickableChildren && comp.children && comp.children.length > 0) {
835
+ if (comp.isPointInside(x, adjustedY)) {
836
+ const cardAdjustedY = adjustedY - comp.y - comp.padding;
837
+ const cardAdjustedX = x - comp.x - comp.padding;
838
+
839
+ for (let j = comp.children.length - 1; j >= 0; j--) {
840
+ const child = comp.children[j];
841
+
842
+ if (child.visible &&
843
+ cardAdjustedY >= child.y &&
844
+ cardAdjustedY <= child.y + child.height &&
845
+ cardAdjustedX >= child.x &&
846
+ cardAdjustedX <= child.x + child.width) {
847
+
848
+ const relativeX = cardAdjustedX - child.x;
849
+ const relativeY = cardAdjustedY - child.y;
850
+
851
+ switch (eventType) {
852
+ case 'start':
853
+ child.pressed = true;
854
+ if (child.onPress) child.onPress(relativeX, relativeY);
855
+ break;
856
+
857
+ case 'move':
858
+ if (!child.hovered) {
859
+ child.hovered = true;
860
+ if (child.onHover) child.onHover();
861
+ }
862
+ if (child.onMove) child.onMove(relativeX, relativeY);
863
+ break;
864
+
865
+ case 'end':
866
+ if (child.pressed) {
867
+ child.pressed = false;
868
+
869
+ if (child instanceof Input) {
870
+ for (let other of this.components) {
871
+ if (other instanceof Input && other !== child && other.focused) {
872
+ other.focused = false;
873
+ other.cursorVisible = false;
874
+ if (other.onBlur) other.onBlur();
875
+ }
876
+ }
877
+
878
+ child.focused = true;
879
+ child.cursorVisible = true;
880
+ if (child.onFocus) child.onFocus();
881
+ } else if (child.onClick) {
882
+ child.onClick();
883
+ } else if (child.onPress) {
884
+ child.onPress(relativeX, relativeY);
885
+ }
886
+ }
887
+ break;
888
+ }
889
+
890
+ return;
891
+ }
892
+ }
893
+ }
894
+ }
895
+
896
+ if (comp.isPointInside(x, adjustedY)) {
897
+ switch (eventType) {
898
+ case 'start':
899
+ comp.pressed = true;
900
+ if (comp.onPress) comp.onPress(x, adjustedY);
901
+ break;
902
+
903
+ case 'move':
904
+ if (!comp.hovered) {
905
+ comp.hovered = true;
906
+ if (comp.onHover) comp.onHover();
907
+ }
908
+ if (comp.onMove) comp.onMove(x, adjustedY);
909
+ break;
910
+
911
+ case 'end':
912
+ if (comp.pressed) {
913
+ comp.pressed = false;
914
+
915
+ if (comp instanceof Input) {
916
+ for (let other of this.components) {
917
+ if (other instanceof Input && other !== comp && other.focused) {
918
+ other.focused = false;
919
+ other.cursorVisible = false;
920
+ if (other.onBlur) other.onBlur();
921
+ }
922
+ }
923
+
924
+ comp.focused = true;
925
+ comp.cursorVisible = true;
926
+ if (comp.onFocus) comp.onFocus();
927
+ } else if (comp.onClick) {
928
+ comp.onClick();
929
+ } else if (comp.onPress) {
930
+ comp.onPress(x, adjustedY);
931
+ }
932
+ }
933
+ break;
934
+ }
935
+
936
+ return;
937
+ } else {
938
+ comp.hovered = false;
939
+ }
940
+ }
941
+ }
942
+ }
943
+
944
+ getMaxScroll() {
945
+ let maxY = 0;
946
+ for (let comp of this.components) {
947
+ if (!this.isFixedComponent(comp)) {
948
+ maxY = Math.max(maxY, comp.y + comp.height);
949
+ }
950
+ }
951
+ return Math.max(0, maxY - this.height + 50);
952
+ }
953
+
954
+ handleResize() {
955
+ this.width = window.innerWidth;
956
+ this.height = window.innerHeight;
957
+ this.setupCanvas();
958
+ for (const comp of this.components) {
959
+ comp._resize(this.width, this.height);
960
+ }
961
+ }
962
+
963
+ add(component) {
964
+ this.components.push(component);
965
+ component._mount();
966
+ return component;
967
+ }
968
+
969
+ remove(component) {
970
+ const index = this.components.indexOf(component);
971
+ if (index > -1) {
972
+ component._unmount();
973
+ this.components.splice(index, 1);
974
+ }
975
+ }
976
+
977
+ markComponentDirty(component) {
978
+ if (this.optimizationEnabled) {
979
+ this.dirtyComponents.add(component);
980
+ }
981
+ }
982
+
983
+ enableOptimization() {
984
+ this.optimizationEnabled = true;
985
+ }
986
+
987
+ /**
988
+ * Dessine un petit triangle rouge pour indiquer overflow (style Flutter)
989
+ */
990
+ drawOverflowIndicators() {
991
+ const ctx = this.ctx;
992
+
993
+ // Pour chaque composant
994
+ for (let comp of this.components) {
995
+ if (!comp.visible) continue;
996
+
997
+ // Position réelle à l'écran
998
+ const isFixed = this.isFixedComponent(comp);
999
+ const screenY = isFixed ? comp.y : comp.y + this.scrollOffset;
1000
+ const screenX = comp.x;
1001
+
1002
+ // Vérifier si le composant TEXT a une largeur/hauteur incorrecte
1003
+ let actualWidth = comp.width;
1004
+ let actualHeight = comp.height;
1005
+
1006
+ // Si c'est un Text, vérifier la taille réelle du texte
1007
+ if (comp instanceof Text && comp.text && ctx.measureText) {
1008
+ try {
1009
+ // Sauvegarder le style actuel
1010
+ ctx.save();
1011
+
1012
+ // Appliquer le style du texte
1013
+ if (comp.fontSize) {
1014
+ ctx.font = `${comp.fontSize}px ${comp.fontFamily || 'Arial'}`;
1015
+ }
1016
+
1017
+ // Mesurer la taille réelle
1018
+ const metrics = ctx.measureText(comp.text);
1019
+ actualWidth = metrics.width + (comp.padding || 0) * 2;
1020
+ actualHeight = (comp.fontSize || 16) + (comp.padding || 0) * 2;
1021
+
1022
+ ctx.restore();
1023
+ } catch (e) {
1024
+ // En cas d'erreur, garder les dimensions par défaut
1025
+ }
1026
+ }
1027
+
1028
+ // Calculer les limites RÉELLES du composant
1029
+ const compLeft = screenX;
1030
+ const compRight = screenX + actualWidth;
1031
+ const compTop = screenY;
1032
+ const compBottom = screenY + actualHeight;
1033
+
1034
+ // Vérifier les débordements avec les dimensions RÉELLES
1035
+ const overflow = {
1036
+ left: compLeft < 0,
1037
+ right: compRight > this.width,
1038
+ top: compTop < 0,
1039
+ bottom: compBottom > this.height
1040
+ };
1041
+
1042
+ // Si aucun débordement, passer au suivant
1043
+ if (!overflow.left && !overflow.right && !overflow.top && !overflow.bottom) {
1044
+ continue;
1045
+ }
1046
+
1047
+ // DEBUG: Afficher les infos du composant
1048
+ if (this.debbug) {
1049
+ console.table({
1050
+ type: comp.constructor?.name,
1051
+ x: comp.x,
1052
+ y: comp.y,
1053
+ declaredSize: `${comp.width}x${comp.height}`,
1054
+ actualSize: `${actualWidth}x${actualHeight}`,
1055
+ screenPos: `(${screenX}, ${screenY})`,
1056
+ overflow
1057
+ });
1058
+ }
1059
+
1060
+ // Dessiner les indicateurs
1061
+ ctx.save();
1062
+
1063
+ // 1. Bordures rouges sur les parties qui débordent
1064
+ ctx.strokeStyle = 'red';
1065
+ ctx.lineWidth = 2;
1066
+ ctx.fillStyle = 'rgba(255, 0, 0, 0.2)';
1067
+
1068
+ // Gauche
1069
+ if (overflow.left) {
1070
+ const overflowWidth = Math.min(actualWidth, -compLeft);
1071
+ ctx.fillRect(compLeft, compTop, overflowWidth, actualHeight);
1072
+ ctx.strokeRect(compLeft, compTop, overflowWidth, actualHeight);
1073
+ }
1074
+
1075
+ // Droite
1076
+ if (overflow.right) {
1077
+ const overflowStart = Math.max(0, this.width - compLeft);
1078
+ const overflowWidth = Math.min(actualWidth, compRight - this.width);
1079
+ ctx.fillRect(this.width - overflowWidth, compTop, overflowWidth, actualHeight);
1080
+ ctx.strokeRect(this.width - overflowWidth, compTop, overflowWidth, actualHeight);
1081
+ }
1082
+
1083
+ // Haut
1084
+ if (overflow.top) {
1085
+ const overflowHeight = Math.min(actualHeight, -compTop);
1086
+ ctx.fillRect(compLeft, compTop, actualWidth, overflowHeight);
1087
+ ctx.strokeRect(compLeft, compTop, actualWidth, overflowHeight);
1088
+ }
1089
+
1090
+ // Bas
1091
+ if (overflow.bottom) {
1092
+ const overflowStart = Math.max(0, this.height - compTop);
1093
+ const overflowHeight = Math.min(actualHeight, compBottom - this.height);
1094
+ ctx.fillRect(compLeft, this.height - overflowHeight, actualWidth, overflowHeight);
1095
+ ctx.strokeRect(compLeft, this.height - overflowHeight, actualWidth, overflowHeight);
1096
+ }
1097
+
1098
+ // 2. Points rouges aux coins
1099
+ ctx.fillStyle = 'red';
1100
+ const markerSize = 6;
1101
+
1102
+ // Coin supérieur gauche
1103
+ if (overflow.left || overflow.top) {
1104
+ ctx.fillRect(compLeft, compTop, markerSize, markerSize);
1105
+ }
1106
+
1107
+ // Coin supérieur droit
1108
+ if (overflow.right || overflow.top) {
1109
+ ctx.fillRect(compRight - markerSize, compTop, markerSize, markerSize);
1110
+ }
1111
+
1112
+ // Coin inférieur gauche
1113
+ if (overflow.left || overflow.bottom) {
1114
+ ctx.fillRect(compLeft, compBottom - markerSize, markerSize, markerSize);
1115
+ }
1116
+
1117
+ // Coin inférieur droit
1118
+ if (overflow.right || overflow.bottom) {
1119
+ ctx.fillRect(compRight - markerSize, compBottom - markerSize, markerSize, markerSize);
1120
+ }
1121
+
1122
+ // 3. Texte d'information (optionnel)
1123
+ if (this.debbug && comp.text) {
1124
+ ctx.fillStyle = 'red';
1125
+ ctx.font = '10px monospace';
1126
+ ctx.textAlign = 'left';
1127
+
1128
+ const overflowText = [];
1129
+ if (overflow.left) overflowText.push('←');
1130
+ if (overflow.right) overflowText.push('→');
1131
+ if (overflow.top) overflowText.push('↑');
1132
+ if (overflow.bottom) overflowText.push('↓');
1133
+
1134
+ if (overflowText.length > 0) {
1135
+ ctx.fillText(
1136
+ `"${comp.text.substring(0, 10)}${comp.text.length > 10 ? '...' : ''}" ${overflowText.join('')}`,
1137
+ compLeft + 5,
1138
+ compTop - 5
1139
+ );
1140
+ }
1141
+ }
1142
+
1143
+ ctx.restore();
1144
+ }
1145
+ }
1146
+
1147
+ startRenderLoop() {
1148
+ const render = () => {
1149
+ // 1️⃣ Scroll inertia
1150
+ if (Math.abs(this.scrollVelocity) > 0.1 && !this.isDragging) {
1151
+ this.scrollOffset += this.scrollVelocity;
1152
+ this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -this.getMaxScroll());
1153
+ this.scrollVelocity *= this.scrollFriction;
1154
+ } else {
1155
+ this.scrollVelocity = 0;
1156
+ }
1157
+
1158
+ // 2️⃣ Clear canvas
1159
+ this.ctx.clearRect(0, 0, this.width, this.height);
1160
+
1161
+ // 3️⃣ Transition handling
1162
+ if (this.transitionState.isTransitioning) {
1163
+ this.updateTransition();
1164
+ } else if (this.optimizationEnabled && this.dirtyComponents.size > 0) {
1165
+ // Dirty components redraw
1166
+ for (let comp of this.dirtyComponents) {
1167
+ if (comp.visible) {
1168
+ const isFixed = this.isFixedComponent(comp);
1169
+ const y = isFixed ? comp.y : comp.y + this.scrollOffset;
1170
+
1171
+ this.ctx.clearRect(comp.x - 2, y - 2, comp.width + 4, comp.height + 4);
1172
+
1173
+ this.ctx.save();
1174
+ if (!isFixed) this.ctx.translate(0, this.scrollOffset);
1175
+ comp.draw(this.ctx);
1176
+ this.ctx.restore();
1177
+
1178
+ // Overflow indicator style Flutter
1179
+ const overflow = comp.getOverflow?.();
1180
+ if (comp.markClean) comp.markClean();
1181
+ }
1182
+ }
1183
+ this.dirtyComponents.clear();
1184
+ } else {
1185
+ // Full redraw
1186
+ const scrollableComponents = [];
1187
+ const fixedComponents = [];
1188
+
1189
+ for (let comp of this.components) {
1190
+ if (this.isFixedComponent(comp)) fixedComponents.push(comp);
1191
+ else scrollableComponents.push(comp);
1192
+ }
1193
+
1194
+ // Scrollable
1195
+ this.ctx.save();
1196
+ this.ctx.translate(0, this.scrollOffset);
1197
+ for (let comp of scrollableComponents) {
1198
+ if (comp.visible) {
1199
+ comp.draw(this.ctx);
1200
+ }
1201
+ }
1202
+ this.ctx.restore();
1203
+
1204
+ // Fixed
1205
+ for (let comp of fixedComponents) {
1206
+ if (comp.visible) {
1207
+ comp.draw(this.ctx);
1208
+ }
1209
+ }
1210
+ }
1211
+
1212
+ // 4️⃣ FPS
1213
+ this._frames++;
1214
+ const now = performance.now();
1215
+ if (now - this._lastFpsTime >= 1000) {
1216
+ this.fps = this._frames;
1217
+ this._frames = 0;
1218
+ this._lastFpsTime = now;
1219
+ }
1220
+
1221
+ if (this.showFps) {
1222
+ this.ctx.save();
1223
+ this.ctx.fillStyle = 'lime';
1224
+ this.ctx.font = '16px monospace';
1225
+ this.ctx.fillText(`FPS: ${this.fps}`, 10, 20);
1226
+ this.ctx.restore();
1227
+ }
1228
+
1229
+ if(this.debbug) {
1230
+ this.drawOverflowIndicators();
1231
+ }
1232
+
1233
+ requestAnimationFrame(render);
1234
+ };
1235
+
1236
+ render();
1237
+ }
1238
+
1239
+ isFixedComponent(comp) {
1240
+ return comp instanceof AppBar ||
1241
+ comp instanceof BottomNavigationBar ||
1242
+ comp instanceof Drawer ||
1243
+ comp instanceof Dialog ||
1244
+ comp instanceof Modal ||
1245
+ comp instanceof FAB ||
1246
+ comp instanceof BottomSheet ||
1247
+ comp instanceof ContextMenu ||
1248
+ comp instanceof OpenStreetMap ||
1249
+ comp instanceof SelectDialog;
1250
+ }
1251
+
1252
+ showToast(message, duration = 3000) {
1253
+ const toast = new Toast(this, {
1254
+ text: message,
1255
+ duration: duration,
1256
+ x: this.width / 2,
1257
+ y: this.height - 100
1258
+ });
1259
+ this.add(toast);
1260
+ toast.show();
1261
+ }
1262
+ }
1263
+
1264
+ export default CanvasFramework;
1265
+
1266
+
1267
+
1268
+
1269
+
1270
+
1271
+