From e6f0e0582d2959c480e817e59b076a246f5d5f9f Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 13 Sep 2019 18:04:10 +0200 Subject: [PATCH] ui/gesture,ui/internal/fling: extract fling logic into its own package We're going to re-use fling extrapolation and animation for Wayland touchpads. Signed-off-by: Elias Naur --- ui/gesture/gesture.go | 93 ++---------------- ui/internal/fling/animation.go | 98 +++++++++++++++++++ .../fling/extrapolation.go} | 32 +++--- .../fling/extrapolation_test.go} | 2 +- 4 files changed, 127 insertions(+), 98 deletions(-) create mode 100644 ui/internal/fling/animation.go rename ui/{gesture/estimator.go => internal/fling/extrapolation.go} (91%) rename ui/{gesture/estimator_test.go => internal/fling/extrapolation_test.go} (97%) diff --git a/ui/gesture/gesture.go b/ui/gesture/gesture.go index 043b566f..db6dc00a 100644 --- a/ui/gesture/gesture.go +++ b/ui/gesture/gesture.go @@ -11,12 +11,11 @@ package gesture import ( "math" - "runtime" - "time" "gioui.org/ui" "gioui.org/ui/f32" "gioui.org/ui/input" + "gioui.org/ui/internal/fling" "gioui.org/ui/pointer" ) @@ -46,8 +45,8 @@ type ClickType uint8 type Scroll struct { dragging bool axis Axis - estimator estimator - flinger flinger + estimator fling.Extrapolation + flinger fling.Animation pid pointer.ID grab bool last int @@ -57,15 +56,6 @@ type Scroll struct { type ScrollState uint8 -type flinger struct { - // Current offset in pixels. - x float32 - // Initial time. - t0 time.Time - // Initial velocity in pixels pr second. - v0 float32 -} - type Axis uint8 const ( @@ -102,16 +92,7 @@ const ( StateFlinging ) -var ( - touchSlop = ui.Dp(3) - // Pixels/second. - minFlingVelocity = ui.Dp(50) - maxFlingVelocity = ui.Dp(8000) -) - -const ( - thresholdVelocity = 1 -) +var touchSlop = ui.Dp(3) // Add the handler to the operation list to receive click events. func (c *Click) Add(ops *ui.Ops) { @@ -168,7 +149,7 @@ func (s *Scroll) Add(ops *ui.Ops) { // Stop any remaining fling movement. func (s *Scroll) Stop() { - s.flinger = flinger{} + s.flinger = fling.Animation{} } // Scroll detects the scrolling distance from the available events and @@ -190,7 +171,7 @@ func (s *Scroll) Scroll(cfg ui.Config, q input.Queue, axis Axis) int { break } s.Stop() - s.estimator = estimator{} + s.estimator = fling.Extrapolation{} v := s.val(e.Position) s.last = int(math.Round(float64(v))) s.estimator.Sample(e.Time, v) @@ -201,16 +182,8 @@ func (s *Scroll) Scroll(cfg ui.Config, q input.Queue, axis Axis) int { break } fling := s.estimator.Estimate() - if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d >= slop || -slop >= d { - if min, v := float32(cfg.Px(minFlingVelocity)), fling.Velocity; v >= min || -min >= v { - max := float32(cfg.Px(maxFlingVelocity)) - if v > max { - v = max - } else if v < -max { - v = -max - } - s.flinger.Init(cfg.Now(), v) - } + if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d < -slop || d > slop { + s.flinger.Start(cfg, fling.Velocity) } fallthrough case pointer.Cancel: @@ -270,56 +243,6 @@ func (s *Scroll) State() ScrollState { } } -func (f *flinger) Init(now time.Time, v0 float32) { - f.t0 = now - f.v0 = v0 - f.x = 0 -} - -func (f *flinger) Active() bool { - return f.v0 != 0 -} - -// Tick computes and returns a fling distance since -// the last time Tick was called. -func (f *flinger) Tick(now time.Time) int { - if !f.Active() { - return 0 - } - var k float32 - if runtime.GOOS == "darwin" { - k = -2 // iOS - } else { - k = -4.2 // Android and default - } - t := now.Sub(f.t0) - // The acceleration x''(t) of a point mass with a drag - // force, f, proportional with velocity, x'(t), is - // governed by the equation - // - // x''(t) = kx'(t) - // - // Given the starting position x(0) = 0, the starting - // velocity x'(0) = v0, the position is then - // given by - // - // x(t) = v0*e^(k*t)/k - v0/k - // - ekt := float32(math.Exp(float64(k) * t.Seconds())) - x := f.v0*ekt/k - f.v0/k - dist := x - f.x - idist := int(math.Round(float64(dist))) - f.x += float32(idist) - // Solving for the velocity x'(t) gives us - // - // x'(t) = v0*e^(k*t) - v := f.v0 * ekt - if v < thresholdVelocity && v > -thresholdVelocity { - f.v0 = 0 - } - return idist -} - func (a Axis) String() string { switch a { case Horizontal: diff --git a/ui/internal/fling/animation.go b/ui/internal/fling/animation.go new file mode 100644 index 00000000..6d9a004d --- /dev/null +++ b/ui/internal/fling/animation.go @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package fling + +import ( + "math" + "runtime" + "time" + + "gioui.org/ui" +) + +type Animation struct { + // Current offset in pixels. + x float32 + // Initial time. + t0 time.Time + // Initial velocity in pixels pr second. + v0 float32 +} + +var ( + // Pixels/second. + minFlingVelocity = ui.Dp(50) + maxFlingVelocity = ui.Dp(8000) +) + +const ( + thresholdVelocity = 1 +) + +// Start a fling given a starting velocity. Returns whether a +// fling was started. +func (f *Animation) Start(c ui.Config, velocity float32) bool { + min := float32(c.Px(minFlingVelocity)) + v := velocity + if -min <= v && v <= min { + return false + } + max := float32(c.Px(maxFlingVelocity)) + if v > max { + v = max + } else if v < -max { + v = -max + } + f.init(c.Now(), v) + return true +} + +func (f *Animation) init(now time.Time, v0 float32) { + f.t0 = now + f.v0 = v0 + f.x = 0 +} + +func (f *Animation) Active() bool { + return f.v0 != 0 +} + +// Tick computes and returns a fling distance since +// the last time Tick was called. +func (f *Animation) Tick(now time.Time) int { + if !f.Active() { + return 0 + } + var k float32 + if runtime.GOOS == "darwin" { + k = -2 // iOS + } else { + k = -4.2 // Android and default + } + t := now.Sub(f.t0) + // The acceleration x''(t) of a point mass with a drag + // force, f, proportional with velocity, x'(t), is + // governed by the equation + // + // x''(t) = kx'(t) + // + // Given the starting position x(0) = 0, the starting + // velocity x'(0) = v0, the position is then + // given by + // + // x(t) = v0*e^(k*t)/k - v0/k + // + ekt := float32(math.Exp(float64(k) * t.Seconds())) + x := f.v0*ekt/k - f.v0/k + dist := x - f.x + idist := int(math.Round(float64(dist))) + f.x += float32(idist) + // Solving for the velocity x'(t) gives us + // + // x'(t) = v0*e^(k*t) + v := f.v0 * ekt + if -thresholdVelocity < v && v < thresholdVelocity { + f.v0 = 0 + } + return idist +} diff --git a/ui/gesture/estimator.go b/ui/internal/fling/extrapolation.go similarity index 91% rename from ui/gesture/estimator.go rename to ui/internal/fling/extrapolation.go index 6eceb45c..655ef84d 100644 --- a/ui/gesture/estimator.go +++ b/ui/internal/fling/extrapolation.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Unlicense OR MIT -package gesture +package fling import ( "math" @@ -9,15 +9,16 @@ import ( "time" ) -// Estimator computes a 1-dimensional velocity estimate +// Extrapolation computes a 1-dimensional velocity estimate // for a set of timestamped points using the least squares // fit of a 2nd order polynomial. The same method is used // by Android. -type estimator struct { +type Extrapolation struct { // Index into points. idx int // Circular buffer of samples. - samples []sample + samples []sample + lastValue float32 // Pre-allocated cache for samples. cache [historySize]sample @@ -36,7 +37,7 @@ type matrix struct { data []float32 } -type estimate struct { +type Estimate struct { Velocity float32 Distance float32 } @@ -50,8 +51,15 @@ const ( maxSampleGap = 40 * time.Millisecond ) -// Sample adds a sample to the estimation. -func (e *estimator) Sample(t time.Duration, val float32) { +// SampleDelta adds a relative sample to the estimation. +func (e *Extrapolation) SampleDelta(t time.Duration, delta float32) { + val := delta + e.lastValue + e.Sample(t, val) +} + +// Sample adds an absolute sample to the estimation. +func (e *Extrapolation) Sample(t time.Duration, val float32) { + e.lastValue = val if e.samples == nil { e.samples = e.cache[:0] } @@ -73,9 +81,9 @@ func (e *estimator) Sample(t time.Duration, val float32) { // Velocity returns an estimate of the implied velocity and // distance for the points sampled, or zero if the estimation method // failed. -func (e *estimator) Estimate() estimate { +func (e *Extrapolation) Estimate() Estimate { if len(e.samples) == 0 { - return estimate{} + return Estimate{} } values := e.values[:0] times := e.times[:0] @@ -97,16 +105,16 @@ func (e *estimator) Estimate() estimate { } coef, ok := polyFit(times, values) if !ok { - return estimate{} + return Estimate{} } dist := values[len(values)-1] - values[0] - return estimate{ + return Estimate{ Velocity: coef[1], Distance: dist, } } -func (e *estimator) get(i int) sample { +func (e *Extrapolation) get(i int) sample { idx := (e.idx + i - 1 + len(e.samples)) % len(e.samples) return e.samples[idx] } diff --git a/ui/gesture/estimator_test.go b/ui/internal/fling/extrapolation_test.go similarity index 97% rename from ui/gesture/estimator_test.go rename to ui/internal/fling/extrapolation_test.go index 49f0525f..3f9d982b 100644 --- a/ui/gesture/estimator_test.go +++ b/ui/internal/fling/extrapolation_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Unlicense OR MIT -package gesture +package fling import "testing"