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