xbintsc 0.5.21 → 0.5.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ /*
2
+ * xbintsc GUI engine — CanvasGradient objects.
3
+ *
4
+ * A gradient is owned by the canvas that created it (`Canvas2D::gradients_`),
5
+ * so a handle that outlives the document resolves to no gradient instead of a
6
+ * dangling pointer. Colour stops stay live: a later `addColorStop` is visible
7
+ * to every fill that already uses the gradient, which is the canvas semantics
8
+ * the DOM layer exposes.
9
+ */
10
+
11
+ #include <algorithm>
12
+ #include <atomic>
13
+ #include <cmath>
14
+
15
+ #include "canvas.h"
16
+
17
+ namespace xtgui {
18
+
19
+ namespace {
20
+
21
+ std::atomic<uint64_t> g_next_gradient_id{1};
22
+
23
+ float clamp_unit(float value) {
24
+ if (value < 0.0f) return 0.0f;
25
+ if (value > 1.0f) return 1.0f;
26
+ return value;
27
+ }
28
+
29
+ } // namespace
30
+
31
+ CanvasGradient::CanvasGradient(uint64_t id, Type type, float x0, float y0, float x1, float y1,
32
+ float r0, float r1)
33
+ : id_(id), type_(type), x0_(x0), y0_(y0), x1_(x1), y1_(y1), r0_(r0), r1_(r1) {}
34
+
35
+ void CanvasGradient::add_color_stop(float offset, const float color[4]) {
36
+ Stop stop;
37
+ stop.offset = clamp_unit(offset);
38
+ for (int i = 0; i < 4; i++) stop.color[i] = color[i];
39
+ auto position = std::upper_bound(stops_.begin(), stops_.end(), stop,
40
+ [](const Stop &a, const Stop &b) { return a.offset < b.offset; });
41
+ stops_.insert(position, stop);
42
+ }
43
+
44
+ bool CanvasGradient::color_at(float x, float y, float out[4]) const {
45
+ if (stops_.empty()) {
46
+ out[0] = out[1] = out[2] = out[3] = 0.0f;
47
+ return true;
48
+ }
49
+ float t = 0.0f;
50
+ if (type_ == Type::Linear) {
51
+ const float dx = x1_ - x0_;
52
+ const float dy = y1_ - y0_;
53
+ const float lengthSquared = dx * dx + dy * dy;
54
+ if (lengthSquared > 1e-9f) {
55
+ t = ((x - x0_) * dx + (y - y0_) * dy) / lengthSquared;
56
+ }
57
+ } else {
58
+ const float dx = x - x1_;
59
+ const float dy = y - y1_;
60
+ const float distance = std::sqrt(dx * dx + dy * dy);
61
+ const float radiusDelta = r1_ - r0_;
62
+ if (std::fabs(radiusDelta) > 1e-6f) t = (distance - r0_) / radiusDelta;
63
+ }
64
+ t = clamp_unit(t);
65
+ if (t <= stops_.front().offset) {
66
+ for (int i = 0; i < 4; i++) out[i] = stops_.front().color[i];
67
+ return true;
68
+ }
69
+ if (t >= stops_.back().offset) {
70
+ for (int i = 0; i < 4; i++) out[i] = stops_.back().color[i];
71
+ return true;
72
+ }
73
+ for (size_t i = 1; i < stops_.size(); i++) {
74
+ const Stop &a = stops_[i - 1];
75
+ const Stop &b = stops_[i];
76
+ if (t > b.offset) continue;
77
+ const float span = b.offset - a.offset;
78
+ const float u = span > 1e-9f ? (t - a.offset) / span : 1.0f;
79
+ for (int channel = 0; channel < 4; channel++) {
80
+ out[channel] = a.color[channel] + (b.color[channel] - a.color[channel]) * u;
81
+ }
82
+ return true;
83
+ }
84
+ for (int i = 0; i < 4; i++) out[i] = stops_.back().color[i];
85
+ return true;
86
+ }
87
+
88
+ CanvasGradient *Canvas2D::create_linear_gradient(float x0, float y0, float x1, float y1) {
89
+ auto gradient = std::make_unique<CanvasGradient>(g_next_gradient_id.fetch_add(1),
90
+ CanvasGradient::Type::Linear, x0, y0, x1, y1,
91
+ 0.0f, 1.0f);
92
+ CanvasGradient *raw = gradient.get();
93
+ gradients_[raw->id()] = std::move(gradient);
94
+ return raw;
95
+ }
96
+
97
+ CanvasGradient *Canvas2D::create_radial_gradient(float x0, float y0, float r0, float x1, float y1,
98
+ float r1) {
99
+ auto gradient = std::make_unique<CanvasGradient>(g_next_gradient_id.fetch_add(1),
100
+ CanvasGradient::Type::Radial, x0, y0, x1, y1, r0,
101
+ r1);
102
+ CanvasGradient *raw = gradient.get();
103
+ gradients_[raw->id()] = std::move(gradient);
104
+ return raw;
105
+ }
106
+
107
+ CanvasGradient *Canvas2D::find_gradient(uint64_t id) const {
108
+ auto found = gradients_.find(id);
109
+ return found == gradients_.end() ? nullptr : found->second.get();
110
+ }
111
+
112
+ } // namespace xtgui
@@ -21,22 +21,22 @@ namespace canvas_detail {
21
21
  * pixel. `alpha` folds in `globalAlpha`. */
22
22
  void blend_pixel(unsigned char *pixel, const float color[4], float coverage, float alpha);
23
23
 
24
- /** Fill one closed polygon loop (already in device space) with `color`,
24
+ /** Fill one closed polygon loop (already in device space) with `paint`,
25
25
  * anti-aliased, blending source-over. */
26
26
  void fill_loop(std::vector<unsigned char> &pixels, int width, int height,
27
- const std::vector<CanvasPoint> &loop, const float color[4], float alpha);
27
+ const std::vector<CanvasPoint> &loop, const CanvasPaint &paint, float alpha);
28
28
 
