canvasframework 0.5.0 → 0.5.1
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/components/Button.js +1 -0
- package/components/ListItem.js +135 -89
- package/components/SwipeableListItem.js +0 -20
- package/core/CanvasFramework.js +134 -144
- package/features/PullToRefresh.js +1 -1
- package/package.json +1 -1
package/components/Button.js
CHANGED
package/components/ListItem.js
CHANGED
|
@@ -53,16 +53,35 @@ class ListItem extends Component {
|
|
|
53
53
|
* @param {number} y - Coordonnée Y
|
|
54
54
|
* @private
|
|
55
55
|
*/
|
|
56
|
-
|
|
56
|
+
handlePress(x, y) {
|
|
57
57
|
if (this.platform === 'material') {
|
|
58
|
-
|
|
58
|
+
// ✅ CORRECTION : Calculer les coordonnées LOCALES du composant
|
|
59
|
+
// x et y sont déjà les coordonnées écran après ajustement par le framework
|
|
60
|
+
|
|
61
|
+
// Calculer les coordonnées relatives au composant
|
|
62
|
+
const localX = x - this.x;
|
|
63
|
+
const localY = y - this.y;
|
|
64
|
+
|
|
65
|
+
// ✅ AJOUTER : Vérifier si le point est vraiment dans le composant
|
|
66
|
+
if (localX < 0 || localX > this.width || localY < 0 || localY > this.height) {
|
|
67
|
+
return; // Le clic n'est pas dans le composant
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
this.ripples.push({
|
|
60
|
-
x:
|
|
61
|
-
y:
|
|
71
|
+
x: localX, // ✅ Coordonnée X relative au composant
|
|
72
|
+
y: localY, // ✅ Coordonnée Y relative au composant
|
|
62
73
|
radius: 0,
|
|
63
74
|
maxRadius: Math.max(this.width, this.height) * 1.5,
|
|
64
|
-
opacity: 1
|
|
75
|
+
opacity: 1,
|
|
76
|
+
startTime: Date.now() // Pour une animation plus précise
|
|
65
77
|
});
|
|
78
|
+
// ✅ TEMPORAIREMENT mettre pressed à true pour le feedback visuel immédiat
|
|
79
|
+
this.pressed = true;
|
|
80
|
+
|
|
81
|
+
// ✅ MAIS le remettre à false après un court délai
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
this.pressed = false;
|
|
84
|
+
}, 150); // 150ms de feedback tactile
|
|
66
85
|
this.animateRipple();
|
|
67
86
|
}
|
|
68
87
|
}
|
|
@@ -72,28 +91,54 @@ class ListItem extends Component {
|
|
|
72
91
|
* @private
|
|
73
92
|
*/
|
|
74
93
|
animateRipple() {
|
|
94
|
+
let animationId = null;
|
|
95
|
+
const startTime = Date.now();
|
|
96
|
+
const duration = 600; // 600ms pour l'animation complète
|
|
97
|
+
|
|
75
98
|
const animate = () => {
|
|
99
|
+
const elapsed = Date.now() - startTime;
|
|
100
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
101
|
+
|
|
76
102
|
let hasActiveRipples = false;
|
|
77
103
|
|
|
78
104
|
for (let ripple of this.ripples) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
// ✅ Animation plus fluide avec easing
|
|
106
|
+
const easedProgress = this.easeOutCubic(progress);
|
|
107
|
+
ripple.radius = easedProgress * ripple.maxRadius;
|
|
108
|
+
|
|
109
|
+
// Fade out à partir de 50% de progression
|
|
110
|
+
if (progress > 0.5) {
|
|
111
|
+
const fadeProgress = (progress - 0.5) / 0.5;
|
|
112
|
+
ripple.opacity = 1 - fadeProgress;
|
|
82
113
|
}
|
|
83
114
|
|
|
84
|
-
if (
|
|
85
|
-
|
|
115
|
+
if (progress < 1) {
|
|
116
|
+
hasActiveRipples = true;
|
|
86
117
|
}
|
|
87
118
|
}
|
|
88
119
|
|
|
120
|
+
// Filtrer les ripples terminés
|
|
89
121
|
this.ripples = this.ripples.filter(r => r.opacity > 0);
|
|
90
122
|
|
|
91
|
-
if (hasActiveRipples) {
|
|
92
|
-
requestAnimationFrame(animate);
|
|
123
|
+
if (hasActiveRipples && this.ripples.length > 0) {
|
|
124
|
+
animationId = requestAnimationFrame(animate);
|
|
125
|
+
} else {
|
|
126
|
+
// ✅ Nettoyer quand l'animation est terminée
|
|
127
|
+
if (animationId) {
|
|
128
|
+
cancelAnimationFrame(animationId);
|
|
129
|
+
}
|
|
130
|
+
this.ripples = [];
|
|
93
131
|
}
|
|
94
132
|
};
|
|
95
133
|
|
|
96
|
-
animate
|
|
134
|
+
animationId = requestAnimationFrame(animate);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fonction d'easing pour animation fluide
|
|
139
|
+
*/
|
|
140
|
+
easeOutCubic(t) {
|
|
141
|
+
return 1 - Math.pow(1 - t, 3);
|
|
97
142
|
}
|
|
98
143
|
|
|
99
144
|
/**
|
|
@@ -103,108 +148,109 @@ class ListItem extends Component {
|
|
|
103
148
|
draw(ctx) {
|
|
104
149
|
ctx.save();
|
|
105
150
|
|
|
106
|
-
// Background
|
|
151
|
+
// 1. Background (toujours opaque)
|
|
107
152
|
ctx.fillStyle = this.pressed ? '#F5F5F5' : this.bgColor;
|
|
108
153
|
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
109
154
|
|
|
110
|
-
//
|
|
155
|
+
// 2. Ripples (si présents)
|
|
111
156
|
if (this.platform === 'material' && this.ripples.length > 0) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
ctx.clip();
|
|
116
|
-
|
|
117
|
-
for (let ripple of this.ripples) {
|
|
118
|
-
ctx.globalAlpha = ripple.opacity;
|
|
119
|
-
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
|
157
|
+
ctx.save();
|
|
158
|
+
|
|
159
|
+
// Clip pour contenir les ripples
|
|
120
160
|
ctx.beginPath();
|
|
121
|
-
ctx.
|
|
122
|
-
ctx.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
161
|
+
ctx.rect(this.x, this.y, this.width, this.height);
|
|
162
|
+
ctx.clip();
|
|
163
|
+
|
|
164
|
+
// Dessiner tous les ripples
|
|
165
|
+
for (let ripple of this.ripples) {
|
|
166
|
+
// Utiliser fillStyle avec alpha intégré au lieu de globalAlpha
|
|
167
|
+
ctx.fillStyle = `rgba(0, 0, 0, ${0.1 * ripple.opacity})`;
|
|
168
|
+
ctx.beginPath();
|
|
169
|
+
ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
|
|
170
|
+
ctx.fill();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ctx.restore();
|
|
126
174
|
}
|
|
127
175
|
|
|
176
|
+
// 3. Contenu (texte, icônes, etc.) - toujours avec alpha = 1
|
|
177
|
+
this.drawContent(ctx);
|
|
178
|
+
|
|
179
|
+
ctx.restore();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Dessine le contenu du ListItem (séparé pour plus de clarté)
|
|
184
|
+
*/
|
|
185
|
+
drawContent(ctx) {
|
|
128
186
|
let leftOffset = 16;
|
|
129
187
|
|
|
130
|
-
// Left Icon ou Image
|
|
131
188
|
if (this.leftIcon) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
189
|
+
ctx.fillStyle = '#757575';
|
|
190
|
+
ctx.font = '24px sans-serif';
|
|
191
|
+
ctx.textAlign = 'left';
|
|
192
|
+
ctx.textBaseline = 'middle';
|
|
193
|
+
ctx.fillText(this.leftIcon, this.x + leftOffset, this.y + this.height / 2);
|
|
194
|
+
leftOffset += 48;
|
|
138
195
|
} else if (this.leftImage) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
ctx.fillText('👤', this.x + leftOffset + 20, this.y + this.height / 2);
|
|
151
|
-
|
|
152
|
-
leftOffset += 56;
|
|
196
|
+
ctx.fillStyle = '#E0E0E0';
|
|
197
|
+
ctx.beginPath();
|
|
198
|
+
ctx.arc(this.x + leftOffset + 20, this.y + this.height / 2, 20, 0, Math.PI * 2);
|
|
199
|
+
ctx.fill();
|
|
200
|
+
|
|
201
|
+
ctx.fillStyle = '#757575';
|
|
202
|
+
ctx.font = '14px sans-serif';
|
|
203
|
+
ctx.textAlign = 'center';
|
|
204
|
+
ctx.textBaseline = 'middle';
|
|
205
|
+
ctx.fillText('👤', this.x + leftOffset + 20, this.y + this.height / 2);
|
|
206
|
+
leftOffset += 56;
|
|
153
207
|
}
|
|
154
208
|
|
|
155
|
-
// Title et Subtitle
|
|
156
209
|
const textX = this.x + leftOffset;
|
|
157
210
|
const centerY = this.y + this.height / 2;
|
|
158
211
|
|
|
159
212
|
if (this.subtitle) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
ctx.textBaseline = 'top';
|
|
171
|
-
ctx.fillText(this.subtitle, textX, centerY + 2);
|
|
213
|
+
ctx.fillStyle = '#000000';
|
|
214
|
+
ctx.font = '16px -apple-system, Roboto, sans-serif';
|
|
215
|
+
ctx.textAlign = 'left';
|
|
216
|
+
ctx.textBaseline = 'bottom';
|
|
217
|
+
ctx.fillText(this.title, textX, centerY - 2);
|
|
218
|
+
|
|
219
|
+
ctx.fillStyle = '#757575';
|
|
220
|
+
ctx.font = '14px -apple-system, Roboto, sans-serif';
|
|
221
|
+
ctx.textBaseline = 'top';
|
|
222
|
+
ctx.fillText(this.subtitle, textX, centerY + 2);
|
|
172
223
|
} else {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
ctx.fillText(this.title, textX, centerY);
|
|
224
|
+
ctx.fillStyle = '#000000';
|
|
225
|
+
ctx.font = '16px -apple-system, Roboto, sans-serif';
|
|
226
|
+
ctx.textAlign = 'left';
|
|
227
|
+
ctx.textBaseline = 'middle';
|
|
228
|
+
ctx.fillText(this.title, textX, centerY);
|
|
179
229
|
}
|
|
180
230
|
|
|
181
|
-
// Right Text ou Icon
|
|
182
231
|
if (this.rightText) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
232
|
+
ctx.fillStyle = '#757575';
|
|
233
|
+
ctx.font = '14px -apple-system, Roboto, sans-serif';
|
|
234
|
+
ctx.textAlign = 'right';
|
|
235
|
+
ctx.textBaseline = 'middle';
|
|
236
|
+
ctx.fillText(this.rightText, this.x + this.width - 16, centerY);
|
|
188
237
|
} else if (this.rightIcon) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
238
|
+
ctx.fillStyle = '#757575';
|
|
239
|
+
ctx.font = '20px sans-serif';
|
|
240
|
+
ctx.textAlign = 'right';
|
|
241
|
+
ctx.textBaseline = 'middle';
|
|
242
|
+
ctx.fillText(this.rightIcon, this.x + this.width - 16, centerY);
|
|
194
243
|
}
|
|
195
244
|
|
|
196
|
-
// Divider
|
|
197
245
|
if (this.divider) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
246
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
247
|
+
ctx.lineWidth = 1;
|
|
248
|
+
ctx.beginPath();
|
|
249
|
+
ctx.moveTo(this.x + leftOffset, this.y + this.height);
|
|
250
|
+
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
251
|
+
ctx.stroke();
|
|
204
252
|
}
|
|
205
|
-
|
|
206
|
-
ctx.restore();
|
|
207
|
-
}
|
|
253
|
+
}
|
|
208
254
|
|
|
209
255
|
/**
|
|
210
256
|
* Vérifie si un point est dans les limites
|
|
@@ -60,7 +60,6 @@ class SwipeableListItem extends Component {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const canvas = this.framework.canvas;
|
|
63
|
-
console.log('📡 Configuration des événements sur le canvas');
|
|
64
63
|
|
|
65
64
|
// Événements souris
|
|
66
65
|
canvas.addEventListener('mousedown', this.handleMouseDown);
|
|
@@ -123,7 +122,6 @@ class SwipeableListItem extends Component {
|
|
|
123
122
|
*/
|
|
124
123
|
handleMouseDown(event) {
|
|
125
124
|
const coords = this.getCoordinates(event);
|
|
126
|
-
console.log('🖱️ MouseDown', coords, 'isInside:', this.isPointInside(coords.x, coords.y));
|
|
127
125
|
this.onDragStart(coords.x, coords.y);
|
|
128
126
|
}
|
|
129
127
|
|
|
@@ -134,7 +132,6 @@ class SwipeableListItem extends Component {
|
|
|
134
132
|
handleMouseMove(event) {
|
|
135
133
|
if (!this.dragging) return;
|
|
136
134
|
const coords = this.getCoordinates(event);
|
|
137
|
-
console.log('🖱️ MouseMove', coords, 'dragging:', this.dragging);
|
|
138
135
|
this.onDragMove(coords.x, coords.y);
|
|
139
136
|
this.requestRender();
|
|
140
137
|
}
|
|
@@ -145,7 +142,6 @@ class SwipeableListItem extends Component {
|
|
|
145
142
|
*/
|
|
146
143
|
handleMouseUp(event) {
|
|
147
144
|
if (!this.dragging) return;
|
|
148
|
-
console.log('🖱️ MouseUp');
|
|
149
145
|
this.onDragEnd();
|
|
150
146
|
this.requestRender();
|
|
151
147
|
}
|
|
@@ -156,7 +152,6 @@ class SwipeableListItem extends Component {
|
|
|
156
152
|
*/
|
|
157
153
|
handleTouchStart(event) {
|
|
158
154
|
const coords = this.getCoordinates(event);
|
|
159
|
-
console.log('👆 TouchStart', coords, 'isInside:', this.isPointInside(coords.x, coords.y));
|
|
160
155
|
if (this.isPointInside(coords.x, coords.y)) {
|
|
161
156
|
event.preventDefault();
|
|
162
157
|
this.onDragStart(coords.x, coords.y);
|
|
@@ -171,7 +166,6 @@ class SwipeableListItem extends Component {
|
|
|
171
166
|
if (!this.dragging) return;
|
|
172
167
|
event.preventDefault();
|
|
173
168
|
const coords = this.getCoordinates(event);
|
|
174
|
-
console.log('👆 TouchMove', coords);
|
|
175
169
|
this.onDragMove(coords.x, coords.y);
|
|
176
170
|
this.requestRender();
|
|
177
171
|
}
|
|
@@ -183,7 +177,6 @@ class SwipeableListItem extends Component {
|
|
|
183
177
|
handleTouchEnd(event) {
|
|
184
178
|
if (!this.dragging) return;
|
|
185
179
|
event.preventDefault();
|
|
186
|
-
console.log('👆 TouchEnd');
|
|
187
180
|
this.onDragEnd();
|
|
188
181
|
this.requestRender();
|
|
189
182
|
}
|
|
@@ -205,14 +198,12 @@ class SwipeableListItem extends Component {
|
|
|
205
198
|
*/
|
|
206
199
|
onDragStart(x, y) {
|
|
207
200
|
const inside = this.isPointInside(x, y);
|
|
208
|
-
console.log('🟢 onDragStart', { x, y, inside, bounds: { x: this.x, y: this.y, w: this.width, h: this.height } });
|
|
209
201
|
|
|
210
202
|
if (inside) {
|
|
211
203
|
this.dragging = true;
|
|
212
204
|
this.startX = x;
|
|
213
205
|
this.startY = y;
|
|
214
206
|
this.hasMoved = false;
|
|
215
|
-
console.log('✅ Swipe démarré');
|
|
216
207
|
} else {
|
|
217
208
|
console.log('❌ Point en dehors des limites');
|
|
218
209
|
}
|
|
@@ -228,7 +219,6 @@ class SwipeableListItem extends Component {
|
|
|
228
219
|
const deltaX = x - this.startX;
|
|
229
220
|
const deltaY = Math.abs(y - this.startY);
|
|
230
221
|
|
|
231
|
-
console.log('🔄 onDragMove', { deltaX, deltaY, hasMoved: this.hasMoved });
|
|
232
222
|
|
|
233
223
|
// Swipe horizontal seulement si déplacement > 5px
|
|
234
224
|
if (Math.abs(deltaX) > 5 || this.hasMoved) {
|
|
@@ -242,7 +232,6 @@ class SwipeableListItem extends Component {
|
|
|
242
232
|
);
|
|
243
233
|
this.dragOffset = Math.min(Math.max(this.dragOffset, -maxOffset), maxOffset);
|
|
244
234
|
|
|
245
|
-
console.log('↔️ Offset mis à jour:', this.dragOffset);
|
|
246
235
|
}
|
|
247
236
|
}
|
|
248
237
|
}
|
|
@@ -252,14 +241,11 @@ class SwipeableListItem extends Component {
|
|
|
252
241
|
*/
|
|
253
242
|
onDragEnd() {
|
|
254
243
|
if (this.dragging) {
|
|
255
|
-
console.log('🔴 onDragEnd', { offset: this.dragOffset, hasMoved: this.hasMoved });
|
|
256
244
|
|
|
257
245
|
if (this.hasMoved) {
|
|
258
246
|
if (this.dragOffset > 80 && this.leftActions[0]) {
|
|
259
|
-
console.log('✅ Action gauche déclenchée');
|
|
260
247
|
this.leftActions[0].onClick?.();
|
|
261
248
|
} else if (this.dragOffset < -80 && this.rightActions[0]) {
|
|
262
|
-
console.log('✅ Action droite déclenchée');
|
|
263
249
|
this.rightActions[0].onClick?.();
|
|
264
250
|
}
|
|
265
251
|
}
|
|
@@ -334,12 +320,6 @@ class SwipeableListItem extends Component {
|
|
|
334
320
|
const inside = x >= this.x && x <= this.x + this.width &&
|
|
335
321
|
y >= this.y && y <= this.y + this.height;
|
|
336
322
|
|
|
337
|
-
console.log('🎯 isPointInside check', {
|
|
338
|
-
point: { x, y },
|
|
339
|
-
bounds: { x: this.x, y: this.y, width: this.width, height: this.height },
|
|
340
|
-
inside
|
|
341
|
-
});
|
|
342
|
-
|
|
343
323
|
return inside;
|
|
344
324
|
}
|
|
345
325
|
}
|
package/core/CanvasFramework.js
CHANGED
|
@@ -156,13 +156,14 @@ class CanvasFramework {
|
|
|
156
156
|
// ✅ AJOUTER: Démarrer le chronomètre
|
|
157
157
|
const startTime = performance.now();
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
// ✅ Créer automatiquement le canvas
|
|
160
|
+
this.canvas = document.createElement('canvas');
|
|
161
|
+
this.canvas.id = canvasId || `canvas-${Date.now()}`;
|
|
162
|
+
this.canvas.style.display = 'block';
|
|
163
|
+
this.canvas.style.touchAction = 'none';
|
|
164
|
+
this.canvas.style.userSelect = 'none';
|
|
165
|
+
document.body.appendChild(this.canvas);
|
|
166
|
+
|
|
166
167
|
// NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
|
|
167
168
|
this.useWebGL = options.useWebGL ?? false; // utilise la valeur si fournie, sinon false
|
|
168
169
|
|
|
@@ -999,71 +1000,73 @@ class CanvasFramework {
|
|
|
999
1000
|
* ✅ OPTIMISATION OPTION 2: Rendu partiel (seulement les composants sales)
|
|
1000
1001
|
* @private
|
|
1001
1002
|
*/
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1003
|
+
_renderDirtyComponents() {
|
|
1004
|
+
const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
|
|
1005
|
+
|
|
1006
|
+
if (!ctx || this.dirtyComponents.size === 0) return;
|
|
1007
|
+
|
|
1008
|
+
// ✅ CORRECTION : Ne pas nettoyer avec fillRect !
|
|
1009
|
+
// À la place, utiliser le clipping pour redessiner proprement
|
|
1010
|
+
|
|
1011
|
+
// Copier dans un tableau
|
|
1012
|
+
const dirtyArray = Array.from(this.dirtyComponents);
|
|
1013
|
+
|
|
1014
|
+
// Vider immédiatement
|
|
1015
|
+
this.dirtyComponents.clear();
|
|
1016
|
+
|
|
1017
|
+
// Séparer les composants
|
|
1018
|
+
const fixedComps = [];
|
|
1019
|
+
const scrollableComps = [];
|
|
1020
|
+
|
|
1021
|
+
dirtyArray.forEach(comp => {
|
|
1022
|
+
if (!comp.visible) return;
|
|
1023
|
+
|
|
1024
|
+
if (this.isFixedComponent(comp)) {
|
|
1025
|
+
fixedComps.push(comp);
|
|
1026
|
+
} else {
|
|
1027
|
+
scrollableComps.push(comp);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// ✅ APPROCHE CORRECTE : Redessiner les composants sales
|
|
1032
|
+
// sans effacer leur zone d'abord
|
|
1033
|
+
|
|
1034
|
+
// 1. Dessiner les scrollables
|
|
1035
|
+
if (scrollableComps.length > 0) {
|
|
1036
|
+
ctx.save();
|
|
1037
|
+
ctx.translate(0, this.scrollOffset);
|
|
1038
|
+
|
|
1039
|
+
for (let comp of scrollableComps) {
|
|
1040
|
+
// ✅ NE PAS faire de fillRect !
|
|
1041
|
+
// Juste dessiner le composant
|
|
1042
|
+
if (comp.draw) {
|
|
1043
|
+
comp.draw(ctx);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (comp.markClean) {
|
|
1047
|
+
comp.markClean();
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
ctx.restore();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// 2. Dessiner les fixes
|
|
1055
|
+
for (let comp of fixedComps) {
|
|
1056
|
+
if (comp.draw) {
|
|
1057
|
+
comp.draw(ctx);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (comp.markClean) {
|
|
1061
|
+
comp.markClean();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Flush si double buffering
|
|
1066
|
+
if (this.optimizations.useDoubleBuffering) {
|
|
1067
|
+
this.flush();
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1067
1070
|
|
|
1068
1071
|
/**
|
|
1069
1072
|
* Active/désactive une optimisation spécifique
|
|
@@ -2467,10 +2470,20 @@ class CanvasFramework {
|
|
|
2467
2470
|
|
|
2468
2471
|
// 4. Modifiez markComponentDirty() pour éviter de marquer pendant le scroll
|
|
2469
2472
|
markComponentDirty(component) {
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2473
|
+
// Vérifications basiques
|
|
2474
|
+
if (!component || !component.visible) return;
|
|
2475
|
+
|
|
2476
|
+
// ✅ TOUJOURS ajouter au set des composants sales
|
|
2477
|
+
// Les conditions de rendu seront vérifiées dans _renderDirtyComponents
|
|
2478
|
+
if (this.optimizationEnabled) {
|
|
2479
|
+
this.dirtyComponents.add(component);
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// ✅ Optionnel : Marquer aussi le composant lui-même
|
|
2483
|
+
if (component._dirty !== undefined) {
|
|
2484
|
+
component._dirty = true;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2474
2487
|
|
|
2475
2488
|
enableOptimization() {
|
|
2476
2489
|
this.optimizationEnabled = true;
|
|
@@ -2637,76 +2650,53 @@ class CanvasFramework {
|
|
|
2637
2650
|
}
|
|
2638
2651
|
|
|
2639
2652
|
startRenderLoop() {
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2653
|
+
let lastScrollOffset = this.scrollOffset;
|
|
2654
|
+
let lastRenderMode = 'full';
|
|
2655
|
+
|
|
2656
|
+
const render = () => {
|
|
2657
|
+
if (!this._splashFinished) {
|
|
2658
|
+
requestAnimationFrame(render);
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
// Vérifier le scroll
|
|
2663
|
+
const scrollChanged = Math.abs(this.scrollOffset - lastScrollOffset) > 0.1;
|
|
2664
|
+
lastScrollOffset = this.scrollOffset;
|
|
2665
|
+
|
|
2666
|
+
// Décider du mode de rendu
|
|
2667
|
+
let renderMode = 'full';
|
|
2668
|
+
|
|
2669
|
+
if (this.optimizationEnabled &&
|
|
2670
|
+
this.dirtyComponents.size > 0 &&
|
|
2671
|
+
!this.isDragging &&
|
|
2672
|
+
Math.abs(this.scrollVelocity) < 0.5 &&
|
|
2673
|
+
!scrollChanged &&
|
|
2674
|
+
this.dirtyComponents.size < 3) { // ✅ SEULEMENT si très peu de composants sales
|
|
2675
|
+
renderMode = 'dirty';
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// ✅ IMPORTANT : NE PAS clear tout le canvas au début
|
|
2679
|
+
// On ne nettoie que si on change de mode ou si c'est nécessaire
|
|
2680
|
+
|
|
2681
|
+
if (renderMode === 'full' || lastRenderMode !== renderMode) {
|
|
2682
|
+
// Rendu complet : clear tout
|
|
2654
2683
|
this.ctx.fillStyle = this.backgroundColor || '#ffffff';
|
|
2655
2684
|
this.ctx.fillRect(0, 0, this.width, this.height);
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
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
|
-
}
|
|
2685
|
+
this.renderFull();
|
|
2686
|
+
} else {
|
|
2687
|
+
// Rendu optimisé : ne clear QUE les zones des composants sales
|
|
2688
|
+
this._renderDirtyComponents();
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
lastRenderMode = renderMode;
|
|
2692
|
+
|
|
2693
|
+
// ... FPS et debug ...
|
|
2694
|
+
|
|
2695
|
+
requestAnimationFrame(render);
|
|
2696
|
+
};
|
|
2697
|
+
|
|
2698
|
+
render();
|
|
2699
|
+
}
|
|
2710
2700
|
|
|
2711
2701
|
// 3. Ajoutez une méthode renderFull() optimisée
|
|
2712
2702
|
renderFull() {
|
|
@@ -247,7 +247,7 @@ class PullToRefresh extends Component {
|
|
|
247
247
|
ctx.save();
|
|
248
248
|
|
|
249
249
|
const progress = Math.min(1, this.pullDistance / this.refreshThreshold);
|
|
250
|
-
const displayHeight = Math.min(this.pullDistance,
|
|
250
|
+
const displayHeight = Math.min(this.pullDistance, 230);
|
|
251
251
|
|
|
252
252
|
// Background
|
|
253
253
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|