canvasframework 0.4.10 → 0.5.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.
@@ -1,1435 +1,782 @@
1
1
  /**
2
- * Adaptateur WebGL pour le rendu Canvas 2D-like
2
+ * Adaptateur WebGL pour le rendu de texte ultra-optimisé
3
+ * Version améliorée avec optimisations supplémentaires
3
4
  * @class WebGLCanvasAdapter
4
5
  */
5
6
  class WebGLCanvasAdapter {
6
- /**
7
- * Crée une instance de WebGLCanvasAdapter
8
- * @param {HTMLCanvasElement} canvas - Élément canvas HTML
9
- * @param {Object} options - Options de configuration WebGL
10
- */
11
- constructor(canvas, options = {}) {
12
- this.canvas = canvas;
13
- this.options = {
14
- alpha: true,
15
- antialias: true,
16
- depth: false,
17
- stencil: false,
18
- powerPreference: 'high-performance',
19
- preserveDrawingBuffer: false,
20
- ...options
21
- };
22
-
23
- // Obtenir le contexte WebGL
24
- this.gl = canvas.getContext('webgl2', this.options) ||
25
- canvas.getContext('webgl', this.options) ||
26
- canvas.getContext('experimental-webgl', this.options);
27
-
28
- if (!this.gl) {
29
- throw new Error('WebGL not supported');
30
- }
31
-
32
- // Dimensions
33
- this.width = canvas.width;
34
- this.height = canvas.height;
35
- this.dpr = window.devicePixelRatio || 1;
36
-
37
- // État du contexte (compatibilité Canvas 2D)
38
- this._state = {
39
- fillStyle: '#000000',
40
- strokeStyle: '#000000',
41
- lineWidth: 1,
42
- lineCap: 'butt',
43
- lineJoin: 'miter',
44
- miterLimit: 10,
45
- globalAlpha: 1,
46
- textAlign: 'start',
47
- textBaseline: 'alphabetic',
48
- font: '10px sans-serif',
49
- shadowColor: 'rgba(0, 0, 0, 0)',
50
- shadowBlur: 0,
51
- shadowOffsetX: 0,
52
- shadowOffsetY: 0,
53
- filter: 'none'
54
- };
55
-
56
- // Pile d'états (pour save/restore)
57
- this._stateStack = [];
58
-
59
- // Transformations
60
- this._transform = {
61
- matrix: [1, 0, 0, 1, 0, 0], // [a, b, c, d, e, f]
62
- stack: []
63
- };
64
-
65
- // Chemins (path)
66
- this._path = {
67
- points: [],
68
- subpaths: [],
69
- currentSubpath: []
70
- };
71
-
72
- // Textures et ressources
73
- this._textures = new Map();
74
- this._shaders = new Map();
75
- this._buffers = new Map();
76
-
77
- // Programmes de shader
78
- this._programs = {
79
- basic: null,
80
- text: null,
81
- gradient: null,
82
- image: null
83
- };
84
-
85
- // Gestionnaire de dégradés
86
- this._gradients = new Map();
87
-
88
- // Couleurs courantes
89
- this._currentFillColor = this._parseColor(this._state.fillStyle);
90
- this._currentStrokeColor = this._parseColor(this._state.strokeStyle);
91
-
92
- // Configuration initiale
93
- this._setupWebGL();
94
- this._compileShaders();
95
- this._createBuffers();
96
-
97
- // Interface CanvasRenderingContext2D-like
98
- this._setupInterface();
99
-
100
- // Test immédiat pour vérifier le rendu
101
- setTimeout(() => {
102
- console.log('Testing WebGL rendering...');
103
- this.fillStyle = 'red';
104
- this.fillRect(10, 10, 100, 100);
105
- this.fillStyle = 'blue';
106
- this.fillRect(150, 10, 100, 100);
107
-
108
- // Vérifier l'état WebGL
109
- const glError = this.gl.getError();
110
- if (glError !== this.gl.NO_ERROR) {
111
- console.error('WebGL error:', glError);
112
- } else {
113
- console.log('WebGL rendering test passed');
114
- }
115
- }, 100);
116
- }
117
-
118
- /**
119
- * Configure l'environnement WebGL de base
120
- * @private
121
- */
122
- _setupWebGL() {
123
- const gl = this.gl;
124
-
125
- // Configuration du viewport
126
- gl.viewport(0, 0, this.width, this.height);
127
-
128
- // Activer le blending pour la transparence
129
- gl.enable(gl.BLEND);
130
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
131
-
132
- // Désactiver le test de profondeur (2D)
133
- gl.disable(gl.DEPTH_TEST);
134
-
135
- // Clear color
136
- gl.clearColor(0, 0, 0, 0);
137
- }
138
-
139
- /**
140
- * Compile les shaders nécessaires
141
- * @private
142
- */
143
- _compileShaders() {
144
- console.log('=== COMPILING SIMPLE SHADER ===');
145
-
146
- // SHADER ULTRA SIMPLE qui compile à coup sûr
147
- const basicVS = `#version 300 es
148
- precision highp float;
149
-
150
- in vec2 a_position;
151
- in vec4 a_color;
152
-
153
- out vec4 v_color;
154
-
155
- void main() {
156
- // DIRECT NDC: a_position contient déjà des coordonnées NDC (-1 à 1)
157
- gl_Position = vec4(a_position, 0.0, 1.0);
158
- v_color = a_color;
159
- }
160
- `;
161
-
162
- const basicFS = `#version 300 es
163
- precision highp float;
164
-
165
- in vec4 v_color;
166
- out vec4 outColor;
167
-
168
- void main() {
169
- outColor = v_color;
170
- }
171
- `;
172
-
173
- this._programs.basic = this._createProgram(basicVS, basicFS);
174
-
175
- // Shaders pour texte et images (simplifiés aussi)
176
- const textVS = `#version 300 es
177
- precision highp float;
178
-
179
- in vec2 a_position;
180
- in vec2 a_texcoord;
181
-
182
- out vec2 v_texcoord;
183
-
184
- void main() {
185
- gl_Position = vec4(a_position, 0.0, 1.0);
186
- v_texcoord = a_texcoord;
187
- }
188
- `;
189
-
190
- const textFS = `#version 300 es
191
- precision highp float;
192
-
193
- in vec2 v_texcoord;
194
- uniform sampler2D u_texture;
195
- uniform vec4 u_color;
196
-
197
- out vec4 outColor;
198
-
199
- void main() {
200
- float alpha = texture(u_texture, v_texcoord).r;
201
- outColor = vec4(u_color.rgb, u_color.a * alpha);
202
- }
203
- `;
204
-
205
- this._programs.text = this._createProgram(textVS, textFS);
206
-
207
- const imageVS = `#version 300 es
208
- precision highp float;
209
-
210
- in vec2 a_position;
211
- in vec2 a_texcoord;
212
-
213
- out vec2 v_texcoord;
214
-
215
- void main() {
216
- gl_Position = vec4(a_position, 0.0, 1.0);
217
- v_texcoord = a_texcoord;
218
- }
219
- `;
220
-
221
- const imageFS = `#version 300 es
222
- precision highp float;
223
-
224
- in vec2 v_texcoord;
225
- uniform sampler2D u_texture;
226
- uniform float u_alpha;
227
-
228
- out vec4 outColor;
229
-
230
- void main() {
231
- vec4 texColor = texture(u_texture, v_texcoord);
232
- outColor = vec4(texColor.rgb, texColor.a * u_alpha);
233
- }
234
- `;
235
-
236
- this._programs.image = this._createProgram(imageVS, imageFS);
237
-
238
- console.log('=== SHADER COMPILATION COMPLETE ===');
239
- }
240
-
241
- /**
242
- * Crée un programme WebGL à partir de sources de shader
243
- * @private
244
- */
245
- _createProgram(vsSource, fsSource) {
246
- const gl = this.gl;
247
-
248
- console.log('Compiling vertex shader...');
249
- const vertexShader = this._compileShader(gl.VERTEX_SHADER, vsSource);
250
- if (!vertexShader) {
251
- console.error('Vertex shader compilation failed');
252
- return null;
253
- }
254
-
255
- console.log('Compiling fragment shader...');
256
- const fragmentShader = this._compileShader(gl.FRAGMENT_SHADER, fsSource);
257
- if (!fragmentShader) {
258
- console.error('Fragment shader compilation failed');
259
- return null;
260
- }
261
-
262
- const program = gl.createProgram();
263
- gl.attachShader(program, vertexShader);
264
- gl.attachShader(program, fragmentShader);
265
- gl.linkProgram(program);
266
-
267
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
268
- console.error('Program link error:', gl.getProgramInfoLog(program));
269
- gl.deleteProgram(program);
270
- return null;
271
- }
272
-
273
- console.log('Shader program compiled successfully');
274
- return program;
275
- }
276
-
277
- /**
278
- * Compile un shader
279
- * @private
280
- */
281
- _compileShader(type, source) {
282
- const gl = this.gl;
283
- const shader = gl.createShader(type);
284
- gl.shaderSource(shader, source);
285
- gl.compileShader(shader);
286
-
287
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
288
- console.error('Shader compile error:', gl.getShaderInfoLog(shader));
289
- gl.deleteShader(shader);
290
- return null;
291
- }
292
-
293
- return shader;
294
- }
295
-
296
- /**
297
- * Crée les buffers GPU nécessaires
298
- * @private
299
- */
300
- _createBuffers() {
301
- const gl = this.gl;
302
-
303
- // Buffer de position
304
- const positionBuffer = gl.createBuffer();
305
- gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
306
- this._buffers.set('position', positionBuffer);
307
-
308
- // Buffer de couleur
309
- const colorBuffer = gl.createBuffer();
310
- gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
311
- this._buffers.set('color', colorBuffer);
312
-
313
- // Buffer d'indices
314
- const indexBuffer = gl.createBuffer();
315
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
316
- this._buffers.set('indices', indexBuffer);
317
-
318
- // Buffer de texture pour le texte
319
- const texcoordBuffer = gl.createBuffer();
320
- gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
321
- this._buffers.set('texcoord', texcoordBuffer);
322
- }
323
-
324
- /**
325
- * Configure l'interface CanvasRenderingContext2D
326
- * @private
327
- */
328
- _setupInterface() {
329
- // Méthodes de dessin de base
330
- this.fillRect = this._fillRect.bind(this);
331
- this.strokeRect = this._strokeRect.bind(this);
332
- this.clearRect = this._clearRect.bind(this);
333
- this.fillText = this._fillText.bind(this);
334
- this.strokeText = this._strokeText.bind(this);
335
- this.drawImage = this._drawImage.bind(this);
336
-
337
- // Chemins (paths)
338
- this.beginPath = this._beginPath.bind(this);
339
- this.moveTo = this._moveTo.bind(this);
340
- this.lineTo = this._lineTo.bind(this);
341
- this.arc = this._arc.bind(this);
342
- this.rect = this._rect.bind(this);
343
- this.closePath = this._closePath.bind(this);
344
- this.fill = this._fill.bind(this);
345
- this.stroke = this._stroke.bind(this);
346
-
347
- // Transformations
348
- this.save = this._save.bind(this);
349
- this.restore = this._restore.bind(this);
350
- this.translate = this._translate.bind(this);
351
- this.rotate = this._rotate.bind(this);
352
- this.scale = this._scale.bind(this);
353
- this.transform = this._transformMethod.bind(this);
354
- this.setTransform = this._setTransform.bind(this);
355
-
356
- // Dégradés et motifs
357
- this.createLinearGradient = this._createLinearGradient.bind(this);
358
- this.createRadialGradient = this._createRadialGradient.bind(this);
359
-
360
- // Mesure de texte
361
- this.measureText = this._measureText.bind(this);
362
-
363
- // Méthodes supplémentaires
364
- this.quadraticCurveTo = this._quadraticCurveTo.bind(this);
365
- this.bezierCurveTo = this._bezierCurveTo.bind(this);
366
- this.ellipse = this._ellipse.bind(this);
367
- this.roundRect = this._roundRect.bind(this);
368
-
369
- // État du contexte
370
- this._setupGettersSetters();
371
- }
372
-
373
- /**
374
- * Vérifie les erreurs WebGL
375
- * @private
376
- */
377
- checkGLError(context) {
378
- const gl = this.gl;
379
- const error = gl.getError();
380
- if (error !== gl.NO_ERROR) {
381
- console.error(`WebGL error (${context}):`, error);
382
- return false;
383
- }
384
- return true;
385
- }
386
-
387
- /**
388
- * Configure les getters/setters pour les propriétés du contexte
389
- * @private
390
- */
391
- _setupGettersSetters() {
392
- const properties = [
393
- 'fillStyle',
394
- 'strokeStyle',
395
- 'lineWidth',
396
- 'lineCap',
397
- 'lineJoin',
398
- 'miterLimit',
399
- 'globalAlpha',
400
- 'textAlign',
401
- 'textBaseline',
402
- 'font',
403
- 'shadowColor',
404
- 'shadowBlur',
405
- 'shadowOffsetX',
406
- 'shadowOffsetY',
407
- 'filter'
408
- ];
409
-
410
- properties.forEach(prop => {
411
- Object.defineProperty(this, prop, {
412
- get: () => this._state[prop],
413
- set: (value) => {
414
- this._state[prop] = value;
415
- this._updateStateDependencies(prop, value);
416
- },
417
- enumerable: true,
418
- configurable: true
419
- });
420
- });
421
- }
422
-
423
- /**
424
- * Met à jour les dépendances d'état
425
- * @private
426
- */
427
- _updateStateDependencies(prop, value) {
428
- switch (prop) {
429
- case 'globalAlpha':
430
- // Ne rien faire, l'alpha est appliqué dans _parseColor
431
- break;
432
- case 'fillStyle':
433
- this._currentFillColor = this._parseColor(value);
434
- break;
435
- case 'strokeStyle':
436
- this._currentStrokeColor = this._parseColor(value);
437
- break;
438
- }
439
- }
440
-
441
- /**
442
- * Met à jour le style de remplissage
443
- * @private
444
- */
445
- _updateFillStyle(value) {
446
- this._currentFillColor = this._parseColor(value);
447
- }
448
-
449
- /**
450
- * Met à jour le style de contour
451
- * @private
452
- */
453
- _updateStrokeStyle(value) {
454
- this._currentStrokeColor = this._parseColor(value);
455
- }
456
-
457
- /**
458
- * Met à jour l'alpha global
459
- * @private
460
- */
461
- _updateAlpha(value) {
462
- // L'alpha est déjà appliqué dans _parseColor
463
- }
464
-
465
- /**
466
- * Dessine une courbe de Bézier quadratique
467
- * @param {number} cpx - Point de contrôle X
468
- * @param {number} cpy - Point de contrôle Y
469
- * @param {number} x - Point final X
470
- * @param {number} y - Point final Y
471
- */
472
- _quadraticCurveTo(cpx, cpy, x, y) {
473
- this._lineTo(x, y); // Implémentation simplifiée
474
- }
475
-
476
- /**
477
- * Dessine une courbe de Bézier cubique
478
- * @param {number} cp1x - Premier point de contrôle X
479
- * @param {number} cp1y - Premier point de contrôle Y
480
- * @param {number} cp2x - Deuxième point de contrôle X
481
- * @param {number} cp2y - Deuxième point de contrôle Y
482
- * @param {number} x - Point final X
483
- * @param {number} y - Point final Y
484
- */
485
- _bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
486
- this._lineTo(x, y); // Implémentation simplifiée
487
- }
488
-
489
- /**
490
- * Dessine une ellipse
491
- * @param {number} x - Centre X
492
- * @param {number} y - Centre Y
493
- * @param {number} radiusX - Rayon horizontal
494
- * @param {number} radiusY - Rayon vertical
495
- * @param {number} rotation - Rotation en radians
496
- * @param {number} startAngle - Angle de départ
497
- * @param {number} endAngle - Angle de fin
498
- * @param {boolean} anticlockwise - Sens anti-horaire
499
- */
500
- _ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise = false) {
501
- // Implémentation simplifiée - utilise arc() pour un cercle
502
- if (radiusX === radiusY) {
503
- this.arc(x, y, radiusX, startAngle, endAngle, anticlockwise);
504
- } else {
505
- // Pour une ellipse, on peut approximer avec plusieurs arcs
506
- const segments = 16;
507
- const angleStep = (endAngle - startAngle) / segments;
508
-
509
- if (!this._path.currentSubpath.length) {
510
- const firstX = x + Math.cos(startAngle) * radiusX;
511
- const firstY = y + Math.sin(startAngle) * radiusY;
512
- this._moveTo(firstX, firstY);
513
- }
514
-
515
- for (let i = 1; i <= segments; i++) {
516
- const angle = startAngle + angleStep * i;
517
- const px = x + Math.cos(angle) * radiusX;
518
- const py = y + Math.sin(angle) * radiusY;
519
- this._lineTo(px, py);
520
- }
521
- }
522
- }
7
+ constructor(canvasElement, options = {}) {
8
+ this.canvas = canvasElement;
9
+ this.dpr = options.dpr || window.devicePixelRatio || 1;
10
+
11
+ // Contexte 2D principal pour les formes
12
+ this.ctx = this.canvas.getContext('2d', {
13
+ alpha: options.alpha !== false,
14
+ desynchronized: true,
15
+ willReadFrequently: false
16
+ });
523
17
 
524
- /**
525
- * Arrondit les coins d'un rectangle
526
- * @param {number} x - Position X
527
- * @param {number} y - Position Y
528
- * @param {number} width - Largeur
529
- * @param {number} height - Hauteur
530
- * @param {number|number[]} radii - Rayon(s) d'arrondi
531
- */
532
- _roundRect(x, y, width, height, radii) {
533
- // Implémentation simplifiée de roundRect
534
- const radius = Array.isArray(radii) ? radii[0] : radii;
535
-
536
- this.beginPath();
537
-
538
- if (radius === 0) {
539
- // Rectangle normal
540
- this.rect(x, y, width, height);
541
- } else {
542
- // Rectangle avec coins arrondis
543
- this.moveTo(x + radius, y);
544
- this.lineTo(x + width - radius, y);
545
- this.quadraticCurveTo(x + width, y, x + width, y + radius);
546
- this.lineTo(x + width, y + height - radius);
547
- this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
548
- this.lineTo(x + radius, y + height);
549
- this.quadraticCurveTo(x, y + height, x, y + height - radius);
550
- this.lineTo(x, y + radius);
551
- this.quadraticCurveTo(x, y, x + radius, y);
552
- this.closePath();
553
- }
18
+ // ✅ OPTIONS D'OPTIMISATION
19
+ this.useTextAtlas = options.useTextAtlas !== false;
20
+ this.enableCulling = options.enableCulling !== false;
21
+ this.enableBatching = options.enableBatching !== false;
22
+ this.useOffscreenCanvas = options.useOffscreenCanvas !== false && typeof OffscreenCanvas !== 'undefined';
23
+
24
+ // WebGL pour le texte
25
+ this._initWebGLTextRenderer();
26
+
27
+ // Cache optimisé avec LRU
28
+ this.textCache = new Map();
29
+ this.charAtlas = new Map();
30
+ this.maxTextCacheSize = options.maxCacheSize || 400;
31
+ this.lruKeys = []; // ✅ NOUVEAU : Tracking LRU pour meilleur cache eviction
32
+
33
+ // Text Atlas optimisé (utilise plusieurs atlas si nécessaire)
34
+ this.atlases = [this._createAtlas()]; // NOUVEAU : Support multi-atlas
35
+ this.currentAtlasIndex = 0;
36
+
37
+ // Batch rendering optimisé
38
+ this.textBatch = [];
39
+ this.batchMode = false;
40
+ this.maxBatchSize = options.maxBatchSize || 1000; // NOUVEAU : Limite batch size
41
+
42
+ // NOUVEAU : Pré-calcul des métriques communes
43
+ this.fontMetricsCache = new Map();
44
+ this.baselineRatios = {
45
+ 'alphabetic': 0.85,
46
+ 'top': 1.0,
47
+ 'middle': 0.65,
48
+ 'bottom': 0,
49
+ 'hanging': 0.9,
50
+ 'ideographic': 0.1
51
+ };
52
+
53
+ // États pour le texte
54
+ this._currentFont = '16px sans-serif';
55
+ this._currentFillStyle = '#000';
56
+ this._currentTextAlign = 'start';
57
+ this._currentTextBaseline = 'alphabetic';
58
+
59
+ // ✅ NOUVEAU : Pool d'objets pour réduire GC
60
+ this.objectPool = {
61
+ points: [],
62
+ rects: [],
63
+ maxPoolSize: 100
64
+ };
65
+
66
+ // ✅ NOUVEAU : Viewport cache pour culling
67
+ this.viewportBounds = {
68
+ left: 0,
69
+ right: this.canvas.width,
70
+ top: 0,
71
+ bottom: this.canvas.height
72
+ };
73
+
74
+ // Stats
75
+ this.stats = {
76
+ cacheHits: 0,
77
+ cacheMisses: 0,
78
+ drawCalls: 0,
79
+ culledTexts: 0,
80
+ batchedDraws: 0,
81
+ atlasCount: 1
82
+ };
83
+
84
+ // ✅ NOUVEAU : Debounced cleanup
85
+ this._cleanupScheduled = false;
86
+ this._textCleanupInterval = setInterval(() => this._cleanOldCache(), 60000);
87
+ }
88
+
89
+ // ────────────────────────────────────────────────
90
+ // ✅ NOUVEAU : Gestion multi-atlas
91
+ // ────────────────────────────────────────────────
92
+ _createAtlas() {
93
+ const canvas = this.useOffscreenCanvas
94
+ ? new OffscreenCanvas(2048, 2048)
95
+ : document.createElement('canvas');
96
+
97
+ if (!this.useOffscreenCanvas) {
98
+ canvas.width = 2048;
99
+ canvas.height = 2048;
554
100
  }
555
-
556
- // ===== IMPLÉMENTATION DES MÉTHODES CANVAS 2D =====
557
-
558
- /**
559
- * Remplit un rectangle
560
- * @param {number} x - Position X
561
- * @param {number} y - Position Y
562
- * @param {number} width - Largeur
563
- * @param {number} height - Hauteur
564
- */
565
- _fillRect(x, y, width, height) {
566
- // FORCER des dimensions positives
567
- const actualX = Math.min(x, x + width);
568
- const actualY = Math.min(y, y + height);
569
- const actualWidth = Math.abs(width);
570
- const actualHeight = Math.abs(height);
571
101
 
572
- console.log('_fillRect corrected:', {
573
- original: {x, y, width, height},
574
- corrected: {actualX, actualY, actualWidth, actualHeight}
575
- });
102
+ const ctx = canvas.getContext('2d', { alpha: true, willReadFrequently: false });
576
103
 
577
- const color = this._parseColor(this.fillStyle);
578
- color[3] *= this.globalAlpha;
104
+ return {
105
+ canvas,
106
+ ctx,
107
+ x: 0,
108
+ y: 0,
109
+ rowHeight: 0,
110
+ usage: 0 // ✅ Track utilization
111
+ };
112
+ }
113
+
114
+ // ────────────────────────────────────────────────
115
+ // Initialisation WebGL
116
+ // ────────────────────────────────────────────────
117
+ _initWebGLTextRenderer() {
118
+ this.textCanvas = this.useOffscreenCanvas
119
+ ? new OffscreenCanvas(256, 256)
120
+ : document.createElement('canvas');
579
121
 
580
- // Créer un rectangle avec 2 triangles (TOUJOURS positif)
581
- const vertices = new Float32Array([
582
- actualX, actualY,
583
- actualX + actualWidth, actualY,
584
- actualX, actualY + actualHeight,
585
- actualX + actualWidth, actualY + actualHeight
586
- ]);
587
-
588
- // Créer un rectangle avec 2 triangles
589
-
590
- const colors = new Float32Array(16);
591
- for (let i = 0; i < 4; i++) {
592
- colors[i*4] = color[0];
593
- colors[i*4+1] = color[1];
594
- colors[i*4+2] = color[2];
595
- colors[i*4+3] = color[3];
596
- }
597
-
598
- const indices = new Uint16Array([0, 1, 2, 2, 1, 3]);
599
-
600
- this._drawTriangles(vertices, colors, indices, this._getCurrentMatrix());
601
- }
602
-
603
- /**
604
- * Dessine le contour d'un rectangle
605
- * @param {number} x - Position X
606
- * @param {number} y - Position Y
607
- * @param {number} width - Largeur
608
- * @param {number} height - Hauteur
609
- */
610
- _strokeRect(x, y, width, height) {
611
- const lineWidth = this.lineWidth;
612
- const halfWidth = lineWidth / 2;
613
-
614
- // Dessiner 4 lignes pour former le rectangle
615
- this._strokeLine(x - halfWidth, y, x + width + halfWidth, y); // Haut
616
- this._strokeLine(x + width, y - halfWidth, x + width, y + height + halfWidth); // Droite
617
- this._strokeLine(x + width + halfWidth, y + height, x - halfWidth, y + height); // Bas
618
- this._strokeLine(x, y + height + halfWidth, x, y - halfWidth); // Gauche
619
- }
620
-
621
- /**
622
- * Efface une zone rectangulaire
623
- * @param {number} x - Position X
624
- * @param {number} y - Position Y
625
- * @param {number} width - Largeur
626
- * @param {number} height - Hauteur
627
- */
628
- _clearRect(x, y, width, height) {
629
- const gl = this.gl;
630
-
631
- // Sauvegarder l'état actuel
632
- gl.enable(gl.SCISSOR_TEST);
633
- gl.scissor(x * this.dpr, (this.height - y - height) * this.dpr,
634
- width * this.dpr, height * this.dpr);
635
-
636
- // Effacer avec la couleur de fond
637
- gl.clearColor(0, 0, 0, 0);
638
- gl.clear(gl.COLOR_BUFFER_BIT);
639
-
640
- // Restaurer
641
- gl.disable(gl.SCISSOR_TEST);
642
- }
643
-
644
- /**
645
- * Dessine du texte rempli
646
- * @param {string} text - Texte à dessiner
647
- * @param {number} x - Position X
648
- * @param {number} y - Position Y
649
- * @param {number} maxWidth - Largeur maximale (optionnelle)
650
- */
651
- _fillText(text, x, y, maxWidth) {
652
- this._renderText(text, x, y, maxWidth, 'fill');
653
- }
654
-
655
- /**
656
- * Dessine le contour du texte
657
- * @param {string} text - Texte à dessiner
658
- * @param {number} x - Position X
659
- * @param {number} y - Position Y
660
- * @param {number} maxWidth - Largeur maximale (optionnelle)
661
- */
662
- _strokeText(text, x, y, maxWidth) {
663
- this._renderText(text, x, y, maxWidth, 'stroke');
664
- }
665
-
666
- /**
667
- * Dessine une image
668
- * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|ImageBitmap} image - Image à dessiner
669
- * @param {number} sx - Source X (pour le découpage)
670
- * @param {number} sy - Source Y (pour le découpage)
671
- * @param {number} sWidth - Largeur source (pour le découpage)
672
- * @param {number} sHeight - Hauteur source (pour le découpage)
673
- * @param {number} dx - Destination X
674
- * @param {number} dy - Destination Y
675
- * @param {number} dWidth - Largeur destination
676
- * @param {number} dHeight - Hauteur destination
677
- */
678
- _drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) {
679
- // Gestion des différentes signatures
680
- if (arguments.length === 3) {
681
- // drawImage(image, dx, dy)
682
- dx = sx;
683
- dy = sy;
684
- sx = sy = 0;
685
- sWidth = image.width;
686
- sHeight = image.height;
687
- dWidth = sWidth;
688
- dHeight = sHeight;
689
- } else if (arguments.length === 5) {
690
- // drawImage(image, dx, dy, dWidth, dHeight)
691
- dWidth = sWidth;
692
- dHeight = sHeight;
693
- sWidth = image.width;
694
- sHeight = image.height;
695
- sx = sy = 0;
696
- }
697
-
698
- const gl = this.gl;
699
- const matrix = this._getCurrentMatrix();
700
-
701
- // Créer ou réutiliser une texture
702
- let texture = this._textures.get(image);
703
- if (!texture) {
704
- texture = gl.createTexture();
705
- gl.bindTexture(gl.TEXTURE_2D, texture);
706
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
707
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
708
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
709
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
710
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
711
- this._textures.set(image, texture);
712
- }
713
-
714
- // Coordonnées de texture normalisées
715
- const texCoords = new Float32Array([
716
- sx / image.width, sy / image.height,
717
- (sx + sWidth) / image.width, sy / image.height,
718
- sx / image.width, (sy + sHeight) / image.height,
719
- (sx + sWidth) / image.width, (sy + sHeight) / image.height
720
- ]);
721
-
722
- // Vertices du rectangle
723
- const vertices = new Float32Array([
724
- dx, dy,
725
- dx + dWidth, dy,
726
- dx, dy + dHeight,
727
- dx + dWidth, dy + dHeight
728
- ]);
729
-
730
- // Dessiner
731
- this._drawTexturedQuad(vertices, texCoords, texture, matrix);
732
- }
733
-
734
- // ===== GESTION DES CHEMINS (PATHS) =====
735
-
736
- _beginPath() {
737
- this._path.points = [];
738
- this._path.subpaths = [];
739
- this._path.currentSubpath = [];
740
- }
741
-
742
- _moveTo(x, y) {
743
- if (this._path.currentSubpath.length > 0) {
744
- this._path.subpaths.push([...this._path.currentSubpath]);
745
- }
746
- this._path.currentSubpath = [{ x, y, type: 'move' }];
747
- this._path.points.push({ x, y, type: 'move' });
748
- }
749
-
750
- _lineTo(x, y) {
751
- this._path.currentSubpath.push({ x, y, type: 'line' });
752
- this._path.points.push({ x, y, type: 'line' });
753
- }
754
-
755
- _arc(x, y, radius, startAngle, endAngle, anticlockwise = false) {
756
- const segments = Math.max(8, Math.ceil(Math.abs(endAngle - startAngle) * radius / 2));
757
- const angleStep = (endAngle - startAngle) / segments;
758
-
759
- if (!this._path.currentSubpath.length) {
760
- const firstX = x + Math.cos(startAngle) * radius;
761
- const firstY = y + Math.sin(startAngle) * radius;
762
- this._moveTo(firstX, firstY);
763
- }
764
-
765
- for (let i = 1; i <= segments; i++) {
766
- const angle = startAngle + angleStep * i;
767
- const px = x + Math.cos(angle) * radius;
768
- const py = y + Math.sin(angle) * radius;
769
- this._lineTo(px, py);
770
- }
771
- }
772
-
773
- _rect(x, y, width, height) {
774
- this._moveTo(x, y);
775
- this._lineTo(x + width, y);
776
- this._lineTo(x + width, y + height);
777
- this._lineTo(x, y + height);
778
- this._closePath();
122
+ if (!this.useOffscreenCanvas) {
123
+ this.textCanvas.width = 256;
124
+ this.textCanvas.height = 256;
779
125
  }
126
+
127
+ this.textCtx = this.textCanvas.getContext('2d', {
128
+ alpha: true,
129
+ willReadFrequently: false
130
+ });
780
131
 
781
- _closePath() {
782
- if (this._path.currentSubpath.length > 1) {
783
- const first = this._path.currentSubpath[0];
784
- this._lineTo(first.x, first.y);
785
- }
786
- this._path.subpaths.push([...this._path.currentSubpath]);
787
- this._path.currentSubpath = [];
132
+ this.glCanvas = this.useOffscreenCanvas
133
+ ? new OffscreenCanvas(256, 256)
134
+ : document.createElement('canvas');
135
+
136
+ if (!this.useOffscreenCanvas) {
137
+ this.glCanvas.width = 256;
138
+ this.glCanvas.height = 256;
788
139
  }
789
140
 
790
- _fill() {
791
- // Implémentation simplifiée - triangulation du polygone
792
- const points = [];
793
- this._path.subpaths.forEach(subpath => {
794
- subpath.forEach(point => {
795
- if (point.type !== 'move') {
796
- points.push(point.x, point.y);
797
- }
798
- });
799
- });
800
-
801
- if (points.length < 6) return; // Pas assez de points pour un triangle
802
-
803
- // Triangulation simple (pour démonstration)
804
- // En production, utiliser une librairie de triangulation comme earcut
805
- this._fillPolygon(points);
806
- }
141
+ this.gl = this.glCanvas.getContext('webgl', {
142
+ alpha: true,
143
+ premultipliedAlpha: true,
144
+ antialias: false,
145
+ preserveDrawingBuffer: false,
146
+ powerPreference: 'high-performance' // ✅ NOUVEAU
147
+ });
807
148
 
808
- _stroke() {
809
- this._path.subpaths.forEach(subpath => {
810
- for (let i = 1; i < subpath.length; i++) {
811
- const p1 = subpath[i - 1];
812
- const p2 = subpath[i];
813
- if (p1.type !== 'move' && p2.type !== 'move') {
814
- this._strokeLine(p1.x, p1.y, p2.x, p2.y);
815
- }
816
- }
817
- });
149
+ if (!this.gl) {
150
+ throw new Error('WebGL non disponible');
151
+ }
152
+
153
+ this._setupWebGL();
154
+ }
155
+
156
+ _setupWebGL() {
157
+ const gl = this.gl;
158
+
159
+ // Shaders identiques
160
+ const vertexShaderSource = `
161
+ attribute vec2 a_position;
162
+ attribute vec2 a_texCoord;
163
+ uniform vec2 u_resolution;
164
+ varying vec2 v_texCoord;
165
+
166
+ void main() {
167
+ vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0;
168
+ gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
169
+ v_texCoord = a_texCoord;
170
+ }
171
+ `;
172
+
173
+ const fragmentShaderSource = `
174
+ precision mediump float;
175
+ uniform sampler2D u_texture;
176
+ varying vec2 v_texCoord;
177
+
178
+ void main() {
179
+ gl_FragColor = texture2D(u_texture, v_texCoord);
180
+ }
181
+ `;
182
+
183
+ const vertexShader = this._createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
184
+ const fragmentShader = this._createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
185
+
186
+ this.program = gl.createProgram();
187
+ gl.attachShader(this.program, vertexShader);
188
+ gl.attachShader(this.program, fragmentShader);
189
+ gl.linkProgram(this.program);
190
+
191
+ if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
192
+ throw new Error('Erreur de linkage du programme WebGL');
193
+ }
194
+
195
+ this.positionLocation = gl.getAttribLocation(this.program, 'a_position');
196
+ this.texCoordLocation = gl.getAttribLocation(this.program, 'a_texCoord');
197
+ this.resolutionLocation = gl.getUniformLocation(this.program, 'u_resolution');
198
+
199
+ this.positionBuffer = gl.createBuffer();
200
+ this.texCoordBuffer = gl.createBuffer();
201
+
202
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
203
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 1,0, 0,1, 1,1]), gl.STATIC_DRAW);
204
+
205
+ gl.enable(gl.BLEND);
206
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
207
+ }
208
+
209
+ _createShader(gl, type, source) {
210
+ const shader = gl.createShader(type);
211
+ gl.shaderSource(shader, source);
212
+ gl.compileShader(shader);
213
+
214
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
215
+ const info = gl.getShaderInfoLog(shader);
216
+ gl.deleteShader(shader);
217
+ throw new Error('Erreur de compilation shader: ' + info);
818
218
  }