29
29
  /** Fill several loops as one path with the nonzero winding rule (so a contour
30
30
  * inside another punches a hole). */
31
31
  void fill_loops(std::vector<unsigned char> &pixels, int width, int height,
32
- const std::vector<std::vector<CanvasPoint>> &loops, const float color[4],
32
+ const std::vector<std::vector<CanvasPoint>> &loops, const CanvasPaint &paint,
33
33
  float alpha);
34
34
 
35
35
  /** Fill a device-space axis-aligned rectangle, anti-aliased. Used by the
36
36
  * `fillRect`/`clearRect` fast path when the transform preserves axis
37
37
  * alignment. */
38
38
  void fill_rect(std::vector<unsigned char> &pixels, int width, int height, float x0, float y0,
39
- float x1, float y1, const float color[4], float alpha);
39
+ float x1, float y1, const CanvasPaint &paint, float alpha);
40
40
 
41
41
  /** Clear a device-space axis-aligned rectangle to transparent. */
42
42
  void clear_rect(std::vector<unsigned char> &pixels, int width, int height, float x0, float y0,
@@ -48,10 +48,10 @@ void draw_image(std::vector<unsigned char> &pixels, int width, int height, const
48
48
  float dx, float dy, float dw, float dh, float sx, float sy, float sw, float sh,
49
49
  float alpha);
50
50
 
51
- /** Composite one grayscale glyph bitmap at `(x, baselineY)` with `color`. */
51
+ /** Composite one grayscale glyph bitmap at `(x, baselineY)` with `paint`. */
52
52
  void blit_glyph(std::vector<unsigned char> &pixels, int width, int height,
53
53
  const std::vector<unsigned char> &glyph, int glyphWidth, int glyphHeight, int left,
54
- int top, float x, float baselineY, const float color[4], float alpha);
54
+ int top, float x, float baselineY, const CanvasPaint &paint, float alpha);
55
55
 
56
56
  } // namespace canvas_detail
57
57
  } // namespace xtgui
@@ -34,6 +34,22 @@ struct Crossing {
34
34
  int winding;
35
35
  };
36
36
 
