diff --git a/Snake.Avalonia/Views/SnakeRenderer.cs b/Snake.Avalonia/Views/SnakeRenderer.cs index 1a09eb4..92d7cd6 100644 --- a/Snake.Avalonia/Views/SnakeRenderer.cs +++ b/Snake.Avalonia/Views/SnakeRenderer.cs @@ -133,53 +133,37 @@ public sealed class SnakeRenderer : Control var t = InterpolationT; var segmentsList = Segments; - var prevList = PreviousSegments; + var count = segmentsList.Count; - // Build path for body (all segments except head) - var bodyGeometry = new StreamGeometry(); - using (var ctx = bodyGeometry.Open()) + // === Body path: from segment[Count-2] down to segment[0] === + // Build with grid-snapped middle segments → sharp corners. + // The tail (segment[Count-1]) is excluded and drawn separately below. + if (count >= 2) { - for (var i = segmentsList.Count - 1; i >= 1; i--) + var bodyGeometry = new StreamGeometry(); + using (var ctx = bodyGeometry.Open()) { - var current = segmentsList[i]; - var (vx, vy) = GetVisualPosition(current, i, t); + // Start at second-to-last segment (grid-snapped for count>2, + // head-interpolated for count==2 — harmless since it's just the head) + var (startX, startY) = GetVisualPosition(segmentsList[count - 2], count - 2, t); + ctx.BeginFigure(new Point(startX, startY), false); - if (i == segmentsList.Count - 1 && segmentsList.Count > 2) + // Draw through middle segments down to head + for (var i = count - 3; i >= 0; i--) { - ctx.BeginFigure(new Point(vx, vy), false); - } - else if (i == segmentsList.Count - 1) - { - ctx.BeginFigure(new Point(vx, vy), false); - } - - // Draw line segment - if (i > 1) - { - var (px, py) = GetVisualPosition(segmentsList[i - 1], i - 1, t); + var (px, py) = GetVisualPosition(segmentsList[i], i, t); ctx.LineTo(new Point(px, py)); } } - if (segmentsList.Count >= 2) - { - var (hx, hy) = GetVisualPosition(segmentsList[0], 0, t); - ctx.LineTo(new Point(hx, hy)); - } - } + // Body thickness + var maxThickness = CellSize * 0.7; + var bodyThickness = Math.Max(CellSize * 0.35, maxThickness * 0.85); + var bodyPen = new Pen(SnakeBodyBrush, bodyThickness, + lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round); + context.DrawGeometry(null, bodyPen, bodyGeometry); - // Calculate body thickness based on segment position (tail thinner) - var maxThickness = CellSize * 0.7; - var minThickness = CellSize * 0.35; - - // Draw body as a thick stroke - var bodyThickness = Math.Max(minThickness, maxThickness * 0.85); - var bodyPen = new Pen(SnakeBodyBrush, bodyThickness, lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round); - context.DrawGeometry(null, bodyPen, bodyGeometry); - - // Draw a slightly thinner inner stroke for gradient effect - if (segmentsList.Count >= 2) - { + // Inner highlight stroke var innerPen = new Pen( new SolidColorBrush(SnakeHeadBrush.Color, 0.4), bodyThickness * 0.5, @@ -188,6 +172,32 @@ public sealed class SnakeRenderer : Control context.DrawGeometry(null, innerPen, bodyGeometry); } + // === Tail: separate fading line from interpolated tail to body === + if (count >= 2) + { + var (tailX, tailY) = GetVisualPosition(segmentsList[count - 1], count - 1, t); + var (bodyEndX, bodyEndY) = GetVisualPosition(segmentsList[count - 2], count - 2, t); + + // Fading tail line — thinner and semi-transparent + var tailThickness = CellSize * 0.35; + var tailPen = new Pen( + new SolidColorBrush(SnakeTailBrush.Color, 0.5), + tailThickness, + lineCap: PenLineCap.Round, + lineJoin: PenLineJoin.Round); + context.DrawLine(tailPen, + new Point(tailX, tailY), + new Point(bodyEndX, bodyEndY)); + + // Soft fading ellipse at the interpolated tail tip + var fadeRadius = tailThickness * 0.8; + context.DrawEllipse( + new SolidColorBrush(SnakeTailBrush.Color, 0.3), + null, + new Point(tailX, tailY), + fadeRadius, fadeRadius); + } + // Draw head DrawHead(context, t); @@ -373,18 +383,31 @@ public sealed class SnakeRenderer : Control private (double X, double Y) GetVisualPosition(Position pos, int segmentIndex, double t) { - // Only interpolate the head — all other segments stay grid-snapped - // to preserve sharp corners when the snake bends. - // Interpolating the tail creates diagonal shortcuts at corners because - // the tail position drifts while the adjacent segment is already at its - // new grid position. - if (segmentIndex != 0) - return (pos.X * CellSize + CellSize * 0.5, pos.Y * CellSize + CellSize * 0.5); + // Head (index 0): interpolated with easing for smooth movement + if (segmentIndex == 0) + { + var from = PreviousSegments != null && PreviousSegments.Count > 0 + ? AnimationHelper.GetPreviousPosition(0, Segments!, PreviousSegments, SnakeDirection) + : pos; - var from = PreviousSegments != null && PreviousSegments.Count > 0 - ? AnimationHelper.GetPreviousPosition(0, Segments!, PreviousSegments, SnakeDirection) - : pos; + return AnimationHelper.InterpolatePosition(from, pos, t, CellSize, useEasing: true); + } - return AnimationHelper.InterpolatePosition(from, pos, t, CellSize, useEasing: true); + // Tail (last segment, only when there are multiple segments): interpolated LINEARLY + // This is used ONLY in the separate tail-drawing code, not in the main body path, + // so it does NOT create diagonal shortcuts at corners. + // The tail line connects from this interpolated position to the grid-snapped + // second-to-last segment. + if (Segments != null && Segments.Count > 1 && segmentIndex == Segments.Count - 1) + { + var from = PreviousSegments != null && PreviousSegments.Count > 0 + ? AnimationHelper.GetPreviousPosition(segmentIndex, Segments!, PreviousSegments, SnakeDirection) + : pos; + + return AnimationHelper.InterpolatePosition(from, pos, t, CellSize, useEasing: false); + } + + // Middle segments: grid-snapped for sharp corners + return (pos.X * CellSize + CellSize * 0.5, pos.Y * CellSize + CellSize * 0.5); } }