cubeforge 0.3.0 → 0.3.1
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/index.d.mts +3 -11
- package/dist/index.js +953 -1351
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1115,31 +1115,6 @@ function createInputRecorder() {
|
|
|
1115
1115
|
};
|
|
1116
1116
|
}
|
|
1117
1117
|
|
|
1118
|
-
// ../../packages/renderer/src/canvas2d.ts
|
|
1119
|
-
var Canvas2DRenderer = class {
|
|
1120
|
-
constructor(canvas) {
|
|
1121
|
-
this.canvas = canvas;
|
|
1122
|
-
const ctx = canvas.getContext("2d");
|
|
1123
|
-
if (!ctx) throw new Error("Could not get 2D context from canvas");
|
|
1124
|
-
this.ctx = ctx;
|
|
1125
|
-
}
|
|
1126
|
-
ctx;
|
|
1127
|
-
clear(color) {
|
|
1128
|
-
if (color) {
|
|
1129
|
-
this.ctx.fillStyle = color;
|
|
1130
|
-
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1131
|
-
} else {
|
|
1132
|
-
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
get width() {
|
|
1136
|
-
return this.canvas.width;
|
|
1137
|
-
}
|
|
1138
|
-
get height() {
|
|
1139
|
-
return this.canvas.height;
|
|
1140
|
-
}
|
|
1141
|
-
};
|
|
1142
|
-
|
|
1143
1118
|
// ../../packages/renderer/src/components/sprite.ts
|
|
1144
1119
|
function createSprite(opts) {
|
|
1145
1120
|
return {
|
|
@@ -1187,400 +1162,862 @@ function createTrail(opts) {
|
|
|
1187
1162
|
};
|
|
1188
1163
|
}
|
|
1189
1164
|
|
|
1190
|
-
// ../../packages/renderer/src/
|
|
1191
|
-
var
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1165
|
+
// ../../packages/renderer/src/shaders.ts
|
|
1166
|
+
var VERT_SRC = `#version 300 es
|
|
1167
|
+
layout(location = 0) in vec2 a_quadPos;
|
|
1168
|
+
layout(location = 1) in vec2 a_uv;
|
|
1169
|
+
|
|
1170
|
+
layout(location = 2) in vec2 i_pos;
|
|
1171
|
+
layout(location = 3) in vec2 i_size;
|
|
1172
|
+
layout(location = 4) in float i_rot;
|
|
1173
|
+
layout(location = 5) in vec2 i_anchor;
|
|
1174
|
+
layout(location = 6) in vec2 i_offset;
|
|
1175
|
+
layout(location = 7) in float i_flipX;
|
|
1176
|
+
layout(location = 8) in vec4 i_color;
|
|
1177
|
+
layout(location = 9) in vec4 i_uvRect;
|
|
1178
|
+
|
|
1179
|
+
uniform vec2 u_camPos;
|
|
1180
|
+
uniform float u_zoom;
|
|
1181
|
+
uniform vec2 u_canvasSize;
|
|
1182
|
+
uniform vec2 u_shake;
|
|
1183
|
+
|
|
1184
|
+
out vec2 v_uv;
|
|
1185
|
+
out vec4 v_color;
|
|
1186
|
+
|
|
1187
|
+
void main() {
|
|
1188
|
+
// Local position: map quad corner (-0.5..0.5) to draw rect, applying anchor
|
|
1189
|
+
vec2 local = (a_quadPos - vec2(i_anchor.x - 0.5, i_anchor.y - 0.5)) * i_size + i_offset;
|
|
1190
|
+
|
|
1191
|
+
// Horizontal flip
|
|
1192
|
+
if (i_flipX > 0.5) local.x = -local.x;
|
|
1193
|
+
|
|
1194
|
+
// Rotate around local origin
|
|
1195
|
+
float c = cos(i_rot);
|
|
1196
|
+
float s = sin(i_rot);
|
|
1197
|
+
local = vec2(c * local.x - s * local.y, s * local.x + c * local.y);
|
|
1198
|
+
|
|
1199
|
+
// World position
|
|
1200
|
+
vec2 world = i_pos + local;
|
|
1201
|
+
|
|
1202
|
+
// Camera \u2192 NDC clip space (Y is flipped: canvas Y down, WebGL Y up)
|
|
1203
|
+
// Equivalent to Canvas2D: translate(W/2 - camX*zoom + shakeX, H/2 - camY*zoom + shakeY); scale(zoom,zoom)
|
|
1204
|
+
float cx = 2.0 * u_zoom / u_canvasSize.x * (world.x - u_camPos.x) + 2.0 * u_shake.x / u_canvasSize.x;
|
|
1205
|
+
float cy = -2.0 * u_zoom / u_canvasSize.y * (world.y - u_camPos.y) - 2.0 * u_shake.y / u_canvasSize.y;
|
|
1206
|
+
|
|
1207
|
+
gl_Position = vec4(cx, cy, 0.0, 1.0);
|
|
1208
|
+
|
|
1209
|
+
// Remap UV [0,1] to the sub-rect defined by i_uvRect
|
|
1210
|
+
v_uv = i_uvRect.xy + a_uv * i_uvRect.zw;
|
|
1211
|
+
v_color = i_color;
|
|
1212
|
+
}
|
|
1213
|
+
`;
|
|
1214
|
+
var FRAG_SRC = `#version 300 es
|
|
1215
|
+
precision mediump float;
|
|
1216
|
+
|
|
1217
|
+
in vec2 v_uv;
|
|
1218
|
+
in vec4 v_color;
|
|
1219
|
+
|
|
1220
|
+
uniform sampler2D u_texture;
|
|
1221
|
+
uniform int u_useTexture;
|
|
1222
|
+
|
|
1223
|
+
out vec4 fragColor;
|
|
1224
|
+
|
|
1225
|
+
void main() {
|
|
1226
|
+
if (u_useTexture == 1) {
|
|
1227
|
+
fragColor = texture(u_texture, v_uv) * v_color;
|
|
1228
|
+
} else {
|
|
1229
|
+
fragColor = v_color;
|
|
1196
1230
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1231
|
+
}
|
|
1232
|
+
`;
|
|
1233
|
+
var PARALLAX_VERT_SRC = `#version 300 es
|
|
1234
|
+
layout(location = 0) in vec2 a_pos;
|
|
1235
|
+
|
|
1236
|
+
out vec2 v_fragCoord;
|
|
1237
|
+
|
|
1238
|
+
void main() {
|
|
1239
|
+
gl_Position = vec4(a_pos, 0.0, 1.0);
|
|
1240
|
+
// Convert NDC (-1..1) to canvas pixel coords (0..canvasSize) in the frag shader
|
|
1241
|
+
v_fragCoord = a_pos * 0.5 + 0.5; // 0..1 normalized screen coord
|
|
1242
|
+
}
|
|
1243
|
+
`;
|
|
1244
|
+
var PARALLAX_FRAG_SRC = `#version 300 es
|
|
1245
|
+
precision mediump float;
|
|
1246
|
+
|
|
1247
|
+
in vec2 v_fragCoord;
|
|
1248
|
+
|
|
1249
|
+
uniform sampler2D u_texture;
|
|
1250
|
+
uniform vec2 u_uvOffset;
|
|
1251
|
+
uniform vec2 u_texSize; // texture size in pixels
|
|
1252
|
+
uniform vec2 u_canvasSize; // canvas size in pixels
|
|
1253
|
+
|
|
1254
|
+
out vec4 fragColor;
|
|
1255
|
+
|
|
1256
|
+
void main() {
|
|
1257
|
+
// Screen pixel position
|
|
1258
|
+
vec2 screenPx = v_fragCoord * u_canvasSize;
|
|
1259
|
+
// Tile: offset by uvOffset and wrap
|
|
1260
|
+
vec2 uv = mod((screenPx / u_texSize + u_uvOffset), 1.0);
|
|
1261
|
+
// Y must be flipped because WebGL origin is bottom-left but canvas is top-left
|
|
1262
|
+
uv.y = 1.0 - uv.y;
|
|
1263
|
+
fragColor = texture(u_texture, uv);
|
|
1264
|
+
}
|
|
1265
|
+
`;
|
|
1266
|
+
|
|
1267
|
+
// ../../packages/renderer/src/colorParser.ts
|
|
1268
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1269
|
+
function parseCSSColor(css) {
|
|
1270
|
+
const hit = cache.get(css);
|
|
1271
|
+
if (hit) return hit;
|
|
1272
|
+
let result = [1, 1, 1, 1];
|
|
1273
|
+
if (css.startsWith("#")) {
|
|
1274
|
+
const h = css.slice(1);
|
|
1275
|
+
if (h.length === 3 || h.length === 4) {
|
|
1276
|
+
const r = parseInt(h[0] + h[0], 16) / 255;
|
|
1277
|
+
const g = parseInt(h[1] + h[1], 16) / 255;
|
|
1278
|
+
const b = parseInt(h[2] + h[2], 16) / 255;
|
|
1279
|
+
const a = h.length === 4 ? parseInt(h[3] + h[3], 16) / 255 : 1;
|
|
1280
|
+
result = [r, g, b, a];
|
|
1281
|
+
} else if (h.length === 6 || h.length === 8) {
|
|
1282
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
1283
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
1284
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
1285
|
+
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
|
|
1286
|
+
result = [r, g, b, a];
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
const m = css.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
|
|
1290
|
+
if (m) {
|
|
1291
|
+
result = [
|
|
1292
|
+
parseInt(m[1]) / 255,
|
|
1293
|
+
parseInt(m[2]) / 255,
|
|
1294
|
+
parseInt(m[3]) / 255,
|
|
1295
|
+
m[4] !== void 0 ? parseFloat(m[4]) : 1
|
|
1296
|
+
];
|
|
1297
|
+
}
|
|
1207
1298
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1299
|
+
cache.set(css, result);
|
|
1300
|
+
return result;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// ../../packages/renderer/src/webglRenderSystem.ts
|
|
1304
|
+
var FLOATS_PER_INSTANCE = 18;
|
|
1305
|
+
var MAX_INSTANCES = 8192;
|
|
1306
|
+
var MAX_TEXT_CACHE = 200;
|
|
1307
|
+
function compileShader(gl, type, src) {
|
|
1308
|
+
const shader = gl.createShader(type);
|
|
1309
|
+
gl.shaderSource(shader, src);
|
|
1310
|
+
gl.compileShader(shader);
|
|
1311
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
1312
|
+
throw new Error(`[WebGLRenderer] Shader compile error:
|
|
1313
|
+
${gl.getShaderInfoLog(shader)}`);
|
|
1211
1314
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1315
|
+
return shader;
|
|
1316
|
+
}
|
|
1317
|
+
function createProgram(gl, vertSrc, fragSrc) {
|
|
1318
|
+
const vert = compileShader(gl, gl.VERTEX_SHADER, vertSrc);
|
|
1319
|
+
const frag = compileShader(gl, gl.FRAGMENT_SHADER, fragSrc);
|
|
1320
|
+
const prog = gl.createProgram();
|
|
1321
|
+
gl.attachShader(prog, vert);
|
|
1322
|
+
gl.attachShader(prog, frag);
|
|
1323
|
+
gl.linkProgram(prog);
|
|
1324
|
+
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
|
1325
|
+
throw new Error(`[WebGLRenderer] Program link error:
|
|
1326
|
+
${gl.getProgramInfoLog(prog)}`);
|
|
1215
1327
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1328
|
+
gl.deleteShader(vert);
|
|
1329
|
+
gl.deleteShader(frag);
|
|
1330
|
+
return prog;
|
|
1331
|
+
}
|
|
1332
|
+
function createWhiteTexture(gl) {
|
|
1333
|
+
const tex = gl.createTexture();
|
|
1334
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1335
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255, 255]));
|
|
1336
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
1337
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
1338
|
+
return tex;
|
|
1339
|
+
}
|
|
1340
|
+
function getTextureKey(sprite) {
|
|
1341
|
+
if (sprite.image?.src) return sprite.image.src;
|
|
1342
|
+
if (sprite.src) return sprite.src;
|
|
1343
|
+
return `__color__:${sprite.color}`;
|
|
1344
|
+
}
|
|
1345
|
+
function getUVRect(sprite) {
|
|
1346
|
+
if (!sprite.image || sprite.image.naturalWidth === 0) return [0, 0, 1, 1];
|
|
1347
|
+
const iw = sprite.image.naturalWidth;
|
|
1348
|
+
const ih = sprite.image.naturalHeight;
|
|
1349
|
+
if (sprite.frameWidth && sprite.frameHeight) {
|
|
1350
|
+
const cols = sprite.frameColumns ?? Math.floor(iw / sprite.frameWidth);
|
|
1351
|
+
const col = sprite.frameIndex % cols;
|
|
1352
|
+
const row = Math.floor(sprite.frameIndex / cols);
|
|
1353
|
+
return [
|
|
1354
|
+
col * sprite.frameWidth / iw,
|
|
1355
|
+
row * sprite.frameHeight / ih,
|
|
1356
|
+
sprite.frameWidth / iw,
|
|
1357
|
+
sprite.frameHeight / ih
|
|
1358
|
+
];
|
|
1218
1359
|
}
|
|
1219
|
-
|
|
1220
|
-
const {
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1360
|
+
if (sprite.frame) {
|
|
1361
|
+
const { sx, sy, sw, sh } = sprite.frame;
|
|
1362
|
+
return [sx / iw, sy / ih, sw / iw, sh / ih];
|
|
1363
|
+
}
|
|
1364
|
+
return [0, 0, 1, 1];
|
|
1365
|
+
}
|
|
1366
|
+
var RenderSystem = class {
|
|
1367
|
+
constructor(canvas, entityIds) {
|
|
1368
|
+
this.canvas = canvas;
|
|
1369
|
+
this.entityIds = entityIds;
|
|
1370
|
+
const gl = canvas.getContext("webgl2", { alpha: false, antialias: false, premultipliedAlpha: false });
|
|
1371
|
+
if (!gl) throw new Error("[WebGLRenderer] WebGL2 is not supported in this browser");
|
|
1372
|
+
this.gl = gl;
|
|
1373
|
+
this.program = createProgram(gl, VERT_SRC, FRAG_SRC);
|
|
1374
|
+
const quadVerts = new Float32Array([
|
|
1375
|
+
-0.5,
|
|
1376
|
+
-0.5,
|
|
1377
|
+
0,
|
|
1378
|
+
0,
|
|
1379
|
+
0.5,
|
|
1380
|
+
-0.5,
|
|
1381
|
+
1,
|
|
1382
|
+
0,
|
|
1383
|
+
-0.5,
|
|
1384
|
+
0.5,
|
|
1385
|
+
0,
|
|
1386
|
+
1,
|
|
1387
|
+
0.5,
|
|
1388
|
+
-0.5,
|
|
1389
|
+
1,
|
|
1390
|
+
0,
|
|
1391
|
+
0.5,
|
|
1392
|
+
0.5,
|
|
1393
|
+
1,
|
|
1394
|
+
1,
|
|
1395
|
+
-0.5,
|
|
1396
|
+
0.5,
|
|
1397
|
+
0,
|
|
1398
|
+
1
|
|
1399
|
+
]);
|
|
1400
|
+
this.quadVAO = gl.createVertexArray();
|
|
1401
|
+
gl.bindVertexArray(this.quadVAO);
|
|
1402
|
+
const quadBuf = gl.createBuffer();
|
|
1403
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
|
1404
|
+
gl.bufferData(gl.ARRAY_BUFFER, quadVerts, gl.STATIC_DRAW);
|
|
1405
|
+
const qStride = 4 * 4;
|
|
1406
|
+
gl.enableVertexAttribArray(0);
|
|
1407
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, qStride, 0);
|
|
1408
|
+
gl.enableVertexAttribArray(1);
|
|
1409
|
+
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, qStride, 2 * 4);
|
|
1410
|
+
this.instanceData = new Float32Array(MAX_INSTANCES * FLOATS_PER_INSTANCE);
|
|
1411
|
+
this.instanceBuffer = gl.createBuffer();
|
|
1412
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
|
|
1413
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.instanceData.byteLength, gl.DYNAMIC_DRAW);
|
|
1414
|
+
const iStride = FLOATS_PER_INSTANCE * 4;
|
|
1415
|
+
let byteOffset = 0;
|
|
1416
|
+
const addAttr = (loc, size) => {
|
|
1417
|
+
gl.enableVertexAttribArray(loc);
|
|
1418
|
+
gl.vertexAttribPointer(loc, size, gl.FLOAT, false, iStride, byteOffset);
|
|
1419
|
+
gl.vertexAttribDivisor(loc, 1);
|
|
1420
|
+
byteOffset += size * 4;
|
|
1421
|
+
};
|
|
1422
|
+
addAttr(2, 2);
|
|
1423
|
+
addAttr(3, 2);
|
|
1424
|
+
addAttr(4, 1);
|
|
1425
|
+
addAttr(5, 2);
|
|
1426
|
+
addAttr(6, 2);
|
|
1427
|
+
addAttr(7, 1);
|
|
1428
|
+
addAttr(8, 4);
|
|
1429
|
+
addAttr(9, 4);
|
|
1430
|
+
gl.bindVertexArray(null);
|
|
1431
|
+
gl.useProgram(this.program);
|
|
1432
|
+
this.uCamPos = gl.getUniformLocation(this.program, "u_camPos");
|
|
1433
|
+
this.uZoom = gl.getUniformLocation(this.program, "u_zoom");
|
|
1434
|
+
this.uCanvasSize = gl.getUniformLocation(this.program, "u_canvasSize");
|
|
1435
|
+
this.uShake = gl.getUniformLocation(this.program, "u_shake");
|
|
1436
|
+
this.uTexture = gl.getUniformLocation(this.program, "u_texture");
|
|
1437
|
+
this.uUseTexture = gl.getUniformLocation(this.program, "u_useTexture");
|
|
1438
|
+
this.whiteTexture = createWhiteTexture(gl);
|
|
1439
|
+
this.parallaxProgram = createProgram(gl, PARALLAX_VERT_SRC, PARALLAX_FRAG_SRC);
|
|
1440
|
+
const fsVerts = new Float32Array([
|
|
1441
|
+
-1,
|
|
1442
|
+
-1,
|
|
1443
|
+
1,
|
|
1444
|
+
-1,
|
|
1445
|
+
-1,
|
|
1446
|
+
1,
|
|
1447
|
+
1,
|
|
1448
|
+
-1,
|
|
1449
|
+
1,
|
|
1450
|
+
1,
|
|
1451
|
+
-1,
|
|
1452
|
+
1
|
|
1453
|
+
]);
|
|
1454
|
+
this.parallaxVAO = gl.createVertexArray();
|
|
1455
|
+
gl.bindVertexArray(this.parallaxVAO);
|
|
1456
|
+
const fsBuf = gl.createBuffer();
|
|
1457
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, fsBuf);
|
|
1458
|
+
gl.bufferData(gl.ARRAY_BUFFER, fsVerts, gl.STATIC_DRAW);
|
|
1459
|
+
gl.enableVertexAttribArray(0);
|
|
1460
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0);
|
|
1461
|
+
gl.bindVertexArray(null);
|
|
1462
|
+
gl.useProgram(this.parallaxProgram);
|
|
1463
|
+
this.pUTexture = gl.getUniformLocation(this.parallaxProgram, "u_texture");
|
|
1464
|
+
this.pUUvOffset = gl.getUniformLocation(this.parallaxProgram, "u_uvOffset");
|
|
1465
|
+
this.pUTexSize = gl.getUniformLocation(this.parallaxProgram, "u_texSize");
|
|
1466
|
+
this.pUCanvasSize = gl.getUniformLocation(this.parallaxProgram, "u_canvasSize");
|
|
1467
|
+
gl.enable(gl.BLEND);
|
|
1468
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
1469
|
+
}
|
|
1470
|
+
gl;
|
|
1471
|
+
program;
|
|
1472
|
+
quadVAO;
|
|
1473
|
+
instanceBuffer;
|
|
1474
|
+
instanceData;
|
|
1475
|
+
whiteTexture;
|
|
1476
|
+
textures = /* @__PURE__ */ new Map();
|
|
1477
|
+
imageCache = /* @__PURE__ */ new Map();
|
|
1478
|
+
// Cached uniform locations — sprite program
|
|
1479
|
+
uCamPos;
|
|
1480
|
+
uZoom;
|
|
1481
|
+
uCanvasSize;
|
|
1482
|
+
uShake;
|
|
1483
|
+
uTexture;
|
|
1484
|
+
uUseTexture;
|
|
1485
|
+
// ── Parallax program ──────────────────────────────────────────────────────
|
|
1486
|
+
parallaxProgram;
|
|
1487
|
+
parallaxVAO;
|
|
1488
|
+
parallaxTextures = /* @__PURE__ */ new Map();
|
|
1489
|
+
parallaxImageCache = /* @__PURE__ */ new Map();
|
|
1490
|
+
// Cached uniform locations — parallax program
|
|
1491
|
+
pUTexture;
|
|
1492
|
+
pUUvOffset;
|
|
1493
|
+
pUTexSize;
|
|
1494
|
+
pUCanvasSize;
|
|
1495
|
+
// ── Text texture cache ────────────────────────────────────────────────────
|
|
1496
|
+
textureCache = /* @__PURE__ */ new Map();
|
|
1497
|
+
/** Insertion-order key list for LRU-style eviction. */
|
|
1498
|
+
textureCacheKeys = [];
|
|
1499
|
+
// ── Texture management (sprite textures — CLAMP_TO_EDGE) ──────────────────
|
|
1500
|
+
loadTexture(src) {
|
|
1501
|
+
const cached = this.textures.get(src);
|
|
1502
|
+
if (cached) return cached;
|
|
1503
|
+
let img = this.imageCache.get(src);
|
|
1504
|
+
if (!img) {
|
|
1505
|
+
img = new Image();
|
|
1506
|
+
img.src = src;
|
|
1507
|
+
img.onload = () => {
|
|
1508
|
+
const tex = this.gl.createTexture();
|
|
1509
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, tex);
|
|
1510
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, img);
|
|
1511
|
+
this.gl.generateMipmap(this.gl.TEXTURE_2D);
|
|
1512
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR_MIPMAP_LINEAR);
|
|
1513
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
1514
|
+
this.textures.set(src, tex);
|
|
1515
|
+
};
|
|
1516
|
+
this.imageCache.set(src, img);
|
|
1319
1517
|
}
|
|
1320
|
-
this.
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
const imgH = layer.imageHeight;
|
|
1344
|
-
const drawX = layer.offsetX - camX * layer.speedX;
|
|
1345
|
-
const drawY = layer.offsetY - camY * layer.speedY;
|
|
1346
|
-
ctx.save();
|
|
1347
|
-
if (layer.repeatX || layer.repeatY) {
|
|
1348
|
-
const pattern = ctx.createPattern(img, layer.repeatX && layer.repeatY ? "repeat" : layer.repeatX ? "repeat-x" : "repeat-y");
|
|
1349
|
-
if (pattern) {
|
|
1350
|
-
const offsetX = (drawX % imgW + imgW) % imgW;
|
|
1351
|
-
const offsetY = (drawY % imgH + imgH) % imgH;
|
|
1352
|
-
const matrix = new DOMMatrix();
|
|
1353
|
-
matrix.translateSelf(offsetX, offsetY);
|
|
1354
|
-
pattern.setTransform(matrix);
|
|
1355
|
-
ctx.fillStyle = pattern;
|
|
1356
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
1357
|
-
}
|
|
1358
|
-
} else {
|
|
1359
|
-
ctx.drawImage(img, drawX, drawY, imgW, imgH);
|
|
1360
|
-
}
|
|
1361
|
-
ctx.restore();
|
|
1518
|
+
return this.whiteTexture;
|
|
1519
|
+
}
|
|
1520
|
+
// ── Parallax texture management (REPEAT wrap mode) ────────────────────────
|
|
1521
|
+
loadParallaxTexture(src) {
|
|
1522
|
+
const cached = this.parallaxTextures.get(src);
|
|
1523
|
+
if (cached) return cached;
|
|
1524
|
+
let img = this.parallaxImageCache.get(src);
|
|
1525
|
+
if (!img) {
|
|
1526
|
+
img = new Image();
|
|
1527
|
+
img.src = src;
|
|
1528
|
+
img.onload = () => {
|
|
1529
|
+
const gl = this.gl;
|
|
1530
|
+
const tex = gl.createTexture();
|
|
1531
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1532
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
|
|
1533
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
1534
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
1535
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1536
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
|
1537
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
|
1538
|
+
this.parallaxTextures.set(src, tex);
|
|
1539
|
+
};
|
|
1540
|
+
this.parallaxImageCache.set(src, img);
|
|
1362
1541
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
const zDiff = sa.zIndex - sb.zIndex;
|
|
1380
|
-
if (zDiff !== 0) return zDiff;
|
|
1381
|
-
const ka = textureKey(a);
|
|
1382
|
-
const kb = textureKey(b);
|
|
1383
|
-
if (ka < kb) return -1;
|
|
1384
|
-
if (ka > kb) return 1;
|
|
1385
|
-
return 0;
|
|
1386
|
-
});
|
|
1387
|
-
for (const id of renderables) {
|
|
1388
|
-
const transform = world.getComponent(id, "Transform");
|
|
1389
|
-
const sprite = world.getComponent(id, "Sprite");
|
|
1390
|
-
if (!sprite.visible) continue;
|
|
1391
|
-
const ss = world.getComponent(id, "SquashStretch");
|
|
1392
|
-
const scaleXMod = ss ? ss.currentScaleX : 1;
|
|
1393
|
-
const scaleYMod = ss ? ss.currentScaleY : 1;
|
|
1394
|
-
ctx.save();
|
|
1395
|
-
ctx.translate(transform.x, transform.y);
|
|
1396
|
-
ctx.rotate(transform.rotation);
|
|
1397
|
-
ctx.scale(
|
|
1398
|
-
transform.scaleX * (sprite.flipX ? -1 : 1) * scaleXMod,
|
|
1399
|
-
transform.scaleY * scaleYMod
|
|
1400
|
-
);
|
|
1401
|
-
const drawX = -sprite.anchorX * sprite.width + sprite.offsetX;
|
|
1402
|
-
const drawY = -sprite.anchorY * sprite.height + sprite.offsetY;
|
|
1403
|
-
if (sprite.image && sprite.image.complete && sprite.image.naturalWidth > 0) {
|
|
1404
|
-
if (sprite.frameWidth && sprite.frameHeight) {
|
|
1405
|
-
const cols = sprite.frameColumns ?? Math.floor(sprite.image.naturalWidth / sprite.frameWidth);
|
|
1406
|
-
const col = sprite.frameIndex % cols;
|
|
1407
|
-
const row = Math.floor(sprite.frameIndex / cols);
|
|
1408
|
-
const sx = col * sprite.frameWidth;
|
|
1409
|
-
const sy = row * sprite.frameHeight;
|
|
1410
|
-
ctx.drawImage(sprite.image, sx, sy, sprite.frameWidth, sprite.frameHeight, drawX, drawY, sprite.width, sprite.height);
|
|
1411
|
-
} else if (sprite.frame) {
|
|
1412
|
-
const { sx, sy, sw, sh } = sprite.frame;
|
|
1413
|
-
ctx.drawImage(sprite.image, sx, sy, sw, sh, drawX, drawY, sprite.width, sprite.height);
|
|
1414
|
-
} else if (sprite.tileX || sprite.tileY) {
|
|
1415
|
-
const repeat = sprite.tileX && sprite.tileY ? "repeat" : sprite.tileX ? "repeat-x" : "repeat-y";
|
|
1416
|
-
const pat = ctx.createPattern(sprite.image, repeat);
|
|
1417
|
-
if (pat) {
|
|
1418
|
-
pat.setTransform(new DOMMatrix().translate(drawX, drawY));
|
|
1419
|
-
ctx.fillStyle = pat;
|
|
1420
|
-
ctx.fillRect(drawX, drawY, sprite.width, sprite.height);
|
|
1421
|
-
}
|
|
1422
|
-
} else {
|
|
1423
|
-
ctx.drawImage(sprite.image, drawX, drawY, sprite.width, sprite.height);
|
|
1424
|
-
}
|
|
1425
|
-
} else {
|
|
1426
|
-
ctx.fillStyle = sprite.color;
|
|
1427
|
-
ctx.fillRect(drawX, drawY, sprite.width, sprite.height);
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
// ── Text texture management ───────────────────────────────────────────────
|
|
1545
|
+
getTextTextureKey(text) {
|
|
1546
|
+
return `${text.text}|${text.fontSize ?? 16}|${text.fontFamily ?? "monospace"}|${text.color ?? "#ffffff"}`;
|
|
1547
|
+
}
|
|
1548
|
+
getOrCreateTextTexture(text) {
|
|
1549
|
+
const key = this.getTextTextureKey(text);
|
|
1550
|
+
const cached = this.textureCache.get(key);
|
|
1551
|
+
if (cached) return cached;
|
|
1552
|
+
if (this.textureCache.size >= MAX_TEXT_CACHE) {
|
|
1553
|
+
const oldest = this.textureCacheKeys.shift();
|
|
1554
|
+
if (oldest) {
|
|
1555
|
+
const old = this.textureCache.get(oldest);
|
|
1556
|
+
if (old) this.gl.deleteTexture(old.tex);
|
|
1557
|
+
this.textureCache.delete(oldest);
|
|
1428
1558
|
}
|
|
1429
|
-
ctx.restore();
|
|
1430
|
-
}
|
|
1431
|
-
const textEntities = world.query("Transform", "Text");
|
|
1432
|
-
textEntities.sort((a, b) => {
|
|
1433
|
-
const ta = world.getComponent(a, "Text");
|
|
1434
|
-
const tb = world.getComponent(b, "Text");
|
|
1435
|
-
return ta.zIndex - tb.zIndex;
|
|
1436
|
-
});
|
|
1437
|
-
for (const id of textEntities) {
|
|
1438
|
-
const transform = world.getComponent(id, "Transform");
|
|
1439
|
-
const text = world.getComponent(id, "Text");
|
|
1440
|
-
if (!text.visible) continue;
|
|
1441
|
-
ctx.save();
|
|
1442
|
-
ctx.translate(transform.x + text.offsetX, transform.y + text.offsetY);
|
|
1443
|
-
ctx.rotate(transform.rotation);
|
|
1444
|
-
ctx.font = `${text.fontSize}px ${text.fontFamily}`;
|
|
1445
|
-
ctx.fillStyle = text.color;
|
|
1446
|
-
ctx.textAlign = text.align;
|
|
1447
|
-
ctx.textBaseline = text.baseline;
|
|
1448
|
-
ctx.fillText(text.text, 0, 0, text.maxWidth);
|
|
1449
|
-
ctx.restore();
|
|
1450
1559
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1560
|
+
const offscreen = document.createElement("canvas");
|
|
1561
|
+
const ctx2d = offscreen.getContext("2d");
|
|
1562
|
+
const font = `${text.fontSize ?? 16}px ${text.fontFamily ?? "monospace"}`;
|
|
1563
|
+
ctx2d.font = font;
|
|
1564
|
+
const metrics = ctx2d.measureText(text.text);
|
|
1565
|
+
const textW = Math.ceil(metrics.width) + 4;
|
|
1566
|
+
const textH = Math.ceil((text.fontSize ?? 16) * 1.5) + 4;
|
|
1567
|
+
offscreen.width = textW;
|
|
1568
|
+
offscreen.height = textH;
|
|
1569
|
+
ctx2d.font = font;
|
|
1570
|
+
ctx2d.fillStyle = text.color ?? "#ffffff";
|
|
1571
|
+
ctx2d.textAlign = "left";
|
|
1572
|
+
ctx2d.textBaseline = "top";
|
|
1573
|
+
ctx2d.fillText(text.text, 2, 2, text.maxWidth);
|
|
1574
|
+
const gl = this.gl;
|
|
1575
|
+
const tex = gl.createTexture();
|
|
1576
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1577
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, offscreen);
|
|
1578
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
1579
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
1580
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1581
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1582
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1583
|
+
const entry = { tex, w: textW, h: textH };
|
|
1584
|
+
this.textureCache.set(key, entry);
|
|
1585
|
+
this.textureCacheKeys.push(key);
|
|
1586
|
+
return entry;
|
|
1587
|
+
}
|
|
1588
|
+
// ── Instanced draw call ────────────────────────────────────────────────────
|
|
1589
|
+
flush(count, textureKey) {
|
|
1590
|
+
if (count === 0) return;
|
|
1591
|
+
const { gl } = this;
|
|
1592
|
+
const isColor = textureKey.startsWith("__color__");
|
|
1593
|
+
const tex = isColor ? this.whiteTexture : this.loadTexture(textureKey);
|
|
1594
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1595
|
+
gl.uniform1i(this.uUseTexture, isColor ? 0 : 1);
|
|
1596
|
+
gl.bindVertexArray(this.quadVAO);
|
|
1597
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
|
|
1598
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.instanceData, 0, count * FLOATS_PER_INSTANCE);
|
|
1599
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count);
|
|
1600
|
+
}
|
|
1601
|
+
flushWithTex(count, tex, useTexture) {
|
|
1602
|
+
if (count === 0) return;
|
|
1603
|
+
const { gl } = this;
|
|
1604
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1605
|
+
gl.uniform1i(this.uUseTexture, useTexture ? 1 : 0);
|
|
1606
|
+
gl.bindVertexArray(this.quadVAO);
|
|
1607
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
|
|
1608
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.instanceData, 0, count * FLOATS_PER_INSTANCE);
|
|
1609
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count);
|
|
1610
|
+
}
|
|
1611
|
+
// ── Write one sprite instance into instanceData ───────────────────────────
|
|
1612
|
+
writeInstance(base, x, y, w, h, rot, anchorX, anchorY, offsetX, offsetY, flipX, r, g, b, a, u, v, uw, vh) {
|
|
1613
|
+
const d = this.instanceData;
|
|
1614
|
+
d[base + 0] = x;
|
|
1615
|
+
d[base + 1] = y;
|
|
1616
|
+
d[base + 2] = w;
|
|
1617
|
+
d[base + 3] = h;
|
|
1618
|
+
d[base + 4] = rot;
|
|
1619
|
+
d[base + 5] = anchorX;
|
|
1620
|
+
d[base + 6] = anchorY;
|
|
1621
|
+
d[base + 7] = offsetX;
|
|
1622
|
+
d[base + 8] = offsetY;
|
|
1623
|
+
d[base + 9] = flipX ? 1 : 0;
|
|
1624
|
+
d[base + 10] = r;
|
|
1625
|
+
d[base + 11] = g;
|
|
1626
|
+
d[base + 12] = b;
|
|
1627
|
+
d[base + 13] = a;
|
|
1628
|
+
d[base + 14] = u;
|
|
1629
|
+
d[base + 15] = v;
|
|
1630
|
+
d[base + 16] = uw;
|
|
1631
|
+
d[base + 17] = vh;
|
|
1632
|
+
}
|
|
1633
|
+
// ── Main update loop ───────────────────────────────────────────────────────
|
|
1634
|
+
update(world, dt) {
|
|
1635
|
+
const { gl, canvas } = this;
|
|
1636
|
+
const W = canvas.width;
|
|
1637
|
+
const H = canvas.height;
|
|
1638
|
+
let camX = 0, camY = 0, zoom = 1;
|
|
1639
|
+
let background = "#000000";
|
|
1640
|
+
let shakeX = 0, shakeY = 0;
|
|
1641
|
+
const camId = world.queryOne("Camera2D");
|
|
1642
|
+
if (camId !== void 0) {
|
|
1643
|
+
const cam = world.getComponent(camId, "Camera2D");
|
|
1644
|
+
background = cam.background;
|
|
1645
|
+
if (cam.followEntityId) {
|
|
1646
|
+
const targetId = this.entityIds.get(cam.followEntityId);
|
|
1647
|
+
if (targetId !== void 0) {
|
|
1648
|
+
const t = world.getComponent(targetId, "Transform");
|
|
1649
|
+
if (t) {
|
|
1650
|
+
if (cam.deadZone) {
|
|
1651
|
+
const halfW = cam.deadZone.w / 2;
|
|
1652
|
+
const halfH = cam.deadZone.h / 2;
|
|
1653
|
+
const dx = t.x - cam.x, dy = t.y - cam.y;
|
|
1654
|
+
if (dx > halfW) cam.x = t.x - halfW;
|
|
1655
|
+
else if (dx < -halfW) cam.x = t.x + halfW;
|
|
1656
|
+
if (dy > halfH) cam.y = t.y - halfH;
|
|
1657
|
+
else if (dy < -halfH) cam.y = t.y + halfH;
|
|
1658
|
+
} else if (cam.smoothing > 0) {
|
|
1659
|
+
cam.x += (t.x - cam.x) * (1 - cam.smoothing);
|
|
1660
|
+
cam.y += (t.y - cam.y) * (1 - cam.smoothing);
|
|
1661
|
+
} else {
|
|
1662
|
+
cam.x = t.x;
|
|
1663
|
+
cam.y = t.y;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1479
1666
|
}
|
|
1480
1667
|
}
|
|
1481
|
-
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1668
|
+
if (cam.bounds) {
|
|
1669
|
+
const halfW = W / (2 * cam.zoom);
|
|
1670
|
+
const halfH = H / (2 * cam.zoom);
|
|
1671
|
+
cam.x = Math.max(cam.bounds.x + halfW, Math.min(cam.bounds.x + cam.bounds.width - halfW, cam.x));
|
|
1672
|
+
cam.y = Math.max(cam.bounds.y + halfH, Math.min(cam.bounds.y + cam.bounds.height - halfH, cam.y));
|
|
1486
1673
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
if (trail.points.length > trail.length) trail.points.length = trail.length;
|
|
1494
|
-
if (trail.points.length < 2) continue;
|
|
1495
|
-
for (let i = 1; i < trail.points.length; i++) {
|
|
1496
|
-
const alpha = 1 - i / trail.points.length;
|
|
1497
|
-
ctx.save();
|
|
1498
|
-
ctx.globalAlpha = alpha;
|
|
1499
|
-
ctx.strokeStyle = trail.color;
|
|
1500
|
-
ctx.lineWidth = trail.width;
|
|
1501
|
-
ctx.lineCap = "round";
|
|
1502
|
-
ctx.beginPath();
|
|
1503
|
-
ctx.moveTo(trail.points[i - 1].x, trail.points[i - 1].y);
|
|
1504
|
-
ctx.lineTo(trail.points[i].x, trail.points[i].y);
|
|
1505
|
-
ctx.stroke();
|
|
1506
|
-
ctx.restore();
|
|
1674
|
+
if (cam.shakeTimer > 0) {
|
|
1675
|
+
cam.shakeTimer -= dt;
|
|
1676
|
+
if (cam.shakeTimer < 0) cam.shakeTimer = 0;
|
|
1677
|
+
const progress = cam.shakeDuration > 0 ? cam.shakeTimer / cam.shakeDuration : 0;
|
|
1678
|
+
shakeX = (world.rng() * 2 - 1) * cam.shakeIntensity * progress;
|
|
1679
|
+
shakeY = (world.rng() * 2 - 1) * cam.shakeIntensity * progress;
|
|
1507
1680
|
}
|
|
1681
|
+
camX = cam.x;
|
|
1682
|
+
camY = cam.y;
|
|
1683
|
+
zoom = cam.zoom;
|
|
1508
1684
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1685
|
+
for (const id of world.query("AnimationState", "Sprite")) {
|
|
1686
|
+
const anim = world.getComponent(id, "AnimationState");
|
|
1687
|
+
const sprite = world.getComponent(id, "Sprite");
|
|
1688
|
+
if (!anim.playing || anim.frames.length === 0) continue;
|
|
1689
|
+
anim.timer += dt;
|
|
1690
|
+
const frameDuration = 1 / anim.fps;
|
|
1691
|
+
while (anim.timer >= frameDuration) {
|
|
1692
|
+
anim.timer -= frameDuration;
|
|
1693
|
+
anim.currentIndex++;
|
|
1694
|
+
if (anim.currentIndex >= anim.frames.length) {
|
|
1695
|
+
anim.currentIndex = anim.loop ? 0 : anim.frames.length - 1;
|
|
1696
|
+
}
|
|
1521
1697
|
}
|
|
1698
|
+
sprite.frameIndex = anim.frames[anim.currentIndex];
|
|
1699
|
+
}
|
|
1700
|
+
for (const id of world.query("SquashStretch", "RigidBody")) {
|
|
1701
|
+
const ss = world.getComponent(id, "SquashStretch");
|
|
1702
|
+
const rb = world.getComponent(id, "RigidBody");
|
|
1703
|
+
const spd = Math.sqrt(rb.vx * rb.vx + rb.vy * rb.vy);
|
|
1704
|
+
const tScX = rb.vy < -100 ? 1 + ss.intensity * 0.4 : spd > 50 ? 1 - ss.intensity * 0.3 : 1;
|
|
1705
|
+
const tScY = rb.vy < -100 ? 1 - ss.intensity * 0.4 : spd > 50 ? 1 + ss.intensity * 0.3 : 1;
|
|
1706
|
+
ss.currentScaleX += (tScX - ss.currentScaleX) * ss.recovery * dt;
|
|
1707
|
+
ss.currentScaleY += (tScY - ss.currentScaleY) * ss.recovery * dt;
|
|
1522
1708
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1709
|
+
const [br, bg, bb] = parseCSSColor(background);
|
|
1710
|
+
gl.viewport(0, 0, W, H);
|
|
1711
|
+
gl.clearColor(br, bg, bb, 1);
|
|
1712
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1713
|
+
const parallaxEntities = world.query("ParallaxLayer");
|
|
1714
|
+
if (parallaxEntities.length > 0) {
|
|
1715
|
+
parallaxEntities.sort((a, b) => {
|
|
1716
|
+
const za = world.getComponent(a, "ParallaxLayer").zIndex;
|
|
1717
|
+
const zb = world.getComponent(b, "ParallaxLayer").zIndex;
|
|
1718
|
+
return za - zb;
|
|
1719
|
+
});
|
|
1720
|
+
gl.useProgram(this.parallaxProgram);
|
|
1721
|
+
gl.uniform2f(this.pUCanvasSize, W, H);
|
|
1722
|
+
gl.uniform1i(this.pUTexture, 0);
|
|
1723
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1724
|
+
gl.bindVertexArray(this.parallaxVAO);
|
|
1725
|
+
for (const id of parallaxEntities) {
|
|
1726
|
+
const layer = world.getComponent(id, "ParallaxLayer");
|
|
1727
|
+
let img = this.parallaxImageCache.get(layer.src);
|
|
1728
|
+
if (!img) {
|
|
1729
|
+
this.loadParallaxTexture(layer.src);
|
|
1730
|
+
continue;
|
|
1533
1731
|
}
|
|
1732
|
+
if (!img.complete || img.naturalWidth === 0) continue;
|
|
1733
|
+
if (layer.imageWidth === 0) layer.imageWidth = img.naturalWidth;
|
|
1734
|
+
if (layer.imageHeight === 0) layer.imageHeight = img.naturalHeight;
|
|
1735
|
+
const tex = this.parallaxTextures.get(layer.src);
|
|
1736
|
+
if (!tex) continue;
|
|
1737
|
+
const imgW = layer.imageWidth;
|
|
1738
|
+
const imgH = layer.imageHeight;
|
|
1739
|
+
const drawX = layer.offsetX - camX * layer.speedX;
|
|
1740
|
+
const drawY = layer.offsetY - camY * layer.speedY;
|
|
1741
|
+
const uvOffsetX = (drawX / imgW % 1 + 1) % 1;
|
|
1742
|
+
const uvOffsetY = (drawY / imgH % 1 + 1) % 1;
|
|
1743
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1744
|
+
gl.uniform2f(this.pUUvOffset, uvOffsetX, uvOffsetY);
|
|
1745
|
+
gl.uniform2f(this.pUTexSize, imgW, imgH);
|
|
1746
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
1534
1747
|
}
|
|
1535
1748
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1749
|
+
gl.useProgram(this.program);
|
|
1750
|
+
gl.uniform2f(this.uCamPos, camX, camY);
|
|
1751
|
+
gl.uniform1f(this.uZoom, zoom);
|
|
1752
|
+
gl.uniform2f(this.uCanvasSize, W, H);
|
|
1753
|
+
gl.uniform2f(this.uShake, shakeX, shakeY);
|
|
1754
|
+
gl.uniform1i(this.uTexture, 0);
|
|
1755
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1756
|
+
const renderables = world.query("Transform", "Sprite");
|
|
1757
|
+
renderables.sort((a, b) => {
|
|
1758
|
+
const sa = world.getComponent(a, "Sprite");
|
|
1759
|
+
const sb = world.getComponent(b, "Sprite");
|
|
1760
|
+
const zd = sa.zIndex - sb.zIndex;
|
|
1761
|
+
if (zd !== 0) return zd;
|
|
1762
|
+
const ka = getTextureKey(sa), kb = getTextureKey(sb);
|
|
1763
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
1764
|
+
});
|
|
1765
|
+
let batchCount = 0;
|
|
1766
|
+
let batchKey = "";
|
|
1767
|
+
for (let i = 0; i <= renderables.length; i++) {
|
|
1768
|
+
if (i === renderables.length) {
|
|
1769
|
+
this.flush(batchCount, batchKey);
|
|
1770
|
+
break;
|
|
1544
1771
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1772
|
+
const id = renderables[i];
|
|
1773
|
+
const transform = world.getComponent(id, "Transform");
|
|
1774
|
+
const sprite = world.getComponent(id, "Sprite");
|
|
1775
|
+
if (!sprite.visible) continue;
|
|
1776
|
+
if (sprite.src && !sprite.image) {
|
|
1777
|
+
let img = this.imageCache.get(sprite.src);
|
|
1778
|
+
if (!img) {
|
|
1779
|
+
img = new Image();
|
|
1780
|
+
img.src = sprite.src;
|
|
1781
|
+
this.imageCache.set(sprite.src, img);
|
|
1782
|
+
img.onload = () => {
|
|
1783
|
+
const tex = this.gl.createTexture();
|
|
1784
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, tex);
|
|
1785
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, img);
|
|
1786
|
+
this.gl.generateMipmap(this.gl.TEXTURE_2D);
|
|
1787
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR_MIPMAP_LINEAR);
|
|
1788
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
1789
|
+
this.textures.set(sprite.src, tex);
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
sprite.image = img;
|
|
1793
|
+
}
|
|
1794
|
+
const key = getTextureKey(sprite);
|
|
1795
|
+
if (key !== batchKey && batchCount > 0 || batchCount >= MAX_INSTANCES) {
|
|
1796
|
+
this.flush(batchCount, batchKey);
|
|
1797
|
+
batchCount = 0;
|
|
1798
|
+
}
|
|
1799
|
+
batchKey = key;
|
|
1800
|
+
const ss = world.getComponent(id, "SquashStretch");
|
|
1801
|
+
const scaleXMod = ss ? ss.currentScaleX : 1;
|
|
1802
|
+
const scaleYMod = ss ? ss.currentScaleY : 1;
|
|
1803
|
+
const [r, g, b, a] = parseCSSColor(sprite.color);
|
|
1804
|
+
const uv = getUVRect(sprite);
|
|
1805
|
+
this.writeInstance(
|
|
1806
|
+
batchCount * FLOATS_PER_INSTANCE,
|
|
1807
|
+
transform.x,
|
|
1808
|
+
transform.y,
|
|
1809
|
+
sprite.width * transform.scaleX * scaleXMod,
|
|
1810
|
+
sprite.height * transform.scaleY * scaleYMod,
|
|
1811
|
+
transform.rotation,
|
|
1812
|
+
sprite.anchorX,
|
|
1813
|
+
sprite.anchorY,
|
|
1814
|
+
sprite.offsetX,
|
|
1815
|
+
sprite.offsetY,
|
|
1816
|
+
sprite.flipX,
|
|
1817
|
+
r,
|
|
1818
|
+
g,
|
|
1819
|
+
b,
|
|
1820
|
+
a,
|
|
1821
|
+
uv[0],
|
|
1822
|
+
uv[1],
|
|
1823
|
+
uv[2],
|
|
1824
|
+
uv[3]
|
|
1825
|
+
);
|
|
1826
|
+
batchCount++;
|
|
1827
|
+
}
|
|
1828
|
+
const textEntities = world.query("Transform", "Text");
|
|
1829
|
+
textEntities.sort((a, b) => {
|
|
1830
|
+
const ta = world.getComponent(a, "Text");
|
|
1831
|
+
const tb = world.getComponent(b, "Text");
|
|
1832
|
+
return ta.zIndex - tb.zIndex;
|
|
1833
|
+
});
|
|
1834
|
+
for (const id of textEntities) {
|
|
1835
|
+
const transform = world.getComponent(id, "Transform");
|
|
1836
|
+
const text = world.getComponent(id, "Text");
|
|
1837
|
+
if (!text.visible) continue;
|
|
1838
|
+
const entry = this.getOrCreateTextTexture(text);
|
|
1839
|
+
if (!entry) continue;
|
|
1840
|
+
this.flush(batchCount, batchKey);
|
|
1841
|
+
batchCount = 0;
|
|
1842
|
+
batchKey = "";
|
|
1843
|
+
this.writeInstance(
|
|
1844
|
+
0,
|
|
1845
|
+
transform.x + text.offsetX,
|
|
1846
|
+
transform.y + text.offsetY,
|
|
1847
|
+
entry.w,
|
|
1848
|
+
entry.h,
|
|
1849
|
+
transform.rotation,
|
|
1850
|
+
0,
|
|
1851
|
+
0,
|
|
1852
|
+
// anchor top-left
|
|
1853
|
+
0,
|
|
1854
|
+
0,
|
|
1855
|
+
false,
|
|
1856
|
+
1,
|
|
1857
|
+
1,
|
|
1858
|
+
1,
|
|
1859
|
+
1,
|
|
1860
|
+
// white tint — color baked into texture
|
|
1861
|
+
0,
|
|
1862
|
+
0,
|
|
1863
|
+
1,
|
|
1864
|
+
1
|
|
1865
|
+
);
|
|
1866
|
+
this.flushWithTex(1, entry.tex, true);
|
|
1867
|
+
}
|
|
1868
|
+
for (const id of world.query("Transform", "ParticlePool")) {
|
|
1869
|
+
const t = world.getComponent(id, "Transform");
|
|
1870
|
+
const pool = world.getComponent(id, "ParticlePool");
|
|
1871
|
+
pool.particles = pool.particles.filter((p) => {
|
|
1872
|
+
p.life -= dt;
|
|
1873
|
+
p.x += p.vx * dt;
|
|
1874
|
+
p.y += p.vy * dt;
|
|
1875
|
+
p.vy += p.gravity * dt;
|
|
1876
|
+
return p.life > 0;
|
|
1877
|
+
});
|
|
1878
|
+
if (pool.active && pool.particles.length < pool.maxParticles) {
|
|
1879
|
+
pool.timer += dt;
|
|
1880
|
+
const spawnCount = Math.floor(pool.timer * pool.rate);
|
|
1881
|
+
pool.timer -= spawnCount / pool.rate;
|
|
1882
|
+
for (let i = 0; i < spawnCount && pool.particles.length < pool.maxParticles; i++) {
|
|
1883
|
+
const angle = pool.angle + (world.rng() - 0.5) * pool.spread;
|
|
1884
|
+
const speed = pool.speed * (0.5 + world.rng() * 0.5);
|
|
1885
|
+
pool.particles.push({
|
|
1886
|
+
x: t.x,
|
|
1887
|
+
y: t.y,
|
|
1888
|
+
vx: Math.cos(angle) * speed,
|
|
1889
|
+
vy: Math.sin(angle) * speed,
|
|
1890
|
+
life: pool.particleLife,
|
|
1891
|
+
maxLife: pool.particleLife,
|
|
1892
|
+
size: pool.particleSize,
|
|
1893
|
+
color: pool.color,
|
|
1894
|
+
gravity: pool.gravity
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
let pCount = 0;
|
|
1899
|
+
const pKey = `__color__`;
|
|
1900
|
+
for (const p of pool.particles) {
|
|
1901
|
+
if (pCount >= MAX_INSTANCES) {
|
|
1902
|
+
this.flush(pCount, pKey);
|
|
1903
|
+
pCount = 0;
|
|
1904
|
+
}
|
|
1905
|
+
const alpha = p.life / p.maxLife;
|
|
1906
|
+
const [r, g, b] = parseCSSColor(p.color);
|
|
1907
|
+
this.writeInstance(
|
|
1908
|
+
pCount * FLOATS_PER_INSTANCE,
|
|
1909
|
+
p.x,
|
|
1910
|
+
p.y,
|
|
1911
|
+
p.size,
|
|
1912
|
+
p.size,
|
|
1913
|
+
0,
|
|
1914
|
+
0.5,
|
|
1915
|
+
0.5,
|
|
1916
|
+
0,
|
|
1917
|
+
0,
|
|
1918
|
+
false,
|
|
1919
|
+
r,
|
|
1920
|
+
g,
|
|
1921
|
+
b,
|
|
1922
|
+
alpha,
|
|
1923
|
+
0,
|
|
1924
|
+
0,
|
|
1925
|
+
1,
|
|
1926
|
+
1
|
|
1927
|
+
);
|
|
1928
|
+
pCount++;
|
|
1929
|
+
}
|
|
1930
|
+
if (pCount > 0) this.flush(pCount, pKey);
|
|
1931
|
+
}
|
|
1932
|
+
for (const id of world.query("Transform", "Trail")) {
|
|
1933
|
+
const t = world.getComponent(id, "Transform");
|
|
1934
|
+
const trail = world.getComponent(id, "Trail");
|
|
1935
|
+
trail.points.unshift({ x: t.x, y: t.y });
|
|
1936
|
+
if (trail.points.length > trail.length) trail.points.length = trail.length;
|
|
1937
|
+
if (trail.points.length < 1) continue;
|
|
1938
|
+
const [tr, tg, tb] = parseCSSColor(trail.color);
|
|
1939
|
+
const trailW = trail.width > 0 ? trail.width : 1;
|
|
1940
|
+
let tCount = 0;
|
|
1941
|
+
for (let i = 0; i < trail.points.length; i++) {
|
|
1942
|
+
if (tCount >= MAX_INSTANCES) {
|
|
1943
|
+
this.flush(tCount, "__color__");
|
|
1944
|
+
tCount = 0;
|
|
1945
|
+
}
|
|
1946
|
+
const alpha = 1 - i / trail.points.length;
|
|
1947
|
+
this.writeInstance(
|
|
1948
|
+
tCount * FLOATS_PER_INSTANCE,
|
|
1949
|
+
trail.points[i].x,
|
|
1950
|
+
trail.points[i].y,
|
|
1951
|
+
trailW,
|
|
1952
|
+
trailW,
|
|
1953
|
+
0,
|
|
1954
|
+
0.5,
|
|
1955
|
+
0.5,
|
|
1956
|
+
0,
|
|
1957
|
+
0,
|
|
1958
|
+
false,
|
|
1959
|
+
tr,
|
|
1960
|
+
tg,
|
|
1961
|
+
tb,
|
|
1962
|
+
alpha,
|
|
1963
|
+
0,
|
|
1964
|
+
0,
|
|
1965
|
+
1,
|
|
1966
|
+
1
|
|
1967
|
+
);
|
|
1968
|
+
tCount++;
|
|
1969
|
+
}
|
|
1970
|
+
if (tCount > 0) this.flush(tCount, "__color__");
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
// ../../packages/renderer/src/canvas2d.ts
|
|
1976
|
+
var Canvas2DRenderer = class {
|
|
1977
|
+
constructor(canvas) {
|
|
1978
|
+
this.canvas = canvas;
|
|
1979
|
+
const ctx = canvas.getContext("2d");
|
|
1980
|
+
if (!ctx) throw new Error("Could not get 2D context from canvas");
|
|
1981
|
+
this.ctx = ctx;
|
|
1982
|
+
}
|
|
1983
|
+
ctx;
|
|
1984
|
+
clear(color) {
|
|
1985
|
+
if (color) {
|
|
1986
|
+
this.ctx.fillStyle = color;
|
|
1987
|
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1988
|
+
} else {
|
|
1989
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
get width() {
|
|
1993
|
+
return this.canvas.width;
|
|
1994
|
+
}
|
|
1995
|
+
get height() {
|
|
1996
|
+
return this.canvas.height;
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
// ../../packages/physics/src/components/rigidbody.ts
|
|
2001
|
+
function createRigidBody(opts) {
|
|
2002
|
+
return {
|
|
2003
|
+
type: "RigidBody",
|
|
2004
|
+
vx: 0,
|
|
2005
|
+
vy: 0,
|
|
2006
|
+
mass: 1,
|
|
2007
|
+
gravityScale: 1,
|
|
2008
|
+
isStatic: false,
|
|
2009
|
+
onGround: false,
|
|
2010
|
+
isNearGround: false,
|
|
2011
|
+
bounce: 0,
|
|
2012
|
+
friction: 0.85,
|
|
2013
|
+
lockX: false,
|
|
2014
|
+
lockY: false,
|
|
2015
|
+
isKinematic: false,
|
|
2016
|
+
dropThrough: 0,
|
|
2017
|
+
...opts
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1584
2021
|
// ../../packages/physics/src/components/boxCollider.ts
|
|
1585
2022
|
function createBoxCollider(width, height, opts) {
|
|
1586
2023
|
return {
|
|
@@ -2538,7 +2975,7 @@ function DevToolsOverlay({ handle, loop, ecs, engine }) {
|
|
|
2538
2975
|
const selectedEntityData = selectedEntity !== null ? entities.find((e) => e.id === selectedEntity) : null;
|
|
2539
2976
|
const timings = engine?.systemTimings;
|
|
2540
2977
|
const fps = (() => {
|
|
2541
|
-
const sys = engine?.
|
|
2978
|
+
const sys = engine?.activeRenderSystem;
|
|
2542
2979
|
if (!sys) return 0;
|
|
2543
2980
|
const ft = sys.frameTimes ?? [];
|
|
2544
2981
|
if (ft.length === 0) return 0;
|
|
@@ -2766,935 +3203,125 @@ function AssetsTab({ assetCache }) {
|
|
|
2766
3203
|
/* @__PURE__ */ jsx("span", { style: { color: C.text, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: src.split("/").pop() }),
|
|
2767
3204
|
loaded && /* @__PURE__ */ jsxs("span", { style: { color: C.muted, fontSize: 9 }, children: [
|
|
2768
3205
|
img.naturalWidth,
|
|
2769
|
-
"\xD7",
|
|
2770
|
-
img.naturalHeight
|
|
2771
|
-
] })
|
|
2772
|
-
] }, src);
|
|
2773
|
-
})
|
|
2774
|
-
] });
|
|
2775
|
-
}
|
|
2776
|
-
function formatValue(v) {
|
|
2777
|
-
if (typeof v === "number") return v.toFixed(2);
|
|
2778
|
-
if (typeof v === "boolean") return v ? "true" : "false";
|
|
2779
|
-
if (v === null || v === void 0) return "\u2014";
|
|
2780
|
-
if (typeof v === "object") return JSON.stringify(v).slice(0, 60);
|
|
2781
|
-
return String(v);
|
|
2782
|
-
}
|
|
2783
|
-
function getActiveKeys(engine) {
|
|
2784
|
-
const kb = engine.input.keyboard;
|
|
2785
|
-
return kb.held ? Array.from(kb.held) : [];
|
|
2786
|
-
}
|
|
2787
|
-
|
|
2788
|
-
// ../devtools/src/debugSystem.ts
|
|
2789
|
-
var DebugSystem = class {
|
|
2790
|
-
constructor(renderer) {
|
|
2791
|
-
this.renderer = renderer;
|
|
2792
|
-
}
|
|
2793
|
-
frameCount = 0;
|
|
2794
|
-
lastFpsTime = 0;
|
|
2795
|
-
fps = 0;
|
|
2796
|
-
update(world, dt) {
|
|
2797
|
-
const { ctx, canvas } = this.renderer;
|
|
2798
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
2799
|
-
this.frameCount++;
|
|
2800
|
-
this.lastFpsTime += dt;
|
|
2801
|
-
if (this.lastFpsTime >= 0.5) {
|
|
2802
|
-
this.fps = Math.round(this.frameCount / this.lastFpsTime);
|
|
2803
|
-
this.frameCount = 0;
|
|
2804
|
-
this.lastFpsTime = 0;
|
|
2805
|
-
}
|
|
2806
|
-
const camId = world.queryOne("Camera2D");
|
|
2807
|
-
let camX = 0, camY = 0, zoom = 1;
|
|
2808
|
-
if (camId !== void 0) {
|
|
2809
|
-
const cam = world.getComponent(camId, "Camera2D");
|
|
2810
|
-
camX = cam.x;
|
|
2811
|
-
camY = cam.y;
|
|
2812
|
-
zoom = cam.zoom;
|
|
2813
|
-
}
|
|
2814
|
-
ctx.save();
|
|
2815
|
-
ctx.translate(canvas.width / 2 - camX * zoom, canvas.height / 2 - camY * zoom);
|
|
2816
|
-
ctx.scale(zoom, zoom);
|
|
2817
|
-
const lw = 1 / zoom;
|
|
2818
|
-
for (const id of world.query("Transform", "BoxCollider")) {
|
|
2819
|
-
const t = world.getComponent(id, "Transform");
|
|
2820
|
-
const c = world.getComponent(id, "BoxCollider");
|
|
2821
|
-
ctx.strokeStyle = c.isTrigger ? "rgba(255,200,0,0.85)" : "rgba(0,255,120,0.85)";
|
|
2822
|
-
ctx.lineWidth = lw;
|
|
2823
|
-
ctx.strokeRect(
|
|
2824
|
-
t.x + c.offsetX - c.width / 2,
|
|
2825
|
-
t.y + c.offsetY - c.height / 2,
|
|
2826
|
-
c.width,
|
|
2827
|
-
c.height
|
|
2828
|
-
);
|
|
2829
|
-
ctx.fillStyle = "rgba(255,255,255,0.5)";
|
|
2830
|
-
ctx.font = `${10 / zoom}px monospace`;
|
|
2831
|
-
ctx.fillText(String(id), t.x + c.offsetX - c.width / 2 + lw, t.y + c.offsetY - c.height / 2 - lw * 2);
|
|
2832
|
-
}
|
|
2833
|
-
if (camId !== void 0) {
|
|
2834
|
-
const camFull = world.getComponent(camId, "Camera2D");
|
|
2835
|
-
if (camFull.bounds) {
|
|
2836
|
-
const b = camFull.bounds;
|
|
2837
|
-
ctx.strokeStyle = "rgba(0, 255, 255, 0.4)";
|
|
2838
|
-
ctx.lineWidth = 1 / zoom;
|
|
2839
|
-
ctx.setLineDash([8 / zoom, 4 / zoom]);
|
|
2840
|
-
ctx.strokeRect(b.x, b.y, b.width, b.height);
|
|
2841
|
-
ctx.setLineDash([]);
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
|
-
ctx.restore();
|
|
2845
|
-
const GRID_SIZE = 128;
|
|
2846
|
-
ctx.save();
|
|
2847
|
-
ctx.strokeStyle = "rgba(255, 255, 255, 0.04)";
|
|
2848
|
-
ctx.lineWidth = 1;
|
|
2849
|
-
ctx.setLineDash([]);
|
|
2850
|
-
const offsetX = camX - canvas.width / (2 * zoom);
|
|
2851
|
-
const offsetY = camY - canvas.height / (2 * zoom);
|
|
2852
|
-
const visibleW = canvas.width / zoom;
|
|
2853
|
-
const visibleH = canvas.height / zoom;
|
|
2854
|
-
const startCol = Math.floor(offsetX / GRID_SIZE);
|
|
2855
|
-
const endCol = Math.ceil((offsetX + visibleW) / GRID_SIZE);
|
|
2856
|
-
const startRow = Math.floor(offsetY / GRID_SIZE);
|
|
2857
|
-
const endRow = Math.ceil((offsetY + visibleH) / GRID_SIZE);
|
|
2858
|
-
ctx.translate(canvas.width / 2 - camX * zoom, canvas.height / 2 - camY * zoom);
|
|
2859
|
-
ctx.scale(zoom, zoom);
|
|
2860
|
-
for (let col = startCol; col <= endCol; col++) {
|
|
2861
|
-
const wx = col * GRID_SIZE;
|
|
2862
|
-
ctx.beginPath();
|
|
2863
|
-
ctx.moveTo(wx, startRow * GRID_SIZE);
|
|
2864
|
-
ctx.lineTo(wx, endRow * GRID_SIZE);
|
|
2865
|
-
ctx.stroke();
|
|
2866
|
-
}
|
|
2867
|
-
for (let row = startRow; row <= endRow; row++) {
|
|
2868
|
-
const wy = row * GRID_SIZE;
|
|
2869
|
-
ctx.beginPath();
|
|
2870
|
-
ctx.moveTo(startCol * GRID_SIZE, wy);
|
|
2871
|
-
ctx.lineTo(endCol * GRID_SIZE, wy);
|
|
2872
|
-
ctx.stroke();
|
|
2873
|
-
}
|
|
2874
|
-
ctx.restore();
|
|
2875
|
-
const entityCount = world.entityCount;
|
|
2876
|
-
const physicsCount = world.query("RigidBody", "BoxCollider").length;
|
|
2877
|
-
const renderCount = world.query("Transform", "Sprite").length;
|
|
2878
|
-
ctx.save();
|
|
2879
|
-
ctx.fillStyle = "rgba(0,0,0,0.65)";
|
|
2880
|
-
ctx.fillRect(8, 8, 184, 84);
|
|
2881
|
-
ctx.fillStyle = "#00ff88";
|
|
2882
|
-
ctx.font = "11px monospace";
|
|
2883
|
-
ctx.fillText(`FPS ${this.fps}`, 16, 26);
|
|
2884
|
-
ctx.fillText(`Entities ${entityCount}`, 16, 42);
|
|
2885
|
-
ctx.fillText(`Physics ${physicsCount}`, 16, 58);
|
|
2886
|
-
ctx.fillText(`Renderables ${renderCount}`, 16, 74);
|
|
2887
|
-
ctx.restore();
|
|
2888
|
-
}
|
|
2889
|
-
};
|
|
2890
|
-
|
|
2891
|
-
// ../../packages/webgl-renderer/src/shaders.ts
|
|
2892
|
-
var VERT_SRC = `#version 300 es
|
|
2893
|
-
layout(location = 0) in vec2 a_quadPos;
|
|
2894
|
-
layout(location = 1) in vec2 a_uv;
|
|
2895
|
-
|
|
2896
|
-
layout(location = 2) in vec2 i_pos;
|
|
2897
|
-
layout(location = 3) in vec2 i_size;
|
|
2898
|
-
layout(location = 4) in float i_rot;
|
|
2899
|
-
layout(location = 5) in vec2 i_anchor;
|
|
2900
|
-
layout(location = 6) in vec2 i_offset;
|
|
2901
|
-
layout(location = 7) in float i_flipX;
|
|
2902
|
-
layout(location = 8) in vec4 i_color;
|
|
2903
|
-
layout(location = 9) in vec4 i_uvRect;
|
|
2904
|
-
|
|
2905
|
-
uniform vec2 u_camPos;
|
|
2906
|
-
uniform float u_zoom;
|
|
2907
|
-
uniform vec2 u_canvasSize;
|
|
2908
|
-
uniform vec2 u_shake;
|
|
2909
|
-
|
|
2910
|
-
out vec2 v_uv;
|
|
2911
|
-
out vec4 v_color;
|
|
2912
|
-
|
|
2913
|
-
void main() {
|
|
2914
|
-
// Local position: map quad corner (-0.5..0.5) to draw rect, applying anchor
|
|
2915
|
-
vec2 local = (a_quadPos - vec2(i_anchor.x - 0.5, i_anchor.y - 0.5)) * i_size + i_offset;
|
|
2916
|
-
|
|
2917
|
-
// Horizontal flip
|
|
2918
|
-
if (i_flipX > 0.5) local.x = -local.x;
|
|
2919
|
-
|
|
2920
|
-
// Rotate around local origin
|
|
2921
|
-
float c = cos(i_rot);
|
|
2922
|
-
float s = sin(i_rot);
|
|
2923
|
-
local = vec2(c * local.x - s * local.y, s * local.x + c * local.y);
|
|
2924
|
-
|
|
2925
|
-
// World position
|
|
2926
|
-
vec2 world = i_pos + local;
|
|
2927
|
-
|
|
2928
|
-
// Camera \u2192 NDC clip space (Y is flipped: canvas Y down, WebGL Y up)
|
|
2929
|
-
// Equivalent to Canvas2D: translate(W/2 - camX*zoom + shakeX, H/2 - camY*zoom + shakeY); scale(zoom,zoom)
|
|
2930
|
-
float cx = 2.0 * u_zoom / u_canvasSize.x * (world.x - u_camPos.x) + 2.0 * u_shake.x / u_canvasSize.x;
|
|
2931
|
-
float cy = -2.0 * u_zoom / u_canvasSize.y * (world.y - u_camPos.y) - 2.0 * u_shake.y / u_canvasSize.y;
|
|
2932
|
-
|
|
2933
|
-
gl_Position = vec4(cx, cy, 0.0, 1.0);
|
|
2934
|
-
|
|
2935
|
-
// Remap UV [0,1] to the sub-rect defined by i_uvRect
|
|
2936
|
-
v_uv = i_uvRect.xy + a_uv * i_uvRect.zw;
|
|
2937
|
-
v_color = i_color;
|
|
2938
|
-
}
|
|
2939
|
-
`;
|
|
2940
|
-
var FRAG_SRC = `#version 300 es
|
|
2941
|
-
precision mediump float;
|
|
2942
|
-
|
|
2943
|
-
in vec2 v_uv;
|
|
2944
|
-
in vec4 v_color;
|
|
2945
|
-
|
|
2946
|
-
uniform sampler2D u_texture;
|
|
2947
|
-
uniform int u_useTexture;
|
|
2948
|
-
|
|
2949
|
-
out vec4 fragColor;
|
|
2950
|
-
|
|
2951
|
-
void main() {
|
|
2952
|
-
if (u_useTexture == 1) {
|
|
2953
|
-
fragColor = texture(u_texture, v_uv) * v_color;
|
|
2954
|
-
} else {
|
|
2955
|
-
fragColor = v_color;
|
|
2956
|
-
}
|
|
2957
|
-
}
|
|
2958
|
-
`;
|
|
2959
|
-
var PARALLAX_VERT_SRC = `#version 300 es
|
|
2960
|
-
layout(location = 0) in vec2 a_pos;
|
|
2961
|
-
|
|
2962
|
-
out vec2 v_fragCoord;
|
|
2963
|
-
|
|
2964
|
-
void main() {
|
|
2965
|
-
gl_Position = vec4(a_pos, 0.0, 1.0);
|
|
2966
|
-
// Convert NDC (-1..1) to canvas pixel coords (0..canvasSize) in the frag shader
|
|
2967
|
-
v_fragCoord = a_pos * 0.5 + 0.5; // 0..1 normalized screen coord
|
|
2968
|
-
}
|
|
2969
|
-
`;
|
|
2970
|
-
var PARALLAX_FRAG_SRC = `#version 300 es
|
|
2971
|
-
precision mediump float;
|
|
2972
|
-
|
|
2973
|
-
in vec2 v_fragCoord;
|
|
2974
|
-
|
|
2975
|
-
uniform sampler2D u_texture;
|
|
2976
|
-
uniform vec2 u_uvOffset;
|
|
2977
|
-
uniform vec2 u_texSize; // texture size in pixels
|
|
2978
|
-
uniform vec2 u_canvasSize; // canvas size in pixels
|
|
2979
|
-
|
|
2980
|
-
out vec4 fragColor;
|
|
2981
|
-
|
|
2982
|
-
void main() {
|
|
2983
|
-
// Screen pixel position
|
|
2984
|
-
vec2 screenPx = v_fragCoord * u_canvasSize;
|
|
2985
|
-
// Tile: offset by uvOffset and wrap
|
|
2986
|
-
vec2 uv = mod((screenPx / u_texSize + u_uvOffset), 1.0);
|
|
2987
|
-
// Y must be flipped because WebGL origin is bottom-left but canvas is top-left
|
|
2988
|
-
uv.y = 1.0 - uv.y;
|
|
2989
|
-
fragColor = texture(u_texture, uv);
|
|
2990
|
-
}
|
|
2991
|
-
`;
|
|
2992
|
-
|
|
2993
|
-
// ../../packages/webgl-renderer/src/colorParser.ts
|
|
2994
|
-
var cache = /* @__PURE__ */ new Map();
|
|
2995
|
-
function parseCSSColor(css) {
|
|
2996
|
-
const hit = cache.get(css);
|
|
2997
|
-
if (hit) return hit;
|
|
2998
|
-
let result = [1, 1, 1, 1];
|
|
2999
|
-
if (css.startsWith("#")) {
|
|
3000
|
-
const h = css.slice(1);
|
|
3001
|
-
if (h.length === 3 || h.length === 4) {
|
|
3002
|
-
const r = parseInt(h[0] + h[0], 16) / 255;
|
|
3003
|
-
const g = parseInt(h[1] + h[1], 16) / 255;
|
|
3004
|
-
const b = parseInt(h[2] + h[2], 16) / 255;
|
|
3005
|
-
const a = h.length === 4 ? parseInt(h[3] + h[3], 16) / 255 : 1;
|
|
3006
|
-
result = [r, g, b, a];
|
|
3007
|
-
} else if (h.length === 6 || h.length === 8) {
|
|
3008
|
-
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
3009
|
-
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
3010
|
-
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
3011
|
-
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
|
|
3012
|
-
result = [r, g, b, a];
|
|
3013
|
-
}
|
|
3014
|
-
} else {
|
|
3015
|
-
const m = css.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
|
|
3016
|
-
if (m) {
|
|
3017
|
-
result = [
|
|
3018
|
-
parseInt(m[1]) / 255,
|
|
3019
|
-
parseInt(m[2]) / 255,
|
|
3020
|
-
parseInt(m[3]) / 255,
|
|
3021
|
-
m[4] !== void 0 ? parseFloat(m[4]) : 1
|
|
3022
|
-
];
|
|
3023
|
-
}
|
|
3024
|
-
}
|
|
3025
|
-
cache.set(css, result);
|
|
3026
|
-
return result;
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
// ../../packages/webgl-renderer/src/webglRenderSystem.ts
|
|
3030
|
-
var FLOATS_PER_INSTANCE = 18;
|
|
3031
|
-
var MAX_INSTANCES = 8192;
|
|
3032
|
-
var MAX_TEXT_CACHE = 200;
|
|
3033
|
-
function compileShader(gl, type, src) {
|
|
3034
|
-
const shader = gl.createShader(type);
|
|
3035
|
-
gl.shaderSource(shader, src);
|
|
3036
|
-
gl.compileShader(shader);
|
|
3037
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
3038
|
-
throw new Error(`[WebGLRenderer] Shader compile error:
|
|
3039
|
-
${gl.getShaderInfoLog(shader)}`);
|
|
3040
|
-
}
|
|
3041
|
-
return shader;
|
|
3042
|
-
}
|
|
3043
|
-
function createProgram(gl, vertSrc, fragSrc) {
|
|
3044
|
-
const vert = compileShader(gl, gl.VERTEX_SHADER, vertSrc);
|
|
3045
|
-
const frag = compileShader(gl, gl.FRAGMENT_SHADER, fragSrc);
|
|
3046
|
-
const prog = gl.createProgram();
|
|
3047
|
-
gl.attachShader(prog, vert);
|
|
3048
|
-
gl.attachShader(prog, frag);
|
|
3049
|
-
gl.linkProgram(prog);
|
|
3050
|
-
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
|
3051
|
-
throw new Error(`[WebGLRenderer] Program link error:
|
|
3052
|
-
${gl.getProgramInfoLog(prog)}`);
|
|
3053
|
-
}
|
|
3054
|
-
gl.deleteShader(vert);
|
|
3055
|
-
gl.deleteShader(frag);
|
|
3056
|
-
return prog;
|
|
3057
|
-
}
|
|
3058
|
-
function createWhiteTexture(gl) {
|
|
3059
|
-
const tex = gl.createTexture();
|
|
3060
|
-
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
3061
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255, 255]));
|
|
3062
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
3063
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
3064
|
-
return tex;
|
|
3065
|
-
}
|
|
3066
|
-
function getTextureKey(sprite) {
|
|
3067
|
-
if (sprite.image?.src) return sprite.image.src;
|
|
3068
|
-
if (sprite.src) return sprite.src;
|
|
3069
|
-
return `__color__:${sprite.color}`;
|
|
3070
|
-
}
|
|
3071
|
-
function getUVRect(sprite) {
|
|
3072
|
-
if (!sprite.image || sprite.image.naturalWidth === 0) return [0, 0, 1, 1];
|
|
3073
|
-
const iw = sprite.image.naturalWidth;
|
|
3074
|
-
const ih = sprite.image.naturalHeight;
|
|
3075
|
-
if (sprite.frameWidth && sprite.frameHeight) {
|
|
3076
|
-
const cols = sprite.frameColumns ?? Math.floor(iw / sprite.frameWidth);
|
|
3077
|
-
const col = sprite.frameIndex % cols;
|
|
3078
|
-
const row = Math.floor(sprite.frameIndex / cols);
|
|
3079
|
-
return [
|
|
3080
|
-
col * sprite.frameWidth / iw,
|
|
3081
|
-
row * sprite.frameHeight / ih,
|
|
3082
|
-
sprite.frameWidth / iw,
|
|
3083
|
-
sprite.frameHeight / ih
|
|
3084
|
-
];
|
|
3085
|
-
}
|
|
3086
|
-
if (sprite.frame) {
|
|
3087
|
-
const { sx, sy, sw, sh } = sprite.frame;
|
|
3088
|
-
return [sx / iw, sy / ih, sw / iw, sh / ih];
|
|
3089
|
-
}
|
|
3090
|
-
return [0, 0, 1, 1];
|
|
3091
|
-
}
|
|
3092
|
-
var WebGLRenderSystem = class {
|
|
3093
|
-
constructor(canvas, entityIds) {
|
|
3094
|
-
this.canvas = canvas;
|
|
3095
|
-
this.entityIds = entityIds;
|
|
3096
|
-
const gl = canvas.getContext("webgl2", { alpha: false, antialias: false, premultipliedAlpha: false });
|
|
3097
|
-
if (!gl) throw new Error("[WebGLRenderer] WebGL2 is not supported in this browser");
|
|
3098
|
-
this.gl = gl;
|
|
3099
|
-
this.program = createProgram(gl, VERT_SRC, FRAG_SRC);
|
|
3100
|
-
const quadVerts = new Float32Array([
|
|
3101
|
-
-0.5,
|
|
3102
|
-
-0.5,
|
|
3103
|
-
0,
|
|
3104
|
-
0,
|
|
3105
|
-
0.5,
|
|
3106
|
-
-0.5,
|
|
3107
|
-
1,
|
|
3108
|
-
0,
|
|
3109
|
-
-0.5,
|
|
3110
|
-
0.5,
|
|
3111
|
-
0,
|
|
3112
|
-
1,
|
|
3113
|
-
0.5,
|
|
3114
|
-
-0.5,
|
|
3115
|
-
1,
|
|
3116
|
-
0,
|
|
3117
|
-
0.5,
|
|
3118
|
-
0.5,
|
|
3119
|
-
1,
|
|
3120
|
-
1,
|
|
3121
|
-
-0.5,
|
|
3122
|
-
0.5,
|
|
3123
|
-
0,
|
|
3124
|
-
1
|
|
3125
|
-
]);
|
|
3126
|
-
this.quadVAO = gl.createVertexArray();
|
|
3127
|
-
gl.bindVertexArray(this.quadVAO);
|
|
3128
|
-
const quadBuf = gl.createBuffer();
|
|
3129
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
|
3130
|
-
gl.bufferData(gl.ARRAY_BUFFER, quadVerts, gl.STATIC_DRAW);
|
|
3131
|
-
const qStride = 4 * 4;
|
|
3132
|
-
gl.enableVertexAttribArray(0);
|
|
3133
|
-
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, qStride, 0);
|
|
3134
|
-
gl.enableVertexAttribArray(1);
|
|
3135
|
-
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, qStride, 2 * 4);
|
|
3136
|
-
this.instanceData = new Float32Array(MAX_INSTANCES * FLOATS_PER_INSTANCE);
|
|
3137
|
-
this.instanceBuffer = gl.createBuffer();
|
|
3138
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
|
|
3139
|
-
gl.bufferData(gl.ARRAY_BUFFER, this.instanceData.byteLength, gl.DYNAMIC_DRAW);
|
|
3140
|
-
const iStride = FLOATS_PER_INSTANCE * 4;
|
|
3141
|
-
let byteOffset = 0;
|
|
3142
|
-
const addAttr = (loc, size) => {
|
|
3143
|
-
gl.enableVertexAttribArray(loc);
|
|
3144
|
-
gl.vertexAttribPointer(loc, size, gl.FLOAT, false, iStride, byteOffset);
|
|
3145
|
-
gl.vertexAttribDivisor(loc, 1);
|
|
3146
|
-
byteOffset += size * 4;
|
|
3147
|
-
};
|
|
3148
|
-
addAttr(2, 2);
|
|
3149
|
-
addAttr(3, 2);
|
|
3150
|
-
addAttr(4, 1);
|
|
3151
|
-
addAttr(5, 2);
|
|
3152
|
-
addAttr(6, 2);
|
|
3153
|
-
addAttr(7, 1);
|
|
3154
|
-
addAttr(8, 4);
|
|
3155
|
-
addAttr(9, 4);
|
|
3156
|
-
gl.bindVertexArray(null);
|
|
3157
|
-
gl.useProgram(this.program);
|
|
3158
|
-
this.uCamPos = gl.getUniformLocation(this.program, "u_camPos");
|
|
3159
|
-
this.uZoom = gl.getUniformLocation(this.program, "u_zoom");
|
|
3160
|
-
this.uCanvasSize = gl.getUniformLocation(this.program, "u_canvasSize");
|
|
3161
|
-
this.uShake = gl.getUniformLocation(this.program, "u_shake");
|
|
3162
|
-
this.uTexture = gl.getUniformLocation(this.program, "u_texture");
|
|
3163
|
-
this.uUseTexture = gl.getUniformLocation(this.program, "u_useTexture");
|
|
3164
|
-
this.whiteTexture = createWhiteTexture(gl);
|
|
3165
|
-
this.parallaxProgram = createProgram(gl, PARALLAX_VERT_SRC, PARALLAX_FRAG_SRC);
|
|
3166
|
-
const fsVerts = new Float32Array([
|
|
3167
|
-
-1,
|
|
3168
|
-
-1,
|
|
3169
|
-
1,
|
|
3170
|
-
-1,
|
|
3171
|
-
-1,
|
|
3172
|
-
1,
|
|
3173
|
-
1,
|
|
3174
|
-
-1,
|
|
3175
|
-
1,
|
|
3176
|
-
1,
|
|
3177
|
-
-1,
|
|
3178
|
-
1
|
|
3179
|
-
]);
|
|
3180
|
-
this.parallaxVAO = gl.createVertexArray();
|
|
3181
|
-
gl.bindVertexArray(this.parallaxVAO);
|
|
3182
|
-
const fsBuf = gl.createBuffer();
|
|
3183
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, fsBuf);
|
|
3184
|
-
gl.bufferData(gl.ARRAY_BUFFER, fsVerts, gl.STATIC_DRAW);
|
|
3185
|
-
gl.enableVertexAttribArray(0);
|
|
3186
|
-
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0);
|
|
3187
|
-
gl.bindVertexArray(null);
|
|
3188
|
-
gl.useProgram(this.parallaxProgram);
|
|
3189
|
-
this.pUTexture = gl.getUniformLocation(this.parallaxProgram, "u_texture");
|
|
3190
|
-
this.pUUvOffset = gl.getUniformLocation(this.parallaxProgram, "u_uvOffset");
|
|
3191
|
-
this.pUTexSize = gl.getUniformLocation(this.parallaxProgram, "u_texSize");
|
|
3192
|
-
this.pUCanvasSize = gl.getUniformLocation(this.parallaxProgram, "u_canvasSize");
|
|
3193
|
-
gl.enable(gl.BLEND);
|
|
3194
|
-
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
3195
|
-
}
|
|
3196
|
-
gl;
|
|
3197
|
-
program;
|
|
3198
|
-
quadVAO;
|
|
3199
|
-
instanceBuffer;
|
|
3200
|
-
instanceData;
|
|
3201
|
-
whiteTexture;
|
|
3202
|
-
textures = /* @__PURE__ */ new Map();
|
|
3203
|
-
imageCache = /* @__PURE__ */ new Map();
|
|
3204
|
-
// Cached uniform locations — sprite program
|
|
3205
|
-
uCamPos;
|
|
3206
|
-
uZoom;
|
|
3207
|
-
uCanvasSize;
|
|
3208
|
-
uShake;
|
|
3209
|
-
uTexture;
|
|
3210
|
-
uUseTexture;
|
|
3211
|
-
// ── Parallax program ──────────────────────────────────────────────────────
|
|
3212
|
-
parallaxProgram;
|
|
3213
|
-
parallaxVAO;
|
|
3214
|
-
parallaxTextures = /* @__PURE__ */ new Map();
|
|
3215
|
-
parallaxImageCache = /* @__PURE__ */ new Map();
|
|
3216
|
-
// Cached uniform locations — parallax program
|
|
3217
|
-
pUTexture;
|
|
3218
|
-
pUUvOffset;
|
|
3219
|
-
pUTexSize;
|
|
3220
|
-
pUCanvasSize;
|
|
3221
|
-
// ── Text texture cache ────────────────────────────────────────────────────
|
|
3222
|
-
textureCache = /* @__PURE__ */ new Map();
|
|
3223
|
-
/** Insertion-order key list for LRU-style eviction. */
|
|
3224
|
-
textureCacheKeys = [];
|
|
3225
|
-
// ── Texture management (sprite textures — CLAMP_TO_EDGE) ──────────────────
|
|
3226
|
-
loadTexture(src) {
|
|
3227
|
-
const cached = this.textures.get(src);
|
|
3228
|
-
if (cached) return cached;
|
|
3229
|
-
let img = this.imageCache.get(src);
|
|
3230
|
-
if (!img) {
|
|
3231
|
-
img = new Image();
|
|
3232
|
-
img.src = src;
|
|
3233
|
-
img.onload = () => {
|
|
3234
|
-
const tex = this.gl.createTexture();
|
|
3235
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, tex);
|
|
3236
|
-
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, img);
|
|
3237
|
-
this.gl.generateMipmap(this.gl.TEXTURE_2D);
|
|
3238
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR_MIPMAP_LINEAR);
|
|
3239
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
3240
|
-
this.textures.set(src, tex);
|
|
3241
|
-
};
|
|
3242
|
-
this.imageCache.set(src, img);
|
|
3243
|
-
}
|
|
3244
|
-
return this.whiteTexture;
|
|
3245
|
-
}
|
|
3246
|
-
// ── Parallax texture management (REPEAT wrap mode) ────────────────────────
|
|
3247
|
-
loadParallaxTexture(src) {
|
|
3248
|
-
const cached = this.parallaxTextures.get(src);
|
|
3249
|
-
if (cached) return cached;
|
|
3250
|
-
let img = this.parallaxImageCache.get(src);
|
|
3251
|
-
if (!img) {
|
|
3252
|
-
img = new Image();
|
|
3253
|
-
img.src = src;
|
|
3254
|
-
img.onload = () => {
|
|
3255
|
-
const gl = this.gl;
|
|
3256
|
-
const tex = gl.createTexture();
|
|
3257
|
-
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
3258
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
|
|
3259
|
-
gl.generateMipmap(gl.TEXTURE_2D);
|
|
3260
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
3261
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
3262
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
|
3263
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
|
3264
|
-
this.parallaxTextures.set(src, tex);
|
|
3265
|
-
};
|
|
3266
|
-
this.parallaxImageCache.set(src, img);
|
|
3267
|
-
}
|
|
3268
|
-
return null;
|
|
3269
|
-
}
|
|
3270
|
-
// ── Text texture management ───────────────────────────────────────────────
|
|
3271
|
-
getTextTextureKey(text) {
|
|
3272
|
-
return `${text.text}|${text.fontSize ?? 16}|${text.fontFamily ?? "monospace"}|${text.color ?? "#ffffff"}`;
|
|
3273
|
-
}
|
|
3274
|
-
getOrCreateTextTexture(text) {
|
|
3275
|
-
const key = this.getTextTextureKey(text);
|
|
3276
|
-
const cached = this.textureCache.get(key);
|
|
3277
|
-
if (cached) return cached;
|
|
3278
|
-
if (this.textureCache.size >= MAX_TEXT_CACHE) {
|
|
3279
|
-
const oldest = this.textureCacheKeys.shift();
|
|
3280
|
-
if (oldest) {
|
|
3281
|
-
const old = this.textureCache.get(oldest);
|
|
3282
|
-
if (old) this.gl.deleteTexture(old.tex);
|
|
3283
|
-
this.textureCache.delete(oldest);
|
|
3284
|
-
}
|
|
3285
|
-
}
|
|
3286
|
-
const offscreen = document.createElement("canvas");
|
|
3287
|
-
const ctx2d = offscreen.getContext("2d");
|
|
3288
|
-
const font = `${text.fontSize ?? 16}px ${text.fontFamily ?? "monospace"}`;
|
|
3289
|
-
ctx2d.font = font;
|
|
3290
|
-
const metrics = ctx2d.measureText(text.text);
|
|
3291
|
-
const textW = Math.ceil(metrics.width) + 4;
|
|
3292
|
-
const textH = Math.ceil((text.fontSize ?? 16) * 1.5) + 4;
|
|
3293
|
-
offscreen.width = textW;
|
|
3294
|
-
offscreen.height = textH;
|
|
3295
|
-
ctx2d.font = font;
|
|
3296
|
-
ctx2d.fillStyle = text.color ?? "#ffffff";
|
|
3297
|
-
ctx2d.textAlign = "left";
|
|
3298
|
-
ctx2d.textBaseline = "top";
|
|
3299
|
-
ctx2d.fillText(text.text, 2, 2, text.maxWidth);
|
|
3300
|
-
const gl = this.gl;
|
|
3301
|
-
const tex = gl.createTexture();
|
|
3302
|
-
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
3303
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, offscreen);
|
|
3304
|
-
gl.generateMipmap(gl.TEXTURE_2D);
|
|
3305
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
3306
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
3307
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
3308
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
3309
|
-
const entry = { tex, w: textW, h: textH };
|
|
3310
|
-
this.textureCache.set(key, entry);
|
|
3311
|
-
this.textureCacheKeys.push(key);
|
|
3312
|
-
return entry;
|
|
3313
|
-
}
|
|
3314
|
-
// ── Instanced draw call ────────────────────────────────────────────────────
|
|
3315
|
-
flush(count, textureKey) {
|
|
3316
|
-
if (count === 0) return;
|
|
3317
|
-
const { gl } = this;
|
|
3318
|
-
const isColor = textureKey.startsWith("__color__");
|
|
3319
|
-
const tex = isColor ? this.whiteTexture : this.loadTexture(textureKey);
|
|
3320
|
-
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
3321
|
-
gl.uniform1i(this.uUseTexture, isColor ? 0 : 1);
|
|
3322
|
-
gl.bindVertexArray(this.quadVAO);
|
|
3323
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
|
|
3324
|
-
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.instanceData, 0, count * FLOATS_PER_INSTANCE);
|
|
3325
|
-
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count);
|
|
3326
|
-
}
|
|
3327
|
-
flushWithTex(count, tex, useTexture) {
|
|
3328
|
-
if (count === 0) return;
|
|
3329
|
-
const { gl } = this;
|
|
3330
|
-
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
3331
|
-
gl.uniform1i(this.uUseTexture, useTexture ? 1 : 0);
|
|
3332
|
-
gl.bindVertexArray(this.quadVAO);
|
|
3333
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
|
|
3334
|
-
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.instanceData, 0, count * FLOATS_PER_INSTANCE);
|
|
3335
|
-
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count);
|
|
3336
|
-
}
|
|
3337
|
-
// ── Write one sprite instance into instanceData ───────────────────────────
|
|
3338
|
-
writeInstance(base, x, y, w, h, rot, anchorX, anchorY, offsetX, offsetY, flipX, r, g, b, a, u, v, uw, vh) {
|
|
3339
|
-
const d = this.instanceData;
|
|
3340
|
-
d[base + 0] = x;
|
|
3341
|
-
d[base + 1] = y;
|
|
3342
|
-
d[base + 2] = w;
|
|
3343
|
-
d[base + 3] = h;
|
|
3344
|
-
d[base + 4] = rot;
|
|
3345
|
-
d[base + 5] = anchorX;
|
|
3346
|
-
d[base + 6] = anchorY;
|
|
3347
|
-
d[base + 7] = offsetX;
|
|
3348
|
-
d[base + 8] = offsetY;
|
|
3349
|
-
d[base + 9] = flipX ? 1 : 0;
|
|
3350
|
-
d[base + 10] = r;
|
|
3351
|
-
d[base + 11] = g;
|
|
3352
|
-
d[base + 12] = b;
|
|
3353
|
-
d[base + 13] = a;
|
|
3354
|
-
d[base + 14] = u;
|
|
3355
|
-
d[base + 15] = v;
|
|
3356
|
-
d[base + 16] = uw;
|
|
3357
|
-
d[base + 17] = vh;
|
|
3358
|
-
}
|
|
3359
|
-
// ── Main update loop ───────────────────────────────────────────────────────
|
|
3360
|
-
update(world, dt) {
|
|
3361
|
-
const { gl, canvas } = this;
|
|
3362
|
-
const W = canvas.width;
|
|
3363
|
-
const H = canvas.height;
|
|
3364
|
-
let camX = 0, camY = 0, zoom = 1;
|
|
3365
|
-
let background = "#000000";
|
|
3366
|
-
let shakeX = 0, shakeY = 0;
|
|
3367
|
-
const camId = world.queryOne("Camera2D");
|
|
3368
|
-
if (camId !== void 0) {
|
|
3369
|
-
const cam = world.getComponent(camId, "Camera2D");
|
|
3370
|
-
background = cam.background;
|
|
3371
|
-
if (cam.followEntityId) {
|
|
3372
|
-
const targetId = this.entityIds.get(cam.followEntityId);
|
|
3373
|
-
if (targetId !== void 0) {
|
|
3374
|
-
const t = world.getComponent(targetId, "Transform");
|
|
3375
|
-
if (t) {
|
|
3376
|
-
if (cam.deadZone) {
|
|
3377
|
-
const halfW = cam.deadZone.w / 2;
|
|
3378
|
-
const halfH = cam.deadZone.h / 2;
|
|
3379
|
-
const dx = t.x - cam.x, dy = t.y - cam.y;
|
|
3380
|
-
if (dx > halfW) cam.x = t.x - halfW;
|
|
3381
|
-
else if (dx < -halfW) cam.x = t.x + halfW;
|
|
3382
|
-
if (dy > halfH) cam.y = t.y - halfH;
|
|
3383
|
-
else if (dy < -halfH) cam.y = t.y + halfH;
|
|
3384
|
-
} else if (cam.smoothing > 0) {
|
|
3385
|
-
cam.x += (t.x - cam.x) * (1 - cam.smoothing);
|
|
3386
|
-
cam.y += (t.y - cam.y) * (1 - cam.smoothing);
|
|
3387
|
-
} else {
|
|
3388
|
-
cam.x = t.x;
|
|
3389
|
-
cam.y = t.y;
|
|
3390
|
-
}
|
|
3391
|
-
}
|
|
3392
|
-
}
|
|
3393
|
-
}
|
|
3394
|
-
if (cam.bounds) {
|
|
3395
|
-
const halfW = W / (2 * cam.zoom);
|
|
3396
|
-
const halfH = H / (2 * cam.zoom);
|
|
3397
|
-
cam.x = Math.max(cam.bounds.x + halfW, Math.min(cam.bounds.x + cam.bounds.width - halfW, cam.x));
|
|
3398
|
-
cam.y = Math.max(cam.bounds.y + halfH, Math.min(cam.bounds.y + cam.bounds.height - halfH, cam.y));
|
|
3399
|
-
}
|
|
3400
|
-
if (cam.shakeTimer > 0) {
|
|
3401
|
-
cam.shakeTimer -= dt;
|
|
3402
|
-
if (cam.shakeTimer < 0) cam.shakeTimer = 0;
|
|
3403
|
-
const progress = cam.shakeDuration > 0 ? cam.shakeTimer / cam.shakeDuration : 0;
|
|
3404
|
-
shakeX = (world.rng() * 2 - 1) * cam.shakeIntensity * progress;
|
|
3405
|
-
shakeY = (world.rng() * 2 - 1) * cam.shakeIntensity * progress;
|
|
3406
|
-
}
|
|
3407
|
-
camX = cam.x;
|
|
3408
|
-
camY = cam.y;
|
|
3409
|
-
zoom = cam.zoom;
|
|
3410
|
-
}
|
|
3411
|
-
for (const id of world.query("AnimationState", "Sprite")) {
|
|
3412
|
-
const anim = world.getComponent(id, "AnimationState");
|
|
3413
|
-
const sprite = world.getComponent(id, "Sprite");
|
|
3414
|
-
if (!anim.playing || anim.frames.length === 0) continue;
|
|
3415
|
-
anim.timer += dt;
|
|
3416
|
-
const frameDuration = 1 / anim.fps;
|
|
3417
|
-
while (anim.timer >= frameDuration) {
|
|
3418
|
-
anim.timer -= frameDuration;
|
|
3419
|
-
anim.currentIndex++;
|
|
3420
|
-
if (anim.currentIndex >= anim.frames.length) {
|
|
3421
|
-
anim.currentIndex = anim.loop ? 0 : anim.frames.length - 1;
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
sprite.frameIndex = anim.frames[anim.currentIndex];
|
|
3425
|
-
}
|
|
3426
|
-
for (const id of world.query("SquashStretch", "RigidBody")) {
|
|
3427
|
-
const ss = world.getComponent(id, "SquashStretch");
|
|
3428
|
-
const rb = world.getComponent(id, "RigidBody");
|
|
3429
|
-
const spd = Math.sqrt(rb.vx * rb.vx + rb.vy * rb.vy);
|
|
3430
|
-
const tScX = rb.vy < -100 ? 1 + ss.intensity * 0.4 : spd > 50 ? 1 - ss.intensity * 0.3 : 1;
|
|
3431
|
-
const tScY = rb.vy < -100 ? 1 - ss.intensity * 0.4 : spd > 50 ? 1 + ss.intensity * 0.3 : 1;
|
|
3432
|
-
ss.currentScaleX += (tScX - ss.currentScaleX) * ss.recovery * dt;
|
|
3433
|
-
ss.currentScaleY += (tScY - ss.currentScaleY) * ss.recovery * dt;
|
|
3434
|
-
}
|
|
3435
|
-
const [br, bg, bb] = parseCSSColor(background);
|
|
3436
|
-
gl.viewport(0, 0, W, H);
|
|
3437
|
-
gl.clearColor(br, bg, bb, 1);
|
|
3438
|
-
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
3439
|
-
const parallaxEntities = world.query("ParallaxLayer");
|
|
3440
|
-
if (parallaxEntities.length > 0) {
|
|
3441
|
-
parallaxEntities.sort((a, b) => {
|
|
3442
|
-
const za = world.getComponent(a, "ParallaxLayer").zIndex;
|
|
3443
|
-
const zb = world.getComponent(b, "ParallaxLayer").zIndex;
|
|
3444
|
-
return za - zb;
|
|
3445
|
-
});
|
|
3446
|
-
gl.useProgram(this.parallaxProgram);
|
|
3447
|
-
gl.uniform2f(this.pUCanvasSize, W, H);
|
|
3448
|
-
gl.uniform1i(this.pUTexture, 0);
|
|
3449
|
-
gl.activeTexture(gl.TEXTURE0);
|
|
3450
|
-
gl.bindVertexArray(this.parallaxVAO);
|
|
3451
|
-
for (const id of parallaxEntities) {
|
|
3452
|
-
const layer = world.getComponent(id, "ParallaxLayer");
|
|
3453
|
-
let img = this.parallaxImageCache.get(layer.src);
|
|
3454
|
-
if (!img) {
|
|
3455
|
-
this.loadParallaxTexture(layer.src);
|
|
3456
|
-
continue;
|
|
3457
|
-
}
|
|
3458
|
-
if (!img.complete || img.naturalWidth === 0) continue;
|
|
3459
|
-
if (layer.imageWidth === 0) layer.imageWidth = img.naturalWidth;
|
|
3460
|
-
if (layer.imageHeight === 0) layer.imageHeight = img.naturalHeight;
|
|
3461
|
-
const tex = this.parallaxTextures.get(layer.src);
|
|
3462
|
-
if (!tex) continue;
|
|
3463
|
-
const imgW = layer.imageWidth;
|
|
3464
|
-
const imgH = layer.imageHeight;
|
|
3465
|
-
const drawX = layer.offsetX - camX * layer.speedX;
|
|
3466
|
-
const drawY = layer.offsetY - camY * layer.speedY;
|
|
3467
|
-
const uvOffsetX = (drawX / imgW % 1 + 1) % 1;
|
|
3468
|
-
const uvOffsetY = (drawY / imgH % 1 + 1) % 1;
|
|
3469
|
-
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
3470
|
-
gl.uniform2f(this.pUUvOffset, uvOffsetX, uvOffsetY);
|
|
3471
|
-
gl.uniform2f(this.pUTexSize, imgW, imgH);
|
|
3472
|
-
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
3473
|
-
}
|
|
3474
|
-
}
|
|
3475
|
-
gl.useProgram(this.program);
|
|
3476
|
-
gl.uniform2f(this.uCamPos, camX, camY);
|
|
3477
|
-
gl.uniform1f(this.uZoom, zoom);
|
|
3478
|
-
gl.uniform2f(this.uCanvasSize, W, H);
|
|
3479
|
-
gl.uniform2f(this.uShake, shakeX, shakeY);
|
|
3480
|
-
gl.uniform1i(this.uTexture, 0);
|
|
3481
|
-
gl.activeTexture(gl.TEXTURE0);
|
|
3482
|
-
const renderables = world.query("Transform", "Sprite");
|
|
3483
|
-
renderables.sort((a, b) => {
|
|
3484
|
-
const sa = world.getComponent(a, "Sprite");
|
|
3485
|
-
const sb = world.getComponent(b, "Sprite");
|
|
3486
|
-
const zd = sa.zIndex - sb.zIndex;
|
|
3487
|
-
if (zd !== 0) return zd;
|
|
3488
|
-
const ka = getTextureKey(sa), kb = getTextureKey(sb);
|
|
3489
|
-
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
3490
|
-
});
|
|
3491
|
-
let batchCount = 0;
|
|
3492
|
-
let batchKey = "";
|
|
3493
|
-
for (let i = 0; i <= renderables.length; i++) {
|
|
3494
|
-
if (i === renderables.length) {
|
|
3495
|
-
this.flush(batchCount, batchKey);
|
|
3496
|
-
break;
|
|
3497
|
-
}
|
|
3498
|
-
const id = renderables[i];
|
|
3499
|
-
const transform = world.getComponent(id, "Transform");
|
|
3500
|
-
const sprite = world.getComponent(id, "Sprite");
|
|
3501
|
-
if (!sprite.visible) continue;
|
|
3502
|
-
if (sprite.src && !sprite.image) {
|
|
3503
|
-
let img = this.imageCache.get(sprite.src);
|
|
3504
|
-
if (!img) {
|
|
3505
|
-
img = new Image();
|
|
3506
|
-
img.src = sprite.src;
|
|
3507
|
-
this.imageCache.set(sprite.src, img);
|
|
3508
|
-
img.onload = () => {
|
|
3509
|
-
const tex = this.gl.createTexture();
|
|
3510
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, tex);
|
|
3511
|
-
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, img);
|
|
3512
|
-
this.gl.generateMipmap(this.gl.TEXTURE_2D);
|
|
3513
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR_MIPMAP_LINEAR);
|
|
3514
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
3515
|
-
this.textures.set(sprite.src, tex);
|
|
3516
|
-
};
|
|
3517
|
-
}
|
|
3518
|
-
sprite.image = img;
|
|
3519
|
-
}
|
|
3520
|
-
const key = getTextureKey(sprite);
|
|
3521
|
-
if (key !== batchKey && batchCount > 0 || batchCount >= MAX_INSTANCES) {
|
|
3522
|
-
this.flush(batchCount, batchKey);
|
|
3523
|
-
batchCount = 0;
|
|
3524
|
-
}
|
|
3525
|
-
batchKey = key;
|
|
3526
|
-
const ss = world.getComponent(id, "SquashStretch");
|
|
3527
|
-
const scaleXMod = ss ? ss.currentScaleX : 1;
|
|
3528
|
-
const scaleYMod = ss ? ss.currentScaleY : 1;
|
|
3529
|
-
const [r, g, b, a] = parseCSSColor(sprite.color);
|
|
3530
|
-
const uv = getUVRect(sprite);
|
|
3531
|
-
this.writeInstance(
|
|
3532
|
-
batchCount * FLOATS_PER_INSTANCE,
|
|
3533
|
-
transform.x,
|
|
3534
|
-
transform.y,
|
|
3535
|
-
sprite.width * transform.scaleX * scaleXMod,
|
|
3536
|
-
sprite.height * transform.scaleY * scaleYMod,
|
|
3537
|
-
transform.rotation,
|
|
3538
|
-
sprite.anchorX,
|
|
3539
|
-
sprite.anchorY,
|
|
3540
|
-
sprite.offsetX,
|
|
3541
|
-
sprite.offsetY,
|
|
3542
|
-
sprite.flipX,
|
|
3543
|
-
r,
|
|
3544
|
-
g,
|
|
3545
|
-
b,
|
|
3546
|
-
a,
|
|
3547
|
-
uv[0],
|
|
3548
|
-
uv[1],
|
|
3549
|
-
uv[2],
|
|
3550
|
-
uv[3]
|
|
3551
|
-
);
|
|
3552
|
-
batchCount++;
|
|
3206
|
+
"\xD7",
|
|
3207
|
+
img.naturalHeight
|
|
3208
|
+
] })
|
|
3209
|
+
] }, src);
|
|
3210
|
+
})
|
|
3211
|
+
] });
|
|
3212
|
+
}
|
|
3213
|
+
function formatValue(v) {
|
|
3214
|
+
if (typeof v === "number") return v.toFixed(2);
|
|
3215
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
3216
|
+
if (v === null || v === void 0) return "\u2014";
|
|
3217
|
+
if (typeof v === "object") return JSON.stringify(v).slice(0, 60);
|
|
3218
|
+
return String(v);
|
|
3219
|
+
}
|
|
3220
|
+
function getActiveKeys(engine) {
|
|
3221
|
+
const kb = engine.input.keyboard;
|
|
3222
|
+
return kb.held ? Array.from(kb.held) : [];
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
// ../devtools/src/debugSystem.ts
|
|
3226
|
+
var DebugSystem = class {
|
|
3227
|
+
constructor(renderer) {
|
|
3228
|
+
this.renderer = renderer;
|
|
3229
|
+
}
|
|
3230
|
+
frameCount = 0;
|
|
3231
|
+
lastFpsTime = 0;
|
|
3232
|
+
fps = 0;
|
|
3233
|
+
update(world, dt) {
|
|
3234
|
+
const { ctx, canvas } = this.renderer;
|
|
3235
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
3236
|
+
this.frameCount++;
|
|
3237
|
+
this.lastFpsTime += dt;
|
|
3238
|
+
if (this.lastFpsTime >= 0.5) {
|
|
3239
|
+
this.fps = Math.round(this.frameCount / this.lastFpsTime);
|
|
3240
|
+
this.frameCount = 0;
|
|
3241
|
+
this.lastFpsTime = 0;
|
|
3553
3242
|
}
|
|
3554
|
-
const
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
const
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
const transform = world.getComponent(id, "Transform");
|
|
3562
|
-
const text = world.getComponent(id, "Text");
|
|
3563
|
-
if (!text.visible) continue;
|
|
3564
|
-
const entry = this.getOrCreateTextTexture(text);
|
|
3565
|
-
if (!entry) continue;
|
|
3566
|
-
this.flush(batchCount, batchKey);
|
|
3567
|
-
batchCount = 0;
|
|
3568
|
-
batchKey = "";
|
|
3569
|
-
this.writeInstance(
|
|
3570
|
-
0,
|
|
3571
|
-
transform.x + text.offsetX,
|
|
3572
|
-
transform.y + text.offsetY,
|
|
3573
|
-
entry.w,
|
|
3574
|
-
entry.h,
|
|
3575
|
-
transform.rotation,
|
|
3576
|
-
0,
|
|
3577
|
-
0,
|
|
3578
|
-
// anchor top-left
|
|
3579
|
-
0,
|
|
3580
|
-
0,
|
|
3581
|
-
false,
|
|
3582
|
-
1,
|
|
3583
|
-
1,
|
|
3584
|
-
1,
|
|
3585
|
-
1,
|
|
3586
|
-
// white tint — color baked into texture
|
|
3587
|
-
0,
|
|
3588
|
-
0,
|
|
3589
|
-
1,
|
|
3590
|
-
1
|
|
3591
|
-
);
|
|
3592
|
-
this.flushWithTex(1, entry.tex, true);
|
|
3243
|
+
const camId = world.queryOne("Camera2D");
|
|
3244
|
+
let camX = 0, camY = 0, zoom = 1;
|
|
3245
|
+
if (camId !== void 0) {
|
|
3246
|
+
const cam = world.getComponent(camId, "Camera2D");
|
|
3247
|
+
camX = cam.x;
|
|
3248
|
+
camY = cam.y;
|
|
3249
|
+
zoom = cam.zoom;
|
|
3593
3250
|
}
|
|
3594
|
-
|
|
3251
|
+
ctx.save();
|
|
3252
|
+
ctx.translate(canvas.width / 2 - camX * zoom, canvas.height / 2 - camY * zoom);
|
|
3253
|
+
ctx.scale(zoom, zoom);
|
|
3254
|
+
const lw = 1 / zoom;
|
|
3255
|
+
for (const id of world.query("Transform", "BoxCollider")) {
|
|
3595
3256
|
const t = world.getComponent(id, "Transform");
|
|
3596
|
-
const
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
for (let i = 0; i < spawnCount && pool.particles.length < pool.maxParticles; i++) {
|
|
3609
|
-
const angle = pool.angle + (world.rng() - 0.5) * pool.spread;
|
|
3610
|
-
const speed = pool.speed * (0.5 + world.rng() * 0.5);
|
|
3611
|
-
pool.particles.push({
|
|
3612
|
-
x: t.x,
|
|
3613
|
-
y: t.y,
|
|
3614
|
-
vx: Math.cos(angle) * speed,
|
|
3615
|
-
vy: Math.sin(angle) * speed,
|
|
3616
|
-
life: pool.particleLife,
|
|
3617
|
-
maxLife: pool.particleLife,
|
|
3618
|
-
size: pool.particleSize,
|
|
3619
|
-
color: pool.color,
|
|
3620
|
-
gravity: pool.gravity
|
|
3621
|
-
});
|
|
3622
|
-
}
|
|
3623
|
-
}
|
|
3624
|
-
let pCount = 0;
|
|
3625
|
-
const pKey = `__color__`;
|
|
3626
|
-
for (const p of pool.particles) {
|
|
3627
|
-
if (pCount >= MAX_INSTANCES) {
|
|
3628
|
-
this.flush(pCount, pKey);
|
|
3629
|
-
pCount = 0;
|
|
3630
|
-
}
|
|
3631
|
-
const alpha = p.life / p.maxLife;
|
|
3632
|
-
const [r, g, b] = parseCSSColor(p.color);
|
|
3633
|
-
this.writeInstance(
|
|
3634
|
-
pCount * FLOATS_PER_INSTANCE,
|
|
3635
|
-
p.x,
|
|
3636
|
-
p.y,
|
|
3637
|
-
p.size,
|
|
3638
|
-
p.size,
|
|
3639
|
-
0,
|
|
3640
|
-
0.5,
|
|
3641
|
-
0.5,
|
|
3642
|
-
0,
|
|
3643
|
-
0,
|
|
3644
|
-
false,
|
|
3645
|
-
r,
|
|
3646
|
-
g,
|
|
3647
|
-
b,
|
|
3648
|
-
alpha,
|
|
3649
|
-
0,
|
|
3650
|
-
0,
|
|
3651
|
-
1,
|
|
3652
|
-
1
|
|
3653
|
-
);
|
|
3654
|
-
pCount++;
|
|
3655
|
-
}
|
|
3656
|
-
if (pCount > 0) this.flush(pCount, pKey);
|
|
3257
|
+
const c = world.getComponent(id, "BoxCollider");
|
|
3258
|
+
ctx.strokeStyle = c.isTrigger ? "rgba(255,200,0,0.85)" : "rgba(0,255,120,0.85)";
|
|
3259
|
+
ctx.lineWidth = lw;
|
|
3260
|
+
ctx.strokeRect(
|
|
3261
|
+
t.x + c.offsetX - c.width / 2,
|
|
3262
|
+
t.y + c.offsetY - c.height / 2,
|
|
3263
|
+
c.width,
|
|
3264
|
+
c.height
|
|
3265
|
+
);
|
|
3266
|
+
ctx.fillStyle = "rgba(255,255,255,0.5)";
|
|
3267
|
+
ctx.font = `${10 / zoom}px monospace`;
|
|
3268
|
+
ctx.fillText(String(id), t.x + c.offsetX - c.width / 2 + lw, t.y + c.offsetY - c.height / 2 - lw * 2);
|
|
3657
3269
|
}
|
|
3658
|
-
|
|
3659
|
-
const
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
for (let i = 0; i < trail.points.length; i++) {
|
|
3668
|
-
if (tCount >= MAX_INSTANCES) {
|
|
3669
|
-
this.flush(tCount, "__color__");
|
|
3670
|
-
tCount = 0;
|
|
3671
|
-
}
|
|
3672
|
-
const alpha = 1 - i / trail.points.length;
|
|
3673
|
-
this.writeInstance(
|
|
3674
|
-
tCount * FLOATS_PER_INSTANCE,
|
|
3675
|
-
trail.points[i].x,
|
|
3676
|
-
trail.points[i].y,
|
|
3677
|
-
trailW,
|
|
3678
|
-
trailW,
|
|
3679
|
-
0,
|
|
3680
|
-
0.5,
|
|
3681
|
-
0.5,
|
|
3682
|
-
0,
|
|
3683
|
-
0,
|
|
3684
|
-
false,
|
|
3685
|
-
tr,
|
|
3686
|
-
tg,
|
|
3687
|
-
tb,
|
|
3688
|
-
alpha,
|
|
3689
|
-
0,
|
|
3690
|
-
0,
|
|
3691
|
-
1,
|
|
3692
|
-
1
|
|
3693
|
-
);
|
|
3694
|
-
tCount++;
|
|
3270
|
+
if (camId !== void 0) {
|
|
3271
|
+
const camFull = world.getComponent(camId, "Camera2D");
|
|
3272
|
+
if (camFull.bounds) {
|
|
3273
|
+
const b = camFull.bounds;
|
|
3274
|
+
ctx.strokeStyle = "rgba(0, 255, 255, 0.4)";
|
|
3275
|
+
ctx.lineWidth = 1 / zoom;
|
|
3276
|
+
ctx.setLineDash([8 / zoom, 4 / zoom]);
|
|
3277
|
+
ctx.strokeRect(b.x, b.y, b.width, b.height);
|
|
3278
|
+
ctx.setLineDash([]);
|
|
3695
3279
|
}
|
|
3696
|
-
if (tCount > 0) this.flush(tCount, "__color__");
|
|
3697
3280
|
}
|
|
3281
|
+
ctx.restore();
|
|
3282
|
+
const GRID_SIZE = 128;
|
|
3283
|
+
ctx.save();
|
|
3284
|
+
ctx.strokeStyle = "rgba(255, 255, 255, 0.04)";
|
|
3285
|
+
ctx.lineWidth = 1;
|
|
3286
|
+
ctx.setLineDash([]);
|
|
3287
|
+
const offsetX = camX - canvas.width / (2 * zoom);
|
|
3288
|
+
const offsetY = camY - canvas.height / (2 * zoom);
|
|
3289
|
+
const visibleW = canvas.width / zoom;
|
|
3290
|
+
const visibleH = canvas.height / zoom;
|
|
3291
|
+
const startCol = Math.floor(offsetX / GRID_SIZE);
|
|
3292
|
+
const endCol = Math.ceil((offsetX + visibleW) / GRID_SIZE);
|
|
3293
|
+
const startRow = Math.floor(offsetY / GRID_SIZE);
|
|
3294
|
+
const endRow = Math.ceil((offsetY + visibleH) / GRID_SIZE);
|
|
3295
|
+
ctx.translate(canvas.width / 2 - camX * zoom, canvas.height / 2 - camY * zoom);
|
|
3296
|
+
ctx.scale(zoom, zoom);
|
|
3297
|
+
for (let col = startCol; col <= endCol; col++) {
|
|
3298
|
+
const wx = col * GRID_SIZE;
|
|
3299
|
+
ctx.beginPath();
|
|
3300
|
+
ctx.moveTo(wx, startRow * GRID_SIZE);
|
|
3301
|
+
ctx.lineTo(wx, endRow * GRID_SIZE);
|
|
3302
|
+
ctx.stroke();
|
|
3303
|
+
}
|
|
3304
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
3305
|
+
const wy = row * GRID_SIZE;
|
|
3306
|
+
ctx.beginPath();
|
|
3307
|
+
ctx.moveTo(startCol * GRID_SIZE, wy);
|
|
3308
|
+
ctx.lineTo(endCol * GRID_SIZE, wy);
|
|
3309
|
+
ctx.stroke();
|
|
3310
|
+
}
|
|
3311
|
+
ctx.restore();
|
|
3312
|
+
const entityCount = world.entityCount;
|
|
3313
|
+
const physicsCount = world.query("RigidBody", "BoxCollider").length;
|
|
3314
|
+
const renderCount = world.query("Transform", "Sprite").length;
|
|
3315
|
+
ctx.save();
|
|
3316
|
+
ctx.fillStyle = "rgba(0,0,0,0.65)";
|
|
3317
|
+
ctx.fillRect(8, 8, 184, 84);
|
|
3318
|
+
ctx.fillStyle = "#00ff88";
|
|
3319
|
+
ctx.font = "11px monospace";
|
|
3320
|
+
ctx.fillText(`FPS ${this.fps}`, 16, 26);
|
|
3321
|
+
ctx.fillText(`Entities ${entityCount}`, 16, 42);
|
|
3322
|
+
ctx.fillText(`Physics ${physicsCount}`, 16, 58);
|
|
3323
|
+
ctx.fillText(`Renderables ${renderCount}`, 16, 74);
|
|
3324
|
+
ctx.restore();
|
|
3698
3325
|
}
|
|
3699
3326
|
};
|
|
3700
3327
|
|
|
@@ -3721,7 +3348,6 @@ function Game({
|
|
|
3721
3348
|
asyncAssets = false,
|
|
3722
3349
|
onReady,
|
|
3723
3350
|
plugins,
|
|
3724
|
-
renderer: CustomRenderer,
|
|
3725
3351
|
style,
|
|
3726
3352
|
className,
|
|
3727
3353
|
children
|
|
@@ -3741,35 +3367,14 @@ function Game({
|
|
|
3741
3367
|
const assets = new AssetManager();
|
|
3742
3368
|
const physics = new PhysicsSystem(gravity, events);
|
|
3743
3369
|
const entityIds = /* @__PURE__ */ new Map();
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
let renderSystem;
|
|
3747
|
-
let activeRenderSystem;
|
|
3748
|
-
if (CustomRenderer === "canvas2d") {
|
|
3749
|
-
canvas2d = new Canvas2DRenderer(canvas);
|
|
3750
|
-
builtinRenderSystem = new RenderSystem(canvas2d, entityIds);
|
|
3751
|
-
renderSystem = builtinRenderSystem;
|
|
3752
|
-
} else if (CustomRenderer) {
|
|
3753
|
-
renderSystem = new CustomRenderer(canvas, entityIds);
|
|
3754
|
-
} else {
|
|
3755
|
-
try {
|
|
3756
|
-
renderSystem = new WebGLRenderSystem(canvas, entityIds);
|
|
3757
|
-
} catch (e) {
|
|
3758
|
-
console.warn("[Cubeforge] WebGL2 unavailable, falling back to Canvas2D:", e);
|
|
3759
|
-
canvas2d = new Canvas2DRenderer(canvas);
|
|
3760
|
-
builtinRenderSystem = new RenderSystem(canvas2d, entityIds);
|
|
3761
|
-
renderSystem = builtinRenderSystem;
|
|
3762
|
-
}
|
|
3763
|
-
}
|
|
3764
|
-
activeRenderSystem = renderSystem;
|
|
3370
|
+
const renderSystem = new RenderSystem(canvas, entityIds);
|
|
3371
|
+
const activeRenderSystem = renderSystem;
|
|
3765
3372
|
let debugSystem = null;
|
|
3766
3373
|
if (debug) {
|
|
3767
3374
|
const debugCanvas2dEl = debugCanvasRef.current;
|
|
3768
3375
|
if (debugCanvas2dEl) {
|
|
3769
3376
|
const debugCanvas2d = new Canvas2DRenderer(debugCanvas2dEl);
|
|
3770
3377
|
debugSystem = new DebugSystem(debugCanvas2d);
|
|
3771
|
-
} else if (canvas2d) {
|
|
3772
|
-
debugSystem = new DebugSystem(canvas2d);
|
|
3773
3378
|
}
|
|
3774
3379
|
}
|
|
3775
3380
|
const systemTimings = /* @__PURE__ */ new Map();
|
|
@@ -3795,8 +3400,6 @@ function Game({
|
|
|
3795
3400
|
const state = {
|
|
3796
3401
|
ecs,
|
|
3797
3402
|
input,
|
|
3798
|
-
renderer: canvas2d,
|
|
3799
|
-
renderSystem: builtinRenderSystem,
|
|
3800
3403
|
activeRenderSystem,
|
|
3801
3404
|
physics,
|
|
3802
3405
|
events,
|
|
@@ -6089,7 +5692,6 @@ export {
|
|
|
6089
5692
|
BoxCollider,
|
|
6090
5693
|
Camera2D,
|
|
6091
5694
|
CameraZone,
|
|
6092
|
-
RenderSystem as Canvas2DRenderSystem,
|
|
6093
5695
|
CapsuleCollider,
|
|
6094
5696
|
Checkpoint,
|
|
6095
5697
|
CircleCollider,
|
|
@@ -6099,6 +5701,7 @@ export {
|
|
|
6099
5701
|
MovingPlatform,
|
|
6100
5702
|
ParallaxLayer,
|
|
6101
5703
|
ParticleEmitter,
|
|
5704
|
+
RenderSystem,
|
|
6102
5705
|
RigidBody,
|
|
6103
5706
|
ScreenFlash,
|
|
6104
5707
|
Script,
|
|
@@ -6109,7 +5712,6 @@ export {
|
|
|
6109
5712
|
Trail,
|
|
6110
5713
|
Transform,
|
|
6111
5714
|
VirtualJoystick,
|
|
6112
|
-
WebGLRenderSystem,
|
|
6113
5715
|
World,
|
|
6114
5716
|
arrive,
|
|
6115
5717
|
createAtlas,
|