Skip to main content

typst_library/layout/
transform.rs

1use crate::foundations::{Content, Smart, cast, elem};
2use crate::layout::{Abs, Alignment, Angle, HAlignment, Length, Ratio, Rel, VAlignment};
3
4/// Moves content without affecting layout.
5///
6/// The `move` function allows you to move content while the layout still 'sees'
7/// it at the original positions. Containers will still be sized as if the
8/// content was not moved.
9///
10/// = Example <example>
11/// ```example
12/// #rect(inset: 0pt, fill: gray, move(
13///   dx: 4pt, dy: 6pt,
14///   rect(
15///     inset: 8pt,
16///     fill: white,
17///     stroke: black,
18///     [Abra cadabra]
19///   )
20/// ))
21/// ```
22///
23/// = Accessibility <accessibility>
24/// Moving is transparent to Assistive Technology (AT). Your content will be
25/// read in the order it appears in the source, regardless of any visual
26/// movement. If you need to hide content from AT altogether in PDF export,
27/// consider using @pdf.artifact.
28#[elem]
29pub struct MoveElem {
30    /// The horizontal displacement of the content.
31    pub dx: Rel<Length>,
32
33    /// The vertical displacement of the content.
34    pub dy: Rel<Length>,
35
36    /// The content to move.
37    #[required]
38    pub body: Content,
39}
40
41/// Rotates content without affecting layout.
42///
43/// Rotates an element by a given angle. The layout will act as if the element
44/// was not rotated unless you specify `{reflow: true}`.
45///
46/// = Example <example>
47/// ```example
48/// #stack(
49///   dir: ltr,
50///   spacing: 1fr,
51///   ..range(16)
52///     .map(i => rotate(24deg * i)[X]),
53/// )
54/// ```
55#[elem]
56pub struct RotateElem {
57    /// The amount of rotation.
58    ///
59    /// ```example
60    /// #rotate(-1.571rad)[Space!]
61    /// ```
62    #[positional]
63    pub angle: Angle,
64
65    /// The origin of the rotation.
66    ///
67    /// If, for instance, you wanted the bottom left corner of the rotated
68    /// element to stay aligned with the baseline, you would set it to
69    /// `bottom + left` instead.
70    ///
71    /// ```example
72    /// #set text(spacing: 8pt)
73    /// #let square = square.with(width: 8pt)
74    ///
75    /// #box(square())
76    /// #box(rotate(30deg, origin: center, square()))
77    /// #box(rotate(30deg, origin: top + left, square()))
78    /// #box(rotate(30deg, origin: bottom + right, square()))
79    /// ```
80    #[fold]
81    #[default(HAlignment::Center + VAlignment::Horizon)]
82    pub origin: Alignment,
83
84    /// Whether the rotation impacts the layout.
85    ///
86    /// If set to `{false}`, the rotated content will retain the bounding box of
87    /// the original content. If set to `{true}`, the bounding box will take the
88    /// rotation of the content into account and adjust the layout accordingly.
89    ///
90    /// ```example
91    /// Hello #rotate(90deg, reflow: true)[World]!
92    /// ```
93    #[default(false)]
94    pub reflow: bool,
95
96    /// The content to rotate.
97    #[required]
98    pub body: Content,
99}
100
101/// Scales content without affecting layout.
102///
103/// Lets you mirror content by specifying a negative scale on a single axis.
104///
105/// = Example <example>
106/// ```example
107/// #set align(center)
108/// #scale(x: -100%)[This is mirrored.]
109/// #scale(x: -100%, reflow: true)[This is mirrored.]
110/// ```
111#[elem]
112pub struct ScaleElem {
113    /// The scaling factor for both axes, as a positional argument. This is just
114    /// an optional shorthand notation for setting `x` and `y` to the same
115    /// value.
116    #[external]
117    #[positional]
118    #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))]
119    pub factor: Smart<ScaleAmount>,
120
121    /// The horizontal scaling factor.
122    ///
123    /// The body will be mirrored horizontally if the parameter is negative.
124    #[parse(
125        let all = args.find()?;
126        args.named("x")?.or(all)
127    )]
128    #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))]
129    pub x: Smart<ScaleAmount>,
130
131    /// The vertical scaling factor.
132    ///
133    /// The body will be mirrored vertically if the parameter is negative.
134    #[parse(args.named("y")?.or(all))]
135    #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))]
136    pub y: Smart<ScaleAmount>,
137
138    /// The origin of the transformation.
139    ///
140    /// ```example
141    /// A#box(scale(75%)[A])A \
142    /// B#box(scale(75%, origin: bottom + left)[B])B
143    /// ```
144    #[fold]
145    #[default(HAlignment::Center + VAlignment::Horizon)]
146    pub origin: Alignment,
147
148    /// Whether the scaling impacts the layout.
149    ///
150    /// If set to `{false}`, the scaled content will be allowed to overlap other
151    /// content. If set to `{true}`, it will compute the new size of the scaled
152    /// content and adjust the layout accordingly.
153    ///
154    /// ```example
155    /// Hello #scale(x: 20%, y: 40%, reflow: true)[World]!
156    /// ```
157    #[default(false)]
158    pub reflow: bool,
159
160    /// The content to scale.
161    #[required]
162    pub body: Content,
163}
164
165/// To what size something shall be scaled.
166#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
167pub enum ScaleAmount {
168    Ratio(Ratio),
169    Length(Length),
170}
171
172cast! {
173    ScaleAmount,
174    self => match self {
175        ScaleAmount::Ratio(ratio) => ratio.into_value(),
176        ScaleAmount::Length(length) => length.into_value(),
177    },
178    ratio: Ratio => ScaleAmount::Ratio(ratio),
179    length: Length => ScaleAmount::Length(length),
180}
181
182/// Skews content.
183///
184/// Skews an element in horizontal and/or vertical direction. The layout will
185/// act as if the element was not skewed unless you specify `{reflow: true}`.
186///
187/// = Example <example>
188/// ```example
189/// #skew(ax: -12deg)[
190///   This is some fake italic text.
191/// ]
192/// ```
193#[elem]
194pub struct SkewElem {
195    /// The horizontal skewing angle.
196    ///
197    /// ```example
198    /// #skew(ax: 30deg)[Skewed]
199    /// ```
200    #[default(Angle::zero())]
201    pub ax: Angle,
202
203    /// The vertical skewing angle.
204    ///
205    /// ```example
206    /// #skew(ay: 30deg)[Skewed]
207    /// ```
208    #[default(Angle::zero())]
209    pub ay: Angle,
210
211    /// The origin of the skew transformation.
212    ///
213    /// The origin will stay fixed during the operation.
214    ///
215    /// ```example
216    /// X #box(skew(ax: -30deg, origin: center + horizon)[X]) X \
217    /// X #box(skew(ax: -30deg, origin: bottom + left)[X]) X \
218    /// X #box(skew(ax: -30deg, origin: top + right)[X]) X
219    /// ```
220    #[fold]
221    #[default(HAlignment::Center + VAlignment::Horizon)]
222    pub origin: Alignment,
223
224    /// Whether the skew transformation impacts the layout.
225    ///
226    /// If set to `{false}`, the skewed content will retain the bounding box of
227    /// the original content. If set to `{true}`, the bounding box will take the
228    /// transformation of the content into account and adjust the layout
229    /// accordingly.
230    ///
231    /// ```example
232    /// Hello #skew(ay: 30deg, reflow: true, "World")!
233    /// ```
234    #[default(false)]
235    pub reflow: bool,
236
237    /// The content to skew.
238    #[required]
239    pub body: Content,
240}
241
242/// A scale-skew-translate transformation.
243#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
244pub struct Transform {
245    pub sx: Ratio,
246    pub ky: Ratio,
247    pub kx: Ratio,
248    pub sy: Ratio,
249    pub tx: Abs,
250    pub ty: Abs,
251}
252
253impl Transform {
254    /// The identity transformation.
255    pub const fn identity() -> Self {
256        Self {
257            sx: Ratio::one(),
258            ky: Ratio::zero(),
259            kx: Ratio::zero(),
260            sy: Ratio::one(),
261            tx: Abs::zero(),
262            ty: Abs::zero(),
263        }
264    }
265
266    /// A translate transform.
267    pub const fn translate(tx: Abs, ty: Abs) -> Self {
268        Self { tx, ty, ..Self::identity() }
269    }
270
271    /// A scale transform.
272    pub const fn scale(sx: Ratio, sy: Ratio) -> Self {
273        Self { sx, sy, ..Self::identity() }
274    }
275
276    /// A scale transform at a specific position.
277    pub fn scale_at(sx: Ratio, sy: Ratio, px: Abs, py: Abs) -> Self {
278        Self::translate(px, py)
279            .pre_concat(Self::scale(sx, sy))
280            .pre_concat(Self::translate(-px, -py))
281    }
282
283    /// A rotate transform at a specific position.
284    pub fn rotate_at(angle: Angle, px: Abs, py: Abs) -> Self {
285        Self::translate(px, py)
286            .pre_concat(Self::rotate(angle))
287            .pre_concat(Self::translate(-px, -py))
288    }
289
290    /// A rotate transform.
291    pub fn rotate(angle: Angle) -> Self {
292        let cos = Ratio::new(angle.cos());
293        let sin = Ratio::new(angle.sin());
294        Self {
295            sx: cos,
296            ky: sin,
297            kx: -sin,
298            sy: cos,
299            ..Self::default()
300        }
301    }
302
303    /// A skew transform.
304    pub fn skew(ax: Angle, ay: Angle) -> Self {
305        Self {
306            kx: Ratio::new(ax.tan()),
307            ky: Ratio::new(ay.tan()),
308            ..Self::identity()
309        }
310    }
311
312    /// Whether this is the identity transformation.
313    pub fn is_identity(self) -> bool {
314        self == Self::identity()
315    }
316
317    /// Whether this transformation only scales along the X and Y axis.
318    pub fn is_only_scale(self) -> bool {
319        self.ky == Ratio::zero()
320            && self.kx == Ratio::zero()
321            && self.tx == Abs::zero()
322            && self.ty == Abs::zero()
323    }
324
325    /// Whether this transformation only translates along the X and Y axis.
326    pub fn is_only_translate(self) -> bool {
327        self.sx == Ratio::one()
328            && self.sy == Ratio::one()
329            && self.ky == Ratio::zero()
330            && self.kx == Ratio::zero()
331    }
332
333    /// Pre-concatenate another transformation.
334    pub fn pre_concat(self, prev: Self) -> Self {
335        Transform {
336            sx: self.sx * prev.sx + self.kx * prev.ky,
337            ky: self.ky * prev.sx + self.sy * prev.ky,
338            kx: self.sx * prev.kx + self.kx * prev.sy,
339            sy: self.ky * prev.kx + self.sy * prev.sy,
340            tx: self.sx.of(prev.tx) + self.kx.of(prev.ty) + self.tx,
341            ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty,
342        }
343    }
344
345    /// Post-concatenate another transformation.
346    pub fn post_concat(self, next: Self) -> Self {
347        next.pre_concat(self)
348    }
349
350    /// Inverts the transformation.
351    ///
352    /// Returns `None` if the determinant of the matrix is zero.
353    pub fn invert(self) -> Option<Self> {
354        // Allow the trivial case to be inlined.
355        if self.is_identity() {
356            return Some(self);
357        }
358
359        // Fast path for scale-translate-only transforms.
360        if self.kx.is_zero() && self.ky.is_zero() {
361            if self.sx.is_zero() || self.sy.is_zero() {
362                return Some(Self::translate(-self.tx, -self.ty));
363            }
364
365            let inv_x = 1.0 / self.sx;
366            let inv_y = 1.0 / self.sy;
367            return Some(Self {
368                sx: Ratio::new(inv_x),
369                ky: Ratio::zero(),
370                kx: Ratio::zero(),
371                sy: Ratio::new(inv_y),
372                tx: -self.tx * inv_x,
373                ty: -self.ty * inv_y,
374            });
375        }
376
377        let det = self.sx * self.sy - self.kx * self.ky;
378        if det.get().abs() < 1e-12 {
379            return None;
380        }
381
382        let inv_det = 1.0 / det;
383        Some(Self {
384            sx: (self.sy * inv_det),
385            ky: (-self.ky * inv_det),
386            kx: (-self.kx * inv_det),
387            sy: (self.sx * inv_det),
388            tx: Abs::pt(
389                (self.kx.get() * self.ty.to_pt() - self.sy.get() * self.tx.to_pt())
390                    * inv_det,
391            ),
392            ty: Abs::pt(
393                (self.ky.get() * self.tx.to_pt() - self.sx.get() * self.ty.to_pt())
394                    * inv_det,
395            ),
396        })
397    }
398}
399
400impl Default for Transform {
401    fn default() -> Self {
402        Self::identity()
403    }
404}