Show opening/closing 9:16 stills from the same filtergraph, and let users drag a keep-region on a source frame to fill the crop insets.
175 lines
6.3 KiB
C#
175 lines
6.3 KiB
C#
using System.Runtime.InteropServices.WindowsRuntime;
|
||
using AmiReel.Models;
|
||
using AmiReel.Services;
|
||
using Microsoft.UI.Input;
|
||
using Microsoft.UI.Xaml;
|
||
using Microsoft.UI.Xaml.Controls;
|
||
using Microsoft.UI.Xaml.Input;
|
||
using Microsoft.UI.Xaml.Media.Imaging;
|
||
using Windows.Foundation;
|
||
|
||
namespace AmiReel.Controls;
|
||
|
||
public sealed partial class SourceCropEditor : UserControl
|
||
{
|
||
private int _pixelWidth;
|
||
private int _pixelHeight;
|
||
private bool _isDragging;
|
||
private Point _dragStart;
|
||
private PixelInsets _insets;
|
||
|
||
public SourceCropEditor()
|
||
{
|
||
InitializeComponent();
|
||
}
|
||
|
||
public PixelInsets Insets => _insets;
|
||
|
||
public async Task LoadVideoFrameAsync(
|
||
string inputFile, string start, string ffmpegPath, string ffprobePath, CancellationToken token)
|
||
{
|
||
LoadProgress.Visibility = Visibility.Visible;
|
||
StatusText.Text = "Loading a source frame...";
|
||
try
|
||
{
|
||
string directory = Path.Combine(Path.GetTempPath(), "AmiReel", "crop-editor");
|
||
Directory.CreateDirectory(directory);
|
||
string framePath = Path.Combine(directory, "source-frame.png");
|
||
await new ShortsPipeline().ExtractSourceFrameAsync(
|
||
inputFile, start, ffmpegPath, ffprobePath, framePath, _ => { }, token);
|
||
|
||
await using FileStream stream = File.OpenRead(framePath);
|
||
BitmapImage bitmap = new();
|
||
await bitmap.SetSourceAsync(stream.AsRandomAccessStream());
|
||
SourceImage.Source = bitmap;
|
||
_pixelWidth = bitmap.PixelWidth;
|
||
_pixelHeight = bitmap.PixelHeight;
|
||
StatusText.Text = $"{_pixelWidth} × {_pixelHeight} · drag on the frame to choose the kept region.";
|
||
RefreshSelectionVisual();
|
||
}
|
||
finally
|
||
{
|
||
LoadProgress.Visibility = Visibility.Collapsed;
|
||
}
|
||
}
|
||
|
||
public void SetInsets(PixelInsets insets)
|
||
{
|
||
_insets = insets;
|
||
RefreshSelectionVisual();
|
||
UpdateInsetsText();
|
||
}
|
||
|
||
private void CropHost_PointerPressed(object sender, PointerRoutedEventArgs e)
|
||
{
|
||
if (_pixelWidth <= 0 || !TryMapToDisplay(e, out double x, out double y, out _, out _))
|
||
return;
|
||
|
||
_isDragging = true;
|
||
_dragStart = new Point(x, y);
|
||
CropHost.CapturePointer(e.Pointer);
|
||
UpdateDraftSelection(x, y);
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void CropHost_PointerMoved(object sender, PointerRoutedEventArgs e)
|
||
{
|
||
if (!_isDragging)
|
||
return;
|
||
if (!TryMapToDisplay(e, out double x, out double y, out _, out _))
|
||
return;
|
||
|
||
UpdateDraftSelection(x, y);
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void CropHost_PointerReleased(object sender, PointerRoutedEventArgs e)
|
||
{
|
||
if (!_isDragging)
|
||
return;
|
||
|
||
_isDragging = false;
|
||
CropHost.ReleasePointerCapture(e.Pointer);
|
||
if (TryMapToDisplay(e, out double x, out double y, out double displayWidth, out double displayHeight))
|
||
CommitSelection(x, y, displayWidth, displayHeight);
|
||
e.Handled = true;
|
||
}
|
||
|
||
private void CropHost_PointerCaptureLost(object sender, PointerRoutedEventArgs e)
|
||
{
|
||
_isDragging = false;
|
||
}
|
||
|
||
private void CropHost_SizeChanged(object sender, SizeChangedEventArgs e) => RefreshSelectionVisual();
|
||
|
||
private void ClearCrop_Click(object sender, RoutedEventArgs e) => SetInsets(default);
|
||
|
||
private bool TryMapToDisplay(
|
||
PointerRoutedEventArgs e, out double x, out double y, out double displayWidth, out double displayHeight)
|
||
{
|
||
(double offsetX, double offsetY, displayWidth, displayHeight) = CurrentFit();
|
||
x = 0;
|
||
y = 0;
|
||
if (displayWidth <= 0 || displayHeight <= 0)
|
||
return false;
|
||
|
||
PointerPoint point = e.GetCurrentPoint(CropHost);
|
||
(x, y) = SourceCropSelection.ClampToDisplay(
|
||
point.Position.X - offsetX, point.Position.Y - offsetY, displayWidth, displayHeight);
|
||
return true;
|
||
}
|
||
|
||
private void UpdateDraftSelection(double x, double y)
|
||
{
|
||
(double offsetX, double offsetY, double displayWidth, double displayHeight) = CurrentFit();
|
||
if (displayWidth <= 0)
|
||
return;
|
||
|
||
_insets = SourceCropSelection.ToInsets(
|
||
_pixelWidth, _pixelHeight, displayWidth, displayHeight,
|
||
_dragStart.X, _dragStart.Y, x - _dragStart.X, y - _dragStart.Y);
|
||
PlaceSelectionRect(offsetX, offsetY, displayWidth, displayHeight);
|
||
UpdateInsetsText();
|
||
}
|
||
|
||
private void CommitSelection(double x, double y, double displayWidth, double displayHeight)
|
||
{
|
||
_insets = SourceCropSelection.ToInsets(
|
||
_pixelWidth, _pixelHeight, displayWidth, displayHeight,
|
||
_dragStart.X, _dragStart.Y, x - _dragStart.X, y - _dragStart.Y);
|
||
RefreshSelectionVisual();
|
||
UpdateInsetsText();
|
||
}
|
||
|
||
private void RefreshSelectionVisual()
|
||
{
|
||
(double offsetX, double offsetY, double displayWidth, double displayHeight) = CurrentFit();
|
||
if (displayWidth <= 0 || _pixelWidth <= 0)
|
||
{
|
||
SelectionRect.Visibility = Visibility.Collapsed;
|
||
return;
|
||
}
|
||
|
||
PlaceSelectionRect(offsetX, offsetY, displayWidth, displayHeight);
|
||
}
|
||
|
||
private void PlaceSelectionRect(double offsetX, double offsetY, double displayWidth, double displayHeight)
|
||
{
|
||
(double x, double y, double width, double height) = SourceCropSelection.ToDisplayRect(
|
||
_pixelWidth, _pixelHeight, displayWidth, displayHeight, _insets);
|
||
Canvas.SetLeft(SelectionRect, offsetX + x);
|
||
Canvas.SetTop(SelectionRect, offsetY + y);
|
||
SelectionRect.Width = width;
|
||
SelectionRect.Height = height;
|
||
SelectionRect.Visibility = _insets.IsEmpty ? Visibility.Collapsed : Visibility.Visible;
|
||
}
|
||
|
||
private (double OffsetX, double OffsetY, double DisplayWidth, double DisplayHeight) CurrentFit() =>
|
||
SourceCropSelection.FitUniform(CropHost.ActualWidth, CropHost.ActualHeight, _pixelWidth, _pixelHeight);
|
||
|
||
private void UpdateInsetsText() =>
|
||
InsetsText.Text = _pixelWidth <= 0
|
||
? "Crop L0 R0 T0 B0"
|
||
: $"Crop L{_insets.Left} R{_insets.Right} T{_insets.Top} B{_insets.Bottom} · {_pixelWidth - _insets.Left - _insets.Right}×{_pixelHeight - _insets.Top - _insets.Bottom}";
|
||
}
|