use super::easing::Easing;
use super::timeline::Animatable;
#[derive(Debug, Clone)]
pub struct ScrollTimeline {
pub start: f64,
pub end: f64,
pub easing: Option<Easing>,
pub clamp: bool,
}
impl ScrollTimeline {
pub fn new(start: f64, end: f64) -> Self {
Self {
start,
end,
easing: None,
clamp: true,
}
}
pub fn with_easing(mut self, easing: Easing) -> Self {
self.easing = Some(easing);
self
}
pub fn unclamped(mut self) -> Self {
self.clamp = false;
self
}
pub fn progress(&self, scroll_position: f64) -> f64 {
if (self.end - self.start).abs() < f64::EPSILON {
return if scroll_position >= self.start {
1.0
} else {
0.0
};
}
let raw = (scroll_position - self.start) / (self.end - self.start);
let clamped = if self.clamp {
raw.clamp(0.0, 1.0)
} else {
raw
};
match &self.easing {
Some(e) => e.ease(clamped),
None => clamped,
}
}
pub fn is_active(&self, scroll_position: f64) -> bool {
let (min, max) = if self.start <= self.end {
(self.start, self.end)
} else {
(self.end, self.start)
};
scroll_position >= min && scroll_position <= max
}
pub fn is_complete(&self, scroll_position: f64) -> bool {
if self.start <= self.end {
scroll_position >= self.end
} else {
scroll_position <= self.end
}
}
}
#[derive(Debug, Clone)]
pub struct ViewTimeline {
pub element_top: f64,
pub element_bottom: f64,
pub viewport_height: f64,
pub start_threshold: f64,
pub end_threshold: f64,
pub easing: Option<Easing>,
}
impl ViewTimeline {
pub fn new(element_top: f64, element_bottom: f64, viewport_height: f64) -> Self {
Self {
element_top,
element_bottom,
viewport_height,
start_threshold: 0.0,
end_threshold: 1.0,
easing: None,
}
}
pub fn with_thresholds(mut self, start: f64, end: f64) -> Self {
self.start_threshold = start.clamp(0.0, 1.0);
self.end_threshold = end.clamp(0.0, 1.0);
self
}
pub fn with_easing(mut self, easing: Easing) -> Self {
self.easing = Some(easing);
self
}
pub fn progress(&self, scroll_top: f64) -> f64 {
let element_height = self.element_bottom - self.element_top;
let enter_point = self.element_top - self.viewport_height;
let total_travel = self.viewport_height + element_height;
let start_point = enter_point + total_travel * self.start_threshold;
let end_point = enter_point + total_travel * self.end_threshold;
if (end_point - start_point).abs() < f64::EPSILON {
return if scroll_top >= start_point { 1.0 } else { 0.0 };
}
let raw = (scroll_top - start_point) / (end_point - start_point);
let clamped = raw.clamp(0.0, 1.0);
match &self.easing {
Some(e) => e.ease(clamped),
None => clamped,
}
}
pub fn is_visible(&self, scroll_top: f64) -> bool {
let viewport_bottom = scroll_top + self.viewport_height;
self.element_bottom > scroll_top && self.element_top < viewport_bottom
}
pub fn is_fully_visible(&self, scroll_top: f64) -> bool {
let viewport_bottom = scroll_top + self.viewport_height;
self.element_top >= scroll_top && self.element_bottom <= viewport_bottom
}
pub fn is_exiting(&self, scroll_top: f64) -> bool {
self.element_top < scroll_top || self.element_bottom > scroll_top + self.viewport_height
}
}
#[derive(Debug, Clone)]
pub struct ScrollTween<T: Animatable> {
pub from: T,
pub to: T,
pub timeline: ScrollTimeline,
}
impl<T: Animatable> ScrollTween<T> {
pub fn new(from: T, to: T, timeline: ScrollTimeline) -> Self {
Self { from, to, timeline }
}
pub fn value_at(&self, scroll_position: f64) -> T {
let progress = self.timeline.progress(scroll_position);
self.from.lerp(&self.to, progress)
}
pub fn is_active(&self, scroll_position: f64) -> bool {
self.timeline.is_active(scroll_position)
}
pub fn is_complete(&self, scroll_position: f64) -> bool {
self.timeline.is_complete(scroll_position)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ParallaxLayer {
pub depth: f64,
}
impl ParallaxLayer {
pub fn new(depth: f64) -> Self {
Self {
depth: depth.clamp(0.0, 1.0),
}
}
pub fn offset(&self, scroll_position: f64) -> f64 {
scroll_position * self.depth
}
pub fn offset_relative(&self, scroll_position: f64, reference: f64) -> f64 {
(scroll_position - reference) * self.depth
}
pub fn with_depth(mut self, depth: f64) -> Self {
self.depth = depth.clamp(0.0, 1.0);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scroll_timeline_basic() {
let timeline = ScrollTimeline::new(0.0, 100.0);
assert_eq!(timeline.progress(0.0), 0.0);
assert_eq!(timeline.progress(50.0), 0.5);
assert_eq!(timeline.progress(100.0), 1.0);
}
#[test]
fn test_scroll_timeline_clamped() {
let timeline = ScrollTimeline::new(100.0, 200.0);
assert_eq!(timeline.progress(0.0), 0.0);
assert_eq!(timeline.progress(300.0), 1.0);
assert_eq!(timeline.progress(150.0), 0.5);
}
#[test]
fn test_scroll_timeline_unclamped() {
let timeline = ScrollTimeline::new(100.0, 200.0).unclamped();
assert_eq!(timeline.progress(0.0), -1.0);
assert_eq!(timeline.progress(300.0), 2.0);
assert_eq!(timeline.progress(150.0), 0.5);
}
#[test]
fn test_scroll_timeline_with_easing() {
let timeline = ScrollTimeline::new(0.0, 100.0).with_easing(Easing::EaseInQuad);
assert_eq!(timeline.progress(0.0), 0.0);
assert_eq!(timeline.progress(50.0), 0.25); assert_eq!(timeline.progress(100.0), 1.0);
}
#[test]
fn test_scroll_timeline_zero_length() {
let timeline = ScrollTimeline::new(100.0, 100.0);
assert_eq!(timeline.progress(99.0), 0.0);
assert_eq!(timeline.progress(100.0), 1.0);
assert_eq!(timeline.progress(101.0), 1.0);
}
#[test]
fn test_scroll_timeline_is_active() {
let timeline = ScrollTimeline::new(100.0, 200.0);
assert!(!timeline.is_active(50.0));
assert!(timeline.is_active(100.0));
assert!(timeline.is_active(150.0));
assert!(timeline.is_active(200.0));
assert!(!timeline.is_active(250.0));
}
#[test]
fn test_scroll_timeline_is_complete() {
let timeline = ScrollTimeline::new(100.0, 200.0);
assert!(!timeline.is_complete(50.0));
assert!(!timeline.is_complete(100.0));
assert!(!timeline.is_complete(150.0));
assert!(timeline.is_complete(200.0));
assert!(timeline.is_complete(250.0));
}
#[test]
fn test_scroll_timeline_reverse() {
let timeline = ScrollTimeline::new(200.0, 100.0);
assert_eq!(timeline.progress(200.0), 0.0);
assert_eq!(timeline.progress(150.0), 0.5);
assert_eq!(timeline.progress(100.0), 1.0);
}
#[test]
fn test_view_timeline_basic() {
let timeline = ViewTimeline::new(500.0, 800.0, 1000.0);
let enter_scroll = -500.0;
let exit_scroll = 800.0;
let progress_at_enter = timeline.progress(enter_scroll);
assert!(progress_at_enter < 0.01);
let progress_at_exit = timeline.progress(exit_scroll);
assert!(progress_at_exit > 0.99);
let midpoint = (enter_scroll + exit_scroll) / 2.0;
let progress_mid = timeline.progress(midpoint);
assert!((progress_mid - 0.5).abs() < 0.1);
}
#[test]
fn test_view_timeline_is_visible() {
let timeline = ViewTimeline::new(500.0, 800.0, 1000.0);
assert!(!timeline.is_visible(-600.0));
assert!(timeline.is_visible(0.0));
assert!(timeline.is_visible(500.0));
assert!(!timeline.is_visible(1000.0));
}
#[test]
fn test_view_timeline_is_fully_visible() {
let timeline = ViewTimeline::new(500.0, 800.0, 1000.0);
assert!(!timeline.is_fully_visible(-300.0)); assert!(timeline.is_fully_visible(0.0)); assert!(timeline.is_fully_visible(500.0)); assert!(!timeline.is_fully_visible(600.0)); }
#[test]
fn test_scroll_tween() {
let timeline = ScrollTimeline::new(0.0, 100.0);
let tween = ScrollTween::new(0.0, 1.0, timeline);
assert_eq!(tween.value_at(0.0), 0.0);
assert_eq!(tween.value_at(50.0), 0.5);
assert_eq!(tween.value_at(100.0), 1.0);
}
#[test]
fn test_scroll_tween_with_tuples() {
let timeline = ScrollTimeline::new(0.0, 100.0);
let tween = ScrollTween::new((0.0, 0.0), (100.0, 200.0), timeline);
assert_eq!(tween.value_at(0.0), (0.0, 0.0));
assert_eq!(tween.value_at(50.0), (50.0, 100.0));
assert_eq!(tween.value_at(100.0), (100.0, 200.0));
}
#[test]
fn test_parallax_layer_basic() {
let background = ParallaxLayer::new(0.0);
let midground = ParallaxLayer::new(0.5);
let foreground = ParallaxLayer::new(1.0);
let scroll = 100.0;
assert_eq!(background.offset(scroll), 0.0);
assert_eq!(midground.offset(scroll), 50.0);
assert_eq!(foreground.offset(scroll), 100.0);
}
#[test]
fn test_parallax_layer_relative() {
let layer = ParallaxLayer::new(0.5);
let reference = 100.0;
assert_eq!(layer.offset_relative(100.0, reference), 0.0);
assert_eq!(layer.offset_relative(200.0, reference), 50.0);
assert_eq!(layer.offset_relative(0.0, reference), -50.0);
}
#[test]
fn test_parallax_layer_clamping() {
let too_low = ParallaxLayer::new(-0.5);
let too_high = ParallaxLayer::new(1.5);
assert_eq!(too_low.depth, 0.0);
assert_eq!(too_high.depth, 1.0);
}
#[test]
fn test_parallax_layer_zero_scroll() {
let layer = ParallaxLayer::new(0.5);
assert_eq!(layer.offset(0.0), 0.0);
}
}