using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Effects; using System.Windows.Media.Imaging; using System.Windows.Shapes; using ytLive.Models; namespace ytLive.Services.Compositor; /// /// Rasterizes the global social bar into a transparent BGRA8 strip the output /// compositor overlays onto the master frame (top or bottom edge). Replicates the /// preview's bar DataTemplate in code — one Path (logo) + one TextBlock (handle) /// per entry, white on transparent, green glow — then software-renders it via /// RenderTargetBitmap and converts Pbgra32 (premultiplied) to straight-alpha /// BGRA for the compositor. WPF glue like : the /// compositor core itself stays pure byte-math. Renders on the UI thread only; /// the returned frame is immutable afterwards, so the frame pump may read it from /// any thread. /// public static class SocialBarRenderer { /// Icon size + the preview's 8px top/bottom margins. private const int ContentHeight = 40; /// Room for the green DropShadowEffect blur to bleed past the content. private const int GlowPad = 24; /// Master-frame width — the bar spans the full 1920px output. public const int Width = 1920; /// Returns the strip frame, or null when there is nothing to render. public static VideoFrame? Render(IEnumerable? entries, SocialBarStyle? style = null) { var look = style ?? SocialBarStyle.Default; var textBrush = look.TextBrush; var glyphEffect = look.TextShadow ? ShadowEffect() : null; var list = entries?.Where(e => e != null).ToList(); if (list == null || list.Count == 0) return null; var height = ContentHeight + GlowPad * 2; var row = new StackPanel { Orientation = Orientation.Horizontal }; foreach (var entry in list) { var item = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 20, 0), }; item.Children.Add(new Path { Data = Geometry.Parse(entry.LogoData), Fill = textBrush, Effect = glyphEffect, Width = 24, Height = 24, Stretch = Stretch.Uniform, VerticalAlignment = VerticalAlignment.Center, }); item.Children.Add(new TextBlock { Text = entry.Handle, Foreground = textBrush, Effect = glyphEffect, FontSize = 14, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(8, 0, 0, 0), }); row.Children.Add(item); } // The glow is baked in so the output bar matches the preview's green halo. var container = new Grid { Width = Width, Height = height, Background = Brushes.Transparent, Effect = new DropShadowEffect { Color = Color.FromRgb(0x2e, 0xcc, 0x71), BlurRadius = 18, ShadowDepth = 0, Opacity = 0.9 }, }; var centered = new Grid { HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, GlowPad, 0, 0) }; if (look.BackgroundEnabled) { // The plate hugs the entries, so it reads as a caption bar rather than a band across // the frame; 20px radius matches the pill language used elsewhere. centered.Children.Add(new Border { Background = look.BackgroundBrush, CornerRadius = new CornerRadius(20), Padding = new Thickness(18, 6, 18, 6), Child = row, }); } else { centered.Children.Add(row); } container.Children.Add(centered); container.Measure(new Size(Width, height)); container.Arrange(new Rect(0, 0, Width, height)); var bitmap = new RenderTargetBitmap(Width, height, 96, 96, PixelFormats.Pbgra32); bitmap.Render(container); var pixels = new byte[Width * height * 4]; bitmap.CopyPixels(pixels, Width * 4, 0); // Pbgra32 is premultiplied — unpremultiply so the compositor's straight-alpha // source-over blend doesn't darken the logo edges with a halo. for (var i = 0; i < pixels.Length; i += 4) { var a = pixels[i + 3]; if (a == 0 || a == 255) continue; pixels[i] = (byte)(pixels[i] * 255 / a); pixels[i + 1] = (byte)(pixels[i + 1] * 255 / a); pixels[i + 2] = (byte)(pixels[i + 2] * 255 / a); } return new VideoFrame(Width, height, pixels); } private static DropShadowEffect ShadowEffect() => new() { Color = Colors.Black, BlurRadius = 4, ShadowDepth = 2, Direction = 315, Opacity = 0.85, }; } /// How the social bar is painted (creator request 2026-10-30: text colour, an optional /// plate with its own colour and opacity, and a text drop shadow). /// /// It is a separate value type, not , so the renderer stays a /// pure function of "entries + look" and the preview/output can never read a half-applied /// config mid-edit. public readonly record struct SocialBarStyle( string TextColor, bool BackgroundEnabled, string BackgroundColor, double BackgroundOpacity, bool TextShadow) { public static SocialBarStyle Default => new("#FFFFFF", false, "#000000", 0.55, false); public static SocialBarStyle From(SocialsConfig? config) => config is null ? Default : new(config.BarTextColor, config.BarBackgroundEnabled, config.BarBackgroundColor, config.BarBackgroundOpacity, config.BarTextShadow); /// Glyph colour. A malformed hex falls back to white rather than throwing inside a /// render tick — a bad colour must never kill the output. public SolidColorBrush TextBrush => Solid(TextColor, 1.0, Colors.White); /// Plate colour carrying the configured opacity in its alpha. public SolidColorBrush BackgroundBrush => Solid(BackgroundColor, Math.Clamp(BackgroundOpacity, 0, 1), Colors.Black); private static SolidColorBrush Solid(string hex, double opacity, Color fallback) { Color colour; try { colour = (Color)ColorConverter.ConvertFromString(hex); } catch (Exception) { colour = fallback; } var brush = new SolidColorBrush(Color.FromArgb((byte)Math.Round(opacity * 255), colour.R, colour.G, colour.B)); brush.Freeze(); return brush; } }