@solidtv/renderer 1.5.5 → 1.6.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.
Files changed (106) hide show
  1. package/dist/src/core/CoreNode.js +2 -2
  2. package/dist/src/core/CoreNode.js.map +1 -1
  3. package/dist/src/core/Stage.js +9 -1
  4. package/dist/src/core/Stage.js.map +1 -1
  5. package/dist/src/core/lib/WebGlContextWrapper.d.ts +17 -1
  6. package/dist/src/core/lib/WebGlContextWrapper.js +21 -0
  7. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  8. package/dist/src/core/lib/colorParser.d.ts +4 -1
  9. package/dist/src/core/lib/colorParser.js +16 -6
  10. package/dist/src/core/lib/colorParser.js.map +1 -1
  11. package/dist/src/core/lib/validateImageBitmap.js +7 -0
  12. package/dist/src/core/lib/validateImageBitmap.js.map +1 -1
  13. package/dist/src/core/platforms/web/WebPlatform.js +39 -2
  14. package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
  15. package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +4 -0
  16. package/dist/src/core/renderers/canvas/CanvasRenderer.js +28 -13
  17. package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
  18. package/dist/src/core/renderers/canvas/CanvasShaderNode.js +1 -0
  19. package/dist/src/core/renderers/canvas/CanvasShaderNode.js.map +1 -1
  20. package/dist/src/core/renderers/webgl/SdfRenderOp.js +2 -2
  21. package/dist/src/core/renderers/webgl/SdfRenderOp.js.map +1 -1
  22. package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +43 -0
  23. package/dist/src/core/renderers/webgl/WebGlRenderer.js +109 -10
  24. package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
  25. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +21 -13
  26. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
  27. package/dist/src/core/renderers/webgl/internal/ShaderUtils.d.ts +8 -3
  28. package/dist/src/core/renderers/webgl/internal/ShaderUtils.js +12 -11
  29. package/dist/src/core/renderers/webgl/internal/ShaderUtils.js.map +1 -1
  30. package/dist/src/core/shaders/canvas/Rounded.js +3 -3
  31. package/dist/src/core/shaders/canvas/Rounded.js.map +1 -1
  32. package/dist/src/core/shaders/canvas/RoundedWithShadow.js +3 -3
  33. package/dist/src/core/shaders/canvas/RoundedWithShadow.js.map +1 -1
  34. package/dist/src/core/shaders/canvas/utils/render.js +3 -3
  35. package/dist/src/core/shaders/canvas/utils/render.js.map +1 -1
  36. package/dist/src/core/shaders/webgl/Border.js +12 -11
  37. package/dist/src/core/shaders/webgl/Border.js.map +1 -1
  38. package/dist/src/core/shaders/webgl/HolePunch.js +12 -5
  39. package/dist/src/core/shaders/webgl/HolePunch.js.map +1 -1
  40. package/dist/src/core/shaders/webgl/LinearGradient.js +3 -19
  41. package/dist/src/core/shaders/webgl/LinearGradient.js.map +1 -1
  42. package/dist/src/core/shaders/webgl/RadialGradient.js +3 -20
  43. package/dist/src/core/shaders/webgl/RadialGradient.js.map +1 -1
  44. package/dist/src/core/shaders/webgl/RadialProgress.js +15 -23
  45. package/dist/src/core/shaders/webgl/RadialProgress.js.map +1 -1
  46. package/dist/src/core/shaders/webgl/RoundedWithBorder.js +35 -32
  47. package/dist/src/core/shaders/webgl/RoundedWithBorder.js.map +1 -1
  48. package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js +58 -45
  49. package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js.map +1 -1
  50. package/dist/src/core/shaders/webgl/RoundedWithShadow.js +15 -13
  51. package/dist/src/core/shaders/webgl/RoundedWithShadow.js.map +1 -1
  52. package/dist/src/core/shaders/webgl/Shadow.js +4 -5
  53. package/dist/src/core/shaders/webgl/Shadow.js.map +1 -1
  54. package/dist/src/core/text-rendering/SdfTextRenderer.js +23 -10
  55. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  56. package/dist/src/core/text-rendering/TextLayoutEngine.js +14 -2
  57. package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
  58. package/dist/src/main-api/Renderer.d.ts +20 -0
  59. package/dist/src/main-api/Renderer.js +6 -0
  60. package/dist/src/main-api/Renderer.js.map +1 -1
  61. package/dist/src/utils.js +1 -1
  62. package/dist/src/utils.js.map +1 -1
  63. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  64. package/package.json +1 -1
  65. package/src/core/CoreNode.ts +2 -2
  66. package/src/core/Stage.renderListUpdate.test.ts +73 -0
  67. package/src/core/Stage.ts +11 -1
  68. package/src/core/lib/WebGlContextWrapper.test.ts +23 -1
  69. package/src/core/lib/WebGlContextWrapper.ts +23 -1
  70. package/src/core/lib/colorParser.test.ts +46 -0
  71. package/src/core/lib/colorParser.ts +17 -6
  72. package/src/core/lib/validateImageBitmap.test.ts +15 -0
  73. package/src/core/lib/validateImageBitmap.ts +8 -0
  74. package/src/core/platforms/web/WebPlatform.loopError.test.ts +145 -0
  75. package/src/core/platforms/web/WebPlatform.ts +39 -2
  76. package/src/core/renderers/canvas/CanvasRenderer.test.ts +230 -0
  77. package/src/core/renderers/canvas/CanvasRenderer.ts +29 -15
  78. package/src/core/renderers/canvas/CanvasShaderNode.test.ts +73 -0
  79. package/src/core/renderers/canvas/CanvasShaderNode.ts +1 -0
  80. package/src/core/renderers/webgl/SdfRenderOp.ts +2 -2
  81. package/src/core/renderers/webgl/WebGlRenderer.reuseRenderOp.test.ts +122 -0
  82. package/src/core/renderers/webgl/WebGlRenderer.sdfBuffer.test.ts +302 -0
  83. package/src/core/renderers/webgl/WebGlRenderer.ts +151 -11
  84. package/src/core/renderers/webgl/WebGlShaderProgram.reuseRenderOp.test.ts +150 -0
  85. package/src/core/renderers/webgl/WebGlShaderProgram.ts +27 -15
  86. package/src/core/renderers/webgl/WebGlShaderProgram.uniformDedup.test.ts +4 -2
  87. package/src/core/renderers/webgl/internal/ShaderUtils.test.ts +124 -0
  88. package/src/core/renderers/webgl/internal/ShaderUtils.ts +14 -13
  89. package/src/core/shaders/canvas/Rounded.ts +3 -3
  90. package/src/core/shaders/canvas/RoundedWithShadow.ts +3 -3
  91. package/src/core/shaders/canvas/utils/render.ts +3 -3
  92. package/src/core/shaders/webgl/Border.ts +12 -11
  93. package/src/core/shaders/webgl/HolePunch.ts +12 -5
  94. package/src/core/shaders/webgl/LinearGradient.ts +3 -19
  95. package/src/core/shaders/webgl/RadialGradient.ts +3 -20
  96. package/src/core/shaders/webgl/RadialProgress.ts +15 -23
  97. package/src/core/shaders/webgl/RoundedWithBorder.ts +35 -32
  98. package/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts +58 -45
  99. package/src/core/shaders/webgl/RoundedWithShadow.ts +15 -13
  100. package/src/core/shaders/webgl/Shadow.ts +4 -5
  101. package/src/core/text-rendering/SdfTextRenderer.test.ts +177 -0
  102. package/src/core/text-rendering/SdfTextRenderer.ts +38 -10
  103. package/src/core/text-rendering/TextLayoutEngine.ts +15 -2
  104. package/src/core/text-rendering/tests/TextLayoutEngine.test.ts +99 -0
  105. package/src/main-api/Renderer.ts +28 -0
  106. package/src/utils.ts +1 -1
