Files
LlamaCasty/Services/TransitionService.cs
T
gramps 4b9a8e515f Control Surface UX: pill toggle, background rename, per-scene properties
- Sliding pill toggle (green ON / red OFF) replaces checkbox
- Geometry mismatch error for custom backgrounds
- Backdrop -> Background: C# rename, SQL column rename with migration
- Source.IsBackground: new -> override (fixes WPF binding)
- Legacy 'Backdrop' display name migrated to 'Background'
- Per-scene UseDefaultBackground + CustomBackgroundPath
- Asset files renamed backdrop -> background
- StagedScene/LiveScene split, thumbnail strip, edit gating
- TransitionService (Cut/Fade/Move)
- Context menu: Capture Desktop, Refresh Desktop
- Resolution badge removed, ON-AIR status light added
2026-08-22 18:10:59 -07:00

155 lines
5.2 KiB
C#

using ytLive.Models;
namespace ytLive.Services;
/// <summary>
/// The three v1 transition types. Clicking a scene thumbnail triggers the selected
/// transition; Go Live always uses Cut for a clean start.
/// </summary>
public enum TransitionType
{
Cut,
Fade,
Move
}
/// <summary>
/// Manages the transition state between StagedScene and LiveScene. When a
/// transition is active, FramePump calls <see cref="BlendFrame"/> each tick
/// to get the blended output. When the transition completes, <see cref="Active"/>
/// becomes false and FramePump reverts to a direct LiveScene render.
/// </summary>
public sealed class TransitionService
{
private const double DefaultDurationMs = 300;
/// <summary>True while a transition is in progress.</summary>
public bool Active { get; private set; }
/// <summary>The transition type for the current in-progress transition.</summary>
public TransitionType CurrentType { get; private set; } = TransitionType.Cut;
/// <summary>Duration of the current transition in milliseconds.</summary>
public double DurationMs { get; private set; } = DefaultDurationMs;
/// <summary>Elapsed time in milliseconds (updated by caller).</summary>
public double ElapsedMs { get; private set; }
/// <summary>0.0 = fully "from" scene, 1.0 = fully "to" scene.</summary>
public float Progress => DurationMs <= 0 ? 1f
: Math.Clamp((float)(ElapsedMs / DurationMs), 0f, 1f);
/// <summary>
/// The "from" frame (captured at transition start). Populated by the caller
/// before starting the transition.
/// </summary>
public VideoFrame? FromFrame { get; private set; }
/// <summary>Begin a transition. Caller should capture FromFrame from the current LiveScene render.</summary>
public void Start(TransitionType type, double durationMs, VideoFrame? fromFrame)
{
CurrentType = type;
DurationMs = durationMs > 0 ? durationMs : DefaultDurationMs;
FromFrame = fromFrame;
ElapsedMs = 0;
Active = true;
if (type == TransitionType.Cut)
{
Active = false;
ElapsedMs = DurationMs;
}
}
/// <summary>
/// Advance the transition clock. Returns true while the transition is still active.
/// </summary>
public bool Tick(double elapsedMs)
{
if (!Active) return false;
ElapsedMs += elapsedMs;
if (ElapsedMs >= DurationMs)
{
ElapsedMs = DurationMs;
Active = false;
return false;
}
return true;
}
/// <summary>
/// Blend the from-frame and the to-frame based on the current progress.
/// Caller passes the freshly-rendered "to" scene; we return the blended output.
/// </summary>
public VideoFrame BlendFrame(VideoFrame toFrame)
{
if (FromFrame == null || CurrentType == TransitionType.Cut)
return toFrame;
var t = Progress;
return CurrentType switch
{
TransitionType.Fade => BlendFade(FromFrame, toFrame, t),
TransitionType.Move => BlendMove(FromFrame, toFrame, t),
_ => toFrame
};
}
private static VideoFrame BlendFade(VideoFrame from, VideoFrame to, float t)
{
var w = from.Width;
var h = from.Height;
var src = from.BgraPixels;
var dst = to.BgraPixels;
var outPixels = new byte[src.Length];
for (var i = 0; i < outPixels.Length; i += 4)
{
outPixels[i] = (byte)(src[i] * (1 - t) + dst[i] * t);
outPixels[i + 1] = (byte)(src[i + 1] * (1 - t) + dst[i + 1] * t);
outPixels[i + 2] = (byte)(src[i + 2] * (1 - t) + dst[i + 2] * t);
outPixels[i + 3] = 255;
}
return new VideoFrame(w, h, outPixels);
}
private static VideoFrame BlendMove(VideoFrame from, VideoFrame to, float t)
{
var w = from.Width;
var h = from.Height;
var src = from.BgraPixels;
var dst = to.BgraPixels;
var outPixels = new byte[src.Length];
var offset = (int)(w * (1 - t));
for (var y = 0; y < h; y++)
{
for (var x = 0; x < w; x++)
{
var di = (y * w + x) * 4;
var fromX = x - offset;
var toX = x + offset;
var srcPixel = fromX >= 0 && fromX < w
? (src[(y * w + fromX) * 4], src[(y * w + fromX) * 4 + 1], src[(y * w + fromX) * 4 + 2])
: ((byte)0, (byte)0, (byte)0);
var dstPixel = toX >= 0 && toX < w
? (dst[(y * w + toX) * 4], dst[(y * w + toX) * 4 + 1], dst[(y * w + toX) * 4 + 2])
: ((byte)0, (byte)0, (byte)0);
var edgeFalloff = Math.Clamp((float)(offset) / (w * 0.3f), 0, 1);
var lerpF = Math.Clamp(1f - edgeFalloff, 0, 1);
outPixels[di] = (byte)(srcPixel.Item1 * (1 - lerpF) + dstPixel.Item1 * lerpF);
outPixels[di + 1] = (byte)(srcPixel.Item2 * (1 - lerpF) + dstPixel.Item2 * lerpF);
outPixels[di + 2] = (byte)(srcPixel.Item3 * (1 - lerpF) + dstPixel.Item3 * lerpF);
outPixels[di + 3] = 255;
}
}
return new VideoFrame(w, h, outPixels);
}
}