canvasframework 0.4.0 → 0.4.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.
- package/components/Input.js +585 -63
- package/core/WebGLCanvasAdapter.js +64 -15
- package/package.json +1 -1
package/components/Input.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import Component from '../core/Component.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Champ de saisie texte
|
|
4
|
+
* Champ de saisie texte avec styles Material 3 (Filled/Outlined) et Cupertino
|
|
5
5
|
* @class
|
|
6
6
|
* @extends Component
|
|
7
7
|
* @property {string} placeholder - Texte d'indication
|
|
8
8
|
* @property {string} value - Valeur
|
|
9
9
|
* @property {number} fontSize - Taille de police
|
|
10
10
|
* @property {boolean} focused - Focus actif
|
|
11
|
-
* @property {string} platform - Plateforme
|
|
11
|
+
* @property {string} platform - Plateforme ('material' ou 'cupertino')
|
|
12
|
+
* @property {string} variant - Variante Material ('filled' ou 'outlined')
|
|
12
13
|
* @property {boolean} cursorVisible - Curseur visible
|
|
13
14
|
* @property {number} cursorPosition - Position du curseur
|
|
14
15
|
* @property {HTMLInputElement} hiddenInput - Input HTML caché
|
|
16
|
+
* @property {string} label - Label flottant (Material)
|
|
17
|
+
* @property {boolean} hasLabel - Si un label est défini
|
|
18
|
+
* @property {boolean} error - État d'erreur
|
|
19
|
+
* @property {string} errorText - Texte d'erreur
|
|
20
|
+
* @property {boolean} disabled - État désactivé
|
|
21
|
+
* @property {string} helperText - Texte d'aide
|
|
22
|
+
* @property {boolean} leadingIcon - Afficher une icône à gauche
|
|
23
|
+
* @property {boolean} trailingIcon - Afficher une icône à droite
|
|
24
|
+
* @property {string} backgroundColor - Couleur de fond personnalisée
|
|
25
|
+
* @property {string} borderColor - Couleur de bordure personnalisée
|
|
26
|
+
* @property {string} focusColor - Couleur au focus personnalisée
|
|
27
|
+
* @property {string} textColor - Couleur du texte personnalisée
|
|
28
|
+
* @property {string} placeholderColor - Couleur du placeholder personnalisée
|
|
15
29
|
*/
|
|
16
30
|
class Input extends Component {
|
|
17
31
|
static activeInput = null;
|
|
@@ -24,30 +38,97 @@ class Input extends Component {
|
|
|
24
38
|
* @param {Object} [options={}] - Options de configuration
|
|
25
39
|
* @param {string} [options.placeholder=''] - Texte d'indication
|
|
26
40
|
* @param {string} [options.value=''] - Valeur initiale
|
|
41
|
+
* @param {string} [options.label=''] - Label flottant (Material)
|
|
27
42
|
* @param {number} [options.fontSize=16] - Taille de police
|
|
43
|
+
* @param {string} [options.variant='filled'] - Variante Material ('filled' ou 'outlined')
|
|
44
|
+
* @param {boolean} [options.error=false] - État d'erreur
|
|
45
|
+
* @param {string} [options.errorText=''] - Texte d'erreur
|
|
46
|
+
* @param {boolean} [options.disabled=false] - État désactivé
|
|
47
|
+
* @param {string} [options.helperText=''] - Texte d'aide
|
|
48
|
+
* @param {boolean} [options.leadingIcon=false] - Afficher une icône à gauche
|
|
49
|
+
* @param {boolean} [options.trailingIcon=false] - Afficher une icône à droite
|
|
50
|
+
* @param {string} [options.backgroundColor] - Couleur de fond personnalisée
|
|
51
|
+
* @param {string} [options.borderColor] - Couleur de bordure personnalisée
|
|
52
|
+
* @param {string} [options.focusColor] - Couleur au focus personnalisée
|
|
53
|
+
* @param {string} [options.textColor] - Couleur du texte personnalisée
|
|
54
|
+
* @param {string} [options.placeholderColor] - Couleur du placeholder personnalisée
|
|
55
|
+
* @param {string} [options.labelColor] - Couleur du label personnalisée
|
|
28
56
|
* @param {Function} [options.onFocus] - Callback au focus
|
|
29
57
|
* @param {Function} [options.onBlur] - Callback au blur
|
|
58
|
+
* @param {Function} [options.onChange] - Callback au changement
|
|
30
59
|
*/
|
|
31
60
|
constructor(framework, options = {}) {
|
|
32
61
|
super(framework, options);
|
|
33
62
|
this.placeholder = options.placeholder || '';
|
|
34
63
|
this.value = options.value || '';
|
|
64
|
+
this.label = options.label || '';
|
|
35
65
|
this.fontSize = options.fontSize || 16;
|
|
36
66
|
this.focused = false;
|
|
37
67
|
this.platform = framework.platform;
|
|
68
|
+
this.variant = options.variant || 'filled'; // 'filled' ou 'outlined'
|
|
38
69
|
this.cursorVisible = true;
|
|
39
70
|
this.cursorPosition = this.value.length;
|
|
71
|
+
this.error = options.error || false;
|
|
72
|
+
this.errorText = options.errorText || '';
|
|
73
|
+
this.disabled = options.disabled || false;
|
|
74
|
+
this.helperText = options.helperText || '';
|
|
75
|
+
this.hasLabel = !!this.label;
|
|
76
|
+
this.leadingIcon = options.leadingIcon || false;
|
|
77
|
+
this.trailingIcon = options.trailingIcon || false;
|
|
78
|
+
|
|
79
|
+
// Couleurs personnalisables
|
|
80
|
+
this.backgroundColor = options.backgroundColor || null;
|
|
81
|
+
this.borderColor = options.borderColor || null;
|
|
82
|
+
this.focusColor = options.focusColor || null;
|
|
83
|
+
this.textColor = options.textColor || null;
|
|
84
|
+
this.placeholderColor = options.placeholderColor || null;
|
|
85
|
+
this.labelColor = options.labelColor || null;
|
|
86
|
+
|
|
87
|
+
// Dimensions pour le padding
|
|
88
|
+
this.paddingLeft = this.leadingIcon ? 56 : 16;
|
|
89
|
+
this.paddingRight = this.trailingIcon ? 56 : 16;
|
|
90
|
+
|
|
91
|
+
// Couleurs Material Design 3 par défaut
|
|
92
|
+
this.m3Colors = {
|
|
93
|
+
primary: '#6750A4',
|
|
94
|
+
onSurface: '#1D1B20',
|
|
95
|
+
onSurfaceVariant: '#49454F',
|
|
96
|
+
surfaceVariant: '#E7E0EC',
|
|
97
|
+
surface: '#FEF7FF',
|
|
98
|
+
error: '#BA1A1A',
|
|
99
|
+
onError: '#FFFFFF',
|
|
100
|
+
outline: '#79747E',
|
|
101
|
+
outlineVariant: '#CAC4D0',
|
|
102
|
+
disabled: '#1D1B20' + '61', // 38% opacity
|
|
103
|
+
disabledContainer: '#E7E0EC' + '80',
|
|
104
|
+
// Valeurs par défaut si pas personnalisé
|
|
105
|
+
defaultBackground: '#F5F5F5', // Gris clair pour filled
|
|
106
|
+
defaultBorder: '#CCCCCC', // Gris pour bordure non focus
|
|
107
|
+
defaultFocus: '#1976D2' // Bleu Material par défaut
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Couleurs iOS Cupertino par défaut
|
|
111
|
+
this.cupertinoColors = {
|
|
112
|
+
blue: '#007AFF',
|
|
113
|
+
label: '#000000',
|
|
114
|
+
placeholder: '#999999',
|
|
115
|
+
background: '#FFFFFF',
|
|
116
|
+
border: '#C7C7CC',
|
|
117
|
+
error: '#FF3B30',
|
|
118
|
+
disabled: '#000000' + '4D' // 30% opacity
|
|
119
|
+
};
|
|
40
120
|
|
|
41
121
|
// Gestion du focus
|
|
42
122
|
this.onFocus = this.onFocus.bind(this);
|
|
43
123
|
this.onBlur = this.onBlur.bind(this);
|
|
124
|
+
this.onChange = options.onChange || (() => {});
|
|
44
125
|
|
|
45
126
|
// Enregistrer cet input
|
|
46
127
|
Input.allInputs.add(this);
|
|
47
128
|
|
|
48
129
|
// Animation du curseur
|
|
49
130
|
this.cursorInterval = setInterval(() => {
|
|
50
|
-
if (this.focused) this.cursorVisible = !this.cursorVisible;
|
|
131
|
+
if (this.focused && !this.disabled) this.cursorVisible = !this.cursorVisible;
|
|
51
132
|
}, 500);
|
|
52
133
|
|
|
53
134
|
// Écouter les clics partout pour détecter quand on clique ailleurs
|
|
@@ -58,10 +139,8 @@ class Input extends Component {
|
|
|
58
139
|
* Écoute les clics globaux pour détecter les clics hors input
|
|
59
140
|
*/
|
|
60
141
|
setupGlobalClickHandler() {
|
|
61
|
-
// On crée un gestionnaire unique pour tous les inputs
|
|
62
142
|
if (!Input.globalClickHandler) {
|
|
63
143
|
Input.globalClickHandler = (e) => {
|
|
64
|
-
// Vérifier si on a cliqué en dehors de TOUS les inputs
|
|
65
144
|
let clickedOnInput = false;
|
|
66
145
|
|
|
67
146
|
for (let input of Input.allInputs) {
|
|
@@ -71,13 +150,11 @@ class Input extends Component {
|
|
|
71
150
|
}
|
|
72
151
|
}
|
|
73
152
|
|
|
74
|
-
// Si on n'a pas cliqué sur un input, détruire tous les inputs HTML
|
|
75
153
|
if (!clickedOnInput) {
|
|
76
154
|
Input.removeAllHiddenInputs();
|
|
77
155
|
}
|
|
78
156
|
};
|
|
79
157
|
|
|
80
|
-
// Attacher l'écouteur avec capture pour qu'il se déclenche tôt
|
|
81
158
|
document.addEventListener('click', Input.globalClickHandler, true);
|
|
82
159
|
document.addEventListener('touchstart', Input.globalClickHandler, true);
|
|
83
160
|
}
|
|
@@ -90,7 +167,6 @@ class Input extends Component {
|
|
|
90
167
|
setupHiddenInput() {
|
|
91
168
|
if (this.hiddenInput) return;
|
|
92
169
|
|
|
93
|
-
// Créer un input HTML caché unique pour cette instance
|
|
94
170
|
this.hiddenInput = document.createElement('input');
|
|
95
171
|
this.hiddenInput.style.position = 'fixed';
|
|
96
172
|
this.hiddenInput.style.opacity = '0';
|
|
@@ -100,9 +176,13 @@ class Input extends Component {
|
|
|
100
176
|
document.body.appendChild(this.hiddenInput);
|
|
101
177
|
|
|
102
178
|
this.hiddenInput.addEventListener('input', (e) => {
|
|
103
|
-
if (this.focused) {
|
|
179
|
+
if (this.focused && !this.disabled) {
|
|
180
|
+
const oldValue = this.value;
|
|
104
181
|
this.value = e.target.value;
|
|
105
182
|
this.cursorPosition = this.value.length;
|
|
183
|
+
if (oldValue !== this.value) {
|
|
184
|
+
this.onChange(this.value);
|
|
185
|
+
}
|
|
106
186
|
}
|
|
107
187
|
});
|
|
108
188
|
|
|
@@ -110,26 +190,30 @@ class Input extends Component {
|
|
|
110
190
|
this.focused = false;
|
|
111
191
|
this.cursorVisible = false;
|
|
112
192
|
|
|
113
|
-
// Détruire l'input HTML après un court délai
|
|
114
193
|
setTimeout(() => {
|
|
115
194
|
this.destroyHiddenInput();
|
|
116
195
|
}, 100);
|
|
117
196
|
});
|
|
197
|
+
|
|
198
|
+
// Gérer les touches spéciales
|
|
199
|
+
this.hiddenInput.addEventListener('keydown', (e) => {
|
|
200
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
this.focused = false;
|
|
203
|
+
this.cursorVisible = false;
|
|
204
|
+
this.destroyHiddenInput();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
118
207
|
}
|
|
119
208
|
|
|
120
209
|
/**
|
|
121
210
|
* Gère le focus
|
|
122
211
|
*/
|
|
123
212
|
onFocus() {
|
|
124
|
-
|
|
125
|
-
if (Input.activeInput === this) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
213
|
+
if (this.disabled || Input.activeInput === this) return;
|
|
128
214
|
|
|
129
|
-
// D'abord, détruire TOUS les autres inputs HTML
|
|
130
215
|
Input.removeAllHiddenInputs();
|
|
131
216
|
|
|
132
|
-
// Désactiver tous les autres inputs visuellement
|
|
133
217
|
for (let input of Input.allInputs) {
|
|
134
218
|
if (input !== this) {
|
|
135
219
|
input.focused = false;
|
|
@@ -137,25 +221,21 @@ class Input extends Component {
|
|
|
137
221
|
}
|
|
138
222
|
}
|
|
139
223
|
|
|
140
|
-
// Activer celui-ci
|
|
141
224
|
this.focused = true;
|
|
142
225
|
this.cursorVisible = true;
|
|
143
226
|
Input.activeInput = this;
|
|
144
227
|
|
|
145
|
-
// Créer l'input HTML si nécessaire
|
|
146
228
|
this.setupHiddenInput();
|
|
147
229
|
|
|
148
230
|
if (this.hiddenInput) {
|
|
149
231
|
this.hiddenInput.value = this.value;
|
|
150
|
-
|
|
232
|
+
this.hiddenInput.disabled = this.disabled;
|
|
151
233
|
const adjustedY = this.y + this.framework.scrollOffset;
|
|
152
234
|
this.hiddenInput.style.top = `${adjustedY}px`;
|
|
153
235
|
|
|
154
|
-
// Focus avec un petit délai
|
|
155
236
|
setTimeout(() => {
|
|
156
237
|
if (this.hiddenInput && this.focused) {
|
|
157
238
|
this.hiddenInput.focus();
|
|
158
|
-
// Positionner le curseur à la fin
|
|
159
239
|
this.hiddenInput.setSelectionRange(this.value.length, this.value.length);
|
|
160
240
|
}
|
|
161
241
|
}, 50);
|
|
@@ -184,19 +264,19 @@ class Input extends Component {
|
|
|
184
264
|
* Gère le clic
|
|
185
265
|
*/
|
|
186
266
|
onClick() {
|
|
187
|
-
this.
|
|
267
|
+
if (!this.disabled) {
|
|
268
|
+
this.onFocus();
|
|
269
|
+
}
|
|
188
270
|
}
|
|
189
271
|
|
|
190
272
|
/**
|
|
191
273
|
* Méthode statique pour détruire tous les inputs HTML
|
|
192
274
|
*/
|
|
193
275
|
static removeAllHiddenInputs() {
|
|
194
|
-
// Désactiver tous les inputs visuels
|
|
195
276
|
for (let input of Input.allInputs) {
|
|
196
277
|
input.focused = false;
|
|
197
278
|
input.cursorVisible = false;
|
|
198
279
|
|
|
199
|
-
// Détruire l'input HTML
|
|
200
280
|
if (input.hiddenInput && input.hiddenInput.parentNode) {
|
|
201
281
|
input.hiddenInput.parentNode.removeChild(input.hiddenInput);
|
|
202
282
|
input.hiddenInput = null;
|
|
@@ -208,9 +288,6 @@ class Input extends Component {
|
|
|
208
288
|
|
|
209
289
|
/**
|
|
210
290
|
* Vérifie si un point est dans les limites
|
|
211
|
-
* @param {number} x - Coordonnée X
|
|
212
|
-
* @param {number} y - Coordonnée Y
|
|
213
|
-
* @returns {boolean} True si le point est dans l'input
|
|
214
291
|
*/
|
|
215
292
|
isPointInside(x, y) {
|
|
216
293
|
return x >= this.x &&
|
|
@@ -220,42 +297,416 @@ class Input extends Component {
|
|
|
220
297
|
}
|
|
221
298
|
|
|
222
299
|
/**
|
|
223
|
-
*
|
|
300
|
+
* Obtient la couleur avec fallback
|
|
301
|
+
* @param {string} customColor - Couleur personnalisée
|
|
302
|
+
* @param {string} defaultColor - Couleur par défaut
|
|
303
|
+
* @returns {string} La couleur à utiliser
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
getColor(customColor, defaultColor) {
|
|
307
|
+
return this.disabled ? this.m3Colors.disabled :
|
|
308
|
+
(customColor || defaultColor);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Obtient la couleur de focus
|
|
313
|
+
* @returns {string} La couleur au focus
|
|
314
|
+
* @private
|
|
315
|
+
*/
|
|
316
|
+
getFocusColor() {
|
|
317
|
+
if (this.disabled) return this.m3Colors.outlineVariant;
|
|
318
|
+
if (this.error && this.errorText) return this.m3Colors.error;
|
|
319
|
+
return this.focusColor || this.m3Colors.defaultFocus;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Obtient la couleur de bordure
|
|
324
|
+
* @returns {string} La couleur de bordure
|
|
325
|
+
* @private
|
|
326
|
+
*/
|
|
327
|
+
getBorderColor() {
|
|
328
|
+
if (this.disabled) return this.m3Colors.outlineVariant;
|
|
329
|
+
if (this.error && this.errorText) return this.m3Colors.error;
|
|
330
|
+
if (this.focused) return this.getFocusColor();
|
|
331
|
+
return this.borderColor || this.m3Colors.defaultBorder;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Obtient la couleur de fond
|
|
336
|
+
* @returns {string} La couleur de fond
|
|
337
|
+
* @private
|
|
338
|
+
*/
|
|
339
|
+
getBackgroundColor() {
|
|
340
|
+
if (this.disabled) return this.m3Colors.disabledContainer;
|
|
341
|
+
return this.backgroundColor || this.m3Colors.defaultBackground;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Obtient la couleur du texte
|
|
346
|
+
* @returns {string} La couleur du texte
|
|
347
|
+
* @private
|
|
348
|
+
*/
|
|
349
|
+
getTextColor() {
|
|
350
|
+
if (this.disabled) return this.m3Colors.disabled;
|
|
351
|
+
return this.textColor || this.m3Colors.onSurface;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Obtient la couleur du placeholder
|
|
356
|
+
* @returns {string} La couleur du placeholder
|
|
357
|
+
* @private
|
|
358
|
+
*/
|
|
359
|
+
getPlaceholderColor() {
|
|
360
|
+
if (this.disabled) return this.m3Colors.disabled;
|
|
361
|
+
return this.placeholderColor || this.m3Colors.onSurfaceVariant;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Obtient la couleur du label
|
|
366
|
+
* @returns {string} La couleur du label
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
getLabelColor() {
|
|
370
|
+
if (this.disabled) return this.m3Colors.disabled;
|
|
371
|
+
if (this.error && this.errorText) return this.m3Colors.error;
|
|
372
|
+
if (this.focused) return this.getFocusColor();
|
|
373
|
+
return this.labelColor || this.m3Colors.onSurfaceVariant;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Dessine l'icône Material
|
|
224
378
|
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
379
|
+
* @param {string} type - Type d'icône ('leading' ou 'trailing')
|
|
380
|
+
* @private
|
|
225
381
|
*/
|
|
226
|
-
|
|
382
|
+
drawMaterialIcon(ctx, type) {
|
|
383
|
+
const isError = this.error && this.errorText;
|
|
384
|
+
|
|
227
385
|
ctx.save();
|
|
386
|
+
ctx.strokeStyle = this.disabled ? this.m3Colors.disabled :
|
|
387
|
+
isError ? this.m3Colors.error :
|
|
388
|
+
this.focused ? this.getFocusColor() : this.m3Colors.onSurfaceVariant;
|
|
389
|
+
ctx.lineWidth = 2;
|
|
390
|
+
ctx.lineCap = 'round';
|
|
228
391
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
392
|
+
// Position de l'icône
|
|
393
|
+
const iconSize = 24;
|
|
394
|
+
const iconX = type === 'leading' ?
|
|
395
|
+
this.x + 16 : this.x + this.width - 40;
|
|
396
|
+
const iconY = this.y + this.height / 2 - iconSize / 2;
|
|
397
|
+
|
|
398
|
+
// Dessiner une icône simple
|
|
399
|
+
ctx.beginPath();
|
|
400
|
+
if (type === 'leading') {
|
|
401
|
+
// Icône de recherche (loupe)
|
|
402
|
+
ctx.arc(iconX + 12, iconY + 12, 8, 0, Math.PI * 2);
|
|
403
|
+
ctx.moveTo(iconX + 18, iconY + 18);
|
|
404
|
+
ctx.lineTo(iconX + 22, iconY + 22);
|
|
405
|
+
} else {
|
|
406
|
+
// Icône d'erreur ou clear
|
|
407
|
+
if (this.error) {
|
|
408
|
+
// Point d'exclamation dans un cercle
|
|
409
|
+
ctx.arc(iconX + 12, iconY + 12, 10, 0, Math.PI * 2);
|
|
410
|
+
ctx.stroke();
|
|
411
|
+
ctx.fillStyle = this.m3Colors.error;
|
|
412
|
+
ctx.fill();
|
|
413
|
+
ctx.fillStyle = this.m3Colors.onError;
|
|
414
|
+
ctx.fillRect(iconX + 11, iconY + 6, 2, 8);
|
|
415
|
+
ctx.fillRect(iconX + 11, iconY + 16, 2, 2);
|
|
416
|
+
ctx.restore();
|
|
417
|
+
return;
|
|
418
|
+
} else {
|
|
419
|
+
// Croix simple (clear)
|
|
420
|
+
ctx.moveTo(iconX + 8, iconY + 8);
|
|
421
|
+
ctx.lineTo(iconX + 16, iconY + 16);
|
|
422
|
+
ctx.moveTo(iconX + 16, iconY + 8);
|
|
423
|
+
ctx.lineTo(iconX + 8, iconY + 16);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
ctx.stroke();
|
|
428
|
+
ctx.restore();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Dessine l'input Material Design 3 - Filled avec bordure en bas
|
|
433
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
434
|
+
* @private
|
|
435
|
+
*/
|
|
436
|
+
drawMaterialFilledInput(ctx) {
|
|
437
|
+
const isActive = this.focused || this.value.length > 0;
|
|
438
|
+
const showError = this.error && this.errorText;
|
|
439
|
+
|
|
440
|
+
// Container avec fond personnalisable
|
|
441
|
+
ctx.fillStyle = this.getBackgroundColor();
|
|
442
|
+
ctx.beginPath();
|
|
443
|
+
// Coins arrondis seulement en haut
|
|
444
|
+
ctx.moveTo(this.x + 12, this.y);
|
|
445
|
+
ctx.lineTo(this.x + this.width - 12, this.y);
|
|
446
|
+
ctx.quadraticCurveTo(this.x + this.width, this.y, this.x + this.width, this.y + 12);
|
|
447
|
+
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
448
|
+
ctx.lineTo(this.x, this.y + this.height);
|
|
449
|
+
ctx.lineTo(this.x, this.y + 12);
|
|
450
|
+
ctx.quadraticCurveTo(this.x, this.y, this.x + 12, this.y);
|
|
451
|
+
ctx.closePath();
|
|
452
|
+
ctx.fill();
|
|
453
|
+
|
|
454
|
+
// Bordure inférieure - toujours visible
|
|
455
|
+
const borderHeight = 1;
|
|
456
|
+
const focusBorderHeight = 2;
|
|
457
|
+
|
|
458
|
+
// Bordure normale (gris par défaut)
|
|
459
|
+
ctx.fillStyle = this.getBorderColor();
|
|
460
|
+
|
|
461
|
+
if (this.focused || showError) {
|
|
462
|
+
// Bordure épaisse au focus/erreur
|
|
463
|
+
ctx.fillRect(this.x, this.y + this.height - focusBorderHeight,
|
|
464
|
+
this.width, focusBorderHeight);
|
|
237
465
|
} else {
|
|
238
|
-
//
|
|
239
|
-
ctx.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
466
|
+
// Bordure fine normale
|
|
467
|
+
ctx.fillRect(this.x, this.y + this.height - borderHeight,
|
|
468
|
+
this.width, borderHeight);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Icône leading
|
|
472
|
+
if (this.leadingIcon) {
|
|
473
|
+
this.drawMaterialIcon(ctx, 'leading');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Icône trailing
|
|
477
|
+
if (this.trailingIcon) {
|
|
478
|
+
this.drawMaterialIcon(ctx, 'trailing');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Label flottant
|
|
482
|
+
if (this.hasLabel) {
|
|
483
|
+
ctx.fillStyle = this.getLabelColor();
|
|
484
|
+
ctx.font = `${isActive ? 12 : this.fontSize}px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
485
|
+
|
|
486
|
+
const labelX = this.x + this.paddingLeft;
|
|
487
|
+
const labelY = this.focused || this.value.length > 0 ?
|
|
488
|
+
this.y + 12 : this.y + this.height / 2;
|
|
489
|
+
|
|
490
|
+
ctx.fillText(this.label, labelX, labelY);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Texte de saisie
|
|
494
|
+
if (this.value || (!this.hasLabel && !this.focused)) {
|
|
495
|
+
const displayText = this.value || this.placeholder;
|
|
496
|
+
ctx.fillStyle = this.value ? this.getTextColor() : this.getPlaceholderColor();
|
|
497
|
+
ctx.font = `${this.fontSize}px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
498
|
+
ctx.textAlign = 'left';
|
|
499
|
+
ctx.textBaseline = 'middle';
|
|
500
|
+
|
|
501
|
+
const textX = this.x + this.paddingLeft;
|
|
502
|
+
const textY = this.hasLabel && (this.focused || this.value.length > 0) ?
|
|
503
|
+
this.y + this.height / 2 + 8 : this.y + this.height / 2;
|
|
504
|
+
|
|
505
|
+
// Troncature du texte
|
|
506
|
+
const maxWidth = this.width - this.paddingLeft - this.paddingRight;
|
|
507
|
+
let displayTextAdjusted = displayText;
|
|
508
|
+
let textWidth = ctx.measureText(displayText).width;
|
|
509
|
+
|
|
510
|
+
if (textWidth > maxWidth) {
|
|
511
|
+
while (textWidth > maxWidth && displayTextAdjusted.length > 0) {
|
|
512
|
+
displayTextAdjusted = displayTextAdjusted.slice(0, -1);
|
|
513
|
+
textWidth = ctx.measureText(displayTextAdjusted + '...').width;
|
|
514
|
+
}
|
|
515
|
+
displayTextAdjusted += '...';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
ctx.fillText(displayTextAdjusted, textX, textY);
|
|
519
|
+
|
|
520
|
+
// Curseur
|
|
521
|
+
if (this.focused && this.cursorVisible && this.value) {
|
|
522
|
+
ctx.fillStyle = this.getFocusColor();
|
|
523
|
+
ctx.fillRect(textX + textWidth, textY - 12, 2, 24);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Texte d'aide ou d'erreur
|
|
528
|
+
if (showError || this.helperText) {
|
|
529
|
+
const helpText = showError ? this.errorText : this.helperText;
|
|
530
|
+
ctx.fillStyle = showError ? this.m3Colors.error : this.m3Colors.onSurfaceVariant;
|
|
531
|
+
ctx.font = `12px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
532
|
+
ctx.textAlign = 'left';
|
|
533
|
+
ctx.textBaseline = 'top';
|
|
534
|
+
ctx.fillText(helpText, this.x + this.paddingLeft, this.y + this.height + 4);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Dessine l'input Material Design 3 - Outlined
|
|
540
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
541
|
+
* @private
|
|
542
|
+
*/
|
|
543
|
+
drawMaterialOutlinedInput(ctx) {
|
|
544
|
+
const isActive = this.focused || this.value.length > 0;
|
|
545
|
+
const showError = this.error && this.errorText;
|
|
546
|
+
|
|
547
|
+
// Background personnalisable
|
|
548
|
+
ctx.fillStyle = this.getBackgroundColor();
|
|
549
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 4);
|
|
550
|
+
ctx.fill();
|
|
551
|
+
|
|
552
|
+
// Bordure avec épaisseur variable
|
|
553
|
+
ctx.strokeStyle = this.getBorderColor();
|
|
554
|
+
ctx.lineWidth = this.focused ? 2 : 1;
|
|
555
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 4);
|
|
556
|
+
ctx.stroke();
|
|
557
|
+
|
|
558
|
+
// Fond pour le label (pour qu'il apparaisse au-dessus de la bordure)
|
|
559
|
+
if (this.hasLabel && (this.focused || this.value.length > 0)) {
|
|
560
|
+
ctx.fillStyle = this.getBackgroundColor();
|
|
561
|
+
ctx.fillRect(this.x + 12, this.y - 6, ctx.measureText(this.label).width + 8, 12);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Icône leading
|
|
565
|
+
if (this.leadingIcon) {
|
|
566
|
+
this.drawMaterialIcon(ctx, 'leading');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Icône trailing
|
|
570
|
+
if (this.trailingIcon) {
|
|
571
|
+
this.drawMaterialIcon(ctx, 'trailing');
|
|
244
572
|
}
|
|
245
573
|
|
|
574
|
+
// Label flottant
|
|
575
|
+
if (this.hasLabel) {
|
|
576
|
+
ctx.fillStyle = this.getLabelColor();
|
|
577
|
+
ctx.font = `${isActive ? 12 : this.fontSize}px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
578
|
+
|
|
579
|
+
const labelX = this.x + this.paddingLeft;
|
|
580
|
+
const labelY = this.focused || this.value.length > 0 ?
|
|
581
|
+
this.y - 2 : this.y + this.height / 2;
|
|
582
|
+
|
|
583
|
+
ctx.fillText(this.label, labelX, labelY);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Texte de saisie
|
|
587
|
+
if (this.value || (!this.hasLabel && !this.focused)) {
|
|
588
|
+
const displayText = this.value || this.placeholder;
|
|
589
|
+
ctx.fillStyle = this.value ? this.getTextColor() : this.getPlaceholderColor();
|
|
590
|
+
ctx.font = `${this.fontSize}px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
591
|
+
ctx.textAlign = 'left';
|
|
592
|
+
ctx.textBaseline = 'middle';
|
|
593
|
+
|
|
594
|
+
const textX = this.x + this.paddingLeft;
|
|
595
|
+
const textY = this.y + this.height / 2;
|
|
596
|
+
|
|
597
|
+
// Troncature du texte
|
|
598
|
+
const maxWidth = this.width - this.paddingLeft - this.paddingRight;
|
|
599
|
+
let displayTextAdjusted = displayText;
|
|
600
|
+
let textWidth = ctx.measureText(displayText).width;
|
|
601
|
+
|
|
602
|
+
if (textWidth > maxWidth) {
|
|
603
|
+
while (textWidth > maxWidth && displayTextAdjusted.length > 0) {
|
|
604
|
+
displayTextAdjusted = displayTextAdjusted.slice(0, -1);
|
|
605
|
+
textWidth = ctx.measureText(displayTextAdjusted + '...').width;
|
|
606
|
+
}
|
|
607
|
+
displayTextAdjusted += '...';
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
ctx.fillText(displayTextAdjusted, textX, textY);
|
|
611
|
+
|
|
612
|
+
// Curseur
|
|
613
|
+
if (this.focused && this.cursorVisible && this.value) {
|
|
614
|
+
ctx.fillStyle = this.getFocusColor();
|
|
615
|
+
ctx.fillRect(textX + textWidth, textY - 12, 2, 24);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Texte d'aide ou d'erreur
|
|
620
|
+
if (showError || this.helperText) {
|
|
621
|
+
const helpText = showError ? this.errorText : this.helperText;
|
|
622
|
+
ctx.fillStyle = showError ? this.m3Colors.error : this.m3Colors.onSurfaceVariant;
|
|
623
|
+
ctx.font = `12px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
624
|
+
ctx.textAlign = 'left';
|
|
625
|
+
ctx.textBaseline = 'top';
|
|
626
|
+
ctx.fillText(helpText, this.x + this.paddingLeft, this.y + this.height + 4);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Dessine l'input iOS Cupertino
|
|
632
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
633
|
+
* @private
|
|
634
|
+
*/
|
|
635
|
+
drawCupertinoInput(ctx) {
|
|
636
|
+
const showError = this.error && this.errorText;
|
|
637
|
+
|
|
638
|
+
// Background personnalisable
|
|
639
|
+
ctx.fillStyle = this.backgroundColor || this.cupertinoColors.background;
|
|
640
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 10);
|
|
641
|
+
ctx.fill();
|
|
642
|
+
|
|
643
|
+
// Bordure
|
|
644
|
+
ctx.strokeStyle = this.disabled ? this.cupertinoColors.border + '80' :
|
|
645
|
+
showError ? this.cupertinoColors.error :
|
|
646
|
+
this.focused ? (this.focusColor || this.cupertinoColors.blue) :
|
|
647
|
+
(this.borderColor || this.cupertinoColors.border);
|
|
648
|
+
ctx.lineWidth = this.focused ? 2 : 1;
|
|
649
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 10);
|
|
650
|
+
ctx.stroke();
|
|
651
|
+
|
|
246
652
|
// Texte
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
653
|
+
if (this.value || this.placeholder) {
|
|
654
|
+
const displayText = this.value || this.placeholder;
|
|
655
|
+
ctx.fillStyle = this.disabled ? this.cupertinoColors.disabled :
|
|
656
|
+
this.value ? (this.textColor || this.cupertinoColors.label) :
|
|
657
|
+
(this.placeholderColor || this.cupertinoColors.placeholder);
|
|
658
|
+
ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
659
|
+
ctx.textAlign = 'left';
|
|
660
|
+
ctx.textBaseline = 'middle';
|
|
661
|
+
|
|
662
|
+
// Calculer la largeur maximale disponible
|
|
663
|
+
const maxWidth = this.width - 32;
|
|
664
|
+
let displayTextAdjusted = displayText;
|
|
665
|
+
let textWidth = ctx.measureText(displayText).width;
|
|
666
|
+
|
|
667
|
+
// Tronquer le texte si nécessaire
|
|
668
|
+
if (textWidth > maxWidth) {
|
|
669
|
+
while (textWidth > maxWidth && displayTextAdjusted.length > 0) {
|
|
670
|
+
displayTextAdjusted = displayTextAdjusted.slice(0, -1);
|
|
671
|
+
textWidth = ctx.measureText(displayTextAdjusted + '...').width;
|
|
672
|
+
}
|
|
673
|
+
displayTextAdjusted += '...';
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
ctx.fillText(displayTextAdjusted, this.x + 16, this.y + this.height / 2);
|
|
677
|
+
|
|
678
|
+
// Curseur
|
|
679
|
+
if (this.focused && this.cursorVisible && this.value) {
|
|
680
|
+
ctx.fillStyle = this.focusColor || this.cupertinoColors.blue;
|
|
681
|
+
ctx.fillRect(this.x + 16 + textWidth, this.y + 10, 2, this.height - 20);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Texte d'erreur
|
|
686
|
+
if (showError) {
|
|
687
|
+
ctx.fillStyle = this.cupertinoColors.error;
|
|
688
|
+
ctx.font = `13px -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
689
|
+
ctx.textAlign = 'left';
|
|
690
|
+
ctx.textBaseline = 'top';
|
|
691
|
+
ctx.fillText(this.errorText, this.x + 16, this.y + this.height + 4);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Dessine l'input
|
|
697
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
698
|
+
*/
|
|
699
|
+
draw(ctx) {
|
|
700
|
+
ctx.save();
|
|
701
|
+
|
|
702
|
+
if (this.platform === 'material') {
|
|
703
|
+
if (this.variant === 'outlined') {
|
|
704
|
+
this.drawMaterialOutlinedInput(ctx);
|
|
705
|
+
} else {
|
|
706
|
+
this.drawMaterialFilledInput(ctx);
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
this.drawCupertinoInput(ctx);
|
|
259
710
|
}
|
|
260
711
|
|
|
261
712
|
ctx.restore();
|
|
@@ -263,15 +714,9 @@ class Input extends Component {
|
|
|
263
714
|
|
|
264
715
|
/**
|
|
265
716
|
* Dessine un rectangle avec coins arrondis
|
|
266
|
-
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
267
|
-
* @param {number} x - Position X
|
|
268
|
-
* @param {number} y - Position Y
|
|
269
|
-
* @param {number} width - Largeur
|
|
270
|
-
* @param {number} height - Hauteur
|
|
271
|
-
* @param {number} radius - Rayon des coins
|
|
272
|
-
* @private
|
|
273
717
|
*/
|
|
274
718
|
roundRect(ctx, x, y, width, height, radius) {
|
|
719
|
+
ctx.beginPath();
|
|
275
720
|
ctx.moveTo(x + radius, y);
|
|
276
721
|
ctx.lineTo(x + width - radius, y);
|
|
277
722
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
@@ -281,6 +726,84 @@ class Input extends Component {
|
|
|
281
726
|
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
282
727
|
ctx.lineTo(x, y + radius);
|
|
283
728
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
729
|
+
ctx.closePath();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Met à jour la valeur
|
|
734
|
+
* @param {string} newValue - Nouvelle valeur
|
|
735
|
+
*/
|
|
736
|
+
setValue(newValue) {
|
|
737
|
+
const oldValue = this.value;
|
|
738
|
+
this.value = newValue;
|
|
739
|
+
this.cursorPosition = this.value.length;
|
|
740
|
+
|
|
741
|
+
if (oldValue !== newValue) {
|
|
742
|
+
this.onChange(newValue);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Change la variante Material
|
|
748
|
+
* @param {string} variant - 'filled' ou 'outlined'
|
|
749
|
+
*/
|
|
750
|
+
setVariant(variant) {
|
|
751
|
+
if (variant === 'filled' || variant === 'outlined') {
|
|
752
|
+
this.variant = variant;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Définit les couleurs personnalisées
|
|
758
|
+
* @param {Object} colors - Objet contenant les couleurs
|
|
759
|
+
* @param {string} [colors.backgroundColor] - Couleur de fond
|
|
760
|
+
* @param {string} [colors.borderColor] - Couleur de bordure
|
|
761
|
+
* @param {string} [colors.focusColor] - Couleur au focus
|
|
762
|
+
* @param {string} [colors.textColor] - Couleur du texte
|
|
763
|
+
* @param {string} [colors.placeholderColor] - Couleur du placeholder
|
|
764
|
+
* @param {string} [colors.labelColor] - Couleur du label
|
|
765
|
+
*/
|
|
766
|
+
setColors(colors) {
|
|
767
|
+
if (colors.backgroundColor !== undefined) this.backgroundColor = colors.backgroundColor;
|
|
768
|
+
if (colors.borderColor !== undefined) this.borderColor = colors.borderColor;
|
|
769
|
+
if (colors.focusColor !== undefined) this.focusColor = colors.focusColor;
|
|
770
|
+
if (colors.textColor !== undefined) this.textColor = colors.textColor;
|
|
771
|
+
if (colors.placeholderColor !== undefined) this.placeholderColor = colors.placeholderColor;
|
|
772
|
+
if (colors.labelColor !== undefined) this.labelColor = colors.labelColor;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Active/désactive l'état d'erreur
|
|
777
|
+
* @param {boolean} error - État d'erreur
|
|
778
|
+
* @param {string} errorText - Texte d'erreur
|
|
779
|
+
*/
|
|
780
|
+
setError(error, errorText = '') {
|
|
781
|
+
this.error = error;
|
|
782
|
+
this.errorText = errorText;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Active/désactive l'état désactivé
|
|
787
|
+
* @param {boolean} disabled - État désactivé
|
|
788
|
+
*/
|
|
789
|
+
setDisabled(disabled) {
|
|
790
|
+
this.disabled = disabled;
|
|
791
|
+
if (disabled && this.focused) {
|
|
792
|
+
this.onBlur();
|
|
793
|
+
this.destroyHiddenInput();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Configure les icônes
|
|
799
|
+
* @param {boolean} leading - Afficher l'icône à gauche
|
|
800
|
+
* @param {boolean} trailing - Afficher l'icône à droite
|
|
801
|
+
*/
|
|
802
|
+
setIcons(leading, trailing) {
|
|
803
|
+
this.leadingIcon = leading;
|
|
804
|
+
this.trailingIcon = trailing;
|
|
805
|
+
this.paddingLeft = leading ? 56 : 16;
|
|
806
|
+
this.paddingRight = trailing ? 56 : 16;
|
|
284
807
|
}
|
|
285
808
|
|
|
286
809
|
/**
|
|
@@ -295,7 +818,6 @@ class Input extends Component {
|
|
|
295
818
|
|
|
296
819
|
Input.allInputs.delete(this);
|
|
297
820
|
|
|
298
|
-
// Si c'était le dernier input, retirer le gestionnaire global
|
|
299
821
|
if (Input.allInputs.size === 0 && Input.globalClickHandler) {
|
|
300
822
|
document.removeEventListener('click', Input.globalClickHandler, true);
|
|
301
823
|
document.removeEventListener('touchstart', Input.globalClickHandler, true);
|
|
@@ -83,6 +83,12 @@ class WebGLCanvasAdapter {
|
|
|
83
83
|
|
|
84
84
|
// Mode batch (true par défaut pour performance)
|
|
85
85
|
this.batchEnabled = true;
|
|
86
|
+
|
|
87
|
+
// ✅ AJOUTER CES LIGNES
|
|
88
|
+
this.colorCache = new Map();
|
|
89
|
+
this.colorCacheMaxSize = 100;
|
|
90
|
+
this.measureTextCache = new Map();
|
|
91
|
+
this.uniformLocations = null; // Sera initialisé dans initWebGL
|
|
86
92
|
|
|
87
93
|
this.initWebGL();
|
|
88
94
|
}
|
|
@@ -302,12 +308,19 @@ class WebGLCanvasAdapter {
|
|
|
302
308
|
}
|
|
303
309
|
|
|
304
310
|
measureText(text) {
|
|
311
|
+
const cacheKey = `${text}_${this.state.font}`;
|
|
312
|
+
|
|
313
|
+
// ✅ Cache lookup
|
|
314
|
+
if (this.measureTextCache.has(cacheKey)) {
|
|
315
|
+
return this.measureTextCache.get(cacheKey);
|
|
316
|
+
}
|
|
317
|
+
|
|
305
318
|
this.textCtx.save();
|
|
306
319
|
this.textCtx.font = this.state.font;
|
|
307
320
|
const metrics = this.textCtx.measureText(text);
|
|
308
321
|
this.textCtx.restore();
|
|
309
|
-
|
|
310
|
-
|
|
322
|
+
|
|
323
|
+
const result = {
|
|
311
324
|
width: metrics.width,
|
|
312
325
|
actualBoundingBoxAscent: metrics.actualBoundingBoxAscent || 0,
|
|
313
326
|
actualBoundingBoxDescent: metrics.actualBoundingBoxDescent || 0,
|
|
@@ -321,6 +334,15 @@ class WebGLCanvasAdapter {
|
|
|
321
334
|
hangingBaseline: metrics.hangingBaseline || 0,
|
|
322
335
|
ideographicBaseline: metrics.ideographicBaseline || 0
|
|
323
336
|
};
|
|
337
|
+
|
|
338
|
+
// ✅ Cache avec limite
|
|
339
|
+
if (this.measureTextCache.size >= 200) {
|
|
340
|
+
const firstKey = this.measureTextCache.keys().next().value;
|
|
341
|
+
this.measureTextCache.delete(firstKey);
|
|
342
|
+
}
|
|
343
|
+
this.measureTextCache.set(cacheKey, result);
|
|
344
|
+
|
|
345
|
+
return result;
|
|
324
346
|
}
|
|
325
347
|
|
|
326
348
|
// --- Paths ---
|
|
@@ -860,6 +882,21 @@ class WebGLCanvasAdapter {
|
|
|
860
882
|
this.solidProgram = this.createProgram(vsSolidSource, fsSolidSource);
|
|
861
883
|
this.textureProgram = this.createProgram(vsTextureSource, fsTextureSource);
|
|
862
884
|
|
|
885
|
+
// ✅ CACHE DES UNIFORM LOCATIONS
|
|
886
|
+
this.uniformLocations = {
|
|
887
|
+
solid: {
|
|
888
|
+
projection: this.gl.getUniformLocation(this.solidProgram, 'uProjectionMatrix'),
|
|
889
|
+
transform: this.gl.getUniformLocation(this.solidProgram, 'uTransformMatrix')
|
|
890
|
+
},
|
|
891
|
+
texture: {
|
|
892
|
+
projection: this.gl.getUniformLocation(this.textureProgram, 'uProjectionMatrix'),
|
|
893
|
+
transform: this.gl.getUniformLocation(this.textureProgram, 'uTransformMatrix'),
|
|
894
|
+
alpha: this.gl.getUniformLocation(this.textureProgram, 'uAlpha'),
|
|
895
|
+
tintColor: this.gl.getUniformLocation(this.textureProgram, 'uTintColor'),
|
|
896
|
+
texture: this.gl.getUniformLocation(this.textureProgram, 'uTexture')
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
|
|
863
900
|
// Buffers
|
|
864
901
|
this.positionBuffer = gl.createBuffer();
|
|
865
902
|
this.colorBuffer = gl.createBuffer();
|
|
@@ -954,13 +991,13 @@ class WebGLCanvasAdapter {
|
|
|
954
991
|
gl.bindVertexArray(this.solidVAO);
|
|
955
992
|
|
|
956
993
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
|
|
957
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.vertices), gl.
|
|
994
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.vertices), gl.DYNAMIC_DRAW);
|
|
958
995
|
|
|
959
996
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
|
|
960
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.colors), gl.
|
|
997
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.colors), gl.DYNAMIC_DRAW);
|
|
961
998
|
|
|
962
|
-
|
|
963
|
-
|
|
999
|
+
gl.uniformMatrix3fv(this.uniformLocations.solid.projection, false, this.projectionMatrix);
|
|
1000
|
+
gl.uniformMatrix3fv(this.uniformLocations.solid.transform, false, new Float32Array(this.state.transform));
|
|
964
1001
|
|
|
965
1002
|
const transformLoc = gl.getUniformLocation(this.solidProgram, 'uTransformMatrix');
|
|
966
1003
|
gl.uniformMatrix3fv(transformLoc, false, new Float32Array(this.state.transform));
|
|
@@ -976,13 +1013,13 @@ class WebGLCanvasAdapter {
|
|
|
976
1013
|
gl.bindVertexArray(this.textureVAO);
|
|
977
1014
|
|
|
978
1015
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
|
|
979
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.textureVertices), gl.
|
|
1016
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.textureVertices), gl.DYNAMIC_DRAW);
|
|
980
1017
|
|
|
981
1018
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
|
|
982
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.textureTexCoords), gl.
|
|
1019
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.textureTexCoords), gl.DYNAMIC_DRAW);
|
|
983
1020
|
|
|
984
|
-
|
|
985
|
-
|
|
1021
|
+
gl.uniformMatrix3fv(this.uniformLocations.solid.projection, false, this.projectionMatrix);
|
|
1022
|
+
gl.uniformMatrix3fv(this.uniformLocations.solid.transform, false, new Float32Array(this.state.transform));
|
|
986
1023
|
|
|
987
1024
|
const transformLoc = gl.getUniformLocation(this.textureProgram, 'uTransformMatrix');
|
|
988
1025
|
gl.uniformMatrix3fv(transformLoc, false, new Float32Array(this.state.transform));
|
|
@@ -1033,8 +1070,8 @@ class WebGLCanvasAdapter {
|
|
|
1033
1070
|
gl.enableVertexAttribArray(colorLoc);
|
|
1034
1071
|
gl.vertexAttribPointer(colorLoc, 4, gl.FLOAT, false, 0, 0);
|
|
1035
1072
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1073
|
+
gl.uniformMatrix3fv(this.uniformLocations.solid.projection, false, this.projectionMatrix);
|
|
1074
|
+
gl.uniformMatrix3fv(this.uniformLocations.solid.transform, false, new Float32Array(this.state.transform));
|
|
1038
1075
|
|
|
1039
1076
|
const transformLoc = gl.getUniformLocation(this.solidProgram, 'uTransformMatrix');
|
|
1040
1077
|
gl.uniformMatrix3fv(transformLoc, false, new Float32Array(this.state.transform));
|
|
@@ -1099,8 +1136,8 @@ class WebGLCanvasAdapter {
|
|
|
1099
1136
|
gl.enableVertexAttribArray(texLoc);
|
|
1100
1137
|
gl.vertexAttribPointer(texLoc, 2, gl.FLOAT, false, 0, 0);
|
|
1101
1138
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1139
|
+
gl.uniformMatrix3fv(this.uniformLocations.solid.projection, false, this.projectionMatrix);
|
|
1140
|
+
gl.uniformMatrix3fv(this.uniformLocations.solid.transform, false, new Float32Array(this.state.transform));
|
|
1104
1141
|
|
|
1105
1142
|
const transformLoc = gl.getUniformLocation(this.textureProgram, 'uTransformMatrix');
|
|
1106
1143
|
gl.uniformMatrix3fv(transformLoc, false, new Float32Array(this.state.transform));
|
|
@@ -1194,7 +1231,12 @@ class WebGLCanvasAdapter {
|
|
|
1194
1231
|
return [0, 0, 0, 1];
|
|
1195
1232
|
}
|
|
1196
1233
|
|
|
1197
|
-
|
|
1234
|
+
// ✅ AJOUTER CACHE LOOKUP
|
|
1235
|
+
if (this.colorCache.has(color)) {
|
|
1236
|
+
return this.colorCache.get(color);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (color._id && this.gradients.has(color._id)) {
|
|
1198
1240
|
return [0, 0, 0, 1];
|
|
1199
1241
|
}
|
|
1200
1242
|
|
|
@@ -1243,6 +1285,13 @@ class WebGLCanvasAdapter {
|
|
|
1243
1285
|
'cyan': [0, 1, 1, 1], 'magenta': [1, 0, 1, 1], 'gray': [0.5, 0.5, 0.5, 1],
|
|
1244
1286
|
'grey': [0.5, 0.5, 0.5, 1], 'transparent': [0, 0, 0, 0]
|
|
1245
1287
|
};
|
|
1288
|
+
|
|
1289
|
+
// ✅ AVANT LE RETURN FINAL, AJOUTER :
|
|
1290
|
+
if (this.colorCache.size >= this.colorCacheMaxSize) {
|
|
1291
|
+
const firstKey = this.colorCache.keys().next().value;
|
|
1292
|
+
this.colorCache.delete(firstKey);
|
|
1293
|
+
}
|
|
1294
|
+
this.colorCache.set(color, result); // result = le tableau [r,g,b,a]
|
|
1246
1295
|
|
|
1247
1296
|
return namedColors[color.toLowerCase()] || [0, 0, 0, 1];
|
|
1248
1297
|
}
|