819
-
820
- // ===== TRANSFORMATIONS =====
821
-
822
- _save() {
823
- // Sauvegarder l'état ET la matrice de transformation
824
- this._stateStack.push({ ...this._state });
825
- this._transform.stack.push([...this._transform.matrix]);
219
+
220
+ return shader;
221
+ }
222
+
223
+ // ────────────────────────────────────────────────
224
+ // OPTIMISATION : Text Atlas avec cache de métriques
225
+ // ────────────────────────────────────────────────
226
+ _getFontMetrics(font) {
227
+ if (this.fontMetricsCache.has(font)) {
228
+ return this.fontMetricsCache.get(font);
229
+ }
230
+
231
+ const fontSize = parseFloat(font) || 16;
232
+ const metrics = {
233
+ fontSize,
234
+ lineHeight: fontSize * 1.5,
235
+ padding: 4
236
+ };
237
+
238
+ this.fontMetricsCache.set(font, metrics);
239
+ return metrics;
240
+ }
241
+
242
+ _rasterizeChar(char, font, color) {
243
+ const key = `${char}|${font}|${color}`; // ✅ NOUVEAU : Key plus court
244
+
245
+ if (this.charAtlas.has(key)) {
246
+ this.stats.cacheHits++;
247
+ return this.charAtlas.get(key);
826
248
  }
827
249
 
828
- _restore() {
829
- if (this._stateStack.length > 0) {
830
- this._state = this._stateStack.pop();
831
- }
832
- if (this._transform.stack.length > 0) {
833
- this._transform.matrix = this._transform.stack.pop();
834
- }
835
- }
250
+ this.stats.cacheMisses++;
836
251
 
837
- _translate(x, y) {
838
- const m = this._transform.matrix;
839
- m[4] += m[0] * x + m[2] * y;
840
- m[5] += m[1] * x + m[3] * y;
252
+ const metrics = this._getFontMetrics(font);
253
+ const atlas = this.atlases[this.currentAtlasIndex];
254
+
255
+ atlas.ctx.font = font;
256
+ const textMetrics = atlas.ctx.measureText(char);
257
+
258
+ const width = Math.ceil(textMetrics.width) + metrics.padding;
259
+ const height = Math.ceil(metrics.lineHeight) + metrics.padding;
260
+
261
+ // ✅ NOUVEAU : Gestion intelligente multi-atlas
262
+ if (atlas.x + width > 2048) {
263
+ atlas.x = 0;
264
+ atlas.y += atlas.rowHeight + 2;
265
+ atlas.rowHeight = 0;
266
+ }
267
+
268
+ if (atlas.y + height > 2048) {
269
+ // Créer un nouvel atlas au lieu de clear
270
+ if (this.atlases.length < 4) { // ✅ Maximum 4 atlas
271
+ this.currentAtlasIndex++;
272
+ this.atlases.push(this._createAtlas());
273
+ this.stats.atlasCount++;
274
+ return this._rasterizeChar(char, font, color); // Retry
275
+ } else {
276
+ // Réutiliser l'atlas le moins utilisé
277
+ this.currentAtlasIndex = this._findLeastUsedAtlas();
278
+ this._clearAtlas(this.currentAtlasIndex);
279
+ return this._rasterizeChar(char, font, color);
280
+ }
281
+ }
282
+
283
+ // Dessiner le caractère
284
+ atlas.ctx.font = font;
285
+ atlas.ctx.fillStyle = color;
286
+ atlas.ctx.textBaseline = 'alphabetic';
287
+ atlas.ctx.fillText(char, atlas.x + 2, atlas.y + metrics.fontSize);
288
+
289
+ const charData = {
290
+ atlasIndex: this.currentAtlasIndex,
291
+ x: atlas.x,
292
+ y: atlas.y,
293
+ width,
294
+ height,
295
+ textWidth: textMetrics.width
296
+ };
297
+
298
+ this.charAtlas.set(key, charData);
299
+ atlas.usage++;
300
+
301
+ atlas.x += width + 2;
302
+ atlas.rowHeight = Math.max(atlas.rowHeight, height);
303
+
304
+ return charData;
305
+ }
306
+
307
+ // ✅ NOUVEAU : Trouve l'atlas le moins utilisé
308
+ _findLeastUsedAtlas() {
309
+ let minUsage = Infinity;
310
+ let minIndex = 0;
311
+
312
+ for (let i = 0; i < this.atlases.length; i++) {
313
+ if (this.atlases[i].usage < minUsage) {
314
+ minUsage = this.atlases[i].usage;
315
+ minIndex = i;
316
+ }
841
317
  }
318
+
319
+ return minIndex;
320
+ }
321
+
322
+ // ✅ NOUVEAU : Clear un atlas spécifique
323
+ _clearAtlas(index) {
324
+ const atlas = this.atlases[index];
325
+ atlas.ctx.clearRect(0, 0, 2048, 2048);
326
+ atlas.x = 0;
327
+ atlas.y = 0;
328
+ atlas.rowHeight = 0;
329
+ atlas.usage = 0;
330
+
331
+ // Supprimer les entrées du cache pour cet atlas
332
+ for (let [key, value] of this.charAtlas.entries()) {
333
+ if (value.atlasIndex === index) {
334
+ this.charAtlas.delete(key);
335
+ }
336
+ }
337
+ }
338
+
339
+ // ────────────────────────────────────────────────
340
+ // ✅ OPTIMISATION : Culling amélioré avec marge
341
+ // ────────────────────────────────────────────────
342
+ _isInViewport(x, y, width, height) {
343
+ if (!this.enableCulling) return true;
344
+
345
+ const margin = 50; // ✅ NOUVEAU : Marge pour pré-render
346
+
347
+ return !(
348
+ x + width < -margin ||
349
+ x > this.viewportBounds.right + margin ||
350
+ y + height < -margin ||
351
+ y > this.viewportBounds.bottom + margin
352
+ );
353
+ }
354
+
355
+ // ✅ NOUVEAU : Update viewport bounds
356
+ updateViewport(left = 0, top = 0, right = this.canvas.width, bottom = this.canvas.height) {
357
+ this.viewportBounds = { left, top, right, bottom };
358
+ }
359
+
360
+ // ────────────────────────────────────────────────
361
+ // ✅ OPTIMISATION : Batch Rendering avec auto-flush
362
+ // ────────────────────────────────────────────────
363
+ beginTextBatch() {
364
+ this.batchMode = true;
365
+ this.textBatch = [];
366
+ }
367
+
368
+ flushTextBatch() {
369
+ if (this.textBatch.length === 0) {
370
+ this.batchMode = false;
371
+ return;
372
+ }
373
+
374
+ // ✅ NOUVEAU : Tri par font/color pour réduire les changements d'état
375
+ this.textBatch.sort((a, b) => {
376
+ const keyA = `${a.font}|${a.color}`;
377
+ const keyB = `${b.font}|${b.color}`;
378
+ return keyA.localeCompare(keyB);
379
+ });
842
380
 
843
- _getCurrentMatrix() {
844
- const m = this._transform.matrix;
845
-
846
- // Si pas de transformation, ajouter un scale pour pixels→NDC
847
- if (m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1) {
848
- // Matrice identité modifiée pour scale
849
- return [
850
- 1, 0,
851
- 0, 1,
852
- 0, 0
853
- ];
854
- }
855
-
856
- return m;
857
- }
858
-
859
- _rotate(angle) {
860
- const cos = Math.cos(angle);
861
- const sin = Math.sin(angle);
862
- const m = this._transform.matrix;
863
-
864
- const m0 = m[0], m1 = m[1], m2 = m[2], m3 = m[3];
865
-
866
- m[0] = m0 * cos + m2 * sin;
867
- m[1] = m1 * cos + m3 * sin;
868
- m[2] = m0 * -sin + m2 * cos;
869
- m[3] = m1 * -sin + m3 * cos;
381
+ let lastFont = '';
382
+ let lastColor = '';
383
+
384
+ // Dessiner tous les textes du batch
385
+ for (let item of this.textBatch) {
386
+ // NOUVEAU : Éviter les changements d'état inutiles
387
+ if (item.font !== lastFont) {
388
+ this._currentFont = item.font;
389
+ lastFont = item.font;
390
+ }
391
+ if (item.color !== lastColor) {
392
+ this._currentFillStyle = item.color;
393
+ lastColor = item.color;
394
+ }
395
+
396
+ this._currentTextAlign = item.align;
397
+ this._currentTextBaseline = item.baseline;
398
+
399
+ this._drawTextImmediate(item.text, item.x, item.y);
400
+ }
401
+
402
+ this.stats.batchedDraws += this.textBatch.length;
403
+ this.textBatch = [];
404
+ this.batchMode = false;
405
+ }
406
+
407
+ // ────────────────────────────────────────────────
408
+ // fillText : MÉTHODE PRINCIPALE
409
+ // ────────────────────────────────────────────────
410
+ fillText(text, x, y) {
411
+ if (!text) return;
412
+
413
+ const font = this._currentFont;
414
+ const color = this._currentFillStyle;
415
+ const align = this._currentTextAlign;
416
+ const baseline = this._currentTextBaseline;
417
+
418
+ // Mode batch
419
+ if (this.batchMode) {
420
+ this.textBatch.push({ text, x, y, font, color, align, baseline });
421
+
422
+ // ✅ NOUVEAU : Auto-flush si batch trop grand
423
+ if (this.textBatch.length >= this.maxBatchSize) {
424
+ this.flushTextBatch();
425
+ this.beginTextBatch(); // Redémarrer le batch
426
+ }
427
+ return;
428
+ }
429
+
430
+ this._drawTextImmediate(text, x, y);
431
+ }
432
+
433
+ _drawTextImmediate(text, x, y) {
434
+ const font = this._currentFont;
435
+ const color = this._currentFillStyle;
436
+ const align = this._currentTextAlign;
437
+ const baseline = this._currentTextBaseline;
438
+
439
+ // ✅ Culling optimisé
440
+ const metrics = this._getFontMetrics(font);
441
+ const estimatedWidth = text.length * metrics.fontSize * 0.6;
442
+
443
+ if (!this._isInViewport(x - estimatedWidth/2, y - metrics.fontSize, estimatedWidth, metrics.fontSize * 2)) {
444
+ this.stats.culledTexts++;
445
+ return;
446
+ }
447
+
448
+ // Mode atlas par défaut
449
+ if (this.useTextAtlas) {
450
+ this._drawTextWithAtlas(text, x, y, font, color, align, baseline);
451
+ } else {
452
+ this._drawTextCached(text, x, y, font, color, align, baseline);
453
+ }
454
+
455
+ this.stats.drawCalls++;
456
+ }
457
+
458
+ // ✅ Dessiner avec Text Atlas (optimisé)
459
+ _drawTextWithAtlas(text, x, y, font, color, align, baseline) {
460
+ // ✅ NOUVEAU : Pré-calcul des métriques
461
+ const metrics = this._getFontMetrics(font);
462
+ let totalWidth = 0;
463
+ const chars = Array.from(text); // Support Unicode
464
+ const charData = [];
465
+
466
+ // Phase 1 : Rasterization (peut être mise en cache)
467
+ for (let char of chars) {
468
+ const data = this._rasterizeChar(char, font, color);
469
+ charData.push(data);
470
+ totalWidth += data.textWidth;
471
+ }
472
+
473
+ // Phase 2 : Calcul positions
474
+ let startX = x;
475
+ if (align === 'center') {
476
+ startX -= totalWidth / 2;
477
+ } else if (align === 'right') {
478
+ startX -= totalWidth;
479
+ } else if (align === 'end') {
480
+ startX -= totalWidth; // ✅ Support 'end'
481
+ }
482
+
483
+ const baselineOffset = metrics.fontSize * (this.baselineRatios[baseline] || 0.85);
484
+
485
+ // Phase 3 : Rendu
486
+ let offsetX = 0;
487
+ for (let i = 0; i < chars.length; i++) {
488
+ const data = charData[i];
489
+ const atlas = this.atlases[data.atlasIndex];
490
+
491
+ this.ctx.drawImage(
492
+ atlas.canvas,
493
+ data.x, data.y, data.width, data.height,
494
+ Math.round(startX + offsetX), Math.round(y - baselineOffset),
495
+ data.width, data.height
496
+ );
497
+
498
+ offsetX += data.textWidth;
499
+ }
500
+ }
501
+
502
+ // ✅ Ancien système avec LRU
503
+ _drawTextCached(text, x, y, font, color, align, baseline) {
504
+ const key = `${text}|${font}|${color}|${align}|${baseline}`;
505
+
506
+ // ✅ NOUVEAU : LRU tracking
507
+ this._touchLRU(key);
508
+
509
+ let cached = this.textCache.get(key);
510
+
511
+ if (!cached) {
512
+ const rasterized = this._rasterizeText(text, font, color, align, baseline);
513
+ const texture = this._createWebGLTexture(rasterized.canvas, rasterized.width, rasterized.height);
514
+
515
+ cached = {
516
+ texture,
517
+ width: rasterized.width,
518
+ height: rasterized.height,
519
+ textWidth: rasterized.textWidth,
520
+ baselineOffset: rasterized.baselineOffset,
521
+ createdAt: Date.now()
522
+ };
523
+
524
+ this.textCache.set(key, cached);
525
+
526
+ // ✅ NOUVEAU : Eviction immédiate si trop grand
527
+ if (this.textCache.size > this.maxTextCacheSize) {
528
+ this._scheduleCleanup();
529
+ }
530
+ }
531
+
532
+ let finalX = x - 8;
533
+ if (align === 'center') finalX -= cached.textWidth / 2;
534
+ else if (align === 'right' || align === 'end') finalX -= cached.textWidth;
535
+
536
+ const finalY = y - 8 - cached.baselineOffset;
537
+
538
+ this._drawTextureToCanvas(cached.texture, cached.width, cached.height,
539
+ Math.round(finalX), Math.round(finalY));
540
+ }
541
+
542
+ // ✅ NOUVEAU : LRU tracking
543
+ _touchLRU(key) {
544
+ const index = this.lruKeys.indexOf(key);
545
+ if (index > -1) {
546
+ this.lruKeys.splice(index, 1);
547
+ }
548
+ this.lruKeys.push(key);
549
+ }
550
+
551
+ // ────────────────────────────────────────────────
552
+ // Méthodes auxiliaires
553
+ // ────────────────────────────────────────────────
554
+ _rasterizeText(text, font, color, align, baseline) {
555
+ const metrics = this._getFontMetrics(font);
556
+ this.textCtx.font = font;
557
+ const textMetrics = this.textCtx.measureText(text);
558
+
559
+ const width = Math.ceil(textMetrics.width) + 16;
560
+ const height = Math.ceil(metrics.lineHeight) + 16;
561
+
562
+ // ✅ NOUVEAU : Resize seulement si nécessaire
563
+ if (this.textCanvas.width < width) {
564
+ this.textCanvas.width = Math.min(width, 4096); // ✅ Limite max
565
+ }
566
+ if (this.textCanvas.height < height) {
567
+ this.textCanvas.height = Math.min(height, 4096);
568
+ }
569
+
570
+ this.textCtx.clearRect(0, 0, width, height);
571
+ this.textCtx.font = font;
572
+ this.textCtx.fillStyle = color;
573
+ this.textCtx.textAlign = 'left';
574
+ this.textCtx.textBaseline = 'alphabetic';
575
+
576
+ const offsetY = metrics.fontSize * (this.baselineRatios[baseline] || 0.85);
577
+ this.textCtx.fillText(text, 8, 8 + offsetY);
578
+
579
+ return {
580
+ canvas: this.textCanvas,
581
+ width,
582
+ height,
583
+ textWidth: textMetrics.width,
584
+ baselineOffset: offsetY
585
+ };
586
+ }
587
+
588
+ _createWebGLTexture(canvas, width, height) {
589
+ const gl = this.gl;
590
+ const texture = gl.createTexture();
591
+
592
+ gl.bindTexture(gl.TEXTURE_2D, texture);
593
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
594
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
595
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
596
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
597
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
598
+
599
+ return texture;
600
+ }
601
+
602
+ _drawTextureToCanvas(texture, width, height, x, y) {
603
+ const gl = this.gl;
604
+
605
+ if (this.glCanvas.width !== width || this.glCanvas.height !== height) {
606
+ this.glCanvas.width = width;
607
+ this.glCanvas.height = height;
870
608
  }
871
609
 
872
- _scale(x, y) {
873
- const m = this._transform.matrix;
874
- m[0] *= x;
875
- m[1] *= x;
876
- m[2] *= y;
877
- m[3] *= y;
878
- }
610
+ gl.viewport(0, 0, width, height);
611
+ gl.clearColor(0, 0, 0, 0);
612
+ gl.clear(gl.COLOR_BUFFER_BIT);
613
+ gl.useProgram(this.program);
879
614
 
880
- _transformMethod(a, b, c, d, e, f) {
881
- const m = this._transform.matrix;
882
- const m0 = m[0], m1 = m[1], m2 = m[2], m3 = m[3], m4 = m[4], m5 = m[5];
883
-
884
- m[0] = m0 * a + m2 * b;
885
- m[1] = m1 * a + m3 * b;
886
- m[2] = m0 * c + m2 * d;
887
- m[3] = m1 * c + m3 * d;
888
- m[4] = m0 * e + m2 * f + m4;
889
- m[5] = m1 * e + m3 * f + m5;
890
- }
615
+ gl.activeTexture(gl.TEXTURE0);
616
+ gl.bindTexture(gl.TEXTURE_2D, texture);
891
617
 
892
- _setTransform(a, b, c, d, e, f) {
893
- this._transform.matrix = [a, b, c, d, e, f];
894
- }
618
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
619
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, width,0, 0,height, width,height]), gl.STATIC_DRAW);
895
620
 
