use super::{Point, Vector2};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Transform {
coefficients: [f64; 6],
}
impl Default for Transform {
fn default() -> Self {
Self::IDENTITY
}
}
impl Transform {
pub const IDENTITY: Self = Self::from_coefficients([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
pub const fn from_coefficients(coefficients: [f64; 6]) -> Self {
Self { coefficients }
}
pub const fn coefficients(self) -> [f64; 6] {
self.coefficients
}
pub const fn translate(horizontal: f64, vertical: f64) -> Self {
Self::from_coefficients([1.0, 0.0, 0.0, 1.0, horizontal, vertical])
}
pub fn rotate(radians: f64) -> Self {
let direction = Vector2::from_angle(radians);
Self::from_coefficients([
direction.x,
direction.y,
-direction.y,
direction.x,
0.0,
0.0,
])
}
pub const fn scale(factor: f64) -> Self {
Self::scale_non_uniform(factor, factor)
}
pub const fn scale_non_uniform(horizontal: f64, vertical: f64) -> Self {
Self::from_coefficients([horizontal, 0.0, 0.0, vertical, 0.0, 0.0])
}
pub fn skew(horizontal: f64, vertical: f64) -> Self {
Self::from_coefficients([
1.0,
libm::tan(vertical),
libm::tan(horizontal),
1.0,
0.0,
0.0,
])
}
pub fn then(self, next: Self) -> Self {
let first = self.coefficients;
let second = next.coefficients;
let translation = next.apply(Point::new(first[4], first[5]));
Self::from_coefficients([
second[0] * first[0] + second[2] * first[1],
second[1] * first[0] + second[3] * first[1],
second[0] * first[2] + second[2] * first[3],
second[1] * first[2] + second[3] * first[3],
translation.x,
translation.y,
])
}
pub fn apply(self, point: Point) -> Point {
let matrix = self.coefficients;
Point::new(
matrix[0] * point.x + matrix[2] * point.y + matrix[4],
matrix[1] * point.x + matrix[3] * point.y + matrix[5],
)
}
pub fn apply_vector(self, vector: Vector2) -> Vector2 {
let matrix = self.coefficients;
Vector2::new(
matrix[0] * vector.x + matrix[2] * vector.y,
matrix[1] * vector.x + matrix[3] * vector.y,
)
}
pub fn is_finite(self) -> bool {
self.coefficients.iter().all(|value| value.is_finite())
}
pub fn inverse(self) -> Option<Self> {
if !self.is_finite() {
return None;
}
let matrix = self.coefficients;
let horizontal_scale = matrix[0].abs().max(matrix[2].abs());
let vertical_scale = matrix[1].abs().max(matrix[3].abs());
if horizontal_scale == 0.0 || vertical_scale == 0.0 {
return None;
}
let normalized = [
matrix[0] / horizontal_scale,
matrix[1] / vertical_scale,
matrix[2] / horizontal_scale,
matrix[3] / vertical_scale,
];
let determinant = normalized[0] * normalized[3] - normalized[1] * normalized[2];
if determinant == 0.0 {
return None;
}
let divide = |coefficient: f64, scale: f64| {
let quotient = coefficient / determinant;
if quotient.is_finite() {
quotient / scale
} else {
(coefficient / scale) / determinant
}
};
let linear = [
divide(normalized[3], horizontal_scale),
divide(-normalized[1], horizontal_scale),
divide(-normalized[2], vertical_scale),
divide(normalized[0], vertical_scale),
];
let inverse = Self::from_coefficients([
linear[0],
linear[1],
linear[2],
linear[3],
-linear[0] * matrix[4] - linear[2] * matrix[5],
-linear[1] * matrix[4] - linear[3] * matrix[5],
]);
inverse.is_finite().then_some(inverse)
}
pub fn decompose(self) -> Option<TransformComponents> {
if !self.is_finite() {
return None;
}
let matrix = self.coefficients;
let horizontal_scale = libm::hypot(matrix[0], matrix[1]);
if horizontal_scale == 0.0 || !horizontal_scale.is_finite() {
return None;
}
let cosine = matrix[0] / horizontal_scale;
let sine = matrix[1] / horizontal_scale;
let projection = cosine * matrix[2] + sine * matrix[3];
let vertical_scale = cosine * matrix[3] - sine * matrix[2];
if vertical_scale == 0.0 || !vertical_scale.is_finite() {
return None;
}
let shear = projection / vertical_scale;
if !shear.is_finite() {
return None;
}
Some(TransformComponents {
translation: Vector2::new(matrix[4], matrix[5]),
rotation: libm::atan2(matrix[1], matrix[0]),
scale: Vector2::new(horizontal_scale, vertical_scale),
shear,
})
}
pub fn interpolate(&self, target: &Self, progress: f64) -> Self {
if progress == 0.0 {
return *self;
}
if progress == 1.0 {
return *target;
}
let (Some(first), Some(second)) = (self.decompose(), target.decompose()) else {
return Self::from_coefficients(std::array::from_fn(|index| {
self.coefficients[index] * (1.0 - progress) + target.coefficients[index] * progress
}));
};
let mut rotation = (second.rotation - first.rotation).rem_euclid(std::f64::consts::TAU);
if rotation > std::f64::consts::PI {
rotation -= std::f64::consts::TAU;
}
TransformComponents {
translation: first.translation.interpolate(&second.translation, progress),
rotation: first.rotation + rotation * progress,
scale: first.scale.interpolate(&second.scale, progress),
shear: first.shear * (1.0 - progress) + second.shear * progress,
}
.compose()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TransformComponents {
pub translation: Vector2,
pub rotation: f64,
pub scale: Vector2,
pub shear: f64,
}
impl TransformComponents {
pub fn compose(self) -> Transform {
let direction = Vector2::from_angle(self.rotation);
Transform::from_coefficients([
direction.x * self.scale.x,
direction.y * self.scale.x,
(direction.x * self.shear - direction.y) * self.scale.y,
(direction.y * self.shear + direction.x) * self.scale.y,
self.translation.x,
self.translation.y,
])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AngleLiteral;
fn close(actual: f64, expected: f64) {
let tolerance = 1e-12 * actual.abs().max(expected.abs()).max(1.0);
assert!(
(actual - expected).abs() <= tolerance,
"{actual} != {expected}"
);
}
fn point_close(actual: Point, expected: Point) {
close(actual.x, expected.x);
close(actual.y, expected.y);
}
#[test]
fn reading_order_matches_sequential_application_and_preserves_vector_semantics() {
let first = Transform::translate(10.0, -4.0);
let second = Transform::rotate(90.degrees());
let point = Point::new(2.0, 3.0);
point_close(
first.then(second).apply(point),
second.apply(first.apply(point)),
);
point_close(first.then(second).apply(point), Point::new(1.0, 12.0));
point_close(second.then(first).apply(point), Point::new(7.0, -2.0));
assert_eq!(
first.apply_vector(Vector2::new(2.0, 3.0)),
Vector2::new(2.0, 3.0)
);
assert_eq!(Transform::default(), Transform::IDENTITY);
let offset = Transform::translate(16_777_217.125, 0.0);
assert_eq!(offset.apply(Point::ORIGIN).x, 16_777_217.125);
}
#[test]
fn inverse_handles_reflection_shear_and_extreme_axis_scales() {
let transforms = [
Transform::IDENTITY,
Transform::scale_non_uniform(-2.0, 3.0)
.then(Transform::skew(15.degrees(), -10.degrees()))
.then(Transform::rotate(37.degrees()))
.then(Transform::translate(31.0, -17.0)),
Transform::scale_non_uniform(1e-200, 1e200),
Transform::scale_non_uniform(1e200, 1e200),
Transform::scale_non_uniform(1e-200, 1e-200),
];
for transform in transforms {
let inverse = transform.inverse().unwrap();
for point in [
Point::ORIGIN,
Point::new(-2.5, 3.75),
Point::new(17.0, -9.0),
] {
point_close(inverse.apply(transform.apply(point)), point);
point_close(transform.apply(inverse.apply(point)), point);
}
}
assert!(Transform::scale_non_uniform(0.0, 1.0).inverse().is_none());
assert!(Transform::scale(f64::from_bits(1)).inverse().is_none());
assert!(Transform::translate(f64::NAN, 1.0).inverse().is_none());
assert!(Transform::scale(f64::INFINITY).inverse().is_none());
}
#[test]
fn decomposition_recomposes_and_carries_reflection_in_vertical_scale() {
for scale in [
Vector2::new(2.0, 3.0),
Vector2::new(-2.0, 3.0),
Vector2::new(-2.0, -3.0),
] {
for shear in [-2.5, 0.0, 1.25, 1e20] {
let transform = TransformComponents {
translation: Vector2::new(21.5, -12.25),
rotation: 0.0,
scale,
shear,
}
.compose();
let parts = transform.decompose().unwrap();
assert!(parts.scale.x > 0.0);
assert_eq!(
parts.scale.y.is_sign_negative(),
(scale.x * scale.y).is_sign_negative()
);
for (actual, expected) in parts
.compose()
.coefficients()
.into_iter()
.zip(transform.coefficients())
{
close(actual, expected);
}
}
}
let rotated = Transform::scale_non_uniform(-2.0, 3.0)
.then(Transform::skew(0.2, -0.15))
.then(Transform::rotate(-127.degrees()));
let recomposed = rotated.decompose().unwrap().compose();
point_close(
recomposed.apply(Point::new(13.0, -8.0)),
rotated.apply(Point::new(13.0, -8.0)),
);
assert!(Transform::scale(0.0).decompose().is_none());
assert!(Transform::rotate(f64::NAN).decompose().is_none());
}
#[test]
fn transform_interpolation_uses_short_rotation_arcs_and_preserves_endpoints() {
let first = Transform::rotate(170.degrees());
let second = Transform::rotate(-170.degrees());
let midpoint = first.interpolate(&second, 0.5);
point_close(midpoint.apply(Point::new(1.0, 0.0)), Point::new(-1.0, 0.0));
assert_eq!(first.interpolate(&second, 0.0), first);
assert_eq!(first.interpolate(&second, 1.0), second);
let half_turn = Transform::IDENTITY.interpolate(&Transform::rotate(180.degrees()), 0.5);
point_close(half_turn.apply(Point::new(1.0, 0.0)), Point::new(0.0, 1.0));
let singular = Transform::scale(0.0).interpolate(&Transform::scale(2.0), 0.5);
point_close(singular.apply(Point::new(2.0, 3.0)), Point::new(2.0, 3.0));
let extended = Transform::IDENTITY.interpolate(&Transform::translate(10.0, -20.0), 1.5);
assert_eq!(extended.apply(Point::ORIGIN), Point::new(15.0, -30.0));
}
}