[gnome-tetravex] Improve end-of-game UX.



commit ed243e1a96372e5fb8d436fcb270035277c9dcac
Author: Arnaud Bonatti <arnaud bonatti gmail com>
Date:   Sun Sep 22 20:21:18 2019 +0200

    Improve end-of-game UX.
    
    Final work on issue #3.
    
    Fixes #3, and fixes #6.

 po/POTFILES.in                   |   2 +
 po/POTFILES.skip                 |   1 +
 src/gnome-tetravex.gresource.xml |   5 +-
 src/gnome-tetravex.vala          |  85 ++++++++++++-------
 src/history.vala                 | 175 +++++++++++++++++++++++++++++++++------
 src/meson.build                  |   1 +
 src/puzzle-view.vala             |  61 ++++++++++++--
 src/score-dialog.vala            |  44 ++--------
 src/score-overlay-entry.ui       |  34 ++++++++
 src/score-overlay.ui             |  72 ++++++++++++++++
 src/score-overlay.vala           | 164 ++++++++++++++++++++++++++++++++++++
 src/tetravex.css                 |  43 ++++++++++
 src/theme-extrusion.vala         |  16 +++-
 src/theme-neoretro.vala          |  51 ++++++++----
 src/theme-nostalgia.vala         |  61 +++++++++-----
 src/theme-synesthesia.vala       |  57 +++++++++----
 16 files changed, 720 insertions(+), 152 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 6ba9966..e1204e8 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -10,6 +10,8 @@ src/help-overlay.ui
 src/puzzle.vala
 src/puzzle-view.vala
 src/score-dialog.vala
+src/score-overlay.vala
+src/score-overlay.ui
 src/theme-extrusion.vala
 src/theme-neoretro.vala
 src/theme-nostalgia.vala
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index 27438b3..1bc8973 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -2,6 +2,7 @@ src/gnome-tetravex.c
 src/puzzle.c
 src/puzzle-view.c
 src/score-dialog.c
+src/score-overlay.c
 src/theme-extrusion.c
 src/theme-neoretro.c
 src/theme-nostalgia.c
diff --git a/src/gnome-tetravex.gresource.xml b/src/gnome-tetravex.gresource.xml
index e405d53..4ee78ee 100644
--- a/src/gnome-tetravex.gresource.xml
+++ b/src/gnome-tetravex.gresource.xml
@@ -1,8 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gnome/Tetravex">
-    <file preprocess="xml-stripblanks">gnome-tetravex.ui</file>
     <file preprocess="xml-stripblanks">app-menu.ui</file>
+    <file preprocess="xml-stripblanks">gnome-tetravex.ui</file>
+    <file preprocess="xml-stripblanks">score-overlay.ui</file>
+    <file preprocess="xml-stripblanks">score-overlay-entry.ui</file>
+    <file>tetravex.css</file>
   </gresource>
   <gresource prefix="/org/gnome/Tetravex/gtk">
     <file preprocess="xml-stripblanks">help-overlay.ui</file>
diff --git a/src/gnome-tetravex.vala b/src/gnome-tetravex.vala
index 1252879..7d958f4 100644
--- a/src/gnome-tetravex.vala
+++ b/src/gnome-tetravex.vala
@@ -45,6 +45,8 @@ private class Tetravex : Gtk.Application
     private SimpleAction solve_action;
     private SimpleAction finish_action;
 
