///|
/// A uniformly sampled, allocation-free-at-read-time representation of a
/// timing curve. Baking is useful when a render loop evaluates the same curve
/// for thousands of particles or UI properties.
pub struct CurveLookupTable {
values : Array[Double]
derivatives : Array[Double]
minimum : Double
maximum : Double
integral : Double
} derive(Debug)
///|
pub fn CurveLookupTable::new(
curve : Curve,
samples : Int,
) -> CurveLookupTable raise MotionError {
if samples < 2 {
raise MotionError::InvalidSampleCount(samples)
}
let values : Array[Double] = []
let derivatives : Array[Double] = []
let step = 1.0 / (samples - 1).to_double()
let mut minimum = curve.apply(0.0)
let mut maximum = minimum
let mut area = 0.0
for index in 0.. 0 {
area = area + (values[index - 1] + value) * step * 0.5
}
}
for index in 0.. Array[Double] {
self.values.copy()
}
///|
pub fn CurveLookupTable::derivatives(self : CurveLookupTable) -> Array[Double] {
self.derivatives.copy()
}
///|
pub fn CurveLookupTable::samples(self : CurveLookupTable) -> Int {
self.values.length()
}
///|
pub fn CurveLookupTable::minimum(self : CurveLookupTable) -> Double {
self.minimum
}
///|
pub fn CurveLookupTable::maximum(self : CurveLookupTable) -> Double {
self.maximum
}
///|
pub fn CurveLookupTable::integral(self : CurveLookupTable) -> Double {
self.integral
}
///|
fn CurveLookupTable::index(
self : CurveLookupTable,
time : Double,
) -> (Int, Double) {
let count = self.values.length()
let position = clamp01(time) * (count - 1).to_double()
let lower = @math.floor(position).to_int().min(count - 2)
(lower, position - lower.to_double())
}
///|
pub fn CurveLookupTable::sample(
self : CurveLookupTable,
time : Double,
) -> Double {
let (index, ratio) = self.index(time)
self.values[index] + (self.values[index + 1] - self.values[index]) * ratio
}
///|
pub fn CurveLookupTable::sample_smooth(
self : CurveLookupTable,
time : Double,
) -> Double {
let (index, ratio) = self.index(time)
let left = self.values[index]
let right = self.values[index + 1]
let left_slope = self.derivatives[index]
let right_slope = self.derivatives[index + 1]
let count = (self.values.length() - 1).to_double()
let step = 1.0 / count
let h00 = 2.0 * ratio * ratio * ratio - 3.0 * ratio * ratio + 1.0
let h10 = ratio * ratio * ratio - 2.0 * ratio * ratio + ratio
let h01 = -2.0 * ratio * ratio * ratio + 3.0 * ratio * ratio
let h11 = ratio * ratio * ratio - ratio * ratio
h00 * left + h10 * left_slope * step + h01 * right + h11 * right_slope * step
}
///|
pub fn CurveLookupTable::derivative_at(
self : CurveLookupTable,
time : Double,
) -> Double {
let (index, ratio) = self.index(time)
self.derivatives[index] +
(self.derivatives[index + 1] - self.derivatives[index]) * ratio
}
///|
pub fn CurveLookupTable::inverse(
self : CurveLookupTable,
value : Double,
iterations? : Int = 8,
) -> Double {
let mut low = 0.0
let mut high = 1.0
let increasing = self.values[self.values.length() - 1] >= self.values[0]
let target = clamp(value, self.minimum, self.maximum)
for _ in 0.. Double {
let target = clamp01(time)
let (index, ratio) = self.index(target)
let step = 1.0 / (self.values.length() - 1).to_double()
let mut area = 0.0
for cursor in 0.. CurveLookupTable raise MotionError {
if samples < 2 {
raise MotionError::InvalidSampleCount(samples)
}
let values : Array[Double] = []
let derivatives : Array[Double] = []
let step = 1.0 / (samples - 1).to_double()
let mut minimum = self.sample(0.0)
let mut maximum = minimum
let mut area = 0.0
for index in 0.. 0 {
area = area + (values[index - 1] + value) * step * 0.5
}
}
for index in 0.. Double {
output_min + self.sample(input) * (output_max - output_min)
}
///|
pub fn CurveLookupTable::error(
self : CurveLookupTable,
curve : Curve,
samples : Int,
) -> Double {
let count = samples.max(2)
let mut maximum = 0.0
for index in 0..<=count {
let time = index.to_double() / count.to_double()
maximum = maximum.max((self.sample_smooth(time) - curve.apply(time)).abs())
}
maximum
}
///|
/// A compact collection of baked curves with shared sample density.
pub struct CurveLutBank {
tables : Array[CurveLookupTable]
labels : Array[String]
} derive(Debug)
///|
pub fn curve_lut_bank() -> CurveLutBank {
{ tables: [], labels: [] }
}
///|
pub fn CurveLutBank::length(self : CurveLutBank) -> Int {
self.tables.length()
}
///|
pub fn CurveLutBank::add(
self : CurveLutBank,
label : String,
curve : Curve,
samples : Int,
) -> Unit raise MotionError {
self.labels.push(label)
self.tables.push(CurveLookupTable::new(curve, samples))
}
///|
pub fn CurveLutBank::label(self : CurveLutBank, index : Int) -> String {
self.labels[index]
}
///|
pub fn CurveLutBank::table(
self : CurveLutBank,
index : Int,
) -> CurveLookupTable {
self.tables[index]
}
///|
pub fn CurveLutBank::sample(
self : CurveLutBank,
index : Int,
time : Double,
) -> Double {
self.tables[index].sample(time)
}
///|
pub fn CurveLutBank::samples(
self : CurveLutBank,
time : Double,
) -> Array[Double] {
let result : Array[Double] = []
for table in self.tables {
result.push(table.sample(time))
}
result
}
///|
pub fn CurveLutBank::max_error(
self : CurveLutBank,
curves : Array[Curve],
samples : Int,
) -> Double {
let count = curves.length().min(self.tables.length())
let mut maximum = 0.0
for index in 0.. CurveLutBuilder {
{ curve, samples: 256, adaptive: true, tolerance: 0.000001 }
}
///|
pub fn CurveLutBuilder::samples(self : CurveLutBuilder) -> Int {
self.samples
}
///|
pub fn CurveLutBuilder::with_samples(
self : CurveLutBuilder,
samples : Int,
) -> CurveLutBuilder {
{
curve: self.curve,
samples: samples.max(2),
adaptive: self.adaptive,
tolerance: self.tolerance,
}
}
///|
pub fn CurveLutBuilder::with_tolerance(
self : CurveLutBuilder,
tolerance : Double,
) -> CurveLutBuilder {
{
curve: self.curve,
samples: self.samples,
adaptive: self.adaptive,
tolerance: tolerance.max(0.000000000001),
}
}
///|
pub fn CurveLutBuilder::adaptive(
self : CurveLutBuilder,
enabled : Bool,
) -> CurveLutBuilder {
{
curve: self.curve,
samples: self.samples,
adaptive: enabled,
tolerance: self.tolerance,
}
}
///|
pub fn CurveLutBuilder::build(
self : CurveLutBuilder,
) -> CurveLookupTable raise MotionError {
if !self.adaptive {
return CurveLookupTable::new(self.curve, self.samples)
}
let mut samples = self.samples
let mut table = CurveLookupTable::new(self.curve, samples)
while samples < 8192 && table.error(self.curve, samples * 2) > self.tolerance {
samples = samples * 2
table = CurveLookupTable::new(self.curve, samples)
}
table
}
///|
pub fn CurveLookupTable::monotonic(self : CurveLookupTable) -> Bool {
let increasing = self.values[self.values.length() - 1] >= self.values[0]
for index in 1.. self.values[index - 1] {
return false
}
}
true
}
///|
pub fn CurveLookupTable::peak_speed(
self : CurveLookupTable,
) -> (Double, Double) {
let mut index = 0
for cursor in 1.. self.derivatives[index].abs() {
index = cursor
}
}
(
index.to_double() / (self.values.length() - 1).to_double(),
self.derivatives[index],
)
}
///|
pub fn CurveLookupTable::range(self : CurveLookupTable) -> Double {
self.maximum - self.minimum
}
///|
pub fn CurveLookupTable::normalized(
self : CurveLookupTable,
) -> CurveLookupTable {
let span = self.range()
if span == 0.0 {
return self
}
let values : Array[Double] = []
for value in self.values {
values.push((value - self.minimum) / span)
}
{
values,
derivatives: self.derivatives.copy(),
minimum: 0.0,
maximum: 1.0,
integral: self.integral / span,
}
}