37
+ /** Resolve `paint` at a device-space pixel centre. Gradients are defined in
38
+ * user space, so the pixel is mapped back through the inverse transform. */
39
+ void paint_color_at(const CanvasPaint &paint, float x, float y, float out[4]) {
40
+ if (paint.gradient != nullptr) {
41
+ const CanvasTransform &inverse = paint.inverse;
42
+ float userX = inverse.a * x + inverse.c * y + inverse.e;
43
+ float userY = inverse.b * x + inverse.d * y + inverse.f;
44
+ if (paint.gradient->color_at(userX, userY, out)) return;
45
+ }
46
+ if (paint.color != nullptr) {
47
+ for (int i = 0; i < 4; i++) out[i] = paint.color[i];
48
+ return;
49
+ }
50
+ out[0] = out[1] = out[2] = out[3] = 0.0f;
51
+ }
52
+
37
53
  } // namespace
38
54
 
39
55
  void blend_pixel(unsigned char *pixel, const float color[4], float coverage, float alpha) {
@@ -74,7 +90,7 @@ static void add_span(float *row, int rowOriginX, float xa, float xb, float weigh
74
90
  }
75
91
 
76
92
  void fill_loops(std::vector<unsigned char> &pixels, int width, int height,
77
- const std::vector<std::vector<CanvasPoint>> &loops, const float color[4],
93
+ const std::vector<std::vector<CanvasPoint>> &loops, const CanvasPaint &paint,
78
94
  float alpha) {
79
95
  if (width <= 0 || height <= 0 || loops.empty()) return;
80
96
  bool havePoint = false;
@@ -146,18 +162,20 @@ void fill_loops(std::vector<unsigned char> &pixels, int width, int height,
146
162
  float c = row[px];
147
163
  if (c <= 0.0f) continue;
148
164
  if (c > 1.0f) c = 1.0f;
165
+ float color[4];
166
+ paint_color_at(paint, (float)(x0 + px) + 0.5f, (float)py + 0.5f, color);
149
167
  blend_pixel(dst + (size_t)px * 4, color, c, alpha);
150
168
  }
151
169
  }
152
170
  }
153
171
 
154
172
  void fill_loop(std::vector<unsigned char> &pixels, int width, int height,
155
- const std::vector<CanvasPoint> &loop, const float color[4], float alpha) {
156
- fill_loops(pixels, width, height, std::vector<std::vector<CanvasPoint>>{loop}, color, alpha);
173
+ const std::vector<CanvasPoint> &loop, const CanvasPaint &paint, float alpha) {
174
+ fill_loops(pixels, width, height, std::vector<std::vector<CanvasPoint>>{loop}, paint, alpha);
157
175
  }
158
176
 
