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.
- package/core/CanvasFramework.js +1053 -131
- package/core/WebGLCanvasAdapter.js +747 -1400
- package/package.json +1 -1
- package/utils/AnimationEngine.js +314 -8
|
@@ -1,1435 +1,782 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Adaptateur WebGL pour le rendu
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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
|
-
|
|
578
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
-
|
|
881
|
-
|
|
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
|
-
|
|
893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|