canvasframework 0.5.16 → 0.5.18

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 (112) hide show
  1. package/dist/canvasframework.js +2 -0
  2. package/dist/canvasframework.js.LICENSE.txt +1 -0
  3. package/package.json +18 -17
  4. package/components/Accordion.js +0 -265
  5. package/components/AndroidDatePickerDialog.js +0 -406
  6. package/components/AppBar.js +0 -398
  7. package/components/AudioPlayer.js +0 -611
  8. package/components/Avatar.js +0 -202
  9. package/components/Banner.js +0 -342
  10. package/components/BottomNavigationBar.js +0 -433
  11. package/components/BottomSheet.js +0 -234
  12. package/components/Button.js +0 -360
  13. package/components/Camera.js +0 -644
  14. package/components/Card.js +0 -193
  15. package/components/Chart.js +0 -700
  16. package/components/Checkbox.js +0 -166
  17. package/components/Chip.js +0 -212
  18. package/components/CircularProgress.js +0 -327
  19. package/components/ContextMenu.js +0 -116
  20. package/components/DatePicker.js +0 -298
  21. package/components/Dialog.js +0 -337
  22. package/components/Divider.js +0 -125
  23. package/components/Drawer.js +0 -276
  24. package/components/FAB.js +0 -270
  25. package/components/FileUpload.js +0 -315
  26. package/components/FloatedCamera.js +0 -644
  27. package/components/IOSDatePickerWheel.js +0 -430
  28. package/components/ImageCarousel.js +0 -219
  29. package/components/ImageComponent.js +0 -223
  30. package/components/Input.js +0 -831
  31. package/components/InputDatalist.js +0 -723
  32. package/components/InputTags.js +0 -624
  33. package/components/List.js +0 -95
  34. package/components/ListItem.js +0 -269
  35. package/components/Modal.js +0 -364
  36. package/components/MorphingFAB.js +0 -428
  37. package/components/MultiSelectDialog.js +0 -206
  38. package/components/NumberInput.js +0 -271
  39. package/components/PasswordInput.js +0 -462
  40. package/components/ProgressBar.js +0 -88
  41. package/components/QRCodeReader.js +0 -539
  42. package/components/RadioButton.js +0 -151
  43. package/components/SearchInput.js +0 -315
  44. package/components/SegmentedControl.js +0 -357
  45. package/components/Select.js +0 -199
  46. package/components/SelectDialog.js +0 -255
  47. package/components/Slider.js +0 -113
  48. package/components/SliverAppBar.js +0 -139
  49. package/components/Snackbar.js +0 -243
  50. package/components/SpeedDialFAB.js +0 -397
  51. package/components/Stepper.js +0 -281
  52. package/components/SwipeableListItem.js +0 -327
  53. package/components/Switch.js +0 -147
  54. package/components/Table.js +0 -492
  55. package/components/Tabs.js +0 -423
  56. package/components/Text.js +0 -141
  57. package/components/TextField.js +0 -151
  58. package/components/TimePicker.js +0 -934
  59. package/components/Toast.js +0 -236
  60. package/components/TreeView.js +0 -420
  61. package/components/Video.js +0 -397
  62. package/components/View.js +0 -140
  63. package/components/VirtualList.js +0 -120
  64. package/core/CanvasFramework.js +0 -3034
  65. package/core/Component.js +0 -243
  66. package/core/ThemeManager.js +0 -358
  67. package/core/UIBuilder.js +0 -267
  68. package/core/WebGLCanvasAdapter.js +0 -782
  69. package/features/Column.js +0 -43
  70. package/features/Grid.js +0 -47
  71. package/features/LayoutComponent.js +0 -43
  72. package/features/OpenStreetMap.js +0 -310
  73. package/features/Positioned.js +0 -33
  74. package/features/PullToRefresh.js +0 -328
  75. package/features/Row.js +0 -40
  76. package/features/SignaturePad.js +0 -257
  77. package/features/Skeleton.js +0 -193
  78. package/features/Stack.js +0 -21
  79. package/index.js +0 -119
  80. package/manager/AccessibilityManager.js +0 -107
  81. package/manager/ErrorHandler.js +0 -59
  82. package/manager/FeatureFlags.js +0 -60
  83. package/manager/MemoryManager.js +0 -107
  84. package/manager/PerformanceMonitor.js +0 -84
  85. package/manager/SecurityManager.js +0 -54
  86. package/utils/AnimationEngine.js +0 -734
  87. package/utils/CryptoManager.js +0 -303
  88. package/utils/DataStore.js +0 -403
  89. package/utils/DevTools.js +0 -1618
  90. package/utils/DevToolsConsole.js +0 -201
  91. package/utils/EventBus.js +0 -407
  92. package/utils/FetchClient.js +0 -74
  93. package/utils/FirebaseAuth.js +0 -653
  94. package/utils/FirebaseCore.js +0 -246
  95. package/utils/FirebaseFirestore.js +0 -581
  96. package/utils/FirebaseFunctions.js +0 -97
  97. package/utils/FirebaseRealtimeDB.js +0 -498
  98. package/utils/FirebaseStorage.js +0 -612
  99. package/utils/FormValidator.js +0 -355
  100. package/utils/GeoLocationService.js +0 -62
  101. package/utils/I18n.js +0 -207
  102. package/utils/IndexedDBManager.js +0 -273
  103. package/utils/InspectionOverlay.js +0 -308
  104. package/utils/NotificationManager.js +0 -60
  105. package/utils/OfflineSyncManager.js +0 -342
  106. package/utils/PayPalPayment.js +0 -678
  107. package/utils/QueryBuilder.js +0 -478
  108. package/utils/SafeArea.js +0 -64
  109. package/utils/SecureStorage.js +0 -289
  110. package/utils/StateManager.js +0 -207
  111. package/utils/StripePayment.js +0 -552
  112. package/utils/WebSocketClient.js +0 -66
