From 831c014ad0618396572d2c56400ceba8a97f8505 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Thu, 30 Jul 2026 17:15:34 +0800
Subject: [PATCH 01/50] Refine DenyShaderResource flag logic for textures
Previously, DenyShaderResource was set if TextureUsages lacked Sampled. Now, it is set only when TextureUsages includes DepthStencilAttachment but not Sampled, restricting denial to unsampled depth-stencil attachments.
---
sources/Zenith.NET.DirectX12/DXFormats.cs | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/sources/Zenith.NET.DirectX12/DXFormats.cs b/sources/Zenith.NET.DirectX12/DXFormats.cs
index 762a56d3..94f0d586 100644
--- a/sources/Zenith.NET.DirectX12/DXFormats.cs
+++ b/sources/Zenith.NET.DirectX12/DXFormats.cs
@@ -618,11 +618,6 @@ public static ResourceFlags DirectX12(TextureUsages textureUsages)
{
ResourceFlags result = default;
- if (!textureUsages.HasFlag(TextureUsages.Sampled))
- {
- result |= ResourceFlags.DenyShaderResource;
- }
-
if (textureUsages.HasFlag(TextureUsages.Storage))
{
result |= ResourceFlags.AllowUnorderedAccess;
@@ -635,6 +630,11 @@ public static ResourceFlags DirectX12(TextureUsages textureUsages)
if (textureUsages.HasFlag(TextureUsages.DepthStencilAttachment))
{
+ if (!textureUsages.HasFlag(TextureUsages.Sampled))
+ {
+ result |= ResourceFlags.DenyShaderResource;
+ }
+
result |= ResourceFlags.AllowDepthStencil;
}
From 5a2fcbf911b0bae8c7b1f27a60e5db526fd8f4ef Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Fri, 31 Jul 2026 10:06:41 +0800
Subject: [PATCH 02/50] Update Avalonia, Slangc.NET, and Uno.WinUI package
versions
Updated Avalonia to 12.1.1, Slangc.NET to 2026.14.1, and Uno.WinUI to 6.6.184 to keep dependencies current.
---
sources/Directory.Packages.props | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/sources/Directory.Packages.props b/sources/Directory.Packages.props
index 44203098..2e82683f 100644
--- a/sources/Directory.Packages.props
+++ b/sources/Directory.Packages.props
@@ -5,7 +5,7 @@
-
+
@@ -20,8 +20,8 @@
-
-
+
+
\ No newline at end of file
From bec2dd23d8ee658cc86759ce58be57bc9371da7d Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Fri, 31 Jul 2026 10:47:49 +0800
Subject: [PATCH 03/50] Add CanTransition property to DXTexture and related
logic
Introduce CanTransition to DXTexture to indicate if a texture supports transitions. Update constructors and related classes to set this property. DXCommandBuffer.TransitionImpl now checks CanTransition before transitioning. Adjust DXGraphicsContext and DXHeap to pass CanTransition when creating textures.
---
sources/Zenith.NET.DirectX12/DXCommandBuffer.cs | 5 +++++
sources/Zenith.NET.DirectX12/DXGraphicsContext.cs | 2 +-
sources/Zenith.NET.DirectX12/DXHeap.cs | 2 +-
sources/Zenith.NET.DirectX12/DXTexture.cs | 7 ++++++-
4 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs b/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
index dd86760d..cfc7b677 100644
--- a/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
+++ b/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
@@ -51,6 +51,11 @@ protected override void TransitionImpl(Texture texture, TextureSubresource subre
{
DXTexture dxTexture = texture.DirectX12();
+ if (!dxTexture.CanTransition)
+ {
+ return;
+ }
+
(BarrierSync syncBefore, BarrierAccess accessBefore, BarrierLayout layoutBefore) = DXFormats.DirectX12(before);
(BarrierSync syncAfter, BarrierAccess accessAfter, BarrierLayout layoutAfter) = DXFormats.DirectX12(after);
diff --git a/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs b/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs
index 8b6d4cd2..0a74bdcc 100644
--- a/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs
+++ b/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs
@@ -167,7 +167,7 @@ protected override Texture CreateTextureImpl(TextureDesc desc, NativeTextureType
ComPtr resource = new();
Device.OpenSharedHandle((void*)nativeTexture, SilkMarshal.GuidPtrOf(), (void**)resource.GetAddressOf()).Success();
- return new DXTexture(this, desc, resource);
+ return new DXTexture(this, desc, resource, false);
}
protected override TextureView CreateTextureViewImpl(TextureViewDesc desc)
diff --git a/sources/Zenith.NET.DirectX12/DXHeap.cs b/sources/Zenith.NET.DirectX12/DXHeap.cs
index eeb420dc..f37ed3aa 100644
--- a/sources/Zenith.NET.DirectX12/DXHeap.cs
+++ b/sources/Zenith.NET.DirectX12/DXHeap.cs
@@ -60,7 +60,7 @@ protected override Texture CreateTextureImpl(ulong offsetInBytes, TextureDesc de
SilkMarshal.GuidPtrOf(),
(void**)resource.GetAddressOf()).Success();
- return new DXTexture(Context, desc, resource);
+ return new DXTexture(Context, desc, resource, true);
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.DirectX12/DXTexture.cs b/sources/Zenith.NET.DirectX12/DXTexture.cs
index ccb19acb..a5da57b9 100644
--- a/sources/Zenith.NET.DirectX12/DXTexture.cs
+++ b/sources/Zenith.NET.DirectX12/DXTexture.cs
@@ -11,6 +11,8 @@ internal unsafe class DXTexture : Texture
public ComPtr Resource;
+ public bool CanTransition;
+
public DXTexture(DXGraphicsContext context, TextureDesc desc) : base(context, desc)
{
ResourceDesc1 resourceDesc = ResourceDesc(desc);
@@ -28,6 +30,8 @@ public DXTexture(DXGraphicsContext context, TextureDesc desc) : base(context, de
SilkMarshal.GuidPtrOf(),
(void**)Resource.GetAddressOf()).Success();
+ CanTransition = true;
+
View = new(context, new()
{
Texture = this,
@@ -37,9 +41,10 @@ public DXTexture(DXGraphicsContext context, TextureDesc desc) : base(context, de
});
}
- public DXTexture(DXGraphicsContext context, TextureDesc desc, ComPtr resource) : base(context, desc)
+ public DXTexture(DXGraphicsContext context, TextureDesc desc, ComPtr resource, bool canTransition) : base(context, desc)
{
Resource = resource;
+ CanTransition = canTransition;
View = new(context, new()
{
From 1220d6d54602a2866b79d2411250daca90fcfc98 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Fri, 31 Jul 2026 10:48:35 +0800
Subject: [PATCH 04/50] Update Texture constructor to include extra boolean
flag
The textures array now initializes each Texture with an additional boolean argument (true), reflecting a change in the Texture class constructor. This likely enables a new behavior or flag during texture initialization.
---
sources/Zenith.NET.DirectX12/DXSwapChain.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sources/Zenith.NET.DirectX12/DXSwapChain.cs b/sources/Zenith.NET.DirectX12/DXSwapChain.cs
index c023f988..a75db726 100644
--- a/sources/Zenith.NET.DirectX12/DXSwapChain.cs
+++ b/sources/Zenith.NET.DirectX12/DXSwapChain.cs
@@ -112,7 +112,7 @@ private void CreateTextures()
ComPtr resource = new();
SwapChain.GetBuffer((uint)i, SilkMarshal.GuidPtrOf(), (void**)resource.GetAddressOf()).Success();
- textures[i] = new(Context, desc, resource);
+ textures[i] = new(Context, desc, resource, true);
}
index = SwapChain.GetCurrentBackBufferIndex();
From b3a6c28cbb4276e443293d8bb0721e6eabf9b7a6 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Fri, 31 Jul 2026 11:11:54 +0800
Subject: [PATCH 05/50] Add D3D11 query for resource copy synchronization
Added a ComPtr to Surface and created the query in the constructor. Wrapped resource copy with Begin/End on the query and replaced DeviceContext.Flush() with a polling loop using GetData to ensure copy completion before proceeding.
---
.../Zenith.NET.Views.WinUI/ZenithView.WinUI.cs | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
index 875ee498..336e2a15 100644
--- a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
+++ b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
@@ -113,6 +113,8 @@ internal unsafe partial class Surface : DisposableObject
[LibraryImport("kernel32")]
private static partial int CloseHandle(nint hObject);
+ public ComPtr Query = new();
+
public ComPtr SwapChain = new();
public ComPtr Texture = new();
@@ -125,6 +127,9 @@ internal unsafe partial class Surface : DisposableObject
public Surface(GraphicsContext graphicsContext, uint width, uint height)
{
+ QueryDesc queryDesc = new();
+ D3D.Success(D3D.Device.CreateQuery(&queryDesc, Query.GetAddressOf()));
+
SwapChainDesc1 swapChainDesc = new()
{
Width = width,
@@ -198,8 +203,14 @@ public void Present()
AcquireSync();
+ D3D.DeviceContext.Begin(Query);
D3D.DeviceContext.CopyResource((ID3D11Resource*)backBuffer.Handle, (ID3D11Resource*)Texture.Handle);
- D3D.DeviceContext.Flush();
+ D3D.DeviceContext.End(Query);
+
+ while (D3D.DeviceContext.GetData(Query, default, 0, 0) is not 0)
+ {
+ Thread.Yield();
+ }
ReleaseSync();
@@ -218,6 +229,7 @@ protected override void Destroy()
Mutex.Dispose();
Texture.Dispose();
SwapChain.Dispose();
+ Query.Dispose();
}
private static Format DrawableFormat()
From 5112ed8d7f81744bf00d0bc30af54d62eae13ff1 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Fri, 31 Jul 2026 11:19:14 +0800
Subject: [PATCH 06/50] Use D3D11 query for GPU sync in Surface rendering
Introduce ComPtr to Surface for GPU synchronization.
Replace DeviceContext.Flush with Begin/End query and GetData polling.
Ensure proper disposal of Query in Surface.Dispose.
Align WinUI implementation with GetData polling logic.
---
.../Platforms/Windows/Surface.cs | 14 +++++++++++++-
sources/Views/Zenith.NET.Views.WPF/Surface.cs | 14 +++++++++++++-
.../Zenith.NET.Views.WinUI/ZenithView.WinUI.cs | 2 +-
3 files changed, 27 insertions(+), 3 deletions(-)
diff --git a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs
index 52163433..89e8c70a 100644
--- a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs
+++ b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs
@@ -11,6 +11,8 @@ internal unsafe partial class Surface : DisposableObject
[LibraryImport("kernel32")]
private static partial int CloseHandle(nint hObject);
+ public ComPtr Query = new();
+
public ComPtr SwapChain = new();
public ComPtr Texture = new();
@@ -23,6 +25,9 @@ internal unsafe partial class Surface : DisposableObject
public Surface(GraphicsContext graphicsContext, uint width, uint height)
{
+ QueryDesc queryDesc = new();
+ D3D.Success(D3D.Device.CreateQuery(&queryDesc, Query.GetAddressOf()));
+
SwapChainDesc1 swapChainDesc = new()
{
Width = width,
@@ -96,8 +101,14 @@ public void Present()
AcquireSync();
+ D3D.DeviceContext.Begin(Query);
D3D.DeviceContext.CopyResource((ID3D11Resource*)backBuffer.Handle, (ID3D11Resource*)Texture.Handle);
- D3D.DeviceContext.Flush();
+ D3D.DeviceContext.End(Query);
+
+ while (D3D.DeviceContext.GetData(Query, default, 0, 0) is 1)
+ {
+ Thread.Yield();
+ }
ReleaseSync();
@@ -116,6 +127,7 @@ protected override void Destroy()
Mutex.Dispose();
Texture.Dispose();
SwapChain.Dispose();
+ Query.Dispose();
}
private static Format DrawableFormat()
diff --git a/sources/Views/Zenith.NET.Views.WPF/Surface.cs b/sources/Views/Zenith.NET.Views.WPF/Surface.cs
index 332cd3c7..b9def545 100644
--- a/sources/Views/Zenith.NET.Views.WPF/Surface.cs
+++ b/sources/Views/Zenith.NET.Views.WPF/Surface.cs
@@ -15,6 +15,8 @@ internal unsafe partial class Surface : DisposableObject
[LibraryImport("kernel32")]
private static partial int CloseHandle(nint hObject);
+ public ComPtr Query = new();
+
public ComPtr D3D9RenderTarget = new();
public ComPtr D3D9RenderSurface = new();
@@ -31,6 +33,9 @@ internal unsafe partial class Surface : DisposableObject
public Surface(GraphicsContext graphicsContext, uint width, uint height)
{
+ QueryDesc queryDesc = new();
+ D3D.Success(D3D.D3D11Device.CreateQuery(&queryDesc, Query.GetAddressOf()));
+
void* sharedHandle = null;
D3D.Success(D3D.D3D9DeviceEx.CreateTexture(width,
height,
@@ -103,8 +108,14 @@ public void Present(D3DImage image)
AcquireSync();
+ D3D.D3D11DeviceContext.Begin(Query);
D3D.D3D11DeviceContext.CopyResource((ID3D11Resource*)D3D9SharedTexture.Handle, (ID3D11Resource*)D3D11RenderTarget.Handle);
- D3D.D3D11DeviceContext.Flush();
+ D3D.D3D11DeviceContext.End(Query);
+
+ while (D3D.D3D11DeviceContext.GetData(Query, default, 0, 0) is 1)
+ {
+ Thread.Yield();
+ }
ReleaseSync();
@@ -126,6 +137,7 @@ protected override void Destroy()
D3D9SharedTexture.Dispose();
D3D9RenderSurface.Dispose();
D3D9RenderTarget.Dispose();
+ Query.Dispose();
}
private static DXGIFormat DrawableFormat()
diff --git a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
index 336e2a15..dd6ffdac 100644
--- a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
+++ b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
@@ -207,7 +207,7 @@ public void Present()
D3D.DeviceContext.CopyResource((ID3D11Resource*)backBuffer.Handle, (ID3D11Resource*)Texture.Handle);
D3D.DeviceContext.End(Query);
- while (D3D.DeviceContext.GetData(Query, default, 0, 0) is not 0)
+ while (D3D.DeviceContext.GetData(Query, default, 0, 0) is 1)
{
Thread.Yield();
}
From 7868b228672974efa8b7210fefe5dbfd074b1194 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Fri, 31 Jul 2026 17:53:51 +0800
Subject: [PATCH 07/50] Refactor UI calls to use async Dispatcher methods
Replaced synchronous Dispatcher.Invoke/Invoke with asynchronous Dispatcher.InvokeAsync/InvokeAsync using DispatcherPriority.Render to enhance UI responsiveness. Added System.Windows.Threading import for DispatcherPriority support.
---
sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs | 2 +-
sources/Views/Zenith.NET.Views.WPF/ZenithView.cs | 3 ++-
sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs | 2 +-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs b/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
index 6557c768..62434f2b 100644
--- a/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
@@ -81,7 +81,7 @@ public override void Render(DrawingContext context)
void IZenithView.UI(Action action)
{
- Dispatcher.Invoke(action);
+ Dispatcher.Post(action, DispatcherPriority.Render);
}
void IZenithView.EnsureResources()
diff --git a/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs b/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
index c0d80027..4fdfae6c 100644
--- a/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
@@ -4,6 +4,7 @@
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
+using System.Windows.Threading;
namespace Zenith.NET.Views.WPF;
@@ -88,7 +89,7 @@ protected override void OnRender(DrawingContext drawingContext)
void IZenithView.UI(Action action)
{
- Dispatcher.Invoke(action);
+ Dispatcher.InvokeAsync(action, DispatcherPriority.Render);
}
void IZenithView.EnsureResources()
diff --git a/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs b/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs
index 97d6a1e3..6465e2c2 100644
--- a/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.WinForms/ZenithView.cs
@@ -63,7 +63,7 @@ protected override void OnPaint(PaintEventArgs e)
void IZenithView.UI(Action action)
{
- Invoke(action);
+ InvokeAsync(action);
}
void IZenithView.EnsureResources()
From c0d52239cfee91ce57d141b779d22fec5158e11e Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Fri, 31 Jul 2026 18:07:28 +0800
Subject: [PATCH 08/50] Remove explicit Render priority from UI dispatcher
calls
Updated IZenithView.UI to invoke actions with the default dispatcher priority by removing the DispatcherPriority.Render argument from Dispatcher.InvokeAsync. This simplifies priority management for UI actions.
---
sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs | 2 +-
sources/Views/Zenith.NET.Views.WPF/ZenithView.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs b/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
index 62434f2b..5f4ec710 100644
--- a/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.Avalonia/ZenithView.cs
@@ -81,7 +81,7 @@ public override void Render(DrawingContext context)
void IZenithView.UI(Action action)
{
- Dispatcher.Post(action, DispatcherPriority.Render);
+ Dispatcher.InvokeAsync(action);
}
void IZenithView.EnsureResources()
diff --git a/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs b/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
index 4fdfae6c..ab827d58 100644
--- a/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
+++ b/sources/Views/Zenith.NET.Views.WPF/ZenithView.cs
@@ -89,7 +89,7 @@ protected override void OnRender(DrawingContext drawingContext)
void IZenithView.UI(Action action)
{
- Dispatcher.InvokeAsync(action, DispatcherPriority.Render);
+ Dispatcher.InvokeAsync(action);
}
void IZenithView.EnsureResources()
From a361a73b49a12d10e7260c3adc3fa7880911fcc6 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Sat, 1 Aug 2026 10:11:07 +0800
Subject: [PATCH 09/50] Update SkiaSharp to version 4.151.0
Upgraded the SkiaSharp NuGet package from 4.150.1 to 4.151.0 in Directory.Packages.props. No other package versions were changed; only minor formatting adjustments were made.
---
sources/Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sources/Directory.Packages.props b/sources/Directory.Packages.props
index 2e82683f..27fc5529 100644
--- a/sources/Directory.Packages.props
+++ b/sources/Directory.Packages.props
@@ -19,7 +19,7 @@
-
+
From 042e823f188b96f4a60ffe5d49fe3ebadc9beda3 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Sat, 1 Aug 2026 16:18:51 +0800
Subject: [PATCH 10/50] Add SkiaSharp integration and SKTexture classes
Introduced Zenith.NET.Extensions.Skia namespace with SkiaSharp integration. Added Extensions.cs for GraphicsContext extension to create SKTexture with GRContext management. Implemented SKTexture and SKTextureDesc types for texture handling; methods are not yet implemented.
---
.../Zenith.NET.Extensions.Skia/Extensions.cs | 21 +++++++++++++
.../Zenith.NET.Extensions.Skia/SKTexture.cs | 30 +++++++++++++++++++
.../SKTextureDesc.cs | 14 +++++++++
3 files changed, 65 insertions(+)
create mode 100644 sources/Extensions/Zenith.NET.Extensions.Skia/Extensions.cs
create mode 100644 sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
create mode 100644 sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/Extensions.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/Extensions.cs
new file mode 100644
index 00000000..c42a4fcc
--- /dev/null
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/Extensions.cs
@@ -0,0 +1,21 @@
+using SkiaSharp;
+
+namespace Zenith.NET.Extensions.Skia;
+
+public static class Extensions
+{
+ private readonly static Dictionary grContexts = [];
+
+ extension(GraphicsContext context)
+ {
+ public SKTexture CreateSKTexture(SKTextureDesc desc)
+ {
+ if (!grContexts.TryGetValue(context, out GRContext? grContext))
+ {
+ grContexts.Add(context, grContext = GRContext.CreateGl());
+ }
+
+ return new(context, grContext, desc);
+ }
+ }
+}
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
new file mode 100644
index 00000000..15a2b4b8
--- /dev/null
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
@@ -0,0 +1,30 @@
+using SkiaSharp;
+
+namespace Zenith.NET.Extensions.Skia;
+
+public class SKTexture : DisposableObject
+{
+ private SKTextureDesc desc;
+
+ internal SKTexture(GraphicsContext context, GRContext grContext, SKTextureDesc desc)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ref readonly SKTextureDesc Desc => ref desc;
+
+ public void Render(TextureLayout currentLayout, TextureLayout finalLayout, Action render)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override void Destroy()
+ {
+ throw new NotImplementedException();
+ }
+
+ public static implicit operator Texture(SKTexture texture)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs
new file mode 100644
index 00000000..a1e2bcfe
--- /dev/null
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs
@@ -0,0 +1,14 @@
+namespace Zenith.NET.Extensions.Skia;
+
+public struct SKTextureDesc
+{
+ public PixelFormat Format;
+
+ public uint Width;
+
+ public uint Height;
+
+ public SampleCount SampleCount;
+
+ public TextureUsages Usages;
+}
From aa6d75189cbea2d0ae51fd262bd0dfc7255cd3a7 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Sat, 1 Aug 2026 22:19:32 +0800
Subject: [PATCH 11/50] Add GetNativeObject support for DX12, Metal, Vulkan
wrappers
Implement GetNativeObject in graphics API wrappers to return native handles or pointers based on NativeObjectType. Expand NativeObjectType enum to cover all supported native objects for DirectX 12, Metal, and Vulkan, enabling type-safe access to underlying resources.
---
.../DXBottomLevelAccelerationStructure.cs | 7 ++-
sources/Zenith.NET.DirectX12/DXBuffer.cs | 7 ++-
.../Zenith.NET.DirectX12/DXCommandBuffer.cs | 6 ++-
.../Zenith.NET.DirectX12/DXCommandQueue.cs | 6 ++-
.../Zenith.NET.DirectX12/DXGraphicsContext.cs | 7 ++-
sources/Zenith.NET.DirectX12/DXTexture.cs | 6 ++-
sources/Zenith.NET.DirectX12/DXTimeline.cs | 6 ++-
.../DXTopLevelAccelerationStructure.cs | 7 ++-
.../Zenith.NET.Metal/MTLGraphicsContext.cs | 6 ++-
sources/Zenith.NET.Metal/MTLTexture.cs | 6 ++-
sources/Zenith.NET.Metal/MTLTextureView.cs | 6 ++-
sources/Zenith.NET.Metal/MTLTimeline.cs | 6 ++-
.../VKBottomLevelAccelerationStructure.cs | 7 ++-
sources/Zenith.NET.Vulkan/VKBuffer.cs | 9 +++-
sources/Zenith.NET.Vulkan/VKCommandBuffer.cs | 6 ++-
sources/Zenith.NET.Vulkan/VKCommandQueue.cs | 7 ++-
.../Zenith.NET.Vulkan/VKGraphicsContext.cs | 10 +++-
sources/Zenith.NET.Vulkan/VKHeap.cs | 6 ++-
sources/Zenith.NET.Vulkan/VKTexture.cs | 8 ++-
sources/Zenith.NET.Vulkan/VKTimeline.cs | 6 ++-
.../VKTopLevelAccelerationStructure.cs | 6 ++-
sources/Zenith.NET/Enums/NativeObjectType.cs | 49 +++++++++++++++++++
22 files changed, 169 insertions(+), 21 deletions(-)
diff --git a/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs b/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs
index 34534b32..3a89ebb8 100644
--- a/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs
+++ b/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs
@@ -70,7 +70,12 @@ public void Update(DXCommandBuffer commandBuffer, BottomLevelAccelerationStructu
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12GpuVirtualAddress => (nint)AccelerationStructure.GPUVirtualAddress,
+ NativeObjectType.D3D12Resource => (nint)AccelerationStructure.Resource.Handle,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.DirectX12/DXBuffer.cs b/sources/Zenith.NET.DirectX12/DXBuffer.cs
index 9cf81f2c..90b92467 100644
--- a/sources/Zenith.NET.DirectX12/DXBuffer.cs
+++ b/sources/Zenith.NET.DirectX12/DXBuffer.cs
@@ -89,7 +89,12 @@ public DXBuffer(DXGraphicsContext context, BufferDesc desc, ResourceFlags flags)
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12GpuVirtualAddress => (nint)GPUVirtualAddress,
+ NativeObjectType.D3D12Resource => (nint)Resource.Handle,
+ _ => default
+ };
}
public override nint Map()
diff --git a/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs b/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
index cfc7b677..c8837a1d 100644
--- a/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
+++ b/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
@@ -21,7 +21,11 @@ public DXCommandBuffer(DXGraphicsContext context, DXCommandQueue queue) : base(c
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12GraphicsCommandList => (nint)CommandList.Handle,
+ _ => default
+ };
}
protected override void BarrierImpl(BarrierStages before, BarrierStages after)
diff --git a/sources/Zenith.NET.DirectX12/DXCommandQueue.cs b/sources/Zenith.NET.DirectX12/DXCommandQueue.cs
index 4eef6297..d95832fb 100644
--- a/sources/Zenith.NET.DirectX12/DXCommandQueue.cs
+++ b/sources/Zenith.NET.DirectX12/DXCommandQueue.cs
@@ -22,7 +22,11 @@ public DXCommandQueue(DXGraphicsContext context, CommandQueueType type) : base(c
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12CommandQueue => (nint)CommandQueue.Handle,
+ _ => default
+ };
}
protected override CommandBuffer CreateCommandBuffer()
diff --git a/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs b/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs
index 0a74bdcc..04959410 100644
--- a/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs
+++ b/sources/Zenith.NET.DirectX12/DXGraphicsContext.cs
@@ -40,7 +40,12 @@ internal unsafe class DXGraphicsContext(bool useValidationLayer) : GraphicsConte
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12Adapter => (nint)Adapter.Handle,
+ NativeObjectType.D3D12Device => (nint)Device.Handle,
+ _ => default
+ };
}
protected override void Initialize(bool useValidationLayer,
diff --git a/sources/Zenith.NET.DirectX12/DXTexture.cs b/sources/Zenith.NET.DirectX12/DXTexture.cs
index a5da57b9..d5532720 100644
--- a/sources/Zenith.NET.DirectX12/DXTexture.cs
+++ b/sources/Zenith.NET.DirectX12/DXTexture.cs
@@ -216,7 +216,11 @@ public CpuDescriptorHandle GetDsvHandle(TextureSubresource subresource)
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12Resource => (nint)Resource.Handle,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.DirectX12/DXTimeline.cs b/sources/Zenith.NET.DirectX12/DXTimeline.cs
index 07f6e77c..7e7f6d42 100644
--- a/sources/Zenith.NET.DirectX12/DXTimeline.cs
+++ b/sources/Zenith.NET.DirectX12/DXTimeline.cs
@@ -20,7 +20,11 @@ public DXTimeline(DXGraphicsContext context, DXCommandQueue queue) : base(contex
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12Fence => (nint)Fence.Handle,
+ _ => default
+ };
}
protected override ulong GetCompletedValue()
diff --git a/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs b/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs
index b32c76b7..b0771bda 100644
--- a/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs
+++ b/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs
@@ -82,7 +82,12 @@ public void Update(DXCommandBuffer commandBuffer, TopLevelAccelerationStructureD
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.D3D12GpuVirtualAddress => (nint)AccelerationStructure.GPUVirtualAddress,
+ NativeObjectType.D3D12Resource => (nint)AccelerationStructure.Resource.Handle,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.Metal/MTLGraphicsContext.cs b/sources/Zenith.NET.Metal/MTLGraphicsContext.cs
index f2ecdd4d..5dfe9013 100644
--- a/sources/Zenith.NET.Metal/MTLGraphicsContext.cs
+++ b/sources/Zenith.NET.Metal/MTLGraphicsContext.cs
@@ -30,7 +30,11 @@ public void Unregister(MTLAllocation allocation)
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.MTLDevice => Device.NativePtr,
+ _ => default
+ };
}
protected override void Initialize(bool useValidationLayer,
diff --git a/sources/Zenith.NET.Metal/MTLTexture.cs b/sources/Zenith.NET.Metal/MTLTexture.cs
index 88cd42ca..68797dd7 100644
--- a/sources/Zenith.NET.Metal/MTLTexture.cs
+++ b/sources/Zenith.NET.Metal/MTLTexture.cs
@@ -42,7 +42,11 @@ public MTLTexture(MTLGraphicsContext context, TextureDesc desc, MtlTexture textu
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.MTLTexture => Texture.NativePtr,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.Metal/MTLTextureView.cs b/sources/Zenith.NET.Metal/MTLTextureView.cs
index 73d580e4..d51fdee8 100644
--- a/sources/Zenith.NET.Metal/MTLTextureView.cs
+++ b/sources/Zenith.NET.Metal/MTLTextureView.cs
@@ -21,7 +21,11 @@ public MTLTextureView(MTLGraphicsContext context, TextureViewDesc desc) : base(c
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.MTLTexture => Texture.NativePtr,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.Metal/MTLTimeline.cs b/sources/Zenith.NET.Metal/MTLTimeline.cs
index ca93362f..ce042a35 100644
--- a/sources/Zenith.NET.Metal/MTLTimeline.cs
+++ b/sources/Zenith.NET.Metal/MTLTimeline.cs
@@ -8,7 +8,11 @@ internal class MTLTimeline(MTLGraphicsContext context, MTLCommandQueue queue) :
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.MTLSharedEvent => Event.NativePtr,
+ _ => default
+ };
}
protected override ulong GetCompletedValue()
diff --git a/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs b/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs
index b542a751..3760d856 100644
--- a/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs
+++ b/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs
@@ -83,7 +83,12 @@ public void Update(VKCommandBuffer commandBuffer, BottomLevelAccelerationStructu
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanAccelerationStructure => (nint)AccelerationStructure.Handle,
+ NativeObjectType.VulkanDeviceAddress => (nint)DeviceAddress,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET.Vulkan/VKBuffer.cs b/sources/Zenith.NET.Vulkan/VKBuffer.cs
index 0370a18f..e982585a 100644
--- a/sources/Zenith.NET.Vulkan/VKBuffer.cs
+++ b/sources/Zenith.NET.Vulkan/VKBuffer.cs
@@ -152,7 +152,14 @@ public VKBuffer(VKGraphicsContext context, BufferDesc desc, VkBuffer buffer, VKA
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanBuffer => (nint)Buffer.Handle,
+ NativeObjectType.VulkanDeviceAddress => (nint)DeviceAddress,
+ NativeObjectType.VulkanDeviceMemory => (nint)Allocation.DeviceMemory.Handle,
+ NativeObjectType.VulkanDeviceMemoryOffset => (nint)Allocation.OffsetInBytes,
+ _ => default
+ };
}
public override nint Map()
diff --git a/sources/Zenith.NET.Vulkan/VKCommandBuffer.cs b/sources/Zenith.NET.Vulkan/VKCommandBuffer.cs
index 96dc0631..513f3e38 100644
--- a/sources/Zenith.NET.Vulkan/VKCommandBuffer.cs
+++ b/sources/Zenith.NET.Vulkan/VKCommandBuffer.cs
@@ -34,7 +34,11 @@ public VKCommandBuffer(VKGraphicsContext context, VKCommandQueue queue) : base(c
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanCommandBuffer => CommandBuffer.Handle,
+ _ => default
+ };
}
protected override void BarrierImpl(BarrierStages before, BarrierStages after)
diff --git a/sources/Zenith.NET.Vulkan/VKCommandQueue.cs b/sources/Zenith.NET.Vulkan/VKCommandQueue.cs
index 97fd5a42..edc102d4 100644
--- a/sources/Zenith.NET.Vulkan/VKCommandQueue.cs
+++ b/sources/Zenith.NET.Vulkan/VKCommandQueue.cs
@@ -22,7 +22,12 @@ public VKCommandQueue(VKGraphicsContext context, CommandQueueType type, Queue qu
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanQueue => Queue.Handle,
+ NativeObjectType.VulkanQueueFamilyIndex => (nint)QueueFamilyIndex,
+ _ => default
+ };
}
protected override CommandBuffer CreateCommandBuffer()
diff --git a/sources/Zenith.NET.Vulkan/VKGraphicsContext.cs b/sources/Zenith.NET.Vulkan/VKGraphicsContext.cs
index 1b200450..c9eb5bc7 100644
--- a/sources/Zenith.NET.Vulkan/VKGraphicsContext.cs
+++ b/sources/Zenith.NET.Vulkan/VKGraphicsContext.cs
@@ -129,7 +129,15 @@ public uint FindMemoryTypeIndex(uint memoryTypeBits, MemoryResidency residency)
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanDevice => Device.Handle,
+ NativeObjectType.VulkanGetDeviceProcAddr => Vk.GetInstanceProcAddr(Instance, "vkGetDeviceProcAddr"),
+ NativeObjectType.VulkanGetInstanceProcAddr => Vk.GetInstanceProcAddr(default, "vkGetInstanceProcAddr"),
+ NativeObjectType.VulkanInstance => Instance.Handle,
+ NativeObjectType.VulkanPhysicalDevice => PhysicalDevice.Handle,
+ _ => default
+ };
}
protected override void Initialize(bool useValidationLayer,
diff --git a/sources/Zenith.NET.Vulkan/VKHeap.cs b/sources/Zenith.NET.Vulkan/VKHeap.cs
index 8971ba17..371d7556 100644
--- a/sources/Zenith.NET.Vulkan/VKHeap.cs
+++ b/sources/Zenith.NET.Vulkan/VKHeap.cs
@@ -25,7 +25,11 @@ public VKHeap(VKGraphicsContext context, HeapDesc desc) : base(context, desc)
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanDeviceMemory => (nint)DeviceMemory.Handle,
+ _ => default
+ };
}
protected override Buffer CreateBufferImpl(ulong offsetInBytes, BufferDesc desc)
diff --git a/sources/Zenith.NET.Vulkan/VKTexture.cs b/sources/Zenith.NET.Vulkan/VKTexture.cs
index 88945368..2eabdcb6 100644
--- a/sources/Zenith.NET.Vulkan/VKTexture.cs
+++ b/sources/Zenith.NET.Vulkan/VKTexture.cs
@@ -78,7 +78,13 @@ public VKTexture(VKGraphicsContext context, TextureDesc desc, Image image, VKAll
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanDeviceMemory => (nint)Allocation.DeviceMemory.Handle,
+ NativeObjectType.VulkanDeviceMemoryOffset => (nint)Allocation.OffsetInBytes,
+ NativeObjectType.VulkanImage => (nint)Image.Handle,
+ _ => default
+ };
}
public ImageView GetAttachmentView(TextureSubresource subresource)
diff --git a/sources/Zenith.NET.Vulkan/VKTimeline.cs b/sources/Zenith.NET.Vulkan/VKTimeline.cs
index 578fc051..a7379bd4 100644
--- a/sources/Zenith.NET.Vulkan/VKTimeline.cs
+++ b/sources/Zenith.NET.Vulkan/VKTimeline.cs
@@ -21,7 +21,11 @@ public VKTimeline(VKGraphicsContext context, VKCommandQueue queue) : base(contex
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanSemaphore => (nint)Semaphore.Handle,
+ _ => default
+ };
}
protected override ulong GetCompletedValue()
diff --git a/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs b/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs
index cbd0eb80..f81304c5 100644
--- a/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs
+++ b/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs
@@ -100,7 +100,11 @@ public void Update(VKCommandBuffer commandBuffer, TopLevelAccelerationStructureD
public override nint GetNativeObject(NativeObjectType type)
{
- return 0;
+ return type switch
+ {
+ NativeObjectType.VulkanAccelerationStructure => (nint)AccelerationStructure.Handle,
+ _ => default
+ };
}
protected override void SetResourceName(string name)
diff --git a/sources/Zenith.NET/Enums/NativeObjectType.cs b/sources/Zenith.NET/Enums/NativeObjectType.cs
index 1872e9d6..a7ac7219 100644
--- a/sources/Zenith.NET/Enums/NativeObjectType.cs
+++ b/sources/Zenith.NET/Enums/NativeObjectType.cs
@@ -2,4 +2,53 @@
public enum NativeObjectType
{
+ D3D12Adapter,
+
+ D3D12CommandQueue,
+
+ D3D12Device,
+
+ D3D12Fence,
+
+ D3D12GpuVirtualAddress,
+
+ D3D12GraphicsCommandList,
+
+ D3D12Resource,
+
+ MTLDevice,
+
+ MTLSharedEvent,
+
+ MTLTexture,
+
+ VulkanAccelerationStructure,
+
+ VulkanBuffer,
+
+ VulkanCommandBuffer,
+
+ VulkanDevice,
+
+ VulkanDeviceAddress,
+
+ VulkanDeviceMemory,
+
+ VulkanDeviceMemoryOffset,
+
+ VulkanGetDeviceProcAddr,
+
+ VulkanGetInstanceProcAddr,
+
+ VulkanImage,
+
+ VulkanInstance,
+
+ VulkanPhysicalDevice,
+
+ VulkanQueue,
+
+ VulkanQueueFamilyIndex,
+
+ VulkanSemaphore
}
From fea79688897f3ec912c52401f3f8fa703f54719a Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Sat, 1 Aug 2026 22:21:51 +0800
Subject: [PATCH 12/50] Always return 0 in MTLTextureView.GetNativeObject
The GetNativeObject method in MTLTextureView now returns 0 for all NativeObjectType values, instead of returning Texture.NativePtr for MTLTexture and default for others.
---
sources/Zenith.NET.Metal/MTLTextureView.cs | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/sources/Zenith.NET.Metal/MTLTextureView.cs b/sources/Zenith.NET.Metal/MTLTextureView.cs
index d51fdee8..73d580e4 100644
--- a/sources/Zenith.NET.Metal/MTLTextureView.cs
+++ b/sources/Zenith.NET.Metal/MTLTextureView.cs
@@ -21,11 +21,7 @@ public MTLTextureView(MTLGraphicsContext context, TextureViewDesc desc) : base(c
public override nint GetNativeObject(NativeObjectType type)
{
- return type switch
- {
- NativeObjectType.MTLTexture => Texture.NativePtr,
- _ => default
- };
+ return 0;
}
protected override void SetResourceName(string name)
From 5eb0aca1d52cc7c591a1e4b4f6700e913dcd7344 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Sun, 2 Aug 2026 15:09:27 +0800
Subject: [PATCH 13/50] Refactor Skia integration: thread-safe, extensible
renderers
Refactors Skia integration in Zenith.NET for improved thread safety and extensibility:
- Replaces GRContext dictionary with thread-safe SKRenderer management using reference counting.
- Adds SKFormats.cs for mapping Zenith.NET formats to SkiaSharp, DirectX12, and Vulkan equivalents.
- Implements SKRenderer for backend context creation and resource management across DirectX12, Metal, and Vulkan.
- Refactors SKTexture to use SKRenderer, improving resource cleanup and API abstraction.
- Ensures robust, multi-API rendering with proper resource lifecycle handling.
---
.../Zenith.NET.Extensions.Skia/Extensions.cs | 27 ++-
.../Zenith.NET.Extensions.Skia/SKFormats.cs | 102 ++++++++++
.../Zenith.NET.Extensions.Skia/SKRenderer.cs | 182 ++++++++++++++++++
.../Zenith.NET.Extensions.Skia/SKTexture.cs | 37 +++-
4 files changed, 333 insertions(+), 15 deletions(-)
create mode 100644 sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
create mode 100644 sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/Extensions.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/Extensions.cs
index c42a4fcc..5e55de6c 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/Extensions.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/Extensions.cs
@@ -1,21 +1,34 @@
-using SkiaSharp;
-
-namespace Zenith.NET.Extensions.Skia;
+namespace Zenith.NET.Extensions.Skia;
public static class Extensions
{
- private readonly static Dictionary grContexts = [];
+ private static readonly Lock @lock = new();
+ private static readonly Dictionary renderers = [];
extension(GraphicsContext context)
{
public SKTexture CreateSKTexture(SKTextureDesc desc)
{
- if (!grContexts.TryGetValue(context, out GRContext? grContext))
+ using Lock.Scope _ = @lock.EnterScope();
+
+ if (!renderers.TryGetValue(context, out SKRenderer? renderer))
{
- grContexts.Add(context, grContext = GRContext.CreateGl());
+ renderers[context] = renderer = new(context);
}
- return new(context, grContext, desc);
+ renderer.AddReference();
+
+ return new(renderer, desc);
+ }
+ }
+
+ internal static void ReleaseRenderer(SKRenderer renderer)
+ {
+ using Lock.Scope _ = @lock.EnterScope();
+
+ if (renderer.RemoveReference() && renderers.Remove(renderer.Context))
+ {
+ renderer.Dispose();
}
}
}
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
new file mode 100644
index 00000000..ae4c8572
--- /dev/null
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
@@ -0,0 +1,102 @@
+using SkiaSharp;
+
+namespace Zenith.NET.Extensions.Skia;
+
+internal static class SKFormats
+{
+ public static SKColorType Skia(PixelFormat format)
+ {
+ return format switch
+ {
+ PixelFormat.R8UNorm => SKColorType.Gray8,
+ PixelFormat.R16Float => SKColorType.AlphaF16,
+ PixelFormat.R8G8B8A8UNorm => SKColorType.Rgba8888,
+ PixelFormat.R8G8B8A8SRgb => SKColorType.Srgba8888,
+ PixelFormat.R16G16B16A16Float => SKColorType.RgbaF16,
+ PixelFormat.R32G32B32A32Float => SKColorType.RgbaF32,
+ PixelFormat.B8G8R8A8UNorm => SKColorType.Bgra8888,
+ _ => default
+ };
+ }
+
+ public static uint Skia(SampleCount sampleCount)
+ {
+ return sampleCount switch
+ {
+ SampleCount.Count1 => 1,
+ SampleCount.Count2 => 2,
+ SampleCount.Count4 => 4,
+ SampleCount.Count8 => 8,
+ SampleCount.Count16 => 16,
+ SampleCount.Count32 => 32,
+ _ => default
+ };
+ }
+
+ public static uint DirectX12(PixelFormat format)
+ {
+ return format switch
+ {
+ PixelFormat.R8UNorm => 61,
+ PixelFormat.R16Float => 54,
+ PixelFormat.R8G8B8A8UNorm => 28,
+ PixelFormat.R8G8B8A8SRgb => 29,
+ PixelFormat.R16G16B16A16Float => 10,
+ PixelFormat.R32G32B32A32Float => 2,
+ PixelFormat.B8G8R8A8UNorm => 87,
+ _ => default
+ };
+ }
+
+ public static uint Vulkan(PixelFormat format)
+ {
+ return format switch
+ {
+ PixelFormat.R8UNorm => 9,
+ PixelFormat.R16Float => 76,
+ PixelFormat.R8G8B8A8UNorm => 37,
+ PixelFormat.R8G8B8A8SRgb => 43,
+ PixelFormat.R16G16B16A16Float => 97,
+ PixelFormat.R32G32B32A32Float => 109,
+ PixelFormat.B8G8R8A8UNorm => 44,
+ _ => default
+ };
+ }
+
+ public static uint Vulkan(TextureUsages textureUsages)
+ {
+ uint result = default;
+
+ if (textureUsages.HasFlag(TextureUsages.Sampled))
+ {
+ result |= 1 << 2;
+ }
+
+ if (textureUsages.HasFlag(TextureUsages.Storage))
+ {
+ result |= 1 << 3;
+ }
+
+ if (textureUsages.HasFlag(TextureUsages.ColorAttachment))
+ {
+ result |= 1 << 4;
+ }
+
+ if (textureUsages.HasFlag(TextureUsages.DepthStencilAttachment))
+ {
+ result |= 1 << 5;
+ }
+
+ if (textureUsages.HasFlag(TextureUsages.TransferSrc))
+ {
+ result |= 1 << 0;
+ }
+
+ if (textureUsages.HasFlag(TextureUsages.TransferDst))
+ {
+ result |= 1 << 1;
+ }
+
+ return result;
+ }
+}
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
new file mode 100644
index 00000000..e2afc93d
--- /dev/null
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
@@ -0,0 +1,182 @@
+using System.Runtime.InteropServices;
+using SkiaSharp;
+
+namespace Zenith.NET.Extensions.Skia;
+
+internal unsafe class SKRenderer : DisposableObject
+{
+ private readonly Lock @lock = new();
+ private readonly nint commandQueue;
+
+ private uint referenceCount;
+
+ public SKRenderer(GraphicsContext context)
+ {
+ Context = context;
+
+ switch (context.GraphicsApi)
+ {
+ case GraphicsApi.DirectX12:
+ {
+ using GRD3DBackendContext backendContext = new()
+ {
+ Adapter = context.GetNativeObject(NativeObjectType.D3D12Adapter),
+ Device = context.GetNativeObject(NativeObjectType.D3D12Device),
+ Queue = context.GraphicsQueue.GetNativeObject(NativeObjectType.D3D12CommandQueue)
+ };
+
+ GRContext = GRContext.CreateDirect3D(backendContext);
+ }
+ break;
+
+ case GraphicsApi.Metal:
+ {
+ nint device = context.GetNativeObject(NativeObjectType.MTLDevice);
+
+ using GRMtlBackendContext backendContext = new()
+ {
+ DeviceHandle = device,
+ QueueHandle = commandQueue = SKObjectiveC.SendMessage(device, "newCommandQueue")
+ };
+
+ GRContext = GRContext.CreateMetal(backendContext);
+ }
+ break;
+
+ case GraphicsApi.Vulkan:
+ {
+ delegate* unmanaged getInstanceProcAddr = (delegate* unmanaged)context.GetNativeObject(NativeObjectType.VulkanGetInstanceProcAddr);
+ delegate* unmanaged getDeviceProcAddr = (delegate* unmanaged)context.GetNativeObject(NativeObjectType.VulkanGetDeviceProcAddr);
+
+ nint instance = context.GetNativeObject(NativeObjectType.VulkanInstance);
+ nint physicalDevice = context.GetNativeObject(NativeObjectType.VulkanPhysicalDevice);
+
+ using GRVkExtensions extensions = GRVkExtensions.Create(GetProcedureAddress, instance, physicalDevice, null, null);
+
+ using GRVkBackendContext backendContext = new()
+ {
+ VkInstance = instance,
+ VkPhysicalDevice = physicalDevice,
+ VkDevice = context.GetNativeObject(NativeObjectType.VulkanDevice),
+ VkQueue = context.GraphicsQueue.GetNativeObject(NativeObjectType.VulkanQueue),
+ GraphicsQueueIndex = (uint)context.GraphicsQueue.GetNativeObject(NativeObjectType.VulkanQueueFamilyIndex),
+ MaxAPIVersion = (1u << 22) | (4u << 12),
+ Extensions = extensions,
+ GetProcedureAddress = GetProcedureAddress
+ };
+
+ GRContext = GRContext.CreateVulkan(backendContext);
+
+ nint GetProcedureAddress(string name, nint instance, nint device)
+ {
+ using ZenithMarshal.Scope scope = new();
+
+ byte* pointer = (byte*)ZenithMarshal.StringToPointer(scope, name, StringEncoding.UTF8);
+
+ return device is 0 ? getInstanceProcAddr(instance, pointer) : getDeviceProcAddr(device, pointer);
+ }
+ }
+ break;
+
+ default:
+ GRContext = default!;
+ break;
+ }
+ }
+
+ public GraphicsContext Context { get; }
+
+ public GRContext GRContext { get; }
+
+ public void AddReference()
+ {
+ referenceCount++;
+ }
+
+ public bool RemoveReference()
+ {
+ return --referenceCount is 0;
+ }
+
+ public void Render(SKSurface surface, Action render)
+ {
+ using Lock.Scope _ = @lock.EnterScope();
+
+ render(surface.Canvas);
+
+ GRContext.Flush(true, true);
+ }
+
+ protected override void Destroy()
+ {
+ GRContext.Dispose();
+
+ if (Context.GraphicsApi is GraphicsApi.Metal)
+ {
+ SKObjectiveC.Release(commandQueue);
+ }
+ }
+
+ public GRBackendTexture CreateBackendTexture(Texture texture)
+ {
+ switch (Context.GraphicsApi)
+ {
+ case GraphicsApi.DirectX12:
+ return new((int)texture.Desc.Width, (int)texture.Desc.Height, new GRD3DTextureResourceInfo
+ {
+ Resource = texture.GetNativeObject(NativeObjectType.D3D12Resource),
+ Format = SKFormats.DirectX12(texture.Desc.Format),
+ SampleCount = 1,
+ LevelCount = 1
+ });
+
+ case GraphicsApi.Metal:
+ return new((int)texture.Desc.Width, (int)texture.Desc.Height, false, new GRMtlTextureInfo() { TextureHandle = texture.GetNativeObject(NativeObjectType.MTLTexture) });
+
+ case GraphicsApi.Vulkan:
+ uint graphicsQueueFamily = (uint)Context.GraphicsQueue.GetNativeObject(NativeObjectType.VulkanQueueFamilyIndex);
+ uint computeQueueFamily = (uint)Context.ComputeQueue.GetNativeObject(NativeObjectType.VulkanQueueFamilyIndex);
+ uint transferQueueFamily = (uint)Context.TransferQueue.GetNativeObject(NativeObjectType.VulkanQueueFamilyIndex);
+ bool concurrent = graphicsQueueFamily != computeQueueFamily || graphicsQueueFamily != transferQueueFamily;
+
+ return new((int)texture.Desc.Width, (int)texture.Desc.Height, new GRVkImageInfo()
+ {
+ Image = (ulong)texture.GetNativeObject(NativeObjectType.VulkanImage),
+ Alloc = new()
+ {
+ Memory = (ulong)texture.GetNativeObject(NativeObjectType.VulkanDeviceMemory),
+ Offset = (ulong)texture.GetNativeObject(NativeObjectType.VulkanDeviceMemoryOffset),
+ Size = Context.GetSizeAndAlignment(texture.Desc).SizeInBytes
+ },
+ Format = SKFormats.Vulkan(texture.Desc.Format),
+ ImageUsageFlags = SKFormats.Vulkan(texture.Desc.Usages),
+ SampleCount = 1,
+ LevelCount = 1,
+ CurrentQueueFamily = concurrent ? uint.MaxValue : graphicsQueueFamily,
+ SharingMode = concurrent ? 1u : 0u
+ });
+
+ default:
+ return default!;
+ }
+ }
+}
+
+internal static partial class SKObjectiveC
+{
+ private const string LibObjC = "/usr/lib/libobjc.A.dylib";
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
+ private static partial nint SendMessage(nint receiver, nint selector);
+
+ [LibraryImport(LibObjC, EntryPoint = "sel_registerName")]
+ private static partial nint RegisterName([MarshalAs(UnmanagedType.LPUTF8Str)] string name);
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_release")]
+ public static partial void Release(nint value);
+
+ public static nint SendMessage(nint receiver, string selector)
+ {
+ return SendMessage(receiver, RegisterName(selector));
+ }
+}
\ No newline at end of file
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
index 15a2b4b8..044af113 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
@@ -4,27 +4,48 @@ namespace Zenith.NET.Extensions.Skia;
public class SKTexture : DisposableObject
{
- private SKTextureDesc desc;
+ private readonly Texture texture;
+ private readonly SKSurface surface;
- internal SKTexture(GraphicsContext context, GRContext grContext, SKTextureDesc desc)
+ internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
{
- throw new NotImplementedException();
+ Renderer = renderer;
+
+ using GRBackendTexture backendTexture = renderer.CreateBackendTexture(texture = renderer.Context.CreateTexture(new()
+ {
+ Type = TextureType.Texture2D,
+ Format = desc.Format,
+ Width = desc.Width,
+ Height = desc.Height,
+ Depth = 1,
+ MipLevels = 1,
+ ArrayLayers = 1,
+ SampleCount = SampleCount.Count1,
+ Usages = desc.Usages | TextureUsages.Sampled | TextureUsages.ColorAttachment | TextureUsages.TransferSrc | TextureUsages.TransferDst
+ }));
+
+ surface = SKSurface.Create(renderer.GRContext, backendTexture, GRSurfaceOrigin.TopLeft, (int)SKFormats.Skia(desc.SampleCount), SKFormats.Skia(desc.Format));
}
- public ref readonly SKTextureDesc Desc => ref desc;
+ internal SKRenderer Renderer { get; }
- public void Render(TextureLayout currentLayout, TextureLayout finalLayout, Action render)
+ public ref readonly TextureDesc Desc => ref texture.Desc;
+
+ public void Render(Action render)
{
- throw new NotImplementedException();
+ Renderer.Render(surface, render);
}
protected override void Destroy()
{
- throw new NotImplementedException();
+ surface.Dispose();
+ texture.Dispose();
+
+ Extensions.ReleaseRenderer(Renderer);
}
public static implicit operator Texture(SKTexture texture)
{
- throw new NotImplementedException();
+ return texture.texture;
}
}
From 416dc3777da5a6499710484c1a820f9bcd70ec28 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Sun, 2 Aug 2026 16:26:04 +0800
Subject: [PATCH 14/50] Add SkiaBoard experiment: cross-platform drawing app
Added SkiaBoard project to Experiments, including project file and source code. Implemented App.cs for window/input/rendering setup with Silk.NET and Zenith.NET, integrating SkiaSharp for drawing. Board.cs provides drawing board features: brush/eraser, color palette, undo/redo, clear, and brush size adjustment. CocoaHelper.cs enables Metal layer interop on macOS. Updated Program.cs to launch SkiaBoard and Zenith.NET.slnx to include the new project.
---
Zenith.NET.slnx | 3 +-
sources/Experiments/SkiaBoard/App.cs | 265 ++++++++
sources/Experiments/SkiaBoard/Board.cs | 601 ++++++++++++++++++
.../SkiaBoard/Helpers/CocoaHelper.cs | 34 +
sources/Experiments/SkiaBoard/Program.cs | 3 +
.../Experiments/SkiaBoard/SkiaBoard.csproj | 20 +
6 files changed, 925 insertions(+), 1 deletion(-)
create mode 100644 sources/Experiments/SkiaBoard/App.cs
create mode 100644 sources/Experiments/SkiaBoard/Board.cs
create mode 100644 sources/Experiments/SkiaBoard/Helpers/CocoaHelper.cs
create mode 100644 sources/Experiments/SkiaBoard/Program.cs
create mode 100644 sources/Experiments/SkiaBoard/SkiaBoard.csproj
diff --git a/Zenith.NET.slnx b/Zenith.NET.slnx
index 3e34441f..92cb7395 100644
--- a/Zenith.NET.slnx
+++ b/Zenith.NET.slnx
@@ -1,9 +1,10 @@
-
+
+
diff --git a/sources/Experiments/SkiaBoard/App.cs b/sources/Experiments/SkiaBoard/App.cs
new file mode 100644
index 00000000..1bfc2efa
--- /dev/null
+++ b/sources/Experiments/SkiaBoard/App.cs
@@ -0,0 +1,265 @@
+using System.Numerics;
+using Silk.NET.Input;
+using Silk.NET.Windowing;
+using SkiaBoard.Helpers;
+using Zenith.NET;
+using Zenith.NET.DirectX12;
+using Zenith.NET.Extensions.Skia;
+using Zenith.NET.Metal;
+using Zenith.NET.Vulkan;
+
+namespace SkiaBoard;
+
+internal static class App
+{
+ private static readonly IWindow window;
+ private static readonly IInputContext input;
+ private static readonly SwapChain swapChain;
+ private static readonly Board board;
+
+ private static SKTexture texture;
+ private static bool controlDown;
+ private static bool shiftDown;
+
+ static App()
+ {
+ GraphicsApi graphicsApi = Environment.GetCommandLineArgs().Skip(1).FirstOrDefault()?.ToLowerInvariant() switch
+ {
+ "dx12" => GraphicsApi.DirectX12,
+ "vulkan" => GraphicsApi.Vulkan,
+ "metal" => GraphicsApi.Metal,
+ _ when OperatingSystem.IsMacOS() => GraphicsApi.Metal,
+ _ when OperatingSystem.IsLinux() => GraphicsApi.Vulkan,
+ _ => GraphicsApi.DirectX12
+ };
+
+ Context = graphicsApi switch
+ {
+ GraphicsApi.DirectX12 => GraphicsContext.CreateDirectX12(useValidationLayer: true),
+ GraphicsApi.Metal => GraphicsContext.CreateMetal(useValidationLayer: true),
+ GraphicsApi.Vulkan => GraphicsContext.CreateVulkan(useValidationLayer: true),
+ _ => default!
+ };
+
+ Context.ValidationMessage += static (_, args) => Console.WriteLine($"[{args.Severity}] {args.Message}");
+
+ window = Window.Create(WindowOptions.Default with
+ {
+ API = GraphicsAPI.None,
+ Title = $"Skia Board [{graphicsApi}]",
+ Size = new(1280, 800)
+ });
+
+ window.Initialize();
+ window.Center();
+
+ input = window.CreateInput();
+
+ Surface surface;
+
+ if (OperatingSystem.IsWindows())
+ {
+ surface = Surface.Win32(window.Native!.Win32!.Value.Hwnd, Width, Height);
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ surface = Surface.Apple(CocoaHelper.CreateLayer(window.Native!.Cocoa!.Value), Width, Height);
+ }
+ else
+ {
+ surface = Surface.Xlib(window.Native!.X11!.Value.Display, (nint)window.Native.X11.Value.Window, Width, Height);
+ }
+
+ swapChain = Context.CreateSwapChain(new()
+ {
+ Surface = surface,
+ Format = PixelFormat.B8G8R8A8UNorm
+ });
+
+ texture = CreateTexture(Width, Height);
+ board = new();
+ }
+
+ public static GraphicsContext Context { get; }
+
+ public static uint Width => (uint)window.FramebufferSize.X;
+
+ public static uint Height => (uint)window.FramebufferSize.Y;
+
+ public static Vector2 DpiScale => (Vector2)window.FramebufferSize / (Vector2)window.Size;
+
+ public static void Run()
+ {
+ IMouse mouse = input.Mice[0];
+ mouse.MouseDown += MouseDown;
+ mouse.MouseUp += MouseUp;
+ mouse.MouseMove += MouseMove;
+ mouse.Scroll += (_, wheel) => board.ResizeBrush(wheel.Y);
+
+ IKeyboard keyboard = input.Keyboards[0];
+ keyboard.KeyDown += KeyDown;
+ keyboard.KeyUp += KeyUp;
+
+ window.Render += Render;
+
+ window.Run();
+
+ texture.Dispose();
+ swapChain.Dispose();
+ input.Dispose();
+ window.Dispose();
+
+ Context.Dispose();
+ }
+
+ private static void MouseDown(IMouse mouse, MouseButton button)
+ {
+ if (button is MouseButton.Left && TryBoardSize(out float width, out float height))
+ {
+ board.PointerDown(new(mouse.Position.X, mouse.Position.Y), width, height);
+ }
+ }
+
+ private static void MouseUp(IMouse mouse, MouseButton button)
+ {
+ if (button is MouseButton.Left && TryBoardSize(out float width, out float height))
+ {
+ board.PointerUp(new(mouse.Position.X, mouse.Position.Y), width, height);
+ }
+ }
+
+ private static void MouseMove(IMouse _, Vector2 position)
+ {
+ if (TryBoardSize(out float width, out float height))
+ {
+ board.PointerMove(new(position.X, position.Y), width, height);
+ }
+ }
+
+ private static void KeyDown(IKeyboard _, Key key, int code)
+ {
+ if (key is Key.ControlLeft or Key.ControlRight)
+ {
+ controlDown = true;
+ }
+ else if (key is Key.ShiftLeft or Key.ShiftRight)
+ {
+ shiftDown = true;
+ }
+ else if (key is Key.Z && controlDown)
+ {
+ if (shiftDown)
+ {
+ board.Redo();
+ }
+ else
+ {
+ board.Undo();
+ }
+ }
+ else if (key is Key.Y && controlDown)
+ {
+ board.Redo();
+ }
+ else if (key is Key.Delete)
+ {
+ board.Clear();
+ }
+ else if (key is Key.B)
+ {
+ board.UseBrush();
+ }
+ else if (key is Key.E)
+ {
+ board.UseEraser();
+ }
+ }
+
+ private static void KeyUp(IKeyboard _, Key key, int code)
+ {
+ if (key is Key.ControlLeft or Key.ControlRight)
+ {
+ controlDown = false;
+ }
+ else if (key is Key.ShiftLeft or Key.ShiftRight)
+ {
+ shiftDown = false;
+ }
+ }
+
+ private static void Render(double delta)
+ {
+ uint width = Width;
+ uint height = Height;
+
+ if (width is 0 || height is 0 || !TryBoardSize(out float boardWidth, out float boardHeight))
+ {
+ return;
+ }
+
+ Vector2 dpiScale = DpiScale;
+
+ Resize(width, height);
+
+ texture.Render(canvas =>
+ {
+ canvas.Save();
+ canvas.Scale(dpiScale.X, dpiScale.Y);
+ board.Draw(canvas, boardWidth, boardHeight);
+ canvas.Restore();
+ });
+
+ CommandBuffer commandBuffer = Context.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.CopyDst);
+ commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, TextureLayout.CopySrc);
+
+ commandBuffer.CopyTexture(texture, default, default, swapChain.Drawable, default, default, new()
+ {
+ Width = width,
+ Height = height,
+ Depth = 1
+ });
+
+ commandBuffer.Transition(texture, default, TextureLayout.CopySrc, TextureLayout.ColorAttachment);
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.CopyDst, TextureLayout.Present);
+
+ commandBuffer.Submit().Wait();
+
+ swapChain.Present();
+ window.Title = $"Skia Board [{Context.GraphicsApi}] - {board.ToolName} {board.BrushWidth:0.#}px";
+ }
+
+ private static SKTexture CreateTexture(uint width, uint height)
+ {
+ return Context.CreateSKTexture(new()
+ {
+ Format = PixelFormat.B8G8R8A8UNorm,
+ Width = width,
+ Height = height,
+ SampleCount = SampleCount.Count1
+ });
+ }
+
+ private static void Resize(uint width, uint height)
+ {
+ if (texture.Desc.Width == width && texture.Desc.Height == height)
+ {
+ return;
+ }
+
+ swapChain.Resize(width, height);
+
+ SKTexture oldTexture = texture;
+ texture = CreateTexture(width, height);
+ oldTexture.Dispose();
+ }
+
+ private static bool TryBoardSize(out float width, out float height)
+ {
+ width = window.Size.X;
+ height = window.Size.Y;
+
+ return width > 0.0f && height > 0.0f;
+ }
+}
diff --git a/sources/Experiments/SkiaBoard/Board.cs b/sources/Experiments/SkiaBoard/Board.cs
new file mode 100644
index 00000000..4f65df7e
--- /dev/null
+++ b/sources/Experiments/SkiaBoard/Board.cs
@@ -0,0 +1,601 @@
+using SkiaSharp;
+
+namespace SkiaBoard;
+
+internal class Board
+{
+ private const float ToolbarWidth = 112.0f;
+ private const float MinBrushWidth = 1.0f;
+ private const float MaxBrushWidth = 48.0f;
+ private const float EraserWidthScale = 2.0f;
+
+ private static readonly SKColor AccentColor = new(119, 190, 145);
+ private static readonly SKColor BackgroundColor = new(219, 225, 221);
+ private static readonly SKColor ToolbarColor = new(25, 42, 38);
+
+ private static readonly SKColor PaperColor = new(250, 249, 246);
+
+ private static readonly SKRect brushButton = new(12.0f, 16.0f, 52.0f, 66.0f);
+ private static readonly SKRect eraserButton = new(60.0f, 16.0f, 100.0f, 66.0f);
+ private static readonly SKRect undoButton = new(12.0f, 78.0f, 40.0f, 112.0f);
+ private static readonly SKRect redoButton = new(42.0f, 78.0f, 70.0f, 112.0f);
+ private static readonly SKRect clearButton = new(72.0f, 78.0f, 100.0f, 112.0f);
+
+ private static readonly SKColor[] colors =
+ [
+ new(28, 34, 32),
+ new(34, 101, 163),
+ new(211, 63, 73),
+ new(235, 151, 45),
+ new(48, 139, 94),
+ new(126, 79, 156)
+ ];
+
+ private readonly List strokes = [];
+ private readonly Stack undoHistory = [];
+ private readonly Stack redoHistory = [];
+
+ private Stroke? activeStroke;
+ private Tool tool;
+ private int colorIndex;
+ private SKPoint pointer;
+ private bool adjustingBrush;
+ private bool hasPointer;
+
+ public float BrushWidth { get; private set; } = 8.0f;
+
+ public string ToolName => tool is Tool.Brush ? "Brush" : "Eraser";
+
+ private float StrokeWidth => tool is Tool.Eraser ? BrushWidth * EraserWidthScale : BrushWidth;
+
+ public void Clear()
+ {
+ activeStroke = null;
+
+ if (strokes.Count is 0)
+ {
+ return;
+ }
+
+ SaveState();
+ strokes.Clear();
+ }
+
+ public void Draw(SKCanvas canvas, float width, float height)
+ {
+ canvas.Clear(BackgroundColor);
+
+ SKRect paper = Paper(width, height);
+
+ using SKPaint paint = new() { IsAntialias = true };
+
+ paint.Color = new SKColor(108, 124, 115, 46);
+ canvas.DrawRoundRect(new SKRect(paper.Left + 4.0f, paper.Top + 6.0f, paper.Right + 4.0f, paper.Bottom + 6.0f), 3.0f, 3.0f, paint);
+
+ paint.Color = PaperColor;
+ canvas.DrawRoundRect(paper, 3.0f, 3.0f, paint);
+
+ DrawGrid(canvas, paper, paint);
+
+ canvas.Save();
+ canvas.ClipRect(paper);
+ canvas.SaveLayer(paper, null);
+
+ foreach (Stroke stroke in strokes)
+ {
+ DrawStroke(canvas, stroke, paint);
+ }
+
+ if (activeStroke is not null)
+ {
+ DrawStroke(canvas, activeStroke, paint);
+ }
+
+ canvas.Restore();
+ canvas.Restore();
+
+ DrawPointer(canvas, paper, paint);
+ DrawToolbar(canvas, height, paint);
+ }
+
+ public void PointerDown(SKPoint point, float width, float height)
+ {
+ pointer = point;
+ hasPointer = true;
+
+ if (point.X < ToolbarWidth)
+ {
+ HandleToolbarClick(point, height);
+ return;
+ }
+
+ if (!Paper(width, height).Contains(point.X, point.Y))
+ {
+ return;
+ }
+
+ activeStroke = new(colors[colorIndex], StrokeWidth, tool is Tool.Eraser);
+ activeStroke.Points.Add(point);
+ }
+
+ public void PointerMove(SKPoint point, float width, float height)
+ {
+ pointer = point;
+ hasPointer = true;
+
+ if (adjustingBrush)
+ {
+ SetBrushWidth(point.Y, height);
+ return;
+ }
+
+ if (activeStroke is null)
+ {
+ return;
+ }
+
+ SKRect paper = Paper(width, height);
+
+ if (paper.Contains(point.X, point.Y))
+ {
+ AddPoint(activeStroke, point, false);
+ }
+ else
+ {
+ AddPoint(activeStroke, ClipToPaper(activeStroke.Points[^1], point, paper), true);
+ CommitStroke();
+ }
+ }
+
+ public void PointerUp(SKPoint point, float width, float height)
+ {
+ pointer = point;
+ hasPointer = true;
+ adjustingBrush = false;
+
+ if (activeStroke is null)
+ {
+ return;
+ }
+
+ SKRect paper = Paper(width, height);
+ AddPoint(activeStroke, paper.Contains(point.X, point.Y) ? point : ClipToPaper(activeStroke.Points[^1], point, paper), true);
+ CommitStroke();
+ }
+
+ public void ResizeBrush(float delta)
+ {
+ BrushWidth = Math.Clamp(BrushWidth + delta, MinBrushWidth, MaxBrushWidth);
+ }
+
+ public void Undo()
+ {
+ activeStroke = null;
+
+ if (undoHistory.TryPop(out Stroke[]? state))
+ {
+ redoHistory.Push([.. strokes]);
+ Restore(state);
+ }
+ }
+
+ public void Redo()
+ {
+ activeStroke = null;
+
+ if (redoHistory.TryPop(out Stroke[]? state))
+ {
+ undoHistory.Push([.. strokes]);
+ Restore(state);
+ }
+ }
+
+ public void UseBrush()
+ {
+ tool = Tool.Brush;
+ }
+
+ public void UseEraser()
+ {
+ tool = Tool.Eraser;
+ }
+
+ private static void DrawGrid(SKCanvas canvas, SKRect paper, SKPaint paint)
+ {
+ paint.Color = new SKColor(92, 119, 105, 38);
+
+ for (float y = paper.Top + 24.0f; y < paper.Bottom; y += 24.0f)
+ {
+ for (float x = paper.Left + 24.0f; x < paper.Right; x += 24.0f)
+ {
+ canvas.DrawCircle(x, y, 1.2f, paint);
+ }
+ }
+ }
+
+ private static void DrawStroke(SKCanvas canvas, Stroke stroke, SKPaint paint)
+ {
+ paint.Color = stroke.Color;
+ paint.BlendMode = stroke.Eraser ? SKBlendMode.Clear : SKBlendMode.SrcOver;
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeCap = SKStrokeCap.Round;
+ paint.StrokeJoin = SKStrokeJoin.Round;
+ paint.StrokeWidth = stroke.Width;
+
+ if (stroke.Points.Count is 1)
+ {
+ paint.Style = SKPaintStyle.Fill;
+ canvas.DrawCircle(stroke.Points[0], stroke.Width * 0.5f, paint);
+ return;
+ }
+
+ SKPathBuilder builder = new();
+ builder.MoveTo(stroke.Points[0]);
+
+ if (stroke.Points.Count is 2)
+ {
+ builder.LineTo(stroke.Points[1]);
+ }
+ else
+ {
+ for (int i = 1; i < stroke.Points.Count - 1; i++)
+ {
+ SKPoint point = stroke.Points[i];
+ SKPoint next = stroke.Points[i + 1];
+ builder.QuadTo(point, new SKPoint((point.X + next.X) * 0.5f, (point.Y + next.Y) * 0.5f));
+ }
+
+ builder.LineTo(stroke.Points[^1]);
+ }
+
+ using SKPath path = builder.Detach();
+ canvas.DrawPath(path, paint);
+ }
+
+ private void DrawToolbar(SKCanvas canvas, float height, SKPaint paint)
+ {
+ paint.BlendMode = SKBlendMode.SrcOver;
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = ToolbarColor;
+ canvas.DrawRect(0.0f, 0.0f, ToolbarWidth, height, paint);
+
+ DrawToolButtons(canvas, paint);
+ DrawDivider(canvas, paint, 126.0f);
+ DrawPalette(canvas, paint);
+ DrawDivider(canvas, paint, 278.0f);
+ DrawBrushSlider(canvas, height, paint);
+ }
+
+ private void DrawToolButtons(SKCanvas canvas, SKPaint paint)
+ {
+ bool canUndo = undoHistory.Count > 0;
+ bool canRedo = redoHistory.Count > 0;
+ bool canClear = strokes.Count > 0;
+
+ DrawButton(canvas, paint, brushButton, tool is Tool.Brush, true);
+ DrawButton(canvas, paint, eraserButton, tool is Tool.Eraser, true);
+ DrawButton(canvas, paint, undoButton, false, canUndo);
+ DrawButton(canvas, paint, redoButton, false, canRedo);
+ DrawButton(canvas, paint, clearButton, false, canClear);
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 4.0f;
+ paint.StrokeCap = SKStrokeCap.Round;
+ paint.Color = tool is Tool.Brush ? ToolbarColor : SKColors.White;
+ canvas.DrawLine(23.0f, 50.0f, 43.0f, 30.0f, paint);
+ canvas.DrawCircle(21.0f, 52.0f, 3.0f, paint);
+
+ paint.Color = tool is Tool.Eraser ? ToolbarColor : SKColors.White;
+ paint.StrokeWidth = 2.5f;
+ canvas.Save();
+ canvas.RotateDegrees(-35.0f, 83.0f, 41.0f);
+ canvas.DrawRoundRect(new SKRect(72.0f, 32.0f, 94.0f, 50.0f), 2.0f, 2.0f, paint);
+ canvas.Restore();
+
+ DrawUndoIcon(canvas, paint, undoButton, false, canUndo);
+ DrawUndoIcon(canvas, paint, redoButton, true, canRedo);
+ DrawClearIcon(canvas, paint, clearButton, canClear);
+ }
+
+ private void DrawPalette(SKCanvas canvas, SKPaint paint)
+ {
+ for (int i = 0; i < colors.Length; i++)
+ {
+ SKRect swatch = ColorButton(i);
+ SKPoint center = new(swatch.MidX, swatch.MidY);
+ bool hovered = HasPointer(swatch);
+
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = colors[i];
+ canvas.DrawCircle(center, hovered ? 15.0f : 13.0f, paint);
+
+ if (i == colorIndex)
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 2.5f;
+ paint.Color = AccentColor;
+ canvas.DrawCircle(center, 18.0f, paint);
+ }
+ }
+ }
+
+ private void DrawBrushSlider(SKCanvas canvas, float height, SKPaint paint)
+ {
+ SKRect track = BrushSlider(height);
+
+ if (track.Height < 24.0f)
+ {
+ return;
+ }
+
+ float amount = (BrushWidth - MinBrushWidth) / (MaxBrushWidth - MinBrushWidth);
+ float y = track.Bottom - (track.Height * amount);
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeCap = SKStrokeCap.Round;
+ paint.StrokeWidth = 3.0f;
+ paint.Color = new SKColor(93, 118, 108);
+ canvas.DrawLine(track.MidX, track.Top, track.MidX, track.Bottom, paint);
+
+ paint.Color = AccentColor;
+ canvas.DrawLine(track.MidX, y, track.MidX, track.Bottom, paint);
+
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = HasPointer(new SKRect(track.Left - 10.0f, y - 12.0f, track.Right + 10.0f, y + 12.0f)) || adjustingBrush ? SKColors.White : AccentColor;
+ canvas.DrawCircle(track.MidX, y, 8.0f, paint);
+
+ float previewY = height - 42.0f;
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = tool is Tool.Eraser ? PaperColor : colors[colorIndex];
+ canvas.DrawCircle(ToolbarWidth * 0.5f, previewY, MathF.Min(BrushWidth * 0.5f, 24.0f), paint);
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.5f;
+ paint.Color = new SKColor(210, 222, 216);
+ canvas.DrawCircle(ToolbarWidth * 0.5f, previewY, MathF.Min(BrushWidth * 0.5f, 24.0f), paint);
+ }
+
+ private void DrawPointer(SKCanvas canvas, SKRect paper, SKPaint paint)
+ {
+ if (!hasPointer || !paper.Contains(pointer.X, pointer.Y))
+ {
+ return;
+ }
+
+ paint.BlendMode = SKBlendMode.SrcOver;
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.5f;
+ paint.Color = tool is Tool.Eraser ? new SKColor(36, 55, 49, 170) : colors[colorIndex];
+
+ canvas.DrawCircle(pointer, StrokeWidth * 0.5f, paint);
+ }
+
+ private static void DrawUndoIcon(SKCanvas canvas, SKPaint paint, SKRect rect, bool redo, bool enabled)
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeCap = SKStrokeCap.Round;
+ paint.StrokeJoin = SKStrokeJoin.Round;
+ paint.StrokeWidth = 2.0f;
+ paint.Color = enabled ? SKColors.White : new SKColor(92, 112, 104);
+
+ canvas.Save();
+
+ if (redo)
+ {
+ canvas.Translate(rect.MidX * 2.0f, 0.0f);
+ canvas.Scale(-1.0f, 1.0f);
+ }
+
+ SKRect arc = new(rect.MidX - 7.0f, rect.MidY - 7.0f, rect.MidX + 7.0f, rect.MidY + 7.0f);
+ canvas.DrawArc(arc, 205.0f, 265.0f, false, paint);
+ canvas.DrawLine(rect.MidX - 8.0f, rect.MidY - 3.0f, rect.MidX - 8.0f, rect.MidY + 3.0f, paint);
+ canvas.DrawLine(rect.MidX - 8.0f, rect.MidY - 3.0f, rect.MidX - 2.0f, rect.MidY - 3.0f, paint);
+
+ canvas.Restore();
+ }
+
+ private static void DrawClearIcon(SKCanvas canvas, SKPaint paint, SKRect rect, bool enabled)
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeCap = SKStrokeCap.Round;
+ paint.StrokeJoin = SKStrokeJoin.Round;
+ paint.StrokeWidth = 2.0f;
+ paint.Color = enabled ? SKColors.White : new SKColor(92, 112, 104);
+
+ float x = rect.MidX;
+ float y = rect.MidY;
+ canvas.DrawRoundRect(new SKRect(x - 6.0f, y - 5.0f, x + 6.0f, y + 8.0f), 1.5f, 1.5f, paint);
+ canvas.DrawLine(x - 8.0f, y - 8.0f, x + 8.0f, y - 8.0f, paint);
+ canvas.DrawLine(x - 3.0f, y - 11.0f, x + 3.0f, y - 11.0f, paint);
+ canvas.DrawLine(x - 2.0f, y - 2.0f, x - 2.0f, y + 5.0f, paint);
+ canvas.DrawLine(x + 2.0f, y - 2.0f, x + 2.0f, y + 5.0f, paint);
+ }
+
+ private static void DrawDivider(SKCanvas canvas, SKPaint paint, float y)
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = new SKColor(72, 94, 86);
+ canvas.DrawLine(16.0f, y, ToolbarWidth - 16.0f, y, paint);
+ }
+
+ private void DrawButton(SKCanvas canvas, SKPaint paint, SKRect rect, bool selected, bool enabled)
+ {
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = selected ? AccentColor : HasPointer(rect) && enabled ? new SKColor(47, 70, 63) : ToolbarColor;
+ canvas.DrawRoundRect(rect, 5.0f, 5.0f, paint);
+ }
+
+ private bool HasPointer(SKRect rect)
+ {
+ return hasPointer && rect.Contains(pointer.X, pointer.Y);
+ }
+
+ private static SKRect Paper(float width, float height)
+ {
+ return new(ToolbarWidth + 16.0f,
+ 16.0f,
+ MathF.Max(ToolbarWidth + 16.0f, width - 16.0f),
+ MathF.Max(16.0f, height - 16.0f));
+ }
+
+ private void HandleToolbarClick(SKPoint point, float height)
+ {
+ if (brushButton.Contains(point.X, point.Y))
+ {
+ UseBrush();
+ return;
+ }
+
+ if (eraserButton.Contains(point.X, point.Y))
+ {
+ UseEraser();
+ return;
+ }
+
+ if (undoButton.Contains(point.X, point.Y))
+ {
+ Undo();
+ return;
+ }
+
+ if (redoButton.Contains(point.X, point.Y))
+ {
+ Redo();
+ return;
+ }
+
+ if (clearButton.Contains(point.X, point.Y))
+ {
+ Clear();
+ return;
+ }
+
+ if (BrushSlider(height).Contains(point.X, point.Y))
+ {
+ adjustingBrush = true;
+ SetBrushWidth(point.Y, height);
+ return;
+ }
+
+ for (int i = 0; i < colors.Length; i++)
+ {
+ if (ColorButton(i).Contains(point.X, point.Y))
+ {
+ colorIndex = i;
+ UseBrush();
+ return;
+ }
+ }
+ }
+
+ private static SKRect ColorButton(int index)
+ {
+ int column = index % 2;
+ int row = index / 2;
+ float left = 16.0f + (column * 48.0f);
+ float top = 142.0f + (row * 44.0f);
+ return new(left, top, left + 32.0f, top + 32.0f);
+ }
+
+ private static SKRect BrushSlider(float height)
+ {
+ return new(36.0f, 304.0f, 76.0f, MathF.Max(304.0f, height - 84.0f));
+ }
+
+ private void SetBrushWidth(float y, float height)
+ {
+ SKRect track = BrushSlider(height);
+ float amount = 1.0f - Math.Clamp((y - track.Top) / track.Height, 0.0f, 1.0f);
+ BrushWidth = MathF.Round((MinBrushWidth + (amount * (MaxBrushWidth - MinBrushWidth))) * 2.0f) * 0.5f;
+ }
+
+ private enum Tool
+ {
+ Brush,
+ Eraser
+ }
+
+ private static void AddPoint(Stroke stroke, SKPoint point, bool includeEndpoint)
+ {
+ SKPoint previous = stroke.Points[^1];
+ float x = point.X - previous.X;
+ float y = point.Y - previous.Y;
+ float distance = MathF.Sqrt((x * x) + (y * y));
+ float spacing = MathF.Max(0.75f, stroke.Width * 0.1f);
+
+ for (float offset = spacing; offset < distance; offset += spacing)
+ {
+ float amount = offset / distance;
+ stroke.Points.Add(new(previous.X + (x * amount), previous.Y + (y * amount)));
+ }
+
+ if (includeEndpoint && distance > 0.01f)
+ {
+ stroke.Points.Add(point);
+ }
+ }
+
+ private static SKPoint ClipToPaper(SKPoint start, SKPoint end, SKRect paper)
+ {
+ float x = end.X - start.X;
+ float y = end.Y - start.Y;
+ float amount = 1.0f;
+
+ if (x > 0.0f)
+ {
+ amount = MathF.Min(amount, (paper.Right - start.X) / x);
+ }
+ else if (x < 0.0f)
+ {
+ amount = MathF.Min(amount, (paper.Left - start.X) / x);
+ }
+
+ if (y > 0.0f)
+ {
+ amount = MathF.Min(amount, (paper.Bottom - start.Y) / y);
+ }
+ else if (y < 0.0f)
+ {
+ amount = MathF.Min(amount, (paper.Top - start.Y) / y);
+ }
+
+ amount = Math.Clamp(amount, 0.0f, 1.0f);
+ return new(start.X + (x * amount), start.Y + (y * amount));
+ }
+
+ private void CommitStroke()
+ {
+ if (activeStroke is null)
+ {
+ return;
+ }
+
+ SaveState();
+ strokes.Add(activeStroke);
+ activeStroke = null;
+ }
+
+ private void SaveState()
+ {
+ undoHistory.Push([.. strokes]);
+ redoHistory.Clear();
+ }
+
+ private void Restore(Stroke[] state)
+ {
+ strokes.Clear();
+ strokes.AddRange(state);
+ }
+
+ private class Stroke(SKColor color, float width, bool eraser)
+ {
+ public SKColor Color { get; } = color;
+
+ public bool Eraser { get; } = eraser;
+
+ public List Points { get; } = [];
+
+ public float Width { get; } = width;
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaBoard/Helpers/CocoaHelper.cs b/sources/Experiments/SkiaBoard/Helpers/CocoaHelper.cs
new file mode 100644
index 00000000..4c4426c5
--- /dev/null
+++ b/sources/Experiments/SkiaBoard/Helpers/CocoaHelper.cs
@@ -0,0 +1,34 @@
+using System.Runtime.InteropServices;
+
+namespace SkiaBoard.Helpers;
+
+internal static partial class CocoaHelper
+{
+ private const string LibObjC = "/usr/lib/libobjc.A.dylib";
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_getClass")]
+ private static partial nint GetClass([MarshalAs(UnmanagedType.LPUTF8Str)] string name);
+
+ [LibraryImport(LibObjC, EntryPoint = "sel_registerName")]
+ private static partial nint Selector([MarshalAs(UnmanagedType.LPUTF8Str)] string name);
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
+ private static partial nint Send(nint receiver, nint selector);
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
+ private static partial nint Send(nint receiver, nint selector, [MarshalAs(UnmanagedType.I1)] bool arg);
+
+ [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
+ private static partial nint Send(nint receiver, nint selector, nint arg);
+
+ public static nint CreateLayer(nint cocoa)
+ {
+ nint layer = Send(GetClass("CAMetalLayer"), Selector("layer"));
+
+ nint view = Send(cocoa, Selector("contentView"));
+ Send(view, Selector("setWantsLayer:"), true);
+ Send(view, Selector("setLayer:"), layer);
+
+ return layer;
+ }
+}
diff --git a/sources/Experiments/SkiaBoard/Program.cs b/sources/Experiments/SkiaBoard/Program.cs
new file mode 100644
index 00000000..816f9b4a
--- /dev/null
+++ b/sources/Experiments/SkiaBoard/Program.cs
@@ -0,0 +1,3 @@
+using SkiaBoard;
+
+App.Run();
diff --git a/sources/Experiments/SkiaBoard/SkiaBoard.csproj b/sources/Experiments/SkiaBoard/SkiaBoard.csproj
new file mode 100644
index 00000000..8b441176
--- /dev/null
+++ b/sources/Experiments/SkiaBoard/SkiaBoard.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ $(StandardTargetFramework)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 4f92cea2df94fbadf806b298ba888baba561f499 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Sun, 2 Aug 2026 16:27:01 +0800
Subject: [PATCH 15/50] Add BOM to Zenith.NET.slnx solution file
The solution file was updated to include a Byte Order Mark (BOM) at the beginning. No other modifications to the solution structure or project references were made.
---
Zenith.NET.slnx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Zenith.NET.slnx b/Zenith.NET.slnx
index 92cb7395..22a0a870 100644
--- a/Zenith.NET.slnx
+++ b/Zenith.NET.slnx
@@ -1,4 +1,4 @@
-
+
From 686b12d0d08293b3e67c12438320c4b159825731 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 08:41:23 +0800
Subject: [PATCH 16/50] Replace SkiaBoard with responsive GPU gallery
---
Zenith.NET.slnx | 2 +-
sources/Experiments/SkiaBoard/Board.cs | 601 ------------------
sources/Experiments/SkiaBoard/Program.cs | 3 -
.../{SkiaBoard => SkiaGallery}/App.cs | 239 ++++---
sources/Experiments/SkiaGallery/Gallery.cs | 534 ++++++++++++++++
.../Experiments/SkiaGallery/GalleryPalette.cs | 16 +
.../SkiaGallery/GalleryResources.cs | 60 ++
.../Experiments/SkiaGallery/GalleryScene.cs | 80 +++
.../Helpers/CocoaHelper.cs | 6 +-
sources/Experiments/SkiaGallery/Program.cs | 3 +
.../SkiaGallery/Scenes/GeometryScene.cs | 310 +++++++++
.../SkiaGallery/Scenes/MotionScene.cs | 181 ++++++
.../SkiaGallery/Scenes/OverviewScene.cs | 121 ++++
.../SkiaGallery/Scenes/PaintScene.cs | 231 +++++++
.../SkiaGallery/Scenes/TypographyScene.cs | 260 ++++++++
.../SkiaGallery.csproj} | 4 +-
16 files changed, 1909 insertions(+), 742 deletions(-)
delete mode 100644 sources/Experiments/SkiaBoard/Board.cs
delete mode 100644 sources/Experiments/SkiaBoard/Program.cs
rename sources/Experiments/{SkiaBoard => SkiaGallery}/App.cs (51%)
create mode 100644 sources/Experiments/SkiaGallery/Gallery.cs
create mode 100644 sources/Experiments/SkiaGallery/GalleryPalette.cs
create mode 100644 sources/Experiments/SkiaGallery/GalleryResources.cs
create mode 100644 sources/Experiments/SkiaGallery/GalleryScene.cs
rename sources/Experiments/{SkiaBoard => SkiaGallery}/Helpers/CocoaHelper.cs (94%)
create mode 100644 sources/Experiments/SkiaGallery/Program.cs
create mode 100644 sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs
create mode 100644 sources/Experiments/SkiaGallery/Scenes/MotionScene.cs
create mode 100644 sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs
create mode 100644 sources/Experiments/SkiaGallery/Scenes/PaintScene.cs
create mode 100644 sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs
rename sources/Experiments/{SkiaBoard/SkiaBoard.csproj => SkiaGallery/SkiaGallery.csproj} (93%)
diff --git a/Zenith.NET.slnx b/Zenith.NET.slnx
index 22a0a870..1802c615 100644
--- a/Zenith.NET.slnx
+++ b/Zenith.NET.slnx
@@ -4,7 +4,7 @@
-
+
diff --git a/sources/Experiments/SkiaBoard/Board.cs b/sources/Experiments/SkiaBoard/Board.cs
deleted file mode 100644
index 4f65df7e..00000000
--- a/sources/Experiments/SkiaBoard/Board.cs
+++ /dev/null
@@ -1,601 +0,0 @@
-using SkiaSharp;
-
-namespace SkiaBoard;
-
-internal class Board
-{
- private const float ToolbarWidth = 112.0f;
- private const float MinBrushWidth = 1.0f;
- private const float MaxBrushWidth = 48.0f;
- private const float EraserWidthScale = 2.0f;
-
- private static readonly SKColor AccentColor = new(119, 190, 145);
- private static readonly SKColor BackgroundColor = new(219, 225, 221);
- private static readonly SKColor ToolbarColor = new(25, 42, 38);
-
- private static readonly SKColor PaperColor = new(250, 249, 246);
-
- private static readonly SKRect brushButton = new(12.0f, 16.0f, 52.0f, 66.0f);
- private static readonly SKRect eraserButton = new(60.0f, 16.0f, 100.0f, 66.0f);
- private static readonly SKRect undoButton = new(12.0f, 78.0f, 40.0f, 112.0f);
- private static readonly SKRect redoButton = new(42.0f, 78.0f, 70.0f, 112.0f);
- private static readonly SKRect clearButton = new(72.0f, 78.0f, 100.0f, 112.0f);
-
- private static readonly SKColor[] colors =
- [
- new(28, 34, 32),
- new(34, 101, 163),
- new(211, 63, 73),
- new(235, 151, 45),
- new(48, 139, 94),
- new(126, 79, 156)
- ];
-
- private readonly List strokes = [];
- private readonly Stack undoHistory = [];
- private readonly Stack redoHistory = [];
-
- private Stroke? activeStroke;
- private Tool tool;
- private int colorIndex;
- private SKPoint pointer;
- private bool adjustingBrush;
- private bool hasPointer;
-
- public float BrushWidth { get; private set; } = 8.0f;
-
- public string ToolName => tool is Tool.Brush ? "Brush" : "Eraser";
-
- private float StrokeWidth => tool is Tool.Eraser ? BrushWidth * EraserWidthScale : BrushWidth;
-
- public void Clear()
- {
- activeStroke = null;
-
- if (strokes.Count is 0)
- {
- return;
- }
-
- SaveState();
- strokes.Clear();
- }
-
- public void Draw(SKCanvas canvas, float width, float height)
- {
- canvas.Clear(BackgroundColor);
-
- SKRect paper = Paper(width, height);
-
- using SKPaint paint = new() { IsAntialias = true };
-
- paint.Color = new SKColor(108, 124, 115, 46);
- canvas.DrawRoundRect(new SKRect(paper.Left + 4.0f, paper.Top + 6.0f, paper.Right + 4.0f, paper.Bottom + 6.0f), 3.0f, 3.0f, paint);
-
- paint.Color = PaperColor;
- canvas.DrawRoundRect(paper, 3.0f, 3.0f, paint);
-
- DrawGrid(canvas, paper, paint);
-
- canvas.Save();
- canvas.ClipRect(paper);
- canvas.SaveLayer(paper, null);
-
- foreach (Stroke stroke in strokes)
- {
- DrawStroke(canvas, stroke, paint);
- }
-
- if (activeStroke is not null)
- {
- DrawStroke(canvas, activeStroke, paint);
- }
-
- canvas.Restore();
- canvas.Restore();
-
- DrawPointer(canvas, paper, paint);
- DrawToolbar(canvas, height, paint);
- }
-
- public void PointerDown(SKPoint point, float width, float height)
- {
- pointer = point;
- hasPointer = true;
-
- if (point.X < ToolbarWidth)
- {
- HandleToolbarClick(point, height);
- return;
- }
-
- if (!Paper(width, height).Contains(point.X, point.Y))
- {
- return;
- }
-
- activeStroke = new(colors[colorIndex], StrokeWidth, tool is Tool.Eraser);
- activeStroke.Points.Add(point);
- }
-
- public void PointerMove(SKPoint point, float width, float height)
- {
- pointer = point;
- hasPointer = true;
-
- if (adjustingBrush)
- {
- SetBrushWidth(point.Y, height);
- return;
- }
-
- if (activeStroke is null)
- {
- return;
- }
-
- SKRect paper = Paper(width, height);
-
- if (paper.Contains(point.X, point.Y))
- {
- AddPoint(activeStroke, point, false);
- }
- else
- {
- AddPoint(activeStroke, ClipToPaper(activeStroke.Points[^1], point, paper), true);
- CommitStroke();
- }
- }
-
- public void PointerUp(SKPoint point, float width, float height)
- {
- pointer = point;
- hasPointer = true;
- adjustingBrush = false;
-
- if (activeStroke is null)
- {
- return;
- }
-
- SKRect paper = Paper(width, height);
- AddPoint(activeStroke, paper.Contains(point.X, point.Y) ? point : ClipToPaper(activeStroke.Points[^1], point, paper), true);
- CommitStroke();
- }
-
- public void ResizeBrush(float delta)
- {
- BrushWidth = Math.Clamp(BrushWidth + delta, MinBrushWidth, MaxBrushWidth);
- }
-
- public void Undo()
- {
- activeStroke = null;
-
- if (undoHistory.TryPop(out Stroke[]? state))
- {
- redoHistory.Push([.. strokes]);
- Restore(state);
- }
- }
-
- public void Redo()
- {
- activeStroke = null;
-
- if (redoHistory.TryPop(out Stroke[]? state))
- {
- undoHistory.Push([.. strokes]);
- Restore(state);
- }
- }
-
- public void UseBrush()
- {
- tool = Tool.Brush;
- }
-
- public void UseEraser()
- {
- tool = Tool.Eraser;
- }
-
- private static void DrawGrid(SKCanvas canvas, SKRect paper, SKPaint paint)
- {
- paint.Color = new SKColor(92, 119, 105, 38);
-
- for (float y = paper.Top + 24.0f; y < paper.Bottom; y += 24.0f)
- {
- for (float x = paper.Left + 24.0f; x < paper.Right; x += 24.0f)
- {
- canvas.DrawCircle(x, y, 1.2f, paint);
- }
- }
- }
-
- private static void DrawStroke(SKCanvas canvas, Stroke stroke, SKPaint paint)
- {
- paint.Color = stroke.Color;
- paint.BlendMode = stroke.Eraser ? SKBlendMode.Clear : SKBlendMode.SrcOver;
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeCap = SKStrokeCap.Round;
- paint.StrokeJoin = SKStrokeJoin.Round;
- paint.StrokeWidth = stroke.Width;
-
- if (stroke.Points.Count is 1)
- {
- paint.Style = SKPaintStyle.Fill;
- canvas.DrawCircle(stroke.Points[0], stroke.Width * 0.5f, paint);
- return;
- }
-
- SKPathBuilder builder = new();
- builder.MoveTo(stroke.Points[0]);
-
- if (stroke.Points.Count is 2)
- {
- builder.LineTo(stroke.Points[1]);
- }
- else
- {
- for (int i = 1; i < stroke.Points.Count - 1; i++)
- {
- SKPoint point = stroke.Points[i];
- SKPoint next = stroke.Points[i + 1];
- builder.QuadTo(point, new SKPoint((point.X + next.X) * 0.5f, (point.Y + next.Y) * 0.5f));
- }
-
- builder.LineTo(stroke.Points[^1]);
- }
-
- using SKPath path = builder.Detach();
- canvas.DrawPath(path, paint);
- }
-
- private void DrawToolbar(SKCanvas canvas, float height, SKPaint paint)
- {
- paint.BlendMode = SKBlendMode.SrcOver;
- paint.Style = SKPaintStyle.Fill;
- paint.Color = ToolbarColor;
- canvas.DrawRect(0.0f, 0.0f, ToolbarWidth, height, paint);
-
- DrawToolButtons(canvas, paint);
- DrawDivider(canvas, paint, 126.0f);
- DrawPalette(canvas, paint);
- DrawDivider(canvas, paint, 278.0f);
- DrawBrushSlider(canvas, height, paint);
- }
-
- private void DrawToolButtons(SKCanvas canvas, SKPaint paint)
- {
- bool canUndo = undoHistory.Count > 0;
- bool canRedo = redoHistory.Count > 0;
- bool canClear = strokes.Count > 0;
-
- DrawButton(canvas, paint, brushButton, tool is Tool.Brush, true);
- DrawButton(canvas, paint, eraserButton, tool is Tool.Eraser, true);
- DrawButton(canvas, paint, undoButton, false, canUndo);
- DrawButton(canvas, paint, redoButton, false, canRedo);
- DrawButton(canvas, paint, clearButton, false, canClear);
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 4.0f;
- paint.StrokeCap = SKStrokeCap.Round;
- paint.Color = tool is Tool.Brush ? ToolbarColor : SKColors.White;
- canvas.DrawLine(23.0f, 50.0f, 43.0f, 30.0f, paint);
- canvas.DrawCircle(21.0f, 52.0f, 3.0f, paint);
-
- paint.Color = tool is Tool.Eraser ? ToolbarColor : SKColors.White;
- paint.StrokeWidth = 2.5f;
- canvas.Save();
- canvas.RotateDegrees(-35.0f, 83.0f, 41.0f);
- canvas.DrawRoundRect(new SKRect(72.0f, 32.0f, 94.0f, 50.0f), 2.0f, 2.0f, paint);
- canvas.Restore();
-
- DrawUndoIcon(canvas, paint, undoButton, false, canUndo);
- DrawUndoIcon(canvas, paint, redoButton, true, canRedo);
- DrawClearIcon(canvas, paint, clearButton, canClear);
- }
-
- private void DrawPalette(SKCanvas canvas, SKPaint paint)
- {
- for (int i = 0; i < colors.Length; i++)
- {
- SKRect swatch = ColorButton(i);
- SKPoint center = new(swatch.MidX, swatch.MidY);
- bool hovered = HasPointer(swatch);
-
- paint.Style = SKPaintStyle.Fill;
- paint.Color = colors[i];
- canvas.DrawCircle(center, hovered ? 15.0f : 13.0f, paint);
-
- if (i == colorIndex)
- {
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 2.5f;
- paint.Color = AccentColor;
- canvas.DrawCircle(center, 18.0f, paint);
- }
- }
- }
-
- private void DrawBrushSlider(SKCanvas canvas, float height, SKPaint paint)
- {
- SKRect track = BrushSlider(height);
-
- if (track.Height < 24.0f)
- {
- return;
- }
-
- float amount = (BrushWidth - MinBrushWidth) / (MaxBrushWidth - MinBrushWidth);
- float y = track.Bottom - (track.Height * amount);
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeCap = SKStrokeCap.Round;
- paint.StrokeWidth = 3.0f;
- paint.Color = new SKColor(93, 118, 108);
- canvas.DrawLine(track.MidX, track.Top, track.MidX, track.Bottom, paint);
-
- paint.Color = AccentColor;
- canvas.DrawLine(track.MidX, y, track.MidX, track.Bottom, paint);
-
- paint.Style = SKPaintStyle.Fill;
- paint.Color = HasPointer(new SKRect(track.Left - 10.0f, y - 12.0f, track.Right + 10.0f, y + 12.0f)) || adjustingBrush ? SKColors.White : AccentColor;
- canvas.DrawCircle(track.MidX, y, 8.0f, paint);
-
- float previewY = height - 42.0f;
- paint.Style = SKPaintStyle.Fill;
- paint.Color = tool is Tool.Eraser ? PaperColor : colors[colorIndex];
- canvas.DrawCircle(ToolbarWidth * 0.5f, previewY, MathF.Min(BrushWidth * 0.5f, 24.0f), paint);
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.5f;
- paint.Color = new SKColor(210, 222, 216);
- canvas.DrawCircle(ToolbarWidth * 0.5f, previewY, MathF.Min(BrushWidth * 0.5f, 24.0f), paint);
- }
-
- private void DrawPointer(SKCanvas canvas, SKRect paper, SKPaint paint)
- {
- if (!hasPointer || !paper.Contains(pointer.X, pointer.Y))
- {
- return;
- }
-
- paint.BlendMode = SKBlendMode.SrcOver;
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.5f;
- paint.Color = tool is Tool.Eraser ? new SKColor(36, 55, 49, 170) : colors[colorIndex];
-
- canvas.DrawCircle(pointer, StrokeWidth * 0.5f, paint);
- }
-
- private static void DrawUndoIcon(SKCanvas canvas, SKPaint paint, SKRect rect, bool redo, bool enabled)
- {
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeCap = SKStrokeCap.Round;
- paint.StrokeJoin = SKStrokeJoin.Round;
- paint.StrokeWidth = 2.0f;
- paint.Color = enabled ? SKColors.White : new SKColor(92, 112, 104);
-
- canvas.Save();
-
- if (redo)
- {
- canvas.Translate(rect.MidX * 2.0f, 0.0f);
- canvas.Scale(-1.0f, 1.0f);
- }
-
- SKRect arc = new(rect.MidX - 7.0f, rect.MidY - 7.0f, rect.MidX + 7.0f, rect.MidY + 7.0f);
- canvas.DrawArc(arc, 205.0f, 265.0f, false, paint);
- canvas.DrawLine(rect.MidX - 8.0f, rect.MidY - 3.0f, rect.MidX - 8.0f, rect.MidY + 3.0f, paint);
- canvas.DrawLine(rect.MidX - 8.0f, rect.MidY - 3.0f, rect.MidX - 2.0f, rect.MidY - 3.0f, paint);
-
- canvas.Restore();
- }
-
- private static void DrawClearIcon(SKCanvas canvas, SKPaint paint, SKRect rect, bool enabled)
- {
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeCap = SKStrokeCap.Round;
- paint.StrokeJoin = SKStrokeJoin.Round;
- paint.StrokeWidth = 2.0f;
- paint.Color = enabled ? SKColors.White : new SKColor(92, 112, 104);
-
- float x = rect.MidX;
- float y = rect.MidY;
- canvas.DrawRoundRect(new SKRect(x - 6.0f, y - 5.0f, x + 6.0f, y + 8.0f), 1.5f, 1.5f, paint);
- canvas.DrawLine(x - 8.0f, y - 8.0f, x + 8.0f, y - 8.0f, paint);
- canvas.DrawLine(x - 3.0f, y - 11.0f, x + 3.0f, y - 11.0f, paint);
- canvas.DrawLine(x - 2.0f, y - 2.0f, x - 2.0f, y + 5.0f, paint);
- canvas.DrawLine(x + 2.0f, y - 2.0f, x + 2.0f, y + 5.0f, paint);
- }
-
- private static void DrawDivider(SKCanvas canvas, SKPaint paint, float y)
- {
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = new SKColor(72, 94, 86);
- canvas.DrawLine(16.0f, y, ToolbarWidth - 16.0f, y, paint);
- }
-
- private void DrawButton(SKCanvas canvas, SKPaint paint, SKRect rect, bool selected, bool enabled)
- {
- paint.Style = SKPaintStyle.Fill;
- paint.Color = selected ? AccentColor : HasPointer(rect) && enabled ? new SKColor(47, 70, 63) : ToolbarColor;
- canvas.DrawRoundRect(rect, 5.0f, 5.0f, paint);
- }
-
- private bool HasPointer(SKRect rect)
- {
- return hasPointer && rect.Contains(pointer.X, pointer.Y);
- }
-
- private static SKRect Paper(float width, float height)
- {
- return new(ToolbarWidth + 16.0f,
- 16.0f,
- MathF.Max(ToolbarWidth + 16.0f, width - 16.0f),
- MathF.Max(16.0f, height - 16.0f));
- }
-
- private void HandleToolbarClick(SKPoint point, float height)
- {
- if (brushButton.Contains(point.X, point.Y))
- {
- UseBrush();
- return;
- }
-
- if (eraserButton.Contains(point.X, point.Y))
- {
- UseEraser();
- return;
- }
-
- if (undoButton.Contains(point.X, point.Y))
- {
- Undo();
- return;
- }
-
- if (redoButton.Contains(point.X, point.Y))
- {
- Redo();
- return;
- }
-
- if (clearButton.Contains(point.X, point.Y))
- {
- Clear();
- return;
- }
-
- if (BrushSlider(height).Contains(point.X, point.Y))
- {
- adjustingBrush = true;
- SetBrushWidth(point.Y, height);
- return;
- }
-
- for (int i = 0; i < colors.Length; i++)
- {
- if (ColorButton(i).Contains(point.X, point.Y))
- {
- colorIndex = i;
- UseBrush();
- return;
- }
- }
- }
-
- private static SKRect ColorButton(int index)
- {
- int column = index % 2;
- int row = index / 2;
- float left = 16.0f + (column * 48.0f);
- float top = 142.0f + (row * 44.0f);
- return new(left, top, left + 32.0f, top + 32.0f);
- }
-
- private static SKRect BrushSlider(float height)
- {
- return new(36.0f, 304.0f, 76.0f, MathF.Max(304.0f, height - 84.0f));
- }
-
- private void SetBrushWidth(float y, float height)
- {
- SKRect track = BrushSlider(height);
- float amount = 1.0f - Math.Clamp((y - track.Top) / track.Height, 0.0f, 1.0f);
- BrushWidth = MathF.Round((MinBrushWidth + (amount * (MaxBrushWidth - MinBrushWidth))) * 2.0f) * 0.5f;
- }
-
- private enum Tool
- {
- Brush,
- Eraser
- }
-
- private static void AddPoint(Stroke stroke, SKPoint point, bool includeEndpoint)
- {
- SKPoint previous = stroke.Points[^1];
- float x = point.X - previous.X;
- float y = point.Y - previous.Y;
- float distance = MathF.Sqrt((x * x) + (y * y));
- float spacing = MathF.Max(0.75f, stroke.Width * 0.1f);
-
- for (float offset = spacing; offset < distance; offset += spacing)
- {
- float amount = offset / distance;
- stroke.Points.Add(new(previous.X + (x * amount), previous.Y + (y * amount)));
- }
-
- if (includeEndpoint && distance > 0.01f)
- {
- stroke.Points.Add(point);
- }
- }
-
- private static SKPoint ClipToPaper(SKPoint start, SKPoint end, SKRect paper)
- {
- float x = end.X - start.X;
- float y = end.Y - start.Y;
- float amount = 1.0f;
-
- if (x > 0.0f)
- {
- amount = MathF.Min(amount, (paper.Right - start.X) / x);
- }
- else if (x < 0.0f)
- {
- amount = MathF.Min(amount, (paper.Left - start.X) / x);
- }
-
- if (y > 0.0f)
- {
- amount = MathF.Min(amount, (paper.Bottom - start.Y) / y);
- }
- else if (y < 0.0f)
- {
- amount = MathF.Min(amount, (paper.Top - start.Y) / y);
- }
-
- amount = Math.Clamp(amount, 0.0f, 1.0f);
- return new(start.X + (x * amount), start.Y + (y * amount));
- }
-
- private void CommitStroke()
- {
- if (activeStroke is null)
- {
- return;
- }
-
- SaveState();
- strokes.Add(activeStroke);
- activeStroke = null;
- }
-
- private void SaveState()
- {
- undoHistory.Push([.. strokes]);
- redoHistory.Clear();
- }
-
- private void Restore(Stroke[] state)
- {
- strokes.Clear();
- strokes.AddRange(state);
- }
-
- private class Stroke(SKColor color, float width, bool eraser)
- {
- public SKColor Color { get; } = color;
-
- public bool Eraser { get; } = eraser;
-
- public List Points { get; } = [];
-
- public float Width { get; } = width;
- }
-}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaBoard/Program.cs b/sources/Experiments/SkiaBoard/Program.cs
deleted file mode 100644
index 816f9b4a..00000000
--- a/sources/Experiments/SkiaBoard/Program.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-using SkiaBoard;
-
-App.Run();
diff --git a/sources/Experiments/SkiaBoard/App.cs b/sources/Experiments/SkiaGallery/App.cs
similarity index 51%
rename from sources/Experiments/SkiaBoard/App.cs
rename to sources/Experiments/SkiaGallery/App.cs
index 1bfc2efa..e6e74c75 100644
--- a/sources/Experiments/SkiaBoard/App.cs
+++ b/sources/Experiments/SkiaGallery/App.cs
@@ -1,62 +1,67 @@
-using System.Numerics;
+using System.Numerics;
using Silk.NET.Input;
using Silk.NET.Windowing;
-using SkiaBoard.Helpers;
+using SkiaGallery.Helpers;
+using SkiaSharp;
using Zenith.NET;
using Zenith.NET.DirectX12;
using Zenith.NET.Extensions.Skia;
using Zenith.NET.Metal;
using Zenith.NET.Vulkan;
-namespace SkiaBoard;
+namespace SkiaGallery;
internal static class App
{
private static readonly IWindow window;
private static readonly IInputContext input;
private static readonly SwapChain swapChain;
- private static readonly Board board;
+ private static readonly Gallery gallery;
+ private static readonly Action drawGallery = DrawGallery;
private static SKTexture texture;
- private static bool controlDown;
- private static bool shiftDown;
+ private static float logicalWidth;
+ private static float logicalHeight;
+ private static double totalSeconds;
static App()
{
- GraphicsApi graphicsApi = Environment.GetCommandLineArgs().Skip(1).FirstOrDefault()?.ToLowerInvariant() switch
+ if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux())
{
- "dx12" => GraphicsApi.DirectX12,
- "vulkan" => GraphicsApi.Vulkan,
- "metal" => GraphicsApi.Metal,
- _ when OperatingSystem.IsMacOS() => GraphicsApi.Metal,
- _ when OperatingSystem.IsLinux() => GraphicsApi.Vulkan,
- _ => GraphicsApi.DirectX12
- };
-
- Context = graphicsApi switch
+ throw new PlatformNotSupportedException("This application only supports Windows, macOS, and Linux.");
+ }
+
+ if (OperatingSystem.IsWindows())
+ {
+ Context = GraphicsContext.CreateDirectX12(useValidationLayer: true);
+ }
+ else if (OperatingSystem.IsMacOS())
{
- GraphicsApi.DirectX12 => GraphicsContext.CreateDirectX12(useValidationLayer: true),
- GraphicsApi.Metal => GraphicsContext.CreateMetal(useValidationLayer: true),
- GraphicsApi.Vulkan => GraphicsContext.CreateVulkan(useValidationLayer: true),
- _ => default!
- };
+ Context = GraphicsContext.CreateMetal(useValidationLayer: true);
+ }
+ else
+ {
+ Context = GraphicsContext.CreateVulkan(useValidationLayer: true);
+ }
Context.ValidationMessage += static (_, args) => Console.WriteLine($"[{args.Severity}] {args.Message}");
window = Window.Create(WindowOptions.Default with
{
API = GraphicsAPI.None,
- Title = $"Skia Board [{graphicsApi}]",
- Size = new(1280, 800)
+ Title = "Skia Gallery - Zenith.NET",
+ Size = new(1280, 800),
+ Position = new(80, 60),
+ IsVisible = true,
+ FramesPerSecond = 60.0,
+ UpdatesPerSecond = 60.0,
+ VSync = true
});
-
window.Initialize();
- window.Center();
input = window.CreateInput();
Surface surface;
-
if (OperatingSystem.IsWindows())
{
surface = Surface.Win32(window.Native!.Win32!.Value.Hwnd, Width, Height);
@@ -65,9 +70,13 @@ _ when OperatingSystem.IsLinux() => GraphicsApi.Vulkan,
{
surface = Surface.Apple(CocoaHelper.CreateLayer(window.Native!.Cocoa!.Value), Width, Height);
}
+ else if (window.Native?.X11 is { } x11)
+ {
+ surface = Surface.Xlib(x11.Display, (nint)x11.Window, Width, Height);
+ }
else
{
- surface = Surface.Xlib(window.Native!.X11!.Value.Display, (nint)window.Native.X11.Value.Window, Width, Height);
+ throw new PlatformNotSupportedException("SkiaGallery requires an X11 or XWayland window on Linux.");
}
swapChain = Context.CreateSwapChain(new()
@@ -77,7 +86,7 @@ _ when OperatingSystem.IsLinux() => GraphicsApi.Vulkan,
});
texture = CreateTexture(Width, Height);
- board = new();
+ gallery = new(Context.GraphicsApi, Context.Capabilities.DeviceName);
}
public static GraphicsContext Context { get; }
@@ -91,99 +100,26 @@ _ when OperatingSystem.IsLinux() => GraphicsApi.Vulkan,
public static void Run()
{
IMouse mouse = input.Mice[0];
- mouse.MouseDown += MouseDown;
- mouse.MouseUp += MouseUp;
mouse.MouseMove += MouseMove;
- mouse.Scroll += (_, wheel) => board.ResizeBrush(wheel.Y);
+ mouse.MouseDown += MouseDown;
IKeyboard keyboard = input.Keyboards[0];
keyboard.KeyDown += KeyDown;
- keyboard.KeyUp += KeyUp;
window.Render += Render;
- window.Run();
-
- texture.Dispose();
- swapChain.Dispose();
- input.Dispose();
- window.Dispose();
-
- Context.Dispose();
- }
-
- private static void MouseDown(IMouse mouse, MouseButton button)
- {
- if (button is MouseButton.Left && TryBoardSize(out float width, out float height))
- {
- board.PointerDown(new(mouse.Position.X, mouse.Position.Y), width, height);
- }
- }
-
- private static void MouseUp(IMouse mouse, MouseButton button)
- {
- if (button is MouseButton.Left && TryBoardSize(out float width, out float height))
- {
- board.PointerUp(new(mouse.Position.X, mouse.Position.Y), width, height);
- }
- }
-
- private static void MouseMove(IMouse _, Vector2 position)
- {
- if (TryBoardSize(out float width, out float height))
- {
- board.PointerMove(new(position.X, position.Y), width, height);
- }
- }
-
- private static void KeyDown(IKeyboard _, Key key, int code)
- {
- if (key is Key.ControlLeft or Key.ControlRight)
- {
- controlDown = true;
- }
- else if (key is Key.ShiftLeft or Key.ShiftRight)
- {
- shiftDown = true;
- }
- else if (key is Key.Z && controlDown)
- {
- if (shiftDown)
- {
- board.Redo();
- }
- else
- {
- board.Undo();
- }
- }
- else if (key is Key.Y && controlDown)
- {
- board.Redo();
- }
- else if (key is Key.Delete)
- {
- board.Clear();
- }
- else if (key is Key.B)
- {
- board.UseBrush();
- }
- else if (key is Key.E)
- {
- board.UseEraser();
- }
- }
-
- private static void KeyUp(IKeyboard _, Key key, int code)
- {
- if (key is Key.ControlLeft or Key.ControlRight)
+ try
{
- controlDown = false;
+ window.Run();
}
- else if (key is Key.ShiftLeft or Key.ShiftRight)
+ finally
{
- shiftDown = false;
+ gallery.Dispose();
+ texture.Dispose();
+ swapChain.Dispose();
+ input.Dispose();
+ window.Dispose();
+ Context.Dispose();
}
}
@@ -192,42 +128,87 @@ private static void Render(double delta)
uint width = Width;
uint height = Height;
- if (width is 0 || height is 0 || !TryBoardSize(out float boardWidth, out float boardHeight))
+ if (width is 0 || height is 0)
{
return;
}
Vector2 dpiScale = DpiScale;
+ float nextLogicalWidth = width / dpiScale.X;
+ float nextLogicalHeight = height / dpiScale.Y;
+ bool viewportChanged = logicalWidth != nextLogicalWidth || logicalHeight != nextLogicalHeight;
+
+ logicalWidth = nextLogicalWidth;
+ logicalHeight = nextLogicalHeight;
+ totalSeconds += Math.Min(delta, 0.1);
- Resize(width, height);
+ bool resized = Resize(width, height);
+ bool shouldRender = resized || viewportChanged || gallery.ShouldRender(totalSeconds);
- texture.Render(canvas =>
+ if (shouldRender)
{
- canvas.Save();
- canvas.Scale(dpiScale.X, dpiScale.Y);
- board.Draw(canvas, boardWidth, boardHeight);
- canvas.Restore();
- });
+ texture.Render(drawGallery);
+ }
CommandBuffer commandBuffer = Context.GraphicsQueue.CommandBuffer();
commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.CopyDst);
commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, TextureLayout.CopySrc);
-
commandBuffer.CopyTexture(texture, default, default, swapChain.Drawable, default, default, new()
{
Width = width,
Height = height,
Depth = 1
});
-
commandBuffer.Transition(texture, default, TextureLayout.CopySrc, TextureLayout.ColorAttachment);
commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.CopyDst, TextureLayout.Present);
commandBuffer.Submit().Wait();
swapChain.Present();
- window.Title = $"Skia Board [{Context.GraphicsApi}] - {board.ToolName} {board.BrushWidth:0.#}px";
+ }
+
+ private static void DrawGallery(SKCanvas canvas)
+ {
+ Vector2 dpiScale = DpiScale;
+
+ canvas.Save();
+ canvas.Scale(dpiScale.X, dpiScale.Y);
+ gallery.Draw(canvas, logicalWidth, logicalHeight, totalSeconds);
+ canvas.Restore();
+ }
+
+ private static void MouseMove(IMouse _, Vector2 position)
+ {
+ gallery.PointerMove(position);
+ }
+
+ private static void MouseDown(IMouse mouse, MouseButton button)
+ {
+ if (button is MouseButton.Left)
+ {
+ gallery.PointerDown(mouse.Position);
+ }
+ }
+
+ private static void KeyDown(IKeyboard _, Key key, int code)
+ {
+ if (key is Key.Left)
+ {
+ gallery.Previous();
+ }
+ else if (key is Key.Right)
+ {
+ gallery.Next();
+ }
+ else if (key is Key.Home)
+ {
+ gallery.Select(0);
+ }
+ else if (key is Key.End)
+ {
+ gallery.Select(gallery.SceneCount - 1);
+ }
}
private static SKTexture CreateTexture(uint width, uint height)
@@ -241,11 +222,11 @@ private static SKTexture CreateTexture(uint width, uint height)
});
}
- private static void Resize(uint width, uint height)
+ private static bool Resize(uint width, uint height)
{
if (texture.Desc.Width == width && texture.Desc.Height == height)
{
- return;
+ return false;
}
swapChain.Resize(width, height);
@@ -253,13 +234,7 @@ private static void Resize(uint width, uint height)
SKTexture oldTexture = texture;
texture = CreateTexture(width, height);
oldTexture.Dispose();
- }
-
- private static bool TryBoardSize(out float width, out float height)
- {
- width = window.Size.X;
- height = window.Size.Y;
- return width > 0.0f && height > 0.0f;
+ return true;
}
-}
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Gallery.cs b/sources/Experiments/SkiaGallery/Gallery.cs
new file mode 100644
index 00000000..ab86c65d
--- /dev/null
+++ b/sources/Experiments/SkiaGallery/Gallery.cs
@@ -0,0 +1,534 @@
+using System.Numerics;
+using SkiaSharp;
+using Zenith.NET;
+
+namespace SkiaGallery;
+
+internal sealed class Gallery : IDisposable
+{
+ private const float ExpandedSidebarWidth = 228.0f;
+ private const float CompactSidebarWidth = 80.0f;
+ private const float CompactBreakpoint = 1152.0f;
+ private const float ContentRight = 32.0f;
+ private const float DefaultContentTop = 142.0f;
+ private const float DenseContentTop = 112.0f;
+ private const float DefaultContentBottom = 82.0f;
+ private const float ReducedContentBottom = 24.0f;
+ private const float DefaultNavigationStep = 60.0f;
+ private const float MinimumNavigationStep = 48.0f;
+ private const float DenseHeightBreakpoint = 680.0f;
+ private const double TransitionDuration = 0.22;
+
+ private readonly GraphicsApi graphicsApi;
+ private readonly string deviceName;
+ private readonly GalleryResources resources = new();
+ private readonly GalleryScene[] scenes;
+ private readonly SKTextBlob[] titleBlobs;
+ private readonly SKTextBlob[] descriptionBlobs;
+ private readonly string[] descriptionTexts;
+ private readonly SKTextBlob[] compactTitleBlobs;
+ private readonly SKTextBlob pausedHeaderBlob;
+ private readonly SKTextBlob pausedTitleBlob;
+ private readonly SKTextBlob pausedDescriptionBlob;
+ private readonly SKPaint activePaint = new() { Color = GalleryPalette.Accent, IsAntialias = true };
+ private readonly SKPaint hoverPaint = new() { Color = new(49, 72, 64), IsAntialias = true };
+ private readonly SKPaint titlePaint = new() { Color = GalleryPalette.Ink, IsAntialias = true };
+ private readonly SKPaint descriptionPaint = new() { Color = GalleryPalette.Muted, IsAntialias = true };
+ private readonly SKPaint pausedIconPaint = new() { Color = GalleryPalette.Accent, IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 2.0f, StrokeCap = SKStrokeCap.Round };
+ private readonly float pausedHeaderWidth;
+ private readonly float pausedTitleWidth;
+ private readonly float pausedDescriptionWidth;
+
+ private int activeIndex;
+ private int hoverIndex = -1;
+ private SKPicture? chromePicture;
+ private SKPicture? navigationPicture;
+ private float viewportWidth = -1.0f;
+ private float viewportHeight = -1.0f;
+ private float sidebarWidth;
+ private float contentLeft;
+ private float sceneWidth;
+ private float sceneHeight;
+ private float navigationTop;
+ private float navigationStep = DefaultNavigationStep;
+ private float contentTop;
+ private float contentBottom;
+ private bool compact;
+ private bool dense;
+ private bool layoutPaused;
+ private bool showFooter = true;
+ private bool showNavigation = true;
+ private double lastSeconds;
+ private double transitionSeconds;
+ private bool dirty = true;
+ private bool transitionCompleteRendered;
+
+ public Gallery(GraphicsApi graphicsApi, string deviceName)
+ {
+ this.graphicsApi = graphicsApi;
+ this.deviceName = deviceName;
+
+ scenes =
+ [
+ new OverviewScene(resources),
+ new GeometryScene(resources),
+ new TypographyScene(resources),
+ new PaintScene(resources),
+ new MotionScene(resources)
+ ];
+
+ titleBlobs = new SKTextBlob[scenes.Length];
+ descriptionBlobs = new SKTextBlob[scenes.Length];
+ descriptionTexts = new string[scenes.Length];
+ compactTitleBlobs = new SKTextBlob[scenes.Length];
+
+ for (int i = 0; i < scenes.Length; i++)
+ {
+ titleBlobs[i] = resources.CreateText(scenes[i].Title, resources.TitleFont);
+ descriptionTexts[i] = scenes[i].Description;
+ descriptionBlobs[i] = resources.CreateText(descriptionTexts[i], resources.BodyFont);
+ compactTitleBlobs[i] = resources.CreateText(scenes[i].Navigation, resources.SectionFont);
+ }
+
+ const string pausedHeader = "GPU scene paused at this window size";
+ const string pausedTitle = "More room needed";
+ const string pausedDescription = "Increase the window size to continue.";
+
+ pausedHeaderBlob = resources.CreateText(pausedHeader, resources.BodyFont);
+ pausedTitleBlob = resources.CreateText(pausedTitle, resources.SectionFont);
+ pausedDescriptionBlob = resources.CreateText(pausedDescription, resources.BodyFont);
+ pausedHeaderWidth = resources.BodyFont.MeasureText(pausedHeader);
+ pausedTitleWidth = resources.SectionFont.MeasureText(pausedTitle);
+ pausedDescriptionWidth = resources.BodyFont.MeasureText(pausedDescription);
+ }
+
+ public int SceneCount => scenes.Length;
+
+ public bool ShouldRender(double seconds)
+ {
+ lastSeconds = seconds;
+
+ return dirty || (!layoutPaused && scenes[activeIndex].IsAnimated) || !transitionCompleteRendered;
+ }
+
+ public void Draw(SKCanvas canvas, float width, float height, double seconds)
+ {
+ SetViewport(width, height);
+ lastSeconds = seconds;
+ dirty = false;
+
+ float titleBaseline = dense ? 55.0f : 73.0f;
+ float descriptionBaseline = dense ? 82.0f : 108.0f;
+ float headerBottom = contentTop - 16.0f;
+
+ canvas.Clear(GalleryPalette.Background);
+ canvas.DrawPicture(chromePicture!);
+
+ if (showNavigation && hoverIndex >= 0 && hoverIndex != activeIndex)
+ {
+ canvas.DrawRoundRect(NavigationRect(hoverIndex), 6.0f, 6.0f, hoverPaint);
+ }
+
+ if (showNavigation)
+ {
+ canvas.DrawRoundRect(NavigationRect(activeIndex), 6.0f, 6.0f, activePaint);
+ }
+
+ canvas.DrawPicture(navigationPicture!);
+
+ canvas.Save();
+ canvas.ClipRect(new(contentLeft, 0.0f, MathF.Max(contentLeft, width - ContentRight), headerBottom));
+
+ if (layoutPaused)
+ {
+ canvas.DrawText(compactTitleBlobs[activeIndex], contentLeft, titleBaseline, titlePaint);
+
+ if (sceneWidth >= pausedHeaderWidth)
+ {
+ canvas.DrawText(pausedHeaderBlob, contentLeft, descriptionBaseline, descriptionPaint);
+ }
+ }
+ else
+ {
+ canvas.DrawText(titleBlobs[activeIndex], contentLeft, titleBaseline, titlePaint);
+ canvas.DrawText(descriptionBlobs[activeIndex], contentLeft + 1.0f, descriptionBaseline, descriptionPaint);
+ }
+
+ canvas.Restore();
+
+ if (layoutPaused)
+ {
+ transitionCompleteRendered = true;
+ DrawPausedState(canvas, width, height);
+ return;
+ }
+
+ float transition = Math.Clamp((float)((seconds - transitionSeconds) / TransitionDuration), 0.0f, 1.0f);
+ float eased = 1.0f - MathF.Pow(1.0f - transition, 3.0f);
+
+ transitionCompleteRendered = transition >= 1.0f;
+
+ canvas.Save();
+ canvas.ClipRect(new(contentLeft, contentTop, MathF.Max(contentLeft, width - ContentRight), MathF.Max(contentTop, height - contentBottom)));
+ canvas.Translate(contentLeft + ((1.0f - eased) * 18.0f), contentTop);
+ scenes[activeIndex].Draw(canvas, sceneWidth, sceneHeight, seconds);
+ canvas.Restore();
+ }
+
+ public void PointerMove(Vector2 position)
+ {
+ int index = HitTest(position);
+
+ if (index != hoverIndex)
+ {
+ hoverIndex = index;
+ dirty = true;
+ }
+ }
+
+ public void PointerDown(Vector2 position)
+ {
+ int index = HitTest(position);
+
+ if (index >= 0)
+ {
+ Select(index);
+ }
+ }
+
+ public void Previous()
+ {
+ Select((activeIndex + scenes.Length - 1) % scenes.Length);
+ }
+
+ public void Next()
+ {
+ Select((activeIndex + 1) % scenes.Length);
+ }
+
+ public void Select(int index)
+ {
+ if ((uint)index >= (uint)scenes.Length || index == activeIndex)
+ {
+ return;
+ }
+
+ activeIndex = index;
+ transitionSeconds = lastSeconds;
+ transitionCompleteRendered = false;
+ dirty = true;
+ }
+
+ public void Dispose()
+ {
+ for (int i = scenes.Length - 1; i >= 0; i--)
+ {
+ scenes[i].Dispose();
+ compactTitleBlobs[i].Dispose();
+ descriptionBlobs[i].Dispose();
+ titleBlobs[i].Dispose();
+ }
+
+ pausedDescriptionBlob.Dispose();
+ pausedTitleBlob.Dispose();
+ pausedHeaderBlob.Dispose();
+ navigationPicture?.Dispose();
+ chromePicture?.Dispose();
+ pausedIconPaint.Dispose();
+ descriptionPaint.Dispose();
+ titlePaint.Dispose();
+ hoverPaint.Dispose();
+ activePaint.Dispose();
+ resources.Dispose();
+ }
+
+ private SKPicture RecordChrome(float width, float height)
+ {
+ using SKPictureRecorder recorder = new();
+ SKCanvas canvas = recorder.BeginRecording(new(0.0f, 0.0f, width, height));
+ using SKPaint paint = new() { IsAntialias = true };
+
+ paint.Color = GalleryPalette.Background;
+ canvas.DrawRect(0.0f, 0.0f, width, height, paint);
+
+ paint.Color = GalleryPalette.Navigation;
+ canvas.DrawRect(0.0f, 0.0f, sidebarWidth, height, paint);
+
+ paint.Color = GalleryPalette.Accent;
+ float logoLeft = compact ? 20.0f : 28.0f;
+ canvas.DrawRoundRect(new(logoLeft, 32.0f, logoLeft + 40.0f, 72.0f), 6.0f, 6.0f, paint);
+
+ paint.Color = GalleryPalette.Navigation;
+ canvas.DrawCircle(logoLeft + 20.0f, 52.0f, 7.0f, paint);
+
+ if (!compact)
+ {
+ paint.Color = SKColors.White;
+ canvas.DrawText("SKIA", 82.0f, 50.0f, SKTextAlign.Left, resources.NavigationFont, paint);
+
+ paint.Color = new(137, 158, 150);
+ canvas.DrawText("GPU GALLERY", 82.0f, 68.0f, SKTextAlign.Left, resources.CaptionFont, paint);
+ }
+
+ paint.Color = new(72, 96, 87);
+ canvas.DrawLine(compact ? 16.0f : 24.0f, 118.0f, compact ? 64.0f : 204.0f, 118.0f, paint);
+
+ float contentRight = MathF.Max(contentLeft, width - ContentRight);
+
+ paint.Color = GalleryPalette.Line;
+ canvas.DrawLine(contentLeft, contentTop - 16.0f, contentRight, contentTop - 16.0f, paint);
+
+ if (showFooter)
+ {
+ float footerLine = height - 56.0f;
+ canvas.DrawLine(contentLeft, footerLine, contentRight, footerLine, paint);
+
+ paint.Color = GalleryPalette.Muted;
+ float footerBaseline = height - 25.0f;
+ string surfaceLabel = $"{graphicsApi} / SKIA GPU SURFACE";
+ canvas.DrawText(surfaceLabel, contentLeft, footerBaseline, SKTextAlign.Left, resources.CaptionFont, paint);
+
+ float labelRight = contentLeft + resources.CaptionFont.MeasureText(surfaceLabel);
+ float deviceRight = contentRight;
+ float deviceWidth = MathF.Max(0.0f, deviceRight - labelRight - 32.0f);
+
+ if (deviceWidth > resources.CaptionFont.MeasureText("..."))
+ {
+ string displayDevice = FitText(deviceName, resources.CaptionFont, MathF.Min(430.0f, deviceWidth));
+ canvas.DrawText(displayDevice, deviceRight, footerBaseline, SKTextAlign.Right, resources.CaptionFont, paint);
+ }
+ }
+
+ return recorder.EndRecording();
+ }
+
+ private static string FitText(string text, SKFont font, float width)
+ {
+ if (font.MeasureText(text) <= width)
+ {
+ return text;
+ }
+
+ const string ellipsis = "...";
+ int minimum = 0;
+ int maximum = text.Length;
+
+ while (minimum < maximum)
+ {
+ int length = (minimum + maximum + 1) / 2;
+ string candidate = string.Concat(text.AsSpan(0, length), ellipsis);
+
+ if (font.MeasureText(candidate) <= width)
+ {
+ minimum = length;
+ }
+ else
+ {
+ maximum = length - 1;
+ }
+ }
+
+ return string.Concat(text.AsSpan(0, minimum), ellipsis);
+ }
+
+ private SKPicture RecordNavigation(float width, float height)
+ {
+ using SKPictureRecorder recorder = new();
+ SKCanvas canvas = recorder.BeginRecording(new(0.0f, 0.0f, width, height));
+ using SKPaint paint = new() { Color = SKColors.White, IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 1.8f };
+
+ if (!showNavigation)
+ {
+ return recorder.EndRecording();
+ }
+
+ for (int i = 0; i < scenes.Length; i++)
+ {
+ float centerX = compact ? 40.0f : 44.0f;
+ float centerY = navigationTop + (i * navigationStep) + 24.0f;
+
+ DrawNavigationIcon(canvas, paint, i, centerX, centerY);
+
+ if (!compact)
+ {
+ paint.Style = SKPaintStyle.Fill;
+ canvas.DrawText(scenes[i].Navigation, 70.0f, centerY + 5.0f, SKTextAlign.Left, resources.NavigationFont, paint);
+ }
+ }
+
+ return recorder.EndRecording();
+ }
+
+ private static void DrawNavigationIcon(SKCanvas canvas, SKPaint paint, int index, float x, float y)
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.Color = SKColors.White;
+
+ if (index is 0)
+ {
+ canvas.DrawRoundRect(new(x - 8.0f, y - 8.0f, x - 1.0f, y - 1.0f), 1.5f, 1.5f, paint);
+ canvas.DrawRoundRect(new(x + 2.0f, y - 8.0f, x + 8.0f, y + 8.0f), 1.5f, 1.5f, paint);
+ canvas.DrawRoundRect(new(x - 8.0f, y + 2.0f, x - 1.0f, y + 8.0f), 1.5f, 1.5f, paint);
+ }
+ else if (index is 1)
+ {
+ canvas.DrawCircle(x - 3.0f, y - 2.0f, 6.0f, paint);
+ canvas.DrawRect(new(x, y - 5.0f, x + 9.0f, y + 7.0f), paint);
+ }
+ else if (index is 2)
+ {
+ canvas.DrawLine(x - 8.0f, y + 8.0f, x, y - 8.0f, paint);
+ canvas.DrawLine(x, y - 8.0f, x + 8.0f, y + 8.0f, paint);
+ canvas.DrawLine(x - 4.0f, y + 1.0f, x + 4.0f, y + 1.0f, paint);
+ }
+ else if (index is 3)
+ {
+ canvas.DrawCircle(x - 4.0f, y - 3.0f, 5.0f, paint);
+ canvas.DrawCircle(x + 4.0f, y - 3.0f, 5.0f, paint);
+ canvas.DrawCircle(x, y + 4.0f, 5.0f, paint);
+ }
+ else
+ {
+ canvas.DrawCircle(x, y, 8.0f, paint);
+ canvas.DrawCircle(x, y, 2.0f, paint);
+ canvas.DrawLine(x - 11.0f, y, x - 7.0f, y, paint);
+ canvas.DrawLine(x + 7.0f, y, x + 11.0f, y, paint);
+ }
+ }
+
+ private void SetViewport(float width, float height)
+ {
+ bool nextCompact = width < CompactBreakpoint;
+ bool nextDense = height < DenseHeightBreakpoint;
+ bool nextShowFooter = !nextDense;
+ float nextContentTop = nextDense ? DenseContentTop : DefaultContentTop;
+ float nextContentBottom = nextShowFooter ? DefaultContentBottom : ReducedContentBottom;
+ float nextSidebarWidth = nextCompact ? CompactSidebarWidth : ExpandedSidebarWidth;
+ float nextContentLeft = nextSidebarWidth + (nextCompact ? 24.0f : 32.0f);
+ float nextSceneWidth = MathF.Max(1.0f, width - nextContentLeft - ContentRight);
+ float nextSceneHeight = MathF.Max(1.0f, height - nextContentTop - nextContentBottom);
+ bool nextLayoutPaused = !scenes[activeIndex].CanRender(nextSceneWidth, nextSceneHeight);
+
+ float nextNavigationTop = nextCompact ? 136.0f : 156.0f;
+ float nextNavigationStep = DefaultNavigationStep;
+ bool nextShowNavigation = true;
+
+ if (nextDense)
+ {
+ nextNavigationTop = 104.0f;
+ float availableStep = (height - nextNavigationTop - 56.0f) / (scenes.Length - 1.0f);
+ nextShowNavigation = availableStep >= MinimumNavigationStep;
+ nextNavigationStep = MathF.Min(DefaultNavigationStep, availableStep);
+ }
+
+ if (width != viewportWidth || height != viewportHeight || nextCompact != compact || nextDense != dense || nextLayoutPaused != layoutPaused || nextShowFooter != showFooter || nextShowNavigation != showNavigation)
+ {
+ if (nextSceneWidth != sceneWidth)
+ {
+ UpdateDescriptionBlobs(nextSceneWidth);
+ }
+
+ compact = nextCompact;
+ dense = nextDense;
+ layoutPaused = nextLayoutPaused;
+ showFooter = nextShowFooter;
+ showNavigation = nextShowNavigation;
+ sidebarWidth = nextSidebarWidth;
+ contentLeft = nextContentLeft;
+ contentTop = nextContentTop;
+ contentBottom = nextContentBottom;
+ navigationTop = nextNavigationTop;
+ navigationStep = nextNavigationStep;
+ sceneWidth = nextSceneWidth;
+ sceneHeight = nextSceneHeight;
+
+ chromePicture?.Dispose();
+ navigationPicture?.Dispose();
+ chromePicture = RecordChrome(width, height);
+ navigationPicture = RecordNavigation(width, height);
+ viewportWidth = width;
+ viewportHeight = height;
+ dirty = true;
+ }
+ }
+
+ private void UpdateDescriptionBlobs(float width)
+ {
+ for (int i = 0; i < scenes.Length; i++)
+ {
+ string text = FitText(scenes[i].Description, resources.BodyFont, width - 2.0f);
+
+ if (text == descriptionTexts[i])
+ {
+ continue;
+ }
+
+ descriptionTexts[i] = text;
+ descriptionBlobs[i].Dispose();
+ descriptionBlobs[i] = resources.CreateText(text, resources.BodyFont);
+ }
+ }
+
+ private int HitTest(Vector2 position)
+ {
+ if (!showNavigation)
+ {
+ return -1;
+ }
+
+ for (int i = 0; i < scenes.Length; i++)
+ {
+ if (NavigationRect(i).Contains(position.X, position.Y))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private SKRect NavigationRect(int index)
+ {
+ float top = navigationTop + (index * navigationStep);
+
+ return compact ? new(10.0f, top, 70.0f, top + 48.0f) : new(18.0f, top, 210.0f, top + 48.0f);
+ }
+
+ private void DrawPausedState(SKCanvas canvas, float width, float height)
+ {
+ SKRect area = new(contentLeft, contentTop, MathF.Max(contentLeft, width - ContentRight), MathF.Max(contentTop, height - contentBottom));
+
+ if (area.Width < 120.0f || area.Height < 54.0f)
+ {
+ return;
+ }
+
+ canvas.Save();
+ canvas.ClipRect(area);
+
+ float centerX = area.MidX;
+ float titleBaseline = area.MidY + 11.0f;
+
+ if (area.Height >= 130.0f)
+ {
+ SKRect icon = new(centerX - 20.0f, titleBaseline - 73.0f, centerX + 20.0f, titleBaseline - 43.0f);
+ canvas.DrawRoundRect(icon, 4.0f, 4.0f, pausedIconPaint);
+ canvas.DrawLine(icon.Left - 7.0f, icon.Top + 7.0f, icon.Left + 3.0f, icon.Top + 7.0f, pausedIconPaint);
+ canvas.DrawLine(icon.Left + 7.0f, icon.Top - 7.0f, icon.Left + 7.0f, icon.Top + 3.0f, pausedIconPaint);
+ canvas.DrawLine(icon.Right - 3.0f, icon.Bottom - 7.0f, icon.Right + 7.0f, icon.Bottom - 7.0f, pausedIconPaint);
+ canvas.DrawLine(icon.Right - 7.0f, icon.Bottom - 3.0f, icon.Right - 7.0f, icon.Bottom + 7.0f, pausedIconPaint);
+ }
+
+ if (area.Width >= pausedTitleWidth + 24.0f)
+ {
+ canvas.DrawText(pausedTitleBlob, centerX - (pausedTitleWidth * 0.5f), titleBaseline, titlePaint);
+ }
+
+ if (area.Width >= pausedDescriptionWidth + 24.0f && area.Height >= 105.0f)
+ {
+ canvas.DrawText(pausedDescriptionBlob, centerX - (pausedDescriptionWidth * 0.5f), titleBaseline + 31.0f, descriptionPaint);
+ }
+
+ canvas.Restore();
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/GalleryPalette.cs b/sources/Experiments/SkiaGallery/GalleryPalette.cs
new file mode 100644
index 00000000..835e2791
--- /dev/null
+++ b/sources/Experiments/SkiaGallery/GalleryPalette.cs
@@ -0,0 +1,16 @@
+using SkiaSharp;
+
+namespace SkiaGallery;
+
+internal static class GalleryPalette
+{
+ public static readonly SKColor Background = new(239, 242, 240);
+ public static readonly SKColor Navigation = new(21, 37, 32);
+ public static readonly SKColor Ink = new(25, 36, 32);
+ public static readonly SKColor Muted = new(99, 115, 108);
+ public static readonly SKColor Line = new(219, 225, 222);
+ public static readonly SKColor Accent = new(37, 153, 116);
+ public static readonly SKColor Coral = new(229, 100, 90);
+ public static readonly SKColor Blue = new(61, 126, 204);
+ public static readonly SKColor Amber = new(231, 168, 66);
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/GalleryResources.cs b/sources/Experiments/SkiaGallery/GalleryResources.cs
new file mode 100644
index 00000000..0787c7d3
--- /dev/null
+++ b/sources/Experiments/SkiaGallery/GalleryResources.cs
@@ -0,0 +1,60 @@
+using SkiaSharp;
+
+namespace SkiaGallery;
+
+internal sealed class GalleryResources : IDisposable
+{
+ public GalleryResources()
+ {
+ string family = OperatingSystem.IsMacOS() ? "SF Pro Display" : OperatingSystem.IsWindows() ? "Segoe UI" : "Noto Sans";
+
+ RegularTypeface = SKTypeface.FromFamilyName(family, SKFontStyle.Normal);
+ MediumTypeface = SKTypeface.FromFamilyName(family, new(SKFontStyleWeight.SemiBold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright));
+
+ CaptionFont = CreateFont(RegularTypeface, 12.0f);
+ NavigationFont = CreateFont(MediumTypeface, 15.0f);
+ BodyFont = CreateFont(RegularTypeface, 16.0f);
+ SectionFont = CreateFont(MediumTypeface, 22.0f);
+ TitleFont = CreateFont(MediumTypeface, 34.0f);
+ }
+
+ public SKTypeface RegularTypeface { get; }
+
+ public SKTypeface MediumTypeface { get; }
+
+ public SKFont CaptionFont { get; }
+
+ public SKFont NavigationFont { get; }
+
+ public SKFont BodyFont { get; }
+
+ public SKFont SectionFont { get; }
+
+ public SKFont TitleFont { get; }
+
+ public SKTextBlob CreateText(string text, SKFont font)
+ {
+ return SKTextBlob.Create(text, font, default)!;
+ }
+
+ public void Dispose()
+ {
+ TitleFont.Dispose();
+ SectionFont.Dispose();
+ BodyFont.Dispose();
+ NavigationFont.Dispose();
+ CaptionFont.Dispose();
+ MediumTypeface.Dispose();
+ RegularTypeface.Dispose();
+ }
+
+ private static SKFont CreateFont(SKTypeface typeface, float size)
+ {
+ return new(typeface, size)
+ {
+ Edging = SKFontEdging.SubpixelAntialias,
+ Hinting = SKFontHinting.Slight,
+ Subpixel = true
+ };
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/GalleryScene.cs b/sources/Experiments/SkiaGallery/GalleryScene.cs
new file mode 100644
index 00000000..67f26270
--- /dev/null
+++ b/sources/Experiments/SkiaGallery/GalleryScene.cs
@@ -0,0 +1,80 @@
+using SkiaSharp;
+
+namespace SkiaGallery;
+
+internal abstract class GalleryScene(GalleryResources resources) : IDisposable
+{
+ private const float MinimumSceneWidth = 500.0f;
+ private const float MinimumWideSceneHeight = 280.0f;
+ private const float MinimumStackedSceneHeight = 420.0f;
+ private const float WideLayoutMinimumWidth = 620.0f;
+ private const float WideLayoutAspectRatio = 1.35f;
+
+ private SKPicture? staticPicture;
+ private float layoutWidth;
+ private float layoutHeight;
+
+ protected GalleryResources Resources { get; } = resources;
+
+ public abstract string Navigation { get; }
+
+ public abstract string Title { get; }
+
+ public abstract string Description { get; }
+
+ public virtual bool IsAnimated => false;
+
+ public virtual bool CanRender(float width, float height)
+ {
+ return width >= MinimumSceneWidth && height >= (UseWideLayout(width, height) ? MinimumWideSceneHeight : MinimumStackedSceneHeight);
+ }
+
+ public void Draw(SKCanvas canvas, float width, float height, double seconds)
+ {
+ EnsureLayout(width, height);
+ canvas.DrawPicture(staticPicture!);
+ DrawDynamic(canvas, width, height, seconds);
+ }
+
+ public void Dispose()
+ {
+ staticPicture?.Dispose();
+ DisposeResources();
+ }
+
+ protected abstract void UpdateLayout(float width, float height);
+
+ protected abstract void DrawStatic(SKCanvas canvas, float width, float height);
+
+ protected virtual void DrawDynamic(SKCanvas canvas, float width, float height, double seconds)
+ {
+ }
+
+ protected virtual void DisposeResources()
+ {
+ }
+
+ protected static bool UseWideLayout(float width, float height)
+ {
+ return width >= WideLayoutMinimumWidth && width >= height * WideLayoutAspectRatio;
+ }
+
+ private void EnsureLayout(float width, float height)
+ {
+ if (staticPicture is not null && layoutWidth == width && layoutHeight == height)
+ {
+ return;
+ }
+
+ staticPicture?.Dispose();
+ layoutWidth = width;
+ layoutHeight = height;
+ UpdateLayout(width, height);
+
+ using SKPictureRecorder recorder = new();
+ SKCanvas canvas = recorder.BeginRecording(new(0.0f, 0.0f, width, height));
+ DrawStatic(canvas, width, height);
+
+ staticPicture = recorder.EndRecording();
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaBoard/Helpers/CocoaHelper.cs b/sources/Experiments/SkiaGallery/Helpers/CocoaHelper.cs
similarity index 94%
rename from sources/Experiments/SkiaBoard/Helpers/CocoaHelper.cs
rename to sources/Experiments/SkiaGallery/Helpers/CocoaHelper.cs
index 4c4426c5..2976ad95 100644
--- a/sources/Experiments/SkiaBoard/Helpers/CocoaHelper.cs
+++ b/sources/Experiments/SkiaGallery/Helpers/CocoaHelper.cs
@@ -1,6 +1,6 @@
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
-namespace SkiaBoard.Helpers;
+namespace SkiaGallery.Helpers;
internal static partial class CocoaHelper
{
@@ -31,4 +31,4 @@ public static nint CreateLayer(nint cocoa)
return layer;
}
-}
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Program.cs b/sources/Experiments/SkiaGallery/Program.cs
new file mode 100644
index 00000000..5dfbda33
--- /dev/null
+++ b/sources/Experiments/SkiaGallery/Program.cs
@@ -0,0 +1,3 @@
+using SkiaGallery;
+
+App.Run();
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs b/sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs
new file mode 100644
index 00000000..44e27a4f
--- /dev/null
+++ b/sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs
@@ -0,0 +1,310 @@
+using SkiaSharp;
+
+namespace SkiaGallery;
+
+internal sealed class GeometryScene : GalleryScene
+{
+ private static readonly SKColor[] DotColors =
+ [
+ new(106, 190, 226, 210),
+ new(242, 130, 117, 210),
+ new(101, 211, 163, 210),
+ new(242, 190, 92, 210)
+ ];
+
+ private readonly SKPath starPath;
+ private readonly SKPoint[] wavePoints = new SKPoint[96];
+ private readonly SKShader shapeShader;
+ private readonly SKPathEffect dashEffect;
+ private readonly SKPaint shapePaint = new() { IsAntialias = true, Style = SKPaintStyle.Fill };
+ private readonly SKPaint outlinePaint = new() { Color = new(246, 250, 248, 225), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 2.0f };
+ private readonly SKPaint ghostPaint = new() { Color = new(157, 218, 222, 100), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 1.25f };
+ private readonly SKPaint dashPaint = new() { Color = new(248, 199, 101, 220), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 2.5f, StrokeCap = SKStrokeCap.Round };
+ private readonly SKPaint dotPaint = new() { IsAntialias = true, Style = SKPaintStyle.Fill };
+ private readonly SKPaint markerPaint = new() { Color = new(255, 247, 225), IsAntialias = true, Style = SKPaintStyle.Fill };
+ private readonly SKPaint markerRingPaint = new() { Color = new(242, 130, 117), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 3.0f };
+
+ private SKPath? clipPath;
+ private SKRect stage;
+ private SKRect clipRect;
+ private SKRect waveRect;
+ private SKPoint starCenter;
+ private SKPoint bezierStart;
+ private SKPoint firstControl;
+ private SKPoint secondControl;
+ private SKPoint bezierEnd;
+ private float starScale;
+
+ public GeometryScene(GalleryResources resources) : base(resources)
+ {
+ starPath = CreateStar(72.0f, 31.0f, 7);
+ shapeShader = SKShader.CreateLinearGradient(new(-78.0f, -72.0f), new(82.0f, 76.0f), [new(242, 130, 117), new(242, 190, 92), new(101, 211, 163)], [0.0f, 0.5f, 1.0f], SKShaderTileMode.Clamp);
+ shapePaint.Shader = shapeShader;
+
+ dashEffect = SKPathEffect.CreateDash([10.0f, 8.0f], 0.0f);
+ dashPaint.PathEffect = dashEffect;
+ }
+
+ public override string Navigation => "Geometry";
+
+ public override string Title => "Geometry blueprint";
+
+ public override string Description => "Transforms, curves, clipping, and stroke phase on one construction surface.";
+
+ public override bool IsAnimated => true;
+
+ protected override void UpdateLayout(float width, float height)
+ {
+ stage = new(0.0f, 0.0f, width, height);
+
+ if (UseWideLayout(width, height))
+ {
+ starCenter = new(width * 0.31f, height * 0.43f);
+ starScale = Math.Clamp(MathF.Min(width * 0.17f, height * 0.25f) / 72.0f, 0.8f, 1.7f);
+ clipRect = new(width * 0.66f, height * 0.18f, width * 0.93f, height * 0.61f);
+ bezierStart = new(width * 0.08f, height * 0.81f);
+ firstControl = new(width * 0.33f, height * 0.57f);
+ secondControl = new(width * 0.64f, height * 0.94f);
+ bezierEnd = new(width * 0.92f, height * 0.72f);
+ waveRect = new(width * 0.09f, height * 0.90f, width * 0.91f, height * 0.97f);
+ }
+ else
+ {
+ starCenter = new(width * 0.50f, height * 0.24f);
+ starScale = Math.Clamp(MathF.Min(width * 0.24f, height * 0.13f) / 72.0f, 0.72f, 1.3f);
+ clipRect = new(width * 0.14f, height * 0.48f, width * 0.86f, height * 0.71f);
+ bezierStart = new(width * 0.08f, height * 0.86f);
+ firstControl = new(width * 0.27f, height * 0.69f);
+ secondControl = new(width * 0.70f, height * 0.96f);
+ bezierEnd = new(width * 0.92f, height * 0.78f);
+ waveRect = new(width * 0.10f, height * 0.39f, width * 0.90f, height * 0.44f);
+ }
+
+ clipPath?.Dispose();
+ using SKPathBuilder builder = new();
+ builder.AddRoundRect(clipRect, 18.0f, 18.0f, SKPathDirection.Clockwise);
+ clipPath = builder.Detach();
+ }
+
+ protected override void DrawDynamic(SKCanvas canvas, float width, float height, double seconds)
+ {
+ float time = (float)seconds;
+
+ DrawTransformedStar(canvas, time, starScale, 16.0f, shapePaint, outlinePaint);
+ DrawTransformedStar(canvas, time, starScale * 0.72f, -24.0f, null, outlinePaint);
+ DrawTransformedStar(canvas, time, starScale * 0.45f, 36.0f, null, ghostPaint);
+
+ for (int i = 0; i < wavePoints.Length; i++)
+ {
+ float amount = i / (wavePoints.Length - 1.0f);
+ float envelope = MathF.Sin(amount * MathF.PI);
+ float y = waveRect.MidY + (MathF.Sin((amount * 13.0f) + (time * 1.5f)) * waveRect.Height * 0.42f * envelope);
+ wavePoints[i] = new(waveRect.Left + (amount * waveRect.Width), y);
+ }
+
+ canvas.DrawPoints(SKPointMode.Polygon, wavePoints, dashPaint);
+ DrawClipField(canvas, time);
+
+ float markerAmount = 0.5f + (MathF.Sin(time * 0.72f) * 0.5f);
+ SKPoint marker = CubicPoint(markerAmount, bezierStart, firstControl, secondControl, bezierEnd);
+ canvas.DrawCircle(marker, 8.0f, markerPaint);
+ canvas.DrawCircle(marker, 8.0f, markerRingPaint);
+ }
+
+ protected override void DrawStatic(SKCanvas canvas, float width, float height)
+ {
+ using SKPaint paint = new() { IsAntialias = true };
+ using SKShader background = SKShader.CreateLinearGradient(new(stage.Left, stage.Top), new(stage.Right, stage.Bottom), [new(28, 71, 78), new(31, 54, 75), new(72, 53, 66)], [0.0f, 0.58f, 1.0f], SKShaderTileMode.Clamp);
+
+ paint.Shader = background;
+ canvas.DrawRoundRect(stage, 6.0f, 6.0f, paint);
+ paint.Shader = null;
+
+ DrawGrid(canvas, paint);
+ DrawStarConstruction(canvas, paint);
+ DrawBezierConstruction(canvas, paint);
+ DrawClipConstruction(canvas, paint);
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = new(198, 228, 226, 120);
+ canvas.DrawRoundRect(stage, 6.0f, 6.0f, paint);
+ }
+
+ protected override void DisposeResources()
+ {
+ clipPath?.Dispose();
+ markerRingPaint.Dispose();
+ markerPaint.Dispose();
+ dotPaint.Dispose();
+ dashPaint.Dispose();
+ ghostPaint.Dispose();
+ outlinePaint.Dispose();
+ shapePaint.Dispose();
+ dashEffect.Dispose();
+ shapeShader.Dispose();
+ starPath.Dispose();
+ }
+
+ private void DrawTransformedStar(SKCanvas canvas, float time, float scale, float speed, SKPaint? fill, SKPaint outline)
+ {
+ canvas.Save();
+ canvas.Translate(starCenter);
+ canvas.RotateDegrees((time * speed) + (speed * 0.7f));
+ canvas.Scale(scale * (1.0f + (MathF.Sin((time * 0.8f) + scale) * 0.025f)));
+
+ if (fill is not null)
+ {
+ canvas.DrawPath(starPath, fill);
+ }
+
+ canvas.DrawPath(starPath, outline);
+ canvas.Restore();
+ }
+
+ private void DrawClipField(SKCanvas canvas, float time)
+ {
+ canvas.Save();
+ canvas.ClipPath(clipPath!, SKClipOperation.Intersect, true);
+
+ int columns = Math.Clamp((int)(clipRect.Width / 31.0f), 7, 14);
+ int rows = Math.Clamp((int)(clipRect.Height / 27.0f), 4, 10);
+ float xStep = clipRect.Width / MathF.Max(1.0f, columns - 1.0f);
+ float yStep = clipRect.Height / MathF.Max(1.0f, rows - 1.0f);
+
+ for (int row = 0; row < rows; row++)
+ {
+ for (int column = 0; column < columns; column++)
+ {
+ float phase = (time * 1.35f) + (row * 0.55f) + (column * 0.34f);
+ float x = clipRect.Left + (column * xStep) + (MathF.Cos(phase) * MathF.Min(4.0f, xStep * 0.12f));
+ float y = clipRect.Top + (row * yStep) + (MathF.Sin(phase) * MathF.Min(5.0f, yStep * 0.17f));
+ dotPaint.Color = DotColors[(row + column) % DotColors.Length];
+ canvas.DrawCircle(x, y, 4.2f, dotPaint);
+ }
+ }
+
+ canvas.Restore();
+ canvas.DrawPath(clipPath!, outlinePaint);
+ }
+
+ private void DrawGrid(SKCanvas canvas, SKPaint paint)
+ {
+ float spacing = Math.Clamp(MathF.Min(stage.Width, stage.Height) / 16.0f, 24.0f, 38.0f);
+ int verticalIndex = 0;
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+
+ for (float x = stage.Left; x <= stage.Right; x += spacing)
+ {
+ paint.Color = verticalIndex % 4 is 0 ? new(190, 226, 225, 40) : new(190, 226, 225, 18);
+ canvas.DrawLine(x, stage.Top, x, stage.Bottom, paint);
+ verticalIndex++;
+ }
+
+ int horizontalIndex = 0;
+ for (float y = stage.Top; y <= stage.Bottom; y += spacing)
+ {
+ paint.Color = horizontalIndex % 4 is 0 ? new(190, 226, 225, 40) : new(190, 226, 225, 18);
+ canvas.DrawLine(stage.Left, y, stage.Right, y, paint);
+ horizontalIndex++;
+ }
+ }
+
+ private void DrawStarConstruction(SKCanvas canvas, SKPaint paint)
+ {
+ float radius = 72.0f * starScale;
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = new(205, 235, 232, 85);
+ canvas.DrawCircle(starCenter, radius * 1.07f, paint);
+ canvas.DrawCircle(starCenter, radius * 0.47f, paint);
+ canvas.DrawLine(starCenter.X - (radius * 1.18f), starCenter.Y, starCenter.X + (radius * 1.18f), starCenter.Y, paint);
+ canvas.DrawLine(starCenter.X, starCenter.Y - (radius * 1.18f), starCenter.X, starCenter.Y + (radius * 1.18f), paint);
+
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = new(220, 241, 238, 180);
+ canvas.DrawText("TRANSFORM / 7-POINT PATH", starCenter.X - radius, starCenter.Y - (radius * 1.34f), SKTextAlign.Left, Resources.CaptionFont, paint);
+ }
+
+ private void DrawBezierConstruction(SKCanvas canvas, SKPaint paint)
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = new(202, 232, 229, 105);
+ canvas.DrawLine(bezierStart, firstControl, paint);
+ canvas.DrawLine(secondControl, bezierEnd, paint);
+
+ using SKPathBuilder builder = new();
+ builder.MoveTo(bezierStart);
+ builder.CubicTo(firstControl, secondControl, bezierEnd);
+ using SKPath bezier = builder.Detach();
+
+ paint.Color = new(242, 130, 117, 235);
+ paint.StrokeWidth = 3.5f;
+ paint.StrokeCap = SKStrokeCap.Round;
+ canvas.DrawPath(bezier, paint);
+
+ SKPoint[] handles = [bezierStart, firstControl, secondControl, bezierEnd];
+ paint.Style = SKPaintStyle.Fill;
+ for (int i = 0; i < handles.Length; i++)
+ {
+ paint.Color = i is 0 or 3 ? new(250, 246, 229) : new(106, 190, 226);
+ canvas.DrawCircle(handles[i], i is 0 or 3 ? 4.5f : 3.5f, paint);
+ }
+
+ paint.Color = new(220, 241, 238, 180);
+ canvas.DrawText("CUBIC TRAJECTORY", bezierStart.X, bezierStart.Y - 18.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
+ }
+
+ private void DrawClipConstruction(SKCanvas canvas, SKPaint paint)
+ {
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = new(232, 246, 243, 20);
+ canvas.DrawRoundRect(clipRect, 18.0f, 18.0f, paint);
+
+ paint.Color = new(220, 241, 238, 180);
+ canvas.DrawText("CLIP WINDOW", clipRect.Left, clipRect.Top - 14.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
+
+ paint.Color = new(248, 199, 101, 180);
+ canvas.DrawText("STROKE PHASE", waveRect.Left, waveRect.Top - 10.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
+ }
+
+ private static SKPoint CubicPoint(float amount, SKPoint start, SKPoint first, SKPoint second, SKPoint end)
+ {
+ float inverse = 1.0f - amount;
+ float inverseSquared = inverse * inverse;
+ float amountSquared = amount * amount;
+
+ return new(
+ (inverseSquared * inverse * start.X) + (3.0f * inverseSquared * amount * first.X) + (3.0f * inverse * amountSquared * second.X) + (amountSquared * amount * end.X),
+ (inverseSquared * inverse * start.Y) + (3.0f * inverseSquared * amount * first.Y) + (3.0f * inverse * amountSquared * second.Y) + (amountSquared * amount * end.Y));
+ }
+
+ private static SKPath CreateStar(float outerRadius, float innerRadius, int points)
+ {
+ using SKPathBuilder builder = new();
+
+ for (int i = 0; i < points * 2; i++)
+ {
+ float angle = (-MathF.PI * 0.5f) + (i * MathF.PI / points);
+ float radius = i % 2 is 0 ? outerRadius : innerRadius;
+ float x = MathF.Cos(angle) * radius;
+ float y = MathF.Sin(angle) * radius;
+
+ if (i is 0)
+ {
+ builder.MoveTo(x, y);
+ }
+ else
+ {
+ builder.LineTo(x, y);
+ }
+ }
+
+ builder.Close();
+ return builder.Detach();
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Scenes/MotionScene.cs b/sources/Experiments/SkiaGallery/Scenes/MotionScene.cs
new file mode 100644
index 00000000..4cffb97d
--- /dev/null
+++ b/sources/Experiments/SkiaGallery/Scenes/MotionScene.cs
@@ -0,0 +1,181 @@
+using SkiaSharp;
+
+namespace SkiaGallery;
+
+internal sealed class MotionScene : GalleryScene
+{
+ private static readonly SKColor[] ParticleColors = [new(77, 151, 235, 220), new(52, 194, 146, 220), new(245, 112, 103, 220), new(247, 190, 80, 220)];
+
+ private readonly Particle[] particles = new Particle[48];
+ private readonly SKPoint[] firstRibbonPoints = new SKPoint[96];
+ private readonly SKPoint[] secondRibbonPoints = new SKPoint[96];
+ private readonly SKPoint[] thirdRibbonPoints = new SKPoint[96];
+ private readonly SKShader firstShader;
+ private readonly SKShader secondShader;
+ private readonly SKPaint firstRibbonPaint = new() { IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 4.0f, StrokeCap = SKStrokeCap.Round };
+ private readonly SKPaint secondRibbonPaint = new() { IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 3.0f, StrokeCap = SKStrokeCap.Round };
+ private readonly SKPaint thirdRibbonPaint = new() { Color = new(122, 100, 186), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 2.5f, StrokeCap = SKStrokeCap.Round };
+ private readonly SKPaint ribbonGlowPaint = new() { IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 12.0f, StrokeCap = SKStrokeCap.Round };
+ private readonly SKPaint orbitPaint = new() { Color = new(238, 242, 239, 38), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 1.0f };
+ private readonly SKPaint particlePaint = new() { IsAntialias = true, Style = SKPaintStyle.Fill };
+ private readonly SKPaint corePaint = new() { Color = new(244, 246, 240), IsAntialias = true, Style = SKPaintStyle.Fill };
+
+ private SKRect arenaRect;
+ private SKRect waveRect;
+ private SKPoint orbitCenter;
+ private float orbitRadius;
+
+ public MotionScene(GalleryResources resources) : base(resources)
+ {
+ for (int i = 0; i < particles.Length; i++)
+ {
+ particles[i] = new(
+ 0.38f + ((i % 6) * 0.105f),
+ 0.16f + ((i % 9) * 0.028f),
+ i * 0.73f,
+ 1.5f + ((i % 3) * 0.75f),
+ i % ParticleColors.Length);
+ }
+
+ firstShader = SKShader.CreateLinearGradient(new(0.0f, 0.0f), new(1400.0f, 0.0f), [GalleryPalette.Blue, GalleryPalette.Accent, GalleryPalette.Amber], SKShaderTileMode.Clamp);
+ secondShader = SKShader.CreateLinearGradient(new(0.0f, 0.0f), new(1400.0f, 0.0f), [GalleryPalette.Coral, GalleryPalette.Amber, GalleryPalette.Blue], SKShaderTileMode.Clamp);
+ firstRibbonPaint.Shader = firstShader;
+ secondRibbonPaint.Shader = secondShader;
+ }
+
+ public override string Navigation => "Motion";
+
+ public override string Title => "Kinetic field";
+
+ public override string Description => "Polylines and a fixed particle pool moving across one GPU field.";
+
+ public override bool IsAnimated => true;
+
+ protected override void UpdateLayout(float width, float height)
+ {
+ arenaRect = new(0.0f, 0.0f, width, height);
+ waveRect = new(-24.0f, 0.0f, width + 24.0f, height);
+
+ if (UseWideLayout(width, height))
+ {
+ orbitCenter = new(width * 0.73f, height * 0.52f);
+ orbitRadius = MathF.Min(width * 0.24f, height * 0.38f);
+ }
+ else
+ {
+ orbitCenter = new(width * 0.57f, height * 0.63f);
+ orbitRadius = MathF.Min(width * 0.35f, height * 0.22f);
+ }
+ }
+
+ protected override void DrawDynamic(SKCanvas canvas, float width, float height, double seconds)
+ {
+ float time = (float)seconds;
+ bool wide = UseWideLayout(width, height);
+ float firstCenter = height * (wide ? 0.28f : 0.22f);
+ float secondCenter = height * (wide ? 0.52f : 0.43f);
+ float thirdCenter = height * (wide ? 0.76f : 0.78f);
+
+ UpdateRibbon(firstRibbonPoints, waveRect.Left, waveRect.Right, firstCenter, height * 0.075f, time * 1.4f, 13.0f);
+ UpdateRibbon(secondRibbonPoints, waveRect.Left, waveRect.Right, secondCenter, height * 0.062f, (-time * 1.1f) + 2.0f, 17.0f);
+ UpdateRibbon(thirdRibbonPoints, waveRect.Left, waveRect.Right, thirdCenter, height * 0.052f, (time * 0.8f) + 4.0f, 9.0f);
+
+ canvas.Save();
+ canvas.ClipRect(arenaRect);
+
+ ribbonGlowPaint.Color = new(61, 126, 204, 32);
+ canvas.DrawPoints(SKPointMode.Polygon, firstRibbonPoints, ribbonGlowPaint);
+ ribbonGlowPaint.Color = new(229, 100, 90, 30);
+ canvas.DrawPoints(SKPointMode.Polygon, secondRibbonPoints, ribbonGlowPaint);
+ ribbonGlowPaint.Color = new(122, 100, 186, 28);
+ canvas.DrawPoints(SKPointMode.Polygon, thirdRibbonPoints, ribbonGlowPaint);
+
+ canvas.DrawPoints(SKPointMode.Polygon, firstRibbonPoints, firstRibbonPaint);
+ canvas.DrawPoints(SKPointMode.Polygon, secondRibbonPoints, secondRibbonPaint);
+ canvas.DrawPoints(SKPointMode.Polygon, thirdRibbonPoints, thirdRibbonPaint);
+
+ canvas.Translate(orbitCenter);
+
+ for (int i = 0; i < particles.Length; i++)
+ {
+ Particle particle = particles[i];
+ float angle = particle.Phase + (time * particle.Speed);
+ float radius = (particle.OrbitFactor * orbitRadius) + (MathF.Sin((time * 0.8f) + particle.Phase) * MathF.Min(5.0f, orbitRadius * 0.035f));
+ float x = MathF.Cos(angle) * radius;
+ float y = MathF.Sin(angle) * radius * 0.74f;
+
+ particlePaint.Color = ParticleColors[particle.ColorIndex];
+ canvas.DrawCircle(x, y, particle.Radius, particlePaint);
+ }
+
+ particlePaint.Color = new(244, 246, 240, 35);
+ canvas.DrawCircle(0.0f, 0.0f, MathF.Max(17.0f, orbitRadius * 0.15f), particlePaint);
+ canvas.DrawCircle(0.0f, 0.0f, MathF.Max(7.0f, orbitRadius * 0.055f) + (MathF.Sin(time * 2.0f) * 1.5f), corePaint);
+ canvas.Restore();
+ }
+
+ protected override void DrawStatic(SKCanvas canvas, float width, float height)
+ {
+ using SKPaint paint = new() { IsAntialias = true };
+ using SKShader background = SKShader.CreateLinearGradient(
+ new(arenaRect.Left, arenaRect.Top),
+ new(arenaRect.Right, arenaRect.Bottom),
+ [new(13, 28, 25), new(28, 39, 48), new(40, 27, 39)],
+ [0.0f, 0.58f, 1.0f],
+ SKShaderTileMode.Clamp);
+
+ paint.Shader = background;
+ canvas.DrawRoundRect(arenaRect, 6.0f, 6.0f, paint);
+ paint.Shader = null;
+
+ paint.Color = new(238, 242, 239, 22);
+ float stepX = width / 14.0f;
+ float stepY = height / 9.0f;
+ for (int row = 1; row < 9; row++)
+ {
+ for (int column = 1; column < 14; column++)
+ {
+ canvas.DrawCircle(column * stepX, row * stepY, 1.0f, paint);
+ }
+ }
+
+ canvas.Save();
+ canvas.Translate(orbitCenter);
+ canvas.Scale(1.0f, 0.74f);
+ for (int orbit = 1; orbit <= 6; orbit++)
+ {
+ canvas.DrawCircle(0.0f, 0.0f, orbitRadius * orbit / 6.0f, orbitPaint);
+ }
+ canvas.Restore();
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = new(238, 242, 239, 50);
+ canvas.DrawRoundRect(arenaRect, 6.0f, 6.0f, paint);
+ }
+
+ protected override void DisposeResources()
+ {
+ corePaint.Dispose();
+ particlePaint.Dispose();
+ orbitPaint.Dispose();
+ ribbonGlowPaint.Dispose();
+ thirdRibbonPaint.Dispose();
+ secondRibbonPaint.Dispose();
+ firstRibbonPaint.Dispose();
+ secondShader.Dispose();
+ firstShader.Dispose();
+ }
+
+ private static void UpdateRibbon(SKPoint[] points, float left, float right, float centerY, float amplitude, float phase, float frequency)
+ {
+ for (int i = 0; i < points.Length; i++)
+ {
+ float amount = i / (points.Length - 1.0f);
+ float y = centerY + (MathF.Sin((amount * frequency) + phase) * amplitude) + (MathF.Sin((amount * frequency * 2.3f) - (phase * 0.37f)) * amplitude * 0.18f);
+ points[i] = new(left + (amount * (right - left)), y);
+ }
+ }
+
+ private readonly record struct Particle(float OrbitFactor, float Speed, float Phase, float Radius, int ColorIndex);
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs b/sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs
new file mode 100644
index 00000000..4e0f060e
--- /dev/null
+++ b/sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs
@@ -0,0 +1,121 @@
+using SkiaSharp;
+
+namespace SkiaGallery;
+
+internal sealed class OverviewScene(GalleryResources resources) : GalleryScene(resources)
+{
+ private SKRect stage;
+
+ public override string Navigation => "Overview";
+
+ public override string Title => "Chromatic assembly";
+
+ public override string Description => "Hard-edged geometry composed from color, scale, and overlap.";
+
+ protected override void UpdateLayout(float width, float height)
+ {
+ stage = new(0.0f, 0.0f, width, height);
+ }
+
+ protected override void DrawStatic(SKCanvas canvas, float width, float height)
+ {
+ using SKPaint paint = new() { IsAntialias = true };
+
+ paint.Color = new(246, 243, 235);
+ canvas.DrawRoundRect(stage, 6.0f, 6.0f, paint);
+
+ float margin = Math.Clamp(MathF.Min(width, height) * 0.06f, 18.0f, 38.0f);
+ float gap = Math.Clamp(MathF.Min(width, height) * 0.022f, 8.0f, 15.0f);
+ SKRect field = new(stage.Left + margin, stage.Top + margin, stage.Right - margin, stage.Bottom - margin);
+
+ if (UseWideLayout(width, height))
+ {
+ DrawWideComposition(canvas, paint, field, gap);
+ }
+ else
+ {
+ DrawTallComposition(canvas, paint, field, gap);
+ }
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = new(190, 187, 179);
+ canvas.DrawRoundRect(stage, 6.0f, 6.0f, paint);
+ }
+
+ private static void DrawWideComposition(SKCanvas canvas, SKPaint paint, SKRect field, float gap)
+ {
+ float split = field.Left + (field.Width * 0.57f);
+ SKRect dark = new(field.Left, field.Top, split - gap, field.Bottom);
+ SKRect right = new(split, field.Top, field.Right, field.Bottom);
+ float row = right.Top + (right.Height * 0.48f);
+ float column = right.Left + (right.Width * 0.61f);
+
+ DrawBlock(canvas, paint, dark, new(24, 39, 34));
+ DrawBlock(canvas, paint, new(right.Left, right.Top, right.Right, row - gap), new(60, 116, 199));
+ DrawBlock(canvas, paint, new(right.Left, row, column - gap, right.Bottom), new(224, 91, 82));
+ DrawBlock(canvas, paint, new(column, row, right.Right, right.Top + (right.Height * 0.73f)), new(235, 174, 61));
+ DrawBlock(canvas, paint, new(column, right.Top + (right.Height * 0.73f) + gap, right.Right, right.Bottom), new(35, 145, 108));
+
+ DrawDarkFieldDetails(canvas, paint, dark, gap);
+ DrawSquareAssembly(canvas, paint, new(dark.Left + (dark.Width * 0.64f), dark.MidY), MathF.Min(dark.Width, dark.Height) * 0.30f);
+ }
+
+ private static void DrawTallComposition(SKCanvas canvas, SKPaint paint, SKRect field, float gap)
+ {
+ float split = field.Top + (field.Height * 0.60f);
+ SKRect dark = new(field.Left, field.Top, field.Right, split - gap);
+ SKRect bottom = new(field.Left, split, field.Right, field.Bottom);
+ float firstColumn = bottom.Left + (bottom.Width * 0.48f);
+ float secondColumn = bottom.Left + (bottom.Width * 0.74f);
+
+ DrawBlock(canvas, paint, dark, new(24, 39, 34));
+ DrawBlock(canvas, paint, new(bottom.Left, bottom.Top, firstColumn - gap, bottom.Bottom), new(60, 116, 199));
+ DrawBlock(canvas, paint, new(firstColumn, bottom.Top, secondColumn - gap, bottom.Bottom), new(224, 91, 82));
+ DrawBlock(canvas, paint, new(secondColumn, bottom.Top, bottom.Right, bottom.MidY - (gap * 0.5f)), new(235, 174, 61));
+ DrawBlock(canvas, paint, new(secondColumn, bottom.MidY + (gap * 0.5f), bottom.Right, bottom.Bottom), new(35, 145, 108));
+
+ DrawDarkFieldDetails(canvas, paint, dark, gap);
+ DrawSquareAssembly(canvas, paint, new(dark.MidX, dark.Top + (dark.Height * 0.48f)), MathF.Min(dark.Width, dark.Height) * 0.28f);
+ }
+
+ private static void DrawDarkFieldDetails(SKCanvas canvas, SKPaint paint, SKRect rect, float gap)
+ {
+ float barWidth = MathF.Max(3.0f, rect.Width * 0.012f);
+ float startX = rect.Left + gap * 1.6f;
+ float bottom = rect.Bottom - (gap * 1.6f);
+
+ for (int i = 0; i < 7; i++)
+ {
+ float height = rect.Height * (0.12f + (i * 0.045f));
+ paint.Color = i % 3 is 0 ? new(235, 174, 61) : i % 3 is 1 ? new(224, 91, 82) : new(35, 145, 108);
+ canvas.DrawRect(new(startX + (i * barWidth * 1.9f), bottom - height, startX + (i * barWidth * 1.9f) + barWidth, bottom), paint);
+ }
+
+ paint.Color = new(244, 240, 229, 45);
+ canvas.DrawRect(new(rect.Left + (rect.Width * 0.08f), rect.Top + (rect.Height * 0.12f), rect.Left + (rect.Width * 0.36f), rect.Top + (rect.Height * 0.15f)), paint);
+ canvas.DrawRect(new(rect.Left + (rect.Width * 0.08f), rect.Top + (rect.Height * 0.19f), rect.Left + (rect.Width * 0.27f), rect.Top + (rect.Height * 0.22f)), paint);
+ }
+
+ private static void DrawSquareAssembly(SKCanvas canvas, SKPaint paint, SKPoint center, float size)
+ {
+ canvas.Save();
+ canvas.RotateDegrees(-9.0f, center.X, center.Y);
+ paint.Color = new(246, 243, 235);
+ canvas.DrawRect(new(center.X - size, center.Y - size, center.X + size, center.Y + size), paint);
+ canvas.Restore();
+
+ canvas.Save();
+ canvas.RotateDegrees(11.0f, center.X, center.Y);
+ paint.Color = new(60, 116, 199, 210);
+ float inset = size * 0.38f;
+ canvas.DrawRect(new(center.X - size + inset, center.Y - size + inset, center.X + size - inset, center.Y + size - inset), paint);
+ canvas.Restore();
+ }
+
+ private static void DrawBlock(SKCanvas canvas, SKPaint paint, SKRect rect, SKColor color)
+ {
+ paint.Color = color;
+ canvas.DrawRect(rect, paint);
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Scenes/PaintScene.cs b/sources/Experiments/SkiaGallery/Scenes/PaintScene.cs
new file mode 100644
index 00000000..42447217
--- /dev/null
+++ b/sources/Experiments/SkiaGallery/Scenes/PaintScene.cs
@@ -0,0 +1,231 @@
+using SkiaSharp;
+
+namespace SkiaGallery;
+
+internal sealed class PaintScene : GalleryScene
+{
+ private readonly SKBitmap bitmap;
+ private readonly SKImage image;
+
+ private SKRect canvasRect;
+ private SKRect spectrumRect;
+ private SKRect blendRect;
+ private SKRect blurRect;
+ private SKRect imageRect;
+
+ public PaintScene(GalleryResources resources) : base(resources)
+ {
+ bitmap = CreateBitmap();
+ image = SKImage.FromBitmap(bitmap);
+ }
+
+ public override string Navigation => "Paint & image";
+
+ public override string Title => "Color laboratory";
+
+ public override string Description => "Shaders, blend modes, blur, sampling, and color transforms in one study.";
+
+ protected override void UpdateLayout(float width, float height)
+ {
+ canvasRect = new(0.0f, 0.0f, width, height);
+
+ if (UseWideLayout(width, height))
+ {
+ float split = width * 0.63f;
+ spectrumRect = new(0.0f, 0.0f, split, height * 0.58f);
+ blendRect = new(split, 0.0f, width, height * 0.58f);
+ blurRect = new(0.0f, height * 0.58f, width * 0.38f, height);
+ imageRect = new(width * 0.38f, height * 0.58f, width, height);
+ }
+ else
+ {
+ spectrumRect = new(0.0f, 0.0f, width, height * 0.38f);
+ blendRect = new(0.0f, height * 0.38f, width * 0.48f, height * 0.68f);
+ blurRect = new(width * 0.48f, height * 0.38f, width, height * 0.68f);
+ imageRect = new(0.0f, height * 0.68f, width, height);
+ }
+ }
+
+ protected override void DrawStatic(SKCanvas canvas, float width, float height)
+ {
+ using SKPaint paint = new() { IsAntialias = true };
+ using SKPaint imagePaint = new() { IsAntialias = true };
+ using SKPaint filteredPaint = new() { IsAntialias = true };
+ using SKMaskFilter blur = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 16.0f);
+ using SKColorFilter colorFilter = SKColorFilter.CreateColorMatrix(
+ [
+ 0.72f, 0.12f, 0.16f, 0.0f, 18.0f / 255.0f,
+ 0.05f, 0.82f, 0.13f, 0.0f, 4.0f / 255.0f,
+ 0.14f, 0.16f, 0.70f, 0.0f, 12.0f / 255.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f, 0.0f
+ ]);
+
+ filteredPaint.ColorFilter = colorFilter;
+ SKSamplingOptions sampling = new(SKCubicResampler.Mitchell);
+
+ paint.Color = new(246, 245, 241);
+ canvas.DrawRoundRect(canvasRect, 5.0f, 5.0f, paint);
+
+ DrawSpectrum(canvas, paint);
+ DrawBlendStudy(canvas, paint);
+ DrawBlurStudy(canvas, paint, blur);
+ DrawImageStudy(canvas, paint, imagePaint, filteredPaint, sampling);
+ DrawDividers(canvas, paint);
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = new(208, 207, 201);
+ canvas.DrawRoundRect(canvasRect, 5.0f, 5.0f, paint);
+ }
+
+ protected override void DisposeResources()
+ {
+ image.Dispose();
+ bitmap.Dispose();
+ }
+
+ private void DrawSpectrum(SKCanvas canvas, SKPaint paint)
+ {
+ SKRect stage = Inset(spectrumRect, 28.0f, 54.0f, 28.0f, 28.0f);
+ using SKShader baseGradient = SKShader.CreateLinearGradient(
+ new(stage.Left, stage.Top),
+ new(stage.Right, stage.Top),
+ [new(43, 90, 176), new(47, 168, 154), new(239, 190, 73), new(226, 91, 94), new(119, 77, 165)],
+ [0.0f, 0.26f, 0.52f, 0.76f, 1.0f],
+ SKShaderTileMode.Clamp);
+ using SKShader lightGradient = SKShader.CreateLinearGradient(
+ new(stage.Left, stage.Top),
+ new(stage.Left, stage.Bottom),
+ [new(255, 255, 255, 18), new(255, 255, 255, 185), new(23, 31, 29, 80)],
+ [0.0f, 0.56f, 1.0f],
+ SKShaderTileMode.Clamp);
+
+ paint.Shader = baseGradient;
+ canvas.DrawRect(stage, paint);
+ paint.Shader = lightGradient;
+ paint.BlendMode = SKBlendMode.Screen;
+ canvas.DrawRect(stage, paint);
+ paint.BlendMode = SKBlendMode.SrcOver;
+ paint.Shader = null;
+
+ paint.Color = GalleryPalette.Ink;
+ canvas.DrawText("SPECTRUM", spectrumRect.Left + 28.0f, spectrumRect.Top + 28.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
+ paint.Color = GalleryPalette.Muted;
+ canvas.DrawText("LINEAR SHADER / FIVE STOPS", spectrumRect.Right - 28.0f, spectrumRect.Top + 28.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
+
+ paint.Color = new(255, 255, 255, 125);
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ for (int i = 1; i < 5; i++)
+ {
+ float x = stage.Left + (stage.Width * i / 5.0f);
+ canvas.DrawLine(x, stage.Top, x, stage.Bottom, paint);
+ }
+
+ paint.Style = SKPaintStyle.Fill;
+ }
+
+ private void DrawBlendStudy(SKCanvas canvas, SKPaint paint)
+ {
+ SKRect stage = Inset(blendRect, 24.0f, 54.0f, 24.0f, 24.0f);
+ float radius = MathF.Min(stage.Width, stage.Height) * 0.28f;
+ SKPoint center = new(stage.MidX, stage.MidY + 8.0f);
+
+ paint.Color = GalleryPalette.Ink;
+ canvas.DrawText("BLEND", blendRect.Left + 24.0f, blendRect.Top + 28.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
+
+ paint.Color = new(229, 100, 90, 175);
+ canvas.DrawCircle(center.X - (radius * 0.42f), center.Y, radius, paint);
+ paint.Color = new(61, 126, 204, 175);
+ paint.BlendMode = SKBlendMode.Plus;
+ canvas.DrawCircle(center.X + (radius * 0.42f), center.Y, radius, paint);
+ paint.BlendMode = SKBlendMode.SrcOver;
+
+ paint.Color = GalleryPalette.Muted;
+ canvas.DrawText("PLUS", blendRect.Right - 24.0f, blendRect.Bottom - 20.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
+ }
+
+ private void DrawBlurStudy(SKCanvas canvas, SKPaint paint, SKMaskFilter blur)
+ {
+ SKRect stage = Inset(blurRect, 24.0f, 52.0f, 24.0f, 24.0f);
+ float radius = MathF.Min(stage.Width, stage.Height) * 0.22f;
+
+ paint.Color = GalleryPalette.Ink;
+ canvas.DrawText("BLUR FIELD", blurRect.Left + 24.0f, blurRect.Top + 28.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
+
+ paint.MaskFilter = blur;
+ paint.Color = new(37, 153, 116, 128);
+ canvas.DrawCircle(stage.MidX - (radius * 0.55f), stage.MidY, radius, paint);
+ paint.Color = new(231, 168, 66, 128);
+ canvas.DrawCircle(stage.MidX + (radius * 0.55f), stage.MidY, radius, paint);
+ paint.MaskFilter = null;
+
+ paint.Color = GalleryPalette.Ink;
+ canvas.DrawCircle(stage.MidX, stage.MidY, 4.0f, paint);
+ }
+
+ private void DrawImageStudy(SKCanvas canvas, SKPaint paint, SKPaint imagePaint, SKPaint filteredPaint, SKSamplingOptions sampling)
+ {
+ SKRect stage = Inset(imageRect, 24.0f, 52.0f, 24.0f, 24.0f);
+ SKRect left = new(stage.Left, stage.Top, stage.MidX - 4.0f, stage.Bottom);
+ SKRect right = new(stage.MidX + 4.0f, stage.Top, stage.Right, stage.Bottom);
+ SKRect source = new(0.0f, 0.0f, bitmap.Width, bitmap.Height);
+
+ paint.Color = GalleryPalette.Ink;
+ canvas.DrawText("IMAGE TRANSFORM", imageRect.Left + 24.0f, imageRect.Top + 28.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
+ paint.Color = GalleryPalette.Muted;
+ canvas.DrawText("ORIGINAL / 4 × 5 MATRIX", imageRect.Right - 24.0f, imageRect.Top + 28.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
+
+ canvas.DrawImage(image, source, left, sampling, imagePaint);
+ canvas.DrawImage(image, source, right, sampling, filteredPaint);
+
+ paint.Color = new(255, 255, 255, 180);
+ canvas.DrawRect(new(stage.MidX - 1.0f, stage.Top, stage.MidX + 1.0f, stage.Bottom), paint);
+ }
+
+ private void DrawDividers(SKCanvas canvas, SKPaint paint)
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = new(208, 207, 201);
+
+ if (spectrumRect.Right < canvasRect.Right)
+ {
+ canvas.DrawLine(spectrumRect.Right, canvasRect.Top, spectrumRect.Right, spectrumRect.Bottom, paint);
+ }
+
+ canvas.DrawLine(blurRect.Left, blurRect.Top, canvasRect.Right, blurRect.Top, paint);
+
+ if (blurRect.Right < canvasRect.Right)
+ {
+ canvas.DrawLine(blurRect.Right, blurRect.Top, blurRect.Right, canvasRect.Bottom, paint);
+ }
+
+ paint.Style = SKPaintStyle.Fill;
+ }
+
+ private static SKRect Inset(SKRect rect, float left, float top, float right, float bottom)
+ {
+ return new(rect.Left + left, rect.Top + top, rect.Right - right, rect.Bottom - bottom);
+ }
+
+ private static SKBitmap CreateBitmap()
+ {
+ SKBitmap result = new(240, 160, SKColorType.Bgra8888, SKAlphaType.Premul);
+
+ for (int y = 0; y < result.Height; y++)
+ {
+ for (int x = 0; x < result.Width; x++)
+ {
+ float horizontal = x / (result.Width - 1.0f);
+ float vertical = y / (result.Height - 1.0f);
+ byte red = (byte)(42.0f + (horizontal * 185.0f));
+ byte green = (byte)(74.0f + ((1.0f - vertical) * 126.0f));
+ byte blue = (byte)(108.0f + (MathF.Sin((horizontal + vertical) * 6.0f) * 52.0f));
+ result.SetPixel(x, y, new(red, green, blue));
+ }
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs b/sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs
new file mode 100644
index 00000000..1b1ebdc0
--- /dev/null
+++ b/sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs
@@ -0,0 +1,260 @@
+using SkiaSharp;
+
+namespace SkiaGallery;
+
+internal sealed class TypographyScene(GalleryResources resources) : GalleryScene(resources)
+{
+ private const float MaximumHeroSize = 94.0f;
+
+ private static readonly string[] ScaleLabels = ["Display", "Title", "Section", "Body", "Caption"];
+ private static readonly float[] ScaleFontSizes = [58.0f, 34.0f, 22.0f, 16.0f, 12.0f];
+
+ private SKRect page;
+ private SKRect masthead;
+ private SKRect scaleColumn;
+ private SKRect specimenArea;
+ private SKRect pathArea;
+
+ public override string Navigation => "Typography";
+
+ public override string Title => "Editorial typography";
+
+ public override string Description => "Hierarchy, metrics, path layout, and painted glyphs on one specimen page.";
+
+ public override bool CanRender(float width, float height)
+ {
+ return base.CanRender(width, height) && (!UseWideLayout(width, height) || height >= 330.0f);
+ }
+
+ protected override void UpdateLayout(float width, float height)
+ {
+ page = new(0.0f, 0.0f, width, height);
+
+ if (UseWideLayout(width, height))
+ {
+ float margin = Math.Clamp(width * 0.045f, 34.0f, 52.0f);
+ float mastheadBottom = height * 0.46f;
+ float columnWidth = width * 0.27f;
+
+ masthead = new(margin, margin, width - margin, mastheadBottom);
+ scaleColumn = new(margin, mastheadBottom + 22.0f, margin + columnWidth, height - margin);
+ specimenArea = new(scaleColumn.Right + 34.0f, mastheadBottom + 22.0f, width - margin, height - margin);
+ pathArea = new(specimenArea.Left, specimenArea.Top + (specimenArea.Height * 0.52f), specimenArea.Right, specimenArea.Bottom);
+ }
+ else
+ {
+ float margin = Math.Clamp(width * 0.06f, 24.0f, 34.0f);
+ float mastheadBottom = height * 0.35f;
+ float bodyTop = mastheadBottom + 18.0f;
+ float scaleWidth = width * 0.36f;
+
+ masthead = new(margin, margin, width - margin, mastheadBottom);
+ scaleColumn = new(margin, bodyTop, margin + scaleWidth, height - margin);
+ specimenArea = new(scaleColumn.Right + 22.0f, bodyTop, width - margin, height - margin);
+ pathArea = new(specimenArea.Left, specimenArea.Top + (specimenArea.Height * 0.60f), specimenArea.Right, specimenArea.Bottom);
+ }
+ }
+
+ protected override void DrawStatic(SKCanvas canvas, float width, float height)
+ {
+ using SKPaint paint = new() { IsAntialias = true };
+ using SKPaint outline = new() { Color = GalleryPalette.Ink, IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 1.4f };
+ using SKMaskFilter blur = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4.5f);
+ using SKPaint shadow = new() { Color = new(25, 36, 32, 38), IsAntialias = true, MaskFilter = blur };
+ using SKShader headlineShader = SKShader.CreateLinearGradient(new(masthead.Left, masthead.Top), new(masthead.Right, masthead.Bottom), [GalleryPalette.Blue, GalleryPalette.Accent, GalleryPalette.Coral, GalleryPalette.Amber], [0.0f, 0.38f, 0.72f, 1.0f], SKShaderTileMode.Clamp);
+
+ paint.Color = new(251, 250, 247);
+ canvas.DrawRoundRect(page, 4.0f, 4.0f, paint);
+
+ DrawEditorialRules(canvas, paint);
+ DrawMasthead(canvas, paint, headlineShader);
+ DrawScaleColumn(canvas, paint);
+ DrawSpecimen(canvas, paint, outline, shadow, headlineShader);
+ DrawPathText(canvas, paint);
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = new(214, 211, 203);
+ canvas.DrawRoundRect(page, 4.0f, 4.0f, paint);
+ }
+
+ private void DrawEditorialRules(SKCanvas canvas, SKPaint paint)
+ {
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = new(214, 211, 203);
+ canvas.DrawLine(masthead.Left, masthead.Top, masthead.Right, masthead.Top, paint);
+ canvas.DrawLine(masthead.Left, masthead.Bottom, masthead.Right, masthead.Bottom, paint);
+ canvas.DrawLine(scaleColumn.Right + 16.0f, scaleColumn.Top, scaleColumn.Right + 16.0f, scaleColumn.Bottom, paint);
+
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = GalleryPalette.Coral;
+ canvas.DrawRect(masthead.Left, masthead.Top - 2.0f, MathF.Min(112.0f, masthead.Width * 0.18f), 4.0f, paint);
+ }
+
+ private void DrawMasthead(SKCanvas canvas, SKPaint paint, SKShader shader)
+ {
+ const string lead = "FORM";
+ const string secondLine = "function.";
+ const float minimumHeadingSize = 32.0f;
+ const float maximumHeadingSize = 108.0f;
+
+ float contentTop = masthead.Top + 48.0f;
+ bool showNote = masthead.Height >= 230.0f;
+ float contentBottom = masthead.Bottom - (showNote ? 34.0f : 14.0f);
+ float availableHeight = MathF.Max(1.0f, contentBottom - contentTop);
+ float headingSize = Math.Clamp(availableHeight / 2.18f, minimumHeadingSize, maximumHeadingSize);
+ using SKFont headingFont = new(Resources.MediumTypeface, headingSize)
+ {
+ Edging = SKFontEdging.SubpixelAntialias,
+ Hinting = SKFontHinting.Slight,
+ Subpixel = true
+ };
+
+ float secondLineX = masthead.Left + (masthead.Width * 0.12f);
+ float maximumTextWidth = masthead.Right - secondLineX;
+ float secondLineWidth = headingFont.MeasureText(secondLine);
+
+ if (secondLineWidth > maximumTextWidth)
+ {
+ headingSize *= maximumTextWidth / secondLineWidth;
+ headingFont.Size = headingSize;
+ }
+
+ paint.Color = GalleryPalette.Muted;
+ canvas.DrawText("SKIA TYPE SPECIMEN / VECTOR EDITION", masthead.Left, masthead.Top + 26.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
+
+ SKFontMetrics headingMetrics = headingFont.Metrics;
+ float firstBaseline = contentTop - headingMetrics.Ascent;
+ float lineGap = Math.Clamp(headingSize * 0.12f, 6.0f, 14.0f);
+ float secondBaseline = firstBaseline + headingMetrics.Descent - headingMetrics.Ascent + lineGap;
+
+ paint.Shader = shader;
+ canvas.DrawText(lead, masthead.Left, firstBaseline, SKTextAlign.Left, headingFont, paint);
+ paint.Shader = null;
+
+ float followsSize = Math.Clamp(headingSize * 0.42f, 20.0f, Resources.TitleFont.Size);
+ using SKFont followsFont = new(Resources.MediumTypeface, followsSize)
+ {
+ Edging = SKFontEdging.SubpixelAntialias,
+ Hinting = SKFontHinting.Slight,
+ Subpixel = true
+ };
+ float followsX = masthead.Left + headingFont.MeasureText(lead) + Math.Clamp(headingSize * 0.16f, 10.0f, 18.0f);
+ float followsBaseline = firstBaseline - (headingSize * 0.08f);
+
+ paint.Color = GalleryPalette.Ink;
+ canvas.DrawText("follows", followsX, followsBaseline, SKTextAlign.Left, followsFont, paint);
+ canvas.DrawText(secondLine, secondLineX, secondBaseline, SKTextAlign.Left, headingFont, paint);
+
+ if (showNote)
+ {
+ paint.Color = GalleryPalette.Muted;
+ string note = masthead.Width >= 680.0f ? "Scale, rhythm, and contour remain native vector geometry." : "Scale, rhythm, contour.";
+ canvas.DrawText(note, masthead.Right, masthead.Bottom - 12.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
+ }
+ }
+
+ private void DrawScaleColumn(SKCanvas canvas, SKPaint paint)
+ {
+ paint.Color = GalleryPalette.Muted;
+ canvas.DrawText("HIERARCHY", scaleColumn.Left, scaleColumn.Top + 17.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
+
+ float top = scaleColumn.Top + 42.0f;
+ float availableHeight = scaleColumn.Bottom - top;
+ float naturalHeight = 0.0f;
+
+ for (int i = 0; i < ScaleFontSizes.Length; i++)
+ {
+ naturalHeight += ScaleFontSizes[i] * 1.12f;
+ }
+
+ float scale = MathF.Min(1.0f, availableHeight / naturalHeight);
+ float baseline = top;
+
+ for (int i = 0; i < ScaleLabels.Length; i++)
+ {
+ float fontSize = MathF.Max(10.0f, ScaleFontSizes[i] * scale);
+ using SKFont font = new(i <= 2 ? Resources.MediumTypeface : Resources.RegularTypeface, fontSize)
+ {
+ Edging = SKFontEdging.SubpixelAntialias,
+ Hinting = SKFontHinting.Slight,
+ Subpixel = true
+ };
+ SKFontMetrics metrics = font.Metrics;
+ baseline -= metrics.Ascent;
+
+ paint.Color = i is 0 ? GalleryPalette.Coral : i is 1 ? GalleryPalette.Ink : GalleryPalette.Muted;
+ canvas.DrawText(ScaleLabels[i], scaleColumn.Left, baseline, SKTextAlign.Left, font, paint);
+ paint.Color = GalleryPalette.Muted;
+ canvas.DrawText(((int)MathF.Round(fontSize)).ToString(), scaleColumn.Right, baseline, SKTextAlign.Right, Resources.CaptionFont, paint);
+
+ baseline += metrics.Descent + (fontSize * 0.12f);
+ }
+ }
+
+ private void DrawSpecimen(SKCanvas canvas, SKPaint paint, SKPaint outline, SKPaint shadow, SKShader shader)
+ {
+ paint.Color = GalleryPalette.Muted;
+ canvas.DrawText("METRICS / PAINT", specimenArea.Left, specimenArea.Top + 17.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
+
+ float heroSize = Math.Clamp(MathF.Min(specimenArea.Width * 0.24f, specimenArea.Height * 0.42f), 38.0f, MaximumHeroSize);
+ using SKFont heroFont = new(Resources.MediumTypeface, heroSize)
+ {
+ Edging = SKFontEdging.SubpixelAntialias,
+ Hinting = SKFontHinting.Slight,
+ Subpixel = true
+ };
+ float baseline = specimenArea.Top + MathF.Min(specimenArea.Height * 0.44f, 118.0f);
+ float capLine = baseline - heroFont.Metrics.CapHeight;
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = GalleryPalette.Line;
+ canvas.DrawLine(specimenArea.Left, capLine, specimenArea.Right, capLine, paint);
+ canvas.DrawLine(specimenArea.Left, baseline, specimenArea.Right, baseline, paint);
+
+ paint.Style = SKPaintStyle.Fill;
+ paint.Shader = shader;
+ canvas.DrawText("Aa", specimenArea.Left, baseline, SKTextAlign.Left, heroFont, paint);
+ paint.Shader = null;
+
+ float wordX = specimenArea.Left + heroFont.MeasureText("Aa") + Math.Clamp(specimenArea.Width * 0.04f, 12.0f, 22.0f);
+ paint.Color = GalleryPalette.Ink;
+ canvas.DrawText("VECTOR", wordX, baseline - 15.0f, SKTextAlign.Left, Resources.SectionFont, paint);
+ canvas.DrawText("OUTLINE", wordX, baseline + 25.0f, SKTextAlign.Left, Resources.SectionFont, shadow);
+ canvas.DrawText("OUTLINE", wordX, baseline + 25.0f, SKTextAlign.Left, Resources.SectionFont, outline);
+
+ paint.Color = GalleryPalette.Muted;
+ canvas.DrawText("CAP", specimenArea.Right, capLine - 5.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
+ canvas.DrawText("BASELINE", specimenArea.Right, baseline - 5.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
+ }
+
+ private void DrawPathText(SKCanvas canvas, SKPaint paint)
+ {
+ SKPoint start = new(pathArea.Left, pathArea.Bottom - 18.0f);
+ SKPoint end = new(pathArea.Right, pathArea.Top + 36.0f);
+ SKPoint first = new(pathArea.Left + (pathArea.Width * 0.32f), pathArea.Top + 8.0f);
+ SKPoint second = new(pathArea.Left + (pathArea.Width * 0.70f), pathArea.Bottom - 3.0f);
+
+ using SKPathBuilder builder = new();
+ builder.MoveTo(start);
+ builder.CubicTo(first, second, end);
+ using SKPath path = builder.Detach();
+
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = 1.0f;
+ paint.Color = GalleryPalette.Line;
+ canvas.DrawLine(start, first, paint);
+ canvas.DrawLine(second, end, paint);
+
+ paint.Style = SKPaintStyle.Fill;
+ paint.Color = GalleryPalette.Accent;
+ string sample = pathArea.Width >= 360.0f ? "TYPE FOLLOWS A REUSABLE CUBIC PATH" : "TYPE FOLLOWS PATH";
+ canvas.DrawTextOnPath(sample, path, 6.0f, -8.0f, SKTextAlign.Left, Resources.BodyFont, paint);
+
+ paint.Color = GalleryPalette.Blue;
+ canvas.DrawCircle(first, 3.5f, paint);
+ canvas.DrawCircle(second, 3.5f, paint);
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaBoard/SkiaBoard.csproj b/sources/Experiments/SkiaGallery/SkiaGallery.csproj
similarity index 93%
rename from sources/Experiments/SkiaBoard/SkiaBoard.csproj
rename to sources/Experiments/SkiaGallery/SkiaGallery.csproj
index 8b441176..0cdd50ec 100644
--- a/sources/Experiments/SkiaBoard/SkiaBoard.csproj
+++ b/sources/Experiments/SkiaGallery/SkiaGallery.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -17,4 +17,4 @@
-
+
\ No newline at end of file
From f9c5983412efab79c3e9080ea13310ababc857c7 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 09:22:54 +0800
Subject: [PATCH 17/50] Refactor: improve structure, accessibility, and
formatting
- Changed Gallery, GalleryResources, and scene classes from sealed to non-sealed for inheritance support.
- Moved scene classes to SkiaGallery.Scenes namespace.
- Made GalleryResources.CreateText static and updated all usages.
- Improved code formatting and consistency.
- Added missing using directives in App.cs and Gallery.cs.
- Added UTF-8 BOM to project and source files.
---
sources/Experiments/SkiaGallery/App.cs | 2 +-
sources/Experiments/SkiaGallery/Gallery.cs | 19 ++++++++++---------
.../Experiments/SkiaGallery/GalleryPalette.cs | 2 +-
.../SkiaGallery/GalleryResources.cs | 14 +++++++-------
.../Experiments/SkiaGallery/GalleryScene.cs | 2 +-
.../SkiaGallery/Helpers/CocoaHelper.cs | 2 +-
sources/Experiments/SkiaGallery/Program.cs | 2 +-
.../SkiaGallery/Scenes/GeometryScene.cs | 6 +++---
.../SkiaGallery/Scenes/MotionScene.cs | 6 +++---
.../SkiaGallery/Scenes/OverviewScene.cs | 8 ++++----
.../SkiaGallery/Scenes/PaintScene.cs | 6 +++---
.../SkiaGallery/Scenes/TypographyScene.cs | 6 +++---
.../SkiaGallery/SkiaGallery.csproj | 2 +-
13 files changed, 39 insertions(+), 38 deletions(-)
diff --git a/sources/Experiments/SkiaGallery/App.cs b/sources/Experiments/SkiaGallery/App.cs
index e6e74c75..2d171dde 100644
--- a/sources/Experiments/SkiaGallery/App.cs
+++ b/sources/Experiments/SkiaGallery/App.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using Silk.NET.Input;
using Silk.NET.Windowing;
using SkiaGallery.Helpers;
diff --git a/sources/Experiments/SkiaGallery/Gallery.cs b/sources/Experiments/SkiaGallery/Gallery.cs
index ab86c65d..31af21ee 100644
--- a/sources/Experiments/SkiaGallery/Gallery.cs
+++ b/sources/Experiments/SkiaGallery/Gallery.cs
@@ -1,10 +1,11 @@
-using System.Numerics;
+using System.Numerics;
+using SkiaGallery.Scenes;
using SkiaSharp;
using Zenith.NET;
namespace SkiaGallery;
-internal sealed class Gallery : IDisposable
+internal class Gallery : IDisposable
{
private const float ExpandedSidebarWidth = 228.0f;
private const float CompactSidebarWidth = 80.0f;
@@ -84,19 +85,19 @@ public Gallery(GraphicsApi graphicsApi, string deviceName)
for (int i = 0; i < scenes.Length; i++)
{
- titleBlobs[i] = resources.CreateText(scenes[i].Title, resources.TitleFont);
+ titleBlobs[i] = GalleryResources.CreateText(scenes[i].Title, resources.TitleFont);
descriptionTexts[i] = scenes[i].Description;
- descriptionBlobs[i] = resources.CreateText(descriptionTexts[i], resources.BodyFont);
- compactTitleBlobs[i] = resources.CreateText(scenes[i].Navigation, resources.SectionFont);
+ descriptionBlobs[i] = GalleryResources.CreateText(descriptionTexts[i], resources.BodyFont);
+ compactTitleBlobs[i] = GalleryResources.CreateText(scenes[i].Navigation, resources.SectionFont);
}
const string pausedHeader = "GPU scene paused at this window size";
const string pausedTitle = "More room needed";
const string pausedDescription = "Increase the window size to continue.";
- pausedHeaderBlob = resources.CreateText(pausedHeader, resources.BodyFont);
- pausedTitleBlob = resources.CreateText(pausedTitle, resources.SectionFont);
- pausedDescriptionBlob = resources.CreateText(pausedDescription, resources.BodyFont);
+ pausedHeaderBlob = GalleryResources.CreateText(pausedHeader, resources.BodyFont);
+ pausedTitleBlob = GalleryResources.CreateText(pausedTitle, resources.SectionFont);
+ pausedDescriptionBlob = GalleryResources.CreateText(pausedDescription, resources.BodyFont);
pausedHeaderWidth = resources.BodyFont.MeasureText(pausedHeader);
pausedTitleWidth = resources.SectionFont.MeasureText(pausedTitle);
pausedDescriptionWidth = resources.BodyFont.MeasureText(pausedDescription);
@@ -465,7 +466,7 @@ private void UpdateDescriptionBlobs(float width)
descriptionTexts[i] = text;
descriptionBlobs[i].Dispose();
- descriptionBlobs[i] = resources.CreateText(text, resources.BodyFont);
+ descriptionBlobs[i] = GalleryResources.CreateText(text, resources.BodyFont);
}
}
diff --git a/sources/Experiments/SkiaGallery/GalleryPalette.cs b/sources/Experiments/SkiaGallery/GalleryPalette.cs
index 835e2791..6d44f3b5 100644
--- a/sources/Experiments/SkiaGallery/GalleryPalette.cs
+++ b/sources/Experiments/SkiaGallery/GalleryPalette.cs
@@ -1,4 +1,4 @@
-using SkiaSharp;
+using SkiaSharp;
namespace SkiaGallery;
diff --git a/sources/Experiments/SkiaGallery/GalleryResources.cs b/sources/Experiments/SkiaGallery/GalleryResources.cs
index 0787c7d3..f6dd490c 100644
--- a/sources/Experiments/SkiaGallery/GalleryResources.cs
+++ b/sources/Experiments/SkiaGallery/GalleryResources.cs
@@ -1,8 +1,8 @@
-using SkiaSharp;
+using SkiaSharp;
namespace SkiaGallery;
-internal sealed class GalleryResources : IDisposable
+internal class GalleryResources : IDisposable
{
public GalleryResources()
{
@@ -32,11 +32,6 @@ public GalleryResources()
public SKFont TitleFont { get; }
- public SKTextBlob CreateText(string text, SKFont font)
- {
- return SKTextBlob.Create(text, font, default)!;
- }
-
public void Dispose()
{
TitleFont.Dispose();
@@ -48,6 +43,11 @@ public void Dispose()
RegularTypeface.Dispose();
}
+ public static SKTextBlob CreateText(string text, SKFont font)
+ {
+ return SKTextBlob.Create(text, font, default)!;
+ }
+
private static SKFont CreateFont(SKTypeface typeface, float size)
{
return new(typeface, size)
diff --git a/sources/Experiments/SkiaGallery/GalleryScene.cs b/sources/Experiments/SkiaGallery/GalleryScene.cs
index 67f26270..045b493b 100644
--- a/sources/Experiments/SkiaGallery/GalleryScene.cs
+++ b/sources/Experiments/SkiaGallery/GalleryScene.cs
@@ -1,4 +1,4 @@
-using SkiaSharp;
+using SkiaSharp;
namespace SkiaGallery;
diff --git a/sources/Experiments/SkiaGallery/Helpers/CocoaHelper.cs b/sources/Experiments/SkiaGallery/Helpers/CocoaHelper.cs
index 2976ad95..eeb49a1f 100644
--- a/sources/Experiments/SkiaGallery/Helpers/CocoaHelper.cs
+++ b/sources/Experiments/SkiaGallery/Helpers/CocoaHelper.cs
@@ -1,4 +1,4 @@
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
namespace SkiaGallery.Helpers;
diff --git a/sources/Experiments/SkiaGallery/Program.cs b/sources/Experiments/SkiaGallery/Program.cs
index 5dfbda33..55224435 100644
--- a/sources/Experiments/SkiaGallery/Program.cs
+++ b/sources/Experiments/SkiaGallery/Program.cs
@@ -1,3 +1,3 @@
-using SkiaGallery;
+using SkiaGallery;
App.Run();
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs b/sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs
index 44e27a4f..89d3a472 100644
--- a/sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs
+++ b/sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs
@@ -1,8 +1,8 @@
-using SkiaSharp;
+using SkiaSharp;
-namespace SkiaGallery;
+namespace SkiaGallery.Scenes;
-internal sealed class GeometryScene : GalleryScene
+internal class GeometryScene : GalleryScene
{
private static readonly SKColor[] DotColors =
[
diff --git a/sources/Experiments/SkiaGallery/Scenes/MotionScene.cs b/sources/Experiments/SkiaGallery/Scenes/MotionScene.cs
index 4cffb97d..c35b60de 100644
--- a/sources/Experiments/SkiaGallery/Scenes/MotionScene.cs
+++ b/sources/Experiments/SkiaGallery/Scenes/MotionScene.cs
@@ -1,8 +1,8 @@
-using SkiaSharp;
+using SkiaSharp;
-namespace SkiaGallery;
+namespace SkiaGallery.Scenes;
-internal sealed class MotionScene : GalleryScene
+internal class MotionScene : GalleryScene
{
private static readonly SKColor[] ParticleColors = [new(77, 151, 235, 220), new(52, 194, 146, 220), new(245, 112, 103, 220), new(247, 190, 80, 220)];
diff --git a/sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs b/sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs
index 4e0f060e..740d972d 100644
--- a/sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs
+++ b/sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs
@@ -1,8 +1,8 @@
-using SkiaSharp;
+using SkiaSharp;
-namespace SkiaGallery;
+namespace SkiaGallery.Scenes;
-internal sealed class OverviewScene(GalleryResources resources) : GalleryScene(resources)
+internal class OverviewScene(GalleryResources resources) : GalleryScene(resources)
{
private SKRect stage;
@@ -82,7 +82,7 @@ private static void DrawTallComposition(SKCanvas canvas, SKPaint paint, SKRect f
private static void DrawDarkFieldDetails(SKCanvas canvas, SKPaint paint, SKRect rect, float gap)
{
float barWidth = MathF.Max(3.0f, rect.Width * 0.012f);
- float startX = rect.Left + gap * 1.6f;
+ float startX = rect.Left + (gap * 1.6f);
float bottom = rect.Bottom - (gap * 1.6f);
for (int i = 0; i < 7; i++)
diff --git a/sources/Experiments/SkiaGallery/Scenes/PaintScene.cs b/sources/Experiments/SkiaGallery/Scenes/PaintScene.cs
index 42447217..8eb79fa1 100644
--- a/sources/Experiments/SkiaGallery/Scenes/PaintScene.cs
+++ b/sources/Experiments/SkiaGallery/Scenes/PaintScene.cs
@@ -1,8 +1,8 @@
-using SkiaSharp;
+using SkiaSharp;
-namespace SkiaGallery;
+namespace SkiaGallery.Scenes;
-internal sealed class PaintScene : GalleryScene
+internal class PaintScene : GalleryScene
{
private readonly SKBitmap bitmap;
private readonly SKImage image;
diff --git a/sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs b/sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs
index 1b1ebdc0..3cb05002 100644
--- a/sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs
+++ b/sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs
@@ -1,8 +1,8 @@
-using SkiaSharp;
+using SkiaSharp;
-namespace SkiaGallery;
+namespace SkiaGallery.Scenes;
-internal sealed class TypographyScene(GalleryResources resources) : GalleryScene(resources)
+internal class TypographyScene(GalleryResources resources) : GalleryScene(resources)
{
private const float MaximumHeroSize = 94.0f;
diff --git a/sources/Experiments/SkiaGallery/SkiaGallery.csproj b/sources/Experiments/SkiaGallery/SkiaGallery.csproj
index 0cdd50ec..331b78d6 100644
--- a/sources/Experiments/SkiaGallery/SkiaGallery.csproj
+++ b/sources/Experiments/SkiaGallery/SkiaGallery.csproj
@@ -1,4 +1,4 @@
-
+
Exe
From aacdc1737cddc5179be2a657472cd160dbae4bee Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 09:33:43 +0800
Subject: [PATCH 18/50] Update GalleryPalette colors & SKTexture usage handling
- Added new static SKColor fields to GalleryPalette: Navigation, Ink, Muted, Line, Accent, Coral, Blue, and Amber.
- Modified SKTexture constructor to set Usages to only ColorAttachment and TransferSrc.
- Removed Usages field from SKTextureDesc struct.
---
sources/Experiments/SkiaGallery/GalleryPalette.cs | 8 ++++++++
.../Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs | 2 +-
.../Zenith.NET.Extensions.Skia/SKTextureDesc.cs | 2 --
3 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/sources/Experiments/SkiaGallery/GalleryPalette.cs b/sources/Experiments/SkiaGallery/GalleryPalette.cs
index 6d44f3b5..20a00c0f 100644
--- a/sources/Experiments/SkiaGallery/GalleryPalette.cs
+++ b/sources/Experiments/SkiaGallery/GalleryPalette.cs
@@ -5,12 +5,20 @@ namespace SkiaGallery;
internal static class GalleryPalette
{
public static readonly SKColor Background = new(239, 242, 240);
+
public static readonly SKColor Navigation = new(21, 37, 32);
+
public static readonly SKColor Ink = new(25, 36, 32);
+
public static readonly SKColor Muted = new(99, 115, 108);
+
public static readonly SKColor Line = new(219, 225, 222);
+
public static readonly SKColor Accent = new(37, 153, 116);
+
public static readonly SKColor Coral = new(229, 100, 90);
+
public static readonly SKColor Blue = new(61, 126, 204);
+
public static readonly SKColor Amber = new(231, 168, 66);
}
\ No newline at end of file
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
index 044af113..57f5010a 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
@@ -21,7 +21,7 @@ internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
MipLevels = 1,
ArrayLayers = 1,
SampleCount = SampleCount.Count1,
- Usages = desc.Usages | TextureUsages.Sampled | TextureUsages.ColorAttachment | TextureUsages.TransferSrc | TextureUsages.TransferDst
+ Usages = TextureUsages.ColorAttachment | TextureUsages.TransferSrc
}));
surface = SKSurface.Create(renderer.GRContext, backendTexture, GRSurfaceOrigin.TopLeft, (int)SKFormats.Skia(desc.SampleCount), SKFormats.Skia(desc.Format));
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs
index a1e2bcfe..3f90d030 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs
@@ -9,6 +9,4 @@ public struct SKTextureDesc
public uint Height;
public SampleCount SampleCount;
-
- public TextureUsages Usages;
}
From 05591497970675feca7f2cf0a5fc4ffe351dd1a8 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 13:46:05 +0800
Subject: [PATCH 19/50] Replace SkiaGallery with InkCanvas experiment
Add an interactive vector whiteboard that demonstrates the minimal Skia GPU interop path: GraphicsContext, swap chain, GPU-backed SKTexture.Render, copy, present.
Finished strokes are baked into an SKPicture so each frame only redraws the in-progress stroke. The eraser sweeps a segment between frames and splits strokes into surviving fragments instead of deleting them outright.
---
Zenith.NET.slnx | 2 +-
.../{SkiaGallery => InkCanvas}/App.cs | 108 ++--
sources/Experiments/InkCanvas/Board.cs | 428 ++++++++++++++
.../Helpers/CocoaHelper.cs | 4 +-
.../InkCanvas.csproj} | 2 +-
sources/Experiments/InkCanvas/Program.cs | 3 +
sources/Experiments/InkCanvas/Stroke.cs | 173 ++++++
sources/Experiments/SkiaGallery/Gallery.cs | 535 ------------------
.../Experiments/SkiaGallery/GalleryPalette.cs | 24 -
.../SkiaGallery/GalleryResources.cs | 60 --
.../Experiments/SkiaGallery/GalleryScene.cs | 80 ---
sources/Experiments/SkiaGallery/Program.cs | 3 -
.../SkiaGallery/Scenes/GeometryScene.cs | 310 ----------
.../SkiaGallery/Scenes/MotionScene.cs | 181 ------
.../SkiaGallery/Scenes/OverviewScene.cs | 121 ----
.../SkiaGallery/Scenes/PaintScene.cs | 231 --------
.../SkiaGallery/Scenes/TypographyScene.cs | 260 ---------
17 files changed, 644 insertions(+), 1881 deletions(-)
rename sources/Experiments/{SkiaGallery => InkCanvas}/App.cs (64%)
create mode 100644 sources/Experiments/InkCanvas/Board.cs
rename sources/Experiments/{SkiaGallery => InkCanvas}/Helpers/CocoaHelper.cs (94%)
rename sources/Experiments/{SkiaGallery/SkiaGallery.csproj => InkCanvas/InkCanvas.csproj} (98%)
create mode 100644 sources/Experiments/InkCanvas/Program.cs
create mode 100644 sources/Experiments/InkCanvas/Stroke.cs
delete mode 100644 sources/Experiments/SkiaGallery/Gallery.cs
delete mode 100644 sources/Experiments/SkiaGallery/GalleryPalette.cs
delete mode 100644 sources/Experiments/SkiaGallery/GalleryResources.cs
delete mode 100644 sources/Experiments/SkiaGallery/GalleryScene.cs
delete mode 100644 sources/Experiments/SkiaGallery/Program.cs
delete mode 100644 sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs
delete mode 100644 sources/Experiments/SkiaGallery/Scenes/MotionScene.cs
delete mode 100644 sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs
delete mode 100644 sources/Experiments/SkiaGallery/Scenes/PaintScene.cs
delete mode 100644 sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs
diff --git a/Zenith.NET.slnx b/Zenith.NET.slnx
index 1802c615..b277f91f 100644
--- a/Zenith.NET.slnx
+++ b/Zenith.NET.slnx
@@ -3,8 +3,8 @@
+
-
diff --git a/sources/Experiments/SkiaGallery/App.cs b/sources/Experiments/InkCanvas/App.cs
similarity index 64%
rename from sources/Experiments/SkiaGallery/App.cs
rename to sources/Experiments/InkCanvas/App.cs
index 2d171dde..fcd06a71 100644
--- a/sources/Experiments/SkiaGallery/App.cs
+++ b/sources/Experiments/InkCanvas/App.cs
@@ -1,7 +1,7 @@
-using System.Numerics;
+using System.Numerics;
+using InkCanvas.Helpers;
using Silk.NET.Input;
using Silk.NET.Windowing;
-using SkiaGallery.Helpers;
using SkiaSharp;
using Zenith.NET;
using Zenith.NET.DirectX12;
@@ -9,20 +9,18 @@
using Zenith.NET.Metal;
using Zenith.NET.Vulkan;
-namespace SkiaGallery;
+namespace InkCanvas;
internal static class App
{
private static readonly IWindow window;
private static readonly IInputContext input;
private static readonly SwapChain swapChain;
- private static readonly Gallery gallery;
- private static readonly Action drawGallery = DrawGallery;
+ private static readonly Board board;
private static SKTexture texture;
private static float logicalWidth;
private static float logicalHeight;
- private static double totalSeconds;
static App()
{
@@ -49,15 +47,11 @@ static App()
window = Window.Create(WindowOptions.Default with
{
API = GraphicsAPI.None,
- Title = "Skia Gallery - Zenith.NET",
- Size = new(1280, 800),
- Position = new(80, 60),
- IsVisible = true,
- FramesPerSecond = 60.0,
- UpdatesPerSecond = 60.0,
- VSync = true
+ Title = "Ink Canvas - Zenith.NET",
+ Size = new(1280, 800)
});
window.Initialize();
+ window.Center();
input = window.CreateInput();
@@ -70,13 +64,9 @@ static App()
{
surface = Surface.Apple(CocoaHelper.CreateLayer(window.Native!.Cocoa!.Value), Width, Height);
}
- else if (window.Native?.X11 is { } x11)
- {
- surface = Surface.Xlib(x11.Display, (nint)x11.Window, Width, Height);
- }
else
{
- throw new PlatformNotSupportedException("SkiaGallery requires an X11 or XWayland window on Linux.");
+ surface = Surface.Xlib(window.Native!.X11!.Value.Display, (nint)window.Native.X11.Value.Window, Width, Height);
}
swapChain = Context.CreateSwapChain(new()
@@ -86,7 +76,7 @@ static App()
});
texture = CreateTexture(Width, Height);
- gallery = new(Context.GraphicsApi, Context.Capabilities.DeviceName);
+ board = new();
}
public static GraphicsContext Context { get; }
@@ -102,25 +92,19 @@ public static void Run()
IMouse mouse = input.Mice[0];
mouse.MouseMove += MouseMove;
mouse.MouseDown += MouseDown;
-
- IKeyboard keyboard = input.Keyboards[0];
- keyboard.KeyDown += KeyDown;
+ mouse.MouseUp += MouseUp;
window.Render += Render;
- try
- {
- window.Run();
- }
- finally
- {
- gallery.Dispose();
- texture.Dispose();
- swapChain.Dispose();
- input.Dispose();
- window.Dispose();
- Context.Dispose();
- }
+ window.Run();
+
+ board.Dispose();
+ texture.Dispose();
+ swapChain.Dispose();
+ input.Dispose();
+ window.Dispose();
+
+ Context.Dispose();
}
private static void Render(double delta)
@@ -134,21 +118,11 @@ private static void Render(double delta)
}
Vector2 dpiScale = DpiScale;
- float nextLogicalWidth = width / dpiScale.X;
- float nextLogicalHeight = height / dpiScale.Y;
- bool viewportChanged = logicalWidth != nextLogicalWidth || logicalHeight != nextLogicalHeight;
-
- logicalWidth = nextLogicalWidth;
- logicalHeight = nextLogicalHeight;
- totalSeconds += Math.Min(delta, 0.1);
+ logicalWidth = width / dpiScale.X;
+ logicalHeight = height / dpiScale.Y;
- bool resized = Resize(width, height);
- bool shouldRender = resized || viewportChanged || gallery.ShouldRender(totalSeconds);
-
- if (shouldRender)
- {
- texture.Render(drawGallery);
- }
+ Resize(width, height);
+ texture.Render(DrawBoard);
CommandBuffer commandBuffer = Context.GraphicsQueue.CommandBuffer();
@@ -168,46 +142,38 @@ private static void Render(double delta)
swapChain.Present();
}
- private static void DrawGallery(SKCanvas canvas)
+ private static void DrawBoard(SKCanvas canvas)
{
Vector2 dpiScale = DpiScale;
canvas.Save();
canvas.Scale(dpiScale.X, dpiScale.Y);
- gallery.Draw(canvas, logicalWidth, logicalHeight, totalSeconds);
+ board.Draw(canvas, logicalWidth, logicalHeight);
canvas.Restore();
}
private static void MouseMove(IMouse _, Vector2 position)
{
- gallery.PointerMove(position);
+ board.PointerMove(new(position.X, position.Y));
}
private static void MouseDown(IMouse mouse, MouseButton button)
{
if (button is MouseButton.Left)
{
- gallery.PointerDown(mouse.Position);
+ board.PointerDown(new(mouse.Position.X, mouse.Position.Y), erase: false);
+ }
+ else if (button is MouseButton.Right)
+ {
+ board.PointerDown(new(mouse.Position.X, mouse.Position.Y), erase: true);
}
}
- private static void KeyDown(IKeyboard _, Key key, int code)
+ private static void MouseUp(IMouse _, MouseButton button)
{
- if (key is Key.Left)
+ if (button is MouseButton.Left or MouseButton.Right)
{
- gallery.Previous();
- }
- else if (key is Key.Right)
- {
- gallery.Next();
- }
- else if (key is Key.Home)
- {
- gallery.Select(0);
- }
- else if (key is Key.End)
- {
- gallery.Select(gallery.SceneCount - 1);
+ board.PointerUp();
}
}
@@ -222,11 +188,11 @@ private static SKTexture CreateTexture(uint width, uint height)
});
}
- private static bool Resize(uint width, uint height)
+ private static void Resize(uint width, uint height)
{
if (texture.Desc.Width == width && texture.Desc.Height == height)
{
- return false;
+ return;
}
swapChain.Resize(width, height);
@@ -234,7 +200,5 @@ private static bool Resize(uint width, uint height)
SKTexture oldTexture = texture;
texture = CreateTexture(width, height);
oldTexture.Dispose();
-
- return true;
}
}
\ No newline at end of file
diff --git a/sources/Experiments/InkCanvas/Board.cs b/sources/Experiments/InkCanvas/Board.cs
new file mode 100644
index 00000000..93cfc6e6
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Board.cs
@@ -0,0 +1,428 @@
+using SkiaSharp;
+
+namespace InkCanvas;
+
+internal sealed class Board : IDisposable
+{
+ private const float ToolbarHeight = 64.0f;
+ private const float StatusHeight = 34.0f;
+ private const float SwatchSize = 30.0f;
+ private const float SwatchGap = 12.0f;
+ private const float ButtonWidth = 64.0f;
+ private const float EraserRadius = 22.0f;
+
+ private static readonly SKColor Surface = new(22, 24, 30);
+ private static readonly SKColor Panel = new(31, 34, 43);
+ private static readonly SKColor Divider = new(48, 52, 63);
+ private static readonly SKColor Label = new(150, 158, 172);
+ private static readonly SKColor Highlight = new(238, 242, 248);
+ private static readonly SKColor Grid = new(32, 35, 43);
+ private static readonly SKColor Selected = new(58, 64, 78);
+ private static readonly SKColor Cursor = new(150, 158, 172, 200);
+
+ private static readonly SKColor[] Palette =
+ [
+ new(238, 242, 248),
+ new(236, 108, 96),
+ new(238, 178, 74),
+ new(96, 196, 154),
+ new(102, 158, 240),
+ new(178, 134, 234)
+ ];
+
+ private static readonly float[] Widths = [2.0f, 4.0f, 8.0f, 16.0f];
+
+ private readonly List strokes = [];
+ private readonly SKRect[] swatchRects = new SKRect[Palette.Length];
+ private readonly SKRect[] widthRects = new SKRect[Widths.Length];
+ private readonly SKTypeface typeface;
+ private readonly SKFont labelFont;
+ private readonly SKPaint fillPaint = new() { IsAntialias = true };
+ private readonly SKPaint strokePaint = new()
+ {
+ IsAntialias = true,
+ Style = SKPaintStyle.Stroke,
+ StrokeCap = SKStrokeCap.Round,
+ StrokeJoin = SKStrokeJoin.Round
+ };
+
+ private SKPicture? bakedStrokes;
+ private Stroke? activeStroke;
+ private SKRect stage;
+ private SKRect canvasArea;
+ private SKRect eraserRect;
+ private SKRect clearRect;
+ private SKPoint pointer;
+ private SKPoint eraserLast;
+ private int colorIndex;
+ private int widthIndex = 1;
+ private bool eraserMode;
+ private bool erasing;
+ private bool pointerInside;
+ private bool bakeDirty = true;
+ private float layoutWidth = -1.0f;
+ private float layoutHeight = -1.0f;
+
+ public Board()
+ {
+ string family = OperatingSystem.IsMacOS() ? "SF Pro Text" : OperatingSystem.IsWindows() ? "Segoe UI" : "Noto Sans";
+
+ typeface = SKTypeface.FromFamilyName(family, SKFontStyle.Normal);
+ labelFont = new(typeface, 12.0f)
+ {
+ Edging = SKFontEdging.SubpixelAntialias,
+ Hinting = SKFontHinting.Slight,
+ Subpixel = true
+ };
+ }
+
+ public void Draw(SKCanvas canvas, float width, float height)
+ {
+ EnsureLayout(width, height);
+
+ fillPaint.Color = Surface;
+ canvas.DrawRect(stage, fillPaint);
+
+ DrawGrid(canvas);
+
+ if (bakeDirty)
+ {
+ bakedStrokes?.Dispose();
+ bakedStrokes = BakeStrokes();
+ bakeDirty = false;
+ }
+
+ canvas.Save();
+ canvas.ClipRect(canvasArea);
+ canvas.DrawPicture(bakedStrokes!);
+
+ if (activeStroke is not null)
+ {
+ DrawStroke(canvas, activeStroke);
+ }
+
+ if ((eraserMode || erasing) && pointerInside)
+ {
+ DrawEraserCursor(canvas);
+ }
+
+ canvas.Restore();
+
+ DrawToolbar(canvas);
+ DrawStatus(canvas);
+ }
+
+ public void PointerMove(SKPoint position)
+ {
+ pointer = position;
+ pointerInside = canvasArea.Contains(position.X, position.Y);
+
+ if (erasing)
+ {
+ Erase(eraserLast, position);
+ eraserLast = position;
+ }
+ else if (activeStroke is not null)
+ {
+ activeStroke.Add(position);
+ }
+ }
+
+ public void PointerDown(SKPoint position, bool erase)
+ {
+ pointer = position;
+
+ if (position.Y <= ToolbarHeight)
+ {
+ HandleToolbarClick(position);
+
+ return;
+ }
+
+ if (!canvasArea.Contains(position.X, position.Y))
+ {
+ return;
+ }
+
+ pointerInside = true;
+
+ if (erase || eraserMode)
+ {
+ erasing = true;
+ eraserLast = position;
+ Erase(position, position);
+
+ return;
+ }
+
+ activeStroke = new(Palette[colorIndex], Widths[widthIndex]);
+ activeStroke.Add(position);
+ }
+
+ public void PointerUp()
+ {
+ erasing = false;
+
+ if (activeStroke is null)
+ {
+ return;
+ }
+
+ strokes.Add(activeStroke);
+ activeStroke = null;
+ bakeDirty = true;
+ }
+
+ public void Clear()
+ {
+ if (strokes.Count is 0)
+ {
+ return;
+ }
+
+ foreach (Stroke stroke in strokes)
+ {
+ stroke.Dispose();
+ }
+
+ strokes.Clear();
+ bakeDirty = true;
+ }
+
+ public void SelectColor(int index)
+ {
+ colorIndex = Math.Clamp(index, 0, Palette.Length - 1);
+ eraserMode = false;
+ }
+
+ public void SelectWidth(int index)
+ {
+ widthIndex = Math.Clamp(index, 0, Widths.Length - 1);
+ }
+
+ public void ToggleEraser()
+ {
+ eraserMode = !eraserMode;
+ }
+
+ public void Dispose()
+ {
+ foreach (Stroke stroke in strokes)
+ {
+ stroke.Dispose();
+ }
+
+ strokes.Clear();
+ activeStroke?.Dispose();
+ bakedStrokes?.Dispose();
+ strokePaint.Dispose();
+ fillPaint.Dispose();
+ labelFont.Dispose();
+ typeface.Dispose();
+ }
+
+ private void EnsureLayout(float width, float height)
+ {
+ if (width == layoutWidth && height == layoutHeight)
+ {
+ return;
+ }
+
+ layoutWidth = width;
+ layoutHeight = height;
+ stage = new(0.0f, 0.0f, width, height);
+ canvasArea = new(0.0f, ToolbarHeight, width, MathF.Max(ToolbarHeight, height - StatusHeight));
+
+ float top = (ToolbarHeight - SwatchSize) * 0.5f;
+ float bottom = top + SwatchSize;
+
+ for (int index = 0; index < swatchRects.Length; index++)
+ {
+ float left = SwatchGap + (index * (SwatchSize + SwatchGap));
+ swatchRects[index] = new(left, top, left + SwatchSize, bottom);
+ }
+
+ float widthLeft = swatchRects[^1].Right + (SwatchGap * 2.0f);
+
+ for (int index = 0; index < widthRects.Length; index++)
+ {
+ float left = widthLeft + (index * (SwatchSize + SwatchGap));
+ widthRects[index] = new(left, top, left + SwatchSize, bottom);
+ }
+
+ float eraserLeft = widthRects[^1].Right + (SwatchGap * 2.0f);
+ eraserRect = new(eraserLeft, top, eraserLeft + ButtonWidth, bottom);
+
+ float clearLeft = MathF.Max(eraserRect.Right + (SwatchGap * 2.0f), width - SwatchGap - ButtonWidth);
+ clearRect = new(clearLeft, top, clearLeft + ButtonWidth, bottom);
+ }
+
+ private SKPicture BakeStrokes()
+ {
+ using SKPictureRecorder recorder = new();
+ SKCanvas canvas = recorder.BeginRecording(canvasArea);
+
+ foreach (Stroke stroke in strokes)
+ {
+ DrawStroke(canvas, stroke);
+ }
+
+ return recorder.EndRecording();
+ }
+
+ private void DrawStroke(SKCanvas canvas, Stroke stroke)
+ {
+ strokePaint.Color = stroke.Color;
+ strokePaint.StrokeWidth = stroke.Width;
+ canvas.DrawPath(stroke.Path, strokePaint);
+ }
+
+ private void DrawGrid(SKCanvas canvas)
+ {
+ const float spacing = 32.0f;
+
+ fillPaint.Color = Grid;
+
+ for (float x = spacing; x < canvasArea.Right; x += spacing)
+ {
+ canvas.DrawRect(x, canvasArea.Top, 1.0f, canvasArea.Height, fillPaint);
+ }
+
+ for (float y = canvasArea.Top + spacing; y < canvasArea.Bottom; y += spacing)
+ {
+ canvas.DrawRect(canvasArea.Left, y, canvasArea.Width, 1.0f, fillPaint);
+ }
+ }
+
+ private void DrawEraserCursor(SKCanvas canvas)
+ {
+ strokePaint.Color = Cursor;
+ strokePaint.StrokeWidth = 1.5f;
+ canvas.DrawCircle(pointer, EraserRadius, strokePaint);
+ }
+
+ private void DrawToolbar(SKCanvas canvas)
+ {
+ fillPaint.Color = Panel;
+ canvas.DrawRect(0.0f, 0.0f, stage.Width, ToolbarHeight, fillPaint);
+
+ fillPaint.Color = Divider;
+ canvas.DrawRect(0.0f, ToolbarHeight - 1.0f, stage.Width, 1.0f, fillPaint);
+
+ for (int index = 0; index < Palette.Length; index++)
+ {
+ SKRect swatch = swatchRects[index];
+
+ fillPaint.Color = Palette[index];
+ canvas.DrawRoundRect(swatch, 6.0f, 6.0f, fillPaint);
+
+ if (index == colorIndex && !eraserMode)
+ {
+ strokePaint.Color = Highlight;
+ strokePaint.StrokeWidth = 2.0f;
+ canvas.DrawRoundRect(SKRect.Inflate(swatch, 4.0f, 4.0f), 9.0f, 9.0f, strokePaint);
+ }
+ }
+
+ for (int index = 0; index < Widths.Length; index++)
+ {
+ SKRect slot = widthRects[index];
+
+ fillPaint.Color = index == widthIndex ? Selected : Panel;
+ canvas.DrawRoundRect(slot, 6.0f, 6.0f, fillPaint);
+
+ fillPaint.Color = eraserMode ? Label : Palette[colorIndex];
+ canvas.DrawCircle(slot.MidX, slot.MidY, Widths[index] * 0.5f, fillPaint);
+ }
+
+ DrawButton(canvas, eraserRect, "ERASE", eraserMode, true);
+ DrawButton(canvas, clearRect, "CLEAR", false, strokes.Count > 0);
+ }
+
+ private void DrawButton(SKCanvas canvas, SKRect rect, string text, bool active, bool enabled)
+ {
+ SKColor accent = active ? Highlight : enabled ? Label : Divider;
+
+ fillPaint.Color = active ? Selected : Panel;
+ canvas.DrawRoundRect(rect, 6.0f, 6.0f, fillPaint);
+
+ strokePaint.Color = accent;
+ strokePaint.StrokeWidth = 1.5f;
+ canvas.DrawRoundRect(rect, 6.0f, 6.0f, strokePaint);
+
+ fillPaint.Color = accent;
+ canvas.DrawText(text, rect.MidX, rect.MidY + 4.0f, SKTextAlign.Center, labelFont, fillPaint);
+ }
+
+ private void DrawStatus(SKCanvas canvas)
+ {
+ float top = stage.Height - StatusHeight;
+
+ fillPaint.Color = Panel;
+ canvas.DrawRect(0.0f, top, stage.Width, StatusHeight, fillPaint);
+
+ fillPaint.Color = Divider;
+ canvas.DrawRect(0.0f, top, stage.Width, 1.0f, fillPaint);
+
+ int points = 0;
+
+ foreach (Stroke stroke in strokes)
+ {
+ points += stroke.PointCount;
+ }
+
+ float baseline = top + (StatusHeight * 0.5f) + 4.0f;
+
+ fillPaint.Color = Label;
+ canvas.DrawText($"STROKES {strokes.Count} POINTS {points}", SwatchGap, baseline, SKTextAlign.Left, labelFont, fillPaint);
+ canvas.DrawText("DRAG TO DRAW RIGHT DRAG TO ERASE", stage.Width - SwatchGap, baseline, SKTextAlign.Right, labelFont, fillPaint);
+ }
+
+ private void HandleToolbarClick(SKPoint position)
+ {
+ for (int index = 0; index < Palette.Length; index++)
+ {
+ if (swatchRects[index].Contains(position.X, position.Y))
+ {
+ SelectColor(index);
+
+ return;
+ }
+ }
+
+ for (int index = 0; index < Widths.Length; index++)
+ {
+ if (widthRects[index].Contains(position.X, position.Y))
+ {
+ SelectWidth(index);
+
+ return;
+ }
+ }
+
+ if (eraserRect.Contains(position.X, position.Y))
+ {
+ ToggleEraser();
+ }
+ else if (clearRect.Contains(position.X, position.Y))
+ {
+ Clear();
+ }
+ }
+
+ private void Erase(SKPoint from, SKPoint to)
+ {
+ for (int index = strokes.Count - 1; index >= 0; index--)
+ {
+ if (strokes[index].Split(from, to, EraserRadius) is not { } fragments)
+ {
+ continue;
+ }
+
+ strokes[index].Dispose();
+ strokes.RemoveAt(index);
+ strokes.InsertRange(index, fragments);
+ bakeDirty = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Helpers/CocoaHelper.cs b/sources/Experiments/InkCanvas/Helpers/CocoaHelper.cs
similarity index 94%
rename from sources/Experiments/SkiaGallery/Helpers/CocoaHelper.cs
rename to sources/Experiments/InkCanvas/Helpers/CocoaHelper.cs
index eeb49a1f..7ef35afb 100644
--- a/sources/Experiments/SkiaGallery/Helpers/CocoaHelper.cs
+++ b/sources/Experiments/InkCanvas/Helpers/CocoaHelper.cs
@@ -1,6 +1,6 @@
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
-namespace SkiaGallery.Helpers;
+namespace InkCanvas.Helpers;
internal static partial class CocoaHelper
{
diff --git a/sources/Experiments/SkiaGallery/SkiaGallery.csproj b/sources/Experiments/InkCanvas/InkCanvas.csproj
similarity index 98%
rename from sources/Experiments/SkiaGallery/SkiaGallery.csproj
rename to sources/Experiments/InkCanvas/InkCanvas.csproj
index 331b78d6..8b441176 100644
--- a/sources/Experiments/SkiaGallery/SkiaGallery.csproj
+++ b/sources/Experiments/InkCanvas/InkCanvas.csproj
@@ -17,4 +17,4 @@
-
\ No newline at end of file
+
diff --git a/sources/Experiments/InkCanvas/Program.cs b/sources/Experiments/InkCanvas/Program.cs
new file mode 100644
index 00000000..f7868902
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Program.cs
@@ -0,0 +1,3 @@
+using InkCanvas;
+
+App.Run();
\ No newline at end of file
diff --git a/sources/Experiments/InkCanvas/Stroke.cs b/sources/Experiments/InkCanvas/Stroke.cs
new file mode 100644
index 00000000..d81d7bfe
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Stroke.cs
@@ -0,0 +1,173 @@
+using SkiaSharp;
+
+namespace InkCanvas;
+
+internal sealed class Stroke(SKColor color, float width) : IDisposable
+{
+ private const float MinimumPointDistance = 1.5f;
+
+ private readonly List points = [];
+
+ private SKPath? path;
+ private SKRect bounds;
+
+ private Stroke(SKColor color, float width, ReadOnlySpan source) : this(color, width)
+ {
+ points.AddRange(source);
+ bounds = ComputeBounds(source, width);
+ }
+
+ public SKColor Color { get; } = color;
+
+ public float Width { get; } = width;
+
+ public int PointCount => points.Count;
+
+ public SKPath Path => path ??= BuildPath();
+
+ public void Add(SKPoint point)
+ {
+ if (points.Count > 0)
+ {
+ SKPoint last = points[^1];
+ float dx = point.X - last.X;
+ float dy = point.Y - last.Y;
+
+ if ((dx * dx) + (dy * dy) < MinimumPointDistance * MinimumPointDistance)
+ {
+ return;
+ }
+ }
+
+ points.Add(point);
+
+ float radius = Width * 0.5f;
+ SKRect extent = new(point.X - radius, point.Y - radius, point.X + radius, point.Y + radius);
+ bounds = points.Count is 1 ? extent : SKRect.Union(bounds, extent);
+
+ path?.Dispose();
+ path = null;
+ }
+
+ public List? Split(SKPoint from, SKPoint to, float radius)
+ {
+ float threshold = radius + (Width * 0.5f);
+ SKRect swept = new(
+ MathF.Min(from.X, to.X) - threshold,
+ MathF.Min(from.Y, to.Y) - threshold,
+ MathF.Max(from.X, to.X) + threshold,
+ MathF.Max(from.Y, to.Y) + threshold);
+
+ if (!swept.IntersectsWith(bounds))
+ {
+ return null;
+ }
+
+ float thresholdSquared = threshold * threshold;
+ List fragments = [];
+ List survivors = [];
+ bool touched = false;
+
+ foreach (SKPoint point in points)
+ {
+ if (SegmentDistanceSquared(from, to, point) <= thresholdSquared)
+ {
+ touched = true;
+
+ if (survivors.Count > 1)
+ {
+ fragments.Add(new(Color, Width, [.. survivors]));
+ }
+
+ survivors.Clear();
+ }
+ else
+ {
+ survivors.Add(point);
+ }
+ }
+
+ if (!touched)
+ {
+ return null;
+ }
+
+ if (survivors.Count > 1)
+ {
+ fragments.Add(new(Color, Width, [.. survivors]));
+ }
+
+ return fragments;
+ }
+
+ public void Dispose()
+ {
+ path?.Dispose();
+ }
+
+ private SKPath BuildPath()
+ {
+ using SKPathBuilder builder = new();
+
+ if (points.Count is 1)
+ {
+ builder.AddCircle(points[0].X, points[0].Y, Width * 0.25f, SKPathDirection.Clockwise);
+
+ return builder.Detach();
+ }
+
+ builder.MoveTo(points[0]);
+
+ for (int index = 1; index < points.Count - 1; index++)
+ {
+ SKPoint current = points[index];
+ SKPoint next = points[index + 1];
+ SKPoint middle = new((current.X + next.X) * 0.5f, (current.Y + next.Y) * 0.5f);
+
+ builder.QuadTo(current, middle);
+ }
+
+ builder.LineTo(points[^1]);
+
+ return builder.Detach();
+ }
+
+ private static SKRect ComputeBounds(ReadOnlySpan points, float width)
+ {
+ float radius = width * 0.5f;
+ SKRect result = new(points[0].X - radius, points[0].Y - radius, points[0].X + radius, points[0].Y + radius);
+
+ for (int index = 1; index < points.Length; index++)
+ {
+ SKPoint point = points[index];
+ result = SKRect.Union(result, new(point.X - radius, point.Y - radius, point.X + radius, point.Y + radius));
+ }
+
+ return result;
+ }
+
+ private static float DistanceSquared(SKPoint first, SKPoint second)
+ {
+ float dx = first.X - second.X;
+ float dy = first.Y - second.Y;
+
+ return (dx * dx) + (dy * dy);
+ }
+
+ private static float SegmentDistanceSquared(SKPoint start, SKPoint end, SKPoint point)
+ {
+ float dx = end.X - start.X;
+ float dy = end.Y - start.Y;
+ float lengthSquared = (dx * dx) + (dy * dy);
+
+ if (lengthSquared < float.Epsilon)
+ {
+ return DistanceSquared(start, point);
+ }
+
+ float amount = Math.Clamp((((point.X - start.X) * dx) + ((point.Y - start.Y) * dy)) / lengthSquared, 0.0f, 1.0f);
+ SKPoint projection = new(start.X + (amount * dx), start.Y + (amount * dy));
+
+ return DistanceSquared(projection, point);
+ }
+}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Gallery.cs b/sources/Experiments/SkiaGallery/Gallery.cs
deleted file mode 100644
index 31af21ee..00000000
--- a/sources/Experiments/SkiaGallery/Gallery.cs
+++ /dev/null
@@ -1,535 +0,0 @@
-using System.Numerics;
-using SkiaGallery.Scenes;
-using SkiaSharp;
-using Zenith.NET;
-
-namespace SkiaGallery;
-
-internal class Gallery : IDisposable
-{
- private const float ExpandedSidebarWidth = 228.0f;
- private const float CompactSidebarWidth = 80.0f;
- private const float CompactBreakpoint = 1152.0f;
- private const float ContentRight = 32.0f;
- private const float DefaultContentTop = 142.0f;
- private const float DenseContentTop = 112.0f;
- private const float DefaultContentBottom = 82.0f;
- private const float ReducedContentBottom = 24.0f;
- private const float DefaultNavigationStep = 60.0f;
- private const float MinimumNavigationStep = 48.0f;
- private const float DenseHeightBreakpoint = 680.0f;
- private const double TransitionDuration = 0.22;
-
- private readonly GraphicsApi graphicsApi;
- private readonly string deviceName;
- private readonly GalleryResources resources = new();
- private readonly GalleryScene[] scenes;
- private readonly SKTextBlob[] titleBlobs;
- private readonly SKTextBlob[] descriptionBlobs;
- private readonly string[] descriptionTexts;
- private readonly SKTextBlob[] compactTitleBlobs;
- private readonly SKTextBlob pausedHeaderBlob;
- private readonly SKTextBlob pausedTitleBlob;
- private readonly SKTextBlob pausedDescriptionBlob;
- private readonly SKPaint activePaint = new() { Color = GalleryPalette.Accent, IsAntialias = true };
- private readonly SKPaint hoverPaint = new() { Color = new(49, 72, 64), IsAntialias = true };
- private readonly SKPaint titlePaint = new() { Color = GalleryPalette.Ink, IsAntialias = true };
- private readonly SKPaint descriptionPaint = new() { Color = GalleryPalette.Muted, IsAntialias = true };
- private readonly SKPaint pausedIconPaint = new() { Color = GalleryPalette.Accent, IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 2.0f, StrokeCap = SKStrokeCap.Round };
- private readonly float pausedHeaderWidth;
- private readonly float pausedTitleWidth;
- private readonly float pausedDescriptionWidth;
-
- private int activeIndex;
- private int hoverIndex = -1;
- private SKPicture? chromePicture;
- private SKPicture? navigationPicture;
- private float viewportWidth = -1.0f;
- private float viewportHeight = -1.0f;
- private float sidebarWidth;
- private float contentLeft;
- private float sceneWidth;
- private float sceneHeight;
- private float navigationTop;
- private float navigationStep = DefaultNavigationStep;
- private float contentTop;
- private float contentBottom;
- private bool compact;
- private bool dense;
- private bool layoutPaused;
- private bool showFooter = true;
- private bool showNavigation = true;
- private double lastSeconds;
- private double transitionSeconds;
- private bool dirty = true;
- private bool transitionCompleteRendered;
-
- public Gallery(GraphicsApi graphicsApi, string deviceName)
- {
- this.graphicsApi = graphicsApi;
- this.deviceName = deviceName;
-
- scenes =
- [
- new OverviewScene(resources),
- new GeometryScene(resources),
- new TypographyScene(resources),
- new PaintScene(resources),
- new MotionScene(resources)
- ];
-
- titleBlobs = new SKTextBlob[scenes.Length];
- descriptionBlobs = new SKTextBlob[scenes.Length];
- descriptionTexts = new string[scenes.Length];
- compactTitleBlobs = new SKTextBlob[scenes.Length];
-
- for (int i = 0; i < scenes.Length; i++)
- {
- titleBlobs[i] = GalleryResources.CreateText(scenes[i].Title, resources.TitleFont);
- descriptionTexts[i] = scenes[i].Description;
- descriptionBlobs[i] = GalleryResources.CreateText(descriptionTexts[i], resources.BodyFont);
- compactTitleBlobs[i] = GalleryResources.CreateText(scenes[i].Navigation, resources.SectionFont);
- }
-
- const string pausedHeader = "GPU scene paused at this window size";
- const string pausedTitle = "More room needed";
- const string pausedDescription = "Increase the window size to continue.";
-
- pausedHeaderBlob = GalleryResources.CreateText(pausedHeader, resources.BodyFont);
- pausedTitleBlob = GalleryResources.CreateText(pausedTitle, resources.SectionFont);
- pausedDescriptionBlob = GalleryResources.CreateText(pausedDescription, resources.BodyFont);
- pausedHeaderWidth = resources.BodyFont.MeasureText(pausedHeader);
- pausedTitleWidth = resources.SectionFont.MeasureText(pausedTitle);
- pausedDescriptionWidth = resources.BodyFont.MeasureText(pausedDescription);
- }
-
- public int SceneCount => scenes.Length;
-
- public bool ShouldRender(double seconds)
- {
- lastSeconds = seconds;
-
- return dirty || (!layoutPaused && scenes[activeIndex].IsAnimated) || !transitionCompleteRendered;
- }
-
- public void Draw(SKCanvas canvas, float width, float height, double seconds)
- {
- SetViewport(width, height);
- lastSeconds = seconds;
- dirty = false;
-
- float titleBaseline = dense ? 55.0f : 73.0f;
- float descriptionBaseline = dense ? 82.0f : 108.0f;
- float headerBottom = contentTop - 16.0f;
-
- canvas.Clear(GalleryPalette.Background);
- canvas.DrawPicture(chromePicture!);
-
- if (showNavigation && hoverIndex >= 0 && hoverIndex != activeIndex)
- {
- canvas.DrawRoundRect(NavigationRect(hoverIndex), 6.0f, 6.0f, hoverPaint);
- }
-
- if (showNavigation)
- {
- canvas.DrawRoundRect(NavigationRect(activeIndex), 6.0f, 6.0f, activePaint);
- }
-
- canvas.DrawPicture(navigationPicture!);
-
- canvas.Save();
- canvas.ClipRect(new(contentLeft, 0.0f, MathF.Max(contentLeft, width - ContentRight), headerBottom));
-
- if (layoutPaused)
- {
- canvas.DrawText(compactTitleBlobs[activeIndex], contentLeft, titleBaseline, titlePaint);
-
- if (sceneWidth >= pausedHeaderWidth)
- {
- canvas.DrawText(pausedHeaderBlob, contentLeft, descriptionBaseline, descriptionPaint);
- }
- }
- else
- {
- canvas.DrawText(titleBlobs[activeIndex], contentLeft, titleBaseline, titlePaint);
- canvas.DrawText(descriptionBlobs[activeIndex], contentLeft + 1.0f, descriptionBaseline, descriptionPaint);
- }
-
- canvas.Restore();
-
- if (layoutPaused)
- {
- transitionCompleteRendered = true;
- DrawPausedState(canvas, width, height);
- return;
- }
-
- float transition = Math.Clamp((float)((seconds - transitionSeconds) / TransitionDuration), 0.0f, 1.0f);
- float eased = 1.0f - MathF.Pow(1.0f - transition, 3.0f);
-
- transitionCompleteRendered = transition >= 1.0f;
-
- canvas.Save();
- canvas.ClipRect(new(contentLeft, contentTop, MathF.Max(contentLeft, width - ContentRight), MathF.Max(contentTop, height - contentBottom)));
- canvas.Translate(contentLeft + ((1.0f - eased) * 18.0f), contentTop);
- scenes[activeIndex].Draw(canvas, sceneWidth, sceneHeight, seconds);
- canvas.Restore();
- }
-
- public void PointerMove(Vector2 position)
- {
- int index = HitTest(position);
-
- if (index != hoverIndex)
- {
- hoverIndex = index;
- dirty = true;
- }
- }
-
- public void PointerDown(Vector2 position)
- {
- int index = HitTest(position);
-
- if (index >= 0)
- {
- Select(index);
- }
- }
-
- public void Previous()
- {
- Select((activeIndex + scenes.Length - 1) % scenes.Length);
- }
-
- public void Next()
- {
- Select((activeIndex + 1) % scenes.Length);
- }
-
- public void Select(int index)
- {
- if ((uint)index >= (uint)scenes.Length || index == activeIndex)
- {
- return;
- }
-
- activeIndex = index;
- transitionSeconds = lastSeconds;
- transitionCompleteRendered = false;
- dirty = true;
- }
-
- public void Dispose()
- {
- for (int i = scenes.Length - 1; i >= 0; i--)
- {
- scenes[i].Dispose();
- compactTitleBlobs[i].Dispose();
- descriptionBlobs[i].Dispose();
- titleBlobs[i].Dispose();
- }
-
- pausedDescriptionBlob.Dispose();
- pausedTitleBlob.Dispose();
- pausedHeaderBlob.Dispose();
- navigationPicture?.Dispose();
- chromePicture?.Dispose();
- pausedIconPaint.Dispose();
- descriptionPaint.Dispose();
- titlePaint.Dispose();
- hoverPaint.Dispose();
- activePaint.Dispose();
- resources.Dispose();
- }
-
- private SKPicture RecordChrome(float width, float height)
- {
- using SKPictureRecorder recorder = new();
- SKCanvas canvas = recorder.BeginRecording(new(0.0f, 0.0f, width, height));
- using SKPaint paint = new() { IsAntialias = true };
-
- paint.Color = GalleryPalette.Background;
- canvas.DrawRect(0.0f, 0.0f, width, height, paint);
-
- paint.Color = GalleryPalette.Navigation;
- canvas.DrawRect(0.0f, 0.0f, sidebarWidth, height, paint);
-
- paint.Color = GalleryPalette.Accent;
- float logoLeft = compact ? 20.0f : 28.0f;
- canvas.DrawRoundRect(new(logoLeft, 32.0f, logoLeft + 40.0f, 72.0f), 6.0f, 6.0f, paint);
-
- paint.Color = GalleryPalette.Navigation;
- canvas.DrawCircle(logoLeft + 20.0f, 52.0f, 7.0f, paint);
-
- if (!compact)
- {
- paint.Color = SKColors.White;
- canvas.DrawText("SKIA", 82.0f, 50.0f, SKTextAlign.Left, resources.NavigationFont, paint);
-
- paint.Color = new(137, 158, 150);
- canvas.DrawText("GPU GALLERY", 82.0f, 68.0f, SKTextAlign.Left, resources.CaptionFont, paint);
- }
-
- paint.Color = new(72, 96, 87);
- canvas.DrawLine(compact ? 16.0f : 24.0f, 118.0f, compact ? 64.0f : 204.0f, 118.0f, paint);
-
- float contentRight = MathF.Max(contentLeft, width - ContentRight);
-
- paint.Color = GalleryPalette.Line;
- canvas.DrawLine(contentLeft, contentTop - 16.0f, contentRight, contentTop - 16.0f, paint);
-
- if (showFooter)
- {
- float footerLine = height - 56.0f;
- canvas.DrawLine(contentLeft, footerLine, contentRight, footerLine, paint);
-
- paint.Color = GalleryPalette.Muted;
- float footerBaseline = height - 25.0f;
- string surfaceLabel = $"{graphicsApi} / SKIA GPU SURFACE";
- canvas.DrawText(surfaceLabel, contentLeft, footerBaseline, SKTextAlign.Left, resources.CaptionFont, paint);
-
- float labelRight = contentLeft + resources.CaptionFont.MeasureText(surfaceLabel);
- float deviceRight = contentRight;
- float deviceWidth = MathF.Max(0.0f, deviceRight - labelRight - 32.0f);
-
- if (deviceWidth > resources.CaptionFont.MeasureText("..."))
- {
- string displayDevice = FitText(deviceName, resources.CaptionFont, MathF.Min(430.0f, deviceWidth));
- canvas.DrawText(displayDevice, deviceRight, footerBaseline, SKTextAlign.Right, resources.CaptionFont, paint);
- }
- }
-
- return recorder.EndRecording();
- }
-
- private static string FitText(string text, SKFont font, float width)
- {
- if (font.MeasureText(text) <= width)
- {
- return text;
- }
-
- const string ellipsis = "...";
- int minimum = 0;
- int maximum = text.Length;
-
- while (minimum < maximum)
- {
- int length = (minimum + maximum + 1) / 2;
- string candidate = string.Concat(text.AsSpan(0, length), ellipsis);
-
- if (font.MeasureText(candidate) <= width)
- {
- minimum = length;
- }
- else
- {
- maximum = length - 1;
- }
- }
-
- return string.Concat(text.AsSpan(0, minimum), ellipsis);
- }
-
- private SKPicture RecordNavigation(float width, float height)
- {
- using SKPictureRecorder recorder = new();
- SKCanvas canvas = recorder.BeginRecording(new(0.0f, 0.0f, width, height));
- using SKPaint paint = new() { Color = SKColors.White, IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 1.8f };
-
- if (!showNavigation)
- {
- return recorder.EndRecording();
- }
-
- for (int i = 0; i < scenes.Length; i++)
- {
- float centerX = compact ? 40.0f : 44.0f;
- float centerY = navigationTop + (i * navigationStep) + 24.0f;
-
- DrawNavigationIcon(canvas, paint, i, centerX, centerY);
-
- if (!compact)
- {
- paint.Style = SKPaintStyle.Fill;
- canvas.DrawText(scenes[i].Navigation, 70.0f, centerY + 5.0f, SKTextAlign.Left, resources.NavigationFont, paint);
- }
- }
-
- return recorder.EndRecording();
- }
-
- private static void DrawNavigationIcon(SKCanvas canvas, SKPaint paint, int index, float x, float y)
- {
- paint.Style = SKPaintStyle.Stroke;
- paint.Color = SKColors.White;
-
- if (index is 0)
- {
- canvas.DrawRoundRect(new(x - 8.0f, y - 8.0f, x - 1.0f, y - 1.0f), 1.5f, 1.5f, paint);
- canvas.DrawRoundRect(new(x + 2.0f, y - 8.0f, x + 8.0f, y + 8.0f), 1.5f, 1.5f, paint);
- canvas.DrawRoundRect(new(x - 8.0f, y + 2.0f, x - 1.0f, y + 8.0f), 1.5f, 1.5f, paint);
- }
- else if (index is 1)
- {
- canvas.DrawCircle(x - 3.0f, y - 2.0f, 6.0f, paint);
- canvas.DrawRect(new(x, y - 5.0f, x + 9.0f, y + 7.0f), paint);
- }
- else if (index is 2)
- {
- canvas.DrawLine(x - 8.0f, y + 8.0f, x, y - 8.0f, paint);
- canvas.DrawLine(x, y - 8.0f, x + 8.0f, y + 8.0f, paint);
- canvas.DrawLine(x - 4.0f, y + 1.0f, x + 4.0f, y + 1.0f, paint);
- }
- else if (index is 3)
- {
- canvas.DrawCircle(x - 4.0f, y - 3.0f, 5.0f, paint);
- canvas.DrawCircle(x + 4.0f, y - 3.0f, 5.0f, paint);
- canvas.DrawCircle(x, y + 4.0f, 5.0f, paint);
- }
- else
- {
- canvas.DrawCircle(x, y, 8.0f, paint);
- canvas.DrawCircle(x, y, 2.0f, paint);
- canvas.DrawLine(x - 11.0f, y, x - 7.0f, y, paint);
- canvas.DrawLine(x + 7.0f, y, x + 11.0f, y, paint);
- }
- }
-
- private void SetViewport(float width, float height)
- {
- bool nextCompact = width < CompactBreakpoint;
- bool nextDense = height < DenseHeightBreakpoint;
- bool nextShowFooter = !nextDense;
- float nextContentTop = nextDense ? DenseContentTop : DefaultContentTop;
- float nextContentBottom = nextShowFooter ? DefaultContentBottom : ReducedContentBottom;
- float nextSidebarWidth = nextCompact ? CompactSidebarWidth : ExpandedSidebarWidth;
- float nextContentLeft = nextSidebarWidth + (nextCompact ? 24.0f : 32.0f);
- float nextSceneWidth = MathF.Max(1.0f, width - nextContentLeft - ContentRight);
- float nextSceneHeight = MathF.Max(1.0f, height - nextContentTop - nextContentBottom);
- bool nextLayoutPaused = !scenes[activeIndex].CanRender(nextSceneWidth, nextSceneHeight);
-
- float nextNavigationTop = nextCompact ? 136.0f : 156.0f;
- float nextNavigationStep = DefaultNavigationStep;
- bool nextShowNavigation = true;
-
- if (nextDense)
- {
- nextNavigationTop = 104.0f;
- float availableStep = (height - nextNavigationTop - 56.0f) / (scenes.Length - 1.0f);
- nextShowNavigation = availableStep >= MinimumNavigationStep;
- nextNavigationStep = MathF.Min(DefaultNavigationStep, availableStep);
- }
-
- if (width != viewportWidth || height != viewportHeight || nextCompact != compact || nextDense != dense || nextLayoutPaused != layoutPaused || nextShowFooter != showFooter || nextShowNavigation != showNavigation)
- {
- if (nextSceneWidth != sceneWidth)
- {
- UpdateDescriptionBlobs(nextSceneWidth);
- }
-
- compact = nextCompact;
- dense = nextDense;
- layoutPaused = nextLayoutPaused;
- showFooter = nextShowFooter;
- showNavigation = nextShowNavigation;
- sidebarWidth = nextSidebarWidth;
- contentLeft = nextContentLeft;
- contentTop = nextContentTop;
- contentBottom = nextContentBottom;
- navigationTop = nextNavigationTop;
- navigationStep = nextNavigationStep;
- sceneWidth = nextSceneWidth;
- sceneHeight = nextSceneHeight;
-
- chromePicture?.Dispose();
- navigationPicture?.Dispose();
- chromePicture = RecordChrome(width, height);
- navigationPicture = RecordNavigation(width, height);
- viewportWidth = width;
- viewportHeight = height;
- dirty = true;
- }
- }
-
- private void UpdateDescriptionBlobs(float width)
- {
- for (int i = 0; i < scenes.Length; i++)
- {
- string text = FitText(scenes[i].Description, resources.BodyFont, width - 2.0f);
-
- if (text == descriptionTexts[i])
- {
- continue;
- }
-
- descriptionTexts[i] = text;
- descriptionBlobs[i].Dispose();
- descriptionBlobs[i] = GalleryResources.CreateText(text, resources.BodyFont);
- }
- }
-
- private int HitTest(Vector2 position)
- {
- if (!showNavigation)
- {
- return -1;
- }
-
- for (int i = 0; i < scenes.Length; i++)
- {
- if (NavigationRect(i).Contains(position.X, position.Y))
- {
- return i;
- }
- }
-
- return -1;
- }
-
- private SKRect NavigationRect(int index)
- {
- float top = navigationTop + (index * navigationStep);
-
- return compact ? new(10.0f, top, 70.0f, top + 48.0f) : new(18.0f, top, 210.0f, top + 48.0f);
- }
-
- private void DrawPausedState(SKCanvas canvas, float width, float height)
- {
- SKRect area = new(contentLeft, contentTop, MathF.Max(contentLeft, width - ContentRight), MathF.Max(contentTop, height - contentBottom));
-
- if (area.Width < 120.0f || area.Height < 54.0f)
- {
- return;
- }
-
- canvas.Save();
- canvas.ClipRect(area);
-
- float centerX = area.MidX;
- float titleBaseline = area.MidY + 11.0f;
-
- if (area.Height >= 130.0f)
- {
- SKRect icon = new(centerX - 20.0f, titleBaseline - 73.0f, centerX + 20.0f, titleBaseline - 43.0f);
- canvas.DrawRoundRect(icon, 4.0f, 4.0f, pausedIconPaint);
- canvas.DrawLine(icon.Left - 7.0f, icon.Top + 7.0f, icon.Left + 3.0f, icon.Top + 7.0f, pausedIconPaint);
- canvas.DrawLine(icon.Left + 7.0f, icon.Top - 7.0f, icon.Left + 7.0f, icon.Top + 3.0f, pausedIconPaint);
- canvas.DrawLine(icon.Right - 3.0f, icon.Bottom - 7.0f, icon.Right + 7.0f, icon.Bottom - 7.0f, pausedIconPaint);
- canvas.DrawLine(icon.Right - 7.0f, icon.Bottom - 3.0f, icon.Right - 7.0f, icon.Bottom + 7.0f, pausedIconPaint);
- }
-
- if (area.Width >= pausedTitleWidth + 24.0f)
- {
- canvas.DrawText(pausedTitleBlob, centerX - (pausedTitleWidth * 0.5f), titleBaseline, titlePaint);
- }
-
- if (area.Width >= pausedDescriptionWidth + 24.0f && area.Height >= 105.0f)
- {
- canvas.DrawText(pausedDescriptionBlob, centerX - (pausedDescriptionWidth * 0.5f), titleBaseline + 31.0f, descriptionPaint);
- }
-
- canvas.Restore();
- }
-}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/GalleryPalette.cs b/sources/Experiments/SkiaGallery/GalleryPalette.cs
deleted file mode 100644
index 20a00c0f..00000000
--- a/sources/Experiments/SkiaGallery/GalleryPalette.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using SkiaSharp;
-
-namespace SkiaGallery;
-
-internal static class GalleryPalette
-{
- public static readonly SKColor Background = new(239, 242, 240);
-
- public static readonly SKColor Navigation = new(21, 37, 32);
-
- public static readonly SKColor Ink = new(25, 36, 32);
-
- public static readonly SKColor Muted = new(99, 115, 108);
-
- public static readonly SKColor Line = new(219, 225, 222);
-
- public static readonly SKColor Accent = new(37, 153, 116);
-
- public static readonly SKColor Coral = new(229, 100, 90);
-
- public static readonly SKColor Blue = new(61, 126, 204);
-
- public static readonly SKColor Amber = new(231, 168, 66);
-}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/GalleryResources.cs b/sources/Experiments/SkiaGallery/GalleryResources.cs
deleted file mode 100644
index f6dd490c..00000000
--- a/sources/Experiments/SkiaGallery/GalleryResources.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using SkiaSharp;
-
-namespace SkiaGallery;
-
-internal class GalleryResources : IDisposable
-{
- public GalleryResources()
- {
- string family = OperatingSystem.IsMacOS() ? "SF Pro Display" : OperatingSystem.IsWindows() ? "Segoe UI" : "Noto Sans";
-
- RegularTypeface = SKTypeface.FromFamilyName(family, SKFontStyle.Normal);
- MediumTypeface = SKTypeface.FromFamilyName(family, new(SKFontStyleWeight.SemiBold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright));
-
- CaptionFont = CreateFont(RegularTypeface, 12.0f);
- NavigationFont = CreateFont(MediumTypeface, 15.0f);
- BodyFont = CreateFont(RegularTypeface, 16.0f);
- SectionFont = CreateFont(MediumTypeface, 22.0f);
- TitleFont = CreateFont(MediumTypeface, 34.0f);
- }
-
- public SKTypeface RegularTypeface { get; }
-
- public SKTypeface MediumTypeface { get; }
-
- public SKFont CaptionFont { get; }
-
- public SKFont NavigationFont { get; }
-
- public SKFont BodyFont { get; }
-
- public SKFont SectionFont { get; }
-
- public SKFont TitleFont { get; }
-
- public void Dispose()
- {
- TitleFont.Dispose();
- SectionFont.Dispose();
- BodyFont.Dispose();
- NavigationFont.Dispose();
- CaptionFont.Dispose();
- MediumTypeface.Dispose();
- RegularTypeface.Dispose();
- }
-
- public static SKTextBlob CreateText(string text, SKFont font)
- {
- return SKTextBlob.Create(text, font, default)!;
- }
-
- private static SKFont CreateFont(SKTypeface typeface, float size)
- {
- return new(typeface, size)
- {
- Edging = SKFontEdging.SubpixelAntialias,
- Hinting = SKFontHinting.Slight,
- Subpixel = true
- };
- }
-}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/GalleryScene.cs b/sources/Experiments/SkiaGallery/GalleryScene.cs
deleted file mode 100644
index 045b493b..00000000
--- a/sources/Experiments/SkiaGallery/GalleryScene.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using SkiaSharp;
-
-namespace SkiaGallery;
-
-internal abstract class GalleryScene(GalleryResources resources) : IDisposable
-{
- private const float MinimumSceneWidth = 500.0f;
- private const float MinimumWideSceneHeight = 280.0f;
- private const float MinimumStackedSceneHeight = 420.0f;
- private const float WideLayoutMinimumWidth = 620.0f;
- private const float WideLayoutAspectRatio = 1.35f;
-
- private SKPicture? staticPicture;
- private float layoutWidth;
- private float layoutHeight;
-
- protected GalleryResources Resources { get; } = resources;
-
- public abstract string Navigation { get; }
-
- public abstract string Title { get; }
-
- public abstract string Description { get; }
-
- public virtual bool IsAnimated => false;
-
- public virtual bool CanRender(float width, float height)
- {
- return width >= MinimumSceneWidth && height >= (UseWideLayout(width, height) ? MinimumWideSceneHeight : MinimumStackedSceneHeight);
- }
-
- public void Draw(SKCanvas canvas, float width, float height, double seconds)
- {
- EnsureLayout(width, height);
- canvas.DrawPicture(staticPicture!);
- DrawDynamic(canvas, width, height, seconds);
- }
-
- public void Dispose()
- {
- staticPicture?.Dispose();
- DisposeResources();
- }
-
- protected abstract void UpdateLayout(float width, float height);
-
- protected abstract void DrawStatic(SKCanvas canvas, float width, float height);
-
- protected virtual void DrawDynamic(SKCanvas canvas, float width, float height, double seconds)
- {
- }
-
- protected virtual void DisposeResources()
- {
- }
-
- protected static bool UseWideLayout(float width, float height)
- {
- return width >= WideLayoutMinimumWidth && width >= height * WideLayoutAspectRatio;
- }
-
- private void EnsureLayout(float width, float height)
- {
- if (staticPicture is not null && layoutWidth == width && layoutHeight == height)
- {
- return;
- }
-
- staticPicture?.Dispose();
- layoutWidth = width;
- layoutHeight = height;
- UpdateLayout(width, height);
-
- using SKPictureRecorder recorder = new();
- SKCanvas canvas = recorder.BeginRecording(new(0.0f, 0.0f, width, height));
- DrawStatic(canvas, width, height);
-
- staticPicture = recorder.EndRecording();
- }
-}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Program.cs b/sources/Experiments/SkiaGallery/Program.cs
deleted file mode 100644
index 55224435..00000000
--- a/sources/Experiments/SkiaGallery/Program.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-using SkiaGallery;
-
-App.Run();
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs b/sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs
deleted file mode 100644
index 89d3a472..00000000
--- a/sources/Experiments/SkiaGallery/Scenes/GeometryScene.cs
+++ /dev/null
@@ -1,310 +0,0 @@
-using SkiaSharp;
-
-namespace SkiaGallery.Scenes;
-
-internal class GeometryScene : GalleryScene
-{
- private static readonly SKColor[] DotColors =
- [
- new(106, 190, 226, 210),
- new(242, 130, 117, 210),
- new(101, 211, 163, 210),
- new(242, 190, 92, 210)
- ];
-
- private readonly SKPath starPath;
- private readonly SKPoint[] wavePoints = new SKPoint[96];
- private readonly SKShader shapeShader;
- private readonly SKPathEffect dashEffect;
- private readonly SKPaint shapePaint = new() { IsAntialias = true, Style = SKPaintStyle.Fill };
- private readonly SKPaint outlinePaint = new() { Color = new(246, 250, 248, 225), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 2.0f };
- private readonly SKPaint ghostPaint = new() { Color = new(157, 218, 222, 100), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 1.25f };
- private readonly SKPaint dashPaint = new() { Color = new(248, 199, 101, 220), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 2.5f, StrokeCap = SKStrokeCap.Round };
- private readonly SKPaint dotPaint = new() { IsAntialias = true, Style = SKPaintStyle.Fill };
- private readonly SKPaint markerPaint = new() { Color = new(255, 247, 225), IsAntialias = true, Style = SKPaintStyle.Fill };
- private readonly SKPaint markerRingPaint = new() { Color = new(242, 130, 117), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 3.0f };
-
- private SKPath? clipPath;
- private SKRect stage;
- private SKRect clipRect;
- private SKRect waveRect;
- private SKPoint starCenter;
- private SKPoint bezierStart;
- private SKPoint firstControl;
- private SKPoint secondControl;
- private SKPoint bezierEnd;
- private float starScale;
-
- public GeometryScene(GalleryResources resources) : base(resources)
- {
- starPath = CreateStar(72.0f, 31.0f, 7);
- shapeShader = SKShader.CreateLinearGradient(new(-78.0f, -72.0f), new(82.0f, 76.0f), [new(242, 130, 117), new(242, 190, 92), new(101, 211, 163)], [0.0f, 0.5f, 1.0f], SKShaderTileMode.Clamp);
- shapePaint.Shader = shapeShader;
-
- dashEffect = SKPathEffect.CreateDash([10.0f, 8.0f], 0.0f);
- dashPaint.PathEffect = dashEffect;
- }
-
- public override string Navigation => "Geometry";
-
- public override string Title => "Geometry blueprint";
-
- public override string Description => "Transforms, curves, clipping, and stroke phase on one construction surface.";
-
- public override bool IsAnimated => true;
-
- protected override void UpdateLayout(float width, float height)
- {
- stage = new(0.0f, 0.0f, width, height);
-
- if (UseWideLayout(width, height))
- {
- starCenter = new(width * 0.31f, height * 0.43f);
- starScale = Math.Clamp(MathF.Min(width * 0.17f, height * 0.25f) / 72.0f, 0.8f, 1.7f);
- clipRect = new(width * 0.66f, height * 0.18f, width * 0.93f, height * 0.61f);
- bezierStart = new(width * 0.08f, height * 0.81f);
- firstControl = new(width * 0.33f, height * 0.57f);
- secondControl = new(width * 0.64f, height * 0.94f);
- bezierEnd = new(width * 0.92f, height * 0.72f);
- waveRect = new(width * 0.09f, height * 0.90f, width * 0.91f, height * 0.97f);
- }
- else
- {
- starCenter = new(width * 0.50f, height * 0.24f);
- starScale = Math.Clamp(MathF.Min(width * 0.24f, height * 0.13f) / 72.0f, 0.72f, 1.3f);
- clipRect = new(width * 0.14f, height * 0.48f, width * 0.86f, height * 0.71f);
- bezierStart = new(width * 0.08f, height * 0.86f);
- firstControl = new(width * 0.27f, height * 0.69f);
- secondControl = new(width * 0.70f, height * 0.96f);
- bezierEnd = new(width * 0.92f, height * 0.78f);
- waveRect = new(width * 0.10f, height * 0.39f, width * 0.90f, height * 0.44f);
- }
-
- clipPath?.Dispose();
- using SKPathBuilder builder = new();
- builder.AddRoundRect(clipRect, 18.0f, 18.0f, SKPathDirection.Clockwise);
- clipPath = builder.Detach();
- }
-
- protected override void DrawDynamic(SKCanvas canvas, float width, float height, double seconds)
- {
- float time = (float)seconds;
-
- DrawTransformedStar(canvas, time, starScale, 16.0f, shapePaint, outlinePaint);
- DrawTransformedStar(canvas, time, starScale * 0.72f, -24.0f, null, outlinePaint);
- DrawTransformedStar(canvas, time, starScale * 0.45f, 36.0f, null, ghostPaint);
-
- for (int i = 0; i < wavePoints.Length; i++)
- {
- float amount = i / (wavePoints.Length - 1.0f);
- float envelope = MathF.Sin(amount * MathF.PI);
- float y = waveRect.MidY + (MathF.Sin((amount * 13.0f) + (time * 1.5f)) * waveRect.Height * 0.42f * envelope);
- wavePoints[i] = new(waveRect.Left + (amount * waveRect.Width), y);
- }
-
- canvas.DrawPoints(SKPointMode.Polygon, wavePoints, dashPaint);
- DrawClipField(canvas, time);
-
- float markerAmount = 0.5f + (MathF.Sin(time * 0.72f) * 0.5f);
- SKPoint marker = CubicPoint(markerAmount, bezierStart, firstControl, secondControl, bezierEnd);
- canvas.DrawCircle(marker, 8.0f, markerPaint);
- canvas.DrawCircle(marker, 8.0f, markerRingPaint);
- }
-
- protected override void DrawStatic(SKCanvas canvas, float width, float height)
- {
- using SKPaint paint = new() { IsAntialias = true };
- using SKShader background = SKShader.CreateLinearGradient(new(stage.Left, stage.Top), new(stage.Right, stage.Bottom), [new(28, 71, 78), new(31, 54, 75), new(72, 53, 66)], [0.0f, 0.58f, 1.0f], SKShaderTileMode.Clamp);
-
- paint.Shader = background;
- canvas.DrawRoundRect(stage, 6.0f, 6.0f, paint);
- paint.Shader = null;
-
- DrawGrid(canvas, paint);
- DrawStarConstruction(canvas, paint);
- DrawBezierConstruction(canvas, paint);
- DrawClipConstruction(canvas, paint);
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = new(198, 228, 226, 120);
- canvas.DrawRoundRect(stage, 6.0f, 6.0f, paint);
- }
-
- protected override void DisposeResources()
- {
- clipPath?.Dispose();
- markerRingPaint.Dispose();
- markerPaint.Dispose();
- dotPaint.Dispose();
- dashPaint.Dispose();
- ghostPaint.Dispose();
- outlinePaint.Dispose();
- shapePaint.Dispose();
- dashEffect.Dispose();
- shapeShader.Dispose();
- starPath.Dispose();
- }
-
- private void DrawTransformedStar(SKCanvas canvas, float time, float scale, float speed, SKPaint? fill, SKPaint outline)
- {
- canvas.Save();
- canvas.Translate(starCenter);
- canvas.RotateDegrees((time * speed) + (speed * 0.7f));
- canvas.Scale(scale * (1.0f + (MathF.Sin((time * 0.8f) + scale) * 0.025f)));
-
- if (fill is not null)
- {
- canvas.DrawPath(starPath, fill);
- }
-
- canvas.DrawPath(starPath, outline);
- canvas.Restore();
- }
-
- private void DrawClipField(SKCanvas canvas, float time)
- {
- canvas.Save();
- canvas.ClipPath(clipPath!, SKClipOperation.Intersect, true);
-
- int columns = Math.Clamp((int)(clipRect.Width / 31.0f), 7, 14);
- int rows = Math.Clamp((int)(clipRect.Height / 27.0f), 4, 10);
- float xStep = clipRect.Width / MathF.Max(1.0f, columns - 1.0f);
- float yStep = clipRect.Height / MathF.Max(1.0f, rows - 1.0f);
-
- for (int row = 0; row < rows; row++)
- {
- for (int column = 0; column < columns; column++)
- {
- float phase = (time * 1.35f) + (row * 0.55f) + (column * 0.34f);
- float x = clipRect.Left + (column * xStep) + (MathF.Cos(phase) * MathF.Min(4.0f, xStep * 0.12f));
- float y = clipRect.Top + (row * yStep) + (MathF.Sin(phase) * MathF.Min(5.0f, yStep * 0.17f));
- dotPaint.Color = DotColors[(row + column) % DotColors.Length];
- canvas.DrawCircle(x, y, 4.2f, dotPaint);
- }
- }
-
- canvas.Restore();
- canvas.DrawPath(clipPath!, outlinePaint);
- }
-
- private void DrawGrid(SKCanvas canvas, SKPaint paint)
- {
- float spacing = Math.Clamp(MathF.Min(stage.Width, stage.Height) / 16.0f, 24.0f, 38.0f);
- int verticalIndex = 0;
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
-
- for (float x = stage.Left; x <= stage.Right; x += spacing)
- {
- paint.Color = verticalIndex % 4 is 0 ? new(190, 226, 225, 40) : new(190, 226, 225, 18);
- canvas.DrawLine(x, stage.Top, x, stage.Bottom, paint);
- verticalIndex++;
- }
-
- int horizontalIndex = 0;
- for (float y = stage.Top; y <= stage.Bottom; y += spacing)
- {
- paint.Color = horizontalIndex % 4 is 0 ? new(190, 226, 225, 40) : new(190, 226, 225, 18);
- canvas.DrawLine(stage.Left, y, stage.Right, y, paint);
- horizontalIndex++;
- }
- }
-
- private void DrawStarConstruction(SKCanvas canvas, SKPaint paint)
- {
- float radius = 72.0f * starScale;
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = new(205, 235, 232, 85);
- canvas.DrawCircle(starCenter, radius * 1.07f, paint);
- canvas.DrawCircle(starCenter, radius * 0.47f, paint);
- canvas.DrawLine(starCenter.X - (radius * 1.18f), starCenter.Y, starCenter.X + (radius * 1.18f), starCenter.Y, paint);
- canvas.DrawLine(starCenter.X, starCenter.Y - (radius * 1.18f), starCenter.X, starCenter.Y + (radius * 1.18f), paint);
-
- paint.Style = SKPaintStyle.Fill;
- paint.Color = new(220, 241, 238, 180);
- canvas.DrawText("TRANSFORM / 7-POINT PATH", starCenter.X - radius, starCenter.Y - (radius * 1.34f), SKTextAlign.Left, Resources.CaptionFont, paint);
- }
-
- private void DrawBezierConstruction(SKCanvas canvas, SKPaint paint)
- {
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = new(202, 232, 229, 105);
- canvas.DrawLine(bezierStart, firstControl, paint);
- canvas.DrawLine(secondControl, bezierEnd, paint);
-
- using SKPathBuilder builder = new();
- builder.MoveTo(bezierStart);
- builder.CubicTo(firstControl, secondControl, bezierEnd);
- using SKPath bezier = builder.Detach();
-
- paint.Color = new(242, 130, 117, 235);
- paint.StrokeWidth = 3.5f;
- paint.StrokeCap = SKStrokeCap.Round;
- canvas.DrawPath(bezier, paint);
-
- SKPoint[] handles = [bezierStart, firstControl, secondControl, bezierEnd];
- paint.Style = SKPaintStyle.Fill;
- for (int i = 0; i < handles.Length; i++)
- {
- paint.Color = i is 0 or 3 ? new(250, 246, 229) : new(106, 190, 226);
- canvas.DrawCircle(handles[i], i is 0 or 3 ? 4.5f : 3.5f, paint);
- }
-
- paint.Color = new(220, 241, 238, 180);
- canvas.DrawText("CUBIC TRAJECTORY", bezierStart.X, bezierStart.Y - 18.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
- }
-
- private void DrawClipConstruction(SKCanvas canvas, SKPaint paint)
- {
- paint.Style = SKPaintStyle.Fill;
- paint.Color = new(232, 246, 243, 20);
- canvas.DrawRoundRect(clipRect, 18.0f, 18.0f, paint);
-
- paint.Color = new(220, 241, 238, 180);
- canvas.DrawText("CLIP WINDOW", clipRect.Left, clipRect.Top - 14.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
-
- paint.Color = new(248, 199, 101, 180);
- canvas.DrawText("STROKE PHASE", waveRect.Left, waveRect.Top - 10.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
- }
-
- private static SKPoint CubicPoint(float amount, SKPoint start, SKPoint first, SKPoint second, SKPoint end)
- {
- float inverse = 1.0f - amount;
- float inverseSquared = inverse * inverse;
- float amountSquared = amount * amount;
-
- return new(
- (inverseSquared * inverse * start.X) + (3.0f * inverseSquared * amount * first.X) + (3.0f * inverse * amountSquared * second.X) + (amountSquared * amount * end.X),
- (inverseSquared * inverse * start.Y) + (3.0f * inverseSquared * amount * first.Y) + (3.0f * inverse * amountSquared * second.Y) + (amountSquared * amount * end.Y));
- }
-
- private static SKPath CreateStar(float outerRadius, float innerRadius, int points)
- {
- using SKPathBuilder builder = new();
-
- for (int i = 0; i < points * 2; i++)
- {
- float angle = (-MathF.PI * 0.5f) + (i * MathF.PI / points);
- float radius = i % 2 is 0 ? outerRadius : innerRadius;
- float x = MathF.Cos(angle) * radius;
- float y = MathF.Sin(angle) * radius;
-
- if (i is 0)
- {
- builder.MoveTo(x, y);
- }
- else
- {
- builder.LineTo(x, y);
- }
- }
-
- builder.Close();
- return builder.Detach();
- }
-}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Scenes/MotionScene.cs b/sources/Experiments/SkiaGallery/Scenes/MotionScene.cs
deleted file mode 100644
index c35b60de..00000000
--- a/sources/Experiments/SkiaGallery/Scenes/MotionScene.cs
+++ /dev/null
@@ -1,181 +0,0 @@
-using SkiaSharp;
-
-namespace SkiaGallery.Scenes;
-
-internal class MotionScene : GalleryScene
-{
- private static readonly SKColor[] ParticleColors = [new(77, 151, 235, 220), new(52, 194, 146, 220), new(245, 112, 103, 220), new(247, 190, 80, 220)];
-
- private readonly Particle[] particles = new Particle[48];
- private readonly SKPoint[] firstRibbonPoints = new SKPoint[96];
- private readonly SKPoint[] secondRibbonPoints = new SKPoint[96];
- private readonly SKPoint[] thirdRibbonPoints = new SKPoint[96];
- private readonly SKShader firstShader;
- private readonly SKShader secondShader;
- private readonly SKPaint firstRibbonPaint = new() { IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 4.0f, StrokeCap = SKStrokeCap.Round };
- private readonly SKPaint secondRibbonPaint = new() { IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 3.0f, StrokeCap = SKStrokeCap.Round };
- private readonly SKPaint thirdRibbonPaint = new() { Color = new(122, 100, 186), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 2.5f, StrokeCap = SKStrokeCap.Round };
- private readonly SKPaint ribbonGlowPaint = new() { IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 12.0f, StrokeCap = SKStrokeCap.Round };
- private readonly SKPaint orbitPaint = new() { Color = new(238, 242, 239, 38), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 1.0f };
- private readonly SKPaint particlePaint = new() { IsAntialias = true, Style = SKPaintStyle.Fill };
- private readonly SKPaint corePaint = new() { Color = new(244, 246, 240), IsAntialias = true, Style = SKPaintStyle.Fill };
-
- private SKRect arenaRect;
- private SKRect waveRect;
- private SKPoint orbitCenter;
- private float orbitRadius;
-
- public MotionScene(GalleryResources resources) : base(resources)
- {
- for (int i = 0; i < particles.Length; i++)
- {
- particles[i] = new(
- 0.38f + ((i % 6) * 0.105f),
- 0.16f + ((i % 9) * 0.028f),
- i * 0.73f,
- 1.5f + ((i % 3) * 0.75f),
- i % ParticleColors.Length);
- }
-
- firstShader = SKShader.CreateLinearGradient(new(0.0f, 0.0f), new(1400.0f, 0.0f), [GalleryPalette.Blue, GalleryPalette.Accent, GalleryPalette.Amber], SKShaderTileMode.Clamp);
- secondShader = SKShader.CreateLinearGradient(new(0.0f, 0.0f), new(1400.0f, 0.0f), [GalleryPalette.Coral, GalleryPalette.Amber, GalleryPalette.Blue], SKShaderTileMode.Clamp);
- firstRibbonPaint.Shader = firstShader;
- secondRibbonPaint.Shader = secondShader;
- }
-
- public override string Navigation => "Motion";
-
- public override string Title => "Kinetic field";
-
- public override string Description => "Polylines and a fixed particle pool moving across one GPU field.";
-
- public override bool IsAnimated => true;
-
- protected override void UpdateLayout(float width, float height)
- {
- arenaRect = new(0.0f, 0.0f, width, height);
- waveRect = new(-24.0f, 0.0f, width + 24.0f, height);
-
- if (UseWideLayout(width, height))
- {
- orbitCenter = new(width * 0.73f, height * 0.52f);
- orbitRadius = MathF.Min(width * 0.24f, height * 0.38f);
- }
- else
- {
- orbitCenter = new(width * 0.57f, height * 0.63f);
- orbitRadius = MathF.Min(width * 0.35f, height * 0.22f);
- }
- }
-
- protected override void DrawDynamic(SKCanvas canvas, float width, float height, double seconds)
- {
- float time = (float)seconds;
- bool wide = UseWideLayout(width, height);
- float firstCenter = height * (wide ? 0.28f : 0.22f);
- float secondCenter = height * (wide ? 0.52f : 0.43f);
- float thirdCenter = height * (wide ? 0.76f : 0.78f);
-
- UpdateRibbon(firstRibbonPoints, waveRect.Left, waveRect.Right, firstCenter, height * 0.075f, time * 1.4f, 13.0f);
- UpdateRibbon(secondRibbonPoints, waveRect.Left, waveRect.Right, secondCenter, height * 0.062f, (-time * 1.1f) + 2.0f, 17.0f);
- UpdateRibbon(thirdRibbonPoints, waveRect.Left, waveRect.Right, thirdCenter, height * 0.052f, (time * 0.8f) + 4.0f, 9.0f);
-
- canvas.Save();
- canvas.ClipRect(arenaRect);
-
- ribbonGlowPaint.Color = new(61, 126, 204, 32);
- canvas.DrawPoints(SKPointMode.Polygon, firstRibbonPoints, ribbonGlowPaint);
- ribbonGlowPaint.Color = new(229, 100, 90, 30);
- canvas.DrawPoints(SKPointMode.Polygon, secondRibbonPoints, ribbonGlowPaint);
- ribbonGlowPaint.Color = new(122, 100, 186, 28);
- canvas.DrawPoints(SKPointMode.Polygon, thirdRibbonPoints, ribbonGlowPaint);
-
- canvas.DrawPoints(SKPointMode.Polygon, firstRibbonPoints, firstRibbonPaint);
- canvas.DrawPoints(SKPointMode.Polygon, secondRibbonPoints, secondRibbonPaint);
- canvas.DrawPoints(SKPointMode.Polygon, thirdRibbonPoints, thirdRibbonPaint);
-
- canvas.Translate(orbitCenter);
-
- for (int i = 0; i < particles.Length; i++)
- {
- Particle particle = particles[i];
- float angle = particle.Phase + (time * particle.Speed);
- float radius = (particle.OrbitFactor * orbitRadius) + (MathF.Sin((time * 0.8f) + particle.Phase) * MathF.Min(5.0f, orbitRadius * 0.035f));
- float x = MathF.Cos(angle) * radius;
- float y = MathF.Sin(angle) * radius * 0.74f;
-
- particlePaint.Color = ParticleColors[particle.ColorIndex];
- canvas.DrawCircle(x, y, particle.Radius, particlePaint);
- }
-
- particlePaint.Color = new(244, 246, 240, 35);
- canvas.DrawCircle(0.0f, 0.0f, MathF.Max(17.0f, orbitRadius * 0.15f), particlePaint);
- canvas.DrawCircle(0.0f, 0.0f, MathF.Max(7.0f, orbitRadius * 0.055f) + (MathF.Sin(time * 2.0f) * 1.5f), corePaint);
- canvas.Restore();
- }
-
- protected override void DrawStatic(SKCanvas canvas, float width, float height)
- {
- using SKPaint paint = new() { IsAntialias = true };
- using SKShader background = SKShader.CreateLinearGradient(
- new(arenaRect.Left, arenaRect.Top),
- new(arenaRect.Right, arenaRect.Bottom),
- [new(13, 28, 25), new(28, 39, 48), new(40, 27, 39)],
- [0.0f, 0.58f, 1.0f],
- SKShaderTileMode.Clamp);
-
- paint.Shader = background;
- canvas.DrawRoundRect(arenaRect, 6.0f, 6.0f, paint);
- paint.Shader = null;
-
- paint.Color = new(238, 242, 239, 22);
- float stepX = width / 14.0f;
- float stepY = height / 9.0f;
- for (int row = 1; row < 9; row++)
- {
- for (int column = 1; column < 14; column++)
- {
- canvas.DrawCircle(column * stepX, row * stepY, 1.0f, paint);
- }
- }
-
- canvas.Save();
- canvas.Translate(orbitCenter);
- canvas.Scale(1.0f, 0.74f);
- for (int orbit = 1; orbit <= 6; orbit++)
- {
- canvas.DrawCircle(0.0f, 0.0f, orbitRadius * orbit / 6.0f, orbitPaint);
- }
- canvas.Restore();
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = new(238, 242, 239, 50);
- canvas.DrawRoundRect(arenaRect, 6.0f, 6.0f, paint);
- }
-
- protected override void DisposeResources()
- {
- corePaint.Dispose();
- particlePaint.Dispose();
- orbitPaint.Dispose();
- ribbonGlowPaint.Dispose();
- thirdRibbonPaint.Dispose();
- secondRibbonPaint.Dispose();
- firstRibbonPaint.Dispose();
- secondShader.Dispose();
- firstShader.Dispose();
- }
-
- private static void UpdateRibbon(SKPoint[] points, float left, float right, float centerY, float amplitude, float phase, float frequency)
- {
- for (int i = 0; i < points.Length; i++)
- {
- float amount = i / (points.Length - 1.0f);
- float y = centerY + (MathF.Sin((amount * frequency) + phase) * amplitude) + (MathF.Sin((amount * frequency * 2.3f) - (phase * 0.37f)) * amplitude * 0.18f);
- points[i] = new(left + (amount * (right - left)), y);
- }
- }
-
- private readonly record struct Particle(float OrbitFactor, float Speed, float Phase, float Radius, int ColorIndex);
-}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs b/sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs
deleted file mode 100644
index 740d972d..00000000
--- a/sources/Experiments/SkiaGallery/Scenes/OverviewScene.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-using SkiaSharp;
-
-namespace SkiaGallery.Scenes;
-
-internal class OverviewScene(GalleryResources resources) : GalleryScene(resources)
-{
- private SKRect stage;
-
- public override string Navigation => "Overview";
-
- public override string Title => "Chromatic assembly";
-
- public override string Description => "Hard-edged geometry composed from color, scale, and overlap.";
-
- protected override void UpdateLayout(float width, float height)
- {
- stage = new(0.0f, 0.0f, width, height);
- }
-
- protected override void DrawStatic(SKCanvas canvas, float width, float height)
- {
- using SKPaint paint = new() { IsAntialias = true };
-
- paint.Color = new(246, 243, 235);
- canvas.DrawRoundRect(stage, 6.0f, 6.0f, paint);
-
- float margin = Math.Clamp(MathF.Min(width, height) * 0.06f, 18.0f, 38.0f);
- float gap = Math.Clamp(MathF.Min(width, height) * 0.022f, 8.0f, 15.0f);
- SKRect field = new(stage.Left + margin, stage.Top + margin, stage.Right - margin, stage.Bottom - margin);
-
- if (UseWideLayout(width, height))
- {
- DrawWideComposition(canvas, paint, field, gap);
- }
- else
- {
- DrawTallComposition(canvas, paint, field, gap);
- }
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = new(190, 187, 179);
- canvas.DrawRoundRect(stage, 6.0f, 6.0f, paint);
- }
-
- private static void DrawWideComposition(SKCanvas canvas, SKPaint paint, SKRect field, float gap)
- {
- float split = field.Left + (field.Width * 0.57f);
- SKRect dark = new(field.Left, field.Top, split - gap, field.Bottom);
- SKRect right = new(split, field.Top, field.Right, field.Bottom);
- float row = right.Top + (right.Height * 0.48f);
- float column = right.Left + (right.Width * 0.61f);
-
- DrawBlock(canvas, paint, dark, new(24, 39, 34));
- DrawBlock(canvas, paint, new(right.Left, right.Top, right.Right, row - gap), new(60, 116, 199));
- DrawBlock(canvas, paint, new(right.Left, row, column - gap, right.Bottom), new(224, 91, 82));
- DrawBlock(canvas, paint, new(column, row, right.Right, right.Top + (right.Height * 0.73f)), new(235, 174, 61));
- DrawBlock(canvas, paint, new(column, right.Top + (right.Height * 0.73f) + gap, right.Right, right.Bottom), new(35, 145, 108));
-
- DrawDarkFieldDetails(canvas, paint, dark, gap);
- DrawSquareAssembly(canvas, paint, new(dark.Left + (dark.Width * 0.64f), dark.MidY), MathF.Min(dark.Width, dark.Height) * 0.30f);
- }
-
- private static void DrawTallComposition(SKCanvas canvas, SKPaint paint, SKRect field, float gap)
- {
- float split = field.Top + (field.Height * 0.60f);
- SKRect dark = new(field.Left, field.Top, field.Right, split - gap);
- SKRect bottom = new(field.Left, split, field.Right, field.Bottom);
- float firstColumn = bottom.Left + (bottom.Width * 0.48f);
- float secondColumn = bottom.Left + (bottom.Width * 0.74f);
-
- DrawBlock(canvas, paint, dark, new(24, 39, 34));
- DrawBlock(canvas, paint, new(bottom.Left, bottom.Top, firstColumn - gap, bottom.Bottom), new(60, 116, 199));
- DrawBlock(canvas, paint, new(firstColumn, bottom.Top, secondColumn - gap, bottom.Bottom), new(224, 91, 82));
- DrawBlock(canvas, paint, new(secondColumn, bottom.Top, bottom.Right, bottom.MidY - (gap * 0.5f)), new(235, 174, 61));
- DrawBlock(canvas, paint, new(secondColumn, bottom.MidY + (gap * 0.5f), bottom.Right, bottom.Bottom), new(35, 145, 108));
-
- DrawDarkFieldDetails(canvas, paint, dark, gap);
- DrawSquareAssembly(canvas, paint, new(dark.MidX, dark.Top + (dark.Height * 0.48f)), MathF.Min(dark.Width, dark.Height) * 0.28f);
- }
-
- private static void DrawDarkFieldDetails(SKCanvas canvas, SKPaint paint, SKRect rect, float gap)
- {
- float barWidth = MathF.Max(3.0f, rect.Width * 0.012f);
- float startX = rect.Left + (gap * 1.6f);
- float bottom = rect.Bottom - (gap * 1.6f);
-
- for (int i = 0; i < 7; i++)
- {
- float height = rect.Height * (0.12f + (i * 0.045f));
- paint.Color = i % 3 is 0 ? new(235, 174, 61) : i % 3 is 1 ? new(224, 91, 82) : new(35, 145, 108);
- canvas.DrawRect(new(startX + (i * barWidth * 1.9f), bottom - height, startX + (i * barWidth * 1.9f) + barWidth, bottom), paint);
- }
-
- paint.Color = new(244, 240, 229, 45);
- canvas.DrawRect(new(rect.Left + (rect.Width * 0.08f), rect.Top + (rect.Height * 0.12f), rect.Left + (rect.Width * 0.36f), rect.Top + (rect.Height * 0.15f)), paint);
- canvas.DrawRect(new(rect.Left + (rect.Width * 0.08f), rect.Top + (rect.Height * 0.19f), rect.Left + (rect.Width * 0.27f), rect.Top + (rect.Height * 0.22f)), paint);
- }
-
- private static void DrawSquareAssembly(SKCanvas canvas, SKPaint paint, SKPoint center, float size)
- {
- canvas.Save();
- canvas.RotateDegrees(-9.0f, center.X, center.Y);
- paint.Color = new(246, 243, 235);
- canvas.DrawRect(new(center.X - size, center.Y - size, center.X + size, center.Y + size), paint);
- canvas.Restore();
-
- canvas.Save();
- canvas.RotateDegrees(11.0f, center.X, center.Y);
- paint.Color = new(60, 116, 199, 210);
- float inset = size * 0.38f;
- canvas.DrawRect(new(center.X - size + inset, center.Y - size + inset, center.X + size - inset, center.Y + size - inset), paint);
- canvas.Restore();
- }
-
- private static void DrawBlock(SKCanvas canvas, SKPaint paint, SKRect rect, SKColor color)
- {
- paint.Color = color;
- canvas.DrawRect(rect, paint);
- }
-}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Scenes/PaintScene.cs b/sources/Experiments/SkiaGallery/Scenes/PaintScene.cs
deleted file mode 100644
index 8eb79fa1..00000000
--- a/sources/Experiments/SkiaGallery/Scenes/PaintScene.cs
+++ /dev/null
@@ -1,231 +0,0 @@
-using SkiaSharp;
-
-namespace SkiaGallery.Scenes;
-
-internal class PaintScene : GalleryScene
-{
- private readonly SKBitmap bitmap;
- private readonly SKImage image;
-
- private SKRect canvasRect;
- private SKRect spectrumRect;
- private SKRect blendRect;
- private SKRect blurRect;
- private SKRect imageRect;
-
- public PaintScene(GalleryResources resources) : base(resources)
- {
- bitmap = CreateBitmap();
- image = SKImage.FromBitmap(bitmap);
- }
-
- public override string Navigation => "Paint & image";
-
- public override string Title => "Color laboratory";
-
- public override string Description => "Shaders, blend modes, blur, sampling, and color transforms in one study.";
-
- protected override void UpdateLayout(float width, float height)
- {
- canvasRect = new(0.0f, 0.0f, width, height);
-
- if (UseWideLayout(width, height))
- {
- float split = width * 0.63f;
- spectrumRect = new(0.0f, 0.0f, split, height * 0.58f);
- blendRect = new(split, 0.0f, width, height * 0.58f);
- blurRect = new(0.0f, height * 0.58f, width * 0.38f, height);
- imageRect = new(width * 0.38f, height * 0.58f, width, height);
- }
- else
- {
- spectrumRect = new(0.0f, 0.0f, width, height * 0.38f);
- blendRect = new(0.0f, height * 0.38f, width * 0.48f, height * 0.68f);
- blurRect = new(width * 0.48f, height * 0.38f, width, height * 0.68f);
- imageRect = new(0.0f, height * 0.68f, width, height);
- }
- }
-
- protected override void DrawStatic(SKCanvas canvas, float width, float height)
- {
- using SKPaint paint = new() { IsAntialias = true };
- using SKPaint imagePaint = new() { IsAntialias = true };
- using SKPaint filteredPaint = new() { IsAntialias = true };
- using SKMaskFilter blur = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 16.0f);
- using SKColorFilter colorFilter = SKColorFilter.CreateColorMatrix(
- [
- 0.72f, 0.12f, 0.16f, 0.0f, 18.0f / 255.0f,
- 0.05f, 0.82f, 0.13f, 0.0f, 4.0f / 255.0f,
- 0.14f, 0.16f, 0.70f, 0.0f, 12.0f / 255.0f,
- 0.0f, 0.0f, 0.0f, 1.0f, 0.0f
- ]);
-
- filteredPaint.ColorFilter = colorFilter;
- SKSamplingOptions sampling = new(SKCubicResampler.Mitchell);
-
- paint.Color = new(246, 245, 241);
- canvas.DrawRoundRect(canvasRect, 5.0f, 5.0f, paint);
-
- DrawSpectrum(canvas, paint);
- DrawBlendStudy(canvas, paint);
- DrawBlurStudy(canvas, paint, blur);
- DrawImageStudy(canvas, paint, imagePaint, filteredPaint, sampling);
- DrawDividers(canvas, paint);
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = new(208, 207, 201);
- canvas.DrawRoundRect(canvasRect, 5.0f, 5.0f, paint);
- }
-
- protected override void DisposeResources()
- {
- image.Dispose();
- bitmap.Dispose();
- }
-
- private void DrawSpectrum(SKCanvas canvas, SKPaint paint)
- {
- SKRect stage = Inset(spectrumRect, 28.0f, 54.0f, 28.0f, 28.0f);
- using SKShader baseGradient = SKShader.CreateLinearGradient(
- new(stage.Left, stage.Top),
- new(stage.Right, stage.Top),
- [new(43, 90, 176), new(47, 168, 154), new(239, 190, 73), new(226, 91, 94), new(119, 77, 165)],
- [0.0f, 0.26f, 0.52f, 0.76f, 1.0f],
- SKShaderTileMode.Clamp);
- using SKShader lightGradient = SKShader.CreateLinearGradient(
- new(stage.Left, stage.Top),
- new(stage.Left, stage.Bottom),
- [new(255, 255, 255, 18), new(255, 255, 255, 185), new(23, 31, 29, 80)],
- [0.0f, 0.56f, 1.0f],
- SKShaderTileMode.Clamp);
-
- paint.Shader = baseGradient;
- canvas.DrawRect(stage, paint);
- paint.Shader = lightGradient;
- paint.BlendMode = SKBlendMode.Screen;
- canvas.DrawRect(stage, paint);
- paint.BlendMode = SKBlendMode.SrcOver;
- paint.Shader = null;
-
- paint.Color = GalleryPalette.Ink;
- canvas.DrawText("SPECTRUM", spectrumRect.Left + 28.0f, spectrumRect.Top + 28.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
- paint.Color = GalleryPalette.Muted;
- canvas.DrawText("LINEAR SHADER / FIVE STOPS", spectrumRect.Right - 28.0f, spectrumRect.Top + 28.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
-
- paint.Color = new(255, 255, 255, 125);
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- for (int i = 1; i < 5; i++)
- {
- float x = stage.Left + (stage.Width * i / 5.0f);
- canvas.DrawLine(x, stage.Top, x, stage.Bottom, paint);
- }
-
- paint.Style = SKPaintStyle.Fill;
- }
-
- private void DrawBlendStudy(SKCanvas canvas, SKPaint paint)
- {
- SKRect stage = Inset(blendRect, 24.0f, 54.0f, 24.0f, 24.0f);
- float radius = MathF.Min(stage.Width, stage.Height) * 0.28f;
- SKPoint center = new(stage.MidX, stage.MidY + 8.0f);
-
- paint.Color = GalleryPalette.Ink;
- canvas.DrawText("BLEND", blendRect.Left + 24.0f, blendRect.Top + 28.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
-
- paint.Color = new(229, 100, 90, 175);
- canvas.DrawCircle(center.X - (radius * 0.42f), center.Y, radius, paint);
- paint.Color = new(61, 126, 204, 175);
- paint.BlendMode = SKBlendMode.Plus;
- canvas.DrawCircle(center.X + (radius * 0.42f), center.Y, radius, paint);
- paint.BlendMode = SKBlendMode.SrcOver;
-
- paint.Color = GalleryPalette.Muted;
- canvas.DrawText("PLUS", blendRect.Right - 24.0f, blendRect.Bottom - 20.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
- }
-
- private void DrawBlurStudy(SKCanvas canvas, SKPaint paint, SKMaskFilter blur)
- {
- SKRect stage = Inset(blurRect, 24.0f, 52.0f, 24.0f, 24.0f);
- float radius = MathF.Min(stage.Width, stage.Height) * 0.22f;
-
- paint.Color = GalleryPalette.Ink;
- canvas.DrawText("BLUR FIELD", blurRect.Left + 24.0f, blurRect.Top + 28.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
-
- paint.MaskFilter = blur;
- paint.Color = new(37, 153, 116, 128);
- canvas.DrawCircle(stage.MidX - (radius * 0.55f), stage.MidY, radius, paint);
- paint.Color = new(231, 168, 66, 128);
- canvas.DrawCircle(stage.MidX + (radius * 0.55f), stage.MidY, radius, paint);
- paint.MaskFilter = null;
-
- paint.Color = GalleryPalette.Ink;
- canvas.DrawCircle(stage.MidX, stage.MidY, 4.0f, paint);
- }
-
- private void DrawImageStudy(SKCanvas canvas, SKPaint paint, SKPaint imagePaint, SKPaint filteredPaint, SKSamplingOptions sampling)
- {
- SKRect stage = Inset(imageRect, 24.0f, 52.0f, 24.0f, 24.0f);
- SKRect left = new(stage.Left, stage.Top, stage.MidX - 4.0f, stage.Bottom);
- SKRect right = new(stage.MidX + 4.0f, stage.Top, stage.Right, stage.Bottom);
- SKRect source = new(0.0f, 0.0f, bitmap.Width, bitmap.Height);
-
- paint.Color = GalleryPalette.Ink;
- canvas.DrawText("IMAGE TRANSFORM", imageRect.Left + 24.0f, imageRect.Top + 28.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
- paint.Color = GalleryPalette.Muted;
- canvas.DrawText("ORIGINAL / 4 × 5 MATRIX", imageRect.Right - 24.0f, imageRect.Top + 28.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
-
- canvas.DrawImage(image, source, left, sampling, imagePaint);
- canvas.DrawImage(image, source, right, sampling, filteredPaint);
-
- paint.Color = new(255, 255, 255, 180);
- canvas.DrawRect(new(stage.MidX - 1.0f, stage.Top, stage.MidX + 1.0f, stage.Bottom), paint);
- }
-
- private void DrawDividers(SKCanvas canvas, SKPaint paint)
- {
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = new(208, 207, 201);
-
- if (spectrumRect.Right < canvasRect.Right)
- {
- canvas.DrawLine(spectrumRect.Right, canvasRect.Top, spectrumRect.Right, spectrumRect.Bottom, paint);
- }
-
- canvas.DrawLine(blurRect.Left, blurRect.Top, canvasRect.Right, blurRect.Top, paint);
-
- if (blurRect.Right < canvasRect.Right)
- {
- canvas.DrawLine(blurRect.Right, blurRect.Top, blurRect.Right, canvasRect.Bottom, paint);
- }
-
- paint.Style = SKPaintStyle.Fill;
- }
-
- private static SKRect Inset(SKRect rect, float left, float top, float right, float bottom)
- {
- return new(rect.Left + left, rect.Top + top, rect.Right - right, rect.Bottom - bottom);
- }
-
- private static SKBitmap CreateBitmap()
- {
- SKBitmap result = new(240, 160, SKColorType.Bgra8888, SKAlphaType.Premul);
-
- for (int y = 0; y < result.Height; y++)
- {
- for (int x = 0; x < result.Width; x++)
- {
- float horizontal = x / (result.Width - 1.0f);
- float vertical = y / (result.Height - 1.0f);
- byte red = (byte)(42.0f + (horizontal * 185.0f));
- byte green = (byte)(74.0f + ((1.0f - vertical) * 126.0f));
- byte blue = (byte)(108.0f + (MathF.Sin((horizontal + vertical) * 6.0f) * 52.0f));
- result.SetPixel(x, y, new(red, green, blue));
- }
- }
-
- return result;
- }
-}
\ No newline at end of file
diff --git a/sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs b/sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs
deleted file mode 100644
index 3cb05002..00000000
--- a/sources/Experiments/SkiaGallery/Scenes/TypographyScene.cs
+++ /dev/null
@@ -1,260 +0,0 @@
-using SkiaSharp;
-
-namespace SkiaGallery.Scenes;
-
-internal class TypographyScene(GalleryResources resources) : GalleryScene(resources)
-{
- private const float MaximumHeroSize = 94.0f;
-
- private static readonly string[] ScaleLabels = ["Display", "Title", "Section", "Body", "Caption"];
- private static readonly float[] ScaleFontSizes = [58.0f, 34.0f, 22.0f, 16.0f, 12.0f];
-
- private SKRect page;
- private SKRect masthead;
- private SKRect scaleColumn;
- private SKRect specimenArea;
- private SKRect pathArea;
-
- public override string Navigation => "Typography";
-
- public override string Title => "Editorial typography";
-
- public override string Description => "Hierarchy, metrics, path layout, and painted glyphs on one specimen page.";
-
- public override bool CanRender(float width, float height)
- {
- return base.CanRender(width, height) && (!UseWideLayout(width, height) || height >= 330.0f);
- }
-
- protected override void UpdateLayout(float width, float height)
- {
- page = new(0.0f, 0.0f, width, height);
-
- if (UseWideLayout(width, height))
- {
- float margin = Math.Clamp(width * 0.045f, 34.0f, 52.0f);
- float mastheadBottom = height * 0.46f;
- float columnWidth = width * 0.27f;
-
- masthead = new(margin, margin, width - margin, mastheadBottom);
- scaleColumn = new(margin, mastheadBottom + 22.0f, margin + columnWidth, height - margin);
- specimenArea = new(scaleColumn.Right + 34.0f, mastheadBottom + 22.0f, width - margin, height - margin);
- pathArea = new(specimenArea.Left, specimenArea.Top + (specimenArea.Height * 0.52f), specimenArea.Right, specimenArea.Bottom);
- }
- else
- {
- float margin = Math.Clamp(width * 0.06f, 24.0f, 34.0f);
- float mastheadBottom = height * 0.35f;
- float bodyTop = mastheadBottom + 18.0f;
- float scaleWidth = width * 0.36f;
-
- masthead = new(margin, margin, width - margin, mastheadBottom);
- scaleColumn = new(margin, bodyTop, margin + scaleWidth, height - margin);
- specimenArea = new(scaleColumn.Right + 22.0f, bodyTop, width - margin, height - margin);
- pathArea = new(specimenArea.Left, specimenArea.Top + (specimenArea.Height * 0.60f), specimenArea.Right, specimenArea.Bottom);
- }
- }
-
- protected override void DrawStatic(SKCanvas canvas, float width, float height)
- {
- using SKPaint paint = new() { IsAntialias = true };
- using SKPaint outline = new() { Color = GalleryPalette.Ink, IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 1.4f };
- using SKMaskFilter blur = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4.5f);
- using SKPaint shadow = new() { Color = new(25, 36, 32, 38), IsAntialias = true, MaskFilter = blur };
- using SKShader headlineShader = SKShader.CreateLinearGradient(new(masthead.Left, masthead.Top), new(masthead.Right, masthead.Bottom), [GalleryPalette.Blue, GalleryPalette.Accent, GalleryPalette.Coral, GalleryPalette.Amber], [0.0f, 0.38f, 0.72f, 1.0f], SKShaderTileMode.Clamp);
-
- paint.Color = new(251, 250, 247);
- canvas.DrawRoundRect(page, 4.0f, 4.0f, paint);
-
- DrawEditorialRules(canvas, paint);
- DrawMasthead(canvas, paint, headlineShader);
- DrawScaleColumn(canvas, paint);
- DrawSpecimen(canvas, paint, outline, shadow, headlineShader);
- DrawPathText(canvas, paint);
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = new(214, 211, 203);
- canvas.DrawRoundRect(page, 4.0f, 4.0f, paint);
- }
-
- private void DrawEditorialRules(SKCanvas canvas, SKPaint paint)
- {
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = new(214, 211, 203);
- canvas.DrawLine(masthead.Left, masthead.Top, masthead.Right, masthead.Top, paint);
- canvas.DrawLine(masthead.Left, masthead.Bottom, masthead.Right, masthead.Bottom, paint);
- canvas.DrawLine(scaleColumn.Right + 16.0f, scaleColumn.Top, scaleColumn.Right + 16.0f, scaleColumn.Bottom, paint);
-
- paint.Style = SKPaintStyle.Fill;
- paint.Color = GalleryPalette.Coral;
- canvas.DrawRect(masthead.Left, masthead.Top - 2.0f, MathF.Min(112.0f, masthead.Width * 0.18f), 4.0f, paint);
- }
-
- private void DrawMasthead(SKCanvas canvas, SKPaint paint, SKShader shader)
- {
- const string lead = "FORM";
- const string secondLine = "function.";
- const float minimumHeadingSize = 32.0f;
- const float maximumHeadingSize = 108.0f;
-
- float contentTop = masthead.Top + 48.0f;
- bool showNote = masthead.Height >= 230.0f;
- float contentBottom = masthead.Bottom - (showNote ? 34.0f : 14.0f);
- float availableHeight = MathF.Max(1.0f, contentBottom - contentTop);
- float headingSize = Math.Clamp(availableHeight / 2.18f, minimumHeadingSize, maximumHeadingSize);
- using SKFont headingFont = new(Resources.MediumTypeface, headingSize)
- {
- Edging = SKFontEdging.SubpixelAntialias,
- Hinting = SKFontHinting.Slight,
- Subpixel = true
- };
-
- float secondLineX = masthead.Left + (masthead.Width * 0.12f);
- float maximumTextWidth = masthead.Right - secondLineX;
- float secondLineWidth = headingFont.MeasureText(secondLine);
-
- if (secondLineWidth > maximumTextWidth)
- {
- headingSize *= maximumTextWidth / secondLineWidth;
- headingFont.Size = headingSize;
- }
-
- paint.Color = GalleryPalette.Muted;
- canvas.DrawText("SKIA TYPE SPECIMEN / VECTOR EDITION", masthead.Left, masthead.Top + 26.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
-
- SKFontMetrics headingMetrics = headingFont.Metrics;
- float firstBaseline = contentTop - headingMetrics.Ascent;
- float lineGap = Math.Clamp(headingSize * 0.12f, 6.0f, 14.0f);
- float secondBaseline = firstBaseline + headingMetrics.Descent - headingMetrics.Ascent + lineGap;
-
- paint.Shader = shader;
- canvas.DrawText(lead, masthead.Left, firstBaseline, SKTextAlign.Left, headingFont, paint);
- paint.Shader = null;
-
- float followsSize = Math.Clamp(headingSize * 0.42f, 20.0f, Resources.TitleFont.Size);
- using SKFont followsFont = new(Resources.MediumTypeface, followsSize)
- {
- Edging = SKFontEdging.SubpixelAntialias,
- Hinting = SKFontHinting.Slight,
- Subpixel = true
- };
- float followsX = masthead.Left + headingFont.MeasureText(lead) + Math.Clamp(headingSize * 0.16f, 10.0f, 18.0f);
- float followsBaseline = firstBaseline - (headingSize * 0.08f);
-
- paint.Color = GalleryPalette.Ink;
- canvas.DrawText("follows", followsX, followsBaseline, SKTextAlign.Left, followsFont, paint);
- canvas.DrawText(secondLine, secondLineX, secondBaseline, SKTextAlign.Left, headingFont, paint);
-
- if (showNote)
- {
- paint.Color = GalleryPalette.Muted;
- string note = masthead.Width >= 680.0f ? "Scale, rhythm, and contour remain native vector geometry." : "Scale, rhythm, contour.";
- canvas.DrawText(note, masthead.Right, masthead.Bottom - 12.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
- }
- }
-
- private void DrawScaleColumn(SKCanvas canvas, SKPaint paint)
- {
- paint.Color = GalleryPalette.Muted;
- canvas.DrawText("HIERARCHY", scaleColumn.Left, scaleColumn.Top + 17.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
-
- float top = scaleColumn.Top + 42.0f;
- float availableHeight = scaleColumn.Bottom - top;
- float naturalHeight = 0.0f;
-
- for (int i = 0; i < ScaleFontSizes.Length; i++)
- {
- naturalHeight += ScaleFontSizes[i] * 1.12f;
- }
-
- float scale = MathF.Min(1.0f, availableHeight / naturalHeight);
- float baseline = top;
-
- for (int i = 0; i < ScaleLabels.Length; i++)
- {
- float fontSize = MathF.Max(10.0f, ScaleFontSizes[i] * scale);
- using SKFont font = new(i <= 2 ? Resources.MediumTypeface : Resources.RegularTypeface, fontSize)
- {
- Edging = SKFontEdging.SubpixelAntialias,
- Hinting = SKFontHinting.Slight,
- Subpixel = true
- };
- SKFontMetrics metrics = font.Metrics;
- baseline -= metrics.Ascent;
-
- paint.Color = i is 0 ? GalleryPalette.Coral : i is 1 ? GalleryPalette.Ink : GalleryPalette.Muted;
- canvas.DrawText(ScaleLabels[i], scaleColumn.Left, baseline, SKTextAlign.Left, font, paint);
- paint.Color = GalleryPalette.Muted;
- canvas.DrawText(((int)MathF.Round(fontSize)).ToString(), scaleColumn.Right, baseline, SKTextAlign.Right, Resources.CaptionFont, paint);
-
- baseline += metrics.Descent + (fontSize * 0.12f);
- }
- }
-
- private void DrawSpecimen(SKCanvas canvas, SKPaint paint, SKPaint outline, SKPaint shadow, SKShader shader)
- {
- paint.Color = GalleryPalette.Muted;
- canvas.DrawText("METRICS / PAINT", specimenArea.Left, specimenArea.Top + 17.0f, SKTextAlign.Left, Resources.CaptionFont, paint);
-
- float heroSize = Math.Clamp(MathF.Min(specimenArea.Width * 0.24f, specimenArea.Height * 0.42f), 38.0f, MaximumHeroSize);
- using SKFont heroFont = new(Resources.MediumTypeface, heroSize)
- {
- Edging = SKFontEdging.SubpixelAntialias,
- Hinting = SKFontHinting.Slight,
- Subpixel = true
- };
- float baseline = specimenArea.Top + MathF.Min(specimenArea.Height * 0.44f, 118.0f);
- float capLine = baseline - heroFont.Metrics.CapHeight;
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = GalleryPalette.Line;
- canvas.DrawLine(specimenArea.Left, capLine, specimenArea.Right, capLine, paint);
- canvas.DrawLine(specimenArea.Left, baseline, specimenArea.Right, baseline, paint);
-
- paint.Style = SKPaintStyle.Fill;
- paint.Shader = shader;
- canvas.DrawText("Aa", specimenArea.Left, baseline, SKTextAlign.Left, heroFont, paint);
- paint.Shader = null;
-
- float wordX = specimenArea.Left + heroFont.MeasureText("Aa") + Math.Clamp(specimenArea.Width * 0.04f, 12.0f, 22.0f);
- paint.Color = GalleryPalette.Ink;
- canvas.DrawText("VECTOR", wordX, baseline - 15.0f, SKTextAlign.Left, Resources.SectionFont, paint);
- canvas.DrawText("OUTLINE", wordX, baseline + 25.0f, SKTextAlign.Left, Resources.SectionFont, shadow);
- canvas.DrawText("OUTLINE", wordX, baseline + 25.0f, SKTextAlign.Left, Resources.SectionFont, outline);
-
- paint.Color = GalleryPalette.Muted;
- canvas.DrawText("CAP", specimenArea.Right, capLine - 5.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
- canvas.DrawText("BASELINE", specimenArea.Right, baseline - 5.0f, SKTextAlign.Right, Resources.CaptionFont, paint);
- }
-
- private void DrawPathText(SKCanvas canvas, SKPaint paint)
- {
- SKPoint start = new(pathArea.Left, pathArea.Bottom - 18.0f);
- SKPoint end = new(pathArea.Right, pathArea.Top + 36.0f);
- SKPoint first = new(pathArea.Left + (pathArea.Width * 0.32f), pathArea.Top + 8.0f);
- SKPoint second = new(pathArea.Left + (pathArea.Width * 0.70f), pathArea.Bottom - 3.0f);
-
- using SKPathBuilder builder = new();
- builder.MoveTo(start);
- builder.CubicTo(first, second, end);
- using SKPath path = builder.Detach();
-
- paint.Style = SKPaintStyle.Stroke;
- paint.StrokeWidth = 1.0f;
- paint.Color = GalleryPalette.Line;
- canvas.DrawLine(start, first, paint);
- canvas.DrawLine(second, end, paint);
-
- paint.Style = SKPaintStyle.Fill;
- paint.Color = GalleryPalette.Accent;
- string sample = pathArea.Width >= 360.0f ? "TYPE FOLLOWS A REUSABLE CUBIC PATH" : "TYPE FOLLOWS PATH";
- canvas.DrawTextOnPath(sample, path, 6.0f, -8.0f, SKTextAlign.Left, Resources.BodyFont, paint);
-
- paint.Color = GalleryPalette.Blue;
- canvas.DrawCircle(first, 3.5f, paint);
- canvas.DrawCircle(second, 3.5f, paint);
- }
-}
\ No newline at end of file
From 837717cc512fdf8d05fdb90a4d83cc3c423fd213 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 13:53:55 +0800
Subject: [PATCH 20/50] Add missing usings and refactor Board.cs logic
Added required using directives to multiple files. Simplified point addition in Board.cs with the null-conditional operator. Refactored EnsureLayout by defining 'top' and 'bottom' as constants at the method's start.
---
sources/Experiments/InkCanvas/App.cs | 2 +-
sources/Experiments/InkCanvas/Board.cs | 12 ++++++------
sources/Experiments/InkCanvas/Helpers/CocoaHelper.cs | 2 +-
sources/Experiments/InkCanvas/Program.cs | 2 +-
sources/Experiments/InkCanvas/Stroke.cs | 2 +-
5 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/sources/Experiments/InkCanvas/App.cs b/sources/Experiments/InkCanvas/App.cs
index fcd06a71..7d727933 100644
--- a/sources/Experiments/InkCanvas/App.cs
+++ b/sources/Experiments/InkCanvas/App.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using InkCanvas.Helpers;
using Silk.NET.Input;
using Silk.NET.Windowing;
diff --git a/sources/Experiments/InkCanvas/Board.cs b/sources/Experiments/InkCanvas/Board.cs
index 93cfc6e6..a29ec101 100644
--- a/sources/Experiments/InkCanvas/Board.cs
+++ b/sources/Experiments/InkCanvas/Board.cs
@@ -1,4 +1,4 @@
-using SkiaSharp;
+using SkiaSharp;
namespace InkCanvas;
@@ -122,9 +122,9 @@ public void PointerMove(SKPoint position)
Erase(eraserLast, position);
eraserLast = position;
}
- else if (activeStroke is not null)
+ else
{
- activeStroke.Add(position);
+ activeStroke?.Add(position);
}
}
@@ -223,6 +223,9 @@ public void Dispose()
private void EnsureLayout(float width, float height)
{
+ const float top = (ToolbarHeight - SwatchSize) * 0.5f;
+ const float bottom = top + SwatchSize;
+
if (width == layoutWidth && height == layoutHeight)
{
return;
@@ -233,9 +236,6 @@ private void EnsureLayout(float width, float height)
stage = new(0.0f, 0.0f, width, height);
canvasArea = new(0.0f, ToolbarHeight, width, MathF.Max(ToolbarHeight, height - StatusHeight));
- float top = (ToolbarHeight - SwatchSize) * 0.5f;
- float bottom = top + SwatchSize;
-
for (int index = 0; index < swatchRects.Length; index++)
{
float left = SwatchGap + (index * (SwatchSize + SwatchGap));
diff --git a/sources/Experiments/InkCanvas/Helpers/CocoaHelper.cs b/sources/Experiments/InkCanvas/Helpers/CocoaHelper.cs
index 7ef35afb..3bec63f1 100644
--- a/sources/Experiments/InkCanvas/Helpers/CocoaHelper.cs
+++ b/sources/Experiments/InkCanvas/Helpers/CocoaHelper.cs
@@ -1,4 +1,4 @@
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
namespace InkCanvas.Helpers;
diff --git a/sources/Experiments/InkCanvas/Program.cs b/sources/Experiments/InkCanvas/Program.cs
index f7868902..6ef9a23a 100644
--- a/sources/Experiments/InkCanvas/Program.cs
+++ b/sources/Experiments/InkCanvas/Program.cs
@@ -1,3 +1,3 @@
-using InkCanvas;
+using InkCanvas;
App.Run();
\ No newline at end of file
diff --git a/sources/Experiments/InkCanvas/Stroke.cs b/sources/Experiments/InkCanvas/Stroke.cs
index d81d7bfe..78b4c8b5 100644
--- a/sources/Experiments/InkCanvas/Stroke.cs
+++ b/sources/Experiments/InkCanvas/Stroke.cs
@@ -1,4 +1,4 @@
-using SkiaSharp;
+using SkiaSharp;
namespace InkCanvas;
From 3ea8181c9b0e9c8e4009f00381ce4eee21448a90 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 18:11:52 +0800
Subject: [PATCH 21/50] Refine InkCanvas drawing and erasing
---
sources/Experiments/InkCanvas/App.cs | 38 ++-
sources/Experiments/InkCanvas/Board.cs | 320 ++++++++++++------------
sources/Experiments/InkCanvas/Stroke.cs | 167 ++++---------
3 files changed, 224 insertions(+), 301 deletions(-)
diff --git a/sources/Experiments/InkCanvas/App.cs b/sources/Experiments/InkCanvas/App.cs
index 7d727933..4d3e2dbf 100644
--- a/sources/Experiments/InkCanvas/App.cs
+++ b/sources/Experiments/InkCanvas/App.cs
@@ -19,6 +19,7 @@ internal static class App
private static readonly Board board;
private static SKTexture texture;
+ private static Vector2 dpiScale = Vector2.One;
private static float logicalWidth;
private static float logicalHeight;
@@ -95,6 +96,7 @@ public static void Run()
mouse.MouseUp += MouseUp;
window.Render += Render;
+ window.Resize += _ => Resize();
window.Run();
@@ -107,7 +109,7 @@ public static void Run()
Context.Dispose();
}
- private static void Render(double delta)
+ private static void Render(double _)
{
uint width = Width;
uint height = Height;
@@ -117,11 +119,10 @@ private static void Render(double delta)
return;
}
- Vector2 dpiScale = DpiScale;
+ dpiScale = DpiScale;
logicalWidth = width / dpiScale.X;
logicalHeight = height / dpiScale.Y;
- Resize(width, height);
texture.Render(DrawBoard);
CommandBuffer commandBuffer = Context.GraphicsQueue.CommandBuffer();
@@ -144,8 +145,6 @@ private static void Render(double delta)
private static void DrawBoard(SKCanvas canvas)
{
- Vector2 dpiScale = DpiScale;
-
canvas.Save();
canvas.Scale(dpiScale.X, dpiScale.Y);
board.Draw(canvas, logicalWidth, logicalHeight);
@@ -159,13 +158,9 @@ private static void MouseMove(IMouse _, Vector2 position)
private static void MouseDown(IMouse mouse, MouseButton button)
{
- if (button is MouseButton.Left)
- {
- board.PointerDown(new(mouse.Position.X, mouse.Position.Y), erase: false);
- }
- else if (button is MouseButton.Right)
+ if (button is MouseButton.Left or MouseButton.Right)
{
- board.PointerDown(new(mouse.Position.X, mouse.Position.Y), erase: true);
+ board.PointerDown(new(mouse.Position.X, mouse.Position.Y), button is MouseButton.Right);
}
}
@@ -173,7 +168,7 @@ private static void MouseUp(IMouse _, MouseButton button)
{
if (button is MouseButton.Left or MouseButton.Right)
{
- board.PointerUp();
+ board.PointerUp(button is MouseButton.Right);
}
}
@@ -188,17 +183,18 @@ private static SKTexture CreateTexture(uint width, uint height)
});
}
- private static void Resize(uint width, uint height)
+ private static void Resize()
{
- if (texture.Desc.Width == width && texture.Desc.Height == height)
- {
- return;
- }
+ uint width = Width;
+ uint height = Height;
- swapChain.Resize(width, height);
+ if (width is not 0 && height is not 0)
+ {
+ SKTexture oldTexture = texture;
- SKTexture oldTexture = texture;
- texture = CreateTexture(width, height);
- oldTexture.Dispose();
+ swapChain.Resize(width, height);
+ texture = CreateTexture(width, height);
+ oldTexture.Dispose();
+ }
}
}
\ No newline at end of file
diff --git a/sources/Experiments/InkCanvas/Board.cs b/sources/Experiments/InkCanvas/Board.cs
index a29ec101..fefce0d0 100644
--- a/sources/Experiments/InkCanvas/Board.cs
+++ b/sources/Experiments/InkCanvas/Board.cs
@@ -35,7 +35,7 @@ internal sealed class Board : IDisposable
private readonly List strokes = [];
private readonly SKRect[] swatchRects = new SKRect[Palette.Length];
private readonly SKRect[] widthRects = new SKRect[Widths.Length];
- private readonly SKTypeface typeface;
+ private readonly SKPathBuilder eraserBuilder = new();
private readonly SKFont labelFont;
private readonly SKPaint fillPaint = new() { IsAntialias = true };
private readonly SKPaint strokePaint = new()
@@ -46,28 +46,28 @@ internal sealed class Board : IDisposable
StrokeJoin = SKStrokeJoin.Round
};
- private SKPicture? bakedStrokes;
+ private SKPicture? bakedCanvas;
private Stroke? activeStroke;
- private SKRect stage;
private SKRect canvasArea;
- private SKRect eraserRect;
private SKRect clearRect;
private SKPoint pointer;
- private SKPoint eraserLast;
private int colorIndex;
private int widthIndex = 1;
- private bool eraserMode;
+ private int nodeCount;
private bool erasing;
- private bool pointerInside;
+ private bool erasePending;
private bool bakeDirty = true;
private float layoutWidth = -1.0f;
private float layoutHeight = -1.0f;
+ private bool CanClear => strokes.Count > 0 || activeStroke is not null || erasing || erasePending;
+
public Board()
{
string family = OperatingSystem.IsMacOS() ? "SF Pro Text" : OperatingSystem.IsWindows() ? "Segoe UI" : "Noto Sans";
- typeface = SKTypeface.FromFamilyName(family, SKFontStyle.Normal);
+ using SKTypeface typeface = SKTypeface.FromFamilyName(family, SKFontStyle.Normal);
+
labelFont = new(typeface, 12.0f)
{
Edging = SKFontEdging.SubpixelAntialias,
@@ -80,28 +80,34 @@ public void Draw(SKCanvas canvas, float width, float height)
{
EnsureLayout(width, height);
- fillPaint.Color = Surface;
- canvas.DrawRect(stage, fillPaint);
+ if (erasePending)
+ {
+ ApplyEraser();
+ }
- DrawGrid(canvas);
+ fillPaint.Color = Surface;
+ canvas.DrawRect(0.0f, 0.0f, width, height, fillPaint);
if (bakeDirty)
{
- bakedStrokes?.Dispose();
- bakedStrokes = BakeStrokes();
+ bakedCanvas?.Dispose();
+ bakedCanvas = BakeCanvas();
bakeDirty = false;
}
canvas.Save();
canvas.ClipRect(canvasArea);
- canvas.DrawPicture(bakedStrokes!);
+ canvas.DrawPicture(bakedCanvas!);
if (activeStroke is not null)
{
- DrawStroke(canvas, activeStroke);
+ strokePaint.Color = activeStroke.Color;
+ strokePaint.StrokeWidth = activeStroke.Width;
+ canvas.DrawPath(activeStroke.Path, strokePaint);
+ canvas.DrawLine(activeStroke.TailStart, activeStroke.TailEnd, strokePaint);
}
- if ((eraserMode || erasing) && pointerInside)
+ if (erasing && canvasArea.Contains(pointer.X, pointer.Y))
{
DrawEraserCursor(canvas);
}
@@ -115,12 +121,11 @@ public void Draw(SKCanvas canvas, float width, float height)
public void PointerMove(SKPoint position)
{
pointer = position;
- pointerInside = canvasArea.Contains(position.X, position.Y);
if (erasing)
{
- Erase(eraserLast, position);
- eraserLast = position;
+ eraserBuilder.LineTo(position);
+ erasePending = true;
}
else
{
@@ -130,79 +135,58 @@ public void PointerMove(SKPoint position)
public void PointerDown(SKPoint position, bool erase)
{
- pointer = position;
-
- if (position.Y <= ToolbarHeight)
+ if (!erase && clearRect.Contains(position.X, position.Y))
{
- HandleToolbarClick(position);
-
- return;
- }
-
- if (!canvasArea.Contains(position.X, position.Y))
- {
- return;
+ pointer = position;
+ ClearCanvas();
}
-
- pointerInside = true;
-
- if (erase || eraserMode)
+ else if (activeStroke is null && !erasing)
{
- erasing = true;
- eraserLast = position;
- Erase(position, position);
+ pointer = position;
- return;
+ if (!erase && position.Y <= ToolbarHeight)
+ {
+ HandleToolbarClick(position);
+ }
+ else if (canvasArea.Contains(position.X, position.Y))
+ {
+ if (erase)
+ {
+ erasing = true;
+ eraserBuilder.MoveTo(position);
+ eraserBuilder.LineTo(position);
+ erasePending = true;
+ }
+ else
+ {
+ activeStroke = new(Palette[colorIndex], Widths[widthIndex]);
+ activeStroke.Add(position);
+ }
+ }
}
-
- activeStroke = new(Palette[colorIndex], Widths[widthIndex]);
- activeStroke.Add(position);
}
- public void PointerUp()
+ public void PointerUp(bool erase)
{
- erasing = false;
-
- if (activeStroke is null)
+ if (erase)
{
- return;
- }
-
- strokes.Add(activeStroke);
- activeStroke = null;
- bakeDirty = true;
- }
+ if (erasing)
+ {
+ erasing = false;
- public void Clear()
- {
- if (strokes.Count is 0)
- {
- return;
+ if (erasePending)
+ {
+ ApplyEraser();
+ }
+ }
}
-
- foreach (Stroke stroke in strokes)
+ else if (activeStroke is not null)
{
- stroke.Dispose();
+ activeStroke.Complete(strokePaint);
+ strokes.Add(activeStroke);
+ activeStroke = null;
+ bakeDirty = true;
}
-
- strokes.Clear();
- bakeDirty = true;
- }
-
- public void SelectColor(int index)
- {
- colorIndex = Math.Clamp(index, 0, Palette.Length - 1);
- eraserMode = false;
- }
-
- public void SelectWidth(int index)
- {
- widthIndex = Math.Clamp(index, 0, Widths.Length - 1);
- }
-
- public void ToggleEraser()
- {
- eraserMode = !eraserMode;
}
public void Dispose()
@@ -214,11 +198,11 @@ public void Dispose()
strokes.Clear();
activeStroke?.Dispose();
- bakedStrokes?.Dispose();
+ bakedCanvas?.Dispose();
+ eraserBuilder.Dispose();
strokePaint.Dispose();
fillPaint.Dispose();
labelFont.Dispose();
- typeface.Dispose();
}
private void EnsureLayout(float width, float height)
@@ -233,7 +217,6 @@ private void EnsureLayout(float width, float height)
layoutWidth = width;
layoutHeight = height;
- stage = new(0.0f, 0.0f, width, height);
canvasArea = new(0.0f, ToolbarHeight, width, MathF.Max(ToolbarHeight, height - StatusHeight));
for (int index = 0; index < swatchRects.Length; index++)
@@ -250,48 +233,42 @@ private void EnsureLayout(float width, float height)
widthRects[index] = new(left, top, left + SwatchSize, bottom);
}
- float eraserLeft = widthRects[^1].Right + (SwatchGap * 2.0f);
- eraserRect = new(eraserLeft, top, eraserLeft + ButtonWidth, bottom);
-
- float clearLeft = MathF.Max(eraserRect.Right + (SwatchGap * 2.0f), width - SwatchGap - ButtonWidth);
+ float clearLeft = MathF.Max(widthRects[^1].Right + (SwatchGap * 2.0f), width - SwatchGap - ButtonWidth);
clearRect = new(clearLeft, top, clearLeft + ButtonWidth, bottom);
+ bakeDirty = true;
}
- private SKPicture BakeStrokes()
+ private SKPicture BakeCanvas()
{
+ const float spacing = 32.0f;
+
using SKPictureRecorder recorder = new();
SKCanvas canvas = recorder.BeginRecording(canvasArea);
+ int nodes = 0;
- foreach (Stroke stroke in strokes)
+ strokePaint.Color = Grid;
+ strokePaint.StrokeWidth = 1.0f;
+
+ for (float x = canvasArea.Left + spacing; x < canvasArea.Right; x += spacing)
{
- DrawStroke(canvas, stroke);
+ canvas.DrawLine(x, canvasArea.Top, x, canvasArea.Bottom, strokePaint);
}
- return recorder.EndRecording();
- }
-
- private void DrawStroke(SKCanvas canvas, Stroke stroke)
- {
- strokePaint.Color = stroke.Color;
- strokePaint.StrokeWidth = stroke.Width;
- canvas.DrawPath(stroke.Path, strokePaint);
- }
-
- private void DrawGrid(SKCanvas canvas)
- {
- const float spacing = 32.0f;
-
- fillPaint.Color = Grid;
-
- for (float x = spacing; x < canvasArea.Right; x += spacing)
+ for (float y = canvasArea.Top + spacing; y < canvasArea.Bottom; y += spacing)
{
- canvas.DrawRect(x, canvasArea.Top, 1.0f, canvasArea.Height, fillPaint);
+ canvas.DrawLine(canvasArea.Left, y, canvasArea.Right, y, strokePaint);
}
- for (float y = canvasArea.Top + spacing; y < canvasArea.Bottom; y += spacing)
+ foreach (Stroke stroke in strokes)
{
- canvas.DrawRect(canvasArea.Left, y, canvasArea.Width, 1.0f, fillPaint);
+ fillPaint.Color = stroke.Color;
+ canvas.DrawPath(stroke.Path, fillPaint);
+ nodes += stroke.NodeCount;
}
+
+ nodeCount = nodes;
+
+ return recorder.EndRecording();
}
private void DrawEraserCursor(SKCanvas canvas)
@@ -304,10 +281,10 @@ private void DrawEraserCursor(SKCanvas canvas)
private void DrawToolbar(SKCanvas canvas)
{
fillPaint.Color = Panel;
- canvas.DrawRect(0.0f, 0.0f, stage.Width, ToolbarHeight, fillPaint);
+ canvas.DrawRect(0.0f, 0.0f, layoutWidth, ToolbarHeight, fillPaint);
fillPaint.Color = Divider;
- canvas.DrawRect(0.0f, ToolbarHeight - 1.0f, stage.Width, 1.0f, fillPaint);
+ canvas.DrawRect(0.0f, ToolbarHeight - 1.0f, layoutWidth, 1.0f, fillPaint);
for (int index = 0; index < Palette.Length; index++)
{
@@ -316,7 +293,7 @@ private void DrawToolbar(SKCanvas canvas)
fillPaint.Color = Palette[index];
canvas.DrawRoundRect(swatch, 6.0f, 6.0f, fillPaint);
- if (index == colorIndex && !eraserMode)
+ if (index == colorIndex)
{
strokePaint.Color = Highlight;
strokePaint.StrokeWidth = 2.0f;
@@ -331,98 +308,117 @@ private void DrawToolbar(SKCanvas canvas)
fillPaint.Color = index == widthIndex ? Selected : Panel;
canvas.DrawRoundRect(slot, 6.0f, 6.0f, fillPaint);
- fillPaint.Color = eraserMode ? Label : Palette[colorIndex];
+ fillPaint.Color = Palette[colorIndex];
canvas.DrawCircle(slot.MidX, slot.MidY, Widths[index] * 0.5f, fillPaint);
}
- DrawButton(canvas, eraserRect, "ERASE", eraserMode, true);
- DrawButton(canvas, clearRect, "CLEAR", false, strokes.Count > 0);
- }
-
- private void DrawButton(SKCanvas canvas, SKRect rect, string text, bool active, bool enabled)
- {
- SKColor accent = active ? Highlight : enabled ? Label : Divider;
+ SKColor accent = CanClear ? Label : Divider;
- fillPaint.Color = active ? Selected : Panel;
- canvas.DrawRoundRect(rect, 6.0f, 6.0f, fillPaint);
+ fillPaint.Color = Panel;
+ canvas.DrawRoundRect(clearRect, 6.0f, 6.0f, fillPaint);
strokePaint.Color = accent;
strokePaint.StrokeWidth = 1.5f;
- canvas.DrawRoundRect(rect, 6.0f, 6.0f, strokePaint);
+ canvas.DrawRoundRect(clearRect, 6.0f, 6.0f, strokePaint);
fillPaint.Color = accent;
- canvas.DrawText(text, rect.MidX, rect.MidY + 4.0f, SKTextAlign.Center, labelFont, fillPaint);
+ canvas.DrawText("CLEAR", clearRect.MidX, clearRect.MidY + 4.0f, SKTextAlign.Center, labelFont, fillPaint);
}
private void DrawStatus(SKCanvas canvas)
{
- float top = stage.Height - StatusHeight;
+ float top = layoutHeight - StatusHeight;
fillPaint.Color = Panel;
- canvas.DrawRect(0.0f, top, stage.Width, StatusHeight, fillPaint);
+ canvas.DrawRect(0.0f, top, layoutWidth, StatusHeight, fillPaint);
fillPaint.Color = Divider;
- canvas.DrawRect(0.0f, top, stage.Width, 1.0f, fillPaint);
-
- int points = 0;
-
- foreach (Stroke stroke in strokes)
- {
- points += stroke.PointCount;
- }
+ canvas.DrawRect(0.0f, top, layoutWidth, 1.0f, fillPaint);
float baseline = top + (StatusHeight * 0.5f) + 4.0f;
fillPaint.Color = Label;
- canvas.DrawText($"STROKES {strokes.Count} POINTS {points}", SwatchGap, baseline, SKTextAlign.Left, labelFont, fillPaint);
- canvas.DrawText("DRAG TO DRAW RIGHT DRAG TO ERASE", stage.Width - SwatchGap, baseline, SKTextAlign.Right, labelFont, fillPaint);
+ canvas.DrawText($"STROKES {strokes.Count} NODES {nodeCount}", SwatchGap, baseline, SKTextAlign.Left, labelFont, fillPaint);
+ canvas.DrawText("DRAG TO DRAW RIGHT DRAG TO ERASE", layoutWidth - SwatchGap, baseline, SKTextAlign.Right, labelFont, fillPaint);
}
private void HandleToolbarClick(SKPoint position)
{
- for (int index = 0; index < Palette.Length; index++)
- {
- if (swatchRects[index].Contains(position.X, position.Y))
- {
- SelectColor(index);
+ int swatch = IndexAt(swatchRects, position);
+ int slot = IndexAt(widthRects, position);
- return;
- }
+ if (swatch >= 0)
+ {
+ colorIndex = swatch;
+ }
+ else if (slot >= 0)
+ {
+ widthIndex = slot;
}
+ }
- for (int index = 0; index < Widths.Length; index++)
+ private void ClearCanvas()
+ {
+ if (CanClear)
{
- if (widthRects[index].Contains(position.X, position.Y))
+ foreach (Stroke stroke in strokes)
{
- SelectWidth(index);
-
- return;
+ stroke.Dispose();
}
- }
- if (eraserRect.Contains(position.X, position.Y))
- {
- ToggleEraser();
+ strokes.Clear();
+ activeStroke?.Dispose();
+ activeStroke = null;
+ eraserBuilder.Reset();
+ erasing = false;
+ erasePending = false;
+ bakeDirty = true;
}
- else if (clearRect.Contains(position.X, position.Y))
+ }
+
+ private static int IndexAt(SKRect[] rects, SKPoint position)
+ {
+ for (int index = 0; index < rects.Length; index++)
{
- Clear();
+ if (rects[index].Contains(position.X, position.Y))
+ {
+ return index;
+ }
}
+
+ return -1;
}
- private void Erase(SKPoint from, SKPoint to)
+ private void ApplyEraser()
{
+ using SKPath centerline = eraserBuilder.Detach();
+
+ if (erasing)
+ {
+ eraserBuilder.MoveTo(pointer);
+ }
+
+ strokePaint.StrokeWidth = EraserRadius * 2.0f;
+
+ using SKPath eraser = strokePaint.GetFillPath(centerline)!;
+ SKRect eraserBounds = eraser.Bounds;
+
+ erasePending = false;
+
for (int index = strokes.Count - 1; index >= 0; index--)
{
- if (strokes[index].Split(from, to, EraserRadius) is not { } fragments)
+ Stroke stroke = strokes[index];
+
+ if (stroke.Erase(eraser, eraserBounds))
{
- continue;
- }
+ if (stroke.IsEmpty)
+ {
+ stroke.Dispose();
+ strokes.RemoveAt(index);
+ }
- strokes[index].Dispose();
- strokes.RemoveAt(index);
- strokes.InsertRange(index, fragments);
- bakeDirty = true;
+ bakeDirty = true;
+ }
}
}
}
\ No newline at end of file
diff --git a/sources/Experiments/InkCanvas/Stroke.cs b/sources/Experiments/InkCanvas/Stroke.cs
index 78b4c8b5..0efe3219 100644
--- a/sources/Experiments/InkCanvas/Stroke.cs
+++ b/sources/Experiments/InkCanvas/Stroke.cs
@@ -6,144 +6,92 @@ internal sealed class Stroke(SKColor color, float width) : IDisposable
{
private const float MinimumPointDistance = 1.5f;
- private readonly List points = [];
-
+ private SKPathBuilder? builder = new();
private SKPath? path;
private SKRect bounds;
-
- private Stroke(SKColor color, float width, ReadOnlySpan source) : this(color, width)
- {
- points.AddRange(source);
- bounds = ComputeBounds(source, width);
- }
+ private SKPoint tailStart;
+ private SKPoint tailEnd;
+ private int sampleCount;
public SKColor Color { get; } = color;
public float Width { get; } = width;
- public int PointCount => points.Count;
-
- public SKPath Path => path ??= BuildPath();
-
- public void Add(SKPoint point)
- {
- if (points.Count > 0)
- {
- SKPoint last = points[^1];
- float dx = point.X - last.X;
- float dy = point.Y - last.Y;
+ public int NodeCount => Path.PointCount;
- if ((dx * dx) + (dy * dy) < MinimumPointDistance * MinimumPointDistance)
- {
- return;
- }
- }
+ public bool IsEmpty => Path.IsEmpty;
- points.Add(point);
+ public SKPath Path => path ??= builder!.Snapshot();
- float radius = Width * 0.5f;
- SKRect extent = new(point.X - radius, point.Y - radius, point.X + radius, point.Y + radius);
- bounds = points.Count is 1 ? extent : SKRect.Union(bounds, extent);
+ public SKPoint TailStart => tailStart;
- path?.Dispose();
- path = null;
- }
+ public SKPoint TailEnd => tailEnd;
- public List? Split(SKPoint from, SKPoint to, float radius)
+ public void Add(SKPoint point)
{
- float threshold = radius + (Width * 0.5f);
- SKRect swept = new(
- MathF.Min(from.X, to.X) - threshold,
- MathF.Min(from.Y, to.Y) - threshold,
- MathF.Max(from.X, to.X) + threshold,
- MathF.Max(from.Y, to.Y) + threshold);
-
- if (!swept.IntersectsWith(bounds))
+ if (sampleCount is 0 || DistanceSquared(tailEnd, point) >= MinimumPointDistance * MinimumPointDistance)
{
- return null;
- }
+ path?.Dispose();
+ path = null;
- float thresholdSquared = threshold * threshold;
- List fragments = [];
- List survivors = [];
- bool touched = false;
-
- foreach (SKPoint point in points)
- {
- if (SegmentDistanceSquared(from, to, point) <= thresholdSquared)
+ if (sampleCount is 0)
{
- touched = true;
-
- if (survivors.Count > 1)
- {
- fragments.Add(new(Color, Width, [.. survivors]));
- }
-
- survivors.Clear();
+ builder!.MoveTo(point);
+ tailStart = point;
}
- else
+ else if (sampleCount > 1)
{
- survivors.Add(point);
+ tailStart = new((tailEnd.X + point.X) * 0.5f, (tailEnd.Y + point.Y) * 0.5f);
+ builder!.QuadTo(tailEnd, tailStart);
}
- }
- if (!touched)
- {
- return null;
- }
-
- if (survivors.Count > 1)
- {
- fragments.Add(new(Color, Width, [.. survivors]));
+ tailEnd = point;
+ sampleCount++;
}
-
- return fragments;
}
- public void Dispose()
+ public void Complete(SKPaint paint)
{
+ builder!.LineTo(tailEnd);
+
path?.Dispose();
- }
- private SKPath BuildPath()
- {
- using SKPathBuilder builder = new();
+ using SKPath centerline = builder.Detach();
- if (points.Count is 1)
- {
- builder.AddCircle(points[0].X, points[0].Y, Width * 0.25f, SKPathDirection.Clockwise);
+ builder.Dispose();
+ builder = null;
- return builder.Detach();
- }
+ paint.StrokeWidth = Width;
+ path = paint.GetFillPath(centerline)!;
- builder.MoveTo(points[0]);
+ bounds = path.TightBounds;
+ }
- for (int index = 1; index < points.Count - 1; index++)
+ public bool Erase(SKPath eraser, SKRect eraserBounds)
+ {
+ if (bounds.IntersectsWith(eraserBounds))
{
- SKPoint current = points[index];
- SKPoint next = points[index + 1];
- SKPoint middle = new((current.X + next.X) * 0.5f, (current.Y + next.Y) * 0.5f);
+ SKPath source = Path;
- builder.QuadTo(current, middle);
- }
+ using SKPath? overlap = source.Op(eraser, SKPathOp.Intersect);
+
+ if (overlap is not null && !overlap.IsEmpty && source.Op(eraser, SKPathOp.Difference) is { } result)
+ {
+ source.Dispose();
+ path = result;
+ bounds = result.TightBounds;
- builder.LineTo(points[^1]);
+ return true;
+ }
+ }
- return builder.Detach();
+ return false;
}
- private static SKRect ComputeBounds(ReadOnlySpan points, float width)
+ public void Dispose()
{
- float radius = width * 0.5f;
- SKRect result = new(points[0].X - radius, points[0].Y - radius, points[0].X + radius, points[0].Y + radius);
-
- for (int index = 1; index < points.Length; index++)
- {
- SKPoint point = points[index];
- result = SKRect.Union(result, new(point.X - radius, point.Y - radius, point.X + radius, point.Y + radius));
- }
-
- return result;
+ path?.Dispose();
+ builder?.Dispose();
}
private static float DistanceSquared(SKPoint first, SKPoint second)
@@ -153,21 +101,4 @@ private static float DistanceSquared(SKPoint first, SKPoint second)
return (dx * dx) + (dy * dy);
}
-
- private static float SegmentDistanceSquared(SKPoint start, SKPoint end, SKPoint point)
- {
- float dx = end.X - start.X;
- float dy = end.Y - start.Y;
- float lengthSquared = (dx * dx) + (dy * dy);
-
- if (lengthSquared < float.Epsilon)
- {
- return DistanceSquared(start, point);
- }
-
- float amount = Math.Clamp((((point.X - start.X) * dx) + ((point.Y - start.Y) * dy)) / lengthSquared, 0.0f, 1.0f);
- SKPoint projection = new(start.X + (amount * dx), start.Y + (amount * dy));
-
- return DistanceSquared(projection, point);
- }
}
\ No newline at end of file
From e67c31b2556c4d44f9cdc8efef3162d7883e94f8 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 18:27:27 +0800
Subject: [PATCH 22/50] Refactor event handling and rendering logic
Refactored App.cs to move event subscriptions into the Run method using inline lambdas. Inlined CreateTexture and DrawBoard logic. Simplified DPI scaling and logical size calculations. Handled texture recreation and swap chain resizing in window.Resize event. Removed nullable assertion in Board.cs and improved null checks in Stroke.cs for eraser operations.
---
sources/Experiments/InkCanvas/App.cs | 168 +++++++++++-------------
sources/Experiments/InkCanvas/Board.cs | 2 +-
sources/Experiments/InkCanvas/Stroke.cs | 2 +-
3 files changed, 78 insertions(+), 94 deletions(-)
diff --git a/sources/Experiments/InkCanvas/App.cs b/sources/Experiments/InkCanvas/App.cs
index 4d3e2dbf..9f8200c0 100644
--- a/sources/Experiments/InkCanvas/App.cs
+++ b/sources/Experiments/InkCanvas/App.cs
@@ -2,7 +2,6 @@
using InkCanvas.Helpers;
using Silk.NET.Input;
using Silk.NET.Windowing;
-using SkiaSharp;
using Zenith.NET;
using Zenith.NET.DirectX12;
using Zenith.NET.Extensions.Skia;
@@ -19,9 +18,6 @@ internal static class App
private static readonly Board board;
private static SKTexture texture;
- private static Vector2 dpiScale = Vector2.One;
- private static float logicalWidth;
- private static float logicalHeight;
static App()
{
@@ -76,8 +72,15 @@ static App()
Format = PixelFormat.B8G8R8A8UNorm
});
- texture = CreateTexture(Width, Height);
board = new();
+
+ texture = Context.CreateSKTexture(new()
+ {
+ Format = PixelFormat.B8G8R8A8UNorm,
+ Width = Width,
+ Height = Height,
+ SampleCount = SampleCount.Count1
+ });
}
public static GraphicsContext Context { get; }
@@ -90,111 +93,92 @@ static App()
public static void Run()
{
- IMouse mouse = input.Mice[0];
- mouse.MouseMove += MouseMove;
- mouse.MouseDown += MouseDown;
- mouse.MouseUp += MouseUp;
-
- window.Render += Render;
- window.Resize += _ => Resize();
-
- window.Run();
-
- board.Dispose();
- texture.Dispose();
- swapChain.Dispose();
- input.Dispose();
- window.Dispose();
+ window.Render += _ =>
+ {
+ if (Width is 0 || Height is 0)
+ {
+ return;
+ }
- Context.Dispose();
- }
+ uint width = (uint)(Width / DpiScale.X);
+ uint height = (uint)(Height / DpiScale.Y);
- private static void Render(double _)
- {
- uint width = Width;
- uint height = Height;
+ texture.Render((canvas) =>
+ {
+ canvas.Save();
+ canvas.Scale(DpiScale.X, DpiScale.Y);
- if (width is 0 || height is 0)
- {
- return;
- }
-
- dpiScale = DpiScale;
- logicalWidth = width / dpiScale.X;
- logicalHeight = height / dpiScale.Y;
+ board.Draw(canvas, Width / DpiScale.X, Height / DpiScale.Y);
- texture.Render(DrawBoard);
+ canvas.Restore();
+ });
- CommandBuffer commandBuffer = Context.GraphicsQueue.CommandBuffer();
+ CommandBuffer commandBuffer = Context.GraphicsQueue.CommandBuffer();
- commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.CopyDst);
- commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, TextureLayout.CopySrc);
- commandBuffer.CopyTexture(texture, default, default, swapChain.Drawable, default, default, new()
- {
- Width = width,
- Height = height,
- Depth = 1
- });
- commandBuffer.Transition(texture, default, TextureLayout.CopySrc, TextureLayout.ColorAttachment);
- commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.CopyDst, TextureLayout.Present);
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.CopyDst);
+ commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, TextureLayout.CopySrc);
- commandBuffer.Submit().Wait();
+ commandBuffer.CopyTexture(texture, default, default, swapChain.Drawable, default, default, new()
+ {
+ Width = width,
+ Height = height,
+ Depth = 1
+ });
- swapChain.Present();
- }
+ commandBuffer.Transition(texture, default, TextureLayout.CopySrc, TextureLayout.ColorAttachment);
+ commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.CopyDst, TextureLayout.Present);
- private static void DrawBoard(SKCanvas canvas)
- {
- canvas.Save();
- canvas.Scale(dpiScale.X, dpiScale.Y);
- board.Draw(canvas, logicalWidth, logicalHeight);
- canvas.Restore();
- }
+ commandBuffer.Submit().Wait();
- private static void MouseMove(IMouse _, Vector2 position)
- {
- board.PointerMove(new(position.X, position.Y));
- }
+ swapChain.Present();
+ };
- private static void MouseDown(IMouse mouse, MouseButton button)
- {
- if (button is MouseButton.Left or MouseButton.Right)
+ window.Resize += _ =>
{
- board.PointerDown(new(mouse.Position.X, mouse.Position.Y), button is MouseButton.Right);
- }
- }
+ if (Width is 0 || Height is 0)
+ {
+ return;
+ }
+
+ texture.Dispose();
+ texture = Context.CreateSKTexture(new()
+ {
+ Format = PixelFormat.B8G8R8A8UNorm,
+ Width = Width,
+ Height = Height,
+ SampleCount = SampleCount.Count1
+ });
+
+ swapChain.Resize(Width, Height);
+ };
- private static void MouseUp(IMouse _, MouseButton button)
- {
- if (button is MouseButton.Left or MouseButton.Right)
+ IMouse mouse = input.Mice[0];
+ mouse.MouseMove += (_, position) => board.PointerMove(new(position.X, position.Y));
+
+ mouse.MouseDown += (_, button) =>
{
- board.PointerUp(button is MouseButton.Right);
- }
- }
+ if (button is MouseButton.Left or MouseButton.Right)
+ {
+ board.PointerDown(new(mouse.Position.X, mouse.Position.Y), button is MouseButton.Right);
+ }
+ };
- private static SKTexture CreateTexture(uint width, uint height)
- {
- return Context.CreateSKTexture(new()
+ mouse.MouseUp += (_, button) =>
{
- Format = PixelFormat.B8G8R8A8UNorm,
- Width = width,
- Height = height,
- SampleCount = SampleCount.Count1
- });
- }
+ if (button is MouseButton.Left or MouseButton.Right)
+ {
+ board.PointerUp(button is MouseButton.Right);
+ }
+ };
- private static void Resize()
- {
- uint width = Width;
- uint height = Height;
+ window.Run();
- if (width is not 0 && height is not 0)
- {
- SKTexture oldTexture = texture;
+ board.Dispose();
+ texture.Dispose();
+ swapChain.Dispose();
+ input.Dispose();
+ window.Dispose();
- swapChain.Resize(width, height);
- texture = CreateTexture(width, height);
- oldTexture.Dispose();
- }
+ Context.Dispose();
}
}
\ No newline at end of file
diff --git a/sources/Experiments/InkCanvas/Board.cs b/sources/Experiments/InkCanvas/Board.cs
index fefce0d0..3c758343 100644
--- a/sources/Experiments/InkCanvas/Board.cs
+++ b/sources/Experiments/InkCanvas/Board.cs
@@ -97,7 +97,7 @@ public void Draw(SKCanvas canvas, float width, float height)
canvas.Save();
canvas.ClipRect(canvasArea);
- canvas.DrawPicture(bakedCanvas!);
+ canvas.DrawPicture(bakedCanvas);
if (activeStroke is not null)
{
diff --git a/sources/Experiments/InkCanvas/Stroke.cs b/sources/Experiments/InkCanvas/Stroke.cs
index 0efe3219..5d711078 100644
--- a/sources/Experiments/InkCanvas/Stroke.cs
+++ b/sources/Experiments/InkCanvas/Stroke.cs
@@ -75,7 +75,7 @@ public bool Erase(SKPath eraser, SKRect eraserBounds)
using SKPath? overlap = source.Op(eraser, SKPathOp.Intersect);
- if (overlap is not null && !overlap.IsEmpty && source.Op(eraser, SKPathOp.Difference) is { } result)
+ if (overlap?.IsEmpty is false && source.Op(eraser, SKPathOp.Difference) is { } result)
{
source.Dispose();
path = result;
From f7d9c268179a796f478cfecfb81cf0aa4d20fdb7 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 18:33:28 +0800
Subject: [PATCH 23/50] Expand texture usage flags to include Sampled and
TransferDst
Added TextureUsages.Sampled and TextureUsages.TransferDst to the texture initialization, allowing the texture to be used as a sampled texture and as a transfer destination, in addition to its previous usages as a color attachment and transfer source.
---
sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
index 57f5010a..40d4301b 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
@@ -21,7 +21,7 @@ internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
MipLevels = 1,
ArrayLayers = 1,
SampleCount = SampleCount.Count1,
- Usages = TextureUsages.ColorAttachment | TextureUsages.TransferSrc
+ Usages = TextureUsages.Sampled | TextureUsages.ColorAttachment | TextureUsages.TransferSrc | TextureUsages.TransferDst
}));
surface = SKSurface.Create(renderer.GRContext, backendTexture, GRSurfaceOrigin.TopLeft, (int)SKFormats.Skia(desc.SampleCount), SKFormats.Skia(desc.Format));
From 91e8aa33c56567afa522ccc1c864120b6979a767 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 21:41:33 +0800
Subject: [PATCH 24/50] Refactor InkCanvas drawing architecture
---
sources/Experiments/InkCanvas/App.cs | 74 +--
sources/Experiments/InkCanvas/Board.cs | 424 ------------------
.../Experiments/InkCanvas/Drawing/Canvas.cs | 272 +++++++++++
.../InkCanvas/Drawing/CanvasController.cs | 95 ++++
.../InkCanvas/{ => Drawing}/Stroke.cs | 9 +-
.../Experiments/InkCanvas/Drawing/Toolbar.cs | 201 +++++++++
6 files changed, 580 insertions(+), 495 deletions(-)
delete mode 100644 sources/Experiments/InkCanvas/Board.cs
create mode 100644 sources/Experiments/InkCanvas/Drawing/Canvas.cs
create mode 100644 sources/Experiments/InkCanvas/Drawing/CanvasController.cs
rename sources/Experiments/InkCanvas/{ => Drawing}/Stroke.cs (95%)
create mode 100644 sources/Experiments/InkCanvas/Drawing/Toolbar.cs
diff --git a/sources/Experiments/InkCanvas/App.cs b/sources/Experiments/InkCanvas/App.cs
index 9f8200c0..e8f9c946 100644
--- a/sources/Experiments/InkCanvas/App.cs
+++ b/sources/Experiments/InkCanvas/App.cs
@@ -1,10 +1,10 @@
using System.Numerics;
+using InkCanvas.Drawing;
using InkCanvas.Helpers;
using Silk.NET.Input;
using Silk.NET.Windowing;
using Zenith.NET;
using Zenith.NET.DirectX12;
-using Zenith.NET.Extensions.Skia;
using Zenith.NET.Metal;
using Zenith.NET.Vulkan;
@@ -15,9 +15,7 @@ internal static class App
private static readonly IWindow window;
private static readonly IInputContext input;
private static readonly SwapChain swapChain;
- private static readonly Board board;
-
- private static SKTexture texture;
+ private static readonly CanvasController canvas;
static App()
{
@@ -72,15 +70,7 @@ static App()
Format = PixelFormat.B8G8R8A8UNorm
});
- board = new();
-
- texture = Context.CreateSKTexture(new()
- {
- Format = PixelFormat.B8G8R8A8UNorm,
- Width = Width,
- Height = Height,
- SampleCount = SampleCount.Count1
- });
+ canvas = new(Context, input, Width, Height);
}
public static GraphicsContext Context { get; }
@@ -100,32 +90,10 @@ public static void Run()
return;
}
- uint width = (uint)(Width / DpiScale.X);
- uint height = (uint)(Height / DpiScale.Y);
-
- texture.Render((canvas) =>
- {
- canvas.Save();
- canvas.Scale(DpiScale.X, DpiScale.Y);
-
- board.Draw(canvas, Width / DpiScale.X, Height / DpiScale.Y);
-
- canvas.Restore();
- });
-
CommandBuffer commandBuffer = Context.GraphicsQueue.CommandBuffer();
commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.Undefined, TextureLayout.CopyDst);
- commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, TextureLayout.CopySrc);
-
- commandBuffer.CopyTexture(texture, default, default, swapChain.Drawable, default, default, new()
- {
- Width = width,
- Height = height,
- Depth = 1
- });
-
- commandBuffer.Transition(texture, default, TextureLayout.CopySrc, TextureLayout.ColorAttachment);
+ canvas.Render(commandBuffer, swapChain.Drawable, DpiScale);
commandBuffer.Transition(swapChain.Drawable, default, TextureLayout.CopyDst, TextureLayout.Present);
commandBuffer.Submit().Wait();
@@ -133,48 +101,20 @@ public static void Run()
swapChain.Present();
};
- window.Resize += _ =>
+ window.FramebufferResize += _ =>
{
if (Width is 0 || Height is 0)
{
return;
}
- texture.Dispose();
- texture = Context.CreateSKTexture(new()
- {
- Format = PixelFormat.B8G8R8A8UNorm,
- Width = Width,
- Height = Height,
- SampleCount = SampleCount.Count1
- });
-
+ canvas.Resize(Width, Height);
swapChain.Resize(Width, Height);
};
- IMouse mouse = input.Mice[0];
- mouse.MouseMove += (_, position) => board.PointerMove(new(position.X, position.Y));
-
- mouse.MouseDown += (_, button) =>
- {
- if (button is MouseButton.Left or MouseButton.Right)
- {
- board.PointerDown(new(mouse.Position.X, mouse.Position.Y), button is MouseButton.Right);
- }
- };
-
- mouse.MouseUp += (_, button) =>
- {
- if (button is MouseButton.Left or MouseButton.Right)
- {
- board.PointerUp(button is MouseButton.Right);
- }
- };
-
window.Run();
- board.Dispose();
- texture.Dispose();
+ canvas.Dispose();
swapChain.Dispose();
input.Dispose();
window.Dispose();
diff --git a/sources/Experiments/InkCanvas/Board.cs b/sources/Experiments/InkCanvas/Board.cs
deleted file mode 100644
index 3c758343..00000000
--- a/sources/Experiments/InkCanvas/Board.cs
+++ /dev/null
@@ -1,424 +0,0 @@
-using SkiaSharp;
-
-namespace InkCanvas;
-
-internal sealed class Board : IDisposable
-{
- private const float ToolbarHeight = 64.0f;
- private const float StatusHeight = 34.0f;
- private const float SwatchSize = 30.0f;
- private const float SwatchGap = 12.0f;
- private const float ButtonWidth = 64.0f;
- private const float EraserRadius = 22.0f;
-
- private static readonly SKColor Surface = new(22, 24, 30);
- private static readonly SKColor Panel = new(31, 34, 43);
- private static readonly SKColor Divider = new(48, 52, 63);
- private static readonly SKColor Label = new(150, 158, 172);
- private static readonly SKColor Highlight = new(238, 242, 248);
- private static readonly SKColor Grid = new(32, 35, 43);
- private static readonly SKColor Selected = new(58, 64, 78);
- private static readonly SKColor Cursor = new(150, 158, 172, 200);
-
- private static readonly SKColor[] Palette =
- [
- new(238, 242, 248),
- new(236, 108, 96),
- new(238, 178, 74),
- new(96, 196, 154),
- new(102, 158, 240),
- new(178, 134, 234)
- ];
-
- private static readonly float[] Widths = [2.0f, 4.0f, 8.0f, 16.0f];
-
- private readonly List strokes = [];
- private readonly SKRect[] swatchRects = new SKRect[Palette.Length];
- private readonly SKRect[] widthRects = new SKRect[Widths.Length];
- private readonly SKPathBuilder eraserBuilder = new();
- private readonly SKFont labelFont;
- private readonly SKPaint fillPaint = new() { IsAntialias = true };
- private readonly SKPaint strokePaint = new()
- {
- IsAntialias = true,
- Style = SKPaintStyle.Stroke,
- StrokeCap = SKStrokeCap.Round,
- StrokeJoin = SKStrokeJoin.Round
- };
-
- private SKPicture? bakedCanvas;
- private Stroke? activeStroke;
- private SKRect canvasArea;
- private SKRect clearRect;
- private SKPoint pointer;
- private int colorIndex;
- private int widthIndex = 1;
- private int nodeCount;
- private bool erasing;
- private bool erasePending;
- private bool bakeDirty = true;
- private float layoutWidth = -1.0f;
- private float layoutHeight = -1.0f;
-
- private bool CanClear => strokes.Count > 0 || activeStroke is not null || erasing || erasePending;
-
- public Board()
- {
- string family = OperatingSystem.IsMacOS() ? "SF Pro Text" : OperatingSystem.IsWindows() ? "Segoe UI" : "Noto Sans";
-
- using SKTypeface typeface = SKTypeface.FromFamilyName(family, SKFontStyle.Normal);
-
- labelFont = new(typeface, 12.0f)
- {
- Edging = SKFontEdging.SubpixelAntialias,
- Hinting = SKFontHinting.Slight,
- Subpixel = true
- };
- }
-
- public void Draw(SKCanvas canvas, float width, float height)
- {
- EnsureLayout(width, height);
-
- if (erasePending)
- {
- ApplyEraser();
- }
-
- fillPaint.Color = Surface;
- canvas.DrawRect(0.0f, 0.0f, width, height, fillPaint);
-
- if (bakeDirty)
- {
- bakedCanvas?.Dispose();
- bakedCanvas = BakeCanvas();
- bakeDirty = false;
- }
-
- canvas.Save();
- canvas.ClipRect(canvasArea);
- canvas.DrawPicture(bakedCanvas);
-
- if (activeStroke is not null)
- {
- strokePaint.Color = activeStroke.Color;
- strokePaint.StrokeWidth = activeStroke.Width;
- canvas.DrawPath(activeStroke.Path, strokePaint);
- canvas.DrawLine(activeStroke.TailStart, activeStroke.TailEnd, strokePaint);
- }
-
- if (erasing && canvasArea.Contains(pointer.X, pointer.Y))
- {
- DrawEraserCursor(canvas);
- }
-
- canvas.Restore();
-
- DrawToolbar(canvas);
- DrawStatus(canvas);
- }
-
- public void PointerMove(SKPoint position)
- {
- pointer = position;
-
- if (erasing)
- {
- eraserBuilder.LineTo(position);
- erasePending = true;
- }
- else
- {
- activeStroke?.Add(position);
- }
- }
-
- public void PointerDown(SKPoint position, bool erase)
- {
- if (!erase && clearRect.Contains(position.X, position.Y))
- {
- pointer = position;
- ClearCanvas();
- }
- else if (activeStroke is null && !erasing)
- {
- pointer = position;
-
- if (!erase && position.Y <= ToolbarHeight)
- {
- HandleToolbarClick(position);
- }
- else if (canvasArea.Contains(position.X, position.Y))
- {
- if (erase)
- {
- erasing = true;
- eraserBuilder.MoveTo(position);
- eraserBuilder.LineTo(position);
- erasePending = true;
- }
- else
- {
- activeStroke = new(Palette[colorIndex], Widths[widthIndex]);
- activeStroke.Add(position);
- }
- }
- }
- }
-
- public void PointerUp(bool erase)
- {
- if (erase)
- {
- if (erasing)
- {
- erasing = false;
-
- if (erasePending)
- {
- ApplyEraser();
- }
- }
- }
- else if (activeStroke is not null)
- {
- activeStroke.Complete(strokePaint);
- strokes.Add(activeStroke);
- activeStroke = null;
- bakeDirty = true;
- }
- }
-
- public void Dispose()
- {
- foreach (Stroke stroke in strokes)
- {
- stroke.Dispose();
- }
-
- strokes.Clear();
- activeStroke?.Dispose();
- bakedCanvas?.Dispose();
- eraserBuilder.Dispose();
- strokePaint.Dispose();
- fillPaint.Dispose();
- labelFont.Dispose();
- }
-
- private void EnsureLayout(float width, float height)
- {
- const float top = (ToolbarHeight - SwatchSize) * 0.5f;
- const float bottom = top + SwatchSize;
-
- if (width == layoutWidth && height == layoutHeight)
- {
- return;
- }
-
- layoutWidth = width;
- layoutHeight = height;
- canvasArea = new(0.0f, ToolbarHeight, width, MathF.Max(ToolbarHeight, height - StatusHeight));
-
- for (int index = 0; index < swatchRects.Length; index++)
- {
- float left = SwatchGap + (index * (SwatchSize + SwatchGap));
- swatchRects[index] = new(left, top, left + SwatchSize, bottom);
- }
-
- float widthLeft = swatchRects[^1].Right + (SwatchGap * 2.0f);
-
- for (int index = 0; index < widthRects.Length; index++)
- {
- float left = widthLeft + (index * (SwatchSize + SwatchGap));
- widthRects[index] = new(left, top, left + SwatchSize, bottom);
- }
-
- float clearLeft = MathF.Max(widthRects[^1].Right + (SwatchGap * 2.0f), width - SwatchGap - ButtonWidth);
- clearRect = new(clearLeft, top, clearLeft + ButtonWidth, bottom);
- bakeDirty = true;
- }
-
- private SKPicture BakeCanvas()
- {
- const float spacing = 32.0f;
-
- using SKPictureRecorder recorder = new();
- SKCanvas canvas = recorder.BeginRecording(canvasArea);
- int nodes = 0;
-
- strokePaint.Color = Grid;
- strokePaint.StrokeWidth = 1.0f;
-
- for (float x = canvasArea.Left + spacing; x < canvasArea.Right; x += spacing)
- {
- canvas.DrawLine(x, canvasArea.Top, x, canvasArea.Bottom, strokePaint);
- }
-
- for (float y = canvasArea.Top + spacing; y < canvasArea.Bottom; y += spacing)
- {
- canvas.DrawLine(canvasArea.Left, y, canvasArea.Right, y, strokePaint);
- }
-
- foreach (Stroke stroke in strokes)
- {
- fillPaint.Color = stroke.Color;
- canvas.DrawPath(stroke.Path, fillPaint);
- nodes += stroke.NodeCount;
- }
-
- nodeCount = nodes;
-
- return recorder.EndRecording();
- }
-
- private void DrawEraserCursor(SKCanvas canvas)
- {
- strokePaint.Color = Cursor;
- strokePaint.StrokeWidth = 1.5f;
- canvas.DrawCircle(pointer, EraserRadius, strokePaint);
- }
-
- private void DrawToolbar(SKCanvas canvas)
- {
- fillPaint.Color = Panel;
- canvas.DrawRect(0.0f, 0.0f, layoutWidth, ToolbarHeight, fillPaint);
-
- fillPaint.Color = Divider;
- canvas.DrawRect(0.0f, ToolbarHeight - 1.0f, layoutWidth, 1.0f, fillPaint);
-
- for (int index = 0; index < Palette.Length; index++)
- {
- SKRect swatch = swatchRects[index];
-
- fillPaint.Color = Palette[index];
- canvas.DrawRoundRect(swatch, 6.0f, 6.0f, fillPaint);
-
- if (index == colorIndex)
- {
- strokePaint.Color = Highlight;
- strokePaint.StrokeWidth = 2.0f;
- canvas.DrawRoundRect(SKRect.Inflate(swatch, 4.0f, 4.0f), 9.0f, 9.0f, strokePaint);
- }
- }
-
- for (int index = 0; index < Widths.Length; index++)
- {
- SKRect slot = widthRects[index];
-
- fillPaint.Color = index == widthIndex ? Selected : Panel;
- canvas.DrawRoundRect(slot, 6.0f, 6.0f, fillPaint);
-
- fillPaint.Color = Palette[colorIndex];
- canvas.DrawCircle(slot.MidX, slot.MidY, Widths[index] * 0.5f, fillPaint);
- }
-
- SKColor accent = CanClear ? Label : Divider;
-
- fillPaint.Color = Panel;
- canvas.DrawRoundRect(clearRect, 6.0f, 6.0f, fillPaint);
-
- strokePaint.Color = accent;
- strokePaint.StrokeWidth = 1.5f;
- canvas.DrawRoundRect(clearRect, 6.0f, 6.0f, strokePaint);
-
- fillPaint.Color = accent;
- canvas.DrawText("CLEAR", clearRect.MidX, clearRect.MidY + 4.0f, SKTextAlign.Center, labelFont, fillPaint);
- }
-
- private void DrawStatus(SKCanvas canvas)
- {
- float top = layoutHeight - StatusHeight;
-
- fillPaint.Color = Panel;
- canvas.DrawRect(0.0f, top, layoutWidth, StatusHeight, fillPaint);
-
- fillPaint.Color = Divider;
- canvas.DrawRect(0.0f, top, layoutWidth, 1.0f, fillPaint);
-
- float baseline = top + (StatusHeight * 0.5f) + 4.0f;
-
- fillPaint.Color = Label;
- canvas.DrawText($"STROKES {strokes.Count} NODES {nodeCount}", SwatchGap, baseline, SKTextAlign.Left, labelFont, fillPaint);
- canvas.DrawText("DRAG TO DRAW RIGHT DRAG TO ERASE", layoutWidth - SwatchGap, baseline, SKTextAlign.Right, labelFont, fillPaint);
- }
-
- private void HandleToolbarClick(SKPoint position)
- {
- int swatch = IndexAt(swatchRects, position);
- int slot = IndexAt(widthRects, position);
-
- if (swatch >= 0)
- {
- colorIndex = swatch;
- }
- else if (slot >= 0)
- {
- widthIndex = slot;
- }
- }
-
- private void ClearCanvas()
- {
- if (CanClear)
- {
- foreach (Stroke stroke in strokes)
- {
- stroke.Dispose();
- }
-
- strokes.Clear();
- activeStroke?.Dispose();
- activeStroke = null;
- eraserBuilder.Reset();
- erasing = false;
- erasePending = false;
- bakeDirty = true;
- }
- }
-
- private static int IndexAt(SKRect[] rects, SKPoint position)
- {
- for (int index = 0; index < rects.Length; index++)
- {
- if (rects[index].Contains(position.X, position.Y))
- {
- return index;
- }
- }
-
- return -1;
- }
-
- private void ApplyEraser()
- {
- using SKPath centerline = eraserBuilder.Detach();
-
- if (erasing)
- {
- eraserBuilder.MoveTo(pointer);
- }
-
- strokePaint.StrokeWidth = EraserRadius * 2.0f;
-
- using SKPath eraser = strokePaint.GetFillPath(centerline)!;
- SKRect eraserBounds = eraser.Bounds;
-
- erasePending = false;
-
- for (int index = strokes.Count - 1; index >= 0; index--)
- {
- Stroke stroke = strokes[index];
-
- if (stroke.Erase(eraser, eraserBounds))
- {
- if (stroke.IsEmpty)
- {
- stroke.Dispose();
- strokes.RemoveAt(index);
- }
-
- bakeDirty = true;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/sources/Experiments/InkCanvas/Drawing/Canvas.cs b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
new file mode 100644
index 00000000..d60e3fbf
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
@@ -0,0 +1,272 @@
+using SkiaSharp;
+
+namespace InkCanvas.Drawing;
+
+internal class Canvas : IDisposable
+{
+ private const float EraserRadius = 22.0f;
+
+ private static readonly SKColor Surface = new(22, 24, 30);
+ private static readonly SKColor Grid = new(32, 35, 43);
+ private static readonly SKColor Cursor = new(150, 158, 172, 200);
+
+ private readonly Toolbar toolbar = new();
+ private readonly List strokes = [];
+
+ private readonly SKPathBuilder eraserBuilder = new();
+ private readonly SKPaint fillPaint = new() { IsAntialias = true };
+ private readonly SKPaint strokePaint = new()
+ {
+ IsAntialias = true,
+ Style = SKPaintStyle.Stroke,
+ StrokeCap = SKStrokeCap.Round,
+ StrokeJoin = SKStrokeJoin.Round
+ };
+
+ private SKPicture? cachedPicture;
+ private int nodeCount;
+ private bool pictureDirty = true;
+
+ private Stroke? activeStroke;
+ private SKPoint pointer;
+ private bool erasing;
+ private bool erasePending;
+
+ private SKRect drawingArea;
+ private SKSize size;
+
+ private bool CanClear => strokes.Count > 0 || activeStroke is not null || erasing || erasePending;
+
+ public void Draw(SKCanvas canvas, float width, float height)
+ {
+ EnsureLayout(width, height);
+
+ if (erasePending)
+ {
+ ApplyEraser();
+ }
+
+ fillPaint.Color = Surface;
+ canvas.DrawRect(0.0f, 0.0f, width, height, fillPaint);
+
+ if (pictureDirty)
+ {
+ cachedPicture?.Dispose();
+ cachedPicture = RecordCanvas();
+ pictureDirty = false;
+ }
+
+ canvas.Save();
+ canvas.ClipRect(drawingArea);
+ canvas.DrawPicture(cachedPicture);
+
+ if (activeStroke is not null)
+ {
+ strokePaint.Color = activeStroke.Color;
+ strokePaint.StrokeWidth = activeStroke.Width;
+ canvas.DrawPath(activeStroke.Path, strokePaint);
+ canvas.DrawLine(activeStroke.TailStart, activeStroke.TailEnd, strokePaint);
+ }
+
+ if (erasing && drawingArea.Contains(pointer.X, pointer.Y))
+ {
+ DrawEraserCursor(canvas);
+ }
+
+ canvas.Restore();
+
+ toolbar.Draw(canvas, strokes.Count, nodeCount, CanClear);
+ }
+
+ public void PointerMove(SKPoint position)
+ {
+ pointer = position;
+
+ if (erasing)
+ {
+ eraserBuilder.LineTo(position);
+ erasePending = true;
+ }
+ else
+ {
+ activeStroke?.Add(position);
+ }
+ }
+
+ public void PointerDown(SKPoint position, bool erase)
+ {
+ if (!erase && toolbar.IsClearButton(position))
+ {
+ pointer = position;
+ ClearCanvas();
+ }
+ else if (activeStroke is null && !erasing)
+ {
+ pointer = position;
+
+ if (!erase && position.Y <= Toolbar.ToolbarHeight)
+ {
+ toolbar.SelectAt(position);
+ }
+ else if (drawingArea.Contains(position.X, position.Y))
+ {
+ if (erase)
+ {
+ erasing = true;
+ eraserBuilder.MoveTo(position);
+ eraserBuilder.LineTo(position);
+ erasePending = true;
+ }
+ else
+ {
+ activeStroke = new(toolbar.SelectedColor, toolbar.SelectedStrokeWidth);
+ activeStroke.Add(position);
+ }
+ }
+ }
+ }
+
+ public void PointerUp(bool erase)
+ {
+ if (erase && erasing)
+ {
+ erasing = false;
+
+ if (erasePending)
+ {
+ ApplyEraser();
+ }
+ }
+ else if (!erase && activeStroke is not null)
+ {
+ activeStroke.Complete(strokePaint);
+ strokes.Add(activeStroke);
+ activeStroke = null;
+ pictureDirty = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (Stroke stroke in strokes)
+ {
+ stroke.Dispose();
+ }
+
+ strokes.Clear();
+ activeStroke?.Dispose();
+ cachedPicture?.Dispose();
+
+ eraserBuilder.Dispose();
+ strokePaint.Dispose();
+ fillPaint.Dispose();
+
+ toolbar.Dispose();
+ }
+
+ private void EnsureLayout(float width, float height)
+ {
+ if (width == size.Width && height == size.Height)
+ {
+ return;
+ }
+
+ size = new(width, height);
+ drawingArea = new(0.0f, Toolbar.ToolbarHeight, width, MathF.Max(Toolbar.ToolbarHeight, height - Toolbar.StatusHeight));
+
+ toolbar.Resize(width, height);
+ pictureDirty = true;
+ }
+
+ private SKPicture RecordCanvas()
+ {
+ const float spacing = 32.0f;
+
+ using SKPictureRecorder recorder = new();
+ SKCanvas canvas = recorder.BeginRecording(drawingArea);
+ int nodes = 0;
+
+ strokePaint.Color = Grid;
+ strokePaint.StrokeWidth = 1.0f;
+
+ for (float x = drawingArea.Left + spacing; x < drawingArea.Right; x += spacing)
+ {
+ canvas.DrawLine(x, drawingArea.Top, x, drawingArea.Bottom, strokePaint);
+ }
+
+ for (float y = drawingArea.Top + spacing; y < drawingArea.Bottom; y += spacing)
+ {
+ canvas.DrawLine(drawingArea.Left, y, drawingArea.Right, y, strokePaint);
+ }
+
+ foreach (Stroke stroke in strokes)
+ {
+ fillPaint.Color = stroke.Color;
+ canvas.DrawPath(stroke.Path, fillPaint);
+ nodes += stroke.NodeCount;
+ }
+
+ nodeCount = nodes;
+
+ return recorder.EndRecording();
+ }
+
+ private void DrawEraserCursor(SKCanvas canvas)
+ {
+ strokePaint.Color = Cursor;
+ strokePaint.StrokeWidth = 1.5f;
+ canvas.DrawCircle(pointer, EraserRadius, strokePaint);
+ }
+
+ private void ClearCanvas()
+ {
+ if (CanClear)
+ {
+ foreach (Stroke stroke in strokes)
+ {
+ stroke.Dispose();
+ }
+
+ strokes.Clear();
+ activeStroke?.Dispose();
+ activeStroke = null;
+ eraserBuilder.Reset();
+ erasing = false;
+ erasePending = false;
+ pictureDirty = true;
+ }
+ }
+
+ private void ApplyEraser()
+ {
+ using SKPath centerline = eraserBuilder.Detach();
+
+ if (erasing)
+ {
+ eraserBuilder.MoveTo(pointer);
+ }
+
+ strokePaint.StrokeWidth = EraserRadius * 2.0f;
+
+ using SKPath eraser = strokePaint.GetFillPath(centerline)!;
+ SKRect eraserBounds = eraser.Bounds;
+
+ erasePending = false;
+
+ for (int index = strokes.Count - 1; index >= 0; index--)
+ {
+ Stroke stroke = strokes[index];
+
+ if (stroke.Erase(eraser, eraserBounds))
+ {
+ if (stroke.IsEmpty)
+ {
+ stroke.Dispose();
+ strokes.RemoveAt(index);
+ }
+
+ pictureDirty = true;
+ }
+ }
+ }
+}
diff --git a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
new file mode 100644
index 00000000..f1dca18b
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
@@ -0,0 +1,95 @@
+using System.Numerics;
+using Silk.NET.Input;
+using Zenith.NET;
+using Zenith.NET.Extensions.Skia;
+
+namespace InkCanvas.Drawing;
+
+internal class CanvasController : IDisposable
+{
+ private readonly GraphicsContext context;
+ private readonly Canvas canvas = new();
+
+ private SKTexture texture;
+
+ public CanvasController(GraphicsContext context, IInputContext input, uint width, uint height)
+ {
+ this.context = context;
+
+ texture = CreateTexture(width, height);
+
+ IMouse mouse = input.Mice[0];
+ mouse.MouseDown += OnMouseDown;
+ mouse.MouseUp += OnMouseUp;
+ mouse.MouseMove += OnMouseMove;
+ }
+
+ public void Render(CommandBuffer commandBuffer, Texture target, Vector2 dpiScale)
+ {
+ float width = texture.Desc.Width / dpiScale.X;
+ float height = texture.Desc.Height / dpiScale.Y;
+
+ texture.Render((skiaCanvas) =>
+ {
+ skiaCanvas.Save();
+ skiaCanvas.Scale(dpiScale.X, dpiScale.Y);
+
+ canvas.Draw(skiaCanvas, width, height);
+
+ skiaCanvas.Restore();
+ });
+
+ commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, TextureLayout.CopySrc);
+ commandBuffer.CopyTexture(texture, default, default, target, default, default, new()
+ {
+ Width = texture.Desc.Width,
+ Height = texture.Desc.Height,
+ Depth = 1
+ });
+ commandBuffer.Transition(texture, default, TextureLayout.CopySrc, TextureLayout.ColorAttachment);
+ }
+
+ public void Resize(uint width, uint height)
+ {
+ texture.Dispose();
+ texture = CreateTexture(width, height);
+ }
+
+ public void Dispose()
+ {
+ canvas.Dispose();
+ texture.Dispose();
+ }
+
+ private SKTexture CreateTexture(uint width, uint height)
+ {
+ return context.CreateSKTexture(new()
+ {
+ Format = PixelFormat.B8G8R8A8UNorm,
+ Width = width,
+ Height = height,
+ SampleCount = SampleCount.Count1
+ });
+ }
+
+ private void OnMouseDown(IMouse mouse, MouseButton button)
+ {
+ if (button is MouseButton.Left or MouseButton.Right)
+ {
+ canvas.PointerDown(new(mouse.Position.X, mouse.Position.Y), button is MouseButton.Right);
+ }
+ }
+
+ private void OnMouseUp(IMouse mouse, MouseButton button)
+ {
+ if (button is MouseButton.Left or MouseButton.Right)
+ {
+ canvas.PointerUp(button is MouseButton.Right);
+ }
+ }
+
+ private void OnMouseMove(IMouse mouse, Vector2 position)
+ {
+ canvas.PointerMove(new(position.X, position.Y));
+ }
+}
diff --git a/sources/Experiments/InkCanvas/Stroke.cs b/sources/Experiments/InkCanvas/Drawing/Stroke.cs
similarity index 95%
rename from sources/Experiments/InkCanvas/Stroke.cs
rename to sources/Experiments/InkCanvas/Drawing/Stroke.cs
index 5d711078..4ebd5b59 100644
--- a/sources/Experiments/InkCanvas/Stroke.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Stroke.cs
@@ -1,13 +1,14 @@
-using SkiaSharp;
+using SkiaSharp;
-namespace InkCanvas;
+namespace InkCanvas.Drawing;
-internal sealed class Stroke(SKColor color, float width) : IDisposable
+internal class Stroke(SKColor color, float width) : IDisposable
{
private const float MinimumPointDistance = 1.5f;
private SKPathBuilder? builder = new();
private SKPath? path;
+
private SKRect bounds;
private SKPoint tailStart;
private SKPoint tailEnd;
@@ -101,4 +102,4 @@ private static float DistanceSquared(SKPoint first, SKPoint second)
return (dx * dx) + (dy * dy);
}
-}
\ No newline at end of file
+}
diff --git a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
new file mode 100644
index 00000000..92cb3095
--- /dev/null
+++ b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
@@ -0,0 +1,201 @@
+using SkiaSharp;
+
+namespace InkCanvas.Drawing;
+
+internal class Toolbar : IDisposable
+{
+ public const float ToolbarHeight = 64.0f;
+ public const float StatusHeight = 34.0f;
+
+ private const float SwatchSize = 30.0f;
+ private const float SwatchGap = 12.0f;
+ private const float ButtonWidth = 64.0f;
+
+ private static readonly SKColor Panel = new(31, 34, 43);
+ private static readonly SKColor Divider = new(48, 52, 63);
+ private static readonly SKColor Label = new(150, 158, 172);
+ private static readonly SKColor Highlight = new(238, 242, 248);
+ private static readonly SKColor Selected = new(58, 64, 78);
+
+ private static readonly SKColor[] Palette =
+ [
+ new(238, 242, 248),
+ new(236, 108, 96),
+ new(238, 178, 74),
+ new(96, 196, 154),
+ new(102, 158, 240),
+ new(178, 134, 234)
+ ];
+
+ private static readonly float[] StrokeWidths = [2.0f, 4.0f, 8.0f, 16.0f];
+
+ private readonly SKRect[] swatchRects = new SKRect[Palette.Length];
+ private readonly SKRect[] strokeWidthRects = new SKRect[StrokeWidths.Length];
+
+ private readonly SKFont labelFont;
+ private readonly SKPaint fillPaint = new() { IsAntialias = true };
+ private readonly SKPaint strokePaint = new()
+ {
+ IsAntialias = true,
+ Style = SKPaintStyle.Stroke
+ };
+
+ private SKRect clearRect;
+ private SKSize size;
+
+ private int colorIndex;
+ private int strokeWidthIndex = 1;
+
+ public Toolbar()
+ {
+ string family = OperatingSystem.IsMacOS() ? "SF Pro Text" : OperatingSystem.IsWindows() ? "Segoe UI" : "Noto Sans";
+
+ using SKTypeface typeface = SKTypeface.FromFamilyName(family, SKFontStyle.Normal);
+
+ labelFont = new(typeface, 12.0f)
+ {
+ Edging = SKFontEdging.SubpixelAntialias,
+ Hinting = SKFontHinting.Slight,
+ Subpixel = true
+ };
+ }
+
+ public SKColor SelectedColor => Palette[colorIndex];
+
+ public float SelectedStrokeWidth => StrokeWidths[strokeWidthIndex];
+
+ public void Resize(float width, float height)
+ {
+ const float top = (ToolbarHeight - SwatchSize) * 0.5f;
+ const float bottom = top + SwatchSize;
+
+ size = new(width, height);
+
+ for (int index = 0; index < swatchRects.Length; index++)
+ {
+ float left = SwatchGap + (index * (SwatchSize + SwatchGap));
+ swatchRects[index] = new(left, top, left + SwatchSize, bottom);
+ }
+
+ float strokeWidthLeft = swatchRects[^1].Right + (SwatchGap * 2.0f);
+
+ for (int index = 0; index < strokeWidthRects.Length; index++)
+ {
+ float left = strokeWidthLeft + (index * (SwatchSize + SwatchGap));
+ strokeWidthRects[index] = new(left, top, left + SwatchSize, bottom);
+ }
+
+ float clearLeft = MathF.Max(strokeWidthRects[^1].Right + (SwatchGap * 2.0f), width - SwatchGap - ButtonWidth);
+ clearRect = new(clearLeft, top, clearLeft + ButtonWidth, bottom);
+ }
+
+ public void Draw(SKCanvas canvas, int strokeCount, int nodeCount, bool canClear)
+ {
+ DrawToolbar(canvas, canClear);
+ DrawStatus(canvas, strokeCount, nodeCount);
+ }
+
+ public bool IsClearButton(SKPoint position)
+ {
+ return clearRect.Contains(position.X, position.Y);
+ }
+
+ public void SelectAt(SKPoint position)
+ {
+ int swatch = IndexAt(swatchRects, position);
+ int strokeWidth = IndexAt(strokeWidthRects, position);
+
+ if (swatch >= 0)
+ {
+ colorIndex = swatch;
+ }
+ else if (strokeWidth >= 0)
+ {
+ strokeWidthIndex = strokeWidth;
+ }
+ }
+
+ public void Dispose()
+ {
+ strokePaint.Dispose();
+ fillPaint.Dispose();
+ labelFont.Dispose();
+ }
+
+ private void DrawToolbar(SKCanvas canvas, bool canClear)
+ {
+ fillPaint.Color = Panel;
+ canvas.DrawRect(0.0f, 0.0f, size.Width, ToolbarHeight, fillPaint);
+
+ fillPaint.Color = Divider;
+ canvas.DrawRect(0.0f, ToolbarHeight - 1.0f, size.Width, 1.0f, fillPaint);
+
+ for (int index = 0; index < Palette.Length; index++)
+ {
+ SKRect swatch = swatchRects[index];
+
+ fillPaint.Color = Palette[index];
+ canvas.DrawRoundRect(swatch, 6.0f, 6.0f, fillPaint);
+
+ if (index == colorIndex)
+ {
+ strokePaint.Color = Highlight;
+ strokePaint.StrokeWidth = 2.0f;
+ canvas.DrawRoundRect(SKRect.Inflate(swatch, 4.0f, 4.0f), 9.0f, 9.0f, strokePaint);
+ }
+ }
+
+ for (int index = 0; index < StrokeWidths.Length; index++)
+ {
+ SKRect slot = strokeWidthRects[index];
+
+ fillPaint.Color = index == strokeWidthIndex ? Selected : Panel;
+ canvas.DrawRoundRect(slot, 6.0f, 6.0f, fillPaint);
+
+ fillPaint.Color = Palette[colorIndex];
+ canvas.DrawCircle(slot.MidX, slot.MidY, StrokeWidths[index] * 0.5f, fillPaint);
+ }
+
+ SKColor accent = canClear ? Label : Divider;
+
+ fillPaint.Color = Panel;
+ canvas.DrawRoundRect(clearRect, 6.0f, 6.0f, fillPaint);
+
+ strokePaint.Color = accent;
+ strokePaint.StrokeWidth = 1.5f;
+ canvas.DrawRoundRect(clearRect, 6.0f, 6.0f, strokePaint);
+
+ fillPaint.Color = accent;
+ canvas.DrawText("CLEAR", clearRect.MidX, clearRect.MidY + 4.0f, SKTextAlign.Center, labelFont, fillPaint);
+ }
+
+ private void DrawStatus(SKCanvas canvas, int strokeCount, int nodeCount)
+ {
+ float top = size.Height - StatusHeight;
+
+ fillPaint.Color = Panel;
+ canvas.DrawRect(0.0f, top, size.Width, StatusHeight, fillPaint);
+
+ fillPaint.Color = Divider;
+ canvas.DrawRect(0.0f, top, size.Width, 1.0f, fillPaint);
+
+ float baseline = top + (StatusHeight * 0.5f) + 4.0f;
+
+ fillPaint.Color = Label;
+ canvas.DrawText($"STROKES {strokeCount} NODES {nodeCount}", SwatchGap, baseline, SKTextAlign.Left, labelFont, fillPaint);
+ canvas.DrawText("DRAG TO DRAW RIGHT DRAG TO ERASE", size.Width - SwatchGap, baseline, SKTextAlign.Right, labelFont, fillPaint);
+ }
+
+ private static int IndexAt(SKRect[] rects, SKPoint position)
+ {
+ for (int index = 0; index < rects.Length; index++)
+ {
+ if (rects[index].Contains(position.X, position.Y))
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+}
From 70a10ca09d99506a2edb505c9990ef9c6502ca7b Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 21:48:29 +0800
Subject: [PATCH 25/50] Add UTF-8 BOM and expose Context property
Added UTF-8 BOM to Canvas.cs, CanvasController.cs, and Toolbar.cs. Replaced private field 'context' with public read-only property 'Context' in CanvasController.cs and updated all references accordingly.
---
sources/Experiments/InkCanvas/Drawing/Canvas.cs | 2 +-
.../Experiments/InkCanvas/Drawing/CanvasController.cs | 9 +++++----
sources/Experiments/InkCanvas/Drawing/Toolbar.cs | 2 +-
3 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/sources/Experiments/InkCanvas/Drawing/Canvas.cs b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
index d60e3fbf..098b6035 100644
--- a/sources/Experiments/InkCanvas/Drawing/Canvas.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
@@ -1,4 +1,4 @@
-using SkiaSharp;
+using SkiaSharp;
namespace InkCanvas.Drawing;
diff --git a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
index f1dca18b..353dd33f 100644
--- a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
+++ b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using Silk.NET.Input;
using Zenith.NET;
using Zenith.NET.Extensions.Skia;
@@ -7,14 +7,13 @@ namespace InkCanvas.Drawing;
internal class CanvasController : IDisposable
{
- private readonly GraphicsContext context;
private readonly Canvas canvas = new();
private SKTexture texture;
public CanvasController(GraphicsContext context, IInputContext input, uint width, uint height)
{
- this.context = context;
+ Context = context;
texture = CreateTexture(width, height);
@@ -24,6 +23,8 @@ public CanvasController(GraphicsContext context, IInputContext input, uint width
mouse.MouseMove += OnMouseMove;
}
+ public GraphicsContext Context { get; }
+
public void Render(CommandBuffer commandBuffer, Texture target, Vector2 dpiScale)
{
float width = texture.Desc.Width / dpiScale.X;
@@ -63,7 +64,7 @@ public void Dispose()
private SKTexture CreateTexture(uint width, uint height)
{
- return context.CreateSKTexture(new()
+ return Context.CreateSKTexture(new()
{
Format = PixelFormat.B8G8R8A8UNorm,
Width = width,
diff --git a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
index 92cb3095..7e3c6f49 100644
--- a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
@@ -1,4 +1,4 @@
-using SkiaSharp;
+using SkiaSharp;
namespace InkCanvas.Drawing;
From f0e10266c07702c1daf90602991333728aa34ec0 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 22:50:17 +0800
Subject: [PATCH 26/50] Align experiment initializer ordering
---
sources/Experiments/CornellBox/App.cs | 8 ++++----
sources/Experiments/FluidTank/App.cs | 8 ++++----
sources/Experiments/InkCanvas/App.cs | 4 ++--
sources/Experiments/InkCanvas/Drawing/Toolbar.cs | 4 ++--
4 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/sources/Experiments/CornellBox/App.cs b/sources/Experiments/CornellBox/App.cs
index 2097c79d..bbebc9c7 100644
--- a/sources/Experiments/CornellBox/App.cs
+++ b/sources/Experiments/CornellBox/App.cs
@@ -49,9 +49,9 @@ static App()
window = Window.Create(WindowOptions.Default with
{
+ Size = new(1280, 720),
API = GraphicsAPI.None,
- Title = "Cornell Box - Zenith.NET",
- Size = new(1280, 720)
+ Title = "Cornell Box - Zenith.NET"
});
window.Initialize();
window.Center();
@@ -86,8 +86,8 @@ static App()
camera = new(input, Matrix4x4.CreateTranslation(278.0f, 273.0f, -800.0f))
{
- Speed = 240.0f,
- FarPlane = 2000.0f
+ FarPlane = 2000.0f,
+ Speed = 240.0f
};
rasterizer = new();
diff --git a/sources/Experiments/FluidTank/App.cs b/sources/Experiments/FluidTank/App.cs
index 876c53fd..36eb12ba 100644
--- a/sources/Experiments/FluidTank/App.cs
+++ b/sources/Experiments/FluidTank/App.cs
@@ -44,9 +44,9 @@ static App()
window = Window.Create(WindowOptions.Default with
{
+ Size = new(1280, 720),
API = GraphicsAPI.None,
- Title = "Fluid Tank - Zenith.NET",
- Size = new(1280, 720)
+ Title = "Fluid Tank - Zenith.NET"
});
window.Initialize();
window.Center();
@@ -86,10 +86,10 @@ static App()
camera = new(input, new(9.2f, 5.3f, -10.8f), new(0.0f, 1.45f, 0.0f))
{
- Speed = 4.0f,
NearPlane = 0.05f,
FarPlane = 80.0f,
- Fov = 48.0f
+ Fov = 48.0f,
+ Speed = 4.0f
};
renderer = new();
diff --git a/sources/Experiments/InkCanvas/App.cs b/sources/Experiments/InkCanvas/App.cs
index e8f9c946..f30122df 100644
--- a/sources/Experiments/InkCanvas/App.cs
+++ b/sources/Experiments/InkCanvas/App.cs
@@ -41,9 +41,9 @@ static App()
window = Window.Create(WindowOptions.Default with
{
+ Size = new(1280, 800),
API = GraphicsAPI.None,
- Title = "Ink Canvas - Zenith.NET",
- Size = new(1280, 800)
+ Title = "Ink Canvas - Zenith.NET"
});
window.Initialize();
window.Center();
diff --git a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
index 7e3c6f49..4826e9d3 100644
--- a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
@@ -54,9 +54,9 @@ public Toolbar()
labelFont = new(typeface, 12.0f)
{
+ Subpixel = true,
Edging = SKFontEdging.SubpixelAntialias,
- Hinting = SKFontHinting.Slight,
- Subpixel = true
+ Hinting = SKFontHinting.Slight
};
}
From 694b4ed133c1a2ca45f2046ff72b39db4da00005 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Mon, 3 Aug 2026 23:01:48 +0800
Subject: [PATCH 27/50] Add Particles to FluidViewMode and refactor rendering
- Introduce Particles value to FluidViewMode enum
- Refactor BeginRenderPass with single-line color attachments
- Simplify reflection texture assignment with ternary operator
---
sources/Experiments/FluidTank/FluidTankRenderer.cs | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/sources/Experiments/FluidTank/FluidTankRenderer.cs b/sources/Experiments/FluidTank/FluidTankRenderer.cs
index 068a679d..a3af1f70 100644
--- a/sources/Experiments/FluidTank/FluidTankRenderer.cs
+++ b/sources/Experiments/FluidTank/FluidTankRenderer.cs
@@ -10,6 +10,7 @@ namespace FluidTank;
internal enum FluidViewMode
{
Water,
+
Particles
}
@@ -339,11 +340,7 @@ public void RenderFluid(CommandBuffer commandBuffer)
commandBuffer.Transition(smoothDepthA, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
commandBuffer.Transition(fluidAttributes, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
- commandBuffer.BeginRenderPass(
- [
- ColorAttachment.Clear(smoothDepthA, Vector4.Zero),
- ColorAttachment.Clear(fluidAttributes, Vector4.Zero)
- ], DepthStencilAttachment.Clear(reconstructionDepth, 1.0f, 0));
+ commandBuffer.BeginRenderPass([ColorAttachment.Clear(smoothDepthA, Vector4.Zero), ColorAttachment.Clear(fluidAttributes, Vector4.Zero)], DepthStencilAttachment.Clear(reconstructionDepth, 1.0f, 0));
commandBuffer.SetPipeline(fluidDepthPipeline);
commandBuffer.SetConstantBuffer(surfaceConstantBuffer, 0);
commandBuffer.Draw(4, simulation.ParticleCount, 0, 0);
@@ -428,9 +425,7 @@ public void Resize(uint width, uint height)
smoothDepthB = GraphicsHelper.CreateTexture(PixelFormat.R32Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage);
smoothThicknessA = GraphicsHelper.CreateTexture(PixelFormat.R16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage | TextureUsages.ColorAttachment);
smoothThicknessB = GraphicsHelper.CreateTexture(PixelFormat.R16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage);
- reflection = App.Context.Capabilities.RayTracingSupported
- ? GraphicsHelper.CreateTexture(PixelFormat.R16G16B16A16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage)
- : null;
+ reflection = App.Context.Capabilities.RayTracingSupported ? GraphicsHelper.CreateTexture(PixelFormat.R16G16B16A16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage) : null;
}
public void Dispose()
From f831af328c21bd385d0384772e8ab957d39394cd Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 09:22:18 +0800
Subject: [PATCH 28/50] Update graphics context flush and submit usage
Changed GRContext.Flush to use surface argument and added GRContext.Submit(true) call, replacing previous boolean arguments. This aligns with updated API usage and may improve resource management.
---
sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
index e2afc93d..bfe54286 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
@@ -104,7 +104,8 @@ public void Render(SKSurface surface, Action render)
render(surface.Canvas);
- GRContext.Flush(true, true);
+ GRContext.Flush(surface);
+ GRContext.Submit(true);
}
protected override void Destroy()
From 4298d3ce5577ddeda46a0eb62146bcd2a2cdd909 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 09:47:49 +0800
Subject: [PATCH 29/50] Refactor multisampling config in texture creation
Replaces SampleCount enum with IsMultisamplingEnabled boolean in SKTextureDesc. CanvasController now sets IsMultisamplingEnabled when creating textures. SKSurface uses sample count 4 if multisampling is enabled, otherwise 1. Removes SampleCount from SKTextureDesc.
---
sources/Experiments/InkCanvas/Drawing/CanvasController.cs | 2 +-
sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs | 2 +-
sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
index 353dd33f..7f0a6a26 100644
--- a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
+++ b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
@@ -69,7 +69,7 @@ private SKTexture CreateTexture(uint width, uint height)
Format = PixelFormat.B8G8R8A8UNorm,
Width = width,
Height = height,
- SampleCount = SampleCount.Count1
+ IsMultisamplingEnabled = true
});
}
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
index 40d4301b..69485538 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
@@ -24,7 +24,7 @@ internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
Usages = TextureUsages.Sampled | TextureUsages.ColorAttachment | TextureUsages.TransferSrc | TextureUsages.TransferDst
}));
- surface = SKSurface.Create(renderer.GRContext, backendTexture, GRSurfaceOrigin.TopLeft, (int)SKFormats.Skia(desc.SampleCount), SKFormats.Skia(desc.Format));
+ surface = SKSurface.Create(renderer.GRContext, backendTexture, GRSurfaceOrigin.TopLeft, desc.IsMultisamplingEnabled ? 4 : 1, SKFormats.Skia(desc.Format));
}
internal SKRenderer Renderer { get; }
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs
index 3f90d030..65c29c7f 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTextureDesc.cs
@@ -8,5 +8,5 @@ public struct SKTextureDesc
public uint Height;
- public SampleCount SampleCount;
+ public bool IsMultisamplingEnabled;
}
From 9f3639ecc487b2b4c7136ba30548f7210cabdce2 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 15:48:38 +0800
Subject: [PATCH 30/50] Fix Skia texture layout tracking
---
.../InkCanvas/Drawing/CanvasController.cs | 4 ++--
.../Zenith.NET.Extensions.Skia/SKFormats.cs | 20 +++++++++++++++++
.../Zenith.NET.Extensions.Skia/SKRenderer.cs | 7 ++++--
.../Zenith.NET.Extensions.Skia/SKTexture.cs | 22 +++++++++++++++++--
4 files changed, 47 insertions(+), 6 deletions(-)
diff --git a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
index 7f0a6a26..feb3b9ab 100644
--- a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
+++ b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
@@ -40,14 +40,14 @@ public void Render(CommandBuffer commandBuffer, Texture target, Vector2 dpiScale
skiaCanvas.Restore();
});
- commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, TextureLayout.CopySrc);
+ commandBuffer.Transition(texture, default, texture.Layout, TextureLayout.CopySrc);
commandBuffer.CopyTexture(texture, default, default, target, default, default, new()
{
Width = texture.Desc.Width,
Height = texture.Desc.Height,
Depth = 1
});
- commandBuffer.Transition(texture, default, TextureLayout.CopySrc, TextureLayout.ColorAttachment);
+ commandBuffer.Transition(texture, default, TextureLayout.CopySrc, texture.Layout);
}
public void Resize(uint width, uint height)
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
index ae4c8572..b29cff7e 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
@@ -48,6 +48,16 @@ public static uint DirectX12(PixelFormat format)
};
}
+ public static uint DirectX12(TextureLayout textureLayout)
+ {
+ return textureLayout switch
+ {
+ TextureLayout.ColorAttachment => 0x4,
+ TextureLayout.ResolveDst => 0x1000,
+ _ => default
+ };
+ }
+
public static uint Vulkan(PixelFormat format)
{
return format switch
@@ -99,4 +109,14 @@ public static uint Vulkan(TextureUsages textureUsages)
return result;
}
+
+ public static uint Vulkan(TextureLayout textureLayout)
+ {
+ return textureLayout switch
+ {
+ TextureLayout.ColorAttachment => 2,
+ TextureLayout.ResolveDst => 7,
+ _ => default
+ };
+ }
}
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
index bfe54286..43f69637 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
@@ -118,7 +118,7 @@ protected override void Destroy()
}
}
- public GRBackendTexture CreateBackendTexture(Texture texture)
+ public GRBackendTexture CreateBackendTexture(Texture texture, TextureLayout layout)
{
switch (Context.GraphicsApi)
{
@@ -126,9 +126,11 @@ public GRBackendTexture CreateBackendTexture(Texture texture)
return new((int)texture.Desc.Width, (int)texture.Desc.Height, new GRD3DTextureResourceInfo
{
Resource = texture.GetNativeObject(NativeObjectType.D3D12Resource),
+ ResourceState = SKFormats.DirectX12(layout),
Format = SKFormats.DirectX12(texture.Desc.Format),
SampleCount = 1,
- LevelCount = 1
+ LevelCount = 1,
+ SampleQualityPattern = layout is TextureLayout.ResolveDst ? uint.MaxValue : 0
});
case GraphicsApi.Metal:
@@ -151,6 +153,7 @@ public GRBackendTexture CreateBackendTexture(Texture texture)
},
Format = SKFormats.Vulkan(texture.Desc.Format),
ImageUsageFlags = SKFormats.Vulkan(texture.Desc.Usages),
+ ImageLayout = SKFormats.Vulkan(layout),
SampleCount = 1,
LevelCount = 1,
CurrentQueueFamily = concurrent ? uint.MaxValue : graphicsQueueFamily,
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
index 69485538..0d00571c 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
@@ -10,8 +10,9 @@ public class SKTexture : DisposableObject
internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
{
Renderer = renderer;
+ Layout = desc.IsMultisamplingEnabled ? TextureLayout.ResolveDst : TextureLayout.ColorAttachment;
- using GRBackendTexture backendTexture = renderer.CreateBackendTexture(texture = renderer.Context.CreateTexture(new()
+ texture = renderer.Context.CreateTexture(new()
{
Type = TextureType.Texture2D,
Format = desc.Format,
@@ -22,7 +23,22 @@ internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
ArrayLayers = 1,
SampleCount = SampleCount.Count1,
Usages = TextureUsages.Sampled | TextureUsages.ColorAttachment | TextureUsages.TransferSrc | TextureUsages.TransferDst
- }));
+ });
+
+ CommandBuffer commandBuffer = renderer.Context.GraphicsQueue.CommandBuffer();
+
+ commandBuffer.Transition(texture, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
+ commandBuffer.BeginRenderPass([ColorAttachment.Clear(texture, default)], null);
+ commandBuffer.EndRenderPass();
+
+ if (Layout is not TextureLayout.ColorAttachment)
+ {
+ commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, Layout);
+ }
+
+ commandBuffer.Submit().Wait();
+
+ using GRBackendTexture backendTexture = renderer.CreateBackendTexture(texture, Layout);
surface = SKSurface.Create(renderer.GRContext, backendTexture, GRSurfaceOrigin.TopLeft, desc.IsMultisamplingEnabled ? 4 : 1, SKFormats.Skia(desc.Format));
}
@@ -31,6 +47,8 @@ internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
public ref readonly TextureDesc Desc => ref texture.Desc;
+ public TextureLayout Layout { get; }
+
public void Render(Action render)
{
Renderer.Render(surface, render);
From 73e3b03bee6cba23b2a2a9eab3b937391a4abba7 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 16:00:55 +0800
Subject: [PATCH 31/50] Refactor layout init and use Vector4.Zero for clear
color
- Import System.Numerics for Vector4 usage.
- Move Layout initialization into constructor and set conditionally.
- Use Vector4.Zero instead of default in BeginRenderPass.
- Assign Layout inline within the if condition for clarity.
---
.../Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
index 0d00571c..c290b426 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
@@ -1,4 +1,5 @@
-using SkiaSharp;
+using System.Numerics;
+using SkiaSharp;
namespace Zenith.NET.Extensions.Skia;
@@ -10,7 +11,6 @@ public class SKTexture : DisposableObject
internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
{
Renderer = renderer;
- Layout = desc.IsMultisamplingEnabled ? TextureLayout.ResolveDst : TextureLayout.ColorAttachment;
texture = renderer.Context.CreateTexture(new()
{
@@ -28,10 +28,11 @@ internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
CommandBuffer commandBuffer = renderer.Context.GraphicsQueue.CommandBuffer();
commandBuffer.Transition(texture, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
- commandBuffer.BeginRenderPass([ColorAttachment.Clear(texture, default)], null);
+
+ commandBuffer.BeginRenderPass([ColorAttachment.Clear(texture, Vector4.Zero)], null);
commandBuffer.EndRenderPass();
- if (Layout is not TextureLayout.ColorAttachment)
+ if ((Layout = desc.IsMultisamplingEnabled ? TextureLayout.ResolveDst : TextureLayout.ColorAttachment) is not TextureLayout.ColorAttachment)
{
commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, Layout);
}
From 9a28eb1fca19cd39d705f1bc22d0ee6a0449d502 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 16:21:46 +0800
Subject: [PATCH 32/50] Simplify Skia backend texture configuration
---
.../Zenith.NET.Extensions.Skia/SKFormats.cs | 74 +++++++------------
.../Zenith.NET.Extensions.Skia/SKRenderer.cs | 8 +-
.../Zenith.NET.Extensions.Skia/SKTexture.cs | 2 +-
3 files changed, 32 insertions(+), 52 deletions(-)
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
index b29cff7e..7a505372 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
@@ -4,38 +4,9 @@ namespace Zenith.NET.Extensions.Skia;
internal static class SKFormats
{
- public static SKColorType Skia(PixelFormat format)
+ public static uint DirectX12(PixelFormat pixelFormat)
{
- return format switch
- {
- PixelFormat.R8UNorm => SKColorType.Gray8,
- PixelFormat.R16Float => SKColorType.AlphaF16,
- PixelFormat.R8G8B8A8UNorm => SKColorType.Rgba8888,
- PixelFormat.R8G8B8A8SRgb => SKColorType.Srgba8888,
- PixelFormat.R16G16B16A16Float => SKColorType.RgbaF16,
- PixelFormat.R32G32B32A32Float => SKColorType.RgbaF32,
- PixelFormat.B8G8R8A8UNorm => SKColorType.Bgra8888,
- _ => default
- };
- }
-
- public static uint Skia(SampleCount sampleCount)
- {
- return sampleCount switch
- {
- SampleCount.Count1 => 1,
- SampleCount.Count2 => 2,
- SampleCount.Count4 => 4,
- SampleCount.Count8 => 8,
- SampleCount.Count16 => 16,
- SampleCount.Count32 => 32,
- _ => default
- };
- }
-
- public static uint DirectX12(PixelFormat format)
- {
- return format switch
+ return pixelFormat switch
{
PixelFormat.R8UNorm => 61,
PixelFormat.R16Float => 54,
@@ -48,19 +19,24 @@ public static uint DirectX12(PixelFormat format)
};
}
- public static uint DirectX12(TextureLayout textureLayout)
+ public static SKColorType Skia(PixelFormat pixelFormat)
{
- return textureLayout switch
+ return pixelFormat switch
{
- TextureLayout.ColorAttachment => 0x4,
- TextureLayout.ResolveDst => 0x1000,
+ PixelFormat.R8UNorm => SKColorType.Gray8,
+ PixelFormat.R16Float => SKColorType.AlphaF16,
+ PixelFormat.R8G8B8A8UNorm => SKColorType.Rgba8888,
+ PixelFormat.R8G8B8A8SRgb => SKColorType.Srgba8888,
+ PixelFormat.R16G16B16A16Float => SKColorType.RgbaF16,
+ PixelFormat.R32G32B32A32Float => SKColorType.RgbaF32,
+ PixelFormat.B8G8R8A8UNorm => SKColorType.Bgra8888,
_ => default
};
}
- public static uint Vulkan(PixelFormat format)
+ public static uint Vulkan(PixelFormat pixelFormat)
{
- return format switch
+ return pixelFormat switch
{
PixelFormat.R8UNorm => 9,
PixelFormat.R16Float => 76,
@@ -73,6 +49,20 @@ public static uint Vulkan(PixelFormat format)
};
}
+ public static uint Skia(SampleCount sampleCount)
+ {
+ return sampleCount switch
+ {
+ SampleCount.Count1 => 1,
+ SampleCount.Count2 => 2,
+ SampleCount.Count4 => 4,
+ SampleCount.Count8 => 8,
+ SampleCount.Count16 => 16,
+ SampleCount.Count32 => 32,
+ _ => default
+ };
+ }
+
public static uint Vulkan(TextureUsages textureUsages)
{
uint result = default;
@@ -109,14 +99,4 @@ public static uint Vulkan(TextureUsages textureUsages)
return result;
}
-
- public static uint Vulkan(TextureLayout textureLayout)
- {
- return textureLayout switch
- {
- TextureLayout.ColorAttachment => 2,
- TextureLayout.ResolveDst => 7,
- _ => default
- };
- }
}
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
index 43f69637..d45b2965 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
@@ -118,7 +118,7 @@ protected override void Destroy()
}
}
- public GRBackendTexture CreateBackendTexture(Texture texture, TextureLayout layout)
+ public GRBackendTexture CreateBackendTexture(Texture texture, bool isMultisamplingEnabled)
{
switch (Context.GraphicsApi)
{
@@ -126,11 +126,11 @@ public GRBackendTexture CreateBackendTexture(Texture texture, TextureLayout layo
return new((int)texture.Desc.Width, (int)texture.Desc.Height, new GRD3DTextureResourceInfo
{
Resource = texture.GetNativeObject(NativeObjectType.D3D12Resource),
- ResourceState = SKFormats.DirectX12(layout),
+ ResourceState = isMultisamplingEnabled ? 0x1000u : 0x4u,
Format = SKFormats.DirectX12(texture.Desc.Format),
SampleCount = 1,
LevelCount = 1,
- SampleQualityPattern = layout is TextureLayout.ResolveDst ? uint.MaxValue : 0
+ SampleQualityPattern = isMultisamplingEnabled ? uint.MaxValue : 0
});
case GraphicsApi.Metal:
@@ -153,7 +153,7 @@ public GRBackendTexture CreateBackendTexture(Texture texture, TextureLayout layo
},
Format = SKFormats.Vulkan(texture.Desc.Format),
ImageUsageFlags = SKFormats.Vulkan(texture.Desc.Usages),
- ImageLayout = SKFormats.Vulkan(layout),
+ ImageLayout = isMultisamplingEnabled ? 7u : 2u,
SampleCount = 1,
LevelCount = 1,
CurrentQueueFamily = concurrent ? uint.MaxValue : graphicsQueueFamily,
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
index c290b426..74bb8caa 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
@@ -39,7 +39,7 @@ internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
commandBuffer.Submit().Wait();
- using GRBackendTexture backendTexture = renderer.CreateBackendTexture(texture, Layout);
+ using GRBackendTexture backendTexture = renderer.CreateBackendTexture(texture, desc.IsMultisamplingEnabled);
surface = SKSurface.Create(renderer.GRContext, backendTexture, GRSurfaceOrigin.TopLeft, desc.IsMultisamplingEnabled ? 4 : 1, SKFormats.Skia(desc.Format));
}
From 8235713c9065f598fca996dbc8ac4cf0ea50f435 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 16:28:46 +0800
Subject: [PATCH 33/50] Refactor SKFormats.DirectX12 location, remove
SampleCount map
- Moved SKFormats.DirectX12 method within SKFormats class; implementation unchanged
- Removed Skia SampleCount to uint mapping method from SKFormats
- No changes to PixelFormat mapping methods for Skia or Vulkan
- Updated SKRenderer.cs to reference relocated SKFormats.DirectX12
---
.../Zenith.NET.Extensions.Skia/SKFormats.cs | 44 +++++++------------
.../Zenith.NET.Extensions.Skia/SKRenderer.cs | 2 +-
2 files changed, 16 insertions(+), 30 deletions(-)
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
index 7a505372..77dd5aa9 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKFormats.cs
@@ -4,21 +4,6 @@ namespace Zenith.NET.Extensions.Skia;
internal static class SKFormats
{
- public static uint DirectX12(PixelFormat pixelFormat)
- {
- return pixelFormat switch
- {
- PixelFormat.R8UNorm => 61,
- PixelFormat.R16Float => 54,
- PixelFormat.R8G8B8A8UNorm => 28,
- PixelFormat.R8G8B8A8SRgb => 29,
- PixelFormat.R16G16B16A16Float => 10,
- PixelFormat.R32G32B32A32Float => 2,
- PixelFormat.B8G8R8A8UNorm => 87,
- _ => default
- };
- }
-
public static SKColorType Skia(PixelFormat pixelFormat)
{
return pixelFormat switch
@@ -34,6 +19,21 @@ public static SKColorType Skia(PixelFormat pixelFormat)
};
}
+ public static uint DirectX12(PixelFormat pixelFormat)
+ {
+ return pixelFormat switch
+ {
+ PixelFormat.R8UNorm => 61,
+ PixelFormat.R16Float => 54,
+ PixelFormat.R8G8B8A8UNorm => 28,
+ PixelFormat.R8G8B8A8SRgb => 29,
+ PixelFormat.R16G16B16A16Float => 10,
+ PixelFormat.R32G32B32A32Float => 2,
+ PixelFormat.B8G8R8A8UNorm => 87,
+ _ => default
+ };
+ }
+
public static uint Vulkan(PixelFormat pixelFormat)
{
return pixelFormat switch
@@ -49,20 +49,6 @@ public static uint Vulkan(PixelFormat pixelFormat)
};
}
- public static uint Skia(SampleCount sampleCount)
- {
- return sampleCount switch
- {
- SampleCount.Count1 => 1,
- SampleCount.Count2 => 2,
- SampleCount.Count4 => 4,
- SampleCount.Count8 => 8,
- SampleCount.Count16 => 16,
- SampleCount.Count32 => 32,
- _ => default
- };
- }
-
public static uint Vulkan(TextureUsages textureUsages)
{
uint result = default;
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
index d45b2965..d88230d5 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
@@ -123,7 +123,7 @@ public GRBackendTexture CreateBackendTexture(Texture texture, bool isMultisampli
switch (Context.GraphicsApi)
{
case GraphicsApi.DirectX12:
- return new((int)texture.Desc.Width, (int)texture.Desc.Height, new GRD3DTextureResourceInfo
+ return new((int)texture.Desc.Width, (int)texture.Desc.Height, new GRD3DTextureResourceInfo()
{
Resource = texture.GetNativeObject(NativeObjectType.D3D12Resource),
ResourceState = isMultisamplingEnabled ? 0x1000u : 0x4u,
From a1e69919e5169345e250ea65aeff8b943c297e28 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 16:57:01 +0800
Subject: [PATCH 34/50] Refactor texture layout to use RequiredLayout property
Replaced all usages of Layout with RequiredLayout in CanvasController and SKTexture. Removed the Layout property and updated all logic and references to ensure correct layout handling during texture transitions and rendering.
---
sources/Experiments/InkCanvas/Drawing/CanvasController.cs | 4 ++--
sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
index feb3b9ab..12b5acd1 100644
--- a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
+++ b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
@@ -40,14 +40,14 @@ public void Render(CommandBuffer commandBuffer, Texture target, Vector2 dpiScale
skiaCanvas.Restore();
});
- commandBuffer.Transition(texture, default, texture.Layout, TextureLayout.CopySrc);
+ commandBuffer.Transition(texture, default, texture.RequiredLayout, TextureLayout.CopySrc);
commandBuffer.CopyTexture(texture, default, default, target, default, default, new()
{
Width = texture.Desc.Width,
Height = texture.Desc.Height,
Depth = 1
});
- commandBuffer.Transition(texture, default, TextureLayout.CopySrc, texture.Layout);
+ commandBuffer.Transition(texture, default, TextureLayout.CopySrc, texture.RequiredLayout);
}
public void Resize(uint width, uint height)
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
index 74bb8caa..c304452a 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKTexture.cs
@@ -32,9 +32,9 @@ internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
commandBuffer.BeginRenderPass([ColorAttachment.Clear(texture, Vector4.Zero)], null);
commandBuffer.EndRenderPass();
- if ((Layout = desc.IsMultisamplingEnabled ? TextureLayout.ResolveDst : TextureLayout.ColorAttachment) is not TextureLayout.ColorAttachment)
+ if ((RequiredLayout = desc.IsMultisamplingEnabled ? TextureLayout.ResolveDst : TextureLayout.ColorAttachment) is not TextureLayout.ColorAttachment)
{
- commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, Layout);
+ commandBuffer.Transition(texture, default, TextureLayout.ColorAttachment, RequiredLayout);
}
commandBuffer.Submit().Wait();
@@ -48,7 +48,7 @@ internal SKTexture(SKRenderer renderer, SKTextureDesc desc)
public ref readonly TextureDesc Desc => ref texture.Desc;
- public TextureLayout Layout { get; }
+ public TextureLayout RequiredLayout { get; }
public void Render(Action render)
{
From 8b4e1f32d6b145a56ee75adff2258eeef4437b07 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 17:01:22 +0800
Subject: [PATCH 35/50] Ignore known Skia D3D12 ClearRenderTargetView warnings
Updated validation message handling to ignore messages containing
"ID3D12CommandList::ClearRenderTargetView", as these are known
non-errors from Skia with the old D3D12 API. All other messages
continue to be printed to the console.
---
sources/Experiments/InkCanvas/App.cs | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/sources/Experiments/InkCanvas/App.cs b/sources/Experiments/InkCanvas/App.cs
index f30122df..7248cc9f 100644
--- a/sources/Experiments/InkCanvas/App.cs
+++ b/sources/Experiments/InkCanvas/App.cs
@@ -37,7 +37,16 @@ static App()
Context = GraphicsContext.CreateVulkan(useValidationLayer: true);
}
- Context.ValidationMessage += static (_, args) => Console.WriteLine($"[{args.Severity}] {args.Message}");
+ Context.ValidationMessage += static (_, args) =>
+ {
+ // This is a warning from Skia when using the old D3D12 API, not a real error message, so it can be ignored.
+ if (args.Message.Contains("ID3D12CommandList::ClearRenderTargetView"))
+ {
+ return;
+ }
+
+ Console.WriteLine($"[{args.Severity}] {args.Message}");
+ };
window = Window.Create(WindowOptions.Default with
{
From e01c0cc0070f8b76cf568a937fc124c9b167bb6b Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 17:38:42 +0800
Subject: [PATCH 36/50] Ignore known Skia D3D12 validation messages
---
sources/Experiments/InkCanvas/App.cs | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/sources/Experiments/InkCanvas/App.cs b/sources/Experiments/InkCanvas/App.cs
index 7248cc9f..7917a439 100644
--- a/sources/Experiments/InkCanvas/App.cs
+++ b/sources/Experiments/InkCanvas/App.cs
@@ -39,8 +39,10 @@ static App()
Context.ValidationMessage += static (_, args) =>
{
- // This is a warning from Skia when using the old D3D12 API, not a real error message, so it can be ignored.
- if (args.Message.Contains("ID3D12CommandList::ClearRenderTargetView"))
+ // These validation messages are caused by known issues in Skia's D3D12 backend.
+ if (args.Message.StartsWith("ID3D12DescriptorHeap::GetGPUDescriptorHandleForHeapStart:", StringComparison.Ordinal)
+ || args.Message.StartsWith("ID3D12Device::CreateSampler2:", StringComparison.Ordinal)
+ || args.Message.StartsWith("ID3D12CommandList::ClearRenderTargetView:", StringComparison.Ordinal))
{
return;
}
From f50281b5a2e5e0c721ef3cf1a90df84c556fe7b0 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 20:00:13 +0800
Subject: [PATCH 37/50] Add MSAA toggle to InkCanvas
---
.../Experiments/InkCanvas/Drawing/Canvas.cs | 2 ++
.../InkCanvas/Drawing/CanvasController.cs | 9 ++++-
.../Experiments/InkCanvas/Drawing/Toolbar.cs | 35 ++++++++++++++++++-
3 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/sources/Experiments/InkCanvas/Drawing/Canvas.cs b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
index 098b6035..4abef493 100644
--- a/sources/Experiments/InkCanvas/Drawing/Canvas.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
@@ -37,6 +37,8 @@ internal class Canvas : IDisposable
private bool CanClear => strokes.Count > 0 || activeStroke is not null || erasing || erasePending;
+ public bool IsMultisamplingEnabled => toolbar.IsMultisamplingEnabled;
+
public void Draw(SKCanvas canvas, float width, float height)
{
EnsureLayout(width, height);
diff --git a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
index 12b5acd1..c75267a3 100644
--- a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
+++ b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
@@ -69,7 +69,7 @@ private SKTexture CreateTexture(uint width, uint height)
Format = PixelFormat.B8G8R8A8UNorm,
Width = width,
Height = height,
- IsMultisamplingEnabled = true
+ IsMultisamplingEnabled = canvas.IsMultisamplingEnabled
});
}
@@ -77,7 +77,14 @@ private void OnMouseDown(IMouse mouse, MouseButton button)
{
if (button is MouseButton.Left or MouseButton.Right)
{
+ bool isMultisamplingEnabled = canvas.IsMultisamplingEnabled;
+
canvas.PointerDown(new(mouse.Position.X, mouse.Position.Y), button is MouseButton.Right);
+
+ if (isMultisamplingEnabled != canvas.IsMultisamplingEnabled)
+ {
+ Resize(texture.Desc.Width, texture.Desc.Height);
+ }
}
}
diff --git a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
index 4826e9d3..2d6e520d 100644
--- a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
@@ -10,6 +10,8 @@ internal class Toolbar : IDisposable
private const float SwatchSize = 30.0f;
private const float SwatchGap = 12.0f;
private const float ButtonWidth = 64.0f;
+ private const float CheckboxSize = 18.0f;
+ private const float MultisamplingWidth = 72.0f;
private static readonly SKColor Panel = new(31, 34, 43);
private static readonly SKColor Divider = new(48, 52, 63);
@@ -41,6 +43,7 @@ internal class Toolbar : IDisposable
};
private SKRect clearRect;
+ private SKRect multisamplingRect;
private SKSize size;
private int colorIndex;
@@ -64,6 +67,8 @@ public Toolbar()
public float SelectedStrokeWidth => StrokeWidths[strokeWidthIndex];
+ public bool IsMultisamplingEnabled { get; private set; } = true;
+
public void Resize(float width, float height)
{
const float top = (ToolbarHeight - SwatchSize) * 0.5f;
@@ -85,7 +90,11 @@ public void Resize(float width, float height)
strokeWidthRects[index] = new(left, top, left + SwatchSize, bottom);
}
- float clearLeft = MathF.Max(strokeWidthRects[^1].Right + (SwatchGap * 2.0f), width - SwatchGap - ButtonWidth);
+ float multisamplingLeft = strokeWidthRects[^1].Right + (SwatchGap * 2.0f);
+
+ multisamplingRect = new(multisamplingLeft, top, multisamplingLeft + MultisamplingWidth, bottom);
+
+ float clearLeft = MathF.Max(multisamplingRect.Right + (SwatchGap * 2.0f), width - SwatchGap - ButtonWidth);
clearRect = new(clearLeft, top, clearLeft + ButtonWidth, bottom);
}
@@ -113,6 +122,10 @@ public void SelectAt(SKPoint position)
{
strokeWidthIndex = strokeWidth;
}
+ else if (multisamplingRect.Contains(position.X, position.Y))
+ {
+ IsMultisamplingEnabled = !IsMultisamplingEnabled;
+ }
}
public void Dispose()
@@ -156,6 +169,26 @@ private void DrawToolbar(SKCanvas canvas, bool canClear)
canvas.DrawCircle(slot.MidX, slot.MidY, StrokeWidths[index] * 0.5f, fillPaint);
}
+ float checkboxTop = (ToolbarHeight - CheckboxSize) * 0.5f;
+ SKRect checkbox = new(multisamplingRect.Left, checkboxTop, multisamplingRect.Left + CheckboxSize, checkboxTop + CheckboxSize);
+
+ fillPaint.Color = IsMultisamplingEnabled ? Selected : Panel;
+ canvas.DrawRoundRect(checkbox, 4.0f, 4.0f, fillPaint);
+
+ strokePaint.Color = IsMultisamplingEnabled ? Highlight : Label;
+ strokePaint.StrokeWidth = 1.5f;
+ canvas.DrawRoundRect(checkbox, 4.0f, 4.0f, strokePaint);
+
+ if (IsMultisamplingEnabled)
+ {
+ strokePaint.StrokeWidth = 2.0f;
+ canvas.DrawLine(checkbox.Left + 4.0f, checkbox.MidY, checkbox.Left + 8.0f, checkbox.Bottom - 4.0f, strokePaint);
+ canvas.DrawLine(checkbox.Left + 8.0f, checkbox.Bottom - 4.0f, checkbox.Right - 3.0f, checkbox.Top + 4.0f, strokePaint);
+ }
+
+ fillPaint.Color = Label;
+ canvas.DrawText("MSAA", checkbox.Right + 8.0f, multisamplingRect.MidY + 4.0f, SKTextAlign.Left, labelFont, fillPaint);
+
SKColor accent = canClear ? Label : Divider;
fillPaint.Color = Panel;
From 07a7ac192e247db8a93aa55bc9d28ac4481970ce Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 20:14:56 +0800
Subject: [PATCH 38/50] Reorder properties and remove IsMultisamplingEnabled
Adjusted code formatting by separating CanClear and IsMultisamplingEnabled properties. Removed IsMultisamplingEnabled from the code. No functional changes; only property order and spacing were modified.
---
sources/Experiments/InkCanvas/Drawing/Canvas.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/sources/Experiments/InkCanvas/Drawing/Canvas.cs b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
index 4abef493..aa11331e 100644
--- a/sources/Experiments/InkCanvas/Drawing/Canvas.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
@@ -35,10 +35,10 @@ internal class Canvas : IDisposable
private SKRect drawingArea;
private SKSize size;
- private bool CanClear => strokes.Count > 0 || activeStroke is not null || erasing || erasePending;
-
public bool IsMultisamplingEnabled => toolbar.IsMultisamplingEnabled;
+ private bool CanClear => strokes.Count > 0 || activeStroke is not null || erasing || erasePending;
+
public void Draw(SKCanvas canvas, float width, float height)
{
EnsureLayout(width, height);
From 6351256f579c41429acccbb9ecf2963c4ca452bf Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 20:15:55 +0800
Subject: [PATCH 39/50] Rename IsMultisamplingEnabled to MSAA across codebase
Renamed the IsMultisamplingEnabled property and variable to MSAA in all relevant files. Updated all references, declarations, and usages in Canvas, CanvasController, and Toolbar classes for improved clarity and consistency. No changes to logic or behavior.
---
sources/Experiments/InkCanvas/Drawing/Canvas.cs | 2 +-
.../InkCanvas/Drawing/CanvasController.cs | 6 +++---
sources/Experiments/InkCanvas/Drawing/Toolbar.cs | 13 +++++++------
3 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/sources/Experiments/InkCanvas/Drawing/Canvas.cs b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
index aa11331e..315447cc 100644
--- a/sources/Experiments/InkCanvas/Drawing/Canvas.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
@@ -35,7 +35,7 @@ internal class Canvas : IDisposable
private SKRect drawingArea;
private SKSize size;
- public bool IsMultisamplingEnabled => toolbar.IsMultisamplingEnabled;
+ public bool MSAA => toolbar.MSAA;
private bool CanClear => strokes.Count > 0 || activeStroke is not null || erasing || erasePending;
diff --git a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
index c75267a3..d35a9987 100644
--- a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
+++ b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
@@ -69,7 +69,7 @@ private SKTexture CreateTexture(uint width, uint height)
Format = PixelFormat.B8G8R8A8UNorm,
Width = width,
Height = height,
- IsMultisamplingEnabled = canvas.IsMultisamplingEnabled
+ IsMultisamplingEnabled = canvas.MSAA
});
}
@@ -77,11 +77,11 @@ private void OnMouseDown(IMouse mouse, MouseButton button)
{
if (button is MouseButton.Left or MouseButton.Right)
{
- bool isMultisamplingEnabled = canvas.IsMultisamplingEnabled;
+ bool isMultisamplingEnabled = canvas.MSAA;
canvas.PointerDown(new(mouse.Position.X, mouse.Position.Y), button is MouseButton.Right);
- if (isMultisamplingEnabled != canvas.IsMultisamplingEnabled)
+ if (isMultisamplingEnabled != canvas.MSAA)
{
Resize(texture.Desc.Width, texture.Desc.Height);
}
diff --git a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
index 2d6e520d..3d5506be 100644
--- a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
@@ -67,7 +67,7 @@ public Toolbar()
public float SelectedStrokeWidth => StrokeWidths[strokeWidthIndex];
- public bool IsMultisamplingEnabled { get; private set; } = true;
+ public bool MSAA { get; private set; } = true;
public void Resize(float width, float height)
{
@@ -124,7 +124,7 @@ public void SelectAt(SKPoint position)
}
else if (multisamplingRect.Contains(position.X, position.Y))
{
- IsMultisamplingEnabled = !IsMultisamplingEnabled;
+ MSAA = !MSAA;
}
}
@@ -137,6 +137,8 @@ public void Dispose()
private void DrawToolbar(SKCanvas canvas, bool canClear)
{
+ const float checkboxTop = (ToolbarHeight - CheckboxSize) * 0.5f;
+
fillPaint.Color = Panel;
canvas.DrawRect(0.0f, 0.0f, size.Width, ToolbarHeight, fillPaint);
@@ -169,17 +171,16 @@ private void DrawToolbar(SKCanvas canvas, bool canClear)
canvas.DrawCircle(slot.MidX, slot.MidY, StrokeWidths[index] * 0.5f, fillPaint);
}
- float checkboxTop = (ToolbarHeight - CheckboxSize) * 0.5f;
SKRect checkbox = new(multisamplingRect.Left, checkboxTop, multisamplingRect.Left + CheckboxSize, checkboxTop + CheckboxSize);
- fillPaint.Color = IsMultisamplingEnabled ? Selected : Panel;
+ fillPaint.Color = MSAA ? Selected : Panel;
canvas.DrawRoundRect(checkbox, 4.0f, 4.0f, fillPaint);
- strokePaint.Color = IsMultisamplingEnabled ? Highlight : Label;
+ strokePaint.Color = MSAA ? Highlight : Label;
strokePaint.StrokeWidth = 1.5f;
canvas.DrawRoundRect(checkbox, 4.0f, 4.0f, strokePaint);
- if (IsMultisamplingEnabled)
+ if (MSAA)
{
strokePaint.StrokeWidth = 2.0f;
canvas.DrawLine(checkbox.Left + 4.0f, checkbox.MidY, checkbox.Left + 8.0f, checkbox.Bottom - 4.0f, strokePaint);
From 9cb81b9f6dd61272d70c29c6725e2e2de2e6752b Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 20:23:50 +0800
Subject: [PATCH 40/50] Refactor: rename multisampling to msaa for consistency
Renamed all references from "multisampling" to "msaa" and "MultisamplingWidth" to "MSAAWidth" for improved naming consistency. Updated variables, constants, and SKRect fields in the MSAA toolbar control, including related logic and UI drawing code.
---
.../InkCanvas/Drawing/CanvasController.cs | 4 ++--
.../Experiments/InkCanvas/Drawing/Toolbar.cs | 17 ++++++++---------
2 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
index d35a9987..4fb096b2 100644
--- a/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
+++ b/sources/Experiments/InkCanvas/Drawing/CanvasController.cs
@@ -77,11 +77,11 @@ private void OnMouseDown(IMouse mouse, MouseButton button)
{
if (button is MouseButton.Left or MouseButton.Right)
{
- bool isMultisamplingEnabled = canvas.MSAA;
+ bool msaa = canvas.MSAA;
canvas.PointerDown(new(mouse.Position.X, mouse.Position.Y), button is MouseButton.Right);
- if (isMultisamplingEnabled != canvas.MSAA)
+ if (msaa != canvas.MSAA)
{
Resize(texture.Desc.Width, texture.Desc.Height);
}
diff --git a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
index 3d5506be..5d31d3eb 100644
--- a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
@@ -11,7 +11,7 @@ internal class Toolbar : IDisposable
private const float SwatchGap = 12.0f;
private const float ButtonWidth = 64.0f;
private const float CheckboxSize = 18.0f;
- private const float MultisamplingWidth = 72.0f;
+ private const float MSAAWidth = 72.0f;
private static readonly SKColor Panel = new(31, 34, 43);
private static readonly SKColor Divider = new(48, 52, 63);
@@ -42,8 +42,8 @@ internal class Toolbar : IDisposable
Style = SKPaintStyle.Stroke
};
+ private SKRect msaaRect;
private SKRect clearRect;
- private SKRect multisamplingRect;
private SKSize size;
private int colorIndex;
@@ -90,11 +90,10 @@ public void Resize(float width, float height)
strokeWidthRects[index] = new(left, top, left + SwatchSize, bottom);
}
- float multisamplingLeft = strokeWidthRects[^1].Right + (SwatchGap * 2.0f);
+ float msaaLeft = strokeWidthRects[^1].Right + (SwatchGap * 2.0f);
+ msaaRect = new(msaaLeft, top, msaaLeft + MSAAWidth, bottom);
- multisamplingRect = new(multisamplingLeft, top, multisamplingLeft + MultisamplingWidth, bottom);
-
- float clearLeft = MathF.Max(multisamplingRect.Right + (SwatchGap * 2.0f), width - SwatchGap - ButtonWidth);
+ float clearLeft = MathF.Max(msaaRect.Right + (SwatchGap * 2.0f), width - SwatchGap - ButtonWidth);
clearRect = new(clearLeft, top, clearLeft + ButtonWidth, bottom);
}
@@ -122,7 +121,7 @@ public void SelectAt(SKPoint position)
{
strokeWidthIndex = strokeWidth;
}
- else if (multisamplingRect.Contains(position.X, position.Y))
+ else if (msaaRect.Contains(position.X, position.Y))
{
MSAA = !MSAA;
}
@@ -171,7 +170,7 @@ private void DrawToolbar(SKCanvas canvas, bool canClear)
canvas.DrawCircle(slot.MidX, slot.MidY, StrokeWidths[index] * 0.5f, fillPaint);
}
- SKRect checkbox = new(multisamplingRect.Left, checkboxTop, multisamplingRect.Left + CheckboxSize, checkboxTop + CheckboxSize);
+ SKRect checkbox = new(msaaRect.Left, checkboxTop, msaaRect.Left + CheckboxSize, checkboxTop + CheckboxSize);
fillPaint.Color = MSAA ? Selected : Panel;
canvas.DrawRoundRect(checkbox, 4.0f, 4.0f, fillPaint);
@@ -188,7 +187,7 @@ private void DrawToolbar(SKCanvas canvas, bool canClear)
}
fillPaint.Color = Label;
- canvas.DrawText("MSAA", checkbox.Right + 8.0f, multisamplingRect.MidY + 4.0f, SKTextAlign.Left, labelFont, fillPaint);
+ canvas.DrawText("MSAA", checkbox.Right + 8.0f, msaaRect.MidY + 4.0f, SKTextAlign.Left, labelFont, fillPaint);
SKColor accent = canClear ? Label : Divider;
From 24d10c565bd3ecd1528c3315c583b5858eceac1d Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 22:32:22 +0800
Subject: [PATCH 41/50] Refactor SKPaint and GRContext initialization
Removed IsAntialias property from SKPaint in Canvas and Toolbar to use default antialiasing. Introduced GRContextOptions in SKRenderer with custom settings (e.g., AvoidStencilBuffers, AllowPathMaskCaching, cache sizes). Updated Direct3D, Metal, and Vulkan GRContext creation to accept options. Set GRContext resource cache limit to 256 MB.
---
sources/Experiments/InkCanvas/Drawing/Canvas.cs | 3 +--
sources/Experiments/InkCanvas/Drawing/Toolbar.cs | 8 ++------
.../Zenith.NET.Extensions.Skia/SKRenderer.cs | 16 +++++++++++++---
3 files changed, 16 insertions(+), 11 deletions(-)
diff --git a/sources/Experiments/InkCanvas/Drawing/Canvas.cs b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
index 315447cc..01100288 100644
--- a/sources/Experiments/InkCanvas/Drawing/Canvas.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Canvas.cs
@@ -14,10 +14,9 @@ internal class Canvas : IDisposable
private readonly List strokes = [];
private readonly SKPathBuilder eraserBuilder = new();
- private readonly SKPaint fillPaint = new() { IsAntialias = true };
+ private readonly SKPaint fillPaint = new();
private readonly SKPaint strokePaint = new()
{
- IsAntialias = true,
Style = SKPaintStyle.Stroke,
StrokeCap = SKStrokeCap.Round,
StrokeJoin = SKStrokeJoin.Round
diff --git a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
index 5d31d3eb..108e02a8 100644
--- a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
@@ -35,12 +35,8 @@ internal class Toolbar : IDisposable
private readonly SKRect[] strokeWidthRects = new SKRect[StrokeWidths.Length];
private readonly SKFont labelFont;
- private readonly SKPaint fillPaint = new() { IsAntialias = true };
- private readonly SKPaint strokePaint = new()
- {
- IsAntialias = true,
- Style = SKPaintStyle.Stroke
- };
+ private readonly SKPaint fillPaint = new();
+ private readonly SKPaint strokePaint = new() { Style = SKPaintStyle.Stroke };
private SKRect msaaRect;
private SKRect clearRect;
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
index d88230d5..761effed 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
@@ -14,6 +14,14 @@ public SKRenderer(GraphicsContext context)
{
Context = context;
+ GRContextOptions options = new()
+ {
+ AvoidStencilBuffers = true,
+ AllowPathMaskCaching = true,
+ RuntimeProgramCacheSize = 1024,
+ GlyphCacheTextureMaximumBytes = 32 * 1024 * 1024
+ };
+
switch (context.GraphicsApi)
{
case GraphicsApi.DirectX12:
@@ -25,7 +33,7 @@ public SKRenderer(GraphicsContext context)
Queue = context.GraphicsQueue.GetNativeObject(NativeObjectType.D3D12CommandQueue)
};
- GRContext = GRContext.CreateDirect3D(backendContext);
+ GRContext = GRContext.CreateDirect3D(backendContext, options);
}
break;
@@ -39,7 +47,7 @@ public SKRenderer(GraphicsContext context)
QueueHandle = commandQueue = SKObjectiveC.SendMessage(device, "newCommandQueue")
};
- GRContext = GRContext.CreateMetal(backendContext);
+ GRContext = GRContext.CreateMetal(backendContext, options);
}
break;
@@ -65,7 +73,7 @@ public SKRenderer(GraphicsContext context)
GetProcedureAddress = GetProcedureAddress
};
- GRContext = GRContext.CreateVulkan(backendContext);
+ GRContext = GRContext.CreateVulkan(backendContext, options);
nint GetProcedureAddress(string name, nint instance, nint device)
{
@@ -82,6 +90,8 @@ nint GetProcedureAddress(string name, nint instance, nint device)
GRContext = default!;
break;
}
+
+ GRContext.SetResourceCacheLimit(256 * 1024 * 1024);
}
public GraphicsContext Context { get; }
From 76f19d80b36a08a7bad51eb0d13da991322cbacb Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Tue, 4 Aug 2026 23:45:57 +0800
Subject: [PATCH 42/50] Remove AllowPathMaskCaching from SKRenderer init
The AllowPathMaskCaching option was removed from GRContextOptions initialization in the SKRenderer class. This option was previously set to true but is no longer present in the updated code.
---
sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
index 761effed..27b641d4 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
@@ -17,7 +17,6 @@ public SKRenderer(GraphicsContext context)
GRContextOptions options = new()
{
AvoidStencilBuffers = true,
- AllowPathMaskCaching = true,
RuntimeProgramCacheSize = 1024,
GlyphCacheTextureMaximumBytes = 32 * 1024 * 1024
};
From 599af42c5446bb3200b71a966137a5135ca1fae5 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Wed, 5 Aug 2026 00:02:04 +0800
Subject: [PATCH 43/50] Remove redundant Skia cache settings
---
sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
index 27b641d4..3f659832 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
@@ -17,8 +17,7 @@ public SKRenderer(GraphicsContext context)
GRContextOptions options = new()
{
AvoidStencilBuffers = true,
- RuntimeProgramCacheSize = 1024,
- GlyphCacheTextureMaximumBytes = 32 * 1024 * 1024
+ RuntimeProgramCacheSize = 1024
};
switch (context.GraphicsApi)
@@ -89,8 +88,6 @@ nint GetProcedureAddress(string name, nint instance, nint device)
GRContext = default!;
break;
}
-
- GRContext.SetResourceCacheLimit(256 * 1024 * 1024);
}
public GraphicsContext Context { get; }
From 6119ef26cc8793c2cf0d2cbeda04ddb03846cb9c Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Wed, 5 Aug 2026 09:28:24 +0800
Subject: [PATCH 44/50] Align declaration ordering
---
sources/Experiments/InkCanvas/Drawing/Toolbar.cs | 12 ++++++------
.../Zenith.NET.Extensions.Skia/SKRenderer.cs | 2 +-
sources/Zenith.NET.DirectX12/DXCommandBuffer.cs | 2 +-
sources/Zenith.NET.Vulkan/VKFormats.cs | 8 ++++----
4 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
index 108e02a8..c6be8471 100644
--- a/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
+++ b/sources/Experiments/InkCanvas/Drawing/Toolbar.cs
@@ -9,9 +9,9 @@ internal class Toolbar : IDisposable
private const float SwatchSize = 30.0f;
private const float SwatchGap = 12.0f;
- private const float ButtonWidth = 64.0f;
private const float CheckboxSize = 18.0f;
private const float MSAAWidth = 72.0f;
+ private const float ButtonWidth = 64.0f;
private static readonly SKColor Panel = new(31, 34, 43);
private static readonly SKColor Divider = new(48, 52, 63);
@@ -99,11 +99,6 @@ public void Draw(SKCanvas canvas, int strokeCount, int nodeCount, bool canClear)
DrawStatus(canvas, strokeCount, nodeCount);
}
- public bool IsClearButton(SKPoint position)
- {
- return clearRect.Contains(position.X, position.Y);
- }
-
public void SelectAt(SKPoint position)
{
int swatch = IndexAt(swatchRects, position);
@@ -123,6 +118,11 @@ public void SelectAt(SKPoint position)
}
}
+ public bool IsClearButton(SKPoint position)
+ {
+ return clearRect.Contains(position.X, position.Y);
+ }
+
public void Dispose()
{
strokePaint.Dispose();
diff --git a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
index 3f659832..b333b4f2 100644
--- a/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
+++ b/sources/Extensions/Zenith.NET.Extensions.Skia/SKRenderer.cs
@@ -157,9 +157,9 @@ public GRBackendTexture CreateBackendTexture(Texture texture, bool isMultisampli
Offset = (ulong)texture.GetNativeObject(NativeObjectType.VulkanDeviceMemoryOffset),
Size = Context.GetSizeAndAlignment(texture.Desc).SizeInBytes
},
+ ImageLayout = isMultisamplingEnabled ? 7u : 2u,
Format = SKFormats.Vulkan(texture.Desc.Format),
ImageUsageFlags = SKFormats.Vulkan(texture.Desc.Usages),
- ImageLayout = isMultisamplingEnabled ? 7u : 2u,
SampleCount = 1,
LevelCount = 1,
CurrentQueueFamily = concurrent ? uint.MaxValue : graphicsQueueFamily,
diff --git a/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs b/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
index c8837a1d..15f9f0ee 100644
--- a/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
+++ b/sources/Zenith.NET.DirectX12/DXCommandBuffer.cs
@@ -285,12 +285,12 @@ protected override void BeginRenderPassImpl(ReadOnlySpan colorA
Type = DXFormats.DirectX12(attachment.DepthLoadOp),
Clear = new() { ClearValue = clearValue }
},
- DepthEndingAccess = new() { Type = ZenithHelper.HasDepth(texture.Desc.Format) ? DXFormats.DirectX12(attachment.DepthStoreOp) : RenderPassEndingAccessType.NoAccess },
StencilBeginningAccess = new()
{
Type = ZenithHelper.HasStencil(texture.Desc.Format) ? DXFormats.DirectX12(attachment.StencilLoadOp) : RenderPassBeginningAccessType.NoAccess,
Clear = new() { ClearValue = clearValue }
},
+ DepthEndingAccess = new() { Type = ZenithHelper.HasDepth(texture.Desc.Format) ? DXFormats.DirectX12(attachment.DepthStoreOp) : RenderPassEndingAccessType.NoAccess },
StencilEndingAccess = new() { Type = ZenithHelper.HasStencil(texture.Desc.Format) ? DXFormats.DirectX12(attachment.StencilStoreOp) : RenderPassEndingAccessType.NoAccess }
};
}
diff --git a/sources/Zenith.NET.Vulkan/VKFormats.cs b/sources/Zenith.NET.Vulkan/VKFormats.cs
index 948165f0..e7380972 100644
--- a/sources/Zenith.NET.Vulkan/VKFormats.cs
+++ b/sources/Zenith.NET.Vulkan/VKFormats.cs
@@ -61,8 +61,8 @@ public static (PipelineStageFlags2 Stage, AccessFlags2 Access) Vulkan(BarrierSta
if (barrierStages.HasFlag(BarrierStages.VertexShading))
{
- stage |= PipelineStageFlags2.IndexInputBit | PipelineStageFlags2.VertexAttributeInputBit | PipelineStageFlags2.VertexShaderBit | PipelineStageFlags2.DrawIndirectBit;
- access |= AccessFlags2.VertexAttributeReadBit | AccessFlags2.UniformReadBit | AccessFlags2.IndexReadBit | AccessFlags2.ShaderReadBit | AccessFlags2.ShaderWriteBit | AccessFlags2.IndirectCommandReadBit | AccessFlags2.AccelerationStructureReadBitKhr;
+ stage |= PipelineStageFlags2.DrawIndirectBit | PipelineStageFlags2.VertexShaderBit | PipelineStageFlags2.IndexInputBit | PipelineStageFlags2.VertexAttributeInputBit;
+ access |= AccessFlags2.IndirectCommandReadBit | AccessFlags2.IndexReadBit | AccessFlags2.VertexAttributeReadBit | AccessFlags2.UniformReadBit | AccessFlags2.ShaderReadBit | AccessFlags2.ShaderWriteBit | AccessFlags2.AccelerationStructureReadBitKhr;
}
if (barrierStages.HasFlag(BarrierStages.FragmentShading))
@@ -73,8 +73,8 @@ public static (PipelineStageFlags2 Stage, AccessFlags2 Access) Vulkan(BarrierSta
if (barrierStages.HasFlag(BarrierStages.ComputeShading))
{
- stage |= PipelineStageFlags2.ComputeShaderBit | PipelineStageFlags2.DrawIndirectBit;
- access |= AccessFlags2.UniformReadBit | AccessFlags2.ShaderReadBit | AccessFlags2.ShaderWriteBit | AccessFlags2.IndirectCommandReadBit | AccessFlags2.AccelerationStructureReadBitKhr;
+ stage |= PipelineStageFlags2.DrawIndirectBit | PipelineStageFlags2.ComputeShaderBit;
+ access |= AccessFlags2.IndirectCommandReadBit | AccessFlags2.UniformReadBit | AccessFlags2.ShaderReadBit | AccessFlags2.ShaderWriteBit | AccessFlags2.AccelerationStructureReadBitKhr;
}
if (barrierStages.HasFlag(BarrierStages.Copy))
From 02413df9b0a1456555a885e27682701c685fd4aa Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Wed, 5 Aug 2026 11:53:13 +0800
Subject: [PATCH 45/50] Preserve scene occlusion in fluid compositing
---
.../Assets/Shaders/FluidComposite.slang | 25 +++++++++++++------
1 file changed, 18 insertions(+), 7 deletions(-)
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
index be78fb65..7dbacd0c 100644
--- a/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
@@ -184,9 +184,8 @@ float3 ReconstructNormal(float2 uv, float centerDepth)
return normal;
}
-float3 SceneAt(float2 uv)
+float3 SceneAt(float2 uv, float depth)
{
- float depth = composite.SceneDepth.SampleLevel(composite.Sampler, uv, 0.0).r;
if (depth <= 0.0)
{
float3 rayView = ViewRay(uv);
@@ -198,22 +197,34 @@ float3 SceneAt(float2 uv)
return composite.SceneColor.SampleLevel(composite.Sampler, uv, 0.0).rgb;
}
+float3 SceneAt(float2 uv)
+{
+ float depth = composite.SceneDepth.SampleLevel(composite.Sampler, uv, 0.0).r;
+
+ return SceneAt(uv, depth);
+}
+
float4 ShadeWater(FullscreenOutput input)
{
float2 uv = input.UV;
+ float sceneDepth = composite.SceneDepth.Load(int3(int2(input.Position.xy), 0)).r;
+ float3 scene = SceneAt(uv, sceneDepth);
if (composite.RenderMode == 1)
{
- return float4(ToSRGB(ACESFilm(SceneAt(uv))), 1.0);
+ return float4(ToSRGB(ACESFilm(scene)), 1.0);
}
float fluidDepth = composite.FluidDepth.SampleLevel(composite.Sampler, uv, 0.0).r;
if (fluidDepth <= 0.0)
{
- float3 background = SceneAt(uv);
+ return float4(ToSRGB(ACESFilm(scene)), 1.0);
+ }
- return float4(ToSRGB(ACESFilm(background)), 1.0);
+ if (sceneDepth > 0.0 && fluidDepth >= sceneDepth)
+ {
+ return float4(ToSRGB(ACESFilm(scene)), 1.0);
}
float thickness = composite.Thickness.SampleLevel(composite.Sampler, uv, 0.0).r;
@@ -222,7 +233,7 @@ float4 ShadeWater(FullscreenOutput input)
if (surfaceConfidence <= 0.01)
{
- return float4(ToSRGB(ACESFilm(SceneAt(uv))), 1.0);
+ return float4(ToSRGB(ACESFilm(scene)), 1.0);
}
float3 normalView = ReconstructNormal(uv, fluidDepth);
@@ -258,7 +269,7 @@ float4 ShadeWater(FullscreenOutput input)
color += sunSpecular * float3(1.0, 0.94, 0.78);
color = lerp(color, float3(0.76, 0.92, 0.96), foam * 0.72);
- color = lerp(SceneAt(uv), color, surfaceConfidence);
+ color = lerp(scene, color, surfaceConfidence);
return float4(ToSRGB(ACESFilm(color)), 1.0);
}
From 4b85f4f30d056d11de1917f8f5ac37be218583f0 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Wed, 5 Aug 2026 14:07:39 +0800
Subject: [PATCH 46/50] Preserve fluid tank frame at distance
---
.../Assets/Shaders/FluidComposite.slang | 33 +++++++++++++++++--
.../Assets/Shaders/FluidSurface.slang | 2 +-
.../FluidTank/Assets/Shaders/Scene.slang | 4 ++-
.../FluidTank/FluidTankRenderer.cs | 6 ++--
4 files changed, 38 insertions(+), 7 deletions(-)
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
index 7dbacd0c..fdc82ff4 100644
--- a/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
@@ -204,10 +204,39 @@ float3 SceneAt(float2 uv)
return SceneAt(uv, depth);
}
+bool FrameOccludesFluid(float2 uv, int2 scenePixel, float sceneDepth)
+{
+ if (composite.SceneColor.Load(int3(scenePixel, 0)).a < 0.5)
+ {
+ return false;
+ }
+
+ int2 dimensions = int2(composite.Width, composite.Height);
+ int2 basePixel = int2(floor(uv * float2(dimensions) - 0.5));
+ float closestDepth = 0.0;
+
+ for (int y = 0; y < 2; y++)
+ {
+ for (int x = 0; x < 2; x++)
+ {
+ int2 pixel = clamp(basePixel + int2(x, y), int2(0, 0), dimensions - 1);
+ float depth = composite.Attributes.Load(int3(pixel, 0)).w;
+
+ if (depth > 0.0 && (closestDepth <= 0.0 || depth < closestDepth))
+ {
+ closestDepth = depth;
+ }
+ }
+ }
+
+ return closestDepth <= 0.0 || closestDepth >= sceneDepth;
+}
+
float4 ShadeWater(FullscreenOutput input)
{
float2 uv = input.UV;
- float sceneDepth = composite.SceneDepth.Load(int3(int2(input.Position.xy), 0)).r;
+ int2 scenePixel = int2(input.Position.xy);
+ float sceneDepth = composite.SceneDepth.Load(int3(scenePixel, 0)).r;
float3 scene = SceneAt(uv, sceneDepth);
if (composite.RenderMode == 1)
@@ -222,7 +251,7 @@ float4 ShadeWater(FullscreenOutput input)
return float4(ToSRGB(ACESFilm(scene)), 1.0);
}
- if (sceneDepth > 0.0 && fluidDepth >= sceneDepth)
+ if (sceneDepth > 0.0 && (fluidDepth >= sceneDepth || FrameOccludesFluid(uv, scenePixel, sceneDepth)))
{
return float4(ToSRGB(ACESFilm(scene)), 1.0);
}
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang
index f0b7b4fc..18194947 100644
--- a/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang
@@ -199,7 +199,7 @@ DepthOutput DepthFS(SurfaceVSOutput input)
DepthOutput output;
output.LinearDepth = -viewPosition.z;
- output.Attributes = float4(input.Speed, input.Density, sphereZ, 1.0);
+ output.Attributes = float4(input.Speed, input.Density, sphereZ, output.LinearDepth);
output.DeviceDepth = clipPosition.z / clipPosition.w;
return output;
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang b/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang
index b69b0367..7b9ced8c 100644
--- a/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang
+++ b/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang
@@ -70,6 +70,8 @@ struct FSOutput
ConstantBuffer scene;
+static const uint FrameMaterialId = 4;
+
[shader("vertex")]
VSOutput VSMain(VSInput input)
{
@@ -115,7 +117,7 @@ FSOutput FSMain(VSOutput input)
float3 color = diffuse + specular + environment + material.Albedo * material.Emission;
FSOutput output;
- output.Color = float4(color, 1.0);
+ output.Color = float4(color, input.MaterialId == FrameMaterialId ? 1.0 : 0.0);
output.LinearDepth = input.ViewDepth;
return output;
diff --git a/sources/Experiments/FluidTank/FluidTankRenderer.cs b/sources/Experiments/FluidTank/FluidTankRenderer.cs
index a3af1f70..1aad59a2 100644
--- a/sources/Experiments/FluidTank/FluidTankRenderer.cs
+++ b/sources/Experiments/FluidTank/FluidTankRenderer.cs
@@ -110,7 +110,7 @@ public FluidTankRenderer()
fluidDepthPipeline = GraphicsHelper.CreateGraphicsPipeline(surfaceVertexShader, "FluidSurface.slang", "DepthFS", [], new()
{
- ColorFormats = [PixelFormat.R32Float, PixelFormat.R16G16B16A16Float],
+ ColorFormats = [PixelFormat.R32Float, PixelFormat.R32G32B32A32Float],
DepthStencilFormat = PixelFormat.D32FloatS8UInt,
SampleCount = SampleCount.Count1
}, RasterizerState.CullNone(), DepthStencilState.DepthReadWrite(), BlendState.Opaque(), PrimitiveTopology.TriangleStrip);
@@ -305,7 +305,7 @@ public void RenderScene(CommandBuffer commandBuffer)
commandBuffer.BeginRenderPass(
[
- ColorAttachment.Clear(sceneColor, new(0.0f, 0.0f, 0.0f, 1.0f)),
+ ColorAttachment.Clear(sceneColor, Vector4.Zero),
ColorAttachment.Clear(sceneLinearDepth, Vector4.Zero)
], DepthStencilAttachment.Clear(DepthStencil, 1.0f, 0));
commandBuffer.SetPipeline(scenePipeline);
@@ -420,7 +420,7 @@ public void Resize(uint width, uint height)
uint reconstructionWidth = Math.Max((width + 2) / 3, 1u);
uint reconstructionHeight = Math.Max((height + 2) / 3, 1u);
reconstructionDepth = GraphicsHelper.CreateTexture(PixelFormat.D32FloatS8UInt, reconstructionWidth, reconstructionHeight, TextureUsages.DepthStencilAttachment);
- fluidAttributes = GraphicsHelper.CreateTexture(PixelFormat.R16G16B16A16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.ColorAttachment);
+ fluidAttributes = GraphicsHelper.CreateTexture(PixelFormat.R32G32B32A32Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.ColorAttachment);
smoothDepthA = GraphicsHelper.CreateTexture(PixelFormat.R32Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage | TextureUsages.ColorAttachment);
smoothDepthB = GraphicsHelper.CreateTexture(PixelFormat.R32Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage);
smoothThicknessA = GraphicsHelper.CreateTexture(PixelFormat.R16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage | TextureUsages.ColorAttachment);
From 4581607d206635fd03354ce05e90c50f7dd494b6 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Wed, 5 Aug 2026 14:35:13 +0800
Subject: [PATCH 47/50] Align fluid occlusion code style
---
.../Assets/Shaders/FluidComposite.slang | 25 +++++++++----------
.../Assets/Shaders/FluidSurface.slang | 7 +++---
.../FluidTank/Assets/Shaders/Scene.slang | 4 +--
.../FluidTank/FluidTankRenderer.cs | 2 +-
4 files changed, 19 insertions(+), 19 deletions(-)
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
index fdc82ff4..9fd415d7 100644
--- a/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidComposite.slang
@@ -204,39 +204,38 @@ float3 SceneAt(float2 uv)
return SceneAt(uv, depth);
}
-bool FrameOccludesFluid(float2 uv, int2 scenePixel, float sceneDepth)
+bool FrameOccludesFluid(FullscreenOutput input, float sceneDepth)
{
- if (composite.SceneColor.Load(int3(scenePixel, 0)).a < 0.5)
+ if (composite.SceneColor.Load(int3(int2(input.Position.xy), 0)).a < 0.5)
{
return false;
}
- int2 dimensions = int2(composite.Width, composite.Height);
- int2 basePixel = int2(floor(uv * float2(dimensions) - 0.5));
- float closestDepth = 0.0;
+ int2 fluidDimensions = int2(composite.Width, composite.Height);
+ int2 baseFluidPixel = int2(floor(input.UV * float2(fluidDimensions) - 0.5));
+ float closestFluidDepth = 0.0;
for (int y = 0; y < 2; y++)
{
for (int x = 0; x < 2; x++)
{
- int2 pixel = clamp(basePixel + int2(x, y), int2(0, 0), dimensions - 1);
- float depth = composite.Attributes.Load(int3(pixel, 0)).w;
+ int2 samplePixel = clamp(baseFluidPixel + int2(x, y), int2(0, 0), fluidDimensions - 1);
+ float sampleDepth = composite.Attributes.Load(int3(samplePixel, 0)).w;
- if (depth > 0.0 && (closestDepth <= 0.0 || depth < closestDepth))
+ if (sampleDepth > 0.0 && (closestFluidDepth <= 0.0 || sampleDepth < closestFluidDepth))
{
- closestDepth = depth;
+ closestFluidDepth = sampleDepth;
}
}
}
- return closestDepth <= 0.0 || closestDepth >= sceneDepth;
+ return closestFluidDepth <= 0.0 || closestFluidDepth >= sceneDepth;
}
float4 ShadeWater(FullscreenOutput input)
{
float2 uv = input.UV;
- int2 scenePixel = int2(input.Position.xy);
- float sceneDepth = composite.SceneDepth.Load(int3(scenePixel, 0)).r;
+ float sceneDepth = composite.SceneDepth.Load(int3(int2(input.Position.xy), 0)).r;
float3 scene = SceneAt(uv, sceneDepth);
if (composite.RenderMode == 1)
@@ -251,7 +250,7 @@ float4 ShadeWater(FullscreenOutput input)
return float4(ToSRGB(ACESFilm(scene)), 1.0);
}
- if (sceneDepth > 0.0 && (fluidDepth >= sceneDepth || FrameOccludesFluid(uv, scenePixel, sceneDepth)))
+ if (sceneDepth > 0.0 && (fluidDepth >= sceneDepth || FrameOccludesFluid(input, sceneDepth)))
{
return float4(ToSRGB(ACESFilm(scene)), 1.0);
}
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang b/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang
index 18194947..cfc68e2b 100644
--- a/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang
+++ b/sources/Experiments/FluidTank/Assets/Shaders/FluidSurface.slang
@@ -191,15 +191,16 @@ DepthOutput DepthFS(SurfaceVSOutput input)
float sphereZ = sqrt(max(1.0 - radiusSquared, 0.0));
float3 viewPosition = input.CenterView + float3(input.Corner * surface.ParticleRadius, surface.ParticleRadius * 0.62);
float4 clipPosition = mul(float4(viewPosition, 1.0), surface.Projection);
+ float linearDepth = -viewPosition.z;
- if (OccludedByScene(input, -viewPosition.z))
+ if (OccludedByScene(input, linearDepth))
{
discard;
}
DepthOutput output;
- output.LinearDepth = -viewPosition.z;
- output.Attributes = float4(input.Speed, input.Density, sphereZ, output.LinearDepth);
+ output.LinearDepth = linearDepth;
+ output.Attributes = float4(input.Speed, input.Density, sphereZ, linearDepth);
output.DeviceDepth = clipPosition.z / clipPosition.w;
return output;
diff --git a/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang b/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang
index 7b9ced8c..91908b11 100644
--- a/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang
+++ b/sources/Experiments/FluidTank/Assets/Shaders/Scene.slang
@@ -1,5 +1,7 @@
#include "SceneCommon.slang"
+static const uint FrameMaterialId = 4;
+
struct SceneConstants
{
float4x4 View;
@@ -70,8 +72,6 @@ struct FSOutput
ConstantBuffer scene;
-static const uint FrameMaterialId = 4;
-
[shader("vertex")]
VSOutput VSMain(VSInput input)
{
diff --git a/sources/Experiments/FluidTank/FluidTankRenderer.cs b/sources/Experiments/FluidTank/FluidTankRenderer.cs
index 1aad59a2..af1f64db 100644
--- a/sources/Experiments/FluidTank/FluidTankRenderer.cs
+++ b/sources/Experiments/FluidTank/FluidTankRenderer.cs
@@ -420,8 +420,8 @@ public void Resize(uint width, uint height)
uint reconstructionWidth = Math.Max((width + 2) / 3, 1u);
uint reconstructionHeight = Math.Max((height + 2) / 3, 1u);
reconstructionDepth = GraphicsHelper.CreateTexture(PixelFormat.D32FloatS8UInt, reconstructionWidth, reconstructionHeight, TextureUsages.DepthStencilAttachment);
- fluidAttributes = GraphicsHelper.CreateTexture(PixelFormat.R32G32B32A32Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.ColorAttachment);
smoothDepthA = GraphicsHelper.CreateTexture(PixelFormat.R32Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage | TextureUsages.ColorAttachment);
+ fluidAttributes = GraphicsHelper.CreateTexture(PixelFormat.R32G32B32A32Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.ColorAttachment);
smoothDepthB = GraphicsHelper.CreateTexture(PixelFormat.R32Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage);
smoothThicknessA = GraphicsHelper.CreateTexture(PixelFormat.R16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage | TextureUsages.ColorAttachment);
smoothThicknessB = GraphicsHelper.CreateTexture(PixelFormat.R16Float, reconstructionWidth, reconstructionHeight, TextureUsages.Sampled | TextureUsages.Storage);
From b64179be19e9d23d49fc706d18320e40f85fb7be Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Wed, 5 Aug 2026 14:49:38 +0800
Subject: [PATCH 48/50] Refactor: compact multi-line inits and calls to
single-line
Refactored object initializations and method calls in FluidSimulation.cs and FluidTankRenderer.cs to use single-line statements. This improves code compactness and readability by consolidating multi-line GridDimensions initialization and BeginRenderPass calls.
---
sources/Experiments/FluidTank/FluidSimulation.cs | 4 +---
sources/Experiments/FluidTank/FluidTankRenderer.cs | 6 +-----
2 files changed, 2 insertions(+), 8 deletions(-)
diff --git a/sources/Experiments/FluidTank/FluidSimulation.cs b/sources/Experiments/FluidTank/FluidSimulation.cs
index 547be4fd..5198fc10 100644
--- a/sources/Experiments/FluidTank/FluidSimulation.cs
+++ b/sources/Experiments/FluidTank/FluidSimulation.cs
@@ -48,9 +48,7 @@ public FluidSimulation()
ParticleCount = DamDimensions.X * DamDimensions.Y * DamDimensions.Z;
Vector3 tankExtent = TankMax - TankMin;
- GridDimensions = new((uint)MathF.Ceiling(tankExtent.X / GridSpacing),
- (uint)MathF.Ceiling(tankExtent.Y / GridSpacing),
- (uint)MathF.Ceiling(tankExtent.Z / GridSpacing));
+ GridDimensions = new((uint)MathF.Ceiling(tankExtent.X / GridSpacing), (uint)MathF.Ceiling(tankExtent.Y / GridSpacing), (uint)MathF.Ceiling(tankExtent.Z / GridSpacing));
CellCount = GridDimensions.X * GridDimensions.Y * GridDimensions.Z;
GridPointCount = (GridDimensions.X + 1) * (GridDimensions.Y + 1) * (GridDimensions.Z + 1);
pressureParityDispatchCount = (GridDimensions.X + 1) / 2 * GridDimensions.Y * GridDimensions.Z;
diff --git a/sources/Experiments/FluidTank/FluidTankRenderer.cs b/sources/Experiments/FluidTank/FluidTankRenderer.cs
index af1f64db..9feb59a3 100644
--- a/sources/Experiments/FluidTank/FluidTankRenderer.cs
+++ b/sources/Experiments/FluidTank/FluidTankRenderer.cs
@@ -303,11 +303,7 @@ public void RenderScene(CommandBuffer commandBuffer)
commandBuffer.Transition(sceneLinearDepth, default, TextureLayout.Undefined, TextureLayout.ColorAttachment);
commandBuffer.Transition(DepthStencil, default, TextureLayout.Undefined, TextureLayout.DepthStencilAttachment);
- commandBuffer.BeginRenderPass(
- [
- ColorAttachment.Clear(sceneColor, Vector4.Zero),
- ColorAttachment.Clear(sceneLinearDepth, Vector4.Zero)
- ], DepthStencilAttachment.Clear(DepthStencil, 1.0f, 0));
+ commandBuffer.BeginRenderPass([ColorAttachment.Clear(sceneColor, Vector4.Zero), ColorAttachment.Clear(sceneLinearDepth, Vector4.Zero)], DepthStencilAttachment.Clear(DepthStencil, 1.0f, 0));
commandBuffer.SetPipeline(scenePipeline);
commandBuffer.SetVertexBuffer(sceneVertexBuffer, 0, 0);
commandBuffer.SetIndexBuffer(sceneIndexBuffer, 0, IndexFormat.UInt32);
From f07e4444a728ad30ad7081b79a35498b3e2b9200 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Wed, 5 Aug 2026 15:25:34 +0800
Subject: [PATCH 49/50] Bump package version to 1.0.0 stable
Updated NuGet.Packaging.props to use version 1.0.0, moving from the 1.0.0-rc release candidate to the stable release.
---
sources/NuGet.Packaging.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sources/NuGet.Packaging.props b/sources/NuGet.Packaging.props
index f80d76e1..e60169d2 100644
--- a/sources/NuGet.Packaging.props
+++ b/sources/NuGet.Packaging.props
@@ -7,7 +7,7 @@
$(MSBuildThisFileDirectory)..\.nuget
- 1.0.0-rc
+ 1.0.0
qian-o
Copyright (c) 2026 qian-o
Zenith.NET is a modern rendering hardware interface for .NET with one consistent C# API for graphics and compute across DirectX 12, Metal 4, and Vulkan 1.4.
From 5ca0e543718a21986a940a87e4408d362d71d427 Mon Sep 17 00:00:00 2001
From: qian-o <1324771795@qq.com>
Date: Wed, 5 Aug 2026 16:25:17 +0800
Subject: [PATCH 50/50] Remove D3D query Begin/End calls from Surface &
ZenithView
Removed explicit DeviceContext.Begin/End(Query) calls in Surface.cs and ZenithView.WinUI.cs. Resource copy and query data polling logic remain unchanged.
---
sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs | 1 -
sources/Views/Zenith.NET.Views.WPF/Surface.cs | 1 -
sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs | 1 -
3 files changed, 3 deletions(-)
diff --git a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs
index 89e8c70a..e41577a8 100644
--- a/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs
+++ b/sources/Views/Zenith.NET.Views.Maui/Platforms/Windows/Surface.cs
@@ -101,7 +101,6 @@ public void Present()
AcquireSync();
- D3D.DeviceContext.Begin(Query);
D3D.DeviceContext.CopyResource((ID3D11Resource*)backBuffer.Handle, (ID3D11Resource*)Texture.Handle);
D3D.DeviceContext.End(Query);
diff --git a/sources/Views/Zenith.NET.Views.WPF/Surface.cs b/sources/Views/Zenith.NET.Views.WPF/Surface.cs
index b9def545..92e36f0e 100644
--- a/sources/Views/Zenith.NET.Views.WPF/Surface.cs
+++ b/sources/Views/Zenith.NET.Views.WPF/Surface.cs
@@ -108,7 +108,6 @@ public void Present(D3DImage image)
AcquireSync();
- D3D.D3D11DeviceContext.Begin(Query);
D3D.D3D11DeviceContext.CopyResource((ID3D11Resource*)D3D9SharedTexture.Handle, (ID3D11Resource*)D3D11RenderTarget.Handle);
D3D.D3D11DeviceContext.End(Query);
diff --git a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
index dd6ffdac..f0dd43f6 100644
--- a/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
+++ b/sources/Views/Zenith.NET.Views.WinUI/ZenithView.WinUI.cs
@@ -203,7 +203,6 @@ public void Present()
AcquireSync();
- D3D.DeviceContext.Begin(Query);
D3D.DeviceContext.CopyResource((ID3D11Resource*)backBuffer.Handle, (ID3D11Resource*)Texture.Handle);
D3D.DeviceContext.End(Query);