feat(glass): unify soft contours and sidebar corner refraction

This commit is contained in:
InfinityPacer
2026-09-07 11:19:07 +08:00
parent 98bd33e3fc
commit 05ace80900
6 changed files with 123 additions and 41 deletions
@@ -3,6 +3,7 @@ import type { Ref } from 'vue'
import { import {
createGlassNavbarDisplacementMap, createGlassNavbarDisplacementMap,
getGlassNavbarOpticalResponse, getGlassNavbarOpticalResponse,
getGlassSidebarOpticalResponse,
NEUTRAL_GLASS_NAVBAR_DISPLACEMENT_MAP, NEUTRAL_GLASS_NAVBAR_DISPLACEMENT_MAP,
} from '@/utils/glassNavbarRefraction' } from '@/utils/glassNavbarRefraction'
import { useEffectiveGlassSettings } from '@/composables/useThemeCustomizer' import { useEffectiveGlassSettings } from '@/composables/useThemeCustomizer'
@@ -214,7 +215,13 @@ function readDisplacementGeometry(surface: NavigationSurface) {
const width = Math.max(1, Math.round(bounds.width)) const width = Math.max(1, Math.round(bounds.width))
const radius = Number.isFinite(borderRadius) ? borderRadius : state.defaultGeometry.radius const radius = Number.isFinite(borderRadius) ? borderRadius : state.defaultGeometry.radius
const optics = opticalResponse.value const optics =
surface === 'sidebar'
? getGlassSidebarOpticalResponse({
deformation: settings.value.glassDeformationStrength,
translation: settings.value.glassTranslationStrength,
})
: opticalResponse.value
return { return {
height, height,
radius, radius,
@@ -241,7 +248,7 @@ async function syncDisplacementMap(surface: NavigationSurface) {
try { try {
if (state.cachedGeometry !== geometryKey) { if (state.cachedGeometry !== geometryKey) {
if (state.failedGeometry === geometryKey) return if (state.failedGeometry === geometryKey) return
const map = createGlassNavbarDisplacementMap({ height, radius, width, optics }) const map = createGlassNavbarDisplacementMap({ height, radius, width, optics, surface })
if (map === NEUTRAL_GLASS_NAVBAR_DISPLACEMENT_MAP) { if (map === NEUTRAL_GLASS_NAVBAR_DISPLACEMENT_MAP) {
state.failedGeometry = geometryKey state.failedGeometry = geometryKey
invalidateDisplacementMap(surface) invalidateDisplacementMap(surface)
@@ -1,7 +1,7 @@
import { mount, flushPromises } from '@vue/test-utils' import { mount, flushPromises } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import GlassNavbarRefractionDefs from '../GlassNavbarRefractionDefs.vue' import GlassNavbarRefractionDefs from '../GlassNavbarRefractionDefs.vue'
import { createGlassNavbarDisplacementMap } from '@/utils/glassNavbarRefraction' import { createGlassNavbarDisplacementMap, getGlassSidebarOpticalResponse } from '@/utils/glassNavbarRefraction'
import { ref } from 'vue' import { ref } from 'vue'
vi.mock('@/utils/glassNavbarRefraction', async importOriginal => ({ vi.mock('@/utils/glassNavbarRefraction', async importOriginal => ({
@@ -190,7 +190,13 @@ describe('GlassNavbarRefractionDefs', () => {
expect.objectContaining({ width: 1423, height: 64, radius: 16 }), expect.objectContaining({ width: 1423, height: 64, radius: 16 }),
) )
expect(createGlassNavbarDisplacementMap).toHaveBeenLastCalledWith( expect(createGlassNavbarDisplacementMap).toHaveBeenLastCalledWith(
expect.objectContaining({ width: 260, height: 800, radius: 0 }), expect.objectContaining({
width: 260,
height: 800,
radius: 0,
surface: 'sidebar',
optics: getGlassSidebarOpticalResponse({ deformation: 48, translation: 48 }),
}),
) )
expect(shell.dataset.glassNavbarRefractionReady).toBe('false') expect(shell.dataset.glassNavbarRefractionReady).toBe('false')
expectReadyForSidebar(260) expectReadyForSidebar(260)
@@ -4,6 +4,16 @@ import { cwd } from 'node:process'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
describe('glass overlay material styles', () => { describe('glass overlay material styles', () => {
it('shares the soft contour between content cards and detached navigation', () => {
const styles = readFileSync(resolve(cwd(), 'src/styles/themes/_glass-v3.scss'), 'utf8')
expect(styles).toContain('box-shadow: var(--glass-v3-surface-edge), var(--glass-v3-shadow)')
expect(styles).toContain('box-shadow: var(--glass-v3-surface-edge), var(--glass-v3-navigation-shadow)')
expect(styles).toMatch(
/&::before\s*\{[\s\S]*?border-radius: inherit;[\s\S]*?box-shadow: var\(--glass-v3-surface-edge\)/u,
)
})
it('reserves space below detached desktop navigation and follows the compact theme radius', () => { it('reserves space below detached desktop navigation and follows the compact theme radius', () => {
const styles = readFileSync(resolve(cwd(), 'src/styles/themes/_glass-v3.scss'), 'utf8') const styles = readFileSync(resolve(cwd(), 'src/styles/themes/_glass-v3.scss'), 'utf8')
+23 -29
View File
@@ -13,6 +13,7 @@
--glass-v3-navigation-radius: var(--app-field-radius, 16px); --glass-v3-navigation-radius: var(--app-field-radius, 16px);
--glass-v3-navigation-inset: 8px; --glass-v3-navigation-inset: 8px;
--glass-v3-navigation-content-gap: 16px; --glass-v3-navigation-content-gap: 16px;
--glass-v3-navigation-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
--glass-v3-navigation-blur: clamp( --glass-v3-navigation-blur: clamp(
0.65px, 0.65px,
calc(0.9px + var(--glass-surface-density, 0.62) * 0.6px - var(--glass-background-visibility, 0.58) * 0.25px), calc(0.9px + var(--glass-surface-density, 0.62) * 0.6px - var(--glass-background-visibility, 0.58) * 0.25px),
@@ -45,6 +46,16 @@
} }
body[data-theme='glass'] { body[data-theme='glass'] {
.v-card,
.layout-navbar,
.layout-vertical-nav::before {
// 在表面上解析反光强度,卡片 hover 的局部参数不能被根节点预先求值。
--glass-v3-surface-edge:
inset 1px 1px 3px rgba(255, 255, 255, calc(var(--glass-v3-rim) * 0.3)),
inset -1px -1px 3px rgba(255, 255, 255, calc(var(--glass-v3-rim) * 0.22)),
inset 0 0 5px rgba(255, 255, 255, calc(var(--glass-v3-rim) * 0.1));
}
// 已有业务类是语义材料的适配入口;海报整图、内部图表和菜单不承载第二层卡片材料。 // 已有业务类是语义材料的适配入口;海报整图、内部图表和菜单不承载第二层卡片材料。
.layout-page-content .layout-page-content
.v-card:not( .v-card:not(
@@ -63,12 +74,7 @@
.dashboard-grid-content-measure > :first-child > .v-card { .dashboard-grid-content-measure > :first-child > .v-card {
border-radius: var(--app-theme-surface-radius, 20px) !important; border-radius: var(--app-theme-surface-radius, 20px) !important;
background: var(--glass-v3-card-background) !important; background: var(--glass-v3-card-background) !important;
box-shadow: box-shadow: var(--glass-v3-surface-edge), var(--glass-v3-shadow) !important;
inset 0 1px 0 rgba(255, 255, 255, var(--glass-v3-rim)),
inset 1px 0 0 rgba(255, 255, 255, calc(var(--glass-v3-rim) * 0.6)),
inset -1px -2px 2px rgba(0, 0, 0, 0.16),
inset 0 0 0 1px rgba(255, 255, 255, calc(var(--glass-v3-rim) * 0.25)),
var(--glass-v3-shadow) !important;
} }
.layout-page-content .layout-page-content
@@ -174,12 +180,21 @@
.layout-vertical-nav { .layout-vertical-nav {
border-radius: var(--glass-v3-navigation-radius); border-radius: var(--glass-v3-navigation-radius);
box-shadow: var(--glass-v3-shadow) !important; box-shadow: var(--glass-v3-navigation-shadow) !important;
block-size: calc(100% - 2 * var(--glass-v3-navigation-inset)); block-size: calc(100% - 2 * var(--glass-v3-navigation-inset));
inline-size: calc(var(--glass-v3-nav-reserved-width) - var(--glass-v3-navigation-inset)) !important; inline-size: calc(var(--glass-v3-nav-reserved-width) - var(--glass-v3-navigation-inset)) !important;
inset-block-start: var(--glass-v3-navigation-inset); inset-block-start: var(--glass-v3-navigation-inset);
inset-inline-start: var(--glass-v3-navigation-inset); inset-inline-start: var(--glass-v3-navigation-inset);
overflow: clip; overflow: clip;
&::before {
// 材质与采样外壳共用圆角,内沿高光必须绕四角延续,不能绘制直边后再裁断。
border-radius: inherit;
border-inline-end: 0;
background: var(--glass-v3-card-background) !important;
// 固定导航共用柔和的掠射光,四角厚度由透镜承担,不叠加等宽硬质亮线。
box-shadow: var(--glass-v3-surface-edge) !important;
}
} }
&.layout-vertical-nav-collapsed .layout-vertical-nav.hovered { &.layout-vertical-nav-collapsed .layout-vertical-nav.hovered {
@@ -188,6 +203,7 @@
.layout-navbar { .layout-navbar {
border-radius: var(--glass-v3-navigation-radius) !important; border-radius: var(--glass-v3-navigation-radius) !important;
box-shadow: var(--glass-v3-surface-edge), var(--glass-v3-navigation-shadow) !important;
inline-size: calc(100% - var(--glass-v3-nav-reserved-width) - 2 * var(--glass-v3-navigation-inset)) !important; inline-size: calc(100% - var(--glass-v3-nav-reserved-width) - 2 * var(--glass-v3-navigation-inset)) !important;
inset-block-start: var(--glass-v3-navigation-inset) !important; inset-block-start: var(--glass-v3-navigation-inset) !important;
inset-inline-start: calc(var(--glass-v3-nav-reserved-width) + var(--glass-v3-navigation-inset)) !important; inset-inline-start: calc(var(--glass-v3-nav-reserved-width) + var(--glass-v3-navigation-inset)) !important;
@@ -237,18 +253,6 @@
background: var(--glass-v3-card-background) !important; background: var(--glass-v3-card-background) !important;
backdrop-filter: var(--glass-navbar-live-filter) !important; backdrop-filter: var(--glass-navbar-live-filter) !important;
-webkit-backdrop-filter: var(--glass-navbar-live-filter) !important; -webkit-backdrop-filter: var(--glass-navbar-live-filter) !important;
box-shadow:
inset 0 -1px 0 rgba(255, 255, 255, var(--glass-v3-rim)),
0 8px 24px rgba(0, 0, 0, 0.1) !important;
}
.layout-vertical-nav::before {
border-inline-end: 0;
background: var(--glass-v3-card-background) !important;
box-shadow:
inset -1px 0 0 rgba(255, 255, 255, var(--glass-v3-rim)),
inset -3px 0 4px rgba(255, 255, 255, 0.035),
8px 0 24px rgba(0, 0, 0, 0.08) !important;
} }
} }
@@ -266,16 +270,6 @@
} }
} }
// 稳定背板已承担背景吸收,侧栏用明亮散射层呈现厚度,避免再覆盖一层深色渐变。
html[data-theme='glass'][data-glass-appearance='frosted'] body[data-theme='glass'] {
.layout-wrapper[data-shell-mode='desktop'] .layout-vertical-nav::before {
background: var(--glass-v3-card-background) !important;
box-shadow:
inset -1px 0 0 rgba(255, 255, 255, 0.32),
8px 0 24px rgba(0, 0, 0, 0.07) !important;
}
}
// 圆角在脱离边缘的首帧采用最终值,真实inset或transform继续使用同一150ms运动节奏。 // 圆角在脱离边缘的首帧采用最终值,真实inset或transform继续使用同一150ms运动节奏。
html[data-theme='glass']:is( html[data-theme='glass']:is(
[data-glass-appearance='clear'], [data-glass-appearance='clear'],
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { import {
createGlassNavbarDisplacementField, createGlassNavbarDisplacementField,
getGlassNavbarOpticalResponse, getGlassNavbarOpticalResponse,
getGlassSidebarOpticalResponse,
supportsGlassNavbarLiveRefraction, supportsGlassNavbarLiveRefraction,
} from '@/utils/glassNavbarRefraction' } from '@/utils/glassNavbarRefraction'
@@ -45,6 +46,16 @@ describe('getGlassNavbarOpticalResponse', () => {
}) })
}) })
describe('getGlassSidebarOpticalResponse', () => {
it.each([0, 50, 99, 100])('keeps the same cubic sliders and equal corner axes at %s', strength => {
const parameters = Object.freeze({ deformation: strength, translation: strength })
const optics = getGlassSidebarOpticalResponse(parameters)
expect(optics.horizontalRatio).toBe(optics.verticalRatio)
expect(optics.horizontalRatio).toBeLessThanOrEqual(0.3)
expect(optics.translationPx).toBe(getGlassNavbarOpticalResponse(parameters).translationPx)
})
})
describe('createGlassNavbarDisplacementField', () => { describe('createGlassNavbarDisplacementField', () => {
function pixelAt(field: ReturnType<typeof createGlassNavbarDisplacementField>, x: number, y: number) { function pixelAt(field: ReturnType<typeof createGlassNavbarDisplacementField>, x: number, y: number) {
const offset = (y * field.width + x) * 4 const offset = (y * field.width + x) * 4
@@ -83,13 +94,19 @@ describe('createGlassNavbarDisplacementField', () => {
{ width: 252, height: 846, radius: 24 }, { width: 252, height: 846, radius: 24 },
{ width: 60, height: 846, radius: 8 }, { width: 60, height: 846, radius: 8 },
{ width: 60, height: 846, radius: 24 }, { width: 60, height: 846, radius: 24 },
...[60, 252].flatMap(width =>
[0, 8, 12, 16, 20, 24].map(radius => ({ width, height: 180, radius, surface: 'sidebar' as const })),
),
])('keeps two-dimensional sampling forward and inside the image for $width x $height r$radius', geometry => { ])('keeps two-dimensional sampling forward and inside the image for $width x $height r$radius', geometry => {
for (const deformation of [0, 48, 100]) for (const deformation of [0, 48, 100])
for (const translation of [0, 48, 100]) for (const translation of [0, 48, 100])
for (const scale of [-22, -34]) { for (const scale of [-22, -34]) {
const field = createGlassNavbarDisplacementField({ const field = createGlassNavbarDisplacementField({
...geometry, ...geometry,
optics: getGlassNavbarOpticalResponse({ deformation, translation }), optics:
'surface' in geometry
? getGlassSidebarOpticalResponse({ deformation, translation })
: getGlassNavbarOpticalResponse({ deformation, translation }),
}) })
const source = (x: number, y: number) => { const source = (x: number, y: number) => {
const pixel = pixelAt(field, x, y) const pixel = pixelAt(field, x, y)
@@ -123,6 +140,31 @@ describe('createGlassNavbarDisplacementField', () => {
} }
}) })
it.each([8, 12, 16, 20, 24])('turns all four sidebar corners with one radial profile at radius %s', radius => {
const field = createGlassNavbarDisplacementField({
width: 252,
height: 180,
radius,
surface: 'sidebar',
optics: getGlassSidebarOpticalResponse({ deformation: 50, translation: 0 }),
})
for (let y = 0; y < radius; y += 1)
for (let x = 0; x < radius; x += 1) {
const topLeft = pixelAt(field, x, y)
const topRight = pixelAt(field, field.width - 1 - x, y)
const bottomLeft = pixelAt(field, x, field.height - 1 - y)
const bottomRight = pixelAt(field, field.width - 1 - x, field.height - 1 - y)
expect(topLeft[0] + topRight[0]).toBe(256)
expect(topLeft[2] + bottomLeft[2]).toBe(256)
expect(bottomRight[0]).toBe(topRight[0])
expect(bottomRight[2]).toBe(bottomLeft[2])
expect(pixelAt(field, y, x)[0]).toBe(topLeft[2])
}
expect(pixelAt(field, 126, 2)[2]).toBeLessThan(128)
expect(pixelAt(field, 126, 177)[2]).toBeGreaterThan(128)
expect(pixelAt(field, 126, 90)).toEqual([128, 128, 128, 255])
})
it('keeps a fixed rectangle optically active without substituting a rounded corner', () => { it('keeps a fixed rectangle optically active without substituting a rounded corner', () => {
const field = createGlassNavbarDisplacementField({ const field = createGlassNavbarDisplacementField({
height: 800, height: 800,
+30 -7
View File
@@ -16,26 +16,38 @@ export interface GlassNavbarRefractionBrowserIdentity {
} }
export interface GlassNavbarDisplacementGeometry { export interface GlassNavbarDisplacementGeometry {
/** 侧栏使用四边等向的窄透镜;省略时保留顶栏的横向阅读保护方案。 */
surface?: 'navbar' | 'sidebar'
/** 折射表面的实际 CSS 像素高度。 */ /** 折射表面的实际 CSS 像素高度。 */
height: number height: number
/** 最终可见外轮廓的圆角半径。 */ /** 最终可见外轮廓的圆角半径。 */
radius: number radius: number
/** 折射表面的实际 CSS 像素宽度。 */ /** 折射表面的实际 CSS 像素宽度。 */
width: number width: number
/** 由当前生效滑杆计算的顶栏光学响应;省略时采用清透自然默认值。 */ /** 由当前生效滑杆计算的导航光学响应;省略时采用清透自然默认值。 */
optics?: GlassNavbarOpticalResponse optics?: GlassNavbarOpticalResponse
} }
/** 顶栏局部取样预算,不包含共享 renderer 的流动、尾波与惯性。 */ /** 导航局部取样预算,不包含共享 renderer 的流动、尾波与惯性。 */
export interface GlassNavbarOpticalResponse { export interface GlassNavbarOpticalResponse {
/** 横向边缘峰值位移与轮廓带宽之比。 */ /** 横向边缘峰值位移与轮廓带宽之比。 */
horizontalRatio: number horizontalRatio: number
/** 纵向峰值位移与轮廓带宽之比,严格小于横向以保护字形高度。 */ /** 纵向峰值位移与轮廓带宽之比;顶栏弱于横向,侧栏沿四边等向响应。 */
verticalRatio: number verticalRatio: number
/** 主体内容统一向右显示的 CSS 像素偏移,外轮廓平缓回零。 */ /** 主体内容统一向右显示的 CSS 像素偏移,外轮廓平缓回零。 */
translationPx: number translationPx: number
} }
/** 侧栏的阅读区由窄边带保护,圆角处两轴使用相同强度,避免转角时透镜厚度消失。 */
export function getGlassSidebarOpticalResponse(
parameters: Pick<GlassOpticalParameters, 'deformation' | 'translation'>,
): GlassNavbarOpticalResponse {
const navbar = getGlassNavbarOpticalResponse(parameters)
// 最大位移不超过带宽的 30%,为圆角内侧的取样回落保留单调余量。
const ratio = (navbar.horizontalRatio / 0.42) * 0.3
return { horizontalRatio: ratio, verticalRatio: ratio, translationPx: navbar.translationPx }
}
/** 导航以低中段可读性为优先,高段保留完整位移预算;不改写共享参数或材质响应。 */ /** 导航以低中段可读性为优先,高段保留完整位移预算;不改写共享参数或材质响应。 */
export function getGlassNavbarOpticalResponse( export function getGlassNavbarOpticalResponse(
parameters: Pick<GlassOpticalParameters, 'deformation' | 'translation'>, parameters: Pick<GlassOpticalParameters, 'deformation' | 'translation'>,
@@ -108,7 +120,10 @@ export function createGlassNavbarDisplacementField({
height, height,
radius, radius,
width, width,
optics = DEFAULT_NAVBAR_OPTICS, surface = 'navbar',
optics = surface === 'sidebar'
? getGlassSidebarOpticalResponse(getGlassOpticalPresetParameters('clear', 'high', 'natural'))
: DEFAULT_NAVBAR_OPTICS,
}: GlassNavbarDisplacementGeometry): GlassNavbarDisplacementField { }: GlassNavbarDisplacementGeometry): GlassNavbarDisplacementField {
const pixelWidth = normalizePixelSize(width) const pixelWidth = normalizePixelSize(width)
const pixelHeight = normalizePixelSize(height) const pixelHeight = normalizePixelSize(height)
@@ -116,7 +131,11 @@ export function createGlassNavbarDisplacementField({
const pixelRadius = Number.isFinite(radius) ? Math.max(0, Math.min(maxRadius, radius)) : 0 const pixelRadius = Number.isFinite(radius) ? Math.max(0, Math.min(maxRadius, radius)) : 0
// 直角固定表面没有圆角半径可供推导,仍使用受最短边约束的直边带;radius=0 不能被当成无折射。 // 直角固定表面没有圆角半径可供推导,仍使用受最短边约束的直边带;radius=0 不能被当成无折射。
const radiusBand = pixelRadius > 0 ? pixelRadius * 1.5 : REFRACTION_BAND_PX const radiusBand = pixelRadius > 0 ? pixelRadius * 1.5 : REFRACTION_BAND_PX
const maximumBand = Math.min(REFRACTION_BAND_PX, radiusBand, Math.min(pixelWidth, pixelHeight) / 2) // 侧栏边带不穿过圆角圆心,四角法线能连续转向且不会在内侧形成聚焦尖点。
const maximumBand =
surface === 'sidebar'
? Math.min(12, pixelRadius || 12, Math.min(pixelWidth, pixelHeight) / 2)
: Math.min(REFRACTION_BAND_PX, radiusBand, Math.min(pixelWidth, pixelHeight) / 2)
const pixels = new Uint8ClampedArray(pixelWidth * pixelHeight * 4) const pixels = new Uint8ClampedArray(pixelWidth * pixelHeight * 4)
for (let offset = 0; offset < pixels.length; offset += 4) { for (let offset = 0; offset < pixels.length; offset += 4) {
@@ -152,7 +171,10 @@ export function createGlassNavbarDisplacementField({
// 横向平移从边缘透镜退出后进入,避免两种回落梯度叠加导致局部反向采样。 // 横向平移从边缘透镜退出后进入,避免两种回落梯度叠加导致局部反向采样。
const horizontalRamp = smoothstep(Math.max(0, Math.min(1, (edgeX - maximumBand) / 64))) const horizontalRamp = smoothstep(Math.max(0, Math.min(1, (edgeX - maximumBand) / 64)))
const verticalRamp = smoothstep(Math.min(1, (edgeY - outerGuard) / Math.max(1, Math.min(12, maximumBand)))) const verticalRamp =
surface === 'sidebar'
? smoothstep(Math.max(0, Math.min(1, (edgeY - maximumBand) / 64)))
: smoothstep(Math.min(1, (edgeY - outerGuard) / Math.max(1, Math.min(12, maximumBand))))
const translationChannel = (optics.translationPx * horizontalRamp * verticalRamp * 255) / HIGH_REFRACTION_SCALE_PX const translationChannel = (optics.translationPx * horizontalRamp * verticalRamp * 255) / HIGH_REFRACTION_SCALE_PX
if (pixelRadius === 0) { if (pixelRadius === 0) {
@@ -175,7 +197,8 @@ export function createGlassNavbarDisplacementField({
const profile = distanceInside < bandWidth ? refractionProfile(distanceInside, bandWidth, outerGuard) : 0 const profile = distanceInside < bandWidth ? refractionProfile(distanceInside, bandWidth, outerGuard) : 0
const verticalProgress = Math.min(1, (distanceInside - outerGuard) / (bandWidth - outerGuard)) const verticalProgress = Math.min(1, (distanceInside - outerGuard) / (bandWidth - outerGuard))
const verticalProfile = Math.sin(Math.PI * verticalProgress) ** 2 // 侧栏的两轴必须共用同一深度剖面,圆角法线旋转时才不会变成扁平或椭圆透镜。
const verticalProfile = surface === 'sidebar' ? profile : Math.sin(Math.PI * verticalProgress) ** 2
const verticalAmplitude = (bandWidth * optics.verticalRatio * 255) / HIGH_REFRACTION_SCALE_PX const verticalAmplitude = (bandWidth * optics.verticalRatio * 255) / HIGH_REFRACTION_SCALE_PX
const gradientX = const gradientX =
roundedRectangleSignedDistance(sampleX + 0.5, sampleY, pixelWidth, pixelHeight, pixelRadius) - roundedRectangleSignedDistance(sampleX + 0.5, sampleY, pixelWidth, pixelHeight, pixelRadius) -