canvasframework 0.4.9 → 0.4.11

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