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
11/// ```example
12/// #rect(inset: 0pt, move(
13///   dx: 6pt, dy: 6pt,
14///   rect(
15///     inset: 8pt,
16///     fill: white,
17///     stroke: black,
18///     [Abra cadabra]
19///   )
20/// ))
21/// ```
22///
23/// # 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
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 `bottom +
69    /// 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
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
151    /// other content. If set to `{true}`, it will compute the new size of
152    /// the scaled 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
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 accordingly.
229    ///
230    /// ```example
231    /// Hello #skew(ay: 30deg, reflow: true, "World")!
232    /// ```
233    #[default(false)]
234    pub reflow: bool,
235
236    /// The content to skew.
237    #[required]
238    pub body: Content,
239}
240
241/// A scale-skew-translate transformation.
242#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
243pub struct Transform {
244    pub sx: Ratio,
245    pub ky: Ratio,
246    pub kx: Ratio,
247    pub sy: Ratio,
248    pub tx: Abs,
249    pub ty: Abs,
250}
251
252impl Transform {
253    /// The identity transformation.
254    pub const fn identity() -> Self {
255        Self {
256            sx: Ratio::one(),
257            ky: Ratio::zero(),
258            kx: Ratio::zero(),
259            sy: Ratio::one(),
260            tx: Abs::zero(),
261            ty: Abs::zero(),
262        }
263    }
264
265    /// A translate transform.
266    pub const fn translate(tx: Abs, ty: Abs) -> Self {
267        Self { tx, ty, ..Self::identity() }
268    }
269
270    /// A scale transform.
271    pub const fn scale(sx: Ratio, sy: Ratio) -> Self {
272        Self { sx, sy, ..Self::identity() }
273    }
274
275    /// A scale transform at a specific position.
276    pub fn scale_at(sx: Ratio, sy: Ratio, px: Abs, py: Abs) -> Self {
277        Self::translate(px, py)
278            .pre_concat(Self::scale(sx, sy))
279            .pre_concat(Self::translate(-px, -py))
280    }
281
282    /// A rotate transform at a specific position.
283    pub fn rotate_at(angle: Angle, px: Abs, py: Abs) -> Self {
284        Self::translate(px, py)
285            .pre_concat(Self::rotate(angle))
286            .pre_concat(Self::translate(-px, -py))
287    }
288
289    /// A rotate transform.
290    pub fn rotate(angle: Angle) -> Self {
291        let cos = Ratio::new(angle.cos());
292        let sin = Ratio::new(angle.sin());
293        Self {
294            sx: cos,
295            ky: sin,
296            kx: -sin,
297            sy: cos,
298            ..Self::default()
299        }
300    }
301
302    /// A skew transform.
303    pub fn skew(ax: Angle, ay: Angle) -> Self {
304        Self {
305            kx: Ratio::new(ax.tan()),
306            ky: Ratio::new(ay.tan()),
307            ..Self::identity()
308        }
309    }
310
311    /// Whether this is the identity transformation.
312    pub fn is_identity(self) -> bool {
313        self == Self::identity()
314    }
315
316    /// Pre-concatenate another transformation.
317    pub fn pre_concat(self, prev: Self) -> Self {
318        Transform {
319            sx: self.sx * prev.sx + self.kx * prev.ky,
320            ky: self.ky * prev.sx + self.sy * prev.ky,
321            kx: self.sx * prev.kx + self.kx * prev.sy,
322            sy: self.ky * prev.kx + self.sy * prev.sy,
323            tx: self.sx.of(prev.tx) + self.kx.of(prev.ty) + self.tx,
324            ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty,
325        }
326    }
327
328    /// Post-concatenate another transformation.
329    pub fn post_concat(self, next: Self) -> Self {
330        next.pre_concat(self)
331    }
332
333    /// Inverts the transformation.
334    ///
335    /// Returns `None` if the determinant of the matrix is zero.
336    pub fn invert(self) -> Option<Self> {
337        // Allow the trivial case to be inlined.
338        if self.is_identity() {
339            return Some(self);
340        }
341
342        // Fast path for scale-translate-only transforms.
343        if self.kx.is_zero() && self.ky.is_zero() {
344            if self.sx.is_zero() || self.sy.is_zero() {
345                return Some(Self::translate(-self.tx, -self.ty));
346            }
347
348            let inv_x = 1.0 / self.sx;
349            let inv_y = 1.0 / self.sy;
350            return Some(Self {
351                sx: Ratio::new(inv_x),
352                ky: Ratio::zero(),
353                kx: Ratio::zero(),
354                sy: Ratio::new(inv_y),
355                tx: -self.tx * inv_x,
356                ty: -self.ty * inv_y,
357            });
358        }
359
360        let det = self.sx * self.sy - self.kx * self.ky;
361        if det.get().abs() < 1e-12 {
362            return None;
363        }
364
365        let inv_det = 1.0 / det;
366        Some(Self {
367            sx: (self.sy * inv_det),
368            ky: (-self.ky * inv_det),
369            kx: (-self.kx * inv_det),
370            sy: (self.sx * inv_det),
371            tx: Abs::pt(
372                (self.kx.get() * self.ty.to_pt() - self.sy.get() * self.tx.to_pt())
373                    * inv_det,
374            ),
375            ty: Abs::pt(
376                (self.ky.get() * self.tx.to_pt() - self.sx.get() * self.ty.to_pt())
377                    * inv_det,
378            ),
379        })
380    }
381}
382
383impl Default for Transform {
384    fn default() -> Self {
385        Self::identity()
386    }
387}