xbintsc 0.5.37 → 0.5.41

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.
Files changed (166) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/codegen/generator/module.js +23 -7
  3. package/dist/src/codegen/generator/module.js.map +1 -1
  4. package/dist/src/codegen/generator/state.js +1 -0
  5. package/dist/src/codegen/generator/state.js.map +1 -1
  6. package/dist/src/codegen/generator/tables.js +1 -0
  7. package/dist/src/codegen/generator/tables.js.map +1 -1
  8. package/dist/src/driver/bundler/deferred.d.ts +63 -0
  9. package/dist/src/driver/bundler/deferred.js +279 -0
  10. package/dist/src/driver/bundler/deferred.js.map +1 -0
  11. package/dist/src/driver/bundler/merge.js +214 -2
  12. package/dist/src/driver/bundler/merge.js.map +1 -1
  13. package/dist/src/driver/runtime-objects.d.ts +1 -1
  14. package/dist/src/driver/runtime-objects.js +1 -0
  15. package/dist/src/driver/runtime-objects.js.map +1 -1
  16. package/dist/src/driver/version.d.ts +1 -1
  17. package/dist/src/driver/version.js +1 -1
  18. package/dist/src/extensions/gui/html.js +98 -15
  19. package/dist/src/extensions/gui/html.js.map +1 -1
  20. package/dist/src/extensions/gui/index.js +1 -0
  21. package/dist/src/extensions/gui/index.js.map +1 -1
  22. package/dist/src/extensions/gui/three.d.ts +4 -3
  23. package/dist/src/extensions/gui/three.js +4 -3
  24. package/dist/src/extensions/gui/three.js.map +1 -1
  25. package/dist/tests/codegen/closures.test.js +7 -0
  26. package/dist/tests/codegen/closures.test.js.map +1 -1
  27. package/dist/tests/e2e/compile-objects-classes.test.js +31 -0
  28. package/dist/tests/e2e/compile-objects-classes.test.js.map +1 -1
  29. package/dist/tests/e2e/gui-canvas-state.test.d.ts +9 -0
  30. package/dist/tests/e2e/gui-canvas-state.test.js +297 -0
  31. package/dist/tests/e2e/gui-canvas-state.test.js.map +1 -0
  32. package/dist/tests/e2e/gui-draw-perf.test.d.ts +22 -0
  33. package/dist/tests/e2e/gui-draw-perf.test.js +405 -0
  34. package/dist/tests/e2e/gui-draw-perf.test.js.map +1 -0
  35. package/dist/tests/e2e/gui-perf.test.d.ts +23 -0
  36. package/dist/tests/e2e/gui-perf.test.js +283 -0
  37. package/dist/tests/e2e/gui-perf.test.js.map +1 -0
  38. package/dist/tests/e2e/gui-script-import-order.test.d.ts +10 -0
  39. package/dist/tests/e2e/gui-script-import-order.test.js +169 -0
  40. package/dist/tests/e2e/gui-script-import-order.test.js.map +1 -0
  41. package/dist/tests/e2e/gui-webgl.test.js +4 -4
  42. package/dist/tests/e2e/gui-webgl.test.js.map +1 -1
  43. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.d.ts +8 -0
  44. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.js +336 -0
  45. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.js.map +1 -0
  46. package/dist/tests/e2e/gui-webgl2-shadow.test.d.ts +10 -0
  47. package/dist/tests/e2e/gui-webgl2-shadow.test.js +108 -0
  48. package/dist/tests/e2e/gui-webgl2-shadow.test.js.map +1 -0
  49. package/dist/tests/e2e/url.test.d.ts +6 -0
  50. package/dist/tests/e2e/url.test.js +114 -0
  51. package/dist/tests/e2e/url.test.js.map +1 -0
  52. package/dist/tests/extensions/gui.test.js +45 -10
  53. package/dist/tests/extensions/gui.test.js.map +1 -1
  54. package/doc/gui/api.md +28 -2
  55. package/doc/gui/canvas.md +110 -27
  56. package/doc/gui/known-issues.md +32 -10
  57. package/doc/gui/rendering.md +77 -0
  58. package/doc/gui/runtime.md +18 -3
  59. package/doc/gui/webgl.md +58 -29
  60. package/doc/gui-scripts.md +19 -19
  61. package/doc/gui.md +43 -9
  62. package/doc/implemented/fetch.md +5 -0
  63. package/doc/implemented/modules.md +13 -0
  64. package/doc/implemented/stdlib.md +17 -0
  65. package/doc/unimplemented/deviations.md +1 -0
  66. package/doc/zh-CN/gui/api.md +25 -2
  67. package/doc/zh-CN/gui/canvas.md +95 -26
  68. package/doc/zh-CN/gui/known-issues.md +21 -4
  69. package/doc/zh-CN/gui/rendering.md +59 -0
  70. package/doc/zh-CN/gui/runtime.md +13 -3
  71. package/doc/zh-CN/gui/webgl.md +30 -8
  72. package/doc/zh-CN/gui-scripts.md +14 -13
  73. package/doc/zh-CN/gui.md +32 -8
  74. package/doc/zh-CN/implemented/fetch.md +4 -0
  75. package/doc/zh-CN/implemented/modules.md +10 -0
  76. package/doc/zh-CN/implemented/stdlib.md +15 -0
  77. package/doc/zh-CN/unimplemented/deviations.md +1 -0
  78. package/package.json +7 -7
  79. package/runtime/ext_gui/browser.cpp +3 -1
  80. package/runtime/ext_gui/canvas.cpp +135 -89
  81. package/runtime/ext_gui/canvas.h +116 -0
  82. package/runtime/ext_gui/canvas_clip.cpp +143 -0
  83. package/runtime/ext_gui/canvas_composite.cpp +241 -0
  84. package/runtime/ext_gui/canvas_coverage.cpp +241 -0
  85. package/runtime/ext_gui/canvas_gradient.cpp +41 -4
  86. package/runtime/ext_gui/canvas_image_data.cpp +4 -0
  87. package/runtime/ext_gui/canvas_internal.h +34 -9
  88. package/runtime/ext_gui/canvas_raster.cpp +207 -178
  89. package/runtime/ext_gui/canvas_shadow.cpp +161 -0
  90. package/runtime/ext_gui/canvas_stroke.cpp +37 -9
  91. package/runtime/ext_gui/canvas_style.cpp +59 -0
  92. package/runtime/ext_gui/canvas_text.cpp +140 -0
  93. package/runtime/ext_gui/css.h +7 -0
  94. package/runtime/ext_gui/css_sheet.cpp +22 -0
  95. package/runtime/ext_gui/document.cpp +25 -1
  96. package/runtime/ext_gui/document.h +8 -0
  97. package/runtime/ext_gui/dom_api_canvas.cpp +16 -1
  98. package/runtime/ext_gui/dom_api_canvas_image.cpp +9 -0
  99. package/runtime/ext_gui/dom_api_canvas_state.cpp +217 -0
  100. package/runtime/ext_gui/dom_api_internal.h +36 -37
  101. package/runtime/ext_gui/dom_api_offscreen.cpp +213 -0
  102. package/runtime/ext_gui/dom_api_proto.cpp +18 -0
  103. package/runtime/ext_gui/dom_api_webgl.cpp +29 -9
  104. package/runtime/ext_gui/dom_api_webgl2.cpp +33 -1
  105. package/runtime/ext_gui/dom_api_webgl_internal.h +58 -0
  106. package/runtime/ext_gui/dom_api_webgl_source.cpp +18 -0
  107. package/runtime/ext_gui/gradient_values.cpp +12 -1
  108. package/runtime/ext_gui/gui.cpp +5 -0
  109. package/runtime/ext_gui/gui_canvas.cpp +4 -0
  110. package/runtime/ext_gui/gui_engine.h +32 -0
  111. package/runtime/ext_gui/gui_events.cpp +182 -58
  112. package/runtime/ext_gui/gui_media.cpp +3 -0
  113. package/runtime/ext_gui/image.cpp +69 -0
  114. package/runtime/ext_gui/image_bitmap_browser.cpp +11 -0
  115. package/runtime/ext_gui/layout.cpp +4 -0
  116. package/runtime/ext_gui/paint.cpp +19 -0
  117. package/runtime/ext_gui/paint_background.cpp +18 -1
  118. package/runtime/ext_gui/perf.cpp +171 -0
  119. package/runtime/ext_gui/perf.h +127 -0
  120. package/runtime/ext_gui/renderer.cpp +109 -41
  121. package/runtime/ext_gui/renderer.h +33 -9
  122. package/runtime/ext_gui/renderer_image.cpp +135 -65
  123. package/runtime/ext_gui/renderer_internal.h +15 -3
  124. package/runtime/ext_gui/renderer_text.cpp +24 -12
  125. package/runtime/ext_gui/script.cpp +31 -0
  126. package/runtime/ext_gui/style.cpp +101 -30
  127. package/runtime/ext_gui/style.h +6 -3
  128. package/runtime/ext_gui/style_print.cpp +7 -1
  129. package/runtime/ext_gui/text.cpp +23 -4
  130. package/runtime/ext_gui/text.h +29 -0
  131. package/runtime/ext_gui/text_cache.cpp +118 -0
  132. package/runtime/ext_gui/text_glyph_cache.cpp +100 -0
  133. package/runtime/ext_gui/webgl.h +48 -146
  134. package/runtime/ext_gui/webgl_blit.cpp +12 -1
  135. package/runtime/ext_gui/webgl_blit_depth.cpp +270 -0
  136. package/runtime/ext_gui/webgl_buffer.cpp +59 -1
  137. package/runtime/ext_gui/webgl_buffer_objects.cpp +106 -9
  138. package/runtime/ext_gui/webgl_clear.cpp +3 -0
  139. package/runtime/ext_gui/webgl_context.cpp +29 -3
  140. package/runtime/ext_gui/webgl_draw.cpp +75 -270
  141. package/runtime/ext_gui/webgl_draw_buffers.cpp +1 -0
  142. package/runtime/ext_gui/webgl_draw_setup.cpp +321 -0
  143. package/runtime/ext_gui/webgl_draw_state.h +130 -0
  144. package/runtime/ext_gui/webgl_glsl.h +3 -0
  145. package/runtime/ext_gui/webgl_glsl_scan.cpp +3 -8
  146. package/runtime/ext_gui/webgl_glsl_unroll.cpp +283 -0
  147. package/runtime/ext_gui/webgl_msl.cpp +4 -0
  148. package/runtime/ext_gui/webgl_objects.h +148 -0
  149. package/runtime/ext_gui/webgl_pipeline.cpp +8 -8
  150. package/runtime/ext_gui/webgl_program_internal.h +35 -0
  151. package/runtime/ext_gui/webgl_shader.cpp +34 -36
  152. package/runtime/ext_gui/webgl_webgl2.cpp +36 -0
  153. package/runtime/ext_gui/window.cpp +56 -1
  154. package/runtime/rt.h +3 -0
  155. package/runtime/xt_fetch/async.inc +60 -0
  156. package/runtime/xt_url_object.c +463 -0
  157. package/src/codegen/generator/module.ts +22 -7
  158. package/src/codegen/generator/state.ts +1 -0
  159. package/src/codegen/generator/tables.ts +1 -0
  160. package/src/driver/bundler/deferred.ts +351 -0
  161. package/src/driver/bundler/merge.ts +219 -2
  162. package/src/driver/runtime-objects.ts +1 -0
  163. package/src/driver/version.ts +1 -1
  164. package/src/extensions/gui/html.ts +100 -17
  165. package/src/extensions/gui/index.ts +1 -0
  166. package/src/extensions/gui/three.ts +4 -3
