| 1 | struct Vertex { |
| 2 | @builtin(position) position: vec4<f32>, |
| 3 | @location(0) uv: vec2<f32>, |
| 4 | @location(1) color: vec4<f32>, |
| 5 | @location(2) local: vec2<f32>, |
| 6 | @location(3) @interpolate(flat) shape: vec4<f32>, |
| 7 | @location(4) @interpolate(flat) stroke: f32, |
| 8 | @location(5) clip_local: vec2<f32>, |
| 9 | @location(6) @interpolate(flat) clip: vec3<f32>, |
| 10 | @location(7) @interpolate(flat) blur: f32, |
| 11 | } |
| 12 | |
| 13 | @group(0) @binding(0) var atlas: texture_2d<f32>; |
| 14 | @group(0) @binding(1) var atlas_sampler: sampler; |
| 15 | |
| 16 | @vertex |
| 17 | fn vertex(@location(0) position: vec2<f32>, @location(1) uv: vec2<f32>, |
| 18 | @location(2) color: vec4<f32>, @location(3) local: vec2<f32>, |
| 19 | @location(4) shape: vec4<f32>, @location(5) stroke: f32, |
| 20 | @location(6) clip_local: vec2<f32>, @location(7) clip: vec3<f32>, |
| 21 | @location(8) blur: f32) -> Vertex { |
| 22 | return Vertex(vec4<f32>(position, 0.0, 1.0), uv, color, local, shape, stroke, clip_local, clip, blur); |
| 23 | } |
| 24 | |
| 25 | fn ellipse_arc(angle: f32, radius: vec2<f32>) -> f32 { |
| 26 | if radius.x == radius.y { |
| 27 | return angle * radius.x; |
| 28 | } |
| 29 | // Four-point Gauss-Legendre quadrature. |
| 30 | let nodes = vec4<f32>(-0.86113631, -0.33998104, 0.33998104, 0.86113631); |
| 31 | let weights = vec4<f32>(0.34785485, 0.65214515, 0.65214515, 0.34785485); |
| 32 | let half = angle * 0.5; |
| 33 | let scale = max(radius.x, radius.y); |
| 34 | var sum = 0.0; |
| 35 | for (var i = 0u; i < 4u; i += 1u) { |
| 36 | let t = half * (nodes[i] + 1.0); |
| 37 | sum += weights[i] * length((radius / scale) * vec2<f32>(sin(t), cos(t))); |
| 38 | } |
| 39 | return half * sum * scale; |
| 40 | } |
| 41 | |
| 42 | fn erf(x: vec2<f32>) -> vec2<f32> { |
| 43 | let s = sign(x); |
| 44 | let a = abs(x); |
| 45 | var y = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; |
| 46 | y *= y; |
| 47 | return s - s / (y * y); |
| 48 | } |
| 49 | |
| 50 | // Coverage at `p` of a rectangle `half` about the origin with round corners `corner`, |
| 51 | // blurred by a Gaussian of deviation `sigma`: exact across, sampled four times along |
| 52 | // (Evan Wallace, "Fast Rounded Rectangle Shadows"). |
| 53 | fn shadow(p: vec2<f32>, half: vec2<f32>, corner: f32, sigma: f32) -> f32 { |
| 54 | let start = clamp(-3.0 * sigma, p.y - half.y, p.y + half.y); |
| 55 | let end = clamp(3.0 * sigma, p.y - half.y, p.y + half.y); |
| 56 | let stride = (end - start) / 4.0; |
| 57 | var y = start + stride * 0.5; |
| 58 | var value = 0.0; |
| 59 | for (var i = 0; i < 4; i += 1) { |
| 60 | let delta = min(half.y - corner - abs(p.y - y), 0.0); |
| 61 | let curved = half.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); |
| 62 | let integral = 0.5 + 0.5 * erf((p.x + vec2<f32>(-curved, curved)) * (0.70710678 / sigma)); |
| 63 | let weight = exp(-y * y / (2.0 * sigma * sigma)) / (2.50662827 * sigma); |
| 64 | value += (integral.y - integral.x) * weight * stride; |
| 65 | y += stride; |
| 66 | } |
| 67 | return value; |
| 68 | } |
| 69 | |
| 70 | // Distance from `p` to the tapered capsule from (-h, 0) to (h, 0) with round ends of radii |
| 71 | // `r0` and `r1` (Inigo Quilez's uneven capsule). |
| 72 | fn taper(p: vec2<f32>, h: f32, r0: f32, r1: f32) -> f32 { |
| 73 | let q = vec2<f32>(abs(p.y), p.x + h); |
| 74 | let span = 2.0 * h; |
| 75 | let b = (r0 - r1) / max(span, 1e-6); |
| 76 | let far = length(q - vec2<f32>(0.0, span)) - r1; |
| 77 | if abs(b) >= 1.0 { |
| 78 | return min(length(q) - r0, far); |
| 79 | } |
| 80 | let a = sqrt(1.0 - b * b); |
| 81 | let k = dot(q, vec2<f32>(-b, a)); |
| 82 | if k < 0.0 { |
| 83 | return length(q) - r0; |
| 84 | } |
| 85 | if k > a * span { |
| 86 | return far; |
| 87 | } |
| 88 | return dot(q, vec2<f32>(a, b)) - r0; |
| 89 | } |
| 90 | |
| 91 | @fragment |
| 92 | fn fragment(input: Vertex) -> @location(0) vec4<f32> { |
| 93 | var color = textureSample(atlas, atlas_sampler, input.uv) * input.color; |
| 94 | if input.blur > 0.0 { |
| 95 | color *= shadow(input.local, input.shape.xy, input.shape.z, input.blur); |
| 96 | } else if input.blur < 0.0 { |
| 97 | color *= clamp(0.5 - taper(input.local, input.shape.x, input.shape.y, input.shape.z), 0.0, 1.0); |
| 98 | } else if input.shape.x > 0.0 && input.shape.y > 0.0 { |
| 99 | let q = abs(input.local) - input.shape.xy + input.shape.zw; |
| 100 | var distance: f32; |
| 101 | if input.shape.z == input.shape.w { |
| 102 | distance = length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - input.shape.z; |
| 103 | } else if all(q > vec2<f32>(0.0)) { |
| 104 | let radius = max(input.shape.zw, vec2<f32>(0.0001)); |
| 105 | let normalized = q / radius; |
| 106 | let k0 = length(normalized); |
| 107 | distance = k0 * (k0 - 1.0) / max(length(normalized / radius), 1e-20); |
| 108 | } else { |
| 109 | let edge = abs(input.local) - input.shape.xy; |
| 110 | distance = max(edge.x, edge.y); |
| 111 | } |
| 112 | var coverage = clamp(0.5 - distance, 0.0, 1.0); |
| 113 | let width = abs(input.stroke); |
| 114 | if width > 0.0 { |
| 115 | coverage *= clamp(distance + width + 0.5, 0.0, 1.0); |
| 116 | } |
| 117 | if input.stroke < 0.0 { |
| 118 | var radius = max(input.shape.zw - width * 0.5, vec2<f32>(0.0)); |
| 119 | if any(radius == vec2<f32>(0.0)) { |
| 120 | radius = vec2<f32>(0.0); |
| 121 | } |
| 122 | let straight = input.shape.xy - width * 0.5 - radius; |
| 123 | let p = abs(input.local); |
| 124 | let quarter = straight.x + straight.y + ellipse_arc(1.57079632679, radius); |
| 125 | var along: f32; |
| 126 | if p.y <= straight.y { |
| 127 | along = p.y; |
| 128 | } else if p.x <= straight.x || radius.x == 0.0 { |
| 129 | along = quarter - p.x; |
| 130 | } else { |
| 131 | let angle = atan2((p.y - straight.y) / radius.y, (p.x - straight.x) / radius.x); |
| 132 | along = straight.y + ellipse_arc(angle, radius); |
| 133 | } |
| 134 | if input.local.x < 0.0 { |
| 135 | along = select(2.0 * quarter - along, 2.0 * quarter + along, input.local.y < 0.0); |
| 136 | } else if input.local.y < 0.0 { |
| 137 | along = 4.0 * quarter - along; |
| 138 | } |
| 139 | let phase = fract(along / (4.0 * width)) * (4.0 * width); |
| 140 | coverage *= clamp(width + 0.5 - abs(phase - 2.0 * width), 0.0, 1.0); |
| 141 | } |
| 142 | color *= coverage; |
| 143 | } |
| 144 | if input.clip.x > 0.0 { |
| 145 | let q = abs(input.clip_local) - input.clip.xy + input.clip.z; |
| 146 | let distance = length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - input.clip.z; |
| 147 | color *= clamp(0.5 - distance, 0.0, 1.0); |
| 148 | } |
| 149 | return color; |
| 150 | } |