@@ -0,0 +1,150 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { WebGlShaderProgram } from './WebGlShaderProgram.js';
3
+
4
+ /**
5
+ * Tests for the batch test in WebGlShaderProgram.reuseRenderOp.
6
+ *
7
+ * The program instance is created without running the constructor (which
8
+ * compiles GLSL against a live GL context); only the fields reuseRenderOp
9
+ * touches are populated manually.
10
+ */
11
+
12
+ type ProgramFlags = {
13
+ useSystemAlpha?: boolean;
14
+ useSystemDimensions?: boolean;
15
+ useTimeValue?: boolean;
16
+ canBatch?: (node: unknown, op: unknown) => boolean;
17
+ };
18
+
19
+ const makeProgram = (flags: ProgramFlags = {}): WebGlShaderProgram => {
20
+ const program = Object.create(
21
+ WebGlShaderProgram.prototype,
22
+ ) as WebGlShaderProgram;
23
+ const p = program as unknown as Record<string, unknown>;
24
+ p['useSystemAlpha'] = flags.useSystemAlpha === true;
25
+ p['useSystemDimensions'] = flags.useSystemDimensions === true;
26
+ p['useTimeValue'] = flags.useTimeValue === true;
27
+ p['lifecycle'] = { canBatch: flags.canBatch, update: undefined };
28
+ return program;
29
+ };
30
+
31
+ type FakeShaderNode = {
32
+ uniforms: object;
33
+ resolvedProps: Record<string, unknown> | undefined;
34
+ };
35
+
36
+ const makeShaderNode = (
37
+ uniforms: object,
38
+ resolvedProps?: Record<string, unknown>,
39
+ ): FakeShaderNode => ({ uniforms, resolvedProps });
40
+
41
+ const makeNode = (
42
+ shader: FakeShaderNode,
43
+ overrides?: Record<string, unknown>,
44
+ ) => ({
45
+ worldAlpha: 1,
46
+ w: 200,
47
+ h: 100,
48
+ props: { shader },
49
+ ...overrides,
50
+ });
51
+
52
+ const makeOp = (shader: FakeShaderNode, overrides?: Record<string, unknown>) =>
53
+ ({
54
+ time: 0,
55
+ worldAlpha: 1,
56
+ w: 200,
57
+ h: 100,
58
+ shader,
59
+ ...overrides,
60
+ } as never);
61
+
62
+ describe('WebGlShaderProgram.reuseRenderOp', () => {
63
+ it('batches the same shader node without comparing props', () => {
64
+ const program = makeProgram();
65
+ const shader = makeShaderNode({}, { radius: 16 });
66
+
67
+ expect(
68
+ program.reuseRenderOp(makeNode(shader) as never, makeOp(shader)),
69
+ ).toBe(true);
70
+ });
71
+
72
+ it('batches distinct shader nodes that share a uniform collection', () => {
73
+ const program = makeProgram();
74
+ const uniforms = {};
75
+ // Distinct resolvedProps objects — reference-inequal props must not matter
76
+ // when the value-key cache handed both nodes the same collection.
77
+ const shaderA = makeShaderNode(uniforms, { radius: [16, 16, 16, 16] });
78
+ const shaderB = makeShaderNode(uniforms, { radius: [16, 16, 16, 16] });
79
+
80
+ expect(
81
+ program.reuseRenderOp(makeNode(shaderA) as never, makeOp(shaderB)),
82
+ ).toBe(true);
83
+ });
84
+
85
+ it('falls back to a prop-value compare for distinct collections', () => {
86
+ const program = makeProgram();
87
+ const shaderA = makeShaderNode({}, { radius: 16, gap: 2 });
88
+ const shaderB = makeShaderNode({}, { radius: 16, gap: 2 });
89
+ const shaderC = makeShaderNode({}, { radius: 16, gap: 4 });
90
+
91
+ expect(
92
+ program.reuseRenderOp(makeNode(shaderA) as never, makeOp(shaderB)),
93
+ ).toBe(true);
94
+ expect(
95
+ program.reuseRenderOp(makeNode(shaderA) as never, makeOp(shaderC)),
96
+ ).toBe(false);
97
+ });
98
+
99
+ it('rejects on system alpha mismatch before any shader compare', () => {
100
+ const program = makeProgram({ useSystemAlpha: true });
101
+ const shader = makeShaderNode({});
102
+
103
+ expect(
104
+ program.reuseRenderOp(
105
+ makeNode(shader, { worldAlpha: 0.5 }) as never,
106
+ makeOp(shader),
107
+ ),
108
+ ).toBe(false);
109
+ });
110
+
111
+ it('rejects on dimension mismatch when the program uses u_dimensions', () => {
112
+ const program = makeProgram({ useSystemDimensions: true });
113
+ const shader = makeShaderNode({});
114
+
115
+ expect(
116
+ program.reuseRenderOp(
117
+ makeNode(shader, { w: 300 }) as never,
118
+ makeOp(shader),
119
+ ),
120
+ ).toBe(false);
121
+ });
122
+
123
+ it('does not read node.time when the program has no time uniform', () => {
124
+ const program = makeProgram();
125
+ const shader = makeShaderNode({});
126
+ const node = makeNode(shader);
127
+ let timeReads = 0;
128
+ Object.defineProperty(node, 'time', {
129
+ get: () => {
130
+ timeReads++;
131
+ return 0;
132
+ },
133
+ });
134
+
135
+ program.reuseRenderOp(node as never, makeOp(shader));
136
+
137
+ expect(timeReads).toBe(0);
138
+ });
139
+
140
+ it('delegates to the shader type canBatch hook when defined', () => {
141
+ const canBatch = vi.fn(() => false);
142
+ const program = makeProgram({ canBatch });
143
+ const shader = makeShaderNode({});
144
+
145
+ expect(
146
+ program.reuseRenderOp(makeNode(shader) as never, makeOp(shader)),
147
+ ).toBe(false);
148
+ expect(canBatch.mock.calls.length).toBe(1);
149
+ });
150
+ });
@@ -158,40 +158,52 @@ export class WebGlShaderProgram implements CoreShaderProgram {
158
158
  return this.lifecycle.canBatch(node, currentRenderOp);
159
159
  }
