From 7d55a08380f07cda8eddf1b8e3348eb1d20f0578 Mon Sep 17 00:00:00 2001 From: Heller Date: Fri, 19 Jun 2026 10:02:07 +0000 Subject: [PATCH] feat: smooth snake animation + beautiful Avalonia UI overhaul - Two-timer architecture: game timer + 60fps render timer for smooth interpolation - Snake body: StreamGeometry path with teal gradient, rounded joins - Directional head with white eyes and dark pupils - Food: pulsating glow, highlight, green leaf animation - Modern dark theme (#0D1117), glassmorphism HUD - Speed indicator bar, score +N popup - High score persistence to JSON - All keyboard shortcuts: Arrows, WASD, Space/P pause, Enter start, R restart, Esc quit - Window resizable, 640x540 default New files: AnimationHelper.cs, HighScoreManager.cs, SnakeRenderer.cs --- Snake.Avalonia/App.axaml | 34 +- Snake.Avalonia/MainWindow.axaml | 13 +- Snake.Avalonia/MainWindow.axaml.cs | 13 +- Snake.Avalonia/Views/AnimationHelper.cs | 105 ++++++ Snake.Avalonia/Views/GameView.axaml | 187 +++++++++-- Snake.Avalonia/Views/GameView.axaml.cs | 362 ++++++++++++++------- Snake.Avalonia/Views/HighScoreManager.cs | 58 ++++ Snake.Avalonia/Views/SnakeRenderer.cs | 386 +++++++++++++++++++++++ Snake.Core/Snake.cs | 19 +- 9 files changed, 1027 insertions(+), 150 deletions(-) create mode 100644 Snake.Avalonia/Views/AnimationHelper.cs create mode 100644 Snake.Avalonia/Views/HighScoreManager.cs create mode 100644 Snake.Avalonia/Views/SnakeRenderer.cs diff --git a/Snake.Avalonia/App.axaml b/Snake.Avalonia/App.axaml index 2e46121..bda64e1 100644 --- a/Snake.Avalonia/App.axaml +++ b/Snake.Avalonia/App.axaml @@ -1,10 +1,38 @@ - + RequestedThemeVariant="Dark"> + + + + + + + + + + + - \ No newline at end of file + diff --git a/Snake.Avalonia/MainWindow.axaml b/Snake.Avalonia/MainWindow.axaml index 7571fae..77d2c5d 100644 --- a/Snake.Avalonia/MainWindow.axaml +++ b/Snake.Avalonia/MainWindow.axaml @@ -3,11 +3,14 @@ xmlns:views="using:Snake.Avalonia.Views" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="420" + mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="540" x:Class="Snake.Avalonia.MainWindow" - Title="Snake" - Width="480" - Height="420" - CanResize="False"> + Title="๐Ÿ Snake" + MinWidth="540" + MinHeight="460" + Width="640" + Height="540" + Background="#0D1117" + ExtendClientAreaToDecorationsHint="False"> diff --git a/Snake.Avalonia/MainWindow.axaml.cs b/Snake.Avalonia/MainWindow.axaml.cs index e6b141b..ae99fac 100644 --- a/Snake.Avalonia/MainWindow.axaml.cs +++ b/Snake.Avalonia/MainWindow.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Input; namespace Snake.Avalonia; @@ -7,5 +8,15 @@ public partial class MainWindow : Window public MainWindow() { InitializeComponent(); + + // Global Escape handler to close the window + KeyDown += (_, e) => + { + if (e.Key == Key.Escape) + { + Close(); + e.Handled = true; + } + }; } -} \ No newline at end of file +} diff --git a/Snake.Avalonia/Views/AnimationHelper.cs b/Snake.Avalonia/Views/AnimationHelper.cs new file mode 100644 index 0000000..55426a1 --- /dev/null +++ b/Snake.Avalonia/Views/AnimationHelper.cs @@ -0,0 +1,105 @@ +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; + } + + /// + /// Interpolates between two positions with given t value. + /// Returns the visual position for rendering. + /// + public static (double X, double Y) InterpolatePosition( + Position from, + Position to, + double t, + double cellSize, + bool useEasing = false) + { + var eased = useEasing ? EaseOutCubic(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, etc. + // current[i] (i>=1) was at old[i-1] + var prevIdx = 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; + } +} diff --git a/Snake.Avalonia/Views/GameView.axaml b/Snake.Avalonia/Views/GameView.axaml index bc68e28..88fe6e4 100644 --- a/Snake.Avalonia/Views/GameView.axaml +++ b/Snake.Avalonia/Views/GameView.axaml @@ -1,30 +1,175 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - -