diff --git a/src/core/shaders/templates/RoundedWithBorderAndShadowTemplate.ts b/src/core/shaders/templates/RoundedWithBorderAndShadowTemplate.ts index c5846a22..d4241fff 100644 --- a/src/core/shaders/templates/RoundedWithBorderAndShadowTemplate.ts +++ b/src/core/shaders/templates/RoundedWithBorderAndShadowTemplate.ts @@ -23,13 +23,30 @@ import { getShadowProps, type ShadowProps } from './ShadowTemplate.js'; export type RoundedWithBorderAndShadowProps = RoundedProps & PrefixedType & - PrefixedType; + PrefixedType & { + /** + * Gap between the border and the content + * + * @default 0 + */ + 'border-gap': number; + /** + * Color of the gap + * + * @default 0x00000000 + */ + 'border-gapColor': number; + }; const props = Object.assign( {}, RoundedTemplate.props, getBorderProps('border'), getShadowProps('shadow'), + { + 'border-gap': 0, + 'border-gapColor': 0x00000000, + }, ) as RoundedWithBorderAndShadowProps; export const RoundedWithBorderAndShadowTemplate: CoreShaderType = diff --git a/src/core/shaders/templates/RoundedWithBorderTemplate.ts b/src/core/shaders/templates/RoundedWithBorderTemplate.ts index f3daf278..4778580c 100644 --- a/src/core/shaders/templates/RoundedWithBorderTemplate.ts +++ b/src/core/shaders/templates/RoundedWithBorderTemplate.ts @@ -21,12 +21,29 @@ import { RoundedTemplate, type RoundedProps } from './RoundedTemplate.js'; import type { PrefixedType } from '../utils.js'; export type RoundedWithBorderProps = RoundedProps & - PrefixedType; + PrefixedType & { + /** + * Gap between the border and the content + * + * @default 0 + */ + 'border-gap': number; + /** + * Color of the gap + * + * @default 0x00000000 + */ + 'border-gapColor': number; + }; const props = Object.assign( {}, RoundedTemplate.props, getBorderProps('border'), + { + 'border-gap': 0, + 'border-gapColor': 0x00000000, + }, ) as RoundedWithBorderProps; export const RoundedWithBorderTemplate: CoreShaderType = diff --git a/src/core/shaders/webgl/RoundedWithBorder.ts b/src/core/shaders/webgl/RoundedWithBorder.ts index d9e7c377..5bf59439 100644 --- a/src/core/shaders/webgl/RoundedWithBorder.ts +++ b/src/core/shaders/webgl/RoundedWithBorder.ts @@ -27,12 +27,55 @@ import { export const RoundedWithBorder: WebGlShaderType = { props: RoundedWithBorderTemplate.props, update(node: CoreNode) { - this.uniformRGBA('u_borderColor', this.props!['border-color']); - this.uniform4fa('u_borderWidth', this.props!['border-w'] as Vec4); + const props = this.props!; + const borderWidth = props['border-w'] as Vec4; + const borderGap = props['border-gap'] || 0; + this.uniformRGBA('u_borderColor', props['border-color']); + this.uniform4fa('u_borderWidth', borderWidth); + this.uniform1f('u_borderGap', borderGap); + this.uniformRGBA('u_borderGapColor', props['border-gapColor']); + + const origWidth = node.w; + const origHeight = node.h; + this.uniform2f('u_dimensions_orig', origWidth, origHeight); + + const expandedWidth = + origWidth + borderWidth[3] + borderWidth[1] + borderGap * 2; // original + left + right + 2*gap + const expandedHeight = + origHeight + borderWidth[0] + borderWidth[2] + borderGap * 2; // original + top + bottom + 2*gap + + // u_dimensions for the shader's SDF functions should be the expanded size + this.uniform2f('u_dimensions', expandedWidth, expandedHeight); + + // The `radius` property is for the content rectangle. + // Factor it against the original dimensions to prevent self-intersection. + const contentRadius = calcFactoredRadiusArray( + this.props!.radius as Vec4, + origWidth, + origHeight, + ); + + // From the content radius, calculate the outer radius of the border. + // For each corner, the total radius is content radius + gap + border thickness. + // Border thickness at a corner is approximated as the max of the two adjacent border sides. + const bTop = borderWidth[0], + bRight = borderWidth[1], + bBottom = borderWidth[2], + bLeft = borderWidth[3]; + const outerRadius: Vec4 = [ + Math.max(0, contentRadius[0] + borderGap + Math.max(bTop, bLeft)), // top-left + Math.max(0, contentRadius[1] + borderGap + Math.max(bTop, bRight)), // top-right + Math.max(0, contentRadius[2] + borderGap + Math.max(bBottom, bRight)), // bottom-right + Math.max(0, contentRadius[3] + borderGap + Math.max(bBottom, bLeft)), // bottom-left + ]; + + // The final radius passed to the shader is the outer radius of the whole shape. + // It also needs to be factored against the expanded dimensions. + // The shader will then work inwards to calculate the radii for the gap and content. this.uniform4fa( 'u_radius', - calcFactoredRadiusArray(this.props!.radius as Vec4, node.w, node.h), + calcFactoredRadiusArray(outerRadius, expandedWidth, expandedHeight), ); }, vertex: ` @@ -50,13 +93,17 @@ export const RoundedWithBorder: WebGlShaderType = { uniform vec2 u_resolution; uniform float u_pixelRatio; uniform vec2 u_dimensions; + uniform vec2 u_dimensions_orig; uniform vec4 u_radius; uniform vec4 u_borderWidth; + uniform float u_borderGap; varying vec4 v_color; varying vec2 v_textureCoords; varying vec2 v_nodeCoords; + varying vec4 v_borderEndRadius; + varying vec2 v_borderEndSize; varying vec4 v_innerRadius; varying vec2 v_innerSize; @@ -64,26 +111,65 @@ export const RoundedWithBorder: WebGlShaderType = { varying float v_borderZero; void main() { - vec2 normalized = a_position * u_pixelRatio; vec2 screenSpace = vec2(2.0 / u_resolution.x, -2.0 / u_resolution.y); v_color = a_color; v_nodeCoords = a_nodeCoords; - v_textureCoords = a_textureCoords; - v_halfDimensions = u_dimensions * 0.5; + float bTop = u_borderWidth.x; + float bRight = u_borderWidth.y; + float bBottom = u_borderWidth.z; + float bLeft = u_borderWidth.w; + float gap = u_borderGap; + + // Calculate the offset to expand the quad for border and gap + vec2 expansionOffset = vec2(0.0); + if (a_nodeCoords.x == 0.0) { // Left edge vertex + expansionOffset.x = -(bLeft + gap); + } else { // Right edge vertex (a_nodeCoords.x == 1.0) + expansionOffset.x = (bRight + gap); + } + if (a_nodeCoords.y == 0.0) { // Top edge vertex + expansionOffset.y = -(bTop + gap); + } else { // Bottom edge vertex (a_nodeCoords.y == 1.0) + expansionOffset.y = (bBottom + gap); + } + + vec2 expanded_a_position = a_position + expansionOffset; + vec2 normalized = expanded_a_position * u_pixelRatio; - v_borderZero = u_borderWidth == vec4(0.0) ? 1.0 : 0.0; + // u_dimensions is expanded, u_dimensions_orig is original content size + v_textureCoords.x = (a_textureCoords.x * u_dimensions.x - (bLeft + gap)) / u_dimensions_orig.x; + v_textureCoords.y = (a_textureCoords.y * u_dimensions.y - (bTop + gap)) / u_dimensions_orig.y; + v_borderZero = (u_borderWidth.x == 0.0 && u_borderWidth.y == 0.0 && u_borderWidth.z == 0.0 && u_borderWidth.w == 0.0) ? 1.0 : 0.0; + // If there's no border, there's no gap from the border logic perspective + // The Rounded shader itself would handle radius if borderZero is true. + v_halfDimensions = u_dimensions * 0.5; // u_dimensions is now expanded_dimensions if(v_borderZero == 0.0) { - v_innerRadius = vec4( - max(0.0, u_radius.x - max(u_borderWidth.x, u_borderWidth.w) - 0.5), - max(0.0, u_radius.y - max(u_borderWidth.x, u_borderWidth.y) - 0.5), - max(0.0, u_radius.z - max(u_borderWidth.z, u_borderWidth.y) - 0.5), - max(0.0, u_radius.w - max(u_borderWidth.z, u_borderWidth.w) - 0.5) + // Calculate radius and size for the inner edge of the border (where the gap begins) + v_borderEndRadius = vec4( + max(0.0, u_radius.x - max(bTop, bLeft) - 0.5), + max(0.0, u_radius.y - max(bTop, bRight) - 0.5), + max(0.0, u_radius.z - max(bBottom, bRight) - 0.5), + max(0.0, u_radius.w - max(bBottom, bLeft) - 0.5) ); + v_borderEndSize = vec2( + (u_dimensions.x - (bLeft + bRight) - 1.0), + (u_dimensions.y - (bTop + bBottom) - 1.0) + ) * 0.5; - v_innerSize = (vec2(u_dimensions.x - (u_borderWidth[3] + u_borderWidth[1]) + 1.0, u_dimensions.y - (u_borderWidth[0] + u_borderWidth[2])) - 2.0) * 0.5; + // Calculate radius and size for the content area (after the gap) + v_innerRadius = vec4( + max(0.0, u_radius.x - max(bTop, bLeft) - u_borderGap - 0.5), + max(0.0, u_radius.y - max(bTop, bRight) - u_borderGap - 0.5), + max(0.0, u_radius.z - max(bBottom, bRight) - u_borderGap - 0.5), + max(0.0, u_radius.w - max(bBottom, bLeft) - u_borderGap - 0.5) + ); + v_innerSize = vec2( + (u_dimensions.x - (bLeft + bRight) - (u_borderGap * 2.0) - 1.0), + (u_dimensions.y - (bTop + bBottom) - (u_borderGap * 2.0) - 1.0) + ) * 0.5; } gl_Position = vec4(normalized.x * screenSpace.x - 1.0, normalized.y * -abs(screenSpace.y) + 1.0, 0.0, 1.0); @@ -107,6 +193,10 @@ export const RoundedWithBorder: WebGlShaderType = { uniform vec4 u_borderWidth; uniform vec4 u_borderColor; + uniform vec4 u_borderGapColor; + + varying vec4 v_borderEndRadius; + varying vec2 v_borderEndSize; varying vec4 v_color; varying vec2 v_textureCoords; @@ -125,28 +215,45 @@ export const RoundedWithBorder: WebGlShaderType = { } void main() { - vec4 color = texture2D(u_texture, v_textureCoords) * v_color; + vec4 contentTexColor = texture2D(u_texture, v_textureCoords) * v_color; vec2 boxUv = v_nodeCoords.xy * u_dimensions - v_halfDimensions; - float outerDist = roundedBox(boxUv, v_halfDimensions, u_radius); + float outerShapeDist = roundedBox(boxUv, v_halfDimensions, u_radius); float edgeWidth = 1.0 / u_pixelRatio; - float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist); + float outerShapeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerShapeDist); - if(v_borderZero == 1.0) { - gl_FragColor = mix(vec4(0.0), color, outerAlpha) * u_alpha; + if(v_borderZero == 1.0) { // No border, effectively no gap from border logic + gl_FragColor = mix(vec4(0.0), contentTexColor, outerShapeAlpha) * u_alpha; return; } - boxUv.x += u_borderWidth.y > u_borderWidth.w ? (u_borderWidth.y - u_borderWidth.w) * 0.5 : -(u_borderWidth.w - u_borderWidth.y) * 0.5; - boxUv.y += u_borderWidth.z > u_borderWidth.x ? ((u_borderWidth.z - u_borderWidth.x) * 0.5 + 0.5) : -(u_borderWidth.x - u_borderWidth.z) * 0.5; + // Adjust boxUv for non-uniform borders + vec2 adjustedBoxUv = boxUv; + adjustedBoxUv.x += (u_borderWidth.y - u_borderWidth.w) * 0.5; + adjustedBoxUv.y += (u_borderWidth.z - u_borderWidth.x) * 0.5; + + // Inner Border Edge (Gap starts here) + float borderEndDist = roundedBox(adjustedBoxUv, v_borderEndSize, v_borderEndRadius); + float borderEndAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, borderEndDist); + + // Content Area (Gap ends here) + float contentDist = roundedBox(adjustedBoxUv, v_innerSize, v_innerRadius); + float contentAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, contentDist); + + // Calculate Masks for mutually exclusive regions based on priority (Border Top, Gap Middle, Content Bottom) + float borderMask = clamp(outerShapeAlpha - borderEndAlpha, 0.0, 1.0); + float gapMask = clamp(borderEndAlpha - contentAlpha, 0.0, 1.0); - float innerDist = roundedBox(boxUv, v_innerSize, v_innerRadius); - float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist); + // Composite Layers + // 1. Content + vec4 composite = mix(vec4(0.0), contentTexColor, contentAlpha); + // 2. Gap + composite = mix(composite, u_borderGapColor, gapMask); + // 3. Border + composite = mix(composite, u_borderColor, borderMask); - vec4 resColor = mix(u_borderColor, color, innerAlpha); - resColor = mix(vec4(0.0), resColor, outerAlpha); - gl_FragColor = resColor * u_alpha; + gl_FragColor = composite * u_alpha; } `, }; diff --git a/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts b/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts index 6be095a8..7b76fe48 100644 --- a/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts +++ b/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts @@ -29,15 +29,53 @@ export const RoundedWithBorderAndShadow: WebGlShaderType 0.0) ? r.y : r.x; vec2 q = abs(p) - s + r.x; float dist = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; - return 1.0 - smoothstep(-u_shadow.w, u_shadow.w + u_shadow.z, dist); + return 1.0 - smoothstep(-(u_shadow.w), (u_shadow.w + u_shadow.z), dist); } void main() { - vec4 color = texture2D(u_texture, v_textureCoords) * v_color; + vec4 contentTexColor = texture2D(u_texture, v_textureCoords) * v_color; vec2 boxUv = v_nodeCoords.xy * u_dimensions - v_halfDimensions; - float outerDist = roundedBox(boxUv, v_halfDimensions - 1.0, u_radius); + + // Outer Shape Distance (Outset Border) + float outerShapeDist = roundedBox(boxUv, v_halfDimensions, u_radius); float edgeWidth = 1.0 / u_pixelRatio; - float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist); + float outerShapeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerShapeDist); + // Shadow + // Shadow uses the same boxUv (relative to current pixel) but assumes the shape is offset by u_shadow.xy + // And the shadow shape is expanded by spread (shadow.w). + // Note: roundedBox returns distance from edge. + // shadowBox calculates alpha based on distance. float shadowAlpha = shadowBox(boxUv - u_shadow.xy, v_halfDimensions + u_shadow.w, u_radius + u_shadow.z); - vec4 shadow = mix(vec4(0.0), u_shadowColor, shadowAlpha); + // If no border, we treat it as Content + Shadow if(v_borderZero == 1.0) { - gl_FragColor = mix(shadow, color, outerAlpha) * u_alpha; - return; + // Mix Shadow with Content based on OuterShapeAlpha (which acts as ContentAlpha here) + // Wait, if no border/gap, outerShapeDist IS the content dist. + vec4 shadow = mix(vec4(0.0), u_shadowColor, shadowAlpha); + gl_FragColor = mix(shadow, contentTexColor, outerShapeAlpha) * u_alpha; + return; } - boxUv.x += u_borderWidth.y > u_borderWidth.w ? (u_borderWidth.y - u_borderWidth.w) * 0.5 : -(u_borderWidth.w - u_borderWidth.y) * 0.5; - boxUv.y += u_borderWidth.z > u_borderWidth.x ? ((u_borderWidth.z - u_borderWidth.x) * 0.5 + 0.5) : -(u_borderWidth.x - u_borderWidth.z) * 0.5; - - float innerDist = roundedBox(boxUv, v_innerSize, v_innerRadius); - float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist); - - vec4 resColor = mix(u_borderColor, color, innerAlpha); - resColor = mix(shadow, resColor, outerAlpha); - gl_FragColor = resColor * u_alpha; + // Adjust boxUv for non-uniform borders + vec2 adjustedBoxUv = boxUv; + adjustedBoxUv.x += (u_borderWidth.y - u_borderWidth.w) * 0.5; + adjustedBoxUv.y += (u_borderWidth.z - u_borderWidth.x) * 0.5; + + // Inner Border Edge (Gap starts here) + float borderEndDist = roundedBox(adjustedBoxUv, v_borderEndSize, v_borderEndRadius); + float borderEndAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, borderEndDist); + + // Content Area + float contentDist = roundedBox(adjustedBoxUv, v_innerSize, v_innerRadius); + float contentAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, contentDist); + + // Calculate Masks for layers + float borderMask = clamp(outerShapeAlpha - borderEndAlpha, 0.0, 1.0); + float gapMask = clamp(borderEndAlpha - contentAlpha, 0.0, 1.0); + + // Composite Layers + // 0. Shadow (Base) + vec4 composite = mix(vec4(0.0), u_shadowColor, shadowAlpha); + + // 1. Content + composite = mix(composite, contentTexColor, contentAlpha); + + // 2. Gap + composite = mix(composite, u_borderGapColor, gapMask); + + // 3. Border + composite = mix(composite, u_borderColor, borderMask); + + // Final Alpha weighting (Use outerShapeAlpha only for edge AA? No, composite already handles shape) + // Actually, composite at this point covers the whole shape (Content+Gap+Border+Shadow). + // Wait, shadowAlpha extends BEYOND outerShapeAlpha. + + // Logic check: + // contentAlpha handles content shape. + // gapMask handles gap shape. + // borderMask handles border shape. + // shadowAlpha handles shadow shape. + + // If we are strictly in Shadow area (outside Border), borderMask=0, gapMask=0, contentAlpha=0. + // composite starts as ShadowColor * shadowAlpha. + // mix(composite, content, 0) -> shadow. + // mix(shadow, gap, 0) -> shadow. + // mix(shadow, border, 0) -> shadow. + // Result: Shadow. Correct. + + // If we are in Border area: + // borderMask=1. + // composite starts as Shadow. + // mix(shadow, content, 0) -> shadow. + // mix(shadow, gap, 0) -> shadow. + // mix(shadow, border, 1) -> Border. Correct. + + gl_FragColor = composite * u_alpha; } `, }; diff --git a/visual-regression/certified-snapshots/chromium-ci/shader-border-1.png b/visual-regression/certified-snapshots/chromium-ci/shader-border-1.png index ec299788..6c5964f9 100644 Binary files a/visual-regression/certified-snapshots/chromium-ci/shader-border-1.png and b/visual-regression/certified-snapshots/chromium-ci/shader-border-1.png differ diff --git a/visual-regression/certified-snapshots/chromium-ci/shader-rounded-1.png b/visual-regression/certified-snapshots/chromium-ci/shader-rounded-1.png index 69521b5d..e1ae7a32 100644 Binary files a/visual-regression/certified-snapshots/chromium-ci/shader-rounded-1.png and b/visual-regression/certified-snapshots/chromium-ci/shader-rounded-1.png differ diff --git a/visual-regression/certified-snapshots/chromium-ci/shader-shadow-1.png b/visual-regression/certified-snapshots/chromium-ci/shader-shadow-1.png index 05d16fb0..bc4aa342 100644 Binary files a/visual-regression/certified-snapshots/chromium-ci/shader-shadow-1.png and b/visual-regression/certified-snapshots/chromium-ci/shader-shadow-1.png differ