160
160
 
161
- const { time, worldAlpha, w, h } = node;
162
-
161
+ // Read node getters only for the system uniforms this program actually
162
+ // uses — node.time in particular is a getter that runs per call.
163
163
  if (this.useTimeValue === true) {
164
- if (time !== currentRenderOp.time) {
164
+ if (node.time !== currentRenderOp.time) {
165
165
  return false;
166
166
  }
167
167
  }
168
168
 
169
169
  if (this.useSystemAlpha === true) {
170
- if (worldAlpha !== currentRenderOp.worldAlpha) {
170
+ if (node.worldAlpha !== currentRenderOp.worldAlpha) {
171
171
  return false;
172
172
  }
173
173
  }
174
174
 
175
175
  if (this.useSystemDimensions === true) {
176
- if (w !== currentRenderOp.w || h !== currentRenderOp.h) {
176
+ if (node.w !== currentRenderOp.w || node.h !== currentRenderOp.h) {
177
177
  return false;
178
178
  }
179
179
  }
180
180
 
181
- let shaderPropsA: Record<string, unknown> | undefined = undefined;
182
- let shaderPropsB: Record<string, unknown> | undefined = undefined;
181
+ const shader = node.props.shader as WebGlShaderNode | null;
182
+ const opShader = currentRenderOp.shader as WebGlShaderNode | null;
183
183
 
184
- const shader = node.props.shader;
184
+ // Same shader node — same resolved props by definition.
185
+ if (shader === opShader) {
186
+ return true;
187
+ }
185
188
 
186
- if (shader !== null) {
187
- shaderPropsA = (shader as WebGlShaderNode).resolvedProps;
189
+ if (shader === null || opShader === null) {
190
+ return false;
188
191
  }
189
192
 
190
- const opShader = currentRenderOp.shader;
191
- if (opShader !== null) {
192
- shaderPropsB = (opShader as WebGlShaderNode).resolvedProps;
193
+ // Shader nodes with equal value keys share their uniform collection by
194
+ // reference (see WebGlShaderNode.update); reference equality implies the
195
+ // resolved prop values match without a key-by-key compare.
196
+ if (shader.uniforms === opShader.uniforms) {
197
+ return true;
193
198
  }
194
199
 
200
+ const shaderPropsA = shader.resolvedProps as
201
+ | Record<string, unknown>
202
+ | undefined;
203
+ const shaderPropsB = opShader.resolvedProps as
204
+ | Record<string, unknown>
205
+ | undefined;
206
+
195
207
  if (
196
208
  (shaderPropsA === undefined && shaderPropsB !== undefined) ||
197
209
  (shaderPropsA !== undefined && shaderPropsB === undefined)
@@ -243,8 +255,8 @@ export class WebGlShaderProgram implements CoreShaderProgram {
243
255
  resolutionH = framebufferDimensions.h;
244
256
  } else {
245
257
  pixelRatio = renderOp.stage.pixelRatio;
246
- resolutionW = this.glw.canvas.width;
247
- resolutionH = this.glw.canvas.height;
258
+ resolutionW = this.glw.canvasW;
259
+ resolutionH = this.glw.canvasH;
248
260
  }
249
261
 
250
262
  if (pixelRatio !== this.lastPixelRatio) {
@@ -13,7 +13,8 @@ type FakeGlw = {
13
13
  uniform1f: ReturnType<typeof vi.fn>;
14
14
  uniform2f: ReturnType<typeof vi.fn>;
15
15
  uniform4f: ReturnType<typeof vi.fn>;
16
- canvas: { width: number; height: number };
16
+ canvasW: number;
17
+ canvasH: number;
17
18
  };
18
19
 
19
20
  const makeProgram = (): { program: WebGlShaderProgram; glw: FakeGlw } => {
@@ -21,7 +22,8 @@ const makeProgram = (): { program: WebGlShaderProgram; glw: FakeGlw } => {
21
22
  uniform1f: vi.fn(),
22
23
  uniform2f: vi.fn(),
23
24
  uniform4f: vi.fn(),
24
- canvas: { width: 1920, height: 1080 },
25
+ canvasW: 1920,
26
+ canvasH: 1080,
25
27
  };
26
28
 
27
29
  const program = Object.create(
@@ -0,0 +1,124 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { genGradientColors } from './ShaderUtils.js';
3
+
4
+ type Vec4 = [number, number, number, number];
5
+
6
+ const smoothstep = (e0: number, e1: number, x: number): number => {
7
+ const t = Math.min(Math.max((x - e0) / (e1 - e0), 0), 1);
8
+ return t * t * (3 - 2 * t);
9
+ };
10
+
11
+ const mix = (a: Vec4, b: Vec4, t: number): Vec4 => [
12
+ a[0] + (b[0] - a[0]) * t,
13
+ a[1] + (b[1] - a[1]) * t,
14
+ a[2] + (b[2] - a[2]) * t,
15
+ a[3] + (b[3] - a[3]) * t,
16
+ ];
17
+
18
+ /**
19
+ * Evaluate the generated GLSL statements as JS. The generated code only uses
20
+ * `mix`, `smoothstep`, `u_colors[i]`, `u_stops[i]` and `dist`, so shimming
21
+ * those makes the string directly executable.
22
+ */
23
+ function evalGradient(stops: number[], colors: Vec4[], dist: number): Vec4 {
24
+ const src = genGradientColors(stops.length).replace(
25
+ 'vec4 colorOut =',
26
+ 'let colorOut =',
27
+ );
28
+ const fn = new Function(
29
+ 'mix',
30
+ 'smoothstep',
31
+ 'u_stops',
32
+ 'u_colors',
33
+ 'dist',
34
+ `${src}; return colorOut;`,
35
+ );
36
+ return fn(mix, smoothstep, stops, colors, dist) as Vec4;
37
+ }
38
+
39
+ /**
40
+ * Reference implementation: the original branchy segment select
41
+ * (below first stop / above last stop / smoothstep within the segment).
42
+ */
43
+ function referenceGradient(
44
+ stops: number[],
45
+ colors: Vec4[],
46
+ dist: number,
47
+ ): Vec4 {
48
+ dist = Math.min(Math.max(dist, 0), 1);
49
+ if (dist <= stops[0]!) return colors[0]!;
50
+ const last = stops.length - 1;
51
+ if (dist >= stops[last]!) return colors[last]!;
52
+ for (let i = 0; i < last; i++) {
53
+ if (dist >= stops[i]! && dist <= stops[i + 1]!) {
54
+ return mix(
55
+ colors[i]!,
56
+ colors[i + 1]!,
57
+ smoothstep(stops[i]!, stops[i + 1]!, dist),
58
+ );
59
+ }
60
+ }
61
+ return colors[last]!;
62
+ }
63
+
64
+ const RED: Vec4 = [1, 0, 0, 1];
65
+ const GREEN: Vec4 = [0, 1, 0, 1];
66
+ const BLUE: Vec4 = [0, 0, 1, 0.5];
67
+ const WHITE: Vec4 = [1, 1, 1, 1];
68
+
69
+ describe('genGradientColors', () => {
70
+ it('emits no branches (no if / ternary / return)', () => {
71
+ const src = genGradientColors(4);
72
+ expect(src.includes('if')).toBe(false);
73
+ expect(src.includes('?')).toBe(false);
74
+ expect(src.includes('return')).toBe(false);
75
+ });
76
+
77
+ it('with a single stop resolves to the first color', () => {
78
+ const out = evalGradient([0], [RED], 0.7);
79
+ expect(out).toEqual(RED);
80
+ });
81
+
82
+ it('clamps to the first color below the first stop', () => {
83
+ const out = evalGradient([0.25, 0.75], [RED, GREEN], 0.1);
84
+ expect(out).toEqual(RED);
85
+ });
86
+
87
+ it('clamps to the last color above the last stop', () => {
88
+ const out = evalGradient([0.25, 0.75], [RED, GREEN], 0.9);
89
+ expect(out).toEqual(GREEN);
90
+ });
91
+
92
+ it('interpolates with smoothstep inside a segment', () => {
93
+ const dist = 0.5;
94
+ const out = evalGradient([0.25, 0.75], [RED, GREEN], dist);
95
+ const expected = mix(RED, GREEN, smoothstep(0.25, 0.75, dist));
96
+ for (let i = 0; i < 4; i++) {
97
+ expect(out[i]).toBeCloseTo(expected[i]!, 6);
98
+ }
99
+ });
100
+
101
+ it('matches the reference segment select across a multi-stop ramp', () => {
102
+ const stops = [0, 0.3, 0.6, 1];
103
+ const colors = [RED, GREEN, BLUE, WHITE];
104
+ for (let d = 0; d <= 100; d++) {
105
+ const dist = d / 100;
106
+ const out = evalGradient(stops, colors, dist);
107
+ const ref = referenceGradient(stops, colors, dist);
108
+ for (let i = 0; i < 4; i++) {
109
+ expect(out[i]).toBeCloseTo(ref[i]!, 6);
110
+ }
111
+ }
112
+ });
113
+
114
+ it('is exact at stop boundaries', () => {
115
+ const stops = [0, 0.5, 1];
116
+ const colors = [RED, GREEN, BLUE];
117
+ expect(evalGradient(stops, colors, 0)).toEqual(RED);
118
+ const mid = evalGradient(stops, colors, 0.5);
119
+ for (let i = 0; i < 4; i++) {
120
+ expect(mid[i]).toBeCloseTo(GREEN[i]!, 6);
121
+ }
122
+ expect(evalGradient(stops, colors, 1)).toEqual(BLUE);
123
+ });
124
+ });
@@ -243,21 +243,22 @@ export const DefaultVertexSource = `
243
243
  `;
244
244
 
245
245
  /**
246
- * generate fragment source for
247
- * @param stops
248
- * @returns
246
+ * Generates branchless gradient-stop evaluation statements for a fragment
247
+ * shader. Expects a `float dist` in scope and `u_stops`/`u_colors` uniform
248
+ * arrays of length `stops`; leaves the result in `vec4 colorOut`.
249
+ *
250
+ * The accumulated `mix()` chain is exactly equivalent to selecting the
251
+ * segment containing `dist` (for ascending stops): below a segment the
252
+ * smoothstep is 0 (no-op), above it is 1 (fully replaced by the next color).
253
+ * No `if`/ternary — Mali 400-class fragment pipelines serialize any branch.
249
254
  */
250
255
  export function genGradientColors(stops: number): string {
251
- let result = `
252
- float stopCalc = (dist - u_stops[0]) / (u_stops[1] - u_stops[0]);
253
- vec4 colorOut = mix(u_colors[0], u_colors[1], stopCalc);
254
- `;
255
- if (stops > 2) {
256
- for (let i = 2; i < stops; i++) {
257
- result += `colorOut = mix(colorOut, u_colors[${i}], clamp((dist - u_stops[${
258
- i - 1
259
- }]) / (u_stops[${i}] - u_stops[${i - 1}]), 0.0, 1.0));`;
260
- }
256
+ let result = `vec4 colorOut = u_colors[0];`;
257
+ for (let i = 1; i < stops; i++) {
258
+ result += `
259
+ colorOut = mix(colorOut, u_colors[${i}], smoothstep(u_stops[${
260
+ i - 1
261
+ }], u_stops[${i}], dist));`;
261
262
  }
262
263
  return result;
263
264
  }
@@ -22,16 +22,16 @@ export const Rounded: CanvasShaderType<RoundedProps, ComputedRoundedValues> = {
22
22
  );
23
23
  },
24
24
  render(ctx, node, renderContext) {
25
- const path = new Path2D();
25
+ ctx.beginPath();
26
26
  roundRect(
27
- path,
27
+ ctx,
28
28
  node.globalTransform!.tx,
29
29
  node.globalTransform!.ty,
30
30
  node.props.w,
31
31
  node.props.h,
32
32
  this.computed.radius!,
33
33
  );
34
- ctx.clip(path);
34
+ ctx.clip();
35
35
 
36
36
  renderContext();
37
37
  },
@@ -49,9 +49,9 @@ export const RoundedWithShadow: CanvasShaderType<
49
49
  );
50
50
  }
51
51
 
52
- const path = new Path2D();
53
- render.roundRect(path, tx, ty, w, h, computed.radius);
54
- ctx.clip(path);
52
+ ctx.beginPath();
53
+ render.roundRect(ctx, tx, ty, w, h, computed.radius);
54
+ ctx.clip();
55
55
  renderContext();
56
56
  },
57
57
  };
@@ -57,9 +57,9 @@ export function roundedRectWithBorder(
57
57
 
58
58
  //draw node content first
59
59
  ctx.save();
60
- const path = new Path2D();
61
- roundRect(path, x, y, width, height, radius);
62
- ctx.clip(path);
60
+ ctx.beginPath();
61
+ roundRect(ctx, x, y, width, height, radius);
62
+ ctx.clip();
63
63
  renderContext();
64
64
  ctx.restore();
65
65
 
@@ -135,24 +135,25 @@ export const Border: WebGlShaderType<BorderProps> = {
135
135
  float outerDist = box(boxUv + v_outerBorderUv, v_outerSize - edgeWidth);
136
136
  float innerDist = box(boxUv + v_innerBorderUv, v_innerSize - edgeWidth);
137
137
 
138
- if(u_borderGap == 0.0) {
139
- float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist);
140
- float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist);
141
- resultColor = mix(resultColor, u_borderColor, outerAlpha * u_borderColor.a);
142
- resultColor = mix(resultColor, color, innerAlpha);
143
- gl_FragColor = resultColor * u_alpha;
144
- return;
145
- }
138
+ float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist);
139
+ float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist);
146
140
 
147
141
  float nodeDist = box(boxUv, v_halfDimensions - edgeWidth);
148
142
  float nodeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, nodeDist);
149
143
 
150
144
  float borderDist = max(-innerDist, outerDist);
151
145
  float borderAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, borderDist);
152
- resultColor = mix(resultColor, color, nodeAlpha);
153
- resultColor = mix(resultColor, u_borderColor, borderAlpha * u_borderColor.a);
154
146
 
155
- gl_FragColor = resultColor * u_alpha;
147
+ // Branchless gap select -- Mali 400 serializes uniform branches, so both
148
+ // composites are computed and mix()-selected on hasGap instead.
149
+ vec4 resNoGap = mix(resultColor, u_borderColor, outerAlpha * u_borderColor.a);
150
+ resNoGap = mix(resNoGap, color, innerAlpha);
151
+
152
+ vec4 resGap = mix(resultColor, color, nodeAlpha);
153
+ resGap = mix(resGap, u_borderColor, borderAlpha * u_borderColor.a);
154
+
155
+ float hasGap = step(0.0001, abs(u_borderGap));
156
+ gl_FragColor = mix(resNoGap, resGap, hasGap) * u_alpha;
156
157
  }
