canvasframework 0.6.3 → 0.7.1
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 +159 -99
- package/components/Avatar.js +142 -143
- package/components/BottomSheet.js +124 -106
- package/components/Button.js +207 -254
- package/components/Checkbox.js +98 -91
- package/components/Chip.js +137 -106
- package/components/Dialog.js +161 -146
- package/components/FAB.js +122 -195
- package/components/Switch.js +146 -112
- package/components/Text.js +104 -103
- package/components/Toast.js +132 -156
- package/components/VirtualList.js +88 -59
- package/core/CanvasFramework.js +115 -44
- package/core/CanvasUtils.js +141 -0
- package/core/Component.js +124 -66
- package/core/ThemeManager.js +162 -176
- package/core/WebGLCanvasAdapter.js +88 -39
- package/package.json +1 -1
package/components/FAB.js
CHANGED
|
@@ -1,269 +1,196 @@
|
|
|
1
1
|
import Component from '../core/Component.js';
|
|
2
|
+
import { roundRect, hexToRgb, hexToRgba, darkenColor } from '../core/CanvasUtils.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @property {string} bgColor - Couleur de fond
|
|
14
|
-
* @property {string} iconColor - Couleur de l'icône
|
|
15
|
-
* @property {Array} ripples - Effets ripple
|
|
5
|
+
* FAB — Floating Action Button (Material Design 3 + Cupertino).
|
|
6
|
+
*
|
|
7
|
+
* Variantes : 'small' | 'medium' | 'large' | 'extended'
|
|
8
|
+
*
|
|
9
|
+
* Corrections :
|
|
10
|
+
* - Utilitaires couleur/dessin viennent de CanvasUtils
|
|
11
|
+
* - markDirty() utilisé à la place de redraw() global
|
|
12
|
+
* - Guard _destroyed dans le RAF
|
|
13
|
+
* - destroy() annule le RAF
|
|
16
14
|
*/
|
|
17
15
|
class FAB extends Component {
|
|
18
16
|
/**
|
|
19
|
-
*
|
|
20
|
-
* @param {
|
|
21
|
-
* @param {
|
|
22
|
-
* @param {
|
|
23
|
-
* @param {
|
|
24
|
-
* @param {string}
|
|
25
|
-
* @param {string}
|
|
26
|
-
* @param {string}
|
|
27
|
-
* @param {string} [options.iconColor='#FFFFFF'] - Couleur de l'icône
|
|
17
|
+
* @param {CanvasFramework} framework
|
|
18
|
+
* @param {Object} [options={}]
|
|
19
|
+
* @param {string} [options.icon='+']
|
|
20
|
+
* @param {boolean} [options.extended=false]
|
|
21
|
+
* @param {string} [options.text='']
|
|
22
|
+
* @param {string} [options.variant='medium'] - 'small'|'medium'|'large'|'extended'
|
|
23
|
+
* @param {string} [options.bgColor]
|
|
24
|
+
* @param {string} [options.iconColor='#FFFFFF']
|
|
28
25
|
*/
|
|
29
26
|
constructor(framework, options = {}) {
|
|
30
27
|
super(framework, options);
|
|
31
|
-
|
|
32
|
-
this.icon
|
|
28
|
+
|
|
29
|
+
this.icon = options.icon || '+';
|
|
33
30
|
this.extended = options.extended || false;
|
|
34
|
-
this.text
|
|
31
|
+
this.text = options.text || '';
|
|
35
32
|
this.platform = framework.platform;
|
|
36
|
-
this.variant
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const sizes = {
|
|
40
|
-
small: 40,
|
|
41
|
-
medium: 56,
|
|
42
|
-
large: 96
|
|
43
|
-
};
|
|
44
|
-
|
|
33
|
+
this.variant = options.variant || 'medium';
|
|
34
|
+
|
|
35
|
+
const sizes = { small: 40, medium: 56, large: 96 };
|
|
45
36
|
this.size = options.size || sizes[this.variant] || 56;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.bgColor = options.bgColor || (framework.platform === 'material' ? '#6750A4' : '#007AFF');
|
|
37
|
+
|
|
38
|
+
this.bgColor = options.bgColor || (framework.platform === 'material' ? '#6750A4' : '#007AFF');
|
|
49
39
|
this.iconColor = options.iconColor || '#FFFFFF';
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
large: 28,
|
|
56
|
-
extended: 16
|
|
57
|
-
}[this.variant] || 16;
|
|
58
|
-
|
|
59
|
-
// Position par défaut en bas à droite
|
|
60
|
-
this.x = options.x !== undefined ? options.x : framework.width - this.size - 16;
|
|
40
|
+
|
|
41
|
+
this.borderRadius = { small: 12, medium: 16, large: 28, extended: 16 }[this.variant] || 16;
|
|
42
|
+
|
|
43
|
+
// Position par défaut : coin bas-droit
|
|
44
|
+
this.x = options.x !== undefined ? options.x : framework.width - this.size - 16;
|
|
61
45
|
this.y = options.y !== undefined ? options.y : framework.height - this.size - 80;
|
|
62
|
-
|
|
63
|
-
// Si extended, ajuster la largeur
|
|
46
|
+
|
|
64
47
|
if (this.extended && this.text) {
|
|
65
48
|
const ctx = framework.ctx;
|
|
66
49
|
ctx.save();
|
|
67
|
-
ctx.font
|
|
50
|
+
ctx.font = 'bold 14px -apple-system, sans-serif';
|
|
68
51
|
const textWidth = ctx.measureText(this.text).width;
|
|
69
52
|
ctx.restore();
|
|
70
|
-
this.width
|
|
53
|
+
this.width = this.size + textWidth + 24;
|
|
71
54
|
this.borderRadius = 16;
|
|
72
55
|
} else {
|
|
73
56
|
this.width = this.size;
|
|
74
57
|
}
|
|
75
58
|
this.height = this.size;
|
|
76
|
-
|
|
77
|
-
// Effet ripple
|
|
59
|
+
|
|
78
60
|
this.ripples = [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.onPress = this.
|
|
61
|
+
this._rafId = null;
|
|
62
|
+
|
|
63
|
+
this.onPress = this._handlePress.bind(this);
|
|
82
64
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
handlePress(x, y) {
|
|
91
|
-
// Créer un ripple au point de clic (Material uniquement)
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────
|
|
67
|
+
// INTERACTIONS
|
|
68
|
+
// ─────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/** @private */
|
|
71
|
+
_handlePress(x, y) {
|
|
92
72
|
if (this.platform === 'material') {
|
|
93
|
-
const
|
|
73
|
+
const adjY = y - (this.framework.scrollOffset || 0);
|
|
94
74
|
this.ripples.push({
|
|
95
75
|
x: x - this.x,
|
|
96
|
-
y:
|
|
76
|
+
y: adjY - this.y,
|
|
97
77
|
radius: 0,
|
|
98
78
|
maxRadius: Math.max(this.width, this.height) * 1.5,
|
|
99
|
-
opacity: 1
|
|
79
|
+
opacity: 1,
|
|
100
80
|
});
|
|
101
|
-
this.
|
|
81
|
+
this._animateRipple();
|
|
102
82
|
}
|
|
103
83
|
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
animateRipple() {
|
|
84
|
+
|
|
85
|
+
/** @private */
|
|
86
|
+
_animateRipple() {
|
|
87
|
+
if (this._rafId) return;
|
|
88
|
+
|
|
110
89
|
const animate = () => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Fade out après 50% de l'expansion
|
|
120
|
-
if (ripple.radius >= ripple.maxRadius * 0.5) {
|
|
121
|
-
ripple.opacity -= 0.05;
|
|
122
|
-
}
|
|
90
|
+
if (this._destroyed) { this._rafId = null; return; }
|
|
91
|
+
|
|
92
|
+
let hasActive = false;
|
|
93
|
+
for (const r of this.ripples) {
|
|
94
|
+
if (r.radius < r.maxRadius) { r.radius += r.maxRadius / 15; hasActive = true; }
|
|
95
|
+
if (r.radius >= r.maxRadius * 0.5) r.opacity -= 0.05;
|
|
123
96
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
requestAnimationFrame(animate);
|
|
97
|
+
this.ripples = this.ripples.filter((r) => r.opacity > 0);
|
|
98
|
+
|
|
99
|
+
this.markDirty();
|
|
100
|
+
|
|
101
|
+
if (hasActive && this.ripples.length > 0) {
|
|
102
|
+
this._rafId = requestAnimationFrame(animate);
|
|
103
|
+
} else {
|
|
104
|
+
this._rafId = null;
|
|
130
105
|
}
|
|
131
106
|
};
|
|
132
|
-
|
|
133
|
-
animate
|
|
107
|
+
|
|
108
|
+
this._rafId = requestAnimationFrame(animate);
|
|
134
109
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
110
|
+
|
|
111
|
+
// ─────────────────────────────────────────
|
|
112
|
+
// DESSIN
|
|
113
|
+
// ─────────────────────────────────────────
|
|
114
|
+
|
|
140
115
|
draw(ctx) {
|
|
141
116
|
ctx.save();
|
|
142
|
-
|
|
143
|
-
// Ombre (elevation)
|
|
117
|
+
|
|
144
118
|
if (!this.pressed) {
|
|
145
|
-
ctx.shadowColor
|
|
146
|
-
ctx.shadowBlur
|
|
119
|
+
ctx.shadowColor = 'rgba(0,0,0,0.3)';
|
|
120
|
+
ctx.shadowBlur = this.platform === 'material' ? 8 : 12;
|
|
147
121
|
ctx.shadowOffsetY = this.platform === 'material' ? 4 : 6;
|
|
148
122
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ctx.fillStyle = this.pressed ? this.darkenColor(this.bgColor) : this.bgColor;
|
|
123
|
+
|
|
124
|
+
ctx.fillStyle = this.pressed ? darkenColor(this.bgColor) : this.bgColor;
|
|
152
125
|
ctx.beginPath();
|
|
153
|
-
|
|
126
|
+
roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
|
|
154
127
|
ctx.fill();
|
|
155
|
-
|
|
156
|
-
ctx.shadowColor
|
|
157
|
-
ctx.shadowBlur
|
|
128
|
+
|
|
129
|
+
ctx.shadowColor = 'transparent';
|
|
130
|
+
ctx.shadowBlur = 0;
|
|
158
131
|
ctx.shadowOffsetY = 0;
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
if (this.platform === 'material') {
|
|
132
|
+
|
|
133
|
+
// Ripple (Material)
|
|
134
|
+
if (this.platform === 'material' && this.ripples.length > 0) {
|
|
162
135
|
ctx.save();
|
|
163
136
|
ctx.beginPath();
|
|
164
|
-
|
|
137
|
+
roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
|
|
165
138
|
ctx.clip();
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
ctx.
|
|
170
|
-
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
|
139
|
+
|
|
140
|
+
for (const r of this.ripples) {
|
|
141
|
+
ctx.globalAlpha = r.opacity;
|
|
142
|
+
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
|
171
143
|
ctx.beginPath();
|
|
172
|
-
ctx.arc(this.x +
|
|
144
|
+
ctx.arc(this.x + r.x, this.y + r.y, r.radius, 0, Math.PI * 2);
|
|
173
145
|
ctx.fill();
|
|
174
146
|
}
|
|
175
|
-
|
|
176
147
|
ctx.restore();
|
|
177
148
|
}
|
|
178
|
-
|
|
179
|
-
// Overlay
|
|
149
|
+
|
|
150
|
+
// Overlay pressé (iOS)
|
|
180
151
|
if (this.pressed && this.platform === 'cupertino') {
|
|
181
|
-
ctx.fillStyle = 'rgba(0,
|
|
152
|
+
ctx.fillStyle = 'rgba(0,0,0,0.1)';
|
|
182
153
|
ctx.beginPath();
|
|
183
|
-
|
|
154
|
+
roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
|
|
184
155
|
ctx.fill();
|
|
185
156
|
}
|
|
186
|
-
|
|
157
|
+
|
|
187
158
|
// Icône
|
|
188
|
-
ctx.fillStyle
|
|
189
|
-
const iconSize
|
|
190
|
-
ctx.font
|
|
191
|
-
ctx.textAlign
|
|
159
|
+
ctx.fillStyle = this.iconColor;
|
|
160
|
+
const iconSize = this.variant === 'large' ? 36 : 24;
|
|
161
|
+
ctx.font = `bold ${iconSize}px sans-serif`;
|
|
162
|
+
ctx.textAlign = 'center';
|
|
192
163
|
ctx.textBaseline = 'middle';
|
|
193
|
-
|
|
164
|
+
|
|
194
165
|
if (this.extended && this.text) {
|
|
195
|
-
// Icône à gauche
|
|
196
166
|
ctx.fillText(this.icon, this.x + this.size / 2, this.y + this.size / 2);
|
|
197
|
-
|
|
198
|
-
// Texte à droite
|
|
199
167
|
ctx.font = 'bold 14px -apple-system, sans-serif';
|
|
200
168
|
ctx.fillText(this.text, this.x + this.size + 12, this.y + this.size / 2);
|
|
201
169
|
} else {
|
|
202
|
-
// Icône centrée
|
|
203
170
|
ctx.fillText(this.icon, this.x + this.width / 2, this.y + this.height / 2);
|
|
204
171
|
}
|
|
205
|
-
|
|
172
|
+
|
|
206
173
|
ctx.restore();
|
|
207
174
|
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Dessine un rectangle avec coins arrondis
|
|
211
|
-
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
212
|
-
* @param {number} x - Position X
|
|
213
|
-
* @param {number} y - Position Y
|
|
214
|
-
* @param {number} width - Largeur
|
|
215
|
-
* @param {number} height - Hauteur
|
|
216
|
-
* @param {number} radius - Rayon des coins
|
|
217
|
-
* @private
|
|
218
|
-
*/
|
|
219
|
-
roundRect(ctx, x, y, width, height, radius) {
|
|
220
|
-
ctx.moveTo(x + radius, y);
|
|
221
|
-
ctx.lineTo(x + width - radius, y);
|
|
222
|
-
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
223
|
-
ctx.lineTo(x + width, y + height - radius);
|
|
224
|
-
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
225
|
-
ctx.lineTo(x + radius, y + height);
|
|
226
|
-
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
227
|
-
ctx.lineTo(x, y + radius);
|
|
228
|
-
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Assombrit une couleur
|
|
233
|
-
* @param {string} color - Couleur hexadécimale
|
|
234
|
-
* @returns {string} Couleur assombrie
|
|
235
|
-
* @private
|
|
236
|
-
*/
|
|
237
|
-
darkenColor(color) {
|
|
238
|
-
const rgb = this.hexToRgb(color);
|
|
239
|
-
return `rgb(${Math.max(0, rgb.r - 30)}, ${Math.max(0, rgb.g - 30)}, ${Math.max(0, rgb.b - 30)})`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Convertit une couleur hex en RGB
|
|
244
|
-
* @param {string} hex - Couleur hexadécimale
|
|
245
|
-
* @returns {{r: number, g: number, b: number}} Objet RGB
|
|
246
|
-
* @private
|
|
247
|
-
*/
|
|
248
|
-
hexToRgb(hex) {
|
|
249
|
-
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
250
|
-
return result ? {
|
|
251
|
-
r: parseInt(result[1], 16),
|
|
252
|
-
g: parseInt(result[2], 16),
|
|
253
|
-
b: parseInt(result[3], 16)
|
|
254
|
-
} : { r: 0, g: 0, b: 0 };
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Vérifie si un point est dans les limites
|
|
259
|
-
* @param {number} x - Coordonnée X
|
|
260
|
-
* @param {number} y - Coordonnée Y
|
|
261
|
-
* @returns {boolean} True si le point est dans le FAB
|
|
262
|
-
*/
|
|
175
|
+
|
|
263
176
|
isPointInside(x, y) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
177
|
+
return (
|
|
178
|
+
x >= this.x && x <= this.x + this.width &&
|
|
179
|
+
y >= this.y && y <= this.y + this.height
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─────────────────────────────────────────
|
|
184
|
+
// DESTROY
|
|
185
|
+
// ─────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
destroy() {
|
|
188
|
+
if (this._rafId) {
|
|
189
|
+
cancelAnimationFrame(this._rafId);
|
|
190
|
+
this._rafId = null;
|
|
191
|
+
}
|
|
192
|
+
this.ripples = [];
|
|
193
|
+
super.destroy();
|
|
267
194
|
}
|
|
268
195
|
}
|
|
269
196
|
|