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..6429035e4
--- /dev/null
+++ b/8.0/SkiaSharp/MandelbrotAnimation/README.md
@@ -0,0 +1,36 @@
+---
+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.
+
+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.
+
+![Mandelbrot Animation app screenshot](Screenshots/MandelbrotAnimation.png "Mandelbrot Animation app screenshot")
+
+[!INCLUDE [Install SkiaSharp](../includes/install-skiasharp.md)]
diff --git a/8.0/SkiaSharp/MandelbrotAnimation/Screenshots/MandelbrotAnimation.png b/8.0/SkiaSharp/MandelbrotAnimation/Screenshots/MandelbrotAnimation.png
new file mode 100644
index 000000000..34b73a8cf
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..3b9e0f935
--- /dev/null
+++ b/8.0/SkiaSharp/PhotoPuzzle/README.md
@@ -0,0 +1,21 @@
+---
+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:
+- dotnetmaui
+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.
+
+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.
+
+![Photo Puzzle app screenshot](Screenshots/PhotoPuzzle-randomimzed.png "Photo Puzzle app screenshot")
+
+[!INCLUDE [Install SkiaSharp](../includes/install-skiasharp.md)]
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..88453ee13
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..46d144d24
Binary files /dev/null and b/8.0/SkiaSharp/PhotoPuzzle/Screenshots/PhotoPuzzle-randomized.png differ
diff --git a/8.0/SkiaSharp/SpinPaint/README.md b/8.0/SkiaSharp/SpinPaint/README.md
new file mode 100644
index 000000000..add996b3f
--- /dev/null
+++ b/8.0/SkiaSharp/SpinPaint/README.md
@@ -0,0 +1,19 @@
+---
+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:
+- dotnetmaui
+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")
+
+[!INCLUDE [Install SkiaSharp](../includes/install-skiasharp.md)]
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/SkiaSharp/includes/install-skiasharp.md b/8.0/SkiaSharp/includes/install-skiasharp.md
new file mode 100644
index 000000000..429d0a561
--- /dev/null
+++ b/8.0/SkiaSharp/includes/install-skiasharp.md
@@ -0,0 +1,38 @@
+---
+ms.topic: include
+---
+
+To use SkiaSharp in your .NET Multi-platform App UI (.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();
+ }
+}
+```