pub const EDGE_SCALE: f32 = 0.7;
pub const EDGE_ALPHA: f32 = 0.5;
pub const MIN_ELEMENT_HEIGHT: f32 = 0.2;
pub const MAX_ELEMENT_HEIGHT: f32 = 0.6;
pub const MIN_TRANSITION_AREA: f32 = 0.35;
pub const MAX_TRANSITION_AREA: f32 = 0.55;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScaleAlpha {
pub scale: f32,
pub alpha: f32,
}
impl ScaleAlpha {
pub const UNCHANGED: Self = Self {
scale: 1.0,
alpha: 1.0,
};
}
pub fn scale_and_alpha(viewport: f32, top: f32, bottom: f32) -> Option<ScaleAlpha> {
if !viewport.is_finite() || !top.is_finite() || !bottom.is_finite() || bottom < top {
return None;
}
if viewport <= 0.0 {
return Some(ScaleAlpha::UNCHANGED);
}
let edge = (viewport - top).min(bottom) / viewport;
let size_ratio = inverse_lerp(
MIN_ELEMENT_HEIGHT,
MAX_ELEMENT_HEIGHT,
(bottom - top) / viewport,
);
let line = MIN_TRANSITION_AREA + (MAX_TRANSITION_AREA - MIN_TRANSITION_AREA) * size_ratio;
if edge >= line || line <= 0.0 {
return Some(ScaleAlpha::UNCHANGED);
}
let progress = ease(1.0 - edge / line);
Some(ScaleAlpha {
scale: 1.0 + (EDGE_SCALE - 1.0) * progress,
alpha: 1.0 + (EDGE_ALPHA - 1.0) * progress,
})
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlacedRow {
pub top: f32,
pub height: f32,
pub scale: f32,
pub alpha: f32,
}
pub fn place_row(viewport: f32, top: f32, height: f32, density: f32) -> Option<PlacedRow> {
if !height.is_finite() || height < 0.0 || !density.is_finite() {
return None;
}
if density <= 0.0 {
let transform = scale_and_alpha(viewport, top, top + height)?;
return Some(PlacedRow {
top,
height: height * transform.scale,
scale: transform.scale,
alpha: transform.alpha,
});
}
let viewport_px = (viewport * density).round();
let top_px = (top * density).round();
let height_px = (height * density).round();
let transform = scale_and_alpha(viewport_px, top_px, top_px + height_px)?;
let scaled_px = (height_px * transform.scale).round();
let above = top_px + top_px + height_px < viewport_px;
let pinned = if above {
top_px + height_px - scaled_px
} else {
top_px
};
Some(PlacedRow {
top: (pinned + odd_pixel(height_px) - odd_pixel(scaled_px)) / density,
height: height_px * transform.scale / density,
scale: transform.scale,
alpha: transform.alpha,
})
}
fn odd_pixel(pixels: f32) -> f32 {
let half = pixels * 0.5;
half - half.floor()
}
fn inverse_lerp(start: f32, stop: f32, value: f32) -> f32 {
((value - start) / (stop - start)).clamp(0.0, 1.0)
}
fn ease(x: f32) -> f32 {
let x = x.clamp(0.0, 1.0);
let mut low = 0.0f32;
let mut high = 1.0f32;
let mut t = x;
for _ in 0..12 {
let value = bezier(t, 0.3, 0.7);
if value < x {
low = t;
} else {
high = t;
}
t = (low + high) * 0.5;
}
bezier(t, 0.0, 1.0)
}
fn bezier(t: f32, first: f32, second: f32) -> f32 {
let inverse = 1.0 - t;
3.0 * inverse * inverse * t * first + 3.0 * inverse * t * t * second + t * t * t
}
#[cfg(test)]
mod tests {
use super::*;
const VIEWPORT: f32 = 227.0;
#[test]
fn a_row_in_the_middle_is_left_alone() {
let middle = scale_and_alpha(VIEWPORT, VIEWPORT * 0.45, VIEWPORT * 0.55).unwrap();
assert_eq!(middle, ScaleAlpha::UNCHANGED);
}
#[test]
fn a_row_at_the_edge_is_shrunk_and_faded_together() {
let edge = scale_and_alpha(VIEWPORT, 0.0, 20.0).unwrap();
assert!(edge.scale < 1.0 && edge.scale >= EDGE_SCALE, "{edge:?}");
assert!(edge.alpha < 1.0 && edge.alpha >= EDGE_ALPHA, "{edge:?}");
let top = scale_and_alpha(VIEWPORT, 0.0, 0.0).unwrap();
assert!((top.scale - EDGE_SCALE).abs() < 1e-3, "{top:?}");
assert!((top.alpha - EDGE_ALPHA).abs() < 1e-3, "{top:?}");
}
#[test]
fn the_two_edges_treat_a_row_the_same() {
let height = 40.0;
let near_top = scale_and_alpha(VIEWPORT, 8.0, 8.0 + height).unwrap();
let near_bottom =
scale_and_alpha(VIEWPORT, VIEWPORT - 8.0 - height, VIEWPORT - 8.0).unwrap();
assert!((near_top.scale - near_bottom.scale).abs() < 1e-5);
assert!((near_top.alpha - near_bottom.alpha).abs() < 1e-5);
}
#[test]
fn a_taller_row_starts_shrinking_further_from_the_edge() {
let top = VIEWPORT - 10.0;
let short = scale_and_alpha(VIEWPORT, top, top + VIEWPORT * 0.2).unwrap();
let tall = scale_and_alpha(VIEWPORT, top, top + VIEWPORT * 0.62).unwrap();
assert!(tall.scale < short.scale, "short {short:?} tall {tall:?}");
}
#[test]
fn a_row_is_placed_from_the_full_heights_above_it_not_the_scaled_ones() {
let first = place_row(VIEWPORT, 0.0, 50.0, 2.0).unwrap();
let second = place_row(VIEWPORT, 50.0, 50.0, 2.0).unwrap();
assert!(first.scale < 1.0, "the first row is at the edge: {first:?}");
assert!(second.top >= 49.0, "{second:?}");
}
#[test]
fn a_row_above_the_centre_line_keeps_its_bottom_edge() {
let above = place_row(VIEWPORT, 4.0, 50.0, 2.0).unwrap();
assert!(above.scale < 1.0, "{above:?}");
assert!(
above.top > 4.0,
"shrinking should pull the top down: {above:?}"
);
let below = place_row(VIEWPORT, VIEWPORT - 54.0, 50.0, 2.0).unwrap();
assert!(below.scale < 1.0, "{below:?}");
assert!(
(below.top - (VIEWPORT - 54.0)).abs() < 0.6,
"the top is pinned below the line: {below:?}"
);
}
#[test]
fn an_odd_pixel_height_carries_the_half_pixel_composes_integer_halving_leaves() {
assert_eq!(odd_pixel(50.0), 0.0);
assert_eq!(odd_pixel(51.0), 0.5);
let odd = place_row(VIEWPORT, 3.0, 25.5, 2.0).unwrap();
assert!(odd.scale < 1.0, "needs to be in the scaled band: {odd:?}");
}
#[test]
fn a_density_of_zero_falls_back_to_continuous_placement_instead_of_dividing_by_it() {
let placed = place_row(VIEWPORT, 10.0, 50.0, 0.0).unwrap();
assert!(
placed.top.is_finite() && placed.height.is_finite(),
"{placed:?}"
);
assert_eq!(placed.top, 10.0);
let negative = place_row(VIEWPORT, 10.0, 50.0, -2.0).unwrap();
assert_eq!(negative, placed, "a nonsense density is not a crash");
}
#[test]
fn an_empty_viewport_leaves_everything_alone_rather_than_dividing_by_it() {
assert_eq!(scale_and_alpha(0.0, 0.0, 10.0), Some(ScaleAlpha::UNCHANGED));
assert_eq!(
scale_and_alpha(-5.0, 0.0, 10.0),
Some(ScaleAlpha::UNCHANGED)
);
}
#[test]
fn invalid_geometry_is_rejected_instead_of_producing_nan() {
assert_eq!(scale_and_alpha(f32::NAN, 0.0, 10.0), None);
assert_eq!(scale_and_alpha(VIEWPORT, 10.0, 9.0), None);
assert_eq!(place_row(VIEWPORT, 0.0, -1.0, 2.0), None);
assert_eq!(place_row(VIEWPORT, 0.0, 10.0, f32::INFINITY), None);
}
#[test]
fn the_easing_is_monotonic_and_spans_the_whole_range() {
assert!((ease(0.0) - 0.0).abs() < 1e-3, "{}", ease(0.0));
assert!((ease(1.0) - 1.0).abs() < 1e-3, "{}", ease(1.0));
let mut previous = -1.0;
for step in 0..=20 {
let value = ease(step as f32 / 20.0);
assert!(value >= previous - 1e-4, "not monotonic at {step}");
previous = value;
}
}
#[test]
fn the_easing_matches_the_current_wear_compose_curve() {
assert!((ease(0.25) - 0.166_779).abs() < 1e-3, "{}", ease(0.25));
}
}