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