896
- // ===== DÉGRADÉS =====
897
-
898
- _createLinearGradient(x0, y0, x1, y1) {
899
- const gradient = {
900
- type: 'linear',
901
- x0, y0, x1, y1,
902
- stops: []
903
- };
904
-
905
- const id = `gradient_${Date.now()}_${Math.random()}`;
906
- this._gradients.set(id, gradient);
907
-
908
- return {
909
- addColorStop: (position, color) => {
910
- gradient.stops.push({ position, color });
911
- },
912
- _id: id
913
- };
914
- }
621
+ gl.enableVertexAttribArray(this.positionLocation);
622
+ gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0);
915
623
 
916
- _createRadialGradient(x0, y0, r0, x1, y1, r1) {
917
- const gradient = {
918
- type: 'radial',
919
- x0, y0, r0, x1, y1, r1,
920
- stops: []
921
- };
922
-
923
- const id = `gradient_${Date.now()}_${Math.random()}`;
924
- this._gradients.set(id, gradient);
925
-
926
- return {
927
- addColorStop: (position, color) => {
928
- gradient.stops.push({ position, color });
929
- },
930
- _id: id
931
- };
932
- }
624
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
625
+ gl.enableVertexAttribArray(this.texCoordLocation);
626
+ gl.vertexAttribPointer(this.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
933
627
 
934
- // ===== MESURE DE TEXTE =====
935
-
936
- _measureText(text) {
937
- const font = this._parseFont(this.font);
938
- const ctx = document.createElement('canvas').getContext('2d');
939
-
940
- // Configurer le contexte temporaire avec la même police
941
- ctx.font = this.font;
942
-
943
- const metrics = ctx.measureText(text);
944
-
945
- // Ajuster selon textBaseline
946
- let actualBoundingBoxAscent = metrics.actualBoundingBoxAscent || font.size;
947
- let actualBoundingBoxDescent = metrics.actualBoundingBoxDescent || 0;
948
-
949
- switch (this.textBaseline) {
950
- case 'top':
951
- metrics.y = actualBoundingBoxAscent;
952
- break;
953
- case 'hanging':
954
- metrics.y = actualBoundingBoxAscent * 0.8;
955
- break;
956
- case 'middle':
957
- metrics.y = (actualBoundingBoxAscent - actualBoundingBoxDescent) / 2;
958
- break;
959
- case 'alphabetic':
960
- metrics.y = 0;
961
- break;
962
- case 'ideographic':
963
- metrics.y = actualBoundingBoxDescent;
964
- break;
965
- case 'bottom':
966
- metrics.y = -actualBoundingBoxDescent;
967
- break;
968
- }
969
-
970
- return metrics;
971
- }
628
+ gl.uniform2f(this.resolutionLocation, width, height);
629
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
972
630
 
973
- // ===== MÉTHODES UTILITAIRES PRIVÉES =====
974
-
975
- /**
976
- * Parse une couleur en RGBA
977
- * @private
978
- */
979
- _parseColor(color) {
980
- // DEBUG: Voir ce qu'on reçoit
981
- console.log('parseColor input:', color, typeof color);
982
-
983
- // Si c'est un gradient (objet avec _id), retourne une couleur fixe
984
- if (color && typeof color === 'object') {
985
- console.warn('Gradient object detected, using fallback color');
986
- // Vous pouvez choisir différentes stratégies :
987
-
988
- // 1. Retourner la première couleur du gradient si disponible
989
- if (color.stops && color.stops.length > 0) {
990
- const firstColor = color.stops[0].color;
991
- if (typeof firstColor === 'string') {
992
- return this._parseColor(firstColor);
993
- }
994
- }
995
-
996
- // 2. Retourner une couleur par défaut basée sur l'ID
997
- if (color._id) {
998
- // Générer une couleur déterministe basée sur l'ID
999
- const hash = this._stringToHash(color._id);
1000
- return [
1001
- (hash % 255) / 255,
1002
- ((hash >> 8) % 255) / 255,
1003
- ((hash >> 16) % 255) / 255,
1004
- 1
1005
- ];
1006
- }
1007
-
1008
- // 3. Couleur de fallback
1009
- return [0.5, 0.5, 0.5, 1]; // Gris
1010
- }
1011
-
1012
- // Si c'est une string, parse normalement
1013
- if (typeof color === 'string') {
1014
- // ... votre parsing de couleur existant ...
1015
- }
1016
-
1017
- // Par défaut : rouge semi-transparent pour debug
1018
- return [1, 0, 0, 0.5];
1019
- }
1020
-
1021
- // Ajoutez cette méthode utilitaire
1022
- _stringToHash(str) {
1023
- let hash = 0;
1024
- for (let i = 0; i < str.length; i++) {
1025
- hash = ((hash << 5) - hash) + str.charCodeAt(i);
1026
- hash = hash & hash; // Convertir en 32-bit integer
1027
- }
1028
- return Math.abs(hash);
1029
- }
1030
-
1031
- /**
1032
- * Convertit une couleur hex en RGBA
1033
- * @private
1034
- */
1035
- _hexToRgb(hex) {
1036
- hex = hex.replace('#', '');
1037
-
1038
- if (hex.length === 3) {
1039
- hex = hex.split('').map(c => c + c).join('');
1040
- }
1041
-
1042
- const r = parseInt(hex.substr(0, 2), 16) / 255;
1043
- const g = parseInt(hex.substr(2, 2), 16) / 255;
1044
- const b = parseInt(hex.substr(4, 2), 16) / 255;
1045
- const a = hex.length === 8 ? parseInt(hex.substr(6, 2), 16) / 255 : 1;
1046
-
1047
- return [r, g, b, a];
1048
- }
631
+ this.ctx.drawImage(this.glCanvas, x, y, width, height);
632
+ }
1049
633
 
