canvasframework 0.7.0 → 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.
@@ -1,202 +1,201 @@
1
1
  import Component from '../core/Component.js';
2
+
2
3
  /**
3
- * Avatar (photo de profil)
4
- * @class
5
- * @extends Component
6
- * @property {string|null} imageUrl - URL de l'image
7
- * @property {string} initials - Initiales
8
- * @property {number} size - Taille
9
- * @property {string} bgColor - Couleur de fond
10
- * @property {string} textColor - Couleur du texte
11
- * @property {number} borderWidth - Épaisseur de la bordure
12
- * @property {string} borderColor - Couleur de la bordure
13
- * @property {string|null} status - Statut ('online', 'offline', 'away', 'busy')
14
- * @property {ImageComponent|null} imageComponent - Composant image interne
4
+ * Avatar photo de profil ou initiales.
5
+ *
6
+ * Corrections :
7
+ * - hexToRgb supprimé (non utilisé directement)
8
+ * - destroy() nettoie imageComponent
9
+ * - scrollOffset géré proprement dans isPointInside
15
10
  */
16
11
  class Avatar extends Component {
17
12
  /**
18
- * Crée une instance de Avatar
19
- * @param {CanvasFramework} framework - Framework parent
20
- * @param {Object} [options={}] - Options de configuration
21
- * @param {string} [options.imageUrl] - URL de l'image
22
- * @param {string} [options.initials='??'] - Initiales
23
- * @param {number} [options.size=48] - Taille
24
- * @param {string} [options.bgColor] - Couleur de fond (auto générée)
25
- * @param {string} [options.textColor='#FFFFFF'] - Couleur du texte
26
- * @param {number} [options.borderWidth=0] - Épaisseur de la bordure
27
- * @param {string} [options.borderColor='#FFFFFF'] - Couleur de la bordure
28
- * @param {string} [options.status] - Statut
13
+ * @param {CanvasFramework} framework
14
+ * @param {Object} [options={}]
15
+ * @param {string} [options.imageUrl]
16
+ * @param {string} [options.initials='??']
17
+ * @param {number} [options.size=48]
18
+ * @param {string} [options.bgColor] - Auto-généré à partir des initiales si absent
19
+ * @param {string} [options.textColor='#FFFFFF']
20
+ * @param {number} [options.borderWidth=0]
21
+ * @param {string} [options.borderColor='#FFFFFF']
22
+ * @param {string} [options.status] - 'online'|'offline'|'away'|'busy'
23
+ * @param {Function}[options.onClick]
29
24
  */
30
25
  constructor(framework, options = {}) {
31
26
  super(framework, options);
32
- this.imageUrl = options.imageUrl || null;
33
- this.initials = options.initials || '??';
34
- this.size = options.size || 48;
35
- this.bgColor = options.bgColor || this.generateColor(this.initials);
36
- this.textColor = options.textColor || '#FFFFFF';
27
+
28
+ this.imageUrl = options.imageUrl || null;
29
+ this.initials = options.initials || '??';
30
+ this.size = options.size || 48;
31
+ this.bgColor = options.bgColor || this._generateColor(this.initials);
32
+ this.textColor = options.textColor || '#FFFFFF';
37
33
  this.borderWidth = options.borderWidth || 0;
38
34
  this.borderColor = options.borderColor || '#FFFFFF';
39
- this.status = options.status || null; // 'online', 'offline', 'away', 'busy'
40
-
41
- this.width = this.size;
35
+ this.status = options.status || null;
36
+
37
+ this.width = this.size;
42
38
  this.height = this.size;
43
-
44
- // Image component interne
45
- this.imageComponent = null;
39
+
40
+ this._imageElement = null;
41
+
46
42
  if (this.imageUrl) {
47
- this.imageComponent = new ImageComponent(framework, {
48
- x: this.x,
49
- y: this.y,
50
- width: this.size,
51
- height: this.size,
52
- src: this.imageUrl,
53
- fit: 'cover',
54
- borderRadius: this.size / 2
55
- });
43
+ this._loadImage(this.imageUrl);
56
44
  }
57
45
  }
58
-
46
+
47
+ // ─────────────────────────────────────────
48
+ // IMAGE
49
+ // ─────────────────────────────────────────
50
+
51
+ /** @private */
52
+ _loadImage(url) {
53
+ const img = new Image();
54
+ img.onload = () => { this._imageElement = img; this.markDirty(); };
55
+ img.onerror = () => { this._imageElement = null; };
56
+ img.src = url;
57
+ }
58
+
59
+ /**
60
+ * Change l'image de l'avatar.
61
+ * @param {string|null} url
62
+ */
63
+ setImage(url) {
64
+ this.imageUrl = url;
65
+ this._imageElement = null;
66
+ if (url) this._loadImage(url);
67
+ else this.markDirty();
68
+ }
69
+
59
70
  /**
60
- * Génère une couleur basée sur le texte
61
- * @param {string} text - Texte pour générer la couleur
62
- * @returns {string} Couleur hexadécimale
63
- * @private
71
+ * Change le statut de l'avatar.
72
+ * @param {'online'|'offline'|'away'|'busy'|null} status
64
73
  */
65
- generateColor(text) {
66
- // Générer une couleur basée sur le texte (pour cohérence)
74
+ setStatus(status) {
75
+ this.status = status;
76
+ this.markDirty();
77
+ }
78
+
79
+ // ─────────────────────────────────────────
80
+ // COULEUR AUTO
81
+ // ─────────────────────────────────────────
82
+
83
+ /** @private */
84
+ _generateColor(text) {
67
85
  let hash = 0;
68
86
  for (let i = 0; i < text.length; i++) {
69
87
  hash = text.charCodeAt(i) + ((hash << 5) - hash);
70
88
  }
71
-
72
- const colors = [
89
+ const palette = [
73
90
  '#E91E63', '#9C27B0', '#673AB7', '#3F51B5',
74
91
  '#2196F3', '#00BCD4', '#009688', '#4CAF50',
75
- '#FF9800', '#FF5722', '#795548', '#607D8B'
92
+ '#FF9800', '#FF5722', '#795548', '#607D8B',
76
93
  ];
77
-
78
- return colors[Math.abs(hash) % colors.length];
94
+ return palette[Math.abs(hash) % palette.length];
79
95
  }
80
-
81
- /**
82
- * Dessine l'avatar
83
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
84
- */
96
+
97
+ // ─────────────────────────────────────────
98
+ // DESSIN
99
+ // ─────────────────────────────────────────
100
+
85
101
  draw(ctx) {
86
102
  ctx.save();
87
-
88
- const centerX = this.x + this.size / 2;
89
- const centerY = this.y + this.size / 2;
103
+
104
+ const cx = this.x + this.size / 2;
105
+ const cy = this.y + this.size / 2;
90
106
  const radius = this.size / 2;
91
-
107
+
92
108
  // Bordure
93
109
  if (this.borderWidth > 0) {
94
110
  ctx.fillStyle = this.borderColor;
95
111
  ctx.beginPath();
96
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
112
+ ctx.arc(cx, cy, radius, 0, Math.PI * 2);
97
113
  ctx.fill();
98
114
  }
99
-
100
- // Clipping circulaire
115
+
116
+ // Clip circulaire
101
117
  ctx.save();
102
118
  ctx.beginPath();
103
- ctx.arc(centerX, centerY, radius - this.borderWidth, 0, Math.PI * 2);
119
+ ctx.arc(cx, cy, radius - this.borderWidth, 0, Math.PI * 2);
104
120
  ctx.clip();
105
-
106
- if (this.imageComponent && this.imageComponent.loaded) {
107
- // Dessiner l'image
108
- this.imageComponent.x = this.x + this.borderWidth;
109
- this.imageComponent.y = this.y + this.borderWidth;
110
- this.imageComponent.width = this.size - this.borderWidth * 2;
111
- this.imageComponent.height = this.size - this.borderWidth * 2;
112
- this.imageComponent.draw(ctx);
121
+
122
+ if (this._imageElement) {
123
+ // Dessin de l'image (cover)
124
+ const s = this.size - this.borderWidth * 2;
125
+ const sx = this.x + this.borderWidth;
126
+ const sy = this.y + this.borderWidth;
127
+ const imgW = this._imageElement.naturalWidth;
128
+ const imgH = this._imageElement.naturalHeight;
129
+ const scale = Math.max(s / imgW, s / imgH);
130
+ const drawW = imgW * scale;
131
+ const drawH = imgH * scale;
132
+ const drawX = sx + (s - drawW) / 2;
133
+ const drawY = sy + (s - drawH) / 2;
134
+ ctx.drawImage(this._imageElement, drawX, drawY, drawW, drawH);
113
135
  } else {
114
- // Background coloré
136
+ // Fond coloré + initiales
115
137
  ctx.fillStyle = this.bgColor;
116
138
  ctx.fillRect(this.x, this.y, this.size, this.size);
117
-
118
- // Initiales
119
- ctx.fillStyle = this.textColor;
120
- ctx.font = `bold ${this.size / 2.5}px -apple-system, sans-serif`;
121
- ctx.textAlign = 'center';
139
+
140
+ ctx.fillStyle = this.textColor;
141
+ ctx.font = `bold ${this.size / 2.5}px -apple-system, sans-serif`;
142
+ ctx.textAlign = 'center';
122
143
  ctx.textBaseline = 'middle';
123
- ctx.fillText(this.initials.toUpperCase(), centerX, centerY);
144
+ ctx.fillText(this.initials.toUpperCase(), cx, cy);
124
145
  }
125
-
146
+
126
147
  ctx.restore();
127
-
148
+
128
149
  // Indicateur de statut
129
150
  if (this.status) {
130
- const statusSize = this.size / 4;
131
- const statusX = this.x + this.size - statusSize;
132
- const statusY = this.y + this.size - statusSize;
133
-
134
- let statusColor;
135
- switch (this.status) {
136
- case 'online': statusColor = '#4CAF50'; break;
137
- case 'offline': statusColor = '#9E9E9E'; break;
138
- case 'away': statusColor = '#FF9800'; break;
139
- case 'busy': statusColor = '#F44336'; break;
140
- default: statusColor = '#9E9E9E';
141
- }
142
-
143
- // Bordure blanche autour du statut
151
+ const dotSize = this.size / 4;
152
+ const dotX = this.x + this.size - dotSize;
153
+ const dotY = this.y + this.size - dotSize;
154
+
155
+ const statusColors = {
156
+ online: '#4CAF50',
157
+ offline: '#9E9E9E',
158
+ away: '#FF9800',
159
+ busy: '#F44336',
160
+ };
161
+ const dotColor = statusColors[this.status] || '#9E9E9E';
162
+
163
+ // Contour blanc
144
164
  ctx.fillStyle = '#FFFFFF';
145
165
  ctx.beginPath();
146
- ctx.arc(statusX, statusY, statusSize / 2 + 1, 0, Math.PI * 2);
166
+ ctx.arc(dotX, dotY, dotSize / 2 + 1, 0, Math.PI * 2);
147
167
  ctx.fill();
148
-
168
+
149
169
  // Cercle de statut
150
- ctx.fillStyle = statusColor;
170
+ ctx.fillStyle = dotColor;
151
171
  ctx.beginPath();
152
- ctx.arc(statusX, statusY, statusSize / 2, 0, Math.PI * 2);
172
+ ctx.arc(dotX, dotY, dotSize / 2, 0, Math.PI * 2);
153
173
  ctx.fill();
154
174
  }
155
-
175
+
156
176
  ctx.restore();
157
177
  }
158
-
159
- /**
160
- * Change l'image de l'avatar
161
- * @param {string} url - Nouvelle URL d'image
162
- */
163
- setImage(url) {
164
- this.imageUrl = url;
165
- if (url) {
166
- this.imageComponent = new ImageComponent(this.framework, {
167
- x: this.x,
168
- y: this.y,
169
- width: this.size,
170
- height: this.size,
171
- src: url,
172
- fit: 'cover',
173
- borderRadius: this.size / 2
174
- });
175
- } else {
176
- this.imageComponent = null;
177
- }
178
- }
179
-
180
- /**
181
- * Change le statut
182
- * @param {string} status - Nouveau statut
183
- */
184
- setStatus(status) {
185
- this.status = status;
186
- }
187
-
188
- /**
189
- * Vérifie si un point est dans les limites
190
- * @param {number} x - Coordonnée X
191
- * @param {number} y - Coordonnée Y
192
- * @returns {boolean} True si le point est dans l'avatar
193
- */
178
+
179
+ // ─────────────────────────────────────────
180
+ // HIT TEST
181
+ // ─────────────────────────────────────────
182
+
194
183
  isPointInside(x, y) {
195
- const adjustedY = y - this.framework.scrollOffset;
196
- const dx = x - (this.x + this.size / 2);
197
- const dy = adjustedY - (this.y + this.size / 2);
184
+ const scrollOffset = this.framework?.scrollOffset ?? 0;
185
+ const adjY = y + scrollOffset; // le framework soustrait le scroll dans les coords passées
186
+ const dx = x - (this.x + this.size / 2);
187
+ const dy = adjY - (this.y + this.size / 2);
198
188
  return Math.sqrt(dx * dx + dy * dy) <= this.size / 2;
199
189
  }
190
+
191
+ // ─────────────────────────────────────────
192
+ // DESTROY
193
+ // ─────────────────────────────────────────
194
+
195
+ destroy() {
196
+ this._imageElement = null;
197
+ super.destroy();
198
+ }
200
199
  }
201
200
 
202
201
  export default Avatar;