Merge lp:~mterry/unity-greeter/animation-fixes into lp:unity-greeter

Proposed by Michael Terry on 2012-03-13
Status: Merged
Merged at revision: 352
Proposed branch: lp:~mterry/unity-greeter/animation-fixes
Merge into: lp:unity-greeter
Prerequisite: lp:~mterry/unity-greeter/responsiveness-fixes
Diff against target: 612 lines (+181/-180) 9 files modified
To merge this branch: bzr merge lp:~mterry/unity-greeter/animation-fixes
Reviewer Review Type Date Requested Status
Ken VanDine 2012-03-13 Approve on 2012-03-13
Unity Greeter Development Team 2012-03-13 Pending
Review via email: mp+97233@code.launchpad.net

Description of the Change

In conjunction with design, some further easing tweaks (different algorithm for scrolling and different duration times).

To post a comment you must log in.
Ken VanDine (ken-vandine) wrote :

Looks good to me and with the minimal testing I did, it functions well.

My only comment is the old code that was removed included some really great comments describing how the animations are calculated. It would be nice to have similar comments describing the current calculations. Certainly not a blocker, but the previous code comments seemed like excellent documentation of the logic used.

review: Approve

Preview Diff

1=== modified file 'src/Makefile.am'
2--- src/Makefile.am 2012-02-21 17:31:59 +0000
3+++ src/Makefile.am 2012-03-13 14:51:31 +0000
4@@ -8,6 +8,7 @@
5 indicator.vapi \
6 animate-timer.vala \
7 background.vala \
8+ cached-image.vala \
9 dash-box.vala \
10 dash-button.vala \
11 dash-entry.vala \
12
13=== modified file 'src/animate-timer.vala'
14--- src/animate-timer.vala 2012-02-21 17:31:59 +0000
15+++ src/animate-timer.vala 2012-03-13 14:51:31 +0000
16@@ -20,6 +20,9 @@
17
18 private class AnimateTimer : Object
19 {
20+ /* x and y are 0.0 to 1.0 */
21+ public delegate double EasingFunc (double x);
22+
23 /* The following are the same intervals that Unity uses */
24 public static const int INSTANT = 150; /* Good for animations that don't convey any information */
25 public static const int FAST = 250; /* Good for animations that convey duplicated information */
26@@ -27,6 +30,7 @@
27 public static const int SLOW = 1000; /* Good for animations that convey information that is only presented in the animation */
28
29 /* speed is in milliseconds */
30+ public unowned EasingFunc easing_func {get; private set;}
31 public int speed {get; set;}
32 public bool is_running { get { return timeout != 0; } }
33 public double progress {get; private set;}
34@@ -35,9 +39,10 @@
35 public signal void animate (double progress);
36
37 /* speed is in milliseconds */
38- public AnimateTimer (int speed)
39+ public AnimateTimer (EasingFunc func, int speed)
40 {
41 Object (speed: speed);
42+ this.easing_func = func;
43 }
44
45 /* temp_speed is in milliseconds */
46@@ -57,38 +62,6 @@
47 length = temp_speed * TimeSpan.MILLISECOND;
48 }
49
50- /* This tells us the consumer of our progress reports is changing the
51- goalposts on us. So we need to adjust our reports accordingly.
52- 'extra_progress' is how much progress is being added to the animation.
53- */
54- public bool extend (double new_progress)
55- {
56- /* As an example, say a consumer is animating from point A to point C in
57- 150ms. This is two points to cover, and at 75ms, they will be 0.5 done.
58-
59- Now, they extend it to point D. This adds an extra point, which is half
60- the original progress. So now they call extend (0.5).
61-
62- We extend in the middle, so we need the slope at halfway through our
63- progress function. That is a constant we have (see calculate_progress
64- for details). So we'd need to add the same amount of normalized time
65- to our process as progress, which is 0.5 / HALFWAY_VELOCITY = 0.318 in
66- this example. Which is (length * 0.318) = 47.7ms more time to add.
67-
68- Now we have 197.7ms as length of our animation. Which changes the
69- progress values we give out. We further need to adjust our progress
70- function to take this 'pause' in the middle of the function into
71- account.
72- */
73-
74- if (progress > HALFWAY_PROGRESS)
75- return false;
76-
77- extra_progress += new_progress;
78- extra_time = (TimeSpan)(length * extra_progress / HALFWAY_VELOCITY);
79- return true;
80- }
81-
82 public void stop ()
83 {
84 if (timeout != 0)
85@@ -102,13 +75,6 @@
86 private TimeSpan extra_time = 0;
87 private double extra_progress = 0.0;
88
89- /* Derivative of our easing function at 0.5 (the halfway point).
90- See calculate_progress() for functions. */
91- private static const double HALFWAY_VELOCITY = 1.5708;
92-
93- /* The y value of our easing function at 0.5 (the halway point) */
94- private static const double HALFWAY_PROGRESS = 0.75;
95-
96 private bool animate_cb ()
97 {
98 if (start_time == 0)
99@@ -129,94 +95,39 @@
100 /* Returns 0.0 to 1.0 where 1.0 is at or past end_time */
101 private double normalize_time (TimeSpan now)
102 {
103- var total = length;
104- var halfway = start_time + length / 2;
105- if (now > halfway)
106- now -= extra_time;
107-
108- return (((double)(now - start_time)) / total).clamp (0.0, 1.0);
109- }
110-
111- /* Incoming progress count is original, un-extra-time enhanced progress */
112- private double normalize_progress (double p)
113- {
114- /* So without extra time, progress goes from 0.0 to 1.0. But with
115- extra time, the beginning and end want to keep the same function,
116- just squeezed into a smaller place in the graph. So we figure out
117- that mapping here. */
118- var extra_mapped = extra_progress / (1.0 + extra_progress);
119- var half_mapped = HALFWAY_PROGRESS / (1.0 + extra_progress);
120- if (p < HALFWAY_PROGRESS)
121- return p * (half_mapped / HALFWAY_PROGRESS);
122- else
123- {
124- var end = 1.0 - half_mapped - extra_mapped;
125- return (p - HALFWAY_PROGRESS) * (end / (1 - HALFWAY_PROGRESS)) + half_mapped + extra_mapped;
126- }
127+ return (((double)(now - start_time)) / length).clamp (0.0, 1.0);
128 }
129
130 /* Returns 0.0 to 1.0 where 1.0 is done.
131 time is not normalized yet! */
132 private double calculate_progress (TimeSpan time)
133 {
134- /* Use a sine wave function similar to what Unity uses. They call it
135- 'easing' and is designed to make the animation start and end slower
136- than in the middle.
137-
138- ((1 - Math.cos (Math.PI * time)) / 2) is the basic sine curve for
139- easing, used by Unity and others. By squaring that and reversing
140- the curve (by using 1 - x and 1 - y), we get a more exaggerated
141- slowdown.
142-
143- For clarity, here is the whole function:
144- y = 1 - ((1 - cos (pi * (1 - x))) / 2) ^ 2
145-
146- Here is the derivative of that function (useful for calculating
147- slope at a given point, used elsewhere in this class):
148- y = (pi * (cos (pi * (x - 1)) - 1)) * sin (pi * (x - 1)) / 2
149-
150- */
151-
152- /* But due to the ability to add extra time into the equation, we
153- are actually three functions:
154- 1) x=[0,A) = normal
155- 2) x=[A,B] = halfway slope
156- 3) x=(B,1.0] = picks up where first function left off */
157-
158- var y = 0.0;
159- var orig_half_point = start_time + length / 2;
160-
161- if (time > start_time + length + extra_time)
162- {
163- y = 1.0;
164- }
165- else if (time < orig_half_point)
166- {
167- var x = normalize_time (time);
168- y = easing_function (x);
169- y = normalize_progress (y);
170- }
171- else if (time < orig_half_point + extra_time)
172- {
173- var xpercent = (time - orig_half_point) / (double)extra_time;
174- y = (xpercent * extra_progress + HALFWAY_PROGRESS) / (1.0 + extra_progress);
175- }
176- else
177- {
178- var x = normalize_time (time);
179- y = easing_function (x);
180- y = normalize_progress (y);
181- }
182-
183+ var x = normalize_time (time);
184+ var y = easing_func (x);
185 return y.clamp (0.0, 1.0);
186 }
187
188- private double easing_function (double x)
189- {
190- var y = (1 - Math.cos (Math.PI * (1.0 - x))) / 2;
191- y = y * y;
192- y = 1.0 - y;
193- return y;
194+ public static double ease_in_out (double x)
195+ {
196+ return (1 - Math.cos (Math.PI * x)) / 2;
197+ }
198+
199+ /*public static double ease_in_quad (double x)
200+ {
201+ return Math.pow (x, 2);
202+ }*/
203+ /*public static double ease_out_quad (double x)
204+ {
205+ return -1 * Math.pow (x - 1, 2) + 1;
206+ }*/
207+
208+ /*public static double ease_in_quint (double x)
209+ {
210+ return Math.pow (x, 5);
211+ }*/
212+ public static double ease_out_quint (double x)
213+ {
214+ return Math.pow (x - 1, 5) + 1;
215 }
216 }
217
218
219=== modified file 'src/background.vala'
220--- src/background.vala 2012-02-21 22:36:02 +0000
221+++ src/background.vala 2012-03-13 14:51:31 +0000
222@@ -247,7 +247,7 @@
223 public Background (Cairo.Surface target_surface)
224 {
225 this.target_surface = target_surface;
226- timer = new AnimateTimer (AnimateTimer.INSTANT);
227+ timer = new AnimateTimer (AnimateTimer.ease_in_out, 700);
228 timer.animate.connect (animate_cb);
229
230 loaders = new HashTable<string?, BackgroundLoader> (str_hash, str_equal);
231@@ -426,6 +426,7 @@
232 {
233 old = current;
234 current = new_background;
235+ timer.stop ();
236 }
237
238 queue_draw ();
239
240=== added file 'src/cached-image.vala'
241--- src/cached-image.vala 1970-01-01 00:00:00 +0000
242+++ src/cached-image.vala 2012-03-13 14:51:31 +0000
243@@ -0,0 +1,56 @@
244+/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*-
245+ *
246+ * Copyright (C) 2012 Canonical Ltd
247+ *
248+ * This program is free software: you can redistribute it and/or modify
249+ * it under the terms of the GNU General Public License version 3 as
250+ * published by the Free Software Foundation.
251+ *
252+ * This program is distributed in the hope that it will be useful,
253+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
254+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
255+ * GNU General Public License for more details.
256+ *
257+ * You should have received a copy of the GNU General Public License
258+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
259+ *
260+ * Authors: Michael Terry <michael.terry@canonical.com>
261+ */
262+
263+public class CachedImage : Gtk.Image
264+{
265+ private static HashTable<Gdk.Pixbuf, Cairo.Surface> surface_table;
266+
267+ public static Cairo.Surface? get_cached_surface (Cairo.Context c, Gdk.Pixbuf pixbuf)
268+ {
269+ if (surface_table == null)
270+ surface_table = new HashTable<Gdk.Pixbuf, Cairo.Surface> (direct_hash, direct_equal);
271+
272+ var surface = surface_table.lookup (pixbuf);
273+ if (surface == null)
274+ {
275+ surface = new Cairo.Surface.similar (c.get_target (), Cairo.Content.COLOR_ALPHA, pixbuf.width, pixbuf.height);
276+ var new_c = new Cairo.Context (surface);
277+ Gdk.cairo_set_source_pixbuf (new_c, pixbuf, 0, 0);
278+ new_c.paint ();
279+ surface_table.insert (pixbuf, surface);
280+ }
281+ return surface;
282+ }
283+
284+ public CachedImage (Gdk.Pixbuf? pixbuf)
285+ {
286+ Object (pixbuf: pixbuf);
287+ }
288+
289+ public override bool draw (Cairo.Context c)
290+ {
291+ var cached_surface = get_cached_surface (c, pixbuf);
292+ if (cached_surface != null)
293+ {
294+ c.set_source_surface (cached_surface, 0, 0);
295+ c.paint ();
296+ }
297+ return false;
298+ }
299+}
300
301=== modified file 'src/dash-button.vala'
302--- src/dash-button.vala 2012-03-09 05:12:16 +0000
303+++ src/dash-button.vala 2012-03-13 14:51:31 +0000
304@@ -54,9 +54,18 @@
305 this.text = text;
306
307 // add chevron
308- var image = new Gtk.Image.from_file (Path.build_filename (Config.PKGDATADIR, "arrow_right.png", null));
309- sizes.add_widget (image);
310- hbox.add (image);
311+ var path = Path.build_filename (Config.PKGDATADIR, "arrow_right.png", null);
312+ try
313+ {
314+ var pixbuf = new Gdk.Pixbuf.from_file (path);
315+ var image = new CachedImage (pixbuf);
316+ sizes.add_widget (image);
317+ hbox.add (image);
318+ }
319+ catch (Error e)
320+ {
321+ debug ("Error loading image %s: %s", path, e.message);
322+ }
323
324 hbox.show_all ();
325 add (hbox);
326
327=== modified file 'src/fadable.vala'
328--- src/fadable.vala 2012-02-21 17:31:59 +0000
329+++ src/fadable.vala 2012-03-13 14:51:31 +0000
330@@ -48,7 +48,7 @@
331
332 construct
333 {
334- timer = new AnimateTimer (AnimateTimer.INSTANT);
335+ timer = new AnimateTimer (AnimateTimer.ease_out_quint, AnimateTimer.INSTANT);
336 timer.animate.connect (animate_cb);
337 }
338
339
340=== modified file 'src/main-window.vala'
341--- src/main-window.vala 2012-03-09 05:00:29 +0000
342+++ src/main-window.vala 2012-03-13 14:51:31 +0000
343@@ -64,7 +64,10 @@
344
345 user_list = new UserList (background, menubar);
346 user_list.expand = true;
347- user_list.user_displayed.connect (() => {
348+ user_list.user_displayed_start.connect (() => {
349+ change_background ();
350+ });
351+ user_list.user_displayed_done.connect (() => {
352 menubar.set_layouts (user_list.selected_entry.keyboard_layouts);
353 change_background ();
354 });
355@@ -145,12 +148,10 @@
356
357 private void change_background ()
358 {
359- /* Set background after user stops scrolling */
360 if (background.current_background != null)
361 {
362- if (change_background_timeout != 0)
363- Source.remove (change_background_timeout);
364- change_background_timeout = Timeout.add (200, change_background_timeout_cb);
365+ if (change_background_timeout == 0)
366+ change_background_timeout = Idle.add (change_background_timeout_cb);
367 }
368 else
369 change_background_timeout_cb ();
370
371=== modified file 'src/session-chooser.vala'
372--- src/session-chooser.vala 2012-02-23 14:46:37 +0000
373+++ src/session-chooser.vala 2012-03-13 14:51:31 +0000
374@@ -80,7 +80,7 @@
375 var pixbuf = get_badge (key);
376 if (pixbuf != null)
377 {
378- var image = new Gtk.Image.from_pixbuf (pixbuf);
379+ var image = new CachedImage (pixbuf);
380 hbox.pack_start (image, false, false, 0);
381 }
382
383
384=== modified file 'src/user-list.vala'
385--- src/user-list.vala 2012-03-13 13:47:33 +0000
386+++ src/user-list.vala 2012-03-13 14:51:31 +0000
387@@ -45,6 +45,10 @@
388
389 /* Default session for this user */
390 public string session;
391+
392+ /* Cached cairo surfaces */
393+ public Cairo.Surface label_in_box_surface;
394+ public Cairo.Surface label_out_of_box_surface;
395 }
396
397 private class AuthenticationMessage
398@@ -113,7 +117,7 @@
399 private DashButton login_button;
400 private Fadable prompt_widget_to_show;
401 private Gtk.Button session_button;
402- private Gtk.Image session_image;
403+ private CachedImage session_image;
404 private SessionChooser session_chooser;
405
406 private enum Mode
407@@ -153,7 +157,8 @@
408 }
409
410 public signal void user_selected (string? username);
411- public signal void user_displayed ();
412+ public signal void user_displayed_start ();
413+ public signal void user_displayed_done ();
414 public signal void respond_to_prompt (string text);
415 public signal void start_session ();
416
417@@ -235,7 +240,7 @@
418 session_button = new Gtk.Button ();
419 session_button.focus_on_click = false;
420 session_button.get_accessible ().set_name (_("Session Options"));
421- session_image = new Gtk.Image.from_pixbuf (get_badge ());
422+ session_image = new CachedImage (get_badge ());
423 session_image.show ();
424 session_button.relief = Gtk.ReliefStyle.NONE;
425 session_button.add (session_image);
426@@ -243,7 +248,7 @@
427 session_button.show ();
428 add_with_class (session_button);
429
430- scroll_timer = new AnimateTimer (AnimateTimer.FAST);
431+ scroll_timer = new AnimateTimer (AnimateTimer.ease_out_quint, AnimateTimer.FAST);
432 scroll_timer.animate.connect (scroll_animate_cb);
433 }
434
435@@ -659,6 +664,7 @@
436 {
437 prompt_widget_to_show.fade_in ();
438 prompt_widget_to_show = null;
439+ user_displayed_start ();
440 }
441
442 /* Stop when we get there */
443@@ -669,7 +675,7 @@
444 private void finished_scrolling ()
445 {
446 session_button.show ();
447- user_displayed ();
448+ user_displayed_done ();
449 mode = Mode.LOGIN;
450 }
451
452@@ -687,27 +693,15 @@
453
454 if (scroll_target_location != entries.index (entry))
455 {
456- var old_target = scroll_target_location;
457 var new_target = entries.index (entry);
458 var new_direction = direction;
459- var new_start = (mode == Mode.SCROLLING) ? scroll_start_location : scroll_location;
460-
461- if (mode == Mode.SCROLLING && scroll_direction != new_direction)
462- return; /* ignore requests when we're already scrolling the opposite way */
463+ var new_start = scroll_location;
464
465 if (scroll_location != new_target)
466 {
467 var new_distance = new_direction * (new_target - new_start);
468- if (mode == Mode.SCROLLING)
469- {
470- var old_distance = new_direction * (old_target - new_start);
471- if (!scroll_timer.extend ((new_distance - old_distance) / old_distance))
472- return;
473- }
474- else if (new_distance > 1)
475- scroll_timer.reset (600);
476- else
477- scroll_timer.reset (400);
478+ // Base rate is 350 (250 + 100). If we find ourselves going further, slow down animation
479+ scroll_timer.reset (250 + int.min((int)(100 * (new_distance)), 500));
480
481 if (prompt_entry.visible)
482 prompt_widget_to_show = prompt_entry;
483@@ -733,7 +727,7 @@
484 user_selected (selected_entry.name);
485
486 if (mode == Mode.LOGIN)
487- user_displayed (); /* didn't need to move, make sure we trigger side effects */
488+ user_displayed_done (); /* didn't need to move, make sure we trigger side effects */
489 }
490 }
491
492@@ -828,6 +822,47 @@
493 session_button.size_allocate (child_allocation);
494 }
495
496+ private Cairo.Surface entry_ensure_label_surface (UserEntry entry, Cairo.Context orig_c, bool in_box)
497+ {
498+ if (in_box && entry.label_in_box_surface != null)
499+ return entry.label_in_box_surface;
500+ else if (!in_box && entry.label_out_of_box_surface != null)
501+ return entry.label_out_of_box_surface;
502+
503+ int w, h;
504+ entry.layout.get_pixel_size (out w, out h);
505+
506+ var bw = (box_width - (in_box ? 1.5 : 0.5)) * grid_size;
507+
508+ var surface = new Cairo.Surface.similar (orig_c.get_target (), Cairo.Content.COLOR_ALPHA, (int)(bw+1), h);
509+ var c = new Cairo.Context (surface);
510+
511+ if (w > bw)
512+ {
513+ var mask = new Cairo.Pattern.linear (0, 0, bw, 0);
514+ if (in_box)
515+ {
516+ mask.add_color_stop_rgba (1.0 - 27.0 / bw, 1.0, 1.0, 1.0, 1.0);
517+ mask.add_color_stop_rgba (1.0 - 21.6 / bw, 1.0, 1.0, 1.0, 0.5);
518+ }
519+ else
520+ mask.add_color_stop_rgba (1.0 - 64.0 / bw, 1.0, 1.0, 1.0, 1.0);
521+ mask.add_color_stop_rgba (1.0, 1.0, 1.0, 1.0, 0.0);
522+ c.set_source (mask);
523+ }
524+ else
525+ c.set_source_rgba (1.0, 1.0, 1.0, 1.0);
526+
527+ Pango.cairo_show_layout (c, entry.layout);
528+
529+ if (in_box)
530+ entry.label_in_box_surface = surface;
531+ else
532+ entry.label_out_of_box_surface = surface;
533+
534+ return surface;
535+ }
536+
537 private void draw_entry (Cairo.Context c, UserEntry entry, double alpha = 0.5, bool in_box = false, Gdk.Pixbuf? badge = null)
538 {
539 c.save ();
540@@ -848,32 +883,18 @@
541 int w, h;
542 entry.layout.get_pixel_size (out w, out h);
543
544- /* Trim label if too wide */
545- var bw = (box_width - (in_box ? 1.1 : 0.5)) * grid_size;
546- if (w > bw)
547- {
548- var mask = new Cairo.Pattern.linear (0, 0, bw, 0);
549- if (in_box)
550- {
551- mask.add_color_stop_rgba (1.0 - 27.0 / bw, 1.0, 1.0, 1.0, 1.0);
552- mask.add_color_stop_rgba (1.0 - 21.6 / bw, 1.0, 1.0, 1.0, 0.5);
553- }
554- else
555- mask.add_color_stop_rgba (1.0 - 64.0 / bw, 1.0, 1.0, 1.0, alpha);
556- mask.add_color_stop_rgba (1.0, 1.0, 1.0, 1.0, 0.0);
557- c.set_source (mask);
558- }
559- else
560- c.set_source_rgba (1.0, 1.0, 1.0, alpha);
561-
562- c.translate (grid_size / 2, (grid_size - h) / 2 + border);
563- c.move_to (0, 0);
564- Pango.cairo_show_layout (c, entry.layout);
565-
566- if (entry.has_messages && (!in_box || w + 6 + message_pixbuf.get_width () < bw))
567- {
568- c.translate (w + 6, (h - message_pixbuf.get_height ()) / 2);
569- Gdk.cairo_set_source_pixbuf (c, message_pixbuf, 0, 0);
570+ var label_x = grid_size / 2;
571+ var label_y = (grid_size - h) / 2 + border;
572+ var label_surface = entry_ensure_label_surface (entry, c, in_box);
573+ c.set_source_surface (label_surface, label_x, label_y);
574+ c.paint_with_alpha (alpha);
575+
576+ var bw = (int) ((box_width - (in_box ? 1.5 : 0.5)) * grid_size);
577+ if (entry.has_messages && (!in_box || label_x + w + 6 + message_pixbuf.get_width () < bw))
578+ {
579+ c.translate (label_x + w + 6, label_y + (h - message_pixbuf.get_height ()) / 2);
580+ var surface = CachedImage.get_cached_surface (c, message_pixbuf);
581+ c.set_source_surface (surface, 0, 0);
582 c.paint_with_alpha (alpha);
583 }
584
585@@ -887,7 +908,8 @@
586 /* FIXME: The 18px offset here is because the visual assets changed size from 40px to 22px. It should be fixed properly somewhere... */
587 var ypadding = (grid_size - badge.height) / 2 - 18;
588 c.translate (box_width * grid_size - grid_size - grid_size / 4 + xpadding, grid_size / 4 - ypadding - border);
589- Gdk.cairo_set_source_pixbuf (c, badge, 0, 0);
590+ var surface = CachedImage.get_cached_surface (c, badge);
591+ c.set_source_surface (surface, 0, 0);
592 c.paint ();
593 c.restore ();
594 }
595@@ -954,7 +976,7 @@
596 var position = index - scroll_location;
597
598 /* Draw entries above the box */
599- if (where == NameLocation.OUTSIDE_BOX && position < 0)
600+ if (where == NameLocation.OUTSIDE_BOX && position < 0 && position > -1 * (int)(n_above + 1))
601 {
602 var h_above = (double) (n_above + 1) * grid_size;
603 c.save ();
604@@ -987,7 +1009,7 @@
605 }
606
607 /* Draw entries below the box */
608- if (where == NameLocation.OUTSIDE_BOX && position > 0)
609+ if (where == NameLocation.OUTSIDE_BOX && position > 0 && position < n_below + 1)
610 {
611 var h_below = (double) (n_below + 1) * grid_size;
612 c.save ();

Subscribers

People subscribed via source and target branches