@flighthq/displayobject-gl 0.1.0 → 0.2.1-next.410.a23f189

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.
Files changed (80) hide show
  1. package/dist/enableGlColorAdjustmentGuards.d.ts +4 -0
  2. package/dist/enableGlColorAdjustmentGuards.d.ts.map +1 -0
  3. package/dist/enableGlColorAdjustmentGuards.js +21 -0
  4. package/dist/enableGlColorAdjustmentGuards.js.map +1 -0
  5. package/dist/glBitmap.d.ts.map +1 -1
  6. package/dist/glBitmap.js +17 -32
  7. package/dist/glBitmap.js.map +1 -1
  8. package/dist/glCache.d.ts.map +1 -1
  9. package/dist/glCache.js +11 -4
  10. package/dist/glCache.js.map +1 -1
  11. package/dist/glColorAdjustment.d.ts +3 -0
  12. package/dist/glColorAdjustment.d.ts.map +1 -0
  13. package/dist/glColorAdjustment.js +353 -0
  14. package/dist/glColorAdjustment.js.map +1 -0
  15. package/dist/glParticleEmitter.d.ts.map +1 -1
  16. package/dist/glParticleEmitter.js +4 -3
  17. package/dist/glParticleEmitter.js.map +1 -1
  18. package/dist/glQuadBatch.d.ts.map +1 -1
  19. package/dist/glQuadBatch.js +11 -7
  20. package/dist/glQuadBatch.js.map +1 -1
  21. package/dist/glRichText.js +3 -2
  22. package/dist/glRichText.js.map +1 -1
  23. package/dist/glShape.d.ts.map +1 -1
  24. package/dist/glShape.js +15 -11
  25. package/dist/glShape.js.map +1 -1
  26. package/dist/glShapeMesh.d.ts +10 -6
  27. package/dist/glShapeMesh.d.ts.map +1 -1
  28. package/dist/glShapeMesh.js +48 -29
  29. package/dist/glShapeMesh.js.map +1 -1
  30. package/dist/glSpriteBatch.d.ts +3 -3
  31. package/dist/glSpriteBatch.d.ts.map +1 -1
  32. package/dist/glSpriteBatch.js +43 -20
  33. package/dist/glSpriteBatch.js.map +1 -1
  34. package/dist/glSpriteRenderer.d.ts.map +1 -1
  35. package/dist/glSpriteRenderer.js +5 -3
  36. package/dist/glSpriteRenderer.js.map +1 -1
  37. package/dist/glTextInput.js +1 -1
  38. package/dist/glTextInput.js.map +1 -1
  39. package/dist/glTextLabel.d.ts.map +1 -1
  40. package/dist/glTextLabel.js +23 -12
  41. package/dist/glTextLabel.js.map +1 -1
  42. package/dist/glTilemap.d.ts.map +1 -1
  43. package/dist/glTilemap.js +11 -6
  44. package/dist/glTilemap.js.map +1 -1
  45. package/dist/glVelocity.js +5 -5
  46. package/dist/glVelocity.js.map +1 -1
  47. package/dist/glVideo.d.ts.map +1 -1
  48. package/dist/glVideo.js +34 -32
  49. package/dist/glVideo.js.map +1 -1
  50. package/dist/index.d.ts +2 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2 -3
  53. package/dist/index.js.map +1 -1
  54. package/package.json +24 -15
  55. package/src/enableGlColorAdjustmentGuards.test.ts +76 -0
  56. package/src/glBitmap.test.ts +11 -3
  57. package/src/glCache.test.ts +6 -2
  58. package/src/glColorAdjustment.test.ts +166 -0
  59. package/src/glRichText.test.ts +7 -2
  60. package/src/glShape.test.ts +2 -1
  61. package/src/glShapeMesh.test.ts +103 -3
  62. package/src/glSpriteBatch.test.ts +61 -3
  63. package/src/glTextLabel.test.ts +17 -12
  64. package/src/glVelocity.test.ts +2 -1
  65. package/src/glVideo.test.ts +47 -14
  66. package/dist/glColorTransformMaterial.d.ts +0 -4
  67. package/dist/glColorTransformMaterial.d.ts.map +0 -1
  68. package/dist/glColorTransformMaterial.js +0 -116
  69. package/dist/glColorTransformMaterial.js.map +0 -1
  70. package/dist/glMaterials.d.ts +0 -4
  71. package/dist/glMaterials.d.ts.map +0 -1
  72. package/dist/glMaterials.js +0 -97
  73. package/dist/glMaterials.js.map +0 -1
  74. package/dist/glUniformColorTransformMaterial.d.ts +0 -4
  75. package/dist/glUniformColorTransformMaterial.d.ts.map +0 -1
  76. package/dist/glUniformColorTransformMaterial.js +0 -56
  77. package/dist/glUniformColorTransformMaterial.js.map +0 -1
  78. package/src/glColorTransformMaterial.test.ts +0 -45
  79. package/src/glMaterials.test.ts +0 -91
  80. package/src/glUniformColorTransformMaterial.test.ts +0 -24
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@flighthq/displayobject-gl",
3
- "version": "0.1.0",
3
+ "version": "0.2.1-next.410.a23f189",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/flighthq/flight.git",
7
+ "directory": "packages/displayobject-gl"
8
+ },
4
9
  "type": "module",
