canvasframework 0.5.58 → 0.5.60

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.
@@ -0,0 +1,891 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Sélecteur de couleur avec variantes Material et Cupertino
5
+ * @class
6
+ * @extends Component
7
+ *
8
+ * Material: Palette + sliders HSL
9
+ * Cupertino: Style iOS avec grille de couleurs
10
+ *
11
+ * VERSION OPTIMISÉE SANS CLIGNOTEMENT
12
+ */
13
+ class ColorPicker extends Component {
14
+ constructor(framework, options = {}) {
15
+ super(framework, options);
16
+
17
+ this.platform = framework.platform;
18
+ this.showHex = options.showHex !== false;
19
+ this.showAlpha = options.showAlpha !== false;
20
+ this.onColorChange = options.onColorChange || (() => {});
21
+
22
+ // Couleur initiale
23
+ const initialColor = options.color || '#6200EE';
24
+ const rgb = this.hexToRgb(initialColor);
25
+ const hsv = this.rgbToHsv(rgb.r, rgb.g, rgb.b);
26
+
27
+ this.hue = hsv.h;
28
+ this.saturation = hsv.s;
29
+ this.value = hsv.v;
30
+ this.alpha = 1;
31
+
32
+ // Dimensions
33
+ if (this.platform === 'material') {
34
+ this.height = 320;
35
+ this.paletteSize = 200;
36
+ this.sliderHeight = 20;
37
+ } else {
38
+ this.height = 480; // Augmenté pour tout contenir
39
+ this.gridSize = 240; // Réduit pour ne pas dépasser
40
+ this.swatchSize = 28; // Réduit pour mieux aligner
41
+ }
42
+
43
+ // État des interactions
44
+ this.draggingPalette = false;
45
+ this.draggingSlider = null;
46
+
47
+ // OPTIMISATION : Cache canvas pour la palette
48
+ this.paletteCanvas = null;
49
+ this.paletteCtx = null;
50
+ this.cachedHue = -1; // Pour savoir quand regénérer
51
+
52
+ // Throttle redraw
53
+ this.needsRedraw = false;
54
+ this.isDrawing = false;
55
+
56
+ // Couleurs prédéfinies iOS
57
+ this.iosColors = [
58
+ ['#FF3B30', '#FF9500', '#FFCC00', '#34C759', '#00C7BE', '#30B0C7', '#32ADE6', '#007AFF', '#5856D6', '#AF52DE'],
59
+ ['#FF2D55', '#A2845E', '#8E8E93', '#FF6482', '#FFB340', '#FFD426', '#5DD167', '#26D9CE', '#44BAD4', '#4AB8EE'],
60
+ ['#0A84FF', '#6E64E8', '#BF5AF2', '#FF6961', '#FF9F0A', '#FFD60A', '#30D158', '#00D9C8', '#40C8E0', '#64D2FF']
61
+ ];
62
+
63
+ // ✅ Flag anti-double appel
64
+ this._isNotifying = false;
65
+ }
66
+
67
+ /**
68
+ * Génère la palette SV dans un canvas caché (UNE SEULE FOIS)
69
+ * @private
70
+ */
71
+ generatePaletteCache() {
72
+ // Si la teinte n'a pas changé, pas besoin de regénérer
73
+ if (this.cachedHue === this.hue && this.paletteCanvas) {
74
+ return;
75
+ }
76
+
77
+ // Créer canvas si nécessaire
78
+ if (!this.paletteCanvas) {
79
+ this.paletteCanvas = document.createElement('canvas');
80
+ this.paletteCanvas.width = this.paletteSize;
81
+ this.paletteCanvas.height = this.paletteSize;
82
+ this.paletteCtx = this.paletteCanvas.getContext('2d');
83
+ }
84
+
85
+ const size = this.paletteSize;
86
+
87
+ // Générer la palette pixel par pixel (UNE SEULE FOIS)
88
+ for (let i = 0; i < size; i++) {
89
+ const s = i / size;
90
+
91
+ for (let j = 0; j < size; j++) {
92
+ const v = 1 - (j / size);
93
+ const rgb = this.hsvToRgb(this.hue, s, v);
94
+
95
+ this.paletteCtx.fillStyle = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
96
+ this.paletteCtx.fillRect(i, j, 1, 1);
97
+ }
98
+ }
99
+
100
+ this.cachedHue = this.hue;
101
+ }
102
+
103
+ /**
104
+ * Dessine le picker Material
105
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
106
+ * @private
107
+ */
108
+ drawMaterial(ctx) {
109
+ let currentY = this.y + 10;
110
+
111
+ // Palette SV (OPTIMISÉE)
112
+ this.drawSVPalette(ctx, this.x + 10, currentY);
113
+ currentY += this.paletteSize + 20;
114
+
115
+ // Slider Hue
116
+ this.drawHueSlider(ctx, this.x + 10, currentY);
117
+ currentY += this.sliderHeight + 20;
118
+
119
+ // Slider Alpha (si activé)
120
+ if (this.showAlpha) {
121
+ this.drawAlphaSlider(ctx, this.x + 10, currentY);
122
+ currentY += this.sliderHeight + 20;
123
+ }
124
+
125
+ // Prévisualisation + code hex
126
+ this.drawMaterialPreview(ctx, this.x + 10, currentY);
127
+ }
128
+
129
+ /**
130
+ * Dessine la palette Saturation-Value (VERSION OPTIMISÉE)
131
+ * @private
132
+ */
133
+ drawSVPalette(ctx, x, y) {
134
+ const size = this.paletteSize;
135
+
136
+ // Générer ou récupérer le cache
137
+ this.generatePaletteCache();
138
+
139
+ // DESSINER LE CACHE (INSTANTANÉ!)
140
+ ctx.drawImage(this.paletteCanvas, x, y);
141
+
142
+ // Curseur de sélection
143
+ const cursorX = x + this.saturation * size;
144
+ const cursorY = y + (1 - this.value) * size;
145
+
146
+ ctx.strokeStyle = this.value > 0.5 ? '#000000' : '#FFFFFF';
147
+ ctx.lineWidth = 2;
148
+ ctx.beginPath();
149
+ ctx.arc(cursorX, cursorY, 8, 0, Math.PI * 2);
150
+ ctx.stroke();
151
+
152
+ ctx.strokeStyle = this.value > 0.5 ? '#FFFFFF' : '#000000';
153
+ ctx.lineWidth = 1;
154
+ ctx.beginPath();
155
+ ctx.arc(cursorX, cursorY, 9, 0, Math.PI * 2);
156
+ ctx.stroke();
157
+
158
+ // Stocker pour interaction
159
+ this.paletteRect = { x, y, width: size, height: size };
160
+ }
161
+
162
+ /**
163
+ * Dessine le slider de teinte
164
+ * @private
165
+ */
166
+ drawHueSlider(ctx, x, y) {
167
+ const width = this.width - 20;
168
+ const height = this.sliderHeight;
169
+
170
+ // Gradient arc-en-ciel
171
+ const gradient = ctx.createLinearGradient(x, y, x + width, y);
172
+ for (let i = 0; i <= 6; i++) {
173
+ const hue = (i / 6) * 360;
174
+ const rgb = this.hsvToRgb(hue, 1, 1);
175
+ gradient.addColorStop(i / 6, `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`);
176
+ }
177
+
178
+ ctx.fillStyle = gradient;
179
+ this.roundRect(ctx, x, y, width, height, height / 2);
180
+ ctx.fill();
181
+
182
+ // Curseur
183
+ const cursorX = x + (this.hue / 360) * width;
184
+
185
+ ctx.fillStyle = '#FFFFFF';
186
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
187
+ ctx.shadowBlur = 4;
188
+ ctx.beginPath();
189
+ ctx.arc(cursorX, y + height / 2, 12, 0, Math.PI * 2);
190
+ ctx.fill();
191
+
192
+ ctx.shadowColor = 'transparent';
193
+ ctx.strokeStyle = '#E0E0E0';
194
+ ctx.lineWidth = 2;
195
+ ctx.stroke();
196
+
197
+ this.hueSliderRect = { x, y, width, height };
198
+ }
199
+
200
+ /**
201
+ * Dessine le slider d'opacité
202
+ * @private
203
+ */
204
+ drawAlphaSlider(ctx, x, y) {
205
+ const width = this.width - 20;
206
+ const height = this.sliderHeight;
207
+
208
+ // Fond à damier (transparence)
209
+ this.drawCheckerboard(ctx, x, y, width, height);
210
+
211
+ // Gradient avec couleur actuelle
212
+ const rgb = this.getCurrentRgb();
213
+ const gradient = ctx.createLinearGradient(x, y, x + width, y);
214
+ gradient.addColorStop(0, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0)`);
215
+ gradient.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1)`);
216
+
217
+ ctx.fillStyle = gradient;
218
+ this.roundRect(ctx, x, y, width, height, height / 2);
219
+ ctx.fill();
220
+
221
+ // Curseur
222
+ const cursorX = x + this.alpha * width;
223
+
224
+ ctx.fillStyle = '#FFFFFF';
225
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
226
+ ctx.shadowBlur = 4;
227
+ ctx.beginPath();
228
+ ctx.arc(cursorX, y + height / 2, 12, 0, Math.PI * 2);
229
+ ctx.fill();
230
+
231
+ ctx.shadowColor = 'transparent';
232
+ ctx.strokeStyle = '#E0E0E0';
233
+ ctx.lineWidth = 2;
234
+ ctx.stroke();
235
+
236
+ this.alphaSliderRect = { x, y, width, height };
237
+ }
238
+
239
+ /**
240
+ * Dessine la prévisualisation Material
241
+ * @private
242
+ */
243
+ drawMaterialPreview(ctx, x, y) {
244
+ const previewSize = 60;
245
+
246
+ // Damier de fond
247
+ this.drawCheckerboard(ctx, x, y, previewSize, previewSize);
248
+
249
+ // Couleur actuelle
250
+ const color = this.getCurrentColor();
251
+ ctx.fillStyle = color;
252
+ ctx.fillRect(x, y, previewSize, previewSize);
253
+
254
+ // Bordure
255
+ ctx.strokeStyle = '#E0E0E0';
256
+ ctx.lineWidth = 1;
257
+ ctx.strokeRect(x, y, previewSize, previewSize);
258
+
259
+ // Code Hex
260
+ if (this.showHex) {
261
+ const hexCode = this.getCurrentHex();
262
+ ctx.fillStyle = '#000000';
263
+ ctx.font = '14px Roboto, monospace';
264
+ ctx.textAlign = 'left';
265
+ ctx.textBaseline = 'middle';
266
+ ctx.fillText(hexCode, x + previewSize + 15, y + previewSize / 2);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Dessine le picker Cupertino (iOS)
272
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
273
+ * @private
274
+ */
275
+ drawCupertino(ctx) {
276
+ let currentY = this.y + 16;
277
+
278
+ // Header avec prévisualisation
279
+ this.drawIOSPreview(ctx, currentY);
280
+ currentY += 96;
281
+
282
+ // Grille de couleurs prédéfinies (CENTRÉE)
283
+ const gridWidth = 10 * (this.swatchSize + 4); // 10 couleurs par ligne
284
+ const gridX = this.x + (this.width - gridWidth) / 2;
285
+ this.drawColorGrid(ctx, gridX, currentY);
286
+ currentY += this.iosColors.length * (this.swatchSize + 4) + 24;
287
+
288
+ // Sliders iOS style
289
+ this.drawIOSSliders(ctx, this.x + 20, currentY);
290
+ }
291
+
292
+ /**
293
+ * Dessine la prévisualisation iOS en haut
294
+ * @private
295
+ */
296
+ drawIOSPreview(ctx, y) {
297
+ const previewSize = 64;
298
+ const centerX = this.x + this.width / 2;
299
+
300
+ // Damier de fond
301
+ this.drawCheckerboard(ctx, centerX - previewSize / 2, y + 8, previewSize, previewSize);
302
+
303
+ // Couleur actuelle
304
+ const color = this.getCurrentColor();
305
+ ctx.fillStyle = color;
306
+ ctx.beginPath();
307
+ ctx.arc(centerX, y + 8 + previewSize / 2, previewSize / 2, 0, Math.PI * 2);
308
+ ctx.fill();
309
+
310
+ // Bordure subtile
311
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
312
+ ctx.lineWidth = 0.5;
313
+ ctx.stroke();
314
+
315
+ // Code Hex en dessous
316
+ if (this.showHex) {
317
+ const hexCode = this.getCurrentHex();
318
+ ctx.fillStyle = '#8E8E93';
319
+ ctx.font = '13px -apple-system, BlinkMacSystemFont, sans-serif';
320
+ ctx.textAlign = 'center';
321
+ ctx.textBaseline = 'top';
322
+ ctx.fillText(hexCode.toUpperCase(), centerX, y + 8 + previewSize + 8);
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Dessine la grille de couleurs iOS
328
+ * @private
329
+ */
330
+ drawColorGrid(ctx, x, y) {
331
+ this.colorSwatches = [];
332
+
333
+ this.iosColors.forEach((row, rowIndex) => {
334
+ row.forEach((color, colIndex) => {
335
+ const swatchX = x + colIndex * (this.swatchSize + 4); // Espacement réduit
336
+ const swatchY = y + rowIndex * (this.swatchSize + 4);
337
+
338
+ // Couleur
339
+ ctx.fillStyle = color;
340
+ ctx.beginPath();
341
+ ctx.arc(swatchX + this.swatchSize / 2, swatchY + this.swatchSize / 2,
342
+ this.swatchSize / 2, 0, Math.PI * 2);
343
+ ctx.fill();
344
+
345
+ // Bordure si sélectionné
346
+ const currentColor = this.getCurrentHex();
347
+ if (color.toUpperCase() === currentColor.toUpperCase()) {
348
+ ctx.strokeStyle = '#007AFF';
349
+ ctx.lineWidth = 2.5;
350
+ ctx.stroke();
351
+ }
352
+
353
+ // Stocker pour clic
354
+ this.colorSwatches.push({
355
+ x: swatchX,
356
+ y: swatchY,
357
+ size: this.swatchSize,
358
+ color: color
359
+ });
360
+ });
361
+ });
362
+ }
363
+
364
+ /**
365
+ * Dessine les sliders iOS (VRAI STYLE iOS)
366
+ * @private
367
+ */
368
+ drawIOSSliders(ctx, x, y) {
369
+ const sliderWidth = this.width - 140; // Espace pour label + valeur
370
+ const sliderHeight = 28;
371
+ const spacing = 16;
372
+
373
+ // Labels des sliders
374
+ const labels = ['Red', 'Green', 'Blue'];
375
+ if (this.showAlpha) labels.push('Opacity');
376
+
377
+ const channels = ['r', 'g', 'b'];
378
+ if (this.showAlpha) channels.push('a');
379
+
380
+ labels.forEach((label, index) => {
381
+ const channel = channels[index];
382
+ const sliderY = y + index * (sliderHeight + spacing);
383
+
384
+ // Label (aligné à gauche)
385
+ ctx.fillStyle = '#000000';
386
+ ctx.font = '15px -apple-system, BlinkMacSystemFont, sans-serif';
387
+ ctx.textAlign = 'left';
388
+ ctx.textBaseline = 'middle';
389
+ ctx.fillText(label, x, sliderY + sliderHeight / 2);
390
+
391
+ // Slider (avec bon espacement)
392
+ const sliderStartX = x + 70; // Espace fixe pour les labels
393
+ this.drawIOSSlider(ctx, sliderStartX, sliderY, sliderWidth, sliderHeight, channel);
394
+ });
395
+ }
396
+
397
+ /**
398
+ * Dessine un slider iOS (VRAI STYLE iOS)
399
+ * @private
400
+ */
401
+ drawIOSSlider(ctx, x, y, width, height, channel) {
402
+ const rgb = this.getCurrentRgb();
403
+ const value = channel === 'r' ? rgb.r / 255 :
404
+ channel === 'g' ? rgb.g / 255 :
405
+ channel === 'b' ? rgb.b / 255 :
406
+ this.alpha;
407
+
408
+ const trackHeight = 4;
409
+ const trackY = y + height / 2 - trackHeight / 2;
410
+
411
+ // Track background (ligne fine iOS)
412
+ ctx.fillStyle = '#D1D1D6';
413
+ this.roundRect(ctx, x, trackY, width, trackHeight, trackHeight / 2);
414
+ ctx.fill();
415
+
416
+ // Track filled avec couleur
417
+ let fillColor;
418
+ if (channel === 'r') {
419
+ const currentRgb = this.getCurrentRgb();
420
+ fillColor = `rgb(${value * 255}, ${currentRgb.g}, ${currentRgb.b})`;
421
+ } else if (channel === 'g') {
422
+ const currentRgb = this.getCurrentRgb();
423
+ fillColor = `rgb(${currentRgb.r}, ${value * 255}, ${currentRgb.b})`;
424
+ } else if (channel === 'b') {
425
+ const currentRgb = this.getCurrentRgb();
426
+ fillColor = `rgb(${currentRgb.r}, ${currentRgb.g}, ${value * 255})`;
427
+ } else if (channel === 'a') {
428
+ fillColor = this.getCurrentColor();
429
+ }
430
+
431
+ ctx.fillStyle = fillColor;
432
+ this.roundRect(ctx, x, trackY, width * value, trackHeight, trackHeight / 2);
433
+ ctx.fill();
434
+
435
+ // Thumb (cercle blanc iOS)
436
+ const thumbX = x + width * value;
437
+ const thumbRadius = 13;
438
+
439
+ // Ombre du thumb
440
+ ctx.save();
441
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.15)';
442
+ ctx.shadowBlur = 3;
443
+ ctx.shadowOffsetY = 1;
444
+
445
+ // Thumb blanc
446
+ ctx.fillStyle = '#FFFFFF';
447
+ ctx.beginPath();
448
+ ctx.arc(thumbX, y + height / 2, thumbRadius, 0, Math.PI * 2);
449
+ ctx.fill();
450
+
451
+ ctx.restore();
452
+
453
+ // Bordure subtile du thumb
454
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.06)';
455
+ ctx.lineWidth = 0.5;
456
+ ctx.beginPath();
457
+ ctx.arc(thumbX, y + height / 2, thumbRadius, 0, Math.PI * 2);
458
+ ctx.stroke();
459
+
460
+ // Valeur en pourcentage (alignée à droite)
461
+ const percentage = Math.round(value * 100);
462
+ ctx.fillStyle = '#8E8E93';
463
+ ctx.font = '13px -apple-system, BlinkMacSystemFont, sans-serif';
464
+ ctx.textAlign = 'left';
465
+ ctx.textBaseline = 'middle';
466
+ ctx.fillText(`${percentage}%`, x + width + 12, y + height / 2);
467
+
468
+ // Stocker pour interaction (zone cliquable élargie)
469
+ if (!this.iosSliders) this.iosSliders = {};
470
+ this.iosSliders[channel] = {
471
+ x,
472
+ y: y + height / 2 - 16, // Zone cliquable plus grande verticalement
473
+ width,
474
+ height: 32
475
+ };
476
+ }
477
+
478
+ /**
479
+ * Dessine un damier (fond transparence)
480
+ * @private
481
+ */
482
+ drawCheckerboard(ctx, x, y, width, height) {
483
+ const squareSize = 8;
484
+ ctx.fillStyle = '#FFFFFF';
485
+ ctx.fillRect(x, y, width, height);
486
+
487
+ ctx.fillStyle = '#E0E0E0';
488
+ for (let i = 0; i < width; i += squareSize) {
489
+ for (let j = 0; j < height; j += squareSize) {
490
+ if ((i / squareSize + j / squareSize) % 2 === 0) {
491
+ ctx.fillRect(x + i, y + j, squareSize, squareSize);
492
+ }
493
+ }
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Dessine le composant
499
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
500
+ */
501
+ draw(ctx) {
502
+ ctx.save();
503
+
504
+ // Background
505
+ ctx.fillStyle = this.platform === 'material' ? '#FFFFFF' : '#F2F2F7';
506
+ ctx.fillRect(this.x, this.y, this.width, this.height);
507
+
508
+ if (this.platform === 'material') {
509
+ this.drawMaterial(ctx);
510
+ } else {
511
+ this.drawCupertino(ctx);
512
+ }
513
+
514
+ ctx.restore();
515
+ }
516
+
517
+ handleTouchStart(x, y) {
518
+ if (this.platform === 'material') {
519
+ // Palette SV
520
+ if (this.paletteRect && this.isPointInRect(x, y, this.paletteRect)) {
521
+ this.draggingPalette = true;
522
+ this.updatePalette(x, y);
523
+ return true;
524
+ }
525
+
526
+ // Slider Hue
527
+ if (this.hueSliderRect && this.isPointInRect(x, y, this.hueSliderRect)) {
528
+ this.draggingSlider = 'hue';
529
+ this.updateHueSlider(x);
530
+ return true;
531
+ }
532
+
533
+ // Slider Alpha
534
+ if (this.showAlpha && this.alphaSliderRect && this.isPointInRect(x, y, this.alphaSliderRect)) {
535
+ this.draggingSlider = 'alpha';
536
+ this.updateAlphaSlider(x);
537
+ return true;
538
+ }
539
+ } else {
540
+ // Swatches iOS
541
+ if (this.colorSwatches) {
542
+ for (let swatch of this.colorSwatches) {
543
+ if (this.isPointInRect(x, y, swatch)) {
544
+ this.setColorFromHex(swatch.color);
545
+ return true;
546
+ }
547
+ }
548
+ }
549
+
550
+ // Sliders iOS
551
+ if (this.iosSliders) {
552
+ for (let channel in this.iosSliders) {
553
+ if (this.isPointInRect(x, y, this.iosSliders[channel])) {
554
+ this.draggingSlider = channel;
555
+ this.updateIOSSlider(x, channel);
556
+ return true;
557
+ }
558
+ }
559
+ }
560
+ }
561
+
562
+ return false;
563
+ }
564
+
565
+ handleTouchMove(x, y) {
566
+ if (this.draggingPalette) {
567
+ this.updatePalette(x, y);
568
+ return true;
569
+ }
570
+
571
+ if (this.draggingSlider) {
572
+ if (this.platform === 'material') {
573
+ if (this.draggingSlider === 'hue') this.updateHueSlider(x);
574
+ if (this.draggingSlider === 'alpha') this.updateAlphaSlider(x);
575
+ } else {
576
+ this.updateIOSSlider(x, this.draggingSlider);
577
+ }
578
+ return true;
579
+ }
580
+
581
+ return false;
582
+ }
583
+
584
+ /**
585
+ * Gère le mouvement de la souris (pour le drag)
586
+ */
587
+ handleHover(x, y) {
588
+ // Si on est en train de drag, continuer
589
+ if (this.draggingPalette || this.draggingSlider) {
590
+ return this.handleTouchMove(x, y);
591
+ }
592
+ return false;
593
+ }
594
+
595
+ handleTouchEnd() {
596
+ this.draggingPalette = false;
597
+ this.draggingSlider = null;
598
+ }
599
+
600
+ /**
601
+ * Gère le relâchement de la souris
602
+ */
603
+ handleMouseUp() {
604
+ this.handleTouchEnd();
605
+ }
606
+
607
+ /**
608
+ * Met à jour la palette SV
609
+ * @private
610
+ */
611
+ updatePalette(x, y) {
612
+ const rect = this.paletteRect;
613
+ this.saturation = Math.max(0, Math.min(1, (x - rect.x) / rect.width));
614
+ this.value = Math.max(0, Math.min(1, 1 - (y - rect.y) / rect.height));
615
+ this.notifyChange();
616
+ }
617
+
618
+ /**
619
+ * Met à jour le slider de teinte
620
+ * @private
621
+ */
622
+ updateHueSlider(x) {
623
+ const rect = this.hueSliderRect;
624
+ this.hue = Math.max(0, Math.min(360, ((x - rect.x) / rect.width) * 360));
625
+ this.notifyChange();
626
+ }
627
+
628
+ /**
629
+ * Met à jour le slider d'alpha
630
+ * @private
631
+ */
632
+ updateAlphaSlider(x) {
633
+ const rect = this.alphaSliderRect;
634
+ this.alpha = Math.max(0, Math.min(1, (x - rect.x) / rect.width));
635
+ this.notifyChange();
636
+ }
637
+
638
+ /**
639
+ * Met à jour un slider iOS
640
+ * @private
641
+ */
642
+ updateIOSSlider(x, channel) {
643
+ const rect = this.iosSliders[channel];
644
+ const value = Math.max(0, Math.min(1, (x - rect.x) / rect.width));
645
+
646
+ const rgb = this.getCurrentRgb();
647
+
648
+ if (channel === 'r') {
649
+ const hsv = this.rgbToHsv(value * 255, rgb.g, rgb.b);
650
+ this.hue = hsv.h;
651
+ this.saturation = hsv.s;
652
+ this.value = hsv.v;
653
+ } else if (channel === 'g') {
654
+ const hsv = this.rgbToHsv(rgb.r, value * 255, rgb.b);
655
+ this.hue = hsv.h;
656
+ this.saturation = hsv.s;
657
+ this.value = hsv.v;
658
+ } else if (channel === 'b') {
659
+ const hsv = this.rgbToHsv(rgb.r, rgb.g, value * 255);
660
+ this.hue = hsv.h;
661
+ this.saturation = hsv.s;
662
+ this.value = hsv.v;
663
+ } else if (channel === 'a') {
664
+ this.alpha = value;
665
+ }
666
+
667
+ this.notifyChange();
668
+ }
669
+
670
+ /**
671
+ * Notifie le changement de couleur (OPTIMISÉ)
672
+ * @private
673
+ */
674
+ notifyChange() {
675
+ // ✅ Éviter les appels multiples
676
+ if (this._isNotifying) return;
677
+ this._isNotifying = true;
678
+
679
+ // ✅ Appeler le callback immédiatement
680
+ if (typeof this.onColorChange === 'function') {
681
+ this.onColorChange(this.getCurrentColor(), this.getCurrentHex());
682
+ }
683
+
684
+ // Redessiner (optionnel)
685
+ if (this.framework && this.framework.redraw) {
686
+ this.framework.redraw();
687
+ }
688
+
689
+ // ✅ Réinitialiser le flag après un court délai
690
+ setTimeout(() => {
691
+ this._isNotifying = false;
692
+ }, 50);
693
+ }
694
+ /**
695
+ * Obtient la couleur RGB actuelle
696
+ * @returns {Object} {r, g, b}
697
+ */
698
+ getCurrentRgb() {
699
+ return this.hsvToRgb(this.hue, this.saturation, this.value);
700
+ }
701
+
702
+ /**
703
+ * Obtient la couleur actuelle (rgba ou hex)
704
+ * @returns {string}
705
+ */
706
+ getCurrentColor() {
707
+ const rgb = this.getCurrentRgb();
708
+ if (this.showAlpha && this.alpha < 1) {
709
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${this.alpha.toFixed(2)})`;
710
+ }
711
+ return this.rgbToHex(rgb.r, rgb.g, rgb.b);
712
+ }
713
+
714
+ /**
715
+ * Obtient le code hexa actuel
716
+ * @returns {string}
717
+ */
718
+ getCurrentHex() {
719
+ const rgb = this.getCurrentRgb();
720
+ return this.rgbToHex(rgb.r, rgb.g, rgb.b);
721
+ }
722
+
723
+ /**
724
+ * Définit la couleur depuis un code hex
725
+ */
726
+ setColorFromHex(hex) {
727
+ const rgb = this.hexToRgb(hex);
728
+ const hsv = this.rgbToHsv(rgb.r, rgb.g, rgb.b);
729
+ this.hue = hsv.h;
730
+ this.saturation = hsv.s;
731
+ this.value = hsv.v;
732
+ this.notifyChange();
733
+ }
734
+
735
+ // ===== UTILITAIRES CONVERSION COULEURS =====
736
+
737
+ hsvToRgb(h, s, v) {
738
+ const c = v * s;
739
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
740
+ const m = v - c;
741
+
742
+ let r, g, b;
743
+
744
+ if (h < 60) { r = c; g = x; b = 0; }
745
+ else if (h < 120) { r = x; g = c; b = 0; }
746
+ else if (h < 180) { r = 0; g = c; b = x; }
747
+ else if (h < 240) { r = 0; g = x; b = c; }
748
+ else if (h < 300) { r = x; g = 0; b = c; }
749
+ else { r = c; g = 0; b = x; }
750
+
751
+ return {
752
+ r: Math.round((r + m) * 255),
753
+ g: Math.round((g + m) * 255),
754
+ b: Math.round((b + m) * 255)
755
+ };
756
+ }
757
+
758
+ rgbToHsv(r, g, b) {
759
+ r /= 255;
760
+ g /= 255;
761
+ b /= 255;
762
+
763
+ const max = Math.max(r, g, b);
764
+ const min = Math.min(r, g, b);
765
+ const delta = max - min;
766
+
767
+ let h = 0;
768
+ if (delta !== 0) {
769
+ if (max === r) h = 60 * (((g - b) / delta) % 6);
770
+ else if (max === g) h = 60 * (((b - r) / delta) + 2);
771
+ else h = 60 * (((r - g) / delta) + 4);
772
+ }
773
+
774
+ if (h < 0) h += 360;
775
+
776
+ const s = max === 0 ? 0 : delta / max;
777
+ const v = max;
778
+
779
+ return { h, s, v };
780
+ }
781
+
782
+ hexToRgb(hex) {
783
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
784
+ return result ? {
785
+ r: parseInt(result[1], 16),
786
+ g: parseInt(result[2], 16),
787
+ b: parseInt(result[3], 16)
788
+ } : { r: 0, g: 0, b: 0 };
789
+ }
790
+
791
+ rgbToHex(r, g, b) {
792
+ return '#' + [r, g, b].map(x => {
793
+ const hex = x.toString(16);
794
+ return hex.length === 1 ? '0' + hex : hex;
795
+ }).join('');
796
+ }
797
+
798
+ roundRect(ctx, x, y, width, height, radius) {
799
+ ctx.beginPath();
800
+ ctx.moveTo(x + radius, y);
801
+ ctx.lineTo(x + width - radius, y);
802
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
803
+ ctx.lineTo(x + width, y + height - radius);
804
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
805
+ ctx.lineTo(x + radius, y + height);
806
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
807
+ ctx.lineTo(x, y + radius);
808
+ ctx.quadraticCurveTo(x, y, x + radius, y);
809
+ ctx.closePath();
810
+ }
811
+
812
+ isPointInRect(x, y, rect) {
813
+ // Support pour les swatches iOS qui ont une propriété 'size' au lieu de width/height
814
+ if (rect.size !== undefined) {
815
+ return x >= rect.x && x <= rect.x + rect.size &&
816
+ y >= rect.y && y <= rect.y + rect.size;
817
+ }
818
+
819
+ return x >= rect.x && x <= rect.x + rect.width &&
820
+ y >= rect.y && y <= rect.y + rect.height;
821
+ }
822
+
823
+ isPointInside(x, y) {
824
+ // Vérifier si le clic est dans le ColorPicker
825
+ if (x < this.x || x > this.x + this.width ||
826
+ y < this.y || y > this.y + this.height) {
827
+ return false;
828
+ }
829
+
830
+
831
+ // Material
832
+ if (this.platform === 'material') {
833
+ // Palette SV
834
+ if (this.paletteRect &&
835
+ x >= this.paletteRect.x && x <= this.paletteRect.x + this.paletteRect.width &&
836
+ y >= this.paletteRect.y && y <= this.paletteRect.y + this.paletteRect.height) {
837
+ this.draggingPalette = true;
838
+ this.updatePalette(x, y);
839
+ return true;
840
+ }
841
+
842
+ // Slider Hue
843
+ if (this.hueSliderRect &&
844
+ x >= this.hueSliderRect.x && x <= this.hueSliderRect.x + this.hueSliderRect.width &&
845
+ y >= this.hueSliderRect.y && y <= this.hueSliderRect.y + this.hueSliderRect.height) {
846
+ this.draggingSlider = 'hue';
847
+ this.updateHueSlider(x);
848
+ return true;
849
+ }
850
+
851
+ // Slider Alpha
852
+ if (this.showAlpha && this.alphaSliderRect &&
853
+ x >= this.alphaSliderRect.x && x <= this.alphaSliderRect.x + this.alphaSliderRect.width &&
854
+ y >= this.alphaSliderRect.y && y <= this.alphaSliderRect.y + this.alphaSliderRect.height) {
855
+ this.draggingSlider = 'alpha';
856
+ this.updateAlphaSlider(x);
857
+ return true;
858
+ }
859
+ }
860
+ // iOS
861
+ else {
862
+ // Swatches
863
+ if (this.colorSwatches) {
864
+ for (let swatch of this.colorSwatches) {
865
+ if (x >= swatch.x && x <= swatch.x + swatch.size &&
866
+ y >= swatch.y && y <= swatch.y + swatch.size) {
867
+ this.setColorFromHex(swatch.color);
868
+ return true;
869
+ }
870
+ }
871
+ }
872
+
873
+ // Sliders iOS
874
+ if (this.iosSliders) {
875
+ for (let channel in this.iosSliders) {
876
+ const rect = this.iosSliders[channel];
877
+ if (x >= rect.x && x <= rect.x + rect.width &&
878
+ y >= rect.y && y <= rect.y + rect.height) {
879
+ this.draggingSlider = channel;
880
+ this.updateIOSSlider(x, channel);
881
+ return true;
882
+ }
883
+ }
884
+ }
885
+ }
886
+
887
+ return true; // Important: retourner true même si pas sur contrôle
888
+ }
889
+ }
890
+
891
+ export default ColorPicker;