using System.Globalization; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using ytLive.Models; namespace ytLive.Services; /// /// Renders one event's celebration animation into a BGRA8 frame for the AlertBox. /// Six event kinds, six UNIQUE animations — each a distinct gesture, palette and /// layout (the creator's ruling: these are the monetization shout-outs and free /// branded product placement; a one-template recoloring loses all interest in two /// iterations). Each card carries the bottom-right brand line so every broadcast /// of an event is an in-video ad placement. /// /// walks 0→1 over the whole animation; each kind splits it /// into enter / hold / exit phases with its own easing. /// public sealed class AlertRenderer { private const double EnterEnd = 0.16; private const double ExitStart = 0.80; private const string Brand = "made with LlamaCasty!"; private static readonly CultureInfo Invariant = CultureInfo.InvariantCulture; private static readonly Color White = Colors.White; private static readonly Color[] ConfettiPalette = { Color.FromRgb(0xf4, 0x2f, 0x5f), Color.FromRgb(0xff, 0xd1, 0x66), Color.FromRgb(0x38, 0xb6, 0xf8), Color.FromRgb(0xa7, 0x8b, 0xfa), Color.FromRgb(0x39, 0xd3, 0x53), }; public VideoFrame Render(ChatMessage e, double t01, int width, int height) { return e.Kind switch { ChatEventKind.SuperChat => SuperChat(e, t01, width, height), ChatEventKind.SuperSticker => SuperSticker(e, t01, width, height), ChatEventKind.NewMember => NewMember(e, t01, width, height), ChatEventKind.MemberGift => MemberGift(e, t01, width, height), ChatEventKind.GiftReceived => GiftReceived(e, t01, width, height), ChatEventKind.MemberMilestone => Milestone(e, t01, width, height), _ => SuperChat(e, t01, width, height), }; } private static VideoFrame SuperChat(ChatMessage e, double t, int w, int h) { var dpi = 96.0; var visual = new DrawingVisual(); using (var ctx = visual.RenderOpen()) { var margin = Math.Max(w, h) * 0.03; var cardX = margin; var cardW = w - margin * 2; var cardH = h - margin * 2; var cardCX = cardX + cardW / 2; var enter = Clamp01(t / EnterEnd); var exit = Clamp01((t - ExitStart) / (1 - ExitStart)); var exitEase = EaseInCubic(exit); var cardY = Lerp(h + cardH, margin, EaseOutCubic(enter)); cardY = Lerp(cardY, -cardH, exitEase); DrawCard(ctx, cardX, cardY, cardW, cardH, "#0d3b2b", "#14532d", 14); DrawBrand(ctx, cardX, cardY, cardW, cardH); var shownAmount = e.SuperChatDisplayString ?? (e.SuperChatCurrency is { Length: > 0 } cur ? $"{cur}{e.SuperChatAmount:0.00}" : $"${e.SuperChatAmount:0.00}"); var fontSize = cardH * 0.24; DrawCentered(ctx, shownAmount, cardCX, cardY + cardH * 0.24, fontSize, FontWeights.Bold, Color.FromRgb(0xff, 0xd1, 0x66), cardW); DrawCentered(ctx, e.AuthorName, cardCX, cardY + cardH * 0.56, cardH * 0.13, FontWeights.SemiBold, White, cardW); DrawCentered(ctx, e.Message, cardCX, cardY + cardH * 0.74, cardH * 0.10, FontWeights.Normal, Color.FromRgb(0xdd, 0xff, 0xf0), cardW * 0.92); if (t >= EnterEnd && t <= ExitStart) { var sweep = (t - EnterEnd) / (ExitStart - EnterEnd); var shineX = cardX + (sweep * 2 - 0.5) * cardW; ctx.DrawRectangle( new LinearGradientBrush( Colors.Transparent, Color.FromArgb(0x30, 0xff, 0xff, 0xff), new Point(0, 0), new Point(1, 0)), null, new Rect(shineX - cardW * 0.25, cardY, cardW * 0.5, cardH)); } } return CreateFrame(visual, w, h, dpi); } private static VideoFrame SuperSticker(ChatMessage e, double t, int w, int h) { var dpi = 96.0; var visual = new DrawingVisual(); using (var ctx = visual.RenderOpen()) { var margin = Math.Max(w, h) * 0.04; var cardW = w - margin * 2; var cardH = h - margin * 2; var cardCX = w / 2.0; var enter = Clamp01(t / EnterEnd); var exit = Clamp01((t - ExitStart) / (1 - ExitStart)); var scale = EaseOutBack(enter) * (0.85 + 0.15 * (1 - exit)); if (t > EnterEnd && t <= ExitStart) scale = 1.0 + 0.02 * Math.Sin(t * 26.0); var alpha = 1.0 - exit; ctx.PushOpacity(alpha); var slick = new RotateTransform(-6, w / 2.0, h / 2.0); ctx.PushTransform(slick); ctx.PushTransform(new ScaleTransform(scale, scale, w / 2.0, h / 2.0)); DrawCard(ctx, margin, margin, cardW, cardH, "#4a044e", "#7e22ce", 20); DrawBrand(ctx, margin, margin, cardW, cardH); var desc = string.IsNullOrWhiteSpace(e.SuperStickerDescription) ? "SUPER STICKER" : e.SuperStickerDescription.ToUpperInvariant(); DrawCentered(ctx, desc, cardCX, h / 2.0 - cardH * 0.12, cardH * 0.17, FontWeights.Bold, White, cardW * 0.9); DrawCentered(ctx, e.AuthorName, cardCX, h / 2.0 + cardH * 0.16, cardH * 0.12, FontWeights.SemiBold, Color.FromRgb(0xff, 0xb3, 0xd9), cardW * 0.9); var tier = e.SuperChatDisplayString ?? (e.SuperChatCurrency is { Length: > 0 } cur ? $"{cur}{e.SuperChatAmount:0.00}" : null); if (!string.IsNullOrWhiteSpace(tier)) DrawCentered(ctx, tier, cardCX, h / 2.0 + cardH * 0.32, cardH * 0.10, FontWeights.Bold, Color.FromRgb(0xff, 0xd1, 0x66), cardW * 0.9); ctx.Pop(); ctx.Pop(); ctx.Pop(); } return CreateFrame(visual, w, h, dpi); } private static VideoFrame NewMember(ChatMessage e, double t, int w, int h) { var dpi = 96.0; var visual = new DrawingVisual(); using (var ctx = visual.RenderOpen()) { var margin = Math.Max(w, h) * 0.035; var cardW = w - margin * 2; var cardH = h - margin * 2; var cardCX = w / 2.0; var enter = Clamp01(t / EnterEnd); var exit = Clamp01((t - ExitStart) / (1 - ExitStart)); var cardY = Lerp(-cardH, margin, EaseOutCubic(enter)); cardY = Lerp(cardY, h, EaseInCubic(exit)); DrawCard(ctx, margin, cardY, cardW, cardH, "#2e1065", "#5b21b6", 16); DrawBrand(ctx, margin, cardY, cardW, cardH); if (t <= ExitStart) { var bandP = EaseOutCubic(Clamp01(t / 0.22)); ctx.DrawRectangle( new LinearGradientBrush(Color.FromArgb(0, 0xff, 0xff, 0xff), Colors.Transparent, new Point(0, 0), new Point(0, 1)), null, new Rect(margin, cardY + cardH * 0.30, cardW * bandP, cardH * 0.18)); } DrawCentered(ctx, e.AuthorName, cardCX, cardY + cardH * 0.36, cardH * 0.22, FontWeights.Bold, White, cardW * 0.9); DrawCentered(ctx, "JOINED THE MEMBERSHIP", cardCX, cardY + cardH * 0.62, cardH * 0.11, FontWeights.SemiBold, Color.FromRgb(0xf5, 0xd0, 0xfe), cardW * 0.9); var level = string.IsNullOrWhiteSpace(e.MembershipLevel) ? "MEMBER" : e.MembershipLevel.ToUpperInvariant(); DrawCentered(ctx, level, cardCX, cardY + cardH * 0.80, cardH * 0.10, FontWeights.Bold, Color.FromRgb(0xff, 0xd1, 0x66), cardW * 0.9); } return CreateFrame(visual, w, h, dpi); } private static VideoFrame MemberGift(ChatMessage e, double t, int w, int h) { var dpi = 96.0; var visual = new DrawingVisual(); using (var ctx = visual.RenderOpen()) { var margin = Math.Max(w, h) * 0.035; var cardW = w - margin * 2; var cardH = h - margin * 2; var cardCX = w / 2.0; var enter = Clamp01(t / EnterEnd); var exit = Clamp01((t - ExitStart) / (1 - ExitStart)); var cardX = Lerp(-cardW, margin, EaseOutCubic(enter)); cardX = Lerp(cardX, -cardW, EaseInCubic(exit)); DrawCard(ctx, cardX, margin, cardW, cardH, "#422006", "#a16207", 16); DrawBrand(ctx, cardX, margin, cardW, cardH); var count = Math.Max(1, e.GiftNewMembersCount); var spread = 1.0 - exit; var hero = count > 1 ? $"×{count}" : "MEMBERSHIP"; DrawCentered(ctx, hero, cardCX, margin + cardH * 0.24, cardH * 0.26, FontWeights.Bold, Color.FromRgb(0xff, 0xd1, 0x66), cardW * 0.8); DrawCentered(ctx, $"{e.AuthorName} is gifting", cardCX, margin + cardH * 0.52, cardH * 0.11, FontWeights.SemiBold, White, cardW * 0.9); var chipCount = Math.Min(count, 6); for (var i = 0; i < chipCount; i++) { var slot = (chipCount - 1) * 0.5; var gap = cardH * 0.13 * EaseOutCubic(Clamp01(t / 0.30)) * spread; var chipX = cardCX + (i - slot) * gap; var chipY = margin + cardH * 0.70 + Math.Sin((i + t * 6) * 2.4) * cardH * 0.015; ctx.DrawRoundedRectangle( new SolidColorBrush(Color.FromRgb(0xff, 0xf3, 0xc4)), null, new Rect(chipX - cardH * 0.05, chipY, cardH * 0.10, cardH * 0.10), cardH * 0.03, cardH * 0.03); } } return CreateFrame(visual, w, h, dpi); } private static VideoFrame GiftReceived(ChatMessage e, double t, int w, int h) { var dpi = 96.0; var visual = new DrawingVisual(); using (var ctx = visual.RenderOpen()) { var margin = Math.Max(w, h) * 0.04; var cardW = w - margin * 2; var cardH = h - margin * 2; var cardCX = w / 2.0; var enter = Clamp01(t / (EnterEnd * 1.4)); var exit = Clamp01((t - ExitStart) / (1 - ExitStart)); var scale = 0.5 + 0.5 * EaseOutBack(enter); var alpha = Math.Min(1.0, enter * 1.6) * (1 - exit); ctx.PushOpacity(alpha); ctx.PushTransform(new ScaleTransform(scale, scale, w / 2.0, h / 2.0)); DrawCard(ctx, margin, margin, cardW, cardH, "#14532d", "#4c1d95", 18); DrawBrand(ctx, margin, margin, cardW, cardH); // Confetti falling inside the card during the hold (deterministic seed). if (t >= EnterEnd && t <= ExitStart) { var hold = (t - EnterEnd) / (ExitStart - EnterEnd); for (var i = 0; i < 14; i++) { var seed = i * 37 + 11; var dotX = margin + cardW * (((seed * 53) % 97) / 97.0); var dotY = margin + cardH * (((seed + hold * 0.85) % 1.0)); var size = cardH * 0.02 + (i % 3) * cardH * 0.006; ctx.DrawRectangle( new SolidColorBrush(ConfettiPalette[i % ConfettiPalette.Length]), null, new Rect(dotX, dotY, size, size)); } } DrawCentered(ctx, "GIFTED MEMBERSHIP", cardCX, margin + cardH * 0.22, cardH * 0.12, FontWeights.Bold, Color.FromRgb(0xff, 0xd1, 0x66), cardW * 0.9); var giver = string.IsNullOrWhiteSpace(e.GifterName) ? e.AuthorName : e.GifterName; DrawCentered(ctx, giver, cardCX, margin + cardH * 0.42, cardH * 0.20, FontWeights.Bold, White, cardW * 0.9); DrawCentered(ctx, "gave you a membership — celebrate!", cardCX, margin + cardH * 0.68, cardH * 0.11, FontWeights.Normal, Color.FromRgb(0xdb, 0xff, 0xf0), cardW * 0.9); ctx.Pop(); ctx.Pop(); } return CreateFrame(visual, w, h, dpi); } private static VideoFrame Milestone(ChatMessage e, double t, int w, int h) { var dpi = 96.0; var visual = new DrawingVisual(); using (var ctx = visual.RenderOpen()) { var margin = Math.Max(w, h) * 0.04; var cardW = w - margin * 2; var cardH = h - margin * 2; var cardCX = w / 2.0; var enter = Clamp01(t / EnterEnd); var exit = Clamp01((t - ExitStart) / (1 - ExitStart)); var cardY = Lerp(h + cardH, margin, EaseOutCubic(enter)); cardY = Lerp(cardY, -cardH, EaseInCubic(exit)); DrawCard(ctx, margin, cardY, cardW, cardH, "#431407", "#c2410c", 14); DrawBrand(ctx, margin, cardY, cardW, cardH); var hero = e.MilestoneMonths > 0 ? $"{e.MilestoneMonths}" : "MILESTONE"; DrawCentered(ctx, hero, cardCX, cardY + cardH * 0.26, cardH * 0.30, FontWeights.Bold, Color.FromRgb(0xff, 0xdd, 0x66), cardW * 0.8); var sub = e.MilestoneMonths > 0 ? "MONTHS STRONG" : "SUPPORTING THE STREAM"; DrawCentered(ctx, sub, cardCX, cardY + cardH * 0.52, cardH * 0.11, FontWeights.SemiBold, White, cardW * 0.9); DrawCentered(ctx, "— " + e.AuthorName + " —", cardCX, cardY + cardH * 0.70, cardH * 0.10, FontWeights.Normal, Color.FromRgb(0xff, 0xe8, 0xd0), cardW * 0.9); // Tenure bar: grows through the first half, retracts on exit. var barP = Clamp01((t - 0.10) / 0.55) * (1 - exit); var barW = cardW * 0.6 * barP; ctx.DrawRoundedRectangle( new SolidColorBrush(Color.FromRgb(0xff, 0xd1, 0x66)), null, new Rect(cardCX - barW / 2, cardY + cardH * 0.84, barW, cardH * 0.045), 4, 4); } return CreateFrame(visual, w, h, dpi); } // ─── Shared drawing ─── private static void DrawCard(DrawingContext ctx, double x, double y, double w, double h, string fromHex, string toHex, double radius) { var brush = new LinearGradientBrush(ParseColor(fromHex), ParseColor(toHex), new Point(0, 0), new Point(1, 1)); ctx.DrawRoundedRectangle(brush, new Pen(new SolidColorBrush(Color.FromArgb(0x66, 0xff, 0xff, 0xff)), Math.Max(1, radius * 0.06)), new Rect(x, y, w, h), radius, radius); } private static void DrawBrand(DrawingContext ctx, double x, double y, double w, double h) { var formatted = new FormattedText( Brand, Invariant, FlowDirection.LeftToRight, new Typeface(new FontFamily("Segoe UI"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal), Math.Max(9, h * 0.055), new SolidColorBrush(Color.FromArgb(0x88, 0xff, 0xff, 0xff)), 96.0); ctx.DrawText(formatted, new Point(x + w - formatted.Width - h * 0.08, y + h - formatted.Height - h * 0.05)); } private static void DrawCentered(DrawingContext ctx, string text, double cx, double cy, double size, FontWeight weight, Color color, double maxWidth) { var formatted = new FormattedText( text, Invariant, FlowDirection.LeftToRight, new Typeface(new FontFamily("Segoe UI"), FontStyles.Normal, weight, FontStretches.Normal), size, new SolidColorBrush(color), 96.0); formatted.MaxTextWidth = maxWidth; formatted.MaxLineCount = 1; formatted.Trimming = TextTrimming.CharacterEllipsis; ctx.DrawText(formatted, new Point(cx - formatted.Width / 2, cy - formatted.Height / 2)); } private static Color ParseColor(string hex) { hex = hex.Trim().TrimStart('#'); if (hex.Length == 6) { var r = byte.Parse(hex.AsSpan(0, 2), NumberStyles.HexNumber); var g = byte.Parse(hex.AsSpan(2, 2), NumberStyles.HexNumber); var b = byte.Parse(hex.AsSpan(4, 2), NumberStyles.HexNumber); return Color.FromRgb(r, g, b); } if (hex.Length == 8) { var a = byte.Parse(hex.AsSpan(0, 2), NumberStyles.HexNumber); var r = byte.Parse(hex.AsSpan(2, 2), NumberStyles.HexNumber); var g = byte.Parse(hex.AsSpan(4, 2), NumberStyles.HexNumber); var b = byte.Parse(hex.AsSpan(6, 2), NumberStyles.HexNumber); return Color.FromArgb(a, r, g, b); } return Colors.White; } private static VideoFrame CreateFrame(DrawingVisual visual, int width, int height, double dpi) { var rtb = new RenderTargetBitmap(width, height, dpi, dpi, PixelFormats.Pbgra32); rtb.Render(visual); var stride = width * 4; var pixels = new byte[stride * height]; rtb.CopyPixels(pixels, stride, 0); for (var i = 0; i < pixels.Length; i += 4) { (pixels[i], pixels[i + 2]) = (pixels[i + 2], pixels[i]); } return new VideoFrame(width, height, pixels); } private static double Clamp01(double v) => Math.Clamp(v, 0.0, 1.0); private static double Lerp(double a, double b, double p) => a + (b - a) * p; private static double EaseInCubic(double p) => p * p * p; private static double EaseOutCubic(double p) => 1 - Math.Pow(1 - p, 3); private static double EaseOutBack(double p) { const double c1 = 1.70158; const double c3 = c1 + 1; return 1 + c3 * Math.Pow(p - 1, 3) + c1 * Math.Pow(p - 1, 2); } }