canvasframework 0.3.15 → 0.3.16

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,35 +1,11 @@
1
1
  import Component from '../core/Component.js';
2
+
2
3
  /**
3
- * Accordéon (section extensible)
4
+ * Accordion (section extensible) avec styles Material & Cupertino + Ripple centré Android
4
5
  * @class
5
6
  * @extends Component
6
- * @property {string} title - Titre
7
- * @property {string} content - Contenu
8
- * @property {string|null} icon - Icône
9
- * @property {boolean} expanded - Déplié
10
- * @property {string} platform - Plateforme
11
- * @property {number} headerHeight - Hauteur de l'en-tête
12
- * @property {number} contentPadding - Padding du contenu
13
- * @property {string} bgColor - Couleur de fond
14
- * @property {string} borderColor - Couleur de la bordure
15
- * @property {Function} onToggle - Callback au toggle
16
- * @property {boolean} animating - En cours d'animation
17
- * @property {number} animProgress - Progression de l'animation
18
- * @property {number} contentHeight - Hauteur du contenu
19
7
  */
20
8
  class Accordion extends Component {
21
- /**
22
- * Crée une instance de Accordion
23
- * @param {CanvasFramework} framework - Framework parent
24
- * @param {Object} [options={}] - Options de configuration
25
- * @param {string} [options.title=''] - Titre
26
- * @param {string} [options.content=''] - Contenu
27
- * @param {string} [options.icon] - Icône
28
- * @param {boolean} [options.expanded=false] - Déplié initialement
29
- * @param {Function} [options.onToggle] - Callback au toggle
30
- * @param {string} [options.bgColor='#FFFFFF'] - Couleur de fond
31
- * @param {string} [options.borderColor='#E0E0E0'] - Couleur de bordure
32
- */
33
9
  constructor(framework, options = {}) {
34
10
  super(framework, options);
35
11
  this.title = options.title || '';
@@ -44,58 +20,47 @@ class Accordion extends Component {
44
20
  this.onToggle = options.onToggle;
45
21
  this.animating = false;
46
22
  this.animProgress = this.expanded ? 1 : 0;
47
-
48
- // Calculer la hauteur du contenu
23
+
49
24
  this.calculateContentHeight();
50
25
  this.height = this.headerHeight + (this.expanded ? this.contentHeight : 0);
51
-
52
- // CORRECTION: Bloquer les clics pendant l'animation
26
+
27
+ // Pour les ripples Material
28
+ this.ripples = [];
29
+ this.rippleColor = 'rgba(1,0,0,0.2)';
30
+
31
+ // Clic
53
32
  this.onClick = () => {
54
- // Ignorer les clics pendant l'animation
55
- if (this.animating) {
56
- return;
33
+ if (this.animating) return;
34
+
35
+ // Ripple centré Material
36
+ if (this.platform === 'material') {
37
+ this.addRipple();
57
38
  }
39
+
58
40
  this.toggle();
59
41
  };
60
42
  }
61
-
62
- /**
63
- * Calcule la hauteur du contenu
64
- * @private
65
- */
43
+
66
44
  calculateContentHeight() {
67
45
  const ctx = this.framework.ctx;
68
46
  ctx.save();
69
47
  ctx.font = '14px -apple-system, sans-serif';
70
-
71
- // Diviser le contenu en lignes
72
- const maxWidth = this.width - (this.contentPadding * 2);
48
+ const maxWidth = this.width - this.contentPadding * 2;
73
49
  const lines = this.wrapText(ctx, this.content, maxWidth);
74
- const lineHeight = 20;
75
-
76
50
  ctx.restore();
77
- this.contentHeight = lines.length * lineHeight + (this.contentPadding * 2);
51
+ const lineHeight = 20;
52
+ this.contentHeight = lines.length * lineHeight + this.contentPadding * 2;
78
53
  }
79
-
80
- /**
81
- * Divise le texte en lignes
82
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
83
- * @param {string} text - Texte
84
- * @param {number} maxWidth - Largeur maximale
85
- * @returns {string[]} Tableau de lignes
86
- * @private
87
- */
54
+
88
55
  wrapText(ctx, text, maxWidth) {
89
56
  const words = text.split(' ');
90
57
  const lines = [];
91
58
  let currentLine = words[0] || '';
92
-
93
59
  for (let i = 1; i < words.length; i++) {
94
60
  const word = words[i];
95
- const width = ctx.measureText(currentLine + " " + word).width;
96
- if (width < maxWidth) {
97
- currentLine += " " + word;
98
- } else {
61
+ const width = ctx.measureText(currentLine + ' ' + word).width;
62
+ if (width < maxWidth) currentLine += ' ' + word;
63
+ else {
99
64
  lines.push(currentLine);
100
65
  currentLine = word;
101
66
  }
@@ -103,64 +68,124 @@ class Accordion extends Component {
103
68
  lines.push(currentLine);
104
69
  return lines;
105
70
  }
106
-
107
- /**
108
- * Alterne l'état déplié/replié
109
- */
71
+
110
72
  toggle() {
111
- // Empêcher les toggles multiples pendant l'animation
112
73
  if (this.animating) return;
113
-
114
74
  this.expanded = !this.expanded;
115
75
  if (this.onToggle) this.onToggle(this.expanded);
116
76
  this.animate();
117
77
  }
118
-
119
- /**
120
- * Anime le toggle
121
- * @private
122
- */
78
+
123
79
  animate() {
124
80
  if (this.animating) return;
125
81
  this.animating = true;
126
-
127
82
  const target = this.expanded ? 1 : 0;
128
83
  const step = 0.1;
129
-
84
+
130
85
  const doAnimate = () => {
131
86
  if (Math.abs(this.animProgress - target) < 0.01) {
132
87
  this.animProgress = target;
133
- this.height = this.headerHeight + (this.contentHeight * this.animProgress);
88
+ this.height = this.headerHeight + this.contentHeight * this.animProgress;
134
89
  this.animating = false;
135
90
  return;
136
91
  }
137
-
138
92
  this.animProgress += this.animProgress < target ? step : -step;
139
- this.height = this.headerHeight + (this.contentHeight * this.animProgress);
93
+ this.height = this.headerHeight + this.contentHeight * this.animProgress;
140
94
  requestAnimationFrame(doAnimate);
141
95
  };
142
-
143
96
  doAnimate();
144
97
  }
145
-
146
- /**
147
- * Dessine l'accordéon
148
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
149
- */
98
+
99
+ addRipple() {
100
+ const ripple = {
101
+ x: this.width / 2,
102
+ y: this.headerHeight / 2,
103
+ radius: 0,
104
+ maxRadius: Math.max(this.width, this.headerHeight) * 1.5,
105
+ opacity: 0.3
106
+ };
107
+ this.ripples.push(ripple);
108
+ this.animateRipples();
109
+ }
110
+
111
+ animateRipples() {
112
+ const animate = () => {
113
+ let active = false;
114
+ for (let ripple of this.ripples) {
115
+ if (ripple.radius < ripple.maxRadius) {
116
+ ripple.radius += ripple.maxRadius / 15;
117
+ ripple.opacity -= 0.03;
118
+ active = true;
119
+ }
120
+ }
121
+ this.ripples = this.ripples.filter(r => r.opacity > 0);
122
+ if (active) requestAnimationFrame(animate);
123
+ };
124
+ animate();
125
+ }
126
+
150
127
  draw(ctx) {
151
128
  ctx.save();
152
-
129
+
130
+ let headerBg = '#FFFFFF';
131
+ let headerTextColor = '#000000';
132
+ let borderColor = this.borderColor;
133
+ let shadowBlur = 0;
134
+ let chevronWidth = 2;
135
+
136
+ if (this.platform === 'material') {
137
+ headerBg = '#F5F5F5';
138
+ headerTextColor = '#212121';
139
+ shadowBlur = 4;
140
+ chevronWidth = 3;
141
+ } else if (this.platform === 'cupertino') {
142
+ headerBg = '#FFFFFF';
143
+ headerTextColor = '#000000';
144
+ borderColor = '#C7C7CC';
145
+ chevronWidth = 1.5;
146
+ }
147
+
148
+ // Ombre Material
149
+ if (shadowBlur > 0) {
150
+ ctx.shadowColor = 'rgba(0,0,0,0.2)';
151
+ ctx.shadowBlur = shadowBlur;
152
+ ctx.shadowOffsetX = 0;
153
+ ctx.shadowOffsetY = 2;
154
+ }
155
+
153
156
  // Background
154
157
  ctx.fillStyle = this.bgColor;
155
158
  ctx.fillRect(this.x, this.y, this.width, this.height);
156
-
157
- // Bordure
158
- ctx.strokeStyle = this.borderColor;
159
- ctx.lineWidth = 1;
160
- ctx.strokeRect(this.x, this.y, this.width, this.height);
161
-
159
+
160
+ // Bordure Cupertino
161
+ if (this.platform === 'cupertino') {
162
+ ctx.strokeStyle = borderColor;
163
+ ctx.lineWidth = 1;
164
+ ctx.strokeRect(this.x, this.y, this.width, this.height);
165
+ }
166
+
162
167
  // Header
163
- // Icône (si présente)
168
+ ctx.fillStyle = headerBg;
169
+ ctx.fillRect(this.x, this.y, this.width, this.headerHeight);
170
+
171
+ // Ripple centré Material
172
+ if (this.platform === 'material' && this.ripples.length) {
173
+ ctx.save();
174
+ ctx.beginPath();
175
+ ctx.rect(this.x, this.y, this.width, this.headerHeight);
176
+ ctx.clip();
177
+ for (let ripple of this.ripples) {
178
+ ctx.globalAlpha = ripple.opacity;
179
+ ctx.fillStyle = this.rippleColor;
180
+ ctx.beginPath();
181
+ ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
182
+ ctx.fill();
183
+ }
184
+ ctx.restore();
185
+ ctx.globalAlpha = 1;
186
+ }
187
+
188
+ // Icône
164
189
  if (this.icon) {
165
190
  ctx.font = '20px sans-serif';
166
191
  ctx.fillStyle = '#666666';
@@ -168,26 +193,27 @@ class Accordion extends Component {
168
193
  ctx.textBaseline = 'middle';
169
194
  ctx.fillText(this.icon, this.x + 16, this.y + this.headerHeight / 2);
170
195
  }
171
-
196
+
172
197
  // Titre
173
- ctx.fillStyle = '#000000';
174
- ctx.font = 'bold 16px -apple-system, sans-serif';
198
+ ctx.fillStyle = headerTextColor;
199
+ ctx.font =
200
+ this.platform === 'material'
201
+ ? 'bold 16px Roboto, sans-serif'
202
+ : 'bold 16px -apple-system, sans-serif';
175
203
  ctx.textAlign = 'left';
176
204
  ctx.textBaseline = 'middle';
177
- const titleX = this.x + (this.icon ? 56 : 16);
205
+ const titleX = this.icon ? this.x + 56 : this.x + 16;
178
206
  ctx.fillText(this.title, titleX, this.y + this.headerHeight / 2);
179
-
180
- // Chevron (flèche)
207
+
208
+ // Chevron
181
209
  const chevronX = this.x + this.width - 30;
182
210
  const chevronY = this.y + this.headerHeight / 2;
183
211
  const chevronRotation = this.animProgress * Math.PI;
184
-
185
212
  ctx.save();
186
213
  ctx.translate(chevronX, chevronY);
187
214
  ctx.rotate(chevronRotation);
188
-
189
215
  ctx.strokeStyle = '#666666';
190
- ctx.lineWidth = 2;
216
+ ctx.lineWidth = chevronWidth;
191
217
  ctx.lineCap = 'round';
192
218
  ctx.lineJoin = 'round';
193
219
  ctx.beginPath();
@@ -195,58 +221,45 @@ class Accordion extends Component {
195
221
  ctx.lineTo(0, 3);
196
222
  ctx.lineTo(6, -3);
197
223
  ctx.stroke();
198
-
199
224
  ctx.restore();
200
-
201
- // Contenu (si expanded ou en train d'animer)
225
+
226
+ // Contenu
202
227
  if (this.animProgress > 0) {
203
228
  ctx.save();
204
-
205
- // Clipping pour l'animation
206
229
  ctx.beginPath();
207
230
  ctx.rect(this.x, this.y + this.headerHeight, this.width, this.contentHeight * this.animProgress);
208
231
  ctx.clip();
209
-
210
- // Divider
211
- ctx.strokeStyle = this.borderColor;
232
+
233
+ ctx.strokeStyle = borderColor;
212
234
  ctx.lineWidth = 1;
213
235
  ctx.beginPath();
214
236
  ctx.moveTo(this.x, this.y + this.headerHeight);
215
237
  ctx.lineTo(this.x + this.width, this.y + this.headerHeight);
216
238
  ctx.stroke();
217
-
218
- // Texte du contenu
239
+
219
240
  ctx.fillStyle = '#666666';
220
241
  ctx.font = '14px -apple-system, sans-serif';
221
242
  ctx.textAlign = 'left';
222
243
  ctx.textBaseline = 'top';
223
-
224
244
  const contentX = this.x + this.contentPadding;
225
245
  const contentY = this.y + this.headerHeight + this.contentPadding;
226
- const maxWidth = this.width - (this.contentPadding * 2);
246
+ const maxWidth = this.width - this.contentPadding * 2;
227
247
  const lines = this.wrapText(ctx, this.content, maxWidth);
228
248
  const lineHeight = 20;
229
-
230
249
  lines.forEach((line, index) => {
231
- ctx.fillText(line, contentX, contentY + (index * lineHeight));
250
+ ctx.fillText(line, contentX, contentY + index * lineHeight);
232
251
  });
233
-
252
+
234
253
  ctx.restore();
235
254
  }
236
-
255
+
237
256
  ctx.restore();
238
257
  }
239
-
240
- /**
241
- * Vérifie si un point est dans les limites
242
- * @param {number} x - Coordonnée X
243
- * @param {number} y - Coordonnée Y
244
- * @returns {boolean} True si le point est dans l'en-tête
245
- */
258
+
246
259
  isPointInside(x, y) {
247
- return x >= this.x && x <= this.x + this.width &&
260
+ return x >= this.x && x <= this.x + this.width &&
248
261
  y >= this.y && y <= this.y + this.headerHeight;
249
262
  }
250
263
  }
251
264
 
252
- export default Accordion;
265
+ export default Accordion;