canvasframework 0.4.9 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/CanvasFramework.js +36 -8
- package/core/WebGLCanvasAdapter.js +1351 -1334
- package/package.json +1 -1
|
@@ -1,1418 +1,1435 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Adaptateur WebGL
|
|
3
|
-
*
|
|
2
|
+
* Adaptateur WebGL pour le rendu Canvas 2D-like
|
|
3
|
+
* @class WebGLCanvasAdapter
|
|
4
4
|
*/
|
|
5
5
|
class WebGLCanvasAdapter {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Configure l'environnement WebGL de base
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
_setupWebGL() {
|
|
123
|
+
const gl = this.gl;
|
|
139
124
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
125
|
+
// Configuration du viewport
|
|
126
|
+
gl.viewport(0, 0, this.width, this.height);
|
|
143
127
|
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
459
|
-
|
|
577
|
+
const color = this._parseColor(this.fillStyle);
|
|
578
|
+
color[3] *= this.globalAlpha;
|
|
460
579
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
1179
|
-
|
|
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
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
-
|
|
1240
|
-
|
|
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
|
-
|
|
1244
|
-
|
|
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
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
-
//
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
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
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
-
|
|
1414
|
-
|
|
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;
|