canvasframework 0.4.9 → 0.4.11

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.
@@ -155,11 +155,53 @@ class CanvasFramework {
155
155
  constructor(canvasId, options = {}) {
156
156
  // ✅ AJOUTER: Démarrer le chronomètre
157
157
  const startTime = performance.now();
158
+
159
+ // ✅ OPTIMISATION OPTION 5: Contexte Canvas optimisé
158
160
  this.canvas = document.getElementById(canvasId);
159
- this.ctx = this.canvas.getContext('2d');
161
+ this.ctx = this.canvas.getContext('2d', {
162
+ alpha: false, // ✅ Gain de 30% de performance
163
+ desynchronized: true, // ✅ Bypass la queue du navigateur
164
+ willReadFrequently: false
165
+ });
166
+
167
+ this.backgroundColor = options.backgroundColor || '#f5f5f5'; // Blanc par défaut
168
+
160
169
  this.width = window.innerWidth;
161
170
  this.height = window.innerHeight;
162
171
  this.dpr = window.devicePixelRatio || 1;
172
+
173
+ // ✅ OPTIMISATION OPTION 2: Configuration des optimisations
174
+ this.optimizations = {
175
+ enabled: options.optimizations !== false, // Activé par défaut
176
+ useDoubleBuffering: true,
177
+ useCaching: true,
178
+ useBatchDrawing: true,
179
+ useSpatialPartitioning: false, // Désactivé par défaut (à activer si beaucoup de composants)
180
+ useImageDataOptimization: true
181
+ };
182
+
183
+ // ✅ OPTIMISATION OPTION 2: Cache pour éviter les changements d'état inutiles
184
+ this._stateCache = {
185
+ fillStyle: null,
186
+ strokeStyle: null,
187
+ font: null,
188
+ textAlign: null,
189
+ textBaseline: null,
190
+ lineWidth: null,
191
+ globalAlpha: 1
192
+ };
193
+
194
+ // ✅ OPTIMISATION OPTION 2: Cache des images/textes
195
+ this.imageCache = new Map();
196
+ this.textCache = new Map();
197
+
198
+ // ✅ OPTIMISATION OPTION 2: Double buffering
199
+ this._doubleBuffer = null;
200
+ this._bufferCtx = null;
201
+ if (this.optimizations.useDoubleBuffering) {
202
+ this._initDoubleBuffer();
203
+ }
204
+
163
205
  this.splashOptions = {
164
206
  enabled: options.splash?.enabled === true, // false par défaut
165
207
  duration: options.splash?.duration || 700,
@@ -178,6 +220,7 @@ class CanvasFramework {
178
220
  logoWidth: options.splash?.logoWidth || 100,
179
221
  logoHeight: options.splash?.logoHeight || 100
180
222
  };
223
+
181
224
  // ✅ MODIFIER : Vérifier si le splash est activé
182
225
  if (this.splashOptions.enabled) {
183
226
  this.showSplashScreen();
@@ -187,7 +230,6 @@ class CanvasFramework {
187
230
 
188
231
  this.platform = this.detectPlatform();
189
232
 
190
-
191
233
  // État actuel + préférence
192
234
  this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
193
235
  this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
@@ -213,7 +255,7 @@ class CanvasFramework {
213
255
  // NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
214
256
  this.useWebGL = options.useWebGL !== false; // true par défaut
215
257
  // Initialiser le contexte approprié
216
- if (this.useWebGL) {
258
+ /*if (this.useWebGL) {
217
259
  try {
218
260
  this.ctx = new WebGLCanvasAdapter(this.canvas);
219
261
  } catch (e) {
@@ -222,7 +264,8 @@ class CanvasFramework {
222
264
  }
223
265
  } else {
224
266
  this.ctx = this.canvas.getContext('2d');
225
- }
267
+ }*/
268
+
226
269
  // Calcule FPS
227
270
  this.fps = 0;
228
271
  this._frames = 0;
@@ -264,7 +307,7 @@ class CanvasFramework {
264
307
 
265
308
  // Optimisation
266
309
  this.dirtyComponents = new Set();
267
- this.optimizationEnabled = false;
310
+ this.optimizationEnabled = this.optimizations.enabled;
268
311
 
269
312
  // AJOUTER CETTE LIGNE
270
313
  this.animator = new AnimationEngine();
@@ -289,10 +332,13 @@ class CanvasFramework {
289
332
  };
290
333
 
291
334
  this.setupCanvas();
335
+
336
+ // ✅ OPTIMISATION OPTION 5: Désactiver l'antialiasing pour meilleures performances
337
+ this._disableImageSmoothing();
338
+
292
339
  this.setupEventListeners();
293
340
  this.setupHistoryListener();
294
341
 
295
-
296
342
  this.startRenderLoop();
297
343
 
298
344
  this.devTools = new DevTools(this);
@@ -335,6 +381,340 @@ class CanvasFramework {
335
381
  // ✅ AJOUTER: Marquer le premier rendu
336
382
  this._firstRenderDone = false;
337
383
  this._startupStartTime = startTime;
384
+
385
+ // ✅ OPTIMISATION OPTION 5: Partition spatiale pour le culling (optionnel)
386
+ if (this.optimizations.useSpatialPartitioning) {
387
+ this._initSpatialPartitioning();
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Initialise le double buffering pour éviter le flickering
393
+ * @private
394
+ */
395
+ _initDoubleBuffer() {
396
+ this._doubleBuffer = document.createElement('canvas');
397
+ this._bufferCtx = this._doubleBuffer.getContext('2d', {
398
+ alpha: false,
399
+ desynchronized: true
400
+ });
401
+ this._doubleBuffer.width = this.width * this.dpr;
402
+ this._doubleBuffer.height = this.height * this.dpr;
403
+ this._doubleBuffer.style.width = this.width + 'px';
404
+ this._doubleBuffer.style.height = this.height + 'px';
405
+ this._bufferCtx.scale(this.dpr, this.dpr);
406
+ this._disableImageSmoothing(this._bufferCtx);
407
+ }
408
+
409
+ /**
410
+ * Désactive l'antialiasing pour meilleures performances
411
+ * @private
412
+ * @param {CanvasRenderingContext2D} [ctx=this.ctx] - Contexte à configurer
413
+ */
414
+ _disableImageSmoothing(ctx = this.ctx) {
415
+ ctx.imageSmoothingEnabled = false;
416
+ ctx.msImageSmoothingEnabled = false;
417
+ ctx.webkitImageSmoothingEnabled = false;
418
+ ctx.mozImageSmoothingEnabled = false;
419
+ }
420
+
421
+ /**
422
+ * Initialise le spatial partitioning pour le viewport culling
423
+ * @private
424
+ */
425
+ _initSpatialPartitioning() {
426
+ // Simple grid spatial partitioning
427
+ this._spatialGrid = {
428
+ cellSize: 100,
429
+ grid: new Map(),
430
+ update: (components) => {
431
+ this._spatialGrid.grid.clear();
432
+ components.forEach(comp => {
433
+ if (!comp.visible) return;
434
+
435
+ const gridX = Math.floor(comp.x / this._spatialGrid.cellSize);
436
+ const gridY = Math.floor(comp.y / this._spatialGrid.cellSize);
437
+ const key = `${gridX},${gridY}`;
438
+
439
+ if (!this._spatialGrid.grid.has(key)) {
440
+ this._spatialGrid.grid.set(key, []);
441
+ }
442
+ this._spatialGrid.grid.get(key).push(comp);
443
+ });
444
+ },
445
+ getVisible: (viewportY) => {
446
+ const visible = [];
447
+ const startY = viewportY - 200; // Marge de 200px
448
+ const endY = viewportY + this.height + 200;
449
+
450
+ this._spatialGrid.grid.forEach((comps, key) => {
451
+ comps.forEach(comp => {
452
+ const compBottom = comp.y + comp.height;
453
+ if (compBottom >= startY && comp.y <= endY) {
454
+ visible.push(comp);
455
+ }
456
+ });
457
+ });
458
+ return visible;
459
+ }
460
+ };
461
+ }
462
+
463
+ /**
464
+ * ✅ OPTIMISATION OPTION 2: Rendu optimisé de rectangles
465
+ * Évite les changements d'état inutiles
466
+ * @param {number} x - Position X
467
+ * @param {number} y - Position Y
468
+ * @param {number} w - Largeur
469
+ * @param {number} h - Hauteur
470
+ * @param {string} color - Couleur de remplissage
471
+ */
472
+ fillRectOptimized(x, y, w, h, color) {
473
+ // Éviter les changements d'état inutiles
474
+ if (this._stateCache.fillStyle !== color) {
475
+ this.ctx.fillStyle = color;
476
+ this._stateCache.fillStyle = color;
477
+ }
478
+ this.ctx.fillRect(x, y, w, h);
479
+ }
480
+
481
+ /**
482
+ * ✅ OPTIMISATION OPTION 2: Texte avec cache
483
+ * Cache le rendu du texte pour éviter de le redessiner à chaque frame
484
+ * @param {string} text - Texte à afficher
485
+ * @param {number} x - Position X
486
+ * @param {number} y - Position Y
487
+ * @param {string} font - Police CSS
488
+ * @param {string} color - Couleur du texte
489
+ */
490
+ fillTextCached(text, x, y, font, color) {
491
+ const key = `${text}_${font}_${color}`;
492
+
493
+ if (!this.textCache.has(key)) {
494
+ // Rendu dans un canvas temporaire
495
+ const temp = document.createElement('canvas');
496
+ const tempCtx = temp.getContext('2d', { alpha: false });
497
+ tempCtx.font = font;
498
+
499
+ const metrics = tempCtx.measureText(text);
500
+ temp.width = Math.ceil(metrics.width);
501
+ temp.height = Math.ceil(parseInt(font) * 1.2);
502
+
503
+ tempCtx.font = font;
504
+ tempCtx.fillStyle = color;
505
+ tempCtx.textBaseline = 'top';
506
+ tempCtx.fillText(text, 0, 0);
507
+
508
+ this.textCache.set(key, {
509
+ canvas: temp,
510
+ width: temp.width,
511
+ height: temp.height,
512
+ baseline: parseInt(font)
513
+ });
514
+ }
515
+
516
+ const cached = this.textCache.get(key);
517
+ this.ctx.drawImage(cached.canvas, x, y - cached.baseline);
518
+ }
519
+
520
+ /**
521
+ * ✅ OPTIMISATION OPTION 5: Rendu batché pour plusieurs rectangles
522
+ * Regroupe les rectangles par couleur pour réduire les appels draw
523
+ * @param {Array} rects - Tableau d'objets {x, y, width, height, color}
524
+ */
525
+ batchRect(rects) {
526
+ if (!rects || rects.length === 0) return;
527
+
528
+ // Regrouper par couleur
529
+ const batches = new Map();
530
+
531
+ rects.forEach(rect => {
532
+ if (!batches.has(rect.color)) {
533
+ batches.set(rect.color, []);
534
+ }
535
+ batches.get(rect.color).push(rect);
536
+ });
537
+
538
+ // Dessiner par batch
539
+ batches.forEach((batchRects, color) => {
540
+ this.ctx.fillStyle = color;
541
+
542
+ // Utiliser un seul path pour tous les rectangles de même couleur
543
+ this.ctx.beginPath();
544
+ batchRects.forEach(rect => {
545
+ this.ctx.rect(rect.x, rect.y, rect.width, rect.height);
546
+ });
547
+ this.ctx.fill();
548
+ });
549
+ }
550
+
551
+ /**
552
+ * ✅ OPTIMISATION OPTION 5: Utiliser ImageData pour les mises à jour fréquentes
553
+ * @param {number} x - Position X
554
+ * @param {number} y - Position Y
555
+ * @param {number} width - Largeur
556
+ * @param {number} height - Hauteur
557
+ * @param {Function} drawFn - Fonction pour manipuler les pixels
558
+ */
559
+ updateRegion(x, y, width, height, drawFn) {
560
+ const imageData = this.ctx.getImageData(x, y, width, height);
561
+ const data = imageData.data;
562
+
563
+ // Manipuler directement les pixels
564
+ drawFn(data, width, height);
565
+
566
+ this.ctx.putImageData(imageData, x, y);
567
+ }
568
+
569
+ /**
570
+ * ✅ OPTIMISATION OPTION 2: Flush du buffer pour le double buffering
571
+ */
572
+ flush() {
573
+ if (this.optimizations.useDoubleBuffering && this._bufferCtx) {
574
+ // Dessiner le buffer sur le canvas réel
575
+ this.ctx.drawImage(this._doubleBuffer, 0, 0);
576
+ // Effacer le buffer pour le prochain frame
577
+ this._bufferCtx.clearRect(0, 0, this.width, this.height);
578
+ }
579
+ }
580
+
581
+ /**
582
+ * ✅ OPTIMISATION OPTION 5: Rendu optimisé avec viewport culling
583
+ * @private
584
+ */
585
+ _renderOptimized() {
586
+ const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
587
+
588
+ if (!ctx) return;
589
+
590
+ // Clear le canvas
591
+ ctx.clearRect(0, 0, this.width, this.height);
592
+
593
+ // Séparer les composants fixes et scrollables
594
+ const scrollableComponents = [];
595
+ const fixedComponents = [];
596
+
597
+ for (let comp of this.components) {
598
+ if (this.isFixedComponent(comp)) {
599
+ fixedComponents.push(comp);
600
+ } else {
601
+ scrollableComponents.push(comp);
602
+ }
603
+ }
604
+
605
+ // Rendu des composants scrollables avec viewport culling optimisé
606
+ ctx.save();
607
+ ctx.translate(0, this.scrollOffset);
608
+
609
+ // ✅ OPTIMISATION: Utiliser le spatial partitioning si activé
610
+ if (this.optimizations.useSpatialPartitioning && this._spatialGrid) {
611
+ const visibleComps = this._spatialGrid.getVisible(-this.scrollOffset);
612
+ for (let comp of visibleComps) {
613
+ if (comp.visible) {
614
+ comp.draw(ctx);
615
+ }
616
+ }
617
+ } else {
618
+ // Rendu standard avec culling simple
619
+ for (let comp of scrollableComponents) {
620
+ if (comp.visible) {
621
+ const screenY = comp.y + this.scrollOffset;
622
+ const isInViewport = screenY + comp.height >= -100 && screenY <= this.height + 100;
623
+
624
+ if (isInViewport) {
625
+ comp.draw(ctx);
626
+ }
627
+ }
628
+ }
629
+ }
630
+
631
+ ctx.restore();
632
+
633
+ // Rendu des composants fixes
634
+ for (let comp of fixedComponents) {
635
+ if (comp.visible) {
636
+ comp.draw(ctx);
637
+ }
638
+ }
639
+
640
+ // Flush si on utilise le double buffering
641
+ if (this.optimizations.useDoubleBuffering) {
642
+ this.flush();
643
+ }
644
+ }
645
+
646
+ /**
647
+ * ✅ OPTIMISATION OPTION 2: Rendu partiel (seulement les composants sales)
648
+ * @private
649
+ */
650
+ _renderDirtyComponents() {
651
+ const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
652
+
653
+ if (!ctx) return;
654
+
655
+ this.dirtyComponents.forEach(comp => {
656
+ if (comp.visible) {
657
+ const isFixed = this.isFixedComponent(comp);
658
+ const y = isFixed ? comp.y : comp.y + this.scrollOffset;
659
+
660
+ // Nettoyer la zone avant de redessiner
661
+ ctx.clearRect(
662
+ comp.x - 1,
663
+ y - 1,
664
+ comp.width + 2,
665
+ comp.height + 2
666
+ );
667
+
668
+ ctx.save();
669
+ if (!isFixed) ctx.translate(0, this.scrollOffset);
670
+ comp.draw(ctx);
671
+ ctx.restore();
672
+
673
+ if (comp.markClean) comp.markClean();
674
+ }
675
+ });
676
+
677
+ this.dirtyComponents.clear();
678
+
679
+ // Flush si on utilise le double buffering
680
+ if (this.optimizations.useDoubleBuffering) {
681
+ this.flush();
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Active/désactive une optimisation spécifique
687
+ * @param {string} optimization - Nom de l'optimisation
688
+ * @param {boolean} enabled - true pour activer, false pour désactiver
689
+ */
690
+ setOptimization(optimization, enabled) {
691
+ if (this.optimizations.hasOwnProperty(optimization)) {
692
+ this.optimizations[optimization] = enabled;
693
+
694
+ switch(optimization) {
695
+ case 'useDoubleBuffering':
696
+ if (enabled && !this._bufferCtx) {
697
+ this._initDoubleBuffer();
698
+ }
699
+ break;
700
+ case 'useSpatialPartitioning':
701
+ if (enabled && !this._spatialGrid) {
702
+ this._initSpatialPartitioning();
703
+ }
704
+ break;
705
+ }
706
+
707
+ // Marquer tous les composants comme sales pour forcer un redessin complet
708
+ this.components.forEach(comp => this.markComponentDirty(comp));
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Obtient l'état des optimisations
714
+ * @returns {Object} État des optimisations
715
+ */
716
+ getOptimizations() {
717
+ return { ...this.optimizations };
338
718
  }
339
719
 
340
720
  /**
@@ -478,7 +858,17 @@ class CanvasFramework {
478
858
  requestAnimationFrame(fade);
479
859
  } else {
480
860
  this._splashFinished = true;
481
- this.ctx.clearRect(0, 0, this.width, this.height);
861
+ // AJOUTER : Réinitialiser complètement le contexte
862
+ this.ctx.clearRect(0, 0, this.width, this.height);
863
+ this.ctx.globalAlpha = 1;
864
+ this.ctx.textAlign = 'start'; // ← IMPORTANT
865
+ this.ctx.textBaseline = 'alphabetic'; // ← IMPORTANT
866
+ this.ctx.font = '10px sans-serif'; // Valeur par défaut
867
+ this.ctx.fillStyle = '#000000';
868
+ this.ctx.strokeStyle = '#000000';
869
+ this.ctx.lineWidth = 1;
870
+ this.ctx.lineCap = 'butt';
871
+ this.ctx.lineJoin = 'miter';
482
872
  }
483
873
  };
484
874
 
@@ -855,13 +1245,16 @@ class CanvasFramework {
855
1245
  this.canvas.style.width = this.width + 'px';
856
1246
  this.canvas.style.height = this.height + 'px';
857
1247
 
1248
+ // ✅ AJOUTER: Appliquer le background au style CSS
1249
+ this.canvas.style.backgroundColor = this.backgroundColor;
858
1250
  // Échelle uniquement pour Canvas 2D
859
- if (!this.useWebGL) {
1251
+ this.ctx.scale(this.dpr, this.dpr);
1252
+ /*if (!this.useWebGL) {
860
1253
  this.ctx.scale(this.dpr, this.dpr);
861
1254
  } else {
862
1255
  // WebGL gère le DPR automatiquement via la matrice de projection
863
1256
  this.ctx.updateProjectionMatrix();
864
- }
1257
+ }*/
865
1258
  }
866
1259
 
867
1260
  setupEventListeners() {
@@ -1573,8 +1966,25 @@ class CanvasFramework {
1573
1966
  }
1574
1967
  return Math.max(0, maxY - this.height + 50);
1575
1968
  }*/
1576
-
1577
- handleResize() {
1969
+
1970
+ handleResize() {
1971
+ if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
1972
+
1973
+ this.resizeTimeout = setTimeout(() => {
1974
+ this.width = window.innerWidth;
1975
+ this.height = window.innerHeight;
1976
+ this.setupCanvas(); // ← Fait déjà tout le boulot
1977
+
1978
+ for (const comp of this.components) {
1979
+ if (comp._resize) {
1980
+ comp._resize(this.width, this.height);
1981
+ }
1982
+ }
1983
+ this._maxScrollDirty = true;
1984
+ }, 150);
1985
+ }
1986
+
1987
+ /*handleResize() {
1578
1988
  if (this.resizeTimeout) clearTimeout(this.resizeTimeout); // ✅ AJOUTER
1579
1989
 
1580
1990
  this.resizeTimeout = setTimeout(() => { // ✅ AJOUTER
@@ -1591,7 +2001,7 @@ class CanvasFramework {
1591
2001
  this._maxScrollDirty = true; // ✅ AJOUTER
1592
2002
  }
1593
2003
  }, 150); // ✅ AJOUTER (throttle 150ms)
1594
- }
2004
+ }*/
1595
2005
 
1596
2006
  add(component) {
1597
2007
  this.components.push(component);
@@ -1780,111 +2190,116 @@ class CanvasFramework {
1780
2190
  }
1781
2191
 
1782
2192
  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
- }
1796
-
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
- }
1832
-
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
- }
2193
+ const render = () => {
2194
+ if (!this._splashFinished) {
2195
+ requestAnimationFrame(render);
2196
+ return;
2197
+ }
2198
+
2199
+ // 1️⃣ Scroll inertia
2200
+ if (Math.abs(this.scrollVelocity) > 0.1 && !this.isDragging) {
2201
+ this.scrollOffset += this.scrollVelocity;
2202
+ this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -this.getMaxScroll());
2203
+ this.scrollVelocity *= this.scrollFriction;
2204
+ } else {
2205
+ this.scrollVelocity = 0;
2206
+ }
2207
+
2208
+ // 2️⃣ Clear canvas AVEC BACKGROUND
2209
+ // Remplacer clearRect par fillRect avec ta couleur
2210
+ this.ctx.fillStyle = this.backgroundColor || '#ffffff';
2211
+ this.ctx.fillRect(0, 0, this.width, this.height);
2212
+
2213
+ // 3️⃣ Transition handling
2214
+ if (this.transitionState.isTransitioning) {
2215
+ this.updateTransition();
2216
+ } else if (this.optimizationEnabled && this.dirtyComponents.size > 0) {
2217
+ // Dirty components redraw
2218
+ for (let comp of this.dirtyComponents) {
2219
+ if (comp.visible) {
2220
+ const isFixed = this.isFixedComponent(comp);
2221
+ const y = isFixed ? comp.y : comp.y + this.scrollOffset;
2222
+
2223
+ // Pour les composants sales, nettoyer la zone AVEC le background
2224
+ this.ctx.fillStyle = this.backgroundColor || '#ffffff';
2225
+ this.ctx.fillRect(comp.x - 2, y - 2, comp.width + 4, comp.height + 4);
2226
+
2227
+ this.ctx.save();
2228
+ if (!isFixed) this.ctx.translate(0, this.scrollOffset);
2229
+ comp.draw(this.ctx);
2230
+ this.ctx.restore();
2231
+
2232
+ // Overflow indicator style Flutter
2233
+ const overflow = comp.getOverflow?.();
2234
+ if (comp.markClean) comp.markClean();
2235
+ }
2236
+ }
2237
+ this.dirtyComponents.clear();
2238
+ } else {
2239
+ // Full redraw
2240
+ const scrollableComponents = [];
2241
+ const fixedComponents = [];
2242
+
2243
+ for (let comp of this.components) {
2244
+ if (this.isFixedComponent(comp)) fixedComponents.push(comp);
2245
+ else scrollableComponents.push(comp);
2246
+ }
2247
+
2248
+ // Scrollable
2249
+ this.ctx.save();
2250
+ this.ctx.translate(0, this.scrollOffset);
2251
+ for (let comp of scrollableComponents) {
2252
+ if (comp.visible) {
2253
+ // ✅ Viewport culling : ne dessiner que ce qui est visible
2254
+ const screenY = comp.y + this.scrollOffset;
2255
+ const isInViewport = screenY + comp.height >= -100 && screenY <= this.height + 100;
2256
+
2257
+ if (isInViewport) {
2258
+ comp.draw(this.ctx);
2259
+ }
2260
+ }
2261
+ }
2262
+ this.ctx.restore();
2263
+
2264
+ // Fixed
2265
+ for (let comp of fixedComponents) {
2266
+ if (comp.visible) {
2267
+ comp.draw(this.ctx);
2268
+ }
2269
+ }
2270
+ }
2271
+
2272
+ // 4️⃣ FPS
2273
+ this._frames++;
2274
+ const now = performance.now();
2275
+ if (now - this._lastFpsTime >= 1000) {
2276
+ this.fps = this._frames;
2277
+ this._frames = 0;
2278
+ this._lastFpsTime = now;
2279
+ }
2280
+
2281
+ if (this.showFps) {
2282
+ this.ctx.save();
2283
+ this.ctx.fillStyle = 'lime';
2284
+ this.ctx.font = '16px monospace';
2285
+ this.ctx.fillText(`FPS: ${this.fps}`, 10, 20);
2286
+ this.ctx.restore();
2287
+ }
2288
+
2289
+ if (this.debbug) {
2290
+ this.drawOverflowIndicators();
2291
+ }
2292
+
2293
+ // ✅ AJOUTER: Marquer le premier rendu
2294
+ if (!this._firstRenderDone && this.components.length > 0) {
2295
+ this._markFirstRender();
2296
+ }
2297
+
2298
+ requestAnimationFrame(render);
2299
+ };
2300
+
2301
+ render();
2302
+ }
1888
2303
 
1889
2304
  // ✅ AJOUTER: Afficher les métriques à l'écran
1890
2305
  displayMetrics() {