+    private ScoreOverlay score_overlay;
+
     private const OptionEntry [] option_entries =
     {
         /* Translators: command-line option description, see 'gnome-tetravex --help' */
@@ -125,6 +127,12 @@ private class Tetravex : Gtk.Application
 
         history = new History (Path.build_filename (Environment.get_user_data_dir (), "gnome-tetravex", 
"history"));
 
+        CssProvider css_provider = new CssProvider ();
+        css_provider.load_from_resource ("/org/gnome/Tetravex/tetravex.css");
+        Gdk.Screen? gdk_screen = Gdk.Screen.get_default ();
+        if (gdk_screen != null) // else..?
+            StyleContext.add_provider_for_screen ((!) gdk_screen, css_provider, 
STYLE_PROVIDER_PRIORITY_APPLICATION);
+
         window = (ApplicationWindow) builder.get_object ("gnome-tetravex-window");
         this.add_window (window);
         window.key_press_event.connect (on_key_press_event);
@@ -175,7 +183,22 @@ private class Tetravex : Gtk.Application
         view.vexpand = true;
         view.button_release_event.connect (view_button_release_event);
         settings.bind ("theme", view, "theme-id", SettingsBindFlags.GET | SettingsBindFlags.NO_SENSITIVITY);
-        grid.attach (view, 0, 0, 3, 1);
+
+        Overlay overlay = new Overlay ();
+        overlay.add (view);
+        overlay.show ();
+
+        score_overlay = new ScoreOverlay ();
+        overlay.add_overlay (score_overlay);
+        overlay.set_overlay_pass_through (score_overlay, true);
+
+        view.bind_property ("boardsize",        score_overlay,  "boardsize",        BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+        view.bind_property ("x-offset-right",   score_overlay,  "margin-left",      BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+        view.bind_property ("right-margin",     score_overlay,  "margin-right",     BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+        view.bind_property ("y-offset",         score_overlay,  "margin-top",       BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+        view.bind_property ("y-offset",         score_overlay,  "margin-bottom",    BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+
+        grid.attach (overlay, 0, 0, 3, 1);
 
         settings.bind ("mouse-use-extra-buttons",   view,
                        "mouse-use-extra-buttons",   SettingsBindFlags.GET | 
SettingsBindFlags.NO_SENSITIVITY);
@@ -356,11 +379,13 @@ private class Tetravex : Gtk.Application
 
     private void new_game ()
     {
+        puzzle_is_finished = false;
         has_been_finished = false;
         pause_action.set_enabled (true);
         solve_action.set_enabled (true);
         finish_action.set_enabled (false);
         new_game_solve_stack.set_visible_child_name ("solve");
+        score_overlay.hide ();
 
         if (puzzle_init_done)
             SignalHandler.disconnect_by_func (puzzle, null, this);
@@ -401,8 +426,10 @@ private class Tetravex : Gtk.Application
             clock_label.set_text ("%02d∶\xE2\x80\x8E%02d".printf (minutes, seconds));
     }
 
+    private bool puzzle_is_finished = false;
     private void solved_cb (Puzzle puzzle)
     {
+        puzzle_is_finished = true;
         undo_action.set_enabled (false);
         redo_action.set_enabled (false);
         pause_action.set_enabled (false);
@@ -431,35 +458,25 @@ private class Tetravex : Gtk.Application
     {
         DateTime date = new DateTime.now_local ();
         uint duration = (uint) (puzzle.elapsed + 0.5);
-        HistoryEntry entry = new HistoryEntry (date, puzzle.size, duration);
-        history.add (entry);
-        history.save ();
-
-        int score_dialog_action = show_scores (entry, true);
-        if (score_dialog_action == ResponseType.CLOSE)
-            window.destroy ();
-        else if (score_dialog_action == ResponseType.OK)
-            new_game ();
-        else if (score_dialog_action != ResponseType.REJECT)
-            new_game_solve_stack.set_visible_child_name ("new-game");
-    }
+        last_history_entry = new HistoryEntry (date, puzzle.size, duration);
 
-    private bool scores_dialog_visible = false; // security for #5
-    private int show_scores (HistoryEntry? selected_entry = null, bool show_quit = false)
-    {
-        if (scores_dialog_visible)
-            return ResponseType.REJECT;
+        if (!puzzle_is_finished) // Ctrl-n has been hit before the animation finished
+            return;
 
-        scores_dialog_visible = true;
-        ScoreDialog dialog = new ScoreDialog (history, puzzle.size, selected_entry, show_quit);
-        dialog.set_modal (true);
-        dialog.set_transient_for (window);
+        HistoryEntry? other_score_0;
+        HistoryEntry? other_score_1;
+        HistoryEntry? other_score_2;
+        uint position = history.get_place ((!) last_history_entry,
+                                           puzzle.size,
+                                       out other_score_0,
+                                       out other_score_1,
+                                       out other_score_2);
+        score_overlay.set_score (puzzle.size, position, (!) last_history_entry, other_score_0, 
other_score_1, other_score_2);
 
-        int result = dialog.run ();
-        dialog.destroy ();
-        scores_dialog_visible = false;
+        new_game_solve_stack.set_visible_child_name ("new-game");
+        view.hide_right_sockets ();
 
-        return result;
+        score_overlay.show ();
     }
 
     private void new_game_cb ()
@@ -489,9 +506,21 @@ private class Tetravex : Gtk.Application
         new_game ();
     }
 
-    private void scores_cb ()
+    private HistoryEntry? last_history_entry = null;
+    private bool scores_dialog_visible = false; // security for #5
+    private void scores_cb (/* SimpleAction action, Variant? variant */)
     {
-        show_scores ();
+        if (scores_dialog_visible)
+            return;
+
+        scores_dialog_visible = true;
+        ScoreDialog dialog = new ScoreDialog (history, puzzle.size, puzzle.is_solved ? last_history_entry : 
null);
+        dialog.set_modal (true);
+        dialog.set_transient_for (window);
+
+        dialog.run ();
+        dialog.destroy ();
+        scores_dialog_visible = false;
     }
 
     private bool view_button_release_event (Widget widget, Gdk.EventButton event)
diff --git a/src/history.vala b/src/history.vala
index 822df20..aab7d41 100644
--- a/src/history.vala
+++ b/src/history.vala
@@ -14,21 +14,108 @@ private class History : Object
     [CCode (notify = false)] public string filename { private get; protected construct; }
     internal List<HistoryEntry> entries = new List<HistoryEntry> ();
 
+    /*\
+    * * getting
+    \*/
+
     internal signal void entry_added (HistoryEntry entry);
 
-    internal History (string filename)
+    internal uint get_place (HistoryEntry   entry,
+                             uint8          puzzle_size,
+                         out HistoryEntry?  other_entry_0,
+                         out HistoryEntry?  other_entry_1,
+                         out HistoryEntry?  other_entry_2)
     {
-        Object (filename: filename);
-        load ();
+        entries.insert_sorted (entry, HistoryEntry.compare_entries);
+        entry_added (entry);
+        save ();
+
+        unowned List<HistoryEntry> entry_item = entries.find (entry);
+        unowned List<HistoryEntry> best_time_item;
+        uint best_position = get_best_time_position (entry_item, out best_time_item);
+        uint position = entries.position (entry_item) - best_position + 1;
+        switch (position)
+        {
+            case 1:
+                unowned List<HistoryEntry>? tmp_item = entry_item.next;
+                if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+                {
+                    other_entry_0 = null;
+                    other_entry_1 = null;
+                    other_entry_2 = null;
+                    break;
+                }
+                other_entry_0 = ((!) tmp_item).data;
+                tmp_item = ((!) tmp_item).next;
+                if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+                {
+                    other_entry_1 = null;
+                    other_entry_2 = null;
+                    break;
+                }
+                other_entry_1 = ((!) tmp_item).data;
+                tmp_item = ((!) tmp_item).next;
+                if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+                    other_entry_2 = null;
+                else
+                    other_entry_2 = ((!) tmp_item).data;
+                break;
+
+            case 2:
+                other_entry_0 = best_time_item.data;
+                unowned List<HistoryEntry>? tmp_item = entry_item.next;
+                if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+                {
+                    other_entry_1 = null;
+                    other_entry_2 = null;
+                    break;
+                }
+                other_entry_1 = ((!) tmp_item).data;
+                tmp_item = ((!) tmp_item).next;
+                if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+                    other_entry_2 = null;
+                else
+                    other_entry_2 = ((!) tmp_item).data;
+                break;
+
+            default:
+                other_entry_0 = best_time_item.data;
+                other_entry_1 = entry_item.prev.data;
+                unowned List<HistoryEntry>? next_entry_item = entry_item.next;
+                if (next_entry_item == null || ((!) next_entry_item).data.size != puzzle_size)
+                    other_entry_2 = null;
+                else
+                    other_entry_2 = ((!) next_entry_item).data;
+                break;
+        }
+        return position;
     }
 
-    internal void add (HistoryEntry entry)
+    private uint get_best_time_position (List<HistoryEntry> entry_item, out unowned List<HistoryEntry> 
best_time_item)
     {
-        entries.append (entry);
-        entry_added (entry);
+        uint8 puzzle_size = entry_item.data.size;
+        best_time_item = entries.first ();
+        if (puzzle_size == 2 || entry_item == best_time_item)
+            return 0;
+
+        best_time_item = entry_item;
+        do { best_time_item = best_time_item.prev; }
+        while (best_time_item != entries && best_time_item.data.size == puzzle_size);
+        best_time_item = best_time_item.next;
+        return entries.position (best_time_item);
+    }
+
+    /*\
+    * * loading
+    \*/
+
+    internal History (string filename)
+    {
+        Object (filename: filename);
+        load ();
     }
 
-    internal void load ()
+    private inline void load ()
     {
         string contents = "";
         try
@@ -57,11 +144,34 @@ private class History : Object
 
             // FIXME use try_parse
 
-            add (new HistoryEntry ((!) date, size, duration));
+            entries.prepend (new HistoryEntry ((!) date, size, duration));
         }
+        entries.sort (HistoryEntry.compare_entries);
+    }
+
+    private inline DateTime? parse_date (string date)
+    {
+        if (date.length < 19 || date[4] != '-' || date[7] != '-' || date[10] != 'T' || date[13] != ':' || 
date[16] != ':')
+            return null;
+
+        // FIXME use try_parse
+
+        int year        = int.parse (date.substring (0, 4));
+        int month       = int.parse (date.substring (5, 2));
+        int day         = int.parse (date.substring (8, 2));
+        int hour        = int.parse (date.substring (11, 2));
+        int minute      = int.parse (date.substring (14, 2));
+        int seconds     = int.parse (date.substring (17, 2));
+        string timezone = date.substring (19);
+
+        return new DateTime (new TimeZone (timezone), year, month, day, hour, minute, seconds);
     }
 
-    internal void save ()
+    /*\
+    * * saving
+    \*/
+
+    private inline void save ()
     {
         string contents = "";
 
@@ -81,24 +191,6 @@ private class History : Object
             warning ("Failed to save history: %s", e.message);
         }
     }
-
-    private DateTime? parse_date (string date)
-    {
-        if (date.length < 19 || date[4] != '-' || date[7] != '-' || date[10] != 'T' || date[13] != ':' || 
date[16] != ':')
-            return null;
-
-        // FIXME use try_parse
-
-        int year        = int.parse (date.substring (0, 4));
-        int month       = int.parse (date.substring (5, 2));
-        int day         = int.parse (date.substring (8, 2));
-        int hour        = int.parse (date.substring (11, 2));
-        int minute      = int.parse (date.substring (14, 2));
-        int seconds     = int.parse (date.substring (17, 2));
-        string timezone = date.substring (19);
-
-        return new DateTime (new TimeZone (timezone), year, month, day, hour, minute, seconds);
-    }
 }
 
 private class HistoryEntry : Object // TODO make struct? needs using HistoryEntry? for the List...
@@ -111,4 +203,33 @@ private class HistoryEntry : Object // TODO make struct? needs using HistoryEntr
     {
         Object (date: date, size: size, duration: duration);
     }
+
+    /*\
+    * * utilities
+    \*/
+
+    internal static string get_duration_string (uint duration)
+    {
+        if (duration >= 3600)
+            /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has taken 
one hour or more; the %u are replaced by the hours (h), minutes (m) and seconds (s); as an example, you might 
want to use "%u:%.2u:%.2u", that is quite international (the ".2" meaning "two digits, padding with 0") */
+            return _("%uh %um %us").printf (duration / 3600, (duration / 60) % 60, duration % 60);
+
+        if (duration >= 60)
+            /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has taken 
between one minute and one hour; the %u are replaced by the minutes (m) and seconds (s); as an example, you 
might want to use "%.2u:%.2u", that is quite international (the ".2" meaning "two digits, padding with 0") */
+            return _("%um %us").printf (duration / 60, duration % 60);
+
+        else
+            /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has taken 
less than one minute; the %u is replaced by the number of seconds (s) it has taken; as an example, you might 
want to use "00:%.2u", that is quite international (the ".2" meaning "two digits, padding with 0") */
+            return _("%us").printf (duration);
+    }
+
+    internal static int compare_entries (HistoryEntry a, HistoryEntry b)
+    {
+        if (a.size != b.size)
+            return (int) a.size - (int) b.size;
+        if (a.duration != b.duration)
+            return (int) a.duration - (int) b.duration;
+        else
+            return a.date.compare (b.date);
+    }
 }
diff --git a/src/meson.build b/src/meson.build
index 5269f8c..55c2674 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -9,6 +9,7 @@ sources = files (
     'puzzle.vala',
     'puzzle-view.vala',
     'score-dialog.vala',
+    'score-overlay.vala',
     'theme-extrusion.vala',
     'theme-neoretro.vala',
     'theme-nostalgia.vala',
diff --git a/src/puzzle-view.vala b/src/puzzle-view.vala
index 0fe08bc..1e08365 100644
--- a/src/puzzle-view.vala
+++ b/src/puzzle-view.vala
@@ -22,6 +22,7 @@ private abstract class Theme : Object
     internal abstract void draw_socket (Cairo.Context context);
     internal abstract void draw_paused_tile (Cairo.Context context);
     internal abstract void draw_tile (Cairo.Context context, Tile tile);
+    internal abstract void set_animation_level (uint8 animation_level /* 0-16 */);
 }
 
 private class PuzzleView : Gtk.DrawingArea
@@ -74,6 +75,7 @@ private class PuzzleView : Gtk.DrawingArea
         private get { if (!puzzle_init_done) assert_not_reached (); return _puzzle; }
         internal set
         {
+            show_right_sockets ();
             uint8 old_puzzle_size = 0;
             if (puzzle_init_done)
             {
@@ -131,6 +133,7 @@ private class PuzzleView : Gtk.DrawingArea
                 theme.configure (tilesize);
             arrow_pattern = null;
             socket_pattern = null;
+            theme.set_animation_level (socket_animation_level);
             queue_draw ();
         }
     }
@@ -155,8 +158,11 @@ private class PuzzleView : Gtk.DrawingArea
     private uint animation_timeout = 0;
 
     /* Set in configure event */
+    [CCode (notify = true)] internal uint boardsize         { internal get; private set; default = 0; }
+    [CCode (notify = true)] internal double x_offset_right  { internal get; private set; default = 0; }
+    [CCode (notify = true)] internal double y_offset        { internal get; private set; default = 0; }
+    [CCode (notify = true)] internal double right_margin    { internal get; private set; default = 0; }
     private double x_offset = 0.0;
-    private double y_offset = 0.0;
     private uint tilesize = 0;
     private uint gap = 0;
     private double arrow_x = 0.0;
@@ -165,7 +171,6 @@ private class PuzzleView : Gtk.DrawingArea
     private double [,] sockets_ys;
     private int board_x_maxi = 0;
     private int board_y_maxi = 0;
-    private int boardsize = 0;
     private double snap_distance = 0.0;
 
     /* Pre-rendered image */
@@ -330,6 +335,9 @@ private class PuzzleView : Gtk.DrawingArea
             arrow_x = x_offset + boardsize;
             arrow_local_y = boardsize * 0.5;
 
+            x_offset_right = arrow_x + gap;
+            right_margin = allocated_width - x_offset_right - boardsize;
+
             /* Precalculate sockets positions */
             for (uint y = 0; y < puzzle.size; y++)
                 for (uint x = 0; x < puzzle.size * 2; x++)
@@ -371,7 +379,7 @@ private class PuzzleView : Gtk.DrawingArea
 
         /* arrow pattern */
         tmp_surface = new Cairo.Surface.similar (context.get_target (), Cairo.Content.COLOR_ALPHA, (int) gap,
-                                                                                                   
boardsize);
+                                                                                                   (int) 
boardsize);
         tmp_context = new Cairo.Context (tmp_surface);
 
         tmp_context.save ();
@@ -536,7 +544,7 @@ private class PuzzleView : Gtk.DrawingArea
 
     private bool on_right_half (double x)
     {
-        return x > x_offset + boardsize + gap * 0.5;
+        return x > x_offset_right - gap * 0.5;
     }
 
     private void drop_tile (double x, double y)
@@ -555,7 +563,7 @@ private class PuzzleView : Gtk.DrawingArea
         int16 tile_x;
         if (on_right_half (x))
         {
-            tile_x = (int16) puzzle.size + (int16) Math.floor ((x - (x_offset + boardsize + gap)) / 
tilesize);
+            tile_x = (int16) puzzle.size + (int16) Math.floor ((x - x_offset_right) / tilesize);
             tile_x = tile_x.clamp ((int16) puzzle.size, 2 * (int16) puzzle.size - 1);
         }
         else
@@ -764,6 +772,10 @@ private class PuzzleView : Gtk.DrawingArea
         tile_selected = false;
     }
 
