Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions examples/tests/shader-border-zero.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { ExampleSettings } from '../common/ExampleSettings.js';

export async function automation(settings: ExampleSettings) {
// Snapshot single page
await test(settings);
await settings.snapshot();
}

/**
* Degenerate-prop paths of the border/shadow shaders: zero-width borders and
* fully transparent shadow colors. These paths are selected branchlessly in
* the fragment shaders (Mali 400 target), so this locks in that a zero border
* renders exactly like plain Rounded and a zero-alpha shadow renders nothing.
*/
export default async function test({ renderer, testRoot }: ExampleSettings) {
const node = renderer.createNode({
x: 0,
y: 0,
w: 1920,
h: 1080,
color: 0xffffffff,
parent: testRoot,
});

// Zero-width border: must render identical to plain Rounded
renderer.createNode({
x: 300,
y: 300,
mount: 0.5,
w: 250,
h: 250,
color: 0xff00ffff,
shader: renderer.createShader('RoundedWithBorder', {
radius: 30,
'border-w': 0,
'border-color': 0xff0000ff,
}),
parent: node,
});

// Zero-width border with a shadow: shadow hugs the node box
renderer.createNode({
x: 700,
y: 300,
mount: 0.5,
w: 250,
h: 250,
color: 0xff00ffff,
shader: renderer.createShader('RoundedWithBorderAndShadow', {
radius: 30,
'border-w': 0,
'border-color': 0xff0000ff,
'shadow-x': 50,
'shadow-spread': 50,
'shadow-blur': 100,
}),
parent: node,
});

// Zero-alpha shadow color: no shadow may appear
renderer.createNode({
x: 1100,
y: 300,
mount: 0.5,
w: 250,
h: 250,
color: 0xff00ffff,
shader: renderer.createShader('Shadow', {
x: 50,
spread: 50,
blur: 100,
color: 0x00000000,
}),
parent: node,
});

// Zero-alpha rounded shadow color: plain rounded corners, no shadow
renderer.createNode({
x: 1500,
y: 300,
mount: 0.5,
w: 250,
h: 250,
color: 0xff00ffff,
shader: renderer.createShader('RoundedWithShadow', {
radius: 30,
'shadow-x': 50,
'shadow-spread': 50,
'shadow-blur': 100,
'shadow-color': 0x00000000,
}),
parent: node,
});
}
124 changes: 124 additions & 0 deletions src/core/renderers/webgl/internal/ShaderUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { describe, expect, it } from 'vitest';
import { genGradientColors } from './ShaderUtils.js';

type Vec4 = [number, number, number, number];

const smoothstep = (e0: number, e1: number, x: number): number => {
const t = Math.min(Math.max((x - e0) / (e1 - e0), 0), 1);
return t * t * (3 - 2 * t);
};

const mix = (a: Vec4, b: Vec4, t: number): Vec4 => [
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
a[3] + (b[3] - a[3]) * t,
];

/**
* Evaluate the generated GLSL statements as JS. The generated code only uses
* `mix`, `smoothstep`, `u_colors[i]`, `u_stops[i]` and `dist`, so shimming
* those makes the string directly executable.
*/
function evalGradient(stops: number[], colors: Vec4[], dist: number): Vec4 {
const src = genGradientColors(stops.length).replace(
'vec4 colorOut =',
'let colorOut =',
);
const fn = new Function(
'mix',
'smoothstep',
'u_stops',
'u_colors',
'dist',
`${src}; return colorOut;`,
);
return fn(mix, smoothstep, stops, colors, dist) as Vec4;
}

/**
* Reference implementation: the original branchy segment select
* (below first stop / above last stop / smoothstep within the segment).
*/
function referenceGradient(
stops: number[],
colors: Vec4[],
dist: number,
): Vec4 {
dist = Math.min(Math.max(dist, 0), 1);
if (dist <= stops[0]!) return colors[0]!;
const last = stops.length - 1;
if (dist >= stops[last]!) return colors[last]!;
for (let i = 0; i < last; i++) {
if (dist >= stops[i]! && dist <= stops[i + 1]!) {
return mix(
colors[i]!,
colors[i + 1]!,
smoothstep(stops[i]!, stops[i + 1]!, dist),
);
}
}
return colors[last]!;
}

