@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.
- package/dist/src/core/CoreNode.js +2 -2
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/Stage.js +9 -1
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.d.ts +17 -1
- package/dist/src/core/lib/WebGlContextWrapper.js +21 -0
- package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
- package/dist/src/core/lib/colorParser.d.ts +4 -1
- package/dist/src/core/lib/colorParser.js +16 -6
- package/dist/src/core/lib/colorParser.js.map +1 -1
- package/dist/src/core/lib/validateImageBitmap.js +7 -0
- package/dist/src/core/lib/validateImageBitmap.js.map +1 -1
- package/dist/src/core/platforms/web/WebPlatform.js +39 -2
- package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +4 -0
- package/dist/src/core/renderers/canvas/CanvasRenderer.js +28 -13
- package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasShaderNode.js +1 -0
- package/dist/src/core/renderers/canvas/CanvasShaderNode.js.map +1 -1
- package/dist/src/core/renderers/webgl/SdfRenderOp.js +2 -2
- package/dist/src/core/renderers/webgl/SdfRenderOp.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +43 -0
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +109 -10
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +21 -13
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
- package/dist/src/core/renderers/webgl/internal/ShaderUtils.d.ts +8 -3
- package/dist/src/core/renderers/webgl/internal/ShaderUtils.js +12 -11
- package/dist/src/core/renderers/webgl/internal/ShaderUtils.js.map +1 -1
- package/dist/src/core/shaders/canvas/Rounded.js +3 -3
- package/dist/src/core/shaders/canvas/Rounded.js.map +1 -1
- package/dist/src/core/shaders/canvas/RoundedWithShadow.js +3 -3
- package/dist/src/core/shaders/canvas/RoundedWithShadow.js.map +1 -1
- package/dist/src/core/shaders/canvas/utils/render.js +3 -3
- package/dist/src/core/shaders/canvas/utils/render.js.map +1 -1
- package/dist/src/core/shaders/webgl/Border.js +12 -11
- package/dist/src/core/shaders/webgl/Border.js.map +1 -1
- package/dist/src/core/shaders/webgl/HolePunch.js +12 -5
- package/dist/src/core/shaders/webgl/HolePunch.js.map +1 -1
- package/dist/src/core/shaders/webgl/LinearGradient.js +3 -19
- package/dist/src/core/shaders/webgl/LinearGradient.js.map +1 -1
- package/dist/src/core/shaders/webgl/RadialGradient.js +3 -20
- package/dist/src/core/shaders/webgl/RadialGradient.js.map +1 -1
- package/dist/src/core/shaders/webgl/RadialProgress.js +15 -23
- package/dist/src/core/shaders/webgl/RadialProgress.js.map +1 -1
- package/dist/src/core/shaders/webgl/RoundedWithBorder.js +35 -32
- package/dist/src/core/shaders/webgl/RoundedWithBorder.js.map +1 -1
- package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js +58 -45
- package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js.map +1 -1
- package/dist/src/core/shaders/webgl/RoundedWithShadow.js +15 -13
- package/dist/src/core/shaders/webgl/RoundedWithShadow.js.map +1 -1
- package/dist/src/core/shaders/webgl/Shadow.js +4 -5
- package/dist/src/core/shaders/webgl/Shadow.js.map +1 -1
- package/dist/src/core/text-rendering/SdfTextRenderer.js +23 -10
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/TextLayoutEngine.js +14 -2
- package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
- package/dist/src/main-api/Renderer.d.ts +20 -0
- package/dist/src/main-api/Renderer.js +6 -0
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/src/utils.js +1 -1
- package/dist/src/utils.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/CoreNode.ts +2 -2
- package/src/core/Stage.renderListUpdate.test.ts +73 -0
- package/src/core/Stage.ts +11 -1
- package/src/core/lib/WebGlContextWrapper.test.ts +23 -1
- package/src/core/lib/WebGlContextWrapper.ts +23 -1
- package/src/core/lib/colorParser.test.ts +46 -0
- package/src/core/lib/colorParser.ts +17 -6
- package/src/core/lib/validateImageBitmap.test.ts +15 -0
- package/src/core/lib/validateImageBitmap.ts +8 -0
- package/src/core/platforms/web/WebPlatform.loopError.test.ts +145 -0
- package/src/core/platforms/web/WebPlatform.ts +39 -2
- package/src/core/renderers/canvas/CanvasRenderer.test.ts +230 -0
- package/src/core/renderers/canvas/CanvasRenderer.ts +29 -15
- package/src/core/renderers/canvas/CanvasShaderNode.test.ts +73 -0
- package/src/core/renderers/canvas/CanvasShaderNode.ts +1 -0
- package/src/core/renderers/webgl/SdfRenderOp.ts +2 -2
- package/src/core/renderers/webgl/WebGlRenderer.reuseRenderOp.test.ts +122 -0
- package/src/core/renderers/webgl/WebGlRenderer.sdfBuffer.test.ts +302 -0
- package/src/core/renderers/webgl/WebGlRenderer.ts +151 -11
- package/src/core/renderers/webgl/WebGlShaderProgram.reuseRenderOp.test.ts +150 -0
- package/src/core/renderers/webgl/WebGlShaderProgram.ts +27 -15
- package/src/core/renderers/webgl/WebGlShaderProgram.uniformDedup.test.ts +4 -2
- package/src/core/renderers/webgl/internal/ShaderUtils.test.ts +124 -0
- package/src/core/renderers/webgl/internal/ShaderUtils.ts +14 -13
- package/src/core/shaders/canvas/Rounded.ts +3 -3
- package/src/core/shaders/canvas/RoundedWithShadow.ts +3 -3
- package/src/core/shaders/canvas/utils/render.ts +3 -3
- package/src/core/shaders/webgl/Border.ts +12 -11
- package/src/core/shaders/webgl/HolePunch.ts +12 -5
- package/src/core/shaders/webgl/LinearGradient.ts +3 -19
- package/src/core/shaders/webgl/RadialGradient.ts +3 -20
- package/src/core/shaders/webgl/RadialProgress.ts +15 -23
- package/src/core/shaders/webgl/RoundedWithBorder.ts +35 -32
- package/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts +58 -45
- package/src/core/shaders/webgl/RoundedWithShadow.ts +15 -13
- package/src/core/shaders/webgl/Shadow.ts +4 -5
- package/src/core/text-rendering/SdfTextRenderer.test.ts +177 -0
- package/src/core/text-rendering/SdfTextRenderer.ts +38 -10
- package/src/core/text-rendering/TextLayoutEngine.ts +15 -2
- package/src/core/text-rendering/tests/TextLayoutEngine.test.ts +99 -0
- package/src/main-api/Renderer.ts +28 -0
- 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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
181
|
+
const shader = node.props.shader as WebGlShaderNode | null;
|
|
182
|
+
const opShader = currentRenderOp.shader as WebGlShaderNode | null;
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
// Same shader node — same resolved props by definition.
|
|
185
|
+
if (shader === opShader) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
185
188
|
|
|
186
|
-
if (shader
|
|
187
|
-
|
|
189
|
+
if (shader === null || opShader === null) {
|
|
190
|
+
return false;
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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.
|
|
247
|
-
resolutionH = this.glw.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
25
|
+
ctx.beginPath();
|
|
26
26
|
roundRect(
|
|
27
|
-
|
|
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(
|
|
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
|
-
|
|
53
|
-
render.roundRect(
|
|
54
|
-
ctx.clip(
|
|
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
|
-
|
|
61
|
-
roundRect(
|
|
62
|
-
ctx.clip(
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
float
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|