feat: common ZoomPanImage

This commit is contained in:
2026-02-26 06:56:26 +03:00
parent 252201c930
commit 2c24b9e3d5
2 changed files with 305 additions and 280 deletions
+3 -280
View File
@@ -11,7 +11,7 @@ using GSS2.Core.Extentions;
namespace GSS2.UI.Core; namespace GSS2.UI.Core;
public class OpenCvImage : Control public class OpenCvImage : ZoomPanImage
{ {
private class OpenCvBitmap : Bitmap private class OpenCvBitmap : Bitmap
{ {
@@ -27,31 +27,6 @@ public class OpenCvImage : Control
{ } { }
} }
private Point? _lastmousePosition = null;
private Dictionary<int, Point>? _lastTouchPositions = null;
private float _zoom = 1;
private float _panX = 0;
private float _panY = 0;
private OpenCvBitmap? _bitmap = null;
public static readonly StyledProperty<Color> BackgroundProperty = AvaloniaProperty.Register<OpenCvImage, Color>(nameof(Background), defaultValue: Colors.Transparent);
public Color Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
public static readonly StyledProperty<Stretch> StretchModeProperty = AvaloniaProperty.Register<OpenCvImage, Stretch>(nameof(StretchMode), defaultValue: Stretch.Uniform);
public Stretch StretchMode
{
get => GetValue(StretchModeProperty);
set => SetValue(StretchModeProperty, value);
}
public static readonly StyledProperty<TileMode> TileModeProperty = AvaloniaProperty.Register<OpenCvImage, TileMode>(nameof(TileMode), defaultValue: TileMode.None);
public TileMode TileMode
{
get => GetValue(TileModeProperty);
set => SetValue(TileModeProperty, value);
}
public static readonly StyledProperty<Mat?> ImageProperty = AvaloniaProperty.Register<OpenCvImage, Mat?>(nameof(Image), defaultValue: null); public static readonly StyledProperty<Mat?> ImageProperty = AvaloniaProperty.Register<OpenCvImage, Mat?>(nameof(Image), defaultValue: null);
public Mat? Image public Mat? Image
{ {
@@ -61,22 +36,12 @@ public class OpenCvImage : Control
public OpenCvImage() public OpenCvImage()
: base() : base()
{ { }
DoubleTapped += (_, _) =>
{
_zoom = 1f;
_panX = 0f;
_panY = 0f;
};
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
if (change.Property == ImageProperty) if (change.Property == ImageProperty)
{ {
_bitmap?.Dispose();
_bitmap = null;
if (Image is null) if (Image is null)
return; return;
@@ -98,252 +63,10 @@ public class OpenCvImage : Control
var alphaChannel = image.Channels() == 4 ? channels[4] : new Mat(Image.Size(), OpenCvSharp.MatType.CV_8UC1, new OpenCvSharp.Scalar(255)); var alphaChannel = image.Channels() == 4 ? channels[4] : new Mat(Image.Size(), OpenCvSharp.MatType.CV_8UC1, new OpenCvSharp.Scalar(255));
Cv2.Merge([channels[2], channels[1], channels[0], alphaChannel], image); Cv2.Merge([channels[2], channels[1], channels[0], alphaChannel], image);
_bitmap = new OpenCvBitmap(image); ImageSource = new OpenCvBitmap(image);
InvalidateVisual(); InvalidateVisual();
} }
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
} }
protected override void OnPointerWheelChanged(PointerWheelEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
var position = ea.GetPosition(this);
// Рассчитываем позицию в координатах изображения (до масштабирования)
var imageX = (position.X - Bounds.Width / 2 - _panX) / _zoom;
var imageY = (position.Y - Bounds.Height / 2 - _panY) / _zoom;
// Применяем масштаб
float zoomFactor = ea.Delta.Y > 0 ? 1.1f : 1 / 1.1f;
_zoom *= zoomFactor;
_zoom = Math.Clamp(_zoom, 1f, 10f);
_zoom = (float)Math.Round(_zoom, 2);
if (_zoom != 1f)
{
// Пересчитываем смещение так, чтобы указатель остался на месте
_panX = (float)(position.X - Bounds.Width / 2 - imageX * _zoom);
_panY = (float)(position.Y - Bounds.Height / 2 - imageY * _zoom);
}
else
{
// Если зум == 1, то возвращаем изображение к исходному смещению
_panX = 0f;
_panY = 0f;
}
}
base.OnPointerWheelChanged(ea);
InvalidateVisual();
}
protected override void OnPointerPressed(PointerPressedEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
if (ea.Properties.IsLeftButtonPressed)
{
if (ea.ClickCount == 2) // По двойному клику возвращаем изображение к смещение и зум к стандартным значениям
{
_zoom = 1f;
_panX = 0f;
_panY = 0f;
}
else
_lastmousePosition = ea.GetPosition(this);
}
}
else if (ea.Pointer.Type == PointerType.Touch)
{
if (_lastTouchPositions is null)
_lastTouchPositions = new Dictionary<int, Point>();
if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2)
_lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this));
else if (_lastTouchPositions.ContainsKey(ea.Pointer.Id))
_lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this);
}
base.OnPointerPressed(ea);
InvalidateVisual();
}
protected override void OnPointerReleased(PointerReleasedEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
if (!ea.Properties.IsLeftButtonPressed)
_lastmousePosition = null;
}
else if (ea.Pointer.Type == PointerType.Touch)
{
if (_lastTouchPositions is not null && _lastTouchPositions.ContainsKey(ea.Pointer.Id))
_lastTouchPositions.Remove(ea.Pointer.Id);
}
base.OnPointerReleased(ea);
InvalidateVisual();
}
protected override void OnPointerMoved(PointerEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
if (_lastmousePosition is null || _zoom == 1) // Если кнопка не нажата или зум == 1, то ничего не делаем
{
base.OnPointerMoved(ea);
return;
}
var position = ea.GetPosition(this);
_panX -= (float)(_lastmousePosition.Value.X - position.X);
_panY -= (float)(_lastmousePosition.Value.Y - position.Y);
_lastmousePosition = position;
}
else if (ea.Pointer.Type == PointerType.Touch)
{
if (_lastTouchPositions is null)
{
base.OnPointerMoved(ea);
return;
}
if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2)
{
_lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this));
base.OnPointerMoved(ea);
return;
}
if (_lastTouchPositions.Count() == 1 && _zoom != 1) // Если есть только одно прикосновение и зум != 1, то токлько перемещение
{
var position = ea.GetPosition(this);
_panX -= (float)(_lastTouchPositions[ea.Pointer.Id].X - position.X);
_panY -= (float)(_lastTouchPositions[ea.Pointer.Id].Y - position.Y); // Ось Y инвертированна
_lastTouchPositions[ea.Pointer.Id] = position;
}
else if (_lastTouchPositions.Count() == 2) // Если два прикосновения, то перемещение с приближением
{
var lastPosition1 = _lastTouchPositions.Skip(0).First().Value;
var lastPosition2 = _lastTouchPositions.Skip(1).First().Value;
var lastDistance = Point.Distance(lastPosition1, lastPosition2);
var lastCenter = PointHelper.Center(lastPosition1, lastPosition2);
_lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this);
var newPosition1 = _lastTouchPositions.Skip(0).First().Value;
var newPosition2 = _lastTouchPositions.Skip(1).First().Value;
var newDistance = Point.Distance(newPosition1, newPosition2);
var newCenter = PointHelper.Center(newPosition1, newPosition2);
var zoomFactor = newDistance / lastDistance;
var newZoom = (float)(_zoom * zoomFactor);
newZoom = (float)Math.Clamp(newZoom, 1f, 10f);
var imageX = (newCenter.X - Bounds.Width / 2 - _panX) / _zoom;
var imageY = (newCenter.Y - Bounds.Height / 2 - _panY) / _zoom;
_zoom = newZoom;
if (_zoom > 1f)
{
_panX = (float)(newCenter.X - Bounds.Width / 2 - imageX * _zoom);
_panY = (float)(newCenter.Y - Bounds.Height / 2 - imageY * _zoom);
}
else
{
_zoom = 1f;
_panX = 0f;
_panY = 0f;
}
}
}
base.OnPointerMoved(ea);
InvalidateVisual();
}
public override void Render(DrawingContext context)
{
using (context.PushClip(new Rect(Bounds.Size)))
{
context.FillRectangle(new SolidColorBrush(Background), new Rect(0, 0, Bounds.Width, Bounds.Height));
if (_bitmap is null)
{
base.Render(context);
return;
}
var (sx, sy) = ComputeStretchScale(StretchMode, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height);
sx *= _zoom;
sy *= _zoom;
var drawWidth = _bitmap.PixelSize.Width * sx;
var drawHeight = _bitmap.PixelSize.Height * sy;
var destRect = new Rect(
(Bounds.Width - drawWidth) / 2 + _panX,
(Bounds.Height - drawHeight) / 2 + _panY,
drawWidth, drawHeight
);
var srcRect = ComputeSourceRect(TileMode, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height);
context.DrawImage(_bitmap, srcRect, destRect);
}
base.Render(context);
}
private (double sx, double sy) ComputeStretchScale(Stretch stretch, double imageWidth, double imageHeight)
{
double viewWidth = Bounds.Width;
double viewHeight = Bounds.Height;
var viewAspect = viewWidth / viewHeight;
var imageAspect = imageWidth / imageHeight;
switch (stretch)
{
case Stretch.None:
return (1, 1);
case Stretch.Fill:
return (viewWidth / imageWidth, viewHeight / imageHeight);
case Stretch.Uniform:
if (imageAspect > viewAspect)
{
var s = viewWidth / imageWidth;
return (s, s);
}
else
{
var s = viewHeight / imageHeight;
return (s, s);
}
case Stretch.UniformToFill:
if (imageAspect > viewAspect)
{
var s = viewHeight / imageHeight;
return (s, s);
}
else
{
var s = viewWidth / imageWidth;
return (s, s);
}
default:
return (1, 1);
}
}
private Rect ComputeSourceRect(TileMode mode, int imageWidth, int imageHeight)
{
switch (mode)
{
case TileMode.None:
return new Rect(0, 0, imageWidth, imageHeight);
case TileMode.Tile:
return new Rect(0, 0, imageWidth * 2, imageHeight * 2);
case TileMode.FlipX:
return new Rect(imageWidth, 0, -imageWidth, imageHeight);
case TileMode.FlipY:
return new Rect(0, imageHeight, imageWidth, -imageHeight);
case TileMode.FlipXY:
return new Rect(imageWidth, imageHeight, -imageWidth, -imageHeight);
default:
return new Rect(0, 0, imageWidth, imageHeight);
}
}
} }
+302
View File
@@ -0,0 +1,302 @@
using Avalonia;
using Avalonia.Media;
using Avalonia.Input;
using Avalonia.Controls;
using GSS2.Core.Extentions;
namespace GSS2.UI.Core;
public class ZoomPanImage : Control
{
private Point? _lastmousePosition = null;
private Dictionary<int, Point>? _lastTouchPositions = null;
private float _zoom = 1;
private float _panX = 0;
private float _panY = 0;
public static readonly StyledProperty<Color> BackgroundProperty = AvaloniaProperty.Register<ZoomPanImage, Color>(nameof(Background), defaultValue: Colors.Transparent);
public Color Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
public static readonly StyledProperty<Stretch> StretchModeProperty = AvaloniaProperty.Register<ZoomPanImage, Stretch>(nameof(StretchMode), defaultValue: Stretch.Uniform);
public Stretch StretchMode
{
get => GetValue(StretchModeProperty);
set => SetValue(StretchModeProperty, value);
}
public static readonly StyledProperty<TileMode> TileModeProperty = AvaloniaProperty.Register<ZoomPanImage, TileMode>(nameof(TileMode), defaultValue: TileMode.None);
public TileMode TileMode
{
get => GetValue(TileModeProperty);
set => SetValue(TileModeProperty, value);
}
public static readonly StyledProperty<IImage?> ImageSourceProperty = AvaloniaProperty.Register<ZoomPanImage, IImage?>(nameof(ImageSource), defaultValue: null);
public IImage? ImageSource
{
get => GetValue(ImageSourceProperty);
set => SetValue(ImageSourceProperty, value);
}
public ZoomPanImage()
: base()
{
DoubleTapped += (_, _) =>
{
_zoom = 1f;
_panX = 0f;
_panY = 0f;
};
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == ImageSourceProperty)
InvalidateVisual();
base.OnPropertyChanged(change);
}
protected override void OnPointerWheelChanged(PointerWheelEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
var position = ea.GetPosition(this);
// Рассчитываем позицию в координатах изображения (до масштабирования)
var imageX = (position.X - Bounds.Width / 2 - _panX) / _zoom;
var imageY = (position.Y - Bounds.Height / 2 - _panY) / _zoom;
// Применяем масштаб
float zoomFactor = ea.Delta.Y > 0 ? 1.1f : 1 / 1.1f;
_zoom *= zoomFactor;
_zoom = Math.Clamp(_zoom, 1f, 10f);
_zoom = (float)Math.Round(_zoom, 2);
if (_zoom != 1f)
{
// Пересчитываем смещение так, чтобы указатель остался на месте
_panX = (float)(position.X - Bounds.Width / 2 - imageX * _zoom);
_panY = (float)(position.Y - Bounds.Height / 2 - imageY * _zoom);
}
else
{
// Если зум == 1, то возвращаем изображение к исходному смещению
_panX = 0f;
_panY = 0f;
}
}
base.OnPointerWheelChanged(ea);
InvalidateVisual();
}
protected override void OnPointerPressed(PointerPressedEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
if (ea.Properties.IsLeftButtonPressed)
{
if (ea.ClickCount == 2) // По двойному клику возвращаем изображение к смещение и зум к стандартным значениям
{
_zoom = 1f;
_panX = 0f;
_panY = 0f;
}
else
_lastmousePosition = ea.GetPosition(this);
}
}
else if (ea.Pointer.Type == PointerType.Touch)
{
if (_lastTouchPositions is null)
_lastTouchPositions = new Dictionary<int, Point>();
if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2)
_lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this));
else if (_lastTouchPositions.ContainsKey(ea.Pointer.Id))
_lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this);
}
base.OnPointerPressed(ea);
InvalidateVisual();
}
protected override void OnPointerReleased(PointerReleasedEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
if (!ea.Properties.IsLeftButtonPressed)
_lastmousePosition = null;
}
else if (ea.Pointer.Type == PointerType.Touch)
{
if (_lastTouchPositions is not null && _lastTouchPositions.ContainsKey(ea.Pointer.Id))
_lastTouchPositions.Remove(ea.Pointer.Id);
}
base.OnPointerReleased(ea);
InvalidateVisual();
}
protected override void OnPointerMoved(PointerEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
if (_lastmousePosition is null || _zoom == 1) // Если кнопка не нажата или зум == 1, то ничего не делаем
{
base.OnPointerMoved(ea);
return;
}
var position = ea.GetPosition(this);
_panX -= (float)(_lastmousePosition.Value.X - position.X);
_panY -= (float)(_lastmousePosition.Value.Y - position.Y);
_lastmousePosition = position;
}
else if (ea.Pointer.Type == PointerType.Touch)
{
if (_lastTouchPositions is null)
{
base.OnPointerMoved(ea);
return;
}
if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2)
{
_lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this));
base.OnPointerMoved(ea);
return;
}
if (_lastTouchPositions.Count() == 1 && _zoom != 1) // Если есть только одно прикосновение и зум != 1, то токлько перемещение
{
var position = ea.GetPosition(this);
_panX -= (float)(_lastTouchPositions[ea.Pointer.Id].X - position.X);
_panY -= (float)(_lastTouchPositions[ea.Pointer.Id].Y - position.Y); // Ось Y инвертированна
_lastTouchPositions[ea.Pointer.Id] = position;
}
else if (_lastTouchPositions.Count() == 2) // Если два прикосновения, то перемещение с приближением
{
var lastPosition1 = _lastTouchPositions.Skip(0).First().Value;
var lastPosition2 = _lastTouchPositions.Skip(1).First().Value;
var lastDistance = Point.Distance(lastPosition1, lastPosition2);
var lastCenter = PointHelper.Center(lastPosition1, lastPosition2);
_lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this);
var newPosition1 = _lastTouchPositions.Skip(0).First().Value;
var newPosition2 = _lastTouchPositions.Skip(1).First().Value;
var newDistance = Point.Distance(newPosition1, newPosition2);
var newCenter = PointHelper.Center(newPosition1, newPosition2);
var zoomFactor = newDistance / lastDistance;
var newZoom = (float)(_zoom * zoomFactor);
newZoom = (float)Math.Clamp(newZoom, 1f, 10f);
var imageX = (newCenter.X - Bounds.Width / 2 - _panX) / _zoom;
var imageY = (newCenter.Y - Bounds.Height / 2 - _panY) / _zoom;
_zoom = newZoom;
if (_zoom > 1f)
{
_panX = (float)(newCenter.X - Bounds.Width / 2 - imageX * _zoom);
_panY = (float)(newCenter.Y - Bounds.Height / 2 - imageY * _zoom);
}
else
{
_zoom = 1f;
_panX = 0f;
_panY = 0f;
}
}
}
base.OnPointerMoved(ea);
InvalidateVisual();
}
public override void Render(DrawingContext context)
{
using (context.PushClip(new Rect(Bounds.Size)))
{
context.FillRectangle(new SolidColorBrush(Background), new Rect(0, 0, Bounds.Width, Bounds.Height));
if (ImageSource is null)
{
base.Render(context);
return;
}
var (sx, sy) = ComputeStretchScale(StretchMode, ImageSource.Size.Width, ImageSource.Size.Height);
sx *= _zoom;
sy *= _zoom;
var drawWidth = ImageSource.Size.Width * sx;
var drawHeight = ImageSource.Size.Height * sy;
var destRect = new Rect(
(Bounds.Width - drawWidth) / 2 + _panX,
(Bounds.Height - drawHeight) / 2 + _panY,
drawWidth, drawHeight
);
var srcRect = ComputeSourceRect(TileMode, (int)ImageSource.Size.Width, (int)ImageSource.Size.Height);
context.DrawImage(ImageSource, srcRect, destRect);
}
base.Render(context);
}
private (double sx, double sy) ComputeStretchScale(Stretch stretch, double imageWidth, double imageHeight)
{
double viewWidth = Bounds.Width;
double viewHeight = Bounds.Height;
var viewAspect = viewWidth / viewHeight;
var imageAspect = imageWidth / imageHeight;
switch (stretch)
{
case Stretch.None:
return (1, 1);
case Stretch.Fill:
return (viewWidth / imageWidth, viewHeight / imageHeight);
case Stretch.Uniform:
if (imageAspect > viewAspect)
{
var s = viewWidth / imageWidth;
return (s, s);
}
else
{
var s = viewHeight / imageHeight;
return (s, s);
}
case Stretch.UniformToFill:
if (imageAspect > viewAspect)
{
var s = viewHeight / imageHeight;
return (s, s);
}
else
{
var s = viewWidth / imageWidth;
return (s, s);
}
default:
return (1, 1);
}
}
private Rect ComputeSourceRect(TileMode mode, int imageWidth, int imageHeight)
{
switch (mode)
{
case TileMode.None:
return new Rect(0, 0, imageWidth, imageHeight);
case TileMode.Tile:
return new Rect(0, 0, imageWidth * 2, imageHeight * 2);
case TileMode.FlipX:
return new Rect(imageWidth, 0, -imageWidth, imageHeight);
case TileMode.FlipY:
return new Rect(0, imageHeight, imageWidth, -imageHeight);
case TileMode.FlipXY:
return new Rect(imageWidth, imageHeight, -imageWidth, -imageHeight);
default:
return new Rect(0, 0, imageWidth, imageHeight);
}
}
}