+    /*\
+    * * history proxies
+    \*/
+
     internal void undo ()
     {
         last_selected_tile = null;
@@ -775,4 +787,43 @@ private class PuzzleView : Gtk.DrawingArea
         last_selected_tile = null;
         puzzle.redo ();
     }
+
+    /*\
+    * * final animation
+    \*/
+
+    private uint8 socket_animation_level = 0;
+    private uint socket_timeout_id = 0;
+
+    internal void hide_right_sockets ()
+    {
+        socket_timeout_id = Timeout.add (75, () => {
+                socket_animation_level++;
+                theme.set_animation_level (socket_animation_level);
+                arrow_pattern = null;
+                socket_pattern = null;
+                queue_draw ();
+
+                if (socket_animation_level < 17)
+                    return Source.CONTINUE;
+                else
+                {
+                    socket_timeout_id = 0;
+                    return Source.REMOVE;
+                }
+            });
+    }
+
+    private inline void show_right_sockets ()
+    {
+        if (socket_timeout_id != 0)
+        {
+            Source.remove (socket_timeout_id);
+            socket_timeout_id = 0;
+        }
+        socket_animation_level = 0;
+        theme.set_animation_level (0);
+        arrow_pattern = null;
+        socket_pattern = null;
+    }
 }
diff --git a/src/score-dialog.vala b/src/score-dialog.vala
index 45350a4..49560ae 100644
--- a/src/score-dialog.vala
+++ b/src/score-dialog.vala
@@ -20,26 +20,14 @@ private class ScoreDialog : Dialog
     private ComboBox size_combo;
     private TreeView scores;
 
