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.
- package/core/CanvasFramework.js +496 -109
- package/package.json +1 -1
package/core/CanvasFramework.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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() {
|