5
10
  "main": "dist/index.js",
6
11
  "types": "dist/index.d.ts",
@@ -27,22 +32,26 @@
27
32
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
33
  },
29
34
  "dependencies": {
30
- "@flighthq/displayobject": "0.1.0",
31
- "@flighthq/displayobject-canvas": "0.1.0",
32
- "@flighthq/geometry": "0.1.0",
33
- "@flighthq/materials": "0.1.0",
34
- "@flighthq/node": "0.1.0",
35
- "@flighthq/path": "0.1.0",
36
- "@flighthq/render": "0.1.0",
37
- "@flighthq/render-gl": "0.1.0",
38
- "@flighthq/shape": "0.1.0",
39
- "@flighthq/text": "0.1.0",
40
- "@flighthq/textinput": "0.1.0",
41
- "@flighthq/textlayout": "0.1.0",
42
- "@flighthq/types": "0.1.0",
43
- "@flighthq/velocity": "0.1.0"
35
+ "@flighthq/color": "0.2.1-next.410.a23f189",
36
+ "@flighthq/displayobject": "0.2.1-next.410.a23f189",
37
+ "@flighthq/displayobject-canvas": "0.2.1-next.410.a23f189",
38
+ "@flighthq/geometry": "0.2.1-next.410.a23f189",
39
+ "@flighthq/image": "0.2.1-next.410.a23f189",
40
+ "@flighthq/log": "0.2.1-next.410.a23f189",
41
+ "@flighthq/node": "0.2.1-next.410.a23f189",
42
+ "@flighthq/path": "0.2.1-next.410.a23f189",
43
+ "@flighthq/render": "0.2.1-next.410.a23f189",
44
+ "@flighthq/render-gl": "0.2.1-next.410.a23f189",
45
+ "@flighthq/shape": "0.2.1-next.410.a23f189",
46
+ "@flighthq/text": "0.2.1-next.410.a23f189",
47
+ "@flighthq/textinput": "0.2.1-next.410.a23f189",
48
+ "@flighthq/textlayout": "0.2.1-next.410.a23f189",
49
+ "@flighthq/texture": "0.2.1-next.410.a23f189",
50
+ "@flighthq/types": "0.2.1-next.410.a23f189",
51
+ "@flighthq/velocity": "0.2.1-next.410.a23f189"
44
52
  },
45
53
  "devDependencies": {
54
+ "@flighthq/particleemitter": "*",
46
55
  "@flighthq/sprite": "*",
47
56
  "typescript": "^5.3.0"
48
57
  },