-    internal ScoreDialog (History history, uint8 size, HistoryEntry? selected_entry = null, bool show_quit = 
false)
+    internal ScoreDialog (History history, uint8 size, HistoryEntry? selected_entry = null)
     {
+        Object (use_header_bar: /* true */ 1);
+
         this.history = history;
         history.entry_added.connect (entry_added_cb);
         this.selected_entry = selected_entry;
 
-        if (show_quit)
-        {
-            /* Translators: label of a button of the Scores dialog, as it is displayed at the end of a game; 
quits the application */
-            add_button (_("Quit"), ResponseType.CLOSE);
-
-
-            /* Translators: label of a button of the Scores dialog, as it is displayed at the end of a game; 
starts a new game */
-            add_button (_("New Game"), ResponseType.OK);
-        }
-        else
-        {
-            /* Translators: label of a button of the Scores dialog, as it is displayed when called from the 
hamburger menu; closes the dialog */
-            add_button (_("OK"), ResponseType.DELETE_EVENT);
-        }
         set_size_request (200, 300);
 
         Box vbox = new Box (Orientation.VERTICAL, 5);
@@ -88,7 +76,7 @@ private class ScoreDialog : Dialog
         scroll.add (scores);
 
         List<unowned HistoryEntry> entries = history.entries.copy ();
-        entries.sort (compare_entries);
+        entries.sort (HistoryEntry.compare_entries);
         foreach (HistoryEntry entry in entries)
             entry_added_cb (entry);
 
@@ -102,7 +90,7 @@ private class ScoreDialog : Dialog
         score_model.clear ();
 
         List<unowned HistoryEntry> entries = history.entries.copy ();
-        entries.sort (compare_entries);
+        entries.sort (HistoryEntry.compare_entries);
 
         foreach (HistoryEntry entry in entries)
         {
@@ -112,18 +100,7 @@ private class ScoreDialog : Dialog
             /* "the preferred date representation for the current locale without the time" */
             string date_label = entry.date.format ("%x");
 
-            string time_label;
-            if (entry.duration >= 3600)
-                /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has 
taken one hour or more; the %u are replaced by the hours (h), minutes (m) and seconds (s); as an example, you 
might want to use "%u:%.2u:%.2u", that is quite international (the ".2" meaning "two digits, padding with 0") 
*/
-                time_label = _("%uh %um %us").printf (entry.duration / 3600, (entry.duration / 60) % 60, 
entry.duration % 60);
-
-            else if (entry.duration >= 60)
-                /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has 
taken between one minute and one hour; the %u are replaced by the minutes (m) and seconds (s); as an example, 
you might want to use "%.2u:%.2u", that is quite international (the ".2" meaning "two digits, padding with 
0") */
-                time_label = _("%um %us").printf (entry.duration / 60, entry.duration % 60);
-
-            else
-                /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has 
taken less than one minute; the %u is replaced by the number of seconds (s) it has taken; as an example, you 
might want to use "00:%.2u", that is quite international (the ".2" meaning "two digits, padding with 0") */
-                time_label = _("%us").printf (entry.duration);
+            string time_label = HistoryEntry.get_duration_string (entry.duration);
 
             int weight = Pango.Weight.NORMAL;
             if (entry == selected_entry)
@@ -149,15 +126,6 @@ private class ScoreDialog : Dialog
         }
     }
 
-    private static int compare_entries (HistoryEntry a, HistoryEntry b)
-    {
-        if (a.size != b.size)
-            return (int) a.size - (int) b.size;
-        if (a.duration != b.duration)
-            return (int) a.duration - (int) b.duration;
-        return a.date.compare (b.date);
-    }
-
     private void size_changed_cb (ComboBox combo)
     {
         TreeIter iter;
diff --git a/src/score-overlay-entry.ui b/src/score-overlay-entry.ui
new file mode 100644
index 0000000..780e5ee
--- /dev/null
+++ b/src/score-overlay-entry.ui
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="ScoreOverlayEntry" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="width-request">160</property> <!-- two tiles of 80 max -->
+    <property name="halign">center</property>
+    <child>
+      <object class="GtkLabel" id="place_label">
+        <property name="visible">True</property>
+        <property name="hexpand">True</property>
+        <property name="halign">start</property>
+        <style>
+          <class name="bold-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="top-attach">2</property>
+        <property name="left-attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="value_label">
+        <property name="visible">True</property>
+        <property name="hexpand">True</property>
+        <property name="halign">end</property>
+      </object>
+      <packing>
+        <property name="top-attach">2</property>
+        <property name="left-attach">2</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/score-overlay.ui b/src/score-overlay.ui
new file mode 100644
index 0000000..72bd54b
--- /dev/null
+++ b/src/score-overlay.ui
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="ScoreOverlay" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="valign">center</property>
+    <property name="orientation">vertical</property>
+    <property name="row-homogeneous">True</property>
+    <property name="column-homogeneous">True</property>
+    <property name="column-spacing">4</property>
+    <style>
+      <class name="score-overlay"/>
+    </style>
+    <child>
+      <object class="GtkLabel">
+        <property name="visible">True</property>
+        <property name="hexpand">True</property>
+        <property name="label" translatable="yes">Congratulations!</property>
+        <style>
+          <class name="score-title"/>
+        </style>
+      </object>
+      <packing>
+        <property name="top-attach">0</property>
+        <property name="left-attach">0</property>
+        <property name="height">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="ScoreOverlayEntry" id="score_0"/>
+      <packing>
+        <property name="top-attach">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="ScoreOverlayEntry" id="score_1"/>
+      <packing>
+        <property name="top-attach">3</property>
+      </packing>
+    </child>
+    <child>
+      <object class="ScoreOverlayEntry" id="score_2"/>
+      <packing>
+        <property name="top-attach">4</property>
+      </packing>
+    </child>
+    <child>
+      <object class="ScoreOverlayEntry" id="score_3"/>
+      <packing>
+        <property name="top-attach">5</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton">
+        <property name="visible">True</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <property name="label" translatable="yes">Show scores</property>
+        <property name="action-name">app.scores</property>
+        <property name="focus-on-click">False</property>
+        <style>
+          <class name="rounded-button"/>
+        </style>
+      </object>
+      <packing>
+        <property name="top-attach">6</property>
+        <property name="left-attach">0</property>
+        <property name="height">2</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/score-overlay.vala b/src/score-overlay.vala
new file mode 100644
index 0000000..27e2ccc
--- /dev/null
+++ b/src/score-overlay.vala
@@ -0,0 +1,164 @@
+/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * Copyright (C) 2019 Arnaud Bonatti
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 2 of the License, or (at your option) any later
+ * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Tetravex/score-overlay.ui")]
+private class ScoreOverlay : Grid
+{
+    [CCode (notify = true)] internal uint boardsize
+    {
+        internal set
+        {
+            if (value < 250 && value != 0)
+                get_style_context ().add_class ("small-window");
+            else
+                get_style_context ().remove_class ("small-window");
+        }
+    }
+
+    /*\
+    * * updating labels
+    \*/
+
+    [GtkChild] private ScoreOverlayEntry score_0;
+    [GtkChild] private ScoreOverlayEntry score_1;
+    [GtkChild] private ScoreOverlayEntry score_2;
+    [GtkChild] private ScoreOverlayEntry score_3;
+
+    internal void set_score (uint8          puzzle_size,
+                             uint /* [1[ */ position,
+                             HistoryEntry   entry,
+                             HistoryEntry?  other_entry_0,
+                             HistoryEntry?  other_entry_1,
+                             HistoryEntry?  other_entry_2)
+    {
+        switch (position)
+        {
+            case 1:
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has been the fastest for a puzzle of this size; introduces the game time */
+                score_0.set_place_label (_("New best time!"));
+                score_0.set_value_label (HistoryEntry.get_duration_string (entry.duration), true);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has been the fastest for a puzzle of this size; introduces the old best time */
+                score_1.set_place_label (_("Second:"));
+                if (other_entry_0 != null)
+                    score_1.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_0).duration));
+                else
+                    score_1.set_value_label (null);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has been the fastest for a puzzle of this size; introduces the old second best time */
+                score_2.set_place_label (_("Third:"));
+                if (other_entry_1 != null)
+                    score_2.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_1).duration));
+                else
+                    score_2.set_value_label (null);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has been the fastest for a puzzle of this size; introduces the old third best time */
+                score_3.set_place_label (_("Out of podium:"));
+                if (other_entry_2 != null)
+                    score_3.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_2).duration));
+                else
+                    score_3.set_value_label (null);
+                break;
+
+            case 2:
+                if (other_entry_0 == null)
+                    assert_not_reached ();
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has made the second best time for a puzzle of this size; introduces the best time ever */
+                score_0.set_place_label (_("Best time:"));
+                score_0.set_value_label (HistoryEntry.get_duration_string (((!) other_entry_0).duration));
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has made the second best time for a puzzle of this size; introduces the game time */
+                score_1.set_place_label (_("Your time:"));
+                score_1.set_value_label (HistoryEntry.get_duration_string (entry.duration), true);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has made the second best time for a puzzle of this size; introduces the old second best time */
+                score_2.set_place_label (_("Third:"));
+                if (other_entry_1 != null)
+                    score_2.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_1).duration));
+                else
+                    score_2.set_value_label (null);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has made the second best time for a puzzle of this size; introduces the old third best time */
+                score_3.set_place_label (_("Out of podium:"));
+                if (other_entry_2 != null)
+                    score_3.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_2).duration));
+                else
+                    score_3.set_value_label (null);
+                break;
+
+            default:
+                if (other_entry_0 == null || other_entry_1 == null)
+                    assert_not_reached ();
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; introduces the best time 
ever */
+                score_0.set_place_label (_("Best time:"));
+                score_0.set_value_label (HistoryEntry.get_duration_string (((!) other_entry_0).duration));
+
+                if (position == 3)
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; introduces the second best 
time */
+                    score_1.set_place_label (_("Second:"));
+
+                else if (position == 4)
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; introduces the third best 
time */
+                    score_1.set_place_label (_("Third:"));
+
+                else
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; the %u is replaced by the 
rank before the one of the game played */
+                    score_1.set_place_label (_("Place %u:").printf (position - 1));
+                score_1.set_value_label (HistoryEntry.get_duration_string (((!) other_entry_1).duration));
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; introduces the game time */
+                score_2.set_place_label (_("Your time:"));
+                score_2.set_value_label (HistoryEntry.get_duration_string (entry.duration), true);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; the %u is replaced by the 
rank after the one of the game played */
+                score_3.set_place_label (_("Place %u:").printf (position + 1));
+                if (other_entry_2 != null)
+                    score_3.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_2).duration));
+                else
+                    score_3.set_value_label (null);
+                break;
+        }
+    }
+}
+
+[GtkTemplate (ui = "/org/gnome/Tetravex/score-overlay-entry.ui")]
+private class ScoreOverlayEntry : Grid
+{
+    [GtkChild] private Label place_label;
+    [GtkChild] private Label value_label;
+
+    internal void set_place_label (string label)
+    {
+        place_label.set_label (label);
+    }
+
+    internal void set_value_label (string? label, bool bold_label = false)
+    {
+        if (label != null)
+        {
+            value_label.set_label ((!) label);
+            value_label.get_style_context ().remove_class ("italic-label");
+        }
+        else
+        {
+            /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if the 
player has made one of the worst scores for a game of this size; says that the rank after the one of the game 
is "free", inviting to do worse */
+            value_label.set_label (_("Free!"));
+            value_label.get_style_context ().add_class ("italic-label");
+        }
+
+        if (bold_label)
+            value_label.get_style_context ().add_class ("bold-label");
+        else
+            value_label.get_style_context ().remove_class ("bold-label");
+    }
+}
diff --git a/src/tetravex.css b/src/tetravex.css
new file mode 100644
index 0000000..cdf373a
--- /dev/null
+++ b/src/tetravex.css
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 Arnaud Bonatti
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 2 of the License, or (at your option) any later
+ * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+              .score-title {
+  font-weight:bolder;
+
+  font-size:200%;
+  margin-bottom:1.5rem;
+  margin-top:0.5rem;
+
+  transition:font-size      0.5s,
+             margin-bottom  0.5s,
+             margin-top     0.5s;
+}
+.small-window .score-title {
+  font-size:150%;
+  margin-bottom:1rem;
+  margin-top:0.2rem;
+}
+
+grid.score-overlay button {
+  margin-top:0.5rem;
+}
+
+.bold-label {
+  font-weight:bolder;
+}
+
+.italic-label {
+  font-style:italic;
+}
+
+button.rounded-button {
+  border-radius:       9999px;
+  -gtk-outline-radius: 9999px;
+}
diff --git a/src/theme-extrusion.vala b/src/theme-extrusion.vala
index 0fd6838..3400588 100644
--- a/src/theme-extrusion.vala
+++ b/src/theme-extrusion.vala
@@ -104,6 +104,7 @@ private class ExtrusionTheme : Theme
     \*/
 
     private uint size = 0;
