canvasframework 0.6.0 → 0.6.2

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.
@@ -572,11 +572,10 @@ class FloatedCamera extends Component {
572
572
 
573
573
  // 2. Mode contain/cover (DROITE)
574
574
  const mb = this._getModeBtnBounds();
575
- ctx.fillStyle = 'rgba(255,255,255,0.9)';
576
- ctx.fillRect(this.x + mb.x, this.y + mb.y, mb.size, mb.size);
577
- ctx.strokeStyle = '#000';
578
- ctx.lineWidth = 2;
579
- ctx.strokeRect(this.x + mb.x, this.y + mb.y, mb.size, mb.size);
575
+ ctx.fillStyle = 'rgba(255,255,255,0.9)';
576
+ ctx.beginPath();
577
+ ctx.arc(this.x + mb.x + mb.size / 2, this.y + mb.y + mb.size / 2, mb.size / 2, 0, Math.PI * 2);
578
+ ctx.fill();
580
579
  if (this.fitMode === 'contain') {
581
580
  this.drawContainIcon(ctx, this.x + mb.x, this.y + mb.y, mb.size);
582
581
  } else {
@@ -381,6 +381,7 @@ class CanvasFramework {
381
381
  };
382
382
  this._firstRenderDone = false;
383
383
  this._startupStartTime = startTime;
384
+ this._needsFullRender = true; // Flag de rendu global (remplace forEach sur scroll)
384
385
  // Dans le constructeur, après this.scrollFriction = 0.95;
385
386
  this.scrollFriction = 0.95;
386
387
 
@@ -585,6 +586,7 @@ class CanvasFramework {
585
586
 
586
587
  this.setupEventListeners();
587
588
  this.setupHistoryListener();
589
+ this._initNavigationGuard(); // ← AJOUTER
588
590
 
589
591
  this.startRenderLoop();
590
592
 
@@ -612,29 +614,27 @@ class CanvasFramework {
612
614
  // ✅ AJOUTER: Mesurer le temps d'init
613
615
  const initTime = performance.now() - startTime;
614
616
 
615
- // ✅ AJOUTER: Stocker les métriques
616
- this.metrics = {
617
- initTime: initTime,
618
- firstRenderTime: null,
619
- firstInteractionTime: null,
620
- totalStartupTime: null
621
- };
617
+ // ✅ Mettre à jour les métriques (initialisées plus haut)
618
+ this.metrics.initTime = initTime;
622
619
 
623
620
  // ✅ AJOUTER: Logger si debug
624
621
  if (options.debug || options.showMetrics) {
625
622
  console.log(`⚡ Framework initialisé en ${initTime.toFixed(2)}ms`);
626
623
  }
627
624
 
628
- // ✅ AJOUTER: Marquer le premier rendu
629
- this._firstRenderDone = false;
630
- this._startupStartTime = startTime;
631
-
632
625
  // ✅ OPTIMISATION OPTION 5: Partition spatiale pour le culling (optionnel)
633
626
  if (this.optimizations.useSpatialPartitioning) {
634
627
  this._initSpatialPartitioning();
635
628
  }
636
629
 
637
630
  }
631
+
632
+ _initNavigationGuard() {
633
+ // Injecter 2 entrées : une base + la route actuelle
634
+ // Ainsi le navigateur a toujours une entrée "avant" à consommer
635
+ window.history.replaceState({ route: '/', _guard: true }, '', '/');
636
+ window.history.pushState({ route: '/', _app: true }, '', '/');
637
+ }
638
638
 
639
639
  /**
640
640
  * Crée un élément DOM temporaire, l'ajoute au body, exécute une callback, puis le supprime
@@ -972,7 +972,10 @@ class CanvasFramework {
972
972
  `;
973
973
 
974
974
  const blob = new Blob([workerCode], { type: 'application/javascript' });
975
- return new Worker(URL.createObjectURL(blob));
975
+ const _scrollWorkerUrl = URL.createObjectURL(blob);
976
+ const _scrollWorker = new Worker(_scrollWorkerUrl);
977
+ URL.revokeObjectURL(_scrollWorkerUrl); // Libérer la mémoire du blob
978
+ return _scrollWorker;
976
979
  }
977
980
 
978
981
  /**
@@ -996,12 +999,9 @@ class CanvasFramework {
996
999
  this._cachedMaxScroll = payload.maxScroll;
997
1000
  this._maxScrollDirty = false;
998
1001
 
1002
+ // ✅ OPTIMISATION : Un simple flag au lieu d'un forEach sur tous les composants
999
1003
  if (Math.abs(payload.scrollVelocity) > 0) {
1000
- this.components.forEach(comp => {
1001
- if (!this.isFixedComponent(comp)) {
1002
- this.markComponentDirty(comp);
1003
- }
1004
- });
1004
+ this._needsFullRender = true;
1005
1005
  }
1006
1006
 
1007
1007
  // ✅ AJOUTER: Continuer l'animation de retour si nécessaire
@@ -1189,6 +1189,13 @@ class CanvasFramework {
1189
1189
  const key = `${text}_${font}_${color}`;
1190
1190
 
1191
1191
  if (!this.textCache.has(key)) {
1192
+ // ✅ Limite LRU : éviter une croissance mémoire infinie
1193
+ if (this.textCache.size >= 500) {
1194
+ // Supprimer la première entrée (la plus ancienne)
1195
+ const firstKey = this.textCache.keys().next().value;
1196
+ this.textCache.delete(firstKey);
1197
+ }
1198
+
1192
1199
  // Rendu dans un canvas temporaire
1193
1200
  const temp = document.createElement('canvas');
1194
1201
  const tempCtx = temp.getContext('2d', {
@@ -1440,6 +1447,7 @@ class CanvasFramework {
1440
1447
  }
1441
1448
 
1442
1449
  // Marquer tous les composants comme sales pour forcer un redessin complet
1450
+ this._needsFullRender = true;
1443
1451
  this.components.forEach(comp => this.markComponentDirty(comp));
1444
1452
  }
1445
1453
  }
@@ -1469,6 +1477,14 @@ class CanvasFramework {
1469
1477
  logoImage.src = opts.logo;
1470
1478
  }
1471
1479
 
1480
+ // ✅ OPTIMISATION : Créer le gradient UNE seule fois avant la boucle
1481
+ let splashGradient = null;
1482
+ if (Array.isArray(opts.backgroundColor) && opts.backgroundColor.length >= 2) {
1483
+ splashGradient = this.ctx.createLinearGradient(0, 0, this.width, this.height);
1484
+ splashGradient.addColorStop(0, opts.backgroundColor[0]);
1485
+ splashGradient.addColorStop(1, opts.backgroundColor[1]);
1486
+ }
1487
+
1472
1488
  const animate = () => {
1473
1489
  const elapsed = performance.now() - startTime;
1474
1490
  const progress = Math.min(elapsed / opts.duration, 1);
@@ -1476,17 +1492,8 @@ class CanvasFramework {
1476
1492
  // Clear
1477
1493
  this.ctx.clearRect(0, 0, this.width, this.height);
1478
1494
 
1479
- // ✅ Background (gradient ou couleur unie)
1480
- if (Array.isArray(opts.backgroundColor) && opts.backgroundColor.length >= 2) {
1481
- // Gradient
1482
- const gradient = this.ctx.createLinearGradient(0, 0, this.width, this.height);
1483
- gradient.addColorStop(0, opts.backgroundColor[0]);
1484
- gradient.addColorStop(1, opts.backgroundColor[1]);
1485
- this.ctx.fillStyle = gradient;
1486
- } else {
1487
- // Couleur unie
1488
- this.ctx.fillStyle = opts.backgroundColor;
1489
- }
1495
+ // ✅ Background (gradient pré-calculé ou couleur unie)
1496
+ this.ctx.fillStyle = splashGradient || opts.backgroundColor;
1490
1497
  this.ctx.fillRect(0, 0, this.width, this.height);
1491
1498
 
1492
1499
  const centerX = this.width / 2;
@@ -1560,6 +1567,14 @@ class CanvasFramework {
1560
1567
  const duration = opts.fadeOutDuration;
1561
1568
  const startTime = performance.now();
1562
1569
 
1570
+ // ✅ OPTIMISATION : Créer le gradient UNE seule fois avant la boucle
1571
+ let fadeGradient = null;
1572
+ if (Array.isArray(opts.backgroundColor) && opts.backgroundColor.length >= 2) {
1573
+ fadeGradient = this.ctx.createLinearGradient(0, 0, this.width, this.height);
1574
+ fadeGradient.addColorStop(0, opts.backgroundColor[0]);
1575
+ fadeGradient.addColorStop(1, opts.backgroundColor[1]);
1576
+ }
1577
+
1563
1578
  const fade = () => {
1564
1579
  const elapsed = performance.now() - startTime;
1565
1580
  const progress = elapsed / duration;
@@ -1569,15 +1584,8 @@ class CanvasFramework {
1569
1584
  this.ctx.clearRect(0, 0, this.width, this.height);
1570
1585
  this.ctx.globalAlpha = alpha;
1571
1586
 
1572
- // Redessiner le background
1573
- if (Array.isArray(opts.backgroundColor) && opts.backgroundColor.length >= 2) {
1574
- const gradient = this.ctx.createLinearGradient(0, 0, this.width, this.height);
1575
- gradient.addColorStop(0, opts.backgroundColor[0]);
1576
- gradient.addColorStop(1, opts.backgroundColor[1]);
1577
- this.ctx.fillStyle = gradient;
1578
- } else {
1579
- this.ctx.fillStyle = opts.backgroundColor;
1580
- }
1587
+ // Redessiner le background avec le gradient pré-calculé
1588
+ this.ctx.fillStyle = fadeGradient || opts.backgroundColor;
1581
1589
  this.ctx.fillRect(0, 0, this.width, this.height);
1582
1590
 
1583
1591
  // Spinner pendant le fade
@@ -1595,17 +1603,18 @@ class CanvasFramework {
1595
1603
  requestAnimationFrame(fade);
1596
1604
  } else {
1597
1605
  this._splashFinished = true;
1598
- // ✅ AJOUTER : Réinitialiser complètement le contexte
1606
+ // ✅ Réinitialiser complètement le contexte
1599
1607
  this.ctx.clearRect(0, 0, this.width, this.height);
1600
1608
  this.ctx.globalAlpha = 1;
1601
- this.ctx.textAlign = 'start'; // ← IMPORTANT
1602
- this.ctx.textBaseline = 'alphabetic'; // ← IMPORTANT
1603
- this.ctx.font = '10px sans-serif'; // Valeur par défaut
1609
+ this.ctx.textAlign = 'start';
1610
+ this.ctx.textBaseline = 'alphabetic';
1611
+ this.ctx.font = '10px sans-serif';
1604
1612
  this.ctx.fillStyle = '#000000';
1605
1613
  this.ctx.strokeStyle = '#000000';
1606
1614
  this.ctx.lineWidth = 1;
1607
1615
  this.ctx.lineCap = 'butt';
1608
1616
  this.ctx.lineJoin = 'miter';
1617
+ this._needsFullRender = true;
1609
1618
  }
1610
1619
  };
1611
1620
 
@@ -1765,19 +1774,14 @@ class CanvasFramework {
1765
1774
  }
1766
1775
  }
1767
1776
 
1777
+ /**
1778
+ * Méthode réservée pour les futures personnalisations de contexte liées au thème.
1779
+ * L'ancienne implémentation modifiait Object.defineProperty sur le prototype global,
1780
+ * ce qui pouvait causer des effets de bord. Désactivée intentionnellement.
1781
+ */
1768
1782
  wrapContext(ctx, theme) {
1769
- const originalFillStyle = Object.getOwnPropertyDescriptor(CanvasRenderingContext2D.prototype, 'fillStyle');
1770
- Object.defineProperty(ctx, 'fillStyle', {
1771
- set: (value) => {
1772
- // Si value est blanc/noir ou une couleur “neutre”, tu remplaces par theme
1773
- if (value === '#FFFFFF' || value === '#000000') {
1774
- originalFillStyle.set.call(ctx, value);
1775
- } else {
1776
- originalFillStyle.set.call(ctx, value);
1777
- }
1778
- },
1779
- get: () => originalFillStyle.get.call(ctx)
1780
- });
1783
+ // Intentionnellement vide : les deux branches produisaient le même effet.
1784
+ // À ré-implémenter proprement si un remplacement de couleur basé sur le thème est requis.
1781
1785
  }
1782
1786
 
1783
1787
  createCanvasWorker() {
@@ -1832,9 +1836,10 @@ class CanvasFramework {
1832
1836
 
1833
1837
  // Protège la boucle
1834
1838
  if (this.components && Array.isArray(this.components)) {
1839
+ this._needsFullRender = true;
1835
1840
  this.components.forEach(comp => comp.markDirty());
1836
1841
  } else {
1837
- console.warn('[setTheme] components pas encore initialisé');
1842
+ if (this.debbug) console.warn('[setTheme] components pas encore initialisé');
1838
1843
  }
1839
1844
  }
1840
1845
 
@@ -1949,13 +1954,22 @@ class CanvasFramework {
1949
1954
  }
1950
1955
 
1951
1956
  setupEventListeners() {
1952
- this.canvas.addEventListener('touchstart', this.handleTouchStart.bind(this));
1953
- this.canvas.addEventListener('touchmove', this.handleTouchMove.bind(this));
1954
- this.canvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
1955
- this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
1956
- this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
1957
- this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
1958
- window.addEventListener('resize', this.handleResize.bind(this));
1957
+ // Sauvegarder les références bound pour pouvoir les retirer dans destroy()
1958
+ this._boundHandleTouchStart = this.handleTouchStart.bind(this);
1959
+ this._boundHandleTouchMove = this.handleTouchMove.bind(this);
1960
+ this._boundHandleTouchEnd = this.handleTouchEnd.bind(this);
1961
+ this._boundHandleMouseDown = this.handleMouseDown.bind(this);
1962
+ this._boundHandleMouseMove = this.handleMouseMove.bind(this);
1963
+ this._boundHandleMouseUp = this.handleMouseUp.bind(this);
1964
+ this._boundHandleResize = this.handleResize.bind(this);
1965
+
1966
+ this.canvas.addEventListener('touchstart', this._boundHandleTouchStart, { passive: false });
1967
+ this.canvas.addEventListener('touchmove', this._boundHandleTouchMove, { passive: false });
1968
+ this.canvas.addEventListener('touchend', this._boundHandleTouchEnd, { passive: false });
1969
+ this.canvas.addEventListener('mousedown', this._boundHandleMouseDown);
1970
+ this.canvas.addEventListener('mousemove', this._boundHandleMouseMove);
1971
+ this.canvas.addEventListener('mouseup', this._boundHandleMouseUp);
1972
+ window.addEventListener('resize', this._boundHandleResize);
1959
1973
  }
1960
1974
 
1961
1975
  /**
@@ -1963,16 +1977,51 @@ class CanvasFramework {
1963
1977
  * @private
1964
1978
  */
1965
1979
  setupHistoryListener() {
1966
- window.addEventListener('popstate', (e) => {
1967
- if (e.state && e.state.route) {
1968
- this.navigateTo(e.state.route, {
1969
- replace: true,
1970
- animate: true,
1971
- direction: 'back'
1972
- });
1973
- }
1974
- });
1975
- }
1980
+ window.addEventListener('popstate', (e) => {
1981
+ // Cas 1 : L'app a un historique interne → naviguer en arrière dans l'app
1982
+ if (this.historyIndex > 0) {
1983
+ this.historyIndex--;
1984
+ const entry = this.history[this.historyIndex];
1985
+
1986
+ // Recharger la vue précédente sans toucher window.history
1987
+ this._navigateInternal(entry.path, {
1988
+ replace: true,
1989
+ animate: true,
1990
+ direction: 'back'
1991
+ });
1992
+
1993
+ // Réinjecter une entrée pour ne jamais vider la pile navigateur
1994
+ window.history.pushState({ route: entry.path, _app: true }, '', entry.path);
1995
+
1996
+ } else {
1997
+ // Cas 2 : Plus d'historique interne → réinjecter et ignorer
1998
+ // (empêche de quitter l'app)
1999
+ const currentPath = this.currentRoute || '/';
2000
+ window.history.pushState({ route: currentPath, _app: true }, '', currentPath);
2001
+
2002
+ // Optionnel : afficher un toast "Appuyez encore pour quitter"
2003
+ this._handleBackExitAttempt();
2004
+ }
2005
+ });
2006
+ }
2007
+
2008
+ _handleBackExitAttempt() {
2009
+ const now = Date.now();
2010
+
2011
+ if (this._lastBackAttempt && (now - this._lastBackAttempt) < 2000) {
2012
+ // Deux back rapides → vraiment quitter (sur PWA/WebView)
2013
+ // Sur navigateur standard ce n'est pas possible, mais sur Capacitor/Cordova oui
2014
+ if (window.navigator.app?.exitApp) {
2015
+ window.navigator.app.exitApp(); // Cordova
2016
+ } else if (window.Capacitor?.Plugins?.App) {
2017
+ window.Capacitor.Plugins.App.exitApp(); // Capacitor
2018
+ }
2019
+ this._lastBackAttempt = null;
2020
+ } else {
2021
+ this._lastBackAttempt = now;
2022
+ this.showToast('Appuyez encore pour quitter', 2000);
2023
+ }
2024
+ }
1976
2025
 
1977
2026
  // ===== MÉTHODES DE ROUTING =====
1978
2027
 
@@ -2207,6 +2256,12 @@ class CanvasFramework {
2207
2256
  }
2208
2257
 
2209
2258
  this._maxScrollDirty = true;
2259
+ this._needsFullRender = true; // Forcer un rendu complet après navigation
2260
+ }
2261
+
2262
+ // Alias pour usage interne (sans toucher window.history)
2263
+ _navigateInternal(path, options = {}) {
2264
+ return this.navigateTo(path, { ...options, _internal: true });
2210
2265
  }
2211
2266
 
2212
2267
  /**
@@ -2697,16 +2752,6 @@ class CanvasFramework {
2697
2752
  return Math.max(0, maxY - this.height + 50);
2698
2753
  }
2699
2754
 
2700
- /*getMaxScroll() {
2701
- let maxY = 0;
2702
- for (let comp of this.components) {
2703
- if (!this.isFixedComponent(comp)) {
2704
- maxY = Math.max(maxY, comp.y + comp.height);
2705
- }
2706
- }
2707
- return Math.max(0, maxY - this.height + 50);
2708
- }*/
2709
-
2710
2755
  handleResize() {
2711
2756
  if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
2712
2757
 
@@ -2717,18 +2762,19 @@ class CanvasFramework {
2717
2762
  // Règle clé : on resize UNIQUEMENT si la largeur a vraiment changé
2718
2763
  // (rotation, split-screen, vraie fenêtre changée)
2719
2764
  // À la fermeture du clavier → largeur = identique → on skip
2720
- if (Math.abs(newWidth - this.width) <= 8) { // tolérance pixels (scrollbar, bordures...)
2721
- console.log("[resize] Largeur identique → probablement clavier (open/close) → ignoré");
2765
+ if (Math.abs(newWidth - this.width) <= 8) {
2766
+ if (this.debbug) console.log("[resize] Largeur identique → probablement clavier (open/close) → ignoré");
2722
2767
  return;
2723
2768
  }
2724
2769
 
2725
- console.log("[resize] Largeur changée → vrai resize");
2770
+ if (this.debbug) console.log("[resize] Largeur changée → vrai resize");
2726
2771
 
2727
2772
  this.width = newWidth;
2728
2773
  this.height = newHeight;
2729
2774
 
2730
2775
  this.setupCanvas(); // ← change canvas.width/height + DPR + style
2731
2776
  this._maxScrollDirty = true;
2777
+ this._needsFullRender = true; // Forcer un rendu complet après resize
2732
2778
 
2733
2779
  if (this.scrollWorker) {
2734
2780
  this.scrollWorker.postMessage({
@@ -2765,6 +2811,7 @@ class CanvasFramework {
2765
2811
  this.components.push(component);
2766
2812
  component._mount();
2767
2813
  this.updateScrollWorkerComponents();
2814
+ this._needsFullRender = true; // Forcer un rendu complet après ajout
2768
2815
  return component;
2769
2816
  }
2770
2817
 
@@ -2774,6 +2821,7 @@ class CanvasFramework {
2774
2821
  component._unmount();
2775
2822
  this.components.splice(index, 1);
2776
2823
  this.updateScrollWorkerComponents();
2824
+ this._needsFullRender = true; // Forcer un rendu complet après suppression
2777
2825
  }
2778
2826
  }
2779
2827
 
@@ -2782,10 +2830,12 @@ class CanvasFramework {
2782
2830
  // Vérifications basiques
2783
2831
  if (!component || !component.visible) return;
2784
2832
 
2785
- // ✅ TOUJOURS ajouter au set des composants sales
2786
- // Les conditions de rendu seront vérifiées dans _renderDirtyComponents
2787
2833
  if (this.optimizationEnabled) {
2834
+ // ✅ Ajouter au set des composants sales pour rendu partiel
2788
2835
  this.dirtyComponents.add(component);
2836
+ } else {
2837
+ // Sans optimisation : forcer un rendu complet
2838
+ this._needsFullRender = true;
2789
2839
  }
2790
2840
 
2791
2841
  // ✅ Optionnel : Marquer aussi le composant lui-même
@@ -2959,8 +3009,7 @@ class CanvasFramework {
2959
3009
  }
2960
3010
 
2961
3011
  startRenderLoop() {
2962
- let lastScrollOffset = this.scrollOffset;
2963
- this._needsRender = true; // ← AJOUTER
3012
+ this._needsRender = true;
2964
3013
 
2965
3014
  const render = () => {
2966
3015
  if (!this._splashFinished) {
@@ -2974,25 +3023,22 @@ class CanvasFramework {
2974
3023
 
2975
3024
  // Si une transition est en cours
2976
3025
  if (this.transitionState.isTransitioning) {
2977
- // Mettre à jour la progression
2978
3026
  const elapsed = Date.now() - this.transitionState.startTime;
2979
3027
  this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
2980
3028
 
2981
- // Rendu spécial pour la transition
2982
3029
  this.renderSimpleTransition();
2983
3030
 
2984
- // Si la transition est terminée
2985
3031
  if (this.transitionState.progress >= 1) {
2986
3032
  this.transitionState.isTransitioning = false;
2987
3033
  this.transitionState.oldComponents = [];
2988
3034
  }
2989
- }
2990
- // Sinon, rendu normal
3035
+ }
3036
+ // Rendu normal — toujours dessiner pour garantir l'affichage
2991
3037
  else {
2992
3038
  this.renderFull();
2993
3039
  }
2994
3040
 
2995
- // Calcul FPS (optionnel)
3041
+ // Calcul FPS
2996
3042
  this._frames++;
2997
3043
  const now = performance.now();
2998
3044
  const elapsed = now - this._lastFpsTime;
@@ -3169,26 +3215,6 @@ class CanvasFramework {
3169
3215
  ctx.restore();
3170
3216
  }
3171
3217
 
3172
- /**
3173
- * Mettre à jour la progression de la transition
3174
- * @private
3175
- */
3176
- updateTransition() {
3177
- if (!this.transitionState.isTransitioning) return;
3178
-
3179
- const elapsed = Date.now() - this.transitionState.startTime;
3180
- this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
3181
-
3182
- // Si la transition est terminée
3183
- if (this.transitionState.progress >= 1) {
3184
- this.transitionState.isTransitioning = false;
3185
-
3186
- // Marquer tous les nouveaux composants comme sales pour le prochain rendu
3187
- this.transitionState.newComponents.forEach(comp => {
3188
- this.markComponentDirty(comp);
3189
- });
3190
- }
3191
- }
3192
3218
 
3193
3219
  /**
3194
3220
  * Rendu complet normal (sans transition)
@@ -3311,14 +3337,18 @@ class CanvasFramework {
3311
3337
  this.ctx.destroy();
3312
3338
  }
3313
3339
 
3314
- // Nettoyer les écouteurs d'événements
3315
- this.canvas.removeEventListener('touchstart', this.handleTouchStart);
3316
- this.canvas.removeEventListener('touchmove', this.handleTouchMove);
3317
- this.canvas.removeEventListener('touchend', this.handleTouchEnd);
3318
- this.canvas.removeEventListener('mousedown', this.handleMouseDown);
3319
- this.canvas.removeEventListener('mousemove', this.handleMouseMove);
3320
- this.canvas.removeEventListener('mouseup', this.handleMouseUp);
3321
- window.removeEventListener('resize', this.handleResize);
3340
+ // Nettoyer les écouteurs d'événements (références bound sauvegardées)
3341
+ this.canvas.removeEventListener('touchstart', this._boundHandleTouchStart);
3342
+ this.canvas.removeEventListener('touchmove', this._boundHandleTouchMove);
3343
+ this.canvas.removeEventListener('touchend', this._boundHandleTouchEnd);
3344
+ this.canvas.removeEventListener('mousedown', this._boundHandleMouseDown);
3345
+ this.canvas.removeEventListener('mousemove', this._boundHandleMouseMove);
3346
+ this.canvas.removeEventListener('mouseup', this._boundHandleMouseUp);
3347
+ window.removeEventListener('resize', this._boundHandleResize);
3348
+
3349
+ // Vider le textCache pour libérer la mémoire
3350
+ if (this.textCache) this.textCache.clear();
3351
+ if (this.imageCache) this.imageCache.clear();
3322
3352
  }
3323
3353
 
3324
3354
  // ✅ AJOUTER: Afficher les métriques à l'écran