diff options
Diffstat (limited to 'crates/typst-library/src/layout/transform.rs')
| -rw-r--r-- | crates/typst-library/src/layout/transform.rs | 408 |
1 files changed, 408 insertions, 0 deletions
diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs new file mode 100644 index 00000000..183df609 --- /dev/null +++ b/crates/typst-library/src/layout/transform.rs @@ -0,0 +1,408 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain, +}; +use crate::layout::{ + Abs, Alignment, Angle, BlockElem, HAlignment, Length, Ratio, Rel, VAlignment, +}; + +/// Moves content without affecting layout. +/// +/// The `move` function allows you to move content while the layout still 'sees' +/// it at the original positions. Containers will still be sized as if the +/// content was not moved. +/// +/// # Example +/// ```example +/// #rect(inset: 0pt, move( +/// dx: 6pt, dy: 6pt, +/// rect( +/// inset: 8pt, +/// fill: white, +/// stroke: black, +/// [Abra cadabra] +/// ) +/// )) +/// ``` +#[elem(Show)] +pub struct MoveElem { + /// The horizontal displacement of the content. + pub dx: Rel<Length>, + + /// The vertical displacement of the content. + pub dy: Rel<Length>, + + /// The content to move. + #[required] + pub body: Content, +} + +impl Show for Packed<MoveElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_move) + .pack() + .spanned(self.span())) + } +} + +/// Rotates content without affecting layout. +/// +/// Rotates an element by a given angle. The layout will act as if the element +/// was not rotated unless you specify `{reflow: true}`. +/// +/// # Example +/// ```example +/// #stack( +/// dir: ltr, +/// spacing: 1fr, +/// ..range(16) +/// .map(i => rotate(24deg * i)[X]), +/// ) +/// ``` +#[elem(Show)] +pub struct RotateElem { + /// The amount of rotation. + /// + /// ```example + /// #rotate(-1.571rad)[Space!] + /// ``` + /// + #[positional] + pub angle: Angle, + + /// The origin of the rotation. + /// + /// If, for instance, you wanted the bottom left corner of the rotated + /// element to stay aligned with the baseline, you would set it to `bottom + + /// left` instead. + /// + /// ```example + /// #set text(spacing: 8pt) + /// #let square = square.with(width: 8pt) + /// + /// #box(square()) + /// #box(rotate(30deg, origin: center, square())) + /// #box(rotate(30deg, origin: top + left, square())) + /// #box(rotate(30deg, origin: bottom + right, square())) + /// ``` + #[fold] + #[default(HAlignment::Center + VAlignment::Horizon)] + pub origin: Alignment, + + /// Whether the rotation impacts the layout. + /// + /// If set to `{false}`, the rotated content will retain the bounding box of + /// the original content. If set to `{true}`, the bounding box will take the + /// rotation of the content into account and adjust the layout accordingly. + /// + /// ```example + /// Hello #rotate(90deg, reflow: true)[World]! + /// ``` + #[default(false)] + pub reflow: bool, + + /// The content to rotate. + #[required] + pub body: Content, +} + +impl Show for Packed<RotateElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rotate) + .pack() + .spanned(self.span())) + } +} + +/// Scales content without affecting layout. +/// +/// Lets you mirror content by specifying a negative scale on a single axis. +/// +/// # Example +/// ```example +/// #set align(center) +/// #scale(x: -100%)[This is mirrored.] +/// #scale(x: -100%, reflow: true)[This is mirrored.] +/// ``` +#[elem(Show)] +pub struct ScaleElem { + /// The scaling factor for both axes, as a positional argument. This is just + /// an optional shorthand notation for setting `x` and `y` to the same + /// value. + #[external] + #[positional] + #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))] + pub factor: Smart<ScaleAmount>, + + /// The horizontal scaling factor. + /// + /// The body will be mirrored horizontally if the parameter is negative. + #[parse( + let all = args.find()?; + args.named("x")?.or(all) + )] + #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))] + pub x: Smart<ScaleAmount>, + + /// The vertical scaling factor. + /// + /// The body will be mirrored vertically if the parameter is negative. + #[parse(args.named("y")?.or(all))] + #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))] + pub y: Smart<ScaleAmount>, + + /// The origin of the transformation. + /// + /// ```example + /// A#box(scale(75%)[A])A \ + /// B#box(scale(75%, origin: bottom + left)[B])B + /// ``` + #[fold] + #[default(HAlignment::Center + VAlignment::Horizon)] + pub origin: Alignment, + + /// Whether the scaling impacts the layout. + /// + /// If set to `{false}`, the scaled content will be allowed to overlap + /// other content. If set to `{true}`, it will compute the new size of + /// the scaled content and adjust the layout accordingly. + /// + /// ```example + /// Hello #scale(x: 20%, y: 40%, reflow: true)[World]! + /// ``` + #[default(false)] + pub reflow: bool, + + /// The content to scale. + #[required] + pub body: Content, +} + +impl Show for Packed<ScaleElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_scale) + .pack() + .spanned(self.span())) + } +} + +/// To what size something shall be scaled. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ScaleAmount { + Ratio(Ratio), + Length(Length), +} + +cast! { + ScaleAmount, + self => match self { + ScaleAmount::Ratio(ratio) => ratio.into_value(), + ScaleAmount::Length(length) => length.into_value(), + }, + ratio: Ratio => ScaleAmount::Ratio(ratio), + length: Length => ScaleAmount::Length(length), +} + +/// Skews content. +/// +/// Skews an element in horizontal and/or vertical direction. The layout will +/// act as if the element was not skewed unless you specify `{reflow: true}`. +/// +/// # Example +/// ```example +/// #skew(ax: -12deg)[ +/// This is some fake italic text. +/// ] +/// ``` +#[elem(Show)] +pub struct SkewElem { + /// The horizontal skewing angle. + /// + /// ```example + /// #skew(ax: 30deg)[Skewed] + /// ``` + /// + #[default(Angle::zero())] + pub ax: Angle, + + /// The vertical skewing angle. + /// + /// ```example + /// #skew(ay: 30deg)[Skewed] + /// ``` + /// + #[default(Angle::zero())] + pub ay: Angle, + + /// The origin of the skew transformation. + /// + /// The origin will stay fixed during the operation. + /// + /// ```example + /// X #box(skew(ax: -30deg, origin: center + horizon)[X]) X \ + /// X #box(skew(ax: -30deg, origin: bottom + left)[X]) X \ + /// X #box(skew(ax: -30deg, origin: top + right)[X]) X + /// ``` + #[fold] + #[default(HAlignment::Center + VAlignment::Horizon)] + pub origin: Alignment, + + /// Whether the skew transformation impacts the layout. + /// + /// If set to `{false}`, the skewed content will retain the bounding box of + /// the original content. If set to `{true}`, the bounding box will take the + /// transformation of the content into account and adjust the layout accordingly. + /// + /// ```example + /// Hello #skew(ay: 30deg, reflow: true, "World")! + /// ``` + #[default(false)] + pub reflow: bool, + + /// The content to skew. + #[required] + pub body: Content, +} + +impl Show for Packed<SkewElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_skew) + .pack() + .spanned(self.span())) + } +} + +/// A scale-skew-translate transformation. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Transform { + pub sx: Ratio, + pub ky: Ratio, + pub kx: Ratio, + pub sy: Ratio, + pub tx: Abs, + pub ty: Abs, +} + +impl Transform { + /// The identity transformation. + pub const fn identity() -> Self { + Self { + sx: Ratio::one(), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: Ratio::one(), + tx: Abs::zero(), + ty: Abs::zero(), + } + } + + /// A translate transform. + pub const fn translate(tx: Abs, ty: Abs) -> Self { + Self { tx, ty, ..Self::identity() } + } + + /// A scale transform. + pub const fn scale(sx: Ratio, sy: Ratio) -> Self { + Self { sx, sy, ..Self::identity() } + } + + /// A rotate transform. + pub fn rotate(angle: Angle) -> Self { + let cos = Ratio::new(angle.cos()); + let sin = Ratio::new(angle.sin()); + Self { + sx: cos, + ky: sin, + kx: -sin, + sy: cos, + ..Self::default() + } + } + + /// A skew transform. + pub fn skew(ax: Angle, ay: Angle) -> Self { + Self { + kx: Ratio::new(ax.tan()), + ky: Ratio::new(ay.tan()), + ..Self::identity() + } + } + + /// Whether this is the identity transformation. + pub fn is_identity(self) -> bool { + self == Self::identity() + } + + /// Pre-concatenate another transformation. + pub fn pre_concat(self, prev: Self) -> Self { + Transform { + sx: self.sx * prev.sx + self.kx * prev.ky, + ky: self.ky * prev.sx + self.sy * prev.ky, + kx: self.sx * prev.kx + self.kx * prev.sy, + sy: self.ky * prev.kx + self.sy * prev.sy, + tx: self.sx.of(prev.tx) + self.kx.of(prev.ty) + self.tx, + ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty, + } + } + + /// Post-concatenate another transformation. + pub fn post_concat(self, next: Self) -> Self { + next.pre_concat(self) + } + + /// Inverts the transformation. + /// + /// Returns `None` if the determinant of the matrix is zero. + pub fn invert(self) -> Option<Self> { + // Allow the trivial case to be inlined. + if self.is_identity() { + return Some(self); + } + + // Fast path for scale-translate-only transforms. + if self.kx.is_zero() && self.ky.is_zero() { + if self.sx.is_zero() || self.sy.is_zero() { + return Some(Self::translate(-self.tx, -self.ty)); + } + + let inv_x = 1.0 / self.sx; + let inv_y = 1.0 / self.sy; + return Some(Self { + sx: Ratio::new(inv_x), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: Ratio::new(inv_y), + tx: -self.tx * inv_x, + ty: -self.ty * inv_y, + }); + } + + let det = self.sx * self.sy - self.kx * self.ky; + if det.get().abs() < 1e-12 { + return None; + } + + let inv_det = 1.0 / det; + Some(Self { + sx: (self.sy * inv_det), + ky: (-self.ky * inv_det), + kx: (-self.kx * inv_det), + sy: (self.sx * inv_det), + tx: Abs::pt( + (self.kx.get() * self.ty.to_pt() - self.sy.get() * self.tx.to_pt()) + * inv_det, + ), + ty: Abs::pt( + (self.ky.get() * self.tx.to_pt() - self.sx.get() * self.ty.to_pt()) + * inv_det, + ), + }) + } +} + +impl Default for Transform { + fn default() -> Self { + Self::identity() + } +} |
