canvasframework 0.4.10 → 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,11 +155,84 @@ 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
+ // 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);
193
+
194
+ this.backgroundColor = options.backgroundColor || '#f5f5f5'; // Blanc par défaut
195
+
160
196
  this.width = window.innerWidth;
161
197
  this.height = window.innerHeight;
162
198
  this.dpr = window.devicePixelRatio || 1;
199
+
200
+ // ✅ OPTIMISATION OPTION 2: Configuration des optimisations
201
+ this.optimizations = {
202
+ enabled: options.optimizations !== false, // Activé par défaut
203
+ useDoubleBuffering: true,
204
+ useCaching: true,
205
+ useBatchDrawing: true,
206
+ useSpatialPartitioning: false, // Désactivé par défaut (à activer si beaucoup de composants)
207
+ useImageDataOptimization: true
208
+ };
209
+
210
+ // ✅ OPTIMISATION OPTION 2: Cache pour éviter les changements d'état inutiles
211
+ this._stateCache = {
212
+ fillStyle: null,
213
+ strokeStyle: null,
214
+ font: null,
215
+ textAlign: null,
216
+ textBaseline: null,
217
+ lineWidth: null,
218
+ globalAlpha: 1
219
+ };
220
+
221
+ // ✅ OPTIMISATION OPTION 2: Cache des images/textes
222
+ this.imageCache = new Map();
223
+ this.textCache = new Map();
224
+
225
+ // ✅ OPTIMISATION OPTION 2: Double buffering
226
+ this._doubleBuffer = null;
227
+ this._bufferCtx = null;
228
+ if (this.optimizations.useDoubleBuffering) {
229
+ this._initDoubleBuffer();
230
+ }
231
+
232
+ // Scroll Worker
233
+ this.scrollWorker = this.createScrollWorker();
234
+ this.scrollWorker.onmessage = this.handleScrollWorkerMessage.bind(this);
235
+
163
236
  this.splashOptions = {
164
237
  enabled: options.splash?.enabled === true, // false par défaut
165
238
  duration: options.splash?.duration || 700,
@@ -178,6 +251,7 @@ class CanvasFramework {
178
251
  logoWidth: options.splash?.logoWidth || 100,
179
252
  logoHeight: options.splash?.logoHeight || 100
180
253
  };
254
+
181
255
  // ✅ MODIFIER : Vérifier si le splash est activé
182
256
  if (this.splashOptions.enabled) {
183
257
  this.showSplashScreen();
@@ -186,8 +260,9 @@ class CanvasFramework {
186
260
  }
187
261
 
188
262
  this.platform = this.detectPlatform();
189
-
190
-
263
+ setTimeout(() => {
264
+ this.initScrollWorker();
265
+ }, 100);
191
266
  // État actuel + préférence
192
267
  this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
193
268
  this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
@@ -210,19 +285,7 @@ class CanvasFramework {
210
285
 
211
286
  //this.applyThemeFromSystem();
212
287
  this.state = {};
213
- // NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
214
- this.useWebGL = options.useWebGL !== false; // true par défaut
215
- // Initialiser le contexte approprié
216
- /*if (this.useWebGL) {
217
- try {
218
- this.ctx = new WebGLCanvasAdapter(this.canvas);
219
- } catch (e) {
220
- this.ctx = this.canvas.getContext('2d');
221
- this.useWebGL = false;
222
- }
223
- } else {
224
- this.ctx = this.canvas.getContext('2d');
225
- }*/
288
+
226
289
  // Calcule FPS
227
290
  this.fps = 0;
228
291
  this._frames = 0;
@@ -264,7 +327,7 @@ class CanvasFramework {
264
327
 
265
328
  // Optimisation
266
329
  this.dirtyComponents = new Set();
267
- this.optimizationEnabled = false;
330
+ this.optimizationEnabled = this.optimizations.enabled;
268
331
 
269
332
  // AJOUTER CETTE LIGNE
270
333
  this.animator = new AnimationEngine();
@@ -289,10 +352,13 @@ class CanvasFramework {
289
352
  };
290
353
 
291
354
  this.setupCanvas();
355
+
356
+ // ✅ OPTIMISATION OPTION 5: Désactiver l'antialiasing pour meilleures performances
357
+ this._disableImageSmoothing();
358
+
292
359
  this.setupEventListeners();
293
360
  this.setupHistoryListener();
294
361
 
295
-
296
362
  this.startRenderLoop();
297
363
 
298
364
  this.devTools = new DevTools(this);
@@ -335,6 +401,705 @@ class CanvasFramework {
335
401
  // ✅ AJOUTER: Marquer le premier rendu
336
402
  this._firstRenderDone = false;
337
403
  this._startupStartTime = startTime;
404
+
405
+ // ✅ OPTIMISATION OPTION 5: Partition spatiale pour le culling (optionnel)
406
+ if (this.optimizations.useSpatialPartitioning) {
407
+ this._initSpatialPartitioning();
408
+ }
409
+ }
410
+
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
+ /**
734
+ * Initialise le double buffering pour éviter le flickering
735
+ * @private
736
+ */
737
+ // Dans _initDoubleBuffer(), assurez-vous de bien configurer le contexte
738
+ _initDoubleBuffer() {
739
+ this._doubleBuffer = document.createElement('canvas');
740
+ this._bufferCtx = this._doubleBuffer.getContext('2d', {
741
+ alpha: false,
742
+ desynchronized: true
743
+ });
744
+ this._doubleBuffer.width = this.width * this.dpr;
745
+ this._doubleBuffer.height = this.height * this.dpr;
746
+ this._doubleBuffer.style.width = this.width + 'px';
747
+ this._doubleBuffer.style.height = this.height + 'px';
748
+ this._bufferCtx.scale(this.dpr, this.dpr);
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);
754
+ }
755
+
756
+
757
+ /**
758
+ * Désactive l'antialiasing pour meilleures performances
759
+ * @private
760
+ * @param {CanvasRenderingContext2D} [ctx=this.ctx] - Contexte à configurer
761
+ */
762
+ _disableImageSmoothing(ctx = this.ctx) {
763
+ ctx.imageSmoothingEnabled = false;
764
+ ctx.msImageSmoothingEnabled = false;
765
+ ctx.webkitImageSmoothingEnabled = false;
766
+ ctx.mozImageSmoothingEnabled = false;
767
+ }
768
+
769
+ /**
770
+ * Initialise le spatial partitioning pour le viewport culling
771
+ * @private
772
+ */
773
+ _initSpatialPartitioning() {
774
+ // Simple grid spatial partitioning
775
+ this._spatialGrid = {
776
+ cellSize: 100,
777
+ grid: new Map(),
778
+ update: (components) => {
779
+ this._spatialGrid.grid.clear();
780
+ components.forEach(comp => {
781
+ if (!comp.visible) return;
782
+
783
+ const gridX = Math.floor(comp.x / this._spatialGrid.cellSize);
784
+ const gridY = Math.floor(comp.y / this._spatialGrid.cellSize);
785
+ const key = `${gridX},${gridY}`;
786
+
787
+ if (!this._spatialGrid.grid.has(key)) {
788
+ this._spatialGrid.grid.set(key, []);
789
+ }
790
+ this._spatialGrid.grid.get(key).push(comp);
791
+ });
792
+ },
793
+ getVisible: (viewportY) => {
794
+ const visible = [];
795
+ const startY = viewportY - 200; // Marge de 200px
796
+ const endY = viewportY + this.height + 200;
797
+
798
+ this._spatialGrid.grid.forEach((comps, key) => {
799
+ comps.forEach(comp => {
800
+ const compBottom = comp.y + comp.height;
801
+ if (compBottom >= startY && comp.y <= endY) {
802
+ visible.push(comp);
803
+ }
804
+ });
805
+ });
806
+ return visible;
807
+ }
808
+ };
809
+ }
810
+
811
+ /**
812
+ * ✅ OPTIMISATION OPTION 2: Rendu optimisé de rectangles
813
+ * Évite les changements d'état inutiles
814
+ * @param {number} x - Position X
815
+ * @param {number} y - Position Y
816
+ * @param {number} w - Largeur
817
+ * @param {number} h - Hauteur
818
+ * @param {string} color - Couleur de remplissage
819
+ */
820
+ fillRectOptimized(x, y, w, h, color) {
821
+ // Éviter les changements d'état inutiles
822
+ if (this._stateCache.fillStyle !== color) {
823
+ this.ctx.fillStyle = color;
824
+ this._stateCache.fillStyle = color;
825
+ }
826
+ this.ctx.fillRect(x, y, w, h);
827
+ }
828
+
829
+ /**
830
+ * ✅ OPTIMISATION OPTION 2: Texte avec cache
831
+ * Cache le rendu du texte pour éviter de le redessiner à chaque frame
832
+ * @param {string} text - Texte à afficher
833
+ * @param {number} x - Position X
834
+ * @param {number} y - Position Y
835
+ * @param {string} font - Police CSS
836
+ * @param {string} color - Couleur du texte
837
+ */
838
+ fillTextCached(text, x, y, font, color) {
839
+ const key = `${text}_${font}_${color}`;
840
+
841
+ if (!this.textCache.has(key)) {
842
+ // Rendu dans un canvas temporaire
843
+ const temp = document.createElement('canvas');
844
+ const tempCtx = temp.getContext('2d', {
845
+ alpha: false
846
+ });
847
+ tempCtx.font = font;
848
+
849
+ const metrics = tempCtx.measureText(text);
850
+ temp.width = Math.ceil(metrics.width);
851
+ temp.height = Math.ceil(parseInt(font) * 1.2);
852
+
853
+ tempCtx.font = font;
854
+ tempCtx.fillStyle = color;
855
+ tempCtx.textBaseline = 'top';
856
+ tempCtx.fillText(text, 0, 0);
857
+
858
+ this.textCache.set(key, {
859
+ canvas: temp,
860
+ width: temp.width,
861
+ height: temp.height,
862
+ baseline: parseInt(font)
863
+ });
864
+ }
865
+
866
+ const cached = this.textCache.get(key);
867
+ this.ctx.drawImage(cached.canvas, x, y - cached.baseline);
868
+ }
869
+
870
+ /**
871
+ * ✅ OPTIMISATION OPTION 5: Rendu batché pour plusieurs rectangles
872
+ * Regroupe les rectangles par couleur pour réduire les appels draw
873
+ * @param {Array} rects - Tableau d'objets {x, y, width, height, color}
874
+ */
875
+ batchRect(rects) {
876
+ if (!rects || rects.length === 0) return;
877
+
878
+ // Regrouper par couleur
879
+ const batches = new Map();
880
+
881
+ rects.forEach(rect => {
882
+ if (!batches.has(rect.color)) {
883
+ batches.set(rect.color, []);
884
+ }
885
+ batches.get(rect.color).push(rect);
886
+ });
887
+
888
+ // Dessiner par batch
889
+ batches.forEach((batchRects, color) => {
890
+ this.ctx.fillStyle = color;
891
+
892
+ // Utiliser un seul path pour tous les rectangles de même couleur
893
+ this.ctx.beginPath();
894
+ batchRects.forEach(rect => {
895
+ this.ctx.rect(rect.x, rect.y, rect.width, rect.height);
896
+ });
897
+ this.ctx.fill();
898
+ });
899
+ }
900
+
901
+ /**
902
+ * ✅ OPTIMISATION OPTION 5: Utiliser ImageData pour les mises à jour fréquentes
903
+ * @param {number} x - Position X
904
+ * @param {number} y - Position Y
905
+ * @param {number} width - Largeur
906
+ * @param {number} height - Hauteur
907
+ * @param {Function} drawFn - Fonction pour manipuler les pixels
908
+ */
909
+ updateRegion(x, y, width, height, drawFn) {
910
+ const imageData = this.ctx.getImageData(x, y, width, height);
911
+ const data = imageData.data;
912
+
913
+ // Manipuler directement les pixels
914
+ drawFn(data, width, height);
915
+
916
+ this.ctx.putImageData(imageData, x, y);
917
+ }
918
+
919
+ /**
920
+ * ✅ OPTIMISATION OPTION 2: Flush du buffer pour le double buffering
921
+ */
922
+ flush() {
923
+ if (this.optimizations.useDoubleBuffering && this._bufferCtx) {
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);
930
+ }
931
+ }
932
+
933
+ /**
934
+ * ✅ OPTIMISATION OPTION 5: Rendu optimisé avec viewport culling
935
+ * @private
936
+ */
937
+ _renderOptimized() {
938
+ const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
939
+
940
+ if (!ctx) return;
941
+
942
+ // Clear le canvas
943
+ ctx.clearRect(0, 0, this.width, this.height);
944
+
945
+ // Séparer les composants fixes et scrollables
946
+ const scrollableComponents = [];
947
+ const fixedComponents = [];
948
+
949
+ for (let comp of this.components) {
950
+ if (this.isFixedComponent(comp)) {
951
+ fixedComponents.push(comp);
952
+ } else {
953
+ scrollableComponents.push(comp);
954
+ }
955
+ }
956
+
957
+ // Rendu des composants scrollables avec viewport culling optimisé
958
+ ctx.save();
959
+ ctx.translate(0, this.scrollOffset);
960
+
961
+ // ✅ OPTIMISATION: Utiliser le spatial partitioning si activé
962
+ if (this.optimizations.useSpatialPartitioning && this._spatialGrid) {
963
+ const visibleComps = this._spatialGrid.getVisible(-this.scrollOffset);
964
+ for (let comp of visibleComps) {
965
+ if (comp.visible) {
966
+ comp.draw(ctx);
967
+ }
968
+ }
969
+ } else {
970
+ // Rendu standard avec culling simple
971
+ for (let comp of scrollableComponents) {
972
+ if (comp.visible) {
973
+ const screenY = comp.y + this.scrollOffset;
974
+ const isInViewport = screenY + comp.height >= -100 && screenY <= this.height + 100;
975
+
976
+ if (isInViewport) {
977
+ comp.draw(ctx);
978
+ }
979
+ }
980
+ }
981
+ }
982
+
983
+ ctx.restore();
984
+
985
+ // Rendu des composants fixes
986
+ for (let comp of fixedComponents) {
987
+ if (comp.visible) {
988
+ comp.draw(ctx);
989
+ }
990
+ }
991
+
992
+ // Flush si on utilise le double buffering
993
+ if (this.optimizations.useDoubleBuffering) {
994
+ this.flush();
995
+ }
996
+ }
997
+
998
+ /**
999
+ * ✅ OPTIMISATION OPTION 2: Rendu partiel (seulement les composants sales)
1000
+ * @private
1001
+ */
1002
+ _renderDirtyComponents() {
1003
+ const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
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
+
1011
+ this.dirtyComponents.forEach(comp => {
1012
+ if (comp.visible) {
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
1035
+ ctx.save();
1036
+ ctx.translate(0, this.scrollOffset);
1037
+ comp.draw(ctx);
1038
+ ctx.restore();
1039
+
1040
+ if (comp.markClean) comp.markClean();
1041
+ }
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
+
1060
+ this.dirtyComponents.clear();
1061
+
1062
+ // Flush si on utilise le double buffering
1063
+ if (this.optimizations.useDoubleBuffering) {
1064
+ this.flush();
1065
+ }
1066
+ }
1067
+
1068
+ /**
1069
+ * Active/désactive une optimisation spécifique
1070
+ * @param {string} optimization - Nom de l'optimisation
1071
+ * @param {boolean} enabled - true pour activer, false pour désactiver
1072
+ */
1073
+ setOptimization(optimization, enabled) {
1074
+ if (this.optimizations.hasOwnProperty(optimization)) {
1075
+ this.optimizations[optimization] = enabled;
1076
+
1077
+ switch (optimization) {
1078
+ case 'useDoubleBuffering':
1079
+ if (enabled && !this._bufferCtx) {
1080
+ this._initDoubleBuffer();
1081
+ }
1082
+ break;
1083
+ case 'useSpatialPartitioning':
1084
+ if (enabled && !this._spatialGrid) {
1085
+ this._initSpatialPartitioning();
1086
+ }
1087
+ break;
1088
+ }
1089
+
1090
+ // Marquer tous les composants comme sales pour forcer un redessin complet
1091
+ this.components.forEach(comp => this.markComponentDirty(comp));
1092
+ }
1093
+ }
1094
+
1095
+ /**
1096
+ * Obtient l'état des optimisations
1097
+ * @returns {Object} État des optimisations
1098
+ */
1099
+ getOptimizations() {
1100
+ return {
1101
+ ...this.optimizations
1102
+ };
338
1103
  }
339
1104
 
340
1105
  /**
@@ -478,17 +1243,17 @@ class CanvasFramework {
478
1243
  requestAnimationFrame(fade);
479
1244
  } else {
480
1245
  this._splashFinished = true;
481
- // ✅ AJOUTER : Réinitialiser complètement le contexte
482
- this.ctx.clearRect(0, 0, this.width, this.height);
483
- this.ctx.globalAlpha = 1;
484
- this.ctx.textAlign = 'start'; // ← IMPORTANT
485
- this.ctx.textBaseline = 'alphabetic'; // ← IMPORTANT
486
- this.ctx.font = '10px sans-serif'; // Valeur par défaut
487
- this.ctx.fillStyle = '#000000';
488
- this.ctx.strokeStyle = '#000000';
489
- this.ctx.lineWidth = 1;
490
- this.ctx.lineCap = 'butt';
491
- 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';
492
1257
  }
493
1258
  };
494
1259
 
@@ -865,14 +1630,10 @@ class CanvasFramework {
865
1630
  this.canvas.style.width = this.width + 'px';
866
1631
  this.canvas.style.height = this.height + 'px';
867
1632
 
1633
+ // ✅ AJOUTER: Appliquer le background au style CSS
1634
+ this.canvas.style.backgroundColor = this.backgroundColor;
868
1635
  // Échelle uniquement pour Canvas 2D
869
- this.ctx.scale(this.dpr, this.dpr);
870
- /*if (!this.useWebGL) {
871
- this.ctx.scale(this.dpr, this.dpr);
872
- } else {
873
- // WebGL gère le DPR automatiquement via la matrice de projection
874
- this.ctx.updateProjectionMatrix();
875
- }*/
1636
+ this.ctx.scale(this.dpr, this.dpr);
876
1637
  }
877
1638
 
878
1639
  setupEventListeners() {
@@ -1345,6 +2106,16 @@ class CanvasFramework {
1345
2106
  const touch = e.touches[0];
1346
2107
  const pos = this.getTouchPos(touch);
1347
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
+
1348
2119
  this.checkComponentsAtPosition(pos.x, pos.y, 'start');
1349
2120
  }
1350
2121
 
@@ -1357,15 +2128,27 @@ class CanvasFramework {
1357
2128
  const deltaY = Math.abs(pos.y - this.lastTouchY);
1358
2129
  if (deltaY > 5) {
1359
2130
  this.isDragging = true;
2131
+ this.scrollWorker.postMessage({
2132
+ type: 'SET_DRAGGING',
2133
+ payload: {
2134
+ isDragging: true,
2135
+ lastTouchY: this.lastTouchY
2136
+ }
2137
+ });
1360
2138
  }
1361
2139
  }
1362
2140
 
1363
2141
  if (this.isDragging) {
1364
2142
  const deltaY = pos.y - this.lastTouchY;
1365
- this.scrollOffset += deltaY;
1366
- const maxScroll = this.getMaxScroll();
1367
- this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -maxScroll);
1368
- 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
+
1369
2152
  this.lastTouchY = pos.y;
1370
2153
  } else {
1371
2154
  this.checkComponentsAtPosition(pos.x, pos.y, 'move');
@@ -1381,12 +2164,28 @@ class CanvasFramework {
1381
2164
  this.checkComponentsAtPosition(pos.x, pos.y, 'end');
1382
2165
  } else {
1383
2166
  this.isDragging = false;
2167
+ this.scrollWorker.postMessage({
2168
+ type: 'SET_DRAGGING',
2169
+ payload: {
2170
+ isDragging: false,
2171
+ lastVelocity: this.scrollVelocity
2172
+ }
2173
+ });
1384
2174
  }
1385
2175
  }
1386
2176
 
1387
2177
  handleMouseDown(e) {
1388
2178
  this.isDragging = false;
1389
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
+
1390
2189
  this.checkComponentsAtPosition(e.clientX, e.clientY, 'start');
1391
2190
  }
1392
2191
 
@@ -1395,15 +2194,26 @@ class CanvasFramework {
1395
2194
  const deltaY = Math.abs(e.clientY - this.lastTouchY);
1396
2195
  if (deltaY > 5) {
1397
2196
  this.isDragging = true;
2197
+ this.scrollWorker.postMessage({
2198
+ type: 'SET_DRAGGING',
2199
+ payload: {
2200
+ isDragging: true,
2201
+ lastTouchY: this.lastTouchY
2202
+ }
2203
+ });
1398
2204
  }
1399
2205
  }
1400
2206
 
1401
2207
  if (this.isDragging) {
1402
2208
  const deltaY = e.clientY - this.lastTouchY;
1403
- this.scrollOffset += deltaY;
1404
- const maxScroll = this.getMaxScroll();
1405
- this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -maxScroll);
1406
- this.scrollVelocity = deltaY;
2209
+
2210
+ this.scrollWorker.postMessage({
2211
+ type: 'HANDLE_TOUCH_MOVE',
2212
+ payload: {
2213
+ deltaY
2214
+ }
2215
+ });
2216
+
1407
2217
  this.lastTouchY = e.clientY;
1408
2218
  } else {
1409
2219
  this.checkComponentsAtPosition(e.clientX, e.clientY, 'move');
@@ -1415,9 +2225,17 @@ class CanvasFramework {
1415
2225
  this.checkComponentsAtPosition(e.clientX, e.clientY, 'end');
1416
2226
  } else {
1417
2227
  this.isDragging = false;
2228
+ this.scrollWorker.postMessage({
2229
+ type: 'SET_DRAGGING',
2230
+ payload: {
2231
+ isDragging: false,
2232
+ lastVelocity: this.scrollVelocity
2233
+ }
2234
+ });
1418
2235
  }
1419
2236
  }
1420
2237
 
2238
+
1421
2239
  getTouchPos(touch) {
1422
2240
  const rect = this.canvas.getBoundingClientRect();
1423
2241
  return {
@@ -1561,8 +2379,12 @@ class CanvasFramework {
1561
2379
  }
1562
2380
 
1563
2381
  getMaxScroll() {
1564
- if (!this._maxScrollDirty) return this._cachedMaxScroll;
2382
+ // Utiliser le cache du Worker
2383
+ if (!this._maxScrollDirty && this._cachedMaxScroll !== undefined) {
2384
+ return this._cachedMaxScroll;
2385
+ }
1565
2386
 
2387
+ // Fallback si le Worker n'est pas encore initialisé
1566
2388
  let maxY = 0;
1567
2389
  for (const comp of this.components) {
1568
2390
  if (this.isFixedComponent(comp) || !comp.visible) continue;
@@ -1570,9 +2392,7 @@ class CanvasFramework {
1570
2392
  if (bottom > maxY) maxY = bottom;
1571
2393
  }
1572
2394
 
1573
- this._cachedMaxScroll = Math.max(0, maxY - this.height + 50);
1574
- this._maxScrollDirty = false;
1575
- return this._cachedMaxScroll;
2395
+ return Math.max(0, maxY - this.height + 50);
1576
2396
  }
1577
2397
 
1578
2398
  /*getMaxScroll() {
@@ -1584,23 +2404,31 @@ class CanvasFramework {
1584
2404
  }
1585
2405
  return Math.max(0, maxY - this.height + 50);
1586
2406
  }*/
1587
-
1588
- handleResize() {
1589
- if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
1590
-
1591
- this.resizeTimeout = setTimeout(() => {
1592
- this.width = window.innerWidth;
1593
- this.height = window.innerHeight;
1594
- this.setupCanvas(); // ← Fait déjà tout le boulot
1595
-
1596
- for (const comp of this.components) {
1597
- if (comp._resize) {
1598
- comp._resize(this.width, this.height);
1599
- }
1600
- }
1601
- this._maxScrollDirty = true;
1602
- }, 150);
1603
- }
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
+ }
1604
2432
 
1605
2433
  /*handleResize() {
1606
2434
  if (this.resizeTimeout) clearTimeout(this.resizeTimeout); // ✅ AJOUTER
@@ -1624,7 +2452,7 @@ class CanvasFramework {
1624
2452
  add(component) {
1625
2453
  this.components.push(component);
1626
2454
  component._mount();
1627
- this._maxScrollDirty = true; // ✅ AJOUTER CETTE LIGNE
2455
+ this.updateScrollWorkerComponents();
1628
2456
  return component;
1629
2457
  }
1630
2458
 
@@ -1633,12 +2461,13 @@ class CanvasFramework {
1633
2461
  if (index > -1) {
1634
2462
  component._unmount();
1635
2463
  this.components.splice(index, 1);
1636
- this._maxScrollDirty = true; // ✅ AJOUTER CETTE LIGNE
2464
+ this.updateScrollWorkerComponents();
1637
2465
  }
1638
2466
  }
1639
2467
 
2468
+ // 4. Modifiez markComponentDirty() pour éviter de marquer pendant le scroll
1640
2469
  markComponentDirty(component) {
1641
- if (this.optimizationEnabled) {
2470
+ if (this.optimizationEnabled && !this.isDragging && Math.abs(this.scrollVelocity) < 5) {
1642
2471
  this.dirtyComponents.add(component);
1643
2472
  }
1644
2473
  }
@@ -1808,81 +2637,47 @@ class CanvasFramework {
1808
2637
  }
1809
2638
 
1810
2639
  startRenderLoop() {
2640
+ let lastScrollOffset = this.scrollOffset;
2641
+ let framesWithoutScroll = 0;
2642
+
1811
2643
  const render = () => {
1812
2644
  if (!this._splashFinished) {
1813
2645
  requestAnimationFrame(render);
1814
2646
  return;
1815
2647
  }
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
2648
 
1825
- // 2️⃣ Clear canvas
1826
- this.ctx.clearRect(0, 0, this.width, this.height);
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);
1827
2656
 
1828
2657
  // 3️⃣ Transition handling
1829
2658
  if (this.transitionState.isTransitioning) {
1830
2659
  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
- }
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();
1849
2674
  }
1850
- this.dirtyComponents.clear();
1851
2675
  } 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
- }
2676
+ // Full redraw (plus stable pendant le scroll)
2677
+ this.renderFull();
1883
2678
  }
1884
2679
 
1885
- // 4️⃣ FPS
2680
+ // 5️⃣ FPS
1886
2681
  this._frames++;
1887
2682
  const now = performance.now();
1888
2683
  if (now - this._lastFpsTime >= 1000) {
@@ -1903,7 +2698,6 @@ class CanvasFramework {
1903
2698
  this.drawOverflowIndicators();
1904
2699
  }
1905
2700
 
1906
- // ✅ AJOUTER: Marquer le premier rendu
1907
2701
  if (!this._firstRenderDone && this.components.length > 0) {
1908
2702
  this._markFirstRender();
1909
2703
  }
@@ -1914,6 +2708,130 @@ class CanvasFramework {
1914
2708
  render();
1915
2709
  }
1916
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
+ }
2834
+
1917
2835
  // ✅ AJOUTER: Afficher les métriques à l'écran
1918
2836
  displayMetrics() {
1919
2837
  const metrics = this.metrics;
@@ -1951,4 +2869,8 @@ class CanvasFramework {
1951
2869
  }
1952
2870
  }
1953
2871
 
1954
- export default CanvasFramework;
2872
+ export default CanvasFramework;
2873
+
2874
+
2875
+
2876
+