@flighthq/displayobject-gl 0.1.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.
- package/dist/glBitmap.d.ts +4 -0
- package/dist/glBitmap.d.ts.map +1 -0
- package/dist/glBitmap.js +106 -0
- package/dist/glBitmap.js.map +1 -0
- package/dist/glCache.d.ts +30 -0
- package/dist/glCache.d.ts.map +1 -0
- package/dist/glCache.js +185 -0
- package/dist/glCache.js.map +1 -0
- package/dist/glClip.d.ts +3 -0
- package/dist/glClip.d.ts.map +1 -0
- package/dist/glClip.js +46 -0
- package/dist/glClip.js.map +1 -0
- package/dist/glClipContours.d.ts +4 -0
- package/dist/glClipContours.d.ts.map +1 -0
- package/dist/glClipContours.js +148 -0
- package/dist/glClipContours.js.map +1 -0
- package/dist/glClipRectangle.d.ts +4 -0
- package/dist/glClipRectangle.d.ts.map +1 -0
- package/dist/glClipRectangle.js +68 -0
- package/dist/glClipRectangle.js.map +1 -0
- package/dist/glColorTransformMaterial.d.ts +4 -0
- package/dist/glColorTransformMaterial.d.ts.map +1 -0
- package/dist/glColorTransformMaterial.js +116 -0
- package/dist/glColorTransformMaterial.js.map +1 -0
- package/dist/glDefaultMaterial.d.ts +4 -0
- package/dist/glDefaultMaterial.d.ts.map +1 -0
- package/dist/glDefaultMaterial.js +20 -0
- package/dist/glDefaultMaterial.js.map +1 -0
- package/dist/glDisplayObject.d.ts +5 -0
- package/dist/glDisplayObject.d.ts.map +1 -0
- package/dist/glDisplayObject.js +41 -0
- package/dist/glDisplayObject.js.map +1 -0
- package/dist/glDisplayObjectRegistration.d.ts +25 -0
- package/dist/glDisplayObjectRegistration.d.ts.map +1 -0
- package/dist/glDisplayObjectRegistration.js +51 -0
- package/dist/glDisplayObjectRegistration.js.map +1 -0
- package/dist/glMaterials.d.ts +4 -0
- package/dist/glMaterials.d.ts.map +1 -0
- package/dist/glMaterials.js +97 -0
- package/dist/glMaterials.js.map +1 -0
- package/dist/glParticleEmitter.d.ts +4 -0
- package/dist/glParticleEmitter.d.ts.map +1 -0
- package/dist/glParticleEmitter.js +258 -0
- package/dist/glParticleEmitter.js.map +1 -0
- package/dist/glQuadBatch.d.ts +3 -0
- package/dist/glQuadBatch.d.ts.map +1 -0
- package/dist/glQuadBatch.js +88 -0
- package/dist/glQuadBatch.js.map +1 -0
- package/dist/glRichText.d.ts +9 -0
- package/dist/glRichText.d.ts.map +1 -0
- package/dist/glRichText.js +149 -0
- package/dist/glRichText.js.map +1 -0
- package/dist/glScale9Mapper.d.ts +3 -0
- package/dist/glScale9Mapper.d.ts.map +1 -0
- package/dist/glScale9Mapper.js +37 -0
- package/dist/glScale9Mapper.js.map +1 -0
- package/dist/glScale9Shape.d.ts +9 -0
- package/dist/glScale9Shape.d.ts.map +1 -0
- package/dist/glScale9Shape.js +118 -0
- package/dist/glScale9Shape.js.map +1 -0
- package/dist/glShape.d.ts +4 -0
- package/dist/glShape.d.ts.map +1 -0
- package/dist/glShape.js +125 -0
- package/dist/glShape.js.map +1 -0
- package/dist/glShapeMesh.d.ts +9 -0
- package/dist/glShapeMesh.d.ts.map +1 -0
- package/dist/glShapeMesh.js +90 -0
- package/dist/glShapeMesh.js.map +1 -0
- package/dist/glSprite.d.ts +3 -0
- package/dist/glSprite.d.ts.map +1 -0
- package/dist/glSprite.js +28 -0
- package/dist/glSprite.js.map +1 -0
- package/dist/glSpriteBatch.d.ts +11 -0
- package/dist/glSpriteBatch.d.ts.map +1 -0
- package/dist/glSpriteBatch.js +241 -0
- package/dist/glSpriteBatch.js.map +1 -0
- package/dist/glSpriteRenderer.d.ts +3 -0
- package/dist/glSpriteRenderer.d.ts.map +1 -0
- package/dist/glSpriteRenderer.js +50 -0
- package/dist/glSpriteRenderer.js.map +1 -0
- package/dist/glTestHelper.d.ts +9 -0
- package/dist/glTestHelper.d.ts.map +1 -0
- package/dist/glTestHelper.js +17 -0
- package/dist/glTestHelper.js.map +1 -0
- package/dist/glTextInput.d.ts +4 -0
- package/dist/glTextInput.d.ts.map +1 -0
- package/dist/glTextInput.js +43 -0
- package/dist/glTextInput.js.map +1 -0
- package/dist/glTextLabel.d.ts +4 -0
- package/dist/glTextLabel.d.ts.map +1 -0
- package/dist/glTextLabel.js +130 -0
- package/dist/glTextLabel.js.map +1 -0
- package/dist/glTilemap.d.ts +3 -0
- package/dist/glTilemap.d.ts.map +1 -0
- package/dist/glTilemap.js +74 -0
- package/dist/glTilemap.js.map +1 -0
- package/dist/glUniformColorTransformMaterial.d.ts +4 -0
- package/dist/glUniformColorTransformMaterial.d.ts.map +1 -0
- package/dist/glUniformColorTransformMaterial.js +56 -0
- package/dist/glUniformColorTransformMaterial.js.map +1 -0
- package/dist/glVelocity.d.ts +22 -0
- package/dist/glVelocity.d.ts.map +1 -0
- package/dist/glVelocity.js +319 -0
- package/dist/glVelocity.js.map +1 -0
- package/dist/glVideo.d.ts +6 -0
- package/dist/glVideo.d.ts.map +1 -0
- package/dist/glVideo.js +60 -0
- package/dist/glVideo.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/moduleMockTestHelper.d.ts +15 -0
- package/dist/moduleMockTestHelper.d.ts.map +1 -0
- package/dist/moduleMockTestHelper.js +25 -0
- package/dist/moduleMockTestHelper.js.map +1 -0
- package/package.json +51 -0
- package/src/glBitmap.test.ts +128 -0
- package/src/glCache.test.ts +230 -0
- package/src/glClip.test.ts +101 -0
- package/src/glClipContours.test.ts +61 -0
- package/src/glClipRectangle.test.ts +47 -0
- package/src/glColorTransformMaterial.test.ts +45 -0
- package/src/glDefaultMaterial.test.ts +19 -0
- package/src/glDisplayObject.test.ts +85 -0
- package/src/glDisplayObjectRegistration.test.ts +130 -0
- package/src/glMaterials.test.ts +91 -0
- package/src/glParticleEmitter.test.ts +98 -0
- package/src/glQuadBatch.test.ts +132 -0
- package/src/glRichText.test.ts +135 -0
- package/src/glScale9Mapper.test.ts +20 -0
- package/src/glScale9Shape.test.ts +90 -0
- package/src/glShape.test.ts +115 -0
- package/src/glShapeMesh.test.ts +64 -0
- package/src/glSprite.test.ts +79 -0
- package/src/glSpriteBatch.test.ts +153 -0
- package/src/glSpriteRenderer.test.ts +75 -0
- package/src/glTextInput.test.ts +51 -0
- package/src/glTextLabel.test.ts +151 -0
- package/src/glTilemap.test.ts +127 -0
- package/src/glUniformColorTransformMaterial.test.ts +24 -0
- package/src/glVelocity.test.ts +155 -0
- package/src/glVideo.test.ts +65 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { RenderProxy2D } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { registerDefaultGlMaterial } from './glDefaultMaterial';
|
|
4
|
+
import { flushGlSpriteBatch } from './glSpriteBatch';
|
|
5
|
+
import { defaultGlSpriteRenderer } from './glSpriteRenderer';
|
|
6
|
+
import { createGlState } from './glTestHelper';
|
|
7
|
+
|
|
8
|
+
function makeAtlas(regionWidth = 32, regionHeight = 32) {
|
|
9
|
+
const img = document.createElement('img');
|
|
10
|
+
return {
|
|
11
|
+
image: { source: img, width: 64, height: 64 },
|
|
12
|
+
regions: [{ x: 0, y: 0, width: regionWidth, height: regionHeight }],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeSpriteNode(data: Record<string, unknown> = {}): RenderProxy2D {
|
|
17
|
+
return {
|
|
18
|
+
source: { data: { atlas: null, id: 0, ...data } },
|
|
19
|
+
blendMode: 0,
|
|
20
|
+
alpha: 1,
|
|
21
|
+
material: null,
|
|
22
|
+
materialData: null,
|
|
23
|
+
renderer: null,
|
|
24
|
+
traverseChildren: false,
|
|
25
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
26
|
+
} as unknown as RenderProxy2D;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('defaultGlSpriteRenderer', () => {
|
|
30
|
+
it('has a createData function', () => {
|
|
31
|
+
expect(typeof defaultGlSpriteRenderer.createData).toBe('function');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('has a submit function', () => {
|
|
35
|
+
expect(typeof defaultGlSpriteRenderer.submit).toBe('function');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('defaultGlSpriteRenderer.submit', () => {
|
|
40
|
+
it('returns early without drawing when atlas is null', () => {
|
|
41
|
+
const { state, gl } = createGlState();
|
|
42
|
+
defaultGlSpriteRenderer.submit(state, makeSpriteNode({ atlas: null }));
|
|
43
|
+
flushGlSpriteBatch(state);
|
|
44
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns early without drawing when atlas.image is null', () => {
|
|
48
|
+
const { state, gl } = createGlState();
|
|
49
|
+
defaultGlSpriteRenderer.submit(state, makeSpriteNode({ atlas: { image: null, regions: [] } }));
|
|
50
|
+
flushGlSpriteBatch(state);
|
|
51
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns early without drawing when id is negative', () => {
|
|
55
|
+
const { state, gl } = createGlState();
|
|
56
|
+
defaultGlSpriteRenderer.submit(state, makeSpriteNode({ atlas: makeAtlas(), id: -1 }));
|
|
57
|
+
flushGlSpriteBatch(state);
|
|
58
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns early without drawing when region width is zero', () => {
|
|
62
|
+
const { state, gl } = createGlState();
|
|
63
|
+
defaultGlSpriteRenderer.submit(state, makeSpriteNode({ atlas: makeAtlas(0, 32), id: 0 }));
|
|
64
|
+
flushGlSpriteBatch(state);
|
|
65
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('draws a quad when the atlas region is valid', () => {
|
|
69
|
+
const { state, gl } = createGlState();
|
|
70
|
+
registerDefaultGlMaterial(state);
|
|
71
|
+
defaultGlSpriteRenderer.submit(state, makeSpriteNode({ atlas: makeAtlas(), id: 0 }));
|
|
72
|
+
flushGlSpriteBatch(state);
|
|
73
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledWith(expect.anything(), 6, expect.anything(), 0, 1);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createRichText } from '@flighthq/text';
|
|
2
|
+
import { enableTextInput, setTextInputSelection } from '@flighthq/textinput';
|
|
3
|
+
import type { RenderProxy2D, RichText } from '@flighthq/types';
|
|
4
|
+
|
|
5
|
+
import { drawGlRichText } from './glRichText';
|
|
6
|
+
import { createGlState } from './glTestHelper';
|
|
7
|
+
import { drawGlTextInputOverlay, enableGlTextInput } from './glTextInput';
|
|
8
|
+
|
|
9
|
+
function makeFocusedInputProxy(): RenderProxy2D {
|
|
10
|
+
const node = createRichText({ data: { height: 40, text: 'hello', width: 100 } });
|
|
11
|
+
enableTextInput(node).focused = true;
|
|
12
|
+
return {
|
|
13
|
+
source: node,
|
|
14
|
+
blendMode: 0,
|
|
15
|
+
alpha: 1,
|
|
16
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
17
|
+
rendererData: { texture: null },
|
|
18
|
+
} as unknown as RenderProxy2D;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('drawGlTextInputOverlay', () => {
|
|
22
|
+
it('is the installed overlay function', () => {
|
|
23
|
+
expect(typeof drawGlTextInputOverlay).toBe('function');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('rasterizes a focused collapsed selection without throwing', () => {
|
|
27
|
+
enableGlTextInput();
|
|
28
|
+
const { state, gl } = createGlState();
|
|
29
|
+
const renderProxy = makeFocusedInputProxy();
|
|
30
|
+
setTextInputSelection(renderProxy.source as RichText, 2, 2);
|
|
31
|
+
|
|
32
|
+
expect(() => drawGlRichText(state, renderProxy)).not.toThrow();
|
|
33
|
+
expect(gl.drawElements).toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rasterizes a focused expanded selection without throwing', () => {
|
|
37
|
+
enableGlTextInput();
|
|
38
|
+
const { state, gl } = createGlState();
|
|
39
|
+
const renderProxy = makeFocusedInputProxy();
|
|
40
|
+
setTextInputSelection(renderProxy.source as RichText, 1, 4);
|
|
41
|
+
|
|
42
|
+
expect(() => drawGlRichText(state, renderProxy)).not.toThrow();
|
|
43
|
+
expect(gl.drawElements).toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('enableGlTextInput', () => {
|
|
48
|
+
it('installs the overlay without throwing', () => {
|
|
49
|
+
expect(() => enableGlTextInput()).not.toThrow();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { getGlRenderStateRuntime } from '@flighthq/render-gl';
|
|
2
|
+
import { createTextLabel, setTextLabelString } from '@flighthq/text';
|
|
3
|
+
import type { RenderProxy2D, TextLabel } from '@flighthq/types';
|
|
4
|
+
import { BatchFormat } from '@flighthq/types';
|
|
5
|
+
|
|
6
|
+
import { registerDefaultGlMaterial } from './glDefaultMaterial';
|
|
7
|
+
import { flushGlSpriteBatch } from './glSpriteBatch';
|
|
8
|
+
import { createGlState } from './glTestHelper';
|
|
9
|
+
import type * as GlTextLabelModule from './glTextLabel';
|
|
10
|
+
import { scopeModuleMocks } from './moduleMockTestHelper';
|
|
11
|
+
|
|
12
|
+
// @flighthq/textlayout.computeTextLayout is stubbed to emit one deterministic glyph group.
|
|
13
|
+
// scopeModuleMocks scopes the stub to this file (registry reset before the mock applies, unmock +
|
|
14
|
+
// reset after), so under a shared (isolate:false) worker it never leaks into the real text-layout
|
|
15
|
+
// consumers (text package) — and a sibling that pre-evaluated ./glTextLabel still picks up the stub.
|
|
16
|
+
let defaultGlTextLabelRenderer: typeof GlTextLabelModule.defaultGlTextLabelRenderer;
|
|
17
|
+
let drawGlTextLabel: typeof GlTextLabelModule.drawGlTextLabel;
|
|
18
|
+
|
|
19
|
+
scopeModuleMocks(['@flighthq/textlayout']);
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
vi.doMock('@flighthq/textlayout', async (importOriginal) => {
|
|
23
|
+
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
computeTextLayout: vi.fn((result: { groups: object[] }) => {
|
|
27
|
+
result.groups.push({
|
|
28
|
+
offsetX: 0,
|
|
29
|
+
offsetY: 0,
|
|
30
|
+
width: 50,
|
|
31
|
+
ascent: 12,
|
|
32
|
+
descent: 4,
|
|
33
|
+
format: {},
|
|
34
|
+
startIndex: 0,
|
|
35
|
+
endIndex: 5,
|
|
36
|
+
});
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
({ defaultGlTextLabelRenderer, drawGlTextLabel } = await import('./glTextLabel'));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function makeTextData() {
|
|
44
|
+
const canvas = document.createElement('canvas');
|
|
45
|
+
canvas.width = 1;
|
|
46
|
+
canvas.height = 1;
|
|
47
|
+
const ctx = canvas.getContext('2d')!;
|
|
48
|
+
return { canvas, ctx, lastHash: '', logW: 0, logH: 0 };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeTextProxy(text = '', rendererData: unknown = null): RenderProxy2D {
|
|
52
|
+
const source = createTextLabel();
|
|
53
|
+
source.data.text = text;
|
|
54
|
+
source.data.textFormat = {};
|
|
55
|
+
source.data.width = 200;
|
|
56
|
+
source.data.height = 100;
|
|
57
|
+
return {
|
|
58
|
+
source,
|
|
59
|
+
blendMode: 0,
|
|
60
|
+
alpha: 1,
|
|
61
|
+
material: null,
|
|
62
|
+
materialData: null,
|
|
63
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
64
|
+
rendererData,
|
|
65
|
+
} as unknown as RenderProxy2D;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('defaultGlTextLabelRenderer', () => {
|
|
69
|
+
it('declares BatchFormat.Quad', () => {
|
|
70
|
+
expect(defaultGlTextLabelRenderer.format).toBe(BatchFormat.Quad);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('has a createData function', () => {
|
|
74
|
+
expect(typeof defaultGlTextLabelRenderer.createData).toBe('function');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('has a submit function pointing to drawGlTextLabel', () => {
|
|
78
|
+
expect(defaultGlTextLabelRenderer.submit).toBe(drawGlTextLabel);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('drawGlTextLabel', () => {
|
|
83
|
+
it('returns early without writing to batch when text is empty', () => {
|
|
84
|
+
const { state } = createGlState();
|
|
85
|
+
registerDefaultGlMaterial(state);
|
|
86
|
+
drawGlTextLabel(state, makeTextProxy('', makeTextData()));
|
|
87
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns early without writing to batch when rendererData is null', () => {
|
|
91
|
+
const { state } = createGlState();
|
|
92
|
+
registerDefaultGlMaterial(state);
|
|
93
|
+
drawGlTextLabel(state, makeTextProxy('hello', null));
|
|
94
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns early without writing to batch when no material renderer is registered', () => {
|
|
98
|
+
const { state } = createGlState();
|
|
99
|
+
drawGlTextLabel(state, makeTextProxy('hello', makeTextData()));
|
|
100
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('writes one instance to the sprite batch when text has content', () => {
|
|
104
|
+
const { state } = createGlState();
|
|
105
|
+
registerDefaultGlMaterial(state);
|
|
106
|
+
drawGlTextLabel(state, makeTextProxy('hello', makeTextData()));
|
|
107
|
+
expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('draws via drawElementsInstanced after flush', () => {
|
|
111
|
+
const { state, gl } = createGlState();
|
|
112
|
+
registerDefaultGlMaterial(state);
|
|
113
|
+
drawGlTextLabel(state, makeTextProxy('hello', makeTextData()));
|
|
114
|
+
flushGlSpriteBatch(state);
|
|
115
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('skips layout and rasterization on repeated calls when the content version is unchanged', () => {
|
|
119
|
+
const { state } = createGlState();
|
|
120
|
+
registerDefaultGlMaterial(state);
|
|
121
|
+
const proxy = makeTextProxy('hello', makeTextData());
|
|
122
|
+
const deleteSpy = vi.spyOn(getGlRenderStateRuntime(state).textureCache, 'delete');
|
|
123
|
+
drawGlTextLabel(state, proxy);
|
|
124
|
+
drawGlTextLabel(state, proxy);
|
|
125
|
+
// textureCache.delete only happens during rasterization (first call); skipped on second.
|
|
126
|
+
expect(deleteSpy).toHaveBeenCalledTimes(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('re-rasterizes when the content version is bumped', () => {
|
|
130
|
+
const { state } = createGlState();
|
|
131
|
+
registerDefaultGlMaterial(state);
|
|
132
|
+
const proxy = makeTextProxy('hello', makeTextData());
|
|
133
|
+
const deleteSpy = vi.spyOn(getGlRenderStateRuntime(state).textureCache, 'delete');
|
|
134
|
+
drawGlTextLabel(state, proxy);
|
|
135
|
+
setTextLabelString(proxy.source as TextLabel, 'world');
|
|
136
|
+
drawGlTextLabel(state, proxy);
|
|
137
|
+
expect(deleteSpy).toHaveBeenCalledTimes(2);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('does not re-rasterize when only alpha changes (version unchanged)', () => {
|
|
141
|
+
const { state } = createGlState();
|
|
142
|
+
registerDefaultGlMaterial(state);
|
|
143
|
+
const proxy = makeTextProxy('hello', makeTextData());
|
|
144
|
+
const deleteSpy = vi.spyOn(getGlRenderStateRuntime(state).textureCache, 'delete');
|
|
145
|
+
drawGlTextLabel(state, proxy);
|
|
146
|
+
proxy.alpha = 0.5;
|
|
147
|
+
drawGlTextLabel(state, proxy);
|
|
148
|
+
// Alpha is applied per-instance in the batch; the expensive raster cache is untouched.
|
|
149
|
+
expect(deleteSpy).toHaveBeenCalledTimes(1);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { RenderProxy2D } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { registerDefaultGlMaterial } from './glDefaultMaterial';
|
|
4
|
+
import { flushGlSpriteBatch } from './glSpriteBatch';
|
|
5
|
+
import { createGlState } from './glTestHelper';
|
|
6
|
+
import { defaultGlTilemapRenderer } from './glTilemap';
|
|
7
|
+
|
|
8
|
+
function makeAtlas() {
|
|
9
|
+
const img = document.createElement('img');
|
|
10
|
+
return {
|
|
11
|
+
image: { source: img, width: 64, height: 64 },
|
|
12
|
+
regions: [{ x: 0, y: 0, width: 16, height: 16 }],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeTileset(atlasOverride?: unknown) {
|
|
17
|
+
return {
|
|
18
|
+
atlas: atlasOverride !== undefined ? atlasOverride : makeAtlas(),
|
|
19
|
+
tileWidth: 16,
|
|
20
|
+
tileHeight: 16,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeTilemapNode(data: Record<string, unknown> = {}): RenderProxy2D {
|
|
25
|
+
return {
|
|
26
|
+
source: {
|
|
27
|
+
data: {
|
|
28
|
+
tileset: makeTileset(),
|
|
29
|
+
columns: 2,
|
|
30
|
+
rows: 2,
|
|
31
|
+
tiles: [0, 0, 0, 0],
|
|
32
|
+
...data,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
blendMode: 0,
|
|
36
|
+
alpha: 1,
|
|
37
|
+
material: null,
|
|
38
|
+
materialData: null,
|
|
39
|
+
renderer: null,
|
|
40
|
+
traverseChildren: false,
|
|
41
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
42
|
+
} as unknown as RenderProxy2D;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('defaultGlTilemapRenderer', () => {
|
|
46
|
+
it('has a createData function', () => {
|
|
47
|
+
expect(typeof defaultGlTilemapRenderer.createData).toBe('function');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('has a submit function', () => {
|
|
51
|
+
expect(typeof defaultGlTilemapRenderer.submit).toBe('function');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('defaultGlTilemapRenderer.submit', () => {
|
|
56
|
+
it('returns early without drawing when tileset is null', () => {
|
|
57
|
+
const { state, gl } = createGlState();
|
|
58
|
+
registerDefaultGlMaterial(state);
|
|
59
|
+
defaultGlTilemapRenderer.submit(state, makeTilemapNode({ tileset: null }));
|
|
60
|
+
flushGlSpriteBatch(state);
|
|
61
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns early without drawing when atlas is null', () => {
|
|
65
|
+
const { state, gl } = createGlState();
|
|
66
|
+
registerDefaultGlMaterial(state);
|
|
67
|
+
defaultGlTilemapRenderer.submit(state, makeTilemapNode({ tileset: makeTileset(null) }));
|
|
68
|
+
flushGlSpriteBatch(state);
|
|
69
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('returns early without drawing when atlas.image is null', () => {
|
|
73
|
+
const { state, gl } = createGlState();
|
|
74
|
+
registerDefaultGlMaterial(state);
|
|
75
|
+
defaultGlTilemapRenderer.submit(
|
|
76
|
+
state,
|
|
77
|
+
makeTilemapNode({ tileset: { atlas: { image: null, regions: [] }, tileWidth: 16, tileHeight: 16 } }),
|
|
78
|
+
);
|
|
79
|
+
flushGlSpriteBatch(state);
|
|
80
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns early without drawing when atlas.image.source is null', () => {
|
|
84
|
+
const { state, gl } = createGlState();
|
|
85
|
+
registerDefaultGlMaterial(state);
|
|
86
|
+
defaultGlTilemapRenderer.submit(
|
|
87
|
+
state,
|
|
88
|
+
makeTilemapNode({ tileset: { atlas: { image: { source: null }, regions: [] }, tileWidth: 16, tileHeight: 16 } }),
|
|
89
|
+
);
|
|
90
|
+
flushGlSpriteBatch(state);
|
|
91
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns early without drawing when columns is 0', () => {
|
|
95
|
+
const { state, gl } = createGlState();
|
|
96
|
+
registerDefaultGlMaterial(state);
|
|
97
|
+
defaultGlTilemapRenderer.submit(state, makeTilemapNode({ columns: 0 }));
|
|
98
|
+
flushGlSpriteBatch(state);
|
|
99
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns early without drawing when rows is 0', () => {
|
|
103
|
+
const { state, gl } = createGlState();
|
|
104
|
+
registerDefaultGlMaterial(state);
|
|
105
|
+
defaultGlTilemapRenderer.submit(state, makeTilemapNode({ rows: 0 }));
|
|
106
|
+
flushGlSpriteBatch(state);
|
|
107
|
+
expect(gl.drawElementsInstanced).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('draws all valid tiles in a single instanced call', () => {
|
|
111
|
+
const { state, gl } = createGlState();
|
|
112
|
+
registerDefaultGlMaterial(state);
|
|
113
|
+
defaultGlTilemapRenderer.submit(state, makeTilemapNode());
|
|
114
|
+
flushGlSpriteBatch(state);
|
|
115
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
|
|
116
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledWith(expect.anything(), 6, expect.anything(), 0, 4);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('excludes out-of-range tile ids from the instanced draw count', () => {
|
|
120
|
+
const { state, gl } = createGlState();
|
|
121
|
+
registerDefaultGlMaterial(state);
|
|
122
|
+
defaultGlTilemapRenderer.submit(state, makeTilemapNode({ tiles: [0, 99, 99, 0] }));
|
|
123
|
+
flushGlSpriteBatch(state);
|
|
124
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledTimes(1);
|
|
125
|
+
expect(gl.drawElementsInstanced).toHaveBeenCalledWith(expect.anything(), 6, expect.anything(), 0, 2);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getGlMaterialRenderer } from '@flighthq/render-gl';
|
|
2
|
+
import { UniformColorTransformMaterialKind } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { createGlState } from './glTestHelper';
|
|
5
|
+
import {
|
|
6
|
+
registerGlUniformColorTransformMaterial,
|
|
7
|
+
uniformColorTransformGlMaterialRenderer,
|
|
8
|
+
} from './glUniformColorTransformMaterial';
|
|
9
|
+
|
|
10
|
+
describe('registerGlUniformColorTransformMaterial', () => {
|
|
11
|
+
it('registers the uniform color transform material renderer', () => {
|
|
12
|
+
const { state } = createGlState();
|
|
13
|
+
registerGlUniformColorTransformMaterial(state);
|
|
14
|
+
expect(getGlMaterialRenderer(state, UniformColorTransformMaterialKind)).toBe(
|
|
15
|
+
uniformColorTransformGlMaterialRenderer,
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('uniformColorTransformGlMaterialRenderer', () => {
|
|
21
|
+
it('declares no per-instance float data', () => {
|
|
22
|
+
expect(uniformColorTransformGlMaterialRenderer.instanceFloatCount).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createDisplayObject } from '@flighthq/displayobject';
|
|
2
|
+
import { createParticleEmitter, createQuadBatch, getQuadBatchRuntime, reserveParticleEmitter } from '@flighthq/sprite';
|
|
3
|
+
import type { QuadBatchRuntime, TextureAtlas, TextureAtlasRegion } from '@flighthq/types';
|
|
4
|
+
import { QuadBatchKind } from '@flighthq/types';
|
|
5
|
+
import { beginVelocityFrame, contributeVelocity, createVelocityField } from '@flighthq/velocity';
|
|
6
|
+
|
|
7
|
+
import { createGlState } from './glTestHelper';
|
|
8
|
+
import {
|
|
9
|
+
createGlVelocityTarget,
|
|
10
|
+
defaultGlDisplayObjectVelocityWriter,
|
|
11
|
+
defaultGlParticleEmitterVelocityWriter,
|
|
12
|
+
defaultGlQuadBatchVelocityWriter,
|
|
13
|
+
drawGlVelocityQuad,
|
|
14
|
+
getGlVelocityWriter,
|
|
15
|
+
registerGlVelocityWriter,
|
|
16
|
+
renderGlVelocity,
|
|
17
|
+
} from './glVelocity';
|
|
18
|
+
|
|
19
|
+
describe('createGlVelocityTarget', () => {
|
|
20
|
+
it('allocates an rgba16f target at the requested size', () => {
|
|
21
|
+
const { state } = createGlState();
|
|
22
|
+
const target = createGlVelocityTarget(state, 128, 64);
|
|
23
|
+
expect(target.format).toBe('rgba16f');
|
|
24
|
+
expect(target.width).toBe(128);
|
|
25
|
+
expect(target.height).toBe(64);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('defaultGlDisplayObjectVelocityWriter', () => {
|
|
30
|
+
it('is a velocity writer function', () => {
|
|
31
|
+
expect(typeof defaultGlDisplayObjectVelocityWriter).toBe('function');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('defaultGlParticleEmitterVelocityWriter', () => {
|
|
36
|
+
it('is a velocity writer function', () => {
|
|
37
|
+
expect(typeof defaultGlParticleEmitterVelocityWriter).toBe('function');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('emits per-particle velocity for an emitter with a velocities array without throwing', () => {
|
|
41
|
+
const { state } = createGlState();
|
|
42
|
+
const target = createGlVelocityTarget(state, 128, 64);
|
|
43
|
+
const region = { id: 0, x: 0, y: 0, width: 16, height: 16, pivotX: null, pivotY: null } as TextureAtlasRegion;
|
|
44
|
+
const atlas = { image: null, regions: [region] } as TextureAtlas;
|
|
45
|
+
const emitter = createParticleEmitter();
|
|
46
|
+
reserveParticleEmitter(emitter, 2);
|
|
47
|
+
emitter.data.atlas = atlas;
|
|
48
|
+
emitter.data.particleCount = 2;
|
|
49
|
+
emitter.data.ids[0] = 0;
|
|
50
|
+
emitter.data.ids[1] = 0;
|
|
51
|
+
emitter.data.transforms.set([10, 10, 0, 1, 40, 20, 0, 1]);
|
|
52
|
+
emitter.data.velocities.set([3, -2, -1, 4]);
|
|
53
|
+
|
|
54
|
+
registerGlVelocityWriter(state, emitter.kind, defaultGlParticleEmitterVelocityWriter);
|
|
55
|
+
const field = createVelocityField();
|
|
56
|
+
beginVelocityFrame(field);
|
|
57
|
+
|
|
58
|
+
expect(() => renderGlVelocity(state, emitter, field, target)).not.toThrow();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('defaultGlQuadBatchVelocityWriter', () => {
|
|
63
|
+
it('is a velocity writer function', () => {
|
|
64
|
+
expect(typeof defaultGlQuadBatchVelocityWriter).toBe('function');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('emits per-instance velocity for a batch with an instanceVelocities array without throwing', () => {
|
|
68
|
+
const { state } = createGlState();
|
|
69
|
+
const target = createGlVelocityTarget(state, 128, 64);
|
|
70
|
+
const region = { id: 0, x: 0, y: 0, width: 32, height: 32, pivotX: null, pivotY: null } as TextureAtlasRegion;
|
|
71
|
+
const atlas = { image: null, regions: [region] } as TextureAtlas;
|
|
72
|
+
// Two vector2 instances at distinct positions, each moving independently.
|
|
73
|
+
const batch = createQuadBatch({
|
|
74
|
+
data: {
|
|
75
|
+
atlas,
|
|
76
|
+
ids: new Uint16Array([0, 0]),
|
|
77
|
+
instanceCount: 2,
|
|
78
|
+
transforms: new Float32Array([0, 0, 40, 10]),
|
|
79
|
+
transformType: 'vector2',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
// The batch owns its per-instance velocity (NOT the VelocityField); whatever drives the quads fills it.
|
|
83
|
+
(getQuadBatchRuntime(batch) as QuadBatchRuntime).instanceVelocities = new Float32Array([3, -2, -1, 4]);
|
|
84
|
+
|
|
85
|
+
registerGlVelocityWriter(state, QuadBatchKind, defaultGlQuadBatchVelocityWriter);
|
|
86
|
+
const field = createVelocityField();
|
|
87
|
+
beginVelocityFrame(field);
|
|
88
|
+
|
|
89
|
+
expect(() => renderGlVelocity(state, batch, field, target)).not.toThrow();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('falls back to coarse batch velocity when no instanceVelocities array is present', () => {
|
|
93
|
+
const { state } = createGlState();
|
|
94
|
+
const target = createGlVelocityTarget(state, 128, 64);
|
|
95
|
+
const region = { id: 0, x: 0, y: 0, width: 32, height: 32, pivotX: null, pivotY: null } as TextureAtlasRegion;
|
|
96
|
+
const atlas = { image: null, regions: [region] } as TextureAtlas;
|
|
97
|
+
const batch = createQuadBatch({
|
|
98
|
+
data: { atlas, ids: new Uint16Array([0]), instanceCount: 1, transforms: new Float32Array([0, 0]) },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
registerGlVelocityWriter(state, QuadBatchKind, defaultGlQuadBatchVelocityWriter);
|
|
102
|
+
const field = createVelocityField();
|
|
103
|
+
beginVelocityFrame(field);
|
|
104
|
+
contributeVelocity(field, batch, 5, 1);
|
|
105
|
+
|
|
106
|
+
expect(() => renderGlVelocity(state, batch, field, target)).not.toThrow();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('drawGlVelocityQuad', () => {
|
|
111
|
+
it('is callable', () => {
|
|
112
|
+
expect(typeof drawGlVelocityQuad).toBe('function');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('getGlVelocityWriter', () => {
|
|
117
|
+
it('returns null for an unregistered kind', () => {
|
|
118
|
+
const { state } = createGlState();
|
|
119
|
+
expect(getGlVelocityWriter(state, 'unregistered')).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('registerGlVelocityWriter', () => {
|
|
124
|
+
it('registers a writer dispatched by kind', () => {
|
|
125
|
+
const { state } = createGlState();
|
|
126
|
+
const root = createDisplayObject();
|
|
127
|
+
registerGlVelocityWriter(state, root.kind, defaultGlDisplayObjectVelocityWriter);
|
|
128
|
+
expect(getGlVelocityWriter(state, root.kind)).toBe(defaultGlDisplayObjectVelocityWriter);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('renderGlVelocity', () => {
|
|
133
|
+
it('dispatches the registered writer for a moving node without throwing', () => {
|
|
134
|
+
const { state } = createGlState();
|
|
135
|
+
const target = createGlVelocityTarget(state, 128, 64);
|
|
136
|
+
const root = createDisplayObject();
|
|
137
|
+
registerGlVelocityWriter(state, root.kind, defaultGlDisplayObjectVelocityWriter);
|
|
138
|
+
|
|
139
|
+
const field = createVelocityField();
|
|
140
|
+
beginVelocityFrame(field);
|
|
141
|
+
contributeVelocity(field, root, 3, -2);
|
|
142
|
+
|
|
143
|
+
expect(() => renderGlVelocity(state, root, field, target)).not.toThrow();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('runs without throwing when no writer is registered', () => {
|
|
147
|
+
const { state } = createGlState();
|
|
148
|
+
const target = createGlVelocityTarget(state, 128, 64);
|
|
149
|
+
const root = createDisplayObject();
|
|
150
|
+
const field = createVelocityField();
|
|
151
|
+
beginVelocityFrame(field);
|
|
152
|
+
|
|
153
|
+
expect(() => renderGlVelocity(state, root, field, target)).not.toThrow();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getGlRenderStateRuntime } from '@flighthq/render-gl';
|
|
2
|
+
import type { RendererData, RenderProxy2D } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { createGlState } from './glTestHelper';
|
|
5
|
+
import { createGlVideoData, defaultGlVideoRenderer, destroyGlVideoData, drawGlVideo } from './glVideo';
|
|
6
|
+
|
|
7
|
+
function makeVideoNode(element: HTMLVideoElement | null = null): RenderProxy2D {
|
|
8
|
+
return {
|
|
9
|
+
source: { data: { source: element !== null ? { element } : null } },
|
|
10
|
+
blendMode: 0,
|
|
11
|
+
alpha: 1,
|
|
12
|
+
transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
|
|
13
|
+
rendererData: { lastElement: null },
|
|
14
|
+
} as unknown as RenderProxy2D;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('createGlVideoData', () => {
|
|
18
|
+
it('allocates per-node data with no element recorded yet', () => {
|
|
19
|
+
const { state } = createGlState();
|
|
20
|
+
const data = createGlVideoData(state, {} as never) as unknown as { lastElement: HTMLVideoElement | null };
|
|
21
|
+
expect(data.lastElement).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('defaultGlVideoRenderer', () => {
|
|
26
|
+
it('has submit, and createData functions', () => {
|
|
27
|
+
expect(typeof defaultGlVideoRenderer.submit).toBe('function');
|
|
28
|
+
expect(typeof defaultGlVideoRenderer.createData).toBe('function');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('destroyGlVideoData', () => {
|
|
33
|
+
it('deletes the cached GPU texture for the recorded element', () => {
|
|
34
|
+
const { state, gl } = createGlState();
|
|
35
|
+
const runtime = getGlRenderStateRuntime(state);
|
|
36
|
+
const element = document.createElement('video');
|
|
37
|
+
const texture = gl.createTexture();
|
|
38
|
+
runtime.textureCache.set(element, texture as WebGLTexture);
|
|
39
|
+
const deleteSpy = vi.spyOn(gl, 'deleteTexture');
|
|
40
|
+
destroyGlVideoData(state, { lastElement: element } as unknown as RendererData);
|
|
41
|
+
expect(deleteSpy).toHaveBeenCalledWith(texture);
|
|
42
|
+
expect(runtime.textureCache.has(element)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('is a no-op when no element was recorded', () => {
|
|
46
|
+
const { state, gl } = createGlState();
|
|
47
|
+
const deleteSpy = vi.spyOn(gl, 'deleteTexture');
|
|
48
|
+
destroyGlVideoData(state, { lastElement: null } as unknown as RendererData);
|
|
49
|
+
expect(deleteSpy).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('drawGlVideo', () => {
|
|
54
|
+
it('returns early when source is null', () => {
|
|
55
|
+
const { state, gl } = createGlState();
|
|
56
|
+
drawGlVideo(state, makeVideoNode(null));
|
|
57
|
+
expect(gl.drawElements).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns early when element has not loaded', () => {
|
|
61
|
+
const { state, gl } = createGlState();
|
|
62
|
+
drawGlVideo(state, makeVideoNode(document.createElement('video')));
|
|
63
|
+
expect(gl.drawElements).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
});
|