@vectojs/core 1.23.0 → 1.25.0

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.
@@ -924,10 +924,11 @@ var FLOATS_PER_POINT = 7;
924
924
  var POINT_STRIDE = FLOATS_PER_POINT * 4;
925
925
  var VERTS_PER_QUAD = 4;
926
926
  var INDICES_PER_QUAD = 6;
927
- var FLOATS_PER_RECT_VERT = 6;
928
- var RECT_VERT_STRIDE = FLOATS_PER_RECT_VERT * 4;
929
- var FLOATS_PER_SPRITE_VERT = 8;
930
- var SPRITE_VERT_STRIDE = FLOATS_PER_SPRITE_VERT * 4;
927
+ var PACKED_QUAD_VERT_STRIDE = 16;
928
+ var PACKED_RECT_VERT_STRIDE = 12;
929
+ var PACKED_UV_OFFSET = 8;
930
+ var PACKED_TINT_OFFSET = 12;
931
+ var PACKED_RECT_TINT_OFFSET = 8;
931
932
  function buildQuadIndices(quads) {
932
933
  const out = new Uint32Array(quads * INDICES_PER_QUAD);
933
934
  for (let i = 0; i < quads; i++) {
@@ -942,6 +943,35 @@ function buildQuadIndices(quads) {
942
943
  }
943
944
  return out;
944
945
  }
946
+ function encodeUnorm(v, max) {
947
+ return v <= 0 ? 0 : v >= 1 ? max : v * max + 0.5 | 0;
948
+ }
949
+ var LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
950
+ function packTint(r, g, b, a) {
951
+ const ri = encodeUnorm(r, 255);
952
+ const gi = encodeUnorm(g, 255);
953
+ const bi = encodeUnorm(b, 255);
954
+ const ai = encodeUnorm(a, 255);
955
+ return LITTLE_ENDIAN ? ai << 24 | bi << 16 | gi << 8 | ri : ri << 24 | gi << 16 | bi << 8 | ai;
956
+ }
957
+ function createPackedBuffer(byteLength) {
958
+ const bytes = new ArrayBuffer(byteLength);
959
+ return {
960
+ bytes,
961
+ f32: new Float32Array(bytes),
962
+ u16: new Uint16Array(bytes),
963
+ u32: new Uint32Array(bytes),
964
+ u8: new Uint8Array(bytes)
965
+ };
966
+ }
967
+ function growPacked(buf, neededBytes) {
968
+ if (neededBytes <= buf.bytes.byteLength) return buf;
969
+ let cap = buf.bytes.byteLength;
970
+ while (cap < neededBytes) cap *= 2;
971
+ const grown = createPackedBuffer(cap);
972
+ grown.u8.set(buf.u8);
973
+ return grown;
974
+ }
945
975
  var POINT_VERT = `#version 300 es
946
976
  in vec2 a_pos;
947
977
  in float a_radius;
@@ -1069,7 +1099,7 @@ function link(gl, vsSrc, fsSrc) {
1069
1099
  }
1070
1100
  return program;
1071
1101
  }
1072
- function writeQuad(out, o, x, y, width, height, u0, v0, u1, v1, r, g, b, al, rotation) {
1102
+ function writeQuad(out, byteOffset, x, y, width, height, u0, v0, u1, v1, r, g, b, al, rotation) {
1073
1103
  let x1;
1074
1104
  let y1;
1075
1105
  let x2;
@@ -1097,40 +1127,39 @@ function writeQuad(out, o, x, y, width, height, u0, v0, u1, v1, r, g, b, al, rot
1097
1127
  x3 = x - hs;
1098
1128
  y3 = y + hc;
1099
1129
  }
1100
- out[o] = x;
1101
- out[o + 1] = y;
1102
- out[o + 2] = u0;
1103
- out[o + 3] = v0;
1104
- out[o + 4] = r;
1105
- out[o + 5] = g;
1106
- out[o + 6] = b;
1107
- out[o + 7] = al;
1108
- out[o + 8] = x1;
1109
- out[o + 9] = y1;
1110
- out[o + 10] = u1;
1111
- out[o + 11] = v0;
1112
- out[o + 12] = r;
1113
- out[o + 13] = g;
1114
- out[o + 14] = b;
1115
- out[o + 15] = al;
1116
- out[o + 16] = x2;
1117
- out[o + 17] = y2;
1118
- out[o + 18] = u1;
1119
- out[o + 19] = v1;
1120
- out[o + 20] = r;
1121
- out[o + 21] = g;
1122
- out[o + 22] = b;
1123
- out[o + 23] = al;
1124
- out[o + 24] = x3;
1125
- out[o + 25] = y3;
1126
- out[o + 26] = u0;
1127
- out[o + 27] = v1;
1128
- out[o + 28] = r;
1129
- out[o + 29] = g;
1130
- out[o + 30] = b;
1131
- out[o + 31] = al;
1130
+ const iu0 = encodeUnorm(u0, 65535);
1131
+ const iv0 = encodeUnorm(v0, 65535);
1132
+ const iu1 = encodeUnorm(u1, 65535);
1133
+ const iv1 = encodeUnorm(v1, 65535);
1134
+ const tint = packTint(r, g, b, al);
1135
+ const f32 = out.f32;
1136
+ const u16 = out.u16;
1137
+ const u32 = out.u32;
1138
+ const fi = byteOffset >> 2;
1139
+ const si = byteOffset + PACKED_UV_OFFSET >> 1;
1140
+ const ti = byteOffset + PACKED_TINT_OFFSET >> 2;
1141
+ f32[fi] = x;
1142
+ f32[fi + 1] = y;
1143
+ u16[si] = iu0;
1144
+ u16[si + 1] = iv0;
1145
+ u32[ti] = tint;
1146
+ f32[fi + 4] = x1;
1147
+ f32[fi + 5] = y1;
1148
+ u16[si + 8] = iu1;
1149
+ u16[si + 9] = iv0;
1150
+ u32[ti + 4] = tint;
1151
+ f32[fi + 8] = x2;
1152
+ f32[fi + 9] = y2;
1153
+ u16[si + 16] = iu1;
1154
+ u16[si + 17] = iv1;
1155
+ u32[ti + 8] = tint;
1156
+ f32[fi + 12] = x3;
1157
+ f32[fi + 13] = y3;
1158
+ u16[si + 24] = iu0;
1159
+ u16[si + 25] = iv1;
1160
+ u32[ti + 12] = tint;
1132
1161
  }
1133
- function writeRectQuad(out, o, x, y, width, height, r, g, b, al, rotation) {
1162
+ function writeRectQuad(out, byteOffset, x, y, width, height, r, g, b, al, rotation) {
1134
1163
  let x1;
1135
1164
  let y1;
1136
1165
  let x2;
@@ -1158,30 +1187,22 @@ function writeRectQuad(out, o, x, y, width, height, r, g, b, al, rotation) {
1158
1187
  x3 = x - hs;
1159
1188
  y3 = y + hc;
1160
1189
  }
1161
- out[o] = x;
1162
- out[o + 1] = y;
1163
- out[o + 2] = r;
1164
- out[o + 3] = g;
1165
- out[o + 4] = b;
1166
- out[o + 5] = al;
1167
- out[o + 6] = x1;
1168
- out[o + 7] = y1;
1169
- out[o + 8] = r;
1170
- out[o + 9] = g;
1171
- out[o + 10] = b;
1172
- out[o + 11] = al;
1173
- out[o + 12] = x2;
1174
- out[o + 13] = y2;
1175
- out[o + 14] = r;
1176
- out[o + 15] = g;
1177
- out[o + 16] = b;
1178
- out[o + 17] = al;
1179
- out[o + 18] = x3;
1180
- out[o + 19] = y3;
1181
- out[o + 20] = r;
1182
- out[o + 21] = g;
1183
- out[o + 22] = b;
1184
- out[o + 23] = al;
1190
+ const tint = packTint(r, g, b, al);
1191
+ const f32 = out.f32;
1192
+ const u32 = out.u32;
1193
+ const fi = byteOffset >> 2;
1194
+ f32[fi] = x;
1195
+ f32[fi + 1] = y;
1196
+ u32[fi + 2] = tint;
1197
+ f32[fi + 3] = x1;
1198
+ f32[fi + 4] = y1;
1199
+ u32[fi + 5] = tint;
1200
+ f32[fi + 6] = x2;
1201
+ f32[fi + 7] = y2;
1202
+ u32[fi + 8] = tint;
1203
+ f32[fi + 9] = x3;
1204
+ f32[fi + 10] = y3;
1205
+ u32[fi + 11] = tint;
1185
1206
  }
1186
1207
  function grow(data, needed) {
1187
1208
  if (needed <= data.length) return data;
@@ -1191,6 +1212,16 @@ function grow(data, needed) {
1191
1212
  grown.set(data);
1192
1213
  return grown;
1193
1214
  }
1215
+ function isSourceReady(source) {
1216
+ const candidate = source;
1217
+ if (typeof candidate.complete === "boolean") {
1218
+ if (!candidate.complete) return false;
1219
+ if (typeof candidate.naturalWidth === "number" && candidate.naturalWidth === 0) return false;
1220
+ return true;
1221
+ }
1222
+ if (typeof candidate.readyState === "number") return candidate.readyState >= 2;
1223
+ return true;
1224
+ }
1194
1225
  function createWebGLPointRenderer(canvas) {
1195
1226
  const gl = canvas.getContext("webgl2", {
1196
1227
  premultipliedAlpha: false
@@ -1228,9 +1259,16 @@ function createWebGLPointRenderer(canvas) {
1228
1259
  gl.bindVertexArray(rectVAO);
1229
1260
  gl.bindBuffer(gl.ARRAY_BUFFER, rectBuffer);
1230
1261
  gl.enableVertexAttribArray(rAPos);
1231
- gl.vertexAttribPointer(rAPos, 2, gl.FLOAT, false, RECT_VERT_STRIDE, 0);
1262
+ gl.vertexAttribPointer(rAPos, 2, gl.FLOAT, false, PACKED_RECT_VERT_STRIDE, 0);
1232
1263
  gl.enableVertexAttribArray(rAColor);
1233
- gl.vertexAttribPointer(rAColor, 4, gl.FLOAT, false, RECT_VERT_STRIDE, 8);
1264
+ gl.vertexAttribPointer(
1265
+ rAColor,
1266
+ 4,
1267
+ gl.UNSIGNED_BYTE,
1268
+ true,
1269
+ PACKED_RECT_VERT_STRIDE,
1270
+ PACKED_RECT_TINT_OFFSET
1271
+ );
1234
1272
  const sAPos = gl.getAttribLocation(spriteProgram, "a_pos");
1235
1273
  const sAUv = gl.getAttribLocation(spriteProgram, "a_uv");
1236
1274
  const sATint = gl.getAttribLocation(spriteProgram, "a_tint");
@@ -1241,11 +1279,25 @@ function createWebGLPointRenderer(canvas) {
1241
1279
  gl.bindVertexArray(spriteVAO);
1242
1280
  gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuffer);
1243
1281
  gl.enableVertexAttribArray(sAPos);
1244
- gl.vertexAttribPointer(sAPos, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 0);
1282
+ gl.vertexAttribPointer(sAPos, 2, gl.FLOAT, false, PACKED_QUAD_VERT_STRIDE, 0);
1245
1283
  gl.enableVertexAttribArray(sAUv);
1246
- gl.vertexAttribPointer(sAUv, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 8);
1284
+ gl.vertexAttribPointer(
1285
+ sAUv,
1286
+ 2,
1287
+ gl.UNSIGNED_SHORT,
1288
+ true,
1289
+ PACKED_QUAD_VERT_STRIDE,
1290
+ PACKED_UV_OFFSET
1291
+ );
1247
1292
  gl.enableVertexAttribArray(sATint);
1248
- gl.vertexAttribPointer(sATint, 4, gl.FLOAT, false, SPRITE_VERT_STRIDE, 16);
1293
+ gl.vertexAttribPointer(
1294
+ sATint,
1295
+ 4,
1296
+ gl.UNSIGNED_BYTE,
1297
+ true,
1298
+ PACKED_QUAD_VERT_STRIDE,
1299
+ PACKED_TINT_OFFSET
1300
+ );
1249
1301
  const gAPos = gl.getAttribLocation(msdfProgram, "a_pos");
1250
1302
  const gAUv = gl.getAttribLocation(msdfProgram, "a_uv");
1251
1303
  const gATint = gl.getAttribLocation(msdfProgram, "a_tint");
@@ -1257,11 +1309,25 @@ function createWebGLPointRenderer(canvas) {
1257
1309
  gl.bindVertexArray(glyphVAO);
1258
1310
  gl.bindBuffer(gl.ARRAY_BUFFER, glyphBuffer);
1259
1311
  gl.enableVertexAttribArray(gAPos);
1260
- gl.vertexAttribPointer(gAPos, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 0);
1312
+ gl.vertexAttribPointer(gAPos, 2, gl.FLOAT, false, PACKED_QUAD_VERT_STRIDE, 0);
1261
1313
  gl.enableVertexAttribArray(gAUv);
1262
- gl.vertexAttribPointer(gAUv, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 8);
1314
+ gl.vertexAttribPointer(
1315
+ gAUv,
1316
+ 2,
1317
+ gl.UNSIGNED_SHORT,
1318
+ true,
1319
+ PACKED_QUAD_VERT_STRIDE,
1320
+ PACKED_UV_OFFSET
1321
+ );
1263
1322
  gl.enableVertexAttribArray(gATint);
1264
- gl.vertexAttribPointer(gATint, 4, gl.FLOAT, false, SPRITE_VERT_STRIDE, 16);
1323
+ gl.vertexAttribPointer(
1324
+ gATint,
1325
+ 4,
1326
+ gl.UNSIGNED_BYTE,
1327
+ true,
1328
+ PACKED_QUAD_VERT_STRIDE,
1329
+ PACKED_TINT_OFFSET
1330
+ );
1265
1331
  const cAPos = gl.getAttribLocation(circleQuadProgram, "a_pos");
1266
1332
  const cAUv = gl.getAttribLocation(circleQuadProgram, "a_uv");
1267
1333
  const cATint = gl.getAttribLocation(circleQuadProgram, "a_tint");
@@ -1271,11 +1337,25 @@ function createWebGLPointRenderer(canvas) {
1271
1337
  gl.bindVertexArray(circleQuadVAO);
1272
1338
  gl.bindBuffer(gl.ARRAY_BUFFER, circleQuadBuffer);
1273
1339
  gl.enableVertexAttribArray(cAPos);
1274
- gl.vertexAttribPointer(cAPos, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 0);
1340
+ gl.vertexAttribPointer(cAPos, 2, gl.FLOAT, false, PACKED_QUAD_VERT_STRIDE, 0);
1275
1341
  gl.enableVertexAttribArray(cAUv);
1276
- gl.vertexAttribPointer(cAUv, 2, gl.FLOAT, false, SPRITE_VERT_STRIDE, 8);
1342
+ gl.vertexAttribPointer(
1343
+ cAUv,
1344
+ 2,
1345
+ gl.UNSIGNED_SHORT,
1346
+ true,
1347
+ PACKED_QUAD_VERT_STRIDE,
1348
+ PACKED_UV_OFFSET
1349
+ );
1277
1350
  gl.enableVertexAttribArray(cATint);
1278
- gl.vertexAttribPointer(cATint, 4, gl.FLOAT, false, SPRITE_VERT_STRIDE, 16);
1351
+ gl.vertexAttribPointer(
1352
+ cATint,
1353
+ 4,
1354
+ gl.UNSIGNED_BYTE,
1355
+ true,
1356
+ PACKED_QUAD_VERT_STRIDE,
1357
+ PACKED_TINT_OFFSET
1358
+ );
1279
1359
  gl.bindVertexArray(null);
1280
1360
  const quadIndexBuffer = gl.createBuffer();
1281
1361
  let quadIndexCapacity = 0;
@@ -1285,7 +1365,6 @@ function createWebGLPointRenderer(canvas) {
1285
1365
  while (cap < quads) cap *= 2;
1286
1366
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, quadIndexBuffer);
1287
1367
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, buildQuadIndices(cap), gl.STATIC_DRAW);
1288
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
1289
1368
  quadIndexCapacity = cap;
1290
1369
  };
1291
1370
  for (const vao of [rectVAO, spriteVAO, glyphVAO, circleQuadVAO]) {
@@ -1300,11 +1379,11 @@ function createWebGLPointRenderer(canvas) {
1300
1379
  let distanceRange = 4;
1301
1380
  let pointData = new Float32Array(FLOATS_PER_POINT * 1024);
1302
1381
  let pointCount = 0;
1303
- let rectData = new Float32Array(FLOATS_PER_RECT_VERT * VERTS_PER_QUAD * 256);
1382
+ let rectData = createPackedBuffer(PACKED_RECT_VERT_STRIDE * VERTS_PER_QUAD * 256);
1304
1383
  let rectCount = 0;
1305
- let spriteData = new Float32Array(FLOATS_PER_SPRITE_VERT * VERTS_PER_QUAD * 256);
1384
+ let spriteData = createPackedBuffer(PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD * 256);
1306
1385
  let spriteCount = 0;
1307
- let glyphData = new Float32Array(FLOATS_PER_SPRITE_VERT * VERTS_PER_QUAD * 1024);
1386
+ let glyphData = createPackedBuffer(PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD * 1024);
1308
1387
  let glyphCount = 0;
1309
1388
  let frameDrawCalls = 0;
1310
1389
  let lastFrameDrawCalls = 0;
@@ -1312,7 +1391,7 @@ function createWebGLPointRenderer(canvas) {
1312
1391
  let atlasSwitches = 0;
1313
1392
  let circleQuadFallbacks = 0;
1314
1393
  let circlePoints = 0;
1315
- let circleQuadData = new Float32Array(FLOATS_PER_SPRITE_VERT * VERTS_PER_QUAD * 64);
1394
+ let circleQuadData = createPackedBuffer(PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD * 64);
1316
1395
  let circleQuadCount = 0;
1317
1396
  let logicalW = 0;
1318
1397
  let logicalH = 0;
@@ -1322,11 +1401,11 @@ function createWebGLPointRenderer(canvas) {
1322
1401
  const maxPointSize = pointSizeRange ? pointSizeRange[1] : Infinity;
1323
1402
  const drawGlyphs = () => {
1324
1403
  if (glyphCount === 0 || !msdfTexture) return;
1325
- const floats = glyphCount * VERTS_PER_QUAD * FLOATS_PER_SPRITE_VERT;
1404
+ const bytes = glyphCount * VERTS_PER_QUAD * PACKED_QUAD_VERT_STRIDE;
1326
1405
  gl.useProgram(msdfProgram);
1327
1406
  gl.bindVertexArray(glyphVAO);
1328
1407
  gl.bindBuffer(gl.ARRAY_BUFFER, glyphBuffer);
1329
- gl.bufferData(gl.ARRAY_BUFFER, glyphData.subarray(0, floats), gl.DYNAMIC_DRAW);
1408
+ gl.bufferData(gl.ARRAY_BUFFER, glyphData.u8.subarray(0, bytes), gl.DYNAMIC_DRAW);
1330
1409
  gl.activeTexture(gl.TEXTURE0);
1331
1410
  gl.bindTexture(gl.TEXTURE_2D, msdfTexture);
1332
1411
  gl.uniform1i(gUTex, 0);
@@ -1378,6 +1457,7 @@ function createWebGLPointRenderer(canvas) {
1378
1457
  },
1379
1458
  setTexture(source) {
1380
1459
  if (source === textureSource && texture) return;
1460
+ if (!isSourceReady(source)) return;
1381
1461
  if (!texture) {
1382
1462
  texture = gl.createTexture();
1383
1463
  gl.bindTexture(gl.TEXTURE_2D, texture);
@@ -1393,8 +1473,8 @@ function createWebGLPointRenderer(canvas) {
1393
1473
  },
1394
1474
  addSprite(x, y, width, height, u0, v0, u1, v1, color = "#ffffff", alpha = 1, rotation = 0) {
1395
1475
  if (!texture) return;
1396
- const stride = FLOATS_PER_SPRITE_VERT * VERTS_PER_QUAD;
1397
- spriteData = grow(spriteData, (spriteCount + 1) * stride);
1476
+ const stride = PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD;
1477
+ spriteData = growPacked(spriteData, (spriteCount + 1) * stride);
1398
1478
  const rgba = parseColorToRGBA(color);
1399
1479
  writeQuad(
1400
1480
  spriteData,
@@ -1420,6 +1500,10 @@ function createWebGLPointRenderer(canvas) {
1420
1500
  distanceRange = range;
1421
1501
  return;
1422
1502
  }
1503
+ if (!isSourceReady(source)) {
1504
+ distanceRange = range;
1505
+ return;
1506
+ }
1423
1507
  atlasSwitches++;
1424
1508
  drawGlyphs();
1425
1509
  distanceRange = range;
@@ -1438,8 +1522,8 @@ function createWebGLPointRenderer(canvas) {
1438
1522
  },
1439
1523
  addGlyph(x, y, width, height, u0, v0, u1, v1, color = "#ffffff", alpha = 1, rotation = 0) {
1440
1524
  if (!msdfTexture) return;
1441
- const stride = FLOATS_PER_SPRITE_VERT * VERTS_PER_QUAD;
1442
- glyphData = grow(glyphData, (glyphCount + 1) * stride);
1525
+ const stride = PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD;
1526
+ glyphData = growPacked(glyphData, (glyphCount + 1) * stride);
1443
1527
  const rgba = parseColorToRGBA(color);
1444
1528
  writeQuad(
1445
1529
  glyphData,
@@ -1464,8 +1548,8 @@ function createWebGLPointRenderer(canvas) {
1464
1548
  const needsQuad = logicalW > 0 && (x < radius || y < radius || x > logicalW - radius || y > logicalH - radius) || radius * 2 * dpr > maxPointSize;
1465
1549
  if (needsQuad) {
1466
1550
  circleQuadFallbacks++;
1467
- const stride = FLOATS_PER_SPRITE_VERT * VERTS_PER_QUAD;
1468
- circleQuadData = grow(circleQuadData, (circleQuadCount + 1) * stride);
1551
+ const stride = PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD;
1552
+ circleQuadData = growPacked(circleQuadData, (circleQuadCount + 1) * stride);
1469
1553
  const rgba = parseColorToRGBA(color);
1470
1554
  const d = radius * 2;
1471
1555
  writeQuad(
@@ -1502,8 +1586,8 @@ function createWebGLPointRenderer(canvas) {
1502
1586
  pointCount++;
1503
1587
  },
1504
1588
  addRect(x, y, width, height, color, alpha = 1, rotation = 0) {
1505
- const stride = FLOATS_PER_RECT_VERT * VERTS_PER_QUAD;
1506
- rectData = grow(rectData, (rectCount + 1) * stride);
1589
+ const stride = PACKED_RECT_VERT_STRIDE * VERTS_PER_QUAD;
1590
+ rectData = growPacked(rectData, (rectCount + 1) * stride);
1507
1591
  const rgba = parseColorToRGBA(color);
1508
1592
  writeRectQuad(
1509
1593
  rectData,
@@ -1522,11 +1606,11 @@ function createWebGLPointRenderer(canvas) {
1522
1606
  },
1523
1607
  flush() {
1524
1608
  if (rectCount > 0) {
1525
- const floats = rectCount * VERTS_PER_QUAD * FLOATS_PER_RECT_VERT;
1609
+ const bytes = rectCount * VERTS_PER_QUAD * PACKED_RECT_VERT_STRIDE;
1526
1610
  gl.useProgram(rectProgram);
1527
1611
  gl.bindVertexArray(rectVAO);
1528
1612
  gl.bindBuffer(gl.ARRAY_BUFFER, rectBuffer);
1529
- gl.bufferData(gl.ARRAY_BUFFER, rectData.subarray(0, floats), gl.DYNAMIC_DRAW);
1613
+ gl.bufferData(gl.ARRAY_BUFFER, rectData.u8.subarray(0, bytes), gl.DYNAMIC_DRAW);
1530
1614
  gl.uniform2f(rURes, logicalW, logicalH);
1531
1615
  ensureQuadIndices(rectCount);
1532
1616
  gl.drawElements(gl.TRIANGLES, rectCount * INDICES_PER_QUAD, gl.UNSIGNED_INT, 0);
@@ -1547,22 +1631,22 @@ function createWebGLPointRenderer(canvas) {
1547
1631
  frameDrawCalls++;
1548
1632
  }
1549
1633
  if (circleQuadCount > 0) {
1550
- const floats = circleQuadCount * VERTS_PER_QUAD * FLOATS_PER_SPRITE_VERT;
1634
+ const bytes = circleQuadCount * VERTS_PER_QUAD * PACKED_QUAD_VERT_STRIDE;
1551
1635
  gl.useProgram(circleQuadProgram);
1552
1636
  gl.bindVertexArray(circleQuadVAO);
1553
1637
  gl.bindBuffer(gl.ARRAY_BUFFER, circleQuadBuffer);
1554
- gl.bufferData(gl.ARRAY_BUFFER, circleQuadData.subarray(0, floats), gl.DYNAMIC_DRAW);
1638
+ gl.bufferData(gl.ARRAY_BUFFER, circleQuadData.u8.subarray(0, bytes), gl.DYNAMIC_DRAW);
1555
1639
  gl.uniform2f(cURes, logicalW, logicalH);
1556
1640
  ensureQuadIndices(circleQuadCount);
1557
1641
  gl.drawElements(gl.TRIANGLES, circleQuadCount * INDICES_PER_QUAD, gl.UNSIGNED_INT, 0);
1558
1642
  frameDrawCalls++;
1559
1643
  }
1560
1644
  if (spriteCount > 0 && texture) {
1561
- const floats = spriteCount * VERTS_PER_QUAD * FLOATS_PER_SPRITE_VERT;
1645
+ const bytes = spriteCount * VERTS_PER_QUAD * PACKED_QUAD_VERT_STRIDE;
1562
1646
  gl.useProgram(spriteProgram);
1563
1647
  gl.bindVertexArray(spriteVAO);
1564
1648
  gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuffer);
1565
- gl.bufferData(gl.ARRAY_BUFFER, spriteData.subarray(0, floats), gl.DYNAMIC_DRAW);
1649
+ gl.bufferData(gl.ARRAY_BUFFER, spriteData.u8.subarray(0, bytes), gl.DYNAMIC_DRAW);
1566
1650
  gl.activeTexture(gl.TEXTURE0);
1567
1651
  gl.bindTexture(gl.TEXTURE_2D, texture);
1568
1652
  gl.uniform1i(sUTex, 0);
@@ -1093,6 +1093,11 @@ var MSDFTextEntity = class extends Entity {
1093
1093
  layoutText = "";
1094
1094
  text = "";
1095
1095
  lastRenderedSeqId = 0;
1096
+ // Atlas-decode subscription (see watchAtlasDecode). Held so `destroy()` can
1097
+ // release it: the handler closes over `this`, so leaving it attached to a
1098
+ // long-lived shared atlas image would retain the whole entity.
1099
+ atlasDecodeTarget = null;
1100
+ atlasDecodeHandler = null;
1096
1101
  rgbColorCache = /* @__PURE__ */ new Map();
1097
1102
  fontStringCache = [];
1098
1103
  layoutResult = null;
@@ -1108,8 +1113,53 @@ var MSDFTextEntity = class extends Entity {
1108
1113
  this.maxWidth = options.maxWidth ?? 1e3;
1109
1114
  this.maxHeight = options.maxHeight ?? 1e3;
1110
1115
  this.textAlign = options.textAlign ?? "left";
1116
+ this.watchAtlasDecode();
1111
1117
  this.setText(text);
1112
1118
  }
1119
+ /**
1120
+ * Repaint once the atlas raster decodes.
1121
+ *
1122
+ * The WebGL backend refuses to upload a not-yet-decoded atlas (it would pin an
1123
+ * empty texture in its identity cache forever), so the upload has to happen on
1124
+ * a LATER frame — and nothing else schedules one. Layout marks the scene dirty
1125
+ * when the worker replies, which for a network-served atlas is long before the
1126
+ * image lands, so the scene is already idle by then.
1127
+ *
1128
+ * Measured on Chromium and Firefox (2026-07-31) with a 600 ms atlas: the
1129
+ * scene's own rAF loop never uploaded a decoded atlas in EITHER render mode.
1130
+ * `onDemand` skips idle frames outright; `always` throttles to 2 FPS when
1131
+ * idle, so whether it recovers is down to whether a throttled tick happens to
1132
+ * land after the decode — Chromium got one, Firefox did not. Neither is a
1133
+ * mechanism, which is why this listener exists rather than relying on the
1134
+ * frame loop to come back around.
1135
+ *
1136
+ * Only `HTMLImageElement`-shaped sources have a decode to wait for; a canvas,
1137
+ * `ImageBitmap`, or `VideoFrame` atlas is ready on arrival.
1138
+ */
1139
+ watchAtlasDecode() {
1140
+ const source = this.texture;
1141
+ if (typeof source.complete !== "boolean" || typeof source.addEventListener !== "function" || typeof source.removeEventListener !== "function") {
1142
+ return;
1143
+ }
1144
+ if (source.complete && typeof source.naturalWidth === "number" && source.naturalWidth > 0) {
1145
+ return;
1146
+ }
1147
+ const target = this.texture;
1148
+ this.atlasDecodeHandler = () => {
1149
+ this.detachAtlasDecodeListener();
1150
+ this.scene?.markDirty();
1151
+ };
1152
+ target.addEventListener("load", this.atlasDecodeHandler);
1153
+ target.addEventListener("error", this.atlasDecodeHandler);
1154
+ this.atlasDecodeTarget = target;
1155
+ }
1156
+ detachAtlasDecodeListener() {
1157
+ if (!this.atlasDecodeTarget || !this.atlasDecodeHandler) return;
1158
+ this.atlasDecodeTarget.removeEventListener("load", this.atlasDecodeHandler);
1159
+ this.atlasDecodeTarget.removeEventListener("error", this.atlasDecodeHandler);
1160
+ this.atlasDecodeTarget = null;
1161
+ this.atlasDecodeHandler = null;
1162
+ }
1113
1163
  /** Change the wrap boundary and re-run layout for the current text. */
1114
1164
  setMaxWidth(maxWidth) {
1115
1165
  if (this.maxWidth === maxWidth) return;
@@ -1281,11 +1331,13 @@ var MSDFTextEntity = class extends Entity {
1281
1331
  }
1282
1332
  destroy() {
1283
1333
  LayoutWorkerManager.cancelLayoutForEntity(this.id);
1334
+ this.detachAtlasDecodeListener();
1284
1335
  super.destroy();
1285
1336
  }
1286
1337
  };
1287
1338
 
1288
1339
  // src/text/SVGEntity.ts
1340
+ var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
1289
1341
  function isSvgWhitespace(ch) {
1290
1342
  return ch === " " || ch === " " || ch === "\n" || ch === "\r";
1291
1343
  }
@@ -1317,12 +1369,21 @@ function readSvgAttribute(source, name) {
1317
1369
  return null;
1318
1370
  }
1319
1371
  var SVGEntity = class extends Entity {
1372
+ /**
1373
+ * Stroke colour of the fallback marker drawn when the source cannot be
1374
+ * rasterized. Set to `'transparent'` to opt out and keep the box empty.
1375
+ * Default `'rgba(248,113,113,0.9)'`.
1376
+ */
1377
+ fallbackStroke = "rgba(248,113,113,0.9)";
1378
+ /** Fill behind the fallback marker. Default `'rgba(248,113,113,0.12)'`. */
1379
+ fallbackFill = "rgba(248,113,113,0.12)";
1320
1380
  svgSource = "";
1321
1381
  imageBitmap = null;
1322
1382
  imageElement = null;
1323
1383
  blobURL = null;
1324
1384
  currentImg = null;
1325
1385
  lodTimeout = null;
1386
+ rasterFailed = false;
1326
1387
  cachedDoc = null;
1327
1388
  baseWidth = 100;
1328
1389
  baseHeight = 100;
@@ -1349,6 +1410,7 @@ var SVGEntity = class extends Entity {
1349
1410
  const parserError = doc.querySelector("parsererror");
1350
1411
  if (parserError) {
1351
1412
  console.error("SVG Parsing error:", parserError.textContent);
1413
+ this.rasterFailed = true;
1352
1414
  } else {
1353
1415
  this.cachedDoc = doc;
1354
1416
  const svgEl = doc.documentElement;
@@ -1368,6 +1430,7 @@ var SVGEntity = class extends Entity {
1368
1430
  }
1369
1431
  } catch (e) {
1370
1432
  console.error("Failed parsing SVG via DOMParser, falling back to attribute scan:", e);
1433
+ this.rasterFailed = true;
1371
1434
  }
1372
1435
  } else {
1373
1436
  const wAttr = readSvgAttribute(this.svgSource, "width");
@@ -1391,6 +1454,7 @@ var SVGEntity = class extends Entity {
1391
1454
  }
1392
1455
  triggerRasterization(scale) {
1393
1456
  if (typeof window === "undefined" || typeof Blob === "undefined") return;
1457
+ this.rasterFailed = false;
1394
1458
  if (this.currentImg) {
1395
1459
  this.currentImg.onload = null;
1396
1460
  this.currentImg.onerror = null;
@@ -1414,6 +1478,7 @@ var SVGEntity = class extends Entity {
1414
1478
  "SVG Parsing validation error in triggerRasterization:",
1415
1479
  parserError.textContent
1416
1480
  );
1481
+ this.rasterFailed = true;
1417
1482
  } else {
1418
1483
  const clonedDoc = doc.cloneNode(true);
1419
1484
  const svgEl = clonedDoc.documentElement;
@@ -1427,10 +1492,14 @@ var SVGEntity = class extends Entity {
1427
1492
  }
1428
1493
  const serializer = new XMLSerializer();
1429
1494
  processedSource = serializer.serializeToString(clonedDoc);
1495
+ if (svgEl.namespaceURI === null && !/\sxmlns\s*=/.test(processedSource)) {
1496
+ processedSource = processedSource.replace(/<svg/i, `<svg xmlns="${SVG_NAMESPACE}"`);
1497
+ }
1430
1498
  }
1431
1499
  }
1432
1500
  } catch (e) {
1433
1501
  console.error("Failed to apply LOD scaling to SVG XML:", e);
1502
+ this.rasterFailed = true;
1434
1503
  }
1435
1504
  const blob = new Blob([processedSource], { type: "image/svg+xml;charset=utf-8" });
1436
1505
  this.blobURL = URL.createObjectURL(blob);
@@ -1459,15 +1528,36 @@ var SVGEntity = class extends Entity {
1459
1528
  }).catch((e) => {
1460
1529
  console.error("Failed to create ImageBitmap from SVG:", e);
1461
1530
  this.currentImg = null;
1531
+ if (!this.imageElement) this.rasterFailed = true;
1532
+ if (this.scene) this.scene.markDirty();
1462
1533
  });
1463
1534
  };
1464
1535
  img.onerror = (e) => {
1465
1536
  if (this.currentImg !== img) return;
1466
1537
  console.error("Failed to load SVG Image element:", e);
1467
1538
  this.currentImg = null;
1539
+ this.rasterFailed = true;
1540
+ if (this.scene) this.scene.markDirty();
1468
1541
  };
1469
1542
  img.src = this.blobURL;
1470
1543
  }
1544
+ /**
1545
+ * Whether the source genuinely rasterized to a bitmap.
1546
+ *
1547
+ * Distinguishes "drew the real artwork" from "drew the fallback marker",
1548
+ * which pixel counts alone cannot tell apart — both are non-blank.
1549
+ */
1550
+ hasRasterBitmap() {
1551
+ return this.imageBitmap !== null;
1552
+ }
1553
+ /**
1554
+ * Whether rasterization failed, so {@link render} draws the fallback marker.
1555
+ *
1556
+ * `false` while a raster is still in flight; only a settled failure sets it.
1557
+ */
1558
+ hasRasterFailed() {
1559
+ return this.rasterFailed;
1560
+ }
1471
1561
  isPointInside(globalX, globalY) {
1472
1562
  const local = this.worldToLocal(globalX, globalY);
1473
1563
  if (!local) return false;
@@ -1492,9 +1582,32 @@ var SVGEntity = class extends Entity {
1492
1582
  }
1493
1583
  if (this.imageBitmap) {
1494
1584
  r.drawImage(this.imageBitmap, 0, 0, this.width, this.height);
1495
- } else if (this.imageElement) {
1585
+ return;
1586
+ }
1587
+ if (this.imageElement) {
1496
1588
  r.drawImage(this.imageElement, 0, 0, this.width, this.height);
1589
+ return;
1497
1590
  }
1591
+ if (this.rasterFailed) this.drawFallback(r);
1592
+ }
1593
+ /** Box outline plus a diagonal cross — the conventional "broken image" mark. */
1594
+ drawFallback(r) {
1595
+ const w = this.width;
1596
+ const h = this.height;
1597
+ if (w <= 0 || h <= 0) return;
1598
+ r.beginPath();
1599
+ r.roundRect(0, 0, w, h, 0);
1600
+ r.fill(this.fallbackFill);
1601
+ r.beginPath();
1602
+ r.roundRect(0, 0, w, h, 0);
1603
+ r.stroke(this.fallbackStroke, 1);
1604
+ const inset = Math.min(w, h) * 0.2;
1605
+ r.beginPath();
1606
+ r.moveTo(inset, inset);
1607
+ r.lineTo(w - inset, h - inset);
1608
+ r.moveTo(w - inset, inset);
1609
+ r.lineTo(inset, h - inset);
1610
+ r.stroke(this.fallbackStroke, 1);
1498
1611
  }
1499
1612
  destroy() {
1500
1613
  if (this.lodTimeout) {