canvasframework 0.5.46 → 0.5.47

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.
@@ -1,280 +1,305 @@
1
1
  import Component from '../core/Component.js';
2
- /**
3
- * Stepper (incrémenteur/décrémenteur)
4
- * @class
5
- * @extends Component
6
- * @property {number} value - Valeur
7
- * @property {number} min - Minimum
8
- * @property {number} max - Maximum
9
- * @property {number} step - Pas d'incrémentation
10
- * @property {string} platform - Plateforme
11
- * @property {Function} onChange - Callback au changement
12
- * @property {number} buttonWidth - Largeur des boutons
13
- * @property {boolean} decrementPressed - Bouton décrémenter pressé
14
- * @property {boolean} incrementPressed - Bouton incrémenter pressé
15
- */
2
+
16
3
  class Stepper extends Component {
17
- /**
18
- * Crée une instance de Stepper
19
- * @param {CanvasFramework} framework - Framework parent
20
- * @param {Object} [options={}] - Options de configuration
21
- * @param {number} [options.value=0] - Valeur initiale
22
- * @param {number} [options.min=0] - Minimum
23
- * @param {number} [options.max=100] - Maximum
24
- * @param {number} [options.step=1] - Pas
25
- * @param {Function} [options.onChange] - Callback au changement
26
- * @param {number} [options.width=120] - Largeur
27
- * @param {number} [options.height=40] - Hauteur
28
- */
29
4
  constructor(framework, options = {}) {
30
5
  super(framework, options);
31
6
  this.value = options.value || 0;
32
- this.min = options.min !== undefined ? options.min : 0;
33
- this.max = options.max !== undefined ? options.max : 100;
7
+ this.min = options.min ?? 0;
8
+ this.max = options.max ?? 100;
34
9
  this.step = options.step || 1;
35
10
  this.platform = framework.platform;
36
11
  this.onChange = options.onChange;
37
12
  this.width = options.width || 120;
38
13
  this.height = options.height || 40;
39
14
  this.buttonWidth = this.height;
40
-
15
+
41
16
  this.decrementPressed = false;
42
17
  this.incrementPressed = false;
18
+ this.ripples = [];
19
+
20
+ this.scale = 1;
21
+
22
+ // SOLUTION: Distinguer entre press (début) et release (fin)
23
+ this._pressStartTime = 0;
24
+ this._pressButton = null; // 'decrement' ou 'increment'
25
+ this._pressActive = false;
26
+ this._clickThreshold = 200; // ms
43
27
 
44
28
  this.onPress = this.handlePress.bind(this);
45
29
  }
46
-
47
- /**
48
- * Incrémente la valeur
49
- */
30
+
50
31
  increment() {
51
32
  if (this.value + this.step <= this.max) {
52
33
  this.value += this.step;
34
+ this.animateScale();
53
35
  if (this.onChange) this.onChange(this.value);
54
36
  }
55
37
  }
56
-
57
- /**
58
- * Décrémente la valeur
59
- */
38
+
60
39
  decrement() {
61
40
  if (this.value - this.step >= this.min) {
62
41
  this.value -= this.step;
42
+ this.animateScale();
63
43
  if (this.onChange) this.onChange(this.value);
64
44
  }
65
45
  }
66
-
67
- /**
68
- * Définit la valeur
69
- * @param {number} value - Nouvelle valeur
70
- */
71
- setValue(value) {
72
- this.value = Math.max(this.min, Math.min(this.max, value));
73
- if (this.onChange) this.onChange(this.value);
46
+
47
+ animateScale() {
48
+ this.scale = 1;
49
+ const animate = () => {
50
+ if (this.scale < 1) {
51
+ this.scale += 0.05;
52
+ if (this.scale > 1) this.scale = 1;
53
+ requestAnimationFrame(animate);
54
+ }
55
+ };
56
+ animate();
57
+ }
58
+
59
+ createRipple(x, y, buttonWidth) {
60
+ const radius = Math.min(this.height / 2, buttonWidth / 2);
61
+ const ripple = { x, y, radius: 0, maxRadius: radius, opacity: 0.3 };
62
+ this.ripples.push(ripple);
63
+
64
+ const animate = () => {
65
+ ripple.radius += ripple.maxRadius / 10;
66
+ ripple.opacity -= 0.03;
67
+ if (ripple.opacity > 0) requestAnimationFrame(animate);
68
+ else this.ripples = this.ripples.filter(r => r.opacity > 0);
69
+ };
70
+ animate();
74
71
  }
75
-
76
- /**
77
- * Dessine le stepper
78
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
79
- */
80
- draw(ctx) {
81
- ctx.save();
72
+
73
+ // SOLUTION: handlePress est appelé DEUX FOIS par le framework
74
+ handlePress(x, y) {
75
+ const adjustedY = y - this.framework.scrollOffset;
76
+ const now = Date.now();
82
77
 
83
- if (this.platform === 'material') {
84
- // Material Design Stepper
85
-
86
- // Bouton décrement (-)
87
- const canDecrement = this.value > this.min;
88
- ctx.fillStyle = this.decrementPressed ? '#E0E0E0' : '#F5F5F5';
89
- ctx.strokeStyle = canDecrement ? '#6200EE' : '#CCCCCC';
90
- ctx.lineWidth = 2;
91
-
92
- ctx.beginPath();
93
- this.roundRect(ctx, this.x, this.y, this.buttonWidth, this.height, 4);
94
- ctx.fill();
95
- ctx.stroke();
78
+ // DÉBUT DU PRESS (premier appel du framework)
79
+ if (!this._pressActive) {
80
+ console.log('🟢 DEBUT press - Premier appel');
96
81
 
97
- // Icône -
98
- ctx.strokeStyle = canDecrement ? '#6200EE' : '#CCCCCC';
99
- ctx.lineWidth = 2;
100
- ctx.lineCap = 'round';
101
- ctx.beginPath();
102
- ctx.moveTo(this.x + this.buttonWidth / 2 - 8, this.y + this.height / 2);
103
- ctx.lineTo(this.x + this.buttonWidth / 2 + 8, this.y + this.height / 2);
104
- ctx.stroke();
105
-
106
- // Zone centrale (valeur)
107
- ctx.fillStyle = '#FFFFFF';
108
- ctx.fillRect(this.x + this.buttonWidth, this.y,
109
- this.width - (this.buttonWidth * 2), this.height);
110
-
111
- ctx.strokeStyle = '#E0E0E0';
112
- ctx.lineWidth = 1;
113
- ctx.strokeRect(this.x + this.buttonWidth, this.y,
114
- this.width - (this.buttonWidth * 2), this.height);
115
-
116
- // Valeur
117
- ctx.fillStyle = '#000000';
118
- ctx.font = 'bold 16px Roboto, sans-serif';
119
- ctx.textAlign = 'center';
120
- ctx.textBaseline = 'middle';
121
- ctx.fillText(this.value.toString(), this.x + this.width / 2, this.y + this.height / 2);
122
-
123
- // Bouton increment (+)
124
- const canIncrement = this.value < this.max;
125
- ctx.fillStyle = this.incrementPressed ? '#E0E0E0' : '#F5F5F5';
126
- ctx.strokeStyle = canIncrement ? '#6200EE' : '#CCCCCC';
127
- ctx.lineWidth = 2;
128
-
129
- ctx.beginPath();
130
- this.roundRect(ctx, this.x + this.width - this.buttonWidth, this.y,
131
- this.buttonWidth, this.height, 4);
132
- ctx.fill();
133
- ctx.stroke();
134
-
135
- // Icône +
136
- ctx.strokeStyle = canIncrement ? '#6200EE' : '#CCCCCC';
137
- ctx.lineWidth = 2;
138
- ctx.lineCap = 'round';
139
- const plusX = this.x + this.width - this.buttonWidth / 2;
140
- const plusY = this.y + this.height / 2;
141
-
142
- ctx.beginPath();
143
- ctx.moveTo(plusX - 8, plusY);
144
- ctx.lineTo(plusX + 8, plusY);
145
- ctx.stroke();
146
-
147
- ctx.beginPath();
148
- ctx.moveTo(plusX, plusY - 8);
149
- ctx.lineTo(plusX, plusY + 8);
150
- ctx.stroke();
151
-
152
- } else {
153
- // Cupertino (iOS) Stepper
82
+ // Déterminer quel bouton est pressé
83
+ if (x >= this.x && x <= this.x + this.buttonWidth) {
84
+ this._pressButton = 'decrement';
85
+ this.decrementPressed = true;
86
+ this._pressStartTime = now;
87
+ this._pressActive = true;
88
+
89
+ // Feedback visuel immédiat
90
+ if (this.platform === 'material') {
91
+ this.createRipple(this.x + this.buttonWidth / 2, this.y + this.height / 2);
92
+ }
93
+ }
94
+ else if (x >= this.x + this.width - this.buttonWidth && x <= this.x + this.width) {
95
+ this._pressButton = 'increment';
96
+ this.incrementPressed = true;
97
+ this._pressStartTime = now;
98
+ this._pressActive = true;
99
+
100
+ // Feedback visuel immédiat
101
+ if (this.platform === 'material') {
102
+ this.createRipple(this.x + this.width - this.buttonWidth / 2, this.y + this.height / 2);
103
+ }
104
+ }
154
105
 
155
- // Container
156
- ctx.strokeStyle = '#C7C7CC';
157
- ctx.lineWidth = 1;
158
- ctx.beginPath();
159
- this.roundRect(ctx, this.x, this.y, this.width, this.height, this.height / 2);
160
- ctx.stroke();
106
+ // Marquer pour redessin
107
+ this.markDirty();
108
+ }
109
+ // FIN DU PRESS (deuxième appel du framework) - C'EST LE VRAI CLIC
110
+ else {
111
+ console.log('🔴 FIN press - Deuxième appel (vrai clic)');
161
112
 
162
- // Divider central
163
- ctx.beginPath();
164
- ctx.moveTo(this.x + this.width / 2, this.y);
165
- ctx.lineTo(this.x + this.width / 2, this.y + this.height);
166
- ctx.stroke();
113
+ // Vérifier que c'est toujours le même bouton
114
+ let sameButton = false;
167
115
 
168
- // Bouton décrement (-)
169
- const canDecrement = this.value > this.min;
170
- if (this.decrementPressed) {
171
- ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
172
- ctx.beginPath();
173
- ctx.arc(this.x + this.width / 4, this.y + this.height / 2,
174
- this.height / 2 - 2, Math.PI / 2, Math.PI * 1.5);
175
- ctx.lineTo(this.x + this.width / 2, this.y);
176
- ctx.lineTo(this.x + this.width / 2, this.y + this.height);
177
- ctx.closePath();
178
- ctx.fill();
116
+ if (this._pressButton === 'decrement' &&
117
+ x >= this.x && x <= this.x + this.buttonWidth) {
118
+ sameButton = true;
179
119
  }
180
-
181
- ctx.strokeStyle = canDecrement ? '#007AFF' : '#C7C7CC';
182
- ctx.lineWidth = 2;
183
- ctx.lineCap = 'round';
184
- ctx.beginPath();
185
- ctx.moveTo(this.x + this.width / 4 - 10, this.y + this.height / 2);
186
- ctx.lineTo(this.x + this.width / 4 + 10, this.y + this.height / 2);
187
- ctx.stroke();
188
-
189
- // Bouton increment (+)
190
- const canIncrement = this.value < this.max;
191
- if (this.incrementPressed) {
192
- ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
193
- ctx.beginPath();
194
- ctx.arc(this.x + (this.width * 3 / 4), this.y + this.height / 2,
195
- this.height / 2 - 2, Math.PI * 1.5, Math.PI / 2);
196
- ctx.lineTo(this.x + this.width / 2, this.y + this.height);
197
- ctx.lineTo(this.x + this.width / 2, this.y);
198
- ctx.closePath();
199
- ctx.fill();
120
+ else if (this._pressButton === 'increment' &&
121
+ x >= this.x + this.width - this.buttonWidth && x <= this.x + this.width) {
122
+ sameButton = true;
200
123
  }
201
124
 
202
- ctx.strokeStyle = canIncrement ? '#007AFF' : '#C7C7CC';
203
- ctx.lineWidth = 2;
204
- ctx.lineCap = 'round';
205
- const plusX = this.x + (this.width * 3 / 4);
206
- const plusY = this.y + this.height / 2;
207
-
208
- ctx.beginPath();
209
- ctx.moveTo(plusX - 10, plusY);
210
- ctx.lineTo(plusX + 10, plusY);
211
- ctx.stroke();
125
+ // Exécuter l'action SEULEMENT sur le deuxième appel (le vrai clic)
126
+ if (sameButton && (now - this._pressStartTime) < 500) {
127
+ if (this._pressButton === 'decrement') {
128
+ this.decrement();
129
+ } else if (this._pressButton === 'increment') {
130
+ this.increment();
131
+ }
132
+ }
212
133
 
213
- ctx.beginPath();
214
- ctx.moveTo(plusX, plusY - 10);
215
- ctx.lineTo(plusX, plusY + 10);
216
- ctx.stroke();
134
+ // Reset après un court délai
135
+ setTimeout(() => {
136
+ this.decrementPressed = false;
137
+ this.incrementPressed = false;
138
+ this._pressActive = false;
139
+ this._pressButton = null;
140
+ this.markDirty();
141
+ }, 150);
217
142
  }
218
-
219
- ctx.restore();
220
143
  }
221
-
222
- /**
223
- * Gère la pression (clic)
224
- * @param {number} x - Coordonnée X
225
- * @param {number} y - Coordonnée Y
226
- * @private
227
- */
228
- handlePress(x, y) {
229
- const adjustedY = y - this.framework.scrollOffset;
230
-
231
- // Bouton décrement (gauche)
232
- if (x >= this.x && x <= this.x + this.buttonWidth) {
233
- this.decrementPressed = true;
234
- this.decrement();
235
- setTimeout(() => { this.decrementPressed = false; }, 150);
236
- }
237
- // Bouton increment (droite)
238
- else if (x >= this.x + this.width - this.buttonWidth && x <= this.x + this.width) {
239
- this.incrementPressed = true;
240
- this.increment();
241
- setTimeout(() => { this.incrementPressed = false; }, 150);
242
- }
144
+
145
+ draw(ctx) {
146
+ ctx.save();
147
+
148
+ // 1️⃣ Draw ripples FIRST
149
+ for (let ripple of this.ripples) {
150
+ ctx.fillStyle = `rgba(0,0,0,${ripple.opacity})`;
151
+ ctx.beginPath();
152
+ ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI*2);
153
+ ctx.fill();
154
+ }
155
+
156
+ // 2️⃣ Scale only the pressed button (optional)
157
+ ctx.translate(this.x + this.width/2, this.y + this.height/2);
158
+ ctx.scale(this.scale, this.scale);
159
+ ctx.translate(-(this.x + this.width/2), -(this.y + this.height/2));
160
+
161
+ // Draw buttons + value
162
+ if (this.platform === 'material') {
163
+ this.drawMaterial(ctx);
164
+ } else {
165
+ this.drawCupertino(ctx);
166
+ }
167
+
168
+ ctx.restore();
169
+ }
170
+
171
+ drawMaterial(ctx) {
172
+ const canDecrement = this.value > this.min;
173
+ const canIncrement = this.value < this.max;
174
+
175
+ // Décrement
176
+ ctx.fillStyle = this.decrementPressed ? '#E0E0E0' : '#F5F5F5';
177
+ ctx.strokeStyle = canDecrement ? '#6200EE' : '#CCCCCC';
178
+ ctx.lineWidth = 2;
179
+ this.roundRect(ctx, this.x, this.y, this.buttonWidth, this.height, 4);
180
+ ctx.fill();
181
+ ctx.stroke();
182
+
183
+ ctx.strokeStyle = canDecrement ? '#6200EE' : '#CCCCCC';
184
+ ctx.lineWidth = 2;
185
+ ctx.lineCap = 'round';
186
+ ctx.beginPath();
187
+ ctx.moveTo(this.x + this.buttonWidth/2 - 8, this.y + this.height/2);
188
+ ctx.lineTo(this.x + this.buttonWidth/2 + 8, this.y + this.height/2);
189
+ ctx.stroke();
190
+
191
+ // Value box
192
+ ctx.fillStyle = '#FFFFFF';
193
+ ctx.fillRect(this.x + this.buttonWidth, this.y, this.width - 2*this.buttonWidth, this.height);
194
+ ctx.strokeStyle = '#E0E0E0';
195
+ ctx.lineWidth = 1;
196
+ ctx.strokeRect(this.x + this.buttonWidth, this.y, this.width - 2*this.buttonWidth, this.height);
197
+
198
+ ctx.fillStyle = '#000000';
199
+ ctx.font = 'bold 16px Roboto, sans-serif';
200
+ ctx.textAlign = 'center';
201
+ ctx.textBaseline = 'middle';
202
+ ctx.fillText(this.value.toString(), this.x + this.width/2, this.y + this.height/2);
203
+
204
+ // Increment
205
+ ctx.fillStyle = this.incrementPressed ? '#E0E0E0' : '#F5F5F5';
206
+ ctx.strokeStyle = canIncrement ? '#6200EE' : '#CCCCCC';
207
+ ctx.lineWidth = 2;
208
+ this.roundRect(ctx, this.x + this.width - this.buttonWidth, this.y, this.buttonWidth, this.height, 4);
209
+ ctx.fill();
210
+ ctx.stroke();
211
+
212
+ ctx.strokeStyle = canIncrement ? '#6200EE' : '#CCCCCC';
213
+ ctx.lineWidth = 2;
214
+ ctx.lineCap = 'round';
215
+ const plusX = this.x + this.width - this.buttonWidth/2;
216
+ const plusY = this.y + this.height/2;
217
+ ctx.beginPath();
218
+ ctx.moveTo(plusX - 8, plusY);
219
+ ctx.lineTo(plusX + 8, plusY);
220
+ ctx.stroke();
221
+ ctx.beginPath();
222
+ ctx.moveTo(plusX, plusY - 8);
223
+ ctx.lineTo(plusX, plusY + 8);
224
+ ctx.stroke();
225
+ }
226
+
227
+ drawCupertino(ctx) {
228
+ const canDecrement = this.value > this.min;
229
+ const canIncrement = this.value < this.max;
230
+
231
+ // Container
232
+ ctx.strokeStyle = '#C7C7CC';
233
+ ctx.lineWidth = 1;
234
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.height/2);
235
+ ctx.stroke();
236
+
237
+ // Divider
238
+ ctx.beginPath();
239
+ ctx.moveTo(this.x + this.width/2, this.y);
240
+ ctx.lineTo(this.x + this.width/2, this.y + this.height);
241
+ ctx.stroke();
242
+
243
+ // Décrement
244
+ if (this.decrementPressed) {
245
+ ctx.fillStyle = 'rgba(0,0,0,0.1)';
246
+ this.roundRect(ctx, this.x, this.y, this.width/2, this.height, this.height/2);
247
+ ctx.fill();
248
+ }
249
+
250
+ ctx.strokeStyle = canDecrement ? '#007AFF' : '#C7C7CC';
251
+ ctx.lineWidth = 2;
252
+ ctx.lineCap = 'round';
253
+ ctx.beginPath();
254
+ ctx.moveTo(this.x + this.width/4 - 10, this.y + this.height/2);
255
+ ctx.lineTo(this.x + this.width/4 + 10, this.y + this.height/2);
256
+ ctx.stroke();
257
+
258
+ if (this.incrementPressed) {
259
+ ctx.fillStyle = 'rgba(0,0,0,0.1)';
260
+ this.roundRect(ctx, this.x + this.width/2, this.y, this.width/2, this.height, this.height/2);
261
+ ctx.fill();
262
+ }
263
+ ctx.strokeStyle = canIncrement ? '#007AFF' : '#C7C7CC';
264
+ ctx.lineWidth = 2;
265
+ ctx.lineCap = 'round';
266
+ const plusX = this.x + (this.width*3/4);
267
+ const plusY = this.y + this.height/2;
268
+ ctx.beginPath();
269
+ ctx.moveTo(plusX - 10, plusY);
270
+ ctx.lineTo(plusX + 10, plusY);
271
+ ctx.stroke();
272
+ ctx.beginPath();
273
+ ctx.moveTo(plusX, plusY - 10);
274
+ ctx.lineTo(plusX, plusY + 10);
275
+ ctx.stroke();
276
+
277
+ // Value
278
+ ctx.fillStyle = '#000';
279
+ ctx.font = 'bold 16px -apple-system, sans-serif';
280
+ ctx.textAlign = 'center';
281
+ ctx.textBaseline = 'middle';
282
+ ctx.fillText(this.value.toString(), this.x + this.width/2, this.y + this.height/2);
243
283
  }
244
-
245
- /**
246
- * Dessine un rectangle avec coins arrondis
247
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
248
- * @param {number} x - Position X
249
- * @param {number} y - Position Y
250
- * @param {number} width - Largeur
251
- * @param {number} height - Hauteur
252
- * @param {number} radius - Rayon des coins
253
- * @private
254
- */
284
+
255
285
  roundRect(ctx, x, y, width, height, radius) {
256
286
  ctx.beginPath();
257
- ctx.moveTo(x + radius, y);
258
- ctx.lineTo(x + width - radius, y);
259
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
260
- ctx.lineTo(x + width, y + height - radius);
261
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
262
- ctx.lineTo(x + radius, y + height);
263
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
264
- ctx.lineTo(x, y + radius);
265
- ctx.quadraticCurveTo(x, y, x + radius, y);
287
+ ctx.moveTo(x+radius, y);
288
+ ctx.lineTo(x+width-radius, y);
289
+ ctx.quadraticCurveTo(x+width, y, x+width, y+radius);
290
+ ctx.lineTo(x+width, y+height-radius);
291
+ ctx.quadraticCurveTo(x+width, y+height, x+width-radius, y+height);
292
+ ctx.lineTo(x+radius, y+height);
293
+ ctx.quadraticCurveTo(x, y+height, x, y+height-radius);
294
+ ctx.lineTo(x, y+radius);
295
+ ctx.quadraticCurveTo(x, y, x+radius, y);
266
296
  ctx.closePath();
267
297
  }
268
-
269
- /**
270
- * Vérifie si un point est dans les limites
271
- * @param {number} x - Coordonnée X
272
- * @param {number} y - Coordonnée Y
273
- * @returns {boolean} True si le point est dans le stepper
274
- */
298
+
275
299
  isPointInside(x, y) {
300
+ const adjustedY = y - this.framework.scrollOffset;
276
301
  return x >= this.x && x <= this.x + this.width &&
277
- y >= this.y && y <= this.y + this.height;
302
+ adjustedY >= this.y && adjustedY <= this.y + this.height;
278
303
  }
279
304
  }
280
305
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.5.46",
3
+ "version": "0.5.47",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/beyons/CanvasFramework.git"