canvasframework 0.5.18 → 0.5.19

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.
Files changed (112) hide show
  1. package/components/Accordion.js +265 -0
  2. package/components/AndroidDatePickerDialog.js +406 -0
  3. package/components/AppBar.js +398 -0
  4. package/components/AudioPlayer.js +611 -0
  5. package/components/Avatar.js +202 -0
  6. package/components/Banner.js +342 -0
  7. package/components/BottomNavigationBar.js +433 -0
  8. package/components/BottomSheet.js +234 -0
  9. package/components/Button.js +358 -0
  10. package/components/Camera.js +644 -0
  11. package/components/Card.js +193 -0
  12. package/components/Chart.js +700 -0
  13. package/components/Checkbox.js +166 -0
  14. package/components/Chip.js +212 -0
  15. package/components/CircularProgress.js +327 -0
  16. package/components/ContextMenu.js +116 -0
  17. package/components/DatePicker.js +298 -0
  18. package/components/Dialog.js +337 -0
  19. package/components/Divider.js +125 -0
  20. package/components/Drawer.js +276 -0
  21. package/components/FAB.js +270 -0
  22. package/components/FileUpload.js +315 -0
  23. package/components/FloatedCamera.js +644 -0
  24. package/components/IOSDatePickerWheel.js +430 -0
  25. package/components/ImageCarousel.js +219 -0
  26. package/components/ImageComponent.js +223 -0
  27. package/components/Input.js +831 -0
  28. package/components/InputDatalist.js +723 -0
  29. package/components/InputTags.js +624 -0
  30. package/components/List.js +95 -0
  31. package/components/ListItem.js +269 -0
  32. package/components/Modal.js +364 -0
  33. package/components/MorphingFAB.js +428 -0
  34. package/components/MultiSelectDialog.js +206 -0
  35. package/components/NumberInput.js +271 -0
  36. package/components/PasswordInput.js +462 -0
  37. package/components/ProgressBar.js +88 -0
  38. package/components/QRCodeReader.js +539 -0
  39. package/components/RadioButton.js +151 -0
  40. package/components/SearchInput.js +315 -0
  41. package/components/SegmentedControl.js +357 -0
  42. package/components/Select.js +199 -0
  43. package/components/SelectDialog.js +255 -0
  44. package/components/Slider.js +113 -0
  45. package/components/SliverAppBar.js +139 -0
  46. package/components/Snackbar.js +243 -0
  47. package/components/SpeedDialFAB.js +397 -0
  48. package/components/Stepper.js +281 -0
  49. package/components/SwipeableListItem.js +327 -0
  50. package/components/Switch.js +147 -0
  51. package/components/Table.js +492 -0
  52. package/components/Tabs.js +423 -0
  53. package/components/Text.js +141 -0
  54. package/components/TextField.js +151 -0
  55. package/components/TimePicker.js +934 -0
  56. package/components/Toast.js +236 -0
  57. package/components/TreeView.js +420 -0
  58. package/components/Video.js +397 -0
  59. package/components/View.js +140 -0
  60. package/components/VirtualList.js +120 -0
  61. package/core/CanvasFramework.js +3045 -0
  62. package/core/Component.js +243 -0
  63. package/core/ThemeManager.js +358 -0
  64. package/core/UIBuilder.js +267 -0
  65. package/core/WebGLCanvasAdapter.js +782 -0
  66. package/features/Column.js +43 -0
  67. package/features/Grid.js +47 -0
  68. package/features/LayoutComponent.js +43 -0
  69. package/features/OpenStreetMap.js +310 -0
  70. package/features/Positioned.js +33 -0
  71. package/features/PullToRefresh.js +328 -0
  72. package/features/Row.js +40 -0
  73. package/features/SignaturePad.js +257 -0
  74. package/features/Skeleton.js +193 -0
  75. package/features/Stack.js +21 -0
  76. package/index.js +119 -0
  77. package/manager/AccessibilityManager.js +107 -0
  78. package/manager/ErrorHandler.js +59 -0
  79. package/manager/FeatureFlags.js +60 -0
  80. package/manager/MemoryManager.js +107 -0
  81. package/manager/PerformanceMonitor.js +84 -0
  82. package/manager/SecurityManager.js +54 -0
  83. package/package.json +22 -16
  84. package/utils/AnimationEngine.js +734 -0
  85. package/utils/CryptoManager.js +303 -0
  86. package/utils/DataStore.js +403 -0
  87. package/utils/DevTools.js +1618 -0
  88. package/utils/DevToolsConsole.js +201 -0
  89. package/utils/EventBus.js +407 -0
  90. package/utils/FetchClient.js +74 -0
  91. package/utils/FirebaseAuth.js +653 -0
  92. package/utils/FirebaseCore.js +246 -0
  93. package/utils/FirebaseFirestore.js +581 -0
  94. package/utils/FirebaseFunctions.js +97 -0
  95. package/utils/FirebaseRealtimeDB.js +498 -0
  96. package/utils/FirebaseStorage.js +612 -0
  97. package/utils/FormValidator.js +355 -0
  98. package/utils/GeoLocationService.js +62 -0
  99. package/utils/I18n.js +207 -0
  100. package/utils/IndexedDBManager.js +273 -0
  101. package/utils/InspectionOverlay.js +308 -0
  102. package/utils/NotificationManager.js +60 -0
  103. package/utils/OfflineSyncManager.js +342 -0
  104. package/utils/PayPalPayment.js +678 -0
  105. package/utils/QueryBuilder.js +478 -0
  106. package/utils/SafeArea.js +64 -0
  107. package/utils/SecureStorage.js +289 -0
  108. package/utils/StateManager.js +207 -0
  109. package/utils/StripePayment.js +552 -0
  110. package/utils/WebSocketClient.js +66 -0
  111. package/dist/canvasframework.js +0 -2
  112. package/dist/canvasframework.js.LICENSE.txt +0 -1
