Transitions in edit mode and on output: program transitions run while recording or streaming; the preview animates the outgoing picture (fade/move fade, wipe shrinks) in every mode
Test: PreviewTransitionTests (with the transition, wipe, and brand logo tests).
This commit is contained in:
@@ -71,6 +71,8 @@
|
||||
Visibility="{Binding BackgroundVisible, Converter={StaticResource BoolToVis}}"/>
|
||||
<Image Source="{Binding ActiveBackgroundImage}" Stretch="UniformToFill"/>
|
||||
</Grid>
|
||||
<Image x:Name="TransitionOverlay" Width="1920" Height="1080" Stretch="Fill"
|
||||
IsHitTestVisible="False" Visibility="Collapsed"/>
|
||||
<Canvas x:Name="OverlayCanvas" Width="1920" Height="1080">
|
||||
<ItemsControl ItemsSource="{Binding StagedScene.Elements}"
|
||||
Width="1920" Height="1080">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>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.</summary>
|
||||
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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Raised before a scene switch so the preview can animate the old picture away.</summary>
|
||||
public event Action<TransitionType, double>? PreviewTransition;
|
||||
|
||||
/// <summary>
|
||||
/// Apply the current transition type to the next StagedScene change while live.
|
||||
/// Called from XAML when user picks a transition from the dropdown.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using ytLive.Models;
|
||||
using ytLive.Services;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user