diff --git a/Controls/PreviewPane.xaml b/Controls/PreviewPane.xaml index f05e892..bd15ede 100644 --- a/Controls/PreviewPane.xaml +++ b/Controls/PreviewPane.xaml @@ -71,6 +71,8 @@ Visibility="{Binding BackgroundVisible, Converter={StaticResource BoolToVis}}"/> + diff --git a/Controls/PreviewPane.xaml.cs b/Controls/PreviewPane.xaml.cs index 211c595..27c6044 100644 --- a/Controls/PreviewPane.xaml.cs +++ b/Controls/PreviewPane.xaml.cs @@ -36,14 +36,60 @@ public partial class PreviewPane : UserControl private void PreviewPane_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { if (e.OldValue is MainViewModel oldVm) + { oldVm.PropertyChanged -= ViewModel_PropertyChanged; + oldVm.PreviewTransition -= PlayPreviewTransition; + } if (e.NewValue is MainViewModel newVm) { newVm.PropertyChanged += ViewModel_PropertyChanged; + newVm.PreviewTransition += PlayPreviewTransition; UpdateZoomBox(); } } + /// Animates the outgoing picture away over the new scene: a fade for Fade and Move, + /// a right-to-left shrink for Wipe. Cut plays nothing. + private void PlayPreviewTransition(ytLive.Services.TransitionType type, double milliseconds) + { + TransitionOverlay.BeginAnimation(System.Windows.UIElement.OpacityProperty, null); + TransitionOverlay.Clip = null; + if (type == ytLive.Services.TransitionType.Cut || milliseconds <= 0) + { + TransitionOverlay.Visibility = Visibility.Collapsed; + return; + } + + var snapshot = new System.Windows.Media.Imaging.RenderTargetBitmap(1920, 1080, 96, 96, + System.Windows.Media.PixelFormats.Pbgra32); + snapshot.Render(PreviewCanvas); + TransitionOverlay.Source = snapshot; + TransitionOverlay.Opacity = 1; + TransitionOverlay.Visibility = Visibility.Visible; + var duration = TimeSpan.FromMilliseconds(milliseconds); + + if (type == ytLive.Services.TransitionType.Wipe) + { + var clip = new System.Windows.Media.RectangleGeometry(new Rect(0, 0, 1920, 1080)); + TransitionOverlay.Clip = clip; + var reveal = new System.Windows.Media.Animation.RectAnimation(new Rect(0, 0, 0, 1080), duration) + { + FillBehavior = System.Windows.Media.Animation.FillBehavior.Stop, + }; + reveal.Completed += (_, _) => TransitionOverlay.Visibility = Visibility.Collapsed; + clip.BeginAnimation(System.Windows.Media.RectangleGeometry.RectProperty, reveal); + } + else + { + var fade = new System.Windows.Media.Animation.DoubleAnimation(1, 0, duration) + { + FillBehavior = System.Windows.Media.Animation.FillBehavior.Stop, + }; + fade.Completed += (_, _) => TransitionOverlay.Visibility = Visibility.Collapsed; + TransitionOverlay.BeginAnimation(System.Windows.UIElement.OpacityProperty, fade); + } + } + private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == nameof(MainViewModel.IsZoomActive) || e.PropertyName == nameof(MainViewModel.ZoomState) || e.PropertyName == nameof(MainViewModel.ZoomShape)) diff --git a/ViewModels/MainViewModel.Scenes.cs b/ViewModels/MainViewModel.Scenes.cs index 5879b5e..d95d8c2 100644 --- a/ViewModels/MainViewModel.Scenes.cs +++ b/ViewModels/MainViewModel.Scenes.cs @@ -115,14 +115,20 @@ public partial class MainViewModel public void TransitionToScene(Scene scene) { if (scene.IsHidden || scene == StagedScene) return; + // The preview animates the outgoing picture before the switch, in every mode. + PreviewTransition?.Invoke(SelectedTransitionType, TransitionDurationMs); StagedScene = scene; - if (IsLive) + // The program output transitions whenever it is running: recording or streaming. + if (IsLive || IsRecording) { LiveScene = scene; _transition.Start(SelectedTransitionType, TransitionDurationMs, null); } } + /// Raised before a scene switch so the preview can animate the old picture away. + public event Action? PreviewTransition; + /// /// Apply the current transition type to the next StagedScene change while live. /// Called from XAML when user picks a transition from the dropdown. diff --git a/ytLive.Tests/PreviewTransitionTests.cs b/ytLive.Tests/PreviewTransitionTests.cs new file mode 100644 index 0000000..ebf775c --- /dev/null +++ b/ytLive.Tests/PreviewTransitionTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using Xunit; +using ytLive.Models; +using ytLive.Services; +using ytLive.ViewModels; + +namespace ytLive.Tests; + +/// +/// A scene switch in edit mode raises the preview transition with the chosen type and +/// length, before the staged scene changes, so the preview can animate the old picture. +/// +[Collection("RealApp")] +public sealed class PreviewTransitionTests +{ + private readonly RealAppHost _app; + + public PreviewTransitionTests(RealAppHost app) => _app = app; + + [Fact] + public void Scene_Switch_In_Edit_Mode_Raises_The_Preview_Transition_Before_The_Switch() + { + _app.Run(Run); + } + + private static void Run() + { + var tempDb = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"ytLlive-preview-{Guid.NewGuid():N}.db"); + MainViewModel.LayoutPathOverride = tempDb; + try + { + using (var schema = new LayoutStore(tempDb)) { } + Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); + + var vm = new MainViewModel(); + var from = vm.StagedScene!; + var to = vm.Scenes.First(s => s != from && !s.IsHidden); + vm.SelectedTransitionType = TransitionType.Fade; + vm.TransitionDurationMs = 800; + + Scene? stagedWhenRaised = null; + TransitionType? typeRaised = null; + double? msRaised = null; + vm.PreviewTransition += (type, ms) => + { + stagedWhenRaised = vm.StagedScene; + typeRaised = type; + msRaised = ms; + }; + + vm.TransitionToScene(to); + + Assert.Same(from, stagedWhenRaised); + Assert.Equal(TransitionType.Fade, typeRaised); + Assert.Equal(800, msRaised); + Assert.Same(to, vm.StagedScene); + } + finally + { + MainViewModel.LayoutPathOverride = null; + Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); + try { System.IO.File.Delete(tempDb); } catch { } + } + } +}