+    private uint8 animation_level = 0;
 
     /* arrow */
     private double arrow_half_h;
@@ -177,6 +178,11 @@ private class ExtrusionTheme : Theme
         size = new_size;
     }
 
+    internal override void set_animation_level (uint8 new_animation_level /* 0-16 */)
+    {
+        animation_level = new_animation_level;
+    }
+
     /*\
     * * drawing arrow
     \*/
@@ -190,7 +196,10 @@ private class ExtrusionTheme : Theme
         context.line_to (arrow_w, neg_arrow_half_h);
         context.close_path ();
 
-        context.set_source_rgba (0.5, 0.5, 0.5, 0.4);
+        if (animation_level == 0)
+            context.set_source_rgba (0.5, 0.5, 0.5, 0.4);
+        else
+            context.set_source_rgba (0.5, 0.5, 0.5, 0.4 * (16.0 - (double) animation_level) / 16.0);
         context.fill ();
     }
 
@@ -202,7 +211,10 @@ private class ExtrusionTheme : Theme
     {
         context.save ();
 
-        context.set_source_rgba (0.5, 0.5, 0.5, 0.3);
+        if (animation_level == 0)
+            context.set_source_rgba (0.5, 0.5, 0.5, 0.3);
+        else
+            context.set_source_rgba (0.5, 0.5, 0.5, 0.3 * (16.0 - (double) animation_level) / 16.0);
         rounded_square (context,
           /* x and y */ tile_margin, tile_margin,
           /* size    */ tile_size,
diff --git a/src/theme-neoretro.vala b/src/theme-neoretro.vala
index 069cf22..07fa149 100644
--- a/src/theme-neoretro.vala
+++ b/src/theme-neoretro.vala
@@ -126,6 +126,7 @@ private class NeoRetroTheme : Theme
     \*/
 
     private uint size = 0;
+    private uint8 animation_level = 0;
 
     /* arrow */
     private double arrow_half_h;
@@ -138,6 +139,9 @@ private class NeoRetroTheme : Theme
     private double arrow_clip_w;
     private double arrow_clip_h;
 
+    private double arrow_border_opacity;
+    private double arrow_fill_opacity;
+
     /* socket */
     private uint socket_margin;
     private int socket_size;
@@ -180,18 +184,7 @@ private class NeoRetroTheme : Theme
         socket_margin = uint.min ((uint) (new_size * 0.05), 2);
         socket_size = (int) new_size - (int) socket_margin * 2;
 
-        socket_pattern = new Cairo.MeshPattern ();
-        socket_pattern.begin_patch ();
-        socket_pattern.move_to (0.0, 0.0);
-        socket_pattern.line_to (1.0, 0.0);
-        socket_pattern.line_to (1.0, 1.0);
-        socket_pattern.line_to (0.0, 1.0);
-        socket_pattern.set_corner_color_rgba (0, 0.3, 0.3, 0.3, 0.3);
-        socket_pattern.set_corner_color_rgba (1, 0.4, 0.4, 0.4, 0.3);
-        socket_pattern.set_corner_color_rgba (2, 0.7, 0.7, 0.7, 0.3);
-        socket_pattern.set_corner_color_rgba (3, 0.6, 0.6, 0.6, 0.3);
-        socket_pattern.end_patch ();
-        socket_pattern.set_matrix (matrix);
+        init_socket_pattern ();
 
         /* tile */
         tile_margin = uint.min ((uint) (new_size * 0.05), 2) - 1;
@@ -209,6 +202,30 @@ private class NeoRetroTheme : Theme
         size = new_size;
     }
 
+    internal override void set_animation_level (uint8 new_animation_level /* 0-16 */)
+    {
+        animation_level = new_animation_level;
+        arrow_border_opacity = animation_level == 0 ? 0.3 : 0.3 * (16.0 - (double) animation_level) / 16.0;
+        arrow_fill_opacity = animation_level == 0 ? 0.1 : 0.1 * (16.0 - (double) animation_level) / 16.0;
+        init_socket_pattern ();
+    }
+
+    private void init_socket_pattern ()
+    {
+        socket_pattern = new Cairo.MeshPattern ();
+        socket_pattern.begin_patch ();
+        socket_pattern.move_to (0.0, 0.0);
+        socket_pattern.line_to (1.0, 0.0);
+        socket_pattern.line_to (1.0, 1.0);
+        socket_pattern.line_to (0.0, 1.0);
+        socket_pattern.set_corner_color_rgba (0, 0.3, 0.3, 0.3, arrow_border_opacity);
+        socket_pattern.set_corner_color_rgba (1, 0.4, 0.4, 0.4, arrow_border_opacity);
+        socket_pattern.set_corner_color_rgba (2, 0.7, 0.7, 0.7, arrow_border_opacity);
+        socket_pattern.set_corner_color_rgba (3, 0.6, 0.6, 0.6, arrow_border_opacity);
+        socket_pattern.end_patch ();
+        socket_pattern.set_matrix (matrix);
+    }
+
     /*\
     * * drawing arrow
     \*/
@@ -252,11 +269,11 @@ private class NeoRetroTheme : Theme
         context.set_line_cap (Cairo.LineCap.ROUND);
 
         context.set_line_width (14.0);
-        context.set_source_rgba (0.4, 0.4, 0.4, 0.3);   // fill color 1, including border
+        context.set_source_rgba (0.4, 0.4, 0.4, arrow_border_opacity);  // fill color 1, including border
         context.stroke_preserve ();
 
         context.set_line_width (12.0);
-        context.set_source_rgba (1.0, 1.0, 1.0, 0.1);   // fill color 2
+        context.set_source_rgba (1.0, 1.0, 1.0, arrow_fill_opacity);    // fill color 2
         context.stroke_preserve ();
 
         /* filling interior */
@@ -264,10 +281,10 @@ private class NeoRetroTheme : Theme
         context.reset_clip ();  // forget the border clip
         context.clip ();       // clip to the current path
 
-        context.set_source_rgba (0.4, 0.4, 0.4, 0.3);   // fill color 1
+        context.set_source_rgba (0.4, 0.4, 0.4, arrow_border_opacity);  // fill color 1
         context.fill_preserve ();
 
-        context.set_source_rgba (1.0, 1.0, 1.0, 0.1);   // fill color 2
+        context.set_source_rgba (1.0, 1.0, 1.0, arrow_fill_opacity);    // fill color 2
         context.fill ();
     }
 
