canvasframework 0.6.2 → 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.
- package/core/CanvasFramework.js +115 -44
- package/core/WebGLCanvasAdapter.js +88 -39
- package/package.json +1 -1
- package/utils/AnimationEngine.js +2 -2
package/core/CanvasFramework.js
CHANGED
|
@@ -227,11 +227,10 @@ class WorkerPool {
|
|
|
227
227
|
result = { state };
|
|
228
228
|
break;
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
413
|
-
|
|
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.
|
|
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
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
else
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
641
|
-
if (
|
|
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
package/utils/AnimationEngine.js
CHANGED
|
@@ -17,8 +17,8 @@ class AnimationEngine {
|
|
|
17
17
|
|
|
18
18
|
// Worker pour les calculs d'animation
|
|
19
19
|
this.worker = null;
|
|
20
|
-
this.workerEnabled =
|
|
21
|
-
this.initWorker();
|
|
20
|
+
this.workerEnabled = false;
|
|
21
|
+
//this.initWorker();
|
|
22
22
|
|
|
23
23
|
// OffscreenCanvas cache
|
|
24
24
|
this.offscreenCache = new Map();
|