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