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.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="AmiReel.Controls.SourceCropEditor"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel Spacing="10" MinWidth="640">
|
||||
<TextBlock x:Name="StatusText"
|
||||
Text="Drag on the frame to keep that region. The rest is cropped away."
|
||||
Style="{StaticResource MutedTextStyle}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<ProgressBar x:Name="LoadProgress"
|
||||
IsIndeterminate="True"
|
||||
Height="6"
|
||||
Visibility="Collapsed"
|
||||
AutomationProperties.Name="Loading source frame" />
|
||||
<Border Background="{ThemeResource PageBrush}"
|
||||
BorderBrush="{ThemeResource BorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<Grid x:Name="CropHost"
|
||||
Height="420"
|
||||
Background="Transparent"
|
||||
PointerPressed="CropHost_PointerPressed"
|
||||
PointerMoved="CropHost_PointerMoved"
|
||||
PointerReleased="CropHost_PointerReleased"
|
||||
PointerCaptureLost="CropHost_PointerCaptureLost"
|
||||
SizeChanged="CropHost_SizeChanged"
|
||||
AutomationProperties.Name="Source crop frame"
|
||||
AutomationProperties.AutomationId="SourceCropHost">
|
||||
<Image x:Name="SourceImage"
|
||||
Stretch="Uniform"
|
||||
IsHitTestVisible="False"
|
||||
AutomationProperties.Name="Source video frame" />
|
||||
<Canvas x:Name="OverlayCanvas" IsHitTestVisible="False">
|
||||
<Rectangle x:Name="SelectionRect"
|
||||
Stroke="{ThemeResource AccentBrush}"
|
||||
StrokeThickness="2"
|
||||
Fill="#33007ACC"
|
||||
Visibility="Collapsed" />
|
||||
</Canvas>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Grid ColumnSpacing="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="InsetsText"
|
||||
Grid.Column="0"
|
||||
Text="Crop L0 R0 T0 B0"
|
||||
Style="{StaticResource MutedTextStyle}"
|
||||
VerticalAlignment="Center" />
|
||||
<Button x:Name="ClearCropButton"
|
||||
Grid.Column="1"
|
||||
Content="Clear crop"
|
||||
Click="ClearCrop_Click"
|
||||
AutomationProperties.Name="Clear crop"
|
||||
AutomationProperties.AutomationId="ClearSourceCropButton" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,174 @@
|
||||
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}";
|
||||
}
|
||||
Reference in New Issue
Block a user