| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- using Avalonia;
- using Avalonia.Media;
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- namespace YZWater.Avalonia.Controls.Renderers;
- /// <summary>
- /// 水箱渲染器。
- /// <list type="bullet">
- /// <item><see cref="Draw"/>:简单矩形水箱(兼容模式)</item>
- /// <item><see cref="DrawSection"/>:剖面池(SCADA 拟真)— 池壁质感 + 液位填充 +
- /// 正弦波浪动画 + 液位大字 + 左侧刻度尺 + 彩色阈值线(启泵/停泵/保护液位)</item>
- /// </list>
- /// </summary>
- public static class TankRenderer
- {
- /// <summary>阈值线定义(液位为与 waterLevel 同单位的原始值)</summary>
- public record ThresholdLine(double Level, Color Color, string Label);
- /// <summary>
- /// 绘制一个简单矩形水箱(兼容模式,保留原样式)。
- /// </summary>
- public static void Draw(
- DrawingContext context,
- Rect bounds,
- double waterLevel,
- Color waterColor,
- string name,
- string code,
- IBrush bgBrush,
- IBrush borderBrush,
- IBrush textPrimaryBrush,
- IBrush textTertiaryBrush,
- IBrush warningBrush,
- IBrush dangerBrush)
- {
- var x = bounds.X;
- var y = bounds.Y;
- var w = bounds.Width;
- var h = bounds.Height;
- // 箱体
- context.DrawRectangle(bgBrush, new Pen(borderBrush, 1.5), bounds, 3, 3);
- // 水位填充
- var clampedLevel = Math.Clamp(waterLevel, 0, 100);
- if (clampedLevel > 0.5)
- {
- var waterH = (h - 4) * clampedLevel / 100.0;
- var waterRect = new Rect(x + 2, y + h - 2 - waterH, w - 4, waterH);
- var waterBrush = new SolidColorBrush(waterColor, 0.65);
- context.DrawRectangle(waterBrush, null, waterRect);
- // 水面波纹
- var waveY = waterRect.Y;
- var waveBrush = new SolidColorBrush(waterColor, 0.35);
- for (int i = 0; i < 3; i++)
- {
- var wx = waterRect.X + 4 + i * (waterRect.Width / 3.0);
- context.DrawLine(new Pen(waveBrush, 1),
- new Point(wx, waveY), new Point(wx + waterRect.Width / 6.0, waveY + 2));
- }
- }
- var centerX = x + w / 2;
- var centerY = y + h / 2;
- // 名称标签
- var nameFontSize = Math.Max(8, Math.Min(w, h) * 0.077);
- DrawTextAt(context, centerX - 20, y + 6, name, nameFontSize, textPrimaryBrush, FontWeight.Bold);
- var codeFontSize = Math.Max(8, Math.Min(w, h) * 0.063);
- DrawTextAt(context, centerX - 14, y + 20, code, codeFontSize, textTertiaryBrush);
- // 液位百分比
- var pctText = $"{clampedLevel:F1}%";
- IBrush pctColor = clampedLevel > 80 ? warningBrush :
- clampedLevel < 20 ? dangerBrush :
- new SolidColorBrush(waterColor);
- var pctFontSize = Math.Max(8, Math.Min(w, h) * 0.092);
- DrawTextAt(context, centerX - 18, centerY + 8, pctText, pctFontSize, pctColor, FontWeight.Bold);
- }
- // ════════════════════════════════════════════════════════
- // 剖面池(拟真模式)
- // ════════════════════════════════════════════════════════
- /// <summary>
- /// 绘制剖面式水池:池壁(混凝土质感)+ 液位填充 + 正弦波浪动画 +
- /// 液位大字 + 左侧刻度尺 + 彩色阈值横线。
- /// </summary>
- /// <param name="context">绘图上下文</param>
- /// <param name="bounds">池体矩形(屏幕坐标,含池壁)</param>
- /// <param name="waterLevel">当前液位(原始值,如米)</param>
- /// <param name="maxLevel">满量程液位(原始值)</param>
- /// <param name="waterColor">水体颜色</param>
- /// <param name="name">池名称</param>
- /// <param name="wavePhase">波浪动画相位(0~360)</param>
- /// <param name="thresholds">阈值线集合(按原始液位值)</param>
- /// <param name="palette">主题画刷</param>
- public static void DrawSection(
- DrawingContext context,
- Rect bounds,
- double waterLevel,
- double maxLevel,
- Color waterColor,
- string name,
- double wavePhase,
- IReadOnlyList<ThresholdLine> thresholds,
- SectionPalette palette)
- {
- var wall = Math.Max(4, Math.Min(bounds.Width, bounds.Height) * 0.06); // 池壁厚度
- var inner = new Rect(bounds.X + wall, bounds.Y + wall * 0.6,
- bounds.Width - wall * 2, bounds.Height - wall * 0.6 - wall * 0.5);
- // ─── 池壁(外框,混凝土质感 + U 形实体壁)───
- // 池底
- var wallBrush = new SolidColorBrush(Color.FromArgb(180, 90, 98, 112));
- context.DrawRectangle(wallBrush, new Pen(ThemeHelper.Border, 1.5), bounds, 2, 2);
- // U 形内壁(左/右/底)
- var innerWall = new SolidColorBrush(Color.FromArgb(140, 55, 62, 74));
- // 左壁
- context.DrawRectangle(innerWall, null,
- new Rect(inner.X - wall, inner.Y, wall, inner.Height));
- // 右壁
- context.DrawRectangle(innerWall, null,
- new Rect(inner.Right, inner.Y, wall, inner.Height));
- // 池底
- context.DrawRectangle(innerWall, null,
- new Rect(inner.X, inner.Bottom, inner.Width, wall));
- // 内壁阴影线(增强立体感)
- context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(100, 40, 45, 55)), 1.5),
- new Point(inner.X, inner.Y), new Point(inner.X, inner.Bottom));
- context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(100, 40, 45, 55)), 1.5),
- new Point(inner.Right, inner.Y), new Point(inner.Right, inner.Bottom));
- context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(100, 40, 45, 55)), 1.5),
- new Point(inner.X, inner.Bottom), new Point(inner.Right, inner.Bottom));
- // ─── 内腔背景 ───
- context.DrawRectangle(palette.CavityBrush, null, inner);
- // ── 液位填充 + 正弦波浪 ──
- if (maxLevel <= 0) maxLevel = 1;
- var levelPct = Math.Clamp(waterLevel / maxLevel, 0, 1);
- var waterTop = inner.Bottom - inner.Height * levelPct;
- // 波浪幅度随池高缩放(比旧版更明显)
- var amp1 = Math.Max(4, inner.Height * 0.045);
- if (levelPct > 0.01)
- {
- var waterRect = new Rect(inner.X, waterTop, inner.Width, inner.Bottom - waterTop);
- var waterBrush = new LinearGradientBrush
- {
- StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
- EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
- GradientStops =
- {
- new GradientStop(Color.FromArgb(210, waterColor.R, waterColor.G, waterColor.B), 0),
- new GradientStop(Color.FromArgb(120, waterColor.R, waterColor.G, waterColor.B), 1),
- }
- };
- context.DrawRectangle(waterBrush, null, waterRect);
- // 水面亮线(液面与空气交界处的高光)
- context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), 1.8),
- new Point(inner.X + 1, waterTop), new Point(inner.Right - 1, waterTop));
- // 正弦波浪(3 层相位错开,半透明叠加)
- DrawWave(context, waterTop, inner, waterColor, wavePhase, 0.55, amp1, 1.0);
- DrawWave(context, waterTop + 2, inner, waterColor, wavePhase * 1.3 + 120, 0.32, amp1 * 1.3, 0.7);
- DrawWave(context, waterTop + 4, inner, waterColor, wavePhase * 0.8 + 240, 0.20, amp1 * 1.6, 0.5);
- // 池内气泡上升动画(6 个气泡,相位不同,持续上升)
- DrawBubbles(context, inner, waterRect, waterColor, wavePhase, levelPct);
- }
- else
- {
- // 空池:池底积水带 + 沉积示意,避免"死白框"
- var siltRect = new Rect(inner.X, inner.Bottom - inner.Height * 0.04, inner.Width, inner.Height * 0.04);
- context.DrawRectangle(new SolidColorBrush(Color.FromArgb(90, 110, 120, 100)), null, siltRect);
- var residRect = new Rect(inner.X, inner.Bottom - inner.Height * 0.02, inner.Width, inner.Height * 0.02);
- context.DrawRectangle(new SolidColorBrush(Color.FromArgb(140, waterColor.R, waterColor.G, waterColor.B)), null, residRect);
- }
- // ─── 阈值线(画在水位之上,一眼看出距离)───
- if (thresholds != null)
- {
- foreach (var th in thresholds)
- {
- if (th.Level <= 0 || th.Level > maxLevel) continue;
- var ty = inner.Bottom - inner.Height * Math.Clamp(th.Level / maxLevel, 0, 1);
- var pen = new Pen(new SolidColorBrush(th.Color, 0.9), 1.5, DashStyle.Dash);
- context.DrawLine(pen, new Point(inner.X, ty), new Point(inner.Right, ty));
- // 阈值标签(左侧小字,紧贴线上方)
- var label = $"{th.Label} {th.Level:F2}";
- var ft = new FormattedText(label, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
- new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold),
- Math.Max(8, inner.Height * 0.055),
- new SolidColorBrush(th.Color));
- context.DrawText(ft, new Point(inner.X + 2, ty - ft.Height - 1));
- }
- }
- // ─── 当前液位值(气泡浮标:跟随水面高度,贴池右侧)───
- var valueText = $"{waterLevel:F2} m";
- var valueSize = Math.Max(11, inner.Height * 0.085);
- var valueFt = new FormattedText(valueText, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
- new Typeface("Consolas", FontStyle.Normal, FontWeight.Bold), valueSize, palette.TextPrimaryBrush);
- var badgeW = valueFt.Width + 14;
- var badgeH = valueFt.Height + 8;
- var badgeX = inner.Right - badgeW - 6;
- var badgeY = Math.Clamp(waterTop - badgeH - 8, inner.Y + 2, inner.Bottom - badgeH - 2);
- // 浮标牌 + 指向水面的指针
- var badgeRect = new Rect(badgeX, badgeY, badgeW, badgeH);
- context.DrawRectangle(palette.CavityBrush, new Pen(palette.BorderBrush, 1), badgeRect, 4, 4);
- context.DrawLine(new Pen(palette.BorderBrush, 1.2),
- new Point(badgeX + badgeW / 2, badgeY + badgeH),
- new Point(badgeX + badgeW / 2, waterTop));
- context.DrawEllipse(new SolidColorBrush(waterColor), null,
- new Rect(badgeX + badgeW / 2 - 3, waterTop - 3, 6, 6));
- context.DrawText(valueFt,
- new Point(badgeRect.Center.X - valueFt.Width / 2, badgeRect.Center.Y - valueFt.Height / 2));
- // ─── 池名称(顶部)───
- var nameSize = Math.Max(9, bounds.Height * 0.07);
- var nameFt = new FormattedText(name, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
- new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold), nameSize, palette.TextPrimaryBrush);
- context.DrawText(nameFt, new Point(bounds.X + (bounds.Width - nameFt.Width) / 2, bounds.Y - nameFt.Height - 2));
- // ─── 水面亮线(液面与空气交界处的高光)───
- if (levelPct > 0.01)
- {
- var waterRect = new Rect(inner.X, waterTop, inner.Width, inner.Bottom - waterTop);
- context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), 1.8),
- new Point(inner.X + 1, waterTop), new Point(inner.Right - 1, waterTop));
- }
- // ─── 左侧刻度尺 ───
- DrawRuler(context, bounds, inner, maxLevel, palette);
- }
- /// <summary>池内气泡上升动画(6 个气泡,不同相位,从池底持续升起破裂)</summary>
- private static void DrawBubbles(DrawingContext context, Rect inner, Rect waterRect,
- Color waterColor, double phase, double levelPct)
- {
- const int bubbleCount = 6;
- var bubbleBrush = new SolidColorBrush(Color.FromArgb(120, waterColor.R, waterColor.G, waterColor.B));
- for (int i = 0; i < bubbleCount; i++)
- {
- // 每个气泡的相位偏移不同
- var p = (phase + i * 67) % 360;
- var t = p / 360.0; // 0→1 从池底到水面
- var bx = inner.X + inner.Width * (0.15 + (i * 0.14 + Math.Sin(p * 0.05) * 0.08));
- var by = inner.Bottom - inner.Height * levelPct * t - (inner.Height * 0.05); // 到水面以下
- // 气泡大小随上升略缩小
- var bSize = Math.Max(1.5, 3.5 * (1 - t * 0.3));
- var bubbleAlpha = (byte)(140 * (1 - t * 0.6));
- // 只在液面以下绘制
- if (by >= inner.Y && by <= waterRect.Bottom)
- {
- context.DrawEllipse(new SolidColorBrush(Color.FromArgb(bubbleAlpha,
- waterColor.R, waterColor.G, waterColor.B)), null,
- new Rect(bx - bSize, by - bSize, bSize * 2, bSize * 2));
- }
- }
- }
- /// <summary>绘制单层正弦波浪(填充至池底,裁剪在内腔范围内)</summary>
- private static void DrawWave(DrawingContext context, double topY, Rect inner,
- Color waterColor, double phase, double alpha, double amplitude, double wavelengthScale)
- {
- if (topY < inner.Y - amplitude || topY > inner.Bottom) return;
- const int steps = 24;
- var geometry = new StreamGeometry();
- using (var ctx = geometry.Open())
- {
- ctx.BeginFigure(new Point(inner.X, inner.Bottom), isFilled: true);
- // 从左到右沿正弦曲线采样
- for (int i = 0; i <= steps; i++)
- {
- var t = (double)i / steps;
- var px = inner.X + inner.Width * t;
- var py = topY + Math.Sin((t * Math.PI * 2 * wavelengthScale) + (phase * Math.PI / 180)) * amplitude;
- ctx.LineTo(new Point(px, py), isStroked: false);
- }
- // 闭合到池底
- ctx.EndFigure(true);
- }
- var brush = new SolidColorBrush(waterColor, alpha);
- context.DrawGeometry(brush, null, geometry);
- }
- /// <summary>左侧刻度尺(0~maxLevel,按 5 段标注)</summary>
- private static void DrawRuler(DrawingContext context, Rect bounds, Rect inner,
- double maxLevel, SectionPalette palette)
- {
- const int segments = 5;
- var tickLen = Math.Max(3, bounds.Width * 0.02);
- var fontSize = Math.Max(7, bounds.Height * 0.045);
- var pen = new Pen(palette.TextTertiaryBrush, 1);
- for (int i = 0; i <= segments; i++)
- {
- var value = maxLevel * i / segments;
- var y = inner.Bottom - inner.Height * ((double)i / segments);
- context.DrawLine(pen, new Point(bounds.X - tickLen, y), new Point(bounds.X - 1, y));
- var label = value.ToString("F1", CultureInfo.CurrentCulture);
- var ft = new FormattedText(label, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
- new Typeface("Consolas"), fontSize, palette.TextTertiaryBrush);
- context.DrawText(ft, new Point(bounds.X - tickLen - ft.Width - 2, y - ft.Height / 2));
- }
- }
- private static void DrawTextAt(DrawingContext context, double x, double y, string text,
- double size, IBrush brush, FontWeight weight = FontWeight.Normal)
- {
- var formatted = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
- new Typeface("Microsoft YaHei", FontStyle.Normal, weight), size, brush);
- context.DrawText(formatted, new Point(x, y));
- }
- }
- /// <summary>剖面池主题画刷集(从 ThemeHelper 取色后传入,控件不用 DynamicResource)</summary>
- public record SectionPalette(
- IBrush WallBrush, // 池壁(混凝土)
- IBrush WallShadowBrush, // 池壁内侧阴影线
- IBrush CavityBrush, // 内腔背景
- IBrush BorderBrush, // 边框
- IBrush TextPrimaryBrush, // 主文字
- IBrush TextTertiaryBrush // 辅助文字(刻度)
- );
|