Files
Minint/Minint/Controls/Viewport.cs
2026-03-29 16:32:04 +03:00

120 lines
4.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}