1package core
2
3import (
4 "math"
5 "time"
6)
7
8// Touchpad motion follows GNOME Shell and niri: a swipe tracker measures the
9// fingers' speed, the gesture lands where that speed would decelerate to a
10// halt, and a spring carries the view there from where the fingers left it.
11
12// trackerHistory is how far back the swipe speed is measured.
13const trackerHistory = 150 * time.Millisecond
14
15// touchpadDeceleration is the per-millisecond speed ratio of a swipe left
16// to decelerate after the fingers lift.
17const touchpadDeceleration = 0.997
18
19type trackerEvent struct {
20 delta float64
21 at time.Duration
22}
23
24// swipeTracker adds up one swipe axis. Times are event timestamps (the
25// device clock), so the speed does not depend on when events are read.
26type swipeTracker struct {
27 history []trackerEvent
28 pos float64
29}
30
31// push adds a movement. An event older than the last one is ignored.
32func (t *swipeTracker) push(delta float64, at time.Duration) {
33 if n := len(t.history); n > 0 && at < t.history[n-1].at {
34 return
35 }
36 t.history = append(t.history, trackerEvent{delta: delta, at: at})
37 t.pos += delta
38 i := 0
39 for i < len(t.history) && at > t.history[i].at+trackerHistory {
40 i++
41 }
42 t.history = t.history[i:]
43}
44
45// velocity is the speed over the recent history, in units per second.
46func (t *swipeTracker) velocity() float64 {
47 if len(t.history) < 2 {
48 return 0
49 }
50 span := (t.history[len(t.history)-1].at - t.history[0].at).Seconds()
51 if span <= 0 {
52 return 0
53 }
54 sum := 0.0
55 for _, e := range t.history {
56 sum += e.delta
57 }
58 return sum / span
59}
60
61// projectedEnd is where the swipe stops once its speed decelerates.
62func (t *swipeTracker) projectedEnd() float64 {
63 return t.pos - t.velocity()/(1000*math.Log(touchpadDeceleration))
64}
65
66// spring is a damped spring from From to To (libadwaita's model, as in
67// niri). Velocity is the initial speed in units per second.
68type spring struct {
69 From, To, Velocity float64
70 DampingRatio float64
71 Stiffness float64
72 Epsilon float64
73}
74
75// viewSpring moves a scrolled view; workspaceSpring switches workspaces.
76// Both are critically damped: fast, with no overshoot.
77func viewSpring(from, velocity float64) spring {
78 return spring{From: from, Velocity: velocity, DampingRatio: 1, Stiffness: 800, Epsilon: 0.0001}
79}
80
81func workspaceSpring(from, velocity float64) spring {
82 return spring{From: from, Velocity: velocity, DampingRatio: 1, Stiffness: 1000, Epsilon: 0.0001}
83}
84
85// f32Epsilon compares damping terms: float64's epsilon is too small for
86// them (libadwaita, niri).
87const f32Epsilon = 1.1920929e-07
88
89func (s spring) coefficients() (beta, omega0 float64) {
90 const mass = 1.0
91 k := max(s.Stiffness, 0)
92 damping := max(s.DampingRatio, 0) * 2 * math.Sqrt(mass*k)
93 return damping / (2 * mass), math.Sqrt(k / mass)
94}
95
96// valueAt is the spring position t after it started.
97func (s spring) valueAt(t time.Duration) float64 {
98 sec := t.Seconds()
99 beta, omega0 := s.coefficients()
100 x0 := s.From - s.To
101 envelope := math.Exp(-beta * sec)
102 switch {
103 case math.Abs(beta-omega0) <= f32Epsilon:
104 // Critically damped.
105 return s.To + envelope*(x0+(beta*x0+s.Velocity)*sec)
106 case beta < omega0:
107 w := math.Sqrt(omega0*omega0 - beta*beta)
108 return s.To + envelope*(x0*math.Cos(w*sec)+((beta*x0+s.Velocity)/w)*math.Sin(w*sec))
109 default:
110 w := math.Sqrt(beta*beta - omega0*omega0)
111 return s.To + envelope*(x0*math.Cosh(w*sec)+((beta*x0+s.Velocity)/w)*math.Sinh(w*sec))
112 }
113}
114
115// duration is how long the spring takes to settle within Epsilon.
116func (s spring) duration() time.Duration {
117 const step = 0.001
118 beta, omega0 := s.coefficients()
119 if beta <= 0 {
120 return time.Duration(math.MaxInt64)
121 }
122 if math.Abs(s.To-s.From) <= math.SmallestNonzeroFloat64 && s.Velocity == 0 {
123 return 0
124 }
125 x0 := -math.Log(max(s.Epsilon, math.SmallestNonzeroFloat64)) / beta
126 if math.Abs(beta-omega0) <= f32Epsilon || beta < omega0 {
127 return seconds(x0)
128 }
129 // Overdamped: the envelope decays faster than the motion; find where
130 // the motion itself settles (Newton's method).
131 at := func(sec float64) float64 { return s.valueAt(seconds(sec)) }
132 y0 := at(x0)
133 m := (at(x0+step) - y0) / step
134 x1 := (s.To - y0 + m*x0) / m
135 y1 := at(x1)
136 for i := 0; math.Abs(s.To-y1) > s.Epsilon; i++ {
137 if i > 1000 {
138 return 0
139 }
140 x0, y0 = x1, y1
141 m = (at(x0+step) - y0) / step
142 x1 = (s.To - y0 + m*x0) / m
143 y1 = at(x1)
144 if math.IsNaN(y1) || math.IsInf(y1, 0) {
145 return seconds(x0)
146 }
147 }
148 return seconds(x1)
149}
150
151func seconds(s float64) time.Duration { return time.Duration(s * float64(time.Second)) }
152
153// motion is a spring started at a given time; a zero start begins at the
154// first frame.
155type motion struct {
156 spring spring
157 start time.Time
158 end time.Duration
159}
160
161func newMotion(s spring, now time.Time) *motion {
162 return &motion{spring: s, start: now, end: s.duration()}
163}
164
165// at is the position at now and whether the motion has settled.
166func (m *motion) at(now time.Time) (float64, bool) {
167 if m.start.IsZero() {
168 m.start = now
169 }
170 t := now.Sub(m.start)
171 if t >= m.end {
172 return m.spring.To, true
173 }
174 return m.spring.valueAt(max(t, 0)), false
175}
176
177// rubberBand resists movement past the edges: the further out, the less
178// it moves, never more than limit.
179type rubberBand struct{ stiffness, limit float64 }
180
181// workspaceBand holds a swipe past the steps it may reach, in steps: a
182// twentieth of a workspace at most. Column swipes scale it to pixels.
183var workspaceBand = rubberBand{stiffness: 0.5, limit: 0.05}
184
185// scaled is the band measured in units k times smaller.
186func (r rubberBand) scaled(k float64) rubberBand {
187 return rubberBand{stiffness: r.stiffness, limit: r.limit * k}
188}
189
190func (r rubberBand) band(x float64) float64 {
191 return (1 - 1/(x*r.stiffness/r.limit+1)) * r.limit
192}
193
194func (r rubberBand) derivative(x float64) float64 {
195 d := r.stiffness*x + r.limit
196 return r.stiffness * r.limit * r.limit / (d * d)
197}
198
199// clamp keeps x in [lo, hi], letting it stretch past the edges.
200func (r rubberBand) clamp(lo, hi, x float64) float64 {
201 c := min(max(x, lo), hi)
202 if x < c {
203 return c - r.band(c-x)
204 }
205 return c + r.band(x-c)
206}
207
208// clampDerivative is the slope of clamp at x: 1 inside the edges.
209func (r rubberBand) clampDerivative(lo, hi, x float64) float64 {
210 if lo <= x && x <= hi {
211 return 1
212 }
213 c := min(max(x, lo), hi)
214 return r.derivative(math.Abs(x - c))
215}