Files
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

443 lines
19 KiB
C#

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<string, Brush> _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<string, Brush> _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<string, bool> _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<TextBlock>(); // Store TextBlocks for the line
var positions = new List<double>(); // 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();
}
}