canvasframework 0.3.6

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 (85) hide show
  1. package/README.md +554 -0
  2. package/components/Accordion.js +252 -0
  3. package/components/AndroidDatePickerDialog.js +398 -0
  4. package/components/AppBar.js +225 -0
  5. package/components/Avatar.js +202 -0
  6. package/components/BottomNavigationBar.js +205 -0
  7. package/components/BottomSheet.js +374 -0
  8. package/components/Button.js +225 -0
  9. package/components/Card.js +193 -0
  10. package/components/Checkbox.js +180 -0
  11. package/components/Chip.js +212 -0
  12. package/components/CircularProgress.js +143 -0
  13. package/components/ContextMenu.js +116 -0
  14. package/components/DatePicker.js +257 -0
  15. package/components/Dialog.js +367 -0
  16. package/components/Divider.js +125 -0
  17. package/components/Drawer.js +261 -0
  18. package/components/FAB.js +270 -0
  19. package/components/FileUpload.js +315 -0
  20. package/components/IOSDatePickerWheel.js +268 -0
  21. package/components/ImageCarousel.js +193 -0
  22. package/components/ImageComponent.js +223 -0
  23. package/components/Input.js +309 -0
  24. package/components/List.js +94 -0
  25. package/components/ListItem.js +223 -0
  26. package/components/Modal.js +364 -0
  27. package/components/MultiSelectDialog.js +206 -0
  28. package/components/NumberInput.js +271 -0
  29. package/components/ProgressBar.js +88 -0
  30. package/components/RadioButton.js +142 -0
  31. package/components/SearchInput.js +315 -0
  32. package/components/SegmentedControl.js +202 -0
  33. package/components/Select.js +199 -0
  34. package/components/SelectDialog.js +255 -0
  35. package/components/Slider.js +113 -0
  36. package/components/Snackbar.js +243 -0
  37. package/components/Stepper.js +281 -0
  38. package/components/SwipeableListItem.js +179 -0
  39. package/components/Switch.js +147 -0
  40. package/components/Table.js +492 -0
  41. package/components/Tabs.js +125 -0
  42. package/components/Text.js +141 -0
  43. package/components/TextField.js +331 -0
  44. package/components/Toast.js +236 -0
  45. package/components/TreeView.js +420 -0
  46. package/components/Video.js +397 -0
  47. package/components/View.js +140 -0
  48. package/components/VirtualList.js +120 -0
  49. package/core/CanvasFramework.js +1271 -0
  50. package/core/CanvasWork.js +32 -0
  51. package/core/Component.js +153 -0
  52. package/core/LogicWorker.js +25 -0
  53. package/core/WebGLCanvasAdapter.js +1369 -0
  54. package/features/Column.js +43 -0
  55. package/features/Grid.js +47 -0
  56. package/features/LayoutComponent.js +43 -0
  57. package/features/OpenStreetMap.js +310 -0
  58. package/features/Positioned.js +33 -0
  59. package/features/PullToRefresh.js +328 -0
  60. package/features/Row.js +40 -0
  61. package/features/SignaturePad.js +257 -0
  62. package/features/Skeleton.js +84 -0
  63. package/features/Stack.js +21 -0
  64. package/index.js +101 -0
  65. package/manager/AccessibilityManager.js +107 -0
  66. package/manager/ErrorHandler.js +59 -0
  67. package/manager/FeatureFlags.js +60 -0
  68. package/manager/MemoryManager.js +107 -0
  69. package/manager/PerformanceMonitor.js +84 -0
  70. package/manager/SecurityManager.js +54 -0
  71. package/package.json +28 -0
  72. package/utils/AnimationEngine.js +428 -0
  73. package/utils/DataStore.js +403 -0
  74. package/utils/EventBus.js +407 -0
  75. package/utils/FetchClient.js +74 -0
  76. package/utils/FormValidator.js +355 -0
  77. package/utils/GeoLocationService.js +62 -0
  78. package/utils/I18n.js +207 -0
  79. package/utils/IndexedDBManager.js +273 -0
  80. package/utils/OfflineSyncManager.js +342 -0
  81. package/utils/QueryBuilder.js +478 -0
  82. package/utils/SafeArea.js +64 -0
  83. package/utils/SecureStorage.js +289 -0
  84. package/utils/StateManager.js +207 -0
  85. package/utils/WebSocketClient.js +66 -0
