///|
/// Post-processing stages for combining easing curves with editorial ranges.
pub enum CurveStage2 {
StageNormalize
StageClamp(Double, Double)
StageRemap(Double, Double)
StageBias(Double)
StageGain(Double)
StageQuantize(Int)
StageMirror
} derive(Debug)
///|
pub fn curve_stages2() -> Array[CurveStage2] {
[
StageNormalize,
StageClamp(0.0, 1.0),
StageRemap(0.0, 1.0),
StageBias(0.0),
StageGain(1.0),
StageQuantize(8),
StageMirror,
]
}
///|
fn apply_curve_stage(stage : CurveStage2, value : Double) -> Double {
match stage {
StageNormalize => clamp01(value)
StageClamp(minimum, maximum) =>
clamp(value, minimum.min(maximum), maximum.max(minimum))
StageRemap(minimum, maximum) => minimum + value * (maximum - minimum)
StageBias(bias) => value + bias
StageGain(gain) => value * gain
StageQuantize(steps) =>
if steps <= 0 {
value
} else {
@math.floor(value * steps.to_double() + 0.5) / steps.to_double()
}
StageMirror => 1.0 - value
}
}
///|
pub struct CurvePipeline2 {
curve : Curve
stages : Array[CurveStage2]
} derive(Debug)
///|
pub fn curve_pipeline2(curve : Curve) -> CurvePipeline2 {
{ curve, stages: [] }
}
///|
pub fn CurvePipeline2::curve(self : CurvePipeline2) -> Curve {
self.curve
}
///|
pub fn CurvePipeline2::stages(self : CurvePipeline2) -> Array[CurveStage2] {
self.stages.copy()
}
///|
pub fn CurvePipeline2::add(
self : CurvePipeline2,
stage : CurveStage2,
) -> CurvePipeline2 {
let stages = self.stages.copy()
stages.push(stage)
{ curve: self.curve, stages }
}
///|
pub fn CurvePipeline2::then(
self : CurvePipeline2,
other : CurvePipeline2,
) -> CurvePipeline2 {
let stages = self.stages.copy()
for stage in other.stages {
stages.push(stage)
}
{ curve: self.curve, stages }
}
///|
pub fn CurvePipeline2::apply(self : CurvePipeline2, time : Double) -> Double {
let mut value = self.curve.apply(clamp01(time))
for stage in self.stages {
value = apply_curve_stage(stage, value)
}
value
}
///|
pub fn CurvePipeline2::sample_many(
self : CurvePipeline2,
count : Int,
) -> Array[Double] {
let values : Array[Double] = []
let actual = count.max(1)
for index in 0.. CurveLookupTable raise MotionError {
let table = CurveLookupTable::new(self.curve, samples.max(2))
if self.stages.length() == 0 {
return table
}
let values : Array[Double] = []
for index in 0.. 0 {
integral = integral + (values[index - 1] + values[index]) * step * 0.5
}
}
{ values, derivatives, minimum, maximum, integral }
}
///|
pub fn CurvePipeline2::range(
self : CurvePipeline2,
samples : Int,
) -> (Double, Double) {
let values = self.sample_many(samples)
if values.length() == 0 {
return (0.0, 0.0)
}
let mut minimum = values[0]
let mut maximum = values[0]
for value in values {
minimum = minimum.min(value)
maximum = maximum.max(value)
}
(minimum, maximum)
}
///|
pub fn compose_curve_pipeline(
first : CurvePipeline2,
second : CurvePipeline2,
) -> CurvePipeline2 {
first.then(second)
}
///|
pub fn curve_pipeline_error(
pipeline : CurvePipeline2,
table : CurveLookupTable,
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(
(pipeline.apply(time) - table.sample_smooth(time)).abs(),
)
}
maximum
}
///|
pub struct CurveRemap2 {
input_min : Double
input_max : Double
output_min : Double
output_max : Double
extrapolation : Extrapolation
} derive(Debug)
///|
pub fn curve_remap2(
input_min : Double,
input_max : Double,
output_min : Double,
output_max : Double,
) -> CurveRemap2 {
{ input_min, input_max, output_min, output_max, extrapolation: Clamp }
}
///|
pub fn CurveRemap2::with_extrapolation(
self : CurveRemap2,
mode : Extrapolation,
) -> CurveRemap2 {
{
input_min: self.input_min,
input_max: self.input_max,
output_min: self.output_min,
output_max: self.output_max,
extrapolation: mode,
}
}
///|
pub fn CurveRemap2::apply(self : CurveRemap2, value : Double) -> Double {
let denominator = self.input_max - self.input_min
if denominator == 0.0 {
return self.output_min
}
let ratio = (value - self.input_min) / denominator
let mapped = match self.extrapolation {
Clamp => clamp01(ratio)
Continue => ratio
Repeat => ratio - @math.floor(ratio)
Mirror => {
let cycle = @math.floor(ratio)
let fraction = ratio - cycle
if cycle.to_int() % 2 == 0 {
fraction
} else {
1.0 - fraction
}
}
}
self.output_min + mapped * (self.output_max - self.output_min)
}
///|
pub fn remap_curve_values(
values : Array[Double],
remap : CurveRemap2,
) -> Array[Double] {
let result : Array[Double] = []
for value in values {
result.push(remap.apply(value))
}
result
}