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.
@@ -227,11 +227,10 @@ class WorkerPool {
227
227
  result = { state };
228
228
  break;
229
229
 
230
- case 'EXECUTE':
231
- const fn = new Function('state', 'args', payload.fnString);
232
- result = await fn(state, payload.args);
233
- break;
234
-
230
+ // NOTE SECURITE : le type EXECUTE (new Function / eval) a ete supprime.
231
+ // Il permettait l'injection de code arbitraire depuis le thread principal.
232
+ // Utiliser le type COMPUTE pour transmettre des donnees, ou ajouter
233
+ // des handlers specifiques dans ce switch.
235
234
  case 'COMPUTE':
236
235
  result = payload.data;
237
236
  break;
@@ -301,19 +300,40 @@ class WorkerPool {
301
300
  this._processQueue();
302
301
  }
303
302
 
304
- execute(type, payload) {
303
+ execute(type, payload, timeoutMs = 10000) {
305
304
  return new Promise((resolve, reject) => {
306
305
  const taskId = ++this.taskIdCounter;
307
-
306
+ let timer = null;
307
+
308
+ // Timeout de tâche : évite qu'un worker gelé bloque indéfiniment
309
+ if (timeoutMs > 0) {
310
+ timer = setTimeout(() => {
311
+ if (this.pendingTasks.has(taskId)) {
312
+ this.pendingTasks.delete(taskId);
313
+ reject(new Error(`Worker task ${taskId} timed out after ${timeoutMs}ms`));
314
+ } else {
315
+ // Tâche encore dans la queue
316
+ const idx = this.taskQueue.findIndex(t => t.taskId === taskId);
317
+ if (idx !== -1) {
318
+ this.taskQueue.splice(idx, 1);
319
+ reject(new Error(`Worker task ${taskId} expired in queue`));
320
+ }
321
+ }
322
+ }, timeoutMs);
323
+ }
324
+
325
+ const wrappedResolve = (v) => { if (timer) clearTimeout(timer); resolve(v); };
326
+ const wrappedReject = (e) => { if (timer) clearTimeout(timer); reject(e); };
327
+
308
328
  this.taskQueue.push({
309
329
  taskId,
310
330
  type,
311
331
  payload,
312
- resolve,
313
- reject,
332
+ resolve: wrappedResolve,
333
+ reject: wrappedReject,
314
334
  timestamp: Date.now()
315
335
  });
316
-
336
+
317
337
  this._processQueue();
318
338
  });
319
339
  }
@@ -352,7 +372,30 @@ class WorkerPool {
352
372
  };
353
373
  }
354
374
 
375
+ /**
376
+ * Annule une tâche en attente dans la queue (avant qu'elle soit assignée).
377
+ * Les tâches déjà en cours d'exécution dans un worker ne peuvent pas être annulées.
378
+ * @param {number} taskId - ID retourné par execute()
379
+ * @returns {boolean} true si la tâche était dans la queue et a été retirée
380
+ */
381
+ cancel(taskId) {
382
+ const idx = this.taskQueue.findIndex(t => t.taskId === taskId);
383
+ if (idx !== -1) {
384
+ const [task] = this.taskQueue.splice(idx, 1);
385
+ task.reject(new Error(`Task ${taskId} cancelled`));
386
+ return true;
387
+ }
388
+ return false;
389
+ }
390
+
355
391
  terminateAll() {
392
+ // Rejeter toutes les tâches en attente avant de terminer
393
+ for (const task of this.taskQueue) {
394
+ task.reject(new Error('WorkerPool terminated'));
395
+ }
396
+ for (const task of this.pendingTasks.values()) {
397
+ task.reject(new Error('WorkerPool terminated'));
398
+ }
356
399
  this.workers.forEach(wrapper => {
357
400
  wrapper.worker.terminate();
358
401
  });
@@ -394,11 +437,24 @@ class CanvasFramework {
394
437
  this.canvas.style.display = 'block';
395
438
  this.canvas.style.touchAction = 'none';
396
439
  this.canvas.style.userSelect = 'none';
440
+ // Accessibilité : le canvas est une région interactive
441
+ this.canvas.setAttribute('role', 'application');
442
+ this.canvas.setAttribute('aria-label', options.ariaLabel || 'Application');
443
+ this.canvas.setAttribute('tabindex', '0'); // permet le focus clavier
397
444
  document.body.appendChild(this.canvas);
398
-
445
+
446
+ // ✅ FIX CRITIQUE : dpr / width / height initialisés AVANT la création du contexte.
447
+ // WebGLCanvasAdapter reçoit dpr en option — s'il est undefined au moment
448
+ // de l'appel, le rendu est décalé sur les écrans haute densité (Retina, etc.).
449
+ this.dpr = window.devicePixelRatio || 1;
450
+ this.width = window.innerWidth;
451
+ this.height = window.innerHeight;
452
+
453
+ this.backgroundColor = options.backgroundColor || '#f5f5f5';
454
+
399
455
  // NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
400
- this.useWebGL = options.useWebGL ?? false; // utilise la valeur si fournie, sinon false
401
-
456
+ this.useWebGL = options.useWebGL ?? false;
457
+
402
458
  // Initialiser le contexte approprié
403
459
  if (this.useWebGL) {
404
460
  try {
@@ -422,14 +478,6 @@ class CanvasFramework {
422
478
  willReadFrequently: false
423
479
  });
424
480
  }
425
- //this.ctx.scale(this.dpr, this.dpr);
426
-
427
- this.backgroundColor = options.backgroundColor || '#f5f5f5'; // Blanc par défaut
428
-
429
- this.width = window.innerWidth;
430
- this.height = window.innerHeight;
431
-
432
- this.dpr = window.devicePixelRatio || 1;
433
481
 
434
482
  // ✅ OPTIMISATION OPTION 2: Configuration des optimisations
435
483
  this.optimizations = {
@@ -453,13 +501,16 @@ class CanvasFramework {
453
501
  };
454
502
 
455
503
  // ✅ OPTIMISATION OPTION 2: Cache des images/textes
504
+ // textCache est utilisé par fillTextCached() pour éviter de rasteriser
505
+ // le même texte plusieurs fois. Limité à 500 entrées LRU.
456
506
  this.imageCache = new Map();
457
- this.textCache = new Map();
507
+ this.textCache = new Map();
458
508
 
459
509
  // ✅ OPTIMISATION OPTION 2: Double buffering
510
+ // Desactivé en mode WebGL : l'adapter gère déjà son propre canvas off-screen.
460
511
  this._doubleBuffer = null;
461
512
  this._bufferCtx = null;
462
- if (this.optimizations.useDoubleBuffering) {
513
+ if (this.optimizations.useDoubleBuffering && !this.useWebGL) {
463
514
  this._initDoubleBuffer();
464
515
  }
465
516
 
@@ -1906,11 +1957,10 @@ class CanvasFramework {
1906
1957
  * @param {*} args - Arguments pour la fonction
1907
1958
  * @returns {Promise} Résultat de l'exécution
1908
1959
  */
1909
- async executeTask(fnString, args = {}) {
1910
- return this.workerPool.execute('EXECUTE', {
1911
- fnString,
1912
- args
1913
- });
1960
+ async executeTask(data, timeoutMs = 10000) {
1961
+ // NOTE : le type EXECUTE (new Function) a été supprimé (sécurité).
1962
+ // Utiliser COMPUTE pour transmettre des données au worker.
1963
+ return this.workerPool.execute('COMPUTE', { data }, timeoutMs);
1914
1964
  }
1915
1965
 
1916
1966
  /**
@@ -3102,10 +3152,7 @@ class CanvasFramework {
3102
3152
  if (isFixed) {
3103
3153
  comp.draw(this.ctx);
3104
3154
  } else {
3105
- this.ctx.save();
3106
- this.ctx.translate(0, 0); // Pas de scroll pendant la transition
3107
- comp.draw(this.ctx);
3108
- this.ctx.restore();
3155
+ comp.draw(this.ctx); // pas de scroll pendant la transition
3109
3156
  }
3110
3157
  }
3111
3158
  }
@@ -3127,10 +3174,7 @@ class CanvasFramework {
3127
3174
  if (isFixed) {
3128
3175
  comp.draw(this.ctx);
3129
3176
  } else {
3130
- this.ctx.save();
3131
- this.ctx.translate(0, 0); // Pas de scroll pendant la transition
3132
- comp.draw(this.ctx);
3133
- this.ctx.restore();
3177
+ comp.draw(this.ctx); // pas de scroll pendant la transition
3134
3178
  }
3135
3179
  }
3136
3180
  }
@@ -3148,10 +3192,7 @@ class CanvasFramework {
3148
3192
  if (isFixed) {
3149
3193
  comp.draw(this.ctx);
3150
3194
  } else {
3151
- this.ctx.save();
3152
- this.ctx.translate(0, 0);
3153
- comp.draw(this.ctx);
3154
- this.ctx.restore();
3195
+ comp.draw(this.ctx); // pas de scroll pendant la transition
3155
3196
  }
3156
3197
  }
3157
3198
  }
@@ -3168,10 +3209,7 @@ class CanvasFramework {
3168
3209
  if (isFixed) {
3169
3210
  comp.draw(this.ctx);
3170
3211
  } else {
3171
- this.ctx.save();
3172
- this.ctx.translate(0, 0);
3173
- comp.draw(this.ctx);
3174
- this.ctx.restore();
3212
+ comp.draw(this.ctx); // pas de scroll pendant la transition
3175
3213
  }
3176
3214
  }
3177
3215
  }
@@ -3282,6 +3320,8 @@ class CanvasFramework {
3282
3320
  const startTime = performance.now();
3283
3321
 
3284
3322
  const animate = (currentTime) => {
3323
+ // Stopper si le framework a été détruit pendant l'animation
3324
+ if (this._destroyed) return;
3285
3325
  const elapsed = currentTime - startTime;
3286
3326
  const progress = Math.min(elapsed / duration, 1);
3287
3327
  const ease = this.easeOutCubic(progress);
@@ -3321,6 +3361,10 @@ class CanvasFramework {
3321
3361
  * Nettoie les ressources
3322
3362
  */
3323
3363
  destroy() {
3364
+ // Marquer comme détruit en premier : toute animation en cours
3365
+ // (scrollTo, RAF) vérifiera ce flag avant de poster des messages.
3366
+ this._destroyed = true;
3367
+
3324
3368
  if (this.scrollWorker) {
3325
3369
  this.scrollWorker.terminate();
3326
3370
  }
@@ -3385,6 +3429,33 @@ class CanvasFramework {
3385
3429
  });
3386
3430
  this.add(toast);
3387
3431
  toast.show();
3432
+ // Annoncer le message aux lecteurs d'écran
3433
+ this.announceToScreenReader(message);
3434
+ }
3435
+
3436
+ /**
3437
+ * Envoie un message à une zone aria-live pour les lecteurs d'écran.
3438
+ * La zone est créée une seule fois et réutilisée.
3439
+ * @param {string} message
3440
+ * @param {"polite"|"assertive"} [priority="polite"]
3441
+ */
3442
+ announceToScreenReader(message, priority = 'polite') {
3443
+ if (!this._ariaLive) {
3444
+ this._ariaLive = document.createElement('div');
3445
+ Object.assign(this._ariaLive.style, {
3446
+ position: 'absolute',
3447
+ left: '-9999px',
3448
+ width: '1px',
3449
+ height: '1px',
3450
+ overflow: 'hidden'
3451
+ });
3452
+ this._ariaLive.setAttribute('aria-live', priority);
3453
+ this._ariaLive.setAttribute('aria-atomic', 'true');
3454
+ document.body.appendChild(this._ariaLive);
3455
+ }
3456
+ // Vider puis repeupler pour forcer l'annonce (même texte répété)
3457
+ this._ariaLive.textContent = '';
3458
+ requestAnimationFrame(() => { this._ariaLive.textContent = message; });
3388
3459
  }
3389
3460
  }
3390
3461
 
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Utilitaires partagés pour le dessin Canvas et la manipulation des couleurs.
3
+ * Importer dans chaque composant au lieu de dupliquer le code.
4
+ *
5
+ * Chemin suggéré : core/CanvasUtils.js
6
+ *
7
+ * @example
8
+ * import { roundRect, hexToRgba, darkenColor } from '../core/CanvasUtils.js';
9
+ */
10
+
11
+ // ─────────────────────────────────────────
12
+ // DESSIN
13
+ // ─────────────────────────────────────────
14
+
15
+ /**
16
+ * Dessine un rectangle avec coins arrondis.
17
+ * N'appelle PAS ctx.beginPath() — faites-le vous-même si nécessaire.
18
+ *
19
+ * @param {CanvasRenderingContext2D} ctx
20
+ * @param {number} x
21
+ * @param {number} y
22
+ * @param {number} width
23
+ * @param {number} height
24
+ * @param {number} radius
25
+ */
26
+ export function roundRect(ctx, x, y, width, height, radius) {
27
+ if (radius <= 0) {
28
+ ctx.rect(x, y, width, height);
29
+ return;
30
+ }
31
+ const r = Math.min(radius, width / 2, height / 2);
32
+ ctx.moveTo(x + r, y);
33
+ ctx.lineTo(x + width - r, y);
34
+ ctx.quadraticCurveTo(x + width, y, x + width, y + r);
35
+ ctx.lineTo(x + width, y + height - r);
36
+ ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
37
+ ctx.lineTo(x + r, y + height);
38
+ ctx.quadraticCurveTo(x, y + height, x, y + height - r);
39
+ ctx.lineTo(x, y + r);
40
+ ctx.quadraticCurveTo(x, y, x + r, y);
41
+ ctx.closePath();
42
+ }
43
+
44
+ /**
45
+ * Dessine un rectangle avec uniquement les coins supérieurs arrondis.
46
+ *
47
+ * @param {CanvasRenderingContext2D} ctx
48
+ * @param {number} x
49
+ * @param {number} y
50
+ * @param {number} width
51
+ * @param {number} height
52
+ * @param {number} radius
53
+ */
54
+ export function roundRectTop(ctx, x, y, width, height, radius) {
55
+ const r = Math.min(radius, width / 2, height / 2);
56
+ ctx.moveTo(x + r, y);
57
+ ctx.lineTo(x + width - r, y);
58
+ ctx.quadraticCurveTo(x + width, y, x + width, y + r);
59
+ ctx.lineTo(x + width, y + height);
60
+ ctx.lineTo(x, y + height);
61
+ ctx.lineTo(x, y + r);
62
+ ctx.quadraticCurveTo(x, y, x + r, y);
63
+ ctx.closePath();
64
+ }
65
+
66
+ // ─────────────────────────────────────────
67
+ // COULEURS
68
+ // ─────────────────────────────────────────
69
+
70
+ /**
71
+ * Convertit une couleur hexadécimale en objet {r, g, b}.
72
+ * Retourne {r:0, g:0, b:0} si la couleur est invalide.
73
+ *
74
+ * @param {string} hex - Ex : '#6750A4' ou '6750A4'
75
+ * @returns {{ r: number, g: number, b: number }}
76
+ */
77
+ export function hexToRgb(hex) {
78
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
79
+ return result
80
+ ? {
81
+ r: parseInt(result[1], 16),
82
+ g: parseInt(result[2], 16),
83
+ b: parseInt(result[3], 16),
84
+ }
85
+ : { r: 0, g: 0, b: 0 };
86
+ }
87
+
88
+ /**
89
+ * Convertit une couleur hexadécimale en chaîne rgba(…).
90
+ *
91
+ * @param {string} hex
92
+ * @param {number} alpha - Opacité entre 0 et 1
93
+ * @returns {string} Ex : 'rgba(103, 80, 164, 0.2)'
94
+ */
95
+ export function hexToRgba(hex, alpha) {
96
+ const { r, g, b } = hexToRgb(hex);
97
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
98
+ }
99
+
100
+ /**
101
+ * Assombrit une couleur (hex, rgb ou rgba).
102
+ *
103
+ * @param {string} color
104
+ * @param {number} [amount=30] - Valeur à soustraire sur chaque canal (0–255)
105
+ * @returns {string}
106
+ */
107
+ export function darkenColor(color, amount = 30) {
108
+ if (!color || color === 'transparent') return 'rgba(0,0,0,0.1)';
109
+
110
+ if (color.startsWith('#')) {
111
+ const { r, g, b } = hexToRgb(color);
112
+ return `rgb(${Math.max(0, r - amount)}, ${Math.max(0, g - amount)}, ${Math.max(0, b - amount)})`;
113
+ }
114
+
115
+ // rgb() ou rgba() : ne touche pas à l'alpha
116
+ let channelIndex = 0;
117
+ return color.replace(/[\d.]+/g, (val) => {
118
+ if (channelIndex < 3) {
119
+ channelIndex++;
120
+ return String(Math.max(0, parseFloat(val) - amount));
121
+ }
122
+ return val;
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Mélange deux couleurs hex avec un ratio donné.
128
+ *
129
+ * @param {string} hex1
130
+ * @param {string} hex2
131
+ * @param {number} [ratio=0.5] - 0 = hex1 pur, 1 = hex2 pur
132
+ * @returns {string} Couleur hex résultante
133
+ */
134
+ export function mixColors(hex1, hex2, ratio = 0.5) {
135
+ const c1 = hexToRgb(hex1);
136
+ const c2 = hexToRgb(hex2);
137
+ const r = Math.round(c1.r + (c2.r - c1.r) * ratio);
138
+ const g = Math.round(c1.g + (c2.g - c1.g) * ratio);
139
+ const b = Math.round(c1.b + (c2.b - c1.b) * ratio);
140
+ return `#${[r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('')}`;
141
+ }