feat: tail interpolates like head (mirrored with ease-in)

Tail now uses the same approach as head: interpolated in the body path
from its previous position, with ease-in (mirror of head's ease-out).
Ghost circle removed — no longer needed since tail is naturally smooth.
Added EaseInCubic to AnimationHelper. InterpolatePosition now uses
string easing mode instead of bool.
This commit is contained in:
Heller
2026-06-19 10:25:31 +00:00
parent 36bfe5ad45
commit c1b9543c47
2 changed files with 32 additions and 35 deletions

View File

@@ -26,7 +26,16 @@ public static class AnimationHelper
} }
/// <summary> /// <summary>
/// Interpolates between two positions with given t value. /// Ease-in cubic — mirror of EaseOutCubic, for tail interpolation.
/// </summary>
public static double EaseInCubic(double t)
{
t = Math.Clamp(t, 0.0, 1.0);
return t * t * t;
}
/// <summary>
/// Interpolates between two positions with given t value and easing mode.
/// Returns the visual position for rendering. /// Returns the visual position for rendering.
/// </summary> /// </summary>
public static (double X, double Y) InterpolatePosition( public static (double X, double Y) InterpolatePosition(
@@ -34,9 +43,14 @@ public static class AnimationHelper
Position to, Position to,
double t, double t,
double cellSize, double cellSize,
bool useEasing = false) string easing = "linear")
{ {
var eased = useEasing ? EaseOutCubic(t) : t; var eased = easing switch
{
"easeOut" => EaseOutCubic(t),
"easeIn" => EaseInCubic(t),
_ => t
};
var x = from.X + (to.X - from.X) * eased; var x = from.X + (to.X - from.X) * eased;
var y = from.Y + (to.Y - from.Y) * eased; var y = from.Y + (to.Y - from.Y) * eased;
return (x * cellSize + cellSize * 0.5, y * cellSize + cellSize * 0.5); return (x * cellSize + cellSize * 0.5, y * cellSize + cellSize * 0.5);

View File

@@ -172,28 +172,6 @@ public sealed class SnakeRenderer : Control
context.DrawGeometry(null, innerPen, bodyGeometry); context.DrawGeometry(null, innerPen, bodyGeometry);
} }
// === 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 opacity = 0.4 * (1.0 - t);
if (opacity > 0)
{
var oldTailPos = PreviousSegments[PreviousSegments.Count - 1];
var currentTailPos = segmentsList[count - 1];
var (ghostX, ghostY) = AnimationHelper.InterpolatePosition(
oldTailPos, currentTailPos, t, CellSize, useEasing: false);
context.DrawEllipse(
new SolidColorBrush(SnakeBodyBrush.Color, opacity),
null,
new Point(ghostX, ghostY),
CellSize * 0.35, CellSize * 0.35);
}
}
// Draw head // Draw head
DrawHead(context, t); DrawHead(context, t);
@@ -379,17 +357,22 @@ public sealed class SnakeRenderer : Control
private (double X, double Y) GetVisualPosition(Position pos, int segmentIndex, double t) private (double X, double Y) GetVisualPosition(Position pos, int segmentIndex, double t)
{ {
// Head (index 0): interpolated with easing for smooth movement if (Segments == null)
if (segmentIndex == 0) return (pos.X * CellSize + CellSize * 0.5, pos.Y * CellSize + CellSize * 0.5);
{
var isHead = segmentIndex == 0;
var isTail = Segments.Count > 1 && segmentIndex == Segments.Count - 1;
// Head and tail interpolate smoothly; middle segments stay grid-snapped
if (!isHead && !isTail)
return (pos.X * CellSize + CellSize * 0.5, pos.Y * CellSize + CellSize * 0.5);
var from = PreviousSegments != null && PreviousSegments.Count > 0 var from = PreviousSegments != null && PreviousSegments.Count > 0
? AnimationHelper.GetPreviousPosition(0, Segments!, PreviousSegments, SnakeDirection) ? AnimationHelper.GetPreviousPosition(segmentIndex, Segments, PreviousSegments, SnakeDirection)
: pos; : pos;
return AnimationHelper.InterpolatePosition(from, pos, t, CellSize, useEasing: true); // Head: ease-out forward. Tail: ease-in backward (mirrored).
} var easingMode = isHead ? "easeOut" : "easeIn";
return AnimationHelper.InterpolatePosition(from, pos, t, CellSize, easing: easingMode);
// All other segments (body and tail): grid-snapped for sharp corners
return (pos.X * CellSize + CellSize * 0.5, pos.Y * CellSize + CellSize * 0.5);
} }
} }