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