fix: smooth tail via separate line segment, body path stays grid-snapped
Tail is now drawn as a separate DrawLine from interpolated tail position to the grid-snapped second-to-last segment. Body path (segment[N-2]→head) stays grid-snapped, preserving sharp corners while tail glides smoothly.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user