@@ -288,7 +305,7 @@ private class NeoRetroTheme : Theme
         context.fill_preserve ();
 
         context.set_line_width (1.0);
-        context.set_source_rgba (0.4, 0.4, 0.4, 0.3);
+        context.set_source_rgba (0.4, 0.4, 0.4, arrow_border_opacity);
         context.stroke ();
 
         context.restore ();
diff --git a/src/theme-nostalgia.vala b/src/theme-nostalgia.vala
index 9a16d39..e03375b 100644
--- a/src/theme-nostalgia.vala
+++ b/src/theme-nostalgia.vala
@@ -74,11 +74,12 @@ private class NostalgiaTheme : Theme
     \*/
 
     private uint size = 0;
+    private uint8 animation_level = 0;
 
     /* arrow */
     private double arrow_half_h;
     private double neg_arrow_half_h;
-    private uint arrow_depth;
+    private double arrow_depth;
     private double arrow_dx;
     private double arrow_dy;
     private double neg_arrow_dy;
@@ -87,7 +88,7 @@ private class NostalgiaTheme : Theme
     private double arrow_w_minus_depth;
 
     /* tile and socket */
-    private uint tile_depth;
+    private double tile_depth;
     private double size_minus_tile_depth;
     private double size_minus_two_tile_depths;    // socket only
 
