canvasframework 0.5.46 → 0.5.47

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.
@@ -107,7 +107,8 @@ class Accordion extends Component {
107
107
 
108
108
  animate();
109
109
  }
110
- addRipple() {
110
+
111
+ addRipple() {
111
112
  const ripple = {
112
113
  x: this.width / 2,
113
114
  y: this.headerHeight / 2,
@@ -13,7 +13,6 @@ export default class Banner extends Component {
13
13
  this.platform = framework.platform || 'material';
14
14
 
15
15
  this.width = options.width || framework.width || window.innerWidth;
16
- this.height = options.height || 64;
17
16
  this.x = options.x || 0;
18
17
  this.y = options.y || 0;
19
18
 
@@ -24,26 +23,25 @@ export default class Banner extends Component {
24
23
  this._lastUpdate = performance.now();
25
24
  this._colors = this._resolveColors();
26
25
 
27
- // Bounds calculées à chaque frame
28
26
  this._actionBounds = [];
29
27
  this._dismissBounds = null;
30
-
31
- // Pour indiquer qu'on gère nos propres clics
32
- this.selfManagedClicks = true;
33
28
 
34
- // Écouter les événements directement sur le canvas
35
- this._setupEventListeners();
29
+ this.selfManagedClicks = true;
30
+ this.ripples = [];
36
31
 
37
- // Ref si fourni
38
32
  if (options.ref) options.ref.current = this;
33
+
34
+ this._calculateHeight();
35
+ this._setupEventListeners();
36
+
37
+ this.bgColor = options.bgColor || null;
38
+ this.textColor = options.textColor || null;
39
+ this.buttonColor = options.buttonColor || null;
40
+ this.rippleColor = options.rippleColor || 'rgba(26,115,232,0.2)';
39
41
  }
40
42
 
41
- /* ===================== Setup ===================== */
42
43
  _setupEventListeners() {
43
- // Stocker les références pour pouvoir les retirer plus tard
44
44
  this._boundHandleClick = this._handleClick.bind(this);
45
-
46
- // Écouter les événements sur le canvas parent
47
45
  if (this.framework && this.framework.canvas) {
48
46
  this.framework.canvas.addEventListener('click', this._boundHandleClick);
49
47
  this.framework.canvas.addEventListener('touchend', this._boundHandleClick);
@@ -57,53 +55,66 @@ export default class Banner extends Component {
57
55
  }
58
56
  }
59
57
 
60
- /* ===================== Lifecycle ===================== */
61
- onMount() {
62
- this._setupEventListeners();
63
- }
58
+ onMount() { this._setupEventListeners(); }
59
+ onUnmount() { this._removeEventListeners(); }
64
60
 
65
- onUnmount() {
66
- this._removeEventListeners();
61
+ _resolveColors() {
62
+ if (this.platform === 'cupertino') {
63
+ return {
64
+ bg: this.bgColor || 'rgba(250,250,250,0.95)',
65
+ fg: this.textColor || '#000',
66
+ accent: this.buttonColor || '#007AFF',
67
+ divider: 'rgba(60,60,67,0.15)'
68
+ };
69
+ }
70
+
71
+ const map = {
72
+ info: '#E8F0FE',
73
+ success: '#E6F4EA',
74
+ warning: '#FEF7E0',
75
+ error: '#FCE8E6'
76
+ };
77
+
78
+ return {
79
+ bg: this.bgColor || map[this.type] || map.info,
80
+ fg: this.textColor || '#1F1F1F',
81
+ accent: this.buttonColor || '#1A73E8'
82
+ };
67
83
  }
68
84
 
69
- /* ===================== Colors ===================== */
70
- _resolveColors() {
71
- if (this.platform === 'cupertino') {
72
- return {
73
- bg: 'rgba(250,250,250,0.95)',
74
- fg: '#000',
75
- accent: '#007AFF',
76
- divider: 'rgba(60,60,67,0.15)'
77
- };
78
- }
85
+ _calculateHeight() {
86
+ const ctx = this.framework.ctx;
87
+ ctx.save();
88
+ ctx.font =
89
+ this.platform === 'cupertino'
90
+ ? '600 15px -apple-system, SF Pro Display'
91
+ : '400 14px Roboto, sans-serif';
79
92
 
80
- // Material v3
81
- const map = {
82
- info: '#E8F0FE',
83
- success: '#E6F4EA',
84
- warning: '#FEF7E0',
85
- error: '#FCE8E6'
86
- };
93
+ const maxWidth = this.width - 32; // padding 16px de chaque côté
94
+ const words = this.text.split(' ');
95
+ let lines = [];
96
+ let line = '';
97
+
98
+ words.forEach(word => {
99
+ const test = line + word + ' ';
100
+ if (ctx.measureText(test).width < maxWidth) {
101
+ line = test;
102
+ } else {
103
+ lines.push(line);
104
+ line = word + ' ';
105
+ }
106
+ });
87
107
 
88
- return {
89
- bg: map[this.type] || map.info,
90
- fg: '#1F1F1F',
91
- accent: '#1A73E8'
92
- };
93
- }
108
+ lines.push(line);
109
+ this._lines = lines;
94
110
 
95
- /* ===================== Show/Hide ===================== */
96
- show() {
97
- this.visible = true;
98
- this.markDirty();
99
- }
100
-
101
- hide() {
102
- this.visible = false;
103
- this.markDirty();
111
+ ctx.restore();
112
+ this.height = Math.max(64, lines.length * 20 + 16); // minimum 64px
104
113
  }
105
114
 
106
- /* ===================== Update ===================== */
115
+ show() { this.visible = true; this.markDirty(); }
116
+ hide() { this.visible = false; this.markDirty(); }
117
+
107
118
  update() {
108
119
  const now = performance.now();
109
120
  const dt = Math.min((now - this._lastUpdate) / 16.6, 3);
@@ -113,27 +124,53 @@ export default class Banner extends Component {
113
124
  this.progress = Math.max(0, Math.min(1, this.progress));
114
125
 
115
126
  if (Math.abs(target - this.progress) > 0.01) this.markDirty();
116
-
117
127
  this._lastUpdate = now;
118
128
  }
119
129
 
120
- /* ===================== Draw ===================== */
130
+ addRipple(x, y) {
131
+ if (this.platform !== 'material') return;
132
+
133
+ this.ripples.push({
134
+ x, y,
135
+ radius: 0,
136
+ maxRadius: Math.max(this.width, this.height),
137
+ opacity: 0.3
138
+ });
139
+
140
+ this.animateRipples();
141
+ }
142
+
143
+ animateRipples() {
144
+ const animate = () => {
145
+ let active = false;
146
+ for (let r of this.ripples) {
147
+ if (r.radius < r.maxRadius) {
148
+ r.radius += r.maxRadius / 15;
149
+ r.opacity -= 0.03;
150
+ active = true;
151
+ }
152
+ }
153
+ this.ripples = this.ripples.filter(r => r.opacity > 0);
154
+ if (active) requestAnimationFrame(animate);
155
+ };
156
+ animate();
157
+ }
158
+
121
159
  draw(ctx) {
122
160
  this.update();
123
161
  if (this.progress <= 0.01) return;
124
162
 
125
- const h = this.height * this.progress;
126
- const visibleHeight = h;
127
-
163
+ const visibleHeight = this.height * this.progress;
128
164
  ctx.save();
129
165
 
130
- // Background
166
+ // Shadow pour Material
131
167
  if (this.platform === 'material') {
132
- ctx.shadowColor = 'rgba(0,0,0,0.18)';
133
- ctx.shadowBlur = 8;
168
+ ctx.shadowColor = 'rgba(0,0,0,0.12)';
169
+ ctx.shadowBlur = 6;
134
170
  ctx.shadowOffsetY = 2;
135
171
  }
136
172
 
173
+ // Background
137
174
  ctx.fillStyle = this._colors.bg;
138
175
  ctx.fillRect(this.x, this.y, this.width, visibleHeight);
139
176
  ctx.shadowColor = 'transparent';
@@ -147,20 +184,39 @@ export default class Banner extends Component {
147
184
  ctx.stroke();
148
185
  }
149
186
 
187
+ // Ripple Material
188
+ if (this.platform === 'material') {
189
+ ctx.save();
190
+ ctx.beginPath();
191
+ ctx.rect(this.x, this.y, this.width, visibleHeight);
192
+ ctx.clip();
193
+
194
+ this.ripples.forEach(r => {
195
+ ctx.globalAlpha = r.opacity;
196
+ ctx.fillStyle = this.rippleColor;
197
+ ctx.beginPath();
198
+ ctx.arc(this.x + r.x, this.y + r.y, r.radius, 0, Math.PI * 2);
199
+ ctx.fill();
200
+ });
201
+
202
+ ctx.restore();
203
+ ctx.globalAlpha = 1;
204
+ }
205
+
150
206
  // Text
151
207
  ctx.fillStyle = this._colors.fg;
152
208
  ctx.font =
153
209
  this.platform === 'cupertino'
154
- ? '400 15px -apple-system'
210
+ ? '600 15px -apple-system, SF Pro Display'
155
211
  : '400 14px Roboto, sans-serif';
156
- ctx.textBaseline = 'middle';
157
212
  ctx.textAlign = 'left';
213
+ ctx.textBaseline = 'middle';
214
+
158
215
  ctx.fillText(this.text, this.x + 16, this.y + visibleHeight / 2);
159
216
 
160
- // Actions - calculer et stocker les bounds
217
+ // Actions
161
218
  this._actionBounds = [];
162
219
  let x = this.width - 16;
163
-
164
220
  for (let i = this.actions.length - 1; i >= 0; i--) {
165
221
  const action = this.actions[i];
166
222
  const textWidth = ctx.measureText(action.label).width + 20;
@@ -171,15 +227,9 @@ export default class Banner extends Component {
171
227
  ctx.textBaseline = 'middle';
172
228
  ctx.fillText(action.label, this.x + x + textWidth / 2, this.y + visibleHeight / 2);
173
229
 
174
- // Stocker la hitbox (en coordonnées écran, pas canvas)
175
230
  this._actionBounds.push({
176
231
  action: action,
177
- bounds: {
178
- x: this.x + x,
179
- y: this.y + (visibleHeight - 44) / 2,
180
- w: textWidth,
181
- h: 44
182
- }
232
+ bounds: { x: this.x + x, y: this.y + (visibleHeight - 44)/2, w: textWidth, h: 44 }
183
233
  });
184
234
 
185
235
  x -= 12;
@@ -191,152 +241,59 @@ export default class Banner extends Component {
191
241
  const cx = this.width - 28;
192
242
  const cy = this.y + visibleHeight / 2;
193
243
 
194
- ctx.fillStyle =
195
- this.platform === 'cupertino'
196
- ? 'rgba(60,60,67,0.6)'
197
- : this._colors.fg;
198
-
199
- ctx.font =
200
- this.platform === 'cupertino'
201
- ? '600 16px -apple-system'
202
- : '500 16px Roboto';
244
+ ctx.fillStyle = this.platform === 'cupertino' ? 'rgba(60,60,67,0.6)' : this._colors.fg;
245
+ ctx.font = this.platform === 'cupertino' ? '600 16px -apple-system' : '500 16px Roboto';
203
246
  ctx.textAlign = 'center';
204
247
  ctx.textBaseline = 'middle';
205
248
  ctx.fillText('×', cx, cy);
206
249
 
207
- this._dismissBounds = {
208
- x: cx - hitSize / 2,
209
- y: cy - hitSize / 2,
210
- w: hitSize,
211
- h: hitSize
212
- };
250
+ this._dismissBounds = { x: cx - hitSize/2, y: cy - hitSize/2, w: hitSize, h: hitSize };
213
251
  } else {
214
252
  this._dismissBounds = null;
215
253
  }
216
254
 
217
255
  ctx.restore();
218
-
219
- // DEBUG: Dessiner les hitboxes
220
- if (this.framework && this.framework.debbug) {
221
- this._drawDebugHitboxes(ctx);
222
- }
223
- }
224
-
225
- /* ===================== Debug ===================== */
226
- _drawDebugHitboxes(ctx) {
227
- ctx.save();
228
- ctx.strokeStyle = 'red';
229
- ctx.lineWidth = 1;
230
- ctx.fillStyle = 'rgba(255, 0, 0, 0.1)';
231
-
232
- // Dessiner la hitbox principale du banner
233
- const h = this.height * this.progress;
234
- ctx.strokeRect(this.x, this.y, this.width, h);
235
-
236
- // Dessiner les hitboxes des actions
237
- if (this._actionBounds && this._actionBounds.length > 0) {
238
- for (const item of this._actionBounds) {
239
- const b = item.bounds;
240
- ctx.fillRect(b.x, b.y, b.w, b.h);
241
- ctx.strokeRect(b.x, b.y, b.w, b.h);
242
-
243
- // Texte de debug
244
- ctx.fillStyle = 'red';
245
- ctx.font = '10px monospace';
246
- ctx.fillText(item.action.label, b.x + 5, b.y + 12);
247
- }
248
- }
249
-
250
- // Dessiner la hitbox du dismiss button
251
- if (this._dismissBounds) {
252
- const b = this._dismissBounds;
253
- ctx.fillRect(b.x, b.y, b.w, b.h);
254
- ctx.strokeRect(b.x, b.y, b.w, b.h);
255
- ctx.fillText('X', b.x + 5, b.y + 12);
256
- }
257
-
258
- ctx.restore();
259
256
  }
260
257
 
261
- /* ===================== Click Handling ===================== */
262
258
  _handleClick(event) {
263
259
  if (this.progress < 0.95) return;
264
-
265
- // Obtenir les coordonnées du clic/touch
266
- let clientX, clientY;
267
-
260
+
261
+ let clientX = event.clientX, clientY = event.clientY;
268
262
  if (event.type === 'touchend') {
269
263
  const touch = event.changedTouches[0];
270
264
  clientX = touch.clientX;
271
265
  clientY = touch.clientY;
272
- } else {
273
- clientX = event.clientX;
274
- clientY = event.clientY;
275
266
  }
276
-
277
- // Convertir en coordonnées canvas SIMPLIFIÉ
267
+
278
268
  const canvasRect = this.framework.canvas.getBoundingClientRect();
279
-
280
- // Coordonnées relatives au canvas (en pixels CSS, pas en pixels canvas)
281
269
  const x = clientX - canvasRect.left;
282
270
  const y = clientY - canvasRect.top;
283
-
284
- console.log('Click converted:', {
285
- clientX, clientY,
286
- canvasLeft: canvasRect.left,
287
- canvasTop: canvasRect.top,
288
- x, y,
289
- bannerX: this.x,
290
- bannerY: this.y,
291
- bannerWidth: this.width,
292
- bannerHeight: this.height * this.progress
293
- });
294
-
295
- // Vérifier si on clique sur le banner (en coordonnées CSS)
296
- const bannerBottom = this.y + (this.height * this.progress);
297
- if (x < this.x || x > this.x + this.width || y < this.y || y > bannerBottom) {
298
- console.log('Click outside banner');
299
- return;
300
- }
301
-
302
- console.log('Click INSIDE banner!');
303
-
304
- // Empêcher la propagation
271
+
272
+ // Ripple effect sur banner
273
+ if (this.platform === 'material') this.addRipple(x - this.x, y - this.y);
274
+
275
+ if (x < this.x || x > this.x + this.width || y < this.y || y > this.y + this.height) return;
276
+
305
277
  event.stopPropagation();
306
-
307
- // 1️⃣ Dismiss button
278
+
279
+ // Dismiss
308
280
  if (this.dismissible && this._dismissBounds) {
309
281
  const b = this._dismissBounds;
310
- console.log('Checking dismiss bounds:', b, 'click:', {x, y});
311
- if (x >= b.x && x <= b.x + b.w &&
312
- y >= b.y && y <= b.y + b.h) {
313
- console.log('Dismiss clicked!');
282
+ if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
314
283
  this.hide();
315
284
  return;
316
285
  }
317
286
  }
318
-
319
- // 2️⃣ Actions
320
- if (this._actionBounds && this._actionBounds.length > 0) {
321
- console.log('Checking', this._actionBounds.length, 'action bounds');
287
+
288
+ // Actions
289
+ if (this._actionBounds.length) {
322
290
  for (const item of this._actionBounds) {
323
291
  const b = item.bounds;
324
- console.log('Checking action:', item.action.label, 'bounds:', b);
325
- if (x >= b.x && x <= b.x + b.w &&
326
- y >= b.y && y <= b.y + b.h) {
327
- console.log('Action clicked:', item.action.label);
292
+ if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
328
293
  item.action.onClick?.();
329
294
  return;
330
295
  }
331
296
  }
332
297
  }
333
-
334
- console.log('Click on banner but not on any button');
335
- }
336
-
337
- /* ===================== Resize ===================== */
338
- _resize(width) {
339
- this.width = width;
340
- this.markDirty();
341
298
  }
342
- }
299
+ }