use crate::gpu::scene::{MonochromeSprite, Quad, Shadow, Underline};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Affine {
pub a: f32,
pub b: f32,
pub c: f32,
pub d: f32,
pub tx: f32,
pub ty: f32,
}
impl Affine {
pub const IDENTITY: Affine = Affine { a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: 0.0, ty: 0.0 };
pub const fn translate(x: f32, y: f32) -> Self {
Self { tx: x, ty: y, ..Self::IDENTITY }
}
pub const fn scale(x: f32, y: f32) -> Self {
Self { a: x, d: y, ..Self::IDENTITY }
}
pub fn rotate(degrees: f32) -> Self {
let (sin, cos) = degrees.to_radians().sin_cos();
Self { a: cos, b: sin, c: -sin, d: cos, tx: 0.0, ty: 0.0 }
}
pub fn skew(x_degrees: f32, y_degrees: f32) -> Self {
Self { c: x_degrees.to_radians().tan(), b: y_degrees.to_radians().tan(), ..Self::IDENTITY }
}
pub fn then(self, next: Affine) -> Affine {
Affine {
a: next.a * self.a + next.c * self.b,
b: next.b * self.a + next.d * self.b,
c: next.a * self.c + next.c * self.d,
d: next.b * self.c + next.d * self.d,
tx: next.a * self.tx + next.c * self.ty + next.tx,
ty: next.b * self.tx + next.d * self.ty + next.ty,
}
}
pub fn about(self, origin: (f32, f32)) -> Affine {
Affine::translate(-origin.0, -origin.1).then(self).then(Affine::translate(origin.0, origin.1))
}
pub fn apply(&self, (x, y): (f32, f32)) -> (f32, f32) {
(self.a * x + self.c * y + self.tx, self.b * x + self.d * y + self.ty)
}
pub fn is_identity(&self) -> bool {
*self == Self::IDENTITY
}
pub fn is_axis_aligned(&self) -> bool {
self.b == 0.0 && self.c == 0.0
}
pub fn scale_factor(&self) -> f32 {
(self.a * self.d - self.b * self.c).abs().sqrt()
}
pub fn bounds_of(&self, [x, y, w, h]: [f32; 4]) -> [f32; 4] {
let corners = [(x, y), (x + w, y), (x, y + h), (x + w, y + h)].map(|corner| self.apply(corner));
let (mut min, mut max) = (corners[0], corners[0]);
for (cx, cy) in corners {
(min.0, min.1, max.0, max.1) = (min.0.min(cx), min.1.min(cy), max.0.max(cx), max.1.max(cy));
}
[min.0, min.1, max.0 - min.0, max.1 - min.1]
}
fn minor_scale(&self) -> f32 {
self.a.abs().min(self.d.abs())
}
pub fn quad(&self, quad: &Quad) -> Quad {
let s = self.minor_scale();
let [top, right, bottom, left] = quad.border_widths;
Quad {
bounds: self.bounds_of(quad.bounds),
clip: self.bounds_of(quad.clip),
corner_radii: quad.corner_radii.map(|radius| radius * s),
border_widths: [top * self.d.abs(), right * self.a.abs(), bottom * self.d.abs(), left * self.a.abs()],
..*quad
}
}
pub fn shadow(&self, shadow: &Shadow) -> Shadow {
let s = self.minor_scale();
Shadow {
bounds: self.bounds_of(shadow.bounds),
clip: self.bounds_of(shadow.clip),
corner_radii: shadow.corner_radii.map(|radius| radius * s),
blur: shadow.blur * s,
..*shadow
}
}
pub fn underline(&self, underline: &Underline) -> Underline {
Underline {
bounds: self.bounds_of(underline.bounds),
clip: self.bounds_of(underline.clip),
thickness: underline.thickness * self.d.abs(),
..*underline
}
}
pub fn sprite(&self, sprite: &MonochromeSprite) -> MonochromeSprite {
MonochromeSprite { bounds: self.bounds_of(sprite.bounds), clip: self.bounds_of(sprite.clip), ..*sprite }
}
}
impl Default for Affine {
fn default() -> Self {
Self::IDENTITY
}
}
#[cfg(test)]
mod tests {
use super::*;
fn near(actual: (f32, f32), expected: (f32, f32)) {
assert!(
(actual.0 - expected.0).abs() < 1e-4 && (actual.1 - expected.1).abs() < 1e-4,
"{actual:?} ≈ {expected:?}"
);
}
#[test]
fn el_orden_es_primero_uno_y_luego_el_otro() {
let t = Affine::scale(2.0, 2.0).then(Affine::translate(10.0, 0.0));
near(t.apply((1.0, 1.0)), (12.0, 2.0));
let t = Affine::translate(10.0, 0.0).then(Affine::scale(2.0, 2.0));
near(t.apply((1.0, 1.0)), (22.0, 2.0));
}
#[test]
fn girar_noventa_grados_lleva_el_eje_x_hacia_abajo() {
near(Affine::rotate(90.0).apply((1.0, 0.0)), (0.0, 1.0));
}
#[test]
fn el_origen_mantiene_su_sitio() {
let about = Affine::scale(2.0, 2.0).about((50.0, 50.0));
near(about.apply((50.0, 50.0)), (50.0, 50.0));
near(about.apply((60.0, 50.0)), (70.0, 50.0));
let rotate = Affine::rotate(180.0).about((10.0, 10.0));
near(rotate.apply((0.0, 0.0)), (20.0, 20.0));
}
#[test]
fn el_rectangulo_envolvente_de_un_giro_de_45_es_mayor() {
let [x, y, w, h] = Affine::rotate(45.0).about((50.0, 50.0)).bounds_of([0.0, 0.0, 100.0, 100.0]);
assert!((w - 141.42).abs() < 0.01 && (h - 141.42).abs() < 0.01);
assert!((x - (50.0 - 70.71)).abs() < 0.01 && (y - (50.0 - 70.71)).abs() < 0.01);
}
#[test]
fn la_identidad_y_el_factor_de_escala() {
assert!(Affine::IDENTITY.is_identity() && Affine::IDENTITY.is_axis_aligned());
assert!(!Affine::rotate(10.0).is_axis_aligned());
assert!((Affine::scale(2.0, 0.5).scale_factor() - 1.0).abs() < 1e-6);
assert!((Affine::rotate(33.0).scale_factor() - 1.0).abs() < 1e-5);
}
#[test]
fn una_figura_escalada_cambia_su_geometria_proporcionalmente() {
use crate::core::{Bounds, Corners, Edges, Point, Rgba, Size, px};
let bounds = Bounds::new(Point::new(px(10.0), px(10.0)), Size::new(px(20.0), px(10.0)));
let quad = Quad::new(bounds, bounds, Rgba::TRANSPARENT)
.corners(Corners::all(px(4.0)))
.border(Edges::all(px(2.0)), Rgba::TRANSPARENT);
let scaled = Affine::scale(2.0, 2.0).about((0.0, 0.0)).quad(&quad);
assert_eq!(scaled.bounds, [20.0, 20.0, 40.0, 20.0]);
assert_eq!(scaled.corner_radii, [8.0; 4]);
assert_eq!(scaled.border_widths, [4.0; 4]);
}
}