const RED: Vec4 = [1, 0, 0, 1];
const GREEN: Vec4 = [0, 1, 0, 1];
const BLUE: Vec4 = [0, 0, 1, 0.5];
const WHITE: Vec4 = [1, 1, 1, 1];

describe('genGradientColors', () => {
it('emits no branches (no if / ternary / return)', () => {
const src = genGradientColors(4);
expect(src.includes('if')).toBe(false);
expect(src.includes('?')).toBe(false);
expect(src.includes('return')).toBe(false);
});

it('with a single stop resolves to the first color', () => {
const out = evalGradient([0], [RED], 0.7);
expect(out).toEqual(RED);
});

it('clamps to the first color below the first stop', () => {
const out = evalGradient([0.25, 0.75], [RED, GREEN], 0.1);
expect(out).toEqual(RED);
});

it('clamps to the last color above the last stop', () => {
const out = evalGradient([0.25, 0.75], [RED, GREEN], 0.9);
expect(out).toEqual(GREEN);
});

it('interpolates with smoothstep inside a segment', () => {
const dist = 0.5;
const out = evalGradient([0.25, 0.75], [RED, GREEN], dist);
const expected = mix(RED, GREEN, smoothstep(0.25, 0.75, dist));
for (let i = 0; i < 4; i++) {
expect(out[i]).toBeCloseTo(expected[i]!, 6);
}
});

it('matches the reference segment select across a multi-stop ramp', () => {
const stops = [0, 0.3, 0.6, 1];
const colors = [RED, GREEN, BLUE, WHITE];
for (let d = 0; d <= 100; d++) {
const dist = d / 100;
const out = evalGradient(stops, colors, dist);
const ref = referenceGradient(stops, colors, dist);
for (let i = 0; i < 4; i++) {
expect(out[i]).toBeCloseTo(ref[i]!, 6);
}
}
});

it('is exact at stop boundaries', () => {
const stops = [0, 0.5, 1];
const colors = [RED, GREEN, BLUE];
expect(evalGradient(stops, colors, 0)).toEqual(RED);
const mid = evalGradient(stops, colors, 0.5);
for (let i = 0; i < 4; i++) {
expect(mid[i]).toBeCloseTo(GREEN[i]!, 6);
}
expect(evalGradient(stops, colors, 1)).toEqual(BLUE);
});
});
27 changes: 14 additions & 13 deletions src/core/renderers/webgl/internal/ShaderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,21 +243,22 @@ export const DefaultVertexSource = `
`;

/**
* generate fragment source for
* @param stops
* @returns
* Generates branchless gradient-stop evaluation statements for a fragment
* shader. Expects a `float dist` in scope and `u_stops`/`u_colors` uniform
* arrays of length `stops`; leaves the result in `vec4 colorOut`.
*
* The accumulated `mix()` chain is exactly equivalent to selecting the
* segment containing `dist` (for ascending stops): below a segment the
* smoothstep is 0 (no-op), above it is 1 (fully replaced by the next color).
* No `if`/ternary — Mali 400-class fragment pipelines serialize any branch.
*/
export function genGradientColors(stops: number): string {
let result = `
float stopCalc = (dist - u_stops[0]) / (u_stops[1] - u_stops[0]);
vec4 colorOut = mix(u_colors[0], u_colors[1], stopCalc);
`;
if (stops > 2) {
for (let i = 2; i < stops; i++) {
result += `colorOut = mix(colorOut, u_colors[${i}], clamp((dist - u_stops[${
i - 1
}]) / (u_stops[${i}] - u_stops[${i - 1}]), 0.0, 1.0));`;
}
let result = `vec4 colorOut = u_colors[0];`;
for (let i = 1; i < stops; i++) {
result += `
colorOut = mix(colorOut, u_colors[${i}], smoothstep(u_stops[${
i - 1
}], u_stops[${i}], dist));`;
}
return result;
}
23 changes: 12 additions & 11 deletions src/core/shaders/webgl/Border.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,24 +135,25 @@ export const Border: WebGlShaderType<BorderProps> = {
float outerDist = box(boxUv + v_outerBorderUv, v_outerSize - edgeWidth);
float innerDist = box(boxUv + v_innerBorderUv, v_innerSize - edgeWidth);

if(u_borderGap == 0.0) {
float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist);
float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist);
resultColor = mix(resultColor, u_borderColor, outerAlpha * u_borderColor.a);
resultColor = mix(resultColor, color, innerAlpha);
gl_FragColor = resultColor * u_alpha;
return;
}
float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist);
float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist);

