1// GLSL 1.20 port of crates/draw/src/draw.wgsl. GLSL 1.20 has no flat interpolation, so
2// the per-quad shape and stroke arrive interpolated between equal values: equality tests
3// on them use a tolerance.
4#ifdef VERTEX
5attribute vec2 position;
6attribute vec2 uv;
7attribute vec4 color;
8attribute vec2 local;
9attribute vec4 shape;
10attribute float stroke;
11attribute vec2 clip_local;
12attribute vec3 clip;
13attribute float blur;
14#endif
15varying vec2 v_uv;
16varying vec4 v_color;
17varying vec2 v_local;
18varying vec4 v_shape;
19varying float v_stroke;
20varying vec2 v_clip_local;
21varying vec3 v_clip;
22varying float v_blur;
23
24#ifdef VERTEX
25void main() {
26 v_uv = uv;
27 v_color = color;
28 v_local = local;
29 v_shape = shape;
30 v_stroke = stroke;
31 v_clip_local = clip_local;
32 v_clip = clip;
33 v_blur = blur;
34 gl_Position = vec4(position, 0.0, 1.0);
35}
36#else
37uniform sampler2D atlas;
38
39bool same(float a, float b) {
40 return abs(a - b) <= 1e-4 * max(1.0, max(abs(a), abs(b)));
41}
42
43float ellipse_arc(float angle, vec2 radius) {
44 if (same(radius.x, radius.y)) {
45 return angle * radius.x;
46 }
47 // Four-point Gauss-Legendre quadrature.
48 vec4 nodes = vec4(-0.86113631, -0.33998104, 0.33998104, 0.86113631);
49 vec4 weights = vec4(0.34785485, 0.65214515, 0.65214515, 0.34785485);
50 float half_angle = angle * 0.5;
51 float scale = max(radius.x, radius.y);
52 float sum = 0.0;
53 for (int i = 0; i < 4; i++) {
54 float t = half_angle * (nodes[i] + 1.0);
55 sum += weights[i] * length((radius / scale) * vec2(sin(t), cos(t)));
56 }
57 return half_angle * sum * scale;
58}
59
60vec2 erf(vec2 x) {
61 vec2 s = sign(x);
62 vec2 a = abs(x);
63 vec2 y = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
64 y *= y;
65 return s - s / (y * y);
66}
67
68float shadow(vec2 p, vec2 half_size, float corner, float sigma) {
69 float start = clamp(-3.0 * sigma, p.y - half_size.y, p.y + half_size.y);
70 float end = clamp(3.0 * sigma, p.y - half_size.y, p.y + half_size.y);
71 float stride = (end - start) / 4.0;
72 float y = start + stride * 0.5;
73 float value = 0.0;
74 for (int i = 0; i < 4; i++) {
75 float delta = min(half_size.y - corner - abs(p.y - y), 0.0);
76 float curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
77 vec2 integral = 0.5 + 0.5 * erf((p.x + vec2(-curved, curved)) * (0.70710678 / sigma));
78 float weight = exp(-y * y / (2.0 * sigma * sigma)) / (2.50662827 * sigma);
79 value += (integral.y - integral.x) * weight * stride;
80 y += stride;
81 }
82 return value;
83}
84
85// Distance from `p` to the tapered capsule from (-h, 0) to (h, 0) with round ends of radii
86// `r0` and `r1` (Inigo Quilez's uneven capsule).
87float taper(vec2 p, float h, float r0, float r1) {
88 vec2 q = vec2(abs(p.y), p.x + h);
89 float span = 2.0 * h;
90 float b = (r0 - r1) / max(span, 1e-6);
91 float far = length(q - vec2(0.0, span)) - r1;
92 if (abs(b) >= 1.0) {
93 return min(length(q) - r0, far);
94 }
95 float a = sqrt(1.0 - b * b);
96 float k = dot(q, vec2(-b, a));
97 if (k < 0.0) {
98 return length(q) - r0;
99 }
100 if (k > a * span) {
101 return far;
102 }
103 return dot(q, vec2(a, b)) - r0;
104}
105
106void main() {
107 vec4 color = texture2D(atlas, v_uv) * v_color;
108 vec4 shape = v_shape;
109 if (v_blur > 1e-6) {
110 color *= shadow(v_local, shape.xy, shape.z, v_blur);
111 } else if (v_blur < -0.5) {
112 color *= clamp(0.5 - taper(v_local, shape.x, shape.y, shape.z), 0.0, 1.0);
113 } else if (shape.x > 0.0 && shape.y > 0.0) {
114 vec2 q = abs(v_local) - shape.xy + shape.zw;
115 float distance;
116 if (same(shape.z, shape.w)) {
117 distance = length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - shape.z;
118 } else if (all(greaterThan(q, vec2(0.0)))) {
119 vec2 radius = max(shape.zw, vec2(0.0001));
120 vec2 normalized = q / radius;
121 float k0 = length(normalized);
122 distance = k0 * (k0 - 1.0) / max(length(normalized / radius), 1e-20);
123 } else {
124 vec2 edge = abs(v_local) - shape.xy;
125 distance = max(edge.x, edge.y);
126 }
127 float coverage = clamp(0.5 - distance, 0.0, 1.0);
128 float width = abs(v_stroke);
129 if (width > 1e-6) {
130 coverage *= clamp(distance + width + 0.5, 0.0, 1.0);
131 }
132 if (v_stroke < -1e-6) {
133 vec2 radius = max(shape.zw - width * 0.5, vec2(0.0));
134 if (radius.x <= 1e-6 || radius.y <= 1e-6) {
135 radius = vec2(0.0);
136 }
137 vec2 straight = shape.xy - width * 0.5 - radius;
138 vec2 p = abs(v_local);
139 float quarter = straight.x + straight.y + ellipse_arc(1.57079632679, radius);
140 float along;
141 if (p.y <= straight.y) {
142 along = p.y;
143 } else if (p.x <= straight.x || radius.x == 0.0) {
144 along = quarter - p.x;
145 } else {
146 float angle = atan((p.y - straight.y) / radius.y, (p.x - straight.x) / radius.x);
147 along = straight.y + ellipse_arc(angle, radius);
148 }
149 if (v_local.x < 0.0) {
150 along = v_local.y < 0.0 ? 2.0 * quarter + along : 2.0 * quarter - along;
151 } else if (v_local.y < 0.0) {
152 along = 4.0 * quarter - along;
153 }
154 float phase = fract(along / (4.0 * width)) * (4.0 * width);
155 coverage *= clamp(width + 0.5 - abs(phase - 2.0 * width), 0.0, 1.0);
156 }
157 color *= coverage;
158 }
159 if (v_clip.x > 0.0) {
160 vec2 q = abs(v_clip_local) - v_clip.xy + v_clip.z;
161 float distance = length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - v_clip.z;
162 color *= clamp(0.5 - distance, 0.0, 1.0);
163 }
164 gl_FragColor = color;
165}
166#endif