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}