using System.Drawing.Drawing2D; using anjianjingling.Lua; namespace anjianjingling.Forms { /// /// 纯 WinForms 自绘积木画布:插片耦合(榫卯)造型。 /// 顶部凹槽 + 底部凸榫互相咬合;容器块为 C 形插槽;参数显示为可插入的白色插槽。 /// public sealed class BlockCanvas : Panel { private enum RowKind { Block, Slot, Else, End } private sealed class Row { public LuaBlock Block = null!; public int Depth; public Rectangle Bounds; public List Owner = null!; public int Index; public RowKind Kind; } // ---- 几何(像素)---- private const int RowH = 30; // 语句块高度 private const int NotchH = 7; // 榫高:凸榫伸出 / 凹槽凹进 private const int NotchX = 26; // 榫相对块左边的起始位置 private const int NotchSlope = 7; // 斜边宽 private const int NotchFlat = 12; // 平台宽 private const int WallW = 22; // 容器左壁宽(子块缩进) private const int FootH = 16; // 容器底部封口高 private const int ElseH = 22; // 否则分隔条高 private const int Radius = 4; private const int MinInner = 24; // 空容器最小内腔高 private const int PadX = 12; private const int PadTop = 12; private readonly List _rows = new(); private LuaBlock? _selected; private LuaBlock? _hover; private Point _mouseDown; private LuaBlock? _dragBlock; private bool _dragging; private (List List, int Index)? _dropTarget; private int _dropY; private int _dropX; private static readonly Font TitleFont = new("Microsoft YaHei UI", 9.5F); private static readonly Font SmallFont = new("Microsoft YaHei UI", 7.5F); public List Blocks { get; } = new(); public event Action? Changed; public event Action? SelectionChanged; public event Action? EditRequested; public LuaBlock? SelectedBlock => _selected; public BlockCanvas() { DoubleBuffered = true; BackColor = Color.FromArgb(244, 247, 251); AutoScroll = true; AllowDrop = true; SetStyle(ControlStyles.ResizeRedraw | ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserMouse, true); } // ===================== 拖入 ===================== public const string DragFormat = "aj_block_key"; protected override void OnDragEnter(DragEventArgs e) { if (e.Data?.GetDataPresent(DragFormat) == true) e.Effect = DragDropEffects.Copy; base.OnDragEnter(e); } protected override void OnDragOver(DragEventArgs e) { if (e.Data?.GetDataPresent(DragFormat) == true) { e.Effect = DragDropEffects.Copy; UpdateDropIndicator(PointToClient(new Point(e.X, e.Y))); } base.OnDragOver(e); } protected override void OnDragLeave(EventArgs e) { _dropTarget = null; Invalidate(); base.OnDragLeave(e); } protected override void OnDragDrop(DragEventArgs e) { if (e.Data?.GetData(DragFormat) is string key && BlockCatalog.Find(key) != null) { UpdateDropIndicator(PointToClient(new Point(e.X, e.Y))); var block = new LuaBlock { Key = key }; InsertAtDropTarget(block); Select(block); } _dropTarget = null; Invalidate(); base.OnDragDrop(e); } // ===================== 数据操作 ===================== public void AddBlock(LuaBlock block, bool atEnd = true) { if (atEnd || _selected == null) Blocks.Add(block); else InsertAfter(_selected, block); Select(block); Invalidate(); Changed?.Invoke(); } private void InsertAfter(LuaBlock anchor, LuaBlock block) { foreach (var (list, _) in EnumerateLists(Blocks)) { int i = list.IndexOf(anchor); if (i >= 0) { list.Insert(i + 1, block); return; } } Blocks.Add(block); } private static IEnumerable<(List List, int Depth)> EnumerateLists(List list, int depth = 0) { yield return (list, depth); foreach (var b in list) { foreach (var x in EnumerateLists(b.Children, depth + 1)) yield return x; foreach (var x in EnumerateLists(b.ElseChildren, depth + 1)) yield return x; } } public void ClearAll() { Blocks.Clear(); _selected = null; Invalidate(); Changed?.Invoke(); SelectionChanged?.Invoke(null); } /// 内容有变化(含参数修改)时由外部触发重绘与变更通知。 public void NotifyChanged() { Invalidate(); Changed?.Invoke(); } public void LoadBlocks(IEnumerable blocks) { Blocks.Clear(); Blocks.AddRange(blocks); _selected = null; AutoScrollPosition = Point.Empty; // 载入后回到顶部,避免旧滚动位置把第一块顶出视野 Invalidate(); Changed?.Invoke(); SelectionChanged?.Invoke(null); } public void Select(LuaBlock? b) { _selected = b; SelectionChanged?.Invoke(b); Invalidate(); } public void RemoveSelected() { if (_selected == null) return; RemoveFrom(Blocks, _selected); _selected = null; Invalidate(); Changed?.Invoke(); SelectionChanged?.Invoke(null); } public void DuplicateSelected() { if (_selected == null) return; var copy = _selected.Clone(); InsertAfter(_selected, copy); Select(copy); Invalidate(); Changed?.Invoke(); } public void MoveSelected(int dir) { if (_selected == null) return; foreach (var (list, _) in EnumerateLists(Blocks)) { int i = list.IndexOf(_selected); if (i < 0) continue; int j = i + dir; if (j < 0 || j >= list.Count) return; list.RemoveAt(i); list.Insert(j, _selected); Invalidate(); Changed?.Invoke(); return; } } public void ToggleElse() { if (_selected == null) return; var def = BlockCatalog.Find(_selected.Key); if (def is not { Shape: BlockShape.If or BlockShape.IfExists }) return; _selected.HasElse = !_selected.HasElse; if (!_selected.HasElse) _selected.ElseChildren.Clear(); Invalidate(); Changed?.Invoke(); } private static bool RemoveFrom(List list, LuaBlock target) { if (list.Remove(target)) return true; foreach (var b in list) { if (RemoveFrom(b.Children, target)) return true; if (RemoveFrom(b.ElseChildren, target)) return true; } return false; } private static bool IsListInside(LuaBlock root, List list) { foreach (var (l, _) in EnumerateLists(root.Children)) if (ReferenceEquals(l, list)) return true; foreach (var (l, _) in EnumerateLists(root.ElseChildren)) if (ReferenceEquals(l, list)) return true; return false; } // ===================== 鼠标 ===================== protected override void OnMouseDown(MouseEventArgs e) { Focus(); var p = ToLogical(e.Location); var row = HitRow(p); if (row == null || row.Kind is RowKind.Slot or RowKind.End) { // 空槽 / 封口行:不进入拖动,仅选中所属容器 Select(row?.Block); base.OnMouseDown(e); return; } if (e.Button == MouseButtons.Right) { Select(row.Block); BuildMenu().Show(this, p); return; } Select(row.Block); _mouseDown = e.Location; _dragBlock = row.Block; _dragging = false; base.OnMouseDown(e); } protected override void OnMouseMove(MouseEventArgs e) { var lp = ToLogical(e.Location); var h = HitRow(lp); var nb = h?.Block; if (!ReferenceEquals(nb, _hover)) { _hover = nb; Cursor = nb == null ? Cursors.Default : Cursors.Hand; Invalidate(); } if (_dragBlock != null && e.Button == MouseButtons.Left) { if (!_dragging && (Math.Abs(e.X - _mouseDown.X) > 6 || Math.Abs(e.Y - _mouseDown.Y) > 6)) _dragging = true; if (_dragging) { Cursor = Cursors.SizeAll; UpdateDropIndicator(lp); } } base.OnMouseMove(e); } protected override void OnMouseUp(MouseEventArgs e) { if (_dragging && _dragBlock != null && _dropTarget is { } t) { var moving = _dragBlock; if (!IsListInside(moving, t.List)) { int oldIdx = t.List.IndexOf(moving); RemoveFrom(Blocks, moving); int idx = t.Index; if (oldIdx >= 0 && oldIdx < idx) idx--; idx = Math.Clamp(idx, 0, t.List.Count); t.List.Insert(idx, moving); Select(moving); Changed?.Invoke(); } } _dragBlock = null; _dragging = false; _dropTarget = null; Cursor = Cursors.Default; Invalidate(); base.OnMouseUp(e); } protected override void OnMouseDoubleClick(MouseEventArgs e) { var row = HitRow(ToLogical(e.Location)); if (row != null && row.Kind == RowKind.Block) EditRequested?.Invoke(row.Block); base.OnMouseDoubleClick(e); } protected override void OnMouseLeave(EventArgs e) { if (_hover != null) { _hover = null; Invalidate(); } base.OnMouseLeave(e); } protected override void OnKeyDown(KeyEventArgs e) { if (_selected != null) { switch (e.KeyCode) { case Keys.Delete: RemoveSelected(); e.Handled = true; break; case Keys.Up when e.Control: MoveSelected(-1); e.Handled = true; break; case Keys.Down when e.Control: MoveSelected(1); e.Handled = true; break; } } base.OnKeyDown(e); } private ContextMenuStrip BuildMenu() { var m = new ContextMenuStrip { Font = new Font("Microsoft YaHei UI", 9F) }; m.Items.Add("编辑参数", null, (s, e) => { if (_selected != null) EditRequested?.Invoke(_selected); }); m.Items.Add("复制一份", null, (s, e) => DuplicateSelected()); m.Items.Add("上移", null, (s, e) => MoveSelected(-1)); m.Items.Add("下移", null, (s, e) => MoveSelected(1)); m.Items.Add(new ToolStripSeparator()); var def = _selected == null ? null : BlockCatalog.Find(_selected.Key); if (def is { Shape: BlockShape.If or BlockShape.IfExists }) m.Items.Add(_selected!.HasElse ? "移除「否则」分支" : "添加「否则」分支", null, (s, e) => ToggleElse()); m.Items.Add("删除", null, (s, e) => RemoveSelected()); return m; } // ===================== 命中 / 插入点 ===================== private Point ToLogical(Point client) => new(client.X - AutoScrollPosition.X, client.Y - AutoScrollPosition.Y); private Row? HitRow(Point p) { // 后画的在上面,倒序命中 for (int i = _rows.Count - 1; i >= 0; i--) if (_rows[i].Bounds.Contains(p)) return _rows[i]; return null; } private void UpdateDropIndicator(Point p) { var hit = ComputeInsert(p); _dropTarget = (hit.List, hit.Index); _dropY = hit.Y; _dropX = hit.X; Invalidate(); } private (List List, int Index, int Y, int X) ComputeInsert(Point p) { if (_rows.Count == 0) return (Blocks, 0, PadTop, PadX); Row? hit = null; foreach (var r in _rows) if (p.Y >= r.Bounds.Top && p.Y <= r.Bounds.Bottom) { hit = r; break; } if (hit == null) { for (int i = _rows.Count - 1; i >= 0; i--) if (p.Y > _rows[i].Bounds.Bottom) { hit = _rows[i]; break; } } if (hit == null) return (Blocks, 0, PadTop, PadX); int x = hit.Bounds.Left; bool upper = p.Y < hit.Bounds.Top + hit.Bounds.Height / 2; switch (hit.Kind) { case RowKind.Slot: return (hit.Owner, hit.Index, hit.Bounds.Top + 2, x + WallW); case RowKind.Else: return upper ? (hit.Block.Children, hit.Block.Children.Count, hit.Bounds.Top + 1, x) : (hit.Owner, hit.Index, hit.Bounds.Bottom + 1, x); case RowKind.End: return (hit.Owner, hit.Index, hit.Bounds.Top + 1, hit.Bounds.Left + WallW); default: if (upper) return (hit.Owner, hit.Index, hit.Bounds.Top + 1, hit.Bounds.Left); var def = BlockCatalog.Find(hit.Block.Key); if (def?.IsContainer == true) return (hit.Block.Children, 0, hit.Bounds.Bottom + 2, hit.Bounds.Left + WallW); return (hit.Owner, hit.Index + 1, hit.Bounds.Bottom + 1, hit.Bounds.Left); } } private void InsertAtDropTarget(LuaBlock block) { if (_dropTarget is { } t) { int idx = Math.Clamp(t.Index, 0, t.List.Count); t.List.Insert(idx, block); } else Blocks.Add(block); Changed?.Invoke(); } // ===================== 绘制 ===================== protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; g.TranslateTransform(AutoScrollPosition.X, AutoScrollPosition.Y); _rows.Clear(); int y = PadTop; int w = Math.Max(200, ClientSize.Width - PadX - 20); DrawList(g, Blocks, 0, ref y, PadX, w); if (Blocks.Count == 0) { using var f = new Font("Microsoft YaHei UI", 10F); using var b = new SolidBrush(Color.FromArgb(150, 160, 178)); g.DrawString("从左侧命令库双击或拖入积木,开始搭建流程", f, b, PadX + 8, PadTop + 6); } if (_dropTarget != null) { using var pen = new Pen(Color.FromArgb(37, 99, 235), 2); int ww = Math.Max(140, ClientSize.Width - _dropX - 30); g.DrawLine(pen, _dropX, _dropY, _dropX + ww, _dropY); g.FillEllipse(Brushes.DodgerBlue, _dropX - 3, _dropY - 3, 6, 6); } int totalH = y + NotchH + 30; if (AutoScrollMinSize.Height != totalH) AutoScrollMinSize = new Size(0, totalH); } private static int MeasureList(List list) { int h = 0; foreach (var b in list) { var def = BlockCatalog.Find(b.Key); if (def == null) continue; h += RowH; if (def.IsContainer) h += InnerHeight(b, def) + FootH; } return h; } private static int InnerHeight(LuaBlock b, BlockDef def) { int h = MeasureList(b.Children); if (def.Shape is BlockShape.If or BlockShape.IfExists && b.HasElse) h += ElseH + MeasureList(b.ElseChildren); return Math.Max(MinInner, h); } private void DrawList(Graphics g, List list, int depth, ref int y, int x, int w) { for (int i = 0; i < list.Count; i++) { var b = list[i]; var def = BlockCatalog.Find(b.Key); if (def == null) continue; bool sel = ReferenceEquals(b, _selected); bool dim = _dragging && ReferenceEquals(b, _dragBlock); var color = CategoryColor(def.Category); if (!def.IsContainer) { var rect = new Rectangle(x, y, w, RowH); using (var path = PuzzlePath(x, y, w, RowH, NotchX)) { FillBlock(g, path, rect, color, sel, dim); } DrawLabel(g, b, def, x + 12, y + 7, w - 24, color); _rows.Add(new Row { Block = b, Depth = depth, Bounds = rect, Owner = list, Index = i, Kind = RowKind.Block }); y += RowH; continue; } // ---------- 容器块(C 形插槽)---------- int innerH = InnerHeight(b, def); int headTop = y; int innerTop = headTop + RowH; int footTop = innerTop + innerH; int footBottom = footTop + FootH; using (var shell = ContainerPath(x, headTop, w, RowH, innerH, FootH, NotchX)) { FillBlock(g, shell, new Rectangle(x, headTop, w, footBottom - headTop), color, sel, dim); } // 内腔插槽底 using (var innerBg = new SolidBrush(Color.FromArgb(233, 238, 246))) g.FillRectangle(innerBg, x + WallW, innerTop, w - WallW, innerH); using (var lipPen = new Pen(Darken(color, 0.18), 1f)) g.DrawRectangle(lipPen, x + WallW, innerTop, w - WallW - 1, innerH - 1); DrawLabel(g, b, def, x + 12, headTop + 7, w - 24, color); _rows.Add(new Row { Block = b, Depth = depth, Bounds = new Rectangle(x, headTop, w, RowH), Owner = list, Index = i, Kind = RowKind.Block }); int cy = innerTop; int iw = w - WallW; if (b.Children.Count == 0) { _rows.Add(new Row { Block = b, Depth = depth + 1, Bounds = new Rectangle(x + WallW, cy, iw, Math.Max(20, innerH - NotchH)), Owner = b.Children, Index = 0, Kind = RowKind.Slot }); DrawSlotHint(g, x + WallW + 8, cy + 6); } else { DrawList(g, b.Children, depth + 1, ref cy, x + WallW, iw); } var tailList = b.Children; if (def.Shape is BlockShape.If or BlockShape.IfExists && b.HasElse) { int elseY = innerTop + NotchH + Math.Max(0, MeasureList(b.Children)); using var eb = new SolidBrush(Darken(color, 0.18)); g.FillRectangle(eb, x, elseY, w, ElseH); using var ef = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold); using var et = new SolidBrush(Color.FromArgb(240, 245, 252)); g.DrawString("否则", ef, et, x + WallW + 4, elseY + 3); _rows.Add(new Row { Block = b, Depth = depth + 1, Bounds = new Rectangle(x + WallW, elseY, iw, ElseH), Owner = b.ElseChildren, Index = 0, Kind = RowKind.Else }); int ey = elseY + ElseH; if (b.ElseChildren.Count == 0) { _rows.Add(new Row { Block = b, Depth = depth + 1, Bounds = new Rectangle(x + WallW, ey, iw, Math.Max(18, footTop - ey)), Owner = b.ElseChildren, Index = 0, Kind = RowKind.Slot }); DrawSlotHint(g, x + WallW + 8, ey + 4); } else { DrawList(g, b.ElseChildren, depth + 1, ref ey, x + WallW, iw); } tailList = b.ElseChildren; } _rows.Add(new Row { Block = b, Depth = depth, Bounds = new Rectangle(x, footTop, w, FootH), Owner = tailList, Index = tailList.Count, Kind = RowKind.End }); y = footBottom; } } private void FillBlock(Graphics g, GraphicsPath path, Rectangle r, Color color, bool sel, bool dim) { using (var fill = new LinearGradientBrush( new Point(r.Left, r.Top), new Point(r.Left, r.Bottom), Lighten(color, dim ? 0.42 : 0.16), dim ? Lighten(color, 0.42) : color)) { g.FillPath(fill, path); } using var edge = new Pen(Darken(color, 0.32), 1f); g.DrawPath(edge, path); if (sel) { using var glow = new Pen(Color.FromArgb(250, 204, 21), 2.5f); g.DrawPath(glow, path); } } private static void DrawSlotHint(Graphics g, int x, int y) { using var f = new Font("Microsoft YaHei UI", 8F); using var b = new SolidBrush(Color.FromArgb(158, 170, 190)); g.DrawString("把积木拖到这里", f, b, x, y); } // ---------- 标题(参数画成白色插槽)---------- private void DrawLabel(Graphics g, LuaBlock b, BlockDef def, int x, int y, int maxW, Color color) { float cx = x; using var white = new SolidBrush(Color.White); using var slotBg = new SolidBrush(Color.FromArgb(246, 248, 252)); using var slotFg = new SolidBrush(Color.FromArgb(48, 58, 74)); using var fmtC = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; foreach (var (text, isArg) in SplitTitle(def, b)) { if (text.Length == 0) continue; if (cx > x + maxW) break; if (!isArg) { g.DrawString(text, TitleFont, white, cx, y); cx += g.MeasureString(text, TitleFont).Width; } else { var sz = g.MeasureString(text, TitleFont); float wpx = Math.Min(sz.Width + 14, Math.Max(48, maxW / 3)); var rect = new RectangleF(cx, y - 2f, wpx, 20); using var path = Rounded(Rectangle.Round(rect), 5); g.FillPath(slotBg, path); using var pen = new Pen(Darken(color, 0.30), 1f); g.DrawPath(pen, path); var clip = new RectangleF(rect.Left + 2, rect.Top, rect.Width - 4, rect.Height); g.DrawString(text, TitleFont, slotFg, clip, fmtC); cx += wpx + 5; } } } private static List<(string Text, bool IsArg)> SplitTitle(BlockDef def, LuaBlock b) { var segs = new List<(string, bool)>(); if (def.Args.Length == 0) { segs.Add((def.Title, false)); return segs; } int pos = 0; for (int i = 0; i < def.Args.Length; i++) { string token = "{" + i + "}"; int idx = def.Title.IndexOf(token, StringComparison.Ordinal); if (idx < 0 || idx < pos) continue; if (idx > pos) segs.Add((def.Title[pos..idx], false)); segs.Add((b.Value(i), true)); pos = idx + token.Length; } if (pos < def.Title.Length) segs.Add((def.Title[pos..], false)); return segs; } // ---------- 路径:插片耦合造型 ---------- private static void TopEdge(GraphicsPath p, int x, int y, int w, int r, int nx) { p.AddLine(x + r, y, x + nx, y); p.AddLine(x + nx, y, x + nx + NotchSlope, y + NotchH); p.AddLine(x + nx + NotchSlope, y + NotchH, x + nx + NotchSlope + NotchFlat, y + NotchH); p.AddLine(x + nx + NotchSlope + NotchFlat, y + NotchH, x + nx + 2 * NotchSlope + NotchFlat, y); p.AddLine(x + nx + 2 * NotchSlope + NotchFlat, y, x + w - r, y); p.AddArc(x + w - 2 * r, y, 2 * r, 2 * r, 270, 90); } private static void BottomEdge(GraphicsPath p, int x, int bottom, int w, int r, int nx) { int nEnd = x + nx + 2 * NotchSlope + NotchFlat; p.AddLine(x + w - r, bottom, nEnd, bottom); p.AddLine(nEnd, bottom, nEnd - NotchSlope, bottom + NotchH); p.AddLine(nEnd - NotchSlope, bottom + NotchH, nEnd - NotchSlope - NotchFlat, bottom + NotchH); p.AddLine(nEnd - NotchSlope - NotchFlat, bottom + NotchH, x + nx, bottom); p.AddLine(x + nx, bottom, x + r, bottom); p.AddArc(x, bottom - 2 * r, 2 * r, 2 * r, 90, 90); } /// 单块:顶部凹槽 + 底部凸榫。 private static GraphicsPath PuzzlePath(int x, int y, int w, int h, int nx) { var p = new GraphicsPath(); int d = Radius * 2; int bottom = y + h; p.StartFigure(); p.AddArc(x, y, d, d, 180, 90); // 左上 TopEdge(p, x, y, w, Radius, nx); // 顶边(凹槽)+ 右上 p.AddLine(x + w, y + Radius, x + w, bottom - Radius); // 右 p.AddArc(x + w - d, bottom - d, d, d, 0, 90); // 右下 BottomEdge(p, x, bottom, w, Radius, nx); // 底边(凸榫)+ 左下 p.CloseFigure(); return p; } /// 容器块:C 形,中间挖出插槽。 private static GraphicsPath ContainerPath(int x, int y, int w, int headH, int innerH, int footH, int nx) { var p = new GraphicsPath(); int d = Radius * 2; int innerTop = y + headH; int footTop = innerTop + innerH; int footBottom = footTop + footH; p.StartFigure(); p.AddArc(x, y, d, d, 180, 90); // 左上 TopEdge(p, x, y, w, Radius, nx); // 顶边(凹槽)+ 右上 p.AddLine(x + w, y + Radius, x + w, footBottom - Radius); // 右外墙 p.AddArc(x + w - d, footBottom - d, d, d, 0, 90); // 右下 BottomEdge(p, x, footBottom, w, Radius, nx); // 底边(凸榫)+ 左下 p.AddLine(x, footBottom - Radius, x, footTop + Radius); // 左外墙上返 p.AddArc(x, footTop, d, d, 90, 90); // 封口左上内角 p.AddLine(x + Radius, footTop, x + WallW, footTop); // 插槽底边 p.AddLine(x + WallW, footTop, x + WallW, innerTop); // 左壁内侧 p.AddLine(x + WallW, innerTop, x + Radius, innerTop); // 插槽顶边 p.AddArc(x, innerTop - d, d, d, 270, 90); // 插槽左上内角 p.CloseFigure(); return p; } private static GraphicsPath Rounded(Rectangle r, int radius) { var p = new GraphicsPath(); int d = radius * 2; p.AddArc(r.Left, r.Top, d, d, 180, 90); p.AddArc(r.Right - d, r.Top, d, d, 270, 90); p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90); p.AddArc(r.Left, r.Bottom - d, d, d, 90, 90); p.CloseFigure(); return p; } // ---------- 颜色 ---------- private static Color CategoryColor(string category) => category switch { "流程" => Color.FromArgb(59, 130, 246), "鼠标" => Color.FromArgb(16, 175, 122), "键盘" => Color.FromArgb(214, 140, 18), "视觉" => Color.FromArgb(139, 92, 246), "系统" => Color.FromArgb(226, 104, 72), "浏览器" => Color.FromArgb(14, 160, 226), _ => Color.FromArgb(100, 116, 139) }; private static Color Lighten(Color c, double f) => Color.FromArgb( (int)Math.Min(255, c.R + 255 * f), (int)Math.Min(255, c.G + 255 * f), (int)Math.Min(255, c.B + 255 * f)); private static Color Darken(Color c, double f) => Color.FromArgb( (int)(c.R * (1 - f)), (int)(c.G * (1 - f)), (int)(c.B * (1 - f))); } }