1 /* GTK - The GIMP Toolkit
2  * Copyright (C) 2014 Lieven van der Heide
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "config.h"
19 #include "gtkkineticscrollingprivate.h"
20 
21 #include <math.h>
22 #include <stdio.h>
23 
24 /*
25  * All our curves are second degree linear differential equations, and
26  * so they can always be written as linear combinations of 2 base
27  * solutions. c1 and c2 are the coefficients to these two base solutions,
28  * and are computed from the initial position and velocity.
29  *
30  * In the case of simple deceleration, the differential equation is
31  *
32  *   y'' = -my'
33  *
34  * With m the resistance factor. For this we use the following 2
35  * base solutions:
36  *
37  *   f1(x) = 1
38  *   f2(x) = exp(-mx)
39  *
40  * In the case of overshoot, the differential equation is
41  *
42  *   y'' = -my' - ky
43  *
44  * With m the resistance, and k the spring stiffness constant. We let
45  * k = m^2 / 4, so that the system is critically damped (ie, returns to its
46  * equilibrium position as quickly as possible, without oscillating), and offset
47  * the whole thing, such that the equilibrium position is at 0. This gives the
48  * base solutions
49  *
50  *   f1(x) = exp(-mx / 2)
51  *   f2(x) = t exp(-mx / 2)
52 */
53 
54 typedef enum {
55   GTK_KINETIC_SCROLLING_PHASE_DECELERATING,
56   GTK_KINETIC_SCROLLING_PHASE_OVERSHOOTING,
57   GTK_KINETIC_SCROLLING_PHASE_FINISHED,
58 } GtkKineticScrollingPhase;
59 
60 struct _GtkKineticScrolling
61 {
62   GtkKineticScrollingPhase phase;
63   double lower;
64   double upper;
65   double overshoot_width;
66   double decel_friction;
67   double overshoot_friction;
68 
69   double c1;
70   double c2;
71   double equilibrium_position;
72 
73   double t;
74   double position;
75   double velocity;
76 };
77 
78 static void gtk_kinetic_scrolling_init_overshoot (GtkKineticScrolling *data,
79                                                   double               equilibrium_position,
80                                                   double               initial_position,
81                                                   double               initial_velocity);
82 
83 GtkKineticScrolling *
gtk_kinetic_scrolling_new(double lower,double upper,double overshoot_width,double decel_friction,double overshoot_friction,double initial_position,double initial_velocity)84 gtk_kinetic_scrolling_new (double lower,
85                            double upper,
86                            double overshoot_width,
87                            double decel_friction,
88                            double overshoot_friction,
89                            double initial_position,
90                            double initial_velocity)
91 {
92   GtkKineticScrolling *data;
93 
94   data = g_slice_new0 (GtkKineticScrolling);
95   data->lower = lower;
96   data->upper = upper;
97   data->decel_friction = decel_friction;
98   data->overshoot_friction = overshoot_friction;
99   if(initial_position < lower)
100     {
101       gtk_kinetic_scrolling_init_overshoot (data,
102                                             lower,
103                                             initial_position,
104                                             initial_velocity);
105     }
106   else if(initial_position > upper)
107     {
108       gtk_kinetic_scrolling_init_overshoot (data,
109                                             upper,
110                                             initial_position,
111                                             initial_velocity);
112     }
113   else
114     {
115       data->phase = GTK_KINETIC_SCROLLING_PHASE_DECELERATING;
116       data->c1 = initial_velocity / decel_friction + initial_position;
117       data->c2 = -initial_velocity / decel_friction;
118       data->t = 0;
119       data->position = initial_position;
120       data->velocity = initial_velocity;
121     }
122 
123   return data;
124 }
125 
126 GtkKineticScrollingChange
gtk_kinetic_scrolling_update_size(GtkKineticScrolling * data,double lower,double upper)127 gtk_kinetic_scrolling_update_size (GtkKineticScrolling *data,
128                                    double               lower,
129                                    double               upper)
130 {
131   GtkKineticScrollingChange change = GTK_KINETIC_SCROLLING_CHANGE_NONE;
132 
133   if (lower != data->lower)
134     {
135       if (data->position <= lower)
136         change |= GTK_KINETIC_SCROLLING_CHANGE_LOWER;
137 
138       data->lower = lower;
139     }
140 
141   if (upper != data->upper)
142     {
143       if (data->position >= data->upper)
144         change |= GTK_KINETIC_SCROLLING_CHANGE_UPPER;
145 
146       data->upper = upper;
147     }
148 
149   if (data->phase == GTK_KINETIC_SCROLLING_PHASE_OVERSHOOTING)
150     change |= GTK_KINETIC_SCROLLING_CHANGE_IN_OVERSHOOT;
151 
152   return change;
153 }
154 
155 void
gtk_kinetic_scrolling_free(GtkKineticScrolling * kinetic)156 gtk_kinetic_scrolling_free (GtkKineticScrolling *kinetic)
157 {
158   g_slice_free (GtkKineticScrolling, kinetic);
159 }
160 
161 static void
gtk_kinetic_scrolling_init_overshoot(GtkKineticScrolling * data,double equilibrium_position,double initial_position,double initial_velocity)162 gtk_kinetic_scrolling_init_overshoot (GtkKineticScrolling *data,
163                                       double               equilibrium_position,
164                                       double               initial_position,
165                                       double               initial_velocity)
166 {
167   data->phase = GTK_KINETIC_SCROLLING_PHASE_OVERSHOOTING;
168   data->equilibrium_position = equilibrium_position;
169   data->c1 = initial_position - equilibrium_position;
170   data->c2 = initial_velocity + data->overshoot_friction / 2 * data->c1;
171   data->t = 0;
172 }
173 
174 gboolean
gtk_kinetic_scrolling_tick(GtkKineticScrolling * data,double time_delta,double * position,double * velocity)175 gtk_kinetic_scrolling_tick (GtkKineticScrolling *data,
176                             double               time_delta,
177                             double              *position,
178                             double              *velocity)
179 {
180   switch(data->phase)
181     {
182     case GTK_KINETIC_SCROLLING_PHASE_DECELERATING:
183       {
184         double last_position = data->position;
185         double last_time = data->t;
186         double exp_part;
187 
188         data->t += time_delta;
189 
190         exp_part = exp (-data->decel_friction * data->t);
191         data->position = data->c1 + data->c2 * exp_part;
192         data->velocity = -data->decel_friction * data->c2 * exp_part;
193 
194         if(data->position < data->lower)
195           {
196             gtk_kinetic_scrolling_init_overshoot(data,data->lower,data->position,data->velocity);
197           }
198         else if (data->position > data->upper)
199           {
200             gtk_kinetic_scrolling_init_overshoot(data, data->upper, data->position, data->velocity);
201           }
202         else if (fabs(data->velocity) < 1 ||
203                  (last_time != 0.0 && fabs(data->position - last_position) < 1))
204           {
205             data->phase = GTK_KINETIC_SCROLLING_PHASE_FINISHED;
206             data->position = round(data->position);
207             data->velocity = 0;
208           }
209         break;
210       }
211 
212     case GTK_KINETIC_SCROLLING_PHASE_OVERSHOOTING:
213       {
214         double exp_part, pos;
215 
216         data->t += time_delta;
217         exp_part = exp(-data->overshoot_friction / 2 * data->t);
218         pos = exp_part * (data->c1 + data->c2 * data->t);
219 
220         if (pos < data->lower - 50 || pos > data->upper + 50)
221           {
222             pos = CLAMP (pos, data->lower - 50, data->upper + 50);
223             gtk_kinetic_scrolling_init_overshoot (data, data->equilibrium_position, pos, 0);
224           }
225         else
226           data->velocity = data->c2 * exp_part - data->overshoot_friction / 2 * pos;
227 
228         data->position = pos + data->equilibrium_position;
229 
230         if(fabs (pos) < 0.1)
231           {
232             data->phase = GTK_KINETIC_SCROLLING_PHASE_FINISHED;
233             data->position = data->equilibrium_position;
234             data->velocity = 0;
235           }
236         break;
237       }
238 
239     case GTK_KINETIC_SCROLLING_PHASE_FINISHED:
240     default:
241       break;
242     }
243 
244   if (position)
245     *position = data->position;
246   if (velocity)
247     *velocity = data->velocity;
248 
249   return data->phase != GTK_KINETIC_SCROLLING_PHASE_FINISHED;
250 }
251 
252