@@ -0,0 +1,76 @@
1
+ import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log';
2
+ import { getGlRenderStateRuntime } from '@flighthq/render-gl';
3
+ import type { ColorTransform } from '@flighthq/types';
4
+
5
+ import { areGlColorAdjustmentGuardsEnabled, enableGlColorAdjustmentGuards } from './enableGlColorAdjustmentGuards';
6
+ import { recordGlSpriteBatchColorTransform } from './glSpriteBatch';
7
+ import { createGlState } from './glTestHelper';
8
+
9
+ function ct(): ColorTransform {
10
+ return {
11
+ redMultiplier: 0.5,
12
+ greenMultiplier: 0.5,
13
+ blueMultiplier: 0.5,
14
+ alphaMultiplier: 1,
15
+ redOffset: 0,
16
+ greenOffset: 0,
17
+ blueOffset: 0,
18
+ alphaOffset: 0,
19
+ } as ColorTransform;
20
+ }
21
+
22
+ describe('areGlColorAdjustmentGuardsEnabled', () => {
23
+ it('reports false until guards are installed, then true', () => {
24
+ const { state } = createGlState();
25
+ expect(areGlColorAdjustmentGuardsEnabled(state)).toBe(false);
26
+ enableGlColorAdjustmentGuards(state);
27
+ expect(areGlColorAdjustmentGuardsEnabled(state)).toBe(true);
28
+ });
29
+ });
30
+
31
+ describe('enableGlColorAdjustmentGuards', () => {
32
+ it('warns once when a color transform is recorded but color adjustment was never enabled', () => {
33
+ const { state } = createGlState();
34
+ const sink = createMemoryLogSink(8);
35
+ addLogSink(sink.sink);
36
+ try {
37
+ enableGlColorAdjustmentGuards(state);
38
+ recordGlSpriteBatchColorTransform(state, ct(), 0);
39
+ const entries = getMemoryLogSinkEntries(sink);
40
+ expect(entries.length).toBe(1);
41
+ const data = entries[0].data as Record<string, unknown>;
42
+ expect(String(data.message)).toContain('enableGlColorAdjustment');
43
+ } finally {
44
+ removeLogSink(sink.sink);
45
+ }
46
+ });
47
+
48
+ it('stays silent for an untinted instance', () => {
49
+ const { state } = createGlState();
50
+ const sink = createMemoryLogSink(8);
51
+ addLogSink(sink.sink);
52
+ try {
53
+ enableGlColorAdjustmentGuards(state);
54
+ recordGlSpriteBatchColorTransform(state, null, 0);
55
+ expect(getMemoryLogSinkEntries(sink).length).toBe(0);
56
+ } finally {
57
+ removeLogSink(sink.sink);
58
+ }
59
+ });
60
+
61
+ it('does not warn when the guard slot is present but color adjustment is also enabled', () => {
62
+ const { state } = createGlState();
63
+ const runtime = getGlRenderStateRuntime(state);
64
+ const sink = createMemoryLogSink(8);
65
+ addLogSink(sink.sink);
66
+ try {
67
+ enableGlColorAdjustmentGuards(state);
68
+ // Simulate the fold being installed so the dispatcher never reaches the guard branch.
69
+ runtime.glColorAdjustmentFold = { drawShapeMeshes: () => {}, flush: () => false, record: () => {} };
70
+ recordGlSpriteBatchColorTransform(state, ct(), 0);
71
+ expect(getMemoryLogSinkEntries(sink).length).toBe(0);
72
+ } finally {
73
+ removeLogSink(sink.sink);
74
+ }
75
+ });
76
+ });
@@ -23,8 +23,8 @@ function makeRenderProxy(image: unknown = null, rendererData: unknown = makeBitm
23
23
  } as unknown as RenderProxy2D;
24
24
  }
25
25
 
