|
@@ -1,5 +1,6 @@
|
|
using System.Threading.Tasks;
|
|
using System.Threading.Tasks;
|
|
using Avalonia;
|
|
using Avalonia;
|
|
|
|
+using Avalonia.Controls;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.Threading;
|
|
using Avalonia.Threading;
|
|
using ChunkyImageLib;
|
|
using ChunkyImageLib;
|
|
@@ -8,9 +9,9 @@ using PixiEditor.AvaloniaUI.Helpers;
|
|
using PixiEditor.AvaloniaUI.Models.Files;
|
|
using PixiEditor.AvaloniaUI.Models.Files;
|
|
using PixiEditor.AvaloniaUI.Models.IO;
|
|
using PixiEditor.AvaloniaUI.Models.IO;
|
|
using PixiEditor.AvaloniaUI.ViewModels.Document;
|
|
using PixiEditor.AvaloniaUI.ViewModels.Document;
|
|
-using PixiEditor.DrawingApi.Core.Surface.ImageData;
|
|
|
|
using PixiEditor.Extensions.Common.Localization;
|
|
using PixiEditor.Extensions.Common.Localization;
|
|
using PixiEditor.Numerics;
|
|
using PixiEditor.Numerics;
|
|
|
|
+using Image = PixiEditor.DrawingApi.Core.Surface.ImageData.Image;
|
|
|
|
|
|
namespace PixiEditor.AvaloniaUI.Views.Dialogs;
|
|
namespace PixiEditor.AvaloniaUI.Views.Dialogs;
|
|
|
|
|
|
@@ -109,6 +110,7 @@ internal partial class ExportFilePopup : PixiEditorPopup
|
|
private Image[] videoPreviewFrames = [];
|
|
private Image[] videoPreviewFrames = [];
|
|
private DispatcherTimer videoPreviewTimer = new DispatcherTimer();
|
|
private DispatcherTimer videoPreviewTimer = new DispatcherTimer();
|
|
private int activeFrame = 0;
|
|
private int activeFrame = 0;
|
|
|
|
+ private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
|
|
|
|
|
|
static ExportFilePopup()
|
|
static ExportFilePopup()
|
|
{
|
|
{
|
|
@@ -132,10 +134,51 @@ internal partial class ExportFilePopup : PixiEditorPopup
|
|
SetBestPercentageCommand = new RelayCommand(SetBestPercentage);
|
|
SetBestPercentageCommand = new RelayCommand(SetBestPercentage);
|
|
ExportCommand = new AsyncRelayCommand(Export);
|
|
ExportCommand = new AsyncRelayCommand(Export);
|
|
this.document = document;
|
|
this.document = document;
|
|
|
|
+ videoPreviewTimer = new DispatcherTimer(DispatcherPriority.Normal)
|
|
|
|
+ {
|
|
|
|
+ Interval = TimeSpan.FromMilliseconds(1000f / document.AnimationDataViewModel.FrameRate)
|
|
|
|
+ };
|
|
|
|
+ videoPreviewTimer.Tick += OnVideoPreviewTimerOnTick;
|
|
|
|
|
|
RenderPreview();
|
|
RenderPreview();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ protected override void OnClosing(WindowClosingEventArgs e)
|
|
|
|
+ {
|
|
|
|
+ base.OnClosing(e);
|
|
|
|
+ videoPreviewTimer.Stop();
|
|
|
|
+ videoPreviewTimer.Tick -= OnVideoPreviewTimerOnTick;
|
|
|
|
+ videoPreviewTimer = null;
|
|
|
|
+ cancellationTokenSource.Dispose();
|
|
|
|
+
|
|
|
|
+ if (ExportPreview != null)
|
|
|
|
+ {
|
|
|
|
+ ExportPreview.Dispose();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (videoPreviewFrames != null)
|
|
|
|
+ {
|
|
|
|
+ foreach (var frame in videoPreviewFrames)
|
|
|
|
+ {
|
|
|
|
+ frame.Dispose();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void OnVideoPreviewTimerOnTick(object? o, EventArgs eventArgs)
|
|
|
|
+ {
|
|
|
|
+ if (videoPreviewFrames.Length > 0)
|
|
|
|
+ {
|
|
|
|
+ ExportPreview.DrawingSurface.Canvas.Clear();
|
|
|
|
+ ExportPreview.DrawingSurface.Canvas.DrawImage(videoPreviewFrames[activeFrame], 0, 0);
|
|
|
|
+ activeFrame = (activeFrame + 1) % videoPreviewFrames.Length;
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ videoPreviewTimer.Stop();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
private void RenderPreview()
|
|
private void RenderPreview()
|
|
{
|
|
{
|
|
if (document == null)
|
|
if (document == null)
|
|
@@ -146,41 +189,88 @@ internal partial class ExportFilePopup : PixiEditorPopup
|
|
videoPreviewTimer.Stop();
|
|
videoPreviewTimer.Stop();
|
|
if (IsVideoExport)
|
|
if (IsVideoExport)
|
|
{
|
|
{
|
|
- videoPreviewFrames = document.RenderFrames(surface =>
|
|
|
|
|
|
+ StartRenderAnimationJob();
|
|
|
|
+ videoPreviewTimer.Interval = TimeSpan.FromMilliseconds(1000f / document.AnimationDataViewModel.FrameRate);
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ var rendered = document.TryRenderWholeImage();
|
|
|
|
+ if (rendered.IsT1)
|
|
{
|
|
{
|
|
- if (SaveWidth != surface.Size.X || SaveHeight != surface.Size.Y)
|
|
|
|
- {
|
|
|
|
- return surface.ResizeNearestNeighbor(new VecI(SaveWidth, SaveHeight));
|
|
|
|
- }
|
|
|
|
|
|
+ VecI previewSize = CalculatePreviewSize(rendered.AsT1.Size);
|
|
|
|
+ ExportPreview = rendered.AsT1.ResizeNearestNeighbor(previewSize);
|
|
|
|
+ rendered.AsT1.Dispose();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
|
|
- return surface;
|
|
|
|
- });
|
|
|
|
- videoPreviewTimer = new DispatcherTimer(DispatcherPriority.Normal)
|
|
|
|
- {
|
|
|
|
- Interval = TimeSpan.FromMilliseconds(1000f / document.AnimationDataViewModel.FrameRate)
|
|
|
|
- };
|
|
|
|
- videoPreviewTimer.Tick += (_, _) =>
|
|
|
|
|
|
+ private void StartRenderAnimationJob()
|
|
|
|
+ {
|
|
|
|
+ if (cancellationTokenSource.Token != null && cancellationTokenSource.Token.CanBeCanceled)
|
|
|
|
+ {
|
|
|
|
+ cancellationTokenSource.Cancel();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ cancellationTokenSource = new CancellationTokenSource();
|
|
|
|
+
|
|
|
|
+ Task.Run(
|
|
|
|
+ () =>
|
|
{
|
|
{
|
|
- if (videoPreviewFrames.Length > 0)
|
|
|
|
|
|
+ videoPreviewFrames = document.RenderFrames(surface =>
|
|
{
|
|
{
|
|
- ExportPreview.DrawingSurface.Canvas.Clear();
|
|
|
|
- ExportPreview.DrawingSurface.Canvas.DrawImage(videoPreviewFrames[activeFrame], 0, 0);
|
|
|
|
- activeFrame = (activeFrame + 1) % videoPreviewFrames.Length;
|
|
|
|
- }
|
|
|
|
- else
|
|
|
|
|
|
+ return Dispatcher.UIThread.Invoke(() =>
|
|
|
|
+ {
|
|
|
|
+ Surface original = surface;
|
|
|
|
+ if (SaveWidth != surface.Size.X || SaveHeight != surface.Size.Y)
|
|
|
|
+ {
|
|
|
|
+ original = surface.ResizeNearestNeighbor(new VecI(SaveWidth, SaveHeight));
|
|
|
|
+ surface.Dispose();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ VecI previewSize = CalculatePreviewSize(original.Size);
|
|
|
|
+ if (previewSize != original.Size)
|
|
|
|
+ {
|
|
|
|
+ var resized = original.ResizeNearestNeighbor(previewSize);
|
|
|
|
+ original.Dispose();
|
|
|
|
+ return resized;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return original;
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+ }, cancellationTokenSource.Token).ContinueWith(_ =>
|
|
|
|
+ {
|
|
|
|
+ Dispatcher.UIThread.Invoke(() =>
|
|
|
|
+ {
|
|
|
|
+ VecI previewSize = CalculatePreviewSize(new VecI(SaveWidth, SaveHeight));
|
|
|
|
+ if (previewSize != ExportPreview.Size)
|
|
{
|
|
{
|
|
- videoPreviewTimer.Stop();
|
|
|
|
|
|
+ ExportPreview?.Dispose();
|
|
|
|
+ ExportPreview = new Surface(previewSize);
|
|
}
|
|
}
|
|
- };
|
|
|
|
-
|
|
|
|
|
|
+ });
|
|
|
|
+
|
|
videoPreviewTimer.Start();
|
|
videoPreviewTimer.Start();
|
|
- }
|
|
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
|
|
- var rendered = document.TryRenderWholeImage();
|
|
|
|
- if (rendered.IsT1)
|
|
|
|
|
|
+ private VecI CalculatePreviewSize(VecI imageSize)
|
|
|
|
+ {
|
|
|
|
+ VecI maxPreviewSize = new VecI(150, 200);
|
|
|
|
+ if (imageSize.X > maxPreviewSize.X || imageSize.Y > maxPreviewSize.Y)
|
|
{
|
|
{
|
|
- ExportPreview = rendered.AsT1.ResizeNearestNeighbor(new VecI(SaveWidth, SaveHeight));
|
|
|
|
|
|
+ float scaleX = maxPreviewSize.X / (float)imageSize.X;
|
|
|
|
+ float scaleY = maxPreviewSize.Y / (float)imageSize.Y;
|
|
|
|
+
|
|
|
|
+ float scale = Math.Min(scaleX, scaleY);
|
|
|
|
+
|
|
|
|
+ int newWidth = (int)(imageSize.X * scale);
|
|
|
|
+ int newHeight = (int)(imageSize.Y * scale);
|
|
|
|
+
|
|
|
|
+ return new VecI(newWidth, newHeight);
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ return imageSize;
|
|
}
|
|
}
|
|
|
|
|
|
private async Task Export()
|
|
private async Task Export()
|