Files
AmiReel/Models/SourceCropSelection.cs
klevze e635b708a2 Improve crop editor and copy publish output to dist
Add resize handles, zoom, view brightness, and a multi-frame still
strip. Keep the crop box on mouse-up, and copy AmiReel.exe to dist.
2026-08-15 22:59:20 +02:00

222 lines
8.6 KiB
C#

namespace AmiReel.Models;
/// <summary>Edge insets, in source pixels, removed by a manual crop.</summary>
public readonly record struct PixelInsets(int Left, int Right, int Top, int Bottom)
{
public bool IsEmpty => Left == 0 && Right == 0 && Top == 0 && Bottom == 0;
}
/// <summary>Which part of an existing crop rectangle a pointer is on.</summary>
public enum CropHit
{
None,
Move,
North,
South,
East,
West,
NorthWest,
NorthEast,
SouthWest,
SouthEast,
}
/// <summary>Maps a dragged display rectangle onto source-pixel crop insets.</summary>
public static class SourceCropSelection
{
public const int MinimumSize = 16;
public static (double OffsetX, double OffsetY, double DisplayWidth, double DisplayHeight) FitUniform(
double containerWidth, double containerHeight, int pixelWidth, int pixelHeight)
{
if (containerWidth <= 0 || containerHeight <= 0 || pixelWidth <= 0 || pixelHeight <= 0)
return (0, 0, 0, 0);
double containerAspect = containerWidth / containerHeight;
double imageAspect = pixelWidth / (double)pixelHeight;
double displayWidth;
double displayHeight;
if (containerAspect > imageAspect)
{
displayHeight = containerHeight;
displayWidth = containerHeight * imageAspect;
}
else
{
displayWidth = containerWidth;
displayHeight = containerWidth / imageAspect;
}
return ((containerWidth - displayWidth) / 2, (containerHeight - displayHeight) / 2, displayWidth, displayHeight);
}
public static PixelInsets ToInsets(
int pixelWidth,
int pixelHeight,
double displayWidth,
double displayHeight,
double x,
double y,
double width,
double height)
{
if (pixelWidth <= 0 || pixelHeight <= 0 || displayWidth <= 0 || displayHeight <= 0)
return default;
Normalize(ref x, ref y, ref width, ref height);
x = Math.Clamp(x, 0, displayWidth);
y = Math.Clamp(y, 0, displayHeight);
width = Math.Clamp(width, 0, displayWidth - x);
height = Math.Clamp(height, 0, displayHeight - y);
double scaleX = pixelWidth / displayWidth;
double scaleY = pixelHeight / displayHeight;
int left = ToEvenPixel(x * scaleX, pixelWidth - MinimumSize);
int top = ToEvenPixel(y * scaleY, pixelHeight - MinimumSize);
int cropWidth = ToEvenPixel(width * scaleX, pixelWidth - left);
int cropHeight = ToEvenPixel(height * scaleY, pixelHeight - top);
if (cropWidth < MinimumSize)
cropWidth = Math.Min(MinimumSize, pixelWidth - left);
if (cropHeight < MinimumSize)
cropHeight = Math.Min(MinimumSize, pixelHeight - top);
return new PixelInsets(left, pixelWidth - left - cropWidth, top, pixelHeight - top - cropHeight);
}
public static (double X, double Y, double Width, double Height) ToDisplayRect(
int pixelWidth, int pixelHeight, double displayWidth, double displayHeight, PixelInsets insets)
{
if (pixelWidth <= 0 || pixelHeight <= 0 || displayWidth <= 0 || displayHeight <= 0)
return (0, 0, Math.Max(0, displayWidth), Math.Max(0, displayHeight));
double scaleX = displayWidth / pixelWidth;
double scaleY = displayHeight / pixelHeight;
double x = insets.Left * scaleX;
double y = insets.Top * scaleY;
double width = Math.Max(1, (pixelWidth - insets.Left - insets.Right) * scaleX);
double height = Math.Max(1, (pixelHeight - insets.Top - insets.Bottom) * scaleY);
return (x, y, width, height);
}
public static (double X, double Y) ClampToDisplay(double x, double y, double displayWidth, double displayHeight) =>
(Math.Clamp(x, 0, Math.Max(0, displayWidth)), Math.Clamp(y, 0, Math.Max(0, displayHeight)));
public static CropHit HitTest(
double rectX, double rectY, double rectWidth, double rectHeight, double pointerX, double pointerY, double handleSize)
{
if (rectWidth <= 0 || rectHeight <= 0 || handleSize < 0)
return CropHit.None;
bool nearLeft = Math.Abs(pointerX - rectX) <= handleSize;
bool nearRight = Math.Abs(pointerX - (rectX + rectWidth)) <= handleSize;
bool nearTop = Math.Abs(pointerY - rectY) <= handleSize;
bool nearBottom = Math.Abs(pointerY - (rectY + rectHeight)) <= handleSize;
bool alongX = pointerX >= rectX - handleSize && pointerX <= rectX + rectWidth + handleSize;
bool alongY = pointerY >= rectY - handleSize && pointerY <= rectY + rectHeight + handleSize;
if (nearTop && nearLeft)
return CropHit.NorthWest;
if (nearTop && nearRight)
return CropHit.NorthEast;
if (nearBottom && nearLeft)
return CropHit.SouthWest;
if (nearBottom && nearRight)
return CropHit.SouthEast;
if (nearTop && alongX)
return CropHit.North;
if (nearBottom && alongX)
return CropHit.South;
if (nearLeft && alongY)
return CropHit.West;
if (nearRight && alongY)
return CropHit.East;
if (pointerX >= rectX && pointerX <= rectX + rectWidth && pointerY >= rectY && pointerY <= rectY + rectHeight)
return CropHit.Move;
return CropHit.None;
}
public static (double X, double Y, double Width, double Height) AdjustRect(
double x,
double y,
double width,
double height,
CropHit hit,
double deltaX,
double deltaY,
double displayWidth,
double displayHeight)
{
const double minSize = 8;
double left = x;
double top = y;
double right = x + width;
double bottom = y + height;
if (hit == CropHit.Move)
{
left = Math.Clamp(left + deltaX, 0, Math.Max(0, displayWidth - width));
top = Math.Clamp(top + deltaY, 0, Math.Max(0, displayHeight - height));
return (left, top, width, height);
}
if (hit is CropHit.West or CropHit.NorthWest or CropHit.SouthWest)
left = Math.Clamp(left + deltaX, 0, right - minSize);
if (hit is CropHit.East or CropHit.NorthEast or CropHit.SouthEast)
right = Math.Clamp(right + deltaX, left + minSize, displayWidth);
if (hit is CropHit.North or CropHit.NorthWest or CropHit.NorthEast)
top = Math.Clamp(top + deltaY, 0, bottom - minSize);
if (hit is CropHit.South or CropHit.SouthWest or CropHit.SouthEast)
bottom = Math.Clamp(bottom + deltaY, top + minSize, displayHeight);
return (left, top, Math.Max(minSize, right - left), Math.Max(minSize, bottom - top));
}
public static PixelInsets Nudge(PixelInsets insets, int pixelWidth, int pixelHeight, int deltaX, int deltaY)
{
int width = pixelWidth - insets.Left - insets.Right;
int height = pixelHeight - insets.Top - insets.Bottom;
if (width < MinimumSize || height < MinimumSize)
return insets;
int left = ToEvenPixel(insets.Left + deltaX, pixelWidth - width);
int top = ToEvenPixel(insets.Top + deltaY, pixelHeight - height);
return new PixelInsets(left, pixelWidth - left - width, top, pixelHeight - top - height);
}
public static PixelInsets NudgeEdge(
PixelInsets insets, int pixelWidth, int pixelHeight, int dLeft, int dTop, int dRight, int dBottom)
{
int left = ToEvenPixel(insets.Left + dLeft, pixelWidth - MinimumSize);
int top = ToEvenPixel(insets.Top + dTop, pixelHeight - MinimumSize);
int right = ToEvenPixel(insets.Right + dRight, pixelWidth - MinimumSize - left);
int bottom = ToEvenPixel(insets.Bottom + dBottom, pixelHeight - MinimumSize - top);
if (pixelWidth - left - right < MinimumSize)
right = Math.Max(0, pixelWidth - left - MinimumSize);
if (pixelHeight - top - bottom < MinimumSize)
bottom = Math.Max(0, pixelHeight - top - MinimumSize);
return new PixelInsets(left, right, top, bottom);
}
private static void Normalize(ref double x, ref double y, ref double width, ref double height)
{
if (width < 0)
{
x += width;
width = -width;
}
if (height < 0)
{
y += height;
height = -height;
}
}
private static int ToEvenPixel(double value, int max)
{
int rounded = (int)Math.Round(value, MidpointRounding.AwayFromZero);
int clamped = Math.Clamp(rounded, 0, Math.Max(0, max));
return (clamped / 2) * 2;
}
}