From 36bfe5ad45b97234b546f16ed523f7b69491f348 Mon Sep 17 00:00:00 2001 From: Heller Date: Fri, 19 Jun 2026 10:22:33 +0000 Subject: [PATCH] fix: tail as full body segment + fading ghost for smooth illusion Tail is now part of the main body path (grid-snapped, full thickness, sharp corners). Smooth movement illusion via fading ghost circle at the old tail position that fades out during the tick. --- Snake.Avalonia/Views/SnakeRenderer.cs | 68 ++++++++++----------------- 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/Snake.Avalonia/Views/SnakeRenderer.cs b/Snake.Avalonia/Views/SnakeRenderer.cs index 92d7cd6..06af464 100644 --- a/Snake.Avalonia/Views/SnakeRenderer.cs +++ b/Snake.Avalonia/Views/SnakeRenderer.cs @@ -135,21 +135,21 @@ public sealed class SnakeRenderer : Control var segmentsList = Segments; var count = segmentsList.Count; - // === 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. + // === Body path: from segment[Count-1] (tail) through segment[0] (head) === + // All segments use GetVisualPosition — middle/body returns grid-snapped, + // head returns interpolated. Tail is grid-snapped, giving sharp corners + // with full body thickness. if (count >= 2) { var bodyGeometry = new StreamGeometry(); using (var ctx = bodyGeometry.Open()) { - // 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); + // Start at tail (last segment, grid-snapped) + var (startX, startY) = GetVisualPosition(segmentsList[count - 1], count - 1, t); ctx.BeginFigure(new Point(startX, startY), false); // Draw through middle segments down to head - for (var i = count - 3; i >= 0; i--) + for (var i = count - 2; i >= 0; i--) { var (px, py) = GetVisualPosition(segmentsList[i], i, t); ctx.LineTo(new Point(px, py)); @@ -172,30 +172,26 @@ public sealed class SnakeRenderer : Control context.DrawGeometry(null, innerPen, bodyGeometry); } - // === Tail: separate fading line from interpolated tail to body === - if (count >= 2) + // === Fading ghost circle at old tail position === + // Creates the illusion of smooth tail movement without affecting + // the body path geometry (which stays grid-snapped with sharp corners). + if (count >= 2 && PreviousSegments != null && PreviousSegments.Count > 0) { - var (tailX, tailY) = GetVisualPosition(segmentsList[count - 1], count - 1, t); - var (bodyEndX, bodyEndY) = GetVisualPosition(segmentsList[count - 2], count - 2, t); + var opacity = 0.4 * (1.0 - t); + if (opacity > 0) + { + var oldTailPos = PreviousSegments[PreviousSegments.Count - 1]; + var currentTailPos = segmentsList[count - 1]; - // 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)); + var (ghostX, ghostY) = AnimationHelper.InterpolatePosition( + oldTailPos, currentTailPos, t, CellSize, useEasing: false); - // 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); + context.DrawEllipse( + new SolidColorBrush(SnakeBodyBrush.Color, opacity), + null, + new Point(ghostX, ghostY), + CellSize * 0.35, CellSize * 0.35); + } } // Draw head @@ -393,21 +389,7 @@ public sealed class SnakeRenderer : Control 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 + // All other segments (body and tail): grid-snapped for sharp corners return (pos.X * CellSize + CellSize * 0.5, pos.Y * CellSize + CellSize * 0.5); } }