Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202//! 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<TransformFunction>, /// `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<TransformFunction>) -> 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)); }}