mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	$ git ls-files servo/*.rs | xargs rustup run nightly rustfmt Differential Revision: https://phabricator.services.mozilla.com/D194020
		
			
				
	
	
		
			281 lines
		
	
	
	
		
			9.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			281 lines
		
	
	
	
		
			9.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
/* 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,
 | 
						|
    ToShmem,
 | 
						|
    Serialize,
 | 
						|
    Deserialize,
 | 
						|
)]
 | 
						|
#[repr(C)]
 | 
						|
pub struct PiecewiseLinearFunctionEntry {
 | 
						|
    pub x: ValueType,
 | 
						|
    pub y: ValueType,
 | 
						|
}
 | 
						|
 | 
						|
impl ToCss for PiecewiseLinearFunctionEntry {
 | 
						|
    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
 | 
						|
    where
 | 
						|
        W: fmt::Write,
 | 
						|
    {
 | 
						|
        self.y.to_css(dest)?;
 | 
						|
        dest.write_char(' ')?;
 | 
						|
        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,
 | 
						|
    ToShmem,
 | 
						|
    Serialize,
 | 
						|
    Deserialize,
 | 
						|
)]
 | 
						|
#[repr(C)]
 | 
						|
#[css(comma)]
 | 
						|
pub struct PiecewiseLinearFunction {
 | 
						|
    #[css(iterable)]
 | 
						|
    #[ignore_malloc_size_of = "Arc"]
 | 
						|
    #[shmem(field_bound)]
 | 
						|
    entries: crate::ArcSlice<PiecewiseLinearFunctionEntry>,
 | 
						|
}
 | 
						|
 | 
						|
/// Parameters to define one linear stop.
 | 
						|
pub type PiecewiseLinearFunctionBuildParameters = (CSSFloat, Option<CSSFloat>);
 | 
						|
 | 
						|
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 = <constant>, 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!");
 | 
						|
    }
 | 
						|
 | 
						|
    #[allow(missing_docs)]
 | 
						|
    pub fn iter(&self) -> Iter<PiecewiseLinearFunctionEntry> {
 | 
						|
        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<ValueType>,
 | 
						|
    y: ValueType,
 | 
						|
}
 | 
						|
 | 
						|
/// Builder object to generate a linear function.
 | 
						|
#[derive(Default)]
 | 
						|
pub struct PiecewiseLinearFunctionBuilder {
 | 
						|
    largest_x: Option<ValueType>,
 | 
						|
    smallest_x: Option<ValueType>,
 | 
						|
    entries: Vec<BuildEntry>,
 | 
						|
}
 | 
						|
 | 
						|
impl PiecewiseLinearFunctionBuilder {
 | 
						|
    /// 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<ValueType>) {
 | 
						|
        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<CSSFloat>) {
 | 
						|
        self.create_entry(y, x_start)
 | 
						|
    }
 | 
						|
 | 
						|
    /// 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::ArcSlice::from_iter(std::iter::once(
 | 
						|
                    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: crate::ArcSlice::from_iter(result.into_iter()),
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |