/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! A piecewise linear function, following CSS linear easing use crate::values::computed::Percentage; use core::slice::Iter; /// draft as in https://github.com/w3c/csswg-drafts/pull/6533. use euclid::approxeq::ApproxEq; use itertools::Itertools; use std::fmt::{self, Write}; use style_traits::{CssWriter, ToCss}; use crate::values::CSSFloat; type ValueType = CSSFloat; /// a single entry in a piecewise linear function. #[allow(missing_docs)] #[derive( Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToResolvedValue, Serialize, Deserialize, )] #[repr(C)] pub struct PiecewiseLinearFunctionEntry { pub x: ValueType, pub y: ValueType, } impl ToCss for PiecewiseLinearFunctionEntry { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { self.y.to_css(dest)?; dest.write_str(" ")?; Percentage(self.x).to_css(dest) } } /// Representation of a piecewise linear function, a series of linear functions. #[derive( Default, Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToResolvedValue, ToCss, Serialize, Deserialize, )] #[repr(C)] #[css(comma)] pub struct PiecewiseLinearFunction { #[css(iterable)] entries: crate::OwnedSlice, } /// Parameters to define one linear stop. pub type PiecewiseLinearFunctionBuildParameters = (CSSFloat, Option); impl PiecewiseLinearFunction { /// Interpolate y value given x and two points. The linear function will be rooted at the asymptote. fn interpolate( x: ValueType, prev: PiecewiseLinearFunctionEntry, next: PiecewiseLinearFunctionEntry, asymptote: &PiecewiseLinearFunctionEntry, ) -> ValueType { // Short circuit if the x is on prev or next. // `next` point is preferred as per spec. if x.approx_eq(&next.x) { return next.y; } if x.approx_eq(&prev.x) { return prev.y; } // Avoid division by zero. if prev.x.approx_eq(&next.x) { return next.y; } let slope = (next.y - prev.y) / (next.x - prev.x); return slope * (x - asymptote.x) + asymptote.y; } /// Get the y value of the piecewise linear function given the x value, as per /// https://drafts.csswg.org/css-easing-2/#linear-easing-function-output pub fn at(&self, x: ValueType) -> ValueType { if !x.is_finite() { return if x > 0.0 { 1.0 } else { 0.0 }; } if self.entries.is_empty() { // Implied y = x, as per spec. return x; } if self.entries.len() == 1 { // Implied y = , as per spec. return self.entries[0].y; } // Spec dictates the valid input domain is [0, 1]. Outside of this range, the output // should be calculated as if the slopes at start and end extend to infinity. However, if the // start/end have two points of the same position, the line should extend along the x-axis. // The function doesn't have to cover the input domain, in which case the extension logic // applies even if the input falls in the input domain. // Also, we're guaranteed to have at least two elements at this point. if x < self.entries[0].x { return Self::interpolate(x, self.entries[0], self.entries[1], &self.entries[0]); } let mut rev_iter = self.entries.iter().rev(); let last = rev_iter.next().unwrap(); if x >= last.x { let second_last = rev_iter.next().unwrap(); return Self::interpolate(x, *second_last, *last, last); } // Now we know the input sits within the domain explicitly defined by our function. for (point_b, point_a) in self.entries.iter().rev().tuple_windows() { // Need to let point A be the _last_ point where its x is less than the input x, // hence the reverse traversal. if x < point_a.x { continue; } return Self::interpolate(x, *point_a, *point_b, point_a); } unreachable!("Input is supposed to be within the entries' min & max!"); } /// Create the piecewise linear function from an iterator that generates the parameter tuple. pub fn from_iter(iter: Iter) -> Self where Iter: Iterator + ExactSizeIterator, { let mut builder = PiecewiseLinearFunctionBuilder::with_capacity(iter.len()); for (y, x_start) in iter { builder = builder.push(y, x_start); } builder.build() } #[allow(missing_docs)] pub fn iter(&self) -> Iter { self.entries.iter() } } /// Entry of a piecewise linear function while building, where the calculation of x value can be deferred. #[derive(Clone, Copy)] struct BuildEntry { x: Option, y: ValueType, } /// Builder object to generate a linear function. #[derive(Default)] pub struct PiecewiseLinearFunctionBuilder { largest_x: Option, smallest_x: Option, entries: Vec, } impl PiecewiseLinearFunctionBuilder { #[allow(missing_docs)] pub fn new() -> Self { PiecewiseLinearFunctionBuilder::default() } /// Create a builder for a known amount of linear stop entries. pub fn with_capacity(len: usize) -> Self { PiecewiseLinearFunctionBuilder { largest_x: None, smallest_x: None, entries: Vec::with_capacity(len), } } fn create_entry(&mut self, y: ValueType, x: Option) { let x = match x { Some(x) if x.is_finite() => x, _ if self.entries.is_empty() => 0.0, // First x is 0 if not specified (Or not finite) _ => { self.entries.push(BuildEntry { x: None, y }); return; }, }; // Specified x value cannot regress, as per spec. let x = match self.largest_x { Some(largest_x) => x.max(largest_x), None => x, }; self.largest_x = Some(x); // Whatever we see the earliest is the smallest value. if self.smallest_x.is_none() { self.smallest_x = Some(x); } self.entries.push(BuildEntry { x: Some(x), y }); } /// Add a new entry into the piecewise linear function with specified y value. /// If the start x value is given, that is where the x value will be. Otherwise, /// the x value is calculated later. If the end x value is specified, a flat segment /// is generated. If start x value is not specified but end x is, it is treated as /// start x. pub fn push(mut self, y: CSSFloat, x_start: Option) -> Self { self.create_entry(y, x_start); self } /// Finish building the piecewise linear function by resolving all undefined x values, /// then return the result. pub fn build(mut self) -> PiecewiseLinearFunction { if self.entries.is_empty() { return PiecewiseLinearFunction::default(); } if self.entries.len() == 1 { // Don't bother resolving anything. return PiecewiseLinearFunction { entries: crate::OwnedSlice::from_slice(&[PiecewiseLinearFunctionEntry { x: 0., y: self.entries[0].y, }]), }; } // Guaranteed at least two elements. // Start element's x value should've been assigned when the first value was pushed. debug_assert!( self.entries[0].x.is_some(), "Expected an entry with x defined!" ); // Spec asserts that if the last entry does not have an x value, it is assigned the largest seen x value. self.entries .last_mut() .unwrap() .x .get_or_insert(self.largest_x.filter(|x| x > &1.0).unwrap_or(1.0)); // Now we have at least two elements with x values, with start & end x values guaranteed. let mut result = Vec::with_capacity(self.entries.len()); result.push(PiecewiseLinearFunctionEntry { x: self.entries[0].x.unwrap(), y: self.entries[0].y, }); for (i, e) in self.entries.iter().enumerate().skip(1) { if e.x.is_none() { // Need to calculate x values by first finding an entry with the first // defined x value (Guaranteed to exist as the list end has it defined). continue; } // x is defined for this element. let divisor = i - result.len() + 1; // Any element(s) with undefined x to assign? if divisor != 1 { // Have at least one element in result at all times. let start_x = result.last().unwrap().x; let increment = (e.x.unwrap() - start_x) / divisor as ValueType; // Grab every element with undefined x to this point, which starts at the end of the result // array, and ending right before the current index. Then, assigned the evenly divided // x values. result.extend( self.entries[result.len()..i] .iter() .enumerate() .map(|(j, e)| { debug_assert!(e.x.is_none(), "Expected an entry with x undefined!"); PiecewiseLinearFunctionEntry { x: increment * (j + 1) as ValueType + start_x, y: e.y, } }), ); } result.push(PiecewiseLinearFunctionEntry { x: e.x.unwrap(), y: e.y, }); } debug_assert_eq!( result.len(), self.entries.len(), "Should've mapped one-to-one!" ); PiecewiseLinearFunction { entries: result.into(), } } }