@@ -0,0 +1,782 @@
1
+ /**
2
+ * Adaptateur WebGL pour le rendu de texte ultra-optimisé
3
+ * Version améliorée avec optimisations supplémentaires
4
+ * @class WebGLCanvasAdapter
5
+ */
6
+ class WebGLCanvasAdapter {
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
+ });
17
+
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;
100
+ }
101
+
102
+ const ctx = canvas.getContext('2d', { alpha: true, willReadFrequently: false });
103
+
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');
121
+
122
+ if (!this.useOffscreenCanvas) {
123
+ this.textCanvas.width = 256;
124
+ this.textCanvas.height = 256;
125
+ }
126
+
127
+ this.textCtx = this.textCanvas.getContext('2d', {
128
+ alpha: true,
129
+ willReadFrequently: false
130
+ });
131
+
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;
139
+ }
140
+
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
+ });
148
+
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);
218
+ }
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);
248
+ }
249
+
250
+ this.stats.cacheMisses++;
251
+
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
+ }
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
+ });
380
+
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;
608
+ }
609
+
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);
614
+
615
+ gl.activeTexture(gl.TEXTURE0);
616
+ gl.bindTexture(gl.TEXTURE_2D, texture);
617
+
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);
620
+
621
+ gl.enableVertexAttribArray(this.positionLocation);
622
+ gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0);
623
+
624
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
625
+ gl.enableVertexAttribArray(this.texCoordLocation);
626
+ gl.vertexAttribPointer(this.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
627
+
628
+ gl.uniform2f(this.resolutionLocation, width, height);
629
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
630
+
631
+ this.ctx.drawImage(this.glCanvas, x, y, width, height);
632
+ }
633
+
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);
662
+ });
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);
772
+ }
773
+
774
+ if (this._textCleanupInterval) {
775
+ clearInterval(this._textCleanupInterval);
776
+ }
777
+
778
+ this.clearCaches();
779
+ }
780
+ }
781
+
782
+ export default WebGLCanvasAdapter;