//! CSS easing functions. use crate::error::{ParserError, PrinterError}; use crate::printer::Printer; use crate::traits::{Parse, ToCss}; use crate::values::number::{CSSInteger, CSSNumber}; #[cfg(feature = "visitor")] use crate::visitor::Visit; use cssparser::*; use std::fmt::Write; /// A CSS [easing function](https://www.w3.org/TR/css-easing-1/#easing-functions). #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum EasingFunction { /// A linear easing function. Linear, /// Equivalent to `cubic-bezier(0.25, 0.1, 0.25, 1)`. Ease, /// Equivalent to `cubic-bezier(0.42, 0, 1, 1)`. EaseIn, /// Equivalent to `cubic-bezier(0, 0, 0.58, 1)`. EaseOut, /// Equivalent to `cubic-bezier(0.42, 0, 0.58, 1)`. EaseInOut, /// A custom cubic Bézier easing function. CubicBezier { /// The x-position of the first point in the curve. x1: CSSNumber, /// The y-position of the first point in the curve. y1: CSSNumber, /// The x-position of the second point in the curve. x2: CSSNumber, /// The y-position of the second point in the curve. y2: CSSNumber, }, /// A step easing function. Steps { /// The number of intervals in the function. count: CSSInteger, /// The step position. #[cfg_attr(feature = "serde", serde(default))] position: StepPosition, }, } impl EasingFunction { /// Returns whether the easing function is equivalent to the `ease` keyword. pub fn is_ease(&self) -> bool { *self == EasingFunction::Ease || *self == EasingFunction::CubicBezier { x1: 0.25, y1: 0.1, x2: 0.25, y2: 1.0, } } } impl<'i> Parse<'i> for EasingFunction { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { let location = input.current_source_location(); if let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { let keyword = match_ignore_ascii_case! { &ident, "linear" => EasingFunction::Linear, "ease" => EasingFunction::Ease, "ease-in" => EasingFunction::EaseIn, "ease-out" => EasingFunction::EaseOut, "ease-in-out" => EasingFunction::EaseInOut, "step-start" => EasingFunction::Steps { count: 1, position: StepPosition::Start }, "step-end" => EasingFunction::Steps { count: 1, position: StepPosition::End }, _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))) }; return Ok(keyword); } let function = input.expect_function()?.clone(); input.parse_nested_block(|input| { match_ignore_ascii_case! { &function, "cubic-bezier" => { let x1 = CSSNumber::parse(input)?; input.expect_comma()?; let y1 = CSSNumber::parse(input)?; input.expect_comma()?; let x2 = CSSNumber::parse(input)?; input.expect_comma()?; let y2 = CSSNumber::parse(input)?; Ok(EasingFunction::CubicBezier { x1, y1, x2, y2 }) }, "steps" => { let count = CSSInteger::parse(input)?; let position = input.try_parse(|input| { input.expect_comma()?; StepPosition::parse(input) }).unwrap_or_default(); Ok(EasingFunction::Steps { count, position }) }, _ => return Err(location.new_unexpected_token_error(Token::Ident(function.clone()))) } }) } } impl ToCss for EasingFunction { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { EasingFunction::Linear => dest.write_str("linear"), EasingFunction::Ease => dest.write_str("ease"), EasingFunction::EaseIn => dest.write_str("ease-in"), EasingFunction::EaseOut => dest.write_str("ease-out"), EasingFunction::EaseInOut => dest.write_str("ease-in-out"), _ if self.is_ease() => dest.write_str("ease"), x if *x == EasingFunction::CubicBezier { x1: 0.42, y1: 0.0, x2: 1.0, y2: 1.0, } => { dest.write_str("ease-in") } x if *x == EasingFunction::CubicBezier { x1: 0.0, y1: 0.0, x2: 0.58, y2: 1.0, } => { dest.write_str("ease-out") } x if *x == EasingFunction::CubicBezier { x1: 0.42, y1: 0.0, x2: 0.58, y2: 1.0, } => { dest.write_str("ease-in-out") } EasingFunction::CubicBezier { x1, y1, x2, y2 } => { dest.write_str("cubic-bezier(")?; x1.to_css(dest)?; dest.delim(',', false)?; y1.to_css(dest)?; dest.delim(',', false)?; x2.to_css(dest)?; dest.delim(',', false)?; y2.to_css(dest)?; dest.write_char(')') } EasingFunction::Steps { count: 1, position: StepPosition::Start, } => dest.write_str("step-start"), EasingFunction::Steps { count: 1, position: StepPosition::End, } => dest.write_str("step-end"), EasingFunction::Steps { count, position } => { dest.write_str("steps(")?; write!(dest, "{}", count)?; dest.delim(',', false)?; position.to_css(dest)?; dest.write_char(')') } } } } impl EasingFunction { /// Returns whether the given string is a valid easing function name. pub fn is_ident(s: &str) -> bool { match s { "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "step-start" | "step-end" => true, _ => false, } } } /// A [step position](https://www.w3.org/TR/css-easing-1/#step-position), used within the `steps()` function. #[derive(Debug, Clone, PartialEq, ToCss)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", content = "value", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum StepPosition { /// The first rise occurs at input progress value of 0. Start, /// The last rise occurs at input progress value of 1. End, /// All rises occur within the range (0, 1). JumpNone, /// The first rise occurs at input progress value of 0 and the last rise occurs at input progress value of 1. JumpBoth, } impl Default for StepPosition { fn default() -> Self { StepPosition::End } } impl<'i> Parse<'i> for StepPosition { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { let location = input.current_source_location(); let ident = input.expect_ident()?; let keyword = match_ignore_ascii_case! { &ident, "start" => StepPosition::Start, "end" => StepPosition::End, "jump-start" => StepPosition::Start, "jump-end" => StepPosition::End, "jump-none" => StepPosition::JumpNone, "jump-both" => StepPosition::JumpBoth, _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))) }; Ok(keyword) } }