@@ -19,8 +19,6 @@ namespace canvas_detail {
19
19
 
20
20
  namespace {
21
21
 
22
- constexpr int kSubSamples = 4;
23
-
24
22
  int clamp_byte(float value) {
25
23
  int result = (int)(value + 0.5f);
26
24
  if (result < 0) result = 0;
@@ -28,12 +26,6 @@ int clamp_byte(float value) {
28
26
  return result;
29
27
  }
30
28
 
31
- /** One edge crossing on the current sub-scanline. */
32
- struct Crossing {
33
- float x;
34
- int winding;
35
- };
36
-
37
29
  /** Resolve `paint` at a device-space pixel centre. Gradients are defined in
38
30
  * user space, so the pixel is mapped back through the inverse transform. */
39
31
  void paint_color_at(const CanvasPaint &paint, float x, float y, float out[4]) {
@@ -54,175 +46,157 @@ void paint_color_at(const CanvasPaint &paint, float x, float y, float out[4]) {
54
46
  out[0] = out[1] = out[2] = out[3] = 0.0f;
55
47
  }
56
48
 
49
+ /** True when the paint is a single colour, so the per-pixel colour resolution
50
+ * (a branch on gradient/pattern and a 4-float copy) can be hoisted out of the
51
+ * pixel loop. A solid paint is the common case in a multi-element scene. */
52
+ bool paint_is_solid(const CanvasPaint &paint) {
53
+ return paint.gradient == nullptr && paint.pattern == nullptr && paint.color != nullptr;
54
+ }
55
+
56
+ void copy_color(const float color[4], float out[4]) {
57
+ out[0] = color[0];
58
+ out[1] = color[1];
59
+ out[2] = color[2];
60
+ out[3] = color[3];
61
+ }
62
+
63
+ unsigned char color_byte(float value) {
64
+ value = value < 0.0f ? 0.0f : (value > 1.0f ? 1.0f : value);
65
+ return (unsigned char)(value * 255.0f + 0.5f);
66
+ }
67
+
68
+ /** True when a source-over paint writes its colour unchanged at full coverage
69
+ * (opaque colour, opaque globalAlpha): the rasteriser can store bytes directly
70
+ * instead of running the compositing math per pixel. */
71
+ bool paint_is_opaque_source(const CanvasPaint &paint, float alpha) {
72
+ return paint_is_solid(paint) && paint.composite == kCanvasCompositeSourceOver &&
73
+ alpha >= 0.999f && paint.color[3] >= 0.999f;
74
+ }
75
+
57
76
  } // namespace
58
77
 
59
78
  void blend_pixel(unsigned char *pixel, const float color[4], float coverage, float alpha) {
60
- /* Fully opaque source-over is a plain copy; this is the hot path for paper
61
- textures, terrain and the opaque parts of every sprite. */
62
- if (alpha >= 0.999f && coverage >= 0.999f && color[3] >= 0.999f) {
63
- pixel[0] = (unsigned char)clamp_byte(color[0] * 255.0f);
64
- pixel[1] = (unsigned char)clamp_byte(color[1] * 255.0f);
65
- pixel[2] = (unsigned char)clamp_byte(color[2] * 255.0f);
66
- pixel[3] = 255;
67
- return;
68
- }
69
- float sa = color[3] * coverage * alpha;
70
- if (sa <= 0.0f) return;
71
- if (sa > 1.0f) sa = 1.0f;
72
- float da = pixel[3] / 255.0f;
73
- float out_a = sa + da * (1.0f - sa);
74
- if (out_a <= 0.0f) {
75
- pixel[0] = pixel[1] = pixel[2] = pixel[3] = 0;
76
- return;
77
- }
78
- const float inv = 1.0f / out_a;
79
- pixel[0] = (unsigned char)clamp_byte((color[0] * sa + (pixel[0] / 255.0f) * da * (1.0f - sa)) * inv * 255.0f);
80
- pixel[1] = (unsigned char)clamp_byte((color[1] * sa + (pixel[1] / 255.0f) * da * (1.0f - sa)) * inv * 255.0f);
81
- pixel[2] = (unsigned char)clamp_byte((color[2] * sa + (pixel[2] / 255.0f) * da * (1.0f - sa)) * inv * 255.0f);
82
- pixel[3] = (unsigned char)clamp_byte(out_a * 255.0f);
79
+ blend_pixel_mode(pixel, color, coverage, alpha, kCanvasCompositeSourceOver);
83
80
  }
84
81
 
85
- /** Add the horizontal span `[xa, xb)` into a bbox-relative coverage row. */
86
- static void add_span(float *row, int rowOriginX, float xa, float xb, float weight, int clipX0,
87
- int clipX1) {
88
- if (xb <= xa) return;
89
- if (xa < (float)clipX0) xa = (float)clipX0;
90
- if (xb > (float)clipX1) xb = (float)clipX1;
91
- if (xb <= xa) return;
92
- int ia = (int)std::floor(xa);
93
- int ib = (int)std::ceil(xb) - 1;
94
- if (ib < ia) return;
95
- if (ia == ib) {
96
- row[ia - rowOriginX] += (xb - xa) * weight;
97
- return;
82
+ /** Source-over with a clip mask applied to the coverage. */
83
+ static void blend_clipped(unsigned char *pixel, const float color[4], float coverage, float alpha,
84
+ const CanvasPaint &paint, const CanvasClipMask *clip, int x, int y) {
85
+ if (clip != nullptr) {
86
+ const float clipCoverage = clip->at(x, y) / 255.0f;
87
+ if (clipCoverage <= 0.0f) return;
88
+ coverage *= clipCoverage;
98
89
  }
99
- row[ia - rowOriginX] += ((float)(ia + 1) - xa) * weight;
100
- for (int x = ia + 1; x < ib; x++) row[x - rowOriginX] += weight;
101
- float last = xb - (float)ib;
102
- if (last > 0.0f) row[ib - rowOriginX] += last * weight;
90
+ blend_pixel_mode(pixel, color, coverage, alpha, paint.composite);
103
91
  }
104
92
 
105
93
  void fill_loops(std::vector<unsigned char> &pixels, int width, int height,
106
94
  const std::vector<std::vector<CanvasPoint>> &loops, const CanvasPaint &paint,
107
- float alpha, bool even_odd) {
108
- if (width <= 0 || height <= 0 || loops.empty()) return;
109
- bool havePoint = false;
110
- float minX = 0.0f, maxX = 0.0f, minY = 0.0f, maxY = 0.0f;
111
- for (const std::vector<CanvasPoint> &loop : loops) {
112
- for (const CanvasPoint &point : loop) {
113
- if (!havePoint) {
114
- minX = maxX = point.x;
115
- minY = maxY = point.y;
116
- havePoint = true;
117
- } else {
118
- minX = std::min(minX, point.x);
119
- maxX = std::max(maxX, point.x);
120
- minY = std::min(minY, point.y);
121
- maxY = std::max(maxY, point.y);
122
- }
123
- }
124
- }
125
- if (!havePoint) return;
126
- int x0 = std::max(0, (int)std::floor(minX));
127
- int x1 = std::min(width, (int)std::ceil(maxX));
128
- int y0 = std::max(0, (int)std::floor(minY));
129
- int y1 = std::min(height, (int)std::ceil(maxY));
130
- if (x1 <= x0 || y1 <= y0) return;
131
-
132
- int boxWidth = x1 - x0;
133
- std::vector<float> coverage((size_t)boxWidth * (size_t)(y1 - y0), 0.0f);
134
- const float weight = 1.0f / (float)kSubSamples;
135
-
136
- std::vector<Crossing> crossings;
137
- for (int py = y0; py < y1; py++) {
138
- float *row = coverage.data() + (size_t)(py - y0) * (size_t)boxWidth;
139
- for (int sample = 0; sample < kSubSamples; sample++) {
140
- float sy = (float)py + ((float)sample + 0.5f) / (float)kSubSamples;
141
- crossings.clear();
142
- for (const std::vector<CanvasPoint> &loop : loops) {
143
- if (loop.size() < 3) continue;
144
- for (size_t i = 0; i < loop.size(); i++) {
145
- const CanvasPoint &p0 = loop[i];
146
- const CanvasPoint &p1 = loop[(i + 1) % loop.size()];
147
- if (p0.y == p1.y) continue;
148
- float top = std::min(p0.y, p1.y);
149
- float bottom = std::max(p0.y, p1.y);
150
- if (sy < top || sy >= bottom) continue;
151
- float t = (sy - p0.y) / (p1.y - p0.y);
152
- Crossing crossing;
153
- crossing.x = p0.x + t * (p1.x - p0.x);
154
- crossing.winding = p1.y > p0.y ? 1 : -1;
155
- crossings.push_back(crossing);
156
- }
157
- }
158
- if (crossings.empty()) continue;
159
- std::sort(crossings.begin(), crossings.end(),
160
- [](const Crossing &a, const Crossing &b) { return a.x < b.x; });
161
- if (even_odd) {
162
- /* Every second crossing toggles the covered span, regardless of the
163
- * direction each contour is wound. */
164
- for (size_t i = 0; i + 1 < crossings.size(); i += 2) {
165
- add_span(row, x0, crossings[i].x, crossings[i + 1].x, weight, x0, x1);
166
- }
167
- continue;
168
- }
169
- int winding = 0;
170
- float spanStart = 0.0f;
171
- for (const Crossing &crossing : crossings) {
172
- if (winding == 0) spanStart = crossing.x;
173
- winding += crossing.winding;
174
- if (winding == 0) add_span(row, x0, spanStart, crossing.x, weight, x0, x1);
175
- }
176
- }
177
- }
178
-
179
- for (int py = y0; py < y1; py++) {
180
- const float *row = coverage.data() + (size_t)(py - y0) * (size_t)boxWidth;
181
- unsigned char *dst = pixels.data() + ((size_t)py * (size_t)width + (size_t)x0) * 4;
182
- for (int px = 0; px < boxWidth; px++) {
183
- float c = row[px];
95
+ float alpha, bool even_odd, const CanvasClipMask *clip) {
96
+ CoverageGrid coverage;
97
+ compute_coverage(width, height, loops, even_odd, &coverage);
98
+ if (coverage.empty()) return;
99
+ const bool solid = paint_is_solid(paint);
100
+ float solid_color[4];
101
+ if (solid) copy_color(paint.color, solid_color);
102
+ const int rows = coverage.y1 - coverage.y0;
103
+ for (int r = 0; r < rows; r++) {
104
+ const uint32_t begin = coverage.row_offset[(size_t)r];
105
+ const uint32_t end = coverage.row_offset[(size_t)r + 1];
106
+ if (begin >= end) continue;
107
+ const int py = coverage.y0 + r;
108
+ const int rx = coverage.row_x[(size_t)r];
109
+ const float *row = coverage.values.data() + begin;
110
+ const int row_width = (int)(end - begin);
111
+ unsigned char *dst = pixels.data() + ((size_t)py * (size_t)width + (size_t)rx) * 4;
112
+ for (int i = 0; i < row_width; i++) {
113
+ float c = row[i];
184
114
  if (c <= 0.0f) continue;
185
115
  if (c > 1.0f) c = 1.0f;
186
116
  float color[4];
187
- paint_color_at(paint, (float)(x0 + px) + 0.5f, (float)py + 0.5f, color);
188
- blend_pixel(dst + (size_t)px * 4, color, c, alpha);
117
+ if (solid) {
118
+ copy_color(solid_color, color);
119
+ } else {
120
+ paint_color_at(paint, (float)(rx + i) + 0.5f, (float)py + 0.5f, color);
121
+ }
122
+ blend_clipped(dst + (size_t)i * 4, color, c, alpha, paint, clip, rx + i, py);
189
123
  }
190
124
  }
191
125
  }
192
126
 
193
127
  void fill_loop(std::vector<unsigned char> &pixels, int width, int height,
194
128
  const std::vector<CanvasPoint> &loop, const CanvasPaint &paint, float alpha,
195
- bool even_odd) {
129
+ bool even_odd, const CanvasClipMask *clip) {
196
130
  fill_loops(pixels, width, height, std::vector<std::vector<CanvasPoint>>{loop}, paint, alpha,
197
- even_odd);
131
+ even_odd, clip);
198
132
  }
199
133
 
200
134
  void fill_rect(std::vector<unsigned char> &pixels, int width, int height, float x0, float y0,
201
- float x1, float y1, const CanvasPaint &paint, float alpha) {
135
+ float x1, float y1, const CanvasPaint &paint, float alpha,
136
+ const CanvasClipMask *clip) {
202
137
  if (x1 < x0) std::swap(x0, x1);
203
138
  if (y1 < y0) std::swap(y0, y1);
204
139
  int ix0 = std::max(0, (int)std::floor(x0));
205
140
  int ix1 = std::min(width, (int)std::ceil(x1));
206
141
  int iy0 = std::max(0, (int)std::floor(y0));
207
142
  int iy1 = std::min(height, (int)std::ceil(y1));
143
+ const bool solid = paint_is_solid(paint);
144
+ float solid_color[4];
145
+ if (solid) copy_color(paint.color, solid_color);
146
+ /* A fully opaque solid fill at full coverage is a byte store: the same bytes
147
+ * `blend_pixel_mode` would compute, without the per-pixel compositing math.
148
+ * Fractional edges (and clipped or translucent fills) keep the general path,
149
+ * so the output is unchanged for every other case. */
150
+ unsigned char opaque_bytes[4] = {0, 0, 0, 0};
151
+ const bool opaque = paint_is_opaque_source(paint, alpha) && clip == nullptr;
152
+ if (opaque) {
153
+ opaque_bytes[0] = color_byte(solid_color[0]);
154
+ opaque_bytes[1] = color_byte(solid_color[1]);
155
+ opaque_bytes[2] = color_byte(solid_color[2]);
156
+ opaque_bytes[3] = 255;
157
+ }
208
158
  for (int py = iy0; py < iy1; py++) {
209
159
  float cy = std::min(y1, (float)(py + 1)) - std::max(y0, (float)py);
210
160
  if (cy <= 0.0f) continue;
211
161
  unsigned char *dst = pixels.data() + ((size_t)py * (size_t)width + (size_t)ix0) * 4;
162
+ if (opaque && cy >= 0.999f) {
163
+ for (int px = ix0; px < ix1; px++) {
164
+ float cx = std::min(x1, (float)(px + 1)) - std::max(x0, (float)px);
165
+ if (cx <= 0.0f) continue;
166
+ unsigned char *pixel = dst + (size_t)(px - ix0) * 4;
167
+ if (cx >= 0.999f) {
168
+ std::memcpy(pixel, opaque_bytes, 4);
169
+ } else {
170
+ float color[4];
171
+ copy_color(solid_color, color);
172
+ blend_pixel(pixel, color, cx, alpha);
173
+ }
174
+ }
175
+ continue;
176
+ }
212
177
  for (int px = ix0; px < ix1; px++) {
213
178
  float cx = std::min(x1, (float)(px + 1)) - std::max(x0, (float)px);
214
179
  if (cx <= 0.0f) continue;
215
180
  float color[4];
216
- paint_color_at(paint, (float)px + 0.5f, (float)py + 0.5f, color);
217
- blend_pixel(dst + (size_t)(px - ix0) * 4, color, cx * cy, alpha);
181
+ if (solid) {
182
+ copy_color(solid_color, color);
183
+ } else {
184
+ paint_color_at(paint, (float)px + 0.5f, (float)py + 0.5f, color);
185
+ }
186
+ blend_clipped(dst + (size_t)(px - ix0) * 4, color, cx * cy, alpha, paint, clip, px, py);
218
187
  }
219
188
  }
220
189
  }
221
190
 
222
191
  void clear_rect(std::vector<unsigned char> &pixels, int width, int height, float x0, float y0,
223
- float x1, float y1) {
192
+ float x1, float y1, const CanvasClipMask *clip) {
224
193
  if (x1 < x0) std::swap(x0, x1);
225
194
  if (y1 < y0) std::swap(y0, y1);
195
+ if (clip == nullptr && x0 <= 0.0f && y0 <= 0.0f && x1 >= (float)width &&
196
+ y1 >= (float)height) {
197
+ std::fill(pixels.begin(), pixels.end(), (unsigned char)0);
198
+ return;
199
+ }
226
200
  int ix0 = std::max(0, (int)std::floor(x0));
227
201
  int ix1 = std::min(width, (int)std::ceil(x1));
228
202
  int iy0 = std::max(0, (int)std::floor(y0));
@@ -235,7 +209,9 @@ void clear_rect(std::vector<unsigned char> &pixels, int width, int height, float
235
209
  float cx = std::min(x1, (float)(px + 1)) - std::max(x0, (float)px);
236
210
  if (cx <= 0.0f) continue;
237
211
  unsigned char *pixel = dst + (size_t)(px - ix0) * 4;
238
- float keep = 1.0f - cx * cy;
212
+ float amount = cx * cy;
213
+ if (clip != nullptr) amount *= clip->at(px, py) / 255.0f;
214
+ float keep = 1.0f - amount;
239
215
  if (keep <= 0.0f) {
240
216
  pixel[0] = pixel[1] = pixel[2] = pixel[3] = 0;
241
217
  } else {
@@ -245,9 +221,55 @@ void clear_rect(std::vector<unsigned char> &pixels, int width, int height, float
245
221
  }
246
222
  }
247
223
 
224
+ /** Sample one source image pixel at continuous coordinates (`0..width`).
225
+ * `smooth` selects bilinear filtering, otherwise the containing texel is used. */
226
+ static void sample_source(const Image &source, float fx, float fy, bool smooth, float out[4]) {
227
+ auto texel_at = [&](int x, int y, float outPixel[4]) {
228
+ if (x < 0) x = 0;
229
+ if (y < 0) y = 0;
230
+ if (x >= source.width) x = source.width - 1;
231
+ if (y >= source.height) y = source.height - 1;
232
+ const unsigned char *texel =
233
+ source.pixels.data() + ((size_t)y * (size_t)source.width + (size_t)x) * 4;
234
+ for (int i = 0; i < 4; i++) outPixel[i] = texel[i] / 255.0f;
235
+ };
236
+ if (!smooth) {
237
+ texel_at((int)fx, (int)fy, out);
238
+ return;
239
+ }
240
+ const float x = fx - 0.5f;
241
+ const float y = fy - 0.5f;
242
+ int x0 = (int)std::floor(x);
243
+ int y0 = (int)std::floor(y);
244
+ const float tx = x - (float)x0;
245
+ const float ty = y - (float)y0;
246
+ float top[4];
247
+ float bottom[4];
248
+ float a[4];
249
+ float b[4];
250
+ texel_at(x0, y0, a);
251
+ texel_at(x0 + 1, y0, b);
252
+ for (int i = 0; i < 4; i++) top[i] = a[i] + (b[i] - a[i]) * tx;
253
+ texel_at(x0, y0 + 1, a);
254
+ texel_at(x0 + 1, y0 + 1, b);
255
+ for (int i = 0; i < 4; i++) bottom[i] = a[i] + (b[i] - a[i]) * tx;
256
+ for (int i = 0; i < 4; i++) out[i] = top[i] + (bottom[i] - top[i]) * ty;
257
+ }
258
+
259
+ /** Source pixel centre for a destination pixel of `drawImage`. */
260
+ static void draw_image_pixel(std::vector<unsigned char> &pixels, int width, const Image &source,
261
+ const CanvasPaint &paint, int px, int py, float u, float v, float sx,
262
+ float sy, float sw, float sh, float alpha, bool smooth,
263
+ const CanvasClipMask *clip) {
264
+ float sampled[4];
265
+ sample_source(source, sx + u * sw, sy + v * sh, smooth, sampled);
266
+ unsigned char *dst = pixels.data() + ((size_t)py * (size_t)width + (size_t)px) * 4;
267
+ blend_clipped(dst, sampled, 1.0f, alpha, paint, clip, px, py);
268
+ }
269
+
248
270
  void draw_image(std::vector<unsigned char> &pixels, int width, int height, const Image &source,
249
271
  float dx, float dy, float dw, float dh, float sx, float sy, float sw, float sh,
250
- float alpha) {
272
+ float alpha, bool smooth, const CanvasClipMask *clip) {
251
273
  if (source.width <= 0 || source.height <= 0 || dw <= 0.0f || dh <= 0.0f || sw <= 0.0f ||
252
274
  sh <= 0.0f)
253
275
  return;
@@ -255,30 +277,46 @@ void draw_image(std::vector<unsigned char> &pixels, int width, int height, const
255
277
  int ix1 = std::min(width, (int)std::ceil(dx + dw));
256
278
  int iy0 = std::max(0, (int)std::floor(dy));
257
279
  int iy1 = std::min(height, (int)std::ceil(dy + dh));
280
+ const CanvasPaint paint;
281
+ /* A 1:1 blit at integer offsets samples exactly one texel per pixel: skip the
282
+ * per-pixel divide and the four bilinear taps. Sprite/offscreen-canvas
283
+ * compositing in a multi-element scene is usually this case. */
284
+ const int dx_int = (int)std::lround(dx);
285
+ const int dy_int = (int)std::lround(dy);
286
+ const int sx_int = (int)std::lround(sx);
287
+ const int sy_int = (int)std::lround(sy);
288
+ const bool one_to_one = std::fabs(dw - sw) < 0.001f && std::fabs(dh - sh) < 0.001f &&
289
+ std::fabs(dx - (float)dx_int) < 0.001f &&
290
+ std::fabs(dy - (float)dy_int) < 0.001f &&
291
+ std::fabs(sx - (float)sx_int) < 0.001f &&
292
+ std::fabs(sy - (float)sy_int) < 0.001f;
293
+ if (one_to_one) {
294
+ for (int py = iy0; py < iy1; py++) {
295
+ int texelY = sy_int + (py - dy_int);
296
+ if (texelY < 0) texelY = 0;
297
+ if (texelY >= source.height) texelY = source.height - 1;
298
+ const unsigned char *source_row =
299
+ source.pixels.data() + (size_t)texelY * (size_t)source.width * 4;
300
+ unsigned char *dst = pixels.data() + ((size_t)py * (size_t)width + (size_t)ix0) * 4;
301
+ for (int px = ix0; px < ix1; px++) {
302
+ int texelX = sx_int + (px - dx_int);
303
+ if (texelX < 0) texelX = 0;
304
+ if (texelX >= source.width) texelX = source.width - 1;
305
+ const unsigned char *texel = source_row + (size_t)texelX * 4;
306
+ float sampled[4] = {texel[0] / 255.0f, texel[1] / 255.0f, texel[2] / 255.0f,
307
+ texel[3] / 255.0f};
308
+ blend_clipped(dst + (size_t)(px - ix0) * 4, sampled, 1.0f, alpha, paint, clip, px, py);
309
+ }
310
+ }
311
+ return;
312
+ }
258
313
  for (int py = iy0; py < iy1; py++) {
259
314
  for (int px = ix0; px < ix1; px++) {
260
315
  float u = ((float)px + 0.5f - dx) / dw;
261
316
  float v = ((float)py + 0.5f - dy) / dh;
262
317
  if (u < 0.0f || u >= 1.0f || v < 0.0f || v >= 1.0f) continue;
263
- int sxi = (int)(sx + u * sw);
264
- int syi = (int)(sy + v * sh);
265
- if (sxi < 0) sxi = 0;
266
- if (syi < 0) syi = 0;
267
- if (sxi >= source.width) sxi = source.width - 1;
268
- if (syi >= source.height) syi = source.height - 1;
269
- const unsigned char *texel =
270
- source.pixels.data() + ((size_t)syi * (size_t)source.width + (size_t)sxi) * 4;
271
- unsigned char *dst = pixels.data() + ((size_t)py * (size_t)width + (size_t)px) * 4;
272
- if (alpha >= 0.999f) {
273
- const unsigned char ta = texel[3];
274
- if (ta == 255) {
275
- dst[0] = texel[0]; dst[1] = texel[1]; dst[2] = texel[2]; dst[3] = 255;
276
- continue;
277
- }
278
- if (ta == 0) continue;
279
- }
280
- float color[4] = {texel[0] / 255.0f, texel[1] / 255.0f, texel[2] / 255.0f, texel[3] / 255.0f};
281
- blend_pixel(dst, color, 1.0f, alpha);
318
+ draw_image_pixel(pixels, width, source, paint, px, py, u, v, sx, sy, sw, sh, alpha, smooth,
319
+ clip);
282
320
  }
283
321
  }
284
322
  }
@@ -287,7 +325,7 @@ void draw_image(std::vector<unsigned char> &pixels, int width, int height, const
287
325
  void draw_image_affine(std::vector<unsigned char> &pixels, int width, int height,
288
326
  const Image &source, float dx, float dy, float dw, float dh, float sx,
289
327
  float sy, float sw, float sh, const CanvasTransform &transform,
290
- float alpha) {
328
+ float alpha, bool smooth, const CanvasClipMask *clip) {
291
329
  if (source.width <= 0 || source.height <= 0 || dw <= 0.0f || dh <= 0.0f || sw <= 0.0f ||
292
330
  sh <= 0.0f)
293
331
  return;
@@ -312,6 +350,7 @@ void draw_image_affine(std::vector<unsigned char> &pixels, int width, int height
312
350
  int ix1 = std::min(width, (int)std::ceil(maxX));
313
351
  int iy0 = std::max(0, (int)std::floor(minY));
314
352
  int iy1 = std::min(height, (int)std::ceil(maxY));
353
+ const CanvasPaint paint;
315
354
  for (int py = iy0; py < iy1; py++) {
316
355
  for (int px = ix0; px < ix1; px++) {
317
356
  float ux = 0.0f;
@@ -320,25 +359,8 @@ void draw_image_affine(std::vector<unsigned char> &pixels, int width, int height
320
359
  float u = (ux - dx) / dw;
321
360
  float v = (uy - dy) / dh;
322
361
  if (u < 0.0f || u >= 1.0f || v < 0.0f || v >= 1.0f) continue;
323
- int sxi = (int)(sx + u * sw);
324
- int syi = (int)(sy + v * sh);
325
- if (sxi < 0) sxi = 0;
326
- if (syi < 0) syi = 0;
327
- if (sxi >= source.width) sxi = source.width - 1;
328
- if (syi >= source.height) syi = source.height - 1;
329
- const unsigned char *texel =
330
- source.pixels.data() + ((size_t)syi * (size_t)source.width + (size_t)sxi) * 4;
331
- unsigned char *dst = pixels.data() + ((size_t)py * (size_t)width + (size_t)px) * 4;
332
- if (alpha >= 0.999f) {
333
- const unsigned char ta = texel[3];
334
- if (ta == 255) {
335
- dst[0] = texel[0]; dst[1] = texel[1]; dst[2] = texel[2]; dst[3] = 255;
336
- continue;
337
- }
338
- if (ta == 0) continue;
339
- }
340
- float color[4] = {texel[0] / 255.0f, texel[1] / 255.0f, texel[2] / 255.0f, texel[3] / 255.0f};
341
- blend_pixel(dst, color, 1.0f, alpha);
362
+ draw_image_pixel(pixels, width, source, paint, px, py, u, v, sx, sy, sw, sh, alpha, smooth,
363
+ clip);
342
364
  }
343
365
  }
344
366
  }
@@ -346,7 +368,7 @@ void draw_image_affine(std::vector<unsigned char> &pixels, int width, int height
346
368
  void blit_glyph(std::vector<unsigned char> &pixels, int width, int height,
347
369
  const std::vector<unsigned char> &glyph, int glyphWidth, int glyphHeight, int left,
348
370
  int top, float x, float baselineY, float scale_x, const CanvasPaint &paint,
349
- float alpha) {
371
+ float alpha, const CanvasClipMask *clip) {
350
372
  if (glyphWidth <= 0 || glyphHeight <= 0) return;
351
373
  if (scale_x <= 0.0f) scale_x = 1.0f;
352
374
  bool condense = scale_x < 0.999f;
@@ -354,6 +376,9 @@ void blit_glyph(std::vector<unsigned char> &pixels, int width, int height,
354
376
  : glyphWidth;
355
377
  int originX = (int)std::lround(x) + (int)std::lround((float)left * scale_x);
356
378
  int originY = (int)std::lround(baselineY) - top;
379
+ const bool solid = paint_is_solid(paint);
380
+ float solid_color[4];
381
+ if (solid) copy_color(paint.color, solid_color);
357
382
  for (int gy = 0; gy < glyphHeight; gy++) {
358
383
  int py = originY + gy;
359
384
  if (py < 0 || py >= height) continue;
@@ -365,9 +390,13 @@ void blit_glyph(std::vector<unsigned char> &pixels, int width, int height,
365
390
  float coverage = glyph[(size_t)gy * (size_t)glyphWidth + (size_t)sourceX] / 255.0f;
366
391
  if (coverage <= 0.0f) continue;
367
392
  float color[4];
368
- paint_color_at(paint, (float)px + 0.5f, (float)py + 0.5f, color);
369
- blend_pixel(pixels.data() + ((size_t)py * (size_t)width + (size_t)px) * 4, color, coverage,
370
- alpha);
393
+ if (solid) {
394
+ copy_color(solid_color, color);
395
+ } else {
396
+ paint_color_at(paint, (float)px + 0.5f, (float)py + 0.5f, color);
397
+ }
398
+ blend_clipped(pixels.data() + ((size_t)py * (size_t)width + (size_t)px) * 4, color,
399
+ coverage, alpha, paint, clip, px, py);
371
400
  }
372
401
  }
373
402
  }
@@ -0,0 +1,161 @@
1
+ /*
2
+ * xbintsc GUI engine — Canvas2D shadows and clip-region compositing.
3
+ *
4
+ * A drawing operation that has a visible `shadowColor` is rendered into a
5
+ * scratch layer first, so the shadow is derived from exactly the pixels the
6
+ * operation covers (including glyph coverage and image alpha). The layer's
7
+ * alpha is offset and box-blurred, tinted with `shadowColor`, composited, and
8
+ * only then is the operation's own colour composited on top.
9
+ *
10
+ * The scratch layer is deliberately drawn *without* the clip mask: the shadow
11
+ * must be able to leave the clip region and be clipped once, at composite time.
12
+ */
13
+
14
+ #include "canvas_internal.h"
15
+
16
+ #include <algorithm>
17
+ #include <cmath>
18
+ #include <utility>
19
+ #include <vector>
20
+
21
+ namespace xtgui {
22
+
23
+ namespace {
24
+
25
+ void box_blur_pass(std::vector<float> &source, std::vector<float> &temp, int width, int height,
26
+ int radius) {
27
+ if (radius <= 0) return;
28
+ const float scale = 1.0f / (float)(radius * 2 + 1);
29
+ temp.resize(source.size());
30
+ /* Horizontal. */
31
+ for (int y = 0; y < height; y++) {
32
+ const float *row = source.data() + (size_t)y * (size_t)width;
33
+ float *out = temp.data() + (size_t)y * (size_t)width;
34
+ float sum = 0.0f;
35
+ for (int x = -radius; x <= radius; x++) sum += row[std::min(width - 1, std::max(0, x))];
36
+ for (int x = 0; x < width; x++) {
37
+ out[x] = sum * scale;
38
+ const int remove = std::min(width - 1, std::max(0, x - radius));
39
+ const int add = std::min(width - 1, std::max(0, x + radius + 1));
40
+ sum += row[add] - row[remove];
41
+ }
42
+ }
43
+ /* Vertical. */
44
+ for (int x = 0; x < width; x++) {
45
+ float sum = 0.0f;
46
+ for (int y = -radius; y <= radius; y++) {
47
+ sum += temp[(size_t)std::min(height - 1, std::max(0, y)) * (size_t)width + (size_t)x];
48
+ }
49
+ for (int y = 0; y < height; y++) {
50
+ source[(size_t)y * (size_t)width + (size_t)x] = sum * scale;
51
+ const int remove = std::min(height - 1, std::max(0, y - radius));
52
+ const int add = std::min(height - 1, std::max(0, y + radius + 1));
53
+ sum += temp[(size_t)add * (size_t)width + (size_t)x] -
54
+ temp[(size_t)remove * (size_t)width + (size_t)x];
55
+ }
56
+ }
57
+ }
58
+
59
+ } // namespace
60
+
61
+ bool Canvas2D::shadow_active() const {
62
+ if (in_shadow_layer_) return false;
63
+ return state_.shadow_color[3] > 0.0f;
64
+ }
65
+
66
+ const CanvasClipMask *Canvas2D::clip_mask() const {
67
+ /* The scratch shadow layer is painted unclipped; the mask is applied when the
68
+ * finished layer is composited back. */
69
+ if (in_shadow_layer_ || !state_.clip) return nullptr;
70
+ return state_.clip.get();
71
+ }
72
+
73
+ bool Canvas2D::begin_shadow_layer() {
74
+ if (in_shadow_layer_ || !shadow_active()) return false;
75
+ shadow_layer_.width = image_.width;
76
+ shadow_layer_.height = image_.height;
77
+ shadow_layer_.pixels.assign((size_t)image_.width * (size_t)image_.height * 4, 0);
78
+ std::swap(shadow_layer_.pixels, image_.pixels);
79
+ in_shadow_layer_ = true;
80
+ return true;
81
+ }
82
+
83
+ void Canvas2D::end_shadow_layer() {
84
+ if (!in_shadow_layer_) return;
85
+ in_shadow_layer_ = false;
86
+ std::swap(image_.pixels, shadow_layer_.pixels);
87
+ composite_shadow_layer(shadow_layer_);
88
+ composite_layer(shadow_layer_);
89
+ /* The shadow can reach beyond the draw's own bounds (offset + blur), so the
90
+ * conservative upload region is the whole canvas. */
91
+ mark_all_dirty();
92
+ generation_++;
93
+ }
94
+
95
+ void Canvas2D::composite_layer(const Image &layer) {
96
+ if (layer.width != image_.width || layer.height != image_.height) return;
97
+ const CanvasClipMask *clip = clip_mask();
98
+ for (int y = 0; y < layer.height; y++) {
99
+ const unsigned char *src = layer.pixels.data() + (size_t)y * (size_t)layer.width * 4;
100
+ unsigned char *dst = image_.pixels.data() + (size_t)y * (size_t)image_.width * 4;
101
+ for (int x = 0; x < layer.width; x++) {
102
+ const unsigned char *texel = src + (size_t)x * 4;
103
+ if (texel[3] == 0) continue;
104
+ float color[4] = {texel[0] / 255.0f, texel[1] / 255.0f, texel[2] / 255.0f,
105
+ texel[3] / 255.0f};
106
+ float coverage = clip != nullptr ? clip->at(x, y) / 255.0f : 1.0f;
107
+ if (coverage <= 0.0f) continue;
108
+ canvas_detail::blend_pixel_mode(dst + (size_t)x * 4, color, coverage, 1.0f,
109
+ state_.global_composite);
110
+ }
111
+ }
112
+ }
113
+
114
+ void Canvas2D::composite_shadow_layer(const Image &layer) {
115
+ if (layer.width != image_.width || layer.height != image_.height) return;
116
+ if (state_.shadow_color[3] <= 0.0f) return;
117
+ const int width = layer.width;
118
+ const int height = layer.height;
119
+ std::vector<float> alpha((size_t)width * (size_t)height, 0.0f);
120
+ for (int y = 0; y < height; y++) {
121
+ const unsigned char *row = layer.pixels.data() + (size_t)y * (size_t)width * 4;
122
+ float *out = alpha.data() + (size_t)y * (size_t)width;
123
+ for (int x = 0; x < width; x++) out[x] = row[(size_t)x * 4 + 3] / 255.0f;
124
+ }
125
+
126
+ /* `shadowBlur` is the diameter of the blur; three box passes approximate a
127
+ * Gaussian closely enough for UI work and stay O(width * height). */
128
+ const float blur = std::isfinite(state_.shadow_blur) ? state_.shadow_blur : 0.0f;
129
+ if (blur > 0.0f) {
130
+ int radius = (int)std::lround(blur * 0.5f);
131
+ if (radius < 1) radius = 1;
132
+ if (radius > 64) radius = 64;
133
+ std::vector<float> temp;
134
+ for (int pass = 0; pass < 3; pass++) box_blur_pass(alpha, temp, width, height, radius);
135
+ }
136
+
137
+ const int offsetX = (int)std::lround(state_.shadow_offset_x);
138
+ const int offsetY = (int)std::lround(state_.shadow_offset_y);
139
+ const CanvasClipMask *clip = clip_mask();
140
+ const float shadowAlpha = state_.shadow_color[3];
141
+ for (int y = 0; y < height; y++) {
142
+ const int sourceY = y - offsetY;
143
+ if (sourceY < 0 || sourceY >= height) continue;
144
+ const float *row = alpha.data() + (size_t)sourceY * (size_t)width;
145
+ unsigned char *dst = image_.pixels.data() + (size_t)y * (size_t)image_.width * 4;
146
+ for (int x = 0; x < width; x++) {
147
+ const int sourceX = x - offsetX;
148
+ if (sourceX < 0 || sourceX >= width) continue;
149
+ const float coverage = row[sourceX];
150
+ if (coverage <= 0.0f) continue;
151
+ float color[4] = {state_.shadow_color[0], state_.shadow_color[1], state_.shadow_color[2],
152
+ shadowAlpha};
153
+ float clipCoverage = clip != nullptr ? clip->at(x, y) / 255.0f : 1.0f;
154
+ if (clipCoverage <= 0.0f) continue;
155
+ canvas_detail::blend_pixel_mode(dst + (size_t)x * 4, color, coverage * clipCoverage, 1.0f,
156
+ kCanvasCompositeSourceOver);
157
+ }
158
+ }
159
+ }
160
+
161
+ } // namespace xtgui