use crate::layout::style::{ToCss, unexpected_token};
use std::{
fmt,
ops::{Mul, MulAssign},
};
use cssparser::{Parser, Token, match_ignore_ascii_case};
use taffy::{Point, Size};
use tiny_skia::Transform as TinyTransform;
use crate::{
layout::style::{
Angle, Animatable, Color, CssSyntaxKind, CssToken, FromCss, Length, ListInterpolationStrategy,
MakeComputed, ParseResult, PercentageNumber, lerp,
},
rendering::Sizing,
};
const DEFAULT_SCALE: f32 = 1.0;
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum Transform {
Translate(Length, Length),
Scale(f32, f32),
Rotate(Angle),
Skew(Angle, Angle),
Matrix(Affine),
}
impl MakeComputed for Transform {
fn make_computed(&mut self, sizing: &Sizing) {
if let Transform::Translate(x, y) = self {
x.make_computed(sizing);
y.make_computed(sizing);
}
}
}
impl Animatable for Transform {
fn list_interpolation_strategy() -> ListInterpolationStrategy {
ListInterpolationStrategy::PadToLongestWithNeutral
}
fn neutral_value_like(other: &Self) -> Option<Self> {
Some(match *other {
Transform::Translate(_, _) => Transform::Translate(Length::zero(), Length::zero()),
Transform::Scale(_, _) => Transform::Scale(1.0, 1.0),
Transform::Rotate(_) => Transform::Rotate(Angle::zero()),
Transform::Skew(_, _) => Transform::Skew(Angle::zero(), Angle::zero()),
Transform::Matrix(_) => Transform::Matrix(Affine::IDENTITY),
})
}
fn interpolate(
&mut self,
from: &Self,
to: &Self,
progress: f32,
sizing: &Sizing,
current_color: Color,
) {
*self = match (*from, *to) {
(Transform::Translate(from_x, from_y), Transform::Translate(to_x, to_y)) => {
let mut x = from_x;
x.interpolate(&from_x, &to_x, progress, sizing, current_color);
let mut y = from_y;
y.interpolate(&from_y, &to_y, progress, sizing, current_color);
Transform::Translate(x, y)
}
(Transform::Scale(from_x, from_y), Transform::Scale(to_x, to_y)) => {
Transform::Scale(lerp(from_x, to_x, progress), lerp(from_y, to_y, progress))
}
(Transform::Rotate(from_angle), Transform::Rotate(to_angle)) => {
let mut angle = from_angle;
angle.interpolate(&from_angle, &to_angle, progress, sizing, current_color);
Transform::Rotate(angle)
}
(Transform::Skew(from_x, from_y), Transform::Skew(to_x, to_y)) => {
let mut x = from_x;
x.interpolate(&from_x, &to_x, progress, sizing, current_color);
let mut y = from_y;
y.interpolate(&from_y, &to_y, progress, sizing, current_color);
Transform::Skew(x, y)
}
(Transform::Matrix(from_affine), Transform::Matrix(to_affine)) => Transform::Matrix(Affine {
a: lerp(from_affine.a, to_affine.a, progress),
b: lerp(from_affine.b, to_affine.b, progress),
c: lerp(from_affine.c, to_affine.c, progress),
d: lerp(from_affine.d, to_affine.d, progress),
x: lerp(from_affine.x, to_affine.x, progress),
y: lerp(from_affine.y, to_affine.y, progress),
}),
_ => {
if progress >= 0.5 {
*to
} else {
*from
}
}
};
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
#[non_exhaustive]
pub struct Affine {
pub a: f32,
pub b: f32,
pub c: f32,
pub d: f32,
pub x: f32,
pub y: f32,
}
impl From<Affine> for TinyTransform {
fn from(transform: Affine) -> Self {
TinyTransform::from_row(
transform.a,
transform.b,
transform.c,
transform.d,
transform.x,
transform.y,
)
}
}
impl Mul<Affine> for Affine {
type Output = Affine;
fn mul(self, rhs: Affine) -> Self::Output {
if self.is_identity() {
return rhs;
}
if rhs.is_identity() {
return self;
}
Affine {
a: self.a * rhs.a + self.c * rhs.b,
b: self.b * rhs.a + self.d * rhs.b,
c: self.a * rhs.c + self.c * rhs.d,
d: self.b * rhs.c + self.d * rhs.d,
x: self.a * rhs.x + self.c * rhs.y + self.x,
y: self.b * rhs.x + self.d * rhs.y + self.y,
}
}
}
impl MulAssign<Affine> for Affine {
fn mul_assign(&mut self, rhs: Affine) {
*self = *self * rhs;
}
}
impl Affine {
pub fn to_cols_array(&self) -> [f32; 6] {
[self.a, self.b, self.c, self.d, self.x, self.y]
}
pub const IDENTITY: Self = Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
x: 0.0,
y: 0.0,
};
pub fn is_identity(self) -> bool {
(self.a - 1.0).abs() < 1e-6
&& self.b.abs() < 1e-6
&& self.c.abs() < 1e-6
&& (self.d - 1.0).abs() < 1e-6
&& self.x.abs() < 1e-6
&& self.y.abs() < 1e-6
}
pub fn decompose_translation(self) -> Point<f32> {
Point {
x: self.x,
y: self.y,
}
}
pub(crate) fn uniform_scale(self) -> f32 {
let sx = (self.a * self.a + self.b * self.b).sqrt();
let sy = (self.c * self.c + self.d * self.d).sqrt();
(sx * sy).sqrt()
}
pub(crate) fn only_translation(self) -> bool {
(self.a - 1.0).abs() < 1e-8
&& self.b.abs() < 1e-8
&& self.c.abs() < 1e-8
&& (self.d - 1.0).abs() < 1e-8
}
pub fn rotation(angle: Angle) -> Self {
let (sin, cos) = angle.to_radians().sin_cos();
Self {
a: cos,
b: sin,
c: -sin,
d: cos,
x: 0.0,
y: 0.0,
}
}
pub const fn translation(x: f32, y: f32) -> Self {
Self {
x,
y,
..Self::IDENTITY
}
}
pub const fn scale(x: f32, y: f32) -> Self {
Self {
a: x,
b: 0.0,
c: 0.0,
d: y,
x: 0.0,
y: 0.0,
}
}
#[inline(always)]
pub fn transform_point(self, point: Point<f32>) -> Point<f32> {
if self.only_translation() {
return Point {
x: point.x + self.x,
y: point.y + self.y,
};
}
Point {
x: self.a * point.x + self.c * point.y + self.x,
y: self.b * point.x + self.d * point.y + self.y,
}
}
pub fn skew(x: Angle, y: Angle) -> Self {
let tanx = x.to_radians().tan();
let tany = y.to_radians().tan();
Self {
a: 1.0,
b: tany,
c: tanx,
d: 1.0,
x: 0.0,
y: 0.0,
}
}
#[inline(always)]
pub fn determinant(self) -> f32 {
self.a * self.d - self.b * self.c
}
#[inline(always)]
pub fn is_invertible(self) -> bool {
self.determinant().abs() > f32::EPSILON
}
pub fn invert(self) -> Option<Self> {
let det = self.determinant();
if det.abs() < f32::EPSILON {
return None;
}
let inv_det = 1.0 / det;
Some(Self {
a: self.d * inv_det,
b: self.b * -inv_det,
c: self.c * -inv_det,
d: self.a * inv_det,
x: (self.d * self.x - self.c * self.y) * -inv_det,
y: (self.b * self.x - self.a * self.y) * inv_det,
})
}
pub(crate) fn from_transforms<'a, I: Iterator<Item = &'a Transform>>(
transforms: I,
sizing: &Sizing,
border_box: Size<f32>,
) -> Affine {
let mut instance = Affine::IDENTITY;
for transform in transforms {
instance *= match *transform {
Transform::Translate(x_length, y_length) => Affine::translation(
x_length.to_px(sizing, border_box.width),
y_length.to_px(sizing, border_box.height),
),
Transform::Scale(x_scale, y_scale) => Affine::scale(x_scale, y_scale),
Transform::Rotate(angle) => Affine::rotation(angle),
Transform::Skew(x_angle, y_angle) => Affine::skew(x_angle, y_angle),
Transform::Matrix(affine) => affine,
};
}
instance
}
}
impl<'i> FromCss<'i> for Affine {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
let a = input.expect_number()?;
input.expect_comma()?;
let b = input.expect_number()?;
input.expect_comma()?;
let c = input.expect_number()?;
input.expect_comma()?;
let d = input.expect_number()?;
input.expect_comma()?;
let x = input.expect_number()?;
input.expect_comma()?;
let y = input.expect_number()?;
Ok(Affine { a, b, c, d, x, y })
}
const VALID_TOKENS: &'static [CssToken] = &[CssToken::Syntax(CssSyntaxKind::Number)];
}
#[derive(Debug, Clone, PartialEq)]
pub struct Transforms(pub Box<[Transform]>);
impl<'i> FromCss<'i> for Transforms {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
let mut transforms = Vec::new();
while !input.is_exhausted() {
let transform = Transform::from_css(input)?;
transforms.push(transform);
}
Ok(Transforms(transforms.into_boxed_slice()))
}
const VALID_TOKENS: &'static [CssToken] = Transform::VALID_TOKENS;
}
impl MakeComputed for Transforms {
fn make_computed(&mut self, sizing: &Sizing) {
for transform in self.0.iter_mut() {
transform.make_computed(sizing);
}
}
}
impl Animatable for Transforms {
fn missing_value() -> Option<Self> {
<Box<[Transform]>>::missing_value().map(Self)
}
fn interpolate(
&mut self,
from: &Self,
to: &Self,
progress: f32,
sizing: &Sizing,
current_color: Color,
) {
self
.0
.interpolate(&from.0, &to.0, progress, sizing, current_color);
}
}
impl std::ops::Deref for Transforms {
type Target = Box<[Transform]>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for Transforms {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<Box<[Transform]>> for Transforms {
fn from(box_slice: Box<[Transform]>) -> Self {
Self(box_slice)
}
}
impl From<Vec<Transform>> for Transforms {
fn from(vec: Vec<Transform>) -> Self {
Self(vec.into_boxed_slice())
}
}
impl<const N: usize> From<[Transform; N]> for Transforms {
fn from(arr: [Transform; N]) -> Self {
Self(Box::from(arr))
}
}
impl ToCss for Transform {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
match self {
Self::Translate(x, y) => {
dest.write_str("translate(")?;
x.to_css(dest)?;
dest.write_str(", ")?;
y.to_css(dest)?;
dest.write_char(')')
}
Self::Scale(x, y) => write!(dest, "scale({x}, {y})"),
Self::Rotate(a) => {
dest.write_str("rotate(")?;
a.to_css(dest)?;
dest.write_char(')')
}
Self::Skew(x, y) => {
dest.write_str("skew(")?;
x.to_css(dest)?;
dest.write_str(", ")?;
y.to_css(dest)?;
dest.write_char(')')
}
Self::Matrix(Affine { a, b, c, d, x, y }) => {
write!(dest, "matrix({a}, {b}, {c}, {d}, {x}, {y})")
}
}
}
}
impl ToCss for Affine {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
let Self { a, b, c, d, x, y } = self;
write!(dest, "matrix({a}, {b}, {c}, {d}, {x}, {y})")
}
}
impl ToCss for Transforms {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
let mut first = true;
for transform in self.iter() {
if !first {
dest.write_char(' ')?;
}
transform.to_css(dest)?;
first = false;
}
Ok(())
}
}
impl<'i> FromCss<'i> for Transform {
fn from_css(parser: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
let location = parser.current_source_location();
let token = parser.next()?;
let Token::Function(function) = token else {
return Err(
location
.new_basic_unexpected_token_error(token.clone())
.into(),
);
};
match_ignore_ascii_case! {function,
"translate" => parser.parse_nested_block(|input| {
let x = Length::from_css(input)?;
input.expect_comma()?;
let y = Length::from_css(input)?;
Ok(Transform::Translate(x, y))
}),
"translatex" => parser.parse_nested_block(|input| Ok(Transform::Translate(
Length::from_css(input)?,
Length::zero(),
))),
"translatey" => parser.parse_nested_block(|input| Ok(Transform::Translate(
Length::zero(),
Length::from_css(input)?,
))),
"scale" => parser.parse_nested_block(|input| {
let PercentageNumber(x) = PercentageNumber::from_css(input)?;
if input.try_parse(Parser::expect_comma).is_ok() {
let PercentageNumber(y) = PercentageNumber::from_css(input)?;
Ok(Transform::Scale(x, y))
} else {
Ok(Transform::Scale(x, x))
}
}),
"scalex" => parser.parse_nested_block(|input| Ok(Transform::Scale(
PercentageNumber::from_css(input)?.0,
DEFAULT_SCALE,
))),
"scaley" => parser.parse_nested_block(|input| Ok(Transform::Scale(
DEFAULT_SCALE,
PercentageNumber::from_css(input)?.0,
))),
"skew" => parser.parse_nested_block(|input| {
let x = Angle::from_css(input)?;
input.expect_comma()?;
let y = Angle::from_css(input)?;
Ok(Transform::Skew(x, y))
}),
"skewx" => parser.parse_nested_block(|input| Ok(Transform::Skew(
Angle::from_css(input)?,
Angle::default(),
))),
"skewy" => parser.parse_nested_block(|input| Ok(Transform::Skew(
Angle::default(),
Angle::from_css(input)?,
))),
"rotate" => parser.parse_nested_block(|input| Ok(Transform::Rotate(
Angle::from_css(input)?,
))),
"matrix" => parser.parse_nested_block(|input| Ok(Transform::Matrix(
Affine::from_css(input)?,
))),
_ => Err(unexpected_token!(location, token)),
}
}
const VALID_TOKENS: &'static [CssToken] = &[CssToken::Syntax(CssSyntaxKind::TransformFunction)];
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transform_from_str() {
assert_eq!(
Transform::from_str("translate(10, 20px)"),
Ok(Transform::Translate(Length::Px(10.0), Length::Px(20.0)))
);
}
#[test]
fn test_transform_scale_from_str() {
assert_eq!(
Transform::from_str("scale(10)"),
Ok(Transform::Scale(10.0, 10.0))
);
}
#[test]
fn test_transform_invert() {
let transform = Affine::rotation(Angle::new(45.0));
assert!(transform.invert().is_some_and(|inverse| {
let random_point = Point {
x: 1234.0,
y: -5678.0,
};
let processed_point = inverse.transform_point(transform.transform_point(random_point));
(random_point.x - processed_point.x).abs() < 1.0
&& (random_point.y - processed_point.y).abs() < 1.0
}));
}
}