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.
- package/core/CanvasFramework.js +1053 -131
- package/core/WebGLCanvasAdapter.js +747 -1400
- package/package.json +1 -1
- package/utils/AnimationEngine.js +314 -8
package/core/CanvasFramework.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1366
|
-
|
|
1367
|
-
this.
|
|
1368
|
-
|
|
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
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
1826
|
-
|
|
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
|
-
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
+
|