8 Commits

Author SHA1 Message Date
Heller
90fd0f592f fix: virtual grid-snapped tail anchor prevents corner diagonals
Body path now goes: interp_tail → grid_tail → segment[N-2] → ... → head.
The short leg from interpolated to grid-snapped tail is always straight
(same direction), and from grid_tail onward the path strictly follows
the grid — preserving sharp corners while tail stays smooth.
2026-06-19 10:27:42 +00:00
Heller
c1b9543c47 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.
2026-06-19 10:25:31 +00:00
Heller
36bfe5ad45 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.
2026-06-19 10:22:33 +00:00
Heller
d83234c8be 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.
2026-06-19 10:18:26 +00:00
Heller
4446bd778f fix: interpolate only head, pin all other segments to grid
Tail interpolation caused diagonal shortcuts at corners because
the interpolated tail position would drift while the adjacent
segment was already at its new grid position. By keeping only
the head interpolated, L-shaped bends render correctly.
2026-06-19 10:14:51 +00:00
Heller
cb06980cfb fix: preserve sharp corners during smooth animation 2026-06-19 10:12:32 +00:00
Heller
d483cd0660 fix: only interpolate head and tail, keep middle segments grid-snapped
Prevents diagonal shortcuts when snake bends — middle segments stay
at exact cell centers so L-shaped corners render correctly.
2026-06-19 10:12:32 +00:00
Heller
5da42c119e chore: gitignore publish directory 2026-06-19 10:09:12 +00:00
4 changed files with 53 additions and 41 deletions

1
.gitignore vendored
View File

@@ -64,3 +64,4 @@ Thumbs.db
## Avalonia ## Avalonia
*.DotSettings.user *.DotSettings.user
publish/

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

@@ -133,53 +133,42 @@ public sealed class SnakeRenderer : Control
var t = InterpolationT; var t = InterpolationT;
var segmentsList = Segments; var segmentsList = Segments;
var prevList = PreviousSegments; var count = segmentsList.Count;
// Build path for body (all segments except head) // === Body path: from interp tail → grid tail (virtual corner anchor) → body → head ===
// The virtual grid-snapped tail prevents diagonal shortcuts at corners:
// the short leg from interp_tail to grid_tail is always straight (same direction),
// and from grid_tail onward the path strictly follows the grid.
if (count >= 2)
{
var bodyGeometry = new StreamGeometry(); var bodyGeometry = new StreamGeometry();
using (var ctx = bodyGeometry.Open()) using (var ctx = bodyGeometry.Open())
{ {
for (var i = segmentsList.Count - 1; i >= 1; i--) // Start at interpolated tail
{ var (tailX, tailY) = GetVisualPosition(segmentsList[count - 1], count - 1, t);
var current = segmentsList[i]; ctx.BeginFigure(new Point(tailX, tailY), false);
var (vx, vy) = GetVisualPosition(current, i, t);
if (i == segmentsList.Count - 1 && segmentsList.Count > 2) // Virtual corner anchor: grid-snapped tail position
{ var lastPos = segmentsList[count - 1];
ctx.BeginFigure(new Point(vx, vy), false); var (gridTailX, gridTailY) = (lastPos.X * CellSize + CellSize * 0.5, lastPos.Y * CellSize + CellSize * 0.5);
} ctx.LineTo(new Point(gridTailX, gridTailY));
else if (i == segmentsList.Count - 1)
{
ctx.BeginFigure(new Point(vx, vy), false);
}
// Draw line segment // Draw through middle segments down to head
if (i > 1) for (var i = count - 2; i >= 0; i--)
{ {
var (px, py) = GetVisualPosition(segmentsList[i - 1], i - 1, t); var (px, py) = GetVisualPosition(segmentsList[i], i, t);
ctx.LineTo(new Point(px, py)); ctx.LineTo(new Point(px, py));
} }
} }
if (segmentsList.Count >= 2) // Body thickness
{
var (hx, hy) = GetVisualPosition(segmentsList[0], 0, t);
ctx.LineTo(new Point(hx, hy));
}
}
// Calculate body thickness based on segment position (tail thinner)
var maxThickness = CellSize * 0.7; var maxThickness = CellSize * 0.7;
var minThickness = CellSize * 0.35; var bodyThickness = Math.Max(CellSize * 0.35, maxThickness * 0.85);
var bodyPen = new Pen(SnakeBodyBrush, bodyThickness,
// Draw body as a thick stroke lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round);
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); context.DrawGeometry(null, bodyPen, bodyGeometry);
// Draw a slightly thinner inner stroke for gradient effect // Inner highlight stroke
if (segmentsList.Count >= 2)
{
var innerPen = new Pen( var innerPen = new Pen(
new SolidColorBrush(SnakeHeadBrush.Color, 0.4), new SolidColorBrush(SnakeHeadBrush.Color, 0.4),
bodyThickness * 0.5, bodyThickness * 0.5,
@@ -376,11 +365,19 @@ public sealed class SnakeRenderer : Control
if (Segments == null) if (Segments == null)
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);
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(segmentIndex, Segments, PreviousSegments, SnakeDirection) ? AnimationHelper.GetPreviousPosition(segmentIndex, Segments, PreviousSegments, SnakeDirection)
: pos; : pos;
var useEasing = segmentIndex == 0; // Head gets easing for snappy feel // Head: ease-out forward. Tail: ease-in backward (mirrored).
return AnimationHelper.InterpolatePosition(from, pos, t, CellSize, useEasing); var easingMode = isHead ? "easeOut" : "easeIn";
return AnimationHelper.InterpolatePosition(from, pos, t, CellSize, easing: easingMode);
} }
} }

Binary file not shown.