float nodeDist = box(boxUv, v_halfDimensions - edgeWidth);
float nodeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, nodeDist);

float borderDist = max(-innerDist, outerDist);
float borderAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, borderDist);
resultColor = mix(resultColor, color, nodeAlpha);
resultColor = mix(resultColor, u_borderColor, borderAlpha * u_borderColor.a);

gl_FragColor = resultColor * u_alpha;
// Branchless gap select -- Mali 400 serializes uniform branches, so both
// composites are computed and mix()-selected on hasGap instead.
vec4 resNoGap = mix(resultColor, u_borderColor, outerAlpha * u_borderColor.a);
resNoGap = mix(resNoGap, color, innerAlpha);

vec4 resGap = mix(resultColor, color, nodeAlpha);
resGap = mix(resGap, u_borderColor, borderAlpha * u_borderColor.a);

float hasGap = step(0.0001, abs(u_borderGap));
gl_FragColor = mix(resNoGap, resGap, hasGap) * u_alpha;
}
`,
};
17 changes: 12 additions & 5 deletions src/core/shaders/webgl/HolePunch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,18 @@ export const HolePunch: WebGlShaderType<HolePunchProps> = {
void main() {
vec4 color = texture2D(u_texture, v_textureCoords) * v_color;
vec2 p = (v_textureCoords.xy * u_dimensions.xy - u_pos) - u_size;
vec4 r = u_radius;
r.xy = (p.x > 0.0) ? r.yz : r.xw;
r.x = (p.y > 0.0) ? r.y : r.x;
p = abs(p) - u_size + r.x;
float dist = min(max(p.x, p.y), 0.0) + length(max(p, 0.0)) - r.x + 2.0;

// Branchless radius selection based on quadrant
// x: TL, y: TR, z: BR, w: BL
vec2 stepVal = step(vec2(0.0), p);
float r = mix(
mix(u_radius.x, u_radius.y, stepVal.x),
mix(u_radius.w, u_radius.z, stepVal.x),
stepVal.y
);

p = abs(p) - u_size + r;
float dist = min(max(p.x, p.y), 0.0) + length(max(p, 0.0)) - r + 2.0;
float roundedAlpha = 1.0 - smoothstep(0.0, u_pixelRatio, dist);
gl_FragColor = mix(color, vec4(0.0), min(color.a, roundedAlpha));
}
Expand Down
22 changes: 3 additions & 19 deletions src/core/shaders/webgl/LinearGradient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '../templates/LinearGradientTemplate.js';
import type { WebGlRenderer } from '../../renderers/webgl/WebGlRenderer.js';
import type { WebGlShaderType } from '../../renderers/webgl/WebGlShaderNode.js';
import { genGradientColors } from '../../renderers/webgl/internal/ShaderUtils.js';

export const LinearGradient: WebGlShaderType<LinearGradientProps> = {
props: LinearGradientTemplate.props,
Expand Down Expand Up @@ -56,7 +57,6 @@ export const LinearGradient: WebGlShaderType<LinearGradientProps> = {
# endif

#define MAX_STOPS ${props.colors.length}
#define LAST_STOP ${props.colors.length - 1}

uniform float u_alpha;

Expand All @@ -72,24 +72,8 @@ export const LinearGradient: WebGlShaderType<LinearGradientProps> = {

vec4 getGradientColor(float dist) {
dist = clamp(dist, 0.0, 1.0);

if(dist <= u_stops[0]) {
return u_colors[0];
}

if(dist >= u_stops[LAST_STOP]) {
return u_colors[LAST_STOP];
}

for(int i = 0; i < LAST_STOP; i++) {
float left = u_stops[i];
float right = u_stops[i + 1];
if(dist >= left && dist <= right) {
float lDist = smoothstep(left, right, dist);
return mix(u_colors[i], u_colors[i + 1], lDist);
}
}
return u_colors[LAST_STOP];
${genGradientColors(props.colors.length)}
return colorOut;
}

void main() {
Expand Down
Loading
Loading