diff options
Diffstat (limited to 'Views/Image')
| -rw-r--r-- | Views/Image/ImageControl.xaml | 16 | ||||
| -rw-r--r-- | Views/Image/ImageControl.xaml.cs | 100 | ||||
| -rw-r--r-- | Views/Image/ImagePanel.xaml | 26 | ||||
| -rw-r--r-- | Views/Image/ImagePanel.xaml.cs | 19 |
4 files changed, 161 insertions, 0 deletions
diff --git a/Views/Image/ImageControl.xaml b/Views/Image/ImageControl.xaml new file mode 100644 index 0000000..c1c176e --- /dev/null +++ b/Views/Image/ImageControl.xaml @@ -0,0 +1,16 @@ +<UserControl xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="Avalar.Views.Image.ImageControl"> + <UserControl.Resources> + <ImageBrush x:Key="ImageBrush" Source="{Binding Bitmap}" Stretch="Uniform"/> + </UserControl.Resources> + <Grid> + <Canvas Name="ImageCanvas" + HorizontalAlignment="Stretch" VerticalAlignment="Stretch" + Background="{StaticResource ImageBrush}" + PointerWheelChanged="OnPointerWheelChanged"/> + </Grid> +</UserControl> diff --git a/Views/Image/ImageControl.xaml.cs b/Views/Image/ImageControl.xaml.cs new file mode 100644 index 0000000..2189bb0 --- /dev/null +++ b/Views/Image/ImageControl.xaml.cs @@ -0,0 +1,100 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using System; + +namespace Avalar.Views.Image +{ + public class ImageControl : UserControl + { + private const double ZoomTick = 0.1; + private double CurrentZoom { get; set; } = 1.0; + + private double InvCurrentZoom => 1 / CurrentZoom; + + private ImageBrush Image { get; } + + private Canvas ImageCanvas { get; } + public ImageControl() + { + InitializeComponent(); + + Image = this.FindResource("ImageBrush") as ImageBrush ?? throw new System.ArgumentException("ImageBrush"); + ImageCanvas = this.FindControl<Canvas>("ImageCanvas"); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private Point MousePointerInImageSpace(Point mouseCoordinates) + { + var imageSize = Image.Source.Size; + var canvasSize = new Size(ImageCanvas.Bounds.Width, ImageCanvas.Bounds.Height); + + double focalPointX; + double focalPointY; + + if (canvasSize.AspectRatio > imageSize.AspectRatio) + { + focalPointY = mouseCoordinates.Y / canvasSize.Height; + + var halfUnusedX = (canvasSize.AspectRatio - imageSize.AspectRatio) * canvasSize.Width / 2; + var clampedX = Math.Clamp(mouseCoordinates.X - halfUnusedX, 0, imageSize.Width); + + focalPointX = clampedX / imageSize.Width; + } + else + { + focalPointX = mouseCoordinates.X / canvasSize.Width; + + var halfUnusedY = 1 / (canvasSize.AspectRatio - imageSize.AspectRatio) * canvasSize.Height / 2; + var clampedY = Math.Clamp(mouseCoordinates.Y - halfUnusedY, 0, imageSize.Height); + + focalPointY = clampedY / imageSize.Height; + } + + return new Point(focalPointX, focalPointY); + } + + private Point TransformToSourceRectSpace(Point from) + { + var sourceRectPosition = Image.SourceRect.Rect.Position; + var sourceRectSize = Image.SourceRect.Rect.Size; + return new Point( + from.X * sourceRectSize.Width + sourceRectPosition.X, + from.Y * sourceRectSize.Height + sourceRectPosition.Y + ); + } + + public void OnPointerWheelChanged(object sender, PointerWheelEventArgs e) + { + var invOldZoom = InvCurrentZoom; + var zoomFactor = e.Delta.Y * ZoomTick + 1.0; + CurrentZoom = Math.Clamp(CurrentZoom * zoomFactor, 1, double.PositiveInfinity); + + var sourceRect = Image.SourceRect.Rect; + + var canvasMouseCoordinates = e.GetPosition(ImageCanvas); + + var currentCenter = sourceRect.Center; + var desiredCenterInImageCoords = MousePointerInImageSpace(canvasMouseCoordinates); + if(e.Delta.Y < 0) + { + desiredCenterInImageCoords = new Point(1 - desiredCenterInImageCoords.X, 1 - desiredCenterInImageCoords.Y); + } + var desiredCenter = TransformToSourceRectSpace(desiredCenterInImageCoords); + + var maxTravelDistance = Math.Abs(invOldZoom - InvCurrentZoom); + + var newCenterX = Math.Clamp(desiredCenter.X, currentCenter.X - maxTravelDistance, currentCenter.X + maxTravelDistance); + var newCenterY = Math.Clamp(desiredCenter.Y, currentCenter.Y - maxTravelDistance, currentCenter.Y + maxTravelDistance); + + var sourceRectNewTopLeft = new Point(newCenterX - InvCurrentZoom / 2, newCenterY - InvCurrentZoom / 2); + Image.SourceRect = new RelativeRect(sourceRectNewTopLeft, new Size(InvCurrentZoom, InvCurrentZoom), RelativeUnit.Relative); + } + } +} diff --git a/Views/Image/ImagePanel.xaml b/Views/Image/ImagePanel.xaml new file mode 100644 index 0000000..f54458d --- /dev/null +++ b/Views/Image/ImagePanel.xaml @@ -0,0 +1,26 @@ +<UserControl xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:vms="clr-namespace:Avalar.ViewModels.Image" + xmlns:local="clr-namespace:Avalar.Views.Image" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="Avalar.Views.Image.ImagePanel"> + <Border Margin="5"> + <ContentControl Content="{Binding ChildViewModel}"> + <ContentControl.DataTemplates> + <DataTemplate DataType="{x:Type vms:ImageLoadedViewModel}"> + <local:ImageControl/> + </DataTemplate> + <DataTemplate DataType="{x:Type vms:ImageNotLoadedViewModel}"> + <Button Command="{Binding OpenFileCommand}"> + <ItemsControl> + <TextBlock Text="No image loaded"/> + <TextBlock Text="Click here or drag an image over this area to load."/> + </ItemsControl> + </Button> + </DataTemplate> + </ContentControl.DataTemplates> + </ContentControl> + </Border> +</UserControl> diff --git a/Views/Image/ImagePanel.xaml.cs b/Views/Image/ImagePanel.xaml.cs new file mode 100644 index 0000000..bc18611 --- /dev/null +++ b/Views/Image/ImagePanel.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalar.Views.Image +{ + public class ImagePanel : UserControl + { + public ImagePanel() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} |
