canvasframework 0.6.3 → 0.7.0

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
 
@@ -89,6 +89,12 @@ class WebGLCanvasAdapter {
89
89
  // ── Stats ─────────────────────────────────────────────────────────
90
90
  this.stats = { drawCalls: 0, glyphsCached: 0, culled: 0, instancesDrawn: 0 };
91
91
 
92
+ // ── Cache couleurs (initialisé ici pour éviter la création lazy dans _parseColor) ──
93
+ this._colorCache = new Map();
94
+
95
+ // ── Flag anti-récursion pour _rebuildAtlas ─────────────────────────
96
+ this._rebuilding = false;
97
+
92
98
  // Nettoyage périodique de l'atlas si saturation
93
99
  this._cleanupTimer = setInterval(() => this._maybeRebuildAtlas(), 30000);
94
100
  }
@@ -108,14 +114,17 @@ class WebGLCanvasAdapter {
108
114
  alpha: true,
109
115
  premultipliedAlpha: false,
110
116
  antialias: false,
111
- preserveDrawingBuffer: true, // nécessaire pour drawImage final
117
+ // preserveDrawingBuffer: false (défaut) — le résultat est copié via drawImage()
118
+ // immédiatement après chaque draw call, donc pas besoin de préserver le buffer.
119
+ // Laisser à false permet au driver GPU d'utiliser le double-buffering natif.
120
+ preserveDrawingBuffer: false,
112
121
  powerPreference: 'high-performance'
113
122
  });
114
123
 
115
124
  // Fallback WebGL1 si WebGL2 indispo
116
125
  this.gl = gl || this._glCanvas.getContext('webgl', {
117
126
  alpha: true, premultipliedAlpha: false,
118
- antialias: false, preserveDrawingBuffer: true
127
+ antialias: false, preserveDrawingBuffer: false
119
128
  });
120
129
 
121
130
  if (!this.gl) throw new Error('WebGL non disponible');
@@ -265,6 +274,54 @@ class WebGLCanvasAdapter {
265
274
  this._instanceData.byteLength,
266
275
  gl.DYNAMIC_DRAW
267
276
  );
277
+
278
+ // VAO WebGL2 : enregistre le layout des attributs une seule fois.
279
+ // Évite de rebinder chaque attribut à chaque draw call (gain CPU mesurable).
280
+ if (this._isWebGL2) {
281
+ this._vao = gl.createVertexArray();
282
+ gl.bindVertexArray(this._vao);
283
+ this._setupAttribPointers();
284
+ gl.bindVertexArray(null);
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Configure les pointeurs d'attributs vertex (appelé une fois dans le VAO,
290
+ * ou à chaque draw call en WebGL1).
291
+ * @private
292
+ */
293
+ _setupAttribPointers() {
294
+ const gl = this.gl;
295
+ const loc = this._loc;
296
+ const ext = this._ext;
297
+ const stride = this._FLOATS_PER_INSTANCE * 4;
298
+
299
+ // Quad (diviseur 0 = même valeur pour toutes les instances)
300
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._quadBuf);
301
+ gl.enableVertexAttribArray(loc.quad);
302
+ gl.vertexAttribPointer(loc.quad, 2, gl.FLOAT, false, 0, 0);
303
+ if (ext) ext.vertexAttribDivisorANGLE(loc.quad, 0);
304
+ else gl.vertexAttribDivisor(loc.quad, 0);
305
+
306
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._instanceBuf);
307
+
308
+ // a_dst : vec4 @ offset 0
309
+ gl.enableVertexAttribArray(loc.dst);
310
+ gl.vertexAttribPointer(loc.dst, 4, gl.FLOAT, false, stride, 0);
311
+ if (ext) ext.vertexAttribDivisorANGLE(loc.dst, 1);
312
+ else gl.vertexAttribDivisor(loc.dst, 1);
313
+
314
+ // a_uv : vec4 @ offset 16
315
+ gl.enableVertexAttribArray(loc.uv);
316
+ gl.vertexAttribPointer(loc.uv, 4, gl.FLOAT, false, stride, 16);
317
+ if (ext) ext.vertexAttribDivisorANGLE(loc.uv, 1);
318
+ else gl.vertexAttribDivisor(loc.uv, 1);
319
+
320
+ // a_color : vec4 @ offset 32
321
+ gl.enableVertexAttribArray(loc.color);
322
+ gl.vertexAttribPointer(loc.color, 4, gl.FLOAT, false, stride, 32);
323
+ if (ext) ext.vertexAttribDivisorANGLE(loc.color, 1);
324
+ else gl.vertexAttribDivisor(loc.color, 1);
268
325
  }
