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}