26
- function makeImageResource(source: unknown = null, width = 32, height = 32) {
27
- return { source, width, height };
26
+ function makeImageResource(source: unknown = null, width = 32, height = 32, data: Uint8ClampedArray | null = null) {
27
+ return { source, data, compressed: null, width, height, version: 1, alphaType: 'straight' };
28
28
  }
29
29
 
30
30
  describe('defaultGlBitmapRenderer', () => {
@@ -53,13 +53,21 @@ describe('drawGlBitmap', () => {
53
53
  expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
54
54
  });
55
55
 
56
- it('returns early without writing to batch when image.source is null', () => {
56
+ it('returns early without writing to batch when the image has neither source nor data', () => {
57
57
  const { state } = createGlState();
58
58
  registerDefaultGlMaterial(state);
59
59
  drawGlBitmap(state, makeRenderProxy(makeImageResource(null)));
60
60
  expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(0);
61
61
  });
62
62
 
63
+ it('writes one instance for a data-only image (a generated Surface with no element)', () => {
64
+ const { state } = createGlState();
65
+ registerDefaultGlMaterial(state);
66
+ const data = new Uint8ClampedArray(32 * 32 * 4);
67
+ drawGlBitmap(state, makeRenderProxy(makeImageResource(null, 32, 32, data)));
68
+ expect(getGlRenderStateRuntime(state).spriteBatchCount).toBe(1);
69
+ });
70
+
63
71
  it('writes one instance to the sprite batch when image is valid', () => {
64
72
  const { state } = createGlState();
65
73
  registerDefaultGlMaterial(state);
@@ -47,7 +47,8 @@ beforeAll(async () => {
47
47
  const actual = await importOriginal<typeof GlRenderGlModule>();
48
48
  return {
49
49
  ...actual,
50
- beginGlRenderTarget: vi.fn(),
50
+ beginGlRenderPass: vi.fn(),
51
+ setGlRenderTransform2D: vi.fn(),
51
52
  createGlRenderTarget: vi.fn((_state: unknown, descriptor: { width: number; height: number }): GlRenderTarget => {
52
53
  const texture = {} as WebGLTexture;
53
54
  return {
@@ -59,6 +60,9 @@ beforeAll(async () => {
59
60
  colorRenderbuffers: [],
60
61
  depthStencilRenderbuffer: null,
61
62
  format: 'rgba8',
63
+ colorSpace: 'srgb',
64
+ clearColors: [],
65
+ clearDepth: 1,
62
66
  sampleCount: 1,
63
67
  width: descriptor.width,
64
68
  height: descriptor.height,
@@ -66,7 +70,7 @@ beforeAll(async () => {
66
70
  }),
67
71
  destroyGlRenderTarget: vi.fn(),
68
72
  drawGlRenderTargetResult: vi.fn(),
69
- endGlRenderTarget: vi.fn(),
73
+ endGlRenderPass: vi.fn(),
70
74
  resizeGlRenderTarget: vi.fn((_state: unknown, target: GlRenderTarget, width: number, height: number) => {
71
75
  target.width = width;
72
76
  target.height = height;
@@ -0,0 +1,166 @@
1
+ import { createImageResource } from '@flighthq/image';
2
+ import { getGlRenderStateRuntime } from '@flighthq/render-gl';
3
+ import type { ColorTransform, ImageResource } from '@flighthq/types';
4
+
5
+ import { enableGlColorAdjustment } from './glColorAdjustment';
6
+ import { defaultGlMaterialRenderer } from './glDefaultMaterial';
7
+ import { flushGlSpriteBatch, prepareGlSpriteBatchWrite, recordGlSpriteBatchColorTransform } from './glSpriteBatch';
8
+ import { createGlState } from './glTestHelper';
9
+
10
+ function makeTexture(): ImageResource {
11
+ return createImageResource(document.createElement('img'));
12
+ }
13
+
14
+ function ct(
15
+ redMultiplier = 1,
16
+ greenMultiplier = 1,
17
+ blueMultiplier = 1,
18
+ alphaMultiplier = 1,
19
+ redOffset = 0,
20
+ greenOffset = 0,
21
+ blueOffset = 0,
22
+ alphaOffset = 0,
23
+ ): ColorTransform {
24
+ return {
25
+ redMultiplier,
26
+ greenMultiplier,
27
+ blueMultiplier,
28
+ alphaMultiplier,
29
+ redOffset,
30
+ greenOffset,
31
+ blueOffset,
32
+ alphaOffset,
33
+ } as ColorTransform;
34
+ }
35
+
36
+ const CT_MODE_NONE = 0;
37
+ const CT_MODE_UNIFORM = 1;
38
+ const CT_MODE_PER_INSTANCE = 2;
39
+
40
+ describe('enableGlColorAdjustment', () => {
41
+ it('installs the fold so recorded tints drive the color-adjustment state machine', () => {
42
+ const { state } = createGlState();
43
+ const runtime = getGlRenderStateRuntime(state);
44
+ enableGlColorAdjustment(state);
45
+ expect(runtime.glColorAdjustmentFold).not.toBeNull();
46
+ expect(runtime.glColorAdjustmentFold).toBeDefined();
47
+ });
48
+
49
+ it('is idempotent', () => {
50
+ const { state } = createGlState();
51
+ const runtime = getGlRenderStateRuntime(state);
52
+ enableGlColorAdjustment(state);
53
+ const fold = runtime.glColorAdjustmentFold;
54
+ enableGlColorAdjustment(state);
55
+ expect(runtime.glColorAdjustmentFold).toBe(fold);
56
+ });
57
+
58
+ it('stays untinted (mode NONE) when no instance carries a color transform', () => {
59
+ const { state } = createGlState();
60
+ const runtime = getGlRenderStateRuntime(state);
61
+ enableGlColorAdjustment(state);
62
+ recordGlSpriteBatchColorTransform(state, null, 0);
63
+ recordGlSpriteBatchColorTransform(state, null, 1);
64
+ expect(runtime.spriteBatchColorTransformMode).toBe(CT_MODE_NONE);
65
+ });
66
+
67
+ it('uses one whole-batch uniform when every instance shares one tint', () => {
68
+ const { state } = createGlState();
69
+ const runtime = getGlRenderStateRuntime(state);
70
+ enableGlColorAdjustment(state);
71
+ const tint = ct(0.5);
72
+ recordGlSpriteBatchColorTransform(state, tint, 0);
73
+ recordGlSpriteBatchColorTransform(state, tint, 1);
74
+ recordGlSpriteBatchColorTransform(state, tint, 2);
75
+ expect(runtime.spriteBatchColorTransformMode).toBe(CT_MODE_UNIFORM);
76
+ expect(runtime.spriteBatchUniformColorTransform).toBe(tint);
77
+ });
78
+
79
+ it('keeps the uniform path for a distinct-but-equal tint', () => {
80
+ const { state } = createGlState();
81
+ const runtime = getGlRenderStateRuntime(state);
82
+ enableGlColorAdjustment(state);
83
+ recordGlSpriteBatchColorTransform(state, ct(0.5), 0);
84
+ recordGlSpriteBatchColorTransform(state, ct(0.5), 1);
85
+ expect(runtime.spriteBatchColorTransformMode).toBe(CT_MODE_UNIFORM);
86
+ });
87
+
88
+ it('promotes to per-instance (never splits) when tints diverge, back-filling the earlier tint', () => {
89
+ const { state } = createGlState();
90
+ const runtime = getGlRenderStateRuntime(state);
91
+ enableGlColorAdjustment(state);
92
+ recordGlSpriteBatchColorTransform(state, ct(0.5), 0);
93
+ recordGlSpriteBatchColorTransform(state, ct(0.25), 1);
94
+ expect(runtime.spriteBatchColorTransformMode).toBe(CT_MODE_PER_INSTANCE);
95
+ expect(runtime.spriteBatchColorTransformData![0]).toBe(0.5);
96
+ expect(runtime.spriteBatchColorTransformData![8]).toBe(0.25);
97
+ });
98
+
99
+ it('promotes with identity fill when a tinted instance follows an untinted one', () => {
100
+ const { state } = createGlState();
101
+ const runtime = getGlRenderStateRuntime(state);
102
+ enableGlColorAdjustment(state);
103
+ recordGlSpriteBatchColorTransform(state, null, 0);
104
+ recordGlSpriteBatchColorTransform(state, ct(0.5), 1);
105
+ expect(runtime.spriteBatchColorTransformMode).toBe(CT_MODE_PER_INSTANCE);
106
+ expect(runtime.spriteBatchColorTransformData![0]).toBe(1);
107
+ expect(runtime.spriteBatchColorTransformData![8]).toBe(0.5);
108
+ });
109
+
110
+ it('writes identity for an untinted instance once the batch is per-instance', () => {
111
+ const { state } = createGlState();
112
+ const runtime = getGlRenderStateRuntime(state);
113
+ enableGlColorAdjustment(state);
114
+ recordGlSpriteBatchColorTransform(state, ct(0.5), 0);
115
+ recordGlSpriteBatchColorTransform(state, ct(0.25), 1);
116
+ recordGlSpriteBatchColorTransform(state, null, 2);
117
+ expect(runtime.spriteBatchColorTransformData![16]).toBe(1);
118
+ expect(runtime.spriteBatchColorTransformData![20]).toBe(0);
119
+ });
120
+
121
+ it('normalizes color offsets by 255', () => {
122
+ const { state } = createGlState();
123
+ const runtime = getGlRenderStateRuntime(state);
124
+ enableGlColorAdjustment(state);
125
+ recordGlSpriteBatchColorTransform(state, ct(1, 1, 1, 1, 255, 0, 0, 0), 0);
126
+ recordGlSpriteBatchColorTransform(state, ct(0.5), 1);
127
+ expect(runtime.spriteBatchColorTransformData![4]).toBe(1);
128
+ });
129
+
130
+ it('drives the uniform color-transform shader on flush', () => {
131
+ const { state, gl } = createGlState();
132
+ const runtime = getGlRenderStateRuntime(state);
133
+ enableGlColorAdjustment(state);
134
+ prepareGlSpriteBatchWrite(state, makeTexture(), null, null, defaultGlMaterialRenderer, 1);
135
+ recordGlSpriteBatchColorTransform(state, ct(0.5), 0);
136
+ runtime.spriteBatchCount = 1;
137
+ flushGlSpriteBatch(state);
138
+ expect(gl.uniform4f).toHaveBeenCalled();
139
+ expect(gl.drawElementsInstanced).toHaveBeenCalled();
140
+ });
141
+
142
+ it('uploads a per-instance color-transform buffer on flush when tints vary', () => {
143
+ const { state, gl } = createGlState();
144
+ const runtime = getGlRenderStateRuntime(state);
145
+ enableGlColorAdjustment(state);
146
+ prepareGlSpriteBatchWrite(state, makeTexture(), null, null, defaultGlMaterialRenderer, 2);
147
+ recordGlSpriteBatchColorTransform(state, ct(0.5), 0);
148
+ recordGlSpriteBatchColorTransform(state, ct(0.25), 1);
149
+ runtime.spriteBatchCount = 2;
150
+ flushGlSpriteBatch(state);
151
+ expect(runtime.spriteBatchColorTransformBuffer).not.toBeNull();
152
+ expect(gl.drawElementsInstanced).toHaveBeenCalledWith(expect.anything(), 6, expect.anything(), 0, 2);
153
+ });
154
+
155
+ it('leaves the lean base shader untouched for an untinted batch on flush', () => {
156
+ const { state, gl } = createGlState();
157
+ const runtime = getGlRenderStateRuntime(state);
158
+ enableGlColorAdjustment(state);
159
+ prepareGlSpriteBatchWrite(state, makeTexture(), null, null, defaultGlMaterialRenderer, 1);
160
+ recordGlSpriteBatchColorTransform(state, null, 0);
161
+ runtime.spriteBatchCount = 1;
162
+ flushGlSpriteBatch(state);
163
+ expect(runtime.spriteBatchColorTransformBuffer ?? null).toBeNull();
164
+ expect(gl.drawElementsInstanced).toHaveBeenCalled();
165
+ });
166
+ });
@@ -85,10 +85,15 @@ describe('drawGlRichText', () => {
85
85
  expect(gl.drawElements).toHaveBeenCalled();
86
86
  });
87
87
 
88
- it('draws resolved htmlText spans', () => {
88
+ it('draws resolved multi-format spans', () => {
89
89
  const { state, gl } = createGlState();
90
90
  const renderProxy = makeRichTextNode();
91
- (renderProxy.source as RichText).data.htmlText = '<b>Bold</b><font color="#00ff00">Green</font>';
91
+ const node = renderProxy.source as RichText;
92
+ node.data.text = 'BoldGreen';
93
+ node.data.textFormatRanges = [
94
+ { start: 0, end: 4, format: { bold: true } },
95
+ { start: 4, end: 9, format: { color: 0x00ff00 } },
96
+ ];
92
97
  drawGlRichText(state, renderProxy);
93
98
  expect(gl.drawElements).toHaveBeenCalled();
94
99
  });
@@ -1,3 +1,4 @@
1
+ import { createImageResource } from '@flighthq/image';
1
2
  import type * as FlightNodeModule from '@flighthq/node';
2
3
  import { getGlRenderStateRuntime } from '@flighthq/render-gl';
3
4
  import type { RenderProxy2D } from '@flighthq/types';
@@ -33,7 +34,7 @@ function makeShapeData() {
33
34
  canvas.width = 1;
34
35
  canvas.height = 1;
35
36
  const ctx = canvas.getContext('2d')!;
36
- return { canvas, ctx, lastContentId: -1, lastW: 0, lastH: 0 };
37
+ return { canvas, ctx, image: createImageResource(canvas), lastContentId: -1, lastW: 0, lastH: 0 };
37
38
  }
38
39
 
39
40
  function makeShapeNode(data: Record<string, unknown> = {}, rendererData: unknown = null): RenderProxy2D {
@@ -1,17 +1,42 @@
1
1
  import { getGlRenderStateRuntime } from '@flighthq/render-gl';
2
- import type { RenderProxy2D } from '@flighthq/types';
2
+ import type { ColorTransform, RenderProxy2D } from '@flighthq/types';
3
3
 
4
- import { drawGlShapeMeshes } from './glShapeMesh';
4
+ import { enableGlColorAdjustment } from './glColorAdjustment';
5
+ import { drawGlShapeMeshBatch, drawGlShapeMeshes, ensureGlShapeMeshProgram } from './glShapeMesh';
5
6
  import { createGlState } from './glTestHelper';
6
7
 
7
- function makeProxy(): RenderProxy2D {
8
+ function makeProxy(overrides?: Partial<RenderProxy2D>): RenderProxy2D {
8
9
  return {
9
10
  alpha: 1,
10
11
  blendMode: null,
12
+ colorTransform: null,
11
13
  transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
14
+ ...overrides,
12
15
  } as unknown as RenderProxy2D;
13
16
  }
14
17
 
18
+ function ct(
19
+ redMultiplier = 1,
20
+ greenMultiplier = 1,
21
+ blueMultiplier = 1,
22
+ alphaMultiplier = 1,
23
+ redOffset = 0,
24
+ greenOffset = 0,
25
+ blueOffset = 0,
26
+ alphaOffset = 0,
27
+ ): ColorTransform {
28
+ return {
29
+ redMultiplier,
30
+ greenMultiplier,
31
+ blueMultiplier,
32
+ alphaMultiplier,
33
+ redOffset,
34
+ greenOffset,
35
+ blueOffset,
36
+ alphaOffset,
37
+ } as ColorTransform;
38
+ }
39
+
15
40
  const TRIANGLE = {
16
41
  vertices: new Float32Array([0, 0, 10, 0, 0, 10]),
17
42
  indices: new Uint16Array([0, 1, 2]),
@@ -19,6 +44,25 @@ const TRIANGLE = {
19
44
  alpha: 1,
20
45
  };
21
46
 
47
+ describe('drawGlShapeMeshBatch', () => {
48
+ it('runs onProgramBound after the matrix upload and before the draw loop', () => {
49
+ const { state, gl } = createGlState();
50
+ const calls: string[] = [];
51
+ (gl.uniformMatrix3fv as ReturnType<typeof vi.fn>).mockImplementation(() => calls.push('matrix'));
52
+ (gl.drawElements as ReturnType<typeof vi.fn>).mockImplementation(() => calls.push('draw'));
53
+
54
+ drawGlShapeMeshBatch(state, makeProxy(), [TRIANGLE], ensureGlShapeMeshProgram(state), () => calls.push('bound'));
55
+
56
+ expect(calls).toEqual(['matrix', 'bound', 'draw']);
57
+ });
58
+
59
+ it('is a no-op for an empty mesh list', () => {
60
+ const { state, gl } = createGlState();
61
+ drawGlShapeMeshBatch(state, makeProxy(), [], ensureGlShapeMeshProgram(state));
62
+ expect(gl.useProgram).not.toHaveBeenCalled();
63
+ });
64
+ });
65
+
22
66
  describe('drawGlShapeMeshes', () => {
23
67
  it('binds the mesh program and draws each mesh', () => {
24
68
  const { state, gl } = createGlState();
@@ -61,4 +105,60 @@ describe('drawGlShapeMeshes', () => {
61
105
 
62
106
  expect(gl.useProgram).not.toHaveBeenCalled();
63
107
  });
108
+
109
+ it('ignores a color transform when color adjustment is not enabled (default path pays nothing)', () => {
110
+ const { state } = createGlState();
111
+
112
+ drawGlShapeMeshes(state, makeProxy({ colorTransform: ct(0.5) }), [TRIANGLE]);
113
+
114
+ // The tinted program is compiled only through the opt-in fold; the base path never touches it.
115
+ expect(getGlRenderStateRuntime(state).shapeMeshColorTransformShader).toBeUndefined();
116
+ });
117
+
118
+ it('tints solid-fill meshes through the fold with the same uniforms as the quad-batch path', () => {
119
+ const { state, gl } = createGlState();
120
+ enableGlColorAdjustment(state);
121
+
122
+ // White fill, half-brightness multiplier, +128 red offset — mirrors the Path-B uniform upload.
123
+ drawGlShapeMeshes(state, makeProxy({ colorTransform: ct(0.5, 0.5, 0.5, 1, 128, 0, 0, 0) }), [
124
+ { ...TRIANGLE, color: 0xffffff, alpha: 1 },
125
+ ]);
126
+
127
+ const shader = getGlRenderStateRuntime(state).shapeMeshColorTransformShader!;
128
+ expect(shader).toBeDefined();
129
+ // Multiplier uploaded verbatim; offsets normalized by 255 — identical to glColorAdjustment's
130
+ // bindGlSpriteBatchUniformColorTransform.
131
+ expect(gl.uniform4f).toHaveBeenCalledWith(shader.colorMultiplierLocation, 0.5, 0.5, 0.5, 1);
132
+ expect(gl.uniform4f).toHaveBeenCalledWith(shader.colorOffsetLocation, 128 / 255, 0, 0, 0);
133
+ // Flat mesh color still uploaded premultiplied (white, alpha 1); the shader un/re-premultiplies.
134
+ expect(gl.uniform4f).toHaveBeenCalledWith(shader.colorLocation, 1, 1, 1, 1);
135
+ expect(gl.drawElements).toHaveBeenCalled();
136
+ });
137
+
138
+ it('falls back to the lean path through the fold when the node carries no transform', () => {
139
+ const { state } = createGlState();
140
+ enableGlColorAdjustment(state);
141
+
142
+ drawGlShapeMeshes(state, makeProxy({ colorTransform: null }), [TRIANGLE]);
143
+
144
+ // No transform → the fold is not consulted (gated on non-null), so no tint shader is compiled.
145
+ expect(getGlRenderStateRuntime(state).shapeMeshColorTransformShader).toBeUndefined();
146
+ });
147
+ });
148
+
149
+ describe('ensureGlShapeMeshProgram', () => {
150
+ it('compiles once and caches the binding per context', () => {
151
+ const { state } = createGlState();
152
+ const first = ensureGlShapeMeshProgram(state);
153
+ const second = ensureGlShapeMeshProgram(state);
154
+ expect(second).toBe(first);
155
+ });
156
+
157
+ it('exposes shared vertex and index buffers for the draw driver', () => {
158
+ const { state } = createGlState();
159
+ const binding = ensureGlShapeMeshProgram(state);
160
+ expect(binding.vertexBuffer).toBeDefined();
161
+ expect(binding.indexBuffer).toBeDefined();
162
+ expect(binding.vertexBuffer).not.toBe(binding.indexBuffer);
163
+ });
64
164
  });
@@ -1,6 +1,8 @@
1
+ import { createImageResource } from '@flighthq/image';
1
2
  import { getGlRenderStateRuntime } from '@flighthq/render-gl';
2
- import type { Material } from '@flighthq/types';
3
+ import type { ColorTransform, ImageResource, Material } from '@flighthq/types';
3
4
 
5
+ import { enableGlColorAdjustment } from './glColorAdjustment';
4
6
  import { defaultGlMaterialRenderer } from './glDefaultMaterial';
5
7
  import {
6
8
  bindGlQuadBatchBaseAttributes,
@@ -8,19 +10,45 @@ import {
8
10
  flushGlSpriteBatch,
9
11
  packGlSpriteBatchMaterialInstance,
10
12
  prepareGlSpriteBatchWrite,
13
+ recordGlSpriteBatchColorTransform,
11
14
  setGlQuadBatchWorldAndTexture,
12
15
  useGlQuadBatchProgram,
13
16
  } from './glSpriteBatch';
14
17
  import { createGlState } from './glTestHelper';
15
18
 
16
- function makeTexture(): HTMLImageElement {
17
- return document.createElement('img');
19
+ function makeTexture(): ImageResource {
20
+ return createImageResource(document.createElement('img'));
18
21
  }
19
22
 
20
23
  function makeMaterial(): Material {
21
24
  return { kind: 'TestMaterial' } as Material;
22
25
  }
23
26
 
27
+ function ct(
28
+ redMultiplier = 1,
29
+ greenMultiplier = 1,
30
+ blueMultiplier = 1,
31
+ alphaMultiplier = 1,
32
+ redOffset = 0,
33
+ greenOffset = 0,
34
+ blueOffset = 0,
35
+ alphaOffset = 0,
36
+ ): ColorTransform {
37
+ return {
38
+ redMultiplier,
39
+ greenMultiplier,
40
+ blueMultiplier,
41
+ alphaMultiplier,
42
+ redOffset,
43
+ greenOffset,
44
+ blueOffset,
45
+ alphaOffset,
46
+ } as ColorTransform;
47
+ }
48
+
49
+ const CT_MODE_NONE = 0;
50
+ const CT_MODE_UNIFORM = 1;
51
+
24
52
  describe('bindGlQuadBatchBaseAttributes', () => {
25
53
  it('sets up the corner and base instance attribute pointers', () => {
26
54
  const { state, gl } = createGlState();
@@ -133,6 +161,36 @@ describe('prepareGlSpriteBatchWrite', () => {
133
161
  });
134
162
  });
135
163
 
164
+ describe('recordGlSpriteBatchColorTransform', () => {
165
+ it('skips the tint (draws untinted) and records no fold state when color adjustment is not enabled', () => {
166
+ const { state, gl } = createGlState();
167
+ const runtime = getGlRenderStateRuntime(state);
168
+ prepareGlSpriteBatchWrite(state, makeTexture(), null, null, defaultGlMaterialRenderer, 1);
169
+ recordGlSpriteBatchColorTransform(state, ct(0.5), 0);
170
+ runtime.spriteBatchCount = 1;
171
+ // No fold installed → the CT mode stays uninitialized and no CT program is bound.
172
+ expect(runtime.spriteBatchColorTransformMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
173
+ flushGlSpriteBatch(state);
174
+ expect(gl.uniform4f).not.toHaveBeenCalled();
175
+ expect(gl.drawElementsInstanced).toHaveBeenCalled();
176
+ });
177
+
178
+ it('is a no-op for an untinted instance whether or not the fold is enabled', () => {
179
+ const { state } = createGlState();
180
+ const runtime = getGlRenderStateRuntime(state);
181
+ expect(() => recordGlSpriteBatchColorTransform(state, null, 0)).not.toThrow();
182
+ expect(runtime.spriteBatchColorTransformMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
183
+ });
184
+
185
+ it('delegates to the installed fold when color adjustment is enabled', () => {
186
+ const { state } = createGlState();
187
+ const runtime = getGlRenderStateRuntime(state);
188
+ enableGlColorAdjustment(state);
189
+ recordGlSpriteBatchColorTransform(state, ct(0.5), 0);
190
+ expect(runtime.spriteBatchColorTransformMode).toBe(CT_MODE_UNIFORM);
191
+ });
192
+ });
193
+
136
194
  describe('setGlQuadBatchWorldAndTexture', () => {
137
195
  it('uploads the world matrix and texture unit', () => {
138
196
  const { state, gl } = createGlState();