@@ -112,21 +113,8 @@ private class NostalgiaTheme : Theme
         if (size != 0 && size == new_size)
             return;
 
-        /* arrow */
-        arrow_half_h = new_size * 0.75;
-        neg_arrow_half_h = -arrow_half_h;
-        arrow_depth = uint.min ((uint) (new_size * 0.025), 2);
-        arrow_dx = 1.4142 * arrow_depth;
-        arrow_dy = arrow_half_h - 6.1623 * arrow_depth;
-        neg_arrow_dy = -arrow_dy;
-        arrow_w = new_size * PuzzleView.gap_factor * 0.5;
-        arrow_x = (new_size * PuzzleView.gap_factor - arrow_w) * 0.5;
-        arrow_w_minus_depth = arrow_w - arrow_depth;
-
-        /* socket and tile */
-        tile_depth = uint.min ((uint) (new_size * 0.05), 4);
-        size_minus_tile_depth = (double) new_size - tile_depth;
-        size_minus_two_tile_depths = (double) (new_size - tile_depth * 2);
+        configure_arrow (new_size);
+        configure_socket (new_size);
 
         /* tiles */
         tile_dx = 2.4142 * tile_depth;
@@ -148,6 +136,35 @@ private class NostalgiaTheme : Theme
         size = new_size;
     }
 
+    internal override void set_animation_level (uint8 new_animation_level /* 0-16 */)
+    {
+        animation_level = new_animation_level;
+        configure_arrow (size);
+        configure_socket (size);
+    }
+
+    private void configure_arrow (uint new_size)
+    {
+        arrow_half_h = new_size * 0.75;
+        neg_arrow_half_h = -arrow_half_h;
+        arrow_depth = double.min (new_size * 0.025, 2.0) - (double) animation_level / 6.0;
+        arrow_depth = double.max (arrow_depth, 0.0);
+        arrow_dx = 1.4142 * arrow_depth;
+        arrow_dy = arrow_half_h - 6.1623 * arrow_depth;
+        neg_arrow_dy = -arrow_dy;
+        arrow_w = new_size * PuzzleView.gap_factor * 0.5;
+        arrow_x = (new_size * PuzzleView.gap_factor - arrow_w) * 0.5;
+        arrow_w_minus_depth = arrow_w - arrow_depth;
+    }
+
+    private void configure_socket (uint new_size)
+    {
+        tile_depth = double.min (new_size * 0.05, 4.0) - (double) animation_level / 4.0;
+        tile_depth = double.max (tile_depth, 0.0);
+        size_minus_tile_depth = (double) new_size - tile_depth;
+        size_minus_two_tile_depths = (double) new_size - tile_depth * 2.0;
+    }
+
     /*\
     * * drawing arrow
     \*/