@@ -1,3034 +0,0 @@
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 SpeedDialFAB from '../components/SpeedDialFAB.js';
10
- import MorphingFAB from '../components/MorphingFAB.js';
11
- import CircularProgress from '../components/CircularProgress.js';
12
- import ImageComponent from '../components/ImageComponent.js';
13
- import DatePicker from '../components/DatePicker.js';
14
- import IOSDatePickerWheel from '../components/IOSDatePickerWheel.js';
15
- import AndroidDatePickerDialog from '../components/AndroidDatePickerDialog.js';
16
- import Avatar from '../components/Avatar.js';
17
- import Snackbar from '../components/Snackbar.js';
18
- import BottomNavigationBar from '../components/BottomNavigationBar.js';
19
- import Video from '../components/Video.js';
20
- import Modal from '../components/Modal.js';
21
- import Drawer from '../components/Drawer.js';
22
- import AppBar from '../components/AppBar.js';
23
- import Chip from '../components/Chip.js';
24
- import Stepper from '../components/Stepper.js';
25
- import Accordion from '../components/Accordion.js';
26
- import Tabs from '../components/Tabs.js';
27
- import Switch from '../components/Switch.js';
28
- import SwipeableListItem from '../components/SwipeableListItem.js';
29
- import ListItem from '../components/ListItem.js';
30
- import List from '../components/List.js';
31
- import VirtualList from '../components/VirtualList.js';
32
- import BottomSheet from '../components/BottomSheet.js';
33
- import ProgressBar from '../components/ProgressBar.js';
34
- import RadioButton from '../components/RadioButton.js';
35
- import Dialog from '../components/Dialog.js';
36
- import ContextMenu from '../components/ContextMenu.js';
37
- import Checkbox from '../components/Checkbox.js';
38
- import Toast from '../components/Toast.js';
39
- import NumberInput from '../components/NumberInput.js';
40
- import TextField from '../components/TextField.js';
41
- import SelectDialog from '../components/SelectDialog.js';
42
- import Select from '../components/Select.js';
43
- import MultiSelectDialog from '../components/MultiSelectDialog.js';
44
- import Divider from '../components/Divider.js';
45
- import FileUpload from '../components/FileUpload.js';
46
- import Table from '../components/Table.js';
47
- import TreeView from '../components/TreeView.js';
48
- import SearchInput from '../components/SearchInput.js';
49
- import ImageCarousel from '../components/ImageCarousel.js';
50
- import PasswordInput from '../components/PasswordInput.js';
51
- import InputTags from '../components/InputTags.js';
52
- import InputDatalist from '../components/InputDatalist.js';
53
- import Banner from '../components/Banner.js';
54
- import Chart from '../components/Chart.js';
55
- import SliverAppBar from '../components/SliverAppBar.js';
56
- import AudioPlayer from '../components/AudioPlayer.js';
57
- import Camera from '../components/Camera.js';
58
- import FloatedCamera from '../components/FloatedCamera.js';
59
- import TimePicker from '../components/TimePicker.js';
60
- import QRCodeReader from '../components/QRCodeReader.js';
61
-
62
- // Utils
63
- import SafeArea from '../utils/SafeArea.js';
64
- import StateManager from '../utils/StateManager.js';
65
- import I18n from '../utils/I18n.js';
66
- import SecureStorage from '../utils/SecureStorage.js';
67
- import FormValidator from '../utils/FormValidator.js';
68
- import DataStore from '../utils/DataStore.js';
69
- import EventBus from '../utils/EventBus.js';
70
- import IndexedDBManager from '../utils/IndexedDBManager.js';
71
- import QueryBuilder from '../utils/QueryBuilder.js';
72
- import OfflineSyncManager from '../utils/OfflineSyncManager.js';
73
- import FetchClient from '../utils/FetchClient.js';
74
- import GeoLocationService from '../utils/GeoLocationService.js';
75
- import WebSocketClient from '../utils/WebSocketClient.js';
76
- import AnimationEngine from '../utils/AnimationEngine.js';
77
- import CryptoManager from '../utils/CryptoManager.js';
78
- import NotificationManager from '../utils/NotificationManager.js';
79
-
80
- // DevTools
81
- import DevTools from '../utils/DevTools.js';
82
- import InspectionOverlay from '../utils/InspectionOverlay.js';
83
- import DevToolsConsole from '../utils/DevToolsConsole.js';
84
-
85
- // Features
86
- import PullToRefresh from '../features/PullToRefresh.js';
87
- import Skeleton from '../features/Skeleton.js';
88
- import SignaturePad from '../features/SignaturePad.js';
89
- import OpenStreetMap from '../features/OpenStreetMap.js';
90
- import LayoutComponent from '../features/LayoutComponent.js';
91
- import Grid from '../features/Grid.js';
92
- import Row from '../features/Row.js';
93
- import Column from '../features/Column.js';
94
- import Positioned from '../features/Positioned.js';
95
- import Stack from '../features/Stack.js';
96
-
97
- // Manager
98
- import ErrorHandler from '../manager/ErrorHandler.js';
99
- import PerformanceMonitor from '../manager/PerformanceMonitor.js';
100
- import AccessibilityManager from '../manager/AccessibilityManager.js';
101
- import MemoryManager from '../manager/MemoryManager.js';
102
- import SecurityManager from '../manager/SecurityManager.js';
103
- import FeatureFlags from '../manager/FeatureFlags.js';
104
-
105
- // WebGL Adapter
106
- import WebGLCanvasAdapter from './WebGLCanvasAdapter.js';
107
- import ui, {
108
- createRef
109
- } from './UIBuilder.js';
110
- import ThemeManager from './ThemeManager.js';
111
-
112
-
113
- const FIXED_COMPONENT_TYPES = new Set([
114
- AppBar,
115
- BottomNavigationBar,
116
- Drawer,
117
- Dialog,
118
- Modal,
119
- Tabs,
120
- FAB,
121
- Toast,
122
- Camera,
123
- QRCodeReader,
124
- Banner,
125
- SliverAppBar,
126
- BottomSheet,
127
- ContextMenu,
128
- OpenStreetMap,
129
- SelectDialog
130
- ]);
131
-
132
- /**
133
- * Framework principal pour créer des interfaces utilisateur basées sur Canvas
134
- * @class
135
- * @property {HTMLCanvasElement} canvas - Élément canvas HTML
136
- * @property {CanvasRenderingContext2D} ctx - Contexte 2D du canvas
137
- * @property {number} width - Largeur du canvas
138
- * @property {number} height - Hauteur du canvas
139
- * @property {number} dpr - Device Pixel Ratio
140
- * @property {string} platform - Plateforme détectée ('material' ou 'cupertino')
141
- * @property {Component[]} components - Liste des composants
142
- * @property {Map} routes - Routes de navigation
143
- * @property {string} currentRoute - Route actuelle
144
- * @property {Object} state - État global
145
- * @property {boolean} isDragging - Indique si un drag est en cours
146
- * @property {number} lastTouchY - Dernière position Y du touch
147
- * @property {number} scrollOffset - Offset de défilement
148
- * @property {number} scrollVelocity - Vitesse de défilement
149
- * @property {number} scrollFriction - Friction du défilement
150
- */
151
- class CanvasFramework {
152
- /**
153
- * Crée une instance de CanvasFramework
154
- * @param {string} canvasId - ID de l'élément canvas
155
- */
156
- constructor(canvasId, options = {}) {
157
- // ✅ AJOUTER: Démarrer le chronomètre
158
- const startTime = performance.now();
159
-
160
- this.metrics = {
161
- initTime: 0,
162
- firstRenderTime: null,
163
- firstInteractionTime: null,
164
- totalStartupTime: null
165
- };
166
- this._firstRenderDone = false;
167
- this._startupStartTime = startTime;
168
-
169
- // ✅ Créer automatiquement le canvas
170
- this.canvas = document.createElement('canvas');
171
- this.canvas.id = canvasId || `canvas-${Date.now()}`;
172
- this.canvas.style.display = 'block';
173
- this.canvas.style.touchAction = 'none';
174
- this.canvas.style.userSelect = 'none';
175
- document.body.appendChild(this.canvas);
176
-
177
- // NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
178
- this.useWebGL = options.useWebGL ?? false; // utilise la valeur si fournie, sinon false
179
-
180
- // Initialiser le contexte approprié
181
- if (this.useWebGL) {
182
- try {
183
- this.ctx = new WebGLCanvasAdapter(this.canvas, {
184
- dpr: this.dpr,
185
- alpha: false
186
- });
187
- } catch (err) {
188
- console.warn("Échec de l’initialisation WebGLCanvasAdapter → fallback Canvas 2D", err);
189
- this.ctx = this.canvas.getContext('2d', {
190
- alpha: false,
191
- desynchronized: true,
192
- willReadFrequently: false
193
- });
194
- this.useWebGL = false;
195
- }
196
- } else {
197
- this.ctx = this.canvas.getContext('2d', {
198
- alpha: false,
199
- desynchronized: true,
200
- willReadFrequently: false
201
- });
202
- }
203
- //this.ctx.scale(this.dpr, this.dpr);
204
-
205
- this.backgroundColor = options.backgroundColor || '#f5f5f5'; // Blanc par défaut
206
-
207
- this.width = window.innerWidth;
208
- this.height = window.innerHeight;
209
- this.dpr = window.devicePixelRatio || 1;
210
-
211
- // ✅ OPTIMISATION OPTION 2: Configuration des optimisations
212
- this.optimizations = {
213
- enabled: options.optimizations !== false, // Activé par défaut
214
- useDoubleBuffering: true,
215
- useCaching: true,
216
- useBatchDrawing: true,
217
- useSpatialPartitioning: false, // Désactivé par défaut (à activer si beaucoup de composants)
218
- useImageDataOptimization: true
219
- };
220
-
221
- // ✅ OPTIMISATION OPTION 2: Cache pour éviter les changements d'état inutiles
222
- this._stateCache = {
223
- fillStyle: null,
224
- strokeStyle: null,
225
- font: null,
226
- textAlign: null,
227
- textBaseline: null,
228
- lineWidth: null,
229
- globalAlpha: 1
230
- };
231
-
232
- // ✅ OPTIMISATION OPTION 2: Cache des images/textes
233
- this.imageCache = new Map();
234
- this.textCache = new Map();
235
-
236
- // ✅ OPTIMISATION OPTION 2: Double buffering
237
- this._doubleBuffer = null;
238
- this._bufferCtx = null;
239
- if (this.optimizations.useDoubleBuffering) {
240
- this._initDoubleBuffer();
241
- }
242
-
243
- // Scroll Worker
244
- this.scrollWorker = this.createScrollWorker();
245
- this.scrollWorker.onmessage = this.handleScrollWorkerMessage.bind(this);
246
-
247
- this.splashOptions = {
248
- enabled: options.splash?.enabled === true, // false par défaut
249
- duration: options.splash?.duration || 700,
250
- fadeOutDuration: options.splash?.fadeOutDuration || 500,
251
- backgroundColor: options.splash?.backgroundColor || ['#667eea', '#764ba2'], // Gradient ou couleur unie
252
- spinnerColor: options.splash?.spinnerColor || 'white',
253
- spinnerBackground: options.splash?.spinnerBackground || 'rgba(255, 255, 255, 0.3)',
254
- textColor: options.splash?.textColor || 'white',
255
- text: options.splash?.text || 'Chargement...',
256
- textSize: options.splash?.textSize || 20,
257
- textFont: options.splash?.textFont || 'Arial',
258
- progressBarColor: options.splash?.progressBarColor || 'white',
259
- progressBarBackground: options.splash?.progressBarBackground || 'rgba(255, 255, 255, 0.3)',
260
- showProgressBar: options.splash?.showProgressBar !== false, // true par défaut
261
- logo: options.splash?.logo || null, // URL d'une image (optionnel)
262
- logoWidth: options.splash?.logoWidth || 100,
263
- logoHeight: options.splash?.logoHeight || 100
264
- };
265
-
266
- // ✅ MODIFIER : Vérifier si le splash est activé
267
- if (this.splashOptions.enabled) {
268
- this.showSplashScreen();
269
- } else {
270
- this._splashFinished = true; // Passer directement au rendu
271
- }
272
-
273
- this.platform = this.detectPlatform();
274
- setTimeout(() => {
275
- this.initScrollWorker();
276
- }, 100);
277
- // État actuel + préférence
278
- this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
279
- this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
280
-
281
- // Applique le thème initial
282
- this.setupSystemThemeListener();
283
-
284
- // Récupère override utilisateur
285
- const savedOverride = localStorage.getItem('themeOverride');
286
- if (savedOverride && ['light', 'dark'].includes(savedOverride)) {
287
- this.userThemeOverride = savedOverride;
288
- this.themeMode = savedOverride;
289
- }
290
-
291
- this.components = [];
292
- // ✅ AJOUTER ICI :
293
- this._cachedMaxScroll = 0;
294
- this._maxScrollDirty = true;
295
- this.resizeTimeout = null;
296
-
297
- //this.applyThemeFromSystem();
298
- this.state = {};
299
-
300
- // Calcule FPS
301
- this.fps = 0;
302
- this._frames = 0;
303
- this._lastFpsTime = performance.now();
304
- this.showFps = options.showFps || false; // false par défaut
305
- this.debbug = options.debug || false; // false par défaut (et correction de la faute de frappe)
306
-
307
- // Worker pour multithreading Canvas Worker
308
- this.worker = this.createCanvasWorker();
309
- this.worker.onmessage = this.handleWorkerMessage.bind(this);
310
- this.worker.postMessage({
311
- type: 'INIT',
312
- payload: {
313
- components: []
314
- }
315
- });
316
-
317
- // Logic Worker
318
- this.logicWorker = this.createLogicWorker();
319
- this.logicWorker.onmessage = this.handleLogicWorkerMessage.bind(this);
320
- this.logicWorkerState = {};
321
- this.logicWorker.postMessage({
322
- type: 'SET_STATE',
323
- payload: this.state
324
- });
325
-
326
- // Envoyer l'état initial au worker
327
- this.logicWorker.postMessage({
328
- type: 'SET_STATE',
329
- payload: this.state
330
- });
331
-
332
- // Gestion des événements
333
- this.isDragging = false;
334
- this.lastTouchY = 0;
335
- this.scrollOffset = 0;
336
- this.scrollVelocity = 0;
337
- this.scrollFriction = 0.95;
338
-
339
- // Optimisation
340
- this.dirtyComponents = new Set();
341
- this.optimizationEnabled = this.optimizations.enabled;
342
-
343
- // AJOUTER CETTE LIGNE
344
- this.animator = new AnimationEngine();
345
-
346
- // ===== NOUVEAU SYSTÈME DE ROUTING =====
347
- this.routes = new Map();
348
- this.currentRoute = '/';
349
- this.currentParams = {};
350
- this.currentQuery = {};
351
- this.history = [];
352
- this.historyIndex = -1;
353
-
354
- // Animation de transition
355
- this.transitionState = {
356
- isTransitioning: false,
357
- progress: 0,
358
- duration: 300,
359
- type: 'slide', // 'slide', 'fade', 'none'
360
- direction: 'forward', // 'forward', 'back'
361
- oldComponents: [],
362
- newComponents: []
363
- };
364
-
365
- this.setupCanvas();
366
-
367
- // ✅ OPTIMISATION OPTION 5: Désactiver l'antialiasing pour meilleures performances
368
- this._disableImageSmoothing();
369
-
370
- this.setupEventListeners();
371
- this.setupHistoryListener();
372
-
373
- this.startRenderLoop();
374
-
375
- this.devTools = new DevTools(this);
376
- this.inspectionOverlay = new InspectionOverlay(this);
377
-
378
- // MODIFICATION: Vérifier explicitement l'option enableDevTools
379
- const shouldEnableDevTools = options.enableDevTools === true;
380
-
381
- if (shouldEnableDevTools) {
382
- this.enableDevTools();
383
- console.log('DevTools enabled. Press Ctrl+Shift+D to toggle.');
384
- }
385
-
386
- // Initialiser le ThemeManager
387
- this.themeManager = new ThemeManager(this, {
388
- lightTheme: options.lightTheme,
389
- darkTheme: options.darkTheme,
390
- storageKey: options.themeStorageKey || 'app-theme-mode'
391
- });
392
-
393
- // Raccourci pour accéder au thème actuel
394
- this.theme = this.themeManager.getTheme();
395
-
396
- // ✅ AJOUTER: Mesurer le temps d'init
397
- const initTime = performance.now() - startTime;
398
-
399
- // ✅ AJOUTER: Stocker les métriques
400
- this.metrics = {
401
- initTime: initTime,
402
- firstRenderTime: null,
403
- firstInteractionTime: null,
404
- totalStartupTime: null
405
- };
406
-
407
- // ✅ AJOUTER: Logger si debug
408
- if (options.debug || options.showMetrics) {
409
- console.log(`⚡ Framework initialisé en ${initTime.toFixed(2)}ms`);
410
- }
411
-
412
- // ✅ AJOUTER: Marquer le premier rendu
413
- this._firstRenderDone = false;
414
- this._startupStartTime = startTime;
415
-
416
- // ✅ OPTIMISATION OPTION 5: Partition spatiale pour le culling (optionnel)
417
- if (this.optimizations.useSpatialPartitioning) {
418
- this._initSpatialPartitioning();
419
- }
420
- }
421
-
422
- /**
423
- * Crée un élément DOM temporaire, l'ajoute au body, exécute une callback, puis le supprime
424
- * @param {string} tagName - 'input', 'select', 'textarea', etc.
425
- * @param {Object} props - propriétés de base (type, value, accept, placeholder...)
426
- * @param {Function} onResult - callback quand l'élément change ou blur (reçoit l'élément)
427
- * @param {Object} [position=null] - {left, top, width, height} en pixels
428
- * @param {Object} [attributes={}] - attributs supplémentaires (id, className, data-*, etc.)
429
- * @returns {HTMLElement} L'élément créé (avant suppression)
430
- */
431
- createTemporaryDomElement(tagName, props = {}, onResult, position = null, attributes = {}) {
432
- const el = document.createElement(tagName);
433
-
434
- // Appliquer les propriétés de base
435
- Object.assign(el, props);
436
-
437
- // Appliquer les attributs personnalisés (id, class, data-*, etc.)
438
- Object.entries(attributes).forEach(([key, value]) => {
439
- if (key === 'className') {
440
- el.className = value; // className est spécial en JS
441
- } else {
442
- el.setAttribute(key, value);
443
- }
444
- });
445
-
446
- // Styles de base pour le rendre invisible
447
- el.style.position = 'absolute';
448
- el.style.opacity = '0';
449
- el.style.zIndex = '9999';
450
-
451
- // Positionnement optionnel
452
- if (position) {
453
- Object.assign(el.style, {
454
- left: `${position.left}px`,
455
- top: `${position.top}px`,
456
- width: `${position.width}px`,
457
- height: `${position.height}px`
458
- });
459
- }
460
-
461
- document.body.appendChild(el);
462
-
463
- const cleanup = () => {
464
- el.remove();
465
- document.removeEventListener('focusout', cleanup);
466
- };
467
-
468
- // Événements
469
- el.addEventListener('change', (e) => {
470
- onResult(e.target);
471
- cleanup();
472
- });
473
-
474
- el.addEventListener('blur', cleanup);
475
-
476
- // Focus automatique pour les champs saisissables
477
- if (['input', 'select', 'textarea'].includes(tagName.toLowerCase())) {
478
- el.focus();
479
- }
480
-
481
- return el;
482
- }
483
-
484
- /**
485
- * Crée le Worker pour le calcul du scroll
486
- */
487
- createScrollWorker() {
488
- const workerCode = `
489
- let state = {
490
- scrollOffset: 0,
491
- scrollVelocity: 0,
492
- scrollFriction: 0.95,
493
- isDragging: false,
494
- maxScroll: 0,
495
- height: 0,
496
- lastTouchY: 0,
497
- components: []
498
- };
499
-
500
- const FIXED_COMPONENT_TYPES = [
501
- 'AppBar', 'BottomNavigationBar', 'Drawer', 'Dialog', 'Modal',
502
- 'Tabs', 'FAB', 'Toast', 'Camera', 'QRCodeReader', 'Banner',
503
- 'SliverAppBar', 'BottomSheet', 'ContextMenu', 'OpenStreetMap', 'SelectDialog'
504
- ];
505
-
506
- const calculateMaxScroll = () => {
507
- let maxY = 0;
508
-
509
- for (const comp of state.components) {
510
- if (FIXED_COMPONENT_TYPES.includes(comp.type) || !comp.visible) continue;
511
- const bottom = comp.y + comp.height;
512
- if (bottom > maxY) maxY = bottom;
513
- }
514
-
515
- return Math.max(0, maxY - state.height + 50);
516
- };
517
-
518
- const updateInertia = () => {
519
- if (Math.abs(state.scrollVelocity) > 0.1 && !state.isDragging) {
520
- state.scrollOffset += state.scrollVelocity;
521
- state.maxScroll = calculateMaxScroll();
522
- state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
523
- state.scrollVelocity *= state.scrollFriction;
524
- } else {
525
- state.scrollVelocity = 0;
526
- }
527
-
528
- return {
529
- scrollOffset: state.scrollOffset,
530
- scrollVelocity: state.scrollVelocity,
531
- maxScroll: state.maxScroll
532
- };
533
- };
534
-
535
- const handleTouchMove = (deltaY) => {
536
- if (state.isDragging) {
537
- state.scrollOffset += deltaY;
538
- state.maxScroll = calculateMaxScroll();
539
- state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
540
- state.scrollVelocity = deltaY;
541
- return {
542
- scrollOffset: state.scrollOffset,
543
- scrollVelocity: state.scrollVelocity,
544
- maxScroll: state.maxScroll
545
- };
546
- }
547
- return null;
548
- };
549
-
550
- self.onmessage = (e) => {
551
- const { type, payload } = e.data;
552
-
553
- switch (type) {
554
- case 'INIT':
555
- state = {
556
- ...state,
557
- ...payload
558
- };
559
- state.maxScroll = calculateMaxScroll();
560
- self.postMessage({
561
- type: 'INITIALIZED',
562
- payload: {
563
- scrollOffset: state.scrollOffset,
564
- maxScroll: state.maxScroll
565
- }
566
- });
567
- break;
568
-
569
- case 'UPDATE_COMPONENTS':
570
- state.components = payload.components;
571
- state.maxScroll = calculateMaxScroll();
572
- self.postMessage({
573
- type: 'MAX_SCROLL_UPDATED',
574
- payload: { maxScroll: state.maxScroll }
575
- });
576
- break;
577
-
578
- case 'UPDATE_DIMENSIONS':
579
- state.height = payload.height;
580
- state.maxScroll = calculateMaxScroll();
581
- self.postMessage({
582
- type: 'MAX_SCROLL_UPDATED',
583
- payload: { maxScroll: state.maxScroll }
584
- });
585
- break;
586
-
587
- case 'SET_DRAGGING':
588
- state.isDragging = payload.isDragging;
589
- if (!payload.isDragging) {
590
- state.scrollVelocity = payload.lastVelocity || 0;
591
- } else {
592
- state.lastTouchY = payload.lastTouchY || 0;
593
- }
594
- break;
595
-
596
- case 'HANDLE_TOUCH_MOVE':
597
- const result = handleTouchMove(payload.deltaY);
598
- if (result) {
599
- self.postMessage({
600
- type: 'SCROLL_UPDATED',
601
- payload: result
602
- });
603
- }
604
- break;
605
-
606
- case 'UPDATE_INERTIA':
607
- const inertiaResult = updateInertia();
608
- self.postMessage({
609
- type: 'SCROLL_UPDATED',
610
- payload: inertiaResult
611
- });
612
- break;
613
-
614
- case 'SET_SCROLL_OFFSET':
615
- state.scrollOffset = payload.scrollOffset;
616
- state.maxScroll = calculateMaxScroll();
617
- state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
618
- self.postMessage({
619
- type: 'SCROLL_UPDATED',
620
- payload: {
621
- scrollOffset: state.scrollOffset,
622
- maxScroll: state.maxScroll
623
- }
624
- });
625
- break;
626
-
627
- case 'GET_STATE':
628
- self.postMessage({
629
- type: 'STATE',
630
- payload: {
631
- scrollOffset: state.scrollOffset,
632
- scrollVelocity: state.scrollVelocity,
633
- maxScroll: state.maxScroll,
634
- isDragging: state.isDragging
635
- }
636
- });
637
- break;
638
- }
639
- };
640
- `;
641
-
642
- const blob = new Blob([workerCode], {
643
- type: 'application/javascript'
644
- });
645
- return new Worker(URL.createObjectURL(blob));
646
- }
647
-
648
- /**
649
- * Gère les messages du Scroll Worker
650
- */
651
- handleScrollWorkerMessage(e) {
652
- const {
653
- type,
654
- payload
655
- } = e.data;
656
-
657
- switch (type) {
658
- case 'SCROLL_UPDATED':
659
- this.scrollOffset = payload.scrollOffset;
660
- this.scrollVelocity = payload.scrollVelocity;
661
- // ✅ CORRECTION IMPORTANTE : Vider le cache dirty pendant le scroll
662
- if (Math.abs(payload.scrollVelocity) > 0.5) {
663
- this.dirtyComponents.clear();
664
- }
665
- // Mettre à jour le cache
666
- this._cachedMaxScroll = payload.maxScroll;
667
- this._maxScrollDirty = false;
668
-
669
- // Marquer les composants comme sales pour mise à jour visuelle
670
- if (Math.abs(payload.scrollVelocity) > 0) {
671
- this.components.forEach(comp => {
672
- if (!this.isFixedComponent(comp)) {
673
- this.markComponentDirty(comp);
674
- }
675
- });
676
- }
677
- break;
678
-
679
- case 'MAX_SCROLL_UPDATED':
680
- this._cachedMaxScroll = payload.maxScroll;
681
- this._maxScrollDirty = false;
682
- break;
683
-
684
- case 'INITIALIZED':
685
- this.scrollOffset = payload.scrollOffset;
686
- this._cachedMaxScroll = payload.maxScroll;
687
- this._maxScrollDirty = false;
688
- break;
689
-
690
- case 'STATE':
691
- // Synchroniser l'état local
692
- this.scrollOffset = payload.scrollOffset;
693
- this.scrollVelocity = payload.scrollVelocity;
694
- this.isDragging = payload.isDragging;
695
- this._cachedMaxScroll = payload.maxScroll;
696
- this._maxScrollDirty = false;
697
- break;
698
- }
699
- }
700
-
701
- /**
702
- * Initialise le Scroll Worker avec les données actuelles
703
- */
704
- initScrollWorker() {
705
- const componentsData = this.components.map(comp => ({
706
- type: comp.constructor.name,
707
- y: comp.y,
708
- height: comp.height,
709
- visible: comp.visible
710
- }));
711
-
712
- this.scrollWorker.postMessage({
713
- type: 'INIT',
714
- payload: {
715
- scrollOffset: this.scrollOffset,
716
- scrollVelocity: this.scrollVelocity,
717
- scrollFriction: this.scrollFriction,
718
- isDragging: this.isDragging,
719
- height: this.height,
720
- components: componentsData
721
- }
722
- });
723
- }
724
-
725
- /**
726
- * Met à jour les composants dans le Worker
727
- */
728
- updateScrollWorkerComponents() {
729
- const componentsData = this.components.map(comp => ({
730
- type: comp.constructor.name,
731
- y: comp.y,
732
- height: comp.height,
733
- visible: comp.visible
734
- }));
735
-
736
- this.scrollWorker.postMessage({
737
- type: 'UPDATE_COMPONENTS',
738
- payload: {
739
- components: componentsData
740
- }
741
- });
742
- }
743
-
744
- /**
745
- * Initialise le double buffering pour éviter le flickering
746
- * @private
747
- */
748
- // Dans _initDoubleBuffer(), assurez-vous de bien configurer le contexte
749
- _initDoubleBuffer() {
750
- this._doubleBuffer = document.createElement('canvas');
751
- this._bufferCtx = this._doubleBuffer.getContext('2d', {
752
- alpha: false,
753
- desynchronized: true
754
- });
755
- this._doubleBuffer.width = this.width * this.dpr;
756
- this._doubleBuffer.height = this.height * this.dpr;
757
- this._doubleBuffer.style.width = this.width + 'px';
758
- this._doubleBuffer.style.height = this.height + 'px';
759
- this._bufferCtx.scale(this.dpr, this.dpr);
760
- this._disableImageSmoothing(this._bufferCtx);
761
-
762
- // ✅ INITIALISER le background du buffer
763
- this._bufferCtx.fillStyle = this.backgroundColor || '#ffffff';
764
- this._bufferCtx.fillRect(0, 0, this.width, this.height);
765
- }
766
-
767
-
768
- /**
769
- * Désactive l'antialiasing pour meilleures performances
770
- * @private
771
- * @param {CanvasRenderingContext2D} [ctx=this.ctx] - Contexte à configurer
772
- */
773
- _disableImageSmoothing(ctx = this.ctx) {
774
- ctx.imageSmoothingEnabled = false;
775
- ctx.msImageSmoothingEnabled = false;
776
- ctx.webkitImageSmoothingEnabled = false;
777
- ctx.mozImageSmoothingEnabled = false;
778
- }
779
-
780
- /**
781
- * Initialise le spatial partitioning pour le viewport culling
782
- * @private
783
- */
784
- _initSpatialPartitioning() {
785
- // Simple grid spatial partitioning
786
- this._spatialGrid = {
787
- cellSize: 100,
788
- grid: new Map(),
789
- update: (components) => {
790
- this._spatialGrid.grid.clear();
791
- components.forEach(comp => {
792
- if (!comp.visible) return;
793
-
794
- const gridX = Math.floor(comp.x / this._spatialGrid.cellSize);
795
- const gridY = Math.floor(comp.y / this._spatialGrid.cellSize);
796
- const key = `${gridX},${gridY}`;
797
-
798
- if (!this._spatialGrid.grid.has(key)) {
799
- this._spatialGrid.grid.set(key, []);
800
- }
801
- this._spatialGrid.grid.get(key).push(comp);
802
- });
803
- },
804
- getVisible: (viewportY) => {
805
- const visible = [];
806
- const startY = viewportY - 200; // Marge de 200px
807
- const endY = viewportY + this.height + 200;
808
-
809
- this._spatialGrid.grid.forEach((comps, key) => {
810
- comps.forEach(comp => {
811
- const compBottom = comp.y + comp.height;
812
- if (compBottom >= startY && comp.y <= endY) {
813
- visible.push(comp);
814
- }
815
- });
816
- });
817
- return visible;
818
- }
819
- };
820
- }
821
-
822
- /**
823
- * ✅ OPTIMISATION OPTION 2: Rendu optimisé de rectangles
824
- * Évite les changements d'état inutiles
825
- * @param {number} x - Position X
826
- * @param {number} y - Position Y
827
- * @param {number} w - Largeur
828
- * @param {number} h - Hauteur
829
- * @param {string} color - Couleur de remplissage
830
- */
831
- fillRectOptimized(x, y, w, h, color) {
832
- // Éviter les changements d'état inutiles
833
- if (this._stateCache.fillStyle !== color) {
834
- this.ctx.fillStyle = color;
835
- this._stateCache.fillStyle = color;
836
- }
837
- this.ctx.fillRect(x, y, w, h);
838
- }
839
-
840
- /**
841
- * ✅ OPTIMISATION OPTION 2: Texte avec cache
842
- * Cache le rendu du texte pour éviter de le redessiner à chaque frame
843
- * @param {string} text - Texte à afficher
844
- * @param {number} x - Position X
845
- * @param {number} y - Position Y
846
- * @param {string} font - Police CSS
847
- * @param {string} color - Couleur du texte
848
- */
849
- fillTextCached(text, x, y, font, color) {
850
- const key = `${text}_${font}_${color}`;
851
-
852
- if (!this.textCache.has(key)) {
853
- // Rendu dans un canvas temporaire
854
- const temp = document.createElement('canvas');
855
- const tempCtx = temp.getContext('2d', {
856
- alpha: false
857
- });
858
- tempCtx.font = font;
859
-
860
- const metrics = tempCtx.measureText(text);
861
- temp.width = Math.ceil(metrics.width);
862
- temp.height = Math.ceil(parseInt(font) * 1.2);
863
-
864
- tempCtx.font = font;
865
- tempCtx.fillStyle = color;
866
- tempCtx.textBaseline = 'top';
867
- tempCtx.fillText(text, 0, 0);
868
-
869
- this.textCache.set(key, {
870
- canvas: temp,
871
- width: temp.width,
872
- height: temp.height,
873
- baseline: parseInt(font)
874
- });
875
- }
876
-
877
- const cached = this.textCache.get(key);
878
- this.ctx.drawImage(cached.canvas, x, y - cached.baseline);
879
- }
880
-
881
- /**
882
- * ✅ OPTIMISATION OPTION 5: Rendu batché pour plusieurs rectangles
883
- * Regroupe les rectangles par couleur pour réduire les appels draw
884
- * @param {Array} rects - Tableau d'objets {x, y, width, height, color}
885
- */
886
- batchRect(rects) {
887
- if (!rects || rects.length === 0) return;
888
-
889
- // Regrouper par couleur
890
- const batches = new Map();
891
-
892
- rects.forEach(rect => {
893
- if (!batches.has(rect.color)) {
894
- batches.set(rect.color, []);
895
- }
896
- batches.get(rect.color).push(rect);
897
- });
898
-
899
- // Dessiner par batch
900
- batches.forEach((batchRects, color) => {
901
- this.ctx.fillStyle = color;
902
-
903
- // Utiliser un seul path pour tous les rectangles de même couleur
904
- this.ctx.beginPath();
905
- batchRects.forEach(rect => {
906
- this.ctx.rect(rect.x, rect.y, rect.width, rect.height);
907
- });
908
- this.ctx.fill();
909
- });
910
- }
911
-
912
- /**
913
- * ✅ OPTIMISATION OPTION 5: Utiliser ImageData pour les mises à jour fréquentes
914
- * @param {number} x - Position X
915
- * @param {number} y - Position Y
916
- * @param {number} width - Largeur
917
- * @param {number} height - Hauteur
918
- * @param {Function} drawFn - Fonction pour manipuler les pixels
919
- */
920
- updateRegion(x, y, width, height, drawFn) {
921
- const imageData = this.ctx.getImageData(x, y, width, height);
922
- const data = imageData.data;
923
-
924
- // Manipuler directement les pixels
925
- drawFn(data, width, height);
926
-
927
- this.ctx.putImageData(imageData, x, y);
928
- }
929
-
930
- /**
931
- * ✅ OPTIMISATION OPTION 2: Flush du buffer pour le double buffering
932
- */
933
- flush() {
934
- if (this.optimizations.useDoubleBuffering && this._bufferCtx) {
935
- // Copier tout le buffer sur le canvas réel
936
- this.ctx.drawImage(this._doubleBuffer, 0, 0, this.width, this.height);
937
-
938
- // Réinitialiser le buffer pour le prochain frame
939
- this._bufferCtx.fillStyle = this.backgroundColor || '#ffffff';
940
- this._bufferCtx.fillRect(0, 0, this.width, this.height);
941
- }
942
- }
943
-
944
- /**
945
- * ✅ OPTIMISATION OPTION 5: Rendu optimisé avec viewport culling
946
- * @private
947
- */
948
- _renderOptimized() {
949
- const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
950
-
951
- if (!ctx) return;
952
-
953
- // Clear le canvas
954
- ctx.clearRect(0, 0, this.width, this.height);
955
-
956
- // Séparer les composants fixes et scrollables
957
- const scrollableComponents = [];
958
- const fixedComponents = [];
959
-
960
- for (let comp of this.components) {
961
- if (this.isFixedComponent(comp)) {
962
- fixedComponents.push(comp);
963
- } else {
964
- scrollableComponents.push(comp);
965
- }
966
- }
967
-
968
- // Rendu des composants scrollables avec viewport culling optimisé
969
- ctx.save();
970
- ctx.translate(0, this.scrollOffset);
971
-
972
- // ✅ OPTIMISATION: Utiliser le spatial partitioning si activé
973
- if (this.optimizations.useSpatialPartitioning && this._spatialGrid) {
974
- const visibleComps = this._spatialGrid.getVisible(-this.scrollOffset);
975
- for (let comp of visibleComps) {
976
- if (comp.visible) {
977
- comp.draw(ctx);
978
- }
979
- }
980
- } else {
981
- // Rendu standard avec culling simple
982
- for (let comp of scrollableComponents) {
983
- if (comp.visible) {
984
- const screenY = comp.y + this.scrollOffset;
985
- const isInViewport = screenY + comp.height >= -100 && screenY <= this.height + 100;
986
-
987
- if (isInViewport) {
988
- comp.draw(ctx);
989
- }
990
- }
991
- }
992
- }
993
-
994
- ctx.restore();
995
-
996
- // Rendu des composants fixes
997
- for (let comp of fixedComponents) {
998
- if (comp.visible) {
999
- comp.draw(ctx);
1000
- }
1001
- }
1002
-
1003
- // Flush si on utilise le double buffering
1004
- if (this.optimizations.useDoubleBuffering) {
1005
- this.flush();
1006
- }
1007
- }
1008
-
1009
- /**
1010
- * ✅ OPTIMISATION OPTION 2: Rendu partiel (seulement les composants sales)
1011
- * @private
1012
- */
1013
- _renderDirtyComponents() {
1014
- const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
1015
-
1016
- if (!ctx || this.dirtyComponents.size === 0) return;
1017
-
1018
- // ✅ CORRECTION : Ne pas nettoyer avec fillRect !
1019
- // À la place, utiliser le clipping pour redessiner proprement
1020
-
1021
- // Copier dans un tableau
1022
- const dirtyArray = Array.from(this.dirtyComponents);
1023
-
1024
- // Vider immédiatement
1025
- this.dirtyComponents.clear();
1026
-
1027
- // Séparer les composants
1028
- const fixedComps = [];
1029
- const scrollableComps = [];
1030
-
1031
- dirtyArray.forEach(comp => {
1032
- if (!comp.visible) return;
1033
-
1034
- if (this.isFixedComponent(comp)) {
1035
- fixedComps.push(comp);
1036
- } else {
1037
- scrollableComps.push(comp);
1038
- }
1039
- });
1040
-
1041
- // ✅ APPROCHE CORRECTE : Redessiner les composants sales
1042
- // sans effacer leur zone d'abord
1043
-
1044
- // 1. Dessiner les scrollables
1045
- if (scrollableComps.length > 0) {
1046
- ctx.save();
1047
- ctx.translate(0, this.scrollOffset);
1048
-
1049
- for (let comp of scrollableComps) {
1050
- // ✅ NE PAS faire de fillRect !
1051
- // Juste dessiner le composant
1052
- if (comp.draw) {
1053
- comp.draw(ctx);
1054
- }
1055
-
1056
- if (comp.markClean) {
1057
- comp.markClean();
1058
- }
1059
- }
1060
-
1061
- ctx.restore();
1062
- }
1063
-
1064
- // 2. Dessiner les fixes
1065
- for (let comp of fixedComps) {
1066
- if (comp.draw) {
1067
- comp.draw(ctx);
1068
- }
1069
-
1070
- if (comp.markClean) {
1071
- comp.markClean();
1072
- }
1073
- }
1074
-
1075
- // Flush si double buffering
1076
- if (this.optimizations.useDoubleBuffering) {
1077
- this.flush();
1078
- }
1079
- }
1080
-
1081
- /**
1082
- * Active/désactive une optimisation spécifique
1083
- * @param {string} optimization - Nom de l'optimisation
1084
- * @param {boolean} enabled - true pour activer, false pour désactiver
1085
- */
1086
- setOptimization(optimization, enabled) {
1087
- if (this.optimizations.hasOwnProperty(optimization)) {
1088
- this.optimizations[optimization] = enabled;
1089
-
1090
- switch (optimization) {
1091
- case 'useDoubleBuffering':
1092
- if (enabled && !this._bufferCtx) {
1093
- this._initDoubleBuffer();
1094
- }
1095
- break;
1096
- case 'useSpatialPartitioning':
1097
- if (enabled && !this._spatialGrid) {
1098
- this._initSpatialPartitioning();
1099
- }
1100
- break;
1101
- }
1102
-
1103
- // Marquer tous les composants comme sales pour forcer un redessin complet
1104
- this.components.forEach(comp => this.markComponentDirty(comp));
1105
- }
1106
- }
1107
-
1108
- /**
1109
- * Obtient l'état des optimisations
1110
- * @returns {Object} État des optimisations
1111
- */
1112
- getOptimizations() {
1113
- return {
1114
- ...this.optimizations
1115
- };
1116
- }
1117
-
1118
- /**
1119
- * Affiche un écran de chargement animé
1120
- * @private
1121
- */
1122
- showSplashScreen() {
1123
- const startTime = performance.now();
1124
- const opts = this.splashOptions;
1125
-
1126
- // ✅ Charger le logo si présent
1127
- let logoImage = null;
1128
- if (opts.logo) {
1129
- logoImage = new Image();
1130
- logoImage.src = opts.logo;
1131
- }
1132
-
1133
- const animate = () => {
1134
- const elapsed = performance.now() - startTime;
1135
- const progress = Math.min(elapsed / opts.duration, 1);
1136
-
1137
- // Clear
1138
- this.ctx.clearRect(0, 0, this.width, this.height);
1139
-
1140
- // ✅ Background (gradient ou couleur unie)
1141
- if (Array.isArray(opts.backgroundColor) && opts.backgroundColor.length >= 2) {
1142
- // Gradient
1143
- const gradient = this.ctx.createLinearGradient(0, 0, this.width, this.height);
1144
- gradient.addColorStop(0, opts.backgroundColor[0]);
1145
- gradient.addColorStop(1, opts.backgroundColor[1]);
1146
- this.ctx.fillStyle = gradient;
1147
- } else {
1148
- // Couleur unie
1149
- this.ctx.fillStyle = opts.backgroundColor;
1150
- }
1151
- this.ctx.fillRect(0, 0, this.width, this.height);
1152
-
1153
- const centerX = this.width / 2;
1154
- const centerY = this.height / 2;
1155
-
1156
- // ✅ Logo (si présent et chargé)
1157
- if (logoImage && logoImage.complete) {
1158
- const logoX = centerX - opts.logoWidth / 2;
1159
- const logoY = centerY - opts.logoHeight - 80;
1160
- this.ctx.drawImage(logoImage, logoX, logoY, opts.logoWidth, opts.logoHeight);
1161
- }
1162
-
1163
- // ✅ Spinner animé
1164
- const radius = 40;
1165
- const rotation = (elapsed / 1000) * Math.PI * 2;
1166
-
1167
- // Cercle de fond
1168
- this.ctx.beginPath();
1169
- this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1170
- this.ctx.strokeStyle = opts.spinnerBackground;
1171
- this.ctx.lineWidth = 4;
1172
- this.ctx.stroke();
1173
-
1174
- // Arc animé
1175
- this.ctx.beginPath();
1176
- this.ctx.arc(centerX, centerY, radius, rotation, rotation + Math.PI * 1.5);
1177
- this.ctx.strokeStyle = opts.spinnerColor;
1178
- this.ctx.lineWidth = 4;
1179
- this.ctx.lineCap = 'round';
1180
- this.ctx.stroke();
1181
-
1182
- // ✅ Texte personnalisé
1183
- this.ctx.fillStyle = opts.textColor;
1184
- this.ctx.font = `${opts.textSize}px ${opts.textFont}`;
1185
- this.ctx.textAlign = 'center';
1186
- this.ctx.fillText(opts.text, centerX, centerY + radius + 40);
1187
-
1188
- // ✅ Barre de progression (optionnelle)
1189
- if (opts.showProgressBar) {
1190
- const barWidth = 200;
1191
- const barHeight = 4;
1192
- const barX = centerX - barWidth / 2;
1193
- const barY = centerY + radius + 70;
1194
-
1195
- // Fond de la barre
1196
- this.ctx.fillStyle = opts.progressBarBackground;
1197
- this.ctx.fillRect(barX, barY, barWidth, barHeight);
1198
-
1199
- // Progression
1200
- this.ctx.fillStyle = opts.progressBarColor;
1201
- this.ctx.fillRect(barX, barY, barWidth * progress, barHeight);
1202
- }
1203
-
1204
- // Continuer ou fade out
1205
- if (progress < 1) {
1206
- requestAnimationFrame(animate);
1207
- } else {
1208
- this.fadeOutSplash();
1209
- }
1210
- };
1211
-
1212
- animate();
1213
- }
1214
-
1215
- /**
1216
- * Fade out du splash screen
1217
- * @private
1218
- */
1219
- fadeOutSplash() {
1220
- const opts = this.splashOptions;
1221
- const duration = opts.fadeOutDuration;
1222
- const startTime = performance.now();
1223
-
1224
- const fade = () => {
1225
- const elapsed = performance.now() - startTime;
1226
- const progress = elapsed / duration;
1227
- const alpha = 1 - Math.min(progress, 1);
1228
-
1229
- if (alpha > 0) {
1230
- this.ctx.clearRect(0, 0, this.width, this.height);
1231
- this.ctx.globalAlpha = alpha;
1232
-
1233
- // Redessiner le background
1234
- if (Array.isArray(opts.backgroundColor) && opts.backgroundColor.length >= 2) {
1235
- const gradient = this.ctx.createLinearGradient(0, 0, this.width, this.height);
1236
- gradient.addColorStop(0, opts.backgroundColor[0]);
1237
- gradient.addColorStop(1, opts.backgroundColor[1]);
1238
- this.ctx.fillStyle = gradient;
1239
- } else {
1240
- this.ctx.fillStyle = opts.backgroundColor;
1241
- }
1242
- this.ctx.fillRect(0, 0, this.width, this.height);
1243
-
1244
- // Spinner pendant le fade
1245
- const centerX = this.width / 2;
1246
- const centerY = this.height / 2;
1247
- const radius = 40;
1248
-
1249
- this.ctx.beginPath();
1250
- this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1251
- this.ctx.strokeStyle = opts.spinnerBackground;
1252
- this.ctx.lineWidth = 4;
1253
- this.ctx.stroke();
1254
-
1255
- this.ctx.globalAlpha = 1;
1256
- requestAnimationFrame(fade);
1257
- } else {
1258
- this._splashFinished = true;
1259
- // ✅ AJOUTER : Réinitialiser complètement le contexte
1260
- this.ctx.clearRect(0, 0, this.width, this.height);
1261
- this.ctx.globalAlpha = 1;
1262
- this.ctx.textAlign = 'start'; // ← IMPORTANT
1263
- this.ctx.textBaseline = 'alphabetic'; // ← IMPORTANT
1264
- this.ctx.font = '10px sans-serif'; // Valeur par défaut
1265
- this.ctx.fillStyle = '#000000';
1266
- this.ctx.strokeStyle = '#000000';
1267
- this.ctx.lineWidth = 1;
1268
- this.ctx.lineCap = 'butt';
1269
- this.ctx.lineJoin = 'miter';
1270
- }
1271
- };
1272
-
1273
- fade();
1274
- }
1275
-
1276
- // ✅ AJOUTER: Méthode pour mesurer le premier rendu
1277
- _markFirstRender() {
1278
- if (!this._firstRenderDone) {
1279
- this._firstRenderDone = true;
1280
- const firstRenderTime = performance.now() - this._startupStartTime - this.metrics.initTime;
1281
- this.metrics.firstRenderTime = firstRenderTime;
1282
- this.metrics.totalStartupTime = performance.now() - this._startupStartTime;
1283
-
1284
- if (this.showMetrics) {
1285
- console.log(`🎨 Premier rendu en ${firstRenderTime.toFixed(2)}ms`);
1286
- console.log(`🚀 Temps total de démarrage: ${this.metrics.totalStartupTime.toFixed(2)}ms`);
1287
- this.displayMetrics();
1288
- }
1289
- }
1290
- }
1291
-
1292
- /**
1293
- * Écoute les changements système (ex: utilisateur bascule dark mode)
1294
- */
1295
- setupSystemThemeListener() {
1296
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
1297
-
1298
- // Ancienne méthode (compatibilité large)
1299
- if (mediaQuery.addEventListener) {
1300
- mediaQuery.addEventListener('change', (e) => {
1301
- if (this.themeMode === 'system') {
1302
- this.applyThemeFromSystem();
1303
- }
1304
- });
1305
- } else {
1306
- // Anciens navigateurs (rare en 2026)
1307
- mediaQuery.addListener((e) => {
1308
- if (this.themeMode === 'system') {
1309
- this.applyThemeFromSystem();
1310
- }
1311
- });
1312
- }
1313
- }
1314
-
1315
- /**
1316
- * Change le mode thème
1317
- * @param {'light'|'dark'|'system'} mode - Mode à appliquer
1318
- * @param {boolean} [save=true] - Sauvegarder le choix utilisateur ?
1319
- */
1320
- setThemeMode(mode) {
1321
- this.themeManager.setMode(mode);
1322
- this.theme = this.themeManager.getTheme();
1323
- }
1324
-
1325
- /**
1326
- * Obtient une couleur du thème
1327
- */
1328
- getColor(colorName) {
1329
- return this.themeManager.getColor(colorName);
1330
- }
1331
-
1332
- /**
1333
- * Ajoute un listener de changement de thème
1334
- */
1335
- onThemeChange(callback) {
1336
- this.themeManager.addListener((theme) => {
1337
- this.theme = theme;
1338
- callback(theme);
1339
- });
1340
- }
1341
-
1342
- /**
1343
- * Bascule entre light et dark
1344
- */
1345
- toggleTheme() {
1346
- this.themeManager.toggle();
1347
- this.theme = this.themeManager.getTheme();
1348
- }
1349
-
1350
- /**
1351
- * Active ou désactive les DevTools
1352
- * @param {boolean} enabled - true pour activer, false pour désactiver
1353
- */
1354
- enableDevTools(enabled = true) {
1355
- if (enabled) {
1356
- // Créer le DevTools s'il n'existe pas
1357
- if (!this.devTools) {
1358
- this.devTools = new DevTools(this);
1359
- }
1360
-
1361
- // Attacher seulement si pas déjà fait
1362
- if (!this.devTools._isAttached) {
1363
- this.devTools.attachToFramework();
1364
- this.devTools._isAttached = true;
1365
- }
1366
-
1367
- // Afficher le bouton
1368
- if (this.devTools.toggleBtn) {
1369
- this.devTools.toggleBtn.style.display = 'block';
1370
- }
1371
- } else {
1372
- // Désactiver complètement
1373
- if (this.devTools) {
1374
- // Détacher du framework
1375
- if (this.devTools.detachFromFramework) {
1376
- this.devTools.detachFromFramework();
1377
- } else if (this.devTools.cleanup) {
1378
- this.devTools.cleanup();
1379
- }
1380
-
1381
- // Supprimer de la page DOM
1382
- if (this.devTools.container && this.devTools.container.parentNode) {
1383
- this.devTools.container.parentNode.removeChild(this.devTools.container);
1384
- }
1385
-
1386
- if (this.devTools.toggleBtn && this.devTools.toggleBtn.parentNode) {
1387
- this.devTools.toggleBtn.parentNode.removeChild(this.devTools.toggleBtn);
1388
- }
1389
-
1390
- this.devTools._isAttached = false;
1391
- }
1392
- }
1393
- }
1394
-
1395
- /**
1396
- * Bascule l'overlay d'inspection
1397
- */
1398
- toggleInspection() {
1399
- this.inspectionOverlay.toggle();
1400
- }
1401
-
1402
- /**
1403
- * Exécute une commande DevTools
1404
- */
1405
- devToolsCommand(command, ...args) {
1406
- switch (command) {
1407
- case 'inspect':
1408
- this.inspectionOverlay.enable();
1409
- break;
1410
- case 'performance':
1411
- this.devTools.switchTab('performance');
1412
- this.devTools.toggle();
1413
- break;
1414
- case 'components':
1415
- this.devTools.switchTab('components');
1416
- this.devTools.toggle();
1417
- break;
1418
- case 'highlight':
1419
- if (args[0]) {
1420
- this.devTools.highlightComponent(args[0]);
1421
- }
1422
- break;
1423
- case 'reflow':
1424
- this.components.forEach(comp => comp.markDirty());
1425
- break;
1426
- }
1427
- }
1428
-
1429
- wrapContext(ctx, theme) {
1430
- const originalFillStyle = Object.getOwnPropertyDescriptor(CanvasRenderingContext2D.prototype, 'fillStyle');
1431
- Object.defineProperty(ctx, 'fillStyle', {
1432
- set: (value) => {
1433
- // Si value est blanc/noir ou une couleur “neutre”, tu remplaces par theme
1434
- if (value === '#FFFFFF' || value === '#000000') {
1435
- originalFillStyle.set.call(ctx, value);
1436
- } else {
1437
- originalFillStyle.set.call(ctx, value);
1438
- }
1439
- },
1440
- get: () => originalFillStyle.get.call(ctx)
1441
- });
1442
- }
1443
-
1444
- createCanvasWorker() {
1445
- const workerCode = `
1446
- let components = [];
1447
-
1448
- self.onmessage = function(e) {
1449
- const { type, payload } = e.data;
1450
-
1451
- switch(type) {
1452
- case 'INIT':
1453
- components = payload.components;
1454
- self.postMessage({ type: 'READY' });
1455
- break;
1456
-
1457
- case 'UPDATE_LAYOUT':
1458
- const updated = components.map(comp => {
1459
- if (comp.dynamicHeight && comp.calculateHeight) {
1460
- comp.height = comp.calculateHeight();
1461
- }
1462
- return { id: comp.id, height: comp.height };
1463
- });
1464
- self.postMessage({ type: 'LAYOUT_DONE', payload: updated });
1465
- break;
1466
-
1467
- case 'SCROLL_INERTIA':
1468
- let { offset, velocity, friction, maxScroll } = payload;
1469
- offset += velocity;
1470
- offset = Math.max(Math.min(offset, 0), -maxScroll);
1471
- velocity *= friction;
1472
- self.postMessage({ type: 'SCROLL_UPDATED', payload: { offset, velocity } });
1473
- break;
1474
- }
1475
- };
1476
- `;
1477
-
1478
- const blob = new Blob([workerCode], {
1479
- type: 'application/javascript'
1480
- });
1481
- return new Worker(URL.createObjectURL(blob));
1482
- }
1483
-
1484
- createLogicWorker() {
1485
- const workerCode = `
1486
- let state = {};
1487
-
1488
- self.onmessage = async function(e) {
1489
- const { type, payload } = e.data;
1490
-
1491
- switch(type) {
1492
- case 'SET_STATE':
1493
- state = payload;
1494
- self.postMessage({ type: 'STATE_UPDATED', payload: state });
1495
- break;
1496
-
1497
- case 'EXECUTE':
1498
- try {
1499
- const fn = new Function('state', 'args', payload.fnString);
1500
- const result = await fn(state, payload.args);
1501
- self.postMessage({ type: 'EXECUTION_RESULT', payload: result });
1502
- } catch (err) {
1503
- self.postMessage({ type: 'EXECUTION_ERROR', payload: err.message });
1504
- }
1505
- break;
1506
- }
1507
- };
1508
- `;
1509
-
1510
- const blob = new Blob([workerCode], {
1511
- type: 'application/javascript'
1512
- });
1513
- return new Worker(URL.createObjectURL(blob));
1514
- }
1515
-
1516
- // Set Theme dynamique
1517
- setTheme(theme) {
1518
- this.theme = theme;
1519
-
1520
- if (!this.useWebGL) {
1521
- this.wrapContext(this.ctx, theme);
1522
- }
1523
-
1524
- // Protège la boucle
1525
- if (this.components && Array.isArray(this.components)) {
1526
- this.components.forEach(comp => comp.markDirty());
1527
- } else {
1528
- console.warn('[setTheme] components pas encore initialisé');
1529
- }
1530
- }
1531
-
1532
- // Switch Theme
1533
- toggleDarkMode() {
1534
- if (this.theme === lightTheme) {
1535
- this.setTheme(darkTheme);
1536
- } else {
1537
- this.setTheme(lightTheme);
1538
- }
1539
- }
1540
-
1541
- enableFpsDisplay(enable = true) {
1542
- this.showFps = enable;
1543
- }
1544
-
1545
- // AJOUTER CETTE MÉTHODE (optionnel - pour faciliter l'accès)
1546
- animate(component, options) {
1547
- return this.animator.animate(component, options);
1548
- }
1549
-
1550
- // ----- Worker UI -----
1551
- handleWorkerMessage(e) {
1552
- const {
1553
- type,
1554
- payload
1555
- } = e.data;
1556
- switch (type) {
1557
- case 'LAYOUT_DONE':
1558
- for (let update of payload) {
1559
- const comp = this.components.find(c => c.id === update.id);
1560
- if (comp) comp.height = update.height;
1561
- }
1562
- break;
1563
- case 'SCROLL_UPDATED':
1564
- this.scrollOffset = payload.offset;
1565
- this.scrollVelocity = payload.velocity;
1566
- break;
1567
- }
1568
- }
1569
-
1570
- updateLayoutAsync() {
1571
- this.worker.postMessage({
1572
- type: 'UPDATE_LAYOUT'
1573
- });
1574
- }
1575
-
1576
- updateScrollInertia() {
1577
- const maxScroll = this.getMaxScroll();
1578
- this.worker.postMessage({
1579
- type: 'SCROLL_INERTIA',
1580
- payload: {
1581
- offset: this.scrollOffset,
1582
- velocity: this.scrollVelocity,
1583
- friction: this.scrollFriction,
1584
- maxScroll
1585
- }
1586
- });
1587
- }
1588
-
1589
- // ------ Logic Worker --------
1590
- handleLogicWorkerMessage(e) {
1591
- const {
1592
- type,
1593
- payload
1594
- } = e.data;
1595
- switch (type) {
1596
- case 'STATE_UPDATED':
1597
- // Le worker a renvoyé le nouvel état global
1598
- this.logicWorkerState = payload;
1599
- break;
1600
-
1601
- case 'EXECUTION_RESULT':
1602
- // Résultat d'une tâche spécifique envoyée au worker
1603
- if (this.onWorkerResult) this.onWorkerResult(payload);
1604
- break;
1605
-
1606
- case 'EXECUTION_ERROR':
1607
- console.error('Logic Worker Error:', payload);
1608
- break;
1609
- }
1610
- }
1611
-
1612
- runLogicTask(taskName, taskData) {
1613
- this.logicWorker.postMessage({
1614
- type: 'EXECUTE_TASK',
1615
- payload: {
1616
- taskName,
1617
- taskData
1618
- }
1619
- });
1620
- }
1621
-
1622
- updateLogicWorkerState(newState) {
1623
- this.logicWorkerState = {
1624
- ...this.logicWorkerState,
1625
- ...newState
1626
- };
1627
- this.logicWorker.postMessage({
1628
- type: 'SET_STATE',
1629
- payload: this.logicWorkerState
1630
- });
1631
- }
1632
-
1633
- detectPlatform() {
1634
- const ua = navigator.userAgent.toLowerCase();
1635
- if (/android/.test(ua)) return 'material';
1636
- if (/iphone|ipad|ipod/.test(ua)) return 'cupertino';
1637
- return 'material';
1638
- }
1639
-
1640
- setupCanvas() {
1641
- this.canvas.width = this.width * this.dpr;
1642
- this.canvas.height = this.height * this.dpr;
1643
- this.canvas.style.width = this.width + 'px';
1644
- this.canvas.style.height = this.height + 'px';
1645
-
1646
- // ✅ AJOUTER: Appliquer le background au style CSS
1647
- this.canvas.style.backgroundColor = this.backgroundColor;
1648
- // Échelle uniquement pour Canvas 2D
1649
- this.ctx.scale(this.dpr, this.dpr);
1650
- }
1651
-
1652
- setupEventListeners() {
1653
- this.canvas.addEventListener('touchstart', this.handleTouchStart.bind(this));
1654
- this.canvas.addEventListener('touchmove', this.handleTouchMove.bind(this));
1655
- this.canvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
1656
- this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
1657
- this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
1658
- this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
1659
- window.addEventListener('resize', this.handleResize.bind(this));
1660
- }
1661
-
1662
- /**
1663
- * Configure l'écoute de l'historique du navigateur
1664
- * @private
1665
- */
1666
- setupHistoryListener() {
1667
- window.addEventListener('popstate', (e) => {
1668
- if (e.state && e.state.route) {
1669
- this.navigateTo(e.state.route, {
1670
- replace: true,
1671
- animate: true,
1672
- direction: 'back'
1673
- });
1674
- }
1675
- });
1676
- }
1677
-
1678
- // ===== MÉTHODES DE ROUTING =====
1679
-
1680
- /**
1681
- * Définit une route avec pattern de paramètres
1682
- * @param {string} pattern - Pattern de la route (ex: '/user/:id', '/posts/:category/:id')
1683
- * @param {Function} component - Fonction qui crée les composants
1684
- * @param {Object} options - Options de la route
1685
- * @returns {CanvasFramework}
1686
- */
1687
- route(pattern, component, options = {}) {
1688
- const route = {
1689
- pattern,
1690
- component,
1691
- regex: this.patternToRegex(pattern),
1692
- paramNames: this.extractParamNames(pattern),
1693
- beforeEnter: options.beforeEnter,
1694
- afterEnter: options.afterEnter,
1695
- beforeLeave: options.beforeLeave,
1696
- afterLeave: options.afterLeave, // ✅ NOUVEAU
1697
- onEnter: options.onEnter, // ✅ NOUVEAU (alias de afterEnter)
1698
- onLeave: options.onLeave, // ✅ NOUVEAU (alias de beforeLeave)
1699
- transition: options.transition || 'slide'
1700
- };
1701
-
1702
- this.routes.set(pattern, route);
1703
- return this;
1704
- }
1705
-
1706
- /**
1707
- * Convertit un pattern de route en regex
1708
- * @private
1709
- */
1710
- patternToRegex(pattern) {
1711
- const regexPattern = pattern
1712
- .replace(/\//g, '\\/')
1713
- .replace(/:([^\/]+)/g, '([^\\/]+)');
1714
- return new RegExp(`^${regexPattern}$`);
1715
- }
1716
-
1717
- /**
1718
- * Extrait les noms des paramètres d'un pattern
1719
- * @private
1720
- */
1721
- extractParamNames(pattern) {
1722
- const matches = pattern.match(/:([^\/]+)/g);
1723
- return matches ? matches.map(m => m.slice(1)) : [];
1724
- }
1725
-
1726
- /**
1727
- * Trouve la route correspondant à un path
1728
- * @private
1729
- */
1730
- matchRoute(path) {
1731
- // Séparer le path et la query string
1732
- const [pathname, queryString] = path.split('?');
1733
-
1734
- for (let [pattern, route] of this.routes) {
1735
- const match = pathname.match(route.regex);
1736
- if (match) {
1737
- const params = {};
1738
- route.paramNames.forEach((name, index) => {
1739
- params[name] = match[index + 1];
1740
- });
1741
-
1742
- const query = this.parseQueryString(queryString);
1743
-
1744
- return {
1745
- route,
1746
- params,
1747
- query,
1748
- pathname
1749
- };
1750
- }
1751
- }
1752
- return null;
1753
- }
1754
-
1755
- /**
1756
- * Parse une query string
1757
- * @private
1758
- */
1759
- parseQueryString(queryString) {
1760
- if (!queryString) return {};
1761
-
1762
- const params = {};
1763
- queryString.split('&').forEach(param => {
1764
- const [key, value] = param.split('=');
1765
- params[decodeURIComponent(key)] = decodeURIComponent(value || '');
1766
- });
1767
- return params;
1768
- }
1769
-
1770
- /**
1771
- * Navigue vers une route
1772
- * @param {string} path - Chemin de destination (ex: '/user/123', '/posts/tech/456?sort=date')
1773
- * @param {Object} options - Options de navigation
1774
- */
1775
- navigate(path, options = {}) {
1776
- this.navigateTo(path, options);
1777
- }
1778
-
1779
- /**
1780
- * Méthode interne de navigation
1781
- * @private
1782
- */
1783
- async navigateTo(path, options = {}) {
1784
- const {
1785
- replace = false,
1786
- animate = true,
1787
- direction = 'forward',
1788
- transition = null,
1789
- state = {}
1790
- } = options;
1791
-
1792
- const match = this.matchRoute(path);
1793
- if (!match) {
1794
- console.warn(`Route not found: ${path}`);
1795
- return;
1796
- }
1797
-
1798
- const { route, params, query, pathname } = match;
1799
-
1800
- // ===== LIFECYCLE: AVANT DE QUITTER L'ANCIENNE ROUTE =====
1801
- const currentRouteData = this.routes.get(this.currentRoute);
1802
- if (currentRouteData?.beforeLeave) {
1803
- const canLeave = await currentRouteData.beforeLeave(this.currentParams, this.currentQuery, this);
1804
- if (canLeave === false) {
1805
- console.log('Navigation cancelled by beforeLeave hook');
1806
- return;
1807
- }
1808
- }
1809
-
1810
- if (currentRouteData?.onLeave) {
1811
- await currentRouteData.onLeave(this.currentParams, this.currentQuery, this);
1812
- }
1813
-
1814
- // ===== LIFECYCLE: AVANT D'ENTRER DANS LA NOUVELLE ROUTE =====
1815
- if (route.beforeEnter) {
1816
- const canEnter = await route.beforeEnter(params, query, this);
1817
- if (canEnter === false) {
1818
- console.log('Navigation cancelled by beforeEnter hook');
1819
- return;
1820
- }
1821
- }
1822
-
1823
- if (route.onEnter) {
1824
- await route.onEnter(params, query, this);
1825
- }
1826
-
1827
- // ===== SAUVEGARDER L'ÉTAT ACTUEL =====
1828
- const oldComponents = [...this.components];
1829
- const oldRoute = this.currentRoute;
1830
- const oldParams = { ...this.currentParams };
1831
- const oldQuery = { ...this.currentQuery };
1832
-
1833
- // ✅ CRUCIAL : ARRÊTER LES ANCIENNES CAMÉRAS AVANT DE CRÉER LES NOUVELLES
1834
- oldComponents.forEach(comp => {
1835
- if (comp.constructor.name === 'FloatedCamera' ||
1836
- comp.constructor.name === 'Camera' ||
1837
- comp.constructor.name === 'Video' ||
1838
- comp.constructor.name === 'QRCodeReader') {
1839
- if (comp.stopCamera && typeof comp.stopCamera === 'function') {
1840
- comp.stopCamera();
1841
- }
1842
- // Destroy le composant pour être sûr
1843
- if (comp.destroy && typeof comp.destroy === 'function') {
1844
- comp.destroy();
1845
- }
1846
- }
1847
- });
1848
-
1849
- // ✅ Nettoyer toutes les vidéos orphelines AVANT de créer les nouveaux composants
1850
- const allVideos = document.querySelectorAll('video');
1851
- allVideos.forEach(v => v.remove());
1852
-
1853
- // ===== METTRE À JOUR L'ÉTAT =====
1854
- this.currentRoute = pathname;
1855
- this.currentParams = params;
1856
- this.currentQuery = query;
1857
-
1858
- // ===== GÉRER L'HISTORIQUE =====
1859
- if (!replace) {
1860
- this.historyIndex++;
1861
- this.history = this.history.slice(0, this.historyIndex);
1862
- this.history.push({ path, params, query, state });
1863
- window.history.pushState({ route: path, params, query, state }, '', path);
1864
- } else {
1865
- this.history[this.historyIndex] = { path, params, query, state };
1866
- window.history.replaceState({ route: path, params, query, state }, '', path);
1867
- }
1868
-
1869
- // ===== CRÉER LES NOUVEAUX COMPOSANTS =====
1870
- this.components = [];
1871
- if (typeof route.component === 'function') {
1872
- route.component(this, params, query);
1873
- }
1874
-
1875
- // ===== LANCER L'ANIMATION DE TRANSITION =====
1876
- if (animate && !this.transitionState.isTransitioning) {
1877
- const transitionType = transition || route.transition || 'slide';
1878
- this.startTransition(oldComponents, this.components, transitionType, direction);
1879
- }
1880
-
1881
- // ===== LIFECYCLE: APRÈS ÊTRE ENTRÉ DANS LA NOUVELLE ROUTE =====
1882
- if (route.afterEnter) {
1883
- route.afterEnter(params, query, this);
1884
- }
1885
-
1886
- if (currentRouteData?.afterLeave) {
1887
- if (animate && this.transitionState.isTransitioning) {
1888
- setTimeout(() => {
1889
- currentRouteData.afterLeave(oldParams, oldQuery, this);
1890
- }, this.transitionState.duration || 300);
1891
- } else {
1892
- currentRouteData.afterLeave(oldParams, oldQuery, this);
1893
- }
1894
- }
1895
-
1896
- this._maxScrollDirty = true;
1897
- }
1898
-
1899
- /**
1900
- * Démarre une animation de transition
1901
- * @private
1902
- */
1903
- startTransition(oldComponents, newComponents, type, direction) {
1904
- this.transitionState = {
1905
- isTransitioning: true,
1906
- progress: 0,
1907
- duration: 300,
1908
- type,
1909
- direction,
1910
- oldComponents: [...oldComponents],
1911
- newComponents: [...newComponents],
1912
- startTime: Date.now()
1913
- };
1914
- }
1915
-
1916
- /**
1917
- * Met à jour l'animation de transition
1918
- * @private
1919
- */
1920
- updateTransition() {
1921
- if (!this.transitionState.isTransitioning) return;
1922
-
1923
- const elapsed = Date.now() - this.transitionState.startTime;
1924
- this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
1925
-
1926
- // Fonction d'easing (ease-in-out)
1927
- const eased = this.easeInOutCubic(this.transitionState.progress);
1928
-
1929
- // Appliquer la transformation selon le type
1930
- this.ctx.save();
1931
- this.applyTransitionTransform(eased);
1932
- this.ctx.restore();
1933
-
1934
- // Terminer la transition
1935
- if (this.transitionState.progress >= 1) {
1936
- this.transitionState.isTransitioning = false;
1937
- this.transitionState.oldComponents = [];
1938
- }
1939
- }
1940
-
1941
- /**
1942
- * Applique la transformation de transition
1943
- * @private
1944
- */
1945
- applyTransitionTransform(progress) {
1946
- const {
1947
- type,
1948
- direction,
1949
- oldComponents,
1950
- newComponents
1951
- } = this.transitionState;
1952
- const directionMultiplier = direction === 'forward' ? 1 : -1;
1953
-
1954
- switch (type) {
1955
- case 'slide':
1956
- // Dessiner l'ancienne vue qui sort
1957
- this.ctx.save();
1958
- this.ctx.translate(-this.width * progress * directionMultiplier, 0);
1959
- this.ctx.globalAlpha = 1 - progress * 0.3;
1960
- for (let comp of oldComponents) {
1961
- if (comp.visible) comp.draw(this.ctx);
1962
- }
1963
- this.ctx.restore();
1964
-
1965
- // Dessiner la nouvelle vue qui entre
1966
- this.ctx.save();
1967
- this.ctx.translate(this.width * (1 - progress) * directionMultiplier, 0);
1968
- for (let comp of newComponents) {
1969
- if (comp.visible) comp.draw(this.ctx);
1970
- }
1971
- this.ctx.restore();
1972
- break;
1973
-
1974
- case 'fade':
1975
- // Dessiner l'ancienne vue qui fade out
1976
- this.ctx.save();
1977
- this.ctx.globalAlpha = 1 - progress;
1978
- for (let comp of oldComponents) {
1979
- if (comp.visible) comp.draw(this.ctx);
1980
- }
1981
- this.ctx.restore();
1982
-
1983
- // Dessiner la nouvelle vue qui fade in
1984
- this.ctx.save();
1985
- this.ctx.globalAlpha = progress;
1986
- for (let comp of newComponents) {
1987
- if (comp.visible) comp.draw(this.ctx);
1988
- }
1989
- this.ctx.restore();
1990
- break;
1991
-
1992
- case 'none':
1993
- // Pas d'animation, juste afficher la nouvelle vue
1994
- for (let comp of newComponents) {
1995
- if (comp.visible) comp.draw(this.ctx);
1996
- }
1997
- break;
1998
- }
1999
- }
2000
-
2001
- /**
2002
- * Fonction d'easing
2003
- * @private
2004
- */
2005
- easeInOutCubic(t) {
2006
- return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
2007
- }
2008
-
2009
- /**
2010
- * Retour en arrière dans l'historique
2011
- */
2012
- goBack() {
2013
- if (this.historyIndex > 0) {
2014
- this.historyIndex--;
2015
- const historyEntry = this.history[this.historyIndex];
2016
- this.navigateTo(historyEntry.path, {
2017
- replace: true,
2018
- animate: true,
2019
- direction: 'back'
2020
- });
2021
- window.history.back();
2022
- }
2023
- }
2024
-
2025
- /**
2026
- * Avancer dans l'historique
2027
- */
2028
- goForward() {
2029
- if (this.historyIndex < this.history.length - 1) {
2030
- this.historyIndex++;
2031
- const historyEntry = this.history[this.historyIndex];
2032
- this.navigateTo(historyEntry.path, {
2033
- replace: true,
2034
- animate: true,
2035
- direction: 'forward'
2036
- });
2037
- window.history.forward();
2038
- }
2039
- }
2040
-
2041
- /**
2042
- * Obtient les paramètres de la route actuelle
2043
- * @returns {Object}
2044
- */
2045
- getParams() {
2046
- return {
2047
- ...this.currentParams
2048
- };
2049
- }
2050
-
2051
- /**
2052
- * Obtient la query string de la route actuelle
2053
- * @returns {Object}
2054
- */
2055
- getQuery() {
2056
- return {
2057
- ...this.currentQuery
2058
- };
2059
- }
2060
-
2061
- /**
2062
- * Obtient un paramètre spécifique
2063
- * @param {string} name
2064
- * @returns {string|undefined}
2065
- */
2066
- getParam(name) {
2067
- return this.currentParams[name];
2068
- }
2069
-
2070
- /**
2071
- * Obtient un paramètre de query spécifique
2072
- * @param {string} name
2073
- * @returns {string|undefined}
2074
- */
2075
- getQueryParam(name) {
2076
- return this.currentQuery[name];
2077
- }
2078
-
2079
- // ===== FIN DES MÉTHODES DE ROUTING =====
2080
-
2081
- handleTouchStart(e) {
2082
- e.preventDefault();
2083
- this.isDragging = false;
2084
- const touch = e.touches[0];
2085
- const pos = this.getTouchPos(touch);
2086
- this.lastTouchY = pos.y;
2087
-
2088
- // Informer le Worker
2089
- this.scrollWorker.postMessage({
2090
- type: 'SET_DRAGGING',
2091
- payload: {
2092
- isDragging: false,
2093
- lastTouchY: this.lastTouchY
2094
- }
2095
- });
2096
-
2097
- this.checkComponentsAtPosition(pos.x, pos.y, 'start');
2098
- }
2099
-
2100
- handleTouchMove(e) {
2101
- e.preventDefault();
2102
- const touch = e.touches[0];
2103
- const pos = this.getTouchPos(touch);
2104
-
2105
- if (!this.isDragging) {
2106
- const deltaY = Math.abs(pos.y - this.lastTouchY);
2107
- if (deltaY > 5) {
2108
- this.isDragging = true;
2109
- this.scrollWorker.postMessage({
2110
- type: 'SET_DRAGGING',
2111
- payload: {
2112
- isDragging: true,
2113
- lastTouchY: this.lastTouchY
2114
- }
2115
- });
2116
- }
2117
- }
2118
-
2119
- if (this.isDragging) {
2120
- const deltaY = pos.y - this.lastTouchY;
2121
-
2122
- // Déléguer le calcul au Worker
2123
- this.scrollWorker.postMessage({
2124
- type: 'HANDLE_TOUCH_MOVE',
2125
- payload: {
2126
- deltaY
2127
- }
2128
- });
2129
-
2130
- this.lastTouchY = pos.y;
2131
- } else {
2132
- this.checkComponentsAtPosition(pos.x, pos.y, 'move');
2133
- }
2134
- }
2135
-
2136
- handleTouchEnd(e) {
2137
- e.preventDefault();
2138
- const touch = e.changedTouches[0];
2139
- const pos = this.getTouchPos(touch);
2140
-
2141
- if (!this.isDragging) {
2142
- this.checkComponentsAtPosition(pos.x, pos.y, 'end');
2143
- } else {
2144
- this.isDragging = false;
2145
- this.scrollWorker.postMessage({
2146
- type: 'SET_DRAGGING',
2147
- payload: {
2148
- isDragging: false,
2149
- lastVelocity: this.scrollVelocity
2150
- }
2151
- });
2152
- }
2153
- }
2154
-
2155
- handleMouseDown(e) {
2156
- this.isDragging = false;
2157
- this.lastTouchY = e.clientY;
2158
-
2159
- this.scrollWorker.postMessage({
2160
- type: 'SET_DRAGGING',
2161
- payload: {
2162
- isDragging: false,
2163
- lastTouchY: this.lastTouchY
2164
- }
2165
- });
2166
-
2167
- this.checkComponentsAtPosition(e.clientX, e.clientY, 'start');
2168
- }
2169
-
2170
- handleMouseMove(e) {
2171
- if (!this.isDragging) {
2172
- const deltaY = Math.abs(e.clientY - this.lastTouchY);
2173
- if (deltaY > 5) {
2174
- this.isDragging = true;
2175
- this.scrollWorker.postMessage({
2176
- type: 'SET_DRAGGING',
2177
- payload: {
2178
- isDragging: true,
2179
- lastTouchY: this.lastTouchY
2180
- }
2181
- });
2182
- }
2183
- }
2184
-
2185
- if (this.isDragging) {
2186
- const deltaY = e.clientY - this.lastTouchY;
2187
-
2188
- this.scrollWorker.postMessage({
2189
- type: 'HANDLE_TOUCH_MOVE',
2190
- payload: {
2191
- deltaY
2192
- }
2193
- });
2194
-
2195
- this.lastTouchY = e.clientY;
2196
- } else {
2197
- this.checkComponentsAtPosition(e.clientX, e.clientY, 'move');
2198
- }
2199
- }
2200
-
2201
- handleMouseUp(e) {
2202
- if (!this.isDragging) {
2203
- this.checkComponentsAtPosition(e.clientX, e.clientY, 'end');
2204
- } else {
2205
- this.isDragging = false;
2206
- this.scrollWorker.postMessage({
2207
- type: 'SET_DRAGGING',
2208
- payload: {
2209
- isDragging: false,
2210
- lastVelocity: this.scrollVelocity
2211
- }
2212
- });
2213
- }
2214
- }
2215
-
2216
-
2217
- getTouchPos(touch) {
2218
- const rect = this.canvas.getBoundingClientRect();
2219
- return {
2220
- x: touch.clientX - rect.left,
2221
- y: touch.clientY - rect.top
2222
- };
2223
- }
2224
-
2225
- checkComponentsAtPosition(x, y, eventType) {
2226
- const isFixedComponent = (comp) =>
2227
- FIXED_COMPONENT_TYPES.has(comp.constructor);
2228
-
2229
- for (let i = this.components.length - 1; i >= 0; i--) {
2230
- const comp = this.components[i];
2231
-
2232
- if (comp.visible) {
2233
- const adjustedY = isFixedComponent(comp) ? y : y - this.scrollOffset;
2234
-
2235
- if (comp instanceof Card && comp.clickableChildren && comp.children && comp.children.length > 0) {
2236
- if (comp.isPointInside(x, adjustedY)) {
2237
- const cardAdjustedY = adjustedY - comp.y - comp.padding;
2238
- const cardAdjustedX = x - comp.x - comp.padding;
2239
-
2240
- for (let j = comp.children.length - 1; j >= 0; j--) {
2241
- const child = comp.children[j];
2242
-
2243
- if (child.visible &&
2244
- cardAdjustedY >= child.y &&
2245
- cardAdjustedY <= child.y + child.height &&
2246
- cardAdjustedX >= child.x &&
2247
- cardAdjustedX <= child.x + child.width) {
2248
-
2249
- const relativeX = cardAdjustedX - child.x;
2250
- const relativeY = cardAdjustedY - child.y;
2251
-
2252
- switch (eventType) {
2253
- case 'start':
2254
- child.pressed = true;
2255
- if (child.onPress) child.onPress?.(relativeX, relativeY);
2256
- break;
2257
-
2258
- case 'move':
2259
- if (!child.hovered) {
2260
- child.hovered = true;
2261
- if (child.onHover) child.onHover();
2262
- }
2263
- if (child.onMove) child.onMove?.(relativeX, relativeY);
2264
- break;
2265
-
2266
- case 'end':
2267
- if (child.pressed) {
2268
- child.pressed = false;
2269
-
2270
- if (child instanceof Input || child instanceof PasswordInput || child instanceof InputTags || child instanceof InputDatalist) {
2271
- for (let other of this.components) {
2272
- if (
2273
- (other instanceof Input ||
2274
- other instanceof PasswordInput ||
2275
- other instanceof InputTags ||
2276
- other instanceof InputDatalist) &&
2277
- other !== child &&
2278
- other.focused
2279
- ) {
2280
- other.focused = false;
2281
- other.cursorVisible = false;
2282
- other.onBlur?.();
2283
- }
2284
- }
2285
-
2286
- child.focused = true;
2287
- child.cursorVisible = true;
2288
- if (child.onFocus) child.onFocus();
2289
- } else if (child.onClick) {
2290
- child.onClick();
2291
- } else if (child.onPress) {
2292
- child.onPress?.(relativeX, relativeY);
2293
- }
2294
- }
2295
- break;
2296
- }
2297
-
2298
- return;
2299
- }
2300
- }
2301
- }
2302
- }
2303
-
2304
- if (comp.isPointInside(x, adjustedY)) {
2305
- switch (eventType) {
2306
- case 'start':
2307
- comp.pressed = true;
2308
- if (comp.onPress) comp.onPress(x, adjustedY);
2309
- break;
2310
-
2311
- case 'move':
2312
- if (!comp.hovered) {
2313
- comp.hovered = true;
2314
- if (comp.onHover) comp.onHover();
2315
- }
2316
- if (comp.onMove) comp.onMove(x, adjustedY);
2317
- break;
2318
-
2319
- case 'end':
2320
- if (comp.pressed) {
2321
- comp.pressed = false;
2322
-
2323
- if (comp instanceof Input || comp instanceof PasswordInput || comp instanceof InputTags || comp instanceof InputDatalist) {
2324
- for (let other of this.components) {
2325
- if (
2326
- (other instanceof Input ||
2327
- other instanceof PasswordInput ||
2328
- other instanceof InputTags ||
2329
- other instanceof InputDatalist) &&
2330
- other !== comp &&
2331
- other.focused
2332
- ) {
2333
- other.focused = false;
2334
- other.cursorVisible = false;
2335
- other.onBlur?.();
2336
- }
2337
- }
2338
-
2339
- comp.focused = true;
2340
- comp.cursorVisible = true;
2341
- if (comp.onFocus) comp.onFocus();
2342
- } else if (comp.onClick) {
2343
- comp.onClick();
2344
- } else if (comp.onPress) {
2345
- comp.onPress(x, adjustedY);
2346
- }
2347
- }
2348
- break;
2349
- }
2350
-
2351
- return;
2352
- } else {
2353
- comp.hovered = false;
2354
- }
2355
- }
2356
- }
2357
- }
2358
-
2359
- getMaxScroll() {
2360
- // Utiliser le cache du Worker
2361
- if (!this._maxScrollDirty && this._cachedMaxScroll !== undefined) {
2362
- return this._cachedMaxScroll;
2363
- }
2364
-
2365
- // Fallback si le Worker n'est pas encore initialisé
2366
- let maxY = 0;
2367
- for (const comp of this.components) {
2368
- if (this.isFixedComponent(comp) || !comp.visible) continue;
2369
- const bottom = comp.y + comp.height;
2370
- if (bottom > maxY) maxY = bottom;
2371
- }
2372
-
2373
- return Math.max(0, maxY - this.height + 50);
2374
- }
2375
-
2376
- /*getMaxScroll() {
2377
- let maxY = 0;
2378
- for (let comp of this.components) {
2379
- if (!this.isFixedComponent(comp)) {
2380
- maxY = Math.max(maxY, comp.y + comp.height);
2381
- }
2382
- }
2383
- return Math.max(0, maxY - this.height + 50);
2384
- }*/
2385
-
2386
- handleResize() {
2387
- if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
2388
-
2389
- this.resizeTimeout = setTimeout(() => {
2390
- this.width = window.innerWidth;
2391
- this.height = window.innerHeight;
2392
- this.setupCanvas();
2393
-
2394
- // Mettre à jour les dimensions dans le Worker
2395
- this.scrollWorker.postMessage({
2396
- type: 'UPDATE_DIMENSIONS',
2397
- payload: {
2398
- height: this.height
2399
- }
2400
- });
2401
-
2402
- for (const comp of this.components) {
2403
- if (comp._resize) {
2404
- comp._resize(this.width, this.height);
2405
- }
2406
- }
2407
- this.updateScrollWorkerComponents();
2408
- }, 150);
2409
- }
2410
-
2411
- /*handleResize() {
2412
- if (this.resizeTimeout) clearTimeout(this.resizeTimeout); // ✅ AJOUTER
2413
-
2414
- this.resizeTimeout = setTimeout(() => { // ✅ AJOUTER
2415
- if (!this.useWebGL) {
2416
- this.width = window.innerWidth;
2417
- this.height = window.innerHeight;
2418
- this.setupCanvas();
2419
-
2420
- for (const comp of this.components) {
2421
- if (comp._resize) {
2422
- comp._resize(this.width, this.height);
2423
- }
2424
- }
2425
- this._maxScrollDirty = true; // ✅ AJOUTER
2426
- }
2427
- }, 150); // ✅ AJOUTER (throttle 150ms)
2428
- }*/
2429
-
2430
- add(component) {
2431
- this.components.push(component);
2432
- component._mount();
2433
- this.updateScrollWorkerComponents();
2434
- return component;
2435
- }
2436
-
2437
- remove(component) {
2438
- const index = this.components.indexOf(component);
2439
- if (index > -1) {
2440
- component._unmount();
2441
- this.components.splice(index, 1);
2442
- this.updateScrollWorkerComponents();
2443
- }
2444
- }
2445
-
2446
- // 4. Modifiez markComponentDirty() pour éviter de marquer pendant le scroll
2447
- markComponentDirty(component) {
2448
- // Vérifications basiques
2449
- if (!component || !component.visible) return;
2450
-
2451
- // ✅ TOUJOURS ajouter au set des composants sales
2452
- // Les conditions de rendu seront vérifiées dans _renderDirtyComponents
2453
- if (this.optimizationEnabled) {
2454
- this.dirtyComponents.add(component);
2455
- }
2456
-
2457
- // ✅ Optionnel : Marquer aussi le composant lui-même
2458
- if (component._dirty !== undefined) {
2459
- component._dirty = true;
2460
- }
2461
- }
2462
-
2463
- enableOptimization() {
2464
- this.optimizationEnabled = true;
2465
- }
2466
-
2467
- /**
2468
- * Dessine un petit triangle rouge pour indiquer overflow (style Flutter)
2469
- */
2470
- drawOverflowIndicators() {
2471
- const ctx = this.ctx;
2472
-
2473
- // Pour chaque composant
2474
- for (let comp of this.components) {
2475
- if (!comp.visible) continue;
2476
-
2477
- // Position réelle à l'écran
2478
- const isFixed = this.isFixedComponent(comp);
2479
- const screenY = isFixed ? comp.y : comp.y + this.scrollOffset;
2480
- const screenX = comp.x;
2481
-
2482
- // Vérifier si le composant TEXT a une largeur/hauteur incorrecte
2483
- let actualWidth = comp.width;
2484
- let actualHeight = comp.height;
2485
-
2486
- // Si c'est un Text, vérifier la taille réelle du texte
2487
- if (comp instanceof Text && comp.text && ctx.measureText) {
2488
- try {
2489
- // Sauvegarder le style actuel
2490
- ctx.save();
2491
-
2492
- // Appliquer le style du texte
2493
- if (comp.fontSize) {
2494
- ctx.font = `${comp.fontSize}px ${comp.fontFamily || 'Arial'}`;
2495
- }
2496
-
2497
- // Mesurer la taille réelle
2498
- const metrics = ctx.measureText(comp.text);
2499
- actualWidth = metrics.width + (comp.padding || 0) * 2;
2500
- actualHeight = (comp.fontSize || 16) + (comp.padding || 0) * 2;
2501
-
2502
- ctx.restore();
2503
- } catch (e) {
2504
- // En cas d'erreur, garder les dimensions par défaut
2505
- }
2506
- }
2507
-
2508
- // Calculer les limites RÉELLES du composant
2509
- const compLeft = screenX;
2510
- const compRight = screenX + actualWidth;
2511
- const compTop = screenY;
2512
- const compBottom = screenY + actualHeight;
2513
-
2514
- // Vérifier les débordements avec les dimensions RÉELLES
2515
- const overflow = {
2516
- left: compLeft < 0,
2517
- right: compRight > this.width,
2518
- top: compTop < 0,
2519
- bottom: compBottom > this.height
2520
- };
2521
-
2522
- // Si aucun débordement, passer au suivant
2523
- if (!overflow.left && !overflow.right && !overflow.top && !overflow.bottom) {
2524
- continue;
2525
- }
2526
-
2527
- // DEBUG: Afficher les infos du composant
2528
- if (this.debbug) {
2529
- console.table({
2530
- type: comp.constructor?.name,
2531
- x: comp.x,
2532
- y: comp.y,
2533
- declaredSize: `${comp.width}x${comp.height}`,
2534
- actualSize: `${actualWidth}x${actualHeight}`,
2535
- screenPos: `(${screenX}, ${screenY})`,
2536
- overflow
2537
- });
2538
- }
2539
-
2540
- // Dessiner les indicateurs
2541
- ctx.save();
2542
-
2543
- // 1. Bordures rouges sur les parties qui débordent
2544
- ctx.strokeStyle = 'red';
2545
- ctx.lineWidth = 2;
2546
- ctx.fillStyle = 'rgba(255, 0, 0, 0.2)';
2547
-
2548
- // Gauche
2549
- if (overflow.left) {
2550
- const overflowWidth = Math.min(actualWidth, -compLeft);
2551
- ctx.fillRect(compLeft, compTop, overflowWidth, actualHeight);
2552
- ctx.strokeRect(compLeft, compTop, overflowWidth, actualHeight);
2553
- }
2554
-
2555
- // Droite
2556
- if (overflow.right) {
2557
- const overflowStart = Math.max(0, this.width - compLeft);
2558
- const overflowWidth = Math.min(actualWidth, compRight - this.width);
2559
- ctx.fillRect(this.width - overflowWidth, compTop, overflowWidth, actualHeight);
2560
- ctx.strokeRect(this.width - overflowWidth, compTop, overflowWidth, actualHeight);
2561
- }
2562
-
2563
- // Haut
2564
- if (overflow.top) {
2565
- const overflowHeight = Math.min(actualHeight, -compTop);
2566
- ctx.fillRect(compLeft, compTop, actualWidth, overflowHeight);
2567
- ctx.strokeRect(compLeft, compTop, actualWidth, overflowHeight);
2568
- }
2569
-
2570
- // Bas
2571
- if (overflow.bottom) {
2572
- const overflowStart = Math.max(0, this.height - compTop);
2573
- const overflowHeight = Math.min(actualHeight, compBottom - this.height);
2574
- ctx.fillRect(compLeft, this.height - overflowHeight, actualWidth, overflowHeight);
2575
- ctx.strokeRect(compLeft, this.height - overflowHeight, actualWidth, overflowHeight);
2576
- }
2577
-
2578
- // 2. Points rouges aux coins
2579
- ctx.fillStyle = 'red';
2580
- const markerSize = 6;
2581
-
2582
- // Coin supérieur gauche
2583
- if (overflow.left || overflow.top) {
2584
- ctx.fillRect(compLeft, compTop, markerSize, markerSize);
2585
- }
2586
-
2587
- // Coin supérieur droit
2588
- if (overflow.right || overflow.top) {
2589
- ctx.fillRect(compRight - markerSize, compTop, markerSize, markerSize);
2590
- }
2591
-
2592
- // Coin inférieur gauche
2593
- if (overflow.left || overflow.bottom) {
2594
- ctx.fillRect(compLeft, compBottom - markerSize, markerSize, markerSize);
2595
- }
2596
-
2597
- // Coin inférieur droit
2598
- if (overflow.right || overflow.bottom) {
2599
- ctx.fillRect(compRight - markerSize, compBottom - markerSize, markerSize, markerSize);
2600
- }
2601
-
2602
- // 3. Texte d'information (optionnel)
2603
- if (this.debbug && comp.text) {
2604
- ctx.fillStyle = 'red';
2605
- ctx.font = '10px monospace';
2606
- ctx.textAlign = 'left';
2607
-
2608
- const overflowText = [];
2609
- if (overflow.left) overflowText.push('←');
2610
- if (overflow.right) overflowText.push('→');
2611
- if (overflow.top) overflowText.push('↑');
2612
- if (overflow.bottom) overflowText.push('↓');
2613
-
2614
- if (overflowText.length > 0) {
2615
- ctx.fillText(
2616
- `"${comp.text.substring(0, 10)}${comp.text.length > 10 ? '...' : ''}" ${overflowText.join('')}`,
2617
- compLeft + 5,
2618
- compTop - 5
2619
- );
2620
- }
2621
- }
2622
-
2623
- ctx.restore();
2624
- }
2625
- }
2626
-
2627
- startRenderLoop() {
2628
- let lastScrollOffset = this.scrollOffset;
2629
-
2630
- const render = () => {
2631
- if (!this._splashFinished) {
2632
- requestAnimationFrame(render);
2633
- return;
2634
- }
2635
-
2636
- // TOUJOURS effacer l'écran en premier
2637
- this.ctx.fillStyle = this.backgroundColor || '#ffffff';
2638
- this.ctx.fillRect(0, 0, this.width, this.height);
2639
-
2640
- // Si une transition est en cours
2641
- if (this.transitionState.isTransitioning) {
2642
- // Mettre à jour la progression
2643
- const elapsed = Date.now() - this.transitionState.startTime;
2644
- this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
2645
-
2646
- // Rendu spécial pour la transition
2647
- this.renderSimpleTransition();
2648
-
2649
- // Si la transition est terminée
2650
- if (this.transitionState.progress >= 1) {
2651
- this.transitionState.isTransitioning = false;
2652
- this.transitionState.oldComponents = [];
2653
- }
2654
- }
2655
- // Sinon, rendu normal
2656
- else {
2657
- this.renderFull();
2658
- }
2659
-
2660
- // Calcul FPS (optionnel)
2661
- this._frames++;
2662
- const now = performance.now();
2663
- const elapsed = now - this._lastFpsTime;
2664
-
2665
- if (elapsed >= 1000) {
2666
- this.fps = Math.round((this._frames * 1000) / elapsed);
2667
- this._frames = 0;
2668
- this._lastFpsTime = now;
2669
- }
2670
-
2671
- if (this.showFps) {
2672
- this.ctx.save();
2673
- this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
2674
- this.ctx.fillRect(10, 10, 100, 40);
2675
- this.ctx.fillStyle = '#00ff00';
2676
- this.ctx.font = 'bold 20px monospace';
2677
- this.ctx.textAlign = 'left';
2678
- this.ctx.textBaseline = 'top';
2679
- this.ctx.fillText(`FPS: ${this.fps}`, 20, 20);
2680
- this.ctx.restore();
2681
- }
2682
-
2683
- // Mettre à jour l'inertie du scroll
2684
- if (Math.abs(this.scrollVelocity) > 0.1 && !this.isDragging) {
2685
- this.scrollWorker.postMessage({ type: 'UPDATE_INERTIA' });
2686
- }
2687
-
2688
- requestAnimationFrame(render);
2689
- };
2690
-
2691
- render();
2692
- }
2693
-
2694
- /**
2695
- * Rendu ultra simple pour la transition slide
2696
- */
2697
- renderSimpleTransition() {
2698
- const { progress, type, direction, oldComponents, newComponents } = this.transitionState;
2699
-
2700
- // Easing pour une animation plus fluide
2701
- const eased = this.easeInOutCubic(progress);
2702
-
2703
- const directionMultiplier = direction === 'forward' ? 1 : -1;
2704
-
2705
- if (type === 'slide') {
2706
- // ✅ AMÉLIORATION 1 : Dessiner l'ANCIENNE vue qui sort
2707
- this.ctx.save();
2708
-
2709
- // L'ancienne vue se déplace vers la gauche/droite
2710
- const oldOffset = -this.width * eased * directionMultiplier;
2711
- this.ctx.translate(oldOffset, 0);
2712
-
2713
- // Légère transparence pour donner de la profondeur
2714
- this.ctx.globalAlpha = 1 - (eased * 0.3);
2715
-
2716
- // Dessiner les anciens composants
2717
- for (let comp of oldComponents) {
2718
- if (comp && comp.visible) {
2719
- const isFixed = this.isFixedComponent(comp);
2720
-
2721
- if (isFixed) {
2722
- comp.draw(this.ctx);
2723
- } else {
2724
- this.ctx.save();
2725
- this.ctx.translate(0, 0); // Pas de scroll pendant la transition
2726
- comp.draw(this.ctx);
2727
- this.ctx.restore();
2728
- }
2729
- }
2730
- }
2731
-
2732
- this.ctx.restore();
2733
-
2734
- // ✅ AMÉLIORATION 2 : Dessiner la NOUVELLE vue qui entre
2735
- this.ctx.save();
2736
-
2737
- // La nouvelle vue arrive de la droite/gauche
2738
- const newOffset = this.width * (1 - eased) * directionMultiplier;
2739
- this.ctx.translate(newOffset, 0);
2740
-
2741
- // Dessiner les nouveaux composants
2742
- for (let comp of newComponents) {
2743
- if (comp && comp.visible) {
2744
- const isFixed = this.isFixedComponent(comp);
2745
-
2746
- if (isFixed) {
2747
- comp.draw(this.ctx);
2748
- } else {
2749
- this.ctx.save();
2750
- this.ctx.translate(0, 0); // Pas de scroll pendant la transition
2751
- comp.draw(this.ctx);
2752
- this.ctx.restore();
2753
- }
2754
- }
2755
- }
2756
-
2757
- this.ctx.restore();
2758
- }
2759
- else if (type === 'fade') {
2760
- // Ancienne vue qui fade out
2761
- this.ctx.save();
2762
- this.ctx.globalAlpha = 1 - eased;
2763
-
2764
- for (let comp of oldComponents) {
2765
- if (comp && comp.visible) {
2766
- const isFixed = this.isFixedComponent(comp);
2767
- if (isFixed) {
2768
- comp.draw(this.ctx);
2769
- } else {
2770
- this.ctx.save();
2771
- this.ctx.translate(0, 0);
2772
- comp.draw(this.ctx);
2773
- this.ctx.restore();
2774
- }
2775
- }
2776
- }
2777
-
2778
- this.ctx.restore();
2779
-
2780
- // Nouvelle vue qui fade in
2781
- this.ctx.save();
2782
- this.ctx.globalAlpha = eased;
2783
-
2784
- for (let comp of newComponents) {
2785
- if (comp && comp.visible) {
2786
- const isFixed = this.isFixedComponent(comp);
2787
- if (isFixed) {
2788
- comp.draw(this.ctx);
2789
- } else {
2790
- this.ctx.save();
2791
- this.ctx.translate(0, 0);
2792
- comp.draw(this.ctx);
2793
- this.ctx.restore();
2794
- }
2795
- }
2796
- }
2797
-
2798
- this.ctx.restore();
2799
- }
2800
- else {
2801
- this.renderFull();
2802
- }
2803
- }
2804
-
2805
- /**
2806
- * Rendu normal (sans transition)
2807
- */
2808
- renderFull() {
2809
- this.ctx.save();
2810
-
2811
- // Séparer les composants fixes et scrollables
2812
- const scrollableComponents = [];
2813
- const fixedComponents = [];
2814
-
2815
- for (let comp of this.components) {
2816
- if (this.isFixedComponent(comp)) {
2817
- fixedComponents.push(comp);
2818
- } else {
2819
- scrollableComponents.push(comp);
2820
- }
2821
- }
2822
-
2823
- // Dessiner les composants scrollables
2824
- if (scrollableComponents.length > 0) {
2825
- this.ctx.save();
2826
- this.ctx.translate(0, this.scrollOffset);
2827
-
2828
- for (let comp of scrollableComponents) {
2829
- if (comp.visible) {
2830
- comp.draw(this.ctx);
2831
- }
2832
- }
2833
-
2834
- this.ctx.restore();
2835
- }
2836
-
2837
- // Dessiner les composants fixes
2838
- for (let comp of fixedComponents) {
2839
- if (comp.visible) {
2840
- comp.draw(this.ctx);
2841
- }
2842
- }
2843
-
2844
- this.ctx.restore();
2845
- }
2846
-
2847
- /**
2848
- * Mettre à jour la progression de la transition
2849
- * @private
2850
- */
2851
- updateTransition() {
2852
- if (!this.transitionState.isTransitioning) return;
2853
-
2854
- const elapsed = Date.now() - this.transitionState.startTime;
2855
- this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
2856
-
2857
- // Si la transition est terminée
2858
- if (this.transitionState.progress >= 1) {
2859
- this.transitionState.isTransitioning = false;
2860
-
2861
- // Marquer tous les nouveaux composants comme sales pour le prochain rendu
2862
- this.transitionState.newComponents.forEach(comp => {
2863
- this.markComponentDirty(comp);
2864
- });
2865
- }
2866
- }
2867
-
2868
- /**
2869
- * Rendu complet normal (sans transition)
2870
- * @private
2871
- */
2872
- renderFull() {
2873
- // Sauvegarder le contexte
2874
- this.ctx.save();
2875
-
2876
- // Séparer les composants
2877
- const scrollableComponents = [];
2878
- const fixedComponents = [];
2879
-
2880
- for (let comp of this.components) {
2881
- if (this.isFixedComponent(comp)) {
2882
- fixedComponents.push(comp);
2883
- } else {
2884
- scrollableComponents.push(comp);
2885
- }
2886
- }
2887
-
2888
- // Dessiner les composants scrollables avec translation
2889
- if (scrollableComponents.length > 0) {
2890
- this.ctx.save();
2891
- this.ctx.translate(0, this.scrollOffset);
2892
-
2893
- for (let comp of scrollableComponents) {
2894
- if (comp.visible) {
2895
- // Viewport culling
2896
- const screenY = comp.y + this.scrollOffset;
2897
- const isInViewport = screenY + comp.height >= -100 && screenY <= this.height + 100;
2898
-
2899
- if (isInViewport) {
2900
- comp.draw(this.ctx);
2901
- }
2902
- }
2903
- }
2904
-
2905
- this.ctx.restore();
2906
- }
2907
-
2908
- // Dessiner les composants fixes
2909
- for (let comp of fixedComponents) {
2910
- if (comp.visible) {
2911
- comp.draw(this.ctx);
2912
- }
2913
- }
2914
-
2915
- // Restaurer le contexte
2916
- this.ctx.restore();
2917
- }
2918
-
2919
-
2920
-
2921
- /**
2922
- * Fait défiler à une position spécifique
2923
- * @param {number} offset - Position cible
2924
- * @param {boolean} animated - Avec animation
2925
- */
2926
- scrollTo(offset, animated = true) {
2927
- if (animated) {
2928
- const startOffset = this.scrollOffset;
2929
- const delta = offset - startOffset;
2930
- const duration = 300;
2931
- const startTime = performance.now();
2932
-
2933
- const animate = (currentTime) => {
2934
- const elapsed = currentTime - startTime;
2935
- const progress = Math.min(elapsed / duration, 1);
2936
- const ease = this.easeOutCubic(progress);
2937
- const currentOffset = startOffset + delta * ease;
2938
-
2939
- this.scrollWorker.postMessage({
2940
- type: 'SET_SCROLL_OFFSET',
2941
- payload: {
2942
- scrollOffset: currentOffset
2943
- }
2944
- });
2945
-
2946
- if (progress < 1) {
2947
- requestAnimationFrame(animate);
2948
- }
2949
- };
2950
-
2951
- requestAnimationFrame(animate);
2952
- } else {
2953
- this.scrollWorker.postMessage({
2954
- type: 'SET_SCROLL_OFFSET',
2955
- payload: {
2956
- scrollOffset: offset
2957
- }
2958
- });
2959
- }
2960
- }
2961
-
2962
- /**
2963
- * Fonction d'easing
2964
- */
2965
- easeOutCubic(t) {
2966
- return 1 - Math.pow(1 - t, 3);
2967
- }
2968
-
2969
- /**
2970
- * Nettoie les ressources
2971
- */
2972
- destroy() {
2973
- if (this.scrollWorker) {
2974
- this.scrollWorker.terminate();
2975
- }
2976
- if (this.worker) {
2977
- this.worker.terminate();
2978
- }
2979
- if (this.logicWorker) {
2980
- this.logicWorker.terminate();
2981
- }
2982
-
2983
- if (this.ctx && typeof this.ctx.destroy === 'function') {
2984
- this.ctx.destroy();
2985
- }
2986
-
2987
- // Nettoyer les écouteurs d'événements
2988
- this.canvas.removeEventListener('touchstart', this.handleTouchStart);
2989
- this.canvas.removeEventListener('touchmove', this.handleTouchMove);
2990
- this.canvas.removeEventListener('touchend', this.handleTouchEnd);
2991
- this.canvas.removeEventListener('mousedown', this.handleMouseDown);
2992
- this.canvas.removeEventListener('mousemove', this.handleMouseMove);
2993
- this.canvas.removeEventListener('mouseup', this.handleMouseUp);
2994
- window.removeEventListener('resize', this.handleResize);
2995
- }
2996
-
2997
- // ✅ AJOUTER: Afficher les métriques à l'écran
2998
- displayMetrics() {
2999
- const metrics = this.metrics;
3000
-
3001
- console.table({
3002
- '⚙️ Initialisation Framework': `${metrics.initTime.toFixed(2)}ms`,
3003
- '🎨 Premier Rendu': `${metrics.firstRenderTime.toFixed(2)}ms`,
3004
- '🚀 Temps Total Startup': `${metrics.totalStartupTime.toFixed(2)}ms`,
3005
- '📊 FPS Actuel': this.fps
3006
- });
3007
- }
3008
-
3009
- // ✅ AJOUTER: Obtenir les métriques
3010
- getMetrics() {
3011
- return {
3012
- ...this.metrics,
3013
- currentFPS: this.fps,
3014
- componentsCount: this.components.length
3015
- };
3016
- }
3017
-
3018
- isFixedComponent(comp) {
3019
- return FIXED_COMPONENT_TYPES.has(comp.constructor);
3020
- }
3021
-
3022
- showToast(message, duration = 3000) {
3023
- const toast = new Toast(this, {
3024
- text: message,
3025
- duration: duration,
3026
- x: this.width / 2,
3027
- y: this.height - 100
3028
- });
3029
- this.add(toast);
3030
- toast.show();
3031
- }
3032
- }
3033
-
3034
- export default CanvasFramework;