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.
- package/dist/src/driver/version.d.ts +1 -1
- package/dist/src/driver/version.js +1 -1
- package/dist/tests/e2e/gui-canvas.test.js +159 -0
- package/dist/tests/e2e/gui-canvas.test.js.map +1 -1
- package/dist/tests/e2e/gui-example-canvas.test.d.ts +11 -0
- package/dist/tests/e2e/gui-example-canvas.test.js +70 -0
- package/dist/tests/e2e/gui-example-canvas.test.js.map +1 -0
- package/doc/ai/extensions.md +4 -3
- package/doc/ai/zh-CN/extensions.md +2 -1
- package/doc/gui/canvas.md +47 -21
- package/doc/gui.md +7 -1
- package/doc/zh-CN/gui/canvas.md +36 -17
- package/doc/zh-CN/gui.md +6 -2
- package/package.json +1 -1
- package/runtime/ext_gui/canvas.cpp +114 -199
- package/runtime/ext_gui/canvas.h +101 -2
- package/runtime/ext_gui/canvas_gradient.cpp +112 -0
- package/runtime/ext_gui/canvas_internal.h +6 -6
- package/runtime/ext_gui/canvas_raster.cpp +27 -5
- package/runtime/ext_gui/canvas_stroke.cpp +252 -0
- package/runtime/ext_gui/canvas_style.cpp +240 -0
- package/runtime/ext_gui/dom_api.cpp +1 -0
- package/runtime/ext_gui/dom_api_canvas.cpp +38 -4
- package/runtime/ext_gui/dom_api_canvas_extra.cpp +232 -0
- package/runtime/ext_gui/dom_api_internal.h +31 -0
- package/runtime/ext_gui/dom_api_proto.cpp +24 -0
- package/src/driver/version.ts +1 -1
|
@@ -12,12 +12,10 @@
|
|
|
12
12
|
#include <algorithm>
|
|
13
13
|
#include <atomic>
|
|
14
14
|
#include <cmath>
|
|
15
|
-
#include <cstdlib>
|
|
16
15
|
#include <cstring>
|
|
17
16
|
#include <unordered_map>
|
|
18
17
|
|
|
19
18
|
#include "canvas_internal.h"
|
|
20
|
-
#include "css.h"
|
|
21
19
|
|
|
22
20
|
namespace xtgui {
|
|
23
21
|
|
|
@@ -32,99 +30,12 @@ std::unordered_map<uint64_t, Canvas2D *> &canvas_registry() {
|
|
|
32
30
|
return registry;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
std::string lower_ascii(const std::string &value) {
|
|
36
|
-
std::string out = value;
|
|
37
|
-
for (char &c : out) {
|
|
38
|
-
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
bool parse_font_size_token(const std::string &token, float fallback, float *out) {
|
|
44
|
-
std::string lower = lower_ascii(token);
|
|
45
|
-
struct Suffix {
|
|
46
|
-
const char *text;
|
|
47
|
-
float factor;
|
|
48
|
-
};
|
|
49
|
-
static const Suffix suffixes[] = {{"px", 1.0f}, {"pt", 96.0f / 72.0f}, {"pc", 16.0f},
|
|
50
|
-
{"in", 96.0f}, {"cm", 96.0f / 2.54f}, {"mm", 96.0f / 25.4f},
|
|
51
|
-
{"q", 96.0f / 101.6f}, {"em", 0.0f}, {"rem", 0.0f}};
|
|
52
|
-
for (const Suffix &suffix : suffixes) {
|
|
53
|
-
size_t length = std::strlen(suffix.text);
|
|
54
|
-
if (lower.size() <= length || lower.compare(lower.size() - length, length, suffix.text) != 0)
|
|
55
|
-
continue;
|
|
56
|
-
char *end = nullptr;
|
|
57
|
-
float value = std::strtof(lower.c_str(), &end);
|
|
58
|
-
if (end == lower.c_str()) return false;
|
|
59
|
-
if (suffix.factor == 0.0f) {
|
|
60
|
-
*out = value * fallback;
|
|
61
|
-
} else {
|
|
62
|
-
*out = value * suffix.factor;
|
|
63
|
-
}
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Parse the CSS `font` shorthand into a `FontSpec` (the pieces Canvas2D
|
|
70
|
-
* understands; unknown tokens are ignored). */
|
|
71
|
-
FontSpec parse_font_string(const std::string &text) {
|
|
72
|
-
FontSpec spec;
|
|
73
|
-
spec.family = "sans-serif";
|
|
74
|
-
spec.pixel_size = 10.0f;
|
|
75
|
-
spec.weight = 400;
|
|
76
|
-
spec.italic = false;
|
|
77
|
-
std::vector<std::string> tokens = xt_css_split_whitespace(text);
|
|
78
|
-
std::vector<std::string> family;
|
|
79
|
-
float size = 10.0f;
|
|
80
|
-
bool have_size = false;
|
|
81
|
-
for (const std::string &token : tokens) {
|
|
82
|
-
std::string lower = lower_ascii(token);
|
|
83
|
-
float parsed = 0.0f;
|
|
84
|
-
if (!have_size && parse_font_size_token(lower, 16.0f, &parsed)) {
|
|
85
|
-
size = parsed;
|
|
86
|
-
have_size = true;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (lower == "italic" || lower == "oblique") {
|
|
90
|
-
spec.italic = true;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (lower == "bold") {
|
|
94
|
-
spec.weight = 700;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (lower == "bolder") {
|
|
98
|
-
spec.weight = 700;
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (lower == "lighter") {
|
|
102
|
-
spec.weight = 300;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (lower == "normal" || lower == "small-caps") continue;
|
|
106
|
-
if (!lower.empty() && lower.back() == ',') lower.pop_back();
|
|
107
|
-
if (!lower.empty()) family.push_back(lower);
|
|
108
|
-
}
|
|
109
|
-
spec.pixel_size = size;
|
|
110
|
-
if (!family.empty()) spec.family = family[0];
|
|
111
|
-
return spec;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
void parse_color_into(const std::string &css, float color[4], std::string *stored) {
|
|
115
|
-
Color parsed = xt_css_parse_color(css);
|
|
116
|
-
if (!parsed.valid) return;
|
|
117
|
-
color[0] = parsed.r;
|
|
118
|
-
color[1] = parsed.g;
|
|
119
|
-
color[2] = parsed.b;
|
|
120
|
-
color[3] = parsed.a;
|
|
121
|
-
*stored = css;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
33
|
CanvasPoint lerp(const CanvasPoint &a, const CanvasPoint &b, float t) {
|
|
125
34
|
return CanvasPoint{a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t};
|
|
126
35
|
}
|
|
127
36
|
|
|
37
|
+
constexpr float kPi = 3.14159265358979323846f;
|
|
38
|
+
|
|
128
39
|
} // namespace
|
|
129
40
|
|
|
130
41
|
Canvas2D* xt_canvas_by_id(uint64_t id) {
|
|
@@ -137,6 +48,20 @@ float CanvasTransform::scale() const {
|
|
|
137
48
|
return std::sqrt(determinant);
|
|
138
49
|
}
|
|
139
50
|
|
|
51
|
+
CanvasTransform CanvasTransform::inverted() const {
|
|
52
|
+
const float determinant = a * d - b * c;
|
|
53
|
+
if (std::fabs(determinant) < 1e-9f) return CanvasTransform();
|
|
54
|
+
const float inv = 1.0f / determinant;
|
|
55
|
+
CanvasTransform result;
|
|
56
|
+
result.a = d * inv;
|
|
57
|
+
result.b = -b * inv;
|
|
58
|
+
result.c = -c * inv;
|
|
59
|
+
result.d = a * inv;
|
|
60
|
+
result.e = (c * f - d * e) * inv;
|
|
61
|
+
result.f = (b * e - a * f) * inv;
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
140
65
|
Canvas2D::Canvas2D(int width, int height) : id_(g_next_canvas_id.fetch_add(1)) {
|
|
141
66
|
resize(width, height);
|
|
142
67
|
canvas_registry()[id_] = this;
|
|
@@ -178,14 +103,17 @@ void Canvas2D::rotate(float radians) {
|
|
|
178
103
|
void Canvas2D::scale(float sx, float sy) { transform(sx, 0.0f, 0.0f, sy, 0.0f, 0.0f); }
|
|
179
104
|
|
|
180
105
|
void Canvas2D::transform(float a, float b, float c, float d, float e, float f) {
|
|
106
|
+
/* Post-multiply: the new matrix applies *after* the current one, so
|
|
107
|
+
* `translate(x, y); rotate(a)` rotates around the translated origin (the
|
|
108
|
+
* canvas rules) instead of rotating the translation itself. */
|
|
181
109
|
const CanvasTransform &m = state_.transform;
|
|
182
110
|
CanvasTransform next;
|
|
183
|
-
next.a = a *
|
|
184
|
-
next.b = b *
|
|
185
|
-
next.c = a *
|
|
186
|
-
next.d = b *
|
|
187
|
-
next.e = a *
|
|
188
|
-
next.f = b *
|
|
111
|
+
next.a = m.a * a + m.c * b;
|
|
112
|
+
next.b = m.b * a + m.d * b;
|
|
113
|
+
next.c = m.a * c + m.c * d;
|
|
114
|
+
next.d = m.b * c + m.d * d;
|
|
115
|
+
next.e = m.a * e + m.c * f + m.e;
|
|
116
|
+
next.f = m.b * e + m.d * f + m.f;
|
|
189
117
|
state_.transform = next;
|
|
190
118
|
}
|
|
191
119
|
|
|
@@ -193,59 +121,6 @@ void Canvas2D::set_transform(float a, float b, float c, float d, float e, float
|
|
|
193
121
|
state_.transform = CanvasTransform{a, b, c, d, e, f};
|
|
194
122
|
}
|
|
195
123
|
|
|
196
|
-
void Canvas2D::set_fill_style(const std::string &css) {
|
|
197
|
-
parse_color_into(css, state_.fill_color, &state_.fill_style);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
void Canvas2D::set_stroke_style(const std::string &css) {
|
|
201
|
-
parse_color_into(css, state_.stroke_color, &state_.stroke_style);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
void Canvas2D::set_global_alpha(float alpha) {
|
|
205
|
-
if (alpha < 0.0f) alpha = 0.0f;
|
|
206
|
-
if (alpha > 1.0f) alpha = 1.0f;
|
|
207
|
-
state_.global_alpha = alpha;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
void Canvas2D::set_font(const std::string &font) {
|
|
211
|
-
state_.font = parse_font_string(font);
|
|
212
|
-
state_.font_string = font;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
void Canvas2D::set_text_align(const std::string &value) {
|
|
216
|
-
std::string lower = lower_ascii(value);
|
|
217
|
-
if (lower == "start" || lower == "left") {
|
|
218
|
-
state_.text_align = 0;
|
|
219
|
-
} else if (lower == "center") {
|
|
220
|
-
state_.text_align = 1;
|
|
221
|
-
} else if (lower == "right" || lower == "end") {
|
|
222
|
-
state_.text_align = 2;
|
|
223
|
-
} else {
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
state_.text_align_string = lower;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
void Canvas2D::set_text_baseline(const std::string &value) {
|
|
230
|
-
std::string lower = lower_ascii(value);
|
|
231
|
-
if (lower == "alphabetic") {
|
|
232
|
-
state_.text_baseline = 0;
|
|
233
|
-
} else if (lower == "top") {
|
|
234
|
-
state_.text_baseline = 1;
|
|
235
|
-
} else if (lower == "middle") {
|
|
236
|
-
state_.text_baseline = 2;
|
|
237
|
-
} else if (lower == "bottom") {
|
|
238
|
-
state_.text_baseline = 3;
|
|
239
|
-
} else if (lower == "hanging") {
|
|
240
|
-
state_.text_baseline = 4;
|
|
241
|
-
} else if (lower == "ideographic") {
|
|
242
|
-
state_.text_baseline = 5;
|
|
243
|
-
} else {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
state_.text_baseline_string = lower;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
124
|
void Canvas2D::begin_path() { path_.clear(); }
|
|
250
125
|
|
|
251
126
|
void Canvas2D::move_to(float x, float y) { path_.push_back(CanvasSubpath{CanvasPoint{x, y}}); }
|
|
@@ -287,19 +162,74 @@ void Canvas2D::flatten_arc(float cx, float cy, float radius, float start, float
|
|
|
287
162
|
}
|
|
288
163
|
}
|
|
289
164
|
|
|
290
|
-
void Canvas2D::
|
|
291
|
-
|
|
292
|
-
flatten_arc(cx, cy, radius, start, end, ccw, &arcPoints);
|
|
293
|
-
if (arcPoints.empty()) return;
|
|
165
|
+
void Canvas2D::append_path_points(const CanvasSubpath &points) {
|
|
166
|
+
if (points.empty()) return;
|
|
294
167
|
if (path_.empty() || path_.back().empty()) {
|
|
295
|
-
path_.push_back(CanvasSubpath{
|
|
168
|
+
path_.push_back(CanvasSubpath{points.front()});
|
|
296
169
|
} else {
|
|
297
170
|
CanvasPoint last = path_.back().back();
|
|
298
|
-
if (last.x !=
|
|
299
|
-
path_.back().push_back(
|
|
171
|
+
if (last.x != points.front().x || last.y != points.front().y) {
|
|
172
|
+
path_.back().push_back(points.front());
|
|
300
173
|
}
|
|
301
174
|
}
|
|
302
|
-
for (size_t i = 1; i <
|
|
175
|
+
for (size_t i = 1; i < points.size(); i++) path_.back().push_back(points[i]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
void Canvas2D::arc(float cx, float cy, float radius, float start, float end, bool ccw) {
|
|
179
|
+
CanvasSubpath arcPoints;
|
|
180
|
+
flatten_arc(cx, cy, radius, start, end, ccw, &arcPoints);
|
|
181
|
+
append_path_points(arcPoints);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
void Canvas2D::ellipse(float cx, float cy, float rx, float ry, float rotation, float start,
|
|
185
|
+
float end, bool counterclockwise) {
|
|
186
|
+
float sweep = end - start;
|
|
187
|
+
if (counterclockwise) {
|
|
188
|
+
while (sweep > 0.0f) sweep -= 2.0f * kPi;
|
|
189
|
+
while (sweep < -2.0f * kPi) sweep += 2.0f * kPi;
|
|
190
|
+
} else {
|
|
191
|
+
while (sweep < 0.0f) sweep += 2.0f * kPi;
|
|
192
|
+
while (sweep > 2.0f * kPi) sweep -= 2.0f * kPi;
|
|
193
|
+
}
|
|
194
|
+
int segments = (int)std::ceil(std::fabs(sweep) / (kPi / 32.0f));
|
|
195
|
+
if (segments < 1) segments = 1;
|
|
196
|
+
if (segments > 2048) segments = 2048;
|
|
197
|
+
const float cosRotation = std::cos(rotation);
|
|
198
|
+
const float sinRotation = std::sin(rotation);
|
|
199
|
+
CanvasSubpath points;
|
|
200
|
+
points.reserve((size_t)segments + 1);
|
|
201
|
+
for (int i = 0; i <= segments; i++) {
|
|
202
|
+
float angle = start + sweep * ((float)i / (float)segments);
|
|
203
|
+
float px = rx * std::cos(angle);
|
|
204
|
+
float py = ry * std::sin(angle);
|
|
205
|
+
points.push_back(CanvasPoint{cx + px * cosRotation - py * sinRotation,
|
|
206
|
+
cy + px * sinRotation + py * cosRotation});
|
|
207
|
+
}
|
|
208
|
+
append_path_points(points);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
void Canvas2D::round_rect(float x, float y, float width, float height, float radius) {
|
|
212
|
+
if (width < 0.0f) {
|
|
213
|
+
x += width;
|
|
214
|
+
width = -width;
|
|
215
|
+
}
|
|
216
|
+
if (height < 0.0f) {
|
|
217
|
+
y += height;
|
|
218
|
+
height = -height;
|
|
219
|
+
}
|
|
220
|
+
float maxRadius = std::min(width, height) * 0.5f;
|
|
221
|
+
if (radius < 0.0f) radius = 0.0f;
|
|
222
|
+
if (radius > maxRadius) radius = maxRadius;
|
|
223
|
+
move_to(x + radius, y);
|
|
224
|
+
line_to(x + width - radius, y);
|
|
225
|
+
quadratic_curve_to(x + width, y, x + width, y + radius);
|
|
226
|
+
line_to(x + width, y + height - radius);
|
|
227
|
+
quadratic_curve_to(x + width, y + height, x + width - radius, y + height);
|
|
228
|
+
line_to(x + radius, y + height);
|
|
229
|
+
quadratic_curve_to(x, y + height, x, y + height - radius);
|
|
230
|
+
line_to(x, y + radius);
|
|
231
|
+
quadratic_curve_to(x, y, x + radius, y);
|
|
232
|
+
close_path();
|
|
303
233
|
}
|
|
304
234
|
|
|
305
235
|
void Canvas2D::quadratic_curve_to(float cpx, float cpy, float x, float y) {
|
|
@@ -353,6 +283,22 @@ std::vector<CanvasSubpath> Canvas2D::device_subpaths() const {
|
|
|
353
283
|
return result;
|
|
354
284
|
}
|
|
355
285
|
|
|
286
|
+
CanvasPaint Canvas2D::make_paint(uint64_t gradient_id, const float color[4]) const {
|
|
287
|
+
CanvasPaint paint;
|
|
288
|
+
paint.color = color;
|
|
289
|
+
paint.gradient = gradient_id == 0 ? nullptr : find_gradient(gradient_id);
|
|
290
|
+
paint.inverse = state_.transform.inverted();
|
|
291
|
+
return paint;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
CanvasPaint Canvas2D::fill_paint() const {
|
|
295
|
+
return make_paint(state_.fill_gradient, state_.fill_color);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
CanvasPaint Canvas2D::stroke_paint() const {
|
|
299
|
+
return make_paint(state_.stroke_gradient, state_.stroke_color);
|
|
300
|
+
}
|
|
301
|
+
|
|
356
302
|
void Canvas2D::fill() {
|
|
357
303
|
std::vector<CanvasSubpath> subpaths = device_subpaths();
|
|
358
304
|
std::vector<CanvasSubpath> loops;
|
|
@@ -361,50 +307,19 @@ void Canvas2D::fill() {
|
|
|
361
307
|
loops.push_back(std::move(subpath));
|
|
362
308
|
}
|
|
363
309
|
if (loops.empty()) return;
|
|
364
|
-
canvas_detail::fill_loops(image_.pixels, image_.width, image_.height, loops,
|
|
310
|
+
canvas_detail::fill_loops(image_.pixels, image_.width, image_.height, loops, fill_paint(),
|
|
365
311
|
state_.global_alpha);
|
|
366
312
|
generation_++;
|
|
367
313
|
}
|
|
368
314
|
|
|
369
|
-
void Canvas2D::stroke_subpath(const CanvasSubpath &points, bool closed, const float color[4]) {
|
|
370
|
-
if (points.size() < 2) return;
|
|
371
|
-
float half = state_.line_width * state_.transform.scale() * 0.5f;
|
|
372
|
-
if (half <= 0.0f) half = 0.5f;
|
|
373
|
-
size_t segmentCount = points.size() - 1;
|
|
374
|
-
for (size_t i = 0; i < segmentCount; i++) {
|
|
375
|
-
const CanvasPoint &p = points[i];
|
|
376
|
-
const CanvasPoint &q = points[i + 1];
|
|
377
|
-
float dx = q.x - p.x;
|
|
378
|
-
float dy = q.y - p.y;
|
|
379
|
-
float length = std::sqrt(dx * dx + dy * dy);
|
|
380
|
-
if (length <= 0.0001f) continue;
|
|
381
|
-
float nx = -dy / length * half;
|
|
382
|
-
float ny = dx / length * half;
|
|
383
|
-
CanvasSubpath quad{CanvasPoint{p.x + nx, p.y + ny}, CanvasPoint{q.x + nx, q.y + ny},
|
|
384
|
-
CanvasPoint{q.x - nx, q.y - ny}, CanvasPoint{p.x - nx, p.y - ny}};
|
|
385
|
-
canvas_detail::fill_loop(image_.pixels, image_.width, image_.height, quad, color,
|
|
386
|
-
state_.global_alpha);
|
|
387
|
-
}
|
|
388
|
-
(void)closed;
|
|
389
|
-
for (const CanvasPoint &p : points) {
|
|
390
|
-
CanvasSubpath disc;
|
|
391
|
-
const int segments = 16;
|
|
392
|
-
for (int i = 0; i < segments; i++) {
|
|
393
|
-
float angle = 2.0f * 3.14159265358979323846f * ((float)i / (float)segments);
|
|
394
|
-
disc.push_back(CanvasPoint{p.x + half * std::cos(angle), p.y + half * std::sin(angle)});
|
|
395
|
-
}
|
|
396
|
-
canvas_detail::fill_loop(image_.pixels, image_.width, image_.height, disc, color,
|
|
397
|
-
state_.global_alpha);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
315
|
void Canvas2D::stroke() {
|
|
402
316
|
std::vector<CanvasSubpath> subpaths = device_subpaths();
|
|
317
|
+
const CanvasPaint paint = stroke_paint();
|
|
403
318
|
for (const CanvasSubpath &subpath : subpaths) {
|
|
404
319
|
if (subpath.size() < 2) continue;
|
|
405
320
|
bool closed = subpath.size() >= 3 && subpath.front().x == subpath.back().x &&
|
|
406
321
|
subpath.front().y == subpath.back().y;
|
|
407
|
-
stroke_subpath(subpath, closed,
|
|
322
|
+
stroke_subpath(subpath, closed, paint);
|
|
408
323
|
}
|
|
409
324
|
generation_++;
|
|
410
325
|
}
|
|
@@ -423,7 +338,7 @@ void Canvas2D::fill_rect(float x, float y, float width, float height) {
|
|
|
423
338
|
to_device(x, y, &x0, &y0);
|
|
424
339
|
to_device(x + width, y + height, &x1, &y1);
|
|
425
340
|
canvas_detail::fill_rect(image_.pixels, image_.width, image_.height, x0, y0, x1, y1,
|
|
426
|
-
|
|
341
|
+
fill_paint(), state_.global_alpha);
|
|
427
342
|
} else {
|
|
428
343
|
CanvasSubpath loop;
|
|
429
344
|
const float corners[4][2] = {
|
|
@@ -433,7 +348,7 @@ void Canvas2D::fill_rect(float x, float y, float width, float height) {
|
|
|
433
348
|
to_device(corner[0], corner[1], &point.x, &point.y);
|
|
434
349
|
loop.push_back(point);
|
|
435
350
|
}
|
|
436
|
-
canvas_detail::fill_loop(image_.pixels, image_.width, image_.height, loop,
|
|
351
|
+
canvas_detail::fill_loop(image_.pixels, image_.width, image_.height, loop, fill_paint(),
|
|
437
352
|
state_.global_alpha);
|
|
438
353
|
}
|
|
439
354
|
generation_++;
|
|
@@ -448,7 +363,7 @@ void Canvas2D::stroke_rect(float x, float y, float width, float height) {
|
|
|
448
363
|
to_device(corner[0], corner[1], &point.x, &point.y);
|
|
449
364
|
loop.push_back(point);
|
|
450
365
|
}
|
|
451
|
-
stroke_subpath(loop, true,
|
|
366
|
+
stroke_subpath(loop, true, stroke_paint());
|
|
452
367
|
generation_++;
|
|
453
368
|
}
|
|
454
369
|
|
|
@@ -456,7 +371,7 @@ float Canvas2D::measure_text(const std::string &text) const {
|
|
|
456
371
|
return xt_text_measure_width(text, state_.font);
|
|
457
372
|
}
|
|
458
373
|
|
|
459
|
-
void Canvas2D::paint_text(const std::string &text, float x, float y, const
|
|
374
|
+
void Canvas2D::paint_text(const std::string &text, float x, float y, const CanvasPaint &paint,
|
|
460
375
|
bool stroke) {
|
|
461
376
|
Font *font = xt_text_resolve(state_.font);
|
|
462
377
|
if (font == nullptr) return;
|
|
@@ -510,7 +425,7 @@ void Canvas2D::paint_text(const std::string &text, float x, float y, const float
|
|
|
510
425
|
GlyphImage image;
|
|
511
426
|
if (xt_text_rasterize(face, glyph.glyph, image) && image.width > 0 && image.height > 0) {
|
|
512
427
|
canvas_detail::blit_glyph(image_.pixels, image_.width, image_.height, image.pixels, image.width,
|
|
513
|
-
image.height, image.left, image.top, deviceX, deviceY,
|
|
428
|
+
image.height, image.left, image.top, deviceX, deviceY, paint,
|
|
514
429
|
state_.global_alpha);
|
|
515
430
|
}
|
|
516
431
|
penX += glyph.x_advance;
|
|
@@ -520,11 +435,11 @@ void Canvas2D::paint_text(const std::string &text, float x, float y, const float
|
|
|
520
435
|
}
|
|
521
436
|
|
|
522
437
|
void Canvas2D::fill_text(const std::string &text, float x, float y) {
|
|
523
|
-
paint_text(text, x, y,
|
|
438
|
+
paint_text(text, x, y, fill_paint(), false);
|
|
524
439
|
}
|
|
525
440
|
|
|
526
441
|
void Canvas2D::stroke_text(const std::string &text, float x, float y) {
|
|
527
|
-
paint_text(text, x, y,
|
|
442
|
+
paint_text(text, x, y, stroke_paint(), true);
|
|
528
443
|
}
|
|
529
444
|
|
|
530
445
|
void Canvas2D::draw_image(const Image &source, float dx, float dy, float dw, float dh, float sx,
|
package/runtime/ext_gui/canvas.h
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
#define XT_GUI_CANVAS_H
|
|
20
20
|
|
|
21
21
|
#include <cstdint>
|
|
22
|
+
#include <memory>
|
|
22
23
|
#include <string>
|
|
24
|
+
#include <unordered_map>
|
|
23
25
|
#include <vector>
|
|
24
26
|
|
|
25
27
|
#include "image.h"
|
|
@@ -40,6 +42,8 @@ struct CanvasTransform {
|
|
|
40
42
|
bool axis_aligned() const { return b == 0.0f && c == 0.0f; }
|
|
41
43
|
/** Approximate uniform scale, used to size stroked lines and text. */
|
|
42
44
|
float scale() const;
|
|
45
|
+
/** The inverse transform (device -> user space); identity when singular. */
|
|
46
|
+
CanvasTransform inverted() const;
|
|
43
47
|
};
|
|
44
48
|
|
|
45
49
|
/** A point in device (canvas) space. */
|
|
@@ -51,6 +55,51 @@ struct CanvasPoint {
|
|
|
51
55
|
/** One subpath of the current path (device space). */
|
|
52
56
|
using CanvasSubpath = std::vector<CanvasPoint>;
|
|
53
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A `CanvasGradient` handle: the colour stops plus the geometric definition of
|
|
60
|
+
* a linear or radial gradient. Gradients are owned by the canvas that created
|
|
61
|
+
* them, so a stale context handle resolves to no gradient instead of a dangling
|
|
62
|
+
* pointer. The DOM layer copies only the id around; the stops stay live, which
|
|
63
|
+
* is what makes `addColorStop` after `fillStyle = gradient` affect later fills.
|
|
64
|
+
*/
|
|
65
|
+
class CanvasGradient {
|
|
66
|
+
public:
|
|
67
|
+
enum class Type { Linear, Radial };
|
|
68
|
+
|
|
69
|
+
CanvasGradient(uint64_t id, Type type, float x0, float y0, float x1, float y1, float r0,
|
|
70
|
+
float r1);
|
|
71
|
+
|
|
72
|
+
uint64_t id() const { return id_; }
|
|
73
|
+
Type type() const { return type_; }
|
|
74
|
+
|
|
75
|
+
/** Add a colour stop (offset clamped to 0..1, stops kept sorted by offset). */
|
|
76
|
+
void add_color_stop(float offset, const float color[4]);
|
|
77
|
+
/** Sample the gradient at a user-space point. A gradient with no stops is
|
|
78
|
+
* transparent black, so this always fills `out`. */
|
|
79
|
+
bool color_at(float x, float y, float out[4]) const;
|
|
80
|
+
|
|
81
|
+
private:
|
|
82
|
+
struct Stop {
|
|
83
|
+
float offset = 0.0f;
|
|
84
|
+
float color[4] = {0.0f, 0.0f, 0.0f, 1.0f};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
uint64_t id_ = 0;
|
|
88
|
+
Type type_ = Type::Linear;
|
|
89
|
+
float x0_ = 0.0f, y0_ = 0.0f, x1_ = 0.0f, y1_ = 0.0f;
|
|
90
|
+
float r0_ = 0.0f, r1_ = 0.0f;
|
|
91
|
+
std::vector<Stop> stops_;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** What a fill or stroke paints with: a solid colour, or a gradient sampled
|
|
95
|
+
* through the inverse of the current transform. `color` points into the
|
|
96
|
+
* canvas state and stays valid for the duration of one draw call. */
|
|
97
|
+
struct CanvasPaint {
|
|
98
|
+
const float *color = nullptr;
|
|
99
|
+
const CanvasGradient *gradient = nullptr;
|
|
100
|
+
CanvasTransform inverse;
|
|
101
|
+
};
|
|
102
|
+
|
|
54
103
|
class Canvas2D {
|
|
55
104
|
public:
|
|
56
105
|
Canvas2D(int width, int height);
|
|
@@ -77,14 +126,30 @@ class Canvas2D {
|
|
|
77
126
|
void transform(float a, float b, float c, float d, float e, float f);
|
|
78
127
|
void set_transform(float a, float b, float c, float d, float e, float f);
|
|
79
128
|
void reset_transform() { state_.transform = CanvasTransform(); }
|
|
129
|
+
const CanvasTransform ¤t_transform() const { return state_.transform; }
|
|
80
130
|
|
|
81
131
|
/* -- style ------------------------------------------------------------- */
|
|
82
132
|
void set_fill_style(const std::string &css);
|
|
83
133
|
const std::string &fill_style() const { return state_.fill_style; }
|
|
84
134
|
void set_stroke_style(const std::string &css);
|
|
85
135
|
const std::string &stroke_style() const { return state_.stroke_style; }
|
|
136
|
+
/** Use a gradient created by this canvas (`0` restores the solid colour). */
|
|
137
|
+
void set_fill_gradient(uint64_t id);
|
|
138
|
+
void set_stroke_gradient(uint64_t id);
|
|
139
|
+
uint64_t fill_gradient_id() const { return state_.fill_gradient; }
|
|
140
|
+
uint64_t stroke_gradient_id() const { return state_.stroke_gradient; }
|
|
86
141
|
void set_line_width(float width) { state_.line_width = width > 0.0f ? width : 0.0f; }
|
|
87
142
|
float line_width() const { return state_.line_width; }
|
|
143
|
+
void set_line_cap(const std::string &value);
|
|
144
|
+
const std::string &line_cap() const { return state_.line_cap_string; }
|
|
145
|
+
void set_line_join(const std::string &value);
|
|
146
|
+
const std::string &line_join() const { return state_.line_join_string; }
|
|
147
|
+
void set_miter_limit(float limit);
|
|
148
|
+
float miter_limit() const { return state_.miter_limit; }
|
|
149
|
+
void set_line_dash(const std::vector<float> &segments);
|
|
150
|
+
const std::vector<float> &line_dash() const { return state_.line_dash; }
|
|
151
|
+
void set_line_dash_offset(float offset) { state_.line_dash_offset = offset; }
|
|
152
|
+
float line_dash_offset() const { return state_.line_dash_offset; }
|
|
88
153
|
void set_global_alpha(float alpha);
|
|
89
154
|
float global_alpha() const { return state_.global_alpha; }
|
|
90
155
|
void set_font(const std::string &font);
|
|
@@ -94,13 +159,22 @@ class Canvas2D {
|
|
|
94
159
|
void set_text_baseline(const std::string &value);
|
|
95
160
|
const std::string &text_baseline() const { return state_.text_baseline_string; }
|
|
96
161
|
|
|
162
|
+
/* -- gradients --------------------------------------------------------- */
|
|
163
|
+
CanvasGradient *create_linear_gradient(float x0, float y0, float x1, float y1);
|
|
164
|
+
CanvasGradient *create_radial_gradient(float x0, float y0, float r0, float x1, float y1,
|
|
165
|
+
float r1);
|
|
166
|
+
CanvasGradient *find_gradient(uint64_t id) const;
|
|
167
|
+
|
|
97
168
|
/* -- paths ------------------------------------------------------------- */
|
|
98
169
|
void begin_path();
|
|
99
170
|
void close_path();
|
|
100
171
|
void move_to(float x, float y);
|
|
101
172
|
void line_to(float x, float y);
|
|
102
173
|
void rect(float x, float y, float width, float height);
|
|
174
|
+
void round_rect(float x, float y, float width, float height, float radius);
|
|
103
175
|
void arc(float cx, float cy, float radius, float start, float end, bool counterclockwise);
|
|
176
|
+
void ellipse(float cx, float cy, float rx, float ry, float rotation, float start, float end,
|
|
177
|
+
bool counterclockwise);
|
|
104
178
|
void quadratic_curve_to(float cpx, float cpy, float x, float y);
|
|
105
179
|
void bezier_curve_to(float c1x, float c1y, float c2x, float c2y, float x, float y);
|
|
106
180
|
void fill();
|
|
@@ -132,6 +206,11 @@ class Canvas2D {
|
|
|
132
206
|
CanvasTransform transform;
|
|
133
207
|
float line_width = 1.0f;
|
|
134
208
|
float global_alpha = 1.0f;
|
|
209
|
+
int line_cap = 0; // 0 butt, 1 round, 2 square
|
|
210
|
+
int line_join = 0; // 0 miter, 1 round, 2 bevel
|
|
211
|
+
float miter_limit = 10.0f;
|
|
212
|
+
std::vector<float> line_dash;
|
|
213
|
+
float line_dash_offset = 0.0f;
|
|
135
214
|
FontSpec font;
|
|
136
215
|
std::string font_string = "10px sans-serif";
|
|
137
216
|
int text_align = 0; // 0 start/left, 1 center, 2 right/end
|
|
@@ -142,6 +221,10 @@ class Canvas2D {
|
|
|
142
221
|
std::string stroke_style = "#000000";
|
|
143
222
|
float fill_color[4] = {0.0f, 0.0f, 0.0f, 1.0f};
|
|
144
223
|
float stroke_color[4] = {0.0f, 0.0f, 0.0f, 1.0f};
|
|
224
|
+
uint64_t fill_gradient = 0;
|
|
225
|
+
uint64_t stroke_gradient = 0;
|
|
226
|
+
std::string line_cap_string = "butt";
|
|
227
|
+
std::string line_join_string = "miter";
|
|
145
228
|
};
|
|
146
229
|
|
|
147
230
|
/** Transform a user-space point into device space. */
|
|
@@ -151,8 +234,23 @@ class Canvas2D {
|
|
|
151
234
|
/** Add one device-space polygon loop to `out`. */
|
|
152
235
|
void flatten_arc(float cx, float cy, float radius, float start, float end, bool ccw,
|
|
153
236
|
CanvasSubpath *out) const;
|
|
154
|
-
|
|
155
|
-
|
|
237
|
+
/** Append a flattened arc to the current subpath, moving/line-to its start as
|
|
238
|
+
* the canvas path rules require. Shared by `arc` and `ellipse`. */
|
|
239
|
+
void append_path_points(const CanvasSubpath &points);
|
|
240
|
+
/** Paint descriptors for the current state. */
|
|
241
|
+
CanvasPaint fill_paint() const;
|
|
242
|
+
CanvasPaint stroke_paint() const;
|
|
243
|
+
CanvasPaint make_paint(uint64_t gradient_id, const float color[4]) const;
|
|
244
|
+
void stroke_subpath(const CanvasSubpath &points, bool closed, const CanvasPaint &paint);
|
|
245
|
+
/** Stroke one polyline with caps and joins; `half` is half the device-space
|
|
246
|
+
* line width. Used by `stroke_subpath` for both solid and dashed paths. */
|
|
247
|
+
void stroke_polyline(const CanvasSubpath &points, bool closed, float half,
|
|
248
|
+
const CanvasPaint &paint);
|
|
249
|
+
/** Split `points` into the "on" pieces of the current dash pattern. */
|
|
250
|
+
void dash_polyline(const CanvasSubpath &points, bool closed,
|
|
251
|
+
std::vector<CanvasSubpath> *out) const;
|
|
252
|
+
void paint_text(const std::string &text, float x, float y, const CanvasPaint &paint,
|
|
253
|
+
bool stroke);
|
|
156
254
|
|
|
157
255
|
Image image_;
|
|
158
256
|
uint64_t id_ = 0;
|
|
@@ -160,6 +258,7 @@ class Canvas2D {
|
|
|
160
258
|
State state_;
|
|
161
259
|
std::vector<State> stack_;
|
|
162
260
|
std::vector<CanvasSubpath> path_;
|
|
261
|
+
std::unordered_map<uint64_t, std::unique_ptr<CanvasGradient>> gradients_;
|
|
163
262
|
};
|
|
164
263
|
|
|
165
264
|
/** The canvas with the given process-wide id, or null. Used by the renderer. */
|