[gnome-break-timer] Add animated transitions to the CircleCounter widget
- From: Dylan McCall <dylanmccall src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-break-timer] Add animated transitions to the CircleCounter widget
- Date: Mon, 23 Nov 2020 07:56:22 +0000 (UTC)
commit b4111b21d158e8a20febd1a358e724a4d776e2c8
Author: Dylan McCall <dylan dylanmccall ca>
Date: Sun Nov 22 23:52:54 2020 -0800
Add animated transitions to the CircleCounter widget
src/settings/meson.build | 3 +-
src/settings/widgets/CircleCounter.vala | 88 ++++++++++++++----
src/settings/widgets/Transition.vala | 159 ++++++++++++++++++++++++++++++++
3 files changed, 232 insertions(+), 18 deletions(-)
---
diff --git a/src/settings/meson.build b/src/settings/meson.build
index 38debc1..f4c5f3f 100644
--- a/src/settings/meson.build
+++ b/src/settings/meson.build
@@ -24,7 +24,8 @@ settings_lib_sources = files(
'widgets/CircleCounter.vala',
'widgets/FixedSizeGrid.vala',
'widgets/OverlayArrow.vala',
- 'widgets/TimeChooser.vala'
+ 'widgets/TimeChooser.vala',
+ 'widgets/Transition.vala'
)
settings_lib_dependencies = [
diff --git a/src/settings/widgets/CircleCounter.vala b/src/settings/widgets/CircleCounter.vala
index 9ce5dfb..c4745a6 100644
--- a/src/settings/widgets/CircleCounter.vala
+++ b/src/settings/widgets/CircleCounter.vala
@@ -26,7 +26,17 @@ public class CircleCounter : Gtk.Widget {
protected const double LINE_WIDTH = 5.0;
protected const int DEFAULT_RADIUS = 48;
+ /* 10 seconds in microseconds */
+ private const int64 FULL_ANIM_TIME = (int64) (10000000 / (Math.PI * 2));
+
+ /* 10 ms in microseconds */
+ private const int64 MIN_ANIM_DURATION = 10000;
+
+ /* 500 ms in microseconds */
+ private const int64 MAX_ANIM_DURATION = 500000;
+
private const double SNAP_INCREMENT = (Math.PI * 2) / 60.0;
+ private const double BASE_ANGLE = 1.5 * Math.PI;
public enum Direction {
COUNT_DOWN,
@@ -39,12 +49,18 @@ public class CircleCounter : Gtk.Widget {
* COUNT_UP: a circle gradually appears as progress increases
*/
public Direction direction {get; set;}
+
/**
* A value from 0.0 to 1.0, where 1.0 means the count is finished. The
* circle will be filled by this amount according to the direction
* property.
*/
- public double progress {get; set;}
+ public double progress {set; get;}
+ public double draw_angle {set; get;}
+
+ private bool first_frame = true;
+
+ private PropertyTransition progress_transition;
public CircleCounter () {
GLib.Object ();
@@ -53,14 +69,57 @@ public class CircleCounter : Gtk.Widget {
this.get_style_context ().add_class ("_circle-counter");
- this.notify["progress"].connect((s, p) => {
- this.queue_draw ();
- });
+ this.progress_transition = new PropertyTransition (
+ this, "draw-angle", PropertyTransition.calculate_value_double
+ );
+
+ this.map.connect (this.on_map_cb);
+ this.draw.connect (this.on_draw_cb);
+ this.notify["progress"].connect (this.on_progress_notify_cb);
+ this.notify["draw-angle"].connect (this.on_draw_angle_notify_cb);
}
- // TODO: Animate between states <3
+ private void on_progress_notify_cb () {
+ double progress_angle = this.get_progress_angle ();
+
+ if (this.first_frame) {
+ this.progress_transition.skip (progress_angle);
+ this.first_frame = false;
+ return;
+ }
- public override bool draw (Cairo.Context cr) {
+ // Animate at a consistent speed regardless of the distance covered.
+ double change = (progress_angle - this.draw_angle).abs ();
+ int64 duration = int64.min(
+ (int64) (change * FULL_ANIM_TIME),
+ MAX_ANIM_DURATION
+ );
+
+ if (duration < MIN_ANIM_DURATION) {
+ this.progress_transition.skip (progress_angle);
+ } else {
+ this.progress_transition.start (progress_angle, EASE_OUT_CUBIC, duration);
+ }
+ }
+
+ private void on_draw_angle_notify_cb () {
+ // TODO: Only redraw if the value has changed enough to be visible.
+ // This will need a value set from the draw function.
+ GLib.info ("Draw angle %s", this.draw_angle.to_string ());
+ this.queue_draw ();
+ }
+
+ private double get_progress_angle () {
+ double result = (this.progress * Math.PI * 2.0) % (Math.PI * 2.0);
+ int snap_count = (int) (result / SNAP_INCREMENT);
+ return (double) snap_count * SNAP_INCREMENT;
+ }
+
+ private void on_map_cb () {
+ this.first_frame = true;
+ }
+
+ private bool on_draw_cb (Cairo.Context cr) {
Gtk.StyleContext style_context = this.get_style_context ();
Gtk.StateFlags state = this.get_state_flags ();
Gtk.Allocation allocation;
@@ -83,28 +142,23 @@ public class CircleCounter : Gtk.Widget {
cr.pop_group_to_source ();
cr.paint_with_alpha (0.3);
- double start_angle = 1.5 * Math.PI;
- double progress_angle = this.progress * Math.PI * 2.0;
- int snap_count = (int) (progress_angle / SNAP_INCREMENT);
- progress_angle = snap_count * SNAP_INCREMENT;
-
if (this.direction == Direction.COUNT_DOWN) {
- if (progress_angle > 0) {
- cr.arc (center_x, center_y, arc_radius, start_angle, start_angle - progress_angle);
+ if (this.draw_angle > 0) {
+ cr.arc (center_x, center_y, arc_radius, BASE_ANGLE, BASE_ANGLE - this.draw_angle);
} else {
// No progress: Draw a full circle (to be gradually emptied)
- cr.arc (center_x, center_y, arc_radius, start_angle, start_angle + Math.PI * 2.0);
+ cr.arc (center_x, center_y, arc_radius, BASE_ANGLE, BASE_ANGLE + Math.PI * 2.0);
}
} else {
- if (progress_angle > 0) {
- cr.arc_negative (center_x, center_y, arc_radius, start_angle, start_angle - progress_angle);
+ if (this.draw_angle > 0) {
+ cr.arc_negative (center_x, center_y, arc_radius, BASE_ANGLE, BASE_ANGLE - this.draw_angle);
}
// No progress: Draw nothing (arc will gradually appear)
}
Gdk.cairo_set_source_rgba (cr, foreground_color);
cr.set_line_width (LINE_WIDTH);
- cr.set_line_cap (Cairo.LineCap.ROUND);
+ cr.set_line_cap (Cairo.LineCap.SQUARE);
cr.stroke ();
return true;
diff --git a/src/settings/widgets/Transition.vala b/src/settings/widgets/Transition.vala
new file mode 100644
index 0000000..9f8c084
--- /dev/null
+++ b/src/settings/widgets/Transition.vala
@@ -0,0 +1,159 @@
+/*
+ * This file is part of GNOME Break Timer.
+ *
+ * GNOME Break Timer 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GNOME Break Timer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GNOME Break Timer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace BreakTimer.Settings.Widgets {
+
+// TODO: I'm a little surprised I had to write this and I may be missing
+// something important that already exists.
+
+/**
+ * Transition utility designed for Gtk.Widget's tick callback mechanism. Create
+ * an instance of this class with a particular output property for intermediate
+ * states, as well as a function to compute the value of that property given a
+ * start and end value and a easing ratio between the two.
+ */
+public class PropertyTransition : GLib.Object {
+ public delegate GLib.Value CalculateValue (GLib.Value start_value, GLib.Value end_value, double ease);
+
+ public enum EasingFunction {
+ LINEAR,
+ EASE_OUT_CUBIC;
+
+ public double calculate (double time) {
+ switch (this) {
+ case LINEAR:
+ return this.linear (time);
+ case EASE_OUT_CUBIC:
+ return this.ease_out_cubic (time);
+ default:
+ GLib.assert_not_reached ();
+ }
+ }
+
+ private double linear (double time) {
+ return time;
+ }
+
+ /*
+ * From clutter-easing.c, based on Robert Penner's easing equations, MIT
+ * license.
+ */
+ private double ease_out_cubic (double time) {
+ double ease = time - 1;
+ return ease * ease * ease + 1;
+ }
+ }
+
+ private Gtk.Widget widget;
+ private string property_name;
+ private unowned CalculateValue calculate_value;
+
+ private GLib.Type property_type;
+
+ private EasingFunction easing_function;
+ private GLib.Value start_value;
+ private GLib.Value target_value;
+ private int64 start_frame_time;
+ private int64 end_frame_time;
+
+ private uint tick_callback_id;
+
+ public PropertyTransition (Gtk.Widget widget, string property_name, CalculateValue calculate_value) {
+ this.widget = widget;
+ this.property_name = property_name;
+ this.calculate_value = calculate_value;
+
+ GLib.ParamSpec? property_paramspec = widget.get_class ().find_property (property_name);
+ GLib.assert_nonnull (property_paramspec);
+ this.property_type = property_paramspec.value_type;
+
+ this.tick_callback_id = 0;
+ }
+
+ public bool start (GLib.Value target_value, EasingFunction easing_function, int64 duration_microseconds)
{
+ GLib.warn_if_fail (target_value.type () == this.get_target_property ().type ());
+
+ Gdk.FrameClock? frame_clock = this.widget.get_frame_clock ();
+
+ if (frame_clock == null) {
+ return this.skip (target_value);
+ }
+
+ this.target_value = target_value;
+
+ this.easing_function = easing_function;
+ this.start_frame_time = frame_clock.get_frame_time ();
+ this.end_frame_time = this.start_frame_time + duration_microseconds;
+ this.start_value = this.get_target_property ();
+
+ if (this.tick_callback_id == 0) {
+ this.tick_callback_id = this.widget.add_tick_callback (this.tick_callback);
+ }
+
+ return true;
+ }
+
+ public bool skip (GLib.Value target_value) {
+ this.set_target_property (target_value);
+ return true;
+ }
+
+ private GLib.Value get_target_property () {
+ GLib.Value result = GLib.Value (this.property_type);
+ this.widget.get_property (this.property_name, ref result);
+ return result;
+ }
+
+ private void set_target_property (GLib.Value target_value) {
+ this.widget.set_property (this.property_name, target_value);
+ }
+
+ private bool tick_callback (Gtk.Widget widget, Gdk.FrameClock frame_clock) {
+ int64 now = frame_clock.get_frame_time ();
+ bool is_complete = this.set_frame (now);
+ if (is_complete) {
+ this.tick_callback_id = 0;
+ return GLib.Source.REMOVE;
+ } else {
+ return GLib.Source.CONTINUE;
+ }
+ }
+
+ private bool set_frame (int64 frame_time) {
+ bool is_complete = frame_time >= this.end_frame_time;
+
+ if (is_complete) {
+ frame_time = this.end_frame_time;
+ }
+
+ int64 time_delta = frame_time - this.start_frame_time;
+ int64 time_total = this.end_frame_time - this.start_frame_time;
+ double ease = this.easing_function.calculate ((double) time_delta / time_total);
+
+ this.set_target_property (
+ this.calculate_value (this.start_value, this.target_value, ease)
+ );
+
+ return is_complete;
+ }
+
+ public static GLib.Value calculate_value_double (GLib.Value start_value, GLib.Value end_value, double
ease) {
+ return start_value.get_double () + ease * (end_value.get_double () - start_value.get_double ());
+ }
+}
+
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]