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,50 +1,65 @@
1
1
  import Component from '../core/Component.js';
2
+ import { roundRect } from '../core/CanvasUtils.js';
2
3
 
3
4
  /**
4
- * Dialog Material (Android) & Cupertino (iOS)
5
+ * Dialog Material (Android) & Cupertino (iOS).
6
+ *
7
+ * Corrections :
8
+ * - roundRect() vient de CanvasUtils
9
+ * - Guard _destroyed dans les boucles RAF (fade in/out)
10
+ * - destroy() nettoie les ressources
5
11
  */
6
12
  class Dialog extends Component {
13
+ /**
14
+ * @param {CanvasFramework} framework
15
+ * @param {Object} [options={}]
16
+ * @param {string} [options.title='']
17
+ * @param {string} [options.message='']
18
+ * @param {string[]} [options.buttons=['OK']]
19
+ * @param {Function} [options.onButtonClick] - (index, label) => void
20
+ */
7
21
  constructor(framework, options = {}) {
8
22
  super(framework, {
9
23
  x: 0,
10
24
  y: 0,
11
25
  width: framework.width,
12
26
  height: framework.height,
13
- visible: false
27
+ visible: false,
14
28
  });
15
29
 
16
- this.platform = framework.platform;
17
-
18
- this.title = options.title || '';
19
- this.message = options.message || '';
20
- this.buttons = options.buttons || ['OK'];
30
+ this.platform = framework.platform;
31
+ this.title = options.title || '';
32
+ this.message = options.message || '';
33
+ this.buttons = options.buttons || ['OK'];
21
34
  this.onButtonClick = options.onButtonClick;
22
35
 
23
- this.dialogWidth = Math.min(320, framework.width - 40);
36
+ this.dialogWidth = Math.min(320, framework.width - 40);
24
37
  this.dialogHeight = 160;
25
- this.opacity = 0;
26
-
27
- this.isVisible = false;
28
- this.buttonRects = [];
38
+ this.opacity = 0;
39
+ this.isVisible = false;
40
+ this.buttonRects = [];
41
+ this._rafId = null;
29
42
 
30
- /* Ripple Android uniquement */
31
- this.ripples = [];
32
- this._animating = false;
43
+ // Ripple (Material uniquement)
44
+ this.ripples = [];
45
+ this._rippleRaf = null;
33
46
 
34
- this.onPress = this.handlePress.bind(this);
47
+ this.onPress = this._handlePress.bind(this);
35
48
 
36
- this.messageLines = this.wrapText(
49
+ // Pré-calcul du wrapping du message
50
+ this.messageLines = this._wrapText(
37
51
  this.message,
38
52
  this.dialogWidth - 48,
39
53
  '16px -apple-system, Roboto, sans-serif'
40
54
  );
41
-
42
55
  if (this.messageLines.length > 2) {
43
56
  this.dialogHeight += (this.messageLines.length - 2) * 22;
44
57
  }
45
58
  }
46
59
 
47
- /* ---------------- DRAW ---------------- */
60
+ // ─────────────────────────────────────────
61
+ // DESSIN
62
+ // ─────────────────────────────────────────
48
63
 
49
64
  draw(ctx) {
50
65
  if (!this.isVisible || this.opacity <= 0) return;
@@ -52,152 +67,133 @@ class Dialog extends Component {
52
67
  ctx.save();
53
68
  ctx.globalAlpha = this.opacity;
54
69
 
55
- // Overlay
70
+ // Overlay sombre
56
71
  ctx.fillStyle = 'rgba(0,0,0,0.5)';
57
72
  ctx.fillRect(0, 0, this.framework.width, this.framework.height);
58
73
 
59
74
  if (this.platform === 'material') {
60
- this.drawMaterial(ctx);
75
+ this._drawMaterial(ctx);
61
76
  } else {
62
- this.drawCupertino(ctx); // 👈 TON DESIGN INITIAL
77
+ this._drawCupertino(ctx);
63
78
  }
64
79
 
65
80
  ctx.restore();
66
81
  }
67
82
 
68
- /* ---------------- MATERIAL (ANDROID) ---------------- */
69
-
70
- drawMaterial(ctx) {
71
- const x = (this.framework.width - this.dialogWidth) / 2;
83
+ /** @private */
84
+ _drawMaterial(ctx) {
85
+ const x = (this.framework.width - this.dialogWidth) / 2;
72
86
  const y = (this.framework.height - this.dialogHeight) / 2;
73
87
 
74
- // Card
75
- ctx.fillStyle = '#FFF';
88
+ // Carte
89
+ ctx.fillStyle = '#FFF';
76
90
  ctx.shadowColor = 'rgba(0,0,0,0.25)';
77
- ctx.shadowBlur = 12;
78
- this.roundRect(ctx, x, y, this.dialogWidth, this.dialogHeight, 6);
91
+ ctx.shadowBlur = 12;
92
+ ctx.beginPath();
93
+ roundRect(ctx, x, y, this.dialogWidth, this.dialogHeight, 6);
79
94
  ctx.fill();
80
95
  ctx.shadowColor = 'transparent';
81
96
 
82
- // Title
83
- ctx.fillStyle = '#000';
84
- ctx.font = '500 20px Roboto, sans-serif';
85
- ctx.textAlign = 'left';
97
+ // Titre
98
+ ctx.fillStyle = '#000';
99
+ ctx.font = '500 20px Roboto, sans-serif';
100
+ ctx.textAlign = 'left';
86
101
  ctx.fillText(this.title, x + 24, y + 36);
87
102
 
88
103
  // Message
89
104
  ctx.fillStyle = '#444';
90
- ctx.font = '16px Roboto, sans-serif';
105
+ ctx.font = '16px Roboto, sans-serif';
91
106
  for (let i = 0; i < this.messageLines.length; i++) {
92
107
  ctx.fillText(this.messageLines[i], x + 24, y + 70 + i * 22);
93
108
  }
94
109
 
95
- // Buttons
110
+ // Boutons
96
111
  this.buttonRects = [];
97
- let btnX = x + this.dialogWidth - 16;
98
- const btnY = y + this.dialogHeight - 28;
99
-
100
- ctx.font = '500 14px Roboto, sans-serif';
112
+ let btnX = x + this.dialogWidth - 16;
113
+ const btnY = y + this.dialogHeight - 28;
114
+ ctx.font = '500 14px Roboto, sans-serif';
101
115
 
102
116
  for (let i = this.buttons.length - 1; i >= 0; i--) {
103
117
  const text = this.buttons[i];
104
- const w = ctx.measureText(text).width + 24;
118
+ const w = ctx.measureText(text).width + 24;
119
+ btnX -= w;
105
120
 
106
- btnX -= w;
107
-
108
- const rect = {
109
- x: btnX,
110
- y: btnY - 18,
111
- width: w,
112
- height: 36,
113
- index: i
114
- };
121
+ const rect = { x: btnX, y: btnY - 18, width: w, height: 36, index: i };
115
122
  this.buttonRects[i] = rect;
116
123
 
117
- // Ripple
124
+ // Ripple clip
118
125
  ctx.save();
119
126
  ctx.beginPath();
120
127
  ctx.rect(rect.x, rect.y, rect.width, rect.height);
121
128
  ctx.clip();
122
-
123
129
  for (const r of this.ripples) {
124
130
  if (r.index === i) {
125
- ctx.globalAlpha = r.alpha;
126
- ctx.fillStyle = 'rgba(98,0,238,0.25)';
131
+ ctx.globalAlpha = r.alpha * this.opacity;
132
+ ctx.fillStyle = 'rgba(98,0,238,0.25)';
127
133
  ctx.beginPath();
128
134
  ctx.arc(r.x, r.y, r.radius, 0, Math.PI * 2);
129
135
  ctx.fill();
130
136
  }
131
137
  }
132
138
  ctx.restore();
139
+ ctx.globalAlpha = this.opacity;
133
140
 
134
- ctx.fillStyle = '#6200EE';
135
- ctx.textAlign = 'center';
141
+ ctx.fillStyle = '#6200EE';
142
+ ctx.textAlign = 'center';
136
143
  ctx.fillText(text, btnX + w / 2, btnY);
137
-
138
144
  btnX -= 8;
139
145
  }
140
146
  }
141
147
 
142
- /* ---------------- CUPERTINO (iOS) — DESIGN ORIGINAL ---------------- */
143
-
144
- drawCupertino(ctx) {
145
- const x = (this.framework.width - this.dialogWidth) / 2;
148
+ /** @private */
149
+ _drawCupertino(ctx) {
150
+ const x = (this.framework.width - this.dialogWidth) / 2;
146
151
  const y = (this.framework.height - this.dialogHeight) / 2;
147
152
 
148
- // Dialog
149
- ctx.fillStyle = '#FFF';
153
+ // Fond
154
+ ctx.fillStyle = '#FFF';
150
155
  ctx.shadowColor = 'rgba(0,0,0,0.3)';
151
- ctx.shadowBlur = 20;
152
- this.roundRect(ctx, x, y, this.dialogWidth, this.dialogHeight, 12);
156
+ ctx.shadowBlur = 20;
157
+ ctx.beginPath();
158
+ roundRect(ctx, x, y, this.dialogWidth, this.dialogHeight, 12);
153
159
  ctx.fill();
154
160
  ctx.shadowColor = 'transparent';
155
161
 
156
- // Title
157
- ctx.fillStyle = '#000';
158
- ctx.font = '600 18px -apple-system, sans-serif';
159
- ctx.textAlign = 'center';
162
+ // Titre
163
+ ctx.fillStyle = '#000';
164
+ ctx.font = '600 18px -apple-system, sans-serif';
165
+ ctx.textAlign = 'center';
160
166
  ctx.fillText(this.title, x + this.dialogWidth / 2, y + 38);
161
167
 
162
168
  // Message
163
169
  ctx.fillStyle = '#666';
164
- ctx.font = '16px -apple-system, sans-serif';
170
+ ctx.font = '16px -apple-system, sans-serif';
165
171
  for (let i = 0; i < this.messageLines.length; i++) {
166
- ctx.fillText(
167
- this.messageLines[i],
168
- x + this.dialogWidth / 2,
169
- y + 72 + i * 22
170
- );
172
+ ctx.fillText(this.messageLines[i], x + this.dialogWidth / 2, y + 72 + i * 22);
171
173
  }
172
174
 
173
- // Divider
174
- const dividerY = y + this.dialogHeight - 54;
175
- ctx.strokeStyle = '#E5E5EA';
175
+ // Séparateur
176
+ const dividerY = y + this.dialogHeight - 54;
177
+ ctx.strokeStyle = '#E5E5EA';
178
+ ctx.lineWidth = 0.5;
176
179
  ctx.beginPath();
177
180
  ctx.moveTo(x, dividerY);
178
181
  ctx.lineTo(x + this.dialogWidth, dividerY);
179
182
  ctx.stroke();
180
183
 
181
- // Buttons
182
- this.buttonRects = [];
183
- const btnW = this.dialogWidth / this.buttons.length;
184
- const btnH = 54;
184
+ // Boutons
185
+ this.buttonRects = [];
186
+ const btnW = this.dialogWidth / this.buttons.length;
187
+ const btnH = 54;
185
188
 
186
189
  for (let i = 0; i < this.buttons.length; i++) {
187
190
  const bx = x + i * btnW;
188
191
  const by = dividerY;
189
192
 
190
- this.buttonRects.push({
191
- x: bx,
192
- y: by,
193
- width: btnW,
194
- height: btnH
195
- });
196
-
197
- ctx.fillStyle =
198
- i === this.buttons.length - 1 ? '#007AFF' : '#8E8E93';
193
+ this.buttonRects.push({ x: bx, y: by, width: btnW, height: btnH });
199
194
 
200
- ctx.font = '600 17px -apple-system, sans-serif';
195
+ ctx.fillStyle = i === this.buttons.length - 1 ? '#007AFF' : '#8E8E93';
196
+ ctx.font = '600 17px -apple-system, sans-serif';
201
197
  ctx.textAlign = 'center';
202
198
  ctx.fillText(this.buttons[i], bx + btnW / 2, by + btnH / 2);
203
199
 
@@ -211,26 +207,24 @@ class Dialog extends Component {
211
207
  }
212
208
  }
213
209
 
214
- /* ---------------- INTERACTION ---------------- */
210
+ // ─────────────────────────────────────────
211
+ // INTERACTION
212
+ // ─────────────────────────────────────────
215
213
 
216
- handlePress(x, y) {
214
+ /** @private */
215
+ _handlePress(x, y) {
217
216
  for (let i = 0; i < this.buttonRects.length; i++) {
218
217
  const r = this.buttonRects[i];
219
- if (
220
- x >= r.x && x <= r.x + r.width &&
221
- y >= r.y && y <= r.y + r.height
222
- ) {
218
+ if (x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height) {
219
+
223
220
  if (this.platform === 'material') {
224
- const max = Math.max(r.width, r.height) * 1.4;
225
221
  this.ripples.push({
226
- index: i,
227
- x,
228
- y,
222
+ index: i, x, y,
229
223
  radius: 0,
230
224
  alpha: 0.35,
231
- max
225
+ max: Math.max(r.width, r.height) * 1.4,
232
226
  });
233
- this.animateRipples();
227
+ this._animateRipples();
234
228
  }
235
229
 
236
230
  setTimeout(() => {
@@ -240,73 +234,97 @@ class Dialog extends Component {
240
234
  return;
241
235
  }
242
236
  }
243
-
237
+ // Clic en dehors = ferme
244
238
  this.hide();
245
239
  }
246
240
 
247
- animateRipples() {
248
- if (this._animating) return;
249
- this._animating = true;
241
+ /** @private */
242
+ _animateRipples() {
243
+ if (this._rippleRaf) return;
250
244
 
251
245
  const step = () => {
252
- let active = false;
246
+ if (this._destroyed) { this._rippleRaf = null; return; }
253
247
 
248
+ let active = false;
254
249
  for (const r of this.ripples) {
255
250
  r.radius += r.max * 0.12;
256
- r.alpha -= 0.04;
251
+ r.alpha -= 0.04;
257
252
  if (r.alpha > 0) active = true;
258
253
  }
254
+ this.ripples = this.ripples.filter((r) => r.alpha > 0);
259
255
 
260
- this.ripples = this.ripples.filter(r => r.alpha > 0);
261
-
262
- if (active) requestAnimationFrame(step);
263
- else this._animating = false;
256
+ if (active) {
257
+ this._rippleRaf = requestAnimationFrame(step);
258
+ } else {
259
+ this._rippleRaf = null;
260
+ }
264
261
  };
265
262
 
266
- requestAnimationFrame(step);
263
+ this._rippleRaf = requestAnimationFrame(step);
267
264
  }
268
265
 
269
- /* ---------------- SHOW / HIDE ---------------- */
266
+ // ─────────────────────────────────────────
267
+ // SHOW / HIDE
268
+ // ─────────────────────────────────────────
270
269
 
271
270
  show() {
272
271
  this.isVisible = true;
273
- this.visible = true;
274
- this.opacity = 0;
272
+ this.visible = true;
273
+ this.opacity = 0;
274
+
275
+ if (this._rafId) cancelAnimationFrame(this._rafId);
275
276
 
276
277
  const fade = () => {
278
+ if (this._destroyed) { this._rafId = null; return; }
277
279
  this.opacity += 0.1;
278
- if (this.opacity < 1) requestAnimationFrame(fade);
280
+ if (this.opacity < 1) {
281
+ this._rafId = requestAnimationFrame(fade);
282
+ } else {
283
+ this.opacity = 1;
284
+ this._rafId = null;
285
+ }
279
286
  };
280
- fade();
287
+ this._rafId = requestAnimationFrame(fade);
281
288
  }
282
289
 
283
290
  hide() {
291
+ if (this._rafId) cancelAnimationFrame(this._rafId);
292
+
284
293
  const fade = () => {
294
+ if (this._destroyed) { this._rafId = null; return; }
285
295
  this.opacity -= 0.1;
286
- if (this.opacity > 0) requestAnimationFrame(fade);
287
- else {
296
+ if (this.opacity > 0) {
297
+ this._rafId = requestAnimationFrame(fade);
298
+ } else {
299
+ this.opacity = 0;
288
300
  this.isVisible = false;
289
- this.visible = false;
301
+ this.visible = false;
302
+ this._rafId = null;
290
303
  this.framework.remove(this);
291
304
  }
292
305
  };
293
- fade();
306
+ this._rafId = requestAnimationFrame(fade);
294
307
  }
295
308
 
296
- /* ---------------- UTILS ---------------- */
309
+ // ─────────────────────────────────────────
310
+ // UTILITAIRES
311
+ // ─────────────────────────────────────────
297
312
 
298
- wrapText(text, maxWidth, font) {
313
+ /** @private */
314
+ _wrapText(text, maxWidth, font) {
315
+ if (!text) return [''];
299
316
  const ctx = this.framework.ctx;
300
- ctx.font = font;
317
+ ctx.font = font;
301
318
 
302
319
  const words = text.split(' ');
303
320
  const lines = [];
304
- let line = words[0];
321
+ let line = words[0];
305
322
 
306
323
  for (let i = 1; i < words.length; i++) {
307
- const test = line + ' ' + words[i];
308
- if (ctx.measureText(test).width < maxWidth) line = test;
309
- else {
324
+ const test = `${line} ${words[i]}`;
325
+ if (ctx.measureText(test).width < maxWidth) {
326
+ line = test;
327
+ } else {
310
328
  lines.push(line);
311
329
  line = words[i];
312
330
  }
@@ -315,23 +333,20 @@ class Dialog extends Component {
315
333
  return lines;
316
334
  }
317
335
 
318
- roundRect(ctx, x, y, w, h, r) {
319
- ctx.beginPath();
320
- ctx.moveTo(x + r, y);
321
- ctx.lineTo(x + w - r, y);
322
- ctx.quadraticCurveTo(x + w, y, x + w, y + r);
323
- ctx.lineTo(x + w, y + h - r);
324
- ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
325
- ctx.lineTo(x + r, y + h);
326
- ctx.quadraticCurveTo(x, y + h, x, y + h - r);
327
- ctx.lineTo(x, y + r);
328
- ctx.quadraticCurveTo(x, y, x + r, y);
329
- ctx.closePath();
330
- }
331
-
332
336
  isPointInside() {
333
337
  return this.isVisible;
334
338
  }
339
+
340
+ // ─────────────────────────────────────────
341
+ // DESTROY
342
+ // ─────────────────────────────────────────
343
+
344
+ destroy() {
345
+ if (this._rafId) { cancelAnimationFrame(this._rafId); this._rafId = null; }
346
+ if (this._rippleRaf){ cancelAnimationFrame(this._rippleRaf); this._rippleRaf = null; }
347
+ this.ripples = [];
348
+ super.destroy();
349
+ }
335
350
  }
336
351
 
337
- export default Dialog;
352
+ export default Dialog;