xbintsc 0.5.38 → 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 (117) 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/driver/version.d.ts +1 -1
  5. package/dist/src/driver/version.js +1 -1
  6. package/dist/src/extensions/gui/html.js +9 -2
  7. package/dist/src/extensions/gui/html.js.map +1 -1
  8. package/dist/tests/codegen/closures.test.js +7 -0
  9. package/dist/tests/codegen/closures.test.js.map +1 -1
  10. package/dist/tests/e2e/compile-objects-classes.test.js +31 -0
  11. package/dist/tests/e2e/compile-objects-classes.test.js.map +1 -1
  12. package/dist/tests/e2e/gui-canvas-state.test.d.ts +9 -0
  13. package/dist/tests/e2e/gui-canvas-state.test.js +297 -0
  14. package/dist/tests/e2e/gui-canvas-state.test.js.map +1 -0
  15. package/dist/tests/e2e/gui-draw-perf.test.d.ts +22 -0
  16. package/dist/tests/e2e/gui-draw-perf.test.js +405 -0
  17. package/dist/tests/e2e/gui-draw-perf.test.js.map +1 -0
  18. package/dist/tests/e2e/gui-perf.test.d.ts +23 -0
  19. package/dist/tests/e2e/gui-perf.test.js +283 -0
  20. package/dist/tests/e2e/gui-perf.test.js.map +1 -0
  21. package/dist/tests/e2e/gui-script-import-order.test.js +9 -1
  22. package/dist/tests/e2e/gui-script-import-order.test.js.map +1 -1
  23. package/dist/tests/e2e/gui-webgl.test.js +4 -4
  24. package/dist/tests/e2e/gui-webgl.test.js.map +1 -1
  25. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.d.ts +8 -0
  26. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.js +336 -0
  27. package/dist/tests/e2e/gui-webgl2-shadow-sampler.test.js.map +1 -0
  28. package/dist/tests/extensions/gui.test.js +23 -0
  29. package/dist/tests/extensions/gui.test.js.map +1 -1
  30. package/doc/gui/api.md +28 -2
  31. package/doc/gui/canvas.md +110 -27
  32. package/doc/gui/known-issues.md +32 -10
  33. package/doc/gui/rendering.md +77 -0
  34. package/doc/gui/runtime.md +18 -3
  35. package/doc/gui/webgl.md +38 -19
  36. package/doc/gui.md +33 -1
  37. package/doc/zh-CN/gui/api.md +25 -2
  38. package/doc/zh-CN/gui/canvas.md +95 -26
  39. package/doc/zh-CN/gui/known-issues.md +21 -4
  40. package/doc/zh-CN/gui/rendering.md +59 -0
  41. package/doc/zh-CN/gui/runtime.md +13 -3
  42. package/doc/zh-CN/gui/webgl.md +27 -5
  43. package/doc/zh-CN/gui.md +25 -2
  44. package/package.json +7 -7
  45. package/runtime/ext_gui/browser.cpp +3 -1
  46. package/runtime/ext_gui/canvas.cpp +135 -89
  47. package/runtime/ext_gui/canvas.h +116 -0
  48. package/runtime/ext_gui/canvas_clip.cpp +143 -0
  49. package/runtime/ext_gui/canvas_composite.cpp +241 -0
  50. package/runtime/ext_gui/canvas_coverage.cpp +241 -0
  51. package/runtime/ext_gui/canvas_gradient.cpp +41 -4
  52. package/runtime/ext_gui/canvas_image_data.cpp +4 -0
  53. package/runtime/ext_gui/canvas_internal.h +34 -9
  54. package/runtime/ext_gui/canvas_raster.cpp +207 -178
  55. package/runtime/ext_gui/canvas_shadow.cpp +161 -0
  56. package/runtime/ext_gui/canvas_stroke.cpp +37 -9
  57. package/runtime/ext_gui/canvas_style.cpp +59 -0
  58. package/runtime/ext_gui/canvas_text.cpp +140 -0
  59. package/runtime/ext_gui/css.h +7 -0
  60. package/runtime/ext_gui/css_sheet.cpp +22 -0
  61. package/runtime/ext_gui/document.cpp +25 -1
  62. package/runtime/ext_gui/document.h +8 -0
  63. package/runtime/ext_gui/dom_api_canvas.cpp +16 -1
  64. package/runtime/ext_gui/dom_api_canvas_image.cpp +9 -0
  65. package/runtime/ext_gui/dom_api_canvas_state.cpp +217 -0
  66. package/runtime/ext_gui/dom_api_internal.h +36 -37
  67. package/runtime/ext_gui/dom_api_offscreen.cpp +213 -0
  68. package/runtime/ext_gui/dom_api_proto.cpp +18 -0
  69. package/runtime/ext_gui/dom_api_webgl.cpp +19 -9
  70. package/runtime/ext_gui/dom_api_webgl_internal.h +58 -0
  71. package/runtime/ext_gui/dom_api_webgl_source.cpp +18 -0
  72. package/runtime/ext_gui/gradient_values.cpp +12 -1
  73. package/runtime/ext_gui/gui.cpp +5 -0
  74. package/runtime/ext_gui/gui_canvas.cpp +4 -0
  75. package/runtime/ext_gui/gui_engine.h +30 -0
  76. package/runtime/ext_gui/gui_events.cpp +182 -58
  77. package/runtime/ext_gui/gui_media.cpp +3 -0
  78. package/runtime/ext_gui/image.cpp +69 -0
  79. package/runtime/ext_gui/image_bitmap_browser.cpp +11 -0
  80. package/runtime/ext_gui/layout.cpp +4 -0
  81. package/runtime/ext_gui/paint.cpp +19 -0
  82. package/runtime/ext_gui/paint_background.cpp +18 -1
  83. package/runtime/ext_gui/perf.cpp +171 -0
  84. package/runtime/ext_gui/perf.h +127 -0
  85. package/runtime/ext_gui/renderer.cpp +109 -41
  86. package/runtime/ext_gui/renderer.h +33 -9
  87. package/runtime/ext_gui/renderer_image.cpp +135 -65
  88. package/runtime/ext_gui/renderer_internal.h +15 -3
  89. package/runtime/ext_gui/renderer_text.cpp +24 -12
  90. package/runtime/ext_gui/style.cpp +101 -30
  91. package/runtime/ext_gui/style.h +6 -3
  92. package/runtime/ext_gui/style_print.cpp +7 -1
  93. package/runtime/ext_gui/text.cpp +23 -4
  94. package/runtime/ext_gui/text.h +29 -0
  95. package/runtime/ext_gui/text_cache.cpp +118 -0
  96. package/runtime/ext_gui/text_glyph_cache.cpp +100 -0
  97. package/runtime/ext_gui/webgl.h +48 -146
  98. package/runtime/ext_gui/webgl_blit.cpp +12 -1
  99. package/runtime/ext_gui/webgl_blit_depth.cpp +270 -0
  100. package/runtime/ext_gui/webgl_buffer.cpp +59 -1
  101. package/runtime/ext_gui/webgl_buffer_objects.cpp +106 -9
  102. package/runtime/ext_gui/webgl_clear.cpp +3 -0
  103. package/runtime/ext_gui/webgl_context.cpp +29 -3
  104. package/runtime/ext_gui/webgl_draw.cpp +75 -270
  105. package/runtime/ext_gui/webgl_draw_buffers.cpp +1 -0
  106. package/runtime/ext_gui/webgl_draw_setup.cpp +321 -0
  107. package/runtime/ext_gui/webgl_draw_state.h +130 -0
  108. package/runtime/ext_gui/webgl_glsl_scan.cpp +3 -8
  109. package/runtime/ext_gui/webgl_objects.h +148 -0
  110. package/runtime/ext_gui/webgl_pipeline.cpp +8 -8
  111. package/runtime/ext_gui/webgl_program_internal.h +35 -0
  112. package/runtime/ext_gui/webgl_shader.cpp +22 -30
  113. package/runtime/ext_gui/webgl_webgl2.cpp +36 -0
  114. package/runtime/ext_gui/window.cpp +56 -1
  115. package/src/codegen/generator/module.ts +22 -7
  116. package/src/driver/version.ts +1 -1
  117. package/src/extensions/gui/html.ts +9 -2
@@ -16,6 +16,7 @@
16
16
  #include <unordered_map>
17
17
 
18
18
  #include "canvas_internal.h"
19
+ #include "perf.h"
19
20
 
20
21
  namespace xtgui {
21
22
 
@@ -85,11 +86,16 @@ void Canvas2D::resize(int width, int height) {
85
86
  image_.width = width;
86
87
  image_.height = height;
87
88
  image_.pixels.assign((size_t)width * (size_t)height * 4, 0);
89
+ /* The DOM resets the rendering context when the bitmap is resized; the clip
90
+ * region in particular has device-space dimensions and cannot survive. */
91
+ state_.clip.reset();
92
+ shadow_layer_.pixels.clear();
88
93
  if (!alpha_) {
89
94
  /* `{ alpha: false }`: the canvas has an opaque black backdrop, so cleared
90
95
  * pixels are black rather than transparent. */
91
96
  for (size_t i = 3; i < image_.pixels.size(); i += 4) image_.pixels[i] = 255;
92
97
  }
98
+ mark_all_dirty();
93
99
  generation_++;
94
100
  }
95
101
 
@@ -292,6 +298,57 @@ void Canvas2D::to_device(float x, float y, float *outX, float *outY) const {
292
298
  state_.transform.apply(x, y, outX, outY);
293
299
  }
294
300
 
301
+ void Canvas2D::mark_dirty(float x0, float y0, float x1, float y1) {
302
+ if (x1 < x0) std::swap(x0, x1);
303
+ if (y1 < y0) std::swap(y0, y1);
304
+ const int ix0 = std::max(0, (int)std::floor(x0));
305
+ const int iy0 = std::max(0, (int)std::floor(y0));
306
+ const int ix1 = std::min(image_.width, (int)std::ceil(x1));
307
+ const int iy1 = std::min(image_.height, (int)std::ceil(y1));
308
+ if (ix1 <= ix0 || iy1 <= iy0) return;
309
+ if (dirty_.empty()) {
310
+ dirty_ = CanvasDirtyRect{ix0, iy0, ix1, iy1};
311
+ return;
312
+ }
313
+ dirty_.x0 = std::min(dirty_.x0, ix0);
314
+ dirty_.y0 = std::min(dirty_.y0, iy0);
315
+ dirty_.x1 = std::max(dirty_.x1, ix1);
316
+ dirty_.y1 = std::max(dirty_.y1, iy1);
317
+ }
318
+
319
+ void Canvas2D::mark_path_dirty(const std::vector<CanvasSubpath> &subpaths, float expand) {
320
+ bool have = false;
321
+ float minX = 0.0f, maxX = 0.0f, minY = 0.0f, maxY = 0.0f;
322
+ for (const CanvasSubpath &subpath : subpaths) {
323
+ for (const CanvasPoint &point : subpath) {
324
+ if (!have) {
325
+ minX = maxX = point.x;
326
+ minY = maxY = point.y;
327
+ have = true;
328
+ } else {
329
+ minX = std::min(minX, point.x);
330
+ maxX = std::max(maxX, point.x);
331
+ minY = std::min(minY, point.y);
332
+ maxY = std::max(maxY, point.y);
333
+ }
334
+ }
335
+ }
336
+ if (!have) return;
337
+ mark_dirty(minX - expand, minY - expand, maxX + expand, maxY + expand);
338
+ }
339
+
340
+ void Canvas2D::mark_all_dirty() {
341
+ dirty_ = CanvasDirtyRect{0, 0, image_.width, image_.height};
342
+ }
343
+
344
+ float Canvas2D::stroke_extent() const {
345
+ float scale = state_.transform.scale();
346
+ if (!(scale > 0.0f)) scale = 1.0f;
347
+ float half = state_.line_width * scale * 0.5f;
348
+ if (!(half > 0.0f)) half = 0.5f;
349
+ return half * std::max(1.0f, state_.miter_limit) + 1.0f;
350
+ }
351
+
295
352
  std::vector<CanvasSubpath> Canvas2D::device_subpaths() const {
296
353
  return device_subpaths_of(path_);
297
354
  }
@@ -320,6 +377,7 @@ CanvasPaint Canvas2D::make_paint(uint64_t gradient_id, uint64_t pattern_id,
320
377
  paint.gradient = gradient_id == 0 ? nullptr : find_gradient(gradient_id);
321
378
  paint.pattern = pattern_id == 0 ? nullptr : find_pattern(pattern_id);
322
379
  paint.inverse = state_.transform.inverted();
380
+ paint.composite = state_.global_composite;
323
381
  return paint;
324
382
  }
325
383
 
@@ -332,6 +390,12 @@ CanvasPaint Canvas2D::stroke_paint() const {
332
390
  }
333
391
 
334
392
  void Canvas2D::fill(bool even_odd) {
393
+ if (begin_shadow_layer()) {
394
+ fill(even_odd);
395
+ end_shadow_layer();
396
+ return;
397
+ }
398
+ g_perf_frame.canvasDraws += 1;
335
399
  std::vector<CanvasSubpath> subpaths = device_subpaths();
336
400
  std::vector<CanvasSubpath> loops;
337
401
  for (CanvasSubpath &subpath : subpaths) {
@@ -339,12 +403,19 @@ void Canvas2D::fill(bool even_odd) {
339
403
  loops.push_back(std::move(subpath));
340
404
  }
341
405
  if (loops.empty()) return;
406
+ mark_path_dirty(loops, 0.0f);
342
407
  canvas_detail::fill_loops(image_.pixels, image_.width, image_.height, loops, fill_paint(),
343
- state_.global_alpha, even_odd);
408
+ state_.global_alpha, even_odd, clip_mask());
344
409
  generation_++;
345
410
  }
346
411
 
347
412
  void Canvas2D::fill_paths(const std::vector<CanvasSubpath> &paths, bool even_odd) {
413
+ if (begin_shadow_layer()) {
414
+ fill_paths(paths, even_odd);
415
+ end_shadow_layer();
416
+ return;
417
+ }
418
+ g_perf_frame.canvasDraws += 1;
348
419
  std::vector<CanvasSubpath> subpaths = device_subpaths_of(paths);
349
420
  std::vector<CanvasSubpath> loops;
350
421
  for (CanvasSubpath &subpath : subpaths) {
@@ -352,13 +423,21 @@ void Canvas2D::fill_paths(const std::vector<CanvasSubpath> &paths, bool even_odd
352
423
  loops.push_back(std::move(subpath));
353
424
  }
354
425
  if (loops.empty()) return;
426
+ mark_path_dirty(loops, 0.0f);
355
427
  canvas_detail::fill_loops(image_.pixels, image_.width, image_.height, loops, fill_paint(),
356
- state_.global_alpha, even_odd);
428
+ state_.global_alpha, even_odd, clip_mask());
357
429
  generation_++;
358
430
  }
359
431
 
360
432
  void Canvas2D::stroke() {
433
+ if (begin_shadow_layer()) {
434
+ stroke();
435
+ end_shadow_layer();
436
+ return;
437
+ }
438
+ g_perf_frame.canvasDraws += 1;
361
439
  std::vector<CanvasSubpath> subpaths = device_subpaths();
440
+ mark_path_dirty(subpaths, stroke_extent());
362
441
  const CanvasPaint paint = stroke_paint();
363
442
  for (const CanvasSubpath &subpath : subpaths) {
364
443
  if (subpath.size() < 2) continue;
@@ -370,7 +449,14 @@ void Canvas2D::stroke() {
370
449
  }
371
450
 
372
451
  void Canvas2D::stroke_paths(const std::vector<CanvasSubpath> &paths) {
452
+ if (begin_shadow_layer()) {
453
+ stroke_paths(paths);
454
+ end_shadow_layer();
455
+ return;
456
+ }
457
+ g_perf_frame.canvasDraws += 1;
373
458
  std::vector<CanvasSubpath> subpaths = device_subpaths_of(paths);
459
+ mark_path_dirty(subpaths, stroke_extent());
374
460
  const CanvasPaint paint = stroke_paint();
375
461
  for (const CanvasSubpath &subpath : subpaths) {
376
462
  if (subpath.size() < 2) continue;
@@ -382,20 +468,30 @@ void Canvas2D::stroke_paths(const std::vector<CanvasSubpath> &paths) {
382
468
  }
383
469
 
384
470
  void Canvas2D::clear_rect(float x, float y, float width, float height) {
471
+ g_perf_frame.canvasDraws += 1;
385
472
  float x0, y0, x1, y1;
386
473
  to_device(x, y, &x0, &y0);
387
474
  to_device(x + width, y + height, &x1, &y1);
388
- canvas_detail::clear_rect(image_.pixels, image_.width, image_.height, x0, y0, x1, y1);
475
+ mark_dirty(x0, y0, x1, y1);
476
+ canvas_detail::clear_rect(image_.pixels, image_.width, image_.height, x0, y0, x1, y1,
477
+ clip_mask());
389
478
  generation_++;
390
479
  }
391
480
 
392
481
  void Canvas2D::fill_rect(float x, float y, float width, float height) {
482
+ if (begin_shadow_layer()) {
483
+ fill_rect(x, y, width, height);
484
+ end_shadow_layer();
485
+ return;
486
+ }
487
+ g_perf_frame.canvasDraws += 1;
393
488
  if (state_.transform.axis_aligned()) {
394
489
  float x0, y0, x1, y1;
395
490
  to_device(x, y, &x0, &y0);
396
491
  to_device(x + width, y + height, &x1, &y1);
492
+ mark_dirty(x0, y0, x1, y1);
397
493
  canvas_detail::fill_rect(image_.pixels, image_.width, image_.height, x0, y0, x1, y1,
398
- fill_paint(), state_.global_alpha);
494
+ fill_paint(), state_.global_alpha, clip_mask());
399
495
  } else {
400
496
  CanvasSubpath loop;
401
497
  const float corners[4][2] = {
@@ -405,13 +501,20 @@ void Canvas2D::fill_rect(float x, float y, float width, float height) {
405
501
  to_device(corner[0], corner[1], &point.x, &point.y);
406
502
  loop.push_back(point);
407
503
  }
504
+ mark_path_dirty(std::vector<CanvasSubpath>{loop}, 0.0f);
408
505
  canvas_detail::fill_loop(image_.pixels, image_.width, image_.height, loop, fill_paint(),
409
- state_.global_alpha);
506
+ state_.global_alpha, false, clip_mask());
410
507
  }
411
508
  generation_++;
412
509
  }
413
510
 
414
511
  void Canvas2D::stroke_rect(float x, float y, float width, float height) {
512
+ if (begin_shadow_layer()) {
513
+ stroke_rect(x, y, width, height);
514
+ end_shadow_layer();
515
+ return;
516
+ }
517
+ g_perf_frame.canvasDraws += 1;
415
518
  CanvasSubpath loop;
416
519
  const float corners[5][2] = {
417
520
  {x, y}, {x + width, y}, {x + width, y + height}, {x, y + height}, {x, y}};
@@ -421,103 +524,46 @@ void Canvas2D::stroke_rect(float x, float y, float width, float height) {
421
524
  loop.push_back(point);
422
525
  }
423
526
  stroke_subpath(loop, true, stroke_paint());
527
+ mark_path_dirty(std::vector<CanvasSubpath>{loop}, stroke_extent());
424
528
  generation_++;
425
529
  }
426
530
 
427
- float Canvas2D::measure_text(const std::string &text) const {
428
- return xt_text_measure_width(text, state_.font);
429
- }
430
-
431
- void Canvas2D::paint_text(const std::string &text, float x, float y, const CanvasPaint &paint,
432
- bool stroke, float max_width) {
433
- Font *font = xt_text_resolve(state_.font);
434
- if (font == nullptr) return;
435
- std::vector<ShapedGlyph> glyphs;
436
- float width = xt_text_shape_run(text, font, glyphs);
437
- float ascent = 0.0f, descent = 0.0f, lineHeight = 0.0f;
438
- xt_text_metrics(state_.font, &ascent, &descent, &lineHeight);
439
-
440
- /* The optional `maxWidth` condenses the run horizontally instead of clipping
441
- * it: advances and glyph bitmaps are squeezed by the same factor. */
442
- float horizontal = 1.0f;
443
- if (max_width > 0.0f && width > max_width && width > 0.0001f) {
444
- horizontal = max_width / width;
445
- }
446
-
447
- float originX = x;
448
- if (state_.text_align == 1) {
449
- originX -= width * horizontal * 0.5f;
450
- } else if (state_.text_align == 2) {
451
- originX -= width * horizontal;
452
- }
453
- float originY = y;
454
- switch (state_.text_baseline) {
455
- case 1:
456
- originY += ascent;
457
- break;
458
- case 2:
459
- originY += (ascent - descent) * 0.5f;
460
- break;
461
- case 3:
462
- originY -= descent;
463
- break;
464
- case 4:
465
- originY += ascent * 0.8f;
466
- break;
467
- case 5:
468
- originY -= descent;
469
- break;
470
- default:
471
- break;
472
- }
473
-
474
- float scale = state_.transform.scale();
475
- if (scale <= 0.0001f) scale = 1.0f;
476
- bool scaled = std::fabs(scale - 1.0f) > 0.01f;
477
-
478
- float penX = 0.0f;
479
- for (const ShapedGlyph &glyph : glyphs) {
480
- float userX = originX + (penX + glyph.x_offset) * horizontal;
481
- float userY = originY + glyph.y_offset;
482
- float deviceX = 0.0f, deviceY = 0.0f;
483
- to_device(userX, userY, &deviceX, &deviceY);
484
- Font *face = glyph.font != nullptr ? glyph.font : font;
485
- if (scaled) {
486
- Font *sized = xt_text_scale(face, state_.font.pixel_size * scale);
487
- if (sized != nullptr) face = sized;
488
- }
489
- GlyphImage image;
490
- if (xt_text_rasterize(face, glyph.glyph, image) && image.width > 0 && image.height > 0) {
491
- canvas_detail::blit_glyph(image_.pixels, image_.width, image_.height, image.pixels, image.width,
492
- image.height, image.left, image.top, deviceX, deviceY, horizontal,
493
- paint, state_.global_alpha);
494
- }
495
- penX += glyph.x_advance;
496
- }
497
- (void)stroke;
498
- generation_++;
499
- }
500
-
501
- void Canvas2D::fill_text(const std::string &text, float x, float y, float max_width) {
502
- paint_text(text, x, y, fill_paint(), false, max_width);
503
- }
504
-
505
- void Canvas2D::stroke_text(const std::string &text, float x, float y, float max_width) {
506
- paint_text(text, x, y, stroke_paint(), true, max_width);
507
- }
508
-
509
531
  void Canvas2D::draw_image(const Image &source, float dx, float dy, float dw, float dh, float sx,
510
532
  float sy, float sw, float sh) {
533
+ if (begin_shadow_layer()) {
534
+ draw_image(source, dx, dy, dw, dh, sx, sy, sw, sh);
535
+ end_shadow_layer();
536
+ return;
537
+ }
538
+ g_perf_frame.canvasDraws += 1;
511
539
  if (state_.transform.axis_aligned()) {
512
540
  float x0, y0, x1, y1;
513
541
  to_device(dx, dy, &x0, &y0);
514
542
  to_device(dx + dw, dy + dh, &x1, &y1);
543
+ mark_dirty(x0, y0, x1, y1);
515
544
  canvas_detail::draw_image(image_.pixels, image_.width, image_.height, source, x0, y0, x1 - x0,
516
- y1 - y0, sx, sy, sw, sh, state_.global_alpha);
545
+ y1 - y0, sx, sy, sw, sh, state_.global_alpha,
546
+ state_.image_smoothing, clip_mask());
517
547
  } else {
548
+ float minX = 0.0f, minY = 0.0f, maxX = 0.0f, maxY = 0.0f;
549
+ const float corners[4][2] = {{dx, dy}, {dx + dw, dy}, {dx + dw, dy + dh}, {dx, dy + dh}};
550
+ for (int i = 0; i < 4; i++) {
551
+ float px = 0.0f, py = 0.0f;
552
+ to_device(corners[i][0], corners[i][1], &px, &py);
553
+ if (i == 0) {
554
+ minX = maxX = px;
555
+ minY = maxY = py;
556
+ } else {
557
+ minX = std::min(minX, px);
558
+ maxX = std::max(maxX, px);
559
+ minY = std::min(minY, py);
560
+ maxY = std::max(maxY, py);
561
+ }
562
+ }
563
+ mark_dirty(minX, minY, maxX, maxY);
518
564
  canvas_detail::draw_image_affine(image_.pixels, image_.width, image_.height, source, dx, dy,
519
565
  dw, dh, sx, sy, sw, sh, state_.transform,
520
- state_.global_alpha);
566
+ state_.global_alpha, state_.image_smoothing, clip_mask());
521
567
  }
522
568
  generation_++;
523
569
  }
@@ -52,6 +52,18 @@ struct CanvasPoint {
52
52
  float y = 0.0f;
53
53
  };
54
54
 
55
+ /** One clip region: per-pixel coverage (0 = fully clipped, 255 = kept). A clip
56
+ * is baked into device space when `clip()` runs, exactly like a browser. */
57
+ struct CanvasClipMask {
58
+ std::vector<uint8_t> data;
59
+ int width = 0;
60
+ int height = 0;
61
+ uint8_t at(int x, int y) const {
62
+ if (x < 0 || y < 0 || x >= width || y >= height) return 0;
63
+ return data[(size_t)y * (size_t)width + (size_t)x];
64
+ }
65
+ };
66
+
55
67
  /** One subpath of the current path (device space). */
56
68
  using CanvasSubpath = std::vector<CanvasPoint>;
57
69
 
@@ -94,6 +106,36 @@ class CanvasGradient {
94
106
  class CanvasGradient;
95
107
  class CanvasPattern;
96
108
 
109
+ /** `globalCompositeOperation` values, in the order the DOM layer maps names. */
110
+ enum CanvasComposite {
111
+ kCanvasCompositeSourceOver = 0,
112
+ kCanvasCompositeSourceIn,
113
+ kCanvasCompositeSourceOut,
114
+ kCanvasCompositeSourceAtop,
115
+ kCanvasCompositeDestinationOver,
116
+ kCanvasCompositeDestinationIn,
117
+ kCanvasCompositeDestinationOut,
118
+ kCanvasCompositeDestinationAtop,
119
+ kCanvasCompositeLighter,
120
+ kCanvasCompositeCopy,
121
+ kCanvasCompositeXor,
122
+ kCanvasCompositeMultiply,
123
+ kCanvasCompositeScreen,
124
+ kCanvasCompositeOverlay,
125
+ kCanvasCompositeDarken,
126
+ kCanvasCompositeLighten,
127
+ kCanvasCompositeColorDodge,
128
+ kCanvasCompositeColorBurn,
129
+ kCanvasCompositeHardLight,
130
+ kCanvasCompositeSoftLight,
131
+ kCanvasCompositeDifference,
132
+ kCanvasCompositeExclusion,
133
+ kCanvasCompositeHue,
134
+ kCanvasCompositeSaturation,
135
+ kCanvasCompositeColor,
136
+ kCanvasCompositeLuminosity,
137
+ };
138
+
97
139
  /** What a fill or stroke paints with: a solid colour, a gradient sampled through
98
140
  * the inverse of the current transform, or a repeating image pattern. `color`
99
141
  * points into the canvas state and stays valid for one draw call. */
@@ -102,6 +144,8 @@ struct CanvasPaint {
102
144
  const CanvasGradient *gradient = nullptr;
103
145
  const CanvasPattern *pattern = nullptr;
104
146
  CanvasTransform inverse;
147
+ /** `globalCompositeOperation` as the `CanvasComposite` enum value. */
148
+ int composite = 0;
105
149
  };
106
150
 
107
151
  /**
@@ -136,6 +180,15 @@ class Canvas2D {
136
180
  uint64_t id() const { return id_; }
137
181
  /** Bumped by every draw; the renderer re-uploads when it changes. */
138
182
  uint64_t generation() const { return generation_; }
183
+ /** Device-space bounds of the pixels changed since the renderer last uploaded
184
+ * them. Empty means nothing visible changed (a no-op or off-canvas draw), so
185
+ * the renderer can skip the upload entirely. */
186
+ struct CanvasDirtyRect {
187
+ int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
188
+ bool empty() const { return x1 <= x0 || y1 <= y0; }
189
+ };
190
+ const CanvasDirtyRect &pending_dirty() const { return dirty_; }
191
+ void clear_dirty() { dirty_ = CanvasDirtyRect(); }
139
192
  const Image &image() const { return image_; }
140
193
  /** Resize the bitmap, discarding its contents (the DOM `canvas.width = …`
141
194
  * behaviour). */
@@ -187,6 +240,24 @@ class Canvas2D {
187
240
  float line_dash_offset() const { return state_.line_dash_offset; }
188
241
  void set_global_alpha(float alpha);
189
242
  float global_alpha() const { return state_.global_alpha; }
243
+ void set_global_composite_operation(const std::string &value);
244
+ const std::string &global_composite_operation() const {
245
+ return state_.global_composite_string;
246
+ }
247
+ void set_shadow_color(const std::string &css);
248
+ const std::string &shadow_color() const { return state_.shadow_color_string; }
249
+ void set_shadow_blur(float blur);
250
+ float shadow_blur() const { return state_.shadow_blur; }
251
+ void set_shadow_offset_x(float offset) { state_.shadow_offset_x = offset; }
252
+ float shadow_offset_x() const { return state_.shadow_offset_x; }
253
+ void set_shadow_offset_y(float offset) { state_.shadow_offset_y = offset; }
254
+ float shadow_offset_y() const { return state_.shadow_offset_y; }
255
+ void set_image_smoothing_enabled(bool enabled) { state_.image_smoothing = enabled; }
256
+ bool image_smoothing_enabled() const { return state_.image_smoothing; }
257
+ void set_image_smoothing_quality(const std::string &value);
258
+ const std::string &image_smoothing_quality() const {
259
+ return state_.image_smoothing_quality;
260
+ }
190
261
  void set_font(const std::string &font);
191
262
  const std::string &font() const { return state_.font_string; }
192
263
  void set_text_align(const std::string &value);
@@ -226,6 +297,15 @@ class Canvas2D {
226
297
  * space and are transformed with the current state, like the current path. */
227
298
  void fill_paths(const std::vector<CanvasSubpath> &paths, bool even_odd);
228
299
  void stroke_paths(const std::vector<CanvasSubpath> &paths);
300
+ /** Intersect the clip region with the current path (or a `Path2D`). */
301
+ void clip(bool even_odd = false);
302
+ void clip_paths(const std::vector<CanvasSubpath> &paths, bool even_odd);
303
+ /** Hit testing in the current user space. */
304
+ bool is_point_in_path(float x, float y, bool even_odd = false) const;
305
+ bool is_point_in_paths(const std::vector<CanvasSubpath> &paths, float x, float y,
306
+ bool even_odd) const;
307
+ bool is_point_in_stroke(float x, float y) const;
308
+ bool is_point_in_stroke_paths(const std::vector<CanvasSubpath> &paths, float x, float y) const;
229
309
 
230
310
  /* -- rectangles -------------------------------------------------------- */
231
311
  void clear_rect(float x, float y, float width, float height);
@@ -286,10 +366,31 @@ class Canvas2D {
286
366
  uint64_t stroke_pattern = 0;
287
367
  std::string line_cap_string = "butt";
288
368
  std::string line_join_string = "miter";
369
+ int global_composite = kCanvasCompositeSourceOver;
370
+ std::string global_composite_string = "source-over";
371
+ float shadow_color[4] = {0.0f, 0.0f, 0.0f, 0.0f};
372
+ std::string shadow_color_string = "rgba(0, 0, 0, 0)";
373
+ float shadow_blur = 0.0f;
374
+ float shadow_offset_x = 0.0f;
375
+ float shadow_offset_y = 0.0f;
376
+ bool image_smoothing = true;
377
+ std::string image_smoothing_quality = "low";
378
+ /** Current clip region; `save`/`restore` share it (a later `clip` installs a
379
+ * new mask instead of mutating the saved one). */
380
+ std::shared_ptr<CanvasClipMask> clip;
289
381
  };
290
382
 
291
383
  /** Transform a user-space point into device space. */
292
384
  void to_device(float x, float y, float *outX, float *outY) const;
385
+ /** Union a device-space rectangle into the pending upload region. */
386
+ void mark_dirty(float x0, float y0, float x1, float y1);
387
+ /** Union the bounds of a device-space path (grown by `expand`) into the
388
+ * pending upload region. */
389
+ void mark_path_dirty(const std::vector<CanvasSubpath> &subpaths, float expand);
390
+ void mark_all_dirty();
391
+ /** How far a stroke can reach outside its centre line (half width, miter
392
+ * limit and a pixel of anti-aliasing slack). */
393
+ float stroke_extent() const;
293
394
  /** The current path transformed into device space. */
294
395
  std::vector<CanvasSubpath> device_subpaths() const;
295
396
  /** Any user-space path transformed into device space. */
@@ -314,11 +415,26 @@ class Canvas2D {
314
415
  std::vector<CanvasSubpath> *out) const;
315
416
  void paint_text(const std::string &text, float x, float y, const CanvasPaint &paint,
316
417
  bool stroke, float max_width);
418
+ /** True when the current state paints a shadow for a drawing operation. */
419
+ bool shadow_active() const;
420
+ /** Redirect the next draw into a scratch layer so its alpha can be turned
421
+ * into a shadow. Returns false while already inside the shadow layer (or when
422
+ * no shadow is active), in which case the caller draws directly. */
423
+ bool begin_shadow_layer();
424
+ void end_shadow_layer();
425
+ /** The current clip coverage, or null when the canvas is unclipped. */
426
+ const CanvasClipMask *clip_mask() const;
427
+ void composite_layer(const Image &layer);
428
+ void composite_shadow_layer(const Image &layer);
317
429
 
318
430
  Image image_;
431
+ /** Scratch bitmap a draw is rendered into while its shadow is built. */
432
+ Image shadow_layer_;
433
+ bool in_shadow_layer_ = false;
319
434
  bool alpha_ = true;
320
435
  uint64_t id_ = 0;
321
436
  uint64_t generation_ = 0;
437
+ CanvasDirtyRect dirty_;
322
438
  State state_;
323
439
  std::vector<State> stack_;
324
440
  std::vector<CanvasSubpath> path_;
@@ -0,0 +1,143 @@
1
+ /*
2
+ * xbintsc GUI engine — Canvas2D clipping and hit testing.
3
+ *
4
+ * `clip()` rasterises the current path into a device-space coverage mask and
5
+ * intersects it with the region already in effect. `save`/`restore` share the
6
+ * mask through a `shared_ptr`; a later `clip()` installs a new mask, so a saved
7
+ * state can never observe a mutated region.
8
+ *
9
+ * `isPointInPath`/`isPointInStroke` run against the same device-space geometry
10
+ * the rasteriser would use, which keeps them consistent with anti-aliasing and
11
+ * the current transform.
12
+ */
13
+
14
+ #include "canvas_internal.h"
15
+
16
+ #include <algorithm>
17
+ #include <cmath>
18
+ #include <memory>
19
+ #include <vector>
20
+
21
+ namespace xtgui {
22
+
23
+ namespace {
24
+
25
+ bool point_in_loops(const std::vector<CanvasSubpath> &loops, float x, float y, bool even_odd) {
26
+ int winding = 0;
27
+ int crossings = 0;
28
+ for (const CanvasSubpath &loop : loops) {
29
+ if (loop.size() < 2) continue;
30
+ for (size_t i = 0; i < loop.size(); i++) {
31
+ const CanvasPoint &p0 = loop[i];
32
+ const CanvasPoint &p1 = loop[(i + 1) % loop.size()];
33
+ if ((p0.y > y) == (p1.y > y)) continue;
34
+ const float intersection = p0.x + (y - p0.y) * (p1.x - p0.x) / (p1.y - p0.y);
35
+ if (intersection <= x) continue;
36
+ if (p1.y > p0.y) winding++;
37
+ else winding--;
38
+ crossings++;
39
+ }
40
+ }
41
+ return even_odd ? (crossings % 2) != 0 : winding != 0;
42
+ }
43
+
44
+ float distance_to_segment(const CanvasPoint &p, const CanvasPoint &a, const CanvasPoint &b) {
45
+ const float dx = b.x - a.x;
46
+ const float dy = b.y - a.y;
47
+ const float lengthSquared = dx * dx + dy * dy;
48
+ if (lengthSquared <= 1e-9f) return std::hypot(p.x - a.x, p.y - a.y);
49
+ float t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lengthSquared;
50
+ t = std::max(0.0f, std::min(1.0f, t));
51
+ const float cx = a.x + t * dx;
52
+ const float cy = a.y + t * dy;
53
+ return std::hypot(p.x - cx, p.y - cy);
54
+ }
55
+
56
+ bool point_in_stroke_loops(const std::vector<CanvasSubpath> &loops, float x, float y, float half) {
57
+ const CanvasPoint point{x, y};
58
+ for (const CanvasSubpath &loop : loops) {
59
+ if (loop.size() < 2) continue;
60
+ for (size_t i = 0; i + 1 < loop.size(); i++) {
61
+ if (distance_to_segment(point, loop[i], loop[i + 1]) <= half) return true;
62
+ }
63
+ if (loop.front().x != loop.back().x || loop.front().y != loop.back().y) {
64
+ if (distance_to_segment(point, loop.back(), loop.front()) <= half) return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+
70
+ } // namespace
71
+
72
+ void Canvas2D::clip(bool even_odd) { clip_paths(path_, even_odd); }
73
+
74
+ void Canvas2D::clip_paths(const std::vector<CanvasSubpath> &paths, bool even_odd) {
75
+ std::vector<CanvasSubpath> subpaths = device_subpaths_of(paths);
76
+ std::vector<CanvasSubpath> loops;
77
+ for (CanvasSubpath &subpath : subpaths) {
78
+ if (subpath.size() < 3) continue;
79
+ loops.push_back(std::move(subpath));
80
+ }
81
+ auto mask = std::make_shared<CanvasClipMask>();
82
+ mask->width = image_.width;
83
+ mask->height = image_.height;
84
+ mask->data.assign((size_t)image_.width * (size_t)image_.height, 0);
85
+ canvas_detail::CoverageGrid coverage;
86
+ canvas_detail::compute_coverage(image_.width, image_.height, loops, even_odd, &coverage);
87
+ if (!coverage.empty()) {
88
+ const int rows = coverage.y1 - coverage.y0;
89
+ for (int r = 0; r < rows; r++) {
90
+ const uint32_t begin = coverage.row_offset[(size_t)r];
91
+ const uint32_t end = coverage.row_offset[(size_t)r + 1];
92
+ if (begin >= end) continue;
93
+ const int y = coverage.y0 + r;
94
+ const int rx = coverage.row_x[(size_t)r];
95
+ const float *values = coverage.values.data() + begin;
96
+ uint8_t *out = mask->data.data() + (size_t)y * (size_t)image_.width + (size_t)rx;
97
+ for (uint32_t i = begin; i < end; i++) {
98
+ float value = std::max(0.0f, std::min(1.0f, values[i - begin]));
99
+ out[i - begin] = (uint8_t)(value * 255.0f + 0.5f);
100
+ }
101
+ }
102
+ }
103
+ if (state_.clip) {
104
+ const CanvasClipMask &previous = *state_.clip;
105
+ if (previous.width == mask->width && previous.height == mask->height) {
106
+ for (size_t i = 0; i < mask->data.size(); i++) {
107
+ mask->data[i] = (uint8_t)((unsigned)mask->data[i] * (unsigned)previous.data[i] / 255u);
108
+ }
109
+ }
110
+ }
111
+ state_.clip = std::move(mask);
112
+ /* A clip changes future draws only; it touches no pixels, so it must not
113
+ * schedule a texture upload. */
114
+ }
115
+
116
+ bool Canvas2D::is_point_in_path(float x, float y, bool even_odd) const {
117
+ return is_point_in_paths(path_, x, y, even_odd);
118
+ }
119
+
120
+ bool Canvas2D::is_point_in_paths(const std::vector<CanvasSubpath> &paths, float x, float y,
121
+ bool even_odd) const {
122
+ float deviceX = 0.0f;
123
+ float deviceY = 0.0f;
124
+ to_device(x, y, &deviceX, &deviceY);
125
+ return point_in_loops(device_subpaths_of(paths), deviceX, deviceY, even_odd);
126
+ }
127
+
128
+ bool Canvas2D::is_point_in_stroke(float x, float y) const {
129
+ return is_point_in_stroke_paths(path_, x, y);
130
+ }
131
+
132
+ bool Canvas2D::is_point_in_stroke_paths(const std::vector<CanvasSubpath> &paths, float x,
133
+ float y) const {
134
+ float scale = state_.transform.scale();
135
+ if (!(scale > 0.0f)) scale = 1.0f;
136
+ const float half = std::max(0.5f, state_.line_width * scale * 0.5f);
137
+ float deviceX = 0.0f;
138
+ float deviceY = 0.0f;
139
+ to_device(x, y, &deviceX, &deviceY);
140
+ return point_in_stroke_loops(device_subpaths_of(paths), deviceX, deviceY, half);
141
+ }
142
+
143
+ } // namespace xtgui