diff --git a/crates/typst/src/layout/mod.rs b/crates/typst/src/layout/mod.rs index 8f3dc7c56..e75bfc5ed 100644 --- a/crates/typst/src/layout/mod.rs +++ b/crates/typst/src/layout/mod.rs @@ -104,6 +104,7 @@ pub fn define(global: &mut Scope) { global.define_elem::(); global.define_elem::(); global.define_elem::(); + global.define_elem::(); global.define_elem::(); global.define_func::(); global.define_func::(); diff --git a/crates/typst/src/layout/transform.rs b/crates/typst/src/layout/transform.rs index 2c718193f..d4010a9ed 100644 --- a/crates/typst/src/layout/transform.rs +++ b/crates/typst/src/layout/transform.rs @@ -335,6 +335,106 @@ cast! { 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 { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + Ok(BlockElem::single_layouter(self.clone(), layout_skew) + .pack() + .spanned(self.span())) + } +} + +/// Layout the skewed content. +#[typst_macros::time(span = elem.span())] +fn layout_skew( + elem: &Packed, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult { + let ax = elem.ax(styles); + let ay = elem.ay(styles); + let align = elem.origin(styles).resolve(styles); + + // Compute the new region's approximate size. + let size = if region.size.is_finite() { + compute_bounding_box(region.size, Transform::skew(ax, ay)).1 + } else { + Size::splat(Abs::inf()) + }; + + measure_and_layout( + engine, + locator, + region, + size, + styles, + elem.body(), + Transform::skew(ax, ay), + align, + elem.reflow(styles), + ) +} + /// A scale-skew-translate transformation. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Transform { @@ -382,6 +482,15 @@ impl Transform { } } + /// 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() diff --git a/tests/ref/transform-skew-both-axes.png b/tests/ref/transform-skew-both-axes.png new file mode 100644 index 000000000..da9cf5ebd Binary files /dev/null and b/tests/ref/transform-skew-both-axes.png differ diff --git a/tests/ref/transform-skew-origin.png b/tests/ref/transform-skew-origin.png new file mode 100644 index 000000000..4f4c4b08c Binary files /dev/null and b/tests/ref/transform-skew-origin.png differ diff --git a/tests/ref/transform-skew-relative-sizing.png b/tests/ref/transform-skew-relative-sizing.png new file mode 100644 index 000000000..41496d171 Binary files /dev/null and b/tests/ref/transform-skew-relative-sizing.png differ diff --git a/tests/ref/transform-skew.png b/tests/ref/transform-skew.png new file mode 100644 index 000000000..76bee82a0 Binary files /dev/null and b/tests/ref/transform-skew.png differ diff --git a/tests/suite/layout/transform.typ b/tests/suite/layout/transform.typ index 3604b72f7..fde5edfd9 100644 --- a/tests/suite/layout/transform.typ +++ b/tests/suite/layout/transform.typ @@ -115,3 +115,53 @@ Hello #scaled[World]! #scale(x: auto, y: 50pt, reflow: true, cylinder) #scale(x: 100pt, y: auto, reflow: true, cylinder) #scale(x: 150%, y: auto, reflow: true, cylinder) + +--- transform-skew --- +// Test skewing along one axis. +#set page(width: 100pt, height: 60pt) +#set text(size: 12pt) +#let skewed(body) = box(skew(ax: -30deg, body)) + +#set skew(reflow: false) +Hello #skewed[World]! + +#set skew(reflow: true) +Hello #skewed[World]! + +--- transform-skew-both-axes --- +// Test skewing along both axes. +#set page(width: 100pt, height: 250pt) +#set text(size: 12pt) +#let skewed(angle) = box(skew(ax: 30deg, ay: angle)[Some Text]) + +#set skew(reflow: true) +#for angle in range(-30, 31, step: 10) { + skewed(angle * 1deg) +} + +--- transform-skew-origin --- +// Test setting skewing origin. +#set page(width: 100pt, height:40pt) +#set text(spacing: 20pt) +#let square = square.with(width: 8pt) +#let skew-square(origin) = box(place(square(stroke: gray)) + + place(skew(ax: -30deg, ay: -30deg, origin: origin, square()))) +#skew-square(center+horizon) +#skew-square(bottom+left) +#skew-square(top+right) +#skew-square(horizon+right) + +--- transform-skew-relative-sizing --- +// Test relative sizing in skewed boxes. +#set page(width: 100pt, height: 60pt) +#set text(size: 12pt) +#let skewed(body) = box(skew( + ax: 30deg, + box(stroke: 0.5pt, width: 30%, clip: true, body) +)) + +#set skew(reflow: false) +Hello #skewed[World]!\ + +#set skew(reflow: true) +Hello #skewed[World]!