Inertial Throw and Pressure Snap
When a user finishes a drag, the pointer has velocity. A good UI carries that momentum forward for a few frames (“throw”) and, when it settles, snaps to a canonical layout — but only if the gesture was confident enough to warrant the snap. This page covers the two types that model exactly that.
The gesture signal — PaneMotionVector
Both downstream types consume a motion summary:
pub struct PaneMotionVector {
pub delta_x: i32,
pub delta_y: i32,
pub speed: f64, // cells / second
pub direction_changes: u16, // sign flips in the gesture window
}direction_changes is a cheap noise indicator. A smooth throw has 0–1
direction changes; a hesitation has 5+. The two models below are careful
to weight noisy gestures as less confident.
PaneInertialThrow — momentum after release
pub struct PaneInertialThrow {
pub velocity_x: f64,
pub velocity_y: f64,
pub damping: f64, // exponential damping per second
pub horizon_ms: u16, // projection horizon
}
impl PaneInertialThrow {
pub fn from_motion(motion: PaneMotionVector) -> Self;
pub fn projected_pointer(self, start: PanePointerPosition)
-> PanePointerPosition;
}from_motion derives a throw profile from the gesture:
- Speed is clamped to [0, 220] cells/second, then raised to the power 0.72 — sublinear so very fast flicks don’t dominate.
- Direction coherence is
1 − 0.55 * (direction_changes / 10)clamped into[0.35, 1.0]. Noisy gestures lose up to 65 % of their projected velocity. - Projected velocity is
(10.0 + speed * 0.55) * coherence.
The physics, compactly
Given an initial velocity and damping , the position at time is an exponentially-decaying integral:
As , the pointer asymptotes at .
horizon_ms picks a cutoff where “close enough” is reached.
projected_pointer(start) returns the final position the UI should
preview.
pointer position
▲
│ ╭──────────── settle here
│ ╭──────╯
│ ╭──────╯
│ ╭────╯
│ ╭────╯
│╭──╯
───┼─────────────────────────────────▶ time
release horizon_msThe throw profile is what the docking preview uses to ask “where would this pane land if I released right now?” — see magnetic docking.
PanePressureSnapProfile — confidence-weighted snap
After the throw settles, the system decides whether to snap to a canonical layout (50/50 split, a preset ratio, a docked edge). The strength of that snap depends on how confident the gesture was:
pub struct PanePressureSnapProfile {
pub strength_bps: u16, // 0..=10_000
pub hysteresis_bps: u16, // 0..=2_000
}
impl PanePressureSnapProfile {
pub fn from_motion(motion: PaneMotionVector) -> Self;
pub fn apply_to_tuning(self, base: PaneSnapTuning) -> PaneSnapTuning;
}The profile is computed from four signals:
- Axis dominance —
max(|dx|, |dy|) / (|dx| + |dy|), clamped to[0.5, 1.0]. A perfect horizontal drag scores 1.0; a diagonal one scores 0.5. - Speed factor —
(speed / 70.0)^0.78, clamped to[0, 1]. Slow drags imply precision intent; fast drags imply canonical intent. - Noise penalty —
direction_changes / 7, clamped to[0, 1]. - Confidence =
speed_factor * (0.65 + axis_dominance * 0.35) * (1 − noise_penalty * 0.72).
strength_bps runs from about 1,500 (no snap) to 10,000 (always snap).
hysteresis_bps runs from about 60 to 560 — confident gestures earn a
wider “sticky” window around the snap point so the layout feels solid.
The snap decision
apply_to_tuning(base) returns a PaneSnapTuning { step_bps, hysteresis_bps } that the layout planner uses downstream:
pub fn decide(self, ratio_bps: u16, previous_snap: Option<u16>)
-> PaneSnapDecision;The decision enum is:
pub struct PaneSnapDecision {
pub reason: PaneSnapReason, // SnappedToStep | Sticky | NoSnap | …
pub target_position: Option<u16>,
}Worked example — fast confident flick vs. slow probe
User slowly probes with low confidence
PaneMotionVector { delta_x: 3, delta_y: 1, speed: 6.0, direction_changes: 4 }.
speed_factor ≈ (6/70)^0.78 ≈ 0.14direction_changes = 4→noise_penalty ≈ 0.57axis_dominance ≈ 0.75confidence ≈ 0.14 * (0.65 + 0.75 * 0.35) * (1 − 0.57 * 0.72) ≈ 0.07
Result: strength_bps ≈ 2,100 — effectively no snap. The ratio is left
where the user put it.
User flicks fast and decisively
PaneMotionVector { delta_x: 80, delta_y: 3, speed: 150.0, direction_changes: 0 }.
speed_factor ≈ (150/70)^0.78 ≈ 1.78 → clamp to 1.0direction_changes = 0→noise_penalty = 0axis_dominance ≈ 0.96confidence ≈ 1.0 * (0.65 + 0.96 * 0.35) * 1.0 ≈ 0.99
Result: strength_bps ≈ 10,000 — snap aggressively to the nearest
canonical ratio. hysteresis_bps ≈ 560, so small moves near that ratio
stay snapped.
The effect users perceive: precise dragging “sticks” where you leave it, and throws clunk satisfyingly into 50/50 or 1/3 positions.
Pitfalls
Don’t compute inertial throw from a single sample. You need at
least the last two positions and a timestamp. If you pass a zero or
noise-only motion vector, from_motion will give you a throw with
barely-nonzero velocity — valid, but meaningless.
Snap strength is bps (basis points), not a percentage. 10,000
bps = 100 %. Pass values in the 0..=10_000 range, not 0..=100.
Momentum is opt-in. The drag machine itself emits a final
Committed effect at the release position. Calling code decides
whether to also run a throw projection and fire follow-up operations.
If you don’t want momentum, don’t compute it.
Where to go next
How the projected landing position interacts with dock zones.
Magnetic dockingThe source of Committed — the release event.
Presets that bias the snap behavior further.
Intelligence modesWhere PaneMotionVector is computed from raw gestures.
How this piece fits in panes.
Panes overview