Files
AmiReel/Controls/SourceCropEditor.xaml.cs
T
klevze 323324d713 Add Shorts look preview and visual crop editor
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.
2026-08-15 21:08:36 +02:00

175 lines
6.3 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.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}";
}