120 lines
4.7 KiB
C#
120 lines
4.7 KiB
C#
using System;
|
||
using Avalonia;
|
||
|
||
namespace Minint.Controls;
|
||
|
||
/// <summary>
|
||
/// Manages zoom level and pan offset for the pixel canvas.
|
||
/// Provides screen↔pixel coordinate transforms.
|
||
/// </summary>
|
||
public sealed class Viewport
|
||
{
|
||
public double Zoom { get; set; } = 1.0;
|
||
public double OffsetX { get; set; }
|
||
public double OffsetY { get; set; }
|
||
|
||
public const double MinZoom = 0.25;
|
||
public const double MaxZoom = 128.0;
|
||
|
||
/// <summary>
|
||
/// Zoom base per 1.0 unit of wheel delta. Actual factor = Pow(base, |delta|).
|
||
/// Touchpad nudge (delta ~0.1) → ~1.01×, mouse tick (delta 1.0) → 1.10×, fast (3.0) → 1.33×.
|
||
/// </summary>
|
||
private const double ZoomBase = 1.10;
|
||
|
||
public (int X, int Y) ScreenToPixel(double screenX, double screenY) =>
|
||
((int)Math.Floor((screenX - OffsetX) / Zoom),
|
||
(int)Math.Floor((screenY - OffsetY) / Zoom));
|
||
|
||
public (double X, double Y) PixelToScreen(int pixelX, int pixelY) =>
|
||
(pixelX * Zoom + OffsetX,
|
||
pixelY * Zoom + OffsetY);
|
||
|
||
public Rect ImageScreenRect(int imageWidth, int imageHeight) =>
|
||
new(OffsetX, OffsetY, imageWidth * Zoom, imageHeight * Zoom);
|
||
|
||
/// <summary>
|
||
/// Zooms keeping the point under cursor fixed.
|
||
/// Uses the actual magnitude of <paramref name="delta"/> for proportional zoom.
|
||
/// </summary>
|
||
public void ZoomAtPoint(double screenX, double screenY, double delta,
|
||
int imageWidth, int imageHeight, double controlWidth, double controlHeight)
|
||
{
|
||
double absDelta = Math.Abs(delta);
|
||
double factor = delta > 0 ? Math.Pow(ZoomBase, absDelta) : 1.0 / Math.Pow(ZoomBase, absDelta);
|
||
double newZoom = Math.Clamp(Zoom * factor, MinZoom, MaxZoom);
|
||
if (Math.Abs(newZoom - Zoom) < 1e-12) return;
|
||
|
||
double pixelX = (screenX - OffsetX) / Zoom;
|
||
double pixelY = (screenY - OffsetY) / Zoom;
|
||
Zoom = newZoom;
|
||
OffsetX = screenX - pixelX * Zoom;
|
||
OffsetY = screenY - pixelY * Zoom;
|
||
|
||
ClampOffset(imageWidth, imageHeight, controlWidth, controlHeight);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Pans by screen-space delta, then clamps so the image can't be scrolled out of view.
|
||
/// </summary>
|
||
public void Pan(double deltaX, double deltaY,
|
||
int imageWidth, int imageHeight, double controlWidth, double controlHeight)
|
||
{
|
||
OffsetX += deltaX;
|
||
OffsetY += deltaY;
|
||
ClampOffset(imageWidth, imageHeight, controlWidth, controlHeight);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sets offset directly (e.g. from middle-mouse drag), then clamps.
|
||
/// </summary>
|
||
public void SetOffset(double offsetX, double offsetY,
|
||
int imageWidth, int imageHeight, double controlWidth, double controlHeight)
|
||
{
|
||
OffsetX = offsetX;
|
||
OffsetY = offsetY;
|
||
ClampOffset(imageWidth, imageHeight, controlWidth, controlHeight);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Ensures at least <c>minVisible</c> pixels of the image remain on screen on each edge.
|
||
/// </summary>
|
||
public void ClampOffset(int imageWidth, int imageHeight, double controlWidth, double controlHeight)
|
||
{
|
||
double extentW = imageWidth * Zoom;
|
||
double extentH = imageHeight * Zoom;
|
||
|
||
double minVisH = Math.Max(32, Math.Min(controlWidth, extentW) * 0.10);
|
||
double minVisV = Math.Max(32, Math.Min(controlHeight, extentH) * 0.10);
|
||
|
||
// Image right edge must be >= minVisH from left of control
|
||
// Image left edge must be <= controlWidth - minVisH from left
|
||
OffsetX = Math.Clamp(OffsetX, minVisH - extentW, controlWidth - minVisH);
|
||
OffsetY = Math.Clamp(OffsetY, minVisV - extentH, controlHeight - minVisV);
|
||
}
|
||
|
||
public void FitToView(int imageWidth, int imageHeight, double controlWidth, double controlHeight)
|
||
{
|
||
if (imageWidth <= 0 || imageHeight <= 0 || controlWidth <= 0 || controlHeight <= 0)
|
||
return;
|
||
|
||
double scaleX = controlWidth / imageWidth;
|
||
double scaleY = controlHeight / imageHeight;
|
||
Zoom = Math.Max(1.0, Math.Floor(Math.Min(scaleX, scaleY)));
|
||
|
||
OffsetX = (controlWidth - imageWidth * Zoom) / 2.0;
|
||
OffsetY = (controlHeight - imageHeight * Zoom) / 2.0;
|
||
}
|
||
|
||
public (double Min, double Max, double Value, double ViewportSize)
|
||
GetScrollInfo(int imageSize, double controlSize, double offset)
|
||
{
|
||
double extent = imageSize * Zoom;
|
||
double minVis = Math.Max(32, Math.Min(controlSize, extent) * 0.10);
|
||
double min = minVis - extent;
|
||
double max = controlSize - minVis;
|
||
double viewportSize = Math.Min(controlSize, extent);
|
||
return (min, max, offset, viewportSize);
|
||
}
|
||
}
|