///|
/// Build a scalar track from strictly increasing keyframes.
pub fn ScalarTrack::new(
frames : Array[Keyframe],
) -> ScalarTrack raise MotionError {
if frames.length() == 0 {
raise MotionError::EmptyTrack
}
for i in 0.. 0 && frames[i].time <= frames[i - 1].time {
raise MotionError::NonIncreasingKeyframe(
frames[i - 1].time,
frames[i].time,
)
}
}
{ frames: frames.copy() }
}
///|
fn ScalarTrack::track_time(
self : ScalarTrack,
time : Double,
mode : Extrapolation,
) -> Double {
let first = self.start_time()
let last = self.end_time()
let span = last - first
if span <= 0.0 || mode is Clamp {
if time < first {
first
} else if time > last {
last
} else {
time
}
} else if time < first {
match mode {
Continue => time
Repeat => {
let offset = time - first
let cycles = @math.floor(offset / span)
first + offset - cycles * span
}
Mirror => {
let offset = time - first
let cycles = @math.floor(offset / span)
let part = offset - cycles * span
if cycles.to_int() % 2 == 0 {
first + part
} else {
last - part
}
}
Clamp => first
}
} else if time > last {
match mode {
Continue => time
Repeat => {
let offset = time - first
let cycles = @math.floor(offset / span)
first + offset - cycles * span
}
Mirror => {
let offset = time - first
let cycles = @math.floor(offset / span)
let part = offset - cycles * span
if cycles.to_int() % 2 == 0 {
first + part
} else {
last - part
}
}
Clamp => last
}
} else {
time
}
}
///|
fn ScalarTrack::sample_track_inside(
self : ScalarTrack,
time : Double,
) -> Double {
let last_index = self.frames.length() - 1
if time <= self.frames[0].time {
return self.frames[0].value
}
if time >= self.frames[last_index].time {
return self.frames[last_index].value
}
let mut segment = 0
for i in 0..= time {
segment = i
break
}
}
let left = self.frames[segment]
let right = self.frames[segment + 1]
let ratio = (time - left.time) / (right.time - left.time)
let progress = left.curve.apply(ratio)
left.value + (right.value - left.value) * progress
}
///|
fn ScalarTrack::extrapolate_track(self : ScalarTrack, time : Double) -> Double {
let first = self.frames[0]
let last_index = self.frames.length() - 1
let last = self.frames[last_index]
if time < first.time {
if last_index == 0 {
first.value
} else {
let next = self.frames[1]
let slope = (next.value - first.value) / (next.time - first.time)
first.value + (time - first.time) * slope
}
} else if time > last.time {
if last_index == 0 {
last.value
} else {
let before = self.frames[last_index - 1]
let slope = (last.value - before.value) / (last.time - before.time)
last.value + (time - last.time) * slope
}
} else {
self.sample_track_inside(time)
}
}
///|
/// Sample at an arbitrary time using the selected boundary policy.
pub fn ScalarTrack::sample(
self : ScalarTrack,
time : Double,
mode? : Extrapolation = Clamp,
) -> Double {
let mapped = self.track_time(time, mode)
if mode is Continue {
self.extrapolate_track(mapped)
} else {
self.sample_track_inside(mapped)
}
}
///|
/// Estimate velocity with a central difference that respects track duration.
pub fn ScalarTrack::velocity(
self : ScalarTrack,
time : Double,
mode? : Extrapolation = Clamp,
) -> Double {
let delta = if self.duration() <= 0.0 {
0.0001
} else {
(self.duration() / 10000.0).max(0.000001)
}
(self.sample(time + delta, mode~) - self.sample(time - delta, mode~)) /
(2.0 * delta)
}
///|
pub fn ScalarTrack::sample_motion(
self : ScalarTrack,
time : Double,
mode? : Extrapolation = Clamp,
) -> MotionSample {
let delta = if self.duration() <= 0.0 {
0.0001
} else {
(self.duration() / 10000.0).max(0.000001)
}
let value = self.sample(time, mode~)
let before = self.sample(time - delta, mode~)
let after = self.sample(time + delta, mode~)
{
time,
value,
velocity: (after - before) / (2.0 * delta),
acceleration: (after - 2.0 * value + before) / (delta * delta),
}
}
///|
/// A mutable builder useful for editors that add frames interactively.
pub struct TrackBuilder {
frames : Array[Keyframe]
}
///|
pub fn TrackBuilder::new() -> TrackBuilder {
{ frames: [] }
}
///|
pub fn TrackBuilder::add(self : TrackBuilder, frame : Keyframe) -> Unit {
self.frames.push(frame)
self.frames.sort_by(fn(a, b) { a.time.compare(b.time) })
}
///|
pub fn TrackBuilder::remove_at(self : TrackBuilder, index : Int) -> Keyframe? {
if index < 0 || index >= self.frames.length() {
None
} else {
Some(self.frames.remove(index))
}
}
///|
pub fn TrackBuilder::replace(
self : TrackBuilder,
index : Int,
frame : Keyframe,
) -> Bool {
if index < 0 || index >= self.frames.length() {
false
} else {
self.frames[index] = frame
self.frames.sort_by(fn(a, b) { a.time.compare(b.time) })
true
}
}
///|
pub fn TrackBuilder::build(
self : TrackBuilder,
) -> ScalarTrack raise MotionError {
ScalarTrack::new(self.frames)
}