style/
piecewise_linear.rs

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
6use crate::derives::*;
7use crate::values::computed::Percentage;
8use core::slice::Iter;
9/// draft as in https://github.com/w3c/csswg-drafts/pull/6533.
10use euclid::approxeq::ApproxEq;
11use itertools::Itertools;
12use std::fmt::{self, Write};
13use style_traits::{CssWriter, ToCss};
14
15use crate::values::CSSFloat;
16
17type 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)]
33pub struct PiecewiseLinearFunctionEntry {
34    pub x: ValueType,
35    pub y: ValueType,
36}
37
38impl 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)]
65pub 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.
73pub type PiecewiseLinearFunctionBuildParameters = (CSSFloat, Option<CSSFloat>);
74
75impl 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)]
149struct BuildEntry {
150    x: Option<ValueType>,
151    y: ValueType,
152}
153
154/// Builder object to generate a linear function.
155#[derive(Default)]
156pub struct PiecewiseLinearFunctionBuilder {
157    largest_x: Option<ValueType>,
158    smallest_x: Option<ValueType>,
159    entries: Vec<BuildEntry>,
160}
161
162impl 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}