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.
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
<StackPanel Spacing="10" MinWidth="640">
|
||||
<TextBlock x:Name="StatusText"
|
||||
Text="Drag on the frame to keep that region. The rest is cropped away."
|
||||
Text="Drag a keep-region, then resize it with the handles. Zoom and brightness are for viewing only."
|
||||
Style="{StaticResource MutedTextStyle}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<ProgressBar x:Name="LoadProgress"
|
||||
@@ -17,32 +17,95 @@
|
||||
Height="6"
|
||||
Visibility="Collapsed"
|
||||
AutomationProperties.Name="Loading source frame" />
|
||||
<Grid ColumnSpacing="16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Slider x:Name="ZoomSlider"
|
||||
Grid.Column="0"
|
||||
Header="Zoom"
|
||||
Minimum="1"
|
||||
Maximum="4"
|
||||
StepFrequency="0.25"
|
||||
TickFrequency="0.5"
|
||||
SnapsTo="StepValues"
|
||||
Value="1"
|
||||
ValueChanged="ZoomSlider_ValueChanged"
|
||||
AutomationProperties.Name="View zoom"
|
||||
AutomationProperties.AutomationId="CropZoomSlider" />
|
||||
<Slider x:Name="BrightnessSlider"
|
||||
Grid.Column="1"
|
||||
Header="View brightness"
|
||||
Minimum="-0.7"
|
||||
Maximum="0.7"
|
||||
StepFrequency="0.05"
|
||||
TickFrequency="0.35"
|
||||
SnapsTo="StepValues"
|
||||
Value="0"
|
||||
ValueChanged="BrightnessSlider_ValueChanged"
|
||||
AutomationProperties.Name="View brightness"
|
||||
AutomationProperties.AutomationId="CropBrightnessSlider" />
|
||||
</Grid>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Disabled"
|
||||
HorizontalScrollMode="Auto"
|
||||
VerticalScrollMode="Disabled"
|
||||
AutomationProperties.Name="Crop preview frames">
|
||||
<StackPanel x:Name="FrameStrip" Orientation="Horizontal" Spacing="8" />
|
||||
</ScrollViewer>
|
||||
<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>
|
||||
<ScrollViewer x:Name="CropScroller"
|
||||
Height="420"
|
||||
ZoomMode="Disabled"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollMode="Auto"
|
||||
VerticalScrollMode="Auto"
|
||||
SizeChanged="CropScroller_SizeChanged"
|
||||
PointerWheelChanged="CropScroller_PointerWheelChanged"
|
||||
AutomationProperties.Name="Source crop view">
|
||||
<Grid x:Name="ImageSurface"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
IsTabStop="True"
|
||||
UseSystemFocusVisuals="True"
|
||||
PointerPressed="ImageSurface_PointerPressed"
|
||||
PointerMoved="ImageSurface_PointerMoved"
|
||||
PointerReleased="ImageSurface_PointerReleased"
|
||||
PointerCaptureLost="ImageSurface_PointerCaptureLost"
|
||||
KeyDown="ImageSurface_KeyDown"
|
||||
AutomationProperties.Name="Source crop frame"
|
||||
AutomationProperties.AutomationId="SourceCropHost">
|
||||
<Image x:Name="SourceImage"
|
||||
Stretch="Fill"
|
||||
IsHitTestVisible="False"
|
||||
AutomationProperties.Name="Source video frame" />
|
||||
<Rectangle x:Name="BrightnessOverlay"
|
||||
IsHitTestVisible="False"
|
||||
Fill="White"
|
||||
Opacity="0" />
|
||||
<Canvas x:Name="OverlayCanvas" IsHitTestVisible="False">
|
||||
<Rectangle x:Name="SelectionRect"
|
||||
Stroke="{ThemeResource AccentBrush}"
|
||||
StrokeThickness="2"
|
||||
Fill="#33007ACC"
|
||||
Visibility="Collapsed" />
|
||||
<Rectangle x:Name="HandleNW" Width="10" Height="10" Fill="{ThemeResource AccentBrush}" Stroke="{ThemeResource PageBrush}" StrokeThickness="1" Visibility="Collapsed" />
|
||||
<Rectangle x:Name="HandleN" Width="10" Height="10" Fill="{ThemeResource AccentBrush}" Stroke="{ThemeResource PageBrush}" StrokeThickness="1" Visibility="Collapsed" />
|
||||
<Rectangle x:Name="HandleNE" Width="10" Height="10" Fill="{ThemeResource AccentBrush}" Stroke="{ThemeResource PageBrush}" StrokeThickness="1" Visibility="Collapsed" />
|
||||
<Rectangle x:Name="HandleE" Width="10" Height="10" Fill="{ThemeResource AccentBrush}" Stroke="{ThemeResource PageBrush}" StrokeThickness="1" Visibility="Collapsed" />
|
||||
<Rectangle x:Name="HandleSE" Width="10" Height="10" Fill="{ThemeResource AccentBrush}" Stroke="{ThemeResource PageBrush}" StrokeThickness="1" Visibility="Collapsed" />
|
||||
<Rectangle x:Name="HandleS" Width="10" Height="10" Fill="{ThemeResource AccentBrush}" Stroke="{ThemeResource PageBrush}" StrokeThickness="1" Visibility="Collapsed" />
|
||||
<Rectangle x:Name="HandleSW" Width="10" Height="10" Fill="{ThemeResource AccentBrush}" Stroke="{ThemeResource PageBrush}" StrokeThickness="1" Visibility="Collapsed" />
|
||||
<Rectangle x:Name="HandleW" Width="10" Height="10" Fill="{ThemeResource AccentBrush}" Stroke="{ThemeResource PageBrush}" StrokeThickness="1" Visibility="Collapsed" />
|
||||
</Canvas>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
<Grid ColumnSpacing="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
|
||||
@@ -1,21 +1,43 @@
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using AmiReel.Models;
|
||||
using AmiReel.Services;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.Foundation;
|
||||
using Windows.System;
|
||||
using Windows.UI.Core;
|
||||
|
||||
namespace AmiReel.Controls;
|
||||
|
||||
public sealed partial class SourceCropEditor : UserControl
|
||||
{
|
||||
private const double HandleHitSize = 12;
|
||||
private const double HandleVisualSize = 10;
|
||||
private const double MinZoom = 1;
|
||||
private const double MaxZoom = 4;
|
||||
private const double ZoomStep = 0.25;
|
||||
private const int ThumbnailWidth = 128;
|
||||
|
||||
private readonly List<Button> _frameButtons = [];
|
||||
private readonly List<string> _framePaths = [];
|
||||
private readonly List<string> _frameLabels = [];
|
||||
private readonly List<BitmapImage?> _fullFrames = [];
|
||||
private int _selectedFrame;
|
||||
|
||||
private int _pixelWidth;
|
||||
private int _pixelHeight;
|
||||
private double _zoom = 1;
|
||||
private bool _isDragging;
|
||||
private CropHit _dragHit = CropHit.None;
|
||||
private Point _dragStart;
|
||||
private Rect _dragOriginRect;
|
||||
private PixelInsets _insets;
|
||||
|
||||
public SourceCropEditor()
|
||||
@@ -26,26 +48,34 @@ public sealed partial class SourceCropEditor : UserControl
|
||||
public PixelInsets Insets => _insets;
|
||||
|
||||
public async Task LoadVideoFrameAsync(
|
||||
string inputFile, string start, string ffmpegPath, string ffprobePath, CancellationToken token)
|
||||
string inputFile, string start, double durationSeconds, string ffmpegPath, string ffprobePath, CancellationToken token)
|
||||
{
|
||||
LoadProgress.Visibility = Visibility.Visible;
|
||||
StatusText.Text = "Loading a source frame...";
|
||||
StatusText.Text = "Loading preview frames...";
|
||||
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);
|
||||
IReadOnlyList<double> offsets = ShortsPipeline.CropPreviewOffsets(durationSeconds);
|
||||
string directory = Path.Combine(Path.GetTempPath(), "AmiReel", "crop-editor", Guid.NewGuid().ToString("N"));
|
||||
ShortsPipeline pipeline = new();
|
||||
IReadOnlyList<string> paths = await pipeline.ExtractSourceFramesAsync(
|
||||
inputFile, start, offsets, ffmpegPath, ffprobePath, directory, _ => { }, 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();
|
||||
_framePaths.Clear();
|
||||
_frameLabels.Clear();
|
||||
_fullFrames.Clear();
|
||||
_frameButtons.Clear();
|
||||
FrameStrip.Children.Clear();
|
||||
|
||||
for (int index = 0; index < paths.Count; index++)
|
||||
{
|
||||
_framePaths.Add(paths[index]);
|
||||
_frameLabels.Add(ShortsPipeline.FormatPreviewOffsetLabel(offsets[index]));
|
||||
_fullFrames.Add(null);
|
||||
FrameStrip.Children.Add(CreateFrameButton(index, paths[index], _frameLabels[index]));
|
||||
}
|
||||
|
||||
await ShowFrameAsync(0);
|
||||
StatusText.Text = $"{_pixelWidth} × {_pixelHeight} · pick a still, then drag a region and fine-tune the handles.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -60,112 +90,394 @@ public sealed partial class SourceCropEditor : UserControl
|
||||
UpdateInsetsText();
|
||||
}
|
||||
|
||||
private void CropHost_PointerPressed(object sender, PointerRoutedEventArgs e)
|
||||
private Button CreateFrameButton(int index, string path, string label)
|
||||
{
|
||||
if (_pixelWidth <= 0 || !TryMapToDisplay(e, out double x, out double y, out _, out _))
|
||||
Image thumb = new()
|
||||
{
|
||||
Width = 112,
|
||||
Height = 63,
|
||||
Stretch = Stretch.Uniform,
|
||||
IsHitTestVisible = false,
|
||||
};
|
||||
_ = LoadThumbnailAsync(thumb, path);
|
||||
|
||||
TextBlock caption = new()
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 11,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Style = (Style)Application.Current.Resources["MutedTextStyle"],
|
||||
};
|
||||
|
||||
StackPanel content = new() { Spacing = 4 };
|
||||
content.Children.Add(thumb);
|
||||
content.Children.Add(caption);
|
||||
|
||||
Button button = new()
|
||||
{
|
||||
Content = content,
|
||||
Padding = new Thickness(6),
|
||||
Tag = index,
|
||||
BorderThickness = new Thickness(2),
|
||||
};
|
||||
AutomationProperties.SetName(button, $"Preview at {label}");
|
||||
AutomationProperties.SetAutomationId(button, $"CropPreviewFrame{index}");
|
||||
button.Click += FrameButton_Click;
|
||||
_frameButtons.Add(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
private async void FrameButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button button || button.Tag is not int index)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await ShowFrameAsync(index);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
StatusText.Text = exception.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowFrameAsync(int index)
|
||||
{
|
||||
if (index < 0 || index >= _framePaths.Count)
|
||||
return;
|
||||
|
||||
_selectedFrame = index;
|
||||
UpdateFrameButtonStyles();
|
||||
|
||||
if (_fullFrames[index] is null)
|
||||
_fullFrames[index] = await LoadBitmapAsync(_framePaths[index], decodePixelWidth: 0);
|
||||
|
||||
BitmapImage bitmap = _fullFrames[index]!;
|
||||
SourceImage.Source = bitmap;
|
||||
if (bitmap.PixelWidth > 0 && bitmap.PixelHeight > 0)
|
||||
{
|
||||
_pixelWidth = bitmap.PixelWidth;
|
||||
_pixelHeight = bitmap.PixelHeight;
|
||||
}
|
||||
|
||||
ApplyViewTransform();
|
||||
RefreshSelectionVisual();
|
||||
UpdateInsetsText();
|
||||
StatusText.Text = $"{_pixelWidth} × {_pixelHeight} · {_frameLabels[index]} ({index + 1} of {_framePaths.Count})";
|
||||
}
|
||||
|
||||
private void UpdateFrameButtonStyles()
|
||||
{
|
||||
Brush accent = (Brush)Application.Current.Resources["AccentBrush"];
|
||||
Brush border = (Brush)Application.Current.Resources["BorderBrush"];
|
||||
for (int index = 0; index < _frameButtons.Count; index++)
|
||||
_frameButtons[index].BorderBrush = index == _selectedFrame ? accent : border;
|
||||
}
|
||||
|
||||
private static async Task LoadThumbnailAsync(Image target, string path)
|
||||
{
|
||||
target.Source = await LoadBitmapAsync(path, ThumbnailWidth);
|
||||
}
|
||||
|
||||
private static async Task<BitmapImage> LoadBitmapAsync(string path, int decodePixelWidth)
|
||||
{
|
||||
await using FileStream stream = File.OpenRead(path);
|
||||
BitmapImage bitmap = new();
|
||||
if (decodePixelWidth > 0)
|
||||
bitmap.DecodePixelWidth = decodePixelWidth;
|
||||
await bitmap.SetSourceAsync(stream.AsRandomAccessStream());
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private void ImageSurface_PointerPressed(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (_pixelWidth <= 0 || !TryMapToDisplay(e, out double x, out double y, out double displayWidth, out double displayHeight))
|
||||
return;
|
||||
|
||||
ImageSurface.Focus(FocusState.Pointer);
|
||||
(double rectX, double rectY, double rectW, double rectH) = CurrentDisplayRect(displayWidth, displayHeight);
|
||||
CropHit hit = _insets.IsEmpty
|
||||
? CropHit.None
|
||||
: SourceCropSelection.HitTest(rectX, rectY, rectW, rectH, x, y, HandleHitSize);
|
||||
|
||||
_isDragging = true;
|
||||
_dragHit = hit;
|
||||
_dragStart = new Point(x, y);
|
||||
CropHost.CapturePointer(e.Pointer);
|
||||
UpdateDraftSelection(x, y);
|
||||
_dragOriginRect = new Rect(rectX, rectY, rectW, rectH);
|
||||
ImageSurface.CapturePointer(e.Pointer);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void CropHost_PointerMoved(object sender, PointerRoutedEventArgs e)
|
||||
private void ImageSurface_PointerMoved(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (!_isDragging)
|
||||
{
|
||||
UpdateHoverCursor(e);
|
||||
return;
|
||||
if (!TryMapToDisplay(e, out double x, out double y, out _, out _))
|
||||
}
|
||||
|
||||
if (!TryMapToDisplay(e, out double x, out double y, out double displayWidth, out double displayHeight))
|
||||
return;
|
||||
|
||||
UpdateDraftSelection(x, y);
|
||||
if (_dragHit == CropHit.None)
|
||||
UpdateDraftSelection(x, y);
|
||||
else
|
||||
UpdateAdjustedSelection(x, y, displayWidth, displayHeight);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void CropHost_PointerReleased(object sender, PointerRoutedEventArgs e)
|
||||
private void ImageSurface_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);
|
||||
EndDrag();
|
||||
ImageSurface.ReleasePointerCapture(e.Pointer);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void CropHost_PointerCaptureLost(object sender, PointerRoutedEventArgs e)
|
||||
private void ImageSurface_PointerCaptureLost(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
_isDragging = false;
|
||||
if (_isDragging)
|
||||
EndDrag();
|
||||
}
|
||||
|
||||
private void CropHost_SizeChanged(object sender, SizeChangedEventArgs e) => RefreshSelectionVisual();
|
||||
private void EndDrag()
|
||||
{
|
||||
_isDragging = false;
|
||||
_dragHit = CropHit.None;
|
||||
RefreshSelectionVisual();
|
||||
UpdateInsetsText();
|
||||
}
|
||||
|
||||
private void ImageSurface_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (_pixelWidth <= 0)
|
||||
return;
|
||||
|
||||
bool shift = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
|
||||
int step = 2;
|
||||
PixelInsets next = _insets;
|
||||
switch (e.Key)
|
||||
{
|
||||
case VirtualKey.Left:
|
||||
next = shift
|
||||
? SourceCropSelection.NudgeEdge(_insets, _pixelWidth, _pixelHeight, -step, 0, 0, 0)
|
||||
: SourceCropSelection.Nudge(_insets, _pixelWidth, _pixelHeight, -step, 0);
|
||||
break;
|
||||
case VirtualKey.Right:
|
||||
next = shift
|
||||
? SourceCropSelection.NudgeEdge(_insets, _pixelWidth, _pixelHeight, 0, 0, -step, 0)
|
||||
: SourceCropSelection.Nudge(_insets, _pixelWidth, _pixelHeight, step, 0);
|
||||
break;
|
||||
case VirtualKey.Up:
|
||||
next = shift
|
||||
? SourceCropSelection.NudgeEdge(_insets, _pixelWidth, _pixelHeight, 0, -step, 0, 0)
|
||||
: SourceCropSelection.Nudge(_insets, _pixelWidth, _pixelHeight, 0, -step);
|
||||
break;
|
||||
case VirtualKey.Down:
|
||||
next = shift
|
||||
? SourceCropSelection.NudgeEdge(_insets, _pixelWidth, _pixelHeight, 0, 0, 0, -step)
|
||||
: SourceCropSelection.Nudge(_insets, _pixelWidth, _pixelHeight, 0, step);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (_insets.IsEmpty)
|
||||
return;
|
||||
|
||||
SetInsets(next);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void CropScroller_SizeChanged(object sender, SizeChangedEventArgs e) => ApplyViewTransform();
|
||||
|
||||
private void CropScroller_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (!e.KeyModifiers.HasFlag(VirtualKeyModifiers.Control))
|
||||
return;
|
||||
|
||||
double delta = e.GetCurrentPoint(CropScroller).Properties.MouseWheelDelta > 0 ? ZoomStep : -ZoomStep;
|
||||
SetZoom(_zoom + delta);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void ZoomSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
|
||||
{
|
||||
if (!IsLoaded)
|
||||
return;
|
||||
SetZoom(e.NewValue);
|
||||
}
|
||||
|
||||
private void BrightnessSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
|
||||
{
|
||||
if (!IsLoaded)
|
||||
return;
|
||||
ApplyBrightness(e.NewValue);
|
||||
}
|
||||
|
||||
private void ClearCrop_Click(object sender, RoutedEventArgs e) => SetInsets(default);
|
||||
|
||||
private void SetZoom(double zoom)
|
||||
{
|
||||
_zoom = Math.Clamp(Math.Round(zoom / ZoomStep) * ZoomStep, MinZoom, MaxZoom);
|
||||
if (Math.Abs(ZoomSlider.Value - _zoom) > 0.001)
|
||||
ZoomSlider.Value = _zoom;
|
||||
ZoomSlider.Header = $"Zoom {_zoom * 100:0}%";
|
||||
ApplyViewTransform();
|
||||
}
|
||||
|
||||
private void ApplyBrightness(double value)
|
||||
{
|
||||
double amount = Math.Clamp(value, -0.7, 0.7);
|
||||
BrightnessOverlay.Fill = new SolidColorBrush(amount >= 0 ? Colors.White : Colors.Black);
|
||||
BrightnessOverlay.Opacity = Math.Abs(amount);
|
||||
BrightnessSlider.Header = amount == 0
|
||||
? "View brightness"
|
||||
: $"View brightness {(amount > 0 ? "+" : "")}{amount:0.00}";
|
||||
}
|
||||
|
||||
private void ApplyViewTransform()
|
||||
{
|
||||
if (_pixelWidth <= 0)
|
||||
return;
|
||||
|
||||
double viewportWidth = CropScroller.ActualWidth > 0 ? CropScroller.ActualWidth : 800;
|
||||
double viewportHeight = CropScroller.ActualHeight > 0 ? CropScroller.ActualHeight : 420;
|
||||
(_, _, double displayWidth, double displayHeight) =
|
||||
SourceCropSelection.FitUniform(viewportWidth, viewportHeight, _pixelWidth, _pixelHeight);
|
||||
ImageSurface.Width = Math.Max(1, displayWidth * _zoom);
|
||||
ImageSurface.Height = Math.Max(1, displayHeight * _zoom);
|
||||
RefreshSelectionVisual();
|
||||
}
|
||||
|
||||
private bool TryMapToDisplay(
|
||||
PointerRoutedEventArgs e, out double x, out double y, out double displayWidth, out double displayHeight)
|
||||
{
|
||||
(double offsetX, double offsetY, displayWidth, displayHeight) = CurrentFit();
|
||||
displayWidth = ImageSurface.Width;
|
||||
displayHeight = ImageSurface.Height;
|
||||
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);
|
||||
PointerPoint point = e.GetCurrentPoint(ImageSurface);
|
||||
(x, y) = SourceCropSelection.ClampToDisplay(point.Position.X, point.Position.Y, displayWidth, displayHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateDraftSelection(double x, double y)
|
||||
{
|
||||
(double offsetX, double offsetY, double displayWidth, double displayHeight) = CurrentFit();
|
||||
double displayWidth = ImageSurface.Width;
|
||||
double displayHeight = ImageSurface.Height;
|
||||
if (displayWidth <= 0)
|
||||
return;
|
||||
|
||||
double left = Math.Min(_dragStart.X, x);
|
||||
double top = Math.Min(_dragStart.Y, y);
|
||||
double width = Math.Abs(x - _dragStart.X);
|
||||
double height = Math.Abs(y - _dragStart.Y);
|
||||
ShowDisplayRect(left, top, width, height);
|
||||
_insets = SourceCropSelection.ToInsets(
|
||||
_pixelWidth, _pixelHeight, displayWidth, displayHeight,
|
||||
_dragStart.X, _dragStart.Y, x - _dragStart.X, y - _dragStart.Y);
|
||||
PlaceSelectionRect(offsetX, offsetY, displayWidth, displayHeight);
|
||||
_pixelWidth, _pixelHeight, displayWidth, displayHeight, left, top, width, height);
|
||||
UpdateInsetsText();
|
||||
}
|
||||
|
||||
private void CommitSelection(double x, double y, double displayWidth, double displayHeight)
|
||||
private void UpdateAdjustedSelection(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();
|
||||
(double nextX, double nextY, double nextW, double nextH) = SourceCropSelection.AdjustRect(
|
||||
_dragOriginRect.X,
|
||||
_dragOriginRect.Y,
|
||||
_dragOriginRect.Width,
|
||||
_dragOriginRect.Height,
|
||||
_dragHit,
|
||||
x - _dragStart.X,
|
||||
y - _dragStart.Y,
|
||||
displayWidth,
|
||||
displayHeight);
|
||||
ShowDisplayRect(nextX, nextY, nextW, nextH);
|
||||
_insets = SourceCropSelection.ToInsets(_pixelWidth, _pixelHeight, displayWidth, displayHeight, nextX, nextY, nextW, nextH);
|
||||
UpdateInsetsText();
|
||||
}
|
||||
|
||||
private void RefreshSelectionVisual()
|
||||
{
|
||||
(double offsetX, double offsetY, double displayWidth, double displayHeight) = CurrentFit();
|
||||
if (displayWidth <= 0 || _pixelWidth <= 0)
|
||||
if (ImageSurface.Width <= 0 || _pixelWidth <= 0 || _insets.IsEmpty)
|
||||
{
|
||||
SelectionRect.Visibility = Visibility.Collapsed;
|
||||
SetHandlesVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
PlaceSelectionRect(offsetX, offsetY, displayWidth, displayHeight);
|
||||
(double x, double y, double width, double height) = CurrentDisplayRect(ImageSurface.Width, ImageSurface.Height);
|
||||
ShowDisplayRect(x, y, width, height);
|
||||
}
|
||||
|
||||
private void PlaceSelectionRect(double offsetX, double offsetY, double displayWidth, double displayHeight)
|
||||
private void ShowDisplayRect(double x, double y, double width, double height)
|
||||
{
|
||||
(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;
|
||||
Canvas.SetLeft(SelectionRect, x);
|
||||
Canvas.SetTop(SelectionRect, y);
|
||||
SelectionRect.Width = Math.Max(1, width);
|
||||
SelectionRect.Height = Math.Max(1, height);
|
||||
SelectionRect.Visibility = Visibility.Visible;
|
||||
PlaceHandle(HandleNW, x, y);
|
||||
PlaceHandle(HandleN, x + (width / 2), y);
|
||||
PlaceHandle(HandleNE, x + width, y);
|
||||
PlaceHandle(HandleE, x + width, y + (height / 2));
|
||||
PlaceHandle(HandleSE, x + width, y + height);
|
||||
PlaceHandle(HandleS, x + (width / 2), y + height);
|
||||
PlaceHandle(HandleSW, x, y + height);
|
||||
PlaceHandle(HandleW, x, y + (height / 2));
|
||||
SetHandlesVisible(true);
|
||||
}
|
||||
|
||||
private (double OffsetX, double OffsetY, double DisplayWidth, double DisplayHeight) CurrentFit() =>
|
||||
SourceCropSelection.FitUniform(CropHost.ActualWidth, CropHost.ActualHeight, _pixelWidth, _pixelHeight);
|
||||
private (double X, double Y, double Width, double Height) CurrentDisplayRect(double displayWidth, double displayHeight) =>
|
||||
SourceCropSelection.ToDisplayRect(_pixelWidth, _pixelHeight, displayWidth, displayHeight, _insets);
|
||||
|
||||
private void PlaceHandle(Microsoft.UI.Xaml.Shapes.Rectangle handle, double centerX, double centerY)
|
||||
{
|
||||
Canvas.SetLeft(handle, centerX - (HandleVisualSize / 2));
|
||||
Canvas.SetTop(handle, centerY - (HandleVisualSize / 2));
|
||||
}
|
||||
|
||||
private void SetHandlesVisible(bool visible)
|
||||
{
|
||||
Visibility visibility = visible ? Visibility.Visible : Visibility.Collapsed;
|
||||
SelectionRect.Visibility = visibility;
|
||||
HandleNW.Visibility = visibility;
|
||||
HandleN.Visibility = visibility;
|
||||
HandleNE.Visibility = visibility;
|
||||
HandleE.Visibility = visibility;
|
||||
HandleSE.Visibility = visibility;
|
||||
HandleS.Visibility = visibility;
|
||||
HandleSW.Visibility = visibility;
|
||||
HandleW.Visibility = visibility;
|
||||
}
|
||||
|
||||
private void UpdateHoverCursor(PointerRoutedEventArgs e)
|
||||
{
|
||||
if (_pixelWidth <= 0 || _insets.IsEmpty || !TryMapToDisplay(e, out double x, out double y, out double displayWidth, out double displayHeight))
|
||||
{
|
||||
ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.Cross);
|
||||
return;
|
||||
}
|
||||
|
||||
(double rectX, double rectY, double rectW, double rectH) = CurrentDisplayRect(displayWidth, displayHeight);
|
||||
CropHit hit = SourceCropSelection.HitTest(rectX, rectY, rectW, rectH, x, y, HandleHitSize);
|
||||
ProtectedCursor = InputSystemCursor.Create(CursorFor(hit));
|
||||
}
|
||||
|
||||
private static InputSystemCursorShape CursorFor(CropHit hit) => hit switch
|
||||
{
|
||||
CropHit.Move => InputSystemCursorShape.SizeAll,
|
||||
CropHit.North or CropHit.South => InputSystemCursorShape.SizeNorthSouth,
|
||||
CropHit.East or CropHit.West => InputSystemCursorShape.SizeWestEast,
|
||||
CropHit.NorthWest or CropHit.SouthEast => InputSystemCursorShape.SizeNorthwestSoutheast,
|
||||
CropHit.NorthEast or CropHit.SouthWest => InputSystemCursorShape.SizeNortheastSouthwest,
|
||||
_ => InputSystemCursorShape.Cross,
|
||||
};
|
||||
|
||||
private void UpdateInsetsText() =>
|
||||
InsetsText.Text = _pixelWidth <= 0
|
||||
|
||||
Reference in New Issue
Block a user