Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8281,13 +8281,13 @@ mod tests {
minify_test(".foo { rotate: acos(cos(45deg))", ".foo{rotate:45deg}");
minify_test(".foo { rotate: acos(-1)", ".foo{rotate:180deg}");
minify_test(".foo { rotate: acos(0)", ".foo{rotate:90deg}");
minify_test(".foo { rotate: acos(1)", ".foo{rotate:none}");
minify_test(".foo { rotate: acos(1)", ".foo{rotate:0deg}");
minify_test(".foo { rotate: acos(45deg)", ".foo{rotate:acos(45deg)}"); // invalid
minify_test(".foo { rotate: acos(-20)", ".foo{rotate:acos(-20)}"); // evaluates to NaN

minify_test(".foo { rotate: atan(tan(45deg))", ".foo{rotate:45deg}");
minify_test(".foo { rotate: atan(1)", ".foo{rotate:45deg}");
minify_test(".foo { rotate: atan(0)", ".foo{rotate:none}");
minify_test(".foo { rotate: atan(0)", ".foo{rotate:0deg}");
minify_test(".foo { rotate: atan(45deg)", ".foo{rotate:atan(45deg)}"); // invalid

minify_test(".foo { rotate: atan2(1px, -1px)", ".foo{rotate:135deg}");
Expand All @@ -8301,6 +8301,9 @@ mod tests {
minify_test(".foo { rotate: atan2(-1, 1)", ".foo{rotate:-45deg}");
// incompatible units
minify_test(".foo { rotate: atan2(1px, -1vw)", ".foo{rotate:atan2(1px, -1vw)}");

minify_test(".foo { transform: rotate(acos(1)) }", ".foo{transform:rotate(0)}");
minify_test(".foo { transform: rotate(atan(0)) }", ".foo{transform:rotate(0)}");
}

#[test]
Expand Down Expand Up @@ -12832,16 +12835,31 @@ mod tests {
minify_test(".foo { translate: 1px 2px 0px }", ".foo{translate:1px 2px}");
minify_test(".foo { translate: 1px 0px 2px }", ".foo{translate:1px 0 2px}");
minify_test(".foo { translate: none }", ".foo{translate:none}");
minify_test(".foo { rotate: none }", ".foo{rotate:none}");
minify_test(".foo { rotate: 0deg }", ".foo{rotate:0deg}");
minify_test(".foo { rotate: -0deg }", ".foo{rotate:0deg}");
minify_test(".foo { rotate: 10deg }", ".foo{rotate:10deg}");
minify_test(".foo { rotate: z 10deg }", ".foo{rotate:10deg}");
minify_test(".foo { rotate: 0 0 1 10deg }", ".foo{rotate:10deg}");
minify_test(".foo { rotate: x 10deg }", ".foo{rotate:x 10deg}");
minify_test(".foo { rotate: 1 0 0 10deg }", ".foo{rotate:x 10deg}");
minify_test(".foo { rotate: y 10deg }", ".foo{rotate:y 10deg}");
minify_test(".foo { rotate: 2 0 0 10deg }", ".foo{rotate:x 10deg}");
minify_test(".foo { rotate: 0 2 0 10deg }", ".foo{rotate:y 10deg}");
minify_test(".foo { rotate: 0 0 2 10deg }", ".foo{rotate:10deg}");
minify_test(".foo { rotate: 0 0 5.3 10deg }", ".foo{rotate:10deg}");
minify_test(".foo { rotate: 0 0 1 0deg }", ".foo{rotate:0deg}");
minify_test(".foo { rotate: 10deg 0 0 -1 }", ".foo{rotate:-10deg}");
minify_test(".foo { rotate: 10deg 0 0 -233 }", ".foo{rotate:-10deg}");
minify_test(".foo { rotate: -1 0 0 0deg }", ".foo{rotate:x 0deg}");
minify_test(".foo { rotate: 0deg 0 0 1 }", ".foo{rotate:0deg}");
minify_test(".foo { rotate: 0deg 0 0 -1 }", ".foo{rotate:0deg}");
minify_test(".foo { rotate: 0 1 0 10deg }", ".foo{rotate:y 10deg}");
minify_test(".foo { rotate: x 0rad }", ".foo{rotate:x 0deg}");
// TODO: In minify mode, convert units to the shortest form.
// minify_test(".foo { rotate: y 0turn }", ".foo{rotate:y 0deg}");
minify_test(".foo { rotate: z 0deg }", ".foo{rotate:0deg}");
minify_test(".foo { rotate: 10deg y }", ".foo{rotate:y 10deg}");
minify_test(".foo { rotate: 1 1 1 10deg }", ".foo{rotate:1 1 1 10deg}");
minify_test(".foo { rotate: 0 0 1 0deg }", ".foo{rotate:none}");
minify_test(".foo { rotate: none }", ".foo{rotate:none}");
minify_test(".foo { scale: 1 }", ".foo{scale:1}");
minify_test(".foo { scale: 1 1 }", ".foo{scale:1}");
minify_test(".foo { scale: 1 1 1 }", ".foo{scale:1}");
Expand Down
96 changes: 60 additions & 36 deletions src/properties/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1518,29 +1518,39 @@ impl Translate {
/// A value for the [rotate](https://drafts.csswg.org/css-transforms-2/#propdef-rotate) property.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "lowercase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct Rotate {
/// Rotation around the x axis.
pub x: f32,
/// Rotation around the y axis.
pub y: f32,
/// Rotation around the z axis.
pub z: f32,
/// The angle of rotation.
pub angle: Angle,
pub enum Rotate {
/// The `none` keyword.
None,

/// Rotation on the x, y, and z axes.
#[cfg_attr(feature = "serde", serde(untagged))]
XYZ {
/// Rotation around the x axis.
x: f32,
/// Rotation around the y axis.
y: f32,
/// Rotation around the z axis.
z: f32,
/// The angle of rotation.
angle: Angle,
},
}

impl<'i> Parse<'i> for Rotate {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
// CSS Transforms 2 §5.1:
// "It must serialize as the keyword none if and only if none was originally specified."
// Keep `none` explicit so identity rotations (e.g. `0deg`) do not round-trip to `none`.
// https://drafts.csswg.org/css-transforms-2/#individual-transforms
if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
return Ok(Rotate {
x: 0.0,
y: 0.0,
z: 1.0,
angle: Angle::Deg(0.0),
});
return Ok(Rotate::None);
}

let angle = input.try_parse(Angle::parse);
Expand All @@ -1564,7 +1574,7 @@ impl<'i> Parse<'i> for Rotate {
)
.unwrap_or((0.0, 0.0, 1.0));
let angle = angle.or_else(|_| Angle::parse(input))?;
Ok(Rotate { x, y, z, angle })
Ok(Rotate::XYZ { x, y, z, angle })
}
}

Expand All @@ -1573,32 +1583,46 @@ impl ToCss for Rotate {
where
W: std::fmt::Write,
{
if self.x == 0.0 && self.y == 0.0 && self.z == 1.0 && self.angle.is_zero() {
dest.write_str("none")?;
return Ok(());
}

if self.x == 1.0 && self.y == 0.0 && self.z == 0.0 {
dest.write_str("x ")?;
} else if self.x == 0.0 && self.y == 1.0 && self.z == 0.0 {
dest.write_str("y ")?;
} else if !(self.x == 0.0 && self.y == 0.0 && self.z == 1.0) {
self.x.to_css(dest)?;
dest.write_char(' ')?;
self.y.to_css(dest)?;
dest.write_char(' ')?;
self.z.to_css(dest)?;
dest.write_char(' ')?;
match self {
Rotate::None => dest.write_str("none"),
Rotate::XYZ { x, y, z, angle } => {
// CSS Transforms 2 §5.1:
// "If the axis is parallel with the x or y axes, it must serialize as the appropriate keyword."
// "If a rotation about the z axis ... must serialize as just an <angle>."
// Normalize parallel vectors (including non-unit vectors); flip the angle for negative axis directions.
// https://drafts.csswg.org/css-transforms-2/#individual-transforms
if *y == 0.0 && *z == 0.0 && *x != 0.0 {
let angle = if *x < 0.0 { angle.clone() * -1.0 } else { angle.clone() };
dest.write_str("x ")?;
angle.to_css(dest)
} else if *x == 0.0 && *z == 0.0 && *y != 0.0 {
let angle = if *y < 0.0 { angle.clone() * -1.0 } else { angle.clone() };
dest.write_str("y ")?;
angle.to_css(dest)
} else if *x == 0.0 && *y == 0.0 && *z != 0.0 {
let angle = if *z < 0.0 { angle.clone() * -1.0 } else { angle.clone() };
angle.to_css(dest)
} else {
x.to_css(dest)?;
dest.write_char(' ')?;
y.to_css(dest)?;
dest.write_char(' ')?;
z.to_css(dest)?;
dest.write_char(' ')?;
angle.to_css(dest)
}
}
}

self.angle.to_css(dest)
}
}

impl Rotate {
/// Converts the rotation to a transform function.
pub fn to_transform(&self) -> Transform {
Transform::Rotate3d(self.x, self.y, self.z, self.angle.clone())
match self {
Rotate::None => Transform::Rotate3d(0.0, 0.0, 1.0, Angle::Deg(0.0)),
Rotate::XYZ { x, y, z, angle } => Transform::Rotate3d(*x, *y, *z, angle.clone()),
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/values/angle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ impl ToCss for Angle {
}
Angle::Turn(val) => (*val, "turn"),
};
// Canonicalize negative zero so serialization is stable (`0deg` instead of `-0deg`).
let value = if value == 0.0 { 0.0 } else { value };

serialize_dimension(value, unit, dest)
}
Expand Down