Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions core/Fling.zig
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,49 @@ pub fn unrubber(overscroll: f32, r: Rubber) f32 {
return std.math.copysign(o * r.limit / (r.c * (r.limit - o)), overscroll);
}

/// A place of interest in a scroll area, in the area's own units.
pub const Notch = struct {
at: f32,
/// How far from `at` a release may land, or a lift come to rest, and still be drawn in.
capture: f32 = 64,
/// Release speeds at or above this pass the notch (units/s).
pass_speed: f32 = 1500,
};

/// The decay a coast needs to end on a notch.
pub const Aim = struct { at: f32, decay: f32 };

/// Where a coast released at `origin` with `v` ends: `origin` when it does not start.
pub fn landing(origin: f32, v: f32, t: Tuning) f32 {
if (@abs(v) <= t.stop) return origin;
return origin + (v - std.math.sign(v) * t.stop) / t.decay;
}

/// The notch a release at `origin` with `v` lands within reach of, and the decay that ends the
/// coast on it exactly, or null for a release that passes: too fast, or no notch ahead within
/// its capture of the landing. The nearest to the landing wins.
pub fn aim(origin: f32, v: f32, t: Tuning, notches: []const Notch) ?Aim {
if (@abs(v) <= t.stop) return null;
const end = landing(origin, v, t);
var best: ?Notch = null;
for (notches) |n| {
if ((n.at - origin) * v <= 0 or @abs(end - n.at) > n.capture or @abs(v) >= n.pass_speed) continue;
if (best == null or @abs(end - n.at) < @abs(end - best.?.at)) best = n;
}
const n = best orelse return null;
return .{ .at = n.at, .decay = (@abs(v) - t.stop) / @abs(n.at - origin) };
}

/// The position of the notch within whose capture `pos` is, the nearest to it, or null.
pub fn nearest(pos: f32, notches: []const Notch) ?f32 {
var best: ?f32 = null;
for (notches) |n| {
if (@abs(pos - n.at) > n.capture) continue;
if (best == null or @abs(pos - n.at) < @abs(pos - best.?)) best = n.at;
}
return best;
}

/// True while a release coast is still running.
coasting: bool = false,
/// Current coast velocity (units/s).
Expand Down Expand Up @@ -208,3 +251,40 @@ test "the rubber band shows a third of the travel at 120 points and never reache
try std.testing.expectApproxEqAbs(x, unrubber(rubber(x, rubber_band), rubber_band), 0.01);
}
}

/// Where a coast released at `origin` with `v` stops, from its closed form.
fn coastEnd(origin: f32, v: f32, decay: f32, t: Tuning) f32 {
return origin + v / decay * (1 - t.stop / @abs(v));
}

test "landing is where the coast ends" {
const t: Tuning = .{ .decay = 2, .stop = 20 };
try std.testing.expectEqual(@as(f32, 290), landing(0, 600, t));
try std.testing.expectEqual(@as(f32, 7), landing(7, 15, t));
for ([_]f32{ 600, -600, 3000, -3000 }) |v| {
try std.testing.expectApproxEqAbs(landing(0, v, t), coastEnd(0, v, t.decay, t), 1e-3);
}
}

test "aim lands the coast on the notch" {
const t: Tuning = .{ .decay = 2, .stop = 20 };
const a = aim(0, 600, t, &.{.{ .at = 300 }}).?;
try std.testing.expectApproxEqAbs(@as(f32, 580.0 / 300.0), a.decay, 1e-6);
try std.testing.expectApproxEqAbs(@as(f32, 300), coastEnd(0, 600, a.decay, t), 1e-3);
const back = aim(0, -600, t, &.{.{ .at = -300 }}).?;
try std.testing.expectApproxEqAbs(@as(f32, -300), coastEnd(0, -600, back.decay, t), 1e-3);
}

test "aim lets a fast, a far or a behind release go" {
const t: Tuning = .{ .decay = 2, .stop = 20 };
try std.testing.expectEqual(@as(?Aim, null), aim(0, 2000, t, &.{.{ .at = 1000 }}));
try std.testing.expectEqual(@as(?Aim, null), aim(0, 600, t, &.{.{ .at = 400 }}));
try std.testing.expectEqual(@as(?Aim, null), aim(0, 600, t, &.{.{ .at = -50 }}));
}

test "the nearest notch to the landing wins" {
const t: Tuning = .{ .decay = 2, .stop = 20 };
try std.testing.expectEqual(@as(f32, 280), aim(0, 600, t, &.{ .{ .at = 330 }, .{ .at = 280 } }).?.at);
try std.testing.expectEqual(@as(?f32, 280), nearest(290, &.{ .{ .at = 330 }, .{ .at = 280 } }));
try std.testing.expectEqual(@as(?f32, null), nearest(400, &.{.{ .at = 330 }}));
}
Loading
Loading