using PCL.Core.App; using PCL.Core.Utils; namespace PCL.Core.UI.Controls; using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Threading; using System.Linq; using System.Windows.Media.Imaging; public partial class MotdRenderer { // Default Color for originalColorMap: #808080 // Minecraft color code mapping private static readonly Dictionary _ColorMapWithBlackBackground = new() { { "0", Brushes.Black }, // Black { "1", new SolidColorBrush(Color.FromRgb(0, 0, 170)) }, // Dark Blue { "2", new SolidColorBrush(Color.FromRgb(0, 170, 0)) }, // Dark Green { "3", new SolidColorBrush(Color.FromRgb(0, 170, 170)) }, // Cyan { "4", new SolidColorBrush(Color.FromRgb(170, 0, 0)) }, // Dark Red { "5", new SolidColorBrush(Color.FromRgb(170, 0, 170)) }, // Purple { "6", new SolidColorBrush(Color.FromRgb(255, 170, 0)) }, // Gold { "7", Brushes.LightGray }, // Gray { "8", Brushes.DarkGray }, // Dark Gray { "9", Brushes.Blue }, // Blue { "a", Brushes.Lime }, // Green { "b", Brushes.Cyan }, // Cyan { "c", Brushes.Red }, // Red { "d", Brushes.Magenta }, // Magenta { "e", Brushes.Yellow }, // Yellow { "f", Brushes.White } // White }; // Color code mapping optimized for white background (#f3f6fa) private static readonly Dictionary _ColorMapWithWhiteBackground = new() { { "0", new SolidColorBrush(Color.FromRgb(51, 51, 51)) }, // Deep Gray #333333 { "1", new SolidColorBrush(Color.FromRgb(0, 48, 135)) }, // Navy Blue #003087 { "2", new SolidColorBrush(Color.FromRgb(0, 128, 0)) }, // Forest Green #008000 { "3", new SolidColorBrush(Color.FromRgb(0, 122, 122)) }, // Cyan #007A7A { "4", new SolidColorBrush(Color.FromRgb(161, 0, 0)) }, // Deep Red #A10000 { "5", new SolidColorBrush(Color.FromRgb(128, 0, 128)) }, // Deep Purple #800080 { "6", new SolidColorBrush(Color.FromRgb(204, 112, 0)) }, // Deep Orange #CC7000 { "7", new SolidColorBrush(Color.FromRgb(102, 102, 102)) }, // Medium Gray #666666 { "8", new SolidColorBrush(Color.FromRgb(68, 68, 68)) }, // Charcoal #444444 { "9", new SolidColorBrush(Color.FromRgb(0, 68, 204)) }, // Royal Blue #0044CC { "a", new SolidColorBrush(Color.FromRgb(0, 153, 0)) }, // Green #009900 { "b", new SolidColorBrush(Color.FromRgb(0, 161, 161)) }, // Cyan #00A1A1 { "c", new SolidColorBrush(Color.FromRgb(204, 0, 0)) }, // Red #CC0000 { "d", new SolidColorBrush(Color.FromRgb(194, 0, 194)) }, // Magenta #C200C2 { "e", new SolidColorBrush(Color.FromRgb(179, 160, 0)) }, // Deep Yellow #B3A000 { "f", new SolidColorBrush(Color.FromRgb(136, 136, 136)) } // White }; // Format code mapping private readonly Dictionary _formatMap = new() { { "l", true }, // Bold { "o", true }, // Italic { "n", true }, // Underline { "m", true }, // Strikethrough { "k", true }, // Obfuscated (not supported) { "r", false } // Reset }; // Store TextBlock and original text for §k obfuscated text private readonly List<(TextBlock TextBlock, string OriginalText)> _obfuscatedTextBlocks = []; private readonly Random _random = new(); private const string RandomChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"; private readonly Color _backgroundColor = Color.FromRgb(243, 246, 250); // #f3f6fa public MotdRenderer() { InitializeComponent(); // 初始化 XAML 定义的控件 // Start timer to update §k text var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(20) }; timer.Tick += _UpdateObfuscatedText; timer.Start(); } private void _UpdateObfuscatedText(object? sender, EventArgs e) { foreach (var (textBlock, originalText) in _obfuscatedTextBlocks) { // Generate random characters of the same length as the original text var obfuscated = string.Join("", Enumerable.Range(0, originalText.Length).Select(_ => RandomChars[_random.Next(RandomChars.Length)])); textBlock.Text = obfuscated; } } private static double _GetRelativeLuminance(Color color) { var r = color.R / 255.0; var g = color.G / 255.0; var b = color.B / 255.0; var rL = r <= 0.03928 ? r / 12.92 : Math.Pow((r + 0.055) / 1.055, 2.4); var gL = g <= 0.03928 ? g / 12.92 : Math.Pow((g + 0.055) / 1.055, 2.4); var bL = b <= 0.03928 ? b / 12.92 : Math.Pow((b + 0.055) / 1.055, 2.4); return 0.2126 * rL + 0.7152 * gL + 0.0722 * bL; } private static double _GetContrastRatio(Color foreground, Color background) { var l1 = _GetRelativeLuminance(foreground); var l2 = _GetRelativeLuminance(background); return (Math.Max(l1, l2) + 0.05) / (Math.Min(l1, l2) + 0.05); } private Color _AdjustColorForContrast(Color inputColor) { var contrastRatio = _GetContrastRatio(inputColor, _backgroundColor); if (contrastRatio >= 4.5) return inputColor; // Contrast is sufficient // Convert RGB to HSL var r = inputColor.R / 255.0; var g = inputColor.G / 255.0; var b = inputColor.B / 255.0; var max = Math.Max(Math.Max(r, g), b); var min = Math.Min(Math.Min(r, g), b); var l = (max + min) / 2.0; double s; double h; if (Math.Abs(max - min) < double.Epsilon) { h = 0.0; s = 0.0; } else { var d = max - min; s = l > 0.5 ? d / (2.0 - max - min) : d / (max + min); h = max switch { _ when Math.Abs(max - r) < double.Epsilon => (g - b) / d + (g < b ? 6.0 : 0.0), _ when Math.Abs(max - g) < double.Epsilon => (b - r) / d + 2.0, _ => (r - g) / d + 4.0 }; h /= 6.0; } // Decrease lightness until contrast ratio ≥ 4.5:1 var newL = l; var adjustedColor = inputColor; while (newL > 0.1 && _GetContrastRatio(adjustedColor, _backgroundColor) < 4.5) { newL -= 0.05; // Gradually decrease lightness double newR, newG, newB; if (s == 0) { newR = newL; newG = newL; newB = newL; } else { var q = newL < 0.5 ? newL * (1.0 + s) : newL + s - newL * s; var p = 2.0 * newL - q; newR = _HueToRgb(p, q, h + 1.0 / 3.0); newG = _HueToRgb(p, q, h); newB = _HueToRgb(p, q, h - 1.0 / 3.0); } adjustedColor = Color.FromRgb((byte)(newR * 255), (byte)(newG * 255), (byte)(newB * 255)); } // If contrast is still insufficient, use default color #555555 return _GetContrastRatio(adjustedColor, _backgroundColor) < 4.5 ? Color.FromRgb(85, 85, 85) : adjustedColor; } private static double _HueToRgb(double p, double q, double t) { if (t < 0) t += 1.0; if (t > 1) t -= 1.0; if (t < 1.0 / 6.0) return p + (q - p) * 6.0 * t; if (t < 0.5) return q; if (t < 2.0 / 3.0) return p + (q - p) * (2.0 / 3.0 - t) * 6.0; return p; } public static bool TryGetColorFromCode(string code, bool isDarkMode, out String? color) { var colorMap = isDarkMode ? _ColorMapWithBlackBackground : _ColorMapWithWhiteBackground; var success = colorMap.TryGetValue(code.ToLower(), out var brush); if (!success) { color = null; return false; } var solidColorBrush = ((SolidColorBrush)brush!).Color; color = $"#{solidColorBrush.R:X2}{solidColorBrush.G:X2}{solidColorBrush.B:X2}"; return success; } public void RenderMotd(string motd, bool isDarkMode = true, int maxLines = int.MaxValue, double fontSize = 12, bool isCentered = true) { MotdCanvas.Children.Clear(); _obfuscatedTextBlocks.Clear(); motd ??= string.Empty; var colorMap = isDarkMode ? _ColorMapWithBlackBackground : _ColorMapWithWhiteBackground; var font = Config.Preference.MotdFont; var fontFamily = new FontFamily(string.IsNullOrWhiteSpace(font) ? "./Resources/#PCL English, Segoe UI, Microsoft YaHei UI" : font); var canvasWidth = MotdCanvas.ActualWidth > 0 ? MotdCanvas.ActualWidth : 300; // Prevent zero width var canvasHeight = MotdCanvas.ActualHeight > 0 ? MotdCanvas.ActualHeight : 34; // Prevent zero height double y = 10; // Split multi-line MOTD motd = motd.Replace("\n", "\r\n"); var lines = motd.Split("\r\n"); var currentColor = colorMap["f"]; var isBold = false; var isItalic = false; var isUnderline = false; var isStrikethrough = false; var isObfuscated = false; // 限制显示的行数不超过maxLines var lineCount = Math.Min(lines.Length, maxLines); for (var lineIndex = 0; lineIndex < lineCount; lineIndex++) { var line = lines[lineIndex].Trim(); var parts = RegexPatterns.MotdCode.Split(line); // Calculate line width double lineWidth = 0; double lineHeight = 0; double tempX = 0; // Temporary x-coordinate for width calculation var textBlocks = new List(); // Store TextBlocks for the line var positions = new List(); // Store x-coordinates for each TextBlock // 找出第一个和最后一个不是格式符的文本部分的索引 var firstNonFormatPartIndex = -1; var lastNonFormatPartIndex = -1; for (var j = 0; j < parts.Length; j++) { var part = parts[j]; if (!string.IsNullOrEmpty(part) && !(part.StartsWith('§') && part.Length == 2) && !RegexPatterns.HexColor.IsMatch(part)) { if (firstNonFormatPartIndex == -1) { firstNonFormatPartIndex = j; } lastNonFormatPartIndex = j; } } for (var i = 0; i < parts.Length; i++) { var part = parts[i]; var partTrimmed = part; if (i == firstNonFormatPartIndex) { partTrimmed = part.TrimStart(); } else if (i == lastNonFormatPartIndex) { partTrimmed = part.TrimEnd(); } if (string.IsNullOrEmpty(partTrimmed)) continue; // Handle § color codes if (partTrimmed.StartsWith('§') && partTrimmed.Length == 2) { var code = partTrimmed[1..].ToLower(); if (colorMap.TryGetValue(code, out var brush)) { currentColor = brush; isBold = false; isItalic = false; isUnderline = false; isStrikethrough = false; isObfuscated = false; } else if (_formatMap.ContainsKey(code)) { switch (code) { case "l": isBold = true; break; case "o": isItalic = true; break; case "n": isUnderline = true; break; case "m": isStrikethrough = true; break; case "k": isObfuscated = true; break; case "r": currentColor = colorMap["f"]; isBold = false; isItalic = false; isUnderline = false; isStrikethrough = false; isObfuscated = false; break; } } continue; } // Handle RGB color codes if (RegexPatterns.HexColor.IsMatch(partTrimmed)) { try { var hex = partTrimmed[1..]; var r = Convert.ToByte(hex[..2], 16); var g = Convert.ToByte(hex.Substring(2, 2), 16); var b = Convert.ToByte(hex.Substring(4, 2), 16); var inputColor = Color.FromRgb(r, g, b); currentColor = new SolidColorBrush(_AdjustColorForContrast(inputColor)); isBold = false; isItalic = false; isUnderline = false; isStrikethrough = false; isObfuscated = false; } catch { // Invalid RGB color, keep current color } continue; } // Render text, always use original text for width calculation var displayText = partTrimmed; TextBlock textBlock; if (isObfuscated) { // Generate initial random characters for §k text foreach (var singleChar in partTrimmed) { displayText = RandomChars[_random.Next(RandomChars.Length)].ToString(); textBlock = _RenderText(displayText, fontFamily, fontSize, currentColor, isBold, isItalic, isUnderline, isStrikethrough, tempX, y, true, _MeasureTextWidth(singleChar.ToString(), fontFamily, fontSize, isBold, isItalic)); _obfuscatedTextBlocks.Add((textBlock, singleChar.ToString())); textBlocks.Add(textBlock); positions.Add(tempX); tempX += _MeasureTextWidth(singleChar.ToString(), fontFamily, fontSize, isBold, isItalic); } } else { textBlock = _RenderText(displayText, fontFamily, fontSize, currentColor, isBold, isItalic, isUnderline, isStrikethrough, tempX, y); textBlocks.Add(textBlock); positions.Add(tempX); } // Update tempX coordinate using original text width if (!isObfuscated) { tempX += _MeasureTextWidth(partTrimmed, fontFamily, fontSize, isBold, isItalic); } var textHeight = _MeasureTextHeight(partTrimmed, fontFamily, fontSize, isBold, isItalic); lineHeight = textHeight > lineHeight ? textHeight : lineHeight; lineWidth = tempX; // Update line width } if (isCentered) { // Center-align: Adjust x-coordinates for each TextBlock var offsetX = (canvasWidth - lineWidth) / 2; for (var i = 0; i < textBlocks.Count; i++) { Canvas.SetLeft(textBlocks[i], positions[i] + offsetX); } } // 计算当前行的垂直位置 double offsetY; if (lineCount == 1) { // 单行文本居中 offsetY = (canvasHeight - lineHeight) / 2; } else { // 多行文本的位置计算 var totalHeight = lineCount * lineHeight; // 使用实际显示的行数计算总高度 var startOffset = (canvasHeight - totalHeight) / 2; offsetY = startOffset + (lineIndex * lineHeight); } // 设置所有文本块的垂直位置 foreach (var textBlock in textBlocks) { Canvas.SetTop(textBlock, offsetY); } } } private TextBlock _RenderText(string text, FontFamily fontFamily, double fontSize, Brush color, bool isBold, bool isItalic, bool isUnderline, bool isStrikethrough, double x, double y, bool withClip = false, double clipWidth = 15) { var textBlock = new TextBlock { Text = text, FontFamily = fontFamily, FontSize = fontSize, Foreground = color, FontWeight = isBold ? FontWeights.Bold : FontWeights.Normal, FontStyle = isItalic ? FontStyles.Italic : FontStyles.Normal }; if (isUnderline || isStrikethrough) { textBlock.TextDecorations = new TextDecorationCollection(); if (isUnderline) textBlock.TextDecorations.Add(TextDecorations.Underline); if (isStrikethrough) textBlock.TextDecorations.Add(TextDecorations.Strikethrough); } if (withClip) { var clipRect = new RectangleGeometry { Rect = new Rect(0, 0, clipWidth, _MeasureTextHeight(text, fontFamily, fontSize, isBold, isItalic)) }; textBlock.Clip = clipRect; } Canvas.SetLeft(textBlock, x); Canvas.SetTop(textBlock, y); if (Content is Canvas canvas) { canvas.Children.Add(textBlock); } return textBlock; } private static FormattedText _CreateFormattedText(string text, FontFamily fontFamily, double fontSize, bool isBold, bool isItalic) { return new FormattedText( text, System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, new Typeface(fontFamily, isItalic ? FontStyles.Italic : FontStyles.Normal, isBold ? FontWeights.Bold : FontWeights.Normal, FontStretches.Normal), fontSize, Brushes.White, 96); } private static double _MeasureTextWidth(string text, FontFamily fontFamily, double fontSize, bool isBold, bool isItalic) { return _CreateFormattedText(text, fontFamily, fontSize, isBold, isItalic).WidthIncludingTrailingWhitespace; } private static double _MeasureTextHeight(string text, FontFamily fontFamily, double fontSize, bool isBold, bool isItalic) { return _CreateFormattedText(text, fontFamily, fontSize, isBold, isItalic).Height; } public void RenderCanvas() { // Ensure Canvas is rendered MotdCanvas.UpdateLayout(); // Generate static random characters for §k text foreach (var (textBlock, originalText) in _obfuscatedTextBlocks) { textBlock.Text = string.Join("", Enumerable.Range(0, originalText.Length).Select(_ => RandomChars[_random.Next(RandomChars.Length)])); } // Capture Canvas using RenderTargetBitmap var rtb = new RenderTargetBitmap( (int)MotdCanvas.Width, (int)MotdCanvas.Height, 96, 96, PixelFormats.Pbgra32); rtb.Render(MotdCanvas); } public void ClearCanvas() { MotdCanvas.Children.Clear(); _obfuscatedTextBlocks.Clear(); } }