xbintsc 0.5.21 → 0.5.23
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/AGENTS.md +1 -1
- package/README.md +15 -5
- package/README.zh-CN.md +11 -4
- package/dist/src/cli/hints.js +11 -0
- package/dist/src/cli/hints.js.map +1 -1
- package/dist/src/cli/main.js +1 -1
- package/dist/src/driver/compiler.js +1 -0
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/driver/version.d.ts +1 -1
- package/dist/src/driver/version.js +1 -1
- package/dist/src/extensions/catalog.js +2 -1
- package/dist/src/extensions/catalog.js.map +1 -1
- package/dist/src/extensions/media/index.d.ts +27 -0
- package/dist/src/extensions/media/index.js +60 -0
- package/dist/src/extensions/media/index.js.map +1 -0
- package/dist/src/extensions/registry.d.ts +12 -0
- package/dist/src/extensions/registry.js +16 -0
- package/dist/src/extensions/registry.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/tests/cli/hints.test.js +15 -0
- package/dist/tests/cli/hints.test.js.map +1 -1
- package/dist/tests/cli/process.test.js +12 -0
- package/dist/tests/cli/process.test.js.map +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 +72 -0
- package/dist/tests/e2e/gui-example-canvas.test.js.map +1 -0
- package/dist/tests/e2e/gui-helpers.d.ts +3 -0
- package/dist/tests/e2e/gui-helpers.js +2 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-media-disabled.test.d.ts +9 -0
- package/dist/tests/e2e/gui-media-disabled.test.js +42 -0
- package/dist/tests/e2e/gui-media-disabled.test.js.map +1 -0
- package/dist/tests/e2e/gui-media.test.d.ts +6 -4
- package/dist/tests/e2e/gui-media.test.js +15 -12
- package/dist/tests/e2e/gui-media.test.js.map +1 -1
- package/dist/tests/e2e/unicode-output.test.d.ts +16 -0
- package/dist/tests/e2e/unicode-output.test.js +65 -0
- package/dist/tests/e2e/unicode-output.test.js.map +1 -0
- package/dist/tests/extensions/media.test.d.ts +1 -0
- package/dist/tests/extensions/media.test.js +58 -0
- package/dist/tests/extensions/media.test.js.map +1 -0
- package/dist/tests/extensions/registry.test.js +8 -0
- package/dist/tests/extensions/registry.test.js.map +1 -1
- package/dist/tests/lexer/identifiers.test.js +12 -0
- package/dist/tests/lexer/identifiers.test.js.map +1 -1
- package/dist/tests/lexer/scanner.test.js +8 -0
- package/dist/tests/lexer/scanner.test.js.map +1 -1
- package/dist/tests/lexer/strings.test.js +30 -0
- package/dist/tests/lexer/strings.test.js.map +1 -1
- package/doc/ai/cli.md +1 -1
- package/doc/ai/extensions.md +35 -22
- package/doc/ai/troubleshooting.md +9 -0
- package/doc/ai/zh-CN/cli.md +1 -1
- package/doc/ai/zh-CN/extensions.md +22 -9
- package/doc/ai/zh-CN/troubleshooting.md +8 -0
- package/doc/gui/build.md +37 -21
- package/doc/gui/canvas.md +47 -21
- package/doc/gui/known-issues.md +7 -6
- package/doc/gui/media.md +26 -11
- package/doc/gui.md +50 -34
- package/doc/zh-CN/gui/build.md +28 -16
- package/doc/zh-CN/gui/canvas.md +36 -17
- package/doc/zh-CN/gui/known-issues.md +6 -5
- package/doc/zh-CN/gui/media.md +21 -10
- package/doc/zh-CN/gui.md +43 -34
- package/package.json +2 -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/runtime/ext_gui/gui_media.cpp +69 -2
- package/runtime/ext_gui/media.cpp +34 -488
- package/runtime/ext_gui/media.h +54 -19
- package/runtime/ext_gui/media_api.h +65 -0
- package/runtime/ext_media/media.cpp +507 -0
- package/runtime/ext_media/media_bridge.c +35 -0
- package/scripts/build-gui.ts +6 -72
- package/scripts/build-media.ts +245 -0
- package/src/cli/hints.ts +11 -0
- package/src/cli/main.ts +1 -1
- package/src/driver/compiler.ts +1 -0
- package/src/driver/version.ts +1 -1
- package/src/extensions/catalog.ts +2 -1
- package/src/extensions/media/index.ts +67 -0
- package/src/extensions/registry.ts +24 -0
- package/src/index.ts +1 -0
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. */
|
|
@@ -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 `
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
156
|
-
fill_loops(pixels, width, height, std::vector<std::vector<CanvasPoint>>{loop},
|
|
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
|
|
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
|
|
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
|