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 ScalingParams {
pub edge_scale: f32,
pub edge_alpha: f32,
pub min_element_height: f32,
pub max_element_height: f32,
pub min_transition_area: f32,
pub max_transition_area: f32,
}
impl Default for ScalingParams {
fn default() -> Self {
Self::WEAR
}
}
impl ScalingParams {
pub const WEAR: Self = Self {
edge_scale: EDGE_SCALE,
edge_alpha: EDGE_ALPHA,
min_element_height: MIN_ELEMENT_HEIGHT,
max_element_height: MAX_ELEMENT_HEIGHT,
min_transition_area: MIN_TRANSITION_AREA,
max_transition_area: MAX_TRANSITION_AREA,
};
pub const fn reduced_motion(self) -> Self {
Self {
edge_scale: 1.0,
edge_alpha: 1.0,
min_element_height: self.min_element_height,
max_element_height: self.max_element_height,
min_transition_area: self.min_transition_area,
max_transition_area: self.max_transition_area,
}
}
}
#[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> {
scale_and_alpha_with(ScalingParams::WEAR, viewport, top, bottom)
}
pub fn scale_and_alpha_with(
params: ScalingParams,
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(
params.min_element_height,
params.max_element_height,
(bottom - top) / viewport,
);
let line = params.min_transition_area
+ (params.max_transition_area - params.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 + (params.edge_scale - 1.0) * progress,
alpha: 1.0 + (params.edge_alpha - 1.0) * progress,
})
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlacedRow {
pub top: f32,
pub height: f32,
pub reported_height: f32,
pub scale: f32,
pub alpha: f32,
}
pub fn place_row(viewport: f32, top: f32, height: f32, density: f32) -> Option<PlacedRow> {
place_row_with(ScalingParams::WEAR, viewport, top, height, density)
}
pub fn place_row_with(
params: ScalingParams,
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_with(params, viewport, top, top + height)?;
let scaled = height * transform.scale;
return Some(PlacedRow {
top,
height: scaled,
reported_height: scaled,
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_with(params, 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,
reported_height: scaled_px / density,
scale: transform.scale,
alpha: transform.alpha,
})
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RowRun {
pub viewport: f32,
pub anchor: usize,
pub anchor_top: f32,
pub gap: f32,
pub density: f32,
}
pub fn place_rows_with(
params: ScalingParams,
run: RowRun,
heights: &[f32],
out: &mut Vec<PlacedRow>,
) {
out.clear();
if heights.is_empty() {
return;
}
let anchor = run.anchor.min(heights.len() - 1);
let unscaled = |top: f32, height: f32| PlacedRow {
top,
height,
reported_height: height,
scale: 1.0,
alpha: 1.0,
};
out.resize(heights.len(), unscaled(0.0, 0.0));
let place = |top: f32, height: f32| {
place_row_with(params, run.viewport, top, height, run.density)
.unwrap_or_else(|| unscaled(top, height))
};
let mut cursor = run.anchor_top;
for (index, &height) in heights.iter().enumerate().skip(anchor) {
let row = place(cursor, height);
cursor += height + run.gap;
out[index] = row;
}
let mut bottom = run.anchor_top;
for index in (0..anchor).rev() {
let height = heights[index];
bottom -= run.gap;
let row = place(bottom - height, height);
bottom -= height;
out[index] = row;
}
}
pub fn place_rows(run: RowRun, heights: &[f32], out: &mut Vec<PlacedRow>) {
place_rows_with(ScalingParams::WEAR, run, heights, out);
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Slot {
pub top: f32,
pub height: f32,
}
impl Slot {
pub fn centre(self) -> f32 {
self.top + self.height * 0.5
}
pub fn bottom(self) -> f32 {
self.top + self.height
}
}
pub fn stack_into(heights: impl IntoIterator<Item = f32>, gap: f32, out: &mut Vec<Slot>) {
out.clear();
let mut cursor = 0.0;
for height in heights {
out.push(Slot {
top: cursor,
height,
});
cursor += height + gap;
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CentreAnchor {
pub index: usize,
pub offset: f32,
}
impl Default for CentreAnchor {
fn default() -> Self {
Self {
index: 1,
offset: 0.0,
}
}
}
pub fn round_to_px(value: f32, density: f32) -> f32 {
if density <= 0.0 || !density.is_finite() || !value.is_finite() {
return value;
}
(value * density + 0.5).floor() / density
}
pub fn centre_offset(slots: &[Slot], viewport: f32, anchor: CentreAnchor, density: f32) -> f32 {
let Some(slot) = slots.get(anchor.index).or_else(|| slots.last()) else {
return 0.0;
};
round_to_px(viewport * 0.5 - slot.centre() - anchor.offset, density)
}
pub fn centre_offset_at(slots: &[Slot], viewport: f32, scroll: f32, density: f32) -> f32 {
if slots.is_empty() {
return 0.0;
}
let scroll = if scroll.is_finite() { scroll } else { 0.0 };
let whole = (scroll.floor().max(0.0) as usize).min(slots.len() - 1);
let fraction = (scroll - whole as f32).clamp(0.0, 1.0);
let mut anchor = slots[whole].centre();
if let Some(next) = slots.get(whole + 1) {
anchor += (next.centre() - anchor) * fraction;
}
round_to_px(viewport * 0.5 - anchor, density)
}
pub fn shift(slots: &mut [Slot], offset: f32) {
for slot in slots.iter_mut() {
slot.top += offset;
}
}
pub fn auto_centring_spacers(slots: &[Slot], viewport_px: f32, anchor: CentreAnchor) -> (f32, f32) {
let leading = slots
.get(anchor.index)
.or_else(|| slots.last())
.map_or(0.0, |slot| {
leading_auto_centring_spacer(viewport_px, slot.centre(), anchor.offset)
});
let trailing = slots.last().map_or(0.0, |slot| {
trailing_auto_centring_spacer(viewport_px, slot.height)
});
(leading, trailing)
}
pub fn leading_auto_centring_spacer(
viewport_px: f32,
anchor_centre_px: f32,
anchor_offset: f32,
) -> f32 {
((viewport_px * 0.5).floor() - anchor_offset - anchor_centre_px).max(0.0)
}
pub fn trailing_auto_centring_spacer(viewport_px: f32, last_height_px: f32) -> f32 {
(viewport_px - (viewport_px * 0.5).floor() - last_height_px * 0.5).max(0.0)
}
pub fn anchor_travel(
anchor_centre: f32,
last_centre: f32,
before_padding: f32,
after_padding: f32,
) -> (f32, f32) {
let end = last_centre + after_padding;
let start = anchor_centre - before_padding;
(start.min(end), end)
}
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)]
#[path = "tests/round_scaling_list_tests.rs"]
mod tests;