canvasframework 0.3.9 → 0.3.11
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/AppBar.js +164 -70
- package/components/BottomNavigationBar.js +206 -69
- package/components/InputDatalist.js +723 -0
- package/components/InputTags.js +586 -0
- package/components/PasswordInput.js +462 -0
- package/components/TimePicker.js +443 -0
- package/core/CanvasFramework.js +7 -4
- package/index.js +4 -1
- package/package.json +1 -1
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import Component from '../core/Component.js';
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
|
-
* Barre de navigation inférieure
|
|
4
|
+
* Barre de navigation inférieure (Material & Cupertino)
|
|
4
5
|
* @class
|
|
5
6
|
* @extends Component
|
|
6
|
-
* @property {Array} items - Items de navigation
|
|
7
|
-
* @property {number} selectedIndex - Index sélectionné
|
|
8
|
-
* @property {Function} onChange - Callback au changement
|
|
9
|
-
* @property {string} platform - Plateforme
|
|
10
|
-
* @property {string} bgColor - Couleur de fond
|
|
11
|
-
* @property {string} selectedColor - Couleur sélectionnée
|
|
12
|
-
* @property {string} unselectedColor - Couleur non sélectionnée
|
|
13
7
|
*/
|
|
14
8
|
class BottomNavigationBar extends Component {
|
|
15
9
|
/**
|
|
@@ -19,45 +13,106 @@ class BottomNavigationBar extends Component {
|
|
|
19
13
|
* @param {Array} [options.items=[]] - Items [{icon, label}]
|
|
20
14
|
* @param {number} [options.selectedIndex=0] - Index sélectionné
|
|
21
15
|
* @param {Function} [options.onChange] - Callback au changement
|
|
22
|
-
* @param {number} [options.height] - Hauteur
|
|
23
|
-
* @param {string} [options.bgColor] - Couleur de fond
|
|
24
|
-
* @param {string} [options.selectedColor] - Couleur sélectionnée
|
|
25
|
-
* @param {string} [options.unselectedColor
|
|
16
|
+
* @param {number} [options.height] - Hauteur
|
|
17
|
+
* @param {string} [options.bgColor] - Couleur de fond
|
|
18
|
+
* @param {string} [options.selectedColor] - Couleur sélectionnée
|
|
19
|
+
* @param {string} [options.unselectedColor] - Couleur non sélectionnée
|
|
26
20
|
*/
|
|
27
21
|
constructor(framework, options = {}) {
|
|
22
|
+
const height = options.height || (framework.platform === 'material' ? 56 : 50);
|
|
23
|
+
|
|
28
24
|
super(framework, {
|
|
29
25
|
x: 0,
|
|
30
|
-
y: framework.height -
|
|
26
|
+
y: framework.height - height,
|
|
31
27
|
width: framework.width,
|
|
32
|
-
height:
|
|
28
|
+
height: height,
|
|
33
29
|
...options
|
|
34
30
|
});
|
|
31
|
+
|
|
35
32
|
this.items = options.items || [];
|
|
36
33
|
this.selectedIndex = options.selectedIndex || 0;
|
|
37
34
|
this.onChange = options.onChange;
|
|
38
35
|
this.platform = framework.platform;
|
|
39
|
-
this.bgColor = options.bgColor || '#FFFFFF';
|
|
40
|
-
this.selectedColor = options.selectedColor || (framework.platform === 'material' ? '#6200EE' : '#007AFF');
|
|
41
|
-
this.unselectedColor = options.unselectedColor || '#757575';
|
|
42
36
|
|
|
43
|
-
//
|
|
37
|
+
// Couleurs selon la plateforme
|
|
38
|
+
if (this.platform === 'material') {
|
|
39
|
+
this.bgColor = options.bgColor || '#FFFFFF';
|
|
40
|
+
this.selectedColor = options.selectedColor || '#6200EE';
|
|
41
|
+
this.unselectedColor = options.unselectedColor || '#757575';
|
|
42
|
+
this.rippleColor = 'rgba(98, 0, 238, 0.2)';
|
|
43
|
+
} else {
|
|
44
|
+
// iOS : background transparent avec blur
|
|
45
|
+
this.bgColor = options.bgColor || 'rgba(248, 248, 248, 0.95)';
|
|
46
|
+
this.selectedColor = options.selectedColor || '#007AFF';
|
|
47
|
+
this.unselectedColor = options.unselectedColor || '#8E8E93';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Ripple effect (Material)
|
|
51
|
+
this.ripples = [];
|
|
52
|
+
|
|
53
|
+
// Animation de l'indicateur (iOS)
|
|
54
|
+
this.indicatorX = 0;
|
|
55
|
+
this.targetIndicatorX = 0;
|
|
56
|
+
this.animatingIndicator = false;
|
|
57
|
+
|
|
44
58
|
this.onPress = this.handlePress.bind(this);
|
|
59
|
+
|
|
60
|
+
// Initialiser la position de l'indicateur
|
|
61
|
+
this.updateIndicatorPosition();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Met à jour la position de l'indicateur iOS
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
updateIndicatorPosition() {
|
|
69
|
+
const itemWidth = this.width / this.items.length;
|
|
70
|
+
this.targetIndicatorX = this.selectedIndex * itemWidth;
|
|
71
|
+
|
|
72
|
+
if (!this.animatingIndicator) {
|
|
73
|
+
this.indicatorX = this.targetIndicatorX;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Anime l'indicateur iOS
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
animateIndicator() {
|
|
82
|
+
this.animatingIndicator = true;
|
|
83
|
+
const animate = () => {
|
|
84
|
+
const diff = this.targetIndicatorX - this.indicatorX;
|
|
85
|
+
if (Math.abs(diff) > 0.5) {
|
|
86
|
+
this.indicatorX += diff * 0.2;
|
|
87
|
+
requestAnimationFrame(animate);
|
|
88
|
+
} else {
|
|
89
|
+
this.indicatorX = this.targetIndicatorX;
|
|
90
|
+
this.animatingIndicator = false;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
animate();
|
|
45
94
|
}
|
|
46
95
|
|
|
47
96
|
/**
|
|
48
97
|
* Dessine la barre de navigation
|
|
49
|
-
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
50
98
|
*/
|
|
51
99
|
draw(ctx) {
|
|
52
100
|
ctx.save();
|
|
53
101
|
|
|
54
|
-
//
|
|
102
|
+
// Background
|
|
103
|
+
ctx.fillStyle = this.bgColor;
|
|
104
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
105
|
+
|
|
106
|
+
// Bordure/Ombre supérieure
|
|
55
107
|
if (this.platform === 'material') {
|
|
56
108
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.1)';
|
|
57
109
|
ctx.shadowBlur = 8;
|
|
58
110
|
ctx.shadowOffsetY = -2;
|
|
111
|
+
ctx.fillRect(this.x, this.y, this.width, 1);
|
|
112
|
+
ctx.shadowColor = 'transparent';
|
|
59
113
|
} else {
|
|
60
|
-
|
|
114
|
+
// iOS : fine ligne de séparation
|
|
115
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
|
|
61
116
|
ctx.lineWidth = 0.5;
|
|
62
117
|
ctx.beginPath();
|
|
63
118
|
ctx.moveTo(this.x, this.y);
|
|
@@ -65,136 +120,218 @@ class BottomNavigationBar extends Component {
|
|
|
65
120
|
ctx.stroke();
|
|
66
121
|
}
|
|
67
122
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
ctx.shadowColor = 'transparent';
|
|
123
|
+
// Ripples (Material)
|
|
124
|
+
if (this.platform === 'material') {
|
|
125
|
+
this.drawRipples(ctx);
|
|
126
|
+
}
|
|
73
127
|
|
|
74
128
|
// Items
|
|
75
129
|
const itemWidth = this.width / this.items.length;
|
|
130
|
+
|
|
76
131
|
for (let i = 0; i < this.items.length; i++) {
|
|
77
132
|
const item = this.items[i];
|
|
78
133
|
const itemX = this.x + i * itemWidth;
|
|
79
134
|
const isSelected = i === this.selectedIndex;
|
|
80
135
|
const color = isSelected ? this.selectedColor : this.unselectedColor;
|
|
81
136
|
|
|
137
|
+
// iOS : Indicateur de sélection (fond arrondi)
|
|
138
|
+
if (this.platform === 'cupertino' && isSelected) {
|
|
139
|
+
ctx.fillStyle = `${this.selectedColor}15`;
|
|
140
|
+
const indicatorWidth = 60;
|
|
141
|
+
const indicatorHeight = 32;
|
|
142
|
+
const indicatorX = itemX + itemWidth / 2 - indicatorWidth / 2;
|
|
143
|
+
const indicatorY = this.y + 6;
|
|
144
|
+
|
|
145
|
+
ctx.beginPath();
|
|
146
|
+
this.roundRect(ctx, indicatorX, indicatorY, indicatorWidth, indicatorHeight, 16);
|
|
147
|
+
ctx.fill();
|
|
148
|
+
}
|
|
149
|
+
|
|
82
150
|
// Icône
|
|
83
|
-
this.
|
|
151
|
+
const iconY = this.platform === 'material' ? this.y + 12 : this.y + 8;
|
|
152
|
+
this.drawIcon(ctx, item.icon, itemX + itemWidth / 2, iconY, color, isSelected);
|
|
84
153
|
|
|
85
154
|
// Label
|
|
86
155
|
ctx.fillStyle = color;
|
|
87
|
-
|
|
156
|
+
const fontSize = this.platform === 'material' ? 12 : 10;
|
|
157
|
+
ctx.font = `${isSelected && this.platform === 'material' ? 'bold ' : ''}${fontSize}px -apple-system, Roboto, sans-serif`;
|
|
88
158
|
ctx.textAlign = 'center';
|
|
89
159
|
ctx.textBaseline = 'top';
|
|
90
|
-
|
|
160
|
+
const labelY = this.platform === 'material' ? this.y + 34 : this.y + 30;
|
|
161
|
+
ctx.fillText(item.label, itemX + itemWidth / 2, labelY);
|
|
91
162
|
}
|
|
92
163
|
|
|
93
164
|
ctx.restore();
|
|
94
165
|
}
|
|
95
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Dessine les ripples (Material)
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
drawRipples(ctx) {
|
|
172
|
+
for (let ripple of this.ripples) {
|
|
173
|
+
ctx.save();
|
|
174
|
+
ctx.globalAlpha = ripple.opacity;
|
|
175
|
+
ctx.fillStyle = this.rippleColor;
|
|
176
|
+
ctx.beginPath();
|
|
177
|
+
ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
|
|
178
|
+
ctx.fill();
|
|
179
|
+
ctx.restore();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Anime les effets ripple
|
|
185
|
+
* @private
|
|
186
|
+
*/
|
|
187
|
+
animateRipple() {
|
|
188
|
+
const animate = () => {
|
|
189
|
+
let hasActiveRipples = false;
|
|
190
|
+
|
|
191
|
+
for (let ripple of this.ripples) {
|
|
192
|
+
if (ripple.radius < ripple.maxRadius) {
|
|
193
|
+
ripple.radius += ripple.maxRadius / 12;
|
|
194
|
+
hasActiveRipples = true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (ripple.radius >= ripple.maxRadius * 0.5) {
|
|
198
|
+
ripple.opacity -= 0.05;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.ripples = this.ripples.filter(r => r.opacity > 0);
|
|
203
|
+
|
|
204
|
+
if (hasActiveRipples) {
|
|
205
|
+
requestAnimationFrame(animate);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
animate();
|
|
210
|
+
}
|
|
211
|
+
|
|
96
212
|
/**
|
|
97
213
|
* Dessine une icône
|
|
98
|
-
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
99
|
-
* @param {string} icon - Type d'icône
|
|
100
|
-
* @param {number} x - Position X
|
|
101
|
-
* @param {number} y - Position Y
|
|
102
|
-
* @param {string} color - Couleur
|
|
103
214
|
* @private
|
|
104
215
|
*/
|
|
105
|
-
drawIcon(ctx, icon, x, y, color) {
|
|
216
|
+
drawIcon(ctx, icon, x, y, color, isSelected) {
|
|
106
217
|
ctx.strokeStyle = color;
|
|
107
218
|
ctx.fillStyle = color;
|
|
108
|
-
ctx.lineWidth = 2;
|
|
219
|
+
ctx.lineWidth = isSelected ? 2.5 : 2;
|
|
109
220
|
ctx.lineCap = 'round';
|
|
110
221
|
ctx.lineJoin = 'round';
|
|
111
222
|
|
|
112
223
|
switch(icon) {
|
|
113
224
|
case 'home':
|
|
114
225
|
ctx.beginPath();
|
|
115
|
-
ctx.moveTo(x, y);
|
|
116
|
-
ctx.lineTo(x - 10, y +
|
|
117
|
-
ctx.lineTo(x - 10, y +
|
|
118
|
-
ctx.lineTo(x + 10, y +
|
|
119
|
-
ctx.lineTo(x + 10, y +
|
|
226
|
+
ctx.moveTo(x, y + 2);
|
|
227
|
+
ctx.lineTo(x - 10, y + 10);
|
|
228
|
+
ctx.lineTo(x - 10, y + 18);
|
|
229
|
+
ctx.lineTo(x + 10, y + 18);
|
|
230
|
+
ctx.lineTo(x + 10, y + 10);
|
|
120
231
|
ctx.closePath();
|
|
121
|
-
ctx.
|
|
232
|
+
if (isSelected) ctx.fill();
|
|
233
|
+
else ctx.stroke();
|
|
122
234
|
break;
|
|
123
235
|
|
|
124
236
|
case 'search':
|
|
125
237
|
ctx.beginPath();
|
|
126
|
-
ctx.arc(x - 2, y +
|
|
238
|
+
ctx.arc(x - 2, y + 6, 7, 0, Math.PI * 2);
|
|
127
239
|
ctx.stroke();
|
|
128
240
|
ctx.beginPath();
|
|
129
|
-
ctx.moveTo(x +
|
|
130
|
-
ctx.lineTo(x +
|
|
241
|
+
ctx.moveTo(x + 4, y + 11);
|
|
242
|
+
ctx.lineTo(x + 9, y + 16);
|
|
131
243
|
ctx.stroke();
|
|
132
244
|
break;
|
|
133
245
|
|
|
134
246
|
case 'favorite':
|
|
135
247
|
ctx.beginPath();
|
|
136
|
-
ctx.moveTo(x, y +
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
ctx.lineTo(x + 10, y + 6);
|
|
143
|
-
ctx.lineTo(x + 4, y + 6);
|
|
144
|
-
ctx.lineTo(x, y);
|
|
145
|
-
ctx.lineTo(x - 4, y + 6);
|
|
146
|
-
ctx.lineTo(x - 10, y + 6);
|
|
248
|
+
ctx.moveTo(x, y + 3);
|
|
249
|
+
for (let i = 0; i < 5; i++) {
|
|
250
|
+
const angle = (i * 4 * Math.PI / 5) - Math.PI / 2;
|
|
251
|
+
const radius = i % 2 === 0 ? 9 : 4;
|
|
252
|
+
ctx.lineTo(x + Math.cos(angle) * radius, y + 10 + Math.sin(angle) * radius);
|
|
253
|
+
}
|
|
147
254
|
ctx.closePath();
|
|
148
|
-
ctx.fill();
|
|
255
|
+
if (isSelected) ctx.fill();
|
|
256
|
+
else ctx.stroke();
|
|
149
257
|
break;
|
|
150
258
|
|
|
151
259
|
case 'person':
|
|
152
260
|
ctx.beginPath();
|
|
153
|
-
ctx.arc(x, y +
|
|
261
|
+
ctx.arc(x, y + 6, 5, 0, Math.PI * 2);
|
|
154
262
|
ctx.stroke();
|
|
155
263
|
ctx.beginPath();
|
|
156
|
-
ctx.arc(x, y +
|
|
264
|
+
ctx.arc(x, y + 20, 9, Math.PI, 0, true);
|
|
157
265
|
ctx.stroke();
|
|
158
266
|
break;
|
|
159
267
|
|
|
160
268
|
case 'settings':
|
|
161
269
|
ctx.beginPath();
|
|
162
|
-
ctx.arc(x, y +
|
|
270
|
+
ctx.arc(x, y + 10, 5, 0, Math.PI * 2);
|
|
163
271
|
ctx.stroke();
|
|
164
272
|
for (let i = 0; i < 4; i++) {
|
|
165
273
|
const angle = (i * Math.PI / 2) - Math.PI / 4;
|
|
166
274
|
ctx.beginPath();
|
|
167
|
-
ctx.moveTo(x + Math.cos(angle) *
|
|
168
|
-
ctx.lineTo(x + Math.cos(angle) *
|
|
275
|
+
ctx.moveTo(x + Math.cos(angle) * 7, y + 10 + Math.sin(angle) * 7);
|
|
276
|
+
ctx.lineTo(x + Math.cos(angle) * 11, y + 10 + Math.sin(angle) * 11);
|
|
169
277
|
ctx.stroke();
|
|
170
278
|
}
|
|
171
279
|
break;
|
|
172
280
|
}
|
|
173
281
|
}
|
|
174
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Dessine un rectangle arrondi
|
|
285
|
+
* @private
|
|
286
|
+
*/
|
|
287
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
288
|
+
ctx.moveTo(x + radius, y);
|
|
289
|
+
ctx.lineTo(x + width - radius, y);
|
|
290
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
291
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
292
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
293
|
+
ctx.lineTo(x + radius, y + height);
|
|
294
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
295
|
+
ctx.lineTo(x, y + radius);
|
|
296
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
297
|
+
}
|
|
298
|
+
|
|
175
299
|
/**
|
|
176
300
|
* Vérifie si un point est dans les limites
|
|
177
|
-
* @param {number} x - Coordonnée X
|
|
178
|
-
* @param {number} y - Coordonnée Y
|
|
179
|
-
* @returns {boolean} True si le point est dans la barre
|
|
180
301
|
*/
|
|
181
302
|
isPointInside(x, y) {
|
|
182
|
-
|
|
303
|
+
return y >= this.y && y <= this.y + this.height;
|
|
183
304
|
}
|
|
184
305
|
|
|
185
306
|
/**
|
|
186
307
|
* Gère la pression (clic)
|
|
187
|
-
* @param {number} x - Coordonnée X
|
|
188
|
-
* @param {number} y - Coordonnée Y
|
|
189
308
|
* @private
|
|
190
309
|
*/
|
|
191
310
|
handlePress(x, y) {
|
|
192
|
-
// Calculer quel item a été cliqué
|
|
193
311
|
const itemWidth = this.width / this.items.length;
|
|
194
312
|
const index = Math.floor(x / itemWidth);
|
|
195
313
|
|
|
196
314
|
if (index >= 0 && index < this.items.length && index !== this.selectedIndex) {
|
|
315
|
+
// Ripple effect (Material)
|
|
316
|
+
if (this.platform === 'material') {
|
|
317
|
+
this.ripples.push({
|
|
318
|
+
x: (index + 0.5) * itemWidth,
|
|
319
|
+
y: this.y + this.height / 2,
|
|
320
|
+
radius: 0,
|
|
321
|
+
maxRadius: itemWidth / 2,
|
|
322
|
+
opacity: 1
|
|
323
|
+
});
|
|
324
|
+
this.animateRipple();
|
|
325
|
+
}
|
|
326
|
+
|
|
197
327
|
this.selectedIndex = index;
|
|
328
|
+
this.updateIndicatorPosition();
|
|
329
|
+
|
|
330
|
+
// Animer l'indicateur (iOS)
|
|
331
|
+
if (this.platform === 'cupertino') {
|
|
332
|
+
this.animateIndicator();
|
|
333
|
+
}
|
|
334
|
+
|
|
198
335
|
if (this.onChange) {
|
|
199
336
|
this.onChange(index, this.items[index]);
|
|
200
337
|
}
|