lightningcss/values/
easing.rs

1//! CSS easing functions.
2
3use crate::error::{ParserError, PrinterError};
4use crate::printer::Printer;
5use crate::traits::{Parse, ToCss};
6use crate::values::number::{CSSInteger, CSSNumber};
7#[cfg(feature = "visitor")]
8use crate::visitor::Visit;
9use cssparser::*;
10use std::fmt::Write;
11
12/// A CSS [easing function](https://www.w3.org/TR/css-easing-1/#easing-functions).
13#[derive(Debug, Clone, PartialEq)]
14#[cfg_attr(feature = "visitor", derive(Visit))]
15#[cfg_attr(
16  feature = "serde",
17  derive(serde::Serialize, serde::Deserialize),
18  serde(tag = "type", rename_all = "kebab-case")
19)]
20#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
21#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
22pub enum EasingFunction {
23  /// A linear easing function.
24  Linear,
25  /// Equivalent to `cubic-bezier(0.25, 0.1, 0.25, 1)`.
26  Ease,
27  /// Equivalent to `cubic-bezier(0.42, 0, 1, 1)`.
28  EaseIn,
29  /// Equivalent to `cubic-bezier(0, 0, 0.58, 1)`.
30  EaseOut,
31  /// Equivalent to `cubic-bezier(0.42, 0, 0.58, 1)`.
32  EaseInOut,
33  /// A custom cubic Bézier easing function.
34  CubicBezier {
35    /// The x-position of the first point in the curve.
36    x1: CSSNumber,
37    /// The y-position of the first point in the curve.
38    y1: CSSNumber,
39    /// The x-position of the second point in the curve.
40    x2: CSSNumber,
41    /// The y-position of the second point in the curve.
42    y2: CSSNumber,
43  },
44  /// A step easing function.
45  Steps {
46    /// The number of intervals in the function.
47    count: CSSInteger,
48    /// The step position.
49    #[cfg_attr(feature = "serde", serde(default))]
50    position: StepPosition,
51  },
52}
53
54impl EasingFunction {
55  /// Returns whether the easing function is equivalent to the `ease` keyword.
56  pub fn is_ease(&self) -> bool {
57    *self == EasingFunction::Ease
58      || *self
59        == EasingFunction::CubicBezier {
60          x1: 0.25,
61          y1: 0.1,
62          x2: 0.25,
63          y2: 1.0,
64        }
65  }
66}
67
68impl<'i> Parse<'i> for EasingFunction {
69  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
70    let location = input.current_source_location();
71    if let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
72      let keyword = match_ignore_ascii_case! { &ident,
73        "linear" => EasingFunction::Linear,
74        "ease" => EasingFunction::Ease,
75        "ease-in" => EasingFunction::EaseIn,
76        "ease-out" => EasingFunction::EaseOut,
77        "ease-in-out" => EasingFunction::EaseInOut,
78        "step-start" => EasingFunction::Steps { count: 1, position: StepPosition::Start },
79        "step-end" => EasingFunction::Steps { count: 1, position: StepPosition::End },
80        _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())))
81      };
82      return Ok(keyword);
83    }
84
85    let function = input.expect_function()?.clone();
86    input.parse_nested_block(|input| {
87      match_ignore_ascii_case! { &function,
88        "cubic-bezier" => {
89          let x1 = CSSNumber::parse(input)?;
90          input.expect_comma()?;
91          let y1 = CSSNumber::parse(input)?;
92          input.expect_comma()?;
93          let x2 = CSSNumber::parse(input)?;
94          input.expect_comma()?;
95          let y2 = CSSNumber::parse(input)?;
96          Ok(EasingFunction::CubicBezier { x1, y1, x2, y2 })
97        },
98        "steps" => {
99          let count = CSSInteger::parse(input)?;
100          let position = input.try_parse(|input| {
101            input.expect_comma()?;
102            StepPosition::parse(input)
103          }).unwrap_or_default();
104          Ok(EasingFunction::Steps { count, position })
105        },
106        _ => return Err(location.new_unexpected_token_error(Token::Ident(function.clone())))
107      }
108    })
109  }
110}
111
112impl ToCss for EasingFunction {
113  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
114  where
115    W: std::fmt::Write,
116  {
117    match self {
118      EasingFunction::Linear => dest.write_str("linear"),
119      EasingFunction::Ease => dest.write_str("ease"),
120      EasingFunction::EaseIn => dest.write_str("ease-in"),
121      EasingFunction::EaseOut => dest.write_str("ease-out"),
122      EasingFunction::EaseInOut => dest.write_str("ease-in-out"),
123      _ if self.is_ease() => dest.write_str("ease"),
124      x if *x
125        == EasingFunction::CubicBezier {
126          x1: 0.42,
127          y1: 0.0,
128          x2: 1.0,
129          y2: 1.0,
130        } =>
131      {
132        dest.write_str("ease-in")
133      }
134      x if *x
135        == EasingFunction::CubicBezier {
136          x1: 0.0,
137          y1: 0.0,
138          x2: 0.58,
139          y2: 1.0,
140        } =>
141      {
142        dest.write_str("ease-out")
143      }
144      x if *x
145        == EasingFunction::CubicBezier {
146          x1: 0.42,
147          y1: 0.0,
148          x2: 0.58,
149          y2: 1.0,
150        } =>
151      {
152        dest.write_str("ease-in-out")
153      }
154      EasingFunction::CubicBezier { x1, y1, x2, y2 } => {
155        dest.write_str("cubic-bezier(")?;
156        x1.to_css(dest)?;
157        dest.delim(',', false)?;
158        y1.to_css(dest)?;
159        dest.delim(',', false)?;
160        x2.to_css(dest)?;
161        dest.delim(',', false)?;
162        y2.to_css(dest)?;
163        dest.write_char(')')
164      }
165      EasingFunction::Steps {
166        count: 1,
167        position: StepPosition::Start,
168      } => dest.write_str("step-start"),
169      EasingFunction::Steps {
170        count: 1,
171        position: StepPosition::End,
172      } => dest.write_str("step-end"),
173      EasingFunction::Steps { count, position } => {
174        dest.write_str("steps(")?;
175        write!(dest, "{}", count)?;
176        dest.delim(',', false)?;
177        position.to_css(dest)?;
178        dest.write_char(')')
179      }
180    }
181  }
182}
183
184impl EasingFunction {
185  /// Returns whether the given string is a valid easing function name.
186  pub fn is_ident(s: &str) -> bool {
187    match s {
188      "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "step-start" | "step-end" => true,
189      _ => false,
190    }
191  }
192}
193
194/// A [step position](https://www.w3.org/TR/css-easing-1/#step-position), used within the `steps()` function.
195#[derive(Debug, Clone, PartialEq, ToCss)]
196#[cfg_attr(feature = "visitor", derive(Visit))]
197#[cfg_attr(
198  feature = "serde",
199  derive(serde::Serialize, serde::Deserialize),
200  serde(tag = "type", content = "value", rename_all = "kebab-case")
201)]
202#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
203pub enum StepPosition {
204  /// The first rise occurs at input progress value of 0.
205  Start,
206  /// The last rise occurs at input progress value of 1.
207  End,
208  /// All rises occur within the range (0, 1).
209  JumpNone,
210  /// The first rise occurs at input progress value of 0 and the last rise occurs at input progress value of 1.
211  JumpBoth,
212}
213
214impl Default for StepPosition {
215  fn default() -> Self {
216    StepPosition::End
217  }
218}
219
220impl<'i> Parse<'i> for StepPosition {
221  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
222    let location = input.current_source_location();
223    let ident = input.expect_ident()?;
224    let keyword = match_ignore_ascii_case! { &ident,
225      "start" => StepPosition::Start,
226      "end" => StepPosition::End,
227      "jump-start" => StepPosition::Start,
228      "jump-end" => StepPosition::End,
229      "jump-none" => StepPosition::JumpNone,
230      "jump-both" => StepPosition::JumpBoth,
231      _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())))
232    };
233    Ok(keyword)
234  }
235}