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 }