diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml
index 2f86a5785..aa1f5d6a6 100644
--- a/.github/workflows/build-pr.yml
+++ b/.github/workflows/build-pr.yml
@@ -77,13 +77,15 @@ jobs:
}
}
- # Only proceed when his project has not been built yet
- if (-not ($processedProjects -contains $file)) {
- if ([string]::IsNullOrEmpty($projectToBuild)) {
- Write-Output "::warning:: Found no csproj for file $file"
- }
- else {
- $projectToBuild = (Resolve-Path -Path $projectToBuild -Relative).Replace("\", "/")
+ if (-not [string]::IsNullOrEmpty($projectToBuild)) {
+ $projectToBuild = (Resolve-Path -Path $projectToBuild -Relative).Replace("\", "/")
+
+ # Only proceed when this project has not been built yet
+ if (-not ($processedProjects -contains $projectToBuild)) {
+ Write-Output "::notice:: $projectToBuild is not in processed builds yet"
+
+ $processedProjects += $projectToBuild
+ Write-Output "::notice:: Added $projectToBuild to processed builds"
if ($excluded_projects -contains $projectToBuild) {
Write-Output "::notice:: Skipping build for excluded project: $projectToBuild"
@@ -95,7 +97,6 @@ jobs:
Write-Output "::group:: Building $projectToBuild"
dotnet build $projectToBuild
- $processedProjects += $projectToBuild
if ($LASTEXITCODE -gt 0) {
Write-Output "::error:: Build failed for $projectToBuild"
@@ -116,6 +117,9 @@ jobs:
}
}
}
+ else {
+ Write-Output "::warning:: Found no csproj for file $file"
+ }
}
if ($failedProjectCount -gt 0) {
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/README.md b/8.0/SkiaSharp/SkiaSharpDemos/README.md
new file mode 100644
index 000000000..4d7b087da
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/README.md
@@ -0,0 +1,20 @@
+---
+name: .NET MAUI - SkiaSharp
+description: "This sample demonstrates the use of SkiaSharp in a .NET MAUI app."
+page_type: sample
+languages:
+- csharp
+- xaml
+products:
+- xamarin
+- dotnetmaui
+urlFragment: skiasharpmaui-demos
+---
+
+# .NET MAUI and SkiaSharp
+
+SkiaSharp is a 2D graphics system for .NET and C# powered by the open-source Skia graphics engine that's used extensively in Google products. You can use SkiaSharp in your .NET Multi-platform App UI (.NET MAUI) apps to draw 2D vector graphics, bitmaps, and text.
+
+This sample demonstrates the use of SkiaSharp in a .NET MAUI app.
+
+[!INCLUDE [Install SkiaSharp](../includes/install-skiasharp.md)]
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos.sln b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos.sln
new file mode 100644
index 000000000..a6f4df013
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos.sln
@@ -0,0 +1,26 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.8.34330.188
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharpDemos", "SkiaSharpDemos\SkiaSharpDemos.csproj", "{63B3F69B-D30F-480D-B914-7390F2D1F52E}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {63B3F69B-D30F-480D-B914-7390F2D1F52E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {63B3F69B-D30F-480D-B914-7390F2D1F52E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {63B3F69B-D30F-480D-B914-7390F2D1F52E}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {63B3F69B-D30F-480D-B914-7390F2D1F52E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {63B3F69B-D30F-480D-B914-7390F2D1F52E}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {9E97E4D6-1629-4123-95D7-1B7108AD826A}
+ EndGlobalSection
+EndGlobal
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/App.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/App.xaml
new file mode 100644
index 000000000..d104d1b59
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/App.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/App.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/App.xaml.cs
new file mode 100644
index 000000000..e1f5b9ecb
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/App.xaml.cs
@@ -0,0 +1,12 @@
+namespace SkiaSharpDemos;
+
+public partial class App : Application
+{
+ public App()
+ {
+ InitializeComponent();
+
+ MainPage = new AppShell();
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/AppShell.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/AppShell.xaml
new file mode 100644
index 000000000..29ba7be20
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/AppShell.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/AppShell.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/AppShell.xaml.cs
new file mode 100644
index 000000000..2d844197d
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/AppShell.xaml.cs
@@ -0,0 +1,10 @@
+namespace SkiaSharpDemos;
+
+public partial class AppShell : Shell
+{
+ public AppShell()
+ {
+ InitializeComponent();
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/BasePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/BasePage.cs
new file mode 100644
index 000000000..e41f9b6ee
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/BasePage.cs
@@ -0,0 +1,20 @@
+using System.Windows.Input;
+
+namespace SkiaSharpDemos
+{
+ public class BasePage : ContentPage
+ {
+ public ICommand NavigateCommand { get; private set; }
+
+ public BasePage()
+ {
+ NavigateCommand = new Command(async (Type pageType) =>
+ {
+ Page page = (Page)Activator.CreateInstance(pageType);
+ await Navigation.PushAsync(page);
+ });
+
+ BindingContext = this;
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BasicBitmapsPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BasicBitmapsPage.cs
new file mode 100644
index 000000000..2ab0f9a77
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BasicBitmapsPage.cs
@@ -0,0 +1,128 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Basics
+{
+ public class BasicBitmapsPage : ContentPage
+ {
+ SKCanvasView canvasView;
+
+ HttpClient httpClient = new HttpClient();
+
+ SKBitmap webBitmap;
+ SKBitmap resourceBitmap;
+ SKBitmap libraryBitmap;
+
+ public BasicBitmapsPage()
+ {
+ Title = "Basic Bitmaps";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ // Load resource bitmap
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.monkey.png"))
+ {
+ resourceBitmap = SKBitmap.Decode(stream);
+ }
+
+ // Add tap gesture recognizer
+ TapGestureRecognizer tapRecognizer = new TapGestureRecognizer();
+ tapRecognizer.Tapped += async (sender, args) =>
+ {
+ // Load bitmap from photo library
+ FileResult photo = await MediaPicker.Default.PickPhotoAsync();
+ if (photo != null)
+ {
+ using (Stream stream = await photo.OpenReadAsync())
+ {
+ if (stream != null)
+ {
+ libraryBitmap = SKBitmap.Decode(stream);
+ canvasView.InvalidateSurface();
+ }
+ }
+ }
+ };
+ canvasView.GestureRecognizers.Add(tapRecognizer);
+ }
+
+ protected override async void OnAppearing()
+ {
+ base.OnAppearing();
+
+ // Load web bitmap.
+ string url = "https://learn.microsoft.com/en-us/dotnet/maui/media/what-is-maui/maui-overview.png";
+
+ try
+ {
+ using (Stream stream = await httpClient.GetStreamAsync(url))
+ using (MemoryStream memStream = new MemoryStream())
+ {
+ await stream.CopyToAsync(memStream);
+ memStream.Seek(0, SeekOrigin.Begin);
+
+ webBitmap = SKBitmap.Decode(memStream);
+ canvasView.InvalidateSurface();
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ if (webBitmap != null)
+ {
+ float x = (info.Width - webBitmap.Width) / 2;
+ float y = (info.Height / 3 - webBitmap.Height) / 2;
+ canvas.DrawBitmap(webBitmap, x, y);
+ }
+
+ if (resourceBitmap != null)
+ {
+ canvas.DrawBitmap(resourceBitmap,
+ new SKRect(0, info.Height / 3, info.Width, 2 * info.Height / 3));
+ }
+
+ if (libraryBitmap != null)
+ {
+ float scale = Math.Min((float)info.Width / libraryBitmap.Width,
+ info.Height / 3f / libraryBitmap.Height);
+
+ float left = (info.Width - scale * libraryBitmap.Width) / 2;
+ float top = (info.Height / 3 - scale * libraryBitmap.Height) / 2;
+ float right = left + scale * libraryBitmap.Width;
+ float bottom = top + scale * libraryBitmap.Height;
+ SKRect rect = new SKRect(left, top, right, bottom);
+ rect.Offset(0, 2 * info.Height / 3);
+
+ canvas.DrawBitmap(libraryBitmap, rect);
+ }
+ else
+ {
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Color = SKColors.Blue;
+ paint.TextAlign = SKTextAlign.Center;
+ paint.TextSize = 48;
+
+ canvas.DrawText("Tap to load bitmap", info.Width / 2, 5 * info.Height / 6, paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BasicsMenuPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BasicsMenuPage.xaml
new file mode 100644
index 000000000..6e35536c2
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BasicsMenuPage.xaml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BasicsMenuPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BasicsMenuPage.xaml.cs
new file mode 100644
index 000000000..7bb6d4e8e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BasicsMenuPage.xaml.cs
@@ -0,0 +1,9 @@
+namespace SkiaSharpDemos.Basics;
+
+public partial class BasicsMenuPage : BasePage
+{
+ public BasicsMenuPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BitmapDissolvePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BitmapDissolvePage.xaml
new file mode 100644
index 000000000..f248fd91f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BitmapDissolvePage.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BitmapDissolvePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BitmapDissolvePage.xaml.cs
new file mode 100644
index 000000000..0d62f97f2
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/BitmapDissolvePage.xaml.cs
@@ -0,0 +1,64 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using System.Reflection;
+
+namespace SkiaSharpDemos.Basics;
+
+public partial class BitmapDissolvePage : ContentPage
+{
+ SKBitmap bitmap1;
+ SKBitmap bitmap2;
+
+ public BitmapDissolvePage()
+ {
+ InitializeComponent();
+
+ // Load two bitmaps
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.seatedmonkey.jpg"))
+ {
+ bitmap1 = SKBitmap.Decode(stream);
+ }
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.facepalm.jpg"))
+ {
+ bitmap2 = SKBitmap.Decode(stream);
+ }
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Find rectangle to fit bitmap
+ float scale = Math.Min((float)info.Width / bitmap1.Width,
+ (float)info.Height / bitmap1.Height);
+ SKRect rect = SKRect.Create(scale * bitmap1.Width,
+ scale * bitmap1.Height);
+ float x = (info.Width - rect.Width) / 2;
+ float y = (info.Height - rect.Height) / 2;
+ rect.Offset(x, y);
+
+ // Get progress value from Slider
+ float progress = (float)progressSlider.Value;
+
+ // Display two bitmaps with transparency
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Color = paint.Color.WithAlpha((byte)(0xFF * (1 - progress)));
+ canvas.DrawBitmap(bitmap1, rect, paint);
+
+ paint.Color = paint.Color.WithAlpha((byte)(0xFF * progress));
+ canvas.DrawBitmap(bitmap2, rect, paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/CodeMoreCodePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/CodeMoreCodePage.cs
new file mode 100644
index 000000000..c69b8301b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/CodeMoreCodePage.cs
@@ -0,0 +1,92 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+using System.Diagnostics;
+
+namespace SkiaSharpDemos.Basics
+{
+ public class CodeMoreCodePage : ContentPage
+ {
+ SKCanvasView canvasView;
+ bool isAnimating;
+ Stopwatch stopwatch = new Stopwatch();
+ double transparency;
+
+ public CodeMoreCodePage()
+ {
+ Title = "Code More Code";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+
+ isAnimating = true;
+ stopwatch.Start();
+ Dispatcher.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+
+ stopwatch.Stop();
+ isAnimating = false;
+ }
+
+ bool OnTimerTick()
+ {
+ const int duration = 5; // seconds
+ double progress = stopwatch.Elapsed.TotalSeconds % duration / duration;
+ transparency = 0.5 * (1 + Math.Sin(progress * 2 * Math.PI));
+ canvasView.InvalidateSurface();
+
+ return isAnimating;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ const string TEXT1 = "CODE";
+ const string TEXT2 = "MORE";
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Set text width to fit in width of canvas
+ paint.TextSize = 100;
+ float textWidth = paint.MeasureText(TEXT1);
+ paint.TextSize *= 0.9f * info.Width / textWidth;
+
+ // Center first text string
+ SKRect textBounds = new SKRect();
+ paint.MeasureText(TEXT1, ref textBounds);
+
+ float xText = info.Width / 2 - textBounds.MidX;
+ float yText = info.Height / 2 - textBounds.MidY;
+
+ paint.Color = SKColors.Blue.WithAlpha((byte)(0xFF * (1 - transparency)));
+ canvas.DrawText(TEXT1, xText, yText, paint);
+
+ // Center second text string
+ textBounds = new SKRect();
+ paint.MeasureText(TEXT2, ref textBounds);
+
+ xText = info.Width / 2 - textBounds.MidX;
+ yText = info.Height / 2 - textBounds.MidY;
+
+ paint.Color = SKColors.Blue.WithAlpha((byte)(0xFF * transparency));
+ canvas.DrawText(TEXT2, xText, yText, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/ColorExplorePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/ColorExplorePage.xaml
new file mode 100644
index 000000000..9a36ee38f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/ColorExplorePage.xaml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/ColorExplorePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/ColorExplorePage.xaml.cs
new file mode 100644
index 000000000..4c6d0b0d4
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/ColorExplorePage.xaml.cs
@@ -0,0 +1,45 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Basics;
+
+public partial class ColorExplorePage : ContentPage
+{
+ public ColorExplorePage()
+ {
+ InitializeComponent();
+
+ hueSlider.Value = 0;
+ saturationSlider.Value = 100;
+ lightnessSlider.Value = 50;
+ valueSlider.Value = 100;
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ hslCanvasView.InvalidateSurface();
+ hsvCanvasView.InvalidateSurface();
+ }
+
+ void OnHslCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKColor color = SKColor.FromHsl((float)hueSlider.Value,
+ (float)saturationSlider.Value,
+ (float)lightnessSlider.Value);
+ args.Surface.Canvas.Clear(color);
+
+ hslLabel.Text = String.Format(" RGB = {0:X2}-{1:X2}-{2:X2} ",
+ color.Red, color.Green, color.Blue);
+ }
+
+ void OnHsvCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKColor color = SKColor.FromHsv((float)hueSlider.Value,
+ (float)saturationSlider.Value,
+ (float)valueSlider.Value);
+ args.Surface.Canvas.Clear(color);
+
+ hsvLabel.Text = String.Format(" RGB = {0:X2}-{1:X2}-{2:X2} ",
+ color.Red, color.Green, color.Blue);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/EllipseFillPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/EllipseFillPage.xaml
new file mode 100644
index 000000000..6ce9e5a88
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/EllipseFillPage.xaml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/EllipseFillPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/EllipseFillPage.xaml.cs
new file mode 100644
index 000000000..c517309aa
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/EllipseFillPage.xaml.cs
@@ -0,0 +1,33 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Basics;
+
+public partial class EllipseFillPage : ContentPage
+{
+ public EllipseFillPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float strokeWidth = 50;
+ float xRadius = (info.Width - strokeWidth) / 2;
+ float yRadius = (info.Height - strokeWidth) / 2;
+
+ SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Blue,
+ StrokeWidth = strokeWidth
+ };
+ canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/ExpandingCirclesPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/ExpandingCirclesPage.cs
new file mode 100644
index 000000000..0b48f0038
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/ExpandingCirclesPage.cs
@@ -0,0 +1,79 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+using System.Diagnostics;
+
+namespace SkiaSharpDemos.Basics
+{
+ public class ExpandingCirclesPage : ContentPage
+ {
+ const double cycleTime = 1000; // in milliseconds
+
+ SKCanvasView canvasView;
+ Stopwatch stopwatch = new Stopwatch();
+ bool pageIsActive;
+ float t;
+ SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke
+ };
+
+ public ExpandingCirclesPage()
+ {
+ Title = "Expanding Circles";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ pageIsActive = true;
+ stopwatch.Start();
+
+ Dispatcher.StartTimer(TimeSpan.FromMicroseconds(33), () =>
+ {
+ t = (float)(stopwatch.Elapsed.TotalMilliseconds % cycleTime / cycleTime);
+ canvasView.InvalidateSurface();
+
+ if (!pageIsActive)
+ {
+ stopwatch.Stop();
+ }
+ return pageIsActive;
+ });
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ pageIsActive = false;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPoint center = new SKPoint(info.Width / 2, info.Height / 2);
+ float baseRadius = Math.Min(info.Width, info.Height) / 12;
+
+ for (int circle = 0; circle < 5; circle++)
+ {
+ float radius = baseRadius * (circle + t);
+
+ paint.StrokeWidth = baseRadius / 2 * (circle == 0 ? t : 1);
+ paint.Color = new SKColor(0, 0, 255,
+ (byte)(255 * (circle == 4 ? (1 - t) : 1)));
+
+ canvas.DrawCircle(center.X, center.Y, radius, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/FramedTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/FramedTextPage.cs
new file mode 100644
index 000000000..842cc571f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/FramedTextPage.cs
@@ -0,0 +1,72 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Basics
+{
+ public class FramedTextPage : ContentPage
+ {
+ public FramedTextPage()
+ {
+ Title = "Framed Text";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ string str = "Hello SkiaSharp!";
+
+ // Create an SKPaint object to display the text
+ SKPaint textPaint = new SKPaint
+ {
+ Color = SKColors.Chocolate
+ };
+
+ // Adjust TextSize property so text is 90% of screen width
+ float textWidth = textPaint.MeasureText(str);
+ textPaint.TextSize = 0.9f * info.Width * textPaint.TextSize / textWidth;
+
+ // Find the text bounds
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText(str, ref textBounds);
+
+ // Calculate offsets to center the text on the screen
+ float xText = info.Width / 2 - textBounds.MidX;
+ float yText = info.Height / 2 - textBounds.MidY;
+
+ // And draw the text
+ canvas.DrawText(str, xText, yText, textPaint);
+
+ // Create a new SKRect object for the frame around the text
+ SKRect frameRect = textBounds;
+ frameRect.Offset(xText, yText);
+ frameRect.Inflate(10, 10);
+
+ // Create an SKPaint object to display the frame
+ SKPaint framePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = 5,
+ Color = SKColors.Blue
+ };
+
+ // Draw one frame
+ canvas.DrawRoundRect(frameRect, 20, 20, framePaint);
+
+ // Inflate the frameRect and draw another
+ frameRect.Inflate(10, 10);
+ framePaint.Color = SKColors.DarkBlue;
+ canvas.DrawRoundRect(frameRect, 30, 30, framePaint);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/OutlinedTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/OutlinedTextPage.cs
new file mode 100644
index 000000000..f3b64100d
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/OutlinedTextPage.cs
@@ -0,0 +1,54 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Basics
+{
+ public class OutlinedTextPage : ContentPage
+ {
+ public OutlinedTextPage()
+ {
+ Title = "Outlined Text";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ string text = "OUTLINE";
+
+ // Create an SKPaint object to display the text
+ SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = 1,
+ FakeBoldText = true,
+ Color = SKColors.Blue
+ };
+
+ // Adjust TextSize property so text is 95% of screen width
+ float textWidth = textPaint.MeasureText(text);
+ textPaint.TextSize = 0.95f * info.Width * textPaint.TextSize / textWidth;
+
+ // Find the text bounds
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText(text, ref textBounds);
+
+ // Calculate offsets to center the text on the screen
+ float xText = info.Width / 2 - textBounds.MidX;
+ float yText = info.Height / 2 - textBounds.MidY;
+
+ // And draw the text
+ canvas.DrawText(text, xText, yText, textPaint);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/PulsatingEllipsePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/PulsatingEllipsePage.xaml
new file mode 100644
index 000000000..0ede24909
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/PulsatingEllipsePage.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/PulsatingEllipsePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/PulsatingEllipsePage.xaml.cs
new file mode 100644
index 000000000..0fd379ffa
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/PulsatingEllipsePage.xaml.cs
@@ -0,0 +1,72 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using System.Diagnostics;
+
+namespace SkiaSharpDemos.Basics;
+
+public partial class PulsatingEllipsePage : ContentPage
+{
+ Stopwatch stopwatch = new Stopwatch();
+ bool pageIsActive;
+ float scale; // ranges from 0 to 1 to 0
+
+ public PulsatingEllipsePage()
+ {
+ InitializeComponent();
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ pageIsActive = true;
+ AnimationLoop();
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ pageIsActive = false;
+ }
+
+ async Task AnimationLoop()
+ {
+ stopwatch.Start();
+
+ while (pageIsActive)
+ {
+ double cycleTime = slider.Value;
+ double t = stopwatch.Elapsed.TotalSeconds % cycleTime / cycleTime;
+ scale = (1 + (float)Math.Sin(2 * Math.PI * t)) / 2;
+ canvasView.InvalidateSurface();
+ await Task.Delay(TimeSpan.FromSeconds(1.0 / 30));
+ }
+ stopwatch.Stop();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float maxRadius = 0.75f * Math.Min(info.Width, info.Height) / 2;
+ float minRadius = 0.25f * maxRadius;
+
+ float xRadius = minRadius * scale + maxRadius * (1 - scale);
+ float yRadius = maxRadius * scale + minRadius * (1 - scale);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Blue;
+ paint.StrokeWidth = 50;
+ canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);
+
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = SKColors.SkyBlue;
+ canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/SimpleCirclePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/SimpleCirclePage.cs
new file mode 100644
index 000000000..fe929560b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/SimpleCirclePage.cs
@@ -0,0 +1,40 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui.Controls;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Basics
+{
+ public class SimpleCirclePage : ContentPage
+ {
+ public SimpleCirclePage()
+ {
+ Title = "Simple Circle";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = Colors.Red.ToSKColor(),
+ StrokeWidth = 25
+ };
+ canvas.DrawCircle(info.Width / 2, info.Height / 2, 100, paint);
+
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = SKColors.Blue;
+ canvas.DrawCircle(info.Width / 2, info.Height / 2, 100, paint);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/SurfaceSizePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/SurfaceSizePage.cs
new file mode 100644
index 000000000..fd3fd77ba
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/SurfaceSizePage.cs
@@ -0,0 +1,55 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Basics
+{
+ public class SurfaceSizePage : ContentPage
+ {
+ SKCanvasView canvasView;
+
+ public SurfaceSizePage()
+ {
+ Title = "Surface Size";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPaint paint = new SKPaint
+ {
+ Color = SKColors.Black,
+ TextSize = 40
+ };
+
+ float fontSpacing = paint.FontSpacing;
+ float x = 20; // left margin
+ float y = fontSpacing; // first baseline
+ float indent = 100;
+
+ canvas.DrawText("SKCanvasView Height and Width:", x, y, paint);
+ y += fontSpacing;
+ canvas.DrawText(String.Format("{0:F2} x {1:F2}",
+ canvasView.Width, canvasView.Height),
+ x + indent, y, paint);
+ y += fontSpacing * 2;
+ canvas.DrawText("SKCanvasView CanvasSize:", x, y, paint);
+ y += fontSpacing;
+ canvas.DrawText(canvasView.CanvasSize.ToString(), x + indent, y, paint);
+ y += fontSpacing * 2;
+ canvas.DrawText("SKImageInfo Size:", x, y, paint);
+ y += fontSpacing;
+ canvas.DrawText(info.Size.ToString(), x + indent, y, paint);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/TapToggleFillPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/TapToggleFillPage.xaml
new file mode 100644
index 000000000..f73781888
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/TapToggleFillPage.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/TapToggleFillPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/TapToggleFillPage.xaml.cs
new file mode 100644
index 000000000..d124d5004
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Basics/TapToggleFillPage.xaml.cs
@@ -0,0 +1,45 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Basics;
+
+public partial class TapToggleFillPage : ContentPage
+{
+ bool showFill = true;
+
+ public TapToggleFillPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnCanvasViewTapped(object sender, EventArgs args)
+ {
+ showFill ^= true;
+ (sender as SKCanvasView).InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = Colors.Red.ToSKColor(),
+ StrokeWidth = 50
+ };
+ canvas.DrawCircle(info.Width / 2, info.Height / 2, 100, paint);
+
+ if (showFill)
+ {
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = SKColors.Blue;
+ canvas.DrawCircle(info.Width / 2, info.Height / 2, 100, paint);
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/BitmapExtensions.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/BitmapExtensions.cs
new file mode 100644
index 000000000..7effe308d
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/BitmapExtensions.cs
@@ -0,0 +1,159 @@
+using SkiaSharp;
+using System.Reflection;
+
+namespace SkiaSharpDemos
+{
+ static class BitmapExtensions
+ {
+ public static SKBitmap LoadBitmapResource(Type type, string resourceID)
+ {
+ Assembly assembly = type.GetTypeInfo().Assembly;
+ using (Stream stream = assembly.GetManifestResourceStream(resourceID))
+ {
+ return SKBitmap.Decode(stream);
+ }
+ }
+
+ public static uint RgbaMakePixel(byte red, byte green, byte blue, byte alpha = 255)
+ {
+ return (uint)((alpha << 24) | (blue << 16) | (green << 8) | red);
+ }
+
+ public static void RgbaGetBytes(this uint pixel, out byte red, out byte green, out byte blue, out byte alpha)
+ {
+ red = (byte)pixel;
+ green = (byte)(pixel >> 8);
+ blue = (byte)(pixel >> 16);
+ alpha = (byte)(pixel >> 24);
+ }
+
+ public static uint BgraMakePixel(byte blue, byte green, byte red, byte alpha = 255)
+ {
+ return (uint)((alpha << 24) | (red << 16) | (green << 8) | blue);
+ }
+
+ public static void BgraGetBytes(this uint pixel, out byte blue, out byte green, out byte red, out byte alpha)
+ {
+ blue = (byte)pixel;
+ green = (byte)(pixel >> 8);
+ red = (byte)(pixel >> 16);
+ alpha = (byte)(pixel >> 24);
+ }
+
+ public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect dest,
+ BitmapStretch stretch,
+ BitmapAlignment horizontal = BitmapAlignment.Center,
+ BitmapAlignment vertical = BitmapAlignment.Center,
+ SKPaint paint = null)
+ {
+ if (stretch == BitmapStretch.Fill)
+ {
+ canvas.DrawBitmap(bitmap, dest, paint);
+ }
+ else
+ {
+ float scale = 1;
+ switch (stretch)
+ {
+ case BitmapStretch.None:
+ break;
+ case BitmapStretch.Uniform:
+ scale = Math.Min(dest.Width / bitmap.Width, dest.Height / bitmap.Height);
+ break;
+ case BitmapStretch.UniformToFill:
+ scale = Math.Max(dest.Width / bitmap.Width, dest.Height / bitmap.Height);
+ break;
+ }
+
+ SKRect display = CalculateDisplayRect(dest, scale * bitmap.Width, scale * bitmap.Height,
+ horizontal, vertical);
+ canvas.DrawBitmap(bitmap, display, paint);
+ }
+ }
+
+ public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect source, SKRect dest,
+ BitmapStretch stretch,
+ BitmapAlignment horizontal = BitmapAlignment.Center,
+ BitmapAlignment vertical = BitmapAlignment.Center,
+ SKPaint paint = null)
+ {
+ if (stretch == BitmapStretch.Fill)
+ {
+ canvas.DrawBitmap(bitmap, source, dest, paint);
+ }
+ else
+ {
+ float scale = 1;
+ switch (stretch)
+ {
+ case BitmapStretch.None:
+ break;
+ case BitmapStretch.Uniform:
+ scale = Math.Min(dest.Width / source.Width, dest.Height / source.Height);
+ break;
+ case BitmapStretch.UniformToFill:
+ scale = Math.Max(dest.Width / source.Width, dest.Height / source.Height);
+ break;
+ }
+
+ SKRect display = CalculateDisplayRect(dest, scale * source.Width, scale * source.Height,
+ horizontal, vertical);
+ canvas.DrawBitmap(bitmap, source, display, paint);
+ }
+ }
+
+ static SKRect CalculateDisplayRect(SKRect dest, float bmpWidth, float bmpHeight,
+ BitmapAlignment horizontal, BitmapAlignment vertical)
+ {
+ float x = 0;
+ float y = 0;
+
+ switch (horizontal)
+ {
+ case BitmapAlignment.Center:
+ x = (dest.Width - bmpWidth) / 2;
+ break;
+ case BitmapAlignment.Start:
+ break;
+ case BitmapAlignment.End:
+ x = dest.Width - bmpWidth;
+ break;
+ }
+
+ switch (vertical)
+ {
+ case BitmapAlignment.Center:
+ y = (dest.Height - bmpHeight) / 2;
+ break;
+ case BitmapAlignment.Start:
+ break;
+ case BitmapAlignment.End:
+ y = dest.Height - bmpHeight;
+ break;
+ }
+
+ x += dest.Left;
+ y += dest.Top;
+
+ return new SKRect(x, y, x + bmpWidth, y + bmpHeight);
+ }
+ }
+
+ public enum BitmapStretch
+ {
+ None,
+ Fill,
+ Uniform,
+ UniformToFill,
+ AspectFit = Uniform,
+ AspectFill = UniformToFill
+ }
+
+ public enum BitmapAlignment
+ {
+ Start,
+ Center,
+ End
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/AnimatedGifPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/AnimatedGifPage.xaml
new file mode 100644
index 000000000..fc703dbb4
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/AnimatedGifPage.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/AnimatedGifPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/AnimatedGifPage.xaml.cs
new file mode 100644
index 000000000..3f7a28645
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/AnimatedGifPage.xaml.cs
@@ -0,0 +1,126 @@
+using System.Diagnostics;
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Bitmaps;
+
+public partial class AnimatedGifPage : ContentPage
+{
+ SKBitmap[] bitmaps;
+ int[] durations;
+ int[] accumulatedDurations;
+ int totalDuration;
+
+ Stopwatch stopwatch = new Stopwatch();
+ bool isAnimating;
+
+ int currentFrame;
+
+ public AnimatedGifPage()
+ {
+ InitializeComponent();
+
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.newtons_cradle_animation_book_2.gif"))
+ using (SKManagedStream skStream = new SKManagedStream(stream))
+ using (SKCodec codec = SKCodec.Create(skStream))
+ {
+ // Get frame count and allocate bitmaps
+ int frameCount = codec.FrameCount;
+ bitmaps = new SKBitmap[frameCount];
+ durations = new int[frameCount];
+ accumulatedDurations = new int[frameCount];
+
+ // Note: There's also a RepetitionCount property of SKCodec not used here
+
+ // Loop through the frames
+ for (int frame = 0; frame < frameCount; frame++)
+ {
+ // From the FrameInfo collection, get the duration of each frame
+ durations[frame] = codec.FrameInfo[frame].Duration;
+
+ // Create a full-color bitmap for each frame
+ SKImageInfo imageInfo = new SKImageInfo(codec.Info.Width, codec.Info.Height);
+ bitmaps[frame] = new SKBitmap(imageInfo);
+
+ // Get the address of the pixels in that bitmap
+ IntPtr pointer = bitmaps[frame].GetPixels();
+
+ // Create an SKCodecOptions value to specify the frame
+ SKCodecOptions codecOptions = new SKCodecOptions(frame);
+
+ // Copy pixels from the frame into the bitmap
+ codec.GetPixels(imageInfo, pointer, codecOptions);
+ }
+
+ // Sum up the total duration
+ for (int frame = 0; frame < durations.Length; frame++)
+ {
+ totalDuration += durations[frame];
+ }
+
+ // Calculate the accumulated durations
+ for (int frame = 0; frame < durations.Length; frame++)
+ {
+ accumulatedDurations[frame] = durations[frame] +
+ (frame == 0 ? 0 : accumulatedDurations[frame - 1]);
+ }
+ }
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+
+ isAnimating = true;
+ stopwatch.Start();
+ Dispatcher.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+
+ stopwatch.Stop();
+ isAnimating = false;
+ }
+
+ bool OnTimerTick()
+ {
+ int msec = (int)(stopwatch.ElapsedMilliseconds % totalDuration);
+ int frame = 0;
+
+ // Find the frame based on the elapsed time
+ for (frame = 0; frame < accumulatedDurations.Length; frame++)
+ {
+ if (msec < accumulatedDurations[frame])
+ {
+ break;
+ }
+ }
+
+ // Save in a field and invalidate the SKCanvasView.
+ if (currentFrame != frame)
+ {
+ currentFrame = frame;
+ canvasView.InvalidateSurface();
+ }
+
+ return isAnimating;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear(SKColors.Black);
+
+ // Get the bitmap and center it
+ SKBitmap bitmap = bitmaps[currentFrame];
+ canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapFlipperPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapFlipperPage.xaml
new file mode 100644
index 000000000..67cf4fb51
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapFlipperPage.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapFlipperPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapFlipperPage.xaml.cs
new file mode 100644
index 000000000..7c19788f1
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapFlipperPage.xaml.cs
@@ -0,0 +1,56 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Bitmaps;
+
+public partial class BitmapFlipperPage : ContentPage
+{
+ SKBitmap bitmap =
+ BitmapExtensions.LoadBitmapResource(typeof(BitmapRotatorPage),
+ "SkiaSharpDemos.Media.seatedmonkey.jpg");
+
+ public BitmapFlipperPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+ canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);
+ }
+
+ void OnFlipVerticalClicked(object sender, EventArgs args)
+ {
+ SKBitmap flippedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);
+
+ using (SKCanvas canvas = new SKCanvas(flippedBitmap))
+ {
+ canvas.Clear();
+ canvas.Scale(-1, 1, bitmap.Width / 2, 0);
+ canvas.DrawBitmap(bitmap, new SKPoint());
+ }
+
+ bitmap = flippedBitmap;
+ canvasView.InvalidateSurface();
+ }
+
+ void OnFlipHorizontalClicked(object sender, EventArgs args)
+ {
+ SKBitmap flippedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);
+
+ using (SKCanvas canvas = new SKCanvas(flippedBitmap))
+ {
+ canvas.Clear();
+ canvas.Scale(1, -1, 0, bitmap.Height / 2);
+ canvas.DrawBitmap(bitmap, new SKPoint());
+ }
+
+ bitmap = flippedBitmap;
+ canvasView.InvalidateSurface();
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapRotatorPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapRotatorPage.xaml
new file mode 100644
index 000000000..62e9b4fcc
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapRotatorPage.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapRotatorPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapRotatorPage.xaml.cs
new file mode 100644
index 000000000..5060f8355
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapRotatorPage.xaml.cs
@@ -0,0 +1,53 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Bitmaps;
+
+public partial class BitmapRotatorPage : ContentPage
+{
+ static readonly SKBitmap originalBitmap =
+ BitmapExtensions.LoadBitmapResource(typeof(BitmapRotatorPage),
+ "SkiaSharpDemos.Media.banana.jpg");
+
+ SKBitmap rotatedBitmap = originalBitmap;
+
+ public BitmapRotatorPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+ canvas.DrawBitmap(rotatedBitmap, info.Rect, BitmapStretch.Uniform);
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ double angle = args.NewValue;
+ double radians = Math.PI * angle / 180;
+ float sine = (float)Math.Abs(Math.Sin(radians));
+ float cosine = (float)Math.Abs(Math.Cos(radians));
+ int originalWidth = originalBitmap.Width;
+ int originalHeight = originalBitmap.Height;
+ int rotatedWidth = (int)(cosine * originalWidth + sine * originalHeight);
+ int rotatedHeight = (int)(cosine * originalHeight + sine * originalWidth);
+
+ rotatedBitmap = new SKBitmap(rotatedWidth, rotatedHeight);
+
+ using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
+ {
+ canvas.Clear(SKColors.LightPink);
+ canvas.Translate(rotatedWidth / 2, rotatedHeight / 2);
+ canvas.RotateDegrees((float)angle);
+ canvas.Translate(-originalWidth / 2, -originalHeight / 2);
+ canvas.DrawBitmap(originalBitmap, new SKPoint());
+ }
+
+ canvasView.InvalidateSurface();
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapsMenuPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapsMenuPage.xaml
new file mode 100644
index 000000000..c3df7f938
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapsMenuPage.xaml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapsMenuPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapsMenuPage.xaml.cs
new file mode 100644
index 000000000..253e2aa59
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/BitmapsMenuPage.xaml.cs
@@ -0,0 +1,9 @@
+namespace SkiaSharpDemos.Bitmaps;
+
+public partial class BitmapsMenuPage : BasePage
+{
+ public BitmapsMenuPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ColorAdjustmentPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ColorAdjustmentPage.xaml
new file mode 100644
index 000000000..e68ffb165
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ColorAdjustmentPage.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ColorAdjustmentPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ColorAdjustmentPage.xaml.cs
new file mode 100644
index 000000000..0425a9588
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ColorAdjustmentPage.xaml.cs
@@ -0,0 +1,107 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Bitmaps;
+
+public partial class ColorAdjustmentPage : ContentPage
+{
+ SKBitmap srcBitmap =
+ BitmapExtensions.LoadBitmapResource(typeof(FillRectanglePage),
+ "SkiaSharpDemos.Media.banana.jpg");
+ SKBitmap dstBitmap;
+
+ public ColorAdjustmentPage()
+ {
+ InitializeComponent();
+
+ dstBitmap = new SKBitmap(srcBitmap.Width, srcBitmap.Height);
+ OnSliderValueChanged(null, null);
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ float hueAdjust = (float)hueSlider.Value;
+ hueLabel.Text = $"Hue Adjustment: {hueAdjust:F0}";
+
+ float saturationAdjust = (float)Math.Pow(2, saturationSlider.Value);
+ saturationLabel.Text = $"Saturation Adjustment: {saturationAdjust:F2}";
+
+ float luminosityAdjust = (float)Math.Pow(2, luminositySlider.Value);
+ luminosityLabel.Text = $"Luminosity Adjustment: {luminosityAdjust:F2}";
+
+ TransferPixels(hueAdjust, saturationAdjust, luminosityAdjust);
+ canvasView.InvalidateSurface();
+ }
+
+ unsafe void TransferPixels(float hueAdjust, float saturationAdjust, float luminosityAdjust)
+ {
+ byte* srcPtr = (byte*)srcBitmap.GetPixels().ToPointer();
+ byte* dstPtr = (byte*)dstBitmap.GetPixels().ToPointer();
+
+ int width = srcBitmap.Width; // same for both bitmaps
+ int height = srcBitmap.Height;
+
+ SKColorType typeOrg = srcBitmap.ColorType;
+ SKColorType typeAdj = dstBitmap.ColorType;
+
+ for (int row = 0; row < height; row++)
+ {
+ for (int col = 0; col < width; col++)
+ {
+ // Get color from original bitmap
+ byte byte1 = *srcPtr++; // red or blue
+ byte byte2 = *srcPtr++; // green
+ byte byte3 = *srcPtr++; // blue or red
+ byte byte4 = *srcPtr++; // alpha
+
+ SKColor color = new SKColor();
+
+ if (typeOrg == SKColorType.Rgba8888)
+ {
+ color = new SKColor(byte1, byte2, byte3, byte4);
+ }
+ else if (typeOrg == SKColorType.Bgra8888)
+ {
+ color = new SKColor(byte3, byte2, byte1, byte4);
+ }
+
+ // Get HSL components
+ color.ToHsl(out float hue, out float saturation, out float luminosity);
+
+ // Adjust HSL components based on adjustments
+ hue = (hue + hueAdjust) % 360;
+ saturation = Math.Max(0, Math.Min(100, saturationAdjust * saturation));
+ luminosity = Math.Max(0, Math.Min(100, luminosityAdjust * luminosity));
+
+ // Recreate color from HSL components
+ color = SKColor.FromHsl(hue, saturation, luminosity);
+
+ // Store the bytes in the adjusted bitmap
+ if (typeAdj == SKColorType.Rgba8888)
+ {
+ *dstPtr++ = color.Red;
+ *dstPtr++ = color.Green;
+ *dstPtr++ = color.Blue;
+ *dstPtr++ = color.Alpha;
+ }
+ else if (typeAdj == SKColorType.Bgra8888)
+ {
+ *dstPtr++ = color.Blue;
+ *dstPtr++ = color.Green;
+ *dstPtr++ = color.Red;
+ *dstPtr++ = color.Alpha;
+ }
+ }
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+ canvas.DrawBitmap(dstBitmap, info.Rect, BitmapStretch.Uniform);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/CroppingRectangle.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/CroppingRectangle.cs
new file mode 100644
index 000000000..9fa3e9085
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/CroppingRectangle.cs
@@ -0,0 +1,141 @@
+using SkiaSharp;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ class CroppingRectangle
+ {
+ const float MINIMUM = 10; // pixels width or height
+
+ SKRect maxRect; // generally the size of the bitmap
+ float? aspectRatio;
+
+ public CroppingRectangle(SKRect maxRect, float? aspectRatio = null)
+ {
+ this.maxRect = maxRect;
+ this.aspectRatio = aspectRatio;
+
+ // Set initial cropping rectangle
+ Rect = new SKRect(0.9f * maxRect.Left + 0.1f * maxRect.Right,
+ 0.9f * maxRect.Top + 0.1f * maxRect.Bottom,
+ 0.1f * maxRect.Left + 0.9f * maxRect.Right,
+ 0.1f * maxRect.Top + 0.9f * maxRect.Bottom);
+
+ // Adjust for aspect ratio
+ if (aspectRatio.HasValue)
+ {
+ SKRect rect = Rect;
+ float aspect = aspectRatio.Value;
+
+ if (rect.Width > aspect * rect.Height)
+ {
+ float width = aspect * rect.Height;
+ rect.Left = (maxRect.Width - width) / 2;
+ rect.Right = rect.Left + width;
+ }
+ else
+ {
+ float height = rect.Width / aspect;
+ rect.Top = (maxRect.Height - height) / 2;
+ rect.Bottom = rect.Top + height;
+ }
+
+ Rect = rect;
+ }
+ }
+
+ public SKRect Rect { get; set; }
+
+ public SKPoint[] Corners
+ {
+ get
+ {
+ return new SKPoint[]
+ {
+ new SKPoint(Rect.Left, Rect.Top),
+ new SKPoint(Rect.Right, Rect.Top),
+ new SKPoint(Rect.Right, Rect.Bottom),
+ new SKPoint(Rect.Left, Rect.Bottom)
+ };
+ }
+ }
+
+ public int HitTest(SKPoint point, float radius)
+ {
+ SKPoint[] corners = Corners;
+
+ for (int index = 0; index < corners.Length; index++)
+ {
+ SKPoint diff = point - corners[index];
+
+ if ((float)Math.Sqrt(diff.X * diff.X + diff.Y * diff.Y) < radius)
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+
+ public void MoveCorner(int index, SKPoint point)
+ {
+ SKRect rect = Rect;
+
+ switch (index)
+ {
+ case 0: // upper-left
+ rect.Left = Math.Min(Math.Max(point.X, maxRect.Left), rect.Right - MINIMUM);
+ rect.Top = Math.Min(Math.Max(point.Y, maxRect.Top), rect.Bottom - MINIMUM);
+ break;
+
+ case 1: // upper-right
+ rect.Right = Math.Max(Math.Min(point.X, maxRect.Right), rect.Left + MINIMUM);
+ rect.Top = Math.Min(Math.Max(point.Y, maxRect.Top), rect.Bottom - MINIMUM);
+ break;
+
+ case 2: // lower-right
+ rect.Right = Math.Max(Math.Min(point.X, maxRect.Right), rect.Left + MINIMUM);
+ rect.Bottom = Math.Max(Math.Min(point.Y, maxRect.Bottom), rect.Top + MINIMUM);
+ break;
+
+ case 3: // lower-left
+ rect.Left = Math.Min(Math.Max(point.X, maxRect.Left), rect.Right - MINIMUM);
+ rect.Bottom = Math.Max(Math.Min(point.Y, maxRect.Bottom), rect.Top + MINIMUM);
+ break;
+ }
+
+ // Adjust for aspect ratio
+ if (aspectRatio.HasValue)
+ {
+ float aspect = aspectRatio.Value;
+
+ if (rect.Width > aspect * rect.Height)
+ {
+ float width = aspect * rect.Height;
+
+ switch (index)
+ {
+ case 0:
+ case 3: rect.Left = rect.Right - width; break;
+ case 1:
+ case 2: rect.Right = rect.Left + width; break;
+ }
+ }
+ else
+ {
+ float height = rect.Width / aspect;
+
+ switch (index)
+ {
+ case 0:
+ case 1: rect.Top = rect.Bottom - height; break;
+ case 2:
+ case 3: rect.Bottom = rect.Top + height; break;
+ }
+ }
+ }
+
+ Rect = rect;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/FillRectanglePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/FillRectanglePage.cs
new file mode 100644
index 000000000..c4990467c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/FillRectanglePage.cs
@@ -0,0 +1,33 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public class FillRectanglePage : ContentPage
+ {
+ SKBitmap bitmap =
+ BitmapExtensions.LoadBitmapResource(typeof(FillRectanglePage),
+ "SkiaSharpDemos.Media.banana.jpg");
+ public FillRectanglePage()
+ {
+ Title = "Fill Rectangle";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ canvas.DrawBitmap(bitmap, info.Rect);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/GradientBitmapPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/GradientBitmapPage.cs
new file mode 100644
index 000000000..25c34029a
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/GradientBitmapPage.cs
@@ -0,0 +1,294 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public class GradientBitmapPage : ContentPage
+ {
+ const int REPS = 100;
+
+ Stopwatch stopwatch = new Stopwatch();
+
+ string[] descriptions = new string[8];
+ SKBitmap[] bitmaps = new SKBitmap[8];
+ int[] elapsedTimes = new int[8];
+
+ SKCanvasView canvasView;
+
+ public GradientBitmapPage()
+ {
+ Title = "Gradient Bitmap";
+
+ bitmaps[0] = FillBitmapSetPixel(out descriptions[0], out elapsedTimes[0]);
+ bitmaps[1] = FillBitmapPixelsProp(out descriptions[1], out elapsedTimes[1]);
+ bitmaps[2] = FillBitmapBytePtr(out descriptions[2], out elapsedTimes[2]);
+ bitmaps[4] = FillBitmapUintPtr(out descriptions[4], out elapsedTimes[4]);
+ bitmaps[6] = FillBitmapUintPtrColor(out descriptions[6], out elapsedTimes[6]);
+ bitmaps[3] = FillBitmapByteBuffer(out descriptions[3], out elapsedTimes[3]);
+ bitmaps[5] = FillBitmapUintBuffer(out descriptions[5], out elapsedTimes[5]);
+ bitmaps[7] = FillBitmapUintBufferColor(out descriptions[7], out elapsedTimes[7]);
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ SKBitmap FillBitmapSetPixel(out string description, out int milliseconds)
+ {
+ description = "SetPixel";
+ SKBitmap bitmap = new SKBitmap(256, 256);
+
+ stopwatch.Restart();
+
+ for (int rep = 0; rep < REPS; rep++)
+ for (int row = 0; row < 256; row++)
+ for (int col = 0; col < 256; col++)
+ {
+ bitmap.SetPixel(col, row, new SKColor((byte)col, 0, (byte)row));
+ }
+
+ milliseconds = (int)stopwatch.ElapsedMilliseconds;
+ return bitmap;
+ }
+
+ SKBitmap FillBitmapPixelsProp(out string description, out int milliseconds)
+ {
+ description = "Pixels property";
+ SKBitmap bitmap = new SKBitmap(256, 256);
+
+ stopwatch.Restart();
+
+ SKColor[] pixels = new SKColor[256 * 256];
+
+ for (int rep = 0; rep < REPS; rep++)
+ for (int row = 0; row < 256; row++)
+ for (int col = 0; col < 256; col++)
+ {
+ pixels[256 * row + col] = new SKColor((byte)col, 0, (byte)row);
+ }
+
+ bitmap.Pixels = pixels;
+
+ milliseconds = (int)stopwatch.ElapsedMilliseconds;
+ return bitmap;
+ }
+
+ SKBitmap FillBitmapBytePtr(out string description, out int milliseconds)
+ {
+ description = "GetPixels byte ptr";
+ SKBitmap bitmap = new SKBitmap(256, 256);
+
+ stopwatch.Restart();
+
+ IntPtr pixelsAddr = bitmap.GetPixels();
+
+ unsafe
+ {
+ for (int rep = 0; rep < REPS; rep++)
+ {
+ byte* ptr = (byte*)pixelsAddr.ToPointer();
+
+ for (int row = 0; row < 256; row++)
+ for (int col = 0; col < 256; col++)
+ {
+ *ptr++ = (byte)(col); // red
+ *ptr++ = 0; // green
+ *ptr++ = (byte)(row); // blue
+ *ptr++ = 0xFF; // alpha
+ }
+ }
+ }
+
+ milliseconds = (int)stopwatch.ElapsedMilliseconds;
+ return bitmap;
+ }
+
+ SKBitmap FillBitmapUintPtr(out string description, out int milliseconds)
+ {
+ description = "GetPixels uint ptr";
+ SKBitmap bitmap = new SKBitmap(256, 256);
+
+ stopwatch.Restart();
+
+ IntPtr pixelsAddr = bitmap.GetPixels();
+
+ unsafe
+ {
+ for (int rep = 0; rep < REPS; rep++)
+ {
+ uint* ptr = (uint*)pixelsAddr.ToPointer();
+
+ for (int row = 0; row < 256; row++)
+ for (int col = 0; col < 256; col++)
+ {
+ *ptr++ = MakePixel((byte)col, 0, (byte)row, 0xFF);
+ }
+ }
+ }
+
+ milliseconds = (int)stopwatch.ElapsedMilliseconds;
+ return bitmap;
+ }
+
+ SKBitmap FillBitmapUintPtrColor(out string description, out int milliseconds)
+ {
+ description = "GetPixels SKColor";
+ SKBitmap bitmap = new SKBitmap(256, 256);
+
+ stopwatch.Restart();
+
+ IntPtr pixelsAddr = bitmap.GetPixels();
+
+ unsafe
+ {
+ for (int rep = 0; rep < REPS; rep++)
+ {
+ uint* ptr = (uint*)pixelsAddr.ToPointer();
+
+ for (int row = 0; row < 256; row++)
+ for (int col = 0; col < 256; col++)
+ {
+ *ptr++ = (uint)new SKColor((byte)col, 0, (byte)row);
+ }
+ }
+ }
+
+ milliseconds = (int)stopwatch.ElapsedMilliseconds;
+ return bitmap;
+ }
+
+ SKBitmap FillBitmapByteBuffer(out string description, out int milliseconds)
+ {
+ description = "SetPixels byte buffer";
+ SKBitmap bitmap = new SKBitmap(256, 256);
+
+ stopwatch.Restart();
+
+ byte[,,] buffer = new byte[256, 256, 4];
+
+ for (int rep = 0; rep < REPS; rep++)
+ for (int row = 0; row < 256; row++)
+ for (int col = 0; col < 256; col++)
+ {
+ buffer[row, col, 0] = (byte)col; // red
+ buffer[row, col, 1] = 0; // green
+ buffer[row, col, 2] = (byte)row; // blue
+ buffer[row, col, 3] = 0xFF; // alpha
+ }
+
+ unsafe
+ {
+ fixed (byte* ptr = buffer)
+ {
+ bitmap.SetPixels((IntPtr)ptr);
+ }
+ }
+
+ milliseconds = (int)stopwatch.ElapsedMilliseconds;
+ return bitmap;
+ }
+
+ SKBitmap FillBitmapUintBuffer(out string description, out int milliseconds)
+ {
+ description = "SetPixels uint buffer";
+ SKBitmap bitmap = new SKBitmap(256, 256);
+
+ stopwatch.Restart();
+
+ uint[,] buffer = new uint[256, 256];
+
+ for (int rep = 0; rep < REPS; rep++)
+ for (int row = 0; row < 256; row++)
+ for (int col = 0; col < 256; col++)
+ {
+ buffer[row, col] = MakePixel((byte)col, 0, (byte)row, 0xFF);
+ }
+
+ unsafe
+ {
+ fixed (uint* ptr = buffer)
+ {
+ bitmap.SetPixels((IntPtr)ptr);
+ }
+ }
+
+ milliseconds = (int)stopwatch.ElapsedMilliseconds;
+ return bitmap;
+ }
+
+ SKBitmap FillBitmapUintBufferColor(out string description, out int milliseconds)
+ {
+ description = "SetPixels SKColor";
+ SKBitmap bitmap = new SKBitmap(256, 256);
+
+ stopwatch.Restart();
+
+ uint[,] buffer = new uint[256, 256];
+
+ for (int rep = 0; rep < REPS; rep++)
+ for (int row = 0; row < 256; row++)
+ for (int col = 0; col < 256; col++)
+ {
+ buffer[row, col] = (uint)new SKColor((byte)col, 0, (byte)row);
+ }
+
+ unsafe
+ {
+ fixed (uint* ptr = buffer)
+ {
+ bitmap.SetPixels((IntPtr)ptr);
+ }
+ }
+
+ milliseconds = (int)stopwatch.ElapsedMilliseconds;
+ return bitmap;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ uint MakePixel(byte red, byte green, byte blue, byte alpha) =>
+ (uint)((alpha << 24) | (blue << 16) | (green << 8) | red);
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ int width = info.Width;
+ int height = info.Height;
+
+ canvas.Clear();
+
+ Display(canvas, 0, new SKRect(0, 0, width / 2, height / 4));
+ Display(canvas, 1, new SKRect(width / 2, 0, width, height / 4));
+ Display(canvas, 2, new SKRect(0, height / 4, width / 2, 2 * height / 4));
+ Display(canvas, 3, new SKRect(width / 2, height / 4, width, 2 * height / 4));
+ Display(canvas, 4, new SKRect(0, 2 * height / 4, width / 2, 3 * height / 4));
+ Display(canvas, 5, new SKRect(width / 2, 2 * height / 4, width, 3 * height / 4));
+ Display(canvas, 6, new SKRect(0, 3 * height / 4, width / 2, height));
+ Display(canvas, 7, new SKRect(width / 2, 3 * height / 4, width, height));
+ }
+
+ void Display(SKCanvas canvas, int index, SKRect rect)
+ {
+ string text = String.Format("{0}: {1:F2} msec", descriptions[index],
+ (double)elapsedTimes[index] / REPS);
+
+ SKRect bounds = new SKRect();
+ using (SKPaint textPaint = new SKPaint())
+ {
+ textPaint.TextSize = (float)(12 * canvasView.CanvasSize.Width / canvasView.Width);
+ textPaint.TextAlign = SKTextAlign.Center;
+ textPaint.MeasureText("Tly", ref bounds);
+
+ canvas.DrawText(text, new SKPoint(rect.MidX, rect.Bottom - bounds.Bottom), textPaint);
+ rect.Bottom -= bounds.Height;
+ canvas.DrawBitmap(bitmaps[index], rect, BitmapStretch.Uniform);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/HelloBitmapPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/HelloBitmapPage.cs
new file mode 100644
index 000000000..11a4d7ad1
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/HelloBitmapPage.cs
@@ -0,0 +1,54 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public partial class HelloBitmapPage : ContentPage
+ {
+ const string TEXT = "Hello, Bitmap!";
+ SKBitmap helloBitmap;
+
+ public HelloBitmapPage()
+ {
+ Title = TEXT;
+
+ // Create bitmap and draw on it
+ using (SKPaint textPaint = new SKPaint { TextSize = 48 })
+ {
+ SKRect bounds = new SKRect();
+ textPaint.MeasureText(TEXT, ref bounds);
+
+ helloBitmap = new SKBitmap((int)bounds.Right,
+ (int)bounds.Height);
+
+ using (SKCanvas bitmapCanvas = new SKCanvas(helloBitmap))
+ {
+ bitmapCanvas.Clear();
+ bitmapCanvas.DrawText(TEXT, 0, -bounds.Top, textPaint);
+ }
+ }
+
+ // Create SKCanvasView to view result
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear(SKColors.Aqua);
+
+ for (float y = 0; y < info.Height; y += helloBitmap.Height)
+ for (float x = 0; x < info.Width; x += helloBitmap.Width)
+ {
+ canvas.DrawBitmap(helloBitmap, x, y);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/LatticeDisplayPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/LatticeDisplayPage.cs
new file mode 100644
index 000000000..25513b205
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/LatticeDisplayPage.cs
@@ -0,0 +1,39 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public class LatticeDisplayPage : ContentPage
+ {
+ SKBitmap bitmap = NinePatchDisplayPage.FiveByFiveBitmap;
+
+ public LatticeDisplayPage()
+ {
+ Title = "Lattice Display";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKLattice lattice = new SKLattice();
+ lattice.XDivs = new int[] { 100, 200, 400 };
+ lattice.YDivs = new int[] { 100, 300, 400 };
+
+ int count = (lattice.XDivs.Length + 1) * (lattice.YDivs.Length + 1);
+ //lattice.Flags = new SKLatticeFlags[count];
+
+ canvas.DrawBitmapLattice(bitmap, lattice, info.Rect);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/LatticeNinePatchPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/LatticeNinePatchPage.cs
new file mode 100644
index 000000000..8409524ab
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/LatticeNinePatchPage.cs
@@ -0,0 +1,35 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public class LatticeNinePatchPage : ContentPage
+ {
+ SKBitmap bitmap = NinePatchDisplayPage.FiveByFiveBitmap;
+
+ public LatticeNinePatchPage()
+ {
+ Title = "Lattice Nine-Patch";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ SKLattice lattice = new SKLattice();
+ lattice.XDivs = new int[] { 100, 400 };
+ lattice.YDivs = new int[] { 100, 400 };
+ //lattice.Flags = new SKLatticeFlags[9];
+
+ canvas.DrawBitmapLattice(bitmap, lattice, info.Rect);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/MonkeyMoustachePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/MonkeyMoustachePage.cs
new file mode 100644
index 000000000..f0681e488
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/MonkeyMoustachePage.cs
@@ -0,0 +1,58 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public partial class MonkeyMoustachePage : ContentPage
+ {
+ SKBitmap monkeyBitmap;
+
+ public MonkeyMoustachePage()
+ {
+ Title = "Monkey Moustache";
+
+ monkeyBitmap = BitmapExtensions.LoadBitmapResource(GetType(),
+ "SkiaSharpDemos.Media.monkeyface.png");
+
+ // Create canvas based on bitmap
+ using (SKCanvas canvas = new SKCanvas(monkeyBitmap))
+ {
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Black;
+ paint.StrokeWidth = 24;
+ paint.StrokeCap = SKStrokeCap.Round;
+
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(380, 390);
+ path.CubicTo(560, 390, 560, 280, 500, 280);
+
+ path.MoveTo(320, 390);
+ path.CubicTo(140, 390, 140, 280, 200, 280);
+
+ canvas.DrawPath(path, paint);
+ }
+ }
+ }
+
+ // Create SKCanvasView to view result
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+ canvas.DrawBitmap(monkeyBitmap, info.Rect, BitmapStretch.Uniform);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/NinePatchDisplayPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/NinePatchDisplayPage.cs
new file mode 100644
index 000000000..8f8e47cd8
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/NinePatchDisplayPage.cs
@@ -0,0 +1,51 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public class NinePatchDisplayPage : ContentPage
+ {
+ static NinePatchDisplayPage()
+ {
+ using (SKCanvas canvas = new SKCanvas(FiveByFiveBitmap))
+ using (SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Red,
+ StrokeWidth = 10
+ })
+ {
+ for (int x = 50; x < 500; x += 100)
+ for (int y = 50; y < 500; y += 100)
+ {
+ canvas.DrawCircle(x, y, 40, paint);
+ }
+ }
+ }
+
+ public static SKBitmap FiveByFiveBitmap { get; } = new SKBitmap(500, 500);
+
+ public NinePatchDisplayPage()
+ {
+ Title = "Nine-Patch Display";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKRectI centerRect = new SKRectI(100, 100, 400, 400);
+ canvas.DrawBitmapNinePatch(FiveByFiveBitmap, centerRect, info.Rect);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PhotoCropperCanvasView.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PhotoCropperCanvasView.cs
new file mode 100644
index 000000000..7b0227605
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PhotoCropperCanvasView.cs
@@ -0,0 +1,177 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ class PhotoCropperCanvasView : SKCanvasView
+ {
+ const int CORNER = 50; // pixel length of cropper corner
+ const int RADIUS = 100; // pixel radius of touch hit-test
+
+ SKBitmap bitmap;
+ CroppingRectangle croppingRect;
+ SKMatrix inverseBitmapMatrix;
+
+ struct TouchPoint
+ {
+ public int CornerIndex { set; get; }
+ public SKPoint Offset { set; get; }
+ }
+
+ Dictionary touchPoints = new Dictionary();
+
+ // Drawing objects
+ SKPaint cornerStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.White,
+ StrokeWidth = 10
+ };
+
+ SKPaint edgeStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.White,
+ StrokeWidth = 2
+ };
+
+ public PhotoCropperCanvasView(SKBitmap bitmap, float? aspectRatio = null)
+ {
+ this.bitmap = bitmap;
+
+ SKRect bitmapRect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
+ croppingRect = new CroppingRectangle(bitmapRect, aspectRatio);
+
+ EnableTouchEvents = true;
+ Touch += OnTouch;
+ IgnorePixelScaling = true;
+ }
+
+ public SKBitmap CroppedBitmap
+ {
+ get
+ {
+ SKRect cropRect = croppingRect.Rect;
+ SKBitmap croppedBitmap = new SKBitmap((int)cropRect.Width,
+ (int)cropRect.Height);
+ SKRect dest = new SKRect(0, 0, cropRect.Width, cropRect.Height);
+ SKRect source = new SKRect(cropRect.Left, cropRect.Top,
+ cropRect.Right, cropRect.Bottom);
+
+ using (SKCanvas canvas = new SKCanvas(croppedBitmap))
+ {
+ canvas.DrawBitmap(bitmap, source, dest);
+ }
+
+ return croppedBitmap;
+ }
+ }
+
+ protected override void OnPaintSurface(SKPaintSurfaceEventArgs args)
+ {
+ base.OnPaintSurface(args);
+
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear(SKColors.Gray);
+
+ // Calculate rectangle for displaying bitmap
+ float scale = Math.Min((float)info.Width / bitmap.Width, (float)info.Height / bitmap.Height);
+ float x = (info.Width - scale * bitmap.Width) / 2;
+ float y = (info.Height - scale * bitmap.Height) / 2;
+ SKRect bitmapRect = new SKRect(x, y, x + scale * bitmap.Width, y + scale * bitmap.Height);
+ canvas.DrawBitmap(bitmap, bitmapRect);
+
+ // Calculate a matrix transform for displaying the cropping rectangle
+ SKMatrix bitmapScaleMatrix = SKMatrix.CreateIdentity();
+ bitmapScaleMatrix.SetScaleTranslate(scale, scale, x, y);
+
+ // Display rectangle
+ SKRect scaledCropRect = bitmapScaleMatrix.MapRect(croppingRect.Rect);
+ canvas.DrawRect(scaledCropRect, edgeStroke);
+
+ // Display heavier corners
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(scaledCropRect.Left, scaledCropRect.Top + CORNER);
+ path.LineTo(scaledCropRect.Left, scaledCropRect.Top);
+ path.LineTo(scaledCropRect.Left + CORNER, scaledCropRect.Top);
+
+ path.MoveTo(scaledCropRect.Right - CORNER, scaledCropRect.Top);
+ path.LineTo(scaledCropRect.Right, scaledCropRect.Top);
+ path.LineTo(scaledCropRect.Right, scaledCropRect.Top + CORNER);
+
+ path.MoveTo(scaledCropRect.Right, scaledCropRect.Bottom - CORNER);
+ path.LineTo(scaledCropRect.Right, scaledCropRect.Bottom);
+ path.LineTo(scaledCropRect.Right - CORNER, scaledCropRect.Bottom);
+
+ path.MoveTo(scaledCropRect.Left + CORNER, scaledCropRect.Bottom);
+ path.LineTo(scaledCropRect.Left, scaledCropRect.Bottom);
+ path.LineTo(scaledCropRect.Left, scaledCropRect.Bottom - CORNER);
+
+ canvas.DrawPath(path, cornerStroke);
+ }
+
+ // Invert the transform for touch tracking
+ bitmapScaleMatrix.TryInvert(out inverseBitmapMatrix);
+ }
+
+ void OnTouch(object sender, SKTouchEventArgs e)
+ {
+ SKPoint pixelLocation = ConvertToPixel(e.Location);
+ SKPoint bitmapLocation = inverseBitmapMatrix.MapPoint(pixelLocation);
+
+ switch (e.ActionType)
+ {
+ case SKTouchAction.Pressed:
+ // Convert radius to bitmap/cropping scale
+ float radius = inverseBitmapMatrix.ScaleX * RADIUS;
+
+ // Find corner that the finger is touching
+ int cornerIndex = croppingRect.HitTest(bitmapLocation, radius);
+
+ if (cornerIndex != -1 && !touchPoints.ContainsKey(e.Id))
+ {
+ TouchPoint touchPoint = new TouchPoint
+ {
+ CornerIndex = cornerIndex,
+ Offset = bitmapLocation - croppingRect.Corners[cornerIndex]
+ };
+
+ touchPoints.Add(e.Id, touchPoint);
+ }
+ break;
+
+ case SKTouchAction.Moved:
+ if (touchPoints.ContainsKey(e.Id))
+ {
+ TouchPoint touchPoint = touchPoints[e.Id];
+ croppingRect.MoveCorner(touchPoint.CornerIndex,
+ bitmapLocation - touchPoint.Offset);
+ InvalidateSurface();
+ }
+ break;
+
+ case SKTouchAction.Released:
+ case SKTouchAction.Cancelled:
+ if (touchPoints.ContainsKey(e.Id))
+ {
+ touchPoints.Remove(e.Id);
+ }
+ break;
+ }
+
+ e.Handled = true;
+ }
+
+ SKPoint ConvertToPixel(SKPoint pt)
+ {
+ return new SKPoint((float)(CanvasSize.Width * pt.X / Width),
+ (float)(CanvasSize.Height * pt.Y / Height));
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PhotoCroppingPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PhotoCroppingPage.xaml
new file mode 100644
index 000000000..a95b95427
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PhotoCroppingPage.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PhotoCroppingPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PhotoCroppingPage.xaml.cs
new file mode 100644
index 000000000..96659de05
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PhotoCroppingPage.xaml.cs
@@ -0,0 +1,41 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps;
+
+public partial class PhotoCroppingPage : ContentPage
+{
+ PhotoCropperCanvasView photoCropper;
+ SKBitmap croppedBitmap;
+
+ public PhotoCroppingPage()
+ {
+ InitializeComponent();
+
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(GetType(),
+ "SkiaSharpDemos.Media.mountainclimbers.jpg");
+
+ photoCropper = new PhotoCropperCanvasView(bitmap);
+ canvasViewHost.Add(photoCropper);
+ }
+
+ void OnDoneButtonClicked(object sender, EventArgs args)
+ {
+ croppedBitmap = photoCropper.CroppedBitmap;
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+ canvas.DrawBitmap(croppedBitmap, info.Rect, BitmapStretch.Uniform);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PixelDimensionsPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PixelDimensionsPage.cs
new file mode 100644
index 000000000..83f858d97
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PixelDimensionsPage.cs
@@ -0,0 +1,46 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public class PixelDimensionsPage : ContentPage
+ {
+ SKBitmap bitmap;
+
+ public PixelDimensionsPage()
+ {
+ Title = "Pixel Dimensions";
+
+ // Load the bitmap from a resource
+ string resourceID = "SkiaSharpDemos.Media.banana.jpg";
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+
+ using (Stream stream = assembly.GetManifestResourceStream(resourceID))
+ {
+ bitmap = SKBitmap.Decode(stream);
+ }
+
+ // Create the SKCanvasView and set the PaintSurface handler
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float x = (info.Width - bitmap.Width) / 2;
+ float y = (info.Height - bitmap.Height) / 2;
+
+ canvas.DrawBitmap(bitmap, x, y);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PixelizedImagePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PixelizedImagePage.cs
new file mode 100644
index 000000000..e4d284e16
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PixelizedImagePage.cs
@@ -0,0 +1,63 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public class PixelizedImagePage : ContentPage
+ {
+ SKBitmap pixelizedBitmap;
+
+ public PixelizedImagePage()
+ {
+ Title = "Pixelize Image";
+
+ SKBitmap originalBitmap = BitmapExtensions.LoadBitmapResource(GetType(),
+ "SkiaSharpDemos.Media.mountainclimbers.jpg");
+
+ // Create tiny bitmap for pixelized face
+ SKBitmap faceBitmap = new SKBitmap(9, 9);
+
+ // Copy subset of original bitmap to that
+ using (SKCanvas canvas = new SKCanvas(faceBitmap))
+ {
+ canvas.Clear();
+ canvas.DrawBitmap(originalBitmap,
+ new SKRect(112, 238, 184, 310), // source
+ new SKRect(0, 0, 9, 9)); // destination
+
+ }
+
+ // Create full-sized bitmap for copy
+ pixelizedBitmap = new SKBitmap(originalBitmap.Width, originalBitmap.Height);
+
+ using (SKCanvas canvas = new SKCanvas(pixelizedBitmap))
+ {
+ canvas.Clear();
+
+ // Draw original in full size
+ canvas.DrawBitmap(originalBitmap, new SKPoint());
+
+ // Draw tiny bitmap to cover face
+ canvas.DrawBitmap(faceBitmap,
+ new SKRect(112, 238, 184, 310)); // destination
+ }
+
+ // Create SKCanvasView to view result
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+ canvas.DrawBitmap(pixelizedBitmap, info.Rect, BitmapStretch.Uniform);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PosterizePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PosterizePage.cs
new file mode 100644
index 000000000..6d80c6d9a
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/PosterizePage.cs
@@ -0,0 +1,43 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public class PosterizePage : ContentPage
+ {
+ SKBitmap bitmap =
+ BitmapExtensions.LoadBitmapResource(typeof(FillRectanglePage),
+ "SkiaSharpDemos.Media.banana.jpg");
+ public PosterizePage()
+ {
+ Title = "Posterize";
+
+ unsafe
+ {
+ uint* ptr = (uint*)bitmap.GetPixels().ToPointer();
+ int pixelCount = bitmap.Width * bitmap.Height;
+
+ for (int i = 0; i < pixelCount; i++)
+ {
+ *ptr++ &= 0xE0E0E0FF;
+ }
+ }
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+ canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/RainbowSinePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/RainbowSinePage.cs
new file mode 100644
index 000000000..68c018a47
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/RainbowSinePage.cs
@@ -0,0 +1,61 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public class RainbowSinePage : ContentPage
+ {
+ SKBitmap bitmap;
+
+ public RainbowSinePage()
+ {
+ Title = "Rainbow Sine";
+
+ bitmap = new SKBitmap(360 * 3, 1024, SKColorType.Bgra8888, SKAlphaType.Unpremul);
+
+ unsafe
+ {
+ // Pointer to first pixel of bitmap
+ uint* basePtr = (uint*)bitmap.GetPixels().ToPointer();
+
+ // Loop through the rows
+ for (int row = 0; row < bitmap.Height; row++)
+ {
+ // Calculate the sine curve angle and the sine value
+ double angle = 2 * Math.PI * row / bitmap.Height;
+ double sine = Math.Sin(angle);
+
+ // Loop through the hues
+ for (int hue = 0; hue < 360; hue++)
+ {
+ // Calculate the column
+ int col = (int)(360 + 360 * sine + hue);
+
+ // Calculate the address
+ uint* ptr = basePtr + bitmap.Width * row + col;
+
+ // Store the color value
+ *ptr = (uint)SKColor.FromHsl(hue, 100, 50);
+ }
+ }
+ }
+
+ // Create the SKCanvasView
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+ canvas.DrawBitmap(bitmap, info.Rect);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/RectangleSubsetPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/RectangleSubsetPage.xaml
new file mode 100644
index 000000000..097d6bc3c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/RectangleSubsetPage.xaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/RectangleSubsetPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/RectangleSubsetPage.xaml.cs
new file mode 100644
index 000000000..919b16f28
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/RectangleSubsetPage.xaml.cs
@@ -0,0 +1,44 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Bitmaps;
+
+public partial class RectangleSubsetPage : ContentPage
+{
+ SKBitmap bitmap =
+ BitmapExtensions.LoadBitmapResource(typeof(RectangleSubsetPage),
+ "SkiaSharpDemos.Media.banana.jpg");
+
+ static readonly SKRect SOURCE = new SKRect(94, 12, 212, 118);
+
+ public RectangleSubsetPage()
+ {
+ InitializeComponent();
+
+ stretchPicker.SelectedIndex = 0;
+ horizontalPicker.SelectedIndex = 0;
+ verticalPicker.SelectedIndex = 0;
+ }
+
+ private void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKRect dest = new SKRect(0, 0, info.Width, info.Height);
+
+ BitmapStretch stretch = (BitmapStretch)stretchPicker.SelectedItem;
+ BitmapAlignment horizontal = (BitmapAlignment)horizontalPicker.SelectedItem;
+ BitmapAlignment vertical = (BitmapAlignment)verticalPicker.SelectedItem;
+
+ canvas.DrawBitmap(bitmap, SOURCE, dest, stretch, horizontal, vertical);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/SaveFileFormatsPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/SaveFileFormatsPage.xaml
new file mode 100644
index 000000000..081c5e1a5
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/SaveFileFormatsPage.xaml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/SaveFileFormatsPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/SaveFileFormatsPage.xaml.cs
new file mode 100644
index 000000000..0bceb5b92
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/SaveFileFormatsPage.xaml.cs
@@ -0,0 +1,69 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Bitmaps;
+
+public partial class SaveFileFormatsPage : ContentPage
+{
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(typeof(SaveFileFormatsPage),
+ "SkiaSharpDemos.Media.monkeyface.png");
+
+ public SaveFileFormatsPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ args.Surface.Canvas.DrawBitmap(bitmap, args.Info.Rect, BitmapStretch.Uniform);
+ }
+
+ void OnFormatPickerChanged(object sender, EventArgs args)
+ {
+ if (formatPicker.SelectedIndex != -1)
+ {
+ SKEncodedImageFormat imageFormat = (SKEncodedImageFormat)formatPicker.SelectedItem;
+ fileNameEntry.Text = Path.ChangeExtension(fileNameEntry.Text, imageFormat.ToString());
+ statusLabel.Text = "OK";
+ }
+ }
+
+ async void OnButtonClicked(object sender, EventArgs args)
+ {
+ if (formatPicker.SelectedIndex != -1 && fileNameEntry.Text.Length != 0)
+ {
+ SKEncodedImageFormat imageFormat = (SKEncodedImageFormat)formatPicker.SelectedItem;
+ int quality = (int)qualitySlider.Value;
+
+ using (MemoryStream memStream = new MemoryStream())
+ using (SKManagedWStream wstream = new SKManagedWStream(memStream))
+ {
+ bitmap.Encode(wstream, imageFormat, quality);
+ byte[] data = memStream.ToArray();
+
+ if (data == null)
+ {
+ statusLabel.Text = "Encode returned null";
+ }
+ else if (data.Length == 0)
+ {
+ statusLabel.Text = "Encode returned empty array";
+ }
+ else
+ {
+ var photoLibrary = new PhotoLibrary();
+ bool success = await photoLibrary.SavePhotoAsync(data, folderNameEntry.Text, fileNameEntry.Text);
+
+ if (!success)
+ {
+ statusLabel.Text = "SavePhotoAsync return false";
+ }
+ else
+ {
+ statusLabel.Text = "Success!";
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ScalingModesPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ScalingModesPage.xaml
new file mode 100644
index 000000000..f20d43a71
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ScalingModesPage.xaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ScalingModesPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ScalingModesPage.xaml.cs
new file mode 100644
index 000000000..8c8dc8935
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/ScalingModesPage.xaml.cs
@@ -0,0 +1,41 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Bitmaps;
+
+public partial class ScalingModesPage : ContentPage
+{
+ SKBitmap bitmap =
+ BitmapExtensions.LoadBitmapResource(typeof(ScalingModesPage),
+ "SkiaSharpDemos.Media.banana.jpg");
+ public ScalingModesPage()
+ {
+ InitializeComponent();
+
+ stretchPicker.SelectedIndex = 0;
+ horizontalPicker.SelectedIndex = 0;
+ verticalPicker.SelectedIndex = 0;
+ }
+
+ private void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKRect dest = new SKRect(0, 0, info.Width, info.Height);
+
+ BitmapStretch stretch = (BitmapStretch)stretchPicker.SelectedItem;
+ BitmapAlignment horizontal = (BitmapAlignment)horizontalPicker.SelectedItem;
+ BitmapAlignment vertical = (BitmapAlignment)verticalPicker.SelectedItem;
+
+ canvas.DrawBitmap(bitmap, dest, stretch, horizontal, vertical);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/UniformScalingPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/UniformScalingPage.cs
new file mode 100644
index 000000000..4cdac605e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Bitmaps/UniformScalingPage.cs
@@ -0,0 +1,40 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Bitmaps
+{
+ public class UniformScalingPage : ContentPage
+ {
+ SKBitmap bitmap =
+ BitmapExtensions.LoadBitmapResource(typeof(UniformScalingPage),
+ "SkiaSharpDemos.Media.banana.jpg");
+ public UniformScalingPage()
+ {
+ Title = "Uniform Scaling";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float scale = Math.Min((float)info.Width / bitmap.Width,
+ (float)info.Height / bitmap.Height);
+ float x = (info.Width - scale * bitmap.Width) / 2;
+ float y = (info.Height - scale * bitmap.Height) / 2;
+ SKRect destRect = new SKRect(x, y, x + scale * bitmap.Width,
+ y + scale * bitmap.Height);
+
+ canvas.DrawBitmap(bitmap, destRect);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AngleArcPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AngleArcPage.xaml
new file mode 100644
index 000000000..1416daeaf
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AngleArcPage.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AngleArcPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AngleArcPage.xaml.cs
new file mode 100644
index 000000000..c111994ef
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AngleArcPage.xaml.cs
@@ -0,0 +1,55 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class AngleArcPage : ContentPage
+{
+ SKPaint outlinePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = 3,
+ Color = SKColors.Black
+ };
+
+ SKPaint arcPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = 15,
+ Color = SKColors.Red
+ };
+
+ public AngleArcPage()
+ {
+ InitializeComponent();
+ }
+
+ void sliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
+ float startAngle = (float)startAngleSlider.Value;
+ float sweepAngle = (float)sweepAngleSlider.Value;
+
+ canvas.DrawOval(rect, outlinePaint);
+
+ using (SKPath path = new SKPath())
+ {
+ path.AddArc(rect, startAngle, sweepAngle);
+ canvas.DrawPath(path, arcPaint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AnimatedDottedTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AnimatedDottedTextPage.cs
new file mode 100644
index 000000000..831ac29c0
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AnimatedDottedTextPage.cs
@@ -0,0 +1,90 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class AnimatedDottedTextPage : ContentPage
+ {
+ const string text = "DOTTED";
+ const float strokeWidth = 10;
+ static readonly float[] dashArray = { 0, 2 * strokeWidth };
+
+ SKCanvasView canvasView;
+ bool pageIsActive;
+
+ public AnimatedDottedTextPage()
+ {
+ Title = "Animated Dotted Text";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ pageIsActive = true;
+
+ Dispatcher.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
+ {
+ canvasView.InvalidateSurface();
+ return pageIsActive;
+ });
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ pageIsActive = false;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Create an SKPaint object to display the text
+ using (SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = strokeWidth,
+ StrokeCap = SKStrokeCap.Round,
+ Color = SKColors.Blue,
+ })
+ {
+ // Adjust TextSize property so text is 95% of screen width
+ float textWidth = textPaint.MeasureText(text);
+ textPaint.TextSize *= 0.95f * info.Width / textWidth;
+
+ // Find the text bounds
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText(text, ref textBounds);
+
+ // Calculate offsets to center the text on the screen
+ float xText = info.Width / 2 - textBounds.MidX;
+ float yText = info.Height / 2 - textBounds.MidY;
+
+ // Animate the phase; t is 0 to 1 every second
+ TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
+ float t = (float)(timeSpan.TotalSeconds % 1 / 1);
+ float phase = -t * 2 * strokeWidth;
+
+ // Create dotted line effect based on dash array and phase
+ using (SKPathEffect dashEffect = SKPathEffect.CreateDash(dashArray, phase))
+ {
+ // Set it to the paint object
+ textPaint.PathEffect = dashEffect;
+
+ // And draw the text
+ canvas.DrawText(text, xText, yText, textPaint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AnotherRoundedHeptagonPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AnotherRoundedHeptagonPage.cs
new file mode 100644
index 000000000..752599144
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/AnotherRoundedHeptagonPage.cs
@@ -0,0 +1,68 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class AnotherRoundedHeptagonPage : ContentPage
+ {
+ public AnotherRoundedHeptagonPage()
+ {
+ Title = "Another Rounded Heptagon";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ int numVertices = 7;
+ float radius = 0.45f * Math.Min(info.Width, info.Height);
+ SKPoint[] vertices = new SKPoint[numVertices];
+ double vertexAngle = -0.5f * Math.PI; // straight up
+
+ // Coordinates of the vertices of the polygon
+ for (int vertex = 0; vertex < numVertices; vertex++)
+ {
+ vertices[vertex] = new SKPoint(radius * (float)Math.Cos(vertexAngle),
+ radius * (float)Math.Sin(vertexAngle));
+ vertexAngle += 2 * Math.PI / numVertices;
+ }
+
+ float cornerRadius = 100;
+
+ // Create the path
+ using (SKPath path = new SKPath())
+ {
+ path.AddPoly(vertices, true);
+
+ // Render the path in the center of the screen
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Blue;
+ paint.StrokeWidth = 10;
+
+ // Set argument to half the desired corner radius!
+ paint.PathEffect = SKPathEffect.CreateCorner(cornerRadius / 2);
+
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.DrawPath(path, paint);
+
+ // Uncomment DrawCircle call to verify corner radius
+ float offset = cornerRadius / (float)Math.Sin(Math.PI * (numVertices - 2) / numVertices / 2);
+ paint.Color = SKColors.Green;
+ // canvas.DrawCircle(vertices[0].X, vertices[0].Y + offset, cornerRadius, paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ArcInfinityPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ArcInfinityPage.cs
new file mode 100644
index 000000000..87e40e5b8
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ArcInfinityPage.cs
@@ -0,0 +1,53 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class ArcInfinityPage : ContentPage
+ {
+ public ArcInfinityPage()
+ {
+ Title = "Arc Infinity";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPath path = new SKPath())
+ {
+ path.LineTo(83, 75);
+ path.ArcTo(100, 100, 0, SKPathArcSize.Large, SKPathDirection.CounterClockwise, 83, -75);
+ path.LineTo(-83, 75);
+ path.ArcTo(100, 100, 0, SKPathArcSize.Large, SKPathDirection.Clockwise, -83, -75);
+ path.Close();
+
+ // Use path.TightBounds for coordinates without control points
+ SKRect pathBounds = path.Bounds;
+
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.Scale(Math.Min(info.Width / pathBounds.Width,
+ info.Height / pathBounds.Height));
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Blue;
+ paint.StrokeWidth = 5;
+
+ canvas.DrawPath(path, paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCircularArcPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCircularArcPage.xaml
new file mode 100644
index 000000000..7affd4c91
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCircularArcPage.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCircularArcPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCircularArcPage.xaml.cs
new file mode 100644
index 000000000..817ddbdac
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCircularArcPage.xaml.cs
@@ -0,0 +1,116 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class BezierCircularArcPage : ContentPage
+{
+ SKPaint blackFill = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Black
+ };
+
+ SKPaint blackStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 3
+ };
+
+ SKPaint dottedStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 3,
+ PathEffect = SKPathEffect.CreateDash(new float[] { 7, 7 }, 0)
+ };
+
+ SKPaint redStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Red,
+ StrokeWidth = 6
+ };
+
+ public BezierCircularArcPage()
+ {
+ InitializeComponent();
+
+ angleSlider.Value = 90;
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Translate to center
+ canvas.Translate(info.Width / 2, info.Height / 2);
+
+ // Draw the circle
+ float radius = Math.Min(info.Width, info.Height) / 3;
+ canvas.DrawCircle(0, 0, radius, blackStroke);
+
+ // Get the value of the Slider
+ float angle = (float)angleSlider.Value;
+
+ // Calculate length of control point line
+ float length = radius * 4 * (float)Math.Tan(Math.PI * angle / 180 / 4) / 3;
+
+ // Calculate sin and cosine for half that angle
+ float sin = (float)Math.Sin(Math.PI * angle / 180 / 2);
+ float cos = (float)Math.Cos(Math.PI * angle / 180 / 2);
+
+ // Find the end points
+ SKPoint point0 = new SKPoint(-radius * sin, radius * cos);
+ SKPoint point3 = new SKPoint(radius * sin, radius * cos);
+
+ // Find the control points
+ SKPoint point0Normalized = Normalize(point0);
+ SKPoint point1 = point0 + new SKPoint(length * point0Normalized.Y,
+ -length * point0Normalized.X);
+
+ SKPoint point3Normalized = Normalize(point3);
+ SKPoint point2 = point3 + new SKPoint(-length * point3Normalized.Y,
+ length * point3Normalized.X);
+
+ // Draw the points
+ canvas.DrawCircle(point0.X, point0.Y, 10, blackFill);
+ canvas.DrawCircle(point1.X, point1.Y, 10, blackFill);
+ canvas.DrawCircle(point2.X, point2.Y, 10, blackFill);
+ canvas.DrawCircle(point3.X, point3.Y, 10, blackFill);
+
+ // Draw the tangent lines
+ canvas.DrawLine(point0.X, point0.Y, point1.X, point1.Y, dottedStroke);
+ canvas.DrawLine(point3.X, point3.Y, point2.X, point2.Y, dottedStroke);
+
+ // Draw the Bezier curve
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(point0);
+ path.CubicTo(point1, point2, point3);
+ canvas.DrawPath(path, redStroke);
+ }
+ }
+
+ // Vector methods
+ SKPoint Normalize(SKPoint v)
+ {
+ float magnitude = Magnitude(v);
+ return new SKPoint(v.X / magnitude, v.Y / magnitude);
+ }
+
+ float Magnitude(SKPoint v)
+ {
+ return (float)Math.Sqrt(v.X * v.X + v.Y * v.Y);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCurvePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCurvePage.xaml
new file mode 100644
index 000000000..fabac88c8
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCurvePage.xaml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCurvePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCurvePage.xaml.cs
new file mode 100644
index 000000000..1c26b7650
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierCurvePage.xaml.cs
@@ -0,0 +1,61 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class BezierCurvePage : InteractivePage
+{
+ public BezierCurvePage()
+ {
+ touchPoints = new TouchPoint[4];
+
+ for (int i = 0; i < 4; i++)
+ {
+ TouchPoint touchPoint = new TouchPoint
+ {
+ Center = new SKPoint(100 + 200 * (i % 2),
+ 100 + 200 * i)
+ };
+ touchPoints[i] = touchPoint;
+ }
+
+ InitializeComponent();
+ baseCanvasView = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Draw path with cubic Bezier curve
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(touchPoints[0].Center);
+ path.CubicTo(touchPoints[1].Center,
+ touchPoints[2].Center,
+ touchPoints[3].Center);
+
+ canvas.DrawPath(path, strokePaint);
+ }
+
+ // Draw tangent lines
+ canvas.DrawLine(touchPoints[0].Center.X,
+ touchPoints[0].Center.Y,
+ touchPoints[1].Center.X,
+ touchPoints[1].Center.Y, dottedStrokePaint);
+
+ canvas.DrawLine(touchPoints[2].Center.X,
+ touchPoints[2].Center.Y,
+ touchPoints[3].Center.X,
+ touchPoints[3].Center.Y, dottedStrokePaint);
+
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ touchPoint.Paint(canvas);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierInfinityPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierInfinityPage.cs
new file mode 100644
index 000000000..b48969566
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/BezierInfinityPage.cs
@@ -0,0 +1,56 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class BezierInfinityPage : ContentPage
+ {
+ public BezierInfinityPage()
+ {
+ Title = "Bezier Infinity";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(0, 0); // Center
+ path.CubicTo(50, -50, 95, -100, 150, -100); // To top of right loop
+ path.CubicTo(205, -100, 250, -55, 250, 0); // To far right of right loop
+ path.CubicTo(250, 55, 205, 100, 150, 100); // To bottom of right loop
+ path.CubicTo(95, 100, 50, 50, 0, 0); // Back to center
+ path.CubicTo(-50, -50, -95, -100, -150, -100); // To top of left loop
+ path.CubicTo(-205, -100, -250, -55, -250, 0); // To far left of left loop
+ path.CubicTo(-250, 55, -205, 100, -150, 100); // To bottom of left loop
+ path.CubicTo(-95, 100, -50, 50, 0, 0); // Back to center
+ path.Close();
+
+ SKRect pathBounds = path.Bounds;
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.Scale(0.9f * Math.Min(info.Width / pathBounds.Width,
+ info.Height / pathBounds.Height));
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Blue;
+ paint.StrokeWidth = 5;
+
+ canvas.DrawPath(path, paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CatsInFramePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CatsInFramePage.cs
new file mode 100644
index 000000000..68771e94a
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CatsInFramePage.cs
@@ -0,0 +1,94 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class CatsInFramePage : ContentPage
+ {
+ // From PathDataCatPage.cs
+ SKPath catPath = SKPath.ParseSvgPathData(
+ "M 160 140 L 150 50 220 103" + // Left ear
+ "M 320 140 L 330 50 260 103" + // Right ear
+ "M 215 230 L 40 200" + // Left whiskers
+ "M 215 240 L 40 240" +
+ "M 215 250 L 40 280" +
+ "M 265 230 L 440 200" + // Right whiskers
+ "M 265 240 L 440 240" +
+ "M 265 250 L 440 280" +
+ "M 240 100" + // Head
+ "A 100 100 0 0 1 240 300" +
+ "A 100 100 0 0 1 240 100 Z" +
+ "M 180 170" + // Left eye
+ "A 40 40 0 0 1 220 170" +
+ "A 40 40 0 0 1 180 170 Z" +
+ "M 300 170" + // Right eye
+ "A 40 40 0 0 1 260 170" +
+ "A 40 40 0 0 1 300 170 Z");
+
+ SKPaint catStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = 5
+ };
+
+ SKPath scallopPath = SKPath.ParseSvgPathData("M 0 0 L 50 0 A 60 60 0 0 1 -50 0 Z");
+
+ SKPaint framePaint = new SKPaint
+ {
+ Color = SKColors.Black
+ };
+
+ public CatsInFramePage()
+ {
+ Title = "Cats in Frame";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ // Move (0, 0) point to center of cat path
+ catPath.Transform(SKMatrix.CreateTranslation(-240, -175));
+
+ // Now catPath is 400 by 250
+ // Scale it down to 160 by 100
+ catPath.Transform(SKMatrix.CreateScale(0.40f, 0.40f));
+
+ // Get the outlines of the contours of the cat path
+ SKPath outlinedCatPath = new SKPath();
+ catStroke.GetFillPath(catPath, outlinedCatPath);
+
+ // Create a 2D path effect from those outlines
+ SKPathEffect fillEffect = SKPathEffect.Create2DPath(
+ new SKMatrix
+ {
+ ScaleX = 170,
+ ScaleY = 110,
+ TransX = 75,
+ TransY = 80,
+ Persp2 = 1
+ },
+ outlinedCatPath);
+
+ // Create a 1D path effect from the scallop path
+ SKPathEffect strokeEffect = SKPathEffect.Create1DPath(scallopPath, 75, 0, SKPath1DPathEffectStyle.Rotate);
+
+ // Set the sum the effects to frame paint
+ framePaint.PathEffect = SKPathEffect.CreateSum(fillEffect, strokeEffect);
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKRect rect = new SKRect(50, 50, info.Width - 50, info.Height - 50);
+ canvas.ClipRect(rect);
+ canvas.DrawRect(rect, framePaint);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CharacterOutlineOutlinesPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CharacterOutlineOutlinesPage.cs
new file mode 100644
index 000000000..83d92aa99
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CharacterOutlineOutlinesPage.cs
@@ -0,0 +1,67 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class CharacterOutlineOutlinesPage : ContentPage
+ {
+ public CharacterOutlineOutlinesPage()
+ {
+ Title = "Character Outline Outlines";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint textPaint = new SKPaint())
+ {
+ // Set Style for the character outlines
+ textPaint.Style = SKPaintStyle.Stroke;
+
+ // Set TextSize based on screen size
+ textPaint.TextSize = Math.Min(info.Width, info.Height);
+
+ // Measure the text
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText("@", ref textBounds);
+
+ // Coordinates to center text on screen
+ float xText = info.Width / 2 - textBounds.MidX;
+ float yText = info.Height / 2 - textBounds.MidY;
+
+ // Get the path for the character outlines
+ using (SKPath textPath = textPaint.GetTextPath("@", xText, yText))
+ {
+ // Create a new path for the outlines of the path
+ using (SKPath outlinePath = new SKPath())
+ {
+ // Convert the path to the outlines of the stroked path
+ textPaint.StrokeWidth = 25;
+ textPaint.GetFillPath(textPath, outlinePath);
+
+ // Stroke that new path
+ using (SKPaint outlinePaint = new SKPaint())
+ {
+ outlinePaint.Style = SKPaintStyle.Stroke;
+ outlinePaint.StrokeWidth = 5;
+ outlinePaint.Color = SKColors.Red;
+
+ canvas.DrawPath(outlinePath, outlinePaint);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CircularTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CircularTextPage.cs
new file mode 100644
index 000000000..ec9ef059f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CircularTextPage.cs
@@ -0,0 +1,45 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class CircularTextPage : ContentPage
+ {
+ const string text = "xt in a circle that shapes the te";
+
+ public CircularTextPage()
+ {
+ Title = "Circular Text";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPath circularPath = new SKPath())
+ {
+ float radius = 0.35f * Math.Min(info.Width, info.Height);
+ circularPath.AddCircle(info.Width / 2, info.Height / 2, radius);
+
+ using (SKPaint textPaint = new SKPaint())
+ {
+ textPaint.TextSize = 100;
+ float textWidth = textPaint.MeasureText(text);
+ textPaint.TextSize *= 2 * 3.14f * radius / textWidth;
+
+ canvas.DrawTextOnPath(text, circularPath, 0, 0, textPaint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ClipOperationsPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ClipOperationsPage.cs
new file mode 100644
index 000000000..0d837fd2c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ClipOperationsPage.cs
@@ -0,0 +1,90 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class ClipOperationsPage : ContentPage
+ {
+ SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Black,
+ TextSize = 40,
+ TextAlign = SKTextAlign.Center
+ };
+
+ SKPaint fillPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Pink
+ };
+
+ public ClipOperationsPage()
+ {
+ Title = "Clip Operations";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float x = 0;
+ float y = 0;
+
+ foreach (SKClipOperation clipOp in Enum.GetValues(typeof(SKClipOperation)))
+ {
+ // Portrait mode
+ if (info.Height > info.Width)
+ {
+ DisplayClipOp(canvas, new SKRect(x, y, x + info.Width, y + info.Height / 2), clipOp);
+ y += info.Height / 2;
+ }
+ // Landscape mode
+ else
+ {
+ DisplayClipOp(canvas, new SKRect(x, y, x + info.Width / 2, y + info.Height), clipOp);
+ x += info.Width / 2;
+ }
+ }
+ }
+
+ void DisplayClipOp(SKCanvas canvas, SKRect rect, SKClipOperation clipOp)
+ {
+ float textSize = textPaint.TextSize;
+ canvas.DrawText(clipOp.ToString(), rect.MidX, rect.Top + textSize, textPaint);
+ rect.Top += textSize;
+
+ float radius = 0.9f * Math.Min(rect.Width / 3, rect.Height / 2);
+ float xCenter = rect.MidX;
+ float yCenter = rect.MidY;
+
+ canvas.Save();
+
+ using (SKPath path1 = new SKPath())
+ {
+ path1.AddCircle(xCenter - radius / 2, yCenter, radius);
+ canvas.ClipPath(path1);
+
+ using (SKPath path2 = new SKPath())
+ {
+ path2.AddCircle(xCenter + radius / 2, yCenter, radius);
+ canvas.ClipPath(path2, clipOp);
+
+ canvas.DrawPaint(fillPaint);
+ }
+ }
+
+ canvas.Restore();
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ClippingTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ClippingTextPage.cs
new file mode 100644
index 000000000..05ce6002f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ClippingTextPage.cs
@@ -0,0 +1,67 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class ClippingTextPage : ContentPage
+ {
+ SKBitmap bitmap;
+
+ public ClippingTextPage()
+ {
+ Title = "Clipping Text";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ string resourceID = "SkiaSharpDemos.Media.pageofcode.png";
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+
+ using (Stream stream = assembly.GetManifestResourceStream(resourceID))
+ {
+ bitmap = SKBitmap.Decode(stream);
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear(SKColors.Blue);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Typeface = SKTypeface.FromFamilyName(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
+ paint.TextSize = 10;
+
+ using (SKPath textPath = paint.GetTextPath("CODE", 0, 0))
+ {
+ // Set transform to center and enlarge clip path to window height
+ SKRect bounds;
+ textPath.GetTightBounds(out bounds);
+
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.Scale(info.Width / bounds.Width, info.Height / bounds.Height);
+ canvas.Translate(-bounds.MidX, -bounds.MidY);
+
+ // Set the clip path
+ canvas.ClipPath(textPath);
+ }
+ }
+
+ // Reset transforms
+ canvas.ResetMatrix();
+
+ // Display bitmap to fill window but maintain aspect ratio
+ SKRect rect = new SKRect(0, 0, info.Width, info.Height);
+ canvas.DrawBitmap(bitmap,
+ rect.AspectFill(new SKSize(bitmap.Width, bitmap.Height)));
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCircularArcPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCircularArcPage.xaml
new file mode 100644
index 000000000..f3492d81f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCircularArcPage.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCircularArcPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCircularArcPage.xaml.cs
new file mode 100644
index 000000000..6efd2a62f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCircularArcPage.xaml.cs
@@ -0,0 +1,93 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class ConicCircularArcPage : ContentPage
+{
+ SKPaint blackFill = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Black
+ };
+
+ SKPaint blackStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 3
+ };
+
+ SKPaint dottedStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 3,
+ PathEffect = SKPathEffect.CreateDash(new float[] { 7, 7 }, 0)
+ };
+
+ SKPaint redStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Red,
+ StrokeWidth = 6
+ };
+
+ public ConicCircularArcPage()
+ {
+ InitializeComponent();
+
+ angleSlider.Value = 90;
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Translate to center
+ canvas.Translate(info.Width / 2, info.Height / 2);
+
+ // Draw the circle
+ float radius = Math.Min(info.Width, info.Height) / 4;
+ canvas.DrawCircle(0, 0, radius, blackStroke);
+
+ // Get the value of the Slider
+ float angle = (float)angleSlider.Value;
+
+ // Calculate sin and cosine for half that angle
+ float sin = (float)Math.Sin(Math.PI * angle / 180 / 2);
+ float cos = (float)Math.Cos(Math.PI * angle / 180 / 2);
+
+ // Find the points and weight
+ SKPoint point0 = new SKPoint(-radius * sin, radius * cos);
+ SKPoint point1 = new SKPoint(0, radius / cos);
+ SKPoint point2 = new SKPoint(radius * sin, radius * cos);
+ float weight = cos;
+
+ // Draw the points
+ canvas.DrawCircle(point0.X, point0.Y, 10, blackFill);
+ canvas.DrawCircle(point1.X, point1.Y, 10, blackFill);
+ canvas.DrawCircle(point2.X, point2.Y, 10, blackFill);
+
+ // Draw the tangent lines
+ canvas.DrawLine(point0.X, point0.Y, point1.X, point1.Y, dottedStroke);
+ canvas.DrawLine(point2.X, point2.Y, point1.X, point1.Y, dottedStroke);
+
+ // Draw the conic
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(point0);
+ path.ConicTo(point1, point2, weight);
+ canvas.DrawPath(path, redStroke);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCurvePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCurvePage.xaml
new file mode 100644
index 000000000..fe73ba653
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCurvePage.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCurvePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCurvePage.xaml.cs
new file mode 100644
index 000000000..abc8d3a9a
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConicCurvePage.xaml.cs
@@ -0,0 +1,67 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class ConicCurvePage : InteractivePage
+{
+ public ConicCurvePage()
+ {
+ touchPoints = new TouchPoint[3];
+
+ for (int i = 0; i < 3; i++)
+ {
+ TouchPoint touchPoint = new TouchPoint
+ {
+ Center = new SKPoint(100 + 100 * i,
+ 100 + (i == 1 ? 300 : 0))
+ };
+ touchPoints[i] = touchPoint;
+ }
+
+ InitializeComponent();
+ baseCanvasView = canvasView;
+ weightSlider.Value = 0.5;
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Draw path with conic curve
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(touchPoints[0].Center);
+ path.ConicTo(touchPoints[1].Center,
+ touchPoints[2].Center,
+ (float)weightSlider.Value);
+
+ canvas.DrawPath(path, strokePaint);
+ }
+
+ // Draw tangent lines
+ canvas.DrawLine(touchPoints[0].Center.X,
+ touchPoints[0].Center.Y,
+ touchPoints[1].Center.X,
+ touchPoints[1].Center.Y, dottedStrokePaint);
+
+ canvas.DrawLine(touchPoints[1].Center.X,
+ touchPoints[1].Center.Y,
+ touchPoints[2].Center.X,
+ touchPoints[2].Center.Y, dottedStrokePaint);
+
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ touchPoint.Paint(canvas);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConveyorBeltPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConveyorBeltPage.cs
new file mode 100644
index 000000000..126d58844
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ConveyorBeltPage.cs
@@ -0,0 +1,136 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class ConveyorBeltPage : ContentPage
+ {
+ SKCanvasView canvasView;
+ bool pageIsActive = false;
+
+ SKPaint conveyerPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = 20,
+ Color = SKColors.DarkGray
+ };
+
+ SKPath bucketPath = new SKPath();
+
+ SKPaint bucketsPaint = new SKPaint
+ {
+ Color = SKColors.BurlyWood,
+ };
+
+ public ConveyorBeltPage()
+ {
+ Title = "Conveyor Belt";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ // Create the path for the bucket starting with the handle
+ bucketPath.AddRect(new SKRect(-5, -3, 25, 3));
+
+ // Sides
+ bucketPath.AddRoundRect(new SKRect(25, -19, 27, 18), 10, 10, SKPathDirection.CounterClockwise);
+ bucketPath.AddRoundRect(new SKRect(63, -19, 65, 18), 10, 10, SKPathDirection.CounterClockwise);
+
+ // Five slats
+ for (int i = 0; i < 5; i++)
+ {
+ bucketPath.MoveTo(25, -19 + 8 * i);
+ bucketPath.LineTo(25, -13 + 8 * i);
+ bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
+ SKPathDirection.CounterClockwise, 65, -13 + 8 * i);
+ bucketPath.LineTo(65, -19 + 8 * i);
+ bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
+ SKPathDirection.Clockwise, 25, -19 + 8 * i);
+ bucketPath.Close();
+ }
+
+ // Arc to suggest the hidden side
+ bucketPath.MoveTo(25, -17);
+ bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
+ SKPathDirection.Clockwise, 65, -17);
+ bucketPath.LineTo(65, -19);
+ bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
+ SKPathDirection.CounterClockwise, 25, -19);
+ bucketPath.Close();
+
+ // Make it a little bigger and correct the orientation
+ bucketPath.Transform(SKMatrix.CreateScale(-2, 2));
+ bucketPath.Transform(SKMatrix.CreateRotationDegrees(90));
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ pageIsActive = true;
+
+ Dispatcher.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
+ {
+ canvasView.InvalidateSurface();
+ return pageIsActive;
+ });
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ pageIsActive = false;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float width = info.Width / 3;
+ float verticalMargin = width / 2 + 150;
+
+ using (SKPath conveyerPath = new SKPath())
+ {
+ // Straight verticals capped by semicircles on top and bottom
+ conveyerPath.MoveTo(width, verticalMargin);
+ conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
+ SKPathDirection.Clockwise, 2 * width, verticalMargin);
+ conveyerPath.LineTo(2 * width, info.Height - verticalMargin);
+ conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
+ SKPathDirection.Clockwise, width, info.Height - verticalMargin);
+ conveyerPath.Close();
+
+ // Draw the conveyor belt itself
+ canvas.DrawPath(conveyerPath, conveyerPaint);
+
+ // Calculate spacing based on length of conveyer path
+ float length = 2 * (info.Height - 2 * verticalMargin) +
+ 2 * ((float)Math.PI * width / 2);
+
+ // Value will be somewhere around 200
+ float spacing = length / (float)Math.Round(length / 200);
+
+ // Now animate the phase; t is 0 to 1 every 2 seconds
+ TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
+ float t = (float)(timeSpan.TotalSeconds % 2 / 2);
+ float phase = -t * spacing;
+
+ // Create the buckets PathEffect
+ using (SKPathEffect bucketsPathEffect =
+ SKPathEffect.Create1DPath(bucketPath, spacing, phase,
+ SKPath1DPathEffectStyle.Rotate))
+ {
+ // Set it to the Paint object and draw the path again
+ bucketsPaint.PathEffect = bucketsPathEffect;
+ canvas.DrawPath(conveyerPath, bucketsPaint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CurvesMenuPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CurvesMenuPage.xaml
new file mode 100644
index 000000000..68817c104
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CurvesMenuPage.xaml
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CurvesMenuPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CurvesMenuPage.xaml.cs
new file mode 100644
index 000000000..053c4eb41
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/CurvesMenuPage.xaml.cs
@@ -0,0 +1,13 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class CurvesMenuPage : BasePage
+{
+ public CurvesMenuPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/DashedHatchLinesPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/DashedHatchLinesPage.cs
new file mode 100644
index 000000000..1c65979cb
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/DashedHatchLinesPage.cs
@@ -0,0 +1,51 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class DashedHatchLinesPage : ContentPage
+ {
+ static SKPathEffect dashEffect =
+ SKPathEffect.CreateDash(new float[] { 30, 30 }, 0);
+
+ static SKPathEffect hatchEffect = SKPathEffect.Create2DLine(20,
+ Multiply(SKMatrix.CreateScale(60, 60),
+ SKMatrix.CreateRotationDegrees(45)));
+
+ SKPaint paint = new SKPaint()
+ {
+ PathEffect = SKPathEffect.CreateCompose(dashEffect, hatchEffect),
+ StrokeCap = SKStrokeCap.Round,
+ Color = SKColors.Blue
+ };
+
+ public DashedHatchLinesPage()
+ {
+ Title = "Dashed Hatch Lines";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+ canvas.DrawOval(info.Width / 2, info.Height / 2,
+ 0.45f * info.Width, 0.45f * info.Height, paint);
+ }
+
+ static SKMatrix Multiply(SKMatrix first, SKMatrix second)
+ {
+ SKMatrix target = SKMatrix.CreateIdentity();
+ SKMatrix.Concat(ref target, first, second);
+ return target;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/DotDashMorphPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/DotDashMorphPage.cs
new file mode 100644
index 000000000..940761c00
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/DotDashMorphPage.cs
@@ -0,0 +1,112 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class DotDashMorphPage : ContentPage
+ {
+ const float strokeWidth = 30;
+ static readonly float[] dashArray = new float[4];
+
+ SKCanvasView canvasView;
+ bool pageIsActive = false;
+
+ SKPaint ellipsePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = strokeWidth,
+ StrokeCap = SKStrokeCap.Round,
+ Color = SKColors.Blue
+ };
+
+ public DotDashMorphPage()
+ {
+ Title = "Dot / Dash Morph";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ pageIsActive = true;
+
+ Dispatcher.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
+ {
+ canvasView.InvalidateSurface();
+ return pageIsActive;
+ });
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ pageIsActive = false;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Create elliptical path
+ using (SKPath ellipsePath = new SKPath())
+ {
+ ellipsePath.AddOval(new SKRect(50, 50, info.Width - 50, info.Height - 50));
+
+ // Create animated path effect
+ TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
+ float t = (float)(timeSpan.TotalSeconds % 3 / 3);
+ float phase = 0;
+
+ if (t < 0.25f) // 1, 0, 1, 2 --> 0, 2, 0, 2
+ {
+ float tsub = 4 * t;
+ dashArray[0] = strokeWidth * (1 - tsub);
+ dashArray[1] = strokeWidth * 2 * tsub;
+ dashArray[2] = strokeWidth * (1 - tsub);
+ dashArray[3] = strokeWidth * 2;
+ }
+ else if (t < 0.5f) // 0, 2, 0, 2 --> 1, 2, 1, 0
+ {
+ float tsub = 4 * (t - 0.25f);
+ dashArray[0] = strokeWidth * tsub;
+ dashArray[1] = strokeWidth * 2;
+ dashArray[2] = strokeWidth * tsub;
+ dashArray[3] = strokeWidth * 2 * (1 - tsub);
+ phase = strokeWidth * tsub;
+ }
+ else if (t < 0.75f) // 1, 2, 1, 0 --> 0, 2, 0, 2
+ {
+ float tsub = 4 * (t - 0.5f);
+ dashArray[0] = strokeWidth * (1 - tsub);
+ dashArray[1] = strokeWidth * 2;
+ dashArray[2] = strokeWidth * (1 - tsub);
+ dashArray[3] = strokeWidth * 2 * tsub;
+ phase = strokeWidth * (1 - tsub);
+ }
+ else // 0, 2, 0, 2 --> 1, 0, 1, 2
+ {
+ float tsub = 4 * (t - 0.75f);
+ dashArray[0] = strokeWidth * tsub;
+ dashArray[1] = strokeWidth * 2 * (1 - tsub);
+ dashArray[2] = strokeWidth * tsub;
+ dashArray[3] = strokeWidth * 2;
+ }
+
+ using (SKPathEffect pathEffect = SKPathEffect.CreateDash(dashArray, phase))
+ {
+ ellipsePaint.PathEffect = pathEffect;
+ canvas.DrawPath(ellipsePath, ellipsePaint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/EllipticalArcPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/EllipticalArcPage.xaml
new file mode 100644
index 000000000..6f2a08b55
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/EllipticalArcPage.xaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/EllipticalArcPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/EllipticalArcPage.xaml.cs
new file mode 100644
index 000000000..440564e48
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/EllipticalArcPage.xaml.cs
@@ -0,0 +1,71 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class EllipticalArcPage : InteractivePage
+{
+ SKColor[] colors = { SKColors.Red, SKColors.Green, SKColors.Blue, SKColors.Gray };
+
+ public EllipticalArcPage()
+ {
+ touchPoints = new TouchPoint[2];
+
+ for (int i = 0; i < 2; i++)
+ {
+ TouchPoint touchPoint = new TouchPoint
+ {
+ Center = new SKPoint(200, 100 + 200 * i)
+ };
+ touchPoints[i] = touchPoint;
+ }
+ InitializeComponent();
+
+ baseCanvasView = canvasView;
+ xRadiusSlider.Value = 125;
+ yRadiusSlider.Value = 120;
+ }
+
+ void sliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPath path = new SKPath())
+ {
+ int colorIndex = 0;
+ SKPoint ellipseSize = new SKPoint((float)xRadiusSlider.Value,
+ (float)yRadiusSlider.Value);
+ float rotation = (float)rotationSlider.Value;
+
+ foreach (SKPathArcSize arcSize in Enum.GetValues(typeof(SKPathArcSize)))
+ foreach (SKPathDirection direction in Enum.GetValues(typeof(SKPathDirection)))
+ {
+ path.MoveTo(touchPoints[0].Center);
+ path.ArcTo(ellipseSize, rotation,
+ arcSize, direction,
+ touchPoints[1].Center);
+
+ strokePaint.Color = colors[colorIndex++];
+ canvas.DrawPath(path, strokePaint);
+ path.Reset();
+ }
+ }
+
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ touchPoint.Paint(canvas);
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ExplodedPieChartPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ExplodedPieChartPage.cs
new file mode 100644
index 000000000..90a8531fc
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/ExplodedPieChartPage.cs
@@ -0,0 +1,103 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class ExplodedPieChartPage : ContentPage
+ {
+ class ChartData
+ {
+ public ChartData(int value, SKColor color)
+ {
+ Value = value;
+ Color = color;
+ }
+
+ public int Value { private set; get; }
+
+ public SKColor Color { private set; get; }
+ }
+
+ ChartData[] chartData =
+ {
+ new ChartData(45, SKColors.Red),
+ new ChartData(13, SKColors.Green),
+ new ChartData(27, SKColors.Blue),
+ new ChartData(19, SKColors.Magenta),
+ new ChartData(40, SKColors.Cyan),
+ new ChartData(22, SKColors.Brown),
+ new ChartData(29, SKColors.Gray)
+ };
+
+ public ExplodedPieChartPage()
+ {
+ Title = "Exploded Pie Chart";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ int totalValues = 0;
+
+ foreach (ChartData item in chartData)
+ {
+ totalValues += item.Value;
+ }
+
+ SKPoint center = new SKPoint(info.Width / 2, info.Height / 2);
+ float explodeOffset = 50;
+ float radius = Math.Min(info.Width / 2, info.Height / 2) - 2 * explodeOffset;
+ SKRect rect = new SKRect(center.X - radius, center.Y - radius,
+ center.X + radius, center.Y + radius);
+
+ float startAngle = 0;
+
+ foreach (ChartData item in chartData)
+ {
+ float sweepAngle = 360f * item.Value / totalValues;
+
+ using (SKPath path = new SKPath())
+ using (SKPaint fillPaint = new SKPaint())
+ using (SKPaint outlinePaint = new SKPaint())
+ {
+ path.MoveTo(center);
+ path.ArcTo(rect, startAngle, sweepAngle, false);
+ path.Close();
+
+ fillPaint.Style = SKPaintStyle.Fill;
+ fillPaint.Color = item.Color;
+
+ outlinePaint.Style = SKPaintStyle.Stroke;
+ outlinePaint.StrokeWidth = 5;
+ outlinePaint.Color = SKColors.Black;
+
+ // Calculate "explode" transform
+ float angle = startAngle + 0.5f * sweepAngle;
+ float x = explodeOffset * (float)Math.Cos(Math.PI * angle / 180);
+ float y = explodeOffset * (float)Math.Sin(Math.PI * angle / 180);
+
+ canvas.Save();
+ canvas.Translate(x, y);
+
+ // Fill and stroke the path
+ canvas.DrawPath(path, fillPaint);
+ canvas.DrawPath(path, outlinePaint);
+ canvas.Restore();
+ }
+
+ startAngle += sweepAngle;
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/FourCircleIntersectClipPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/FourCircleIntersectClipPage.cs
new file mode 100644
index 000000000..ccb59e6fd
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/FourCircleIntersectClipPage.cs
@@ -0,0 +1,60 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class FourCircleIntersectClipPage : ContentPage
+ {
+ public FourCircleIntersectClipPage()
+ {
+ Title = "Four Circle Intersect Clip";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float size = Math.Min(info.Width, info.Height);
+ float radius = 0.4f * size;
+ float offset = size / 2 - radius;
+
+ // Translate to center
+ canvas.Translate(info.Width / 2, info.Height / 2);
+
+ using (SKPath path = new SKPath())
+ {
+ path.AddCircle(-offset, -offset, radius);
+ canvas.ClipPath(path, SKClipOperation.Intersect);
+
+ path.Reset();
+ path.AddCircle(-offset, offset, radius);
+ canvas.ClipPath(path, SKClipOperation.Intersect);
+
+ path.Reset();
+ path.AddCircle(offset, -offset, radius);
+ canvas.ClipPath(path, SKClipOperation.Intersect);
+
+ path.Reset();
+ path.AddCircle(offset, offset, radius);
+ canvas.ClipPath(path, SKClipOperation.Intersect);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = SKColors.Blue;
+ canvas.DrawPaint(paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/FourLeafCloverPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/FourLeafCloverPage.cs
new file mode 100644
index 000000000..f8d610b7b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/FourLeafCloverPage.cs
@@ -0,0 +1,95 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class FourLeafCloverPage : ContentPage
+ {
+ public FourLeafCloverPage()
+ {
+ Title = "Four-Leaf Clover";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float xCenter = info.Width / 2;
+ float yCenter = info.Height / 2;
+ float radius = 0.24f * Math.Min(info.Width, info.Height);
+
+ using (SKRegion wholeScreenRegion = new SKRegion())
+ {
+ wholeScreenRegion.SetRect(new SKRectI(0, 0, info.Width, info.Height));
+
+ using (SKRegion leftRegion = new SKRegion(wholeScreenRegion))
+ using (SKRegion rightRegion = new SKRegion(wholeScreenRegion))
+ using (SKRegion topRegion = new SKRegion(wholeScreenRegion))
+ using (SKRegion bottomRegion = new SKRegion(wholeScreenRegion))
+ {
+ using (SKPath circlePath = new SKPath())
+ {
+ // Make basic circle path
+ circlePath.AddCircle(xCenter, yCenter, radius);
+
+ // Left leaf
+ circlePath.Transform(SKMatrix.CreateTranslation(-radius, 0));
+ leftRegion.SetPath(circlePath);
+
+ // Right leaf
+ circlePath.Transform(SKMatrix.CreateTranslation(2 * radius, 0));
+ rightRegion.SetPath(circlePath);
+
+ // Make union of right with left
+ leftRegion.Op(rightRegion, SKRegionOperation.Union);
+
+ // Top leaf
+ circlePath.Transform(SKMatrix.CreateTranslation(-radius, -radius));
+ topRegion.SetPath(circlePath);
+
+ // Combine with bottom leaf
+ circlePath.Transform(SKMatrix.CreateTranslation(0, 2 * radius));
+ bottomRegion.SetPath(circlePath);
+
+ // Make union of top with bottom
+ bottomRegion.Op(topRegion, SKRegionOperation.Union);
+
+ // Exclusive-OR left and right with top and bottom
+ leftRegion.Op(bottomRegion, SKRegionOperation.XOR);
+
+ // Set that as clip region
+ canvas.ClipRegion(leftRegion);
+
+ // Set transform for drawing lines from center
+ canvas.Translate(xCenter, yCenter);
+
+ // Draw 360 lines
+ for (double angle = 0; angle < 360; angle++)
+ {
+ float x = 2 * radius * (float)Math.Cos(Math.PI * angle / 180);
+ float y = 2 * radius * (float)Math.Sin(Math.PI * angle / 180);
+
+ using (SKPaint strokePaint = new SKPaint())
+ {
+ strokePaint.Color = SKColors.Green;
+ strokePaint.StrokeWidth = 2;
+
+ canvas.DrawLine(0, 0, x, y, strokePaint);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/GlobularTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/GlobularTextPage.cs
new file mode 100644
index 000000000..4745ffd3b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/GlobularTextPage.cs
@@ -0,0 +1,70 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class GlobularTextPage : ContentPage
+ {
+ SKPath globePath;
+
+ public GlobularTextPage()
+ {
+ Title = "Globular Text";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ using (SKPaint textPaint = new SKPaint())
+ {
+ textPaint.Typeface = SKTypeface.FromFamilyName("Times New Roman");
+ textPaint.TextSize = 100;
+
+ using (SKPath textPath = textPaint.GetTextPath("HELLO", 0, 0))
+ {
+ SKRect textPathBounds;
+ textPath.GetBounds(out textPathBounds);
+
+ globePath = textPath.CloneWithTransform((SKPoint pt) =>
+ {
+ double longitude = (Math.PI / textPathBounds.Width) *
+ (pt.X - textPathBounds.Left) - Math.PI / 2;
+ double latitude = (Math.PI / textPathBounds.Height) *
+ (pt.Y - textPathBounds.Top) - Math.PI / 2;
+
+ longitude *= 0.75;
+ latitude *= 0.75;
+
+ float x = (float)(Math.Cos(latitude) * Math.Sin(longitude));
+ float y = (float)Math.Sin(latitude);
+
+ return new SKPoint(x, y);
+ });
+ }
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint pathPaint = new SKPaint())
+ {
+ pathPaint.Style = SKPaintStyle.Fill;
+ pathPaint.Color = SKColors.Blue;
+ pathPaint.StrokeWidth = 3;
+ pathPaint.IsAntialias = true;
+
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.Scale(0.45f * Math.Min(info.Width, info.Height)); // radius
+ canvas.DrawPath(globePath, pathPaint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/HatchFillPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/HatchFillPage.cs
new file mode 100644
index 000000000..8de0ba1b8
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/HatchFillPage.cs
@@ -0,0 +1,80 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class HatchFillPage : ContentPage
+ {
+ SKPaint fillPaint = new SKPaint();
+
+ SKPathEffect horzLinesPath = SKPathEffect.Create2DLine(3, SKMatrix.CreateScale(6, 6));
+
+ SKPathEffect vertLinesPath = SKPathEffect.Create2DLine(6,
+ Multiply(SKMatrix.CreateRotationDegrees(90), SKMatrix.CreateScale(24, 24)));
+
+ SKPathEffect diagLinesPath = SKPathEffect.Create2DLine(12,
+ Multiply(SKMatrix.CreateScale(36, 36), SKMatrix.CreateRotationDegrees(45)));
+
+ SKPaint strokePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = 3,
+ Color = SKColors.Black
+ };
+
+ public HatchFillPage()
+ {
+ Title = "Hatch Fill";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPath roundRectPath = new SKPath())
+ {
+ // Create a path
+ roundRectPath.AddRoundRect(new SKRect(50, 50, info.Width - 50, info.Height - 50), 100, 100);
+
+ // Horizontal hatch marks
+ fillPaint.PathEffect = horzLinesPath;
+ fillPaint.Color = SKColors.Red;
+ canvas.DrawPath(roundRectPath, fillPaint);
+
+ // Vertical hatch marks
+ fillPaint.PathEffect = vertLinesPath;
+ fillPaint.Color = SKColors.Blue;
+ canvas.DrawPath(roundRectPath, fillPaint);
+
+ // Diagonal hatch marks -- use clipping
+ fillPaint.PathEffect = diagLinesPath;
+ fillPaint.Color = SKColors.Green;
+
+ canvas.Save();
+ canvas.ClipPath(roundRectPath);
+ canvas.DrawRect(new SKRect(0, 0, info.Width, info.Height), fillPaint);
+ canvas.Restore();
+
+ // Outline the path
+ canvas.DrawPath(roundRectPath, strokePaint);
+ }
+ }
+
+ static SKMatrix Multiply(SKMatrix first, SKMatrix second)
+ {
+ SKMatrix target = SKMatrix.CreateIdentity();
+ SKMatrix.Concat(ref target, first, second);
+ return target;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/JitterExperimentPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/JitterExperimentPage.xaml
new file mode 100644
index 000000000..b9c2934f1
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/JitterExperimentPage.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/JitterExperimentPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/JitterExperimentPage.xaml.cs
new file mode 100644
index 000000000..32e127a05
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/JitterExperimentPage.xaml.cs
@@ -0,0 +1,47 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class JitterExperimentPage : ContentPage
+{
+ public JitterExperimentPage()
+ {
+ InitializeComponent();
+ }
+
+ void sliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float segLength = (float)segLengthSlider.Value;
+ float deviation = (float)deviationSlider.Value;
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 5;
+ paint.Color = SKColors.Blue;
+
+ using (SKPathEffect pathEffect = SKPathEffect.CreateDiscrete(segLength, deviation))
+ {
+ paint.PathEffect = pathEffect;
+
+ SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
+ canvas.DrawRect(rect, paint);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/JitterTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/JitterTextPage.cs
new file mode 100644
index 000000000..82b62d013
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/JitterTextPage.cs
@@ -0,0 +1,50 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class JitterTextPage : ContentPage
+ {
+ public JitterTextPage()
+ {
+ Title = "Jitter Text";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ string text = "FUZZY";
+
+ using (SKPaint textPaint = new SKPaint())
+ {
+ textPaint.Color = SKColors.Purple;
+ textPaint.PathEffect = SKPathEffect.CreateDiscrete(3f, 10f);
+
+ // Adjust TextSize property so text is 95% of screen width
+ float textWidth = textPaint.MeasureText(text);
+ textPaint.TextSize *= 0.95f * info.Width / textWidth;
+
+ // Find the text bounds
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText(text, ref textBounds);
+
+ // Calculate offsets to center the text on the screen
+ float xText = info.Width / 2 - textBounds.MidX;
+ float yText = info.Height / 2 - textBounds.MidY;
+
+ canvas.DrawText(text, xText, yText, textPaint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/LinkedChainPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/LinkedChainPage.cs
new file mode 100644
index 000000000..5b4949345
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/LinkedChainPage.cs
@@ -0,0 +1,110 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class LinkedChainPage : ContentPage
+ {
+ const float linkRadius = 30;
+ const float linkThickness = 5;
+
+ Func catenary = (float a, float x) => (float)(a * Math.Cosh(x / a));
+
+ SKPaint linksPaint = new SKPaint
+ {
+ Color = SKColors.Silver
+ };
+
+ public LinkedChainPage()
+ {
+ Title = "Linked Chain";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ // Create the path for the individual links
+ SKRect outer = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
+ SKRect inner = outer;
+ inner.Inflate(-linkThickness, -linkThickness);
+
+ using (SKPath linkPath = new SKPath())
+ {
+ linkPath.AddArc(outer, 55, 160);
+ linkPath.ArcTo(inner, 215, -160, false);
+ linkPath.Close();
+
+ linkPath.AddArc(outer, 235, 160);
+ linkPath.ArcTo(inner, 395, -160, false);
+ linkPath.Close();
+
+ // Set that path as the 1D path effect for linksPaint
+ linksPaint.PathEffect =
+ SKPathEffect.Create1DPath(linkPath, 1.3f * linkRadius, 0,
+ SKPath1DPathEffectStyle.Rotate);
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear(SKColors.Black);
+
+ // Width and height of catenary
+ int width = info.Width;
+ float height = info.Height - linkRadius;
+
+ // Find the optimum 'a' for this width and height
+ float optA = FindOptimumA(width, height);
+
+ // Calculate the vertical offset for that value of 'a'
+ float yOffset = catenary(optA, -width / 2);
+
+ // Create a path for the catenary
+ SKPoint[] points = new SKPoint[width];
+
+ for (int x = 0; x < width; x++)
+ {
+ points[x] = new SKPoint(x, yOffset - catenary(optA, x - width / 2));
+ }
+
+ using (SKPath path = new SKPath())
+ {
+ path.AddPoly(points, false);
+
+ // And render that path with the linksPaint object
+ canvas.DrawPath(path, linksPaint);
+ }
+ }
+
+ float FindOptimumA(float width, float height)
+ {
+ Func left = (float a) => (float)Math.Cosh(width / 2 / a);
+ Func right = (float a) => 1 + height / a;
+
+ float gtA = 1; // starting value for left > right
+ float ltA = 10000; // starting value for left < right
+
+ while (Math.Abs(gtA - ltA) > 0.1f)
+ {
+ float avgA = (gtA + ltA) / 2;
+
+ if (left(avgA) < right(avgA))
+ {
+ ltA = avgA;
+ }
+ else
+ {
+ gtA = avgA;
+ }
+ }
+
+ return (gtA + ltA) / 2;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/MonkeyThroughKeyholePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/MonkeyThroughKeyholePage.cs
new file mode 100644
index 000000000..af782c75e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/MonkeyThroughKeyholePage.cs
@@ -0,0 +1,58 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class MonkeyThroughKeyholePage : ContentPage
+ {
+ SKBitmap bitmap;
+ SKPath keyholePath = SKPath.ParseSvgPathData(
+ "M 300 130 L 250 350 L 450 350 L 400 130 A 70 70 0 1 0 300 130 Z");
+
+ public MonkeyThroughKeyholePage()
+ {
+ Title = "Monkey through Keyhole";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.seatedmonkey.jpg"))
+ {
+ bitmap = SKBitmap.Decode(stream);
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Set transform to center and enlarge clip path to window height
+ SKRect bounds;
+ keyholePath.GetTightBounds(out bounds);
+
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.Scale(0.98f * info.Height / bounds.Height);
+ canvas.Translate(-bounds.MidX, -bounds.MidY);
+
+ // Set the clip path
+ canvas.ClipPath(keyholePath);
+
+ // Reset transforms
+ canvas.ResetMatrix();
+
+ // Display monkey to fill height of window but maintain aspect ratio
+ canvas.DrawBitmap(bitmap,
+ new SKRect((info.Width - info.Height) / 2, 0,
+ (info.Width + info.Height) / 2, info.Height));
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/OneDimensionalPathEffectPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/OneDimensionalPathEffectPage.xaml
new file mode 100644
index 000000000..35e9feeca
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/OneDimensionalPathEffectPage.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Translate
+ Rotate
+ Morph
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/OneDimensionalPathEffectPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/OneDimensionalPathEffectPage.xaml.cs
new file mode 100644
index 000000000..d45fb7173
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/OneDimensionalPathEffectPage.xaml.cs
@@ -0,0 +1,71 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class OneDimensionalPathEffectPage : ContentPage
+{
+ SKPathEffect translatePathEffect =
+ SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 -10 L 10 -10, 10 10, -10 10 Z"),
+ 24, 0, SKPath1DPathEffectStyle.Translate);
+
+ SKPathEffect rotatePathEffect =
+ SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 0 L 0 -10, 10 0, 0 10 Z"),
+ 20, 0, SKPath1DPathEffectStyle.Rotate);
+
+ SKPathEffect morphPathEffect =
+ SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -25 -10 L 25 -10, 25 10, -25 10 Z"),
+ 55, 0, SKPath1DPathEffectStyle.Morph);
+
+ SKPaint pathPaint = new SKPaint
+ {
+ Color = SKColors.Blue
+ };
+
+ public OneDimensionalPathEffectPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(new SKPoint(0, 0));
+ path.CubicTo(new SKPoint(2 * info.Width, info.Height),
+ new SKPoint(-info.Width, info.Height),
+ new SKPoint(info.Width, 0));
+
+ switch ((string)effectStylePicker.SelectedItem)
+ {
+ case "Translate":
+ pathPaint.PathEffect = translatePathEffect;
+ break;
+
+ case "Rotate":
+ pathPaint.PathEffect = rotatePathEffect;
+ break;
+
+ case "Morph":
+ pathPaint.PathEffect = morphPathEffect;
+ break;
+ }
+
+ canvas.DrawPath(path, pathPaint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathDataCatPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathDataCatPage.cs
new file mode 100644
index 000000000..d74756bc0
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathDataCatPage.cs
@@ -0,0 +1,66 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class PathDataCatPage : ContentPage
+ {
+ SKPath catPath = SKPath.ParseSvgPathData(
+ "M 160 140 L 150 50 220 103" + // Left ear
+ "M 320 140 L 330 50 260 103" + // Right ear
+ "M 215 230 L 40 200" + // Left whiskers
+ "M 215 240 L 40 240" +
+ "M 215 250 L 40 280" +
+ "M 265 230 L 440 200" + // Right whiskers
+ "M 265 240 L 440 240" +
+ "M 265 250 L 440 280" +
+ "M 240 100" + // Head
+ "A 100 100 0 0 1 240 300" +
+ "A 100 100 0 0 1 240 100 Z" +
+ "M 180 170" + // Left eye
+ "A 40 40 0 0 1 220 170" +
+ "A 40 40 0 0 1 180 170 Z" +
+ "M 300 170" + // Right eye
+ "A 40 40 0 0 1 260 170" +
+ "A 40 40 0 0 1 300 170 Z");
+
+ SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Orange,
+ StrokeWidth = 5
+ };
+
+ public PathDataCatPage()
+ {
+ Title = "Path Data Cat";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear(SKColors.Black);
+
+ SKRect bounds;
+ catPath.GetBounds(out bounds);
+
+ canvas.Translate(info.Width / 2, info.Height / 2);
+
+ canvas.Scale(0.9f * Math.Min(info.Width / bounds.Width,
+ info.Height / bounds.Height));
+
+ canvas.Translate(-bounds.MidX, -bounds.MidY);
+
+ canvas.DrawPath(catPath, paint);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathDataHelloPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathDataHelloPage.cs
new file mode 100644
index 000000000..16661c23b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathDataHelloPage.cs
@@ -0,0 +1,56 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class PathDataHelloPage : ContentPage
+ {
+ SKPath helloPath = SKPath.ParseSvgPathData(
+ "M 0 0 L 0 100 M 0 50 L 50 50 M 50 0 L 50 100" + // H
+ "M 125 0 C 60 -10, 60 60, 125 50, 60 40, 60 110, 125 100" + // E
+ "M 150 0 L 150 100, 200 100" + // L
+ "M 225 0 L 225 100, 275 100" + // L
+ "M 300 50 A 25 50 0 1 0 300 49.9 Z"); // O
+
+ SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Blue,
+ StrokeWidth = 10,
+ StrokeCap = SKStrokeCap.Round,
+ StrokeJoin = SKStrokeJoin.Round
+ };
+
+ public PathDataHelloPage()
+ {
+ Title = "Path Data Hello";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKRect bounds;
+ helloPath.GetTightBounds(out bounds);
+
+ canvas.Translate(info.Width / 2, info.Height / 2);
+
+ canvas.Scale(info.Width / (bounds.Width + paint.StrokeWidth),
+ info.Height / (bounds.Height + paint.StrokeWidth));
+
+ canvas.Translate(-bounds.MidX, -bounds.MidY);
+
+ canvas.DrawPath(helloPath, paint);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathExtensions.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathExtensions.cs
new file mode 100644
index 000000000..dfedf1944
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathExtensions.cs
@@ -0,0 +1,167 @@
+using SkiaSharp;
+
+namespace SkiaSharpDemos.Curves
+{
+ static class PathExtensions
+ {
+ public static SKPath CloneWithTransform(this SKPath pathIn, Func transform)
+ {
+ SKPath pathOut = new SKPath();
+
+ using (SKPath.RawIterator iterator = pathIn.CreateRawIterator())
+ {
+ SKPoint[] points = new SKPoint[4];
+ SKPathVerb pathVerb = SKPathVerb.Move;
+ SKPoint firstPoint = new SKPoint();
+ SKPoint lastPoint = new SKPoint();
+
+ while ((pathVerb = iterator.Next(points)) != SKPathVerb.Done)
+ {
+ switch (pathVerb)
+ {
+ case SKPathVerb.Move:
+ pathOut.MoveTo(transform(points[0]));
+ firstPoint = lastPoint = points[0];
+ break;
+
+ case SKPathVerb.Line:
+ SKPoint[] linePoints = Interpolate(points[0], points[1]);
+
+ foreach (SKPoint pt in linePoints)
+ {
+ pathOut.LineTo(transform(pt));
+ }
+
+ lastPoint = points[1];
+ break;
+
+ case SKPathVerb.Cubic:
+ SKPoint[] cubicPoints = FlattenCubic(points[0], points[1], points[2], points[3]);
+
+ foreach (SKPoint pt in cubicPoints)
+ {
+ pathOut.LineTo(transform(pt));
+ }
+
+ lastPoint = points[3];
+ break;
+
+ case SKPathVerb.Quad:
+ SKPoint[] quadPoints = FlattenQuadratic(points[0], points[1], points[2]);
+
+ foreach (SKPoint pt in quadPoints)
+ {
+ pathOut.LineTo(transform(pt));
+ }
+
+ lastPoint = points[2];
+ break;
+
+ case SKPathVerb.Conic:
+ SKPoint[] conicPoints = FlattenConic(points[0], points[1], points[2], iterator.ConicWeight());
+
+ foreach (SKPoint pt in conicPoints)
+ {
+ pathOut.LineTo(transform(pt));
+ }
+
+ lastPoint = points[2];
+ break;
+
+ case SKPathVerb.Close:
+ SKPoint[] closePoints = Interpolate(lastPoint, firstPoint);
+
+ foreach (SKPoint pt in closePoints)
+ {
+ pathOut.LineTo(transform(pt));
+ }
+
+ firstPoint = lastPoint = new SKPoint(0, 0);
+ pathOut.Close();
+ break;
+ }
+ }
+ }
+ return pathOut;
+ }
+
+ static SKPoint[] Interpolate(SKPoint pt0, SKPoint pt1)
+ {
+ int count = (int)Math.Max(1, Length(pt0, pt1));
+ SKPoint[] points = new SKPoint[count];
+
+ for (int i = 0; i < count; i++)
+ {
+ float t = (i + 1f) / count;
+ float x = (1 - t) * pt0.X + t * pt1.X;
+ float y = (1 - t) * pt0.Y + t * pt1.Y;
+ points[i] = new SKPoint(x, y);
+ }
+
+ return points;
+ }
+
+ static SKPoint[] FlattenCubic(SKPoint pt0, SKPoint pt1, SKPoint pt2, SKPoint pt3)
+ {
+ int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2) + Length(pt2, pt3));
+ SKPoint[] points = new SKPoint[count];
+
+ for (int i = 0; i < count; i++)
+ {
+ float t = (i + 1f) / count;
+ float x = (1 - t) * (1 - t) * (1 - t) * pt0.X +
+ 3 * t * (1 - t) * (1 - t) * pt1.X +
+ 3 * t * t * (1 - t) * pt2.X +
+ t * t * t * pt3.X;
+ float y = (1 - t) * (1 - t) * (1 - t) * pt0.Y +
+ 3 * t * (1 - t) * (1 - t) * pt1.Y +
+ 3 * t * t * (1 - t) * pt2.Y +
+ t * t * t * pt3.Y;
+ points[i] = new SKPoint(x, y);
+ }
+
+ return points;
+ }
+
+ static SKPoint[] FlattenQuadratic(SKPoint pt0, SKPoint pt1, SKPoint pt2)
+ {
+ int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
+ SKPoint[] points = new SKPoint[count];
+
+ for (int i = 0; i < count; i++)
+ {
+ float t = (i + 1f) / count;
+ float x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X;
+ float y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y;
+ points[i] = new SKPoint(x, y);
+ }
+
+ return points;
+ }
+
+ static SKPoint[] FlattenConic(SKPoint pt0, SKPoint pt1, SKPoint pt2, float weight)
+ {
+ int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
+ SKPoint[] points = new SKPoint[count];
+
+ for (int i = 0; i < count; i++)
+ {
+ float t = (i + 1f) / count;
+ float denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t;
+ float x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X;
+ float y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y;
+ x /= denominator;
+ y /= denominator;
+ points[i] = new SKPoint(x, y);
+ }
+
+ return points;
+ }
+
+ static double Length(SKPoint pt0, SKPoint pt1)
+ {
+ return Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2));
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathLengthPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathLengthPage.xaml
new file mode 100644
index 000000000..1cfb7362c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathLengthPage.xaml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathLengthPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathLengthPage.xaml.cs
new file mode 100644
index 000000000..5b2c16b92
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathLengthPage.xaml.cs
@@ -0,0 +1,70 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class PathLengthPage : InteractivePage
+{
+ const string text = "Compute length of path";
+
+ static SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Black,
+ TextSize = 10,
+ };
+
+ static readonly float baseTextWidth = textPaint.MeasureText(text);
+
+ public PathLengthPage()
+ {
+ touchPoints = new TouchPoint[4];
+
+ for (int i = 0; i < 4; i++)
+ {
+ TouchPoint touchPoint = new TouchPoint
+ {
+ Center = new SKPoint(100 + 200 * (i % 2),
+ 100 + 200 * i)
+ };
+ touchPoints[i] = touchPoint;
+ }
+
+ InitializeComponent();
+ baseCanvasView = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Draw path with cubic Bezier curve
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(touchPoints[0].Center);
+ path.CubicTo(touchPoints[1].Center,
+ touchPoints[2].Center,
+ touchPoints[3].Center);
+
+ canvas.DrawPath(path, strokePaint);
+
+ // Get path length
+ SKPathMeasure pathMeasure = new SKPathMeasure(path, false, 1);
+
+ // Find new text size
+ textPaint.TextSize = pathMeasure.Length / baseTextWidth * 10;
+
+ // Draw text on path
+ canvas.DrawTextOnPath(text, path, 0, 0, textPaint);
+ }
+
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ touchPoint.Paint(canvas);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathTileFillPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathTileFillPage.cs
new file mode 100644
index 000000000..aeec343ab
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PathTileFillPage.cs
@@ -0,0 +1,48 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class PathTileFillPage : ContentPage
+ {
+ SKPath tilePath = SKPath.ParseSvgPathData(
+ "M -20 -20 L 2 -20, 2 -40, 18 -40, 18 -20, 40 -20, " +
+ "40 -12, 20 -12, 20 12, 40 12, 40 40, 22 40, 22 20, " +
+ "-2 20, -2 40, -20 40, -20 8, -40 8, -40 -8, -20 -8 Z");
+
+ public PathTileFillPage()
+ {
+ Title = "Path Tile Fill";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Color = SKColors.Red;
+
+ using (SKPathEffect pathEffect =
+ SKPathEffect.Create2DPath(SKMatrix.MakeScale(64, 64), tilePath))
+ {
+ paint.PathEffect = pathEffect;
+
+ canvas.DrawRoundRect(
+ new SKRect(50, 50, info.Width - 50, info.Height - 50),
+ 100, 100, paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PrettyAnalogClockPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PrettyAnalogClockPage.cs
new file mode 100644
index 000000000..a4d7ef25d
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/PrettyAnalogClockPage.cs
@@ -0,0 +1,139 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class PrettyAnalogClockPage : ContentPage
+ {
+ SKCanvasView canvasView;
+ bool pageIsActive;
+
+ // Clock hands pointing straight up
+ SKPath hourHandPath = SKPath.ParseSvgPathData(
+ "M 0 -60 C 0 -30 20 -30 5 -20 L 5 0" +
+ "C 5 7.5 -5 7.5 -5 0 L -5 -20" +
+ "C -20 -30 0 -30 0 -60 Z");
+
+ SKPath minuteHandPath = SKPath.ParseSvgPathData(
+ "M 0 -80 C 0 -75 0 -70 2.5 -60 L 2.5 0" +
+ "C 2.5 5 -2.5 5 -2.5 0 L -2.5 -60" +
+ "C 0 -70 0 -75 0 -80 Z");
+
+ SKPath secondHandPath = SKPath.ParseSvgPathData(
+ "M 0 10 L 0 -80");
+
+ // SKPaint objects
+ SKPaint handStrokePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 2,
+ StrokeCap = SKStrokeCap.Round
+ };
+
+ SKPaint handFillPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Gray
+ };
+
+ SKPaint minuteMarkPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 3,
+ StrokeCap = SKStrokeCap.Round,
+ PathEffect = SKPathEffect.CreateDash(new float[] { 0, 3 * 3.14159f }, 0)
+ };
+
+ SKPaint hourMarkPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 6,
+ StrokeCap = SKStrokeCap.Round,
+ PathEffect = SKPathEffect.CreateDash(new float[] { 0, 15 * 3.14159f }, 0)
+ };
+
+ public PrettyAnalogClockPage()
+ {
+ Title = "Pretty Analog Clock";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ pageIsActive = true;
+
+ Dispatcher.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
+ {
+ canvasView.InvalidateSurface();
+ return pageIsActive;
+ });
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ pageIsActive = false;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Transform for 100-radius circle in center
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.Scale(Math.Min(info.Width / 200, info.Height / 200));
+
+ // Draw circles for hour and minute marks
+ SKRect rect = new SKRect(-90, -90, 90, 90);
+ canvas.DrawOval(rect, minuteMarkPaint);
+ canvas.DrawOval(rect, hourMarkPaint);
+
+ // Get time
+ DateTime dateTime = DateTime.Now;
+
+ // Draw hour hand
+ canvas.Save();
+ canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
+ canvas.DrawPath(hourHandPath, handStrokePaint);
+ canvas.DrawPath(hourHandPath, handFillPaint);
+ canvas.Restore();
+
+ // Draw minute hand
+ canvas.Save();
+ canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
+ canvas.DrawPath(minuteHandPath, handStrokePaint);
+ canvas.DrawPath(minuteHandPath, handFillPaint);
+ canvas.Restore();
+
+ // Draw second hand
+ double t = dateTime.Millisecond / 1000.0;
+
+ if (t < 0.5)
+ {
+ t = 0.5 * Easing.SpringIn.Ease(t / 0.5);
+ }
+ else
+ {
+ t = 0.5 * (1 + Easing.SpringOut.Ease((t - 0.5) / 0.5));
+ }
+
+ canvas.Save();
+ canvas.RotateDegrees(6 * (dateTime.Second + (float)t));
+ canvas.DrawPath(secondHandPath, handStrokePaint);
+ canvas.Restore();
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/QuadraticCurvePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/QuadraticCurvePage.xaml
new file mode 100644
index 000000000..1dde64a73
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/QuadraticCurvePage.xaml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/QuadraticCurvePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/QuadraticCurvePage.xaml.cs
new file mode 100644
index 000000000..5dfe4e5e2
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/QuadraticCurvePage.xaml.cs
@@ -0,0 +1,60 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class QuadraticCurvePage : InteractivePage
+{
+ public QuadraticCurvePage()
+ {
+ touchPoints = new TouchPoint[3];
+
+ for (int i = 0; i < 3; i++)
+ {
+ TouchPoint touchPoint = new TouchPoint
+ {
+ Center = new SKPoint(100 + 100 * i,
+ 100 + (i == 1 ? 300 : 0))
+ };
+ touchPoints[i] = touchPoint;
+ }
+
+ InitializeComponent();
+ baseCanvasView = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Draw path with quadratic Bezier
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(touchPoints[0].Center);
+ path.QuadTo(touchPoints[1].Center,
+ touchPoints[2].Center);
+
+ canvas.DrawPath(path, strokePaint);
+ }
+
+ // Draw tangent lines
+ canvas.DrawLine(touchPoints[0].Center.X,
+ touchPoints[0].Center.Y,
+ touchPoints[1].Center.X,
+ touchPoints[1].Center.Y, dottedStrokePaint);
+
+ canvas.DrawLine(touchPoints[1].Center.X,
+ touchPoints[1].Center.Y,
+ touchPoints[2].Center.X,
+ touchPoints[2].Center.Y, dottedStrokePaint);
+
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ touchPoint.Paint(canvas);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/RegionOperationsPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/RegionOperationsPage.cs
new file mode 100644
index 000000000..334becde4
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/RegionOperationsPage.cs
@@ -0,0 +1,100 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class RegionOperationsPage : ContentPage
+ {
+ SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Black,
+ TextSize = 40,
+ TextAlign = SKTextAlign.Center
+ };
+
+ SKPaint fillPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Pink
+ };
+
+ public RegionOperationsPage()
+ {
+ Title = "Region Operations";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float x = 0;
+ float y = 0;
+ float width = info.Height > info.Width ? info.Width / 2 : info.Width / 3;
+ float height = info.Height > info.Width ? info.Height / 3 : info.Height / 2;
+
+ foreach (SKRegionOperation regionOp in Enum.GetValues(typeof(SKRegionOperation)))
+ {
+ DisplayClipOp(canvas, new SKRect(x, y, x + width, y + height), regionOp);
+
+ if ((x += width) >= info.Width)
+ {
+ x = 0;
+ y += height;
+ }
+ }
+ }
+
+ void DisplayClipOp(SKCanvas canvas, SKRect rect, SKRegionOperation regionOp)
+ {
+ float textSize = textPaint.TextSize;
+ canvas.DrawText(regionOp.ToString(), rect.MidX, rect.Top + textSize, textPaint);
+ rect.Top += textSize;
+
+ float radius = 0.9f * Math.Min(rect.Width / 3, rect.Height / 2);
+ float xCenter = rect.MidX;
+ float yCenter = rect.MidY;
+
+ SKRectI recti = new SKRectI((int)rect.Left, (int)rect.Top,
+ (int)rect.Right, (int)rect.Bottom);
+
+ using (SKRegion wholeRectRegion = new SKRegion())
+ {
+ wholeRectRegion.SetRect(recti);
+
+ using (SKRegion region1 = new SKRegion(wholeRectRegion))
+ using (SKRegion region2 = new SKRegion(wholeRectRegion))
+ {
+ using (SKPath path1 = new SKPath())
+ {
+ path1.AddCircle(xCenter - radius / 2, yCenter, radius);
+ region1.SetPath(path1);
+ }
+
+ using (SKPath path2 = new SKPath())
+ {
+ path2.AddCircle(xCenter + radius / 2, yCenter, radius);
+ region2.SetPath(path2);
+ }
+
+ region1.Op(region2, regionOp);
+
+ canvas.Save();
+ canvas.ClipRegion(region1);
+ canvas.DrawPaint(fillPaint);
+ canvas.Restore();
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/RegionPaintPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/RegionPaintPage.cs
new file mode 100644
index 000000000..8664c9528
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/RegionPaintPage.cs
@@ -0,0 +1,66 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class RegionPaintPage : ContentPage
+ {
+ public RegionPaintPage()
+ {
+ Title = "Region Paint";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ int radius = 10;
+
+ // Create circular path
+ using (SKPath circlePath = new SKPath())
+ {
+ circlePath.AddCircle(0, 0, radius);
+
+ // Create circular region
+ using (SKRegion circleRegion = new SKRegion())
+ {
+ circleRegion.SetRect(new SKRectI(-radius, -radius, radius, radius));
+ circleRegion.SetPath(circlePath);
+
+ // Set transform to move it to center and scale up
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.Scale(Math.Min(info.Width / 2, info.Height / 2) / radius);
+
+ // Fill region
+ using (SKPaint fillPaint = new SKPaint())
+ {
+ fillPaint.Style = SKPaintStyle.Fill;
+ fillPaint.Color = SKColors.Orange;
+
+ canvas.DrawRegion(circleRegion, fillPaint);
+ }
+
+ // Stroke path for comparison
+ using (SKPaint strokePaint = new SKPaint())
+ {
+ strokePaint.Style = SKPaintStyle.Stroke;
+ strokePaint.Color = SKColors.Blue;
+ strokePaint.StrokeWidth = 0.1f;
+
+ canvas.DrawPath(circlePath, strokePaint);
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/RoundedHeptagonPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/RoundedHeptagonPage.cs
new file mode 100644
index 000000000..20292a965
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/RoundedHeptagonPage.cs
@@ -0,0 +1,83 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class RoundedHeptagonPage : ContentPage
+ {
+ public RoundedHeptagonPage()
+ {
+ Title = "Rounded Heptagon";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float cornerRadius = 100;
+ int numVertices = 7;
+ float radius = 0.45f * Math.Min(info.Width, info.Height);
+
+ SKPoint[] vertices = new SKPoint[numVertices];
+ SKPoint[] midPoints = new SKPoint[numVertices];
+
+ double vertexAngle = -0.5f * Math.PI; // straight up
+
+ // Coordinates of the vertices of the polygon
+ for (int vertex = 0; vertex < numVertices; vertex++)
+ {
+ vertices[vertex] = new SKPoint(radius * (float)Math.Cos(vertexAngle),
+ radius * (float)Math.Sin(vertexAngle));
+ vertexAngle += 2 * Math.PI / numVertices;
+ }
+
+ // Coordinates of the midpoints of the sides connecting the vertices
+ for (int vertex = 0; vertex < numVertices; vertex++)
+ {
+ int prevVertex = (vertex + numVertices - 1) % numVertices;
+ midPoints[vertex] = new SKPoint((vertices[prevVertex].X + vertices[vertex].X) / 2,
+ (vertices[prevVertex].Y + vertices[vertex].Y) / 2);
+ }
+
+ // Create the path
+ using (SKPath path = new SKPath())
+ {
+ // Begin at the first midpoint
+ path.MoveTo(midPoints[0]);
+
+ for (int vertex = 0; vertex < numVertices; vertex++)
+ {
+ SKPoint nextMidPoint = midPoints[(vertex + 1) % numVertices];
+
+ // Draws a line from the current point, and then the arc
+ path.ArcTo(vertices[vertex], nextMidPoint, cornerRadius);
+
+ // Connect the arc with the next midpoint
+ path.LineTo(nextMidPoint);
+ }
+ path.Close();
+
+ // Render the path in the center of the screen
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Blue;
+ paint.StrokeWidth = 10;
+
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.DrawPath(path, paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/SquaringTheCirclePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/SquaringTheCirclePage.cs
new file mode 100644
index 000000000..f8a3257d1
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/SquaringTheCirclePage.cs
@@ -0,0 +1,109 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class SquaringTheCirclePage : ContentPage
+ {
+ SKCanvasView canvasView;
+ bool pageIsActive;
+
+ SKPoint[,] points =
+ {
+ { new SKPoint( 0, 100), new SKPoint( 0, 125), new SKPoint() },
+ { new SKPoint( 55, 100), new SKPoint( 62.5f, 62.5f), new SKPoint() },
+ { new SKPoint( 100, 55), new SKPoint( 62.5f, 62.5f), new SKPoint() },
+ { new SKPoint( 100, 0), new SKPoint( 125, 0), new SKPoint() },
+ { new SKPoint( 100, -55), new SKPoint( 62.5f, -62.5f), new SKPoint() },
+ { new SKPoint( 55, -100), new SKPoint( 62.5f, -62.5f), new SKPoint() },
+ { new SKPoint( 0, -100), new SKPoint( 0, -125), new SKPoint() },
+ { new SKPoint( -55, -100), new SKPoint(-62.5f, -62.5f), new SKPoint() },
+ { new SKPoint(-100, -55), new SKPoint(-62.5f, -62.5f), new SKPoint() },
+ { new SKPoint(-100, 0), new SKPoint( -125, 0), new SKPoint() },
+ { new SKPoint(-100, 55), new SKPoint(-62.5f, 62.5f), new SKPoint() },
+ { new SKPoint( -55, 100), new SKPoint(-62.5f, 62.5f), new SKPoint() },
+ { new SKPoint( 0, 100), new SKPoint( 0, 125), new SKPoint() }
+ };
+
+ SKPaint blueStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Blue,
+ StrokeWidth = 10
+ };
+
+ SKPaint cyanFill = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Cyan
+ };
+
+ public SquaringTheCirclePage()
+ {
+ Title = "Squaring the Circle";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ pageIsActive = true;
+
+ Dispatcher.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
+ {
+ canvasView.InvalidateSurface();
+ return pageIsActive;
+ });
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ pageIsActive = false;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.Scale(Math.Min(info.Width / 300, info.Height / 300));
+
+ // Interpolate
+ TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
+ float t = (float)(timeSpan.TotalSeconds % 3 / 3); // 0 to 1 every 3 seconds
+ t = (1 + (float)Math.Sin(2 * Math.PI * t)) / 2; // 0 to 1 to 0 sinusoidally
+
+ for (int i = 0; i < 13; i++)
+ {
+ points[i, 2] = new SKPoint(
+ (1 - t) * points[i, 0].X + t * points[i, 1].X,
+ (1 - t) * points[i, 0].Y + t * points[i, 1].Y);
+ }
+
+ // Create the path and draw it
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(points[0, 2]);
+
+ for (int i = 1; i < 13; i += 3)
+ {
+ path.CubicTo(points[i, 2], points[i + 1, 2], points[i + 2, 2]);
+ }
+ path.Close();
+
+ canvas.DrawPath(path, cyanFill);
+ canvas.DrawPath(path, blueStroke);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TangentArcPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TangentArcPage.xaml
new file mode 100644
index 000000000..afbdb2b3a
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TangentArcPage.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TangentArcPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TangentArcPage.xaml.cs
new file mode 100644
index 000000000..9b9e523bf
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TangentArcPage.xaml.cs
@@ -0,0 +1,93 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class TangentArcPage : InteractivePage
+{
+ public TangentArcPage()
+ {
+ touchPoints = new TouchPoint[3];
+
+ for (int i = 0; i < 3; i++)
+ {
+ TouchPoint touchPoint = new TouchPoint
+ {
+ Center = new SKPoint(i == 0 ? 100 : 300,
+ i != 2 ? 100 : 300)
+ };
+ touchPoints[i] = touchPoint;
+ }
+
+ InitializeComponent();
+
+ baseCanvasView = canvasView;
+ radiusSlider.Value = 100;
+ }
+
+ void sliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Draw the two lines that meet at an angle
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(touchPoints[0].Center);
+ path.LineTo(touchPoints[1].Center);
+ path.LineTo(touchPoints[2].Center);
+ canvas.DrawPath(path, dottedStrokePaint);
+ }
+
+ // Draw the circle that the arc wraps around
+ float radius = (float)radiusSlider.Value;
+
+ SKPoint v1 = Normalize(touchPoints[0].Center - touchPoints[1].Center);
+ SKPoint v2 = Normalize(touchPoints[2].Center - touchPoints[1].Center);
+
+ double dotProduct = v1.X * v2.X + v1.Y * v2.Y;
+ double angleBetween = Math.Acos(dotProduct);
+ float hypotenuse = radius / (float)Math.Sin(angleBetween / 2);
+ SKPoint vMid = Normalize(new SKPoint((v1.X + v2.X) / 2, (v1.Y + v2.Y) / 2));
+ SKPoint center = new SKPoint(touchPoints[1].Center.X + vMid.X * hypotenuse,
+ touchPoints[1].Center.Y + vMid.Y * hypotenuse);
+
+ canvas.DrawCircle(center.X, center.Y, radius, this.strokePaint);
+
+ // Draw the tangent arc
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(touchPoints[0].Center);
+ path.ArcTo(touchPoints[1].Center, touchPoints[2].Center, radius);
+ canvas.DrawPath(path, redStrokePaint);
+ }
+
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ touchPoint.Paint(canvas);
+ }
+ }
+
+ // Vector methods
+ SKPoint Normalize(SKPoint v)
+ {
+ float magnitude = Magnitude(v);
+ return new SKPoint(v.X / magnitude, v.Y / magnitude);
+ }
+
+ float Magnitude(SKPoint v)
+ {
+ return (float)Math.Sqrt(v.X * v.X + v.Y * v.Y);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TapToOutlineThePathPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TapToOutlineThePathPage.xaml
new file mode 100644
index 000000000..474637921
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TapToOutlineThePathPage.xaml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TapToOutlineThePathPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TapToOutlineThePathPage.xaml.cs
new file mode 100644
index 000000000..00c8f002c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TapToOutlineThePathPage.xaml.cs
@@ -0,0 +1,78 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves;
+
+public partial class TapToOutlineThePathPage : ContentPage
+{
+ bool outlineThePath = false;
+
+ SKPaint redThickStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Red,
+ StrokeWidth = 100
+ };
+
+ SKPaint redThinStroke = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Red,
+ StrokeWidth = 20
+ };
+
+ SKPaint blueFill = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Blue
+ };
+
+ public TapToOutlineThePathPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnTouch(object sender, SKTouchEventArgs e)
+ {
+ if (e.ActionType == SKTouchAction.Pressed)
+ {
+ outlineThePath ^= true;
+ (sender as SKCanvasView).InvalidateSurface();
+ }
+
+ e.Handled = true;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPath circlePath = new SKPath())
+ {
+ circlePath.AddCircle(info.Width / 2, info.Height / 2,
+ Math.Min(info.Width / 2, info.Height / 2) -
+ redThickStroke.StrokeWidth);
+
+ if (!outlineThePath)
+ {
+ canvas.DrawPath(circlePath, blueFill);
+ canvas.DrawPath(circlePath, redThickStroke);
+ }
+ else
+ {
+ using (SKPath outlinePath = new SKPath())
+ {
+ redThickStroke.GetFillPath(circlePath, outlinePath);
+
+ canvas.DrawPath(outlinePath, blueFill);
+ canvas.DrawPath(outlinePath, redThinStroke);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TextPathEffectPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TextPathEffectPage.cs
new file mode 100644
index 000000000..63b2f86c8
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/TextPathEffectPage.cs
@@ -0,0 +1,71 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class TextPathEffectPage : ContentPage
+ {
+ const string character = "@";
+ const float littleSize = 50;
+
+ SKPathEffect pathEffect;
+
+ SKPaint textPathPaint = new SKPaint
+ {
+ TextSize = littleSize
+ };
+
+ SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black
+ };
+
+ public TextPathEffectPage()
+ {
+ Title = "Text Path Effect";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ // Get the bounds of textPathPaint
+ SKRect textPathPaintBounds = new SKRect();
+ textPathPaint.MeasureText(character, ref textPathPaintBounds);
+
+ // Create textPath centered around (0, 0)
+ SKPath textPath = textPathPaint.GetTextPath(character,
+ -textPathPaintBounds.MidX,
+ -textPathPaintBounds.MidY);
+ // Create the path effect
+ pathEffect = SKPathEffect.Create1DPath(textPath, littleSize, 0,
+ SKPath1DPathEffectStyle.Translate);
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Set textPaint TextSize based on screen size
+ textPaint.TextSize = Math.Min(info.Width, info.Height);
+
+ // Do not measure the text with PathEffect set!
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText(character, ref textBounds);
+
+ // Coordinates to center text on screen
+ float xText = info.Width / 2 - textBounds.MidX;
+ float yText = info.Height / 2 - textBounds.MidY;
+
+ // Set the PathEffect property and display text
+ textPaint.PathEffect = pathEffect;
+ canvas.DrawText(character, xText, yText, textPaint);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/UnicycleHalfPipePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/UnicycleHalfPipePage.cs
new file mode 100644
index 000000000..687e106fe
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Curves/UnicycleHalfPipePage.cs
@@ -0,0 +1,94 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Curves
+{
+ public class UnicycleHalfPipePage : ContentPage
+ {
+ SKCanvasView canvasView;
+ bool pageIsActive;
+
+ SKPaint strokePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = 3,
+ Color = SKColors.Black
+ };
+
+ SKPath unicyclePath = SKPath.ParseSvgPathData(
+ "M 0 0" +
+ "A 25 25 0 0 0 0 -50" +
+ "A 25 25 0 0 0 0 0 Z" +
+ "M 0 -25 L 0 -100" +
+ "A 15 15 0 0 0 0 -130" +
+ "A 15 15 0 0 0 0 -100 Z" +
+ "M -25 -85 L 25 -85");
+
+ public UnicycleHalfPipePage()
+ {
+ Title = "Unicycle Half-Pipe";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ pageIsActive = true;
+
+ Dispatcher.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
+ {
+ canvasView.InvalidateSurface();
+ return pageIsActive;
+ });
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ pageIsActive = false;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPath pipePath = new SKPath())
+ {
+ pipePath.MoveTo(50, 50);
+ pipePath.CubicTo(0, 1.25f * info.Height,
+ info.Width - 0, 1.25f * info.Height,
+ info.Width - 50, 50);
+
+ canvas.DrawPath(pipePath, strokePaint);
+
+ using (SKPathMeasure pathMeasure = new SKPathMeasure(pipePath))
+ {
+ float length = pathMeasure.Length;
+
+ // Animate t from 0 to 1 every three seconds
+ TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
+ float t = (float)(timeSpan.TotalSeconds % 5 / 5);
+
+ // t from 0 to 1 to 0 but slower at beginning and end
+ t = (float)((1 - Math.Cos(t * 2 * Math.PI)) / 2);
+
+ SKMatrix matrix;
+ pathMeasure.GetMatrix(t * length, out matrix,
+ SKPathMeasureMatrixFlags.GetPositionAndTangent);
+
+ canvas.SetMatrix(matrix);
+ canvas.DrawPath(unicyclePath, strokePaint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/AlgorithmicBrickWallPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/AlgorithmicBrickWallPage.cs
new file mode 100644
index 000000000..e25a7f775
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/AlgorithmicBrickWallPage.cs
@@ -0,0 +1,82 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class AlgorithmicBrickWallPage : ContentPage
+ {
+ static AlgorithmicBrickWallPage()
+ {
+ const int brickWidth = 64;
+ const int brickHeight = 24;
+ const int morterThickness = 6;
+ const int bitmapWidth = brickWidth + morterThickness;
+ const int bitmapHeight = 2 * (brickHeight + morterThickness);
+
+ SKBitmap bitmap = new SKBitmap(bitmapWidth, bitmapHeight);
+
+ using (SKCanvas canvas = new SKCanvas(bitmap))
+ using (SKPaint brickPaint = new SKPaint())
+ {
+ brickPaint.Color = new SKColor(0xB2, 0x22, 0x22);
+
+ canvas.Clear(new SKColor(0xF0, 0xEA, 0xD6));
+ canvas.DrawRect(new SKRect(morterThickness / 2,
+ morterThickness / 2,
+ morterThickness / 2 + brickWidth,
+ morterThickness / 2 + brickHeight),
+ brickPaint);
+
+ int ySecondBrick = 3 * morterThickness / 2 + brickHeight;
+
+ canvas.DrawRect(new SKRect(0,
+ ySecondBrick,
+ bitmapWidth / 2 - morterThickness / 2,
+ ySecondBrick + brickHeight),
+ brickPaint);
+
+ canvas.DrawRect(new SKRect(bitmapWidth / 2 + morterThickness / 2,
+ ySecondBrick,
+ bitmapWidth,
+ ySecondBrick + brickHeight),
+ brickPaint);
+ }
+
+ // Save as public property for other programs
+ BrickWallTile = bitmap;
+ }
+
+ public static SKBitmap BrickWallTile { private set; get; }
+
+ public AlgorithmicBrickWallPage()
+ {
+ Title = "Algorithmic Brick Wall";
+
+ // Create SKCanvasView
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Create bitmap tiling
+ paint.Shader = SKShader.CreateBitmap(BrickWallTile,
+ SKShaderTileMode.Repeat,
+ SKShaderTileMode.Repeat);
+ // Draw background
+ canvas.DrawRect(info.Rect, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/AnimatedBitmapTilePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/AnimatedBitmapTilePage.cs
new file mode 100644
index 000000000..35607fdf5
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/AnimatedBitmapTilePage.cs
@@ -0,0 +1,95 @@
+using System.Diagnostics;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class AnimatedBitmapTilePage : ContentPage
+ {
+ const int SIZE = 64;
+
+ SKCanvasView canvasView;
+ SKBitmap bitmap = new SKBitmap(SIZE, SIZE);
+ float angle;
+
+ // For animation
+ bool isAnimating;
+ Stopwatch stopwatch = new Stopwatch();
+
+ public AnimatedBitmapTilePage()
+ {
+ Title = "Animated Bitmap Tile";
+
+ // Initialize bitmap prior to animation
+ DrawBitmap();
+
+ // Create SKCanvasView
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+
+ isAnimating = true;
+ stopwatch.Start();
+ Dispatcher.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+
+ stopwatch.Stop();
+ isAnimating = false;
+ }
+
+ bool OnTimerTick()
+ {
+ const int duration = 10; // seconds
+ angle = (float)(360f * (stopwatch.Elapsed.TotalSeconds % duration) / duration);
+ DrawBitmap();
+ canvasView.InvalidateSurface();
+
+ return isAnimating;
+ }
+
+ void DrawBitmap()
+ {
+ using (SKCanvas canvas = new SKCanvas(bitmap))
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Blue;
+ paint.StrokeWidth = SIZE / 8;
+
+ canvas.Clear();
+ canvas.Translate(SIZE / 2, SIZE / 2);
+ canvas.RotateDegrees(angle);
+ canvas.DrawLine(-SIZE, -SIZE, SIZE, SIZE, paint);
+ canvas.DrawLine(-SIZE, SIZE, SIZE, -SIZE, paint);
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateBitmap(bitmap,
+ SKShaderTileMode.Mirror,
+ SKShaderTileMode.Mirror);
+ canvas.DrawRect(info.Rect, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BitmapTileFlipModesPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BitmapTileFlipModesPage.xaml
new file mode 100644
index 000000000..20467763a
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BitmapTileFlipModesPage.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BitmapTileFlipModesPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BitmapTileFlipModesPage.xaml.cs
new file mode 100644
index 000000000..49bd2b244
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BitmapTileFlipModesPage.xaml.cs
@@ -0,0 +1,56 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class BitmapTileFlipModesPage : ContentPage
+{
+ SKBitmap bitmap;
+
+ public BitmapTileFlipModesPage()
+ {
+ InitializeComponent();
+
+ SKBitmap origBitmap = BitmapExtensions.LoadBitmapResource(
+ GetType(), "SkiaSharpDemos.Media.seatedmonkey.jpg");
+
+ // Define cropping rect
+ SKRectI cropRect = new SKRectI(5, 27, 296, 260);
+
+ // Get the cropped bitmap
+ SKBitmap croppedBitmap = new SKBitmap(cropRect.Width, cropRect.Height);
+ origBitmap.ExtractSubset(croppedBitmap, cropRect);
+
+ // Resize to half the width and height
+ SKImageInfo info = new SKImageInfo(cropRect.Width / 2, cropRect.Height / 2);
+ bitmap = croppedBitmap.Resize(info, SKFilterQuality.Low);
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Get tile modes from Pickers
+ SKShaderTileMode xTileMode =
+ (SKShaderTileMode)(xModePicker.SelectedIndex == -1 ?
+ 0 : xModePicker.SelectedItem);
+ SKShaderTileMode yTileMode =
+ (SKShaderTileMode)(yModePicker.SelectedIndex == -1 ?
+ 0 : yModePicker.SelectedItem);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateBitmap(bitmap, xTileMode, yTileMode);
+ canvas.DrawRect(info.Rect, paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BlueBananaPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BlueBananaPage.cs
new file mode 100644
index 000000000..7e1680828
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BlueBananaPage.cs
@@ -0,0 +1,66 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class BlueBananaPage : ContentPage
+ {
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(BlueBananaPage),
+ "SkiaSharpDemos.Media.banana.jpg");
+
+ SKBitmap blueBananaBitmap;
+
+ public BlueBananaPage()
+ {
+ Title = "Blue Banana";
+
+ // Load banana matte bitmap (black on transparent)
+ SKBitmap matteBitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(BlueBananaPage),
+ "SkiaSharpDemos.Media.bananamatte.png");
+
+ // Create a bitmap with a solid blue banana and transparent otherwise
+ blueBananaBitmap = new SKBitmap(matteBitmap.Width, matteBitmap.Height);
+
+ using (SKCanvas canvas = new SKCanvas(blueBananaBitmap))
+ {
+ canvas.Clear();
+ canvas.DrawBitmap(matteBitmap, new SKPoint(0, 0));
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Color = SKColors.Blue;
+ paint.BlendMode = SKBlendMode.SrcIn;
+ canvas.DrawPaint(paint);
+ }
+ }
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.BlendMode = SKBlendMode.Color;
+ canvas.DrawBitmap(blueBananaBitmap,
+ info.Rect,
+ BitmapStretch.Uniform,
+ paint: paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BlurryReflectionPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BlurryReflectionPage.cs
new file mode 100644
index 000000000..8c619b1bf
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BlurryReflectionPage.cs
@@ -0,0 +1,74 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class BlurryReflectionPage : ContentPage
+ {
+ const string TEXT = "Reflection";
+
+ public BlurryReflectionPage()
+ {
+ Title = "Blurry Reflection";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Set text color to blue
+ paint.Color = SKColors.Blue;
+
+ // Set text size to fill 90% of width
+ paint.TextSize = 100;
+ float width = paint.MeasureText(TEXT);
+ float scale = 0.9f * info.Width / width;
+ paint.TextSize *= scale;
+
+ // Get text bounds
+ SKRect textBounds = new SKRect();
+ paint.MeasureText(TEXT, ref textBounds);
+
+ // Calculate offsets to position text above center
+ float xText = info.Width / 2 - textBounds.MidX;
+ float yText = info.Height / 2;
+
+ // Draw unreflected text
+ canvas.DrawText(TEXT, xText, yText, paint);
+
+ // Shift textBounds to match displayed text
+ textBounds.Offset(xText, yText);
+
+ // Use those offsets to create a gradient for the reflected text
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(0, textBounds.Top),
+ new SKPoint(0, textBounds.Bottom),
+ new SKColor[] { paint.Color.WithAlpha(0),
+ paint.Color.WithAlpha(0x80) },
+ null,
+ SKShaderTileMode.Clamp);
+
+ // Create a blur mask filter
+ paint.MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, paint.TextSize / 36);
+
+ // Scale the canvas to flip upside-down around the vertical center
+ canvas.Scale(1, -1, 0, yText);
+
+ // Draw reflected text
+ canvas.DrawText(TEXT, xText, yText, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BrickWallCompositingPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BrickWallCompositingPage.xaml
new file mode 100644
index 000000000..9ab9db7c2
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BrickWallCompositingPage.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BrickWallCompositingPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BrickWallCompositingPage.xaml.cs
new file mode 100644
index 000000000..905a0edcf
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/BrickWallCompositingPage.xaml.cs
@@ -0,0 +1,102 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class BrickWallCompositingPage : ContentPage
+{
+ SKBitmap monkeyBitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(BrickWallCompositingPage),
+ "SkiaSharpDemos.Media.seatedmonkey.jpg");
+
+ SKBitmap matteBitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(BrickWallCompositingPage),
+ "SkiaSharpDemos.Media.seatedmonkeymatte.png");
+
+ int step = 0;
+
+ public BrickWallCompositingPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnButtonClicked(object sender, EventArgs args)
+ {
+ Button btn = (Button)sender;
+ step = (step + 1) % 5;
+
+ switch (step)
+ {
+ case 0: btn.Text = "Show sitting monkey"; break;
+ case 1: btn.Text = "Draw matte with DstIn"; break;
+ case 2: btn.Text = "Draw sidewalk with DstOver"; break;
+ case 3: btn.Text = "Draw brick wall with DstOver"; break;
+ case 4: btn.Text = "Reset"; break;
+ }
+
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float x = (info.Width - monkeyBitmap.Width) / 2;
+ float y = info.Height - monkeyBitmap.Height;
+
+ // Draw monkey bitmap
+ if (step >= 1)
+ {
+ canvas.DrawBitmap(monkeyBitmap, x, y);
+ }
+
+ // Draw matte to exclude monkey's surroundings
+ if (step >= 2)
+ {
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.BlendMode = SKBlendMode.DstIn;
+ canvas.DrawBitmap(matteBitmap, x, y, paint);
+ }
+ }
+
+ const float sidewalkHeight = 80;
+ SKRect rect = new SKRect(info.Rect.Left, info.Rect.Bottom - sidewalkHeight,
+ info.Rect.Right, info.Rect.Bottom);
+
+ // Draw gravel sidewalk for monkey to sit on
+ if (step >= 3)
+ {
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateCompose(
+ SKShader.CreateColor(SKColors.SandyBrown),
+ SKShader.CreatePerlinNoiseTurbulence(0.1f, 0.3f, 1, 9));
+
+ paint.BlendMode = SKBlendMode.DstOver;
+ canvas.DrawRect(rect, paint);
+ }
+ }
+
+ // Draw bitmap tiled brick wall behind monkey
+ if (step >= 4)
+ {
+ using (SKPaint paint = new SKPaint())
+ {
+ SKBitmap bitmap = AlgorithmicBrickWallPage.BrickWallTile;
+ float yAdjust = (info.Height - sidewalkHeight) % bitmap.Height;
+
+ paint.Shader = SKShader.CreateBitmap(bitmap,
+ SKShaderTileMode.Repeat,
+ SKShaderTileMode.Repeat,
+ SKMatrix.CreateTranslation(0, yAdjust));
+ paint.BlendMode = SKBlendMode.DstOver;
+ canvas.DrawRect(info.Rect, paint);
+ }
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/CenteredTilesPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/CenteredTilesPage.cs
new file mode 100644
index 000000000..6b699b44f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/CenteredTilesPage.cs
@@ -0,0 +1,51 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class CenteredTilesPage : ContentPage
+ {
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(CenteredTilesPage),
+ "SkiaSharpDemos.Media.monkey.png");
+
+ public CenteredTilesPage()
+ {
+ Title = "Centered Tiles";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Find coordinates to center bitmap in canvas...
+ float x = (info.Width - bitmap.Width) / 2f;
+ float y = (info.Height - bitmap.Height) / 2f;
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // ... but use them to create a translate transform
+ SKMatrix matrix = SKMatrix.CreateTranslation(x, y);
+ paint.Shader = SKShader.CreateBitmap(bitmap,
+ SKShaderTileMode.Repeat,
+ SKShaderTileMode.Repeat,
+ matrix);
+
+ // Use that tiled bitmap pattern to fill a circle
+ canvas.DrawCircle(info.Rect.MidX, info.Rect.MidY,
+ Math.Min(info.Width, info.Height) / 2,
+ paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ChainLinkFencePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ChainLinkFencePage.cs
new file mode 100644
index 000000000..235e0b76e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ChainLinkFencePage.cs
@@ -0,0 +1,113 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class ChainLinkFencePage : ContentPage
+ {
+ SKBitmap monkeyBitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(ChainLinkFencePage), "SkiaSharpDemos.Media.seatedmonkey.jpg");
+
+ SKBitmap tileBitmap;
+
+ public ChainLinkFencePage()
+ {
+ Title = "Chain-Link Fence";
+
+ // Create bitmap for chain-link tiling
+ int tileSize = DeviceInfo.Current.Idiom == DeviceIdiom.Desktop ? 64 : 128;
+ tileBitmap = CreateChainLinkTile(tileSize);
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ SKBitmap CreateChainLinkTile(int tileSize)
+ {
+ tileBitmap = new SKBitmap(tileSize, tileSize);
+ float wireThickness = tileSize / 12f;
+
+ using (SKCanvas canvas = new SKCanvas(tileBitmap))
+ using (SKPaint paint = new SKPaint())
+ {
+ canvas.Clear();
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = wireThickness;
+ paint.IsAntialias = true;
+
+ // Draw straight wires first
+ paint.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0),
+ new SKPoint(0, tileSize),
+ new SKColor[] { SKColors.Silver, SKColors.Black },
+ new float[] { 0.4f, 0.6f },
+ SKShaderTileMode.Clamp);
+
+ canvas.DrawLine(0, tileSize / 2,
+ tileSize / 2, tileSize / 2 - wireThickness / 2, paint);
+
+ canvas.DrawLine(tileSize, tileSize / 2,
+ tileSize / 2, tileSize / 2 + wireThickness / 2, paint);
+
+ // Draw curved wires
+ using (SKPath path = new SKPath())
+ {
+ path.MoveTo(tileSize / 2, 0);
+ path.LineTo(tileSize / 2 - wireThickness / 2, tileSize / 2);
+ path.ArcTo(wireThickness / 2, wireThickness / 2,
+ 0,
+ SKPathArcSize.Small,
+ SKPathDirection.CounterClockwise,
+ tileSize / 2, tileSize / 2 + wireThickness / 2);
+
+ paint.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0),
+ new SKPoint(0, tileSize),
+ new SKColor[] { SKColors.Silver, SKColors.Black },
+ null,
+ SKShaderTileMode.Clamp);
+ canvas.DrawPath(path, paint);
+
+ path.Reset();
+ path.MoveTo(tileSize / 2, tileSize);
+ path.LineTo(tileSize / 2 + wireThickness / 2, tileSize / 2);
+ path.ArcTo(wireThickness / 2, wireThickness / 2,
+ 0,
+ SKPathArcSize.Small,
+ SKPathDirection.CounterClockwise,
+ tileSize / 2, tileSize / 2 - wireThickness / 2);
+
+ paint.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0),
+ new SKPoint(0, tileSize),
+ new SKColor[] { SKColors.White, SKColors.Silver },
+ null,
+ SKShaderTileMode.Clamp);
+ canvas.DrawPath(path, paint);
+ }
+ return tileBitmap;
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ canvas.DrawBitmap(monkeyBitmap, info.Rect, BitmapStretch.UniformToFill,
+ BitmapAlignment.Center, BitmapAlignment.Start);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateBitmap(tileBitmap,
+ SKShaderTileMode.Repeat,
+ SKShaderTileMode.Repeat,
+ SKMatrix.CreateRotationDegrees(45));
+ canvas.DrawRect(info.Rect, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ChainLinkTile.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ChainLinkTile.cs
new file mode 100644
index 000000000..5bc8c2662
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ChainLinkTile.cs
@@ -0,0 +1,174 @@
+using SkiaSharp;
+
+namespace SkiaSharpDemos.Effects
+{
+ // This class is not used.
+ // It was replaced by much shorter code in ChainLinkFencePage.cs, but this file
+ // demonstrates the more complex logic when the result is not rotated.
+ class ChainLinkTile
+ {
+ readonly int tileSize;
+ readonly float wireThickness;
+ readonly float cornerOffset;
+ readonly SKColor[] shadeGradientColors = new SKColor[] { SKColors.Silver, SKColors.Black };
+ readonly float[] shadeGradientOffsets = new float[] { 0.4f, 0.6f };
+
+ public ChainLinkTile(int tileSize)
+ {
+ this.tileSize = tileSize;
+ wireThickness = tileSize / 16;
+
+ // Where the wires cross the edge near the corner
+ cornerOffset = wireThickness / (float)Math.Sqrt(2);
+
+ SKBitmap bitmap = new SKBitmap(tileSize, tileSize);
+
+ using (SKCanvas canvas = new SKCanvas(bitmap))
+ using (SKPaint paint = new SKPaint())
+ using (SKPath path = new SKPath())
+ {
+ canvas.Clear();
+ paint.IsAntialias = true;
+
+ // Divide bitmap into upper-Left and lower-right quadrants
+ LinkCrossQuadrant(canvas, path, paint, new SKRect(0, 0, tileSize / 2, tileSize / 2));
+ LinkCrossQuadrant(canvas, path, paint, new SKRect(tileSize / 2, tileSize / 2, tileSize, tileSize));
+
+ // Convert SKPaint object for drawing lines
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = wireThickness;
+
+ // Divide bitmap into upper-right and lower-left quadrants
+ MostlyEmptyQuadrant(canvas, paint, new SKRect(tileSize / 2, 0, tileSize, tileSize / 2));
+ MostlyEmptyQuadrant(canvas, paint, new SKRect(0, tileSize / 2, tileSize / 2, tileSize));
+ }
+
+ // Set public property
+ Bitmap = bitmap;
+ }
+
+ public SKBitmap Bitmap { private set; get; }
+
+ void LinkCrossQuadrant(SKCanvas canvas, SKPath path, SKPaint paint, SKRect rect)
+ {
+ SKPoint center = new SKPoint(rect.MidX, rect.MidY);
+
+ paint.Shader = SKShader.CreateLinearGradient(new SKPoint(rect.Right, rect.Top),
+ new SKPoint(rect.Left, rect.Bottom),
+ shadeGradientColors,
+ shadeGradientOffsets, SKShaderTileMode.Clamp);
+
+ // Create and draw path
+ StraightWire(path, center, new SKPoint(rect.Left, rect.Top), // the corner point
+ new SKPoint(rect.Left, rect.Top + cornerOffset), // the point near the corner that goes to center
+ new SKPoint(rect.Left + cornerOffset, rect.Top)); // the other point near the corner
+ canvas.DrawPath(path, paint);
+
+ StraightWire(path, center, new SKPoint(rect.Right, rect.Bottom),
+ new SKPoint(rect.Right, rect.Bottom - cornerOffset),
+ new SKPoint(rect.Right - cornerOffset, rect.Bottom));
+ canvas.DrawPath(path, paint);
+
+ paint.Shader = SKShader.CreateLinearGradient(new SKPoint(rect.Right, rect.Top),
+ new SKPoint(rect.Left, rect.Bottom),
+ new SKColor[] { SKColors.Silver, SKColors.Black },
+ null, SKShaderTileMode.Clamp);
+
+ CurvedWire(path, center, new SKPoint(rect.Right, rect.Top),
+ new SKPoint(rect.Right, rect.Top + cornerOffset),
+ new SKPoint(rect.Right - cornerOffset, rect.Top));
+
+ canvas.DrawPath(path, paint);
+
+ paint.Shader = SKShader.CreateLinearGradient(new SKPoint(rect.Right, rect.Top),
+ new SKPoint(rect.Left, rect.Bottom),
+ new SKColor[] { SKColors.White, SKColors.Silver },
+ null, SKShaderTileMode.Clamp);
+
+ CurvedWire(path, center, new SKPoint(rect.Left, rect.Bottom),
+ new SKPoint(rect.Left, rect.Bottom - cornerOffset),
+ new SKPoint(rect.Left + cornerOffset, rect.Bottom));
+
+ canvas.DrawPath(path, paint);
+ }
+
+ void MostlyEmptyQuadrant(SKCanvas canvas, SKPaint paint, SKRect rect)
+ {
+ using (new SKAutoCanvasRestore(canvas))
+ {
+ canvas.ClipRect(rect, SKClipOperation.Intersect);
+
+ // Fill in the lines on the upper-left and lower-right
+ paint.Shader = null;
+ paint.Color = SKColors.Silver;
+
+ canvas.DrawLine(rect.Left - tileSize, rect.Top + tileSize,
+ rect.Left + tileSize, rect.Top - tileSize, paint);
+
+ canvas.DrawLine(rect.Right - tileSize, rect.Bottom + tileSize,
+ rect.Right + tileSize, rect.Bottom - tileSize, paint);
+
+ // Fill in the line on the lower-left
+ paint.Shader = SKShader.CreateLinearGradient(new SKPoint(rect.MidX, rect.MidY),
+ new SKPoint(rect.Left - rect.Width / 2, rect.Bottom + rect.Height / 2),
+ shadeGradientColors,
+ shadeGradientOffsets,
+ SKShaderTileMode.Clamp);
+
+ canvas.DrawLine(rect.Left - tileSize, rect.Bottom - tileSize,
+ rect.Left + tileSize, rect.Bottom + tileSize, paint);
+
+
+ // Fill in the line on the upper-right
+ paint.Shader = SKShader.CreateLinearGradient(new SKPoint(rect.Right + rect.Width / 2, rect.Top - rect.Height / 2),
+ new SKPoint(rect.MidX, rect.MidY),
+ shadeGradientColors,
+ shadeGradientOffsets,
+ SKShaderTileMode.Clamp);
+
+ canvas.DrawLine(rect.Right - tileSize, rect.Top - tileSize,
+ rect.Right + tileSize, rect.Top + tileSize, paint);
+ }
+ }
+
+ void StraightWire(SKPath path, SKPoint center, SKPoint corner, SKPoint pt1, SKPoint pt2)
+ {
+ path.Reset();
+
+ SKPoint vector = center - pt1;
+ path.MoveTo(pt1);
+ path.LineTo(pt1 + vector);
+ path.LineTo(pt2 + vector);
+ path.LineTo(pt2);
+ path.LineTo(corner);
+ path.Close();
+ }
+
+ void CurvedWire(SKPath path, SKPoint center, SKPoint corner, SKPoint pt1, SKPoint pt2)
+ {
+ path.Reset();
+
+ SKPoint vector = center - pt1;
+
+ // Vector that goes beyond the center to the end of the curve
+ SKPoint vector2 = vector;
+ float length = (float)Math.Sqrt(Math.Pow(vector2.X, 2) + Math.Pow(vector2.Y, 2));
+ vector2.X /= length;
+ vector2.Y /= length;
+ vector2.X *= (length + wireThickness);
+ vector2.Y *= (length + wireThickness);
+
+ path.MoveTo(pt2);
+ path.LineTo(pt2 + vector);
+ path.ArcTo(new SKPoint(wireThickness, wireThickness),
+ 0,
+ SKPathArcSize.Small,
+ SKPathDirection.CounterClockwise,
+ pt1 + vector2);
+ path.LineTo(pt1);
+ path.LineTo(corner);
+ path.Close();
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ComposedPerlinNoisePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ComposedPerlinNoisePage.xaml
new file mode 100644
index 000000000..dfaf076e0
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ComposedPerlinNoisePage.xaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ComposedPerlinNoisePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ComposedPerlinNoisePage.xaml.cs
new file mode 100644
index 000000000..20da6f5ba
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ComposedPerlinNoisePage.xaml.cs
@@ -0,0 +1,63 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class ComposedPerlinNoisePage : ContentPage
+{
+ public ComposedPerlinNoisePage()
+ {
+ InitializeComponent();
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnStepperValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Get values from sliders and stepper
+ float baseFreqX = (float)Math.Pow(10, baseFrequencyXSlider.Value - 4);
+ baseFrequencyXText.Text = String.Format("Base Frequency X = {0:F4}", baseFreqX);
+
+ float baseFreqY = (float)Math.Pow(10, baseFrequencyYSlider.Value - 4);
+ baseFrequencyYText.Text = String.Format("Base Frequency Y = {0:F4}", baseFreqY);
+
+ int numOctaves = (int)octavesStepper.Value;
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateCompose(
+ SKShader.CreateColor(SKColors.Blue),
+ SKShader.CreatePerlinNoiseFractalNoise(baseFreqX,
+ baseFreqY,
+ numOctaves,
+ 0));
+
+ SKRect rect = new SKRect(0, 0, info.Width, info.Height / 2);
+ canvas.DrawRect(rect, paint);
+
+ paint.Shader = SKShader.CreateCompose(
+ SKShader.CreateColor(SKColors.Blue),
+ SKShader.CreatePerlinNoiseTurbulence(baseFreqX,
+ baseFreqY,
+ numOctaves,
+ 0));
+
+ rect = new SKRect(0, info.Height / 2, info.Width, info.Height);
+ canvas.DrawRect(rect, paint);
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/CompositingMaskPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/CompositingMaskPage.cs
new file mode 100644
index 000000000..8f24aff00
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/CompositingMaskPage.cs
@@ -0,0 +1,71 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class CompositingMaskPage : ContentPage
+ {
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(CompositingMaskPage),
+ "SkiaSharpDemos.Media.mountainclimbers.jpg");
+
+ static readonly SKPoint CENTER = new SKPoint(180, 300);
+ static readonly float RADIUS = 120;
+
+ public CompositingMaskPage()
+ {
+ Title = "Compositing Mask";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Find rectangle to display bitmap
+ float scale = Math.Min((float)info.Width / bitmap.Width,
+ (float)info.Height / bitmap.Height);
+
+ SKRect rect = SKRect.Create(scale * bitmap.Width, scale * bitmap.Height);
+
+ float x = (info.Width - rect.Width) / 2;
+ float y = (info.Height - rect.Height) / 2;
+ rect.Offset(x, y);
+
+ // Display bitmap in rectangle
+ canvas.DrawBitmap(bitmap, rect);
+
+ // Adjust center and radius for scaled and offset bitmap
+ SKPoint center = new SKPoint(scale * CENTER.X + x,
+ scale * CENTER.Y + y);
+ float radius = scale * RADIUS;
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateRadialGradient(
+ center,
+ radius,
+ new SKColor[] { SKColors.Black,
+ SKColors.Transparent },
+ new float[] { 0.6f, 1 },
+ SKShaderTileMode.Clamp);
+
+ paint.BlendMode = SKBlendMode.DstIn;
+
+ // Display rectangle using that gradient and blend mode
+ canvas.DrawRect(rect, paint);
+ }
+
+ canvas.DrawColor(SKColors.Pink, SKBlendMode.DstOver);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ConicalGradientPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ConicalGradientPage.xaml
new file mode 100644
index 000000000..2acc8ff1c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ConicalGradientPage.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ConicalGradientPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ConicalGradientPage.xaml.cs
new file mode 100644
index 000000000..c7fc058cd
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ConicalGradientPage.xaml.cs
@@ -0,0 +1,74 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class ConicalGradientPage : InteractivePage
+{
+ const int RADIUS1 = 50;
+ const int RADIUS2 = 100;
+
+ public ConicalGradientPage()
+ {
+ touchPoints = new TouchPoint[2];
+
+ touchPoints[0] = new TouchPoint
+ {
+ Center = new SKPoint(100, 100),
+ Radius = RADIUS1
+ };
+
+ touchPoints[1] = new TouchPoint
+ {
+ Center = new SKPoint(300, 300),
+ Radius = RADIUS2
+ };
+
+ InitializeComponent();
+ baseCanvasView = canvasView;
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKColor[] colors = { SKColors.Red, SKColors.Green, SKColors.Blue };
+ SKShaderTileMode tileMode =
+ (SKShaderTileMode)(tileModePicker.SelectedIndex == -1 ?
+ 0 : tileModePicker.SelectedItem);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateTwoPointConicalGradient(touchPoints[0].Center,
+ RADIUS1,
+ touchPoints[1].Center,
+ RADIUS2,
+ colors,
+ null,
+ tileMode);
+ canvas.DrawRect(info.Rect, paint);
+ }
+
+ // Display the touch points here rather than by TouchPoint
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Black;
+ paint.StrokeWidth = 3;
+
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ canvas.DrawCircle(touchPoint.Center, touchPoint.Radius, paint);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ConicalSpecularHighlightPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ConicalSpecularHighlightPage.cs
new file mode 100644
index 000000000..1925aa2b8
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ConicalSpecularHighlightPage.cs
@@ -0,0 +1,46 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class ConicalSpecularHighlightPage : ContentPage
+ {
+ public ConicalSpecularHighlightPage()
+ {
+ Title = "Conical Specular Highlight";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float radius = 0.4f * Math.Min(info.Width, info.Height);
+ SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);
+ SKPoint offCenter = center - new SKPoint(radius / 2, radius / 2);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateTwoPointConicalGradient(
+ offCenter,
+ 1,
+ center,
+ radius,
+ new SKColor[] { SKColors.White, SKColors.Red },
+ null,
+ SKShaderTileMode.Clamp);
+
+ canvas.DrawCircle(center, radius, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/CornerToCornerGradientPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/CornerToCornerGradientPage.cs
new file mode 100644
index 000000000..c78b1d61c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/CornerToCornerGradientPage.cs
@@ -0,0 +1,69 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class CornerToCornerGradientPage : ContentPage
+ {
+ bool drawBackground;
+
+ public CornerToCornerGradientPage()
+ {
+ Title = "Corner-to-Corner Gradient";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ TapGestureRecognizer tap = new TapGestureRecognizer();
+ tap.Tapped += (sender, args) =>
+ {
+ drawBackground ^= true;
+ canvasView.InvalidateSurface();
+ };
+ canvasView.GestureRecognizers.Add(tap);
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Create 300-pixel square centered rectangle
+ float x = (info.Width - 300) / 2;
+ float y = (info.Height - 300) / 2;
+ SKRect rect = new SKRect(x, y, x + 300, y + 300);
+
+ // Create linear gradient from upper-left to lower-right
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(rect.Left, rect.Top),
+ new SKPoint(rect.Right, rect.Bottom),
+ new SKColor[] { SKColors.Red, SKColors.Blue },
+ new float[] { 0, 1 },
+ SKShaderTileMode.Repeat);
+
+ // Draw the gradient on the rectangle
+ canvas.DrawRect(rect, paint);
+
+ if (drawBackground)
+ {
+ // Draw the gradient on the whole canvas
+ canvas.DrawRect(info.Rect, paint);
+
+ // Outline the smaller rectangle
+ paint.Shader = null;
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Black;
+ canvas.DrawRect(rect, paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DistantLightExperimentPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DistantLightExperimentPage.xaml
new file mode 100644
index 000000000..7b7927688
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DistantLightExperimentPage.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DistantLightExperimentPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DistantLightExperimentPage.xaml.cs
new file mode 100644
index 000000000..932ec1eba
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DistantLightExperimentPage.xaml.cs
@@ -0,0 +1,58 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class DistantLightExperimentPage : ContentPage
+{
+ const string TEXT = "Lighting";
+
+ public DistantLightExperimentPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float z = (float)zSlider.Value;
+ float surfaceScale = (float)surfaceScaleSlider.Value;
+ float lightConstant = (float)lightConstantSlider.Value;
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.IsAntialias = true;
+
+ // Size text to 90% of canvas width
+ paint.TextSize = 100;
+ float textWidth = paint.MeasureText(TEXT);
+ paint.TextSize *= 0.9f * info.Width / textWidth;
+
+ // Find coordinates to center text
+ SKRect textBounds = new SKRect();
+ paint.MeasureText(TEXT, ref textBounds);
+
+ float xText = info.Rect.MidX - textBounds.MidX;
+ float yText = info.Rect.MidY - textBounds.MidY;
+
+ // Create distant light image filter
+ paint.ImageFilter = SKImageFilter.CreateDistantLitDiffuse(
+ new SKPoint3(2, 3, z),
+ SKColors.White,
+ surfaceScale,
+ lightConstant);
+
+ canvas.DrawText(TEXT, xText, yText, paint);
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DodgeAndBurnPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DodgeAndBurnPage.xaml
new file mode 100644
index 000000000..3d7e26020
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DodgeAndBurnPage.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DodgeAndBurnPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DodgeAndBurnPage.xaml.cs
new file mode 100644
index 000000000..e43a2fdb7
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DodgeAndBurnPage.xaml.cs
@@ -0,0 +1,68 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class DodgeAndBurnPage : ContentPage
+{
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(DodgeAndBurnPage),
+ "SkiaSharpDemos.Media.banana.jpg");
+
+ public DodgeAndBurnPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if ((Slider)sender == dodgeSlider)
+ {
+ dodgeCanvasView.InvalidateSurface();
+ }
+ else
+ {
+ burnCanvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Find largest size rectangle in canvas
+ float scale = Math.Min((float)info.Width / bitmap.Width,
+ (float)info.Height / bitmap.Height);
+ SKRect rect = SKRect.Create(scale * bitmap.Width, scale * bitmap.Height);
+ float x = (info.Width - rect.Width) / 2;
+ float y = (info.Height - rect.Height) / 2;
+ rect.Offset(x, y);
+
+ // Display bitmap
+ canvas.DrawBitmap(bitmap, rect);
+
+ // Display gray rectangle with blend mode
+ using (SKPaint paint = new SKPaint())
+ {
+ if ((SKCanvasView)sender == dodgeCanvasView)
+ {
+ byte value = (byte)(255 * dodgeSlider.Value);
+ paint.Color = new SKColor(value, value, value);
+ paint.BlendMode = SKBlendMode.ColorDodge;
+ }
+ else
+ {
+ byte value = (byte)(255 * (1 - burnSlider.Value));
+ paint.Color = new SKColor(value, value, value);
+ paint.BlendMode = SKBlendMode.ColorBurn;
+ }
+
+ canvas.DrawRect(rect, paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DropShadowExperimentPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DropShadowExperimentPage.xaml
new file mode 100644
index 000000000..9b133814f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DropShadowExperimentPage.xaml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DropShadowExperimentPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DropShadowExperimentPage.xaml.cs
new file mode 100644
index 000000000..6d395d370
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/DropShadowExperimentPage.xaml.cs
@@ -0,0 +1,56 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class DropShadowExperimentPage : ContentPage
+{
+ const string TEXT = "Drop Shadow";
+
+ public DropShadowExperimentPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Get values from sliders
+ float dx = (float)dxSlider.Value;
+ float dy = (float)dySlider.Value;
+ float sigmaX = (float)sigmaXSlider.Value;
+ float sigmaY = (float)sigmaYSlider.Value;
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Set SKPaint properties
+ paint.TextSize = info.Width / 7;
+ paint.Color = SKColors.Blue;
+ paint.ImageFilter = SKImageFilter.CreateDropShadow(
+ dx,
+ dy,
+ sigmaX,
+ sigmaY,
+ SKColors.Red);
+
+ SKRect textBounds = new SKRect();
+ paint.MeasureText(TEXT, ref textBounds);
+
+ // Center the text in the display rectangle
+ float xText = info.Width / 2 - textBounds.MidX;
+ float yText = info.Height / 2 - textBounds.MidY;
+
+ canvas.DrawText(TEXT, xText, yText, paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/EffectsMenuPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/EffectsMenuPage.xaml
new file mode 100644
index 000000000..915f88310
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/EffectsMenuPage.xaml
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/EffectsMenuPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/EffectsMenuPage.xaml.cs
new file mode 100644
index 000000000..b06f13342
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/EffectsMenuPage.xaml.cs
@@ -0,0 +1,9 @@
+namespace SkiaSharpDemos.Effects;
+
+public partial class EffectsMenuPage : BasePage
+{
+ public EffectsMenuPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientAnimationPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientAnimationPage.cs
new file mode 100644
index 000000000..6c6541c0e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientAnimationPage.cs
@@ -0,0 +1,74 @@
+using System.Diagnostics;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class GradientAnimationPage : ContentPage
+ {
+ SKCanvasView canvasView;
+ bool isAnimating;
+ double angle;
+ Stopwatch stopwatch = new Stopwatch();
+
+ public GradientAnimationPage()
+ {
+ Title = "Gradient Animation";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+
+ isAnimating = true;
+ stopwatch.Start();
+ Dispatcher.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+
+ stopwatch.Stop();
+ isAnimating = false;
+ }
+
+ bool OnTimerTick()
+ {
+ const int duration = 3000;
+ angle = 2 * Math.PI * (stopwatch.ElapsedMilliseconds % duration) / duration;
+ canvasView.InvalidateSurface();
+
+ return isAnimating;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(0, 0),
+ info.Width < info.Height ? new SKPoint(info.Width, 0) :
+ new SKPoint(0, info.Height),
+ new SKColor[] { SKColors.White, SKColors.Black },
+ null,
+ SKShaderTileMode.Mirror,
+ SKMatrix.CreateRotation((float)angle, info.Rect.MidX, info.Rect.MidY));
+
+ canvas.DrawRect(info.Rect, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientTextPage.cs
new file mode 100644
index 000000000..27e562cfb
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientTextPage.cs
@@ -0,0 +1,74 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class GradientTextPage : ContentPage
+ {
+ const string TEXT = "GRADIENT";
+
+ public GradientTextPage()
+ {
+ Title = "Gradient Text";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Create gradient for background
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(0, 0),
+ new SKPoint(info.Width, info.Height),
+ new SKColor[] { new SKColor(0x40, 0x40, 0x40),
+ new SKColor(0xC0, 0xC0, 0xC0) },
+ null,
+ SKShaderTileMode.Clamp);
+
+ // Draw background
+ canvas.DrawRect(info.Rect, paint);
+
+ // Set TextSize to fill 90% of width
+ paint.TextSize = 100;
+ float width = paint.MeasureText(TEXT);
+ float scale = 0.9f * info.Width / width;
+ paint.TextSize *= scale;
+
+ // Get text bounds
+ SKRect textBounds = new SKRect();
+ paint.MeasureText(TEXT, ref textBounds);
+
+ // Calculate offsets to center the text on the screen
+ float xText = info.Width / 2 - textBounds.MidX;
+ float yText = info.Height / 2 - textBounds.MidY;
+
+ // Shift textBounds by that amount
+ textBounds.Offset(xText, yText);
+
+ // Create gradient for text
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(textBounds.Left, textBounds.Top),
+ new SKPoint(textBounds.Right, textBounds.Bottom),
+ new SKColor[] { new SKColor(0x40, 0x40, 0x40),
+ new SKColor(0xC0, 0xC0, 0xC0) },
+ null,
+ SKShaderTileMode.Clamp);
+
+ // Draw text
+ canvas.DrawText(TEXT, xText, yText, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientTransitionsPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientTransitionsPage.xaml
new file mode 100644
index 000000000..81769675f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientTransitionsPage.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientTransitionsPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientTransitionsPage.xaml.cs
new file mode 100644
index 000000000..8eb314ce0
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GradientTransitionsPage.xaml.cs
@@ -0,0 +1,110 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class GradientTransitionsPage : ContentPage
+{
+ SKBitmap bitmap1 = BitmapExtensions.LoadBitmapResource(
+ typeof(GradientTransitionsPage),
+ "SkiaSharpDemos.Media.seatedmonkey.jpg");
+
+ SKBitmap bitmap2 = BitmapExtensions.LoadBitmapResource(
+ typeof(GradientTransitionsPage),
+ "SkiaSharpDemos.Media.facepalm.jpg");
+
+ enum TransitionMode
+ {
+ Linear,
+ Radial,
+ Sweep
+ };
+
+ public GradientTransitionsPage()
+ {
+ InitializeComponent();
+
+ foreach (TransitionMode mode in Enum.GetValues(typeof(TransitionMode)))
+ {
+ transitionPicker.Items.Add(mode.ToString());
+ }
+
+ transitionPicker.SelectedIndex = 0;
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Assume both bitmaps are square for display rectangle
+ float size = Math.Min(info.Width, info.Height);
+ SKRect rect = SKRect.Create(size, size);
+ float x = (info.Width - size) / 2;
+ float y = (info.Height - size) / 2;
+ rect.Offset(x, y);
+
+ using (SKPaint paint0 = new SKPaint())
+ using (SKPaint paint1 = new SKPaint())
+ using (SKPaint paint2 = new SKPaint())
+ {
+ SKColor[] colors = new SKColor[] { SKColors.Black,
+ SKColors.Transparent };
+
+ float progress = (float)progressSlider.Value;
+
+ float[] positions = new float[]{ 1.1f * progress - 0.1f,
+ 1.1f * progress };
+
+ switch ((TransitionMode)transitionPicker.SelectedIndex)
+ {
+ case TransitionMode.Linear:
+ paint0.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(rect.Left, 0),
+ new SKPoint(rect.Right, 0),
+ colors,
+ positions,
+ SKShaderTileMode.Clamp);
+ break;
+
+ case TransitionMode.Radial:
+ paint0.Shader = SKShader.CreateRadialGradient(
+ new SKPoint(rect.MidX, rect.MidY),
+ (float)Math.Sqrt(Math.Pow(rect.Width / 2, 2) +
+ Math.Pow(rect.Height / 2, 2)),
+ colors,
+ positions,
+ SKShaderTileMode.Clamp);
+ break;
+
+ case TransitionMode.Sweep:
+ paint0.Shader = SKShader.CreateSweepGradient(
+ new SKPoint(rect.MidX, rect.MidY),
+ colors,
+ positions);
+ break;
+ }
+
+ canvas.DrawRect(rect, paint0);
+
+ paint1.BlendMode = SKBlendMode.SrcOut;
+ canvas.DrawBitmap(bitmap1, rect, paint1);
+
+ paint2.BlendMode = SKBlendMode.DstOver;
+ canvas.DrawBitmap(bitmap2, rect, paint2);
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GrayScaleMatrixPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GrayScaleMatrixPage.cs
new file mode 100644
index 000000000..2eb6b391f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/GrayScaleMatrixPage.cs
@@ -0,0 +1,46 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class GrayScaleMatrixPage : ContentPage
+ {
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(GrayScaleMatrixPage),
+ "SkiaSharpDemos.Media.banana.jpg");
+
+ public GrayScaleMatrixPage()
+ {
+ Title = "Gray-Scale Matrix";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.ColorFilter =
+ SKColorFilter.CreateColorMatrix(new float[]
+ {
+ 0.21f, 0.72f, 0.07f, 0, 0,
+ 0.21f, 0.72f, 0.07f, 0, 0,
+ 0.21f, 0.72f, 0.07f, 0, 0,
+ 0, 0, 0, 1, 0
+ });
+
+ canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform, paint: paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ImageBlurExperimentPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ImageBlurExperimentPage.xaml
new file mode 100644
index 000000000..1f929e6bf
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ImageBlurExperimentPage.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ImageBlurExperimentPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ImageBlurExperimentPage.xaml.cs
new file mode 100644
index 000000000..0eb6f865b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ImageBlurExperimentPage.xaml.cs
@@ -0,0 +1,60 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class ImageBlurExperimentPage : ContentPage
+{
+ const string TEXT = "Blur My Text";
+
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(MaskBlurExperimentPage),
+ "SkiaSharpDemos.Media.seatedmonkey.jpg");
+
+ public ImageBlurExperimentPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear(SKColors.Pink);
+
+ // Get values from sliders
+ float sigmaX = (float)sigmaXSlider.Value;
+ float sigmaY = (float)sigmaYSlider.Value;
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Set SKPaint properties
+ paint.TextSize = (info.Width - 100) / (TEXT.Length / 2);
+ paint.ImageFilter = SKImageFilter.CreateDilate((int)(sigmaX), (int)(sigmaY)); // .CreateBlur(sigmaX, sigmaY);
+
+ // Get text bounds and calculate display rectangle
+ SKRect textBounds = new SKRect();
+ paint.MeasureText(TEXT, ref textBounds);
+ SKRect textRect = new SKRect(0, 0, info.Width, textBounds.Height + 50);
+
+ // Center the text in the display rectangle
+ float xText = textRect.Width / 2 - textBounds.MidX;
+ float yText = textRect.Height / 2 - textBounds.MidY;
+
+ canvas.DrawText(TEXT, xText, yText, paint);
+
+ // Calculate rectangle for bitmap
+ SKRect bitmapRect = new SKRect(0, textRect.Bottom, info.Width, info.Height);
+ bitmapRect.Inflate(-50, -50);
+
+ canvas.DrawBitmap(bitmap, bitmapRect, BitmapStretch.Uniform, paint: paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/InfinityColorsPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/InfinityColorsPage.cs
new file mode 100644
index 000000000..6cf06c61f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/InfinityColorsPage.cs
@@ -0,0 +1,118 @@
+using System.Diagnostics;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class InfinityColorsPage : ContentPage
+ {
+ const int STROKE_WIDTH = 50;
+
+ SKCanvasView canvasView;
+
+ // Path information
+ SKPath infinityPath;
+ SKRect pathBounds;
+ float gradientCycleLength;
+
+ // Gradient information
+ SKColor[] colors = new SKColor[8];
+
+ // For animation
+ bool isAnimating;
+ float offset;
+ Stopwatch stopwatch = new Stopwatch();
+
+ public InfinityColorsPage()
+ {
+ Title = "Infinity Colors";
+
+ // Create path for infinity sign
+ infinityPath = new SKPath();
+ infinityPath.MoveTo(0, 0); // Center
+ infinityPath.CubicTo(50, -50, 95, -100, 150, -100); // To top of right loop
+ infinityPath.CubicTo(205, -100, 250, -55, 250, 0); // To far right of right loop
+ infinityPath.CubicTo(250, 55, 205, 100, 150, 100); // To bottom of right loop
+ infinityPath.CubicTo(95, 100, 50, 50, 0, 0); // Back to center
+ infinityPath.CubicTo(-50, -50, -95, -100, -150, -100); // To top of left loop
+ infinityPath.CubicTo(-205, -100, -250, -55, -250, 0); // To far left of left loop
+ infinityPath.CubicTo(-250, 55, -205, 100, -150, 100); // To bottom of left loop
+ infinityPath.CubicTo(-95, 100, -50, 50, 0, 0); // Back to center
+ infinityPath.Close();
+
+ // Calculate path information
+ pathBounds = infinityPath.Bounds;
+ gradientCycleLength = pathBounds.Width +
+ pathBounds.Height * pathBounds.Height / pathBounds.Width;
+
+ // Create SKColor array for gradient
+ for (int i = 0; i < colors.Length; i++)
+ {
+ colors[i] = SKColor.FromHsl(i * 360f / (colors.Length - 1), 100, 50);
+ }
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+
+ isAnimating = true;
+ stopwatch.Start();
+ Dispatcher.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+
+ stopwatch.Stop();
+ isAnimating = false;
+ }
+
+ bool OnTimerTick()
+ {
+ const int duration = 2; // seconds
+ double progress = stopwatch.Elapsed.TotalSeconds % duration / duration;
+ offset = (float)(gradientCycleLength * progress);
+ canvasView.InvalidateSurface();
+
+ return isAnimating;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Set transforms to shift path to center and scale to canvas size
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.Scale(0.95f *
+ Math.Min(info.Width / (pathBounds.Width + STROKE_WIDTH),
+ info.Height / (pathBounds.Height + STROKE_WIDTH)));
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = STROKE_WIDTH;
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(pathBounds.Left, pathBounds.Top),
+ new SKPoint(pathBounds.Right, pathBounds.Bottom),
+ colors,
+ null,
+ SKShaderTileMode.Repeat,
+ SKMatrix.MakeTranslation(offset, 0));
+
+ canvas.DrawPath(infinityPath, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/InteractiveLinearGradientPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/InteractiveLinearGradientPage.xaml
new file mode 100644
index 000000000..63feebeda
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/InteractiveLinearGradientPage.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/InteractiveLinearGradientPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/InteractiveLinearGradientPage.xaml.cs
new file mode 100644
index 000000000..a3738bd87
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/InteractiveLinearGradientPage.xaml.cs
@@ -0,0 +1,95 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class InteractiveLinearGradientPage : InteractivePage
+{
+ public InteractiveLinearGradientPage()
+ {
+ InitializeComponent();
+
+ touchPoints = new TouchPoint[2];
+
+ for (int i = 0; i < 2; i++)
+ {
+ touchPoints[i] = new TouchPoint
+ {
+ Center = new SKPoint(100 + i * 200, 100 + i * 200)
+ };
+ }
+
+ baseCanvasView = canvasView;
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKColor[] colors = { SKColors.Red, SKColors.Green, SKColors.Blue };
+ SKShaderTileMode tileMode =
+ (SKShaderTileMode)(tileModePicker.SelectedIndex == -1 ?
+ 0 : tileModePicker.SelectedItem);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateLinearGradient(touchPoints[0].Center,
+ touchPoints[1].Center,
+ colors,
+ null,
+ tileMode);
+ canvas.DrawRect(info.Rect, paint);
+ }
+
+ // Display the touch points here rather than by TouchPoint
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Black;
+ paint.StrokeWidth = 3;
+
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ canvas.DrawCircle(touchPoint.Center, touchPoint.Radius, paint);
+ }
+
+ // Draw gradient line connecting touchpoints
+ canvas.DrawLine(touchPoints[0].Center, touchPoints[1].Center, paint);
+
+ // Draw lines perpendicular to the gradient line
+ SKPoint vector = touchPoints[1].Center - touchPoints[0].Center;
+ float length = (float)Math.Sqrt(Math.Pow(vector.X, 2) +
+ Math.Pow(vector.Y, 2));
+ vector.X /= length;
+ vector.Y /= length;
+ SKPoint rotate90 = new SKPoint(-vector.Y, vector.X);
+ rotate90.X *= 200;
+ rotate90.Y *= 200;
+
+ canvas.DrawLine(touchPoints[0].Center,
+ touchPoints[0].Center + rotate90,
+ paint);
+
+ canvas.DrawLine(touchPoints[0].Center,
+ touchPoints[0].Center - rotate90,
+ paint);
+
+ canvas.DrawLine(touchPoints[1].Center,
+ touchPoints[1].Center + rotate90,
+ paint);
+
+ canvas.DrawLine(touchPoints[1].Center,
+ touchPoints[1].Center - rotate90,
+ paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/LightenAndDarkenPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/LightenAndDarkenPage.xaml
new file mode 100644
index 000000000..7337a6d6e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/LightenAndDarkenPage.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/LightenAndDarkenPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/LightenAndDarkenPage.xaml.cs
new file mode 100644
index 000000000..dd1bd5957
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/LightenAndDarkenPage.xaml.cs
@@ -0,0 +1,68 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class LightenAndDarkenPage : ContentPage
+{
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(LightenAndDarkenPage),
+ "SkiaSharpDemos.Media.banana.jpg");
+
+ public LightenAndDarkenPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if ((Slider)sender == lightenSlider)
+ {
+ lightenCanvasView.InvalidateSurface();
+ }
+ else
+ {
+ darkenCanvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Find largest size rectangle in canvas
+ float scale = Math.Min((float)info.Width / bitmap.Width,
+ (float)info.Height / bitmap.Height);
+ SKRect rect = SKRect.Create(scale * bitmap.Width, scale * bitmap.Height);
+ float x = (info.Width - rect.Width) / 2;
+ float y = (info.Height - rect.Height) / 2;
+ rect.Offset(x, y);
+
+ // Display bitmap
+ canvas.DrawBitmap(bitmap, rect);
+
+ // Display gray rectangle with blend mode
+ using (SKPaint paint = new SKPaint())
+ {
+ if ((SKCanvasView)sender == lightenCanvasView)
+ {
+ byte value = (byte)(255 * lightenSlider.Value);
+ paint.Color = new SKColor(value, value, value);
+ paint.BlendMode = SKBlendMode.Lighten;
+ }
+ else
+ {
+ byte value = (byte)(255 * (1 - darkenSlider.Value));
+ paint.Color = new SKColor(value, value, value);
+ paint.BlendMode = SKBlendMode.Darken;
+ }
+
+ canvas.DrawRect(rect, paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/MaskBlurExperimentPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/MaskBlurExperimentPage.xaml
new file mode 100644
index 000000000..2055a6023
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/MaskBlurExperimentPage.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/MaskBlurExperimentPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/MaskBlurExperimentPage.xaml.cs
new file mode 100644
index 000000000..0bc40e3fb
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/MaskBlurExperimentPage.xaml.cs
@@ -0,0 +1,68 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class MaskBlurExperimentPage : ContentPage
+{
+ const string TEXT = "Blur My Text";
+
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(MaskBlurExperimentPage),
+ "SkiaSharpDemos.Media.seatedmonkey.jpg");
+
+ public MaskBlurExperimentPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear(SKColors.Pink);
+
+ // Get values from XAML controls
+ SKBlurStyle blurStyle =
+ (SKBlurStyle)(blurStylePicker.SelectedIndex == -1 ?
+ 0 : blurStylePicker.SelectedItem);
+
+ float sigma = (float)sigmaSlider.Value;
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Set SKPaint properties
+ paint.TextSize = (info.Width - 100) / (TEXT.Length / 2);
+ paint.MaskFilter = SKMaskFilter.CreateBlur(blurStyle, sigma);
+
+ // Get text bounds and calculate display rectangle
+ SKRect textBounds = new SKRect();
+ paint.MeasureText(TEXT, ref textBounds);
+ SKRect textRect = new SKRect(0, 0, info.Width, textBounds.Height + 50);
+
+ // Center the text in the display rectangle
+ float xText = textRect.Width / 2 - textBounds.MidX;
+ float yText = textRect.Height / 2 - textBounds.MidY;
+
+ canvas.DrawText(TEXT, xText, yText, paint);
+
+ // Calculate rectangle for bitmap
+ SKRect bitmapRect = new SKRect(0, textRect.Bottom, info.Width, info.Height);
+ bitmapRect.Inflate(-50, -50);
+
+ canvas.DrawBitmap(bitmap, bitmapRect, BitmapStretch.Uniform, paint: paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/NonSeparableBlendModesPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/NonSeparableBlendModesPage.xaml
new file mode 100644
index 000000000..92513a63b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/NonSeparableBlendModesPage.xaml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/NonSeparableBlendModesPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/NonSeparableBlendModesPage.xaml.cs
new file mode 100644
index 000000000..23620915c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/NonSeparableBlendModesPage.xaml.cs
@@ -0,0 +1,63 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class NonSeparableBlendModesPage : ContentPage
+{
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(NonSeparableBlendModesPage),
+ "SkiaSharpDemos.Media.banana.jpg");
+ SKColor color;
+
+ public NonSeparableBlendModesPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs e)
+ {
+ // Calculate new color based on sliders
+ color = SKColor.FromHsl((float)hueSlider.Value,
+ (float)satSlider.Value,
+ (float)lumSlider.Value);
+
+ // Use labels to display HSL and RGB color values
+ color.ToHsl(out float hue, out float sat, out float lum);
+
+ hslLabel.Text = String.Format("HSL = {0:F0} {1:F0} {2:F0}",
+ hue, sat, lum);
+
+ rgbLabel.Text = String.Format("RGB = {0:X2} {1:X2} {2:X2}",
+ color.Red, color.Green, color.Blue);
+
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+ canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);
+
+ // Get blend mode from Picker
+ SKBlendMode blendMode =
+ (SKBlendMode)(blendModePicker.SelectedIndex == -1 ?
+ 0 : blendModePicker.SelectedItem);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Color = color;
+ paint.BlendMode = blendMode;
+ canvas.DrawRect(info.Rect, paint);
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PastelMatrixPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PastelMatrixPage.cs
new file mode 100644
index 000000000..54c1d7faf
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PastelMatrixPage.cs
@@ -0,0 +1,46 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class PastelMatrixPage : ContentPage
+ {
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(PastelMatrixPage),
+ "SkiaSharpDemos.Media.mountainclimbers.jpg");
+
+ public PastelMatrixPage()
+ {
+ Title = "Pastel Matrix";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.ColorFilter =
+ SKColorFilter.CreateColorMatrix(new float[]
+ {
+ 0.75f, 0.25f, 0.25f, 0, 0,
+ 0.25f, 0.75f, 0.25f, 0, 0,
+ 0.25f, 0.25f, 0.75f, 0, 0,
+ 0, 0, 0, 1, 0
+ });
+
+ canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform, paint: paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PerlinNoisePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PerlinNoisePage.xaml
new file mode 100644
index 000000000..f93b4b4e9
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PerlinNoisePage.xaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PerlinNoisePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PerlinNoisePage.xaml.cs
new file mode 100644
index 000000000..6c43255c6
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PerlinNoisePage.xaml.cs
@@ -0,0 +1,61 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class PerlinNoisePage : ContentPage
+{
+ public PerlinNoisePage()
+ {
+ InitializeComponent();
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnStepperValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Get values from sliders and stepper
+ float baseFreqX = (float)Math.Pow(10, baseFrequencyXSlider.Value - 4);
+ baseFrequencyXText.Text = String.Format("Base Frequency X = {0:F4}", baseFreqX);
+
+ float baseFreqY = (float)Math.Pow(10, baseFrequencyYSlider.Value - 4);
+ baseFrequencyYText.Text = String.Format("Base Frequency Y = {0:F4}", baseFreqY);
+
+ int numOctaves = (int)octavesStepper.Value;
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader =
+ SKShader.CreatePerlinNoiseFractalNoise(baseFreqX,
+ baseFreqY,
+ numOctaves,
+ 0);
+
+ SKRect rect = new SKRect(0, 0, info.Width, info.Height / 2);
+ canvas.DrawRect(rect, paint);
+
+ paint.Shader =
+ SKShader.CreatePerlinNoiseTurbulence(baseFreqX,
+ baseFreqY,
+ numOctaves,
+ 0);
+
+ rect = new SKRect(0, info.Height / 2, info.Width, info.Height);
+ canvas.DrawRect(rect, paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PhotographicBrickWallPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PhotographicBrickWallPage.cs
new file mode 100644
index 000000000..7b8090a25
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PhotographicBrickWallPage.cs
@@ -0,0 +1,42 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class PhotographicBrickWallPage : ContentPage
+ {
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(PhotographicBrickWallPage),
+ "SkiaSharpDemos.Media.brickwalltile.jpg");
+
+ public PhotographicBrickWallPage()
+ {
+ Title = "Photographic Brick Wall";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Create bitmap tiling
+ paint.Shader = SKShader.CreateBitmap(bitmap,
+ SKShaderTileMode.Mirror,
+ SKShaderTileMode.Mirror);
+ // Draw background
+ canvas.DrawRect(info.Rect, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffCanvasView.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffCanvasView.cs
new file mode 100644
index 000000000..5cbb90703
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffCanvasView.cs
@@ -0,0 +1,76 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ class PorterDuffCanvasView : SKCanvasView
+ {
+ static SKBitmap srcBitmap, dstBitmap;
+
+ static PorterDuffCanvasView()
+ {
+ dstBitmap = new SKBitmap(300, 300);
+ srcBitmap = new SKBitmap(300, 300);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ using (SKCanvas canvas = new SKCanvas(dstBitmap))
+ {
+ canvas.Clear();
+ paint.Color = new SKColor(0xC0, 0x80, 0x00);
+ canvas.DrawRect(new SKRect(0, 0, 200, 200), paint);
+ }
+ using (SKCanvas canvas = new SKCanvas(srcBitmap))
+ {
+ canvas.Clear();
+ paint.Color = new SKColor(0x00, 0x80, 0xC0);
+ canvas.DrawRect(new SKRect(100, 100, 300, 300), paint);
+ }
+ }
+ }
+
+ SKBlendMode blendMode;
+
+ public PorterDuffCanvasView(SKBlendMode blendMode)
+ {
+ this.blendMode = blendMode;
+ }
+
+ protected override void OnPaintSurface(SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Find largest square that fits
+ float rectSize = Math.Min(info.Width, info.Height);
+ float x = (info.Width - rectSize) / 2;
+ float y = (info.Height - rectSize) / 2;
+ SKRect rect = new SKRect(x, y, x + rectSize, y + rectSize);
+
+ // Draw destination bitmap
+ canvas.DrawBitmap(dstBitmap, rect);
+
+ // Draw source bitmap
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.BlendMode = blendMode;
+ canvas.DrawBitmap(srcBitmap, rect, paint);
+ }
+
+ // Draw outline
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Black;
+ paint.StrokeWidth = 2;
+ rect.Inflate(-1, -1);
+ canvas.DrawRect(rect, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffGridPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffGridPage.cs
new file mode 100644
index 000000000..7f3da613f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffGridPage.cs
@@ -0,0 +1,61 @@
+using SkiaSharp;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class PorterDuffGridPage : ContentPage
+ {
+ public PorterDuffGridPage()
+ {
+ Title = "Porter-Duff Grid";
+
+ SKBlendMode[] blendModes =
+ {
+ SKBlendMode.Src, SKBlendMode.Dst, SKBlendMode.SrcOver, SKBlendMode.DstOver,
+ SKBlendMode.SrcIn, SKBlendMode.DstIn, SKBlendMode.SrcOut, SKBlendMode.DstOut,
+ SKBlendMode.SrcATop, SKBlendMode.DstATop, SKBlendMode.Xor, SKBlendMode.Plus,
+ SKBlendMode.Modulate, SKBlendMode.Clear
+ };
+
+ Grid grid = new Grid
+ {
+ Margin = new Thickness(5)
+ };
+
+ for (int row = 0; row < 4; row++)
+ {
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
+ }
+
+ for (int col = 0; col < 4; col++)
+ {
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
+ }
+
+ for (int i = 0; i < blendModes.Length; i++)
+ {
+ SKBlendMode blendMode = blendModes[i];
+ int row = 2 * (i / 4);
+ int col = i % 4;
+
+ Label label = new Label
+ {
+ Text = blendMode.ToString(),
+ HorizontalTextAlignment = TextAlignment.Center
+ };
+ Grid.SetRow(label, row);
+ Grid.SetColumn(label, col);
+ grid.Add(label);
+
+ PorterDuffCanvasView canvasView = new PorterDuffCanvasView(blendMode);
+
+ Grid.SetRow(canvasView, row + 1);
+ Grid.SetColumn(canvasView, col);
+ grid.Add(canvasView);
+ }
+
+ Content = grid;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffTransparencyPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffTransparencyPage.xaml
new file mode 100644
index 000000000..dc15f94fc
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffTransparencyPage.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffTransparencyPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffTransparencyPage.xaml.cs
new file mode 100644
index 000000000..da00e0680
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PorterDuffTransparencyPage.xaml.cs
@@ -0,0 +1,69 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class PorterDuffTransparencyPage : ContentPage
+{
+ public PorterDuffTransparencyPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Make square display rectangle smaller than canvas
+ float size = 0.9f * Math.Min(info.Width, info.Height);
+ float x = (info.Width - size) / 2;
+ float y = (info.Height - size) / 2;
+ SKRect rect = new SKRect(x, y, x + size, y + size);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Draw destination
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(rect.Right, rect.Top),
+ new SKPoint(rect.Left, rect.Bottom),
+ new SKColor[] { new SKColor(0xC0, 0x80, 0x00),
+ new SKColor(0xC0, 0x80, 0x00, 0) },
+ new float[] { 0.4f, 0.6f },
+ SKShaderTileMode.Clamp);
+
+ canvas.DrawRect(rect, paint);
+
+ // Draw source
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(rect.Left, rect.Top),
+ new SKPoint(rect.Right, rect.Bottom),
+ new SKColor[] { new SKColor(0x00, 0x80, 0xC0),
+ new SKColor(0x00, 0x80, 0xC0, 0) },
+ new float[] { 0.4f, 0.6f },
+ SKShaderTileMode.Clamp);
+
+ // Get the blend mode from the picker
+ paint.BlendMode = blendModePicker.SelectedIndex == -1 ? 0 :
+ (SKBlendMode)blendModePicker.SelectedItem;
+
+ canvas.DrawRect(rect, paint);
+
+ // Stroke surrounding rectangle
+ paint.Shader = null;
+ paint.BlendMode = SKBlendMode.SrcOver;
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Black;
+ paint.StrokeWidth = 3;
+ canvas.DrawRect(rect, paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PosterizeTablePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PosterizeTablePage.cs
new file mode 100644
index 000000000..40217a716
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PosterizeTablePage.cs
@@ -0,0 +1,48 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class PosterizeTablePage : ContentPage
+ {
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(PosterizeTablePage),
+ "SkiaSharpDemos.Media.monkeyface.png");
+
+ byte[] colorTable = new byte[256];
+
+ public PosterizeTablePage()
+ {
+ Title = "Posterize Table";
+
+ // Create color table
+ for (int i = 0; i < 256; i++)
+ {
+ colorTable[i] = (byte)(0xC0 & i);
+ }
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.ColorFilter =
+ SKColorFilter.CreateTable(null, null, colorTable, colorTable);
+
+ canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform, paint: paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PrimaryColorsPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PrimaryColorsPage.cs
new file mode 100644
index 000000000..65521eb67
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/PrimaryColorsPage.cs
@@ -0,0 +1,85 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class PrimaryColorsPage : ContentPage
+ {
+ bool isSubtractive;
+
+ public PrimaryColorsPage()
+ {
+ Title = "Primary Colors";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+
+ // Switch between additive and subtractive primaries at tap
+ TapGestureRecognizer tap = new TapGestureRecognizer();
+ tap.Tapped += (sender, args) =>
+ {
+ isSubtractive ^= true;
+ canvasView.InvalidateSurface();
+ };
+ canvasView.GestureRecognizers.Add(tap);
+
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);
+ float radius = Math.Min(info.Width, info.Height) / 4;
+ float distance = 0.8f * radius; // from canvas center to circle center
+ SKPoint center1 = center +
+ new SKPoint(distance * (float)Math.Cos(9 * Math.PI / 6),
+ distance * (float)Math.Sin(9 * Math.PI / 6));
+ SKPoint center2 = center +
+ new SKPoint(distance * (float)Math.Cos(1 * Math.PI / 6),
+ distance * (float)Math.Sin(1 * Math.PI / 6));
+ SKPoint center3 = center +
+ new SKPoint(distance * (float)Math.Cos(5 * Math.PI / 6),
+ distance * (float)Math.Sin(5 * Math.PI / 6));
+
+ using (SKPaint paint = new SKPaint())
+ {
+ if (!isSubtractive)
+ {
+ paint.BlendMode = SKBlendMode.Plus;
+ System.Diagnostics.Debug.WriteLine(paint.BlendMode);
+
+ paint.Color = SKColors.Red;
+ canvas.DrawCircle(center1, radius, paint);
+
+ paint.Color = SKColors.Lime; // == (00, FF, 00)
+ canvas.DrawCircle(center2, radius, paint);
+
+ paint.Color = SKColors.Blue;
+ canvas.DrawCircle(center3, radius, paint);
+ }
+ else
+ {
+ paint.BlendMode = SKBlendMode.Multiply;
+ System.Diagnostics.Debug.WriteLine(paint.BlendMode);
+
+ paint.Color = SKColors.Cyan;
+ canvas.DrawCircle(center1, radius, paint);
+
+ paint.Color = SKColors.Magenta;
+ canvas.DrawCircle(center2, radius, paint);
+
+ paint.Color = SKColors.Yellow;
+ canvas.DrawCircle(center3, radius, paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialGradientMaskPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialGradientMaskPage.cs
new file mode 100644
index 000000000..4cf24f81b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialGradientMaskPage.cs
@@ -0,0 +1,67 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class RadialGradientMaskPage : ContentPage
+ {
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(RadialGradientMaskPage),
+ "SkiaSharpDemos.Media.mountainclimbers.jpg");
+
+ static readonly SKPoint CENTER = new SKPoint(180, 300);
+ static readonly float RADIUS = 120;
+
+ public RadialGradientMaskPage()
+ {
+ Title = "Radial Gradient Mask";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Find rectangle to display bitmap
+ float scale = Math.Min((float)info.Width / bitmap.Width,
+ (float)info.Height / bitmap.Height);
+
+ SKRect rect = SKRect.Create(scale * bitmap.Width, scale * bitmap.Height);
+
+ float x = (info.Width - rect.Width) / 2;
+ float y = (info.Height - rect.Height) / 2;
+ rect.Offset(x, y);
+
+ // Display bitmap in rectangle
+ canvas.DrawBitmap(bitmap, rect);
+
+ // Adjust center and radius for scaled and offset bitmap
+ SKPoint center = new SKPoint(scale * CENTER.X + x,
+ scale * CENTER.Y + y);
+ float radius = scale * RADIUS;
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateRadialGradient(
+ center,
+ radius,
+ new SKColor[] { SKColors.Transparent,
+ SKColors.White },
+ new float[] { 0.6f, 1 },
+ SKShaderTileMode.Clamp);
+
+ // Display rectangle using that gradient
+ canvas.DrawRect(rect, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialGradientPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialGradientPage.xaml
new file mode 100644
index 000000000..96fb056c8
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialGradientPage.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialGradientPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialGradientPage.xaml.cs
new file mode 100644
index 000000000..5ae2fbd42
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialGradientPage.xaml.cs
@@ -0,0 +1,42 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class RadialGradientPage : ContentPage
+{
+ public RadialGradientPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKShaderTileMode tileMode =
+ (SKShaderTileMode)(tileModePicker.SelectedIndex == -1 ?
+ 0 : tileModePicker.SelectedItem);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateRadialGradient(
+ new SKPoint(info.Rect.MidX, info.Rect.MidY),
+ 100,
+ new SKColor[] { SKColors.Black, SKColors.White },
+ null,
+ tileMode);
+
+ canvas.DrawRect(info.Rect, paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialSpecularHighlightPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialSpecularHighlightPage.cs
new file mode 100644
index 000000000..eaf73dd84
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RadialSpecularHighlightPage.cs
@@ -0,0 +1,44 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class RadialSpecularHighlightPage : ContentPage
+ {
+ public RadialSpecularHighlightPage()
+ {
+ Title = "Radial Specular Highlight";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float radius = 0.4f * Math.Min(info.Width, info.Height);
+ SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);
+ SKPoint offCenter = center - new SKPoint(radius / 2, radius / 2);
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateRadialGradient(
+ offCenter,
+ radius / 2,
+ new SKColor[] { SKColors.White, SKColors.Red },
+ null,
+ SKShaderTileMode.Clamp);
+
+ canvas.DrawCircle(center, radius, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RainbowArcGradientPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RainbowArcGradientPage.cs
new file mode 100644
index 000000000..0de74d1f5
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RainbowArcGradientPage.cs
@@ -0,0 +1,63 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class RainbowArcGradientPage : ContentPage
+ {
+ public RainbowArcGradientPage()
+ {
+ Title = "Rainbow Arc Gradient";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ float rainbowWidth = Math.Min(info.Width, info.Height) / 4f;
+
+ // Center of arc and gradient is lower-right corner
+ SKPoint center = new SKPoint(info.Width, info.Height);
+
+ // Find outer, inner, and middle radius
+ float outerRadius = Math.Min(info.Width, info.Height);
+ float innerRadius = outerRadius - rainbowWidth;
+ float radius = outerRadius - rainbowWidth / 2;
+
+ // Calculate the colors and positions
+ SKColor[] colors = new SKColor[8];
+ float[] positions = new float[8];
+
+ for (int i = 0; i < colors.Length; i++)
+ {
+ colors[i] = SKColor.FromHsl(i * 360f / 7, 100, 50);
+ positions[i] = (i + (7f - i) * innerRadius / outerRadius) / 7f;
+ }
+
+ // Create sweep gradient based on center and outer radius
+ paint.Shader = SKShader.CreateRadialGradient(center,
+ outerRadius,
+ colors,
+ positions,
+ SKShaderTileMode.Clamp);
+ // Draw a circle with a wide line
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = rainbowWidth;
+
+ canvas.DrawCircle(center, radius, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RainbowGradientPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RainbowGradientPage.cs
new file mode 100644
index 000000000..4aadad5f7
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/RainbowGradientPage.cs
@@ -0,0 +1,81 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class RainbowGradientPage : ContentPage
+ {
+ public RainbowGradientPage()
+ {
+ Title = "Rainbow Gradient";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPath path = new SKPath())
+ {
+ float rainbowWidth = Math.Min(info.Width, info.Height) / 2f;
+
+ // Create path from upper-left to lower-right corner
+ path.MoveTo(0, 0);
+ path.LineTo(rainbowWidth / 2, 0);
+ path.LineTo(info.Width, info.Height - rainbowWidth / 2);
+ path.LineTo(info.Width, info.Height);
+ path.LineTo(info.Width - rainbowWidth / 2, info.Height);
+ path.LineTo(0, rainbowWidth / 2);
+ path.Close();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ SKColor[] colors = new SKColor[8];
+
+ for (int i = 0; i < colors.Length; i++)
+ {
+ colors[i] = SKColor.FromHsl(i * 360f / (colors.Length - 1), 100, 50);
+ }
+
+ // Vector on lower-left edge, from top to bottom
+ SKPoint edgeVector = new SKPoint(info.Width - rainbowWidth / 2, info.Height) -
+ new SKPoint(0, rainbowWidth / 2);
+
+ // Rotate 90 degrees counter-clockwise:
+ SKPoint gradientVector = new SKPoint(edgeVector.Y, -edgeVector.X);
+
+ // Normalize
+ float length = (float)Math.Sqrt(Math.Pow(gradientVector.X, 2) +
+ Math.Pow(gradientVector.Y, 2));
+ gradientVector.X /= length;
+ gradientVector.Y /= length;
+
+ // Make it the width of the rainbow
+ gradientVector.X *= rainbowWidth;
+ gradientVector.Y *= rainbowWidth;
+
+ // Calculate the two points
+ SKPoint point1 = new SKPoint(0, rainbowWidth / 2);
+ SKPoint point2 = point1 + gradientVector;
+
+ paint.Shader = SKShader.CreateLinearGradient(point1,
+ point2,
+ colors,
+ null,
+ SKShaderTileMode.Repeat);
+
+ canvas.DrawPath(path, paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ReflectionGradientPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ReflectionGradientPage.cs
new file mode 100644
index 000000000..edab1951e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/ReflectionGradientPage.cs
@@ -0,0 +1,71 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class ReflectionGradientPage : ContentPage
+ {
+ const string TEXT = "Reflection";
+
+ public ReflectionGradientPage()
+ {
+ Title = "Reflection Gradient";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Set text color to blue
+ paint.Color = SKColors.Blue;
+
+ // Set text size to fill 90% of width
+ paint.TextSize = 100;
+ float width = paint.MeasureText(TEXT);
+ float scale = 0.9f * info.Width / width;
+ paint.TextSize *= scale;
+
+ // Get text bounds
+ SKRect textBounds = new SKRect();
+ paint.MeasureText(TEXT, ref textBounds);
+
+ // Calculate offsets to position text above center
+ float xText = info.Width / 2 - textBounds.MidX;
+ float yText = info.Height / 2;
+
+ // Draw unreflected text
+ canvas.DrawText(TEXT, xText, yText, paint);
+
+ // Shift textBounds to match displayed text
+ textBounds.Offset(xText, yText);
+
+ // Use those offsets to create a gradient for the reflected text
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(0, textBounds.Top),
+ new SKPoint(0, textBounds.Bottom),
+ new SKColor[] { paint.Color.WithAlpha(0),
+ paint.Color.WithAlpha(0x80) },
+ null,
+ SKShaderTileMode.Clamp);
+
+ // Scale the canvas to flip upside-down around the vertical center
+ canvas.Scale(1, -1, 0, yText);
+
+ // Draw reflected text
+ canvas.DrawText(TEXT, xText, yText, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/SeparableBlendModesPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/SeparableBlendModesPage.xaml
new file mode 100644
index 000000000..3e242ffc1
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/SeparableBlendModesPage.xaml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/SeparableBlendModesPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/SeparableBlendModesPage.xaml.cs
new file mode 100644
index 000000000..6c8a98688
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/SeparableBlendModesPage.xaml.cs
@@ -0,0 +1,71 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class SeparableBlendModesPage : ContentPage
+{
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(SeparableBlendModesPage),
+ "SkiaSharpDemos.Media.banana.jpg");
+
+ public SeparableBlendModesPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs e)
+ {
+ if (sender == graySlider)
+ {
+ redSlider.Value = greenSlider.Value = blueSlider.Value = graySlider.Value;
+ }
+
+ colorLabel.Text = String.Format("Color = {0:X2} {1:X2} {2:X2}",
+ (byte)(255 * redSlider.Value),
+ (byte)(255 * greenSlider.Value),
+ (byte)(255 * blueSlider.Value));
+
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Draw bitmap in top half
+ SKRect rect = new SKRect(0, 0, info.Width, info.Height / 2);
+ canvas.DrawBitmap(bitmap, rect, BitmapStretch.Uniform);
+
+ // Draw bitmap in bottom halr
+ rect = new SKRect(0, info.Height / 2, info.Width, info.Height);
+ canvas.DrawBitmap(bitmap, rect, BitmapStretch.Uniform);
+
+ // Get values from XAML controls
+ SKBlendMode blendMode =
+ (SKBlendMode)(blendModePicker.SelectedIndex == -1 ?
+ 0 : blendModePicker.SelectedItem);
+
+ SKColor color = new SKColor((byte)(255 * redSlider.Value),
+ (byte)(255 * greenSlider.Value),
+ (byte)(255 * blueSlider.Value));
+
+ // Draw rectangle with blend mode in bottom half
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Color = color;
+ paint.BlendMode = blendMode;
+ canvas.DrawRect(rect, paint);
+ }
+ }
+}
+
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/StoneWallPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/StoneWallPage.cs
new file mode 100644
index 000000000..5eceb04b3
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/StoneWallPage.cs
@@ -0,0 +1,46 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class StoneWallPage : ContentPage
+ {
+ SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
+ typeof(StoneWallPage),
+ "SkiaSharpDemos.Media.stonewalltile.jpg");
+
+ public StoneWallPage()
+ {
+ Title = "Stone Wall";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Create scale transform
+ SKMatrix matrix = SKMatrix.CreateScale(0.5f, 0.5f);
+
+ // Create bitmap tiling
+ paint.Shader = SKShader.CreateBitmap(bitmap,
+ SKShaderTileMode.Mirror,
+ SKShaderTileMode.Mirror,
+ matrix);
+ // Draw background
+ canvas.DrawRect(info.Rect, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/SweepGradientPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/SweepGradientPage.cs
new file mode 100644
index 000000000..b1d878a2c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/SweepGradientPage.cs
@@ -0,0 +1,69 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class SweepGradientPage : ContentPage
+ {
+ bool drawBackground;
+
+ public SweepGradientPage()
+ {
+ Title = "Sweep Gradient";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ TapGestureRecognizer tap = new TapGestureRecognizer();
+ tap.Tapped += (sender, args) =>
+ {
+ drawBackground ^= true;
+ canvasView.InvalidateSurface();
+ };
+ canvasView.GestureRecognizers.Add(tap);
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ // Define an array of rainbow colors
+ SKColor[] colors = new SKColor[8];
+
+ for (int i = 0; i < colors.Length; i++)
+ {
+ colors[i] = SKColor.FromHsl(i * 360f / 7, 100, 50);
+ }
+
+ SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);
+
+ // Create sweep gradient based on center of canvas
+ paint.Shader = SKShader.CreateSweepGradient(center, colors, null);
+
+ // Draw a circle with a wide line
+ const int strokeWidth = 50;
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = strokeWidth;
+
+ float radius = (Math.Min(info.Width, info.Height) - strokeWidth) / 2;
+ canvas.DrawCircle(center, radius, paint);
+
+ if (drawBackground)
+ {
+ // Draw the gradient on the whole canvas
+ paint.Style = SKPaintStyle.Fill;
+ canvas.DrawRect(info.Rect, paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/TileAlignmentPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/TileAlignmentPage.cs
new file mode 100644
index 000000000..05e9825e5
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/TileAlignmentPage.cs
@@ -0,0 +1,70 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Effects
+{
+ public class TileAlignmentPage : ContentPage
+ {
+ bool isAligned;
+
+ public TileAlignmentPage()
+ {
+ Title = "Tile Alignment";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+
+ // Add tap handler
+ TapGestureRecognizer tap = new TapGestureRecognizer();
+ tap.Tapped += (sender, args) =>
+ {
+ isAligned ^= true;
+ canvasView.InvalidateSurface();
+ };
+ canvasView.GestureRecognizers.Add(tap);
+
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ SKRect rect = new SKRect(info.Width / 7,
+ info.Height / 7,
+ 6 * info.Width / 7,
+ 6 * info.Height / 7);
+
+ // Get bitmap from other program
+ SKBitmap bitmap = AlgorithmicBrickWallPage.BrickWallTile;
+
+ // Create bitmap tiling
+ if (!isAligned)
+ {
+ paint.Shader = SKShader.CreateBitmap(bitmap,
+ SKShaderTileMode.Repeat,
+ SKShaderTileMode.Repeat);
+ }
+ else
+ {
+ SKMatrix matrix = SKMatrix.CreateTranslation(rect.Left, rect.Top);
+ paint.Shader = SKShader.CreateBitmap(bitmap,
+ SKShaderTileMode.Repeat,
+ SKShaderTileMode.Repeat,
+ matrix);
+ }
+
+ // Draw rectangle
+ canvas.DrawRect(rect, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/TiledPerlinNoisePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/TiledPerlinNoisePage.xaml
new file mode 100644
index 000000000..37cfc08aa
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/TiledPerlinNoisePage.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/TiledPerlinNoisePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/TiledPerlinNoisePage.xaml.cs
new file mode 100644
index 000000000..279f640f6
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Effects/TiledPerlinNoisePage.xaml.cs
@@ -0,0 +1,70 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Effects;
+
+public partial class TiledPerlinNoisePage : ContentPage
+{
+ const int TILE_SIZE = 200;
+
+ public TiledPerlinNoisePage()
+ {
+ InitializeComponent();
+ }
+
+ void OnStepperValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Get seed value from stepper
+ float seed = (float)seedStepper.Value;
+
+ SKRect tileRect = new SKRect(0, 0, TILE_SIZE, TILE_SIZE);
+
+ using (SKBitmap bitmap = new SKBitmap(TILE_SIZE, TILE_SIZE))
+ {
+ using (SKCanvas bitmapCanvas = new SKCanvas(bitmap))
+ {
+ bitmapCanvas.Clear();
+
+ // Draw tiled turbulence noise on bitmap
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreatePerlinNoiseTurbulence(
+ 0.02f, 0.02f, 1, seed,
+ new SKPointI(TILE_SIZE, TILE_SIZE));
+
+ bitmapCanvas.DrawRect(tileRect, paint);
+ }
+ }
+
+ // Draw tiled bitmap shader on canvas
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Shader = SKShader.CreateBitmap(bitmap,
+ SKShaderTileMode.Repeat,
+ SKShaderTileMode.Repeat);
+ canvas.DrawRect(info.Rect, paint);
+ }
+
+ // Draw rectangle showing tile
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Black;
+ paint.StrokeWidth = 2;
+
+ canvas.DrawRect(tileRect, paint);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/InteractivePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/InteractivePage.cs
new file mode 100644
index 000000000..4a8627b85
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/InteractivePage.cs
@@ -0,0 +1,55 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos
+{
+ public class InteractivePage : ContentPage
+ {
+ protected SKCanvasView baseCanvasView;
+ protected TouchPoint[] touchPoints;
+
+ protected SKPaint strokePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 3
+ };
+
+ protected SKPaint redStrokePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Red,
+ StrokeWidth = 15
+ };
+
+ protected SKPaint dottedStrokePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 3,
+ PathEffect = SKPathEffect.CreateDash(new float[] { 7, 7 }, 0)
+ };
+
+ protected void OnTouch(object sender, SKTouchEventArgs e)
+ {
+ bool touchPointMoved = false;
+
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ float scale = baseCanvasView.CanvasSize.Width / (float)baseCanvasView.Width;
+ SKPoint point = new SKPoint(scale * (float)e.Location.X,
+ scale * (float)e.Location.Y);
+ touchPointMoved |= touchPoint.ProcessTouchEvent(e.Id, e.ActionType, point);
+ }
+
+ if (touchPointMoved)
+ {
+ baseCanvasView.InvalidateSurface();
+ }
+
+ e.Handled = true;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MainPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MainPage.xaml
new file mode 100644
index 000000000..6b5e7525b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MainPage.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MainPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MainPage.xaml.cs
new file mode 100644
index 000000000..a9b1cc0cb
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MainPage.xaml.cs
@@ -0,0 +1,9 @@
+namespace SkiaSharpDemos;
+
+public partial class MainPage : BasePage
+{
+ public MainPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MatrixDisplay.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MatrixDisplay.cs
new file mode 100644
index 000000000..59180d4cc
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MatrixDisplay.cs
@@ -0,0 +1,103 @@
+using SkiaSharp;
+
+namespace SkiaSharpDemos
+{
+ class MatrixDisplay
+ {
+ public SKPaint MatrixPaint { set; get; } = new SKPaint
+ {
+ Color = SKColors.Black,
+ TextSize = 48,
+ StrokeWidth = 2
+ };
+
+ public string PerspectiveFormat { set; get; } = "F0";
+
+ public SKSize Measure(SKMatrix matrix)
+ {
+ return MeasureAndPaint(null, matrix, new SKPoint(), false);
+ }
+
+ public void Paint(SKCanvas canvas, SKMatrix matrix, SKPoint location)
+ {
+ MeasureAndPaint(canvas, matrix, location, true);
+ }
+
+ SKSize MeasureAndPaint(SKCanvas canvas, SKMatrix matrix, SKPoint location, bool doPaint)
+ {
+ float[] values = matrix.Values;
+ string[] texts = new string[9];
+ SKRect[] bounds = new SKRect[9];
+ float[] widths = new float[3];
+
+ for (int i = 0; i < 9; i++)
+ {
+ int row = i % 3;
+ int col = i / 3;
+
+ // Format string differently based on row
+ texts[i] = values[i].ToString(row == 2 ? "F0" : (col == 2 ? PerspectiveFormat : "F2"));
+
+ // Measure string with a '-' even if one is not present
+ bool isNegative = texts[i][0] == '-';
+ string text = (isNegative ? "" : "-") + texts[i];
+ MatrixPaint.MeasureText(text, ref bounds[i]);
+
+ // Get maximum width for each column
+ widths[col] = Math.Max(widths[col], bounds[i].Width);
+
+ // Measure the text again without the '-' in front
+ MatrixPaint.MeasureText(texts[i], ref bounds[i]);
+ }
+
+ // Some formatting constants
+ float horzGap = MatrixPaint.TextSize;
+ float horzMarg = MatrixPaint.TextSize;
+ float vertMarg = MatrixPaint.FontSpacing / 4;
+
+ // Calculate the total width and height of the matrix display
+ float totalWidth = widths[0] + widths[1] + widths[2] + 2 * horzGap + 2 * horzMarg;
+ float totalHeight = 3 * MatrixPaint.FontSpacing + 2 * vertMarg;
+
+ if (doPaint)
+ {
+ SKPaintStyle saveStyle = MatrixPaint.Style;
+
+ for (int i = 0; i < 9; i++)
+ {
+ int row = i % 3;
+ int col = i / 3;
+
+ // Find x, y of upper-left corner of text
+ float x = location.X + horzMarg;
+
+ for (int c = 0; c < col; c++)
+ {
+ x += widths[c] + horzGap;
+ }
+
+ float y = location.Y + vertMarg + row * MatrixPaint.FontSpacing;
+
+ // Adjust for right-justified text
+ x += widths[col] - bounds[i].Width;
+ y += (MatrixPaint.FontSpacing - bounds[i].Height) / 2 - bounds[i].Top;
+
+ // Draw the text
+ MatrixPaint.Style = SKPaintStyle.Fill;
+ canvas.DrawText(texts[i], x, y, MatrixPaint);
+ }
+
+ // Display vertical lines at the sides of the matrix
+ MatrixPaint.Style = SKPaintStyle.Stroke;
+ canvas.DrawLine(location.X + horzMarg / 2, location.Y + vertMarg,
+ location.X + horzMarg / 2, location.Y + totalHeight - vertMarg, MatrixPaint);
+ canvas.DrawLine(location.X + totalWidth - horzMarg / 2, location.Y + vertMarg,
+ location.X + totalWidth - horzMarg / 2, location.Y + totalHeight - vertMarg, MatrixPaint);
+
+ MatrixPaint.Style = saveStyle;
+ }
+ return new SKSize(totalWidth, totalHeight);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MauiProgram.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MauiProgram.cs
new file mode 100644
index 000000000..e4c0f6ef6
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/MauiProgram.cs
@@ -0,0 +1,27 @@
+using Microsoft.Extensions.Logging;
+using SkiaSharp.Views.Maui.Controls.Hosting;
+
+namespace SkiaSharpDemos;
+
+public static class MauiProgram
+{
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder
+ .UseMauiApp()
+ .UseSkiaSharp()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
+ });
+
+#if DEBUG
+ builder.Logging.AddDebug();
+#endif
+
+ return builder.Build();
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/banana.jpg b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/banana.jpg
new file mode 100644
index 000000000..99c9388aa
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/banana.jpg differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/bananamatte.png b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/bananamatte.png
new file mode 100644
index 000000000..7b8d3dcb4
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/bananamatte.png differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/brickwalltile.jpg b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/brickwalltile.jpg
new file mode 100644
index 000000000..953553f1c
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/brickwalltile.jpg differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/facepalm.jpg b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/facepalm.jpg
new file mode 100644
index 000000000..3a53d198f
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/facepalm.jpg differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/monkey.png b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/monkey.png
new file mode 100644
index 000000000..ee9a60754
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/monkey.png differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/monkeyface.png b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/monkeyface.png
new file mode 100644
index 000000000..e07a82624
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/monkeyface.png differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/mountainclimbers.jpg b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/mountainclimbers.jpg
new file mode 100644
index 000000000..37d3695a3
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/mountainclimbers.jpg differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/newtons_cradle_animation_book_2.gif b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/newtons_cradle_animation_book_2.gif
new file mode 100644
index 000000000..7c05ee1e7
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/newtons_cradle_animation_book_2.gif differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/pageofcode.png b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/pageofcode.png
new file mode 100644
index 000000000..63821c822
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/pageofcode.png differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/seatedmonkey.jpg b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/seatedmonkey.jpg
new file mode 100644
index 000000000..22034b184
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/seatedmonkey.jpg differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/seatedmonkeymatte.png b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/seatedmonkeymatte.png
new file mode 100644
index 000000000..8e8d27040
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/seatedmonkeymatte.png differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/stonewalltile.jpg b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/stonewalltile.jpg
new file mode 100644
index 000000000..0f9178ad9
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Media/stonewalltile.jpg differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/AnimatedSpiralPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/AnimatedSpiralPage.cs
new file mode 100644
index 000000000..4f7076f96
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/AnimatedSpiralPage.cs
@@ -0,0 +1,97 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+using System.Diagnostics;
+
+namespace SkiaSharpDemos.Paths
+{
+ public class AnimatedSpiralPage : ContentPage
+ {
+ const double cycleTime = 250; // in milliseconds
+
+ SKCanvasView canvasView;
+ Stopwatch stopwatch = new Stopwatch();
+ bool pageIsActive;
+ float dashPhase;
+
+ public AnimatedSpiralPage()
+ {
+ Title = "Animated Spiral";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ pageIsActive = true;
+ stopwatch.Start();
+
+ Dispatcher.StartTimer(TimeSpan.FromMilliseconds(33), () =>
+ {
+ double t = stopwatch.Elapsed.TotalMilliseconds % cycleTime / cycleTime;
+ dashPhase = (float)(10 * t);
+ canvasView.InvalidateSurface();
+
+ if (!pageIsActive)
+ {
+ stopwatch.Stop();
+ }
+
+ return pageIsActive;
+ });
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ pageIsActive = false;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPoint center = new SKPoint(info.Width / 2, info.Height / 2);
+ float radius = Math.Min(center.X, center.Y);
+
+ using (SKPath path = new SKPath())
+ {
+ for (float angle = 0; angle < 3600; angle += 1)
+ {
+ float scaledRadius = radius * angle / 3600;
+ double radians = Math.PI * angle / 180;
+ float x = center.X + scaledRadius * (float)Math.Cos(radians);
+ float y = center.Y + scaledRadius * (float)Math.Sin(radians);
+ SKPoint point = new SKPoint(x, y);
+
+ if (angle == 0)
+ {
+ path.MoveTo(point);
+ }
+ else
+ {
+ path.LineTo(point);
+ }
+ }
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Red;
+ paint.StrokeWidth = 5;
+ paint.PathEffect = SKPathEffect.CreateDash(new float[] { 5, 5 }, dashPhase);
+
+ canvas.DrawPath(path, paint);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/ArchimedeanSpiralPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/ArchimedeanSpiralPage.cs
new file mode 100644
index 000000000..988f8f371
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/ArchimedeanSpiralPage.cs
@@ -0,0 +1,61 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Paths
+{
+ public class ArchimedeanSpiralPage : ContentPage
+ {
+ public ArchimedeanSpiralPage()
+ {
+ Title = "Archimedean Spiral";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPoint center = new SKPoint(info.Width / 2, info.Height / 2);
+ float radius = Math.Min(center.X, center.Y);
+
+ using (SKPath path = new SKPath())
+ {
+ for (float angle = 0; angle < 3600; angle += 1)
+ {
+ float scaledRadius = radius * angle / 3600;
+ double radians = Math.PI * angle / 180;
+ float x = center.X + scaledRadius * (float)Math.Cos(radians);
+ float y = center.Y + scaledRadius * (float)Math.Sin(radians);
+ SKPoint point = new SKPoint(x, y);
+
+ if (angle == 0)
+ {
+ path.MoveTo(point);
+ }
+ else
+ {
+ path.LineTo(point);
+ }
+ }
+
+ SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Red,
+ StrokeWidth = 5
+ };
+
+ canvas.DrawPath(path, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/DotsAndDashesPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/DotsAndDashesPage.xaml
new file mode 100644
index 000000000..f36201372
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/DotsAndDashesPage.xaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+ 10, 10
+ 30, 10
+ 10, 10, 30, 10
+ 0, 20
+ 20, 20
+ 0, 20, 20, 20
+
+
+ 0
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/DotsAndDashesPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/DotsAndDashesPage.xaml.cs
new file mode 100644
index 000000000..96959fc5f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/DotsAndDashesPage.xaml.cs
@@ -0,0 +1,65 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Paths;
+
+public partial class DotsAndDashesPage : ContentPage
+{
+ public DotsAndDashesPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Blue,
+ StrokeWidth = 10,
+ StrokeCap = (SKStrokeCap)strokeCapPicker.SelectedItem,
+ PathEffect = SKPathEffect.CreateDash(GetPickerArray(dashArrayPicker), 20)
+ };
+
+ SKPath path = new SKPath();
+ path.MoveTo(0.2f * info.Width, 0.2f * info.Height);
+ path.LineTo(0.8f * info.Width, 0.8f * info.Height);
+ path.LineTo(0.2f * info.Width, 0.8f * info.Height);
+ path.LineTo(0.8f * info.Width, 0.2f * info.Height);
+
+ canvas.DrawPath(path, paint);
+ }
+
+ float[] GetPickerArray(Picker picker)
+ {
+ if (picker.SelectedIndex == -1)
+ {
+ return new float[0];
+ }
+
+ string str = (string)picker.SelectedItem;
+ string[] strs = str.Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ float[] array = new float[strs.Length];
+
+ for (int i = 0; i < strs.Length; i++)
+ {
+ array[i] = Convert.ToSingle(strs[i]);
+ }
+ return array;
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FingerPaintPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FingerPaintPage.xaml
new file mode 100644
index 000000000..93058da37
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FingerPaintPage.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FingerPaintPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FingerPaintPage.xaml.cs
new file mode 100644
index 000000000..b23c90002
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FingerPaintPage.xaml.cs
@@ -0,0 +1,86 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Paths;
+
+public partial class FingerPaintPage : ContentPage
+{
+ Dictionary inProgressPaths = new Dictionary();
+ List completedPaths = new List();
+
+ SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Blue,
+ StrokeWidth = 10,
+ StrokeCap = SKStrokeCap.Round,
+ StrokeJoin = SKStrokeJoin.Round
+ };
+
+ public FingerPaintPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnTouch(object sender, SKTouchEventArgs e)
+ {
+ switch (e.ActionType)
+ {
+ case SKTouchAction.Pressed:
+ if (!inProgressPaths.ContainsKey(e.Id))
+ {
+ SKPath path = new SKPath();
+ path.MoveTo(ConvertToPixel(e.Location));
+ inProgressPaths.Add(e.Id, path);
+ canvasView.InvalidateSurface();
+ }
+ break;
+ case SKTouchAction.Moved:
+ if (inProgressPaths.ContainsKey(e.Id))
+ {
+ SKPath path = inProgressPaths[e.Id];
+ path.LineTo(ConvertToPixel(e.Location));
+ canvasView.InvalidateSurface();
+ }
+ break;
+ case SKTouchAction.Released:
+ if (inProgressPaths.ContainsKey(e.Id))
+ {
+ completedPaths.Add(inProgressPaths[e.Id]);
+ inProgressPaths.Remove(e.Id);
+ canvasView.InvalidateSurface();
+ }
+ break;
+ case SKTouchAction.Cancelled:
+ if (inProgressPaths.ContainsKey(e.Id))
+ {
+ inProgressPaths.Remove(e.Id);
+ canvasView.InvalidateSurface();
+ }
+ break;
+ }
+ e.Handled = true;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKCanvas canvas = args.Surface.Canvas;
+ canvas.Clear();
+
+ foreach (SKPath path in completedPaths)
+ {
+ canvas.DrawPath(path, paint);
+ }
+
+ foreach (SKPath path in inProgressPaths.Values)
+ {
+ canvas.DrawPath(path, paint);
+ }
+ }
+
+ SKPoint ConvertToPixel(SKPoint pt)
+ {
+ return new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
+ (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FivePointedStarPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FivePointedStarPage.xaml
new file mode 100644
index 000000000..7922cc84c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FivePointedStarPage.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+ Fill only
+ Stroke only
+ Stroke then Fill
+ Fill then Stroke
+
+
+ 0
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FivePointedStarPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FivePointedStarPage.xaml.cs
new file mode 100644
index 000000000..92ae5b5e3
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/FivePointedStarPage.xaml.cs
@@ -0,0 +1,82 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Paths;
+
+public partial class FivePointedStarPage : ContentPage
+{
+ public FivePointedStarPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPoint center = new SKPoint(info.Width / 2, info.Height / 2);
+ float radius = 0.45f * Math.Min(info.Width, info.Height);
+
+ SKPath path = new SKPath
+ {
+ FillType = (SKPathFillType)fillTypePicker.SelectedItem
+ };
+ path.MoveTo(info.Width / 2, info.Height / 2 - radius);
+
+ for (int i = 1; i < 5; i++)
+ {
+ // angle from vertical
+ double angle = i * 4 * Math.PI / 5;
+ path.LineTo(center + new SKPoint(radius * (float)Math.Sin(angle),
+ -radius * (float)Math.Cos(angle)));
+ }
+ path.Close();
+
+ SKPaint strokePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Red,
+ StrokeWidth = 50,
+ StrokeJoin = SKStrokeJoin.Round
+ };
+
+ SKPaint fillPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Blue
+ };
+
+ switch ((string)drawingModePicker.SelectedItem)
+ {
+ case "Fill only":
+ canvas.DrawPath(path, fillPaint);
+ break;
+
+ case "Stroke only":
+ canvas.DrawPath(path, strokePaint);
+ break;
+
+ case "Stroke then Fill":
+ canvas.DrawPath(path, strokePaint);
+ canvas.DrawPath(path, fillPaint);
+ break;
+
+ case "Fill then Stroke":
+ canvas.DrawPath(path, fillPaint);
+ canvas.DrawPath(path, strokePaint);
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/MultipleLinesPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/MultipleLinesPage.xaml
new file mode 100644
index 000000000..e1927b815
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/MultipleLinesPage.xaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/MultipleLinesPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/MultipleLinesPage.xaml.cs
new file mode 100644
index 000000000..00f507e25
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/MultipleLinesPage.xaml.cs
@@ -0,0 +1,55 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Paths;
+
+public partial class MultipleLinesPage : ContentPage
+{
+ public MultipleLinesPage()
+ {
+ InitializeComponent();
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Create an array of points scattered through the page
+ SKPoint[] points = new SKPoint[10];
+
+ for (int i = 0; i < 2; i++)
+ {
+ float x = (0.1f + 0.8f * i) * info.Width;
+
+ for (int j = 0; j < 5; j++)
+ {
+ float y = (0.1f + 0.2f * j) * info.Height;
+ points[2 * j + i] = new SKPoint(x, y);
+ }
+ }
+
+ SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.DarkOrchid,
+ StrokeWidth = 50,
+ StrokeCap = (SKStrokeCap)strokeCapPicker.SelectedItem
+ };
+
+ // Render the points by calling DrawPoints
+ SKPointMode pointMode = (SKPointMode)pointModePicker.SelectedItem;
+ canvas.DrawPoints(pointMode, points, paint);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/OverlappingCirclesPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/OverlappingCirclesPage.cs
new file mode 100644
index 000000000..4e0c23e57
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/OverlappingCirclesPage.cs
@@ -0,0 +1,55 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Paths
+{
+ public class OverlappingCirclesPage : ContentPage
+ {
+ public OverlappingCirclesPage()
+ {
+ Title = "Overlapping Circles";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPoint center = new SKPoint(info.Width / 2, info.Height / 2);
+ float radius = Math.Min(info.Width, info.Height) / 4;
+
+ SKPath path = new SKPath
+ {
+ FillType = SKPathFillType.EvenOdd
+ };
+
+ path.AddCircle(center.X - radius / 2, center.Y - radius / 2, radius);
+ path.AddCircle(center.X - radius / 2, center.Y + radius / 2, radius);
+ path.AddCircle(center.X + radius / 2, center.Y - radius / 2, radius);
+ path.AddCircle(center.X + radius / 2, center.Y + radius / 2, radius);
+
+ SKPaint paint = new SKPaint()
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Cyan
+ };
+
+ canvas.DrawPath(path, paint);
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 10;
+ paint.Color = SKColors.Magenta;
+
+ canvas.DrawPath(path, paint);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/PathsMenuPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/PathsMenuPage.xaml
new file mode 100644
index 000000000..9c27dc138
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/PathsMenuPage.xaml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/PathsMenuPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/PathsMenuPage.xaml.cs
new file mode 100644
index 000000000..4295eb2f5
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/PathsMenuPage.xaml.cs
@@ -0,0 +1,9 @@
+namespace SkiaSharpDemos.Paths;
+
+public partial class PathsMenuPage : BasePage
+{
+ public PathsMenuPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/StrokeCapsPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/StrokeCapsPage.cs
new file mode 100644
index 000000000..f7168ff1e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/StrokeCapsPage.cs
@@ -0,0 +1,69 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Paths
+{
+ public class StrokeCapsPage : ContentPage
+ {
+ public StrokeCapsPage()
+ {
+ Title = "Stroke Caps";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPaint textPaint = new SKPaint
+ {
+ Color = SKColors.Black,
+ TextSize = 75,
+ TextAlign = SKTextAlign.Center
+ };
+
+ SKPaint thickLinePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Orange,
+ StrokeWidth = 50
+ };
+
+ SKPaint thinLinePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 2
+ };
+
+ float xText = info.Width / 2;
+ float xLine1 = 100;
+ float xLine2 = info.Width - xLine1;
+ float y = textPaint.FontSpacing;
+
+ foreach (SKStrokeCap strokeCap in Enum.GetValues(typeof(SKStrokeCap)))
+ {
+ // Display text
+ canvas.DrawText(strokeCap.ToString(), xText, y, textPaint);
+ y += textPaint.FontSpacing;
+
+ // Display thick line
+ thickLinePaint.StrokeCap = strokeCap;
+ canvas.DrawLine(xLine1, y, xLine2, y, thickLinePaint);
+
+ // Display thin line
+ canvas.DrawLine(xLine1, y, xLine2, y, thinLinePaint);
+ y += 2 * textPaint.FontSpacing;
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/StrokeJoinsPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/StrokeJoinsPage.cs
new file mode 100644
index 000000000..6586d6417
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/StrokeJoinsPage.cs
@@ -0,0 +1,79 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Paths
+{
+ public class StrokeJoinsPage : ContentPage
+ {
+ public StrokeJoinsPage()
+ {
+ Title = "Stroke Joins";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPaint textPaint = new SKPaint
+ {
+ Color = SKColors.Black,
+ TextSize = 75,
+ TextAlign = SKTextAlign.Right
+ };
+
+ SKPaint thickLinePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Orange,
+ StrokeWidth = 50
+ };
+
+ SKPaint thinLinePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ StrokeWidth = 2
+ };
+
+ float xText = info.Width - 100;
+ float xLine1 = 100;
+ float xLine2 = info.Width - xLine1;
+ float y = 2 * textPaint.FontSpacing;
+ string[] strStrokeJoins = { "Miter", "Round", "Bevel" };
+
+ foreach (string strStrokeJoin in strStrokeJoins)
+ {
+ // Display text
+ canvas.DrawText(strStrokeJoin, xText, y, textPaint);
+
+ // Get stroke-join value
+ SKStrokeJoin strokeJoin;
+ Enum.TryParse(strStrokeJoin, out strokeJoin);
+
+ // Create path
+ SKPath path = new SKPath();
+ path.MoveTo(xLine1, y - 80);
+ path.LineTo(xLine1, y + 80);
+ path.LineTo(xLine2, y + 80);
+
+ // Display thick line
+ thickLinePaint.StrokeJoin = strokeJoin;
+ canvas.DrawPath(path, thickLinePaint);
+
+ // Display thin line
+ canvas.DrawPath(path, thinLinePaint);
+ y += 3 * textPaint.FontSpacing;
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/TwoTriangleContoursPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/TwoTriangleContoursPage.cs
new file mode 100644
index 000000000..5ef32fac1
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Paths/TwoTriangleContoursPage.cs
@@ -0,0 +1,61 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Paths
+{
+ public class TwoTriangleContoursPage : ContentPage
+ {
+ public TwoTriangleContoursPage()
+ {
+ Title = "Two Triangle Contours";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Create the path
+ SKPath path = new SKPath();
+
+ // Define the first contour
+ path.MoveTo(0.5f * info.Width, 0.1f * info.Height);
+ path.LineTo(0.2f * info.Width, 0.4f * info.Height);
+ path.LineTo(0.8f * info.Width, 0.4f * info.Height);
+ path.LineTo(0.5f * info.Width, 0.1f * info.Height);
+
+ // Define the second contour
+ path.MoveTo(0.5f * info.Width, 0.6f * info.Height);
+ path.LineTo(0.2f * info.Width, 0.9f * info.Height);
+ path.LineTo(0.8f * info.Width, 0.9f * info.Height);
+ path.Close();
+
+ // Create two SKPaint objects
+ SKPaint strokePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Magenta,
+ StrokeWidth = 50
+ };
+
+ SKPaint fillPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Cyan
+ };
+
+ // Fill and stroke the path
+ canvas.DrawPath(path, fillPaint);
+ canvas.DrawPath(path, strokePaint);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/AndroidManifest.xml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 000000000..b68b0c8bc
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/MainActivity.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/MainActivity.cs
new file mode 100644
index 000000000..592fc3a82
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/MainActivity.cs
@@ -0,0 +1,11 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace SkiaSharpDemos;
+
+[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
+public class MainActivity : MauiAppCompatActivity
+{
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/MainApplication.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/MainApplication.cs
new file mode 100644
index 000000000..12be5cb28
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/MainApplication.cs
@@ -0,0 +1,16 @@
+using Android.App;
+using Android.Runtime;
+
+namespace SkiaSharpDemos;
+
+[Application]
+public class MainApplication : MauiApplication
+{
+ public MainApplication(IntPtr handle, JniHandleOwnership ownership)
+ : base(handle, ownership)
+ {
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/PhotoLibrary.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/PhotoLibrary.cs
new file mode 100644
index 000000000..747f09dd8
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/PhotoLibrary.cs
@@ -0,0 +1,47 @@
+using Android.Media;
+using Java.IO;
+using Environment = Android.OS.Environment;
+using File = Java.IO.File;
+
+namespace SkiaSharpDemos
+{
+ public class PhotoLibrary
+ {
+ public async Task SavePhotoAsync(byte[] data, string folder, string filename)
+ {
+ try
+ {
+ File picturesDirectory = Environment.GetExternalStoragePublicDirectory(Environment.DirectoryPictures);
+ File folderDirectory = picturesDirectory;
+
+ if (!string.IsNullOrEmpty(folder))
+ {
+ folderDirectory = new File(picturesDirectory, folder);
+ folderDirectory.Mkdirs();
+ }
+
+ using (File bitmapFile = new File(folderDirectory, filename))
+ {
+ bitmapFile.CreateNewFile();
+
+ using (FileOutputStream outputStream = new FileOutputStream(bitmapFile))
+ {
+ await outputStream.WriteAsync(data);
+ }
+
+ // Make sure it shows up in the Photos gallery promptly.
+ MediaScannerConnection.ScanFile(Platform.AppContext,
+ new string[] { bitmapFile.Path },
+ new string[] { "image/png", "image/jpeg" }, null);
+ }
+ }
+ catch
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/Resources/values/colors.xml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 000000000..3b8595ecd
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #512BD4
+ #2B0B98
+ #2B0B98
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/AppDelegate.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/AppDelegate.cs
new file mode 100644
index 000000000..ab6a119cf
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace SkiaSharpDemos;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/Entitlements.plist b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/Entitlements.plist
new file mode 100644
index 000000000..8e87c0cb0
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/Entitlements.plist
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+ com.apple.security.network.client
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/Info.plist b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/Info.plist
new file mode 100644
index 000000000..f24aacc0d
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/Info.plist
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UIDeviceFamily
+
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/PhotoLibrary.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/PhotoLibrary.cs
new file mode 100644
index 000000000..047e789ad
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/PhotoLibrary.cs
@@ -0,0 +1,25 @@
+using Foundation;
+using UIKit;
+
+namespace SkiaSharpDemos
+{
+ public class PhotoLibrary
+ {
+ TaskCompletionSource taskCompletionSource;
+
+ public Task SavePhotoAsync(byte[] data, string folder, string filename)
+ {
+ NSData nsData = NSData.FromArray(data);
+ UIImage image = new UIImage(nsData);
+ TaskCompletionSource taskCompletionSource = new TaskCompletionSource();
+
+ image.SaveToPhotosAlbum((UIImage img, NSError error) =>
+ {
+ taskCompletionSource.SetResult(error == null);
+ });
+
+ return taskCompletionSource.Task;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/Program.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/Program.cs
new file mode 100644
index 000000000..c4ce23e19
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/MacCatalyst/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace SkiaSharpDemos;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Tizen/Main.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Tizen/Main.cs
new file mode 100644
index 000000000..5802f96e4
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Tizen/Main.cs
@@ -0,0 +1,17 @@
+using System;
+using Microsoft.Maui;
+using Microsoft.Maui.Hosting;
+
+namespace SkiaSharpDemos;
+
+class Program : MauiApplication
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+
+ static void Main(string[] args)
+ {
+ var app = new Program();
+ app.Run(args);
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Tizen/tizen-manifest.xml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Tizen/tizen-manifest.xml
new file mode 100644
index 000000000..9ac92d21a
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Tizen/tizen-manifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ maui-appicon-placeholder
+
+
+
+
+ http://tizen.org/privilege/internet
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/App.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/App.xaml
new file mode 100644
index 000000000..6aa402d3f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/App.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/App.xaml.cs
new file mode 100644
index 000000000..98657ab52
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/App.xaml.cs
@@ -0,0 +1,25 @@
+using Microsoft.UI.Xaml;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace SkiaSharpDemos.WinUI;
+
+///
+/// Provides application-specific behavior to supplement the default Application class.
+///
+public partial class App : MauiWinUIApplication
+{
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/Package.appxmanifest b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 000000000..c03e7a9cc
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+ $placeholder$
+ User Name
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/PhotoLibrary.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/PhotoLibrary.cs
new file mode 100644
index 000000000..466056540
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/PhotoLibrary.cs
@@ -0,0 +1,57 @@
+using System.Runtime.InteropServices.WindowsRuntime;
+
+using Windows.Storage;
+using Windows.Storage.Streams;
+
+namespace SkiaSharpDemos
+{
+ public class PhotoLibrary
+ {
+ public async Task SavePhotoAsync(byte[] data, string folder, string filename)
+ {
+ StorageFolder picturesDirectory = KnownFolders.PicturesLibrary;
+ StorageFolder folderDirectory = picturesDirectory;
+
+ // Get the folder or create it if necessary
+ if (!string.IsNullOrEmpty(folder))
+ {
+ try
+ {
+ folderDirectory = await picturesDirectory.GetFolderAsync(folder);
+ }
+ catch
+ { }
+
+ if (folderDirectory == null)
+ {
+ try
+ {
+ folderDirectory = await picturesDirectory.CreateFolderAsync(folder);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+
+ try
+ {
+ // Create the file.
+ StorageFile storageFile = await folderDirectory.CreateFileAsync(filename,
+ CreationCollisionOption.GenerateUniqueName);
+
+ // Convert byte[] to Windows buffer and write it out.
+ IBuffer buffer = WindowsRuntimeBuffer.Create(data, 0, data.Length, data.Length);
+ await FileIO.WriteBufferAsync(storageFile, buffer);
+ }
+ catch
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/app.manifest b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/app.manifest
new file mode 100644
index 000000000..5aa4d3d7c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/Windows/app.manifest
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/AppDelegate.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 000000000..ab6a119cf
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace SkiaSharpDemos;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/Info.plist b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/Info.plist
new file mode 100644
index 000000000..26196b997
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/Info.plist
@@ -0,0 +1,37 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+ NSPhotoLibraryUsageDescription
+ SkiaSharpDemos accesses your photo library
+
+ NSPhotoLibraryAddUsageDescription
+ SkiaSharpDemos adds images to your photo library
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/PhotoLibrary.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/PhotoLibrary.cs
new file mode 100644
index 000000000..047e789ad
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/PhotoLibrary.cs
@@ -0,0 +1,25 @@
+using Foundation;
+using UIKit;
+
+namespace SkiaSharpDemos
+{
+ public class PhotoLibrary
+ {
+ TaskCompletionSource taskCompletionSource;
+
+ public Task SavePhotoAsync(byte[] data, string folder, string filename)
+ {
+ NSData nsData = NSData.FromArray(data);
+ UIImage image = new UIImage(nsData);
+ TaskCompletionSource taskCompletionSource = new TaskCompletionSource();
+
+ image.SaveToPhotosAlbum((UIImage img, NSError error) =>
+ {
+ taskCompletionSource.SetResult(error == null);
+ });
+
+ return taskCompletionSource.Task;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/Program.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/Program.cs
new file mode 100644
index 000000000..c4ce23e19
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Platforms/iOS/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace SkiaSharpDemos;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Properties/launchSettings.json b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Properties/launchSettings.json
new file mode 100644
index 000000000..90f92d965
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "MsixPackage",
+ "nativeDebugging": false
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/AppIcon/appicon.svg b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/AppIcon/appicon.svg
new file mode 100644
index 000000000..49f980057
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/AppIcon/appicon.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/AppIcon/appiconfg.svg b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 000000000..e9b7139de
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Fonts/OpenSans-Regular.ttf b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 000000000..04a336a52
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Fonts/OpenSans-Semibold.ttf b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 000000000..1df30e6c0
Binary files /dev/null and b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Images/dotnet_bot.svg b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Images/dotnet_bot.svg
new file mode 100644
index 000000000..e19b01271
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Images/dotnet_bot.svg
@@ -0,0 +1,95 @@
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Raw/AboutAssets.txt b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Raw/AboutAssets.txt
new file mode 100644
index 000000000..29b76b2ea
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Raw/AboutAssets.txt
@@ -0,0 +1,18 @@
+Any raw assets you want to be deployed with your application can be placed in
+this directory (and child directories). Deployment of the asset to your application
+is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
+
+
+
+These files will be deployed with you package and will be accessible using Essentials:
+
+ async Task LoadMauiAsset()
+ {
+ using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
+ using var reader = new StreamReader(stream);
+
+ var contents = reader.ReadToEnd();
+ }
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Splash/splash.svg b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Splash/splash.svg
new file mode 100644
index 000000000..4b713836f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Splash/splash.svg
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Styles/Colors.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Styles/Colors.xaml
new file mode 100644
index 000000000..66867878f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Styles/Colors.xaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ #512BD4
+ #DFD8F7
+ #2B0B98
+ White
+ Black
+ #E1E1E1
+ #C8C8C8
+ #ACACAC
+ #919191
+ #6E6E6E
+ #404040
+ #212121
+ #141414
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #F7B548
+ #FFD590
+ #FFE5B9
+ #28C2D1
+ #7BDDEF
+ #C3F2F4
+ #3E8EED
+ #72ACF1
+ #A7CBF6
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Styles/Styles.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Styles/Styles.xaml
new file mode 100644
index 000000000..d45ca5bad
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Resources/Styles/Styles.xaml
@@ -0,0 +1,409 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/SkiaSharpDemos.csproj b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/SkiaSharpDemos.csproj
new file mode 100644
index 000000000..b5bb0b205
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/SkiaSharpDemos.csproj
@@ -0,0 +1,81 @@
+
+
+
+ net8.0-android;net8.0-ios;net8.0-maccatalyst
+ $(TargetFrameworks);net8.0-windows10.0.19041.0
+
+
+
+
+
+
+ Exe
+ SkiaSharpDemos
+ true
+ true
+ enable
+ enable
+ true
+
+
+ SkiaSharpDemos
+
+
+ com.companyname.skiasharpdemos
+
+
+ 1.0
+ 1
+
+ 11.0
+ 13.1
+ 21.0
+ 10.0.17763.0
+ 10.0.17763.0
+ 6.5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/TouchPoint.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/TouchPoint.cs
new file mode 100644
index 000000000..730a8bc71
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/TouchPoint.cs
@@ -0,0 +1,85 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos
+{
+ public class TouchPoint
+ {
+ // For painting
+ SKPaint paint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill
+ };
+
+ // For dragging
+ bool isBeingDragged;
+ long touchId;
+ SKPoint previousPoint;
+
+ public SKPoint Center { get; set; }
+ public float Radius { get; set; } = 75;
+ public SKColor Color { get; set; } = new SKColor(0, 0, 255, 64);
+
+ public TouchPoint()
+ {
+ }
+
+ public TouchPoint(float x, float y)
+ {
+ Center = new SKPoint(x, y);
+ }
+
+ public void Paint(SKCanvas canvas)
+ {
+ paint.Color = Color;
+ canvas.DrawCircle(Center.X, Center.Y, Radius, paint);
+ }
+
+ public bool ProcessTouchEvent(long id, SKTouchAction type, SKPoint location)
+ {
+ bool centerMoved = false;
+
+ switch (type)
+ {
+ case SKTouchAction.Pressed:
+ if (!isBeingDragged && PointInCircle(location))
+ {
+ isBeingDragged = true;
+ touchId = id;
+ previousPoint = location;
+ centerMoved = false;
+ }
+ break;
+
+ case SKTouchAction.Moved:
+ if (isBeingDragged && touchId == id)
+ {
+ Center += location - previousPoint;
+ previousPoint = location;
+ centerMoved = true;
+ }
+ break;
+
+ case SKTouchAction.Released:
+ if (isBeingDragged && touchId == id)
+ {
+ Center += location - previousPoint;
+ isBeingDragged = false;
+ centerMoved = true;
+ }
+ break;
+
+ case SKTouchAction.Cancelled:
+ isBeingDragged = false;
+ break;
+ }
+ return centerMoved;
+ }
+
+ bool PointInCircle(SKPoint pt)
+ {
+ return (Math.Pow(pt.X - Center.X, 2) + Math.Pow(pt.Y - Center.Y, 2)) < (Radius * Radius);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AccumulatedTranslatePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AccumulatedTranslatePage.cs
new file mode 100644
index 000000000..7b180651f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AccumulatedTranslatePage.cs
@@ -0,0 +1,46 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class AccumulatedTranslatePage : ContentPage
+ {
+ public AccumulatedTranslatePage()
+ {
+ Title = "Accumulated Translate";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint strokePaint = new SKPaint())
+ {
+ strokePaint.Color = SKColors.Black;
+ strokePaint.Style = SKPaintStyle.Stroke;
+ strokePaint.StrokeWidth = 3;
+
+ int rectangleCount = 20;
+ SKRect rect = new SKRect(0, 0, 250, 250);
+ float xTranslate = (info.Width - rect.Width) / (rectangleCount - 1);
+ float yTranslate = (info.Height - rect.Height) / (rectangleCount - 1);
+
+ for (int i = 0; i < rectangleCount; i++)
+ {
+ canvas.DrawRect(rect, strokePaint);
+ canvas.Translate(xTranslate, yTranslate);
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AnimatedRotation3DPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AnimatedRotation3DPage.cs
new file mode 100644
index 000000000..54bbf1bd5
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AnimatedRotation3DPage.cs
@@ -0,0 +1,103 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class AnimatedRotation3DPage : ContentPage
+ {
+ SKCanvasView canvasView;
+ float xRotationDegrees, yRotationDegrees, zRotationDegrees;
+ string text = "SkiaSharp";
+ SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Black,
+ TextSize = 100,
+ StrokeWidth = 3,
+ };
+ SKRect textBounds;
+
+ public AnimatedRotation3DPage()
+ {
+ Title = "Animated Rotation 3D";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ // Measure the text
+ textPaint.MeasureText(text, ref textBounds);
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+
+ new Animation((value) => xRotationDegrees = 360 * (float)value).
+ Commit(this, "xRotationAnimation", length: 5000, repeat: () => true);
+
+ new Animation((value) => yRotationDegrees = 360 * (float)value).
+ Commit(this, "yRotationAnimation", length: 7000, repeat: () => true);
+
+ new Animation((value) =>
+ {
+ zRotationDegrees = 360 * (float)value;
+ canvasView.InvalidateSurface();
+ }).Commit(this, "zRotationAnimation", length: 11000, repeat: () => true);
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ this.AbortAnimation("xRotationAnimation");
+ this.AbortAnimation("yRotationAnimation");
+ this.AbortAnimation("zRotationAnimation");
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Find center of canvas
+ float xCenter = info.Width / 2;
+ float yCenter = info.Height / 2;
+
+ // Translate center to origin
+ SKMatrix matrix = SKMatrix.CreateTranslation(-xCenter, -yCenter);
+
+ // Scale so text fits
+ float scale = Math.Min(info.Width / textBounds.Width, info.Height / textBounds.Height);
+ matrix = matrix.PostConcat(SKMatrix.CreateScale(scale, scale));
+
+ // Calculate composite 3D transforms
+ float depth = 0.75f * scale * textBounds.Width;
+
+ SKMatrix44 matrix44 = SKMatrix44.CreateIdentity();
+ matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(1, 0, 0, xRotationDegrees));
+ matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(0, 1, 0, yRotationDegrees));
+ matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(0, 0, 1, zRotationDegrees));
+
+ SKMatrix44 perspectiveMatrix = SKMatrix44.CreateIdentity();
+ perspectiveMatrix[3, 2] = -1 / depth;
+ matrix44.PostConcat(perspectiveMatrix);
+
+ // Concatenate with 2D matrix
+ matrix = matrix.PostConcat(matrix44.Matrix);
+
+ // Translate back to center
+ matrix = matrix.PostConcat(SKMatrix.CreateTranslation(xCenter, yCenter));
+
+ // Set the matrix and display the text
+ canvas.SetMatrix(matrix);
+ float xText = xCenter - textBounds.MidX;
+ float yText = yCenter - textBounds.MidY;
+ canvas.DrawText(text, xText, yText, textPaint);
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AnisotropicScalingPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AnisotropicScalingPage.cs
new file mode 100644
index 000000000..a2f1acb4e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AnisotropicScalingPage.cs
@@ -0,0 +1,52 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class AnisotropicScalingPage : ContentPage
+ {
+ public AnisotropicScalingPage()
+ {
+ Title = "Anisotropic Scaling";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPath path = HendecagramArrayPage.HendecagramPath;
+ SKRect pathBounds = path.Bounds;
+
+ using (SKPaint fillPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Pink
+ })
+ using (SKPaint strokePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Blue,
+ StrokeWidth = 3,
+ StrokeJoin = SKStrokeJoin.Round
+ })
+ {
+ canvas.Scale(info.Width / pathBounds.Width,
+ info.Height / pathBounds.Height);
+ canvas.Translate(-pathBounds.Left, -pathBounds.Top);
+
+ canvas.DrawPath(path, fillPaint);
+ canvas.DrawPath(path, strokePaint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AnisotropicTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AnisotropicTextPage.cs
new file mode 100644
index 000000000..1311fadda
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/AnisotropicTextPage.cs
@@ -0,0 +1,50 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class AnisotropicTextPage : ContentPage
+ {
+ public AnisotropicTextPage()
+ {
+ Title = "Anisotropic Text";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Blue,
+ StrokeWidth = 0.1f,
+ StrokeJoin = SKStrokeJoin.Round
+ })
+ {
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText("HELLO", ref textBounds);
+
+ // Inflate bounds by the stroke width
+ textBounds.Inflate(textPaint.StrokeWidth / 2,
+ textPaint.StrokeWidth / 2);
+
+ canvas.Scale(info.Width / textBounds.Width,
+ info.Height / textBounds.Height);
+ canvas.Translate(-textBounds.Left, -textBounds.Top);
+
+ canvas.DrawText("HELLO", 0, 0, textPaint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicRotatePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicRotatePage.xaml
new file mode 100644
index 000000000..18aefb254
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicRotatePage.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicRotatePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicRotatePage.xaml.cs
new file mode 100644
index 000000000..f5f3d273e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicRotatePage.xaml.cs
@@ -0,0 +1,41 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class BasicRotatePage : ContentPage
+{
+ public BasicRotatePage()
+ {
+ InitializeComponent();
+ }
+
+ void sliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Blue,
+ TextAlign = SKTextAlign.Center,
+ TextSize = 100
+ })
+ {
+ canvas.RotateDegrees((float)rotateSlider.Value);
+ canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicScalePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicScalePage.xaml
new file mode 100644
index 000000000..136fdf2fa
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicScalePage.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicScalePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicScalePage.xaml.cs
new file mode 100644
index 000000000..d9edbb699
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BasicScalePage.xaml.cs
@@ -0,0 +1,58 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class BasicScalePage : ContentPage
+{
+ public BasicScalePage()
+ {
+ InitializeComponent();
+
+ xScaleSlider.Value = 1;
+ yScaleSlider.Value = 1;
+ }
+
+ void sliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint strokePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Red,
+ StrokeWidth = 3,
+ PathEffect = SKPathEffect.CreateDash(new float[] { 7, 7 }, 0)
+ })
+ using (SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Blue,
+ TextSize = 50
+ })
+ {
+ canvas.Scale((float)xScaleSlider.Value,
+ (float)yScaleSlider.Value);
+
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText(Title, ref textBounds);
+
+ float margin = 10;
+ SKRect borderRect = SKRect.Create(new SKPoint(margin, margin), textBounds.Size);
+ canvas.DrawRoundRect(borderRect, 20, 20, strokePaint);
+ canvas.DrawText(Title, margin, -textBounds.Top + margin, textPaint);
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BitmapScatterViewPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BitmapScatterViewPage.xaml
new file mode 100644
index 000000000..a5a88a5e2
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BitmapScatterViewPage.xaml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BitmapScatterViewPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BitmapScatterViewPage.xaml.cs
new file mode 100644
index 000000000..700adb868
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/BitmapScatterViewPage.xaml.cs
@@ -0,0 +1,102 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class BitmapScatterViewPage : ContentPage
+{
+ List bitmapCollection = new List();
+
+ Dictionary bitmapDictionary = new Dictionary();
+
+ public BitmapScatterViewPage()
+ {
+ InitializeComponent();
+
+ // Load in all the available bitmaps
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+ string[] resourceIDs = assembly.GetManifestResourceNames();
+ SKPoint position = new SKPoint();
+
+ foreach (string resourceID in resourceIDs)
+ {
+ if (resourceID.EndsWith(".png") ||
+ resourceID.EndsWith(".jpg"))
+ {
+ using (Stream stream = assembly.GetManifestResourceStream(resourceID))
+ {
+ SKBitmap bitmap = SKBitmap.Decode(stream);
+ bitmapCollection.Add(new TouchManipulationBitmap(bitmap)
+ {
+ Matrix = SKMatrix.CreateTranslation(position.X, position.Y),
+ });
+ position.X += 100;
+ position.Y += 100;
+ }
+ }
+ }
+ }
+
+ void OnTouch(object sender, SKTouchEventArgs e)
+ {
+ // Convert point to pixels
+ SKPoint point = new SKPoint((float)(canvasView.CanvasSize.Width * e.Location.X / canvasView.Width), (float)(canvasView.CanvasSize.Height * e.Location.Y / canvasView.Height));
+
+ switch (e.ActionType)
+ {
+ case SKTouchAction.Pressed:
+ for (int i = bitmapCollection.Count - 1; i >= 0; i--)
+ {
+ TouchManipulationBitmap bitmap = bitmapCollection[i];
+
+ if (bitmap.HitTest(point))
+ {
+ // Move bitmap to end of collection
+ bitmapCollection.Remove(bitmap);
+ bitmapCollection.Add(bitmap);
+
+ // Do the touch processing
+ bitmapDictionary.Add(e.Id, bitmap);
+ bitmap.ProcessTouchEvent(e.Id, e.ActionType, point);
+ canvasView.InvalidateSurface();
+ break;
+ }
+ }
+ break;
+
+ case SKTouchAction.Moved:
+ if (bitmapDictionary.ContainsKey(e.Id))
+ {
+ TouchManipulationBitmap bitmap = bitmapDictionary[e.Id];
+ bitmap.ProcessTouchEvent(e.Id, e.ActionType, point);
+ canvasView.InvalidateSurface();
+ }
+ break;
+
+ case SKTouchAction.Released:
+ case SKTouchAction.Cancelled:
+ if (bitmapDictionary.ContainsKey(e.Id))
+ {
+ TouchManipulationBitmap bitmap = bitmapDictionary[e.Id];
+ bitmap.ProcessTouchEvent(e.Id, e.ActionType, point);
+ bitmapDictionary.Remove(e.Id);
+ canvasView.InvalidateSurface();
+ }
+ break;
+ }
+
+ e.Handled = true;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKCanvas canvas = args.Surface.Canvas;
+ canvas.Clear();
+
+ foreach (TouchManipulationBitmap bitmap in bitmapCollection)
+ {
+ bitmap.Paint(canvas);
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredRotatePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredRotatePage.xaml
new file mode 100644
index 000000000..5c4ed23ab
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredRotatePage.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredRotatePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredRotatePage.xaml.cs
new file mode 100644
index 000000000..158c468d2
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredRotatePage.xaml.cs
@@ -0,0 +1,42 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class CenteredRotatePage : ContentPage
+{
+ public CenteredRotatePage()
+ {
+ InitializeComponent();
+ }
+
+ void sliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Blue,
+ TextAlign = SKTextAlign.Center,
+ TextSize = 100
+ })
+ {
+ canvas.RotateDegrees((float)rotateSlider.Value, info.Width / 2, info.Height / 2);
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.DrawText(Title, 0, 0, textPaint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredScalePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredScalePage.xaml
new file mode 100644
index 000000000..89b574aea
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredScalePage.xaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredScalePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredScalePage.xaml.cs
new file mode 100644
index 000000000..7020be11b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/CenteredScalePage.xaml.cs
@@ -0,0 +1,62 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class CenteredScalePage : ContentPage
+{
+ public CenteredScalePage()
+ {
+ InitializeComponent();
+
+ xScaleSlider.Value = 1;
+ yScaleSlider.Value = 1;
+ }
+
+ void sliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint strokePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Red,
+ StrokeWidth = 3,
+ PathEffect = SKPathEffect.CreateDash(new float[] { 7, 7 }, 0)
+ })
+ using (SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Blue,
+ TextSize = 50
+ })
+ {
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText(Title, ref textBounds);
+ float margin = (info.Width - textBounds.Width) / 2;
+
+ float sx = (float)xScaleSlider.Value;
+ float sy = (float)yScaleSlider.Value;
+ float px = margin + textBounds.Width / 2;
+ float py = margin + textBounds.Height / 2;
+
+ canvas.Scale(sx, sy, px, py);
+
+ SKRect borderRect = SKRect.Create(new SKPoint(margin, margin), textBounds.Size);
+ canvas.DrawRoundRect(borderRect, 20, 20, strokePaint);
+ canvas.DrawText(Title, margin, -textBounds.Top + margin, textPaint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/HendecagramAnimationPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/HendecagramAnimationPage.cs
new file mode 100644
index 000000000..f2716c707
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/HendecagramAnimationPage.cs
@@ -0,0 +1,76 @@
+using System.Diagnostics;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class HendecagramAnimationPage : ContentPage
+ {
+ const double cycleTime = 5000; // in milliseconds
+
+ SKCanvasView canvasView;
+ Stopwatch stopwatch = new Stopwatch();
+ bool pageIsActive;
+ float angle;
+
+ public HendecagramAnimationPage()
+ {
+ Title = "Hedecagram Animation";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ pageIsActive = true;
+ stopwatch.Start();
+
+ Dispatcher.StartTimer(TimeSpan.FromMilliseconds(33), () =>
+ {
+ double t = stopwatch.Elapsed.TotalMilliseconds % cycleTime / cycleTime;
+ angle = (float)(360 * t);
+ canvasView.InvalidateSurface();
+
+ if (!pageIsActive)
+ {
+ stopwatch.Stop();
+ }
+
+ return pageIsActive;
+ });
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ pageIsActive = false;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ float radius = (float)Math.Min(info.Width, info.Height) / 2 - 100;
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = SKColor.FromHsl(angle, 100, 50);
+
+ float x = radius * (float)Math.Sin(Math.PI * angle / 180);
+ float y = -radius * (float)Math.Cos(Math.PI * angle / 180);
+ canvas.Translate(x, y);
+ canvas.DrawPath(HendecagramArrayPage.HendecagramPath, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/HendecagramArrayPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/HendecagramArrayPage.cs
new file mode 100644
index 000000000..dac556358
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/HendecagramArrayPage.cs
@@ -0,0 +1,69 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class HendecagramArrayPage : ContentPage
+ {
+ Random random = new Random();
+ public static readonly SKPath HendecagramPath;
+
+ static HendecagramArrayPage()
+ {
+ // Create 11-pointed star
+ HendecagramPath = new SKPath();
+ for (int i = 0; i < 11; i++)
+ {
+ double angle = 5 * i * 2 * Math.PI / 11;
+ SKPoint pt = new SKPoint(100 * (float)Math.Sin(angle),
+ -100 * (float)Math.Cos(angle));
+ if (i == 0)
+ {
+ HendecagramPath.MoveTo(pt);
+ }
+ else
+ {
+ HendecagramPath.LineTo(pt);
+ }
+ }
+ HendecagramPath.Close();
+ }
+
+ public HendecagramArrayPage()
+ {
+ Title = "Hendecagram";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ for (int x = 100; x < info.Width + 100; x += 200)
+ for (int y = 100; y < info.Height + 100; y += 200)
+ {
+ // Set random color
+ byte[] bytes = new byte[3];
+ random.NextBytes(bytes);
+ paint.Color = new SKColor(bytes[0], bytes[1], bytes[2]);
+
+ // Display the hendecagram
+ canvas.Save();
+ canvas.Translate(x, y);
+ canvas.DrawPath(HendecagramPath, paint);
+ canvas.Restore();
+ }
+ }
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/IsotropicScalingPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/IsotropicScalingPage.cs
new file mode 100644
index 000000000..2c6b3815f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/IsotropicScalingPage.cs
@@ -0,0 +1,54 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class IsotropicScalingPage : ContentPage
+ {
+ public IsotropicScalingPage()
+ {
+ Title = "Isotropic Scaling";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ SKPath path = HendecagramArrayPage.HendecagramPath;
+ SKRect pathBounds = path.Bounds;
+
+ using (SKPaint fillPaint = new SKPaint())
+ {
+ fillPaint.Style = SKPaintStyle.Fill;
+
+ float scale = Math.Min(info.Width / pathBounds.Width,
+ info.Height / pathBounds.Height);
+
+ for (int i = 0; i <= 10; i++)
+ {
+ fillPaint.Color = new SKColor((byte)(255 * (10 - i) / 10),
+ 0,
+ (byte)(255 * i / 10));
+ canvas.Save();
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ canvas.Scale(scale);
+ canvas.Translate(-pathBounds.MidX, -pathBounds.MidY);
+ canvas.DrawPath(path, fillPaint);
+ canvas.Restore();
+
+ scale *= 0.9f;
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ObliqueTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ObliqueTextPage.cs
new file mode 100644
index 000000000..8a3d27cfe
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ObliqueTextPage.cs
@@ -0,0 +1,47 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class ObliqueTextPage : ContentPage
+ {
+ public ObliqueTextPage()
+ {
+ Title = "Oblique Text";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint textPaint = new SKPaint()
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Maroon,
+ TextAlign = SKTextAlign.Center,
+ TextSize = info.Width / 8 // empirically determined
+ })
+ {
+ canvas.Translate(info.Width / 2, info.Height / 2);
+ SkewDegrees(canvas, -20, 0);
+ canvas.DrawText(Title, 0, 0, textPaint);
+ }
+ }
+
+ void SkewDegrees(SKCanvas canvas, double xDegrees, double yDegrees)
+ {
+ canvas.Skew((float)Math.Tan(Math.PI * xDegrees / 180),
+ (float)Math.Tan(Math.PI * yDegrees / 180));
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/PathTransformPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/PathTransformPage.cs
new file mode 100644
index 000000000..0ed7ce0b4
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/PathTransformPage.cs
@@ -0,0 +1,45 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class PathTransformPage : ContentPage
+ {
+ SKPath transformedPath = HendecagramArrayPage.HendecagramPath;
+
+ public PathTransformPage()
+ {
+ Title = "Path Transform";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+
+ SKMatrix matrix = SKMatrix.CreateScale(3, 3);
+ matrix = matrix.PostConcat(SKMatrix.CreateRotationDegrees(360f / 22));
+ matrix = matrix.PostConcat(SKMatrix.CreateTranslation(300, 300));
+
+ transformedPath.Transform(matrix);
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint paint = new SKPaint())
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.Magenta;
+ paint.StrokeWidth = 5;
+
+ canvas.DrawPath(transformedPath, paint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/RotateAndRevolvePage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/RotateAndRevolvePage.cs
new file mode 100644
index 000000000..1185a803b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/RotateAndRevolvePage.cs
@@ -0,0 +1,75 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class RotateAndRevolvePage : ContentPage
+ {
+ SKCanvasView canvasView;
+ float revolveDegrees, rotateDegrees;
+
+ public RotateAndRevolvePage()
+ {
+ Title = "Rotate and Revolve";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+
+ new Animation((value) => revolveDegrees = 360 * (float)value).
+ Commit(this, "revolveAnimation", length: 10000, repeat: () => true);
+
+ new Animation((value) =>
+ {
+ rotateDegrees = 360 * (float)value;
+ canvasView.InvalidateSurface();
+ }).Commit(this, "rotateAnimation", length: 1000, repeat: () => true);
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ this.AbortAnimation("revolveAnimation");
+ this.AbortAnimation("rotateAnimation");
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint fillPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Red
+ })
+ {
+ // Translate to center of canvas
+ canvas.Translate(info.Width / 2, info.Height / 2);
+
+ // Rotate around center of canvas
+ canvas.RotateDegrees(revolveDegrees);
+
+ // Translate horizontally
+ float radius = Math.Min(info.Width, info.Height) / 3;
+ canvas.Translate(radius, 0);
+
+ // Rotate around center of object
+ canvas.RotateDegrees(rotateDegrees);
+
+ // Draw a square
+ canvas.DrawRect(new SKRect(-50, -50, 50, 50), fillPaint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/RotatedTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/RotatedTextPage.cs
new file mode 100644
index 000000000..9cb409782
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/RotatedTextPage.cs
@@ -0,0 +1,52 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class RotatedTextPage : ContentPage
+ {
+ static readonly string text = " ROTATE";
+
+ public RotatedTextPage()
+ {
+ Title = "Rotated Text";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint textPaint = new SKPaint
+ {
+ Color = SKColors.Black,
+ TextSize = 72
+ })
+ {
+ float xCenter = info.Width / 2;
+ float yCenter = info.Height / 2;
+
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText(text, ref textBounds);
+ float yText = yCenter - textBounds.Height / 2 - textBounds.Top;
+
+ for (int degrees = 0; degrees < 360; degrees += 30)
+ {
+ canvas.Save();
+ canvas.RotateDegrees(degrees, xCenter, yCenter);
+ canvas.DrawText(text, xCenter, yText, textPaint);
+ canvas.Restore();
+ }
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/Rotation3DPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/Rotation3DPage.xaml
new file mode 100644
index 000000000..9f26326eb
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/Rotation3DPage.xaml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/Rotation3DPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/Rotation3DPage.xaml.cs
new file mode 100644
index 000000000..63eddd288
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/Rotation3DPage.xaml.cs
@@ -0,0 +1,67 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class Rotation3DPage : ContentPage
+{
+ SKBitmap bitmap;
+
+ public Rotation3DPage()
+ {
+ InitializeComponent();
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.seatedmonkey.jpg"))
+ {
+ bitmap = SKBitmap.Decode(stream);
+ }
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Find center of canvas
+ float xCenter = info.Width / 2;
+ float yCenter = info.Height / 2;
+
+ // Translate center to origin
+ SKMatrix matrix = SKMatrix.CreateTranslation(-xCenter, -yCenter);
+
+ // Use 3D matrix for 3D rotations and perspective
+ SKMatrix44 matrix44 = SKMatrix44.CreateIdentity();
+ matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(1, 0, 0, (float)xRotateSlider.Value));
+ matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(0, 1, 0, (float)yRotateSlider.Value));
+ matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(0, 0, 1, (float)zRotateSlider.Value));
+
+ SKMatrix44 perspectiveMatrix = SKMatrix44.CreateIdentity();
+ perspectiveMatrix[3, 2] = -1 / (float)depthSlider.Value;
+ matrix44.PostConcat(perspectiveMatrix);
+
+ // Concatenate with 2D matrix
+ matrix = matrix.PostConcat(matrix44.Matrix);
+
+ // Translate back to center
+ matrix = matrix.PostConcat(SKMatrix.CreateTranslation(xCenter, yCenter));
+
+ // Set the matrix and display the bitmap
+ canvas.SetMatrix(matrix);
+ float xBitmap = xCenter - bitmap.Width / 2;
+ float yBitmap = yCenter - bitmap.Height / 2;
+ canvas.DrawBitmap(bitmap, xBitmap, yBitmap);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowAffineMatrixPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowAffineMatrixPage.xaml
new file mode 100644
index 000000000..1105d62bf
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowAffineMatrixPage.xaml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowAffineMatrixPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowAffineMatrixPage.xaml.cs
new file mode 100644
index 000000000..24c259a09
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowAffineMatrixPage.xaml.cs
@@ -0,0 +1,107 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class ShowAffineMatrixPage : ContentPage
+{
+ SKMatrix matrix;
+ SKBitmap bitmap;
+ SKSize bitmapSize;
+
+ TouchPoint[] touchPoints = new TouchPoint[3];
+
+ MatrixDisplay matrixDisplay = new MatrixDisplay();
+
+ public ShowAffineMatrixPage()
+ {
+ InitializeComponent();
+
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.seatedmonkey.jpg"))
+ {
+ bitmap = SKBitmap.Decode(stream);
+ }
+
+ touchPoints[0] = new TouchPoint(100, 100); // upper-left corner
+ touchPoints[1] = new TouchPoint(bitmap.Width, 100); // upper-right corner
+ touchPoints[2] = new TouchPoint(100, bitmap.Height + 100); // lower-left corner
+
+ bitmapSize = new SKSize(bitmap.Width, bitmap.Height);
+ matrix = ComputeMatrix(bitmapSize, touchPoints[0].Center,
+ touchPoints[1].Center,
+ touchPoints[2].Center);
+ }
+
+ void OnTouch(object sender, SKTouchEventArgs e)
+ {
+ bool touchPointMoved = false;
+
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ float scale = canvasView.CanvasSize.Width / (float)canvasView.Width;
+ SKPoint point = new SKPoint(scale * (float)e.Location.X,
+ scale * (float)e.Location.Y);
+ touchPointMoved |= touchPoint.ProcessTouchEvent(e.Id, e.ActionType, point);
+ }
+
+ if (touchPointMoved)
+ {
+ matrix = ComputeMatrix(bitmapSize, touchPoints[0].Center,
+ touchPoints[1].Center,
+ touchPoints[2].Center);
+ canvasView.InvalidateSurface();
+ }
+
+ e.Handled = true;
+ }
+
+ static SKMatrix ComputeMatrix(SKSize size, SKPoint ptUL, SKPoint ptUR, SKPoint ptLL)
+ {
+ // Scale transform
+ SKMatrix S = SKMatrix.CreateScale(1 / size.Width, 1 / size.Height);
+
+ // Affine transform
+ SKMatrix A = new SKMatrix
+ {
+ ScaleX = ptUR.X - ptUL.X,
+ SkewY = ptUR.Y - ptUL.Y,
+ SkewX = ptLL.X - ptUL.X,
+ ScaleY = ptLL.Y - ptUL.Y,
+ TransX = ptUL.X,
+ TransY = ptUL.Y,
+ Persp2 = 1
+ };
+
+ SKMatrix result = SKMatrix.CreateIdentity();
+ SKMatrix.Concat(ref result, A, S);
+ return result;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Display the bitmap using the matrix
+ canvas.Save();
+ canvas.SetMatrix(matrix);
+ canvas.DrawBitmap(bitmap, 0, 0);
+ canvas.Restore();
+
+ // Display the matrix in the lower-right corner
+ SKSize matrixSize = matrixDisplay.Measure(matrix);
+
+ matrixDisplay.Paint(canvas, matrix, new SKPoint(info.Width - matrixSize.Width, info.Height - matrixSize.Height));
+
+ // Display the touchpoints
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ touchPoint.Paint(canvas);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowNonAffineMatrixPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowNonAffineMatrixPage.xaml
new file mode 100644
index 000000000..626f0c2e2
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowNonAffineMatrixPage.xaml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowNonAffineMatrixPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowNonAffineMatrixPage.xaml.cs
new file mode 100644
index 000000000..59a7a099b
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/ShowNonAffineMatrixPage.xaml.cs
@@ -0,0 +1,134 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class ShowNonAffineMatrixPage : ContentPage
+{
+ SKMatrix matrix;
+ SKBitmap bitmap;
+ SKSize bitmapSize;
+
+ TouchPoint[] touchPoints = new TouchPoint[4];
+
+ MatrixDisplay matrixDisplay = new MatrixDisplay
+ {
+ PerspectiveFormat = "F5"
+ };
+
+ public ShowNonAffineMatrixPage()
+ {
+ InitializeComponent();
+
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.banana.jpg"))
+ {
+ bitmap = SKBitmap.Decode(stream);
+ }
+
+ touchPoints[0] = new TouchPoint(100, 100); // upper-left corner
+ touchPoints[1] = new TouchPoint(bitmap.Width, 100); // upper-right corner
+ touchPoints[2] = new TouchPoint(100, bitmap.Height + 100); // lower-left corner
+ touchPoints[3] = new TouchPoint(bitmap.Width, bitmap.Height + 100); // lower-right corner
+
+ bitmapSize = new SKSize(bitmap.Width, bitmap.Height);
+ matrix = ComputeMatrix(bitmapSize, touchPoints[0].Center, touchPoints[1].Center,
+ touchPoints[2].Center, touchPoints[3].Center);
+ }
+
+ void OnTouch(object sender, SKTouchEventArgs e)
+ {
+ bool touchPointMoved = false;
+
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ float scale = canvasView.CanvasSize.Width / (float)canvasView.Width;
+ SKPoint point = new SKPoint(scale * (float)e.Location.X,
+ scale * (float)e.Location.Y);
+ touchPointMoved |= touchPoint.ProcessTouchEvent(e.Id, e.ActionType, point);
+ }
+
+ if (touchPointMoved)
+ {
+ matrix = ComputeMatrix(bitmapSize, touchPoints[0].Center, touchPoints[1].Center,
+ touchPoints[2].Center, touchPoints[3].Center);
+ canvasView.InvalidateSurface();
+ }
+
+ e.Handled = true;
+ }
+
+ static SKMatrix ComputeMatrix(SKSize size, SKPoint ptUL, SKPoint ptUR, SKPoint ptLL, SKPoint ptLR)
+ {
+ // Scale transform
+ SKMatrix S = SKMatrix.CreateScale(1 / size.Width, 1 / size.Height);
+
+ // Affine transform
+ SKMatrix A = new SKMatrix
+ {
+ ScaleX = ptUR.X - ptUL.X,
+ SkewY = ptUR.Y - ptUL.Y,
+ SkewX = ptLL.X - ptUL.X,
+ ScaleY = ptLL.Y - ptUL.Y,
+ TransX = ptUL.X,
+ TransY = ptUL.Y,
+ Persp2 = 1
+ };
+
+ // Non-Affine transform
+ SKMatrix inverseA;
+ A.TryInvert(out inverseA);
+ SKPoint abPoint = inverseA.MapPoint(ptLR);
+ float a = abPoint.X;
+ float b = abPoint.Y;
+
+ float scaleX = a / (a + b - 1);
+ float scaleY = b / (a + b - 1);
+
+ SKMatrix N = new SKMatrix
+ {
+ ScaleX = scaleX,
+ ScaleY = scaleY,
+ Persp0 = scaleX - 1,
+ Persp1 = scaleY - 1,
+ Persp2 = 1
+ };
+
+ // Multiply S * N * A
+ SKMatrix result = SKMatrix.CreateIdentity();
+ result = result.PostConcat(S);
+ result = result.PostConcat(N);
+ result = result.PostConcat(A);
+
+ return result;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Display the bitmap using the matrix
+ canvas.Save();
+ canvas.SetMatrix(matrix);
+ canvas.DrawBitmap(bitmap, 0, 0);
+ canvas.Restore();
+
+ // Display the matrix in the lower-right corner
+ SKSize matrixSize = matrixDisplay.Measure(matrix);
+
+ matrixDisplay.Paint(canvas, matrix,
+ new SKPoint(info.Width - matrixSize.Width,
+ info.Height - matrixSize.Height));
+
+ // Display the touchpoints
+ foreach (TouchPoint touchPoint in touchPoints)
+ {
+ touchPoint.Paint(canvas);
+ }
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SingleFingerCornerScalePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SingleFingerCornerScalePage.xaml
new file mode 100644
index 000000000..7e07c190d
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SingleFingerCornerScalePage.xaml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SingleFingerCornerScalePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SingleFingerCornerScalePage.xaml.cs
new file mode 100644
index 000000000..2162b8a4d
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SingleFingerCornerScalePage.xaml.cs
@@ -0,0 +1,116 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class SingleFingerCornerScalePage : ContentPage
+{
+ SKBitmap bitmap;
+ SKMatrix currentMatrix = SKMatrix.CreateIdentity();
+
+ // Information for translating and scaling
+ long? touchId = null;
+ SKPoint pressedLocation;
+ SKMatrix pressedMatrix;
+
+ // Information for scaling
+ bool isScaling;
+ SKPoint pivotPoint;
+
+ public SingleFingerCornerScalePage()
+ {
+ InitializeComponent();
+
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.seatedmonkey.jpg"))
+ {
+ bitmap = SKBitmap.Decode(stream);
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ canvas.SetMatrix(currentMatrix);
+ canvas.DrawBitmap(bitmap, 0, 0);
+ }
+
+ void OnTouch(object sender, SKTouchEventArgs e)
+ {
+ // Convert point to pixels
+ SKPoint point = new SKPoint((float)(canvasView.CanvasSize.Width * e.Location.X / canvasView.Width), (float)(canvasView.CanvasSize.Height * e.Location.Y / canvasView.Height));
+
+ switch (e.ActionType)
+ {
+ case SKTouchAction.Pressed:
+ // Track only one finger
+ if (touchId.HasValue)
+ return;
+
+ // Check if the finger is within the boundaries of the bitmap
+ SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
+ rect = currentMatrix.MapRect(rect);
+ if (!rect.Contains(point))
+ return;
+
+ // First assume there will be no scaling
+ isScaling = false;
+
+ // If touch is outside interior ellipse, make this a scaling operation
+ if (Math.Pow((point.X - rect.MidX) / (rect.Width / 2), 2) +
+ Math.Pow((point.Y - rect.MidY) / (rect.Height / 2), 2) > 1)
+ {
+ isScaling = true;
+ float xPivot = point.X < rect.MidX ? rect.Right : rect.Left;
+ float yPivot = point.Y < rect.MidY ? rect.Bottom : rect.Top;
+ pivotPoint = new SKPoint(xPivot, yPivot);
+ }
+
+ // Common for either pan or scale
+ touchId = e.Id;
+ pressedLocation = point;
+ pressedMatrix = currentMatrix;
+ break;
+
+ case SKTouchAction.Moved:
+ if (!touchId.HasValue || e.Id != touchId.Value)
+ return;
+
+ SKMatrix matrix = SKMatrix.CreateIdentity();
+
+ // Translating
+ if (!isScaling)
+ {
+ SKPoint delta = point - pressedLocation;
+ matrix = SKMatrix.CreateTranslation(delta.X, delta.Y);
+ }
+ // Scaling
+ else
+ {
+ float scaleX = (point.X - pivotPoint.X) / (pressedLocation.X - pivotPoint.X);
+ float scaleY = (point.Y - pivotPoint.Y) / (pressedLocation.Y - pivotPoint.Y);
+ matrix = SKMatrix.CreateScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);
+ }
+
+ // Concatenate the matrices
+ matrix = matrix.PreConcat(pressedMatrix);
+ currentMatrix = matrix;
+ canvasView.InvalidateSurface();
+ break;
+
+ case SKTouchAction.Released:
+ case SKTouchAction.Cancelled:
+ touchId = null;
+ break;
+ }
+
+ e.Handled = true;
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewAngleExperimentPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewAngleExperimentPage.xaml
new file mode 100644
index 000000000..b86a6a4f2
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewAngleExperimentPage.xaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewAngleExperimentPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewAngleExperimentPage.xaml.cs
new file mode 100644
index 000000000..727ea497c
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewAngleExperimentPage.xaml.cs
@@ -0,0 +1,57 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class SkewAngleExperimentPage : ContentPage
+{
+ public SkewAngleExperimentPage()
+ {
+ InitializeComponent();
+ }
+
+ void sliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Blue,
+ TextSize = 200
+ })
+ {
+ float xCenter = info.Width / 2;
+ float yCenter = info.Height / 2;
+
+ string text = "SKEW";
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText(text, ref textBounds);
+ float xText = xCenter - textBounds.MidX;
+ float yText = yCenter - textBounds.MidY;
+
+ canvas.Translate(xCenter, yCenter);
+ SkewDegrees(canvas, xSkewSlider.Value, ySkewSlider.Value);
+ canvas.Translate(-xCenter, -yCenter);
+ canvas.DrawText(text, xText, yText, textPaint);
+ }
+ }
+
+ void SkewDegrees(SKCanvas canvas, double xDegrees, double yDegrees)
+ {
+ canvas.Skew((float)Math.Tan(Math.PI * xDegrees / 180),
+ (float)Math.Tan(Math.PI * yDegrees / 180));
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewExperimentPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewExperimentPage.xaml
new file mode 100644
index 000000000..f7d5c08c2
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewExperimentPage.xaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewExperimentPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewExperimentPage.xaml.cs
new file mode 100644
index 000000000..e1585b7fc
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewExperimentPage.xaml.cs
@@ -0,0 +1,44 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class SkewExperimentPage : ContentPage
+{
+ public SkewExperimentPage()
+ {
+ InitializeComponent();
+ }
+
+ void sliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint textPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Blue,
+ TextSize = 200
+ })
+ {
+ string text = "SKEW";
+ SKRect textBounds = new SKRect();
+ textPaint.MeasureText(text, ref textBounds);
+
+ canvas.Skew((float)xSkewSlider.Value, (float)ySkewSlider.Value);
+ canvas.DrawText(text, 0, -textBounds.Top, textPaint);
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewShadowTextPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewShadowTextPage.cs
new file mode 100644
index 000000000..17c5e0878
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/SkewShadowTextPage.cs
@@ -0,0 +1,53 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class SkewShadowTextPage : ContentPage
+ {
+ public SkewShadowTextPage()
+ {
+ Title = "Shadow Text";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint textPaint = new SKPaint())
+ {
+ textPaint.Style = SKPaintStyle.Fill;
+ textPaint.TextSize = info.Width / 6; // empirically determined
+
+ // Common to shadow and text
+ string text = "shadow";
+ float xText = 20;
+ float yText = info.Height / 2;
+
+ // Shadow
+ textPaint.Color = SKColors.LightGray;
+ canvas.Save();
+ canvas.Translate(xText, yText);
+ canvas.Skew((float)Math.Tan(-Math.PI / 4), 0);
+ canvas.Scale(1, 3);
+ canvas.Translate(-xText, -yText);
+ canvas.DrawText(text, xText, yText, textPaint);
+ canvas.Restore();
+
+ // Text
+ textPaint.Color = SKColors.Blue;
+ canvas.DrawText(text, xText, yText, textPaint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TaperTransform.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TaperTransform.cs
new file mode 100644
index 000000000..d095a67dd
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TaperTransform.cs
@@ -0,0 +1,103 @@
+using SkiaSharp;
+
+namespace SkiaSharpDemos.Transforms
+{
+ enum TaperSide { Left, Top, Right, Bottom }
+
+ enum TaperCorner { LeftOrTop, RightOrBottom, Both }
+
+ static class TaperTransform
+ {
+ public static SKMatrix Make(SKSize size, TaperSide taperSide, TaperCorner taperCorner, float taperFraction)
+ {
+ SKMatrix matrix = SKMatrix.CreateIdentity();
+
+ switch (taperSide)
+ {
+ case TaperSide.Left:
+ matrix.ScaleX = taperFraction;
+ matrix.ScaleY = taperFraction;
+ matrix.Persp0 = (taperFraction - 1) / size.Width;
+
+ switch (taperCorner)
+ {
+ case TaperCorner.RightOrBottom:
+ break;
+
+ case TaperCorner.LeftOrTop:
+ matrix.SkewY = size.Height * matrix.Persp0;
+ matrix.TransY = size.Height * (1 - taperFraction);
+ break;
+
+ case TaperCorner.Both:
+ matrix.SkewY = (size.Height / 2) * matrix.Persp0;
+ matrix.TransY = size.Height * (1 - taperFraction) / 2;
+ break;
+ }
+ break;
+
+ case TaperSide.Top:
+ matrix.ScaleX = taperFraction;
+ matrix.ScaleY = taperFraction;
+ matrix.Persp1 = (taperFraction - 1) / size.Height;
+
+ switch (taperCorner)
+ {
+ case TaperCorner.RightOrBottom:
+ break;
+
+ case TaperCorner.LeftOrTop:
+ matrix.SkewX = size.Width * matrix.Persp1;
+ matrix.TransX = size.Width * (1 - taperFraction);
+ break;
+
+ case TaperCorner.Both:
+ matrix.SkewX = (size.Width / 2) * matrix.Persp1;
+ matrix.TransX = size.Width * (1 - taperFraction) / 2;
+ break;
+ }
+ break;
+
+ case TaperSide.Right:
+ matrix.ScaleX = 1 / taperFraction;
+ matrix.Persp0 = (1 - taperFraction) / (size.Width * taperFraction);
+
+ switch (taperCorner)
+ {
+ case TaperCorner.RightOrBottom:
+ break;
+
+ case TaperCorner.LeftOrTop:
+ matrix.SkewY = size.Height * matrix.Persp0;
+ break;
+
+ case TaperCorner.Both:
+ matrix.SkewY = (size.Height / 2) * matrix.Persp0;
+ break;
+ }
+ break;
+
+ case TaperSide.Bottom:
+ matrix.ScaleY = 1 / taperFraction;
+ matrix.Persp1 = (1 - taperFraction) / (size.Height * taperFraction);
+
+ switch (taperCorner)
+ {
+ case TaperCorner.RightOrBottom:
+ break;
+
+ case TaperCorner.LeftOrTop:
+ matrix.SkewX = size.Width * matrix.Persp1;
+ break;
+
+ case TaperCorner.Both:
+ matrix.SkewX = (size.Width / 2) * matrix.Persp1;
+ break;
+ }
+ break;
+ }
+ return matrix;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TaperTransformPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TaperTransformPage.xaml
new file mode 100644
index 000000000..611e1d832
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TaperTransformPage.xaml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TaperTransformPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TaperTransformPage.xaml.cs
new file mode 100644
index 000000000..6319ca349
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TaperTransformPage.xaml.cs
@@ -0,0 +1,78 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class TaperTransformPage : ContentPage
+{
+ SKBitmap bitmap;
+
+ MatrixDisplay matrixDisplay = new MatrixDisplay
+ {
+ PerspectiveFormat = "F5"
+ };
+
+ public TaperTransformPage()
+ {
+ InitializeComponent();
+
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.facepalm.jpg"))
+ {
+ bitmap = SKBitmap.Decode(stream);
+ }
+
+ taperFractionSlider.Value = 1;
+ }
+
+ void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnPickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ if (canvasView != null)
+ {
+ canvasView.InvalidateSurface();
+ }
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+ canvas.Clear();
+
+ TaperSide taperSide = (TaperSide)taperSidePicker.SelectedItem;
+ TaperCorner taperCorner = (TaperCorner)taperCornerPicker.SelectedItem;
+ float taperFraction = (float)taperFractionSlider.Value;
+
+ SKMatrix taperMatrix =
+ TaperTransform.Make(new SKSize(bitmap.Width, bitmap.Height),
+ taperSide, taperCorner, taperFraction);
+
+ // Display the matrix in the lower-right corner
+ SKSize matrixSize = matrixDisplay.Measure(taperMatrix);
+
+ matrixDisplay.Paint(canvas, taperMatrix,
+ new SKPoint(info.Width - matrixSize.Width,
+ info.Height - matrixSize.Height));
+
+ // Center bitmap on canvas
+ float x = (info.Width - bitmap.Width) / 2;
+ float y = (info.Height - bitmap.Height) / 2;
+
+ SKMatrix matrix = SKMatrix.CreateTranslation(-x, -y);
+ matrix = matrix.PostConcat(taperMatrix);
+ matrix = matrix.PostConcat(SKMatrix.CreateTranslation(x, y));
+
+ canvas.SetMatrix(matrix);
+ canvas.DrawBitmap(bitmap, x, y);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TestPerspectivePage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TestPerspectivePage.xaml
new file mode 100644
index 000000000..189aeafa7
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TestPerspectivePage.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TestPerspectivePage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TestPerspectivePage.xaml.cs
new file mode 100644
index 000000000..cda7c1a23
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TestPerspectivePage.xaml.cs
@@ -0,0 +1,65 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class TestPerspectivePage : ContentPage
+{
+ SKBitmap bitmap;
+
+ public TestPerspectivePage()
+ {
+ InitializeComponent();
+
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.seatedmonkey.jpg"))
+ {
+ bitmap = SKBitmap.Decode(stream);
+ }
+ }
+
+ void OnPersp0SliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ Slider slider = (Slider)sender;
+ persp0Label.Text = String.Format("Persp0 = {0:F4}", slider.Value / 100);
+ canvasView.InvalidateSurface();
+ }
+
+ void OnPersp1SliderValueChanged(object sender, ValueChangedEventArgs args)
+ {
+ Slider slider = (Slider)sender;
+ persp1Label.Text = String.Format("Persp1 = {0:F4}", slider.Value / 100);
+ canvasView.InvalidateSurface();
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Calculate perspective matrix
+ SKMatrix perspectiveMatrix = SKMatrix.CreateIdentity();
+ perspectiveMatrix.Persp0 = (float)persp0Slider.Value / 100;
+ perspectiveMatrix.Persp1 = (float)persp1Slider.Value / 100;
+
+ // Center of screen
+ float xCenter = info.Width / 2;
+ float yCenter = info.Height / 2;
+
+ SKMatrix matrix = SKMatrix.CreateTranslation(-xCenter, -yCenter);
+ matrix = matrix.PostConcat(perspectiveMatrix);
+ matrix = matrix.PostConcat(SKMatrix.CreateTranslation(xCenter, yCenter));
+
+ // Coordinates to center bitmap on canvas
+ float x = xCenter - bitmap.Width / 2;
+ float y = yCenter - bitmap.Height / 2;
+
+ canvas.SetMatrix(matrix);
+ canvas.DrawBitmap(bitmap, x, y);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationBitmap.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationBitmap.cs
new file mode 100644
index 000000000..ee1bc31f9
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationBitmap.cs
@@ -0,0 +1,114 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms
+{
+ class TouchManipulationBitmap
+ {
+ SKBitmap bitmap;
+
+ Dictionary touchDictionary =
+ new Dictionary();
+
+ public TouchManipulationManager TouchManager { get; set; }
+ public SKMatrix Matrix { get; set; }
+
+ public TouchManipulationBitmap(SKBitmap bitmap)
+ {
+ this.bitmap = bitmap;
+ Matrix = SKMatrix.CreateIdentity();
+
+ TouchManager = new TouchManipulationManager
+ {
+ Mode = TouchManipulationMode.ScaleRotate
+ };
+ }
+
+ public void Paint(SKCanvas canvas)
+ {
+ canvas.Save();
+ SKMatrix matrix = Matrix;
+ canvas.Concat(ref matrix);
+ canvas.DrawBitmap(bitmap, 0, 0);
+ canvas.Restore();
+ }
+
+ public bool HitTest(SKPoint location)
+ {
+ // Invert the matrix
+ SKMatrix inverseMatrix;
+
+ if (Matrix.TryInvert(out inverseMatrix))
+ {
+ // Transform the point using the inverted matrix
+ SKPoint transformedPoint = inverseMatrix.MapPoint(location);
+
+ // Check if it's in the untransformed bitmap rectangle
+ SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
+ return rect.Contains(transformedPoint);
+ }
+ return false;
+ }
+
+ public void ProcessTouchEvent(long id, SKTouchAction type, SKPoint location)
+ {
+ switch (type)
+ {
+ case SKTouchAction.Pressed:
+ touchDictionary.Add(id, new TouchManipulationInfo
+ {
+ PreviousPoint = location,
+ NewPoint = location
+ });
+ break;
+
+ case SKTouchAction.Moved:
+ TouchManipulationInfo info = touchDictionary[id];
+ info.NewPoint = location;
+ Manipulate();
+ info.PreviousPoint = info.NewPoint;
+ break;
+
+ case SKTouchAction.Released:
+ touchDictionary[id].NewPoint = location;
+ Manipulate();
+ touchDictionary.Remove(id);
+ break;
+
+ case SKTouchAction.Cancelled:
+ touchDictionary.Remove(id);
+ break;
+ }
+ }
+
+ void Manipulate()
+ {
+ TouchManipulationInfo[] infos = new TouchManipulationInfo[touchDictionary.Count];
+ touchDictionary.Values.CopyTo(infos, 0);
+ SKMatrix touchMatrix = SKMatrix.CreateIdentity();
+
+ if (infos.Length == 1)
+ {
+ SKPoint prevPoint = infos[0].PreviousPoint;
+ SKPoint newPoint = infos[0].NewPoint;
+ SKPoint pivotPoint = Matrix.MapPoint(bitmap.Width / 2, bitmap.Height / 2);
+
+ touchMatrix = TouchManager.OneFingerManipulate(prevPoint, newPoint, pivotPoint);
+ }
+ else if (infos.Length >= 2)
+ {
+ int pivotIndex = infos[0].NewPoint == infos[0].PreviousPoint ? 0 : 1;
+ SKPoint pivotPoint = infos[pivotIndex].NewPoint;
+ SKPoint newPoint = infos[1 - pivotIndex].NewPoint;
+ SKPoint prevPoint = infos[1 - pivotIndex].PreviousPoint;
+
+ touchMatrix = TouchManager.TwoFingerManipulate(prevPoint, newPoint, pivotPoint);
+ }
+
+ SKMatrix matrix = Matrix;
+ matrix = matrix.PostConcat(touchMatrix);
+ Matrix = matrix;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationInfo.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationInfo.cs
new file mode 100644
index 000000000..862230f0e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationInfo.cs
@@ -0,0 +1,11 @@
+using SkiaSharp;
+
+namespace SkiaSharpDemos.Transforms
+{
+ class TouchManipulationInfo
+ {
+ public SKPoint PreviousPoint { get; set; }
+ public SKPoint NewPoint { get; set; }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationManager.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationManager.cs
new file mode 100644
index 000000000..df62ffded
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationManager.cs
@@ -0,0 +1,105 @@
+using SkiaSharp;
+
+namespace SkiaSharpDemos.Transforms
+{
+ class TouchManipulationManager
+ {
+ public TouchManipulationMode Mode { set; get; }
+
+ public SKMatrix OneFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
+ {
+ if (Mode == TouchManipulationMode.None)
+ {
+ return SKMatrix.MakeIdentity();
+ }
+
+ SKMatrix touchMatrix = SKMatrix.MakeIdentity();
+ SKPoint delta = newPoint - prevPoint;
+
+ if (Mode == TouchManipulationMode.ScaleDualRotate) // One-finger rotation
+ {
+ SKPoint oldVector = prevPoint - pivotPoint;
+ SKPoint newVector = newPoint - pivotPoint;
+
+ // Avoid rotation if fingers are too close to center
+ if (Magnitude(newVector) > 25 && Magnitude(oldVector) > 25)
+ {
+ float prevAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
+ float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
+
+ // Calculate rotation matrix
+ float angle = newAngle - prevAngle;
+ touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
+
+ // Effectively rotate the old vector
+ float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
+ oldVector.X = magnitudeRatio * newVector.X;
+ oldVector.Y = magnitudeRatio * newVector.Y;
+
+ // Recalculate delta
+ delta = newVector - oldVector;
+ }
+ }
+
+ // Multiply the rotation matrix by a translation matrix
+ SKMatrix.PostConcat(ref touchMatrix, SKMatrix.MakeTranslation(delta.X, delta.Y));
+
+ return touchMatrix;
+ }
+
+ public SKMatrix TwoFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
+ {
+ SKMatrix touchMatrix = SKMatrix.MakeIdentity();
+ SKPoint oldVector = prevPoint - pivotPoint;
+ SKPoint newVector = newPoint - pivotPoint;
+
+ if (Mode == TouchManipulationMode.ScaleRotate ||
+ Mode == TouchManipulationMode.ScaleDualRotate)
+ {
+ // Find angles from pivot point to touch points
+ float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
+ float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
+
+ // Calculate rotation matrix
+ float angle = newAngle - oldAngle;
+ touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
+
+ // Effectively rotate the old vector
+ float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
+ oldVector.X = magnitudeRatio * newVector.X;
+ oldVector.Y = magnitudeRatio * newVector.Y;
+ }
+
+ float scaleX = 1;
+ float scaleY = 1;
+
+ if (Mode == TouchManipulationMode.AnisotropicScale)
+ {
+ scaleX = newVector.X / oldVector.X;
+ scaleY = newVector.Y / oldVector.Y;
+
+ }
+ else if (Mode == TouchManipulationMode.IsotropicScale ||
+ Mode == TouchManipulationMode.ScaleRotate ||
+ Mode == TouchManipulationMode.ScaleDualRotate)
+ {
+ scaleX = scaleY = Magnitude(newVector) / Magnitude(oldVector);
+ }
+
+ if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
+ !float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
+ {
+ SKMatrix.PostConcat(ref touchMatrix,
+ SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y));
+ }
+
+ return touchMatrix;
+ }
+
+ float Magnitude(SKPoint point)
+ {
+ return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationMode.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationMode.cs
new file mode 100644
index 000000000..b033c6bae
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationMode.cs
@@ -0,0 +1,12 @@
+namespace SkiaSharpDemos.Transforms
+{
+ enum TouchManipulationMode
+ {
+ None,
+ PanOnly,
+ IsotropicScale, // includes panning
+ AnisotropicScale, // includes panning
+ ScaleRotate, // implies isotropic scaling
+ ScaleDualRotate // adds one-finger rotation
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationPage.xaml
new file mode 100644
index 000000000..218dc313f
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationPage.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationPage.xaml.cs
new file mode 100644
index 000000000..535b1d454
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TouchManipulationPage.xaml.cs
@@ -0,0 +1,90 @@
+using System.Reflection;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace SkiaSharpDemos.Transforms;
+
+public partial class TouchManipulationPage : ContentPage
+{
+ TouchManipulationBitmap bitmap;
+ List touchIds = new List();
+ MatrixDisplay matrixDisplay = new MatrixDisplay();
+
+ public TouchManipulationPage()
+ {
+ InitializeComponent();
+
+ Assembly assembly = GetType().GetTypeInfo().Assembly;
+
+ using (Stream stream = assembly.GetManifestResourceStream("SkiaSharpDemos.Media.mountainclimbers.jpg"))
+ {
+ SKBitmap bitmap = SKBitmap.Decode(stream);
+ this.bitmap = new TouchManipulationBitmap(bitmap);
+ this.bitmap.TouchManager.Mode = TouchManipulationMode.ScaleRotate;
+ }
+ }
+
+ void OnTouchModePickerSelectedIndexChanged(object sender, EventArgs args)
+ {
+ if (bitmap != null)
+ {
+ Picker picker = (Picker)sender;
+ bitmap.TouchManager.Mode = (TouchManipulationMode)picker.SelectedItem;
+ }
+ }
+
+ void OnTouch(object sender, SKTouchEventArgs e)
+ {
+ // Convert point to pixels
+ SKPoint point = new SKPoint((float)(canvasView.CanvasSize.Width * e.Location.X / canvasView.Width), (float)(canvasView.CanvasSize.Height * e.Location.Y / canvasView.Height));
+
+ switch (e.ActionType)
+ {
+ case SKTouchAction.Pressed:
+ if (bitmap.HitTest(point))
+ {
+ touchIds.Add(e.Id);
+ bitmap.ProcessTouchEvent(e.Id, e.ActionType, point);
+ break;
+ }
+ break;
+
+ case SKTouchAction.Moved:
+ if (touchIds.Contains(e.Id))
+ {
+ bitmap.ProcessTouchEvent(e.Id, e.ActionType, point);
+ canvasView.InvalidateSurface();
+ }
+ break;
+
+ case SKTouchAction.Released:
+ case SKTouchAction.Cancelled:
+ if (touchIds.Contains(e.Id))
+ {
+ bitmap.ProcessTouchEvent(e.Id, e.ActionType, point);
+ touchIds.Remove(e.Id);
+ canvasView.InvalidateSurface();
+ }
+ break;
+ }
+
+ e.Handled = true;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ // Display the bitmap
+ bitmap.Paint(canvas);
+
+ // Display the matrix in the lower-right corner
+ SKSize matrixSize = matrixDisplay.Measure(bitmap.Matrix);
+
+ matrixDisplay.Paint(canvas, bitmap.Matrix, new SKPoint(info.Width - matrixSize.Width, info.Height - matrixSize.Height));
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TransformsMenuPage.xaml b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TransformsMenuPage.xaml
new file mode 100644
index 000000000..7382949ac
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TransformsMenuPage.xaml
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TransformsMenuPage.xaml.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TransformsMenuPage.xaml.cs
new file mode 100644
index 000000000..471ca1d88
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TransformsMenuPage.xaml.cs
@@ -0,0 +1,9 @@
+namespace SkiaSharpDemos.Transforms;
+
+public partial class TransformsMenuPage : BasePage
+{
+ public TransformsMenuPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TranslateTextEffectsPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TranslateTextEffectsPage.cs
new file mode 100644
index 000000000..c994512b8
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/TranslateTextEffectsPage.cs
@@ -0,0 +1,69 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class TranslateTextEffectsPage : ContentPage
+ {
+ public TranslateTextEffectsPage()
+ {
+ Title = "Translate Text Effects";
+
+ SKCanvasView canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ float textSize = 150;
+
+ using (SKPaint textPaint = new SKPaint())
+ {
+ textPaint.Style = SKPaintStyle.Fill;
+ textPaint.TextSize = textSize;
+ textPaint.FakeBoldText = true;
+
+ float x = 10;
+ float y = textSize;
+
+ // Shadow
+ canvas.Translate(10, 10);
+ textPaint.Color = SKColors.Black;
+ canvas.DrawText("SHADOW", x, y, textPaint);
+ canvas.Translate(-10, -10);
+ textPaint.Color = SKColors.Pink;
+ canvas.DrawText("SHADOW", x, y, textPaint);
+
+ y += 2 * textSize;
+
+ // Engrave
+ canvas.Translate(-5, -5);
+ textPaint.Color = SKColors.Black;
+ canvas.DrawText("ENGRAVE", x, y, textPaint);
+ canvas.ResetMatrix();
+ textPaint.Color = SKColors.White;
+ canvas.DrawText("ENGRAVE", x, y, textPaint);
+
+ y += 2 * textSize;
+
+ // Emboss
+ canvas.Save();
+ canvas.Translate(5, 5);
+ textPaint.Color = SKColors.Black;
+ canvas.DrawText("EMBOSS", x, y, textPaint);
+ canvas.Restore();
+ textPaint.Color = SKColors.White;
+ canvas.DrawText("EMBOSS", x, y, textPaint);
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/UglyAnalogClockPage.cs b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/UglyAnalogClockPage.cs
new file mode 100644
index 000000000..fd2253ac5
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/SkiaSharpDemos/Transforms/UglyAnalogClockPage.cs
@@ -0,0 +1,94 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SkiaSharpDemos.Transforms
+{
+ public class UglyAnalogClockPage : ContentPage
+ {
+ SKCanvasView canvasView;
+ bool pageIsActive;
+
+ public UglyAnalogClockPage()
+ {
+ Title = "Ugly Analog Clock";
+
+ canvasView = new SKCanvasView();
+ canvasView.PaintSurface += OnCanvasViewPaintSurface;
+ Content = canvasView;
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ pageIsActive = true;
+
+ Dispatcher.StartTimer(TimeSpan.FromSeconds(1), () =>
+ {
+ canvasView.InvalidateSurface();
+ return pageIsActive;
+ });
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ pageIsActive = false;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ using (SKPaint strokePaint = new SKPaint())
+ using (SKPaint fillPaint = new SKPaint())
+ {
+ strokePaint.Style = SKPaintStyle.Stroke;
+ strokePaint.Color = SKColors.Black;
+ strokePaint.StrokeCap = SKStrokeCap.Round;
+
+ fillPaint.Style = SKPaintStyle.Fill;
+ fillPaint.Color = SKColors.Gray;
+
+ // Transform for 100-radius circle centered at origin
+ canvas.Translate(info.Width / 2f, info.Height / 2f);
+ canvas.Scale(Math.Min(info.Width / 200f, info.Height / 200f));
+
+ // Hour and minute marks
+ for (int angle = 0; angle < 360; angle += 6)
+ {
+ canvas.DrawCircle(0, -90, angle % 30 == 0 ? 4 : 2, fillPaint);
+ canvas.RotateDegrees(6);
+ }
+
+ DateTime dateTime = DateTime.Now;
+
+ // Hour hand
+ strokePaint.StrokeWidth = 20;
+ canvas.Save();
+ canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
+ canvas.DrawLine(0, 0, 0, -50, strokePaint);
+ canvas.Restore();
+
+ // Minute hand
+ strokePaint.StrokeWidth = 10;
+ canvas.Save();
+ canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
+ canvas.DrawLine(0, 0, 0, -70, strokePaint);
+ canvas.Restore();
+
+ // Second hand
+ strokePaint.StrokeWidth = 2;
+ canvas.Save();
+ canvas.RotateDegrees(6 * dateTime.Second);
+ canvas.DrawLine(0, 10, 0, -80, strokePaint);
+ canvas.Restore();
+ }
+ }
+ }
+}
+