Compare commits
7 Commits
5da42c119e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90fd0f592f | ||
|
|
c1b9543c47 | ||
|
|
36bfe5ad45 | ||
|
|
d83234c8be | ||
|
|
4446bd778f | ||
|
|
cb06980cfb | ||
|
|
d483cd0660 |
@@ -26,7 +26,16 @@ public static class AnimationHelper
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public static (double X, double Y) InterpolatePosition(
|
||||
@@ -34,9 +43,14 @@ public static class AnimationHelper
|
||||
Position to,
|
||||
double t,
|
||||
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 y = from.Y + (to.Y - from.Y) * eased;
|
||||
return (x * cellSize + cellSize * 0.5, y * cellSize + cellSize * 0.5);
|
||||
|
||||
@@ -133,53 +133,42 @@ 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 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)
|
||||
{
|
||||
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 interpolated tail
|
||||
var (tailX, tailY) = GetVisualPosition(segmentsList[count - 1], count - 1, t);
|
||||
ctx.BeginFigure(new Point(tailX, tailY), false);
|
||||
|
||||
if (i == segmentsList.Count - 1 && segmentsList.Count > 2)
|
||||
{
|
||||
ctx.BeginFigure(new Point(vx, vy), false);
|
||||
}
|
||||
else if (i == segmentsList.Count - 1)
|
||||
{
|
||||
ctx.BeginFigure(new Point(vx, vy), false);
|
||||
}
|
||||
// Virtual corner anchor: grid-snapped tail position
|
||||
var lastPos = segmentsList[count - 1];
|
||||
var (gridTailX, gridTailY) = (lastPos.X * CellSize + CellSize * 0.5, lastPos.Y * CellSize + CellSize * 0.5);
|
||||
ctx.LineTo(new Point(gridTailX, gridTailY));
|
||||
|
||||
// Draw line segment
|
||||
if (i > 1)
|
||||
// Draw through middle segments down to head
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -376,11 +365,19 @@ public sealed class SnakeRenderer : Control
|
||||
if (Segments == null)
|
||||
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
|
||||
? AnimationHelper.GetPreviousPosition(segmentIndex, Segments, PreviousSegments, SnakeDirection)
|
||||
: pos;
|
||||
|
||||
var useEasing = segmentIndex == 0; // Head gets easing for snappy feel
|
||||
return AnimationHelper.InterpolatePosition(from, pos, t, CellSize, useEasing);
|
||||
// Head: ease-out forward. Tail: ease-in backward (mirrored).
|
||||
var easingMode = isHead ? "easeOut" : "easeIn";
|
||||
return AnimationHelper.InterpolatePosition(from, pos, t, CellSize, easing: easingMode);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user