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.
- package/components/Breadcrumb.js +391 -0
- package/components/ColorPicker.js +891 -0
- package/components/Paginatedcontainer.js +840 -0
- package/components/Popover.js +493 -0
- package/components/Rating.js +494 -0
- package/core/CanvasFramework.js +5 -0
- package/core/UIBuilder.js +10 -0
- package/index.js +5 -0
- package/package.json +1 -1
|
@@ -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;
|