summaryrefslogtreecommitdiff
path: root/Views/Image
diff options
context:
space:
mode:
Diffstat (limited to 'Views/Image')
-rw-r--r--Views/Image/ImageControl.xaml16
-rw-r--r--Views/Image/ImageControl.xaml.cs100
-rw-r--r--Views/Image/ImagePanel.xaml26
-rw-r--r--Views/Image/ImagePanel.xaml.cs19
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);
+ }
+ }
+}