canvasframework 0.5.0 → 0.5.2

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.
@@ -200,6 +200,7 @@ class Button extends Component {
200
200
 
201
201
  this.ripples = this.ripples.filter(r => r.opacity > 0);
202
202
 
203
+
203
204
  if (hasActiveRipples) {
204
205
  requestAnimationFrame(animate);
205
206
  }
@@ -53,16 +53,35 @@ class ListItem extends Component {
53
53
  * @param {number} y - Coordonnée Y
54
54
  * @private
55
55
  */
56
- handlePress(x, y) {
56
+ handlePress(x, y) {
57
57
  if (this.platform === 'material') {
58
- const adjustedY = y - this.framework.scrollOffset;
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: x - this.x,
61
- y: adjustedY - this.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
- if (ripple.radius < ripple.maxRadius) {
80
- ripple.radius += ripple.maxRadius / 15;
81
- hasActiveRipples = true;
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 (ripple.radius >= ripple.maxRadius * 0.5) {
85
- ripple.opacity -= 0.05;
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
- // Ripple effect (Material)
155
+ // 2. Ripples (si présents)
111
156
  if (this.platform === 'material' && this.ripples.length > 0) {
112
- ctx.save();
113
- ctx.beginPath();
114
- ctx.rect(this.x, this.y, this.width, this.height);
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.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
122
- ctx.fill();
123
- }
124
-
125
- ctx.restore();
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
- ctx.fillStyle = '#757575';
133
- ctx.font = '24px sans-serif';
134
- ctx.textAlign = 'left';
135
- ctx.textBaseline = 'middle';
136
- ctx.fillText(this.leftIcon, this.x + leftOffset, this.y + this.height / 2);
137
- leftOffset += 48;
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
- // Circle pour l'avatar
140
- ctx.fillStyle = '#E0E0E0';
141
- ctx.beginPath();
142
- ctx.arc(this.x + leftOffset + 20, this.y + this.height / 2, 20, 0, Math.PI * 2);
143
- ctx.fill();
144
-
145
- // TODO: Charger vraie image
146
- ctx.fillStyle = '#757575';
147
- ctx.font = '14px sans-serif';
148
- ctx.textAlign = 'center';
149
- ctx.textBaseline = 'middle';
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
- // Title
161
- ctx.fillStyle = '#000000';
162
- ctx.font = '16px -apple-system, Roboto, sans-serif';
163
- ctx.textAlign = 'left';
164
- ctx.textBaseline = 'bottom';
165
- ctx.fillText(this.title, textX, centerY - 2);
166
-
167
- // Subtitle
168
- ctx.fillStyle = '#757575';
169
- ctx.font = '14px -apple-system, Roboto, sans-serif';
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
- // Title seul (centré verticalement)
174
- ctx.fillStyle = '#000000';
175
- ctx.font = '16px -apple-system, Roboto, sans-serif';
176
- ctx.textAlign = 'left';
177
- ctx.textBaseline = 'middle';
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
- ctx.fillStyle = '#757575';
184
- ctx.font = '14px -apple-system, Roboto, sans-serif';
185
- ctx.textAlign = 'right';
186
- ctx.textBaseline = 'middle';
187
- ctx.fillText(this.rightText, this.x + this.width - 16, centerY);
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
- ctx.fillStyle = '#757575';
190
- ctx.font = '20px sans-serif';
191
- ctx.textAlign = 'right';
192
- ctx.textBaseline = 'middle';
193
- ctx.fillText(this.rightIcon, this.x + this.width - 16, centerY);
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
- ctx.strokeStyle = '#E0E0E0';
199
- ctx.lineWidth = 1;
200
- ctx.beginPath();
201
- ctx.moveTo(this.x + leftOffset, this.y + this.height);
202
- ctx.lineTo(this.x + this.width, this.y + this.height);
203
- ctx.stroke();
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
  }
@@ -155,14 +155,24 @@ 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é
160
- this.canvas = document.getElementById(canvasId);
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
- });*/
158
+
159
+ this.metrics = {
160
+ initTime: 0,
161
+ firstRenderTime: null,
162
+ firstInteractionTime: null,
163
+ totalStartupTime: null
164
+ };
165
+ this._firstRenderDone = false;
166
+ this._startupStartTime = startTime;
167
+
168
+ // ✅ Créer automatiquement le canvas
169
+ this.canvas = document.createElement('canvas');
170
+ this.canvas.id = canvasId || `canvas-${Date.now()}`;
171
+ this.canvas.style.display = 'block';
172
+ this.canvas.style.touchAction = 'none';
173
+ this.canvas.style.userSelect = 'none';
174
+ document.body.appendChild(this.canvas);
175
+
166
176
  // NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
167
177
  this.useWebGL = options.useWebGL ?? false; // utilise la valeur si fournie, sinon false
168
178
 
@@ -999,71 +1009,73 @@ class CanvasFramework {
999
1009
  * ✅ OPTIMISATION OPTION 2: Rendu partiel (seulement les composants sales)
1000
1010
  * @private
1001
1011
  */
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
- }
1012
+ _renderDirtyComponents() {
1013
+ const ctx = this.optimizations.useDoubleBuffering ? this._bufferCtx : this.ctx;
1014
+
1015
+ if (!ctx || this.dirtyComponents.size === 0) return;
1016
+
1017
+ // CORRECTION : Ne pas nettoyer avec fillRect !
1018
+ // À la place, utiliser le clipping pour redessiner proprement
1019
+
1020
+ // Copier dans un tableau
1021
+ const dirtyArray = Array.from(this.dirtyComponents);
1022
+
1023
+ // Vider immédiatement
1024
+ this.dirtyComponents.clear();
1025
+
1026
+ // Séparer les composants
1027
+ const fixedComps = [];
1028
+ const scrollableComps = [];
1029
+
1030
+ dirtyArray.forEach(comp => {
1031
+ if (!comp.visible) return;
1032
+
1033
+ if (this.isFixedComponent(comp)) {
1034
+ fixedComps.push(comp);
1035
+ } else {
1036
+ scrollableComps.push(comp);
1037
+ }
1038
+ });
1039
+
1040
+ // APPROCHE CORRECTE : Redessiner les composants sales
1041
+ // sans effacer leur zone d'abord
1042
+
1043
+ // 1. Dessiner les scrollables
1044
+ if (scrollableComps.length > 0) {
1045
+ ctx.save();
1046
+ ctx.translate(0, this.scrollOffset);
1047
+
1048
+ for (let comp of scrollableComps) {
1049
+ // ✅ NE PAS faire de fillRect !
1050
+ // Juste dessiner le composant
1051
+ if (comp.draw) {
1052
+ comp.draw(ctx);
1053
+ }
1054
+
1055
+ if (comp.markClean) {
1056
+ comp.markClean();
1057
+ }
1058
+ }
1059
+
1060
+ ctx.restore();
1061
+ }
1062
+
1063
+ // 2. Dessiner les fixes
1064
+ for (let comp of fixedComps) {
1065
+ if (comp.draw) {
1066
+ comp.draw(ctx);
1067
+ }
1068
+
1069
+ if (comp.markClean) {
1070
+ comp.markClean();
1071
+ }
1072
+ }
1073
+
1074
+ // Flush si double buffering
1075
+ if (this.optimizations.useDoubleBuffering) {
1076
+ this.flush();
1077
+ }
1078
+ }
1067
1079
 
1068
1080
  /**
1069
1081
  * Active/désactive une optimisation spécifique
@@ -2467,10 +2479,20 @@ class CanvasFramework {
2467
2479
 
2468
2480
  // 4. Modifiez markComponentDirty() pour éviter de marquer pendant le scroll
2469
2481
  markComponentDirty(component) {
2470
- if (this.optimizationEnabled && !this.isDragging && Math.abs(this.scrollVelocity) < 5) {
2471
- this.dirtyComponents.add(component);
2472
- }
2473
- }
2482
+ // Vérifications basiques
2483
+ if (!component || !component.visible) return;
2484
+
2485
+ // ✅ TOUJOURS ajouter au set des composants sales
2486
+ // Les conditions de rendu seront vérifiées dans _renderDirtyComponents
2487
+ if (this.optimizationEnabled) {
2488
+ this.dirtyComponents.add(component);
2489
+ }
2490
+
2491
+ // ✅ Optionnel : Marquer aussi le composant lui-même
2492
+ if (component._dirty !== undefined) {
2493
+ component._dirty = true;
2494
+ }
2495
+ }
2474
2496
 
2475
2497
  enableOptimization() {
2476
2498
  this.optimizationEnabled = true;
@@ -2636,77 +2658,86 @@ class CanvasFramework {
2636
2658
  }
2637
2659
  }
2638
2660
 
2639
- startRenderLoop() {
2640
- let lastScrollOffset = this.scrollOffset;
2641
- let framesWithoutScroll = 0;
2642
-
2643
- const render = () => {
2644
- if (!this._splashFinished) {
2645
- requestAnimationFrame(render);
2646
- return;
2647
- }
2648
-
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
- }
2661
+ startRenderLoop() {
2662
+ let lastScrollOffset = this.scrollOffset;
2663
+ let lastRenderMode = 'full';
2664
+
2665
+ const render = () => {
2666
+ if (!this._splashFinished) {
2667
+ requestAnimationFrame(render);
2668
+ return;
2669
+ }
2670
+
2671
+ // Vérifier le scroll
2672
+ const scrollChanged = Math.abs(this.scrollOffset - lastScrollOffset) > 0.1;
2673
+ lastScrollOffset = this.scrollOffset;
2674
+
2675
+ // Décider du mode de rendu
2676
+ let renderMode = 'full';
2677
+
2678
+ if (this.optimizationEnabled &&
2679
+ this.dirtyComponents.size > 0 &&
2680
+ !this.isDragging &&
2681
+ Math.abs(this.scrollVelocity) < 0.5 &&
2682
+ !scrollChanged &&
2683
+ this.dirtyComponents.size < 3) {
2684
+ renderMode = 'dirty';
2685
+ }
2686
+
2687
+ if (renderMode === 'full' || lastRenderMode !== renderMode) {
2688
+ this.ctx.fillStyle = this.backgroundColor || '#ffffff';
2689
+ this.ctx.fillRect(0, 0, this.width, this.height);
2690
+ this.renderFull();
2691
+ } else {
2692
+ this._renderDirtyComponents();
2693
+ }
2694
+
2695
+ lastRenderMode = renderMode;
2696
+
2697
+ // AJOUTER : Calcul et affichage du FPS
2698
+ this._frames++;
2699
+ const now = performance.now();
2700
+ const elapsed = now - this._lastFpsTime;
2701
+
2702
+ if (elapsed >= 1000) {
2703
+ this.fps = Math.round((this._frames * 1000) / elapsed);
2704
+ this._frames = 0;
2705
+ this._lastFpsTime = now;
2706
+ }
2707
+
2708
+ // AJOUTER : Afficher le FPS si activé
2709
+ if (this.showFps) {
2710
+ this.ctx.save();
2711
+ this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
2712
+ this.ctx.fillRect(10, 10, 100, 40);
2713
+ this.ctx.fillStyle = '#00ff00';
2714
+ this.ctx.font = 'bold 20px monospace';
2715
+ this.ctx.textAlign = 'left';
2716
+ this.ctx.textBaseline = 'top';
2717
+ this.ctx.fillText(`FPS: ${this.fps}`, 20, 20);
2718
+ this.ctx.restore();
2719
+ }
2720
+
2721
+ // ✅ AJOUTER : Indicateurs de débogage si activés
2722
+ if (this.debbug) {
2723
+ this.drawOverflowIndicators();
2724
+ }
2725
+
2726
+ // ✅ AJOUTER : Marquer le premier rendu
2727
+ if (!this._firstRenderDone) {
2728
+ this._markFirstRender();
2729
+ }
2730
+
2731
+ // ✅ AJOUTER : Mettre à jour l'inertie si nécessaire
2732
+ if (Math.abs(this.scrollVelocity) > 0.1 && !this.isDragging) {
2733
+ this.scrollWorker.postMessage({ type: 'UPDATE_INERTIA' });
2734
+ }
2735
+
2736
+ requestAnimationFrame(render);
2737
+ };
2738
+
2739
+ render();
2740
+ }
2710
2741
 
2711
2742
  // 3. Ajoutez une méthode renderFull() optimisée
2712
2743
  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, 100);
250
+ const displayHeight = Math.min(this.pullDistance, 230);
251
251
 
252
252
  // Background
253
253
  ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",