tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

piecewise_linear.rs (10178B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 //! A piecewise linear function, following CSS linear easing
      6 use crate::derives::*;
      7 use crate::values::computed::Percentage;
      8 use core::slice::Iter;
      9 /// draft as in https://github.com/w3c/csswg-drafts/pull/6533.
     10 use euclid::approxeq::ApproxEq;
     11 use itertools::Itertools;
     12 use std::fmt::{self, Write};
     13 use style_traits::{CssWriter, ToCss};
     14 
     15 use crate::values::CSSFloat;
     16 
     17 type ValueType = CSSFloat;
     18 /// a single entry in a piecewise linear function.
     19 #[allow(missing_docs)]
     20 #[derive(
     21    Clone,
     22    Copy,
     23    Debug,
     24    MallocSizeOf,
     25    PartialEq,
     26    SpecifiedValueInfo,
     27    ToResolvedValue,
     28    ToShmem,
     29    Serialize,
     30    Deserialize,
     31 )]
     32 #[repr(C)]
     33 pub struct PiecewiseLinearFunctionEntry {
     34    pub x: ValueType,
     35    pub y: ValueType,
     36 }
     37 
     38 impl ToCss for PiecewiseLinearFunctionEntry {
     39    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
     40    where
     41        W: fmt::Write,
     42    {
     43        self.y.to_css(dest)?;
     44        dest.write_char(' ')?;
     45        Percentage(self.x).to_css(dest)
     46    }
     47 }
     48 
     49 /// Representation of a piecewise linear function, a series of linear functions.
     50 #[derive(
     51    Default,
     52    Clone,
     53    Debug,
     54    MallocSizeOf,
     55    PartialEq,
     56    SpecifiedValueInfo,
     57    ToResolvedValue,
     58    ToCss,
     59    ToShmem,
     60    Serialize,
     61    Deserialize,
     62 )]
     63 #[repr(C)]
     64 #[css(comma)]
     65 pub struct PiecewiseLinearFunction {
     66    #[css(iterable)]
     67    #[ignore_malloc_size_of = "Arc"]
     68    #[shmem(field_bound)]
     69    entries: crate::ArcSlice<PiecewiseLinearFunctionEntry>,
     70 }
     71 
     72 /// Parameters to define one linear stop.
     73 pub type PiecewiseLinearFunctionBuildParameters = (CSSFloat, Option<CSSFloat>);
     74 
     75 impl PiecewiseLinearFunction {
     76    /// Interpolate y value given x and two points. The linear function will be rooted at the asymptote.
     77    fn interpolate(
     78        x: ValueType,
     79        prev: PiecewiseLinearFunctionEntry,
     80        next: PiecewiseLinearFunctionEntry,
     81        asymptote: &PiecewiseLinearFunctionEntry,
     82    ) -> ValueType {
     83        // Short circuit if the x is on prev or next.
     84        // `next` point is preferred as per spec.
     85        if x.approx_eq(&next.x) {
     86            return next.y;
     87        }
     88        if x.approx_eq(&prev.x) {
     89            return prev.y;
     90        }
     91        // Avoid division by zero.
     92        if prev.x.approx_eq(&next.x) {
     93            return next.y;
     94        }
     95        let slope = (next.y - prev.y) / (next.x - prev.x);
     96        return slope * (x - asymptote.x) + asymptote.y;
     97    }
     98 
     99    /// Get the y value of the piecewise linear function given the x value, as per
    100    /// https://drafts.csswg.org/css-easing-2/#linear-easing-function-output
    101    pub fn at(&self, x: ValueType) -> ValueType {
    102        if !x.is_finite() {
    103            return if x > 0.0 { 1.0 } else { 0.0 };
    104        }
    105        if self.entries.is_empty() {
    106            // Implied y = x, as per spec.
    107            return x;
    108        }
    109        if self.entries.len() == 1 {
    110            // Implied y = <constant>, as per spec.
    111            return self.entries[0].y;
    112        }
    113        // Spec dictates the valid input domain is [0, 1]. Outside of this range, the output
    114        // should be calculated as if the slopes at start and end extend to infinity. However, if the
    115        // start/end have two points of the same position, the line should extend along the x-axis.
    116        // The function doesn't have to cover the input domain, in which case the extension logic
    117        // applies even if the input falls in the input domain.
    118        // Also, we're guaranteed to have at least two elements at this point.
    119        if x < self.entries[0].x {
    120            return Self::interpolate(x, self.entries[0], self.entries[1], &self.entries[0]);
    121        }
    122        let mut rev_iter = self.entries.iter().rev();
    123        let last = rev_iter.next().unwrap();
    124        if x >= last.x {
    125            let second_last = rev_iter.next().unwrap();
    126            return Self::interpolate(x, *second_last, *last, last);
    127        }
    128 
    129        // Now we know the input sits within the domain explicitly defined by our function.
    130        for (point_b, point_a) in self.entries.iter().rev().tuple_windows() {
    131            // Need to let point A be the _last_ point where its x is less than the input x,
    132            // hence the reverse traversal.
    133            if x < point_a.x {
    134                continue;
    135            }
    136            return Self::interpolate(x, *point_a, *point_b, point_a);
    137        }
    138        unreachable!("Input is supposed to be within the entries' min & max!");
    139    }
    140 
    141    #[allow(missing_docs)]
    142    pub fn iter(&self) -> Iter<'_, PiecewiseLinearFunctionEntry> {
    143        self.entries.iter()
    144    }
    145 }
    146 
    147 /// Entry of a piecewise linear function while building, where the calculation of x value can be deferred.
    148 #[derive(Clone, Copy)]
    149 struct BuildEntry {
    150    x: Option<ValueType>,
    151    y: ValueType,
    152 }
    153 
    154 /// Builder object to generate a linear function.
    155 #[derive(Default)]
    156 pub struct PiecewiseLinearFunctionBuilder {
    157    largest_x: Option<ValueType>,
    158    smallest_x: Option<ValueType>,
    159    entries: Vec<BuildEntry>,
    160 }
    161 
    162 impl PiecewiseLinearFunctionBuilder {
    163    /// Create a builder for a known amount of linear stop entries.
    164    pub fn with_capacity(len: usize) -> Self {
    165        PiecewiseLinearFunctionBuilder {
    166            largest_x: None,
    167            smallest_x: None,
    168            entries: Vec::with_capacity(len),
    169        }
    170    }
    171 
    172    fn create_entry(&mut self, y: ValueType, x: Option<ValueType>) {
    173        let x = match x {
    174            Some(x) if x.is_finite() => x,
    175            _ if self.entries.is_empty() => 0.0, // First x is 0 if not specified (Or not finite)
    176            _ => {
    177                self.entries.push(BuildEntry { x: None, y });
    178                return;
    179            },
    180        };
    181        // Specified x value cannot regress, as per spec.
    182        let x = match self.largest_x {
    183            Some(largest_x) => x.max(largest_x),
    184            None => x,
    185        };
    186        self.largest_x = Some(x);
    187        // Whatever we see the earliest is the smallest value.
    188        if self.smallest_x.is_none() {
    189            self.smallest_x = Some(x);
    190        }
    191        self.entries.push(BuildEntry { x: Some(x), y });
    192    }
    193 
    194    /// Add a new entry into the piecewise linear function with specified y value.
    195    /// If the start x value is given, that is where the x value will be. Otherwise,
    196    /// the x value is calculated later. If the end x value is specified, a flat segment
    197    /// is generated. If start x value is not specified but end x is, it is treated as
    198    /// start x.
    199    pub fn push(&mut self, y: CSSFloat, x_start: Option<CSSFloat>) {
    200        self.create_entry(y, x_start)
    201    }
    202 
    203    /// Finish building the piecewise linear function by resolving all undefined x values,
    204    /// then return the result.
    205    pub fn build(mut self) -> PiecewiseLinearFunction {
    206        if self.entries.is_empty() {
    207            return PiecewiseLinearFunction::default();
    208        }
    209        if self.entries.len() == 1 {
    210            // Don't bother resolving anything.
    211            return PiecewiseLinearFunction {
    212                entries: crate::ArcSlice::from_iter(std::iter::once(
    213                    PiecewiseLinearFunctionEntry {
    214                        x: 0.,
    215                        y: self.entries[0].y,
    216                    },
    217                )),
    218            };
    219        }
    220        // Guaranteed at least two elements.
    221        // Start element's x value should've been assigned when the first value was pushed.
    222        debug_assert!(
    223            self.entries[0].x.is_some(),
    224            "Expected an entry with x defined!"
    225        );
    226        // Spec asserts that if the last entry does not have an x value, it is assigned the largest seen x value.
    227        self.entries
    228            .last_mut()
    229            .unwrap()
    230            .x
    231            .get_or_insert(self.largest_x.filter(|x| x > &1.0).unwrap_or(1.0));
    232        // Now we have at least two elements with x values, with start & end x values guaranteed.
    233 
    234        let mut result = Vec::with_capacity(self.entries.len());
    235        result.push(PiecewiseLinearFunctionEntry {
    236            x: self.entries[0].x.unwrap(),
    237            y: self.entries[0].y,
    238        });
    239        for (i, e) in self.entries.iter().enumerate().skip(1) {
    240            if e.x.is_none() {
    241                // Need to calculate x values by first finding an entry with the first
    242                // defined x value (Guaranteed to exist as the list end has it defined).
    243                continue;
    244            }
    245            // x is defined for this element.
    246            let divisor = i - result.len() + 1;
    247            // Any element(s) with undefined x to assign?
    248            if divisor != 1 {
    249                // Have at least one element in result at all times.
    250                let start_x = result.last().unwrap().x;
    251                let increment = (e.x.unwrap() - start_x) / divisor as ValueType;
    252                // Grab every element with undefined x to this point, which starts at the end of the result
    253                // array, and ending right before the current index. Then, assigned the evenly divided
    254                // x values.
    255                result.extend(
    256                    self.entries[result.len()..i]
    257                        .iter()
    258                        .enumerate()
    259                        .map(|(j, e)| {
    260                            debug_assert!(e.x.is_none(), "Expected an entry with x undefined!");
    261                            PiecewiseLinearFunctionEntry {
    262                                x: increment * (j + 1) as ValueType + start_x,
    263                                y: e.y,
    264                            }
    265                        }),
    266                );
    267            }
    268            result.push(PiecewiseLinearFunctionEntry {
    269                x: e.x.unwrap(),
    270                y: e.y,
    271            });
    272        }
    273        debug_assert_eq!(
    274            result.len(),
    275            self.entries.len(),
    276            "Should've mapped one-to-one!"
    277        );
    278        PiecewiseLinearFunction {
    279            entries: crate::ArcSlice::from_iter(result.into_iter()),
    280        }
    281    }
    282 }