159
177
  void fill_rect(std::vector<unsigned char> &pixels, int width, int height, float x0, float y0,
160
- float x1, float y1, const float color[4], float alpha) {
178
+ float x1, float y1, const CanvasPaint &paint, float alpha) {
161
179
  if (x1 < x0) std::swap(x0, x1);
162
180
  if (y1 < y0) std::swap(y0, y1);
163
181
  int ix0 = std::max(0, (int)std::floor(x0));
@@ -171,6 +189,8 @@ void fill_rect(std::vector<unsigned char> &pixels, int width, int height, float
171
189
  for (int px = ix0; px < ix1; px++) {
172
190
  float cx = std::min(x1, (float)(px + 1)) - std::max(x0, (float)px);
173
191
  if (cx <= 0.0f) continue;
192
+ float color[4];
193
+ paint_color_at(paint, (float)px + 0.5f, (float)py + 0.5f, color);
174
194
  blend_pixel(dst + (size_t)(px - ix0) * 4, color, cx * cy, alpha);
175
195
  }
176
196
  }
@@ -233,7 +253,7 @@ void draw_image(std::vector<unsigned char> &pixels, int width, int height, const
233
253
 
234
254
  void blit_glyph(std::vector<unsigned char> &pixels, int width, int height,
235
255
  const std::vector<unsigned char> &glyph, int glyphWidth, int glyphHeight, int left,
236
- int top, float x, float baselineY, const float color[4], float alpha) {
256
+ int top, float x, float baselineY, const CanvasPaint &paint, float alpha) {
237
257
  int originX = (int)std::lround(x) + left;
238
258
  int originY = (int)std::lround(baselineY) - top;
239
259
  for (int gy = 0; gy < glyphHeight; gy++) {
@@ -244,6 +264,8 @@ void blit_glyph(std::vector<unsigned char> &pixels, int width, int height,
244
264
  if (px < 0 || px >= width) continue;
245
265
  float coverage = glyph[(size_t)gy * (size_t)glyphWidth + (size_t)gx] / 255.0f;
246
266
  if (coverage <= 0.0f) continue;
267
+ float color[4];
268
+ paint_color_at(paint, (float)px + 0.5f, (float)py + 0.5f, color);
247
269
  blend_pixel(pixels.data() + ((size_t)py * (size_t)width + (size_t)px) * 4, color, coverage,
248
270
  alpha);
249
271
  }
@@ -0,0 +1,252 @@
1
+ /*
2
+ * xbintsc GUI engine — Canvas2D stroking.
3
+ *
4
+ * A stroked path is flattened into segments; each segment becomes a quad and
5
+ * every joint/cap is filled with the shape the current `lineJoin`/`lineCap`
6
+ * asks for. Dash patterns split a polyline into the "on" pieces first, so a
7
+ * dashed stroke is just a set of open polylines with the same cap rules.
8
+ *
9
+ * Everything here works in device space: `canvas.cpp` flattens the path and
10
+ * passes the paint (solid colour or gradient) through.
11
+ */
12
+
13
+ #include <algorithm>
14
+ #include <cmath>
15
+ #include <vector>
16
+
17
+ #include "canvas_internal.h"
18
+
19
+ namespace xtgui {
20
+
21
+ namespace {
22
+
23
+ constexpr float kPi = 3.14159265358979323846f;
24
+
25
+ CanvasPoint lerp_point(const CanvasPoint &a, const CanvasPoint &b, float t) {
26
+ return CanvasPoint{a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t};
27
+ }
28
+
29
+ bool same_point(const CanvasPoint &a, const CanvasPoint &b) {
30
+ return std::fabs(a.x - b.x) < 1e-6f && std::fabs(a.y - b.y) < 1e-6f;
31
+ }
32
+
33
+ } // namespace
34
+
35
+ void Canvas2D::stroke_subpath(const CanvasSubpath &points, bool closed, const CanvasPaint &paint) {
36
+ if (points.size() < 2) return;
37
+ float scale = state_.transform.scale();
38
+ if (!(scale > 0.0f)) scale = 1.0f;
39
+ float half = state_.line_width * scale * 0.5f;
40
+ if (half <= 0.0f) half = 0.5f;
41
+ if (!state_.line_dash.empty()) {
42
+ std::vector<CanvasSubpath> pieces;
43
+ dash_polyline(points, closed, &pieces);
44
+ for (const CanvasSubpath &piece : pieces) stroke_polyline(piece, false, half, paint);
45
+ return;
46
+ }
47
+ stroke_polyline(points, closed, half, paint);
48
+ }
49
+
50
+ void Canvas2D::stroke_polyline(const CanvasSubpath &points, bool closed, float half,
51
+ const CanvasPaint &paint) {
52
+ CanvasSubpath pts = points;
53
+ if (closed && !same_point(pts.front(), pts.back())) pts.push_back(pts.front());
54
+ const size_t n = pts.size();
55
+ if (n < 2) return;
56
+
57
+ auto fill_loop = [&](const CanvasSubpath &loop) {
58
+ canvas_detail::fill_loop(image_.pixels, image_.width, image_.height, loop, paint,
59
+ state_.global_alpha);
60
+ };
61
+ auto fill_disc = [&](const CanvasPoint &p) {
62
+ CanvasSubpath disc;
63
+ const int segments = 24;
64
+ disc.reserve((size_t)segments);
65
+ for (int i = 0; i < segments; i++) {
66
+ float angle = 2.0f * kPi * ((float)i / (float)segments);
67
+ disc.push_back(CanvasPoint{p.x + half * std::cos(angle), p.y + half * std::sin(angle)});
68
+ }
69
+ fill_loop(disc);
70
+ };
71
+
72
+ /* Segments. A square cap extends the first/last quad along the line by half
73
+ * the width; round caps add a disc at the endpoints below. */
74
+ for (size_t i = 0; i + 1 < n; i++) {
75
+ CanvasPoint p = pts[i];
76
+ CanvasPoint q = pts[i + 1];
77
+ float dx = q.x - p.x;
78
+ float dy = q.y - p.y;
79
+ float length = std::sqrt(dx * dx + dy * dy);
80
+ if (length <= 1e-6f) continue;
81
+ float ux = dx / length;
82
+ float uy = dy / length;
83
+ float nx = -uy * half;
84
+ float ny = ux * half;
85
+ if (!closed && i == 0 && state_.line_cap == 2) {
86
+ p.x -= ux * half;
87
+ p.y -= uy * half;
88
+ }
89
+ if (!closed && i + 2 == n && state_.line_cap == 2) {
90
+ q.x += ux * half;
91
+ q.y += uy * half;
92
+ }
93
+ CanvasSubpath quad{CanvasPoint{p.x + nx, p.y + ny}, CanvasPoint{q.x + nx, q.y + ny},
94
+ CanvasPoint{q.x - nx, q.y - ny}, CanvasPoint{p.x - nx, p.y - ny}};
95
+ fill_loop(quad);
96
+ }
97
+
98
+ /* Joins: round adds a disc, bevel a triangle, miter the intersect of the two
99
+ * outer offset lines (clamped by `miterLimit`). */
100
+ auto add_join = [&](const CanvasPoint &previous, const CanvasPoint &p, const CanvasPoint &next) {
101
+ float v1x = p.x - previous.x;
102
+ float v1y = p.y - previous.y;
103
+ float v2x = next.x - p.x;
104
+ float v2y = next.y - p.y;
105
+ float len1 = std::sqrt(v1x * v1x + v1y * v1y);
106
+ float len2 = std::sqrt(v2x * v2x + v2y * v2y);
107
+ if (len1 <= 1e-6f || len2 <= 1e-6f) return;
108
+ v1x /= len1;
109
+ v1y /= len1;
110
+ v2x /= len2;
111
+ v2y /= len2;
112
+ float cross = v1x * v2y - v1y * v2x;
113
+ if (std::fabs(cross) < 1e-6f) return;
114
+ if (state_.line_join == 1) {
115
+ fill_disc(p);
116
+ return;
117
+ }
118
+ /* The outer side is the opposite side of the turn. */
119
+ float side = cross > 0.0f ? -1.0f : 1.0f;
120
+ CanvasPoint a{p.x - v1y * half * side, p.y + v1x * half * side};
121
+ CanvasPoint b{p.x - v2y * half * side, p.y + v2x * half * side};
122
+ if (state_.line_join == 2) {
123
+ fill_loop(CanvasSubpath{a, b, p});
124
+ return;
125
+ }
126
+ float t = ((b.x - a.x) * v2y - (b.y - a.y) * v2x) / cross;
127
+ CanvasPoint miter{a.x + v1x * t, a.y + v1y * t};
128
+ float miterLength = std::sqrt((miter.x - p.x) * (miter.x - p.x) +
129
+ (miter.y - p.y) * (miter.y - p.y));
130
+ if (std::isfinite(miterLength) && miterLength <= half * state_.miter_limit) {
131
+ fill_loop(CanvasSubpath{a, miter, b, p});
132
+ } else {
133
+ fill_loop(CanvasSubpath{a, b, p});
134
+ }
135
+ };
136
+
137
+ if (closed) {
138
+ const size_t unique = n - 1;
139
+ for (size_t i = 0; i < unique; i++) {
140
+ const CanvasPoint &previous = pts[(i + unique - 1) % unique];
141
+ const CanvasPoint &next = pts[(i + 1) % unique];
142
+ add_join(previous, pts[i], next);
143
+ }
144
+ } else {
145
+ for (size_t i = 1; i + 1 < n; i++) add_join(pts[i - 1], pts[i], pts[i + 1]);
146
+ if (state_.line_cap == 1) {
147
+ fill_disc(pts.front());
148
+ fill_disc(pts.back());
149
+ }
150
+ }
151
+ }
152
+
153
+ void Canvas2D::dash_polyline(const CanvasSubpath &points, bool closed,
154
+ std::vector<CanvasSubpath> *out) const {
155
+ if (points.size() < 2) return;
156
+ float scale = state_.transform.scale();
157
+ if (!(scale > 0.0f)) scale = 1.0f;
158
+ std::vector<float> pattern;
159
+ pattern.reserve(state_.line_dash.size());
160
+ float total = 0.0f;
161
+ for (float entry : state_.line_dash) {
162
+ pattern.push_back(entry * scale);
163
+ total += entry * scale;
164
+ }
165
+ if (pattern.empty() || total <= 1e-6f) {
166
+ out->push_back(points);
167
+ return;
168
+ }
169
+
170
+ CanvasSubpath pts = points;
171
+ if (closed && !same_point(pts.front(), pts.back())) pts.push_back(pts.front());
172
+ const size_t n = pts.size();
173
+
174
+ /* Consume `lineDashOffset` from the repeating pattern before drawing. */
175
+ float dashOffset = state_.line_dash_offset;
176
+ if (!std::isfinite(dashOffset)) dashOffset = 0.0f;
177
+ float offset = std::fmod(dashOffset * scale, total);
178
+ if (offset < 0.0f) offset += total;
179
+ size_t index = 0;
180
+ float remain = pattern[0];
181
+ int guard = 0;
182
+ while (offset > 1e-6f && guard++ < 100000) {
183
+ if (offset >= remain) {
184
+ offset -= remain;
185
+ index = (index + 1) % pattern.size();
186
+ remain = pattern[index];
187
+ } else {
188
+ remain -= offset;
189
+ offset = 0.0f;
190
+ }
191
+ }
192
+ auto skip_empty = [&]() {
193
+ int skipped = 0;
194
+ while (remain <= 1e-6f && skipped++ < 100000) {
195
+ index = (index + 1) % pattern.size();
196
+ remain = pattern[index];
197
+ }
198
+ };
199
+ skip_empty();
200
+ bool on = (index % 2 == 0);
201
+
202
+ CanvasSubpath current;
203
+ bool have_current = false;
204
+ auto finish_dash = [&]() {
205
+ if (have_current) {
206
+ out->push_back(current);
207
+ current.clear();
208
+ have_current = false;
209
+ }
210
+ };
211
+
212
+ for (size_t i = 0; i + 1 < n; i++) {
213
+ const CanvasPoint &p = pts[i];
214
+ const CanvasPoint &q = pts[i + 1];
215
+ float dx = q.x - p.x;
216
+ float dy = q.y - p.y;
217
+ float length = std::sqrt(dx * dx + dy * dy);
218
+ if (length <= 1e-6f) continue;
219
+ float position = 0.0f;
220
+ while (position < length - 1e-6f) {
221
+ if (remain <= 1e-6f) {
222
+ skip_empty();
223
+ on = (index % 2 == 0);
224
+ continue;
225
+ }
226
+ float step = std::min(remain, length - position);
227
+ if (on) {
228
+ if (!have_current) {
229
+ current.clear();
230
+ current.push_back(lerp_point(p, q, position / length));
231
+ have_current = true;
232
+ }
233
+ position += step;
234
+ current.push_back(lerp_point(p, q, position / length));
235
+ } else {
236
+ position += step;
237
+ }
238
+ remain -= step;
239
+ if (remain <= 1e-6f) {
240
+ if (on) finish_dash();
241
+ index = (index + 1) % pattern.size();
242
+ remain = pattern[index];
243
+ on = (index % 2 == 0);
244
+ skip_empty();
245
+ on = (index % 2 == 0);
246
+ }
247
+ }
248
+ }
249
+ finish_dash();
250
+ }
251
+
252
+ } // namespace xtgui