//! CSS Transforms 1's 2D `transform` and `transform-origin` for block-level, //! non-replaced boxes -- the transformation matrix "The Transform Rendering Model" //! computes for one box, as one plain-data function a generated candidate implements: //! //! - [`resolve`-shaped](TransformResolver): given the box's border-box size, its //! `transform` list and its `transform-origin`, returns the affine matrix //! `[a, b, c, d, e, f]` that takes a point in the box's own border-box coordinates to //! its parent's coordinates, relative to where normal flow put the box's border-box //! origin. The origin is already applied: translate by the origin, apply the list left //! to right, translate back, exactly the three steps CSS Transforms 1 names. //! //! A transform never moves other boxes (CSS Transforms 1, "The Transform Rendering //! Model": the transform does not affect the flow). Normal flow places every box exactly //! as it would with `transform: none`, and `crates/layout/src/fragment.rs` applies this //! matrix afterwards, when it reports where a box ends up: `Fragment::walk_absolute` //! composes each box's matrix with every ancestor's and reports the bounding box of the //! transformed border box, which is what `getBoundingClientRect()` returns and what //! `oracle/oracle.mjs` `domDump()` records. That composition is hand-written bookkeeping, //! not part of what a candidate writes -- [`compose`] and [`bounding_box`] below are the //! two pieces of it that belong beside the matrix algebra rather than in `fragment.rs`. //! //! The reference box is always the border box, `transform-box`'s initial value. //! //! Scope. In: `translate()`, `translateX()`, `translateY()` (lengths, and percentages of //! the border box's own width and height), `scale()`, `scaleX()`, `scaleY()`, //! `rotate()`, `skew()`, `skewX()`, `skewY()` and `matrix()`, in lists of any length //! composed left to right; `transform-origin` as lengths, percentages and keywords, in //! one or two values, with `z` staying 0; and transforms on nested boxes, where a //! descendant's bounding box goes through every ancestor's matrix. //! //! Out, and not modelled anywhere in this module: 3D functions and `perspective`; //! `transform-box`; `translate`, `rotate` and `scale` as individual properties (CSS //! Transforms 2); a transformed box becoming the containing block for its positioned //! descendants, and establishing a stacking context -- both real effects, but both //! change other generated functions' inputs, so the corpus has no positioned or //! `z-index` descendant of a transformed box; painting transformed boxes in //! `crates/layout/src/bin/paint.rs`; and scrollable overflow from transforms. //! //! [`resolve_baseline`] is this function's deliberately wrong starting candidate: every //! box gets the identity matrix, whatever its `transform` says -- the engine's actual //! behaviour before this module existed, where `walk_absolute` reported every border box //! where normal flow put it. use dom::computed::{LengthPercentage, TransformFunction}; /// The affine matrix that changes nothing: `transform: none`, and what every box in a /// document that never sets `transform` carries. pub const IDENTITY: [f64; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; /// Plain-data inputs to one box's transformation matrix. #[derive(Debug, Clone, PartialEq)] pub struct TransformInputs { /// The box's own border-box width, the reference box `transform-box`'s initial value /// names. Percentages in `translate()` and in `transform-origin`'s horizontal /// component resolve against it. pub border_box_width: f64, /// The box's own border-box height, the vertical counterpart of /// [`border_box_width`](TransformInputs::border_box_width). pub border_box_height: f64, /// The computed `transform` list, in written order, which CSS Transforms 1 §11 /// composes left to right. Empty for `transform: none`. pub functions: Vec, /// `transform-origin`'s horizontal component: a length in CSS pixels, or a /// percentage of [`border_box_width`](TransformInputs::border_box_width). The /// initial value is `50%`. pub origin_x: LengthPercentage, /// `transform-origin`'s vertical component, against /// [`border_box_height`](TransformInputs::border_box_height). The initial value is /// `50%`. pub origin_y: LengthPercentage, } /// One box's transformation matrix, `[a, b, c, d, e, f]` in CSS Transforms 1 §11's own /// order: the point `(x, y)` maps to `(a*x + c*y + e, b*x + d*y + f)`. `x` and `y` are in /// the box's own border-box coordinates -- `(0, 0)` is the border box's top-left corner /// as normal flow placed it -- and so is the result, so the matrix says how the box moves /// relative to where it would otherwise sit, never where on the page it ends up. #[derive(Debug, Clone, Copy, PartialEq)] pub struct TransformOutputs { pub matrix: [f64; 6], } /// One candidate's implementation: plain data in, plain data out. Every candidate module /// the generation loop compiles exports a function of this type named `resolve`. pub type TransformResolver = fn(&TransformInputs) -> TransformOutputs; /// This function's deliberately wrong starting candidate (this module's own doc /// comment): every box gets the identity matrix, `transform` ignored outright -- what /// `fragment.rs` did before this module existed. pub fn resolve_baseline(_inputs: &TransformInputs) -> TransformOutputs { TransformOutputs { matrix: IDENTITY } } /// `outer` applied after `inner`: the matrix mapping a point through `inner` first and /// then `outer`. `fragment.rs`'s bookkeeping, not part of what a candidate writes -- it /// is what carries an ancestor's matrix down to a descendant, and what a candidate that /// builds its answer one function at a time can compose its own list with. pub fn compose(outer: [f64; 6], inner: [f64; 6]) -> [f64; 6] { let [a1, b1, c1, d1, e1, f1] = outer; let [a2, b2, c2, d2, e2, f2] = inner; [ a1 * a2 + c1 * b2, b1 * a2 + d1 * b2, a1 * c2 + c1 * d2, b1 * c2 + d1 * d2, a1 * e2 + c1 * f2 + e1, b1 * e2 + d1 * f2 + f1, ] } /// The point `(x, y)` mapped through `matrix`. pub fn map_point(matrix: [f64; 6], x: f64, y: f64) -> (f64, f64) { let [a, b, c, d, e, f] = matrix; (a * x + c * y + e, b * x + d * y + f) } /// The axis-aligned bounding box of the rectangle `(x, y, width, height)` mapped through /// `matrix`, as `(x, y, width, height)` again: the four corners through the matrix, then /// the minimum and maximum of each axis. This is what `getBoundingClientRect()` reports /// for a transformed box, and what `fragment.rs` hands `walk_absolute`'s callback. pub fn bounding_box(matrix: [f64; 6], x: f64, y: f64, width: f64, height: f64) -> (f64, f64, f64, f64) { let corners = [ map_point(matrix, x, y), map_point(matrix, x + width, y), map_point(matrix, x, y + height), map_point(matrix, x + width, y + height), ]; let min_x = corners.iter().map(|c| c.0).fold(f64::INFINITY, f64::min); let max_x = corners.iter().map(|c| c.0).fold(f64::NEG_INFINITY, f64::max); let min_y = corners.iter().map(|c| c.1).fold(f64::INFINITY, f64::min); let max_y = corners.iter().map(|c| c.1).fold(f64::NEG_INFINITY, f64::max); (min_x, min_y, max_x - min_x, max_y - min_y) } #[cfg(test)] mod tests { use super::*; fn inputs(functions: Vec) -> TransformInputs { TransformInputs { border_box_width: 200.0, border_box_height: 100.0, functions, origin_x: LengthPercentage::Percentage(0.5), origin_y: LengthPercentage::Percentage(0.5), } } #[test] fn baseline_ignores_every_function_and_returns_the_identity() { let out = resolve_baseline(&inputs(vec![ TransformFunction::Translate { x: LengthPercentage::Length(40.0), y: LengthPercentage::Length(-10.0), }, TransformFunction::Rotate { radians: std::f64::consts::FRAC_PI_2 }, ])); assert_eq!(out.matrix, IDENTITY, "not the translation, and not the rotation"); } #[test] fn baseline_ignores_a_non_default_origin_too() { let mut i = inputs(vec![TransformFunction::Scale { x: 2.0, y: 3.0 }]); i.origin_x = LengthPercentage::Length(0.0); i.origin_y = LengthPercentage::Length(0.0); assert_eq!(resolve_baseline(&i).matrix, IDENTITY); } #[test] fn composing_with_the_identity_changes_nothing() { let m = [2.0, 0.5, -0.25, 3.0, 12.0, -7.0]; assert_eq!(compose(IDENTITY, m), m); assert_eq!(compose(m, IDENTITY), m); } /// `compose` applies `outer` after `inner`, so a translation composed outside a /// scale moves the already-scaled point, not the point before scaling. #[test] fn compose_applies_the_outer_matrix_second() { let scale = [2.0, 0.0, 0.0, 2.0, 0.0, 0.0]; let translate = [1.0, 0.0, 0.0, 1.0, 10.0, 0.0]; assert_eq!(map_point(compose(translate, scale), 5.0, 0.0), (20.0, 0.0)); assert_eq!(map_point(compose(scale, translate), 5.0, 0.0), (30.0, 0.0)); } #[test] fn the_identity_bounding_box_is_the_rect_itself() { assert_eq!(bounding_box(IDENTITY, 10.0, 20.0, 30.0, 40.0), (10.0, 20.0, 30.0, 40.0)); } /// A quarter turn about the origin takes a 30x40 rect at (10, 20) to a 40x30 one: /// the bounding box swaps the two extents, and the corners land where the matrix /// puts them rather than where the rect started. #[test] fn a_right_angle_rotation_swaps_the_bounding_extents() { let quarter_turn = [0.0, 1.0, -1.0, 0.0, 0.0, 0.0]; let (x, y, w, h) = bounding_box(quarter_turn, 10.0, 20.0, 30.0, 40.0); assert_eq!((w, h), (40.0, 30.0)); assert_eq!((x, y), (-60.0, 10.0)); } }