@@ -0,0 +1,1369 @@
1
+ /**
2
+ * Adaptateur WebGL amélioré qui émule l'API Canvas 2D
3
+ * Version complète avec toutes les API Canvas 2D
4
+ */
5
+ class WebGLCanvasAdapter {
6
+ constructor(canvas) {
7
+ this.canvas = canvas;
8
+ this.gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
9
+
10
+ if (!this.gl) {
11
+ throw new Error('WebGL non supporté');
12
+ }
13
+
14
+ // État du contexte (comme Canvas 2D)
15
+ this.state = {
16
+ fillStyle: '#000000',
17
+ strokeStyle: '#000000',
18
+ lineWidth: 1,
19
+ font: '10px sans-serif',
20
+ textAlign: 'start',
21
+ textBaseline: 'alphabetic',
22
+ globalAlpha: 1,
23
+ shadowColor: 'transparent',
24
+ shadowBlur: 0,
25
+ shadowOffsetX: 0,
26
+ shadowOffsetY: 0,
27
+ transform: [1, 0, 0, 1, 0, 0],
28
+ clipPath: null,
29
+ lineCap: 'butt',
30
+ lineJoin: 'miter',
31
+ miterLimit: 10,
32
+ filter: 'none',
33
+ lineDash: [],
34
+ lineDashOffset: 0,
35
+ direction: 'ltr'
36
+ };
37
+
38
+ this.stateStack = [];
39
+
40
+ // Système de batching
41
+ this.batch = {
42
+ vertices: [],
43
+ colors: [],
44
+ texCoords: [],
45
+ indices: [],
46
+ currentTexture: null,
47
+ textureVertices: [],
48
+ textureTexCoords: [],
49
+ textureIndices: [],
50
+ elementOffset: 0,
51
+ textureElementOffset: 0
52
+ };
53
+
54
+ // Canvas offscreen pour le texte et les textures
55
+ this.textCanvas = document.createElement('canvas');
56
+ this.textCtx = this.textCanvas.getContext('2d');
57
+ this.textCanvas.width = 2048;
58
+ this.textCanvas.height = 2048;
59
+ this.textAtlas = {
60
+ canvas: this.textCanvas,
61
+ ctx: this.textCtx,
62
+ currentX: 0,
63
+ currentY: 0,
64
+ lineHeight: 0,
65
+ cache: new Map()
66
+ };
67
+
68
+ // Cache de textures pour le texte (avec LRU)
69
+ this.textCache = new Map();
70
+ this.textureLRU = [];
71
+ this.maxTextureCacheSize = 100;
72
+
73
+ // Cache des gradients
74
+ this.gradients = new Map();
75
+
76
+ // Buffer pour les images
77
+ this.imageCache = new Map();
78
+
79
+ // État du path courant
80
+ this.currentPath = null;
81
+ this.currentSubpath = [];
82
+ this.currentPoint = null;
83
+
84
+ // Mode batch (true par défaut pour performance)
85
+ this.batchEnabled = true;
86
+
87
+ this.initWebGL();
88
+ }
89
+
90
+ // ===== API CANVAS 2D COMPLÈTE =====
91
+
92
+ // --- Sauvegarde et restauration d'état ---
93
+ save() {
94
+ this.stateStack.push({
95
+ fillStyle: this.state.fillStyle,
96
+ strokeStyle: this.state.strokeStyle,
97
+ lineWidth: this.state.lineWidth,
98
+ font: this.state.font,
99
+ textAlign: this.state.textAlign,
100
+ textBaseline: this.state.textBaseline,
101
+ globalAlpha: this.state.globalAlpha,
102
+ shadowColor: this.state.shadowColor,
103
+ shadowBlur: this.state.shadowBlur,
104
+ shadowOffsetX: this.state.shadowOffsetX,
105
+ shadowOffsetY: this.state.shadowOffsetY,
106
+ transform: [...this.state.transform],
107
+ clipPath: this.state.clipPath ? [...this.state.clipPath] : null,
108
+ lineCap: this.state.lineCap,
109
+ lineJoin: this.state.lineJoin,
110
+ miterLimit: this.state.miterLimit,
111
+ filter: this.state.filter,
112
+ lineDash: [...this.state.lineDash],
113
+ lineDashOffset: this.state.lineDashOffset,
114
+ direction: this.state.direction
115
+ });
116
+ }
117
+
118
+ restore() {
119
+ if (this.stateStack.length === 0) return;
120
+
121
+ const savedState = this.stateStack.pop();
122
+ Object.assign(this.state, savedState);
123
+
124
+ if (this.state.clipPath) {
125
+ this.applyClip();
126
+ }
127
+ }
128
+
129
+ // --- Transformations ---
130
+ setTransform(a, b, c, d, e, f) {
131
+ this.state.transform = [a, b, c, d, e, f];
132
+ }
133
+
134
+ resetTransform() {
135
+ this.state.transform = [1, 0, 0, 1, 0, 0];
136
+ }
137
+
138
+ transform(a, b, c, d, e, f) {
139
+ const [a1, b1, c1, d1, e1, f1] = this.state.transform;
140
+
141
+ this.state.transform = [
142
+ a1 * a + c1 * b,
143
+ b1 * a + d1 * b,
144
+ a1 * c + c1 * d,
145
+ b1 * c + d1 * d,
146
+ a1 * e + c1 * f + e1,
147
+ b1 * e + d1 * f + f1
148
+ ];
149
+ }
150
+
151
+ translate(x, y) {
152
+ this.transform(1, 0, 0, 1, x, y);
153
+ }
154
+
155
+ rotate(angle) {
156
+ const cos = Math.cos(angle);
157
+ const sin = Math.sin(angle);
158
+ this.transform(cos, sin, -sin, cos, 0, 0);
159
+ }
160
+
161
+ scale(x, y) {
162
+ this.transform(x, 0, 0, y, 0, 0);
163
+ }
164
+
165
+ getTransform() {
166
+ const [a, b, c, d, e, f] = this.state.transform;
167
+ return new DOMMatrix([a, b, c, d, e, f]);
168
+ }
169
+
170
+ // --- Text ---
171
+ createTextAtlas() {
172
+ const gl = this.gl;
173
+
174
+ this.textAtlasTexture = gl.createTexture();
175
+ gl.bindTexture(gl.TEXTURE_2D, this.textAtlasTexture);
176
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.textCanvas.width, this.textCanvas.height,
177
+ 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
178
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
179
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
180
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
181
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
182
+
183
+ this.textCtx.fillStyle = 'rgba(0,0,0,0)';
184
+ this.textCtx.fillRect(0, 0, this.textCanvas.width, this.textCanvas.height);
185
+ }
186
+
187
+ getTextGlyph(text, font, color = '#000000') {
188
+ const cacheKey = `${text}_${font}_${color}`;
189
+
190
+ if (this.textAtlas.cache.has(cacheKey)) {
191
+ return this.textAtlas.cache.get(cacheKey);
192
+ }
193
+
194
+ this.textCtx.save();
195
+ this.textCtx.font = font;
196
+ this.textCtx.fillStyle = color;
197
+ this.textCtx.textAlign = 'left';
198
+ this.textCtx.textBaseline = 'alphabetic';
199
+
200
+ const metrics = this.textCtx.measureText(text);
201
+ const textWidth = Math.ceil(metrics.width);
202
+ const textHeight = Math.ceil(parseInt(font) || 16);
203
+
204
+ if (this.textAtlas.currentX + textWidth > this.textCanvas.width) {
205
+ this.textAtlas.currentX = 0;
206
+ this.textAtlas.currentY += this.textAtlas.lineHeight + 2;
207
+ this.textAtlas.lineHeight = 0;
208
+ }
209
+
210
+ if (this.textAtlas.currentY + textHeight > this.textCanvas.height) {
211
+ this.textAtlas.currentX = 0;
212
+ this.textAtlas.currentY = 0;
213
+ this.textAtlas.lineHeight = 0;
214
+
215
+ this.textCtx.fillStyle = 'rgba(0,0,0,0)';
216
+ this.textCtx.fillRect(0, 0, this.textCanvas.width, this.textCanvas.height);
217
+ this.textAtlas.cache.clear();
218
+ }
219
+
220
+ this.textCtx.fillText(text, this.textAtlas.currentX, this.textAtlas.currentY + textHeight);
221
+
222
+ const texX = this.textAtlas.currentX / this.textCanvas.width;
223
+ const texY = this.textAtlas.currentY / this.textCanvas.height;
224
+ const texWidth = textWidth / this.textCanvas.width;
225
+ const texHeight = textHeight / this.textCanvas.height;
226
+
227
+ const glyph = {
228
+ x: this.textAtlas.currentX,
229
+ y: this.textAtlas.currentY,
230
+ width: textWidth,
231
+ height: textHeight,
232
+ texX, texY, texWidth, texHeight,
233
+ bearingY: metrics.actualBoundingBoxAscent || textHeight,
234
+ metrics: metrics
235
+ };
236
+
237
+ this.textAtlas.cache.set(cacheKey, glyph);
238
+ this.textAtlas.currentX += textWidth + 2;
239
+ this.textAtlas.lineHeight = Math.max(this.textAtlas.lineHeight, textHeight);
240
+ this.updateTextAtlasTexture();
241
+ this.textCtx.restore();
242
+
243
+ return glyph;
244
+ }
245
+
246
+ updateTextAtlasTexture() {
247
+ const gl = this.gl;
248
+ gl.bindTexture(gl.TEXTURE_2D, this.textAtlasTexture);
249
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.textCanvas);
250
+ }
251
+
252
+ fillText(text, x, y, maxWidth) {
253
+ const glyph = this.getTextGlyph(text, this.state.font, this.state.fillStyle);
254
+
255
+ let drawX = x;
256
+ if (this.state.textAlign === 'center') {
257
+ drawX -= glyph.width / 2;
258
+ } else if (this.state.textAlign === 'right' || this.state.textAlign === 'end') {
259
+ drawX -= glyph.width;
260
+ }
261
+
262
+ let drawY = y;
263
+ if (this.state.textBaseline === 'top') {
264
+ drawY += glyph.bearingY;
265
+ } else if (this.state.textBaseline === 'middle') {
266
+ drawY += glyph.bearingY / 2;
267
+ } else if (this.state.textBaseline === 'bottom' || this.state.textBaseline === 'ideographic' || this.state.textBaseline === 'hanging') {
268
+ drawY -= (glyph.height - glyph.bearingY);
269
+ }
270
+
271
+ let scale = 1;
272
+ if (maxWidth && maxWidth < glyph.width) {
273
+ scale = maxWidth / glyph.width;
274
+ }
275
+
276
+ this.drawTexturedQuad(
277
+ this.textAtlasTexture,
278
+ drawX,
279
+ drawY - glyph.bearingY,
280
+ glyph.width * scale,
281
+ glyph.height * scale,
282
+ [
283
+ glyph.texX, glyph.texY,
284
+ glyph.texX + glyph.texWidth, glyph.texY,
285
+ glyph.texX + glyph.texWidth, glyph.texY + glyph.texHeight,
286
+ glyph.texX, glyph.texY + glyph.texHeight
287
+ ]
288
+ );
289
+ }
290
+
291
+ strokeText(text, x, y, maxWidth) {
292
+ const lineWidth = this.state.lineWidth;
293
+ const offsets = [
294
+ [-lineWidth, -lineWidth], [-lineWidth, 0], [-lineWidth, lineWidth],
295
+ [0, -lineWidth], [0, lineWidth],
296
+ [lineWidth, -lineWidth], [lineWidth, 0], [lineWidth, lineWidth]
297
+ ];
298
+
299
+ for (const [offsetX, offsetY] of offsets) {
300
+ this.fillText(text, x + offsetX, y + offsetY, maxWidth);
301
+ }
302
+ }
303
+
304
+ measureText(text) {
305
+ this.textCtx.save();
306
+ this.textCtx.font = this.state.font;
307
+ const metrics = this.textCtx.measureText(text);
308
+ this.textCtx.restore();
309
+
310
+ return {
311
+ width: metrics.width,
312
+ actualBoundingBoxAscent: metrics.actualBoundingBoxAscent || 0,
313
+ actualBoundingBoxDescent: metrics.actualBoundingBoxDescent || 0,
314
+ actualBoundingBoxLeft: metrics.actualBoundingBoxLeft || 0,
315
+ actualBoundingBoxRight: metrics.actualBoundingBoxRight || 0,
316
+ fontBoundingBoxAscent: metrics.fontBoundingBoxAscent || 0,
317
+ fontBoundingBoxDescent: metrics.fontBoundingBoxDescent || 0,
318
+ emHeightAscent: metrics.emHeightAscent || 0,
319
+ emHeightDescent: metrics.emHeightDescent || 0,
320
+ alphabeticBaseline: metrics.alphabeticBaseline || 0,
321
+ hangingBaseline: metrics.hangingBaseline || 0,
322
+ ideographicBaseline: metrics.ideographicBaseline || 0
323
+ };
324
+ }
325
+
326
+ // --- Paths ---
327
+ beginPath() {
328
+ this.currentPath = [];
329
+ this.currentSubpath = [];
330
+ this.currentPoint = null;
331
+ }
332
+
333
+ moveTo(x, y) {
334
+ if (!this.currentPath) this.beginPath();
335
+ this.currentSubpath = [[x, y]];
336
+ this.currentPath.push([x, y]);
337
+ this.currentPoint = [x, y];
338
+ }
339
+
340
+ lineTo(x, y) {
341
+ if (!this.currentPoint) {
342
+ this.moveTo(x, y);
343
+ return;
344
+ }
345
+ this.currentSubpath.push([x, y]);
346
+ this.currentPath.push([x, y]);
347
+ this.currentPoint = [x, y];
348
+ }
349
+
350
+ closePath() {
351
+ if (this.currentSubpath.length > 0) {
352
+ const firstPoint = this.currentSubpath[0];
353
+ this.lineTo(firstPoint[0], firstPoint[1]);
354
+ }
355
+ this.currentSubpath = [];
356
+ }
357
+
358
+ arc(x, y, radius, startAngle, endAngle, anticlockwise = false) {
359
+ if (!this.currentPoint) {
360
+ this.moveTo(x + Math.cos(startAngle) * radius, y + Math.sin(startAngle) * radius);
361
+ }
362
+
363
+ const angleStep = (endAngle - startAngle) / 32;
364
+ const direction = anticlockwise ? -1 : 1;
365
+
366
+ for (let i = 1; i <= 32; i++) {
367
+ const angle = startAngle + direction * i * angleStep;
368
+ const px = x + Math.cos(angle) * radius;
369
+ const py = y + Math.sin(angle) * radius;
370
+ this.lineTo(px, py);
371
+ }
372
+ }
373
+
374
+ arcTo(x1, y1, x2, y2, radius) {
375
+ if (!this.currentPoint) return;
376
+
377
+ const [x0, y0] = this.currentPoint;
378
+ const v01 = { x: x1 - x0, y: y1 - y0 };
379
+ const v12 = { x: x2 - x1, y: y2 - y1 };
380
+
381
+ const len01 = Math.sqrt(v01.x * v01.x + v01.y * v01.y);
382
+ const len12 = Math.sqrt(v12.x * v12.x + v12.y * v12.y);
383
+
384
+ const norm01 = { x: v01.x / len01, y: v01.y / len01 };
385
+ const norm12 = { x: v12.x / len12, y: v12.y / len12 };
386
+
387
+ const angle = Math.acos(norm01.x * norm12.x + norm01.y * norm12.y);
388
+ const distance = radius / Math.tan(angle / 2);
389
+
390
+ const p1 = { x: x1 - norm01.x * distance, y: y1 - norm01.y * distance };
391
+ const p2 = { x: x1 + norm12.x * distance, y: y1 + norm12.y * distance };
392
+
393
+ const centerX = p1.x + norm01.y * radius * (norm01.x * norm12.y - norm01.y * norm12.x > 0 ? 1 : -1);
394
+ const centerY = p1.y - norm01.x * radius * (norm01.x * norm12.y - norm01.y * norm12.x > 0 ? 1 : -1);
395
+
396
+ const startAngle = Math.atan2(p1.y - centerY, p1.x - centerX);
397
+ const endAngle = Math.atan2(p2.y - centerY, p2.x - centerX);
398
+
399
+ this.lineTo(p1.x, p1.y);
400
+ this.arc(centerX, centerY, radius, startAngle, endAngle, norm01.x * norm12.y - norm01.y * norm12.x < 0);
401
+ }
402
+
403
+ ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise = false) {
404
+ if (!this.currentPoint) {
405
+ const px = x + Math.cos(startAngle) * Math.cos(rotation) * radiusX - Math.sin(startAngle) * Math.sin(rotation) * radiusY;
406
+ const py = y + Math.cos(startAngle) * Math.sin(rotation) * radiusX + Math.sin(startAngle) * Math.cos(rotation) * radiusY;
407
+ this.moveTo(px, py);
408
+ }
409
+
410
+ const segments = Math.ceil(32 * Math.max(radiusX, radiusY) / 10);
411
+ const angleStep = (endAngle - startAngle) / segments;
412
+ const direction = anticlockwise ? -1 : 1;
413
+
414
+ for (let i = 1; i <= segments; i++) {
415
+ const angle = startAngle + direction * i * angleStep;
416
+ const px = x + Math.cos(angle) * Math.cos(rotation) * radiusX - Math.sin(angle) * Math.sin(rotation) * radiusY;
417
+ const py = y + Math.cos(angle) * Math.sin(rotation) * radiusX + Math.sin(angle) * Math.cos(rotation) * radiusY;
418
+ this.lineTo(px, py);
419
+ }
420
+ }
421
+
422
+ rect(x, y, width, height) {
423
+ this.moveTo(x, y);
424
+ this.lineTo(x + width, y);
425
+ this.lineTo(x + width, y + height);
426
+ this.lineTo(x, y + height);
427
+ this.closePath();
428
+ }
429
+
430
+ bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
431
+ if (!this.currentPoint) {
432
+ this.currentPoint = [x, y];
433
+ return;
434
+ }
435
+
436
+ const [x0, y0] = this.currentPoint;
437
+ const segments = 20;
438
+
439
+ for (let i = 1; i <= segments; i++) {
440
+ const t = i / segments;
441
+ const t1 = 1 - t;
442
+
443
+ const bx = t1 * t1 * t1 * x0 +
444
+ 3 * t1 * t1 * t * cp1x +
445
+ 3 * t1 * t * t * cp2x +
446
+ t * t * t * x;
447
+
448
+ const by = t1 * t1 * t1 * y0 +
449
+ 3 * t1 * t1 * t * cp1y +
450
+ 3 * t1 * t * t * cp2y +
451
+ t * t * t * y;
452
+
453
+ this.lineTo(bx, by);
454
+ }
455
+ }
456
+
457
+ quadraticCurveTo(cpx, cpy, x, y) {
458
+ if (!this.currentPoint) {
459
+ this.currentPoint = [x, y];
460
+ return;
461
+ }
462
+
463
+ const [x0, y0] = this.currentPoint;
464
+ const segments = 20;
465
+
466
+ for (let i = 1; i <= segments; i++) {
467
+ const t = i / segments;
468
+ const t1 = 1 - t;
469
+
470
+ const qx = t1 * t1 * x0 + 2 * t1 * t * cpx + t * t * x;
471
+ const qy = t1 * t1 * y0 + 2 * t1 * t * cpy + t * t * y;
472
+
473
+ this.lineTo(qx, qy);
474
+ }
475
+ }
476
+
477
+ // --- Drawing ---
478
+ fill(fillRule = 'nonzero') {
479
+ if (!this.currentPath || this.currentPath.length < 3) return;
480
+
481
+ const triangles = this.triangulatePolygon(this.currentPath, fillRule);
482
+ const color = this.parseColor(this.state.fillStyle);
483
+ const alpha = color[3] * this.state.globalAlpha;
484
+
485
+ const vertices = [];
486
+ const colors = [];
487
+
488
+ for (const triangle of triangles) {
489
+ for (const point of triangle) {
490
+ const [px, py] = this.transformPoint(point[0], point[1]);
491
+ vertices.push(px, py);
492
+ colors.push(color[0], color[1], color[2], alpha);
493
+ }
494
+ }
495
+
496
+ this.drawTriangles(vertices, colors);
497
+ }
498
+
499
+ stroke() {
500
+ if (!this.currentPath || this.currentPath.length < 2) return;
501
+
502
+ const color = this.parseColor(this.state.strokeStyle);
503
+ const alpha = color[3] * this.state.globalAlpha;
504
+ const lineWidth = this.state.lineWidth;
505
+
506
+ for (let i = 0; i < this.currentPath.length - 1; i++) {
507
+ const p1 = this.currentPath[i];
508
+ const p2 = this.currentPath[i + 1];
509
+
510
+ this.drawLine(p1[0], p1[1], p2[0], p2[1], lineWidth, color, alpha);
511
+ }
512
+ }
513
+
514
+ clip(fillRule = 'nonzero') {
515
+ if (!this.currentPath) return;
516
+
517
+ this.state.clipPath = [...this.currentPath];
518
+ this.applyClip();
519
+ }
520
+
521
+ isPointInPath(x, y, fillRule = 'nonzero') {
522
+ if (!this.currentPath || this.currentPath.length < 3) return false;
523
+
524
+ let wn = 0;
525
+ const points = this.currentPath;
526
+
527
+ for (let i = 0; i < points.length; i++) {
528
+ const p1 = points[i];
529
+ const p2 = points[(i + 1) % points.length];
530
+
531
+ if (p1[1] <= y) {
532
+ if (p2[1] > y && this.isLeft(p1, p2, [x, y]) > 0) {
533
+ wn++;
534
+ }
535
+ } else {
536
+ if (p2[1] <= y && this.isLeft(p1, p2, [x, y]) < 0) {
537
+ wn--;
538
+ }
539
+ }
540
+ }
541
+
542
+ if (fillRule === 'nonzero') {
543
+ return wn !== 0;
544
+ } else {
545
+ return Math.abs(wn % 2) === 1;
546
+ }
547
+ }
548
+
549
+ isPointInStroke(x, y) {
550
+ if (!this.currentPath || this.currentPath.length < 2) return false;
551
+
552
+ const lineWidth = this.state.lineWidth;
553
+
554
+ for (let i = 0; i < this.currentPath.length - 1; i++) {
555
+ const p1 = this.currentPath[i];
556
+ const p2 = this.currentPath[i + 1];
557
+
558
+ if (this.isPointNearLine(x, y, p1[0], p1[1], p2[0], p2[1], lineWidth)) {
559
+ return true;
560
+ }
561
+ }
562
+
563
+ return false;
564
+ }
565
+
566
+ // --- Drawing rectangles ---
567
+ clearRect(x, y, width, height) {
568
+ const [x1, y1] = this.transformPoint(x, y);
569
+ const [x2, y2] = this.transformPoint(x + width, y + height);
570
+
571
+ const vertices = [
572
+ x1, y1, x2, y1, x2, y2,
573
+ x1, y1, x2, y2, x1, y2
574
+ ];
575
+
576
+ const colors = new Array(6 * 4).fill(0);
577
+ for (let i = 0; i < 6; i++) {
578
+ colors[i * 4 + 3] = 1;
579
+ }
580
+
581
+ this.drawTriangles(vertices, colors);
582
+ }
583
+
584
+ fillRect(x, y, width, height) {
585
+ this.rect(x, y, width, height);
586
+ this.fill();
587
+ }
588
+
589
+ strokeRect(x, y, width, height) {
590
+ this.rect(x, y, width, height);
591
+ this.stroke();
592
+ }
593
+
594
+ // --- Images ---
595
+ drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) {
596
+ if (arguments.length === 3) {
597
+ dx = sx; dy = sy;
598
+ sWidth = dWidth = image.width;
599
+ sHeight = dHeight = image.height;
600
+ sx = sy = 0;
601
+ } else if (arguments.length === 5) {
602
+ dx = sx; dy = sy;
603
+ dWidth = sWidth; dHeight = sHeight;
604
+ sx = sy = 0;
605
+ sWidth = image.width;
606
+ sHeight = image.height;
607
+ }
608
+
609
+ const texture = this.getImageTexture(image);
610
+ if (!texture) return;
611
+
612
+ const texWidth = image.width || texture.width;
613
+ const texHeight = image.height || texture.height;
614
+
615
+ const texCoords = [
616
+ sx / texWidth, sy / texHeight,
617
+ (sx + sWidth) / texWidth, sy / texHeight,
618
+ (sx + sWidth) / texWidth, (sy + sHeight) / texHeight,
619
+ sx / texWidth, (sy + sHeight) / texHeight
620
+ ];
621
+
622
+ this.drawTexturedQuad(texture, dx, dy, dWidth, dHeight, texCoords);
623
+ }
624
+
625
+ createImageData(width, height) {
626
+ return {
627
+ width,
628
+ height,
629
+ data: new Uint8ClampedArray(width * height * 4)
630
+ };
631
+ }
632
+
633
+ getImageData(sx, sy, sw, sh) {
634
+ const gl = this.gl;
635
+ const pixels = new Uint8Array(sw * sh * 4);
636
+ gl.readPixels(sx, this.canvas.height - sy - sh, sw, sh, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
637
+
638
+ const imageData = this.createImageData(sw, sh);
639
+
640
+ for (let y = 0; y < sh; y++) {
641
+ for (let x = 0; x < sw; x++) {
642
+ const srcIdx = ((sh - 1 - y) * sw + x) * 4;
643
+ const dstIdx = (y * sw + x) * 4;
644
+
645
+ imageData.data[dstIdx] = pixels[srcIdx];
646
+ imageData.data[dstIdx + 1] = pixels[srcIdx + 1];
647
+ imageData.data[dstIdx + 2] = pixels[srcIdx + 2];
648
+ imageData.data[dstIdx + 3] = pixels[srcIdx + 3];
649
+ }
650
+ }
651
+
652
+ return imageData;
653
+ }
654
+
655
+ putImageData(imageData, dx, dy, dirtyX = 0, dirtyY = 0, dirtyWidth = imageData.width, dirtyHeight = imageData.height) {
656
+ const gl = this.gl;
657
+ const texture = gl.createTexture();
658
+ gl.bindTexture(gl.TEXTURE_2D, texture);
659
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, imageData.width, imageData.height, 0,
660
+ gl.RGBA, gl.UNSIGNED_BYTE, imageData.data);
661
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
662
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
663
+
664
+ this.drawTexturedQuad(texture, dx + dirtyX, dy + dirtyY, dirtyWidth, dirtyHeight);
665
+ gl.deleteTexture(texture);
666
+ }
667
+
668
+ // --- Gradients and patterns ---
669
+ createLinearGradient(x0, y0, x1, y1) {
670
+ const gradient = {
671
+ type: 'linear',
672
+ x0, y0, x1, y1,
673
+ stops: [],
674
+ texture: null,
675
+ dirty: true
676
+ };
677
+
678
+ const id = `gradient_${Date.now()}_${Math.random()}`;
679
+ this.gradients.set(id, gradient);
680
+
681
+ return {
682
+ addColorStop: (position, color) => {
683
+ gradient.stops.push({ position, color });
684
+ gradient.dirty = true;
685
+ },
686
+ _id: id
687
+ };
688
+ }
689
+
690
+ createRadialGradient(x0, y0, r0, x1, y1, r1) {
691
+ const gradient = {
692
+ type: 'radial',
693
+ x0, y0, r0, x1, y1, r1,
694
+ stops: [],
695
+ texture: null,
696
+ dirty: true
697
+ };
698
+
699
+ const id = `gradient_${Date.now()}_${Math.random()}`;
700
+ this.gradients.set(id, gradient);
701
+
702
+ return {
703
+ addColorStop: (position, color) => {
704
+ gradient.stops.push({ position, color });
705
+ gradient.dirty = true;
706
+ },
707
+ _id: id
708
+ };
709
+ }
710
+
711
+ createPattern(image, repetition) {
712
+ const pattern = {
713
+ image,
714
+ repetition: repetition || 'repeat',
715
+ texture: null
716
+ };
717
+
718
+ return {
719
+ _pattern: pattern
720
+ };
721
+ }
722
+
723
+ // --- Properties ---
724
+ set fillStyle(value) { this.state.fillStyle = value; }
725
+ get fillStyle() { return this.state.fillStyle; }
726
+
727
+ set strokeStyle(value) { this.state.strokeStyle = value; }
728
+ get strokeStyle() { return this.state.strokeStyle; }
729
+
730
+ set lineWidth(value) { this.state.lineWidth = value; }
731
+ get lineWidth() { return this.state.lineWidth; }
732
+
733
+ set font(value) { this.state.font = value; }
734
+ get font() { return this.state.font; }
735
+
736
+ set textAlign(value) { this.state.textAlign = value; }
737
+ get textAlign() { return this.state.textAlign; }
738
+
739
+ set textBaseline(value) { this.state.textBaseline = value; }
740
+ get textBaseline() { return this.state.textBaseline; }
741
+
742
+ set globalAlpha(value) { this.state.globalAlpha = Math.max(0, Math.min(1, value)); }
743
+ get globalAlpha() { return this.state.globalAlpha; }
744
+
745
+ set shadowColor(value) { this.state.shadowColor = value; }
746
+ get shadowColor() { return this.state.shadowColor; }
747
+
748
+ set shadowBlur(value) { this.state.shadowBlur = value; }
749
+ get shadowBlur() { return this.state.shadowBlur; }
750
+
751
+ set shadowOffsetX(value) { this.state.shadowOffsetX = value; }
752
+ get shadowOffsetX() { return this.state.shadowOffsetX; }
753
+
754
+ set shadowOffsetY(value) { this.state.shadowOffsetY = value; }
755
+ get shadowOffsetY() { return this.state.shadowOffsetY; }
756
+
757
+ set lineCap(value) { this.state.lineCap = value; }
758
+ get lineCap() { return this.state.lineCap; }
759
+
760
+ set lineJoin(value) { this.state.lineJoin = value; }
761
+ get lineJoin() { return this.state.lineJoin; }
762
+
763
+ set miterLimit(value) { this.state.miterLimit = value; }
764
+ get miterLimit() { return this.state.miterLimit; }
765
+
766
+ set filter(value) { this.state.filter = value; }
767
+ get filter() { return this.state.filter; }
768
+
769
+ getLineDash() { return [...this.state.lineDash]; }
770
+ setLineDash(segments) { this.state.lineDash = [...segments]; }
771
+
772
+ getLineDashOffset() { return this.state.lineDashOffset; }
773
+ setLineDashOffset(value) { this.state.lineDashOffset = value; }
774
+
775
+ set direction(value) { this.state.direction = value; }
776
+ get direction() { return this.state.direction; }
777
+
778
+ // --- WebGL specific ---
779
+ getContextAttributes() {
780
+ return {
781
+ alpha: true,
782
+ depth: false,
783
+ stencil: false,
784
+ antialias: true,
785
+ premultipliedAlpha: true,
786
+ preserveDrawingBuffer: false,
787
+ powerPreference: 'default',
788
+ desynchronized: false
789
+ };
790
+ }
791
+
792
+ // --- Méthodes utilitaires ---
793
+ drawFocusIfNeeded(element) {
794
+ // Pas d'implémentation pour WebGL
795
+ }
796
+
797
+ scrollPathIntoView() {
798
+ // Pas d'implémentation pour WebGL
799
+ }
800
+
801
+ // ===== MÉTHODES WEBGL INTERNES =====
802
+
803
+ initWebGL() {
804
+ const gl = this.gl;
805
+
806
+ // Shaders
807
+ const vsSolidSource = `
808
+ attribute vec2 aPosition;
809
+ attribute vec4 aColor;
810
+ uniform mat3 uProjectionMatrix;
811
+ uniform mat3 uTransformMatrix;
812
+ varying vec4 vColor;
813
+
814
+ void main() {
815
+ vec2 pos = (uTransformMatrix * vec3(aPosition, 1.0)).xy;
816
+ vec2 ndc = (uProjectionMatrix * vec3(pos, 1.0)).xy;
817
+ gl_Position = vec4(ndc, 0.0, 1.0);
818
+ vColor = aColor;
819
+ }
820
+ `;
821
+
822
+ const fsSolidSource = `
823
+ precision mediump float;
824
+ varying vec4 vColor;
825
+
826
+ void main() {
827
+ gl_FragColor = vColor;
828
+ }
829
+ `;
830
+
831
+ const vsTextureSource = `
832
+ attribute vec2 aPosition;
833
+ attribute vec2 aTexCoord;
834
+ uniform mat3 uProjectionMatrix;
835
+ uniform mat3 uTransformMatrix;
836
+ varying vec2 vTexCoord;
837
+
838
+ void main() {
839
+ vec2 pos = (uTransformMatrix * vec3(aPosition, 1.0)).xy;
840
+ vec2 ndc = (uProjectionMatrix * vec3(pos, 1.0)).xy;
841
+ gl_Position = vec4(ndc, 0.0, 1.0);
842
+ vTexCoord = aTexCoord;
843
+ }
844
+ `;
845
+
846
+ const fsTextureSource = `
847
+ precision mediump float;
848
+ varying vec2 vTexCoord;
849
+ uniform sampler2D uTexture;
850
+ uniform float uAlpha;
851
+ uniform vec4 uTintColor;
852
+
853
+ void main() {
854
+ vec4 texColor = texture2D(uTexture, vTexCoord);
855
+ float alpha = texColor.a * uAlpha;
856
+ gl_FragColor = vec4(mix(texColor.rgb, uTintColor.rgb, uTintColor.a), alpha);
857
+ }
858
+ `;
859
+
860
+ this.solidProgram = this.createProgram(vsSolidSource, fsSolidSource);
861
+ this.textureProgram = this.createProgram(vsTextureSource, fsTextureSource);
862
+
863
+ // Buffers
864
+ this.positionBuffer = gl.createBuffer();
865
+ this.colorBuffer = gl.createBuffer();
866
+ this.texCoordBuffer = gl.createBuffer();
867
+ this.indexBuffer = gl.createBuffer();
868
+
869
+ // VAOs
870
+ this.solidVAO = gl.createVertexArray();
871
+ gl.bindVertexArray(this.solidVAO);
872
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
873
+ const posLoc = gl.getAttribLocation(this.solidProgram, 'aPosition');
874
+ gl.enableVertexAttribArray(posLoc);
875
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
876
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
877
+ const colorLoc = gl.getAttribLocation(this.solidProgram, 'aColor');
878
+ gl.enableVertexAttribArray(colorLoc);
879
+ gl.vertexAttribPointer(colorLoc, 4, gl.FLOAT, false, 0, 0);
880
+ gl.bindVertexArray(null);
881
+
882
+ this.textureVAO = gl.createVertexArray();
883
+ gl.bindVertexArray(this.textureVAO);
884
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
885
+ const texPosLoc = gl.getAttribLocation(this.textureProgram, 'aPosition');
886
+ gl.enableVertexAttribArray(texPosLoc);
887
+ gl.vertexAttribPointer(texPosLoc, 2, gl.FLOAT, false, 0, 0);
888
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
889
+ const texCoordLoc = gl.getAttribLocation(this.textureProgram, 'aTexCoord');
890
+ gl.enableVertexAttribArray(texCoordLoc);
891
+ gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 0, 0);
892
+ gl.bindVertexArray(null);
893
+
894
+ // Configuration WebGL
895
+ gl.enable(gl.BLEND);
896
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
897
+ gl.enable(gl.SCISSOR_TEST);
898
+
899
+ this.updateProjectionMatrix();
900
+ this.createTextAtlas();
901
+ }
902
+
903
+ createProgram(vsSource, fsSource) {
904
+ const gl = this.gl;
905
+ const vs = this.compileShader(gl.VERTEX_SHADER, vsSource);
906
+ const fs = this.compileShader(gl.FRAGMENT_SHADER, fsSource);
907
+
908
+ const program = gl.createProgram();
909
+ gl.attachShader(program, vs);
910
+ gl.attachShader(program, fs);
911
+ gl.linkProgram(program);
912
+
913
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
914
+ console.error('Erreur link program:', gl.getProgramInfoLog(program));
915
+ return null;
916
+ }
917
+
918
+ return program;
919
+ }
920
+
921
+ compileShader(type, source) {
922
+ const gl = this.gl;
923
+ const shader = gl.createShader(type);
924
+ gl.shaderSource(shader, source);
925
+ gl.compileShader(shader);
926
+
927
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
928
+ console.error('Erreur compilation shader:', gl.getShaderInfoLog(shader));
929
+ gl.deleteShader(shader);
930
+ return null;
931
+ }
932
+
933
+ return shader;
934
+ }
935
+
936
+ updateProjectionMatrix() {
937
+ const w = this.canvas.width;
938
+ const h = this.canvas.height;
939
+
940
+ this.projectionMatrix = new Float32Array([
941
+ 2/w, 0, 0,
942
+ 0, -2/h, 0,
943
+ -1, 1, 1
944
+ ]);
945
+ }
946
+
947
+ flush() {
948
+ if (!this.batchEnabled) return;
949
+
950
+ const gl = this.gl;
951
+
952
+ if (this.batch.vertices.length > 0) {
953
+ gl.useProgram(this.solidProgram);
954
+ gl.bindVertexArray(this.solidVAO);
955
+
956
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
957
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.vertices), gl.STATIC_DRAW);
958
+
959
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
960
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.colors), gl.STATIC_DRAW);
961
+
962
+ const projLoc = gl.getUniformLocation(this.solidProgram, 'uProjectionMatrix');
963
+ gl.uniformMatrix3fv(projLoc, false, this.projectionMatrix);
964
+
965
+ const transformLoc = gl.getUniformLocation(this.solidProgram, 'uTransformMatrix');
966
+ gl.uniformMatrix3fv(transformLoc, false, new Float32Array(this.state.transform));
967
+
968
+ gl.drawArrays(gl.TRIANGLES, 0, this.batch.vertices.length / 2);
969
+
970
+ this.batch.vertices = [];
971
+ this.batch.colors = [];
972
+ }
973
+
974
+ if (this.batch.textureVertices.length > 0 && this.batch.currentTexture) {
975
+ gl.useProgram(this.textureProgram);
976
+ gl.bindVertexArray(this.textureVAO);
977
+
978
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
979
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.textureVertices), gl.STATIC_DRAW);
980
+
981
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
982
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.batch.textureTexCoords), gl.STATIC_DRAW);
983
+
984
+ const projLoc = gl.getUniformLocation(this.textureProgram, 'uProjectionMatrix');
985
+ gl.uniformMatrix3fv(projLoc, false, this.projectionMatrix);
986
+
987
+ const transformLoc = gl.getUniformLocation(this.textureProgram, 'uTransformMatrix');
988
+ gl.uniformMatrix3fv(transformLoc, false, new Float32Array(this.state.transform));
989
+
990
+ const alphaLoc = gl.getUniformLocation(this.textureProgram, 'uAlpha');
991
+ gl.uniform1f(alphaLoc, this.state.globalAlpha);
992
+
993
+ const tintLoc = gl.getUniformLocation(this.textureProgram, 'uTintColor');
994
+ gl.uniform4f(tintLoc, 0, 0, 0, 0);
995
+
996
+ gl.activeTexture(gl.TEXTURE0);
997
+ gl.bindTexture(gl.TEXTURE_2D, this.batch.currentTexture);
998
+ gl.uniform1i(gl.getUniformLocation(this.textureProgram, 'uTexture'), 0);
999
+
1000
+ gl.drawArrays(gl.TRIANGLES, 0, this.batch.textureVertices.length / 2);
1001
+
1002
+ this.batch.textureVertices = [];
1003
+ this.batch.textureTexCoords = [];
1004
+ this.batch.currentTexture = null;
1005
+ }
1006
+ }
1007
+
1008
+ drawTriangles(vertices, colors) {
1009
+ if (this.batchEnabled) {
1010
+ const baseIndex = this.batch.vertices.length / 2;
1011
+
1012
+ this.batch.vertices.push(...vertices);
1013
+ this.batch.colors.push(...colors);
1014
+
1015
+ if (this.batch.vertices.length >= 6000) {
1016
+ this.flush();
1017
+ }
1018
+ } else {
1019
+ const gl = this.gl;
1020
+ gl.useProgram(this.solidProgram);
1021
+
1022
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
1023
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
1024
+
1025
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
1026
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
1027
+
1028
+ const posLoc = gl.getAttribLocation(this.solidProgram, 'aPosition');
1029
+ gl.enableVertexAttribArray(posLoc);
1030
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
1031
+
1032
+ const colorLoc = gl.getAttribLocation(this.solidProgram, 'aColor');
1033
+ gl.enableVertexAttribArray(colorLoc);
1034
+ gl.vertexAttribPointer(colorLoc, 4, gl.FLOAT, false, 0, 0);
1035
+
1036
+ const projLoc = gl.getUniformLocation(this.solidProgram, 'uProjectionMatrix');
1037
+ gl.uniformMatrix3fv(projLoc, false, this.projectionMatrix);
1038
+
1039
+ const transformLoc = gl.getUniformLocation(this.solidProgram, 'uTransformMatrix');
1040
+ gl.uniformMatrix3fv(transformLoc, false, new Float32Array(this.state.transform));
1041
+
1042
+ gl.drawArrays(gl.TRIANGLES, 0, vertices.length / 2);
1043
+ }
1044
+ }
1045
+
1046
+ drawTexturedQuad(texture, x, y, width, height, texCoords = null) {
1047
+ if (this.batchEnabled) {
1048
+ if (this.batch.currentTexture && this.batch.currentTexture !== texture) {
1049
+ this.flush();
1050
+ }
1051
+
1052
+ this.batch.currentTexture = texture;
1053
+
1054
+ const [x1, y1] = this.transformPoint(x, y);
1055
+ const [x2, y2] = this.transformPoint(x + width, y + height);
1056
+
1057
+ const vertices = [
1058
+ x1, y1, x2, y1, x2, y2,
1059
+ x1, y1, x2, y2, x1, y2
1060
+ ];
1061
+
1062
+ const uvs = texCoords || [
1063
+ 0, 0, 1, 0, 1, 1,
1064
+ 0, 0, 1, 1, 0, 1
1065
+ ];
1066
+
1067
+ this.batch.textureVertices.push(...vertices);
1068
+ this.batch.textureTexCoords.push(...uvs);
1069
+
1070
+ if (this.batch.textureVertices.length >= 6000) {
1071
+ this.flush();
1072
+ }
1073
+ } else {
1074
+ const gl = this.gl;
1075
+ gl.useProgram(this.textureProgram);
1076
+
1077
+ const [x1, y1] = this.transformPoint(x, y);
1078
+ const [x2, y2] = this.transformPoint(x + width, y + height);
1079
+
1080
+ const vertices = [
1081
+ x1, y1, x2, y1, x2, y2, x1, y1, x2, y2, x1, y2
1082
+ ];
1083
+
1084
+ const uvs = texCoords || [
1085
+ 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1
1086
+ ];
1087
+
1088
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
1089
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
1090
+
1091
+ const posLoc = gl.getAttribLocation(this.textureProgram, 'aPosition');
1092
+ gl.enableVertexAttribArray(posLoc);
1093
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
1094
+
1095
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
1096
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(uvs), gl.STATIC_DRAW);
1097
+
1098
+ const texLoc = gl.getAttribLocation(this.textureProgram, 'aTexCoord');
1099
+ gl.enableVertexAttribArray(texLoc);
1100
+ gl.vertexAttribPointer(texLoc, 2, gl.FLOAT, false, 0, 0);
1101
+
1102
+ const projLoc = gl.getUniformLocation(this.textureProgram, 'uProjectionMatrix');
1103
+ gl.uniformMatrix3fv(projLoc, false, this.projectionMatrix);
1104
+
1105
+ const transformLoc = gl.getUniformLocation(this.textureProgram, 'uTransformMatrix');
1106
+ gl.uniformMatrix3fv(transformLoc, false, new Float32Array(this.state.transform));
1107
+
1108
+ const alphaLoc = gl.getUniformLocation(this.textureProgram, 'uAlpha');
1109
+ gl.uniform1f(alphaLoc, this.state.globalAlpha);
1110
+
1111
+ gl.activeTexture(gl.TEXTURE0);
1112
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1113
+ gl.uniform1i(gl.getUniformLocation(this.textureProgram, 'uTexture'), 0);
1114
+
1115
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
1116
+ }
1117
+ }
1118
+
1119
+ getImageTexture(image) {
1120
+ if (!image) return null;
1121
+
1122
+ const cacheKey = image.src || image._id || 'canvas_' + Date.now();
1123
+
1124
+ if (this.imageCache.has(cacheKey)) {
1125
+ return this.imageCache.get(cacheKey);
1126
+ }
1127
+
1128
+ const gl = this.gl;
1129
+ const texture = gl.createTexture();
1130
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1131
+
1132
+ if (image instanceof HTMLImageElement || image instanceof HTMLCanvasElement) {
1133
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
1134
+ } else if (image instanceof ImageData) {
1135
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
1136
+ gl.RGBA, gl.UNSIGNED_BYTE, image.data);
1137
+ } else if (image instanceof ImageBitmap) {
1138
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
1139
+ }
1140
+
1141
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1142
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1143
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1144
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1145
+
1146
+ const textureInfo = { texture, width: image.width || 0, height: image.height || 0 };
1147
+ this.imageCache.set(cacheKey, textureInfo);
1148
+
1149
+ if (this.imageCache.size > 50) {
1150
+ const firstKey = this.imageCache.keys().next().value;
1151
+ const oldTex = this.imageCache.get(firstKey);
1152
+ gl.deleteTexture(oldTex.texture);
1153
+ this.imageCache.delete(firstKey);
1154
+ }
1155
+
1156
+ return textureInfo;
1157
+ }
1158
+
1159
+ applyClip() {
1160
+ if (!this.state.clipPath) {
1161
+ this.gl.disable(this.gl.SCISSOR_TEST);
1162
+ return;
1163
+ }
1164
+
1165
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1166
+
1167
+ for (const point of this.state.clipPath) {
1168
+ const [px, py] = this.transformPoint(point[0], point[1]);
1169
+ minX = Math.min(minX, px);
1170
+ minY = Math.min(minY, py);
1171
+ maxX = Math.max(maxX, px);
1172
+ maxY = Math.max(maxY, py);
1173
+ }
1174
+
1175
+ const width = Math.max(0, maxX - minX);
1176
+ const height = Math.max(0, maxY - minY);
1177
+
1178
+ this.gl.enable(this.gl.SCISSOR_TEST);
1179
+ this.gl.scissor(minX, this.canvas.height - minY - height, width, height);
1180
+ }
1181
+
1182
+ // ===== MÉTHODES UTILITAIRES =====
1183
+
1184
+ transformPoint(x, y) {
1185
+ const [a, b, c, d, e, f] = this.state.transform;
1186
+ return [
1187
+ a * x + c * y + e,
1188
+ b * x + d * y + f
1189
+ ];
1190
+ }
1191
+
1192
+ parseColor(color) {
1193
+ if (typeof color !== 'string') {
1194
+ return [0, 0, 0, 1];
1195
+ }
1196
+
1197
+ if (color._id && this.gradients.has(color._id)) {
1198
+ return [0, 0, 0, 1];
1199
+ }
1200
+
1201
+ if (color._pattern) {
1202
+ return [0, 0, 0, 1];
1203
+ }
1204
+
1205
+ if (color.startsWith('#')) {
1206
+ if (color.length === 4) {
1207
+ const r = parseInt(color[1], 16) / 15;
1208
+ const g = parseInt(color[2], 16) / 15;
1209
+ const b = parseInt(color[3], 16) / 15;
1210
+ return [r, g, b, 1];
1211
+ } else if (color.length === 5) {
1212
+ const r = parseInt(color[1], 16) / 15;
1213
+ const g = parseInt(color[2], 16) / 15;
1214
+ const b = parseInt(color[3], 16) / 15;
1215
+ const a = parseInt(color[4], 16) / 15;
1216
+ return [r, g, b, a];
1217
+ } else if (color.length === 7) {
1218
+ const r = parseInt(color.substr(1, 2), 16) / 255;
1219
+ const g = parseInt(color.substr(3, 2), 16) / 255;
1220
+ const b = parseInt(color.substr(5, 2), 16) / 255;
1221
+ return [r, g, b, 1];
1222
+ } else if (color.length === 9) {
1223
+ const r = parseInt(color.substr(1, 2), 16) / 255;
1224
+ const g = parseInt(color.substr(3, 2), 16) / 255;
1225
+ const b = parseInt(color.substr(5, 2), 16) / 255;
1226
+ const a = parseInt(color.substr(7, 2), 16) / 255;
1227
+ return [r, g, b, a];
1228
+ }
1229
+ }
1230
+
1231
+ const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
1232
+ if (rgbMatch) {
1233
+ const r = parseInt(rgbMatch[1]) / 255;
1234
+ const g = parseInt(rgbMatch[2]) / 255;
1235
+ const b = parseInt(rgbMatch[3]) / 255;
1236
+ const a = rgbMatch[4] ? parseFloat(rgbMatch[4]) : 1;
1237
+ return [r, g, b, a];
1238
+ }
1239
+
1240
+ const namedColors = {
1241
+ 'black': [0, 0, 0, 1], 'white': [1, 1, 1, 1], 'red': [1, 0, 0, 1],
1242
+ 'green': [0, 1, 0, 1], 'blue': [0, 0, 1, 1], 'yellow': [1, 1, 0, 1],
1243
+ 'cyan': [0, 1, 1, 1], 'magenta': [1, 0, 1, 1], 'gray': [0.5, 0.5, 0.5, 1],
1244
+ 'grey': [0.5, 0.5, 0.5, 1], 'transparent': [0, 0, 0, 0]
1245
+ };
1246
+
1247
+ return namedColors[color.toLowerCase()] || [0, 0, 0, 1];
1248
+ }
1249
+
1250
+ triangulatePolygon(polygon, fillRule) {
1251
+ const triangles = [];
1252
+ const vertices = [...polygon];
1253
+
1254
+ while (vertices.length > 3) {
1255
+ for (let i = 0; i < vertices.length; i++) {
1256
+ const prev = vertices[(i - 1 + vertices.length) % vertices.length];
1257
+ const curr = vertices[i];
1258
+ const next = vertices[(i + 1) % vertices.length];
1259
+
1260
+ if (this.isEar(prev, curr, next, vertices, fillRule)) {
1261
+ triangles.push([prev, curr, next]);
1262
+ vertices.splice(i, 1);
1263
+ break;
1264
+ }
1265
+ }
1266
+ }
1267
+
1268
+ if (vertices.length === 3) {
1269
+ triangles.push(vertices);
1270
+ }
1271
+
1272
+ return triangles;
1273
+ }
1274
+
1275
+ isEar(a, b, c, polygon, fillRule) {
1276
+ const cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
1277
+ if (fillRule === 'nonzero' && cross <= 0) return false;
1278
+ if (fillRule === 'evenodd' && Math.abs(cross) < 0.001) return false;
1279
+
1280
+ const triangle = [a, b, c];
1281
+ for (const p of polygon) {
1282
+ if (p !== a && p !== b && p !== c && this.isPointInTriangle(p, triangle)) {
1283
+ return false;
1284
+ }
1285
+ }
1286
+
1287
+ return true;
1288
+ }
1289
+
1290
+ isPointInTriangle(p, triangle) {
1291
+ const [a, b, c] = triangle;
1292
+ const area = 0.5 * (-b[1] * c[0] + a[1] * (-b[0] + c[0]) + a[0] * (b[1] - c[1]) + b[0] * c[1]);
1293
+ const s = 1 / (2 * area) * (a[1] * c[0] - a[0] * c[1] + (c[1] - a[1]) * p[0] + (a[0] - c[0]) * p[1]);
1294
+ const t = 1 / (2 * area) * (a[0] * b[1] - a[1] * b[0] + (a[1] - b[1]) * p[0] + (b[0] - a[0]) * p[1]);
1295
+ return s > 0 && t > 0 && 1 - s - t > 0;
1296
+ }
1297
+
1298
+ isLeft(p1, p2, p3) {
1299
+ return (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p3[0] - p1[0]) * (p2[1] - p1[1]);
1300
+ }
1301
+
1302
+ isPointNearLine(px, py, x1, y1, x2, y2, tolerance) {
1303
+ const A = px - x1;
1304
+ const B = py - y1;
1305
+ const C = x2 - x1;
1306
+ const D = y2 - y1;
1307
+
1308
+ const dot = A * C + B * D;
1309
+ const lenSq = C * C + D * D;
1310
+ let param = -1;
1311
+
1312
+ if (lenSq !== 0) {
1313
+ param = dot / lenSq;
1314
+ }
1315
+
1316
+ let xx, yy;
1317
+
1318
+ if (param < 0) {
1319
+ xx = x1;
1320
+ yy = y1;
1321
+ } else if (param > 1) {
1322
+ xx = x2;
1323
+ yy = y2;
1324
+ } else {
1325
+ xx = x1 + param * C;
1326
+ yy = y1 + param * D;
1327
+ }
1328
+
1329
+ const dx = px - xx;
1330
+ const dy = py - yy;
1331
+ return Math.sqrt(dx * dx + dy * dy) <= tolerance;
1332
+ }
1333
+
1334
+ drawLine(x1, y1, x2, y2, width, color, alpha) {
1335
+ const dx = x2 - x1;
1336
+ const dy = y2 - y1;
1337
+ const length = Math.sqrt(dx * dx + dy * dy);
1338
+ const angle = Math.atan2(dy, dx);
1339
+
1340
+ const halfWidth = width / 2;
1341
+ const cos = Math.cos(angle);
1342
+ const sin = Math.sin(angle);
1343
+
1344
+ const vertices = [
1345
+ x1 - sin * halfWidth, y1 + cos * halfWidth,
1346
+ x1 + sin * halfWidth, y1 - cos * halfWidth,
1347
+ x2 - sin * halfWidth, y2 + cos * halfWidth,
1348
+ x1 + sin * halfWidth, y1 - cos * halfWidth,
1349
+ x2 + sin * halfWidth, y2 - cos * halfWidth,
1350
+ x2 - sin * halfWidth, y2 + cos * halfWidth
1351
+ ];
1352
+
1353
+ const colors = new Array(6 * 4).fill(0);
1354
+ for (let i = 0; i < 6; i++) {
1355
+ colors[i * 4] = color[0];
1356
+ colors[i * 4 + 1] = color[1];
1357
+ colors[i * 4 + 2] = color[2];
1358
+ colors[i * 4 + 3] = alpha;
1359
+ }
1360
+
1361
+ this.drawTriangles(vertices, colors);
1362
+ }
1363
+
1364
+ endFrame() {
1365
+ this.flush();
1366
+ }
1367
+ }
1368
+
1369
+ export default WebGLCanvasAdapter;