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 "gtkkineticscrolling.h"
20 
21 #include <stdio.h>
22 
23 #include "fallback-c89.c"
24 
25 /*
26  * All our curves are second degree linear differential equations, and
27  * so they can always be written as linear combinations of 2 base
28  * solutions. c1 and c2 are the coefficients to these two base solutions,
29  * and are computed from the initial position and velocity.
30  *
31  * In the case of simple deceleration, the differential equation is
32  *
33  *   y'' = -my'
34  *
35  * With m the resistence factor. For this we use the following 2
36  * base solutions:
37  *
38  *   f1(x) = 1
39  *   f2(x) = exp(-mx)
40  *
41  * In the case of overshoot, the differential equation is
42  *
43  *   y'' = -my' - ky
44  *
45  * With m the resistance, and k the spring stiffness constant. We let
46  * k = m^2 / 4, so that the system is critically damped (ie, returns to its
47  * equilibrium position as quickly as possible, without oscillating), and offset
48  * the whole thing, such that the equilibrium position is at 0. This gives the
49  * base solutions
50  *
51  *   f1(x) = exp(-mx / 2)
52  *   f2(x) = t exp(-mx / 2)
53 */
54 
55 typedef enum {
56   GTK_KINETIC_SCROLLING_PHASE_DECELERATING,
57   GTK_KINETIC_SCROLLING_PHASE_OVERSHOOTING,
58   GTK_KINETIC_SCROLLING_PHASE_FINISHED,
59 } GtkKineticScrollingPhase;
60 
61 struct _GtkKineticScrolling
62 {
63   GtkKineticScrollingPhase phase;
64   gdouble lower;
65   gdouble upper;
66   gdouble overshoot_width;
67   gdouble decel_friction;
68   gdouble overshoot_friction;
69 
70   gdouble c1;
71   gdouble c2;
72   gdouble equilibrium_position;
73 
74   gdouble t;
75   gdouble position;
76   gdouble velocity;
77 };
78 
79 static void gtk_kinetic_scrolling_init_overshoot (GtkKineticScrolling *data,
80                                                   gdouble              equilibrium_position,
81                                                   gdouble              initial_position,
82                                                   gdouble              initial_velocity);
83 
84 GtkKineticScrolling *
gtk_kinetic_scrolling_new(gdouble lower,gdouble upper,gdouble overshoot_width,gdouble decel_friction,gdouble overshoot_friction,gdouble initial_position,gdouble initial_velocity)85 gtk_kinetic_scrolling_new (gdouble lower,
86                            gdouble upper,
87                            gdouble overshoot_width,
88                            gdouble decel_friction,
89                            gdouble overshoot_friction,
90                            gdouble initial_position,
91                            gdouble initial_velocity)
92 {
93   GtkKineticScrolling *data;
94 
95   data = g_slice_new0 (GtkKineticScrolling);
96   data->lower = lower;
97   data->upper = upper;
98   data->decel_friction = decel_friction;
99   data->overshoot_friction = overshoot_friction;
100   if(initial_position < lower)
101     {
102       gtk_kinetic_scrolling_init_overshoot (data,
103                                             lower,
104                                             initial_position,
105                                             initial_velocity);
106     }
107   else if(initial_position > upper)
108     {
109       gtk_kinetic_scrolling_init_overshoot (data,
110                                             upper,
111                                             initial_position,
112                                             initial_velocity);
113     }
114   else
115     {
116       data->phase = GTK_KINETIC_SCROLLING_PHASE_DECELERATING;
117       data->c1 = initial_velocity / decel_friction + initial_position;
118       data->c2 = -initial_velocity / decel_friction;
119       data->t = 0;
120       data->position = initial_position;
121       data->velocity = initial_velocity;
122     }
123 
124   return data;
125 }
126 
127 GtkKineticScrollingChange
gtk_kinetic_scrolling_update_size(GtkKineticScrolling * data,gdouble lower,gdouble upper)128 gtk_kinetic_scrolling_update_size (GtkKineticScrolling *data,
129                                    gdouble              lower,
130                                    gdouble              upper)
131 {
132   GtkKineticScrollingChange change = GTK_KINETIC_SCROLLING_CHANGE_NONE;
133 
134   if (lower != data->lower)
135     {
136       if (data->position <= lower)
137         change |= GTK_KINETIC_SCROLLING_CHANGE_LOWER;
138 
139       data->lower = lower;
140     }
141 
142   if (upper != data->upper)
143     {
144       if (data->position >= data->upper)
145         change |= GTK_KINETIC_SCROLLING_CHANGE_UPPER;
146 
147       data->upper = upper;
148     }
149 
150   if (data->phase == GTK_KINETIC_SCROLLING_PHASE_OVERSHOOTING)
151     change |= GTK_KINETIC_SCROLLING_CHANGE_IN_OVERSHOOT;
152 
153   return change;
154 }
155 
156 void
gtk_kinetic_scrolling_free(GtkKineticScrolling * kinetic)157 gtk_kinetic_scrolling_free (GtkKineticScrolling *kinetic)
158 {
159   g_slice_free (GtkKineticScrolling, kinetic);
160 }
161 
162 static void
gtk_kinetic_scrolling_init_overshoot(GtkKineticScrolling * data,gdouble equilibrium_position,gdouble initial_position,gdouble initial_velocity)163 gtk_kinetic_scrolling_init_overshoot (GtkKineticScrolling *data,
164                                       gdouble              equilibrium_position,
165                                       gdouble              initial_position,
166                                       gdouble              initial_velocity)
167 {
168   data->phase = GTK_KINETIC_SCROLLING_PHASE_OVERSHOOTING;
169   data->equilibrium_position = equilibrium_position;
170   data->c1 = initial_position - equilibrium_position;
171   data->c2 = initial_velocity + data->overshoot_friction / 2 * data->c1;
172   data->t = 0;
173 }
174 
175 gboolean
gtk_kinetic_scrolling_tick(GtkKineticScrolling * data,gdouble time_delta,gdouble * position,gdouble * velocity)176 gtk_kinetic_scrolling_tick (GtkKineticScrolling *data,
177                             gdouble              time_delta,
178                             gdouble             *position,
179                             gdouble             *velocity)
180 {
181   switch(data->phase)
182     {
183     case GTK_KINETIC_SCROLLING_PHASE_DECELERATING:
184       {
185         gdouble last_position = data->position;
186         gdouble last_time = data->t;
187         gdouble exp_part;
188 
189         data->t += time_delta;
190 
191         exp_part = exp (-data->decel_friction * data->t);
192         data->position = data->c1 + data->c2 * exp_part;
193         data->velocity = -data->decel_friction * data->c2 * exp_part;
194 
195         if(data->position < data->lower)
196           {
197             gtk_kinetic_scrolling_init_overshoot(data,data->lower,data->position,data->velocity);
198           }
199         else if (data->position > data->upper)
200           {
201             gtk_kinetic_scrolling_init_overshoot(data, data->upper, data->position, data->velocity);
202           }
203         else if (fabs(data->velocity) < 1 ||
204                  (last_time != 0.0 && fabs(data->position - last_position) < 1))
205           {
206             data->phase = GTK_KINETIC_SCROLLING_PHASE_FINISHED;
207             data->position = round(data->position);
208             data->velocity = 0;
209           }
210         break;
211       }
212 
213     case GTK_KINETIC_SCROLLING_PHASE_OVERSHOOTING:
214       {
215         gdouble exp_part, pos;
216 
217         data->t += time_delta;
218         exp_part = exp(-data->overshoot_friction / 2 * data->t);
219         pos = exp_part * (data->c1 + data->c2 * data->t);
220 
221         if (pos < data->lower - 50 || pos > data->upper + 50)
222           {
223             pos = CLAMP (pos, data->lower - 50, data->upper + 50);
224             gtk_kinetic_scrolling_init_overshoot (data, data->equilibrium_position, pos, 0);
225           }
226         else
227           data->velocity = data->c2 * exp_part - data->overshoot_friction / 2 * pos;
228 
229         data->position = pos + data->equilibrium_position;
230 
231         if(fabs (pos) < 0.1)
232           {
233             data->phase = GTK_KINETIC_SCROLLING_PHASE_FINISHED;
234             data->position = data->equilibrium_position;
235             data->velocity = 0;
236           }
237         break;
238       }
239 
240     case GTK_KINETIC_SCROLLING_PHASE_FINISHED:
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