diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml
index 2f86a5785..8589c2674 100644
--- a/.github/workflows/build-pr.yml
+++ b/.github/workflows/build-pr.yml
@@ -2,6 +2,8 @@ name: Build Changed C# Projects for PR
on:
pull_request:
+ paths-ignore:
+ - '**.md'
jobs:
build:
@@ -36,7 +38,7 @@ jobs:
run: |
$failedProjectCount=0
# Get the list of changed files
- $changedFiles = git diff --name-only -r HEAD^1 HEAD
+ $changedFiles = git diff --name-only -r --diff-filter=d HEAD^1 HEAD
$excluded_projects_file="./eng/excluded_projects_" + "${{ runner.os }}".ToLower() + ".txt"
$excluded_projects=@()
@@ -77,13 +79,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 +99,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 +119,9 @@ jobs:
}
}
}
+ else {
+ Write-Output "::warning:: Found no csproj for file $file"
+ }
}
if ($failedProjectCount -gt 0) {
diff --git a/8.0/PlatformIntegration/PlatformIntegrationDemos/PlatformIntegrationDemo/Platforms/MacCatalyst/Info.plist b/8.0/PlatformIntegration/PlatformIntegrationDemos/PlatformIntegrationDemo/Platforms/MacCatalyst/Info.plist
index 7fb000a1e..c362a939a 100644
--- a/8.0/PlatformIntegration/PlatformIntegrationDemos/PlatformIntegrationDemo/Platforms/MacCatalyst/Info.plist
+++ b/8.0/PlatformIntegration/PlatformIntegrationDemos/PlatformIntegrationDemo/Platforms/MacCatalyst/Info.plist
@@ -53,6 +53,10 @@
Camera Photos
NSCalendarsUsageDescription
Calendar Access
+ NSCalendarsFullAccessUsageDescription
+ Full access to the Calendar is needed to sign you up for parties!
+ NSCalendarsWriteOnlyAccessUsageDescription
+ Just writing events to the Calendar, nothing to see here.
NSLocationAlwaysAndWhenInUseUsageDescription
Get Location
NSLocationAlwaysUsageDescription
diff --git a/8.0/PlatformIntegration/PlatformIntegrationDemos/PlatformIntegrationDemo/Platforms/iOS/Info.plist b/8.0/PlatformIntegration/PlatformIntegrationDemos/PlatformIntegrationDemo/Platforms/iOS/Info.plist
index 058e981a1..e82a4c2a9 100644
--- a/8.0/PlatformIntegration/PlatformIntegrationDemos/PlatformIntegrationDemo/Platforms/iOS/Info.plist
+++ b/8.0/PlatformIntegration/PlatformIntegrationDemos/PlatformIntegrationDemo/Platforms/iOS/Info.plist
@@ -47,6 +47,10 @@
Camera Photos
NSCalendarsUsageDescription
Calendar Access
+ NSCalendarsFullAccessUsageDescription
+ Full access to the Calendar is needed to sign you up for parties!
+ NSCalendarsWriteOnlyAccessUsageDescription
+ Just writing events to the Calendar, nothing to see here.
NSLocationAlwaysAndWhenInUseUsageDescription
Get Location
NSLocationAlwaysUsageDescription
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation.sln b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation.sln
new file mode 100644
index 000000000..51b3afec8
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation.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}") = "MandelbrotAnimation", "MandelbrotAnimation\MandelbrotAnimation.csproj", "{AADA2731-0754-4634-944E-1DCC3C1374EB}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {AADA2731-0754-4634-944E-1DCC3C1374EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AADA2731-0754-4634-944E-1DCC3C1374EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AADA2731-0754-4634-944E-1DCC3C1374EB}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {AADA2731-0754-4634-944E-1DCC3C1374EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AADA2731-0754-4634-944E-1DCC3C1374EB}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {9E213343-022C-4D68-9714-5D25F8876EC7}
+ EndGlobalSection
+EndGlobal
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/App.xaml b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/App.xaml
new file mode 100644
index 000000000..de421799a
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/App.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/App.xaml.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/App.xaml.cs
new file mode 100644
index 000000000..5164215ea
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/App.xaml.cs
@@ -0,0 +1,12 @@
+namespace MandelbrotAnimation;
+
+public partial class App : Application
+{
+ public App()
+ {
+ InitializeComponent();
+
+ MainPage = new AppShell();
+ }
+}
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/AppShell.xaml b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/AppShell.xaml
new file mode 100644
index 000000000..5609f73a4
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/AppShell.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/AppShell.xaml.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/AppShell.xaml.cs
new file mode 100644
index 000000000..e43d2a380
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/AppShell.xaml.cs
@@ -0,0 +1,10 @@
+namespace MandelbrotAnimation;
+
+public partial class AppShell : Shell
+{
+ public AppShell()
+ {
+ InitializeComponent();
+ }
+}
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/BitmapInfo.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/BitmapInfo.cs
new file mode 100644
index 000000000..d09160536
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/BitmapInfo.cs
@@ -0,0 +1,19 @@
+namespace MandelbrotAnimation
+{
+ class BitmapInfo
+ {
+ public int PixelWidth { get; private set; }
+
+ public int PixelHeight { get; private set; }
+
+ public int[] IterationCounts { get; private set; }
+
+ public BitmapInfo(int pixelWidth, int pixelHeight, int[] iterationCounts)
+ {
+ PixelWidth = pixelWidth;
+ PixelHeight = pixelHeight;
+ IterationCounts = iterationCounts;
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MainPage.xaml b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MainPage.xaml
new file mode 100644
index 000000000..5a50f007b
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MainPage.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MainPage.xaml.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MainPage.xaml.cs
new file mode 100644
index 000000000..a26a67d10
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MainPage.xaml.cs
@@ -0,0 +1,226 @@
+using System.Diagnostics;
+using System.Numerics;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace MandelbrotAnimation;
+
+public partial class MainPage : ContentPage
+{
+ const int COUNT = 10; // The number of bitmaps in the animation. This can go up to 50!
+ const int BITMAP_SIZE = 1000; // Program uses square bitmaps exclusively
+
+ // Uncomment just one of these, or define your own
+ static readonly Complex center = new Complex(-1.17651152924355, 0.298520986549558);
+ // static readonly Complex center = new Complex(-0.774693089457127, 0.124226621261617);
+ // static readonly Complex center = new Complex(-0.556624880053304, 0.634696788141351);
+
+ SKBitmap[] bitmaps = new SKBitmap[COUNT]; // array of bitmaps
+ Stopwatch stopwatch = new Stopwatch(); // for the animation
+ int bitmapIndex;
+ double bitmapProgress = 0;
+
+ // File path for storing each bitmap in local storage
+ string FolderPath() => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+
+ string FilePath(int zoomLevel) => Path.Combine(FolderPath(), String.Format("R{0}I{1}Z{2:D2}.png", center.Real, center.Imaginary, zoomLevel));
+
+ // Bitmap pixel for Rgba8888 format
+ uint MakePixel(byte alpha, byte red, byte green, byte blue) => (uint)((alpha << 24) | (blue << 16) | (green << 8) | red);
+
+ public MainPage()
+ {
+ InitializeComponent();
+
+ LoadAndStartAnimation();
+ }
+
+ async void LoadAndStartAnimation()
+ {
+ // Show total bitmap storage
+ TallyBitmapSizes();
+
+ // Create progressReporter for async operation
+ Progress progressReporter =
+ new Progress((double progress) => progressBar.Progress = progress);
+
+ // Create (unused) CancellationTokenSource for async operation
+ CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
+
+ // Loop through all the zoom levels
+ for (int zoomLevel = 0; zoomLevel < COUNT; zoomLevel++)
+ {
+ // If the file exists, load it
+ if (File.Exists(FilePath(zoomLevel)))
+ {
+ statusLabel.Text = $"Loading bitmap for zoom level {zoomLevel}";
+
+ using (Stream stream = File.OpenRead(FilePath(zoomLevel)))
+ {
+ bitmaps[zoomLevel] = SKBitmap.Decode(stream);
+ }
+ }
+ // Otherwise, create a new bitmap
+ else
+ {
+ statusLabel.Text = $"Creating bitmap for zoom level {zoomLevel}";
+
+ CancellationToken cancelToken = cancelTokenSource.Token;
+
+ // Do the (generally lengthy) Mandelbrot calculation
+ BitmapInfo bitmapInfo =
+ await Mandelbrot.CalculateAsync(center,
+ 4 / Math.Pow(2, zoomLevel),
+ 4 / Math.Pow(2, zoomLevel),
+ BITMAP_SIZE, BITMAP_SIZE,
+ (int)Math.Pow(2, 10), progressReporter, cancelToken);
+
+ // Create bitmap & get pointer to the pixel bits
+ SKBitmap bitmap = new SKBitmap(BITMAP_SIZE, BITMAP_SIZE, SKColorType.Rgba8888, SKAlphaType.Opaque);
+ IntPtr basePtr = bitmap.GetPixels();
+
+ // Set pixel bits to color based on iteration count
+ for (int row = 0; row < bitmap.Width; row++)
+ {
+ for (int col = 0; col < bitmap.Height; col++)
+ {
+ int iterationCount = bitmapInfo.IterationCounts[row * bitmap.Width + col];
+ uint pixel = 0xFF000000; // black
+
+ if (iterationCount != -1)
+ {
+ double proportion = (iterationCount / 32.0) % 1;
+ byte red = 0, green = 0, blue = 0;
+
+ if (proportion < 0.5)
+ {
+ red = (byte)(255 * (1 - 2 * proportion));
+ blue = (byte)(255 * 2 * proportion);
+ }
+ else
+ {
+ proportion = 2 * (proportion - 0.5);
+ green = (byte)(255 * proportion);
+ blue = (byte)(255 * (1 - proportion));
+ }
+
+ pixel = MakePixel(0xFF, red, green, blue);
+ }
+
+ // Calculate pointer to pixel
+ IntPtr pixelPtr = basePtr + 4 * (row * bitmap.Width + col);
+
+ unsafe // requires compiling with unsafe flag
+ {
+ *(uint*)pixelPtr.ToPointer() = pixel;
+ }
+ }
+ }
+
+ // Save as PNG file
+ SKData data = SKImage.FromBitmap(bitmap).Encode();
+
+ try
+ {
+ File.WriteAllBytes(FilePath(zoomLevel), data.ToArray());
+ }
+ catch
+ {
+ // Probably out of space, but just ignore
+ }
+
+ // Store in array
+ bitmaps[zoomLevel] = bitmap;
+
+ // Show new bitmap sizes
+ TallyBitmapSizes();
+ }
+
+ // Display the bitmap
+ bitmapIndex = zoomLevel;
+ canvasView.InvalidateSurface();
+ }
+
+ // Now start the animation
+ stopwatch.Start();
+ Dispatcher.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
+ }
+
+ bool OnTimerTick()
+ {
+ int cycle = 6000 * COUNT; // total cycle length in milliseconds
+
+ // Time in milliseconds from 0 to cycle
+ int time = (int)(stopwatch.ElapsedMilliseconds % cycle);
+
+ // Make it sinusoidal, including bitmap index and gradation between bitmaps
+ double progress = COUNT * 0.5 * (1 + Math.Sin(2 * Math.PI * time / cycle - Math.PI / 2));
+
+ // These are the field values that the PaintSurface handler uses
+ bitmapIndex = (int)progress;
+ bitmapProgress = progress - bitmapIndex;
+
+ // It doesn't often happen that we get up to COUNT, but an exception would be raised
+ if (bitmapIndex < COUNT)
+ {
+ // Show progress in UI
+ statusLabel.Text = $"Displaying bitmap for zoom level {bitmapIndex}";
+ progressBar.Progress = bitmapProgress;
+
+ // Update the canvas
+ canvasView.InvalidateSurface();
+ }
+ return true;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ SKImageInfo info = args.Info;
+ SKSurface surface = args.Surface;
+ SKCanvas canvas = surface.Canvas;
+
+ canvas.Clear();
+
+ if (bitmaps[bitmapIndex] != null)
+ {
+ // Determine destination rect as square in canvas
+ int dimension = Math.Min(info.Width, info.Height);
+ float x = (info.Width - dimension) / 2;
+ float y = (info.Height - dimension) / 2;
+ SKRect destRect = new SKRect(x, y, x + dimension, y + dimension);
+
+ // Calculate source rectangle based on fraction:
+ // bitmapProgress == 0: full bitmap
+ // bitmapProgress == 1: half of length and width of bitmap
+ float fraction = 0.5f * (1 - (float)Math.Pow(2, -bitmapProgress));
+ SKBitmap bitmap = bitmaps[bitmapIndex];
+ int width = bitmap.Width;
+ int height = bitmap.Height;
+ SKRect sourceRect = new SKRect(fraction * width, fraction * height,
+ (1 - fraction) * width, (1 - fraction) * height);
+
+ // Display the bitmap
+ canvas.DrawBitmap(bitmap, sourceRect, destRect);
+ }
+ }
+
+ void TallyBitmapSizes()
+ {
+ long fileSize = 0;
+
+ foreach (string filename in Directory.EnumerateFiles(FolderPath()))
+ {
+ fileSize += new FileInfo(filename).Length;
+ }
+ storageLabel.Text = $"Total storage: {fileSize:N0} bytes";
+ }
+
+ void OnDeleteButtonClicked(object sender, EventArgs args)
+ {
+ foreach (string filepath in Directory.EnumerateFiles(FolderPath()))
+ {
+ File.Delete(filepath);
+ }
+ TallyBitmapSizes();
+ }
+}
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MandelbrotAnimation.csproj b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MandelbrotAnimation.csproj
new file mode 100644
index 000000000..fb6a46428
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MandelbrotAnimation.csproj
@@ -0,0 +1,67 @@
+
+
+
+ net8.0-android;net8.0-ios;net8.0-maccatalyst
+ $(TargetFrameworks);net8.0-windows10.0.19041.0
+
+
+
+
+
+
+ Exe
+ MandelbrotAnimation
+ true
+ true
+ enable
+ enable
+ true
+
+
+ MandelbrotAnimation
+
+
+ com.companyname.mandelbrotanimation
+
+
+ 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/MandelbrotAnimation/MandelbrotAnimation/Mandlebrot.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Mandlebrot.cs
new file mode 100644
index 000000000..04f9e05cb
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Mandlebrot.cs
@@ -0,0 +1,67 @@
+using System.Numerics;
+
+namespace MandelbrotAnimation
+{
+ static class Mandelbrot
+ {
+ public static Task CalculateAsync(Complex center,
+ double width, double height,
+ int pixelWidth, int pixelHeight,
+ int iterations,
+ IProgress progress,
+ CancellationToken cancelToken)
+ {
+ return Task.Run(() =>
+ {
+ int[] iterationCounts = new int[pixelWidth * pixelHeight];
+ int index = 0;
+
+ for (int row = 0; row < pixelHeight; row++)
+ {
+ progress.Report((double)row / pixelHeight);
+ cancelToken.ThrowIfCancellationRequested();
+
+ double y = center.Imaginary + height / 2 - row * height / pixelHeight;
+
+ for (int col = 0; col < pixelWidth; col++)
+ {
+ double x = center.Real - width / 2 + col * width / pixelWidth;
+ Complex c = new Complex(x, y);
+
+ if ((c - new Complex(-1, 0)).Magnitude < 1.0 / 4)
+ {
+ iterationCounts[index++] = -1;
+ }
+ // http://www.reenigne.org/blog/algorithm-for-mandelbrot-cardioid/
+ else if (c.Magnitude * c.Magnitude * (8 * c.Magnitude * c.Magnitude - 3) < 3.0 / 32 - c.Real)
+ {
+ iterationCounts[index++] = -1;
+ }
+ else
+ {
+ Complex z = 0;
+ int iteration = 0;
+
+ do
+ {
+ z = z * z + c;
+ iteration++;
+ }
+ while (iteration < iterations && z.Magnitude < 2);
+
+ if (iteration == iterations)
+ {
+ iterationCounts[index++] = -1;
+ }
+ else
+ {
+ iterationCounts[index++] = iteration;
+ }
+ }
+ }
+ }
+ return new BitmapInfo(pixelWidth, pixelHeight, iterationCounts);
+ }, cancelToken);
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MauiProgram.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MauiProgram.cs
new file mode 100644
index 000000000..b09d6567e
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/MauiProgram.cs
@@ -0,0 +1,27 @@
+using Microsoft.Extensions.Logging;
+using SkiaSharp.Views.Maui.Controls.Hosting;
+
+namespace MandelbrotAnimation;
+
+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/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/AndroidManifest.xml b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 000000000..ddd284fbc
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/MainActivity.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/MainActivity.cs
new file mode 100644
index 000000000..bd48ed5f4
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/MainActivity.cs
@@ -0,0 +1,11 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace MandelbrotAnimation;
+
+[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/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/MainApplication.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/MainApplication.cs
new file mode 100644
index 000000000..84db9d9e9
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/MainApplication.cs
@@ -0,0 +1,16 @@
+using Android.App;
+using Android.Runtime;
+
+namespace MandelbrotAnimation;
+
+[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/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/Resources/values/colors.xml b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 000000000..3b8595ecd
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #512BD4
+ #2B0B98
+ #2B0B98
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/MacCatalyst/AppDelegate.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/MacCatalyst/AppDelegate.cs
new file mode 100644
index 000000000..897dde96d
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/MacCatalyst/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace MandelbrotAnimation;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/MacCatalyst/Entitlements.plist b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/MacCatalyst/Entitlements.plist
new file mode 100644
index 000000000..8e87c0cb0
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/MacCatalyst/Entitlements.plist
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+ com.apple.security.network.client
+
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/MacCatalyst/Info.plist b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/MacCatalyst/Info.plist
new file mode 100644
index 000000000..f24aacc0d
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/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/MandelbrotAnimation/MandelbrotAnimation/Platforms/MacCatalyst/Program.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/MacCatalyst/Program.cs
new file mode 100644
index 000000000..843c1954d
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/MacCatalyst/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace MandelbrotAnimation;
+
+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/MandelbrotAnimation/MandelbrotAnimation/Platforms/Tizen/Main.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Tizen/Main.cs
new file mode 100644
index 000000000..171083a8c
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Tizen/Main.cs
@@ -0,0 +1,17 @@
+using System;
+using Microsoft.Maui;
+using Microsoft.Maui.Hosting;
+
+namespace MandelbrotAnimation;
+
+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/MandelbrotAnimation/MandelbrotAnimation/Platforms/Tizen/tizen-manifest.xml b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Tizen/tizen-manifest.xml
new file mode 100644
index 000000000..f4d4f2bc9
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Tizen/tizen-manifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ maui-appicon-placeholder
+
+
+
+
+ http://tizen.org/privilege/internet
+
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Windows/App.xaml b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Windows/App.xaml
new file mode 100644
index 000000000..8833ea31b
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Windows/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Windows/App.xaml.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Windows/App.xaml.cs
new file mode 100644
index 000000000..10bfdbf4c
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/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 MandelbrotAnimation.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/MandelbrotAnimation/MandelbrotAnimation/Platforms/Windows/Package.appxmanifest b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 000000000..1494a4cac
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+ $placeholder$
+ User Name
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Windows/app.manifest b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Windows/app.manifest
new file mode 100644
index 000000000..ede33a0f4
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/Windows/app.manifest
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/iOS/AppDelegate.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 000000000..897dde96d
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace MandelbrotAnimation;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/iOS/Info.plist b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/iOS/Info.plist
new file mode 100644
index 000000000..358337bbd
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/iOS/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/iOS/Program.cs b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/iOS/Program.cs
new file mode 100644
index 000000000..843c1954d
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Platforms/iOS/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace MandelbrotAnimation;
+
+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/MandelbrotAnimation/MandelbrotAnimation/Properties/launchSettings.json b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Properties/launchSettings.json
new file mode 100644
index 000000000..90f92d965
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "MsixPackage",
+ "nativeDebugging": false
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/AppIcon/appicon.svg b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/AppIcon/appicon.svg
new file mode 100644
index 000000000..49f980057
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/AppIcon/appicon.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/AppIcon/appiconfg.svg b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 000000000..e9b7139de
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Fonts/OpenSans-Regular.ttf b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 000000000..04a336a52
Binary files /dev/null and b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Fonts/OpenSans-Semibold.ttf b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 000000000..1df30e6c0
Binary files /dev/null and b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Images/dotnet_bot.svg b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Images/dotnet_bot.svg
new file mode 100644
index 000000000..e19b01271
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Images/dotnet_bot.svg
@@ -0,0 +1,95 @@
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Raw/AboutAssets.txt b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Raw/AboutAssets.txt
new file mode 100644
index 000000000..29b76b2ea
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/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/MandelbrotAnimation/MandelbrotAnimation/Resources/Splash/splash.svg b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Splash/splash.svg
new file mode 100644
index 000000000..4b713836f
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Splash/splash.svg
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Styles/Colors.xaml b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Styles/Colors.xaml
new file mode 100644
index 000000000..66867878f
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/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/MandelbrotAnimation/MandelbrotAnimation/Resources/Styles/Styles.xaml b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Styles/Styles.xaml
new file mode 100644
index 000000000..d45ca5bad
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/MandelbrotAnimation/Resources/Styles/Styles.xaml
@@ -0,0 +1,409 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/README.md b/8.0/SkiaSharp/MandelbrotAnimation/README.md
new file mode 100644
index 000000000..06d960e96
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/README.md
@@ -0,0 +1,71 @@
+---
+name: .NET MAUI - Mandelbrot Animation
+description: "This sample demonstrates bitmap animation using SkiaSharp in a .NET MAUI app."
+page_type: sample
+languages:
+- csharp
+- xaml
+products:
+- dotnet-maui
+urlFragment: skiasharpmaui-mandelbrotanimation
+---
+
+# Mandelbrot Animation
+
+This sample demonstrates the use of SkiaSharp bitmap animations in a .NET Multi-platform App UI (.NET MAUI) app. Running the sample requires some patience because it first needs to create up to 50 bitmaps of various zoom levels of the famous Mandelbrot Set. After that's finished, however, the program animates those bitmaps to simulate a continuous zoom.
+
+![Mandelbrot Animation app screenshot](Screenshots/MandelbrotAnimation.png "Mandelbrot Animation app screenshot")
+
+As the sample is creating the 50 bitmaps, it stores them in application local storage as PNG files. This allows the sample to access those PNG files the next time that you run the program, so you don't have to wait for them to be created. However, these 50 bitmaps occupy over 20 megabytes of storage on your device.
+
+The *MainPage.xaml.cs* file begins with several constants that you can change:
+
+- The `COUNT` constant indicates the number of bitmaps in the animation. It is initially set to 10, but you can set it to any value up to 50. Setting it to values beyond 50 adds very little, however, because at that zoom level the algorithm runs into problems caused by the resolution of double-precision floating point numbers.
+- The `BITMAP_SIZE` constant indicates the square size of the bitmap. It is set to 1000 to create bitmaps of 1000-by-1000 pixels.
+- The `center` field indicates the `Complex` point that the program zooms in on. There are three possible values in the sample,.
+
+The bitmaps that the sample saves in local application storage incorporate the `center` value in their filenames. This means that if you run the sample with one `Complex` point, and then change `center` and run it again with another `Complex` point, the app's local storage will contain bitmaps for both points. A `Label` at the lower-left corner of the program displays the total storage space of all the bitmaps created by the sample.
+
+At any time while the program is running, you can click the **Delete All** button in the lower-right corner of the program to delete all the bitmaps. You can even do this as the sample is animating the bitmaps, because at that point, all the bitmaps have been loaded into memory. Uninstalling the program from the device also clears the app's local storage.
+
+When you first run the program, the `Label` at the top of the program shows which bitmap is being created. The `ProgressBar` indicates the progress of the Mandelbrot algorithm. Each successive bitmap is another level of zoom - it displays 1/2 of the width and height of a square of the complex plane as the previous bitmap. Once the sample has all the bitmaps created and loaded into memory, the `Label` and `ProgressBar` show the zoom level of the bitmap being displayed, and the degree to which that bitmap is zoomed.
+
+> [!NOTE]
+> On some devices, the animation runs smoother if the sample is not being run under control of Visual Studio's debugger.
+
+## Use SkiaSharp in an app
+
+To use SkiaSharp in your .NET MAUI app you should:
+
+1. Add the `SkiaSharp.Views.Maui.Controls` NuGet package to your app. This will also install dependent SkiaSharp packages.
+1. Initialize SkiaSharp in your app by calling the `UseSkiaSharp` method on the `MauiAppBuilder` object in your `MauiProgram` class:
+
+
+```csharp
+using Microsoft.Extensions.Logging;
+using SkiaSharp.Views.Maui.Controls.Hosting;
+
+namespace MyMauiApp;
+
+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/MandelbrotAnimation/Screenshots/MandelbrotAnimation.png b/8.0/SkiaSharp/MandelbrotAnimation/Screenshots/MandelbrotAnimation.png
new file mode 100644
index 000000000..e184d57c9
Binary files /dev/null and b/8.0/SkiaSharp/MandelbrotAnimation/Screenshots/MandelbrotAnimation.png differ
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle.sln b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle.sln
new file mode 100644
index 000000000..416049053
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle.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}") = "PhotoPuzzle", "PhotoPuzzle\PhotoPuzzle.csproj", "{78AD9A54-ABCF-4D57-AD18-9E5A5904E746}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {78AD9A54-ABCF-4D57-AD18-9E5A5904E746}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {78AD9A54-ABCF-4D57-AD18-9E5A5904E746}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {78AD9A54-ABCF-4D57-AD18-9E5A5904E746}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {78AD9A54-ABCF-4D57-AD18-9E5A5904E746}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {78AD9A54-ABCF-4D57-AD18-9E5A5904E746}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1D5F87EE-78D1-4BF8-B75C-AE4318891AA9}
+ EndGlobalSection
+EndGlobal
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/App.xaml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/App.xaml
new file mode 100644
index 000000000..4dea453b4
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/App.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/App.xaml.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/App.xaml.cs
new file mode 100644
index 000000000..f1af5eafa
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/App.xaml.cs
@@ -0,0 +1,12 @@
+namespace PhotoPuzzle;
+
+public partial class App : Application
+{
+ public App()
+ {
+ InitializeComponent();
+
+ MainPage = new AppShell();
+ }
+}
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/AppShell.xaml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/AppShell.xaml
new file mode 100644
index 000000000..eae457552
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/AppShell.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/AppShell.xaml.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/AppShell.xaml.cs
new file mode 100644
index 000000000..e3a2e7bac
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/AppShell.xaml.cs
@@ -0,0 +1,10 @@
+namespace PhotoPuzzle;
+
+public partial class AppShell : Shell
+{
+ public AppShell()
+ {
+ InitializeComponent();
+ }
+}
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/BitmapExtensions.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/BitmapExtensions.cs
new file mode 100644
index 000000000..c86539eaf
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/BitmapExtensions.cs
@@ -0,0 +1,100 @@
+using SkiaSharp;
+
+namespace PhotoPuzzle
+{
+ static class BitmapExtensions
+ {
+ 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);
+ }
+ }
+
+ 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/PhotoPuzzle/PhotoPuzzle/CroppingRectangle.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/CroppingRectangle.cs
new file mode 100644
index 000000000..21b904b07
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/CroppingRectangle.cs
@@ -0,0 +1,141 @@
+using SkiaSharp;
+
+namespace PhotoPuzzle
+{
+ 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/PhotoPuzzle/PhotoPuzzle/MauiProgram.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/MauiProgram.cs
new file mode 100644
index 000000000..a0f44d73c
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/MauiProgram.cs
@@ -0,0 +1,27 @@
+using Microsoft.Extensions.Logging;
+using SkiaSharp.Views.Maui.Controls.Hosting;
+
+namespace PhotoPuzzle;
+
+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/PhotoPuzzle/PhotoPuzzle/Pages/CropPhotoPage.xaml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/CropPhotoPage.xaml
new file mode 100644
index 000000000..6123583e1
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/CropPhotoPage.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/CropPhotoPage.xaml.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/CropPhotoPage.xaml.cs
new file mode 100644
index 000000000..e3c01a657
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/CropPhotoPage.xaml.cs
@@ -0,0 +1,51 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace PhotoPuzzle;
+
+public partial class CropPhotoPage : ContentPage
+{
+ PhotoCropperCanvasView photoCropper;
+
+ public CropPhotoPage(SKBitmap bitmap)
+ {
+ InitializeComponent();
+
+ photoCropper = new PhotoCropperCanvasView(bitmap, 1f);
+ canvasViewHost.Add(photoCropper);
+ }
+
+ async void OnDoneButtonClicked(object sender, EventArgs args)
+ {
+ SKBitmap croppedBitmap = photoCropper.CroppedBitmap;
+ int width = croppedBitmap.Width / 4;
+ int height = croppedBitmap.Height / 4;
+
+ ImageSource[] imgSources = new ImageSource[15];
+
+ for (int row = 0; row < 4; row++)
+ {
+ for (int col = 0; col < 4; col++)
+ {
+ // Skip the last one!
+ if (row == 3 && col == 3)
+ break;
+
+ // Create a bitmap 1/4 the width and height of the original
+ SKBitmap bitmap = new SKBitmap(width, height);
+ SKRect dest = new SKRect(0, 0, width, height);
+ SKRect source = new SKRect(col * width, row * height, (col + 1) * width, (row + 1) * height);
+
+ // Copy 1/16 of the original into that bitmap
+ using (SKCanvas canvas = new SKCanvas(bitmap))
+ {
+ canvas.DrawBitmap(croppedBitmap, source, dest);
+ }
+
+ imgSources[4 * row + col] = (SKBitmapImageSource)bitmap;
+ }
+ }
+
+ await Navigation.PushAsync(new PhotoPuzzlePage(imgSources));
+ }
+}
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PhotoPuzzlePage.xaml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PhotoPuzzlePage.xaml
new file mode 100644
index 000000000..6539602f5
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PhotoPuzzlePage.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PhotoPuzzlePage.xaml.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PhotoPuzzlePage.xaml.cs
new file mode 100644
index 000000000..77da02cc9
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PhotoPuzzlePage.xaml.cs
@@ -0,0 +1,162 @@
+namespace PhotoPuzzle;
+
+public partial class PhotoPuzzlePage : ContentPage
+{
+ // Number of tiles horizontally and vertically.
+ static readonly int NUM = 4;
+
+ // Array of tiles, and empty row & column.
+ PhotoPuzzleTile[,] tiles = new PhotoPuzzleTile[NUM, NUM];
+ int emptyRow = NUM - 1;
+ int emptyCol = NUM - 1;
+
+ double tileSize;
+ bool isBusy;
+
+ public PhotoPuzzlePage(ImageSource[] imageSources)
+ {
+ InitializeComponent();
+
+ // Loop through the rows and columns.
+ for (int row = 0; row < NUM; row++)
+ {
+ for (int col = 0; col < NUM; col++)
+ {
+ // But skip the last one!
+ if (row == NUM - 1 && col == NUM - 1)
+ break;
+
+ // Get the bitmap for each tile and instantiate it.
+ ImageSource imageSource = imageSources[NUM * row + col];
+
+ PhotoPuzzleTile tile = new PhotoPuzzleTile(row, col, imageSource);
+
+ // Add tap recognition.
+ TapGestureRecognizer tapGestureRecognizer = new TapGestureRecognizer
+ {
+ Command = new Command(OnTileTapped),
+ CommandParameter = tile
+ };
+ tile.GestureRecognizers.Add(tapGestureRecognizer);
+
+ // Add it to the array and the AbsoluteLayout.
+ tiles[row, col] = tile;
+ absoluteLayout.Children.Add(tile);
+ }
+ }
+ }
+
+ void OnContentViewSizeChanged(object sender, EventArgs args)
+ {
+ ContentView contentView = (ContentView)sender;
+ double width = contentView.Width;
+ double height = contentView.Height;
+
+ if (width <= 0 || height <= 0)
+ return;
+
+ // Orient StackLayout based on portrait/landscape mode.
+ stackLayout.Orientation = (width < height) ? StackOrientation.Vertical :
+ StackOrientation.Horizontal;
+
+ // Calculate tile size and position based on ContentView size.
+ tileSize = Math.Min(width, height) / NUM;
+ absoluteLayout.WidthRequest = NUM * tileSize;
+ absoluteLayout.HeightRequest = NUM * tileSize;
+
+ foreach (View view in absoluteLayout.Children)
+ {
+ PhotoPuzzleTile tile = (PhotoPuzzleTile)view;
+
+ // Set tile bounds.
+ AbsoluteLayout.SetLayoutBounds(tile, new Rect(tile.Col * tileSize,
+ tile.Row * tileSize,
+ tileSize,
+ tileSize));
+ }
+ }
+
+ async void OnTileTapped(object parameter)
+ {
+ if (isBusy)
+ return;
+
+ isBusy = true;
+ PhotoPuzzleTile tappedTile = (PhotoPuzzleTile)parameter;
+ await ShiftIntoEmpty(tappedTile.Row, tappedTile.Col);
+ isBusy = false;
+ }
+
+ async Task ShiftIntoEmpty(int tappedRow, int tappedCol, uint length = 100)
+ {
+ // Shift columns.
+ if (tappedRow == emptyRow && tappedCol != emptyCol)
+ {
+ int inc = Math.Sign(tappedCol - emptyCol);
+ int begCol = emptyCol + inc;
+ int endCol = tappedCol + inc;
+
+ for (int col = begCol; col != endCol; col += inc)
+ {
+ await AnimateTile(emptyRow, col, emptyRow, emptyCol, length);
+ }
+ }
+ // Shift rows.
+ else if (tappedCol == emptyCol && tappedRow != emptyRow)
+ {
+ int inc = Math.Sign(tappedRow - emptyRow);
+ int begRow = emptyRow + inc;
+ int endRow = tappedRow + inc;
+
+ for (int row = begRow; row != endRow; row += inc)
+ {
+ await AnimateTile(row, emptyCol, emptyRow, emptyCol, length);
+ }
+ }
+ }
+
+ async Task AnimateTile(int row, int col, int newRow, int newCol, uint length)
+ {
+ // The tile to be animated.
+ PhotoPuzzleTile animaTile = tiles[row, col];
+
+ // The destination rectangle.
+ Rect rect = new Rect(emptyCol * tileSize,
+ emptyRow * tileSize,
+ tileSize,
+ tileSize);
+
+ // Animate it!
+ await animaTile.LayoutTo(rect, length);
+
+ // Set layout bounds to same Rectangle.
+ AbsoluteLayout.SetLayoutBounds(animaTile, rect);
+
+ // Set several variables and properties for new layout.
+ tiles[newRow, newCol] = animaTile;
+ animaTile.Row = newRow;
+ animaTile.Col = newCol;
+ tiles[row, col] = null;
+ emptyRow = row;
+ emptyCol = col;
+ }
+
+ async void OnRandomizeButtonClicked(object sender, EventArgs args)
+ {
+ Button button = (Button)sender;
+ button.IsEnabled = false;
+ Random rand = new Random();
+
+ isBusy = true;
+
+ // Simulate some fast crazy taps.
+ for (int i = 0; i < 100; i++)
+ {
+ await ShiftIntoEmpty(rand.Next(NUM), emptyCol, 25);
+ await ShiftIntoEmpty(emptyRow, rand.Next(NUM), 25);
+ }
+ button.IsEnabled = true;
+
+ isBusy = false;
+ }
+}
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PickPhotoPage.xaml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PickPhotoPage.xaml
new file mode 100644
index 000000000..e31b23891
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PickPhotoPage.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PickPhotoPage.xaml.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PickPhotoPage.xaml.cs
new file mode 100644
index 000000000..c4786cebe
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/PickPhotoPage.xaml.cs
@@ -0,0 +1,28 @@
+using SkiaSharp;
+
+namespace PhotoPuzzle;
+
+public partial class PickPhotoPage : ContentPage
+{
+ public PickPhotoPage()
+ {
+ InitializeComponent();
+ }
+
+ async void OnPickButtonClicked(object sender, EventArgs args)
+ {
+ // Load bitmap from photo library
+ FileResult photo = await MediaPicker.Default.PickPhotoAsync();
+ if (photo != null)
+ {
+ using (Stream stream = await photo.OpenReadAsync())
+ {
+ if (stream != null)
+ {
+ SKBitmap bitmap = SKBitmap.Decode(stream);
+ await Navigation.PushAsync(new RotatePhotoPage(bitmap));
+ }
+ }
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/RotatePhotoPage.xaml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/RotatePhotoPage.xaml
new file mode 100644
index 000000000..2595a585b
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/RotatePhotoPage.xaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/RotatePhotoPage.xaml.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/RotatePhotoPage.xaml.cs
new file mode 100644
index 000000000..9cd046dc6
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Pages/RotatePhotoPage.xaml.cs
@@ -0,0 +1,63 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+
+namespace PhotoPuzzle;
+
+public partial class RotatePhotoPage : ContentPage
+{
+ SKBitmap bitmap;
+
+ public RotatePhotoPage(SKBitmap bitmap)
+ {
+ this.bitmap = bitmap;
+
+ 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 OnRotateRightButtonClicked(object sender, EventArgs args)
+ {
+ SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);
+
+ using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
+ {
+ canvas.Clear();
+ canvas.Translate(bitmap.Height, 0);
+ canvas.RotateDegrees(90);
+ canvas.DrawBitmap(bitmap, new SKPoint());
+ }
+
+ bitmap = rotatedBitmap;
+ canvasView.InvalidateSurface();
+ }
+
+ void OnRotateLeftButtonClicked(object sender, EventArgs args)
+ {
+ SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);
+
+ using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
+ {
+ canvas.Clear();
+ canvas.Translate(0, bitmap.Width);
+ canvas.RotateDegrees(-90);
+ canvas.DrawBitmap(bitmap, new SKPoint());
+ }
+
+ bitmap = rotatedBitmap;
+ canvasView.InvalidateSurface();
+ }
+
+ async void OnDoneButtonClicked(object sender, EventArgs args)
+ {
+ await Navigation.PushAsync(new CropPhotoPage(bitmap));
+ }
+}
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/PhotoCropperCanvasView.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/PhotoCropperCanvasView.cs
new file mode 100644
index 000000000..7ad98cdf5
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/PhotoCropperCanvasView.cs
@@ -0,0 +1,177 @@
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace PhotoPuzzle
+{
+ 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 = bitmapScaleMatrix.PostConcat(SKMatrix.CreateScaleTranslation(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/PhotoPuzzle/PhotoPuzzle/PhotoPuzzle.csproj b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/PhotoPuzzle.csproj
new file mode 100644
index 000000000..8e8bd37ee
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/PhotoPuzzle.csproj
@@ -0,0 +1,66 @@
+
+
+
+ net8.0-android;net8.0-ios;net8.0-maccatalyst
+ $(TargetFrameworks);net8.0-windows10.0.19041.0
+
+
+
+
+
+
+ Exe
+ PhotoPuzzle
+ true
+ true
+ enable
+ enable
+
+
+ PhotoPuzzle
+
+
+ com.companyname.photopuzzle
+
+
+ 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/PhotoPuzzle/PhotoPuzzle/PhotoPuzzleTile.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/PhotoPuzzleTile.cs
new file mode 100644
index 000000000..dd2876aab
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/PhotoPuzzleTile.cs
@@ -0,0 +1,21 @@
+namespace PhotoPuzzle
+{
+ class PhotoPuzzleTile : ContentView
+ {
+ public int Row { get; set; }
+ public int Col { get; set; }
+
+ public PhotoPuzzleTile(int row, int col, ImageSource imageSource)
+ {
+ Row = row;
+ Col = col;
+
+ Padding = new Thickness(1);
+ Content = new Image
+ {
+ Source = imageSource
+ };
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Android/AndroidManifest.xml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 000000000..30233c173
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Android/MainActivity.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Android/MainActivity.cs
new file mode 100644
index 000000000..f3c17a1b0
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Android/MainActivity.cs
@@ -0,0 +1,11 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace PhotoPuzzle;
+
+[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/PhotoPuzzle/PhotoPuzzle/Platforms/Android/MainApplication.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Android/MainApplication.cs
new file mode 100644
index 000000000..26a1c34a0
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Android/MainApplication.cs
@@ -0,0 +1,16 @@
+using Android.App;
+using Android.Runtime;
+
+namespace PhotoPuzzle;
+
+[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/PhotoPuzzle/PhotoPuzzle/Platforms/Android/Resources/values/colors.xml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 000000000..3b8595ecd
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #512BD4
+ #2B0B98
+ #2B0B98
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/AppDelegate.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/AppDelegate.cs
new file mode 100644
index 000000000..94111fbfa
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace PhotoPuzzle;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/Entitlements.plist b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/Entitlements.plist
new file mode 100644
index 000000000..8e87c0cb0
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/Entitlements.plist
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+ com.apple.security.network.client
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/Info.plist b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/Info.plist
new file mode 100644
index 000000000..a28cb0e5c
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/Info.plist
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UIDeviceFamily
+
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+ NSPhotoLibraryAddUsageDescription
+ This app needs access to the photo gallery for picking photos and videos.
+ NSPhotoLibraryUsageDescription
+ This app needs access to the photo gallery for picking photos and videos.
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/Program.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/Program.cs
new file mode 100644
index 000000000..cc7bc1faf
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/MacCatalyst/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace PhotoPuzzle;
+
+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/PhotoPuzzle/PhotoPuzzle/Platforms/Tizen/Main.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Tizen/Main.cs
new file mode 100644
index 000000000..e75da3915
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Tizen/Main.cs
@@ -0,0 +1,17 @@
+using System;
+using Microsoft.Maui;
+using Microsoft.Maui.Hosting;
+
+namespace PhotoPuzzle;
+
+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/PhotoPuzzle/PhotoPuzzle/Platforms/Tizen/tizen-manifest.xml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Tizen/tizen-manifest.xml
new file mode 100644
index 000000000..7a13d0eeb
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Tizen/tizen-manifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ maui-appicon-placeholder
+
+
+
+
+ http://tizen.org/privilege/internet
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Windows/App.xaml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Windows/App.xaml
new file mode 100644
index 000000000..92f11cbd5
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Windows/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Windows/App.xaml.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Windows/App.xaml.cs
new file mode 100644
index 000000000..536be3de2
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/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 PhotoPuzzle.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/PhotoPuzzle/PhotoPuzzle/Platforms/Windows/Package.appxmanifest b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 000000000..0d4671afc
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+ $placeholder$
+ User Name
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Windows/app.manifest b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Windows/app.manifest
new file mode 100644
index 000000000..6bc0a9406
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/Windows/app.manifest
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/iOS/AppDelegate.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 000000000..94111fbfa
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace PhotoPuzzle;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/iOS/Info.plist b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/iOS/Info.plist
new file mode 100644
index 000000000..11d122a71
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/iOS/Info.plist
@@ -0,0 +1,36 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+ NSPhotoLibraryAddUsageDescription
+ This app needs access to the photo gallery for picking photos and videos.
+ NSPhotoLibraryUsageDescription
+ This app needs access to the photo gallery for picking photos and videos.
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/iOS/Program.cs b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/iOS/Program.cs
new file mode 100644
index 000000000..cc7bc1faf
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Platforms/iOS/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace PhotoPuzzle;
+
+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/PhotoPuzzle/PhotoPuzzle/Properties/launchSettings.json b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Properties/launchSettings.json
new file mode 100644
index 000000000..90f92d965
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "MsixPackage",
+ "nativeDebugging": false
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/AppIcon/appicon.svg b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/AppIcon/appicon.svg
new file mode 100644
index 000000000..49f980057
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/AppIcon/appicon.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/AppIcon/appiconfg.svg b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 000000000..e9b7139de
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Fonts/OpenSans-Regular.ttf b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 000000000..04a336a52
Binary files /dev/null and b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Fonts/OpenSans-Semibold.ttf b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 000000000..1df30e6c0
Binary files /dev/null and b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Images/dotnet_bot.svg b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Images/dotnet_bot.svg
new file mode 100644
index 000000000..e19b01271
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Images/dotnet_bot.svg
@@ -0,0 +1,95 @@
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Raw/AboutAssets.txt b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Raw/AboutAssets.txt
new file mode 100644
index 000000000..29b76b2ea
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/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/PhotoPuzzle/PhotoPuzzle/Resources/Splash/splash.svg b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Splash/splash.svg
new file mode 100644
index 000000000..4b713836f
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Splash/splash.svg
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Styles/Colors.xaml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Styles/Colors.xaml
new file mode 100644
index 000000000..66867878f
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/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/PhotoPuzzle/PhotoPuzzle/Resources/Styles/Styles.xaml b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Styles/Styles.xaml
new file mode 100644
index 000000000..d45ca5bad
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/PhotoPuzzle/Resources/Styles/Styles.xaml
@@ -0,0 +1,409 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/PhotoPuzzle/README.md b/8.0/SkiaSharp/PhotoPuzzle/README.md
new file mode 100644
index 000000000..b3a0b51f3
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/README.md
@@ -0,0 +1,56 @@
+---
+name: .NET MAUI - Photo Puzzle
+description: "Photo Puzzle is a variation of the classic 14-15 puzzle, written using .NET MAUI and SkiaSharp."
+page_type: sample
+languages:
+- csharp
+- xaml
+products:
+- dotnet-maui
+urlFragment: skiasharpmaui-photopuzzle
+---
+
+# Photo Puzzle
+
+This sample demonstrates the use of SkiaSharp in a .NET Multi-platform App UI (.NET MAUI) app. It's a variation of the classic 15 puzzle. On the early Mac, the 15 puzzle was called PUZZLE. In early Windows, it was the only sample for Microsoft Pascal for Windows 1.0, where it was called MUZZLE (for "Microsoft Puzzle"). This is the .NET MAUI version.
+
+![Photo Puzzle app screenshot](Screenshots/PhotoPuzzle-randomized.png "Photo Puzzle app screenshot")
+
+After pressing the **Randomize** button, just tap a tile to move it into an empty position. You can *tap* any tile in the row or column of the empty position to move one, two, or three tiles at once. Use the numbers in the lower-right corner of each tile as a guide.
+
+## Use SkiaSharp in an app
+
+To use SkiaSharp in your .NET MAUI app you should:
+
+1. Add the `SkiaSharp.Views.Maui.Controls` NuGet package to your app. This will also install dependent SkiaSharp packages.
+1. Initialize SkiaSharp in your app by calling the `UseSkiaSharp` method on the `MauiAppBuilder` object in your `MauiProgram` class:
+
+
+```csharp
+using Microsoft.Extensions.Logging;
+using SkiaSharp.Views.Maui.Controls.Hosting;
+
+namespace MyMauiApp;
+
+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/PhotoPuzzle/Screenshots/PhotoPuzzle-completed.png b/8.0/SkiaSharp/PhotoPuzzle/Screenshots/PhotoPuzzle-completed.png
new file mode 100644
index 000000000..e7d549ed8
Binary files /dev/null and b/8.0/SkiaSharp/PhotoPuzzle/Screenshots/PhotoPuzzle-completed.png differ
diff --git a/8.0/SkiaSharp/PhotoPuzzle/Screenshots/PhotoPuzzle-randomized.png b/8.0/SkiaSharp/PhotoPuzzle/Screenshots/PhotoPuzzle-randomized.png
new file mode 100644
index 000000000..2a4df72b6
Binary files /dev/null and b/8.0/SkiaSharp/PhotoPuzzle/Screenshots/PhotoPuzzle-randomized.png differ
diff --git a/8.0/SkiaSharp/SkiaSharpDemos/README.md b/8.0/SkiaSharp/SkiaSharpDemos/README.md
new file mode 100644
index 000000000..4d5b4bb5e
--- /dev/null
+++ b/8.0/SkiaSharp/SkiaSharpDemos/README.md
@@ -0,0 +1,54 @@
+---
+name: .NET MAUI - SkiaSharp
+description: "This sample demonstrates the use of SkiaSharp in a .NET MAUI app."
+page_type: sample
+languages:
+- csharp
+- xaml
+products:
+- dotnet-maui
+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.
+
+## Use SkiaSharp in an app
+
+To use SkiaSharp in your .NET MAUI app you should:
+
+1. Add the `SkiaSharp.Views.Maui.Controls` NuGet package to your app. This will also install dependent SkiaSharp packages.
+1. Initialize SkiaSharp in your app by calling the `UseSkiaSharp` method on the `MauiAppBuilder` object in your `MauiProgram` class:
+
+
+```csharp
+using Microsoft.Extensions.Logging;
+using SkiaSharp.Views.Maui.Controls.Hosting;
+
+namespace MyMauiApp;
+
+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.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();
+ }
+ }
+ }
+}
+
diff --git a/8.0/SkiaSharp/SpinPaint/README.md b/8.0/SkiaSharp/SpinPaint/README.md
new file mode 100644
index 000000000..f853759fe
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/README.md
@@ -0,0 +1,54 @@
+---
+name: .NET MAUI - SpinPaint
+description: "SpinPaint is a .NET MAUI sample that simulates a revolving disk that you can paint on by touching and moving your fingers."
+page_type: sample
+languages:
+- csharp
+- xaml
+products:
+- dotnet-maui
+urlFragment: skiasharpmaui-spinpaint
+---
+
+# SpinPaint
+
+SpinPaint demonstrates the use of SkiaSharp in a .NET Multi-platform App UI (.NET MAUI) app. It simulates a revolving disk that you can paint on by touching and moving your fingers. SpinPaint responds to touch by painting a line under your finger, but it also duplicates that line in three mirror images in the other three quadrants of the disk. The current color of the line is indicated by the color of the title of the program above the disk.
+
+![SpinPaint app screenshot](Screenshots/SpinPaint.png "SpinPaint app screenshot")
+
+## Use SkiaSharp in an app
+
+To use SkiaSharp in your .NET MAUI app you should:
+
+1. Add the `SkiaSharp.Views.Maui.Controls` NuGet package to your app. This will also install dependent SkiaSharp packages.
+1. Initialize SkiaSharp in your app by calling the `UseSkiaSharp` method on the `MauiAppBuilder` object in your `MauiProgram` class:
+
+
+```csharp
+using Microsoft.Extensions.Logging;
+using SkiaSharp.Views.Maui.Controls.Hosting;
+
+namespace MyMauiApp;
+
+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/SpinPaint/Screenshots/SpinPaint.png b/8.0/SkiaSharp/SpinPaint/Screenshots/SpinPaint.png
new file mode 100644
index 000000000..bf455c488
Binary files /dev/null and b/8.0/SkiaSharp/SpinPaint/Screenshots/SpinPaint.png differ
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint.sln b/8.0/SkiaSharp/SpinPaint/SpinPaint.sln
new file mode 100644
index 000000000..6832bb310
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint.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}") = "SpinPaint", "SpinPaint\SpinPaint.csproj", "{E5BDA1A6-D804-447D-8EC2-72A96AC8AC75}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {E5BDA1A6-D804-447D-8EC2-72A96AC8AC75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E5BDA1A6-D804-447D-8EC2-72A96AC8AC75}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E5BDA1A6-D804-447D-8EC2-72A96AC8AC75}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {E5BDA1A6-D804-447D-8EC2-72A96AC8AC75}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E5BDA1A6-D804-447D-8EC2-72A96AC8AC75}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {5E381975-60F0-415E-A31A-F7854FE27EB8}
+ EndGlobalSection
+EndGlobal
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/App.xaml b/8.0/SkiaSharp/SpinPaint/SpinPaint/App.xaml
new file mode 100644
index 000000000..044f0fea8
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/App.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/App.xaml.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/App.xaml.cs
new file mode 100644
index 000000000..c01dca53a
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/App.xaml.cs
@@ -0,0 +1,12 @@
+namespace SpinPaint;
+
+public partial class App : Application
+{
+ public App()
+ {
+ InitializeComponent();
+
+ MainPage = new AppShell();
+ }
+}
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/AppShell.xaml b/8.0/SkiaSharp/SpinPaint/SpinPaint/AppShell.xaml
new file mode 100644
index 000000000..8f61eb7e4
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/AppShell.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/AppShell.xaml.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/AppShell.xaml.cs
new file mode 100644
index 000000000..5a6b18713
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/AppShell.xaml.cs
@@ -0,0 +1,10 @@
+namespace SpinPaint;
+
+public partial class AppShell : Shell
+{
+ public AppShell()
+ {
+ InitializeComponent();
+ }
+}
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/MainPage.xaml b/8.0/SkiaSharp/SpinPaint/SpinPaint/MainPage.xaml
new file mode 100644
index 000000000..7b61b6e8c
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/MainPage.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/MainPage.xaml.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/MainPage.xaml.cs
new file mode 100644
index 000000000..7d55a3c8f
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/MainPage.xaml.cs
@@ -0,0 +1,245 @@
+using System.Diagnostics;
+using SkiaSharp;
+using SkiaSharp.Views.Maui;
+using SkiaSharp.Views.Maui.Controls;
+
+namespace SpinPaint;
+
+public partial class MainPage : ContentPage
+{
+ // These should be contrasting colors
+ static readonly SKColor backgroundColor = SKColors.Black;
+ static readonly SKColor crossHairColor = SKColors.White;
+
+ // Current bitmap being drawn upon by user
+ SKBitmap bitmap;
+ SKCanvas bitmapCanvas;
+ int bitmapSize; // bitmaps used here are always square
+
+ // SKPaint for user drawings on bitmap
+ SKPaint fingerPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = 10,
+ StrokeCap = SKStrokeCap.Round,
+ };
+
+ // SKPaint for crosshairs on bitmap
+ SKPaint thinLinePaint = new SKPaint
+ {
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = 1,
+ Color = crossHairColor
+ };
+
+ // SKPath for clipping drawings to circle
+ SKPath clipPath = new SKPath();
+
+ // Animation helpers
+ Stopwatch stopwatch = new Stopwatch();
+ float angle;
+
+ // Item to store in touch-tracking dictionary
+ class FingerInfo
+ {
+ public SKPoint ThisPosition;
+ public SKPoint LastPosition;
+ }
+
+ // Touch-tracking dictionary for tracking multiple fingers
+ Dictionary idDictionary = new Dictionary();
+
+ public MainPage()
+ {
+ InitializeComponent();
+
+ // Start animation
+ stopwatch.Start();
+ Dispatcher.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
+ }
+
+ // For each touch event, simply store information in idDictionary.
+ // Do not draw at this time!
+ void OnTouch(object sender, SKTouchEventArgs e)
+ {
+ switch (e.ActionType)
+ {
+ case SKTouchAction.Pressed:
+ if (e.InContact)
+ {
+ idDictionary.Add(e.Id, new FingerInfo
+ {
+ ThisPosition = e.Location,
+ LastPosition = new SKPoint(float.PositiveInfinity, float.PositiveInfinity)
+ });
+ }
+ break;
+ case SKTouchAction.Moved:
+ if (idDictionary.ContainsKey(e.Id))
+ {
+ idDictionary[e.Id].ThisPosition = e.Location;
+ }
+ break;
+ case SKTouchAction.Released:
+ case SKTouchAction.Cancelled:
+ if (idDictionary.ContainsKey(e.Id))
+ {
+ idDictionary.Remove(e.Id);
+ }
+ break;
+ }
+
+ e.Handled = true;
+ }
+
+ // Every 1/60th second, update bitmap with user drawings
+ bool OnTimerTick()
+ {
+ if (bitmap == null)
+ {
+ return true;
+ }
+
+ // Determine the current color.
+ float tColor = stopwatch.ElapsedMilliseconds % 10000 / 10000f;
+ fingerPaint.Color = SKColor.FromHsl(360 * tColor, 100, 50);
+ titleLabel.TextColor = fingerPaint.Color.ToMauiColor();
+
+ // Determine the rotation angle.
+ float tAngle = stopwatch.ElapsedMilliseconds % 5000 / 5000f;
+ angle = 360 * tAngle;
+ SKMatrix matrix = SKMatrix.CreateRotationDegrees(-angle, bitmap.Width / 2, bitmap.Height / 2);// SKMatrix.MakeRotationDegrees(-angle, bitmap.Width / 2, bitmap.Height / 2);
+
+ // Loop trough the fingers touching the screen.
+ foreach (long id in idDictionary.Keys)
+ {
+ FingerInfo fingerInfo = idDictionary[id];
+
+ // Get the canvas size in pixels. It's square so it's only one number.
+ float canvasSize = 0;
+ canvasSize = (canvasView as SKCanvasView).CanvasSize.Width;
+
+ // Convert .NET MAUI coordinates to pixels for drawing on the bitmap.
+ // Also, make an offset factor if there's been resizing and the bitmap
+ // is now larger than the canvas. (It's never smaller.)
+ float factor = canvasSize / (float)canvasView.Width; // scaling factor
+ float offset = (bitmapSize - canvasSize) / 2; // bitmap always >= canvas
+
+ SKPoint convertedPoint = new SKPoint(factor * (float)fingerInfo.ThisPosition.X + offset,
+ factor * (float)fingerInfo.ThisPosition.Y + offset);
+
+ // Now rotate the point based on the rotation angle
+ SKPoint pt0 = matrix.MapPoint(convertedPoint);
+
+ if (!float.IsPositiveInfinity(fingerInfo.LastPosition.X))
+ {
+ // Draw four lines in four quadrants.
+ SKPoint pt1 = fingerInfo.LastPosition;
+ bitmapCanvas.DrawLine(pt0.X, pt0.Y, pt1.X, pt1.Y, fingerPaint);
+
+ float x0Flip = bitmap.Width - pt0.X;
+ float y0Flip = bitmap.Height - pt0.Y;
+ float x1Flip = bitmap.Width - pt1.X;
+ float y1Flip = bitmap.Height - pt1.Y;
+
+ bitmapCanvas.DrawLine(x0Flip, pt0.Y, x1Flip, pt1.Y, fingerPaint);
+ bitmapCanvas.DrawLine(pt0.X, y0Flip, pt1.X, y1Flip, fingerPaint);
+ bitmapCanvas.DrawLine(x0Flip, y0Flip, x1Flip, y1Flip, fingerPaint);
+ }
+
+ // Save the current point for next time through.
+ fingerInfo.LastPosition = pt0;
+ }
+
+ // Redraw the canvas.
+ canvasView.InvalidateSurface();
+
+ return true;
+ }
+
+ void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ // Get the canvas
+ SKCanvas canvas = args.Surface.Canvas;
+
+ // These two dimensions should be the same.
+ int canvasSize = Math.Min(args.Info.Width, args.Info.Height);
+
+ // If bitmap does not exist, create it
+ if (bitmap == null)
+ {
+ // Set three fields
+ bitmapSize = canvasSize;
+ bitmap = new SKBitmap(bitmapSize, bitmapSize);
+ bitmapCanvas = new SKCanvas(bitmap);
+
+ // Establishes circular clipping and colors background
+ PrepBitmap(bitmapCanvas, bitmapSize);
+ }
+
+ // If the canvas has become larger, make a new bitmap of that size.
+ else if (bitmapSize < canvasSize)
+ {
+ // New versions of the three fields
+ int newBitmapSize = canvasSize;
+ SKBitmap newBitmap = new SKBitmap(newBitmapSize, newBitmapSize);
+ SKCanvas newBitmapCanvas = new SKCanvas(newBitmap);
+
+ // New circular clipping and background
+ PrepBitmap(newBitmapCanvas, newBitmapSize);
+
+ // Copy old bitmap to new bitmap
+ float diff = (newBitmapSize - bitmapSize) / 2f;
+ newBitmapCanvas.DrawBitmap(bitmap, diff, diff);
+
+ // Dispose old bitmap and its canvas
+ bitmapCanvas.Dispose();
+ bitmap.Dispose();
+
+ // Set fields to new values
+ bitmap = newBitmap;
+ bitmapCanvas = newBitmapCanvas;
+ bitmapSize = newBitmapSize;
+ }
+
+ // Clear the canvas
+ canvas.Clear(SKColors.White);
+
+ // Set the rotate transform
+ float radius = canvasSize / 2;
+ canvas.RotateDegrees(angle, radius, radius);
+
+ // Set a circular clipping area
+ clipPath.Reset();
+ clipPath.AddCircle(radius, radius, radius);
+ canvas.ClipPath(clipPath);
+
+ // Draw the bitmap
+ float offset = (canvasSize - bitmapSize) / 2f;
+ canvas.DrawBitmap(bitmap, offset, offset);
+
+ // Draw the cross hairs
+ canvas.DrawLine(radius, 0, radius, canvasSize, thinLinePaint);
+ canvas.DrawLine(0, radius, canvasSize, radius, thinLinePaint);
+ }
+
+ static void PrepBitmap(SKCanvas bitmapCanvas, int bitmapSize)
+ {
+ // Set clipping path based on bitmap size
+ using (SKPath bitmapClipPath = new SKPath())
+ {
+ bitmapClipPath.AddCircle(bitmapSize / 2, bitmapSize / 2, bitmapSize / 2);
+ bitmapCanvas.ClipPath(bitmapClipPath);
+ }
+
+ // Color the bitmap background
+ bitmapCanvas.Clear(backgroundColor);
+ }
+
+ // Clear the bitmap of all user drawings.
+ void OnClearButtonClicked(object sender, EventArgs args)
+ {
+ // Color the bitmap background, erasing all user drawing
+ bitmapCanvas.Clear(backgroundColor);
+ }
+}
\ No newline at end of file
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/MauiProgram.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/MauiProgram.cs
new file mode 100644
index 000000000..214306a42
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/MauiProgram.cs
@@ -0,0 +1,27 @@
+using Microsoft.Extensions.Logging;
+using SkiaSharp.Views.Maui.Controls.Hosting;
+
+namespace SpinPaint;
+
+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/SpinPaint/SpinPaint/Platforms/Android/AndroidManifest.xml b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 000000000..ddd284fbc
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Android/MainActivity.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Android/MainActivity.cs
new file mode 100644
index 000000000..7532a97e5
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Android/MainActivity.cs
@@ -0,0 +1,11 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace SpinPaint;
+
+[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/SpinPaint/SpinPaint/Platforms/Android/MainApplication.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Android/MainApplication.cs
new file mode 100644
index 000000000..d9ece4970
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Android/MainApplication.cs
@@ -0,0 +1,16 @@
+using Android.App;
+using Android.Runtime;
+
+namespace SpinPaint;
+
+[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/SpinPaint/SpinPaint/Platforms/Android/Resources/values/colors.xml b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 000000000..3b8595ecd
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #512BD4
+ #2B0B98
+ #2B0B98
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/MacCatalyst/AppDelegate.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/MacCatalyst/AppDelegate.cs
new file mode 100644
index 000000000..de74d8dde
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/MacCatalyst/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace SpinPaint;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/MacCatalyst/Entitlements.plist b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/MacCatalyst/Entitlements.plist
new file mode 100644
index 000000000..8e87c0cb0
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/MacCatalyst/Entitlements.plist
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+ com.apple.security.network.client
+
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/MacCatalyst/Info.plist b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/MacCatalyst/Info.plist
new file mode 100644
index 000000000..f24aacc0d
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/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/SpinPaint/SpinPaint/Platforms/MacCatalyst/Program.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/MacCatalyst/Program.cs
new file mode 100644
index 000000000..680a26b80
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/MacCatalyst/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace SpinPaint;
+
+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/SpinPaint/SpinPaint/Platforms/Tizen/Main.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Tizen/Main.cs
new file mode 100644
index 000000000..18fcae18a
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Tizen/Main.cs
@@ -0,0 +1,17 @@
+using System;
+using Microsoft.Maui;
+using Microsoft.Maui.Hosting;
+
+namespace SpinPaint;
+
+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/SpinPaint/SpinPaint/Platforms/Tizen/tizen-manifest.xml b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Tizen/tizen-manifest.xml
new file mode 100644
index 000000000..9b8a355ce
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Tizen/tizen-manifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ maui-appicon-placeholder
+
+
+
+
+ http://tizen.org/privilege/internet
+
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Windows/App.xaml b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Windows/App.xaml
new file mode 100644
index 000000000..7b7780893
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Windows/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Windows/App.xaml.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Windows/App.xaml.cs
new file mode 100644
index 000000000..d4755e069
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/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 SpinPaint.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/SpinPaint/SpinPaint/Platforms/Windows/Package.appxmanifest b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 000000000..03ec2a109
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+ $placeholder$
+ User Name
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Windows/app.manifest b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Windows/app.manifest
new file mode 100644
index 000000000..7f6da2c05
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/Windows/app.manifest
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/iOS/AppDelegate.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 000000000..de74d8dde
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace SpinPaint;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/iOS/Info.plist b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/iOS/Info.plist
new file mode 100644
index 000000000..358337bbd
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/iOS/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/iOS/Program.cs b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/iOS/Program.cs
new file mode 100644
index 000000000..680a26b80
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Platforms/iOS/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace SpinPaint;
+
+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/SpinPaint/SpinPaint/Properties/launchSettings.json b/8.0/SkiaSharp/SpinPaint/SpinPaint/Properties/launchSettings.json
new file mode 100644
index 000000000..90f92d965
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "MsixPackage",
+ "nativeDebugging": false
+ }
+ }
+}
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/AppIcon/appicon.svg b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/AppIcon/appicon.svg
new file mode 100644
index 000000000..49f980057
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/AppIcon/appicon.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/AppIcon/appiconfg.svg b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 000000000..e9b7139de
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Fonts/OpenSans-Regular.ttf b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 000000000..04a336a52
Binary files /dev/null and b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Fonts/OpenSans-Semibold.ttf b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 000000000..1df30e6c0
Binary files /dev/null and b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Images/dotnet_bot.svg b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Images/dotnet_bot.svg
new file mode 100644
index 000000000..e19b01271
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Images/dotnet_bot.svg
@@ -0,0 +1,95 @@
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Raw/AboutAssets.txt b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Raw/AboutAssets.txt
new file mode 100644
index 000000000..29b76b2ea
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/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/SpinPaint/SpinPaint/Resources/Splash/splash.svg b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Splash/splash.svg
new file mode 100644
index 000000000..4b713836f
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Splash/splash.svg
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Styles/Colors.xaml b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Styles/Colors.xaml
new file mode 100644
index 000000000..66867878f
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/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/SpinPaint/SpinPaint/Resources/Styles/Styles.xaml b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Styles/Styles.xaml
new file mode 100644
index 000000000..d45ca5bad
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/Resources/Styles/Styles.xaml
@@ -0,0 +1,409 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/SkiaSharp/SpinPaint/SpinPaint/SpinPaint.csproj b/8.0/SkiaSharp/SpinPaint/SpinPaint/SpinPaint.csproj
new file mode 100644
index 000000000..a87b7b4e9
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/SpinPaint/SpinPaint.csproj
@@ -0,0 +1,66 @@
+
+
+
+ net8.0-android;net8.0-ios;net8.0-maccatalyst
+ $(TargetFrameworks);net8.0-windows10.0.19041.0
+
+
+
+
+
+
+ Exe
+ SpinPaint
+ true
+ true
+ enable
+ enable
+
+
+ SpinPaint
+
+
+ com.companyname.spinpaint
+
+
+ 1.0
+ 1
+
+ 11.0
+ 13.1
+ 21.0
+ 10.0.17763.0
+ 10.0.17763.0
+ 6.5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8.0/Tutorials/ConvertToMvvm/step5_viewmodel_notes/Views/AllNotesPage.xaml b/8.0/Tutorials/ConvertToMvvm/step5_viewmodel_notes/Views/AllNotesPage.xaml
index 316131f4c..11d6a7988 100644
--- a/8.0/Tutorials/ConvertToMvvm/step5_viewmodel_notes/Views/AllNotesPage.xaml
+++ b/8.0/Tutorials/ConvertToMvvm/step5_viewmodel_notes/Views/AllNotesPage.xaml
@@ -9,7 +9,7 @@
-
+
@@ -34,4 +34,4 @@
-
\ No newline at end of file
+
diff --git a/8.0/Tutorials/ConvertToMvvm/step6_bugs/Views/AllNotesPage.xaml b/8.0/Tutorials/ConvertToMvvm/step6_bugs/Views/AllNotesPage.xaml
index 5d7258252..b594d77a3 100644
--- a/8.0/Tutorials/ConvertToMvvm/step6_bugs/Views/AllNotesPage.xaml
+++ b/8.0/Tutorials/ConvertToMvvm/step6_bugs/Views/AllNotesPage.xaml
@@ -8,10 +8,10 @@
-
+
-
+
@@ -36,4 +36,4 @@
-
\ No newline at end of file
+