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/Dialog.js
CHANGED
|
@@ -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
|
|
17
|
-
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
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
|
|
36
|
+
this.dialogWidth = Math.min(320, framework.width - 40);
|
|
24
37
|
this.dialogHeight = 160;
|
|
25
|
-
this.opacity
|
|
26
|
-
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
38
|
+
this.opacity = 0;
|
|
39
|
+
this.isVisible = false;
|
|
40
|
+
this.buttonRects = [];
|
|
41
|
+
this._rafId = null;
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
this.ripples
|
|
32
|
-
this.
|
|
43
|
+
// Ripple (Material uniquement)
|
|
44
|
+
this.ripples = [];
|
|
45
|
+
this._rippleRaf = null;
|
|
33
46
|
|
|
34
|
-
this.onPress = this.
|
|
47
|
+
this.onPress = this._handlePress.bind(this);
|
|
35
48
|
|
|
36
|
-
|
|
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
|
-
|
|
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.
|
|
75
|
+
this._drawMaterial(ctx);
|
|
61
76
|
} else {
|
|
62
|
-
this.
|
|
77
|
+
this._drawCupertino(ctx);
|
|
63
78
|
}
|
|
64
79
|
|
|
65
80
|
ctx.restore();
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
//
|
|
75
|
-
ctx.fillStyle
|
|
88
|
+
// Carte
|
|
89
|
+
ctx.fillStyle = '#FFF';
|
|
76
90
|
ctx.shadowColor = 'rgba(0,0,0,0.25)';
|
|
77
|
-
ctx.shadowBlur
|
|
78
|
-
|
|
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
|
-
//
|
|
83
|
-
ctx.fillStyle
|
|
84
|
-
ctx.font
|
|
85
|
-
ctx.textAlign
|
|
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
|
|
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
|
-
//
|
|
110
|
+
// Boutons
|
|
96
111
|
this.buttonRects = [];
|
|
97
|
-
let btnX
|
|
98
|
-
const btnY
|
|
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
|
|
118
|
+
const w = ctx.measureText(text).width + 24;
|
|
119
|
+
btnX -= w;
|
|
105
120
|
|
|
106
|
-
btnX
|
|
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
|
|
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
|
|
135
|
-
ctx.textAlign
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
//
|
|
149
|
-
ctx.fillStyle
|
|
153
|
+
// Fond
|
|
154
|
+
ctx.fillStyle = '#FFF';
|
|
150
155
|
ctx.shadowColor = 'rgba(0,0,0,0.3)';
|
|
151
|
-
ctx.shadowBlur
|
|
152
|
-
|
|
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
|
-
//
|
|
157
|
-
ctx.fillStyle
|
|
158
|
-
ctx.font
|
|
159
|
-
ctx.textAlign
|
|
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
|
|
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
|
-
//
|
|
174
|
-
const dividerY
|
|
175
|
-
ctx.strokeStyle
|
|
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
|
-
//
|
|
182
|
-
this.buttonRects
|
|
183
|
-
const btnW
|
|
184
|
-
const btnH
|
|
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.
|
|
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
|
-
|
|
210
|
+
// ─────────────────────────────────────────
|
|
211
|
+
// INTERACTION
|
|
212
|
+
// ─────────────────────────────────────────
|
|
215
213
|
|
|
216
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
this.
|
|
241
|
+
/** @private */
|
|
242
|
+
_animateRipples() {
|
|
243
|
+
if (this._rippleRaf) return;
|
|
250
244
|
|
|
251
245
|
const step = () => {
|
|
252
|
-
|
|
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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
266
|
+
// ─────────────────────────────────────────
|
|
267
|
+
// SHOW / HIDE
|
|
268
|
+
// ─────────────────────────────────────────
|
|
270
269
|
|
|
271
270
|
show() {
|
|
272
271
|
this.isVisible = true;
|
|
273
|
-
this.visible
|
|
274
|
-
this.opacity
|
|
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)
|
|
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)
|
|
287
|
-
|
|
296
|
+
if (this.opacity > 0) {
|
|
297
|
+
this._rafId = requestAnimationFrame(fade);
|
|
298
|
+
} else {
|
|
299
|
+
this.opacity = 0;
|
|
288
300
|
this.isVisible = false;
|
|
289
|
-
this.visible
|
|
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
|
-
|
|
309
|
+
// ─────────────────────────────────────────
|
|
310
|
+
// UTILITAIRES
|
|
311
|
+
// ─────────────────────────────────────────
|
|
297
312
|
|
|
298
|
-
|
|
313
|
+
/** @private */
|
|
314
|
+
_wrapText(text, maxWidth, font) {
|
|
315
|
+
if (!text) return [''];
|
|
299
316
|
const ctx = this.framework.ctx;
|
|
300
|
-
ctx.font
|
|
317
|
+
ctx.font = font;
|
|
301
318
|
|
|
302
319
|
const words = text.split(' ');
|
|
303
320
|
const lines = [];
|
|
304
|
-
let line
|
|
321
|
+
let line = words[0];
|
|
305
322
|
|
|
306
323
|
for (let i = 1; i < words.length; i++) {
|
|
307
|
-
const test = line
|
|
308
|
-
if (ctx.measureText(test).width < maxWidth)
|
|
309
|
-
|
|
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;
|