libgui 2.01
libgui is a GUI library for the GameCube, Wii, and Wii U
 
Loading...
Searching...
No Matches
GuiGeometry.cpp
1/****************************************************************************
2 * libgui
3 * Daryl Borth 2026
4 * GuiGeometry.cpp
5 ***************************************************************************/
6
7#include "GuiGeometry.h"
8#include <cmath>
9#include <algorithm>
10#include <functional>
11
12namespace {
13
14const float kPi = 3.14159265358979f;
15
16float clampf(float v, float lo, float hi)
17{
18 return v < lo ? lo : (v > hi ? hi : v);
19}
20
21//A closed outline. Every ring produced for one shape has the same vertex count,
22//so two rings (say, offset -0.5 and +0.5) can be joined into a band.
23struct Ring {
24 std::vector<GuiPoint> pts;
25};
26
27typedef std::function<Ring(float)> RingFn; //offset from the shape's edge (+ outward) -> ring
28
29//Number of segments for a quarter circle of radius r within the tolerance
30int arcCount(float radius, float tol)
31{
32 if(radius <= tol)
33 return 1;
34 float ang = acosf(clampf(1.0f - tol / radius, -1.0f, 1.0f));
35 if(ang < 1e-4f)
36 return 24;
37 return (int)clampf(ceilf((kPi * 0.5f) / ang), 2.0f, 24.0f);
38}
39
40//Number of segments for a full circle/ellipse of the given largest radius
41int circleCount(float radius, float tol)
42{
43 if(radius <= tol)
44 return 8;
45 float ang = acosf(clampf(1.0f - tol / radius, -1.0f, 1.0f));
46 if(ang < 1e-4f)
47 return 96;
48 return (int)clampf(ceilf((2.0f * kPi) / ang), 12.0f, 96.0f);
49}
50
51//Evaluates a GuiFill at a point. A gradient is linear in position, so
52//evaluating it per vertex and letting the GPU interpolate is exact.
53struct FillEval {
54 PixelColor from, to;
55 bool gradient;
56 float ux, uy, t0, invSpan;
57
58 FillEval(const GuiFill& f, const GuiRect& b) :
59 from(f.from), to(f.to), gradient(f.gradient), ux(0), uy(0), t0(0), invSpan(0)
60 {
61 if(!gradient)
62 return;
63 float rad = f.angle * kPi / 180.0f;
64 ux = cosf(rad);
65 uy = sinf(rad);
66 float d0 = b.x * ux + b.y * uy;
67 float d1 = (b.x + b.w) * ux + b.y * uy;
68 float d2 = b.x * ux + (b.y + b.h) * uy;
69 float d3 = (b.x + b.w) * ux + (b.y + b.h) * uy;
70 float lo = std::min(std::min(d0, d1), std::min(d2, d3));
71 float hi = std::max(std::max(d0, d1), std::max(d2, d3));
72 t0 = lo;
73 invSpan = (hi - lo) > 1e-6f ? 1.0f / (hi - lo) : 0.0f;
74 }
75
76 static uint8_t mix(uint8_t a, uint8_t b, float t)
77 {
78 return (uint8_t)clampf(a + (b - a) * t + 0.5f, 0.0f, 255.0f);
79 }
80
81 PixelColor at(GuiPoint p, float alphaScale) const
82 {
83 PixelColor c = from;
84 if(gradient)
85 {
86 float t = clampf((p.x * ux + p.y * uy - t0) * invSpan, 0.0f, 1.0f);
87 c.r = mix(from.r, to.r, t);
88 c.g = mix(from.g, to.g, t);
89 c.b = mix(from.b, to.b, t);
90 c.a = mix(from.a, to.a, t);
91 }
92 c.a = (uint8_t)clampf(c.a * alphaScale + 0.5f, 0.0f, 255.0f);
93 return c;
94 }
95};
96
97void pushVertex(std::vector<MeshVertex>& out, GuiPoint p, const PixelColor& c)
98{
99 MeshVertex v;
100 v.x = p.x;
101 v.y = p.y;
102 v.c = c;
103 out.push_back(v);
104}
105
106//Pushes one triangle, wound the same way as every other (positive area = clockwise on screen). A corner that
107//is sharper than the stroke is wide folds a sliver of the band over itself; the sliver still covers the same
108//pixels, but it must not be wound the other way for a backend that culls.
109void pushTri(std::vector<MeshVertex>& out, const GuiPoint& a, PixelColor ca, const GuiPoint& b, PixelColor cb, const GuiPoint& c, PixelColor cc)
110{
111 float s = (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
112 pushVertex(out, a, ca);
113 if(s < 0.0f)
114 {
115 pushVertex(out, c, cc);
116 pushVertex(out, b, cb);
117 }
118 else
119 {
120 pushVertex(out, b, cb);
121 pushVertex(out, c, cc);
122 }
123}
124
125
126//Triangles centre -> ring[i] -> ring[i+1] (closed), all at one alpha scale
127void emitFan(std::vector<MeshVertex>& out, GuiPoint centre, const Ring& r, const FillEval& e, float a)
128{
129 size_t n = r.pts.size();
130 if(n < 3)
131 return;
132 PixelColor cc = e.at(centre, a);
133 for(size_t i = 0; i < n; i++)
134 {
135 const GuiPoint& p0 = r.pts[i];
136 const GuiPoint& p1 = r.pts[(i + 1) % n];
137 pushTri(out, centre, cc, p0, e.at(p0, a), p1, e.at(p1, a));
138 }
139}
140
141//Quads between ring A and the (outer) ring B: one alpha scale per ring
142void emitBand(std::vector<MeshVertex>& out, const Ring& A, float aA, const Ring& B, float aB, const FillEval& e)
143{
144 size_t n = A.pts.size();
145 if(n < 3 || B.pts.size() != n)
146 return;
147 for(size_t i = 0; i < n; i++)
148 {
149 size_t j = (i + 1) % n;
150 PixelColor ca0 = e.at(A.pts[i], aA), ca1 = e.at(A.pts[j], aA);
151 PixelColor cb0 = e.at(B.pts[i], aB), cb1 = e.at(B.pts[j], aB);
152 pushTri(out, A.pts[i], ca0, B.pts[i], cb0, B.pts[j], cb1);
153 pushTri(out, A.pts[i], ca0, B.pts[j], cb1, A.pts[j], ca1);
154 }
155}
156
157Ring roundRectRing(const GuiRect& r, const GuiCorners& c, float d, int n)
158{
159 Ring ring;
160 float half = 0.5f * std::min(r.w, r.h);
161 d = std::max(d, -half); //never invert the rectangle
162 float rad[4] = {
163 clampf(c.tl, 0.0f, half), clampf(c.tr, 0.0f, half),
164 clampf(c.br, 0.0f, half), clampf(c.bl, 0.0f, half)
165 };
166 GuiPoint centre[4] = {
167 { r.x + rad[0], r.y + rad[0] },
168 { r.x + r.w - rad[1], r.y + rad[1] },
169 { r.x + r.w - rad[2], r.y + r.h - rad[2] },
170 { r.x + rad[3], r.y + r.h - rad[3] }
171 };
172 //The corner of the offset rectangle, used where the offset radius would go negative
173 GuiPoint sharp[4] = {
174 { r.x - d, r.y - d },
175 { r.x + r.w + d, r.y - d },
176 { r.x + r.w + d, r.y + r.h + d },
177 { r.x - d, r.y + r.h + d }
178 };
179 const float start[4] = { 180.0f, 270.0f, 0.0f, 90.0f };
180
181 ring.pts.reserve(4 * (n + 1));
182 for(int k = 0; k < 4; k++)
183 {
184 float rr = rad[k] + d;
185 for(int i = 0; i <= n; i++)
186 {
187 if(rr >= 0.0f)
188 {
189 float th = (start[k] + 90.0f * (float)i / (float)n) * kPi / 180.0f;
190 GuiPoint p = { centre[k].x + rr * cosf(th), centre[k].y + rr * sinf(th) };
191 ring.pts.push_back(p);
192 }
193 else
194 ring.pts.push_back(sharp[k]);
195 }
196 }
197 return ring;
198}
199
200Ring ellipseRing(const GuiRect& r, float d, int count)
201{
202 Ring ring;
203 float cx = r.x + r.w * 0.5f, cy = r.y + r.h * 0.5f;
204 float rx = std::max(0.0f, r.w * 0.5f + d), ry = std::max(0.0f, r.h * 0.5f + d);
205 ring.pts.reserve(count);
206 for(int i = 0; i < count; i++)
207 {
208 float t = 2.0f * kPi * (float)i / (float)count;
209 GuiPoint p = { cx + rx * cosf(t), cy + ry * sinf(t) };
210 ring.pts.push_back(p);
211 }
212 return ring;
213}
214
215//A convex polygon with a miter vector per corner, so any offset ring is p + d*m
216struct Poly {
217 std::vector<GuiPoint> p;
218 std::vector<GuiPoint> m;
219 std::vector<GuiPoint> edge; //per vertex: how far the miter moves it along the edge before it (x) and after it (y), per unit offset, and
220 std::vector<GuiPoint> len; //the lengths of those two edges; polyRing() uses them so an offset never folds a short edge over
221 GuiRect bounds;
222 GuiPoint centre;
223};
224
225bool buildPoly(const std::vector<GuiPoint>& in, Poly& out)
226{
227 out.p.clear();
228 out.m.clear();
229 out.edge.clear();
230 out.len.clear();
231 for(size_t i = 0; i < in.size(); i++)
232 {
233 const GuiPoint& q = in[i];
234 if(!out.p.empty() && fabsf(q.x - out.p.back().x) < 1e-4f && fabsf(q.y - out.p.back().y) < 1e-4f)
235 continue; //repeated point
236 out.p.push_back(q);
237 }
238 while(out.p.size() > 1 && fabsf(out.p.front().x - out.p.back().x) < 1e-4f && fabsf(out.p.front().y - out.p.back().y) < 1e-4f)
239 out.p.pop_back();
240 size_t n = out.p.size();
241 if(n < 3)
242 return false;
243
244 float area = 0.0f; //positive = clockwise on screen (+Y down)
245 for(size_t i = 0; i < n; i++)
246 {
247 const GuiPoint& a = out.p[i];
248 const GuiPoint& b = out.p[(i + 1) % n];
249 area += a.x * b.y - b.x * a.y;
250 }
251 if(fabsf(area) < 1e-6f)
252 return false;
253 if(area < 0.0f)
254 std::reverse(out.p.begin(), out.p.end());
255
256 float minx = out.p[0].x, maxx = minx, miny = out.p[0].y, maxy = miny, sx = 0, sy = 0;
257 for(size_t i = 0; i < n; i++)
258 {
259 minx = std::min(minx, out.p[i].x);
260 maxx = std::max(maxx, out.p[i].x);
261 miny = std::min(miny, out.p[i].y);
262 maxy = std::max(maxy, out.p[i].y);
263 sx += out.p[i].x;
264 sy += out.p[i].y;
265 }
266 out.bounds.x = minx;
267 out.bounds.y = miny;
268 out.bounds.w = maxx - minx;
269 out.bounds.h = maxy - miny;
270 out.centre.x = sx / (float)n;
271 out.centre.y = sy / (float)n;
272
273 for(size_t i = 0; i < n; i++)
274 {
275 const GuiPoint& a = out.p[(i + n - 1) % n];
276 const GuiPoint& b = out.p[i];
277 const GuiPoint& c = out.p[(i + 1) % n];
278 float e1x = b.x - a.x, e1y = b.y - a.y, l1 = sqrtf(e1x * e1x + e1y * e1y);
279 float e2x = c.x - b.x, e2y = c.y - b.y, l2 = sqrtf(e2x * e2x + e2y * e2y);
280 float n1x = e1y / l1, n1y = -e1x / l1; //outward normal of a clockwise-on-screen edge
281 float n2x = e2y / l2, n2y = -e2x / l2;
282 float denom = 1.0f + n1x * n2x + n1y * n2y;
283 if(denom < 0.25f)
284 denom = 0.25f; //limit the miter on very sharp corners
285 GuiPoint m = { (n1x + n2x) / denom, (n1y + n2y) / denom };
286 out.m.push_back(m);
287 GuiPoint along = { (m.x * e1x + m.y * e1y) / l1, (m.x * e2x + m.y * e2y) / l2 };
288 GuiPoint lens = { l1, l2 };
289 out.edge.push_back(along);
290 out.len.push_back(lens);
291 }
292 return true;
293}
294
295Ring polyRing(const Poly& poly, float d)
296{
297 Ring ring;
298 ring.pts.reserve(poly.p.size());
299 for(size_t i = 0; i < poly.p.size(); i++)
300 {
301 // A corner next to a very short edge (the start of a curve) would be pushed past the edge's far end and fold it
302 // over. Moving no further than half an edge toward a neighbour keeps every offset edge the right way round.
303 float k = 1.0f;
304 float toNext = d * poly.edge[i].y;
305 float toPrev = -d * poly.edge[i].x;
306 if(toNext > 0.5f * poly.len[i].y)
307 k = 0.5f * poly.len[i].y / toNext;
308 if(toPrev > 0.5f * poly.len[i].x)
309 k = std::min(k, 0.5f * poly.len[i].x / toPrev);
310 GuiPoint q = { poly.p[i].x + k * d * poly.m[i].x, poly.p[i].y + k * d * poly.m[i].y };
311 ring.pts.push_back(q);
312 }
313 return ring;
314}
315
316float smoothstep01(float t)
317{
318 t = clampf(t, 0.0f, 1.0f);
319 return t * t * (3.0f - 2.0f * t);
320}
321
322
323//True when every corner turns the same way (collinear points are allowed)
324bool isConvex(const std::vector<GuiPoint>& p)
325{
326 size_t n = p.size();
327 float sign = 0.0f;
328 for(size_t i = 0; i < n; i++)
329 {
330 const GuiPoint& a = p[i];
331 const GuiPoint& b = p[(i + 1) % n];
332 const GuiPoint& c = p[(i + 2) % n];
333 float cr = (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x);
334 if(fabsf(cr) < 1e-6f)
335 continue;
336 if(sign == 0.0f)
337 sign = cr > 0.0f ? 1.0f : -1.0f;
338 else if((cr > 0.0f ? 1.0f : -1.0f) != sign)
339 return false;
340 }
341 return true;
342}
343
344bool inTriangle(const GuiPoint& p, const GuiPoint& a, const GuiPoint& b, const GuiPoint& c, float sgn)
345{
346 float d1 = ((b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x)) * sgn;
347 float d2 = ((c.x - b.x) * (p.y - b.y) - (c.y - b.y) * (p.x - b.x)) * sgn;
348 float d3 = ((a.x - c.x) * (p.y - c.y) - (a.y - c.y) * (p.x - c.x)) * sgn;
349 return d1 >= 0.0f && d2 >= 0.0f && d3 >= 0.0f;
350}
351
352//Ear-clipping triangulation of a simple polygon (either winding). Appends index triples to `tri`.
353//Collinear corners are dropped. If rounding leaves no ear to clip (a polygon that nearly touches
354//itself), the rest is fanned so the call always finishes.
355void triangulate(const std::vector<GuiPoint>& p, std::vector<size_t>& tri)
356{
357 size_t n = p.size();
358 if(n < 3)
359 return;
360 float area = 0.0f;
361 for(size_t i = 0; i < n; i++)
362 {
363 const GuiPoint& a = p[i];
364 const GuiPoint& b = p[(i + 1) % n];
365 area += a.x * b.y - b.x * a.y;
366 }
367 const float sgn = area >= 0.0f ? 1.0f : -1.0f;
368
369 std::vector<size_t> idx(n);
370 for(size_t i = 0; i < n; i++)
371 idx[i] = i;
372
373 size_t i = 0, sinceClip = 0;
374 while(idx.size() > 3)
375 {
376 size_t m = idx.size();
377 if(sinceClip >= m)
378 {
379 //no ear anywhere: fan what is left
380 for(size_t k = 1; k + 1 < m; k++)
381 {
382 tri.push_back(idx[0]);
383 tri.push_back(idx[k]);
384 tri.push_back(idx[k + 1]);
385 }
386 return;
387 }
388 i %= m;
389 const size_t ia = idx[(i + m - 1) % m], ib = idx[i], ic = idx[(i + 1) % m];
390 const GuiPoint &a = p[ia], &b = p[ib], &c = p[ic];
391 float cr = ((b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x)) * sgn;
392 bool clip = false, emit = false;
393 if(fabsf(cr) < 1e-6f)
394 clip = true; //collinear: no area, just remove it
395 else if(cr > 0.0f)
396 {
397 bool empty = true;
398 for(size_t k = 0; k < m && empty; k++)
399 {
400 size_t ik = idx[k];
401 if(ik == ia || ik == ib || ik == ic)
402 continue;
403 const GuiPoint& q = p[ik];
404 if((q.x == a.x && q.y == a.y) || (q.x == b.x && q.y == b.y) || (q.x == c.x && q.y == c.y))
405 continue;
406 if(inTriangle(q, a, b, c, sgn))
407 empty = false;
408 }
409 if(empty)
410 clip = emit = true;
411 }
412 if(clip)
413 {
414 if(emit)
415 {
416 tri.push_back(ia);
417 tri.push_back(ib);
418 tri.push_back(ic);
419 }
420 idx.erase(idx.begin() + (long)i);
421 sinceClip = 0;
422 }
423 else
424 {
425 i++;
426 sinceClip++;
427 }
428 }
429 if(idx.size() == 3)
430 {
431 tri.push_back(idx[0]);
432 tri.push_back(idx[1]);
433 tri.push_back(idx[2]);
434 }
435}
436
437//Solid fill of a concave polygon: the interior (inset by feather/2) as ear-clipped triangles, then the
438//same anti-aliasing band as fillShape()
439void fillConcave(std::vector<MeshVertex>& out, float feather, const Poly& poly, const GuiFill& fill, bool edgeFringe)
440{
441 FillEval ev(fill, poly.bounds);
442 const float h = feather * 0.5f;
443 Ring inner = polyRing(poly, -h);
444 std::vector<size_t> tri;
445 triangulate(inner.pts, tri);
446 for(size_t k = 0; k + 2 < tri.size(); k += 3)
447 {
448 const GuiPoint &a = inner.pts[tri[k]], &b = inner.pts[tri[k + 1]], &c = inner.pts[tri[k + 2]];
449 pushTri(out, a, ev.at(a, 1.0f), b, ev.at(b, 1.0f), c, ev.at(c, 1.0f));
450 }
451 if(feather > 0.0f && edgeFringe)
452 emitBand(out, inner, 1.0f, polyRing(poly, h), 0.0f, ev);
453}
454
455} // namespace
456
458{
459 GuiFill f;
460 f.from = c;
461 f.to = c;
462 f.angle = 0.0f;
463 f.gradient = false;
464 return f;
465}
466
468{
469 GuiFill f;
470 f.from = a;
471 f.to = b;
472 f.angle = angleDeg;
473 f.gradient = true;
474 return f;
475}
476
477GuiMesh::GuiMesh(float featherWidth, float curveTolerance) :
478 feather(featherWidth < 0.0f ? 0.0f : featherWidth),
479 tolerance(curveTolerance < 0.01f ? 0.01f : curveTolerance)
480{}
481
483{
484 verts.clear();
485}
486
487//Solid fill with an anti-aliasing fringe centered on the edge: opaque out to
488//-feather/2, fading to alpha 0 at +feather/2.
489static void fillShape(std::vector<MeshVertex>& out, float feather, const RingFn& ring, GuiPoint centre, const GuiRect& bounds, const GuiFill& fill, bool edgeFringe)
490{
491 FillEval ev(fill, bounds);
492 float h = feather * 0.5f;
493 Ring inner = ring(-h);
494 emitFan(out, centre, inner, ev, 1.0f);
495 if(feather > 0.0f && edgeFringe)
496 emitBand(out, inner, 1.0f, ring(h), 0.0f, ev);
497}
498
499//A stroke between two offsets from the edge, feathered on both sides. A stroke
500//thinner than the feather is drawn as a fainter, wider line, so its total
501//coverage stays equal to its width.
502static void strokeShape(std::vector<MeshVertex>& out, float feather, const RingFn& ring, const GuiRect& bounds, float width, STROKE align, const GuiFill& fill)
503{
504 if(width <= 0.0f)
505 return;
506 float d1, d2;
507 switch(align)
508 {
509 case STROKE::INSIDE: d1 = -width; d2 = 0.0f; break;
510 case STROKE::OUTSIDE: d1 = 0.0f; d2 = width; break;
511 default: d1 = -width * 0.5f; d2 = width * 0.5f; break;
512 }
513 FillEval ev(fill, bounds);
514 float h = feather * 0.5f;
515 if(feather <= 0.0f)
516 {
517 emitBand(out, ring(d1), 1.0f, ring(d2), 1.0f, ev);
518 return;
519 }
520 if(width < feather)
521 {
522 //Triangle profile whose area equals the width: peak alpha 2w/f over a feather-wide base,
523 //or full alpha over a base of 2w once that would exceed 1
524 float mid = (d1 + d2) * 0.5f;
525 float a = std::min(1.0f, 2.0f * width / feather);
526 float hb = width / a; //half of the base width
527 Ring rm = ring(mid);
528 emitBand(out, ring(mid - hb), 0.0f, rm, a, ev);
529 emitBand(out, rm, a, ring(mid + hb), 0.0f, ev);
530 return;
531 }
532 Ring a0 = ring(d1 - h), a1 = ring(d1 + h), b1 = ring(d2 - h), b0 = ring(d2 + h);
533 emitBand(out, a0, 0.0f, a1, 1.0f, ev);
534 emitBand(out, a1, 1.0f, b1, 1.0f, ev);
535 emitBand(out, b1, 1.0f, b0, 0.0f, ev);
536}
537
538void GuiMesh::addRect(const GuiRect& r, const GuiFill& fill, bool edgeFringe)
539{
540 std::vector<GuiPoint> pts(4);
541 pts[0].x = r.x; pts[0].y = r.y;
542 pts[1].x = r.x + r.w; pts[1].y = r.y;
543 pts[2].x = r.x + r.w; pts[2].y = r.y + r.h;
544 pts[3].x = r.x; pts[3].y = r.y + r.h;
545 addPolygon(pts, fill, edgeFringe);
546}
547
548void GuiMesh::addRoundRect(const GuiRect& r, const GuiCorners& c, const GuiFill& fill, bool edgeFringe)
549{
550 if(r.w <= 0.0f || r.h <= 0.0f)
551 return;
552 if(c.tl <= 0.0f && c.tr <= 0.0f && c.br <= 0.0f && c.bl <= 0.0f)
553 {
554 addRect(r, fill, edgeFringe);
555 return;
556 }
557 float maxR = std::max(std::max(c.tl, c.tr), std::max(c.br, c.bl));
558 maxR = std::min(maxR, 0.5f * std::min(r.w, r.h));
559 int n = arcCount(maxR + feather, tolerance);
560 GuiPoint centre = { r.x + r.w * 0.5f, r.y + r.h * 0.5f };
561 RingFn ring = [=](float d) { return roundRectRing(r, c, d, n); };
562 fillShape(verts, feather, ring, centre, r, fill, edgeFringe);
563}
564
565void GuiMesh::addEllipse(const GuiRect& r, const GuiFill& fill, bool edgeFringe)
566{
567 if(r.w <= 0.0f || r.h <= 0.0f)
568 return;
569 int n = circleCount(0.5f * std::max(r.w, r.h) + feather, tolerance);
570 GuiPoint centre = { r.x + r.w * 0.5f, r.y + r.h * 0.5f };
571 RingFn ring = [=](float d) { return ellipseRing(r, d, n); };
572 fillShape(verts, feather, ring, centre, r, fill, edgeFringe);
573}
574
575void GuiMesh::addPolygon(const std::vector<GuiPoint>& pts, const GuiFill& fill, bool edgeFringe)
576{
577 Poly poly;
578 if(!buildPoly(pts, poly))
579 return;
580 if(!isConvex(poly.p))
581 {
582 fillConcave(verts, feather, poly, fill, edgeFringe);
583 return;
584 }
585 RingFn ring = [&poly](float d) { return polyRing(poly, d); };
586 fillShape(verts, feather, ring, poly.centre, poly.bounds, fill, edgeFringe);
587}
588
589void GuiMesh::addLine(GuiPoint a, GuiPoint b, float width, const GuiFill& fill)
590{
591 float dx = b.x - a.x, dy = b.y - a.y;
592 float len = sqrtf(dx * dx + dy * dy);
593 if(len < 1e-4f || width <= 0.0f)
594 return;
595 float nx = dy / len * width * 0.5f, ny = -dx / len * width * 0.5f;
596 std::vector<GuiPoint> pts(4);
597 pts[0].x = a.x + nx; pts[0].y = a.y + ny;
598 pts[1].x = b.x + nx; pts[1].y = b.y + ny;
599 pts[2].x = b.x - nx; pts[2].y = b.y - ny;
600 pts[3].x = a.x - nx; pts[3].y = a.y - ny;
601 addPolygon(pts, fill);
602}
603
604void GuiMesh::strokeRoundRect(const GuiRect& r, const GuiCorners& c, float width, STROKE align, const GuiFill& fill)
605{
606 if(r.w <= 0.0f || r.h <= 0.0f || width <= 0.0f)
607 return;
608 float half = 0.5f * std::min(r.w, r.h);
609 if(align == STROKE::INSIDE)
610 width = std::min(width, half);
611 else if(align == STROKE::CENTRE)
612 width = std::min(width, 2.0f * half);
613 float maxR = std::min(std::max(std::max(c.tl, c.tr), std::max(c.br, c.bl)), half);
614 int n = arcCount(maxR + width + feather, tolerance);
615 RingFn ring = [=](float d) { return roundRectRing(r, c, d, n); };
616 strokeShape(verts, feather, ring, r, width, align, fill);
617}
618
619void GuiMesh::strokeEllipse(const GuiRect& r, float width, STROKE align, const GuiFill& fill)
620{
621 if(r.w <= 0.0f || r.h <= 0.0f || width <= 0.0f)
622 return;
623 float half = 0.5f * std::min(r.w, r.h);
624 if(align == STROKE::INSIDE)
625 width = std::min(width, half);
626 else if(align == STROKE::CENTRE)
627 width = std::min(width, 2.0f * half);
628 int n = circleCount(0.5f * std::max(r.w, r.h) + width + feather, tolerance);
629 RingFn ring = [=](float d) { return ellipseRing(r, d, n); };
630 strokeShape(verts, feather, ring, r, width, align, fill);
631}
632
633void GuiMesh::strokePolygon(const std::vector<GuiPoint>& pts, float width, STROKE align, const GuiFill& fill)
634{
635 Poly poly;
636 if(width <= 0.0f || !buildPoly(pts, poly))
637 return;
638 RingFn ring = [&poly](float d) { return polyRing(poly, d); };
639 strokeShape(verts, feather, ring, poly.bounds, width, align, fill);
640}
641
642void GuiMesh::strokeArc(GuiPoint centre, float radius, float startDeg, float sweepDeg, float width, PixelColor color)
643{
644 if(radius <= 0.0f || width <= 0.0f || sweepDeg == 0.0f)
645 return;
646 if(sweepDeg < 0.0f)
647 {
648 startDeg += sweepDeg;
649 sweepDeg = -sweepDeg;
650 }
651 sweepDeg = std::min(sweepDeg, 360.0f);
652
653 float h = feather * 0.5f;
654 float radii[4];
655 float alphas[4];
656 int rings;
657 if(feather > 0.0f && width < feather)
658 {
659 float a = std::min(1.0f, 2.0f * width / feather);
660 float hb = width / a;
661 radii[0] = radius - hb; alphas[0] = 0.0f;
662 radii[1] = radius; alphas[1] = a;
663 radii[2] = radius + hb; alphas[2] = 0.0f;
664 rings = 3;
665 }
666 else if(feather > 0.0f)
667 {
668 radii[0] = radius - width * 0.5f - h; alphas[0] = 0.0f;
669 radii[1] = radius - width * 0.5f + h; alphas[1] = 1.0f;
670 radii[2] = radius + width * 0.5f - h; alphas[2] = 1.0f;
671 radii[3] = radius + width * 0.5f + h; alphas[3] = 0.0f;
672 rings = 4;
673 }
674 else
675 {
676 radii[0] = radius - width * 0.5f; alphas[0] = 1.0f;
677 radii[1] = radius + width * 0.5f; alphas[1] = 1.0f;
678 rings = 2;
679 }
680
681 float outer = radius + width * 0.5f + h;
682 float step = acosf(clampf(1.0f - tolerance / outer, -1.0f, 1.0f));
683 if(step < 1e-4f)
684 step = 1e-4f;
685 int segs = (int)clampf(ceilf((sweepDeg * kPi / 180.0f) / step), 1.0f, 128.0f);
686
687 GuiRect bounds = { centre.x - outer, centre.y - outer, 2.0f * outer, 2.0f * outer };
688 FillEval ev(GuiFill::solid(color), bounds);
689
690 std::vector<Ring> samples((size_t)rings);
691 for(int k = 0; k < rings; k++)
692 {
693 for(int i = 0; i <= segs; i++)
694 {
695 float th = (startDeg + sweepDeg * (float)i / (float)segs) * kPi / 180.0f;
696 GuiPoint p = { centre.x + radii[k] * cosf(th), centre.y + radii[k] * sinf(th) };
697 samples[(size_t)k].pts.push_back(p);
698 }
699 }
700 for(int k = 0; k + 1 < rings; k++)
701 {
702 const Ring& A = samples[(size_t)k];
703 const Ring& B = samples[(size_t)k + 1];
704 for(int i = 0; i < segs; i++)
705 {
706 PixelColor ca0 = ev.at(A.pts[i], alphas[k]), ca1 = ev.at(A.pts[i + 1], alphas[k]);
707 PixelColor cb0 = ev.at(B.pts[i], alphas[k + 1]), cb1 = ev.at(B.pts[i + 1], alphas[k + 1]);
708 pushTri(verts, A.pts[i], ca0, B.pts[i], cb0, B.pts[i + 1], cb1);
709 pushTri(verts, A.pts[i], ca0, B.pts[i + 1], cb1, A.pts[i + 1], ca1);
710 }
711 }
712}
713
714void GuiMesh::addShadow(const GuiRect& r, const GuiCorners& c, float blur, float spread, float dx, float dy, PixelColor color)
715{
716 if(r.w <= 0.0f || r.h <= 0.0f || color.a == 0)
717 return;
718 blur = std::max(blur, 0.0f);
719 GuiRect s = { r.x + dx, r.y + dy, r.w, r.h };
720 // One band per ~1.5px of blur is enough to follow the smoothstep falloff; a fixed six
721 // bands made a 4px key shadow cost as many triangles as a 26px dialog shadow
722 const int maxSteps = 6;
723 const int steps = (int)clampf(ceilf(blur / 1.5f), 2.0f, (float)maxSteps);
724 float maxR = std::min(std::max(std::max(c.tl, c.tr), std::max(c.br, c.bl)), 0.5f * std::min(r.w, r.h));
725 int n = arcCount(maxR + std::max(spread, 0.0f) + blur, tolerance * 2.0f);
726 FillEval ev(GuiFill::solid(color), s);
727 GuiPoint centre = { s.x + s.w * 0.5f, s.y + s.h * 0.5f };
728
729 std::vector<Ring> rings;
730 float alpha[maxSteps + 1];
731 for(int k = 0; k <= steps; k++)
732 {
733 float t = (float)k / (float)steps;
734 rings.push_back(roundRectRing(s, c, spread - blur * 0.5f + blur * t, n));
735 alpha[k] = 1.0f - smoothstep01(t);
736 }
737 emitFan(verts, centre, rings[0], ev, alpha[0]);
738 for(int k = 0; k < steps; k++)
739 emitBand(verts, rings[(size_t)k], alpha[k], rings[(size_t)k + 1], alpha[k + 1], ev);
740}
741
742std::vector<GuiPoint> GuiRoundRectPolygon(const GuiRect& r, float radius, float angleDeg, float tolerance)
743{
744 std::vector<GuiPoint> out;
745 if(r.w <= 0.0f || r.h <= 0.0f)
746 return out;
747
748 const float half = 0.5f * std::min(r.w, r.h);
749 const float rad = clampf(radius, 0.0f, half);
750 const int n = arcCount(rad, tolerance < 0.01f ? 0.01f : tolerance);
751 Ring ring = roundRectRing(r, GuiCorners(rad), 0.0f, n);
752
753 const float a = angleDeg * kPi / 180.0f;
754 const float c = cosf(a), s = sinf(a);
755 const float cx = r.x + r.w * 0.5f, cy = r.y + r.h * 0.5f;
756 out.reserve(ring.pts.size());
757 for(size_t i = 0; i < ring.pts.size(); i++)
758 {
759 const float dx = ring.pts[i].x - cx, dy = ring.pts[i].y - cy;
760 GuiPoint p = { cx + dx * c - dy * s, cy + dx * s + dy * c };
761 out.push_back(p);
762 }
763 return out;
764}
765
766
767namespace {
768
769//Appends the points of a cubic after its start point, split until the control points are within `tol` of the chord
770void flattenCubic(std::vector<GuiPoint>& out, GuiPoint p0, GuiPoint p1, GuiPoint p2, GuiPoint p3, float tol, int depth)
771{
772 float dx = p3.x - p0.x, dy = p3.y - p0.y;
773 float len2 = dx * dx + dy * dy;
774 float d1, d2;
775 if(len2 < 1e-12f)
776 {
777 d1 = sqrtf((p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y));
778 d2 = sqrtf((p2.x - p0.x) * (p2.x - p0.x) + (p2.y - p0.y) * (p2.y - p0.y));
779 }
780 else
781 {
782 float inv = 1.0f / sqrtf(len2);
783 d1 = fabsf((p1.x - p0.x) * dy - (p1.y - p0.y) * dx) * inv;
784 d2 = fabsf((p2.x - p0.x) * dy - (p2.y - p0.y) * dx) * inv;
785 }
786 //the curve stays within 3/4 of the larger control-point distance of its chord
787 if(depth >= 12 || 0.75f * std::max(d1, d2) <= tol)
788 {
789 out.push_back(p3);
790 return;
791 }
792 GuiPoint p01 = { (p0.x + p1.x) * 0.5f, (p0.y + p1.y) * 0.5f };
793 GuiPoint p12 = { (p1.x + p2.x) * 0.5f, (p1.y + p2.y) * 0.5f };
794 GuiPoint p23 = { (p2.x + p3.x) * 0.5f, (p2.y + p3.y) * 0.5f };
795 GuiPoint a = { (p01.x + p12.x) * 0.5f, (p01.y + p12.y) * 0.5f };
796 GuiPoint b = { (p12.x + p23.x) * 0.5f, (p12.y + p23.y) * 0.5f };
797 GuiPoint m = { (a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f };
798 flattenCubic(out, p0, p01, a, m, tol, depth + 1);
799 flattenCubic(out, m, b, p23, p3, tol, depth + 1);
800}
801
802} // namespace
803
804std::vector<std::vector<GuiPoint> > GuiFlattenPath(const GuiPath& path, float scale, float tolerance)
805{
806 std::vector<std::vector<GuiPoint> > contours;
807 std::vector<GuiPoint> cur;
808 if(tolerance < 0.001f)
809 tolerance = 0.001f;
810 size_t k = 0;
811 auto pt = [&](size_t i) { GuiPoint p = { path.pts[i * 2] * scale, path.pts[i * 2 + 1] * scale }; return p; };
812 auto finish = [&]() {
813 //a closing point equal to the start is redundant
814 if(cur.size() > 1 && fabsf(cur.front().x - cur.back().x) < 1e-5f && fabsf(cur.front().y - cur.back().y) < 1e-5f)
815 cur.pop_back();
816 if(cur.size() >= 3)
817 contours.push_back(cur);
818 cur.clear();
819 };
820 for(size_t c = 0; c < path.cmds.size(); c++)
821 {
822 switch(path.cmds[c])
823 {
824 case GuiPath::MOVE:
825 finish();
826 if((k + 1) * 2 > path.pts.size()) return contours;
827 cur.push_back(pt(k++));
828 break;
829 case GuiPath::LINE:
830 if((k + 1) * 2 > path.pts.size()) return contours;
831 cur.push_back(pt(k++));
832 break;
833 case GuiPath::CUBIC:
834 if((k + 3) * 2 > path.pts.size() || cur.empty()) return contours;
835 flattenCubic(cur, cur.back(), pt(k), pt(k + 1), pt(k + 2), tolerance, 0);
836 k += 3;
837 break;
838 case GuiPath::CLOSE:
839 finish();
840 break;
841 default:
842 break;
843 }
844 }
845 finish();
846 return contours;
847}
848
849GuiRect GuiPathBounds(const GuiPath& path, float scale)
850{
851 GuiRect r = { 0, 0, 0, 0 };
852 if(path.pts.size() < 2)
853 return r;
854 float minx = path.pts[0], maxx = minx, miny = path.pts[1], maxy = miny;
855 for(size_t i = 0; i + 1 < path.pts.size(); i += 2)
856 {
857 minx = std::min(minx, path.pts[i]);
858 maxx = std::max(maxx, path.pts[i]);
859 miny = std::min(miny, path.pts[i + 1]);
860 maxy = std::max(maxy, path.pts[i + 1]);
861 }
862 r.x = minx * scale;
863 r.y = miny * scale;
864 r.w = (maxx - minx) * scale;
865 r.h = (maxy - miny) * scale;
866 return r;
867}
void addShadow(const GuiRect &r, const GuiCorners &corners, float blur, float spread, float dx, float dy, PixelColor color)
Adds a soft drop shadow or glow for a rounded rectangle.
void clear()
Removes all geometry (keeps the feather and tolerance settings)
GuiMesh(float featherWidth=1.0f, float curveTolerance=0.3f)
Constructor.
void addLine(GuiPoint a, GuiPoint b, float width, const GuiFill &fill)
Adds a straight line with flat ends.
void strokePolygon(const std::vector< GuiPoint > &pts, float width, STROKE align, const GuiFill &fill)
Adds a polygon outline with mitered corners (the miter is capped on very sharp corners).
void strokeEllipse(const GuiRect &r, float width, STROKE align, const GuiFill &fill)
Adds an ellipse outline.
void addRoundRect(const GuiRect &r, const GuiCorners &corners, const GuiFill &fill, bool edgeFringe=true)
Adds a filled rounded rectangle.
void strokeArc(GuiPoint centre, float radius, float startDeg, float sweepDeg, float width, PixelColor color)
Adds an arc stroke centered on a radius, with flat ends.
void addPolygon(const std::vector< GuiPoint > &pts, const GuiFill &fill, bool edgeFringe=true)
Adds a filled polygon (triangle, chevron, a controller silhouette, ...).
void addRect(const GuiRect &r, const GuiFill &fill, bool edgeFringe=true)
Adds a filled rectangle.
void strokeRoundRect(const GuiRect &r, const GuiCorners &corners, float width, STROKE align, const GuiFill &fill)
Adds a rounded-rectangle outline.
void addEllipse(const GuiRect &r, const GuiFill &fill, bool edgeFringe=true)
Adds a filled ellipse inscribed in a rectangle (a circle when w == h)
std::vector< GuiPoint > GuiRoundRectPolygon(const GuiRect &r, float radius, float angleDeg, float tolerance)
A rounded rectangle as a convex polygon, rotated about its center.
STROKE
Where a stroke sits relative to the shape's geometric edge.
Definition GuiGeometry.h:52
std::vector< std::vector< GuiPoint > > GuiFlattenPath(const GuiPath &path, float scale, float tolerance)
Turns a path into one polygon per contour, multiplied by scale.
GuiRect GuiPathBounds(const GuiPath &path, float scale)
Gets the bounding box of a path's points (control points included, so it can be a little larger than ...
@ INSIDE
Entirely inside the edge (the shape keeps its size)
Definition GuiGeometry.h:53
@ CENTRE
Centered on the edge.
Definition GuiGeometry.h:54
@ OUTSIDE
Entirely outside the edge.
Definition GuiGeometry.h:55
Per-corner radii, in the order top-left, top-right, bottom-right, bottom-left.
Definition GuiGeometry.h:40
float tl
Top-left radius.
Definition GuiGeometry.h:41
float bl
Bottom-left radius.
Definition GuiGeometry.h:44
float tr
Top-right radius.
Definition GuiGeometry.h:42
float br
Bottom-right radius.
Definition GuiGeometry.h:43
A solid color or a two-color linear gradient.
Definition GuiGeometry.h:60
bool gradient
true for a gradient, false for a solid color.
Definition GuiGeometry.h:64
PixelColor from
Solid color, or the gradient's start color.
Definition GuiGeometry.h:61
static GuiFill linear(PixelColor a, PixelColor b, float angleDeg)
A linear gradient across the shape's bounding box.
static GuiFill solid(PixelColor c)
A single flat color.
PixelColor to
The gradient's end color (unused when solid).
Definition GuiGeometry.h:62
float angle
Gradient direction in degrees: 0 = left to right, 90 = top to bottom.
Definition GuiGeometry.h:63
A closed outline made of straight lines and cubic Beziers, in whatever units the caller likes.
Definition GuiGeometry.h:81
std::vector< uint8_t > cmds
One CMD per segment.
Definition GuiGeometry.h:83
std::vector< float > pts
x, y pairs, as many as the commands consume.
Definition GuiGeometry.h:84
A 2D point in design-canvas pixels.
Definition GuiGeometry.h:23
float x
X coordinate.
Definition GuiGeometry.h:24
float y
Y coordinate.
Definition GuiGeometry.h:25
An axis-aligned rectangle (top-left origin) in design-canvas pixels.
Definition GuiGeometry.h:30
float x
Left edge.
Definition GuiGeometry.h:31
float w
Width.
Definition GuiGeometry.h:33
float h
Height.
Definition GuiGeometry.h:34
float y
Top edge.
Definition GuiGeometry.h:32
A single 2D vertex with its own color, in design-canvas coordinates.
Definition Mesh.h:27
float y
Y position (design-canvas pixels).
Definition Mesh.h:29
float x
X position (design-canvas pixels).
Definition Mesh.h:28
PixelColor c
Straight (non-premultiplied) RGBA color.
Definition Mesh.h:30
8-bit-per-channel RGBA color used by the video interfaces.
Definition Mesh.h:16
uint8_t g
Green color component.
Definition Mesh.h:18
uint8_t r
Red color component.
Definition Mesh.h:17
uint8_t b
Blue color component.
Definition Mesh.h:19
uint8_t a
Alpha component.
Definition Mesh.h:20