use std::ops::{Add, Div, Sub};
use crate::{
math::Easing,
rhythm::{
ClockTime, TempoTracks, Time, TrackId,
track::{EventTrack, EventTracks, TrackEvent},
},
};
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScrollSpeedChangeEvent {
pub time: Time,
speed: f64,
pub ease: Easing,
}
impl ScrollSpeedChangeEvent {
pub fn new(time: Time, speed: f64, ease: Easing) -> Option<ScrollSpeedChangeEvent> {
if speed.is_finite() {
Some(Self { time, speed, ease })
} else {
None
}
}
pub fn speed(&self) -> f64 {
self.speed
}
pub fn with_speed(self, speed: f64) -> Option<Self> {
Self::new(self.time, speed, self.ease)
}
}
impl TrackEvent for ScrollSpeedChangeEvent {
type Cumulated = ScrollPosition;
fn time(&self) -> Time {
self.time
}
fn cumulate<'id>(
current_cumulated: Self::Cumulated,
delta: ClockTime,
full_duration: ClockTime,
current: &Self,
next: &Self,
) -> Self::Cumulated {
if full_duration.seconds() <= 1e-6 {
return current_cumulated;
}
let p = delta / full_duration;
let val = current_cumulated.0
+ current.ease.integrate(current.speed, next.speed, p) * full_duration.seconds();
if !val.is_finite() {
return ScrollPosition(f64::INFINITY);
}
ScrollPosition(val)
}
fn initial_cumulate(first_time: ClockTime, event: &Self) -> Self::Cumulated {
ScrollPosition(first_time.seconds() * event.speed)
}
}
pub type ScrollSpeedTracks = EventTracks<ScrollSpeedTrack>;
#[derive(Default, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScrollPosition(f64);
impl ScrollPosition {
pub const ZERO: Self = Self(0.);
pub const INF: Self = Self(f64::INFINITY);
pub fn new(position: f64) -> Option<Self> {
(!position.is_nan()).then_some(Self(position))
}
pub fn pos(self) -> f64 {
self.0
}
pub fn scale(self, s: f64) -> Option<Self> {
let v = self.0 * s;
v.is_nan().then_some(Self(v))
}
}
impl Eq for ScrollPosition {}
impl PartialOrd for ScrollPosition {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ScrollPosition {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0
.partial_cmp(&other.0)
.expect("Scroll position is never NaN")
}
}
impl Add for ScrollPosition {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let v = self.0 + rhs.0;
if v.is_nan() {
Self(f64::INFINITY)
} else {
Self(v)
}
}
}
impl Sub for ScrollPosition {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
let v = self.0 - rhs.0;
if v.is_nan() {
Self(f64::INFINITY)
} else {
Self(v)
}
}
}
impl Div for ScrollPosition {
type Output = f64;
fn div(self, rhs: Self) -> Self::Output {
self.0 / rhs.0
}
}
#[derive(Debug, Clone)]
pub struct ScrollSpeedTrack(EventTrack<ScrollSpeedChangeEvent>);
impl ScrollSpeedTrack {
pub fn calculate_scroll_position(&self, clock_time: ClockTime) -> ScrollPosition {
if self.is_empty() {
return ScrollPosition(clock_time.seconds());
}
let index = self
.change_clock_times()
.partition_point(|t| *t <= clock_time);
if index == 0 {
let first = self.0.content()[0];
let val = self.0.cumulated()[0].0
- (self.0.clock_times()[0] - clock_time).seconds() * first.speed;
if !val.is_finite() {
return ScrollPosition(f64::INFINITY);
} else {
return ScrollPosition(val);
}
}
if index == self.len() {
let last_index = self.len() - 1;
let last_ev = self.0.content()[last_index];
let val = self.0.cumulated()[last_index].0
+ (clock_time - self.0.clock_times()[last_index]).seconds() * last_ev.speed;
if !val.is_finite() {
return ScrollPosition(f64::INFINITY);
} else {
return ScrollPosition(val);
}
}
let base = self.0.cumulated()[index - 1].0;
let elapsed_time_since = clock_time - self.0.clock_times()[index - 1];
let full_elapsed_time = self.0.clock_times()[index] - self.0.clock_times()[index - 1];
let sp0 = self.0.content()[index - 1].speed;
let sp1 = self.0.content()[index].speed;
let ease = self.0.content()[index - 1].ease;
let p = elapsed_time_since / full_elapsed_time;
let val = base + ease.integrate(sp0, sp1, p) * full_elapsed_time.seconds();
if !val.is_finite() {
return ScrollPosition(f64::INFINITY);
}
ScrollPosition(val)
}
pub fn new(tempo_track_id: TrackId) -> Self {
Self(EventTrack::new(tempo_track_id))
}
pub fn with_events(
tempo_track_id: TrackId,
events: Vec<ScrollSpeedChangeEvent>,
tempo_tracks: &TempoTracks,
) -> Self {
Self(EventTrack::with_events(
tempo_track_id,
events,
tempo_tracks,
))
}
pub fn add_event(&mut self, event: ScrollSpeedChangeEvent, tempo_tracks: &TempoTracks) {
self.0.add_event(event, tempo_tracks)
}
pub fn add_events(
&mut self,
events: impl Iterator<Item = ScrollSpeedChangeEvent>,
tempo_tracks: &TempoTracks,
) {
self.0.add_events(events, tempo_tracks)
}
pub fn remove_event(&mut self, id: u64) {
self.0.remove_event(id)
}
pub fn remove_events(&mut self, ids: impl IntoIterator<Item = u64>) {
self.0.remove_events(ids)
}
pub fn replace_event(
&mut self,
id: u64,
replace_with: ScrollSpeedChangeEvent,
tempo_tracks: &TempoTracks,
) {
self.0.replace_event(id, replace_with, tempo_tracks)
}
pub fn replace_events(
&mut self,
events: impl Iterator<Item = (u64, ScrollSpeedChangeEvent)>,
tempo_tracks: &TempoTracks,
) {
self.0.replace_events(events, tempo_tracks);
}
pub fn rebuild_after_tempo_change(&mut self, tempo_tracks: &TempoTracks) {
self.0.rebuild_after_tempo_change(tempo_tracks);
}
pub fn change_tempo_track(&mut self, tempo_track_id: TrackId, tempo_tracks: &TempoTracks) {
self.0.change_tempo_track(tempo_track_id, tempo_tracks);
}
#[inline]
pub fn change_times(&self) -> &[Time] {
self.0.times()
}
#[inline]
pub fn change_clock_times(&self) -> &[ClockTime] {
self.0.clock_times()
}
#[inline]
pub fn speed_changes(&self) -> &[ScrollSpeedChangeEvent] {
self.0.content()
}
#[inline]
pub fn cumulative_scroll_position(&self) -> &[ScrollPosition] {
self.0.cumulated()
}
#[inline]
pub fn len(&self) -> usize {
self.0.ids().len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.0.ids().is_empty()
}
}
#[cfg(test)]
mod test {
use crate::rhythm::ClockTime;
use super::*;
fn test_tempo_tracks() -> TempoTracks {
TempoTracks::with_tracks(
crate::rhythm::TempoTrack::with_events(vec![crate::rhythm::TempoChangeEvent {
time: Time::Clock(ClockTime::ZERO),
tempo: crate::rhythm::Tempo::from_bpm(60.0).unwrap(),
ease: crate::math::Easing::InConst,
}])
.unwrap(),
std::iter::empty(),
)
}
fn clock_time(s: f64) -> Time {
Time::Clock(ClockTime::from_seconds(s).unwrap())
}
#[test]
fn query_empty_track() {
let scroll_track = ScrollSpeedTrack::new(TrackId::Main);
let timing = ClockTime::from_seconds(10.0).unwrap();
assert_eq!(
scroll_track.calculate_scroll_position(timing),
ScrollPosition(10.0)
);
}
#[test]
fn query_with_speed_change() {
let ease = Easing::InConst;
let scroll_track = ScrollSpeedTrack::with_events(
TrackId::Main,
vec![
ScrollSpeedChangeEvent::new(clock_time(0.), 2., ease).unwrap(),
ScrollSpeedChangeEvent::new(clock_time(1.), 1., ease).unwrap(),
],
&test_tempo_tracks(),
);
let timing = ClockTime::from_seconds(2.).unwrap();
assert_eq!(
scroll_track.calculate_scroll_position(timing),
ScrollPosition(3.)
);
}
#[test]
fn query_with_multiple_speed_change() {
let ease = Easing::InConst;
let scroll_track = ScrollSpeedTrack::with_events(
TrackId::Main,
vec![
ScrollSpeedChangeEvent::new(clock_time(0.), 1., ease).unwrap(),
ScrollSpeedChangeEvent::new(clock_time(1.), 2., ease).unwrap(),
ScrollSpeedChangeEvent::new(clock_time(2.), 3., ease).unwrap(),
ScrollSpeedChangeEvent::new(clock_time(3.), 4., ease).unwrap(),
ScrollSpeedChangeEvent::new(clock_time(4.), 5., ease).unwrap(),
ScrollSpeedChangeEvent::new(clock_time(5.), 6., ease).unwrap(),
],
&test_tempo_tracks(),
);
let timing = ClockTime::from_seconds(3.5).unwrap();
assert_eq!(
scroll_track.calculate_scroll_position(timing),
ScrollPosition(1.0 + 2.0 + 3.0 + 4.0 * 0.5)
);
}
#[test]
fn query_before_first_point() {
let ease = Easing::InConst;
let scroll_track = ScrollSpeedTrack::with_events(
TrackId::Main,
vec![ScrollSpeedChangeEvent::new(clock_time(0.), 1., ease).unwrap()],
&test_tempo_tracks(),
);
let timing = ClockTime::from_seconds(-5.).unwrap();
assert_eq!(
scroll_track.calculate_scroll_position(timing),
ScrollPosition(-5.)
);
}
#[test]
fn query_after_last_point() {
let ease = Easing::InConst;
let scroll_track = ScrollSpeedTrack::with_events(
TrackId::Main,
vec![ScrollSpeedChangeEvent::new(clock_time(10.), 1., ease).unwrap()],
&test_tempo_tracks(),
);
let timing = ClockTime::from_seconds(20.).unwrap();
assert_eq!(
scroll_track.calculate_scroll_position(timing),
ScrollPosition(20.)
);
}
#[test]
fn query_speed_changes_at_same_timing() {
let ease = Easing::InConst;
let scroll_track = ScrollSpeedTrack::with_events(
TrackId::Main,
vec![
ScrollSpeedChangeEvent::new(clock_time(0.), 1., ease).unwrap(),
ScrollSpeedChangeEvent::new(clock_time(10.), 1., ease).unwrap(),
ScrollSpeedChangeEvent::new(clock_time(10.), 2., ease).unwrap(),
ScrollSpeedChangeEvent::new(clock_time(10.), 3., ease).unwrap(),
ScrollSpeedChangeEvent::new(clock_time(10.), 4., ease).unwrap(),
],
&test_tempo_tracks(),
);
let timing = ClockTime::from_seconds(20.).unwrap();
assert_eq!(
scroll_track.calculate_scroll_position(timing),
ScrollPosition(10. + 4. * 10.)
);
}
#[test]
fn query_with_linear_speed_change() {
let ease = Easing::Linear;
let scroll_track = ScrollSpeedTrack::with_events(
TrackId::Main,
vec![
ScrollSpeedChangeEvent::new(clock_time(0.), 2., ease).unwrap(),
ScrollSpeedChangeEvent::new(clock_time(1.), 1., ease).unwrap(),
],
&test_tempo_tracks(),
);
let timing = ClockTime::from_seconds(2.).unwrap();
assert_eq!(
scroll_track.calculate_scroll_position(timing),
ScrollPosition(1.0 + ease.integrate(2.0, 1.0, 1.0))
);
}
#[test]
fn query_with_non_zero_start_time() {
let ease = Easing::Linear;
let scroll_track = ScrollSpeedTrack::with_events(
TrackId::Main,
vec![ScrollSpeedChangeEvent::new(clock_time(10.), 1., ease).unwrap()],
&test_tempo_tracks(),
);
let timing = ClockTime::from_seconds(5.).unwrap();
assert_eq!(
scroll_track.calculate_scroll_position(timing),
ScrollPosition(5.0)
);
let timing = ClockTime::from_seconds(0.).unwrap();
assert_eq!(
scroll_track.calculate_scroll_position(timing),
ScrollPosition(0.0)
);
}
use proptest::prelude::*;
fn arb_time() -> impl Strategy<Value = Time> {
let non_nan = prop::num::f64::POSITIVE | prop::num::f64::NEGATIVE;
(non_nan).prop_map(|t| Time::Clock(ClockTime::from_seconds(t).unwrap()))
}
fn arb_event() -> impl Strategy<Value = ScrollSpeedChangeEvent> {
let non_nan = prop::num::f64::POSITIVE | prop::num::f64::NEGATIVE;
(non_nan, non_nan).prop_map(|(t, s)| ScrollSpeedChangeEvent {
time: Time::Clock(ClockTime::from_seconds(t).unwrap()),
speed: s,
ease: Easing::Linear,
})
}
fn arb_clock_events(max_len: usize) -> impl Strategy<Value = Vec<ScrollSpeedChangeEvent>> {
prop::collection::vec(arb_event(), 0..=max_len)
}
fn arb_clock_track(max_len: usize) -> impl Strategy<Value = ScrollSpeedTrack> {
arb_clock_events(max_len)
.prop_map(|v| ScrollSpeedTrack::with_events(TrackId::Main, v, &test_tempo_tracks()))
}
proptest! {
#[test]
fn query_never_triggers_nan(track in arb_clock_track(100), time in arb_time()) {
let Time::Clock(clock_time) = time else {
panic!("Test input must be clock time")
};
let scroll_pos = track.calculate_scroll_position(clock_time);
assert!(!scroll_pos.0.is_nan())
}
}
}