bloody-engine 1.0.1 → 1.0.3
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/dist/node/index.js +2359 -0
- package/dist/web/index.js +34 -37
- package/dist/web/index.umd.js +7 -7
- package/package.json +1 -1
- package/dist/node/batch-renderer-JqZ4TYcL.js +0 -308
- package/dist/node/browser-resource-loader-D51BD3k_.js +0 -146
- package/dist/node/camera-A8EGrk7U.js +0 -271
- package/dist/node/index-node.js +0 -2117
- package/dist/node/node-resource-loader-MzkD-IGo.js +0 -166
- package/dist/node/resource-loader-factory-DQ-PAVcN.js +0 -93
- package/dist/node/resource-pipeline-Dac9qRso.js +0 -211
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
class SpriteBatchRenderer {
|
|
2
|
-
/**
|
|
3
|
-
* Create a new sprite batch renderer (V2)
|
|
4
|
-
* @param gl WebGL rendering context
|
|
5
|
-
* @param shader Shader program to use (should be SHADERS_V2)
|
|
6
|
-
* @param maxQuads Maximum number of quads to batch (default 1000)
|
|
7
|
-
*/
|
|
8
|
-
constructor(gl, shader, maxQuads = 1e3) {
|
|
9
|
-
this.vertexBuffer = null;
|
|
10
|
-
this.quads = [];
|
|
11
|
-
this.isDirty = false;
|
|
12
|
-
this.verticesPerQuad = 6;
|
|
13
|
-
this.floatsPerVertex = 10;
|
|
14
|
-
this.texture = null;
|
|
15
|
-
this.depthTestEnabled = true;
|
|
16
|
-
this.gl = gl;
|
|
17
|
-
this.shader = shader;
|
|
18
|
-
this.maxQuads = maxQuads;
|
|
19
|
-
const totalFloats = maxQuads * this.verticesPerQuad * this.floatsPerVertex;
|
|
20
|
-
this.vertexData = new Float32Array(totalFloats);
|
|
21
|
-
const buf = gl.createBuffer();
|
|
22
|
-
if (!buf) {
|
|
23
|
-
throw new Error("Failed to create vertex buffer");
|
|
24
|
-
}
|
|
25
|
-
this.vertexBuffer = buf;
|
|
26
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
27
|
-
gl.bufferData(gl.ARRAY_BUFFER, this.vertexData.byteLength, gl.DYNAMIC_DRAW);
|
|
28
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Set the texture for batch rendering
|
|
32
|
-
* @param texture The texture to use when rendering
|
|
33
|
-
*/
|
|
34
|
-
setTexture(texture) {
|
|
35
|
-
this.texture = texture;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Add a sprite quad to the batch
|
|
39
|
-
* @param quad Sprite quad instance to add
|
|
40
|
-
*/
|
|
41
|
-
addQuad(quad) {
|
|
42
|
-
if (this.quads.length >= this.maxQuads) {
|
|
43
|
-
console.warn(`Sprite batch renderer at max capacity (${this.maxQuads})`);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
this.quads.push(quad);
|
|
47
|
-
this.isDirty = true;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Clear all quads from the batch
|
|
51
|
-
*/
|
|
52
|
-
clear() {
|
|
53
|
-
this.quads = [];
|
|
54
|
-
this.isDirty = true;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Get number of quads currently in batch
|
|
58
|
-
*/
|
|
59
|
-
getQuadCount() {
|
|
60
|
-
return this.quads.length;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Update the batch - rebuilds vertex buffer if quads changed
|
|
64
|
-
*/
|
|
65
|
-
update() {
|
|
66
|
-
if (!this.isDirty || this.quads.length === 0) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
let vertexIndex = 0;
|
|
70
|
-
for (const quad of this.quads) {
|
|
71
|
-
const {
|
|
72
|
-
x,
|
|
73
|
-
y,
|
|
74
|
-
z = 0,
|
|
75
|
-
width,
|
|
76
|
-
height,
|
|
77
|
-
rotation,
|
|
78
|
-
color = { r: 1, g: 1, b: 1, a: 1 },
|
|
79
|
-
uvRect = { uMin: 0, vMin: 0, uMax: 1, vMax: 1 },
|
|
80
|
-
texIndex = 0
|
|
81
|
-
} = quad;
|
|
82
|
-
const vertices = this.generateQuadVertices({
|
|
83
|
-
x,
|
|
84
|
-
y,
|
|
85
|
-
z,
|
|
86
|
-
width,
|
|
87
|
-
height,
|
|
88
|
-
rotation,
|
|
89
|
-
color,
|
|
90
|
-
uvRect,
|
|
91
|
-
texIndex
|
|
92
|
-
});
|
|
93
|
-
for (const vertex of vertices) {
|
|
94
|
-
this.vertexData[vertexIndex++] = vertex.x;
|
|
95
|
-
this.vertexData[vertexIndex++] = vertex.y;
|
|
96
|
-
this.vertexData[vertexIndex++] = vertex.z;
|
|
97
|
-
this.vertexData[vertexIndex++] = vertex.u;
|
|
98
|
-
this.vertexData[vertexIndex++] = vertex.v;
|
|
99
|
-
this.vertexData[vertexIndex++] = vertex.r;
|
|
100
|
-
this.vertexData[vertexIndex++] = vertex.g;
|
|
101
|
-
this.vertexData[vertexIndex++] = vertex.b;
|
|
102
|
-
this.vertexData[vertexIndex++] = vertex.a;
|
|
103
|
-
this.vertexData[vertexIndex++] = vertex.texIndex;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (this.vertexBuffer) {
|
|
107
|
-
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
108
|
-
this.gl.bufferSubData(
|
|
109
|
-
this.gl.ARRAY_BUFFER,
|
|
110
|
-
0,
|
|
111
|
-
this.vertexData.subarray(0, vertexIndex)
|
|
112
|
-
);
|
|
113
|
-
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
|
|
114
|
-
}
|
|
115
|
-
this.isDirty = false;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Set whether depth testing is enabled
|
|
119
|
-
* When enabled, sprites with lower Z values appear behind sprites with higher Z values
|
|
120
|
-
* @param enabled Whether to enable depth testing (default true)
|
|
121
|
-
*/
|
|
122
|
-
setDepthTestEnabled(enabled) {
|
|
123
|
-
this.depthTestEnabled = enabled;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Render the batch
|
|
127
|
-
* @param camera Optional camera for view transform (defaults to identity matrix)
|
|
128
|
-
*/
|
|
129
|
-
render(camera) {
|
|
130
|
-
if (this.quads.length === 0) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
this.update();
|
|
134
|
-
this.shader.use();
|
|
135
|
-
if (this.depthTestEnabled) {
|
|
136
|
-
this.gl.enable(this.gl.DEPTH_TEST);
|
|
137
|
-
this.gl.depthFunc(this.gl.LEQUAL);
|
|
138
|
-
} else {
|
|
139
|
-
this.gl.disable(this.gl.DEPTH_TEST);
|
|
140
|
-
}
|
|
141
|
-
if (this.vertexBuffer) {
|
|
142
|
-
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
143
|
-
const posAttr = this.shader.getAttributeLocation("aPosition");
|
|
144
|
-
const texCoordAttr = this.shader.getAttributeLocation("aTexCoord");
|
|
145
|
-
const colorAttr = this.shader.getAttributeLocation("aColor");
|
|
146
|
-
const texIndexAttr = this.shader.getAttributeLocation("aTexIndex");
|
|
147
|
-
const stride = this.floatsPerVertex * 4;
|
|
148
|
-
if (posAttr !== -1) {
|
|
149
|
-
this.gl.enableVertexAttribArray(posAttr);
|
|
150
|
-
this.gl.vertexAttribPointer(
|
|
151
|
-
posAttr,
|
|
152
|
-
3,
|
|
153
|
-
// 3 floats (x, y, z)
|
|
154
|
-
this.gl.FLOAT,
|
|
155
|
-
false,
|
|
156
|
-
stride,
|
|
157
|
-
0
|
|
158
|
-
// offset
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
if (texCoordAttr !== -1) {
|
|
162
|
-
this.gl.enableVertexAttribArray(texCoordAttr);
|
|
163
|
-
this.gl.vertexAttribPointer(
|
|
164
|
-
texCoordAttr,
|
|
165
|
-
2,
|
|
166
|
-
// 2 floats (u, v)
|
|
167
|
-
this.gl.FLOAT,
|
|
168
|
-
false,
|
|
169
|
-
stride,
|
|
170
|
-
3 * 4
|
|
171
|
-
// offset after position
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
if (colorAttr !== -1) {
|
|
175
|
-
this.gl.enableVertexAttribArray(colorAttr);
|
|
176
|
-
this.gl.vertexAttribPointer(
|
|
177
|
-
colorAttr,
|
|
178
|
-
4,
|
|
179
|
-
// 4 floats (r, g, b, a)
|
|
180
|
-
this.gl.FLOAT,
|
|
181
|
-
false,
|
|
182
|
-
stride,
|
|
183
|
-
5 * 4
|
|
184
|
-
// offset after texCoord
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
if (texIndexAttr !== -1) {
|
|
188
|
-
this.gl.enableVertexAttribArray(texIndexAttr);
|
|
189
|
-
this.gl.vertexAttribPointer(
|
|
190
|
-
texIndexAttr,
|
|
191
|
-
1,
|
|
192
|
-
// 1 float (texIndex)
|
|
193
|
-
this.gl.FLOAT,
|
|
194
|
-
false,
|
|
195
|
-
stride,
|
|
196
|
-
9 * 4
|
|
197
|
-
// offset after color
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
if (this.texture) {
|
|
201
|
-
this.texture.bind(0);
|
|
202
|
-
const textureUniform = this.shader.getUniformLocation("uTexture");
|
|
203
|
-
if (textureUniform !== null) {
|
|
204
|
-
this.gl.uniform1i(textureUniform, 0);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
const matrixUniform = this.shader.getUniformLocation("uMatrix");
|
|
208
|
-
if (matrixUniform !== null) {
|
|
209
|
-
const matrix = camera ? camera.getViewMatrix() : new Float32Array([
|
|
210
|
-
1,
|
|
211
|
-
0,
|
|
212
|
-
0,
|
|
213
|
-
0,
|
|
214
|
-
0,
|
|
215
|
-
1,
|
|
216
|
-
0,
|
|
217
|
-
0,
|
|
218
|
-
0,
|
|
219
|
-
0,
|
|
220
|
-
1,
|
|
221
|
-
0,
|
|
222
|
-
0,
|
|
223
|
-
0,
|
|
224
|
-
0,
|
|
225
|
-
1
|
|
226
|
-
]);
|
|
227
|
-
this.gl.uniformMatrix4fv(matrixUniform, false, matrix);
|
|
228
|
-
}
|
|
229
|
-
const vertexCount = this.quads.length * this.verticesPerQuad;
|
|
230
|
-
this.gl.drawArrays(this.gl.TRIANGLES, 0, vertexCount);
|
|
231
|
-
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Generate vertices for a quad with rotation applied
|
|
236
|
-
* Returns 6 vertices (2 triangles)
|
|
237
|
-
* @private
|
|
238
|
-
*/
|
|
239
|
-
generateQuadVertices(instance) {
|
|
240
|
-
const { x, y, z, width, height, rotation, color, uvRect, texIndex } = instance;
|
|
241
|
-
const halfW = width / 2;
|
|
242
|
-
const halfH = height / 2;
|
|
243
|
-
const cos = Math.cos(rotation);
|
|
244
|
-
const sin = Math.sin(rotation);
|
|
245
|
-
const rotatePoint = (px, py) => {
|
|
246
|
-
return [px * cos - py * sin, px * sin + py * cos];
|
|
247
|
-
};
|
|
248
|
-
const corners = [
|
|
249
|
-
[-halfW, -halfH],
|
|
250
|
-
// bottom-left
|
|
251
|
-
[halfW, -halfH],
|
|
252
|
-
// bottom-right
|
|
253
|
-
[halfW, halfH],
|
|
254
|
-
// top-right
|
|
255
|
-
[halfW, halfH],
|
|
256
|
-
// top-right (duplicate)
|
|
257
|
-
[-halfW, halfH],
|
|
258
|
-
// top-left
|
|
259
|
-
[-halfW, -halfH]
|
|
260
|
-
// bottom-left (duplicate)
|
|
261
|
-
];
|
|
262
|
-
const texCoords = [
|
|
263
|
-
[uvRect.uMin, uvRect.vMin],
|
|
264
|
-
// bottom-left
|
|
265
|
-
[uvRect.uMax, uvRect.vMin],
|
|
266
|
-
// bottom-right
|
|
267
|
-
[uvRect.uMax, uvRect.vMax],
|
|
268
|
-
// top-right
|
|
269
|
-
[uvRect.uMax, uvRect.vMax],
|
|
270
|
-
// top-right
|
|
271
|
-
[uvRect.uMin, uvRect.vMax],
|
|
272
|
-
// top-left
|
|
273
|
-
[uvRect.uMin, uvRect.vMin]
|
|
274
|
-
// bottom-left
|
|
275
|
-
];
|
|
276
|
-
const vertices = [];
|
|
277
|
-
for (let i = 0; i < corners.length; i++) {
|
|
278
|
-
const [localX, localY] = corners[i];
|
|
279
|
-
const [rotX, rotY] = rotatePoint(localX, localY);
|
|
280
|
-
const [u, v] = texCoords[i];
|
|
281
|
-
vertices.push({
|
|
282
|
-
x: x + rotX,
|
|
283
|
-
y: y + rotY,
|
|
284
|
-
z,
|
|
285
|
-
u,
|
|
286
|
-
v,
|
|
287
|
-
r: color.r,
|
|
288
|
-
g: color.g,
|
|
289
|
-
b: color.b,
|
|
290
|
-
a: color.a,
|
|
291
|
-
texIndex
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
return vertices;
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Dispose resources
|
|
298
|
-
*/
|
|
299
|
-
dispose() {
|
|
300
|
-
if (this.vertexBuffer) {
|
|
301
|
-
this.gl.deleteBuffer(this.vertexBuffer);
|
|
302
|
-
this.vertexBuffer = null;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
export {
|
|
307
|
-
SpriteBatchRenderer
|
|
308
|
-
};
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
class BrowserResourceLoader {
|
|
2
|
-
/**
|
|
3
|
-
* Create a new browser resource loader
|
|
4
|
-
* @param baseUrl Optional base URL for resolving relative paths (defaults to current origin)
|
|
5
|
-
* @param timeout Default timeout for requests in milliseconds (default: 10000)
|
|
6
|
-
*/
|
|
7
|
-
constructor(baseUrl = "", timeout = 1e4) {
|
|
8
|
-
this.baseUrl = baseUrl || this.getCurrentOrigin();
|
|
9
|
-
this.defaultTimeout = timeout;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Get the current origin (protocol + host + port)
|
|
13
|
-
*/
|
|
14
|
-
getCurrentOrigin() {
|
|
15
|
-
return typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Resolve a relative path against the base URL
|
|
19
|
-
* @param path Relative or absolute path
|
|
20
|
-
* @returns Resolved absolute URL
|
|
21
|
-
*/
|
|
22
|
-
resolvePath(path) {
|
|
23
|
-
try {
|
|
24
|
-
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
25
|
-
return path;
|
|
26
|
-
}
|
|
27
|
-
if (path.startsWith("//")) {
|
|
28
|
-
return window.location.protocol + path;
|
|
29
|
-
}
|
|
30
|
-
if (path.startsWith("/")) {
|
|
31
|
-
return this.baseUrl + path;
|
|
32
|
-
}
|
|
33
|
-
return `${this.baseUrl}/${path}`;
|
|
34
|
-
} catch {
|
|
35
|
-
return path;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Load a single resource from a URL
|
|
40
|
-
* @param path URL or relative path to the resource
|
|
41
|
-
* @param options Optional loading configuration
|
|
42
|
-
* @returns Promise resolving to the resource content
|
|
43
|
-
*/
|
|
44
|
-
async load(path, options) {
|
|
45
|
-
const url = this.resolvePath(path);
|
|
46
|
-
try {
|
|
47
|
-
const fetchOptions = {
|
|
48
|
-
credentials: options?.credentials || "same-origin"
|
|
49
|
-
};
|
|
50
|
-
if (options?.headers) {
|
|
51
|
-
fetchOptions.headers = options.headers;
|
|
52
|
-
}
|
|
53
|
-
const controller = new AbortController();
|
|
54
|
-
const timeoutId = setTimeout(() => controller.abort(), this.defaultTimeout);
|
|
55
|
-
fetchOptions.signal = controller.signal;
|
|
56
|
-
const response = await fetch(url, fetchOptions);
|
|
57
|
-
clearTimeout(timeoutId);
|
|
58
|
-
if (!response.ok) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`HTTP ${response.status}: ${response.statusText} for URL: ${url}`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
const text = await response.text();
|
|
64
|
-
return text;
|
|
65
|
-
} catch (error) {
|
|
66
|
-
if (error instanceof Error) {
|
|
67
|
-
if (error.name === "AbortError") {
|
|
68
|
-
throw new Error(
|
|
69
|
-
`Request timeout after ${this.defaultTimeout}ms for URL: ${url}`
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
throw new Error(`Failed to load resource from ${url}: ${error.message}`);
|
|
73
|
-
}
|
|
74
|
-
throw new Error(`Failed to load resource from ${url}: Unknown error`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Load multiple resources in parallel
|
|
79
|
-
* @param paths Array of URLs or paths
|
|
80
|
-
* @param options Optional loading configuration
|
|
81
|
-
* @returns Promise resolving to array of load results
|
|
82
|
-
*/
|
|
83
|
-
async loadMultiple(paths, options) {
|
|
84
|
-
const promises = paths.map(async (path) => {
|
|
85
|
-
try {
|
|
86
|
-
const data = await this.load(path, options);
|
|
87
|
-
return {
|
|
88
|
-
data,
|
|
89
|
-
path,
|
|
90
|
-
success: true
|
|
91
|
-
};
|
|
92
|
-
} catch (error) {
|
|
93
|
-
return {
|
|
94
|
-
data: "",
|
|
95
|
-
path,
|
|
96
|
-
success: false,
|
|
97
|
-
error: error instanceof Error ? error.message : String(error)
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
return Promise.all(promises);
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Check if the path is valid for loading in the browser
|
|
105
|
-
* @param path URL or path to check
|
|
106
|
-
* @returns true if the path can be loaded
|
|
107
|
-
*/
|
|
108
|
-
canLoad(path) {
|
|
109
|
-
const validPatterns = [
|
|
110
|
-
/^https?:\/\//i,
|
|
111
|
-
// Absolute HTTP(S) URLs
|
|
112
|
-
/^\/\//,
|
|
113
|
-
// Protocol-relative URLs
|
|
114
|
-
/^\//,
|
|
115
|
-
// Absolute paths
|
|
116
|
-
/^\.\.?\//
|
|
117
|
-
// Relative paths starting with ./ or ../
|
|
118
|
-
];
|
|
119
|
-
const hasFileExtension = /\.[a-z0-9]+$/i.test(path);
|
|
120
|
-
return validPatterns.some((pattern) => pattern.test(path)) || hasFileExtension;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Set a new base URL for resolving relative paths
|
|
124
|
-
* @param baseUrl New base URL
|
|
125
|
-
*/
|
|
126
|
-
setBaseUrl(baseUrl) {
|
|
127
|
-
this.baseUrl = baseUrl;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Get the current base URL
|
|
131
|
-
* @returns Current base URL
|
|
132
|
-
*/
|
|
133
|
-
getBaseUrl() {
|
|
134
|
-
return this.baseUrl;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Set the default request timeout
|
|
138
|
-
* @param timeout Timeout in milliseconds
|
|
139
|
-
*/
|
|
140
|
-
setTimeout(timeout) {
|
|
141
|
-
this.defaultTimeout = timeout;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
export {
|
|
145
|
-
BrowserResourceLoader
|
|
146
|
-
};
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
class Matrix4 {
|
|
2
|
-
/**
|
|
3
|
-
* Create an identity matrix
|
|
4
|
-
* @returns 4x4 identity matrix in column-major order
|
|
5
|
-
*/
|
|
6
|
-
static identity() {
|
|
7
|
-
return new Float32Array([
|
|
8
|
-
1,
|
|
9
|
-
0,
|
|
10
|
-
0,
|
|
11
|
-
0,
|
|
12
|
-
// column 0
|
|
13
|
-
0,
|
|
14
|
-
1,
|
|
15
|
-
0,
|
|
16
|
-
0,
|
|
17
|
-
// column 1
|
|
18
|
-
0,
|
|
19
|
-
0,
|
|
20
|
-
1,
|
|
21
|
-
0,
|
|
22
|
-
// column 2
|
|
23
|
-
0,
|
|
24
|
-
0,
|
|
25
|
-
0,
|
|
26
|
-
1
|
|
27
|
-
// column 3
|
|
28
|
-
]);
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Create a translation matrix
|
|
32
|
-
* @param x Translation along X axis
|
|
33
|
-
* @param y Translation along Y axis
|
|
34
|
-
* @param z Translation along Z axis (default 0)
|
|
35
|
-
* @returns 4x4 translation matrix in column-major order
|
|
36
|
-
*/
|
|
37
|
-
static translation(x, y, z = 0) {
|
|
38
|
-
return new Float32Array([
|
|
39
|
-
1,
|
|
40
|
-
0,
|
|
41
|
-
0,
|
|
42
|
-
0,
|
|
43
|
-
// column 0
|
|
44
|
-
0,
|
|
45
|
-
1,
|
|
46
|
-
0,
|
|
47
|
-
0,
|
|
48
|
-
// column 1
|
|
49
|
-
0,
|
|
50
|
-
0,
|
|
51
|
-
1,
|
|
52
|
-
0,
|
|
53
|
-
// column 2
|
|
54
|
-
x,
|
|
55
|
-
y,
|
|
56
|
-
z,
|
|
57
|
-
1
|
|
58
|
-
// column 3
|
|
59
|
-
]);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Create a scale matrix
|
|
63
|
-
* @param x Scale factor along X axis
|
|
64
|
-
* @param y Scale factor along Y axis
|
|
65
|
-
* @param z Scale factor along Z axis (default 1)
|
|
66
|
-
* @returns 4x4 scale matrix in column-major order
|
|
67
|
-
*/
|
|
68
|
-
static scale(x, y, z = 1) {
|
|
69
|
-
return new Float32Array([
|
|
70
|
-
x,
|
|
71
|
-
0,
|
|
72
|
-
0,
|
|
73
|
-
0,
|
|
74
|
-
// column 0
|
|
75
|
-
0,
|
|
76
|
-
y,
|
|
77
|
-
0,
|
|
78
|
-
0,
|
|
79
|
-
// column 1
|
|
80
|
-
0,
|
|
81
|
-
0,
|
|
82
|
-
z,
|
|
83
|
-
0,
|
|
84
|
-
// column 2
|
|
85
|
-
0,
|
|
86
|
-
0,
|
|
87
|
-
0,
|
|
88
|
-
1
|
|
89
|
-
// column 3
|
|
90
|
-
]);
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Multiply two matrices (result = a * b)
|
|
94
|
-
* @param a First matrix (left operand)
|
|
95
|
-
* @param b Second matrix (right operand)
|
|
96
|
-
* @returns Result of matrix multiplication in column-major order
|
|
97
|
-
*/
|
|
98
|
-
static multiply(a, b) {
|
|
99
|
-
const result = new Float32Array(16);
|
|
100
|
-
for (let col = 0; col < 4; col++) {
|
|
101
|
-
for (let row = 0; row < 4; row++) {
|
|
102
|
-
let sum = 0;
|
|
103
|
-
for (let k = 0; k < 4; k++) {
|
|
104
|
-
sum += a[k * 4 + row] * b[col * 4 + k];
|
|
105
|
-
}
|
|
106
|
-
result[col * 4 + row] = sum;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return result;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Create a view matrix from camera position and zoom
|
|
113
|
-
* The view matrix transforms world coordinates to camera/eye coordinates
|
|
114
|
-
*
|
|
115
|
-
* View = Translation(-cameraX, -cameraY, 0) * Scale(zoom, zoom, 1)
|
|
116
|
-
*
|
|
117
|
-
* @param x Camera X position (translation will be negative)
|
|
118
|
-
* @param y Camera Y position (translation will be negative)
|
|
119
|
-
* @param zoom Camera zoom level (1.0 = no zoom, >1 = zoom in, <1 = zoom out)
|
|
120
|
-
* @returns 4x4 view matrix in column-major order
|
|
121
|
-
*/
|
|
122
|
-
static createViewMatrix(x, y, zoom) {
|
|
123
|
-
const translation = Matrix4.translation(-x, -y, 0);
|
|
124
|
-
const scale = Matrix4.scale(zoom, zoom, 1);
|
|
125
|
-
return Matrix4.multiply(translation, scale);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
class Camera {
|
|
129
|
-
/**
|
|
130
|
-
* Create a new camera
|
|
131
|
-
* @param x Initial X position (default 0)
|
|
132
|
-
* @param y Initial Y position (default 0)
|
|
133
|
-
* @param zoom Initial zoom level (default 1.0)
|
|
134
|
-
*/
|
|
135
|
-
constructor(x = 0, y = 0, zoom = 1) {
|
|
136
|
-
this._viewMatrix = null;
|
|
137
|
-
this._viewMatrixDirty = true;
|
|
138
|
-
this._x = x;
|
|
139
|
-
this._y = y;
|
|
140
|
-
this._zoom = zoom;
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Get the camera X position
|
|
144
|
-
*/
|
|
145
|
-
get x() {
|
|
146
|
-
return this._x;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Set the camera X position
|
|
150
|
-
*/
|
|
151
|
-
set x(value) {
|
|
152
|
-
this._x = value;
|
|
153
|
-
this._viewMatrixDirty = true;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Get the camera Y position
|
|
157
|
-
*/
|
|
158
|
-
get y() {
|
|
159
|
-
return this._y;
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Set the camera Y position
|
|
163
|
-
*/
|
|
164
|
-
set y(value) {
|
|
165
|
-
this._y = value;
|
|
166
|
-
this._viewMatrixDirty = true;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Get the camera zoom level
|
|
170
|
-
*/
|
|
171
|
-
get zoom() {
|
|
172
|
-
return this._zoom;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Set the camera zoom level
|
|
176
|
-
* Values: 1.0 = no zoom, >1 = zoom in, <1 = zoom out
|
|
177
|
-
*/
|
|
178
|
-
set zoom(value) {
|
|
179
|
-
this._zoom = Math.max(1e-3, value);
|
|
180
|
-
this._viewMatrixDirty = true;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Set both X and Y position at once
|
|
184
|
-
* @param x New X position
|
|
185
|
-
* @param y New Y position
|
|
186
|
-
*/
|
|
187
|
-
setPosition(x, y) {
|
|
188
|
-
this._x = x;
|
|
189
|
-
this._y = y;
|
|
190
|
-
this._viewMatrixDirty = true;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Move the camera by a relative offset
|
|
194
|
-
* @param dx X offset to add to current position
|
|
195
|
-
* @param dy Y offset to add to current position
|
|
196
|
-
*/
|
|
197
|
-
move(dx, dy) {
|
|
198
|
-
this._x += dx;
|
|
199
|
-
this._y += dy;
|
|
200
|
-
this._viewMatrixDirty = true;
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Scale the zoom by a factor
|
|
204
|
-
* @param factor Multiplier for current zoom (e.g., 1.1 to zoom in 10%)
|
|
205
|
-
*/
|
|
206
|
-
zoomBy(factor) {
|
|
207
|
-
this._zoom = Math.max(1e-3, this._zoom * factor);
|
|
208
|
-
this._viewMatrixDirty = true;
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Reset camera to default position and zoom
|
|
212
|
-
*/
|
|
213
|
-
reset() {
|
|
214
|
-
this._x = 0;
|
|
215
|
-
this._y = 0;
|
|
216
|
-
this._zoom = 1;
|
|
217
|
-
this._viewMatrixDirty = true;
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Get the view matrix for this camera
|
|
221
|
-
* The view matrix transforms world coordinates to camera space
|
|
222
|
-
* Caches the result until camera properties change
|
|
223
|
-
*
|
|
224
|
-
* @returns 4x4 view matrix in column-major order
|
|
225
|
-
*/
|
|
226
|
-
getViewMatrix() {
|
|
227
|
-
if (this._viewMatrixDirty || this._viewMatrix === null) {
|
|
228
|
-
this._viewMatrix = Matrix4.createViewMatrix(this._x, this._y, this._zoom);
|
|
229
|
-
this._viewMatrixDirty = false;
|
|
230
|
-
}
|
|
231
|
-
return this._viewMatrix;
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Convert screen coordinates to world coordinates
|
|
235
|
-
* Useful for mouse picking and interaction
|
|
236
|
-
*
|
|
237
|
-
* @param screenX Screen X coordinate (pixels)
|
|
238
|
-
* @param screenY Screen Y coordinate (pixels)
|
|
239
|
-
* @param viewportWidth Viewport width in pixels
|
|
240
|
-
* @param viewportHeight Viewport height in pixels
|
|
241
|
-
* @returns World coordinates {x, y}
|
|
242
|
-
*/
|
|
243
|
-
screenToWorld(screenX, screenY, viewportWidth, viewportHeight) {
|
|
244
|
-
const centeredX = screenX - viewportWidth / 2;
|
|
245
|
-
const centeredY = screenY - viewportHeight / 2;
|
|
246
|
-
const worldX = centeredX / this._zoom + this._x;
|
|
247
|
-
const worldY = centeredY / this._zoom + this._y;
|
|
248
|
-
return { x: worldX, y: worldY };
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Convert world coordinates to screen coordinates
|
|
252
|
-
* Useful for UI positioning and debug rendering
|
|
253
|
-
*
|
|
254
|
-
* @param worldX World X coordinate
|
|
255
|
-
* @param worldY World Y coordinate
|
|
256
|
-
* @param viewportWidth Viewport width in pixels
|
|
257
|
-
* @param viewportHeight Viewport height in pixels
|
|
258
|
-
* @returns Screen coordinates {x, y} in pixels
|
|
259
|
-
*/
|
|
260
|
-
worldToScreen(worldX, worldY, viewportWidth, viewportHeight) {
|
|
261
|
-
const centeredX = (worldX - this._x) * this._zoom;
|
|
262
|
-
const centeredY = (worldY - this._y) * this._zoom;
|
|
263
|
-
const screenX = centeredX + viewportWidth / 2;
|
|
264
|
-
const screenY = centeredY + viewportHeight / 2;
|
|
265
|
-
return { x: screenX, y: screenY };
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
export {
|
|
269
|
-
Camera,
|
|
270
|
-
Matrix4
|
|
271
|
-
};
|