using Snake.Core; namespace Snake.Avalonia.Views; /// /// Interpolation helpers for smooth snake animation. /// public static class AnimationHelper { /// /// Ease-out cubic for smooth deceleration (head overshoot feel). /// public static double EaseOutCubic(double t) { t = Math.Clamp(t, 0.0, 1.0); return 1.0 - Math.Pow(1.0 - t, 3); } /// /// Ease-in-out quad for general smooth interpolation. /// public static double EaseInOutQuad(double t) { t = Math.Clamp(t, 0.0, 1.0); return t < 0.5 ? 2.0 * t * t : 1.0 - Math.Pow(-2.0 * t + 2.0, 2) / 2.0; } /// /// Ease-in cubic — mirror of EaseOutCubic, for tail interpolation. /// public static double EaseInCubic(double t) { t = Math.Clamp(t, 0.0, 1.0); return t * t * t; } /// /// Interpolates between two positions with given t value and easing mode. /// Returns the visual position for rendering. /// public static (double X, double Y) InterpolatePosition( Position from, Position to, double t, double cellSize, string easing = "linear") { 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); } /// /// Maps a current segment index to its previous position for interpolation. /// PreviousSegments is the full snapshot before Move(). /// Segments count may differ from PreviousSegments if the snake grew. /// public static Position GetPreviousPosition( int segmentIndex, IReadOnlyList currentSegments, IReadOnlyList previousSegments, Direction direction) { if (previousSegments.Count == 0) return currentSegments[segmentIndex]; // Head (index 0) always interpolates from the old head if (segmentIndex == 0) return previousSegments[0]; // For body segments, the mapping depends on whether snake grew var grew = currentSegments.Count > previousSegments.Count; if (grew) { // When growing: all old segments kept their positions, // new head was added at front. Body segments shifted by 1. // current[i] (i>=1) = old[i-1] var prevIdx = segmentIndex - 1; if (prevIdx < previousSegments.Count) return previousSegments[prevIdx]; } else { // When not growing: tail removed, new head added. // current[0] = new head, current[1] = old head, … current[N-2] = old[N-3] // BUT the last segment (tail): it was old[N-1] (the removed tail's position) // because visually it slides from where the old tail was. var isLast = segmentIndex == currentSegments.Count - 1; var prevIdx = isLast ? segmentIndex : segmentIndex - 1; if (prevIdx < previousSegments.Count) return previousSegments[prevIdx]; } // Fallback: return current position (no interpolation) return currentSegments[segmentIndex]; } /// /// Computes a pulsating radius for food glow effect. /// public static double PulsateRadius(double baseRadius, double elapsedSeconds, double amplitude = 0.15, double frequency = 2.5) { return baseRadius * (1.0 + amplitude * Math.Sin(elapsedSeconds * Math.PI * 2.0 * frequency)); } /// /// Computes opacity for score popup fade-out animation. /// public static double FadeOutOpacity(double elapsedSeconds, double durationSeconds = 1.0) { var t = Math.Clamp(elapsedSeconds / durationSeconds, 0.0, 1.0); return 1.0 - t; } }