diff --git a/samples/AvalonDraw/GradientMeshEditorWindow.axaml b/samples/AvalonDraw/GradientMeshEditorWindow.axaml new file mode 100644 index 0000000000..c355a93131 --- /dev/null +++ b/samples/AvalonDraw/GradientMeshEditorWindow.axaml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/samples/AvalonDraw/GradientMeshEditorWindow.axaml.cs b/samples/AvalonDraw/GradientMeshEditorWindow.axaml.cs new file mode 100644 index 0000000000..8288b66785 --- /dev/null +++ b/samples/AvalonDraw/GradientMeshEditorWindow.axaml.cs @@ -0,0 +1,77 @@ +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Svg.Model; + +namespace AvalonDraw; + +public partial class GradientMeshEditorWindow : Window +{ + private readonly Canvas _canvas; + private GradientMeshPoint? _dragging; + + public ObservableCollection Points { get; } = new(); + + public GradientMeshEditorWindow(GradientMesh mesh) + { + InitializeComponent(); + _canvas = this.FindControl("MeshCanvas"); + foreach (var p in mesh.Points) + Points.Add(p); + _canvas.PointerPressed += OnPointerPressed; + _canvas.PointerReleased += OnPointerReleased; + _canvas.PointerMoved += OnPointerMoved; + _canvas.DataContext = this; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + var pos = e.GetPosition(_canvas); + foreach (var p in Points) + { + var rect = new Rect(p.Position.X - 5, p.Position.Y - 5, 10, 10); + if (rect.Contains(pos)) + { + _dragging = p; + break; + } + } + } + + private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + _dragging = null; + } + + private void OnPointerMoved(object? sender, PointerEventArgs e) + { + if (_dragging is null) + return; + + var pos = e.GetPosition(_canvas); + var index = Points.IndexOf(_dragging); + if (index >= 0) + { + Points[index] = _dragging with { Position = new ShimSkiaSharp.SKPoint((float)pos.X, (float)pos.Y) }; + _dragging = Points[index]; + } + } + + public GradientMesh Result + { + get + { + var mesh = new GradientMesh(); + mesh.Points.AddRange(Points); + return mesh; + } + } +} diff --git a/samples/AvalonDraw/MainWindow.axaml.cs b/samples/AvalonDraw/MainWindow.axaml.cs index 23df361bcf..b245cef779 100644 --- a/samples/AvalonDraw/MainWindow.axaml.cs +++ b/samples/AvalonDraw/MainWindow.axaml.cs @@ -261,6 +261,16 @@ public MainWindow() }; return picker; } + if (entry is GradientMeshEntry meshEntry) + { + var btn = new Button { Content = "Edit Mesh", VerticalAlignment = VerticalAlignment.Center }; + btn.Click += async (_, _) => + { + var dlg = new GradientMeshEditorWindow(meshEntry.Mesh); + await dlg.ShowDialog(this); + }; + return btn; + } if (entry is GradientStopsEntry gEntry) { var btn = new Button { Content = entry.Value ?? "Edit", VerticalAlignment = VerticalAlignment.Center }; diff --git a/samples/AvalonDraw/Services/GradientMeshEntry.cs b/samples/AvalonDraw/Services/GradientMeshEntry.cs new file mode 100644 index 0000000000..7a919eb11d --- /dev/null +++ b/samples/AvalonDraw/Services/GradientMeshEntry.cs @@ -0,0 +1,20 @@ +using System; +using Svg.Model; + +namespace AvalonDraw.Services; + +public class GradientMeshEntry : PropertyEntry +{ + public GradientMesh Mesh { get; } + + public GradientMeshEntry(GradientMesh mesh) + : base("Mesh", string.Empty, (_, __) => { }) + { + Mesh = mesh; + } + + public override void Apply(object target) + { + // Placeholder for applying the mesh to an object. + } +} diff --git a/src/Svg.Model/GradientMesh.cs b/src/Svg.Model/GradientMesh.cs new file mode 100644 index 0000000000..46f7837145 --- /dev/null +++ b/src/Svg.Model/GradientMesh.cs @@ -0,0 +1,22 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +using System.Collections.Generic; +using ShimSkiaSharp; + +namespace Svg.Model; + +/// +/// Represents a simple gradient mesh consisting of colored points. +/// +public sealed class GradientMesh +{ + /// + /// List of mesh points. + /// + public List Points { get; } = new(); +} + +/// +/// Defines a single mesh point with position and color. +/// +public sealed record GradientMeshPoint(SKPoint Position, SKColor Color); diff --git a/src/Svg.Model/Services/GradientMeshService.cs b/src/Svg.Model/Services/GradientMeshService.cs new file mode 100644 index 0000000000..4bb24118c3 --- /dev/null +++ b/src/Svg.Model/Services/GradientMeshService.cs @@ -0,0 +1,35 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +using System.Collections.Generic; +using System.Linq; +using ShimSkiaSharp; + +namespace Svg.Model.Services; + +/// +/// Provides conversion helpers for instances. +/// +public static class GradientMeshService +{ + /// + /// Convert gradient mesh points to a mesh shader if supported. Currently this + /// implementation falls back to a linear gradient created between the first + /// and last mesh points when mesh shaders are not available. + /// + public static SKShader? ToShader(GradientMesh mesh) + { + if (mesh.Points.Count < 2) + return null; + + // TODO: use mesh gradient when available in ShimSkiaSharp + var first = mesh.Points.First(); + var last = mesh.Points.Last(); + return SKShader.CreateLinearGradient( + first.Position, + last.Position, + new[] { (SKColorF)first.Color, (SKColorF)last.Color }, + SKColorSpace.Srgb, + new[] { 0f, 1f }, + SKShaderTileMode.Clamp); + } +}