1050
- /**
1051
- * Parse la police
1052
- * @private
1053
- */
1054
- _parseFont(font) {
1055
- const match = font.match(/(\d+)px\s+(.+)/);
1056
- return {
1057
- size: match ? parseInt(match[1]) : 10,
1058
- family: match ? match[2] : 'sans-serif'
1059
- };
1060
- }
1061
-
1062
- /**
1063
- * Dessine une ligne avec style
1064
- * @private
1065
- */
1066
- _strokeLine(x1, y1, x2, y2) {
1067
- const gl = this.gl;
1068
- const matrix = this._getCurrentMatrix();
1069
- const color = this._parseColor(this.strokeStyle);
1070
- const lineWidth = this.lineWidth;
1071
-
1072
- // Appliquer l'alpha global
1073
- color[3] *= this.globalAlpha;
1074
-
1075
- // Calculer le vecteur de la ligne
1076
- const dx = x2 - x1;
1077
- const dy = y2 - y1;
1078
- const length = Math.sqrt(dx * dx + dy * dy);
1079
-
1080
- if (length === 0) return;
1081
-
1082
- // Normaliser
1083
- const nx = -dy / length;
1084
- const ny = dx / length;
1085
-
1086
- const halfWidth = lineWidth / 2;
1087
-
1088
- // Créer un quadrilatère pour la ligne
1089
- const vertices = new Float32Array([
1090
- x1 + nx * halfWidth, y1 + ny * halfWidth,
1091
- x1 - nx * halfWidth, y1 - ny * halfWidth,
1092
- x2 + nx * halfWidth, y2 + ny * halfWidth,
1093
- x2 - nx * halfWidth, y2 - ny * halfWidth
1094
- ]);
1095
-
1096
- const colors = new Float32Array([
1097
- ...color, ...color, ...color, ...color
1098
- ]);
1099
-
1100
- const indices = new Uint16Array([0, 1, 2, 2, 1, 3]);
1101
-
1102
- this._drawTriangles(vertices, colors, indices, matrix);
1103
- }
1104
-
1105
- /**
1106
- * Rendu de texte
1107
- * @private
1108
- */
1109
- _renderText(text, x, y, maxWidth, type) {
1110
- // Pour une implémentation complète, il faudrait :
1111
- // 1. Générer une texture atlas de caractères
1112
- // 2. Utiliser un shader de texte
1113
- // 3. Dessiner chaque caractère
1114
-
1115
- // Pour l'instant, on utilise un canvas 2D temporaire
1116
- const tempCanvas = document.createElement('canvas');
1117
- const tempCtx = tempCanvas.getContext('2d');
1118
-
1119
- // Configurer le contexte temporaire
1120
- tempCtx.font = this.font;
1121
- tempCtx.textAlign = this.textAlign;
1122
- tempCtx.textBaseline = this.textBaseline;
1123
-
1124
- // Dessiner le texte
1125
- if (type === 'fill') {
1126
- tempCtx.fillStyle = this.fillStyle;
1127
- tempCtx.fillText(text, 0, 0, maxWidth);
1128
- } else {
1129
- tempCtx.strokeStyle = this.strokeStyle;
1130
- tempCtx.lineWidth = this.lineWidth;
1131
- tempCtx.strokeText(text, 0, 0, maxWidth);
1132
- }
1133
-
1134
- // Obtenir les dimensions
1135
- const metrics = tempCtx.measureText(text);
1136
- const width = metrics.width;
1137
- const height = parseInt(this.font) || 16;
1138
-
1139
- // Ajuster la position selon textAlign et textBaseline
1140
- let drawX = x;
1141
- let drawY = y;
1142
-
1143
- switch (this.textAlign) {
1144
- case 'center':
1145
- drawX -= width / 2;
1146
- break;
1147
- case 'right':
1148
- case 'end':
1149
- drawX -= width;
1150
- break;
1151
- }
1152
-
1153
- switch (this.textBaseline) {
1154
- case 'top':
1155
- case 'hanging':
1156
- drawY += height * 0.8;
1157
- break;
1158
- case 'middle':
1159
- drawY += height / 2;
1160
- break;
1161
- case 'bottom':
1162
- drawY -= height * 0.2;
1163
- break;
1164
- }
1165
-
1166
- // Dessiner l'image générée
1167
- this.drawImage(tempCanvas, drawX, drawY, width, height);
1168
- }
1169
-
1170
- /**
1171
- * Dessine des triangles
1172
- * @private
1173
- */
1174
- _drawTriangles(vertices, colors, indices, matrix) {
1175
- const gl = this.gl;
1176
- const program = this._programs.basic;
1177
- console.log('_drawTriangles called:', {
1178
- vertices: '[' + Array.from(vertices).slice(0, 8).join(', ') + '...]',
1179
- colorsRGBA: Array.from(colors).slice(0, 4), // Affiche R,G,B,A
1180
- colorHex: this._rgbaToHex(Array.from(colors).slice(0, 4)),
1181
- indices: Array.from(indices),
1182
- screenCoords: `Rect at (${vertices[0]}, ${vertices[1]}) to (${vertices[4]}, ${vertices[5]})`
634
+ // ────────────────────────────────────────────────
635
+ // Nettoyage optimisé avec debounce
636
+ // ────────────────────────────────────────────────
637
+ _scheduleCleanup() {
638
+ if (this._cleanupScheduled) return;
639
+
640
+ this._cleanupScheduled = true;
641
+ requestIdleCallback(() => {
642
+ this._cleanOldCache();
643
+ this._cleanupScheduled = false;
644
+ }, { timeout: 1000 });
645
+ }
646
+
647
+ _cleanOldCache() {
648
+ if (this.textCache.size <= this.maxTextCacheSize) return;
649
+
650
+ const gl = this.gl;
651
+ const toRemove = this.textCache.size - this.maxTextCacheSize;
652
+
653
+ // NOUVEAU : Utiliser LRU pour supprimer les moins utilisés
654
+ const keysToRemove = this.lruKeys.splice(0, toRemove);
655
+
656
+ keysToRemove.forEach(key => {
657
+ const entry = this.textCache.get(key);
658
+ if (entry?.texture) {
659
+ gl.deleteTexture(entry.texture);
660
+ }
661
+ this.textCache.delete(key);
1183
662
  });
1184
- if (!program) {
1185
- console.error('Basic shader program not compiled!');
1186
- return;
1187
- }
1188
-
1189
- gl.useProgram(program);
1190
-
1191
- // 1. POSITIONS - utiliser le buffer existant
1192
- const positionBuffer = this._buffers.get('position');
1193
- gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
1194
- gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.DYNAMIC_DRAW);
1195
-
1196
- const positionAttr = gl.getAttribLocation(program, 'a_position');
1197
- gl.enableVertexAttribArray(positionAttr);
1198
- gl.vertexAttribPointer(positionAttr, 2, gl.FLOAT, false, 0, 0);
1199
-
1200
- // 2. COULEURS - utiliser le buffer existant
1201
- const colorBuffer = this._buffers.get('color');
1202
- gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
1203
- gl.bufferData(gl.ARRAY_BUFFER, colors, gl.DYNAMIC_DRAW);
1204
-
1205
- const colorAttr = gl.getAttribLocation(program, 'a_color');
1206
- gl.enableVertexAttribArray(colorAttr);
1207
- gl.vertexAttribPointer(colorAttr, 4, gl.FLOAT, false, 0, 0);
1208
-
1209
- // 3. INDICES - utiliser le buffer existant
1210
- const indexBuffer = this._buffers.get('indices');
1211
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
1212
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.DYNAMIC_DRAW);
1213
-
1214
- // 4. MATRICE
1215
- const matrixUniform = gl.getUniformLocation(program, 'u_matrix');
1216
- if (matrixUniform) {
1217
- const m = matrix || [1, 0, 0, 1, 0, 0];
1218
- gl.uniformMatrix3fv(matrixUniform, false, new Float32Array([
1219
- m[0], m[1], 0,
1220
- m[2], m[3], 0,
1221
- m[4], m[5], 1
1222
- ]));
1223
- }
1224
-
1225
- // 5. RÉSOLUTION
1226
- const resolutionUniform = gl.getUniformLocation(program, 'u_resolution');
1227
- if (resolutionUniform) {
1228
- gl.uniform2f(resolutionUniform, this.width, this.height);
1229
- }
1230
-
1231
- // 6. DESSINER
1232
- gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
1233
-
1234
- // Vérifier les erreurs
1235
- this.checkGLError('drawTriangles');
1236
- }
1237
-
1238
- // Ajoutez cette méthode utilitaire
1239
- _rgbaToHex(rgba) {
1240
- const [r, g, b, a] = rgba;
1241
- const toHex = (n) => Math.round(n * 255).toString(16).padStart(2, '0');
1242
- return `#${toHex(r)}${toHex(g)}${toHex(b)} (alpha: ${a.toFixed(2)})`;
1243
- }
1244
-
1245
- /**
1246
- * Dessine un quadrilatère texturé
1247
- * @private
1248
- */
1249
- _drawTexturedQuad(vertices, texCoords, texture, matrix) {
1250
- const gl = this.gl;
1251
- const program = this._programs.image;
1252
-
1253
- gl.useProgram(program);
1254
-
1255
- // Upload vertices
1256
- const positionBuffer = this._buffers.get('position');
1257
- gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
1258
- gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW);
1259
-
1260
- const positionAttr = gl.getAttribLocation(program, 'a_position');
1261
- gl.enableVertexAttribArray(positionAttr);
1262
- gl.vertexAttribPointer(positionAttr, 2, gl.FLOAT, false, 0, 0);
1263
-
1264
- // Upload texture coordinates
1265
- const texcoordBuffer = this._buffers.get('texcoord');
1266
- gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
1267
- gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STREAM_DRAW);
1268
-
1269
- const texcoordAttr = gl.getAttribLocation(program, 'a_texcoord');
1270
- gl.enableVertexAttribArray(texcoordAttr);
1271
- gl.vertexAttribPointer(texcoordAttr, 2, gl.FLOAT, false, 0, 0);
1272
-
1273
- // Set texture
1274
- gl.activeTexture(gl.TEXTURE0);
1275
- gl.bindTexture(gl.TEXTURE_2D, texture);
1276
- const textureUniform = gl.getUniformLocation(program, 'u_texture');
1277
- gl.uniform1i(textureUniform, 0);
1278
-
1279
- // Set matrix
1280
- const matrixUniform = gl.getUniformLocation(program, 'u_matrix');
1281
- gl.uniformMatrix3fv(matrixUniform, false, new Float32Array([
1282
- matrix[0], matrix[1], 0,
1283
- matrix[2], matrix[3], 0,
1284
- matrix[4], matrix[5], 1
1285
- ]));
1286
-
1287
- // Set alpha
1288
- const alphaUniform = gl.getUniformLocation(program, 'u_alpha');
1289
- gl.uniform1f(alphaUniform, this.globalAlpha);
1290
-
1291
- // Draw as triangle strip
1292
- gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
1293
- }
1294
-
1295
- /**
1296
- * Remplit un polygone
1297
- * @private
1298
- */
1299
- _fillPolygon(points) {
1300
- // Pour une implémentation complète, utiliser une librairie de triangulation
1301
- // Ici, dessin simple pour démonstration
1302
- const gl = this.gl;
1303
-
1304
- if (points.length < 6) return;
1305
-
1306
- // Convertir en vertices
1307
- const vertices = new Float32Array(points);
1308
-
1309
- // Générer des indices pour triangulation simple
1310
- const indices = [];
1311
- for (let i = 1; i < points.length / 2 - 1; i++) {
1312
- indices.push(0, i, i + 1);
1313
- }
1314
-
1315
- const indexArray = new Uint16Array(indices);
1316
-
1317
- // Couleur de remplissage
1318
- const color = this._parseColor(this.fillStyle);
1319
- color[3] *= this.globalAlpha;
1320
-
1321
- const colors = new Float32Array(points.length / 2 * 4);
1322
- for (let i = 0; i < points.length / 2; i++) {
1323
- colors[i * 4] = color[0];
1324
- colors[i * 4 + 1] = color[1];
1325
- colors[i * 4 + 2] = color[2];
1326
- colors[i * 4 + 3] = color[3];
1327
- }
1328
-
1329
- // Dessiner
1330
- this._drawTriangles(vertices, colors, indexArray, this._getCurrentMatrix());
1331
- }
1332
-
1333
- /**
1334
- * Rend un dégradé
1335
- * @private
1336
- */
1337
- _renderGradient(gradientId) {
1338
- // Implémentation simplifiée - retourne la première couleur
1339
- const gradient = this._gradients.get(gradientId);
1340
- if (gradient && gradient.stops.length > 0) {
1341
- return this._parseColor(gradient.stops[0].color);
1342
- }
1343
- return [0, 0, 0, 1];
1344
- }
1345
-
1346
- // ===== MÉTHODES DE GESTION DU CONTEXTE =====
1347
-
1348
- /**
1349
- * Met à jour la matrice de projection pour le DPR
1350
- */
1351
- updateProjectionMatrix() {
1352
- const gl = this.gl;
1353
-
1354
- // La matrice de projection met à l'échelle selon le DPR
1355
- const scaleX = 2 / this.width * this.dpr;
1356
- const scaleY = -2 / this.height * this.dpr; // Inverser l'axe Y
1357
-
1358
- // Pour le shader, nous ajoutons une transformation de base
1359
- // qui convertit des coordonnées pixels en coordonnées NDC
1360
- this._baseMatrix = [
1361
- scaleX, 0, 0,
1362
- 0, scaleY, 0,
1363
- -1, 1, 1
1364
- ];
1365
- }
1366
-
1367
- /**
1368
- * Efface tout le canvas
1369
- */
1370
- clear() {
1371
- this.gl.clear(this.gl.COLOR_BUFFER_BIT);
1372
- }
1373
-
1374
- /**
1375
- * Redimensionne le canvas
1376
- * @param {number} width - Nouvelle largeur
1377
- * @param {number} height - Nouvelle hauteur
1378
- */
1379
- resize(width, height) {
1380
- this.width = width;
1381
- this.height = height;
1382
- this.canvas.width = width * this.dpr;
1383
- this.canvas.height = height * this.dpr;
1384
- this.gl.viewport(0, 0, width * this.dpr, height * this.dpr);
1385
- this.updateProjectionMatrix();
663
+ }
664
+
665
+ // ────────────────────────────────────────────────
666
+ // Stats & utils
667
+ // ────────────────────────────────────────────────
668
+ getStats() {
669
+ return {
670
+ ...this.stats,
671
+ atlasSize: this.charAtlas.size,
672
+ cacheSize: this.textCache.size,
673
+ cacheHitRate: this.stats.cacheHits / (this.stats.cacheHits + this.stats.cacheMisses) || 0,
674
+ atlasCount: this.atlases.length,
675
+ avgAtlasUsage: this.atlases.reduce((sum, a) => sum + a.usage, 0) / this.atlases.length
676
+ };
677
+ }
678
+
679
+ resetStats() {
680
+ this.stats = {
681
+ cacheHits: 0,
682
+ cacheMisses: 0,
683
+ drawCalls: 0,
684
+ culledTexts: 0,
685
+ batchedDraws: 0,
686
+ atlasCount: this.atlases.length
687
+ };
688
+ }
689
+
690
+ // ✅ NOUVEAU : Clear all caches
691
+ clearCaches() {
692
+ this.charAtlas.clear();
693
+ this.fontMetricsCache.clear();
694
+
695
+ const gl = this.gl;
696
+ this.textCache.forEach(entry => {
697
+ if (entry.texture) gl.deleteTexture(entry.texture);
698
+ });
699
+ this.textCache.clear();
700
+ this.lruKeys = [];
701
+
702
+ // Clear all atlases
703
+ this.atlases.forEach((atlas, i) => this._clearAtlas(i));
704
+ this.currentAtlasIndex = 0;
705
+ }
706
+
707
+ // ────────────────────────────────────────────────
708
+ // API Canvas 2D standard
709
+ // ────────────────────────────────────────────────
710
+ measureText(text) {
711
+ const oldFont = this.ctx.font;
712
+ this.ctx.font = this._currentFont;
713
+ const metrics = this.ctx.measureText(text);
714
+ this.ctx.font = oldFont;
715
+ return metrics;
716
+ }
717
+
718
+ set font(value) { this._currentFont = value; this.ctx.font = value; }
719
+ get font() { return this._currentFont; }
720
+ set fillStyle(value) { this._currentFillStyle = value; this.ctx.fillStyle = value; }
721
+ get fillStyle() { return this._currentFillStyle; }
722
+ set textAlign(value) { this._currentTextAlign = value; this.ctx.textAlign = value; }
723
+ get textAlign() { return this._currentTextAlign; }
724
+ set textBaseline(value) { this._currentTextBaseline = value; this.ctx.textBaseline = value; }
725
+ get textBaseline() { return this._currentTextBaseline; }
726
+
727
+ clearRect(...args) { this.ctx.clearRect(...args); }
728
+ fillRect(...args) { this.ctx.fillRect(...args); }
729
+ strokeRect(...args) { this.ctx.strokeRect(...args); }
730
+ beginPath() { this.ctx.beginPath(); }
731
+ moveTo(...args) { this.ctx.moveTo(...args); }
732
+ lineTo(...args) { this.ctx.lineTo(...args); }
733
+ arc(...args) { this.ctx.arc(...args); }
734
+ closePath() { this.ctx.closePath(); }
735
+ fill() { this.ctx.fill(); }
736
+ stroke() { this.ctx.stroke(); }
737
+ drawImage(...args) { this.ctx.drawImage(...args); }
738
+ save() { this.ctx.save(); }
739
+ restore() { this.ctx.restore(); }
740
+ translate(...args) { this.ctx.translate(...args); }
741
+ rotate(...args) { this.ctx.rotate(...args); }
742
+ scale(...args) { this.ctx.scale(...args); }
743
+ createLinearGradient(...args) { return this.ctx.createLinearGradient(...args); }
744
+
745
+ set strokeStyle(value) { this.ctx.strokeStyle = value; }
746
+ get strokeStyle() { return this.ctx.strokeStyle; }
747
+ set lineWidth(value) { this.ctx.lineWidth = value; }
748
+ get lineWidth() { return this.ctx.lineWidth; }
749
+ set globalAlpha(value) { this.ctx.globalAlpha = value; }
750
+ get globalAlpha() { return this.ctx.globalAlpha; }
751
+
752
+ resize(width, height) {
753
+ this.canvas.width = width * this.dpr;
754
+ this.canvas.height = height * this.dpr;
755
+ this.canvas.style.width = `${width}px`;
756
+ this.canvas.style.height = `${height}px`;
757
+ this.ctx.scale(this.dpr, this.dpr);
758
+
759
+ // NOUVEAU : Update viewport
760
+ this.updateViewport(0, 0, width, height);
761
+ }
762
+
763
+ destroy() {
764
+ if (this.gl) {
765
+ const gl = this.gl;
766
+ this.textCache.forEach(entry => {
767
+ if (entry.texture) gl.deleteTexture(entry.texture);
768
+ });
769
+ gl.deleteBuffer(this.positionBuffer);
770
+ gl.deleteBuffer(this.texCoordBuffer);
771
+ gl.deleteProgram(this.program);
1386
772
  }
1387
-
1388
- /**
1389
- * Libère les ressources WebGL
1390
- */
1391
- dispose() {
1392
- const gl = this.gl;
1393
-
1394
- // Supprimer les textures
1395
- this._textures.forEach(texture => {
1396
- gl.deleteTexture(texture);
1397
- });
1398
- this._textures.clear();
1399
-
1400
- // Supprimer les buffers
1401
- this._buffers.forEach(buffer => {
1402
- gl.deleteBuffer(buffer);
1403
- });
1404
- this._buffers.clear();
1405
-
1406
- // Supprimer les programmes
1407
- Object.values(this._programs).forEach(program => {
1408
- if (program) gl.deleteProgram(program);
1409
- });
1410
-
1411
- // Vider les autres ressources
1412
- this._gradients.clear();
1413
- this._shaders.clear();
773
+
774
+ if (this._textCleanupInterval) {
775
+ clearInterval(this._textCleanupInterval);
1414
776
  }
1415
-
1416
- // ===== MÉTHODES DE COMPATIBILITÉ CANVAS 2D =====
1417
-
1418
- /**
1419
- * Interface CanvasRenderingContext2D complète
1420
- * Certaines méthodes sont des no-ops pour la compatibilité
1421
- */
1422
- setLineDash() {} // À implémenter si besoin
1423
- getLineDash() { return []; }
1424
- getImageData() { return { data: new Uint8ClampedArray(0), width: 0, height: 0 }; }
1425
- putImageData() {}
1426
- createImageData() { return { data: new Uint8ClampedArray(0), width: 0, height: 0 }; }
1427
- createPattern() { return {}; }
1428
- isPointInPath() { return false; }
1429
- isPointInStroke() { return false; }
1430
- clip() {}
1431
- resetClip() {}
1432
- getContextAttributes() { return this.options; }
777
+
778
+ this.clearCaches();
779
+ }
1433
780
  }
1434
781
 
1435
782
  export default WebGLCanvasAdapter;