canvasframework 0.4.11 → 0.5.0

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,21 +155,48 @@ class CanvasFramework {
155
155
  constructor(canvasId, options = {}) {
156
156
  // ✅ AJOUTER: Démarrer le chronomètre
157
157
  const startTime = performance.now();
158
-
158
+
159
159
  // ✅ OPTIMISATION OPTION 5: Contexte Canvas optimisé
160
160
  this.canvas = document.getElementById(canvasId);
161
- this.ctx = this.canvas.getContext('2d', {
161
+ /*this.ctx = this.canvas.getContext('2d', {
162
162
  alpha: false, // ✅ Gain de 30% de performance
163
163
  desynchronized: true, // ✅ Bypass la queue du navigateur
164
164
  willReadFrequently: false
165
- });
166
-
167
- this.backgroundColor = options.backgroundColor || '#f5f5f5'; // Blanc par défaut
165
+ });*/
166
+ // NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
167
+ this.useWebGL = options.useWebGL ?? false; // utilise la valeur si fournie, sinon false
168
+
169
+ // Initialiser le contexte approprié
170
+ if (this.useWebGL) {
171
+ try {
172
+ this.ctx = new WebGLCanvasAdapter(this.canvas, {
173
+ dpr: this.dpr,
174
+ alpha: false
175
+ });
176
+ } catch (err) {
177
+ console.warn("Échec de l’initialisation WebGLCanvasAdapter → fallback Canvas 2D", err);
178
+ this.ctx = this.canvas.getContext('2d', {
179
+ alpha: false,
180
+ desynchronized: true,
181
+ willReadFrequently: false
182
+ });
183
+ this.useWebGL = false;
184
+ }
185
+ } else {
186
+ this.ctx = this.canvas.getContext('2d', {
187
+ alpha: false,
188
+ desynchronized: true,
189
+ willReadFrequently: false
190
+ });
191
+ }
192
+ //this.ctx.scale(this.dpr, this.dpr);
168
193
 
194
+ this.backgroundColor = options.backgroundColor || '#f5f5f5'; // Blanc par défaut
195
+
169
196
  this.width = window.innerWidth;
170
197
  this.height = window.innerHeight;
171
198
  this.dpr = window.devicePixelRatio || 1;
172
-
199
+
173
200
  // ✅ OPTIMISATION OPTION 2: Configuration des optimisations
174
201
  this.optimizations = {
175
202
  enabled: options.optimizations !== false, // Activé par défaut
@@ -179,7 +206,7 @@ class CanvasFramework {
179
206
  useSpatialPartitioning: false, // Désactivé par défaut (à activer si beaucoup de composants)
180
207
  useImageDataOptimization: true
181
208
  };
182
-
209
+
183
210
  // ✅ OPTIMISATION OPTION 2: Cache pour éviter les changements d'état inutiles
184
211
  this._stateCache = {
185
212
  fillStyle: null,
@@ -190,18 +217,22 @@ class CanvasFramework {
190
217
  lineWidth: null,
191
218
  globalAlpha: 1
192
219
  };
193
-
220
+
194
221
  // ✅ OPTIMISATION OPTION 2: Cache des images/textes
195
222
  this.imageCache = new Map();
196
223
  this.textCache = new Map();
197
-
224
+
198
225
  // ✅ OPTIMISATION OPTION 2: Double buffering
199
226
  this._doubleBuffer = null;
200
227
  this._bufferCtx = null;
201
228
  if (this.optimizations.useDoubleBuffering) {
202
229
  this._initDoubleBuffer();
203
230
  }
204
-
231
+
232
+ // Scroll Worker
233
+ this.scrollWorker = this.createScrollWorker();
234
+ this.scrollWorker.onmessage = this.handleScrollWorkerMessage.bind(this);
235
+
205
236
  this.splashOptions = {
206
237
  enabled: options.splash?.enabled === true, // false par défaut
207
238
  duration: options.splash?.duration || 700,
@@ -220,7 +251,7 @@ class CanvasFramework {
220
251
  logoWidth: options.splash?.logoWidth || 100,
221
252
  logoHeight: options.splash?.logoHeight || 100
222
253
  };
223
-
254
+
224
255
  // ✅ MODIFIER : Vérifier si le splash est activé
225
256
  if (this.splashOptions.enabled) {
226
257
  this.showSplashScreen();
@@ -229,7 +260,9 @@ class CanvasFramework {
229
260
  }
230
261
 
231
262
  this.platform = this.detectPlatform();
232
-
263
+ setTimeout(() => {
264
+ this.initScrollWorker();
265
+ }, 100);
233
266
  // État actuel + préférence
234
267
  this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
235
268
  this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
@@ -252,20 +285,7 @@ class CanvasFramework {
252
285
 
253
286
  //this.applyThemeFromSystem();
254
287
  this.state = {};
255
- // NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
256
- this.useWebGL = options.useWebGL !== false; // true par défaut
257
- // Initialiser le contexte approprié
258
- /*if (this.useWebGL) {
259
- try {
260
- this.ctx = new WebGLCanvasAdapter(this.canvas);
261
- } catch (e) {
262
- this.ctx = this.canvas.getContext('2d');
263
- this.useWebGL = false;
264
- }
265
- } else {
266
- this.ctx = this.canvas.getContext('2d');
267
- }*/
268
-
288
+
269
289
  // Calcule FPS
270
290
  this.fps = 0;
271
291
  this._frames = 0;
@@ -332,10 +352,10 @@ class CanvasFramework {
332
352
  };
333
353
 
334
354
  this.setupCanvas();
335
-
355
+
336
356
  // ✅ OPTIMISATION OPTION 5: Désactiver l'antialiasing pour meilleures performances
337
357
  this._disableImageSmoothing();
338
-
358
+
339
359
  this.setupEventListeners();
340
360
  this.setupHistoryListener();
341
361
 
@@ -381,17 +401,340 @@ class CanvasFramework {
381
401
  // ✅ AJOUTER: Marquer le premier rendu
382
402
  this._firstRenderDone = false;
383
403
  this._startupStartTime = startTime;
384
-
404
+
385
405
  // ✅ OPTIMISATION OPTION 5: Partition spatiale pour le culling (optionnel)
386
406
  if (this.optimizations.useSpatialPartitioning) {
387
407
  this._initSpatialPartitioning();
388
408
  }
389
409
  }
390
-
410
+
391
411
  /**
412
+ * Crée un élément DOM temporaire, l'ajoute au body, exécute une callback, puis le supprime
413
+ * @param {string} tagName - 'input', 'select', 'textarea', etc.
414
+ * @param {Object} props - propriétés de base (type, value, accept, placeholder...)
415
+ * @param {Function} onResult - callback quand l'élément change ou blur (reçoit l'élément)
416
+ * @param {Object} [position=null] - {left, top, width, height} en pixels
417
+ * @param {Object} [attributes={}] - attributs supplémentaires (id, className, data-*, etc.)
418
+ * @returns {HTMLElement} L'élément créé (avant suppression)
419
+ */
420
+ createTemporaryDomElement(tagName, props = {}, onResult, position = null, attributes = {}) {
421
+ const el = document.createElement(tagName);
422
+
423
+ // Appliquer les propriétés de base
424
+ Object.assign(el, props);
425
+
426
+ // Appliquer les attributs personnalisés (id, class, data-*, etc.)
427
+ Object.entries(attributes).forEach(([key, value]) => {
428
+ if (key === 'className') {
429
+ el.className = value; // className est spécial en JS
430
+ } else {
431
+ el.setAttribute(key, value);
432
+ }
433
+ });
434
+
435
+ // Styles de base pour le rendre invisible
436
+ el.style.position = 'absolute';
437
+ el.style.opacity = '0';
438
+ el.style.zIndex = '9999';
439
+
440
+ // Positionnement optionnel
441
+ if (position) {
442
+ Object.assign(el.style, {
443
+ left: `${position.left}px`,
444
+ top: `${position.top}px`,
445
+ width: `${position.width}px`,
446
+ height: `${position.height}px`
447
+ });
448
+ }
449
+
450
+ document.body.appendChild(el);
451
+
452
+ const cleanup = () => {
453
+ el.remove();
454
+ document.removeEventListener('focusout', cleanup);
455
+ };
456
+
457
+ // Événements
458
+ el.addEventListener('change', (e) => {
459
+ onResult(e.target);
460
+ cleanup();
461
+ });
462
+
463
+ el.addEventListener('blur', cleanup);
464
+
465
+ // Focus automatique pour les champs saisissables
466
+ if (['input', 'select', 'textarea'].includes(tagName.toLowerCase())) {
467
+ el.focus();
468
+ }
469
+
470
+ return el;
471
+ }
472
+
473
+ /**
474
+ * Crée le Worker pour le calcul du scroll
475
+ */
476
+ createScrollWorker() {
477
+ const workerCode = `
478
+ let state = {
479
+ scrollOffset: 0,
480
+ scrollVelocity: 0,
481
+ scrollFriction: 0.95,
482
+ isDragging: false,
483
+ maxScroll: 0,
484
+ height: 0,
485
+ lastTouchY: 0,
486
+ components: []
487
+ };
488
+
489
+ const FIXED_COMPONENT_TYPES = [
490
+ 'AppBar', 'BottomNavigationBar', 'Drawer', 'Dialog', 'Modal',
491
+ 'Tabs', 'FAB', 'Toast', 'Camera', 'QRCodeReader', 'Banner',
492
+ 'SliverAppBar', 'BottomSheet', 'ContextMenu', 'OpenStreetMap', 'SelectDialog'
493
+ ];
494
+
495
+ const calculateMaxScroll = () => {
496
+ let maxY = 0;
497
+
498
+ for (const comp of state.components) {
499
+ if (FIXED_COMPONENT_TYPES.includes(comp.type) || !comp.visible) continue;
500
+ const bottom = comp.y + comp.height;
501
+ if (bottom > maxY) maxY = bottom;
502
+ }
503
+
504
+ return Math.max(0, maxY - state.height + 50);
505
+ };
506
+
507
+ const updateInertia = () => {
508
+ if (Math.abs(state.scrollVelocity) > 0.1 && !state.isDragging) {
509
+ state.scrollOffset += state.scrollVelocity;
510
+ state.maxScroll = calculateMaxScroll();
511
+ state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
512
+ state.scrollVelocity *= state.scrollFriction;
513
+ } else {
514
+ state.scrollVelocity = 0;
515
+ }
516
+
517
+ return {
518
+ scrollOffset: state.scrollOffset,
519
+ scrollVelocity: state.scrollVelocity,
520
+ maxScroll: state.maxScroll
521
+ };
522
+ };
523
+
524
+ const handleTouchMove = (deltaY) => {
525
+ if (state.isDragging) {
526
+ state.scrollOffset += deltaY;
527
+ state.maxScroll = calculateMaxScroll();
528
+ state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
529
+ state.scrollVelocity = deltaY;
530
+ return {
531
+ scrollOffset: state.scrollOffset,
532
+ scrollVelocity: state.scrollVelocity,
533
+ maxScroll: state.maxScroll
534
+ };
535
+ }
536
+ return null;
537
+ };
538
+
539
+ self.onmessage = (e) => {
540
+ const { type, payload } = e.data;
541
+
542
+ switch (type) {
543
+ case 'INIT':
544
+ state = {
545
+ ...state,
546
+ ...payload
547
+ };
548
+ state.maxScroll = calculateMaxScroll();
549
+ self.postMessage({
550
+ type: 'INITIALIZED',
551
+ payload: {
552
+ scrollOffset: state.scrollOffset,
553
+ maxScroll: state.maxScroll
554
+ }
555
+ });
556
+ break;
557
+
558
+ case 'UPDATE_COMPONENTS':
559
+ state.components = payload.components;
560
+ state.maxScroll = calculateMaxScroll();
561
+ self.postMessage({
562
+ type: 'MAX_SCROLL_UPDATED',
563
+ payload: { maxScroll: state.maxScroll }
564
+ });
565
+ break;
566
+
567
+ case 'UPDATE_DIMENSIONS':
568
+ state.height = payload.height;
569
+ state.maxScroll = calculateMaxScroll();
570
+ self.postMessage({
571
+ type: 'MAX_SCROLL_UPDATED',
572
+ payload: { maxScroll: state.maxScroll }
573
+ });
574
+ break;
575
+
576
+ case 'SET_DRAGGING':
577
+ state.isDragging = payload.isDragging;
578
+ if (!payload.isDragging) {
579
+ state.scrollVelocity = payload.lastVelocity || 0;
580
+ } else {
581
+ state.lastTouchY = payload.lastTouchY || 0;
582
+ }
583
+ break;
584
+
585
+ case 'HANDLE_TOUCH_MOVE':
586
+ const result = handleTouchMove(payload.deltaY);
587
+ if (result) {
588
+ self.postMessage({
589
+ type: 'SCROLL_UPDATED',
590
+ payload: result
591
+ });
592
+ }
593
+ break;
594
+
595
+ case 'UPDATE_INERTIA':
596
+ const inertiaResult = updateInertia();
597
+ self.postMessage({
598
+ type: 'SCROLL_UPDATED',
599
+ payload: inertiaResult
600
+ });
601
+ break;
602
+
603
+ case 'SET_SCROLL_OFFSET':
604
+ state.scrollOffset = payload.scrollOffset;
605
+ state.maxScroll = calculateMaxScroll();
606
+ state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
607
+ self.postMessage({
608
+ type: 'SCROLL_UPDATED',
609
+ payload: {
610
+ scrollOffset: state.scrollOffset,
611
+ maxScroll: state.maxScroll
612
+ }
613
+ });
614
+ break;
615
+
616
+ case 'GET_STATE':
617
+ self.postMessage({
618
+ type: 'STATE',
619
+ payload: {
620
+ scrollOffset: state.scrollOffset,
621
+ scrollVelocity: state.scrollVelocity,
622
+ maxScroll: state.maxScroll,
623
+ isDragging: state.isDragging
624
+ }
625
+ });
626
+ break;
627
+ }
628
+ };
629
+ `;
630
+
631
+ const blob = new Blob([workerCode], {
632
+ type: 'application/javascript'
633
+ });
634
+ return new Worker(URL.createObjectURL(blob));
635
+ }
636
+
637
+ /**
638
+ * Gère les messages du Scroll Worker
639
+ */
640
+ handleScrollWorkerMessage(e) {
641
+ const {
642
+ type,
643
+ payload
644
+ } = e.data;
645
+
646
+ switch (type) {
647
+ case 'SCROLL_UPDATED':
648
+ this.scrollOffset = payload.scrollOffset;
649
+ this.scrollVelocity = payload.scrollVelocity;
650
+ // ✅ CORRECTION IMPORTANTE : Vider le cache dirty pendant le scroll
651
+ if (Math.abs(payload.scrollVelocity) > 0.5) {
652
+ this.dirtyComponents.clear();
653
+ }
654
+ // Mettre à jour le cache
655
+ this._cachedMaxScroll = payload.maxScroll;
656
+ this._maxScrollDirty = false;
657
+
658
+ // Marquer les composants comme sales pour mise à jour visuelle
659
+ if (Math.abs(payload.scrollVelocity) > 0) {
660
+ this.components.forEach(comp => {
661
+ if (!this.isFixedComponent(comp)) {
662
+ this.markComponentDirty(comp);
663
+ }
664
+ });
665
+ }
666
+ break;
667
+
668
+ case 'MAX_SCROLL_UPDATED':
669
+ this._cachedMaxScroll = payload.maxScroll;
670
+ this._maxScrollDirty = false;
671
+ break;
672
+
673
+ case 'INITIALIZED':
674
+ this.scrollOffset = payload.scrollOffset;
675
+ this._cachedMaxScroll = payload.maxScroll;
676
+ this._maxScrollDirty = false;
677
+ break;
678
+
679
+ case 'STATE':
680
+ // Synchroniser l'état local
681
+ this.scrollOffset = payload.scrollOffset;
682
+ this.scrollVelocity = payload.scrollVelocity;
683
+ this.isDragging = payload.isDragging;
684
+ this._cachedMaxScroll = payload.maxScroll;
685
+ this._maxScrollDirty = false;
686
+ break;
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Initialise le Scroll Worker avec les données actuelles
692
+ */
693
+ initScrollWorker() {
694
+ const componentsData = this.components.map(comp => ({
695
+ type: comp.constructor.name,
696
+ y: comp.y,
697
+ height: comp.height,
698
+ visible: comp.visible
699
+ }));
700
+
701
+ this.scrollWorker.postMessage({
702
+ type: 'INIT',
703
+ payload: {
704
+ scrollOffset: this.scrollOffset,
705
+ scrollVelocity: this.scrollVelocity,
706
+ scrollFriction: this.scrollFriction,
707
+ isDragging: this.isDragging,
708
+ height: this.height,
709
+ components: componentsData
710
+ }
711
+ });
712
+ }
713
+
714
+ /**
715
+ * Met à jour les composants dans le Worker
716
+ */
717
+ updateScrollWorkerComponents() {
718
+ const componentsData = this.components.map(comp => ({
719
+ type: comp.constructor.name,
720
+ y: comp.y,
721
+ height: comp.height,
722
+ visible: comp.visible
723
+ }));
724
+
725
+ this.scrollWorker.postMessage({
726
+ type: 'UPDATE_COMPONENTS',
727
+ payload: {
728
+ components: componentsData
729
+ }
730
+ });
731
+ }
732
+
733
+ /**
392
734
  * Initialise le double buffering pour éviter le flickering
393
735
  * @private
394
736
  */
737
+ // Dans _initDoubleBuffer(), assurez-vous de bien configurer le contexte
395
738
  _initDoubleBuffer() {
396
739
  this._doubleBuffer = document.createElement('canvas');
397
740
  this._bufferCtx = this._doubleBuffer.getContext('2d', {
@@ -404,8 +747,13 @@ class CanvasFramework {
404
747
  this._doubleBuffer.style.height = this.height + 'px';
405
748
  this._bufferCtx.scale(this.dpr, this.dpr);
406
749
  this._disableImageSmoothing(this._bufferCtx);
750
+
751
+ // ✅ INITIALISER le background du buffer
752
+ this._bufferCtx.fillStyle = this.backgroundColor || '#ffffff';
753
+ this._bufferCtx.fillRect(0, 0, this.width, this.height);
407
754
  }
408
-
755
+
756
+
409
757
  /**
410
758
  * Désactive l'antialiasing pour meilleures performances
411
759
  * @private
@@ -417,7 +765,7 @@ class CanvasFramework {
417
765
  ctx.webkitImageSmoothingEnabled = false;
418
766
  ctx.mozImageSmoothingEnabled = false;
419
767
  }
420
-
768
+
421
769
  /**
422
770
  * Initialise le spatial partitioning pour le viewport culling
423
771
  * @private
@@ -431,11 +779,11 @@ class CanvasFramework {
431
779
  this._spatialGrid.grid.clear();
432
780
  components.forEach(comp => {
433
781
  if (!comp.visible) return;
434
-
782
+
435
783
  const gridX = Math.floor(comp.x / this._spatialGrid.cellSize);
436
784
  const gridY = Math.floor(comp.y / this._spatialGrid.cellSize);
437
785
  const key = `${gridX},${gridY}`;
438
-
786
+
439
787
  if (!this._spatialGrid.grid.has(key)) {
440
788
  this._spatialGrid.grid.set(key, []);
441
789
  }
@@ -446,7 +794,7 @@ class CanvasFramework {
446
794
  const visible = [];
447
795
  const startY = viewportY - 200; // Marge de 200px
448
796
  const endY = viewportY + this.height + 200;
449
-
797
+
450
798
  this._spatialGrid.grid.forEach((comps, key) => {
451
799
  comps.forEach(comp => {
452
800
  const compBottom = comp.y + comp.height;
@@ -459,7 +807,7 @@ class CanvasFramework {
459
807
  }
460
808
  };
461
809
  }
462
-
810
+
463
811
  /**
464
812
  * ✅ OPTIMISATION OPTION 2: Rendu optimisé de rectangles
465
813
  * Évite les changements d'état inutiles
@@ -477,7 +825,7 @@ class CanvasFramework {
477
825
  }
478
826
  this.ctx.fillRect(x, y, w, h);
479
827
  }
480
-
828
+
481
829
  /**
482
830
  * ✅ OPTIMISATION OPTION 2: Texte avec cache
483
831
  * Cache le rendu du texte pour éviter de le redessiner à chaque frame
@@ -489,22 +837,24 @@ class CanvasFramework {
489
837
  */
490
838
  fillTextCached(text, x, y, font, color) {
491
839
  const key = `${text}_${font}_${color}`;
492
-
840
+
493
841
  if (!this.textCache.has(key)) {
494
842
  // Rendu dans un canvas temporaire
495
843
  const temp = document.createElement('canvas');
496
- const tempCtx = temp.getContext('2d', { alpha: false });
844
+ const tempCtx = temp.getContext('2d', {
845
+ alpha: false
846
+ });
497
847
  tempCtx.font = font;
498
-
848
+
499
849
  const metrics = tempCtx.measureText(text);
500
850
  temp.width = Math.ceil(metrics.width);
501
851
  temp.height = Math.ceil(parseInt(font) * 1.2);
502
-
852
+
503
853
  tempCtx.font = font;
504
854
  tempCtx.fillStyle = color;
505
855
  tempCtx.textBaseline = 'top';
506
856
  tempCtx.fillText(text, 0, 0);
507
-
857
+
508
858
  this.textCache.set(key, {
509
859
  canvas: temp,
510
860
  width: temp.width,
@@ -512,11 +862,11 @@ class CanvasFramework {
512
862
  baseline: parseInt(font)
513
863
  });
514
864
  }
515
-
865
+
516
866
  const cached = this.textCache.get(key);
517
867
  this.ctx.drawImage(cached.canvas, x, y - cached.baseline);
518
868
  }
519
-
869
+
520
870
  /**
521
871
  * ✅ OPTIMISATION OPTION 5: Rendu batché pour plusieurs rectangles
522
872
  * Regroupe les rectangles par couleur pour réduire les appels draw
@@ -524,21 +874,21 @@ class CanvasFramework {
524
874
  */
525
875
  batchRect(rects) {
526
876
  if (!rects || rects.length === 0) return;
527
-
877
+
528
878
  // Regrouper par couleur
529
879
  const batches = new Map();
530
-
880
+
531
881
  rects.forEach(rect => {
532
882
  if (!batches.has(rect.color)) {
533
883
  batches.set(rect.color, []);
534
884
  }
535
885
  batches.get(rect.color).push(rect);
536
886
  });
537
-
887
+
538
888
  // Dessiner par batch
539
889
  batches.forEach((batchRects, color) => {
540
890
  this.ctx.fillStyle = color;
541
-
891
+
542
892
  // Utiliser un seul path pour tous les rectangles de même couleur
543
893
  this.ctx.beginPath();
544
894
  batchRects.forEach(rect => {
@@ -547,7 +897,7 @@ class CanvasFramework {
547
897
  this.ctx.fill();
548
898
  });
549
899
  }
550
-
900
+
551
901
  /**
552
902
  * ✅ OPTIMISATION OPTION 5: Utiliser ImageData pour les mises à jour fréquentes
553
903
  * @param {number} x - Position X
@@ -559,37 +909,39 @@ class CanvasFramework {
559
909
  updateRegion(x, y, width, height, drawFn) {
560
910
  const imageData = this.ctx.getImageData(x, y, width, height);
561
911
  const data = imageData.data;
562
-
912
+
563
913
  // Manipuler directement les pixels
564
914
  drawFn(data, width, height);
565
-
915
+
566
916
  this.ctx.putImageData(imageData, x, y);
567
917
  }
568
-
918
+
569
919
  /**
570
920
  * ✅ OPTIMISATION OPTION 2: Flush du buffer pour le double buffering
571
921
  */
572
922
  flush() {
573
923
  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);
924
+ // Copier tout le buffer sur le canvas réel
925
+ this.ctx.drawImage(this._doubleBuffer, 0, 0, this.width, this.height);
926
+
927
+ // Réinitialiser le buffer pour le prochain frame
928
+ this._bufferCtx.fillStyle = this.backgroundColor || '#ffffff';
929
+ this._bufferCtx.fillRect(0, 0, this.width, this.height);
578
930
  }
579
931
  }
580
-
932
+
581
933
  /**
582
934
  * ✅ OPTIMISATION OPTION 5: Rendu optimisé avec viewport culling
583
935
  * @private
584
936
  */
585
937
  _renderOptimized() {
586
938
  const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
587
-
939
+
588
940
  if (!ctx) return;
589
-
941
+
590
942
  // Clear le canvas
591
943
  ctx.clearRect(0, 0, this.width, this.height);
592
-
944
+
593
945
  // Séparer les composants fixes et scrollables
594
946
  const scrollableComponents = [];
595
947
  const fixedComponents = [];
@@ -605,7 +957,7 @@ class CanvasFramework {
605
957
  // Rendu des composants scrollables avec viewport culling optimisé
606
958
  ctx.save();
607
959
  ctx.translate(0, this.scrollOffset);
608
-
960
+
609
961
  // ✅ OPTIMISATION: Utiliser le spatial partitioning si activé
610
962
  if (this.optimizations.useSpatialPartitioning && this._spatialGrid) {
611
963
  const visibleComps = this._spatialGrid.getVisible(-this.scrollOffset);
@@ -627,7 +979,7 @@ class CanvasFramework {
627
979
  }
628
980
  }
629
981
  }
630
-
982
+
631
983
  ctx.restore();
632
984
 
633
985
  // Rendu des composants fixes
@@ -636,52 +988,83 @@ class CanvasFramework {
636
988
  comp.draw(ctx);
637
989
  }
638
990
  }
639
-
991
+
640
992
  // Flush si on utilise le double buffering
641
993
  if (this.optimizations.useDoubleBuffering) {
642
994
  this.flush();
643
995
  }
644
996
  }
645
-
997
+
646
998
  /**
647
999
  * ✅ OPTIMISATION OPTION 2: Rendu partiel (seulement les composants sales)
648
1000
  * @private
649
1001
  */
650
1002
  _renderDirtyComponents() {
651
1003
  const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
652
-
653
- if (!ctx) return;
654
-
1004
+
1005
+ if (!ctx || this.dirtyComponents.size === 0) return;
1006
+
1007
+ // Séparer les composants sales par type
1008
+ const fixedDirty = [];
1009
+ const scrollableDirty = [];
1010
+
655
1011
  this.dirtyComponents.forEach(comp => {
656
1012
  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
-
1013
+ if (this.isFixedComponent(comp)) {
1014
+ fixedDirty.push(comp);
1015
+ } else {
1016
+ scrollableDirty.push(comp);
1017
+ }
1018
+ }
1019
+ });
1020
+
1021
+ // CORRECTION : Traiter séparément pour éviter les problèmes de contexte
1022
+
1023
+ // 1. Dessiner les composants scrollables d'abord
1024
+ if (scrollableDirty.length > 0) {
1025
+ for (let comp of scrollableDirty) {
1026
+ const screenY = comp.y + this.scrollOffset;
1027
+
1028
+ // Nettoyer la zone avec le background
1029
+ ctx.save();
1030
+ ctx.fillStyle = this.backgroundColor || '#ffffff';
1031
+ ctx.fillRect(comp.x - 2, screenY - 2, comp.width + 4, comp.height + 4);
1032
+ ctx.restore();
1033
+
1034
+ // Dessiner le composant avec translation
668
1035
  ctx.save();
669
- if (!isFixed) ctx.translate(0, this.scrollOffset);
1036
+ ctx.translate(0, this.scrollOffset);
670
1037
  comp.draw(ctx);
671
1038
  ctx.restore();
672
-
1039
+
673
1040
  if (comp.markClean) comp.markClean();
674
1041
  }
675
- });
676
-
1042
+ }
1043
+
1044
+ // 2. Dessiner les composants fixes ensuite
1045
+ if (fixedDirty.length > 0) {
1046
+ for (let comp of fixedDirty) {
1047
+ // Nettoyer la zone avec le background
1048
+ ctx.save();
1049
+ ctx.fillStyle = this.backgroundColor || '#ffffff';
1050
+ ctx.fillRect(comp.x - 2, comp.y - 2, comp.width + 4, comp.height + 4);
1051
+ ctx.restore();
1052
+
1053
+ // Dessiner le composant sans translation
1054
+ comp.draw(ctx);
1055
+
1056
+ if (comp.markClean) comp.markClean();
1057
+ }
1058
+ }
1059
+
677
1060
  this.dirtyComponents.clear();
678
-
1061
+
679
1062
  // Flush si on utilise le double buffering
680
1063
  if (this.optimizations.useDoubleBuffering) {
681
1064
  this.flush();
682
1065
  }
683
1066
  }
684
-
1067
+
685
1068
  /**
686
1069
  * Active/désactive une optimisation spécifique
687
1070
  * @param {string} optimization - Nom de l'optimisation
@@ -690,8 +1073,8 @@ class CanvasFramework {
690
1073
  setOptimization(optimization, enabled) {
691
1074
  if (this.optimizations.hasOwnProperty(optimization)) {
692
1075
  this.optimizations[optimization] = enabled;
693
-
694
- switch(optimization) {
1076
+
1077
+ switch (optimization) {
695
1078
  case 'useDoubleBuffering':
696
1079
  if (enabled && !this._bufferCtx) {
697
1080
  this._initDoubleBuffer();
@@ -703,18 +1086,20 @@ class CanvasFramework {
703
1086
  }
704
1087
  break;
705
1088
  }
706
-
1089
+
707
1090
  // Marquer tous les composants comme sales pour forcer un redessin complet
708
1091
  this.components.forEach(comp => this.markComponentDirty(comp));
709
1092
  }
710
1093
  }
711
-
1094
+
712
1095
  /**
713
1096
  * Obtient l'état des optimisations
714
1097
  * @returns {Object} État des optimisations
715
1098
  */
716
1099
  getOptimizations() {
717
- return { ...this.optimizations };
1100
+ return {
1101
+ ...this.optimizations
1102
+ };
718
1103
  }
719
1104
 
720
1105
  /**
@@ -858,17 +1243,17 @@ class CanvasFramework {
858
1243
  requestAnimationFrame(fade);
859
1244
  } else {
860
1245
  this._splashFinished = true;
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';
1246
+ // ✅ AJOUTER : Réinitialiser complètement le contexte
1247
+ this.ctx.clearRect(0, 0, this.width, this.height);
1248
+ this.ctx.globalAlpha = 1;
1249
+ this.ctx.textAlign = 'start'; // ← IMPORTANT
1250
+ this.ctx.textBaseline = 'alphabetic'; // ← IMPORTANT
1251
+ this.ctx.font = '10px sans-serif'; // Valeur par défaut
1252
+ this.ctx.fillStyle = '#000000';
1253
+ this.ctx.strokeStyle = '#000000';
1254
+ this.ctx.lineWidth = 1;
1255
+ this.ctx.lineCap = 'butt';
1256
+ this.ctx.lineJoin = 'miter';
872
1257
  }
873
1258
  };
874
1259
 
@@ -1245,16 +1630,10 @@ class CanvasFramework {
1245
1630
  this.canvas.style.width = this.width + 'px';
1246
1631
  this.canvas.style.height = this.height + 'px';
1247
1632
 
1248
- // ✅ AJOUTER: Appliquer le background au style CSS
1249
- this.canvas.style.backgroundColor = this.backgroundColor;
1633
+ // ✅ AJOUTER: Appliquer le background au style CSS
1634
+ this.canvas.style.backgroundColor = this.backgroundColor;
1250
1635
  // Échelle uniquement pour Canvas 2D
1251
- this.ctx.scale(this.dpr, this.dpr);
1252
- /*if (!this.useWebGL) {
1253
- this.ctx.scale(this.dpr, this.dpr);
1254
- } else {
1255
- // WebGL gère le DPR automatiquement via la matrice de projection
1256
- this.ctx.updateProjectionMatrix();
1257
- }*/
1636
+ this.ctx.scale(this.dpr, this.dpr);
1258
1637
  }
1259
1638
 
1260
1639
  setupEventListeners() {
@@ -1727,6 +2106,16 @@ class CanvasFramework {
1727
2106
  const touch = e.touches[0];
1728
2107
  const pos = this.getTouchPos(touch);
1729
2108
  this.lastTouchY = pos.y;
2109
+
2110
+ // Informer le Worker
2111
+ this.scrollWorker.postMessage({
2112
+ type: 'SET_DRAGGING',
2113
+ payload: {
2114
+ isDragging: false,
2115
+ lastTouchY: this.lastTouchY
2116
+ }
2117
+ });
2118
+
1730
2119
  this.checkComponentsAtPosition(pos.x, pos.y, 'start');
1731
2120
  }
1732
2121
 
@@ -1739,15 +2128,27 @@ class CanvasFramework {
1739
2128
  const deltaY = Math.abs(pos.y - this.lastTouchY);
1740
2129
  if (deltaY > 5) {
1741
2130
  this.isDragging = true;
2131
+ this.scrollWorker.postMessage({
2132
+ type: 'SET_DRAGGING',
2133
+ payload: {
2134
+ isDragging: true,
2135
+ lastTouchY: this.lastTouchY
2136
+ }
2137
+ });
1742
2138
  }
1743
2139
  }
1744
2140
 
1745
2141
  if (this.isDragging) {
1746
2142
  const deltaY = pos.y - this.lastTouchY;
1747
- this.scrollOffset += deltaY;
1748
- const maxScroll = this.getMaxScroll();
1749
- this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -maxScroll);
1750
- this.scrollVelocity = deltaY;
2143
+
2144
+ // Déléguer le calcul au Worker
2145
+ this.scrollWorker.postMessage({
2146
+ type: 'HANDLE_TOUCH_MOVE',
2147
+ payload: {
2148
+ deltaY
2149
+ }
2150
+ });
2151
+
1751
2152
  this.lastTouchY = pos.y;
1752
2153
  } else {
1753
2154
  this.checkComponentsAtPosition(pos.x, pos.y, 'move');
@@ -1763,12 +2164,28 @@ class CanvasFramework {
1763
2164
  this.checkComponentsAtPosition(pos.x, pos.y, 'end');
1764
2165
  } else {
1765
2166
  this.isDragging = false;
2167
+ this.scrollWorker.postMessage({
2168
+ type: 'SET_DRAGGING',
2169
+ payload: {
2170
+ isDragging: false,
2171
+ lastVelocity: this.scrollVelocity
2172
+ }
2173
+ });
1766
2174
  }
1767
2175
  }
1768
2176
 
1769
2177
  handleMouseDown(e) {
1770
2178
  this.isDragging = false;
1771
2179
  this.lastTouchY = e.clientY;
2180
+
2181
+ this.scrollWorker.postMessage({
2182
+ type: 'SET_DRAGGING',
2183
+ payload: {
2184
+ isDragging: false,
2185
+ lastTouchY: this.lastTouchY
2186
+ }
2187
+ });
2188
+
1772
2189
  this.checkComponentsAtPosition(e.clientX, e.clientY, 'start');
1773
2190
  }
1774
2191
 
@@ -1777,15 +2194,26 @@ class CanvasFramework {
1777
2194
  const deltaY = Math.abs(e.clientY - this.lastTouchY);
1778
2195
  if (deltaY > 5) {
1779
2196
  this.isDragging = true;
2197
+ this.scrollWorker.postMessage({
2198
+ type: 'SET_DRAGGING',
2199
+ payload: {
2200
+ isDragging: true,
2201
+ lastTouchY: this.lastTouchY
2202
+ }
2203
+ });
1780
2204
  }
1781
2205
  }
1782
2206
 
1783
2207
  if (this.isDragging) {
1784
2208
  const deltaY = e.clientY - this.lastTouchY;
1785
- this.scrollOffset += deltaY;
1786
- const maxScroll = this.getMaxScroll();
1787
- this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -maxScroll);
1788
- this.scrollVelocity = deltaY;
2209
+
2210
+ this.scrollWorker.postMessage({
2211
+ type: 'HANDLE_TOUCH_MOVE',
2212
+ payload: {
2213
+ deltaY
2214
+ }
2215
+ });
2216
+
1789
2217
  this.lastTouchY = e.clientY;
1790
2218
  } else {
1791
2219
  this.checkComponentsAtPosition(e.clientX, e.clientY, 'move');
@@ -1797,9 +2225,17 @@ class CanvasFramework {
1797
2225
  this.checkComponentsAtPosition(e.clientX, e.clientY, 'end');
1798
2226
  } else {
1799
2227
  this.isDragging = false;
2228
+ this.scrollWorker.postMessage({
2229
+ type: 'SET_DRAGGING',
2230
+ payload: {
2231
+ isDragging: false,
2232
+ lastVelocity: this.scrollVelocity
2233
+ }
2234
+ });
1800
2235
  }
1801
2236
  }
1802
2237
 
2238
+
1803
2239
  getTouchPos(touch) {
1804
2240
  const rect = this.canvas.getBoundingClientRect();
1805
2241
  return {
@@ -1943,8 +2379,12 @@ class CanvasFramework {
1943
2379
  }
1944
2380
 
1945
2381
  getMaxScroll() {
1946
- if (!this._maxScrollDirty) return this._cachedMaxScroll;
2382
+ // Utiliser le cache du Worker
2383
+ if (!this._maxScrollDirty && this._cachedMaxScroll !== undefined) {
2384
+ return this._cachedMaxScroll;
2385
+ }
1947
2386
 
2387
+ // Fallback si le Worker n'est pas encore initialisé
1948
2388
  let maxY = 0;
1949
2389
  for (const comp of this.components) {
1950
2390
  if (this.isFixedComponent(comp) || !comp.visible) continue;
@@ -1952,9 +2392,7 @@ class CanvasFramework {
1952
2392
  if (bottom > maxY) maxY = bottom;
1953
2393
  }
1954
2394
 
1955
- this._cachedMaxScroll = Math.max(0, maxY - this.height + 50);
1956
- this._maxScrollDirty = false;
1957
- return this._cachedMaxScroll;
2395
+ return Math.max(0, maxY - this.height + 50);
1958
2396
  }
1959
2397
 
1960
2398
  /*getMaxScroll() {
@@ -1966,23 +2404,31 @@ class CanvasFramework {
1966
2404
  }
1967
2405
  return Math.max(0, maxY - this.height + 50);
1968
2406
  }*/
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
- }
2407
+
2408
+ handleResize() {
2409
+ if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
2410
+
2411
+ this.resizeTimeout = setTimeout(() => {
2412
+ this.width = window.innerWidth;
2413
+ this.height = window.innerHeight;
2414
+ this.setupCanvas();
2415
+
2416
+ // Mettre à jour les dimensions dans le Worker
2417
+ this.scrollWorker.postMessage({
2418
+ type: 'UPDATE_DIMENSIONS',
2419
+ payload: {
2420
+ height: this.height
2421
+ }
2422
+ });
2423
+
2424
+ for (const comp of this.components) {
2425
+ if (comp._resize) {
2426
+ comp._resize(this.width, this.height);
2427
+ }
2428
+ }
2429
+ this.updateScrollWorkerComponents();
2430
+ }, 150);
2431
+ }
1986
2432
 
1987
2433
  /*handleResize() {
1988
2434
  if (this.resizeTimeout) clearTimeout(this.resizeTimeout); // ✅ AJOUTER
@@ -2006,7 +2452,7 @@ class CanvasFramework {
2006
2452
  add(component) {
2007
2453
  this.components.push(component);
2008
2454
  component._mount();
2009
- this._maxScrollDirty = true; // ✅ AJOUTER CETTE LIGNE
2455
+ this.updateScrollWorkerComponents();
2010
2456
  return component;
2011
2457
  }
2012
2458
 
@@ -2015,12 +2461,13 @@ class CanvasFramework {
2015
2461
  if (index > -1) {
2016
2462
  component._unmount();
2017
2463
  this.components.splice(index, 1);
2018
- this._maxScrollDirty = true; // ✅ AJOUTER CETTE LIGNE
2464
+ this.updateScrollWorkerComponents();
2019
2465
  }
2020
2466
  }
2021
2467
 
2468
+ // 4. Modifiez markComponentDirty() pour éviter de marquer pendant le scroll
2022
2469
  markComponentDirty(component) {
2023
- if (this.optimizationEnabled) {
2470
+ if (this.optimizationEnabled && !this.isDragging && Math.abs(this.scrollVelocity) < 5) {
2024
2471
  this.dirtyComponents.add(component);
2025
2472
  }
2026
2473
  }
@@ -2190,116 +2637,200 @@ class CanvasFramework {
2190
2637
  }
2191
2638
 
2192
2639
  startRenderLoop() {
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
- }
2640
+ let lastScrollOffset = this.scrollOffset;
2641
+ let framesWithoutScroll = 0;
2247
2642
 
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();
2643
+ const render = () => {
2644
+ if (!this._splashFinished) {
2645
+ requestAnimationFrame(render);
2646
+ return;
2647
+ }
2263
2648
 
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
- }
2649
+ // 1️⃣ Vérifier si le scroll a changé
2650
+ const scrollChanged = Math.abs(this.scrollOffset - lastScrollOffset) > 0.1;
2651
+ lastScrollOffset = this.scrollOffset;
2652
+
2653
+ // 2️⃣ Clear canvas AVEC BACKGROUND (toujours faire un clear complet)
2654
+ this.ctx.fillStyle = this.backgroundColor || '#ffffff';
2655
+ this.ctx.fillRect(0, 0, this.width, this.height);
2656
+
2657
+ // 3️⃣ Transition handling
2658
+ if (this.transitionState.isTransitioning) {
2659
+ this.updateTransition();
2660
+ }
2661
+ // 4️⃣ Rendu optimisé UNIQUEMENT si pas de scroll actif et peu de composants sales
2662
+ else if (this.optimizationEnabled &&
2663
+ this.dirtyComponents.size > 0 &&
2664
+ !this.isDragging &&
2665
+ Math.abs(this.scrollVelocity) < 0.5) {
2666
+
2667
+ // ✅ OPTIMISATION : Si on scroll, on fait un rendu complet pour éviter le clignotement
2668
+ if (scrollChanged && this.dirtyComponents.size < this.components.length / 2) {
2669
+ // Si beaucoup de composants sales + scroll, rendu complet
2670
+ this.renderFull();
2671
+ } else {
2672
+ // Sinon, rendu optimisé
2673
+ this._renderDirtyComponents();
2674
+ }
2675
+ } else {
2676
+ // Full redraw (plus stable pendant le scroll)
2677
+ this.renderFull();
2678
+ }
2679
+
2680
+ // 5️⃣ FPS
2681
+ this._frames++;
2682
+ const now = performance.now();
2683
+ if (now - this._lastFpsTime >= 1000) {
2684
+ this.fps = this._frames;
2685
+ this._frames = 0;
2686
+ this._lastFpsTime = now;
2687
+ }
2688
+
2689
+ if (this.showFps) {
2690
+ this.ctx.save();
2691
+ this.ctx.fillStyle = 'lime';
2692
+ this.ctx.font = '16px monospace';
2693
+ this.ctx.fillText(`FPS: ${this.fps}`, 10, 20);
2694
+ this.ctx.restore();
2695
+ }
2696
+
2697
+ if (this.debbug) {
2698
+ this.drawOverflowIndicators();
2699
+ }
2700
+
2701
+ if (!this._firstRenderDone && this.components.length > 0) {
2702
+ this._markFirstRender();
2703
+ }
2704
+
2705
+ requestAnimationFrame(render);
2706
+ };
2707
+
2708
+ render();
2709
+ }
2710
+
2711
+ // 3. Ajoutez une méthode renderFull() optimisée
2712
+ renderFull() {
2713
+ // Sauvegarder le contexte
2714
+ this.ctx.save();
2715
+
2716
+ // Séparer les composants
2717
+ const scrollableComponents = [];
2718
+ const fixedComponents = [];
2719
+
2720
+ for (let comp of this.components) {
2721
+ if (this.isFixedComponent(comp)) {
2722
+ fixedComponents.push(comp);
2723
+ } else {
2724
+ scrollableComponents.push(comp);
2725
+ }
2726
+ }
2727
+
2728
+ // ✅ OPTIMISATION : Dessiner les composants scrollables avec translation
2729
+ if (scrollableComponents.length > 0) {
2730
+ this.ctx.save();
2731
+ this.ctx.translate(0, this.scrollOffset);
2732
+
2733
+ for (let comp of scrollableComponents) {
2734
+ if (comp.visible) {
2735
+ // Viewport culling
2736
+ const screenY = comp.y + this.scrollOffset;
2737
+ const isInViewport = screenY + comp.height >= -100 && screenY <= this.height + 100;
2738
+
2739
+ if (isInViewport) {
2740
+ comp.draw(this.ctx);
2741
+ }
2742
+ }
2743
+ }
2744
+
2745
+ this.ctx.restore();
2746
+ }
2747
+
2748
+ // Dessiner les composants fixes
2749
+ for (let comp of fixedComponents) {
2750
+ if (comp.visible) {
2751
+ comp.draw(this.ctx);
2752
+ }
2753
+ }
2754
+
2755
+ // Restaurer le contexte
2756
+ this.ctx.restore();
2757
+ }
2758
+
2759
+ /**
2760
+ * Fait défiler à une position spécifique
2761
+ * @param {number} offset - Position cible
2762
+ * @param {boolean} animated - Avec animation
2763
+ */
2764
+ scrollTo(offset, animated = true) {
2765
+ if (animated) {
2766
+ const startOffset = this.scrollOffset;
2767
+ const delta = offset - startOffset;
2768
+ const duration = 300;
2769
+ const startTime = performance.now();
2770
+
2771
+ const animate = (currentTime) => {
2772
+ const elapsed = currentTime - startTime;
2773
+ const progress = Math.min(elapsed / duration, 1);
2774
+ const ease = this.easeOutCubic(progress);
2775
+ const currentOffset = startOffset + delta * ease;
2776
+
2777
+ this.scrollWorker.postMessage({
2778
+ type: 'SET_SCROLL_OFFSET',
2779
+ payload: {
2780
+ scrollOffset: currentOffset
2781
+ }
2782
+ });
2783
+
2784
+ if (progress < 1) {
2785
+ requestAnimationFrame(animate);
2786
+ }
2787
+ };
2788
+
2789
+ requestAnimationFrame(animate);
2790
+ } else {
2791
+ this.scrollWorker.postMessage({
2792
+ type: 'SET_SCROLL_OFFSET',
2793
+ payload: {
2794
+ scrollOffset: offset
2795
+ }
2796
+ });
2797
+ }
2798
+ }
2799
+
2800
+ /**
2801
+ * Fonction d'easing
2802
+ */
2803
+ easeOutCubic(t) {
2804
+ return 1 - Math.pow(1 - t, 3);
2805
+ }
2806
+
2807
+ /**
2808
+ * Nettoie les ressources
2809
+ */
2810
+ destroy() {
2811
+ if (this.scrollWorker) {
2812
+ this.scrollWorker.terminate();
2813
+ }
2814
+ if (this.worker) {
2815
+ this.worker.terminate();
2816
+ }
2817
+ if (this.logicWorker) {
2818
+ this.logicWorker.terminate();
2819
+ }
2820
+
2821
+ if (this.ctx && typeof this.ctx.destroy === 'function') {
2822
+ this.ctx.destroy();
2823
+ }
2824
+
2825
+ // Nettoyer les écouteurs d'événements
2826
+ this.canvas.removeEventListener('touchstart', this.handleTouchStart);
2827
+ this.canvas.removeEventListener('touchmove', this.handleTouchMove);
2828
+ this.canvas.removeEventListener('touchend', this.handleTouchEnd);
2829
+ this.canvas.removeEventListener('mousedown', this.handleMouseDown);
2830
+ this.canvas.removeEventListener('mousemove', this.handleMouseMove);
2831
+ this.canvas.removeEventListener('mouseup', this.handleMouseUp);
2832
+ window.removeEventListener('resize', this.handleResize);
2833
+ }
2303
2834
 
2304
2835
  // ✅ AJOUTER: Afficher les métriques à l'écran
2305
2836
  displayMetrics() {
@@ -2338,4 +2869,8 @@ class CanvasFramework {
2338
2869
  }
2339
2870
  }
2340
2871
 
2341
- export default CanvasFramework;
2872
+ export default CanvasFramework;
2873
+
2874
+
2875
+
2876
+