canvasframework 0.4.6 → 0.4.8
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.
- package/core/CanvasFramework.js +1683 -1394
- package/core/ThemeManager.js +358 -0
- package/index.js +1 -17
- package/package.json +1 -1
package/core/CanvasFramework.js
CHANGED
|
@@ -81,7 +81,6 @@ import DevTools from '../utils/DevTools.js';
|
|
|
81
81
|
import InspectionOverlay from '../utils/InspectionOverlay.js';
|
|
82
82
|
import DevToolsConsole from '../utils/DevToolsConsole.js';
|
|
83
83
|
|
|
84
|
-
|
|
85
84
|
// Features
|
|
86
85
|
import PullToRefresh from '../features/PullToRefresh.js';
|
|
87
86
|
import Skeleton from '../features/Skeleton.js';
|
|
@@ -104,46 +103,29 @@ import FeatureFlags from '../manager/FeatureFlags.js';
|
|
|
104
103
|
|
|
105
104
|
// WebGL Adapter
|
|
106
105
|
import WebGLCanvasAdapter from './WebGLCanvasAdapter.js';
|
|
107
|
-
import ui, {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
text: '#000000',
|
|
113
|
-
primary: '#6200EE',
|
|
114
|
-
secondary: '#03DAC6',
|
|
115
|
-
buttonText: '#FFFFFF',
|
|
116
|
-
buttonBackground: '#6200EE',
|
|
117
|
-
border: '#E0E0E0'
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
export const darkTheme = {
|
|
121
|
-
background: '#121212',
|
|
122
|
-
text: '#FFFFFF',
|
|
123
|
-
primary: '#BB86FC',
|
|
124
|
-
secondary: '#03DAC6',
|
|
125
|
-
buttonText: '#000000',
|
|
126
|
-
buttonBackground: '#BB86FC',
|
|
127
|
-
border: '#333333'
|
|
128
|
-
};
|
|
106
|
+
import ui, {
|
|
107
|
+
createRef
|
|
108
|
+
} from './UIBuilder.js';
|
|
109
|
+
import ThemeManager from './ThemeManager.js';
|
|
110
|
+
|
|
129
111
|
|
|
130
112
|
const FIXED_COMPONENT_TYPES = new Set([
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
113
|
+
AppBar,
|
|
114
|
+
BottomNavigationBar,
|
|
115
|
+
Drawer,
|
|
116
|
+
Dialog,
|
|
117
|
+
Modal,
|
|
118
|
+
Tabs,
|
|
119
|
+
FAB,
|
|
120
|
+
Toast,
|
|
121
|
+
Camera,
|
|
122
|
+
QRCodeReader,
|
|
123
|
+
Banner,
|
|
124
|
+
SliverAppBar,
|
|
125
|
+
BottomSheet,
|
|
126
|
+
ContextMenu,
|
|
127
|
+
OpenStreetMap,
|
|
128
|
+
SelectDialog
|
|
147
129
|
]);
|
|
148
130
|
|
|
149
131
|
/**
|
|
@@ -166,295 +148,513 @@ const FIXED_COMPONENT_TYPES = new Set([
|
|
|
166
148
|
* @property {number} scrollFriction - Friction du défilement
|
|
167
149
|
*/
|
|
168
150
|
class CanvasFramework {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.height = window.innerHeight;
|
|
178
|
-
this.dpr = window.devicePixelRatio || 1;
|
|
179
|
-
|
|
180
|
-
this.platform = this.detectPlatform();
|
|
181
|
-
|
|
182
|
-
// Thèmes
|
|
183
|
-
this.lightTheme = lightTheme;
|
|
184
|
-
this.darkTheme = darkTheme;
|
|
185
|
-
// État actuel + préférence
|
|
186
|
-
this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
|
|
187
|
-
this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
|
|
188
|
-
|
|
189
|
-
// Applique le thème initial
|
|
190
|
-
this.setupSystemThemeListener();
|
|
191
|
-
|
|
192
|
-
// Récupère override utilisateur
|
|
193
|
-
const savedOverride = localStorage.getItem('themeOverride');
|
|
194
|
-
if (savedOverride && ['light', 'dark'].includes(savedOverride)) {
|
|
195
|
-
this.userThemeOverride = savedOverride;
|
|
196
|
-
this.themeMode = savedOverride;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
this.components = [];
|
|
200
|
-
this.theme = lightTheme; // thème par défaut
|
|
201
|
-
// ✅ AJOUTER ICI :
|
|
202
|
-
this._cachedMaxScroll = 0;
|
|
203
|
-
this._maxScrollDirty = true;
|
|
204
|
-
this.resizeTimeout = null;
|
|
205
|
-
|
|
206
|
-
//this.applyThemeFromSystem();
|
|
207
|
-
this.state = {};
|
|
208
|
-
// NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
|
|
209
|
-
this.useWebGL = options.useWebGL !== false; // true par défaut
|
|
210
|
-
// Initialiser le contexte approprié
|
|
211
|
-
if (this.useWebGL) {
|
|
212
|
-
try {
|
|
213
|
-
this.ctx = new WebGLCanvasAdapter(this.canvas);
|
|
214
|
-
} catch (e) {
|
|
151
|
+
/**
|
|
152
|
+
* Crée une instance de CanvasFramework
|
|
153
|
+
* @param {string} canvasId - ID de l'élément canvas
|
|
154
|
+
*/
|
|
155
|
+
constructor(canvasId, options = {}) {
|
|
156
|
+
// ✅ AJOUTER: Démarrer le chronomètre
|
|
157
|
+
const startTime = performance.now();
|
|
158
|
+
this.canvas = document.getElementById(canvasId);
|
|
215
159
|
this.ctx = this.canvas.getContext('2d');
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// Gestion des événements
|
|
243
|
-
this.isDragging = false;
|
|
244
|
-
this.lastTouchY = 0;
|
|
245
|
-
this.scrollOffset = 0;
|
|
246
|
-
this.scrollVelocity = 0;
|
|
247
|
-
this.scrollFriction = 0.95;
|
|
248
|
-
|
|
249
|
-
// Optimisation
|
|
250
|
-
this.dirtyComponents = new Set();
|
|
251
|
-
this.optimizationEnabled = false;
|
|
252
|
-
|
|
253
|
-
// AJOUTER CETTE LIGNE
|
|
254
|
-
this.animator = new AnimationEngine();
|
|
255
|
-
|
|
256
|
-
// ===== NOUVEAU SYSTÈME DE ROUTING =====
|
|
257
|
-
this.routes = new Map();
|
|
258
|
-
this.currentRoute = '/';
|
|
259
|
-
this.currentParams = {};
|
|
260
|
-
this.currentQuery = {};
|
|
261
|
-
this.history = [];
|
|
262
|
-
this.historyIndex = -1;
|
|
263
|
-
|
|
264
|
-
// Animation de transition
|
|
265
|
-
this.transitionState = {
|
|
266
|
-
isTransitioning: false,
|
|
267
|
-
progress: 0,
|
|
268
|
-
duration: 300,
|
|
269
|
-
type: 'slide', // 'slide', 'fade', 'none'
|
|
270
|
-
direction: 'forward', // 'forward', 'back'
|
|
271
|
-
oldComponents: [],
|
|
272
|
-
newComponents: []
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
this.setupCanvas();
|
|
276
|
-
this.setupEventListeners();
|
|
277
|
-
this.setupHistoryListener();
|
|
278
|
-
this.startRenderLoop();
|
|
279
|
-
|
|
280
|
-
this.devTools = new DevTools(this);
|
|
281
|
-
this.inspectionOverlay = new InspectionOverlay(this);
|
|
282
|
-
|
|
283
|
-
// MODIFICATION: Vérifier explicitement l'option enableDevTools
|
|
284
|
-
const shouldEnableDevTools = options.enableDevTools === true;
|
|
285
|
-
|
|
286
|
-
if (shouldEnableDevTools) {
|
|
287
|
-
this.enableDevTools();
|
|
288
|
-
console.log('DevTools enabled. Press Ctrl+Shift+D to toggle.');
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Détecte le thème système et applique si mode = 'system'
|
|
294
|
-
*/
|
|
295
|
-
applyThemeFromSystem() {
|
|
296
|
-
// ✅ Vérifier que tout est initialisé
|
|
297
|
-
if (!this.lightTheme || !this.darkTheme) {
|
|
298
|
-
console.warn('Thèmes non initialisés');
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (this.themeMode === 'system') {
|
|
303
|
-
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
304
|
-
const newTheme = prefersDark ? this.darkTheme : this.lightTheme;
|
|
305
|
-
this.setTheme(newTheme);
|
|
306
|
-
} else {
|
|
307
|
-
// Mode forcé
|
|
308
|
-
this.setTheme(
|
|
309
|
-
this.themeMode === 'dark' ? this.darkTheme : this.lightTheme
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Écoute les changements système (ex: utilisateur bascule dark mode)
|
|
316
|
-
*/
|
|
317
|
-
setupSystemThemeListener() {
|
|
318
|
-
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
319
|
-
|
|
320
|
-
// Ancienne méthode (compatibilité large)
|
|
321
|
-
if (mediaQuery.addEventListener) {
|
|
322
|
-
mediaQuery.addEventListener('change', (e) => {
|
|
323
|
-
if (this.themeMode === 'system') {
|
|
324
|
-
this.applyThemeFromSystem();
|
|
160
|
+
this.width = window.innerWidth;
|
|
161
|
+
this.height = window.innerHeight;
|
|
162
|
+
this.dpr = window.devicePixelRatio || 1;
|
|
163
|
+
this.splashOptions = {
|
|
164
|
+
enabled: options.splash?.enabled === true, // false par défaut
|
|
165
|
+
duration: options.splash?.duration || 700,
|
|
166
|
+
fadeOutDuration: options.splash?.fadeOutDuration || 500,
|
|
167
|
+
backgroundColor: options.splash?.backgroundColor || ['#667eea', '#764ba2'], // Gradient ou couleur unie
|
|
168
|
+
spinnerColor: options.splash?.spinnerColor || 'white',
|
|
169
|
+
spinnerBackground: options.splash?.spinnerBackground || 'rgba(255, 255, 255, 0.3)',
|
|
170
|
+
textColor: options.splash?.textColor || 'white',
|
|
171
|
+
text: options.splash?.text || 'Chargement...',
|
|
172
|
+
textSize: options.splash?.textSize || 20,
|
|
173
|
+
textFont: options.splash?.textFont || 'Arial',
|
|
174
|
+
progressBarColor: options.splash?.progressBarColor || 'white',
|
|
175
|
+
progressBarBackground: options.splash?.progressBarBackground || 'rgba(255, 255, 255, 0.3)',
|
|
176
|
+
showProgressBar: options.splash?.showProgressBar !== false, // true par défaut
|
|
177
|
+
logo: options.splash?.logo || null, // URL d'une image (optionnel)
|
|
178
|
+
logoWidth: options.splash?.logoWidth || 100,
|
|
179
|
+
logoHeight: options.splash?.logoHeight || 100
|
|
180
|
+
};
|
|
181
|
+
// ✅ MODIFIER : Vérifier si le splash est activé
|
|
182
|
+
if (this.splashOptions.enabled) {
|
|
183
|
+
this.showSplashScreen();
|
|
184
|
+
} else {
|
|
185
|
+
this._splashFinished = true; // Passer directement au rendu
|
|
325
186
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
187
|
+
|
|
188
|
+
this.platform = this.detectPlatform();
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
// État actuel + préférence
|
|
192
|
+
this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
|
|
193
|
+
this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
|
|
194
|
+
|
|
195
|
+
// Applique le thème initial
|
|
196
|
+
this.setupSystemThemeListener();
|
|
197
|
+
|
|
198
|
+
// Récupère override utilisateur
|
|
199
|
+
const savedOverride = localStorage.getItem('themeOverride');
|
|
200
|
+
if (savedOverride && ['light', 'dark'].includes(savedOverride)) {
|
|
201
|
+
this.userThemeOverride = savedOverride;
|
|
202
|
+
this.themeMode = savedOverride;
|
|
332
203
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
} else if (mode === 'system') {
|
|
355
|
-
this.userThemeOverride = null;
|
|
356
|
-
localStorage.removeItem('themeOverride');
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
this.applyThemeFromSystem();
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Active ou désactive les DevTools
|
|
364
|
-
* @param {boolean} enabled - true pour activer, false pour désactiver
|
|
365
|
-
*/
|
|
366
|
-
enableDevTools(enabled = true) {
|
|
367
|
-
if (enabled) {
|
|
368
|
-
// Créer le DevTools s'il n'existe pas
|
|
369
|
-
if (!this.devTools) {
|
|
370
|
-
this.devTools = new DevTools(this);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Attacher seulement si pas déjà fait
|
|
374
|
-
if (!this.devTools._isAttached) {
|
|
375
|
-
this.devTools.attachToFramework();
|
|
376
|
-
this.devTools._isAttached = true;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Afficher le bouton
|
|
380
|
-
if (this.devTools.toggleBtn) {
|
|
381
|
-
this.devTools.toggleBtn.style.display = 'block';
|
|
382
|
-
}
|
|
383
|
-
} else {
|
|
384
|
-
// Désactiver complètement
|
|
385
|
-
if (this.devTools) {
|
|
386
|
-
// Détacher du framework
|
|
387
|
-
if (this.devTools.detachFromFramework) {
|
|
388
|
-
this.devTools.detachFromFramework();
|
|
389
|
-
} else if (this.devTools.cleanup) {
|
|
390
|
-
this.devTools.cleanup();
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Supprimer de la page DOM
|
|
394
|
-
if (this.devTools.container && this.devTools.container.parentNode) {
|
|
395
|
-
this.devTools.container.parentNode.removeChild(this.devTools.container);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (this.devTools.toggleBtn && this.devTools.toggleBtn.parentNode) {
|
|
399
|
-
this.devTools.toggleBtn.parentNode.removeChild(this.devTools.toggleBtn);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
this.devTools._isAttached = false;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Bascule l'overlay d'inspection
|
|
409
|
-
*/
|
|
410
|
-
toggleInspection() {
|
|
411
|
-
this.inspectionOverlay.toggle();
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Exécute une commande DevTools
|
|
416
|
-
*/
|
|
417
|
-
devToolsCommand(command, ...args) {
|
|
418
|
-
switch (command) {
|
|
419
|
-
case 'inspect':
|
|
420
|
-
this.inspectionOverlay.enable();
|
|
421
|
-
break;
|
|
422
|
-
case 'performance':
|
|
423
|
-
this.devTools.switchTab('performance');
|
|
424
|
-
this.devTools.toggle();
|
|
425
|
-
break;
|
|
426
|
-
case 'components':
|
|
427
|
-
this.devTools.switchTab('components');
|
|
428
|
-
this.devTools.toggle();
|
|
429
|
-
break;
|
|
430
|
-
case 'highlight':
|
|
431
|
-
if (args[0]) {
|
|
432
|
-
this.devTools.highlightComponent(args[0]);
|
|
204
|
+
|
|
205
|
+
this.components = [];
|
|
206
|
+
// ✅ AJOUTER ICI :
|
|
207
|
+
this._cachedMaxScroll = 0;
|
|
208
|
+
this._maxScrollDirty = true;
|
|
209
|
+
this.resizeTimeout = null;
|
|
210
|
+
|
|
211
|
+
//this.applyThemeFromSystem();
|
|
212
|
+
this.state = {};
|
|
213
|
+
// NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
|
|
214
|
+
this.useWebGL = options.useWebGL !== false; // true par défaut
|
|
215
|
+
// Initialiser le contexte approprié
|
|
216
|
+
if (this.useWebGL) {
|
|
217
|
+
try {
|
|
218
|
+
this.ctx = new WebGLCanvasAdapter(this.canvas);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
this.ctx = this.canvas.getContext('2d');
|
|
221
|
+
this.useWebGL = false;
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
this.ctx = this.canvas.getContext('2d');
|
|
433
225
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
226
|
+
// Calcule FPS
|
|
227
|
+
this.fps = 0;
|
|
228
|
+
this._frames = 0;
|
|
229
|
+
this._lastFpsTime = performance.now();
|
|
230
|
+
this.showFps = options.showFps || false; // false par défaut
|
|
231
|
+
this.debbug = options.debug || false; // false par défaut (et correction de la faute de frappe)
|
|
232
|
+
|
|
233
|
+
// Worker pour multithreading Canvas Worker
|
|
234
|
+
this.worker = this.createCanvasWorker();
|
|
235
|
+
this.worker.onmessage = this.handleWorkerMessage.bind(this);
|
|
236
|
+
this.worker.postMessage({
|
|
237
|
+
type: 'INIT',
|
|
238
|
+
payload: {
|
|
239
|
+
components: []
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Logic Worker
|
|
244
|
+
this.logicWorker = this.createLogicWorker();
|
|
245
|
+
this.logicWorker.onmessage = this.handleLogicWorkerMessage.bind(this);
|
|
246
|
+
this.logicWorkerState = {};
|
|
247
|
+
this.logicWorker.postMessage({
|
|
248
|
+
type: 'SET_STATE',
|
|
249
|
+
payload: this.state
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Envoyer l'état initial au worker
|
|
253
|
+
this.logicWorker.postMessage({
|
|
254
|
+
type: 'SET_STATE',
|
|
255
|
+
payload: this.state
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Gestion des événements
|
|
259
|
+
this.isDragging = false;
|
|
260
|
+
this.lastTouchY = 0;
|
|
261
|
+
this.scrollOffset = 0;
|
|
262
|
+
this.scrollVelocity = 0;
|
|
263
|
+
this.scrollFriction = 0.95;
|
|
264
|
+
|
|
265
|
+
// Optimisation
|
|
266
|
+
this.dirtyComponents = new Set();
|
|
267
|
+
this.optimizationEnabled = false;
|
|
268
|
+
|
|
269
|
+
// AJOUTER CETTE LIGNE
|
|
270
|
+
this.animator = new AnimationEngine();
|
|
271
|
+
|
|
272
|
+
// ===== NOUVEAU SYSTÈME DE ROUTING =====
|
|
273
|
+
this.routes = new Map();
|
|
274
|
+
this.currentRoute = '/';
|
|
275
|
+
this.currentParams = {};
|
|
276
|
+
this.currentQuery = {};
|
|
277
|
+
this.history = [];
|
|
278
|
+
this.historyIndex = -1;
|
|
279
|
+
|
|
280
|
+
// Animation de transition
|
|
281
|
+
this.transitionState = {
|
|
282
|
+
isTransitioning: false,
|
|
283
|
+
progress: 0,
|
|
284
|
+
duration: 300,
|
|
285
|
+
type: 'slide', // 'slide', 'fade', 'none'
|
|
286
|
+
direction: 'forward', // 'forward', 'back'
|
|
287
|
+
oldComponents: [],
|
|
288
|
+
newComponents: []
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
this.setupCanvas();
|
|
292
|
+
this.setupEventListeners();
|
|
293
|
+
this.setupHistoryListener();
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
this.startRenderLoop();
|
|
297
|
+
|
|
298
|
+
this.devTools = new DevTools(this);
|
|
299
|
+
this.inspectionOverlay = new InspectionOverlay(this);
|
|
300
|
+
|
|
301
|
+
// MODIFICATION: Vérifier explicitement l'option enableDevTools
|
|
302
|
+
const shouldEnableDevTools = options.enableDevTools === true;
|
|
303
|
+
|
|
304
|
+
if (shouldEnableDevTools) {
|
|
305
|
+
this.enableDevTools();
|
|
306
|
+
console.log('DevTools enabled. Press Ctrl+Shift+D to toggle.');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Initialiser le ThemeManager
|
|
310
|
+
this.themeManager = new ThemeManager(this, {
|
|
311
|
+
lightTheme: options.lightTheme,
|
|
312
|
+
darkTheme: options.darkTheme,
|
|
313
|
+
storageKey: options.themeStorageKey || 'app-theme-mode'
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Raccourci pour accéder au thème actuel
|
|
317
|
+
this.theme = this.themeManager.getTheme();
|
|
318
|
+
|
|
319
|
+
// ✅ AJOUTER: Mesurer le temps d'init
|
|
320
|
+
const initTime = performance.now() - startTime;
|
|
321
|
+
|
|
322
|
+
// ✅ AJOUTER: Stocker les métriques
|
|
323
|
+
this.metrics = {
|
|
324
|
+
initTime: initTime,
|
|
325
|
+
firstRenderTime: null,
|
|
326
|
+
firstInteractionTime: null,
|
|
327
|
+
totalStartupTime: null
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// ✅ AJOUTER: Logger si debug
|
|
331
|
+
if (options.debug || options.showMetrics) {
|
|
332
|
+
console.log(`⚡ Framework initialisé en ${initTime.toFixed(2)}ms`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ✅ AJOUTER: Marquer le premier rendu
|
|
336
|
+
this._firstRenderDone = false;
|
|
337
|
+
this._startupStartTime = startTime;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Affiche un écran de chargement animé
|
|
342
|
+
* @private
|
|
343
|
+
*/
|
|
344
|
+
showSplashScreen() {
|
|
345
|
+
const startTime = performance.now();
|
|
346
|
+
const opts = this.splashOptions;
|
|
347
|
+
|
|
348
|
+
// ✅ Charger le logo si présent
|
|
349
|
+
let logoImage = null;
|
|
350
|
+
if (opts.logo) {
|
|
351
|
+
logoImage = new Image();
|
|
352
|
+
logoImage.src = opts.logo;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const animate = () => {
|
|
356
|
+
const elapsed = performance.now() - startTime;
|
|
357
|
+
const progress = Math.min(elapsed / opts.duration, 1);
|
|
358
|
+
|
|
359
|
+
// Clear
|
|
360
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
361
|
+
|
|
362
|
+
// ✅ Background (gradient ou couleur unie)
|
|
363
|
+
if (Array.isArray(opts.backgroundColor) && opts.backgroundColor.length >= 2) {
|
|
364
|
+
// Gradient
|
|
365
|
+
const gradient = this.ctx.createLinearGradient(0, 0, this.width, this.height);
|
|
366
|
+
gradient.addColorStop(0, opts.backgroundColor[0]);
|
|
367
|
+
gradient.addColorStop(1, opts.backgroundColor[1]);
|
|
368
|
+
this.ctx.fillStyle = gradient;
|
|
369
|
+
} else {
|
|
370
|
+
// Couleur unie
|
|
371
|
+
this.ctx.fillStyle = opts.backgroundColor;
|
|
372
|
+
}
|
|
373
|
+
this.ctx.fillRect(0, 0, this.width, this.height);
|
|
374
|
+
|
|
375
|
+
const centerX = this.width / 2;
|
|
376
|
+
const centerY = this.height / 2;
|
|
377
|
+
|
|
378
|
+
// ✅ Logo (si présent et chargé)
|
|
379
|
+
if (logoImage && logoImage.complete) {
|
|
380
|
+
const logoX = centerX - opts.logoWidth / 2;
|
|
381
|
+
const logoY = centerY - opts.logoHeight - 80;
|
|
382
|
+
this.ctx.drawImage(logoImage, logoX, logoY, opts.logoWidth, opts.logoHeight);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ✅ Spinner animé
|
|
386
|
+
const radius = 40;
|
|
387
|
+
const rotation = (elapsed / 1000) * Math.PI * 2;
|
|
388
|
+
|
|
389
|
+
// Cercle de fond
|
|
390
|
+
this.ctx.beginPath();
|
|
391
|
+
this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
392
|
+
this.ctx.strokeStyle = opts.spinnerBackground;
|
|
393
|
+
this.ctx.lineWidth = 4;
|
|
394
|
+
this.ctx.stroke();
|
|
395
|
+
|
|
396
|
+
// Arc animé
|
|
397
|
+
this.ctx.beginPath();
|
|
398
|
+
this.ctx.arc(centerX, centerY, radius, rotation, rotation + Math.PI * 1.5);
|
|
399
|
+
this.ctx.strokeStyle = opts.spinnerColor;
|
|
400
|
+
this.ctx.lineWidth = 4;
|
|
401
|
+
this.ctx.lineCap = 'round';
|
|
402
|
+
this.ctx.stroke();
|
|
403
|
+
|
|
404
|
+
// ✅ Texte personnalisé
|
|
405
|
+
this.ctx.fillStyle = opts.textColor;
|
|
406
|
+
this.ctx.font = `${opts.textSize}px ${opts.textFont}`;
|
|
407
|
+
this.ctx.textAlign = 'center';
|
|
408
|
+
this.ctx.fillText(opts.text, centerX, centerY + radius + 40);
|
|
409
|
+
|
|
410
|
+
// ✅ Barre de progression (optionnelle)
|
|
411
|
+
if (opts.showProgressBar) {
|
|
412
|
+
const barWidth = 200;
|
|
413
|
+
const barHeight = 4;
|
|
414
|
+
const barX = centerX - barWidth / 2;
|
|
415
|
+
const barY = centerY + radius + 70;
|
|
416
|
+
|
|
417
|
+
// Fond de la barre
|
|
418
|
+
this.ctx.fillStyle = opts.progressBarBackground;
|
|
419
|
+
this.ctx.fillRect(barX, barY, barWidth, barHeight);
|
|
420
|
+
|
|
421
|
+
// Progression
|
|
422
|
+
this.ctx.fillStyle = opts.progressBarColor;
|
|
423
|
+
this.ctx.fillRect(barX, barY, barWidth * progress, barHeight);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Continuer ou fade out
|
|
427
|
+
if (progress < 1) {
|
|
428
|
+
requestAnimationFrame(animate);
|
|
429
|
+
} else {
|
|
430
|
+
this.fadeOutSplash();
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
animate();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Fade out du splash screen
|
|
439
|
+
* @private
|
|
440
|
+
*/
|
|
441
|
+
fadeOutSplash() {
|
|
442
|
+
const opts = this.splashOptions;
|
|
443
|
+
const duration = opts.fadeOutDuration;
|
|
444
|
+
const startTime = performance.now();
|
|
445
|
+
|
|
446
|
+
const fade = () => {
|
|
447
|
+
const elapsed = performance.now() - startTime;
|
|
448
|
+
const progress = elapsed / duration;
|
|
449
|
+
const alpha = 1 - Math.min(progress, 1);
|
|
450
|
+
|
|
451
|
+
if (alpha > 0) {
|
|
452
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
453
|
+
this.ctx.globalAlpha = alpha;
|
|
454
|
+
|
|
455
|
+
// Redessiner le background
|
|
456
|
+
if (Array.isArray(opts.backgroundColor) && opts.backgroundColor.length >= 2) {
|
|
457
|
+
const gradient = this.ctx.createLinearGradient(0, 0, this.width, this.height);
|
|
458
|
+
gradient.addColorStop(0, opts.backgroundColor[0]);
|
|
459
|
+
gradient.addColorStop(1, opts.backgroundColor[1]);
|
|
460
|
+
this.ctx.fillStyle = gradient;
|
|
461
|
+
} else {
|
|
462
|
+
this.ctx.fillStyle = opts.backgroundColor;
|
|
463
|
+
}
|
|
464
|
+
this.ctx.fillRect(0, 0, this.width, this.height);
|
|
465
|
+
|
|
466
|
+
// Spinner pendant le fade
|
|
467
|
+
const centerX = this.width / 2;
|
|
468
|
+
const centerY = this.height / 2;
|
|
469
|
+
const radius = 40;
|
|
470
|
+
|
|
471
|
+
this.ctx.beginPath();
|
|
472
|
+
this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
473
|
+
this.ctx.strokeStyle = opts.spinnerBackground;
|
|
474
|
+
this.ctx.lineWidth = 4;
|
|
475
|
+
this.ctx.stroke();
|
|
476
|
+
|
|
477
|
+
this.ctx.globalAlpha = 1;
|
|
478
|
+
requestAnimationFrame(fade);
|
|
479
|
+
} else {
|
|
480
|
+
this._splashFinished = true;
|
|
481
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
fade();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ✅ AJOUTER: Méthode pour mesurer le premier rendu
|
|
489
|
+
_markFirstRender() {
|
|
490
|
+
if (!this._firstRenderDone) {
|
|
491
|
+
this._firstRenderDone = true;
|
|
492
|
+
const firstRenderTime = performance.now() - this._startupStartTime - this.metrics.initTime;
|
|
493
|
+
this.metrics.firstRenderTime = firstRenderTime;
|
|
494
|
+
this.metrics.totalStartupTime = performance.now() - this._startupStartTime;
|
|
495
|
+
|
|
496
|
+
if (this.showMetrics) {
|
|
497
|
+
console.log(`🎨 Premier rendu en ${firstRenderTime.toFixed(2)}ms`);
|
|
498
|
+
console.log(`🚀 Temps total de démarrage: ${this.metrics.totalStartupTime.toFixed(2)}ms`);
|
|
499
|
+
this.displayMetrics();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Écoute les changements système (ex: utilisateur bascule dark mode)
|
|
506
|
+
*/
|
|
507
|
+
setupSystemThemeListener() {
|
|
508
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
509
|
+
|
|
510
|
+
// Ancienne méthode (compatibilité large)
|
|
511
|
+
if (mediaQuery.addEventListener) {
|
|
512
|
+
mediaQuery.addEventListener('change', (e) => {
|
|
513
|
+
if (this.themeMode === 'system') {
|
|
514
|
+
this.applyThemeFromSystem();
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
} else {
|
|
518
|
+
// Anciens navigateurs (rare en 2026)
|
|
519
|
+
mediaQuery.addListener((e) => {
|
|
520
|
+
if (this.themeMode === 'system') {
|
|
521
|
+
this.applyThemeFromSystem();
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Change le mode thème
|
|
529
|
+
* @param {'light'|'dark'|'system'} mode - Mode à appliquer
|
|
530
|
+
* @param {boolean} [save=true] - Sauvegarder le choix utilisateur ?
|
|
531
|
+
*/
|
|
532
|
+
setThemeMode(mode) {
|
|
533
|
+
this.themeManager.setMode(mode);
|
|
534
|
+
this.theme = this.themeManager.getTheme();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Obtient une couleur du thème
|
|
539
|
+
*/
|
|
540
|
+
getColor(colorName) {
|
|
541
|
+
return this.themeManager.getColor(colorName);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Ajoute un listener de changement de thème
|
|
546
|
+
*/
|
|
547
|
+
onThemeChange(callback) {
|
|
548
|
+
this.themeManager.addListener((theme) => {
|
|
549
|
+
this.theme = theme;
|
|
550
|
+
callback(theme);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Bascule entre light et dark
|
|
556
|
+
*/
|
|
557
|
+
toggleTheme() {
|
|
558
|
+
this.themeManager.toggle();
|
|
559
|
+
this.theme = this.themeManager.getTheme();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Active ou désactive les DevTools
|
|
564
|
+
* @param {boolean} enabled - true pour activer, false pour désactiver
|
|
565
|
+
*/
|
|
566
|
+
enableDevTools(enabled = true) {
|
|
567
|
+
if (enabled) {
|
|
568
|
+
// Créer le DevTools s'il n'existe pas
|
|
569
|
+
if (!this.devTools) {
|
|
570
|
+
this.devTools = new DevTools(this);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Attacher seulement si pas déjà fait
|
|
574
|
+
if (!this.devTools._isAttached) {
|
|
575
|
+
this.devTools.attachToFramework();
|
|
576
|
+
this.devTools._isAttached = true;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Afficher le bouton
|
|
580
|
+
if (this.devTools.toggleBtn) {
|
|
581
|
+
this.devTools.toggleBtn.style.display = 'block';
|
|
582
|
+
}
|
|
448
583
|
} else {
|
|
449
|
-
|
|
584
|
+
// Désactiver complètement
|
|
585
|
+
if (this.devTools) {
|
|
586
|
+
// Détacher du framework
|
|
587
|
+
if (this.devTools.detachFromFramework) {
|
|
588
|
+
this.devTools.detachFromFramework();
|
|
589
|
+
} else if (this.devTools.cleanup) {
|
|
590
|
+
this.devTools.cleanup();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Supprimer de la page DOM
|
|
594
|
+
if (this.devTools.container && this.devTools.container.parentNode) {
|
|
595
|
+
this.devTools.container.parentNode.removeChild(this.devTools.container);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (this.devTools.toggleBtn && this.devTools.toggleBtn.parentNode) {
|
|
599
|
+
this.devTools.toggleBtn.parentNode.removeChild(this.devTools.toggleBtn);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
this.devTools._isAttached = false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Bascule l'overlay d'inspection
|
|
609
|
+
*/
|
|
610
|
+
toggleInspection() {
|
|
611
|
+
this.inspectionOverlay.toggle();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Exécute une commande DevTools
|
|
616
|
+
*/
|
|
617
|
+
devToolsCommand(command, ...args) {
|
|
618
|
+
switch (command) {
|
|
619
|
+
case 'inspect':
|
|
620
|
+
this.inspectionOverlay.enable();
|
|
621
|
+
break;
|
|
622
|
+
case 'performance':
|
|
623
|
+
this.devTools.switchTab('performance');
|
|
624
|
+
this.devTools.toggle();
|
|
625
|
+
break;
|
|
626
|
+
case 'components':
|
|
627
|
+
this.devTools.switchTab('components');
|
|
628
|
+
this.devTools.toggle();
|
|
629
|
+
break;
|
|
630
|
+
case 'highlight':
|
|
631
|
+
if (args[0]) {
|
|
632
|
+
this.devTools.highlightComponent(args[0]);
|
|
633
|
+
}
|
|
634
|
+
break;
|
|
635
|
+
case 'reflow':
|
|
636
|
+
this.components.forEach(comp => comp.markDirty());
|
|
637
|
+
break;
|
|
450
638
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
wrapContext(ctx, theme) {
|
|
642
|
+
const originalFillStyle = Object.getOwnPropertyDescriptor(CanvasRenderingContext2D.prototype, 'fillStyle');
|
|
643
|
+
Object.defineProperty(ctx, 'fillStyle', {
|
|
644
|
+
set: (value) => {
|
|
645
|
+
// Si value est blanc/noir ou une couleur “neutre”, tu remplaces par theme
|
|
646
|
+
if (value === '#FFFFFF' || value === '#000000') {
|
|
647
|
+
originalFillStyle.set.call(ctx, value);
|
|
648
|
+
} else {
|
|
649
|
+
originalFillStyle.set.call(ctx, value);
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
get: () => originalFillStyle.get.call(ctx)
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
createCanvasWorker() {
|
|
657
|
+
const workerCode = `
|
|
458
658
|
let components = [];
|
|
459
659
|
|
|
460
660
|
self.onmessage = function(e) {
|
|
@@ -486,13 +686,15 @@ class CanvasFramework {
|
|
|
486
686
|
}
|
|
487
687
|
};
|
|
488
688
|
`;
|
|
489
|
-
|
|
490
|
-
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
491
|
-
return new Worker(URL.createObjectURL(blob));
|
|
492
|
-
}
|
|
493
689
|
|
|
494
|
-
|
|
495
|
-
|
|
690
|
+
const blob = new Blob([workerCode], {
|
|
691
|
+
type: 'application/javascript'
|
|
692
|
+
});
|
|
693
|
+
return new Worker(URL.createObjectURL(blob));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
createLogicWorker() {
|
|
697
|
+
const workerCode = `
|
|
496
698
|
let state = {};
|
|
497
699
|
|
|
498
700
|
self.onmessage = async function(e) {
|
|
@@ -516,1122 +718,1209 @@ class CanvasFramework {
|
|
|
516
718
|
}
|
|
517
719
|
};
|
|
518
720
|
`;
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
// Protège la boucle
|
|
533
|
-
if (this.components && Array.isArray(this.components)) {
|
|
534
|
-
this.components.forEach(comp => comp.markDirty());
|
|
535
|
-
} else {
|
|
536
|
-
console.warn('[setTheme] components pas encore initialisé');
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Switch Theme
|
|
541
|
-
toggleDarkMode() {
|
|
542
|
-
if (this.theme === lightTheme) {
|
|
543
|
-
this.setTheme(darkTheme);
|
|
544
|
-
} else {
|
|
545
|
-
this.setTheme(lightTheme);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
enableFpsDisplay(enable = true) {
|
|
550
|
-
this.showFps = enable;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// AJOUTER CETTE MÉTHODE (optionnel - pour faciliter l'accès)
|
|
554
|
-
animate(component, options) {
|
|
555
|
-
return this.animator.animate(component, options);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// ----- Worker UI -----
|
|
559
|
-
handleWorkerMessage(e) {
|
|
560
|
-
const { type, payload } = e.data;
|
|
561
|
-
switch(type) {
|
|
562
|
-
case 'LAYOUT_DONE':
|
|
563
|
-
for (let update of payload) {
|
|
564
|
-
const comp = this.components.find(c => c.id === update.id);
|
|
565
|
-
if (comp) comp.height = update.height;
|
|
721
|
+
|
|
722
|
+
const blob = new Blob([workerCode], {
|
|
723
|
+
type: 'application/javascript'
|
|
724
|
+
});
|
|
725
|
+
return new Worker(URL.createObjectURL(blob));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Set Theme dynamique
|
|
729
|
+
setTheme(theme) {
|
|
730
|
+
this.theme = theme;
|
|
731
|
+
|
|
732
|
+
if (!this.useWebGL) {
|
|
733
|
+
this.wrapContext(this.ctx, theme);
|
|
566
734
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
this.
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
updateLogicWorkerState(newState) {
|
|
620
|
-
this.logicWorkerState = { ...this.logicWorkerState, ...newState };
|
|
621
|
-
this.logicWorker.postMessage({ type: 'SET_STATE', payload: this.logicWorkerState });
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
detectPlatform() {
|
|
625
|
-
const ua = navigator.userAgent.toLowerCase();
|
|
626
|
-
if (/android/.test(ua)) return 'material';
|
|
627
|
-
if (/iphone|ipad|ipod/.test(ua)) return 'cupertino';
|
|
628
|
-
return 'material';
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
setupCanvas() {
|
|
632
|
-
this.canvas.width = this.width * this.dpr;
|
|
633
|
-
this.canvas.height = this.height * this.dpr;
|
|
634
|
-
this.canvas.style.width = this.width + 'px';
|
|
635
|
-
this.canvas.style.height = this.height + 'px';
|
|
636
|
-
|
|
637
|
-
// Échelle uniquement pour Canvas 2D
|
|
638
|
-
if (!this.useWebGL) {
|
|
639
|
-
this.ctx.scale(this.dpr, this.dpr);
|
|
640
|
-
} else {
|
|
641
|
-
// WebGL gère le DPR automatiquement via la matrice de projection
|
|
642
|
-
this.ctx.updateProjectionMatrix();
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
setupEventListeners() {
|
|
647
|
-
this.canvas.addEventListener('touchstart', this.handleTouchStart.bind(this));
|
|
648
|
-
this.canvas.addEventListener('touchmove', this.handleTouchMove.bind(this));
|
|
649
|
-
this.canvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
|
|
650
|
-
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
|
651
|
-
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
|
652
|
-
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
|
653
|
-
window.addEventListener('resize', this.handleResize.bind(this));
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Configure l'écoute de l'historique du navigateur
|
|
658
|
-
* @private
|
|
659
|
-
*/
|
|
660
|
-
setupHistoryListener() {
|
|
661
|
-
window.addEventListener('popstate', (e) => {
|
|
662
|
-
if (e.state && e.state.route) {
|
|
663
|
-
this.navigateTo(e.state.route, {
|
|
664
|
-
replace: true,
|
|
665
|
-
animate: true,
|
|
666
|
-
direction: 'back'
|
|
735
|
+
|
|
736
|
+
// Protège la boucle
|
|
737
|
+
if (this.components && Array.isArray(this.components)) {
|
|
738
|
+
this.components.forEach(comp => comp.markDirty());
|
|
739
|
+
} else {
|
|
740
|
+
console.warn('[setTheme] components pas encore initialisé');
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Switch Theme
|
|
745
|
+
toggleDarkMode() {
|
|
746
|
+
if (this.theme === lightTheme) {
|
|
747
|
+
this.setTheme(darkTheme);
|
|
748
|
+
} else {
|
|
749
|
+
this.setTheme(lightTheme);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
enableFpsDisplay(enable = true) {
|
|
754
|
+
this.showFps = enable;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// AJOUTER CETTE MÉTHODE (optionnel - pour faciliter l'accès)
|
|
758
|
+
animate(component, options) {
|
|
759
|
+
return this.animator.animate(component, options);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ----- Worker UI -----
|
|
763
|
+
handleWorkerMessage(e) {
|
|
764
|
+
const {
|
|
765
|
+
type,
|
|
766
|
+
payload
|
|
767
|
+
} = e.data;
|
|
768
|
+
switch (type) {
|
|
769
|
+
case 'LAYOUT_DONE':
|
|
770
|
+
for (let update of payload) {
|
|
771
|
+
const comp = this.components.find(c => c.id === update.id);
|
|
772
|
+
if (comp) comp.height = update.height;
|
|
773
|
+
}
|
|
774
|
+
break;
|
|
775
|
+
case 'SCROLL_UPDATED':
|
|
776
|
+
this.scrollOffset = payload.offset;
|
|
777
|
+
this.scrollVelocity = payload.velocity;
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
updateLayoutAsync() {
|
|
783
|
+
this.worker.postMessage({
|
|
784
|
+
type: 'UPDATE_LAYOUT'
|
|
667
785
|
});
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
*/
|
|
681
|
-
route(pattern, component, options = {}) {
|
|
682
|
-
const route = {
|
|
683
|
-
pattern,
|
|
684
|
-
component,
|
|
685
|
-
regex: this.patternToRegex(pattern),
|
|
686
|
-
paramNames: this.extractParamNames(pattern),
|
|
687
|
-
beforeEnter: options.beforeEnter,
|
|
688
|
-
afterEnter: options.afterEnter,
|
|
689
|
-
beforeLeave: options.beforeLeave,
|
|
690
|
-
afterLeave: options.afterLeave, // ✅ NOUVEAU
|
|
691
|
-
onEnter: options.onEnter, // ✅ NOUVEAU (alias de afterEnter)
|
|
692
|
-
onLeave: options.onLeave, // ✅ NOUVEAU (alias de beforeLeave)
|
|
693
|
-
transition: options.transition || 'slide'
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
this.routes.set(pattern, route);
|
|
697
|
-
return this;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Convertit un pattern de route en regex
|
|
702
|
-
* @private
|
|
703
|
-
*/
|
|
704
|
-
patternToRegex(pattern) {
|
|
705
|
-
const regexPattern = pattern
|
|
706
|
-
.replace(/\//g, '\\/')
|
|
707
|
-
.replace(/:([^\/]+)/g, '([^\\/]+)');
|
|
708
|
-
return new RegExp(`^${regexPattern}$`);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/**
|
|
712
|
-
* Extrait les noms des paramètres d'un pattern
|
|
713
|
-
* @private
|
|
714
|
-
*/
|
|
715
|
-
extractParamNames(pattern) {
|
|
716
|
-
const matches = pattern.match(/:([^\/]+)/g);
|
|
717
|
-
return matches ? matches.map(m => m.slice(1)) : [];
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* Trouve la route correspondant à un path
|
|
722
|
-
* @private
|
|
723
|
-
*/
|
|
724
|
-
matchRoute(path) {
|
|
725
|
-
// Séparer le path et la query string
|
|
726
|
-
const [pathname, queryString] = path.split('?');
|
|
727
|
-
|
|
728
|
-
for (let [pattern, route] of this.routes) {
|
|
729
|
-
const match = pathname.match(route.regex);
|
|
730
|
-
if (match) {
|
|
731
|
-
const params = {};
|
|
732
|
-
route.paramNames.forEach((name, index) => {
|
|
733
|
-
params[name] = match[index + 1];
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
updateScrollInertia() {
|
|
789
|
+
const maxScroll = this.getMaxScroll();
|
|
790
|
+
this.worker.postMessage({
|
|
791
|
+
type: 'SCROLL_INERTIA',
|
|
792
|
+
payload: {
|
|
793
|
+
offset: this.scrollOffset,
|
|
794
|
+
velocity: this.scrollVelocity,
|
|
795
|
+
friction: this.scrollFriction,
|
|
796
|
+
maxScroll
|
|
797
|
+
}
|
|
734
798
|
});
|
|
735
|
-
|
|
736
|
-
const query = this.parseQueryString(queryString);
|
|
737
|
-
|
|
738
|
-
return { route, params, query, pathname };
|
|
739
|
-
}
|
|
740
799
|
}
|
|
741
|
-
return null;
|
|
742
|
-
}
|
|
743
800
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
this.navigateTo(path, options);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* Méthode interne de navigation
|
|
770
|
-
* @private
|
|
771
|
-
*/
|
|
772
|
-
async navigateTo(path, options = {}) {
|
|
773
|
-
const {
|
|
774
|
-
replace = false,
|
|
775
|
-
animate = true,
|
|
776
|
-
direction = 'forward',
|
|
777
|
-
transition = null,
|
|
778
|
-
state = {}
|
|
779
|
-
} = options;
|
|
780
|
-
|
|
781
|
-
const match = this.matchRoute(path);
|
|
782
|
-
if (!match) {
|
|
783
|
-
console.warn(`Route not found: ${path}`);
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const { route, params, query, pathname } = match;
|
|
788
|
-
|
|
789
|
-
// ===== LIFECYCLE: AVANT DE QUITTER L'ANCIENNE ROUTE =====
|
|
790
|
-
|
|
791
|
-
// Hook beforeLeave de la route actuelle (peut bloquer la navigation)
|
|
792
|
-
const currentRouteData = this.routes.get(this.currentRoute);
|
|
793
|
-
if (currentRouteData?.beforeLeave) {
|
|
794
|
-
const canLeave = await currentRouteData.beforeLeave(this.currentParams, this.currentQuery);
|
|
795
|
-
if (canLeave === false) {
|
|
796
|
-
console.log('Navigation cancelled by beforeLeave hook');
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
801
|
+
// ------ Logic Worker --------
|
|
802
|
+
handleLogicWorkerMessage(e) {
|
|
803
|
+
const {
|
|
804
|
+
type,
|
|
805
|
+
payload
|
|
806
|
+
} = e.data;
|
|
807
|
+
switch (type) {
|
|
808
|
+
case 'STATE_UPDATED':
|
|
809
|
+
// Le worker a renvoyé le nouvel état global
|
|
810
|
+
this.logicWorkerState = payload;
|
|
811
|
+
break;
|
|
812
|
+
|
|
813
|
+
case 'EXECUTION_RESULT':
|
|
814
|
+
// Résultat d'une tâche spécifique envoyée au worker
|
|
815
|
+
if (this.onWorkerResult) this.onWorkerResult(payload);
|
|
816
|
+
break;
|
|
817
|
+
|
|
818
|
+
case 'EXECUTION_ERROR':
|
|
819
|
+
console.error('Logic Worker Error:', payload);
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
799
822
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
823
|
+
|
|
824
|
+
runLogicTask(taskName, taskData) {
|
|
825
|
+
this.logicWorker.postMessage({
|
|
826
|
+
type: 'EXECUTE_TASK',
|
|
827
|
+
payload: {
|
|
828
|
+
taskName,
|
|
829
|
+
taskData
|
|
830
|
+
}
|
|
831
|
+
});
|
|
804
832
|
}
|
|
805
833
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
834
|
+
updateLogicWorkerState(newState) {
|
|
835
|
+
this.logicWorkerState = {
|
|
836
|
+
...this.logicWorkerState,
|
|
837
|
+
...newState
|
|
838
|
+
};
|
|
839
|
+
this.logicWorker.postMessage({
|
|
840
|
+
type: 'SET_STATE',
|
|
841
|
+
payload: this.logicWorkerState
|
|
842
|
+
});
|
|
815
843
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
844
|
+
|
|
845
|
+
detectPlatform() {
|
|
846
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
847
|
+
if (/android/.test(ua)) return 'material';
|
|
848
|
+
if (/iphone|ipad|ipod/.test(ua)) return 'cupertino';
|
|
849
|
+
return 'material';
|
|
820
850
|
}
|
|
821
851
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const oldParams = { ...this.currentParams };
|
|
828
|
-
const oldQuery = { ...this.currentQuery };
|
|
852
|
+
setupCanvas() {
|
|
853
|
+
this.canvas.width = this.width * this.dpr;
|
|
854
|
+
this.canvas.height = this.height * this.dpr;
|
|
855
|
+
this.canvas.style.width = this.width + 'px';
|
|
856
|
+
this.canvas.style.height = this.height + 'px';
|
|
829
857
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
858
|
+
// Échelle uniquement pour Canvas 2D
|
|
859
|
+
if (!this.useWebGL) {
|
|
860
|
+
this.ctx.scale(this.dpr, this.dpr);
|
|
861
|
+
} else {
|
|
862
|
+
// WebGL gère le DPR automatiquement via la matrice de projection
|
|
863
|
+
this.ctx.updateProjectionMatrix();
|
|
864
|
+
}
|
|
865
|
+
}
|
|
835
866
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
window.history.pushState(
|
|
845
|
-
{ route: path, params, query, state },
|
|
846
|
-
'',
|
|
847
|
-
path
|
|
848
|
-
);
|
|
849
|
-
} else {
|
|
850
|
-
this.history[this.historyIndex] = { path, params, query, state };
|
|
851
|
-
window.history.replaceState(
|
|
852
|
-
{ route: path, params, query, state },
|
|
853
|
-
'',
|
|
854
|
-
path
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// ===== CRÉER LES NOUVEAUX COMPOSANTS =====
|
|
859
|
-
|
|
860
|
-
this.components = [];
|
|
861
|
-
if (typeof route.component === 'function') {
|
|
862
|
-
route.component(this, params, query);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// ===== LANCER L'ANIMATION DE TRANSITION =====
|
|
866
|
-
|
|
867
|
-
if (animate && !this.transitionState.isTransitioning) {
|
|
868
|
-
const transitionType = transition || route.transition || 'slide';
|
|
869
|
-
this.startTransition(oldComponents, this.components, transitionType, direction);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// ===== LIFECYCLE: APRÈS ÊTRE ENTRÉ DANS LA NOUVELLE ROUTE =====
|
|
873
|
-
|
|
874
|
-
// Hook afterEnter (appelé immédiatement après la création des composants)
|
|
875
|
-
if (route.afterEnter) {
|
|
876
|
-
route.afterEnter(params, query);
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// ✅ NOUVEAU : Hook afterLeave de l'ancienne route (après transition complète)
|
|
880
|
-
if (currentRouteData?.afterLeave) {
|
|
881
|
-
// Si animation, attendre la fin de la transition
|
|
882
|
-
if (animate && this.transitionState.isTransitioning) {
|
|
883
|
-
setTimeout(() => {
|
|
884
|
-
currentRouteData.afterLeave(oldParams, oldQuery);
|
|
885
|
-
}, this.transitionState.duration || 300);
|
|
886
|
-
} else {
|
|
887
|
-
// Pas d'animation, appeler immédiatement
|
|
888
|
-
currentRouteData.afterLeave(oldParams, oldQuery);
|
|
889
|
-
}
|
|
867
|
+
setupEventListeners() {
|
|
868
|
+
this.canvas.addEventListener('touchstart', this.handleTouchStart.bind(this));
|
|
869
|
+
this.canvas.addEventListener('touchmove', this.handleTouchMove.bind(this));
|
|
870
|
+
this.canvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
|
|
871
|
+
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
|
872
|
+
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
|
873
|
+
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
|
874
|
+
window.addEventListener('resize', this.handleResize.bind(this));
|
|
890
875
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Configure l'écoute de l'historique du navigateur
|
|
879
|
+
* @private
|
|
880
|
+
*/
|
|
881
|
+
setupHistoryListener() {
|
|
882
|
+
window.addEventListener('popstate', (e) => {
|
|
883
|
+
if (e.state && e.state.route) {
|
|
884
|
+
this.navigateTo(e.state.route, {
|
|
885
|
+
replace: true,
|
|
886
|
+
animate: true,
|
|
887
|
+
direction: 'back'
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ===== MÉTHODES DE ROUTING =====
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Définit une route avec pattern de paramètres
|
|
897
|
+
* @param {string} pattern - Pattern de la route (ex: '/user/:id', '/posts/:category/:id')
|
|
898
|
+
* @param {Function} component - Fonction qui crée les composants
|
|
899
|
+
* @param {Object} options - Options de la route
|
|
900
|
+
* @returns {CanvasFramework}
|
|
901
|
+
*/
|
|
902
|
+
route(pattern, component, options = {}) {
|
|
903
|
+
const route = {
|
|
904
|
+
pattern,
|
|
905
|
+
component,
|
|
906
|
+
regex: this.patternToRegex(pattern),
|
|
907
|
+
paramNames: this.extractParamNames(pattern),
|
|
908
|
+
beforeEnter: options.beforeEnter,
|
|
909
|
+
afterEnter: options.afterEnter,
|
|
910
|
+
beforeLeave: options.beforeLeave,
|
|
911
|
+
afterLeave: options.afterLeave, // ✅ NOUVEAU
|
|
912
|
+
onEnter: options.onEnter, // ✅ NOUVEAU (alias de afterEnter)
|
|
913
|
+
onLeave: options.onLeave, // ✅ NOUVEAU (alias de beforeLeave)
|
|
914
|
+
transition: options.transition || 'slide'
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
this.routes.set(pattern, route);
|
|
918
|
+
return this;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Convertit un pattern de route en regex
|
|
923
|
+
* @private
|
|
924
|
+
*/
|
|
925
|
+
patternToRegex(pattern) {
|
|
926
|
+
const regexPattern = pattern
|
|
927
|
+
.replace(/\//g, '\\/')
|
|
928
|
+
.replace(/:([^\/]+)/g, '([^\\/]+)');
|
|
929
|
+
return new RegExp(`^${regexPattern}$`);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Extrait les noms des paramètres d'un pattern
|
|
934
|
+
* @private
|
|
935
|
+
*/
|
|
936
|
+
extractParamNames(pattern) {
|
|
937
|
+
const matches = pattern.match(/:([^\/]+)/g);
|
|
938
|
+
return matches ? matches.map(m => m.slice(1)) : [];
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Trouve la route correspondant à un path
|
|
943
|
+
* @private
|
|
944
|
+
*/
|
|
945
|
+
matchRoute(path) {
|
|
946
|
+
// Séparer le path et la query string
|
|
947
|
+
const [pathname, queryString] = path.split('?');
|
|
948
|
+
|
|
949
|
+
for (let [pattern, route] of this.routes) {
|
|
950
|
+
const match = pathname.match(route.regex);
|
|
951
|
+
if (match) {
|
|
952
|
+
const params = {};
|
|
953
|
+
route.paramNames.forEach((name, index) => {
|
|
954
|
+
params[name] = match[index + 1];
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
const query = this.parseQueryString(queryString);
|
|
958
|
+
|
|
959
|
+
return {
|
|
960
|
+
route,
|
|
961
|
+
params,
|
|
962
|
+
query,
|
|
963
|
+
pathname
|
|
964
|
+
};
|
|
965
|
+
}
|
|
954
966
|
}
|
|
955
|
-
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
956
969
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
970
|
+
/**
|
|
971
|
+
* Parse une query string
|
|
972
|
+
* @private
|
|
973
|
+
*/
|
|
974
|
+
parseQueryString(queryString) {
|
|
975
|
+
if (!queryString) return {};
|
|
976
|
+
|
|
977
|
+
const params = {};
|
|
978
|
+
queryString.split('&').forEach(param => {
|
|
979
|
+
const [key, value] = param.split('=');
|
|
980
|
+
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
|
981
|
+
});
|
|
982
|
+
return params;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Navigue vers une route
|
|
987
|
+
* @param {string} path - Chemin de destination (ex: '/user/123', '/posts/tech/456?sort=date')
|
|
988
|
+
* @param {Object} options - Options de navigation
|
|
989
|
+
*/
|
|
990
|
+
navigate(path, options = {}) {
|
|
991
|
+
this.navigateTo(path, options);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Méthode interne de navigation
|
|
996
|
+
* @private
|
|
997
|
+
*/
|
|
998
|
+
async navigateTo(path, options = {}) {
|
|
999
|
+
const {
|
|
1000
|
+
replace = false,
|
|
1001
|
+
animate = true,
|
|
1002
|
+
direction = 'forward',
|
|
1003
|
+
transition = null,
|
|
1004
|
+
state = {}
|
|
1005
|
+
} = options;
|
|
1006
|
+
|
|
1007
|
+
const match = this.matchRoute(path);
|
|
1008
|
+
if (!match) {
|
|
1009
|
+
console.warn(`Route not found: ${path}`);
|
|
1010
|
+
return;
|
|
962
1011
|
}
|
|
963
|
-
this.ctx.restore();
|
|
964
|
-
break;
|
|
965
1012
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1013
|
+
const {
|
|
1014
|
+
route,
|
|
1015
|
+
params,
|
|
1016
|
+
query,
|
|
1017
|
+
pathname
|
|
1018
|
+
} = match;
|
|
1019
|
+
|
|
1020
|
+
// ===== LIFECYCLE: AVANT DE QUITTER L'ANCIENNE ROUTE =====
|
|
1021
|
+
|
|
1022
|
+
// Hook beforeLeave de la route actuelle (peut bloquer la navigation)
|
|
1023
|
+
const currentRouteData = this.routes.get(this.currentRoute);
|
|
1024
|
+
if (currentRouteData?.beforeLeave) {
|
|
1025
|
+
const canLeave = await currentRouteData.beforeLeave(this.currentParams, this.currentQuery);
|
|
1026
|
+
if (canLeave === false) {
|
|
1027
|
+
console.log('Navigation cancelled by beforeLeave hook');
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
972
1030
|
}
|
|
973
|
-
this.ctx.restore();
|
|
974
1031
|
|
|
975
|
-
//
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1032
|
+
// ✅ NOUVEAU : Hook onLeave (alias plus intuitif de beforeLeave, mais ne bloque pas)
|
|
1033
|
+
if (currentRouteData?.onLeave) {
|
|
1034
|
+
await currentRouteData.onLeave(this.currentParams, this.currentQuery);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// ===== LIFECYCLE: AVANT D'ENTRER DANS LA NOUVELLE ROUTE =====
|
|
1038
|
+
|
|
1039
|
+
// Hook beforeEnter de la nouvelle route (peut bloquer la navigation)
|
|
1040
|
+
if (route.beforeEnter) {
|
|
1041
|
+
const canEnter = await route.beforeEnter(params, query);
|
|
1042
|
+
if (canEnter === false) {
|
|
1043
|
+
console.log('Navigation cancelled by beforeEnter hook');
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
980
1046
|
}
|
|
981
|
-
this.ctx.restore();
|
|
982
|
-
break;
|
|
983
1047
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
if (comp.visible) comp.draw(this.ctx);
|
|
1048
|
+
// ✅ NOUVEAU : Hook onEnter (appelé juste avant de créer les composants)
|
|
1049
|
+
if (route.onEnter) {
|
|
1050
|
+
await route.onEnter(params, query);
|
|
988
1051
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const deltaY = Math.abs(pos.y - this.lastTouchY);
|
|
1085
|
-
if (deltaY > 5) {
|
|
1086
|
-
this.isDragging = true;
|
|
1087
|
-
}
|
|
1052
|
+
|
|
1053
|
+
// ===== SAUVEGARDER L'ÉTAT ACTUEL =====
|
|
1054
|
+
|
|
1055
|
+
// Sauvegarder l'ancienne route pour l'animation et les hooks
|
|
1056
|
+
const oldComponents = [...this.components];
|
|
1057
|
+
const oldRoute = this.currentRoute;
|
|
1058
|
+
const oldParams = {
|
|
1059
|
+
...this.currentParams
|
|
1060
|
+
};
|
|
1061
|
+
const oldQuery = {
|
|
1062
|
+
...this.currentQuery
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
// ===== METTRE À JOUR L'ÉTAT =====
|
|
1066
|
+
|
|
1067
|
+
this.currentRoute = pathname;
|
|
1068
|
+
this.currentParams = params;
|
|
1069
|
+
this.currentQuery = query;
|
|
1070
|
+
|
|
1071
|
+
// ===== GÉRER L'HISTORIQUE =====
|
|
1072
|
+
|
|
1073
|
+
if (!replace) {
|
|
1074
|
+
this.historyIndex++;
|
|
1075
|
+
this.history = this.history.slice(0, this.historyIndex);
|
|
1076
|
+
this.history.push({
|
|
1077
|
+
path,
|
|
1078
|
+
params,
|
|
1079
|
+
query,
|
|
1080
|
+
state
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// Mettre à jour l'historique du navigateur
|
|
1084
|
+
window.history.pushState({
|
|
1085
|
+
route: path,
|
|
1086
|
+
params,
|
|
1087
|
+
query,
|
|
1088
|
+
state
|
|
1089
|
+
},
|
|
1090
|
+
'',
|
|
1091
|
+
path
|
|
1092
|
+
);
|
|
1093
|
+
} else {
|
|
1094
|
+
this.history[this.historyIndex] = {
|
|
1095
|
+
path,
|
|
1096
|
+
params,
|
|
1097
|
+
query,
|
|
1098
|
+
state
|
|
1099
|
+
};
|
|
1100
|
+
window.history.replaceState({
|
|
1101
|
+
route: path,
|
|
1102
|
+
params,
|
|
1103
|
+
query,
|
|
1104
|
+
state
|
|
1105
|
+
},
|
|
1106
|
+
'',
|
|
1107
|
+
path
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// ===== CRÉER LES NOUVEAUX COMPOSANTS =====
|
|
1112
|
+
|
|
1113
|
+
this.components = [];
|
|
1114
|
+
if (typeof route.component === 'function') {
|
|
1115
|
+
route.component(this, params, query);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// ===== LANCER L'ANIMATION DE TRANSITION =====
|
|
1119
|
+
|
|
1120
|
+
if (animate && !this.transitionState.isTransitioning) {
|
|
1121
|
+
const transitionType = transition || route.transition || 'slide';
|
|
1122
|
+
this.startTransition(oldComponents, this.components, transitionType, direction);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// ===== LIFECYCLE: APRÈS ÊTRE ENTRÉ DANS LA NOUVELLE ROUTE =====
|
|
1126
|
+
|
|
1127
|
+
// Hook afterEnter (appelé immédiatement après la création des composants)
|
|
1128
|
+
if (route.afterEnter) {
|
|
1129
|
+
route.afterEnter(params, query);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ✅ NOUVEAU : Hook afterLeave de l'ancienne route (après transition complète)
|
|
1133
|
+
if (currentRouteData?.afterLeave) {
|
|
1134
|
+
// Si animation, attendre la fin de la transition
|
|
1135
|
+
if (animate && this.transitionState.isTransitioning) {
|
|
1136
|
+
setTimeout(() => {
|
|
1137
|
+
currentRouteData.afterLeave(oldParams, oldQuery);
|
|
1138
|
+
}, this.transitionState.duration || 300);
|
|
1139
|
+
} else {
|
|
1140
|
+
// Pas d'animation, appeler immédiatement
|
|
1141
|
+
currentRouteData.afterLeave(oldParams, oldQuery);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// ✅ OPTIONNEL : Marquer les composants comme "dirty" pour forcer le rendu
|
|
1146
|
+
this._maxScrollDirty = true;
|
|
1088
1147
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const pos = this.getTouchPos(touch);
|
|
1106
|
-
|
|
1107
|
-
if (!this.isDragging) {
|
|
1108
|
-
this.checkComponentsAtPosition(pos.x, pos.y, 'end');
|
|
1109
|
-
} else {
|
|
1110
|
-
this.isDragging = false;
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
handleMouseDown(e) {
|
|
1115
|
-
this.isDragging = false;
|
|
1116
|
-
this.lastTouchY = e.clientY;
|
|
1117
|
-
this.checkComponentsAtPosition(e.clientX, e.clientY, 'start');
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
handleMouseMove(e) {
|
|
1121
|
-
if (!this.isDragging) {
|
|
1122
|
-
const deltaY = Math.abs(e.clientY - this.lastTouchY);
|
|
1123
|
-
if (deltaY > 5) {
|
|
1124
|
-
this.isDragging = true;
|
|
1125
|
-
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Démarre une animation de transition
|
|
1151
|
+
* @private
|
|
1152
|
+
*/
|
|
1153
|
+
startTransition(oldComponents, newComponents, type, direction) {
|
|
1154
|
+
this.transitionState = {
|
|
1155
|
+
isTransitioning: true,
|
|
1156
|
+
progress: 0,
|
|
1157
|
+
duration: 300,
|
|
1158
|
+
type,
|
|
1159
|
+
direction,
|
|
1160
|
+
oldComponents: [...oldComponents],
|
|
1161
|
+
newComponents: [...newComponents],
|
|
1162
|
+
startTime: Date.now()
|
|
1163
|
+
};
|
|
1126
1164
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
if (child.visible &&
|
|
1175
|
-
cardAdjustedY >= child.y &&
|
|
1176
|
-
cardAdjustedY <= child.y + child.height &&
|
|
1177
|
-
cardAdjustedX >= child.x &&
|
|
1178
|
-
cardAdjustedX <= child.x + child.width) {
|
|
1179
|
-
|
|
1180
|
-
const relativeX = cardAdjustedX - child.x;
|
|
1181
|
-
const relativeY = cardAdjustedY - child.y;
|
|
1182
|
-
|
|
1183
|
-
switch (eventType) {
|
|
1184
|
-
case 'start':
|
|
1185
|
-
child.pressed = true;
|
|
1186
|
-
if (child.onPress) child.onPress?.(relativeX, relativeY);
|
|
1187
|
-
break;
|
|
1188
|
-
|
|
1189
|
-
case 'move':
|
|
1190
|
-
if (!child.hovered) {
|
|
1191
|
-
child.hovered = true;
|
|
1192
|
-
if (child.onHover) child.onHover();
|
|
1193
|
-
}
|
|
1194
|
-
if (child.onMove) child.onMove?.(relativeX, relativeY);
|
|
1195
|
-
break;
|
|
1196
|
-
|
|
1197
|
-
case 'end':
|
|
1198
|
-
if (child.pressed) {
|
|
1199
|
-
child.pressed = false;
|
|
1200
|
-
|
|
1201
|
-
if (child instanceof Input || child instanceof PasswordInput || child instanceof InputTags || child instanceof InputDatalist) {
|
|
1202
|
-
for (let other of this.components) {
|
|
1203
|
-
if (
|
|
1204
|
-
(other instanceof Input ||
|
|
1205
|
-
other instanceof PasswordInput ||
|
|
1206
|
-
other instanceof InputTags ||
|
|
1207
|
-
other instanceof InputDatalist) &&
|
|
1208
|
-
other !== child &&
|
|
1209
|
-
other.focused
|
|
1210
|
-
) {
|
|
1211
|
-
other.focused = false;
|
|
1212
|
-
other.cursorVisible = false;
|
|
1213
|
-
other.onBlur?.();
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
child.focused = true;
|
|
1218
|
-
child.cursorVisible = true;
|
|
1219
|
-
if (child.onFocus) child.onFocus();
|
|
1220
|
-
} else if (child.onClick) {
|
|
1221
|
-
child.onClick();
|
|
1222
|
-
} else if (child.onPress) {
|
|
1223
|
-
child.onPress?.(relativeX, relativeY);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
break;
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Met à jour l'animation de transition
|
|
1168
|
+
* @private
|
|
1169
|
+
*/
|
|
1170
|
+
updateTransition() {
|
|
1171
|
+
if (!this.transitionState.isTransitioning) return;
|
|
1172
|
+
|
|
1173
|
+
const elapsed = Date.now() - this.transitionState.startTime;
|
|
1174
|
+
this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
|
|
1175
|
+
|
|
1176
|
+
// Fonction d'easing (ease-in-out)
|
|
1177
|
+
const eased = this.easeInOutCubic(this.transitionState.progress);
|
|
1178
|
+
|
|
1179
|
+
// Appliquer la transformation selon le type
|
|
1180
|
+
this.ctx.save();
|
|
1181
|
+
this.applyTransitionTransform(eased);
|
|
1182
|
+
this.ctx.restore();
|
|
1183
|
+
|
|
1184
|
+
// Terminer la transition
|
|
1185
|
+
if (this.transitionState.progress >= 1) {
|
|
1186
|
+
this.transitionState.isTransitioning = false;
|
|
1187
|
+
this.transitionState.oldComponents = [];
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Applique la transformation de transition
|
|
1193
|
+
* @private
|
|
1194
|
+
*/
|
|
1195
|
+
applyTransitionTransform(progress) {
|
|
1196
|
+
const {
|
|
1197
|
+
type,
|
|
1198
|
+
direction,
|
|
1199
|
+
oldComponents,
|
|
1200
|
+
newComponents
|
|
1201
|
+
} = this.transitionState;
|
|
1202
|
+
const directionMultiplier = direction === 'forward' ? 1 : -1;
|
|
1203
|
+
|
|
1204
|
+
switch (type) {
|
|
1205
|
+
case 'slide':
|
|
1206
|
+
// Dessiner l'ancienne vue qui sort
|
|
1207
|
+
this.ctx.save();
|
|
1208
|
+
this.ctx.translate(-this.width * progress * directionMultiplier, 0);
|
|
1209
|
+
this.ctx.globalAlpha = 1 - progress * 0.3;
|
|
1210
|
+
for (let comp of oldComponents) {
|
|
1211
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
1227
1212
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1213
|
+
this.ctx.restore();
|
|
1214
|
+
|
|
1215
|
+
// Dessiner la nouvelle vue qui entre
|
|
1216
|
+
this.ctx.save();
|
|
1217
|
+
this.ctx.translate(this.width * (1 - progress) * directionMultiplier, 0);
|
|
1218
|
+
for (let comp of newComponents) {
|
|
1219
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
1220
|
+
}
|
|
1221
|
+
this.ctx.restore();
|
|
1222
|
+
break;
|
|
1223
|
+
|
|
1224
|
+
case 'fade':
|
|
1225
|
+
// Dessiner l'ancienne vue qui fade out
|
|
1226
|
+
this.ctx.save();
|
|
1227
|
+
this.ctx.globalAlpha = 1 - progress;
|
|
1228
|
+
for (let comp of oldComponents) {
|
|
1229
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
1230
|
+
}
|
|
1231
|
+
this.ctx.restore();
|
|
1232
|
+
|
|
1233
|
+
// Dessiner la nouvelle vue qui fade in
|
|
1234
|
+
this.ctx.save();
|
|
1235
|
+
this.ctx.globalAlpha = progress;
|
|
1236
|
+
for (let comp of newComponents) {
|
|
1237
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
1238
|
+
}
|
|
1239
|
+
this.ctx.restore();
|
|
1240
|
+
break;
|
|
1241
|
+
|
|
1242
|
+
case 'none':
|
|
1243
|
+
// Pas d'animation, juste afficher la nouvelle vue
|
|
1244
|
+
for (let comp of newComponents) {
|
|
1245
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
1246
|
+
}
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Fonction d'easing
|
|
1253
|
+
* @private
|
|
1254
|
+
*/
|
|
1255
|
+
easeInOutCubic(t) {
|
|
1256
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Retour en arrière dans l'historique
|
|
1261
|
+
*/
|
|
1262
|
+
goBack() {
|
|
1263
|
+
if (this.historyIndex > 0) {
|
|
1264
|
+
this.historyIndex--;
|
|
1265
|
+
const historyEntry = this.history[this.historyIndex];
|
|
1266
|
+
this.navigateTo(historyEntry.path, {
|
|
1267
|
+
replace: true,
|
|
1268
|
+
animate: true,
|
|
1269
|
+
direction: 'back'
|
|
1270
|
+
});
|
|
1271
|
+
window.history.back();
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Avancer dans l'historique
|
|
1277
|
+
*/
|
|
1278
|
+
goForward() {
|
|
1279
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
1280
|
+
this.historyIndex++;
|
|
1281
|
+
const historyEntry = this.history[this.historyIndex];
|
|
1282
|
+
this.navigateTo(historyEntry.path, {
|
|
1283
|
+
replace: true,
|
|
1284
|
+
animate: true,
|
|
1285
|
+
direction: 'forward'
|
|
1286
|
+
});
|
|
1287
|
+
window.history.forward();
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Obtient les paramètres de la route actuelle
|
|
1293
|
+
* @returns {Object}
|
|
1294
|
+
*/
|
|
1295
|
+
getParams() {
|
|
1296
|
+
return {
|
|
1297
|
+
...this.currentParams
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Obtient la query string de la route actuelle
|
|
1303
|
+
* @returns {Object}
|
|
1304
|
+
*/
|
|
1305
|
+
getQuery() {
|
|
1306
|
+
return {
|
|
1307
|
+
...this.currentQuery
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Obtient un paramètre spécifique
|
|
1313
|
+
* @param {string} name
|
|
1314
|
+
* @returns {string|undefined}
|
|
1315
|
+
*/
|
|
1316
|
+
getParam(name) {
|
|
1317
|
+
return this.currentParams[name];
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Obtient un paramètre de query spécifique
|
|
1322
|
+
* @param {string} name
|
|
1323
|
+
* @returns {string|undefined}
|
|
1324
|
+
*/
|
|
1325
|
+
getQueryParam(name) {
|
|
1326
|
+
return this.currentQuery[name];
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// ===== FIN DES MÉTHODES DE ROUTING =====
|
|
1330
|
+
|
|
1331
|
+
handleTouchStart(e) {
|
|
1332
|
+
e.preventDefault();
|
|
1333
|
+
this.isDragging = false;
|
|
1334
|
+
const touch = e.touches[0];
|
|
1335
|
+
const pos = this.getTouchPos(touch);
|
|
1336
|
+
this.lastTouchY = pos.y;
|
|
1337
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'start');
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
handleTouchMove(e) {
|
|
1341
|
+
e.preventDefault();
|
|
1342
|
+
const touch = e.touches[0];
|
|
1343
|
+
const pos = this.getTouchPos(touch);
|
|
1344
|
+
|
|
1345
|
+
if (!this.isDragging) {
|
|
1346
|
+
const deltaY = Math.abs(pos.y - this.lastTouchY);
|
|
1347
|
+
if (deltaY > 5) {
|
|
1348
|
+
this.isDragging = true;
|
|
1231
1349
|
}
|
|
1232
|
-
}
|
|
1233
1350
|
}
|
|
1234
|
-
|
|
1235
|
-
if (
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
case 'move':
|
|
1243
|
-
if (!comp.hovered) {
|
|
1244
|
-
comp.hovered = true;
|
|
1245
|
-
if (comp.onHover) comp.onHover();
|
|
1246
|
-
}
|
|
1247
|
-
if (comp.onMove) comp.onMove(x, adjustedY);
|
|
1248
|
-
break;
|
|
1249
|
-
|
|
1250
|
-
case 'end':
|
|
1251
|
-
if (comp.pressed) {
|
|
1252
|
-
comp.pressed = false;
|
|
1253
|
-
|
|
1254
|
-
if (comp instanceof Input || comp instanceof PasswordInput || comp instanceof InputTags || comp instanceof InputDatalist) {
|
|
1255
|
-
for (let other of this.components) {
|
|
1256
|
-
if (
|
|
1257
|
-
(other instanceof Input ||
|
|
1258
|
-
other instanceof PasswordInput ||
|
|
1259
|
-
other instanceof InputTags ||
|
|
1260
|
-
other instanceof InputDatalist) &&
|
|
1261
|
-
other !== comp &&
|
|
1262
|
-
other.focused
|
|
1263
|
-
) {
|
|
1264
|
-
other.focused = false;
|
|
1265
|
-
other.cursorVisible = false;
|
|
1266
|
-
other.onBlur?.();
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
comp.focused = true;
|
|
1271
|
-
comp.cursorVisible = true;
|
|
1272
|
-
if (comp.onFocus) comp.onFocus();
|
|
1273
|
-
} else if (comp.onClick) {
|
|
1274
|
-
comp.onClick();
|
|
1275
|
-
} else if (comp.onPress) {
|
|
1276
|
-
comp.onPress(x, adjustedY);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
break;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
return;
|
|
1351
|
+
|
|
1352
|
+
if (this.isDragging) {
|
|
1353
|
+
const deltaY = pos.y - this.lastTouchY;
|
|
1354
|
+
this.scrollOffset += deltaY;
|
|
1355
|
+
const maxScroll = this.getMaxScroll();
|
|
1356
|
+
this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -maxScroll);
|
|
1357
|
+
this.scrollVelocity = deltaY;
|
|
1358
|
+
this.lastTouchY = pos.y;
|
|
1283
1359
|
} else {
|
|
1284
|
-
|
|
1360
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'move');
|
|
1285
1361
|
}
|
|
1286
|
-
}
|
|
1287
1362
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
this._cachedMaxScroll = Math.max(0, maxY - this.height + 50);
|
|
1301
|
-
this._maxScrollDirty = false;
|
|
1302
|
-
return this._cachedMaxScroll;
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
/*getMaxScroll() {
|
|
1306
|
-
let maxY = 0;
|
|
1307
|
-
for (let comp of this.components) {
|
|
1308
|
-
if (!this.isFixedComponent(comp)) {
|
|
1309
|
-
maxY = Math.max(maxY, comp.y + comp.height);
|
|
1310
|
-
}
|
|
1363
|
+
|
|
1364
|
+
handleTouchEnd(e) {
|
|
1365
|
+
e.preventDefault();
|
|
1366
|
+
const touch = e.changedTouches[0];
|
|
1367
|
+
const pos = this.getTouchPos(touch);
|
|
1368
|
+
|
|
1369
|
+
if (!this.isDragging) {
|
|
1370
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'end');
|
|
1371
|
+
} else {
|
|
1372
|
+
this.isDragging = false;
|
|
1373
|
+
}
|
|
1311
1374
|
}
|
|
1312
|
-
return Math.max(0, maxY - this.height + 50);
|
|
1313
|
-
}*/
|
|
1314
1375
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
this.
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
}
|
|
1376
|
+
handleMouseDown(e) {
|
|
1377
|
+
this.isDragging = false;
|
|
1378
|
+
this.lastTouchY = e.clientY;
|
|
1379
|
+
this.checkComponentsAtPosition(e.clientX, e.clientY, 'start');
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
handleMouseMove(e) {
|
|
1383
|
+
if (!this.isDragging) {
|
|
1384
|
+
const deltaY = Math.abs(e.clientY - this.lastTouchY);
|
|
1385
|
+
if (deltaY > 5) {
|
|
1386
|
+
this.isDragging = true;
|
|
1387
|
+
}
|
|
1328
1388
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
remove(component) {
|
|
1342
|
-
const index = this.components.indexOf(component);
|
|
1343
|
-
if (index > -1) {
|
|
1344
|
-
component._unmount();
|
|
1345
|
-
this.components.splice(index, 1);
|
|
1346
|
-
this._maxScrollDirty = true; // ✅ AJOUTER CETTE LIGNE
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
markComponentDirty(component) {
|
|
1351
|
-
if (this.optimizationEnabled) {
|
|
1352
|
-
this.dirtyComponents.add(component);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
enableOptimization() {
|
|
1357
|
-
this.optimizationEnabled = true;
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
/**
|
|
1361
|
-
* Dessine un petit triangle rouge pour indiquer overflow (style Flutter)
|
|
1362
|
-
*/
|
|
1363
|
-
drawOverflowIndicators() {
|
|
1364
|
-
const ctx = this.ctx;
|
|
1365
|
-
|
|
1366
|
-
// Pour chaque composant
|
|
1367
|
-
for (let comp of this.components) {
|
|
1368
|
-
if (!comp.visible) continue;
|
|
1369
|
-
|
|
1370
|
-
// Position réelle à l'écran
|
|
1371
|
-
const isFixed = this.isFixedComponent(comp);
|
|
1372
|
-
const screenY = isFixed ? comp.y : comp.y + this.scrollOffset;
|
|
1373
|
-
const screenX = comp.x;
|
|
1374
|
-
|
|
1375
|
-
// Vérifier si le composant TEXT a une largeur/hauteur incorrecte
|
|
1376
|
-
let actualWidth = comp.width;
|
|
1377
|
-
let actualHeight = comp.height;
|
|
1378
|
-
|
|
1379
|
-
// Si c'est un Text, vérifier la taille réelle du texte
|
|
1380
|
-
if (comp instanceof Text && comp.text && ctx.measureText) {
|
|
1381
|
-
try {
|
|
1382
|
-
// Sauvegarder le style actuel
|
|
1383
|
-
ctx.save();
|
|
1384
|
-
|
|
1385
|
-
// Appliquer le style du texte
|
|
1386
|
-
if (comp.fontSize) {
|
|
1387
|
-
ctx.font = `${comp.fontSize}px ${comp.fontFamily || 'Arial'}`;
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// Mesurer la taille réelle
|
|
1391
|
-
const metrics = ctx.measureText(comp.text);
|
|
1392
|
-
actualWidth = metrics.width + (comp.padding || 0) * 2;
|
|
1393
|
-
actualHeight = (comp.fontSize || 16) + (comp.padding || 0) * 2;
|
|
1394
|
-
|
|
1395
|
-
ctx.restore();
|
|
1396
|
-
} catch (e) {
|
|
1397
|
-
// En cas d'erreur, garder les dimensions par défaut
|
|
1389
|
+
|
|
1390
|
+
if (this.isDragging) {
|
|
1391
|
+
const deltaY = e.clientY - this.lastTouchY;
|
|
1392
|
+
this.scrollOffset += deltaY;
|
|
1393
|
+
const maxScroll = this.getMaxScroll();
|
|
1394
|
+
this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -maxScroll);
|
|
1395
|
+
this.scrollVelocity = deltaY;
|
|
1396
|
+
this.lastTouchY = e.clientY;
|
|
1397
|
+
} else {
|
|
1398
|
+
this.checkComponentsAtPosition(e.clientX, e.clientY, 'move');
|
|
1398
1399
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
handleMouseUp(e) {
|
|
1403
|
+
if (!this.isDragging) {
|
|
1404
|
+
this.checkComponentsAtPosition(e.clientX, e.clientY, 'end');
|
|
1405
|
+
} else {
|
|
1406
|
+
this.isDragging = false;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
getTouchPos(touch) {
|
|
1411
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
1412
|
+
return {
|
|
1413
|
+
x: touch.clientX - rect.left,
|
|
1414
|
+
y: touch.clientY - rect.top
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
checkComponentsAtPosition(x, y, eventType) {
|
|
1419
|
+
const isFixedComponent = (comp) =>
|
|
1420
|
+
FIXED_COMPONENT_TYPES.has(comp.constructor);
|
|
1421
|
+
|
|
1422
|
+
for (let i = this.components.length - 1; i >= 0; i--) {
|
|
1423
|
+
const comp = this.components[i];
|
|
1424
|
+
|
|
1425
|
+
if (comp.visible) {
|
|
1426
|
+
const adjustedY = isFixedComponent(comp) ? y : y - this.scrollOffset;
|
|
1427
|
+
|
|
1428
|
+
if (comp instanceof Card && comp.clickableChildren && comp.children && comp.children.length > 0) {
|
|
1429
|
+
if (comp.isPointInside(x, adjustedY)) {
|
|
1430
|
+
const cardAdjustedY = adjustedY - comp.y - comp.padding;
|
|
1431
|
+
const cardAdjustedX = x - comp.x - comp.padding;
|
|
1432
|
+
|
|
1433
|
+
for (let j = comp.children.length - 1; j >= 0; j--) {
|
|
1434
|
+
const child = comp.children[j];
|
|
1435
|
+
|
|
1436
|
+
if (child.visible &&
|
|
1437
|
+
cardAdjustedY >= child.y &&
|
|
1438
|
+
cardAdjustedY <= child.y + child.height &&
|
|
1439
|
+
cardAdjustedX >= child.x &&
|
|
1440
|
+
cardAdjustedX <= child.x + child.width) {
|
|
1441
|
+
|
|
1442
|
+
const relativeX = cardAdjustedX - child.x;
|
|
1443
|
+
const relativeY = cardAdjustedY - child.y;
|
|
1444
|
+
|
|
1445
|
+
switch (eventType) {
|
|
1446
|
+
case 'start':
|
|
1447
|
+
child.pressed = true;
|
|
1448
|
+
if (child.onPress) child.onPress?.(relativeX, relativeY);
|
|
1449
|
+
break;
|
|
1450
|
+
|
|
1451
|
+
case 'move':
|
|
1452
|
+
if (!child.hovered) {
|
|
1453
|
+
child.hovered = true;
|
|
1454
|
+
if (child.onHover) child.onHover();
|
|
1455
|
+
}
|
|
1456
|
+
if (child.onMove) child.onMove?.(relativeX, relativeY);
|
|
1457
|
+
break;
|
|
1458
|
+
|
|
1459
|
+
case 'end':
|
|
1460
|
+
if (child.pressed) {
|
|
1461
|
+
child.pressed = false;
|
|
1462
|
+
|
|
1463
|
+
if (child instanceof Input || child instanceof PasswordInput || child instanceof InputTags || child instanceof InputDatalist) {
|
|
1464
|
+
for (let other of this.components) {
|
|
1465
|
+
if (
|
|
1466
|
+
(other instanceof Input ||
|
|
1467
|
+
other instanceof PasswordInput ||
|
|
1468
|
+
other instanceof InputTags ||
|
|
1469
|
+
other instanceof InputDatalist) &&
|
|
1470
|
+
other !== child &&
|
|
1471
|
+
other.focused
|
|
1472
|
+
) {
|
|
1473
|
+
other.focused = false;
|
|
1474
|
+
other.cursorVisible = false;
|
|
1475
|
+
other.onBlur?.();
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
child.focused = true;
|
|
1480
|
+
child.cursorVisible = true;
|
|
1481
|
+
if (child.onFocus) child.onFocus();
|
|
1482
|
+
} else if (child.onClick) {
|
|
1483
|
+
child.onClick();
|
|
1484
|
+
} else if (child.onPress) {
|
|
1485
|
+
child.onPress?.(relativeX, relativeY);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
if (comp.isPointInside(x, adjustedY)) {
|
|
1498
|
+
switch (eventType) {
|
|
1499
|
+
case 'start':
|
|
1500
|
+
comp.pressed = true;
|
|
1501
|
+
if (comp.onPress) comp.onPress(x, adjustedY);
|
|
1502
|
+
break;
|
|
1503
|
+
|
|
1504
|
+
case 'move':
|
|
1505
|
+
if (!comp.hovered) {
|
|
1506
|
+
comp.hovered = true;
|
|
1507
|
+
if (comp.onHover) comp.onHover();
|
|
1508
|
+
}
|
|
1509
|
+
if (comp.onMove) comp.onMove(x, adjustedY);
|
|
1510
|
+
break;
|
|
1511
|
+
|
|
1512
|
+
case 'end':
|
|
1513
|
+
if (comp.pressed) {
|
|
1514
|
+
comp.pressed = false;
|
|
1515
|
+
|
|
1516
|
+
if (comp instanceof Input || comp instanceof PasswordInput || comp instanceof InputTags || comp instanceof InputDatalist) {
|
|
1517
|
+
for (let other of this.components) {
|
|
1518
|
+
if (
|
|
1519
|
+
(other instanceof Input ||
|
|
1520
|
+
other instanceof PasswordInput ||
|
|
1521
|
+
other instanceof InputTags ||
|
|
1522
|
+
other instanceof InputDatalist) &&
|
|
1523
|
+
other !== comp &&
|
|
1524
|
+
other.focused
|
|
1525
|
+
) {
|
|
1526
|
+
other.focused = false;
|
|
1527
|
+
other.cursorVisible = false;
|
|
1528
|
+
other.onBlur?.();
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
comp.focused = true;
|
|
1533
|
+
comp.cursorVisible = true;
|
|
1534
|
+
if (comp.onFocus) comp.onFocus();
|
|
1535
|
+
} else if (comp.onClick) {
|
|
1536
|
+
comp.onClick();
|
|
1537
|
+
} else if (comp.onPress) {
|
|
1538
|
+
comp.onPress(x, adjustedY);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
return;
|
|
1545
|
+
} else {
|
|
1546
|
+
comp.hovered = false;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
getMaxScroll() {
|
|
1553
|
+
if (!this._maxScrollDirty) return this._cachedMaxScroll;
|
|
1554
|
+
|
|
1555
|
+
let maxY = 0;
|
|
1556
|
+
for (const comp of this.components) {
|
|
1557
|
+
if (this.isFixedComponent(comp) || !comp.visible) continue;
|
|
1558
|
+
const bottom = comp.y + comp.height;
|
|
1559
|
+
if (bottom > maxY) maxY = bottom;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
this._cachedMaxScroll = Math.max(0, maxY - this.height + 50);
|
|
1563
|
+
this._maxScrollDirty = false;
|
|
1564
|
+
return this._cachedMaxScroll;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/*getMaxScroll() {
|
|
1568
|
+
let maxY = 0;
|
|
1569
|
+
for (let comp of this.components) {
|
|
1570
|
+
if (!this.isFixedComponent(comp)) {
|
|
1571
|
+
maxY = Math.max(maxY, comp.y + comp.height);
|
|
1513
1572
|
}
|
|
1514
1573
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
startRenderLoop() {
|
|
1521
|
-
const render = () => {
|
|
1522
|
-
// 1️⃣ Scroll inertia
|
|
1523
|
-
if (Math.abs(this.scrollVelocity) > 0.1 && !this.isDragging) {
|
|
1524
|
-
this.scrollOffset += this.scrollVelocity;
|
|
1525
|
-
this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -this.getMaxScroll());
|
|
1526
|
-
this.scrollVelocity *= this.scrollFriction;
|
|
1527
|
-
} else {
|
|
1528
|
-
this.scrollVelocity = 0;
|
|
1529
|
-
}
|
|
1574
|
+
return Math.max(0, maxY - this.height + 50);
|
|
1575
|
+
}*/
|
|
1530
1576
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1577
|
+
handleResize() {
|
|
1578
|
+
if (this.resizeTimeout) clearTimeout(this.resizeTimeout); // ✅ AJOUTER
|
|
1533
1579
|
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
for (let comp of this.dirtyComponents) {
|
|
1540
|
-
if (comp.visible) {
|
|
1541
|
-
const isFixed = this.isFixedComponent(comp);
|
|
1542
|
-
const y = isFixed ? comp.y : comp.y + this.scrollOffset;
|
|
1580
|
+
this.resizeTimeout = setTimeout(() => { // ✅ AJOUTER
|
|
1581
|
+
if (!this.useWebGL) {
|
|
1582
|
+
this.width = window.innerWidth;
|
|
1583
|
+
this.height = window.innerHeight;
|
|
1584
|
+
this.setupCanvas();
|
|
1543
1585
|
|
|
1544
|
-
|
|
1586
|
+
for (const comp of this.components) {
|
|
1587
|
+
if (comp._resize) {
|
|
1588
|
+
comp._resize(this.width, this.height);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
this._maxScrollDirty = true; // ✅ AJOUTER
|
|
1592
|
+
}
|
|
1593
|
+
}, 150); // ✅ AJOUTER (throttle 150ms)
|
|
1594
|
+
}
|
|
1545
1595
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1596
|
+
add(component) {
|
|
1597
|
+
this.components.push(component);
|
|
1598
|
+
component._mount();
|
|
1599
|
+
this._maxScrollDirty = true; // ✅ AJOUTER CETTE LIGNE
|
|
1600
|
+
return component;
|
|
1601
|
+
}
|
|
1550
1602
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1603
|
+
remove(component) {
|
|
1604
|
+
const index = this.components.indexOf(component);
|
|
1605
|
+
if (index > -1) {
|
|
1606
|
+
component._unmount();
|
|
1607
|
+
this.components.splice(index, 1);
|
|
1608
|
+
this._maxScrollDirty = true; // ✅ AJOUTER CETTE LIGNE
|
|
1555
1609
|
}
|
|
1556
|
-
|
|
1557
|
-
} else {
|
|
1558
|
-
// Full redraw
|
|
1559
|
-
const scrollableComponents = [];
|
|
1560
|
-
const fixedComponents = [];
|
|
1610
|
+
}
|
|
1561
1611
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1612
|
+
markComponentDirty(component) {
|
|
1613
|
+
if (this.optimizationEnabled) {
|
|
1614
|
+
this.dirtyComponents.add(component);
|
|
1565
1615
|
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
enableOptimization() {
|
|
1619
|
+
this.optimizationEnabled = true;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* Dessine un petit triangle rouge pour indiquer overflow (style Flutter)
|
|
1624
|
+
*/
|
|
1625
|
+
drawOverflowIndicators() {
|
|
1626
|
+
const ctx = this.ctx;
|
|
1627
|
+
|
|
1628
|
+
// Pour chaque composant
|
|
1629
|
+
for (let comp of this.components) {
|
|
1630
|
+
if (!comp.visible) continue;
|
|
1631
|
+
|
|
1632
|
+
// Position réelle à l'écran
|
|
1633
|
+
const isFixed = this.isFixedComponent(comp);
|
|
1634
|
+
const screenY = isFixed ? comp.y : comp.y + this.scrollOffset;
|
|
1635
|
+
const screenX = comp.x;
|
|
1636
|
+
|
|
1637
|
+
// Vérifier si le composant TEXT a une largeur/hauteur incorrecte
|
|
1638
|
+
let actualWidth = comp.width;
|
|
1639
|
+
let actualHeight = comp.height;
|
|
1640
|
+
|
|
1641
|
+
// Si c'est un Text, vérifier la taille réelle du texte
|
|
1642
|
+
if (comp instanceof Text && comp.text && ctx.measureText) {
|
|
1643
|
+
try {
|
|
1644
|
+
// Sauvegarder le style actuel
|
|
1645
|
+
ctx.save();
|
|
1646
|
+
|
|
1647
|
+
// Appliquer le style du texte
|
|
1648
|
+
if (comp.fontSize) {
|
|
1649
|
+
ctx.font = `${comp.fontSize}px ${comp.fontFamily || 'Arial'}`;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Mesurer la taille réelle
|
|
1653
|
+
const metrics = ctx.measureText(comp.text);
|
|
1654
|
+
actualWidth = metrics.width + (comp.padding || 0) * 2;
|
|
1655
|
+
actualHeight = (comp.fontSize || 16) + (comp.padding || 0) * 2;
|
|
1656
|
+
|
|
1657
|
+
ctx.restore();
|
|
1658
|
+
} catch (e) {
|
|
1659
|
+
// En cas d'erreur, garder les dimensions par défaut
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Calculer les limites RÉELLES du composant
|
|
1664
|
+
const compLeft = screenX;
|
|
1665
|
+
const compRight = screenX + actualWidth;
|
|
1666
|
+
const compTop = screenY;
|
|
1667
|
+
const compBottom = screenY + actualHeight;
|
|
1668
|
+
|
|
1669
|
+
// Vérifier les débordements avec les dimensions RÉELLES
|
|
1670
|
+
const overflow = {
|
|
1671
|
+
left: compLeft < 0,
|
|
1672
|
+
right: compRight > this.width,
|
|
1673
|
+
top: compTop < 0,
|
|
1674
|
+
bottom: compBottom > this.height
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
// Si aucun débordement, passer au suivant
|
|
1678
|
+
if (!overflow.left && !overflow.right && !overflow.top && !overflow.bottom) {
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// DEBUG: Afficher les infos du composant
|
|
1683
|
+
if (this.debbug) {
|
|
1684
|
+
console.table({
|
|
1685
|
+
type: comp.constructor?.name,
|
|
1686
|
+
x: comp.x,
|
|
1687
|
+
y: comp.y,
|
|
1688
|
+
declaredSize: `${comp.width}x${comp.height}`,
|
|
1689
|
+
actualSize: `${actualWidth}x${actualHeight}`,
|
|
1690
|
+
screenPos: `(${screenX}, ${screenY})`,
|
|
1691
|
+
overflow
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// Dessiner les indicateurs
|
|
1696
|
+
ctx.save();
|
|
1697
|
+
|
|
1698
|
+
// 1. Bordures rouges sur les parties qui débordent
|
|
1699
|
+
ctx.strokeStyle = 'red';
|
|
1700
|
+
ctx.lineWidth = 2;
|
|
1701
|
+
ctx.fillStyle = 'rgba(255, 0, 0, 0.2)';
|
|
1702
|
+
|
|
1703
|
+
// Gauche
|
|
1704
|
+
if (overflow.left) {
|
|
1705
|
+
const overflowWidth = Math.min(actualWidth, -compLeft);
|
|
1706
|
+
ctx.fillRect(compLeft, compTop, overflowWidth, actualHeight);
|
|
1707
|
+
ctx.strokeRect(compLeft, compTop, overflowWidth, actualHeight);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// Droite
|
|
1711
|
+
if (overflow.right) {
|
|
1712
|
+
const overflowStart = Math.max(0, this.width - compLeft);
|
|
1713
|
+
const overflowWidth = Math.min(actualWidth, compRight - this.width);
|
|
1714
|
+
ctx.fillRect(this.width - overflowWidth, compTop, overflowWidth, actualHeight);
|
|
1715
|
+
ctx.strokeRect(this.width - overflowWidth, compTop, overflowWidth, actualHeight);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Haut
|
|
1719
|
+
if (overflow.top) {
|
|
1720
|
+
const overflowHeight = Math.min(actualHeight, -compTop);
|
|
1721
|
+
ctx.fillRect(compLeft, compTop, actualWidth, overflowHeight);
|
|
1722
|
+
ctx.strokeRect(compLeft, compTop, actualWidth, overflowHeight);
|
|
1723
|
+
}
|
|
1566
1724
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1725
|
+
// Bas
|
|
1726
|
+
if (overflow.bottom) {
|
|
1727
|
+
const overflowStart = Math.max(0, this.height - compTop);
|
|
1728
|
+
const overflowHeight = Math.min(actualHeight, compBottom - this.height);
|
|
1729
|
+
ctx.fillRect(compLeft, this.height - overflowHeight, actualWidth, overflowHeight);
|
|
1730
|
+
ctx.strokeRect(compLeft, this.height - overflowHeight, actualWidth, overflowHeight);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// 2. Points rouges aux coins
|
|
1734
|
+
ctx.fillStyle = 'red';
|
|
1735
|
+
const markerSize = 6;
|
|
1736
|
+
|
|
1737
|
+
// Coin supérieur gauche
|
|
1738
|
+
if (overflow.left || overflow.top) {
|
|
1739
|
+
ctx.fillRect(compLeft, compTop, markerSize, markerSize);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// Coin supérieur droit
|
|
1743
|
+
if (overflow.right || overflow.top) {
|
|
1744
|
+
ctx.fillRect(compRight - markerSize, compTop, markerSize, markerSize);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Coin inférieur gauche
|
|
1748
|
+
if (overflow.left || overflow.bottom) {
|
|
1749
|
+
ctx.fillRect(compLeft, compBottom - markerSize, markerSize, markerSize);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Coin inférieur droit
|
|
1753
|
+
if (overflow.right || overflow.bottom) {
|
|
1754
|
+
ctx.fillRect(compRight - markerSize, compBottom - markerSize, markerSize, markerSize);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// 3. Texte d'information (optionnel)
|
|
1758
|
+
if (this.debbug && comp.text) {
|
|
1759
|
+
ctx.fillStyle = 'red';
|
|
1760
|
+
ctx.font = '10px monospace';
|
|
1761
|
+
ctx.textAlign = 'left';
|
|
1762
|
+
|
|
1763
|
+
const overflowText = [];
|
|
1764
|
+
if (overflow.left) overflowText.push('←');
|
|
1765
|
+
if (overflow.right) overflowText.push('→');
|
|
1766
|
+
if (overflow.top) overflowText.push('↑');
|
|
1767
|
+
if (overflow.bottom) overflowText.push('↓');
|
|
1768
|
+
|
|
1769
|
+
if (overflowText.length > 0) {
|
|
1770
|
+
ctx.fillText(
|
|
1771
|
+
`"${comp.text.substring(0, 10)}${comp.text.length > 10 ? '...' : ''}" ${overflowText.join('')}`,
|
|
1772
|
+
compLeft + 5,
|
|
1773
|
+
compTop - 5
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
ctx.restore();
|
|
1588
1779
|
}
|
|
1589
|
-
|
|
1780
|
+
}
|
|
1590
1781
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1782
|
+
startRenderLoop() {
|
|
1783
|
+
const render = () => {
|
|
1784
|
+
if (!this._splashFinished) {
|
|
1785
|
+
requestAnimationFrame(render);
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
// 1️⃣ Scroll inertia
|
|
1789
|
+
if (Math.abs(this.scrollVelocity) > 0.1 && !this.isDragging) {
|
|
1790
|
+
this.scrollOffset += this.scrollVelocity;
|
|
1791
|
+
this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -this.getMaxScroll());
|
|
1792
|
+
this.scrollVelocity *= this.scrollFriction;
|
|
1793
|
+
} else {
|
|
1794
|
+
this.scrollVelocity = 0;
|
|
1795
|
+
}
|
|
1599
1796
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1797
|
+
// 2️⃣ Clear canvas
|
|
1798
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
1799
|
+
|
|
1800
|
+
// 3️⃣ Transition handling
|
|
1801
|
+
if (this.transitionState.isTransitioning) {
|
|
1802
|
+
this.updateTransition();
|
|
1803
|
+
} else if (this.optimizationEnabled && this.dirtyComponents.size > 0) {
|
|
1804
|
+
// Dirty components redraw
|
|
1805
|
+
for (let comp of this.dirtyComponents) {
|
|
1806
|
+
if (comp.visible) {
|
|
1807
|
+
const isFixed = this.isFixedComponent(comp);
|
|
1808
|
+
const y = isFixed ? comp.y : comp.y + this.scrollOffset;
|
|
1809
|
+
|
|
1810
|
+
this.ctx.clearRect(comp.x - 2, y - 2, comp.width + 4, comp.height + 4);
|
|
1811
|
+
|
|
1812
|
+
this.ctx.save();
|
|
1813
|
+
if (!isFixed) this.ctx.translate(0, this.scrollOffset);
|
|
1814
|
+
comp.draw(this.ctx);
|
|
1815
|
+
this.ctx.restore();
|
|
1816
|
+
|
|
1817
|
+
// Overflow indicator style Flutter
|
|
1818
|
+
const overflow = comp.getOverflow?.();
|
|
1819
|
+
if (comp.markClean) comp.markClean();
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
this.dirtyComponents.clear();
|
|
1823
|
+
} else {
|
|
1824
|
+
// Full redraw
|
|
1825
|
+
const scrollableComponents = [];
|
|
1826
|
+
const fixedComponents = [];
|
|
1827
|
+
|
|
1828
|
+
for (let comp of this.components) {
|
|
1829
|
+
if (this.isFixedComponent(comp)) fixedComponents.push(comp);
|
|
1830
|
+
else scrollableComponents.push(comp);
|
|
1831
|
+
}
|
|
1633
1832
|
|
|
1634
|
-
|
|
1833
|
+
// Scrollable
|
|
1834
|
+
this.ctx.save();
|
|
1835
|
+
this.ctx.translate(0, this.scrollOffset);
|
|
1836
|
+
for (let comp of scrollableComponents) {
|
|
1837
|
+
if (comp.visible) {
|
|
1838
|
+
// ✅ Viewport culling : ne dessiner que ce qui est visible
|
|
1839
|
+
const screenY = comp.y + this.scrollOffset;
|
|
1840
|
+
const isInViewport = screenY + comp.height >= -100 && screenY <= this.height + 100;
|
|
1841
|
+
|
|
1842
|
+
if (isInViewport) {
|
|
1843
|
+
comp.draw(this.ctx);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
this.ctx.restore();
|
|
1848
|
+
|
|
1849
|
+
// Fixed
|
|
1850
|
+
for (let comp of fixedComponents) {
|
|
1851
|
+
if (comp.visible) {
|
|
1852
|
+
comp.draw(this.ctx);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// 4️⃣ FPS
|
|
1858
|
+
this._frames++;
|
|
1859
|
+
const now = performance.now();
|
|
1860
|
+
if (now - this._lastFpsTime >= 1000) {
|
|
1861
|
+
this.fps = this._frames;
|
|
1862
|
+
this._frames = 0;
|
|
1863
|
+
this._lastFpsTime = now;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
if (this.showFps) {
|
|
1867
|
+
this.ctx.save();
|
|
1868
|
+
this.ctx.fillStyle = 'lime';
|
|
1869
|
+
this.ctx.font = '16px monospace';
|
|
1870
|
+
this.ctx.fillText(`FPS: ${this.fps}`, 10, 20);
|
|
1871
|
+
this.ctx.restore();
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
if (this.debbug) {
|
|
1875
|
+
this.drawOverflowIndicators();
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// ✅ AJOUTER: Marquer le premier rendu
|
|
1879
|
+
if (!this._firstRenderDone && this.components.length > 0) {
|
|
1880
|
+
this._markFirstRender();
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
requestAnimationFrame(render);
|
|
1884
|
+
};
|
|
1885
|
+
|
|
1886
|
+
render();
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// ✅ AJOUTER: Afficher les métriques à l'écran
|
|
1890
|
+
displayMetrics() {
|
|
1891
|
+
const metrics = this.metrics;
|
|
1892
|
+
|
|
1893
|
+
console.table({
|
|
1894
|
+
'⚙️ Initialisation Framework': `${metrics.initTime.toFixed(2)}ms`,
|
|
1895
|
+
'🎨 Premier Rendu': `${metrics.firstRenderTime.toFixed(2)}ms`,
|
|
1896
|
+
'🚀 Temps Total Startup': `${metrics.totalStartupTime.toFixed(2)}ms`,
|
|
1897
|
+
'📊 FPS Actuel': this.fps
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// ✅ AJOUTER: Obtenir les métriques
|
|
1902
|
+
getMetrics() {
|
|
1903
|
+
return {
|
|
1904
|
+
...this.metrics,
|
|
1905
|
+
currentFPS: this.fps,
|
|
1906
|
+
componentsCount: this.components.length
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1635
1909
|
|
|
1910
|
+
isFixedComponent(comp) {
|
|
1911
|
+
return FIXED_COMPONENT_TYPES.has(comp.constructor);
|
|
1912
|
+
}
|
|
1636
1913
|
|
|
1914
|
+
showToast(message, duration = 3000) {
|
|
1915
|
+
const toast = new Toast(this, {
|
|
1916
|
+
text: message,
|
|
1917
|
+
duration: duration,
|
|
1918
|
+
x: this.width / 2,
|
|
1919
|
+
y: this.height - 100
|
|
1920
|
+
});
|
|
1921
|
+
this.add(toast);
|
|
1922
|
+
toast.show();
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1637
1925
|
|
|
1926
|
+
export default CanvasFramework;
|