@solidtv/renderer 1.5.0-1 → 1.5.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/src/common/EventEmitter.d.ts +8 -0
- package/dist/src/common/EventEmitter.js +20 -0
- package/dist/src/common/EventEmitter.js.map +1 -1
- package/dist/src/core/Autosizer.js +3 -1
- package/dist/src/core/Autosizer.js.map +1 -1
- package/dist/src/core/CoreNode.d.ts +39 -0
- package/dist/src/core/CoreNode.js +105 -25
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextNode.js +4 -2
- package/dist/src/core/CoreTextNode.js.map +1 -1
- package/dist/src/core/Stage.js +4 -1
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/TextureMemoryManager.d.ts +62 -2
- package/dist/src/core/TextureMemoryManager.js +90 -4
- package/dist/src/core/TextureMemoryManager.js.map +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.d.ts +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.js +10 -3
- package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
- package/dist/src/core/lib/textureCompression.js +68 -4
- package/dist/src/core/lib/textureCompression.js.map +1 -1
- package/dist/src/core/renderers/CoreRenderer.d.ts +1 -0
- package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.js +5 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +8 -0
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +54 -14
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.d.ts +28 -0
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +70 -10
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
- package/dist/src/core/renderers/webgl/internal/RendererUtils.js +9 -2
- package/dist/src/core/renderers/webgl/internal/RendererUtils.js.map +1 -1
- package/dist/src/core/text-rendering/SdfTextRenderer.js +16 -3
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/main-api/Renderer.d.ts +16 -0
- package/dist/src/main-api/Renderer.js +2 -0
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/src/utils.js +23 -7
- package/dist/src/utils.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/common/EventEmitter.ts +21 -0
- package/src/core/Autosizer.ts +3 -1
- package/src/core/CoreNode.test.ts +283 -0
- package/src/core/CoreNode.ts +131 -26
- package/src/core/CoreTextNode.test.ts +1 -0
- package/src/core/CoreTextNode.ts +6 -2
- package/src/core/Stage.ts +4 -0
- package/src/core/TextureMemoryManager.test.ts +323 -7
- package/src/core/TextureMemoryManager.ts +101 -5
- package/src/core/lib/WebGlContextWrapper.test.ts +58 -0
- package/src/core/lib/WebGlContextWrapper.ts +13 -3
- package/src/core/lib/textureCompression.test.ts +158 -0
- package/src/core/lib/textureCompression.ts +78 -4
- package/src/core/renderers/CoreRenderer.ts +1 -0
- package/src/core/renderers/canvas/CanvasRenderer.ts +7 -1
- package/src/core/renderers/webgl/WebGlRenderer.ts +64 -16
- package/src/core/renderers/webgl/WebGlShaderProgram.ts +78 -15
- package/src/core/renderers/webgl/WebGlShaderProgram.uniformDedup.test.ts +233 -0
- package/src/core/renderers/webgl/internal/RendererUtils.test.ts +73 -0
- package/src/core/renderers/webgl/internal/RendererUtils.ts +9 -2
- package/src/core/text-rendering/SdfTextRenderer.test.ts +11 -10
- package/src/core/text-rendering/SdfTextRenderer.ts +17 -3
- package/src/main-api/Renderer.ts +19 -0
- package/src/utils.test.ts +57 -0
- package/src/utils.ts +24 -8
|
@@ -7,6 +7,8 @@ import { type TextureOptions } from './CoreTextureManager.js';
|
|
|
7
7
|
import { createBound } from './lib/utils.js';
|
|
8
8
|
import { ImageTexture } from './textures/ImageTexture.js';
|
|
9
9
|
import { Matrix3d } from './lib/Matrix3d.js';
|
|
10
|
+
import { EventEmitter } from '../common/EventEmitter.js';
|
|
11
|
+
import { premultiplyColorABGR } from '../utils.js';
|
|
10
12
|
|
|
11
13
|
describe('set color()', () => {
|
|
12
14
|
const defaultProps = (overrides?: Partial<CoreNodeProps>): CoreNodeProps => ({
|
|
@@ -23,6 +25,7 @@ describe('set color()', () => {
|
|
|
23
25
|
colorTl: 0,
|
|
24
26
|
colorTop: 0,
|
|
25
27
|
colorTr: 0,
|
|
28
|
+
placeholderColor: 0,
|
|
26
29
|
h: 0,
|
|
27
30
|
mount: 0,
|
|
28
31
|
mountX: 0,
|
|
@@ -1180,4 +1183,284 @@ describe('set color()', () => {
|
|
|
1180
1183
|
expect(node.clippingRect.h).toBe(30);
|
|
1181
1184
|
});
|
|
1182
1185
|
});
|
|
1186
|
+
|
|
1187
|
+
describe('RecalcUniforms scoping', () => {
|
|
1188
|
+
const makeAttachedNode = () => {
|
|
1189
|
+
// Fresh stage per node: earlier tests can mutate the shared stage
|
|
1190
|
+
// mock's bound objects through by-reference strictBound assignment
|
|
1191
|
+
// in createRenderBounds.
|
|
1192
|
+
const localStage = mock<Stage>({
|
|
1193
|
+
strictBound: createBound(0, 0, 200, 200),
|
|
1194
|
+
preloadBound: createBound(0, 0, 200, 200),
|
|
1195
|
+
defaultTexture: {
|
|
1196
|
+
state: 'loaded',
|
|
1197
|
+
},
|
|
1198
|
+
renderer: mock<CoreRenderer>() as CoreRenderer,
|
|
1199
|
+
});
|
|
1200
|
+
const parent = new CoreNode(localStage, defaultProps());
|
|
1201
|
+
parent.globalTransform = Matrix3d.identity();
|
|
1202
|
+
parent.worldAlpha = 1;
|
|
1203
|
+
|
|
1204
|
+
const node = new CoreNode(
|
|
1205
|
+
localStage,
|
|
1206
|
+
defaultProps({ parent, w: 100, h: 100 }),
|
|
1207
|
+
);
|
|
1208
|
+
node.alpha = 1;
|
|
1209
|
+
return node;
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
it('should not set RecalcUniforms on pure translation', () => {
|
|
1213
|
+
const node = makeAttachedNode();
|
|
1214
|
+
node.update(0, clippingRect);
|
|
1215
|
+
|
|
1216
|
+
node.x = 50;
|
|
1217
|
+
node.y = 25;
|
|
1218
|
+
|
|
1219
|
+
expect(node.updateType & UpdateType.Local).toBe(UpdateType.Local);
|
|
1220
|
+
expect(node.updateType & UpdateType.RecalcUniforms).toBe(0);
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
it('should set RecalcUniforms when w changes', () => {
|
|
1224
|
+
const node = makeAttachedNode();
|
|
1225
|
+
node.update(0, clippingRect);
|
|
1226
|
+
|
|
1227
|
+
node.w = 150;
|
|
1228
|
+
|
|
1229
|
+
expect(node.updateType & UpdateType.RecalcUniforms).toBe(
|
|
1230
|
+
UpdateType.RecalcUniforms,
|
|
1231
|
+
);
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
it('should set RecalcUniforms when h changes', () => {
|
|
1235
|
+
const node = makeAttachedNode();
|
|
1236
|
+
node.update(0, clippingRect);
|
|
1237
|
+
|
|
1238
|
+
node.h = 75;
|
|
1239
|
+
|
|
1240
|
+
expect(node.updateType & UpdateType.RecalcUniforms).toBe(
|
|
1241
|
+
UpdateType.RecalcUniforms,
|
|
1242
|
+
);
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
it('should run the shader updater on resize but not on translation', () => {
|
|
1246
|
+
const node = makeAttachedNode();
|
|
1247
|
+
const shader = {
|
|
1248
|
+
shaderKey: 'test',
|
|
1249
|
+
update: vi.fn(),
|
|
1250
|
+
attachNode: vi.fn(),
|
|
1251
|
+
time: undefined,
|
|
1252
|
+
};
|
|
1253
|
+
// Assignment raises RecalcUniforms | IsRenderable via the setter
|
|
1254
|
+
node.shader = shader as never;
|
|
1255
|
+
node.update(0, clippingRect);
|
|
1256
|
+
expect(shader.update).toHaveBeenCalledTimes(1);
|
|
1257
|
+
|
|
1258
|
+
// Pure translation: no uniform recompute
|
|
1259
|
+
node.x = 50;
|
|
1260
|
+
node.update(0, clippingRect);
|
|
1261
|
+
expect(shader.update).toHaveBeenCalledTimes(1);
|
|
1262
|
+
|
|
1263
|
+
// Resize: uniforms depend on dimensions, must recompute
|
|
1264
|
+
node.w = 150;
|
|
1265
|
+
node.update(0, clippingRect);
|
|
1266
|
+
expect(shader.update).toHaveBeenCalledTimes(2);
|
|
1267
|
+
});
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
describe('placeholderColor', () => {
|
|
1271
|
+
// A texture fake on a real EventEmitter so CoreNode's loadTextureTask
|
|
1272
|
+
// subscribes for real and we can drive the loaded/freed/failed handler
|
|
1273
|
+
// chain by emitting, like the engine does.
|
|
1274
|
+
function emittingTexture(state: string): ImageTexture & {
|
|
1275
|
+
emit: (event: string, data?: unknown) => void;
|
|
1276
|
+
} {
|
|
1277
|
+
return Object.assign(new EventEmitter(), {
|
|
1278
|
+
state,
|
|
1279
|
+
preventCleanup: false,
|
|
1280
|
+
retryCount: 0,
|
|
1281
|
+
maxRetryCount: 1,
|
|
1282
|
+
dimensions: { w: 100, h: 100 },
|
|
1283
|
+
setRenderableOwner: vi.fn(),
|
|
1284
|
+
}) as unknown as ImageTexture & {
|
|
1285
|
+
emit: (event: string, data?: unknown) => void;
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function visibleNode(): CoreNode {
|
|
1290
|
+
const parent = new CoreNode(stage, defaultProps());
|
|
1291
|
+
parent.globalTransform = Matrix3d.identity();
|
|
1292
|
+
parent.worldAlpha = 1;
|
|
1293
|
+
|
|
1294
|
+
const node = new CoreNode(stage, defaultProps({ parent }));
|
|
1295
|
+
node.alpha = 1;
|
|
1296
|
+
node.x = 0;
|
|
1297
|
+
node.y = 0;
|
|
1298
|
+
node.w = 100;
|
|
1299
|
+
node.h = 100;
|
|
1300
|
+
return node;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Flush the queueMicrotask(loadTextureTask) so listeners attach.
|
|
1304
|
+
const flushMicrotasks = () => Promise.resolve();
|
|
1305
|
+
|
|
1306
|
+
it('renders the placeholder while the texture is loading', () => {
|
|
1307
|
+
const node = visibleNode();
|
|
1308
|
+
node.placeholderColor = 0x336699ff;
|
|
1309
|
+
node.texture = emittingTexture('initial');
|
|
1310
|
+
|
|
1311
|
+
node.update(0, clippingRect);
|
|
1312
|
+
|
|
1313
|
+
expect(node.placeholderActive).toBe(true);
|
|
1314
|
+
expect(node.isRenderable).toBe(true);
|
|
1315
|
+
expect(node.renderTexture).toBe(stage.defaultTexture);
|
|
1316
|
+
|
|
1317
|
+
const expected = premultiplyColorABGR(0x336699ff, 1);
|
|
1318
|
+
expect(node.premultipliedColorTl).toBe(expected);
|
|
1319
|
+
expect(node.premultipliedColorTr).toBe(expected);
|
|
1320
|
+
expect(node.premultipliedColorBl).toBe(expected);
|
|
1321
|
+
expect(node.premultipliedColorBr).toBe(expected);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
it('is inactive without a placeholderColor (loading renders nothing)', () => {
|
|
1325
|
+
const node = visibleNode();
|
|
1326
|
+
node.texture = emittingTexture('initial');
|
|
1327
|
+
|
|
1328
|
+
node.update(0, clippingRect);
|
|
1329
|
+
|
|
1330
|
+
expect(node.placeholderActive).toBe(false);
|
|
1331
|
+
expect(node.isRenderable).toBe(false);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
it('is inactive without a texture (color-only nodes are unaffected)', () => {
|
|
1335
|
+
const node = visibleNode();
|
|
1336
|
+
node.placeholderColor = 0x336699ff;
|
|
1337
|
+
|
|
1338
|
+
node.update(0, clippingRect);
|
|
1339
|
+
|
|
1340
|
+
expect(node.placeholderActive).toBe(false);
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
it('switches to the texture and regular colors once loaded', async () => {
|
|
1344
|
+
const node = visibleNode();
|
|
1345
|
+
node.color = 0xffffffff;
|
|
1346
|
+
node.placeholderColor = 0x336699ff;
|
|
1347
|
+
const texture = emittingTexture('initial');
|
|
1348
|
+
node.texture = texture;
|
|
1349
|
+
node.update(0, clippingRect);
|
|
1350
|
+
expect(node.renderTexture).toBe(stage.defaultTexture);
|
|
1351
|
+
|
|
1352
|
+
await flushMicrotasks();
|
|
1353
|
+
(texture as { state: string }).state = 'loaded';
|
|
1354
|
+
texture.emit('loaded', { w: 100, h: 100 });
|
|
1355
|
+
node.isQuadDirty = false;
|
|
1356
|
+
node.update(1, clippingRect);
|
|
1357
|
+
|
|
1358
|
+
expect(node.placeholderActive).toBe(false);
|
|
1359
|
+
expect(node.isRenderable).toBe(true);
|
|
1360
|
+
expect(node.renderTexture).toBe(texture);
|
|
1361
|
+
expect(node.premultipliedColorTl).toBe(
|
|
1362
|
+
premultiplyColorABGR(0xffffffff, 1),
|
|
1363
|
+
);
|
|
1364
|
+
// The color switch must reach the GPU quad buffer
|
|
1365
|
+
expect(node.isQuadDirty).toBe(true);
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
it('shows the placeholder again while a freed texture reloads', async () => {
|
|
1369
|
+
const node = visibleNode();
|
|
1370
|
+
node.placeholderColor = 0x336699ff;
|
|
1371
|
+
const texture = emittingTexture('initial');
|
|
1372
|
+
node.texture = texture;
|
|
1373
|
+
node.update(0, clippingRect);
|
|
1374
|
+
|
|
1375
|
+
await flushMicrotasks();
|
|
1376
|
+
(texture as { state: string }).state = 'loaded';
|
|
1377
|
+
texture.emit('loaded', { w: 100, h: 100 });
|
|
1378
|
+
node.update(1, clippingRect);
|
|
1379
|
+
expect(node.placeholderActive).toBe(false);
|
|
1380
|
+
|
|
1381
|
+
(texture as { state: string }).state = 'freed';
|
|
1382
|
+
texture.emit('freed');
|
|
1383
|
+
node.update(2, clippingRect);
|
|
1384
|
+
|
|
1385
|
+
expect(node.placeholderActive).toBe(true);
|
|
1386
|
+
expect(node.isRenderable).toBe(true);
|
|
1387
|
+
expect(node.renderTexture).toBe(stage.defaultTexture);
|
|
1388
|
+
expect(node.premultipliedColorTl).toBe(
|
|
1389
|
+
premultiplyColorABGR(0x336699ff, 1),
|
|
1390
|
+
);
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
it('keeps the placeholder when the texture permanently fails', async () => {
|
|
1394
|
+
const node = visibleNode();
|
|
1395
|
+
node.placeholderColor = 0x336699ff;
|
|
1396
|
+
const texture = emittingTexture('initial');
|
|
1397
|
+
node.texture = texture;
|
|
1398
|
+
node.update(0, clippingRect);
|
|
1399
|
+
|
|
1400
|
+
await flushMicrotasks();
|
|
1401
|
+
(texture as { state: string }).state = 'failed';
|
|
1402
|
+
(texture as { retryCount: number }).retryCount = 2; // > maxRetryCount (1)
|
|
1403
|
+
texture.emit('failed', new Error('404'));
|
|
1404
|
+
node.update(1, clippingRect);
|
|
1405
|
+
|
|
1406
|
+
expect(node.placeholderActive).toBe(true);
|
|
1407
|
+
expect(node.isRenderable).toBe(true);
|
|
1408
|
+
expect(node.renderTexture).toBe(stage.defaultTexture);
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
it('a permanently failed texture without a placeholder stays non-renderable', async () => {
|
|
1412
|
+
const node = visibleNode();
|
|
1413
|
+
const texture = emittingTexture('initial');
|
|
1414
|
+
node.texture = texture;
|
|
1415
|
+
node.update(0, clippingRect);
|
|
1416
|
+
|
|
1417
|
+
await flushMicrotasks();
|
|
1418
|
+
(texture as { state: string }).state = 'failed';
|
|
1419
|
+
(texture as { retryCount: number }).retryCount = 2;
|
|
1420
|
+
texture.emit('failed', new Error('404'));
|
|
1421
|
+
node.update(1, clippingRect);
|
|
1422
|
+
|
|
1423
|
+
expect(node.isRenderable).toBe(false);
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
it('deactivates when placeholderColor is cleared while loading', () => {
|
|
1427
|
+
const node = visibleNode();
|
|
1428
|
+
node.placeholderColor = 0x336699ff;
|
|
1429
|
+
node.texture = emittingTexture('initial');
|
|
1430
|
+
node.update(0, clippingRect);
|
|
1431
|
+
expect(node.isRenderable).toBe(true);
|
|
1432
|
+
|
|
1433
|
+
node.placeholderColor = 0;
|
|
1434
|
+
node.update(1, clippingRect);
|
|
1435
|
+
|
|
1436
|
+
expect(node.placeholderActive).toBe(false);
|
|
1437
|
+
expect(node.isRenderable).toBe(false);
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
it('updates the shown color when placeholderColor changes while active', () => {
|
|
1441
|
+
const node = visibleNode();
|
|
1442
|
+
node.placeholderColor = 0x336699ff;
|
|
1443
|
+
node.texture = emittingTexture('initial');
|
|
1444
|
+
node.update(0, clippingRect);
|
|
1445
|
+
|
|
1446
|
+
node.placeholderColor = 0x993311ff;
|
|
1447
|
+
node.update(1, clippingRect);
|
|
1448
|
+
|
|
1449
|
+
expect(node.premultipliedColorTl).toBe(
|
|
1450
|
+
premultiplyColorABGR(0x993311ff, 1),
|
|
1451
|
+
);
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
it('does not activate when the assigned texture is already loaded', () => {
|
|
1455
|
+
const node = visibleNode();
|
|
1456
|
+
node.placeholderColor = 0x336699ff;
|
|
1457
|
+
const texture = emittingTexture('loaded');
|
|
1458
|
+
node.texture = texture;
|
|
1459
|
+
|
|
1460
|
+
node.update(0, clippingRect);
|
|
1461
|
+
|
|
1462
|
+
expect(node.placeholderActive).toBe(false);
|
|
1463
|
+
expect(node.renderTexture).toBe(texture);
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1183
1466
|
});
|
package/src/core/CoreNode.ts
CHANGED
|
@@ -392,6 +392,23 @@ export interface CoreNodeProps {
|
|
|
392
392
|
* rendering.
|
|
393
393
|
*/
|
|
394
394
|
colorBl: number;
|
|
395
|
+
/**
|
|
396
|
+
* Placeholder color shown while the Node's texture is not yet loaded.
|
|
397
|
+
*
|
|
398
|
+
* @remarks
|
|
399
|
+
* When set to a non-zero color and the Node has a texture (e.g. {@link src}),
|
|
400
|
+
* the Node renders a solid rectangle of this color until the texture
|
|
401
|
+
* finishes loading, instead of rendering nothing. The placeholder renders
|
|
402
|
+
* through the Node's shader, so rounded corners and borders apply to it.
|
|
403
|
+
* It also shows again if the texture is freed under memory pressure (until
|
|
404
|
+
* the reload completes) and remains if the texture permanently fails.
|
|
405
|
+
*
|
|
406
|
+
* The color value is a number in the format 0xRRGGBBAA. Set to `0` to
|
|
407
|
+
* disable (default). Has no effect on Nodes without a texture.
|
|
408
|
+
*
|
|
409
|
+
* @default `0`
|
|
410
|
+
*/
|
|
411
|
+
placeholderColor: number;
|
|
395
412
|
/**
|
|
396
413
|
* The Node's parent Node.
|
|
397
414
|
*
|
|
@@ -793,6 +810,15 @@ export class CoreNode extends EventEmitter {
|
|
|
793
810
|
private hasColorProps = false;
|
|
794
811
|
public textureLoaded = false;
|
|
795
812
|
|
|
813
|
+
/**
|
|
814
|
+
* True while this node should render its `placeholderColor` instead of its
|
|
815
|
+
* texture: `placeholderColor` is non-zero, a texture is set, and that
|
|
816
|
+
* texture is not loaded. Read by the renderers' quad path to substitute the
|
|
817
|
+
* stage's default (1x1 white) texture. Maintained by
|
|
818
|
+
* {@link updatePlaceholderActive} — never written elsewhere.
|
|
819
|
+
*/
|
|
820
|
+
public placeholderActive = false;
|
|
821
|
+
|
|
796
822
|
public updateType = UpdateType.All;
|
|
797
823
|
public childUpdateType = UpdateType.None;
|
|
798
824
|
|
|
@@ -953,6 +979,31 @@ export class CoreNode extends EventEmitter {
|
|
|
953
979
|
}
|
|
954
980
|
|
|
955
981
|
//#region Textures
|
|
982
|
+
/**
|
|
983
|
+
* Recompute {@link placeholderActive} after any of its inputs changed
|
|
984
|
+
* (placeholderColor, texture, textureLoaded).
|
|
985
|
+
*
|
|
986
|
+
* @remarks
|
|
987
|
+
* On a toggle this raises `PremultipliedColors` (the quad's vertex colors
|
|
988
|
+
* switch between the placeholder color and the regular color props — this
|
|
989
|
+
* also marks the quad dirty) and `IsRenderable` (a loading texture with a
|
|
990
|
+
* placeholder is renderable). Both are processed in the same frame's update
|
|
991
|
+
* pass, before quads are submitted.
|
|
992
|
+
*/
|
|
993
|
+
private updatePlaceholderActive(): void {
|
|
994
|
+
const active =
|
|
995
|
+
this.props.placeholderColor !== 0 &&
|
|
996
|
+
this.props.texture !== null &&
|
|
997
|
+
this.textureLoaded === false;
|
|
998
|
+
|
|
999
|
+
if (active !== this.placeholderActive) {
|
|
1000
|
+
this.placeholderActive = active;
|
|
1001
|
+
this.setUpdateType(
|
|
1002
|
+
UpdateType.PremultipliedColors | UpdateType.IsRenderable,
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
956
1007
|
loadTexture(): void {
|
|
957
1008
|
if (this.props.texture === null) {
|
|
958
1009
|
return;
|
|
@@ -1023,6 +1074,7 @@ export class CoreNode extends EventEmitter {
|
|
|
1023
1074
|
}
|
|
1024
1075
|
|
|
1025
1076
|
this.textureLoaded = true;
|
|
1077
|
+
this.updatePlaceholderActive();
|
|
1026
1078
|
this.setUpdateType(UpdateType.IsRenderable);
|
|
1027
1079
|
|
|
1028
1080
|
// Texture was loaded. In case the RAF loop has already stopped, we request
|
|
@@ -1057,9 +1109,12 @@ export class CoreNode extends EventEmitter {
|
|
|
1057
1109
|
|
|
1058
1110
|
private onTextureFailed: TextureFailedEventHandler = (_, error) => {
|
|
1059
1111
|
// immediately set isRenderable to false, so that we handle the error
|
|
1060
|
-
// without waiting for the next frame loop
|
|
1112
|
+
// without waiting for the next frame loop. With a placeholder set, the
|
|
1113
|
+
// same frame's update pass recomputes this to true and renders the
|
|
1114
|
+
// placeholder instead.
|
|
1061
1115
|
this.textureLoaded = false;
|
|
1062
1116
|
this.isRenderable = false;
|
|
1117
|
+
this.updatePlaceholderActive();
|
|
1063
1118
|
this.updateTextureOwnership(false);
|
|
1064
1119
|
this.setUpdateType(UpdateType.IsRenderable);
|
|
1065
1120
|
|
|
@@ -1081,9 +1136,12 @@ export class CoreNode extends EventEmitter {
|
|
|
1081
1136
|
|
|
1082
1137
|
private onTextureFreed: TextureFreedEventHandler = () => {
|
|
1083
1138
|
// immediately set isRenderable to false, so that we handle the error
|
|
1084
|
-
// without waiting for the next frame loop
|
|
1139
|
+
// without waiting for the next frame loop. With a placeholder set, the
|
|
1140
|
+
// same frame's update pass recomputes this to true and renders the
|
|
1141
|
+
// placeholder while the texture reloads.
|
|
1085
1142
|
this.textureLoaded = false;
|
|
1086
1143
|
this.isRenderable = false;
|
|
1144
|
+
this.updatePlaceholderActive();
|
|
1087
1145
|
this.updateTextureOwnership(false);
|
|
1088
1146
|
this.setUpdateType(UpdateType.IsRenderable);
|
|
1089
1147
|
|
|
@@ -1355,7 +1413,13 @@ export class CoreNode extends EventEmitter {
|
|
|
1355
1413
|
this.calculateRenderCoords();
|
|
1356
1414
|
this.updateBoundingRect();
|
|
1357
1415
|
|
|
1358
|
-
|
|
1416
|
+
// RecalcUniforms is intentionally NOT set here: shader uniforms are a
|
|
1417
|
+
// function of resolvedProps + w/h only (that is exactly the shader
|
|
1418
|
+
// value-key cache key), so pure transform changes (translate, scale,
|
|
1419
|
+
// rotate) cannot affect them. The flag is raised where w/h actually
|
|
1420
|
+
// change: the w/h setters, Autosizer.applyDimensions, text layout
|
|
1421
|
+
// application, and the shader setter itself.
|
|
1422
|
+
updateType |= UpdateType.RenderState;
|
|
1359
1423
|
|
|
1360
1424
|
//only propagate children updates if not autosizing
|
|
1361
1425
|
if ((updateType & UpdateType.Autosize) === 0) {
|
|
@@ -1420,27 +1484,39 @@ export class CoreNode extends EventEmitter {
|
|
|
1420
1484
|
if (updateType & UpdateType.PremultipliedColors) {
|
|
1421
1485
|
const alpha = this.worldAlpha;
|
|
1422
1486
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
const merged = premultiplyColorABGR(tl, alpha);
|
|
1432
|
-
|
|
1433
|
-
this.premultipliedColorTl = merged;
|
|
1434
|
-
|
|
1435
|
-
if (same === true) {
|
|
1436
|
-
this.premultipliedColorTr =
|
|
1487
|
+
if (this.placeholderActive === true) {
|
|
1488
|
+
// Placeholder rendering: all four corners take the placeholder color.
|
|
1489
|
+
// The quad samples the stage's default 1x1 white texture, so this is
|
|
1490
|
+
// exactly the color-rect path.
|
|
1491
|
+
const merged = premultiplyColorABGR(props.placeholderColor, alpha);
|
|
1492
|
+
this.premultipliedColorTl =
|
|
1493
|
+
this.premultipliedColorTr =
|
|
1437
1494
|
this.premultipliedColorBl =
|
|
1438
1495
|
this.premultipliedColorBr =
|
|
1439
1496
|
merged;
|
|
1440
1497
|
} else {
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1498
|
+
const tl = props.colorTl;
|
|
1499
|
+
const tr = props.colorTr;
|
|
1500
|
+
const bl = props.colorBl;
|
|
1501
|
+
const br = props.colorBr;
|
|
1502
|
+
|
|
1503
|
+
// Fast equality check (covers all 4 corners)
|
|
1504
|
+
const same = tl === tr && tl === bl && tl === br;
|
|
1505
|
+
|
|
1506
|
+
const merged = premultiplyColorABGR(tl, alpha);
|
|
1507
|
+
|
|
1508
|
+
this.premultipliedColorTl = merged;
|
|
1509
|
+
|
|
1510
|
+
if (same === true) {
|
|
1511
|
+
this.premultipliedColorTr =
|
|
1512
|
+
this.premultipliedColorBl =
|
|
1513
|
+
this.premultipliedColorBr =
|
|
1514
|
+
merged;
|
|
1515
|
+
} else {
|
|
1516
|
+
this.premultipliedColorTr = premultiplyColorABGR(tr, alpha);
|
|
1517
|
+
this.premultipliedColorBl = premultiplyColorABGR(bl, alpha);
|
|
1518
|
+
this.premultipliedColorBr = premultiplyColorABGR(br, alpha);
|
|
1519
|
+
}
|
|
1444
1520
|
}
|
|
1445
1521
|
}
|
|
1446
1522
|
|
|
@@ -1754,15 +1830,18 @@ export class CoreNode extends EventEmitter {
|
|
|
1754
1830
|
// preemptive check for failed textures this will mark the current node as non-renderable
|
|
1755
1831
|
// and will prevent further checks until the texture is reloaded or retry is reset on the texture
|
|
1756
1832
|
if (this.texture.retryCount > this.texture.maxRetryCount) {
|
|
1757
|
-
// texture has failed to load, we cannot render
|
|
1833
|
+
// texture has failed to load, we cannot render the texture itself —
|
|
1834
|
+
// but a placeholder color still renders in its place
|
|
1758
1835
|
this.updateTextureOwnership(false);
|
|
1759
|
-
this.setRenderable(
|
|
1836
|
+
this.setRenderable(this.placeholderActive);
|
|
1760
1837
|
return;
|
|
1761
1838
|
}
|
|
1762
1839
|
|
|
1763
1840
|
needsTextureOwnership = true;
|
|
1764
|
-
// Use cached boolean instead of string comparison
|
|
1765
|
-
|
|
1841
|
+
// Use cached boolean instead of string comparison; a placeholder
|
|
1842
|
+
// renders while the texture is loading
|
|
1843
|
+
newIsRenderable =
|
|
1844
|
+
this.textureLoaded === true || this.placeholderActive === true;
|
|
1766
1845
|
} else if (
|
|
1767
1846
|
// check shader
|
|
1768
1847
|
(this.props.shader !== this.stage.renderer.getDefaultShaderNode() ||
|
|
@@ -2049,6 +2128,9 @@ export class CoreNode extends EventEmitter {
|
|
|
2049
2128
|
}
|
|
2050
2129
|
|
|
2051
2130
|
get renderTexture(): Texture | null {
|
|
2131
|
+
if (this.placeholderActive === true) {
|
|
2132
|
+
return this.stage.defaultTexture;
|
|
2133
|
+
}
|
|
2052
2134
|
return this.props.texture || this.stage.defaultTexture;
|
|
2053
2135
|
}
|
|
2054
2136
|
|
|
@@ -2232,7 +2314,9 @@ export class CoreNode extends EventEmitter {
|
|
|
2232
2314
|
const props = this.props;
|
|
2233
2315
|
if (props.w !== value) {
|
|
2234
2316
|
props.w = value;
|
|
2235
|
-
|
|
2317
|
+
// Dimensions feed shader uniforms (e.g. factored corner radius), so a
|
|
2318
|
+
// resize must recompute them; see the Global-update branch in update().
|
|
2319
|
+
let updateType = UpdateType.Local | UpdateType.RecalcUniforms;
|
|
2236
2320
|
|
|
2237
2321
|
if (
|
|
2238
2322
|
props.texture !== null &&
|
|
@@ -2262,7 +2346,9 @@ export class CoreNode extends EventEmitter {
|
|
|
2262
2346
|
const props = this.props;
|
|
2263
2347
|
if (props.h !== value) {
|
|
2264
2348
|
props.h = value;
|
|
2265
|
-
|
|
2349
|
+
// Dimensions feed shader uniforms (e.g. factored corner radius), so a
|
|
2350
|
+
// resize must recompute them; see the Global-update branch in update().
|
|
2351
|
+
let updateType = UpdateType.Local | UpdateType.RecalcUniforms;
|
|
2266
2352
|
|
|
2267
2353
|
if (
|
|
2268
2354
|
props.texture !== null &&
|
|
@@ -2514,6 +2600,24 @@ export class CoreNode extends EventEmitter {
|
|
|
2514
2600
|
this.setUpdateType(UpdateType.PremultipliedColors);
|
|
2515
2601
|
}
|
|
2516
2602
|
|
|
2603
|
+
get placeholderColor(): number {
|
|
2604
|
+
return this.props.placeholderColor;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
set placeholderColor(value: number) {
|
|
2608
|
+
const p = this.props;
|
|
2609
|
+
if (p.placeholderColor === value) return;
|
|
2610
|
+
|
|
2611
|
+
p.placeholderColor = value;
|
|
2612
|
+
this.updatePlaceholderActive();
|
|
2613
|
+
|
|
2614
|
+
// If the placeholder is (still) showing, the new color must reach the
|
|
2615
|
+
// quad buffer even though the active state did not toggle.
|
|
2616
|
+
if (this.placeholderActive === true) {
|
|
2617
|
+
this.setUpdateType(UpdateType.PremultipliedColors);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2517
2621
|
get colorTop(): number {
|
|
2518
2622
|
return this.props.colorTop;
|
|
2519
2623
|
}
|
|
@@ -2934,6 +3038,7 @@ export class CoreNode extends EventEmitter {
|
|
|
2934
3038
|
this.textureCoords = undefined;
|
|
2935
3039
|
this.props.texture = value;
|
|
2936
3040
|
this.textureLoaded = value !== null && value.state === 'loaded';
|
|
3041
|
+
this.updatePlaceholderActive();
|
|
2937
3042
|
|
|
2938
3043
|
if (value !== null) {
|
|
2939
3044
|
if (this.autosizer !== null) {
|
package/src/core/CoreTextNode.ts
CHANGED
|
@@ -271,8 +271,12 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
|
|
|
271
271
|
this.props.w = width;
|
|
272
272
|
this.props.h = height;
|
|
273
273
|
|
|
274
|
-
// Text dimensions may have changed, recalculate transforms and bounds
|
|
275
|
-
|
|
274
|
+
// Text dimensions may have changed, recalculate transforms and bounds.
|
|
275
|
+
// RecalcUniforms because the direct props.w/h write above bypasses the
|
|
276
|
+
// w/h setters and dimensions feed shader uniforms.
|
|
277
|
+
this.setUpdateType(
|
|
278
|
+
UpdateType.Local | UpdateType.RenderBounds | UpdateType.RecalcUniforms,
|
|
279
|
+
);
|
|
276
280
|
|
|
277
281
|
// Handle SDF renderer (uses layout caching)
|
|
278
282
|
if (textRendererType === 'sdf') {
|
package/src/core/Stage.ts
CHANGED
|
@@ -185,6 +185,7 @@ export class Stage {
|
|
|
185
185
|
boundsMargin,
|
|
186
186
|
enableContextSpy,
|
|
187
187
|
forceWebGL2,
|
|
188
|
+
disableVertexArrayObject,
|
|
188
189
|
numImageWorkers,
|
|
189
190
|
textureMemory,
|
|
190
191
|
renderEngine,
|
|
@@ -256,6 +257,7 @@ export class Stage {
|
|
|
256
257
|
canvas,
|
|
257
258
|
contextSpy: this.contextSpy,
|
|
258
259
|
forceWebGL2,
|
|
260
|
+
disableVertexArrayObject,
|
|
259
261
|
});
|
|
260
262
|
|
|
261
263
|
this.shManager = new CoreShaderManager(this);
|
|
@@ -380,6 +382,7 @@ export class Stage {
|
|
|
380
382
|
colorRight: 0x00000000,
|
|
381
383
|
colorTl: 0x00000000,
|
|
382
384
|
colorTr: 0x00000000,
|
|
385
|
+
placeholderColor: 0x00000000,
|
|
383
386
|
colorBl: 0x00000000,
|
|
384
387
|
colorBr: 0x00000000,
|
|
385
388
|
zIndex: 0,
|
|
@@ -1041,6 +1044,7 @@ export class Stage {
|
|
|
1041
1044
|
colorTr,
|
|
1042
1045
|
colorBl,
|
|
1043
1046
|
colorBr,
|
|
1047
|
+
placeholderColor: props.placeholderColor ?? 0,
|
|
1044
1048
|
zIndex: props.zIndex ?? 0,
|
|
1045
1049
|
parent: props.parent ?? null,
|
|
1046
1050
|
texture: props.texture ?? null,
|