canvasframework 0.4.10 → 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'
@@ -223,6 +265,7 @@ class CanvasFramework {
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
  /**
@@ -865,6 +1245,8 @@ class CanvasFramework {
865
1245
  this.canvas.style.width = this.width + 'px';
866
1246
  this.canvas.style.height = this.height + 'px';
867
1247
 
1248
+ // ✅ AJOUTER: Appliquer le background au style CSS
1249
+ this.canvas.style.backgroundColor = this.backgroundColor;
868
1250
  // Échelle uniquement pour Canvas 2D
869
1251
  this.ctx.scale(this.dpr, this.dpr);
870
1252
  /*if (!this.useWebGL) {
@@ -1808,111 +2190,116 @@ class CanvasFramework {
1808
2190
  }
1809
2191
 
1810
2192
  startRenderLoop() {
1811
- const render = () => {
1812
- if (!this._splashFinished) {
1813
- requestAnimationFrame(render);
1814
- return;
1815
- }
1816
- // 1️⃣ Scroll inertia
1817
- if (Math.abs(this.scrollVelocity) > 0.1 && !this.isDragging) {
1818
- this.scrollOffset += this.scrollVelocity;
1819
- this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -this.getMaxScroll());
1820
- this.scrollVelocity *= this.scrollFriction;
1821
- } else {
1822
- this.scrollVelocity = 0;
1823
- }
1824
-
1825
- // 2️⃣ Clear canvas
1826
- this.ctx.clearRect(0, 0, this.width, this.height);
1827
-
1828
- // 3️⃣ Transition handling
1829
- if (this.transitionState.isTransitioning) {
1830
- this.updateTransition();
1831
- } else if (this.optimizationEnabled && this.dirtyComponents.size > 0) {
1832
- // Dirty components redraw
1833
- for (let comp of this.dirtyComponents) {
1834
- if (comp.visible) {
1835
- const isFixed = this.isFixedComponent(comp);
1836
- const y = isFixed ? comp.y : comp.y + this.scrollOffset;
1837
-
1838
- this.ctx.clearRect(comp.x - 2, y - 2, comp.width + 4, comp.height + 4);
1839
-
1840
- this.ctx.save();
1841
- if (!isFixed) this.ctx.translate(0, this.scrollOffset);
1842
- comp.draw(this.ctx);
1843
- this.ctx.restore();
1844
-
1845
- // Overflow indicator style Flutter
1846
- const overflow = comp.getOverflow?.();
1847
- if (comp.markClean) comp.markClean();
1848
- }
1849
- }
1850
- this.dirtyComponents.clear();
1851
- } else {
1852
- // Full redraw
1853
- const scrollableComponents = [];
1854
- const fixedComponents = [];
1855
-
1856
- for (let comp of this.components) {
1857
- if (this.isFixedComponent(comp)) fixedComponents.push(comp);
1858
- else scrollableComponents.push(comp);
1859
- }
1860
-
1861
- // Scrollable
1862
- this.ctx.save();
1863
- this.ctx.translate(0, this.scrollOffset);
1864
- for (let comp of scrollableComponents) {
1865
- if (comp.visible) {
1866
- // ✅ Viewport culling : ne dessiner que ce qui est visible
1867
- const screenY = comp.y + this.scrollOffset;
1868
- const isInViewport = screenY + comp.height >= -100 && screenY <= this.height + 100;
1869
-
1870
- if (isInViewport) {
1871
- comp.draw(this.ctx);
1872
- }
1873
- }
1874
- }
1875
- this.ctx.restore();
1876
-
1877
- // Fixed
1878
- for (let comp of fixedComponents) {
1879
- if (comp.visible) {
1880
- comp.draw(this.ctx);
1881
- }
1882
- }
1883
- }
1884
-
1885
- // 4️⃣ FPS
1886
- this._frames++;
1887
- const now = performance.now();
1888
- if (now - this._lastFpsTime >= 1000) {
1889
- this.fps = this._frames;
1890
- this._frames = 0;
1891
- this._lastFpsTime = now;
1892
- }
1893
-
1894
- if (this.showFps) {
1895
- this.ctx.save();
1896
- this.ctx.fillStyle = 'lime';
1897
- this.ctx.font = '16px monospace';
1898
- this.ctx.fillText(`FPS: ${this.fps}`, 10, 20);
1899
- this.ctx.restore();
1900
- }
1901
-
1902
- if (this.debbug) {
1903
- this.drawOverflowIndicators();
1904
- }
1905
-
1906
- // ✅ AJOUTER: Marquer le premier rendu
1907
- if (!this._firstRenderDone && this.components.length > 0) {
1908
- this._markFirstRender();
1909
- }
1910
-
1911
- requestAnimationFrame(render);
1912
- };
1913
-
1914
- render();
1915
- }
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
+ }
1916
2303
 
1917
2304
  // ✅ AJOUTER: Afficher les métriques à l'écran
1918
2305
  displayMetrics() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",