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.
This commit is contained in:
@@ -135,21 +135,21 @@ public sealed class SnakeRenderer : Control
|
|||||||
var segmentsList = Segments;
|
var segmentsList = Segments;
|
||||||
var count = segmentsList.Count;
|
var count = segmentsList.Count;
|
||||||
|
|
||||||
// === Body path: from segment[Count-2] down to segment[0] ===
|
// === Body path: from segment[Count-1] (tail) through segment[0] (head) ===
|
||||||
// Build with grid-snapped middle segments → sharp corners.
|
// All segments use GetVisualPosition — middle/body returns grid-snapped,
|
||||||
// The tail (segment[Count-1]) is excluded and drawn separately below.
|
// head returns interpolated. Tail is grid-snapped, giving sharp corners
|
||||||
|
// with full body thickness.
|
||||||
if (count >= 2)
|
if (count >= 2)
|
||||||
{
|
{
|
||||||
var bodyGeometry = new StreamGeometry();
|
var bodyGeometry = new StreamGeometry();
|
||||||
using (var ctx = bodyGeometry.Open())
|
using (var ctx = bodyGeometry.Open())
|
||||||
{
|
{
|
||||||
// Start at second-to-last segment (grid-snapped for count>2,
|
// Start at tail (last segment, grid-snapped)
|
||||||
// head-interpolated for count==2 — harmless since it's just the head)
|
var (startX, startY) = GetVisualPosition(segmentsList[count - 1], count - 1, t);
|
||||||
var (startX, startY) = GetVisualPosition(segmentsList[count - 2], count - 2, t);
|
|
||||||
ctx.BeginFigure(new Point(startX, startY), false);
|
ctx.BeginFigure(new Point(startX, startY), false);
|
||||||
|
|
||||||
// Draw through middle segments down to head
|
// 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);
|
var (px, py) = GetVisualPosition(segmentsList[i], i, t);
|
||||||
ctx.LineTo(new Point(px, py));
|
ctx.LineTo(new Point(px, py));
|
||||||
@@ -172,30 +172,26 @@ public sealed class SnakeRenderer : Control
|
|||||||
context.DrawGeometry(null, innerPen, bodyGeometry);
|
context.DrawGeometry(null, innerPen, bodyGeometry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Tail: separate fading line from interpolated tail to body ===
|
// === Fading ghost circle at old tail position ===
|
||||||
if (count >= 2)
|
// 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 opacity = 0.4 * (1.0 - t);
|
||||||
var (bodyEndX, bodyEndY) = GetVisualPosition(segmentsList[count - 2], count - 2, t);
|
if (opacity > 0)
|
||||||
|
{
|
||||||
|
var oldTailPos = PreviousSegments[PreviousSegments.Count - 1];
|
||||||
|
var currentTailPos = segmentsList[count - 1];
|
||||||
|
|
||||||
// Fading tail line — thinner and semi-transparent
|
var (ghostX, ghostY) = AnimationHelper.InterpolatePosition(
|
||||||
var tailThickness = CellSize * 0.35;
|
oldTailPos, currentTailPos, t, CellSize, useEasing: false);
|
||||||
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(
|
context.DrawEllipse(
|
||||||
new SolidColorBrush(SnakeTailBrush.Color, 0.3),
|
new SolidColorBrush(SnakeBodyBrush.Color, opacity),
|
||||||
null,
|
null,
|
||||||
new Point(tailX, tailY),
|
new Point(ghostX, ghostY),
|
||||||
fadeRadius, fadeRadius);
|
CellSize * 0.35, CellSize * 0.35);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw head
|
// Draw head
|
||||||
@@ -393,21 +389,7 @@ public sealed class SnakeRenderer : Control
|
|||||||
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
|
// All other segments (body and tail): grid-snapped for sharp corners
|
||||||
// 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);
|
return (pos.X * CellSize + CellSize * 0.5, pos.Y * CellSize + CellSize * 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user