@@ -161,7 +178,10 @@ private class NostalgiaTheme : Theme
         context.line_to (arrow_w, arrow_half_h);
         context.line_to (arrow_w, neg_arrow_half_h);
         context.close_path ();
-        context.set_source_rgba (0.0, 0.0, 0.0, 0.125);
+        if (animation_level == 0)
+            context.set_source_rgba (0, 0, 0, 0.125);
+        else
+            context.set_source_rgba (0, 0, 0, 0.125 * (16.0 - (double) animation_level) / 16.0);
         context.fill ();
 
         /* Arrow highlight */
@@ -193,7 +213,10 @@ private class NostalgiaTheme : Theme
     {
         /* Background */
         context.rectangle (tile_depth, tile_depth, size_minus_two_tile_depths, size_minus_two_tile_depths);
-        context.set_source_rgba (0.0, 0.0, 0.0, 0.125);
+        if (animation_level == 0)
+            context.set_source_rgba (0, 0, 0, 0.125);
+        else
+            context.set_source_rgba (0, 0, 0, 0.125 * (16.0 - (double) animation_level) / 16.0);
         context.fill ();
 
         /* Shadow */
diff --git a/src/theme-synesthesia.vala b/src/theme-synesthesia.vala
index 7579ece..f9cfc91 100644
--- a/src/theme-synesthesia.vala
+++ b/src/theme-synesthesia.vala
@@ -61,6 +61,7 @@ private class SynesthesiaTheme : Theme
     \*/
 
     private uint size = 0;
+    private uint8 animation_level = 0;
 
     /* arrow */
     private double arrow_half_h;
@@ -107,19 +108,7 @@ private class SynesthesiaTheme : Theme
         tile_depth = uint.min ((uint) (new_size * 0.05), 4);
         size_minus_tile_depth = (double) new_size - tile_depth;
         size_minus_two_tile_depths = (double) (new_size - tile_depth * 2);
-
-        socket_pattern = new Cairo.MeshPattern ();
-        socket_pattern.begin_patch ();
-        socket_pattern.move_to (0.5, 0.0);
-        socket_pattern.line_to (1.0, 0.5);
-        socket_pattern.line_to (0.5, 1.0);
-        socket_pattern.line_to (0.0, 0.5);
-        socket_pattern.set_corner_color_rgba (0, 0.45, 0.45, 0.45, 0.5);
-        socket_pattern.set_corner_color_rgba (1, 0.6 , 0.6 , 0.6 , 0.5);
-        socket_pattern.set_corner_color_rgba (2, 0.7 , 0.7 , 0.7 , 0.5);
-        socket_pattern.set_corner_color_rgba (3, 0.55, 0.55, 0.55, 0.5);
-        socket_pattern.end_patch ();
-        socket_pattern.set_matrix (matrix);
+        init_socket_pattern ();
 
         /* tile */
         tile_margin = uint.min ((uint) (new_size * 0.05), 2) - 1;
@@ -138,6 +127,38 @@ private class SynesthesiaTheme : Theme
         size = new_size;
     }
 
+    internal override void set_animation_level (uint8 new_animation_level /* 0-16 */)
+    {
+        animation_level = new_animation_level;
+        init_socket_pattern ();
+    }
+
+    private void init_socket_pattern ()
+    {
+        socket_pattern = new Cairo.MeshPattern ();
+        socket_pattern.begin_patch ();
+        socket_pattern.move_to (0.5, 0.0);
+        socket_pattern.line_to (1.0, 0.5);
+        socket_pattern.line_to (0.5, 1.0);
+        socket_pattern.line_to (0.0, 0.5);
+        if (animation_level == 0)
+        {
+            socket_pattern.set_corner_color_rgba (0, 0.45, 0.45, 0.45, 0.5);
+            socket_pattern.set_corner_color_rgba (1, 0.6 , 0.6 , 0.6 , 0.5);
+            socket_pattern.set_corner_color_rgba (2, 0.7 , 0.7 , 0.7 , 0.5);
+            socket_pattern.set_corner_color_rgba (3, 0.55, 0.55, 0.55, 0.5);
+        }
+        else
+        {
+            socket_pattern.set_corner_color_rgba (0, 0.45, 0.45, 0.45, 0.5 * (16.0 - (double) 
animation_level) / 16.0);
+            socket_pattern.set_corner_color_rgba (1, 0.6 , 0.6 , 0.6 , 0.5 * (16.0 - (double) 
animation_level) / 16.0);
+            socket_pattern.set_corner_color_rgba (2, 0.7 , 0.7 , 0.7 , 0.5 * (16.0 - (double) 
animation_level) / 16.0);
+            socket_pattern.set_corner_color_rgba (3, 0.55, 0.55, 0.55, 0.5 * (16.0 - (double) 
animation_level) / 16.0);
+        }
+        socket_pattern.end_patch ();
+        socket_pattern.set_matrix (matrix);
+    }
+
     /*\
     * * drawing arrow
     \*/
@@ -150,7 +171,10 @@ private class SynesthesiaTheme : Theme
         context.line_to (arrow_w, arrow_half_h);
         context.line_to (arrow_w, neg_arrow_half_h);
         context.close_path ();
-        context.set_source_rgba (0.5, 0.5, 0.6, 0.4);
+        if (animation_level == 0)
+            context.set_source_rgba (0.5, 0.5, 0.6, 0.4);
+        else
+            context.set_source_rgba (0.5, 0.5, 0.6, 0.4 * (16.0 - (double) animation_level) / 16.0);
         context.fill ();
     }
 
@@ -173,7 +197,10 @@ private class SynesthesiaTheme : Theme
         context.fill_preserve ();
 
         context.set_line_width (1.0);
-        context.set_source_rgba (0.4, 0.4, 0.4, 0.3);
+        if (animation_level == 0)
+            context.set_source_rgba (0.4, 0.4, 0.4, 0.3);
+        else
+            context.set_source_rgba (0.4, 0.4, 0.4, 0.3 * (16.0 - (double) animation_level) / 16.0);
         context.stroke ();
 
         context.restore ();


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]