diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
index b0e9f84eb42..056b3767a4c 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
+++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
@@ -273,6 +273,7 @@
+
@@ -485,6 +486,7 @@
AutoFocusBehaviorPage.xaml
+ ColorPickerButtonPage.xaml
@@ -494,6 +496,9 @@
EnumValuesExtensionPage.xaml
+
+ CanvasPathGeometryPage.xaml
+ FocusBehaviorPage.xaml
@@ -975,6 +980,10 @@
MSBuild:CompileDesigner
+
+ Designer
+ MSBuild:Compile
+ MSBuild:CompileDesigner
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometry.png b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometry.png
new file mode 100644
index 00000000000..5c909a581e1
Binary files /dev/null and b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometry.png differ
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometryPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometryPage.xaml
new file mode 100644
index 00000000000..44a17d85327
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometryPage.xaml
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometryPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometryPage.xaml.cs
new file mode 100644
index 00000000000..be47e48ce48
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/CanvasPathGeometryPage.xaml.cs
@@ -0,0 +1,330 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Graphics.Canvas.UI.Xaml;
+using Microsoft.Toolkit.Uwp.UI.Extensions;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry;
+using Windows.System;
+using Windows.UI;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Controls.Primitives;
+using Windows.UI.Xaml.Media;
+
+namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
+{
+ ///
+ /// An empty page that can be used on its own or navigated to within a Frame.
+ ///
+ public sealed partial class CanvasPathGeometryPage : Page
+ {
+ private const string Sample1 =
+ "F0 M 656.500,400.500 C 656.500,350.637 598.572,307.493 514.292,286.708 C 493.507,202.428 450.363,144.500 400.500,144.500 C 350.637,144.500 307.493,202.428 286.708,286.708 C 202.428,307.493 144.500,350.637 144.500,400.500 C 144.500,450.363 202.428,493.507 286.708,514.292 C 307.493,598.572 350.637,656.500 400.500,656.500 C 450.363,656.500 493.507,598.572 514.292,514.292 C 598.572,493.507 656.500,450.363 656.500,400.500 ZM 581.519,219.481 C 546.261,184.222 474.793,194.676 400.500,239.574 C 326.207,194.676 254.739,184.222 219.481,219.481 C 184.222,254.739 194.676,326.207 239.574,400.500 C 194.676,474.792 184.222,546.261 219.481,581.519 C 254.739,616.778 326.207,606.324 400.500,561.426 C 474.793,606.324 546.261,616.778 581.519,581.519 C 616.778,546.261 606.324,474.792 561.426,400.500 C 606.324,326.207 616.778,254.739 581.519,219.481 ZU 112.5 112.5 570 570 36 36";
+
+ private const string Sample2 =
+ "F1 M 331.341,81.975 L 398.766,218.593 L 549.533,240.500 L 440.437,346.842 L 466.191,497.000 L 331.341,426.105 L 196.491,497.000 L 222.245,346.842 L 113.150,240.500 L 263.916,218.593 L 331.341,81.975 Z";
+
+ private const string Sample3 =
+ "F1 M 545.497,397.058 C 454.492,512.882 286.824,533.003 171.000,441.998 C 78.340,369.194 62.244,235.059 135.048,142.400 C 193.291,68.272 300.599,55.395 374.726,113.639 C 434.028,160.233 444.330,246.079 397.736,305.381 C 360.460,352.823 291.783,361.064 244.341,323.788 C 206.388,293.968 199.795,239.026 229.616,201.073 C 253.472,170.711 297.425,165.436 327.788,189.293 C 352.078,208.378 356.297,243.540 337.212,267.830 C 321.944,287.262 293.814,290.638 274.382,275.370 C 258.836,263.155 256.136,240.651 268.350,225.106 C 278.122,212.669 296.125,210.509 308.562,220.280 C 318.511,228.098 320.239,242.500 312.422,252.449";
+
+ private const string Sample4 =
+ "F1 M 311.717,332.110 C 285.669,332.110 264.552,310.994 264.552,284.945 C 264.552,258.897 285.669,237.781 311.717,237.781 C 337.765,237.781 358.881,258.897 358.881,284.945 C 358.881,310.994 337.765,332.110 311.717,332.110 Z M 505.712,232.846 C 634.939,203.833 411.705,171.395 371.772,213.383 C 411.705,171.395 311.872,-30.889 311.872,92.013 C 311.872,-30.889 212.038,171.395 251.972,213.383 C 212.038,171.395 -11.196,203.833 118.031,232.846 C -11.196,203.833 150.338,361.289 214.951,327.320 C 150.338,361.289 112.205,583.622 192.072,460.719 C 112.205,583.622 311.872,478.651 311.872,397.737 C 311.872,478.651 511.538,583.622 431.672,460.719 C 511.538,583.622 473.405,361.289 408.792,327.320 C 473.405,361.289 634.939,203.833 505.712,232.846 Z";
+
+ private const string Sample5 =
+ "F1 M 391.853,348.284 C 391.853,357.113 384.696,364.271 375.867,364.271 L 301.927,364.271 C 293.098,364.271 285.940,357.113 285.940,348.284 L 285.940,274.345 C 285.940,265.515 293.098,258.358 301.927,258.358 L 375.867,258.358 C 384.696,258.358 391.853,265.515 391.853,274.345 L 391.853,348.284 Z M 544.748,282.990 L 485.488,267.081 C 472.521,263.600 466.301,248.839 472.866,237.128 L 502.867,183.604 C 512.642,166.166 494.336,146.433 476.214,154.872 L 420.592,180.776 C 408.421,186.445 394.169,179.136 391.670,165.944 L 380.248,105.658 C 376.526,86.017 349.819,82.667 341.362,100.780 L 315.403,156.378 C 309.723,168.543 294.107,172.105 283.714,163.607 L 236.213,124.767 C 220.737,112.113 198.125,126.714 203.289,146.025 L 219.141,205.301 C 222.610,218.271 212.937,231.038 199.512,231.208 L 138.159,231.988 C 118.170,232.242 110.233,257.962 126.602,269.436 L 176.847,304.655 C 187.841,312.361 188.638,328.358 178.464,337.118 L 131.965,377.153 C 116.816,390.196 127.269,415.001 147.184,413.268 L 208.312,407.950 C 221.687,406.786 232.580,418.529 230.417,431.779 L 220.531,492.336 C 217.310,512.066 241.261,524.348 255.403,510.220 L 298.811,466.854 C 308.310,457.365 324.202,459.358 331.062,470.899 L 362.415,523.643 C 372.629,540.827 398.872,534.840 400.624,514.927 L 406.001,453.804 C 407.178,440.430 420.634,431.742 433.307,436.173 L 491.227,456.425 C 510.098,463.022 526.353,441.568 514.895,425.187 L 479.725,374.908 C 472.030,363.906 476.753,348.601 489.310,343.850 L 546.697,322.133 C 565.393,315.057 564.054,288.173 544.748,282.990 Z";
+
+ private const string ErrorString =
+ "F1 M 19.648,24.605 L 19.648,30.220 L 29.404,30.220 L 29.404,28.149 C 29.404,27.229 29.581,26.573 29.936,26.181 C 30.290,25.790 30.753,25.594 31.325,25.594 C 31.885,25.594 " +
+ "32.342,25.790 32.696,26.181 C 33.051,26.573 33.228,27.229 33.228,28.149 L 33.228,34.044 L 15.227,34.044 C 14.307,34.044 13.651,33.867 13.259,33.512 C 12.867,33.158 12.672,32.695 " +
+ "12.672,32.122 C 12.672,31.563 12.870,31.106 13.268,30.751 C 13.666,30.397 14.319,30.220 15.227,30.220 L 15.824,30.220 L 15.824,15.260 L 15.227,15.260 C 14.307,15.260 13.651,15.082 " +
+ "13.259,14.728 C 12.867,14.373 12.672,13.910 12.672,13.338 C 12.672,12.766 12.867,12.303 13.259,11.948 C 13.651,11.594 14.307,11.417 15.227,11.417 L 32.388,11.436 L 32.388,17.255 C " +
+ "32.388,18.163 32.214,18.813 31.866,19.205 C 31.518,19.596 31.058,19.792 30.486,19.792 C 29.914,19.792 29.451,19.600 29.096,19.214 C 28.742,18.829 28.564,18.176 28.564,17.255 L " +
+ "28.564,15.260 L 19.648,15.260 L 19.648,20.781 L 23.006,20.781 C 23.006,19.786 23.099,19.146 23.285,18.860 C 23.671,18.250 24.218,17.946 24.927,17.946 C 25.487,17.946 25.944,18.142 " +
+ "26.298,18.533 C 26.652,18.925 26.830,19.581 26.830,20.501 L 26.830,24.903 C 26.830,25.737 26.730,26.297 26.531,26.582 C 26.133,27.167 25.599,27.459 24.927,27.459 C 24.218,27.459 " +
+ "23.671,27.155 23.285,26.545 C 23.099,26.259 23.006,25.612 23.006,24.605 L 19.648,24.605 ZM 45.707,17.106 L 45.707,19.494 C 47.311,18.337 48.577,17.567 49.503,17.181 C 50.430,16.795 " +
+ "51.297,16.603 52.105,16.603 C 53.349,16.603 54.555,17.063 55.724,17.983 C 56.520,18.605 56.918,19.239 56.918,19.886 C 56.918,20.433 56.728,20.896 56.349,21.275 C 55.970,21.655 " +
+ "55.513,21.844 54.978,21.844 C 54.505,21.844 54.008,21.608 53.486,21.135 C 52.963,20.663 52.497,20.427 52.087,20.427 C 51.552,20.427 50.753,20.762 49.690,21.434 C 48.626,22.105 " +
+ "47.299,23.113 45.707,24.456 L 45.707,30.220 L 51.154,30.220 C 52.074,30.220 52.730,30.397 53.122,30.751 C 53.514,31.106 53.710,31.569 53.710,32.141 C 53.710,32.701 53.514,33.158 " +
+ "53.122,33.512 C 52.730,33.867 52.074,34.044 51.154,34.044 L 39.607,34.044 C 38.687,34.044 38.031,33.867 37.639,33.512 C 37.248,33.158 37.052,32.695 37.052,32.122 C 37.052,31.563 " +
+ "37.248,31.106 37.639,30.751 C 38.031,30.397 38.687,30.220 39.607,30.220 L 41.883,30.220 L 41.883,20.930 L 40.503,20.930 C 39.582,20.930 38.927,20.753 38.535,20.399 C 38.143,20.044 " +
+ "37.947,19.581 37.947,19.009 C 37.947,18.449 38.143,17.992 38.535,17.638 C 38.927,17.283 39.582,17.106 40.503,17.106 L 45.707,17.106 ZM 68.633,17.106 L 68.633,19.494 C 70.237,18.337 " +
+ "71.502,17.567 72.429,17.181 C 73.355,16.795 74.222,16.603 75.031,16.603 C 76.274,16.603 77.480,17.063 78.650,17.983 C 79.445,18.605 79.843,19.239 79.843,19.886 C 79.843,20.433 " +
+ "79.654,20.896 79.274,21.275 C 78.895,21.655 78.438,21.844 77.903,21.844 C 77.431,21.844 76.933,21.608 76.411,21.135 C 75.889,20.663 75.423,20.427 75.012,20.427 C 74.477,20.427 " +
+ "73.678,20.762 72.615,21.434 C 71.552,22.105 70.224,23.113 68.633,24.456 L 68.633,30.220 L 74.079,30.220 C 74.999,30.220 75.656,30.397 76.047,30.751 C 76.439,31.106 76.635,31.569 " +
+ "76.635,32.141 C 76.635,32.701 76.439,33.158 76.047,33.512 C 75.656,33.867 74.999,34.044 74.079,34.044 L 62.533,34.044 C 61.612,34.044 60.957,33.867 60.565,33.512 C 60.173,33.158 " +
+ "59.977,32.695 59.977,32.122 C 59.977,31.563 60.173,31.106 60.565,30.751 C 60.957,30.397 61.612,30.220 62.533,30.220 L 64.809,30.220 L 64.809,20.930 L 63.428,20.930 C 62.508,20.930 " +
+ "61.852,20.753 61.460,20.399 C 61.069,20.044 60.873,19.581 60.873,19.009 C 60.873,18.449 61.069,17.992 61.460,17.638 C 61.852,17.283 62.508,17.106 63.428,17.106 L 68.633,17.106 ZM " +
+ "98.460,25.911 C 98.460,24.680 98.018,23.548 97.135,22.516 C 95.929,21.123 94.343,20.427 92.379,20.427 C 90.650,20.427 89.208,20.980 88.051,22.087 C 86.895,23.194 86.316,24.474 " +
+ "86.316,25.929 C 86.316,27.123 86.901,28.239 88.070,29.278 C 89.239,30.316 90.675,30.835 92.379,30.835 C 94.095,30.835 95.537,30.316 96.706,29.278 C 97.875,28.239 98.460,27.117 " +
+ "98.460,25.911 Z M 102.284,25.892 C 102.284,27.360 101.876,28.780 101.062,30.154 C 100.247,31.529 99.035,32.623 97.425,33.438 C 95.814,34.252 94.132,34.659 92.379,34.659 C " +
+ "90.638,34.659 88.971,34.258 87.380,33.456 C 85.788,32.654 84.575,31.563 83.742,30.182 C 82.909,28.802 82.492,27.360 82.492,25.855 C 82.492,24.325 82.915,22.824 83.761,21.350 C " +
+ "84.606,19.876 85.822,18.717 87.408,17.871 C 88.993,17.026 90.650,16.603 92.379,16.603 C 94.119,16.603 95.795,17.035 97.406,17.899 C 99.016,18.763 100.232,19.926 101.053,21.387 C " +
+ "101.873,22.849 102.284,24.350 102.284,25.892 ZM 114.483,17.106 L 114.483,19.494 C 116.088,18.337 117.353,17.567 118.279,17.181 C 119.206,16.795 120.073,16.603 120.882,16.603 C " +
+ "122.125,16.603 123.331,17.063 124.500,17.983 C 125.296,18.605 125.694,19.239 125.694,19.886 C 125.694,20.433 125.504,20.896 125.125,21.275 C 124.746,21.655 124.289,21.844 " +
+ "123.754,21.844 C 123.282,21.844 122.784,21.608 122.262,21.135 C 121.740,20.663 121.273,20.427 120.863,20.427 C 120.328,20.427 119.529,20.762 118.466,21.434 C 117.403,22.105 " +
+ "116.075,23.113 114.483,24.456 L 114.483,30.220 L 119.930,30.220 C 120.850,30.220 121.506,30.397 121.898,30.751 C 122.290,31.106 122.486,31.569 122.486,32.141 C 122.486,32.701 " +
+ "122.290,33.158 121.898,33.512 C 121.506,33.867 120.850,34.044 119.930,34.044 L 108.384,34.044 C 107.463,34.044 106.807,33.867 106.416,33.512 C 106.024,33.158 105.828,32.695 " +
+ "105.828,32.122 C 105.828,31.563 106.024,31.106 106.416,30.751 C 106.807,30.397 107.463,30.220 108.384,30.220 L 110.659,30.220 L 110.659,20.930 L 109.279,20.930 C 108.359,20.930 " +
+ "107.703,20.753 107.311,20.399 C 106.919,20.044 106.723,19.581 106.723,19.009 C 106.723,18.449 106.919,17.992 107.311,17.638 C 107.703,17.283 108.359,17.106 109.279,17.106 L " +
+ "114.483,17.106 ZM 140.431,32.645 C 140.431,33.192 140.225,33.655 139.815,34.034 C 139.405,34.414 138.838,34.603 138.118,34.603 C 137.396,34.603 136.830,34.414 136.420,34.034 C " +
+ "136.010,33.655 135.804,33.192 135.804,32.645 C 135.804,32.110 136.006,31.653 136.411,31.274 C 136.815,30.895 137.384,30.705 138.118,30.705 C 138.851,30.705 139.420,30.891 " +
+ "139.824,31.264 C 140.228,31.637 140.431,32.098 140.431,32.645 Z M 141.046,13.655 L 139.983,25.183 C 139.933,25.780 139.734,26.244 139.386,26.573 C 139.038,26.903 138.603,27.067 " +
+ "138.080,27.067 C 137.558,27.067 137.123,26.903 136.774,26.573 C 136.426,26.244 136.227,25.780 136.178,25.183 L 135.096,13.655 C 135.046,13.071 135.021,12.685 135.021,12.499 C " +
+ "135.021,11.529 135.313,10.749 135.898,10.158 C 136.482,9.567 137.210,9.272 138.080,9.272 C 138.938,9.272 139.662,9.570 140.253,10.167 C 140.844,10.764 141.139,11.516 141.139,12.424 " +
+ "C 141.139,12.611 141.108,13.021 141.046,13.655 Z";
+
+ private const string ParseError1 = "Parameter \"(pathData matches.Count == 0)\" must be false, was true: ";
+ private const string ParseError2 = "Parameter \"(pathData matches.Count > 1)\" must be false, was true: ";
+ private const string ParseError3 = "PATH_ERR003";
+
+ private DispatcherQueueTimer _typeTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
+
+ private List _colors;
+ private List _samples;
+
+ private string _data = string.Empty;
+ private StringBuilder _logger;
+
+ private float _strokeThickness;
+ private Color _strokeColor;
+ private Color _fillColor;
+ private bool _selectionChanged = false;
+ private bool _isParsing = false;
+
+ private CanvasGeometry _errorGeometry;
+ private GeometryStreamReader _reader;
+
+ private SolidColorBrush _commandBrush;
+ private SolidColorBrush _commandErrorBrush;
+
+ public string InputText { get; set; }
+
+ public CanvasPathGeometryPage()
+ {
+ this.InitializeComponent();
+ _reader = new GeometryStreamReader();
+ _logger = new StringBuilder();
+ _colors = new List()
+ {
+ Colors.Transparent,
+ Colors.Black,
+ Colors.White,
+ Colors.Crimson,
+ CanvasPathGeometry.CreateColor("#bf5af2"),
+ CanvasPathGeometry.CreateColor("#0a84ff"),
+ CanvasPathGeometry.CreateColor("#32d74b"),
+ CanvasPathGeometry.CreateColor("#ff9500"),
+ CanvasPathGeometry.CreateColor("#ffd60a")
+ };
+
+ _commandBrush = new SolidColorBrush(Colors.White);
+ _commandErrorBrush = new SolidColorBrush(Colors.Red);
+
+ var colorList = new List()
+ {
+ "Transparent",
+ "Black",
+ "White",
+ "Crimson",
+ "Purple",
+ "LightBlue",
+ "LightGreen",
+ "Orange",
+ "Yellow"
+ };
+
+ this._samples = new List()
+ {
+ string.Empty,
+ Sample1,
+ Sample2,
+ Sample3,
+ Sample4,
+ Sample5
+ };
+
+ var sampleList = new List()
+ {
+ "None",
+ "Sample 1",
+ "Sample 2",
+ "Sample 3",
+ "Sample 4",
+ "Sample 5",
+ };
+
+ StrokeList.ItemsSource = colorList;
+ FillList.ItemsSource = colorList;
+
+ StrokeThickness.Value = 1;
+ StrokeList.SelectedIndex = 1;
+ FillList.SelectedIndex = 0;
+
+ _selectionChanged = false;
+ }
+
+ private void ParseData()
+ {
+ _data = InputData.Text;
+ _isParsing = true;
+ RenderCanvas.Invalidate();
+ _isParsing = false;
+ }
+
+ private void OnCanvasDraw(CanvasControl sender, CanvasDrawEventArgs args)
+ {
+ if (string.IsNullOrWhiteSpace(_data))
+ {
+ CommandsList.Text = string.Empty;
+ return;
+ }
+
+ this._errorGeometry ??= CanvasPathGeometry.CreateGeometry(sender, ErrorString);
+
+ _logger?.Clear();
+ CommandsList.Text = string.Empty;
+
+ try
+ {
+ _logger?.AppendLine("// The following commands represent the CanvasPathBuilder command(s) needed");
+ _logger?.AppendLine("// to create the CanvasGeometry from the specified Win2d Path Mini Language.");
+ var geometry = CanvasPathGeometry.CreateGeometry(sender, _data);
+ _reader.StartLogging();
+ geometry.SendPathTo(_reader);
+ _logger?.AppendLine(_reader.EndLogging());
+ CommandsList.Text = _logger?.ToString() ?? string.Empty;
+
+ args.DrawingSession.FillGeometry(geometry, _fillColor);
+ args.DrawingSession.DrawGeometry(geometry, _strokeColor, _strokeThickness);
+ RootPivot.SelectedIndex = 0;
+ CommandsList.Foreground = _commandBrush;
+ }
+ catch (ArgumentException argEx)
+ {
+ var message = argEx.Message;
+ var errorCode = message.Substring(0, 11);
+ var parseError = string.Empty;
+ if (message.StartsWith(ParseError1))
+ {
+ parseError = "Parse Error: No matching data!";
+ }
+ else if (message.StartsWith(ParseError2))
+ {
+ parseError = "Parse Error: Multiple FillRule elements present in Path Data!";
+ }
+ else if (message.StartsWith(ParseError3))
+ {
+ var tokens = message.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+ if (tokens.Length == 3)
+ {
+ parseError = $"Parse Error at {tokens[1]}. Cannot parse '{tokens[2]}'.";
+ }
+ }
+ else
+ {
+ parseError = "Parsing error! Invalid input data!";
+ }
+
+ args.DrawingSession.FillGeometry(_errorGeometry, Colors.Black);
+ CommandsList.Text = parseError;
+ RootPivot.SelectedIndex = 1;
+ CommandsList.Foreground = _commandErrorBrush;
+ }
+ catch (Exception)
+ {
+ args.DrawingSession.FillGeometry(_errorGeometry, Colors.Black);
+ CommandsList.Text = "Parsing error! Invalid input data!";
+ RootPivot.SelectedIndex = 1;
+ CommandsList.Foreground = _commandErrorBrush;
+ }
+ }
+
+ private void OnStrokeThicknessChanged(object sender, RangeBaseValueChangedEventArgs e)
+ {
+ _strokeThickness = (float)StrokeThickness.Value;
+ _selectionChanged = true;
+ RenderCanvas.Invalidate();
+ }
+
+ private void OnStrokeColorChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (StrokeList.SelectedIndex > -1)
+ {
+ _strokeColor = _colors[StrokeList.SelectedIndex];
+ _selectionChanged = true;
+ }
+
+ RenderCanvas.Invalidate();
+ }
+
+ private void OnFillColorChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (FillList.SelectedIndex > -1)
+ {
+ _fillColor = _colors[FillList.SelectedIndex];
+ _selectionChanged = true;
+ }
+
+ RenderCanvas.Invalidate();
+ }
+
+ private void ShowSample(int index)
+ {
+ InputData.Text = _samples.ElementAt(index);
+
+ if (!_selectionChanged)
+ {
+ StrokeThickness.Value = 4;
+ StrokeList.SelectedIndex = 1;
+ FillList.SelectedIndex = 5;
+ _selectionChanged = false;
+ }
+
+ _data = InputData.Text;
+ RenderCanvas.Invalidate();
+ }
+
+ private void OnClearCanvas(object sender, RoutedEventArgs e)
+ {
+ InputData.Text = string.Empty;
+ ParseData();
+ }
+
+ private void OnShowRoundedStarSample(object sender, RoutedEventArgs e)
+ {
+ ShowSample(1);
+ }
+
+ private void OnShowStarSample(object sender, RoutedEventArgs e)
+ {
+ ShowSample(2);
+ }
+
+ private void OnShowSpiralSample(object sender, RoutedEventArgs e)
+ {
+ ShowSample(3);
+ }
+
+ private void OnShowFlowerSample(object sender, RoutedEventArgs e)
+ {
+ ShowSample(4);
+ }
+
+ private void OnShowGearSample(object sender, RoutedEventArgs e)
+ {
+ ShowSample(5);
+ }
+
+ public void OnInputTextChanged(object sender, RoutedEventArgs e)
+ {
+ // Call the ParseData method only after 0.3 seconds have elapsed since last trigger.
+ _typeTimer.Debounce(ParseData, TimeSpan.FromSeconds(0.3));
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/GeometryStreamReader.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/GeometryStreamReader.cs
new file mode 100644
index 00000000000..a0b3dbed959
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/CanvasPathGeometry/GeometryStreamReader.cs
@@ -0,0 +1,127 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text;
+using Microsoft.Graphics.Canvas.Geometry;
+
+namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
+{
+ ///
+ /// Class to read the path data.
+ ///
+ internal class GeometryStreamReader : ICanvasPathReceiver
+ {
+ private readonly StringBuilder _cmdBuilder;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public GeometryStreamReader()
+ {
+ _cmdBuilder = new StringBuilder();
+ }
+
+ ///
+ /// Starts logging the data for the sample app
+ ///
+ public void StartLogging()
+ {
+ _cmdBuilder.Clear();
+ _cmdBuilder.AppendLine($"using (var pathBuilder = new CanvasPathBuilder(null))");
+ _cmdBuilder.AppendLine("{\n");
+ }
+
+ ///
+ /// Finishes reading the geometry path data and returns the data as formatted string.
+ ///
+ /// commands to create the CanvasGeometry
+ public string EndLogging()
+ {
+ _cmdBuilder.AppendLine("}");
+ return _cmdBuilder.ToString();
+ }
+
+ ///
+ /// Starts a new figure at the specified point, with the specified figure fill option.
+ ///
+ /// Start point
+ ///
+ public void BeginFigure(Vector2 point, CanvasFigureFill fill)
+ {
+ _cmdBuilder.AppendLine($"\n pathBuilder.BeginFigure(new Vector2({point.X}, {point.Y}));");
+ }
+
+ ///
+ /// Adds a single arc to the path, specified by start and end points through which an ellipse will be fitted.
+ ///
+ /// Start Point
+ /// radiusX
+ /// radiusY
+ /// rotationAngle
+ ///
+ ///
+ public void AddArc(Vector2 point, float x, float y, float z, CanvasSweepDirection sweepDirection, CanvasArcSize arcSize)
+ {
+ _cmdBuilder.AppendLine($" pathBuilder.AddArc(new Vector2({point.X}, {point.Y}), {x}, {y}, {z}, {sweepDirection}, {arcSize});");
+ }
+
+ ///
+ /// Adds a cubic bezier to the path. The bezier starts where the path left off, and has the specified control points and end point.
+ ///
+ /// First ControlPoint
+ /// Second Control Point
+ /// EndPoint
+ public void AddCubicBezier(Vector2 controlPoint1, Vector2 controlPoint2, Vector2 endPoint)
+ {
+ _cmdBuilder.AppendLine($" pathBuilder.AddCubicBezier(new Vector2({controlPoint1.X}, {controlPoint1.Y}), new Vector2({controlPoint2.X}, {controlPoint2.Y}), new Vector2({endPoint.X}, {endPoint.Y}));");
+ }
+
+ ///
+ /// Adds a line segment to the path, with the specified end point.
+ ///
+ /// EndPoint
+ public void AddLine(Vector2 endPoint)
+ {
+ _cmdBuilder.AppendLine($" pathBuilder.AddLine(new Vector2({endPoint.X}, {endPoint.Y}));");
+ }
+
+ ///
+ /// Adds a quadratic bezier to the path. The bezier starts where the path left off, and has the specified control point and end point.
+ ///
+ /// Control Point
+ /// EndPoint
+ public void AddQuadraticBezier(Vector2 controlPoint, Vector2 endPoint)
+ {
+ _cmdBuilder.AppendLine($" pathBuilder.AddQuadraticBezier(new Vector2({controlPoint.X}, {controlPoint.Y}), new Vector2({endPoint.X}, {endPoint.Y}));");
+ }
+
+ ///
+ /// Specifies the method used to determine which points are inside the geometry described by this path builder, and which points are outside.
+ ///
+ ///
+ public void SetFilledRegionDetermination(CanvasFilledRegionDetermination filledRegionDetermination)
+ {
+ _cmdBuilder.AppendLine($" pathBuilder.SetFilledRegionDetermination(CanvasFilledRegionDetermination.{filledRegionDetermination});");
+ }
+
+ ///
+ /// Specifies stroke and join options to be applied to new segments added to the path builder.
+ ///
+ ///
+ public void SetSegmentOptions(CanvasFigureSegmentOptions figureSegmentOptions)
+ {
+ // Do nothing
+ }
+
+ ///
+ /// >Ends the current figure; optionally, closes it.
+ ///
+ ///
+ public void EndFigure(CanvasFigureLoop figureLoop)
+ {
+ _cmdBuilder.AppendLine($" pathBuilder.EndFigure({(figureLoop == CanvasFigureLoop.Closed ? "CanvasFigureLoop.Closed" : "CanvasFigureLoop.Open")});");
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
index 5b356756f2d..d16fbbaa95c 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
@@ -900,6 +900,14 @@
"Icon": "/Assets/Helpers.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/parsers/MarkdownParser.md"
},
+ {
+ "Name": "Win2d Path Mini Language Parser",
+ "Type": "CanvasPathGeometryPage",
+ "Subcategory": "Parser",
+ "About": "CanvasPathGeometry class allows you to convert Win2d Path Mini Language string to CanvasGeometry, Brushes, CanvasStrokes or CanvasStrokeStyles.",
+ "Icon": "/SamplePages/CanvasPathGeometry/CanvasPathGeometry.png",
+ "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/parsers/CanvasPathGeometry.md"
+ },
{
"Name": "LiveTile",
"Type": "LiveTilePage",
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasDrawingSessionExtensions.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasDrawingSessionExtensions.cs
new file mode 100644
index 00000000000..c1d870f08f9
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasDrawingSessionExtensions.cs
@@ -0,0 +1,307 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Graphics.Canvas.Geometry;
+using Windows.Foundation;
+using Windows.UI;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry
+{
+ ///
+ /// Extension methods for CanvasDrawingSession.
+ ///
+ public static class CanvasDrawingSessionExtensions
+ {
+ ///
+ /// Draws a circle of at the given center, having the specified radius, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Center of the Circle
+ /// Radius of the Circle
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawCircle(this CanvasDrawingSession session, Vector2 centerPoint, float radius, ICanvasStroke stroke)
+ {
+ session.DrawCircle(centerPoint, radius, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws a circle of at the given center, having the specified radius, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Offset of the Center in x axis
+ /// Ordinate of the Center in the y axis
+ /// Radius of the Circle
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawCircle(this CanvasDrawingSession session, float x, float y, float radius, ICanvasStroke stroke)
+ {
+ session.DrawCircle(x, y, radius, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws an Ellipse of at the given center, having the specified radius, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Center of the Circle
+ /// Radius in the X axis
+ /// Radius in the Y axis
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawEllipse(this CanvasDrawingSession session, Vector2 centerPoint, float radiusX, float radiusY, ICanvasStroke stroke)
+ {
+ session.DrawEllipse(centerPoint, radiusX, radiusY, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws an Ellipse of at the given center, having the specified radius, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Offset of the Center on the x axis
+ /// Offset of the Center on the y axis
+ /// Radius in the X axis
+ /// Radius in the Y axis
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawEllipse(this CanvasDrawingSession session, float x, float y, float radiusX, float radiusY, ICanvasStroke stroke)
+ {
+ session.DrawEllipse(x, y, radiusX, radiusY, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws a geometry relative to the origin, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// CanvasGeometry to render
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawGeometry(this CanvasDrawingSession session, CanvasGeometry geometry, ICanvasStroke stroke)
+ {
+ session.DrawGeometry(geometry, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws a geometry relative to the specified position, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// CanvasGeometry to render
+ /// Offset
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawGeometry(this CanvasDrawingSession session, CanvasGeometry geometry, Vector2 offset, ICanvasStroke stroke)
+ {
+ session.DrawGeometry(geometry, offset, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws a geometry relative to the specified position, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// CanvasGeometry to render
+ /// Offset on the x axis
+ /// Offset on the y axis
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawGeometry(this CanvasDrawingSession session, CanvasGeometry geometry, float x, float y, ICanvasStroke stroke)
+ {
+ session.DrawGeometry(geometry, x, y, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws a line between the specified positions, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Starting position of the line
+ /// Ending position of the line
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawLine(this CanvasDrawingSession session, Vector2 point0, Vector2 point1, ICanvasStroke stroke)
+ {
+ session.DrawLine(point0, point1, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws a line between the specified positions, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Offset of Starting position of the line on x-axis
+ /// Offset of Starting position of the line on y-axis
+ /// Offset of Ending position of the line on x-axis
+ /// Offset of Ending position of the line on y-axis
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawLine(this CanvasDrawingSession session, float x0, float y0, float x1, float y1, ICanvasStroke stroke)
+ {
+ session.DrawLine(x0, y0, x1, y1, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws a Rectangle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Rectangle dimensions
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawRectangle(this CanvasDrawingSession session, Rect rect, ICanvasStroke stroke)
+ {
+ session.DrawRectangle(rect, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws a Rectangle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Offset of the top left corner of the Rectangle on the x-axis
+ /// Offset of the top left corner of the Rectangle on the y-axis
+ /// Width of the Rectangle
+ /// Height of the Rectangle
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawRectangle(this CanvasDrawingSession session, float x, float y, float w, float h, ICanvasStroke stroke)
+ {
+ session.DrawRectangle(x, y, w, h, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws a Rounded Rectangle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Rectangle dimensions
+ /// Corner Radius on the x axis
+ /// Corner Radius on the y axis
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawRoundedRectangle(this CanvasDrawingSession session, Rect rect, float radiusX, float radiusY, ICanvasStroke stroke)
+ {
+ session.DrawRoundedRectangle(rect, radiusX, radiusY, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws a Rounded Rectangle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Offset of the top left corner of the Rounded Rectangle on the x-axis
+ /// Offset of the top left corner of the Rounded Rectangle on the y-axis
+ /// Width of the Rounded Rectangle
+ /// Height of the Rounded Rectangle
+ /// Corner Radius on the x axis
+ /// Corner Radius on the y axis
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawRoundedRectangle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, ICanvasStroke stroke)
+ {
+ session.DrawRoundedRectangle(x, y, w, h, radiusX, radiusY, stroke.Brush, stroke.Width, stroke.Style);
+ }
+
+ ///
+ /// Draws a Squircle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Offset of the top left corner of the Squircle on the x-axis
+ /// Offset of the top left corner of the Squircle on the y-axis
+ /// Width of the Squircle
+ /// Height of the Squircle
+ /// Corner Radius on the x axis
+ /// Corner Radius on the y axis
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, ICanvasStroke stroke)
+ {
+ using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY);
+ session.DrawGeometry(geometry, stroke);
+ }
+
+ ///
+ /// Draws a Squircle of the specified dimensions, using a CanvasStroke to define the stroke width, the stroke color and stroke style.
+ ///
+ /// CanvasDrawingSession
+ /// Offset of the top left corner of the Squircle on the x-axis
+ /// Offset of the top left corner of the Squircle on the y-axis
+ /// Width of the Squircle
+ /// Height of the Squircle
+ /// Corner Radius on the x axis
+ /// Corner Radius on the y axis
+ /// Offset of the Squircle from the origin.
+ /// CanvasStroke defining the stroke width, the stroke
+ /// color and stroke style.
+ public static void DrawSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, Vector2 offset, ICanvasStroke stroke)
+ {
+ using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY);
+ session.DrawGeometry(geometry, offset, stroke);
+ }
+
+ ///
+ /// Fills a Squircle of the specified dimensions, using the given color.
+ ///
+ /// CanvasDrawingSession
+ /// Offset of the top left corner of the Squircle on the x-axis
+ /// Offset of the top left corner of the Squircle on the y-axis
+ /// Width of the Squircle
+ /// Height of the Squircle
+ /// Corner Radius on the x axis
+ /// Corner Radius on the y axis
+ /// Color to fill the Squircle.
+ public static void FillSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, Color color)
+ {
+ using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY);
+ session.FillGeometry(geometry, color);
+ }
+
+ ///
+ /// Fills a Squircle of the specified dimensions, using the given brush.
+ ///
+ /// CanvasDrawingSession
+ /// Offset of the top left corner of the Squircle on the x-axis
+ /// Offset of the top left corner of the Squircle on the y-axis
+ /// Width of the Squircle
+ /// Height of the Squircle
+ /// Corner Radius on the x axis
+ /// Corner Radius on the y axis
+ /// Brush to fill the Squircle.
+ public static void FillSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, ICanvasBrush brush)
+ {
+ using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY);
+ session.FillGeometry(geometry, brush);
+ }
+
+ ///
+ /// Fills a Squircle of the specified dimensions, using the given color at specified offset.
+ ///
+ /// CanvasDrawingSession
+ /// Offset of the top left corner of the Squircle on the x-axis
+ /// Offset of the top left corner of the Squircle on the y-axis
+ /// Width of the Squircle
+ /// Height of the Squircle
+ /// Corner Radius on the x axis
+ /// Corner Radius on the y axis
+ /// Offset of the Squircle from the origin.
+ /// Color to fill the Squircle.
+ public static void FillSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, Vector2 offset, Color color)
+ {
+ using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY);
+ session.FillGeometry(geometry, offset, color);
+ }
+
+ ///
+ /// Fills a Squircle of the specified dimensions, using the given brush at specified offset.
+ ///
+ /// CanvasDrawingSession
+ /// Offset of the top left corner of the Squircle on the x-axis
+ /// Offset of the top left corner of the Squircle on the y-axis
+ /// Width of the Squircle
+ /// Height of the Squircle
+ /// Corner Radius on the x axis
+ /// Corner Radius on the y axis
+ /// Offset of the Squircle from the origin.
+ /// Brush to fill the Squircle.
+ public static void FillSquircle(this CanvasDrawingSession session, float x, float y, float w, float h, float radiusX, float radiusY, Vector2 offset, ICanvasBrush brush)
+ {
+ using var geometry = CanvasPathGeometry.CreateSquircle(session.Device, x, y, w, h, radiusX, radiusY);
+ session.FillGeometry(geometry, offset, brush);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathBuilderExtensions.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathBuilderExtensions.cs
new file mode 100644
index 00000000000..b704ce13f18
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathBuilderExtensions.cs
@@ -0,0 +1,462 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry
+{
+ ///
+ /// Defines extension methods for CanvasPathBuilder.
+ ///
+ public static class CanvasPathBuilderExtensions
+ {
+ private const float SquircleFactor = 1.125f;
+ private const float ControlPointFactor = 46f / 64f;
+
+ ///
+ /// Adds a line in the form of a cubic bezier. The control point of the quadratic bezier will be the endpoint of the line itself.
+ ///
+ ///
+ /// Ending location of the line segment.
+ public static void AddLineAsQuadraticBezier(this CanvasPathBuilder pathBuilder, Vector2 end)
+ {
+ pathBuilder.AddQuadraticBezier(end, end);
+ }
+
+ ///
+ /// Adds a line in the form of a cubic bezier. The two control points of the cubic bezier will be the endpoints of the line itself.
+ ///
+ ///
+ /// Starting location of the line segment.
+ /// Ending location of the line segment.
+ public static void AddLineAsCubicBezier(this CanvasPathBuilder pathBuilder, Vector2 start, Vector2 end)
+ {
+ pathBuilder.AddCubicBezier(start, end, end);
+ }
+
+ ///
+ /// Adds a circle figure to the path.
+ ///
+ ///
+ /// Center location of the circle.
+ /// Radius of the circle.
+ public static void AddCircleFigure(this CanvasPathBuilder pathBuilder, Vector2 center, float radius)
+ {
+ pathBuilder.AddEllipseFigure(center.X, center.Y, radius, radius);
+ }
+
+ ///
+ /// Adds a circle figure to the path.
+ ///
+ ///
+ /// X coordinate of the center location of the circle.
+ /// Y coordinate of the center location of the circle.
+ /// Radius of the circle.
+ public static void AddCircleFigure(this CanvasPathBuilder pathBuilder, float x, float y, float radius)
+ {
+ pathBuilder.AddEllipseFigure(x, y, radius, radius);
+ }
+
+ ///
+ /// Adds an ellipse figure to the path.
+ ///
+ ///
+ /// Center location of the ellipse.
+ /// Radius of the ellipse on the X-axis.
+ /// Radius of the ellipse on the Y-axis.
+ public static void AddEllipseFigure(this CanvasPathBuilder pathBuilder, Vector2 center, float radiusX, float radiusY)
+ {
+ pathBuilder.AddEllipseFigure(center.X, center.Y, radiusX, radiusY);
+ }
+
+ ///
+ /// Adds an ellipse figure to the path.
+ ///
+ ///
+ /// X coordinate of the center location of the ellipse.
+ /// Y coordinate of the center location of the ellipse.
+ /// Radius of the ellipse on the X-axis.
+ /// Radius of the ellipse on the Y-axis.
+ public static void AddEllipseFigure(this CanvasPathBuilder pathBuilder, float x, float y, float radiusX, float radiusY)
+ {
+ // Sanitize the radiusX by taking the absolute value
+ radiusX = Math.Abs(radiusX);
+
+ // Sanitize the radiusY by taking the absolute value
+ radiusY = Math.Abs(radiusY);
+
+ try
+ {
+ pathBuilder.BeginFigure(x + radiusX, y);
+ }
+ catch (ArgumentException)
+ {
+ // An ArgumentException will be raised if another figure was already begun( and not ended) before calling AddEllipseFigure() method.
+ static void Throw() => throw new InvalidOperationException("A call to CanvasPathBuilder.AddEllipseFigure occurred, " +
+ "when another figure was already begun. Please call CanvasPathBuilder.EndFigure method, " +
+ "before calling CanvasPathBuilder.AddEllipseFigure, to end the previous figure.");
+
+ Throw();
+ }
+
+ // First Semi-Ellipse
+ pathBuilder.AddArc(new Vector2(x - radiusX, y), radiusX, radiusY, Scalar.Pi, CanvasSweepDirection.Clockwise, CanvasArcSize.Large);
+
+ // Second Semi-Ellipse
+ pathBuilder.AddArc(new Vector2(x + radiusX, y), radiusX, radiusY, Scalar.Pi, CanvasSweepDirection.Clockwise, CanvasArcSize.Large);
+
+ // End Figure
+ pathBuilder.EndFigure(CanvasFigureLoop.Closed);
+ }
+
+ ///
+ /// Adds a n-sided polygon figure to the path.
+ ///
+ ///
+ /// Number of sides of the polygon.
+ /// Center location of the polygon.
+ /// Radius of the circle circumscribing the polygon i.e. the distance
+ /// of each of the vertices of the polygon from the center.
+ public static void AddPolygonFigure(this CanvasPathBuilder pathBuilder, int numSides, Vector2 center, float radius)
+ {
+ pathBuilder.AddPolygonFigure(numSides, center.X, center.Y, radius);
+ }
+
+ ///
+ /// Adds a n-sided polygon figure to the path.
+ ///
+ ///
+ /// Number of sides of the polygon.
+ /// X coordinate of the center location of the polygon.
+ /// Y coordinate of the center location of the polygon.
+ /// Radius of the circle circumscribing the polygon i.e. the distance
+ /// of each of the vertices of the polygon from the center.
+ public static void AddPolygonFigure(this CanvasPathBuilder pathBuilder, int numSides, float x, float y, float radius)
+ {
+ // Sanitize the radius by taking the absolute value
+ radius = Math.Abs(radius);
+
+ // A polygon should have at least 3 sides
+ if (numSides <= 2)
+ {
+ ThrowArgumentOutOfRangeException();
+ }
+
+ // Calculate the first vertex location based on the number of sides
+ var angle = Scalar.TwoPi / numSides;
+ var startAngle = numSides % 2 == 1 ? Scalar.PiByTwo : Scalar.PiByTwo - (angle / 2f);
+
+ var startX = x + (float)(radius * Math.Cos(startAngle));
+ var startY = y - (float)(radius * Math.Sin(startAngle));
+
+ try
+ {
+ pathBuilder.BeginFigure(startX, startY);
+ }
+ catch (ArgumentException)
+ {
+ // An ArgumentException will be raised if another figure was already begun( and not ended) before calling AddPolygonFigure() method.
+ static void Throw() => throw new InvalidOperationException("A call to CanvasPathBuilder.AddPolygonFigure occurred, " +
+ "when another figure was already begun. Please call CanvasPathBuilder.EndFigure method, " +
+ "before calling CanvasPathBuilder.AddPolygonFigure, to end the previous figure.");
+
+ Throw();
+ }
+
+ // Add lines to the remaining vertices
+ for (var i = 1; i < numSides; i++)
+ {
+ var posX = x + (float)(radius * Math.Cos(startAngle + (i * angle)));
+ var posY = y - (float)(radius * Math.Sin(startAngle + (i * angle)));
+ pathBuilder.AddLine(posX, posY);
+ }
+
+ // Add a line to the first vertex so that the lines join properly
+ pathBuilder.AddLine(startX, startY);
+
+ // End the Figure
+ pathBuilder.EndFigure(CanvasFigureLoop.Closed);
+
+ static void ThrowArgumentOutOfRangeException() => throw new ArgumentOutOfRangeException($"Parameter {nameof(numSides)} must be greater than 2.");
+ }
+
+ ///
+ /// Adds a Rectangle to the Path.
+ ///
+ ///
+ /// X offset of the TopLeft corner of the Rectangle
+ /// Y offset of the TopLeft corner of the Rectangle
+ /// Width of the Rectangle
+ /// Height of the Rectangle
+ public static void AddRectangleFigure(this CanvasPathBuilder pathBuilder, float x, float y, float width, float height)
+ {
+ // Sanitize the width by taking the absolute value
+ width = Math.Abs(width);
+
+ // Sanitize the height by taking the absolute value
+ height = Math.Abs(height);
+
+ try
+ {
+ pathBuilder.BeginFigure(x, y);
+ }
+ catch (ArgumentException)
+ {
+ // An ArgumentException will be raised if another figure was already begun( and not ended) before calling AddPolygonFigure() method.
+ static void Throw() => throw new InvalidOperationException("A call to CanvasPathBuilder.AddRectangleFigure occurred, " +
+ "when another figure was already begun. Please call CanvasPathBuilder.EndFigure method, " +
+ "before calling CanvasPathBuilder.AddRectangleFigure, to end the previous figure.");
+
+ Throw();
+ }
+
+ // Top Side
+ pathBuilder.AddLine(x + width, y);
+
+ // Right Side
+ pathBuilder.AddLine(x + width, y + height);
+
+ // Bottom Side
+ pathBuilder.AddLine(x, y + height);
+
+ // Left Side
+ pathBuilder.AddLine(x, y);
+
+ // End the Figure
+ pathBuilder.EndFigure(CanvasFigureLoop.Closed);
+ }
+
+ ///
+ /// Adds a RoundedRectangle to the Path.
+ ///
+ ///
+ /// X offset of the TopLeft corner of the RoundedRectangle
+ /// Y offset of the TopLeft corner of the RoundedRectangle
+ /// Width of the RoundedRectangle
+ /// Height of the RoundedRectangle
+ /// Corner Radius on the x-axis
+ /// Corner Radius on the y-axis
+ public static void AddRoundedRectangleFigure(this CanvasPathBuilder pathBuilder, float x, float y, float width, float height, float radiusX, float radiusY)
+ {
+ // Sanitize the width by taking the absolute value
+ width = Math.Abs(width);
+
+ // Sanitize the height by taking the absolute value
+ height = Math.Abs(height);
+
+ var rect = new CanvasRoundRect(x, y, width, height, radiusX, radiusY);
+ pathBuilder.AddRoundedRectangleFigure(ref rect, true);
+ }
+
+ ///
+ /// Adds a RoundedRectangle to the Path. (To be used internally)
+ ///
+ ///
+ /// CanvasRoundRect
+ /// Flag to indicate whether exception should be raised
+ internal static void AddRoundedRectangleFigure(this CanvasPathBuilder pathBuilder, ref CanvasRoundRect rect, bool raiseException = false)
+ {
+ try
+ {
+ // Begin path
+ pathBuilder.BeginFigure(new Vector2(rect.LeftTopX, rect.LeftTopY));
+ }
+ catch (ArgumentException)
+ {
+ if (!raiseException)
+ {
+ return;
+ }
+
+ // An ArgumentException will be raised if another figure was already begun( and not ended) before calling AddPolygonFigure() method.
+ static void Throw() => throw new InvalidOperationException("A call to CanvasPathBuilder.AddRoundedRectangleFigure occurred, " +
+ "when another figure was already begun. Please call CanvasPathBuilder.EndFigure method, " +
+ "before calling CanvasPathBuilder.AddRoundedRectangleFigure, to end the previous figure.");
+
+ Throw();
+ }
+
+ // Top line
+ pathBuilder.AddLine(new Vector2(rect.RightTopX, rect.RightTopY));
+
+ // Upper-right corner
+ var radiusX = rect.TopRightX - rect.RightTopX;
+ var radiusY = rect.TopRightY - rect.RightTopY;
+ var center = new Vector2(rect.RightTopX, rect.TopRightY);
+ pathBuilder.AddArc(center, radiusX, radiusY, 3f * Scalar.PiByTwo, Scalar.PiByTwo);
+
+ // Right line
+ pathBuilder.AddLine(new Vector2(rect.BottomRightX, rect.BottomRightY));
+
+ // Lower-right corner
+ radiusX = rect.BottomRightX - rect.RightBottomX;
+ radiusY = rect.RightBottomY - rect.BottomRightY;
+ center = new Vector2(rect.RightBottomX, rect.BottomRightY);
+ pathBuilder.AddArc(center, radiusX, radiusY, 0f, Scalar.PiByTwo);
+
+ // Bottom line
+ pathBuilder.AddLine(new Vector2(rect.LeftBottomX, rect.LeftBottomY));
+
+ // Lower-left corner
+ radiusX = rect.LeftBottomX - rect.BottomLeftX;
+ radiusY = rect.LeftBottomY - rect.BottomLeftY;
+ center = new Vector2(rect.LeftBottomX, rect.BottomLeftY);
+ pathBuilder.AddArc(center, radiusX, radiusY, Scalar.PiByTwo, Scalar.PiByTwo);
+
+ // Left line
+ pathBuilder.AddLine(new Vector2(rect.TopLeftX, rect.TopLeftY));
+
+ // Upper-left corner
+ radiusX = rect.LeftTopX - rect.TopLeftX;
+ radiusY = rect.TopLeftY - rect.LeftTopY;
+ center = new Vector2(rect.LeftTopX, rect.TopLeftY);
+ pathBuilder.AddArc(center, radiusX, radiusY, 2f * Scalar.PiByTwo, Scalar.PiByTwo);
+
+ // End path
+ pathBuilder.EndFigure(CanvasFigureLoop.Closed);
+ }
+
+ ///
+ /// Adds a Squircle to the Path.
+ ///
+ ///
+ /// X offset of the TopLeft corner of the Squircle
+ /// Y offset of the TopLeft corner of the Squircle
+ /// Width of the Squircle
+ /// Height of the Squircle
+ /// Corner Radius on the x-axis
+ /// Corner Radius on the y-axis
+ public static void AddSquircleFigure(this CanvasPathBuilder pathBuilder, float x, float y, float width, float height, float radiusX, float radiusY)
+ {
+ // Sanitize the width by taking the absolute value
+ width = Math.Abs(width);
+
+ // Sanitize the height by taking the absolute value
+ height = Math.Abs(height);
+
+ var rect = new CanvasRoundRect(x, y, width, height, radiusX * SquircleFactor, radiusY * SquircleFactor);
+ pathBuilder.AddSquircleFigure(ref rect, true);
+ }
+
+ ///
+ /// Adds a Squircle to the Path. (To be used internally)
+ ///
+ ///
+ /// CanvasRoundRect
+ /// Flag to indicate whether exception should be raised
+ internal static void AddSquircleFigure(this CanvasPathBuilder pathBuilder, ref CanvasRoundRect rect, bool raiseException = false)
+ {
+ try
+ {
+ // Begin path
+ pathBuilder.BeginFigure(new Vector2(rect.LeftTopX, rect.LeftTopY));
+ }
+ catch (ArgumentException)
+ {
+ if (!raiseException)
+ {
+ return;
+ }
+
+ // An ArgumentException will be raised if another figure was already begun( and not ended) before calling AddPolygonFigure() method.
+ static void Throw() => throw new InvalidOperationException("A call to CanvasPathBuilder.AddSquircleFigure occurred, " +
+ "when another figure was already begun. Please call CanvasPathBuilder.EndFigure method, " +
+ "before calling CanvasPathBuilder.AddSquircleFigure, to end the previous figure.");
+
+ Throw();
+ }
+
+ // Top line
+ pathBuilder.AddLine(new Vector2(rect.RightTopX, rect.RightTopY));
+
+ // Upper-right corner
+ var rightTopControlPoint = new Vector2(rect.RightTopX + ((rect.TopRightX - rect.RightTopX) * ControlPointFactor), rect.RightTopY);
+ var topRightControlPoint = new Vector2(rect.TopRightX, rect.TopRightY - ((rect.TopRightY - rect.RightTopY) * ControlPointFactor));
+
+ // Top Right Curve
+ pathBuilder.AddCubicBezier(rightTopControlPoint, topRightControlPoint, new Vector2(rect.TopRightX, rect.TopRightY));
+
+ // Right line
+ pathBuilder.AddLine(new Vector2(rect.BottomRightX, rect.BottomRightY));
+
+ // Lower-right corner
+ var bottomRightControlPoint = new Vector2(rect.BottomRightX, rect.BottomRightY + ((rect.RightBottomY - rect.BottomRightY) * ControlPointFactor));
+ var rightBottomControlPoint = new Vector2(rect.RightBottomX + ((rect.BottomRightX - rect.RightBottomX) * ControlPointFactor), rect.RightBottomY);
+
+ // Bottom Right Curve
+ pathBuilder.AddCubicBezier(bottomRightControlPoint, rightBottomControlPoint, new Vector2(rect.RightBottomX, rect.RightBottomY));
+
+ // Bottom line
+ pathBuilder.AddLine(new Vector2(rect.LeftBottomX, rect.LeftBottomY));
+
+ // Lower-left corner
+ var leftBottomControlPoint = new Vector2(rect.LeftBottomX - ((rect.LeftBottomX - rect.BottomLeftX) * ControlPointFactor), rect.LeftBottomY);
+ var bottomLeftControlPoint = new Vector2(rect.BottomLeftX, rect.BottomLeftY + ((rect.LeftBottomY - rect.BottomLeftY) * ControlPointFactor));
+
+ // Bottom Left Curve
+ pathBuilder.AddCubicBezier(leftBottomControlPoint, bottomLeftControlPoint, new Vector2(rect.BottomLeftX, rect.BottomLeftY));
+
+ // Left line
+ pathBuilder.AddLine(new Vector2(rect.TopLeftX, rect.TopLeftY));
+
+ // Upper-left corner
+ var topLeftControlPoint = new Vector2(rect.TopLeftX, rect.TopLeftY - ((rect.TopLeftY - rect.LeftTopY) * ControlPointFactor));
+ var leftTopControlPoint = new Vector2(rect.LeftTopX - ((rect.LeftTopX - rect.TopLeftX) * ControlPointFactor), rect.LeftTopY);
+
+ // Top Left Curve
+ pathBuilder.AddCubicBezier(topLeftControlPoint, leftTopControlPoint, new Vector2(rect.LeftTopX, rect.LeftTopY));
+
+ // End path
+ pathBuilder.EndFigure(CanvasFigureLoop.Closed);
+ }
+
+ ///
+ /// Builds a path with the given collection of points.
+ ///
+ ///
+ /// Specifies whether the figure is open or closed.
+ /// This affects the appearance of fills and strokes, as well as geometry operations.
+ /// Collection of Vector2 points on the path.
+ /// object
+ public static CanvasPathBuilder BuildPathWithLines(this CanvasPathBuilder builder, CanvasFigureLoop canvasFigureLoop, IEnumerable points)
+ {
+ var first = true;
+
+ foreach (var point in points)
+ {
+ if (first)
+ {
+ builder.BeginFigure(point);
+ first = false;
+ }
+ else
+ {
+ builder.AddLine(point);
+ }
+ }
+
+ builder.EndFigure(canvasFigureLoop);
+ return builder;
+ }
+
+ ///
+ /// Builds a path with the given collection of points in the (x, y) pattern.
+ ///
+ ///
+ /// Specifies whether the figure is open or closed.
+ /// This affects the appearance of fills and strokes, as well as geometry operations.
+ /// Collection of points in the (x, y) pattern on the path.
+ /// object
+ public static CanvasPathBuilder BuildPathWithLines(this CanvasPathBuilder builder, CanvasFigureLoop canvasFigureLoop, IEnumerable<(float x, float y)> nodes)
+ {
+ var vectors = nodes.Select(n => new Vector2(n.x, n.y));
+ return BuildPathWithLines(builder, canvasFigureLoop, vectors);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathGeometry.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathGeometry.cs
new file mode 100644
index 00000000000..5a6bad1c378
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasPathGeometry.cs
@@ -0,0 +1,140 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Text;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers;
+using Windows.UI;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry
+{
+ ///
+ /// Helper Class for creating Win2d objects.
+ ///
+ public static class CanvasPathGeometry
+ {
+ ///
+ /// Parses the Path data string and converts it to CanvasGeometry.
+ ///
+ /// Path data
+ ///
+ public static CanvasGeometry CreateGeometry(string pathData)
+ {
+ return CreateGeometry(null, pathData);
+ }
+
+ ///
+ /// Parses the Path data string and converts it to CanvasGeometry.
+ ///
+ ///
+ /// Path data
+ ///
+ public static CanvasGeometry CreateGeometry(ICanvasResourceCreator resourceCreator, string pathData)
+ {
+ using (new CultureShield("en-US"))
+ {
+ // Get the CanvasGeometry from the path data
+ return CanvasGeometryParser.Parse(resourceCreator, pathData);
+ }
+ }
+
+ ///
+ /// Creates a Squircle geometry with the specified extents.
+ ///
+ /// Resource creator
+ /// X offset of the TopLeft corner of the Squircle
+ /// Y offset of the TopLeft corner of the Squircle
+ /// Width of the Squircle
+ /// Height of the Squircle
+ /// Corner Radius on the x-axis
+ /// Corner Radius on the y-axis
+ ///
+ public static CanvasGeometry CreateSquircle(ICanvasResourceCreator resourceCreator, float x, float y, float width, float height, float radiusX, float radiusY)
+ {
+ using var pathBuilder = new CanvasPathBuilder(resourceCreator);
+ pathBuilder.AddSquircleFigure(x, y, width, height, radiusX, radiusY);
+ return CanvasGeometry.CreatePath(pathBuilder);
+ }
+
+ ///
+ /// Parses the given Brush data string and converts it to ICanvasBrush.
+ ///
+ /// ICanvasResourceCreator
+ /// Brush data in string format
+ ///
+ public static ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator, string brushData)
+ {
+ using (new CultureShield("en-US"))
+ {
+ return CanvasBrushParser.Parse(resourceCreator, brushData);
+ }
+ }
+
+ ///
+ /// Parses the given Stroke data string and converts it to ICanvasStroke.
+ ///
+ /// ICanvasResourceCreator
+ /// Stroke data in string format
+ ///
+ public static ICanvasStroke CreateStroke(ICanvasResourceCreator resourceCreator, string strokeData)
+ {
+ using (new CultureShield("en-US"))
+ {
+ return CanvasStrokeParser.Parse(resourceCreator, strokeData);
+ }
+ }
+
+ ///
+ /// Parses the give CanvasStrokeStyle data string and converts it to CanvasStrokeStyle.
+ ///
+ /// CanvasStrokeStyle data in string format
+ /// object
+ public static CanvasStrokeStyle CreateStrokeStyle(string styleData)
+ {
+ using (new CultureShield("en-US"))
+ {
+ return CanvasStrokeStyleParser.Parse(styleData);
+ }
+ }
+
+ ///
+ /// Converts the color string in Hexadecimal or HDR color format to the corresponding Color object.
+ /// The hexadecimal color string should be in #RRGGBB or #AARRGGBB format.
+ /// The '#' character is optional.
+ /// The HDR color string should be in R G B A format.
+ /// (R, G, B & A should have value in the range between 0 and 1, inclusive)
+ ///
+ /// Color string in Hexadecimal or HDR format
+ /// Color
+ public static Color CreateColor(string colorString)
+ {
+ using (new CultureShield("en-US"))
+ {
+ return ColorParser.Parse(colorString);
+ }
+ }
+
+ ///
+ /// Converts a Vector4 High Dynamic Range Color to Color object.
+ /// Negative components of the Vector4 will be sanitized by taking the absolute
+ /// value of the component. The HDR Color components should have value in
+ /// the range between 0 and 1, inclusive. If they are more than 1, they
+ /// will be clamped at 1.
+ /// Vector4's X, Y, Z, W components match to Color's R, G, B, A components respectively.
+ ///
+ /// High Dynamic Range Color
+ /// Color
+ public static Color CreateColor(Vector4 hdrColor)
+ {
+ using (new CultureShield("en-US"))
+ {
+ return ColorParser.Parse(hdrColor);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasStroke.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasStroke.cs
new file mode 100644
index 00000000000..5daf59081c1
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CanvasStroke.cs
@@ -0,0 +1,112 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Graphics.Canvas.Geometry;
+using Windows.UI;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry
+{
+ ///
+ /// Class to represent the Stroke which can be used to render an outline on a
+ ///
+ public sealed class CanvasStroke : ICanvasStroke
+ {
+ ///
+ /// Gets or sets the brush with which the stroke will be rendered
+ ///
+ public ICanvasBrush Brush { get; set; }
+
+ ///
+ /// Gets or sets the width of the
+ ///
+ public float Width { get; set; }
+
+ ///
+ /// Gets or sets the Style of the
+ ///
+ public CanvasStrokeStyle Style { get; set; }
+
+ ///
+ /// Gets or sets the Transform matrix of the brush.
+ ///
+ public Matrix3x2 Transform
+ {
+ get => GetTransform();
+
+ set => SetTransform(value);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The brush with which the will be rendered
+ /// Width of the
+ public CanvasStroke(ICanvasBrush brush, float strokeWidth = 1f)
+ : this(brush, strokeWidth, new CanvasStrokeStyle())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The brush with which the will be rendered
+ /// Width of the
+ /// Style of the
+ public CanvasStroke(ICanvasBrush brush, float strokeWidth, CanvasStrokeStyle strokeStyle)
+ {
+ Brush = brush;
+ Width = strokeWidth;
+ Style = strokeStyle;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// ICanvasResourceCreator
+ /// Color of the
+ /// Width of the
+ public CanvasStroke(ICanvasResourceCreator device, Color strokeColor, float strokeWidth = 1f)
+ : this(device, strokeColor, strokeWidth, new CanvasStrokeStyle())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// ICanvasResourceCreator
+ /// Color of the
+ /// Width of the
+ /// Style of the
+ public CanvasStroke(ICanvasResourceCreator device, Color strokeColor, float strokeWidth, CanvasStrokeStyle strokeStyle)
+ {
+ Brush = new CanvasSolidColorBrush(device, strokeColor);
+ Width = strokeWidth;
+ Style = strokeStyle;
+ }
+
+ ///
+ /// Sets the 's Transform.
+ ///
+ /// Transform matrix to set
+ private void SetTransform(Matrix3x2 value)
+ {
+ if (Brush != null)
+ {
+ Brush.Transform = value;
+ }
+ }
+
+ ///
+ /// Gets the 's Transform. If stroke is null, then returns Matrix3x2.Identity.
+ ///
+ /// Transform matrix of the
+ private Matrix3x2 GetTransform()
+ {
+ return Brush?.Transform ?? Matrix3x2.Identity;
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CompositorGeometryExtensions.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CompositorGeometryExtensions.cs
new file mode 100644
index 00000000000..1d7c863dab6
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CompositorGeometryExtensions.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Graphics.Canvas.Geometry;
+using Windows.UI.Composition;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry
+{
+ ///
+ /// Extension methods for compositor to support Win2d Path Mini Language.
+ ///
+ public static class CompositorGeometryExtensions
+ {
+ ///
+ /// Creates a based on the specified path data.
+ ///
+ ///
+ /// Path data (Win2d Path Mini Language) in string format.
+ ///
+ public static CompositionPath CreatePath(this Compositor compositor, string pathData)
+ {
+ // Create CanvasGeometry
+ var geometry = CanvasPathGeometry.CreateGeometry(pathData);
+
+ // Create CompositionPath
+ return new CompositionPath(geometry);
+ }
+
+ ///
+ /// Creates a based on the given path data.
+ ///
+ ///
+ /// Path data (Win2d Path Mini Language) in string format.
+ ///
+ public static CompositionPathGeometry CreatePathGeometry(this Compositor compositor, string pathData)
+ {
+ // Create CanvasGeometry
+ var geometry = CanvasPathGeometry.CreateGeometry(pathData);
+
+ // Create CompositionPathGeometry
+ return compositor.CreatePathGeometry(new CompositionPath(geometry));
+ }
+
+ ///
+ /// Creates a based on the specified path data.
+ ///
+ ///
+ /// Path data (Win2d Path Mini Language) in string format.
+ ///
+ public static CompositionSpriteShape CreateSpriteShape(this Compositor compositor, string pathData)
+ {
+ // Create CanvasGeometry
+ var geometry = CanvasPathGeometry.CreateGeometry(pathData);
+
+ // Create CompositionPathGeometry
+ var pathGeometry = compositor.CreatePathGeometry(new CompositionPath(geometry));
+
+ // Create CompositionSpriteShape
+ return compositor.CreateSpriteShape(pathGeometry);
+ }
+
+ ///
+ /// Creates a from the specified .
+ ///
+ ///
+ ///
+ /// CompositionGeometricClip
+ public static CompositionGeometricClip CreateGeometricClip(this Compositor compositor, CanvasGeometry geometry)
+ {
+ // Create the CompositionPath
+ var path = new CompositionPath(geometry);
+
+ // Create the CompositionPathGeometry
+ var pathGeometry = compositor.CreatePathGeometry(path);
+
+ // Create the CompositionGeometricClip
+ return compositor.CreateGeometricClip(pathGeometry);
+ }
+
+ ///
+ /// Parses the specified path data and converts it to .
+ ///
+ ///
+ /// Path data (Win2d Path Mini Language) in string format.
+ ///
+ public static CompositionGeometricClip CreateGeometricClip(this Compositor compositor, string pathData)
+ {
+ // Create the CanvasGeometry from the path data
+ var geometry = CanvasPathGeometry.CreateGeometry(pathData);
+
+ // Create the CompositionGeometricClip
+ return compositor.CreateGeometricClip(geometry);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/CanvasRoundRect.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/CanvasRoundRect.cs
new file mode 100644
index 00000000000..103ec3628bb
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/CanvasRoundRect.cs
@@ -0,0 +1,280 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core
+{
+ ///
+ /// Structure which encapsulates the details of each of the core points of the path of the rounded rectangle which is calculated based on
+ /// either the given (Size, CornerRadius, BorderThickness and Padding) or (Size, RadiusX and RadiusY).
+ ///
+ internal struct CanvasRoundRect
+ {
+ private const float Factor = 0.5f;
+
+ private readonly float _leftTopWidth;
+ private readonly float _topLeftHeight;
+ private readonly float _topRightHeight;
+ private readonly float _rightTopWidth;
+ private readonly float _rightBottomWidth;
+ private readonly float _bottomRightHeight;
+ private readonly float _bottomLeftHeight;
+ private readonly float _leftBottomWidth;
+
+ // This is the location of the properties within the Rect
+ // |--LeftTop----------------------RightTop--|
+ // | |
+ // TopLeft TopRight
+ // | |
+ // | |
+ // | |
+ // | |
+ // | |
+ // | |
+ // BottomLeft BottomRight
+ // | |
+ // |--LeftBottom----------------RightBottom--|
+ internal float LeftTopX { get; private set; }
+
+ internal float LeftTopY { get; private set; }
+
+ internal float TopLeftX { get; private set; }
+
+ internal float TopLeftY { get; private set; }
+
+ internal float TopRightX { get; private set; }
+
+ internal float TopRightY { get; private set; }
+
+ internal float RightTopX { get; private set; }
+
+ internal float RightTopY { get; private set; }
+
+ internal float RightBottomX { get; private set; }
+
+ internal float RightBottomY { get; private set; }
+
+ internal float BottomRightX { get; private set; }
+
+ internal float BottomRightY { get; private set; }
+
+ internal float BottomLeftX { get; private set; }
+
+ internal float BottomLeftY { get; private set; }
+
+ internal float LeftBottomX { get; private set; }
+
+ internal float LeftBottomY { get; private set; }
+
+ internal float Width { get; }
+
+ internal float Height { get; }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Origin of the Rect (absolute location of Top Left corner)
+ /// Size of the Rect
+ /// CornerRadius
+ /// BorderThickness
+ /// Padding
+ /// Flag to indicate whether outer or inner border needs
+ /// to be calculated
+ internal CanvasRoundRect(Vector2 origin, Vector2 size, Vector4 cornerRadius, Vector4 borderThickness, Vector4 padding, bool isOuterBorder)
+ : this()
+ {
+ Width = Math.Max(0f, size.X);
+ Height = Math.Max(0f, size.Y);
+
+ var left = Factor * (borderThickness.X + padding.X);
+ var top = Factor * (borderThickness.Y + padding.Y);
+ var right = Factor * (borderThickness.Z + padding.Z);
+ var bottom = Factor * (borderThickness.W + padding.W);
+
+ if (isOuterBorder)
+ {
+ // Top Left corner radius
+ if (cornerRadius.X.IsZero())
+ {
+ _leftTopWidth = _topLeftHeight = 0f;
+ }
+ else
+ {
+ _leftTopWidth = cornerRadius.X + left;
+ _topLeftHeight = cornerRadius.X + top;
+ }
+
+ // Top Right corner radius
+ if (cornerRadius.Y.IsZero())
+ {
+ _topRightHeight = _rightTopWidth = 0f;
+ }
+ else
+ {
+ _topRightHeight = cornerRadius.Y + top;
+ _rightTopWidth = cornerRadius.Y + right;
+ }
+
+ // Bottom Right corner radius
+ if (cornerRadius.Z.IsZero())
+ {
+ _rightBottomWidth = _bottomRightHeight = 0f;
+ }
+ else
+ {
+ _rightBottomWidth = cornerRadius.Z + right;
+ _bottomRightHeight = cornerRadius.Z + bottom;
+ }
+
+ // Bottom Left corner radius
+ if (cornerRadius.W.IsZero())
+ {
+ _bottomLeftHeight = _leftBottomWidth = 0f;
+ }
+ else
+ {
+ _bottomLeftHeight = cornerRadius.W + bottom;
+ _leftBottomWidth = cornerRadius.W + left;
+ }
+ }
+ else
+ {
+ _leftTopWidth = Math.Max(0f, cornerRadius.X - left);
+ _topLeftHeight = Math.Max(0f, cornerRadius.X - top);
+ _topRightHeight = Math.Max(0f, cornerRadius.Y - top);
+ _rightTopWidth = Math.Max(0f, cornerRadius.Y - right);
+ _rightBottomWidth = Math.Max(0f, cornerRadius.Z - right);
+ _bottomRightHeight = Math.Max(0f, cornerRadius.Z - bottom);
+ _bottomLeftHeight = Math.Max(0f, cornerRadius.W - bottom);
+ _leftBottomWidth = Math.Max(0f, cornerRadius.W - left);
+ }
+
+ // Calculate the anchor points
+ ComputeCoordinates(origin.X, origin.Y);
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Top Left corner of the Rounded Rectangle
+ /// Dimensions of the Rounded Rectangle
+ /// Radius of the corners on the x-axis
+ /// Radius of the corners on the y-axis
+ internal CanvasRoundRect(Vector2 origin, Vector2 size, float radiusX, float radiusY)
+ : this(origin.X, origin.Y, size.X, size.Y, radiusX, radiusY)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// X offset of the Top Left corner of the Rounded Rectangle
+ /// Y offset of the Top Left corner of the Rounded Rectangle
+ /// Width of the Rounded Rectangle.
+ /// Height of the Rounded Rectangle.
+ /// Radius of the corners on the x-axis
+ /// Radius of the corners on the y-axis
+ internal CanvasRoundRect(float x, float y, float width, float height, float radiusX, float radiusY)
+ : this()
+ {
+ Width = Math.Max(0f, width);
+ Height = Math.Max(0f, height);
+
+ // Sanitize the radii by taking the absolute value
+ radiusX = Math.Min(Math.Abs(radiusX), width / 2f);
+ radiusY = Math.Min(Math.Abs(radiusY), height / 2);
+
+ _leftTopWidth = radiusX;
+ _rightTopWidth = radiusX;
+ _rightBottomWidth = radiusX;
+ _leftBottomWidth = radiusX;
+ _topLeftHeight = radiusY;
+ _topRightHeight = radiusY;
+ _bottomRightHeight = radiusY;
+ _bottomLeftHeight = radiusY;
+
+ ComputeCoordinates(x, y);
+ }
+
+ ///
+ /// Computes the coordinates of the crucial points on the CanvasRoundRect
+ ///
+ /// X coordinate of the origin.
+ /// Y coordinate of the origin.
+ private void ComputeCoordinates(float originX, float originY)
+ {
+ // compute the coordinates of the key points
+ var leftTopX = _leftTopWidth;
+ var leftTopY = 0f;
+ var rightTopX = Width - _rightTopWidth;
+ var rightTopY = 0f;
+ var topRightX = Width;
+ var topRightY = _topRightHeight;
+ var bottomRightX = Width;
+ var bottomRightY = Height - _bottomRightHeight;
+ var rightBottomX = Width - _rightBottomWidth;
+ var rightBottomY = Height;
+ var leftBottomX = _leftBottomWidth;
+ var leftBottomY = Height;
+ var bottomLeftX = 0f;
+ var bottomLeftY = Height - _bottomLeftHeight;
+ var topLeftX = 0f;
+ var topLeftY = _topLeftHeight;
+
+ // check anchors for overlap and resolve by partitioning corners according to
+ // the percentage of each one.
+ // top edge
+ if (leftTopX > rightTopX)
+ {
+ var v = _leftTopWidth / (_leftTopWidth + _rightTopWidth) * Width;
+ leftTopX = v;
+ rightTopX = v;
+ }
+
+ // right edge
+ if (topRightY > bottomRightY)
+ {
+ var v = _topRightHeight / (_topRightHeight + _bottomRightHeight) * Height;
+ topRightY = v;
+ bottomRightY = v;
+ }
+
+ // bottom edge
+ if (leftBottomX > rightBottomX)
+ {
+ var v = _leftBottomWidth / (_leftBottomWidth + _rightBottomWidth) * Width;
+ rightBottomX = v;
+ leftBottomX = v;
+ }
+
+ // left edge
+ if (topLeftY > bottomLeftY)
+ {
+ var v = _topLeftHeight / (_topLeftHeight + _bottomLeftHeight) * Height;
+ bottomLeftY = v;
+ topLeftY = v;
+ }
+
+ // Apply origin translation
+ LeftTopX = leftTopX + originX;
+ LeftTopY = leftTopY + originY;
+ RightTopX = rightTopX + originX;
+ RightTopY = rightTopY + originY;
+ TopRightX = topRightX + originX;
+ TopRightY = topRightY + originY;
+ BottomRightX = bottomRightX + originX;
+ BottomRightY = bottomRightY + originY;
+ RightBottomX = rightBottomX + originX;
+ RightBottomY = rightBottomY + originY;
+ LeftBottomX = leftBottomX + originX;
+ LeftBottomY = leftBottomY + originY;
+ BottomLeftX = bottomLeftX + originX;
+ BottomLeftY = bottomLeftY + originY;
+ TopLeftX = topLeftX + originX;
+ TopLeftY = topLeftY + originY;
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/GeometryTypeDefinitions.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/GeometryTypeDefinitions.cs
new file mode 100644
index 00000000000..027000a6547
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/GeometryTypeDefinitions.cs
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core
+{
+ ///
+ /// Enum for the various PathFigures.
+ ///
+ internal enum PathFigureType
+ {
+ FillRule,
+ PathFigure,
+ EllipseFigure,
+ PolygonFigure,
+ RectangleFigure,
+ RoundedRectangleFigure
+ }
+
+ ///
+ /// Enum for the various PathElements.
+ ///
+ internal enum PathElementType
+ {
+ MoveTo,
+ Line,
+ HorizontalLine,
+ VerticalLine,
+ QuadraticBezier,
+ SmoothQuadraticBezier,
+ CubicBezier,
+ SmoothCubicBezier,
+ Arc,
+ ClosePath
+ }
+
+ ///
+ /// Enum for the various types of Brushes.
+ ///
+ internal enum BrushType
+ {
+ SolidColor,
+ LinearGradient,
+ RadialGradient,
+ LinearGradientHdr,
+ RadialGradientHdr
+ }
+
+ ///
+ /// Enum for the various types of GradientStop attributes.
+ ///
+ internal enum GradientStopAttributeType
+ {
+ Main,
+ Additional,
+ MainHdr,
+ AdditionalHdr
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/PathElementFactory.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/PathElementFactory.cs
new file mode 100644
index 00000000000..ee77de7b668
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/PathElementFactory.cs
@@ -0,0 +1,157 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Text.RegularExpressions;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core
+{
+ ///
+ /// Factory class to instantiate various PathElements.
+ ///
+ internal static class PathElementFactory
+ {
+ ///
+ /// Creates a default Path Element for the given PathFigureType.
+ ///
+ /// PathFigureType
+ /// ICanvasPathElement
+ internal static ICanvasPathElement CreateDefaultPathElement(PathFigureType figureType)
+ {
+ if (figureType == PathFigureType.FillRule)
+ {
+ return new FillRuleElement();
+ }
+
+ static ICanvasPathElement Throw() => throw new ArgumentException("Creation of Only Default FillRuleElement is supported.");
+
+ return Throw();
+ }
+
+ ///
+ /// Creates a default Path Element for the given PathElementType.
+ ///
+ /// PathElementType
+ /// ICanvasPathElement
+ internal static ICanvasPathElement CreateDefaultPathElement(PathElementType elementType)
+ {
+ if (elementType == PathElementType.ClosePath)
+ {
+ return new ClosePathElement();
+ }
+
+ static ICanvasPathElement Throw() => throw new ArgumentException("Creation of Only Default ClosePathElement is supported.");
+
+ return Throw();
+ }
+
+ ///
+ /// Instantiates a PathElement based on the PathFigureType.
+ ///
+ /// PathFigureType
+ /// Match object
+ /// Index of the path element in the Path data
+ /// ICanvasPathElement
+ internal static ICanvasPathElement CreatePathFigure(PathFigureType figureType, Match match, int index)
+ {
+ var element = CreatePathElement(figureType);
+ element?.Initialize(match, index);
+ return element;
+ }
+
+ ///
+ /// Instantiates a PathElement based on the PathFigureType.
+ ///
+ /// PathFigureType
+ /// Capture object
+ /// Index of the capture
+ /// Indicates whether the coordinates are absolute or relative
+ /// ICanvasPathElement
+ internal static ICanvasPathElement CreateAdditionalPathFigure(PathFigureType figureType, Capture capture, int index, bool isRelative)
+ {
+ var element = CreatePathElement(figureType);
+ element?.InitializeAdditional(capture, index, isRelative);
+ return element;
+ }
+
+ ///
+ /// Instantiates a PathElement based on the PathElementType.
+ ///
+ /// PathElementType
+ /// Match object
+ /// Index of the path element in the Path data
+ /// ICanvasPathElement
+ internal static ICanvasPathElement CreatePathElement(PathElementType elementType, Match match, int index)
+ {
+ var element = CreatePathElement(elementType);
+ element?.Initialize(match, index);
+ return element;
+ }
+
+ ///
+ /// Instantiates a PathElement based on the PathElementType.
+ ///
+ /// PathElementType
+ /// Capture object
+ /// Index of the capture
+ /// Indicates whether the coordinates are absolute or relative
+ /// ICanvasPathElement
+ internal static ICanvasPathElement CreateAdditionalPathElement(PathElementType elementType, Capture capture, int index, bool isRelative)
+ {
+ // Additional attributes in MoveTo Command must be converted
+ // to Line commands
+ if (elementType == PathElementType.MoveTo)
+ {
+ elementType = PathElementType.Line;
+ }
+
+ var element = CreatePathElement(elementType);
+ element?.InitializeAdditional(capture, index, isRelative);
+ return element;
+ }
+
+ ///
+ /// Instantiates a PathElement based on the PathFigureType.
+ ///
+ /// PathFigureType
+ /// ICanvasPathElement
+ private static ICanvasPathElement CreatePathElement(PathFigureType figureType)
+ {
+ return figureType switch
+ {
+ PathFigureType.FillRule => new FillRuleElement(),
+ PathFigureType.PathFigure => new CanvasPathFigure(),
+ PathFigureType.EllipseFigure => new CanvasEllipseFigure(),
+ PathFigureType.PolygonFigure => new CanvasPolygonFigure(),
+ PathFigureType.RectangleFigure => new CanvasRectangleFigure(),
+ PathFigureType.RoundedRectangleFigure => new CanvasRoundRectangleFigure(),
+ _ => throw new ArgumentOutOfRangeException(nameof(figureType), figureType, "Invalid PathFigureType!")
+ };
+ }
+
+ ///
+ /// Instantiates a PathElement based on the PathElementType.
+ ///
+ /// PathElementType
+ /// ICanvasPathElement
+ private static ICanvasPathElement CreatePathElement(PathElementType elementType)
+ {
+ return elementType switch
+ {
+ PathElementType.MoveTo => new MoveToElement(),
+ PathElementType.Line => new LineElement(),
+ PathElementType.HorizontalLine => new HorizontalLineElement(),
+ PathElementType.VerticalLine => new VerticalLineElement(),
+ PathElementType.QuadraticBezier => new QuadraticBezierElement(),
+ PathElementType.SmoothQuadraticBezier => new SmoothQuadraticBezierElement(),
+ PathElementType.CubicBezier => new CubicBezierElement(),
+ PathElementType.SmoothCubicBezier => new SmoothCubicBezierElement(),
+ PathElementType.Arc => new ArcElement(),
+ PathElementType.ClosePath => new ClosePathElement(),
+ _ => throw new ArgumentOutOfRangeException(nameof(elementType), elementType, "Invalid PathElementType!")
+ };
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/RegexFactory.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/RegexFactory.cs
new file mode 100644
index 00000000000..79112c46df4
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Core/RegexFactory.cs
@@ -0,0 +1,602 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+
+[assembly: InternalsVisibleTo("UnitTests.UWP")]
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core
+{
+ ///
+ /// Contains all the Regular Expressions which are used for parsing the Win2d Path Mini Language.
+ ///
+ internal static class RegexFactory
+ {
+ // Whitespace
+ private const string Spacer = @"\s*";
+
+ // Whitespace or comma
+ private const string SpaceOrComma = @"(?:\s+|\s*,\s*)";
+
+ // Whitespace or comma or a minus/plus sign (look ahead)
+ private const string Sep = @"(?:\s+|\s*,\s*|(?=[-+.]))";
+
+ // Whitespace or comma or a '#' sign (look ahead)
+ private const string ColorSep = @"(?:\s+|\s*,\s*|(?=[#]))";
+
+ // Positive Integer
+ private const string Integer = @"[+-]?[0-9]+";
+
+ // Positive Integer
+ private const string PositiveInteger = @"[+]?[0-9]+";
+
+ // Floating point number
+ private const string Float = @"(?:[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?)";
+
+ // Positive Floating point number
+ private const string PositiveFloat = @"(?:[+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?)";
+
+ // Floating point number between 0 and 1, inclusive
+ // private const string Float01 = @"(?:(?[Mm]{Spacer}{Pos}(?:{Sep}{Pos})*{Spacer})";
+
+ // Line
+ private static readonly string Line = $"(?[Ll]{Spacer}{Pos}(?:{Sep}{Pos})*{Spacer})";
+
+ // Horizontal Line
+ private static readonly string HorizontalLine = $"(?[Hh]{Spacer}{Float}(?:{Sep}{Float})*{Spacer})";
+
+ // Vertical Line
+ private static readonly string VerticalLine = $"(?[Vv]{Spacer}{Float}(?:{Sep}{Float})*{Spacer})";
+
+ // Quadratic Bezier
+ private static readonly string QuadraticBezier = $"(?[Qq]{Spacer}{Pos}{Sep}{Pos}(?:{Sep}{Pos}{Sep}{Pos})*{Spacer})";
+
+ // Smooth Quadratic Bezier
+ private static readonly string SmoothQuadraticBezier = $"(?[Tt]{Spacer}{Pos}(?:{Sep}{Pos})*{Spacer})";
+
+ // Cubic Bezier
+ private static readonly string CubicBezier = $"(?[Cc]{Spacer}{Pos}{Sep}{Pos}{Sep}{Pos}(?:{Sep}{Pos}{Sep}{Pos}{Sep}{Pos})*{Spacer})";
+
+ // Smooth Cubic Bezier
+ private static readonly string SmoothCubicBezier = $"(?[Ss]{Spacer}{Pos}{Sep}{Pos}(?:{Sep}{Pos}{Sep}{Pos})*{Spacer})";
+
+ // Arc
+ private static readonly string Arc = $"(?[Aa]{Spacer}{Float}{Sep}{Float}{Sep}{Float}{SpaceOrComma}[01]{SpaceOrComma}[01]{Sep}{Pos}" +
+ $"(?:{Sep}{Float}{Sep}{Float}{Sep}{Float}{SpaceOrComma}[01]{SpaceOrComma}[01]{Sep}{Pos})*{Spacer})";
+
+ // Close Path
+ private static readonly string ClosePath = $"(?[Zz]{Spacer})";
+
+ // CanvasPathFigure
+ private static readonly string CanvasPathFigureRegexString =
+ $"{MoveTo}" + // M x,y
+ "(" +
+ $"{Line}+|" + // L x,y
+ $"{HorizontalLine}+|" + // H x
+ $"{VerticalLine}+|" + // V y
+ $"{QuadraticBezier}+|" + // Q x1,y1 x,y
+ $"{SmoothQuadraticBezier}+|" + // T x,y
+ $"{CubicBezier}+|" + // C x1,y1 x2,y2 x,y
+ $"{SmoothCubicBezier}+|" + // S x2,y2 x,y
+ $"{Arc}+|" + // A radX, radY, angle, isLargeArc, sweepDirection, x, y
+ ")+" +
+ $"{ClosePath}?"; // Close Path (Optional)
+
+ // Fill Rule
+ private static readonly string FillRule = $"{Spacer}(?[Ff]{Spacer}[01])";
+
+ // PathFigure
+ private static readonly string PathFigure = $"{Spacer}(?{CanvasPathFigureRegexString})";
+
+ // Ellipse Figure
+ private static readonly string EllipseFigure = $"{Spacer}(?[Oo]{Spacer}{Float}{Sep}{Float}{Sep}{Pos}" +
+ $"(?:{Sep}{Float}{Sep}{Float}{Sep}{Pos})*)";
+
+ // Polygon Figure
+ private static readonly string PolygonFigure = $"{Spacer}(?[Pp]{Spacer}{Integer}{Sep}{Float}{Sep}{Pos}" +
+ $"(?:{Sep}{Integer}{Sep}{Float}{Sep}{Pos})*)";
+
+ // Rectangle Figure
+ private static readonly string RectangleFigure = $"{Spacer}(?[Rr]{Spacer}{Pos}{Sep}{Float}{Sep}{Float}" +
+ $"(?:{Sep}{Pos}{Sep}{Float}{Sep}{Float})*)";
+
+ // Rounded Rectangle Figure
+ private static readonly string RoundedRectangleFigure = $"{Spacer}(?[Uu]{Spacer}{Pos}{Sep}{Float}{Sep}{Float}{Sep}{Float}{Sep}{Float}" +
+ $"(?:{Sep}{Pos}{Sep}{Float}{Sep}{Float}{Sep}{Float}{Sep}{Float})*)";
+
+ // CanvasGeometry
+ private static readonly string CanvasGeometryRegexString =
+ $"{FillRule}?" + // F0 or F1
+ "(" +
+ $"{PathFigure}+|" + // Path Figure
+ $"{EllipseFigure}+|" + // O radX, radY, centerX, centerY
+ $"{PolygonFigure}+|" + // P numSides, radius, centerX, centerY
+ $"{RectangleFigure}+|" + // R x, y, width, height
+ $"{RoundedRectangleFigure}+" + // U x, y, width, height, radiusX, radiusY
+ ")+";
+
+ // MoveTo
+ private static readonly string MoveToAttributes = $"(?{Float}){Sep}(?{Float})";
+ private static readonly string MoveToRegexString = $"{Spacer}(?(?[Mm]){Spacer}{MoveToAttributes})" +
+ $"(?{Sep}{Pos})*";
+
+ // Line
+ private static readonly string LineAttributes = $"(?{Float}){Sep}(?{Float})";
+ private static readonly string LineRegexString = $"{Spacer}(?(?[Ll]){Spacer}{LineAttributes})" +
+ $"(?{Sep}{Pos})*";
+
+ // Horizontal Line
+ private static readonly string HorizontalLineAttributes = $"(?{Float})";
+ private static readonly string HorizontalLineRegexString = $"{Spacer}(?(?[Hh]){Spacer}{HorizontalLineAttributes})" +
+ $"(?{Sep}{Float})*";
+
+ // Vertical Line
+ private static readonly string VerticalLineAttributes = $"(?{Float})";
+ private static readonly string VerticalLineRegexString = $"{Spacer}(?(?[Vv]){Spacer}{VerticalLineAttributes})" +
+ $"(?{Sep}{Float})*";
+
+ // Quadratic Bezier
+ private static readonly string QuadraticBezierAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float})";
+ private static readonly string QuadraticBezierRegexString = $"{Spacer}(?(?[Qq]){Spacer}{QuadraticBezierAttributes})" +
+ $"(?{Sep}{Pos}{Sep}{Pos})*";
+
+ // Smooth Quadratic Bezier
+ private static readonly string SmoothQuadraticBezierAttributes = $"(?{Float}){Sep}(?{Float})";
+ private static readonly string SmoothQuadraticBezierRegexString = $"{Spacer}(?(?[Tt]){Spacer}{SmoothQuadraticBezierAttributes})" +
+ $"(?{Sep}{Pos})*";
+
+ // Cubic Bezier
+ private static readonly string CubicBezierAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}" +
+ $"(?{Float}){Sep}(?{Float})";
+
+ private static readonly string CubicBezierRegexString = $"{Spacer}(?(?[Cc]){Spacer}{CubicBezierAttributes})" +
+ $"(?{Sep}{Pos}{Sep}{Pos}{Sep}{Pos})*";
+
+ // Smooth Cubic Bezier
+ private static readonly string SmoothCubicBezierAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float})";
+ private static readonly string SmoothCubicBezierRegexString = $"{Spacer}(?(?[Ss]){Spacer}{SmoothCubicBezierAttributes})" +
+ $"(?{Sep}{Pos}{Sep}{Pos})*";
+
+ // Arc
+ private static readonly string ArcAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){SpaceOrComma}" +
+ $"(?[01]){SpaceOrComma}(?[01]){Sep}(?{Float}){Sep}(?{Float})";
+
+ private static readonly string ArcRegexString = $"{Spacer}(?(?[Aa]){Spacer}{ArcAttributes})" +
+ $"(?{Sep}{Float}{Sep}{Float}{Sep}{Float}{SpaceOrComma}[01]{SpaceOrComma}[01]{Sep}{Pos})*";
+
+ // Close Path
+ private static readonly string ClosePathRegexString = $"{Spacer}(?(?[Zz])){Spacer}";
+
+ // Fill Rule
+ private static readonly string FillRuleRegexString = $"{Spacer}(?(?[Ff]){Spacer}(?[01]))";
+
+ // Path Figure
+ private static readonly string PathFigureRegexString = $"{Spacer}(?{PathFigure})";
+
+ // Ellipse Figure
+ private static readonly string EllipseFigureAttributes = $"(?{Float}){Sep}(?{Float}){Sep}" +
+ $"(?{Float}){Sep}(?{Float})";
+
+ private static readonly string EllipseFigureRegexString = $"{Spacer}(?(?[Oo]){Spacer}{EllipseFigureAttributes})" +
+ $"(?{Sep}{Float}{Sep}{Float}{Sep}{Pos})*";
+
+ // Polygon Figure
+ private static readonly string PolygonFigureAttributes = $"(?{Integer}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float})";
+ private static readonly string PolygonFigureRegexString = $"{Spacer}(?(?[Pp]){Spacer}{PolygonFigureAttributes})" +
+ $"(?{Sep}{Integer}{Sep}{Float}{Sep}{Pos})*";
+
+ // Rectangle Figure
+ private static readonly string RectangleFigureAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float})";
+ private static readonly string RectangleFigureRegexString = $"{Spacer}(?(?[Rr]){Spacer}{RectangleFigureAttributes})" +
+ $"(?{Sep}{Pos}{Sep}{Float}{Sep}{Float})*";
+
+ // Rectangle Figure
+ private static readonly string RoundedRectangleFigureAttributes = $"(?{Float}){Sep}(?{Float}){Sep}(?{Float}){Sep}(?{Float})" +
+ $"{Sep}(?{Float}){Sep}(?{Float})";
+
+ private static readonly string RoundedRectangleFigureRegexString = $"{Spacer}(?(?[Uu]){Spacer}{RoundedRectangleFigureAttributes})" +
+ $"(?{Sep}{Pos}{Sep}{Float}{Sep}{Float}{Sep}{Float}{Sep}{Float})*";
+
+ // ARGB Color
+ private static readonly string HexColor = $"(?:#?(?:{Hex}{{2}})?{Hex}{{6}})";
+
+ // Alpha
+ private static readonly string Alpha = $"(?{Hex}{{2}})";
+
+ // Red
+ private static readonly string Red = $"(?{Hex}{{2}})";
+
+ // Green
+ private static readonly string Green = $"(?{Hex}{{2}})";
+
+ // Blue
+ private static readonly string Blue = $"(?{Hex}{{2}})";
+
+ // Hexadecimal Color
+ private static readonly string RgbColor = $"(?{HexColor})";
+
+ // HDR Color (Vector4 in which each component has a value between 0 and 1, inclusive)
+ private static readonly string HdrColor = $"(?{Float01}{Sep}{Float01}{Sep}{Float01}{Sep}{Float01})";
+
+ // Hexadecimal Color Attributes
+ private static readonly string RgbColorAttributes = $"(?#{{0,1}}{Alpha}{{0,1}}{Red}{Green}{Blue})";
+
+ // HDR Color Attributes (Vector4 in which each component has a value between 0 and 1, inclusive)
+ private static readonly string HdrColorAttributes = $"(?(?{Float01}){Sep}(?{Float01}){Sep}(?{Float01}){Sep}(?{Float01}))";
+
+ private static readonly string ColorRegexString = $"(?:{RgbColorAttributes}|{HdrColorAttributes})";
+
+ // Start Point
+ private static readonly string StartPoint = $"(?[Mm]{Spacer}{Pos}{Spacer})";
+
+ // End Point
+ private static readonly string EndPoint = $"(?[Zz]{Spacer}{Pos}{Spacer})";
+
+ // Opacity
+ private static readonly string Opacity = $"(?[Oo]{Spacer}{Float01}{Spacer})";
+
+ // Alpha Mode
+ private static readonly string AlphaMode = $"(?[Aa]{Spacer}[012]{Spacer})";
+
+ // Buffer Precision
+ private static readonly string BufferPrecision = $"(?[Bb]{Spacer}[01234]{Spacer})";
+
+ // Edge Behavior
+ private static readonly string EdgeBehavior = $"(?[Ee]{Spacer}[012]{Spacer})";
+
+ // PreInterpolation Color Space
+ private static readonly string PreColorSpace = $"(?[Pp]{Spacer}[012]{Spacer})";
+
+ // PostInterpolation Color Space
+ private static readonly string PostColorSpace = $"(?[Rr]{Spacer}[012]{Spacer})";
+
+ // Radius in X-axis
+ private static readonly string RadiusX = $"(?{Float})";
+
+ // Radius in Y-axis
+ private static readonly string RadiusY = $"(?{Float})";
+
+ // Center location on X-axis
+ private static readonly string CenterX = $"(?{Float})";
+
+ // Center location on Y-axis
+ private static readonly string CenterY = $"(?{Float})";
+
+ // Origin Offset
+ private static readonly string OriginOffset = $"(?[Ff]{Spacer}{Pos}{Spacer})";
+
+ // GradientStops
+ private static readonly string GradientStops = $"(?[Ss]{Spacer}{Float01}{ColorSep}{HexColor}(?:{Sep}{Float01}{ColorSep}{HexColor})*{Spacer})";
+
+ // GradientStopHdrs
+ private static readonly string GradientStopHdrs = $"(?[Ss]{Spacer}{Float01}{Sep}{HdrColor}(?:{Sep}{Float01}{Sep}{HdrColor})*{Spacer})";
+
+ // Solid Color Brush
+ private static readonly string SolidColorBrush = $"(?[Ss][Cc]{Spacer}(?:{RgbColor}|{HdrColor}){Spacer}{Opacity}?)";
+
+ // LinearGradient
+ private static readonly string LinearGradient = $"(?[Ll][Gg]{Spacer}{StartPoint}{EndPoint}" +
+ $"{Opacity}?{AlphaMode}?{BufferPrecision}?{EdgeBehavior}?{PreColorSpace}?{PostColorSpace}?" +
+ $"{GradientStops}+{Spacer})";
+
+ // RadialGradient
+ private static readonly string RadialGradient = $"(?[Rr][Gg]{Spacer}{RadiusX}{Sep}{RadiusY}{Sep}{CenterX}{Sep}{CenterY}{Spacer}" +
+ $"{Opacity}?{AlphaMode}?{BufferPrecision}?{EdgeBehavior}?{OriginOffset}?{PreColorSpace}?{PostColorSpace}?" +
+ $"{GradientStops}+{Spacer})";
+
+ // LinearGradientHdr
+ private static readonly string LinearGradientHdr = $"(?[Ll][Hh]{Spacer}{StartPoint}{EndPoint}" +
+ $"{Opacity}?{AlphaMode}?{BufferPrecision}?{EdgeBehavior}?{PreColorSpace}?{PostColorSpace}?" +
+ $"{GradientStopHdrs}+{Spacer})";
+
+ // RadialGradientHdr
+ private static readonly string RadialGradientHdr = $"(?[Rr][Hh]{Spacer}{RadiusX}{Sep}{RadiusY}{Sep}{CenterX}{Sep}{CenterY}{Spacer}" +
+ $"{Opacity}?{AlphaMode}?{BufferPrecision}?{EdgeBehavior}?{OriginOffset}?{PreColorSpace}?{PostColorSpace}?" +
+ $"{GradientStopHdrs}+{Spacer})";
+
+ // Regex for the CanvasBrush
+ private static readonly string CanvasBrushRegexString = $"(?{SolidColorBrush}|{LinearGradient}|{RadialGradient}|{LinearGradientHdr}|{RadialGradientHdr})";
+
+ // Start Point
+ private static readonly string StartPointAttr = $"(?:[Mm]{Spacer}(?{Float}){Sep}(?{Float}){Spacer})";
+
+ // End Point
+ private static readonly string EndPointAttr = $"(?:[Zz]{Spacer}(?{Float}){Sep}(?{Float}){Spacer})";
+
+ // Opacity
+ private static readonly string OpacityAttr = $"(?:[Oo]{Spacer}(?{Float01}){Spacer})";
+
+ // Alpha Mode
+ private static readonly string AlphaModeAttr = $"(?:[Aa]{Spacer}(?[012]){Spacer})";
+
+ // Buffer Precision
+ private static readonly string BufferPrecisionAttr = $"(?:[Bb]{Spacer}(?[01234]){Spacer})";
+
+ // Edge Behavior
+ private static readonly string EdgeBehaviorAttr = $"(?:[Ee]{Spacer}(?[012]){Spacer})";
+
+ // PreInterpolation Color Space
+ private static readonly string PreColorSpaceAttr = $"(?:[Pp]{Spacer}(?[012]){Spacer})";
+
+ // PostInterpolation Color Space
+ private static readonly string PostColorSpaceAttr = $"(?:[Rr]{Spacer}(?[012]){Spacer})";
+
+ // Origin Offset
+ private static readonly string OriginOffsetAttr = $"(?[Ff]{Spacer}(?{Float}){Sep}(?{Float}){Spacer})";
+
+ // GradientStop Attributes
+ private static readonly string GradientStopAttributes = $"(?{Float01}){ColorSep}{RgbColorAttributes}";
+ private static readonly string GradientStopMainAttributes = $"(?[Ss]{Spacer}{GradientStopAttributes})";
+ private static readonly string GradientStopRegexString = $"(?{GradientStopMainAttributes}" + $"(?{Sep}{Float01}{ColorSep}{HexColor})*{Spacer})";
+
+ // GradientStopHdr Attributes
+ private static readonly string GradientStopHdrAttributes = $"(?{Float01}){Sep}{HdrColorAttributes}";
+ private static readonly string GradientStopHdrMainAttributes = $"(?(?[Ss]){Spacer}{GradientStopHdrAttributes})";
+ private static readonly string GradientStopHdrRegexString = $"(?{GradientStopHdrMainAttributes}" + $"(?{Sep}{Float01}{Sep}{HdrColor})*{Spacer})";
+
+ // Regex for SolidColorBrush Attributes
+ private static readonly string SolidColorBrushRegexString = $"(?:[Ss][Cc]{Spacer}(?:{RgbColorAttributes}|{HdrColorAttributes}){Spacer}{OpacityAttr}?)";
+
+ // Regex for LinearGradient Attributes
+ private static readonly string LinearGradientRegexString = $"[Ll][Gg]{Spacer}{StartPointAttr}{EndPointAttr}" +
+ $"{OpacityAttr}?{AlphaModeAttr}?{BufferPrecisionAttr}?" +
+ $"{EdgeBehaviorAttr}?{PreColorSpaceAttr}?{PostColorSpaceAttr}?" +
+ $"{GradientStops}+{Spacer}";
+
+ // Regex for RadialGradient Attributes
+ private static readonly string RadialGradientRegexString = $"[Rr][Gg]{Spacer}{RadiusX}{Sep}{RadiusY}{Sep}{CenterX}{Sep}{CenterY}{Spacer}" +
+ $"{OpacityAttr}?{AlphaModeAttr}?{BufferPrecisionAttr}?{EdgeBehaviorAttr}?" +
+ $"{OriginOffsetAttr}?{PreColorSpaceAttr}?{PostColorSpaceAttr}?" +
+ $"{GradientStops}+{Spacer}";
+
+ // Regex for LinearGradientHdr Attributes
+ private static readonly string LinearGradientHdrRegexString = $"[Ll][Hh]{Spacer}{StartPointAttr}{EndPointAttr}" +
+ $"{OpacityAttr}?{AlphaModeAttr}?{BufferPrecisionAttr}?" +
+ $"{EdgeBehaviorAttr}?{PreColorSpaceAttr}?{PostColorSpaceAttr}?" +
+ $"{GradientStopHdrs}+{Spacer}";
+
+ // Regex for RadialGradientHdr Attributes
+ private static readonly string RadialGradientHdrRegexString = $"[Rr][Hh]{Spacer}{RadiusX}{Sep}{RadiusY}{Sep}{CenterX}{Sep}{CenterY}{Spacer}" +
+ $"{OpacityAttr}?{AlphaModeAttr}?{BufferPrecisionAttr}?{EdgeBehaviorAttr}?" +
+ $"{OriginOffsetAttr}?{PreColorSpaceAttr}?{PostColorSpaceAttr}?" +
+ $"{GradientStopHdrs}+{Spacer}";
+
+ // CanvasStrokeStyle attributes
+ private static readonly string DashStyle = $"(?:[Dd][Ss]{Spacer}(?[01234]){Spacer})";
+ private static readonly string LineJoin = $"(?:[Ll][Jj]{Spacer}(?[0123]){Spacer})";
+ private static readonly string MiterLimit = $"(?:[Mm][Ll]{Spacer}(?{Float}){Spacer})";
+ private static readonly string DashOffset = $"(?:[Dd][Oo]{Spacer}(?{Float}){Spacer})";
+ private static readonly string StartCap = $"(?:[Ss][Cc]{Spacer}(?[0123]){Spacer})";
+ private static readonly string EndCap = $"(?:[Ee][Cc]{Spacer}(?[0123]){Spacer})";
+ private static readonly string DashCap = $"(?:[Dd][Cc]{Spacer}(?[0123]){Spacer})";
+ private static readonly string TransformBehavior = $"(?:[Tt][Bb]{Spacer}(?[012]){Spacer})";
+ private static readonly string CustomDashAttribute = $"(?{Float}){Sep}(?{Float})";
+ private static readonly string CustomDashStyle = $"(?[Cc][Dd][Ss]{Spacer}(?{CustomDashAttribute})" + $"(?{Sep}{Float}{Sep}{Float})*{Spacer})";
+
+ // CanvasStrokeStyle Regex
+ private static readonly string CanvasStrokeStyleRegexString = $"(?[Cc][Ss][Ss]{Spacer}{DashStyle}?{LineJoin}?{MiterLimit}?{DashOffset}?" +
+ $"{StartCap}?{EndCap}?{DashCap}?{TransformBehavior}?{CustomDashStyle}?)";
+
+ // CanvasStroke Regex
+ private static readonly string CanvasStrokeRegexString = $"(?[Ss][Tt]{Spacer}" +
+ $"(?{Float}){Spacer}" +
+ $"{CanvasBrushRegexString}{Spacer}" +
+ $"{CanvasStrokeStyleRegexString}?)";
+
+ private static readonly Dictionary PathFigureRegexes;
+ private static readonly Dictionary PathFigureAttributeRegexes;
+ private static readonly Dictionary PathElementRegexes;
+ private static readonly Dictionary PathElementAttributeRegexes;
+ private static readonly Dictionary BrushRegexes;
+ private static readonly Dictionary GradientStopAttributeRegexes;
+
+ ///
+ /// Gets the Regex to perform validation of Path data.
+ ///
+ public static Regex ValidationRegex { get; }
+
+ ///
+ /// Gets the Regex for parsing the CanvasGeometry string.
+ ///
+ public static Regex CanvasGeometryRegex { get; }
+
+ ///
+ /// Gets the Regex for parsing Hexadecimal Color string.
+ ///
+ public static Regex ColorRegex { get; }
+
+ ///
+ /// Gets the Regex for parsing the ICanvasBrush string.
+ ///
+ public static Regex CanvasBrushRegex { get; }
+
+ ///
+ /// Gets the Regex for parsing the GradientStop string.
+ ///
+ public static Regex GradientStopRegex { get; }
+
+ ///
+ /// Gets the Regex for parsing the GradientStopHdr string.
+ ///
+ public static Regex GradientStopHdrRegex { get; }
+
+ ///
+ /// Gets the Regex for parsing the CanvasStrokeStyle string.
+ ///
+ public static Regex CanvasStrokeStyleRegex { get; }
+
+ ///
+ /// Gets the Regex for parsing the CanvasStroke string.
+ ///
+ public static Regex CanvasStrokeRegex { get; }
+
+ ///
+ /// Gets the Regex for parsing the CustomDashStyle attributes.
+ ///
+ public static Regex CustomDashAttributeRegex { get; }
+
+ ///
+ /// Initializes static members of the class.
+ ///
+ static RegexFactory()
+ {
+ PathFigureRegexes = new Dictionary
+ {
+ [PathFigureType.FillRule] = new Regex(FillRuleRegexString, RegexOptions.Compiled),
+ [PathFigureType.PathFigure] = new Regex(PathFigureRegexString, RegexOptions.Compiled),
+ [PathFigureType.EllipseFigure] = new Regex(EllipseFigureRegexString, RegexOptions.Compiled),
+ [PathFigureType.PolygonFigure] = new Regex(PolygonFigureRegexString, RegexOptions.Compiled),
+ [PathFigureType.RectangleFigure] = new Regex(RectangleFigureRegexString, RegexOptions.Compiled),
+ [PathFigureType.RoundedRectangleFigure] = new Regex(RoundedRectangleFigureRegexString, RegexOptions.Compiled)
+ };
+
+ PathFigureAttributeRegexes = new Dictionary
+ {
+ // Not Applicable for FillRuleElement
+ [PathFigureType.FillRule] = null,
+
+ // Not Applicable for CanvasPathFigure
+ [PathFigureType.PathFigure] = null,
+ [PathFigureType.EllipseFigure] = new Regex($"{Sep}{EllipseFigureAttributes}", RegexOptions.Compiled),
+ [PathFigureType.PolygonFigure] = new Regex($"{Sep}{PolygonFigureAttributes}", RegexOptions.Compiled),
+ [PathFigureType.RectangleFigure] = new Regex($"{Sep}{RectangleFigureAttributes}", RegexOptions.Compiled),
+ [PathFigureType.RoundedRectangleFigure] = new Regex($"{Sep}{RoundedRectangleFigureAttributes}", RegexOptions.Compiled)
+ };
+
+ PathElementRegexes = new Dictionary
+ {
+ [PathElementType.MoveTo] = new Regex(MoveToRegexString, RegexOptions.Compiled),
+ [PathElementType.Line] = new Regex(LineRegexString, RegexOptions.Compiled),
+ [PathElementType.HorizontalLine] = new Regex(HorizontalLineRegexString, RegexOptions.Compiled),
+ [PathElementType.VerticalLine] = new Regex(VerticalLineRegexString, RegexOptions.Compiled),
+ [PathElementType.QuadraticBezier] = new Regex(QuadraticBezierRegexString, RegexOptions.Compiled),
+ [PathElementType.SmoothQuadraticBezier] = new Regex(SmoothQuadraticBezierRegexString, RegexOptions.Compiled),
+ [PathElementType.CubicBezier] = new Regex(CubicBezierRegexString, RegexOptions.Compiled),
+ [PathElementType.SmoothCubicBezier] = new Regex(SmoothCubicBezierRegexString, RegexOptions.Compiled),
+ [PathElementType.Arc] = new Regex(ArcRegexString, RegexOptions.Compiled),
+ [PathElementType.ClosePath] = new Regex(ClosePathRegexString, RegexOptions.Compiled)
+ };
+
+ PathElementAttributeRegexes = new Dictionary
+ {
+ [PathElementType.MoveTo] = new Regex($"{Sep}{MoveToAttributes}", RegexOptions.Compiled),
+ [PathElementType.Line] = new Regex($"{Sep}{LineAttributes}", RegexOptions.Compiled),
+ [PathElementType.HorizontalLine] = new Regex($"{Sep}{HorizontalLineAttributes}", RegexOptions.Compiled),
+ [PathElementType.VerticalLine] = new Regex($"{Sep}{VerticalLineAttributes}", RegexOptions.Compiled),
+ [PathElementType.QuadraticBezier] = new Regex($"{Sep}{QuadraticBezierAttributes}", RegexOptions.Compiled),
+ [PathElementType.SmoothQuadraticBezier] = new Regex($"{Sep}{SmoothQuadraticBezierAttributes}", RegexOptions.Compiled),
+ [PathElementType.CubicBezier] = new Regex($"{Sep}{CubicBezierAttributes}", RegexOptions.Compiled),
+ [PathElementType.SmoothCubicBezier] = new Regex($"{Sep}{SmoothCubicBezierAttributes}", RegexOptions.Compiled),
+ [PathElementType.Arc] = new Regex($"{Sep}{ArcAttributes}", RegexOptions.Compiled),
+
+ // Not Applicable for ClosePathElement as it has no attributes
+ [PathElementType.ClosePath] = null
+ };
+
+ BrushRegexes = new Dictionary
+ {
+ [BrushType.SolidColor] = new Regex(SolidColorBrushRegexString, RegexOptions.Compiled),
+ [BrushType.LinearGradient] = new Regex(LinearGradientRegexString, RegexOptions.Compiled),
+ [BrushType.LinearGradientHdr] = new Regex(LinearGradientHdrRegexString, RegexOptions.Compiled),
+ [BrushType.RadialGradient] = new Regex(RadialGradientRegexString, RegexOptions.Compiled),
+ [BrushType.RadialGradientHdr] = new Regex(RadialGradientHdrRegexString, RegexOptions.Compiled)
+ };
+
+ GradientStopAttributeRegexes = new Dictionary
+ {
+ [GradientStopAttributeType.Main] = new Regex(GradientStopMainAttributes, RegexOptions.Compiled),
+ [GradientStopAttributeType.Additional] = new Regex($"{Sep}{GradientStopAttributes}", RegexOptions.Compiled),
+ [GradientStopAttributeType.MainHdr] = new Regex(GradientStopHdrMainAttributes, RegexOptions.Compiled),
+ [GradientStopAttributeType.AdditionalHdr] = new Regex($"{Sep}{GradientStopHdrAttributes}", RegexOptions.Compiled)
+ };
+
+ ValidationRegex = new Regex(@"\s+");
+ CanvasGeometryRegex = new Regex(CanvasGeometryRegexString, RegexOptions.Compiled);
+ ColorRegex = new Regex(ColorRegexString, RegexOptions.Compiled);
+ CanvasBrushRegex = new Regex(CanvasBrushRegexString, RegexOptions.Compiled);
+ GradientStopRegex = new Regex(GradientStopRegexString, RegexOptions.Compiled);
+ GradientStopHdrRegex = new Regex(GradientStopHdrRegexString, RegexOptions.Compiled);
+ CanvasStrokeStyleRegex = new Regex(CanvasStrokeStyleRegexString, RegexOptions.Compiled);
+ CanvasStrokeRegex = new Regex(CanvasStrokeRegexString, RegexOptions.Compiled);
+ CustomDashAttributeRegex = new Regex($"{Sep}{CustomDashAttribute}", RegexOptions.Compiled);
+ }
+
+ ///
+ /// Get the Regex for the given PathFigureType
+ ///
+ /// PathFigureType
+ /// Regex
+ internal static Regex GetRegex(PathFigureType figureType)
+ {
+ return PathFigureRegexes[figureType];
+ }
+
+ ///
+ /// Get the Regex for the given PathElementType
+ ///
+ /// PathElementType
+ /// Regex
+ internal static Regex GetRegex(PathElementType elementType)
+ {
+ return PathElementRegexes[elementType];
+ }
+
+ ///
+ /// Get the Regex for extracting attributes of the given PathFigureType
+ ///
+ /// PathFigureType
+ /// Regex
+ internal static Regex GetAttributesRegex(PathFigureType figureType)
+ {
+ return PathFigureAttributeRegexes[figureType];
+ }
+
+ ///
+ /// Get the Regex for extracting attributes of the given PathElementType
+ ///
+ /// PathElementType
+ /// Regex
+ internal static Regex GetAttributesRegex(PathElementType elementType)
+ {
+ return PathElementAttributeRegexes[elementType];
+ }
+
+ ///
+ /// Gets the Regex for extracting the attributes of the given BrushType
+ ///
+ /// BrushType
+ /// Regex
+ internal static Regex GetAttributesRegex(BrushType brushType)
+ {
+ return BrushRegexes[brushType];
+ }
+
+ ///
+ /// Gets the Regex for extracting the attributes of the given
+ /// GradientStopAttributeType
+ ///
+ /// GradientStopAttributeType
+ /// Regex
+ internal static Regex GetAttributesRegex(GradientStopAttributeType gsAttrType)
+ {
+ return GradientStopAttributeRegexes[gsAttrType];
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CultureShield.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CultureShield.cs
new file mode 100644
index 00000000000..8dc577f7915
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/CultureShield.cs
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Globalization;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry
+{
+ ///
+ /// Class which can be used to encapsulate code statement(s) so that they are executed in a specific culture.
+ ///
+ /// Usage example:
+ ///
+ /// The following code block will be executed using the French culture.
+ ///
+ /// using (new CultureShield("fr-FR"))
+ ///
+ /// {
+ ///
+ /// ...
+ ///
+ /// }
+ ///
+ internal readonly ref struct CultureShield
+ {
+ private readonly CultureInfo _prevCulture;
+
+ ///
+ /// Initializes a new instance of the struct so that the encapsulated code statement(s) can be executed using the specified culture.
+ ///
+ /// Usage example:
+ ///
+ /// The following code block will be executed using the French culture.
+ ///
+ /// using (new CultureShield("fr-FR"))
+ ///
+ /// {
+ ///
+ /// ...
+ ///
+ /// }
+ ///
+ /// The culture in which the encapsulated code statement(s) are to be executed.
+ internal CultureShield(string culture)
+ {
+ _prevCulture = CultureInfo.CurrentCulture;
+ CultureInfo.CurrentCulture = new CultureInfo(culture);
+ }
+
+ ///
+ /// Disposes the CultureShield object.
+ ///
+ public void Dispose()
+ {
+ CultureInfo.CurrentCulture = _prevCulture;
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/AbstractCanvasBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/AbstractCanvasBrushElement.cs
new file mode 100644
index 00000000000..dd0e9c1f27f
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/AbstractCanvasBrushElement.cs
@@ -0,0 +1,72 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush
+{
+ ///
+ /// Abstract base class for all Brush Elements
+ ///
+ internal abstract class AbstractCanvasBrushElement : ICanvasBrushElement
+ {
+#pragma warning disable SA1401 // Fields should be private
+ protected float _opacity;
+#pragma warning restore SA1401 // Fields should be private
+
+ ///
+ /// Gets or sets the Brush data defining the Brush Element
+ ///
+ public string Data { get; protected set; }
+
+ ///
+ /// Gets or sets the number of non-whitespace characters in
+ /// the Brush Data
+ ///
+ public int ValidationCount { get; protected set; }
+
+ ///
+ /// Initializes the Brush Element with the given Capture
+ ///
+ /// Capture object
+ public virtual void Initialize(Capture capture)
+ {
+ Data = capture.Value;
+
+ var regex = GetAttributesRegex();
+ var match = regex.Match(Data);
+ if (!match.Success)
+ {
+ return;
+ }
+
+ GetAttributes(match);
+
+ // Get the number of non-whitespace characters in the data
+ ValidationCount = RegexFactory.ValidationRegex.Replace(Data, string.Empty).Length;
+ }
+
+ ///
+ /// Creates the ICanvasBrush from the parsed data
+ ///
+ /// ICanvasResourceCreator object
+ /// ICanvasBrush
+ public abstract ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator);
+
+ ///
+ /// Gets the Regex for extracting Brush Element Attributes
+ ///
+ /// Regex
+ protected abstract Regex GetAttributesRegex();
+
+ ///
+ /// Gets the Brush Element Attributes from the Match
+ ///
+ /// Match object
+ protected abstract void GetAttributes(Match match);
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/ICanvasBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/ICanvasBrushElement.cs
new file mode 100644
index 00000000000..0e6bcea4202
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/ICanvasBrushElement.cs
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush
+{
+ ///
+ /// Interface for a Brush Element
+ ///
+ internal interface ICanvasBrushElement
+ {
+ ///
+ /// Gets the Brush data defining the Brush Element
+ ///
+ string Data { get; }
+
+ ///
+ /// Gets the number of non-whitespace characters in
+ /// the Brush Data
+ ///
+ int ValidationCount { get; }
+
+ ///
+ /// Initializes the Brush Element with the given Capture
+ ///
+ /// Capture object
+ void Initialize(Capture capture);
+
+ ///
+ /// Creates the ICanvasBrush from the parsed data
+ ///
+ /// ICanvasResourceCreator object
+ /// ICanvasBrush
+ ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator);
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientBrushElement.cs
new file mode 100644
index 00000000000..412343ef16b
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientBrushElement.cs
@@ -0,0 +1,206 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers;
+using Windows.UI;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush
+{
+ ///
+ /// Represents a CanvasLinearGradientBrush with GradientStops
+ ///
+ internal sealed class LinearGradientBrushElement : AbstractCanvasBrushElement
+ {
+ private Vector2 _startPoint;
+ private Vector2 _endPoint;
+ private CanvasAlphaMode _alphaMode;
+ private CanvasBufferPrecision _bufferPrecision;
+ private CanvasEdgeBehavior _edgeBehavior;
+ private CanvasColorSpace _preInterpolationColorSpace;
+ private CanvasColorSpace _postInterpolationColorSpace;
+ private List _gradientStops;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Capture object
+ public LinearGradientBrushElement(Capture capture)
+ {
+ // Set the default values
+ _startPoint = Vector2.Zero;
+ _endPoint = Vector2.Zero;
+ _opacity = 1f;
+ _alphaMode = (CanvasAlphaMode)0;
+ _bufferPrecision = (CanvasBufferPrecision)0;
+ _edgeBehavior = (CanvasEdgeBehavior)0;
+
+ // Default ColorSpace is sRGB
+ _preInterpolationColorSpace = CanvasColorSpace.Srgb;
+ _postInterpolationColorSpace = CanvasColorSpace.Srgb;
+ _gradientStops = new List();
+
+ // Initialize
+ Initialize(capture);
+ }
+
+ ///
+ /// Creates the CanvasLinearGradientBrush from the parsed data
+ ///
+ /// ICanvasResourceCreator object
+ /// An instance of
+ public override ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator)
+ {
+ var brush = new CanvasLinearGradientBrush(
+ resourceCreator,
+ _gradientStops.ToArray(),
+ _edgeBehavior,
+ _alphaMode,
+ _preInterpolationColorSpace,
+ _postInterpolationColorSpace,
+ _bufferPrecision)
+ {
+ StartPoint = _startPoint,
+ EndPoint = _endPoint,
+ Opacity = _opacity
+ };
+
+ return brush;
+ }
+
+ ///
+ /// Gets the Regex for extracting Brush Element Attributes
+ ///
+ /// Regex
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(BrushType.LinearGradient);
+ }
+
+ ///
+ /// Gets the Brush Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ // Start Point
+ float.TryParse(match.Groups["StartX"].Value, out var startX);
+ float.TryParse(match.Groups["StartY"].Value, out var startY);
+ _startPoint = new Vector2(startX, startY);
+
+ // End Point
+ float.TryParse(match.Groups["EndX"].Value, out var endX);
+ float.TryParse(match.Groups["EndY"].Value, out var endY);
+ _endPoint = new Vector2(endX, endY);
+
+ // Opacity (optional)
+ var group = match.Groups["Opacity"];
+ if (group.Success)
+ {
+ float.TryParse(group.Value, out _opacity);
+ }
+
+ // Alpha Mode (optional)
+ group = match.Groups["AlphaMode"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _alphaMode);
+ }
+
+ // Buffer Precision (optional)
+ group = match.Groups["BufferPrecision"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _bufferPrecision);
+ }
+
+ // Edge Behavior (optional)
+ group = match.Groups["EdgeBehavior"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _edgeBehavior);
+ }
+
+ // Pre Interpolation ColorSpace (optional)
+ group = match.Groups["PreColorSpace"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _preInterpolationColorSpace);
+ }
+
+ // Post Interpolation ColorSpace (optional)
+ group = match.Groups["PostColorSpace"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _postInterpolationColorSpace);
+ }
+
+ // GradientStops
+ group = match.Groups["GradientStops"];
+ if (group.Success)
+ {
+ _gradientStops.Clear();
+ foreach (Capture capture in group.Captures)
+ {
+ var gradientMatch = RegexFactory.GradientStopRegex.Match(capture.Value);
+ if (!gradientMatch.Success)
+ {
+ continue;
+ }
+
+ float position;
+ Color color;
+
+ // Main Attributes
+ var main = gradientMatch.Groups["Main"];
+ if (main.Success)
+ {
+ var mainMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.Main).Match(main.Value);
+ float.TryParse(mainMatch.Groups["Position"].Value, out position);
+ color = ColorParser.Parse(mainMatch);
+
+ _gradientStops.Add(new CanvasGradientStop()
+ {
+ Color = color,
+ Position = position
+ });
+ }
+
+ // Additional Attributes
+ var additional = gradientMatch.Groups["Additional"];
+ if (!additional.Success)
+ {
+ continue;
+ }
+
+ foreach (Capture addCapture in additional.Captures)
+ {
+ var addMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.Additional).Match(addCapture.Value);
+ float.TryParse(addMatch.Groups["Position"].Value, out position);
+ color = ColorParser.Parse(addMatch);
+
+ _gradientStops.Add(new CanvasGradientStop()
+ {
+ Color = color,
+ Position = position
+ });
+ }
+ }
+
+ // Sort the stops based on their position
+ if (_gradientStops.Any())
+ {
+ _gradientStops = _gradientStops.OrderBy(g => g.Position).ToList();
+ }
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientHdrBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientHdrBrushElement.cs
new file mode 100644
index 00000000000..1a348439fb2
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/LinearGradientHdrBrushElement.cs
@@ -0,0 +1,212 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush
+{
+ ///
+ /// Represents a CanvasLinearGradientBrush with GradientStopHdrs
+ ///
+ internal sealed class LinearGradientHdrBrushElement : AbstractCanvasBrushElement
+ {
+ private Vector2 _startPoint;
+ private Vector2 _endPoint;
+ private CanvasAlphaMode _alphaMode;
+ private CanvasBufferPrecision _bufferPrecision;
+ private CanvasEdgeBehavior _edgeBehavior;
+ private CanvasColorSpace _preInterpolationColorSpace;
+ private CanvasColorSpace _postInterpolationColorSpace;
+ private List _gradientStopHdrs;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Capture object
+ public LinearGradientHdrBrushElement(Capture capture)
+ {
+ // Set the default values
+ _startPoint = Vector2.Zero;
+ _endPoint = Vector2.Zero;
+ _opacity = 1f;
+ _alphaMode = (CanvasAlphaMode)0;
+ _bufferPrecision = (CanvasBufferPrecision)0;
+ _edgeBehavior = (CanvasEdgeBehavior)0;
+
+ // Default ColorSpace is sRGB
+ _preInterpolationColorSpace = CanvasColorSpace.Srgb;
+ _postInterpolationColorSpace = CanvasColorSpace.Srgb;
+ _gradientStopHdrs = new List();
+
+ // Initialize
+ Initialize(capture);
+ }
+
+ ///
+ /// Creates the CanvasLinearGradientBrush from the parsed data
+ ///
+ /// ICanvasResourceCreator object
+ /// Instance of
+ public override ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator)
+ {
+ var brush = CanvasLinearGradientBrush.CreateHdr(
+ resourceCreator,
+ _gradientStopHdrs.ToArray(),
+ _edgeBehavior,
+ _alphaMode,
+ _preInterpolationColorSpace,
+ _postInterpolationColorSpace,
+ _bufferPrecision);
+
+ brush.StartPoint = _startPoint;
+ brush.EndPoint = _endPoint;
+ brush.Opacity = _opacity;
+
+ return brush;
+ }
+
+ ///
+ /// Gets the Regex for extracting Brush Element Attributes
+ ///
+ /// Regex
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(BrushType.LinearGradientHdr);
+ }
+
+ ///
+ /// Gets the Brush Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ // Start Point
+ float.TryParse(match.Groups["StartX"].Value, out var startX);
+ float.TryParse(match.Groups["StartY"].Value, out var startY);
+ _startPoint = new Vector2(startX, startY);
+
+ // End Point
+ float.TryParse(match.Groups["EndX"].Value, out var endX);
+ float.TryParse(match.Groups["EndY"].Value, out var endY);
+ _endPoint = new Vector2(endX, endY);
+
+ // Opacity (optional)
+ var group = match.Groups["Opacity"];
+ if (group.Success)
+ {
+ float.TryParse(group.Value, out _opacity);
+ }
+
+ // Alpha Mode (optional)
+ group = match.Groups["AlphaMode"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _alphaMode);
+ }
+
+ // Buffer Precision (optional)
+ group = match.Groups["BufferPrecision"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _bufferPrecision);
+ }
+
+ // Edge Behavior (optional)
+ group = match.Groups["EdgeBehavior"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _edgeBehavior);
+ }
+
+ // Pre Interpolation ColorSpace (optional)
+ group = match.Groups["PreColorSpace"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _preInterpolationColorSpace);
+ }
+
+ // Post Interpolation ColorSpace (optional)
+ group = match.Groups["PostColorSpace"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _postInterpolationColorSpace);
+ }
+
+ // GradientStopHdrs
+ group = match.Groups["GradientStops"];
+ if (group.Success)
+ {
+ _gradientStopHdrs.Clear();
+ foreach (Capture capture in group.Captures)
+ {
+ var gradientMatch = RegexFactory.GradientStopHdrRegex.Match(capture.Value);
+ if (!gradientMatch.Success)
+ {
+ continue;
+ }
+
+ // Main Attributes
+ var main = gradientMatch.Groups["Main"];
+ if (main.Success)
+ {
+ var mainMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.MainHdr)
+ .Match(main.Value);
+ float.TryParse(mainMatch.Groups["Position"].Value, out float position);
+ float.TryParse(mainMatch.Groups["X"].Value, out float x);
+ float.TryParse(mainMatch.Groups["Y"].Value, out float y);
+ float.TryParse(mainMatch.Groups["Z"].Value, out float z);
+ float.TryParse(mainMatch.Groups["W"].Value, out float w);
+
+ var color = new Vector4(x, y, z, w);
+
+ _gradientStopHdrs.Add(new CanvasGradientStopHdr()
+ {
+ Color = color,
+ Position = position
+ });
+ }
+
+ // Additional Attributes
+ var additional = gradientMatch.Groups["Additional"];
+ if (!additional.Success)
+ {
+ continue;
+ }
+
+ foreach (Capture addCapture in additional.Captures)
+ {
+ var addMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.AdditionalHdr)
+ .Match(addCapture.Value);
+ float.TryParse(addMatch.Groups["Position"].Value, out float position);
+ float.TryParse(addMatch.Groups["X"].Value, out float x);
+ float.TryParse(addMatch.Groups["Y"].Value, out float y);
+ float.TryParse(addMatch.Groups["Z"].Value, out float z);
+ float.TryParse(addMatch.Groups["W"].Value, out float w);
+
+ var color = new Vector4(x, y, z, w);
+
+ _gradientStopHdrs.Add(new CanvasGradientStopHdr()
+ {
+ Color = color,
+ Position = position
+ });
+ }
+ }
+
+ // Sort the stops based on their position
+ if (_gradientStopHdrs.Any())
+ {
+ _gradientStopHdrs = _gradientStopHdrs.OrderBy(g => g.Position).ToList();
+ }
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientBrushElement.cs
new file mode 100644
index 00000000000..9e47a089886
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientBrushElement.cs
@@ -0,0 +1,230 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers;
+using Windows.UI;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush
+{
+ ///
+ /// Represents a CanvasRadialGradientBrush with GradientStops
+ ///
+ internal sealed class RadialGradientBrushElement : AbstractCanvasBrushElement
+ {
+ private float _radiusX;
+ private float _radiusY;
+ private Vector2 _center;
+ private Vector2 _originOffset;
+ private CanvasAlphaMode _alphaMode;
+ private CanvasBufferPrecision _bufferPrecision;
+ private CanvasEdgeBehavior _edgeBehavior;
+ private CanvasColorSpace _preInterpolationColorSpace;
+ private CanvasColorSpace _postInterpolationColorSpace;
+ private List _gradientStops;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Capture object
+ public RadialGradientBrushElement(Capture capture)
+ {
+ // Set the default values
+ _radiusX = 0f;
+ _radiusY = 0f;
+ _center = Vector2.Zero;
+ _originOffset = Vector2.Zero;
+ _opacity = 1f;
+ _alphaMode = (CanvasAlphaMode)0;
+ _bufferPrecision = (CanvasBufferPrecision)0;
+ _edgeBehavior = (CanvasEdgeBehavior)0;
+
+ // Default ColorSpace is sRGB
+ _preInterpolationColorSpace = CanvasColorSpace.Srgb;
+ _postInterpolationColorSpace = CanvasColorSpace.Srgb;
+ _gradientStops = new List();
+
+ // Initialize
+ Initialize(capture);
+ }
+
+ ///
+ /// Creates the CanvasLinearGradientBrush from the parsed data
+ ///
+ /// ICanvasResourceCreator object
+ /// Instance of
+ public override ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator)
+ {
+ var brush = new CanvasRadialGradientBrush(
+ resourceCreator,
+ _gradientStops.ToArray(),
+ _edgeBehavior,
+ _alphaMode,
+ _preInterpolationColorSpace,
+ _postInterpolationColorSpace,
+ _bufferPrecision)
+ {
+ RadiusX = _radiusX,
+ RadiusY = _radiusY,
+ Center = _center,
+ OriginOffset = _originOffset,
+ Opacity = _opacity
+ };
+
+ return brush;
+ }
+
+ ///
+ /// Gets the Regex for extracting Brush Element Attributes
+ ///
+ /// Regex
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(BrushType.RadialGradient);
+ }
+
+ ///
+ /// Gets the Brush Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ // RadiusX
+ float.TryParse(match.Groups["RadiusX"].Value, out _radiusX);
+
+ // Sanitize by taking the absolute value
+ _radiusX = Math.Abs(_radiusX);
+
+ // RadiusY
+ float.TryParse(match.Groups["RadiusY"].Value, out _radiusY);
+
+ // Sanitize by taking the absolute value
+ _radiusY = Math.Abs(_radiusY);
+
+ // CenterX
+ float.TryParse(match.Groups["CenterX"].Value, out var centerX);
+
+ // CenterY
+ float.TryParse(match.Groups["CenterY"].Value, out var centerY);
+ _center = new Vector2(centerX, centerY);
+
+ // Opacity (optional)
+ var group = match.Groups["Opacity"];
+ if (group.Success)
+ {
+ float.TryParse(group.Value, out _opacity);
+ }
+
+ // Origin Offset (optional)
+ group = match.Groups["OriginOffset"];
+ if (group.Success)
+ {
+ float.TryParse(match.Groups["OffsetX"].Value, out var offsetX);
+ float.TryParse(match.Groups["OffsetY"].Value, out var offsetY);
+ _originOffset = new Vector2(offsetX, offsetY);
+ }
+
+ // Alpha Mode (optional)
+ group = match.Groups["AlphaMode"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _alphaMode);
+ }
+
+ // Buffer Precision (optional)
+ group = match.Groups["BufferPrecision"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _bufferPrecision);
+ }
+
+ // Edge Behavior (optional)
+ group = match.Groups["EdgeBehavior"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _edgeBehavior);
+ }
+
+ // Pre Interpolation ColorSpace (optional)
+ group = match.Groups["PreColorSpace"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _preInterpolationColorSpace);
+ }
+
+ // Post Interpolation ColorSpace (optional)
+ group = match.Groups["PostColorSpace"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _postInterpolationColorSpace);
+ }
+
+ // Gradient Stops
+ group = match.Groups["GradientStops"];
+ if (group.Success)
+ {
+ _gradientStops.Clear();
+ foreach (Capture capture in group.Captures)
+ {
+ var gradientMatch = RegexFactory.GradientStopRegex.Match(capture.Value);
+ if (!gradientMatch.Success)
+ {
+ continue;
+ }
+
+ float position;
+ Color color;
+
+ // Main Attributes
+ var main = gradientMatch.Groups["Main"];
+ if (main.Success)
+ {
+ var mainMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.Main).Match(main.Value);
+ float.TryParse(mainMatch.Groups["Position"].Value, out position);
+ color = ColorParser.Parse(mainMatch);
+
+ _gradientStops.Add(new CanvasGradientStop()
+ {
+ Color = color,
+ Position = position
+ });
+ }
+
+ // Additional Attributes
+ var additional = gradientMatch.Groups["Additional"];
+ if (!additional.Success)
+ {
+ continue;
+ }
+
+ foreach (Capture addCapture in additional.Captures)
+ {
+ var addMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.Additional).Match(addCapture.Value);
+ float.TryParse(addMatch.Groups["Position"].Value, out position);
+ color = ColorParser.Parse(addMatch);
+
+ _gradientStops.Add(new CanvasGradientStop()
+ {
+ Color = color,
+ Position = position
+ });
+ }
+ }
+
+ // Sort the stops based on their position
+ if (_gradientStops.Any())
+ {
+ _gradientStops = _gradientStops.OrderBy(g => g.Position).ToList();
+ }
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientHdrBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientHdrBrushElement.cs
new file mode 100644
index 00000000000..e1143d38eaf
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/RadialGradientHdrBrushElement.cs
@@ -0,0 +1,237 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush
+{
+ ///
+ /// Represents a CanvasRadialGradientBrush with GradientStopHdrs
+ ///
+ internal sealed class RadialGradientHdrBrushElement : AbstractCanvasBrushElement
+ {
+ private float _radiusX;
+ private float _radiusY;
+ private Vector2 _center;
+ private Vector2 _originOffset;
+ private CanvasAlphaMode _alphaMode;
+ private CanvasBufferPrecision _bufferPrecision;
+ private CanvasEdgeBehavior _edgeBehavior;
+ private CanvasColorSpace _preInterpolationColorSpace;
+ private CanvasColorSpace _postInterpolationColorSpace;
+ private List _gradientStopHdrs;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Capture object
+ public RadialGradientHdrBrushElement(Capture capture)
+ {
+ // Set the default values
+ _radiusX = 0f;
+ _radiusY = 0f;
+ _center = Vector2.Zero;
+ _originOffset = Vector2.Zero;
+ _opacity = 1f;
+ _alphaMode = (CanvasAlphaMode)0;
+ _bufferPrecision = (CanvasBufferPrecision)0;
+ _edgeBehavior = (CanvasEdgeBehavior)0;
+
+ // Default ColorSpace is sRGB
+ _preInterpolationColorSpace = CanvasColorSpace.Srgb;
+ _postInterpolationColorSpace = CanvasColorSpace.Srgb;
+ _gradientStopHdrs = new List();
+
+ // Initialize
+ Initialize(capture);
+ }
+
+ ///
+ /// Creates the CanvasLinearGradientBrush from the parsed data
+ ///
+ /// ICanvasResourceCreator object
+ /// Instance of
+ public override ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator)
+ {
+ var brush = CanvasRadialGradientBrush.CreateHdr(
+ resourceCreator,
+ _gradientStopHdrs.ToArray(),
+ _edgeBehavior,
+ _alphaMode,
+ _preInterpolationColorSpace,
+ _postInterpolationColorSpace,
+ _bufferPrecision);
+
+ brush.RadiusX = _radiusX;
+ brush.RadiusY = _radiusY;
+ brush.Center = _center;
+ brush.OriginOffset = _originOffset;
+ brush.Opacity = _opacity;
+
+ return brush;
+ }
+
+ ///
+ /// Gets the Regex for extracting Brush Element Attributes
+ ///
+ /// Regex
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(BrushType.RadialGradientHdr);
+ }
+
+ ///
+ /// Gets the Brush Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ // RadiusX
+ float.TryParse(match.Groups["RadiusX"].Value, out _radiusX);
+
+ // Sanitize by taking the absolute value
+ _radiusX = Math.Abs(_radiusX);
+
+ // RadiusY
+ float.TryParse(match.Groups["RadiusY"].Value, out _radiusY);
+
+ // Sanitize by taking the absolute value
+ _radiusY = Math.Abs(_radiusY);
+
+ // CenterX
+ float.TryParse(match.Groups["CenterX"].Value, out var centerX);
+
+ // CenterY
+ float.TryParse(match.Groups["CenterY"].Value, out var centerY);
+ _center = new Vector2(centerX, centerY);
+
+ // Opacity (optional)
+ var group = match.Groups["Opacity"];
+ if (group.Success)
+ {
+ float.TryParse(group.Value, out _opacity);
+ }
+
+ // Origin Offset (optional)
+ group = match.Groups["OriginOffset"];
+ if (group.Success)
+ {
+ float.TryParse(match.Groups["OffsetX"].Value, out var offsetX);
+ float.TryParse(match.Groups["OffsetY"].Value, out var offsetY);
+ _originOffset = new Vector2(offsetX, offsetY);
+ }
+
+ // Alpha Mode (optional)
+ group = match.Groups["AlphaMode"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _alphaMode);
+ }
+
+ // Buffer Precision (optional)
+ group = match.Groups["BufferPrecision"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _bufferPrecision);
+ }
+
+ // Edge Behavior (optional)
+ group = match.Groups["EdgeBehavior"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _edgeBehavior);
+ }
+
+ // Pre Interpolation ColorSpace (optional)
+ group = match.Groups["PreColorSpace"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _preInterpolationColorSpace);
+ }
+
+ // Post Interpolation ColorSpace (optional)
+ group = match.Groups["PostColorSpace"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out _postInterpolationColorSpace);
+ }
+
+ // GradientStopHdrs
+ group = match.Groups["GradientStops"];
+ if (group.Success)
+ {
+ _gradientStopHdrs.Clear();
+ foreach (Capture capture in group.Captures)
+ {
+ var gradientMatch = RegexFactory.GradientStopHdrRegex.Match(capture.Value);
+ if (!gradientMatch.Success)
+ {
+ continue;
+ }
+
+ float position;
+ float x = 0, y = 0, z = 0, w = 0;
+ var main = gradientMatch.Groups["Main"];
+ if (main.Success)
+ {
+ var mainMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.MainHdr)
+ .Match(main.Value);
+
+ float.TryParse(mainMatch.Groups["Position"].Value, out position);
+ float.TryParse(mainMatch.Groups["X"].Value, out x);
+ float.TryParse(mainMatch.Groups["Y"].Value, out y);
+ float.TryParse(mainMatch.Groups["Z"].Value, out z);
+ float.TryParse(mainMatch.Groups["W"].Value, out w);
+
+ var color = new Vector4(x, y, z, w);
+
+ _gradientStopHdrs.Add(new CanvasGradientStopHdr()
+ {
+ Color = color,
+ Position = position
+ });
+ }
+
+ var additional = gradientMatch.Groups["Additional"];
+ if (!additional.Success)
+ {
+ continue;
+ }
+
+ foreach (Capture addCapture in additional.Captures)
+ {
+ var addMatch = RegexFactory.GetAttributesRegex(GradientStopAttributeType.AdditionalHdr)
+ .Match(addCapture.Value);
+ float.TryParse(addMatch.Groups["Position"].Value, out position);
+ float.TryParse(addMatch.Groups["X"].Value, out x);
+ float.TryParse(addMatch.Groups["Y"].Value, out y);
+ float.TryParse(addMatch.Groups["Z"].Value, out z);
+ float.TryParse(addMatch.Groups["W"].Value, out w);
+
+ var color = new Vector4(x, y, z, w);
+
+ _gradientStopHdrs.Add(new CanvasGradientStopHdr()
+ {
+ Color = color,
+ Position = position
+ });
+ }
+ }
+
+ // Sort the stops based on their position
+ if (_gradientStopHdrs.Any())
+ {
+ _gradientStopHdrs = _gradientStopHdrs.OrderBy(g => g.Position).ToList();
+ }
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/SolidColorBrushElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/SolidColorBrushElement.cs
new file mode 100644
index 00000000000..7314835f7b9
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Brush/SolidColorBrushElement.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers;
+using Windows.UI;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush
+{
+ ///
+ /// Represents a CanvasSolidColorBrush
+ ///
+ internal sealed class SolidColorBrushElement : AbstractCanvasBrushElement
+ {
+ private Color _color;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Capture object
+ public SolidColorBrushElement(Capture capture)
+ {
+ // Set the default values
+ _color = Colors.Transparent;
+ _opacity = 1f;
+
+ Initialize(capture);
+ }
+
+ ///
+ /// Creates the ICanvasBrush from the parsed data
+ ///
+ /// ICanvasResourceCreator object
+ /// Instance of
+ public override ICanvasBrush CreateBrush(ICanvasResourceCreator resourceCreator)
+ {
+ return new CanvasSolidColorBrush(resourceCreator, _color)
+ {
+ Opacity = _opacity
+ };
+ }
+
+ ///
+ /// Gets the Regex for extracting Brush Element Attributes
+ ///
+ /// Regex
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(BrushType.SolidColor);
+ }
+
+ ///
+ /// Gets the Brush Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ // Parse the Color
+ _color = ColorParser.Parse(match);
+
+ // Opacity (optional)
+ var group = match.Groups["Opacity"];
+ if (group.Success)
+ {
+ float.TryParse(group.Value, out _opacity);
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/AbstractPathElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/AbstractPathElement.cs
new file mode 100644
index 00000000000..f6fb00c4f00
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/AbstractPathElement.cs
@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Abstract base class for all Path Elements
+ ///
+ internal abstract class AbstractPathElement : ICanvasPathElement
+ {
+ ///
+ /// Gets or sets index of the Path Element in the Path Data
+ ///
+ public int Index { get; protected set; } = -1;
+
+ ///
+ /// Gets or sets path data defining the Path Element
+ ///
+ public string Data { get; protected set; } = string.Empty;
+
+ ///
+ /// Gets or sets number of non-whitespace characters in
+ /// the Path Element Data
+ ///
+ public int ValidationCount { get; protected set; } = 0;
+
+ ///
+ /// Gets or sets a value indicating whether the path element contains
+ /// absolute or relative coordinates.
+ ///
+ public bool IsRelative { get; protected set; } = false;
+
+ ///
+ /// Initializes the Path Element with the given Match
+ ///
+ /// Match object
+ /// Index within the match
+ public virtual void Initialize(Match match, int index)
+ {
+ var main = match.Groups["Main"];
+ Index = index;
+ Data = main.Value;
+ var command = match.Groups["Command"].Value[0];
+ IsRelative = char.IsLower(command);
+
+ // Get the Path Element attributes
+ GetAttributes(match);
+
+ // Get the number of non-whitespace characters in the data
+ ValidationCount = RegexFactory.ValidationRegex.Replace(main.Value, string.Empty).Length;
+ }
+
+ ///
+ /// Initializes the Path Element with the given Capture
+ ///
+ /// Capture object
+ /// Index of the Path Element in the Path data.
+ /// Indicates whether the Path Element coordinates are
+ /// absolute or relative
+ public virtual void InitializeAdditional(Capture capture, int index, bool isRelative)
+ {
+ Index = index;
+ Data = capture.Value;
+ IsRelative = isRelative;
+
+ var match = GetAttributesRegex().Match(Data);
+ if (match.Captures.Count != 1)
+ {
+ return;
+ }
+
+ // Get the Path Element attributes
+ GetAttributes(match);
+
+ // Get the number of non-whitespace characters in the data
+ ValidationCount = RegexFactory.ValidationRegex.Replace(Data, string.Empty).Length;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public abstract Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement);
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected abstract Regex GetAttributesRegex();
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected abstract void GetAttributes(Match match);
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ArcElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ArcElement.cs
new file mode 100644
index 00000000000..9026d44f924
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ArcElement.cs
@@ -0,0 +1,101 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Arc Element in a Path Geometry
+ ///
+ internal class ArcElement : AbstractPathElement
+ {
+ private float _radiusX;
+ private float _radiusY;
+ private float _angle;
+ private CanvasArcSize _arcSize;
+ private CanvasSweepDirection _sweepDirection;
+ private float _x;
+ private float _y;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ArcElement()
+ {
+ _radiusX = _radiusY = _angle = 0;
+ _arcSize = CanvasArcSize.Small;
+ _sweepDirection = CanvasSweepDirection.Clockwise;
+ _sweepDirection = 0;
+ _x = _y = 0;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ var point = new Vector2(_x, _y);
+ if (IsRelative)
+ {
+ point += currentPoint;
+ }
+
+ // Execute command
+ pathBuilder.AddArc(point, _radiusX, _radiusY, _angle, _sweepDirection, _arcSize);
+
+ // Set Last Element
+ lastElement = this;
+
+ // Return current point
+ return point;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathElementType.Arc);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ float.TryParse(match.Groups["RadiusX"].Value, out _radiusX);
+
+ // Sanitize by taking only the absolute value
+ _radiusX = Math.Abs(_radiusX);
+ float.TryParse(match.Groups["RadiusY"].Value, out _radiusY);
+
+ // Sanitize by taking only the absolute value
+ _radiusY = Math.Abs(_radiusY);
+
+ // Angle is provided in degrees
+ float.TryParse(match.Groups["Angle"].Value, out _angle);
+
+ // Convert angle to radians as CanvasPathBuilder.AddArc() method
+ // requires the angle to be in radians
+ _angle *= Scalar.DegreesToRadians;
+ Enum.TryParse(match.Groups["IsLargeArc"].Value, out _arcSize);
+ Enum.TryParse(match.Groups["SweepDirection"].Value, out _sweepDirection);
+ float.TryParse(match.Groups["X"].Value, out _x);
+ float.TryParse(match.Groups["Y"].Value, out _y);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasEllipseFigure.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasEllipseFigure.cs
new file mode 100644
index 00000000000..7f07c806e40
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasEllipseFigure.cs
@@ -0,0 +1,83 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Ellipse Figure in a Path Geometry
+ ///
+ internal class CanvasEllipseFigure : AbstractPathElement
+ {
+ private float _radiusX;
+ private float _radiusY;
+ private float _x;
+ private float _y;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CanvasEllipseFigure()
+ {
+ _radiusX = _radiusY = _x = _y = 0;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the EllipseFigure
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the EllipseFigure
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ var center = new Vector2(_x, _y);
+ if (IsRelative)
+ {
+ center += currentPoint;
+ }
+
+ // Execute command
+ pathBuilder.AddEllipseFigure(center.X, center.Y, _radiusX, _radiusY);
+
+ // No need to update the lastElement or currentPoint here as we are creating
+ // a separate closed figure here. So current point will not change.
+ return currentPoint;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathFigureType.EllipseFigure);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ float.TryParse(match.Groups["RadiusX"].Value, out _radiusX);
+
+ // Sanitize by taking the absolute value
+ _radiusX = Math.Abs(_radiusX);
+ float.TryParse(match.Groups["RadiusY"].Value, out _radiusY);
+
+ // Sanitize by taking the absolute value
+ _radiusY = Math.Abs(_radiusY);
+ float.TryParse(match.Groups["X"].Value, out _x);
+ float.TryParse(match.Groups["Y"].Value, out _y);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPathFigure.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPathFigure.cs
new file mode 100644
index 00000000000..63df1371edc
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPathFigure.cs
@@ -0,0 +1,136 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class which contains a collection of ICanvasPathElements
+ /// which can be used to create CanvasGeometry.
+ ///
+ internal class CanvasPathFigure : AbstractPathElement
+ {
+ // Collection of Path Elements
+ private List _elements;
+
+ public CanvasPathFigure()
+ {
+ _elements = new List();
+ ValidationCount = 0;
+ }
+
+ ///
+ /// Initializes the Path Element with the given Match
+ ///
+ /// Match object
+ /// Index of the path element in the Path data.
+ public override void Initialize(Match match, int index)
+ {
+ Index = index;
+ var main = match.Groups["Main"];
+ Data = main.Value;
+
+ var elements = new List();
+ foreach (PathElementType type in Enum.GetValues(typeof(PathElementType)))
+ {
+ foreach (Capture elementCapture in match.Groups[type.ToString()].Captures)
+ {
+ var elementRootIndex = elementCapture.Index;
+ var regex = RegexFactory.GetRegex(type);
+ var elementMatch = regex.Match(elementCapture.Value);
+ var isRelative = false;
+
+ // Process the 'Main' Group which contains the Path Command and
+ // corresponding attributes
+ if (elementMatch.Groups["Main"].Captures.Count == 1)
+ {
+ var figure = PathElementFactory.CreatePathElement(type, elementMatch, elementRootIndex);
+ elements.Add(figure);
+ isRelative = figure.IsRelative;
+ }
+
+ // Process the 'Additional' Group which contains just the attributes
+ elements.AddRange(from Capture capture in elementMatch.Groups["Additional"].Captures
+ select PathElementFactory.CreateAdditionalPathElement(type, capture, elementRootIndex + capture.Index, isRelative));
+ }
+ }
+
+ // Sort the path elements based on their index value
+ _elements.AddRange(elements.OrderBy(e => e.Index));
+ if (_elements.Count <= 0)
+ {
+ return;
+ }
+
+ // Check if the last path element in the figure is an ClosePathElement
+ // which would indicate that the path needs to be closed. Otherwise,
+ // add a default ClosePathElement at the end to indicate that the path
+ // is not closed.
+ var lastElement = _elements.ElementAt(_elements.Count - 1);
+ if ((lastElement as ClosePathElement) == null)
+ {
+ _elements.Add(PathElementFactory.CreateDefaultPathElement(PathElementType.ClosePath));
+ }
+
+ // Validation Count will be the cumulative sum of the validation count
+ // of child elements of the PathFigure
+ ValidationCount = _elements.Sum(x => x.ValidationCount);
+ }
+
+ ///
+ /// Initializes the Path Element with the given Capture
+ ///
+ /// Capture object
+ /// Index of the Path Element in the Path data.
+ /// Indicates whether the Path Element coordinates are
+ /// absolute or relative
+ public override void InitializeAdditional(Capture capture, int index, bool isRelative)
+ {
+ // Do nothing as this scenario is not valid for CanvasPathFigure
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ foreach (var pathElement in _elements)
+ {
+ currentPoint = pathElement.CreatePath(pathBuilder, currentPoint, ref lastElement);
+ }
+
+ return currentPoint;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return null;
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ // Do nothing
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPolygonFigure.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPolygonFigure.cs
new file mode 100644
index 00000000000..806d1060385
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasPolygonFigure.cs
@@ -0,0 +1,85 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Polygon Figure in a Path Geometry
+ ///
+ internal class CanvasPolygonFigure : AbstractPathElement
+ {
+ private int _numSides;
+ private float _radius;
+ private float _x;
+ private float _y;
+
+ public CanvasPolygonFigure()
+ {
+ _numSides = 0;
+ _radius = _x = _y = 0;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the PolygonFigure
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the PolygonFigure
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ var center = new Vector2(_x, _y);
+ if (IsRelative)
+ {
+ center += currentPoint;
+ }
+
+ // Execute command
+ pathBuilder.AddPolygonFigure(_numSides, center.X, center.Y, _radius);
+
+ // No need to update the lastElement or currentPoint here as we are creating
+ // a separate closed figure here. So current point will not change.
+ return currentPoint;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathFigureType.PolygonFigure);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ // Sides
+ int.TryParse(match.Groups["Sides"].Value, out _numSides);
+
+ // Sanitize by taking the absolute value
+ _numSides = Math.Abs(_numSides);
+
+ // Radius
+ float.TryParse(match.Groups["Radius"].Value, out _radius);
+
+ // Sanitize by taking the absolute value
+ _radius = Math.Abs(_radius);
+
+ float.TryParse(match.Groups["X"].Value, out _x);
+ float.TryParse(match.Groups["Y"].Value, out _y);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRectangleFigure.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRectangleFigure.cs
new file mode 100644
index 00000000000..c691fee2602
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRectangleFigure.cs
@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Rectangle Figure in a Path Geometry
+ ///
+ internal sealed class CanvasRectangleFigure : AbstractPathElement
+ {
+ private float _x;
+ private float _y;
+ private float _width;
+ private float _height;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CanvasRectangleFigure()
+ {
+ _x = _y = _width = _height = 0f;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the PolygonFigure
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the PolygonFigure
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ var topLeft = new Vector2(_x, _y);
+ if (IsRelative)
+ {
+ topLeft += currentPoint;
+ }
+
+ // Execute command
+ pathBuilder.AddRectangleFigure(topLeft.X, topLeft.Y, _width, _height);
+
+ // No need to update the lastElement or currentPoint here as we are creating
+ // a separate closed figure here.So current point will not change.
+ return currentPoint;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathFigureType.RectangleFigure);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ // X
+ float.TryParse(match.Groups["X"].Value, out _x);
+
+ // Y
+ float.TryParse(match.Groups["Y"].Value, out _y);
+
+ // Width
+ float.TryParse(match.Groups["Width"].Value, out _width);
+
+ // Sanitize by taking the absolute value
+ _width = Math.Abs(_width);
+
+ // Height
+ float.TryParse(match.Groups["Height"].Value, out _height);
+
+ // Sanitize by taking the absolute value
+ _height = Math.Abs(_height);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRoundRectangleFigure.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRoundRectangleFigure.cs
new file mode 100644
index 00000000000..f9fa6027f8a
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CanvasRoundRectangleFigure.cs
@@ -0,0 +1,104 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the RoundRectangle Figure in a Path Geometry
+ ///
+ internal sealed class CanvasRoundRectangleFigure : AbstractPathElement
+ {
+ private float _x;
+ private float _y;
+ private float _width;
+ private float _height;
+ private float _radiusX;
+ private float _radiusY;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CanvasRoundRectangleFigure()
+ {
+ _x = _y = _width = _height = _radiusX = _radiusY = 0f;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the PolygonFigure
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the PolygonFigure
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ var topLeft = new Vector2(_x, _y);
+ if (IsRelative)
+ {
+ topLeft += currentPoint;
+ }
+
+ // Execute command
+ pathBuilder.AddRoundedRectangleFigure(topLeft.X, topLeft.Y, _width, _height, _radiusX, _radiusY);
+
+ // No need to update the lastElement or currentPoint here as we are creating
+ // a separate closed figure here.So current point will not change.
+ return currentPoint;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathFigureType.RoundedRectangleFigure);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ // X
+ float.TryParse(match.Groups["X"].Value, out _x);
+
+ // Y
+ float.TryParse(match.Groups["Y"].Value, out _y);
+
+ // Width
+ float.TryParse(match.Groups["Width"].Value, out _width);
+
+ // Sanitize by taking the absolute value
+ _width = Math.Abs(_width);
+
+ // Height
+ float.TryParse(match.Groups["Height"].Value, out _height);
+
+ // Sanitize by taking the absolute value
+ _height = Math.Abs(_height);
+
+ // RadiusX
+ float.TryParse(match.Groups["RadiusX"].Value, out _radiusX);
+
+ // Sanitize by taking the absolute value
+ _radiusX = Math.Abs(_radiusX);
+
+ // RadiusY
+ float.TryParse(match.Groups["RadiusY"].Value, out _radiusY);
+
+ // Sanitize by taking the absolute value
+ _radiusY = Math.Abs(_radiusY);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ClosePathElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ClosePathElement.cs
new file mode 100644
index 00000000000..61ffb6255bb
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ClosePathElement.cs
@@ -0,0 +1,101 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the ClosePath command in a Path Geometry
+ ///
+ internal class ClosePathElement : AbstractPathElement
+ {
+ private bool _isFigureClosed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ClosePathElement()
+ {
+ _isFigureClosed = false;
+ }
+
+ ///
+ /// Initializes the Path Element with the given Match
+ ///
+ /// Match object
+ /// Index of the Path Element in the Path data.
+ public override void Initialize(Match match, int index)
+ {
+ var main = match.Groups["Main"];
+ Index = index;
+ Data = main.Value;
+ var command = match.Groups["Command"];
+
+ // If the Command is captured, it means that 'Z' is provided and hence
+ // the figure must be closed
+ if (command.Captures.Count == 1)
+ {
+ _isFigureClosed = true;
+ }
+
+ // Get the number of non-whitespace characters in the data
+ ValidationCount = RegexFactory.ValidationRegex.Replace(main.Value, string.Empty).Length;
+ }
+
+ ///
+ /// Initializes the Path Element with the given Capture
+ ///
+ /// Capture object
+ /// Index of the Path Element in the Path data.
+ /// Indicates whether the Path Element coordinates are
+ /// absolute or relative
+ public override void InitializeAdditional(Capture capture, int index, bool isRelative)
+ {
+ // Do nothing as this scenario is not valid for this Path Element
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Execute command
+ pathBuilder.EndFigure(_isFigureClosed ? CanvasFigureLoop.Closed : CanvasFigureLoop.Open);
+
+ // Set Last Element
+ lastElement = this;
+
+ // Return current point
+ return currentPoint;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ // Attributes are not present
+ return null;
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ // Do Nothing
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CubicBezierElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CubicBezierElement.cs
new file mode 100644
index 00000000000..282d7740b22
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/CubicBezierElement.cs
@@ -0,0 +1,105 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Cubic Bezier Element in a Path Geometry
+ ///
+ internal class CubicBezierElement : AbstractPathElement
+ {
+ private float _x1;
+ private float _y1;
+ private float _x2;
+ private float _y2;
+ private float _x;
+ private float _y;
+
+ private Vector2 _absoluteControlPoint2;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CubicBezierElement()
+ {
+ _x1 = _y1 = 0;
+ _x2 = _y2 = 0;
+ _x = _y = 0;
+ _absoluteControlPoint2 = Vector2.Zero;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ var controlPoint1 = new Vector2(_x1, _y1);
+ var controlPoint2 = new Vector2(_x2, _y2);
+ var point = new Vector2(_x, _y);
+
+ if (IsRelative)
+ {
+ controlPoint1 += currentPoint;
+ controlPoint2 += currentPoint;
+ point += currentPoint;
+ }
+
+ // Save the second absolute control point so that it can be used by the following
+ // SmoothCubicBezierElement (if any)
+ _absoluteControlPoint2 = controlPoint2;
+
+ // Execute command
+ pathBuilder.AddCubicBezier(controlPoint1, controlPoint2, point);
+
+ // Set Last Element
+ lastElement = this;
+
+ // Return current point
+ return point;
+ }
+
+ ///
+ /// Gets the Second Control Point of this Cubic Bezier
+ ///
+ /// Vector2
+ public Vector2 GetControlPoint()
+ {
+ return _absoluteControlPoint2;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathElementType.CubicBezier);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ float.TryParse(match.Groups["X1"].Value, out _x1);
+ float.TryParse(match.Groups["Y1"].Value, out _y1);
+ float.TryParse(match.Groups["X2"].Value, out _x2);
+ float.TryParse(match.Groups["Y2"].Value, out _y2);
+ float.TryParse(match.Groups["X"].Value, out _x);
+ float.TryParse(match.Groups["Y"].Value, out _y);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/FillRuleElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/FillRuleElement.cs
new file mode 100644
index 00000000000..897219f3401
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/FillRuleElement.cs
@@ -0,0 +1,78 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Fill Rule Element in a Path Geometry
+ ///
+ internal class FillRuleElement : AbstractPathElement
+ {
+ private CanvasFilledRegionDetermination _fillValue;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FillRuleElement()
+ {
+ _fillValue = CanvasFilledRegionDetermination.Alternate;
+ }
+
+ ///
+ /// Initializes the Path Element with the given Capture
+ ///
+ /// Capture object
+ /// Index of the Path Element in the Path data.
+ /// Indicates whether the Path Element coordinates are
+ /// absolute or relative
+ public override void InitializeAdditional(Capture capture, int index, bool isRelative)
+ {
+ // Do nothing as this scenario is not valid for this Path Element
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Execute command
+ pathBuilder.SetFilledRegionDetermination(_fillValue);
+
+ // Set Last Element
+ lastElement = this;
+
+ // Return current point
+ return currentPoint;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ // Not applicable for this Path Element
+ return null;
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ Enum.TryParse(match.Groups["FillValue"].Value, out _fillValue);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/HorizontalLineElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/HorizontalLineElement.cs
new file mode 100644
index 00000000000..4b98fcc0d59
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/HorizontalLineElement.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Horizontal Line Element in a Path Geometry
+ ///
+ internal class HorizontalLineElement : AbstractPathElement
+ {
+ private float _x;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public HorizontalLineElement()
+ {
+ _x = 0;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ var point = IsRelative ?
+ new Vector2(currentPoint.X + _x, currentPoint.Y) : new Vector2(_x, currentPoint.Y);
+
+ // Execute command
+ pathBuilder.AddLine(point);
+
+ // Set Last Element
+ lastElement = this;
+
+ // Return current point
+ return point;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathElementType.HorizontalLine);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ float.TryParse(match.Groups["X"].Value, out _x);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ICanvasPathElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ICanvasPathElement.cs
new file mode 100644
index 00000000000..fe57f2e4a4a
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/ICanvasPathElement.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Interface for a Path Element which serves
+ /// as a building block for CanvasPathGeometry
+ ///
+ internal interface ICanvasPathElement
+ {
+ ///
+ /// Gets index of the Path Element in the Path Data
+ ///
+ int Index { get; }
+
+ ///
+ /// Gets path data defining the Path Element
+ ///
+ string Data { get; }
+
+ ///
+ /// Gets number of non-whitespace characters in
+ /// the Path Element Data
+ ///
+ int ValidationCount { get; }
+
+ ///
+ /// Gets a value indicating whether the path element contains
+ /// absolute or relative coordinates.
+ ///
+ bool IsRelative { get; }
+
+ ///
+ /// Initializes the Path Element with the given Match
+ ///
+ /// Match object
+ /// Index of the Path Element in the Path data.
+ void Initialize(Match match, int index);
+
+ ///
+ /// Initializes the Path Element with the given Capture
+ ///
+ /// Capture object
+ /// Index of the Path Element in the Path data.
+ /// Indicates whether the Path Element coordinates are
+ /// absolute or relative
+ void InitializeAdditional(Capture capture, int index, bool isRelative);
+
+ ///
+ /// Adds the Path Element to the PathBuilder.
+ ///
+ /// CanvasPathBuilder object
+ /// The current point on the path before the path element is added
+ /// The previous PathElement in the Path.
+ /// The current point on the path after the path element is added
+ Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement);
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/LineElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/LineElement.cs
new file mode 100644
index 00000000000..186b63f18cd
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/LineElement.cs
@@ -0,0 +1,74 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Line Element in a Path Geometry
+ ///
+ internal class LineElement : AbstractPathElement
+ {
+ private float _x;
+ private float _y;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public LineElement()
+ {
+ _x = _y = 0;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ var point = new Vector2(_x, _y);
+ if (IsRelative)
+ {
+ point += currentPoint;
+ }
+
+ // Execute command
+ pathBuilder.AddLine(point);
+
+ // Set Last Element
+ lastElement = this;
+
+ // Return current point
+ return point;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathElementType.Line);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ float.TryParse(match.Groups["X"].Value, out _x);
+ float.TryParse(match.Groups["Y"].Value, out _y);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/MoveToElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/MoveToElement.cs
new file mode 100644
index 00000000000..29d85724ecd
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/MoveToElement.cs
@@ -0,0 +1,87 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the MoveTo Command in a Path Geometry
+ ///
+ internal class MoveToElement : AbstractPathElement
+ {
+ private float _x;
+ private float _y;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MoveToElement()
+ {
+ _x = _y = 0;
+ }
+
+ ///
+ /// Initializes the Path Element with the given Capture
+ ///
+ /// Capture object
+ /// Index of the Path Element in the Path data.
+ /// Indicates whether the Path Element coordinates are
+ /// absolute or relative
+ public override void InitializeAdditional(Capture capture, int index, bool isRelative)
+ {
+ // Do nothing as this scenario is not valid for MoveTo Command
+ // Additional coordinates specified with MoveTo will be converted to Line Commands
+ }
+
+ ///
+ /// Adds the Path Element to the PathBuilder.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ var point = new Vector2(_x, _y);
+ if (IsRelative)
+ {
+ point += currentPoint;
+ }
+
+ // Execute command
+ pathBuilder.BeginFigure(point);
+
+ // Set Last Element
+ lastElement = this;
+
+ // Return current point
+ return point;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathElementType.MoveTo);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ float.TryParse(match.Groups["X"].Value, out _x);
+ float.TryParse(match.Groups["Y"].Value, out _y);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/QuadraticBezierElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/QuadraticBezierElement.cs
new file mode 100644
index 00000000000..d697610ae75
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/QuadraticBezierElement.cs
@@ -0,0 +1,97 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Quadratic Bezier Element in a Path Geometry
+ ///
+ internal class QuadraticBezierElement : AbstractPathElement
+ {
+ private float _x1;
+ private float _y1;
+ private float _x;
+ private float _y;
+ private Vector2 _absoluteControlPoint;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public QuadraticBezierElement()
+ {
+ _x1 = _y1 = 0;
+ _x = _y = 0;
+ _absoluteControlPoint = Vector2.Zero;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ var controlPoint = new Vector2(_x1, _y1);
+ var point = new Vector2(_x, _y);
+
+ if (IsRelative)
+ {
+ controlPoint += currentPoint;
+ point += currentPoint;
+ }
+
+ // Save the absolute control point so that it can be used by the following
+ // SmoothQuadraticBezierElement (if any)
+ _absoluteControlPoint = controlPoint;
+
+ // Execute command
+ pathBuilder.AddQuadraticBezier(controlPoint, point);
+
+ // Set Last Element
+ lastElement = this;
+
+ // Return current point
+ return point;
+ }
+
+ ///
+ /// Gets the Control Point of this Quadratic Bezier
+ ///
+ /// Vector2
+ public Vector2 GetControlPoint()
+ {
+ return _absoluteControlPoint;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathElementType.QuadraticBezier);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ float.TryParse(match.Groups["X1"].Value, out _x1);
+ float.TryParse(match.Groups["Y1"].Value, out _y1);
+ float.TryParse(match.Groups["X"].Value, out _x);
+ float.TryParse(match.Groups["Y"].Value, out _y);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothCubicBezierElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothCubicBezierElement.cs
new file mode 100644
index 00000000000..6c6c820966d
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothCubicBezierElement.cs
@@ -0,0 +1,117 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Smooth Cubic Bezier Element in a Path Geometry
+ ///
+ internal class SmoothCubicBezierElement : AbstractPathElement
+ {
+ private float _x2;
+ private float _y2;
+ private float _x;
+ private float _y;
+ private Vector2 _absoluteControlPoint2;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SmoothCubicBezierElement()
+ {
+ _x2 = _y2 = 0;
+ _x = _y = 0;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ // Check if the last element was a Cubic Bezier
+ Vector2 controlPoint1;
+ if (lastElement is CubicBezierElement cubicBezier)
+ {
+ // Reflect the second control point of the cubic bezier over the current point. The
+ // resulting point will be the first control point of this Bezier.
+ controlPoint1 = Utils.Reflect(cubicBezier.GetControlPoint(), currentPoint);
+ }
+
+ // Or if the last element was s Smooth Cubic Bezier
+ else
+ {
+ // If the last element was a Smooth Cubic Bezier then reflect its second control point
+ // over the current point. The resulting point will be the first control point of this
+ // Bezier. Otherwise, if the last element was not a Smooth Cubic Bezier then the
+ // currentPoint will be the first control point of this Bezier
+ controlPoint1 = lastElement is SmoothCubicBezierElement smoothCubicBezier
+ ? Utils.Reflect(smoothCubicBezier.GetControlPoint(), currentPoint)
+ : currentPoint;
+ }
+
+ var controlPoint2 = new Vector2(_x2, _y2);
+ var point = new Vector2(_x, _y);
+
+ if (IsRelative)
+ {
+ controlPoint2 += currentPoint;
+ point += currentPoint;
+ }
+
+ // Save the second absolute control point so that it can be used by the following
+ // SmoothCubicBezierElement (if any)
+ _absoluteControlPoint2 = controlPoint2;
+
+ // Execute command
+ pathBuilder.AddCubicBezier(controlPoint1, controlPoint2, point);
+
+ // Set Last Element
+ lastElement = this;
+
+ // Return current point
+ return point;
+ }
+
+ ///
+ /// Gets the Second Control Point of this Cubic Bezier
+ ///
+ /// Vector2
+ public Vector2 GetControlPoint()
+ {
+ return _absoluteControlPoint2;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathElementType.SmoothCubicBezier);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ float.TryParse(match.Groups["X2"].Value, out _x2);
+ float.TryParse(match.Groups["Y2"].Value, out _y2);
+ float.TryParse(match.Groups["X"].Value, out _x);
+ float.TryParse(match.Groups["Y"].Value, out _y);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothQuadraticBezierElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothQuadraticBezierElement.cs
new file mode 100644
index 00000000000..444a3262e14
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/SmoothQuadraticBezierElement.cs
@@ -0,0 +1,106 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Smooth Quadratic Bezier Element in a Path Geometry
+ ///
+ internal class SmoothQuadraticBezierElement : AbstractPathElement
+ {
+ private float _x;
+ private float _y;
+ private Vector2 _absoluteControlPoint;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SmoothQuadraticBezierElement()
+ {
+ _x = _y = 0;
+ _absoluteControlPoint = Vector2.Zero;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ // Check if the last element was a Quadratic Bezier
+ if (lastElement is QuadraticBezierElement quadBezier)
+ {
+ // Reflect the control point of the Quadratic Bezier over the current point. The
+ // resulting point will be the control point of this Bezier.
+ _absoluteControlPoint = Utils.Reflect(quadBezier.GetControlPoint(), currentPoint);
+ }
+
+ // Or if the last element was s Smooth Quadratic Bezier
+ else
+ {
+ // If the last element was a Smooth Quadratic Bezier then reflect its control point
+ // over the current point. The resulting point will be the control point of this
+ // Bezier. Otherwise, if the last element was not a Smooth Quadratic Bezier,
+ // then the currentPoint will be the control point of this Bezier.
+ _absoluteControlPoint = lastElement is SmoothQuadraticBezierElement smoothQuadBezier
+ ? Utils.Reflect(smoothQuadBezier.GetControlPoint(), currentPoint)
+ : currentPoint;
+ }
+
+ var point = new Vector2(_x, _y);
+
+ if (IsRelative)
+ {
+ point += currentPoint;
+ }
+
+ // Execute command
+ pathBuilder.AddQuadraticBezier(_absoluteControlPoint, point);
+
+ // Set Last Element
+ lastElement = this;
+
+ // Return current point
+ return point;
+ }
+
+ ///
+ /// Gets the Control Point of this Quadratic Bezier
+ ///
+ /// Vector2
+ public Vector2 GetControlPoint()
+ {
+ return _absoluteControlPoint;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathElementType.SmoothQuadraticBezier);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ float.TryParse(match.Groups["X"].Value, out _x);
+ float.TryParse(match.Groups["Y"].Value, out _y);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/VerticalLineElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/VerticalLineElement.cs
new file mode 100644
index 00000000000..2b06607c4c7
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Path/VerticalLineElement.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path
+{
+ ///
+ /// Class representing the Vertical Line Element in a Path Geometry
+ ///
+ internal class VerticalLineElement : AbstractPathElement
+ {
+ private float _y;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public VerticalLineElement()
+ {
+ _y = 0;
+ }
+
+ ///
+ /// Adds the Path Element to the Path.
+ ///
+ /// CanvasPathBuilder object
+ /// The last active location in the Path before adding
+ /// the Path Element
+ /// The previous PathElement in the Path.
+ /// The latest location in the Path after adding the Path Element
+ public override Vector2 CreatePath(CanvasPathBuilder pathBuilder, Vector2 currentPoint, ref ICanvasPathElement lastElement)
+ {
+ // Calculate coordinates
+ var point = IsRelative ?
+ new Vector2(currentPoint.X, currentPoint.Y + _y) : new Vector2(currentPoint.X, _y);
+
+ // Execute command
+ pathBuilder.AddLine(point);
+
+ // Set Last Element
+ lastElement = this;
+
+ // Return current point
+ return point;
+ }
+
+ ///
+ /// Get the Regex for extracting Path Element Attributes
+ ///
+ /// Instance of
+ protected override Regex GetAttributesRegex()
+ {
+ return RegexFactory.GetAttributesRegex(PathElementType.VerticalLine);
+ }
+
+ ///
+ /// Gets the Path Element Attributes from the Match
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ float.TryParse(match.Groups["Y"].Value, out _y);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/AbstractCanvasStrokeElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/AbstractCanvasStrokeElement.cs
new file mode 100644
index 00000000000..ce2e8f4f2c6
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/AbstractCanvasStrokeElement.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke
+{
+ ///
+ /// Abstract base class for Stroke Element.
+ ///
+ internal abstract class AbstractCanvasStrokeElement : ICanvasStrokeElement
+ {
+ ///
+ /// Gets or sets the Stroke data defining the Brush Element
+ ///
+ public string Data { get; protected set; }
+
+ ///
+ /// Gets or sets the number of non-whitespace characters in the Stroke Data.
+ ///
+ public int ValidationCount { get; protected set; }
+
+ ///
+ /// Initializes the Stroke Element with the given Capture.
+ ///
+ /// Match object
+ public virtual void Initialize(Match match)
+ {
+ Data = match.Value;
+
+ if (!match.Success)
+ {
+ return;
+ }
+
+ GetAttributes(match);
+
+ // Update the validation count
+ Validate();
+ }
+
+ ///
+ /// Gets the number of non-whitespace characters in the data.
+ ///
+ protected virtual void Validate()
+ {
+ ValidationCount = RegexFactory.ValidationRegex.Replace(Data, string.Empty).Length;
+ }
+
+ ///
+ /// Creates the ICanvasStroke from the parsed data.
+ ///
+ /// ICanvasStroke
+ public abstract ICanvasStroke CreateStroke(ICanvasResourceCreator resourceCreator);
+
+ ///
+ /// Gets the Stroke Element Attributes from the Match.
+ ///
+ /// Match object
+ protected abstract void GetAttributes(Match match);
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeElement.cs
new file mode 100644
index 00000000000..419dc1f4269
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeElement.cs
@@ -0,0 +1,105 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke
+{
+ ///
+ /// Represents a Stroke Element.
+ ///
+ internal sealed class CanvasStrokeElement : AbstractCanvasStrokeElement
+ {
+ private float _width;
+ private ICanvasBrushElement _brush;
+ private ICanvasStrokeStyleElement _style;
+ private int _widthValidationCount;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Match object
+ public CanvasStrokeElement(Match match)
+ {
+ _width = 1f;
+ _brush = null;
+ _style = null;
+ _widthValidationCount = 0;
+
+ Initialize(match);
+ }
+
+ ///
+ /// Creates the ICanvasStroke from the parsed data.
+ ///
+ /// ICanvasStroke
+ public override ICanvasStroke CreateStroke(ICanvasResourceCreator resourceCreator)
+ {
+ return new CanvasStroke(_brush.CreateBrush(resourceCreator), _width, _style.Style);
+ }
+
+ ///
+ /// Gets the Stroke Element Attributes from the Match.
+ ///
+ /// Match object
+ protected override void GetAttributes(Match match)
+ {
+ // Stroke Width
+ var group = match.Groups["StrokeWidth"];
+ float.TryParse(group.Value, out _width);
+
+ // Sanitize by taking the absolute value
+ _width = Math.Abs(_width);
+
+ _widthValidationCount = RegexFactory.ValidationRegex.Replace(group.Value, string.Empty).Length;
+
+ // Stroke Brush
+ group = match.Groups["CanvasBrush"];
+ if (group.Success)
+ {
+ _brush = CanvasBrushParser.Parse(group.Value);
+ }
+
+ // If the ICanvasBrushElement was not created, then the ICanvasStroke cannot be created
+ if (_brush == null)
+ {
+ static void Throw(string value) => throw new ArgumentException($"Unable to create a valid ICanvasBrush for the ICanvasStroke with the following Brush data - '{value}'.");
+
+ Throw(group.Value);
+ }
+
+ // Stroke Style
+ _style = CanvasStrokeStyleParser.Parse(match);
+ }
+
+ ///
+ /// Gets the number of non-whitespace characters in the data.
+ ///
+ protected override void Validate()
+ {
+ // Add 2 to the Validation Count to include the stroke command 'ST'
+ ValidationCount += 2;
+
+ // StrokeWidth Validation Count
+ ValidationCount += _widthValidationCount;
+
+ // Stroke Brush Validation Count
+ if (_brush != null)
+ {
+ ValidationCount += _brush.ValidationCount;
+ }
+
+ // Stroke Style Validation Count
+ if (_style != null)
+ {
+ ValidationCount += _style.ValidationCount;
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeStyleElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeStyleElement.cs
new file mode 100644
index 00000000000..79e2d396d15
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/CanvasStrokeStyleElement.cs
@@ -0,0 +1,182 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke
+{
+ ///
+ /// Represents a CanvasStrokeStyle Element.
+ ///
+ internal sealed class CanvasStrokeStyleElement : ICanvasStrokeStyleElement
+ {
+ ///
+ /// Gets the Stroke data defining the Brush Element
+ ///
+ public string Data { get; private set; }
+
+ ///
+ /// Gets the number of non-whitespace characters in
+ /// the Stroke Data
+ ///
+ public int ValidationCount { get; private set; }
+
+ ///
+ /// Gets the CanvasStrokeStyle obtained by parsing
+ /// the style data.
+ ///
+ public CanvasStrokeStyle Style { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The matching data
+ public CanvasStrokeStyleElement(Match match)
+ {
+ Data = match.Groups["CanvasStrokeStyle"].Value;
+
+ Style = new CanvasStrokeStyle();
+
+ Initialize(match);
+
+ // Get the number of non-whitespace characters in the data
+ ValidationCount = RegexFactory.ValidationRegex.Replace(Data, string.Empty).Length;
+ }
+
+ ///
+ /// Initializes the Stroke Element with the given Capture.
+ ///
+ /// Match object
+ public void Initialize(Match match)
+ {
+ var group = match.Groups["CanvasStrokeStyle"];
+ if (group.Success)
+ {
+ // DashStyle
+ group = match.Groups["DashStyle"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out CanvasDashStyle dashStyle);
+ Style.DashStyle = dashStyle;
+ }
+
+ // LineJoin
+ group = match.Groups["LineJoin"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out CanvasLineJoin lineJoin);
+ Style.LineJoin = lineJoin;
+ }
+
+ // MiterLimit
+ group = match.Groups["MiterLimit"];
+ if (group.Success)
+ {
+ float.TryParse(group.Value, out var miterLimit);
+
+ // Sanitize by taking the absolute value
+ Style.MiterLimit = Math.Abs(miterLimit);
+ }
+
+ // DashOffset
+ group = match.Groups["DashOffset"];
+ if (group.Success)
+ {
+ float.TryParse(group.Value, out var dashOffset);
+ Style.DashOffset = dashOffset;
+ }
+
+ // StartCap
+ group = match.Groups["StartCap"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out CanvasCapStyle capStyle);
+ Style.StartCap = capStyle;
+ }
+
+ // EndCap
+ group = match.Groups["EndCap"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out CanvasCapStyle capStyle);
+ Style.EndCap = capStyle;
+ }
+
+ // DashCap
+ group = match.Groups["DashCap"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out CanvasCapStyle capStyle);
+ Style.DashCap = capStyle;
+ }
+
+ // TransformBehavior
+ group = match.Groups["TransformBehavior"];
+ if (group.Success)
+ {
+ Enum.TryParse(group.Value, out CanvasStrokeTransformBehavior transformBehavior);
+ Style.TransformBehavior = transformBehavior;
+ }
+
+ // CustomDashStyle
+ group = match.Groups["CustomDashStyle"];
+ if (group.Success)
+ {
+ List dashes = new List();
+ group = match.Groups["Main"];
+ if (group.Success)
+ {
+ if (float.TryParse(match.Groups["DashSize"].Value, out var dashSize))
+ {
+ // Sanitize by taking the absolute value
+ dashes.Add(Math.Abs(dashSize));
+ }
+
+ if (float.TryParse(match.Groups["SpaceSize"].Value, out var spaceSize))
+ {
+ // Sanitize by taking the absolute value
+ dashes.Add(Math.Abs(spaceSize));
+ }
+ }
+
+ group = match.Groups["Additional"];
+ if (group.Success)
+ {
+ foreach (Capture capture in group.Captures)
+ {
+ var dashMatch = RegexFactory.CustomDashAttributeRegex.Match(capture.Value);
+ if (!dashMatch.Success)
+ {
+ continue;
+ }
+
+ if (float.TryParse(dashMatch.Groups["DashSize"].Value, out var dashSize))
+ {
+ // Sanitize by taking the absolute value
+ dashes.Add(Math.Abs(dashSize));
+ }
+
+ if (float.TryParse(dashMatch.Groups["SpaceSize"].Value, out var spaceSize))
+ {
+ // Sanitize by taking the absolute value
+ dashes.Add(Math.Abs(spaceSize));
+ }
+ }
+ }
+
+ // Any valid dashes?
+ if (dashes.Any())
+ {
+ Style.CustomDashStyle = dashes.ToArray();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeElement.cs
new file mode 100644
index 00000000000..4cec2b20473
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeElement.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke
+{
+ ///
+ /// Interface for Stroke Element
+ ///
+ internal interface ICanvasStrokeElement
+ {
+ ///
+ /// Gets the Stroke data defining the Brush Element.
+ ///
+ string Data { get; }
+
+ ///
+ /// Gets the number of non-whitespace characters in the Stroke Data.
+ ///
+ int ValidationCount { get; }
+
+ ///
+ /// Initializes the Stroke Element with the given Capture.
+ ///
+ /// Match object
+ void Initialize(Match match);
+
+ ///
+ /// Creates the ICanvasStroke from the parsed data.
+ ///
+ /// ICanvasStroke
+ ICanvasStroke CreateStroke(ICanvasResourceCreator resourceCreator);
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeStyleElement.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeStyleElement.cs
new file mode 100644
index 00000000000..5a7fee6e268
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Elements/Stroke/ICanvasStrokeStyleElement.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke
+{
+ ///
+ /// Interface for the CanvasStrokeStyle Element
+ ///
+ internal interface ICanvasStrokeStyleElement
+ {
+ ///
+ /// Gets the Stroke data defining the Brush Element.
+ ///
+ string Data { get; }
+
+ ///
+ /// Gets the number of non-whitespace characters in the Stroke Data.
+ ///
+ int ValidationCount { get; }
+
+ ///
+ /// Gets the CanvasStrokeStyle obtained by parsing the style data.
+ ///
+ CanvasStrokeStyle Style { get; }
+
+ ///
+ /// Initializes the Stroke Element with the given Capture.
+ ///
+ /// Match object
+ void Initialize(Match match);
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/ICanvasStroke.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/ICanvasStroke.cs
new file mode 100644
index 00000000000..e4209e85dcf
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/ICanvasStroke.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Graphics.Canvas.Geometry;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry
+{
+ ///
+ /// Interface to represent the Stroke which can be used to render an outline on a .
+ ///
+ public interface ICanvasStroke
+ {
+ ///
+ /// Gets or sets the brush with which the will be rendered.
+ ///
+ ICanvasBrush Brush { get; set; }
+
+ ///
+ /// Gets or sets the width of the .
+ ///
+ float Width { get; set; }
+
+ ///
+ /// Gets or sets the Style of the .
+ ///
+ CanvasStrokeStyle Style { get; set; }
+
+ ///
+ /// Gets or sets transform matrix of the brush.
+ ///
+ Matrix3x2 Transform { get; set; }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasBrushParser.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasBrushParser.cs
new file mode 100644
index 00000000000..6f63b4cb518
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasBrushParser.cs
@@ -0,0 +1,122 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Runtime.CompilerServices;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Brush;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers
+{
+ ///
+ /// Parser for ICanvasBrush.
+ ///
+ internal static class CanvasBrushParser
+ {
+ ///
+ /// Parses the Brush data string and converts it into ICanvasBrushElement.
+ ///
+ /// Brush data
+ /// ICanvasBrushElement
+ internal static ICanvasBrushElement Parse(string brushData)
+ {
+ var matches = RegexFactory.CanvasBrushRegex.Matches(brushData);
+
+ // If no match is found or no captures in the match, then it means that the brush data is invalid.
+ if (matches.Count == 0)
+ {
+ ThrowForZeroCount();
+ }
+
+ // If the match contains more than one captures, it means that there are multiple brushes present in the brush data.
+ // There should be only one brush defined in the brush data.
+ if (matches.Count > 1)
+ {
+ ThrowForNotOneCount();
+ }
+
+ // There should be only one match
+ var match = matches[0];
+ AbstractCanvasBrushElement brushElement = null;
+ if (match.Groups["SolidColorBrush"].Success)
+ {
+ brushElement = new SolidColorBrushElement(match.Groups["SolidColorBrush"].Captures[0]);
+ }
+ else if (match.Groups["LinearGradient"].Success)
+ {
+ brushElement = new LinearGradientBrushElement(match.Groups["LinearGradient"].Captures[0]);
+ }
+ else if (match.Groups["LinearGradientHdr"].Success)
+ {
+ brushElement = new LinearGradientHdrBrushElement(match.Groups["LinearGradientHdr"].Captures[0]);
+ }
+ else if (match.Groups["RadialGradient"].Success)
+ {
+ brushElement = new RadialGradientBrushElement(match.Groups["RadialGradient"].Captures[0]);
+ }
+ else if (match.Groups["RadialGradientHdr"].Success)
+ {
+ brushElement = new RadialGradientHdrBrushElement(match.Groups["RadialGradientHdr"].Captures[0]);
+ }
+
+ if (brushElement == null)
+ {
+ return null;
+ }
+
+ // Perform validation to check if there are any invalid characters in the brush data that were not captured
+ var preValidationCount = RegexFactory.ValidationRegex.Replace(brushData, string.Empty).Length;
+ var postValidationCount = brushElement.ValidationCount;
+
+ // If there are invalid characters, extract them and add them to the ArgumentException message
+ if (preValidationCount != postValidationCount)
+ {
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static void ThrowForInvalidCharacters(AbstractCanvasBrushElement brushElement, string brushData)
+ {
+ var parseIndex = 0;
+ if (!string.IsNullOrWhiteSpace(brushElement.Data))
+ {
+ parseIndex = brushData.IndexOf(brushElement.Data, parseIndex, StringComparison.Ordinal);
+ }
+
+ var errorString = brushData.Substring(parseIndex);
+ if (errorString.Length > 30)
+ {
+ errorString = $"{errorString.Substring(0, 30)}...";
+ }
+
+ throw new ArgumentException($"BRUSH_ERR003:Brush data contains invalid characters!\nIndex: {parseIndex}\n{errorString}");
+ }
+
+ ThrowForInvalidCharacters(brushElement, brushData);
+ }
+
+ return brushElement;
+
+ static void ThrowForZeroCount() => throw new ArgumentException("BRUSH_ERR001:Invalid Brush data! No matching brush data found!");
+ static void ThrowForNotOneCount() => throw new ArgumentException("BRUSH_ERR002:Multiple Brushes defined in Brush Data! " +
+ "There should be only one Brush definition within the Brush Data. " +
+ "You can either remove Brush definitions or split the Brush Data " +
+ "into multiple Brush Data and call the CanvasPathGeometry.CreateBrush() method on each of them.");
+ }
+
+ ///
+ /// Parses the Brush data string and converts it into ICanvasBrush.
+ ///
+ /// ICanvasResourceCreator
+ /// Brush data string
+ /// ICanvasBrush
+ internal static ICanvasBrush Parse(ICanvasResourceCreator resourceCreator, string brushData)
+ {
+ // Parse the brush data to get the ICanvasBrushElement
+ var brushElement = Parse(brushData);
+
+ // Create ICanvasBrush from the brushElement
+ return brushElement.CreateBrush(resourceCreator);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasGeometryParser.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasGeometryParser.cs
new file mode 100644
index 00000000000..df11070fedd
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasGeometryParser.cs
@@ -0,0 +1,137 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Path;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers
+{
+ ///
+ /// Parser for CanvasGeometry.
+ ///
+ internal static class CanvasGeometryParser
+ {
+ ///
+ /// Parses the Path data in string format and converts it to .
+ ///
+ /// ICanvasResourceCreator
+ /// Path data
+ ///
+ internal static CanvasGeometry Parse(ICanvasResourceCreator resourceCreator, string pathData)
+ {
+ var pathFigures = new List();
+
+ var matches = RegexFactory.CanvasGeometryRegex.Matches(pathData);
+
+ // If no match is found or no captures in the match, then it means // that the path data is invalid.
+ if (matches.Count == 0)
+ {
+ ThrowForZeroCount();
+ }
+
+ // If the match contains more than one captures, it means that there are multiple FillRuleElements present in the path data.
+ // There can be only one FillRuleElement in the path data (at the beginning).
+ if (matches.Count > 1)
+ {
+ ThrowForNotOneCount();
+ }
+
+ var figures = new List();
+
+ foreach (PathFigureType type in Enum.GetValues(typeof(PathFigureType)))
+ {
+ foreach (Capture figureCapture in matches[0].Groups[type.ToString()].Captures)
+ {
+ var figureRootIndex = figureCapture.Index;
+ var regex = RegexFactory.GetRegex(type);
+ var figureMatch = regex.Match(figureCapture.Value);
+ if (!figureMatch.Success)
+ {
+ continue;
+ }
+
+ // Process the 'Main' Group which contains the Path Command and
+ // corresponding attributes
+ var figure = PathElementFactory.CreatePathFigure(type, figureMatch, figureRootIndex);
+ figures.Add(figure);
+
+ // Process the 'Additional' Group which contains just the attributes
+ figures.AddRange(from Capture capture in figureMatch.Groups["Additional"].Captures
+ select PathElementFactory.CreateAdditionalPathFigure(type, capture, figureRootIndex + capture.Index, figure.IsRelative));
+ }
+ }
+
+ // Sort the figures by their indices
+ pathFigures.AddRange(figures.OrderBy(f => f.Index));
+ if (pathFigures.Count > 0)
+ {
+ // Check if the first element in the _figures list is a FillRuleElement
+ // which would indicate the fill rule to be followed while creating the
+ // path. If it is not present, then insert a default FillRuleElement at
+ // the beginning.
+ if ((pathFigures.ElementAt(0) as FillRuleElement) == null)
+ {
+ pathFigures.Insert(0, PathElementFactory.CreateDefaultPathElement(PathFigureType.FillRule));
+ }
+ }
+ else
+ {
+ return null;
+ }
+
+ // Perform validation to check if there are any invalid characters in the path data that were not captured
+ var preValidationCount = RegexFactory.ValidationRegex.Replace(pathData, string.Empty).Length;
+ var postValidationCount = pathFigures.Sum(x => x.ValidationCount);
+
+ // If there are invalid characters, extract them and add them to the ArgumentException message
+ if (preValidationCount != postValidationCount)
+ {
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static void ThrowForInvalidCharacters(List pathFigures, string pathData)
+ {
+ var parseIndex = 0;
+ foreach (var figure in pathFigures)
+ {
+ parseIndex = pathData.IndexOf(figure.Data, parseIndex, StringComparison.Ordinal) + figure.Data.Length;
+ }
+
+ var errorString = pathData.Substring(parseIndex);
+ if (errorString.Length > 30)
+ {
+ errorString = $"{errorString.Substring(0, 30)}...";
+ }
+
+ throw new ArgumentException($"PATH_ERR003:Path data contains invalid characters!\nIndex: {parseIndex}\n{errorString}");
+ }
+
+ ThrowForInvalidCharacters(pathFigures, pathData);
+ }
+
+ ICanvasPathElement lastElement = null;
+ var currentPoint = Vector2.Zero;
+
+ using var pathBuilder = new CanvasPathBuilder(resourceCreator);
+ foreach (var pathFigure in pathFigures)
+ {
+ currentPoint = pathFigure.CreatePath(pathBuilder, currentPoint, ref lastElement);
+ }
+
+ return CanvasGeometry.CreatePath(pathBuilder);
+
+ static void ThrowForZeroCount() => throw new ArgumentException("PATH_ERR000:Invalid Path data! No matching path data found!");
+ static void ThrowForNotOneCount() => throw new ArgumentException("PATH_ERR001:Multiple FillRule elements present in Path Data!\n" +
+ "There should be only one FillRule within the Path Data. " +
+ "You can either remove additional FillRule elements or split the Path Data " +
+ "into multiple Path Data and call the CanvasPathGeometry.CreateGeometry() method on each of them.");
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeParser.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeParser.cs
new file mode 100644
index 00000000000..9f91969fb27
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeParser.cs
@@ -0,0 +1,98 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Runtime.CompilerServices;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers
+{
+ ///
+ /// Parser for CanvasStroke
+ ///
+ internal static class CanvasStrokeParser
+ {
+ ///
+ /// Parses the Stroke Data string and converts it into ICanvasStrokeElement.
+ ///
+ /// Stroke Data string
+ /// ICanvasStrokeElement
+ internal static ICanvasStrokeElement Parse(string strokeData)
+ {
+ var matches = RegexFactory.CanvasStrokeRegex.Matches(strokeData);
+
+ // If no match is found or no captures in the match, then it means
+ // that the stroke data is invalid.
+ if (matches.Count == 0)
+ {
+ ThrowForZeroCount();
+ }
+
+ // If the match contains more than one captures, it means that there
+ // are multiple CanvasStrokes present in the stroke data. There should
+ // be only one CanvasStroke defined in the stroke data.
+ if (matches.Count > 1)
+ {
+ ThrowForNotOneCount();
+ }
+
+ // There should be only one match
+ var match = matches[0];
+ var strokeElement = new CanvasStrokeElement(match);
+
+ // Perform validation to check if there are any invalid characters in the stroke data that were not captured
+ var preValidationCount = RegexFactory.ValidationRegex.Replace(strokeData, string.Empty).Length;
+ var postValidationCount = strokeElement.ValidationCount;
+
+ // If there are invalid characters, extract them and add them to the ArgumentException message
+ if (preValidationCount != postValidationCount)
+ {
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static void ThrowForInvalidCharacters(CanvasStrokeElement strokeElement, string strokeData)
+ {
+ var parseIndex = 0;
+ if (!string.IsNullOrWhiteSpace(strokeElement.Data))
+ {
+ parseIndex = strokeData.IndexOf(strokeElement.Data, parseIndex, StringComparison.Ordinal);
+ }
+
+ var errorString = strokeData.Substring(parseIndex);
+ if (errorString.Length > 30)
+ {
+ errorString = $"{errorString.Substring(0, 30)}...";
+ }
+
+ throw new ArgumentException($"STROKE_ERR003:Stroke data contains invalid characters!\nIndex: {parseIndex}\n{errorString}");
+ }
+
+ ThrowForInvalidCharacters(strokeElement, strokeData);
+ }
+
+ return strokeElement;
+
+ static void ThrowForZeroCount() => throw new ArgumentException("STROKE_ERR001:Invalid Stroke data! No matching CanvasStroke found!");
+ static void ThrowForNotOneCount() => throw new ArgumentException("STROKE_ERR002:Multiple CanvasStrokes defined in Stroke Data! " +
+ "There should be only one CanvasStroke definition within the Stroke Data. " +
+ "You can either remove CanvasStroke definitions or split the Stroke Data " +
+ "into multiple Stroke Data and call the CanvasPathGeometry.CreateStroke() method on each of them.");
+ }
+
+ ///
+ /// Parses the Stroke Data string and converts it into CanvasStroke.
+ ///
+ /// ICanvasResourceCreator
+ /// Stroke Data string
+ /// ICanvasStroke
+ internal static ICanvasStroke Parse(ICanvasResourceCreator resourceCreator, string strokeData)
+ {
+ // Parse the stroke data to create the ICanvasStrokeElement
+ var strokeElement = Parse(strokeData);
+
+ // Create the CanvasStroke from the strokeElement
+ return strokeElement.CreateStroke(resourceCreator);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeStyleParser.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeStyleParser.cs
new file mode 100644
index 00000000000..c0418ee5619
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/CanvasStrokeStyleParser.cs
@@ -0,0 +1,93 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Elements.Stroke;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers
+{
+ ///
+ /// Parser for the CanvasStrokeStyle
+ ///
+ internal static class CanvasStrokeStyleParser
+ {
+ ///
+ /// Parses the given style data and converts it to CanvasStrokeStyle.
+ ///
+ /// Style data
+ /// CanvasStrokeStyle
+ internal static CanvasStrokeStyle Parse(string styleData)
+ {
+ var matches = RegexFactory.CanvasStrokeStyleRegex.Matches(styleData);
+
+ // If no match is found or no captures in the match, then it means that the style data is invalid.
+ if (matches.Count == 0)
+ {
+ ThrowForZeroCount();
+ }
+
+ // If the match contains more than one captures, it means that there
+ // are multiple CanvasStrokeStyles present in the CanvasStrokeStyle data. There should
+ // be only one CanvasStrokeStyle defined in the CanvasStrokeStyle data.
+ if (matches.Count > 1)
+ {
+ ThrowForNotOneCount();
+ }
+
+ // There should be only one match
+ var match = matches[0];
+ var styleElement = new CanvasStrokeStyleElement(match);
+
+ // Perform validation to check if there are any invalid characters in the brush data that were not captured
+ var preValidationCount = RegexFactory.ValidationRegex.Replace(styleData, string.Empty).Length;
+ var postValidationCount = styleElement.ValidationCount;
+
+ // If there are invalid characters, extract them and add them to the ArgumentException message
+ if (preValidationCount != postValidationCount)
+ {
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static void ThrowForInvalidCharacters(CanvasStrokeStyleElement styleElement, string styleData)
+ {
+ var parseIndex = 0;
+ if (!string.IsNullOrWhiteSpace(styleElement.Data))
+ {
+ parseIndex = styleData.IndexOf(styleElement.Data, parseIndex, StringComparison.Ordinal);
+ }
+
+ var errorString = styleData.Substring(parseIndex);
+ if (errorString.Length > 30)
+ {
+ errorString = $"{errorString.Substring(0, 30)}...";
+ }
+
+ throw new ArgumentException($"STYLE_ERR003:Style data contains invalid characters!\nIndex: {parseIndex}\n{errorString}");
+ }
+
+ ThrowForInvalidCharacters(styleElement, styleData);
+ }
+
+ return styleElement.Style;
+
+ static void ThrowForZeroCount() => throw new ArgumentException("STYLE_ERR001:Invalid CanvasStrokeStyle data! No matching CanvasStrokeStyle found!");
+ static void ThrowForNotOneCount() => throw new ArgumentException("STYLE_ERR002:Multiple CanvasStrokeStyles defined in CanvasStrokeStyle Data! " +
+ "There should be only one CanvasStrokeStyle definition within the CanvasStrokeStyle Data. " +
+ "You can either remove CanvasStrokeStyle definitions or split the CanvasStrokeStyle Data " +
+ "into multiple CanvasStrokeStyle Data and call the CanvasPathGeometry.CreateStrokeStyle() method on each of them.");
+ }
+
+ ///
+ /// Parses and constructs a ICanvasStrokeStyleElement from the specified Match object.
+ ///
+ /// Match object
+ /// ICanvasStrokeStyleElement
+ internal static ICanvasStrokeStyleElement Parse(Match match)
+ {
+ return new CanvasStrokeStyleElement(match);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/ColorParser.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/ColorParser.cs
new file mode 100644
index 00000000000..3bc1955d473
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Parsers/ColorParser.cs
@@ -0,0 +1,146 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+using Windows.UI;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry.Parsers
+{
+ ///
+ /// Parser for Color
+ ///
+ internal static class ColorParser
+ {
+ ///
+ /// Converts the color string in Hexadecimal or HDR color format to the corresponding Color object.
+ /// The hexadecimal color string should be in #RRGGBB or #AARRGGBB format.
+ /// The '#' character is optional.
+ /// The HDR color string should be in R G B A format.
+ /// (R, G, B & A should have value in the range between 0 and 1, inclusive)
+ ///
+ /// Color string in Hexadecimal or HDR format
+ /// Color
+ internal static Color Parse(string colorString)
+ {
+ var match = RegexFactory.ColorRegex.Match(colorString);
+
+ if (!match.Success)
+ {
+ ThrowArgumentException();
+ }
+
+ // Perform validation to check if there are any invalid characters in the colorString that were not captured
+ var preValidationCount = RegexFactory.ValidationRegex.Replace(colorString, string.Empty).Length;
+ var postValidationCount = RegexFactory.ValidationRegex.Replace(match.Value, string.Empty).Length;
+
+ // If there are invalid characters, extract them and add them to the ArgumentException message
+ if (preValidationCount != postValidationCount)
+ {
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static void ThrowForInvalidCharacters(Match match, string colorString)
+ {
+ var parseIndex = 0;
+ if (!string.IsNullOrWhiteSpace(match.Value))
+ {
+ parseIndex = colorString.IndexOf(match.Value, parseIndex, StringComparison.Ordinal);
+ }
+
+ var errorString = colorString.Substring(parseIndex);
+ if (errorString.Length > 30)
+ {
+ errorString = $"{errorString.Substring(0, 30)}...";
+ }
+
+ throw new ArgumentException($"COLOR_ERR003:Color data contains invalid characters!\nIndex: {parseIndex}\n{errorString}");
+ }
+
+ ThrowForInvalidCharacters(match, colorString);
+ }
+
+ return Parse(match);
+
+ static void ThrowArgumentException() => throw new ArgumentException("COLOR_ERR001:Invalid value provided in Color Data! No matching color found in the Color Data.");
+ }
+
+ ///
+ /// Converts a Vector4 High Dynamic Range Color to Color. Negative components of the Vector4 will be sanitized by taking the absolute value of the component.
+ /// The HDR Color components should have value in the range between 0 and 1, inclusive. If they are more than 1, they will be clamped at 1.
+ ///
+ /// High Dynamic Range Color
+ /// Color
+ internal static Color Parse(Vector4 hdrColor)
+ {
+ // Vector4's X, Y, Z, W components match to
+ // Color's R, G, B, A components respectively
+ return Parse(hdrColor.X, hdrColor.Y, hdrColor.Z, hdrColor.W);
+ }
+
+ ///
+ /// Converts the given HDR color values to Color.
+ ///
+ /// Red Component
+ /// Green Component
+ /// Blue Component
+ /// Alpha Component
+ /// Instance of Color.
+ internal static Color Parse(float x, float y, float z, float w)
+ {
+ Vector4 v4 = new Vector4(x, y, z, w);
+ v4 = Vector4.Min(Vector4.Abs(v4) * 255f, new Vector4(255f));
+
+ var r = (byte)v4.X;
+ var g = (byte)v4.Y;
+ var b = (byte)v4.Z;
+ var a = (byte)v4.W;
+
+ return Color.FromArgb(a, r, g, b);
+ }
+
+ ///
+ /// Parses and constructs a Color object from the specified Match object.
+ ///
+ /// Match object
+ /// Color
+ internal static Color Parse(Match match)
+ {
+ if (match.Groups["RgbColor"].Success)
+ {
+ // Alpha component
+ byte alpha = 255;
+ var alphaStr = match.Groups["Alpha"].Value;
+ if (!string.IsNullOrWhiteSpace(alphaStr))
+ {
+ alpha = (byte)Convert.ToInt32(alphaStr, 16);
+ }
+
+ // Red component
+ var red = (byte)Convert.ToInt32(match.Groups["Red"].Value, 16);
+
+ // Green component
+ var green = (byte)Convert.ToInt32(match.Groups["Green"].Value, 16);
+
+ // Blue component
+ var blue = (byte)Convert.ToInt32(match.Groups["Blue"].Value, 16);
+
+ return Color.FromArgb(alpha, red, green, blue);
+ }
+
+ if (match.Groups["HdrColor"].Success)
+ {
+ float.TryParse(match.Groups["X"].Value, out var x);
+ float.TryParse(match.Groups["Y"].Value, out var y);
+ float.TryParse(match.Groups["Z"].Value, out var z);
+ float.TryParse(match.Groups["W"].Value, out var w);
+
+ return Parse(x, y, z, w);
+ }
+
+ return Colors.Transparent;
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Scalar.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Scalar.cs
new file mode 100644
index 00000000000..e93b1b62af7
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Scalar.cs
@@ -0,0 +1,64 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry
+{
+ ///
+ /// Class containing some constants
+ /// represented as Floating point numbers.
+ ///
+ internal static class Scalar
+ {
+ // Pi related floating point constants
+
+ ///
+ /// (float)Math.PI radians ( or 180 degrees).
+ ///
+ internal const float Pi = (float)Math.PI;
+
+ ///
+ /// Two times (float)Math.PI radians ( or 360 degrees).
+ ///
+ internal const float TwoPi = 2f * Pi;
+
+ ///
+ /// Half of (float)Math.PI radians ( or 90 degrees).
+ ///
+ internal const float PiByTwo = Pi / 2f;
+
+ ///
+ /// One third of (float)Math.PI radians ( or 60 degrees).
+ ///
+ internal const float PiByThree = Pi / 3f;
+
+ ///
+ /// One fourth of (float)Math.PI radians ( or 45 degrees).
+ ///
+ internal const float PiByFour = Pi / 4f;
+
+ ///
+ /// One sixth of (float)Math.PI radians ( or 30 degrees).
+ ///
+ internal const float PiBySix = Pi / 6f;
+
+ ///
+ /// Three times half of (float)Math.PI radians ( or 270 degrees).
+ ///
+ internal const float ThreePiByTwo = 3f * Pi / 2f;
+
+ // Conversion constants
+
+ ///
+ /// 1 degree in radians.
+ ///
+ internal const float DegreesToRadians = Pi / 180f;
+
+ ///
+ /// 1 radian in degrees.
+ ///
+ internal const float RadiansToDegrees = 180f / Pi;
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Utils.cs b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Utils.cs
new file mode 100644
index 00000000000..9a893a799a6
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Geometry/Utils.cs
@@ -0,0 +1,817 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using Windows.Foundation;
+using Windows.UI;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Media;
+
+namespace Microsoft.Toolkit.Uwp.UI.Media.Geometry
+{
+ ///
+ /// Class containing collection of useful methods for various types
+ ///
+ public static class Utils
+ {
+ // Constant values
+
+ // Smallest double value such that 1.0 + DoubleEpsilon != 1.0
+ internal const double DoubleEpsilon = 2.2250738585072014E-308;
+
+ // Smallest float value such that 1.0f + FloatMin != 1.0f
+ internal const float FloatMin = 1.175494351E-38F;
+
+ ///
+ /// Returns whether or not two doubles are "close".
+ ///
+ /// The first double to compare.
+ /// The second double to compare.
+ ///
+ /// bool - the result of the AreClose comparision.
+ ///
+ public static bool IsCloseTo(this double value1, double value2)
+ {
+ // In case they are Infinities or NaN (then epsilon check does not work)
+ if ((double.IsInfinity(value1) &&
+ double.IsInfinity(value2)) ||
+ (double.IsNaN(value1) && double.IsNaN(value2)))
+ {
+ return true;
+ }
+
+ // This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DoubleEpsilon
+ var eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DoubleEpsilon;
+ var delta = value1 - value2;
+ return (-eps < delta) && (eps > delta);
+ }
+
+ ///
+ /// Returns whether or not the first double is less than the second double.
+ ///
+ /// The first double to compare.
+ /// The second double to compare.
+ ///
+ /// bool - the result of the LessThan comparision.
+ ///
+ public static bool IsLessThan(this double value1, double value2)
+ {
+ return (value1 < value2) && !value1.IsCloseTo(value2);
+ }
+
+ ///
+ /// Returns whether or not the first double is greater than the second double.
+ ///
+ /// The first double to compare.
+ /// The second double to compare.
+ ///
+ /// bool - the result of the GreaterThan comparision.
+ ///
+ public static bool IsGreaterThan(this double value1, double value2)
+ {
+ return (value1 > value2) && !value1.IsCloseTo(value2);
+ }
+
+ ///
+ /// Returns whether or not the double is "close" to 1. Same as AreClose(double, 1),
+ /// but this is faster.
+ ///
+ /// The double to compare to 1.
+ ///
+ /// bool - the result of the AreClose comparision.
+ ///
+ public static bool IsOne(this double value)
+ {
+ return Math.Abs(value - 1.0d) < 10.0d * DoubleEpsilon;
+ }
+
+ ///
+ /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0),
+ /// but this is faster.
+ ///
+ /// The double to compare to 0.
+ ///
+ /// bool - the result of the AreClose comparision.
+ ///
+ public static bool IsZero(this double value)
+ {
+ return Math.Abs(value) < 10.0d * DoubleEpsilon;
+ }
+
+ ///
+ /// Returns whether or not two floats are "close".
+ ///
+ /// The first float to compare.
+ /// The second float to compare.
+ ///
+ /// bool - the result of the AreClose comparision.
+ ///
+ public static bool IsCloseTo(this float value1, float value2)
+ {
+ // In case they are Infinities or NaN (then epsilon check does not work)
+ if ((float.IsInfinity(value1) &&
+ float.IsInfinity(value2)) ||
+ (float.IsNaN(value1) && float.IsNaN(value2)))
+ {
+ return true;
+ }
+
+ // This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < FloatMin
+ var eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0f) * FloatMin;
+ var delta = value1 - value2;
+ return (-eps < delta) && (eps > delta);
+ }
+
+ ///
+ /// Returns whether or not the first float is less than the second float.
+ ///
+ /// The first float to compare.
+ /// The second float to compare.
+ ///
+ /// bool - the result of the LessThan comparision.
+ ///
+ public static bool IsLessThan(this float value1, float value2)
+ {
+ return (value1 < value2) && !value1.IsCloseTo(value2);
+ }
+
+ ///
+ /// Returns whether or not the first float is greater than the second float.
+ ///
+ /// The first float to compare.
+ /// The second float to compare.
+ ///
+ /// bool - the result of the GreaterThan comparision.
+ ///
+ public static bool IsGreaterThan(this float value1, float value2)
+ {
+ return (value1 > value2) && !value1.IsCloseTo(value2);
+ }
+
+ ///
+ /// Returns whether or not the float is "close" to 1. Same as AreClose(float, 1),
+ /// but this is faster.
+ ///
+ /// The float to compare to 1.
+ ///
+ /// bool - the result of the AreClose comparision.
+ ///
+ public static bool IsOne(this float value)
+ {
+ return Math.Abs(value - 1.0f) < 10.0f * FloatMin;
+ }
+
+ ///
+ /// IsZero - Returns whether or not the float is "close" to 0. Same as AreClose(float, 0),
+ /// but this is faster.
+ ///
+ /// The float to compare to 0.
+ ///
+ /// bool - the result of the AreClose comparision.
+ ///
+ public static bool IsZero(this float value)
+ {
+ return Math.Abs(value) < 10.0f * FloatMin;
+ }
+
+ ///
+ /// Compares two points for fuzzy equality. This function
+ /// helps compensate for the fact that double values can
+ /// acquire error when operated upon
+ ///
+ /// The first point to compare
+ /// The second point to compare
+ /// Whether or not the two points are equal
+ public static bool IsCloseTo(this Point point1, Point point2)
+ {
+ return point1.X.IsCloseTo(point2.X) && point1.Y.IsCloseTo(point2.Y);
+ }
+
+ ///
+ /// Compares two Size instances for fuzzy equality. This function
+ /// helps compensate for the fact that double values can
+ /// acquire error when operated upon
+ ///
+ /// The first size to compare
+ /// The second size to compare
+ /// Whether or not the two Size instances are equal
+ public static bool IsCloseTo(this Size size1, Size size2)
+ {
+ return size1.Width.IsCloseTo(size2.Width) && size1.Height.IsCloseTo(size2.Height);
+ }
+
+ ///
+ /// Compares two rectangles for fuzzy equality. This function
+ /// helps compensate for the fact that double values can
+ /// acquire error when operated upon
+ ///
+ /// The first rectangle to compare
+ /// The second rectangle to compare
+ /// Whether or not the two rectangles are equal
+ public static bool IsCloseTo(this Rect rect1, Rect rect2)
+ {
+ // If they're both empty, don't bother with the double logic.
+ if (rect1.IsEmpty)
+ {
+ return rect2.IsEmpty;
+ }
+
+ // At this point, rect1 isn't empty, so the first thing we can test is rect2.IsEmpty, followed by property-wise compares.
+ return (!rect2.IsEmpty)
+ && rect1.X.IsCloseTo(rect2.X) && rect1.Y.IsCloseTo(rect2.Y)
+ && rect1.Height.IsCloseTo(rect2.Height) && rect1.Width.IsCloseTo(rect2.Width);
+ }
+
+ ///
+ /// Rounds the given value based on the DPI scale
+ ///
+ /// Value to round
+ /// DPI Scale
+ /// The rounded value
+ public static double RoundLayoutValue(double value, double dpiScale)
+ {
+ double newValue;
+
+ // If DPI == 1, don't use DPI-aware rounding.
+ if (!dpiScale.IsCloseTo(1.0))
+ {
+ newValue = Math.Round(value * dpiScale) / dpiScale;
+
+ // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value.
+ if (double.IsNaN(newValue) ||
+ double.IsInfinity(newValue) ||
+ newValue.IsCloseTo(double.MaxValue))
+ {
+ newValue = value;
+ }
+ }
+ else
+ {
+ newValue = Math.Round(value);
+ }
+
+ return newValue;
+ }
+
+ ///
+ /// Calculates the linear interpolated value based on the given values.
+ ///
+ /// Starting value.
+ /// Ending value.
+ /// Weight-age given to the ending value.
+ /// Linear interpolated value.
+ public static float Lerp(this float start, float end, float amount)
+ {
+ return start + ((end - start) * amount);
+ }
+
+ ///
+ /// Verifies if this Thickness contains only valid values. The set of validity checks is passed as parameters.
+ ///
+ /// Thickness value
+ /// allows negative values
+ /// allows double.NaN
+ /// allows double.PositiveInfinity
+ /// allows double.NegativeInfinity
+ /// Whether or not the thickness complies to the range specified
+ public static bool IsValid(this Thickness thick, bool allowNegative, bool allowNaN, bool allowPositiveInfinity, bool allowNegativeInfinity)
+ {
+ if (!allowNegative)
+ {
+ if (thick.Left < 0d || thick.Right < 0d || thick.Top < 0d || thick.Bottom < 0d)
+ {
+ return false;
+ }
+ }
+
+ if (!allowNaN)
+ {
+ if (double.IsNaN(thick.Left) || double.IsNaN(thick.Right)
+ || double.IsNaN(thick.Top) || double.IsNaN(thick.Bottom))
+ {
+ return false;
+ }
+ }
+
+ if (!allowPositiveInfinity)
+ {
+ if (double.IsPositiveInfinity(thick.Left) || double.IsPositiveInfinity(thick.Right)
+ || double.IsPositiveInfinity(thick.Top) || double.IsPositiveInfinity(thick.Bottom))
+ {
+ return false;
+ }
+ }
+
+ if (!allowNegativeInfinity)
+ {
+ if (double.IsNegativeInfinity(thick.Left) || double.IsNegativeInfinity(thick.Right)
+ || double.IsNegativeInfinity(thick.Top) || double.IsNegativeInfinity(thick.Bottom))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Method to add up the left and right size as width, as well as the top and bottom size as height.
+ ///
+ /// Thickness
+ /// Size
+ public static Size CollapseThickness(this Thickness thick)
+ {
+ return new Size(thick.Left + thick.Right, thick.Top + thick.Bottom);
+ }
+
+ ///
+ /// Verifies if the Thickness contains only zero values.
+ ///
+ /// Thickness
+ /// Size
+ public static bool IsZero(this Thickness thick)
+ {
+ return thick.Left.IsZero()
+ && thick.Top.IsZero()
+ && thick.Right.IsZero()
+ && thick.Bottom.IsZero();
+ }
+
+ ///
+ /// Verifies if all the values in Thickness are same.
+ ///
+ /// Thickness
+ /// true if yes, otherwise false
+ public static bool IsUniform(this Thickness thick)
+ {
+ return thick.Left.IsCloseTo(thick.Top)
+ && thick.Left.IsCloseTo(thick.Right)
+ && thick.Left.IsCloseTo(thick.Bottom);
+ }
+
+ ///
+ /// Converts the Thickness object to Vector4. If the Thickness object's component have values NaN, PositiveInfinity or NegativeInfinity, then Vector4.Zero will be returned.
+ ///
+ /// Thickness object
+ /// Vector4
+ public static Vector4 ToVector4(this Thickness thickness)
+ {
+ if (thickness.IsValid(true, false, false, false))
+ {
+ // Sanitize the component by taking only
+ return new Vector4(
+ (float)thickness.Left,
+ (float)thickness.Top,
+ (float)thickness.Right,
+ (float)thickness.Bottom);
+ }
+
+ return Vector4.Zero;
+ }
+
+ ///
+ /// Converts the Thickness object to Vector4. If the Thickness object contains negative components they will be converted to positive values. If the Thickness object's component have values NaN, PositiveInfinity or NegativeInfinity, then Vector4.Zero will be returned.
+ ///
+ /// Thickness object
+ /// Vector2
+ public static Vector4 ToAbsVector4(this Thickness thickness)
+ {
+ if (thickness.IsValid(true, false, false, false))
+ {
+ // Sanitize the component by taking only
+ return new Vector4(
+ Math.Abs((float)thickness.Left),
+ Math.Abs((float)thickness.Top),
+ Math.Abs((float)thickness.Right),
+ Math.Abs((float)thickness.Bottom));
+ }
+
+ return Vector4.Zero;
+ }
+
+ ///
+ /// Gets the top left corner of the thickness structure.
+ ///
+ /// Thickness object
+ /// Vector2
+ public static Vector2 GetOffset(this Thickness thickness)
+ {
+ return new Vector2((float)thickness.Left, (float)thickness.Top);
+ }
+
+ ///
+ /// Verifies if the CornerRadius contains only zero values.
+ ///
+ /// CornerRadius
+ /// true if yes, otherwise false
+ public static bool IsZero(this CornerRadius corner)
+ {
+ return corner.TopLeft.IsZero()
+ && corner.TopRight.IsZero()
+ && corner.BottomRight.IsZero()
+ && corner.BottomLeft.IsZero();
+ }
+
+ ///
+ /// Verifies if the CornerRadius contains same values.
+ ///
+ /// CornerRadius
+ /// true if yes, otherwise false
+ public static bool IsUniform(this CornerRadius corner)
+ {
+ var topLeft = corner.TopLeft;
+ return topLeft.IsCloseTo(corner.TopRight) &&
+ topLeft.IsCloseTo(corner.BottomLeft) &&
+ topLeft.IsCloseTo(corner.BottomRight);
+ }
+
+ ///
+ /// Converts the given corner value to a valid positive value. Returns zero if the corner value is Infinity or NaN or 0.
+ ///
+ /// Corner value
+ /// Valid Corner value
+ public static double ConvertToValidCornerValue(double corner)
+ {
+ if (double.IsNaN(corner) ||
+ double.IsInfinity(corner) ||
+ (corner < 0d))
+ {
+ return 0d;
+ }
+
+ return corner;
+ }
+
+ ///
+ /// Converts the CornerRadius object to Vector4. If the CornerRadius object's component have values NaN, PositiveInfinity or NegativeInfinity, then Vector4.Zero will be returned.
+ ///
+ /// CornerRadius object
+ /// Vector4
+ public static Vector4 ToVector4(this CornerRadius corner)
+ {
+ return new Vector4(
+ (float)corner.TopLeft,
+ (float)corner.TopRight,
+ (float)corner.BottomRight,
+ (float)corner.BottomLeft);
+ }
+
+ ///
+ /// Deflates rectangle by given thickness.
+ ///
+ /// Rectangle
+ /// Thickness
+ /// Deflated Rectangle
+ public static Rect Deflate(this Rect rect, Thickness thick)
+ {
+ return new Rect(
+ rect.Left + thick.Left,
+ rect.Top + thick.Top,
+ Math.Max(0.0, rect.Width - thick.Left - thick.Right),
+ Math.Max(0.0, rect.Height - thick.Top - thick.Bottom));
+ }
+
+ ///
+ /// Inflates rectangle by given thickness.
+ ///
+ /// Rectangle
+ /// Thickness
+ /// Inflated Rectangle
+ public static Rect Inflate(this Rect rect, Thickness thick)
+ {
+ return new Rect(
+ rect.Left - thick.Left,
+ rect.Top - thick.Top,
+ Math.Max(0.0, rect.Width + thick.Left + thick.Right),
+ Math.Max(0.0, rect.Height + thick.Top + thick.Bottom));
+ }
+
+ ///
+ /// Verifies if the given brush is a SolidColorBrush and its color does not include transparency.
+ ///
+ /// Brush
+ /// true if yes, otherwise false
+ public static bool IsOpaqueSolidColorBrush(this Brush brush)
+ {
+ return (brush as SolidColorBrush)?.Color.A == 0xff;
+ }
+
+ ///
+ /// Verifies if the given brush is the same as the otherBrush.
+ ///
+ /// Given
+ /// The to match it with
+ /// true if yes, otherwise false
+ public static bool IsEqualTo(this Brush brush, Brush otherBrush)
+ {
+ if (brush.GetType() != otherBrush.GetType())
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(brush, otherBrush))
+ {
+ return true;
+ }
+
+ // Are both instances of SolidColorBrush
+ if ((brush is SolidColorBrush solidBrushA) && (otherBrush is SolidColorBrush solidBrushB))
+ {
+ return (solidBrushA.Color == solidBrushB.Color)
+ && solidBrushA.Opacity.IsCloseTo(solidBrushB.Opacity);
+ }
+
+ // Are both instances of LinearGradientBrush
+ if ((brush is LinearGradientBrush linGradBrushA) && (otherBrush is LinearGradientBrush linGradBrushB))
+ {
+ var result = (linGradBrushA.ColorInterpolationMode == linGradBrushB.ColorInterpolationMode)
+ && (linGradBrushA.EndPoint == linGradBrushB.EndPoint)
+ && (linGradBrushA.MappingMode == linGradBrushB.MappingMode)
+ && linGradBrushA.Opacity.IsCloseTo(linGradBrushB.Opacity)
+ && (linGradBrushA.StartPoint == linGradBrushB.StartPoint)
+ && (linGradBrushA.SpreadMethod == linGradBrushB.SpreadMethod)
+ && (linGradBrushA.GradientStops.Count == linGradBrushB.GradientStops.Count);
+ if (!result)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < linGradBrushA.GradientStops.Count; i++)
+ {
+ result = (linGradBrushA.GradientStops[i].Color == linGradBrushB.GradientStops[i].Color)
+ && linGradBrushA.GradientStops[i].Offset.IsCloseTo(linGradBrushB.GradientStops[i].Offset);
+
+ if (!result)
+ {
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ // Are both instances of ImageBrush
+ if ((brush is ImageBrush imgBrushA) && (otherBrush is ImageBrush imgBrushB))
+ {
+ var result = (imgBrushA.AlignmentX == imgBrushB.AlignmentX)
+ && (imgBrushA.AlignmentY == imgBrushB.AlignmentY)
+ && imgBrushA.Opacity.IsCloseTo(imgBrushB.Opacity)
+ && (imgBrushA.Stretch == imgBrushB.Stretch)
+ && (imgBrushA.ImageSource == imgBrushB.ImageSource);
+
+ return result;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Compares one URI with another URI.
+ ///
+ /// URI to compare with
+ /// URI to compare
+ /// true if yes, otherwise false
+ public static bool IsEqualTo(this Uri uri, Uri otherUri)
+ {
+ return
+ Uri.Compare(uri, otherUri, UriComponents.AbsoluteUri, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase) == 0;
+ }
+
+ ///
+ /// Reflects point 'a' over point 'b'.
+ ///
+ /// Point to be reflected
+ /// Point of reflection
+ /// Reflected point
+ public static Vector2 Reflect(Vector2 a, Vector2 b)
+ {
+ // Let 'c' be the reflected point. Then point 'b'
+ // becomes the middle point between 'a' and 'c'.
+ // As per MidPoint formula,
+ // b.X = (a.X + c.X) / 2 and
+ // b.Y = (a.Y + c.Y) / 2
+ // Therefore, c.X = 2 * b.X - a.X
+ // c.y = 2 * b.Y - a.Y
+ return new Vector2((2f * b.X) - a.X, (2f * b.Y) - a.Y);
+ }
+
+ ///
+ /// Converts a Vector2 structure (x,y) to Vector3 structure (x, y, 0).
+ ///
+ /// Input Vector2
+ /// Vector3
+ public static Vector3 ToVector3(this Vector2 v)
+ {
+ return new Vector3(v, 0);
+ }
+
+ ///
+ /// Verifies if the Vector4 contains only zero values.
+ ///
+ /// Vector4
+ /// true if yes, otherwise false
+ public static bool IsZero(this Vector4 vector)
+ {
+ return vector.X.IsZero()
+ && vector.Y.IsZero()
+ && vector.Z.IsZero()
+ && vector.W.IsZero();
+ }
+
+ ///
+ /// Useful in converting the four components of Thickness or Padding to two components by taking a sum of alternate components (X & Z and Y & W).
+ ///
+ /// Vector4
+ /// Vector3
+ public static Vector2 Collapse(this Vector4 vector)
+ {
+ return new Vector2(vector.X + vector.Z, vector.Y + vector.W);
+ }
+
+ ///
+ /// Useful in converting the four components of Thickness or Padding to two components by adding alternate components - (X & Z and Y & W).
+ ///
+ /// Vector4
+ /// Size
+ public static Size ToSize(this Vector4 vector)
+ {
+ return new Size(vector.X + vector.Z, vector.Y + vector.W);
+ }
+
+ ///
+ /// Converts the Vector4 to Thickness - Left(X), Top(Y), Right(Z), Bottom(W).
+ ///
+ /// Vector4
+ /// Thickness
+ public static Thickness ToThickness(this Vector4 vector)
+ {
+ return new Thickness(vector.X, vector.Y, vector.Z, vector.W);
+ }
+
+ ///
+ /// Converts the Vector4 to CornerRadius - TopLeft(X), TopRight(Y), BottomRight(Z), BottomLeft(W).
+ ///
+ /// Vector4
+ /// CornerRadius
+ public static CornerRadius ToCornerRadius(this Vector4 vector)
+ {
+ return new CornerRadius(vector.X, vector.Y, vector.Z, vector.W);
+ }
+
+ ///
+ /// Calculates the linear interpolated Color based on the given Color values.
+ ///
+ /// Source Color.
+ /// Target Color.
+ /// Weightage given to the target color.
+ /// Linear Interpolated Color.
+ public static Color Lerp(this Color colorFrom, Color colorTo, float amount)
+ {
+ // Convert colorFrom components to lerp-able floats
+ float sa = colorFrom.A,
+ sr = colorFrom.R,
+ sg = colorFrom.G,
+ sb = colorFrom.B;
+
+ // Convert colorTo components to lerp-able floats
+ float ea = colorTo.A,
+ er = colorTo.R,
+ eg = colorTo.G,
+ eb = colorTo.B;
+
+ // lerp the colors to get the difference
+ byte a = (byte)Math.Max(0, Math.Min(255, sa.Lerp(ea, amount))),
+ r = (byte)Math.Max(0, Math.Min(255, sr.Lerp(er, amount))),
+ g = (byte)Math.Max(0, Math.Min(255, sg.Lerp(eg, amount))),
+ b = (byte)Math.Max(0, Math.Min(255, sb.Lerp(eb, amount)));
+
+ // return the new color
+ return Color.FromArgb(a, r, g, b);
+ }
+
+ ///
+ /// Darkens the color by the given percentage.
+ ///
+ /// Source color.
+ /// Percentage to darken. Value should be between 0 and 1.
+ /// Color
+ public static Color DarkerBy(this Color color, float amount)
+ {
+ return color.Lerp(Colors.Black, amount);
+ }
+
+ ///
+ /// Lightens the color by the given percentage.
+ ///
+ /// Source color.
+ /// Percentage to lighten. Value should be between 0 and 1.
+ /// Color
+ public static Color LighterBy(this Color color, float amount)
+ {
+ return color.Lerp(Colors.White, amount);
+ }
+
+ ///
+ /// Converts the Point structure P (X,Y) to Vector3 structure
+ /// V (P.X, P.Y, 0);
+ ///
+ /// Point structure
+ /// Vector3
+ public static Vector3 ToVector3(this Point p)
+ {
+ return new Vector3((float)p.X, (float)p.Y, 0f);
+ }
+
+ ///
+ /// Calculates the best size that can fit in the destination area based on the given stretch and alignment options.
+ ///
+ /// Width of the source.
+ /// Height of the source.
+ /// Width of the destination area.
+ /// Height of the destination area.
+ /// Defines how the source should stretch to fit the destination.
+ /// Horizontal Alignment
+ /// Vertical Alignment
+ /// The best fitting Rectangle in the destination area.
+ public static Rect GetOptimumSize(double srcWidth, double srcHeight, double destWidth, double destHeight, Stretch stretch, AlignmentX horizontalAlignment, AlignmentY verticalAlignment)
+ {
+ var ratio = srcWidth / srcHeight;
+ var targetWidth = 0d;
+ var targetHeight = 0d;
+
+ // Stretch Mode
+ switch (stretch)
+ {
+ case Stretch.None:
+ targetWidth = srcWidth;
+ targetHeight = srcHeight;
+ break;
+ case Stretch.Fill:
+ targetWidth = destWidth;
+ targetHeight = destHeight;
+ break;
+ case Stretch.Uniform:
+ // If width is greater than height
+ if (ratio > 1.0)
+ {
+ targetHeight = Math.Min(destWidth / ratio, destHeight);
+ targetWidth = targetHeight * ratio;
+ }
+ else
+ {
+ targetWidth = Math.Min(destHeight * ratio, destWidth);
+ targetHeight = targetWidth / ratio;
+ }
+
+ break;
+ case Stretch.UniformToFill:
+ // If width is greater than height
+ if (ratio > 1.0)
+ {
+ targetHeight = Math.Max(destWidth / ratio, destHeight);
+ targetWidth = targetHeight * ratio;
+ }
+ else
+ {
+ targetWidth = Math.Max(destHeight * ratio, destWidth);
+ targetHeight = targetWidth / ratio;
+ }
+
+ break;
+ }
+
+ var left = 0d;
+ switch (horizontalAlignment)
+ {
+ case AlignmentX.Left:
+ left = 0;
+ break;
+ case AlignmentX.Center:
+ left = (destWidth - targetWidth) / 2.0;
+ break;
+ case AlignmentX.Right:
+ left = destWidth - targetWidth;
+ break;
+ }
+
+ var top = 0d;
+ switch (verticalAlignment)
+ {
+ case AlignmentY.Top:
+ top = 0;
+ break;
+ case AlignmentY.Center:
+ top = (destHeight - targetHeight) / 2.0;
+ break;
+ case AlignmentY.Bottom:
+ top = destHeight - targetHeight;
+ break;
+ }
+
+ return new Rect(left, top, targetWidth, targetHeight);
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Media/Microsoft.Toolkit.Uwp.UI.Media.csproj b/Microsoft.Toolkit.Uwp.UI.Media/Microsoft.Toolkit.Uwp.UI.Media.csproj
index e7ccf6e9bda..99b82d778b1 100644
--- a/Microsoft.Toolkit.Uwp.UI.Media/Microsoft.Toolkit.Uwp.UI.Media.csproj
+++ b/Microsoft.Toolkit.Uwp.UI.Media/Microsoft.Toolkit.Uwp.UI.Media.csproj
@@ -35,6 +35,9 @@
- SurfaceLoader: A class that can load and draw images and other objects to Win2D surfaces and brushes.
PipelineBuilder: A class that allows to build custom effects pipelines and create CompositionBrush instances from them.
+
+ Geometry:
+ - CanvasPathGeometry: A class that parses Win2d Path Mini Language and converts it to CanvasGeometry, CanvasBrush, CanvasStroke, CanvasStrokeStyle or Color.
UWP Toolkit Windows UI XAML brushes blur
diff --git a/UITests/UITests.App/UITests.App.csproj b/UITests/UITests.App/UITests.App.csproj
index bcb7be34ecc..55c0c6f73c5 100644
--- a/UITests/UITests.App/UITests.App.csproj
+++ b/UITests/UITests.App/UITests.App.csproj
@@ -259,4 +259,4 @@
-->
-
+
\ No newline at end of file
diff --git a/UnitTests/UnitTests.UWP/Geometry/Test_CanvasPathGeometry.cs b/UnitTests/UnitTests.UWP/Geometry/Test_CanvasPathGeometry.cs
new file mode 100644
index 00000000000..3e835bf7d7b
--- /dev/null
+++ b/UnitTests/UnitTests.UWP/Geometry/Test_CanvasPathGeometry.cs
@@ -0,0 +1,362 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
+
+namespace UnitTests.UWP.Geometry
+{
+ [TestClass]
+ public class Test_CanvasPathGeometry
+ {
+ [TestCategory("CanvasPathGeometry - Color")]
+ [TestMethod]
+ public void Test_CreateColor_1()
+ {
+ var color = CanvasPathGeometry.CreateColor("3FFF7FFF");
+ Assert.AreEqual(63, color.A);
+ Assert.AreEqual(255, color.R);
+ Assert.AreEqual(127, color.G);
+ Assert.AreEqual(255, color.B);
+ }
+
+ [TestCategory("CanvasPathGeometry - Color")]
+ [TestMethod]
+ public void Test_CreateColor_2()
+ {
+ var color = CanvasPathGeometry.CreateColor("FF7FFF");
+ Assert.AreEqual(255, color.A);
+ Assert.AreEqual(255, color.R);
+ Assert.AreEqual(127, color.G);
+ Assert.AreEqual(255, color.B);
+ }
+
+ [TestCategory("CanvasPathGeometry - Color")]
+ [TestMethod]
+ public void Test_CreateColor_3()
+ {
+ var color = CanvasPathGeometry.CreateColor("0.25 1 0.5 0.5");
+ Assert.AreEqual(127, color.A);
+ Assert.AreEqual(63, color.R);
+ Assert.AreEqual(255, color.G);
+ Assert.AreEqual(127, color.B);
+ }
+
+ [TestCategory("CanvasPathGeometry - Color")]
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CreateColor_4()
+ {
+ var color1 = CanvasPathGeometry.CreateColor("XYZXYZ");
+ }
+
+ [TestCategory("CanvasPathGeometry - Color")]
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CreateColor_5()
+ {
+ var color1 = CanvasPathGeometry.CreateColor("FFF");
+ }
+
+ [TestCategory("CanvasPathGeometry - Color")]
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CreateColor_6()
+ {
+ var color1 = CanvasPathGeometry.CreateColor("F112233");
+ }
+
+ [TestCategory("CanvasPathGeometry - Color")]
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CreateColor_7()
+ {
+ var color1 = CanvasPathGeometry.CreateColor("4 5 6 7");
+ }
+
+ [TestCategory("CanvasPathGeometry - Color")]
+ [TestMethod]
+ public void Test_CreateColor_8()
+ {
+ var color = CanvasPathGeometry.CreateColor(new Vector4(4, 5, 6, 7));
+ Assert.AreEqual(255, color.A);
+ Assert.AreEqual(255, color.R);
+ Assert.AreEqual(255, color.G);
+ Assert.AreEqual(255, color.B);
+ }
+
+ [TestCategory("CanvasPathGeometry - Color")]
+ [TestMethod]
+ public void Test_CreateColor_9()
+ {
+ var color = CanvasPathGeometry.CreateColor(new Vector4(4, 0.25f, 6, 0.5f));
+ Assert.AreEqual(255, color.R);
+ Assert.AreEqual(63, color.G);
+ Assert.AreEqual(255, color.B);
+ Assert.AreEqual(127, color.A);
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasStrokeStyle")]
+ [TestMethod]
+ public void Test_CanvasStrokeStyle_1()
+ {
+ var style = CanvasPathGeometry.CreateStrokeStyle("CSS DS 1 LJ1 ML 0.5 DO4 SC 0 EC1 DC3 TB1CDS 2 2 0 2 1 3");
+ Assert.AreEqual(true, style.LineJoin == CanvasLineJoin.Bevel);
+ Assert.AreEqual(true, style.MiterLimit == 0.5f);
+ Assert.AreEqual(true, style.StartCap == CanvasCapStyle.Flat);
+ Assert.AreEqual(true, style.EndCap == CanvasCapStyle.Square);
+ Assert.AreEqual(true, style.TransformBehavior == CanvasStrokeTransformBehavior.Fixed);
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasStrokeStyle")]
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasStrokeStyle_2()
+ {
+ // Invalid data
+ var style = CanvasPathGeometry.CreateStrokeStyle("CDS DS 1 LJ1 ML 0.5 DO4 SC 0 EC1 DC3 TB1CDS 2 2 0 2 1 3");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasStrokeStyle")]
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasStrokeStyle_3()
+ {
+ // Multiple CanvasStrokeStyles in the same data
+ var style = CanvasPathGeometry.CreateStrokeStyle("CSS DS 1 LJ1 ML 0.5 DO4 SC 0 EC1 DC3 TB1CDS 2 2 0 2 1 3 CSS DS 1 LJ1 ML 0.5 DO4 SC 0 EC1 DC3 TB1");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasStrokeStyle")]
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasStrokeStyle_4()
+ {
+ // Extra characters in the data
+ var style = CanvasPathGeometry.CreateStrokeStyle("CSS DS 1 LJ1 ML 0.5 DO4 SC 0 EC1 DC3 TB1CDS 2 2 0 2 1 3 12 23 34");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasStroke")]
+ [UITestMethod]
+ public void Test_CanvasStroke_1()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var stroke = CanvasPathGeometry.CreateStroke(device, "ST 4.5 LG M 0 0 Z80 80 S 0.00 #ffff0000, 0.5 #ff00ff00, 0.99 #ff0000ff CSS DS 2 LJ1 Do 2 SC 1 EC 2 CDS 2 2 0 2 1 3");
+ Assert.AreEqual(true, stroke.Width == 4.5f);
+ Assert.AreEqual(true, stroke.Style.LineJoin == CanvasLineJoin.Bevel);
+ Assert.AreEqual(true, stroke.Style.StartCap == CanvasCapStyle.Square);
+ Assert.AreEqual(true, stroke.Style.EndCap == CanvasCapStyle.Round);
+ Assert.AreEqual(true, stroke.Style.TransformBehavior == CanvasStrokeTransformBehavior.Normal);
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasStroke")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasStroke_2()
+ {
+ // Invalid data
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var stroke = CanvasPathGeometry.CreateStroke(device, "ST 4.5 6 LG M 0 0 Z80 80 S 0.00 #ffff0000, 0.5 #ff00ff00, 0.99 #ff0000ff CSS DS 2 Do 2 SC 1 EC 2 CDS 2 2 0 2 1 3");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasStroke")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasStroke_3()
+ {
+ // Multiple CanvasStrokes in the same data
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var stroke = CanvasPathGeometry.CreateStroke(device, "ST 4.5 LG M 0 0 Z80 80 S 0.00 #ffff0000, 0.5 #ff00ff00, 0.99 #ff0000ff CSS DS 2 Do 2 SC 1 EC 2 CDS 2 2 0 2 1 3 ST 4.5 LG M 0 0 Z80 80 S 0.00 #ffff0000, 0.5 #ff00ff00, 0.99 #ff0000ff CSS DS 2 Do 2 SC 1 EC 2 CDS 2 2 0 2 1 3");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasStroke")]
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasStroke_4()
+ {
+ // Extra characters in the stroke data
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var stroke = CanvasPathGeometry.CreateStroke(device, "ST 4.5 LG M 0 0 Z80 80 S 0.00 #ffff0000, 0.5 #ff00ff00, 0.99 #ff0000ff CSS DS 2 Do 2 SC 1 EC 2 CDS 2 2 0 2 1 3 A 10");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasBrush")]
+ [UITestMethod]
+ public void Test_CanvasBrush()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var solidbrush = CanvasPathGeometry.CreateBrush(device, "SC #FF1233AA O .75");
+ Assert.IsNotNull(solidbrush, "Could not create SolidColorBrush instance.");
+ var linearbrush = CanvasPathGeometry.CreateBrush(device, "LG M 0 80 Z0 0 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.6 #fff58535, 1.00 #fff9af41");
+ Assert.IsNotNull(linearbrush, "Could not create LinearGradientBrush instance.");
+ var radialbrush = CanvasPathGeometry.CreateBrush(device, "RG 40 60 320 400 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.72 #fff58535, 1.00 #fff9af41");
+ Assert.IsNotNull(radialbrush, "Could not create RadialGradientBrush instance.");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasBrush")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasBrush_SolidBrush_1()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var solidbrush = CanvasPathGeometry.CreateBrush(device, "SC 3 #FF1233AA O .75");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasBrush")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasBrush_SolidBrush_2()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var solidbrush = CanvasPathGeometry.CreateBrush(device, "SC #FF1233AA O .75 SC #FF1233AA O .75");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasBrush")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasBrush_SolidBrush_3()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var solidbrush = CanvasPathGeometry.CreateBrush(device, "SC #FF1233AA O .75 23 35");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasBrush")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasBrush_LinearGradientBrush_1()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var linearbrush = CanvasPathGeometry.CreateBrush(device, "LG M 0 8 0 Z0 0 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.6 #fff58535, 1.00 #fff9af41");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasBrush")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasBrush_LinearGradientBrush_2()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var linearbrush = CanvasPathGeometry.CreateBrush(device, "LG M 0 80 Z0 0 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.6 #fff58535, 1.00 #fff9af41 LG M 0 80 Z0 0 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.6 #fff58535, 1.00 #fff9af41");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasBrush")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasBrush_LinearGradientBrush_3()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var linearbrush = CanvasPathGeometry.CreateBrush(device, "LG M 0 80 Z0 0 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.6 #fff58535, 1.00 #fff9af41 3.0");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasBrush")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasBrush_RadialGradientBrush_1()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var radialbrush = CanvasPathGeometry.CreateBrush(device, "RG 4 0 6 0 320 400 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.72 #fff58535, 1.00 #fff9af41");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasBrush")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasBrush_RadialGradientBrush_2()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var radialbrush = CanvasPathGeometry.CreateBrush(device, "RG 40 60 320 400 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.72 #fff58535, 1.00 #fff9af41 RG 40 60 320 400 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.72 #fff58535, 1.00 #fff9af41");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasBrush")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasBrush_RadialGradientBrush_3()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var radialbrush = CanvasPathGeometry.CreateBrush(device, "RG 40 60 320 400 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.72 #fff58535, 1.00 #fff9af41 5.0");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasBrush")]
+ [UITestMethod]
+ public void Test_CanvasBrush_Squircle()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ var squircle = CanvasPathGeometry.CreateSquircle(device, 10, 10, 200, 200, 130, 130);
+ Assert.IsNotNull(squircle, "Could not create Squircle CanvasGeometry");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasGeometry")]
+ [UITestMethod]
+ public void Test_CanvasGeometry_1()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ const string sample = "F1 M 331.341,81.975 L 398.766,218.593 L 549.533,240.500 L 440.437,346.842 L 466.191,497.000 L 331.341,426.105 L 196.491,497.000 L 222.245,346.842 L 113.150,240.500 L 263.916,218.593 L 331.341,81.975 Z";
+
+ var geometry = CanvasPathGeometry.CreateGeometry(device, sample);
+ Assert.IsNotNull(geometry, "Unable to create CanvasGeometry!");
+
+ geometry = CanvasPathGeometry.CreateGeometry(sample);
+ Assert.IsNotNull(geometry, "Unable to create CanvasGeometry!");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasGeometry")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasGeometry_2()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ const string sample = "F1 M 331.341,81.975 L 398.766,218.593 L 549.533,240.500 L 440.437,346.842 L 466.191,497.000 L 331.341,426.105 L 196.491,497.000 L 222.245,346.842 L 113.150,240.500 L 263.916,218.593 L 331.341,81.975 Z";
+
+ var geometry = CanvasPathGeometry.CreateGeometry(device, $"{sample} {sample}");
+ }
+
+ [TestCategory("CanvasPathGeometry - CanvasGeometry")]
+ [UITestMethod]
+ [ExpectedException(typeof(ArgumentException))]
+ public void Test_CanvasGeometry_3()
+ {
+ CanvasDevice device = new CanvasDevice();
+ Assert.IsNotNull(device, "Could not create CanvasDevice instance.");
+
+ const string sample = "F1 M 331.341,81.975 L 398.766,218.593 L 549.533,240.500 L 440.437,346.842 L 466.191,497.000 L 331.341,426.105 L 196.491,497.000 L 222.245,346.842 L 113.150,240.500 L 263.916,218.593 L 331.341,81.975 Z S A M P L E E R R O R";
+
+ var geometry = CanvasPathGeometry.CreateGeometry(device, sample);
+ }
+ }
+}
diff --git a/UnitTests/UnitTests.UWP/Geometry/Test_RegexFactory.cs b/UnitTests/UnitTests.UWP/Geometry/Test_RegexFactory.cs
new file mode 100644
index 00000000000..17a7fe33359
--- /dev/null
+++ b/UnitTests/UnitTests.UWP/Geometry/Test_RegexFactory.cs
@@ -0,0 +1,141 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry.Core;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace UnitTests.UWP.Geometry
+{
+ [TestClass]
+ public class Test_RegexFactory
+ {
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_Color()
+ {
+ Assert.AreEqual(true, RegexFactory.ColorRegex.IsMatch("#FF0000"));
+ Assert.AreEqual(false, RegexFactory.ColorRegex.IsMatch("#FL000"));
+ Assert.AreEqual(true, RegexFactory.ColorRegex.IsMatch("FF0000"));
+ Assert.AreEqual(true, RegexFactory.ColorRegex.IsMatch("FFFF00A7"));
+ Assert.AreEqual(false, RegexFactory.ColorRegex.IsMatch("GHFF00T7"));
+ Assert.AreEqual(true, RegexFactory.ColorRegex.IsMatch("#FFFF00A7"));
+ Assert.AreEqual(false, RegexFactory.ColorRegex.IsMatch("#GHIJ00K7"));
+ Assert.AreEqual(true, RegexFactory.ColorRegex.IsMatch("0.9333333, 0.3176471, 0.1411765, 1"));
+ Assert.AreEqual(true, RegexFactory.ColorRegex.IsMatch("0.9333333 0.3176471 0.1411765 1"));
+ }
+
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_SolidBrush()
+ {
+ Assert.AreEqual(true, RegexFactory.CanvasBrushRegex.IsMatch("SC #FFAABBCC"));
+ Assert.AreEqual(true, RegexFactory.CanvasBrushRegex.IsMatch("SC FF1233AA O .75"));
+ Assert.AreEqual(false, RegexFactory.CanvasBrushRegex.IsMatch("SC 5 FF1233AA O .75"));
+ Assert.AreEqual(true, RegexFactory.CanvasBrushRegex.IsMatch("SC 0.1 0.3 0.4 0.5"));
+ Assert.AreEqual(true, RegexFactory.CanvasBrushRegex.IsMatch("SC 0.1 0.3 0.4 0.5 O 0.9"));
+ }
+
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_LinearGradientBrush()
+ {
+ Assert.AreEqual(true, RegexFactory.CanvasBrushRegex.IsMatch("LG M 0 80 Z0 0 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.72 #fff58535, 1.00 #fff9af41"));
+ Assert.AreEqual(true, RegexFactory.CanvasBrushRegex.IsMatch("LG M 0 80 Z0 0 O 0.75 A 1 E 2 S 0.00 #ffee5124, 0.3 #fff05627, 0.75 #fff58535, 1.00 #fff9af41"));
+ Assert.AreEqual(false, RegexFactory.CanvasBrushRegex.IsMatch("LG M 0 8 0 Z0 0 O 0.75 A 1 E 2 S 0.00 #ffee5124, 0.3 #fff05627, 0.75 #fff58535, 1.00 #fff9af41"));
+ }
+
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_LinearGradientBrushHdr()
+ {
+ Assert.AreEqual(true, RegexFactory.CanvasBrushRegex.IsMatch("LH M 0 80 Z0 0 P1 R1 S 0.00 0.9333333, 0.3176471, 0.1411765, 1, 0.18 0.9411765, 0.3372549, 0.1529412, 1, 0.26 0.945098, 0.3568628, 0.1607843, 1, 0.72 0.9607843, 0.5215687, 0.2078431, 1, 1.00 0.9764706, 0.6862745, 0.254902, 1"));
+ Assert.AreEqual(false, RegexFactory.CanvasBrushRegex.IsMatch("LH M 0 80 Z0 0 P1 R10 S 0.00 0.9333333, 0.3176471, 0.1411765, 1, 0.18 0.9411765, 0.3372549, 0.1529412, 1, 0.26 0.945098, 0.3568628, 0.1607843, 1, 0.72 0.9607843, 0.5215687, 0.2078431, 1, 1.00 0.9764706, 0.6862745, 0.254902, 1"));
+ }
+
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_RadialGradientBrush()
+ {
+ Assert.AreEqual(true, RegexFactory.CanvasBrushRegex.IsMatch("RG 40 60 320 400 S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.72 #fff58535, 1.00 #fff9af41"));
+ Assert.AreEqual(true, RegexFactory.CanvasBrushRegex.IsMatch("RG 40 60 320 400 A 1 B2 E 2 S 0.00 #ffee5124, 0.3 #fff05627, 0.75 #fff58535, 1.00 #fff9af41"));
+ Assert.AreEqual(false, RegexFactory.CanvasBrushRegex.IsMatch("RG 40 60 3 20 400 A 1 B2 E 2 S 0.00 #ffee5124, 0.3 #fff05627, 0.75 #fff58535, 1.00 #fff9af41"));
+ Assert.AreEqual(false, RegexFactory.CanvasBrushRegex.IsMatch("RG 40 60 320 400 A 1 B7 E 2 S 0.00 #ffee5124, 0.3 #fff05627, 0.75 #fff58535, 1.00 #fff9af41"));
+ }
+
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_RadialGradientBrushHdr()
+ {
+ Assert.AreEqual(true, RegexFactory.CanvasBrushRegex.IsMatch("RH 400 400 320 320 O 0.94 A 1 E2 S 0.00 0.9333333, 0.3176471, 0.1411765, 1, 0.18 0.9411765, 0.3372549, 0.1529412, 1, 0.26 0.945098, 0.3568628, 0.1607843, 1, 0.72 0.9607843, 0.5215687, 0.2078431, 1, 1.00 0.9764706, 0.6862745, 0.254902, 1"));
+ Assert.AreEqual(false, RegexFactory.CanvasBrushRegex.IsMatch("RH 400 40 0 320 3 20 O 0.94 A 1 E2 S 0.00 0.9333333, 0.3176471, 0.1411765, 1, 0.18 0.9411765, 0.3372549, 0.1529412, 1, 0.26 0.945098, 0.3568628, 0.1607843, 1, 0.72 0.9607843, 0.5215687, 0.2078431, 1, 1.00 0.9764706, 0.6862745, 0.254902, 1"));
+ }
+
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_GradientStop()
+ {
+ Assert.AreEqual(true, RegexFactory.GradientStopRegex.IsMatch("S 0.00 #ffee5124, 0.18 #fff05627, 0.26 #fff15b29, 0.72 #fff58535, 1.00 #fff9af41"));
+ Assert.AreEqual(true, RegexFactory.GradientStopRegex.IsMatch("S 0.00 #ffee5124 0.18 #fff05627 0.26 #fff15b29 0.72 #fff58535 1.00 #fff9af41"));
+ Assert.AreEqual(false, RegexFactory.GradientStopRegex.IsMatch("S #ffee5124 0.18 #fff05627 0.26 #fff15b29 0.72 #fff58535 1.00 #fff9af41"));
+ }
+
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_GradientStopHdr()
+ {
+ Assert.AreEqual(true, RegexFactory.GradientStopHdrRegex.IsMatch("S 0.00 0.9333333, 0.3176471, 0.1411765, 1, 0.18 0.9411765, 0.3372549, 0.1529412, 1, 0.26 0.945098, 0.3568628, 0.1607843, 1, 0.72 0.9607843, 0.5215687, 0.2078431, 1, 1.00 0.9764706, 0.6862745, 0.254902, 1"));
+ Assert.AreEqual(true, RegexFactory.GradientStopHdrRegex.IsMatch("S 0.00 0.9333333 0.3176471 0.1411765 1 0.18 0.9411765 0.3372549 0.1529412 1 0.26 0.945098 0.3568628 0.1607843 1 0.72 0.9607843 0.5215687 0.2078431 1 1.00 0.9764706 0.6862745 0.254902 1"));
+ Assert.AreEqual(false, RegexFactory.GradientStopHdrRegex.IsMatch("ST 0.00 0.9333333 0.3176471 0.1411765 1 0.18 0.9411765 0.3372549 0.1529412 1 0.26 0.945098 0.3568628 0.1607843 1 0.72 0.9607843 0.5215687 0.2078431 1 1.00 0.9764706 0.6862745 0.254902 1"));
+ }
+
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_CanvasStrokeStyle()
+ {
+ Assert.AreEqual(true, RegexFactory.CanvasStrokeStyleRegex.IsMatch("CSS CDS 2 2 0 2 1 3"));
+ Assert.AreEqual(true, RegexFactory.CanvasStrokeStyleRegex.IsMatch("CSS"));
+ Assert.AreEqual(false, RegexFactory.CanvasStrokeStyleRegex.IsMatch("CSD CDS 2 2 0 2 1 3"));
+ Assert.AreEqual(true, RegexFactory.CanvasStrokeStyleRegex.IsMatch("CSS DS2 DO2 SC 1 EC 2"));
+ Assert.AreEqual(true, RegexFactory.CanvasStrokeStyleRegex.IsMatch("CSS DS 1 LJ1 ML 0.5 DO4 SC 0 EC1 DC3 TB1CDS 2 2 0 2 1 3"));
+ Assert.AreEqual(false, RegexFactory.CanvasStrokeStyleRegex.IsMatch("CSD DSD 1 LJ1 ML 0.5 DO4 SC 0 EC1 DC3 TB1CDS 2 2 0 2 1 3"));
+ }
+
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_CanvasStroke()
+ {
+ Assert.AreEqual(true, RegexFactory.CanvasStrokeRegex.IsMatch("ST 4.5 LG M 0 0 Z80 80 S 0.00 #ffff0000, 0.5 #ff00ff00, 0.99 0000ff CSS DS2 Do2 SC 1 EC 2 CDS 2 2 0 2 1 3"));
+ Assert.AreEqual(false, RegexFactory.CanvasStrokeRegex.IsMatch("ST 4 5 LG M 0 0 Z80 80 S 0.00 #ffff0000, 0.5 #ff00ff00, 0.99 0000ff CSS DS2 Do2 SC 1 EC 2 CDS 2 2 0 2 1 3"));
+ Assert.AreEqual(false, RegexFactory.CanvasStrokeRegex.IsMatch("ST 2 3 SC #ff0000 5"));
+ Assert.AreEqual(true, RegexFactory.CanvasStrokeRegex.IsMatch("ST 2 SC #ff0000"));
+ }
+
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_CustomDashAttribute()
+ {
+ Assert.AreEqual(true, RegexFactory.CustomDashAttributeRegex.IsMatch("2 2 0 2 1 3"));
+ Assert.AreEqual(false, RegexFactory.CustomDashAttributeRegex.IsMatch("2"));
+ }
+
+ [TestCategory("Geometry - RegexFactory")]
+ [TestMethod]
+ public void Test_Regex_CanvasGeometry()
+ {
+ const string sample1 = "F0 M 656.500,400.500 C 656.500,350.637 598.572,307.493 514.292,286.708 C 493.507,202.428 450.363,144.500 400.500,144.500 C 350.637,144.500 307.493,202.428 286.708,286.708 C 202.428,307.493 144.500,350.637 144.500,400.500 C 144.500,450.363 202.428,493.507 286.708,514.292 C 307.493,598.572 350.637,656.500 400.500,656.500 C 450.363,656.500 493.507,598.572 514.292,514.292 C 598.572,493.507 656.500,450.363 656.500,400.500 ZM 581.519,219.481 C 546.261,184.222 474.793,194.676 400.500,239.574 C 326.207,194.676 254.739,184.222 219.481,219.481 C 184.222,254.739 194.676,326.207 239.574,400.500 C 194.676,474.792 184.222,546.261 219.481,581.519 C 254.739,616.778 326.207,606.324 400.500,561.426 C 474.793,606.324 546.261,616.778 581.519,581.519 C 616.778,546.261 606.324,474.792 561.426,400.500 C 606.324,326.207 616.778,254.739 581.519,219.481 ZU 112.5 112.5 570 570 36 36";
+ const string sample2 = "F1 M 331.341,81.975 L 398.766,218.593 L 549.533,240.500 L 440.437,346.842 L 466.191,497.000 L 331.341,426.105 L 196.491,497.000 L 222.245,346.842 L 113.150,240.500 L 263.916,218.593 L 331.341,81.975 Z";
+ const string sample3 = "F1 M 545.497,397.058 C 454.492,512.882 286.824,533.003 171.000,441.998 C 78.340,369.194 62.244,235.059 135.048,142.400 C 193.291,68.272 300.599,55.395 374.726,113.639 C 434.028,160.233 444.330,246.079 397.736,305.381 C 360.460,352.823 291.783,361.064 244.341,323.788 C 206.388,293.968 199.795,239.026 229.616,201.073 C 253.472,170.711 297.425,165.436 327.788,189.293 C 352.078,208.378 356.297,243.540 337.212,267.830 C 321.944,287.262 293.814,290.638 274.382,275.370 C 258.836,263.155 256.136,240.651 268.350,225.106 C 278.122,212.669 296.125,210.509 308.562,220.280 C 318.511,228.098 320.239,242.500 312.422,252.449";
+ const string sample4 = "F1 M 311.717,332.110 C 285.669,332.110 264.552,310.994 264.552,284.945 C 264.552,258.897 285.669,237.781 311.717,237.781 C 337.765,237.781 358.881,258.897 358.881,284.945 C 358.881,310.994 337.765,332.110 311.717,332.110 Z M 505.712,232.846 C 634.939,203.833 411.705,171.395 371.772,213.383 C 411.705,171.395 311.872,-30.889 311.872,92.013 C 311.872,-30.889 212.038,171.395 251.972,213.383 C 212.038,171.395 -11.196,203.833 118.031,232.846 C -11.196,203.833 150.338,361.289 214.951,327.320 C 150.338,361.289 112.205,583.622 192.072,460.719 C 112.205,583.622 311.872,478.651 311.872,397.737 C 311.872,478.651 511.538,583.622 431.672,460.719 C 511.538,583.622 473.405,361.289 408.792,327.320 C 473.405,361.289 634.939,203.833 505.712,232.846 Z";
+ const string sample5 = "F1 MB 391.853,348.284 C 391.853,357.113 384.696,364.271 375.867,364.271 L 301.927,364.271 C 293.098,364.271 285.940,357.113 285.940,348.284 L 285.940,274.345 C 285.940,265.515 293.098,258.358 301.927,258.358 L 375.867,258.358 C 384.696,258.358 391.853,265.515 391.853,274.345 L 391.853,348.284 Z M 544.748,282.990 L 485.488,267.081 C 472.521,263.600 466.301,248.839 472.866,237.128 L 502.867,183.604 C 512.642,166.166 494.336,146.433 476.214,154.872 L 420.592,180.776 C 408.421,186.445 394.169,179.136 391.670,165.944 L 380.248,105.658 C 376.526,86.017 349.819,82.667 341.362,100.780 L 315.403,156.378 C 309.723,168.543 294.107,172.105 283.714,163.607 L 236.213,124.767 C 220.737,112.113 198.125,126.714 203.289,146.025 L 219.141,205.301 C 222.610,218.271 212.937,231.038 199.512,231.208 L 138.159,231.988 C 118.170,232.242 110.233,257.962 126.602,269.436 L 176.847,304.655 C 187.841,312.361 188.638,328.358 178.464,337.118 L 131.965,377.153 C 116.816,390.196 127.269,415.001 147.184,413.268 L 208.312,407.950 C 221.687,406.786 232.580,418.529 230.417,431.779 L 220.531,492.336 C 217.310,512.066 241.261,524.348 255.403,510.220 L 298.811,466.854 C 308.310,457.365 324.202,459.358 331.062,470.899 L 362.415,523.643 C 372.629,540.827 398.872,534.840 400.624,514.927 L 406.001,453.804 C 407.178,440.430 420.634,431.742 433.307,436.173 L 491.227,456.425 C 510.098,463.022 526.353,441.568 514.895,425.187 L 479.725,374.908 C 472.030,363.906 476.753,348.601 489.310,343.850 L 546.697,322.133 C 565.393,315.057 564.054,288.173 544.748,282.990 Z";
+ const string sample6 = "G1 N 331.341,81.975 Q 398.766,218.593 H 549.533,240.500 H 440.437,346.842 H 466.191,497.000 H 331.341,426.105 H 196.491,497.000 H 222.245,346.842 H 113.150,240.500 H 263.916,218.593 H 331.341,81.975 Z";
+
+ Assert.AreEqual(true, RegexFactory.CanvasGeometryRegex.IsMatch(sample1));
+ Assert.AreEqual(true, RegexFactory.CanvasGeometryRegex.IsMatch(sample2));
+ Assert.AreEqual(true, RegexFactory.CanvasGeometryRegex.IsMatch(sample3));
+ Assert.AreEqual(true, RegexFactory.CanvasGeometryRegex.IsMatch(sample4));
+ Assert.AreEqual(true, RegexFactory.CanvasGeometryRegex.IsMatch(sample5));
+ Assert.AreEqual(false, RegexFactory.CanvasGeometryRegex.IsMatch(sample6));
+ }
+ }
+}
diff --git a/UnitTests/UnitTests.UWP/Geometry/Test_Utils.cs b/UnitTests/UnitTests.UWP/Geometry/Test_Utils.cs
new file mode 100644
index 00000000000..80dde13c7aa
--- /dev/null
+++ b/UnitTests/UnitTests.UWP/Geometry/Test_Utils.cs
@@ -0,0 +1,606 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+using Windows.Foundation;
+using Windows.UI;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Media;
+using Microsoft.Toolkit.Uwp.UI.Media.Geometry;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
+
+namespace UnitTests.UWP.Geometry
+{
+ [TestClass]
+ public class Test_Utils
+ {
+ [TestCategory("Geometry - Double")]
+ [TestMethod]
+ public void Test_Utils_Double_IsCloseTo()
+ {
+ double d1 = 4.000000000000000001d;
+ double d2 = 4.000000000000000002d;
+ Assert.AreEqual(true, d1.IsCloseTo(d2));
+
+ double d3 = 5.0001d;
+ double d4 = 5.00000000000000001d;
+ Assert.AreEqual(false, d3.IsCloseTo(d4));
+ }
+
+ [TestCategory("Geometry - Double")]
+ [TestMethod]
+ public void Test_Utils_Double_IsLessThan()
+ {
+ double d1 = 4.000010000000000001d;
+ double d2 = 4.000020000000000002d;
+ Assert.AreEqual(true, d1.IsLessThan(d2));
+
+ double d3 = double.PositiveInfinity;
+ double d4 = 5.00000000000000001d;
+ Assert.AreEqual(false, d3.IsLessThan(d4));
+ }
+
+ [TestCategory("Geometry - Double")]
+ [TestMethod]
+ public void Test_Utils_Double_IsGreaterThan()
+ {
+ double d1 = 4.000010000000000001d;
+ double d2 = 4.000020000000000002d;
+ Assert.AreEqual(false, d1.IsGreaterThan(d2));
+
+ double d3 = double.PositiveInfinity;
+ double d4 = 5.00000000000000001d;
+ Assert.AreEqual(true, d3.IsGreaterThan(d4));
+ }
+
+ [TestCategory("Geometry - Double")]
+ [TestMethod]
+ public void Test_Utils_Double_IsOne()
+ {
+ double d1 = 0.9999999999d;
+ Assert.AreEqual(false, d1.IsOne());
+
+ double d3 = 1.00000000000000001d;
+ Assert.AreEqual(true, d3.IsOne());
+ }
+
+ [TestCategory("Geometry - Double")]
+ [TestMethod]
+ public void Test_Utils_Double_IsZero()
+ {
+ double d1 = 0.0000000009d;
+ Assert.AreEqual(false, d1.IsZero());
+
+ double d3 = 0.00000000000000001e-275d;
+ Assert.AreEqual(false, d3.IsZero());
+ }
+
+ [TestCategory("Geometry - Float")]
+ [TestMethod]
+ public void Test_Utils_Float_IsCloseTo()
+ {
+ float f1 = 4.000000000000000001f;
+ float f2 = 4.000000000000000002f;
+ Assert.AreEqual(true, f1.IsCloseTo(f2));
+
+ float f3 = 5.0001f;
+ float f4 = 5.00000000000000001f;
+ Assert.AreEqual(false, f3.IsCloseTo(f4));
+ }
+
+ [TestCategory("Geometry - Float")]
+ [TestMethod]
+ public void Test_Utils_Float_IsLessThan()
+ {
+ float f1 = 4.000010000000000001f;
+ float f2 = 4.000020000000000002f;
+ Assert.AreEqual(true, f1.IsLessThan(f2));
+
+ float f3 = float.PositiveInfinity;
+ float f4 = 5.00000000000000001f;
+ Assert.AreEqual(false, f3.IsLessThan(f4));
+ }
+
+ [TestCategory("Geometry - Float")]
+ [TestMethod]
+ public void Test_Utils_Float_IsGreaterThan()
+ {
+ float f1 = 4.000010000000000001f;
+ float f2 = 4.000020000000000002f;
+ Assert.AreEqual(false, f1.IsGreaterThan(f2));
+
+ float f3 = float.PositiveInfinity;
+ float f4 = 5.00000000000000001f;
+ Assert.AreEqual(true, f3.IsGreaterThan(f4));
+ }
+
+ [TestCategory("Geometry - Float")]
+ [TestMethod]
+ public void Test_Utils_Float_IsOne()
+ {
+ float f1 = 0.99999f;
+ Assert.AreEqual(false, f1.IsOne());
+
+ float f2 = 1.00000000000000001f;
+ Assert.AreEqual(true, f2.IsOne());
+ }
+
+ [TestCategory("Geometry - Float")]
+ [TestMethod]
+ public void Test_Utils_Float_IsZero()
+ {
+ float f1 = 0.000009f;
+ Assert.AreEqual(false, f1.IsZero());
+
+ float f2 = 0.0000000000000000001e-27f;
+ Assert.AreEqual(true, f2.IsZero());
+ }
+
+ [TestCategory("Geometry - Point")]
+ [TestMethod]
+ public void Test_Utils_Point_IsCloseTo()
+ {
+ Point p1 = new Point(4.000000000000000001f, 6.000000000000000001f);
+ Point p2 = new Point(4.000000000000000002f, 6.000000000000000002f);
+ Assert.AreEqual(true, p1.IsCloseTo(p2));
+
+ Point p3 = new Point(4, 5.0001f);
+ Point p4 = new Point(4, 5.00000000000000001f);
+ Assert.AreEqual(false, p3.IsCloseTo(p4));
+ }
+
+ [TestCategory("Geometry - Size")]
+ [TestMethod]
+ public void Test_Utils_Size_IsCloseTo()
+ {
+ Size s1 = new Size(14.000000000000000001f, 26.000000000000000001f);
+ Size s2 = new Size(14.000000000000000002f, 26.000000000000000002f);
+ Assert.AreEqual(true, s1.IsCloseTo(s2));
+
+ Size s3 = new Size(54, 55.0001f);
+ Size s4 = new Size(54, 55.00000000000000001f);
+ Assert.AreEqual(false, s3.IsCloseTo(s4));
+ }
+
+ [TestCategory("Geometry - Rect")]
+ [TestMethod]
+ public void Test_Utils_Rect_IsCloseTo()
+ {
+ Rect r1 = new Rect(4.000000000000000001f, 6.000000000000000001f, 54, 55.0001f);
+ Rect r2 = new Rect(4.000000000000000002f, 6.000000000000000002f, 54, 55.0001f);
+ Assert.AreEqual(true, r1.IsCloseTo(r2));
+
+ Rect r3 = new Rect(4, 5.0001f, 54, 55.0001f);
+ Rect r4 = new Rect(4, 5.00000000000000001f, 54, 55.0001f);
+ Assert.AreEqual(false, r3.IsCloseTo(r4));
+ }
+
+ [TestCategory("Geometry - Double")]
+ [TestMethod]
+ public void Test_Utils_Double_RoundLayoutValue()
+ {
+ Assert.AreEqual(6d, Utils.RoundLayoutValue(5.5d, 1.000000000000000000001d));
+
+ Assert.AreEqual(3.5d, Utils.RoundLayoutValue(3.4d, 2d));
+
+ Assert.AreEqual(3.4d, Utils.RoundLayoutValue(3.4d, 0.0d));
+ }
+
+ [TestCategory("Geometry - Float")]
+ [TestMethod]
+ public void Test_Utils_Float_Lerp()
+ {
+ Assert.AreEqual(3f, 2f.Lerp(4f, 0.5f));
+
+ Assert.AreEqual(9.6f, 6f.Lerp(10f, 0.9f));
+ }
+
+ [TestCategory("Geometry - Thickness")]
+ [TestMethod]
+ public void Test_Utils_Thickness_IsValid()
+ {
+ Assert.AreEqual(true, new Thickness(5).IsValid(false, false, false, false));
+
+ Assert.AreEqual(true, new Thickness(5, -5, 5, -5).IsValid(true, false, false, false));
+ Assert.AreEqual(false, new Thickness(5, -5, 5, -5).IsValid(false, false, false, false));
+
+ Assert.AreEqual(false, new Thickness(5, double.NaN, 5, 5).IsValid(false, false, false, false));
+ Assert.AreEqual(true, new Thickness(5, double.NaN, 5, 5).IsValid(false, true, false, false));
+
+ Assert.AreEqual(true, new Thickness(5, double.PositiveInfinity, double.NegativeInfinity, 5).IsValid(true, false, true, true));
+ Assert.AreEqual(false, new Thickness(5, double.PositiveInfinity, double.NegativeInfinity, 5).IsValid(false, false, false, true));
+ Assert.AreEqual(false, new Thickness(5, double.PositiveInfinity, double.NegativeInfinity, 5).IsValid(false, false, true, false));
+ Assert.AreEqual(false, new Thickness(5, double.PositiveInfinity, double.NegativeInfinity, 5).IsValid(false, false, false, false));
+ }
+
+ [TestCategory("Geometry - Thickness")]
+ [TestMethod]
+ public void Test_Utils_Thickness_CollapseThickness()
+ {
+ var th = new Thickness(10, 20, 30, 40);
+ Assert.AreEqual(new Size(40, 60), th.CollapseThickness());
+ }
+
+ [TestCategory("Geometry - Thickness")]
+ [TestMethod]
+ public void Test_Utils_Thickness_IsZero()
+ {
+ var th1 = new Thickness(10, 20, 30, 40);
+ Assert.AreEqual(false, th1.IsZero());
+
+ var th2 = new Thickness(0, 0, 0, 0.0e-279);
+ Assert.AreEqual(true, th2.IsZero());
+ }
+
+ [TestCategory("Geometry - Thickness")]
+ [TestMethod]
+ public void Test_Utils_Thickness_IsUniform()
+ {
+ var th1 = new Thickness(10, 20, 30, 40);
+ Assert.AreEqual(false, th1.IsUniform());
+
+ var th2 = new Thickness(10, 10, 10, 10);
+ Assert.AreEqual(true, th2.IsUniform());
+ }
+
+ [TestCategory("Geometry - Thickness")]
+ [TestMethod]
+ public void Test_Utils_Thickness_ToVector4()
+ {
+ var th1 = new Thickness(10, 20, 30, 40);
+ Assert.AreEqual(new Vector4(10, 20, 30, 40), th1.ToVector4());
+ }
+
+ [TestCategory("Geometry - Thickness")]
+ [TestMethod]
+ public void Test_Utils_Thickness_ToAbsVector4()
+ {
+ var th1 = new Thickness(10, -20, 30, 40);
+ Assert.AreEqual(new Vector4(10, 20, 30, 40), th1.ToAbsVector4());
+ }
+
+ [TestCategory("Geometry - Thickness")]
+ [TestMethod]
+ public void Test_Utils_Thickness_GetOffset()
+ {
+ var th1 = new Thickness(10, -20, 30, 40);
+ Assert.AreEqual(new Vector2(10, -20), th1.GetOffset());
+ }
+
+ [TestCategory("Geometry - CornerRadius")]
+ [TestMethod]
+ public void Test_Utils_CornerRadius_IsZero()
+ {
+ var cr1 = new CornerRadius(10, 20, 30, 40);
+ Assert.AreEqual(false, cr1.IsZero());
+
+ var cr2 = new CornerRadius(0, 0, 0, 0.0e-279);
+ Assert.AreEqual(true, cr2.IsZero());
+ }
+
+ [TestCategory("Geometry - CornerRadius")]
+ [TestMethod]
+ public void Test_Utils_CornerRadius_IsUniform()
+ {
+ var cr1 = new CornerRadius(10, 20, 30, 40);
+ Assert.AreEqual(false, cr1.IsUniform());
+
+ var cr2 = new CornerRadius(10, 10, 10, 10);
+ Assert.AreEqual(true, cr2.IsUniform());
+ }
+
+ [TestCategory("Geometry - CornerRadius")]
+ [TestMethod]
+ public void Test_Utils_ConvertToValidCornerValue()
+ {
+ Assert.AreEqual(0d, Utils.ConvertToValidCornerValue(double.NaN));
+ Assert.AreEqual(0d, Utils.ConvertToValidCornerValue(double.PositiveInfinity));
+ Assert.AreEqual(0d, Utils.ConvertToValidCornerValue(double.NegativeInfinity));
+ Assert.AreEqual(0d, Utils.ConvertToValidCornerValue(-2d));
+ Assert.AreEqual(2d, Utils.ConvertToValidCornerValue(2d));
+ }
+
+ [TestCategory("Geometry - CornerRadius")]
+ [TestMethod]
+ public void Test_Utils_CornerRadius_ToVector4()
+ {
+ var cr1 = new CornerRadius(10, 20, 30, 40);
+ Assert.AreEqual(new Vector4(10, 20, 30, 40), cr1.ToVector4());
+ }
+
+ [TestCategory("Geometry - Rect")]
+ [TestMethod]
+ public void Test_Utils_Rect_Deflate()
+ {
+ var rect1 = new Rect(10, 20, 30, 40);
+ Assert.AreEqual(new Rect(20, 30, 10, 20), rect1.Deflate(new Thickness(10)));
+
+ var rect2 = new Rect(10, 20, 30, 40);
+ Assert.AreEqual(new Rect(30, 40, 0, 0), rect2.Deflate(new Thickness(20)));
+
+ var rect3 = new Rect(10, 20, 30, 40);
+ Assert.AreEqual(new Rect(0, 10, 50, 60), rect3.Deflate(new Thickness(-10)));
+ }
+
+ [TestCategory("Geometry - Rect")]
+ [TestMethod]
+ public void Test_Utils_Rect_Inflate()
+ {
+ var rect1 = new Rect(10, 20, 30, 40);
+ Assert.AreEqual(new Rect(0, 10, 50, 60), rect1.Inflate(new Thickness(10)));
+
+ var rect2 = new Rect(10, 20, 30, 40);
+ Assert.AreEqual(new Rect(-10, 0, 70, 80), rect2.Inflate(new Thickness(20)));
+
+ var rect3 = new Rect(10, 20, 30, 40);
+ Assert.AreEqual(new Rect(30, 40, 0, 0), rect3.Inflate(new Thickness(-20)));
+ }
+
+ [TestCategory("Geometry - Brush")]
+ [UITestMethod]
+ public void Test_Utils_IsOpaqueSolidColorBrush()
+ {
+ Assert.AreEqual(true, new SolidColorBrush(Color.FromArgb(255, 255, 0, 0)).IsOpaqueSolidColorBrush());
+ Assert.AreEqual(false, new SolidColorBrush(Color.FromArgb(155, 255, 0, 0)).IsOpaqueSolidColorBrush());
+ Assert.AreEqual(false, new LinearGradientBrush().IsOpaqueSolidColorBrush());
+ }
+
+ [TestCategory("Geometry - Brush")]
+ [UITestMethod]
+ public void Test_Utils_Brush_IsEqualTo()
+ {
+ var scb1 = new SolidColorBrush(Colors.Red);
+ var scb2 = new SolidColorBrush(Color.FromArgb(255, 255, 0, 0));
+ var scb3 = new SolidColorBrush(Color.FromArgb(255, 128, 128, 255));
+ Assert.AreEqual(true, scb1.IsEqualTo(scb2));
+ Assert.AreEqual(false, scb1.IsEqualTo(scb3));
+ Assert.AreEqual(false, scb2.IsEqualTo(scb3));
+
+ var gColl1 = new GradientStopCollection
+ {
+ new GradientStop() { Color = Colors.Red, Offset = 0 },
+ new GradientStop() { Color = Colors.Green, Offset = 0.5 },
+ new GradientStop() { Color = Colors.Blue, Offset = 1 }
+ };
+ var lgb1 = new LinearGradientBrush()
+ {
+ StartPoint = new Point(0, 0),
+ EndPoint = new Point(0, 1),
+ GradientStops = gColl1
+ };
+
+ var gColl2 = new GradientStopCollection
+ {
+ new GradientStop() { Color = Colors.Red, Offset = 0 },
+ new GradientStop() { Color = Colors.Green, Offset = 0.5 },
+ new GradientStop() { Color = Colors.Blue, Offset = 1 }
+ };
+ var lgb2 = new LinearGradientBrush()
+ {
+ StartPoint = new Point(0, 0),
+ EndPoint = new Point(0, 1),
+ GradientStops = gColl2
+ };
+
+ var gColl3 = new GradientStopCollection
+ {
+ new GradientStop() { Color = Colors.Red, Offset = 0 },
+ new GradientStop() { Color = Colors.Green, Offset = 0.5 },
+ new GradientStop() { Color = Colors.Yellow, Offset = 1 }
+ };
+ var lgb3 = new LinearGradientBrush()
+ {
+ StartPoint = new Point(0, 0),
+ EndPoint = new Point(0, 1),
+ GradientStops = gColl3
+ };
+
+ Assert.AreEqual(true, lgb1.IsEqualTo(lgb2));
+ Assert.AreEqual(false, lgb1.IsEqualTo(lgb3));
+ Assert.AreEqual(false, lgb2.IsEqualTo(lgb3));
+ }
+
+ [TestCategory("Geometry - Uri")]
+ [TestMethod]
+ public void Test_Utils_Uri_IsEqualTo()
+ {
+ var u1 = new Uri("https://www.microsoft.com/uwp");
+ var u2 = new Uri("https://www.microsoft.com/");
+ var u3 = new Uri(u2, new Uri("uwp", UriKind.Relative));
+
+ Assert.AreEqual(true, u1.IsEqualTo(u3));
+ Assert.AreEqual(false, u1.IsEqualTo(u2));
+ Assert.AreEqual(false, u2.IsEqualTo(u3));
+ }
+
+ [TestCategory("Geometry - Vector2")]
+ [TestMethod]
+ public void Test_Utils_Vector2_Reflect()
+ {
+ var a = new Vector2(10, 10);
+ var b = Vector2.Zero;
+ var c = new Vector2(-10, -10);
+
+ Assert.AreEqual(c, Utils.Reflect(a, b));
+ }
+
+ [TestCategory("Geometry - Vector2")]
+ [TestMethod]
+ public void Test_Utils_Vector2_ToVector3()
+ {
+ var a = new Vector2(10, 10);
+ var b = new Vector3(10, 10, 0);
+
+ Assert.AreEqual(b, a.ToVector3());
+ }
+
+ [TestCategory("Geometry - Vector4")]
+ [TestMethod]
+ public void Test_Utils_Vector4_IsZero()
+ {
+ var a = new Vector4(10, 10, 0, 0);
+ var b = new Vector4(0);
+
+ Assert.AreEqual(false, a.IsZero());
+ Assert.AreEqual(true, b.IsZero());
+ }
+
+ [TestCategory("Geometry - Vector4")]
+ [TestMethod]
+ public void Test_Utils_Vector4_Collapse()
+ {
+ var a = new Vector4(10, 20, 30, 40);
+ var b = new Vector2(40, 60);
+
+ Assert.AreEqual(b, a.Collapse());
+ }
+
+ [TestCategory("Geometry - Vector4")]
+ [TestMethod]
+ public void Test_Utils_Vector4_ToSize()
+ {
+ var a = new Vector4(10, 20, 30, 40);
+ var b = new Size(40, 60);
+
+ Assert.AreEqual(b, a.ToSize());
+ }
+
+ [TestCategory("Geometry - Vector4")]
+ [TestMethod]
+ public void Test_Utils_Vector4_Thickness()
+ {
+ var a = new Vector4(10, 20, 30, 40);
+ var b = new Thickness(10, 20, 30, 40);
+
+ Assert.AreEqual(b, a.ToThickness());
+ }
+
+ [TestCategory("Geometry - Vector4")]
+ [TestMethod]
+ public void Test_Utils_Vector4_ToCornerRadius()
+ {
+ var a = new Vector4(10, 20, 30, 40);
+ var b = new CornerRadius(10, 20, 30, 40);
+
+ Assert.AreEqual(b, a.ToCornerRadius());
+ }
+
+ [TestCategory("Geometry - Color")]
+ [TestMethod]
+ public void Test_Utils_Color_Lerp()
+ {
+ var colorFrom = Colors.Red;
+ var colorTo = Colors.Green;
+
+ var expectedColor = CanvasPathGeometry.CreateColor("#FF7F4000");
+
+ Assert.AreEqual(expectedColor, colorFrom.Lerp(colorTo, 0.5f));
+ }
+
+ [TestCategory("Geometry - Color")]
+ [TestMethod]
+ public void Test_Utils_Color_DarkerBy()
+ {
+ var colorFrom = Colors.Red;
+ var expectedColor = CanvasPathGeometry.CreateColor("#FF7F0000");
+
+ Assert.AreEqual(expectedColor, colorFrom.DarkerBy(0.5f));
+ }
+
+ [TestCategory("Geometry - Color")]
+ [TestMethod]
+ public void Test_Utils_Color_LighterBy()
+ {
+ var colorFrom = Colors.Red;
+ var expectedColor = CanvasPathGeometry.CreateColor("#FFFF7F7F");
+
+ Assert.AreEqual(expectedColor, colorFrom.LighterBy(0.5f));
+ }
+
+ [TestCategory("Geometry - Point")]
+ [TestMethod]
+ public void Test_Utils_Point_ToVector3()
+ {
+ var point = new Point(10.5d, 15.5d);
+ var vecPoint = new Vector3(10.5f, 15.5f, 0f);
+
+ Assert.AreEqual(vecPoint, point.ToVector3());
+ }
+
+ [TestCategory("Geometry - Rect")]
+ [TestMethod]
+ public void Test_Utils_Rect_GetOptimumSize()
+ {
+ var srcWidth = 500;
+ var srcHeight = 400;
+ var destWidth = 800;
+ var destHeight = 800;
+
+ Assert.AreEqual(new Rect(0, 0, 500, 400), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.None, AlignmentX.Left, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, 200, 500, 400), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.None, AlignmentX.Left, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, 400, 500, 400), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.None, AlignmentX.Left, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(150, 0, 500, 400), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.None, AlignmentX.Center, AlignmentY.Top));
+ Assert.AreEqual(new Rect(150, 200, 500, 400), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.None, AlignmentX.Center, AlignmentY.Center));
+ Assert.AreEqual(new Rect(150, 400, 500, 400), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.None, AlignmentX.Center, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(300, 0, 500, 400), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.None, AlignmentX.Right, AlignmentY.Top));
+ Assert.AreEqual(new Rect(300, 200, 500, 400), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.None, AlignmentX.Right, AlignmentY.Center));
+ Assert.AreEqual(new Rect(300, 400, 500, 400), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.None, AlignmentX.Right, AlignmentY.Bottom));
+
+ Assert.AreEqual(new Rect(0, 0, 800, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Fill, AlignmentX.Left, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, 0, 800, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Fill, AlignmentX.Left, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, 0, 800, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Fill, AlignmentX.Left, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(0, 0, 800, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Fill, AlignmentX.Center, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, 0, 800, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Fill, AlignmentX.Center, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, 0, 800, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Fill, AlignmentX.Center, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(0, 0, 800, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Fill, AlignmentX.Right, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, 0, 800, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Fill, AlignmentX.Right, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, 0, 800, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Fill, AlignmentX.Right, AlignmentY.Bottom));
+
+ Assert.AreEqual(new Rect(0, 0, 800, 640), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Left, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, 80, 800, 640), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Left, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, 160, 800, 640), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Left, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(0, 0, 800, 640), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Center, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, 80, 800, 640), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Center, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, 160, 800, 640), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Center, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(0, 0, 800, 640), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Right, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, 80, 800, 640), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Right, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, 160, 800, 640), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Right, AlignmentY.Bottom));
+
+ Assert.AreEqual(new Rect(0, 0, 1000, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Left, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, 0, 1000, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Left, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, 0, 1000, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Left, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(-100, 0, 1000, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Center, AlignmentY.Top));
+ Assert.AreEqual(new Rect(-100, 0, 1000, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Center, AlignmentY.Center));
+ Assert.AreEqual(new Rect(-100, 0, 1000, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Center, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(-200, 0, 1000, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Right, AlignmentY.Top));
+ Assert.AreEqual(new Rect(-200, 0, 1000, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Right, AlignmentY.Center));
+ Assert.AreEqual(new Rect(-200, 0, 1000, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Right, AlignmentY.Bottom));
+
+ srcWidth = 400;
+ srcHeight = 500;
+
+ Assert.AreEqual(new Rect(0, 0, 640, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Left, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, 0, 640, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Left, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, 0, 640, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Left, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(80, 0, 640, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Center, AlignmentY.Top));
+ Assert.AreEqual(new Rect(80, 0, 640, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Center, AlignmentY.Center));
+ Assert.AreEqual(new Rect(80, 0, 640, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Center, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(160, 0, 640, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Right, AlignmentY.Top));
+ Assert.AreEqual(new Rect(160, 0, 640, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Right, AlignmentY.Center));
+ Assert.AreEqual(new Rect(160, 0, 640, 800), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.Uniform, AlignmentX.Right, AlignmentY.Bottom));
+
+ Assert.AreEqual(new Rect(0, 0, 800, 1000), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Left, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, -100, 800, 1000), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Left, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, -200, 800, 1000), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Left, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(0, 0, 800, 1000), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Center, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, -100, 800, 1000), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Center, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, -200, 800, 1000), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Center, AlignmentY.Bottom));
+ Assert.AreEqual(new Rect(0, 0, 800, 1000), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Right, AlignmentY.Top));
+ Assert.AreEqual(new Rect(0, -100, 800, 1000), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Right, AlignmentY.Center));
+ Assert.AreEqual(new Rect(0, -200, 800, 1000), Utils.GetOptimumSize(srcWidth, srcHeight, destWidth, destHeight, Stretch.UniformToFill, AlignmentX.Right, AlignmentY.Bottom));
+ }
+ }
+}
\ No newline at end of file
diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
index 0c9df38b3d9..ce29975833b 100644
--- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
+++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
@@ -129,6 +129,9 @@
4.3.0
+
+ 1.25.0
+
@@ -147,6 +150,9 @@
+
+
+
@@ -252,10 +258,18 @@
{b1e850ff-dde6-44d5-a830-34250e97a687}Microsoft.Toolkit.Uwp.Connectivity
+
+ {b24a296c-b3eb-4e06-a64e-74ac2d1acc91}
+ Microsoft.Toolkit.Uwp.UI.Animations
+ {e9faabfb-d726-42c1-83c1-cb46a29fea81}Microsoft.Toolkit.Uwp.UI.Controls
+
+ {75f9ee44-3efa-47bc-aedd-351b9834a0af}
+ Microsoft.Toolkit.Uwp.UI.Media
+ {3dd8aa7c-3569-4e51-992f-0c2257e8878e}Microsoft.Toolkit.Uwp.UI