Implement non-proportional multitouch pinch zooming
This commit is contained in:
parent
04d9ce227b
commit
dd6980bacb
4 changed files with 125 additions and 17 deletions
|
@ -130,7 +130,7 @@ impl InputState {
|
|||
}
|
||||
|
||||
/// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture).
|
||||
/// * `zoom = 1`: no change (default).
|
||||
/// * `zoom = 1`: no change
|
||||
/// * `zoom < 1`: pinch together
|
||||
/// * `zoom > 1`: pinch spread
|
||||
#[inline(always)]
|
||||
|
@ -144,6 +144,30 @@ impl InputState {
|
|||
.unwrap_or(self.raw.zoom_delta)
|
||||
}
|
||||
|
||||
/// 2D non-proportional zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture).
|
||||
///
|
||||
/// For multitouch devices the user can do a horizontal or vertical pinch gesture.
|
||||
/// In these cases a non-proportional zoom factor is a available.
|
||||
/// In other cases, this reverts to `Vec2::splat(self.zoom_delta())`.
|
||||
///
|
||||
/// For horizontal pinches, this will return `[z, 1]`,
|
||||
/// for vertical pinches this will return `[1, z]`,
|
||||
/// and otherwise this will return `[z, z]`,
|
||||
/// where `z` is the zoom factor:
|
||||
/// * `zoom = 1`: no change
|
||||
/// * `zoom < 1`: pinch together
|
||||
/// * `zoom > 1`: pinch spread
|
||||
#[inline(always)]
|
||||
pub fn zoom_delta_2d(&self) -> Vec2 {
|
||||
// If a multi touch gesture is detected, it measures the exact and linear proportions of
|
||||
// the distances of the finger tips. It is therefore potentially more accurate than
|
||||
// `raw.zoom_delta` which is based on the `ctrl-scroll` event which, in turn, may be
|
||||
// synthesized from an original touch gesture.
|
||||
self.multi_touch()
|
||||
.map(|touch| touch.zoom_delta_2d)
|
||||
.unwrap_or_else(|| Vec2::splat(self.raw.zoom_delta))
|
||||
}
|
||||
|
||||
pub fn wants_repaint(&self) -> bool {
|
||||
self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty()
|
||||
}
|
||||
|
|
|
@ -11,20 +11,36 @@ use epaint::emath::{Pos2, Vec2};
|
|||
pub struct MultiTouchInfo {
|
||||
/// Point in time when the gesture started.
|
||||
pub start_time: f64,
|
||||
|
||||
/// Position of the pointer at the time the gesture started.
|
||||
pub start_pos: Pos2,
|
||||
|
||||
/// Number of touches (fingers) on the surface. Value is ≥ 2 since for a single touch no
|
||||
/// `MultiTouchInfo` is created.
|
||||
pub num_touches: usize,
|
||||
/// Zoom factor (Pinch or Zoom). Moving fingers closer together or further appart will change
|
||||
/// this value. This is a relative value, comparing the average distances of the fingers in
|
||||
/// the current and previous frame. If the fingers did not move since the previous frame,
|
||||
/// this value is `1.0`.
|
||||
|
||||
/// Proportional zoom factor (pinch gesture).
|
||||
/// * `zoom = 1`: no change
|
||||
/// * `zoom < 1`: pinch together
|
||||
/// * `zoom > 1`: pinch spread
|
||||
pub zoom_delta: f32,
|
||||
|
||||
/// 2D non-proportional zoom factor (pinch gesture).
|
||||
///
|
||||
/// For horizontal pinches, this will return `[z, 1]`,
|
||||
/// for vertical pinches this will return `[1, z]`,
|
||||
/// and otherwise this will return `[z, z]`,
|
||||
/// where `z` is the zoom factor:
|
||||
/// * `zoom = 1`: no change
|
||||
/// * `zoom < 1`: pinch together
|
||||
/// * `zoom > 1`: pinch spread
|
||||
pub zoom_delta_2d: Vec2,
|
||||
|
||||
/// Rotation in radians. Moving fingers around each other will change this value. This is a
|
||||
/// relative value, comparing the orientation of fingers in the current frame with the previous
|
||||
/// frame. If all fingers are resting, this value is `0.0`.
|
||||
pub rotation_delta: f32,
|
||||
|
||||
/// Relative movement (comparing previous frame and current frame) of the average position of
|
||||
/// all touch points. Without movement this value is `Vec2::ZERO`.
|
||||
///
|
||||
|
@ -34,6 +50,7 @@ pub struct MultiTouchInfo {
|
|||
/// the pointer, but touch movement is always measured in the units delivered by the device,
|
||||
/// and may depend on hardware and system settings.
|
||||
pub translation_delta: Vec2,
|
||||
|
||||
/// Current force of the touch (average of the forces of the individual fingers). This is a
|
||||
/// value in the interval `[0.0 .. =1.0]`.
|
||||
///
|
||||
|
@ -67,6 +84,7 @@ pub(crate) struct TouchState {
|
|||
struct GestureState {
|
||||
start_time: f64,
|
||||
start_pointer_pos: Pos2,
|
||||
pinch_type: PinchType,
|
||||
previous: Option<DynGestureState>,
|
||||
current: DynGestureState,
|
||||
}
|
||||
|
@ -74,7 +92,10 @@ struct GestureState {
|
|||
/// Gesture data that can change over time
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct DynGestureState {
|
||||
/// used for proportional zooming
|
||||
avg_distance: f32,
|
||||
/// used for non-proportional zooming
|
||||
avg_abs_distance2: Vec2,
|
||||
avg_pos: Pos2,
|
||||
avg_force: f32,
|
||||
heading: f32,
|
||||
|
@ -154,11 +175,27 @@ impl TouchState {
|
|||
// changed. In this case, we take `current` as `previous`, pretending that there
|
||||
// was no change for the current frame.
|
||||
let state_previous = state.previous.unwrap_or(state.current);
|
||||
|
||||
let zoom_delta = state.current.avg_distance / state_previous.avg_distance;
|
||||
|
||||
let zoom_delta2 = match state.pinch_type {
|
||||
PinchType::Horizontal => Vec2::new(
|
||||
state.current.avg_abs_distance2.x / state_previous.avg_abs_distance2.x,
|
||||
1.0,
|
||||
),
|
||||
PinchType::Vertical => Vec2::new(
|
||||
1.0,
|
||||
state.current.avg_abs_distance2.y / state_previous.avg_abs_distance2.y,
|
||||
),
|
||||
PinchType::Proportional => Vec2::splat(zoom_delta),
|
||||
};
|
||||
|
||||
MultiTouchInfo {
|
||||
start_time: state.start_time,
|
||||
start_pos: state.start_pointer_pos,
|
||||
num_touches: self.active_touches.len(),
|
||||
zoom_delta: state.current.avg_distance / state_previous.avg_distance,
|
||||
zoom_delta,
|
||||
zoom_delta_2d: zoom_delta2,
|
||||
rotation_delta: normalized_angle(state.current.heading, state_previous.heading),
|
||||
translation_delta: state.current.avg_pos - state_previous.avg_pos,
|
||||
force: state.current.avg_force,
|
||||
|
@ -177,6 +214,7 @@ impl TouchState {
|
|||
self.gesture_state = Some(GestureState {
|
||||
start_time: time,
|
||||
start_pointer_pos: pointer_pos,
|
||||
pinch_type: PinchType::classify(&self.active_touches),
|
||||
previous: None,
|
||||
current: dyn_state,
|
||||
});
|
||||
|
@ -187,16 +225,18 @@ impl TouchState {
|
|||
}
|
||||
}
|
||||
|
||||
/// `None` if less than two fingers
|
||||
fn calc_dynamic_state(&self) -> Option<DynGestureState> {
|
||||
let num_touches = self.active_touches.len();
|
||||
if num_touches < 2 {
|
||||
None
|
||||
} else {
|
||||
let mut state = DynGestureState {
|
||||
avg_distance: 0.,
|
||||
avg_distance: 0.0,
|
||||
avg_abs_distance2: Vec2::ZERO,
|
||||
avg_pos: Pos2::ZERO,
|
||||
avg_force: 0.,
|
||||
heading: 0.,
|
||||
avg_force: 0.0,
|
||||
heading: 0.0,
|
||||
};
|
||||
let num_touches_recip = 1. / num_touches as f32;
|
||||
|
||||
|
@ -213,8 +253,11 @@ impl TouchState {
|
|||
// second pass: calculate distances from center:
|
||||
for touch in self.active_touches.values() {
|
||||
state.avg_distance += state.avg_pos.distance(touch.pos);
|
||||
state.avg_abs_distance2.x += (state.avg_pos.x - touch.pos.x).abs();
|
||||
state.avg_abs_distance2.y += (state.avg_pos.y - touch.pos.y).abs();
|
||||
}
|
||||
state.avg_distance *= num_touches_recip;
|
||||
state.avg_abs_distance2 *= num_touches_recip;
|
||||
|
||||
// Calculate the direction from the first touch to the center position.
|
||||
// This is not the perfect way of calculating the direction if more than two fingers
|
||||
|
@ -271,3 +314,40 @@ fn normalizing_angle_from_350_to_0_yields_10() {
|
|||
<= 5. * f32::EPSILON // many conversions (=divisions) involved => high error rate
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum PinchType {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
Proportional,
|
||||
}
|
||||
|
||||
impl PinchType {
|
||||
fn classify(touches: &BTreeMap<TouchId, ActiveTouch>) -> Self {
|
||||
// For non-proportional 2d zooming:
|
||||
// If the user is pinching with two fingers that have roughly the same Y coord,
|
||||
// then the Y zoom is unstable and should be 1.
|
||||
// Similarly, if the fingers are directly above/below each other,
|
||||
// we should only zoom on the Y axis.
|
||||
// If the fingers are roughly on a diagonal, we revert to the proportional zooming.
|
||||
|
||||
if touches.len() == 2 {
|
||||
let mut touches = touches.values();
|
||||
let t0 = touches.next().unwrap().pos;
|
||||
let t1 = touches.next().unwrap().pos;
|
||||
|
||||
let dx = (t0.x - t1.x).abs();
|
||||
let dy = (t0.y - t1.y).abs();
|
||||
|
||||
if dx > 3.0 * dy {
|
||||
Self::Horizontal
|
||||
} else if dy > 3.0 * dx {
|
||||
Self::Vertical
|
||||
} else {
|
||||
Self::Proportional
|
||||
}
|
||||
} else {
|
||||
Self::Proportional
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -418,12 +418,17 @@ impl Widget for Plot {
|
|||
// Zooming
|
||||
if allow_zoom {
|
||||
if let Some(hover_pos) = response.hover_pos() {
|
||||
let zoom_factor = ui.input().zoom_delta();
|
||||
let zoom_factor = if data_aspect.is_some() {
|
||||
Vec2::splat(ui.input().zoom_delta())
|
||||
} else {
|
||||
ui.input().zoom_delta_2d()
|
||||
};
|
||||
#[allow(clippy::float_cmp)]
|
||||
if zoom_factor != 1.0 {
|
||||
if zoom_factor != Vec2::splat(1.0) {
|
||||
transform.zoom(zoom_factor, hover_pos);
|
||||
auto_bounds = false;
|
||||
}
|
||||
|
||||
let scroll_delta = ui.input().scroll_delta;
|
||||
if scroll_delta != Vec2::ZERO {
|
||||
transform.translate_bounds(-scroll_delta);
|
||||
|
|
|
@ -161,15 +161,14 @@ impl ScreenTransform {
|
|||
}
|
||||
|
||||
/// Zoom by a relative factor with the given screen position as center.
|
||||
pub fn zoom(&mut self, zoom_factor: f32, center: Pos2) {
|
||||
let zoom_factor = zoom_factor as f64;
|
||||
pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) {
|
||||
let center = self.value_from_position(center);
|
||||
|
||||
let mut new_bounds = self.bounds;
|
||||
new_bounds.min[0] = center.x + (new_bounds.min[0] - center.x) / zoom_factor;
|
||||
new_bounds.max[0] = center.x + (new_bounds.max[0] - center.x) / zoom_factor;
|
||||
new_bounds.min[1] = center.y + (new_bounds.min[1] - center.y) / zoom_factor;
|
||||
new_bounds.max[1] = center.y + (new_bounds.max[1] - center.y) / zoom_factor;
|
||||
new_bounds.min[0] = center.x + (new_bounds.min[0] - center.x) / (zoom_factor.x as f64);
|
||||
new_bounds.max[0] = center.x + (new_bounds.max[0] - center.x) / (zoom_factor.x as f64);
|
||||
new_bounds.min[1] = center.y + (new_bounds.min[1] - center.y) / (zoom_factor.y as f64);
|
||||
new_bounds.max[1] = center.y + (new_bounds.max[1] - center.y) / (zoom_factor.y as f64);
|
||||
|
||||
if new_bounds.is_valid() {
|
||||
self.bounds = new_bounds;
|
||||
|
|
Loading…
Reference in a new issue