269
326
 
270
327
  _setupAtlasTexture() {
@@ -373,8 +430,12 @@ class WebGLCanvasAdapter {
373
430
  /**
374
431
  * Reconstruit l'atlas depuis zéro avec uniquement les glyphes actuels.
375
432
  * Appelé quand l'atlas est saturé.
433
+ * Protégé contre la réentrance par _rebuilding.
376
434
  */
377
435
  _rebuildAtlas() {
436
+ if (this._rebuilding) return; // évite la récursion infinie
437
+ this._rebuilding = true;
438
+
378
439
  const ctx = this._atlasCtx;
379
440
  ctx.clearRect(0, 0, this.atlasSize, this.atlasSize);
380
441
 
@@ -391,7 +452,8 @@ class WebGLCanvasAdapter {
391
452
  if (char && font) this._getGlyph(char, font);
392
453
  }
393
454
 
394
- this._atlasDirty = true;
455
+ this._atlasDirty = true;
456
+ this._rebuilding = false;
395
457
  }
396
458
 
397
459
  _maybeRebuildAtlas() {
@@ -403,14 +465,18 @@ class WebGLCanvasAdapter {
403
465
 
404
466
  /**
405
467
  * Upload la texture atlas vers le GPU si elle a changé.
468
+ * Utilise texSubImage2D (mise à jour partielle) plutôt que texImage2D (ré-allocation
469
+ * complète) pour éviter un round-trip mémoire GPU inutile à chaque modification.
406
470
  * Appelé UNE seule fois par frame, juste avant le draw call.
407
471
  */
408
472
  _uploadAtlasIfDirty() {
409
473
  if (!this._atlasDirty) return;
410
474
  const gl = this.gl;
411
475
  gl.bindTexture(gl.TEXTURE_2D, this._atlasTex);
412
- gl.texImage2D(
413
- gl.TEXTURE_2D, 0, gl.RGBA,
476
+ // texSubImage2D met à jour les données sans réallouer le stockage GPU
477
+ gl.texSubImage2D(
478
+ gl.TEXTURE_2D, 0,
479
+ 0, 0, // offset x, y dans la texture
414
480
  gl.RGBA, gl.UNSIGNED_BYTE,
415
481
  this._atlasCanvas
416
482
  );
@@ -558,43 +624,29 @@ class WebGLCanvasAdapter {
558
624
  gl.bindTexture(gl.TEXTURE_2D, this._atlasTex);
559
625
  gl.uniform1i(loc.atlas, 0);
560
626
 
561
- // 6. Buffer quad (commun à toutes les instances)
562
- gl.bindBuffer(gl.ARRAY_BUFFER, this._quadBuf);
563
- gl.enableVertexAttribArray(loc.quad);
564
- gl.vertexAttribPointer(loc.quad, 2, gl.FLOAT, false, 0, 0);
565
- if (ext) ext.vertexAttribDivisorANGLE(loc.quad, 0);
566
- else gl.vertexAttribDivisor(loc.quad, 0);
567
-
568
- // 7. Buffer instances — upload uniquement les instances actives
569
- const stride = this._FLOATS_PER_INSTANCE * 4; // bytes
627
+ // 6. Upload uniquement les instances actives dans le buffer GPU
570
628
  gl.bindBuffer(gl.ARRAY_BUFFER, this._instanceBuf);
571
629
  gl.bufferSubData(
572
630
  gl.ARRAY_BUFFER, 0,
573
631
  this._instanceData.subarray(0, this._instanceCount * this._FLOATS_PER_INSTANCE)
574
632
  );
575
633
 
576
- // a_dst : vec4 @ offset 0
577
- gl.enableVertexAttribArray(loc.dst);
578
- gl.vertexAttribPointer(loc.dst, 4, gl.FLOAT, false, stride, 0);
579
- if (ext) ext.vertexAttribDivisorANGLE(loc.dst, 1);
580
- else gl.vertexAttribDivisor(loc.dst, 1);
581
-
582
- // a_uv : vec4 @ offset 16
583
- gl.enableVertexAttribArray(loc.uv);
584
- gl.vertexAttribPointer(loc.uv, 4, gl.FLOAT, false, stride, 16);
585
- if (ext) ext.vertexAttribDivisorANGLE(loc.uv, 1);
586
- else gl.vertexAttribDivisor(loc.uv, 1);
587
-
588
- // a_color : vec4 @ offset 32
589
- gl.enableVertexAttribArray(loc.color);
590
- gl.vertexAttribPointer(loc.color, 4, gl.FLOAT, false, stride, 32);
591
- if (ext) ext.vertexAttribDivisorANGLE(loc.color, 1);
592
- else gl.vertexAttribDivisor(loc.color, 1);
634
+ // 7. Lier les attributs vertex
635
+ if (this._vao) {
636
+ // WebGL2 : le VAO a déjà mémorisé le layout — un seul bind suffit
637
+ gl.bindVertexArray(this._vao);
638
+ } else {
639
+ // WebGL1 : rebinder les attributs manuellement à chaque draw call
640
+ this._setupAttribPointers();
641
+ }
593
642
 
594
643
  // 8. LE draw call unique — dessine N instances du quad unitaire
595
644
  if (ext) ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, this._instanceCount);
596
645
  else gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this._instanceCount);
597
646
 
647
+ // Libérer le VAO après usage
648
+ if (this._vao) gl.bindVertexArray(null);
649
+
598
650
  // 9. Copier le résultat WebGL sur le canvas 2D principal
599
651
  this.ctx.drawImage(this._glCanvas, 0, 0);
600
652
 
@@ -632,18 +684,14 @@ class WebGLCanvasAdapter {
632
684
 
633
685
  /**
634
686
  * Parse une couleur CSS → [r, g, b, a] normalisés [0..1]
635
- * Supporte : #rgb #rrggbb rgba() rgb() et les couleurs nommées communes
687
+ * Supporte : #rgb #rrggbb #rrggbbaa rgba() rgb() et les couleurs nommées communes.
688
+ * Le cache (_colorCache) est initialisé dans le constructeur.
636
689
  */
637
690
  _parseColor(color) {
638
691
  if (!color) return [0, 0, 0, 1];
639
692
 
640
- // Cache
641
- if (this._colorCache) {
642
- const cached = this._colorCache.get(color);
643
- if (cached) return cached;
644
- } else {
645
- this._colorCache = new Map();
646
- }
693
+ const cached = this._colorCache.get(color);
694
+ if (cached) return cached;
647
695
 
648
696
  let r = 0, g = 0, b = 0, a = 1;
649
697
 
@@ -769,6 +817,7 @@ class WebGLCanvasAdapter {
769
817
  gl.deleteTexture(this._atlasTex);
770
818
  gl.deleteBuffer(this._quadBuf);
771
819
  gl.deleteBuffer(this._instanceBuf);
820
+ if (this._vao) gl.deleteVertexArray(this._vao);
772
821
  gl.deleteProgram(this._program);
773
822
  }
774
823
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/beyons/CanvasFramework.git"