canvasframework 0.4.7 → 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 +1681 -1367
- package/index.js +1 -16
- package/package.json +1 -1
package/core/CanvasFramework.js
CHANGED
|
@@ -103,27 +103,29 @@ import FeatureFlags from '../manager/FeatureFlags.js';
|
|
|
103
103
|
|
|
104
104
|
// WebGL Adapter
|
|
105
105
|
import WebGLCanvasAdapter from './WebGLCanvasAdapter.js';
|
|
106
|
-
import ui, {
|
|
106
|
+
import ui, {
|
|
107
|
+
createRef
|
|
108
|
+
} from './UIBuilder.js';
|
|
107
109
|
import ThemeManager from './ThemeManager.js';
|
|
108
110
|
|
|
109
111
|
|
|
110
112
|
const FIXED_COMPONENT_TYPES = new Set([
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
127
129
|
]);
|
|
128
130
|
|
|
129
131
|
/**
|
|
@@ -146,290 +148,513 @@ const FIXED_COMPONENT_TYPES = new Set([
|
|
|
146
148
|
* @property {number} scrollFriction - Friction du défilement
|
|
147
149
|
*/
|
|
148
150
|
class CanvasFramework {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.height = window.innerHeight;
|
|
158
|
-
this.dpr = window.devicePixelRatio || 1;
|
|
159
|
-
|
|
160
|
-
this.platform = this.detectPlatform();
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// État actuel + préférence
|
|
164
|
-
this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
|
|
165
|
-
this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
|
|
166
|
-
|
|
167
|
-
// Applique le thème initial
|
|
168
|
-
this.setupSystemThemeListener();
|
|
169
|
-
|
|
170
|
-
// Récupère override utilisateur
|
|
171
|
-
const savedOverride = localStorage.getItem('themeOverride');
|
|
172
|
-
if (savedOverride && ['light', 'dark'].includes(savedOverride)) {
|
|
173
|
-
this.userThemeOverride = savedOverride;
|
|
174
|
-
this.themeMode = savedOverride;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
this.components = [];
|
|
178
|
-
// ✅ AJOUTER ICI :
|
|
179
|
-
this._cachedMaxScroll = 0;
|
|
180
|
-
this._maxScrollDirty = true;
|
|
181
|
-
this.resizeTimeout = null;
|
|
182
|
-
|
|
183
|
-
//this.applyThemeFromSystem();
|
|
184
|
-
this.state = {};
|
|
185
|
-
// NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
|
|
186
|
-
this.useWebGL = options.useWebGL !== false; // true par défaut
|
|
187
|
-
// Initialiser le contexte approprié
|
|
188
|
-
if (this.useWebGL) {
|
|
189
|
-
try {
|
|
190
|
-
this.ctx = new WebGLCanvasAdapter(this.canvas);
|
|
191
|
-
} 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);
|
|
192
159
|
this.ctx = this.canvas.getContext('2d');
|
|
193
|
-
this.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// Gestion des événements
|
|
220
|
-
this.isDragging = false;
|
|
221
|
-
this.lastTouchY = 0;
|
|
222
|
-
this.scrollOffset = 0;
|
|
223
|
-
this.scrollVelocity = 0;
|
|
224
|
-
this.scrollFriction = 0.95;
|
|
225
|
-
|
|
226
|
-
// Optimisation
|
|
227
|
-
this.dirtyComponents = new Set();
|
|
228
|
-
this.optimizationEnabled = false;
|
|
229
|
-
|
|
230
|
-
// AJOUTER CETTE LIGNE
|
|
231
|
-
this.animator = new AnimationEngine();
|
|
232
|
-
|
|
233
|
-
// ===== NOUVEAU SYSTÈME DE ROUTING =====
|
|
234
|
-
this.routes = new Map();
|
|
235
|
-
this.currentRoute = '/';
|
|
236
|
-
this.currentParams = {};
|
|
237
|
-
this.currentQuery = {};
|
|
238
|
-
this.history = [];
|
|
239
|
-
this.historyIndex = -1;
|
|
240
|
-
|
|
241
|
-
// Animation de transition
|
|
242
|
-
this.transitionState = {
|
|
243
|
-
isTransitioning: false,
|
|
244
|
-
progress: 0,
|
|
245
|
-
duration: 300,
|
|
246
|
-
type: 'slide', // 'slide', 'fade', 'none'
|
|
247
|
-
direction: 'forward', // 'forward', 'back'
|
|
248
|
-
oldComponents: [],
|
|
249
|
-
newComponents: []
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
this.setupCanvas();
|
|
253
|
-
this.setupEventListeners();
|
|
254
|
-
this.setupHistoryListener();
|
|
255
|
-
this.startRenderLoop();
|
|
256
|
-
|
|
257
|
-
this.devTools = new DevTools(this);
|
|
258
|
-
this.inspectionOverlay = new InspectionOverlay(this);
|
|
259
|
-
|
|
260
|
-
// MODIFICATION: Vérifier explicitement l'option enableDevTools
|
|
261
|
-
const shouldEnableDevTools = options.enableDevTools === true;
|
|
262
|
-
|
|
263
|
-
if (shouldEnableDevTools) {
|
|
264
|
-
this.enableDevTools();
|
|
265
|
-
console.log('DevTools enabled. Press Ctrl+Shift+D to toggle.');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Initialiser le ThemeManager
|
|
269
|
-
this.themeManager = new ThemeManager(this, {
|
|
270
|
-
lightTheme: options.lightTheme,
|
|
271
|
-
darkTheme: options.darkTheme,
|
|
272
|
-
storageKey: options.themeStorageKey || 'app-theme-mode'
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Raccourci pour accéder au thème actuel
|
|
276
|
-
this.theme = this.themeManager.getTheme();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Écoute les changements système (ex: utilisateur bascule dark mode)
|
|
281
|
-
*/
|
|
282
|
-
setupSystemThemeListener() {
|
|
283
|
-
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
284
|
-
|
|
285
|
-
// Ancienne méthode (compatibilité large)
|
|
286
|
-
if (mediaQuery.addEventListener) {
|
|
287
|
-
mediaQuery.addEventListener('change', (e) => {
|
|
288
|
-
if (this.themeMode === 'system') {
|
|
289
|
-
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
|
|
290
186
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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;
|
|
297
203
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Ajoute un listener de changement de thème
|
|
321
|
-
*/
|
|
322
|
-
onThemeChange(callback) {
|
|
323
|
-
this.themeManager.addListener((theme) => {
|
|
324
|
-
this.theme = theme;
|
|
325
|
-
callback(theme);
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Bascule entre light et dark
|
|
331
|
-
*/
|
|
332
|
-
toggleTheme() {
|
|
333
|
-
this.themeManager.toggle();
|
|
334
|
-
this.theme = this.themeManager.getTheme();
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Active ou désactive les DevTools
|
|
339
|
-
* @param {boolean} enabled - true pour activer, false pour désactiver
|
|
340
|
-
*/
|
|
341
|
-
enableDevTools(enabled = true) {
|
|
342
|
-
if (enabled) {
|
|
343
|
-
// Créer le DevTools s'il n'existe pas
|
|
344
|
-
if (!this.devTools) {
|
|
345
|
-
this.devTools = new DevTools(this);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Attacher seulement si pas déjà fait
|
|
349
|
-
if (!this.devTools._isAttached) {
|
|
350
|
-
this.devTools.attachToFramework();
|
|
351
|
-
this.devTools._isAttached = true;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Afficher le bouton
|
|
355
|
-
if (this.devTools.toggleBtn) {
|
|
356
|
-
this.devTools.toggleBtn.style.display = 'block';
|
|
357
|
-
}
|
|
358
|
-
} else {
|
|
359
|
-
// Désactiver complètement
|
|
360
|
-
if (this.devTools) {
|
|
361
|
-
// Détacher du framework
|
|
362
|
-
if (this.devTools.detachFromFramework) {
|
|
363
|
-
this.devTools.detachFromFramework();
|
|
364
|
-
} else if (this.devTools.cleanup) {
|
|
365
|
-
this.devTools.cleanup();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Supprimer de la page DOM
|
|
369
|
-
if (this.devTools.container && this.devTools.container.parentNode) {
|
|
370
|
-
this.devTools.container.parentNode.removeChild(this.devTools.container);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (this.devTools.toggleBtn && this.devTools.toggleBtn.parentNode) {
|
|
374
|
-
this.devTools.toggleBtn.parentNode.removeChild(this.devTools.toggleBtn);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
this.devTools._isAttached = false;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Bascule l'overlay d'inspection
|
|
384
|
-
*/
|
|
385
|
-
toggleInspection() {
|
|
386
|
-
this.inspectionOverlay.toggle();
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Exécute une commande DevTools
|
|
391
|
-
*/
|
|
392
|
-
devToolsCommand(command, ...args) {
|
|
393
|
-
switch (command) {
|
|
394
|
-
case 'inspect':
|
|
395
|
-
this.inspectionOverlay.enable();
|
|
396
|
-
break;
|
|
397
|
-
case 'performance':
|
|
398
|
-
this.devTools.switchTab('performance');
|
|
399
|
-
this.devTools.toggle();
|
|
400
|
-
break;
|
|
401
|
-
case 'components':
|
|
402
|
-
this.devTools.switchTab('components');
|
|
403
|
-
this.devTools.toggle();
|
|
404
|
-
break;
|
|
405
|
-
case 'highlight':
|
|
406
|
-
if (args[0]) {
|
|
407
|
-
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');
|
|
408
225
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
}
|
|
423
583
|
} else {
|
|
424
|
-
|
|
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;
|
|
425
638
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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 = `
|
|
433
658
|
let components = [];
|
|
434
659
|
|
|
435
660
|
self.onmessage = function(e) {
|
|
@@ -461,13 +686,15 @@ class CanvasFramework {
|
|
|
461
686
|
}
|
|
462
687
|
};
|
|
463
688
|
`;
|
|
464
|
-
|
|
465
|
-
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
466
|
-
return new Worker(URL.createObjectURL(blob));
|
|
467
|
-
}
|
|
468
689
|
|
|
469
|
-
|
|
470
|
-
|
|
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 = `
|
|
471
698
|
let state = {};
|
|
472
699
|
|
|
473
700
|
self.onmessage = async function(e) {
|
|
@@ -491,1122 +718,1209 @@ class CanvasFramework {
|
|
|
491
718
|
}
|
|
492
719
|
};
|
|
493
720
|
`;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// Protège la boucle
|
|
508
|
-
if (this.components && Array.isArray(this.components)) {
|
|
509
|
-
this.components.forEach(comp => comp.markDirty());
|
|
510
|
-
} else {
|
|
511
|
-
console.warn('[setTheme] components pas encore initialisé');
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Switch Theme
|
|
516
|
-
toggleDarkMode() {
|
|
517
|
-
if (this.theme === lightTheme) {
|
|
518
|
-
this.setTheme(darkTheme);
|
|
519
|
-
} else {
|
|
520
|
-
this.setTheme(lightTheme);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
enableFpsDisplay(enable = true) {
|
|
525
|
-
this.showFps = enable;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// AJOUTER CETTE MÉTHODE (optionnel - pour faciliter l'accès)
|
|
529
|
-
animate(component, options) {
|
|
530
|
-
return this.animator.animate(component, options);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// ----- Worker UI -----
|
|
534
|
-
handleWorkerMessage(e) {
|
|
535
|
-
const { type, payload } = e.data;
|
|
536
|
-
switch(type) {
|
|
537
|
-
case 'LAYOUT_DONE':
|
|
538
|
-
for (let update of payload) {
|
|
539
|
-
const comp = this.components.find(c => c.id === update.id);
|
|
540
|
-
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);
|
|
541
734
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
this.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
updateLogicWorkerState(newState) {
|
|
595
|
-
this.logicWorkerState = { ...this.logicWorkerState, ...newState };
|
|
596
|
-
this.logicWorker.postMessage({ type: 'SET_STATE', payload: this.logicWorkerState });
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
detectPlatform() {
|
|
600
|
-
const ua = navigator.userAgent.toLowerCase();
|
|
601
|
-
if (/android/.test(ua)) return 'material';
|
|
602
|
-
if (/iphone|ipad|ipod/.test(ua)) return 'cupertino';
|
|
603
|
-
return 'material';
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
setupCanvas() {
|
|
607
|
-
this.canvas.width = this.width * this.dpr;
|
|
608
|
-
this.canvas.height = this.height * this.dpr;
|
|
609
|
-
this.canvas.style.width = this.width + 'px';
|
|
610
|
-
this.canvas.style.height = this.height + 'px';
|
|
611
|
-
|
|
612
|
-
// Échelle uniquement pour Canvas 2D
|
|
613
|
-
if (!this.useWebGL) {
|
|
614
|
-
this.ctx.scale(this.dpr, this.dpr);
|
|
615
|
-
} else {
|
|
616
|
-
// WebGL gère le DPR automatiquement via la matrice de projection
|
|
617
|
-
this.ctx.updateProjectionMatrix();
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
setupEventListeners() {
|
|
622
|
-
this.canvas.addEventListener('touchstart', this.handleTouchStart.bind(this));
|
|
623
|
-
this.canvas.addEventListener('touchmove', this.handleTouchMove.bind(this));
|
|
624
|
-
this.canvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
|
|
625
|
-
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
|
626
|
-
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
|
627
|
-
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
|
628
|
-
window.addEventListener('resize', this.handleResize.bind(this));
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Configure l'écoute de l'historique du navigateur
|
|
633
|
-
* @private
|
|
634
|
-
*/
|
|
635
|
-
setupHistoryListener() {
|
|
636
|
-
window.addEventListener('popstate', (e) => {
|
|
637
|
-
if (e.state && e.state.route) {
|
|
638
|
-
this.navigateTo(e.state.route, {
|
|
639
|
-
replace: true,
|
|
640
|
-
animate: true,
|
|
641
|
-
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'
|
|
642
785
|
});
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
*/
|
|
656
|
-
route(pattern, component, options = {}) {
|
|
657
|
-
const route = {
|
|
658
|
-
pattern,
|
|
659
|
-
component,
|
|
660
|
-
regex: this.patternToRegex(pattern),
|
|
661
|
-
paramNames: this.extractParamNames(pattern),
|
|
662
|
-
beforeEnter: options.beforeEnter,
|
|
663
|
-
afterEnter: options.afterEnter,
|
|
664
|
-
beforeLeave: options.beforeLeave,
|
|
665
|
-
afterLeave: options.afterLeave, // ✅ NOUVEAU
|
|
666
|
-
onEnter: options.onEnter, // ✅ NOUVEAU (alias de afterEnter)
|
|
667
|
-
onLeave: options.onLeave, // ✅ NOUVEAU (alias de beforeLeave)
|
|
668
|
-
transition: options.transition || 'slide'
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
this.routes.set(pattern, route);
|
|
672
|
-
return this;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Convertit un pattern de route en regex
|
|
677
|
-
* @private
|
|
678
|
-
*/
|
|
679
|
-
patternToRegex(pattern) {
|
|
680
|
-
const regexPattern = pattern
|
|
681
|
-
.replace(/\//g, '\\/')
|
|
682
|
-
.replace(/:([^\/]+)/g, '([^\\/]+)');
|
|
683
|
-
return new RegExp(`^${regexPattern}$`);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Extrait les noms des paramètres d'un pattern
|
|
688
|
-
* @private
|
|
689
|
-
*/
|
|
690
|
-
extractParamNames(pattern) {
|
|
691
|
-
const matches = pattern.match(/:([^\/]+)/g);
|
|
692
|
-
return matches ? matches.map(m => m.slice(1)) : [];
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/**
|
|
696
|
-
* Trouve la route correspondant à un path
|
|
697
|
-
* @private
|
|
698
|
-
*/
|
|
699
|
-
matchRoute(path) {
|
|
700
|
-
// Séparer le path et la query string
|
|
701
|
-
const [pathname, queryString] = path.split('?');
|
|
702
|
-
|
|
703
|
-
for (let [pattern, route] of this.routes) {
|
|
704
|
-
const match = pathname.match(route.regex);
|
|
705
|
-
if (match) {
|
|
706
|
-
const params = {};
|
|
707
|
-
route.paramNames.forEach((name, index) => {
|
|
708
|
-
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
|
+
}
|
|
709
798
|
});
|
|
710
|
-
|
|
711
|
-
const query = this.parseQueryString(queryString);
|
|
712
|
-
|
|
713
|
-
return { route, params, query, pathname };
|
|
714
|
-
}
|
|
715
799
|
}
|
|
716
|
-
return null;
|
|
717
|
-
}
|
|
718
800
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
this.navigateTo(path, options);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* Méthode interne de navigation
|
|
745
|
-
* @private
|
|
746
|
-
*/
|
|
747
|
-
async navigateTo(path, options = {}) {
|
|
748
|
-
const {
|
|
749
|
-
replace = false,
|
|
750
|
-
animate = true,
|
|
751
|
-
direction = 'forward',
|
|
752
|
-
transition = null,
|
|
753
|
-
state = {}
|
|
754
|
-
} = options;
|
|
755
|
-
|
|
756
|
-
const match = this.matchRoute(path);
|
|
757
|
-
if (!match) {
|
|
758
|
-
console.warn(`Route not found: ${path}`);
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const { route, params, query, pathname } = match;
|
|
763
|
-
|
|
764
|
-
// ===== LIFECYCLE: AVANT DE QUITTER L'ANCIENNE ROUTE =====
|
|
765
|
-
|
|
766
|
-
// Hook beforeLeave de la route actuelle (peut bloquer la navigation)
|
|
767
|
-
const currentRouteData = this.routes.get(this.currentRoute);
|
|
768
|
-
if (currentRouteData?.beforeLeave) {
|
|
769
|
-
const canLeave = await currentRouteData.beforeLeave(this.currentParams, this.currentQuery);
|
|
770
|
-
if (canLeave === false) {
|
|
771
|
-
console.log('Navigation cancelled by beforeLeave hook');
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
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
|
+
}
|
|
774
822
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
823
|
+
|
|
824
|
+
runLogicTask(taskName, taskData) {
|
|
825
|
+
this.logicWorker.postMessage({
|
|
826
|
+
type: 'EXECUTE_TASK',
|
|
827
|
+
payload: {
|
|
828
|
+
taskName,
|
|
829
|
+
taskData
|
|
830
|
+
}
|
|
831
|
+
});
|
|
779
832
|
}
|
|
780
833
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
+
});
|
|
790
843
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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';
|
|
795
850
|
}
|
|
796
851
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const oldParams = { ...this.currentParams };
|
|
803
|
-
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';
|
|
804
857
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
+
}
|
|
810
866
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
window.history.pushState(
|
|
820
|
-
{ route: path, params, query, state },
|
|
821
|
-
'',
|
|
822
|
-
path
|
|
823
|
-
);
|
|
824
|
-
} else {
|
|
825
|
-
this.history[this.historyIndex] = { path, params, query, state };
|
|
826
|
-
window.history.replaceState(
|
|
827
|
-
{ route: path, params, query, state },
|
|
828
|
-
'',
|
|
829
|
-
path
|
|
830
|
-
);
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// ===== CRÉER LES NOUVEAUX COMPOSANTS =====
|
|
834
|
-
|
|
835
|
-
this.components = [];
|
|
836
|
-
if (typeof route.component === 'function') {
|
|
837
|
-
route.component(this, params, query);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// ===== LANCER L'ANIMATION DE TRANSITION =====
|
|
841
|
-
|
|
842
|
-
if (animate && !this.transitionState.isTransitioning) {
|
|
843
|
-
const transitionType = transition || route.transition || 'slide';
|
|
844
|
-
this.startTransition(oldComponents, this.components, transitionType, direction);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// ===== LIFECYCLE: APRÈS ÊTRE ENTRÉ DANS LA NOUVELLE ROUTE =====
|
|
848
|
-
|
|
849
|
-
// Hook afterEnter (appelé immédiatement après la création des composants)
|
|
850
|
-
if (route.afterEnter) {
|
|
851
|
-
route.afterEnter(params, query);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// ✅ NOUVEAU : Hook afterLeave de l'ancienne route (après transition complète)
|
|
855
|
-
if (currentRouteData?.afterLeave) {
|
|
856
|
-
// Si animation, attendre la fin de la transition
|
|
857
|
-
if (animate && this.transitionState.isTransitioning) {
|
|
858
|
-
setTimeout(() => {
|
|
859
|
-
currentRouteData.afterLeave(oldParams, oldQuery);
|
|
860
|
-
}, this.transitionState.duration || 300);
|
|
861
|
-
} else {
|
|
862
|
-
// Pas d'animation, appeler immédiatement
|
|
863
|
-
currentRouteData.afterLeave(oldParams, oldQuery);
|
|
864
|
-
}
|
|
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));
|
|
865
875
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
929
966
|
}
|
|
930
|
-
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
931
969
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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;
|
|
937
1011
|
}
|
|
938
|
-
this.ctx.restore();
|
|
939
|
-
break;
|
|
940
1012
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
+
}
|
|
947
1030
|
}
|
|
948
|
-
this.ctx.restore();
|
|
949
1031
|
|
|
950
|
-
//
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
+
}
|
|
955
1046
|
}
|
|
956
|
-
this.ctx.restore();
|
|
957
|
-
break;
|
|
958
1047
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
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);
|
|
963
1051
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
const deltaY = Math.abs(pos.y - this.lastTouchY);
|
|
1060
|
-
if (deltaY > 5) {
|
|
1061
|
-
this.isDragging = true;
|
|
1062
|
-
}
|
|
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;
|
|
1063
1147
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const pos = this.getTouchPos(touch);
|
|
1081
|
-
|
|
1082
|
-
if (!this.isDragging) {
|
|
1083
|
-
this.checkComponentsAtPosition(pos.x, pos.y, 'end');
|
|
1084
|
-
} else {
|
|
1085
|
-
this.isDragging = false;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
handleMouseDown(e) {
|
|
1090
|
-
this.isDragging = false;
|
|
1091
|
-
this.lastTouchY = e.clientY;
|
|
1092
|
-
this.checkComponentsAtPosition(e.clientX, e.clientY, 'start');
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
handleMouseMove(e) {
|
|
1096
|
-
if (!this.isDragging) {
|
|
1097
|
-
const deltaY = Math.abs(e.clientY - this.lastTouchY);
|
|
1098
|
-
if (deltaY > 5) {
|
|
1099
|
-
this.isDragging = true;
|
|
1100
|
-
}
|
|
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
|
+
};
|
|
1101
1164
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
if (child.visible &&
|
|
1150
|
-
cardAdjustedY >= child.y &&
|
|
1151
|
-
cardAdjustedY <= child.y + child.height &&
|
|
1152
|
-
cardAdjustedX >= child.x &&
|
|
1153
|
-
cardAdjustedX <= child.x + child.width) {
|
|
1154
|
-
|
|
1155
|
-
const relativeX = cardAdjustedX - child.x;
|
|
1156
|
-
const relativeY = cardAdjustedY - child.y;
|
|
1157
|
-
|
|
1158
|
-
switch (eventType) {
|
|
1159
|
-
case 'start':
|
|
1160
|
-
child.pressed = true;
|
|
1161
|
-
if (child.onPress) child.onPress?.(relativeX, relativeY);
|
|
1162
|
-
break;
|
|
1163
|
-
|
|
1164
|
-
case 'move':
|
|
1165
|
-
if (!child.hovered) {
|
|
1166
|
-
child.hovered = true;
|
|
1167
|
-
if (child.onHover) child.onHover();
|
|
1168
|
-
}
|
|
1169
|
-
if (child.onMove) child.onMove?.(relativeX, relativeY);
|
|
1170
|
-
break;
|
|
1171
|
-
|
|
1172
|
-
case 'end':
|
|
1173
|
-
if (child.pressed) {
|
|
1174
|
-
child.pressed = false;
|
|
1175
|
-
|
|
1176
|
-
if (child instanceof Input || child instanceof PasswordInput || child instanceof InputTags || child instanceof InputDatalist) {
|
|
1177
|
-
for (let other of this.components) {
|
|
1178
|
-
if (
|
|
1179
|
-
(other instanceof Input ||
|
|
1180
|
-
other instanceof PasswordInput ||
|
|
1181
|
-
other instanceof InputTags ||
|
|
1182
|
-
other instanceof InputDatalist) &&
|
|
1183
|
-
other !== child &&
|
|
1184
|
-
other.focused
|
|
1185
|
-
) {
|
|
1186
|
-
other.focused = false;
|
|
1187
|
-
other.cursorVisible = false;
|
|
1188
|
-
other.onBlur?.();
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
child.focused = true;
|
|
1193
|
-
child.cursorVisible = true;
|
|
1194
|
-
if (child.onFocus) child.onFocus();
|
|
1195
|
-
} else if (child.onClick) {
|
|
1196
|
-
child.onClick();
|
|
1197
|
-
} else if (child.onPress) {
|
|
1198
|
-
child.onPress?.(relativeX, relativeY);
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
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);
|
|
1202
1212
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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;
|
|
1206
1349
|
}
|
|
1207
|
-
}
|
|
1208
1350
|
}
|
|
1209
|
-
|
|
1210
|
-
if (
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
case 'move':
|
|
1218
|
-
if (!comp.hovered) {
|
|
1219
|
-
comp.hovered = true;
|
|
1220
|
-
if (comp.onHover) comp.onHover();
|
|
1221
|
-
}
|
|
1222
|
-
if (comp.onMove) comp.onMove(x, adjustedY);
|
|
1223
|
-
break;
|
|
1224
|
-
|
|
1225
|
-
case 'end':
|
|
1226
|
-
if (comp.pressed) {
|
|
1227
|
-
comp.pressed = false;
|
|
1228
|
-
|
|
1229
|
-
if (comp instanceof Input || comp instanceof PasswordInput || comp instanceof InputTags || comp instanceof InputDatalist) {
|
|
1230
|
-
for (let other of this.components) {
|
|
1231
|
-
if (
|
|
1232
|
-
(other instanceof Input ||
|
|
1233
|
-
other instanceof PasswordInput ||
|
|
1234
|
-
other instanceof InputTags ||
|
|
1235
|
-
other instanceof InputDatalist) &&
|
|
1236
|
-
other !== comp &&
|
|
1237
|
-
other.focused
|
|
1238
|
-
) {
|
|
1239
|
-
other.focused = false;
|
|
1240
|
-
other.cursorVisible = false;
|
|
1241
|
-
other.onBlur?.();
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
comp.focused = true;
|
|
1246
|
-
comp.cursorVisible = true;
|
|
1247
|
-
if (comp.onFocus) comp.onFocus();
|
|
1248
|
-
} else if (comp.onClick) {
|
|
1249
|
-
comp.onClick();
|
|
1250
|
-
} else if (comp.onPress) {
|
|
1251
|
-
comp.onPress(x, adjustedY);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
break;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
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;
|
|
1258
1359
|
} else {
|
|
1259
|
-
|
|
1360
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'move');
|
|
1260
1361
|
}
|
|
1261
|
-
}
|
|
1262
1362
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
this._cachedMaxScroll = Math.max(0, maxY - this.height + 50);
|
|
1276
|
-
this._maxScrollDirty = false;
|
|
1277
|
-
return this._cachedMaxScroll;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
/*getMaxScroll() {
|
|
1281
|
-
let maxY = 0;
|
|
1282
|
-
for (let comp of this.components) {
|
|
1283
|
-
if (!this.isFixedComponent(comp)) {
|
|
1284
|
-
maxY = Math.max(maxY, comp.y + comp.height);
|
|
1285
|
-
}
|
|
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
|
+
}
|
|
1286
1374
|
}
|
|
1287
|
-
return Math.max(0, maxY - this.height + 50);
|
|
1288
|
-
}*/
|
|
1289
1375
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
this.
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
}
|
|
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
|
+
}
|
|
1303
1388
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
remove(component) {
|
|
1317
|
-
const index = this.components.indexOf(component);
|
|
1318
|
-
if (index > -1) {
|
|
1319
|
-
component._unmount();
|
|
1320
|
-
this.components.splice(index, 1);
|
|
1321
|
-
this._maxScrollDirty = true; // ✅ AJOUTER CETTE LIGNE
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
markComponentDirty(component) {
|
|
1326
|
-
if (this.optimizationEnabled) {
|
|
1327
|
-
this.dirtyComponents.add(component);
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
enableOptimization() {
|
|
1332
|
-
this.optimizationEnabled = true;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
/**
|
|
1336
|
-
* Dessine un petit triangle rouge pour indiquer overflow (style Flutter)
|
|
1337
|
-
*/
|
|
1338
|
-
drawOverflowIndicators() {
|
|
1339
|
-
const ctx = this.ctx;
|
|
1340
|
-
|
|
1341
|
-
// Pour chaque composant
|
|
1342
|
-
for (let comp of this.components) {
|
|
1343
|
-
if (!comp.visible) continue;
|
|
1344
|
-
|
|
1345
|
-
// Position réelle à l'écran
|
|
1346
|
-
const isFixed = this.isFixedComponent(comp);
|
|
1347
|
-
const screenY = isFixed ? comp.y : comp.y + this.scrollOffset;
|
|
1348
|
-
const screenX = comp.x;
|
|
1349
|
-
|
|
1350
|
-
// Vérifier si le composant TEXT a une largeur/hauteur incorrecte
|
|
1351
|
-
let actualWidth = comp.width;
|
|
1352
|
-
let actualHeight = comp.height;
|
|
1353
|
-
|
|
1354
|
-
// Si c'est un Text, vérifier la taille réelle du texte
|
|
1355
|
-
if (comp instanceof Text && comp.text && ctx.measureText) {
|
|
1356
|
-
try {
|
|
1357
|
-
// Sauvegarder le style actuel
|
|
1358
|
-
ctx.save();
|
|
1359
|
-
|
|
1360
|
-
// Appliquer le style du texte
|
|
1361
|
-
if (comp.fontSize) {
|
|
1362
|
-
ctx.font = `${comp.fontSize}px ${comp.fontFamily || 'Arial'}`;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
// Mesurer la taille réelle
|
|
1366
|
-
const metrics = ctx.measureText(comp.text);
|
|
1367
|
-
actualWidth = metrics.width + (comp.padding || 0) * 2;
|
|
1368
|
-
actualHeight = (comp.fontSize || 16) + (comp.padding || 0) * 2;
|
|
1369
|
-
|
|
1370
|
-
ctx.restore();
|
|
1371
|
-
} catch (e) {
|
|
1372
|
-
// 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');
|
|
1373
1399
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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
|
-
|
|
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);
|
|
1488
1572
|
}
|
|
1489
1573
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
startRenderLoop() {
|
|
1496
|
-
const render = () => {
|
|
1497
|
-
// 1️⃣ Scroll inertia
|
|
1498
|
-
if (Math.abs(this.scrollVelocity) > 0.1 && !this.isDragging) {
|
|
1499
|
-
this.scrollOffset += this.scrollVelocity;
|
|
1500
|
-
this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -this.getMaxScroll());
|
|
1501
|
-
this.scrollVelocity *= this.scrollFriction;
|
|
1502
|
-
} else {
|
|
1503
|
-
this.scrollVelocity = 0;
|
|
1504
|
-
}
|
|
1574
|
+
return Math.max(0, maxY - this.height + 50);
|
|
1575
|
+
}*/
|
|
1505
1576
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1577
|
+
handleResize() {
|
|
1578
|
+
if (this.resizeTimeout) clearTimeout(this.resizeTimeout); // ✅ AJOUTER
|
|
1508
1579
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
for (let comp of this.dirtyComponents) {
|
|
1515
|
-
if (comp.visible) {
|
|
1516
|
-
const isFixed = this.isFixedComponent(comp);
|
|
1517
|
-
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();
|
|
1518
1585
|
|
|
1519
|
-
|
|
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
|
+
}
|
|
1520
1595
|
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1596
|
+
add(component) {
|
|
1597
|
+
this.components.push(component);
|
|
1598
|
+
component._mount();
|
|
1599
|
+
this._maxScrollDirty = true; // ✅ AJOUTER CETTE LIGNE
|
|
1600
|
+
return component;
|
|
1601
|
+
}
|
|
1525
1602
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
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
|
|
1530
1609
|
}
|
|
1531
|
-
|
|
1532
|
-
} else {
|
|
1533
|
-
// Full redraw
|
|
1534
|
-
const scrollableComponents = [];
|
|
1535
|
-
const fixedComponents = [];
|
|
1610
|
+
}
|
|
1536
1611
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1612
|
+
markComponentDirty(component) {
|
|
1613
|
+
if (this.optimizationEnabled) {
|
|
1614
|
+
this.dirtyComponents.add(component);
|
|
1540
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
|
+
}
|
|
1541
1724
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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();
|
|
1563
1779
|
}
|
|
1564
|
-
|
|
1780
|
+
}
|
|
1565
1781
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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
|
+
}
|
|
1574
1796
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
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
|
+
}
|
|
1608
1832
|
|
|
1609
|
-
|
|
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
|
+
}
|
|
1610
1909
|
|
|
1910
|
+
isFixedComponent(comp) {
|
|
1911
|
+
return FIXED_COMPONENT_TYPES.has(comp.constructor);
|
|
1912
|
+
}
|
|
1611
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
|
+
}
|
|
1612
1925
|
|
|
1926
|
+
export default CanvasFramework;
|