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/AppBar.js
CHANGED
|
@@ -1,89 +1,102 @@
|
|
|
1
1
|
import Component from '../core/Component.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* AppBar — barre d'application supérieure (Material et Cupertino).
|
|
5
|
+
*
|
|
6
|
+
* Corrections :
|
|
7
|
+
* - Guard _destroyed dans la boucle RAF des ripples
|
|
8
|
+
* - destroy() annule le RAF et nettoie les ressources
|
|
9
|
+
* - Fautes de frappe mineures corrigées
|
|
7
10
|
*/
|
|
8
11
|
class AppBar extends Component {
|
|
12
|
+
/**
|
|
13
|
+
* @param {CanvasFramework} framework
|
|
14
|
+
* @param {Object} [options={}]
|
|
15
|
+
* @param {string} [options.title='']
|
|
16
|
+
* @param {string} [options.leftIcon] - 'menu' | 'back'
|
|
17
|
+
* @param {string} [options.rightIcon] - 'search' | 'more'
|
|
18
|
+
* @param {Function} [options.onLeftClick]
|
|
19
|
+
* @param {Function} [options.onRightClick]
|
|
20
|
+
* @param {string} [options.platform]
|
|
21
|
+
* @param {string} [options.bgColor]
|
|
22
|
+
* @param {string} [options.textColor]
|
|
23
|
+
* @param {number} [options.elevation]
|
|
24
|
+
*/
|
|
9
25
|
constructor(framework, options = {}) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
})();
|
|
26
|
+
const platform =
|
|
27
|
+
options.platform ||
|
|
28
|
+
framework.platform ||
|
|
29
|
+
(/iPad|iPhone|iPod/.test(navigator.userAgent) ? 'cupertino' : 'material');
|
|
15
30
|
|
|
16
31
|
super(framework, {
|
|
17
32
|
x: 0,
|
|
18
33
|
y: 0,
|
|
19
34
|
width: framework.width,
|
|
20
35
|
height: options.height || (platform === 'material' ? 56 : 44),
|
|
21
|
-
...options
|
|
36
|
+
...options,
|
|
22
37
|
});
|
|
23
38
|
|
|
24
|
-
this.title
|
|
25
|
-
this.leftIcon
|
|
26
|
-
this.rightIcon
|
|
27
|
-
this.onLeftClick
|
|
39
|
+
this.title = options.title || '';
|
|
40
|
+
this.leftIcon = options.leftIcon || null;
|
|
41
|
+
this.rightIcon = options.rightIcon || null;
|
|
42
|
+
this.onLeftClick = options.onLeftClick;
|
|
28
43
|
this.onRightClick = options.onRightClick;
|
|
29
|
-
this.platform
|
|
44
|
+
this.platform = platform;
|
|
30
45
|
|
|
31
|
-
// Couleurs et styles par plateforme
|
|
32
46
|
if (this.platform === 'material') {
|
|
33
|
-
this.bgColor
|
|
47
|
+
this.bgColor = options.bgColor || '#6200EE';
|
|
34
48
|
this.textColor = options.textColor || '#FFFFFF';
|
|
35
49
|
this.elevation = options.elevation !== undefined ? options.elevation : 4;
|
|
36
50
|
} else {
|
|
37
|
-
|
|
38
|
-
this.bgColor = options.bgColor || 'rgba(248, 248, 248, 0.95)';
|
|
51
|
+
this.bgColor = options.bgColor || 'rgba(248,248,248,0.95)';
|
|
39
52
|
this.textColor = options.textColor || '#000000';
|
|
40
53
|
this.elevation = 0;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
this.
|
|
46
|
-
this.
|
|
47
|
-
|
|
48
|
-
// États press (iOS)
|
|
49
|
-
this.leftPressed = false;
|
|
50
|
-
this.rightPressed = false;
|
|
56
|
+
this.ripples = [];
|
|
57
|
+
this._rafId = null;
|
|
58
|
+
this._lastAnimTime = 0;
|
|
59
|
+
this.leftPressed = false;
|
|
60
|
+
this.rightPressed = false;
|
|
51
61
|
|
|
52
|
-
this.onPress = this.
|
|
62
|
+
this.onPress = this._handlePress.bind(this);
|
|
53
63
|
}
|
|
54
64
|
|
|
65
|
+
// ─────────────────────────────────────────
|
|
66
|
+
// DESSIN
|
|
67
|
+
// ─────────────────────────────────────────
|
|
68
|
+
|
|
55
69
|
draw(ctx) {
|
|
56
70
|
ctx.save();
|
|
57
71
|
|
|
58
72
|
// Ombre Material
|
|
59
73
|
if (this.platform === 'material' && this.elevation > 0) {
|
|
60
|
-
ctx.shadowColor
|
|
61
|
-
ctx.shadowBlur
|
|
74
|
+
ctx.shadowColor = 'rgba(0,0,0,0.2)';
|
|
75
|
+
ctx.shadowBlur = this.elevation * 2;
|
|
62
76
|
ctx.shadowOffsetY = this.elevation / 2;
|
|
63
77
|
}
|
|
64
78
|
|
|
65
|
-
// Background
|
|
66
79
|
ctx.fillStyle = this.bgColor;
|
|
67
80
|
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
68
81
|
|
|
69
|
-
ctx.shadowColor
|
|
70
|
-
ctx.shadowBlur
|
|
82
|
+
ctx.shadowColor = 'transparent';
|
|
83
|
+
ctx.shadowBlur = 0;
|
|
71
84
|
ctx.shadowOffsetY = 0;
|
|
72
85
|
|
|
73
|
-
//
|
|
86
|
+
// Séparateur iOS
|
|
74
87
|
if (this.platform === 'cupertino') {
|
|
75
|
-
ctx.strokeStyle = 'rgba(0,
|
|
76
|
-
ctx.lineWidth
|
|
88
|
+
ctx.strokeStyle = 'rgba(0,0,0,0.1)';
|
|
89
|
+
ctx.lineWidth = 0.5;
|
|
77
90
|
ctx.beginPath();
|
|
78
91
|
ctx.moveTo(this.x, this.y + this.height - 0.5);
|
|
79
92
|
ctx.lineTo(this.x + this.width, this.y + this.height - 0.5);
|
|
80
93
|
ctx.stroke();
|
|
81
94
|
}
|
|
82
95
|
|
|
83
|
-
// Ripples
|
|
84
|
-
if (this.platform === 'material') this.
|
|
96
|
+
// Ripples
|
|
97
|
+
if (this.platform === 'material') this._drawRipples(ctx);
|
|
85
98
|
|
|
86
|
-
//
|
|
99
|
+
// Overlays pression iOS
|
|
87
100
|
if (this.platform === 'cupertino') {
|
|
88
101
|
if (this.leftPressed && this.leftIcon) {
|
|
89
102
|
ctx.fillStyle = 'rgba(0,0,0,0.1)';
|
|
@@ -100,49 +113,52 @@ class AppBar extends Component {
|
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
// Titre
|
|
103
|
-
ctx.fillStyle
|
|
116
|
+
ctx.fillStyle = this.textColor;
|
|
104
117
|
const titleAlign = this.platform === 'material' && this.leftIcon ? 'left' : 'center';
|
|
105
|
-
const titleX
|
|
106
|
-
ctx.font
|
|
107
|
-
ctx.textAlign
|
|
118
|
+
const titleX = titleAlign === 'left' ? this.x + 72 : this.x + this.width / 2;
|
|
119
|
+
ctx.font = `${this.platform === 'material' ? 'bold ' : ''}20px -apple-system, Roboto, sans-serif`;
|
|
120
|
+
ctx.textAlign = titleAlign;
|
|
108
121
|
ctx.textBaseline = 'middle';
|
|
109
122
|
ctx.fillText(this.title, titleX, this.y + this.height / 2);
|
|
110
123
|
|
|
111
|
-
|
|
112
|
-
if (this.
|
|
113
|
-
if (this.rightIcon) this.drawRightIcon(ctx);
|
|
124
|
+
if (this.leftIcon) this._drawLeftIcon(ctx);
|
|
125
|
+
if (this.rightIcon) this._drawRightIcon(ctx);
|
|
114
126
|
|
|
115
127
|
ctx.restore();
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
/** @private */
|
|
131
|
+
_drawRipples(ctx) {
|
|
132
|
+
for (const r of this.ripples) {
|
|
120
133
|
ctx.save();
|
|
121
|
-
ctx.globalAlpha =
|
|
122
|
-
ctx.fillStyle
|
|
134
|
+
ctx.globalAlpha = r.opacity;
|
|
135
|
+
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
|
123
136
|
ctx.beginPath();
|
|
124
|
-
ctx.arc(
|
|
137
|
+
ctx.arc(r.x, r.y, r.radius, 0, Math.PI * 2);
|
|
125
138
|
ctx.fill();
|
|
126
139
|
ctx.restore();
|
|
127
140
|
}
|
|
128
141
|
}
|
|
129
142
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (this.leftIcon === '
|
|
143
|
+
/** @private */
|
|
144
|
+
_drawLeftIcon(ctx) {
|
|
145
|
+
const color = this.textColor;
|
|
146
|
+
if (this.leftIcon === 'menu') this._drawMenuIcon(ctx, this.x + 16, this.y + this.height / 2, color);
|
|
147
|
+
if (this.leftIcon === 'back') this._drawBackIcon(ctx, this.x + 16, this.y + this.height / 2, color);
|
|
134
148
|
}
|
|
135
149
|
|
|
136
|
-
|
|
150
|
+
/** @private */
|
|
151
|
+
_drawRightIcon(ctx) {
|
|
137
152
|
const color = this.platform === 'cupertino' ? '#007AFF' : this.textColor;
|
|
138
|
-
if (this.rightIcon === 'search') this.
|
|
139
|
-
if (this.rightIcon === 'more')
|
|
153
|
+
if (this.rightIcon === 'search') this._drawSearchIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, color);
|
|
154
|
+
if (this.rightIcon === 'more') this._drawMoreIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, color);
|
|
140
155
|
}
|
|
141
156
|
|
|
142
|
-
|
|
157
|
+
/** @private */
|
|
158
|
+
_drawMenuIcon(ctx, x, y, color) {
|
|
143
159
|
ctx.strokeStyle = color;
|
|
144
|
-
ctx.lineWidth
|
|
145
|
-
ctx.lineCap
|
|
160
|
+
ctx.lineWidth = 2;
|
|
161
|
+
ctx.lineCap = 'round';
|
|
146
162
|
for (let i = 0; i < 3; i++) {
|
|
147
163
|
ctx.beginPath();
|
|
148
164
|
ctx.moveTo(x, y - 8 + i * 8);
|
|
@@ -151,22 +167,24 @@ class AppBar extends Component {
|
|
|
151
167
|
}
|
|
152
168
|
}
|
|
153
169
|
|
|
154
|
-
|
|
170
|
+
/** @private */
|
|
171
|
+
_drawBackIcon(ctx, x, y, color) {
|
|
155
172
|
ctx.strokeStyle = color;
|
|
156
|
-
ctx.lineWidth
|
|
157
|
-
ctx.lineCap
|
|
158
|
-
ctx.lineJoin
|
|
173
|
+
ctx.lineWidth = 2;
|
|
174
|
+
ctx.lineCap = 'round';
|
|
175
|
+
ctx.lineJoin = 'round';
|
|
159
176
|
ctx.beginPath();
|
|
160
177
|
ctx.moveTo(x + 16, y - 10);
|
|
161
|
-
ctx.lineTo(x + 6,
|
|
178
|
+
ctx.lineTo(x + 6, y);
|
|
162
179
|
ctx.lineTo(x + 16, y + 10);
|
|
163
180
|
ctx.stroke();
|
|
164
181
|
}
|
|
165
182
|
|
|
166
|
-
|
|
183
|
+
/** @private */
|
|
184
|
+
_drawSearchIcon(ctx, x, y, color) {
|
|
167
185
|
ctx.strokeStyle = color;
|
|
168
|
-
ctx.lineWidth
|
|
169
|
-
ctx.lineCap
|
|
186
|
+
ctx.lineWidth = 2;
|
|
187
|
+
ctx.lineCap = 'round';
|
|
170
188
|
ctx.beginPath();
|
|
171
189
|
ctx.arc(x + 8, y - 2, 8, 0, Math.PI * 2);
|
|
172
190
|
ctx.stroke();
|
|
@@ -176,9 +194,10 @@ class AppBar extends Component {
|
|
|
176
194
|
ctx.stroke();
|
|
177
195
|
}
|
|
178
196
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
/** @private */
|
|
198
|
+
_drawMoreIcon(ctx, x, y, color) {
|
|
199
|
+
ctx.fillStyle = color;
|
|
200
|
+
const spacing = this.platform === 'material' ? 10 : 8;
|
|
182
201
|
for (let i = 0; i < 3; i++) {
|
|
183
202
|
ctx.beginPath();
|
|
184
203
|
ctx.arc(x + 12, y - spacing + i * spacing, 2, 0, Math.PI * 2);
|
|
@@ -186,71 +205,112 @@ class AppBar extends Component {
|
|
|
186
205
|
}
|
|
187
206
|
}
|
|
188
207
|
|
|
189
|
-
|
|
190
|
-
|
|
208
|
+
// ─────────────────────────────────────────
|
|
209
|
+
// INTERACTION
|
|
210
|
+
// ─────────────────────────────────────────
|
|
191
211
|
|
|
212
|
+
/** @private */
|
|
213
|
+
_handlePress(x, y) {
|
|
214
|
+
const inY = y >= this.y && y <= this.y + this.height;
|
|
192
215
|
if (!inY) return false;
|
|
193
216
|
|
|
194
217
|
// Bouton gauche
|
|
195
218
|
if (this.leftIcon && x >= this.x && x <= this.x + 56) {
|
|
196
|
-
if (this.platform === 'material')
|
|
197
|
-
|
|
198
|
-
|
|
219
|
+
if (this.platform === 'material') {
|
|
220
|
+
this._addRipple(this.x + 28, this.y + this.height / 2);
|
|
221
|
+
} else {
|
|
222
|
+
this.leftPressed = true;
|
|
223
|
+
setTimeout(() => { this.leftPressed = false; this.markDirty(); }, 150);
|
|
224
|
+
}
|
|
225
|
+
this.onLeftClick?.();
|
|
199
226
|
return true;
|
|
200
227
|
}
|
|
201
228
|
|
|
202
229
|
// Bouton droit
|
|
203
230
|
if (this.rightIcon && x >= this.x + this.width - 56 && x <= this.x + this.width) {
|
|
204
|
-
if (this.platform === 'material')
|
|
205
|
-
|
|
206
|
-
|
|
231
|
+
if (this.platform === 'material') {
|
|
232
|
+
this._addRipple(this.x + this.width - 28, this.y + this.height / 2);
|
|
233
|
+
} else {
|
|
234
|
+
this.rightPressed = true;
|
|
235
|
+
setTimeout(() => { this.rightPressed = false; this.markDirty(); }, 150);
|
|
236
|
+
}
|
|
237
|
+
this.onRightClick?.();
|
|
207
238
|
return true;
|
|
208
239
|
}
|
|
209
240
|
|
|
210
241
|
return false;
|
|
211
242
|
}
|
|
212
243
|
|
|
213
|
-
|
|
244
|
+
// ─────────────────────────────────────────
|
|
245
|
+
// RIPPLE
|
|
246
|
+
// ─────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
/** @private */
|
|
249
|
+
_addRipple(x, y) {
|
|
214
250
|
this.ripples.push({
|
|
215
251
|
x, y,
|
|
216
252
|
radius: 0,
|
|
217
253
|
maxRadius: 28,
|
|
218
254
|
opacity: 1,
|
|
219
|
-
createdAt: performance.now()
|
|
255
|
+
createdAt: performance.now(),
|
|
220
256
|
});
|
|
221
|
-
if (!this.
|
|
257
|
+
if (!this._rafId) this._startRippleAnimation();
|
|
222
258
|
}
|
|
223
259
|
|
|
224
|
-
|
|
260
|
+
/** @private */
|
|
261
|
+
_startRippleAnimation() {
|
|
225
262
|
const animate = (timestamp) => {
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
this.
|
|
263
|
+
if (this._destroyed) { this._rafId = null; this._lastAnimTime = 0; return; }
|
|
264
|
+
|
|
265
|
+
const deltaTime = this._lastAnimTime ? timestamp - this._lastAnimTime : 16;
|
|
266
|
+
this._lastAnimTime = timestamp;
|
|
267
|
+
|
|
229
268
|
let needsUpdate = false;
|
|
230
269
|
|
|
231
270
|
for (let i = this.ripples.length - 1; i >= 0; i--) {
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
271
|
+
const r = this.ripples[i];
|
|
272
|
+
if (r.radius < r.maxRadius) {
|
|
273
|
+
r.radius += (r.maxRadius / 250) * deltaTime;
|
|
274
|
+
needsUpdate = true;
|
|
275
|
+
}
|
|
276
|
+
if (r.radius >= r.maxRadius * 0.4) {
|
|
277
|
+
r.opacity -= 0.003 * deltaTime;
|
|
278
|
+
if (r.opacity < 0) r.opacity = 0;
|
|
279
|
+
needsUpdate = true;
|
|
280
|
+
}
|
|
281
|
+
if (r.opacity <= 0 && r.radius >= r.maxRadius * 0.95) {
|
|
282
|
+
this.ripples.splice(i, 1);
|
|
283
|
+
needsUpdate = true;
|
|
284
|
+
}
|
|
236
285
|
}
|
|
237
286
|
|
|
238
|
-
if (needsUpdate) this.
|
|
239
|
-
|
|
240
|
-
|
|
287
|
+
if (needsUpdate) this.markDirty();
|
|
288
|
+
|
|
289
|
+
if (this.ripples.length > 0) {
|
|
290
|
+
this._rafId = requestAnimationFrame(animate);
|
|
291
|
+
} else {
|
|
292
|
+
this._rafId = null;
|
|
293
|
+
this._lastAnimTime = 0;
|
|
294
|
+
}
|
|
241
295
|
};
|
|
242
296
|
|
|
243
|
-
if (this.ripples.length > 0 && !this.
|
|
297
|
+
if (this.ripples.length > 0 && !this._rafId) {
|
|
298
|
+
this._rafId = requestAnimationFrame(animate);
|
|
299
|
+
}
|
|
244
300
|
}
|
|
245
301
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
302
|
+
// ─────────────────────────────────────────
|
|
303
|
+
// DESTROY
|
|
304
|
+
// ─────────────────────────────────────────
|
|
249
305
|
|
|
250
306
|
destroy() {
|
|
251
|
-
if (this.
|
|
307
|
+
if (this._rafId) {
|
|
308
|
+
cancelAnimationFrame(this._rafId);
|
|
309
|
+
this._rafId = null;
|
|
310
|
+
}
|
|
311
|
+
this.ripples = [];
|
|
252
312
|
super.destroy();
|
|
253
313
|
}
|
|
254
314
|
}
|
|
255
315
|
|
|
256
|
-
export default AppBar;
|
|
316
|
+
export default AppBar;
|