From 2c24b9e3d5173d6b42e78d0ff205c9239579083d Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Thu, 26 Feb 2026 06:56:26 +0300 Subject: [PATCH] feat: common ZoomPanImage --- GSS2.UI.Core/OpenCvImage.cs | 283 +------------------------------- GSS2.UI.Core/ZoomPanImage.cs | 302 +++++++++++++++++++++++++++++++++++ 2 files changed, 305 insertions(+), 280 deletions(-) create mode 100644 GSS2.UI.Core/ZoomPanImage.cs diff --git a/GSS2.UI.Core/OpenCvImage.cs b/GSS2.UI.Core/OpenCvImage.cs index 754cd96..9122831 100644 --- a/GSS2.UI.Core/OpenCvImage.cs +++ b/GSS2.UI.Core/OpenCvImage.cs @@ -11,7 +11,7 @@ using GSS2.Core.Extentions; namespace GSS2.UI.Core; -public class OpenCvImage : Control +public class OpenCvImage : ZoomPanImage { private class OpenCvBitmap : Bitmap { @@ -27,31 +27,6 @@ public class OpenCvImage : Control { } } - private Point? _lastmousePosition = null; - private Dictionary? _lastTouchPositions = null; - private float _zoom = 1; - private float _panX = 0; - private float _panY = 0; - private OpenCvBitmap? _bitmap = null; - - public static readonly StyledProperty BackgroundProperty = AvaloniaProperty.Register(nameof(Background), defaultValue: Colors.Transparent); - public Color Background - { - get => GetValue(BackgroundProperty); - set => SetValue(BackgroundProperty, value); - } - public static readonly StyledProperty StretchModeProperty = AvaloniaProperty.Register(nameof(StretchMode), defaultValue: Stretch.Uniform); - public Stretch StretchMode - { - get => GetValue(StretchModeProperty); - set => SetValue(StretchModeProperty, value); - } - public static readonly StyledProperty TileModeProperty = AvaloniaProperty.Register(nameof(TileMode), defaultValue: TileMode.None); - public TileMode TileMode - { - get => GetValue(TileModeProperty); - set => SetValue(TileModeProperty, value); - } public static readonly StyledProperty ImageProperty = AvaloniaProperty.Register(nameof(Image), defaultValue: null); public Mat? Image { @@ -61,22 +36,12 @@ public class OpenCvImage : Control public OpenCvImage() : base() - { - DoubleTapped += (_, _) => - { - _zoom = 1f; - _panX = 0f; - _panY = 0f; - }; - } + { } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == ImageProperty) { - _bitmap?.Dispose(); - _bitmap = null; - if (Image is null) 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)); Cv2.Merge([channels[2], channels[1], channels[0], alphaChannel], image); - _bitmap = new OpenCvBitmap(image); + ImageSource = new OpenCvBitmap(image); 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(); - - 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); - } - } } \ No newline at end of file diff --git a/GSS2.UI.Core/ZoomPanImage.cs b/GSS2.UI.Core/ZoomPanImage.cs new file mode 100644 index 0000000..ad3cb36 --- /dev/null +++ b/GSS2.UI.Core/ZoomPanImage.cs @@ -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? _lastTouchPositions = null; + private float _zoom = 1; + private float _panX = 0; + private float _panY = 0; + + public static readonly StyledProperty BackgroundProperty = AvaloniaProperty.Register(nameof(Background), defaultValue: Colors.Transparent); + public Color Background + { + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + public static readonly StyledProperty StretchModeProperty = AvaloniaProperty.Register(nameof(StretchMode), defaultValue: Stretch.Uniform); + public Stretch StretchMode + { + get => GetValue(StretchModeProperty); + set => SetValue(StretchModeProperty, value); + } + public static readonly StyledProperty TileModeProperty = AvaloniaProperty.Register(nameof(TileMode), defaultValue: TileMode.None); + public TileMode TileMode + { + get => GetValue(TileModeProperty); + set => SetValue(TileModeProperty, value); + } + public static readonly StyledProperty ImageSourceProperty = AvaloniaProperty.Register(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(); + + 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); + } + } +} \ No newline at end of file