157
158
  `,
158
159
  };
@@ -46,11 +46,18 @@ export const HolePunch: WebGlShaderType<HolePunchProps> = {
46
46
  void main() {
47
47
  vec4 color = texture2D(u_texture, v_textureCoords) * v_color;
48
48
  vec2 p = (v_textureCoords.xy * u_dimensions.xy - u_pos) - u_size;
49
- vec4 r = u_radius;
50
- r.xy = (p.x > 0.0) ? r.yz : r.xw;
51
- r.x = (p.y > 0.0) ? r.y : r.x;
52
- p = abs(p) - u_size + r.x;
53
- float dist = min(max(p.x, p.y), 0.0) + length(max(p, 0.0)) - r.x + 2.0;
49
+
50
+ // Branchless radius selection based on quadrant
51
+ // x: TL, y: TR, z: BR, w: BL
52
+ vec2 stepVal = step(vec2(0.0), p);
53
+ float r = mix(
54
+ mix(u_radius.x, u_radius.y, stepVal.x),
55
+ mix(u_radius.w, u_radius.z, stepVal.x),
56
+ stepVal.y
57
+ );
58
+
59
+ p = abs(p) - u_size + r;
60
+ float dist = min(max(p.x, p.y), 0.0) + length(max(p, 0.0)) - r + 2.0;
54
61
  float roundedAlpha = 1.0 - smoothstep(0.0, u_pixelRatio, dist);
55
62
  gl_FragColor = mix(color, vec4(0.0), min(color.a, roundedAlpha));
56
63
  }
@@ -6,6 +6,7 @@ import {
6
6
  } from '../templates/LinearGradientTemplate.js';
7
7
  import type { WebGlRenderer } from '../../renderers/webgl/WebGlRenderer.js';
8
8
  import type { WebGlShaderType } from '../../renderers/webgl/WebGlShaderNode.js';
9
+ import { genGradientColors } from '../../renderers/webgl/internal/ShaderUtils.js';
9
10
 
10
11
  export const LinearGradient: WebGlShaderType<LinearGradientProps> = {
11
12
  props: LinearGradientTemplate.props,
@@ -56,7 +57,6 @@ export const LinearGradient: WebGlShaderType<LinearGradientProps> = {
56
57
  # endif
57
58
 
58
59
  #define MAX_STOPS ${props.colors.length}
59
- #define LAST_STOP ${props.colors.length - 1}
60
60
 
61
61
  uniform float u_alpha;
62
62
 
@@ -72,24 +72,8 @@ export const LinearGradient: WebGlShaderType<LinearGradientProps> = {
72
72
 
73
73
  vec4 getGradientColor(float dist) {
74
74
  dist = clamp(dist, 0.0, 1.0);
75
-
76
- if(dist <= u_stops[0]) {
77
- return u_colors[0];
78
- }
79
-
80
- if(dist >= u_stops[LAST_STOP]) {
81
- return u_colors[LAST_STOP];
82
- }
83
-
84
- for(int i = 0; i < LAST_STOP; i++) {
85
- float left = u_stops[i];
86
- float right = u_stops[i + 1];
87
- if(dist >= left && dist <= right) {
88
- float lDist = smoothstep(left, right, dist);
89
- return mix(u_colors[i], u_colors[i + 1], lDist);
90
- }
91
- }
92
- return u_colors[LAST_STOP];
75
+ ${genGradientColors(props.colors.length)}
76
+ return colorOut;
93
77
  }
94
78
 
95
79
  void main() {
@@ -6,6 +6,7 @@ import {
6
6
  } from '../templates/RadialGradientTemplate.js';
7
7
  import type { WebGlShaderType } from '../../renderers/webgl/WebGlShaderNode.js';
8
8
  import type { WebGlRenderer } from '../../renderers/webgl/WebGlRenderer.js';
9
+ import { genGradientColors } from '../../renderers/webgl/internal/ShaderUtils.js';
9
10
 
10
11
  export const RadialGradient: WebGlShaderType<RadialGradientProps> = {
11
12
  props: RadialGradientTemplate.props,
@@ -37,7 +38,6 @@ export const RadialGradient: WebGlShaderType<RadialGradientProps> = {
37
38
  # endif
38
39
 
39
40
  #define MAX_STOPS ${props.colors.length}
40
- #define LAST_STOP ${props.colors.length - 1}
41
41
 
42
42
  uniform float u_alpha;
43
43
  uniform vec2 u_dimensions;
@@ -56,25 +56,8 @@ export const RadialGradient: WebGlShaderType<RadialGradientProps> = {
56
56
 
57
57
  vec4 getGradientColor(float dist) {
58
58
  dist = clamp(dist, 0.0, 1.0);
59
-
60
- if(dist <= u_stops[0]) {
61
- return u_colors[0];
62
- }
63
-
64
- if(dist >= u_stops[LAST_STOP]) {
65
- return u_colors[LAST_STOP];
66
- }
67
-
68
- for(int i = 0; i < LAST_STOP; i++) {
69
- float left = u_stops[i];
70
- float right = u_stops[i + 1];
71
- if(dist >= left && dist <= right) {
72
- float lDist = smoothstep(left, right, dist);
73
- return mix(u_colors[i], u_colors[i + 1], lDist);
74
- }
75
- }
76
-
77
- return u_colors[LAST_STOP];
59
+ ${genGradientColors(props.colors.length)}
60
+ return colorOut;
78
61
  }
79
62
 
80
63
  void main() {