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.
- package/components/Accordion.js +265 -0
- package/components/AndroidDatePickerDialog.js +406 -0
- package/components/AppBar.js +398 -0
- package/components/AudioPlayer.js +611 -0
- package/components/Avatar.js +202 -0
- package/components/Banner.js +342 -0
- package/components/BottomNavigationBar.js +433 -0
- package/components/BottomSheet.js +234 -0
- package/components/Button.js +358 -0
- package/components/Camera.js +644 -0
- package/components/Card.js +193 -0
- package/components/Chart.js +700 -0
- package/components/Checkbox.js +166 -0
- package/components/Chip.js +212 -0
- package/components/CircularProgress.js +327 -0
- package/components/ContextMenu.js +116 -0
- package/components/DatePicker.js +298 -0
- package/components/Dialog.js +337 -0
- package/components/Divider.js +125 -0
- package/components/Drawer.js +276 -0
- package/components/FAB.js +270 -0
- package/components/FileUpload.js +315 -0
- package/components/FloatedCamera.js +644 -0
- package/components/IOSDatePickerWheel.js +430 -0
- package/components/ImageCarousel.js +219 -0
- package/components/ImageComponent.js +223 -0
- package/components/Input.js +831 -0
- package/components/InputDatalist.js +723 -0
- package/components/InputTags.js +624 -0
- package/components/List.js +95 -0
- package/components/ListItem.js +269 -0
- package/components/Modal.js +364 -0
- package/components/MorphingFAB.js +428 -0
- package/components/MultiSelectDialog.js +206 -0
- package/components/NumberInput.js +271 -0
- package/components/PasswordInput.js +462 -0
- package/components/ProgressBar.js +88 -0
- package/components/QRCodeReader.js +539 -0
- package/components/RadioButton.js +151 -0
- package/components/SearchInput.js +315 -0
- package/components/SegmentedControl.js +357 -0
- package/components/Select.js +199 -0
- package/components/SelectDialog.js +255 -0
- package/components/Slider.js +113 -0
- package/components/SliverAppBar.js +139 -0
- package/components/Snackbar.js +243 -0
- package/components/SpeedDialFAB.js +397 -0
- package/components/Stepper.js +281 -0
- package/components/SwipeableListItem.js +327 -0
- package/components/Switch.js +147 -0
- package/components/Table.js +492 -0
- package/components/Tabs.js +423 -0
- package/components/Text.js +141 -0
- package/components/TextField.js +151 -0
- package/components/TimePicker.js +934 -0
- package/components/Toast.js +236 -0
- package/components/TreeView.js +420 -0
- package/components/Video.js +397 -0
- package/components/View.js +140 -0
- package/components/VirtualList.js +120 -0
- package/core/CanvasFramework.js +3045 -0
- package/core/Component.js +243 -0
- package/core/ThemeManager.js +358 -0
- package/core/UIBuilder.js +267 -0
- package/core/WebGLCanvasAdapter.js +782 -0
- package/features/Column.js +43 -0
- package/features/Grid.js +47 -0
- package/features/LayoutComponent.js +43 -0
- package/features/OpenStreetMap.js +310 -0
- package/features/Positioned.js +33 -0
- package/features/PullToRefresh.js +328 -0
- package/features/Row.js +40 -0
- package/features/SignaturePad.js +257 -0
- package/features/Skeleton.js +193 -0
- package/features/Stack.js +21 -0
- package/index.js +119 -0
- package/manager/AccessibilityManager.js +107 -0
- package/manager/ErrorHandler.js +59 -0
- package/manager/FeatureFlags.js +60 -0
- package/manager/MemoryManager.js +107 -0
- package/manager/PerformanceMonitor.js +84 -0
- package/manager/SecurityManager.js +54 -0
- package/package.json +22 -16
- package/utils/AnimationEngine.js +734 -0
- package/utils/CryptoManager.js +303 -0
- package/utils/DataStore.js +403 -0
- package/utils/DevTools.js +1618 -0
- package/utils/DevToolsConsole.js +201 -0
- package/utils/EventBus.js +407 -0
- package/utils/FetchClient.js +74 -0
- package/utils/FirebaseAuth.js +653 -0
- package/utils/FirebaseCore.js +246 -0
- package/utils/FirebaseFirestore.js +581 -0
- package/utils/FirebaseFunctions.js +97 -0
- package/utils/FirebaseRealtimeDB.js +498 -0
- package/utils/FirebaseStorage.js +612 -0
- package/utils/FormValidator.js +355 -0
- package/utils/GeoLocationService.js +62 -0
- package/utils/I18n.js +207 -0
- package/utils/IndexedDBManager.js +273 -0
- package/utils/InspectionOverlay.js +308 -0
- package/utils/NotificationManager.js +60 -0
- package/utils/OfflineSyncManager.js +342 -0
- package/utils/PayPalPayment.js +678 -0
- package/utils/QueryBuilder.js +478 -0
- package/utils/SafeArea.js +64 -0
- package/utils/SecureStorage.js +289 -0
- package/utils/StateManager.js +207 -0
- package/utils/StripePayment.js +552 -0
- package/utils/WebSocketClient.js +66 -0
- package/dist/canvasframework.js +0 -2
- 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;
|